Compare commits
672 Commits
muwire-0.6
...
iconfix-re
Author | SHA1 | Date | |
---|---|---|---|
![]() |
fdb64f5539 | ||
![]() |
5b4f3202d6 | ||
![]() |
eb1f2fe19d | ||
![]() |
17c59102ad | ||
![]() |
26e8300d18 | ||
![]() |
47ac0fd9ac | ||
![]() |
0b8b489169 | ||
![]() |
fb32690c7c | ||
![]() |
a11c504271 | ||
![]() |
76e726b520 | ||
![]() |
4f626615d8 | ||
![]() |
061a1a88dd | ||
![]() |
ad20d7cf9a | ||
![]() |
895df6cf94 | ||
![]() |
59b5d88829 | ||
![]() |
f382d2ecbf | ||
![]() |
6740d09479 | ||
![]() |
8cbada110e | ||
![]() |
33982dd24b | ||
![]() |
274edcc599 | ||
![]() |
af218a369c | ||
![]() |
f0aaa83b7f | ||
![]() |
b9c34cb944 | ||
![]() |
59353a6718 | ||
![]() |
c25546e1e1 | ||
![]() |
f9fb9e4f07 | ||
![]() |
72f2b2bd37 | ||
![]() |
eb242b0889 | ||
![]() |
6508522c9c | ||
![]() |
f38b8217c2 | ||
![]() |
c9c5e8617a | ||
![]() |
8c4bafda82 | ||
![]() |
c2044044c0 | ||
![]() |
cb54b30967 | ||
![]() |
c041f6baaa | ||
![]() |
bf28278f72 | ||
![]() |
6462675091 | ||
![]() |
5adf8d8276 | ||
![]() |
2fbab55f68 | ||
![]() |
0d783a6bcd | ||
![]() |
017454c4b3 | ||
![]() |
ec41985d31 | ||
![]() |
5daad35ee2 | ||
![]() |
8df9f63bc7 | ||
![]() |
367a43825f | ||
![]() |
7b34b0cffc | ||
![]() |
bb6692c38e | ||
![]() |
f1a2b103a8 | ||
![]() |
c1324c92ba | ||
![]() |
179c3438cd | ||
![]() |
7fa6812ee9 | ||
![]() |
a1c714b46e | ||
![]() |
4f7cf4fbfc | ||
![]() |
2d3e843d64 | ||
![]() |
2e36812740 | ||
![]() |
61340f346a | ||
![]() |
992daa1e45 | ||
![]() |
3b825263a7 | ||
![]() |
e1bf6c0821 | ||
![]() |
a6eca11479 | ||
![]() |
11aa6dda70 | ||
![]() |
3116e20c7c | ||
![]() |
58a92e7442 | ||
![]() |
d18cdb15cd | ||
![]() |
ed02b718d9 | ||
![]() |
564db3473c | ||
![]() |
6d6063829a | ||
![]() |
ecaec1df3b | ||
![]() |
8b99f83db8 | ||
![]() |
33b159477a | ||
![]() |
91d8175cc5 | ||
![]() |
b4c6c77167 | ||
![]() |
fb59d1ca0c | ||
![]() |
3de4c65d2f | ||
![]() |
91ea2c0184 | ||
![]() |
4a81a3539e | ||
fcfb506787 | |||
![]() |
44dc7b808f | ||
![]() |
339f4aaa3e | ||
![]() |
bf06c3b15f | ||
![]() |
b5e41d72b8 | ||
![]() |
2fe9309519 | ||
![]() |
2410ed7199 | ||
![]() |
9167c9edf7 | ||
![]() |
028a8d5044 | ||
![]() |
356d7fe2ff | ||
![]() |
9da7a90653 | ||
![]() |
2001419f1a | ||
![]() |
eec9bab081 | ||
![]() |
0a66267264 | ||
![]() |
ad698cf1b9 | ||
![]() |
fd9866c519 | ||
![]() |
83bea0c823 | ||
![]() |
71789d96d2 | ||
![]() |
7860aa2b1c | ||
![]() |
301c2ec0e2 | ||
![]() |
c306864781 | ||
![]() |
acee9a5805 | ||
![]() |
d34c4e1990 | ||
![]() |
7be3821e53 | ||
![]() |
872e932629 | ||
![]() |
84c7da1fe0 | ||
![]() |
4aed958319 | ||
![]() |
5fc0283da7 | ||
![]() |
c4d908f571 | ||
![]() |
4d5497c12f | ||
![]() |
1d22abfa88 | ||
![]() |
7a7ebc9690 | ||
![]() |
16d3a109ca | ||
![]() |
7864eebb24 | ||
![]() |
9f7aaec991 | ||
![]() |
1c214ad68a | ||
![]() |
3436af75bf | ||
![]() |
9b6a2fd952 | ||
![]() |
85ad3109f9 | ||
![]() |
293ff76ae9 | ||
![]() |
acb70f72d6 | ||
![]() |
62bb4f9e5f | ||
![]() |
03d6fb15f2 | ||
![]() |
699f3ce1b6 | ||
![]() |
7f9c8bddb6 | ||
![]() |
d111983d68 | ||
![]() |
50148e5603 | ||
![]() |
1054fe0935 | ||
![]() |
2de2badb0b | ||
![]() |
424922f2e3 | ||
![]() |
adce4b1574 | ||
![]() |
355535e660 | ||
![]() |
09db68182c | ||
![]() |
1e67139e74 | ||
![]() |
9837e1e3d7 | ||
![]() |
2c52486476 | ||
![]() |
a88dc17064 | ||
![]() |
862967bf8e | ||
![]() |
9f1f718870 | ||
![]() |
2fd0a3833f | ||
![]() |
435170cb1b | ||
![]() |
1c5fec7e9a | ||
![]() |
e2a0a37abf | ||
![]() |
a4bee73b8a | ||
![]() |
056e5800c2 | ||
![]() |
6e0d51c221 | ||
![]() |
496e2e7f91 | ||
![]() |
a560b14d91 | ||
![]() |
faad6b6b0e | ||
![]() |
dfc62b943f | ||
![]() |
244ce43794 | ||
![]() |
f0c8c11094 | ||
![]() |
11e320ef53 | ||
![]() |
aae88e80ee | ||
![]() |
bbf97311d1 | ||
![]() |
23b6995bf2 | ||
![]() |
518bdc44e6 | ||
![]() |
5368dbe181 | ||
![]() |
e216678d9a | ||
![]() |
4582cfa0b5 | ||
![]() |
5ea64ecb90 | ||
![]() |
bd9315954a | ||
![]() |
83bdf76c08 | ||
![]() |
a2ed308cd0 | ||
![]() |
4020df0a77 | ||
![]() |
6f4b4a2c2d | ||
![]() |
83cd5e57a2 | ||
![]() |
bb69535874 | ||
![]() |
b7033e3277 | ||
![]() |
4a9cea7d2e | ||
![]() |
2aea965d72 | ||
![]() |
9a6a1c8371 | ||
![]() |
2042bfccb7 | ||
![]() |
0d4b0df19d | ||
![]() |
f363296ed1 | ||
![]() |
8b33a5a284 | ||
![]() |
7e70dbda86 | ||
![]() |
c23db1293f | ||
![]() |
54f4874ad6 | ||
![]() |
886effa3b6 | ||
![]() |
64d8b98ee2 | ||
![]() |
2f2f620ae5 | ||
![]() |
9a74cc5026 | ||
![]() |
e3c5fe291d | ||
![]() |
c77b848d44 | ||
![]() |
cf5b5b164d | ||
![]() |
3a340e40c8 | ||
![]() |
e9eafe9380 | ||
![]() |
270a8519b4 | ||
![]() |
f8bbeb8ac0 | ||
![]() |
2a4db868aa | ||
![]() |
59219da1a2 | ||
![]() |
a5fb824f71 | ||
![]() |
68bc0bbf30 | ||
![]() |
c6c1ac1d93 | ||
![]() |
9646eadcb1 | ||
![]() |
db91c9171d | ||
![]() |
e542a50260 | ||
![]() |
a9539c5999 | ||
![]() |
d93dbbeb8b | ||
![]() |
45659f0dca | ||
![]() |
31a607ed7d | ||
![]() |
7a6538beff | ||
![]() |
509b5c3b99 | ||
![]() |
fbb710cfc8 | ||
![]() |
244015465a | ||
![]() |
7285c12b97 | ||
![]() |
aac259c0fe | ||
![]() |
e3f58f8f5a | ||
![]() |
045859fe04 | ||
![]() |
3a8c66e857 | ||
![]() |
773513b257 | ||
![]() |
83fe2e9b75 | ||
![]() |
455b0ea48e | ||
![]() |
f4c96db841 | ||
![]() |
fca8870283 | ||
![]() |
3efb04d7bb | ||
![]() |
62ce8ffa46 | ||
![]() |
05b70a4573 | ||
![]() |
b339784826 | ||
![]() |
488f2964ee | ||
![]() |
369779ab6a | ||
![]() |
f5fe3da09d | ||
![]() |
392deee34c | ||
![]() |
7183f15c5c | ||
![]() |
ca33535630 | ||
![]() |
54abf82a91 | ||
![]() |
14546737fd | ||
![]() |
0f069f2fc9 | ||
![]() |
9a44603d2f | ||
![]() |
38a027c308 | ||
![]() |
2ba81ccc84 | ||
![]() |
0408349c07 | ||
![]() |
95cb7f3214 | ||
![]() |
69810d7203 | ||
![]() |
f202fa34f3 | ||
![]() |
c082e25c81 | ||
![]() |
2bb07ff7b5 | ||
![]() |
ff952890bc | ||
![]() |
fc393619d8 | ||
![]() |
2882c73876 | ||
![]() |
cbb1de046b | ||
![]() |
a272a45928 | ||
![]() |
3133581363 | ||
![]() |
c3d0dce281 | ||
![]() |
8f710e68c2 | ||
![]() |
15430d6c03 | ||
![]() |
166b71f128 | ||
![]() |
d724986ec6 | ||
![]() |
198c5b5538 | ||
![]() |
96d71ed08f | ||
![]() |
bb7385688c | ||
![]() |
e70bec3a51 | ||
![]() |
ed04c40420 | ||
![]() |
e9f00c2995 | ||
![]() |
fd75d8229b | ||
![]() |
0ff9ca8572 | ||
![]() |
a07f01b641 | ||
![]() |
b9333913c6 | ||
![]() |
fcb5c573f9 | ||
![]() |
1610766e01 | ||
![]() |
e2a9db8056 | ||
![]() |
a0cb214e2b | ||
![]() |
f2bf921d4c | ||
![]() |
aa0fcfb7de | ||
![]() |
48cfce71a8 | ||
![]() |
8798ea38e8 | ||
![]() |
17cd60afe3 | ||
![]() |
c10c1118e8 | ||
![]() |
28425e93dc | ||
![]() |
032338bb48 | ||
![]() |
12e56b1c9a | ||
![]() |
cc8801c48b | ||
![]() |
57c75978b6 | ||
![]() |
bfe198e1a6 | ||
![]() |
8e274f940e | ||
![]() |
9f3942c1c7 | ||
![]() |
d60d57ee43 | ||
![]() |
8e3a433afb | ||
![]() |
49cf56fabb | ||
![]() |
2b6565d107 | ||
![]() |
366a2ef841 | ||
![]() |
bcd24e56ac | ||
![]() |
c7d1f0c23c | ||
![]() |
853b9f67fc | ||
![]() |
a505a2449a | ||
![]() |
c11d81c6c3 | ||
![]() |
ee5e90c4ab | ||
![]() |
64d2a87d26 | ||
![]() |
f0304dbe7d | ||
![]() |
bdad8d9309 | ||
![]() |
8c110bbae5 | ||
![]() |
2cc1e384bc | ||
![]() |
9337d1b74d | ||
![]() |
16ed5dd346 | ||
![]() |
7b55fc9ed8 | ||
![]() |
d5c8050572 | ||
![]() |
83546d68d2 | ||
a891c83518 | |||
aa56cc23c0 | |||
a2b37ef567 | |||
4bc04ae631 | |||
56da9a16b0 | |||
2935ee1a1d | |||
855183397b | |||
e27704c1af | |||
5c18b4a141 | |||
dcd233b7ad | |||
7cee8a28ba | |||
7446fc949a | |||
598ab90f63 | |||
043028c296 | |||
cd1757fac3 | |||
9d4b365e63 | |||
![]() |
b12d57e30a | ||
![]() |
f33d1b6db3 | ||
![]() |
9e451460da | ||
![]() |
ffa52c129a | ||
b779fb75a0 | |||
fbe6b53278 | |||
b2bd95788d | |||
83d4a2624b | |||
03e20e21aa | |||
8a08955675 | |||
4ec54ebe54 | |||
758af6f48e | |||
a7bdd47fcd | |||
f7caa77a18 | |||
7641f64536 | |||
02baaace48 | |||
![]() |
d90067ff39 | ||
c910a215f5 | |||
65e073b1b9 | |||
489a7518c3 | |||
3733e48bbd | |||
c3723a1348 | |||
0e0f52bc77 | |||
60b9e990cf | |||
28ad0ae30f | |||
9142de85cd | |||
4eb31c11e3 | |||
e8afe358a5 | |||
![]() |
3db4317fc1 | ||
![]() |
5ad2b28527 | ||
![]() |
3036765f81 | ||
![]() |
8f9b1e5a8b | ||
![]() |
e6d59a2438 | ||
![]() |
32609b4779 | ||
![]() |
74ac4cfecf | ||
![]() |
69173c4156 | ||
![]() |
6283287bee | ||
![]() |
8e3f76f68c | ||
![]() |
574294fdc6 | ||
![]() |
8bd41546cd | ||
![]() |
ba5425c958 | ||
![]() |
22580f002c | ||
![]() |
5c773cec80 | ||
![]() |
7df00e6709 | ||
![]() |
5c05bd2562 | ||
![]() |
9df1d043e4 | ||
![]() |
6ea1a15641 | ||
![]() |
c0575facec | ||
![]() |
09168844e0 | ||
![]() |
e21d482393 | ||
![]() |
f5fc3e40c2 | ||
![]() |
796a0138fa | ||
![]() |
505b4ddb06 | ||
![]() |
a35216ff56 | ||
![]() |
fba92fe9b9 | ||
![]() |
1cc511b0ae | ||
![]() |
fa94c8ebfa | ||
![]() |
88b68a3c5c | ||
![]() |
b3e0d2ee7a | ||
![]() |
ce293cbda8 | ||
![]() |
3abc617e9f | ||
![]() |
67ee634f20 | ||
![]() |
503d54927f | ||
![]() |
5788329e1a | ||
![]() |
f0ffc68122 | ||
![]() |
3d710cebe5 | ||
![]() |
7d67573c92 | ||
![]() |
3acc676448 | ||
![]() |
2bf03b6b84 | ||
![]() |
b8ba6df4d5 | ||
![]() |
9fa7fa07b4 | ||
![]() |
1c7253ea0a | ||
![]() |
d947ad2997 | ||
![]() |
dd0bd6f5f8 | ||
![]() |
f05b6d0b40 | ||
![]() |
906c69a482 | ||
![]() |
5375b7aec0 | ||
![]() |
ea5da2431a | ||
![]() |
14b3a9ac9e | ||
![]() |
40bbef4583 | ||
![]() |
f811653247 | ||
![]() |
f321000071 | ||
![]() |
6eb85283cd | ||
![]() |
2973759cd9 | ||
![]() |
fe945a9941 | ||
![]() |
5f7e949310 | ||
![]() |
11edb2cb3c | ||
![]() |
ff1f801155 | ||
![]() |
0a98083c64 | ||
![]() |
75b2852f6e | ||
![]() |
5774cdee94 | ||
![]() |
2b0f4e52ca | ||
![]() |
1d20dc917b | ||
![]() |
63e3b3710c | ||
![]() |
0878b89082 | ||
![]() |
fecf0ecae8 | ||
![]() |
fec8d4ef9f | ||
![]() |
067ac8582a | ||
![]() |
31cac25a23 | ||
![]() |
6bcc44e01e | ||
![]() |
31652b34d7 | ||
![]() |
41a15fc7d5 | ||
![]() |
da3d7d7a50 | ||
![]() |
3a079d9f21 | ||
![]() |
ba0c85fe07 | ||
![]() |
ecb2283886 | ||
![]() |
cf9a18cee5 | ||
![]() |
982a93a04b | ||
![]() |
58137d11d1 | ||
![]() |
d87bec927d | ||
![]() |
dc8dd96495 | ||
![]() |
add9fb6feb | ||
![]() |
c500e95ab6 | ||
![]() |
477c3285d2 | ||
![]() |
1f5b112bfe | ||
![]() |
b0d09853e4 | ||
![]() |
b96d997037 | ||
![]() |
a631ec1e14 | ||
![]() |
62a06bc891 | ||
![]() |
3534b23194 | ||
![]() |
c561ae9140 | ||
![]() |
5926457eb5 | ||
![]() |
37c93e352b | ||
![]() |
be8fecda39 | ||
![]() |
7ec6257ac0 | ||
![]() |
c4ea58c330 | ||
![]() |
a482fe5c93 | ||
![]() |
2ee84848c4 | ||
![]() |
e29d7f6872 | ||
![]() |
5ded824ef2 | ||
![]() |
c607560cb8 | ||
![]() |
8b341bb125 | ||
![]() |
6bc5a9075b | ||
![]() |
6b1d2bc5ce | ||
![]() |
0cbbaf6a63 | ||
![]() |
3363b99675 | ||
![]() |
4ab4785539 | ||
![]() |
e595fa97e8 | ||
![]() |
65a7088463 | ||
![]() |
2d5bd653c1 | ||
![]() |
a864343c05 | ||
![]() |
696b348469 | ||
![]() |
b08333c5ea | ||
![]() |
0cf368c1af | ||
![]() |
62ab957892 | ||
![]() |
2b9e722165 | ||
![]() |
8cf4b23762 | ||
![]() |
1285c68521 | ||
![]() |
daa9e0bafc | ||
![]() |
8efd9c2c88 | ||
![]() |
918549f164 | ||
![]() |
e30a4666cb | ||
![]() |
26167abc08 | ||
![]() |
93f7c67f37 | ||
![]() |
f9a0a5e08a | ||
![]() |
d8ae275df2 | ||
![]() |
fce879be5d | ||
![]() |
0b58e22714 | ||
![]() |
dd230c4dfc | ||
![]() |
fba0b001c0 | ||
![]() |
6978c7b992 | ||
![]() |
7355e76e1b | ||
![]() |
5147cf21a0 | ||
![]() |
e8dd7d710d | ||
![]() |
fc9114eaa5 | ||
![]() |
20b7104c41 | ||
![]() |
570616951a | ||
![]() |
e075bfac55 | ||
![]() |
b6411a555c | ||
![]() |
d395475727 | ||
![]() |
8ae0a16b8a | ||
![]() |
38fcdfc97a | ||
![]() |
a0fb07cf99 | ||
![]() |
3747f9a5d5 | ||
![]() |
3a738f8f62 | ||
![]() |
ca56363438 | ||
![]() |
e06cb05e2a | ||
![]() |
8ab2dd7900 | ||
![]() |
26116d313a | ||
![]() |
738f177d6c | ||
![]() |
62c4579bbd | ||
![]() |
18d84685ec | ||
![]() |
c05a7a021c | ||
![]() |
a9935eba62 | ||
![]() |
e3d80bf809 | ||
![]() |
a59a1d3f30 | ||
![]() |
37ed75a3e8 | ||
![]() |
cd4b600ba2 | ||
![]() |
fcd6dbcfbd | ||
![]() |
f3ab15bd74 | ||
![]() |
cddaad0f29 | ||
![]() |
ecb597e0a0 | ||
![]() |
ec2a934f73 | ||
![]() |
e1d630fdee | ||
![]() |
5807672503 | ||
![]() |
2fadb314d3 | ||
![]() |
ec5c15ff64 | ||
![]() |
c169a7613f | ||
![]() |
0f762968ae | ||
![]() |
8e6517e7d8 | ||
![]() |
6946bff7f9 | ||
![]() |
37dcedb99b | ||
![]() |
afb92b0e4e | ||
![]() |
7c39dff34f | ||
![]() |
e41c122d2d | ||
![]() |
117c5eaf67 | ||
![]() |
10fab2b47f | ||
![]() |
3f71df3d29 | ||
![]() |
813e211200 | ||
![]() |
1adb130fba | ||
![]() |
f69d4027db | ||
![]() |
e0d006ec69 | ||
![]() |
81d8af57ed | ||
![]() |
42c48a8e37 | ||
![]() |
3b1349b643 | ||
![]() |
0250ea329c | ||
![]() |
b722c64ad8 | ||
![]() |
effa3b567e | ||
![]() |
64f198d599 | ||
![]() |
131b2defbb | ||
![]() |
df5aab67ac | ||
![]() |
fdc030904c | ||
![]() |
2a4fae8de4 | ||
![]() |
662b065116 | ||
![]() |
300938fa44 | ||
![]() |
086e27876d | ||
![]() |
247c62bfb4 | ||
![]() |
a13315c324 | ||
![]() |
65f40ef23a | ||
![]() |
96a611ff78 | ||
![]() |
0f4119b74f | ||
![]() |
6847329093 | ||
![]() |
9d2bcf70c7 | ||
![]() |
aa33709f04 | ||
![]() |
eacaedaf3d | ||
![]() |
f9c428cfcd | ||
![]() |
aa1ede46d2 | ||
![]() |
3c43244631 | ||
![]() |
b468a6f19b | ||
![]() |
cfdc750ac0 | ||
![]() |
6f8b006227 | ||
![]() |
3f4bf986f3 | ||
![]() |
bef1033e12 | ||
![]() |
13061d60a4 | ||
![]() |
5c6917a7e6 | ||
![]() |
2ec15cfbbc | ||
![]() |
1325a8dc65 | ||
![]() |
b5d8fcf25b | ||
![]() |
c22ff0678e | ||
![]() |
07051b813a | ||
![]() |
5c22af6576 | ||
![]() |
c3e1298ea3 | ||
![]() |
949b616fdd | ||
![]() |
2b1d95e2ef | ||
![]() |
3d967da110 | ||
![]() |
66fde32b64 | ||
![]() |
80a89a5ac0 | ||
![]() |
c59e038c2a | ||
![]() |
844bd8fd6e | ||
![]() |
7d9ebb5b0b | ||
![]() |
7fd7444dbf | ||
![]() |
13af6cce22 | ||
![]() |
458dbec5fd | ||
![]() |
2137d6d30b | ||
![]() |
b28de0c119 | ||
![]() |
0fd4695b7c | ||
![]() |
74dddc4da4 | ||
![]() |
8bff987d30 | ||
![]() |
de8684bafc | ||
![]() |
905f559aa9 | ||
![]() |
c7f57c0b15 | ||
![]() |
0f0f46f425 | ||
![]() |
d6a3c8b24c | ||
![]() |
8c661ca1ae | ||
![]() |
f579c8754f | ||
![]() |
5c17536683 | ||
![]() |
8536353c26 | ||
![]() |
84375c0201 | ||
![]() |
9c0c187a18 | ||
![]() |
8ae735e5c0 | ||
![]() |
8224dda3fd | ||
![]() |
c852d7474e | ||
![]() |
71685d2052 | ||
![]() |
e57e513ca1 | ||
![]() |
aa4fb14540 | ||
![]() |
5f74abc944 | ||
![]() |
c4135389a4 | ||
![]() |
a6e0834722 | ||
![]() |
bc628b9c00 | ||
![]() |
9b2669a8b8 | ||
![]() |
a0f70f7677 | ||
![]() |
23b2c912e2 | ||
![]() |
ecfd4180c0 | ||
![]() |
42489ba6b2 | ||
![]() |
61207f893d | ||
![]() |
4e32359718 | ||
![]() |
8d4af48eca | ||
![]() |
693f63534d | ||
![]() |
b057e848d0 | ||
![]() |
0114224d1f | ||
![]() |
beab2be713 | ||
![]() |
edd4a1ff4b | ||
![]() |
85814b7544 | ||
![]() |
d46fbd66f0 | ||
![]() |
06bd9c80e8 | ||
![]() |
54b8628435 | ||
![]() |
b37a548771 | ||
![]() |
a14689acff | ||
![]() |
a73bc956bf | ||
![]() |
d595a768b8 | ||
![]() |
0fd6421fae | ||
![]() |
6e9a36461a | ||
![]() |
d115f54812 | ||
![]() |
f627f661f2 | ||
![]() |
0e7ec3dfb3 | ||
![]() |
0188bd34a9 | ||
![]() |
a2becfa6e2 | ||
![]() |
ea32af9b91 | ||
![]() |
c74c26e4c6 | ||
![]() |
382e21225b | ||
![]() |
81c406cbf6 | ||
![]() |
d9eb46d65c | ||
![]() |
dadfed20f1 | ||
![]() |
6dad29a772 | ||
![]() |
884253fe29 | ||
![]() |
a5eccbdc2b | ||
![]() |
d0318e3e83 | ||
![]() |
d1c308f118 | ||
![]() |
3871170e44 | ||
![]() |
95dd5c4a7c | ||
![]() |
0bff4b55a5 | ||
![]() |
a2022415c2 | ||
![]() |
2b8bd8144f | ||
![]() |
7bf520ac8c | ||
![]() |
ad8983e889 | ||
![]() |
d0b62af32e | ||
![]() |
bc8e259974 | ||
![]() |
ff0a4661fd | ||
![]() |
9151df6816 | ||
![]() |
9c0878408b | ||
![]() |
61baa53076 | ||
![]() |
b2841ee9ab | ||
![]() |
9edea17fb7 | ||
![]() |
ac17618f0c | ||
![]() |
e94ed4eafa | ||
![]() |
8c33a5e62f | ||
![]() |
f9f1017e5b | ||
![]() |
5d2d831b9e | ||
![]() |
562d9a0f4a | ||
![]() |
b981f9199b | ||
![]() |
efef0f3734 | ||
![]() |
cd0b860210 | ||
![]() |
9cb0655cfa | ||
![]() |
3775f28af7 | ||
![]() |
c33b824871 | ||
![]() |
cf396b739e | ||
![]() |
631963f43c | ||
![]() |
06cedb4f41 | ||
![]() |
7a0c60a164 | ||
![]() |
4c038ad932 | ||
![]() |
f6dd38685a | ||
![]() |
2eab0f0567 |
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
||||
# Dot directories
|
||||
.gradle/
|
||||
.idea/
|
||||
.git/
|
||||
|
||||
# Build directories
|
||||
build/
|
||||
**/build/
|
||||
|
||||
# We execute COPY . .
|
||||
# Modifying these files would unnecessarily invalidate the build context
|
||||
Dockerfile
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -2,5 +2,7 @@
|
||||
**/.settings
|
||||
**/build
|
||||
.gradle
|
||||
.project
|
||||
.classpath
|
||||
**/.project
|
||||
**/.classpath
|
||||
**/*.rej
|
||||
**/*.orig
|
||||
|
9
.tx/config
Normal file
9
.tx/config
Normal file
@@ -0,0 +1,9 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = he: iw, id: in, ru_RU: ru, sv_SE: sv, tr_TR: tr, uk_UA: uk, yi: ji, zh_CN: zh
|
||||
|
||||
[I2P.MuWire]
|
||||
file_filter = webui/locale/messages_<lang>.po
|
||||
source_file = webui/locale/messages_en.po
|
||||
source_lang = en
|
||||
minimum_perc = 10
|
64
Dockerfile
Normal file
64
Dockerfile
Normal file
@@ -0,0 +1,64 @@
|
||||
FROM jlesage/baseimage-gui:alpine-3.10-glibc
|
||||
|
||||
# Docker image version is provided via build arg.
|
||||
ARG DOCKER_IMAGE_VERSION=unknown
|
||||
|
||||
# JDK version
|
||||
ARG JDK=11
|
||||
|
||||
# Important directories
|
||||
ARG TMP_DIR=/muwire-tmp
|
||||
ENV APP_HOME=/muwire
|
||||
|
||||
# Define working directory.
|
||||
WORKDIR $TMP_DIR
|
||||
|
||||
# Put sources into dir
|
||||
COPY . .
|
||||
|
||||
# Install final dependencies
|
||||
RUN add-pkg openjdk${JDK}-jre
|
||||
|
||||
# Build and untar in future distribution dir
|
||||
RUN add-pkg --virtual openjdk${JDK}-jdk \
|
||||
&& ./gradlew --no-daemon clean assemble \
|
||||
&& mkdir -p ${APP_HOME} \
|
||||
# Extract to ${APP_HOME and ignore the first dir
|
||||
# First dir in tar is the "MuWire-<version>"
|
||||
&& tar -C ${APP_HOME} --strip 1 -xvf gui/build/distributions/MuWire*.tar \
|
||||
# Cleanup
|
||||
&& rm -rf "${TMP_DIR}" /root/.gradle /root/.java \
|
||||
&& del-pkg openjdk${JDK}-jdk
|
||||
|
||||
WORKDIR ${APP_HOME}
|
||||
|
||||
# Maximize only the main/initial window.
|
||||
RUN \
|
||||
sed-patch 's/<application type="normal">/<application type="normal" title="MuWire">/' \
|
||||
/etc/xdg/openbox/rc.xml
|
||||
|
||||
# Generate and install favicons.
|
||||
RUN \
|
||||
APP_ICON_URL=https://github.com/zlatinb/muwire/raw/master/gui/griffon-app/resources/MuWire-128x128.png && \
|
||||
install_app_icon.sh "$APP_ICON_URL"
|
||||
|
||||
# Add files.
|
||||
COPY docker/rootfs/ /
|
||||
|
||||
# Set environment variables.
|
||||
ENV APP_NAME="MuWire" \
|
||||
S6_KILL_GRACETIME=8000
|
||||
|
||||
# Define mountable directories.
|
||||
VOLUME ["$APP_HOME/.MuWire"]
|
||||
VOLUME ["/incompletes"]
|
||||
VOLUME ["/output"]
|
||||
|
||||
|
||||
# Metadata.
|
||||
LABEL \
|
||||
org.label-schema.name="muwire" \
|
||||
org.label-schema.description="Docker container for MuWire" \
|
||||
org.label-schema.version="$DOCKER_IMAGE_VERSION" \
|
||||
org.label-schema.vcs-url="https://github.com/zlatinb/muwire" \
|
||||
org.label-schema.schema-version="1.0"
|
46
README.md
46
README.md
@@ -1,12 +1,14 @@
|
||||
The GitHub repo is mirrored from the in-I2P GitLab repo. Please open PRs and issues at http://git.idk.i2p/zlatinb/muwire
|
||||
|
||||
# MuWire - Easy Anonymous File-Sharing
|
||||
|
||||
MuWire is an easy to use file-sharing program which offers anonymity using [I2P technology](http://geti2p.net). It works on any platform Java works on, including Windows,MacOS,Linux.
|
||||
|
||||
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
|
||||
The current stable release - 0.7.1 is avaiable for download at https://muwire.com. The latest plugin build and instructions how to install the plugin are available inside I2P at http://muwire.i2p.
|
||||
|
||||
The current stable release - 0.6.6 is avaiable for download at https://muwire.com. You can find technical documentation in the [doc] folder. Also check out the [Wiki] for various documentation.
|
||||
You can find technical documentation in the [doc] folder. Also check out the [Wiki] for various other documentation.
|
||||
|
||||
### Building
|
||||
## Building
|
||||
|
||||
You need JDK 9 or newer. After installing that and setting up the appropriate paths, just type
|
||||
|
||||
@@ -21,25 +23,39 @@ If you want to run the unit tests, type
|
||||
|
||||
If you want to build binary bundles that do not depend on Java or I2P, see the [muwire-pkg] project
|
||||
|
||||
### Running the GUI
|
||||
## Running the GUI
|
||||
|
||||
After you build the application, look inside `gui/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar gui-x.y.z-all.jar` in a terminal or command prompt.
|
||||
Type
|
||||
```
|
||||
./gradlew gui:run
|
||||
```
|
||||
|
||||
If you have an I2P router running on the same machine that is all you need to do. If you use a custom I2CP host and port, create a file `i2p.properties` and put `i2cp.tcp.host=<host>` and `i2cp.tcp.port=<port>` in there. On Windows that file should go into `%HOME%\AppData\Roaming\MuWire`, on Mac into `$HOME/Library/Application Support/MuWire` and on Linux `$HOME/.MuWire`
|
||||
The setup wizard will ask you for the host and port of an I2P or I2Pd router.
|
||||
|
||||
[Default I2CP port]\: `7654`
|
||||
|
||||
### Running the CLI
|
||||
## Running the CLI
|
||||
|
||||
Look inside `cli-lanterna/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar cli-lanterna-x.y.z-all.jar` in a terminal. The CLI will ask you about the router host and port on startup, no need to edit any files. However, the CLI does not have an options window yet, so if you need to change any options you will need to edit the configuration files. The CLI options are documented here [cli options]
|
||||
|
||||
The CLI is under active development and doesn't have all the features of the GUI.
|
||||
|
||||
### Web UI
|
||||
## Running the Web UI / Plugin
|
||||
|
||||
If you are a Grails/Scala/JRuby/Kotlin developer and are interested in building a Web UI for MuWire, please get in touch. The MuWire core is written in Groovy and should be easy to integrate with any JVM-based language.
|
||||
There is a Web-based UI under development. It is intended to be run as a plugin to the Java I2P router. Instructions how to build it are available at the wiki [Plugin] page.
|
||||
|
||||
### GPG Fingerprint
|
||||
## Docker
|
||||
|
||||
MuWire is available as a Docker image. For more information see the [Docker] page.
|
||||
|
||||
## Translations
|
||||
If you want to help translate MuWire, instructions are on the wiki https://github.com/zlatinb/muwire/wiki/Translate
|
||||
|
||||
## Related Projects
|
||||
### MuWire Tracker Daemon
|
||||
The MuWire Tracker Daemon (or mwtrackerd for short) is a project to bring functionality similar to BitTorrent tracking to MuWire. For more info see the [Tracker] page.
|
||||
### MuCats
|
||||
MuCats is a project to create a website for hosting hashes of files shared on the MuWire network. For more info see the [MuCats] project.
|
||||
|
||||
## GPG Fingerprint
|
||||
|
||||
```
|
||||
471B 9FD4 5517 A5ED 101F C57D A728 3207 2D52 5E41
|
||||
@@ -53,3 +69,9 @@ You can find the full key at https://keybase.io/zlatinb
|
||||
[doc]: https://github.com/zlatinb/muwire/tree/master/doc
|
||||
[muwire-pkg]: https://github.com/zlatinb/muwire-pkg
|
||||
[cli options]: https://github.com/zlatinb/muwire/wiki/CLI-Configuration-Options
|
||||
[I2P Github]: https://github.com/i2p/i2p.i2p
|
||||
[Plugin]: https://github.com/zlatinb/muwire/wiki/Plugin
|
||||
[Docker]: https://github.com/zlatinb/muwire/wiki/Docker
|
||||
[jlesage/docker-baseimage-gui]: https://github.com/jlesage/docker-baseimage-gui
|
||||
[Tracker]: https://github.com/zlatinb/muwire/wiki/Tracker-Daemon
|
||||
[MuCats]: https://github.com/zlatinb/mucats
|
||||
|
46
TODO.md
46
TODO.md
@@ -1,8 +1,6 @@
|
||||
# TODO List
|
||||
|
||||
Not in any particular order yet
|
||||
|
||||
### Big Items
|
||||
### Network
|
||||
|
||||
##### Bloom Filters
|
||||
|
||||
@@ -12,15 +10,37 @@ This reduces query traffic by not sending last hop queries to peers that definit
|
||||
|
||||
This helps with scalability
|
||||
|
||||
##### Web UI, REST Interface, etc.
|
||||
### Core
|
||||
|
||||
Basically any non-gui non-cli user interface
|
||||
|
||||
##### Metadata editing and search
|
||||
|
||||
To enable parsing of metadata from known file types and the user editing it or adding manual metadata
|
||||
|
||||
### Small Items
|
||||
|
||||
* Wrapper of some kind for in-place upgrades
|
||||
* Metadata parsing and search
|
||||
* Automatic adjustment of number of I2P tunnels
|
||||
* Persist trust immediately
|
||||
* Check if user-selected download and incomplete locations exist and are writeable
|
||||
* Enum i18n
|
||||
* Ability to share trust list only with trusted users
|
||||
* Confidential files visible only to certain users
|
||||
* Advertise file feed and browseability in upload headers
|
||||
* Manual polling / shared folder re-scan (because polling NAS doesn't work)
|
||||
|
||||
### Chat
|
||||
* echo "unknown/innappropriate command" in the console
|
||||
* break up lines on CR/LF, send multiple messages
|
||||
* Style timestamps and persona names
|
||||
* enforce # in room names or ignore it
|
||||
* auto-create/join channel on server start
|
||||
* jump from notification window to room with message
|
||||
|
||||
### Swing GUI
|
||||
* I2P Status panel - display message when connected to external router
|
||||
* Search box - left identation
|
||||
* Ability to disable switching of tabs on actions
|
||||
* Ability to trust/browse/subscribe from uploads tab
|
||||
|
||||
### Web UI/Plugin
|
||||
* HTML 5 media players
|
||||
* Remove versions from jar names
|
||||
* Security: POST nonces, CSP headers
|
||||
* Upload files from browser to plugin via drag-and-drop
|
||||
* Check permissions, display better errors when sharing local folders
|
||||
|
||||
|
||||
|
20
build.gradle
20
build.gradle
@@ -2,12 +2,26 @@ subprojects {
|
||||
apply plugin: 'groovy'
|
||||
|
||||
dependencies {
|
||||
compile "net.i2p:i2p:${i2pVersion}"
|
||||
compile 'org.codehaus.groovy:groovy-all:2.4.15'
|
||||
compile "org.codehaus.groovy:groovy:${groovyVersion}"
|
||||
compile "org.codehaus.groovy:groovy-jsr223:${groovyVersion}"
|
||||
compile "org.codehaus.groovy:groovy-json:${groovyVersion}"
|
||||
}
|
||||
|
||||
compileGroovy {
|
||||
groovyOptions.optimizationOptions.indy = true
|
||||
groovyOptions.optimizationOptions.indy = false
|
||||
sourceCompatibility = project.sourceCompatibility
|
||||
targetCompatibility = project.targetCompatibility
|
||||
options.compilerArgs += project.compilerArgs
|
||||
options.deprecation = true
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
|
||||
compileJava {
|
||||
sourceCompatibility = project.sourceCompatibility
|
||||
targetCompatibility = project.targetCompatibility
|
||||
options.compilerArgs += project.compilerArgs
|
||||
options.deprecation = true
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
@@ -32,7 +32,7 @@ import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
|
||||
class CliLanterna {
|
||||
private static final String MW_VERSION = "0.6.7"
|
||||
private static final String MW_VERSION = "0.7.1"
|
||||
|
||||
private static volatile Core core
|
||||
|
||||
|
@@ -3,6 +3,7 @@ package com.muwire.clilanterna
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.files.DirectoryWatchedEvent
|
||||
@@ -27,7 +28,6 @@ class FilesModel {
|
||||
core.eventBus.register(FileLoadedEvent.class, this)
|
||||
core.eventBus.register(FileUnsharedEvent.class, this)
|
||||
core.eventBus.register(FileHashedEvent.class, this)
|
||||
core.eventBus.register(AllFilesLoadedEvent.class, this)
|
||||
|
||||
Runnable refreshModel = {refreshModel()}
|
||||
Timer timer = new Timer(true)
|
||||
@@ -37,15 +37,6 @@ class FilesModel {
|
||||
|
||||
}
|
||||
|
||||
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
def eventBus = core.eventBus
|
||||
guiThread.invokeLater {
|
||||
core.muOptions.watchedDirectories.each {
|
||||
eventBus.publish(new FileSharedEvent(file: new File(it)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||
guiThread.invokeLater {
|
||||
sharedFiles.add(e.loadedFile)
|
||||
@@ -72,7 +63,7 @@ class FilesModel {
|
||||
sharedFiles.each {
|
||||
long size = it.getCachedLength()
|
||||
boolean comment = it.comment != null
|
||||
boolean certified = core.certificateManager.hasLocalCertificate(it.getInfoHash())
|
||||
boolean certified = core.certificateManager.hasLocalCertificate(new InfoHash(it.getRoot()))
|
||||
String hits = String.valueOf(it.getHits())
|
||||
String downloaders = String.valueOf(it.getDownloaders().size())
|
||||
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment, certified, hits, downloaders)
|
||||
|
@@ -21,7 +21,6 @@ import com.muwire.core.filecert.UICreateCertificateEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
import com.muwire.core.files.UIPersistFilesEvent
|
||||
|
||||
class FilesView extends BasicWindow {
|
||||
private final FilesModel model
|
||||
@@ -84,7 +83,6 @@ class FilesView extends BasicWindow {
|
||||
|
||||
Button unshareButton = new Button("Unshare", {
|
||||
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||
core.eventBus.publish(new UIPersistFilesEvent())
|
||||
MessageDialog.showMessageDialog(textGUI, "File Unshared", "Unshared "+sf.getFile().getName(), MessageDialogButton.OK)
|
||||
} )
|
||||
Button addCommentButton = new Button("Add Comment", {
|
||||
|
@@ -1,11 +1,38 @@
|
||||
apply plugin : 'application'
|
||||
mainClassName = 'com.muwire.core.Core'
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||
dependencies {
|
||||
compile "net.i2p:router:${i2pVersion}"
|
||||
compile "net.i2p.client:mstreaming:${i2pVersion}"
|
||||
compile "net.i2p.client:streaming:${i2pVersion}"
|
||||
|
||||
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
|
||||
testCompile 'junit:junit:4.12'
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'maven-publish'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api "net.i2p:i2p:${i2pVersion}"
|
||||
api "net.i2p:router:${i2pVersion}"
|
||||
api "net.i2p.client:mstreaming:${i2pVersion}"
|
||||
implementation "net.i2p.client:streaming:${i2pVersion}"
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.codehaus.groovy:groovy-all:3.0.4'
|
||||
}
|
||||
|
||||
|
||||
// this is necessary because applying both groovy and java-library doesn't work well
|
||||
configurations {
|
||||
apiElements.outgoing.variants {
|
||||
classes {
|
||||
artifact file: compileGroovy.destinationDir, builtBy: compileGroovy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// publish core to local maven repo for sister projects
|
||||
publishing {
|
||||
publications {
|
||||
muCore(MavenPublication) {
|
||||
from components.java
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
mavenLocal()
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,12 @@
|
||||
package com.muwire.core
|
||||
|
||||
import com.muwire.core.files.PersisterDoneEvent
|
||||
import com.muwire.core.files.PersisterFolderService
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.logging.Level
|
||||
import java.util.zip.ZipException
|
||||
|
||||
import com.muwire.core.chat.ChatDisconnectionEvent
|
||||
import com.muwire.core.chat.ChatManager
|
||||
@@ -29,9 +34,18 @@ import com.muwire.core.filecert.CertificateManager
|
||||
import com.muwire.core.filecert.UICreateCertificateEvent
|
||||
import com.muwire.core.filecert.UIFetchCertificatesEvent
|
||||
import com.muwire.core.filecert.UIImportCertificateEvent
|
||||
import com.muwire.core.filefeeds.FeedClient
|
||||
import com.muwire.core.filefeeds.FeedFetchEvent
|
||||
import com.muwire.core.filefeeds.FeedItemFetchedEvent
|
||||
import com.muwire.core.filefeeds.FeedManager
|
||||
import com.muwire.core.filefeeds.UIDownloadFeedItemEvent
|
||||
import com.muwire.core.filefeeds.UIFilePublishedEvent
|
||||
import com.muwire.core.filefeeds.UIFeedConfigurationEvent
|
||||
import com.muwire.core.filefeeds.UIFeedDeletedEvent
|
||||
import com.muwire.core.filefeeds.UIFeedUpdateEvent
|
||||
import com.muwire.core.filefeeds.UIFileUnpublishedEvent
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.files.FileHashedEvent
|
||||
import com.muwire.core.files.FileHashingEvent
|
||||
import com.muwire.core.files.FileHasher
|
||||
import com.muwire.core.files.FileLoadedEvent
|
||||
import com.muwire.core.files.FileManager
|
||||
@@ -41,7 +55,11 @@ import com.muwire.core.files.HasherService
|
||||
import com.muwire.core.files.PersisterService
|
||||
import com.muwire.core.files.SideCarFileEvent
|
||||
import com.muwire.core.files.UICommentEvent
|
||||
import com.muwire.core.files.UIPersistFilesEvent
|
||||
import com.muwire.core.files.directories.UISyncDirectoryEvent
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConfigurationEvent
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConvertedEvent
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConverter
|
||||
import com.muwire.core.files.directories.WatchedDirectoryManager
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.DirectoryWatchedEvent
|
||||
@@ -67,6 +85,7 @@ import com.muwire.core.upload.UploadManager
|
||||
import com.muwire.core.util.MuWireLogManager
|
||||
import com.muwire.core.content.ContentControlEvent
|
||||
import com.muwire.core.content.ContentManager
|
||||
import com.muwire.core.tracker.TrackerResponder
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.I2PAppContext
|
||||
@@ -74,10 +93,8 @@ import net.i2p.client.I2PClientFactory
|
||||
import net.i2p.client.I2PSession
|
||||
import net.i2p.client.streaming.I2PSocketManager
|
||||
import net.i2p.client.streaming.I2PSocketManagerFactory
|
||||
import net.i2p.client.streaming.I2PSocketOptions
|
||||
import net.i2p.client.streaming.I2PSocketManager.DisconnectListener
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.crypto.SigType
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.data.PrivateKey
|
||||
import net.i2p.data.Signature
|
||||
@@ -91,21 +108,25 @@ public class Core {
|
||||
|
||||
final EventBus eventBus
|
||||
final Persona me
|
||||
final String version;
|
||||
final File home
|
||||
final Properties i2pOptions
|
||||
final MuWireSettings muOptions
|
||||
|
||||
private final TrustService trustService
|
||||
private final TrustSubscriber trustSubscriber
|
||||
final I2PSession i2pSession;
|
||||
private I2PSocketManager i2pSocketManager
|
||||
final TrustService trustService
|
||||
final TrustSubscriber trustSubscriber
|
||||
private final PersisterService persisterService
|
||||
private final HostCache hostCache
|
||||
private final ConnectionManager connectionManager
|
||||
private final PersisterFolderService persisterFolderService
|
||||
final HostCache hostCache
|
||||
final ConnectionManager connectionManager
|
||||
private final CacheClient cacheClient
|
||||
private final UpdateClient updateClient
|
||||
private final ConnectionAcceptor connectionAcceptor
|
||||
final ConnectionAcceptor connectionAcceptor
|
||||
private final ConnectionEstablisher connectionEstablisher
|
||||
private final HasherService hasherService
|
||||
private final DownloadManager downloadManager
|
||||
final DownloadManager downloadManager
|
||||
private final DirectoryWatcher directoryWatcher
|
||||
final FileManager fileManager
|
||||
final UploadManager uploadManager
|
||||
@@ -113,6 +134,11 @@ public class Core {
|
||||
final CertificateManager certificateManager
|
||||
final ChatServer chatServer
|
||||
final ChatManager chatManager
|
||||
final FeedManager feedManager
|
||||
private final FeedClient feedClient
|
||||
private final WatchedDirectoryConverter watchedDirectoryConverter
|
||||
final WatchedDirectoryManager watchedDirectoryManager
|
||||
private final TrackerResponder trackerResponder
|
||||
|
||||
private final Router router
|
||||
|
||||
@@ -122,43 +148,53 @@ public class Core {
|
||||
|
||||
public Core(MuWireSettings props, File home, String myVersion) {
|
||||
this.home = home
|
||||
this.version = myVersion
|
||||
this.muOptions = props
|
||||
|
||||
i2pOptions = new Properties()
|
||||
def i2pOptionsFile = new File(home,"i2p.properties")
|
||||
// Read defaults
|
||||
def defaultI2PFile = getClass()
|
||||
.getClassLoader().getResource("defaults/i2p.properties");
|
||||
try {
|
||||
defaultI2PFile.withInputStream { i2pOptions.load(it) }
|
||||
} catch (ZipException mystery) {
|
||||
log.log(Level.SEVERE, "couldn't load default i2p properties", mystery)
|
||||
}
|
||||
|
||||
def i2pOptionsFile = new File(home, "i2p.properties")
|
||||
if (i2pOptionsFile.exists()) {
|
||||
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
|
||||
|
||||
if (!i2pOptions.containsKey("inbound.nickname"))
|
||||
i2pOptions["inbound.nickname"] = "MuWire"
|
||||
if (!i2pOptions.containsKey("outbound.nickname"))
|
||||
i2pOptions["outbound.nickname"] = "MuWire"
|
||||
} else {
|
||||
i2pOptions["inbound.nickname"] = "MuWire"
|
||||
i2pOptions["outbound.nickname"] = "MuWire"
|
||||
i2pOptions["inbound.length"] = "3"
|
||||
i2pOptions["inbound.quantity"] = "4"
|
||||
i2pOptions["outbound.length"] = "3"
|
||||
i2pOptions["outbound.quantity"] = "4"
|
||||
i2pOptions["i2cp.tcp.host"] = "127.0.0.1"
|
||||
i2pOptions["i2cp.tcp.port"] = "7654"
|
||||
if (!i2pOptions.containsKey("outbound.nickname"))
|
||||
i2pOptions["outbound.nickname"] = "MuWire"
|
||||
}
|
||||
if (!(i2pOptions.containsKey("i2np.ntcp.port")
|
||||
&& i2pOptions.containsKey("i2np.udp.port")
|
||||
)) {
|
||||
Random r = new Random()
|
||||
int port = r.nextInt(60000) + 4000
|
||||
int port = 9151 + r.nextInt(1 + 30777 - 9151) // this range matches what the i2p router would choose
|
||||
i2pOptions["i2np.ntcp.port"] = String.valueOf(port)
|
||||
i2pOptions["i2np.udp.port"] = String.valueOf(port)
|
||||
i2pOptionsFile.withOutputStream { i2pOptions.store(it, "") }
|
||||
}
|
||||
|
||||
i2pOptions['i2cp.leaseSetEncType']='4,0'
|
||||
|
||||
if (!props.embeddedRouter) {
|
||||
log.info "Initializing I2P context"
|
||||
I2PAppContext.getGlobalContext().logManager()
|
||||
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
|
||||
router = null
|
||||
if (!(I2PAppContext.getGlobalContext() instanceof RouterContext)) {
|
||||
log.info "Initializing I2P context"
|
||||
I2PAppContext.getGlobalContext().logManager()
|
||||
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
|
||||
router = null
|
||||
}
|
||||
} else {
|
||||
log.info("launching embedded router")
|
||||
Properties routerProps = new Properties()
|
||||
routerProps.setProperty("i2p.dir.base", home.getAbsolutePath())
|
||||
routerProps.setProperty("i2p.dir.config", home.getAbsolutePath())
|
||||
routerProps.setProperty("geoip.dir", home.getAbsolutePath() + File.separator + "geoip")
|
||||
routerProps.setProperty("router.excludePeerCaps", "KLM")
|
||||
routerProps.setProperty("i2np.inboundKBytesPerSecond", String.valueOf(props.inBw))
|
||||
routerProps.setProperty("i2np.outboundKBytesPerSecond", String.valueOf(props.outBw))
|
||||
@@ -185,15 +221,13 @@ public class Core {
|
||||
|
||||
|
||||
// options like tunnel length and quantity
|
||||
I2PSession i2pSession
|
||||
I2PSocketManager socketManager
|
||||
keyDat.withInputStream {
|
||||
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
|
||||
i2pSocketManager = new I2PSocketManagerFactory().createDisconnectedManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
|
||||
}
|
||||
socketManager.getDefaultOptions().setReadTimeout(60000)
|
||||
socketManager.getDefaultOptions().setConnectTimeout(30000)
|
||||
socketManager.addDisconnectListener({eventBus.publish(new RouterDisconnectedEvent())} as DisconnectListener)
|
||||
i2pSession = socketManager.getSession()
|
||||
i2pSocketManager.getDefaultOptions().setReadTimeout(60000)
|
||||
i2pSocketManager.getDefaultOptions().setConnectTimeout(30000)
|
||||
i2pSocketManager.addDisconnectListener({eventBus.publish(new RouterDisconnectedEvent())} as DisconnectListener)
|
||||
i2pSession = i2pSocketManager.getSession()
|
||||
|
||||
def destination = new Destination()
|
||||
spk = new SigningPrivateKey(Constants.SIG_TYPE)
|
||||
@@ -254,7 +288,17 @@ public class Core {
|
||||
log.info "initializing persistence service"
|
||||
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
|
||||
eventBus.register(UILoadedEvent.class, persisterService)
|
||||
eventBus.register(UIPersistFilesEvent.class, persisterService)
|
||||
|
||||
log.info "initializing folder persistence service"
|
||||
persisterFolderService = new PersisterFolderService(this, new File(home, "files"), eventBus)
|
||||
eventBus.register(PersisterDoneEvent.class, persisterFolderService)
|
||||
eventBus.register(FileDownloadedEvent.class, persisterFolderService)
|
||||
eventBus.register(FileLoadedEvent.class, persisterFolderService)
|
||||
eventBus.register(FileHashedEvent.class, persisterFolderService)
|
||||
eventBus.register(FileUnsharedEvent.class, persisterFolderService)
|
||||
eventBus.register(UICommentEvent.class, persisterFolderService)
|
||||
eventBus.register(UIFilePublishedEvent.class, persisterFolderService)
|
||||
eventBus.register(UIFileUnpublishedEvent.class, persisterFolderService)
|
||||
|
||||
log.info("initializing host cache")
|
||||
File hostStorage = new File(home, "hosts.json")
|
||||
@@ -274,13 +318,16 @@ public class Core {
|
||||
log.info("initializing cache client")
|
||||
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
|
||||
|
||||
if (!props.plugin) {
|
||||
log.info("initializing update client")
|
||||
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me, spk)
|
||||
eventBus.register(FileDownloadedEvent.class, updateClient)
|
||||
eventBus.register(UIResultBatchEvent.class, updateClient)
|
||||
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me, spk)
|
||||
eventBus.register(FileDownloadedEvent.class, updateClient)
|
||||
eventBus.register(UIResultBatchEvent.class, updateClient)
|
||||
} else
|
||||
log.info("running as plugin, not initializing update client")
|
||||
|
||||
log.info("initializing connector")
|
||||
I2PConnector i2pConnector = new I2PConnector(socketManager)
|
||||
I2PConnector i2pConnector = new I2PConnector(i2pSocketManager)
|
||||
|
||||
log.info("initializing certificate client")
|
||||
CertificateClient certificateClient = new CertificateClient(eventBus, i2pConnector)
|
||||
@@ -294,6 +341,19 @@ public class Core {
|
||||
register(TrustEvent.class, chatServer)
|
||||
}
|
||||
|
||||
log.info("initializing feed manager")
|
||||
feedManager = new FeedManager(eventBus, home)
|
||||
eventBus.with {
|
||||
register(FeedItemFetchedEvent.class, feedManager)
|
||||
register(FeedFetchEvent.class, feedManager)
|
||||
register(UIFeedConfigurationEvent.class, feedManager)
|
||||
register(UIFeedDeletedEvent.class, feedManager)
|
||||
}
|
||||
|
||||
log.info("initializing feed client")
|
||||
feedClient = new FeedClient(i2pConnector, eventBus, me, feedManager)
|
||||
eventBus.register(UIFeedUpdateEvent.class, feedClient)
|
||||
|
||||
log.info "initializing results sender"
|
||||
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props, certificateManager, chatServer)
|
||||
|
||||
@@ -305,6 +365,7 @@ public class Core {
|
||||
log.info("initializing download manager")
|
||||
downloadManager = new DownloadManager(eventBus, trustService, meshManager, props, i2pConnector, home, me)
|
||||
eventBus.register(UIDownloadEvent.class, downloadManager)
|
||||
eventBus.register(UIDownloadFeedItemEvent.class, downloadManager)
|
||||
eventBus.register(UILoadedEvent.class, downloadManager)
|
||||
eventBus.register(FileDownloadedEvent.class, downloadManager)
|
||||
eventBus.register(UIDownloadCancelledEvent.class, downloadManager)
|
||||
@@ -313,7 +374,10 @@ public class Core {
|
||||
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
|
||||
|
||||
log.info("initializing upload manager")
|
||||
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, props)
|
||||
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, persisterFolderService, props)
|
||||
|
||||
log.info("initializing tracker responder")
|
||||
trackerResponder = new TrackerResponder(i2pSession, props, fileManager, downloadManager, meshManager, trustService, me)
|
||||
|
||||
log.info("initializing connection establisher")
|
||||
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
||||
@@ -329,16 +393,11 @@ public class Core {
|
||||
}
|
||||
|
||||
log.info("initializing acceptor")
|
||||
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
|
||||
I2PAcceptor i2pAcceptor = new I2PAcceptor(i2pSocketManager)
|
||||
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
|
||||
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher,
|
||||
certificateManager, chatServer)
|
||||
|
||||
log.info("initializing directory watcher")
|
||||
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
|
||||
eventBus.register(DirectoryWatchedEvent.class, directoryWatcher)
|
||||
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
|
||||
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
|
||||
|
||||
log.info("initializing hasher service")
|
||||
hasherService = new HasherService(new FileHasher(), eventBus, fileManager, props)
|
||||
@@ -360,9 +419,32 @@ public class Core {
|
||||
BrowseManager browseManager = new BrowseManager(i2pConnector, eventBus, me)
|
||||
eventBus.register(UIBrowseEvent.class, browseManager)
|
||||
|
||||
log.info("initializing watched directory converter")
|
||||
watchedDirectoryConverter = new WatchedDirectoryConverter(this)
|
||||
eventBus.register(AllFilesLoadedEvent.class, watchedDirectoryConverter)
|
||||
|
||||
log.info("initializing watched directory manager")
|
||||
watchedDirectoryManager = new WatchedDirectoryManager(home, eventBus, fileManager)
|
||||
eventBus.with {
|
||||
register(WatchedDirectoryConfigurationEvent.class, watchedDirectoryManager)
|
||||
register(WatchedDirectoryConvertedEvent.class, watchedDirectoryManager)
|
||||
register(FileSharedEvent.class, watchedDirectoryManager)
|
||||
register(DirectoryUnsharedEvent.class, watchedDirectoryManager)
|
||||
register(UISyncDirectoryEvent.class, watchedDirectoryManager)
|
||||
}
|
||||
|
||||
log.info("initializing directory watcher")
|
||||
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, watchedDirectoryManager)
|
||||
eventBus.with {
|
||||
register(DirectoryWatchedEvent.class, directoryWatcher)
|
||||
register(WatchedDirectoryConvertedEvent.class, directoryWatcher)
|
||||
register(DirectoryUnsharedEvent.class, directoryWatcher)
|
||||
register(WatchedDirectoryConfigurationEvent.class, directoryWatcher)
|
||||
}
|
||||
}
|
||||
|
||||
public void startServices() {
|
||||
i2pSession.connect()
|
||||
hasherService.start()
|
||||
trustService.start()
|
||||
trustService.waitForLoad()
|
||||
@@ -372,7 +454,10 @@ public class Core {
|
||||
connectionAcceptor.start()
|
||||
connectionEstablisher.start()
|
||||
hostCache.waitForLoad()
|
||||
updateClient.start()
|
||||
updateClient?.start()
|
||||
feedManager.start()
|
||||
feedClient.start()
|
||||
trackerResponder.start()
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
@@ -382,8 +467,16 @@ public class Core {
|
||||
}
|
||||
log.info("saving settings")
|
||||
saveMuSettings()
|
||||
log.info("shutting down host cache")
|
||||
hostCache.stop()
|
||||
log.info("shutting down trust subscriber")
|
||||
trustSubscriber.stop()
|
||||
log.info("shutting down trust service")
|
||||
trustService.stop()
|
||||
log.info("shutting down persister service")
|
||||
persisterService.stop()
|
||||
log.info("shutting down persisterFolder service")
|
||||
persisterFolderService.stop()
|
||||
log.info("shutting down download manager")
|
||||
downloadManager.shutdown()
|
||||
log.info("shutting down connection acceptor")
|
||||
@@ -392,18 +485,34 @@ public class Core {
|
||||
connectionEstablisher.stop()
|
||||
log.info("shutting down directory watcher")
|
||||
directoryWatcher.stop()
|
||||
log.info("shutting down watch directory manager")
|
||||
watchedDirectoryManager.shutdown()
|
||||
log.info("shutting down cache client")
|
||||
cacheClient.stop()
|
||||
log.info("shutting down chat server")
|
||||
chatServer.stop()
|
||||
log.info("shutting down chat manager")
|
||||
chatManager.shutdown()
|
||||
log.info("shutting down feed manager")
|
||||
feedManager.stop()
|
||||
log.info("shutting down feed client")
|
||||
feedClient.stop()
|
||||
log.info("shutting down tracker responder")
|
||||
trackerResponder.stop()
|
||||
log.info("shutting down connection manager")
|
||||
connectionManager.shutdown()
|
||||
if (updateClient != null) {
|
||||
log.info("shutting down update client")
|
||||
updateClient.stop()
|
||||
}
|
||||
log.info("killing socket manager")
|
||||
i2pSocketManager.destroySocketManager()
|
||||
if (router != null) {
|
||||
log.info("shutting down embedded router")
|
||||
router.shutdown(0)
|
||||
}
|
||||
log.info("shutting down event bus");
|
||||
eventBus.shutdown()
|
||||
log.info("shutdown complete")
|
||||
}
|
||||
|
||||
@@ -411,6 +520,11 @@ public class Core {
|
||||
File f = new File(home, "MuWire.properties")
|
||||
f.withPrintWriter("UTF-8", { muOptions.write(it) })
|
||||
}
|
||||
|
||||
public void saveI2PSettings() {
|
||||
File f = new File(home, "i2p.properties")
|
||||
f.withOutputStream { i2pOptions.store(it, "I2P Options") }
|
||||
}
|
||||
|
||||
static main(args) {
|
||||
def home = System.getProperty("user.home") + File.separator + ".MuWire"
|
||||
@@ -436,7 +550,7 @@ public class Core {
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home, "0.6.7")
|
||||
Core core = new Core(props, home, "0.7.1")
|
||||
core.startServices()
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
|
@@ -2,6 +2,7 @@ package com.muwire.core
|
||||
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.logging.Level
|
||||
|
||||
@@ -12,7 +13,7 @@ import groovy.util.logging.Log
|
||||
class EventBus {
|
||||
|
||||
private Map handlers = new HashMap()
|
||||
private final Executor executor = Executors.newSingleThreadExecutor {r ->
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor {r ->
|
||||
def rv = new Thread(r)
|
||||
rv.setDaemon(true)
|
||||
rv.setName("event-bus")
|
||||
@@ -53,4 +54,8 @@ class EventBus {
|
||||
log.info("Unregistering $handler for type $eventType")
|
||||
handlers[eventType]?.remove(handler)
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
executor.shutdownNow()
|
||||
}
|
||||
}
|
||||
|
@@ -31,6 +31,17 @@ class MuWireSettings {
|
||||
boolean shareHiddenFiles
|
||||
boolean searchComments
|
||||
boolean browseFiles
|
||||
boolean allowTracking
|
||||
|
||||
boolean fileFeed
|
||||
boolean advertiseFeed
|
||||
boolean autoPublishSharedFiles
|
||||
boolean defaultFeedAutoDownload
|
||||
int defaultFeedUpdateInterval
|
||||
int defaultFeedItemsToKeep
|
||||
boolean defaultFeedSequential
|
||||
|
||||
|
||||
boolean startChatServer
|
||||
int maxChatConnections
|
||||
boolean advertiseChat
|
||||
@@ -41,6 +52,7 @@ class MuWireSettings {
|
||||
int meshExpiration
|
||||
int speedSmoothSeconds
|
||||
boolean embeddedRouter
|
||||
boolean plugin
|
||||
int inBw, outBw
|
||||
Set<String> watchedKeywords
|
||||
Set<String> watchedRegexes
|
||||
@@ -76,11 +88,23 @@ class MuWireSettings {
|
||||
hostRejectInterval = Integer.valueOf(props.getProperty("hostRejectInterval", "1"))
|
||||
meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60"))
|
||||
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false"))
|
||||
plugin = Boolean.valueOf(props.getProperty("plugin","false"))
|
||||
inBw = Integer.valueOf(props.getProperty("inBw","256"))
|
||||
outBw = Integer.valueOf(props.getProperty("outBw","128"))
|
||||
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
|
||||
browseFiles = Boolean.valueOf(props.getProperty("browseFiles","true"))
|
||||
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60"))
|
||||
allowTracking = Boolean.valueOf(props.getProperty("allowTracking","true"))
|
||||
|
||||
// feed settings
|
||||
fileFeed = Boolean.valueOf(props.getProperty("fileFeed","true"))
|
||||
advertiseFeed = Boolean.valueOf(props.getProperty("advertiseFeed","true"))
|
||||
autoPublishSharedFiles = Boolean.valueOf(props.getProperty("autoPublishSharedFiles", "false"))
|
||||
defaultFeedAutoDownload = Boolean.valueOf(props.getProperty("defaultFeedAutoDownload", "false"))
|
||||
defaultFeedItemsToKeep = Integer.valueOf(props.getProperty("defaultFeedItemsToKeep", "1000"))
|
||||
defaultFeedSequential = Boolean.valueOf(props.getProperty("defaultFeedSequential", "false"))
|
||||
defaultFeedUpdateInterval = Integer.valueOf(props.getProperty("defaultFeedUpdateInterval", "60000"))
|
||||
|
||||
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","10"))
|
||||
totalUploadSlots = Integer.valueOf(props.getProperty("totalUploadSlots","-1"))
|
||||
uploadSlotsPerUser = Integer.valueOf(props.getProperty("uploadSlotsPerUser","-1"))
|
||||
startChatServer = Boolean.valueOf(props.getProperty("startChatServer","false"))
|
||||
@@ -130,10 +154,22 @@ class MuWireSettings {
|
||||
props.setProperty("hostRejectInterval", String.valueOf(hostRejectInterval))
|
||||
props.setProperty("meshExpiration", String.valueOf(meshExpiration))
|
||||
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
|
||||
props.setProperty("plugin", String.valueOf(plugin))
|
||||
props.setProperty("inBw", String.valueOf(inBw))
|
||||
props.setProperty("outBw", String.valueOf(outBw))
|
||||
props.setProperty("searchComments", String.valueOf(searchComments))
|
||||
props.setProperty("browseFiles", String.valueOf(browseFiles))
|
||||
props.setProperty("allowTracking", String.valueOf(allowTracking))
|
||||
|
||||
// feed settings
|
||||
props.setProperty("fileFeed", String.valueOf(fileFeed))
|
||||
props.setProperty("advertiseFeed", String.valueOf(advertiseFeed))
|
||||
props.setProperty("autoPublishSharedFiles", String.valueOf(autoPublishSharedFiles))
|
||||
props.setProperty("defaultFeedAutoDownload", String.valueOf(defaultFeedAutoDownload))
|
||||
props.setProperty("defaultFeedItemsToKeep", String.valueOf(defaultFeedItemsToKeep))
|
||||
props.setProperty("defaultFeedSequential", String.valueOf(defaultFeedSequential))
|
||||
props.setProperty("defaultFeedUpdateInterval", String.valueOf(defaultFeedUpdateInterval))
|
||||
|
||||
props.setProperty("speedSmoothSeconds", String.valueOf(speedSmoothSeconds))
|
||||
props.setProperty("totalUploadSlots", String.valueOf(totalUploadSlots))
|
||||
props.setProperty("uploadSlotsPerUser", String.valueOf(uploadSlotsPerUser))
|
||||
|
@@ -233,7 +233,7 @@ class ChatConnection implements ChatLink {
|
||||
daos.close()
|
||||
byte [] signed = baos.toByteArray()
|
||||
def spk = sender.destination.getSigningPublicKey()
|
||||
def signature = new Signature(Constants.SIG_TYPE, sig)
|
||||
def signature = new Signature(spk.getType(), sig)
|
||||
DSAEngine.getInstance().verifySignature(signature, signed, spk)
|
||||
}
|
||||
|
||||
|
@@ -244,7 +244,7 @@ abstract class Connection implements Closeable {
|
||||
else
|
||||
payload = String.join(" ",search.keywords).getBytes(StandardCharsets.UTF_8)
|
||||
def spk = originator.destination.getSigningPublicKey()
|
||||
def signature = new Signature(Constants.SIG_TYPE, sig)
|
||||
def signature = new Signature(spk.getType(), sig)
|
||||
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
|
||||
log.info("signature didn't match keywords")
|
||||
return
|
||||
@@ -255,7 +255,6 @@ abstract class Connection implements Closeable {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: make this mandatory at some point
|
||||
byte[] sig2 = null
|
||||
long queryTime = 0
|
||||
if (search.sig2 != null) {
|
||||
@@ -267,7 +266,7 @@ abstract class Connection implements Closeable {
|
||||
queryTime = search.queryTime
|
||||
byte [] payload = (search.uuid + String.valueOf(queryTime)).getBytes(StandardCharsets.US_ASCII)
|
||||
def spk = originator.destination.getSigningPublicKey()
|
||||
def signature = new Signature(Constants.SIG_TYPE, sig2)
|
||||
def signature = new Signature(spk.getType(), sig2)
|
||||
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
|
||||
log.info("extended signature didn't match uuid and timestamp")
|
||||
return
|
||||
@@ -278,8 +277,10 @@ abstract class Connection implements Closeable {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else
|
||||
} else {
|
||||
log.info("no extended signature in query")
|
||||
return
|
||||
}
|
||||
|
||||
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
|
||||
searchHash : infohash,
|
||||
|
@@ -15,9 +15,11 @@ import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.chat.ChatServer
|
||||
import com.muwire.core.filecert.Certificate
|
||||
import com.muwire.core.filecert.CertificateManager
|
||||
import com.muwire.core.filefeeds.FeedItems
|
||||
import com.muwire.core.files.FileManager
|
||||
import com.muwire.core.hostcache.HostCache
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
@@ -58,7 +60,7 @@ class ConnectionAcceptor {
|
||||
|
||||
private volatile shutdown
|
||||
|
||||
private volatile int browsed
|
||||
volatile int browsed
|
||||
|
||||
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
||||
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
||||
@@ -161,6 +163,9 @@ class ConnectionAcceptor {
|
||||
case (byte)'I':
|
||||
processIRC(e)
|
||||
break
|
||||
case (byte)'F':
|
||||
processFEED(e)
|
||||
break
|
||||
default:
|
||||
throw new Exception("Invalid read $read")
|
||||
}
|
||||
@@ -310,6 +315,9 @@ class ConnectionAcceptor {
|
||||
boolean chat = false
|
||||
if (headers.containsKey('Chat'))
|
||||
chat = Boolean.parseBoolean(headers['Chat'])
|
||||
boolean feed = false
|
||||
if (headers.containsKey('Feed'))
|
||||
feed = Boolean.parseBoolean(headers['Feed'])
|
||||
|
||||
byte [] personaBytes = Base64.decode(headers['Sender'])
|
||||
Persona sender = new Persona(new ByteArrayInputStream(personaBytes))
|
||||
@@ -329,6 +337,7 @@ class ConnectionAcceptor {
|
||||
def json = slurper.parse(payload)
|
||||
results[i] = ResultsParser.parse(sender, resultsUUID, json)
|
||||
results[i].chat = chat
|
||||
results[i].feed = feed
|
||||
}
|
||||
eventBus.publish(new UIResultBatchEvent(uuid: resultsUUID, results: results))
|
||||
} catch (IOException bad) {
|
||||
@@ -369,13 +378,21 @@ class ConnectionAcceptor {
|
||||
|
||||
def sharedFiles = fileManager.getSharedFiles().values()
|
||||
|
||||
os.write("Count: ${sharedFiles.size()}\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Count: ${sharedFiles.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
boolean chat = chatServer.running.get() && settings.advertiseChat
|
||||
os.write("Chat: ${chat}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
boolean feed = settings.fileFeed && settings.advertiseFeed
|
||||
os.write("Feed: ${feed}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
||||
JsonOutput jsonOutput = new JsonOutput()
|
||||
sharedFiles.each {
|
||||
it.hit(browser, System.currentTimeMillis(), "Browse Host");
|
||||
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
|
||||
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
|
||||
def obj = ResultsSender.sharedFileToObj(it, false, certificates)
|
||||
def json = jsonOutput.toJson(obj)
|
||||
dos.writeShort((short)json.length())
|
||||
@@ -519,5 +536,58 @@ class ConnectionAcceptor {
|
||||
throw new Exception("Invalid IRC connection")
|
||||
chatServer.handle(e)
|
||||
}
|
||||
|
||||
private void processFEED(Endpoint e) {
|
||||
try {
|
||||
byte[] EED = new byte[5];
|
||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
dis.readFully(EED);
|
||||
if (EED != "EED\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new Exception("Invalid FEED connection")
|
||||
|
||||
OutputStream os = e.getOutputStream()
|
||||
|
||||
Map<String, String> headers = DataUtil.readAllHeaders(dis)
|
||||
if (!headers.containsKey("Persona"))
|
||||
throw new Exception("Persona header missing")
|
||||
Persona requestor = new Persona(new ByteArrayInputStream(Base64.decode(headers['Persona'])))
|
||||
if (requestor.destination != e.destination)
|
||||
throw new Exception("Requestor persona mismatch")
|
||||
|
||||
if (!settings.fileFeed) {
|
||||
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
|
||||
long timestamp = 0
|
||||
if (headers.containsKey("Timestamp")) {
|
||||
timestamp = Long.parseLong(headers['Timestamp'])
|
||||
}
|
||||
|
||||
List<SharedFile> published = fileManager.getPublishedSince(timestamp)
|
||||
|
||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Count: ${published.size()}\r\n".getBytes(StandardCharsets.US_ASCII));
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
||||
JsonOutput jsonOutput = new JsonOutput()
|
||||
final long now = System.currentTimeMillis();
|
||||
published.each {
|
||||
it.hit(requestor, now, "Feed Update");
|
||||
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
|
||||
def obj = FeedItems.sharedFileToObj(it, certificates)
|
||||
def json = jsonOutput.toJson(obj)
|
||||
dos.writeShort((short)json.length())
|
||||
dos.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||
}
|
||||
dos.flush()
|
||||
dos.close()
|
||||
} finally {
|
||||
e.close()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -34,6 +34,8 @@ class ConnectionEstablisher {
|
||||
final ExecutorService executor, closer
|
||||
|
||||
final Set inProgress = new ConcurrentHashSet()
|
||||
|
||||
private volatile boolean shutdown
|
||||
|
||||
ConnectionEstablisher(){}
|
||||
|
||||
@@ -60,12 +62,15 @@ class ConnectionEstablisher {
|
||||
}
|
||||
|
||||
void stop() {
|
||||
shutdown = true
|
||||
timer.cancel()
|
||||
executor.shutdownNow()
|
||||
closer.shutdownNow()
|
||||
}
|
||||
|
||||
private void connectIfNeeded() {
|
||||
if (shutdown)
|
||||
return
|
||||
if (!connectionManager.needsConnections())
|
||||
return
|
||||
if (inProgress.size() >= CONCURRENT)
|
||||
@@ -89,6 +94,8 @@ class ConnectionEstablisher {
|
||||
}
|
||||
|
||||
private void connect(Destination toTry) {
|
||||
if (shutdown)
|
||||
return
|
||||
log.info("starting connect to ${toTry.toBase32()}")
|
||||
try {
|
||||
def endpoint = i2pConnector.connect(toTry)
|
||||
@@ -123,6 +130,8 @@ class ConnectionEstablisher {
|
||||
}
|
||||
|
||||
private void fail(Endpoint endpoint) {
|
||||
if (shutdown)
|
||||
return
|
||||
if (!closer.isShutdown()) {
|
||||
closer.execute {
|
||||
endpoint.close()
|
||||
|
@@ -105,8 +105,8 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
||||
@Override
|
||||
void shutdown() {
|
||||
super.shutdown()
|
||||
peerConnections.values().stream().parallel().forEach({v -> v.close()})
|
||||
leafConnections.values().stream().parallel().forEach({v -> v.close()})
|
||||
peerConnections.values().stream().forEach({v -> v.close()})
|
||||
leafConnections.values().stream().forEach({v -> v.close()})
|
||||
peerConnections.clear()
|
||||
leafConnections.clear()
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package com.muwire.core.download
|
||||
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.filefeeds.UIDownloadFeedItemEvent
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.files.FileHasher
|
||||
import com.muwire.core.mesh.Mesh
|
||||
@@ -62,11 +63,6 @@ public class DownloadManager {
|
||||
|
||||
|
||||
public void onUIDownloadEvent(UIDownloadEvent e) {
|
||||
|
||||
File incompletes = muSettings.incompleteLocation
|
||||
if (incompletes == null)
|
||||
incompletes = new File(home, "incompletes")
|
||||
incompletes.mkdirs()
|
||||
|
||||
def size = e.result[0].size
|
||||
def infohash = e.result[0].infohash
|
||||
@@ -79,12 +75,29 @@ public class DownloadManager {
|
||||
destinations.addAll(e.sources)
|
||||
destinations.remove(me.destination)
|
||||
|
||||
Pieces pieces = getPieces(infohash, size, pieceSize, e.sequential)
|
||||
doDownload(infohash, e.target, size, pieceSize, e.sequential, destinations)
|
||||
|
||||
def downloader = new Downloader(eventBus, this, me, e.target, size,
|
||||
infohash, pieceSize, connector, destinations,
|
||||
incompletes, pieces)
|
||||
downloaders.put(infohash, downloader)
|
||||
}
|
||||
|
||||
public void onUIDownloadFeedItemEvent(UIDownloadFeedItemEvent e) {
|
||||
Set<Destination> singleSource = new HashSet<>()
|
||||
singleSource.add(e.item.getPublisher().getDestination())
|
||||
doDownload(e.item.getInfoHash(), e.target, e.item.getSize(), e.item.getPieceSize(),
|
||||
e.sequential, singleSource)
|
||||
}
|
||||
|
||||
private void doDownload(InfoHash infoHash, File target, long size, int pieceSize,
|
||||
boolean sequential, Set<Destination> destinations) {
|
||||
File incompletes = muSettings.incompleteLocation
|
||||
if (incompletes == null)
|
||||
incompletes = new File(home, "incompletes")
|
||||
incompletes.mkdirs()
|
||||
|
||||
Pieces pieces = getPieces(infoHash, size, pieceSize, sequential)
|
||||
def downloader = new Downloader(eventBus, this, me, target, size,
|
||||
infoHash, pieceSize, connector, destinations,
|
||||
incompletes, pieces)
|
||||
downloaders.put(infoHash, downloader)
|
||||
persistDownloaders()
|
||||
executor.execute({downloader.download()} as Runnable)
|
||||
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
||||
@@ -229,4 +242,8 @@ public class DownloadManager {
|
||||
downloaders.values().each { it.stop() }
|
||||
Downloader.executorService.shutdownNow()
|
||||
}
|
||||
|
||||
public boolean isDownloading(InfoHash infoHash) {
|
||||
downloaders.containsKey(infoHash)
|
||||
}
|
||||
}
|
||||
|
@@ -183,15 +183,14 @@ class DownloadSession {
|
||||
mapped.position(position)
|
||||
|
||||
byte[] tmp = new byte[0x1 << 13]
|
||||
DataInputStream dis = new DataInputStream(is)
|
||||
while(mapped.hasRemaining()) {
|
||||
if (mapped.remaining() < tmp.length)
|
||||
tmp = new byte[mapped.remaining()]
|
||||
int read = is.read(tmp)
|
||||
if (read == -1)
|
||||
throw new IOException()
|
||||
dis.readFully(tmp)
|
||||
synchronized(this) {
|
||||
mapped.put(tmp, 0, read)
|
||||
dataSinceLastRead.addAndGet(read)
|
||||
mapped.put(tmp)
|
||||
dataSinceLastRead.addAndGet(tmp.length)
|
||||
pieces.markPartial(piece, mapped.position())
|
||||
}
|
||||
}
|
||||
|
@@ -92,6 +92,22 @@ public class Downloader {
|
||||
public synchronized InfoHash getInfoHash() {
|
||||
infoHash
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
file
|
||||
}
|
||||
|
||||
public int getNPieces() {
|
||||
nPieces
|
||||
}
|
||||
|
||||
public int getPieceSize() {
|
||||
pieceSize
|
||||
}
|
||||
|
||||
public long getLength() {
|
||||
length
|
||||
}
|
||||
|
||||
private synchronized void setInfoHash(InfoHash infoHash) {
|
||||
this.infoHash = infoHash
|
||||
@@ -144,7 +160,7 @@ public class Downloader {
|
||||
long dataRead = dataSinceLastRead.getAndSet(0)
|
||||
long now = System.currentTimeMillis()
|
||||
if (now > lastSpeedRead)
|
||||
currSpeed = (int) (dataRead * 1000.0 / (now - lastSpeedRead))
|
||||
currSpeed = (int) (dataRead * 1000.0d / (now - lastSpeedRead))
|
||||
lastSpeedRead = now
|
||||
}
|
||||
|
||||
@@ -249,6 +265,10 @@ public class Downloader {
|
||||
}
|
||||
active
|
||||
}
|
||||
|
||||
public int getTotalWorkers() {
|
||||
return activeWorkers.size();
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
paused = false
|
||||
@@ -385,8 +405,9 @@ public class Downloader {
|
||||
}
|
||||
eventBus.publish(
|
||||
new FileDownloadedEvent(
|
||||
downloadedFile : new DownloadedFile(file.getCanonicalFile(), getInfoHash(), pieceSizePow2, successfulDestinations),
|
||||
downloader : Downloader.this))
|
||||
downloadedFile : new DownloadedFile(file.getCanonicalFile(), getInfoHash().getRoot(), pieceSizePow2, successfulDestinations),
|
||||
downloader : Downloader.this,
|
||||
infoHash: getInfoHash()))
|
||||
|
||||
}
|
||||
endpoint?.close()
|
||||
|
@@ -2,10 +2,11 @@ package com.muwire.core.download
|
||||
|
||||
class Pieces {
|
||||
private final BitSet done, claimed
|
||||
private final int nPieces
|
||||
final int nPieces
|
||||
private final float ratio
|
||||
private final Random random = new Random()
|
||||
private final Map<Integer,Integer> partials = new HashMap<>()
|
||||
private int cachedDone;
|
||||
|
||||
Pieces(int nPieces) {
|
||||
this(nPieces, 1.0f)
|
||||
@@ -78,6 +79,7 @@ class Pieces {
|
||||
if (piece >= nPieces)
|
||||
throw new IllegalArgumentException("invalid piece marked as downloaded? $piece/$nPieces")
|
||||
done.set(piece)
|
||||
cachedDone = done.cardinality();
|
||||
claimed.set(piece)
|
||||
partials.remove(piece)
|
||||
}
|
||||
@@ -91,11 +93,11 @@ class Pieces {
|
||||
}
|
||||
|
||||
synchronized boolean isComplete() {
|
||||
done.cardinality() == nPieces
|
||||
cachedDone == nPieces
|
||||
}
|
||||
|
||||
synchronized int donePieces() {
|
||||
done.cardinality()
|
||||
cachedDone
|
||||
}
|
||||
|
||||
synchronized boolean isDownloaded(int piece) {
|
||||
@@ -104,6 +106,7 @@ class Pieces {
|
||||
|
||||
synchronized void clearAll() {
|
||||
done.clear()
|
||||
cachedDone = 0
|
||||
claimed.clear()
|
||||
partials.clear()
|
||||
}
|
||||
|
@@ -10,17 +10,20 @@ import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Signature
|
||||
import net.i2p.data.SigningPrivateKey
|
||||
import net.i2p.data.SigningPublicKey
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class Certificate {
|
||||
private final byte version
|
||||
private final InfoHash infoHash
|
||||
private final Name name, comment
|
||||
private final long timestamp
|
||||
private final Persona issuer
|
||||
final Name name, comment
|
||||
final long timestamp
|
||||
final Persona issuer
|
||||
private final byte[] sig
|
||||
|
||||
private volatile byte [] payload
|
||||
|
||||
private String base64;
|
||||
|
||||
Certificate(InputStream is) {
|
||||
version = (byte) (is.read() & 0xFF)
|
||||
if (version > Constants.FILE_CERT_VERSION)
|
||||
@@ -102,7 +105,7 @@ class Certificate {
|
||||
|
||||
byte [] payload = baos.toByteArray()
|
||||
SigningPublicKey spk = issuer.destination.getSigningPublicKey()
|
||||
Signature signature = new Signature(Constants.SIG_TYPE, sig)
|
||||
Signature signature = new Signature(spk.getType(), sig)
|
||||
DSAEngine.getInstance().verifySignature(signature, payload, spk)
|
||||
}
|
||||
|
||||
@@ -131,6 +134,15 @@ class Certificate {
|
||||
os.write(payload)
|
||||
}
|
||||
|
||||
public String toBase64() {
|
||||
if (base64 == null) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
write(baos)
|
||||
base64 = Base64.encode(baos.toByteArray())
|
||||
}
|
||||
return base64;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
version.hashCode() ^ infoHash.hashCode() ^ timestamp.hashCode() ^ name.hashCode() ^ issuer.hashCode() ^ Objects.hashCode(comment)
|
||||
|
@@ -32,7 +32,8 @@ class CertificateClient {
|
||||
fetcherThread.execute({
|
||||
Endpoint endpoint = null
|
||||
try {
|
||||
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.CONNECTING))
|
||||
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.CONNECTING,
|
||||
user : e.host, infoHash : e.infoHash))
|
||||
endpoint = connector.connect(e.host.destination)
|
||||
|
||||
String infoHashString = Base64.encode(e.infoHash.getRoot())
|
||||
@@ -62,7 +63,8 @@ class CertificateClient {
|
||||
int count = Integer.parseInt(headers['Count'])
|
||||
|
||||
// start pulling the certs
|
||||
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.FETCHING, count : count))
|
||||
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.FETCHING, count : count,
|
||||
user : e.host, infoHash : e.infoHash))
|
||||
|
||||
DataInputStream dis = new DataInputStream(is)
|
||||
for (int i = 0; i < count; i++) {
|
||||
@@ -77,11 +79,14 @@ class CertificateClient {
|
||||
continue
|
||||
}
|
||||
if (cert.infoHash == e.infoHash)
|
||||
eventBus.publish(new CertificateFetchedEvent(certificate : cert))
|
||||
eventBus.publish(new CertificateFetchedEvent(certificate : cert, user : e.host, infoHash : e.infoHash))
|
||||
}
|
||||
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.DONE, count : count,
|
||||
user : e.host, infoHash : e.infoHash))
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING,"Fetching certificates failed", bad)
|
||||
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.FAILED))
|
||||
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.FAILED,
|
||||
user : e.host, infoHash : e.infoHash))
|
||||
} finally {
|
||||
endpoint?.close()
|
||||
}
|
||||
|
@@ -1,8 +1,12 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class CertificateFetchEvent extends Event {
|
||||
CertificateFetchStatus status
|
||||
int count
|
||||
Persona user
|
||||
InfoHash infoHash
|
||||
}
|
||||
|
@@ -1,7 +1,11 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class CertificateFetchedEvent extends Event {
|
||||
Certificate certificate
|
||||
Persona user
|
||||
InfoHash infoHash
|
||||
}
|
||||
|
@@ -70,7 +70,7 @@ class CertificateManager {
|
||||
}
|
||||
|
||||
void onUICreateCertificateEvent(UICreateCertificateEvent e) {
|
||||
InfoHash infoHash = e.sharedFile.getInfoHash()
|
||||
InfoHash infoHash = new InfoHash(e.sharedFile.getRoot())
|
||||
String name = e.sharedFile.getFile().getName()
|
||||
long timestamp = System.currentTimeMillis()
|
||||
|
||||
@@ -119,7 +119,7 @@ class CertificateManager {
|
||||
added
|
||||
}
|
||||
|
||||
boolean hasLocalCertificate(InfoHash infoHash) {
|
||||
public boolean hasLocalCertificate(InfoHash infoHash) {
|
||||
if (!byInfoHash.containsKey(infoHash))
|
||||
return false
|
||||
Set<Certificate> set = byInfoHash.get(infoHash)
|
||||
@@ -130,6 +130,13 @@ class CertificateManager {
|
||||
return false
|
||||
}
|
||||
|
||||
public boolean isImported(Certificate certificate) {
|
||||
Set<Certificate> forInfoHash = byInfoHash.get(certificate.infoHash)
|
||||
if (forInfoHash == null)
|
||||
return false
|
||||
forInfoHash.contains(certificate)
|
||||
}
|
||||
|
||||
Set<Certificate> getByInfoHash(InfoHash infoHash) {
|
||||
Set<Certificate> rv = new HashSet<>()
|
||||
if (byInfoHash.containsKey(infoHash))
|
||||
|
110
core/src/main/groovy/com/muwire/core/filefeeds/FeedClient.groovy
Normal file
110
core/src/main/groovy/com/muwire/core/filefeeds/FeedClient.groovy
Normal file
@@ -0,0 +1,110 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import java.util.logging.Level
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
|
||||
@Log
|
||||
class FeedClient {
|
||||
|
||||
private final I2PConnector connector
|
||||
private final EventBus eventBus
|
||||
private final Persona me
|
||||
private final FeedManager feedManager
|
||||
|
||||
private final ExecutorService feedFetcher = Executors.newCachedThreadPool()
|
||||
private final Timer feedUpdater = new Timer("feed-updater", true)
|
||||
|
||||
FeedClient(I2PConnector connector, EventBus eventBus, Persona me, FeedManager feedManager) {
|
||||
this.connector = connector
|
||||
this.eventBus = eventBus
|
||||
this.me = me
|
||||
this.feedManager = feedManager
|
||||
}
|
||||
|
||||
private void start() {
|
||||
feedUpdater.schedule({updateAnyFeeds()} as TimerTask, 60000, 60000)
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
feedUpdater.cancel()
|
||||
feedFetcher.shutdown()
|
||||
}
|
||||
|
||||
private void updateAnyFeeds() {
|
||||
feedManager.getFeedsToUpdate().each { feed ->
|
||||
feedFetcher.execute({updateFeed(feed)} as Runnable)
|
||||
}
|
||||
}
|
||||
|
||||
void onUIFeedUpdateEvent(UIFeedUpdateEvent e) {
|
||||
Feed feed = feedManager.getFeed(e.host)
|
||||
if (feed == null) {
|
||||
log.severe("UI request to update non-existent feed " + e.host.getHumanReadableName())
|
||||
return
|
||||
}
|
||||
|
||||
feedFetcher.execute({updateFeed(feed)} as Runnable)
|
||||
}
|
||||
|
||||
private void updateFeed(Feed feed) {
|
||||
log.info("updating feed " + feed.getPublisher().getHumanReadableName())
|
||||
Endpoint endpoint = null
|
||||
try {
|
||||
eventBus.publish(new FeedFetchEvent(host : feed.getPublisher(), status : FeedFetchStatus.CONNECTING))
|
||||
feed.setLastUpdateAttempt(System.currentTimeMillis())
|
||||
endpoint = connector.connect(feed.getPublisher().getDestination())
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
os.write("FEED\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Persona:${me.toBase64()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Timestamp:${feed.getLastUpdated()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
|
||||
InputStream is = endpoint.getInputStream()
|
||||
String code = DataUtil.readTillRN(is)
|
||||
if (!code.startsWith("200"))
|
||||
throw new IOException("Invalid code $code")
|
||||
|
||||
// parse all headers
|
||||
Map<String,String> headers = DataUtil.readAllHeaders(is)
|
||||
|
||||
if (!headers.containsKey("Count"))
|
||||
throw new IOException("No count header")
|
||||
|
||||
int items = Integer.parseInt(headers['Count'])
|
||||
|
||||
eventBus.publish(new FeedFetchEvent(host : feed.getPublisher(), status : FeedFetchStatus.FETCHING, totalItems: items))
|
||||
|
||||
JsonSlurper slurper = new JsonSlurper()
|
||||
DataInputStream dis = new DataInputStream(new GZIPInputStream(is))
|
||||
for (int i = 0; i < items; i++) {
|
||||
int size = dis.readUnsignedShort()
|
||||
byte [] tmp = new byte[size]
|
||||
dis.readFully(tmp)
|
||||
def json = slurper.parse(tmp)
|
||||
FeedItem item = FeedItems.objToFeedItem(json, feed.getPublisher())
|
||||
eventBus.publish(new FeedItemFetchedEvent(item: item))
|
||||
}
|
||||
|
||||
eventBus.publish(new FeedFetchEvent(host : feed.getPublisher(), status : FeedFetchStatus.FINISHED))
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING, "Feed update failed", bad)
|
||||
eventBus.publish(new FeedFetchEvent(host : feed.getPublisher(), status : FeedFetchStatus.FAILED))
|
||||
} finally {
|
||||
endpoint?.close()
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class FeedFetchEvent extends Event {
|
||||
Persona host
|
||||
FeedFetchStatus status
|
||||
int totalItems
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class FeedItemFetchedEvent extends Event {
|
||||
FeedItem item
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class FeedItemLoadedEvent extends Event {
|
||||
FeedItem item
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.files.FileHasher
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class FeedItems {
|
||||
|
||||
public static def sharedFileToObj(SharedFile sf, int certificates) {
|
||||
def json = [:]
|
||||
json.type = "FeedItem"
|
||||
json.version = 1
|
||||
json.name = Base64.encode(DataUtil.encodei18nString(sf.getFile().getName()))
|
||||
json.infoHash = Base64.encode(sf.getRoot())
|
||||
json.size = sf.getCachedLength()
|
||||
json.pieceSize = sf.getPieceSize()
|
||||
|
||||
if (sf.getComment() != null)
|
||||
json.comment = sf.getComment()
|
||||
|
||||
json.certificates = certificates
|
||||
|
||||
json.timestamp = sf.getPublishedTimestamp()
|
||||
|
||||
json
|
||||
}
|
||||
|
||||
public static FeedItem objToFeedItem(def obj, Persona publisher) throws InvalidFeedItemException {
|
||||
if (obj.timestamp == null)
|
||||
throw new InvalidFeedItemException("No timestamp");
|
||||
if (obj.name == null)
|
||||
throw new InvalidFeedItemException("No name");
|
||||
if (obj.size == null || obj.size <= 0 || obj.size > FileHasher.MAX_SIZE)
|
||||
throw new InvalidFeedItemException("length missing or invalid ${obj.size}")
|
||||
if (obj.pieceSize == null || obj.pieceSize < FileHasher.MIN_PIECE_SIZE_POW2 || obj.pieceSize > FileHasher.MAX_PIECE_SIZE_POW2)
|
||||
throw new InvalidFeedItemException("piece size missing or invalid ${obj.pieceSize}")
|
||||
if (obj.infoHash == null)
|
||||
throw new InvalidFeedItemException("Infohash missing")
|
||||
|
||||
|
||||
InfoHash infoHash
|
||||
try {
|
||||
infoHash = new InfoHash(Base64.decode(obj.infoHash))
|
||||
} catch (Exception bad) {
|
||||
throw new InvalidFeedItemException("Invalid infohash", bad)
|
||||
}
|
||||
|
||||
String name
|
||||
try {
|
||||
name = DataUtil.readi18nString(Base64.decode(obj.name))
|
||||
} catch (Exception bad) {
|
||||
throw new InvalidFeedItemException("Invalid name", bad)
|
||||
}
|
||||
|
||||
int certificates = 0
|
||||
if (obj.certificates != null)
|
||||
certificates = obj.certificates
|
||||
|
||||
new FeedItem(publisher, obj.timestamp, name, obj.size, obj.pieceSize, infoHash, certificates, obj.comment)
|
||||
}
|
||||
|
||||
public static def feedItemToObj(FeedItem item) {
|
||||
def json = [:]
|
||||
json.type = "FeedItem"
|
||||
json.version = 1
|
||||
json.name = Base64.encode(DataUtil.encodei18nString(item.getName()))
|
||||
json.infoHash = Base64.encode(item.getInfoHash().getRoot())
|
||||
json.size = item.getSize()
|
||||
json.pieceSize = item.getPieceSize()
|
||||
json.timestamp = item.getTimestamp()
|
||||
json.certificates = item.getCertificates()
|
||||
json.comment = item.getComment()
|
||||
json
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class FeedLoadedEvent extends Event {
|
||||
Feed feed
|
||||
}
|
@@ -0,0 +1,225 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.stream.Collectors
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.Persona
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
@Log
|
||||
class FeedManager {
|
||||
|
||||
private final EventBus eventBus
|
||||
private final File metadataFolder, itemsFolder
|
||||
private final Map<Persona, Feed> feeds = new ConcurrentHashMap<>()
|
||||
private final Map<Persona, Set<FeedItem>> feedItems = new ConcurrentHashMap<>()
|
||||
|
||||
private final ExecutorService persister = Executors.newSingleThreadExecutor({r ->
|
||||
new Thread(r, "feed persister")
|
||||
} as ThreadFactory)
|
||||
|
||||
|
||||
FeedManager(EventBus eventBus, File home) {
|
||||
this.eventBus = eventBus
|
||||
File feedsFolder = new File(home, "filefeeds")
|
||||
if (!feedsFolder.exists())
|
||||
feedsFolder.mkdir()
|
||||
this.metadataFolder = new File(feedsFolder, "metadata")
|
||||
if (!metadataFolder.exists())
|
||||
metadataFolder.mkdir()
|
||||
this.itemsFolder = new File(feedsFolder, "items")
|
||||
if (!itemsFolder.exists())
|
||||
itemsFolder.mkdir()
|
||||
}
|
||||
|
||||
public Feed getFeed(Persona persona) {
|
||||
feeds.get(persona)
|
||||
}
|
||||
|
||||
public Set<FeedItem> getFeedItems(Persona persona) {
|
||||
feedItems.getOrDefault(persona, Collections.emptySet())
|
||||
}
|
||||
|
||||
public List<Feed> getFeedsToUpdate() {
|
||||
long now = System.currentTimeMillis()
|
||||
feeds.values().stream().
|
||||
filter({Feed f -> !f.getStatus().isActive()}).
|
||||
filter({Feed f -> f.getLastUpdateAttempt() + f.getUpdateInterval() <= now})
|
||||
.collect(Collectors.toList())
|
||||
}
|
||||
|
||||
void start() {
|
||||
log.info("starting feed manager")
|
||||
persister.submit({loadFeeds()} as Runnable)
|
||||
persister.submit({loadItems()} as Runnable)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
persister.shutdown()
|
||||
}
|
||||
|
||||
private void loadFeeds() {
|
||||
def slurper = new JsonSlurper()
|
||||
Files.walk(metadataFolder.toPath()).
|
||||
filter( { it.getFileName().toString().endsWith(".json")}).
|
||||
forEach( {
|
||||
def parsed = slurper.parse(it.toFile())
|
||||
Persona publisher = new Persona(new ByteArrayInputStream(Base64.decode(parsed.publisher)))
|
||||
Feed feed = new Feed(publisher)
|
||||
feed.setUpdateInterval(parsed.updateInterval)
|
||||
feed.setLastUpdated(parsed.lastUpdated)
|
||||
feed.setLastUpdateAttempt(parsed.lastUpdateAttempt)
|
||||
feed.setItemsToKeep(parsed.itemsToKeep)
|
||||
feed.setAutoDownload(parsed.autoDownload)
|
||||
feed.setSequential(parsed.sequential)
|
||||
|
||||
feed.setStatus(FeedFetchStatus.IDLE)
|
||||
|
||||
feeds.put(feed.getPublisher(), feed)
|
||||
|
||||
eventBus.publish(new FeedLoadedEvent(feed : feed))
|
||||
})
|
||||
}
|
||||
|
||||
private void loadItems() {
|
||||
def slurper = new JsonSlurper()
|
||||
feeds.keySet().each { persona ->
|
||||
File itemsFile = getItemsFile(feeds[persona])
|
||||
if (!itemsFile.exists())
|
||||
return // no items yet?
|
||||
itemsFile.eachLine { line ->
|
||||
def parsed = slurper.parseText(line)
|
||||
FeedItem item = FeedItems.objToFeedItem(parsed, persona)
|
||||
Set<FeedItem> items = feedItems.get(persona)
|
||||
if (items == null) {
|
||||
items = new ConcurrentHashSet<>()
|
||||
feedItems.put(persona, items)
|
||||
}
|
||||
items.add(item)
|
||||
eventBus.publish(new FeedItemLoadedEvent(item : item))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onFeedItemFetchedEvent(FeedItemFetchedEvent e) {
|
||||
Set<FeedItem> set = feedItems.get(e.item.getPublisher())
|
||||
if (set == null) {
|
||||
set = new ConcurrentHashSet<>()
|
||||
feedItems.put(e.getItem().getPublisher(), set)
|
||||
}
|
||||
set.add(e.item)
|
||||
}
|
||||
|
||||
void onFeedFetchEvent(FeedFetchEvent e) {
|
||||
|
||||
Feed feed = feeds.get(e.host)
|
||||
if (feed == null) {
|
||||
log.severe("Fetching non-existent feed " + e.host.getHumanReadableName())
|
||||
return
|
||||
}
|
||||
|
||||
feed.setStatus(e.status)
|
||||
|
||||
if (e.status.isActive())
|
||||
return
|
||||
|
||||
if (e.status == FeedFetchStatus.FINISHED) {
|
||||
feed.setStatus(FeedFetchStatus.IDLE)
|
||||
feed.setLastUpdated(e.getTimestamp())
|
||||
}
|
||||
// save feed items, then save feed. This will save partial fetches too
|
||||
// which is ok because the items are stored in a Set
|
||||
persister.submit({saveFeedItems(e.host)} as Runnable)
|
||||
persister.submit({saveFeedMetadata(feed)} as Runnable)
|
||||
}
|
||||
|
||||
void onUIFeedConfigurationEvent(UIFeedConfigurationEvent e) {
|
||||
feeds.put(e.feed.getPublisher(), e.feed)
|
||||
persister.submit({saveFeedMetadata(e.feed)} as Runnable)
|
||||
}
|
||||
|
||||
void onUIFeedDeletedEvent(UIFeedDeletedEvent e) {
|
||||
Feed f = feeds.get(e.host)
|
||||
if (f == null) {
|
||||
log.severe("Deleting a non-existing feed " + e.host.getHumanReadableName())
|
||||
return
|
||||
}
|
||||
persister.submit({deleteFeed(f)} as Runnable)
|
||||
}
|
||||
|
||||
private void saveFeedItems(Persona publisher) {
|
||||
Set<FeedItem> set = feedItems.get(publisher)
|
||||
if (set == null)
|
||||
return // can happen if nothing was published
|
||||
|
||||
Feed feed = feeds[publisher]
|
||||
if (feed == null) {
|
||||
log.severe("Persisting items for non-existing feed " + publisher.getHumanReadableName())
|
||||
return
|
||||
}
|
||||
|
||||
if (feed.getItemsToKeep() == 0)
|
||||
return
|
||||
|
||||
List<FeedItem> list = new ArrayList<>(set)
|
||||
if (feed.getItemsToKeep() > 0 && list.size() > feed.getItemsToKeep()) {
|
||||
log.info("will persist ${feed.getItemsToKeep()}/${list.size()} items")
|
||||
list.sort({l, r ->
|
||||
Long.compare(r.getTimestamp(), l.getTimestamp())
|
||||
} as Comparator<FeedItem>)
|
||||
list = list[0..feed.getItemsToKeep() - 1]
|
||||
}
|
||||
|
||||
|
||||
File itemsFile = getItemsFile(feed)
|
||||
itemsFile.withPrintWriter { writer ->
|
||||
list.each { item ->
|
||||
def obj = FeedItems.feedItemToObj(item)
|
||||
def json = JsonOutput.toJson(obj)
|
||||
writer.println(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveFeedMetadata(Feed feed) {
|
||||
File metadataFile = getMetadataFile(feed)
|
||||
metadataFile.withPrintWriter { writer ->
|
||||
def json = [:]
|
||||
json.publisher = feed.getPublisher().toBase64()
|
||||
json.itemsToKeep = feed.getItemsToKeep()
|
||||
json.lastUpdated = feed.getLastUpdated()
|
||||
json.updateInterval = feed.getUpdateInterval()
|
||||
json.autoDownload = feed.isAutoDownload()
|
||||
json.sequential = feed.isSequential()
|
||||
json.lastUpdateAttempt = feed.getLastUpdateAttempt()
|
||||
json = JsonOutput.toJson(json)
|
||||
writer.println(json)
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteFeed(Feed feed) {
|
||||
feeds.remove(feed.getPublisher())
|
||||
feedItems.remove(feed.getPublisher())
|
||||
getItemsFile(feed).delete()
|
||||
getMetadataFile(feed).delete()
|
||||
}
|
||||
|
||||
private File getItemsFile(Feed feed) {
|
||||
return new File(itemsFolder, feed.getPublisher().destination.toBase32() + ".json")
|
||||
}
|
||||
|
||||
private File getMetadataFile(Feed feed) {
|
||||
return new File(metadataFolder, feed.getPublisher().destination.toBase32() + ".json")
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UIDownloadFeedItemEvent extends Event {
|
||||
FeedItem item
|
||||
File target
|
||||
boolean sequential
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
/**
|
||||
* Emitted when configuration of a feed changes.
|
||||
* The object should already contain the updated values.
|
||||
*/
|
||||
class UIFeedConfigurationEvent extends Event {
|
||||
Feed feed
|
||||
boolean newFeed
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class UIFeedDeletedEvent extends Event {
|
||||
Persona host
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class UIFeedUpdateEvent extends Event {
|
||||
Persona host
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class UIFilePublishedEvent extends Event {
|
||||
SharedFile sf
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class UIFileUnpublishedEvent extends Event {
|
||||
SharedFile sf
|
||||
}
|
@@ -0,0 +1,173 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.DownloadedFile
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.Service
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.util.DataUtil
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
|
||||
import java.util.stream.Collectors
|
||||
|
||||
abstract class BasePersisterService extends Service{
|
||||
|
||||
protected static FileLoadedEvent fromJson(def json) {
|
||||
if (json.file == null || json.length == null || json.infoHash == null || json.hashList == null)
|
||||
throw new IllegalArgumentException()
|
||||
if (!(json.hashList instanceof List))
|
||||
throw new IllegalArgumentException()
|
||||
|
||||
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
||||
file = file.getCanonicalFile()
|
||||
if (!file.exists() || file.isDirectory())
|
||||
return null
|
||||
long length = Long.valueOf(json.length)
|
||||
if (length != file.length())
|
||||
return null
|
||||
|
||||
List hashList = (List) json.hashList
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
hashList.each {
|
||||
byte [] hash = Base64.decode it.toString()
|
||||
if (hash == null)
|
||||
throw new IllegalArgumentException()
|
||||
baos.write hash
|
||||
}
|
||||
byte[] hashListBytes = baos.toByteArray()
|
||||
|
||||
InfoHash ih = InfoHash.fromHashList(hashListBytes)
|
||||
byte [] root = Base64.decode(json.infoHash.toString())
|
||||
if (root == null)
|
||||
throw new IllegalArgumentException()
|
||||
if (!Arrays.equals(root, ih.getRoot()))
|
||||
return null
|
||||
|
||||
int pieceSize = 0
|
||||
if (json.pieceSize != null)
|
||||
pieceSize = json.pieceSize
|
||||
|
||||
if (json.sources != null) {
|
||||
List sources = (List)json.sources
|
||||
Set<Destination> sourceSet = sources.stream().map({ d -> new Destination(d.toString())}).collect Collectors.toSet()
|
||||
DownloadedFile df = new DownloadedFile(file, ih.getRoot(), pieceSize, sourceSet)
|
||||
df.setComment(json.comment)
|
||||
return new FileLoadedEvent(loadedFile : df, infoHash: ih)
|
||||
}
|
||||
|
||||
|
||||
SharedFile sf = new SharedFile(file, ih.getRoot(), pieceSize)
|
||||
sf.setComment(json.comment)
|
||||
if (json.downloaders != null)
|
||||
sf.getDownloaders().addAll(json.downloaders)
|
||||
if (json.searchers != null) {
|
||||
json.searchers.each {
|
||||
Persona searcher = null
|
||||
if (it.searcher != null)
|
||||
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
|
||||
long timestamp = it.timestamp
|
||||
String query = it.query
|
||||
sf.hit(searcher, timestamp, query)
|
||||
}
|
||||
}
|
||||
return new FileLoadedEvent(loadedFile: sf, infoHash: ih)
|
||||
|
||||
}
|
||||
|
||||
protected static FileLoadedEvent fromJsonLite(json) {
|
||||
if (json.file == null || json.length == null || json.root == null)
|
||||
throw new IllegalArgumentException()
|
||||
|
||||
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
||||
file = file.getCanonicalFile()
|
||||
if (!file.exists() || file.isDirectory())
|
||||
return null
|
||||
long length = Long.valueOf(json.length)
|
||||
if (length != file.length())
|
||||
return null
|
||||
|
||||
byte[] root = Base64.decode(json.root)
|
||||
InfoHash ih = new InfoHash(root)
|
||||
|
||||
int pieceSize = 0
|
||||
if (json.pieceSize != null)
|
||||
pieceSize = json.pieceSize
|
||||
|
||||
boolean published = false
|
||||
long publishedTimestamp = -1
|
||||
if (json.published != null && json.published) {
|
||||
published = true
|
||||
publishedTimestamp = json.publishedTimestamp
|
||||
}
|
||||
|
||||
if (json.sources != null) {
|
||||
List sources = (List)json.sources
|
||||
Set<Destination> sourceSet = sources.stream().map({ d -> new Destination(d.toString())}).collect Collectors.toSet()
|
||||
DownloadedFile df = new DownloadedFile(file, ih.getRoot(), pieceSize, sourceSet)
|
||||
if (published)
|
||||
df.publish(publishedTimestamp)
|
||||
df.setComment(json.comment)
|
||||
return new FileLoadedEvent(loadedFile : df, infoHash: ih)
|
||||
}
|
||||
|
||||
|
||||
SharedFile sf = new SharedFile(file, ih.getRoot(), pieceSize)
|
||||
sf.setComment(json.comment)
|
||||
if (published)
|
||||
sf.publish(publishedTimestamp)
|
||||
if (json.downloaders != null)
|
||||
sf.getDownloaders().addAll(json.downloaders)
|
||||
if (json.searchers != null) {
|
||||
json.searchers.each {
|
||||
Persona searcher = null
|
||||
if (it.searcher != null) {
|
||||
try {
|
||||
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
|
||||
} catch (Exception ignore) {
|
||||
return
|
||||
}
|
||||
}
|
||||
long timestamp = it.timestamp
|
||||
String query = it.query
|
||||
sf.hit(searcher, timestamp, query)
|
||||
}
|
||||
}
|
||||
return new FileLoadedEvent(loadedFile: sf, infoHash: ih)
|
||||
}
|
||||
|
||||
protected static toJson(SharedFile sf) {
|
||||
def json = [:]
|
||||
json.file = sf.getB64EncodedFileName()
|
||||
json.length = sf.getCachedLength()
|
||||
json.root = Base64.encode(sf.getRoot())
|
||||
json.pieceSize = sf.getPieceSize()
|
||||
json.comment = sf.getComment()
|
||||
json.hits = sf.getHits()
|
||||
json.downloaders = sf.getDownloaders()
|
||||
|
||||
if (!sf.searches.isEmpty()) {
|
||||
Set searchers = new HashSet<>()
|
||||
sf.searches.each {
|
||||
def search = [:]
|
||||
if (it.searcher != null)
|
||||
search.searcher = it.searcher.toBase64()
|
||||
search.timestamp = it.timestamp
|
||||
search.query = it.query
|
||||
searchers.add(search)
|
||||
}
|
||||
json.searchers = searchers
|
||||
}
|
||||
|
||||
if (sf instanceof DownloadedFile) {
|
||||
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
|
||||
}
|
||||
|
||||
if (sf.isPublished()) {
|
||||
json.published = true
|
||||
json.publishedTimestamp = sf.getPublishedTimestamp()
|
||||
}
|
||||
|
||||
json
|
||||
}
|
||||
}
|
@@ -4,8 +4,9 @@ import com.muwire.core.Event
|
||||
|
||||
class DirectoryUnsharedEvent extends Event {
|
||||
File directory
|
||||
boolean deleted
|
||||
|
||||
public String toString() {
|
||||
super.toString() + " unshared directory "+ directory.toString()
|
||||
super.toString() + " unshared directory "+ directory.toString() + " deleted $deleted"
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,9 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConfigurationEvent
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConvertedEvent
|
||||
import com.muwire.core.files.directories.WatchedDirectoryManager
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.util.SystemVersion
|
||||
@@ -33,27 +36,27 @@ class DirectoryWatcher {
|
||||
}
|
||||
|
||||
private final File home
|
||||
private final MuWireSettings muOptions
|
||||
private final EventBus eventBus
|
||||
private final FileManager fileManager
|
||||
private final WatchedDirectoryManager watchedDirectoryManager
|
||||
private final Thread watcherThread, publisherThread
|
||||
private final Map<File, Long> waitingFiles = new ConcurrentHashMap<>()
|
||||
private final Map<File, WatchKey> watchedDirectories = new ConcurrentHashMap<>()
|
||||
private WatchService watchService
|
||||
private volatile boolean shutdown
|
||||
|
||||
DirectoryWatcher(EventBus eventBus, FileManager fileManager, File home, MuWireSettings muOptions) {
|
||||
DirectoryWatcher(EventBus eventBus, FileManager fileManager, File home, WatchedDirectoryManager watchedDirectoryManager) {
|
||||
this.home = home
|
||||
this.muOptions = muOptions
|
||||
this.eventBus = eventBus
|
||||
this.fileManager = fileManager
|
||||
this.watchedDirectoryManager = watchedDirectoryManager
|
||||
this.watcherThread = new Thread({watch() } as Runnable, "directory-watcher")
|
||||
watcherThread.setDaemon(true)
|
||||
this.publisherThread = new Thread({publish()} as Runnable, "watched-files-publisher")
|
||||
publisherThread.setDaemon(true)
|
||||
}
|
||||
|
||||
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
void onWatchedDirectoryConvertedEvent(WatchedDirectoryConvertedEvent e) {
|
||||
watchService = FileSystems.getDefault().newWatchService()
|
||||
watcherThread.start()
|
||||
publisherThread.start()
|
||||
@@ -71,26 +74,26 @@ class DirectoryWatcher {
|
||||
Path path = canonical.toPath()
|
||||
WatchKey wk = path.register(watchService, kinds)
|
||||
watchedDirectories.put(canonical, wk)
|
||||
|
||||
if (muOptions.watchedDirectories.add(canonical.toString()))
|
||||
saveMuSettings()
|
||||
}
|
||||
|
||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||
WatchKey wk = watchedDirectories.remove(e.directory)
|
||||
wk?.cancel()
|
||||
|
||||
if (muOptions.watchedDirectories.remove(e.directory.toString()))
|
||||
saveMuSettings()
|
||||
}
|
||||
|
||||
private void saveMuSettings() {
|
||||
File muSettingsFile = new File(home, "MuWire.properties")
|
||||
muSettingsFile.withPrintWriter("UTF-8", {
|
||||
muOptions.write(it)
|
||||
})
|
||||
void onWatchedDirectoryConfigurationEvent(WatchedDirectoryConfigurationEvent e) {
|
||||
if (watchService == null)
|
||||
return // still converting
|
||||
if (!e.autoWatch) {
|
||||
WatchKey wk = watchedDirectories.remove(e.directory)
|
||||
wk?.cancel()
|
||||
} else if (!watchedDirectories.containsKey(e.directory)) {
|
||||
Path path = e.directory.toPath()
|
||||
def wk = path.register(watchService, kinds)
|
||||
watchedDirectories.put(e.directory, wk)
|
||||
} // else it was already watched
|
||||
}
|
||||
|
||||
|
||||
private void watch() {
|
||||
try {
|
||||
while(!shutdown) {
|
||||
@@ -115,7 +118,7 @@ class DirectoryWatcher {
|
||||
File f= join(parent, path)
|
||||
log.fine("created entry $f")
|
||||
if (f.isDirectory())
|
||||
f.toPath().register(watchService, kinds)
|
||||
eventBus.publish(new FileSharedEvent(file : f, fromWatch : true))
|
||||
else
|
||||
waitingFiles.put(f, System.currentTimeMillis())
|
||||
}
|
||||
@@ -133,6 +136,10 @@ class DirectoryWatcher {
|
||||
SharedFile sf = fileManager.fileToSharedFile.get(f)
|
||||
if (sf != null)
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf, deleted : true))
|
||||
else if (watchedDirectoryManager.isWatched(f))
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : f, deleted : true))
|
||||
else
|
||||
log.fine("Entry was not relevant");
|
||||
}
|
||||
|
||||
private static File join(Path parent, Path path) {
|
||||
@@ -149,7 +156,7 @@ class DirectoryWatcher {
|
||||
waitingFiles.each { file, timestamp ->
|
||||
if (now - timestamp > WAIT_TIME) {
|
||||
log.fine("publishing file $file")
|
||||
eventBus.publish new FileSharedEvent(file : file)
|
||||
eventBus.publish new FileSharedEvent(file : file, fromWatch: true)
|
||||
published << file
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.DownloadedFile
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.download.Downloader
|
||||
|
||||
import net.i2p.data.Destination
|
||||
@@ -9,4 +10,5 @@ import net.i2p.data.Destination
|
||||
class FileDownloadedEvent extends Event {
|
||||
Downloader downloader
|
||||
DownloadedFile downloadedFile
|
||||
InfoHash infoHash
|
||||
}
|
||||
|
@@ -1,16 +1,18 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class FileHashedEvent extends Event {
|
||||
|
||||
SharedFile sharedFile
|
||||
InfoHash infoHash
|
||||
String error
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
super.toString() + " sharedFile " + sharedFile?.file.getAbsolutePath() + " error: $error"
|
||||
super.toString() + " sharedFile " + sharedFile?.file?.getAbsolutePath() + " error: $error"
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,10 @@
|
||||
package com.muwire.core.files;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public interface FileListCallback<T> {
|
||||
|
||||
public void onFile(File f, T value);
|
||||
|
||||
public void onDirectory(File f);
|
||||
}
|
@@ -1,9 +1,12 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class FileLoadedEvent extends Event {
|
||||
|
||||
SharedFile loadedFile
|
||||
InfoHash infoHash
|
||||
String source
|
||||
}
|
||||
|
@@ -1,5 +1,8 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import java.util.stream.Collectors
|
||||
import java.util.stream.Stream
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
@@ -24,7 +27,8 @@ class FileManager {
|
||||
final Map<String, Set<File>> nameToFiles = new HashMap<>()
|
||||
final Map<String, Set<File>> commentToFile = new HashMap<>()
|
||||
final SearchIndex index = new SearchIndex()
|
||||
final FileTree negativeTree = new FileTree()
|
||||
final FileTree<Void> negativeTree = new FileTree<>()
|
||||
final FileTree<SharedFile> positiveTree = new FileTree<>()
|
||||
final Set<File> sideCarFiles = new HashSet<>()
|
||||
|
||||
FileManager(EventBus eventBus, MuWireSettings settings) {
|
||||
@@ -32,7 +36,7 @@ class FileManager {
|
||||
this.eventBus = eventBus
|
||||
|
||||
for (String negative : settings.negativeFileTree) {
|
||||
negativeTree.add(new File(negative))
|
||||
negativeTree.add(new File(negative), null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +79,7 @@ class FileManager {
|
||||
|
||||
private void addToIndex(SharedFile sf) {
|
||||
log.info("Adding shared file " + sf.getFile())
|
||||
InfoHash infoHash = sf.getInfoHash()
|
||||
InfoHash infoHash = new InfoHash(sf.getRoot())
|
||||
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
||||
if (existing == null) {
|
||||
log.info("adding new root")
|
||||
@@ -84,11 +88,12 @@ class FileManager {
|
||||
}
|
||||
existing.add(sf)
|
||||
fileToSharedFile.put(sf.file, sf)
|
||||
positiveTree.add(sf.file, sf);
|
||||
|
||||
negativeTree.remove(sf.file)
|
||||
String parent = sf.getFile().getParent()
|
||||
if (parent != null && settings.watchedDirectories.contains(parent)) {
|
||||
negativeTree.add(sf.file.getParentFile())
|
||||
negativeTree.add(sf.file.getParentFile(),null)
|
||||
}
|
||||
saveNegativeTree()
|
||||
|
||||
@@ -117,7 +122,7 @@ class FileManager {
|
||||
|
||||
void onFileUnsharedEvent(FileUnsharedEvent e) {
|
||||
SharedFile sf = e.unsharedFile
|
||||
InfoHash infoHash = sf.getInfoHash()
|
||||
InfoHash infoHash = new InfoHash(sf.getRoot())
|
||||
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
||||
if (existing != null) {
|
||||
existing.remove(sf)
|
||||
@@ -127,8 +132,9 @@ class FileManager {
|
||||
}
|
||||
|
||||
fileToSharedFile.remove(sf.file)
|
||||
positiveTree.remove(sf.file)
|
||||
if (!e.deleted && negativeTree.fileToNode.containsKey(sf.file.getParentFile())) {
|
||||
negativeTree.add(sf.file)
|
||||
negativeTree.add(sf.file,null)
|
||||
saveNegativeTree()
|
||||
}
|
||||
|
||||
@@ -190,6 +196,10 @@ class FileManager {
|
||||
Set<SharedFile> getSharedFiles(byte []root) {
|
||||
return rootToFiles.get(new InfoHash(root))
|
||||
}
|
||||
|
||||
boolean isShared(InfoHash infoHash) {
|
||||
rootToFiles.containsKey(infoHash)
|
||||
}
|
||||
|
||||
void onSearchEvent(SearchEvent e) {
|
||||
// hash takes precedence
|
||||
@@ -239,14 +249,26 @@ class FileManager {
|
||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||
negativeTree.remove(e.directory)
|
||||
saveNegativeTree()
|
||||
e.directory.listFiles().each {
|
||||
if (it.isDirectory())
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : it))
|
||||
else {
|
||||
SharedFile sf = fileToSharedFile.get(it)
|
||||
if (sf != null)
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||
if (!e.deleted) {
|
||||
e.directory.listFiles().each {
|
||||
if (it.isDirectory())
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : it))
|
||||
else {
|
||||
SharedFile sf = fileToSharedFile.get(it)
|
||||
if (sf != null)
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
def cb = new DirDeletionCallback()
|
||||
positiveTree.traverse(e.directory, cb)
|
||||
positiveTree.remove(e.directory)
|
||||
cb.unsharedFiles.each {
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : it, deleted: true))
|
||||
}
|
||||
cb.subDirs.each {
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : it, deleted : true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,4 +276,34 @@ class FileManager {
|
||||
settings.negativeFileTree.clear()
|
||||
settings.negativeFileTree.addAll(negativeTree.fileToNode.keySet().collect { it.getAbsolutePath() })
|
||||
}
|
||||
|
||||
public List<SharedFile> getPublishedSince(long timestamp) {
|
||||
synchronized(fileToSharedFile) {
|
||||
fileToSharedFile.values().stream().
|
||||
filter({sf -> sf.isPublished()}).
|
||||
filter({sf -> sf.getPublishedTimestamp() >= timestamp}).
|
||||
collect(Collectors.toList())
|
||||
}
|
||||
}
|
||||
|
||||
private static class DirDeletionCallback implements FileTreeCallback<SharedFile> {
|
||||
|
||||
final List<File> subDirs = new ArrayList<>()
|
||||
final List<SharedFile> unsharedFiles = new ArrayList<>()
|
||||
|
||||
@Override
|
||||
public void onDirectoryEnter(File file) {
|
||||
subDirs.add(file)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDirectoryLeave() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFile(File file, SharedFile value) {
|
||||
unsharedFiles << value
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -5,9 +5,10 @@ import com.muwire.core.Event
|
||||
class FileSharedEvent extends Event {
|
||||
|
||||
File file
|
||||
boolean fromWatch
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + " file: "+file.getAbsolutePath()
|
||||
return super.toString() + " file: "+file.getAbsolutePath() + " fromWatch: $fromWatch"
|
||||
}
|
||||
}
|
||||
|
@@ -2,12 +2,12 @@ package com.muwire.core.files
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class FileTree {
|
||||
class FileTree<T> {
|
||||
|
||||
private final TreeNode root = new TreeNode()
|
||||
private final Map<File, TreeNode> fileToNode = new ConcurrentHashMap<>()
|
||||
|
||||
synchronized void add(File file) {
|
||||
synchronized void add(File file, T value) {
|
||||
List<File> path = new ArrayList<>()
|
||||
path.add(file)
|
||||
while (file.getParentFile() != null) {
|
||||
@@ -23,12 +23,14 @@ class FileTree {
|
||||
if (existing == null) {
|
||||
existing = new TreeNode()
|
||||
existing.file = element
|
||||
existing.isFile = element.isFile()
|
||||
existing.parent = current
|
||||
fileToNode.put(element, existing)
|
||||
current.children.add(existing)
|
||||
}
|
||||
current = existing
|
||||
}
|
||||
current.value = value;
|
||||
}
|
||||
|
||||
synchronized boolean remove(File file) {
|
||||
@@ -45,13 +47,64 @@ class FileTree {
|
||||
true
|
||||
}
|
||||
|
||||
public static class TreeNode {
|
||||
synchronized void traverse(FileTreeCallback<T> callback) {
|
||||
doTraverse(root, callback);
|
||||
}
|
||||
|
||||
synchronized void traverse(File from, FileTreeCallback<T> callback) {
|
||||
if (from == null) {
|
||||
doTraverse(root, callback);
|
||||
} else {
|
||||
TreeNode node = fileToNode.get(from);
|
||||
if (node == null)
|
||||
return
|
||||
doTraverse(node, callback);
|
||||
}
|
||||
}
|
||||
|
||||
private void doTraverse(TreeNode<T> node, FileTreeCallback<T> callback) {
|
||||
boolean leave = false
|
||||
if (node.file != null) {
|
||||
if (node.isFile)
|
||||
callback.onFile(node.file, node.value)
|
||||
else {
|
||||
leave = true
|
||||
callback.onDirectoryEnter(node.file)
|
||||
}
|
||||
}
|
||||
|
||||
node.children.each {
|
||||
doTraverse(it, callback)
|
||||
}
|
||||
|
||||
if (leave)
|
||||
callback.onDirectoryLeave()
|
||||
}
|
||||
|
||||
synchronized void list(File parent, FileListCallback<T> callback) {
|
||||
TreeNode<T> node
|
||||
if (parent == null)
|
||||
node = root
|
||||
else
|
||||
node = fileToNode.get(parent)
|
||||
|
||||
node.children.each {
|
||||
if (it.isFile)
|
||||
callback.onFile(it.file, it.value)
|
||||
else
|
||||
callback.onDirectory(it.file)
|
||||
}
|
||||
}
|
||||
|
||||
public static class TreeNode<T> {
|
||||
TreeNode parent
|
||||
File file
|
||||
boolean isFile
|
||||
T value;
|
||||
final Set<TreeNode> children = new HashSet<>()
|
||||
|
||||
public int hashCode() {
|
||||
file.hashCode()
|
||||
Objects.hash(file)
|
||||
}
|
||||
|
||||
public boolean equals(Object o) {
|
||||
|
@@ -0,0 +1,9 @@
|
||||
package com.muwire.core.files;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public interface FileTreeCallback<T> {
|
||||
public void onDirectoryEnter(File file);
|
||||
public void onDirectoryLeave();
|
||||
public void onFile(File file, T value);
|
||||
}
|
@@ -53,7 +53,6 @@ class HasherService {
|
||||
|
||||
private void process(File f) {
|
||||
if (f.isDirectory()) {
|
||||
eventBus.publish(new DirectoryWatchedEvent(directory : f))
|
||||
f.listFiles().each {
|
||||
eventBus.publish new FileSharedEvent(file: it)
|
||||
}
|
||||
@@ -65,7 +64,8 @@ class HasherService {
|
||||
} else {
|
||||
eventBus.publish new FileHashingEvent(hashingFile: f)
|
||||
def hash = hasher.hashFile f
|
||||
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash, FileHasher.getPieceSize(f.length())))
|
||||
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash.getRoot(), FileHasher.getPieceSize(f.length())),
|
||||
infoHash : hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,12 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
/**
|
||||
* Should be triggered by the old PersisterService
|
||||
* once it has finished reading the old file
|
||||
*
|
||||
* @see PersisterService
|
||||
*/
|
||||
class PersisterDoneEvent extends Event{
|
||||
}
|
@@ -0,0 +1,195 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.*
|
||||
import com.muwire.core.filefeeds.UIFilePublishedEvent
|
||||
import com.muwire.core.filefeeds.UIFileUnpublishedEvent
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.logging.Level
|
||||
|
||||
/**
|
||||
* A persister that stores information about the files shared using
|
||||
* individual JSON files in directories.
|
||||
*
|
||||
* The absolute path's 32bit hash to the shared file is used
|
||||
* to build the directory and filename.
|
||||
*
|
||||
* This persister only starts working once the old persister has finished loading
|
||||
* @see PersisterFolderService#getJsonPath
|
||||
*/
|
||||
@Log
|
||||
class PersisterFolderService extends BasePersisterService {
|
||||
|
||||
final static int CUT_LENGTH = 6
|
||||
|
||||
private final Core core;
|
||||
final File location
|
||||
final EventBus listener
|
||||
final int interval
|
||||
final Timer timer
|
||||
final ExecutorService persisterExecutor = Executors.newSingleThreadExecutor({ r ->
|
||||
new Thread(r, "file persister")
|
||||
} as ThreadFactory)
|
||||
|
||||
PersisterFolderService(Core core, File location, EventBus listener) {
|
||||
this.core = core;
|
||||
this.location = location
|
||||
this.listener = listener
|
||||
this.interval = interval
|
||||
timer = new Timer("file-folder persister timer", true)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
persisterExecutor.shutdown()
|
||||
}
|
||||
|
||||
void onPersisterDoneEvent(PersisterDoneEvent persisterDoneEvent) {
|
||||
log.info("Old persister done")
|
||||
load()
|
||||
}
|
||||
|
||||
void onFileHashedEvent(FileHashedEvent hashedEvent) {
|
||||
if (core.getMuOptions().getAutoPublishSharedFiles() && hashedEvent.sharedFile != null)
|
||||
hashedEvent.sharedFile.publish(System.currentTimeMillis())
|
||||
persistFile(hashedEvent.sharedFile, hashedEvent.infoHash)
|
||||
}
|
||||
|
||||
void onFileDownloadedEvent(FileDownloadedEvent downloadedEvent) {
|
||||
if (core.getMuOptions().getShareDownloadedFiles()) {
|
||||
if (core.getMuOptions().getAutoPublishSharedFiles())
|
||||
downloadedEvent.downloadedFile.publish(System.currentTimeMillis())
|
||||
persistFile(downloadedEvent.downloadedFile, downloadedEvent.infoHash)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rid of the json and hashlists of unshared files
|
||||
* @param unsharedEvent
|
||||
*/
|
||||
void onFileUnsharedEvent(FileUnsharedEvent unsharedEvent) {
|
||||
def jsonPath = getJsonPath(unsharedEvent.unsharedFile)
|
||||
def jsonFile = jsonPath.toFile()
|
||||
if(jsonFile.isFile()){
|
||||
jsonFile.delete()
|
||||
}
|
||||
def hashListPath = getHashListPath(unsharedEvent.unsharedFile)
|
||||
def hashListFile = hashListPath.toFile()
|
||||
if (hashListFile.isFile())
|
||||
hashListFile.delete()
|
||||
}
|
||||
|
||||
void onFileLoadedEvent(FileLoadedEvent loadedEvent) {
|
||||
if(loadedEvent.source == "PersisterService"){
|
||||
log.info("Migrating persisted file from PersisterService: "
|
||||
+ loadedEvent.loadedFile.file.absolutePath.toString())
|
||||
persistFile(loadedEvent.loadedFile, loadedEvent.infoHash)
|
||||
}
|
||||
}
|
||||
|
||||
void onUICommentEvent(UICommentEvent e) {
|
||||
persistFile(e.sharedFile,null)
|
||||
}
|
||||
|
||||
void onUIFilePublishedEvent(UIFilePublishedEvent e) {
|
||||
persistFile(e.sf, null)
|
||||
}
|
||||
|
||||
void onUIFileUnpublishedEvent(UIFileUnpublishedEvent e) {
|
||||
persistFile(e.sf, null)
|
||||
}
|
||||
|
||||
void load() {
|
||||
log.fine("Loading...")
|
||||
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
|
||||
|
||||
if (location.exists() && location.isDirectory()) {
|
||||
try {
|
||||
_load()
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.log(Level.WARNING, "couldn't load files", e)
|
||||
}
|
||||
} else {
|
||||
location.mkdirs()
|
||||
listener.publish(new AllFilesLoadedEvent())
|
||||
}
|
||||
loaded = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads every JSON into memory
|
||||
*/
|
||||
private void _load() {
|
||||
int loaded = 0
|
||||
def slurper = new JsonSlurper()
|
||||
Files.walk(location.toPath())
|
||||
.filter({
|
||||
it.getFileName().toString().endsWith(".json")
|
||||
})
|
||||
.forEach({
|
||||
def parsed = slurper.parse it.toFile()
|
||||
def event = fromJsonLite parsed
|
||||
if (event == null) return
|
||||
|
||||
log.fine("loaded file $event.loadedFile.file")
|
||||
listener.publish event
|
||||
loaded++
|
||||
if (loaded % 10 == 0)
|
||||
Thread.sleep(20)
|
||||
|
||||
})
|
||||
listener.publish(new AllFilesLoadedEvent())
|
||||
}
|
||||
|
||||
private void persistFile(SharedFile sf, InfoHash ih) {
|
||||
persisterExecutor.submit({
|
||||
def jsonPath = getJsonPath(sf)
|
||||
|
||||
def startTime = System.currentTimeMillis()
|
||||
jsonPath.parent.toFile().mkdirs()
|
||||
jsonPath.toFile().withPrintWriter { writer ->
|
||||
def json = toJson sf
|
||||
json = JsonOutput.toJson(json)
|
||||
writer.println json
|
||||
}
|
||||
|
||||
if (ih != null) {
|
||||
def hashListPath = getHashListPath(sf)
|
||||
hashListPath.toFile().bytes = ih.hashList
|
||||
}
|
||||
log.fine("Time(ms) to write json+hashList: " + (System.currentTimeMillis() - startTime))
|
||||
} as Runnable)
|
||||
}
|
||||
private Path getJsonPath(SharedFile sf){
|
||||
def pathHash = sf.getB64PathHash()
|
||||
return Paths.get(
|
||||
location.getAbsolutePath(),
|
||||
pathHash.substring(0, CUT_LENGTH),
|
||||
pathHash.substring(CUT_LENGTH) + ".json"
|
||||
)
|
||||
}
|
||||
|
||||
private Path getHashListPath(SharedFile sf) {
|
||||
def pathHash = sf.getB64PathHash()
|
||||
return Paths.get(
|
||||
location.getAbsolutePath(),
|
||||
pathHash.substring(0, CUT_LENGTH),
|
||||
pathHash.substring(CUT_LENGTH) + ".hashlist"
|
||||
)
|
||||
}
|
||||
|
||||
InfoHash loadInfoHash(SharedFile sf) {
|
||||
def path = getHashListPath(sf)
|
||||
InfoHash.fromHashList(path.toFile().bytes)
|
||||
}
|
||||
}
|
@@ -1,40 +1,24 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import java.nio.file.CopyOption
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.logging.Level
|
||||
import java.util.stream.Collectors
|
||||
|
||||
import com.muwire.core.DownloadedFile
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.Service
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
|
||||
@Log
|
||||
class PersisterService extends Service {
|
||||
class PersisterService extends BasePersisterService {
|
||||
|
||||
final File location
|
||||
final EventBus listener
|
||||
final int interval
|
||||
final Timer timer
|
||||
final FileManager fileManager
|
||||
final ExecutorService persisterExecutor = Executors.newSingleThreadExecutor({ r ->
|
||||
new Thread(r, "file persister")
|
||||
} as ThreadFactory)
|
||||
|
||||
PersisterService(File location, EventBus listener, int interval, FileManager fileManager) {
|
||||
this.location = location
|
||||
@@ -51,10 +35,6 @@ class PersisterService extends Service {
|
||||
void onUILoadedEvent(UILoadedEvent e) {
|
||||
timer.schedule({load()} as TimerTask, 1)
|
||||
}
|
||||
|
||||
void onUIPersistFilesEvent(UIPersistFilesEvent e) {
|
||||
persistFiles()
|
||||
}
|
||||
|
||||
void load() {
|
||||
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
|
||||
@@ -69,6 +49,7 @@ class PersisterService extends Service {
|
||||
def event = fromJson parsed
|
||||
if (event != null) {
|
||||
log.fine("loaded file $event.loadedFile.file")
|
||||
event.source = "PersisterService"
|
||||
listener.publish event
|
||||
loaded++
|
||||
if (loaded % 10 == 0)
|
||||
@@ -76,126 +57,18 @@ class PersisterService extends Service {
|
||||
}
|
||||
}
|
||||
}
|
||||
listener.publish(new AllFilesLoadedEvent())
|
||||
} catch (IllegalArgumentException|NumberFormatException e) {
|
||||
// Backup the old hashes
|
||||
location.renameTo(
|
||||
new File(location.absolutePath + ".bak")
|
||||
)
|
||||
listener.publish(new PersisterDoneEvent())
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "couldn't load files",e)
|
||||
}
|
||||
} else {
|
||||
listener.publish(new AllFilesLoadedEvent())
|
||||
listener.publish(new PersisterDoneEvent())
|
||||
}
|
||||
timer.schedule({persistFiles()} as TimerTask, 1000, interval)
|
||||
loaded = true
|
||||
}
|
||||
|
||||
private static FileLoadedEvent fromJson(def json) {
|
||||
if (json.file == null || json.length == null || json.infoHash == null || json.hashList == null)
|
||||
throw new IllegalArgumentException()
|
||||
if (!(json.hashList instanceof List))
|
||||
throw new IllegalArgumentException()
|
||||
|
||||
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
||||
file = file.getCanonicalFile()
|
||||
if (!file.exists() || file.isDirectory())
|
||||
return null
|
||||
long length = Long.valueOf(json.length)
|
||||
if (length != file.length())
|
||||
return null
|
||||
|
||||
List hashList = (List) json.hashList
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
hashList.each {
|
||||
byte [] hash = Base64.decode it.toString()
|
||||
if (hash == null)
|
||||
throw new IllegalArgumentException()
|
||||
baos.write hash
|
||||
}
|
||||
byte[] hashListBytes = baos.toByteArray()
|
||||
|
||||
InfoHash ih = InfoHash.fromHashList(hashListBytes)
|
||||
byte [] root = Base64.decode(json.infoHash.toString())
|
||||
if (root == null)
|
||||
throw new IllegalArgumentException()
|
||||
if (!Arrays.equals(root, ih.getRoot()))
|
||||
return null
|
||||
|
||||
int pieceSize = 0
|
||||
if (json.pieceSize != null)
|
||||
pieceSize = json.pieceSize
|
||||
|
||||
if (json.sources != null) {
|
||||
List sources = (List)json.sources
|
||||
Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet()
|
||||
DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet)
|
||||
df.setComment(json.comment)
|
||||
return new FileLoadedEvent(loadedFile : df)
|
||||
}
|
||||
|
||||
|
||||
SharedFile sf = new SharedFile(file, ih, pieceSize)
|
||||
sf.setComment(json.comment)
|
||||
if (json.downloaders != null)
|
||||
sf.getDownloaders().addAll(json.downloaders)
|
||||
if (json.searchers != null) {
|
||||
json.searchers.each {
|
||||
Persona searcher = null
|
||||
if (it.searcher != null)
|
||||
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
|
||||
long timestamp = it.timestamp
|
||||
String query = it.query
|
||||
sf.hit(searcher, timestamp, query)
|
||||
}
|
||||
}
|
||||
return new FileLoadedEvent(loadedFile: sf)
|
||||
|
||||
}
|
||||
|
||||
private void persistFiles() {
|
||||
persisterExecutor.submit( {
|
||||
def sharedFiles = fileManager.getSharedFiles()
|
||||
|
||||
File tmp = File.createTempFile("muwire-files", "tmp")
|
||||
tmp.deleteOnExit()
|
||||
tmp.withPrintWriter { writer ->
|
||||
sharedFiles.each { k, v ->
|
||||
def json = toJson(k,v)
|
||||
json = JsonOutput.toJson(json)
|
||||
writer.println json
|
||||
}
|
||||
}
|
||||
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
tmp.delete()
|
||||
} as Runnable)
|
||||
}
|
||||
|
||||
private def toJson(File f, SharedFile sf) {
|
||||
def json = [:]
|
||||
json.file = sf.getB64EncodedFileName()
|
||||
json.length = sf.getCachedLength()
|
||||
InfoHash ih = sf.getInfoHash()
|
||||
json.infoHash = sf.getB64EncodedHashRoot()
|
||||
json.pieceSize = sf.getPieceSize()
|
||||
json.hashList = sf.getB64EncodedHashList()
|
||||
json.comment = sf.getComment()
|
||||
json.hits = sf.getHits()
|
||||
json.downloaders = sf.getDownloaders()
|
||||
|
||||
if (!sf.searches.isEmpty()) {
|
||||
Set searchers = new HashSet<>()
|
||||
sf.searches.each {
|
||||
def search = [:]
|
||||
if (it.searcher != null)
|
||||
search.searcher = it.searcher.toBase64()
|
||||
search.timestamp = it.timestamp
|
||||
search.query = it.query
|
||||
searchers.add(search)
|
||||
}
|
||||
json.searchers = searchers
|
||||
}
|
||||
|
||||
if (sf instanceof DownloadedFile) {
|
||||
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
|
||||
}
|
||||
|
||||
json
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +0,0 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UIPersistFilesEvent extends Event {
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UISyncDirectoryEvent extends Event {
|
||||
File directory
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class WatchedDirectory {
|
||||
final File directory
|
||||
final String encodedName
|
||||
boolean autoWatch
|
||||
int syncInterval
|
||||
long lastSync
|
||||
|
||||
WatchedDirectory(File directory) {
|
||||
this.directory = directory.getCanonicalFile()
|
||||
this.encodedName = Base64.encode(DataUtil.encodei18nString(directory.getAbsolutePath()))
|
||||
}
|
||||
|
||||
def toJson() {
|
||||
def rv = [:]
|
||||
rv.directory = encodedName
|
||||
rv.autoWatch = autoWatch
|
||||
rv.syncInterval = syncInterval
|
||||
rv.lastSync = lastSync
|
||||
rv
|
||||
}
|
||||
|
||||
static WatchedDirectory fromJson(def json) {
|
||||
String dirName = DataUtil.readi18nString(Base64.decode(json.directory))
|
||||
File dir = new File(dirName)
|
||||
def rv = new WatchedDirectory(dir)
|
||||
rv.autoWatch = json.autoWatch
|
||||
rv.syncInterval = json.syncInterval
|
||||
rv.lastSync = json.lastSync
|
||||
rv
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class WatchedDirectoryConfigurationEvent extends Event {
|
||||
File directory
|
||||
boolean autoWatch
|
||||
int syncInterval
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
/**
|
||||
* Emitted when converting an old watched directory entry to the
|
||||
* new format.
|
||||
*/
|
||||
class WatchedDirectoryConvertedEvent extends Event {
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
|
||||
/**
|
||||
* converts the setting-based format to new folder-based format.
|
||||
*/
|
||||
class WatchedDirectoryConverter {
|
||||
|
||||
private final Core core
|
||||
|
||||
WatchedDirectoryConverter(Core core) {
|
||||
this.core = core
|
||||
}
|
||||
|
||||
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
core.getMuOptions().getWatchedDirectories().each {
|
||||
File directory = new File(it)
|
||||
directory = directory.getCanonicalFile()
|
||||
core.eventBus.publish(new WatchedDirectoryConfigurationEvent(directory : directory, autoWatch: true))
|
||||
}
|
||||
core.getMuOptions().getWatchedDirectories().clear()
|
||||
core.saveMuSettings()
|
||||
core.eventBus.publish(new WatchedDirectoryConvertedEvent())
|
||||
}
|
||||
}
|
@@ -0,0 +1,220 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.stream.Stream
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.DirectoryWatchedEvent
|
||||
import com.muwire.core.files.FileListCallback
|
||||
import com.muwire.core.files.FileManager
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
|
||||
@Log
|
||||
class WatchedDirectoryManager {
|
||||
|
||||
private final File home
|
||||
private final EventBus eventBus
|
||||
private final FileManager fileManager
|
||||
|
||||
private final Map<File, WatchedDirectory> watchedDirs = new ConcurrentHashMap<>()
|
||||
|
||||
private final ExecutorService diskIO = Executors.newSingleThreadExecutor({r ->
|
||||
Thread t = new Thread(r, "disk-io")
|
||||
t.setDaemon(true)
|
||||
t
|
||||
} as ThreadFactory)
|
||||
|
||||
private final Timer timer = new Timer("directory-timer", true)
|
||||
|
||||
private boolean converting = true
|
||||
|
||||
WatchedDirectoryManager(File home, EventBus eventBus, FileManager fileManager) {
|
||||
this.home = new File(home, "directories")
|
||||
this.home.mkdir()
|
||||
this.eventBus = eventBus
|
||||
this.fileManager = fileManager
|
||||
}
|
||||
|
||||
public boolean isWatched(File f) {
|
||||
watchedDirs.containsKey(f)
|
||||
}
|
||||
|
||||
public Stream<WatchedDirectory> getWatchedDirsStream() {
|
||||
watchedDirs.values().stream()
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
diskIO.shutdown()
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
void onUISyncDirectoryEvent(UISyncDirectoryEvent e) {
|
||||
def wd = watchedDirs.get(e.directory)
|
||||
if (wd == null) {
|
||||
log.warning("Got a sync event for non-watched dir ${e.directory}")
|
||||
return
|
||||
}
|
||||
diskIO.submit({sync(wd, System.currentTimeMillis())} as Runnable)
|
||||
}
|
||||
|
||||
void onWatchedDirectoryConfigurationEvent(WatchedDirectoryConfigurationEvent e) {
|
||||
if (converting) {
|
||||
def newDir = new WatchedDirectory(e.directory)
|
||||
// conversion is always autowatch really
|
||||
newDir.autoWatch = e.autoWatch
|
||||
persist(newDir)
|
||||
} else {
|
||||
def wd = watchedDirs.get(e.directory)
|
||||
if (wd == null) {
|
||||
log.severe("got a configuration event for a non-watched directory ${e.directory}")
|
||||
return
|
||||
}
|
||||
wd.autoWatch = e.autoWatch
|
||||
wd.syncInterval = e.syncInterval
|
||||
persist(wd)
|
||||
}
|
||||
}
|
||||
|
||||
void onWatchedDirectoryConvertedEvent(WatchedDirectoryConvertedEvent e) {
|
||||
converting = false
|
||||
diskIO.submit({
|
||||
def slurper = new JsonSlurper()
|
||||
Files.walk(home.toPath()).filter({
|
||||
it.getFileName().toString().endsWith(".json")
|
||||
}).
|
||||
forEach {
|
||||
def parsed = slurper.parse(it.toFile())
|
||||
WatchedDirectory wd = WatchedDirectory.fromJson(parsed)
|
||||
watchedDirs.put(wd.directory, wd)
|
||||
}
|
||||
watchedDirs.values().stream().filter({it.autoWatch}).forEach {
|
||||
eventBus.publish(new DirectoryWatchedEvent(directory : it.directory))
|
||||
eventBus.publish(new FileSharedEvent(file : it.directory))
|
||||
}
|
||||
timer.schedule({sync()} as TimerTask, 1000, 1000)
|
||||
} as Runnable)
|
||||
}
|
||||
|
||||
private void persist(WatchedDirectory dir) {
|
||||
diskIO.submit({doPersist(dir)} as Runnable)
|
||||
}
|
||||
|
||||
private void doPersist(WatchedDirectory dir) {
|
||||
def json = JsonOutput.toJson(dir.toJson())
|
||||
def targetFile = new File(home, dir.getEncodedName() + ".json")
|
||||
targetFile.text = json
|
||||
}
|
||||
|
||||
void onFileSharedEvent(FileSharedEvent e) {
|
||||
if (e.file.isFile() || watchedDirs.containsKey(e.file))
|
||||
return
|
||||
|
||||
def wd = new WatchedDirectory(e.file)
|
||||
if (e.fromWatch) {
|
||||
// parent should be already watched, copy settings
|
||||
def parent = watchedDirs.get(e.file.getParentFile())
|
||||
if (parent == null) {
|
||||
log.severe("watching found a directory without a watched parent? ${e.file}")
|
||||
return
|
||||
}
|
||||
wd.autoWatch = parent.autoWatch
|
||||
wd.syncInterval = parent.syncInterval
|
||||
} else
|
||||
wd.autoWatch = true
|
||||
|
||||
watchedDirs.put(wd.directory, wd)
|
||||
persist(wd)
|
||||
if (wd.autoWatch)
|
||||
eventBus.publish(new DirectoryWatchedEvent(directory: wd.directory))
|
||||
}
|
||||
|
||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||
def wd = watchedDirs.remove(e.directory)
|
||||
if (wd == null) {
|
||||
log.warning("unshared a directory that wasn't watched? ${e.directory}")
|
||||
return
|
||||
}
|
||||
|
||||
File persistFile = new File(home, wd.getEncodedName() + ".json")
|
||||
persistFile.delete()
|
||||
}
|
||||
|
||||
private void sync() {
|
||||
long now = System.currentTimeMillis()
|
||||
watchedDirs.values().stream().
|
||||
filter({!it.autoWatch}).
|
||||
filter({it.syncInterval > 0}).
|
||||
filter({it.lastSync + it.syncInterval * 1000 < now}).
|
||||
forEach({wd -> diskIO.submit({sync(wd, now)} as Runnable )})
|
||||
}
|
||||
|
||||
private void sync(WatchedDirectory wd, long now) {
|
||||
log.fine("syncing ${wd.directory}")
|
||||
wd.lastSync = now
|
||||
doPersist(wd)
|
||||
eventBus.publish(new WatchedDirectorySyncEvent(directory: wd.directory, when: now))
|
||||
|
||||
def cb = new DirSyncCallback()
|
||||
fileManager.positiveTree.list(wd.directory, cb)
|
||||
|
||||
Set<File> filesOnFS = new HashSet<>()
|
||||
Set<File> dirsOnFS = new HashSet<>()
|
||||
wd.directory.listFiles().each {
|
||||
File canonical = it.getCanonicalFile()
|
||||
if (canonical.isFile())
|
||||
filesOnFS.add(canonical)
|
||||
else
|
||||
dirsOnFS.add(canonical)
|
||||
}
|
||||
|
||||
Set<File> addedFiles = new HashSet<>(filesOnFS)
|
||||
addedFiles.removeAll(cb.files)
|
||||
addedFiles.each {
|
||||
eventBus.publish(new FileSharedEvent(file : it, fromWatch : true))
|
||||
}
|
||||
Set<File> addedDirs = new HashSet<>(dirsOnFS)
|
||||
addedDirs.removeAll(cb.dirs)
|
||||
addedDirs.each {
|
||||
eventBus.publish(new FileSharedEvent(file : it, fromWatch : true))
|
||||
}
|
||||
|
||||
Set<File> deletedFiles = new HashSet<>(cb.files)
|
||||
deletedFiles.removeAll(filesOnFS)
|
||||
deletedFiles.each {
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : fileManager.getFileToSharedFile().get(it), deleted : true))
|
||||
}
|
||||
Set<File> deletedDirs = new HashSet<>(cb.dirs)
|
||||
deletedDirs.removeAll(dirsOnFS)
|
||||
deletedDirs.each {
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : it, deleted: true))
|
||||
}
|
||||
}
|
||||
|
||||
private static class DirSyncCallback implements FileListCallback<SharedFile> {
|
||||
|
||||
private final Set<File> files = new HashSet<>()
|
||||
private final Set<File> dirs = new HashSet<>()
|
||||
|
||||
@Override
|
||||
public void onFile(File f, SharedFile value) {
|
||||
files.add(f)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDirectory(File f) {
|
||||
dirs.add(f)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class WatchedDirectorySyncEvent extends Event {
|
||||
File directory
|
||||
long when
|
||||
}
|
@@ -1,5 +1,7 @@
|
||||
package com.muwire.core.hostcache
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.connection.ConnectionManager
|
||||
@@ -27,6 +29,7 @@ class CacheClient {
|
||||
final long interval
|
||||
final MuWireSettings settings
|
||||
final Timer timer
|
||||
private final AtomicBoolean stopped = new AtomicBoolean();
|
||||
|
||||
public CacheClient(EventBus eventBus, HostCache cache,
|
||||
ConnectionManager manager, I2PSession session,
|
||||
@@ -47,9 +50,12 @@ class CacheClient {
|
||||
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
stopped.set(true)
|
||||
}
|
||||
|
||||
private void queryIfNeeded() {
|
||||
if (stopped.get())
|
||||
return
|
||||
if (!manager.getConnections().isEmpty())
|
||||
return
|
||||
if (!cache.getHosts(1).isEmpty())
|
||||
@@ -65,7 +71,12 @@ class CacheClient {
|
||||
options.setSendLeaseSet(true)
|
||||
CacheServers.getCacheServers().each {
|
||||
log.info "Querying hostcache ${it.toBase32()}"
|
||||
session.sendMessage(it, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 1, 0, options)
|
||||
try {
|
||||
session.sendMessage(it, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 1, 0, options)
|
||||
} catch (Exception e) {
|
||||
if (!stopped.get())
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +127,8 @@ class CacheClient {
|
||||
|
||||
@Override
|
||||
public void disconnected(I2PSession session) {
|
||||
log.severe "I2P session disconnected"
|
||||
if (!stopped.get())
|
||||
log.severe "Cache client I2P session disconnected"
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -8,10 +8,8 @@ class CacheServers {
|
||||
private static Set<Destination> CACHES = [
|
||||
// zlatinb
|
||||
new Destination("Wddh2E6FyyXBF7SvUYHKdN-vjf3~N6uqQWNeBDTM0P33YjiQCOsyedrjmDZmWFrXUJfJLWnCb5bnKezfk4uDaMyj~uvDG~yvLVcFgcPWSUd7BfGgym-zqcG1q1DcM8vfun-US7YamBlmtC6MZ2j-~Igqzmgshita8aLPCfNAA6S6e2UMjjtG7QIXlxpMec75dkHdJlVWbzrk9z8Qgru3YIk0UztYgEwDNBbm9wInsbHhr3HtAfa02QcgRVqRN2PnQXuqUJs7R7~09FZPEviiIcUpkY3FeyLlX1sgQFBeGeA96blaPvZNGd6KnNdgfLgMebx5SSxC-N4KZMSMBz5cgonQF3~m2HHFRSI85zqZNG5X9bJN85t80ltiv1W1es8ZnQW4es11r7MrvJNXz5bmSH641yJIvS6qI8OJJNpFVBIQSXLD-96TayrLQPaYw~uNZ-eXaE6G5dYhiuN8xHsFI1QkdaUaVZnvDGfsRbpS5GtpUbBDbyLkdPurG0i7dN1wAAAA"),
|
||||
// sNL
|
||||
new Destination("JC63wJNOqSJmymkj4~UJWywBTvDGikKMoYP0HX2Wz9c5l3otXSkwnxWAFL4cKr~Ygh3BNNi2t93vuLIiI1W8AsE42kR~PwRx~Y-WvIHXR6KUejRmOp-n8WidtjKg9k4aDy428uSOedqXDxys5mpoeQXwDsv1CoPTTwnmb1GWFy~oTGIsCguCl~aJWGnqiKarPO3GJQ~ev-NbvAQzUfC3HeP1e6pdI5CGGjExahTCID5UjpJw8GaDXWlGmYWWH303Xu4x-vAHQy1dJLsOBCn8dZravsn5BKJk~j0POUon45CCx-~NYtaPe0Itt9cMdD2ciC76Rep1D0X0sm1SjlSs8sZ52KmF3oaLZ6OzgI9QLMIyBUrfi41sK5I0qTuUVBAkvW1xr~L-20dYJ9TrbOaOb2-vDIfKaxVi6xQOuhgQDiSBhd3qv2m0xGu-BM9DQYfNA0FdMjnZmqjmji9RMavzQSsVFIbQGLbrLepiEFlb7TseCK5UtRp8TxnG7L4gbYevBQAEAAcAAA=="),
|
||||
// dark_trion
|
||||
new Destination("Gec9L29FVcQvYDgpcYuEYdltJn06PPoOWAcAM8Af-gDm~ehlrJcwlLXXs0hidq~yP2A0X7QcDi6i6shAfuEofTchxGJl8LRNqj9lio7WnB7cIixXWL~uCkD7Np5LMX0~akNX34oOb9RcBYVT2U5rFGJmJ7OtBv~IBkGeLhsMrqaCjahd0jdBO~QJ-t82ZKZhh044d24~JEfF9zSJxdBoCdAcXzryGNy7sYtFVDFsPKJudAxSW-UsSQiGw2~k-TxyF0r-iAt1IdzfNu8Lu0WPqLdhDYJWcPldx2PR5uJorI~zo~z3I5RX3NwzarlbD4nEP5s65ahPSfVCEkzmaJUBgP8DvBqlFaX89K4nGRYc7jkEjJ8cX4L6YPXUpTPWcfKkW259WdQY3YFh6x7rzijrGZewpczOLCrt-bZRYgDrUibmZxKZmNhy~lQu4gYVVjkz1i4tL~DWlhIc4y0x2vItwkYLArPPi~ejTnt-~Lhb7oPMXRcWa3UrwGKpFvGZY4NXBQAEAAcAAA==")
|
||||
// echelon
|
||||
new Destination("2MJTl8gYVPK43iJZJa~-5K1OchgPaPHXpqZmKIiKFvxyy8BlIJzUSrF4mazdta--shFHISfT0PEeI95j1yDyKMpGxatUyjSt3ZnyTfAehQR-H2kYV9FvjHo68uA9X5AaGYHKRYLuWMkihMXygd8ywoLjZtFP0UbKMPggfOZaWmjHF4081XoUXt~7MEAeYSQowndiUx0AH3HxNEiv0N373JJS61OsIXb5ctqVKkwIiX1R0ZxESzpP9Xwp8-T0ou8fsLksygbKyH~3K1CyTHjTS51Ux-U-CjOPH9rtCOjjAaifdyMpK0PxW1fVdoGswFywTz9Q-6DUMsIu5TsPMF0-UO1Wn8vCpVAWbBJAOtKCfBrGzp-E~GCbfCNs5xY19nLobMD5ehjsBdI1lXwGDCQ7kBOwC58uuC3BOoazgrB6IrGskyMTexawtthO9mhuPm91bq4xhNaCYHAe059xg5emnM7jFBVzQgjaZ5lOLn~HqcWofJ7oc0doE6XI6kOo~YncBQAEAAcAAA==")
|
||||
]
|
||||
|
||||
static List<Destination> getCacheServers() {
|
||||
|
@@ -10,7 +10,7 @@ import net.i2p.util.ConcurrentHashSet
|
||||
class Mesh {
|
||||
private final InfoHash infoHash
|
||||
private final Set<Persona> sources = new ConcurrentHashSet<>()
|
||||
private final Pieces pieces
|
||||
final Pieces pieces
|
||||
|
||||
Mesh(InfoHash infoHash, Pieces pieces) {
|
||||
this.infoHash = infoHash
|
||||
|
@@ -35,7 +35,7 @@ class BrowseManager {
|
||||
browserThread.execute({
|
||||
Endpoint endpoint = null
|
||||
try {
|
||||
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.CONNECTING))
|
||||
eventBus.publish(new BrowseStatusEvent(host : e.host, status : BrowseStatus.CONNECTING))
|
||||
endpoint = connector.connect(e.host.destination)
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
os.write("BROWSE\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
@@ -55,8 +55,10 @@ class BrowseManager {
|
||||
|
||||
int results = Integer.parseInt(headers['Count'])
|
||||
|
||||
boolean chat = headers.containsKey("Chat") && Boolean.parseBoolean(headers['Chat'])
|
||||
|
||||
// at this stage, start pulling the results
|
||||
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FETCHING, totalResults : results))
|
||||
eventBus.publish(new BrowseStatusEvent(host: e.host, status : BrowseStatus.FETCHING, totalResults : results))
|
||||
|
||||
JsonSlurper slurper = new JsonSlurper()
|
||||
DataInputStream dis = new DataInputStream(new GZIPInputStream(is))
|
||||
@@ -67,14 +69,15 @@ class BrowseManager {
|
||||
dis.readFully(tmp)
|
||||
def json = slurper.parse(tmp)
|
||||
UIResultEvent result = ResultsParser.parse(e.host, uuid, json)
|
||||
result.chat = chat
|
||||
eventBus.publish(result)
|
||||
}
|
||||
|
||||
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FINISHED))
|
||||
eventBus.publish(new BrowseStatusEvent(host: e.host, status : BrowseStatus.FINISHED))
|
||||
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING, "browse failed", bad)
|
||||
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FAILED))
|
||||
eventBus.publish(new BrowseStatusEvent(host: e.host, status : BrowseStatus.FAILED))
|
||||
} finally {
|
||||
endpoint?.close()
|
||||
}
|
||||
|
@@ -1,8 +1,10 @@
|
||||
package com.muwire.core.search
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class BrowseStatusEvent extends Event {
|
||||
Persona host
|
||||
BrowseStatus status
|
||||
int totalResults
|
||||
}
|
||||
|
@@ -77,18 +77,19 @@ class ResultsSender {
|
||||
if (it.getComment() != null) {
|
||||
comment = DataUtil.readi18nString(Base64.decode(it.getComment()))
|
||||
}
|
||||
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
|
||||
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
|
||||
def uiResultEvent = new UIResultEvent( sender : me,
|
||||
name : it.getFile().getName(),
|
||||
size : length,
|
||||
infohash : it.getInfoHash(),
|
||||
infohash : new InfoHash(it.getRoot()),
|
||||
pieceSize : pieceSize,
|
||||
uuid : uuid,
|
||||
browse : settings.browseFiles,
|
||||
sources : suggested,
|
||||
comment : comment,
|
||||
certificates : certificates,
|
||||
chat : chatServer.running.get() && settings.advertiseChat
|
||||
chat : chatServer.running.get() && settings.advertiseChat,
|
||||
feed : settings.fileFeed && settings.advertiseFeed
|
||||
)
|
||||
uiResultEvents << uiResultEvent
|
||||
}
|
||||
@@ -119,7 +120,7 @@ class ResultsSender {
|
||||
me.write(os)
|
||||
os.writeShort((short)results.length)
|
||||
results.each {
|
||||
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
|
||||
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
|
||||
def obj = sharedFileToObj(it, settings.browseFiles, certificates)
|
||||
def json = jsonOutput.toJson(obj)
|
||||
os.writeShort((short)json.length())
|
||||
@@ -138,10 +139,12 @@ class ResultsSender {
|
||||
os.write("Count: $results.length\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
boolean chat = chatServer.running.get() && settings.advertiseChat
|
||||
os.write("Chat: $chat\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
boolean feed = settings.fileFeed && settings.advertiseFeed
|
||||
os.write("Feed: $feed\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
||||
results.each {
|
||||
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
|
||||
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
|
||||
def obj = sharedFileToObj(it, settings.browseFiles, certificates)
|
||||
def json = jsonOutput.toJson(obj)
|
||||
dos.writeShort((short)json.length())
|
||||
@@ -170,7 +173,7 @@ class ResultsSender {
|
||||
obj.type = "Result"
|
||||
obj.version = 2
|
||||
obj.name = encodedName
|
||||
obj.infohash = Base64.encode(sf.getInfoHash().getRoot())
|
||||
obj.infohash = Base64.encode(sf.getRoot())
|
||||
obj.size = sf.getCachedLength()
|
||||
obj.pieceSize = sf.getPieceSize()
|
||||
|
||||
|
@@ -18,7 +18,8 @@ class UIResultEvent extends Event {
|
||||
boolean browse
|
||||
int certificates
|
||||
boolean chat
|
||||
|
||||
boolean feed
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
super.toString() + "name:$name size:$size sender:${sender.getHumanReadableName()} pieceSize $pieceSize"
|
||||
|
@@ -0,0 +1,218 @@
|
||||
package com.muwire.core.tracker
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.logging.Level
|
||||
import java.util.stream.Collectors
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.download.DownloadManager
|
||||
import com.muwire.core.download.Pieces
|
||||
import com.muwire.core.files.FileManager
|
||||
import com.muwire.core.mesh.Mesh
|
||||
import com.muwire.core.mesh.MeshManager
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
import com.muwire.core.trust.TrustService
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.client.I2PSession
|
||||
import net.i2p.client.I2PSessionMuxedListener
|
||||
import net.i2p.client.SendMessageOptions
|
||||
import net.i2p.client.datagram.I2PDatagramDissector
|
||||
import net.i2p.client.datagram.I2PDatagramMaker
|
||||
import net.i2p.data.Base64
|
||||
|
||||
@Log
|
||||
class TrackerResponder {
|
||||
private final I2PSession i2pSession
|
||||
private final MuWireSettings muSettings
|
||||
private final FileManager fileManager
|
||||
private final DownloadManager downloadManager
|
||||
private final MeshManager meshManager
|
||||
private final TrustService trustService
|
||||
private final Persona me
|
||||
|
||||
private final Map<UUID,Long> uuids = new HashMap<>()
|
||||
private final Timer expireTimer = new Timer("tracker-responder-timer", true)
|
||||
|
||||
private static final long UUID_LIFETIME = 10 * 60 * 1000
|
||||
|
||||
private volatile boolean shutdown
|
||||
|
||||
TrackerResponder(I2PSession i2pSession, MuWireSettings muSettings,
|
||||
FileManager fileManager, DownloadManager downloadManager,
|
||||
MeshManager meshManager, TrustService trustService,
|
||||
Persona me) {
|
||||
this.i2pSession = i2pSession
|
||||
this.muSettings = muSettings
|
||||
this.fileManager = fileManager
|
||||
this.downloadManager = downloadManager
|
||||
this.meshManager = meshManager
|
||||
this.trustService = trustService
|
||||
this.me = me
|
||||
}
|
||||
|
||||
void start() {
|
||||
i2pSession.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, Constants.TRACKER_PORT)
|
||||
expireTimer.schedule({expireUUIDs()} as TimerTask, UUID_LIFETIME, UUID_LIFETIME)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
shutdown = true
|
||||
expireTimer.cancel()
|
||||
}
|
||||
|
||||
private void expireUUIDs() {
|
||||
final long now = System.currentTimeMillis()
|
||||
synchronized(uuids) {
|
||||
for (Iterator<UUID> iter = uuids.keySet().iterator(); iter.hasNext();) {
|
||||
UUID uuid = iter.next();
|
||||
Long time = uuids.get(uuid)
|
||||
if (now - time > UUID_LIFETIME)
|
||||
iter.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void respond(host, json) {
|
||||
log.info("responding to host $host with json $json")
|
||||
|
||||
def message = JsonOutput.toJson(json)
|
||||
def maker = new I2PDatagramMaker(i2pSession)
|
||||
message = maker.makeI2PDatagram(message.bytes)
|
||||
def options = new SendMessageOptions()
|
||||
options.setSendLeaseSet(false)
|
||||
i2pSession.sendMessage(host, message, 0, message.length, I2PSession.PROTO_DATAGRAM, Constants.TRACKER_PORT, Constants.TRACKER_PORT, options)
|
||||
}
|
||||
|
||||
class Listener implements I2PSessionMuxedListener {
|
||||
|
||||
@Override
|
||||
public void messageAvailable(I2PSession session, int msgId, long size) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
|
||||
if (proto != I2PSession.PROTO_DATAGRAM) {
|
||||
log.warning "Received unexpected protocol $proto"
|
||||
return
|
||||
}
|
||||
|
||||
byte[] payload = session.receiveMessage(msgId)
|
||||
def dissector = new I2PDatagramDissector()
|
||||
try {
|
||||
dissector.loadI2PDatagram(payload)
|
||||
def sender = dissector.getSender()
|
||||
|
||||
log.info("got a tracker datagram from ${sender.toBase32()}")
|
||||
|
||||
// if not trusted, just drop it
|
||||
TrustLevel trustLevel = trustService.getLevel(sender)
|
||||
|
||||
if (trustLevel == TrustLevel.DISTRUSTED ||
|
||||
(trustLevel == TrustLevel.NEUTRAL && !muSettings.allowUntrusted)) {
|
||||
log.info("dropping, untrusted")
|
||||
return
|
||||
}
|
||||
|
||||
payload = dissector.getPayload()
|
||||
def slurper = new JsonSlurper()
|
||||
def json = slurper.parse(payload)
|
||||
|
||||
if (json.type != "TrackerPing") {
|
||||
log.warning("unknown type $json.type")
|
||||
return
|
||||
}
|
||||
|
||||
def response = [:]
|
||||
response.type = "TrackerPong"
|
||||
response.me = me.toBase64()
|
||||
|
||||
if (json.infoHash == null) {
|
||||
log.warning("infoHash missing")
|
||||
return
|
||||
}
|
||||
|
||||
if (json.uuid == null) {
|
||||
log.warning("uuid missing")
|
||||
return
|
||||
}
|
||||
|
||||
UUID uuid = UUID.fromString(json.uuid)
|
||||
synchronized(uuids) {
|
||||
if (uuids.containsKey(uuid)) {
|
||||
log.warning("duplicate uuid $uuid")
|
||||
return
|
||||
}
|
||||
uuids.put(uuid, System.currentTimeMillis())
|
||||
}
|
||||
response.uuid = json.uuid
|
||||
|
||||
if (!muSettings.allowTracking) {
|
||||
response.code = 403
|
||||
respond(sender, response)
|
||||
return
|
||||
}
|
||||
|
||||
if (json.version != 1) {
|
||||
log.warning("unknown version $json.version")
|
||||
response.code = 400
|
||||
response.message = "I only support version 1"
|
||||
respond(sender,response)
|
||||
return
|
||||
}
|
||||
|
||||
byte[] infoHashBytes = Base64.decode(json.infoHash)
|
||||
InfoHash infoHash = new InfoHash(infoHashBytes)
|
||||
|
||||
log.info("servicing request for infoHash ${json.infoHash} with uuid ${json.uuid}")
|
||||
|
||||
if (!(fileManager.isShared(infoHash) || downloadManager.isDownloading(infoHash))) {
|
||||
response.code = 404
|
||||
respond(sender, response)
|
||||
return
|
||||
}
|
||||
|
||||
Mesh mesh = meshManager.get(infoHash)
|
||||
|
||||
if (fileManager.isShared(infoHash))
|
||||
response.code = 200
|
||||
else if (mesh != null) {
|
||||
response.code = 206
|
||||
Pieces pieces = mesh.getPieces()
|
||||
response.xHave = DataUtil.encodeXHave(pieces, pieces.getnPieces())
|
||||
}
|
||||
|
||||
if (mesh != null)
|
||||
response.altlocs = mesh.getRandom(10, me).stream().map({it.toBase64()}).collect(Collectors.toList())
|
||||
|
||||
respond(sender,response)
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "invalid datagram", e)
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportAbuse(I2PSession session, int severity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnected(I2PSession session) {
|
||||
if (!shutdown)
|
||||
log.severe("Tracker Responder session disconnected")
|
||||
}
|
||||
|
||||
@Override
|
||||
public void errorOccurred(I2PSession session, String message, Throwable error) {
|
||||
log.log(Level.SEVERE, message, error)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -10,8 +10,8 @@ import net.i2p.util.ConcurrentHashSet
|
||||
class RemoteTrustList {
|
||||
public enum Status { NEW, UPDATING, UPDATED, UPDATE_FAILED }
|
||||
|
||||
private final Persona persona
|
||||
private final Set<TrustEntry> good, bad
|
||||
final Persona persona
|
||||
final Set<TrustEntry> good, bad
|
||||
volatile long timestamp
|
||||
volatile boolean forceUpdate
|
||||
Status status = Status.NEW
|
||||
|
@@ -130,8 +130,8 @@ class TrustService extends Service {
|
||||
}
|
||||
|
||||
public static class TrustEntry {
|
||||
private final Persona persona
|
||||
private final String reason
|
||||
final Persona persona
|
||||
final String reason
|
||||
TrustEntry(Persona persona, String reason) {
|
||||
this.persona = persona
|
||||
this.reason = reason
|
||||
|
@@ -26,7 +26,7 @@ class TrustSubscriber {
|
||||
private final I2PConnector i2pConnector
|
||||
private final MuWireSettings settings
|
||||
|
||||
private final Map<Destination, RemoteTrustList> remoteTrustLists = new ConcurrentHashMap<>()
|
||||
final Map<Destination, RemoteTrustList> remoteTrustLists = new ConcurrentHashMap<>()
|
||||
|
||||
private final Object waitLock = new Object()
|
||||
private volatile boolean shutdown
|
||||
@@ -50,7 +50,7 @@ class TrustSubscriber {
|
||||
thread?.interrupt()
|
||||
updateThreads.shutdownNow()
|
||||
}
|
||||
|
||||
|
||||
void onTrustSubscriptionEvent(TrustSubscriptionEvent e) {
|
||||
if (!e.subscribe) {
|
||||
remoteTrustLists.remove(e.persona.destination)
|
||||
@@ -62,6 +62,10 @@ class TrustSubscriber {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSubscribed(Persona p) {
|
||||
remoteTrustLists.containsKey(p.destination)
|
||||
}
|
||||
|
||||
private void checkLoop() {
|
||||
try {
|
||||
|
@@ -2,6 +2,7 @@ package com.muwire.core.update
|
||||
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
@@ -48,6 +49,7 @@ class UpdateClient {
|
||||
private volatile boolean updateDownloading
|
||||
|
||||
private volatile String text
|
||||
private volatile boolean shutdown
|
||||
|
||||
UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings,
|
||||
FileManager fileManager, Persona me, SigningPrivateKey spk) {
|
||||
@@ -63,11 +65,12 @@ class UpdateClient {
|
||||
}
|
||||
|
||||
void start() {
|
||||
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, 2)
|
||||
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, Constants.UPDATE_PORT)
|
||||
timer.schedule({checkUpdate()} as TimerTask, 60000, 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
shutdown = true
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
@@ -83,7 +86,7 @@ class UpdateClient {
|
||||
}
|
||||
|
||||
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||
if (e.downloadedFile.infoHash != updateInfoHash)
|
||||
if (e.infoHash != updateInfoHash)
|
||||
return
|
||||
updateDownloading = false
|
||||
eventBus.publish(new UpdateDownloadedEvent(version : version, signer : signer, text : text))
|
||||
@@ -108,7 +111,7 @@ class UpdateClient {
|
||||
ping = maker.makeI2PDatagram(ping.bytes)
|
||||
def options = new SendMessageOptions()
|
||||
options.setSendLeaseSet(true)
|
||||
session.sendMessage(UpdateServers.UPDATE_SERVER, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 2, 0, options)
|
||||
session.sendMessage(UpdateServers.UPDATE_SERVER, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, Constants.UPDATE_PORT, 0, options)
|
||||
}
|
||||
|
||||
class Listener implements I2PSessionMuxedListener {
|
||||
@@ -198,7 +201,8 @@ class UpdateClient {
|
||||
|
||||
@Override
|
||||
public void disconnected(I2PSession session) {
|
||||
log.severe("I2P session disconnected")
|
||||
if (!shutdown)
|
||||
log.severe("I2P session disconnected")
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -106,7 +106,7 @@ class ContentUploader extends Uploader {
|
||||
return done ? 100 : 0
|
||||
int position = mapped.position()
|
||||
int total = request.getRange().end - request.getRange().start
|
||||
(int)(position * 100.0 / total)
|
||||
(int)(position * 100.0d / total)
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -45,7 +45,7 @@ class HashListUploader extends Uploader {
|
||||
|
||||
@Override
|
||||
public synchronized int getProgress() {
|
||||
(int)(mapped.position() * 100.0 / mapped.capacity())
|
||||
(int)(mapped.position() * 100.0d / mapped.capacity())
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -22,7 +22,7 @@ class Request {
|
||||
|
||||
static Request parseContentRequest(InfoHash infoHash, InputStream is) throws IOException {
|
||||
|
||||
Map<String, String> headers = parseHeaders(is)
|
||||
Map<String, String> headers = DataUtil.readAllHeaders(is)
|
||||
|
||||
if (!headers.containsKey("Range"))
|
||||
throw new IOException("Range header not found")
|
||||
@@ -60,7 +60,7 @@ class Request {
|
||||
}
|
||||
|
||||
static Request parseHashListRequest(InfoHash infoHash, InputStream is) throws IOException {
|
||||
Map<String,String> headers = parseHeaders(is)
|
||||
Map<String,String> headers = DataUtil.readAllHeaders(is)
|
||||
Persona downloader = null
|
||||
if (headers.containsKey("X-Persona")) {
|
||||
def encoded = headers["X-Persona"].trim()
|
||||
@@ -69,55 +69,4 @@ class Request {
|
||||
}
|
||||
new HashListRequest(infoHash : infoHash, headers : headers, downloader : downloader)
|
||||
}
|
||||
|
||||
private static Map<String, String> parseHeaders(InputStream is) {
|
||||
Map<String,String> headers = new HashMap<>()
|
||||
byte [] tmp = new byte[Constants.MAX_HEADER_SIZE]
|
||||
while(headers.size() < Constants.MAX_HEADERS) {
|
||||
boolean r = false
|
||||
boolean n = false
|
||||
int idx = 0
|
||||
while (true) {
|
||||
byte read = is.read()
|
||||
if (read == -1)
|
||||
throw new IOException("Stream closed")
|
||||
|
||||
if (!r && read == N)
|
||||
throw new IOException("Received N before R")
|
||||
if (read == R) {
|
||||
if (r)
|
||||
throw new IOException("double R")
|
||||
r = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (r && !n) {
|
||||
if (read != N)
|
||||
throw new IOException("R not followed by N")
|
||||
n = true
|
||||
break
|
||||
}
|
||||
if (idx == 0x1 << 14)
|
||||
throw new IOException("Header too long")
|
||||
tmp[idx++] = read
|
||||
}
|
||||
|
||||
if (idx == 0)
|
||||
break
|
||||
|
||||
String header = new String(tmp, 0, idx, StandardCharsets.US_ASCII)
|
||||
log.fine("Read header $header")
|
||||
|
||||
int keyIdx = header.indexOf(":")
|
||||
if (keyIdx < 1)
|
||||
throw new IOException("Header key not found")
|
||||
if (keyIdx == header.length())
|
||||
throw new IOException("Header value not found")
|
||||
String key = header.substring(0, keyIdx)
|
||||
String value = header.substring(keyIdx + 1)
|
||||
headers.put(key, value)
|
||||
}
|
||||
headers
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ import com.muwire.core.download.DownloadManager
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.download.SourceDiscoveredEvent
|
||||
import com.muwire.core.files.FileManager
|
||||
import com.muwire.core.files.PersisterFolderService
|
||||
import com.muwire.core.mesh.Mesh
|
||||
import com.muwire.core.mesh.MeshManager
|
||||
|
||||
@@ -22,6 +23,7 @@ import net.i2p.data.Base64
|
||||
public class UploadManager {
|
||||
private final EventBus eventBus
|
||||
private final FileManager fileManager
|
||||
private final PersisterFolderService persisterService
|
||||
private final MeshManager meshManager
|
||||
private final DownloadManager downloadManager
|
||||
private final MuWireSettings props
|
||||
@@ -34,9 +36,11 @@ public class UploadManager {
|
||||
|
||||
public UploadManager(EventBus eventBus, FileManager fileManager,
|
||||
MeshManager meshManager, DownloadManager downloadManager,
|
||||
PersisterFolderService persisterService,
|
||||
MuWireSettings props) {
|
||||
this.eventBus = eventBus
|
||||
this.fileManager = fileManager
|
||||
this.persisterService = persisterService
|
||||
this.meshManager = meshManager
|
||||
this.downloadManager = downloadManager
|
||||
this.props = props
|
||||
@@ -162,7 +166,7 @@ public class UploadManager {
|
||||
|
||||
InfoHash fullInfoHash
|
||||
if (downloader == null) {
|
||||
fullInfoHash = sharedFiles.iterator().next().infoHash
|
||||
fullInfoHash = persisterService.loadInfoHash(sharedFiles.iterator().next())
|
||||
} else {
|
||||
byte [] hashList = downloader.getInfoHash().getHashList()
|
||||
if (hashList != null && hashList.length > 0)
|
||||
|
@@ -49,7 +49,7 @@ abstract class Uploader {
|
||||
final long now = System.currentTimeMillis()
|
||||
long interval = Math.max(1000, now - lastSpeedRead)
|
||||
lastSpeedRead = now;
|
||||
int currSpeed = (int) (dataSinceLastRead * 1000.0 / interval)
|
||||
int currSpeed = (int) (dataSinceLastRead * 1000.0d / interval)
|
||||
dataSinceLastRead = 0
|
||||
|
||||
// normalize to speedArr.size
|
||||
|
@@ -4,6 +4,8 @@ import net.i2p.crypto.SigType;
|
||||
|
||||
public class Constants {
|
||||
public static final byte PERSONA_VERSION = (byte)1;
|
||||
public static final String INVALID_NICKNAME_CHARS = "'\"();<>=@$%";
|
||||
public static final int MAX_NICKNAME_LENGTH = 30;
|
||||
public static final byte FILE_CERT_VERSION = (byte)2;
|
||||
public static final int CHAT_VERSION = 1;
|
||||
|
||||
@@ -17,5 +19,8 @@ public class Constants {
|
||||
|
||||
public static final int MAX_COMMENT_LENGTH = 0x1 << 15;
|
||||
|
||||
public static final long MAX_QUERY_AGE = 5 * 60 * 1000L;
|
||||
public static final long MAX_QUERY_AGE = 5 * 60 * 1000L;
|
||||
|
||||
public static final int UPDATE_PORT = 2;
|
||||
public static final int TRACKER_PORT = 3;
|
||||
}
|
||||
|
@@ -10,9 +10,9 @@ public class DownloadedFile extends SharedFile {
|
||||
|
||||
private final Set<Destination> sources;
|
||||
|
||||
public DownloadedFile(File file, InfoHash infoHash, int pieceSize, Set<Destination> sources)
|
||||
public DownloadedFile(File file, byte[] root, int pieceSize, Set<Destination> sources)
|
||||
throws IOException {
|
||||
super(file, infoHash, pieceSize);
|
||||
super(file, root, pieceSize);
|
||||
this.sources = sources;
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,25 @@
|
||||
package com.muwire.core;
|
||||
|
||||
public class InvalidNicknameException extends Exception {
|
||||
|
||||
public InvalidNicknameException() {
|
||||
}
|
||||
|
||||
public InvalidNicknameException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InvalidNicknameException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public InvalidNicknameException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public InvalidNicknameException(String message, Throwable cause, boolean enableSuppression,
|
||||
boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
|
||||
}
|
@@ -7,6 +7,8 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import com.muwire.core.util.DataUtil;
|
||||
|
||||
import net.i2p.crypto.DSAEngine;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataFormatException;
|
||||
@@ -25,12 +27,15 @@ public class Persona {
|
||||
private volatile String base64;
|
||||
private volatile byte[] payload;
|
||||
|
||||
public Persona(InputStream personaStream) throws IOException, DataFormatException, InvalidSignatureException {
|
||||
public Persona(InputStream personaStream) throws IOException, DataFormatException, InvalidSignatureException, InvalidNicknameException {
|
||||
version = (byte) (personaStream.read() & 0xFF);
|
||||
if (version != Constants.PERSONA_VERSION)
|
||||
throw new IOException("Unknown version "+version);
|
||||
|
||||
name = new Name(personaStream);
|
||||
if (!DataUtil.isValidName(name.name))
|
||||
throw new InvalidNicknameException(name.name + " is not a valid nickname");
|
||||
|
||||
destination = Destination.create(personaStream);
|
||||
sig = new byte[SIG_LEN];
|
||||
DataInputStream dis = new DataInputStream(personaStream);
|
||||
@@ -38,7 +43,7 @@ public class Persona {
|
||||
if (!verify(version, name, destination, sig))
|
||||
throw new InvalidSignatureException(getHumanReadableName() + " didn't verify");
|
||||
}
|
||||
|
||||
|
||||
private static boolean verify(byte version, Name name, Destination destination, byte [] sig)
|
||||
throws IOException, DataFormatException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
@@ -47,7 +52,7 @@ public class Persona {
|
||||
destination.writeBytes(baos);
|
||||
byte[] payload = baos.toByteArray();
|
||||
SigningPublicKey spk = destination.getSigningPublicKey();
|
||||
Signature signature = new Signature(Constants.SIG_TYPE, sig);
|
||||
Signature signature = new Signature(spk.getType(), sig);
|
||||
return DSAEngine.getInstance().verifySignature(signature, payload, spk);
|
||||
}
|
||||
|
||||
@@ -68,11 +73,19 @@ public class Persona {
|
||||
humanReadableName = name.getName() + "@" + destination.toBase32().substring(0,32);
|
||||
return humanReadableName;
|
||||
}
|
||||
|
||||
public Destination getDestination() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public String toBase64() throws DataFormatException, IOException {
|
||||
public String toBase64() {
|
||||
if (base64 == null) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
write(baos);
|
||||
try {
|
||||
write(baos);
|
||||
} catch (Exception impossible) {
|
||||
throw new RuntimeException(impossible);
|
||||
}
|
||||
base64 = Base64.encode(baos.toByteArray());
|
||||
}
|
||||
return base64;
|
||||
|
@@ -2,7 +2,10 @@ package com.muwire.core;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -16,44 +19,49 @@ import net.i2p.data.Base64;
|
||||
public class SharedFile {
|
||||
|
||||
private final File file;
|
||||
private final InfoHash infoHash;
|
||||
private final byte[] root;
|
||||
private final int pieceSize;
|
||||
|
||||
private final String cachedPath;
|
||||
private final long cachedLength;
|
||||
|
||||
|
||||
private String b64PathHash;
|
||||
private final String b64EncodedFileName;
|
||||
private final String b64EncodedHashRoot;
|
||||
private final List<String> b64EncodedHashList;
|
||||
|
||||
private volatile String comment;
|
||||
private final Set<String> downloaders = Collections.synchronizedSet(new HashSet<>());
|
||||
private final Set<SearchEntry> searches = Collections.synchronizedSet(new HashSet<>());
|
||||
private volatile boolean published;
|
||||
private volatile long publishedTimestamp;
|
||||
|
||||
public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException {
|
||||
public SharedFile(File file, byte[] root, int pieceSize) throws IOException {
|
||||
this.file = file;
|
||||
this.infoHash = infoHash;
|
||||
this.root = root;
|
||||
this.pieceSize = pieceSize;
|
||||
this.cachedPath = file.getAbsolutePath();
|
||||
this.cachedLength = file.length();
|
||||
this.b64EncodedFileName = Base64.encode(DataUtil.encodei18nString(file.toString()));
|
||||
this.b64EncodedHashRoot = Base64.encode(infoHash.getRoot());
|
||||
|
||||
List<String> b64List = new ArrayList<String>();
|
||||
byte[] tmp = new byte[32];
|
||||
for (int i = 0; i < infoHash.getHashList().length / 32; i++) {
|
||||
System.arraycopy(infoHash.getHashList(), i * 32, tmp, 0, 32);
|
||||
b64List.add(Base64.encode(tmp));
|
||||
}
|
||||
this.b64EncodedHashList = b64List;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public InfoHash getInfoHash() {
|
||||
return infoHash;
|
||||
public byte[] getPathHash() throws NoSuchAlgorithmException {
|
||||
MessageDigest digester = MessageDigest.getInstance("SHA-256");
|
||||
digester.update(file.getAbsolutePath().getBytes());
|
||||
return digester.digest();
|
||||
}
|
||||
|
||||
public String getB64PathHash() throws NoSuchAlgorithmException {
|
||||
if(b64PathHash == null){
|
||||
b64PathHash = Base64.encode(getPathHash());
|
||||
}
|
||||
return b64PathHash;
|
||||
}
|
||||
|
||||
public byte[] getRoot() {
|
||||
return root;
|
||||
}
|
||||
|
||||
public int getPieceSize() {
|
||||
@@ -73,14 +81,6 @@ public class SharedFile {
|
||||
return b64EncodedFileName;
|
||||
}
|
||||
|
||||
public String getB64EncodedHashRoot() {
|
||||
return b64EncodedHashRoot;
|
||||
}
|
||||
|
||||
public List<String> getB64EncodedHashList() {
|
||||
return b64EncodedHashList;
|
||||
}
|
||||
|
||||
public String getCachedPath() {
|
||||
return cachedPath;
|
||||
}
|
||||
@@ -116,10 +116,28 @@ public class SharedFile {
|
||||
public void addDownloader(String name) {
|
||||
downloaders.add(name);
|
||||
}
|
||||
|
||||
public void publish(long timestamp) {
|
||||
published = true;
|
||||
publishedTimestamp = timestamp;
|
||||
}
|
||||
|
||||
public void unpublish() {
|
||||
published = false;
|
||||
publishedTimestamp = 0;
|
||||
}
|
||||
|
||||
public boolean isPublished() {
|
||||
return published;
|
||||
}
|
||||
|
||||
public long getPublishedTimestamp() {
|
||||
return publishedTimestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return file.hashCode() ^ infoHash.hashCode();
|
||||
return file.hashCode() ^ Arrays.hashCode(root);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -127,7 +145,7 @@ public class SharedFile {
|
||||
if (!(o instanceof SharedFile))
|
||||
return false;
|
||||
SharedFile other = (SharedFile)o;
|
||||
return file.equals(other.file) && infoHash.equals(other.infoHash);
|
||||
return file.equals(other.file) && Arrays.equals(root, other.root);
|
||||
}
|
||||
|
||||
public static class SearchEntry {
|
||||
@@ -141,6 +159,18 @@ public class SharedFile {
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
public Persona getSearcher() {
|
||||
return searcher;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return Objects.hash(searcher) ^ Objects.hash(timestamp) ^ query.hashCode();
|
||||
}
|
||||
|
81
core/src/main/java/com/muwire/core/filefeeds/Feed.java
Normal file
81
core/src/main/java/com/muwire/core/filefeeds/Feed.java
Normal file
@@ -0,0 +1,81 @@
|
||||
package com.muwire.core.filefeeds;
|
||||
|
||||
import com.muwire.core.Persona;
|
||||
|
||||
public class Feed {
|
||||
|
||||
private final Persona publisher;
|
||||
|
||||
private int updateInterval;
|
||||
private long lastUpdated;
|
||||
private volatile long lastUpdateAttempt;
|
||||
private int itemsToKeep;
|
||||
private boolean autoDownload;
|
||||
private boolean sequential;
|
||||
private FeedFetchStatus status;
|
||||
|
||||
public Feed(Persona publisher) {
|
||||
this.publisher = publisher;
|
||||
this.status = FeedFetchStatus.IDLE;
|
||||
}
|
||||
|
||||
public int getUpdateInterval() {
|
||||
return updateInterval;
|
||||
}
|
||||
|
||||
public void setUpdateInterval(int updateInterval) {
|
||||
this.updateInterval = updateInterval;
|
||||
}
|
||||
|
||||
public long getLastUpdated() {
|
||||
return lastUpdated;
|
||||
}
|
||||
|
||||
public void setLastUpdated(long lastUpdated) {
|
||||
this.lastUpdated = lastUpdated;
|
||||
}
|
||||
|
||||
public int getItemsToKeep() {
|
||||
return itemsToKeep;
|
||||
}
|
||||
|
||||
public void setItemsToKeep(int itemsToKeep) {
|
||||
this.itemsToKeep = itemsToKeep;
|
||||
}
|
||||
|
||||
public boolean isAutoDownload() {
|
||||
return autoDownload;
|
||||
}
|
||||
|
||||
public void setAutoDownload(boolean autoDownload) {
|
||||
this.autoDownload = autoDownload;
|
||||
}
|
||||
|
||||
public Persona getPublisher() {
|
||||
return publisher;
|
||||
}
|
||||
|
||||
public void setStatus(FeedFetchStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public FeedFetchStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setSequential(boolean sequential) {
|
||||
this.sequential = sequential;
|
||||
}
|
||||
|
||||
public boolean isSequential() {
|
||||
return sequential;
|
||||
}
|
||||
|
||||
public void setLastUpdateAttempt(long lastUpdateAttempt) {
|
||||
this.lastUpdateAttempt = lastUpdateAttempt;
|
||||
}
|
||||
|
||||
public long getLastUpdateAttempt() {
|
||||
return lastUpdateAttempt;
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
package com.muwire.core.filefeeds;
|
||||
|
||||
public enum FeedFetchStatus {
|
||||
IDLE(false),
|
||||
CONNECTING(true),
|
||||
FETCHING(true),
|
||||
FINISHED(false),
|
||||
FAILED(false);
|
||||
|
||||
private final boolean active;
|
||||
|
||||
FeedFetchStatus(boolean active) {
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
}
|
79
core/src/main/java/com/muwire/core/filefeeds/FeedItem.java
Normal file
79
core/src/main/java/com/muwire/core/filefeeds/FeedItem.java
Normal file
@@ -0,0 +1,79 @@
|
||||
package com.muwire.core.filefeeds;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import com.muwire.core.InfoHash;
|
||||
import com.muwire.core.Persona;
|
||||
|
||||
public class FeedItem {
|
||||
|
||||
private final Persona publisher;
|
||||
private final long timestamp;
|
||||
private final String name;
|
||||
private final long size;
|
||||
private final int pieceSize;
|
||||
private final InfoHash infoHash;
|
||||
private final int certificates;
|
||||
private final String comment;
|
||||
|
||||
public FeedItem(Persona publisher, long timestamp, String name, long size, int pieceSize, InfoHash infoHash,
|
||||
int certificates, String comment) {
|
||||
super();
|
||||
this.publisher = publisher;
|
||||
this.timestamp = timestamp;
|
||||
this.name = name;
|
||||
this.size = size;
|
||||
this.pieceSize = pieceSize;
|
||||
this.infoHash = infoHash;
|
||||
this.certificates = certificates;
|
||||
this.comment = comment;
|
||||
}
|
||||
|
||||
public Persona getPublisher() {
|
||||
return publisher;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public int getPieceSize() {
|
||||
return pieceSize;
|
||||
}
|
||||
|
||||
public InfoHash getInfoHash() {
|
||||
return infoHash;
|
||||
}
|
||||
|
||||
public int getCertificates() {
|
||||
return certificates;
|
||||
}
|
||||
|
||||
public String getComment() {
|
||||
return comment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(publisher, timestamp, name, infoHash);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof FeedItem))
|
||||
return false;
|
||||
FeedItem other = (FeedItem)o;
|
||||
return Objects.equals(publisher, other.publisher) &&
|
||||
timestamp == other.timestamp &&
|
||||
Objects.equals(name, other.name) &&
|
||||
Objects.equals(infoHash, other.infoHash);
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package com.muwire.core.filefeeds;
|
||||
|
||||
public class InvalidFeedItemException extends Exception {
|
||||
|
||||
public InvalidFeedItemException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public InvalidFeedItemException(String message, Throwable cause, boolean enableSuppression,
|
||||
boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidFeedItemException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidFeedItemException(String message) {
|
||||
super(message);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidFeedItemException(Throwable cause) {
|
||||
super(cause);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
}
|
@@ -58,12 +58,12 @@ public class DataUtil {
|
||||
if (header.length != 3)
|
||||
throw new IllegalArgumentException("header length $header.length");
|
||||
|
||||
return (((int)(header[0] & 0x7F)) << 16) |
|
||||
(((int)(header[1] & 0xFF) << 8)) |
|
||||
((int)header[2] & 0xFF);
|
||||
return ((header[0] & 0x7F) << 16) |
|
||||
((header[1] & 0xFF) << 8) |
|
||||
(header[2] & 0xFF);
|
||||
}
|
||||
|
||||
static String readi18nString(byte [] encoded) {
|
||||
public static String readi18nString(byte [] encoded) {
|
||||
if (encoded.length < 2)
|
||||
throw new IllegalArgumentException("encoding too short $encoded.length");
|
||||
int length = ((encoded[0] & 0xFF) << 8) | (encoded[1] & 0xFF);
|
||||
@@ -174,7 +174,7 @@ public class DataUtil {
|
||||
clean.setAccessible(true);
|
||||
clean.invoke(cleaner.invoke(cb));
|
||||
} else {
|
||||
Class unsafeClass;
|
||||
Class<?> unsafeClass;
|
||||
try {
|
||||
unsafeClass = Class.forName("sun.misc.Unsafe");
|
||||
} catch(Exception ex) {
|
||||
@@ -216,4 +216,13 @@ public class DataUtil {
|
||||
Signature sig = DSAEngine.getInstance().sign(payload, spk);
|
||||
return sig.getData();
|
||||
}
|
||||
|
||||
public static boolean isValidName(String name) {
|
||||
if (name.length() > Constants.MAX_NICKNAME_LENGTH)
|
||||
return false;
|
||||
for (int i = 0; i < Constants.INVALID_NICKNAME_CHARS.length(); i++)
|
||||
if (name.indexOf(Constants.INVALID_NICKNAME_CHARS.charAt(i)) >= 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
8
core/src/main/resources/defaults/i2p.properties
Normal file
8
core/src/main/resources/defaults/i2p.properties
Normal file
@@ -0,0 +1,8 @@
|
||||
inbound.nickname=MuWire
|
||||
outbound.nickname=MuWire
|
||||
inbound.length=3
|
||||
inbound.quantity=4
|
||||
outbound.length=3
|
||||
outbound.quantity=4
|
||||
i2cp.tcp.host=127.0.0.1
|
||||
i2cp.tcp.port=7654
|
@@ -95,7 +95,7 @@ class ConnectionAcceptorTest {
|
||||
connectionEstablisher = connectionEstablisherMock.proxyInstance()
|
||||
|
||||
acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor,
|
||||
hostCache, trustService, searchManager, uploadManager, null, connectionEstablisher, null)
|
||||
hostCache, trustService, searchManager, uploadManager, null, connectionEstablisher, null, null)
|
||||
acceptor.start()
|
||||
Thread.sleep(100)
|
||||
}
|
||||
|
@@ -39,13 +39,13 @@ class FileManagerTest {
|
||||
@Test
|
||||
void testHash1Result() {
|
||||
File f = new File("a b.c")
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf = new SharedFile(f,ih, 0)
|
||||
byte [] root = new byte[32]
|
||||
SharedFile sf = new SharedFile(f,root, 0)
|
||||
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
||||
manager.onFileHashedEvent(fhe)
|
||||
|
||||
UUID uuid = UUID.randomUUID()
|
||||
SearchEvent se = new SearchEvent(searchHash: ih.getRoot(), uuid: uuid)
|
||||
SearchEvent se = new SearchEvent(searchHash: root, uuid: uuid)
|
||||
|
||||
manager.onSearchEvent(se)
|
||||
Thread.sleep(20)
|
||||
@@ -58,14 +58,14 @@ class FileManagerTest {
|
||||
|
||||
@Test
|
||||
void testHash2Results() {
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
|
||||
SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
|
||||
byte [] root = new byte[32]
|
||||
SharedFile sf1 = new SharedFile(new File("a b.c"), root, 0)
|
||||
SharedFile sf2 = new SharedFile(new File("d e.f"), root, 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
|
||||
|
||||
UUID uuid = UUID.randomUUID()
|
||||
SearchEvent se = new SearchEvent(searchHash: ih.getRoot(), uuid: uuid)
|
||||
SearchEvent se = new SearchEvent(searchHash: root, uuid: uuid)
|
||||
|
||||
manager.onSearchEvent(se)
|
||||
Thread.sleep(20)
|
||||
@@ -81,7 +81,7 @@ class FileManagerTest {
|
||||
void testHash0Results() {
|
||||
File f = new File("a b.c")
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf = new SharedFile(f,ih, 0)
|
||||
SharedFile sf = new SharedFile(f,ih.getRoot(), 0)
|
||||
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
||||
manager.onFileHashedEvent(fhe)
|
||||
|
||||
@@ -95,7 +95,7 @@ class FileManagerTest {
|
||||
void testKeyword1Result() {
|
||||
File f = new File("a b.c")
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf = new SharedFile(f,ih,0)
|
||||
SharedFile sf = new SharedFile(f,ih.getRoot(),0)
|
||||
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
||||
manager.onFileHashedEvent(fhe)
|
||||
|
||||
@@ -113,12 +113,12 @@ class FileManagerTest {
|
||||
void testKeyword2Results() {
|
||||
File f1 = new File("a b.c")
|
||||
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(f1, ih1, 0)
|
||||
SharedFile sf1 = new SharedFile(f1, ih1.getRoot(), 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1)
|
||||
|
||||
File f2 = new File("c d.e")
|
||||
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
|
||||
SharedFile sf2 = new SharedFile(f2, ih2, 0)
|
||||
SharedFile sf2 = new SharedFile(f2, ih2.getRoot(), 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
|
||||
|
||||
UUID uuid = UUID.randomUUID()
|
||||
@@ -136,7 +136,7 @@ class FileManagerTest {
|
||||
void testKeyword0Results() {
|
||||
File f = new File("a b.c")
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf = new SharedFile(f,ih,0)
|
||||
SharedFile sf = new SharedFile(f,ih.getRoot(),0)
|
||||
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
||||
manager.onFileHashedEvent(fhe)
|
||||
|
||||
@@ -149,8 +149,8 @@ class FileManagerTest {
|
||||
@Test
|
||||
void testRemoveFileExistingHash() {
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
|
||||
SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
|
||||
SharedFile sf1 = new SharedFile(new File("a b.c"), ih.getRoot(), 0)
|
||||
SharedFile sf2 = new SharedFile(new File("d e.f"), ih.getRoot(), 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
|
||||
|
||||
@@ -167,12 +167,12 @@ class FileManagerTest {
|
||||
void testRemoveFile() {
|
||||
File f1 = new File("a b.c")
|
||||
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(f1, ih1, 0)
|
||||
SharedFile sf1 = new SharedFile(f1, ih1.getRoot(), 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1)
|
||||
|
||||
File f2 = new File("c d.e")
|
||||
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
|
||||
SharedFile sf2 = new SharedFile(f2, ih2, 0)
|
||||
SharedFile sf2 = new SharedFile(f2, ih2.getRoot(), 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
|
||||
|
||||
manager.onFileUnsharedEvent new FileUnsharedEvent(deleted : true, unsharedFile: sf2)
|
||||
@@ -198,7 +198,7 @@ class FileManagerTest {
|
||||
comment = Base64.encode(DataUtil.encodei18nString(comment))
|
||||
File f1 = new File("MuWire-0.5.10.AppImage")
|
||||
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(f1, ih1, 0)
|
||||
SharedFile sf1 = new SharedFile(f1, ih1.getRoot(), 0)
|
||||
sf1.setComment(comment)
|
||||
|
||||
manager.onFileLoadedEvent(new FileLoadedEvent(loadedFile : sf1))
|
||||
@@ -206,7 +206,7 @@ class FileManagerTest {
|
||||
|
||||
File f2 = new File("MuWire-0.6.0.AppImage")
|
||||
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
|
||||
SharedFile sf2 = new SharedFile(f2, ih2, 0)
|
||||
SharedFile sf2 = new SharedFile(f2, ih2.getRoot(), 0)
|
||||
sf2.setComment(comment)
|
||||
|
||||
manager.onFileLoadedEvent(new FileLoadedEvent(loadedFile : sf2))
|
||||
|
@@ -10,8 +10,8 @@ class FileTreeTest {
|
||||
File b = new File(a, "b")
|
||||
File c = new File(b, "c")
|
||||
|
||||
FileTree tree = new FileTree()
|
||||
tree.add(c)
|
||||
FileTree<Void> tree = new FileTree<>()
|
||||
tree.add(c,null)
|
||||
|
||||
assert tree.root.children.size() == 1
|
||||
assert tree.fileToNode.size() == 3
|
||||
@@ -28,15 +28,110 @@ class FileTreeTest {
|
||||
File c = new File(b, "c")
|
||||
File d = new File(b, "d")
|
||||
|
||||
FileTree tree = new FileTree()
|
||||
tree.add(c)
|
||||
FileTree<Void> tree = new FileTree<>()
|
||||
tree.add(c,null)
|
||||
|
||||
assert tree.fileToNode.size() == 3
|
||||
|
||||
tree.add(d)
|
||||
tree.add(d, null)
|
||||
assert tree.fileToNode.size() == 4
|
||||
|
||||
tree.remove(d)
|
||||
assert tree.fileToNode.size() == 3
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTraverse() {
|
||||
Stack stack = new Stack()
|
||||
Set<String> values = new HashSet<>()
|
||||
StringBuilder sb = new StringBuilder()
|
||||
def cb = new FileTreeCallback<String>() {
|
||||
|
||||
@Override
|
||||
public void onDirectoryEnter(File file) {
|
||||
stack.push(file)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDirectoryLeave() {
|
||||
stack.pop()
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFile(File file, String value) {
|
||||
values.add(value)
|
||||
}
|
||||
}
|
||||
|
||||
File a = new File("a")
|
||||
a.createNewFile()
|
||||
File b = new File("b")
|
||||
b.mkdir()
|
||||
File c = new File(b, "c")
|
||||
c.createNewFile()
|
||||
File d = new File(b, "d")
|
||||
d.mkdir()
|
||||
File e = new File(d, "e")
|
||||
e.createNewFile()
|
||||
FileTree<String> tree = new FileTree<>()
|
||||
|
||||
tree.add(a, "a")
|
||||
tree.add(b, "b")
|
||||
tree.add(c, "c")
|
||||
tree.add(d, "d")
|
||||
tree.add(e, "e")
|
||||
|
||||
tree.traverse(cb)
|
||||
|
||||
assert stack.isEmpty()
|
||||
assert values.size() == 3
|
||||
assert values.contains("a")
|
||||
assert values.contains("c")
|
||||
assert values.contains("e")
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testList() {
|
||||
Set<File> directories = new HashSet<>()
|
||||
Set<String> values = new HashSet<>()
|
||||
def cb = new FileListCallback<String>() {
|
||||
|
||||
@Override
|
||||
public void onDirectory(File file) {
|
||||
directories.add(file)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFile(File file, String value) {
|
||||
values.add(value)
|
||||
}
|
||||
}
|
||||
|
||||
File a = new File("a")
|
||||
a.createNewFile()
|
||||
File b = new File("b")
|
||||
b.mkdir()
|
||||
File c = new File(b, "c")
|
||||
c.createNewFile()
|
||||
|
||||
FileTree<String> tree = new FileTree<>()
|
||||
|
||||
tree.add(a, "a")
|
||||
tree.add(b, "b")
|
||||
tree.add(c, "c")
|
||||
|
||||
tree.list(null, cb)
|
||||
|
||||
assert directories.size() == 1
|
||||
assert directories.contains(b)
|
||||
assert values.size() == 1
|
||||
assert values.contains("a")
|
||||
|
||||
directories.clear()
|
||||
values.clear()
|
||||
tree.list(b, cb)
|
||||
assert directories.isEmpty()
|
||||
assert values.size() == 1
|
||||
assert values.contains("c")
|
||||
}
|
||||
}
|
||||
|
@@ -45,7 +45,7 @@ class HasherServiceTest {
|
||||
def hashed = listener.poll()
|
||||
assert hashed instanceof FileHashedEvent
|
||||
assert hashed.sharedFile.file == f.getCanonicalFile()
|
||||
assert hashed.sharedFile.infoHash != null
|
||||
assert hashed.sharedFile.root != null
|
||||
assert listener.isEmpty()
|
||||
}
|
||||
|
||||
|
@@ -85,7 +85,7 @@ class PersisterServiceLoadingTest {
|
||||
def loadedFile = listener.publishedFiles[0]
|
||||
assert loadedFile != null
|
||||
assert loadedFile.file == sharedFile1.getCanonicalFile()
|
||||
assert loadedFile.infoHash == ih1
|
||||
assert loadedFile.root == ih1.getRoot()
|
||||
}
|
||||
|
||||
private static String getSharedFileJsonName(File sharedFile) {
|
||||
@@ -128,7 +128,7 @@ class PersisterServiceLoadingTest {
|
||||
def loadedFile = listener.publishedFiles[0]
|
||||
assert loadedFile != null
|
||||
assert loadedFile.file == sharedFile1.getCanonicalFile()
|
||||
assert loadedFile.infoHash == ih1
|
||||
assert loadedFile.root == ih1.getRoot()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -169,10 +169,10 @@ class PersisterServiceLoadingTest {
|
||||
assert listener.publishedFiles.size() == 2
|
||||
def loadedFile1 = listener.publishedFiles[0]
|
||||
assert loadedFile1.file == sharedFile1.getCanonicalFile()
|
||||
assert loadedFile1.infoHash == ih1
|
||||
assert loadedFile1.root == ih1.getRoot()
|
||||
def loadedFile2 = listener.publishedFiles[1]
|
||||
assert loadedFile2.file == sharedFile2.getCanonicalFile()
|
||||
assert loadedFile2.infoHash == ih2
|
||||
assert loadedFile2.root == ih2.getRoot()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@@ -2,6 +2,7 @@ package com.muwire.core.files
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
import com.muwire.core.Destinations
|
||||
@@ -16,6 +17,7 @@ import groovy.json.JsonSlurper
|
||||
import net.i2p.data.Base32
|
||||
import net.i2p.data.Base64
|
||||
|
||||
@Ignore
|
||||
class PersisterServiceSavingTest {
|
||||
|
||||
File f
|
||||
|
49
doc/collections.md
Normal file
49
doc/collections.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# MuWire Collections
|
||||
Status: Draft, Proposal, Unimplemented
|
||||
|
||||
MuWire collections are files containing meta-information about a grouping of files. They serve a similar purpose like .torrent files, but the internal format is rather different to account for the MuWire identity management.
|
||||
|
||||
A user wishing to create a collection of files needs to have shared all the files that are going to be part of the collection. Their full MuWire ID will be stored in the collection, so anyone wishing to download any of the files in the collection will try to download from them first.
|
||||
|
||||
The collection will be signed, so anyone can verify that the embedded full MuWire ID authored the collection.
|
||||
|
||||
### File Format
|
||||
|
||||
Header:
|
||||
|
||||
```
|
||||
byte 0: Collection version, currently fixed at "1".
|
||||
bytes 1,2 : unsigned 16-bit value of the number of files in the collection. Empty files or directories are not allowed.
|
||||
bytes 3-N: Full MuWire ID of the publisher of the collection, in Persona format.
|
||||
bytes N+1 to N+9: Timestamp of the collection, in milliseconds since epoch UTC
|
||||
bytes N+9 to M: Free-form description of the collection (comment). Format is UTF-8, maximum size is 32kb.
|
||||
```
|
||||
|
||||
The header is followed by a file entry for each file in the collection. The format is the follows:
|
||||
```
|
||||
byte 0: File entry version, currently fixed at "1".
|
||||
byte 1-33: hash of the file
|
||||
byte 34: Unsigned 8-bit number of path elements from root to where the file will ultimately be placed upon download.
|
||||
bytes 35-N : UTF-8 encoded length-prefixed path elements. Each element can be at most 32kb long. The last element is the name of the file.
|
||||
bytes N-M: free from description of the file (comment). Format is UTF-8, maximum size is 32kb.
|
||||
```
|
||||
|
||||
After the file entries follows a footer, which is simply a signature of the byte payload of the header and the file entries.
|
||||
|
||||
### Downloading
|
||||
|
||||
Since the collection is created from individual shared files, every file within the collection is searchable. It is possible to extend the shared file data structure to contain refererences to any collections the file belongs to - TBD.
|
||||
|
||||
When a user searches for a keyword or hash, they can find either the collection metafile itself or a file which is a member of one or more collections. In the latter case, the user is given the option to download the collection metafile.
|
||||
|
||||
If the user chooses to download the collection metafile, they will be presented with a dialog containing the metainformation contained in the collection descriptor. They will be able to see directory structure contained in the collection and will be able to choose individual files to download.
|
||||
|
||||
TBD - what happens when some of the files are already downloaded but are not in the final directory location?
|
||||
|
||||
Finally, when starting the download, the downloader always queries the persona in the collection first, regardless of who returned the search result.
|
||||
|
||||
### Sharing
|
||||
|
||||
When downloading the collection descriptor, the user makes the descriptor available for indexing. This way collection descriptors can propagate on the network.
|
||||
TBD - do they also index the comments and file names in the descriptor, even if they haven't downloaded the files?
|
||||
|
26
docker/rootfs/etc/cont-init.d/00-app-user-map.sh
Executable file
26
docker/rootfs/etc/cont-init.d/00-app-user-map.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/with-contenv sh
|
||||
|
||||
#
|
||||
# Add the app user to the password and group databases. This is needed just to
|
||||
# make sure that mapping between the user/group ID and its name is possible.
|
||||
#
|
||||
|
||||
set -e # Exit immediately if a command exits with a non-zero status.
|
||||
set -u # Treat unset variables as an error.
|
||||
|
||||
cp /defaults/passwd /etc/passwd
|
||||
cp /defaults/group /etc/group
|
||||
cp /defaults/shadow /etc/shadow
|
||||
chown root:shadow /etc/shadow
|
||||
chmod 640 /etc/shadow
|
||||
|
||||
echo "$APP_USER:x:$USER_ID:$GROUP_ID::${APP_HOME:-/dev/null}:/sbin/nologin" >> /etc/passwd
|
||||
echo "$APP_USER:x:$GROUP_ID:" >> /etc/group
|
||||
|
||||
# Make sure APP_HOME is editable by the user
|
||||
if [[ -n "$APP_HOME" ]] ; then
|
||||
chown -R "$APP_USER" "$APP_HOME"
|
||||
chmod -R u+rw "$APP_HOME"
|
||||
fi
|
||||
|
||||
# vim:ft=sh:ts=4:sw=4:et:sts=4
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user