mirror of
https://github.com/MarginaliaSearch/MarginaliaSearch.git
synced 2025-10-05 21:22:39 +02:00
Compare commits
2309 Commits
v23.06.0
...
deploy-028
Author | SHA1 | Date | |
---|---|---|---|
|
6ee01dabea | ||
|
1b80e282a7 | ||
|
a65d18f1d1 | ||
|
6e214293e5 | ||
|
52582a6d7d | ||
|
ec0e39ad32 | ||
|
6a15aee4b0 | ||
|
bd5111e8a2 | ||
|
1ecbeb0272 | ||
|
390f053406 | ||
|
b03c43224c | ||
|
9b4ce9e9eb | ||
|
81ac02a695 | ||
|
47f624fb3b | ||
|
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 | ||
|
e8c0648e04 | ||
|
ebc84c22fb | ||
|
8bd9a00c38 | ||
|
972d03efdf | ||
|
aa0d256d6a | ||
|
4d75fa2908 | ||
|
1a05cba60a | ||
|
bf92c270dc | ||
|
e507844616 | ||
|
ca12dd59f7 | ||
|
6f222b9800 | ||
|
fca62f261e | ||
|
c7f0276005 | ||
|
46409c4c2d | ||
|
46df58d28b | ||
|
15912f31d0 | ||
|
dd380a5fb3 | ||
|
93f49f1fb3 | ||
|
b83bb5a48a | ||
|
704de50a9b | ||
|
fcfe07fb7d | ||
|
ccf4990add | ||
|
f2638dd845 | ||
|
239980ecae | ||
|
6cb784df75 | ||
|
efee904531 | ||
|
bee815b1c4 | ||
|
e296b02649 | ||
|
2656fcfe2c | ||
|
c019a029ec | ||
|
db0216936e | ||
|
46d761f34f | ||
|
4598c7f40f | ||
|
1d486bddee | ||
|
606db54dc8 | ||
|
d8073f0dde | ||
|
df85468c01 | ||
|
4404ad98ae | ||
|
e7192a9cad | ||
|
019b61b330 | ||
|
f997707049 | ||
|
c56ee10185 | ||
|
8210e49b4e | ||
|
e51bf8619d | ||
|
69b28fd07d | ||
|
99884c2c7e | ||
|
a8f2e9ee2c | ||
|
a91b909103 | ||
|
d6b8b38955 | ||
|
99e031c529 | ||
|
998f239ed9 | ||
|
0961f627b1 | ||
|
6483308bb0 | ||
|
a42f707b2d | ||
|
eef37927ba | ||
|
7440da240d | ||
|
d0239368e2 | ||
|
4f8048be31 | ||
|
807fb2d052 | ||
|
ce293029c7 | ||
|
b5ed21be21 | ||
|
251fc63b42 | ||
|
47f3855a4b | ||
|
71dfe9f33e | ||
|
afad4f5ebb | ||
|
4ab1cd9502 | ||
|
52e2ab45bf | ||
|
be444f9172 | ||
|
715d61dfea | ||
|
bf37a3eb25 | ||
|
c2b45bec8d | ||
|
cdfe284f9a | ||
|
08eed17e66 | ||
|
00eb8b90dc | ||
|
912129311d | ||
|
624b78ec3a | ||
|
1d0cea1d55 | ||
|
f01f608474 | ||
|
c22feaf42e | ||
|
63e857f7cd | ||
|
9979c9defe | ||
|
7763df0715 | ||
|
e088eb9ec8 | ||
|
19402772fc | ||
|
ba724bc1b2 | ||
|
8de3e6ab80 | ||
|
659d2134ba | ||
|
867410c66b | ||
|
483c2dbb44 | ||
|
e5c9791b14 | ||
|
58556af6c7 | ||
|
2e29038ecd | ||
|
36a23707c1 | ||
|
c1ea60b399 | ||
|
b08e302dd5 | ||
|
ea66195b97 | ||
|
86a5cc5c5f | ||
|
8f0cbf267b | ||
|
2f8488610a | ||
|
d95f01b701 | ||
|
c9d7635370 | ||
|
6b5fb0f841 | ||
|
12bd74d4f3 | ||
|
37c4cc68ed | ||
|
1c948eb3d8 | ||
|
cd90ca820f | ||
|
9786f82220 | ||
|
6f4e767a04 | ||
|
5411950b87 | ||
|
6ff7e9648f | ||
|
5c071ce4d3 | ||
|
caf3d231a8 | ||
|
730e8f74e4 | ||
|
aba134284f | ||
|
2a6183f9e0 | ||
|
ee143bbc48 | ||
|
d3f01bd171 | ||
|
05ba3bab96 | ||
|
d2b6b2044c | ||
|
7611b7900d | ||
|
9ad32ee9c7 | ||
|
866db6c63f | ||
|
01476577b8 | ||
|
e237df4a10 | ||
|
f11103d31d | ||
|
9288d311d4 | ||
|
77d5e39fe0 | ||
|
27e781761d | ||
|
92cac52813 | ||
|
66bb12e55a | ||
|
a5d980ee56 | ||
|
19c2ceec9b | ||
|
507f26ad47 | ||
|
fd44e09ebd | ||
|
09fd0a1d0e | ||
|
667b0ca0b0 | ||
|
a56953c798 | ||
|
7470c170b1 | ||
|
bc330acfc9 | ||
|
789e8eea85 | ||
|
35b29e4f9e | ||
|
69f333c0bf | ||
|
c069c8c182 | ||
|
9e4aa7da7c | ||
|
e22e65eee4 | ||
|
cb55c76664 | ||
|
d6b07e4d01 | ||
|
995657c6ce | ||
|
58f2f86ea8 | ||
|
7bc1cff286 | ||
|
8f455f3b6d | ||
|
f91d92cccb | ||
|
08ca6399ec | ||
|
c0b5ea0e7d | ||
|
f21a3983aa | ||
|
f6e2216b87 | ||
|
92ed513e4f | ||
|
d7ab21fe34 | ||
|
bca4bbb6c8 | ||
|
e618aa34e9 | ||
|
6e41e78f36 | ||
|
c4dd9a0547 | ||
|
5ec10634d8 | ||
|
cdae74d395 | ||
|
8b74e3aa0d | ||
|
23169ad818 | ||
|
d36e36c8fd | ||
|
948d4d5f08 | ||
|
0960e18f8e | ||
|
825fd10efa | ||
|
1ec6f9cde2 | ||
|
a5118fe8f1 | ||
|
6c88f00a9d | ||
|
bf783dad7a | ||
|
8a53e107fa | ||
|
0ed938545b | ||
|
480abfe966 | ||
|
89e4343fdb | ||
|
8c16a2aede | ||
|
5deec63667 | ||
|
363368b150 | ||
|
74caf9e38a | ||
|
7087ab5f07 | ||
|
0b0cf48849 | ||
|
00d9773b44 | ||
|
ac2d7034db | ||
|
88b9ec70c6 | ||
|
77261a38cd | ||
|
3c7c77fe21 | ||
|
4ee3f6ba3f | ||
|
4c016b0318 | ||
|
f59cab300e | ||
|
ec7826659a | ||
|
98b5f22104 | ||
|
2283ceb77d | ||
|
fba466d6e2 | ||
|
cbbf60a599 | ||
|
c125d8ab48 | ||
|
f03146de4b | ||
|
dbb758d1a8 | ||
|
da8bcc6e24 | ||
|
96eecc6ea5 | ||
|
74644d59f3 | ||
|
0f9b90eb1c | ||
|
ae9537b68e | ||
|
2619d196bb | ||
|
17db23c2c1 | ||
|
040bea1f75 | ||
|
dc8277223a | ||
|
98d1898610 | ||
|
1400fb4a9b | ||
|
647bbfa617 | ||
|
b73fcc19fe | ||
|
d9e6c4f266 | ||
|
34653f03a2 | ||
|
f0a8ca440f | ||
|
d89db10645 | ||
|
413dc6ced4 | ||
|
78f21dd19a | ||
|
2cb209ae9c | ||
|
979a620ead | ||
|
7a17933c65 | ||
|
019fa763cd | ||
|
097a163cf5 | ||
|
2ae0b8c159 | ||
|
31ae71c7d6 | ||
|
5ce894564c | ||
|
813fa08bdd | ||
|
e5792ba8b3 | ||
|
62cc9df206 | ||
|
42375f0e53 | ||
|
24dce8c03b | ||
|
eda615de0f | ||
|
a000256223 | ||
|
9bd0e3ce58 | ||
|
b4d1e0e81e | ||
|
d2fdaafc7a | ||
|
7d86586594 | ||
|
11c26e700e | ||
|
8274e8a953 | ||
|
42afe490b7 |
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,6 +1,7 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
polar: marginalia-search
|
||||
github: MarginaliaSearch
|
||||
patreon: marginalia_nu
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ build/
|
||||
lombok.config
|
||||
Dockerfile
|
||||
run
|
||||
jte-classes
|
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.
|
51
README.md
51
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).
|
||||
|
||||
@@ -51,6 +66,30 @@ You can email <kontakt@marginalia.nu> with any questions or feedback.
|
||||
The bulk of the project is available with AGPL 3.0, with exceptions. Some parts are co-licensed under MIT,
|
||||
third party code may have different licenses. See the appropriate readme.md / license.md.
|
||||
|
||||
## Versioning
|
||||
|
||||
The project uses modified Calendar Versioning, where the first two pairs of numbers are a year and month coinciding
|
||||
with the latest crawling operation, and the third number is a patch number.
|
||||
|
||||
```
|
||||
version
|
||||
--
|
||||
yy.mm.VV
|
||||
-----
|
||||
crawl
|
||||
```
|
||||
|
||||
For example, `23.03.02` is a release with crawl data from March 2023 (released in May 2023).
|
||||
It is the second patch for the 23.02 release.
|
||||
|
||||
Versions with the same year and month are compatible with each other, or offer an upgrade path where the same
|
||||
data set can be used, but across different crawl sets data format changes may be introduced, and you're generally
|
||||
expected to re-crawl the data from scratch as crawler data has shelf life approximately as long as the major release
|
||||
cycles of this project. After about 2-3 months it gets noticeably stale with many dead links.
|
||||
|
||||
For development purposes, crawling is discouraged and sample data is available. See [📄 run/readme.md](run/readme.md)
|
||||
for more information.
|
||||
|
||||
## Funding
|
||||
|
||||
### Donations
|
||||
|
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)
|
||||
|
55
build.gradle
55
build.gradle
@@ -1,6 +1,11 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id("org.jetbrains.gradle.plugin.idea-ext") version "1.0"
|
||||
id "me.champeau.jmh" version "0.6.6"
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
group 'marginalia'
|
||||
@@ -9,27 +14,61 @@ version 'SNAPSHOT'
|
||||
compileJava.options.encoding = "UTF-8"
|
||||
compileTestJava.options.encoding = "UTF-8"
|
||||
|
||||
task dist(type: Copy) {
|
||||
from subprojects.collect { it.tasks.withType(Tar) }
|
||||
into "$buildDir/dist"
|
||||
subprojects.forEach {it ->
|
||||
// Enable preview features for the entire project
|
||||
|
||||
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 reproducible builds for the entire project
|
||||
it.tasks.withType(AbstractArchiveTask).configureEach {
|
||||
preserveFileTimestamps = false
|
||||
reproducibleFileOrder = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ext {
|
||||
jvmVersion = 24
|
||||
dockerImageBase='container-registry.oracle.com/graalvm/jdk:24'
|
||||
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/samples"))
|
||||
excludeDirs.add(file("$projectDir/run/dist"))
|
||||
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(17))
|
||||
languageVersion.set(JavaLanguageVersion.of(rootProject.ext.jvmVersion))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,43 +0,0 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id "io.freefair.lombok" version "5.3.3.3"
|
||||
|
||||
id 'jvm-test-suite'
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(17))
|
||||
}
|
||||
}
|
||||
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 "5.3.3.3"
|
||||
|
||||
id 'jvm-test-suite'
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(17))
|
||||
}
|
||||
}
|
||||
|
||||
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: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,45 +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.SearchResultItem;
|
||||
import nu.marginalia.index.client.model.results.SearchResultSet;
|
||||
import nu.marginalia.model.gson.GsonFactory;
|
||||
import nu.marginalia.service.descriptor.ServiceDescriptors;
|
||||
import nu.marginalia.service.id.ServiceId;
|
||||
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import java.util.List;
|
||||
|
||||
@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();
|
||||
|
||||
@Inject
|
||||
public IndexClient(ServiceDescriptors descriptors) {
|
||||
super(descriptors.forId(ServiceId.Index), WmsaHome.getHostsFile(), GsonFactory::get);
|
||||
|
||||
setTimeout(30);
|
||||
}
|
||||
|
||||
@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,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,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,92 +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.DocumentFlags;
|
||||
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;
|
||||
|
||||
public SearchResultKeywordScore(int subquery,
|
||||
String keyword,
|
||||
long encodedWordMetadata,
|
||||
long encodedDocMetadata,
|
||||
boolean hasPriorityTerms) {
|
||||
this.subquery = subquery;
|
||||
this.keyword = keyword;
|
||||
this.encodedWordMetadata = encodedWordMetadata;
|
||||
this.encodedDocMetadata = encodedDocMetadata;
|
||||
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 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,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,10 +0,0 @@
|
||||
# Core Service Clients
|
||||
|
||||
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).
|
@@ -1,44 +0,0 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id "io.freefair.lombok" version "5.3.3.3"
|
||||
|
||||
id 'jvm-test-suite'
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(17))
|
||||
}
|
||||
}
|
||||
|
||||
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 @@
|
||||
# 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,34 +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.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;
|
||||
|
||||
@Singleton
|
||||
public class SearchClient extends AbstractDynamicClient {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
@Inject
|
||||
public SearchClient(ServiceDescriptors descriptors) {
|
||||
super(descriptors.forId(ServiceId.Search), WmsaHome.getHostsFile(), GsonFactory::get);
|
||||
}
|
||||
|
||||
@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,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,41 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id "io.freefair.lombok" version "5.3.3.3"
|
||||
|
||||
|
||||
id 'jvm-test-suite'
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(17))
|
||||
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.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) {}
|
117
code/common/config/java/nu/marginalia/WmsaHome.java
Normal file
117
code/common/config/java/nu/marginalia/WmsaHome.java
Normal file
@@ -0,0 +1,117 @@
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -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,51 @@
|
||||
package nu.marginalia.storage;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
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;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Optional;
|
||||
|
||||
record FileStorageManifest(FileStorageType type, String description) {
|
||||
private static final Gson gson = GsonFactory.get();
|
||||
private static final String fileName = "marginalia-manifest.json";
|
||||
private static final Logger logger = LoggerFactory.getLogger(FileStorageManifest.class);
|
||||
|
||||
public static Optional<FileStorageManifest> find(Path directory) {
|
||||
Path expectedFileName = directory.resolve(fileName);
|
||||
|
||||
if (!Files.isRegularFile(expectedFileName) ||
|
||||
!Files.isReadable(expectedFileName)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try (var reader = Files.newBufferedReader(expectedFileName)) {
|
||||
return Optional.of(gson.fromJson(reader, FileStorageManifest.class));
|
||||
}
|
||||
catch (Exception e) {
|
||||
logger.warn("Failed to read manifest " + expectedFileName, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public void write(FileStorage dir) {
|
||||
Path expectedFileName = dir.asPath().resolve(fileName);
|
||||
|
||||
try (var writer = Files.newBufferedWriter(expectedFileName,
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING))
|
||||
{
|
||||
gson.toJson(this, writer);
|
||||
}
|
||||
catch (Exception e) {
|
||||
logger.warn("Failed to write manifest " + expectedFileName, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
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(long storageId) {
|
||||
return new FileStorageId(storageId);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return Long.toString(id);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -1,22 +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 LanguageModels(Path ngramBloomFilter, Path termFrequencies, Path openNLPSentenceDetectionData, Path posRules, Path posDict, Path openNLPTokenData) {
|
||||
this.ngramBloomFilter = ngramBloomFilter;
|
||||
this.termFrequencies = termFrequencies;
|
||||
this.openNLPSentenceDetectionData = openNLPSentenceDetectionData;
|
||||
this.posRules = posRules;
|
||||
this.posDict = posDict;
|
||||
this.openNLPTokenData = openNLPTokenData;
|
||||
}
|
||||
}
|
@@ -1,5 +0,0 @@
|
||||
package nu.marginalia;
|
||||
|
||||
public record UserAgent(String uaString) {
|
||||
|
||||
}
|
@@ -1,127 +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.Properties;
|
||||
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 Path getDisk(String name) {
|
||||
var pathStr = getDiskProperties().getProperty(name);
|
||||
if (null == pathStr) {
|
||||
throw new RuntimeException("Disk " + name + " was not configured");
|
||||
}
|
||||
Path p = Path.of(pathStr);
|
||||
if (!Files.isDirectory(p)) {
|
||||
throw new RuntimeException("Disk " + name + " does not exist or is not a directory!");
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
public static Properties getDiskProperties() {
|
||||
Path settingsFile = getHomePath().resolve("conf/disks.properties");
|
||||
|
||||
if (!Files.isRegularFile(settingsFile)) {
|
||||
throw new RuntimeException("Could not find disk settings " + settingsFile);
|
||||
}
|
||||
|
||||
try (var is = Files.newInputStream(settingsFile)) {
|
||||
var props = new Properties();
|
||||
props.load(is);
|
||||
return props;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
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,23 +1,42 @@
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'org.flywaydb:flyway-mysql:10.0.1'
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
id "io.freefair.lombok" version "5.3.3.3"
|
||||
|
||||
id 'jvm-test-suite'
|
||||
id "org.flywaydb.flyway" version "10.0.1"
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(17))
|
||||
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
|
||||
@@ -26,25 +45,28 @@ dependencies {
|
||||
|
||||
implementation libs.trove
|
||||
|
||||
implementation libs.rxjava
|
||||
implementation libs.bundles.mariadb
|
||||
flywayMigration 'org.flywaydb:flyway-mysql:10.0.1'
|
||||
|
||||
testImplementation libs.bundles.slf4j.test
|
||||
testImplementation libs.bundles.junit
|
||||
testImplementation libs.mockito
|
||||
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
flyway {
|
||||
url = 'jdbc:mariadb://localhost:3306/WMSA_prod'
|
||||
user = 'wmsa'
|
||||
password = 'wmsa'
|
||||
schemas = ['WMSA_prod']
|
||||
configurations = [ 'compileClasspath', 'flywayMigration' ]
|
||||
locations = ['filesystem:src/main/resources/db/migration']
|
||||
cleanDisabled = false
|
||||
}
|
||||
|
||||
|
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) { }
|
||||
}
|
@@ -2,16 +2,12 @@ package nu.marginalia.db;
|
||||
|
||||
import com.google.inject.ImplementedBy;
|
||||
import gnu.trove.set.hash.TIntHashSet;
|
||||
import nu.marginalia.model.EdgeDomain;
|
||||
import nu.marginalia.model.id.EdgeId;
|
||||
|
||||
@ImplementedBy(DomainBlacklistImpl.class)
|
||||
public interface DomainBlacklist {
|
||||
boolean isBlacklisted(int domainId);
|
||||
default boolean isBlacklisted(EdgeId<EdgeDomain> domainId) {
|
||||
return isBlacklisted(domainId.id());
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
217
code/common/db/java/nu/marginalia/db/DomainTypes.java
Normal file
217
code/common/db/java/nu/marginalia/db/DomainTypes.java
Normal file
@@ -0,0 +1,217 @@
|
||||
package nu.marginalia.db;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import gnu.trove.list.TIntList;
|
||||
import gnu.trove.list.array.TIntArrayList;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** A list of domains that are known to be of a certain type */
|
||||
@Singleton
|
||||
public class DomainTypes {
|
||||
|
||||
public enum Type {
|
||||
BLOG,
|
||||
CRAWL,
|
||||
TEST
|
||||
}
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DomainTypes.class);
|
||||
|
||||
private final HikariDataSource dataSource;
|
||||
|
||||
@Inject
|
||||
public DomainTypes(HikariDataSource dataSource) {
|
||||
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<>();
|
||||
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
SELECT DOMAIN_NAME
|
||||
FROM DOMAIN_SELECTION INNER JOIN DOMAIN_SELECTION_TYPE ON DOMAIN_TYPE_ID = DOMAIN_SELECTION_TYPE.ID
|
||||
WHERE DOMAIN_SELECTION_TYPE.NAME = ?
|
||||
"""))
|
||||
{
|
||||
stmt.setString(1, type.name());
|
||||
var rs = stmt.executeQuery();
|
||||
while (rs.next()) {
|
||||
ret.add(rs.getString(1));
|
||||
}
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/** Retrieve the domain id of all domains of a certain type,
|
||||
* ignoring entries that are not in the EC_DOMAIN table */
|
||||
public TIntList getKnownDomainsByType(Type type) {
|
||||
TIntList ret = new TIntArrayList();
|
||||
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
SELECT EC_DOMAIN.ID
|
||||
FROM DOMAIN_SELECTION
|
||||
INNER JOIN DOMAIN_SELECTION_TYPE ON DOMAIN_TYPE_ID = DOMAIN_SELECTION_TYPE.ID
|
||||
INNER JOIN EC_DOMAIN ON DOMAIN_SELECTION.DOMAIN_NAME = EC_DOMAIN.DOMAIN_NAME
|
||||
WHERE DOMAIN_SELECTION_TYPE.NAME = ?
|
||||
"""))
|
||||
{
|
||||
stmt.setString(1, type.name());
|
||||
var rs = stmt.executeQuery();
|
||||
while (rs.next()) {
|
||||
ret.add(rs.getInt(1));
|
||||
}
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/** Reload the list of domains of a certain type from the source */
|
||||
public void reloadDomainsList(Type type) throws IOException, SQLException {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
SELECT SOURCE, ID FROM DOMAIN_SELECTION_TYPE WHERE NAME = ?
|
||||
""");
|
||||
var deleteStatement = conn.prepareStatement("""
|
||||
DELETE FROM DOMAIN_SELECTION WHERE DOMAIN_TYPE_ID = ?
|
||||
""");
|
||||
var insertStatement = conn.prepareStatement("""
|
||||
INSERT IGNORE INTO DOMAIN_SELECTION (DOMAIN_NAME, DOMAIN_TYPE_ID) VALUES (?, ?)
|
||||
""")
|
||||
)
|
||||
{
|
||||
stmt.setString(1, type.name());
|
||||
var rsp = stmt.executeQuery();
|
||||
|
||||
if (!rsp.next()) {
|
||||
throw new RuntimeException("No such domain selection type: " + type);
|
||||
}
|
||||
|
||||
var source = rsp.getString(1);
|
||||
int typeId = rsp.getInt(2);
|
||||
|
||||
List<String> downloadDomains = downloadDomainsList(source);
|
||||
|
||||
try {
|
||||
conn.setAutoCommit(false);
|
||||
deleteStatement.setInt(1, typeId);
|
||||
deleteStatement.executeUpdate();
|
||||
|
||||
for (String domain : downloadDomains) {
|
||||
insertStatement.setString(1, domain);
|
||||
insertStatement.setInt(2, typeId);
|
||||
insertStatement.executeUpdate();
|
||||
// Could use batch insert here, but this executes infrequently, so it's not worth the hassle
|
||||
}
|
||||
|
||||
conn.commit();
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
conn.rollback();
|
||||
throw ex;
|
||||
}
|
||||
finally {
|
||||
conn.setAutoCommit(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
try (var br = new BufferedReader(new InputStreamReader(new URL(source).openStream()))) {
|
||||
String line;
|
||||
|
||||
while ((line = br.readLine()) != null) {
|
||||
line = cleanDomainListLine(line);
|
||||
|
||||
|
||||
if (isValidDomainListEntry(line))
|
||||
ret.add(line);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("-- found {}", ret.size());
|
||||
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private String cleanDomainListLine(String line) {
|
||||
line = line.trim();
|
||||
|
||||
int hashIdx = line.indexOf('#');
|
||||
if (hashIdx >= 0)
|
||||
line = line.substring(0, hashIdx).trim();
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
private boolean isValidDomainListEntry(String line) {
|
||||
if (line.isBlank())
|
||||
return false;
|
||||
if (!line.matches("[a-z0-9\\-.]+"))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -2,17 +2,30 @@
|
||||
|
||||
This module primarily contains SQL files for the URLs database. The most central tables are `EC_DOMAIN`, `EC_URL` and `EC_PAGE_DATA`.
|
||||
|
||||
## Flyway
|
||||
|
||||
The system uses flyway to track database changes and allow easy migrations, this is accessible via gradle tasks.
|
||||
|
||||
* `flywayMigrate`
|
||||
* `flywayBaseline`
|
||||
* `flywayRepair`
|
||||
* `flywayClean` (dangerous as in wipes your entire database)
|
||||
|
||||
Refer to the [Flyway documentation](https://documentation.red-gate.com/fd/flyway-documentation-138346877.html) for guidance.
|
||||
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](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
|
||||
|
||||
* [current](src/main/resources/sql/current) - The current database model
|
||||
* [migrations](src/main/resources/sql/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
|
||||
|
||||

|
||||
|
||||

|
||||
|
@@ -1,7 +1,8 @@
|
||||
|
||||
CREATE TABLE IF NOT EXISTS EC_DOMAIN_BLACKLIST (
|
||||
ID INT PRIMARY KEY AUTO_INCREMENT,
|
||||
URL_DOMAIN VARCHAR(255) UNIQUE NOT NULL
|
||||
URL_DOMAIN VARCHAR(255) UNIQUE NOT NULL,
|
||||
COMMENT VARCHAR(255) DEFAULT NULL
|
||||
)
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
@@ -0,0 +1,19 @@
|
||||
CREATE TABLE IF NOT EXISTS DOMAIN_SELECTION_TYPE (
|
||||
ID INT PRIMARY KEY AUTO_INCREMENT,
|
||||
NAME VARCHAR(255) UNIQUE,
|
||||
SOURCE VARCHAR(255) NOT NULL
|
||||
)
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_bin;
|
||||
|
||||
CREATE TABLE DOMAIN_SELECTION (
|
||||
DOMAIN_NAME VARCHAR(255) PRIMARY KEY,
|
||||
DOMAIN_TYPE_ID INT,
|
||||
FOREIGN KEY (DOMAIN_TYPE_ID) REFERENCES DOMAIN_SELECTION_TYPE(ID) ON DELETE CASCADE
|
||||
)
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
INSERT IGNORE INTO DOMAIN_SELECTION_TYPE(NAME, SOURCE)
|
||||
VALUES ('BLOG', 'https://raw.githubusercontent.com/MarginaliaSearch/PublicData/master/sets/blogs.txt'),
|
||||
('TEST', 'https://downloads.marginalia.nu/domain-list-test.txt');
|
@@ -0,0 +1,27 @@
|
||||
CREATE TABLE IF NOT EXISTS SERVICE_HEARTBEAT (
|
||||
SERVICE_NAME VARCHAR(255) PRIMARY KEY COMMENT "Full name of the service, including node id if applicable, e.g. search-service:0",
|
||||
SERVICE_BASE VARCHAR(255) NOT NULL COMMENT "Base name of the service, e.g. search-service",
|
||||
INSTANCE VARCHAR(255) NOT NULL COMMENT "UUID of the service instance",
|
||||
ALIVE BOOLEAN NOT NULL DEFAULT TRUE COMMENT "Set to false when the service is doing an orderly shutdown",
|
||||
HEARTBEAT_TIME TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT "Service was last seen at this point"
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS PROCESS_HEARTBEAT (
|
||||
PROCESS_NAME VARCHAR(255) PRIMARY KEY COMMENT "Full name of the process, including node id if applicable, e.g. converter:0",
|
||||
PROCESS_BASE VARCHAR(255) NOT NULL COMMENT "Base name of the process, e.g. converter",
|
||||
INSTANCE VARCHAR(255) NOT NULL COMMENT "UUID of the process instance",
|
||||
STATUS ENUM ('STARTING', 'RUNNING', 'STOPPED') NOT NULL DEFAULT 'STARTING' COMMENT "Status of the process",
|
||||
PROGRESS INT NOT NULL DEFAULT 0 COMMENT "Progress of the process",
|
||||
HEARTBEAT_TIME TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT "Process was last seen at this point"
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS SERVICE_EVENTLOG(
|
||||
ID BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT "Unique id",
|
||||
SERVICE_NAME VARCHAR(255) NOT NULL COMMENT "Full name of the service, including node id if applicable, e.g. search-service:0",
|
||||
SERVICE_BASE VARCHAR(255) NOT NULL COMMENT "Base name of the service, e.g. search-service",
|
||||
INSTANCE VARCHAR(255) NOT NULL COMMENT "UUID of the service instance",
|
||||
EVENT_TIME TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT "Event time",
|
||||
EVENT_TYPE VARCHAR(255) NOT NULL COMMENT "Event type",
|
||||
EVENT_MESSAGE VARCHAR(255) NOT NULL COMMENT "Event message"
|
||||
);
|
||||
|
@@ -0,0 +1,21 @@
|
||||
CREATE TABLE IF NOT EXISTS MESSAGE_QUEUE (
|
||||
ID BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'Unique id',
|
||||
RELATED_ID BIGINT NOT NULL DEFAULT -1 COMMENT 'Unique id a related message',
|
||||
SENDER_INBOX VARCHAR(255) COMMENT 'Name of the sender inbox',
|
||||
RECIPIENT_INBOX VARCHAR(255) NOT NULL COMMENT 'Name of the recipient inbox',
|
||||
FUNCTION VARCHAR(255) NOT NULL COMMENT 'Which function to run',
|
||||
PAYLOAD TEXT COMMENT 'Message to recipient',
|
||||
-- These fields are used to avoid double processing of messages
|
||||
-- instance marks the unique instance of the party, and the tick marks
|
||||
-- the current polling iteration. Both are necessary.
|
||||
OWNER_INSTANCE VARCHAR(255) COMMENT 'Instance UUID corresponding to the party that has claimed the message',
|
||||
OWNER_TICK BIGINT DEFAULT -1 COMMENT 'Used by recipient to determine which messages it has processed',
|
||||
STATE ENUM('NEW', 'ACK', 'OK', 'ERR', 'DEAD')
|
||||
NOT NULL DEFAULT 'NEW' COMMENT 'Processing state',
|
||||
CREATED_TIME TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'Time of creation',
|
||||
UPDATED_TIME TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'Time of last update',
|
||||
TTL INT COMMENT 'Time to live in seconds'
|
||||
);
|
||||
|
||||
CREATE INDEX MESSAGE_QUEUE_STATE_IDX ON MESSAGE_QUEUE(STATE);
|
||||
CREATE INDEX MESSAGE_QUEUE_OI_TICK_IDX ON MESSAGE_QUEUE(OWNER_INSTANCE, OWNER_TICK);
|
@@ -0,0 +1,42 @@
|
||||
CREATE TABLE IF NOT EXISTS FILE_STORAGE_BASE (
|
||||
ID BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
NAME VARCHAR(255) NOT NULL UNIQUE,
|
||||
PATH VARCHAR(255) NOT NULL UNIQUE COMMENT 'The path to the storage base',
|
||||
TYPE ENUM ('SSD_INDEX', 'SSD_WORK', 'SLOW', 'BACKUP') NOT NULL,
|
||||
PERMIT_TEMP BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'If true, the storage can be used for temporary files'
|
||||
)
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_bin;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS FILE_STORAGE (
|
||||
ID BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
BASE_ID BIGINT NOT NULL,
|
||||
PATH VARCHAR(255) NOT NULL COMMENT 'The path to the storage relative to the base',
|
||||
DESCRIPTION VARCHAR(255) NOT NULL,
|
||||
TYPE ENUM ('CRAWL_SPEC', 'CRAWL_DATA', 'PROCESSED_DATA', 'INDEX_STAGING', 'LEXICON_STAGING', 'INDEX_LIVE', 'LEXICON_LIVE', 'SEARCH_SETS', 'BACKUP', 'EXPORT') NOT NULL,
|
||||
DO_PURGE BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'If true, the storage may be cleaned',
|
||||
CREATE_DATE TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
CONSTRAINT CONS UNIQUE (BASE_ID, PATH),
|
||||
FOREIGN KEY (BASE_ID) REFERENCES FILE_STORAGE_BASE(ID) ON DELETE CASCADE
|
||||
)
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_bin;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS FILE_STORAGE_RELATION (
|
||||
SOURCE_ID BIGINT NOT NULL,
|
||||
TARGET_ID BIGINT NOT NULL,
|
||||
CONSTRAINT CONS UNIQUE (SOURCE_ID, TARGET_ID),
|
||||
FOREIGN KEY (SOURCE_ID) REFERENCES FILE_STORAGE(ID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (TARGET_ID) REFERENCES FILE_STORAGE(ID) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE VIEW FILE_STORAGE_VIEW
|
||||
AS SELECT
|
||||
CONCAT(BASE.PATH, '/', STORAGE.PATH) AS PATH,
|
||||
STORAGE.TYPE AS TYPE,
|
||||
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,28 @@
|
||||
INSERT IGNORE INTO FILE_STORAGE_BASE(NAME, PATH, TYPE, PERMIT_TEMP)
|
||||
VALUES
|
||||
('Index Storage', '/vol', 'SSD_INDEX', false),
|
||||
('Data Storage', '/samples', 'SLOW', true);
|
||||
|
||||
INSERT IGNORE INTO FILE_STORAGE(BASE_ID, PATH, DESCRIPTION, TYPE)
|
||||
SELECT ID, 'iw', "Index Staging Area", 'INDEX_STAGING'
|
||||
FROM FILE_STORAGE_BASE WHERE NAME='Index Storage';
|
||||
|
||||
INSERT IGNORE INTO FILE_STORAGE(BASE_ID, PATH, DESCRIPTION, TYPE)
|
||||
SELECT ID, 'ir', "Index Live Area", 'INDEX_LIVE'
|
||||
FROM FILE_STORAGE_BASE WHERE NAME='Index Storage';
|
||||
|
||||
INSERT IGNORE INTO FILE_STORAGE(BASE_ID, PATH, DESCRIPTION, TYPE)
|
||||
SELECT ID, 'lw', "Lexicon Staging Area", 'LEXICON_STAGING'
|
||||
FROM FILE_STORAGE_BASE WHERE NAME='Index Storage';
|
||||
|
||||
INSERT IGNORE INTO FILE_STORAGE(BASE_ID, PATH, DESCRIPTION, TYPE)
|
||||
SELECT ID, 'lr', "Lexicon Live Area", 'LEXICON_LIVE'
|
||||
FROM FILE_STORAGE_BASE WHERE NAME='Index Storage';
|
||||
|
||||
INSERT IGNORE INTO FILE_STORAGE(BASE_ID, PATH, DESCRIPTION, TYPE)
|
||||
SELECT ID, 'ss', "Search Sets", 'SEARCH_SETS'
|
||||
FROM FILE_STORAGE_BASE WHERE NAME='Index Storage';
|
||||
|
||||
INSERT IGNORE INTO FILE_STORAGE(BASE_ID, PATH, DESCRIPTION, TYPE)
|
||||
SELECT ID, 'export', "Exported Data", 'EXPORT'
|
||||
FROM FILE_STORAGE_BASE WHERE TYPE='EXPORT';
|
@@ -0,0 +1,7 @@
|
||||
INSERT INTO MESSAGE_QUEUE(RECIPIENT_INBOX,FUNCTION,PAYLOAD) VALUES
|
||||
('fsm:converter_monitor','INITIAL',''),
|
||||
('fsm:loader_monitor','INITIAL',''),
|
||||
('fsm:crawler_monitor','INITIAL',''),
|
||||
('fsm:message_queue_monitor','INITIAL',''),
|
||||
('fsm:process_liveness_monitor','INITIAL',''),
|
||||
('fsm:file_storage_monitor','INITIAL','');
|
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE IF NOT EXISTS TASK_HEARTBEAT (
|
||||
TASK_NAME VARCHAR(255) PRIMARY KEY COMMENT "Full name of the task, including node id if applicable, e.g. reconvert:0",
|
||||
TASK_BASE VARCHAR(255) NOT NULL COMMENT "Base name of the task, e.g. reconvert",
|
||||
INSTANCE VARCHAR(255) NOT NULL COMMENT "UUID of the task instance",
|
||||
SERVICE_INSTANCE VARCHAR(255) NOT NULL COMMENT "UUID of the parent service",
|
||||
STATUS ENUM ('STARTING', 'RUNNING', 'STOPPED') NOT NULL DEFAULT 'STARTING' COMMENT "Status of the task",
|
||||
PROGRESS INT NOT NULL DEFAULT 0 COMMENT "Progress of the task",
|
||||
STAGE_NAME VARCHAR(255) DEFAULT "",
|
||||
HEARTBEAT_TIME TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT "Task was last seen at this point"
|
||||
);
|
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX IF NOT EXISTS SERVICE_EVENTLOG__EVENT_TYPE_IDX ON SERVICE_EVENTLOG (EVENT_TYPE);
|
||||
CREATE INDEX IF NOT EXISTS SERVICE_EVENTLOG__SERVICE_NAME_IDX ON SERVICE_EVENTLOG (SERVICE_NAME);
|
@@ -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
|
||||
);
|
@@ -0,0 +1,10 @@
|
||||
ALTER TABLE FILE_STORAGE_BASE DROP COLUMN PERMIT_TEMP;
|
||||
ALTER TABLE FILE_STORAGE_BASE ADD COLUMN TYPE_NEW VARCHAR(255) NOT NULL;
|
||||
|
||||
UPDATE FILE_STORAGE_BASE SET TYPE_NEW = 'CURRENT' WHERE TYPE='SSD_INDEX';
|
||||
UPDATE FILE_STORAGE_BASE SET TYPE_NEW = 'WORK' WHERE TYPE='SSD_WORK';
|
||||
UPDATE FILE_STORAGE_BASE SET TYPE_NEW = 'STORAGE' WHERE TYPE='SLOW';
|
||||
UPDATE FILE_STORAGE_BASE SET TYPE_NEW = 'BACKUP' WHERE TYPE='BACKUP';
|
||||
|
||||
ALTER TABLE FILE_STORAGE_BASE DROP COLUMN TYPE;
|
||||
ALTER TABLE FILE_STORAGE_BASE CHANGE COLUMN TYPE_NEW TYPE VARCHAR(255) NOT NULL;
|
@@ -0,0 +1 @@
|
||||
UPDATE MESSAGE_QUEUE SET STATE='DEAD' WHERE STATE='NEW';
|
@@ -0,0 +1 @@
|
||||
DELETE FROM FILE_STORAGE WHERE TYPE IN ('INDEX_STAGING', 'INDEX_LIVE', 'SEARCH_SETS', 'LINKDB_LIVE', 'LINKDB_STAGING');
|
@@ -0,0 +1 @@
|
||||
ALTER TABLE EC_DOMAIN ADD COLUMN NODE_AFFINITY INT NOT NULL;
|
@@ -0,0 +1,9 @@
|
||||
ALTER TABLE WMSA_prod.EC_DOMAIN_LINK
|
||||
MODIFY COLUMN ID BIGINT NOT NULL AUTO_INCREMENT;
|
||||
|
||||
DELIMITER $$
|
||||
CREATE OR REPLACE PROCEDURE PURGE_LINKS_TABLE (IN nodeId INT)
|
||||
BEGIN
|
||||
DELETE EC_DOMAIN_LINK FROM EC_DOMAIN_LINK INNER JOIN WMSA_prod.EC_DOMAIN ON EC_DOMAIN_LINK.SOURCE_DOMAIN_ID = EC_DOMAIN.ID WHERE NODE_AFFINITY = nodeId;
|
||||
END$$
|
||||
DELIMITER ;
|
@@ -0,0 +1 @@
|
||||
ALTER TABLE WMSA_prod.NODE_CONFIGURATION ADD COLUMN KEEP_WARCS BOOLEAN DEFAULT FALSE;
|
@@ -0,0 +1,12 @@
|
||||
|
||||
CREATE TABLE IF NOT EXISTS CONF_DOMAIN_RANKING_SET (
|
||||
NAME VARCHAR(255) PRIMARY KEY COLLATE utf8mb4_unicode_ci,
|
||||
DESCRIPTION VARCHAR(255) NOT NULL,
|
||||
ALGORITHM VARCHAR(255) NOT NULL,
|
||||
DEPTH INT NOT NULL,
|
||||
DEFINITION LONGTEXT NOT NULL
|
||||
) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
|
||||
|
||||
INSERT IGNORE INTO CONF_DOMAIN_RANKING_SET(NAME, DESCRIPTION, ALGORITHM, DEPTH, DEFINITION) VALUES ('NONE', 'Reserved: No Ranking Algorithm', 'SPECIAL', 50000, '');
|
||||
INSERT IGNORE INTO CONF_DOMAIN_RANKING_SET(NAME, DESCRIPTION, ALGORITHM, DEPTH, DEFINITION) VALUES ('BLOGS', 'Reserved: Blogs Set', 'SPECIAL', 50000, '');
|
||||
INSERT IGNORE INTO CONF_DOMAIN_RANKING_SET(NAME, DESCRIPTION, ALGORITHM, DEPTH, DEFINITION) VALUES ('RANK', 'Reserved: Main Domain Ranking', 'SPECIAL', 50000, '');
|
@@ -0,0 +1 @@
|
||||
ALTER TABLE MESSAGE_QUEUE ADD COLUMN AUDIT_RELATED_ID LONG NOT NULL DEFAULT -1 COMMENT 'To be applied to any new messages created while handling a message';
|
@@ -0,0 +1 @@
|
||||
DROP TABLE EC_DOMAIN_LINK;
|
@@ -0,0 +1 @@
|
||||
ALTER TABLE CONF_DOMAIN_RANKING_SET DROP COLUMN ALGORITHM;
|
@@ -0,0 +1 @@
|
||||
ALTER TABLE WMSA_prod.NODE_CONFIGURATION ADD COLUMN NODE_PROFILE VARCHAR(255) DEFAULT 'MIXED';
|
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE IF NOT EXISTS WMSA_prod.NSFW_DOMAINS (
|
||||
ID INT NOT NULL AUTO_INCREMENT,
|
||||
TIER INT NOT NULL,
|
||||
PRIMARY KEY (ID)
|
||||
);
|
@@ -0,0 +1,213 @@
|
||||
|
||||
-- Create metadata tables for domain ping status and security information
|
||||
|
||||
-- These are not ICMP pings, but rather HTTP(S) pings to check the availability and security
|
||||
-- of web servers associated with domains, to assess uptime and changes in security configurations
|
||||
-- indicating ownership changes or security issues.
|
||||
|
||||
-- Note: DOMAIN_ID and NODE_ID are used to identify the domain and the node that performed the ping.
|
||||
-- These are strictly speaking foreign keys to the EC_DOMAIN table, but as it
|
||||
-- is strictly append-only, we do not need to enforce foreign key constraints.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS DOMAIN_AVAILABILITY_INFORMATION (
|
||||
DOMAIN_ID INT NOT NULL PRIMARY KEY,
|
||||
NODE_ID INT NOT NULL,
|
||||
|
||||
SERVER_AVAILABLE BOOLEAN NOT NULL, -- Indicates if the server is available (true) or not (false)
|
||||
SERVER_IP VARBINARY(16), -- IP address of the server (IPv4 or IPv6)
|
||||
SERVER_IP_ASN INTEGER, -- Autonomous System number
|
||||
|
||||
DATA_HASH BIGINT, -- Hash of the data for integrity checks
|
||||
SECURITY_CONFIG_HASH BIGINT, -- Hash of the security configuration for integrity checks
|
||||
|
||||
HTTP_SCHEMA ENUM('HTTP', 'HTTPS'), -- HTTP or HTTPS protocol used
|
||||
HTTP_ETAG VARCHAR(255), -- ETag of the resource as per HTTP headers
|
||||
HTTP_LAST_MODIFIED VARCHAR(255), -- Last modified date of the resource as per HTTP headers
|
||||
HTTP_STATUS INT, -- HTTP status code (e.g., 200, 404, etc.)
|
||||
HTTP_LOCATION VARCHAR(255), -- If the server redirects, this is the location of the redirect
|
||||
HTTP_RESPONSE_TIME_MS SMALLINT UNSIGNED, -- Response time in milliseconds
|
||||
|
||||
ERROR_CLASSIFICATION ENUM('NONE', 'TIMEOUT', 'SSL_ERROR', 'DNS_ERROR', 'CONNECTION_ERROR', 'HTTP_CLIENT_ERROR', 'HTTP_SERVER_ERROR', 'UNKNOWN'), -- Classification of the error if the server is not available
|
||||
ERROR_MESSAGE VARCHAR(255), -- Error message if the server is not available
|
||||
|
||||
TS_LAST_PING TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- Timestamp of the last ping
|
||||
TS_LAST_AVAILABLE TIMESTAMP, -- Timestamp of the last time the server was available
|
||||
TS_LAST_ERROR TIMESTAMP, -- Timestamp of the last error encountered
|
||||
|
||||
NEXT_SCHEDULED_UPDATE TIMESTAMP NOT NULL,
|
||||
BACKOFF_CONSECUTIVE_FAILURES INT NOT NULL DEFAULT 0, -- Number of consecutive failures to ping the server
|
||||
BACKOFF_FETCH_INTERVAL INT NOT NULL DEFAULT 60 -- Interval in seconds for the next scheduled ping
|
||||
) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS DOMAIN_AVAILABILITY_INFORMATION__NODE_ID__DOMAIN_ID_IDX ON DOMAIN_AVAILABILITY_INFORMATION (NODE_ID, DOMAIN_ID);
|
||||
CREATE INDEX IF NOT EXISTS DOMAIN_AVAILABILITY_INFORMATION__NEXT_SCHEDULED_UPDATE_IDX ON DOMAIN_AVAILABILITY_INFORMATION (NODE_ID, NEXT_SCHEDULED_UPDATE);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS DOMAIN_SECURITY_INFORMATION (
|
||||
DOMAIN_ID INT NOT NULL PRIMARY KEY,
|
||||
NODE_ID INT NOT NULL,
|
||||
|
||||
ASN INTEGER, -- Autonomous System Number (ASN) of the server
|
||||
HTTP_SCHEMA ENUM('HTTP', 'HTTPS'), -- HTTP or HTTPS protocol used
|
||||
HTTP_VERSION VARCHAR(10), -- HTTP version used (e.g., HTTP/1.1, HTTP/2)
|
||||
HTTP_COMPRESSION VARCHAR(50), -- Compression method used (e.g., gzip, deflate, br)
|
||||
HTTP_CACHE_CONTROL TEXT, -- Cache control directives from HTTP headers
|
||||
|
||||
SSL_CERT_NOT_BEFORE TIMESTAMP, -- Valid from date (usually same as issued)
|
||||
SSL_CERT_NOT_AFTER TIMESTAMP, -- Valid until date (usually same as expires)
|
||||
|
||||
SSL_CERT_ISSUER VARCHAR(255), -- CA that issued the cert
|
||||
SSL_CERT_SUBJECT VARCHAR(255), -- Certificate subject/CN
|
||||
|
||||
SSL_CERT_PUBLIC_KEY_HASH BINARY(32), -- SHA-256 hash of the public key
|
||||
SSL_CERT_SERIAL_NUMBER VARCHAR(100), -- Unique cert serial number
|
||||
SSL_CERT_FINGERPRINT_SHA256 BINARY(32), -- SHA-256 fingerprint for exact identification
|
||||
SSL_CERT_SAN TEXT, -- Subject Alternative Names (JSON array)
|
||||
SSL_CERT_WILDCARD BOOLEAN, -- Wildcard certificate (*.example.com)
|
||||
|
||||
SSL_PROTOCOL VARCHAR(20), -- TLS 1.2, TLS 1.3, etc.
|
||||
SSL_CIPHER_SUITE VARCHAR(100), -- e.g., TLS_AES_256_GCM_SHA384
|
||||
SSL_KEY_EXCHANGE VARCHAR(50), -- ECDHE, RSA, etc.
|
||||
SSL_CERTIFICATE_CHAIN_LENGTH TINYINT, -- Number of certs in chain
|
||||
|
||||
SSL_CERTIFICATE_VALID BOOLEAN, -- Valid cert chain
|
||||
|
||||
HEADER_CORS_ALLOW_ORIGIN TEXT, -- Could be *, specific domains, or null
|
||||
HEADER_CORS_ALLOW_CREDENTIALS BOOLEAN, -- Credential handling
|
||||
HEADER_CONTENT_SECURITY_POLICY_HASH INT, -- CSP header, hash of the policy
|
||||
HEADER_STRICT_TRANSPORT_SECURITY VARCHAR(255), -- HSTS header
|
||||
HEADER_REFERRER_POLICY VARCHAR(50), -- Referrer handling
|
||||
HEADER_X_FRAME_OPTIONS VARCHAR(50), -- Clickjacking protection
|
||||
HEADER_X_CONTENT_TYPE_OPTIONS VARCHAR(50), -- MIME sniffing protection
|
||||
HEADER_X_XSS_PROTECTION VARCHAR(50), -- XSS protection header
|
||||
|
||||
HEADER_SERVER VARCHAR(255), -- Server header (e.g., Apache, Nginx, etc.)
|
||||
HEADER_X_POWERED_BY VARCHAR(255), -- X-Powered-By header (if present)
|
||||
|
||||
TS_LAST_UPDATE TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -- Timestamp of the last SSL check
|
||||
) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
|
||||
|
||||
|
||||
CREATE INDEX IF NOT EXISTS DOMAIN_SECURITY_INFORMATION__NODE_ID__DOMAIN_ID_IDX ON DOMAIN_SECURITY_INFORMATION (NODE_ID, DOMAIN_ID);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS DOMAIN_SECURITY_EVENTS (
|
||||
CHANGE_ID BIGINT AUTO_INCREMENT PRIMARY KEY, -- Unique identifier for the change
|
||||
DOMAIN_ID INT NOT NULL, -- Domain ID, used as a foreign key to EC_DOMAIN
|
||||
NODE_ID INT NOT NULL,
|
||||
|
||||
TS_CHANGE TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- Timestamp of the change
|
||||
|
||||
CHANGE_ASN BOOLEAN NOT NULL DEFAULT FALSE, -- Indicates if the change is related to ASN (Autonomous System Number)
|
||||
CHANGE_CERTIFICATE_FINGERPRINT BOOLEAN NOT NULL DEFAULT FALSE, -- Indicates if the change is related to SSL certificate fingerprint
|
||||
CHANGE_CERTIFICATE_PROFILE BOOLEAN NOT NULL DEFAULT FALSE, -- Indicates if the change is related to SSL certificate profile (e.g., algorithm, exchange)
|
||||
CHANGE_CERTIFICATE_SAN BOOLEAN NOT NULL DEFAULT FALSE, -- Indicates if the change is related to SSL certificate SAN (Subject Alternative Name)
|
||||
CHANGE_CERTIFICATE_PUBLIC_KEY BOOLEAN NOT NULL DEFAULT FALSE, -- Indicates if the change is related to SSL certificate public key
|
||||
CHANGE_SECURITY_HEADERS BOOLEAN NOT NULL DEFAULT FALSE, -- Indicates if the change is related to security headers
|
||||
CHANGE_IP_ADDRESS BOOLEAN NOT NULL DEFAULT FALSE, -- Indicates if the change is related to IP address
|
||||
CHANGE_SOFTWARE BOOLEAN NOT NULL DEFAULT FALSE, -- Indicates if the change is related to the generator (e.g., web server software)
|
||||
OLD_CERT_TIME_TO_EXPIRY INT, -- Time to expiry of the old certificate in hours, if applicable
|
||||
|
||||
SECURITY_SIGNATURE_BEFORE BLOB NOT NULL, -- Security signature before the change, gzipped json record
|
||||
SECURITY_SIGNATURE_AFTER BLOB NOT NULL -- Security signature after the change, gzipped json record
|
||||
) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS DOMAIN_SECURITY_EVENTS__NODE_ID__DOMAIN_ID_IDX ON DOMAIN_SECURITY_EVENTS (NODE_ID, DOMAIN_ID);
|
||||
CREATE INDEX IF NOT EXISTS DOMAIN_SECURITY_EVENTS__TS_CHANGE_IDX ON DOMAIN_SECURITY_EVENTS (TS_CHANGE);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS DOMAIN_AVAILABILITY_EVENTS (
|
||||
DOMAIN_ID INT NOT NULL,
|
||||
NODE_ID INT NOT NULL,
|
||||
|
||||
AVAILABLE BOOLEAN NOT NULL, -- True if the service is available, false if it is not
|
||||
OUTAGE_TYPE ENUM('NONE', 'TIMEOUT', 'SSL_ERROR', 'DNS_ERROR', 'CONNECTION_ERROR', 'HTTP_CLIENT_ERROR', 'HTTP_SERVER_ERROR', 'UNKNOWN') NOT NULL,
|
||||
HTTP_STATUS_CODE INT, -- HTTP status code if available (e.g., 200, 404, etc.)
|
||||
ERROR_MESSAGE VARCHAR(255), -- Specific error details
|
||||
|
||||
TS_CHANGE TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Timestamp of the last update
|
||||
|
||||
AVAILABILITY_RECORD_ID BIGINT AUTO_INCREMENT,
|
||||
P_KEY_MONTH TINYINT NOT NULL DEFAULT MONTH(TS_CHANGE), -- Month of the change for partitioning
|
||||
PRIMARY KEY (AVAILABILITY_RECORD_ID, P_KEY_MONTH)
|
||||
)
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_bin
|
||||
PARTITION BY RANGE (P_KEY_MONTH) (
|
||||
PARTITION p0 VALUES LESS THAN (1), -- January
|
||||
PARTITION p1 VALUES LESS THAN (2), -- February
|
||||
PARTITION p2 VALUES LESS THAN (3), -- March
|
||||
PARTITION p3 VALUES LESS THAN (4), -- April
|
||||
PARTITION p4 VALUES LESS THAN (5), -- May
|
||||
PARTITION p5 VALUES LESS THAN (6), -- June
|
||||
PARTITION p6 VALUES LESS THAN (7), -- July
|
||||
PARTITION p7 VALUES LESS THAN (8), -- August
|
||||
PARTITION p8 VALUES LESS THAN (9), -- September
|
||||
PARTITION p9 VALUES LESS THAN (10), -- October
|
||||
PARTITION p10 VALUES LESS THAN (11), -- November
|
||||
PARTITION p11 VALUES LESS THAN (12) -- December
|
||||
);
|
||||
|
||||
CREATE INDEX DOMAIN_AVAILABILITY_EVENTS__DOMAIN_ID_TS_IDX ON DOMAIN_AVAILABILITY_EVENTS (DOMAIN_ID, TS_CHANGE);
|
||||
CREATE INDEX DOMAIN_AVAILABILITY_EVENTS__TS_CHANGE_IDX ON DOMAIN_AVAILABILITY_EVENTS (TS_CHANGE);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS DOMAIN_DNS_INFORMATION (
|
||||
DNS_ROOT_DOMAIN_ID INT AUTO_INCREMENT PRIMARY KEY,
|
||||
ROOT_DOMAIN_NAME VARCHAR(255) NOT NULL UNIQUE,
|
||||
NODE_AFFINITY INT NOT NULL, -- Node ID that performs the DNS check, assign randomly across nodes
|
||||
|
||||
DNS_A_RECORDS TEXT, -- JSON array of IPv4 addresses
|
||||
DNS_AAAA_RECORDS TEXT, -- JSON array of IPv6 addresses
|
||||
DNS_CNAME_RECORD VARCHAR(255), -- Canonical name (if applicable)
|
||||
DNS_MX_RECORDS TEXT, -- JSON array of mail exchange records
|
||||
DNS_CAA_RECORDS TEXT, -- Certificate Authority Authorization
|
||||
DNS_TXT_RECORDS TEXT, -- TXT records (SPF, DKIM, verification, etc.)
|
||||
DNS_NS_RECORDS TEXT, -- Name servers (JSON array)
|
||||
DNS_SOA_RECORD TEXT, -- Start of Authority (JSON object)
|
||||
|
||||
TS_LAST_DNS_CHECK TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
TS_NEXT_DNS_CHECK TIMESTAMP NOT NULL,
|
||||
DNS_CHECK_PRIORITY TINYINT DEFAULT 0 -- Priority of the DNS check, in case we want to schedule a refresh sooner
|
||||
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
CREATE INDEX DOMAIN_DNS_INFORMATION__PRIORITY_NEXT_CHECK_IDX ON DOMAIN_DNS_INFORMATION (NODE_AFFINITY, DNS_CHECK_PRIORITY DESC, TS_NEXT_DNS_CHECK);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS DOMAIN_DNS_EVENTS (
|
||||
DNS_ROOT_DOMAIN_ID INT NOT NULL,
|
||||
NODE_ID INT NOT NULL,
|
||||
|
||||
TS_CHANGE TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- DNS change type flags
|
||||
CHANGE_A_RECORDS BOOLEAN NOT NULL DEFAULT FALSE, -- IPv4 address changes
|
||||
CHANGE_AAAA_RECORDS BOOLEAN NOT NULL DEFAULT FALSE, -- IPv6 address changes
|
||||
CHANGE_CNAME BOOLEAN NOT NULL DEFAULT FALSE, -- CNAME changes
|
||||
CHANGE_MX_RECORDS BOOLEAN NOT NULL DEFAULT FALSE, -- Mail server changes
|
||||
CHANGE_CAA_RECORDS BOOLEAN NOT NULL DEFAULT FALSE, -- Certificate authority changes
|
||||
CHANGE_TXT_RECORDS BOOLEAN NOT NULL DEFAULT FALSE, -- TXT record changes (SPF, DKIM, etc.)
|
||||
CHANGE_NS_RECORDS BOOLEAN NOT NULL DEFAULT FALSE, -- Name server changes (big red flag!)
|
||||
CHANGE_SOA_RECORD BOOLEAN NOT NULL DEFAULT FALSE, -- Start of Authority changes
|
||||
|
||||
DNS_SIGNATURE_BEFORE BLOB NOT NULL, -- Compressed JSON snapshot of DNS records before change
|
||||
DNS_SIGNATURE_AFTER BLOB NOT NULL, -- Compressed JSON snapshot of DNS records after change
|
||||
|
||||
DNS_EVENT_ID BIGINT AUTO_INCREMENT,
|
||||
P_KEY_MONTH TINYINT NOT NULL DEFAULT MONTH(TS_CHANGE), -- Month of the change for partitioning
|
||||
PRIMARY KEY (DNS_EVENT_ID, P_KEY_MONTH)
|
||||
)
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_bin
|
||||
PARTITION BY RANGE (P_KEY_MONTH) (
|
||||
PARTITION p0 VALUES LESS THAN (1), -- January
|
||||
PARTITION p1 VALUES LESS THAN (2), -- February
|
||||
PARTITION p2 VALUES LESS THAN (3), -- March
|
||||
PARTITION p3 VALUES LESS THAN (4), -- April
|
||||
PARTITION p4 VALUES LESS THAN (5), -- May
|
||||
PARTITION p5 VALUES LESS THAN (6), -- June
|
||||
PARTITION p6 VALUES LESS THAN (7), -- July
|
||||
PARTITION p7 VALUES LESS THAN (8), -- August
|
||||
PARTITION p8 VALUES LESS THAN (9), -- September
|
||||
PARTITION p9 VALUES LESS THAN (10), -- October
|
||||
PARTITION p10 VALUES LESS THAN (11), -- November
|
||||
PARTITION p11 VALUES LESS THAN (12) -- December
|
||||
);
|
||||
|
||||
CREATE INDEX DOMAIN_DNS_EVENTS__DNS_ROOT_DOMAIN_ID_TS_IDX ON DOMAIN_DNS_EVENTS (DNS_ROOT_DOMAIN_ID, TS_CHANGE);
|
||||
CREATE INDEX DOMAIN_DNS_EVENTS__TS_CHANGE_IDX ON DOMAIN_DNS_EVENTS (TS_CHANGE);
|
@@ -0,0 +1,6 @@
|
||||
-- Add additional summary columns to DOMAIN_SECURITY_EVENTS table
|
||||
-- to make it easier to make sense of certificate changes
|
||||
|
||||
ALTER TABLE DOMAIN_SECURITY_EVENTS ADD COLUMN CHANGE_CERTIFICATE_SERIAL_NUMBER BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE DOMAIN_SECURITY_EVENTS ADD COLUMN CHANGE_CERTIFICATE_ISSUER BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
OPTIMIZE TABLE DOMAIN_SECURITY_EVENTS;
|
@@ -0,0 +1,7 @@
|
||||
-- Add additional summary columns to DOMAIN_SECURITY_INFORMATION table
|
||||
-- to make it easier to get more information about the SSL certificate's validity
|
||||
|
||||
ALTER TABLE DOMAIN_SECURITY_INFORMATION ADD COLUMN SSL_CHAIN_VALID BOOLEAN DEFAULT NULL;
|
||||
ALTER TABLE DOMAIN_SECURITY_INFORMATION ADD COLUMN SSL_HOST_VALID BOOLEAN DEFAULT NULL;
|
||||
ALTER TABLE DOMAIN_SECURITY_INFORMATION ADD COLUMN SSL_DATE_VALID BOOLEAN DEFAULT NULL;
|
||||
OPTIMIZE TABLE DOMAIN_SECURITY_INFORMATION;
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user