mirror of
https://github.com/MarginaliaSearch/MarginaliaSearch.git
synced 2025-10-05 21:22:39 +02:00
Compare commits
2469 Commits
v23.08.1
...
f1a71e9033
Author | SHA1 | Date | |
---|---|---|---|
|
f1a71e9033 | ||
|
7b525918c9 | ||
|
0f3aede66f | ||
|
88236f3836 | ||
|
ad31a22fbb | ||
|
2785ae8241 | ||
|
1ed1f2f299 | ||
|
b7d3b67a1d | ||
|
d28010b7e6 | ||
|
2689bd9eaa | ||
|
f6d5d7f196 | ||
|
abf1186fa7 | ||
|
94a77ebddf | ||
|
4e2f76a477 | ||
|
4cd1834938 | ||
|
5cbbea67ed | ||
|
b688f15550 | ||
|
f55af8ef48 | ||
|
adc815e282 | ||
|
ca8455e049 | ||
|
4ea724d2cb | ||
|
40600e7297 | ||
|
7795742538 | ||
|
82d33ce69b | ||
|
e49cc5c244 | ||
|
0af389ad93 | ||
|
48791f56bd | ||
|
be83726427 | ||
|
708caa8791 | ||
|
32394f42b9 | ||
|
b8e3445ce0 | ||
|
17a78a7b7e | ||
|
5a75dd8093 | ||
|
a9713347a0 | ||
|
4694d36ed2 | ||
|
70bdd1f51e | ||
|
187b4828e6 | ||
|
93fc14dc94 | ||
|
fbfea8539b | ||
|
0929d77247 | ||
|
db8f8c1f55 | ||
|
dcb2723386 | ||
|
00c1f495f6 | ||
|
73a923983a | ||
|
e9ed0c5669 | ||
|
5b2bec6144 | ||
|
f26bb8e2b1 | ||
|
4455495dc6 | ||
|
b84d17aa51 | ||
|
9d008390ae | ||
|
a40c2a8146 | ||
|
a3416bf48e | ||
|
ee2461d9fc | ||
|
54c91a84e3 | ||
|
a6371fc54c | ||
|
8faa9a572d | ||
|
fdce940263 | ||
|
af8a13a7fb | ||
|
9e332de6b4 | ||
|
d457bb5d44 | ||
|
c661ebb619 | ||
|
53e744398a | ||
|
1d71baf3e5 | ||
|
bb5fc0f348 | ||
|
c8f112d040 | ||
|
ae31bc8498 | ||
|
da5046c3bf | ||
|
f67257baf2 | ||
|
924fb05661 | ||
|
c231a82062 | ||
|
2c1082d7f0 | ||
|
06947bd026 | ||
|
519aebd7c6 | ||
|
42cc27586e | ||
|
360881fafd | ||
|
4c6fdf6ebe | ||
|
554de21f68 | ||
|
00194acbfe | ||
|
97dabcefaa | ||
|
cc790644d4 | ||
|
8f893ee6c0 | ||
|
938721b793 | ||
|
f68bcefc75 | ||
|
164a646af6 | ||
|
0cfd759f85 | ||
|
b53002200c | ||
|
78246b9a63 | ||
|
b552e79927 | ||
|
bffc159486 | ||
|
b8000721bd | ||
|
2ee0b0e420 | ||
|
1432fc87d7 | ||
|
ec5f32b1d8 | ||
|
edd453531e | ||
|
096496ada1 | ||
|
8ca6209260 | ||
|
673c65d3c9 | ||
|
acb9ec7b15 | ||
|
47079e05db | ||
|
c93056e77f | ||
|
6f7530e807 | ||
|
87ce4a1b52 | ||
|
52194cbe7a | ||
|
fd1ac03c78 | ||
|
5e5b86efb4 | ||
|
f332ec6191 | ||
|
c25c1af437 | ||
|
eb0c911b45 | ||
|
1979870ce4 | ||
|
0ba2ea38e1 | ||
|
d6cfbceeea | ||
|
e369d200cc | ||
|
946d64c8da | ||
|
42f043a60f | ||
|
b46f2e1407 | ||
|
18aa1b9764 | ||
|
2f3950e0d5 | ||
|
61d803869e | ||
|
df6434d177 | ||
|
59519ed7c4 | ||
|
874fc2d250 | ||
|
69e8ec0eef | ||
|
a7eb5f54e6 | ||
|
b29ba3e228 | ||
|
5fa5029c60 | ||
|
4257f60f00 | ||
|
ce221d3a0e | ||
|
f0741142a3 | ||
|
0899e4d895 | ||
|
bbf7c5a1cb | ||
|
686a40e69b | ||
|
8af254f44f | ||
|
2c21bd9287 | ||
|
f9645e2f00 | ||
|
81e311b558 | ||
|
507c09146a | ||
|
f682425594 | ||
|
de67006c4f | ||
|
eea32bb7b4 | ||
|
e976940a4e | ||
|
b564b33028 | ||
|
1cca16a58e | ||
|
70b4ed6d81 | ||
|
45dc6412c1 | ||
|
b3b95edcb5 | ||
|
338d300e1a | ||
|
fa685bf1f4 | ||
|
d79a3e2b2a | ||
|
854382b2be | ||
|
8710adbc2a | ||
|
acdf7b4785 | ||
|
b5d27c1406 | ||
|
55eb7dc116 | ||
|
f0e8bc8baf | ||
|
91a6ad2337 | ||
|
9a182b9ddb | ||
|
fefbcf15ce | ||
|
9a789bf62d | ||
|
0525303b68 | ||
|
6953d65de5 | ||
|
a7a18ced2e | ||
|
7c94c941b2 | ||
|
ea99b62356 | ||
|
3dc21d34d8 | ||
|
51912e0176 | ||
|
de1b4d5372 | ||
|
50ac926060 | ||
|
d711ee75b5 | ||
|
291ff0c4de | ||
|
2fd2710355 | ||
|
e3b957063d | ||
|
aee262e5f6 | ||
|
4a98a3c711 | ||
|
68f52ca350 | ||
|
2a2d951c2f | ||
|
379a1be074 | ||
|
827aadafcd | ||
|
aa7679d6ce | ||
|
6fe6de766d | ||
|
4245ac4c07 | ||
|
1c49a0f5ad | ||
|
9a6e5f646d | ||
|
fa92994a31 | ||
|
bc49406881 | ||
|
90325be447 | ||
|
dc89587af3 | ||
|
7b552afd6b | ||
|
73557edc67 | ||
|
83919e448a | ||
|
6f5b75b84d | ||
|
db315e2813 | ||
|
e9977e08b7 | ||
|
1df3757e5f | ||
|
ca283f9684 | ||
|
85360e61b2 | ||
|
e2ccff21bc | ||
|
c5b5b0c699 | ||
|
9a65946e22 | ||
|
1d2ab21e27 | ||
|
0610cc19ad | ||
|
a676306a7f | ||
|
8d68cd14fb | ||
|
4773c5a52b | ||
|
74bd562ae4 | ||
|
c9751287b0 | ||
|
5da24e3fc4 | ||
|
20a4e86eec | ||
|
477a184948 | ||
|
8940ce99db | ||
|
0ac0fa4dca | ||
|
942f15ef14 | ||
|
f668f33d5b | ||
|
6789975cd2 | ||
|
c3ba608776 | ||
|
733d2687fe | ||
|
f6daac8ed0 | ||
|
c2eeee4a06 | ||
|
3b0c701df4 | ||
|
c6fb2db43b | ||
|
9bc8fe05ae | ||
|
440ffcf6f8 | ||
|
b07709cc72 | ||
|
9a6acdcbe0 | ||
|
23b9b0bf1b | ||
|
749c8ed954 | ||
|
9f4b6939ca | ||
|
1d08e44e8d | ||
|
fc2e156e78 | ||
|
5e68a89e9f | ||
|
d380661307 | ||
|
cccdf5c329 | ||
|
f085b4ea12 | ||
|
e208f7d3ba | ||
|
b577085cb2 | ||
|
b9240476f6 | ||
|
8f50f86d0b | ||
|
e3b7ead7a9 | ||
|
9a845ba604 | ||
|
b9381f1603 | ||
|
6a60127267 | ||
|
e8ffcfbb19 | ||
|
caf0850f81 | ||
|
62e3bb675e | ||
|
4dc3e7da7a | ||
|
92b09883ec | ||
|
87082b4ef8 | ||
|
84d3f6087f | ||
|
f93ba371a5 | ||
|
5eec27c68d | ||
|
ab01576f91 | ||
|
054e5ccf44 | ||
|
4351ea5128 | ||
|
49cfa3a5e9 | ||
|
683854b23f | ||
|
e880fa8945 | ||
|
2482dc572e | ||
|
4589f11898 | ||
|
e43b6e610b | ||
|
4772117a1f | ||
|
3fc7ea521c | ||
|
4372f5af03 | ||
|
4ad89b6c75 | ||
|
ad0519e031 | ||
|
596ece1230 | ||
|
07b6e1585b | ||
|
cb5e2778eb | ||
|
8f5ea7896c | ||
|
76c398e0b1 | ||
|
4a94f04a8d | ||
|
df72f670d4 | ||
|
eaa22c2f5a | ||
|
7be173aeca | ||
|
36685bdca7 | ||
|
ad04057609 | ||
|
eb76ae22e2 | ||
|
4b858ab341 | ||
|
c6e3c8aa3b | ||
|
9128d3907c | ||
|
4ef16d13d4 | ||
|
838a5626ec | ||
|
6b426209c7 | ||
|
452b5731d9 | ||
|
c91cf49630 | ||
|
8503030f18 | ||
|
744f7d3ef7 | ||
|
215e12afe9 | ||
|
2716bce918 | ||
|
caf2e6fbb7 | ||
|
233f0acfb1 | ||
|
e3a4ff02e9 | ||
|
c786283ae1 | ||
|
a3f65ac0e0 | ||
|
aba1a32af0 | ||
|
c9c442345b | ||
|
2e126ba30e | ||
|
2087985f49 | ||
|
2b13ebd18b | ||
|
6d92c125fe | ||
|
f638cfa39a | ||
|
89447c12af | ||
|
c71fc46f04 | ||
|
f96874d828 | ||
|
583a84d5a0 | ||
|
f65b946448 | ||
|
3682815855 | ||
|
3a94357660 | ||
|
673b0d3de1 | ||
|
ea942bc664 | ||
|
7ed5083c54 | ||
|
08bb2c097b | ||
|
495fb325be | ||
|
05c25bbaec | ||
|
2a028b84f3 | ||
|
a091a23623 | ||
|
e8897acb45 | ||
|
b89ffcf2be | ||
|
dbcc9055b0 | ||
|
d9740557f4 | ||
|
0d6cd015fd | ||
|
c6034efcc8 | ||
|
76068014ad | ||
|
1c3ed67127 | ||
|
fc0cb6bd9a | ||
|
c2601bac78 | ||
|
f5641b72e9 | ||
|
36efe2e219 | ||
|
983fe3829e | ||
|
668c87aa86 | ||
|
9d3f9adb05 | ||
|
a43a1773f1 | ||
|
1e7a3a3c4f | ||
|
62b696b1c3 | ||
|
f1a900f383 | ||
|
700364b86d | ||
|
7e725ddaed | ||
|
120209e138 | ||
|
a771a5b6ce | ||
|
dac5b54128 | ||
|
6cfb143c15 | ||
|
23c818281b | ||
|
8aad253cf6 | ||
|
556d7af9dc | ||
|
b7a5219ed3 | ||
|
a23ec521fe | ||
|
fff3babc6d | ||
|
b2bfb8217c | ||
|
3b2ac414dc | ||
|
0ba6515a01 | ||
|
16c6b0f151 | ||
|
e998692900 | ||
|
eeb1695a87 | ||
|
a0ab910940 | ||
|
b9f31048d7 | ||
|
12c304289a | ||
|
6ee01dabea | ||
|
1b80e282a7 | ||
|
a65d18f1d1 | ||
|
90a1ff220b | ||
|
d6c7092335 | ||
|
b716333856 | ||
|
b504b8482c | ||
|
80da1e9ad1 | ||
|
d3f744a441 | ||
|
60fb539875 | ||
|
7f5094fedf | ||
|
45066636a5 | ||
|
e2d6898c51 | ||
|
58ef767b94 | ||
|
f9f268c67a | ||
|
f44c2bdee9 | ||
|
6fdf477c18 | ||
|
6b6e455e3f | ||
|
a3a126540c | ||
|
842b19da40 | ||
|
2a30e93bf0 | ||
|
3d998f12c0 | ||
|
cbccc2ac23 | ||
|
2cfc23f9b7 | ||
|
88fe394cdb | ||
|
f30fcebd4f | ||
|
5d885927b4 | ||
|
7622c8358e | ||
|
69ed9aef47 | ||
|
4c78c223da | ||
|
71b9935dd6 | ||
|
ad38f2fd83 | ||
|
9c47388846 | ||
|
d9ab10e33f | ||
|
e13ea7f42b | ||
|
f38daeb036 | ||
|
6e214293e5 | ||
|
52582a6d7d | ||
|
ec0e39ad32 | ||
|
6a15aee4b0 | ||
|
bd5111e8a2 | ||
|
1ecbeb0272 | ||
|
b91354925d | ||
|
3f85c9c154 | ||
|
390f053406 | ||
|
89e03d6914 | ||
|
14e0bc9f26 | ||
|
7065b46c6f | ||
|
0372190c90 | ||
|
ceaf32fb90 | ||
|
b03c43224c | ||
|
9b4ce9e9eb | ||
|
81ac02a695 | ||
|
47f624fb3b | ||
|
b57db01415 | ||
|
ce7d522608 | ||
|
18649b6ee9 | ||
|
f6417aef1a | ||
|
2aa7e376b0 | ||
|
f33bc44860 | ||
|
a2826efd44 | ||
|
c866f19cbb | ||
|
518278493b | ||
|
1ac0bab0b8 | ||
|
08b45ed10a | ||
|
f2cfb91973 | ||
|
2f79524eb3 | ||
|
3b00142c96 | ||
|
294ab19177 | ||
|
6f1659ecb2 | ||
|
982dcb28f0 | ||
|
fc686d8b2e | ||
|
69ef0f334a | ||
|
446746f3bd | ||
|
24ab8398bb | ||
|
d2ceeff4cf | ||
|
cf64214b1c | ||
|
e50d09cc01 | ||
|
bce3892ce0 | ||
|
36581b25c2 | ||
|
52ff7fb4dd | ||
|
a4e49e658a | ||
|
e2c56dc3ca | ||
|
470b866008 | ||
|
4895a2ac7a | ||
|
fd32ae9fa7 | ||
|
470651ea4c | ||
|
8d4829e783 | ||
|
1290bc15dc | ||
|
e7fa558954 | ||
|
720685bf3f | ||
|
cbec63c7da | ||
|
b03ca75785 | ||
|
184aedc071 | ||
|
0275bad281 | ||
|
fd83a9d0b8 | ||
|
d556f8ae3a | ||
|
e37559837b | ||
|
3564c4aaee | ||
|
92c54563ab | ||
|
d7a5d90b07 | ||
|
0a0e88fd6e | ||
|
b4fc0c4368 | ||
|
87ee8765b8 | ||
|
1adf4835fa | ||
|
b7b5d0bf46 | ||
|
416059adde | ||
|
db7930016a | ||
|
82456ad673 | ||
|
0882a6d9cd | ||
|
5020029c2d | ||
|
ac44d0b093 | ||
|
4b32b9b10e | ||
|
9f041d6631 | ||
|
13fb1efce4 | ||
|
c1225165b7 | ||
|
67ad7a3bbc | ||
|
ed62ec8a35 | ||
|
42b24cfa34 | ||
|
1ffaab2da6 | ||
|
5f93c7f767 | ||
|
4001c68c82 | ||
|
6b811489c5 | ||
|
e9d317c65d | ||
|
16b05a4737 | ||
|
021cd73cbb | ||
|
4253bd53b5 | ||
|
14c87461a5 | ||
|
9afed0a18e | ||
|
afad4deb94 | ||
|
f071c947e4 | ||
|
79996c9348 | ||
|
db907ab06a | ||
|
c49cd9dd95 | ||
|
eec9df3b0a | ||
|
e5f3288de6 | ||
|
d587544d3a | ||
|
1a9ae1bc40 | ||
|
e0c81e956a | ||
|
542fb12b38 | ||
|
65ec734566 | ||
|
10b6a25c63 | ||
|
6260f6bec7 | ||
|
d6d5467696 | ||
|
034560ca75 | ||
|
e994fddae4 | ||
|
345f01f306 | ||
|
5a8e286689 | ||
|
39a055aa94 | ||
|
37aaa90dc9 | ||
|
24022c5adc | ||
|
1de9ecc0b6 | ||
|
9b80245ea0 | ||
|
4e1595c1a6 | ||
|
0be8585fa5 | ||
|
a0fe070fe7 | ||
|
abe9da0fc6 | ||
|
56d0128b0a | ||
|
840b68ac55 | ||
|
c34ff6d6c3 | ||
|
32780967d8 | ||
|
7330bc489d | ||
|
ea23f33738 | ||
|
4a8a028118 | ||
|
a25bc647be | ||
|
a720dba3a2 | ||
|
284f382867 | ||
|
a80717f138 | ||
|
d6da715fa4 | ||
|
c1ec7aa491 | ||
|
3daf37e283 | ||
|
44a774d3a8 | ||
|
597aeaf496 | ||
|
06df7892c2 | ||
|
dc26854268 | ||
|
9f16326cba | ||
|
ed66d0b3a7 | ||
|
c3afc82dad | ||
|
08e25e539e | ||
|
4946044dd0 | ||
|
edf382e1c5 | ||
|
644cba32e4 | ||
|
34b76390b2 | ||
|
43cd507971 | ||
|
cc40e99fdc | ||
|
8a944cf4c6 | ||
|
1c128e6d82 | ||
|
be039d1a8c | ||
|
4edc0d3267 | ||
|
890f521d0d | ||
|
b1814a30f7 | ||
|
f59a9eb025 | ||
|
599534806b | ||
|
7e8253dac7 | ||
|
97a6780ea3 | ||
|
eb634beec8 | ||
|
269ebd1654 | ||
|
39ce40bfeb | ||
|
c187b2e1c1 | ||
|
42eaa4588b | ||
|
4f40a5fbeb | ||
|
3f3d42bc01 | ||
|
61c8d53e1b | ||
|
a7a3d85be9 | ||
|
306232fb54 | ||
|
5aef844f0d | ||
|
d56b5c828a | ||
|
ab58a4636f | ||
|
00be269238 | ||
|
879e6a9424 | ||
|
fba3455732 | ||
|
14283da7f5 | ||
|
93df4d1fc0 | ||
|
b12a0b998c | ||
|
3b6f4e321b | ||
|
8428111771 | ||
|
e9fd4415ef | ||
|
4c95c3dcad | ||
|
c5281536fb | ||
|
4431dae7ac | ||
|
4df4d0a7a8 | ||
|
9f05083b94 | ||
|
fc92e9b9c0 | ||
|
328fb5d927 | ||
|
36889950e8 | ||
|
c96a94878b | ||
|
1c57d7d73a | ||
|
a443d22356 | ||
|
aa59d4afa4 | ||
|
df0f18d0e7 | ||
|
0819d46f97 | ||
|
5e2b63473e | ||
|
f9590703f1 | ||
|
f12fc11337 | ||
|
c309030184 | ||
|
fd5af01629 | ||
|
d4c43c7a79 | ||
|
18700e1919 | ||
|
120b431998 | ||
|
71dad99326 | ||
|
c1e8afdf86 | ||
|
fa32dddc24 | ||
|
a266fcbf30 | ||
|
6e47e58e0e | ||
|
9dc43d8b4a | ||
|
83967e3305 | ||
|
4db980a291 | ||
|
089b177868 | ||
|
9c8e9a68d5 | ||
|
413d5cc788 | ||
|
58539b92ac | ||
|
fe72f16df1 | ||
|
b49a244a2e | ||
|
3f0b4c010f | ||
|
c6e0cd93f7 | ||
|
80a7ccb080 | ||
|
54dec347c4 | ||
|
d6ee3f0785 | ||
|
8be88afcf3 | ||
|
0e3c00d3e1 | ||
|
4279a7f1aa | ||
|
251006d4f9 | ||
|
c3e99dc12a | ||
|
aaaa2de022 | ||
|
fc1388422a | ||
|
b07080db16 | ||
|
e9d86dca4a | ||
|
1d693f0efa | ||
|
5874a163dc | ||
|
5ec7a1deab | ||
|
7fea2808ed | ||
|
8da74484f0 | ||
|
923d5a7234 | ||
|
58f88749b8 | ||
|
77f727a5ba | ||
|
667cfb53dc | ||
|
fe36d4ed20 | ||
|
acf4bef98d | ||
|
2a737c34bb | ||
|
90a577af82 | ||
|
f0c9b935d8 | ||
|
7b5493dd51 | ||
|
c246a59158 | ||
|
0b99781d24 | ||
|
39db9620c1 | ||
|
1781599363 | ||
|
6b2d18fb9b | ||
|
59b1d200ab | ||
|
897010a2cf | ||
|
602af7a77e | ||
|
a7d91c8527 | ||
|
7151602124 | ||
|
884e33bd4a | ||
|
e84d5c497a | ||
|
2d2d3e2466 | ||
|
647dd9b12f | ||
|
de4e2849ce | ||
|
3c43f1954e | ||
|
fa2462ec39 | ||
|
f4ad7145db | ||
|
068b450180 | ||
|
05b909a21f | ||
|
3d179cddce | ||
|
1a2aae496a | ||
|
353cdffb3f | ||
|
2e3f1313c7 | ||
|
58e6f141ce | ||
|
500f63e921 | ||
|
6dfbedda1e | ||
|
9715ddb105 | ||
|
1fc6313a77 | ||
|
b1249d5b8a | ||
|
ef95d59b07 | ||
|
acdd8664f5 | ||
|
6b12eac58a | ||
|
bb3f1f395a | ||
|
b661beef41 | ||
|
9888c47f19 | ||
|
dcef7e955b | ||
|
b3973a1dd7 | ||
|
8bd05d6d90 | ||
|
59df8e356e | ||
|
7161162a35 | ||
|
d7c4c5141f | ||
|
88e9b8fb05 | ||
|
b6265cee11 | ||
|
c91af247e9 | ||
|
7a31227de1 | ||
|
4f477604c5 | ||
|
2970f4395b | ||
|
d1ec909b36 | ||
|
c67c5bbf42 | ||
|
ecb0e57a1a | ||
|
8c61f61b46 | ||
|
662a18c933 | ||
|
1c2426a052 | ||
|
34df7441ac | ||
|
5387e2bd80 | ||
|
0f3b24d0f8 | ||
|
a732095d2a | ||
|
6607f0112f | ||
|
4913730de9 | ||
|
1db64f9d56 | ||
|
4dcff14498 | ||
|
426658f64e | ||
|
2181b22f05 | ||
|
42bd79a609 | ||
|
b91c1e528a | ||
|
b1130d7a04 | ||
|
8364bcdc97 | ||
|
626cab5fab | ||
|
cfd4712191 | ||
|
9f18ced73d | ||
|
18e91269ab | ||
|
e315ca5758 | ||
|
3ceea17c1d | ||
|
b34527c1a3 | ||
|
185bf28fca | ||
|
78cc25584a | ||
|
62ba30bacf | ||
|
3bb84eb206 | ||
|
be7d13ccce | ||
|
8c088a7c0b | ||
|
ea9a642b9b | ||
|
27f528af6a | ||
|
20ca41ec95 | ||
|
7671f0d9e4 | ||
|
44d6bc71b7 | ||
|
9d302e2973 | ||
|
f553701224 | ||
|
f076d05595 | ||
|
b513809710 | ||
|
7519b28e21 | ||
|
3eac4dd57f | ||
|
4c2810720a | ||
|
8480ba8daa | ||
|
fbba392491 | ||
|
530eb35949 | ||
|
c2dd2175a2 | ||
|
b8581b0f56 | ||
|
2ea34767d8 | ||
|
e9af838231 | ||
|
ae0cad47c4 | ||
|
5fbc8ef998 | ||
|
32c6dd9e6a | ||
|
6ece6a6cfb | ||
|
39cd1c18f8 | ||
|
eb65daaa88 | ||
|
0bebdb6e33 | ||
|
1e50e392c6 | ||
|
fb673de370 | ||
|
eee73ab16c | ||
|
5354e034bf | ||
|
72384ad6ca | ||
|
a2b076f9be | ||
|
c8b0a32c0f | ||
|
f0d74aa3bb | ||
|
74a1f100f4 | ||
|
eb049658e4 | ||
|
db138b2a6f | ||
|
1673fc284c | ||
|
503ea57d5b | ||
|
18ca926c7f | ||
|
db99242db2 | ||
|
2b9d2985ba | ||
|
eeb6ecd711 | ||
|
1f58aeadbf | ||
|
3d68be64da | ||
|
668f3b16ef | ||
|
98a340a0d1 | ||
|
8862100f7e | ||
|
274941f6de | ||
|
abec83582d | ||
|
569520c9b6 | ||
|
088310e998 | ||
|
270cab874b | ||
|
4c74e280d3 | ||
|
5b347e17ac | ||
|
55d6ab933f | ||
|
43b74e9706 | ||
|
579a115243 | ||
|
2c67f50a43 | ||
|
78a958e2b0 | ||
|
4e939389b2 | ||
|
e67a9bdb91 | ||
|
567e4e1237 | ||
|
4342e42722 | ||
|
bc818056e6 | ||
|
de2feac238 | ||
|
1e770205a5 | ||
|
e44ecd6d69 | ||
|
5b93a0e633 | ||
|
08fb0e5efe | ||
|
bcf67782ea | ||
|
ef3f175ede | ||
|
bbe4b5d9fd | ||
|
c67a635103 | ||
|
20b24133fb | ||
|
f2567677e8 | ||
|
bc2c2061f2 | ||
|
1c7f5a31a5 | ||
|
59a8ea60f7 | ||
|
aa9b1244ea | ||
|
2d17233366 | ||
|
b245cc9f38 | ||
|
6614d05bdf | ||
|
55aeb03c4a | ||
|
faa589962f | ||
|
c7edd6b39f | ||
|
79da622e3b | ||
|
3da8337ba6 | ||
|
a32d230f0a | ||
|
3772bfd387 | ||
|
02a7900d1a | ||
|
a1fb92468f | ||
|
b7f0a2a98e | ||
|
5fb76b2e79 | ||
|
ad8c97f342 | ||
|
dc1b6373eb | ||
|
983d6d067c | ||
|
a84a06975c | ||
|
d2864c13ec | ||
|
03ba53ce51 | ||
|
d4a6684931 | ||
|
6f0485287a | ||
|
59e2dd4c26 | ||
|
ca1807caae | ||
|
26c20e18ac | ||
|
7c90b6b414 | ||
|
b63c54c4ce | ||
|
fecd2f4ec3 | ||
|
39e420de88 | ||
|
dc83619861 | ||
|
87d1c89701 | ||
|
a42a7769e2 | ||
|
202bda884f | ||
|
2315fdc731 | ||
|
b5469bd8a1 | ||
|
6a6318d04c | ||
|
55933f8d40 | ||
|
be6382e0d0 | ||
|
45e771f96b | ||
|
8dde502cc9 | ||
|
3e66767af3 | ||
|
9ec9d1b338 | ||
|
dcad0d7863 | ||
|
94e1aa0baf | ||
|
b62f043910 | ||
|
6ea22d0d21 | ||
|
8c69dc31b8 | ||
|
00734ea87f | ||
|
3009713db4 | ||
|
9b2ceaf37c | ||
|
8019c2ce18 | ||
|
a9e312b8b1 | ||
|
4da3563d8a | ||
|
48d0a3089a | ||
|
594df64b20 | ||
|
06efb5abfc | ||
|
78eb1417a7 | ||
|
8c8f2ad5ee | ||
|
f71e79d10f | ||
|
1b27c5cf06 | ||
|
67edc8f90d | ||
|
5f576b7d0c | ||
|
8b05c788fd | ||
|
236f033bc9 | ||
|
510fc75121 | ||
|
0376f2e6e3 | ||
|
0b65164f60 | ||
|
9be477de33 | ||
|
84f55b84ff | ||
|
ab5c30ad51 | ||
|
0c839453c5 | ||
|
5e4c5d03ae | ||
|
710af4999a | ||
|
a5b0a1ae62 | ||
|
e9f71ee39b | ||
|
baeb4a46cd | ||
|
5e2a8e9f27 | ||
|
cc1a5bdf90 | ||
|
7f7b1ffaba | ||
|
0ea8092350 | ||
|
483d29497e | ||
|
bae44497fe | ||
|
0d59202aca | ||
|
0ca43f0c9c | ||
|
3bc99639a0 | ||
|
927bc0b63c | ||
|
d968801dc1 | ||
|
89db69d360 | ||
|
895cee7004 | ||
|
4bb71b8439 | ||
|
e4a41f7dd1 | ||
|
69ad6287b1 | ||
|
81cdd6385d | ||
|
e76c42329f | ||
|
e6ef4734ea | ||
|
41a59dcf45 | ||
|
df4bc1d7e9 | ||
|
2b222efa75 | ||
|
94d4d2edb7 | ||
|
7ae19a92ba | ||
|
56d14e56d7 | ||
|
a557c7ae7f | ||
|
b66879ccb1 | ||
|
f1b7157ca2 | ||
|
7622335e84 | ||
|
0da2047eae | ||
|
5ee4321110 | ||
|
9459b9933b | ||
|
87fb564f89 | ||
|
5ca8523220 | ||
|
1118657ffd | ||
|
b1f970152d | ||
|
e1783891ab | ||
|
64d32471dd | ||
|
232cc465d9 | ||
|
8c963bd4ba | ||
|
6a079c1c75 | ||
|
2dc9f2e639 | ||
|
b66fb9caf6 | ||
|
6d18e6d840 | ||
|
2a3c63f209 | ||
|
9f70cecaef | ||
|
47e58a21c6 | ||
|
3714104976 | ||
|
f6f036b9b1 | ||
|
b510b7feb8 | ||
|
c08203e2ed | ||
|
86497fd32f | ||
|
3b998573fd | ||
|
e161882ec7 | ||
|
357f349e30 | ||
|
e4769f541d | ||
|
2a173e2861 | ||
|
a6a900266c | ||
|
bdba53f055 | ||
|
eb2fe18867 | ||
|
a7468c8d23 | ||
|
fb2beb1eac | ||
|
0fb03e3d62 | ||
|
67db3f295e | ||
|
dafaab3ef7 | ||
|
3f11ca409f | ||
|
694eed79ef | ||
|
4220169119 | ||
|
bbdde789e7 | ||
|
0a53ac68a0 | ||
|
eab61cd48a | ||
|
e65d75a0f9 | ||
|
3b99cffb3d | ||
|
a97c05107e | ||
|
5002870d1f | ||
|
73861e613f | ||
|
0ce2ba9ad9 | ||
|
3ddcebaa36 | ||
|
b91463383e | ||
|
7444a2f36c | ||
|
461bc3eb1a | ||
|
cf7f84f033 | ||
|
fdee07048d | ||
|
2fbf201761 | ||
|
4018e4c434 | ||
|
f3382b5bd8 | ||
|
9fc82574f0 | ||
|
589f4dafb9 | ||
|
c5d657ef98 | ||
|
3c2bb566da | ||
|
9287ee0141 | ||
|
2769c8f869 | ||
|
ddb66f33ba | ||
|
79500b8fbc | ||
|
187eea43a4 | ||
|
a89ed6fa9f | ||
|
e0c0ed27bc | ||
|
20abb91657 | ||
|
291ca8daf1 | ||
|
8d168be138 | ||
|
6e1aa7b391 | ||
|
deab9b9516 | ||
|
39d99a906a | ||
|
6f72e6e0d3 | ||
|
d786d79483 | ||
|
01510f6c2e | ||
|
7ba43e9e3f | ||
|
97bfcd1353 | ||
|
aa3c85c196 | ||
|
ee2d5496d0 | ||
|
5c858a2b94 | ||
|
fb75a3827d | ||
|
7d546d0e2a | ||
|
8fcb6ffd7a | ||
|
f97de0c15a | ||
|
be9e192b78 | ||
|
75ae1c9526 | ||
|
33761a0236 | ||
|
19b69b1764 | ||
|
8b804359a9 | ||
|
f050bf5c4c | ||
|
fdc3efa250 | ||
|
5fdd2c71f8 | ||
|
c97c66a41c | ||
|
7b64377fd6 | ||
|
e11ebf18e5 | ||
|
ba47d72bf4 | ||
|
52bc0272f8 | ||
|
d4bce13a03 | ||
|
b9842b57e0 | ||
|
95776e9bee | ||
|
077d8dcd11 | ||
|
9ec41e27c6 | ||
|
200743c84f | ||
|
6d7998e349 | ||
|
7d1ef08a0f | ||
|
ea6b148df2 | ||
|
3ec9c4c5fa | ||
|
0b6b5dab07 | ||
|
ff17473105 | ||
|
dc5f97e737 | ||
|
d919179ba3 | ||
|
f09669a5b0 | ||
|
b3b0f6fed3 | ||
|
88caca60f9 | ||
|
923ebbac81 | ||
|
df298df852 | ||
|
552b246099 | ||
|
80e6d0069c | ||
|
b941604135 | ||
|
52eb5bc84f | ||
|
4d23fe6261 | ||
|
14519294d2 | ||
|
51e46ad2b0 | ||
|
665c8831a3 | ||
|
47dfbacb00 | ||
|
f94911541a | ||
|
89d8af640d | ||
|
6e4252cf4c | ||
|
79ce4de2ab | ||
|
d6575dfee4 | ||
|
a91ab4c203 | ||
|
6a3079a167 | ||
|
c728a1e2f2 | ||
|
d874d76a09 | ||
|
70bc8831f5 | ||
|
41c11be075 | ||
|
163ce19846 | ||
|
9eb16cb667 | ||
|
af40fa327b | ||
|
cf6d28e71e | ||
|
3791ea1e18 | ||
|
34258b92d1 | ||
|
e5db3f11e1 | ||
|
9f47ce8d15 | ||
|
a5b4951f23 | ||
|
8b8bf0748f | ||
|
5cc71ae586 | ||
|
33fcfe4b63 | ||
|
a31a3b53c4 | ||
|
a456ec9599 | ||
|
a2bc9a98c0 | ||
|
e24a98390c | ||
|
6f858cd627 | ||
|
a293266ccd | ||
|
b8e0dc93d7 | ||
|
d774c39031 | ||
|
ab17af99da | ||
|
b0ac3c586f | ||
|
139fa85b18 | ||
|
bfeb9a4538 | ||
|
3d6c79ae5f | ||
|
c9e9f73ea9 | ||
|
80e482b155 | ||
|
9351593495 | ||
|
d74436f546 | ||
|
76e9053dd0 | ||
|
dbb8bcdd8e | ||
|
7305afa0f8 | ||
|
481f999b70 | ||
|
4b16022556 | ||
|
89dd201a7b | ||
|
ab486323f2 | ||
|
6460c11107 | ||
|
89f7f3c17c | ||
|
fe800b3af7 | ||
|
2a1077ff43 | ||
|
01a16ff388 | ||
|
eb60ddb729 | ||
|
db5faeceee | ||
|
45d3e6aa71 | ||
|
d84a2c183f | ||
|
ecb5eedeae | ||
|
90a2d4ae38 | ||
|
2b8ab97ec1 | ||
|
43ca9c8a12 | ||
|
69d99c91dd | ||
|
a8cc98a0f6 | ||
|
2ee58f4bc9 | ||
|
938431e514 | ||
|
b2de3c70fa | ||
|
542690d9f6 | ||
|
596a7fb4ea | ||
|
c3f726a01f | ||
|
4538ade156 | ||
|
f4709d8f32 | ||
|
3dda8c228c | ||
|
ccf6b7caf3 | ||
|
fed33ed64a | ||
|
ca27d95ce1 | ||
|
3566fe296a | ||
|
c91435e314 | ||
|
31f30069a4 | ||
|
e5726a75d2 | ||
|
c757d116bf | ||
|
23cce0c78a | ||
|
1bd29a586c | ||
|
4565bfe359 | ||
|
336d6fdd14 | ||
|
95cde242ca | ||
|
9224176202 | ||
|
0d2390fd13 | ||
|
4a0356e26f | ||
|
73f973cc06 | ||
|
e9e8580913 | ||
|
8b85a58fea | ||
|
40512511af | ||
|
10d8fc4fe7 | ||
|
9899d45ea8 | ||
|
3eea471ca6 | ||
|
3dec4b6b34 | ||
|
162fc25ebc | ||
|
e9854f194c | ||
|
9c292a4f62 | ||
|
edb42836da | ||
|
1ff88ff0bc | ||
|
28e7c8e5e0 | ||
|
463b3ed0ce | ||
|
8e78286068 | ||
|
f4eeef145e | ||
|
87aa869338 | ||
|
60ad4786bc | ||
|
a74df7f905 | ||
|
9f9c6736ab | ||
|
b95646625f | ||
|
6e47eae903 | ||
|
934af0dd4b | ||
|
a8bec13ed9 | ||
|
1cf62f5850 | ||
|
8047e77757 | ||
|
2a92de29ce | ||
|
99523ca079 | ||
|
35f49bbb60 | ||
|
50ec922c2b | ||
|
cfbbeaa26e | ||
|
a3b0189934 | ||
|
8f367d96f8 | ||
|
f78ef36cd4 | ||
|
dc67c81f99 | ||
|
50ba8fd099 | ||
|
99b3b00b68 | ||
|
f6d981761d | ||
|
8290c19e24 | ||
|
7a69dff6cf | ||
|
bfb7ed2c99 | ||
|
e19dc9b13e | ||
|
74148c790e | ||
|
3d77456110 | ||
|
ab6a4b1749 | ||
|
aeeb1d0cb7 | ||
|
185b79f2a5 | ||
|
8d0f9652c7 | ||
|
5353805cc6 | ||
|
5407da5650 | ||
|
b1bfe6f76e | ||
|
74e25370ca | ||
|
bb5d946c26 | ||
|
abab5bdc8a | ||
|
30bf845c81 | ||
|
77efce0673 | ||
|
67a98fb0b0 | ||
|
7d471ec30d | ||
|
f3182a9264 | ||
|
805cb5ad58 | ||
|
fdf05cedae | ||
|
9c5f463775 | ||
|
893fae6d59 | ||
|
5660f291af | ||
|
efd56efc63 | ||
|
d94373f4b1 | ||
|
0d01a48260 | ||
|
00ab2684fa | ||
|
a5585110a6 | ||
|
965c89798e | ||
|
982b03382b | ||
|
24b805472a | ||
|
6ce029b317 | ||
|
63e5b0ab18 | ||
|
6dda2c2d83 | ||
|
3fb3c0b92e | ||
|
aa2c960b74 | ||
|
4fbcc02f96 | ||
|
9aa8f13731 | ||
|
65bee366dc | ||
|
53700e6667 | ||
|
7f498e10b7 | ||
|
6eb0f13411 | ||
|
773377fe84 | ||
|
4372c8c835 | ||
|
099133bdbc | ||
|
b09e2dbeb7 | ||
|
96bcf03ad5 | ||
|
0999f07320 | ||
|
5d2b455572 | ||
|
ea75ddc0e0 | ||
|
2db0e446cb | ||
|
557bdaa694 | ||
|
9eb1f120fc | ||
|
266d6e4bea | ||
|
e4c97a91d8 | ||
|
b0a874a842 | ||
|
bca40de107 | ||
|
93652e0937 | ||
|
0a383a712d | ||
|
03d5dec24c | ||
|
b2a3cac351 | ||
|
a18edad04c | ||
|
92522e8d97 | ||
|
049d94ce31 | ||
|
dbc6a95276 | ||
|
75b0888032 | ||
|
2ad93ad41a | ||
|
623ee5570f | ||
|
fd2bad39f3 | ||
|
e6c8a6febe | ||
|
4ece5f847b | ||
|
e4f04af044 | ||
|
b730b17f52 | ||
|
98c40958ab | ||
|
41b52f5bcd | ||
|
4264fb9f49 | ||
|
016a4c62e1 | ||
|
2f38c95886 | ||
|
df89661ed2 | ||
|
41da4f422d | ||
|
2e89b55593 | ||
|
7babdb87d5 | ||
|
680ad19c7d | ||
|
f01267bc6b | ||
|
df6a05b9a7 | ||
|
8569bb8e11 | ||
|
ca6e2db2b9 | ||
|
2080e31616 | ||
|
c379be846c | ||
|
9bc665628b | ||
|
ee49c01d86 | ||
|
b21f8538a8 | ||
|
dd15676d33 | ||
|
ec5a17ad13 | ||
|
e48f52faba | ||
|
8462e88b8f | ||
|
bf26ead010 | ||
|
c2cedfa83c | ||
|
eba2844361 | ||
|
c6c8b059bf | ||
|
d8a99784e5 | ||
|
57929ff242 | ||
|
4430a39120 | ||
|
6228f46af1 | ||
|
ac67b6b5da | ||
|
1a268c24c8 | ||
|
38e2089c3f | ||
|
e2107901ec | ||
|
15745b692e | ||
|
696fd8909d | ||
|
02b1c4b172 | ||
|
285e657f68 | ||
|
046ffc7752 | ||
|
2ef66ce0ca | ||
|
dc5c668940 | ||
|
f19148132a | ||
|
6d7b886aaa | ||
|
b316b55be9 | ||
|
80900107f7 | ||
|
7e4efa45b8 | ||
|
86ea28d6bc | ||
|
34703da144 | ||
|
1282f78bc5 | ||
|
2d5d965f7f | ||
|
afe56c7cf1 | ||
|
7d51cf882f | ||
|
499deac2ef | ||
|
9685993adb | ||
|
261dcdadc8 | ||
|
314a901bf0 | ||
|
1caad7e19e | ||
|
e585116dab | ||
|
40f42bf654 | ||
|
eaf7fbb9e9 | ||
|
d05a2e57e9 | ||
|
f8684118f3 | ||
|
2e1f669aea | ||
|
6c3abff664 | ||
|
dcb43a3308 | ||
|
ec600b967d | ||
|
aebb2652e8 | ||
|
52a9a0d410 | ||
|
4123e99469 | ||
|
51a8a242ac | ||
|
60ef826e07 | ||
|
2ad564404e | ||
|
2bb9f18411 | ||
|
7a1edc0880 | ||
|
b812e96c6d | ||
|
22b35d5d91 | ||
|
d36055a2d0 | ||
|
0d227f3543 | ||
|
accc598967 | ||
|
02c4a2d4ba | ||
|
6665e447aa | ||
|
7eb955cc42 | ||
|
f4d79c203d | ||
|
4d29581ea4 | ||
|
0b31c4cfbb | ||
|
5c098005cc | ||
|
ae87e41cec | ||
|
dfd19b5eb9 | ||
|
8ed5b51a32 | ||
|
9d0e5dee02 | ||
|
ffd970036d | ||
|
fa162698c2 | ||
|
ad3857938d | ||
|
179a6002c2 | ||
|
d28fc86956 | ||
|
6303977e9c | ||
|
97695693f2 | ||
|
1ab875a75d | ||
|
31881874a9 | ||
|
f090f0101b | ||
|
9881cac2da | ||
|
12590d3449 | ||
|
abf7a8d78d | ||
|
ecfe17521a | ||
|
0d29e2a39d | ||
|
12a2ab93db | ||
|
d90bd340bb | ||
|
21afe94096 | ||
|
fa36689597 | ||
|
85c99ae808 | ||
|
a4ecd5f4ce | ||
|
6401a513d7 | ||
|
d86926be5f | ||
|
a6b03a66dc | ||
|
d023e399d2 | ||
|
e8ab1e14e0 | ||
|
a6e15cb338 | ||
|
4fbb863a10 | ||
|
6ee4d1eb90 | ||
|
738e0e5fed | ||
|
0e4dd3d76d | ||
|
10fe5a78cb | ||
|
975b8ae2e9 | ||
|
935234939c | ||
|
87e38e6181 | ||
|
f73fc8dd57 | ||
|
3faa5bf521 | ||
|
6973712480 | ||
|
02df421c94 | ||
|
95b9af92a0 | ||
|
8ee64c0771 | ||
|
b805f6daa8 | ||
|
dae22ccbe0 | ||
|
9d00243d7f | ||
|
5461634616 | ||
|
40bca93884 | ||
|
b798f28443 | ||
|
fff2ce5721 | ||
|
69f88255e9 | ||
|
08ff79827e | ||
|
67703e2274 | ||
|
d0d6bb173c | ||
|
54caf17107 | ||
|
2168b7cf7d | ||
|
90744433c9 | ||
|
5371f078f7 | ||
|
0dd14a4bd0 | ||
|
9974b31a09 | ||
|
0ffbbaf4b9 | ||
|
6839415a0b | ||
|
55f3ac4846 | ||
|
801cf4b5da | ||
|
e0459d0c0d | ||
|
23759a7243 | ||
|
55b2b7636b | ||
|
36160988e2 | ||
|
9f982a0c3d | ||
|
dcbec9414f | ||
|
a07cf1ba93 | ||
|
4a8afa6b9f | ||
|
bb06cc9ff3 | ||
|
9c06f446fb | ||
|
2d076cbd67 | ||
|
fb2eef24d6 | ||
|
e2f68d9ccf | ||
|
d4f4d751c0 | ||
|
b4eac2516e | ||
|
4435f6245c | ||
|
9b922af075 | ||
|
0112ae725c | ||
|
619392edf9 | ||
|
0894822b68 | ||
|
206a7ce6c1 | ||
|
a69ab311c7 | ||
|
a61327fa0b | ||
|
6985ab762a | ||
|
0e8300979b | ||
|
0b60411e5f | ||
|
f83f777fff | ||
|
89aae93e60 | ||
|
65b74f9cab | ||
|
7543e98035 | ||
|
59ec70eb73 | ||
|
365229991b | ||
|
959a8e29ee | ||
|
197c82acd4 | ||
|
9539fdb53c | ||
|
5659df4388 | ||
|
24bf29d369 | ||
|
17dc00d05f | ||
|
4fcd4a8197 | ||
|
daf2a8df54 | ||
|
43489c98d8 | ||
|
88997a1c4f | ||
|
d12c77305c | ||
|
ab4e2b222e | ||
|
b867eadbef | ||
|
19163fa883 | ||
|
a7c33809c4 | ||
|
650f3843bb | ||
|
9e766bc056 | ||
|
48aff52e00 | ||
|
9d7616317e | ||
|
d227a09fb1 | ||
|
f48cf77c4d | ||
|
3549be216f | ||
|
c3e3a3dbc5 | ||
|
55a7c1db00 | ||
|
bb315221ab | ||
|
c38766c5a6 | ||
|
c837321df1 | ||
|
af7f6b89ec | ||
|
29a4d3df23 | ||
|
bcbb9afac0 | ||
|
7d1cafc070 | ||
|
5951c67a8b | ||
|
c454007730 | ||
|
4e49cca43d | ||
|
49a8c06095 | ||
|
d01d9fa670 | ||
|
a53a32f006 | ||
|
3548d54cf6 | ||
|
01f242ac7e | ||
|
2840d9d403 | ||
|
9fecfc5025 | ||
|
1b901e01f2 | ||
|
974aa35558 | ||
|
4021a0ae98 | ||
|
b7a95be731 | ||
|
616649f040 | ||
|
ac3c692b5f | ||
|
6087f9635c | ||
|
2ad0bfda1e | ||
|
cf8b12bcdc | ||
|
08f8b6e022 | ||
|
800ed6b1e9 | ||
|
df93e57a9a | ||
|
908535a3a0 | ||
|
7fe2ab6f39 | ||
|
c9ee0c909e | ||
|
38aedb50ac | ||
|
4772e0b59d | ||
|
9c49e876d5 | ||
|
152007cd5c | ||
|
70e2e41955 | ||
|
4d71c776fc | ||
|
0f41105436 | ||
|
2d49071e96 | ||
|
89889ecbbd | ||
|
41576e74d4 | ||
|
c8ee354d0b | ||
|
4e5f069809 | ||
|
6690e9bde8 | ||
|
e4b34b6ee6 | ||
|
3952ef6ca5 | ||
|
463d333846 | ||
|
7eb5e6aa66 | ||
|
282022d64e | ||
|
91a98a8807 | ||
|
32fe864a33 | ||
|
e1c9313396 | ||
|
f430a084e8 | ||
|
a86b596897 | ||
|
6dd87b0378 | ||
|
c9f029c214 | ||
|
6b88db10ad | ||
|
8a891c2159 | ||
|
ad2ac8eee3 | ||
|
f46733a47a | ||
|
934167323d | ||
|
64baa41e64 | ||
|
5165cf6d15 | ||
|
4489b21528 | ||
|
f623b37577 | ||
|
f4a2fea451 | ||
|
a748fc5448 | ||
|
0dcca0cb83 | ||
|
b80a83339b | ||
|
eb74d08f2a | ||
|
e79ab0c70e | ||
|
e419e26f3a | ||
|
6102fd99bf | ||
|
def36719d3 | ||
|
462aa9af26 | ||
|
a09c84e1b8 | ||
|
44b33798f3 | ||
|
2f0b648fad | ||
|
de0e56f027 | ||
|
973ced7b13 | ||
|
cb4b824a85 | ||
|
c583a538b1 | ||
|
e0224085b4 | ||
|
44c1e1d6d9 | ||
|
c620e9c026 | ||
|
1bb88968c5 | ||
|
df75e8f4aa | ||
|
adf846bfd2 | ||
|
1748fcc5ac | ||
|
08416393e0 | ||
|
fce26015c9 | ||
|
155be1078d | ||
|
6efc0f21fe | ||
|
f3255e080d | ||
|
0da03d4cfc | ||
|
5f6a3ef9d0 | ||
|
afc4fed591 | ||
|
cb505f98ef | ||
|
a0b3634cb6 | ||
|
e23359bae9 | ||
|
5531ed632a | ||
|
150ee21f3c | ||
|
c96da0ce1e | ||
|
a0d9e66ff7 | ||
|
55f627ed4c | ||
|
7dd8c78c6b | ||
|
8bf7d090fd | ||
|
6bfe04b609 | ||
|
491d6bec46 | ||
|
4fb86ac692 | ||
|
6cba6aef3b | ||
|
7e216db463 | ||
|
adc90c8f1e | ||
|
e3316a3672 | ||
|
a3a6d6292b | ||
|
8cb9455c32 | ||
|
dc65b2ee01 | ||
|
98a1adbf81 | ||
|
0bd1e15cce | ||
|
eda926767e | ||
|
cd1a18c045 | ||
|
6f567fbea8 | ||
|
0ebadd03a5 | ||
|
2253b556b2 | ||
|
6a7a7009c7 | ||
|
3c75057dcd | ||
|
212d101727 | ||
|
760b80659d | ||
|
04879c005d | ||
|
cb82927756 | ||
|
8b9629f2f6 | ||
|
f6db16b313 | ||
|
4668b1ddcb | ||
|
dcf9d9caad | ||
|
7a69b76001 | ||
|
ac07ef822f | ||
|
e7d4bcd872 | ||
|
a28c6d7cfe | ||
|
d816f048f5 | ||
|
b09ddd0036 | ||
|
0a73b02a00 | ||
|
8769704462 | ||
|
214551f1df | ||
|
2cc74c005a | ||
|
ed250f57f2 | ||
|
e92c25f7e0 | ||
|
3ab563f314 | ||
|
426338cb45 | ||
|
5fa2375898 | ||
|
41782a0ab5 | ||
|
9b06433b82 | ||
|
def607d840 | ||
|
2b811fb422 | ||
|
36cc62c10c | ||
|
975d92912c | ||
|
8bbaf457de | ||
|
7641a02f31 | ||
|
ce16239e34 | ||
|
d64bd227cf | ||
|
c5ab0a9054 | ||
|
dac948973d | ||
|
9d008d1d6f | ||
|
f52457213e | ||
|
579295a673 | ||
|
af8ff8ce99 | ||
|
7fa3e86e64 | ||
|
3359f72239 | ||
|
41fa154aa6 | ||
|
deaba0152d | ||
|
feaef6093e | ||
|
078fa4fdd0 | ||
|
2dc77a0638 | ||
|
cfd9a7187f | ||
|
f434a8b492 | ||
|
d2658d6f84 | ||
|
8c559c8121 | ||
|
2353c73c57 | ||
|
599e719ad4 | ||
|
b6d365bacd | ||
|
52f0c0d336 | ||
|
be55f3f937 | ||
|
fda1c05164 | ||
|
1329d4abd8 | ||
|
f064992137 | ||
|
8a81a480a1 | ||
|
d729c400e5 | ||
|
ad4810d991 | ||
|
6a67043537 | ||
|
864d6c28e7 | ||
|
bb6b51ad91 | ||
|
65e3caf402 | ||
|
b7d9a7ae89 | ||
|
ed73d79ec1 | ||
|
c538c25008 | ||
|
4b47fadbab | ||
|
fcdc843c15 | ||
|
dbdcf459a7 | ||
|
ef25d60666 | ||
|
7f7021ce64 | ||
|
448a941de2 | ||
|
5766da69ec | ||
|
617e633d7a | ||
|
b770a1143f | ||
|
e1151ecf2a | ||
|
ae7c760772 | ||
|
81815f3e0a | ||
|
3890c413a3 | ||
|
8e02f567d7 | ||
|
87bb93e1d4 | ||
|
e596c929ac | ||
|
9852b0e609 | ||
|
51b0d6c0d3 | ||
|
15391c7a88 | ||
|
fe62593286 | ||
|
4cc11e183c | ||
|
de8e753fc8 | ||
|
f82ebd7716 | ||
|
bd0704d5a4 | ||
|
1968485881 | ||
|
002afca1c5 | ||
|
411b3f3138 | ||
|
a4b810f511 | ||
|
cd8f33f830 | ||
|
824765b1ee | ||
|
9e8138f853 | ||
|
fe8d583fdd | ||
|
0bd3365c24 | ||
|
d8f4e7d72b | ||
|
afc047cd27 | ||
|
00ef4f9803 | ||
|
07e4d7ec6d | ||
|
258a344810 | ||
|
2a03014652 | ||
|
8ae1f08095 | ||
|
57e6a12d08 | ||
|
46423612e3 | ||
|
29bf473d74 | ||
|
9689f3faee | ||
|
93fa58c93d | ||
|
186a98cc99 | ||
|
9993f265ca | ||
|
144f967dbf | ||
|
b31c9bb726 | ||
|
c0820b5e5c | ||
|
65b8a1d5d9 | ||
|
a0648844fb | ||
|
c4a27003c6 | ||
|
41abd8982f | ||
|
86bbc1043e | ||
|
9a045a0588 | ||
|
9415539b38 | ||
|
84bab2783d | ||
|
0d6e7673e4 | ||
|
d78e9e715f | ||
|
a8ec59eb75 | ||
|
20fc0ef13c | ||
|
37ae8cb33c | ||
|
9f1649636e | ||
|
3a65fe8917 | ||
|
99a6e56e99 | ||
|
e696fd9e92 | ||
|
c943954bb4 | ||
|
eaf836dc66 | ||
|
dbf64b0987 | ||
|
8d0af9548b | ||
|
67aa20ea2c | ||
|
5604e9f531 | ||
|
1a51ec2d69 | ||
|
3eb0800742 | ||
|
427f3e922f | ||
|
823ca73a3f | ||
|
7fc0d4d786 | ||
|
b8e336e809 | ||
|
9429bf5c45 | ||
|
f7f0100174 | ||
|
fc00701a1e | ||
|
09447f2ad2 | ||
|
ff0ef1eebc | ||
|
1d34224416 | ||
|
56d35aa596 | ||
|
2201b1a506 | ||
|
5cdb07023b | ||
|
6154e16951 | ||
|
f4ff7185f0 | ||
|
6357d30ea0 | ||
|
8d4ef982d0 | ||
|
4740156cfa | ||
|
f8e7f75831 | ||
|
085137ca63 | ||
|
3fd2a83184 | ||
|
66c1281301 | ||
|
73947d9eca | ||
|
a69c0b2718 | ||
|
6c764bceeb | ||
|
273aeb7bae | ||
|
d185858266 | ||
|
453bd6064b | ||
|
904f2587cd | ||
|
14172312dc | ||
|
c600d7aa47 | ||
|
3c9234078a | ||
|
ee8e0497ae | ||
|
fd5d121648 | ||
|
30bdb4b4e9 | ||
|
2ee492fb74 | ||
|
36a5c8b44c | ||
|
07b625c58d | ||
|
746a865106 | ||
|
f85ec28a16 | ||
|
0307c55f9f | ||
|
d05c916491 | ||
|
c73e43f5c9 | ||
|
e61e7f44b9 | ||
|
f9b6ac03c6 | ||
|
296ccc5f8e | ||
|
8cb5825617 | ||
|
cee707abd8 | ||
|
92717a4832 | ||
|
37a7296759 | ||
|
ebbe49d17b | ||
|
b7e330855f | ||
|
ac89224fb0 | ||
|
9ec262ae00 | ||
|
64acdb5f2a | ||
|
a175b36382 | ||
|
16526d283c | ||
|
752e677555 | ||
|
f796af1ae8 | ||
|
2515993536 | ||
|
66b3e71e56 | ||
|
652d151373 | ||
|
300b1a1b84 | ||
|
6c3b49417f | ||
|
dcc5cfb7c0 | ||
|
d970836605 | ||
|
8021bd0aae | ||
|
8f91156d80 | ||
|
fab36d6e63 | ||
|
3d54879c14 | ||
|
e17fcde865 | ||
|
6950dffcb4 | ||
|
02dd5c5853 | ||
|
5a1087dbf9 | ||
|
7564dfeb7a | ||
|
10bad635a8 | ||
|
7cc8b0fed5 | ||
|
a77846373b | ||
|
bcd0dabb92 | ||
|
9d68062553 | ||
|
e66d0b7431 | ||
|
ba26f6ce84 | ||
|
929caed0b9 | ||
|
8340aa2b6c | ||
|
1188fe3bf0 | ||
|
b15f47d80e | ||
|
ef261cbbd7 | ||
|
06997ff255 | ||
|
9d7df87886 | ||
|
a4b2323ca3 | ||
|
e8de468b0b | ||
|
d83a3bf4e2 | ||
|
f2b39ad055 | ||
|
95d1bd98e4 | ||
|
8acbc6a6b4 | ||
|
467ba5be20 | ||
|
29ddf9e61d | ||
|
92e119cab3 | ||
|
92049ba8e4 | ||
|
54330b9921 | ||
|
d1aeb030f2 | ||
|
f89274d1ea | ||
|
7286596fb4 | ||
|
a2fc83d94e | ||
|
2161799cc3 | ||
|
c88f132057 | ||
|
c6313a5906 | ||
|
eadcdb5bed | ||
|
6e7649b5f7 | ||
|
d986f90074 | ||
|
53c575db3f | ||
|
6dcc20038c | ||
|
fa145f632b | ||
|
785d8deadd | ||
|
93a2d5afbf | ||
|
d60c6b18d4 | ||
|
d1e02569f4 | ||
|
9ce67029ca | ||
|
98f3382cea | ||
|
52a0255814 | ||
|
eb59ac8535 | ||
|
acc2b4e10f | ||
|
6f830f0e08 | ||
|
6edc318597 | ||
|
182c0cf28e | ||
|
0b105b5986 | ||
|
081c7d22bc | ||
|
6aee896657 | ||
|
cae1bad274 | ||
|
1b8b97b8ec | ||
|
0846606b12 | ||
|
245ebcdfc6 | ||
|
1b1e711c93 | ||
|
c088c25b09 | ||
|
958d64720e | ||
|
805afad4fe | ||
|
400f4840ad | ||
|
ee7792596d | ||
|
0081328aca | ||
|
3fff7f6878 | ||
|
f15dd06473 | ||
|
dd26819d66 | ||
|
562012fb22 | ||
|
a6d257df5b | ||
|
41d896ba3e | ||
|
51cdf46645 | ||
|
1eb0adf6d3 | ||
|
40c9d2050f | ||
|
3a325845c7 | ||
|
6a1bfd6270 | ||
|
b91ea1d7ca | ||
|
c5760cd535 | ||
|
91c7960800 | ||
|
2079a5574b | ||
|
27ffb8fa8a | ||
|
22c8fb3f59 | ||
|
964419803a | ||
|
6271d5d544 | ||
|
175bd310f5 | ||
|
67ee6f4126 | ||
|
01b312f14c | ||
|
18638c62de | ||
|
753d000788 | ||
|
19e781b104 | ||
|
aa2df327db | ||
|
321fa94b8f | ||
|
ca80957143 | ||
|
41cdb8f71b | ||
|
304d4c9acf | ||
|
7fd4c092e3 | ||
|
2fe5705542 | ||
|
e968365858 | ||
|
36ad4c7466 | ||
|
5a62b3058f | ||
|
ec8fe9f031 | ||
|
a1df9e886a | ||
|
ce5ae1931d | ||
|
b9445d4f62 | ||
|
fd1eec99b5 | ||
|
e162406d40 | ||
|
c41e68aaab | ||
|
4665af6c42 | ||
|
c0b15427fe | ||
|
f29a9d972d | ||
|
b192373ae7 | ||
|
c042650382 | ||
|
07a916a720 | ||
|
5134044530 | ||
|
4c62065e74 | ||
|
d28fc99119 | ||
|
c9fb45c85f | ||
|
7c6e18f7a7 | ||
|
176b9c9666 | ||
|
ecd9c35233 | ||
|
71e32c57d9 | ||
|
2fefd0e4e3 | ||
|
81eaf79a25 | ||
|
8dea7217a6 | ||
|
c0fb9e17e8 | ||
|
83776a8dce | ||
|
98c0972619 | ||
|
56d832d661 | ||
|
de3a350afe | ||
|
708a741960 | ||
|
0caef1b307 | ||
|
264e2db539 | ||
|
734996002c | ||
|
205e5016e8 | ||
|
a0f28a7f9b | ||
|
14b7680328 | ||
|
f44222ce53 | ||
|
f310ad8d98 | ||
|
d56b394bcc | ||
|
55c9501e57 | ||
|
fad9575154 | ||
|
97e11e1ac9 | ||
|
e6a1e164b2 | ||
|
e4f8f81e89 | ||
|
176b3bb526 | ||
|
b07752fa9b | ||
|
68fd0efbde | ||
|
c80d3eb812 | ||
|
f9320995d6 | ||
|
f592c9f04d | ||
|
bd7970fb1f | ||
|
c47730f2cc | ||
|
41cccfd2aa | ||
|
aff690f7d6 | ||
|
d4b0539d39 | ||
|
cb55273769 | ||
|
fbad625126 | ||
|
e49ba887e9 | ||
|
edc1acbb7e | ||
|
d304c10641 | ||
|
302c53a8e7 | ||
|
ef02b712ad | ||
|
aca217cf9a | ||
|
9e3386dbbb | ||
|
fdec565b34 | ||
|
33c2188c87 | ||
|
b3c8fa74cc | ||
|
e53bb70bef | ||
|
109bec372c | ||
|
5c2561d05d | ||
|
0e970b8037 | ||
|
1694b4d6ef | ||
|
396299c1db | ||
|
71d789aab0 | ||
|
41ca50ff0e | ||
|
6d2e14a656 | ||
|
4078708aea | ||
|
343ea9c6d8 | ||
|
60361f88ed | ||
|
f7560cb1d8 | ||
|
1f66568d59 | ||
|
7af07cef95 | ||
|
41a540a629 | ||
|
f599944942 | ||
|
1e06aee6a2 | ||
|
7bbaedef97 | ||
|
87048511fe | ||
|
c770f0b68b | ||
|
78c00ad512 | ||
|
a19879d494 | ||
|
ac1aca36b0 | ||
|
1f3b89cf28 | ||
|
f732f6ae6f | ||
|
0b9f3d1751 | ||
|
0806aa6dfe | ||
|
32436d099c | ||
|
4ce692ccaf | ||
|
3caa4eed75 | ||
|
c70f508ae8 | ||
|
9e64d7aaf9 | ||
|
72b773f06d | ||
|
5f978b865b | ||
|
57a4f92722 | ||
|
87351e89ca | ||
|
7920c67a48 | ||
|
192e356169 | ||
|
31232e49fb | ||
|
116595d218 | ||
|
9d93a31755 | ||
|
9f7df59945 | ||
|
d2418521a7 | ||
|
9330b5b1d9 | ||
|
faa50bf578 | ||
|
f0d9618dfc | ||
|
310a880fa8 | ||
|
fc6e3b6da0 | ||
|
50771045d0 | ||
|
8f522470ed | ||
|
dc90c9ac65 | ||
|
e46e174b59 | ||
|
7f3f3f577c | ||
|
75d87c73d1 | ||
|
0fe44c9bf2 | ||
|
7a1d20ed0a | ||
|
70c83b60a1 | ||
|
7ba296ccdf | ||
|
0b112cb4d4 | ||
|
68ac8d3e09 | ||
|
f6fa8bd722 | ||
|
6aee27a3f1 | ||
|
401568033c | ||
|
ea73be6831 | ||
|
ba8a75c84b | ||
|
a1f3ccdd6d | ||
|
647d38007f | ||
|
e7dd28b926 | ||
|
b5fc9673d9 | ||
|
a065040323 | ||
|
dec3b1092d | ||
|
407915a86e | ||
|
c488599879 | ||
|
bcecc93e39 | ||
|
ff7d1a250e | ||
|
70f338c3de | ||
|
c847d83011 | ||
|
5ce46a61d4 | ||
|
775974d5ec | ||
|
c7af40c368 | ||
|
00a974a721 | ||
|
7428ba2dd7 | ||
|
b37223c053 | ||
|
24051fec03 | ||
|
f811a29f87 | ||
|
acf7bcc7a6 | ||
|
9707366348 | ||
|
9e5fe71f5b | ||
|
5d1b7da728 | ||
|
3ea1ddae22 | ||
|
1694e9c78c | ||
|
4763077b76 | ||
|
c0eaca220c | ||
|
25d086c4e1 | ||
|
88551043cd | ||
|
f779f760c4 | ||
|
f18f82e229 | ||
|
67ef2b45fa | ||
|
d72e871265 | ||
|
4c9bc13309 | ||
|
84563b0d46 | ||
|
c5aab7e8db | ||
|
1755b646b8 | ||
|
85f906ea53 | ||
|
e1a155a9c8 | ||
|
0454447e41 | ||
|
7b40c0bbee | ||
|
dc773c5c20 | ||
|
b6253b03c2 | ||
|
a5bc29245b | ||
|
bfae478251 | ||
|
a7cd490593 | ||
|
283d2caa81 | ||
|
dd8fb04886 | ||
|
ce8dca7659 | ||
|
5bd3934d22 | ||
|
128f550ee5 | ||
|
3a56a06c4f | ||
|
126ac3816f | ||
|
d02bed1a55 | ||
|
b7ed0ce537 | ||
|
a742503508 | ||
|
33312ab09e | ||
|
c422f0b9fb | ||
|
7797de80e3 | ||
|
c92f1b8df8 | ||
|
bde68ba48b | ||
|
bf44805e69 | ||
|
edf9aa2c23 | ||
|
4801c47273 | ||
|
bcad6492d6 | ||
|
5ab2a22e88 | ||
|
d7bd540683 | ||
|
62954f98de | ||
|
722b56c8ca | ||
|
f3f12058dc | ||
|
3da38d0483 | ||
|
d715b1f9ca | ||
|
e13fa25e11 | ||
|
34d4834ff6 | ||
|
117ddd17d7 | ||
|
6f2bf38f0e | ||
|
320882c34a | ||
|
8bbb533c9a | ||
|
3113b5a551 | ||
|
c0cc05177f | ||
|
0b34d43804 | ||
|
6c7d7427bf | ||
|
54ed3b86ba | ||
|
2001d0f707 | ||
|
0f9cd9c87d | ||
|
2e7db61808 | ||
|
5329968155 | ||
|
2e536e3141 | ||
|
cf935a5331 | ||
|
fa81e5b8ee | ||
|
9fea22b90d | ||
|
0889b6d247 | ||
|
1328bc4938 | ||
|
787a20cbaa | ||
|
a73f1ab0ac | ||
|
30c0dad3ae | ||
|
440e097d78 | ||
|
b74a3ebd85 | ||
|
45987a1d98 | ||
|
8f0950fc44 | ||
|
30bc3f9281 | ||
|
f655ec5a5c | ||
|
84b4158555 | ||
|
91dd45cf64 | ||
|
37af60254f | ||
|
f0e736d4ea | ||
|
e3ebb0c5bb | ||
|
6382f779c3 | ||
|
8ef34883a8 | ||
|
5c46af0edb | ||
|
b6511fbfe2 | ||
|
eccb12b366 | ||
|
d0982e7ba5 | ||
|
fc30da0d48 | ||
|
e6a1052ba7 | ||
|
968dce50fc | ||
|
3bbffd3c22 | ||
|
072b5fcd12 | ||
|
fabffa80f0 | ||
|
064265b0b9 | ||
|
2d5d11645d | ||
|
cc813a5624 | ||
|
156c067f79 | ||
|
b33b013d41 | ||
|
e74e2f705f | ||
|
2e438847fc | ||
|
9301c47d93 | ||
|
20ec58b07f | ||
|
98983c1015 | ||
|
67195592c6 | ||
|
21abfc6424 | ||
|
d1e88df71e | ||
|
f36cfe34ab | ||
|
8a1934008c | ||
|
b41bb9cfcf | ||
|
d58324bbef | ||
|
cbbd45d3e5 | ||
|
b89633ae4b | ||
|
96357e9bfd | ||
|
d530c3096f | ||
|
ae0c1c3f2d | ||
|
0cc2564380 | ||
|
38d20022ad | ||
|
280132dad0 | ||
|
61de4e2789 | ||
|
f9d3455320 | ||
|
2ff64c3c12 | ||
|
902f235b5b | ||
|
97d43a6fa2 | ||
|
9bc65ff0ca | ||
|
6cd6a615fd | ||
|
5639f0653d | ||
|
251174c9a2 | ||
|
42ea87d637 | ||
|
7c8a60b8cf | ||
|
2f4500be5a | ||
|
fa7534a362 | ||
|
a258f0af7a | ||
|
01621c6344 | ||
|
c7934342a6 | ||
|
f5c324c06b | ||
|
f615cf2391 | ||
|
c984a97262 | ||
|
a02c06a837 | ||
|
21d6aa421c | ||
|
e5d274fe1c | ||
|
166a391eae | ||
|
5fb24bb27f | ||
|
5a5430b383 | ||
|
67a1e1c874 | ||
|
4155fbe94c | ||
|
347fe6b7be | ||
|
ff3ceb981e | ||
|
1dafa0c74d | ||
|
09917837d0 | ||
|
dd507a3808 | ||
|
e67dcf4d68 | ||
|
dd9406d0ac | ||
|
6a80ac62a5 | ||
|
98efb08e17 | ||
|
f58a9f46be | ||
|
fd77e62a13 | ||
|
376228e199 | ||
|
8a5b853fae | ||
|
1cbf23e7e7 | ||
|
63554ba171 | ||
|
5de37cb820 | ||
|
e5cee1f46d | ||
|
e9a01caa5c | ||
|
858357a246 | ||
|
ef16502159 | ||
|
29e2c43e01 | ||
|
7aa2f80117 | ||
|
d29f9c4ffd | ||
|
7617b4cbc2 | ||
|
e0c769fd19 | ||
|
ebd10a5f28 | ||
|
2b77184281 | ||
|
e23976f6c4 | ||
|
0b8dc02eba | ||
|
fde1d0677e | ||
|
48986574ae | ||
|
c7a6a71d07 | ||
|
1847845151 | ||
|
7cb92195d1 | ||
|
72afa0341f | ||
|
0152004c42 | ||
|
30ca5046b5 | ||
|
8e9698c9a0 | ||
|
3047e2dd7c | ||
|
a8b9d21f2d | ||
|
c77a5b7cb6 | ||
|
23f2068e33 | ||
|
ffadfb4149 | ||
|
b7e38cfbae | ||
|
659743b39c | ||
|
cbac42bdd1 | ||
|
69758c5859 | ||
|
81bfd7e5fb | ||
|
fd8a5e695d | ||
|
8f74dbdbb4 | ||
|
fd5a7eac87 | ||
|
6bac3c75cb | ||
|
5d6e0e3790 | ||
|
2871a326e6 | ||
|
abb42f0f36 | ||
|
f6fcb04817 | ||
|
b8796d825d | ||
|
e97259aca3 | ||
|
88f49834fd | ||
|
4415f52e18 | ||
|
98d742d634 | ||
|
6c1ca10be7 | ||
|
aeaf2d546a | ||
|
c7cb6664b4 | ||
|
79adba9284 | ||
|
37b7f52f2c | ||
|
c89e0ab255 | ||
|
f613f4f2df | ||
|
a497e4c920 | ||
|
0f637fb722 | ||
|
ba48c8e25b | ||
|
abbadc92a0 | ||
|
97fcbdd6d9 | ||
|
d7686b665e | ||
|
b8855afd10 | ||
|
5de41a3a7f | ||
|
84cdac83d6 | ||
|
436a55ee1e | ||
|
313cc2965c | ||
|
95f74c5ea7 | ||
|
8d1c3c754d | ||
|
72152f9d80 | ||
|
ebd365a128 | ||
|
c130d7cf5f | ||
|
0406e76889 | ||
|
c2b28c0f8d | ||
|
9aa5038756 | ||
|
a860f8f1a8 | ||
|
487c016a32 | ||
|
e4bddb4993 | ||
|
731afcb864 | ||
|
efb73ff4e7 | ||
|
2ed2f35a9b | ||
|
119151cad3 | ||
|
758f9b5aa5 | ||
|
e06a8c1de2 | ||
|
29ce8ca0cf | ||
|
eb4158df0b | ||
|
12fda1a36b | ||
|
e927f99777 | ||
|
044bcf55bd | ||
|
e475af9f49 | ||
|
c6abcd91fa | ||
|
10fc489822 | ||
|
d76d926c38 | ||
|
2b3c167845 | ||
|
1d75b974b5 | ||
|
584bb3a648 | ||
|
7b5ec6b98f | ||
|
23526f6d1a | ||
|
c0930ead0f | ||
|
809b3ee023 | ||
|
23f0c79fba | ||
|
81dd3809e9 | ||
|
2bf0c4497d | ||
|
93122bdd18 | ||
|
978550f809 | ||
|
84fea0fd05 | ||
|
2df3e0f881 | ||
|
c98117f69d | ||
|
ede5d1f890 | ||
|
39911e3acd | ||
|
3d1c15ef99 | ||
|
f718482e98 | ||
|
8dafd13cd7 | ||
|
0b19b28a64 | ||
|
c245f7ce3a | ||
|
607d647483 | ||
|
9a38a455c9 | ||
|
16e0738731 | ||
|
eacbf87979 | ||
|
108b4cb648 | ||
|
a9dff407a1 | ||
|
9e26109e36 | ||
|
6308a8dfcd | ||
|
4baf9527d7 | ||
|
199c459697 | ||
|
61288c5e68 | ||
|
8375237de5 | ||
|
c8d820c17b | ||
|
6319b8ef51 | ||
|
397a85eaa4 | ||
|
3889c4bdd9 | ||
|
c899f1cb85 | ||
|
d8956c51d0 | ||
|
5dd55c7cad | ||
|
c0e61d4c87 | ||
|
97e17282ab | ||
|
94c882af7d | ||
|
89c6d85f2f | ||
|
cf366c602f | ||
|
77ccab7d80 | ||
|
f51ba63742 | ||
|
9044518be5 | ||
|
9e0367eef4 | ||
|
235bb6c1b9 | ||
|
49344d7ea8 | ||
|
1b418d77ff | ||
|
80cc302627 | ||
|
8e1abc3f10 | ||
|
e498c6907a | ||
|
08e8fc6736 | ||
|
f6e9ef6de9 | ||
|
c51159672e | ||
|
233b51e29e | ||
|
54c8e13a68 | ||
|
405300b4b2 | ||
|
a6abd31ead | ||
|
4c26674ff4 | ||
|
40768e935b | ||
|
23be648456 | ||
|
13ee31770a | ||
|
93dc80000c | ||
|
e0cd3cd991 | ||
|
81ae501e73 | ||
|
9b781f8404 | ||
|
f797a92f87 | ||
|
0a579814a2 | ||
|
ec6c9bca62 | ||
|
a433bbbe45 | ||
|
8ca20f184d | ||
|
d160954080 | ||
|
14372e0ef0 | ||
|
03bffa27ac | ||
|
028b5a4f0d | ||
|
cd12f49fc0 | ||
|
a144749a8d | ||
|
1bd146fb8e | ||
|
5f6c3da7a4 | ||
|
d0aa754252 | ||
|
dbe9235f3a | ||
|
d78569986b | ||
|
95323e6caa | ||
|
f809d22fc6 | ||
|
763d61db8d | ||
|
10cad3abb2 | ||
|
9338f35cd8 | ||
|
ead6fa9daa | ||
|
ad660cf420 | ||
|
75f8ae2815 | ||
|
70aa04c047 | ||
|
4aa47e87f2 | ||
|
f8050816ac | ||
|
5b0a6d7ec1 | ||
|
3b4d08f52b | ||
|
6bbf40d7d2 | ||
|
d895f83520 | ||
|
f6b9e8c5eb | ||
|
98bcdf6028 | ||
|
9b385ec7cc | ||
|
5c040f7a46 | ||
|
46232c7fd4 | ||
|
c67d95c00f | ||
|
5e5aaf9a7e | ||
|
35996d0adb | ||
|
eaeb23d41e | ||
|
c71f6ad417 | ||
|
87a8593291 | ||
|
4799dd769e | ||
|
24b4606f96 | ||
|
9f672a0cf4 | ||
|
064bc5ee76 | ||
|
a52d78c8ee | ||
|
a00cabe223 | ||
|
dbe974f510 | ||
|
a284682deb | ||
|
07d7507ac6 | ||
|
c68d17d482 | ||
|
9e185e80ce | ||
|
676e7c7947 | ||
|
04212b2cef | ||
|
bafc2a1f30 | ||
|
563e388a45 | ||
|
d31d8ec5b0 | ||
|
2b00cd632d | ||
|
5f427d2b4c | ||
|
8c0ce4fc1d | ||
|
10a74f45ea | ||
|
320dad7f1a | ||
|
88ac72c8eb | ||
|
f74b9df0a7 | ||
|
a6f1335375 | ||
|
f321fa5ad3 | ||
|
03d999444d | ||
|
763ed260c3 | ||
|
764e7d1315 | ||
|
048f685073 | ||
|
e4d7958379 | ||
|
bdcbfb11a8 | ||
|
3f288e264b | ||
|
dd593c292c | ||
|
fa87c7e1b7 | ||
|
39c1857c61 | ||
|
c57a2d0dc3 | ||
|
a2e6616100 | ||
|
ba4513e82c | ||
|
6525b16e1f | ||
|
b6a92506d1 | ||
|
ffa0366deb | ||
|
00c4686ef0 | ||
|
3101b74580 | ||
|
4e694fdff6 | ||
|
194a6057dd | ||
|
e710e057e2 | ||
|
28188a6e59 | ||
|
70a5df96c8 | ||
|
460998d512 | ||
|
e741301417 | ||
|
5ed5298409 | ||
|
b911665691 | ||
|
56eb83319d | ||
|
1e6800565a | ||
|
c909120ae1 | ||
|
9894f37412 | ||
|
229c63c46d | ||
|
6a04cdfddf | ||
|
c70670bacb | ||
|
7bb3e44a76 | ||
|
b958acb76a | ||
|
b22f4fbb72 |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1,5 +1,6 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
polar: marginalia-search
|
||||
github: MarginaliaSearch
|
||||
patreon: marginalia_nu
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@@ -7,3 +7,11 @@ build/
|
||||
lombok.config
|
||||
Dockerfile
|
||||
run
|
||||
jte-classes
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
.factorypath
|
||||
bin/
|
||||
*.log
|
||||
*.hprof
|
||||
|
6
Additional Contributors.md
Normal file
6
Additional Contributors.md
Normal file
@@ -0,0 +1,6 @@
|
||||
Not everyone shows up in the git commit history, doesn't mean they didn't contribute valuable changes.
|
||||
In such circumstances, their deeds will be recorded here.
|
||||
|
||||
* [@samstorment](https://www.github.com/samstorment) provided a design overhaul for [https://explore.marginalia.nu/](https://explore.marginalia.nu/) in [10cad3](https://github.com/MarginaliaSearch/MarginaliaSearch/commit/10cad3abb29b8a87bf5fd56afbc192335e3e94d7)
|
||||
via [issue #44](https://github.com/MarginaliaSearch/MarginaliaSearch/issues/44).
|
||||
* [@dreimolo](https://github.com/dreimolo) provided build script [fixes for apple silicon](https://github.com/MarginaliaSearch/MarginaliaSearch/pull/64)
|
@@ -14,4 +14,7 @@ we can discuss its feasibility.
|
||||
Search is essentially a fractal of interesting
|
||||
problems, so even if you don't have an idea,
|
||||
just a skillset (really any), odds are there's
|
||||
something interesting I could point you to.
|
||||
something interesting I could point you to.
|
||||
|
||||
Make sure you check out the [ide-configuration guide](doc/ide-configuration.md)
|
||||
to get your IDE set up quickly and easily.
|
27
README.md
27
README.md
@@ -13,18 +13,33 @@ The long term plan is to refine the search engine so that it provide enough publ
|
||||
that the project can be funded through grants, donations and commercial API licenses
|
||||
(non-commercial share-alike is always free).
|
||||
|
||||
## Set up
|
||||
The system can both be run as a copy of Marginalia Search, or as a white-label search engine
|
||||
for your own data (either crawled or side-loaded). At present the logic isn't very configurable, and a lot of the judgements
|
||||
made are based on the Marginalia project's goals, but additional configurability is being
|
||||
worked on!
|
||||
|
||||
Start by running [⚙️ run/setup.sh](run/setup.sh). This will download supplementary model data that is necessary to run the code.
|
||||
These are also necessary to run the tests.
|
||||
Here's a demo of the set-up and operation of the self-hostable barebones mode of the search engine: [🌎 https://www.youtube.com/watch?v=PNwMkenQQ24](https://www.youtube.com/watch?v=PNwMkenQQ24)
|
||||
|
||||
## Set up
|
||||
|
||||
To set up a local test environment, follow the instructions in [📄 run/readme.md](run/readme.md)!
|
||||
|
||||
Further documentation is available at [🌎 https://docs.marginalia.nu/](https://docs.marginalia.nu/).
|
||||
|
||||
Before compiling, it's necessary to run [⚙️ run/setup.sh](run/setup.sh).
|
||||
This will download supplementary model data that is necessary to run the code.
|
||||
These are also necessary to run the tests.
|
||||
|
||||
If you wish to hack on the code, check out [📄 doc/ide-configuration.md](doc/ide-configuration.md).
|
||||
|
||||
## Hardware Requirements
|
||||
|
||||
A production-like environment requires at least 128 Gb of RAM and ideally 2 Tb+ of enterprise
|
||||
grade SSD storage, as well as some additional terabytes of slower harddrives for storing crawl
|
||||
data. It can be made to run on smaller hardware by limiting size of the index.
|
||||
A production-like environment requires a lot of RAM and ideally enterprise SSDs for
|
||||
the index, as well as some additional terabytes of slower harddrives for storing crawl
|
||||
data. It can be made to run on smaller hardware by limiting size of the index.
|
||||
|
||||
The system will definitely run on a 32 Gb machine, possibly smaller, but at that size it may not perform
|
||||
very well as it relies on disk caching to be fast.
|
||||
|
||||
A local developer's deployment is possible with much smaller hardware (and index size).
|
||||
|
||||
|
95
ROADMAP.md
Normal file
95
ROADMAP.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Roadmap 2025
|
||||
|
||||
This is a roadmap with major features planned for Marginalia Search.
|
||||
|
||||
It's not set in any particular order and other features will definitely
|
||||
be implemented as well.
|
||||
|
||||
Major goals:
|
||||
|
||||
* Reach 1 billion pages indexed
|
||||
|
||||
|
||||
* Improve technical ability of indexing and search. ~~Although this area has improved a bit, the
|
||||
search engine is still not very good at dealing with longer queries.~~ (As of PR [#129](https://github.com/MarginaliaSearch/MarginaliaSearch/pull/129), this has improved significantly. There is still more work to be done )
|
||||
|
||||
## Hybridize crawler w/ Common Crawl data
|
||||
|
||||
Sometimes Marginalia's relatively obscure crawler is blocked when attempting to crawl a website, or for
|
||||
other technical reasons it may be prevented from doing so. A possible work-around is to hybridize the
|
||||
crawler so that it attempts to fetch such inaccessible websites from common crawl. This is an important
|
||||
step on the road to 1 billion pages indexed.
|
||||
|
||||
As a rough sketch, the crawler would identify target websites, consume CC's index, and then fetch the WARC data
|
||||
with byte range queries.
|
||||
|
||||
Retaining the ability to independently crawl the web is still strongly desirable so going full CC is not an option.
|
||||
|
||||
## Safe Search
|
||||
|
||||
The search engine has a bit of a problem showing spicy content mixed in with the results. It would be desirable to have a way to filter this out. It's likely something like a URL blacklist (e.g. [UT1](https://dsi.ut-capitole.fr/blacklists/index_en.php) )
|
||||
combined with naive bayesian filter would go a long way, or something more sophisticated...?
|
||||
|
||||
## Additional Language Support
|
||||
|
||||
It would be desirable if the search engine supported more languages than English. This is partially about
|
||||
rooting out assumptions regarding character encoding, but there's most likely some amount of custom logic
|
||||
associated with each language added, at least a models file or two, as well as some fine tuning.
|
||||
|
||||
It would be very helpful to find a speaker of a large language other than English to help in the fine tuning.
|
||||
|
||||
## Custom ranking logic
|
||||
|
||||
Stract does an interesting thing where they have configurable search filters.
|
||||
|
||||
This looks like a good idea that wouldn't just help clean up the search filters on the main
|
||||
website, but might be cheap enough we might go as far as to offer a number of ad-hoc custom search
|
||||
filter for any API consumer.
|
||||
|
||||
I've talked to the stract dev and he does not think it's a good idea to mimic their optics language, which is quite ad-hoc, but instead to work together to find some new common description language for this.
|
||||
|
||||
## Specialized crawler for github
|
||||
|
||||
One of the search engine's biggest limitations right now is that it does not index github at all. A specialized crawler that fetches at least the readme.md would go a long way toward providing search capabilities in this domain.
|
||||
|
||||
# Completed
|
||||
|
||||
## Support for binary formats like PDF (COMPLETED 2025-05)
|
||||
|
||||
The crawler needs to be modified to retain them, and the conversion logic needs to parse them.
|
||||
The documents database probably should have some sort of flag indicating it's a PDF as well.
|
||||
|
||||
PDF parsing is known to be a bit of a security liability so some thought needs to be put in
|
||||
that direction as well.
|
||||
|
||||
## Show favicons next to search results (COMPLETED 2025-03)
|
||||
|
||||
This is expected from search engines. Basic proof of concept sketch of fetching this data has been done, but the feature is some way from being reality.
|
||||
|
||||
## Web Design Overhaul (COMPLETED 2025-01)
|
||||
|
||||
The design is kinda clunky and hard to maintain, and needlessly outdated-looking.
|
||||
|
||||
PR [#127](https://github.com/MarginaliaSearch/MarginaliaSearch/pull/127)
|
||||
|
||||
## Finalize RSS support (COMPLETED 2024-11)
|
||||
|
||||
Marginalia has experimental RSS preview support for a few domains. This works well and
|
||||
it should be extended to all domains. It would also be interesting to offer search of the
|
||||
RSS data itself, or use the RSS set to feed a special live index that updates faster than the
|
||||
main dataset.
|
||||
|
||||
Completed with PR [#122](https://github.com/MarginaliaSearch/MarginaliaSearch/pull/122) and PR [#125](https://github.com/MarginaliaSearch/MarginaliaSearch/pull/125)
|
||||
|
||||
## Proper Position Index (COMPLETED 2024-09)
|
||||
|
||||
The search engine uses a fixed width bit mask to indicate word positions. It has the benefit
|
||||
of being very fast to evaluate and works well for what it is, but is inaccurate and has the
|
||||
drawback of making support for quoted search terms inaccurate and largely reliant on indexing
|
||||
word n-grams known beforehand. This limits the ability to interpret longer queries.
|
||||
|
||||
The positions mask should be supplemented or replaced with a more accurate (e.g.) gamma coded positions
|
||||
list, as is the civilized way of doing this.
|
||||
|
||||
Completed with PR [#99](https://github.com/MarginaliaSearch/MarginaliaSearch/pull/99)
|
||||
|
78
build.gradle
78
build.gradle
@@ -1,6 +1,12 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id("org.jetbrains.gradle.plugin.idea-ext") version "1.0"
|
||||
id "me.champeau.jmh" version "0.7.3"
|
||||
|
||||
// This is a workaround for a bug in the Jib plugin that causes it to stall randomly
|
||||
// https://github.com/GoogleContainerTools/jib/issues/3347
|
||||
id 'com.google.cloud.tools.jib' version '3.4.5' apply(false)
|
||||
id 'com.adarshr.test-logger' version '4.0.0'
|
||||
}
|
||||
|
||||
group 'marginalia'
|
||||
@@ -9,50 +15,64 @@ version 'SNAPSHOT'
|
||||
compileJava.options.encoding = "UTF-8"
|
||||
compileTestJava.options.encoding = "UTF-8"
|
||||
|
||||
tasks.register('dist', Copy) {
|
||||
from subprojects.collect { it.tasks.withType(Tar) }
|
||||
into "$buildDir/dist"
|
||||
subprojects.forEach {it ->
|
||||
// Enable preview features for the entire project
|
||||
|
||||
doLast {
|
||||
copy {
|
||||
from tarTree("$buildDir/dist/converter-process.tar")
|
||||
into "$projectDir/run/dist/"
|
||||
}
|
||||
copy {
|
||||
from tarTree("$buildDir/dist/crawler-process.tar")
|
||||
into "$projectDir/run/dist/"
|
||||
}
|
||||
copy {
|
||||
from tarTree("$buildDir/dist/loader-process.tar")
|
||||
into "$projectDir/run/dist/"
|
||||
}
|
||||
copy {
|
||||
from tarTree("$buildDir/dist/website-adjacencies-calculator.tar")
|
||||
into "$projectDir/run/dist/"
|
||||
}
|
||||
copy {
|
||||
from tarTree("$buildDir/dist/crawl-job-extractor-process.tar")
|
||||
into "$projectDir/run/dist/"
|
||||
}
|
||||
if (it.path.contains(':code:')) {
|
||||
sourceSets.main.java.srcDirs += file('java')
|
||||
sourceSets.main.resources.srcDirs += file('resources')
|
||||
sourceSets.test.java.srcDirs += file('test')
|
||||
sourceSets.test.resources.srcDirs += file('test-resources')
|
||||
}
|
||||
|
||||
it.tasks.withType(JavaCompile).configureEach {
|
||||
options.compilerArgs += ['--enable-preview']
|
||||
}
|
||||
it.tasks.withType(JavaExec).configureEach {
|
||||
jvmArgs += ['--enable-preview']
|
||||
}
|
||||
it.tasks.withType(Test).configureEach {
|
||||
jvmArgs += ['--enable-preview',
|
||||
'--enable-native-access=ALL-UNNAMED',
|
||||
'--sun-misc-unsafe-memory-access=allow',
|
||||
'-Dsystem.uringQueueCount=1']
|
||||
}
|
||||
|
||||
// Enable reproducible builds for the entire project
|
||||
it.tasks.withType(AbstractArchiveTask).configureEach {
|
||||
preserveFileTimestamps = false
|
||||
reproducibleFileOrder = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ext {
|
||||
jvmVersion = 25
|
||||
dockerImageBase='container-registry.oracle.com/graalvm/jdk:25'
|
||||
dockerImageTag='latest'
|
||||
dockerImageRegistry='marginalia'
|
||||
jibVersion = '3.4.5'
|
||||
}
|
||||
|
||||
idea {
|
||||
module {
|
||||
// Exclude these directories from being indexed by IntelliJ
|
||||
// as they tend to bring the IDE to its knees and use up all
|
||||
// Inotify spots in a hurry
|
||||
excludeDirs.add(file("$projectDir/run/node-1"))
|
||||
excludeDirs.add(file("$projectDir/run/node-2"))
|
||||
excludeDirs.add(file("$projectDir/run/model"))
|
||||
excludeDirs.add(file("$projectDir/run/dist"))
|
||||
excludeDirs.add(file("$projectDir/run/samples"))
|
||||
excludeDirs.add(file("$projectDir/run/db"))
|
||||
excludeDirs.add(file("$projectDir/run/logs"))
|
||||
excludeDirs.add(file("$projectDir/run/install"))
|
||||
excludeDirs.add(file("$projectDir/run/data"))
|
||||
excludeDirs.add(file("$projectDir/run/conf"))
|
||||
excludeDirs.add(file("$projectDir/run/vol"))
|
||||
excludeDirs.add(file("$projectDir/run/test-data"))
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(20))
|
||||
languageVersion.set(JavaLanguageVersion.of(rootProject.ext.jvmVersion))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,43 +0,0 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id "io.freefair.lombok" version "8.2.2"
|
||||
|
||||
id 'jvm-test-suite'
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(20))
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
implementation project(':code:common:model')
|
||||
implementation project(':code:common:config')
|
||||
implementation project(':code:common:service-discovery')
|
||||
implementation project(':code:common:service-client')
|
||||
|
||||
implementation libs.lombok
|
||||
annotationProcessor libs.lombok
|
||||
implementation libs.bundles.slf4j
|
||||
|
||||
implementation libs.prometheus
|
||||
implementation libs.notnull
|
||||
implementation libs.guice
|
||||
implementation libs.rxjava
|
||||
implementation libs.gson
|
||||
|
||||
testImplementation libs.bundles.slf4j.test
|
||||
testImplementation libs.bundles.junit
|
||||
testImplementation libs.mockito
|
||||
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
task fastTests(type: Test) {
|
||||
useJUnitPlatform {
|
||||
excludeTags "slow"
|
||||
}
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
# Assistant API
|
||||
|
||||
Client and models for talking to the [assistant-service](../../services-core/assistant-service),
|
||||
implemented with the base client from [service-client](../../common/service-client).
|
||||
|
||||
## Central Classes
|
||||
|
||||
* [AssistantClient](src/main/java/nu/marginalia/assistant/client/AssistantClient.java)
|
@@ -1,62 +0,0 @@
|
||||
package nu.marginalia.assistant.client;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import nu.marginalia.assistant.client.model.DictionaryResponse;
|
||||
import nu.marginalia.client.AbstractDynamicClient;
|
||||
import nu.marginalia.client.exception.RouteNotConfiguredException;
|
||||
import nu.marginalia.WmsaHome;
|
||||
import nu.marginalia.model.gson.GsonFactory;
|
||||
import nu.marginalia.service.descriptor.ServiceDescriptors;
|
||||
import nu.marginalia.service.id.ServiceId;
|
||||
import nu.marginalia.client.Context;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
@Singleton
|
||||
public class AssistantClient extends AbstractDynamicClient {
|
||||
|
||||
@Inject
|
||||
public AssistantClient(ServiceDescriptors descriptors) {
|
||||
super(descriptors.forId(ServiceId.Assistant), WmsaHome.getHostsFile(), GsonFactory::get);
|
||||
}
|
||||
|
||||
public Observable<DictionaryResponse> dictionaryLookup(Context ctx, String word) {
|
||||
try {
|
||||
return super.get(ctx, "/dictionary/" + URLEncoder.encode(word, StandardCharsets.UTF_8), DictionaryResponse.class);
|
||||
}
|
||||
catch (RouteNotConfiguredException ex) {
|
||||
return Observable.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Observable<List<String>> spellCheck(Context ctx, String word) {
|
||||
try {
|
||||
return (Observable<List<String>>) (Object) super.get(ctx, "/spell-check/" + URLEncoder.encode(word, StandardCharsets.UTF_8), List.class);
|
||||
}
|
||||
catch (RouteNotConfiguredException ex) {
|
||||
return Observable.empty();
|
||||
}
|
||||
}
|
||||
public Observable<String> unitConversion(Context ctx, String value, String from, String to) {
|
||||
try {
|
||||
return super.get(ctx, "/unit-conversion?value=" + value + "&from=" + from + "&to=" + to);
|
||||
}
|
||||
catch (RouteNotConfiguredException ex) {
|
||||
return Observable.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public Observable<String> evalMath(Context ctx, String expression) {
|
||||
try {
|
||||
return super.get(ctx, "/eval-expression?value=" + URLEncoder.encode(expression, StandardCharsets.UTF_8));
|
||||
}
|
||||
catch (RouteNotConfiguredException ex) {
|
||||
return Observable.empty();
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
package nu.marginalia.assistant.client.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@ToString
|
||||
public class DictionaryEntry {
|
||||
public final String type;
|
||||
public final String word;
|
||||
public final String definition;
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
package nu.marginalia.assistant.client.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ToString @Getter @AllArgsConstructor @NoArgsConstructor
|
||||
public class DictionaryResponse {
|
||||
public String word;
|
||||
public List<DictionaryEntry> entries;
|
||||
}
|
@@ -1,47 +0,0 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id "io.freefair.lombok" version "8.2.2"
|
||||
|
||||
id 'jvm-test-suite'
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(20))
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':code:common:model')
|
||||
implementation project(':code:common:config')
|
||||
implementation project(':code:common:service-discovery')
|
||||
implementation project(':code:common:service-client')
|
||||
implementation project(':code:libraries:message-queue')
|
||||
implementation project(':code:features-index:index-query')
|
||||
|
||||
implementation libs.lombok
|
||||
annotationProcessor libs.lombok
|
||||
implementation libs.bundles.slf4j
|
||||
|
||||
implementation libs.prometheus
|
||||
implementation libs.notnull
|
||||
implementation libs.guice
|
||||
implementation libs.rxjava
|
||||
implementation libs.protobuf
|
||||
implementation libs.bundles.gson
|
||||
implementation libs.fastutil
|
||||
|
||||
testImplementation libs.bundles.slf4j.test
|
||||
testImplementation libs.bundles.junit
|
||||
testImplementation libs.mockito
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
task fastTests(type: Test) {
|
||||
useJUnitPlatform {
|
||||
excludeTags "slow"
|
||||
}
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
# Index API
|
||||
|
||||
Client and models for talking to the [index-service](../../services-core/index-service),
|
||||
implemented with the base client from [service-client](../../common/service-client).
|
||||
|
||||
## Central Classes
|
||||
|
||||
* [IndexClient](src/main/java/nu/marginalia/index/client/IndexClient.java)
|
@@ -1,59 +0,0 @@
|
||||
package nu.marginalia.index.client;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import io.prometheus.client.Summary;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import nu.marginalia.WmsaHome;
|
||||
import nu.marginalia.client.AbstractDynamicClient;
|
||||
import nu.marginalia.client.Context;
|
||||
import nu.marginalia.index.client.model.query.SearchSpecification;
|
||||
import nu.marginalia.index.client.model.results.SearchResultSet;
|
||||
import nu.marginalia.model.gson.GsonFactory;
|
||||
import nu.marginalia.mq.MessageQueueFactory;
|
||||
import nu.marginalia.mq.outbox.MqOutbox;
|
||||
import nu.marginalia.service.descriptor.ServiceDescriptors;
|
||||
import nu.marginalia.service.id.ServiceId;
|
||||
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import java.util.UUID;
|
||||
|
||||
@Singleton
|
||||
public class IndexClient extends AbstractDynamicClient {
|
||||
|
||||
private static final Summary wmsa_search_index_api_time = Summary.build().name("wmsa_search_index_api_time").help("-").register();
|
||||
|
||||
private final MqOutbox outbox;
|
||||
|
||||
@Inject
|
||||
public IndexClient(ServiceDescriptors descriptors,
|
||||
MessageQueueFactory messageQueueFactory) {
|
||||
super(descriptors.forId(ServiceId.Index), WmsaHome.getHostsFile(), GsonFactory::get);
|
||||
|
||||
String inboxName = ServiceId.Index.name + ":" + "0";
|
||||
String outboxName = System.getProperty("service-name", UUID.randomUUID().toString());
|
||||
|
||||
outbox = messageQueueFactory.createOutbox(inboxName, outboxName, UUID.randomUUID());
|
||||
|
||||
setTimeout(30);
|
||||
}
|
||||
|
||||
|
||||
public MqOutbox outbox() {
|
||||
return outbox;
|
||||
}
|
||||
|
||||
@CheckReturnValue
|
||||
public SearchResultSet query(Context ctx, SearchSpecification specs) {
|
||||
return wmsa_search_index_api_time.time(
|
||||
() -> this.postGet(ctx, "/search/", specs, SearchResultSet.class).blockingFirst()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@CheckReturnValue
|
||||
public Observable<Boolean> isBlocked(Context ctx) {
|
||||
return super.get(ctx, "/is-blocked", Boolean.class);
|
||||
}
|
||||
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
package nu.marginalia.index.client;
|
||||
|
||||
public class IndexMqEndpoints {
|
||||
public static final String INDEX_IS_BLOCKED = "INDEX-IS-BLOCKED";
|
||||
public static final String INDEX_REPARTITION = "INDEX-REPARTITION";
|
||||
|
||||
public static final String INDEX_RELOAD_LEXICON = "INDEX-RELOAD-LEXICON";
|
||||
public static final String INDEX_REINDEX = "INDEX-REINDEX";
|
||||
|
||||
}
|
@@ -1,32 +0,0 @@
|
||||
package nu.marginalia.index.client.model.query;
|
||||
|
||||
import lombok.*;
|
||||
import nu.marginalia.index.client.model.results.ResultRankingParameters;
|
||||
import nu.marginalia.index.query.limit.QueryLimits;
|
||||
import nu.marginalia.index.query.limit.QueryStrategy;
|
||||
import nu.marginalia.index.query.limit.SpecificationLimit;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ToString @Getter @Builder @With @AllArgsConstructor
|
||||
public class SearchSpecification {
|
||||
public List<SearchSubquery> subqueries;
|
||||
|
||||
/** If present and not empty, limit the search to these domain IDs */
|
||||
public List<Integer> domains;
|
||||
|
||||
public SearchSetIdentifier searchSetIdentifier;
|
||||
|
||||
public final String humanQuery;
|
||||
|
||||
public final SpecificationLimit quality;
|
||||
public final SpecificationLimit year;
|
||||
public final SpecificationLimit size;
|
||||
public final SpecificationLimit rank;
|
||||
|
||||
public final QueryLimits queryLimits;
|
||||
|
||||
public final QueryStrategy queryStrategy;
|
||||
|
||||
public final ResultRankingParameters rankingParams;
|
||||
}
|
@@ -1,64 +0,0 @@
|
||||
package nu.marginalia.index.client.model.query;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public class SearchSubquery {
|
||||
|
||||
/** These terms must be present in the document and are used in ranking*/
|
||||
public final List<String> searchTermsInclude;
|
||||
|
||||
/** These terms must be absent from the document */
|
||||
public final List<String> searchTermsExclude;
|
||||
|
||||
/** These terms must be present in the document, but are not used in ranking */
|
||||
public final List<String> searchTermsAdvice;
|
||||
|
||||
/** If these optional terms are present in the document, rank it highly */
|
||||
public final List<String> searchTermsPriority;
|
||||
|
||||
/** Terms that we require to be in the same sentence */
|
||||
public final List<List<String>> searchTermCoherences;
|
||||
|
||||
private double value = 0;
|
||||
|
||||
public SearchSubquery(List<String> searchTermsInclude,
|
||||
List<String> searchTermsExclude,
|
||||
List<String> searchTermsAdvice,
|
||||
List<String> searchTermsPriority,
|
||||
List<List<String>> searchTermCoherences) {
|
||||
this.searchTermsInclude = searchTermsInclude;
|
||||
this.searchTermsExclude = searchTermsExclude;
|
||||
this.searchTermsAdvice = searchTermsAdvice;
|
||||
this.searchTermsPriority = searchTermsPriority;
|
||||
this.searchTermCoherences = searchTermCoherences;
|
||||
}
|
||||
|
||||
public SearchSubquery setValue(double value) {
|
||||
if (Double.isInfinite(value) || Double.isNaN(value)) {
|
||||
this.value = Double.MAX_VALUE;
|
||||
} else {
|
||||
this.value = value;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (!searchTermsInclude.isEmpty()) sb.append("include=").append(searchTermsInclude.stream().collect(Collectors.joining(",", "[", "] ")));
|
||||
if (!searchTermsExclude.isEmpty()) sb.append("exclude=").append(searchTermsExclude.stream().collect(Collectors.joining(",", "[", "] ")));
|
||||
if (!searchTermsAdvice.isEmpty()) sb.append("advice=").append(searchTermsAdvice.stream().collect(Collectors.joining(",", "[", "] ")));
|
||||
if (!searchTermsPriority.isEmpty()) sb.append("priority=").append(searchTermsPriority.stream().collect(Collectors.joining(",", "[", "] ")));
|
||||
if (!searchTermCoherences.isEmpty()) sb.append("coherences=").append(searchTermCoherences.stream().map(coh->coh.stream().collect(Collectors.joining(",", "[", "] "))).collect(Collectors.joining(", ")));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
package nu.marginalia.index.client.model.results;
|
||||
|
||||
/** Tuning parameters for BM25.
|
||||
*
|
||||
* @param k determines the size of the impact of a single term
|
||||
* @param b determines the magnitude of the length normalization
|
||||
*
|
||||
* @see nu.marginalia.ranking.factors.Bm25Factor
|
||||
*/
|
||||
public record Bm25Parameters(double k, double b) {
|
||||
}
|
@@ -1,38 +0,0 @@
|
||||
package nu.marginalia.index.client.model.results;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@ToString
|
||||
public class ResultRankingContext {
|
||||
private final int docCount;
|
||||
public final ResultRankingParameters params;
|
||||
|
||||
private final Object2IntOpenHashMap<String> fullCounts = new Object2IntOpenHashMap<>(10, 0.5f);
|
||||
private final Object2IntOpenHashMap<String> priorityCounts = new Object2IntOpenHashMap<>(10, 0.5f);
|
||||
|
||||
public ResultRankingContext(int docCount,
|
||||
ResultRankingParameters params,
|
||||
Map<String, Integer> fullCounts,
|
||||
Map<String, Integer> prioCounts
|
||||
) {
|
||||
this.docCount = docCount;
|
||||
this.params = params;
|
||||
this.fullCounts.putAll(fullCounts);
|
||||
this.priorityCounts.putAll(prioCounts);
|
||||
}
|
||||
|
||||
public int termFreqDocCount() {
|
||||
return docCount;
|
||||
}
|
||||
|
||||
public int frequency(String keyword) {
|
||||
return fullCounts.getOrDefault(keyword, 1);
|
||||
}
|
||||
|
||||
public int priorityFrequency(String keyword) {
|
||||
return priorityCounts.getOrDefault(keyword, 1);
|
||||
}
|
||||
}
|
@@ -1,60 +0,0 @@
|
||||
package nu.marginalia.index.client.model.results;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
|
||||
@Builder @AllArgsConstructor
|
||||
public class ResultRankingParameters {
|
||||
|
||||
/** Tuning for BM25 when applied to full document matches */
|
||||
public final Bm25Parameters fullParams;
|
||||
/** Tuning for BM25 when applied to priority matches, terms with relevance signal indicators */
|
||||
public final Bm25Parameters prioParams;
|
||||
|
||||
/** Documents below this length are penalized */
|
||||
public int shortDocumentThreshold;
|
||||
|
||||
public double shortDocumentPenalty;
|
||||
|
||||
|
||||
/** Scaling factor associated with domain rank (unscaled rank value is 0-255; high is good) */
|
||||
public double domainRankBonus;
|
||||
|
||||
/** Scaling factor associated with document quality (unscaled rank value is 0-15; high is bad) */
|
||||
public double qualityPenalty;
|
||||
|
||||
/** Average sentence length values below this threshold are penalized, range [0-4), 2 or 3 is probably what you want */
|
||||
public int shortSentenceThreshold;
|
||||
|
||||
/** Magnitude of penalty for documents with low average sentence length */
|
||||
public double shortSentencePenalty;
|
||||
|
||||
public double bm25FullWeight;
|
||||
public double bm25PrioWeight;
|
||||
public double tcfWeight;
|
||||
|
||||
public TemporalBias temporalBias;
|
||||
public double temporalBiasWeight;
|
||||
|
||||
public static ResultRankingParameters sensibleDefaults() {
|
||||
return builder()
|
||||
.fullParams(new Bm25Parameters(1.2, 0.5))
|
||||
.prioParams(new Bm25Parameters(1.5, 0))
|
||||
.shortDocumentThreshold(2000)
|
||||
.shortDocumentPenalty(2.)
|
||||
.domainRankBonus(1/25.)
|
||||
.qualityPenalty(1/15.)
|
||||
.shortSentenceThreshold(2)
|
||||
.shortSentencePenalty(5)
|
||||
.bm25FullWeight(1.)
|
||||
.bm25PrioWeight(1.)
|
||||
.tcfWeight(2.)
|
||||
.temporalBias(TemporalBias.NONE)
|
||||
.temporalBiasWeight(1. / (10.))
|
||||
.build();
|
||||
}
|
||||
|
||||
public enum TemporalBias {
|
||||
RECENT, OLD, NONE
|
||||
};
|
||||
}
|
@@ -1,84 +0,0 @@
|
||||
package nu.marginalia.index.client.model.results;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import nu.marginalia.model.EdgeUrl;
|
||||
import nu.marginalia.model.id.EdgeId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Represents a document matching a search query */
|
||||
@AllArgsConstructor @Getter
|
||||
public class SearchResultItem {
|
||||
/** Encoded ID that contains both the URL id and its ranking */
|
||||
public final long combinedId;
|
||||
|
||||
/** How did the subqueries match against the document ? */
|
||||
public final List<SearchResultKeywordScore> keywordScores;
|
||||
|
||||
/** How many other potential results existed in the same domain */
|
||||
public int resultsFromDomain;
|
||||
|
||||
public SearchResultItem(long val) {
|
||||
this.combinedId = val;
|
||||
this.keywordScores = new ArrayList<>(16);
|
||||
}
|
||||
|
||||
public EdgeId<EdgeUrl> getUrlId() {
|
||||
return new EdgeId<>(getUrlIdInt());
|
||||
}
|
||||
|
||||
public int getUrlIdInt() {
|
||||
return (int)(combinedId & 0xFFFF_FFFFL);
|
||||
}
|
||||
public int getRanking() {
|
||||
return (int)(combinedId >>> 32);
|
||||
}
|
||||
|
||||
/* Used for evaluation */
|
||||
private transient SearchResultPreliminaryScore scoreValue = null;
|
||||
public void setScore(SearchResultPreliminaryScore score) {
|
||||
scoreValue = score;
|
||||
}
|
||||
public SearchResultPreliminaryScore getScore() {
|
||||
return scoreValue;
|
||||
}
|
||||
|
||||
private transient int domainId = Integer.MIN_VALUE;
|
||||
public void setDomainId(int domainId) {
|
||||
this.domainId = domainId;
|
||||
}
|
||||
public int getDomainId() {
|
||||
return this.domainId;
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return getUrlIdInt();
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + "[ url= " + getUrlId() + ", rank=" + getRanking() + "]";
|
||||
}
|
||||
|
||||
public boolean equals(Object other) {
|
||||
if (other == null)
|
||||
return false;
|
||||
if (other == this)
|
||||
return true;
|
||||
if (other instanceof SearchResultItem o) {
|
||||
return o.getUrlIdInt() == getUrlIdInt();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public long deduplicationKey() {
|
||||
final int domainId = getDomainId();
|
||||
|
||||
if (domainId == Integer.MAX_VALUE || domainId == Integer.MIN_VALUE) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return domainId;
|
||||
}
|
||||
}
|
@@ -1,99 +0,0 @@
|
||||
package nu.marginalia.index.client.model.results;
|
||||
|
||||
import nu.marginalia.model.idx.WordFlags;
|
||||
import nu.marginalia.model.idx.WordMetadata;
|
||||
import nu.marginalia.model.idx.DocumentMetadata;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class SearchResultKeywordScore {
|
||||
public final int subquery;
|
||||
public final String keyword;
|
||||
private final long encodedWordMetadata;
|
||||
private final long encodedDocMetadata;
|
||||
private final boolean hasPriorityTerms;
|
||||
|
||||
private final int htmlFeatures;
|
||||
|
||||
public SearchResultKeywordScore(int subquery,
|
||||
String keyword,
|
||||
long encodedWordMetadata,
|
||||
long encodedDocMetadata,
|
||||
int htmlFeatures,
|
||||
boolean hasPriorityTerms) {
|
||||
this.subquery = subquery;
|
||||
this.keyword = keyword;
|
||||
this.encodedWordMetadata = encodedWordMetadata;
|
||||
this.encodedDocMetadata = encodedDocMetadata;
|
||||
this.htmlFeatures = htmlFeatures;
|
||||
this.hasPriorityTerms = hasPriorityTerms;
|
||||
}
|
||||
|
||||
public boolean hasTermFlag(WordFlags flag) {
|
||||
return WordMetadata.hasFlags(encodedWordMetadata, flag.asBit());
|
||||
}
|
||||
|
||||
public int positionCount() {
|
||||
return Long.bitCount(positions());
|
||||
}
|
||||
|
||||
public int subquery() {
|
||||
return subquery;
|
||||
}
|
||||
public long positions() {
|
||||
return WordMetadata.decodePositions(encodedWordMetadata);
|
||||
}
|
||||
|
||||
public boolean isKeywordSpecial() {
|
||||
return keyword.contains(":") || hasTermFlag(WordFlags.Synthetic);
|
||||
}
|
||||
|
||||
public boolean isKeywordRegular() {
|
||||
return !keyword.contains(":")
|
||||
&& !hasTermFlag(WordFlags.Synthetic);
|
||||
}
|
||||
|
||||
public long encodedWordMetadata() {
|
||||
return encodedWordMetadata;
|
||||
}
|
||||
|
||||
public long encodedDocMetadata() {
|
||||
return encodedDocMetadata;
|
||||
}
|
||||
|
||||
public int htmlFeatures() {
|
||||
return htmlFeatures;
|
||||
}
|
||||
|
||||
public boolean hasPriorityTerms() {
|
||||
return hasPriorityTerms;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) return true;
|
||||
if (obj == null || obj.getClass() != this.getClass()) return false;
|
||||
var that = (SearchResultKeywordScore) obj;
|
||||
return this.subquery == that.subquery &&
|
||||
Objects.equals(this.keyword, that.keyword) &&
|
||||
this.encodedWordMetadata == that.encodedWordMetadata &&
|
||||
this.encodedDocMetadata == that.encodedDocMetadata &&
|
||||
this.hasPriorityTerms == that.hasPriorityTerms;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(subquery, keyword, encodedWordMetadata, encodedDocMetadata, hasPriorityTerms);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SearchResultKeywordScore[" +
|
||||
"set=" + subquery + ", " +
|
||||
"keyword=" + keyword + ", " +
|
||||
"encodedWordMetadata=" + new WordMetadata(encodedWordMetadata) + ", " +
|
||||
"encodedDocMetadata=" + new DocumentMetadata(encodedDocMetadata) + ", " +
|
||||
"hasPriorityTerms=" + hasPriorityTerms + ']';
|
||||
}
|
||||
|
||||
}
|
@@ -1,31 +0,0 @@
|
||||
package nu.marginalia.index.client.model.results;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import static java.lang.Boolean.compare;
|
||||
import static java.lang.Double.compare;
|
||||
|
||||
public record SearchResultPreliminaryScore(
|
||||
boolean disqualified,
|
||||
boolean hasPriorityTerm,
|
||||
double searchRankingScore)
|
||||
implements Comparable<SearchResultPreliminaryScore>
|
||||
{
|
||||
|
||||
final static int PREFER_HIGH = 1;
|
||||
final static int PREFER_LOW = -1;
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull SearchResultPreliminaryScore other) {
|
||||
int diff;
|
||||
|
||||
diff = PREFER_HIGH * compare(hasPriorityTerm, other.hasPriorityTerm);
|
||||
if (diff != 0) return diff;
|
||||
|
||||
return PREFER_LOW * compare(searchRankingScore, other.searchRankingScore);
|
||||
}
|
||||
|
||||
public boolean isDisqualified() {
|
||||
return disqualified;
|
||||
}
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
package nu.marginalia.index.client.model.results;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@AllArgsConstructor @Getter @ToString
|
||||
public class SearchResultSet {
|
||||
public List<SearchResultItem> results;
|
||||
public ResultRankingContext rankingContext;
|
||||
public int size() {
|
||||
return results.size();
|
||||
}
|
||||
}
|
@@ -1,30 +0,0 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id "io.freefair.lombok" version "8.2.2"
|
||||
|
||||
id 'jvm-test-suite'
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(20))
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':code:common:db')
|
||||
|
||||
testImplementation libs.bundles.slf4j.test
|
||||
testImplementation libs.bundles.junit
|
||||
testImplementation libs.mockito
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
task fastTests(type: Test) {
|
||||
useJUnitPlatform {
|
||||
excludeTags "slow"
|
||||
}
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
package nu.marginalia.mqapi;
|
||||
|
||||
public class ProcessInboxNames {
|
||||
public static final String CONVERTER_INBOX = "converter";
|
||||
public static final String LOADER_INBOX = "loader";
|
||||
public static final String CRAWLER_INBOX = "crawler";
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
package nu.marginalia.mqapi.converting;
|
||||
|
||||
public enum ConvertAction {
|
||||
ConvertCrawlData,
|
||||
SideloadEncyclopedia,
|
||||
SideloadStackexchange
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
package nu.marginalia.mqapi.converting;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import nu.marginalia.db.storage.model.FileStorageId;
|
||||
|
||||
@AllArgsConstructor
|
||||
public class ConvertRequest {
|
||||
public final ConvertAction action;
|
||||
public final String inputSource;
|
||||
public final FileStorageId crawlStorage;
|
||||
public final FileStorageId processedDataStorage;
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
package nu.marginalia.mqapi.crawling;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import nu.marginalia.db.storage.model.FileStorageId;
|
||||
|
||||
/** A request to start a crawl */
|
||||
@AllArgsConstructor
|
||||
public class CrawlRequest {
|
||||
public FileStorageId specStorage;
|
||||
public FileStorageId crawlStorage;
|
||||
}
|
@@ -1,9 +0,0 @@
|
||||
package nu.marginalia.mqapi.loading;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import nu.marginalia.db.storage.model.FileStorageId;
|
||||
|
||||
@AllArgsConstructor
|
||||
public class LoadRequest {
|
||||
public FileStorageId processedDataStorage;
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
# Clients
|
||||
|
||||
## Core Services
|
||||
|
||||
* [assistant-api](assistant-api/)
|
||||
* [search-api](search-api/)
|
||||
* [index-api](index-api/)
|
||||
|
||||
These are clients for the [core services](../services-core/), along with what models
|
||||
are necessary for speaking to them. They each implement the abstract client classes from
|
||||
[service-client](../common/service-client).
|
||||
|
||||
All that is necessary is to `@Inject` them into the constructor and then
|
||||
requests can be sent.
|
||||
|
||||
**Note:** If you are looking for the public API, it's handled by the api service in [services-satellite/api-service](../services-satellite/api-service).
|
||||
|
||||
## MQ-API Process API
|
||||
|
||||
[process-mqapi](process-mqapi/) defines requests and inboxes for the message queue based API used
|
||||
for interacting with processes.
|
||||
|
||||
See [libraries/message-queue](../libraries/message-queue) and [services-satellite/control-service](../services-satellite/control-service).
|
@@ -1,45 +0,0 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id "io.freefair.lombok" version "8.2.2"
|
||||
|
||||
id 'jvm-test-suite'
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(20))
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':code:common:model')
|
||||
implementation project(':code:common:config')
|
||||
implementation project(':code:libraries:message-queue')
|
||||
implementation project(':code:common:service-discovery')
|
||||
implementation project(':code:common:service-client')
|
||||
|
||||
implementation libs.lombok
|
||||
annotationProcessor libs.lombok
|
||||
implementation libs.bundles.slf4j
|
||||
|
||||
implementation libs.prometheus
|
||||
implementation libs.notnull
|
||||
implementation libs.guice
|
||||
implementation libs.rxjava
|
||||
implementation libs.gson
|
||||
|
||||
testImplementation libs.bundles.slf4j.test
|
||||
testImplementation libs.bundles.junit
|
||||
testImplementation libs.mockito
|
||||
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
task fastTests(type: Test) {
|
||||
useJUnitPlatform {
|
||||
excludeTags "slow"
|
||||
}
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
# Search API
|
||||
|
||||
Client and models for talking to the [search-service](../../services-core/search-service),
|
||||
implemented with the base client from [service-client](../../common/service-client).
|
||||
|
||||
## Central Classes
|
||||
|
||||
* [SearchClient](src/main/java/nu/marginalia/search/client/SearchClient.java)
|
@@ -1,52 +0,0 @@
|
||||
package nu.marginalia.search.client;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import nu.marginalia.client.AbstractDynamicClient;
|
||||
import nu.marginalia.model.gson.GsonFactory;
|
||||
import nu.marginalia.mq.MessageQueueFactory;
|
||||
import nu.marginalia.mq.outbox.MqOutbox;
|
||||
import nu.marginalia.search.client.model.ApiSearchResults;
|
||||
import nu.marginalia.service.descriptor.ServiceDescriptors;
|
||||
import nu.marginalia.service.id.ServiceId;
|
||||
import nu.marginalia.WmsaHome;
|
||||
import nu.marginalia.client.Context;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
@Singleton
|
||||
public class SearchClient extends AbstractDynamicClient {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
private final MqOutbox outbox;
|
||||
|
||||
@Inject
|
||||
public SearchClient(ServiceDescriptors descriptors,
|
||||
MessageQueueFactory messageQueueFactory) {
|
||||
|
||||
super(descriptors.forId(ServiceId.Search), WmsaHome.getHostsFile(), GsonFactory::get);
|
||||
|
||||
String inboxName = ServiceId.Search.name + ":" + "0";
|
||||
String outboxName = System.getProperty("service-name", UUID.randomUUID().toString());
|
||||
|
||||
outbox = messageQueueFactory.createOutbox(inboxName, outboxName, UUID.randomUUID());
|
||||
|
||||
}
|
||||
|
||||
|
||||
public MqOutbox outbox() {
|
||||
return outbox;
|
||||
}
|
||||
|
||||
@CheckReturnValue
|
||||
public Observable<ApiSearchResults> query(Context ctx, String queryString, int count, int profile) {
|
||||
return this.get(ctx, String.format("/api/search?query=%s&count=%d&index=%d", URLEncoder.encode(queryString, StandardCharsets.UTF_8), count, profile), ApiSearchResults.class);
|
||||
}
|
||||
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
package nu.marginalia.search.client;
|
||||
|
||||
public class SearchMqEndpoints {
|
||||
/** Flushes the URL caches, run if significant changes have occurred in the URLs database */
|
||||
public static final String FLUSH_CACHES = "FLUSH_CACHES";
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
package nu.marginalia.search.client.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@AllArgsConstructor @Getter
|
||||
public class ApiSearchResult {
|
||||
public String url;
|
||||
public String title;
|
||||
public String description;
|
||||
public double quality;
|
||||
|
||||
public List<List<ApiSearchResultQueryDetails>> details = new ArrayList<>();
|
||||
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
package nu.marginalia.search.client.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@AllArgsConstructor @Getter
|
||||
public class ApiSearchResultQueryDetails {
|
||||
|
||||
String keyword;
|
||||
int count;
|
||||
|
||||
Set<String> flagsUnstableAPI;
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
package nu.marginalia.search.client.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.With;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@With
|
||||
public class ApiSearchResults {
|
||||
private final String license;
|
||||
|
||||
private final String query;
|
||||
private final List<ApiSearchResult> results;
|
||||
}
|
@@ -1,31 +1,42 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id "io.freefair.lombok" version "8.2.2"
|
||||
|
||||
|
||||
id 'jvm-test-suite'
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(20))
|
||||
languageVersion.set(JavaLanguageVersion.of(rootProject.ext.jvmVersion))
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "$rootProject.projectDir/srcsets.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation project(':code:common:service-discovery')
|
||||
implementation project(':code:common:service-client')
|
||||
}
|
||||
implementation project(':code:common:db')
|
||||
implementation project(':code:common:model')
|
||||
|
||||
test {
|
||||
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
|
||||
maxHeapSize = "8G"
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
task fastTests(type: Test) {
|
||||
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
|
||||
maxHeapSize = "8G"
|
||||
useJUnitPlatform {
|
||||
excludeTags "slow"
|
||||
implementation libs.bundles.slf4j
|
||||
implementation libs.bundles.mariadb
|
||||
implementation libs.bundles.httpcomponents
|
||||
implementation libs.mockito
|
||||
implementation libs.guava
|
||||
implementation dependencies.create(libs.guice.get()) {
|
||||
exclude group: 'com.google.guava'
|
||||
}
|
||||
implementation libs.gson
|
||||
|
||||
testImplementation libs.bundles.slf4j.test
|
||||
testImplementation libs.bundles.junit
|
||||
|
||||
|
||||
testImplementation project(':code:libraries:test-helpers')
|
||||
|
||||
testImplementation platform('org.testcontainers:testcontainers-bom:1.17.4')
|
||||
testImplementation libs.commons.codec
|
||||
testImplementation 'org.testcontainers:mariadb:1.17.4'
|
||||
testImplementation 'org.testcontainers:junit-jupiter:1.17.4'
|
||||
testImplementation project(':code:libraries:test-helpers')
|
||||
|
||||
}
|
||||
|
67
code/common/config/java/nu/marginalia/IndexLocations.java
Normal file
67
code/common/config/java/nu/marginalia/IndexLocations.java
Normal file
@@ -0,0 +1,67 @@
|
||||
package nu.marginalia;
|
||||
|
||||
import nu.marginalia.storage.FileStorageService;
|
||||
import nu.marginalia.storage.model.FileStorageBaseType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/** The IndexLocations class is responsible for knowledge about the locations
|
||||
* of various important system paths. The methods take a FileStorageService,
|
||||
* as these paths are node-dependent.
|
||||
*/
|
||||
public class IndexLocations {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(IndexLocations.class);
|
||||
/** Return the path to the current link database */
|
||||
public static Path getLinkdbLivePath(FileStorageService fileStorage) {
|
||||
return getStorage(fileStorage, FileStorageBaseType.CURRENT, "ldbr");
|
||||
}
|
||||
|
||||
/** Return the path to the next link database */
|
||||
public static Path getLinkdbWritePath(FileStorageService fileStorage) {
|
||||
return getStorage(fileStorage, FileStorageBaseType.CURRENT, "ldbw");
|
||||
}
|
||||
|
||||
/** Return the path to the current live index */
|
||||
public static Path getCurrentIndex(FileStorageService fileStorage) {
|
||||
return getStorage(fileStorage, FileStorageBaseType.CURRENT, "ir");
|
||||
}
|
||||
|
||||
/** Return the path to the designated index construction area */
|
||||
public static Path getIndexConstructionArea(FileStorageService fileStorage) {
|
||||
return getStorage(fileStorage, FileStorageBaseType.CURRENT, "iw");
|
||||
}
|
||||
|
||||
/** Return the path to the search sets */
|
||||
public static Path getSearchSetsPath(FileStorageService fileStorage) {
|
||||
return getStorage(fileStorage, FileStorageBaseType.CURRENT, "ss");
|
||||
}
|
||||
|
||||
private static Path getStorage(FileStorageService service, FileStorageBaseType baseType, String pathPart) {
|
||||
try {
|
||||
var base = service.getStorageBase(baseType);
|
||||
if (base == null) {
|
||||
throw new IllegalStateException("File storage base " + baseType + " is not configured!");
|
||||
}
|
||||
|
||||
// Ensure the directory exists
|
||||
Path ret = base.asPath().resolve(pathPart);
|
||||
if (!Files.exists(ret)) {
|
||||
logger.info("Creating system directory {}", ret);
|
||||
|
||||
Files.createDirectories(ret);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
catch (SQLException | IOException ex) {
|
||||
throw new IllegalStateException("Error fetching storage " + baseType + " / " + pathPart, ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
27
code/common/config/java/nu/marginalia/LanguageModels.java
Normal file
27
code/common/config/java/nu/marginalia/LanguageModels.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package nu.marginalia;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class LanguageModels {
|
||||
public final Path termFrequencies;
|
||||
|
||||
public final Path openNLPSentenceDetectionData;
|
||||
public final Path posRules;
|
||||
public final Path posDict;
|
||||
public final Path fasttextLanguageModel;
|
||||
public final Path segments;
|
||||
|
||||
public LanguageModels(Path termFrequencies,
|
||||
Path openNLPSentenceDetectionData,
|
||||
Path posRules,
|
||||
Path posDict,
|
||||
Path fasttextLanguageModel,
|
||||
Path segments) {
|
||||
this.termFrequencies = termFrequencies;
|
||||
this.openNLPSentenceDetectionData = openNLPSentenceDetectionData;
|
||||
this.posRules = posRules;
|
||||
this.posDict = posDict;
|
||||
this.fasttextLanguageModel = fasttextLanguageModel;
|
||||
this.segments = segments;
|
||||
}
|
||||
}
|
8
code/common/config/java/nu/marginalia/UserAgent.java
Normal file
8
code/common/config/java/nu/marginalia/UserAgent.java
Normal file
@@ -0,0 +1,8 @@
|
||||
package nu.marginalia;
|
||||
|
||||
/**
|
||||
* A record representing a User Agent.
|
||||
* @param uaString - the header value of the User Agent
|
||||
* @param uaIdentifier - what we look for in robots.txt
|
||||
*/
|
||||
public record UserAgent(String uaString, String uaIdentifier) {}
|
120
code/common/config/java/nu/marginalia/WmsaHome.java
Normal file
120
code/common/config/java/nu/marginalia/WmsaHome.java
Normal file
@@ -0,0 +1,120 @@
|
||||
package nu.marginalia;
|
||||
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class WmsaHome {
|
||||
public static UserAgent getUserAgent() {
|
||||
return new UserAgent(
|
||||
System.getProperty("crawler.userAgentString", "Mozilla/5.0 (compatible; Marginalia-like bot; +https://git.marginalia.nu/))"),
|
||||
System.getProperty("crawler.userAgentIdentifier", "search.marginalia.nu")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public static Path getUploadDir() {
|
||||
return Path.of(
|
||||
System.getProperty("executor.uploadDir", "/uploads")
|
||||
);
|
||||
}
|
||||
|
||||
public static Path getHomePath() {
|
||||
String[] possibleLocations = new String[] {
|
||||
System.getenv("WMSA_HOME"),
|
||||
System.getProperty("system.homePath"),
|
||||
"/var/lib/wmsa",
|
||||
"/wmsa"
|
||||
};
|
||||
|
||||
Optional<String> retStr = Stream.of(possibleLocations)
|
||||
.filter(Objects::nonNull)
|
||||
.map(Path::of)
|
||||
.filter(Files::isDirectory)
|
||||
.map(Path::toString)
|
||||
.findFirst();
|
||||
|
||||
if (retStr.isEmpty()) {
|
||||
// Check parent directories for a fingerprint of the project's installation boilerplate
|
||||
var prodRoot = Stream.iterate(Paths.get("").toAbsolutePath(), f -> f != null && Files.exists(f), Path::getParent)
|
||||
.filter(p -> Files.exists(p.resolve("conf/properties/system.properties")))
|
||||
.filter(p -> Files.exists(p.resolve("model/tfreq-new-algo3.bin")))
|
||||
.findAny();
|
||||
if (prodRoot.isPresent()) {
|
||||
return prodRoot.get();
|
||||
}
|
||||
|
||||
// Check if we are running in a test environment by looking for fingerprints
|
||||
// matching the base of the source tree for the project, then looking up the
|
||||
// run directory which contains a template for the installation we can use as
|
||||
// though it's the project root for testing purposes
|
||||
|
||||
var testRoot = Stream.iterate(Paths.get("").toAbsolutePath(), f -> f != null && Files.exists(f), Path::getParent)
|
||||
.filter(p -> Files.exists(p.resolve("run/env")))
|
||||
.filter(p -> Files.exists(p.resolve("run/setup.sh")))
|
||||
.map(p -> p.resolve("run"))
|
||||
.findAny();
|
||||
|
||||
return testRoot.orElseThrow(() -> new IllegalStateException("""
|
||||
Could not find $WMSA_HOME, either set environment
|
||||
variable, the 'system.homePath' java property,
|
||||
or ensure either /wmsa or /var/lib/wmsa exists
|
||||
"""));
|
||||
}
|
||||
|
||||
var ret = Path.of(retStr.get());
|
||||
|
||||
if (!Files.isDirectory(ret.resolve("model"))) {
|
||||
throw new IllegalStateException("You need to run 'run/setup.sh' to download models to run/ before this will work!");
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static Path getDataPath() {
|
||||
return getHomePath().resolve("data");
|
||||
}
|
||||
|
||||
public static Path getAdsDefinition() {
|
||||
return getHomePath().resolve("data").resolve("adblock.txt");
|
||||
}
|
||||
|
||||
public static Path getIPLocationDatabse() {
|
||||
return getHomePath().resolve("data").resolve("IP2LOCATION-LITE-DB1.CSV");
|
||||
|
||||
}
|
||||
|
||||
public static Path getAsnMappingDatabase() {
|
||||
return getHomePath().resolve("data").resolve("asn-data-raw-table");
|
||||
}
|
||||
|
||||
public static Path getAsnInfoDatabase() {
|
||||
return getHomePath().resolve("data").resolve("asn-used-autnums");
|
||||
}
|
||||
|
||||
public static LanguageModels getLanguageModels() {
|
||||
final Path home = getHomePath();
|
||||
|
||||
return new LanguageModels(
|
||||
home.resolve("model/tfreq-new-algo3.bin"),
|
||||
home.resolve("model/opennlp-sentence.bin"),
|
||||
home.resolve("model/English.RDR"),
|
||||
home.resolve("model/English.DICT"),
|
||||
home.resolve("model/lid.176.ftz"),
|
||||
home.resolve("model/segments.bin")
|
||||
);
|
||||
}
|
||||
|
||||
public static Path getAtagsPath() {
|
||||
return getHomePath().resolve("data/atags.parquet");
|
||||
}
|
||||
|
||||
|
||||
public static Path getLangugeConfig() {
|
||||
return getHomePath().resolve("conf/languages.xml");
|
||||
}
|
||||
}
|
@@ -0,0 +1,126 @@
|
||||
package nu.marginalia.nodecfg;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import nu.marginalia.nodecfg.model.NodeConfiguration;
|
||||
import nu.marginalia.nodecfg.model.NodeProfile;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class NodeConfigurationService {
|
||||
private final Logger logger = LoggerFactory.getLogger(NodeConfigurationService.class);
|
||||
|
||||
private final HikariDataSource dataSource;
|
||||
|
||||
@Inject
|
||||
public NodeConfigurationService(HikariDataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
public NodeConfiguration create(int id, String description, boolean acceptQueries, boolean keepWarcs, NodeProfile nodeProfile) throws SQLException {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var is = conn.prepareStatement("""
|
||||
INSERT IGNORE INTO NODE_CONFIGURATION(ID, DESCRIPTION, ACCEPT_QUERIES, KEEP_WARCS, NODE_PROFILE) VALUES(?, ?, ?, ?, ?)
|
||||
""")
|
||||
)
|
||||
{
|
||||
is.setInt(1, id);
|
||||
is.setString(2, description);
|
||||
is.setBoolean(3, acceptQueries);
|
||||
is.setBoolean(4, keepWarcs);
|
||||
is.setString(5, nodeProfile.name());
|
||||
|
||||
if (is.executeUpdate() <= 0) {
|
||||
throw new IllegalStateException("Failed to insert configuration");
|
||||
}
|
||||
|
||||
return get(id);
|
||||
}
|
||||
}
|
||||
|
||||
public List<NodeConfiguration> getAll() {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var qs = conn.prepareStatement("""
|
||||
SELECT ID, DESCRIPTION, ACCEPT_QUERIES, AUTO_CLEAN, PRECESSION, AUTO_ASSIGN_DOMAINS, KEEP_WARCS, NODE_PROFILE, DISABLED
|
||||
FROM NODE_CONFIGURATION
|
||||
""")) {
|
||||
var rs = qs.executeQuery();
|
||||
|
||||
List<NodeConfiguration> ret = new ArrayList<>();
|
||||
|
||||
while (rs.next()) {
|
||||
ret.add(new NodeConfiguration(
|
||||
rs.getInt("ID"),
|
||||
rs.getString("DESCRIPTION"),
|
||||
rs.getBoolean("ACCEPT_QUERIES"),
|
||||
rs.getBoolean("AUTO_CLEAN"),
|
||||
rs.getBoolean("PRECESSION"),
|
||||
rs.getBoolean("AUTO_ASSIGN_DOMAINS"),
|
||||
rs.getBoolean("KEEP_WARCS"),
|
||||
NodeProfile.valueOf(rs.getString("NODE_PROFILE")),
|
||||
rs.getBoolean("DISABLED")
|
||||
));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
logger.warn("Failed to get node configurations", ex);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
public NodeConfiguration get(int nodeId) throws SQLException {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var qs = conn.prepareStatement("""
|
||||
SELECT ID, DESCRIPTION, ACCEPT_QUERIES, AUTO_CLEAN, PRECESSION, AUTO_ASSIGN_DOMAINS, KEEP_WARCS, NODE_PROFILE, DISABLED
|
||||
FROM NODE_CONFIGURATION
|
||||
WHERE ID=?
|
||||
""")) {
|
||||
qs.setInt(1, nodeId);
|
||||
var rs = qs.executeQuery();
|
||||
if (rs.next()) {
|
||||
return new NodeConfiguration(
|
||||
rs.getInt("ID"),
|
||||
rs.getString("DESCRIPTION"),
|
||||
rs.getBoolean("ACCEPT_QUERIES"),
|
||||
rs.getBoolean("AUTO_CLEAN"),
|
||||
rs.getBoolean("PRECESSION"),
|
||||
rs.getBoolean("AUTO_ASSIGN_DOMAINS"),
|
||||
rs.getBoolean("KEEP_WARCS"),
|
||||
NodeProfile.valueOf(rs.getString("NODE_PROFILE")),
|
||||
rs.getBoolean("DISABLED")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void save(NodeConfiguration config) throws SQLException {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var us = conn.prepareStatement("""
|
||||
UPDATE NODE_CONFIGURATION
|
||||
SET DESCRIPTION=?, ACCEPT_QUERIES=?, AUTO_CLEAN=?, PRECESSION=?, AUTO_ASSIGN_DOMAINS=?, KEEP_WARCS=?, DISABLED=?, NODE_PROFILE=?
|
||||
WHERE ID=?
|
||||
"""))
|
||||
{
|
||||
us.setString(1, config.description());
|
||||
us.setBoolean(2, config.acceptQueries());
|
||||
us.setBoolean(3, config.autoClean());
|
||||
us.setBoolean(4, config.includeInPrecession());
|
||||
us.setBoolean(5, config.autoAssignDomains());
|
||||
us.setBoolean(6, config.keepWarcs());
|
||||
us.setBoolean(7, config.disabled());
|
||||
us.setString(8, config.profile().name());
|
||||
us.setInt(9, config.node());
|
||||
|
||||
if (us.executeUpdate() <= 0)
|
||||
throw new IllegalStateException("Failed to update configuration");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package nu.marginalia.nodecfg.model;
|
||||
|
||||
public record NodeConfiguration(int node,
|
||||
String description,
|
||||
boolean acceptQueries,
|
||||
boolean autoClean,
|
||||
boolean includeInPrecession,
|
||||
boolean autoAssignDomains,
|
||||
boolean keepWarcs,
|
||||
NodeProfile profile,
|
||||
boolean disabled
|
||||
)
|
||||
{
|
||||
public int getId() {
|
||||
return node;
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
package nu.marginalia.nodecfg.model;
|
||||
|
||||
public enum NodeProfile {
|
||||
BATCH_CRAWL,
|
||||
REALTIME,
|
||||
MIXED,
|
||||
SIDELOAD;
|
||||
|
||||
public boolean isBatchCrawl() {
|
||||
return this == BATCH_CRAWL;
|
||||
}
|
||||
public boolean isRealtime() {
|
||||
return this == REALTIME;
|
||||
}
|
||||
public boolean isMixed() {
|
||||
return this == MIXED;
|
||||
}
|
||||
public boolean isSideload() {
|
||||
return this == SIDELOAD;
|
||||
}
|
||||
|
||||
public boolean permitBatchCrawl() {
|
||||
return isBatchCrawl() || isMixed();
|
||||
}
|
||||
public boolean permitSideload() { return isSideload() || isMixed(); }
|
||||
}
|
@@ -0,0 +1,141 @@
|
||||
package nu.marginalia.proxy;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Configuration for SOCKS proxy settings used by crawlers to distribute IP footprint.
|
||||
*/
|
||||
public class SocksProxyConfiguration {
|
||||
|
||||
private final boolean enabled;
|
||||
private final List<SocksProxy> proxies;
|
||||
private final ProxySelectionStrategy strategy;
|
||||
|
||||
public SocksProxyConfiguration() {
|
||||
this.enabled = Boolean.parseBoolean(System.getProperty("crawler.socksProxy.enabled", "false"));
|
||||
this.strategy = ProxySelectionStrategy.valueOf(
|
||||
System.getProperty("crawler.socksProxy.strategy", "ROUND_ROBIN")
|
||||
);
|
||||
this.proxies = parseProxies();
|
||||
}
|
||||
|
||||
private List<SocksProxy> parseProxies() {
|
||||
String proxyList = System.getProperty("crawler.socksProxy.list", "");
|
||||
if (proxyList.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return Arrays.stream(proxyList.split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.map(this::parseProxy)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private SocksProxy parseProxy(String proxyString) {
|
||||
try {
|
||||
// Expected format: "host:port" or "host:port:username:password"
|
||||
String[] parts = proxyString.split(":");
|
||||
if (parts.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String host = parts[0];
|
||||
int port = Integer.parseInt(parts[1]);
|
||||
|
||||
if (parts.length >= 4) {
|
||||
String username = parts[2];
|
||||
String password = parts[3];
|
||||
return new SocksProxy(host, port, username, password);
|
||||
} else {
|
||||
return new SocksProxy(host, port);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled && !proxies.isEmpty();
|
||||
}
|
||||
|
||||
public List<SocksProxy> getProxies() {
|
||||
return proxies;
|
||||
}
|
||||
|
||||
public ProxySelectionStrategy getStrategy() {
|
||||
return strategy;
|
||||
}
|
||||
|
||||
public enum ProxySelectionStrategy {
|
||||
ROUND_ROBIN,
|
||||
RANDOM
|
||||
}
|
||||
|
||||
public static class SocksProxy {
|
||||
private final String host;
|
||||
private final int port;
|
||||
private final String username;
|
||||
private final String password;
|
||||
|
||||
public SocksProxy(String host, int port) {
|
||||
this(host, port, null, null);
|
||||
}
|
||||
|
||||
public SocksProxy(String host, int port, String username, String password) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public boolean hasAuthentication() {
|
||||
return username != null && password != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (hasAuthentication()) {
|
||||
return String.format("%s:%d (auth: %s)", host, port, username);
|
||||
} else {
|
||||
return String.format("%s:%d", host, port);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SocksProxy that = (SocksProxy) o;
|
||||
return port == that.port &&
|
||||
Objects.equals(host, that.host) &&
|
||||
Objects.equals(username, that.username) &&
|
||||
Objects.equals(password, that.password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(host, port, username, password);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
package nu.marginalia.proxy;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Manages SOCKS proxy selection and rotation for crawler requests.
|
||||
*/
|
||||
public class SocksProxyManager {
|
||||
private static final Logger logger = LoggerFactory.getLogger(SocksProxyManager.class);
|
||||
|
||||
private final SocksProxyConfiguration config;
|
||||
private final AtomicInteger roundRobinIndex = new AtomicInteger(0);
|
||||
|
||||
public SocksProxyManager(SocksProxyConfiguration config) {
|
||||
this.config = config;
|
||||
|
||||
if (config.isEnabled()) {
|
||||
logger.info("SOCKS proxy support enabled with {} proxies using {} strategy",
|
||||
config.getProxies().size(), config.getStrategy());
|
||||
for (SocksProxyConfiguration.SocksProxy proxy : config.getProxies()) {
|
||||
logger.info(" - {}", proxy);
|
||||
}
|
||||
} else {
|
||||
logger.info("SOCKS proxy support disabled");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the next proxy to use based on the configured strategy.
|
||||
*/
|
||||
@Nonnull
|
||||
public SocksProxyConfiguration.SocksProxy selectProxy() {
|
||||
if (!config.isEnabled()) {
|
||||
throw new IllegalStateException("Proxies not configured");
|
||||
}
|
||||
|
||||
List<SocksProxyConfiguration.SocksProxy> proxies = config.getProxies();
|
||||
if (proxies.isEmpty()) {
|
||||
throw new IllegalStateException("Proxies not configured");
|
||||
}
|
||||
|
||||
SocksProxyConfiguration.SocksProxy selectedProxy;
|
||||
switch (config.getStrategy()) {
|
||||
case ROUND_ROBIN:
|
||||
int index = roundRobinIndex.getAndIncrement() % proxies.size();
|
||||
selectedProxy = proxies.get(index);
|
||||
break;
|
||||
case RANDOM:
|
||||
int randomIndex = ThreadLocalRandom.current().nextInt(proxies.size());
|
||||
selectedProxy = proxies.get(randomIndex);
|
||||
break;
|
||||
default:
|
||||
selectedProxy = proxies.get(0);
|
||||
break;
|
||||
}
|
||||
|
||||
return selectedProxy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current proxy configuration.
|
||||
*/
|
||||
public SocksProxyConfiguration getConfiguration() {
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if proxy support is enabled and proxies are available.
|
||||
*/
|
||||
public boolean isProxyEnabled() {
|
||||
return config.isEnabled() && !config.getProxies().isEmpty();
|
||||
}
|
||||
}
|
@@ -1,9 +1,9 @@
|
||||
package nu.marginalia.db.storage;
|
||||
package nu.marginalia.storage;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import nu.marginalia.db.storage.model.FileStorage;
|
||||
import nu.marginalia.db.storage.model.FileStorageType;
|
||||
import nu.marginalia.model.gson.GsonFactory;
|
||||
import nu.marginalia.storage.model.FileStorage;
|
||||
import nu.marginalia.storage.model.FileStorageType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@@ -0,0 +1,582 @@
|
||||
package nu.marginalia.storage;
|
||||
|
||||
import com.google.inject.name.Named;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import nu.marginalia.storage.model.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/** Manages file storage for processes and services
|
||||
*/
|
||||
@Singleton
|
||||
public class FileStorageService {
|
||||
private final HikariDataSource dataSource;
|
||||
private final int node;
|
||||
private final Logger logger = LoggerFactory.getLogger(FileStorageService.class);
|
||||
|
||||
private static final DateTimeFormatter dirNameDatePattern = DateTimeFormatter.ofPattern("__uu-MM-dd'T'HH_mm_ss.SSS"); // filesystem safe ISO8601
|
||||
|
||||
@Inject
|
||||
public FileStorageService(HikariDataSource dataSource,
|
||||
@Named("wmsa-system-node") Integer node) {
|
||||
this.dataSource = dataSource;
|
||||
this.node = node;
|
||||
|
||||
logger.info("Resolving file storage root into {}", resolveStoragePath("/").toAbsolutePath());
|
||||
}
|
||||
|
||||
/** Resolve a storage path from a relative path, injecting the system configured storage root
|
||||
* if set */
|
||||
public static Path resolveStoragePath(String path) {
|
||||
if (path.startsWith("/")) {
|
||||
// Since Path.of("ANYTHING").resolve("/foo") = "/foo", we need to strip
|
||||
// the leading slash
|
||||
return resolveStoragePath(path.substring(1));
|
||||
}
|
||||
|
||||
return Path
|
||||
.of(System.getProperty("storage.root", "/"))
|
||||
.resolve(path);
|
||||
}
|
||||
|
||||
/** @return the storage base with the given id, or null if it does not exist */
|
||||
public FileStorageBase getStorageBase(FileStorageBaseId id) throws SQLException {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
SELECT ID, NAME, NODE, PATH, TYPE
|
||||
FROM FILE_STORAGE_BASE WHERE ID = ?
|
||||
""")) {
|
||||
stmt.setLong(1, id.id());
|
||||
try (var rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return new FileStorageBase(
|
||||
new FileStorageBaseId(rs.getLong("ID")),
|
||||
FileStorageBaseType.valueOf(rs.getString("TYPE")),
|
||||
rs.getInt("NODE"),
|
||||
rs.getString("NAME"),
|
||||
rs.getString("PATH")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void synchronizeStorageManifests(FileStorageBase base) {
|
||||
Set<String> ignoredPaths = new HashSet<>();
|
||||
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
SELECT FILE_STORAGE.PATH
|
||||
FROM FILE_STORAGE INNER JOIN FILE_STORAGE_BASE
|
||||
ON BASE_ID = FILE_STORAGE_BASE.ID
|
||||
WHERE BASE_ID = ?
|
||||
AND NODE = ?
|
||||
""")) {
|
||||
|
||||
stmt.setLong(1, base.id().id());
|
||||
stmt.setInt(2, node);
|
||||
|
||||
var rs = stmt.executeQuery();
|
||||
while (rs.next()) {
|
||||
ignoredPaths.add(rs.getString(1));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
File basePathFile = base.asPath().toFile();
|
||||
File[] files = basePathFile.listFiles(pathname -> pathname.isDirectory() && !ignoredPaths.contains(pathname.getName()));
|
||||
if (files == null) return;
|
||||
for (File file : files) {
|
||||
var maybeManifest = FileStorageManifest.find(file.toPath());
|
||||
if (maybeManifest.isEmpty()) continue;
|
||||
var manifest = maybeManifest.get();
|
||||
|
||||
logger.info("Discovered new file storage: " + file.getName() + " (" + manifest.type() + ")");
|
||||
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
INSERT INTO FILE_STORAGE(BASE_ID, PATH, TYPE, DESCRIPTION)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""")) {
|
||||
stmt.setLong(1, base.id().id());
|
||||
stmt.setString(2, file.getName());
|
||||
stmt.setString(3, manifest.type().name());
|
||||
stmt.setString(4, manifest.description());
|
||||
stmt.execute();
|
||||
conn.commit();
|
||||
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void relateFileStorages(FileStorageId source, FileStorageId target) {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
INSERT INTO FILE_STORAGE_RELATION(SOURCE_ID, TARGET_ID) VALUES (?, ?)
|
||||
""")) {
|
||||
stmt.setLong(1, source.id());
|
||||
stmt.setLong(2, target.id());
|
||||
stmt.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public List<FileStorage> getSourceFromStorage(FileStorage storage) throws SQLException {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
SELECT SOURCE_ID FROM FILE_STORAGE_RELATION WHERE TARGET_ID = ?
|
||||
""")) {
|
||||
stmt.setLong(1, storage.id().id());
|
||||
var rs = stmt.executeQuery();
|
||||
List<FileStorage> ret = new ArrayList<>();
|
||||
while (rs.next()) {
|
||||
ret.add(getStorage(new FileStorageId(rs.getLong(1))));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return the storage base with the given type, or null if it does not exist */
|
||||
public FileStorageBase getStorageBase(FileStorageBaseType type) throws SQLException {
|
||||
return getStorageBase(type, node);
|
||||
}
|
||||
|
||||
public FileStorageBase getStorageBase(FileStorageBaseType type, int node) throws SQLException {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
SELECT ID, NAME, NODE, PATH, TYPE
|
||||
FROM FILE_STORAGE_BASE WHERE TYPE = ? AND NODE = ?
|
||||
""")) {
|
||||
stmt.setString(1, type.name());
|
||||
stmt.setInt(2, node);
|
||||
try (var rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return new FileStorageBase(
|
||||
new FileStorageBaseId(rs.getLong("ID")),
|
||||
FileStorageBaseType.valueOf(rs.getString("TYPE")),
|
||||
rs.getInt("NODE"),
|
||||
rs.getString("NAME"),
|
||||
rs.getString("PATH")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public FileStorageBase createStorageBase(String name, Path path, FileStorageBaseType type) throws SQLException {
|
||||
return createStorageBase(name, path, node, type);
|
||||
}
|
||||
|
||||
public FileStorageBase createStorageBase(String name, Path path, int node, FileStorageBaseType type) throws SQLException {
|
||||
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
INSERT INTO FILE_STORAGE_BASE(NAME, PATH, TYPE, NODE)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""")) {
|
||||
stmt.setString(1, name);
|
||||
stmt.setString(2, path.toString());
|
||||
stmt.setString(3, type.name());
|
||||
stmt.setInt(4, node);
|
||||
|
||||
int update = stmt.executeUpdate();
|
||||
if (update < 0) {
|
||||
throw new SQLException("Failed to create storage base");
|
||||
}
|
||||
}
|
||||
|
||||
return getStorageBase(type);
|
||||
}
|
||||
|
||||
private Path allocateDirectory(Path basePath, String prefix) throws IOException {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String timestampPart = now.format(dirNameDatePattern);
|
||||
Path maybePath = basePath.resolve(prefix + timestampPart);
|
||||
|
||||
try {
|
||||
Files.createDirectory(maybePath,
|
||||
PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxr-xr-x"))
|
||||
);
|
||||
}
|
||||
catch (FileAlreadyExistsException ex) {
|
||||
// in case of a race condition, try again with some random cruft at the end
|
||||
maybePath = basePath.resolve(prefix + timestampPart + "_" + Long.toHexString(ThreadLocalRandom.current().nextLong()));
|
||||
|
||||
Files.createDirectory(maybePath,
|
||||
PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxr-xr-x"))
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure umask didn't mess with the access permissions
|
||||
Files.setPosixFilePermissions(maybePath, PosixFilePermissions.fromString("rwxr-xr-x"));
|
||||
|
||||
return maybePath;
|
||||
}
|
||||
|
||||
/** Allocate a storage area of the given type */
|
||||
public FileStorage allocateStorage(FileStorageType type,
|
||||
String prefix,
|
||||
String description) throws IOException, SQLException
|
||||
{
|
||||
var base = getStorageBase(FileStorageBaseType.forFileStorageType(type));
|
||||
|
||||
if (null == base)
|
||||
throw new IllegalStateException("No storage base for type " + type + " on node " + node);
|
||||
|
||||
Path newDir = allocateDirectory(base.asPath(), prefix);
|
||||
|
||||
String relDir = base.asPath().relativize(newDir).normalize().toString();
|
||||
|
||||
try (var conn = dataSource.getConnection();
|
||||
var insert = conn.prepareStatement("""
|
||||
INSERT INTO FILE_STORAGE(PATH, TYPE, DESCRIPTION, BASE_ID)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""");
|
||||
var query = conn.prepareStatement("""
|
||||
SELECT ID FROM FILE_STORAGE WHERE PATH = ? AND BASE_ID = ?
|
||||
""")
|
||||
) {
|
||||
insert.setString(1, relDir);
|
||||
insert.setString(2, type.name());
|
||||
insert.setString(3, description);
|
||||
insert.setLong(4, base.id().id());
|
||||
|
||||
if (insert.executeUpdate() < 1) {
|
||||
throw new SQLException("Failed to insert storage");
|
||||
}
|
||||
|
||||
|
||||
query.setString(1, relDir);
|
||||
query.setLong(2, base.id().id());
|
||||
var rs = query.executeQuery();
|
||||
|
||||
if (rs.next()) {
|
||||
var storage = getStorage(new FileStorageId(rs.getLong("ID")));
|
||||
|
||||
// Write a manifest file so we can pick this up later without needing to insert it into DB
|
||||
// (e.g. when loading from outside the system)
|
||||
var manifest = new FileStorageManifest(type, description);
|
||||
manifest.write(storage);
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
throw new SQLException("Failed to insert storage");
|
||||
}
|
||||
|
||||
|
||||
public FileStorage getStorageByType(FileStorageType type) throws SQLException {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
SELECT PATH, STATE, DESCRIPTION, ID, BASE_ID, CREATE_DATE
|
||||
FROM FILE_STORAGE_VIEW WHERE TYPE = ? AND NODE = ?
|
||||
""")) {
|
||||
stmt.setString(1, type.name());
|
||||
stmt.setInt(2, node);
|
||||
|
||||
long storageId;
|
||||
long baseId;
|
||||
String path;
|
||||
String state;
|
||||
String description;
|
||||
LocalDateTime createDateTime;
|
||||
|
||||
try (var rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
baseId = rs.getLong("BASE_ID");
|
||||
storageId = rs.getLong("ID");
|
||||
createDateTime = rs.getTimestamp("CREATE_DATE").toLocalDateTime();
|
||||
path = rs.getString("PATH");
|
||||
state = rs.getString("STATE");
|
||||
description = rs.getString("DESCRIPTION");
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
|
||||
var base = getStorageBase(new FileStorageBaseId(baseId));
|
||||
|
||||
return new FileStorage(
|
||||
new FileStorageId(storageId),
|
||||
base,
|
||||
type,
|
||||
createDateTime,
|
||||
path,
|
||||
FileStorageState.parse(state),
|
||||
description
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<FileStorage> getStorage(List<FileStorageId> ids) throws SQLException {
|
||||
List<FileStorage> ret = new ArrayList<>();
|
||||
for (var id : ids) {
|
||||
var storage = getStorage(id);
|
||||
if (storage == null) continue;
|
||||
ret.add(storage);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/** @return the storage with the given id, or null if it does not exist */
|
||||
public FileStorage getStorage(FileStorageId id) throws SQLException {
|
||||
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
SELECT PATH, TYPE, STATE, DESCRIPTION, CREATE_DATE, ID, BASE_ID
|
||||
FROM FILE_STORAGE_VIEW WHERE ID = ?
|
||||
""")) {
|
||||
stmt.setLong(1, id.id());
|
||||
|
||||
long storageId;
|
||||
long baseId;
|
||||
String path;
|
||||
String state;
|
||||
String description;
|
||||
FileStorageType type;
|
||||
LocalDateTime createDateTime;
|
||||
|
||||
try (var rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
baseId = rs.getLong("BASE_ID");
|
||||
storageId = rs.getLong("ID");
|
||||
type = FileStorageType.valueOf(rs.getString("TYPE"));
|
||||
path = rs.getString("PATH");
|
||||
state = rs.getString("STATE");
|
||||
description = rs.getString("DESCRIPTION");
|
||||
createDateTime = rs.getTimestamp("CREATE_DATE").toLocalDateTime();
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
|
||||
var base = getStorageBase(new FileStorageBaseId(baseId));
|
||||
|
||||
return new FileStorage(
|
||||
new FileStorageId(storageId),
|
||||
base,
|
||||
type,
|
||||
createDateTime,
|
||||
path,
|
||||
FileStorageState.parse(state),
|
||||
description
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void deregisterFileStorage(FileStorageId id) throws SQLException {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
DELETE FROM FILE_STORAGE WHERE ID = ?
|
||||
""")) {
|
||||
stmt.setLong(1, id.id());
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public List<FileStorage> getEachFileStorage() {
|
||||
List<FileStorage> ret = new ArrayList<>();
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
SELECT PATH, STATE, TYPE, DESCRIPTION, CREATE_DATE, ID, BASE_ID
|
||||
FROM FILE_STORAGE_VIEW
|
||||
WHERE NODE=?
|
||||
""")) {
|
||||
|
||||
stmt.setInt(1, node);
|
||||
|
||||
long storageId;
|
||||
long baseId;
|
||||
String path;
|
||||
String state;
|
||||
String description;
|
||||
LocalDateTime createDateTime;
|
||||
FileStorageType type;
|
||||
|
||||
try (var rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
baseId = rs.getLong("BASE_ID");
|
||||
storageId = rs.getLong("ID");
|
||||
path = rs.getString("PATH");
|
||||
state = rs.getString("STATE");
|
||||
|
||||
try {
|
||||
type = FileStorageType.valueOf(rs.getString("TYPE"));
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
logger.warn("Illegal file storage type {} in db", rs.getString("TYPE"));
|
||||
continue;
|
||||
}
|
||||
|
||||
description = rs.getString("DESCRIPTION");
|
||||
createDateTime = rs.getTimestamp("CREATE_DATE").toLocalDateTime();
|
||||
var base = getStorageBase(new FileStorageBaseId(baseId));
|
||||
|
||||
ret.add(new FileStorage(
|
||||
new FileStorageId(storageId),
|
||||
base,
|
||||
type,
|
||||
createDateTime,
|
||||
path,
|
||||
FileStorageState.parse(state),
|
||||
description
|
||||
));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public List<FileStorage> getEachFileStorage(FileStorageType type) {
|
||||
return getEachFileStorage(node, type);
|
||||
}
|
||||
|
||||
public List<FileStorage> getEachFileStorage(int node, FileStorageType type) {
|
||||
List<FileStorage> ret = new ArrayList<>();
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
SELECT PATH, STATE, TYPE, DESCRIPTION, CREATE_DATE, ID, BASE_ID
|
||||
FROM FILE_STORAGE_VIEW
|
||||
WHERE NODE=? AND TYPE=?
|
||||
""")) {
|
||||
|
||||
stmt.setInt(1, node);
|
||||
stmt.setString(2, type.name());
|
||||
|
||||
long storageId;
|
||||
long baseId;
|
||||
String path;
|
||||
String state;
|
||||
String description;
|
||||
LocalDateTime createDateTime;
|
||||
|
||||
try (var rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
baseId = rs.getLong("BASE_ID");
|
||||
storageId = rs.getLong("ID");
|
||||
path = rs.getString("PATH");
|
||||
state = rs.getString("STATE");
|
||||
|
||||
description = rs.getString("DESCRIPTION");
|
||||
createDateTime = rs.getTimestamp("CREATE_DATE").toLocalDateTime();
|
||||
var base = getStorageBase(new FileStorageBaseId(baseId));
|
||||
|
||||
ret.add(new FileStorage(
|
||||
new FileStorageId(storageId),
|
||||
base,
|
||||
type,
|
||||
createDateTime,
|
||||
path,
|
||||
FileStorageState.parse(state),
|
||||
description
|
||||
));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
public void flagFileForDeletion(FileStorageId id) throws SQLException {
|
||||
setFileStorageState(id, FileStorageState.DELETE);
|
||||
}
|
||||
|
||||
public void enableFileStorage(FileStorageId id) throws SQLException {
|
||||
setFileStorageState(id, FileStorageState.ACTIVE);
|
||||
}
|
||||
public void disableFileStorage(FileStorageId id) throws SQLException {
|
||||
setFileStorageState(id, FileStorageState.UNSET);
|
||||
}
|
||||
|
||||
public void setFileStorageState(FileStorageId id, FileStorageState state) throws SQLException {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var flagStmt = conn.prepareStatement("UPDATE FILE_STORAGE SET STATE = ? WHERE ID = ?")) {
|
||||
String value = state == FileStorageState.UNSET ? "" : state.name();
|
||||
flagStmt.setString(1, value);
|
||||
flagStmt.setLong(2, id.id());
|
||||
flagStmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public void disableFileStorageOfType(int nodeId, FileStorageType type) throws SQLException {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var flagStmt = conn.prepareStatement("""
|
||||
UPDATE FILE_STORAGE
|
||||
INNER JOIN FILE_STORAGE_BASE ON BASE_ID=FILE_STORAGE_BASE.ID
|
||||
SET FILE_STORAGE.STATE = ''
|
||||
WHERE FILE_STORAGE.TYPE = ?
|
||||
AND FILE_STORAGE.TYPE = 'ACTIVE'
|
||||
AND FILE_STORAGE_BASE.NODE=?
|
||||
""")) {
|
||||
flagStmt.setString(1, type.name());
|
||||
flagStmt.setInt(2, nodeId);
|
||||
flagStmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public List<FileStorageId> getActiveFileStorages(FileStorageType type) throws SQLException {
|
||||
return getActiveFileStorages(node, type);
|
||||
}
|
||||
public Optional<FileStorageId> getOnlyActiveFileStorage(FileStorageType type) throws SQLException {
|
||||
return getOnlyActiveFileStorage(node, type);
|
||||
}
|
||||
|
||||
public Optional<FileStorageId> getOnlyActiveFileStorage(int nodeId, FileStorageType type) throws SQLException {
|
||||
var storages = getActiveFileStorages(nodeId, type);
|
||||
if (storages.size() > 1) {
|
||||
throw new IllegalStateException("Expected [0,1] instances of FileStorage with type " + type + ", found " + storages.size());
|
||||
}
|
||||
return storages.stream().findFirst();
|
||||
}
|
||||
|
||||
public List<FileStorageId> getActiveFileStorages(int nodeId, FileStorageType type) throws SQLException
|
||||
{
|
||||
|
||||
try (var conn = dataSource.getConnection();
|
||||
var queryStmt = conn.prepareStatement("""
|
||||
SELECT FILE_STORAGE.ID FROM FILE_STORAGE
|
||||
INNER JOIN FILE_STORAGE_BASE ON BASE_ID=FILE_STORAGE_BASE.ID
|
||||
WHERE FILE_STORAGE.TYPE = ?
|
||||
AND STATE='ACTIVE'
|
||||
AND FILE_STORAGE_BASE.NODE=?
|
||||
""")) {
|
||||
queryStmt.setString(1, type.name());
|
||||
queryStmt.setInt(2, nodeId);
|
||||
var rs = queryStmt.executeQuery();
|
||||
List<FileStorageId> ids = new ArrayList<>();
|
||||
while (rs.next()) {
|
||||
ids.add(new FileStorageId(rs.getInt(1)));
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,80 @@
|
||||
package nu.marginalia.storage.model;
|
||||
|
||||
import nu.marginalia.storage.FileStorageService;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents a file storage area
|
||||
*
|
||||
* @param id the id of the storage in the database
|
||||
* @param base the base of the storage
|
||||
* @param type the type of data expected
|
||||
* @param path the full path of the storage on disk
|
||||
* @param description a description of the storage
|
||||
*/
|
||||
public record FileStorage (
|
||||
FileStorageId id,
|
||||
FileStorageBase base,
|
||||
FileStorageType type,
|
||||
LocalDateTime createDateTime,
|
||||
String path,
|
||||
FileStorageState state,
|
||||
String description)
|
||||
{
|
||||
|
||||
public int node() {
|
||||
return base.node();
|
||||
}
|
||||
|
||||
public Path asPath() {
|
||||
return FileStorageService.resolveStoragePath(path);
|
||||
}
|
||||
|
||||
|
||||
public boolean isActive() {
|
||||
return FileStorageState.ACTIVE.equals(state);
|
||||
}
|
||||
public boolean isNoState() {
|
||||
return FileStorageState.UNSET.equals(state);
|
||||
}
|
||||
public boolean isDelete() {
|
||||
return FileStorageState.DELETE.equals(state);
|
||||
}
|
||||
public boolean isNew() {
|
||||
return FileStorageState.NEW.equals(state);
|
||||
}
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
FileStorage that = (FileStorage) o;
|
||||
|
||||
// Exclude timestamp as it may different due to how the objects
|
||||
// are constructed
|
||||
|
||||
if (!Objects.equals(id, that.id)) return false;
|
||||
if (!Objects.equals(base, that.base)) return false;
|
||||
if (type != that.type) return false;
|
||||
if (!Objects.equals(path, that.path)) return false;
|
||||
return Objects.equals(description, that.description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = id != null ? id.hashCode() : 0;
|
||||
result = 31 * result + (base != null ? base.hashCode() : 0);
|
||||
result = 31 * result + (type != null ? type.hashCode() : 0);
|
||||
result = 31 * result + (path != null ? path.hashCode() : 0);
|
||||
result = 31 * result + (description != null ? description.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
public String date() {
|
||||
return createDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package nu.marginalia.storage.model;
|
||||
|
||||
import nu.marginalia.storage.FileStorageService;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Represents a file storage base directory
|
||||
*
|
||||
* @param id the id of the storage base in the database
|
||||
* @param type the type of the storage base
|
||||
* @param name the name of the storage base
|
||||
* @param path the path of the storage base
|
||||
*/
|
||||
public record FileStorageBase(FileStorageBaseId id,
|
||||
FileStorageBaseType type,
|
||||
int node,
|
||||
String name,
|
||||
String path
|
||||
) {
|
||||
|
||||
public Path asPath() {
|
||||
return FileStorageService.resolveStoragePath(path);
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return id.id() >= 0;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package nu.marginalia.storage.model;
|
||||
|
||||
public record FileStorageBaseId(long id) {
|
||||
|
||||
public String toString() {
|
||||
return Long.toString(id);
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package nu.marginalia.storage.model;
|
||||
|
||||
public enum FileStorageBaseType {
|
||||
CURRENT,
|
||||
WORK,
|
||||
STORAGE,
|
||||
BACKUP;
|
||||
|
||||
|
||||
public static FileStorageBaseType forFileStorageType(FileStorageType type) {
|
||||
return switch (type) {
|
||||
case EXPORT, CRAWL_DATA, PROCESSED_DATA, CRAWL_SPEC -> STORAGE;
|
||||
case BACKUP -> BACKUP;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@@ -1,10 +1,10 @@
|
||||
package nu.marginalia.db.storage.model;
|
||||
package nu.marginalia.storage.model;
|
||||
|
||||
public record FileStorageId(long id) {
|
||||
public static FileStorageId parse(String str) {
|
||||
return new FileStorageId(Long.parseLong(str));
|
||||
}
|
||||
public static FileStorageId of(int storageId) {
|
||||
public static FileStorageId of(long storageId) {
|
||||
return new FileStorageId(storageId);
|
||||
}
|
||||
|
@@ -0,0 +1,15 @@
|
||||
package nu.marginalia.storage.model;
|
||||
|
||||
public enum FileStorageState {
|
||||
UNSET,
|
||||
NEW,
|
||||
ACTIVE,
|
||||
DELETE;
|
||||
|
||||
public static FileStorageState parse(String value) {
|
||||
if ("".equals(value)) {
|
||||
return UNSET;
|
||||
}
|
||||
return valueOf(value);
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package nu.marginalia.storage.model;
|
||||
|
||||
public enum FileStorageType {
|
||||
@Deprecated
|
||||
CRAWL_SPEC, //
|
||||
|
||||
CRAWL_DATA,
|
||||
PROCESSED_DATA,
|
||||
BACKUP,
|
||||
EXPORT;
|
||||
}
|
64
code/common/config/resources/log4j2-json.xml
Normal file
64
code/common/config/resources/log4j2-json.xml
Normal file
@@ -0,0 +1,64 @@
|
||||
<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" >
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%d{HH:mm:ss,SSS} %style{%-8markerSimpleName}{FG_Cyan} %highlight{%-5level}{FATAL=red, ERROR=red, WARN=yellow} %-24t %-20c{1} -- %msg%n"/>
|
||||
<Filters>
|
||||
<MarkerFilter marker="PROCESS" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="QUERY" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="HTTP" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="CRAWLER" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="CONVERTER" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
</Filters>
|
||||
</Console>
|
||||
<Console name="ProcessConsole" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%style{P}{FG_Cyan} %msg%n"/>
|
||||
<Filters>
|
||||
<MarkerFilter marker="PROCESS" onMatch="ALLOW" onMismatch="DENY" />
|
||||
</Filters>
|
||||
</Console>
|
||||
<RollingFile name="LogToFileService" fileName="${env:WMSA_LOG_DIR:-/var/log/wmsa}/wmsa-${sys:service-name}-${env:WMSA_SERVICE_NODE:-0}.log" filePattern="/var/log/wmsa/wmsa-${sys:service-name}-${env:WMSA_SERVICE_NODE:-0}-log-%d{MM-dd-yy-HH-mm-ss}-%i.log.gz"
|
||||
ignoreExceptions="false">
|
||||
<JSONLayout compact="true" eventEol="true" properties="true" stacktraceAsString="true" includeTimeMillis="true"/>
|
||||
<Filters>
|
||||
<MarkerFilter marker="QUERY" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="HTTP" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="CRAWLER" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="PROCESS" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="CONVERTER" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
</Filters>
|
||||
<SizeBasedTriggeringPolicy size="10MB" />
|
||||
</RollingFile>
|
||||
<RollingFile name="LogToFileCrawler" fileName="${env:WMSA_LOG_DIR:-/var/log/wmsa}/crawler-audit-${env:WMSA_SERVICE_NODE:-0}.log" filePattern="/var/log/wmsa/crawler-audit-${env:WMSA_SERVICE_NODE:-0}-log-%d{MM-dd-yy-HH-mm-ss}-%i.log.gz"
|
||||
ignoreExceptions="false">
|
||||
<PatternLayout>
|
||||
<Pattern>%d{yyyy-MM-dd HH:mm:ss,SSS}: %msg{nolookups}%n</Pattern>
|
||||
</PatternLayout>
|
||||
<SizeBasedTriggeringPolicy size="100MB" />
|
||||
<Filters>
|
||||
<MarkerFilter marker="CRAWLER" onMatch="ALLOW" onMismatch="DENY" />
|
||||
</Filters>
|
||||
</RollingFile>
|
||||
<RollingFile name="LogToFileConverter" fileName="${env:WMSA_LOG_DIR:-/var/log/wmsa}/converter-audit-${env:WMSA_SERVICE_NODE:-0}.log" filePattern="/var/log/wmsa/converter-audit-${env:WMSA_SERVICE_NODE:-0}-log-%d{MM-dd-yy-HH-mm-ss}-%i.log.gz"
|
||||
ignoreExceptions="false">
|
||||
<PatternLayout>
|
||||
<Pattern>%d{yyyy-MM-dd HH:mm:ss,SSS}: %msg{nolookups}%n</Pattern>
|
||||
</PatternLayout>
|
||||
<SizeBasedTriggeringPolicy size="100MB" />
|
||||
<Filters>
|
||||
<MarkerFilter marker="CONVERTER" onMatch="ALLOW" onMismatch="DENY" />
|
||||
</Filters>
|
||||
</RollingFile>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Logger name="org.apache.zookeeper" level="WARN" />
|
||||
<Logger name="org.apache.pdfbox" level="ERROR" />
|
||||
<Logger name="org.apache.fontbox.ttf" level="ERROR" />
|
||||
<Root level="info">
|
||||
<AppenderRef ref="Console"/>
|
||||
<AppenderRef ref="ProcessConsole"/>
|
||||
<AppenderRef ref="LogToFileService"/>
|
||||
<AppenderRef ref="LogToFileCrawler"/>
|
||||
<AppenderRef ref="LogToFileConverter"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
103
code/common/config/resources/log4j2-prod.xml
Normal file
103
code/common/config/resources/log4j2-prod.xml
Normal file
@@ -0,0 +1,103 @@
|
||||
<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" >
|
||||
<Appenders>
|
||||
<Console name="ConsoleInfo" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="- %d{HH:mm:ss,SSS} %-20c{1} -- %msg%n"/>
|
||||
<Filters>
|
||||
<LevelMatchFilter level="INFO" onMatch="ALLOW" onMismatch="DENY"/>
|
||||
<MarkerFilter marker="PROCESS" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="QUERY" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="HTTP" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="CRAWLER" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="CONVERTER" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
</Filters>
|
||||
</Console>
|
||||
<Console name="ConsoleWarn" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="⚠ %d{HH:mm:ss,SSS} %-20c{1} -- %msg%n"/>
|
||||
<Filters>
|
||||
<LevelMatchFilter level="WARN" onMatch="ALLOW" onMismatch="DENY"/>
|
||||
<MarkerFilter marker="PROCESS" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="QUERY" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="HTTP" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="CRAWLER" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="CONVERTER" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
</Filters>
|
||||
</Console>
|
||||
<Console name="ConsoleError" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="🔥 %d{HH:mm:ss,SSS} %-20c{1} -- %msg%n"/>
|
||||
<Filters>
|
||||
<LevelMatchFilter level="ERROR" onMatch="ALLOW" onMismatch="DENY"/>
|
||||
<MarkerFilter marker="PROCESS" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="QUERY" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="HTTP" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="CRAWLER" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="CONVERTER" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
</Filters>
|
||||
</Console>
|
||||
<Console name="ConsoleFatal" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="💀 %d{HH:mm:ss,SSS} %-20c{1} -- %msg%n"/>
|
||||
<Filters>
|
||||
<LevelMatchFilter level="FATAL" onMatch="ALLOW" onMismatch="DENY"/>
|
||||
<MarkerFilter marker="PROCESS" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="QUERY" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="HTTP" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="CRAWLER" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="CONVERTER" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
</Filters>
|
||||
</Console>
|
||||
<Console name="ProcessConsole" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%style{%msg%n}{FG_Cyan}"/>
|
||||
<Filters>
|
||||
<MarkerFilter marker="PROCESS" onMatch="ALLOW" onMismatch="DENY" />
|
||||
</Filters>
|
||||
</Console>
|
||||
<RollingFile name="LogToFileService" fileName="${env:WMSA_LOG_DIR:-/var/log/wmsa}/wmsa-${sys:service-name}-${env:WMSA_SERVICE_NODE:-0}.log" filePattern="/var/log/wmsa/wmsa-${sys:service-name}-${env:WMSA_SERVICE_NODE:-0}-log-%d{MM-dd-yy-HH-mm-ss}-%i.log.gz"
|
||||
ignoreExceptions="false">
|
||||
<PatternLayout>
|
||||
<Pattern>%-5level %d{yyyy-MM-dd HH:mm:ss,SSS} %-20t %-20c{1}: %msg{nolookups}%n</Pattern>
|
||||
</PatternLayout>
|
||||
<SizeBasedTriggeringPolicy size="10MB" />
|
||||
<Filters>
|
||||
<MarkerFilter marker="PROCESS" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="QUERY" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="HTTP" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="CRAWLER" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
<MarkerFilter marker="CONVERTER" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
</Filters>
|
||||
</RollingFile>
|
||||
<RollingFile name="LogToFileCrawler" fileName="${env:WMSA_LOG_DIR:-/var/log/wmsa}/crawler-audit-${env:WMSA_SERVICE_NODE:-0}.log" filePattern="/var/log/wmsa/crawler-audit-${env:WMSA_SERVICE_NODE:-0}-log-%d{MM-dd-yy-HH-mm-ss}-%i.log.gz"
|
||||
ignoreExceptions="false">
|
||||
<PatternLayout>
|
||||
<Pattern>%d{yyyy-MM-dd HH:mm:ss,SSS}: %msg{nolookups}%n</Pattern>
|
||||
</PatternLayout>
|
||||
<SizeBasedTriggeringPolicy size="100MB" />
|
||||
<Filters>
|
||||
<MarkerFilter marker="CRAWLER" onMatch="ALLOW" onMismatch="DENY" />
|
||||
</Filters>
|
||||
</RollingFile>
|
||||
<RollingFile name="LogToFileConverter" fileName="${env:WMSA_LOG_DIR:-/var/log/wmsa}/converter-audit-${env:WMSA_SERVICE_NODE:-0}.log" filePattern="/var/log/wmsa/converter-audit-${env:WMSA_SERVICE_NODE:-0}-log-%d{MM-dd-yy-HH-mm-ss}-%i.log.gz"
|
||||
ignoreExceptions="false">
|
||||
<PatternLayout>
|
||||
<Pattern>%d{yyyy-MM-dd HH:mm:ss,SSS}: %msg{nolookups}%n</Pattern>
|
||||
</PatternLayout>
|
||||
<SizeBasedTriggeringPolicy size="100MB" />
|
||||
<Filters>
|
||||
<MarkerFilter marker="CONVERTER" onMatch="ALLOW" onMismatch="DENY" />
|
||||
</Filters>
|
||||
</RollingFile>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Logger name="org.apache.zookeeper" level="WARN" />
|
||||
<Logger name="org.apache.pdfbox" level="ERROR" />
|
||||
<Logger name="org.apache.fontbox.ttf" level="ERROR" />
|
||||
<Root level="info">
|
||||
<AppenderRef ref="ConsoleInfo"/>
|
||||
<AppenderRef ref="ConsoleWarn"/>
|
||||
<AppenderRef ref="ConsoleError"/>
|
||||
<AppenderRef ref="ConsoleFatal"/>
|
||||
<AppenderRef ref="ProcessConsole"/>
|
||||
<AppenderRef ref="LogToFileService"/>
|
||||
<AppenderRef ref="LogToFileConverer"/>
|
||||
<AppenderRef ref="LogToFileCrawler"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
50
code/common/config/resources/log4j2-test.xml
Normal file
50
code/common/config/resources/log4j2-test.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" >
|
||||
<Appenders>
|
||||
<Console name="ConsoleInfo" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="- %d{HH:mm:ss,SSS} %-20c{1} -- %msg%n"/>
|
||||
<Filters>
|
||||
<LevelMatchFilter level="INFO" onMatch="ALLOW" onMismatch="DENY"/>
|
||||
<MarkerFilter marker="PROCESS" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
</Filters>
|
||||
</Console>
|
||||
<Console name="ConsoleWarn" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="⚠ %d{HH:mm:ss,SSS} %-20c{1} -- %msg%n"/>
|
||||
<Filters>
|
||||
<LevelMatchFilter level="WARN" onMatch="ALLOW" onMismatch="DENY"/>
|
||||
<MarkerFilter marker="PROCESS" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
</Filters>
|
||||
</Console>
|
||||
<Console name="ConsoleError" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="🔥 %d{HH:mm:ss,SSS} %-20c{1} -- %msg%n"/>
|
||||
<Filters>
|
||||
<LevelMatchFilter level="ERROR" onMatch="ALLOW" onMismatch="DENY"/>
|
||||
<MarkerFilter marker="PROCESS" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
</Filters>
|
||||
</Console>
|
||||
<Console name="ConsoleFatal" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="💀 %d{HH:mm:ss,SSS} %-20c{1} -- %msg%n"/>
|
||||
<Filters>
|
||||
<LevelMatchFilter level="FATAL" onMatch="ALLOW" onMismatch="DENY"/>
|
||||
<MarkerFilter marker="PROCESS" onMatch="DENY" onMismatch="NEUTRAL" />
|
||||
</Filters>
|
||||
</Console>
|
||||
<Console name="ProcessConsole" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%style{%msg%n}{FG_Cyan}"/>
|
||||
<Filters>
|
||||
<MarkerFilter marker="PROCESS" onMatch="ALLOW" onMismatch="DENY" />
|
||||
</Filters>
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Logger name="org.apache.zookeeper" level="WARN" />
|
||||
<Logger name="org.apache.pdfbox" level="ERROR" />
|
||||
<Logger name="org.apache.fontbox.ttf" level="ERROR" />
|
||||
<Root level="info">
|
||||
<AppenderRef ref="ConsoleInfo"/>
|
||||
<AppenderRef ref="ConsoleWarn"/>
|
||||
<AppenderRef ref="ConsoleError"/>
|
||||
<AppenderRef ref="ConsoleFatal"/>
|
||||
<AppenderRef ref="ProcessConsole"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
@@ -1,30 +0,0 @@
|
||||
package nu.marginalia;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class LanguageModels {
|
||||
public final Path ngramBloomFilter;
|
||||
public final Path termFrequencies;
|
||||
|
||||
public final Path openNLPSentenceDetectionData;
|
||||
public final Path posRules;
|
||||
public final Path posDict;
|
||||
public final Path openNLPTokenData;
|
||||
public final Path fasttextLanguageModel;
|
||||
|
||||
public LanguageModels(Path ngramBloomFilter,
|
||||
Path termFrequencies,
|
||||
Path openNLPSentenceDetectionData,
|
||||
Path posRules,
|
||||
Path posDict,
|
||||
Path openNLPTokenData,
|
||||
Path fasttextLanguageModel) {
|
||||
this.ngramBloomFilter = ngramBloomFilter;
|
||||
this.termFrequencies = termFrequencies;
|
||||
this.openNLPSentenceDetectionData = openNLPSentenceDetectionData;
|
||||
this.posRules = posRules;
|
||||
this.posDict = posDict;
|
||||
this.openNLPTokenData = openNLPTokenData;
|
||||
this.fasttextLanguageModel = fasttextLanguageModel;
|
||||
}
|
||||
}
|
@@ -1,5 +0,0 @@
|
||||
package nu.marginalia;
|
||||
|
||||
public record UserAgent(String uaString) {
|
||||
|
||||
}
|
@@ -1,98 +0,0 @@
|
||||
package nu.marginalia;
|
||||
|
||||
|
||||
import nu.marginalia.service.ServiceHomeNotConfiguredException;
|
||||
import nu.marginalia.service.descriptor.HostsFile;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class WmsaHome {
|
||||
public static UserAgent getUserAgent() throws IOException {
|
||||
var uaPath = getHomePath().resolve("conf/user-agent");
|
||||
|
||||
if (!Files.exists(uaPath)) {
|
||||
throw new FileNotFoundException("Could not find " + uaPath);
|
||||
}
|
||||
|
||||
return new UserAgent(Files.readString(uaPath).trim());
|
||||
}
|
||||
|
||||
|
||||
public static Path getHomePath() {
|
||||
var retStr = Optional.ofNullable(System.getenv("WMSA_HOME")).orElseGet(WmsaHome::findDefaultHomePath);
|
||||
|
||||
var ret = Path.of(retStr);
|
||||
|
||||
if (!Files.isDirectory(ret)) {
|
||||
throw new ServiceHomeNotConfiguredException("Could not find $WMSA_HOME, either set environment variable or ensure " + retStr + " exists");
|
||||
}
|
||||
|
||||
|
||||
if (!Files.isDirectory(ret.resolve("model"))) {
|
||||
throw new ServiceHomeNotConfiguredException("You need to run 'run/setup.sh' to download models to run/ before this will work!");
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static String findDefaultHomePath() {
|
||||
|
||||
// Assume this is a local developer and not a production system, since it would have WMSA_HOME set.
|
||||
// Developers probably have a "run/" somewhere upstream from cwd.
|
||||
//
|
||||
|
||||
return Stream.iterate(Paths.get("").toAbsolutePath(), f -> f != null && Files.exists(f), Path::getParent)
|
||||
.filter(p -> Files.exists(p.resolve("run/env")))
|
||||
.filter(p -> Files.exists(p.resolve("run/setup.sh")))
|
||||
.map(p -> p.resolve("run"))
|
||||
.findAny()
|
||||
.orElse(Path.of("/var/lib/wmsa"))
|
||||
.toString();
|
||||
}
|
||||
|
||||
public static HostsFile getHostsFile() {
|
||||
Path hostsFile = getHomePath().resolve("conf/hosts");
|
||||
if (Files.isRegularFile(hostsFile)) {
|
||||
try {
|
||||
return new HostsFile(hostsFile);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to load hosts file " + hostsFile, e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return new HostsFile();
|
||||
}
|
||||
}
|
||||
|
||||
public static Path getAdsDefinition() {
|
||||
return getHomePath().resolve("data").resolve("adblock.txt");
|
||||
}
|
||||
|
||||
public static Path getIPLocationDatabse() {
|
||||
return getHomePath().resolve("data").resolve("IP2LOCATION-LITE-DB1.CSV");
|
||||
}
|
||||
|
||||
public static LanguageModels getLanguageModels() {
|
||||
final Path home = getHomePath();
|
||||
|
||||
return new LanguageModels(
|
||||
home.resolve("model/ngrams.bin"),
|
||||
home.resolve("model/tfreq-new-algo3.bin"),
|
||||
home.resolve("model/opennlp-sentence.bin"),
|
||||
home.resolve("model/English.RDR"),
|
||||
home.resolve("model/English.DICT"),
|
||||
home.resolve("model/opennlp-tok.bin"),
|
||||
home.resolve("model/lid.176.ftz"));
|
||||
}
|
||||
|
||||
private static final boolean debugMode = Boolean.getBoolean("wmsa-debug");
|
||||
public static boolean isDebug() {
|
||||
return debugMode;
|
||||
}
|
||||
}
|
@@ -0,0 +1,125 @@
|
||||
package nu.marginalia.nodecfg;
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import nu.marginalia.nodecfg.model.NodeConfiguration;
|
||||
import nu.marginalia.nodecfg.model.NodeProfile;
|
||||
import nu.marginalia.test.TestMigrationLoader;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.parallel.Execution;
|
||||
import org.junit.jupiter.api.parallel.ExecutionMode;
|
||||
import org.testcontainers.containers.MariaDBContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@Testcontainers
|
||||
@Execution(ExecutionMode.SAME_THREAD)
|
||||
@Tag("slow")
|
||||
public class NodeConfigurationServiceTest {
|
||||
@Container
|
||||
static MariaDBContainer<?> mariaDBContainer = new MariaDBContainer<>("mariadb")
|
||||
.withDatabaseName("WMSA_prod")
|
||||
.withUsername("wmsa")
|
||||
.withPassword("wmsa")
|
||||
.withNetworkAliases("mariadb");
|
||||
|
||||
static HikariDataSource dataSource;
|
||||
static NodeConfigurationService nodeConfigurationService;
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() {
|
||||
HikariConfig config = new HikariConfig();
|
||||
config.setJdbcUrl(mariaDBContainer.getJdbcUrl());
|
||||
config.setUsername("wmsa");
|
||||
config.setPassword("wmsa");
|
||||
|
||||
dataSource = new HikariDataSource(config);
|
||||
|
||||
TestMigrationLoader.flywayMigration(dataSource);
|
||||
|
||||
nodeConfigurationService = new NodeConfigurationService(dataSource);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() throws SQLException {
|
||||
var a = nodeConfigurationService.create(1, "Test", false, false, NodeProfile.MIXED);
|
||||
var b = nodeConfigurationService.create(2, "Foo", true, false, NodeProfile.MIXED);
|
||||
|
||||
assertEquals(1, a.node());
|
||||
assertEquals("Test", a.description());
|
||||
assertFalse(a.acceptQueries());
|
||||
|
||||
assertEquals(2, b.node());
|
||||
assertEquals("Foo", b.description());
|
||||
assertTrue(b.acceptQueries());
|
||||
|
||||
var list = nodeConfigurationService.getAll();
|
||||
assertEquals(2, list.size());
|
||||
assertEquals(a, list.get(0));
|
||||
assertEquals(b, list.get(1));
|
||||
}
|
||||
|
||||
|
||||
// Test all the fields that are only exposed via save()
|
||||
@Test
|
||||
public void testSaveChanges() throws SQLException {
|
||||
var original = nodeConfigurationService.create(1, "Test", false, false, NodeProfile.MIXED);
|
||||
|
||||
assertEquals(1, original.node());
|
||||
assertEquals("Test", original.description());
|
||||
assertFalse(original.acceptQueries());
|
||||
|
||||
var precession = new NodeConfiguration(
|
||||
original.node(),
|
||||
"Foo",
|
||||
true,
|
||||
original.autoClean(),
|
||||
original.includeInPrecession(),
|
||||
!original.autoAssignDomains(),
|
||||
original.keepWarcs(),
|
||||
original.profile(),
|
||||
original.disabled()
|
||||
);
|
||||
|
||||
nodeConfigurationService.save(precession);
|
||||
precession = nodeConfigurationService.get(original.node());
|
||||
assertNotEquals(original.autoAssignDomains(), precession.autoAssignDomains());
|
||||
|
||||
var autoClean = new NodeConfiguration(
|
||||
original.node(),
|
||||
"Foo",
|
||||
true,
|
||||
!original.autoClean(),
|
||||
original.includeInPrecession(),
|
||||
original.autoAssignDomains(),
|
||||
original.keepWarcs(),
|
||||
original.profile(),
|
||||
original.disabled()
|
||||
);
|
||||
|
||||
nodeConfigurationService.save(autoClean);
|
||||
autoClean = nodeConfigurationService.get(original.node());
|
||||
assertNotEquals(original.autoClean(), autoClean.autoClean());
|
||||
|
||||
var disabled = new NodeConfiguration(
|
||||
original.node(),
|
||||
"Foo",
|
||||
true,
|
||||
autoClean.autoClean(),
|
||||
autoClean.includeInPrecession(),
|
||||
autoClean.autoAssignDomains(),
|
||||
autoClean.keepWarcs(),
|
||||
autoClean.profile(),
|
||||
!autoClean.disabled()
|
||||
);
|
||||
nodeConfigurationService.save(disabled);
|
||||
disabled = nodeConfigurationService.get(original.node());
|
||||
assertNotEquals(autoClean.disabled(), disabled.disabled());
|
||||
}
|
||||
}
|
@@ -0,0 +1,162 @@
|
||||
package nu.marginalia.storage;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import nu.marginalia.storage.model.FileStorage;
|
||||
import nu.marginalia.storage.model.FileStorageBase;
|
||||
import nu.marginalia.storage.model.FileStorageBaseType;
|
||||
import nu.marginalia.storage.model.FileStorageType;
|
||||
import nu.marginalia.test.TestMigrationLoader;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.api.parallel.Execution;
|
||||
import org.junit.jupiter.api.parallel.ExecutionMode;
|
||||
import org.testcontainers.containers.MariaDBContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Testcontainers
|
||||
@Execution(ExecutionMode.SAME_THREAD)
|
||||
@Tag("slow")
|
||||
public class FileStorageServiceTest {
|
||||
@Container
|
||||
static MariaDBContainer<?> mariaDBContainer = new MariaDBContainer<>("mariadb")
|
||||
.withDatabaseName("WMSA_prod")
|
||||
.withUsername("wmsa")
|
||||
.withPassword("wmsa")
|
||||
.withNetworkAliases("mariadb");
|
||||
|
||||
static HikariDataSource dataSource;
|
||||
static FileStorageService fileStorageService;
|
||||
|
||||
static List<Path> tempDirs = new ArrayList<>();
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() {
|
||||
HikariConfig config = new HikariConfig();
|
||||
config.setJdbcUrl(mariaDBContainer.getJdbcUrl());
|
||||
config.setUsername("wmsa");
|
||||
config.setPassword("wmsa");
|
||||
|
||||
dataSource = new HikariDataSource(config);
|
||||
|
||||
TestMigrationLoader.flywayMigration(dataSource);
|
||||
}
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void setupEach() {
|
||||
fileStorageService = new FileStorageService(dataSource, 0);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDownEach() {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.createStatement()) {
|
||||
stmt.execute("DELETE FROM FILE_STORAGE");
|
||||
stmt.execute("DELETE FROM FILE_STORAGE_BASE");
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void teardown() {
|
||||
dataSource.close();
|
||||
|
||||
Lists.reverse(tempDirs).forEach(path -> {
|
||||
try {
|
||||
System.out.println("Deleting " + path);
|
||||
Files.delete(path);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Path createTempDir() {
|
||||
try {
|
||||
Path dir = Files.createTempDirectory("file-storage-test");
|
||||
tempDirs.add(dir);
|
||||
return dir;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPathOverride() {
|
||||
try {
|
||||
System.setProperty("storage.root", "/tmp");
|
||||
|
||||
var path = new FileStorageBase(null, null, 0, null, "test").asPath();
|
||||
Assertions.assertEquals(Path.of("/tmp/test"), path);
|
||||
}
|
||||
finally {
|
||||
System.clearProperty("storage.root");
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void testPathOverride3() {
|
||||
try {
|
||||
System.setProperty("storage.root", "/tmp");
|
||||
|
||||
var path = new FileStorageBase(null, null, 0, null, "/test").asPath();
|
||||
Assertions.assertEquals(Path.of("/tmp/test"), path);
|
||||
}
|
||||
finally {
|
||||
System.clearProperty("storage.root");
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void testPathOverride2() {
|
||||
try {
|
||||
System.setProperty("storage.root", "/tmp");
|
||||
|
||||
var path = new FileStorage(null, null, null, null, "test", null, null).asPath();
|
||||
|
||||
Assertions.assertEquals(Path.of("/tmp/test"), path);
|
||||
}
|
||||
finally {
|
||||
System.clearProperty("storage.root");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateBase() throws SQLException {
|
||||
String name = "test-" + UUID.randomUUID();
|
||||
|
||||
var storage = new FileStorageService(dataSource, 0);
|
||||
var base = storage.createStorageBase(name, createTempDir(), FileStorageBaseType.WORK);
|
||||
|
||||
Assertions.assertEquals(name, base.name());
|
||||
Assertions.assertEquals(FileStorageBaseType.WORK, base.type());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllocateTemp() throws IOException, SQLException {
|
||||
String name = "test-" + UUID.randomUUID();
|
||||
|
||||
// ensure a base exists
|
||||
var base = fileStorageService.createStorageBase(name, createTempDir(), FileStorageBaseType.STORAGE);
|
||||
tempDirs.add(base.asPath());
|
||||
|
||||
var storage = new FileStorageService(dataSource, 0);
|
||||
|
||||
var fileStorage = storage.allocateStorage(FileStorageType.CRAWL_DATA, "xyz", "thisShouldSucceed");
|
||||
System.out.println("Allocated " + fileStorage.asPath());
|
||||
Assertions.assertTrue(Files.exists(fileStorage.asPath()));
|
||||
tempDirs.add(fileStorage.asPath());
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -1,28 +1,42 @@
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'org.flywaydb:flyway-mysql:10.0.1'
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
id "io.freefair.lombok" version "8.2.2"
|
||||
|
||||
id 'jvm-test-suite'
|
||||
id "org.flywaydb.flyway" version "8.2.0"
|
||||
id "org.flywaydb.flyway" version "10.0.1"
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(20))
|
||||
languageVersion.set(JavaLanguageVersion.of(rootProject.ext.jvmVersion))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
configurations {
|
||||
flywayMigration.extendsFrom(implementation)
|
||||
}
|
||||
|
||||
apply from: "$rootProject.projectDir/srcsets.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation project(':code:common:model')
|
||||
|
||||
implementation libs.lombok
|
||||
annotationProcessor libs.lombok
|
||||
implementation libs.bundles.slf4j
|
||||
|
||||
implementation libs.guice
|
||||
implementation libs.guava
|
||||
implementation dependencies.create(libs.guice.get()) {
|
||||
exclude group: 'com.google.guava'
|
||||
}
|
||||
implementation libs.bundles.gson
|
||||
|
||||
implementation libs.notnull
|
||||
@@ -31,9 +45,8 @@ dependencies {
|
||||
|
||||
implementation libs.trove
|
||||
|
||||
implementation libs.rxjava
|
||||
implementation libs.bundles.mariadb
|
||||
flywayMigration 'org.flywaydb:flyway-mysql:9.8.1'
|
||||
flywayMigration 'org.flywaydb:flyway-mysql:10.0.1'
|
||||
|
||||
testImplementation libs.bundles.slf4j.test
|
||||
testImplementation libs.bundles.junit
|
||||
@@ -41,8 +54,10 @@ dependencies {
|
||||
|
||||
|
||||
testImplementation platform('org.testcontainers:testcontainers-bom:1.17.4')
|
||||
testImplementation libs.commons.codec
|
||||
testImplementation 'org.testcontainers:mariadb:1.17.4'
|
||||
testImplementation 'org.testcontainers:junit-jupiter:1.17.4'
|
||||
testImplementation project(':code:libraries:test-helpers')
|
||||
}
|
||||
|
||||
flyway {
|
||||
@@ -52,19 +67,6 @@ flyway {
|
||||
schemas = ['WMSA_prod']
|
||||
configurations = [ 'compileClasspath', 'flywayMigration' ]
|
||||
locations = ['filesystem:src/main/resources/db/migration']
|
||||
cleanDisabled = false
|
||||
}
|
||||
|
||||
|
||||
test {
|
||||
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
|
||||
maxHeapSize = "8G"
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
task fastTests(type: Test) {
|
||||
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
|
||||
maxHeapSize = "8G"
|
||||
useJUnitPlatform {
|
||||
excludeTags "slow"
|
||||
}
|
||||
}
|
||||
|
179
code/common/db/java/nu/marginalia/db/DbDomainQueries.java
Normal file
179
code/common/db/java/nu/marginalia/db/DbDomainQueries.java
Normal file
@@ -0,0 +1,179 @@
|
||||
package nu.marginalia.db;
|
||||
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.util.concurrent.UncheckedExecutionException;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import nu.marginalia.model.EdgeDomain;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@Singleton
|
||||
public class DbDomainQueries {
|
||||
private final HikariDataSource dataSource;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DbDomainQueries.class);
|
||||
|
||||
private final Cache<EdgeDomain, Integer> domainIdCache = CacheBuilder.newBuilder().maximumSize(10_000).build();
|
||||
private final Cache<EdgeDomain, DomainIdWithNode> domainWithNodeCache = CacheBuilder.newBuilder().maximumSize(10_000).build();
|
||||
private final Cache<Integer, EdgeDomain> domainNameCache = CacheBuilder.newBuilder().maximumSize(10_000).build();
|
||||
private final Cache<String, List<DomainWithNode>> siblingsCache = CacheBuilder.newBuilder().maximumSize(10_000).build();
|
||||
|
||||
@Inject
|
||||
public DbDomainQueries(HikariDataSource dataSource)
|
||||
{
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
|
||||
public Integer getDomainId(EdgeDomain domain) throws NoSuchElementException {
|
||||
try {
|
||||
return domainIdCache.get(domain, () -> {
|
||||
try (var connection = dataSource.getConnection();
|
||||
var stmt = connection.prepareStatement("SELECT ID FROM EC_DOMAIN WHERE DOMAIN_NAME=?")) {
|
||||
|
||||
stmt.setString(1, domain.toString());
|
||||
var rsp = stmt.executeQuery();
|
||||
if (rsp.next()) {
|
||||
return rsp.getInt(1);
|
||||
}
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
|
||||
throw new NoSuchElementException();
|
||||
});
|
||||
}
|
||||
catch (UncheckedExecutionException ex) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
catch (ExecutionException ex) {
|
||||
throw new RuntimeException(ex.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public DomainIdWithNode getDomainIdWithNode(EdgeDomain domain) throws NoSuchElementException {
|
||||
try {
|
||||
return domainWithNodeCache.get(domain, () -> {
|
||||
try (var connection = dataSource.getConnection();
|
||||
var stmt = connection.prepareStatement("SELECT ID, NODE_AFFINITY FROM EC_DOMAIN WHERE DOMAIN_NAME=?")) {
|
||||
|
||||
stmt.setString(1, domain.toString());
|
||||
var rsp = stmt.executeQuery();
|
||||
if (rsp.next()) {
|
||||
return new DomainIdWithNode(rsp.getInt(1), rsp.getInt(2));
|
||||
}
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
|
||||
throw new NoSuchElementException();
|
||||
});
|
||||
}
|
||||
catch (UncheckedExecutionException ex) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
catch (ExecutionException ex) {
|
||||
throw new RuntimeException(ex.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
public OptionalInt tryGetDomainId(EdgeDomain domain) {
|
||||
|
||||
Integer maybeId = domainIdCache.getIfPresent(domain);
|
||||
if (maybeId != null) {
|
||||
return OptionalInt.of(maybeId);
|
||||
}
|
||||
|
||||
try (var connection = dataSource.getConnection()) {
|
||||
|
||||
try (var stmt = connection.prepareStatement("SELECT ID FROM EC_DOMAIN WHERE DOMAIN_NAME=?")) {
|
||||
stmt.setString(1, domain.toString());
|
||||
var rsp = stmt.executeQuery();
|
||||
if (rsp.next()) {
|
||||
var id = rsp.getInt(1);
|
||||
|
||||
domainIdCache.put(domain, id);
|
||||
return OptionalInt.of(id);
|
||||
}
|
||||
}
|
||||
return OptionalInt.empty();
|
||||
}
|
||||
catch (UncheckedExecutionException ex) {
|
||||
throw new RuntimeException(ex.getCause());
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<EdgeDomain> getDomain(int id) {
|
||||
|
||||
EdgeDomain existing = domainNameCache.getIfPresent(id);
|
||||
if (existing != null) {
|
||||
return Optional.of(existing);
|
||||
}
|
||||
|
||||
try (var connection = dataSource.getConnection()) {
|
||||
try (var stmt = connection.prepareStatement("SELECT DOMAIN_NAME FROM EC_DOMAIN WHERE ID=?")) {
|
||||
stmt.setInt(1, id);
|
||||
var rsp = stmt.executeQuery();
|
||||
if (rsp.next()) {
|
||||
var val = new EdgeDomain(rsp.getString(1));
|
||||
domainNameCache.put(id, val);
|
||||
return Optional.of(val);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public List<DomainWithNode> otherSubdomains(EdgeDomain domain, int cnt) throws ExecutionException {
|
||||
String topDomain = domain.topDomain;
|
||||
|
||||
return siblingsCache.get(topDomain, () -> {
|
||||
List<DomainWithNode> ret = new ArrayList<>();
|
||||
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("SELECT DOMAIN_NAME, NODE_AFFINITY FROM EC_DOMAIN WHERE DOMAIN_TOP = ? LIMIT ?")) {
|
||||
stmt.setString(1, topDomain);
|
||||
stmt.setInt(2, cnt);
|
||||
|
||||
var rs = stmt.executeQuery();
|
||||
while (rs.next()) {
|
||||
var sibling = new EdgeDomain(rs.getString(1));
|
||||
|
||||
if (sibling.equals(domain))
|
||||
continue;
|
||||
|
||||
ret.add(new DomainWithNode(sibling, rs.getInt(2)));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logger.error("Failed to get domain neighbors");
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public record DomainWithNode (EdgeDomain domain, int nodeAffinity) {
|
||||
public boolean isIndexed() {
|
||||
return nodeAffinity > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public record DomainIdWithNode (int domainId, int nodeAffinity) { }
|
||||
}
|
13
code/common/db/java/nu/marginalia/db/DomainBlacklist.java
Normal file
13
code/common/db/java/nu/marginalia/db/DomainBlacklist.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package nu.marginalia.db;
|
||||
|
||||
import com.google.inject.ImplementedBy;
|
||||
import gnu.trove.set.hash.TIntHashSet;
|
||||
|
||||
@ImplementedBy(DomainBlacklistImpl.class)
|
||||
public interface DomainBlacklist {
|
||||
boolean isBlacklisted(int domainId);
|
||||
default TIntHashSet getSpamDomains() {
|
||||
return new TIntHashSet();
|
||||
}
|
||||
void waitUntilLoaded() throws InterruptedException;
|
||||
}
|
126
code/common/db/java/nu/marginalia/db/DomainBlacklistImpl.java
Normal file
126
code/common/db/java/nu/marginalia/db/DomainBlacklistImpl.java
Normal file
@@ -0,0 +1,126 @@
|
||||
package nu.marginalia.db;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import gnu.trove.set.hash.TIntHashSet;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Singleton
|
||||
public class DomainBlacklistImpl implements DomainBlacklist {
|
||||
private final boolean blacklistDisabled = Boolean.getBoolean("blacklist.disable");
|
||||
|
||||
private final HikariDataSource dataSource;
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
|
||||
private volatile TIntHashSet spamDomainSet = new TIntHashSet();
|
||||
private volatile boolean isLoaded = false;
|
||||
|
||||
@Inject
|
||||
public DomainBlacklistImpl(HikariDataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
|
||||
Thread.ofPlatform().daemon().name("BlacklistUpdater").start(this::updateSpamList);
|
||||
}
|
||||
|
||||
private void updateSpamList() {
|
||||
// If the blacklist is disabled, we don't need to do anything
|
||||
if (blacklistDisabled) {
|
||||
isLoaded = true;
|
||||
|
||||
flagLoaded();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
spamDomainSet = getSpamDomains();
|
||||
|
||||
// Set the flag to true after the first loading attempt, regardless of success,
|
||||
// to avoid deadlocking threads that are waiting for this condition
|
||||
flagLoaded();
|
||||
|
||||
// Sleep for 10 minutes before trying again
|
||||
try {
|
||||
TimeUnit.MINUTES.sleep(10);
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void flagLoaded() {
|
||||
if (!isLoaded) {
|
||||
synchronized (this) {
|
||||
isLoaded = true;
|
||||
notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Block until the blacklist has been loaded */
|
||||
@Override
|
||||
public void waitUntilLoaded() throws InterruptedException {
|
||||
if (blacklistDisabled)
|
||||
return;
|
||||
|
||||
if (!isLoaded) {
|
||||
logger.info("Waiting for blacklist to be loaded");
|
||||
synchronized (this) {
|
||||
while (!isLoaded) {
|
||||
wait(5000);
|
||||
}
|
||||
}
|
||||
logger.info("Blacklist loaded, size = {}", spamDomainSet.size());
|
||||
}
|
||||
}
|
||||
|
||||
public TIntHashSet getSpamDomains() {
|
||||
final TIntHashSet result = new TIntHashSet(1_000_000);
|
||||
|
||||
if (blacklistDisabled) {
|
||||
return result;
|
||||
}
|
||||
|
||||
try (var connection = dataSource.getConnection()) {
|
||||
try (var stmt = connection.prepareStatement("""
|
||||
SELECT EC_DOMAIN.ID
|
||||
FROM EC_DOMAIN
|
||||
INNER JOIN EC_DOMAIN_BLACKLIST
|
||||
ON (EC_DOMAIN_BLACKLIST.URL_DOMAIN = EC_DOMAIN.DOMAIN_TOP
|
||||
OR EC_DOMAIN_BLACKLIST.URL_DOMAIN = EC_DOMAIN.DOMAIN_NAME)
|
||||
"""))
|
||||
{
|
||||
stmt.setFetchSize(1000);
|
||||
var rsp = stmt.executeQuery();
|
||||
while (rsp.next()) {
|
||||
result.add(rsp.getInt(1));
|
||||
}
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
logger.error("Failed to load spam domain list", ex);
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBlacklisted(int domainId) {
|
||||
|
||||
if (spamDomainSet.contains(domainId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -0,0 +1,162 @@
|
||||
package nu.marginalia.db;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class DomainRankingSetsService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(DomainRankingSetsService.class);
|
||||
private final HikariDataSource dataSource;
|
||||
|
||||
@Inject
|
||||
public DomainRankingSetsService(HikariDataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
public Optional<DomainRankingSet> get(String name) throws SQLException {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
SELECT NAME, DESCRIPTION, DEPTH, DEFINITION
|
||||
FROM CONF_DOMAIN_RANKING_SET
|
||||
WHERE NAME = ?
|
||||
""")) {
|
||||
stmt.setString(1, name);
|
||||
var rs = stmt.executeQuery();
|
||||
|
||||
if (!rs.next()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(new DomainRankingSet(
|
||||
rs.getString("NAME"),
|
||||
rs.getString("DESCRIPTION"),
|
||||
rs.getInt("DEPTH"),
|
||||
rs.getString("DEFINITION")
|
||||
));
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
logger.error("Failed to get domain set", ex);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public void upsert(DomainRankingSet domainRankingSet) {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
REPLACE INTO CONF_DOMAIN_RANKING_SET(NAME, DESCRIPTION, DEPTH, DEFINITION)
|
||||
VALUES (?, ?, ?, ?)
|
||||
"""))
|
||||
{
|
||||
stmt.setString(1, domainRankingSet.name());
|
||||
stmt.setString(2, domainRankingSet.description());
|
||||
stmt.setInt(3, domainRankingSet.depth());
|
||||
stmt.setString(4, domainRankingSet.definition());
|
||||
stmt.executeUpdate();
|
||||
|
||||
if (!conn.getAutoCommit())
|
||||
conn.commit();
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
logger.error("Failed to update domain set", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void delete(DomainRankingSet domainRankingSet) {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
DELETE FROM CONF_DOMAIN_RANKING_SET
|
||||
WHERE NAME = ?
|
||||
"""))
|
||||
{
|
||||
stmt.setString(1, domainRankingSet.name());
|
||||
stmt.executeUpdate();
|
||||
|
||||
if (!conn.getAutoCommit())
|
||||
conn.commit();
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
logger.error("Failed to delete domain set", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public List<DomainRankingSet> getAll() {
|
||||
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
SELECT NAME, DESCRIPTION, DEPTH, DEFINITION
|
||||
FROM CONF_DOMAIN_RANKING_SET
|
||||
""")) {
|
||||
var rs = stmt.executeQuery();
|
||||
List<DomainRankingSet> ret = new ArrayList<>();
|
||||
|
||||
while (rs.next()) {
|
||||
ret.add(
|
||||
new DomainRankingSet(
|
||||
rs.getString("NAME"),
|
||||
rs.getString("DESCRIPTION"),
|
||||
rs.getInt("DEPTH"),
|
||||
rs.getString("DEFINITION"))
|
||||
);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
logger.error("Failed to get domain set", ex);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a domain ranking set, parameters for the ranking algorithms.
|
||||
*
|
||||
* @param name Key and name of the set
|
||||
* @param description Human-readable description
|
||||
* @param depth Depth of the algorithm
|
||||
* @param definition Definition of the set, typically a list of domains or globs for domain-names
|
||||
*/
|
||||
public record DomainRankingSet(String name,
|
||||
String description,
|
||||
int depth,
|
||||
String definition) {
|
||||
|
||||
public Path fileName(Path base) {
|
||||
return base.resolve(name().toLowerCase() + ".dat");
|
||||
}
|
||||
|
||||
public String[] domains() {
|
||||
return Arrays.stream(definition().split("\n+"))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isBlank())
|
||||
.filter(s -> !s.startsWith("#"))
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
public boolean isSpecial() {
|
||||
return name().equals("BLOGS") || name().equals("NONE") || name().equals("RANK");
|
||||
}
|
||||
|
||||
public DomainRankingSet withName(String name) {
|
||||
return this.name == name ? this : new DomainRankingSet(name, description, depth, definition);
|
||||
}
|
||||
|
||||
public DomainRankingSet withDescription(String description) {
|
||||
return this.description == description ? this : new DomainRankingSet(name, description, depth, definition);
|
||||
}
|
||||
|
||||
public DomainRankingSet withDepth(int depth) {
|
||||
return this.depth == depth ? this : new DomainRankingSet(name, description, depth, definition);
|
||||
}
|
||||
|
||||
public DomainRankingSet withDefinition(String definition) {
|
||||
return this.definition == definition ? this : new DomainRankingSet(name, description, depth, definition);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,13 +1,13 @@
|
||||
package nu.marginalia.db;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import nu.marginalia.model.EdgeDomain;
|
||||
import nu.marginalia.model.id.EdgeIdList;
|
||||
import gnu.trove.list.TIntList;
|
||||
import gnu.trove.list.array.TIntArrayList;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
@@ -22,8 +22,9 @@ public class DomainTypes {
|
||||
|
||||
public enum Type {
|
||||
BLOG,
|
||||
CRAWL,
|
||||
TEST
|
||||
};
|
||||
}
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DomainTypes.class);
|
||||
|
||||
@@ -34,6 +35,32 @@ public class DomainTypes {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
public String getUrlForSelection(Type type) {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var qs = conn.prepareStatement("SELECT SOURCE FROM DOMAIN_SELECTION_TYPE WHERE NAME = ?"))
|
||||
{
|
||||
qs.setString(1, type.name());
|
||||
var rs = qs.executeQuery();
|
||||
if (rs.next()) {
|
||||
return rs.getString("SOURCE");
|
||||
}
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public void updateUrlForSelection(Type type, String newValue) throws SQLException {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var us = conn.prepareStatement("REPLACE INTO DOMAIN_SELECTION_TYPE(NAME, SOURCE) VALUES (?, ?)")) {
|
||||
us.setString(1, type.name());
|
||||
us.setString(2, newValue);
|
||||
us.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/** Get all domains of a certain type, including domains that are not in the EC_DOMAIN table */
|
||||
public List<String> getAllDomainsByType(Type type) {
|
||||
List<String> ret = new ArrayList<>();
|
||||
@@ -58,10 +85,10 @@ public class DomainTypes {
|
||||
return ret;
|
||||
}
|
||||
|
||||
/** Retrieve the EdgeId of all domains of a certain type,
|
||||
/** Retrieve the domain id of all domains of a certain type,
|
||||
* ignoring entries that are not in the EC_DOMAIN table */
|
||||
public EdgeIdList<EdgeDomain> getKnownDomainsByType(Type type) {
|
||||
EdgeIdList<EdgeDomain> ret = new EdgeIdList<>();
|
||||
public TIntList getKnownDomainsByType(Type type) {
|
||||
TIntList ret = new TIntArrayList();
|
||||
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
@@ -135,7 +162,18 @@ public class DomainTypes {
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> downloadList(Type type) throws IOException {
|
||||
var url = getUrlForSelection(type);
|
||||
if (url.isBlank())
|
||||
return List.of();
|
||||
return downloadDomainsList(url);
|
||||
}
|
||||
|
||||
|
||||
private List<String> downloadDomainsList(String source) throws IOException {
|
||||
if (source.isBlank())
|
||||
return List.of();
|
||||
|
||||
List<String> ret = new ArrayList<>();
|
||||
|
||||
logger.info("Downloading domain list from {}", source);
|
@@ -17,21 +17,15 @@ It's well documented and these are probably the only four tasks you'll ever need
|
||||
If you are not running the system via docker, you need to provide alternative connection details than
|
||||
the defaults (TODO: how?).
|
||||
|
||||
The migration files are in [resources/db/migration](src/main/resources/db/migration). The file name convention
|
||||
The migration files are in [resources/db/migration](resources/db/migration). The file name convention
|
||||
incorporates the project's cal-ver versioning; and are applied in lexicographical order.
|
||||
|
||||
VYY_MM_v_nnn__description.sql
|
||||
|
||||
## Central Paths
|
||||
|
||||
* [migrations](src/main/resources/db/migration) - Flyway migrations
|
||||
* [migrations](resources/db/migration) - Flyway migrations
|
||||
|
||||
## See Also
|
||||
|
||||
* [common/service](../service) implements DatabaseModule, which is from where the services get database connections.
|
||||
|
||||
## Relation diagrams for EC_DOMAIN and EC_URL
|
||||
|
||||

|
||||
|
||||

|
||||
|
@@ -0,0 +1,9 @@
|
||||
ALTER TABLE FILE_STORAGE MODIFY COLUMN TYPE ENUM ('CRAWL_SPEC', 'CRAWL_DATA', 'PROCESSED_DATA', 'INDEX_STAGING', 'LEXICON_STAGING', 'INDEX_LIVE', 'LEXICON_LIVE', 'SEARCH_SETS', 'BACKUP', 'EXPORT', 'LINKDB_LIVE', 'LINKDB_STAGING') NOT NULL;
|
||||
|
||||
INSERT IGNORE INTO FILE_STORAGE(BASE_ID, PATH, DESCRIPTION, TYPE)
|
||||
SELECT ID, 'ldbr', "Linkdb Current", 'LINKDB_LIVE'
|
||||
FROM FILE_STORAGE_BASE WHERE NAME='Index Storage';
|
||||
|
||||
INSERT IGNORE INTO FILE_STORAGE(BASE_ID, PATH, DESCRIPTION, TYPE)
|
||||
SELECT ID, 'ldbw', "Linkdb Staging Area", 'LINKDB_STAGING'
|
||||
FROM FILE_STORAGE_BASE WHERE NAME='Index Storage';
|
@@ -0,0 +1,3 @@
|
||||
DROP VIEW EC_URL_VIEW;
|
||||
DROP TABLE EC_PAGE_DATA;
|
||||
DROP TABLE EC_URL;
|
@@ -0,0 +1,3 @@
|
||||
INSERT IGNORE INTO FILE_STORAGE_BASE(NAME, PATH, TYPE, PERMIT_TEMP)
|
||||
VALUES
|
||||
('Backup Storage', '/backup', 'BACKUP', true);
|
@@ -0,0 +1 @@
|
||||
DELETE FROM FILE_STORAGE WHERE TYPE IN ('LEXICON_STAGING', 'LEXICON_LIVE');
|
@@ -0,0 +1,21 @@
|
||||
ALTER TABLE FILE_STORAGE_BASE MODIFY COLUMN NAME VARCHAR(255) NOT NULL;
|
||||
ALTER TABLE FILE_STORAGE_BASE MODIFY COLUMN PATH VARCHAR(255) NOT NULL;
|
||||
DROP INDEX PATH ON FILE_STORAGE_BASE;
|
||||
DROP INDEX NAME ON FILE_STORAGE_BASE;
|
||||
ALTER TABLE FILE_STORAGE_BASE ADD COLUMN NODE INT NOT NULL DEFAULT -1;
|
||||
CREATE UNIQUE INDEX FILE_STORAGE_BASE__NODE_NAME ON FILE_STORAGE_BASE(NODE, NAME);
|
||||
CREATE UNIQUE INDEX FILE_STORAGE_BASE__NODE_PATH ON FILE_STORAGE_BASE(NODE, PATH);
|
||||
|
||||
|
||||
DROP VIEW FILE_STORAGE_VIEW;
|
||||
CREATE VIEW FILE_STORAGE_VIEW
|
||||
AS SELECT
|
||||
CONCAT(BASE.PATH, '/', STORAGE.PATH) AS PATH,
|
||||
STORAGE.TYPE AS TYPE,
|
||||
NODE AS NODE,
|
||||
DESCRIPTION AS DESCRIPTION,
|
||||
CREATE_DATE AS CREATE_DATE,
|
||||
STORAGE.ID AS ID,
|
||||
BASE.ID AS BASE_ID
|
||||
FROM FILE_STORAGE STORAGE
|
||||
INNER JOIN FILE_STORAGE_BASE BASE ON STORAGE.BASE_ID=BASE.ID;
|
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE TASK_HEARTBEAT ADD COLUMN NODE INT NOT NULL DEFAULT -1;
|
||||
ALTER TABLE PROCESS_HEARTBEAT ADD COLUMN NODE INT NOT NULL DEFAULT -1;
|
||||
ALTER TABLE SERVICE_HEARTBEAT ADD COLUMN NODE INT NOT NULL DEFAULT -1;
|
@@ -0,0 +1,17 @@
|
||||
ALTER TABLE FILE_STORAGE ADD COLUMN STATE VARCHAR(255) NOT NULL DEFAULT '';
|
||||
ALTER TABLE FILE_STORAGE DROP COLUMN DO_PURGE;
|
||||
|
||||
DROP VIEW FILE_STORAGE_VIEW;
|
||||
|
||||
CREATE VIEW FILE_STORAGE_VIEW
|
||||
AS SELECT
|
||||
CONCAT(BASE.PATH, '/', STORAGE.PATH) AS PATH,
|
||||
STORAGE.TYPE AS TYPE,
|
||||
STATE AS STATE,
|
||||
NODE AS NODE,
|
||||
DESCRIPTION AS DESCRIPTION,
|
||||
CREATE_DATE AS CREATE_DATE,
|
||||
STORAGE.ID AS ID,
|
||||
BASE.ID AS BASE_ID
|
||||
FROM FILE_STORAGE STORAGE
|
||||
INNER JOIN FILE_STORAGE_BASE BASE ON STORAGE.BASE_ID=BASE.ID;
|
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE NODE_CONFIGURATION (
|
||||
ID INT PRIMARY KEY,
|
||||
DESCRIPTION VARCHAR(255),
|
||||
ACCEPT_QUERIES BOOLEAN,
|
||||
AUTO_CLEAN BOOLEAN DEFAULT TRUE,
|
||||
PRECESSION BOOLEAN DEFAULT TRUE,
|
||||
DISABLED BOOLEAN DEFAULT FALSE
|
||||
);
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user