Compare commits
2967 commits
v0.5.2
...
py3-latest
Author | SHA1 | Date | |
---|---|---|---|
7edbda70f5 | |||
![]() |
290025958f | ||
![]() |
25c5658b72 | ||
![]() |
2970e3a205 | ||
![]() |
866179f6a3 | ||
![]() |
e8cf14bcf5 | ||
![]() |
fedcf9c1c6 | ||
![]() |
117bcf25d9 | ||
![]() |
a429349cd4 | ||
![]() |
d8e52eaabd | ||
![]() |
f2ef6e5d9c | ||
![]() |
dd2bb07cfb | ||
![]() |
06a9d1e0ff | ||
![]() |
c354f9e24d | ||
![]() |
77b4297224 | ||
![]() |
edc5310cd2 | ||
![]() |
99a8409513 | ||
![]() |
3550a64837 | ||
![]() |
85ef28e6fb | ||
![]() |
1500d9356b | ||
![]() |
f1a71770fa | ||
![]() |
f79a73cef4 | ||
![]() |
0731787518 | ||
![]() |
ad95eede10 | ||
![]() |
459b0a73ca | ||
![]() |
b7870edd2e | ||
![]() |
d5703541be | ||
![]() |
ba96654e1d | ||
![]() |
ac72d623f0 | ||
![]() |
fd857985f6 | ||
![]() |
966f671efe | ||
![]() |
86109ae4b2 | ||
![]() |
611fc774c8 | ||
![]() |
0ed0b746a4 | ||
![]() |
49e68c3a78 | ||
![]() |
3ac677c9a7 | ||
![]() |
016cfe9e16 | ||
![]() |
712ee18634 | ||
![]() |
5579c6b3cc | ||
![]() |
c3815c56ea | ||
![]() |
b257338b0a | ||
![]() |
ac70f83879 | ||
![]() |
2ad80afa10 | ||
![]() |
fe048cd08c | ||
![]() |
f9d7ccd83c | ||
![]() |
b29884db78 | ||
![]() |
a5190234ab | ||
![]() |
00db9c9f87 | ||
![]() |
02ceb70a4f | ||
![]() |
7ce118d645 | ||
![]() |
eb397cf4c7 | ||
![]() |
f8c9f2da4f | ||
![]() |
69d7eacfa4 | ||
![]() |
f498aedb96 | ||
![]() |
5ee928852b | ||
![]() |
7078badefa | ||
![]() |
edd2760fed | ||
![]() |
7d1ec41d09 | ||
![]() |
7acd8df906 | ||
![]() |
a1eb6eede5 | ||
![]() |
eab7fc2be4 | ||
![]() |
454c0b2e7e | ||
![]() |
03da34c5d6 | ||
![]() |
c3f4591f91 | ||
![]() |
3ad7bc87e5 | ||
![]() |
b4f4c12521 | ||
![]() |
39d86fec9c | ||
![]() |
02c27b841f | ||
![]() |
8dafbef6ad | ||
![]() |
c831d175ad | ||
![]() |
3cbfbae42d | ||
![]() |
a1105562cd | ||
![]() |
b3c9de5e47 | ||
![]() |
3ffa3c2f79 | ||
![]() |
03273527da | ||
![]() |
2795e20b0c | ||
![]() |
eb86df5fb6 | ||
![]() |
ecfb6b6b3a | ||
![]() |
1b4f93f14b | ||
![]() |
40db30a260 | ||
![]() |
3a4a5404c0 | ||
![]() |
2bb12a247b | ||
![]() |
38a3ea6373 | ||
![]() |
2798ad6fb2 | ||
![]() |
2e7b0071a3 | ||
![]() |
12e82bc9c4 | ||
![]() |
774691fa39 | ||
![]() |
52d6c9fedf | ||
![]() |
d68c635e9a | ||
![]() |
21557b5517 | ||
![]() |
11fe0ece67 | ||
![]() |
92363d5227 | ||
![]() |
cf0c5db5b9 | ||
![]() |
57dda4e6d6 | ||
![]() |
0a3bf43e1c | ||
![]() |
48455e3e45 | ||
![]() |
c515e26cd6 | ||
![]() |
5cac059ef4 | ||
![]() |
8f6e27904c | ||
![]() |
e757f2a2d4 | ||
![]() |
b104d5dd41 | ||
![]() |
f6106be733 | ||
![]() |
9305a2e7ac | ||
![]() |
4f6833c488 | ||
![]() |
e2b1cf3938 | ||
![]() |
6fcfe5b394 | ||
![]() |
12013d64c8 | ||
![]() |
54fb2fde7c | ||
![]() |
147dd4bc35 | ||
![]() |
32c9d5fa70 | ||
![]() |
dd08b89c81 | ||
![]() |
6770b450b3 | ||
![]() |
9b2772b171 | ||
![]() |
29dac8a188 | ||
![]() |
352da6bf62 | ||
![]() |
288050e5b4 | ||
![]() |
785d2351eb | ||
![]() |
9d1bed11af | ||
![]() |
b2342e64bd | ||
![]() |
dcbfb8afe0 | ||
![]() |
4b8dfc5114 | ||
![]() |
c0baf8b68d | ||
![]() |
73dc69605b | ||
![]() |
52ed8c18ca | ||
![]() |
19bc0358b5 | ||
![]() |
392350ff79 | ||
![]() |
f0b0f57643 | ||
![]() |
85790f8866 | ||
![]() |
ce5b4c3eda | ||
![]() |
fde3b51129 | ||
![]() |
550d02d473 | ||
![]() |
4da89580c1 | ||
![]() |
bf092b83ab | ||
![]() |
0309b81695 | ||
![]() |
e74fdc4036 | ||
![]() |
b9c65d75ef | ||
![]() |
49f8e0bc3a | ||
![]() |
c4f8c0177e | ||
![]() |
8c20927f68 | ||
![]() |
5b09f7af41 | ||
![]() |
1695571afa | ||
![]() |
8dc5aee8aa | ||
![]() |
94765af0f3 | ||
![]() |
5a226baaa5 | ||
![]() |
b7bc197012 | ||
![]() |
a0dfbe31f6 | ||
![]() |
964545dd1f | ||
![]() |
817ab04941 | ||
![]() |
91d0ce3a50 | ||
![]() |
e97236201c | ||
![]() |
e14f5bf847 | ||
![]() |
79f10ffe0c | ||
![]() |
0bc9374a7d | ||
![]() |
8a71bf65cd | ||
![]() |
9d198ff7f2 | ||
![]() |
cafeebf120 | ||
![]() |
46fba195da | ||
![]() |
501bd51bd1 | ||
![]() |
f7874e1ca3 | ||
![]() |
8d964d1b8e | ||
![]() |
051e404a80 | ||
![]() |
6c1abf4004 | ||
![]() |
0907edb6b1 | ||
![]() |
6ff14d1bbd | ||
![]() |
4ad5c065f1 | ||
![]() |
c17b8d53d3 | ||
![]() |
9022a1098a | ||
![]() |
6e758ff363 | ||
![]() |
29c3523353 | ||
![]() |
47ff6c6801 | ||
![]() |
6bd49e8aff | ||
![]() |
ddbd5c7b19 | ||
![]() |
635c3b27cd | ||
![]() |
6776dabdb3 | ||
![]() |
14cbaf47c8 | ||
![]() |
4eb50377c3 | ||
![]() |
ea6016d004 | ||
![]() |
79d26060b3 | ||
![]() |
97ad084c21 | ||
![]() |
179e5cb651 | ||
![]() |
367745b5ea | ||
![]() |
5c38a78b79 | ||
![]() |
a02ed56c69 | ||
![]() |
f868fed51d | ||
![]() |
e4f42b8ce3 | ||
![]() |
eeb48fc72e | ||
![]() |
85733abade | ||
![]() |
8db4344171 | ||
![]() |
0a9391d28b | ||
![]() |
cfef7ab071 | ||
![]() |
38c1727b94 | ||
![]() |
36d96d484e | ||
![]() |
439f8fc476 | ||
![]() |
07faa3d6d3 | ||
![]() |
3c7022ea9d | ||
![]() |
a657afcd47 | ||
![]() |
f3a839f422 | ||
![]() |
ad3920b26a | ||
![]() |
8ffd8d7a3e | ||
![]() |
71001491df | ||
![]() |
701765b53b | ||
![]() |
fa880d99f1 | ||
![]() |
0a9a9b5a57 | ||
![]() |
56acac8cd3 | ||
![]() |
995d3bf717 | ||
![]() |
1de7485858 | ||
![]() |
e1c0fd6984 | ||
![]() |
108a3de433 | ||
![]() |
740fe65355 | ||
![]() |
abde3d4cf7 | ||
![]() |
c90c887f8f | ||
![]() |
a4d91f7081 | ||
![]() |
31d4304915 | ||
![]() |
1eec388252 | ||
![]() |
70de3213d6 | ||
![]() |
f41d022038 | ||
![]() |
723d1f4370 | ||
![]() |
ca94703fc3 | ||
![]() |
a5971adbe6 | ||
![]() |
dfeebbabe8 | ||
![]() |
66194ce435 | ||
![]() |
2de3c9a544 | ||
![]() |
5fb342a825 | ||
![]() |
3156d2f94b | ||
![]() |
ba156bbdec | ||
![]() |
6beb76eac8 | ||
![]() |
d3d18234df | ||
![]() |
faa24a8b41 | ||
![]() |
f749228a2c | ||
![]() |
7e17a4e967 | ||
![]() |
19f003141b | ||
![]() |
53a6063576 | ||
![]() |
33af83b2cd | ||
![]() |
3426d5fe63 | ||
![]() |
f2934c10b4 | ||
![]() |
a2457b2488 | ||
![]() |
193632c3f9 | ||
![]() |
a1c176bb3f | ||
![]() |
02fd1dc4d0 | ||
![]() |
296e4aab57 | ||
![]() |
7ba2c9344d | ||
![]() |
09e65e1d95 | ||
![]() |
c4f65a5d7b | ||
![]() |
37a401fdef | ||
![]() |
e7d1e1f097 | ||
![]() |
6df3036f11 | ||
![]() |
e2a582d892 | ||
![]() |
aaabcb6b1a | ||
![]() |
7bf790003e | ||
![]() |
f46b945cdc | ||
![]() |
27761c5045 | ||
![]() |
e0bf4dc9ec | ||
![]() |
1fc67a3d71 | ||
![]() |
5baacf963d | ||
![]() |
b790bcac9b | ||
![]() |
219b90668f | ||
![]() |
2862587c15 | ||
![]() |
6218a92895 | ||
![]() |
58f03e21ef | ||
![]() |
b85477787d | ||
![]() |
6a1235bd45 | ||
![]() |
33d6a9c402 | ||
![]() |
17f65a5179 | ||
![]() |
f8e2cbe429 | ||
![]() |
f0a706f6ab | ||
![]() |
8b994e42c2 | ||
![]() |
ae9a76a6c9 | ||
![]() |
9b85d8638d | ||
![]() |
a9c75a3146 | ||
![]() |
1cc0ec3f31 | ||
![]() |
b1819ff71d | ||
![]() |
fca1033f83 | ||
![]() |
32855d0479 | ||
![]() |
2c826eba2d | ||
![]() |
8facd9ff84 | ||
![]() |
64e5e0c80e | ||
![]() |
8aa4e27938 | ||
![]() |
bc76bf291a | ||
![]() |
70cc982e2e | ||
![]() |
61ac6a30d3 | ||
![]() |
d2627f36d5 | ||
![]() |
d36324e0d3 | ||
![]() |
113b57415f | ||
![]() |
fefd2474b1 | ||
![]() |
28ce08de8e | ||
![]() |
037f0a3ff4 | ||
![]() |
a3546d56b0 | ||
![]() |
95bf4ecb42 | ||
![]() |
c91f2f0a09 | ||
![]() |
6d425f30fe | ||
![]() |
8e79a7da63 | ||
![]() |
10c02c31c2 | ||
![]() |
a0f5e1bde8 | ||
![]() |
2e9cff928c | ||
![]() |
46210b2f04 | ||
![]() |
6dae187e22 | ||
![]() |
a7e783a26b | ||
![]() |
60af3ceda9 | ||
![]() |
11415fe082 | ||
![]() |
df93fa0ffe | ||
![]() |
849d514f28 | ||
![]() |
4d8ee4bafb | ||
![]() |
ac8aaaff75 | ||
![]() |
238ede9419 | ||
![]() |
835174270e | ||
![]() |
62a2ec7254 | ||
![]() |
a9368bb3c8 | ||
![]() |
e75e199334 | ||
![]() |
2b7aebd89d | ||
![]() |
3e08eabc86 | ||
![]() |
a16d55c863 | ||
![]() |
e51ae580b9 | ||
![]() |
914576b9db | ||
![]() |
3edb34ec56 | ||
![]() |
224093b3dd | ||
![]() |
77c3e43978 | ||
![]() |
03350d7454 | ||
![]() |
2b5e57e840 | ||
![]() |
39442977db | ||
![]() |
0af90aad37 | ||
![]() |
c5d51c9cab | ||
![]() |
0dbcec8092 | ||
![]() |
76e4b75c2d | ||
![]() |
c1ad7914f1 | ||
![]() |
9085a4b0cc | ||
![]() |
820346c98d | ||
![]() |
995d87c167 | ||
![]() |
fe739fa848 | ||
![]() |
b6d0bf8f6b | ||
![]() |
aec1ab4ed2 | ||
![]() |
7d5f3354b6 | ||
![]() |
feb58e4b0e | ||
![]() |
163825c03e | ||
![]() |
3fc80f834d | ||
![]() |
20b0db7ddb | ||
![]() |
b2e7cbb927 | ||
![]() |
5987274edf | ||
![]() |
ba218974c4 | ||
![]() |
721d4a22f1 | ||
![]() |
32b0153d34 | ||
![]() |
71d32d7414 | ||
![]() |
796ee572ce | ||
![]() |
60146a083c | ||
![]() |
df87bd41b4 | ||
![]() |
3d73599deb | ||
![]() |
48124e12d9 | ||
![]() |
17fb740c51 | ||
![]() |
c6b07f1294 | ||
![]() |
3ccce46314 | ||
![]() |
7c1da5da52 | ||
![]() |
c5de1447c8 | ||
![]() |
e16ace433c | ||
![]() |
975f53b95b | ||
![]() |
8a994b5559 | ||
![]() |
2acf24c336 | ||
![]() |
2c3f1ba7ad | ||
![]() |
c01245a4e0 | ||
![]() |
f119f7d0d2 | ||
![]() |
62d4edadf6 | ||
![]() |
8bf17d3a69 | ||
![]() |
0881e274a9 | ||
![]() |
7ca09ba75b | ||
![]() |
bde8b30d5c | ||
![]() |
87d1c736e2 | ||
![]() |
eba81cc7d2 | ||
![]() |
69eb831c7e | ||
![]() |
99e6326974 | ||
![]() |
50bbe47bf2 | ||
![]() |
8bfef12ad4 | ||
![]() |
6085cfd1a7 | ||
![]() |
d660a268e8 | ||
![]() |
c161140a90 | ||
![]() |
7af8d1cd93 | ||
![]() |
845b50915d | ||
![]() |
dbbad3097c | ||
![]() |
1abaa6fddc | ||
![]() |
7ecf09a496 | ||
![]() |
c0639fef75 | ||
![]() |
c08d266822 | ||
![]() |
6bc3c168c6 | ||
![]() |
1fe7127082 | ||
![]() |
cfaaaf57ec | ||
![]() |
93d2ee65fe | ||
![]() |
9c08e41b9e | ||
![]() |
abee87bbec | ||
![]() |
d4b6f79746 | ||
![]() |
a7c26f893f | ||
![]() |
24b8cdf87a | ||
![]() |
fd43aa61ef | ||
![]() |
77869830c5 | ||
![]() |
87fc8ced5e | ||
![]() |
909967629b | ||
![]() |
afe0d82f18 | ||
![]() |
1ad97a6696 | ||
![]() |
e7e8e59c1e | ||
![]() |
f3665b172f | ||
![]() |
23b3cd3986 | ||
![]() |
f7ee6744af | ||
![]() |
ac45217816 | ||
![]() |
8c51e81a0b | ||
![]() |
9d777951dd | ||
![]() |
2778b17f8d | ||
![]() |
98c98fbac7 | ||
![]() |
9b1f6337c3 | ||
![]() |
2019093431 | ||
![]() |
eac25caf28 | ||
![]() |
b421893434 | ||
![]() |
f1b19f5fc7 | ||
![]() |
61f1a741fc | ||
![]() |
f01d335835 | ||
![]() |
5c1b34387c | ||
![]() |
dfd55c3957 | ||
![]() |
b21895fa78 | ||
![]() |
495d695c5a | ||
![]() |
3309489c24 | ||
![]() |
8a5a75e68f | ||
![]() |
c1df78b97f | ||
![]() |
4c31aae97b | ||
![]() |
0839fdfc5e | ||
![]() |
d062f01127 | ||
![]() |
e91fb90a45 | ||
![]() |
6539ca5eb0 | ||
![]() |
b138ebc519 | ||
![]() |
79c1cd15ab | ||
![]() |
10c1986c54 | ||
![]() |
d7cabb47ca | ||
![]() |
8de1714f08 | ||
![]() |
20ba9cd589 | ||
![]() |
af1ac9bce8 | ||
![]() |
31a6e3ee9a | ||
![]() |
dca1dcdd2d | ||
![]() |
a54f5f3e9f | ||
![]() |
51f49cd45a | ||
![]() |
eb63eb7b1d | ||
![]() |
b4f7e51e96 | ||
![]() |
c2d2189039 | ||
![]() |
1eda3258de | ||
![]() |
0171cb0844 | ||
![]() |
08a0a63631 | ||
![]() |
8ed7d0385d | ||
![]() |
02d45e9c39 | ||
![]() |
2a402a0674 | ||
![]() |
1be56b5a39 | ||
![]() |
1e175bc41f | ||
![]() |
c16569a6ab | ||
![]() |
d19cc64611 | ||
![]() |
958882c1c5 | ||
![]() |
2f7323043f | ||
![]() |
fbc7b6fc4f | ||
![]() |
3178b69172 | ||
![]() |
28fcf3c1ea | ||
![]() |
71939097b0 | ||
![]() |
2fd337bb55 | ||
![]() |
5e26161e84 | ||
![]() |
04ecb89e9a | ||
![]() |
23f851343f | ||
![]() |
5ce1782d05 | ||
![]() |
daee14533c | ||
![]() |
31f505b309 | ||
![]() |
c8214bf3ea | ||
![]() |
1935a69c04 | ||
![]() |
ea5f64bfea | ||
![]() |
3dd04b27de | ||
![]() |
9940b7bff3 | ||
![]() |
901ccf2d14 | ||
![]() |
6a1a821ed4 | ||
![]() |
aa9fe09337 | ||
![]() |
bdb655243f | ||
![]() |
566c29363f | ||
![]() |
37b8c0241f | ||
![]() |
1a17645e93 | ||
![]() |
5fba850d74 | ||
![]() |
bd90e0ce52 | ||
![]() |
c24cfa721b | ||
![]() |
1670d96908 | ||
![]() |
ec3c44c5b3 | ||
![]() |
12bfad8fe6 | ||
![]() |
594edc6e9a | ||
![]() |
99304a09ca | ||
![]() |
5c93aadce3 | ||
![]() |
f0c10efca6 | ||
![]() |
c10dd5239e | ||
![]() |
fa0d1a50b5 | ||
![]() |
66a1c4d242 | ||
![]() |
b7c6b84826 | ||
![]() |
1c587bde25 | ||
![]() |
e1dc29c374 | ||
![]() |
59e0ffd8e0 | ||
![]() |
f7c767c1c8 | ||
![]() |
fca9db7972 | ||
![]() |
afd23849a6 | ||
![]() |
1b2eee058c | ||
![]() |
777486a5be | ||
![]() |
8b6f221e22 | ||
![]() |
97ecb7e3aa | ||
![]() |
5df5e25d68 | ||
![]() |
66a950a481 | ||
![]() |
29346cdef5 | ||
![]() |
4f8e941e39 | ||
![]() |
756f5a1608 | ||
![]() |
416e7d6fe0 | ||
![]() |
7b210429b5 | ||
![]() |
c52d47b15f | ||
![]() |
9a43626aa6 | ||
![]() |
c14e722303 | ||
![]() |
07633ba79d | ||
![]() |
6ff7fe55fc | ||
![]() |
a14c36cd3e | ||
![]() |
c21fe3d23a | ||
![]() |
89e8fd3d3a | ||
![]() |
966f393e20 | ||
![]() |
d85c27e67b | ||
![]() |
a5f8a53196 | ||
![]() |
9299e5b614 | ||
![]() |
6c31a3b77e | ||
![]() |
6262c80886 | ||
![]() |
5aa115c88a | ||
![]() |
511587dd8b | ||
![]() |
5d34bb9062 | ||
![]() |
4025d753e3 | ||
![]() |
58214c0ac3 | ||
![]() |
5d113757df | ||
![]() |
b41a03674f | ||
![]() |
8c1f64243f | ||
![]() |
cdd9dd4f6f | ||
![]() |
57f2a43864 | ||
![]() |
74d7fb7835 | ||
![]() |
dd61429e2f | ||
![]() |
8f27f50b34 | ||
![]() |
96e7fbdca1 | ||
![]() |
39352eb97e | ||
![]() |
1c607645c7 | ||
![]() |
2ad3493fb0 | ||
![]() |
331dc99086 | ||
![]() |
4424c8272d | ||
![]() |
16162955af | ||
![]() |
23006e495f | ||
![]() |
4351af35f4 | ||
![]() |
e8af5db2e8 | ||
![]() |
9d048371b7 | ||
![]() |
a187726ba8 | ||
![]() |
0ff1bcfd19 | ||
![]() |
08fee35bcf | ||
![]() |
08574bf676 | ||
![]() |
5c27a0efcc | ||
![]() |
7576f96604 | ||
![]() |
456e330854 | ||
![]() |
8d95eb937f | ||
![]() |
2f50fef787 | ||
![]() |
ac69007292 | ||
![]() |
e8b0a3d1c4 | ||
![]() |
dfbbbd9381 | ||
![]() |
4ab339b375 | ||
![]() |
fce24cedbd | ||
![]() |
f9b62564ca | ||
![]() |
d569d9488a | ||
![]() |
f172751df3 | ||
![]() |
13233d47bd | ||
![]() |
74d7d92a4d | ||
![]() |
d3a0f5c268 | ||
![]() |
63f213a5d5 | ||
![]() |
6d4c4d9f27 | ||
![]() |
b2be4672ec | ||
![]() |
1bfe328a1b | ||
![]() |
1f453b6c13 | ||
![]() |
ee8e3c3c9c | ||
![]() |
37f315dfc2 | ||
![]() |
1e1e560795 | ||
![]() |
8d88cfcd68 | ||
![]() |
74badf9c9c | ||
![]() |
86087550f1 | ||
![]() |
8dfc200f24 | ||
![]() |
cb4a4bd707 | ||
![]() |
24ba2a150b | ||
![]() |
e1d92bf0ec | ||
![]() |
270f3e9ffd | ||
![]() |
e1f73697ff | ||
![]() |
e82155aac4 | ||
![]() |
d7669413af | ||
![]() |
28d4fc5d12 | ||
![]() |
dac4fcd52b | ||
![]() |
8dff33b38a | ||
![]() |
6bae1f8a4b | ||
![]() |
10ceeb7f02 | ||
![]() |
448bb3ce98 | ||
![]() |
0531d47721 | ||
![]() |
b21719e2f2 | ||
![]() |
2960db2352 | ||
![]() |
cb3629343b | ||
![]() |
fa7013fdf7 | ||
![]() |
db868dba81 | ||
![]() |
608a411d97 | ||
![]() |
20c63c73b3 | ||
![]() |
5ca3401eb9 | ||
![]() |
435a3c285e | ||
![]() |
6405cae706 | ||
![]() |
6451e7f9f1 | ||
![]() |
127fa5fa82 | ||
![]() |
63fd0a9fa1 | ||
![]() |
344ad44854 | ||
![]() |
43a5742258 | ||
![]() |
924a61309a | ||
![]() |
9dd5c88da4 | ||
![]() |
0598bcf332 | ||
![]() |
ead1b3e5f5 | ||
![]() |
dd493c87fa | ||
![]() |
29640e614c | ||
![]() |
73e0aa17c4 | ||
![]() |
917a2e59ce | ||
![]() |
119e1a9bf0 | ||
![]() |
6eb79ba75e | ||
![]() |
1f9eafa619 | ||
![]() |
d5da404ed4 | ||
![]() |
1b41aa70cc | ||
![]() |
284b1a4f8a | ||
![]() |
fe432ad843 | ||
![]() |
15fca6bd12 | ||
![]() |
57c0daa294 | ||
![]() |
fee95654fa | ||
![]() |
bb436f9931 | ||
![]() |
3682f0aed4 | ||
![]() |
43c366d2fb | ||
![]() |
b21b885aa9 | ||
![]() |
6bb929a896 | ||
![]() |
f5829f6012 | ||
![]() |
d06b4abecf | ||
![]() |
d7db631b95 | ||
![]() |
93e6ec4933 | ||
![]() |
2fbf2c7771 | ||
![]() |
b474677db1 | ||
![]() |
dbcd8602c5 | ||
![]() |
1793407748 | ||
![]() |
6f0d4a50d1 | ||
![]() |
10817aefae | ||
![]() |
4293a44c93 | ||
![]() |
96759e9303 | ||
![]() |
448483371c | ||
![]() |
0738964e64 | ||
![]() |
deec2e62ce | ||
![]() |
c52da69367 | ||
![]() |
2de35266c4 | ||
![]() |
e6b8097b43 | ||
![]() |
55c7585334 | ||
![]() |
62d278a367 | ||
![]() |
38e20b7c31 | ||
![]() |
d3fce8ca36 | ||
![]() |
2a7d7acce0 | ||
![]() |
eab63c6af8 | ||
![]() |
4f0613689a | ||
![]() |
743463dce9 | ||
![]() |
0b04176f18 | ||
![]() |
166a65e1b1 | ||
![]() |
5da4537d7c | ||
![]() |
b9e71c9f6f | ||
![]() |
500c96abe2 | ||
![]() |
149c4f5c7b | ||
![]() |
76bc9fcddf | ||
![]() |
f999f167b1 | ||
![]() |
9ac96cdd50 | ||
![]() |
3c4bc6ae35 | ||
![]() |
879b504b0f | ||
![]() |
baa5df1d01 | ||
![]() |
912c958ac0 | ||
![]() |
d166a16a24 | ||
![]() |
1bd1ddf410 | ||
![]() |
7890771faa | ||
![]() |
376fd0d439 | ||
![]() |
c414e6caa2 | ||
![]() |
ed7a3b2356 | ||
![]() |
adffbd1973 | ||
![]() |
6750682e4f | ||
![]() |
d1fb4067e7 | ||
![]() |
ab9fe173a8 | ||
![]() |
8a7ae368d8 | ||
![]() |
248fc5f015 | ||
![]() |
e16611f15a | ||
![]() |
24b3651d2e | ||
![]() |
0e236e53fd | ||
![]() |
61ba9848e5 | ||
![]() |
01ff89315b | ||
![]() |
155d8d4dfd | ||
![]() |
c7822ed6e6 | ||
![]() |
1ed40b3b82 | ||
![]() |
18dc359cfc | ||
![]() |
b871849df4 | ||
![]() |
7d1ca3862d | ||
![]() |
2a887870ff | ||
![]() |
1d5bde01cc | ||
![]() |
8537939d26 | ||
![]() |
fcb3ac3917 | ||
![]() |
d63a4b3912 | ||
![]() |
6a245a202c | ||
![]() |
429043f60c | ||
![]() |
8f491fe6e1 | ||
![]() |
92358bafc0 | ||
![]() |
d93e89899b | ||
![]() |
2bdd073608 | ||
![]() |
7801937f74 | ||
![]() |
1d7e0c47dd | ||
![]() |
7b9b48e62d | ||
![]() |
d610f94e7d | ||
![]() |
7742f2f5fb | ||
![]() |
3f7e22497d | ||
![]() |
e745760520 | ||
![]() |
bd5c2b1daa | ||
![]() |
0bbeede975 | ||
![]() |
30865c9d1c | ||
![]() |
1cfe874893 | ||
![]() |
5da46ca29c | ||
![]() |
cc21cbd1bd | ||
![]() |
79ba4a9d23 | ||
![]() |
44ef0cbe59 | ||
![]() |
88f2b39576 | ||
![]() |
bf10cdef63 | ||
![]() |
3696db89ab | ||
![]() |
eeaa5d21d8 | ||
![]() |
f4bec3bb4d | ||
![]() |
dc6f3cf0b2 | ||
![]() |
b5a1310add | ||
![]() |
b22343f65c | ||
![]() |
b9b317e213 | ||
![]() |
6cd18bbf04 | ||
![]() |
8c6400e4d6 | ||
![]() |
b6e1559a80 | ||
![]() |
605ae75dda | ||
![]() |
39f318fbd5 | ||
![]() |
21def81439 | ||
![]() |
7e9ab8321a | ||
![]() |
4094d3a9bf | ||
![]() |
0877fec638 | ||
![]() |
f40c3e6b81 | ||
![]() |
bb705ae863 | ||
![]() |
c4a3a53be0 | ||
![]() |
713ff17e91 | ||
![]() |
0c659a477d | ||
![]() |
26678a65f8 | ||
![]() |
c5116fb318 | ||
![]() |
fa970fa102 | ||
![]() |
fbafd23177 | ||
![]() |
be742c78e7 | ||
![]() |
3e97c154a0 | ||
![]() |
1eb97ea381 | ||
![]() |
f6e06456b0 | ||
![]() |
5e90cd9714 | ||
![]() |
06406fa46c | ||
![]() |
08b7034d6f | ||
![]() |
5b91aef4ec | ||
![]() |
d8a121cd06 | ||
![]() |
902a1b1c88 | ||
![]() |
c9a2b86c16 | ||
![]() |
27fcb70774 | ||
![]() |
e488841031 | ||
![]() |
6cffa1c0ca | ||
![]() |
d3e8fcea47 | ||
![]() |
9526424a47 | ||
![]() |
149278abd0 | ||
![]() |
314c8b22db | ||
![]() |
c502688ce3 | ||
![]() |
866346b059 | ||
![]() |
de8286829a | ||
![]() |
18c407bfc2 | ||
![]() |
076684176b | ||
![]() |
a2cb1615b3 | ||
![]() |
6b5fa140b9 | ||
![]() |
356d0521e6 | ||
![]() |
5a08ab93d3 | ||
![]() |
8185f4dfda | ||
![]() |
f4f0e2afa8 | ||
![]() |
67d6b1e724 | ||
![]() |
e34a9d452a | ||
![]() |
2819a36469 | ||
![]() |
8815b4e0c3 | ||
![]() |
960635b993 | ||
![]() |
f9dcb29e92 | ||
![]() |
5a746769d0 | ||
![]() |
87b4500467 | ||
![]() |
951e47469a | ||
![]() |
c1db963c76 | ||
![]() |
a252ec36f0 | ||
![]() |
33b478199a | ||
![]() |
21f285e099 | ||
![]() |
fec312ed09 | ||
![]() |
eb2627721e | ||
![]() |
ff32f822ba | ||
![]() |
80bfccd9d3 | ||
![]() |
eb5a24064a | ||
![]() |
b971ccc673 | ||
![]() |
945687bdad | ||
![]() |
6f56d0a944 | ||
![]() |
66c48ba4ec | ||
![]() |
f83ade8d33 | ||
![]() |
eae0d1b2a6 | ||
![]() |
743f92d15e | ||
![]() |
822dec5c03 | ||
![]() |
40b84755de | ||
![]() |
1b307166ee | ||
![]() |
7a483e7912 | ||
![]() |
841230fe80 | ||
![]() |
900ae4e1ea | ||
![]() |
72b6d6c676 | ||
![]() |
f979ed133f | ||
![]() |
fb2cf5f04d | ||
![]() |
62401b24ec | ||
![]() |
43f833e604 | ||
![]() |
612a3f4401 | ||
![]() |
4c2cf99fd2 | ||
![]() |
aebd9b410d | ||
![]() |
8eee9caa01 | ||
![]() |
d278a30d19 | ||
![]() |
1117569148 | ||
![]() |
753396ac0c | ||
![]() |
9a267ffcaf | ||
![]() |
6254143fc6 | ||
![]() |
862e19a263 | ||
![]() |
eeef6fe65f | ||
![]() |
c05916477c | ||
![]() |
63d7e73cff | ||
![]() |
8cb629fb55 | ||
![]() |
d596f28f46 | ||
![]() |
8f26c0aeae | ||
![]() |
e4978d8a05 | ||
![]() |
4f43d977ed | ||
![]() |
d0c39e6bf4 | ||
![]() |
0965d98dbd | ||
![]() |
45fea827af | ||
![]() |
350ee13d66 | ||
![]() |
bb7af2e8ed | ||
![]() |
9cda561091 | ||
![]() |
d38846f126 | ||
![]() |
3b764439af | ||
![]() |
d1e404f093 | ||
![]() |
6c4440c2d1 | ||
![]() |
8e2a7c2b2d | ||
![]() |
fce30baa12 | ||
![]() |
c63215c992 | ||
![]() |
815fe02c83 | ||
![]() |
e2e1a5b38c | ||
![]() |
4222c31b3e | ||
![]() |
e5d3b0e7b8 | ||
![]() |
422064e092 | ||
![]() |
d548c6bdfa | ||
![]() |
efb7b147af | ||
![]() |
0f8b220f59 | ||
![]() |
1bba253156 | ||
![]() |
a2d29a4531 | ||
![]() |
589869c5ed | ||
![]() |
7b41922c2d | ||
![]() |
7262fbfb4e | ||
![]() |
9c0e8ee833 | ||
![]() |
9119d72b9b | ||
![]() |
891aac4713 | ||
![]() |
2fa006d74e | ||
![]() |
a6c97a304f | ||
![]() |
cfa4f8fa63 | ||
![]() |
ce0cf09b10 | ||
![]() |
1567fb745d | ||
![]() |
416f563261 | ||
![]() |
25d6eea906 | ||
![]() |
4e819ac035 | ||
![]() |
dbcaa6bf85 | ||
![]() |
3205187090 | ||
![]() |
ed85981409 | ||
![]() |
5d920ff7df | ||
![]() |
5456f0e106 | ||
![]() |
8962c16670 | ||
![]() |
2ed1572c3c | ||
![]() |
89cb673502 | ||
![]() |
98c9c8dd43 | ||
![]() |
fd46f141ea | ||
![]() |
41aec089bc | ||
![]() |
31697022fd | ||
![]() |
948a1c3d03 | ||
![]() |
20371895c9 | ||
![]() |
617027eb52 | ||
![]() |
6b9106b178 | ||
![]() |
6207ccd559 | ||
![]() |
043ac5a510 | ||
![]() |
4eaeade618 | ||
![]() |
f318f76994 | ||
![]() |
fd085d2d37 | ||
![]() |
327f580218 | ||
![]() |
7bef78e10f | ||
![]() |
b54916b1dc | ||
![]() |
c2ab102c0e | ||
![]() |
3f3e73455b | ||
![]() |
4f09a5111b | ||
![]() |
baf820bcdb | ||
![]() |
538f69235f | ||
![]() |
2b9f1257be | ||
![]() |
6e58e8d50f | ||
![]() |
90420f1a89 | ||
![]() |
021b822c4f | ||
![]() |
4ac54845fc | ||
![]() |
4c9d3ee3a6 | ||
![]() |
e688671972 | ||
![]() |
6bd63ff42a | ||
![]() |
efc5211451 | ||
![]() |
f2bf5b12bd | ||
![]() |
dccda1af92 | ||
![]() |
4ca0e6b781 | ||
![]() |
4016e7c217 | ||
![]() |
907a26a8b9 | ||
![]() |
dc23bfeb87 | ||
![]() |
4bfd4bd714 | ||
![]() |
9ddb984004 | ||
![]() |
e618c0e9ef | ||
![]() |
b55d2b53df | ||
![]() |
5733ec8363 | ||
![]() |
d7d75a1fe8 | ||
![]() |
ef6ccb330b | ||
![]() |
afbacdfc96 | ||
![]() |
5842441062 | ||
![]() |
f083301b4c | ||
![]() |
8edbecce3c | ||
![]() |
b114c52c0d | ||
![]() |
4671f47222 | ||
![]() |
4be0e1ee7f | ||
![]() |
dd4c213805 | ||
![]() |
7e57a8f71e | ||
![]() |
86d3d35619 | ||
![]() |
f195111354 | ||
![]() |
ec6fd48b86 | ||
![]() |
1516d55a88 | ||
![]() |
bdb0dc32a7 | ||
![]() |
5ff2f792e6 | ||
![]() |
8246505289 | ||
![]() |
526a5d3fb1 | ||
![]() |
f970815645 | ||
![]() |
f83c77e7ea | ||
![]() |
654cce92cd | ||
![]() |
8f0bfbc553 | ||
![]() |
bc39e52f56 | ||
![]() |
a822238735 | ||
![]() |
b168772d7f | ||
![]() |
034e104c06 | ||
![]() |
0c0f117bc3 | ||
![]() |
1d4ab8833b | ||
![]() |
90fee9788d | ||
![]() |
bf7597e1b2 | ||
![]() |
54ff940c2b | ||
![]() |
446641c31c | ||
![]() |
572d55752c | ||
![]() |
04394d8ced | ||
![]() |
c7ea66bfef | ||
![]() |
bfc5e2dce6 | ||
![]() |
a7e8293d1a | ||
![]() |
698f0cc230 | ||
![]() |
f414f0746c | ||
![]() |
235b8f359c | ||
![]() |
2326cf3de8 | ||
![]() |
b8879853d5 | ||
![]() |
498fd4bf01 | ||
![]() |
996f326c74 | ||
![]() |
43b68faf73 | ||
![]() |
8429ad7db7 | ||
![]() |
879b522914 | ||
![]() |
be584aa3d1 | ||
![]() |
b82f57e7a2 | ||
![]() |
998ec3eb4f | ||
![]() |
f6e3a74567 | ||
![]() |
6d8f55cf75 | ||
![]() |
c4d8466195 | ||
![]() |
36ff506dfe | ||
![]() |
af1fb7aaa6 | ||
![]() |
1a944735df | ||
![]() |
1c8fba4286 | ||
![]() |
0260b30335 | ||
![]() |
0f72085c2a | ||
![]() |
6ad8a10f37 | ||
![]() |
8a38983dfc | ||
![]() |
9f5600b7f7 | ||
![]() |
a7632889a2 | ||
![]() |
58a4bf479c | ||
![]() |
1ce4f99b80 | ||
![]() |
f94ecb3ec5 | ||
![]() |
ce7c22fd57 | ||
![]() |
a5c7e59601 | ||
![]() |
efbf70726f | ||
![]() |
490b1dc01b | ||
![]() |
ec81965393 | ||
![]() |
21536b8948 | ||
![]() |
100c2c8741 | ||
![]() |
d47e4a3e0e | ||
![]() |
0c9ea8f580 | ||
![]() |
2320eb8723 | ||
![]() |
17bbeefeca | ||
![]() |
31372e269d | ||
![]() |
ac799a60da | ||
![]() |
6a1d716ba1 | ||
![]() |
ed12cc1186 | ||
![]() |
8370ac8426 | ||
![]() |
a20ff59572 | ||
![]() |
8587f01caa | ||
![]() |
c7078be407 | ||
![]() |
718a00974b | ||
![]() |
d612676a80 | ||
![]() |
d7bcfb415b | ||
![]() |
6928a17e61 | ||
![]() |
d097092e8e | ||
![]() |
79eb6573be | ||
![]() |
ffed8c9181 | ||
![]() |
ff8573635d | ||
![]() |
643244ffd1 | ||
![]() |
9fd059aef8 | ||
![]() |
6764a7ad2f | ||
![]() |
5642d0aae6 | ||
![]() |
763e5f4ac0 | ||
![]() |
87abdb92e9 | ||
![]() |
447ab47d59 | ||
![]() |
9ed88f25f0 | ||
![]() |
73814550e5 | ||
![]() |
226f7dea65 | ||
![]() |
ed3de771e8 | ||
![]() |
edf3cf3b65 | ||
![]() |
7a54615156 | ||
![]() |
1e1f967292 | ||
![]() |
6f5d4fdc51 | ||
![]() |
e7a6be035e | ||
![]() |
ee762f349c | ||
![]() |
4d98b05e6c | ||
![]() |
4f4591658d | ||
![]() |
0c70e95232 | ||
![]() |
594e8b8c20 | ||
![]() |
4c358b9f08 | ||
![]() |
7b1594c69c | ||
![]() |
6d27feba97 | ||
![]() |
4363dcbbc1 | ||
![]() |
a208f47b6a | ||
![]() |
84268cd43c | ||
![]() |
380c32dee2 | ||
![]() |
bfc7e7c33f | ||
![]() |
8594e4ce4a | ||
![]() |
752dabe554 | ||
![]() |
042db64a00 | ||
![]() |
f55fd8d861 | ||
![]() |
cc41572d48 | ||
![]() |
42de962cbf | ||
![]() |
f527b8225f | ||
![]() |
dd9ccfa3d2 | ||
![]() |
24b6780c1f | ||
![]() |
941571f71f | ||
![]() |
65be9f438b | ||
![]() |
15d13ac9f6 | ||
![]() |
6df3acaf1e | ||
![]() |
b6ee24dcd5 | ||
![]() |
1a3e5b7893 | ||
![]() |
5df8e10b95 | ||
![]() |
bddf2d6537 | ||
![]() |
3d05bdcb63 | ||
![]() |
52e28eefce | ||
![]() |
b5b0626251 | ||
![]() |
4a4f311cf8 | ||
![]() |
bad4d14cf6 | ||
![]() |
b814a633c6 | ||
![]() |
73524d70dc | ||
![]() |
9fad83e46c | ||
![]() |
9fbf4771f2 | ||
![]() |
41cd7da5bd | ||
![]() |
3d975fd767 | ||
![]() |
f8f857c820 | ||
![]() |
9546ed0bb6 | ||
![]() |
a3f957427f | ||
![]() |
9b36c55422 | ||
![]() |
b6286372fb | ||
![]() |
74e71a1971 | ||
![]() |
9050f1a039 | ||
![]() |
de303bf453 | ||
![]() |
3d8d3a9237 | ||
![]() |
cf354d59fb | ||
![]() |
706852d9a7 | ||
![]() |
63e405c27e | ||
![]() |
33e8c6fb73 | ||
![]() |
faba28dd94 | ||
![]() |
16f36824e6 | ||
![]() |
5c1ec0b141 | ||
![]() |
e24d1016a5 | ||
![]() |
a82ee338ef | ||
![]() |
60405bf222 | ||
![]() |
1da6c8c84e | ||
![]() |
32329c1817 | ||
![]() |
4aee7a6c61 | ||
![]() |
e6c2937c1b | ||
![]() |
1bbf9b62ad | ||
![]() |
fa9e024b42 | ||
![]() |
8c52038671 | ||
![]() |
7aff97b6ff | ||
![]() |
77530f13ee | ||
![]() |
0a1c22530a | ||
![]() |
e6c0fe0370 | ||
![]() |
63ba0a5551 | ||
![]() |
c7bfe0d537 | ||
![]() |
05887c976a | ||
![]() |
ccc8fda24f | ||
![]() |
abb458bdd3 | ||
![]() |
41429dd254 | ||
![]() |
268a39e93b | ||
![]() |
8411c60d4a | ||
![]() |
de91f7ec15 | ||
![]() |
6094af819b | ||
![]() |
5f21563d7d | ||
![]() |
5b9afe70b2 | ||
![]() |
27f47460e3 | ||
![]() |
ad1bd045f7 | ||
![]() |
9a9a8bfdc7 | ||
![]() |
c88152cac2 | ||
![]() |
61c72ac3ea | ||
![]() |
84c39f3baa | ||
![]() |
7d6ef195fd | ||
![]() |
52ac972332 | ||
![]() |
9aa599f9d2 | ||
![]() |
a5ce7e5a1f | ||
![]() |
f8511bf199 | ||
![]() |
cfdc6bac7b | ||
![]() |
33e4c088b9 | ||
![]() |
a620bf2174 | ||
![]() |
e77d63294e | ||
![]() |
82c55ba038 | ||
![]() |
e1394d7a7d | ||
![]() |
9f99fa8edc | ||
![]() |
02e67a901f | ||
![]() |
f331f5e92c | ||
![]() |
6e5bf5fef6 | ||
![]() |
c7b4e28f82 | ||
![]() |
9235ecfe7b | ||
![]() |
7f234721ec | ||
![]() |
242b3edbc4 | ||
![]() |
b7894faa96 | ||
![]() |
f3a4b9c709 | ||
![]() |
ea638dd0e0 | ||
![]() |
f0b53c4cbb | ||
![]() |
3eae349a0a | ||
![]() |
ff5004cb8d | ||
![]() |
567855e2d2 | ||
![]() |
d20da5d803 | ||
![]() |
b98a9d2e80 | ||
![]() |
955164aa3c | ||
![]() |
db8f9988eb | ||
![]() |
9b2cae8e33 | ||
![]() |
ac325c5c5e | ||
![]() |
e92f3ea100 | ||
![]() |
75d8338f2d | ||
![]() |
a1b5dad1c8 | ||
![]() |
0e2f7fb122 | ||
![]() |
ee631730c7 | ||
![]() |
f7fd445c73 | ||
![]() |
5c0fc38272 | ||
![]() |
6df0321962 | ||
![]() |
65d19d350c | ||
![]() |
bc93796727 | ||
![]() |
6f0531c663 | ||
![]() |
545acebbaf | ||
![]() |
af49404320 | ||
![]() |
717802860d | ||
![]() |
edd3f35790 | ||
![]() |
20806a8c97 | ||
![]() |
f071cc5c04 | ||
![]() |
90c9078bf5 | ||
![]() |
203e70afbc | ||
![]() |
2599e54fd0 | ||
![]() |
2737425242 | ||
![]() |
40569eee2e | ||
![]() |
883c2851ff | ||
![]() |
4fe4d0a7e7 | ||
![]() |
a46d8fe7f3 | ||
![]() |
dd70d27a0e | ||
![]() |
b46ee0c495 | ||
![]() |
28ffb3fd18 | ||
![]() |
d1456850d1 | ||
![]() |
050e2febab | ||
![]() |
f56c8ef755 | ||
![]() |
1a9529157f | ||
![]() |
8c5c3cb7a6 | ||
![]() |
8ab9b06185 | ||
![]() |
91c5556f21 | ||
![]() |
95cf47d9a4 | ||
![]() |
dfad2370aa | ||
![]() |
331e25cc41 | ||
![]() |
6dcf7e8088 | ||
![]() |
99690a6145 | ||
![]() |
27bcc3c685 | ||
![]() |
bb60558968 | ||
![]() |
af38a3927a | ||
![]() |
bf6771152e | ||
![]() |
48b6c81b36 | ||
![]() |
d95da7372a | ||
![]() |
231037b0fe | ||
![]() |
c481d20ce8 | ||
![]() |
dc32556983 | ||
![]() |
d7b43f4722 | ||
![]() |
ca29fcec7d | ||
![]() |
bd637d661a | ||
![]() |
ef8174af70 | ||
![]() |
dee562437b | ||
![]() |
a1a4a73260 | ||
![]() |
627edeb0f2 | ||
![]() |
16f29b65f2 | ||
![]() |
6d2a863af5 | ||
![]() |
35e61a0c69 | ||
![]() |
c474699695 | ||
![]() |
1e2dadf75e | ||
![]() |
002303a765 | ||
![]() |
59426c31f7 | ||
![]() |
fd895d0ef5 | ||
![]() |
8220272953 | ||
![]() |
c8fc1ebefa | ||
![]() |
ac9531eb98 | ||
![]() |
56d68ce161 | ||
![]() |
a3ef3b34e1 | ||
![]() |
30e348f965 | ||
![]() |
b981ddadca | ||
![]() |
bcd721e2ef | ||
![]() |
a96ff8399f | ||
![]() |
2f4dec45a6 | ||
![]() |
b216e42397 | ||
![]() |
9b6c624554 | ||
![]() |
12154613c2 | ||
![]() |
a42dee5a44 | ||
![]() |
d4d86172f0 | ||
![]() |
a49f454826 | ||
![]() |
cd9a965057 | ||
![]() |
f5bc26e9fe | ||
![]() |
c55d69d587 | ||
![]() |
e508357cfb | ||
![]() |
1c578b2b3f | ||
![]() |
4ce2ef732d | ||
![]() |
65705aba10 | ||
![]() |
e97873fb7e | ||
![]() |
13d1df3cef | ||
![]() |
7ffb7db888 | ||
![]() |
74366379ba | ||
![]() |
6b89d05a3c | ||
![]() |
b0b9a4d33c | ||
![]() |
fc0fe0557b | ||
![]() |
116347ef66 | ||
![]() |
58516913b4 | ||
![]() |
fb836fcf6f | ||
![]() |
1ad44ace0a | ||
![]() |
32ddaed376 | ||
![]() |
4e388e5dc2 | ||
![]() |
44c467f472 | ||
![]() |
f90d0d2dae | ||
![]() |
b47920169f | ||
![]() |
81bf349871 | ||
![]() |
9d849a16ec | ||
![]() |
383749a9c0 | ||
![]() |
5e2feb5803 | ||
![]() |
93645681b0 | ||
![]() |
174e8d3c19 | ||
![]() |
511a5c0d1f | ||
![]() |
c92c840c41 | ||
![]() |
952a1a1da8 | ||
![]() |
e89cb4cc0e | ||
![]() |
89cc70091c | ||
![]() |
cfaa2c542d | ||
![]() |
c6bbe9e020 | ||
![]() |
15d2fd4581 | ||
![]() |
819c2b0bc3 | ||
![]() |
30984c62fa | ||
![]() |
bd649cfb33 | ||
![]() |
5c57cd6541 | ||
![]() |
02e2080ce2 | ||
![]() |
c00601743c | ||
![]() |
523f8dae2a | ||
![]() |
4e56fe9fc3 | ||
![]() |
2829146598 | ||
![]() |
b5ed158cc6 | ||
![]() |
7314f20563 | ||
![]() |
0559eb1a9a | ||
![]() |
40f732cd48 | ||
![]() |
eb837e3685 | ||
![]() |
8ddfbf37c3 | ||
![]() |
0729ea1c38 | ||
![]() |
39541fe348 | ||
![]() |
259c5199d6 | ||
![]() |
af0b563036 | ||
![]() |
c2b879ccab | ||
![]() |
8447c6d79f | ||
![]() |
46fb61c68c | ||
![]() |
860ef650f9 | ||
![]() |
ef6f9b9d70 | ||
![]() |
9f701c8f35 | ||
![]() |
e18d3d0c93 | ||
![]() |
dd46a64e79 | ||
![]() |
31a7effa5f | ||
![]() |
42f3f405ea | ||
![]() |
1b612f7112 | ||
![]() |
6e364ce4b9 | ||
![]() |
89eb44f3ef | ||
![]() |
53364d6ddc | ||
![]() |
4631f2223b | ||
![]() |
20fb9ab97d | ||
![]() |
dcbac42571 | ||
![]() |
0258d926a2 | ||
![]() |
e79e60f0bb | ||
![]() |
ef0c0b1e32 | ||
![]() |
14631c65a6 | ||
![]() |
f706f7508e | ||
![]() |
6662b4f047 | ||
![]() |
13c453d610 | ||
![]() |
e8026ef074 | ||
![]() |
cefc6f8eac | ||
![]() |
bf809ac118 | ||
![]() |
88ba0c1154 | ||
![]() |
43d2d5334a | ||
![]() |
de41efbaf8 | ||
![]() |
81419ca68b | ||
![]() |
276cbc0ace | ||
![]() |
6ada5110a4 | ||
![]() |
f6ee45249d | ||
![]() |
be36a20249 | ||
![]() |
5173597868 | ||
![]() |
86657529c9 | ||
![]() |
6460b7b0a1 | ||
![]() |
3b91be8449 | ||
![]() |
055aea58bb | ||
![]() |
1914c69c47 | ||
![]() |
fdb06ad973 | ||
![]() |
37979b4f9c | ||
![]() |
0a03c30e8a | ||
![]() |
e8dbf1511b | ||
![]() |
f163abb4a5 | ||
![]() |
fa99f3dbbe | ||
![]() |
76e21a6378 | ||
![]() |
0d3c403f08 | ||
![]() |
7c6ff85acd | ||
![]() |
4d60c61512 | ||
![]() |
31376ee2fd | ||
![]() |
b067bbd935 | ||
![]() |
f980170a82 | ||
![]() |
8b7ef14f65 | ||
![]() |
5eb8549aef | ||
![]() |
728b76aa7e | ||
![]() |
c20ed92fab | ||
![]() |
13f6a8da53 | ||
![]() |
28426f3ee3 | ||
![]() |
fb925745ba | ||
![]() |
271cb66c69 | ||
![]() |
b3f2812191 | ||
![]() |
1824363f86 | ||
![]() |
439d2bcf7f | ||
![]() |
447f5fd5c8 | ||
![]() |
ce78e330d3 | ||
![]() |
7a47d58ad4 | ||
![]() |
e9f467ed77 | ||
![]() |
aedfb1d8da | ||
![]() |
0ab424c0d2 | ||
![]() |
674b1efa0f | ||
![]() |
a7509988af | ||
![]() |
f58f738fe4 | ||
![]() |
ba9228b142 | ||
![]() |
0beef302c6 | ||
![]() |
da538f6424 | ||
![]() |
8384c2cba3 | ||
![]() |
fbf430fac9 | ||
![]() |
29516304b0 | ||
![]() |
c853de4315 | ||
![]() |
e2d17fe20f | ||
![]() |
9e5be7ffcf | ||
![]() |
44dc3035a5 | ||
![]() |
468666cdc8 | ||
![]() |
00972f5d73 | ||
![]() |
bffe6e7a32 | ||
![]() |
cc3bb912be | ||
![]() |
9a64b6080d | ||
![]() |
42156edc76 | ||
![]() |
7ae98477d5 | ||
![]() |
626b6f5ccd | ||
![]() |
9c82de7df2 | ||
![]() |
e7de562b93 | ||
![]() |
06d679d1ca | ||
![]() |
33cf30a07b | ||
![]() |
6ac1960c47 | ||
![]() |
1350096545 | ||
![]() |
18b9df62e4 | ||
![]() |
42495a9914 | ||
![]() |
b2f25d7f0a | ||
![]() |
30b71328b9 | ||
![]() |
7c97e37dc3 | ||
![]() |
f42d088fbc | ||
![]() |
a5e63de016 | ||
![]() |
f7d3718457 | ||
![]() |
3fa0f1b9e8 | ||
![]() |
f1a54bd7d0 | ||
![]() |
1803e47c02 | ||
![]() |
296e44deec | ||
![]() |
c546065611 | ||
![]() |
7e759e6ae8 | ||
![]() |
a9608c096e | ||
![]() |
4db723fa6f | ||
![]() |
64e8efbc07 | ||
![]() |
15f4bc5bd6 | ||
![]() |
ce6668ef8c | ||
![]() |
f60ab3e01f | ||
![]() |
78a7aa91c0 | ||
![]() |
a574f3d173 | ||
![]() |
c9223686d2 | ||
![]() |
c86afe29ba | ||
![]() |
0c56a7a23b | ||
![]() |
b66676de85 | ||
![]() |
c7a468eb14 | ||
![]() |
1c5b5e3621 | ||
![]() |
d78c757b52 | ||
![]() |
9871a4e36f | ||
![]() |
3e82595193 | ||
![]() |
044456cd98 | ||
![]() |
c0ba724578 | ||
![]() |
79daa12b06 | ||
![]() |
1f1e6006ec | ||
![]() |
8ee8acf655 | ||
![]() |
5e928769ec | ||
![]() |
a0b60a8347 | ||
![]() |
dbe2a7b240 | ||
![]() |
e8298726ca | ||
![]() |
4c675b8311 | ||
![]() |
1ab9bc40a5 | ||
![]() |
859c81c81b | ||
![]() |
83ed87cec0 | ||
![]() |
9b6feb4a01 | ||
![]() |
40841b9b07 | ||
![]() |
d1227b6447 | ||
![]() |
fc417e7273 | ||
![]() |
bda238344c | ||
![]() |
45add916f4 | ||
![]() |
ca549cf081 | ||
![]() |
fb3c5eaafa | ||
![]() |
8e4873cf2a | ||
![]() |
47316741c1 | ||
![]() |
86dd57d13e | ||
![]() |
4fe6ae9811 | ||
![]() |
edc1a71d0d | ||
![]() |
ed74403850 | ||
![]() |
9454c05b7e | ||
![]() |
cdd7f4b6bb | ||
![]() |
e4819c1753 | ||
![]() |
e8e0a19758 | ||
![]() |
a8a271e6fb | ||
![]() |
fc6aa7cc1c | ||
![]() |
b0140a655f | ||
![]() |
ef7ee02f65 | ||
![]() |
3d1d5c1151 | ||
![]() |
e0c56c6342 | ||
![]() |
2ceada84a6 | ||
![]() |
5b50a113d1 | ||
![]() |
fe52cc7797 | ||
![]() |
3923f2baf4 | ||
![]() |
0157ade1c9 | ||
![]() |
a863c743c4 | ||
![]() |
62d3b6a3f2 | ||
![]() |
e435274329 | ||
![]() |
55380298f1 | ||
![]() |
71e65119ae | ||
![]() |
dc788043ff | ||
![]() |
a98c465c9a | ||
![]() |
dace0671c2 | ||
![]() |
0882f9dd3c | ||
![]() |
c734e13753 | ||
![]() |
b54483db51 | ||
![]() |
5d0bf2e80b | ||
![]() |
46e694a5c8 | ||
![]() |
e0756a56a4 | ||
![]() |
55e2a2fd5d | ||
![]() |
eecc257822 | ||
![]() |
36f2998c91 | ||
![]() |
131926cad8 | ||
![]() |
d588327c1c | ||
![]() |
68c7f502b4 | ||
![]() |
06fa669f8c | ||
![]() |
d3885eefda | ||
![]() |
74e551cf1a | ||
![]() |
a1333d8d6b | ||
![]() |
3dfb40b57e | ||
![]() |
316cd7710f | ||
![]() |
cbefc5b81e | ||
![]() |
233e161327 | ||
![]() |
7cbbc33319 | ||
![]() |
61cc0a1864 | ||
![]() |
99f01475a0 | ||
![]() |
481e029600 | ||
![]() |
5ff22467a6 | ||
![]() |
3fd44a1813 | ||
![]() |
224a354480 | ||
![]() |
18cdf5d89d | ||
![]() |
293afa6059 | ||
![]() |
06fbcde815 | ||
![]() |
4195a72621 | ||
![]() |
d3bf001752 | ||
![]() |
d4604cba34 | ||
![]() |
221b4a9391 | ||
![]() |
5f5d4d7987 | ||
![]() |
8a17fe927f | ||
![]() |
090d1e900e | ||
![]() |
d0f9d4de02 | ||
![]() |
6bc1ac1156 | ||
![]() |
c77e403f27 | ||
![]() |
5178a288ca | ||
![]() |
7c6702057d | ||
![]() |
c1aad3882c | ||
![]() |
8cf30893f2 | ||
![]() |
da9af181dd | ||
![]() |
92e3c028bb | ||
![]() |
cf7d194dbf | ||
![]() |
eae6a34bfc | ||
![]() |
d9f851f0c1 | ||
![]() |
9590dce8a4 | ||
![]() |
5af77449fa | ||
![]() |
2c39ab01ea | ||
![]() |
035d70cf57 | ||
![]() |
8c76e258d7 | ||
![]() |
f05c7ffbfa | ||
![]() |
a49fcb0176 | ||
![]() |
87503b875a | ||
![]() |
d89d770e73 | ||
![]() |
083d64d151 | ||
![]() |
281f379ced | ||
![]() |
9dab48d135 | ||
![]() |
56221ebc95 | ||
![]() |
0b4b1cb3f3 | ||
![]() |
30e417a614 | ||
![]() |
538de08c07 | ||
![]() |
f540645d80 | ||
![]() |
a58743b820 | ||
![]() |
0ed1499d02 | ||
![]() |
5158a710dc | ||
![]() |
413b6cc43c | ||
![]() |
82a10f2558 | ||
![]() |
b97acb3174 | ||
![]() |
451976c3c8 | ||
![]() |
8e6494c451 | ||
![]() |
3579f9e0c4 | ||
![]() |
39d442a05d | ||
![]() |
0045206735 | ||
![]() |
7f46767411 | ||
![]() |
5414691f1f | ||
![]() |
13731fb094 | ||
![]() |
40ab9d9366 | ||
![]() |
93d6129732 | ||
![]() |
776e214789 | ||
![]() |
d8f6621c7d | ||
![]() |
aca2b4423e | ||
![]() |
204e3f8e3e | ||
![]() |
f54443d7f8 | ||
![]() |
3d82ad9222 | ||
![]() |
393c66385f | ||
![]() |
b008195abd | ||
![]() |
ad2c11413e | ||
![]() |
1e2853c950 | ||
![]() |
171e5d3766 | ||
![]() |
46535cc7a8 | ||
![]() |
16b40a731a | ||
![]() |
9b4b9334a3 | ||
![]() |
96d40d9816 | ||
![]() |
407b003cd6 | ||
![]() |
8129d5e6af | ||
![]() |
c12454a8e9 | ||
![]() |
0f49d412b5 | ||
![]() |
b33551e62e | ||
![]() |
8bb4d149b6 | ||
![]() |
854fda4530 | ||
![]() |
0deea7133d | ||
![]() |
1eaa8afc5f | ||
![]() |
1fc364fa07 | ||
![]() |
f275ba36a1 | ||
![]() |
1aa741a25b | ||
![]() |
6b2f619096 | ||
![]() |
f74ddc1010 | ||
![]() |
6a240919bf | ||
![]() |
2518867d50 | ||
![]() |
046b95d5b1 | ||
![]() |
12dab27729 | ||
![]() |
29964aa90c | ||
![]() |
f0f7b86c7d | ||
![]() |
2d437975a8 | ||
![]() |
a9502a11d6 | ||
![]() |
6aa502b1a5 | ||
![]() |
ea40d212c2 | ||
![]() |
befed2200e | ||
![]() |
7afe1807b7 | ||
![]() |
91478aa128 | ||
![]() |
424b36ffca | ||
![]() |
52f7dc71e6 | ||
![]() |
402e6e7433 | ||
![]() |
6592906d0d | ||
![]() |
678f4102a5 | ||
![]() |
c70e3bb3dd | ||
![]() |
b089355b5a | ||
![]() |
00cf1badfc | ||
![]() |
07159ea5df | ||
![]() |
52f3b8b507 | ||
![]() |
d1387e50e2 | ||
![]() |
65112b6008 | ||
![]() |
4e4dc30a91 | ||
![]() |
70292dac00 | ||
![]() |
e46cb2f240 | ||
![]() |
9fee86df59 | ||
![]() |
19b61a32af | ||
![]() |
60091a2b5e | ||
![]() |
68d246a3e9 | ||
![]() |
9749f59b5a | ||
![]() |
15c09a0f03 | ||
![]() |
971212e7e9 | ||
![]() |
807d461289 | ||
![]() |
e033b72942 | ||
![]() |
98b476de71 | ||
![]() |
8fd2af1870 | ||
![]() |
2a1849d027 | ||
![]() |
d04b759913 | ||
![]() |
2772e705ae | ||
![]() |
d44a4e8ae2 | ||
![]() |
53a40fa914 | ||
![]() |
86d1d4898a | ||
![]() |
e4af2d5e86 | ||
![]() |
d124e2ed15 | ||
![]() |
00c9c14efb | ||
![]() |
6fc66b7b13 | ||
![]() |
9658c2d553 | ||
![]() |
577761a6bb | ||
![]() |
6c610c509f | ||
![]() |
1eacc26ef9 | ||
![]() |
5f33516054 | ||
![]() |
0766205cb0 | ||
![]() |
0825626fc9 | ||
![]() |
470352ddc8 | ||
![]() |
5ccff7b406 | ||
![]() |
66c03c6798 | ||
![]() |
d7912c6973 | ||
![]() |
afa20c92e8 | ||
![]() |
95562a8965 | ||
![]() |
a1abbd406d | ||
![]() |
b05d125db2 | ||
![]() |
54efe5a351 | ||
![]() |
8594fa8199 | ||
![]() |
238667d989 | ||
![]() |
87536619e3 | ||
![]() |
84ab076fe8 | ||
![]() |
57f3cee390 | ||
![]() |
714aea2e27 | ||
![]() |
52081f4a2a | ||
![]() |
b88b422e2f | ||
![]() |
d3499e71ef | ||
![]() |
c7d94548be | ||
![]() |
c866932861 | ||
![]() |
317bd2fcec | ||
![]() |
b06ba06f40 | ||
![]() |
dc52a8a08b | ||
![]() |
5f1e7ffd0c | ||
![]() |
21a80650b0 | ||
![]() |
3b7ec3d913 | ||
![]() |
f1c89f0917 | ||
![]() |
8692ea1a74 | ||
![]() |
a5c25ce438 | ||
![]() |
12d494bf38 | ||
![]() |
f0f9b93694 | ||
![]() |
a97f5a8488 | ||
![]() |
a4224555d2 | ||
![]() |
1b0055ac61 | ||
![]() |
7dcde3e585 | ||
![]() |
4fe33268ac | ||
![]() |
b84abea670 | ||
![]() |
a5fcc7d65f | ||
![]() |
313d9a4ef9 | ||
![]() |
658aace8f1 | ||
![]() |
49562d43c3 | ||
![]() |
4241b62760 | ||
![]() |
47fcee4df8 | ||
![]() |
6c2eae29f5 | ||
![]() |
3869cb9bf3 | ||
![]() |
89c871f171 | ||
![]() |
12c95e6fca | ||
![]() |
2affb9b863 | ||
![]() |
710a85429e | ||
![]() |
2efa6f5cb1 | ||
![]() |
3f1a103e82 | ||
![]() |
1ce87ecfa8 | ||
![]() |
e102d256b2 | ||
![]() |
3b6b79229f | ||
![]() |
70c1e57ff0 | ||
![]() |
ad02f384d2 | ||
![]() |
0efa6a5816 | ||
![]() |
6edabeeecc | ||
![]() |
b96559b76f | ||
![]() |
3a9ded9315 | ||
![]() |
ee97eb6d27 | ||
![]() |
16f7dafcdf | ||
![]() |
1cce926ef8 | ||
![]() |
1cf50e5fcd | ||
![]() |
d8654edd2c | ||
![]() |
f3b97dedc8 | ||
![]() |
b45bbff97c | ||
![]() |
23284e1f35 | ||
![]() |
c9c6a4b867 | ||
![]() |
069c59d3d7 | ||
![]() |
4f2352355f | ||
![]() |
984f50b792 | ||
![]() |
426309a8f5 | ||
![]() |
b4b4694eb5 | ||
![]() |
5505a8609d | ||
![]() |
030ad0a8de | ||
![]() |
71d05a6fc2 | ||
![]() |
867cf478b1 | ||
![]() |
b43a05b662 | ||
![]() |
92db3878cf | ||
![]() |
b6d11d6be3 | ||
![]() |
669572b0e6 | ||
![]() |
5887f90202 | ||
![]() |
81d50ed3bb | ||
![]() |
9bdf3ad0c8 | ||
![]() |
0b38d73cc5 | ||
![]() |
7a3dbf626e | ||
![]() |
767ec1795a | ||
![]() |
ecbf230293 | ||
![]() |
839b63e389 | ||
![]() |
73078531aa | ||
![]() |
c5c3f7a667 | ||
![]() |
8738817eab | ||
![]() |
33877c73bb | ||
![]() |
0e3698fa2f | ||
![]() |
65f48800b9 | ||
![]() |
7738e6a381 | ||
![]() |
ca3b01f898 | ||
![]() |
e05c432d14 | ||
![]() |
53a8c2d574 | ||
![]() |
6783fb9010 | ||
![]() |
a7adb517ca | ||
![]() |
b7a66295f6 | ||
![]() |
27f2c44532 | ||
![]() |
efd1efad4d | ||
![]() |
b2ea4c758d | ||
![]() |
cfce057783 | ||
![]() |
5854beebc6 | ||
![]() |
879eb6295d | ||
![]() |
3ae761dd9d | ||
![]() |
f5ab2f63c0 | ||
![]() |
c89170a22d | ||
![]() |
84b36e72e4 | ||
![]() |
9298922601 | ||
![]() |
9913e7198c | ||
![]() |
9bd10d5102 | ||
![]() |
4c62840cef | ||
![]() |
e6f0a86c5a | ||
![]() |
c9da6d2ee2 | ||
![]() |
f33350a4ef | ||
![]() |
debe9959e4 | ||
![]() |
a357b021ba | ||
![]() |
f7099b2bc7 | ||
![]() |
8d3b1d10d2 | ||
![]() |
f143000f4a | ||
![]() |
7954caf957 | ||
![]() |
a07a31a2bb | ||
![]() |
405394707f | ||
![]() |
f13b8d1557 | ||
![]() |
6e1f4fada9 | ||
![]() |
892ef23ca0 | ||
![]() |
8e1c0a7fa4 | ||
![]() |
113e7e0d3d | ||
![]() |
ea619ce99a | ||
![]() |
e9c7d6a0cc | ||
![]() |
2f57e9cbee | ||
![]() |
0de16855b0 | ||
![]() |
4fedace179 | ||
![]() |
8d2da6f30b | ||
![]() |
8f427b727c | ||
![]() |
f1f0332d22 | ||
![]() |
7c0541034a | ||
![]() |
e8895a4d76 | ||
![]() |
a42bcfceb6 | ||
![]() |
fcda2e8706 | ||
![]() |
efe3680cd7 | ||
![]() |
272e30d761 | ||
![]() |
30940fad2e | ||
![]() |
d772280147 | ||
![]() |
7475fa69fa | ||
![]() |
ec8b53263c | ||
![]() |
0f567385a8 | ||
![]() |
dae1197ce4 | ||
![]() |
1296ab9b60 | ||
![]() |
b1575a979e | ||
![]() |
d677eb1698 | ||
![]() |
71ad1d9906 | ||
![]() |
731b79fc6c | ||
![]() |
e001448adb | ||
![]() |
3041064d21 | ||
![]() |
30134dbbc9 | ||
![]() |
4208885e36 | ||
![]() |
656f48b354 | ||
![]() |
68eb9a37ca | ||
![]() |
6c4ce03d59 | ||
![]() |
86e4679e04 | ||
![]() |
71806e6f95 | ||
![]() |
7e8a93e974 | ||
![]() |
b87ad9cd2f | ||
![]() |
10bab2b6e4 | ||
![]() |
5aab10fab2 | ||
![]() |
77aa23a375 | ||
![]() |
b09da2641e | ||
![]() |
26b0a7d75d | ||
![]() |
b6bf38b85c | ||
![]() |
b6424b4596 | ||
![]() |
dc2051fb59 | ||
![]() |
8bdc61ddcc | ||
![]() |
d4a25230ec | ||
![]() |
e548a4b6aa | ||
![]() |
8607fe43a6 | ||
![]() |
718178c1b6 | ||
![]() |
930205b1c9 | ||
![]() |
ec05e6864a | ||
![]() |
e08a702408 | ||
![]() |
d5b31a5545 | ||
![]() |
811c694f80 | ||
![]() |
8775222a61 | ||
![]() |
3aebdae305 | ||
![]() |
d3122020b9 | ||
![]() |
822ea1b450 | ||
![]() |
515057c7d0 | ||
![]() |
54b93febba | ||
![]() |
5d0600a0af | ||
![]() |
1ca2b40bf9 | ||
![]() |
ff85241962 | ||
![]() |
bc227b5f56 | ||
![]() |
283231ac6e | ||
![]() |
fc46bb65f8 | ||
![]() |
c5ca4dd7b3 | ||
![]() |
c493f732f9 | ||
![]() |
a59fb4fd1e | ||
![]() |
f671e699e6 | ||
![]() |
05fb60f809 | ||
![]() |
e564470490 | ||
![]() |
c7a8a3933e | ||
![]() |
e1fdb90da6 | ||
![]() |
df0562d6b4 | ||
![]() |
9afc39d5f1 | ||
![]() |
4a1c4df861 | ||
![]() |
b054ed6e03 | ||
![]() |
8a02a51d0c | ||
![]() |
da64785ea4 | ||
![]() |
8b91abd0dc | ||
![]() |
b6364941a0 | ||
![]() |
4fdd362ed5 | ||
![]() |
7bff5f562c | ||
![]() |
3de13a2e6d | ||
![]() |
329e885da7 | ||
![]() |
d125551c0d | ||
![]() |
250741e431 | ||
![]() |
22d6a32e4e | ||
![]() |
11c66f6ce4 | ||
![]() |
4ffd642732 | ||
![]() |
f7f6b15ef1 | ||
![]() |
efcb1715ee | ||
![]() |
8f5bcdff99 | ||
![]() |
a0b2a8ba74 | ||
![]() |
3013ee083d | ||
![]() |
0ae14f673c | ||
![]() |
b4eb09d5e9 | ||
![]() |
49d1bc08c3 | ||
![]() |
e9e5c41f79 | ||
![]() |
fac3721dfd | ||
![]() |
e687d5db69 | ||
![]() |
4fa48f7940 | ||
![]() |
f7fa770fc5 | ||
![]() |
a311366ee7 | ||
![]() |
9675ef1293 | ||
![]() |
f004000d5c | ||
![]() |
9fb7e1b46d | ||
![]() |
c2f5a12ce1 | ||
![]() |
671fb3476c | ||
![]() |
c28a168868 | ||
![]() |
3057302baa | ||
![]() |
94bc33f61c | ||
![]() |
e874730679 | ||
![]() |
1ae84c11ad | ||
![]() |
9cb2bc4825 | ||
![]() |
2bd69b2681 | ||
![]() |
f3b17d4896 | ||
![]() |
8f5a1ce1b6 | ||
![]() |
2aa0021a08 | ||
![]() |
81ec684025 | ||
![]() |
247b89aec5 | ||
![]() |
8b09a02028 | ||
![]() |
597c5063f3 | ||
![]() |
4843719c58 | ||
![]() |
d7c1c0d04c | ||
![]() |
4fa43fdf60 | ||
![]() |
cafb9ddcf4 | ||
![]() |
3f6b8def05 | ||
![]() |
40f0ea95ce | ||
![]() |
ca3b02229f | ||
![]() |
8039aa3eb1 | ||
![]() |
5a494ca50e | ||
![]() |
ef8e8c5a30 | ||
![]() |
a19629d105 | ||
![]() |
6e598c74f5 | ||
![]() |
35cf4f3009 | ||
![]() |
150d3df9b5 | ||
![]() |
ceff73ee5b | ||
![]() |
595b2e40d9 | ||
![]() |
9d09401dc1 | ||
![]() |
d9ad3ae2c9 | ||
![]() |
26cb600ae3 | ||
![]() |
435210b73c | ||
![]() |
8dc465f244 | ||
![]() |
8e708dc02a | ||
![]() |
b53b6723cc | ||
![]() |
af4b4ae85c | ||
![]() |
0e84bf95da | ||
![]() |
8e93470b94 | ||
![]() |
7cdb1caaf2 | ||
![]() |
bf346a6ed1 | ||
![]() |
c0101933bd | ||
![]() |
24a7c26819 | ||
![]() |
13019941b3 | ||
![]() |
a0451065a2 | ||
![]() |
54b87c19b1 | ||
![]() |
8828f21f2e | ||
![]() |
7fee135bf5 | ||
![]() |
b69e466de0 | ||
![]() |
37fcf2fed7 | ||
![]() |
8f69575b95 | ||
![]() |
2f71c2b784 | ||
![]() |
f3a809a495 | ||
![]() |
ad955b157b | ||
![]() |
a87b6c7022 | ||
![]() |
5104d0d250 | ||
![]() |
add83c4c7a | ||
![]() |
42b9ec819b | ||
![]() |
e93f9eb080 | ||
![]() |
0244db9182 | ||
![]() |
461f9c7ad6 | ||
![]() |
fd8e1ed623 | ||
![]() |
79b89f8a1b | ||
![]() |
6b33ad8a32 | ||
![]() |
40cf4f2c2a | ||
![]() |
d5ef1488b2 | ||
![]() |
3beab611a3 | ||
![]() |
89a67c0749 | ||
![]() |
fd842c43a6 | ||
![]() |
e51788ac05 | ||
![]() |
9b046ecc75 | ||
![]() |
aa68e69a18 | ||
![]() |
2e573e95e5 | ||
![]() |
e95bc31def | ||
![]() |
4eb843fd8c | ||
![]() |
05db08c8d2 | ||
![]() |
513de18af5 | ||
![]() |
151a6ce9e3 | ||
![]() |
d4a1764d20 | ||
![]() |
59c6ee7ade | ||
![]() |
4fbfa1d579 | ||
![]() |
5285699a21 | ||
![]() |
75879de47a | ||
![]() |
e23dc5bda3 | ||
![]() |
8998bf85bd | ||
![]() |
7b1601c840 | ||
![]() |
026b6a4b01 | ||
![]() |
d927e85eca | ||
![]() |
a877a9fd91 | ||
![]() |
abc481604f | ||
![]() |
c703458e00 | ||
![]() |
75861db84b | ||
![]() |
223957084a | ||
![]() |
8c0c0868e3 | ||
![]() |
a65d21d7e4 | ||
![]() |
82e6bc5d31 | ||
![]() |
09f1ad0625 | ||
![]() |
852aaae52e | ||
![]() |
270922b460 | ||
![]() |
b39da7e356 | ||
![]() |
a438803a5a | ||
![]() |
1b8b3cf1ee | ||
![]() |
c2176ba27d | ||
![]() |
a69f898e12 | ||
![]() |
0139d862b5 | ||
![]() |
0459c75dc0 | ||
![]() |
547242b1cb | ||
![]() |
b8d83c7ec4 | ||
![]() |
5c644d1b36 | ||
![]() |
7d3d0f7ceb | ||
![]() |
101d2ea9b6 | ||
![]() |
af7c7da735 | ||
![]() |
c1397bbaf7 | ||
![]() |
16a9f38844 | ||
![]() |
508d2472e9 | ||
![]() |
f162987a4f | ||
![]() |
17be72e55e | ||
![]() |
d61cd96d8f | ||
![]() |
6daf583b4b | ||
![]() |
b23a4c8288 | ||
![]() |
b1240426cd | ||
![]() |
30ab2cf9a7 | ||
![]() |
0c985a5fda | ||
![]() |
049d326836 | ||
![]() |
960bf3f3bf | ||
![]() |
edf959f26d | ||
![]() |
077d9d3918 | ||
![]() |
76c4a6bb7c | ||
![]() |
ee81aea2fa | ||
![]() |
0cf6fb2c9d | ||
![]() |
9d3913ed70 | ||
![]() |
d275dfea2f | ||
![]() |
11d8485399 | ||
![]() |
6b926b12a4 | ||
![]() |
9adec79401 | ||
![]() |
c3b146611b | ||
![]() |
a454f3de6e | ||
![]() |
b29d17d39a | ||
![]() |
ff3fdd4c72 | ||
![]() |
0533f29e7a | ||
![]() |
75adfebf9f | ||
![]() |
e96435378a | ||
![]() |
ccdfe77356 | ||
![]() |
adf39b6f6a | ||
![]() |
ad1d81dccf | ||
![]() |
8828629a30 | ||
![]() |
1ead9b59ba | ||
![]() |
c401cabee7 | ||
![]() |
1549f2aa4d | ||
![]() |
89beb12d59 | ||
![]() |
4083e552fd | ||
![]() |
3d7dea52a0 | ||
![]() |
c5f77a1c38 | ||
![]() |
3561ddf7d3 | ||
![]() |
81e96d25ba | ||
![]() |
1ad966bd80 | ||
![]() |
4f472982da | ||
![]() |
f1396b65b9 | ||
![]() |
ba12489c34 | ||
![]() |
4586d3be78 | ||
![]() |
2204e0cf9c | ||
![]() |
8787774764 | ||
![]() |
feb00d5b8a | ||
![]() |
660fc982ca | ||
![]() |
1189c76691 | ||
![]() |
27a3414a27 | ||
![]() |
4d717a5c19 | ||
![]() |
e7744eace9 | ||
![]() |
3bdb6a237f | ||
![]() |
98dc46e01a | ||
![]() |
a88f56bec3 | ||
![]() |
fbc10b8e32 | ||
![]() |
42fd7b216d | ||
![]() |
511731f0b4 | ||
![]() |
ad23021b29 | ||
![]() |
05a37b3a9c | ||
![]() |
8ca368f9c7 | ||
![]() |
dd924f389f | ||
![]() |
bbb54d6a6a | ||
![]() |
96e9cc9c32 | ||
![]() |
b1f16857fc | ||
![]() |
738fd1a09b | ||
![]() |
645249afa9 | ||
![]() |
a51d794885 | ||
![]() |
9d1f491a89 | ||
![]() |
e8a0d56ff8 | ||
![]() |
7048987157 | ||
![]() |
e03731fd24 | ||
![]() |
7097859b79 | ||
![]() |
e87df8a57a | ||
![]() |
982fb27f58 | ||
![]() |
0d1a5846a1 | ||
![]() |
b9e79f8d98 | ||
![]() |
666ef49924 | ||
![]() |
d2ea8a001f | ||
![]() |
16efba2b91 | ||
![]() |
e93b5c3c1c | ||
![]() |
9dabd1f344 | ||
![]() |
e96dd14e0d | ||
![]() |
3e970df09b | ||
![]() |
b0a8c4d278 | ||
![]() |
b55832df34 | ||
![]() |
4c7013644f | ||
![]() |
4859ffdf54 | ||
![]() |
bca5d8a6c5 | ||
![]() |
af57083afd | ||
![]() |
f10233a0a6 | ||
![]() |
4b9455e84d | ||
![]() |
9305612419 | ||
![]() |
357fd895bf | ||
![]() |
d7ae583473 | ||
![]() |
f8f4718474 | ||
![]() |
304d2f0874 | ||
![]() |
bda31aea59 | ||
![]() |
dea669ac26 | ||
![]() |
40693471e9 | ||
![]() |
bee8aac0cc | ||
![]() |
2f2c49cc47 | ||
![]() |
b57a9f5c58 | ||
![]() |
6afe2dd720 | ||
![]() |
aef3ecc3f2 | ||
![]() |
01ce86ce18 | ||
![]() |
99362c78bf | ||
![]() |
c0bf9a9a76 | ||
![]() |
1833b3e89f | ||
![]() |
6b71f91775 | ||
![]() |
5275988f37 | ||
![]() |
93162beaa0 | ||
![]() |
61699c7477 | ||
![]() |
a1a2434d98 | ||
![]() |
fd56ddaa54 | ||
![]() |
c438b9f8a5 | ||
![]() |
91e83b3397 | ||
![]() |
50bfc8db09 | ||
![]() |
3d0a20452f | ||
![]() |
b60e6a7d85 | ||
![]() |
c5b8495477 | ||
![]() |
809632794f | ||
![]() |
62ecc1cbc1 | ||
![]() |
cd822156c9 | ||
![]() |
e85c5e3b36 | ||
![]() |
194d57c85f | ||
![]() |
02ab6d8f59 | ||
![]() |
a548426c39 | ||
![]() |
5f851c7880 | ||
![]() |
f91e264889 | ||
![]() |
514f8396b7 | ||
![]() |
d5eac963a9 | ||
![]() |
c39ff89fab | ||
![]() |
3f19f64efd | ||
![]() |
9135a1a1cc | ||
![]() |
39366bfbf1 | ||
![]() |
5eeec0a828 | ||
![]() |
4f28a0f84b | ||
![]() |
56f057143e | ||
![]() |
39568737dd | ||
![]() |
13e0d9b306 | ||
![]() |
d6b7159db1 | ||
![]() |
2575476086 | ||
![]() |
211d68f590 | ||
![]() |
3e51b888cb | ||
![]() |
ff8f41c1eb | ||
![]() |
c6fd61ddf3 | ||
![]() |
9ca4b823bb | ||
![]() |
f513737628 | ||
![]() |
90051972d4 | ||
![]() |
6fa37c618b | ||
![]() |
47db295088 | ||
![]() |
6405ca5cef | ||
![]() |
2ba74d9232 | ||
![]() |
ed09595a87 | ||
![]() |
7c35bcdead | ||
![]() |
189627c89e | ||
![]() |
07bf8ef3b1 | ||
![]() |
32d902d37f | ||
![]() |
3028a8ebc9 | ||
![]() |
5c503203dc | ||
![]() |
644f65b5e4 | ||
![]() |
f593199af7 | ||
![]() |
26a5f12a8e | ||
![]() |
f3257a6743 | ||
![]() |
9f35efc31d | ||
![]() |
98c6add637 | ||
![]() |
3904dcda39 | ||
![]() |
6376f7dd56 | ||
![]() |
ae0a78dfb1 | ||
![]() |
1ad021ffa9 | ||
![]() |
ee6ab8e82a | ||
![]() |
b56c985a63 | ||
![]() |
abbf6ae2c8 | ||
![]() |
403ec56ef8 | ||
![]() |
a6e67fa9c3 | ||
![]() |
b6a401d697 | ||
![]() |
23be1d18a4 | ||
![]() |
d721d459eb | ||
![]() |
e1dd0f0979 | ||
![]() |
866947c445 | ||
![]() |
8ff3bbc9a7 | ||
![]() |
498191261c | ||
![]() |
f9091ea951 | ||
![]() |
8b9d75d726 | ||
![]() |
7e4a4459b4 | ||
![]() |
70fd33e587 | ||
![]() |
a265c2a7ca | ||
![]() |
fee5766abf | ||
![]() |
03573e46f5 | ||
![]() |
64b4789bc1 | ||
![]() |
13e651c822 | ||
![]() |
7d3fd6f5b6 | ||
![]() |
0c91fd396e | ||
![]() |
1827e1e033 | ||
![]() |
771dce4c8c | ||
![]() |
ab2fe909f0 | ||
![]() |
6fb9c6ef05 | ||
![]() |
9b41950e4c | ||
![]() |
bc09b6280b | ||
![]() |
3e7a4f593a | ||
![]() |
679a88c095 | ||
![]() |
97d1d0d63b | ||
![]() |
ee30f78ab0 | ||
![]() |
566b689ca6 | ||
![]() |
c2edbb30b5 | ||
![]() |
a6f86329c5 | ||
![]() |
05c1df81ad | ||
![]() |
327badb3ca | ||
![]() |
335a7a76b1 | ||
![]() |
1566da1f8f | ||
![]() |
1250b45694 | ||
![]() |
48e8b956d2 | ||
![]() |
56e80f7c0c | ||
![]() |
bd6cb3e5be | ||
![]() |
f2db4b2d1b | ||
![]() |
4a823c57d5 | ||
![]() |
2f74f64d65 | ||
![]() |
d44677e46f | ||
![]() |
4afb6b3d9c | ||
![]() |
18d3e8905e | ||
![]() |
fd14a4ac80 | ||
![]() |
cc48a0ad86 | ||
![]() |
aad1a836db | ||
![]() |
3d3bcaba7a | ||
![]() |
201169c4d7 | ||
![]() |
1ff6c701e8 | ||
![]() |
3fb9f900f6 | ||
![]() |
9c4093dc7c | ||
![]() |
acfe4deacf | ||
![]() |
342f9f6096 | ||
![]() |
f5b4936197 | ||
![]() |
0c6c7d2725 | ||
![]() |
136772073d | ||
![]() |
f5846853b3 | ||
![]() |
6db011d807 | ||
![]() |
fe59352800 | ||
![]() |
dedf6cd92f | ||
![]() |
99fdbcd1fe | ||
![]() |
005358f4bf | ||
![]() |
ea1cd63929 | ||
![]() |
c8ac4c0278 | ||
![]() |
49a332724e | ||
![]() |
a21c417c0b | ||
![]() |
155f05cb63 | ||
![]() |
6d644922d5 | ||
![]() |
b0a125c113 | ||
![]() |
d17cb3fafc | ||
![]() |
814fa881d0 | ||
![]() |
fd451ae766 | ||
![]() |
089bb92f70 | ||
![]() |
5b6e1f699d | ||
![]() |
03c469ff3a | ||
![]() |
c122dc108a | ||
![]() |
22c682f393 | ||
![]() |
d7b6c54b08 | ||
![]() |
a715650af6 | ||
![]() |
a72da8af56 | ||
![]() |
9b3a50c124 | ||
![]() |
5d7b8cfd48 | ||
![]() |
6318c0889a | ||
![]() |
0aae28e836 | ||
![]() |
b298571395 | ||
![]() |
4f6a496c36 | ||
![]() |
a3a7af73bd | ||
![]() |
520fd6e063 | ||
![]() |
bcc3b4d3ac | ||
![]() |
5ff8d6be38 | ||
![]() |
0f5658e65e | ||
![]() |
b89eb4f1e8 | ||
![]() |
9546817ad2 | ||
![]() |
6a31ce3e2e | ||
![]() |
905095317c | ||
![]() |
d8ff0e1745 | ||
![]() |
f9ac614147 | ||
![]() |
581a18f83d | ||
![]() |
f2c76c0a3b | ||
![]() |
7121a96ebd | ||
![]() |
8986d8037a | ||
![]() |
d97631fb8c | ||
![]() |
be1eb14fd0 | ||
![]() |
92e353be40 | ||
![]() |
c595ba3dc0 | ||
![]() |
ccd002b97f | ||
![]() |
c7d067ea3c | ||
![]() |
adbf787bd4 | ||
![]() |
7ee5cbe473 | ||
![]() |
c9c1d10ee4 | ||
![]() |
b5452bce6b | ||
![]() |
6628b07bba | ||
![]() |
8f34afc630 | ||
![]() |
1ac69ae315 | ||
![]() |
8a331e7400 | ||
![]() |
ca42740e1b | ||
![]() |
1d5c0597d9 | ||
![]() |
7576e60797 | ||
![]() |
acc734d22d | ||
![]() |
8ab7d26cd1 | ||
![]() |
e86375f721 | ||
![]() |
fde44d78d7 | ||
![]() |
a2567277c6 | ||
![]() |
feda6f88ff | ||
![]() |
74e5f5310b | ||
![]() |
51fdf95295 | ||
![]() |
0009b1b7d1 | ||
![]() |
74e50e209a | ||
![]() |
1208294747 | ||
![]() |
3de182a4ed | ||
![]() |
90ff9ac7fb | ||
![]() |
6b92d011d2 | ||
![]() |
8b9a8997ac | ||
![]() |
9df86ecaa9 | ||
![]() |
cfeb2797e3 | ||
![]() |
06a50b189d | ||
![]() |
7c1a4e353d | ||
![]() |
2b611ae798 | ||
![]() |
fdd82f7ac4 | ||
![]() |
ea9d345548 | ||
![]() |
24372470f6 | ||
![]() |
51f1781df4 | ||
![]() |
972bef4f7d | ||
![]() |
a2da8eca68 | ||
![]() |
620d8caea9 | ||
![]() |
4ddaa9b57f | ||
![]() |
22bb77e7a0 | ||
![]() |
3582b2c114 | ||
![]() |
9db715ee17 | ||
![]() |
1aaa1ba975 | ||
![]() |
35bd5e8595 | ||
![]() |
86e607c403 | ||
![]() |
c7247fc462 | ||
![]() |
25add764e4 | ||
![]() |
7d8490bdcb | ||
![]() |
f8fe426f70 | ||
![]() |
9b61ae44ab | ||
![]() |
991e59d0fc | ||
![]() |
7f3cf9583c | ||
![]() |
ff7ac90785 | ||
![]() |
b01e381087 | ||
![]() |
5026f1b0a8 | ||
![]() |
9d09890457 | ||
![]() |
4483180569 | ||
![]() |
4465146615 | ||
![]() |
3b7750c24e | ||
![]() |
7962baa2da | ||
![]() |
6f37654303 | ||
![]() |
2781e31be0 | ||
![]() |
b39d86d020 | ||
![]() |
754a3db1b9 | ||
![]() |
bc1d79a07d | ||
![]() |
8242e91238 | ||
![]() |
ff24899a62 | ||
![]() |
c3250378ee | ||
![]() |
9d4515954b | ||
![]() |
46455737cc | ||
![]() |
09413f5fc7 | ||
![]() |
2e74b73ba0 | ||
![]() |
818983831a | ||
![]() |
c64e97f0d6 | ||
![]() |
9034449e96 | ||
![]() |
00b6842f35 | ||
![]() |
99e5af67b7 | ||
![]() |
e8f049a765 | ||
![]() |
53afd97346 | ||
![]() |
604792a4dd | ||
![]() |
9b83c683b5 | ||
![]() |
9d511ba165 | ||
![]() |
c519239d87 | ||
![]() |
9f39bee486 | ||
![]() |
fa409e63f8 | ||
![]() |
e048fa6c6a | ||
![]() |
940d583f9a | ||
![]() |
ed965041e6 | ||
![]() |
1a5e661e14 | ||
![]() |
0f7ed0e04d | ||
![]() |
d50603f722 | ||
![]() |
e3931e8892 | ||
![]() |
4efcfa8bf4 | ||
![]() |
9551d11f5c | ||
![]() |
b40ef09108 | ||
![]() |
95d9d4ee9a | ||
![]() |
75cf8bbb0a | ||
![]() |
c40f0c6919 | ||
![]() |
231df44c94 | ||
![]() |
807dc866e5 | ||
![]() |
3d5c25efe4 | ||
![]() |
5a7a80f932 | ||
![]() |
3f52f78af2 | ||
![]() |
a66b71fb9c | ||
![]() |
65eff6b6d9 | ||
![]() |
027ee83c6f | ||
![]() |
60af54a17e | ||
![]() |
19c335e39a | ||
![]() |
d32303de57 | ||
![]() |
de360a8585 | ||
![]() |
24c1596048 | ||
![]() |
9484a27801 | ||
![]() |
c8eb93db07 | ||
![]() |
3c8867bf72 | ||
![]() |
ef2f1d56d4 | ||
![]() |
046877599e | ||
![]() |
b322b750c2 | ||
![]() |
4a75d9d4f5 | ||
![]() |
68ea4d02cb | ||
![]() |
8b3ff6454f | ||
![]() |
de28643e20 | ||
![]() |
3030e00b21 | ||
![]() |
7ba3d86af4 | ||
![]() |
2f3b3ebf2b | ||
![]() |
136a0fbe28 | ||
![]() |
7905e12fc3 | ||
![]() |
07caaa6b48 | ||
![]() |
59a04f101d | ||
![]() |
4265c4126a | ||
![]() |
0e85beec30 | ||
![]() |
f1276beb97 | ||
![]() |
365ba9b5f4 | ||
![]() |
f997a69ebc | ||
![]() |
e2b6dd37b7 | ||
![]() |
bc9bddf918 | ||
![]() |
b41570b663 | ||
![]() |
4c6e01d38d | ||
![]() |
8ef1ef0ce5 | ||
![]() |
e4fc4ca410 | ||
![]() |
0b9bc56959 | ||
![]() |
e9beeb85fc | ||
![]() |
a71e82c315 | ||
![]() |
d176150248 | ||
![]() |
ab9fa9ec0c | ||
![]() |
3d57fbb1b8 | ||
![]() |
f0f9240fc8 | ||
![]() |
b1bfe39cd5 | ||
![]() |
c82b19687a | ||
![]() |
be755fe25e | ||
![]() |
8e2be5cfe2 | ||
![]() |
cf1154f2c5 | ||
![]() |
f7ce401564 | ||
![]() |
5dbaf02e33 | ||
![]() |
2ef537ee6c | ||
![]() |
74048ce53f | ||
![]() |
96ceb253e8 | ||
![]() |
aa9e8b067f | ||
![]() |
e5963f8a76 | ||
![]() |
e5b851b171 | ||
![]() |
282f06b65e | ||
![]() |
e8a439cad3 | ||
![]() |
cd025316db | ||
![]() |
6e55c8b7c7 | ||
![]() |
38c663ee95 | ||
![]() |
ca473d6c3b | ||
![]() |
36b74e1c6a | ||
![]() |
8e3e96fe65 | ||
![]() |
0f0f3894ff | ||
![]() |
a2182e8a8d | ||
![]() |
4042de460e | ||
![]() |
0dd34403a2 | ||
![]() |
edb9d3f719 | ||
![]() |
1f5db0aa24 | ||
![]() |
74b2408668 | ||
![]() |
f53612bef7 | ||
![]() |
4d991cda6d | ||
![]() |
6984575901 | ||
![]() |
75b44f6980 | ||
![]() |
3c46f60042 | ||
![]() |
7dbc323f76 | ||
![]() |
903b62ba7c | ||
![]() |
53da40fe5d | ||
![]() |
fdb7b4cc0f | ||
![]() |
d0f85f3d04 | ||
![]() |
49d863fa54 | ||
![]() |
6fe5e2b751 | ||
![]() |
59d45f445a | ||
![]() |
09d19da3bc | ||
![]() |
1036d0e4a0 | ||
![]() |
8ba4201237 | ||
![]() |
7c69118c2c | ||
![]() |
6238bb7f2d | ||
![]() |
42ea01dde9 | ||
![]() |
c0b8e802ec | ||
![]() |
43bd8a2d6c | ||
![]() |
e85efe2c64 | ||
![]() |
988d169687 | ||
![]() |
43e6f374fa | ||
![]() |
c91011e6ea | ||
![]() |
f45c0b2377 | ||
![]() |
9b8eeb6a73 | ||
![]() |
468fe8f266 | ||
![]() |
5ccdfbd40a | ||
![]() |
ad969dcca7 | ||
![]() |
08b31416f3 | ||
![]() |
c494b01435 | ||
![]() |
c0e69e91a1 | ||
![]() |
1c029981ae | ||
![]() |
f9e64bc874 | ||
![]() |
8d26a572dd | ||
![]() |
936371a7ec | ||
![]() |
628cc992e9 | ||
![]() |
636813c603 | ||
![]() |
2eb6cc8f55 | ||
![]() |
8ae9b5261e | ||
![]() |
d6d9e911fe | ||
![]() |
797abdea80 | ||
![]() |
7153982981 | ||
![]() |
fcfd428b54 | ||
![]() |
20e3dc5fa7 | ||
![]() |
32ca6ac1a7 | ||
![]() |
02ad7542b3 | ||
![]() |
57c042ef79 | ||
![]() |
614a18913e | ||
![]() |
bfb7392198 | ||
![]() |
7eca3f4bc5 | ||
![]() |
52a468d9fd | ||
![]() |
2cf86642a2 | ||
![]() |
e76164c7a9 | ||
![]() |
bf0d359116 | ||
![]() |
5f37bf3eef | ||
![]() |
d8453384ab | ||
![]() |
f4cdc31788 | ||
![]() |
20a063105c | ||
![]() |
f8f26f0a89 | ||
![]() |
522e9ebf39 | ||
![]() |
27a9ea78b8 | ||
![]() |
7fa019321c | ||
![]() |
f98f52a50e | ||
![]() |
bc71555a73 | ||
![]() |
d3d7489232 | ||
![]() |
94cd5d32e5 | ||
![]() |
db339eecd3 | ||
![]() |
99dd5d5f63 | ||
![]() |
53cd2cdbf3 | ||
![]() |
4bbea46b90 | ||
![]() |
38230c62e6 | ||
![]() |
932db9bbe4 | ||
![]() |
bfd3d18a10 | ||
![]() |
b584c586ec | ||
![]() |
11463dbba5 | ||
![]() |
8fdfba97d9 | ||
![]() |
158f9e37e5 | ||
![]() |
28fbf5a802 | ||
![]() |
905976a1b6 | ||
![]() |
5ad614743b | ||
![]() |
ebdb1ed322 | ||
![]() |
b1989ef02e | ||
![]() |
ded6025eee | ||
![]() |
d591df39cc | ||
![]() |
d44f08382a | ||
![]() |
5e926ec98e | ||
![]() |
a27422a0fb | ||
![]() |
2af08c0ba1 | ||
![]() |
b6c0c955c5 | ||
![]() |
32a0f96ecd | ||
![]() |
37ef2802de | ||
![]() |
9f762a0230 | ||
![]() |
f1c320dd22 | ||
![]() |
4c7500e248 | ||
![]() |
8487056edb | ||
![]() |
bf69d24566 | ||
![]() |
1fc2a917d4 | ||
![]() |
c0a96983a7 | ||
![]() |
a4132c9cb1 | ||
![]() |
5c5e4b914c | ||
![]() |
1db2327b3d | ||
![]() |
0a380dc0ab | ||
![]() |
29aa1b7b93 | ||
![]() |
5a08545e2d | ||
![]() |
65d68fb8ac | ||
![]() |
d093764731 | ||
![]() |
24982aee42 | ||
![]() |
ef4a4acbc0 | ||
![]() |
b503d59c49 | ||
![]() |
f451ce6c91 | ||
![]() |
4f729aa98f | ||
![]() |
bdb5e1597c | ||
![]() |
6a1c5d96ac | ||
![]() |
c1aff56cf2 | ||
![]() |
bfd57561b7 | ||
![]() |
4cd393e4d8 | ||
![]() |
ac230219ee | ||
![]() |
c96dce3d0b | ||
![]() |
9e13994c54 | ||
![]() |
b8d68e2589 | ||
![]() |
5f753d41d4 | ||
![]() |
d2d335314d | ||
![]() |
545f4964a1 | ||
![]() |
7e88c4431a | ||
![]() |
e1d3a31c6c | ||
![]() |
231195ba0a | ||
![]() |
6cb700b302 | ||
![]() |
504b147cee | ||
![]() |
d9b3078f7b | ||
![]() |
b35bff4164 | ||
![]() |
5b6d4f972e | ||
![]() |
eb97ea4f34 | ||
![]() |
a69531c4c9 | ||
![]() |
527c2b4f54 | ||
![]() |
467cd480cf | ||
![]() |
6610914fdb | ||
![]() |
fa96ab453f | ||
![]() |
5bdc3b4fb5 | ||
![]() |
eee073c103 | ||
![]() |
35efd6b107 | ||
![]() |
f45ecb6cf4 | ||
![]() |
db8fe8d890 | ||
![]() |
bd9f88416c | ||
![]() |
cbac57dc88 | ||
![]() |
504d7812e5 | ||
![]() |
b88ee9a87a | ||
![]() |
2aba9cc3c2 | ||
![]() |
961fcbf660 | ||
![]() |
56c9ac8e46 | ||
![]() |
1f7b25b60c | ||
![]() |
34ec05d4b4 | ||
![]() |
bf672bdec3 | ||
![]() |
bb27e3124f | ||
![]() |
551aa3ef67 | ||
![]() |
de550d7cae | ||
![]() |
f50ec0eeee | ||
![]() |
6e51b5ba32 | ||
![]() |
8281176873 | ||
![]() |
eaa344abca | ||
![]() |
1f22009360 | ||
![]() |
7d7272a266 | ||
![]() |
83e372ce47 | ||
![]() |
8db16a1421 | ||
![]() |
21d0b829d3 | ||
![]() |
08219a0285 | ||
![]() |
d426b6bb02 | ||
![]() |
688c240b35 | ||
![]() |
b82cf8d08d | ||
![]() |
c44cb11800 | ||
![]() |
b9e0275417 | ||
![]() |
dc82f7ab3e | ||
![]() |
a6ce2a0253 | ||
![]() |
a0d85d7d83 | ||
![]() |
6a4882d81d | ||
![]() |
ac1a03d17b | ||
![]() |
0e930efd95 | ||
![]() |
d281f112d9 | ||
![]() |
5a42cb92cd | ||
![]() |
3459d35ed2 | ||
![]() |
699a8be721 | ||
![]() |
1f4a5643db | ||
![]() |
492408def7 | ||
![]() |
74763465a8 | ||
![]() |
c069d4f67c | ||
![]() |
bf41c7b651 | ||
![]() |
3f5a5b4f9b | ||
![]() |
b60a1ec455 | ||
![]() |
13157eea1e | ||
![]() |
dd11f87673 | ||
![]() |
87910a236b | ||
![]() |
096675c87e | ||
![]() |
7d3beeb9e0 | ||
![]() |
96a097e33d | ||
![]() |
1bb3140f5b | ||
![]() |
6bf3d34c6c | ||
![]() |
3cd7e4e48e | ||
![]() |
2777c4c537 | ||
![]() |
e525ea2431 | ||
![]() |
c8f37674c6 | ||
![]() |
12ca870e38 | ||
![]() |
f630e6c25e | ||
![]() |
7c6bea6ddd | ||
![]() |
efbef25c76 | ||
![]() |
1384da4691 | ||
![]() |
26a250d1df | ||
![]() |
426fe561c9 | ||
![]() |
5950b04c40 | ||
![]() |
434cfce32a | ||
![]() |
febdea6c64 | ||
![]() |
6c0062dbc1 | ||
![]() |
49735b7e55 | ||
![]() |
1d6168f457 | ||
![]() |
2a161f4421 | ||
![]() |
f30b2b6fc2 | ||
![]() |
ebbe19131b | ||
![]() |
03cabcb07c | ||
![]() |
401d3ec1c9 | ||
![]() |
fd1f104f4e | ||
![]() |
66e2192e65 | ||
![]() |
0d6d19502f | ||
![]() |
a7b22e2055 | ||
![]() |
71fbcee76c | ||
![]() |
e157894694 | ||
![]() |
f08f354eb6 | ||
![]() |
23db93d20f | ||
![]() |
67212ee29e | ||
![]() |
15d8589069 | ||
![]() |
14cd9315c1 | ||
![]() |
f1786c2ee6 | ||
![]() |
7d9cb65ba1 | ||
![]() |
9f72fdeb41 | ||
![]() |
b7106995b7 | ||
![]() |
dca3c775d1 | ||
![]() |
916709a7e4 | ||
![]() |
066f54f521 | ||
![]() |
64199c7ded | ||
![]() |
79ca1069ec | ||
![]() |
43c8dacd70 | ||
![]() |
f773bf3336 | ||
![]() |
9a9bd71634 | ||
![]() |
25657ebdca | ||
![]() |
c7146613a1 | ||
![]() |
c58d28861d | ||
![]() |
ff69b04216 | ||
![]() |
7653cba247 | ||
![]() |
b0ee0dae93 | ||
![]() |
0224863b1f | ||
![]() |
94c7ce9f42 | ||
![]() |
2a3b8a7692 | ||
![]() |
fec3ff7d8e | ||
![]() |
4b346243eb | ||
![]() |
c2b177434d | ||
![]() |
c5d90ddd19 | ||
![]() |
d607696a75 | ||
![]() |
4088c88a56 | ||
![]() |
3489d5de09 | ||
![]() |
db8c85d249 | ||
![]() |
f4a040064c | ||
![]() |
a89f66e8b5 | ||
![]() |
e9ce137e14 | ||
![]() |
2f7bfa95b2 | ||
![]() |
c84fcf2034 | ||
![]() |
0d3fa43f00 | ||
![]() |
2e04ba1880 | ||
![]() |
85b2a81568 | ||
![]() |
0725464388 | ||
![]() |
f506ac9701 | ||
![]() |
97b3563e6b | ||
![]() |
9ac5746e3c | ||
![]() |
1f83b6691b | ||
![]() |
1f1cbf01d7 | ||
![]() |
eefe7e1b5a | ||
![]() |
bc02de4b18 | ||
![]() |
42874038e2 | ||
![]() |
d55fbd1728 | ||
![]() |
aa402ebaf3 | ||
![]() |
ae21f056d5 | ||
![]() |
a4a23f3ea0 | ||
![]() |
7a996b3c8d | ||
![]() |
2db48b8c22 | ||
![]() |
90e7a69926 | ||
![]() |
3c7de29ca7 | ||
![]() |
9dbd6d881f | ||
![]() |
67395b5e47 | ||
![]() |
47d585768b | ||
![]() |
0f65783266 | ||
![]() |
fa3a981c19 | ||
![]() |
868e956c3f | ||
![]() |
742c2fe684 | ||
![]() |
1d06d30812 | ||
![]() |
5801863b76 | ||
![]() |
7fc2681252 | ||
![]() |
eae2d59da7 | ||
![]() |
4c11c96164 | ||
![]() |
dd5fba69d6 | ||
![]() |
653219d1de | ||
![]() |
6167dda01d | ||
![]() |
0b7d1ad90d | ||
![]() |
5e6f6c5b85 | ||
![]() |
f0f32cf25c | ||
![]() |
daefbada16 | ||
![]() |
d6721f0656 | ||
![]() |
de363b57ce | ||
![]() |
e291555e60 | ||
![]() |
0c0ec30560 | ||
![]() |
5d6169c232 | ||
![]() |
830f98573e | ||
![]() |
b0d574dfb0 | ||
![]() |
b73742839b | ||
![]() |
89bb12de71 | ||
![]() |
ed11ae283f | ||
![]() |
fb43dfb4f2 | ||
![]() |
6dc084c3ab | ||
![]() |
ba9cd9b0ca | ||
![]() |
aacde33614 | ||
![]() |
3f59727ab6 | ||
![]() |
b028eee90e | ||
![]() |
a871b5dbe7 | ||
![]() |
7900578077 | ||
![]() |
35a18bd0b2 | ||
![]() |
e6680b4f60 | ||
![]() |
077b3df3cd | ||
![]() |
edd726ac9f | ||
![]() |
cbf184846a | ||
![]() |
2677ad92d3 | ||
![]() |
f9c0c21714 | ||
![]() |
442c01eabf | ||
![]() |
d467aabd4c | ||
![]() |
f4f4aa0e50 | ||
![]() |
ffb67ab23e | ||
![]() |
1a3b48a60d | ||
![]() |
84b535767b | ||
![]() |
5908d2ca53 | ||
![]() |
27a582634f | ||
![]() |
47245f485a | ||
![]() |
545459be88 | ||
![]() |
b5d3995874 | ||
![]() |
487f5cb8c2 | ||
![]() |
cc31081f1c | ||
![]() |
a9b9902e60 | ||
![]() |
f183e756f3 | ||
![]() |
9ec050341b | ||
![]() |
bf042ce7a3 | ||
![]() |
d2cf613872 | ||
![]() |
f67cb7b145 | ||
![]() |
9f5ea86ebd | ||
![]() |
b8fb53ba20 | ||
![]() |
db744f576e | ||
![]() |
3f772d1e1e | ||
![]() |
438bdbc141 | ||
![]() |
728e497057 | ||
![]() |
d346a532ff | ||
![]() |
34a6337c01 | ||
![]() |
e42631932b | ||
![]() |
e9b2c76a88 | ||
![]() |
270eb677c7 | ||
![]() |
9a772eaecc | ||
![]() |
fae5d22e86 | ||
![]() |
c5629adf23 | ||
![]() |
9b480b41e1 | ||
![]() |
fbed15318b | ||
![]() |
14c906561d | ||
![]() |
e125895615 | ||
![]() |
6bef5ca7f4 | ||
![]() |
f33f1faeea | ||
![]() |
b261dc5dfd | ||
![]() |
164a9b947f | ||
![]() |
46440565dd | ||
![]() |
49adea1654 | ||
![]() |
169cf2e6ae | ||
![]() |
dafe9981a2 | ||
![]() |
c78af9773c | ||
![]() |
1a7a22eb91 | ||
![]() |
4c7ff9bb53 | ||
![]() |
1ee592f5b7 | ||
![]() |
10632c15ef | ||
![]() |
25bb14037c | ||
![]() |
d7db7a8060 | ||
![]() |
fe2344ea12 | ||
![]() |
7898b237fc | ||
![]() |
f3edd8013d | ||
![]() |
279e2ae865 | ||
![]() |
c056384bd5 | ||
![]() |
a06ce64bbf | ||
![]() |
7e314287d6 | ||
![]() |
7bda72027a | ||
![]() |
c5c0df838f | ||
![]() |
75d94aaf06 | ||
![]() |
16bfb35109 | ||
![]() |
6c8a7e3cfa | ||
![]() |
409841a3cf | ||
![]() |
ffa3055a33 | ||
![]() |
41aa15ec26 | ||
![]() |
880d536837 | ||
![]() |
1144f28982 | ||
![]() |
3384bd1e58 | ||
![]() |
0cd795b157 | ||
![]() |
03f3e9e4fd | ||
![]() |
e1f7752aff | ||
![]() |
52afc9d0b2 | ||
![]() |
8ea8e8c28a | ||
![]() |
c6792b7674 | ||
![]() |
de14c55311 | ||
![]() |
2cb8cc9107 | ||
![]() |
309308dac0 | ||
![]() |
8650651567 | ||
![]() |
4bb7e04f2a | ||
![]() |
7e53bed95f | ||
![]() |
e527dbdf88 | ||
![]() |
8295f9cd6b | ||
![]() |
2f38f8d9f3 | ||
![]() |
44afaf7a4b | ||
![]() |
ccfcdbc6ca | ||
![]() |
929432d469 | ||
![]() |
e15cb8a43d | ||
![]() |
fbe96a0a6d | ||
![]() |
fa65a6dc05 | ||
![]() |
9921fc07dd | ||
![]() |
12a0c955bc | ||
![]() |
2be3ff02bd | ||
![]() |
c6c6af1268 | ||
![]() |
e3baf8944e | ||
![]() |
233d5f31f2 | ||
![]() |
cfb2276ecd | ||
![]() |
16f7a6d3cb | ||
![]() |
b8bb8cab25 | ||
![]() |
f3c4e1dc19 | ||
![]() |
5cbcb7e878 | ||
![]() |
fed61d7109 | ||
![]() |
d7ba9f6924 | ||
![]() |
cbf175ac96 | ||
![]() |
afcd6dfa14 | ||
![]() |
f55c2659cd | ||
![]() |
7705cb1158 | ||
![]() |
9755df6fa6 | ||
![]() |
e564d730ce | ||
![]() |
e37938abe0 | ||
![]() |
9b5f7928bf | ||
![]() |
d7496d6fd5 | ||
![]() |
2a131f7978 | ||
![]() |
f46f9fa6ea | ||
![]() |
2473a09f7b | ||
![]() |
ec513f0b60 | ||
![]() |
03bec6a36c | ||
![]() |
ad826dd846 | ||
![]() |
fd8742ae3a | ||
![]() |
96e45bb517 | ||
![]() |
909b9da9da | ||
![]() |
50937990e5 | ||
![]() |
4fbfb5838a | ||
![]() |
e2a7d86446 | ||
![]() |
ef6c9325e0 | ||
![]() |
0e2f742b7e | ||
![]() |
7d85a10e82 | ||
![]() |
dff261eb18 | ||
![]() |
349ee5858f | ||
![]() |
461f0a274a | ||
![]() |
a2eef70030 | ||
![]() |
2efb6cf4f1 | ||
![]() |
36db8a4dc9 | ||
![]() |
52ed170292 | ||
![]() |
871588aa9f | ||
![]() |
a72c4eafff | ||
![]() |
97cec2ba46 | ||
![]() |
4e846774b4 | ||
![]() |
a6be8ddc2d | ||
![]() |
b65670b9e4 | ||
![]() |
ea635d4630 | ||
![]() |
2e9737c149 | ||
![]() |
0f8de2b7c1 | ||
![]() |
2188c5d9fa | ||
![]() |
b5d879c1cd | ||
![]() |
fd365a19f7 | ||
![]() |
adbaadae81 | ||
![]() |
09ad4133fb | ||
![]() |
da49981ecd | ||
![]() |
ebd00bfb9f | ||
![]() |
353ddd3105 | ||
![]() |
e80887d779 | ||
![]() |
06ddf26d09 | ||
![]() |
c5c41f2d3e | ||
![]() |
c5b47fe4a0 | ||
![]() |
fe7a53fb8a | ||
![]() |
bad2a79f05 | ||
![]() |
75100c335f | ||
![]() |
b31c928898 | ||
![]() |
656c818211 | ||
![]() |
c95637eee1 | ||
![]() |
819f765bce | ||
![]() |
f42ad03f91 | ||
![]() |
26bad8d05b | ||
![]() |
117d577e82 | ||
![]() |
0aa6358938 | ||
![]() |
048207f70b | ||
![]() |
a724dd472f | ||
![]() |
ad900479f6 | ||
![]() |
2e4767926c | ||
![]() |
4d3603136f | ||
![]() |
8a15bf059d | ||
![]() |
b9a5c94923 | ||
![]() |
c08b83c299 | ||
![]() |
4aff146812 | ||
![]() |
76bebb1eed | ||
![]() |
71ac8ff996 | ||
![]() |
92b9397ee2 | ||
![]() |
a59cf9b9aa | ||
![]() |
221aa16433 | ||
![]() |
44507d8556 | ||
![]() |
1897bce814 | ||
![]() |
04492a3262 | ||
![]() |
f857330f47 | ||
![]() |
7fb8cd06bc | ||
![]() |
2854e202e1 | ||
![]() |
63b609f725 | ||
![]() |
2eef0bded1 | ||
![]() |
45e2b350a9 | ||
![]() |
ccf2897a65 | ||
![]() |
22059e71a2 | ||
![]() |
8ba684e8c3 | ||
![]() |
ce4a91b679 | ||
![]() |
ddc3f72afb | ||
![]() |
d57d82f439 | ||
![]() |
39734dfadb | ||
![]() |
23dc8e063c | ||
![]() |
f5fb6b83ff |
447 changed files with 19173 additions and 37835 deletions
40
.forgejo/workflows/build-on-commit.yml
Normal file
40
.forgejo/workflows/build-on-commit.yml
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
name: Build Docker Image on Commit
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- '!' # Exclude tags
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-publish:
|
||||||
|
runs-on: docker-builder
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set REPO_VARS
|
||||||
|
id: repo-url
|
||||||
|
run: |
|
||||||
|
echo "REPO_HOST=$(echo "${{ github.server_url }}" | sed 's~http[s]*://~~g')" >> $GITHUB_ENV
|
||||||
|
echo "REPO_PATH=${{ github.repository }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Login to OCI registry
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.OCI_TOKEN }}" | docker login $REPO_HOST -u "${{ secrets.OCI_USER }}" --password-stdin
|
||||||
|
|
||||||
|
- name: Build and push Docker images
|
||||||
|
run: |
|
||||||
|
# Build Docker image with commit SHA
|
||||||
|
docker build -t $REPO_HOST/$REPO_PATH:${{ github.sha }} .
|
||||||
|
docker push $REPO_HOST/$REPO_PATH:${{ github.sha }}
|
||||||
|
|
||||||
|
# Build Docker image with nightly tag
|
||||||
|
docker tag $REPO_HOST/$REPO_PATH:${{ github.sha }} $REPO_HOST/$REPO_PATH:nightly
|
||||||
|
docker push $REPO_HOST/$REPO_PATH:nightly
|
||||||
|
|
||||||
|
# Remove local images to save storage
|
||||||
|
docker rmi $REPO_HOST/$REPO_PATH:${{ github.sha }}
|
||||||
|
docker rmi $REPO_HOST/$REPO_PATH:nightly
|
37
.forgejo/workflows/build-on-tag.yml
Normal file
37
.forgejo/workflows/build-on-tag.yml
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
name: Build and Publish Docker Image on Tag
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-publish:
|
||||||
|
runs-on: docker-builder
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set REPO_VARS
|
||||||
|
id: repo-url
|
||||||
|
run: |
|
||||||
|
echo "REPO_HOST=$(echo "${{ github.server_url }}" | sed 's~http[s]*://~~g')" >> $GITHUB_ENV
|
||||||
|
echo "REPO_PATH=${{ github.repository }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Login to OCI registry
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.OCI_TOKEN }}" | docker login $REPO_HOST -u "${{ secrets.OCI_USER }}" --password-stdin
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
run: |
|
||||||
|
TAG=${{ github.ref_name }} # Get the tag name from the context
|
||||||
|
# Build and push multi-platform Docker images
|
||||||
|
docker build -t $REPO_HOST/$REPO_PATH:$TAG --push .
|
||||||
|
# Tag and push latest
|
||||||
|
docker tag $REPO_HOST/$REPO_PATH:$TAG $REPO_HOST/$REPO_PATH:latest
|
||||||
|
docker push $REPO_HOST/$REPO_PATH:latest
|
||||||
|
|
||||||
|
# Remove the local image to save storage
|
||||||
|
docker rmi $REPO_HOST/$REPO_PATH:$TAG
|
||||||
|
docker rmi $REPO_HOST/$REPO_PATH:latest
|
10
.github/FUNDING.yml
vendored
Normal file
10
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
github: canewsin
|
||||||
|
patreon: # Replace with a single Patreon username e.g., user1
|
||||||
|
open_collective: # Replace with a single Open Collective username e.g., user1
|
||||||
|
ko_fi: canewsin
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: canewsin
|
||||||
|
issuehunt: # Replace with a single IssueHunt username e.g., user1
|
||||||
|
otechie: # Replace with a single Otechie username e.g., user1
|
||||||
|
custom: ['https://paypal.me/PramUkesh', 'https://zerolink.ml/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/']
|
33
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
33
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve ZeroNet
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 1: Please describe your environment
|
||||||
|
|
||||||
|
* ZeroNet version: _____
|
||||||
|
* Operating system: _____
|
||||||
|
* Web browser: _____
|
||||||
|
* Tor status: not available/always/disabled
|
||||||
|
* Opened port: yes/no
|
||||||
|
* Special configuration: ____
|
||||||
|
|
||||||
|
### Step 2: Describe the problem:
|
||||||
|
|
||||||
|
#### Steps to reproduce:
|
||||||
|
|
||||||
|
1. _____
|
||||||
|
2. _____
|
||||||
|
3. _____
|
||||||
|
|
||||||
|
#### Observed Results:
|
||||||
|
|
||||||
|
* What happened? This could be a screenshot, a description, log output (you can send log/debug.log file to hello@zeronet.io if necessary), etc.
|
||||||
|
|
||||||
|
#### Expected Results:
|
||||||
|
|
||||||
|
* What did you expect to happen?
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for ZeroNet
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
72
.github/workflows/codeql-analysis.yml
vendored
Normal file
72
.github/workflows/codeql-analysis.yml
vendored
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ py3-latest ]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ py3-latest ]
|
||||||
|
schedule:
|
||||||
|
- cron: '32 19 * * 2'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'javascript', 'python' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||||
|
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
|
||||||
|
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||||
|
# queries: security-extended,security-and-quality
|
||||||
|
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
|
|
||||||
|
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||||
|
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||||
|
|
||||||
|
# - run: |
|
||||||
|
# echo "Run, Build Application using script"
|
||||||
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
51
.github/workflows/tests.yml
vendored
Normal file
51
.github/workflows/tests.yml
vendored
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
name: tests
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
strategy:
|
||||||
|
max-parallel: 16
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.7", "3.8", "3.9"]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout ZeroNet
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: "true"
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Prepare for installation
|
||||||
|
run: |
|
||||||
|
python3 -m pip install setuptools
|
||||||
|
python3 -m pip install --upgrade pip wheel
|
||||||
|
python3 -m pip install --upgrade codecov coveralls flake8 mock pytest==4.6.3 pytest-cov selenium
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: |
|
||||||
|
python3 -m pip install --upgrade -r requirements.txt
|
||||||
|
python3 -m pip list
|
||||||
|
|
||||||
|
- name: Prepare for tests
|
||||||
|
run: |
|
||||||
|
openssl version -a
|
||||||
|
echo 0 | sudo tee /proc/sys/net/ipv6/conf/all/disable_ipv6
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: |
|
||||||
|
catchsegv python3 -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini
|
||||||
|
export ZERONET_LOG_DIR="log/CryptMessage"; catchsegv python3 -m pytest -x plugins/CryptMessage/Test
|
||||||
|
export ZERONET_LOG_DIR="log/Bigfile"; catchsegv python3 -m pytest -x plugins/Bigfile/Test
|
||||||
|
export ZERONET_LOG_DIR="log/AnnounceLocal"; catchsegv python3 -m pytest -x plugins/AnnounceLocal/Test
|
||||||
|
export ZERONET_LOG_DIR="log/OptionalManager"; catchsegv python3 -m pytest -x plugins/OptionalManager/Test
|
||||||
|
export ZERONET_LOG_DIR="log/Multiuser"; mv plugins/disabled-Multiuser plugins/Multiuser && catchsegv python -m pytest -x plugins/Multiuser/Test
|
||||||
|
export ZERONET_LOG_DIR="log/Bootstrapper"; mv plugins/disabled-Bootstrapper plugins/Bootstrapper && catchsegv python -m pytest -x plugins/Bootstrapper/Test
|
||||||
|
find src -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')"
|
||||||
|
find plugins -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')"
|
||||||
|
flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/
|
12
.gitignore
vendored
12
.gitignore
vendored
|
@ -7,9 +7,14 @@ __pycache__/
|
||||||
|
|
||||||
# Hidden files
|
# Hidden files
|
||||||
.*
|
.*
|
||||||
|
!/.forgejo
|
||||||
|
!/.github
|
||||||
!/.gitignore
|
!/.gitignore
|
||||||
!/.travis.yml
|
!/.travis.yml
|
||||||
|
!/.gitlab-ci.yml
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.bak
|
||||||
|
|
||||||
# Data dir
|
# Data dir
|
||||||
data/*
|
data/*
|
||||||
|
@ -18,13 +23,14 @@ data/*
|
||||||
# Virtualenv
|
# Virtualenv
|
||||||
env/*
|
env/*
|
||||||
|
|
||||||
# Tor, downloaded automatically
|
# Tor data
|
||||||
tools/tor/data
|
tools/tor/data
|
||||||
tools/tor/*exe
|
|
||||||
tools/tor/*dll
|
|
||||||
|
|
||||||
# PhantomJS, downloaded manually for unit tests
|
# PhantomJS, downloaded manually for unit tests
|
||||||
tools/phantomjs
|
tools/phantomjs
|
||||||
|
|
||||||
# ZeroNet config file
|
# ZeroNet config file
|
||||||
zeronet.conf
|
zeronet.conf
|
||||||
|
|
||||||
|
# ZeroNet log files
|
||||||
|
log/*
|
||||||
|
|
48
.gitlab-ci.yml
Normal file
48
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
stages:
|
||||||
|
- test
|
||||||
|
|
||||||
|
.test_template: &test_template
|
||||||
|
stage: test
|
||||||
|
before_script:
|
||||||
|
- pip install --upgrade pip wheel
|
||||||
|
# Selenium and requests can't be installed without a requests hint on Python 3.4
|
||||||
|
- pip install --upgrade requests>=2.22.0
|
||||||
|
- pip install --upgrade codecov coveralls flake8 mock pytest==4.6.3 pytest-cov selenium
|
||||||
|
- pip install --upgrade -r requirements.txt
|
||||||
|
script:
|
||||||
|
- pip list
|
||||||
|
- openssl version -a
|
||||||
|
- python -m pytest -x plugins/CryptMessage/Test --color=yes
|
||||||
|
- python -m pytest -x plugins/Bigfile/Test --color=yes
|
||||||
|
- python -m pytest -x plugins/AnnounceLocal/Test --color=yes
|
||||||
|
- python -m pytest -x plugins/OptionalManager/Test --color=yes
|
||||||
|
- python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini --color=yes
|
||||||
|
- mv plugins/disabled-Multiuser plugins/Multiuser
|
||||||
|
- python -m pytest -x plugins/Multiuser/Test --color=yes
|
||||||
|
- mv plugins/disabled-Bootstrapper plugins/Bootstrapper
|
||||||
|
- python -m pytest -x plugins/Bootstrapper/Test --color=yes
|
||||||
|
- flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/
|
||||||
|
|
||||||
|
test:py3.4:
|
||||||
|
image: python:3.4.3
|
||||||
|
<<: *test_template
|
||||||
|
|
||||||
|
test:py3.5:
|
||||||
|
image: python:3.5.7
|
||||||
|
<<: *test_template
|
||||||
|
|
||||||
|
test:py3.6:
|
||||||
|
image: python:3.6.9
|
||||||
|
<<: *test_template
|
||||||
|
|
||||||
|
test:py3.7-openssl1.1.0:
|
||||||
|
image: python:3.7.0b5
|
||||||
|
<<: *test_template
|
||||||
|
|
||||||
|
test:py3.7-openssl1.1.1:
|
||||||
|
image: python:3.7.4
|
||||||
|
<<: *test_template
|
||||||
|
|
||||||
|
test:py3.8:
|
||||||
|
image: python:3.8.0b3
|
||||||
|
<<: *test_template
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "plugins"]
|
||||||
|
path = plugins
|
||||||
|
url = https://github.com/ZeroNetX/ZeroNet-Plugins.git
|
50
.travis.yml
50
.travis.yml
|
@ -1,21 +1,47 @@
|
||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- 2.7
|
- 3.4
|
||||||
|
- 3.5
|
||||||
|
- 3.6
|
||||||
|
- 3.7
|
||||||
|
- 3.8
|
||||||
|
services:
|
||||||
|
- docker
|
||||||
|
cache: pip
|
||||||
|
before_install:
|
||||||
|
- pip install --upgrade pip wheel
|
||||||
|
- pip install --upgrade codecov coveralls flake8 mock pytest==4.6.3 pytest-cov selenium
|
||||||
|
# - docker build -t zeronet .
|
||||||
|
# - docker run -d -v $PWD:/root/data -p 15441:15441 -p 127.0.0.1:43110:43110 zeronet
|
||||||
install:
|
install:
|
||||||
- pip install -U pip wheel
|
- pip install --upgrade -r requirements.txt
|
||||||
- pip install -r requirements.txt
|
- pip list
|
||||||
before_script:
|
before_script:
|
||||||
- openssl version -a
|
- openssl version -a
|
||||||
|
# Add an IPv6 config - see the corresponding Travis issue
|
||||||
|
# https://github.com/travis-ci/travis-ci/issues/8361
|
||||||
|
- if [ "${TRAVIS_OS_NAME}" == "linux" ]; then
|
||||||
|
sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6';
|
||||||
|
fi
|
||||||
script:
|
script:
|
||||||
- python -m pytest plugins/CryptMessage/Test
|
- catchsegv python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini
|
||||||
- python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini
|
- export ZERONET_LOG_DIR="log/CryptMessage"; catchsegv python -m pytest -x plugins/CryptMessage/Test
|
||||||
before_install:
|
- export ZERONET_LOG_DIR="log/Bigfile"; catchsegv python -m pytest -x plugins/Bigfile/Test
|
||||||
- pip install -U pytest mock pytest-cov
|
- export ZERONET_LOG_DIR="log/AnnounceLocal"; catchsegv python -m pytest -x plugins/AnnounceLocal/Test
|
||||||
- pip install codecov
|
- export ZERONET_LOG_DIR="log/OptionalManager"; catchsegv python -m pytest -x plugins/OptionalManager/Test
|
||||||
- pip install coveralls
|
- export ZERONET_LOG_DIR="log/Multiuser"; mv plugins/disabled-Multiuser plugins/Multiuser && catchsegv python -m pytest -x plugins/Multiuser/Test
|
||||||
|
- export ZERONET_LOG_DIR="log/Bootstrapper"; mv plugins/disabled-Bootstrapper plugins/Bootstrapper && catchsegv python -m pytest -x plugins/Bootstrapper/Test
|
||||||
|
- find src -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')"
|
||||||
|
- find plugins -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')"
|
||||||
|
- flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/
|
||||||
|
after_failure:
|
||||||
|
- zip -r log.zip log/
|
||||||
|
- curl --upload-file ./log.zip https://transfer.sh/log.zip
|
||||||
after_success:
|
after_success:
|
||||||
- codecov
|
- codecov
|
||||||
- coveralls --rcfile=src/Test/coverage.ini
|
- coveralls --rcfile=src/Test/coverage.ini
|
||||||
cache:
|
notifications:
|
||||||
directories:
|
email:
|
||||||
- $HOME/.cache/pip
|
recipients:
|
||||||
|
hello@zeronet.io
|
||||||
|
on_success: change
|
||||||
|
|
519
CHANGELOG.md
519
CHANGELOG.md
|
@ -1,3 +1,519 @@
|
||||||
|
### ZeroNet 0.9.0 (2023-07-12) Rev4630
|
||||||
|
- Fix RDos Issue in Plugins https://github.com/ZeroNetX/ZeroNet-Plugins/pull/9
|
||||||
|
- Add trackers to Config.py for failsafety incase missing trackers.txt
|
||||||
|
- Added Proxy links
|
||||||
|
- Fix pysha3 dep installation issue
|
||||||
|
- FileRequest -> Remove Unnecessary check, Fix error wording
|
||||||
|
- Fix Response when site is missing for `actionAs`
|
||||||
|
|
||||||
|
|
||||||
|
### ZeroNet 0.8.5 (2023-02-12) Rev4625
|
||||||
|
- Fix(https://github.com/ZeroNetX/ZeroNet/pull/202) for SSL cert gen failed on Windows.
|
||||||
|
- default theme-class for missing value in `users.json`.
|
||||||
|
- Fetch Stats Plugin changes.
|
||||||
|
|
||||||
|
### ZeroNet 0.8.4 (2022-12-12) Rev4620
|
||||||
|
- Increase Minimum Site size to 25MB.
|
||||||
|
|
||||||
|
### ZeroNet 0.8.3 (2022-12-11) Rev4611
|
||||||
|
- main.py -> Fix accessing unassigned varible
|
||||||
|
- ContentManager -> Support for multiSig
|
||||||
|
- SiteStrorage.py -> Fix accessing unassigned varible
|
||||||
|
- ContentManager.py Improve Logging of Valid Signers
|
||||||
|
|
||||||
|
### ZeroNet 0.8.2 (2022-11-01) Rev4610
|
||||||
|
- Fix Startup Error when plugins dir missing
|
||||||
|
- Move trackers to seperate file & Add more trackers
|
||||||
|
- Config:: Skip loading missing tracker files
|
||||||
|
- Added documentation for getRandomPort fn
|
||||||
|
|
||||||
|
### ZeroNet 0.8.1 (2022-10-01) Rev4600
|
||||||
|
- fix readdress loop (cherry-pick previously added commit from conservancy)
|
||||||
|
- Remove Patreon badge
|
||||||
|
- Update README-ru.md (#177)
|
||||||
|
- Include inner_path of failed request for signing in error msg and response
|
||||||
|
- Don't Fail Silently When Cert is Not Selected
|
||||||
|
- Console Log Updates, Specify min supported ZeroNet version for Rust version Protocol Compatibility
|
||||||
|
- Update FUNDING.yml
|
||||||
|
|
||||||
|
### ZeroNet 0.8.0 (2022-05-27) Rev4591
|
||||||
|
- Revert File Open to catch File Access Errors.
|
||||||
|
|
||||||
|
### ZeroNet 0.7.9-patch (2022-05-26) Rev4586
|
||||||
|
- Use xescape(s) from zeronet-conservancy
|
||||||
|
- actionUpdate response Optimisation
|
||||||
|
- Fetch Plugins Repo Updates
|
||||||
|
- Fix Unhandled File Access Errors
|
||||||
|
- Create codeql-analysis.yml
|
||||||
|
|
||||||
|
### ZeroNet 0.7.9 (2022-05-26) Rev4585
|
||||||
|
- Rust Version Compatibility for update Protocol msg
|
||||||
|
- Removed Non Working Trakers.
|
||||||
|
- Dynamically Load Trackers from Dashboard Site.
|
||||||
|
- Tracker Supply Improvements.
|
||||||
|
- Fix Repo Url for Bug Report
|
||||||
|
- First Party Tracker Update Service using Dashboard Site.
|
||||||
|
- remove old v2 onion service [#158](https://github.com/ZeroNetX/ZeroNet/pull/158)
|
||||||
|
|
||||||
|
### ZeroNet 0.7.8 (2022-03-02) Rev4580
|
||||||
|
- Update Plugins with some bug fixes and Improvements
|
||||||
|
|
||||||
|
### ZeroNet 0.7.6 (2022-01-12) Rev4565
|
||||||
|
- Sync Plugin Updates
|
||||||
|
- Clean up tor v3 patch [#115](https://github.com/ZeroNetX/ZeroNet/pull/115)
|
||||||
|
- Add More Default Plugins to Repo
|
||||||
|
- Doubled Site Publish Limits
|
||||||
|
- Update ZeroNet Repo Urls [#103](https://github.com/ZeroNetX/ZeroNet/pull/103)
|
||||||
|
- UI/UX: Increases Size of Notifications Close Button [#106](https://github.com/ZeroNetX/ZeroNet/pull/106)
|
||||||
|
- Moved Plugins to Seperate Repo
|
||||||
|
- Added `access_key` variable in Config, this used to access restrited plugins when multiuser plugin is enabled. When MultiUserPlugin is enabled we cannot access some pages like /Stats, this key will remove such restriction with access key.
|
||||||
|
- Added `last_connection_id_current_version` to ConnectionServer, helpful to estimate no of connection from current client version.
|
||||||
|
- Added current version: connections to /Stats page. see the previous point.
|
||||||
|
|
||||||
|
### ZeroNet 0.7.5 (2021-11-28) Rev4560
|
||||||
|
- Add more default trackers
|
||||||
|
- Change default homepage address to `1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d`
|
||||||
|
- Change default update site address to `1Update8crprmciJHwp2WXqkx2c4iYp18`
|
||||||
|
|
||||||
|
### ZeroNet 0.7.3 (2021-11-28) Rev4555
|
||||||
|
- Fix xrange is undefined error
|
||||||
|
- Fix Incorrect viewport on mobile while loading
|
||||||
|
- Tor-V3 Patch by anonymoose
|
||||||
|
|
||||||
|
|
||||||
|
### ZeroNet 0.7.1 (2019-07-01) Rev4206
|
||||||
|
### Added
|
||||||
|
- Built-in logging console in the web UI to see what's happening in the background. (pull down top-right 0 button to see it)
|
||||||
|
- Display database rebuild errors [Thanks to Lola]
|
||||||
|
- New plugin system that allows to install and manage builtin/third party extensions to the ZeroNet client using the web interface.
|
||||||
|
- Support multiple trackers_file
|
||||||
|
- Add OpenSSL 1.1 support to CryptMessage plugin based on Bitmessage modifications [Thanks to radfish]
|
||||||
|
- Display visual error message on startup errors
|
||||||
|
- Fix max opened files changing on Windows platform
|
||||||
|
- Display TLS1.3 compatibility on /Stats page
|
||||||
|
- Add fake SNI and ALPN to peer connections to make it more like standard https connections
|
||||||
|
- Hide and ignore tracker_proxy setting in Tor: Always mode as it's going to use Tor anyway.
|
||||||
|
- Deny websocket connections from unknown origins
|
||||||
|
- Restrict open_browser values to avoid RCE on sandbox escape
|
||||||
|
- Offer access web interface by IP address in case of unknown host
|
||||||
|
- Link to site's sidebar with "#ZeroNet:OpenSidebar" hash
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Allow .. in file names [Thanks to imachug]
|
||||||
|
- Change unstable trackers
|
||||||
|
- More clean errors on sites.json/users.json load error
|
||||||
|
- Various tweaks for tracker rating on unstable connections
|
||||||
|
- Use OpenSSL 1.1 dlls from default Python Windows distribution if possible
|
||||||
|
- Re-factor domain resolving for easier domain plugins
|
||||||
|
- Disable UDP connections if --proxy is used
|
||||||
|
- New, decorator-based Websocket API permission system to avoid future typo mistakes
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix parsing config lines that have no value
|
||||||
|
- Fix start.py [Thanks to imachug]
|
||||||
|
- Allow multiple values of the same key in the config file [Thanks ssdifnskdjfnsdjk for reporting]
|
||||||
|
- Fix parsing config file lines that has % in the value [Thanks slrslr for reporting]
|
||||||
|
- Fix bootstrapper plugin hash reloads [Thanks geekless for reporting]
|
||||||
|
- Fix CryptMessage plugin OpenSSL dll loading on Windows (ZeroMail errors) [Thanks cxgreat2014 for reporting]
|
||||||
|
- Fix startup error when using OpenSSL 1.1 [Thanks to imachug]
|
||||||
|
- Fix a bug that did not loaded merged site data for 5 sec after the merged site got added
|
||||||
|
- Fix typo that allowed to add new plugins in public proxy mode. [Thanks styromaniac for reporting]
|
||||||
|
- Fix loading non-big files with "|all" postfix [Thanks to krzotr]
|
||||||
|
- Fix OpenSSL cert generation error crash by change Windows console encoding to utf8
|
||||||
|
|
||||||
|
#### Wrapper html injection vulnerability [Reported by ivanq]
|
||||||
|
|
||||||
|
In ZeroNet before rev4188 the wrapper template variables was rendered incorrectly.
|
||||||
|
|
||||||
|
Result: The opened site was able to gain WebSocket connection with unrestricted ADMIN/NOSANDBOX access, change configuration values and possible RCE on client's machine.
|
||||||
|
|
||||||
|
Fix: Fixed the template rendering code, disallowed WebSocket connections from unknown locations, restricted open_browser configuration values to avoid possible RCE in case of sandbox escape.
|
||||||
|
|
||||||
|
Note: The fix is also back ported to ZeroNet Py 2.x version (Rev3870)
|
||||||
|
|
||||||
|
|
||||||
|
### ZeroNet 0.7.0 (2019-06-12) Rev4106 (First release targeting Python 3.4+)
|
||||||
|
### Added
|
||||||
|
- 5-10x faster signature verification by using libsecp256k1 (Thanks to ZeroMux)
|
||||||
|
- Generated SSL certificate randomization to avoid protocol filters (Thanks to ValdikSS)
|
||||||
|
- Offline mode
|
||||||
|
- P2P source code update using ZeroNet protocol
|
||||||
|
- ecdsaSign/Verify commands to CryptMessage plugin (Thanks to imachug)
|
||||||
|
- Efficient file rename: change file names instead of re-downloading the file.
|
||||||
|
- Make redirect optional on site cloning (Thanks to Lola)
|
||||||
|
- EccPrivToPub / EccPubToPriv functions (Thanks to imachug)
|
||||||
|
- Detect and change dark/light theme based on OS setting (Thanks to filips123)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Re-factored code to Python3 runtime (compatible with Python 3.4-3.8)
|
||||||
|
- More safe database sync mode
|
||||||
|
- Removed bundled third-party libraries where it's possible
|
||||||
|
- Use lang=en instead of lang={lang} in urls to avoid url encode problems
|
||||||
|
- Remove environment details from error page
|
||||||
|
- Don't push content.json updates larger than 10kb to significantly reduce bw usage for site with many files
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix sending files with \0 characters
|
||||||
|
- Security fix: Escape error detail to avoid XSS (reported by krzotr)
|
||||||
|
- Fix signature verification using libsecp256k1 for compressed addresses (mostly certificates generated in the browser)
|
||||||
|
- Fix newsfeed if you have more than 1000 followed topic/post on one site.
|
||||||
|
- Fix site download as zip file
|
||||||
|
- Fix displaying sites with utf8 title
|
||||||
|
- Error message if dbRebuild fails (Thanks to Lola)
|
||||||
|
- Fix browser reopen if executing start.py again. (Thanks to imachug)
|
||||||
|
|
||||||
|
|
||||||
|
### ZeroNet 0.6.5 (2019-02-16) Rev3851 (Last release targeting Python 2.7.x)
|
||||||
|
### Added
|
||||||
|
- IPv6 support in peer exchange, bigfiles, optional file finding, tracker sharing, socket listening and connecting (based on tangdou1 modifications)
|
||||||
|
- New tracker database format with IPv6 support
|
||||||
|
- Display notification if there is an unpublished modification for your site
|
||||||
|
- Listen and shut down normally for SIGTERM (Thanks to blurHY)
|
||||||
|
- Support tilde `~` in filenames (by d14na)
|
||||||
|
- Support map for Namecoin subdomain names (Thanks to lola)
|
||||||
|
- Add log level to config page
|
||||||
|
- Support `{data}` for data dir variable in trackers_file value
|
||||||
|
- Quick check content.db on startup and rebuild if necessary
|
||||||
|
- Don't show meek proxy option if the tor client does not supports it
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Refactored port open checking with IPv6 support
|
||||||
|
- Consider non-local IPs as external even is the open port check fails (for CJDNS and Yggdrasil support)
|
||||||
|
- Add IPv6 tracker and change unstable tracker
|
||||||
|
- Don't correct sent local time with the calculated time correction
|
||||||
|
- Disable CSP for Edge
|
||||||
|
- Only support CREATE commands in dbschema indexes node and SELECT from storage.query
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Check the length of master seed when executing cryptGetPrivatekey CLI command
|
||||||
|
- Only reload source code on file modification / creation
|
||||||
|
- Detection and issue warning for latest no-script plugin
|
||||||
|
- Fix atomic write of a non-existent file
|
||||||
|
- Fix sql queries with lots of variables and sites with lots of content.json
|
||||||
|
- Fix multi-line parsing of zeronet.conf
|
||||||
|
- Fix site deletion from users.json
|
||||||
|
- Fix site cloning before site downloaded (Reported by unsystemizer)
|
||||||
|
- Fix queryJson for non-list nodes (Reported by MingchenZhang)
|
||||||
|
|
||||||
|
|
||||||
|
## ZeroNet 0.6.4 (2018-10-20) Rev3660
|
||||||
|
### Added
|
||||||
|
- New plugin: UiConfig. A web interface that allows changing ZeroNet settings.
|
||||||
|
- New plugin: AnnounceShare. Share trackers between users, automatically announce client's ip as tracker if Bootstrapper plugin is enabled.
|
||||||
|
- Global tracker stats on ZeroHello: Include statistics from all served sites instead of displaying request statistics only for one site.
|
||||||
|
- Support custom proxy for trackers. (Configurable with /Config)
|
||||||
|
- Adding peers to sites manually using zeronet_peers get parameter
|
||||||
|
- Copy site address with peers link on the sidebar.
|
||||||
|
- Zip file listing and streaming support for Bigfiles.
|
||||||
|
- Tracker statistics on /Stats page
|
||||||
|
- Peer reputation save/restore to speed up sync time after startup.
|
||||||
|
- Full support fileGet, fileList, dirList calls on tar.gz/zip files.
|
||||||
|
- Archived_before support to user content rules to allow deletion of all user files before the specified date
|
||||||
|
- Show and manage "Connecting" sites on ZeroHello
|
||||||
|
- Add theme support to ZeroNet sites
|
||||||
|
- Dark theme for ZeroHello, ZeroBlog, ZeroTalk
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Dynamic big file allocation: More efficient storage usage by don't pre-allocate the whole file at the beginning, but expand the size as the content downloads.
|
||||||
|
- Reduce the request frequency to unreliable trackers.
|
||||||
|
- Only allow 5 concurrent checkSites to run in parallel to reduce load under Tor/slow connection.
|
||||||
|
- Stop site downloading if it reached 95% of site limit to avoid download loop for sites out of limit
|
||||||
|
- The pinned optional files won't be removed from download queue after 30 retries and won't be deleted even if the site owner removes it.
|
||||||
|
- Don't remove incomplete (downloading) sites on startup
|
||||||
|
- Remove --pin_bigfile argument as big files are automatically excluded from optional files limit.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Trayicon compatibility with latest gevent
|
||||||
|
- Request number counting for zero:// trackers
|
||||||
|
- Peer reputation boost for zero:// trackers.
|
||||||
|
- Blocklist of peers loaded from peerdb (Thanks tangdou1 for report)
|
||||||
|
- Sidebar map loading on foreign languages (Thx tangdou1 for report)
|
||||||
|
- FileGet on non-existent files (Thanks mcdev for reporting)
|
||||||
|
- Peer connecting bug for sites with low amount of peers
|
||||||
|
|
||||||
|
#### "The Vacation" Sandbox escape bug [Reported by GitCenter / Krixano / ZeroLSTN]
|
||||||
|
|
||||||
|
In ZeroNet 0.6.3 Rev3615 and earlier as a result of invalid file type detection, a malicious site could escape the iframe sandbox.
|
||||||
|
|
||||||
|
Result: Browser iframe sandbox escape
|
||||||
|
|
||||||
|
Applied fix: Replaced the previous, file extension based file type identification with a proper one.
|
||||||
|
|
||||||
|
Affected versions: All versions before ZeroNet Rev3616
|
||||||
|
|
||||||
|
|
||||||
|
## ZeroNet 0.6.3 (2018-06-26)
|
||||||
|
### Added
|
||||||
|
- New plugin: ContentFilter that allows to have shared site and user block list.
|
||||||
|
- Support Tor meek proxies to avoid tracker blocking of GFW
|
||||||
|
- Detect network level tracker blocking and easy setting meek proxy for tracker connections.
|
||||||
|
- Support downloading 2GB+ sites as .zip (Thx to Radtoo)
|
||||||
|
- Support ZeroNet as a transparent proxy (Thx to JeremyRand)
|
||||||
|
- Allow fileQuery as CORS command (Thx to imachug)
|
||||||
|
- Windows distribution includes Tor and meek client by default
|
||||||
|
- Download sites as zip link to sidebar
|
||||||
|
- File server port randomization
|
||||||
|
- Implicit SSL for all connection
|
||||||
|
- fileList API command for zip files
|
||||||
|
- Auto download bigfiles size limit on sidebar
|
||||||
|
- Local peer number to the sidebar
|
||||||
|
- Open site directory button in sidebar
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Switched to Azure Tor meek proxy as Amazon one became unavailable
|
||||||
|
- Refactored/rewritten tracker connection manager
|
||||||
|
- Improved peer discovery for optional files without opened port
|
||||||
|
- Also delete Bigfile's piecemap on deletion
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Important security issue: Iframe sandbox escape [Reported by Ivanq / gitcenter]
|
||||||
|
- Local peer discovery when running multiple clients on the same machine
|
||||||
|
- Uploading small files with Bigfile plugin
|
||||||
|
- Ctrl-c shutdown when running CLI commands
|
||||||
|
- High CPU/IO usage when Multiuser plugin enabled
|
||||||
|
- Firefox back button
|
||||||
|
- Peer discovery on older Linux kernels
|
||||||
|
- Optional file handling when multiple files have the same hash_id (first 4 chars of the hash)
|
||||||
|
- Msgpack 0.5.5 and 0.5.6 compatibility
|
||||||
|
|
||||||
|
## ZeroNet 0.6.2 (2018-02-18)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- New plugin: AnnounceLocal to make ZeroNet work without an internet connection on the local network.
|
||||||
|
- Allow dbQuey and userGetSettings using the `as` API command on different sites with Cors permission
|
||||||
|
- New config option: `--log_level` to reduce log verbosity and IO load
|
||||||
|
- Prefer to connect to recent peers from trackers first
|
||||||
|
- Mark peers with port 1 is also unconnectable for future fix for trackers that do not support port 0 announce
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Don't keep connection for sites that have not been modified in the last week
|
||||||
|
- Change unreliable trackers to new ones
|
||||||
|
- Send maximum 10 findhash request in one find optional files round (15sec)
|
||||||
|
- Change "Unique to site" to "No certificate" for default option in cert selection dialog.
|
||||||
|
- Dont print warnings if not in debug mode
|
||||||
|
- Generalized tracker logging format
|
||||||
|
- Only recover sites from sites.json if they had peers
|
||||||
|
- Message from local peers does not means internet connection
|
||||||
|
- Removed `--debug_gevent` and turned on Gevent block logging by default
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Limit connections to 512 to avoid reaching 1024 limit on windows
|
||||||
|
- Exception when logging foreign operating system socket errors
|
||||||
|
- Don't send private (local) IPs on pex
|
||||||
|
- Don't connect to private IPs in tor always mode
|
||||||
|
- Properly recover data from msgpack unpacker on file stream start
|
||||||
|
- Symlinked data directory deletion when deleting site using Windows
|
||||||
|
- De-duplicate peers before publishing
|
||||||
|
- Bigfile info for non-existing files
|
||||||
|
|
||||||
|
|
||||||
|
## ZeroNet 0.6.1 (2018-01-25)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- New plugin: Chart
|
||||||
|
- Collect and display charts about your contribution to ZeroNet network
|
||||||
|
- Allow list as argument replacement in sql queries. (Thanks to imachug)
|
||||||
|
- Newsfeed query time statistics (Click on "From XX sites in X.Xs on ZeroHello)
|
||||||
|
- New UiWebsocket API command: As to run commands as other site
|
||||||
|
- Ranged ajax queries for big files
|
||||||
|
- Filter feed by type and site address
|
||||||
|
- FileNeed, Bigfile upload command compatibility with merger sites
|
||||||
|
- Send event on port open / tor status change
|
||||||
|
- More description on permission request
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Reduce memory usage of sidebar geoip database cache
|
||||||
|
- Change unreliable tracker to new one
|
||||||
|
- Don't display Cors permission ask if it already granted
|
||||||
|
- Avoid UI blocking when rebuilding a merger site
|
||||||
|
- Skip listing ignored directories on signing
|
||||||
|
- In Multiuser mode show the seed welcome message when adding new certificate instead of first visit
|
||||||
|
- Faster async port opening on multiple network interfaces
|
||||||
|
- Allow javascript modals
|
||||||
|
- Only zoom sidebar globe if mouse button is pressed down
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Open port checking error reporting (Thanks to imachug)
|
||||||
|
- Out-of-range big file requests
|
||||||
|
- Don't output errors happened on gevent greenlets twice
|
||||||
|
- Newsfeed skip sites with no database
|
||||||
|
- Newsfeed queries with multiple params
|
||||||
|
- Newsfeed queries with UNION and UNION ALL
|
||||||
|
- Fix site clone with sites larger that 10MB
|
||||||
|
- Unreliable Websocket connection when requesting files from different sites at the same time
|
||||||
|
|
||||||
|
|
||||||
|
## ZeroNet 0.6.0 (2017-10-17)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- New plugin: Big file support
|
||||||
|
- Automatic pinning on Big file download
|
||||||
|
- Enable TCP_NODELAY for supporting sockets
|
||||||
|
- actionOptionalFileList API command arguments to list non-downloaded files or only big files
|
||||||
|
- serverShowdirectory API command arguments to allow to display site's directory in OS file browser
|
||||||
|
- fileNeed API command to initialize optional file downloading
|
||||||
|
- wrapperGetAjaxKey API command to request nonce for AJAX request
|
||||||
|
- Json.gz support for database files
|
||||||
|
- P2P port checking (Thanks for grez911)
|
||||||
|
- `--download_optional auto` argument to enable automatic optional file downloading for newly added site
|
||||||
|
- Statistics for big files and protocol command requests on /Stats
|
||||||
|
- Allow to set user limitation based on auth_address
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- More aggressive and frequent connection timeout checking
|
||||||
|
- Use out of msgpack context file streaming for files larger than 512KB
|
||||||
|
- Allow optional files workers over the worker limit
|
||||||
|
- Automatic redirection to wrapper on nonce_error
|
||||||
|
- Send websocket event on optional file deletion
|
||||||
|
- Optimize sites.json saving
|
||||||
|
- Enable faster C-based msgpack packer by default
|
||||||
|
- Major optimization on Bootstrapper plugin SQL queries
|
||||||
|
- Don't reset bad file counter on restart, to allow easier give up on unreachable files
|
||||||
|
- Incoming connection limit changed from 1000 to 500 to avoid reaching socket limit on Windows
|
||||||
|
- Changed tracker boot.zeronet.io domain, because zeronet.io got banned in some countries
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
- Sub-directories in user directories
|
||||||
|
|
||||||
|
## ZeroNet 0.5.7 (2017-07-19)
|
||||||
|
### Added
|
||||||
|
- New plugin: CORS to request read permission to other site's content
|
||||||
|
- New API command: userSetSettings/userGetSettings to store site's settings in users.json
|
||||||
|
- Avoid file download if the file size does not match with the requested one
|
||||||
|
- JavaScript and wrapper less file access using /raw/ prefix ([Example](http://127.0.0.1:43110/raw/1AsRLpuRxr3pb9p3TKoMXPSWHzh6i7fMGi/en.tar.gz/index.html))
|
||||||
|
- --silent command line option to disable logging to stdout
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Better error reporting on sign/verification errors
|
||||||
|
- More test for sign and verification process
|
||||||
|
- Update to OpenSSL v1.0.2l
|
||||||
|
- Limit compressed files to 6MB to avoid zip/tar.gz bomb
|
||||||
|
- Allow space, [], () characters in filenames
|
||||||
|
- Disable cross-site resource loading to improve privacy. [Reported by Beardog108]
|
||||||
|
- Download directly accessed Pdf/Svg/Swf files instead of displaying them to avoid wrapper escape using in JS in SVG file. [Reported by Beardog108]
|
||||||
|
- Disallow potentially unsafe regular expressions to avoid ReDoS [Reported by MuxZeroNet]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Detecting data directory when running Windows distribution exe [Reported by Plasmmer]
|
||||||
|
- OpenSSL loading under Android 6+
|
||||||
|
- Error on exiting when no connection server started
|
||||||
|
|
||||||
|
|
||||||
|
## ZeroNet 0.5.6 (2017-06-15)
|
||||||
|
### Added
|
||||||
|
- Callback for certSelect API command
|
||||||
|
- More compact list formatting in json
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Remove obsolete auth_key_sha512 and signature format
|
||||||
|
- Improved Spanish translation (Thanks to Pupiloho)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Opened port checking (Thanks l5h5t7 & saber28 for reporting)
|
||||||
|
- Standalone update.py argument parsing (Thanks Zalex for reporting)
|
||||||
|
- uPnP crash on startup (Thanks Vertux for reporting)
|
||||||
|
- CoffeeScript 1.12.6 compatibility (Thanks kavamaken & imachug)
|
||||||
|
- Multi value argument parsing
|
||||||
|
- Database error when running from directory that contains special characters (Thanks Pupiloho for reporting)
|
||||||
|
- Site lock violation logging
|
||||||
|
|
||||||
|
|
||||||
|
#### Proxy bypass during source upgrade [Reported by ZeroMux]
|
||||||
|
|
||||||
|
In ZeroNet before 0.5.6 during the client's built-in source code upgrade mechanism,
|
||||||
|
ZeroNet did not respect Tor and/or proxy settings.
|
||||||
|
|
||||||
|
Result: ZeroNet downloaded the update without using the Tor network and potentially leaked the connections.
|
||||||
|
|
||||||
|
Fix: Removed the problematic code line from the updater that removed the proxy settings from the socket library.
|
||||||
|
|
||||||
|
Affected versions: ZeroNet 0.5.5 and earlier, Fixed in: ZeroNet 0.5.6
|
||||||
|
|
||||||
|
|
||||||
|
#### XSS vulnerability using DNS rebinding. [Reported by Beardog108]
|
||||||
|
|
||||||
|
In ZeroNet before 0.5.6 the web interface did not validate the request's Host parameter.
|
||||||
|
|
||||||
|
Result: An attacker using a specially crafted DNS entry could have bypassed the browser's cross-site-scripting protection
|
||||||
|
and potentially gained access to user's private data stored on site.
|
||||||
|
|
||||||
|
Fix: By default ZeroNet only accept connections from 127.0.0.1 and localhost hosts.
|
||||||
|
If you bind the ui server to an external interface, then it also adds the first http request's host to the allowed host list
|
||||||
|
or you can define it manually using --ui_host.
|
||||||
|
|
||||||
|
Affected versions: ZeroNet 0.5.5 and earlier, Fixed in: ZeroNet 0.5.6
|
||||||
|
|
||||||
|
|
||||||
|
## ZeroNet 0.5.5 (2017-05-18)
|
||||||
|
### Added
|
||||||
|
- Outgoing socket binding by --bind parameter
|
||||||
|
- Database rebuilding progress bar
|
||||||
|
- Protect low traffic site's peers from cleanup closing
|
||||||
|
- Local site blacklisting
|
||||||
|
- Cloned site source code upgrade from parent
|
||||||
|
- Input placeholder support for displayPrompt
|
||||||
|
- Alternative interaction for wrapperConfirm
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- New file priorities for faster site display on first visit
|
||||||
|
- Don't add ? to url if push/replaceState url starts with #
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- PermissionAdd/Remove admin command requirement
|
||||||
|
- Multi-line confirmation dialog
|
||||||
|
|
||||||
|
|
||||||
|
## ZeroNet 0.5.4 (2017-04-14)
|
||||||
|
### Added
|
||||||
|
- Major speed and CPU usage enhancements in Tor always mode
|
||||||
|
- Send skipped modifications to outdated clients
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Upgrade libs to latest version
|
||||||
|
- Faster port opening and closing
|
||||||
|
- Deny site limit modification in MultiUser mode
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Filling database from optional files
|
||||||
|
- OpenSSL detection on systems with OpenSSL 1.1
|
||||||
|
- Users.json corruption on systems with slow hdd
|
||||||
|
- Fix leaking files in data directory by webui
|
||||||
|
|
||||||
|
|
||||||
|
## ZeroNet 0.5.3 (2017-02-27)
|
||||||
|
### Added
|
||||||
|
- Tar.gz/zip packed site support
|
||||||
|
- Utf8 filenames in archive files
|
||||||
|
- Experimental --db_mode secure database mode to prevent data loss on systems with unreliable power source.
|
||||||
|
- Admin user support in MultiUser mode
|
||||||
|
- Optional deny adding new sites in MultiUser mode
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Faster update and publish times by new socket sharing algorithm
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix missing json_row errors when using Mute plugin
|
||||||
|
|
||||||
|
|
||||||
|
## ZeroNet 0.5.2 (2017-02-09)
|
||||||
|
### Added
|
||||||
|
- User muting
|
||||||
|
- Win/Mac signed exe/.app
|
||||||
|
- Signed commits
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Faster site updates after startup
|
||||||
|
- New macOS package for 10.10 compatibility
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix "New version just released" popup on page first visit
|
||||||
|
- Fix disappearing optional files bug (Thanks l5h5t7 for reporting)
|
||||||
|
- Fix skipped updates on unreliable connections (Thanks P2P for reporting)
|
||||||
|
- Sandbox escape security fix (Thanks Firebox for reporting)
|
||||||
|
- Fix error reporting on async websocket functions
|
||||||
|
|
||||||
|
|
||||||
## ZeroNet 0.5.1 (2016-11-18)
|
## ZeroNet 0.5.1 (2016-11-18)
|
||||||
### Added
|
### Added
|
||||||
- Multi language interface
|
- Multi language interface
|
||||||
|
@ -7,6 +523,7 @@
|
||||||
### Fixed
|
### Fixed
|
||||||
- Parallel optional file downloading
|
- Parallel optional file downloading
|
||||||
|
|
||||||
|
|
||||||
## ZeroNet 0.5.0 (2016-11-08)
|
## ZeroNet 0.5.0 (2016-11-08)
|
||||||
### Added
|
### Added
|
||||||
- New Plugin: Allow list/delete/pin/manage files on ZeroHello
|
- New Plugin: Allow list/delete/pin/manage files on ZeroHello
|
||||||
|
@ -52,7 +569,6 @@
|
||||||
- Display user file sizes in sidebar
|
- Display user file sizes in sidebar
|
||||||
- Concurrent worker number depends on --connection_limit
|
- Concurrent worker number depends on --connection_limit
|
||||||
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Close databases after 5 min idle time
|
- Close databases after 5 min idle time
|
||||||
- Better site size calculation
|
- Better site size calculation
|
||||||
|
@ -62,7 +578,6 @@
|
||||||
- Newsfeed scans only last 3 days to speed up database queries
|
- Newsfeed scans only last 3 days to speed up database queries
|
||||||
- Updated ZeroBundle-win to Python 2.7.12
|
- Updated ZeroBundle-win to Python 2.7.12
|
||||||
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fix for important security problem, which is allowed anyone to publish new content without valid certificate from ID provider. Thanks Kaffie for pointing it out!
|
- Fix for important security problem, which is allowed anyone to publish new content without valid certificate from ID provider. Thanks Kaffie for pointing it out!
|
||||||
- Fix sidebar error when no certificate provider selected
|
- Fix sidebar error when no certificate provider selected
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
@ -645,7 +645,7 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
@ -664,11 +664,11 @@ might be different; for a GUI interface, you would use an "about box".
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
<http://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
The GNU General Public License does not permit incorporating your program
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
may consider it more useful to permit linking proprietary applications with
|
may consider it more useful to permit linking proprietary applications with
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
Public License instead of this License. But first, please read
|
Public License instead of this License. But first, please read
|
||||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
37
Dockerfile
37
Dockerfile
|
@ -1,32 +1,33 @@
|
||||||
FROM ubuntu:16.04
|
FROM alpine:3.15
|
||||||
|
|
||||||
MAINTAINER Felix Imobersteg <felix@whatwedo.ch>
|
|
||||||
|
|
||||||
#Base settings
|
#Base settings
|
||||||
ENV DEBIAN_FRONTEND noninteractive
|
|
||||||
ENV HOME /root
|
ENV HOME /root
|
||||||
|
|
||||||
|
COPY requirements.txt /root/requirements.txt
|
||||||
|
|
||||||
#Install ZeroNet
|
#Install ZeroNet
|
||||||
RUN \
|
RUN apk --update --no-cache --no-progress add python3 python3-dev py3-pip gcc g++ autoconf automake libtool libffi-dev musl-dev make tor openssl \
|
||||||
apt-get update -y; \
|
&& pip3 install -r /root/requirements.txt \
|
||||||
apt-get -y install msgpack-python python-gevent python-pip python-dev tor; \
|
&& apk del python3-dev gcc g++ autoconf automake libtool libffi-dev musl-dev make \
|
||||||
pip install msgpack-python --upgrade; \
|
&& echo "ControlPort 9051" >> /etc/tor/torrc \
|
||||||
apt-get clean -y; \
|
&& echo "CookieAuthentication 1" >> /etc/tor/torrc
|
||||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*; \
|
|
||||||
echo "ControlPort 9051" >> /etc/tor/torrc; \
|
RUN python3 -V \
|
||||||
echo "CookieAuthentication 1" >> /etc/tor/torrc
|
&& python3 -m pip list \
|
||||||
|
&& tor --version \
|
||||||
|
&& openssl version
|
||||||
|
|
||||||
#Add Zeronet source
|
#Add Zeronet source
|
||||||
ADD . /root
|
COPY . /root
|
||||||
VOLUME /root/data
|
VOLUME /root/data
|
||||||
|
|
||||||
#Control if Tor proxy is started
|
#Control if Tor proxy is started
|
||||||
ENV ENABLE_TOR false
|
ENV ENABLE_TOR true
|
||||||
|
|
||||||
|
WORKDIR /root
|
||||||
|
|
||||||
#Set upstart command
|
#Set upstart command
|
||||||
CMD cd /root && (! ${ENABLE_TOR} || /etc/init.d/tor start) && python zeronet.py --ui_ip 0.0.0.0
|
CMD (! ${ENABLE_TOR} || tor&) && python3 zeronet.py --ui_ip 0.0.0.0 --fileserver_port 26117
|
||||||
|
|
||||||
#Expose ports
|
#Expose ports
|
||||||
EXPOSE 43110
|
EXPOSE 43110 26117
|
||||||
EXPOSE 15441
|
|
||||||
|
|
34
Dockerfile.arm64v8
Normal file
34
Dockerfile.arm64v8
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
FROM alpine:3.12
|
||||||
|
|
||||||
|
#Base settings
|
||||||
|
ENV HOME /root
|
||||||
|
|
||||||
|
COPY requirements.txt /root/requirements.txt
|
||||||
|
|
||||||
|
#Install ZeroNet
|
||||||
|
RUN apk --update --no-cache --no-progress add python3 python3-dev gcc libffi-dev musl-dev make tor openssl \
|
||||||
|
&& pip3 install -r /root/requirements.txt \
|
||||||
|
&& apk del python3-dev gcc libffi-dev musl-dev make \
|
||||||
|
&& echo "ControlPort 9051" >> /etc/tor/torrc \
|
||||||
|
&& echo "CookieAuthentication 1" >> /etc/tor/torrc
|
||||||
|
|
||||||
|
RUN python3 -V \
|
||||||
|
&& python3 -m pip list \
|
||||||
|
&& tor --version \
|
||||||
|
&& openssl version
|
||||||
|
|
||||||
|
#Add Zeronet source
|
||||||
|
COPY . /root
|
||||||
|
VOLUME /root/data
|
||||||
|
|
||||||
|
#Control if Tor proxy is started
|
||||||
|
ENV ENABLE_TOR false
|
||||||
|
|
||||||
|
WORKDIR /root
|
||||||
|
|
||||||
|
#Set upstart command
|
||||||
|
CMD (! ${ENABLE_TOR} || tor&) && python3 zeronet.py --ui_ip 0.0.0.0 --fileserver_port 26552
|
||||||
|
|
||||||
|
#Expose ports
|
||||||
|
EXPOSE 43110 26552
|
||||||
|
|
367
LICENSE
367
LICENSE
|
@ -1,340 +1,27 @@
|
||||||
GNU GENERAL PUBLIC LICENSE
|
This program is free software: you can redistribute it and/or modify
|
||||||
Version 2, June 1991
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, version 3.
|
||||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
|
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
This program is distributed in the hope that it will be useful,
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
of this license document, but changing it is not allowed.
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
Preamble
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
The licenses for most software are designed to take away your
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
freedom to share and change it. By contrast, the GNU General Public
|
|
||||||
License is intended to guarantee your freedom to share and change free
|
|
||||||
software--to make sure the software is free for all its users. This
|
Additional Conditions :
|
||||||
General Public License applies to most of the Free Software
|
|
||||||
Foundation's software and to any other program whose authors commit to
|
Contributing to this repo
|
||||||
using it. (Some other Free Software Foundation software is covered by
|
This repo is governed by GPLv3, same is located at the root of the ZeroNet git repo,
|
||||||
the GNU Lesser General Public License instead.) You can apply it to
|
unless specified separately all code is governed by that license, contributions to this repo
|
||||||
your programs, too.
|
are divided into two key types, key contributions and non-key contributions, key contributions
|
||||||
|
are which, directly affects the code performance, quality and features of software,
|
||||||
When we speak of free software, we are referring to freedom, not
|
non key contributions include things like translation datasets, image, graphic or video
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
contributions that does not affect the main usability of software but improves the existing
|
||||||
have the freedom to distribute copies of free software (and charge for
|
usability of certain thing or feature, these also include tests written with code, since their
|
||||||
this service if you wish), that you receive source code or can get it
|
purpose is to check, whether something is working or not as intended. All the non-key contributions
|
||||||
if you want it, that you can change the software or use pieces of it
|
are governed by [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/), unless specified
|
||||||
in new free programs; and that you know you can do these things.
|
above, a contribution is ruled by the type of contribution if there is a conflict between two
|
||||||
|
contributing parties of repo in any case.
|
||||||
To protect your rights, we need to make restrictions that forbid
|
|
||||||
anyone to deny you these rights or to ask you to surrender the rights.
|
|
||||||
These restrictions translate to certain responsibilities for you if you
|
|
||||||
distribute copies of the software, or if you modify it.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must give the recipients all the rights that
|
|
||||||
you have. You must make sure that they, too, receive or can get the
|
|
||||||
source code. And you must show them these terms so they know their
|
|
||||||
rights.
|
|
||||||
|
|
||||||
We protect your rights with two steps: (1) copyright the software, and
|
|
||||||
(2) offer you this license which gives you legal permission to copy,
|
|
||||||
distribute and/or modify the software.
|
|
||||||
|
|
||||||
Also, for each author's protection and ours, we want to make certain
|
|
||||||
that everyone understands that there is no warranty for this free
|
|
||||||
software. If the software is modified by someone else and passed on, we
|
|
||||||
want its recipients to know that what they have is not the original, so
|
|
||||||
that any problems introduced by others will not reflect on the original
|
|
||||||
authors' reputations.
|
|
||||||
|
|
||||||
Finally, any free program is threatened constantly by software
|
|
||||||
patents. We wish to avoid the danger that redistributors of a free
|
|
||||||
program will individually obtain patent licenses, in effect making the
|
|
||||||
program proprietary. To prevent this, we have made it clear that any
|
|
||||||
patent must be licensed for everyone's free use or not licensed at all.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
||||||
|
|
||||||
0. This License applies to any program or other work which contains
|
|
||||||
a notice placed by the copyright holder saying it may be distributed
|
|
||||||
under the terms of this General Public License. The "Program", below,
|
|
||||||
refers to any such program or work, and a "work based on the Program"
|
|
||||||
means either the Program or any derivative work under copyright law:
|
|
||||||
that is to say, a work containing the Program or a portion of it,
|
|
||||||
either verbatim or with modifications and/or translated into another
|
|
||||||
language. (Hereinafter, translation is included without limitation in
|
|
||||||
the term "modification".) Each licensee is addressed as "you".
|
|
||||||
|
|
||||||
Activities other than copying, distribution and modification are not
|
|
||||||
covered by this License; they are outside its scope. The act of
|
|
||||||
running the Program is not restricted, and the output from the Program
|
|
||||||
is covered only if its contents constitute a work based on the
|
|
||||||
Program (independent of having been made by running the Program).
|
|
||||||
Whether that is true depends on what the Program does.
|
|
||||||
|
|
||||||
1. You may copy and distribute verbatim copies of the Program's
|
|
||||||
source code as you receive it, in any medium, provided that you
|
|
||||||
conspicuously and appropriately publish on each copy an appropriate
|
|
||||||
copyright notice and disclaimer of warranty; keep intact all the
|
|
||||||
notices that refer to this License and to the absence of any warranty;
|
|
||||||
and give any other recipients of the Program a copy of this License
|
|
||||||
along with the Program.
|
|
||||||
|
|
||||||
You may charge a fee for the physical act of transferring a copy, and
|
|
||||||
you may at your option offer warranty protection in exchange for a fee.
|
|
||||||
|
|
||||||
2. You may modify your copy or copies of the Program or any portion
|
|
||||||
of it, thus forming a work based on the Program, and copy and
|
|
||||||
distribute such modifications or work under the terms of Section 1
|
|
||||||
above, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) You must cause the modified files to carry prominent notices
|
|
||||||
stating that you changed the files and the date of any change.
|
|
||||||
|
|
||||||
b) You must cause any work that you distribute or publish, that in
|
|
||||||
whole or in part contains or is derived from the Program or any
|
|
||||||
part thereof, to be licensed as a whole at no charge to all third
|
|
||||||
parties under the terms of this License.
|
|
||||||
|
|
||||||
c) If the modified program normally reads commands interactively
|
|
||||||
when run, you must cause it, when started running for such
|
|
||||||
interactive use in the most ordinary way, to print or display an
|
|
||||||
announcement including an appropriate copyright notice and a
|
|
||||||
notice that there is no warranty (or else, saying that you provide
|
|
||||||
a warranty) and that users may redistribute the program under
|
|
||||||
these conditions, and telling the user how to view a copy of this
|
|
||||||
License. (Exception: if the Program itself is interactive but
|
|
||||||
does not normally print such an announcement, your work based on
|
|
||||||
the Program is not required to print an announcement.)
|
|
||||||
|
|
||||||
These requirements apply to the modified work as a whole. If
|
|
||||||
identifiable sections of that work are not derived from the Program,
|
|
||||||
and can be reasonably considered independent and separate works in
|
|
||||||
themselves, then this License, and its terms, do not apply to those
|
|
||||||
sections when you distribute them as separate works. But when you
|
|
||||||
distribute the same sections as part of a whole which is a work based
|
|
||||||
on the Program, the distribution of the whole must be on the terms of
|
|
||||||
this License, whose permissions for other licensees extend to the
|
|
||||||
entire whole, and thus to each and every part regardless of who wrote it.
|
|
||||||
|
|
||||||
Thus, it is not the intent of this section to claim rights or contest
|
|
||||||
your rights to work written entirely by you; rather, the intent is to
|
|
||||||
exercise the right to control the distribution of derivative or
|
|
||||||
collective works based on the Program.
|
|
||||||
|
|
||||||
In addition, mere aggregation of another work not based on the Program
|
|
||||||
with the Program (or with a work based on the Program) on a volume of
|
|
||||||
a storage or distribution medium does not bring the other work under
|
|
||||||
the scope of this License.
|
|
||||||
|
|
||||||
3. You may copy and distribute the Program (or a work based on it,
|
|
||||||
under Section 2) in object code or executable form under the terms of
|
|
||||||
Sections 1 and 2 above provided that you also do one of the following:
|
|
||||||
|
|
||||||
a) Accompany it with the complete corresponding machine-readable
|
|
||||||
source code, which must be distributed under the terms of Sections
|
|
||||||
1 and 2 above on a medium customarily used for software interchange; or,
|
|
||||||
|
|
||||||
b) Accompany it with a written offer, valid for at least three
|
|
||||||
years, to give any third party, for a charge no more than your
|
|
||||||
cost of physically performing source distribution, a complete
|
|
||||||
machine-readable copy of the corresponding source code, to be
|
|
||||||
distributed under the terms of Sections 1 and 2 above on a medium
|
|
||||||
customarily used for software interchange; or,
|
|
||||||
|
|
||||||
c) Accompany it with the information you received as to the offer
|
|
||||||
to distribute corresponding source code. (This alternative is
|
|
||||||
allowed only for noncommercial distribution and only if you
|
|
||||||
received the program in object code or executable form with such
|
|
||||||
an offer, in accord with Subsection b above.)
|
|
||||||
|
|
||||||
The source code for a work means the preferred form of the work for
|
|
||||||
making modifications to it. For an executable work, complete source
|
|
||||||
code means all the source code for all modules it contains, plus any
|
|
||||||
associated interface definition files, plus the scripts used to
|
|
||||||
control compilation and installation of the executable. However, as a
|
|
||||||
special exception, the source code distributed need not include
|
|
||||||
anything that is normally distributed (in either source or binary
|
|
||||||
form) with the major components (compiler, kernel, and so on) of the
|
|
||||||
operating system on which the executable runs, unless that component
|
|
||||||
itself accompanies the executable.
|
|
||||||
|
|
||||||
If distribution of executable or object code is made by offering
|
|
||||||
access to copy from a designated place, then offering equivalent
|
|
||||||
access to copy the source code from the same place counts as
|
|
||||||
distribution of the source code, even though third parties are not
|
|
||||||
compelled to copy the source along with the object code.
|
|
||||||
|
|
||||||
4. You may not copy, modify, sublicense, or distribute the Program
|
|
||||||
except as expressly provided under this License. Any attempt
|
|
||||||
otherwise to copy, modify, sublicense or distribute the Program is
|
|
||||||
void, and will automatically terminate your rights under this License.
|
|
||||||
However, parties who have received copies, or rights, from you under
|
|
||||||
this License will not have their licenses terminated so long as such
|
|
||||||
parties remain in full compliance.
|
|
||||||
|
|
||||||
5. You are not required to accept this License, since you have not
|
|
||||||
signed it. However, nothing else grants you permission to modify or
|
|
||||||
distribute the Program or its derivative works. These actions are
|
|
||||||
prohibited by law if you do not accept this License. Therefore, by
|
|
||||||
modifying or distributing the Program (or any work based on the
|
|
||||||
Program), you indicate your acceptance of this License to do so, and
|
|
||||||
all its terms and conditions for copying, distributing or modifying
|
|
||||||
the Program or works based on it.
|
|
||||||
|
|
||||||
6. Each time you redistribute the Program (or any work based on the
|
|
||||||
Program), the recipient automatically receives a license from the
|
|
||||||
original licensor to copy, distribute or modify the Program subject to
|
|
||||||
these terms and conditions. You may not impose any further
|
|
||||||
restrictions on the recipients' exercise of the rights granted herein.
|
|
||||||
You are not responsible for enforcing compliance by third parties to
|
|
||||||
this License.
|
|
||||||
|
|
||||||
7. If, as a consequence of a court judgment or allegation of patent
|
|
||||||
infringement or for any other reason (not limited to patent issues),
|
|
||||||
conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot
|
|
||||||
distribute so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you
|
|
||||||
may not distribute the Program at all. For example, if a patent
|
|
||||||
license would not permit royalty-free redistribution of the Program by
|
|
||||||
all those who receive copies directly or indirectly through you, then
|
|
||||||
the only way you could satisfy both it and this License would be to
|
|
||||||
refrain entirely from distribution of the Program.
|
|
||||||
|
|
||||||
If any portion of this section is held invalid or unenforceable under
|
|
||||||
any particular circumstance, the balance of the section is intended to
|
|
||||||
apply and the section as a whole is intended to apply in other
|
|
||||||
circumstances.
|
|
||||||
|
|
||||||
It is not the purpose of this section to induce you to infringe any
|
|
||||||
patents or other property right claims or to contest validity of any
|
|
||||||
such claims; this section has the sole purpose of protecting the
|
|
||||||
integrity of the free software distribution system, which is
|
|
||||||
implemented by public license practices. Many people have made
|
|
||||||
generous contributions to the wide range of software distributed
|
|
||||||
through that system in reliance on consistent application of that
|
|
||||||
system; it is up to the author/donor to decide if he or she is willing
|
|
||||||
to distribute software through any other system and a licensee cannot
|
|
||||||
impose that choice.
|
|
||||||
|
|
||||||
This section is intended to make thoroughly clear what is believed to
|
|
||||||
be a consequence of the rest of this License.
|
|
||||||
|
|
||||||
8. If the distribution and/or use of the Program is restricted in
|
|
||||||
certain countries either by patents or by copyrighted interfaces, the
|
|
||||||
original copyright holder who places the Program under this License
|
|
||||||
may add an explicit geographical distribution limitation excluding
|
|
||||||
those countries, so that distribution is permitted only in or among
|
|
||||||
countries not thus excluded. In such case, this License incorporates
|
|
||||||
the limitation as if written in the body of this License.
|
|
||||||
|
|
||||||
9. The Free Software Foundation may publish revised and/or new versions
|
|
||||||
of the General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the Program
|
|
||||||
specifies a version number of this License which applies to it and "any
|
|
||||||
later version", you have the option of following the terms and conditions
|
|
||||||
either of that version or of any later version published by the Free
|
|
||||||
Software Foundation. If the Program does not specify a version number of
|
|
||||||
this License, you may choose any version ever published by the Free Software
|
|
||||||
Foundation.
|
|
||||||
|
|
||||||
10. If you wish to incorporate parts of the Program into other free
|
|
||||||
programs whose distribution conditions are different, write to the author
|
|
||||||
to ask for permission. For software which is copyrighted by the Free
|
|
||||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
|
||||||
make exceptions for this. Our decision will be guided by the two goals
|
|
||||||
of preserving the free status of all derivatives of our free software and
|
|
||||||
of promoting the sharing and reuse of software generally.
|
|
||||||
|
|
||||||
NO WARRANTY
|
|
||||||
|
|
||||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
|
||||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
|
||||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
|
||||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
|
||||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
||||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
|
||||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
|
||||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
|
||||||
REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
|
||||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
|
||||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
|
||||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
|
||||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
|
||||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
|
||||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
|
||||||
POSSIBILITY OF SUCH DAMAGES.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
convey the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
{description}
|
|
||||||
Copyright (C) {year} {fullname}
|
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation; either version 2 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along
|
|
||||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program is interactive, make it output a short notice like this
|
|
||||||
when it starts in an interactive mode:
|
|
||||||
|
|
||||||
Gnomovision version 69, Copyright (C) year name of author
|
|
||||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, the commands you use may
|
|
||||||
be called something other than `show w' and `show c'; they could even be
|
|
||||||
mouse-clicks or menu items--whatever suits your program.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or your
|
|
||||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
|
||||||
necessary. Here is a sample; alter the names:
|
|
||||||
|
|
||||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
|
||||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
|
||||||
|
|
||||||
{signature of Ty Coon}, 1 April 1989
|
|
||||||
Ty Coon, President of Vice
|
|
||||||
|
|
||||||
This General Public License does not permit incorporating your program into
|
|
||||||
proprietary programs. If your program is a subroutine library, you may
|
|
||||||
consider it more useful to permit linking proprietary applications with the
|
|
||||||
library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License.
|
|
||||||
|
|
||||||
|
|
133
README-ru.md
Normal file
133
README-ru.md
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
# ZeroNet [](https://github.com/ZeroNetX/ZeroNet/actions/workflows/tests.yml) [](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/faq/) [](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/) [](https://hub.docker.com/r/canewsin/zeronet)
|
||||||
|
|
||||||
|
[简体中文](./README-zh-cn.md)
|
||||||
|
[English](./README.md)
|
||||||
|
|
||||||
|
Децентрализованные вебсайты, использующие криптографию Bitcoin и протокол BitTorrent — https://zeronet.dev ([Зеркало в ZeroNet](http://127.0.0.1:43110/1ZeroNetyV5mKY9JF1gsm82TuBXHpfdLX/)). В отличии от Bitcoin, ZeroNet'у не требуется блокчейн для работы, однако он использует ту же криптографию, чтобы обеспечить сохранность и проверку данных.
|
||||||
|
|
||||||
|
## Зачем?
|
||||||
|
|
||||||
|
- Мы верим в открытую, свободную, и неподдающуюся цензуре сеть и связь.
|
||||||
|
- Нет единой точки отказа: Сайт остаётся онлайн, пока его обслуживает хотя бы 1 пир.
|
||||||
|
- Нет затрат на хостинг: Сайты обслуживаются посетителями.
|
||||||
|
- Невозможно отключить: Он нигде, потому что он везде.
|
||||||
|
- Скорость и возможность работать без Интернета: Вы сможете получить доступ к сайту, потому что его копия хранится на вашем компьютере и у ваших пиров.
|
||||||
|
|
||||||
|
## Особенности
|
||||||
|
|
||||||
|
- Обновление сайтов в реальном времени
|
||||||
|
- Поддержка доменов `.bit` ([Namecoin](https://www.namecoin.org))
|
||||||
|
- Легкая установка: просто распакуйте и запустите
|
||||||
|
- Клонирование сайтов "в один клик"
|
||||||
|
- Беспарольная [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
|
||||||
|
авторизация: Ваша учетная запись защищена той же криптографией, что и ваш Bitcoin-кошелек
|
||||||
|
- Встроенный SQL-сервер с синхронизацией данных P2P: Позволяет упростить разработку сайта и ускорить загрузку страницы
|
||||||
|
- Анонимность: Полная поддержка сети Tor, используя скрытые службы `.onion` вместо адресов IPv4
|
||||||
|
- Зашифрованное TLS подключение
|
||||||
|
- Автоматическое открытие UPnP–порта
|
||||||
|
- Плагин для поддержки нескольких пользователей (openproxy)
|
||||||
|
- Работа с любыми браузерами и операционными системами
|
||||||
|
|
||||||
|
## Текущие ограничения
|
||||||
|
|
||||||
|
- Файловые транзакции не сжаты
|
||||||
|
- Нет приватных сайтов
|
||||||
|
|
||||||
|
## Как это работает?
|
||||||
|
|
||||||
|
- После запуска `zeronet.py` вы сможете посещать сайты в ZeroNet, используя адрес
|
||||||
|
`http://127.0.0.1:43110/{zeronet_адрес}`
|
||||||
|
(Например: `http://127.0.0.1:43110/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d`).
|
||||||
|
- Когда вы посещаете новый сайт в ZeroNet, он пытается найти пиров с помощью протокола BitTorrent,
|
||||||
|
чтобы скачать у них файлы сайта (HTML, CSS, JS и т.д.).
|
||||||
|
- После посещения сайта вы тоже становитесь его пиром.
|
||||||
|
- Каждый сайт содержит файл `content.json`, который содержит SHA512 хеши всех остальные файлы
|
||||||
|
и подпись, созданную с помощью закрытого ключа сайта.
|
||||||
|
- Если владелец сайта (тот, кто владеет закрытым ключом для адреса сайта) изменяет сайт, он
|
||||||
|
подписывает новый `content.json` и публикует его для пиров. После этого пиры проверяют целостность `content.json`
|
||||||
|
(используя подпись), скачвают изменённые файлы и распространяют новый контент для других пиров.
|
||||||
|
|
||||||
|
[Презентация о криптографии ZeroNet, обновлениях сайтов, многопользовательских сайтах »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000)
|
||||||
|
[Часто задаваемые вопросы »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/faq/)
|
||||||
|
[Документация разработчика ZeroNet »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/site_development/getting_started/)
|
||||||
|
|
||||||
|
## Скриншоты
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
[Больше скриншотов в документации ZeroNet »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/using_zeronet/sample_sites/)
|
||||||
|
|
||||||
|
## Как присоединиться?
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
- Скачайте и распакуйте архив [ZeroNet-win.zip](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-win.zip) (26МБ)
|
||||||
|
- Запустите `ZeroNet.exe`
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
- Скачайте и распакуйте архив [ZeroNet-mac.zip](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-mac.zip) (14МБ)
|
||||||
|
- Запустите `ZeroNet.app`
|
||||||
|
|
||||||
|
### Linux (64 бит)
|
||||||
|
|
||||||
|
- Скачайте и распакуйте архив [ZeroNet-linux.zip](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-linux.zip) (14МБ)
|
||||||
|
- Запустите `./ZeroNet.sh`
|
||||||
|
|
||||||
|
> **Note**
|
||||||
|
> Запустите таким образом: `./ZeroNet.sh --ui_ip '*' --ui_restrict ваш_ip_адрес`, чтобы разрешить удалённое подключение к веб–интерфейсу.
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
Официальный образ находится здесь: https://hub.docker.com/r/canewsin/zeronet/
|
||||||
|
|
||||||
|
### Android (arm, arm64, x86)
|
||||||
|
|
||||||
|
- Для работы требуется Android как минимум версии 5.0 Lollipop
|
||||||
|
- [<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
||||||
|
alt="Download from Google Play"
|
||||||
|
height="80">](https://play.google.com/store/apps/details?id=in.canews.zeronetmobile)
|
||||||
|
- Скачать APK: https://github.com/canewsin/zeronet_mobile/releases
|
||||||
|
|
||||||
|
### Android (arm, arm64, x86) Облегчённый клиент только для просмотра (1МБ)
|
||||||
|
|
||||||
|
- Для работы требуется Android как минимум версии 4.1 Jelly Bean
|
||||||
|
- [<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
||||||
|
alt="Download from Google Play"
|
||||||
|
height="80">](https://play.google.com/store/apps/details?id=dev.zeronetx.app.lite)
|
||||||
|
|
||||||
|
### Установка из исходного кода
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wget https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-src.zip
|
||||||
|
unzip ZeroNet-src.zip
|
||||||
|
cd ZeroNet
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install python3-pip
|
||||||
|
sudo python3 -m pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
- Запустите `python3 zeronet.py`
|
||||||
|
|
||||||
|
Откройте приветственную страницу ZeroHello в вашем браузере по ссылке http://127.0.0.1:43110/
|
||||||
|
|
||||||
|
## Как мне создать сайт в ZeroNet?
|
||||||
|
|
||||||
|
- Кликните на **⋮** > **"Create new, empty site"** в меню на сайте [ZeroHello](http://127.0.0.1:43110/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d).
|
||||||
|
- Вы будете **перенаправлены** на совершенно новый сайт, который может быть изменён только вами!
|
||||||
|
- Вы можете найти и изменить контент вашего сайта в каталоге **data/[адрес_вашего_сайта]**
|
||||||
|
- После изменений откройте ваш сайт, переключите влево кнопку "0" в правом верхнем углу, затем нажмите кнопки **sign** и **publish** внизу
|
||||||
|
|
||||||
|
Следующие шаги: [Документация разработчика ZeroNet](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/site_development/getting_started/)
|
||||||
|
|
||||||
|
## Поддержите проект
|
||||||
|
|
||||||
|
- Bitcoin: 1ZeroNetyV5mKY9JF1gsm82TuBXHpfdLX (Рекомендуем)
|
||||||
|
- LiberaPay: https://liberapay.com/PramUkesh
|
||||||
|
- Paypal: https://paypal.me/PramUkesh
|
||||||
|
- Другие способы: [Donate](!https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/#help-to-keep-zeronet-development-alive)
|
||||||
|
|
||||||
|
#### Спасибо!
|
||||||
|
|
||||||
|
- Здесь вы можете получить больше информации, помощь, прочитать список изменений и исследовать ZeroNet сайты: https://www.reddit.com/r/zeronetx/
|
||||||
|
- Общение происходит на канале [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) или в [Gitter](https://gitter.im/canewsin/ZeroNet)
|
||||||
|
- Электронная почта: canews.in@gmail.com
|
219
README-zh-cn.md
219
README-zh-cn.md
|
@ -1,187 +1,132 @@
|
||||||
# ZeroNet [](https://travis-ci.org/HelloZeroNet/ZeroNet) [](https://zeronet.readthedocs.org/en/latest/faq/) [](https://zeronet.readthedocs.org/en/latest/help_zeronet/donate/)
|
# ZeroNet [](https://github.com/ZeroNetX/ZeroNet/actions/workflows/tests.yml) [](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/faq/) [](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/) [](https://hub.docker.com/r/canewsin/zeronet)
|
||||||
|
|
||||||
[English](./README.md)
|
[English](./README.md)
|
||||||
|
|
||||||
使用 Bitcoin 加密和 BitTorrent 网络的去中心化网络 - https://zeronet.io
|
使用 Bitcoin 加密和 BitTorrent 网络的去中心化网络 - https://zeronet.dev
|
||||||
|
|
||||||
|
|
||||||
## 为什么?
|
## 为什么?
|
||||||
|
|
||||||
* 我们相信开放,自由,无审查的网络
|
* 我们相信开放,自由,无审查的网络和通讯
|
||||||
* 不会受单点故障影响:只要有在线的节点,站点就会保持在线
|
* 不会受单点故障影响:只要有在线的节点,站点就会保持在线
|
||||||
* 无托管费用: 站点由访问者托管
|
* 无托管费用:站点由访问者托管
|
||||||
* 无法关闭: 因为节点无处不在
|
* 无法关闭:因为节点无处不在
|
||||||
* 快速并可离线运行: 即使没有互联网连接也可以使用
|
* 快速并可离线运行:即使没有互联网连接也可以使用
|
||||||
|
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
* 实时站点更新
|
* 实时站点更新
|
||||||
* 支持 Namecoin 的 .bit 域名
|
* 支持 Namecoin 的 .bit 域名
|
||||||
* 安装方便: 只需解压并运行
|
* 安装方便:只需解压并运行
|
||||||
* 一键克隆存在的站点
|
* 一键克隆存在的站点
|
||||||
* 无需密码、基于 [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) 的认证:用与比特币钱包相同的加密方法用来保护你的账户
|
* 无需密码、基于 [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
|
||||||
你的账户被使用和比特币钱包相同的加密方法
|
的认证:您的账户被与比特币钱包相同的加密方法保护
|
||||||
* 内建 SQL 服务器和 P2P 数据同步: 让开发更简单并提升加载速度
|
* 内建 SQL 服务器和 P2P 数据同步:让开发更简单并提升加载速度
|
||||||
* 匿名性: 完整的 Tor 网络支持,支持通过 .onion 隐藏服务相互连接而不是通过IPv4地址连接
|
* 匿名性:完整的 Tor 网络支持,支持通过 .onion 隐藏服务相互连接而不是通过 IPv4 地址连接
|
||||||
* TLS 加密连接
|
* TLS 加密连接
|
||||||
* 自动打开 uPnP 端口
|
* 自动打开 uPnP 端口
|
||||||
* 插件和多用户 (开放式代理) 支持
|
* 多用户(openproxy)支持的插件
|
||||||
* 全平台兼容
|
* 适用于任何浏览器 / 操作系统
|
||||||
|
|
||||||
|
|
||||||
## 原理
|
## 原理
|
||||||
|
|
||||||
* 在你运行`zeronet.py`后你将可以通过`http://127.0.0.1:43110/{zeronet_address}` (比如.
|
* 在运行 `zeronet.py` 后,您将可以通过
|
||||||
`http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`)。访问 zeronet 中的站点。
|
`http://127.0.0.1:43110/{zeronet_address}`(例如:
|
||||||
|
`http://127.0.0.1:43110/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d`)访问 zeronet 中的站点
|
||||||
|
* 在您浏览 zeronet 站点时,客户端会尝试通过 BitTorrent 网络来寻找可用的节点,从而下载需要的文件(html,css,js...)
|
||||||
|
* 您将会储存每一个浏览过的站点
|
||||||
|
* 每个站点都包含一个名为 `content.json` 的文件,它储存了其他所有文件的 sha512 散列值以及一个通过站点私钥生成的签名
|
||||||
|
* 如果站点的所有者(拥有站点地址的私钥)修改了站点,并且他 / 她签名了新的 `content.json` 然后推送至其他节点,
|
||||||
|
那么这些节点将会在使用签名验证 `content.json` 的真实性后,下载修改后的文件并将新内容推送至另外的节点
|
||||||
|
|
||||||
* 在你浏览 zeronet 站点时,客户端会尝试通过 BitTorrent 网络来寻找可用的节点,从而下载需要的文件 (html, css, js...)
|
#### [关于 ZeroNet 加密,站点更新,多用户站点的幻灯片 »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000)
|
||||||
|
#### [常见问题 »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/faq/)
|
||||||
|
|
||||||
* 你将会储存每一个浏览过的站点
|
#### [ZeroNet 开发者文档 »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/site_development/getting_started/)
|
||||||
* 每个站点都包含一个名为 `content.json` ,它储存了其他所有文件的 sha512 hash 值
|
|
||||||
和一个通过站点私钥建立的签名
|
|
||||||
* 如果站点的所有者 (拥有私钥的那个人) 修改了站点, 并且他/她签名了新的 `content.json` 然后推送至其他节点,
|
|
||||||
那么所有节点将会在验证 `content.json` 的真实性 (使用签名)后, 下载修改后的文件并推送至其他节点。
|
|
||||||
|
|
||||||
#### [有关于 ZeroNet 加密, 站点更新, 多用户站点的幻灯片 »](https://docs.google.com/presentation/d/1qBxkroB_iiX2zHEn0dt-N-qRZgyEzui46XS2hEa3AA4/pub?start=false&loop=false&delayms=3000)
|
|
||||||
#### [常见问题 »](https://zeronet.readthedocs.org/en/latest/faq/)
|
|
||||||
|
|
||||||
#### [ZeroNet开发者文档 »](https://zeronet.readthedocs.org/en/latest/site_development/getting_started/)
|
|
||||||
|
|
||||||
|
|
||||||
## 屏幕截图
|
## 屏幕截图
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
#### [在 ZeroNet 文档里查看更多的屏幕截图 »](https://zeronet.readthedocs.org/en/latest/using_zeronet/sample_sites/)
|
#### [ZeroNet 文档中的更多屏幕截图 »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/using_zeronet/sample_sites/)
|
||||||
|
|
||||||
|
|
||||||
## 如何加入 ?
|
## 如何加入
|
||||||
|
|
||||||
* 下载 ZeroBundle 文件包:
|
### Windows
|
||||||
* [Microsoft Windows](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-win.zip)
|
|
||||||
* [Apple OS X](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-mac-osx.zip)
|
|
||||||
* [Linux 64bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz)
|
|
||||||
* [Linux 32bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux32.tar.gz)
|
|
||||||
* 解压缩
|
|
||||||
* 运行 `ZeroNet.cmd` (win), `ZeroNet(.app)` (osx), `ZeroNet.sh` (linux)
|
|
||||||
|
|
||||||
如果你在 OS X 上遇到了 "classic environment no longer supported" 错误,请打开一个终端然后把 ZeroNet.app 拖进去
|
- 下载 [ZeroNet-win.zip](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-win.zip) (26MB)
|
||||||
|
- 在任意位置解压缩
|
||||||
|
- 运行 `ZeroNet.exe`
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
在你打开时他将会自动下载最新版本的 ZeroNet 。
|
- 下载 [ZeroNet-mac.zip](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-mac.zip) (14MB)
|
||||||
|
- 在任意位置解压缩
|
||||||
|
- 运行 `ZeroNet.app`
|
||||||
|
|
||||||
|
### Linux (x86-64bit)
|
||||||
|
|
||||||
### Linux 命令行
|
- `wget https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-linux.zip`
|
||||||
|
- `unzip ZeroNet-linux.zip`
|
||||||
|
- `cd ZeroNet-linux`
|
||||||
|
- 使用以下命令启动 `./ZeroNet.sh`
|
||||||
|
- 在浏览器打开 http://127.0.0.1:43110/ 即可访问 ZeroHello 页面
|
||||||
|
|
||||||
|
__提示:__ 若要允许在 Web 界面上的远程连接,使用以下命令启动 `./ZeroNet.sh --ui_ip '*' --ui_restrict your.ip.address`
|
||||||
|
|
||||||
* `wget https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz`
|
### 从源代码安装
|
||||||
* `tar xvpfz ZeroBundle-linux64.tar.gz`
|
|
||||||
* `cd ZeroBundle`
|
|
||||||
* 执行 `./ZeroNet.sh` 来启动
|
|
||||||
|
|
||||||
在你打开时他将会自动下载最新版本的 ZeroNet 。
|
- `wget https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-src.zip`
|
||||||
|
- `unzip ZeroNet-src.zip`
|
||||||
|
- `cd ZeroNet`
|
||||||
|
- `sudo apt-get update`
|
||||||
|
- `sudo apt-get install python3-pip`
|
||||||
|
- `sudo python3 -m pip install -r requirements.txt`
|
||||||
|
- 使用以下命令启动 `python3 zeronet.py`
|
||||||
|
- 在浏览器打开 http://127.0.0.1:43110/ 即可访问 ZeroHello 页面
|
||||||
|
|
||||||
#### 在 Debian Linux 中手动安装
|
### Android (arm, arm64, x86)
|
||||||
|
- minimum Android version supported 21 (Android 5.0 Lollipop)
|
||||||
|
- [<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
||||||
|
alt="Download from Google Play"
|
||||||
|
height="80">](https://play.google.com/store/apps/details?id=in.canews.zeronetmobile)
|
||||||
|
- APK download: https://github.com/canewsin/zeronet_mobile/releases
|
||||||
|
|
||||||
* `sudo apt-get update`
|
### Android (arm, arm64, x86) Thin Client for Preview Only (Size 1MB)
|
||||||
* `sudo apt-get install msgpack-python python-gevent`
|
- minimum Android version supported 16 (JellyBean)
|
||||||
* `wget https://github.com/HelloZeroNet/ZeroNet/archive/master.tar.gz`
|
- [<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
||||||
* `tar xvpfz master.tar.gz`
|
alt="Download from Google Play"
|
||||||
* `cd ZeroNet-master`
|
height="80">](https://play.google.com/store/apps/details?id=dev.zeronetx.app.lite)
|
||||||
* 执行 `python zeronet.py` 来启动
|
|
||||||
* 在你的浏览器中打开 http://127.0.0.1:43110/
|
|
||||||
|
|
||||||
|
|
||||||
### [Vagrant](https://www.vagrantup.com/)
|
|
||||||
|
|
||||||
* `vagrant up`
|
|
||||||
* 通过 `vagrant ssh` 连接到 VM
|
|
||||||
* `cd /vagrant`
|
|
||||||
* 运行 `python zeronet.py --ui_ip 0.0.0.0`
|
|
||||||
* 在你的浏览器中打开 http://127.0.0.1:43110/
|
|
||||||
|
|
||||||
### [Docker](https://www.docker.com/)
|
|
||||||
* `docker run -d -v <local_data_folder>:/root/data -p 15441:15441 -p 43110:43110 nofish/zeronet`
|
|
||||||
* 这个 Docker 镜像包含了 Tor ,但默认是禁用的,因为一些托管商不允许你在他们的服务器上运行 Tor。如果你希望启用它,
|
|
||||||
设置 `ENABLE_TOR` 环境变量为 `true` (默认: `false`). E.g.:
|
|
||||||
|
|
||||||
`docker run -d -e "ENABLE_TOR=true" -v <local_data_folder>:/root/data -p 15441:15441 -p 43110:43110 nofish/zeronet`
|
|
||||||
* 在你的浏览器中打开 http://127.0.0.1:43110/
|
|
||||||
|
|
||||||
### [Virtualenv](https://virtualenv.readthedocs.org/en/latest/)
|
|
||||||
|
|
||||||
* `virtualenv env`
|
|
||||||
* `source env/bin/activate`
|
|
||||||
* `pip install msgpack-python gevent`
|
|
||||||
* `python zeronet.py`
|
|
||||||
* 在你的浏览器中打开 http://127.0.0.1:43110/
|
|
||||||
|
|
||||||
## 现有限制
|
## 现有限制
|
||||||
|
|
||||||
* 没有类似于 BitTorrent 的文件拆分来支持大文件
|
* 传输文件时没有压缩
|
||||||
* ~~没有比 BitTorrent 更好的匿名性~~ (已添加内置的完整 Tor 支持)
|
|
||||||
* 传输文件时没有压缩~~和加密~~ (已添加 TLS 支持)
|
|
||||||
* 不支持私有站点
|
* 不支持私有站点
|
||||||
|
|
||||||
|
|
||||||
## 如何创建一个 ZeroNet 站点?
|
## 如何创建一个 ZeroNet 站点?
|
||||||
|
|
||||||
|
* 点击 [ZeroHello](http://127.0.0.1:43110/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d) 站点的 **⋮** > **「新建空站点」** 菜单项
|
||||||
|
* 您将被**重定向**到一个全新的站点,该站点只能由您修改
|
||||||
|
* 您可以在 **data/[您的站点地址]** 目录中找到并修改网站的内容
|
||||||
|
* 修改后打开您的网站,将右上角的「0」按钮拖到左侧,然后点击底部的**签名**并**发布**按钮
|
||||||
|
|
||||||
如果 zeronet 在运行,把它关掉
|
接下来的步骤:[ZeroNet 开发者文档](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/site_development/getting_started/)
|
||||||
执行:
|
|
||||||
```bash
|
|
||||||
$ zeronet.py siteCreate
|
|
||||||
...
|
|
||||||
- Site private key: 23DKQpzxhbVBrAtvLEc2uvk7DZweh4qL3fn3jpM3LgHDczMK2TtYUq
|
|
||||||
- Site address: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
|
|
||||||
...
|
|
||||||
- Site created!
|
|
||||||
$ zeronet.py
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
你已经完成了! 现在任何人都可以通过
|
|
||||||
`http://localhost:43110/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2`
|
|
||||||
来访问你的站点
|
|
||||||
|
|
||||||
下一步: [ZeroNet 开发者文档](https://zeronet.readthedocs.org/en/latest/site_development/getting_started/)
|
|
||||||
|
|
||||||
|
|
||||||
## 我要如何修改 ZeroNet 站点?
|
|
||||||
|
|
||||||
* 修改位于 data/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 的目录.
|
|
||||||
在你改好之后:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ zeronet.py siteSign 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
|
|
||||||
- Signing site: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2...
|
|
||||||
Private key (input hidden):
|
|
||||||
```
|
|
||||||
|
|
||||||
* 输入你在创建站点时获得的私钥
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ zeronet.py sitePublish 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
|
|
||||||
...
|
|
||||||
Site:13DNDk..bhC2 Publishing to 3/10 peers...
|
|
||||||
Site:13DNDk..bhC2 Successfuly published to 3 peers
|
|
||||||
- Serving files....
|
|
||||||
```
|
|
||||||
|
|
||||||
* 就是这样! 你现在已经成功的签名并推送了你的更改。
|
|
||||||
|
|
||||||
|
|
||||||
## 帮助这个项目
|
## 帮助这个项目
|
||||||
|
- Bitcoin: 1ZeroNetyV5mKY9JF1gsm82TuBXHpfdLX (Preferred)
|
||||||
|
- LiberaPay: https://liberapay.com/PramUkesh
|
||||||
|
- Paypal: https://paypal.me/PramUkesh
|
||||||
|
- Others: [Donate](!https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/#help-to-keep-zeronet-development-alive)
|
||||||
|
|
||||||
- Bitcoin: 1QDhxQ6PraUZa21ET5fYUCPgdrwBomnFgX
|
|
||||||
- Paypal: https://zeronet.readthedocs.org/en/latest/help_zeronet/donate/
|
|
||||||
- Gratipay: https://gratipay.com/zeronet/
|
|
||||||
|
|
||||||
### 赞助商
|
#### 感谢您!
|
||||||
|
|
||||||
* 在 OSX/Safari 下 [BrowserStack.com](https://www.browserstack.com) 带来更好的兼容性
|
* 更多信息,帮助,变更记录和 zeronet 站点:https://www.reddit.com/r/zeronetx/
|
||||||
|
* 前往 [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) 或 [gitter](https://gitter.im/canewsin/ZeroNet) 和我们聊天
|
||||||
#### 感谢!
|
* [这里](https://gitter.im/canewsin/ZeroNet)是一个 gitter 上的中文聊天室
|
||||||
|
* Email: canews.in@gmail.com
|
||||||
* 更多信息, 帮助, 变更记录和 zeronet 站点: https://www.reddit.com/r/zeronet/
|
|
||||||
* 在: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) 和我们聊天,或者使用 [gitter](https://gitter.im/HelloZeroNet/ZeroNet)
|
|
||||||
* [这里](https://gitter.im/ZeroNet-zh/Lobby)是一个 gitter 上的中文聊天室
|
|
||||||
* Email: hello@noloop.me
|
|
||||||
|
|
186
README.md
186
README.md
|
@ -1,8 +1,6 @@
|
||||||
# ZeroNet [](https://travis-ci.org/HelloZeroNet/ZeroNet) [](https://zeronet.readthedocs.org/en/latest/faq/) [](https://zeronet.readthedocs.org/en/latest/help_zeronet/donate/)
|
# ZeroNet [](https://github.com/ZeroNetX/ZeroNet/actions/workflows/tests.yml) [](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/faq/) [](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/) [](https://hub.docker.com/r/canewsin/zeronet)
|
||||||
|
<!--TODO: Update Onion Site -->
|
||||||
[简体中文](./README-zh-cn.md)
|
Decentralized websites using Bitcoin crypto and the BitTorrent network - https://zeronet.dev / [ZeroNet Site](http://127.0.0.1:43110/1ZeroNetyV5mKY9JF1gsm82TuBXHpfdLX/), Unlike Bitcoin, ZeroNet Doesn't need a blockchain to run, But uses cryptography used by BTC, to ensure data integrity and validation.
|
||||||
|
|
||||||
Decentralized websites using Bitcoin crypto and the BitTorrent network - https://zeronet.io
|
|
||||||
|
|
||||||
|
|
||||||
## Why?
|
## Why?
|
||||||
|
@ -35,152 +33,124 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - https:/
|
||||||
|
|
||||||
* After starting `zeronet.py` you will be able to visit zeronet sites using
|
* After starting `zeronet.py` you will be able to visit zeronet sites using
|
||||||
`http://127.0.0.1:43110/{zeronet_address}` (eg.
|
`http://127.0.0.1:43110/{zeronet_address}` (eg.
|
||||||
`http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`).
|
`http://127.0.0.1:43110/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d`).
|
||||||
* When you visit a new zeronet site, it tries to find peers using the BitTorrent
|
* When you visit a new zeronet site, it tries to find peers using the BitTorrent
|
||||||
network so it can download the site files (html, css, js...) from them.
|
network so it can download the site files (html, css, js...) from them.
|
||||||
* Each visited site is also served by you.
|
* Each visited site is also served by you.
|
||||||
* Every site contains a `content.json` file which holds all other files in a sha512 hash
|
* Every site contains a `content.json` file which holds all other files in a sha512 hash
|
||||||
and a signature generated using the site's private key.
|
and a signature generated using the site's private key.
|
||||||
* If the site owner (who has the private key for the site address) modifies the
|
* If the site owner (who has the private key for the site address) modifies the
|
||||||
site, then he/she signs the new `content.json` and publishes it to the peers.
|
site and signs the new `content.json` and publishes it to the peers.
|
||||||
Afterwards, the peers verify the `content.json` integrity (using the
|
Afterwards, the peers verify the `content.json` integrity (using the
|
||||||
signature), they download the modified files and publish the new content to
|
signature), they download the modified files and publish the new content to
|
||||||
other peers.
|
other peers.
|
||||||
|
|
||||||
#### [Slideshow about ZeroNet cryptography, site updates, multi-user sites »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000)
|
#### [Slideshow about ZeroNet cryptography, site updates, multi-user sites »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000)
|
||||||
#### [Frequently asked questions »](https://zeronet.readthedocs.org/en/latest/faq/)
|
#### [Frequently asked questions »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/faq/)
|
||||||
|
|
||||||
#### [ZeroNet Developer Documentation »](https://zeronet.readthedocs.org/en/latest/site_development/getting_started/)
|
#### [ZeroNet Developer Documentation »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/site_development/getting_started/)
|
||||||
|
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
#### [More screenshots in ZeroNet docs »](https://zeronet.readthedocs.org/en/latest/using_zeronet/sample_sites/)
|
#### [More screenshots in ZeroNet docs »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/using_zeronet/sample_sites/)
|
||||||
|
|
||||||
|
|
||||||
## How to join
|
## How to join
|
||||||
|
|
||||||
* Download ZeroBundle package:
|
### Windows
|
||||||
* [Microsoft Windows](https://github.com/HelloZeroNet/ZeroNet-win/archive/dist/ZeroNet-win.zip)
|
|
||||||
* [Apple macOS](https://github.com/HelloZeroNet/ZeroNet-mac/archive/dist/ZeroNet-mac.zip)
|
|
||||||
* [Linux 64bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz)
|
|
||||||
* [Linux 32bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux32.tar.gz)
|
|
||||||
* Unpack anywhere
|
|
||||||
* Run `ZeroNet.exe` (win), `ZeroNet(.app)` (osx), `ZeroNet.sh` (linux)
|
|
||||||
|
|
||||||
### Linux terminal
|
- Download [ZeroNet-win.zip](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-win.zip) (26MB)
|
||||||
|
- Unpack anywhere
|
||||||
|
- Run `ZeroNet.exe`
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
* `wget https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz`
|
- Download [ZeroNet-mac.zip](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-mac.zip) (14MB)
|
||||||
* `tar xvpfz ZeroBundle-linux64.tar.gz`
|
- Unpack anywhere
|
||||||
* `cd ZeroBundle`
|
- Run `ZeroNet.app`
|
||||||
* Start with `./ZeroNet.sh`
|
|
||||||
|
### Linux (x86-64bit)
|
||||||
|
- `wget https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-linux.zip`
|
||||||
|
- `unzip ZeroNet-linux.zip`
|
||||||
|
- `cd ZeroNet-linux`
|
||||||
|
- Start with: `./ZeroNet.sh`
|
||||||
|
- Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/
|
||||||
|
|
||||||
|
__Tip:__ Start with `./ZeroNet.sh --ui_ip '*' --ui_restrict your.ip.address` to allow remote connections on the web interface.
|
||||||
|
|
||||||
|
### Android (arm, arm64, x86)
|
||||||
|
- minimum Android version supported 21 (Android 5.0 Lollipop)
|
||||||
|
- [<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
||||||
|
alt="Download from Google Play"
|
||||||
|
height="80">](https://play.google.com/store/apps/details?id=in.canews.zeronetmobile)
|
||||||
|
- APK download: https://github.com/canewsin/zeronet_mobile/releases
|
||||||
|
|
||||||
It downloads the latest version of ZeroNet then starts it automatically.
|
### Android (arm, arm64, x86) Thin Client for Preview Only (Size 1MB)
|
||||||
|
- minimum Android version supported 16 (JellyBean)
|
||||||
#### Manual install for Debian Linux
|
- [<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
||||||
|
alt="Download from Google Play"
|
||||||
* `sudo apt-get update`
|
height="80">](https://play.google.com/store/apps/details?id=dev.zeronetx.app.lite)
|
||||||
* `sudo apt-get install msgpack-python python-gevent`
|
|
||||||
* `wget https://github.com/HelloZeroNet/ZeroNet/archive/master.tar.gz`
|
|
||||||
* `tar xvpfz master.tar.gz`
|
|
||||||
* `cd ZeroNet-master`
|
|
||||||
* Start with `python zeronet.py`
|
|
||||||
* Open http://127.0.0.1:43110/ in your browser
|
|
||||||
|
|
||||||
|
|
||||||
### [Vagrant](https://www.vagrantup.com/)
|
#### Docker
|
||||||
|
There is an official image, built from source at: https://hub.docker.com/r/canewsin/zeronet/
|
||||||
|
|
||||||
* `vagrant up`
|
### Online Proxies
|
||||||
* Access VM with `vagrant ssh`
|
Proxies are like seed boxes for sites(i.e ZNX runs on a cloud vps), you can try zeronet experience from proxies. Add your proxy below if you have one.
|
||||||
* `cd /vagrant`
|
|
||||||
* Run `python zeronet.py --ui_ip 0.0.0.0`
|
|
||||||
* Open http://127.0.0.1:43110/ in your browser
|
|
||||||
|
|
||||||
### [Docker](https://www.docker.com/)
|
#### Official ZNX Proxy :
|
||||||
* `docker run -d -v <local_data_folder>:/root/data -p 15441:15441 -p 127.0.0.1:43110:43110 nofish/zeronet`
|
|
||||||
* This Docker image includes the Tor proxy, which is disabled by default. Beware that some
|
|
||||||
hosting providers may not allow you running Tor in their servers. If you want to enable it,
|
|
||||||
set `ENABLE_TOR` environment variable to `true` (Default: `false`). E.g.:
|
|
||||||
|
|
||||||
`docker run -d -e "ENABLE_TOR=true" -v <local_data_folder>:/root/data -p 15441:15441 -p 127.0.0.1:43110:43110 nofish/zeronet`
|
https://proxy.zeronet.dev/
|
||||||
* Open http://127.0.0.1:43110/ in your browser
|
|
||||||
|
|
||||||
### [Virtualenv](https://virtualenv.readthedocs.org/en/latest/)
|
https://zeronet.dev/
|
||||||
|
|
||||||
* `virtualenv env`
|
#### From Community
|
||||||
* `source env/bin/activate`
|
|
||||||
* `pip install msgpack-python gevent`
|
https://0net-preview.com/
|
||||||
* `python zeronet.py`
|
|
||||||
* Open http://127.0.0.1:43110/ in your browser
|
https://portal.ngnoid.tv/
|
||||||
|
|
||||||
|
https://zeronet.ipfsscan.io/
|
||||||
|
|
||||||
|
|
||||||
|
### Install from source
|
||||||
|
|
||||||
|
- `wget https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-src.zip`
|
||||||
|
- `unzip ZeroNet-src.zip`
|
||||||
|
- `cd ZeroNet`
|
||||||
|
- `sudo apt-get update`
|
||||||
|
- `sudo apt-get install python3-pip`
|
||||||
|
- `sudo python3 -m pip install -r requirements.txt`
|
||||||
|
- Start with: `python3 zeronet.py`
|
||||||
|
- Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/
|
||||||
|
|
||||||
## Current limitations
|
## Current limitations
|
||||||
|
|
||||||
* No torrent-like file splitting for big file support
|
* File transactions are not compressed
|
||||||
* ~~No more anonymous than Bittorrent~~ (built-in full Tor support added)
|
|
||||||
* File transactions are not compressed ~~or encrypted yet~~ (TLS encryption added)
|
|
||||||
* No private sites
|
* No private sites
|
||||||
|
|
||||||
|
|
||||||
## How can I create a ZeroNet site?
|
## How can I create a ZeroNet site?
|
||||||
|
|
||||||
Shut down zeronet if you are running it already
|
* Click on **⋮** > **"Create new, empty site"** menu item on the site [ZeroHello](http://127.0.0.1:43110/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d).
|
||||||
|
* You will be **redirected** to a completely new site that is only modifiable by you!
|
||||||
```bash
|
* You can find and modify your site's content in **data/[yoursiteaddress]** directory
|
||||||
$ zeronet.py siteCreate
|
* After the modifications open your site, drag the topright "0" button to left, then press **sign** and **publish** buttons on the bottom
|
||||||
...
|
|
||||||
- Site private key: 23DKQpzxhbVBrAtvLEc2uvk7DZweh4qL3fn3jpM3LgHDczMK2TtYUq
|
|
||||||
- Site address: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
|
|
||||||
...
|
|
||||||
- Site created!
|
|
||||||
$ zeronet.py
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
Congratulations, you're finished! Now anyone can access your site using
|
|
||||||
`http://localhost:43110/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2`
|
|
||||||
|
|
||||||
Next steps: [ZeroNet Developer Documentation](https://zeronet.readthedocs.org/en/latest/site_development/getting_started/)
|
|
||||||
|
|
||||||
|
|
||||||
## How can I modify a ZeroNet site?
|
|
||||||
|
|
||||||
* Modify files located in data/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 directory.
|
|
||||||
After you're finished:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ zeronet.py siteSign 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
|
|
||||||
- Signing site: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2...
|
|
||||||
Private key (input hidden):
|
|
||||||
```
|
|
||||||
|
|
||||||
* Enter the private key you got when you created the site, then:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ zeronet.py sitePublish 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
|
|
||||||
...
|
|
||||||
Site:13DNDk..bhC2 Publishing to 3/10 peers...
|
|
||||||
Site:13DNDk..bhC2 Successfuly published to 3 peers
|
|
||||||
- Serving files....
|
|
||||||
```
|
|
||||||
|
|
||||||
* That's it! You've successfully signed and published your modifications.
|
|
||||||
|
|
||||||
|
Next steps: [ZeroNet Developer Documentation](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/site_development/getting_started/)
|
||||||
|
|
||||||
## Help keep this project alive
|
## Help keep this project alive
|
||||||
|
- Bitcoin: 1ZeroNetyV5mKY9JF1gsm82TuBXHpfdLX (Preferred)
|
||||||
- Bitcoin: 1QDhxQ6PraUZa21ET5fYUCPgdrwBomnFgX
|
- LiberaPay: https://liberapay.com/PramUkesh
|
||||||
- Paypal: https://zeronet.readthedocs.org/en/latest/help_zeronet/donate/
|
- Paypal: https://paypal.me/PramUkesh
|
||||||
- Gratipay: https://gratipay.com/zeronet/
|
- Others: [Donate](!https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/#help-to-keep-zeronet-development-alive)
|
||||||
|
|
||||||
### Sponsors
|
|
||||||
|
|
||||||
* Better OSX/Safari compatibility made possible by [BrowserStack.com](https://www.browserstack.com)
|
|
||||||
|
|
||||||
#### Thank you!
|
#### Thank you!
|
||||||
|
|
||||||
* More info, help, changelog, zeronet sites: https://www.reddit.com/r/zeronet/
|
* More info, help, changelog, zeronet sites: https://www.reddit.com/r/zeronetx/
|
||||||
* Come, chat with us: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) or on [gitter](https://gitter.im/HelloZeroNet/ZeroNet)
|
* Come, chat with us: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) or on [gitter](https://gitter.im/canewsin/ZeroNet)
|
||||||
* Email: hello@zeronet.io (PGP: CB9613AE)
|
* Email: canews.in@gmail.com
|
||||||
|
|
2
Vagrantfile
vendored
2
Vagrantfile
vendored
|
@ -40,6 +40,6 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||||
config.vm.provision "shell",
|
config.vm.provision "shell",
|
||||||
inline: "sudo apt-get install msgpack-python python-gevent python-pip python-dev -y"
|
inline: "sudo apt-get install msgpack-python python-gevent python-pip python-dev -y"
|
||||||
config.vm.provision "shell",
|
config.vm.provision "shell",
|
||||||
inline: "sudo pip install msgpack-python --upgrade"
|
inline: "sudo pip install msgpack --upgrade"
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
1
plugins
Submodule
1
plugins
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 689d9309f73371f4681191b125ec3f2e14075eeb
|
|
@ -1,118 +0,0 @@
|
||||||
import hashlib
|
|
||||||
import time
|
|
||||||
|
|
||||||
from Plugin import PluginManager
|
|
||||||
from Peer import Peer
|
|
||||||
from util import helper
|
|
||||||
from Crypt import CryptRsa
|
|
||||||
|
|
||||||
allow_reload = False # No source reload supported in this plugin
|
|
||||||
time_full_announced = {} # Tracker address: Last announced all site to tracker
|
|
||||||
connection_pool = {} # Tracker address: Peer object
|
|
||||||
|
|
||||||
|
|
||||||
# Process result got back from tracker
|
|
||||||
def processPeerRes(site, peers):
|
|
||||||
added = 0
|
|
||||||
# Ip4
|
|
||||||
found_ip4 = 0
|
|
||||||
for packed_address in peers["ip4"]:
|
|
||||||
found_ip4 += 1
|
|
||||||
peer_ip, peer_port = helper.unpackAddress(packed_address)
|
|
||||||
if site.addPeer(peer_ip, peer_port):
|
|
||||||
added += 1
|
|
||||||
# Onion
|
|
||||||
found_onion = 0
|
|
||||||
for packed_address in peers["onion"]:
|
|
||||||
found_onion += 1
|
|
||||||
peer_onion, peer_port = helper.unpackOnionAddress(packed_address)
|
|
||||||
if site.addPeer(peer_onion, peer_port):
|
|
||||||
added += 1
|
|
||||||
|
|
||||||
if added:
|
|
||||||
site.worker_manager.onPeers()
|
|
||||||
site.updateWebsocket(peers_added=added)
|
|
||||||
site.log.debug("Found %s ip4, %s onion peers, new: %s" % (found_ip4, found_onion, added))
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("Site")
|
|
||||||
class SitePlugin(object):
|
|
||||||
def announceTracker(self, tracker_protocol, tracker_address, fileserver_port=0, add_types=[], my_peer_id="", mode="start"):
|
|
||||||
if tracker_protocol != "zero":
|
|
||||||
return super(SitePlugin, self).announceTracker(
|
|
||||||
tracker_protocol, tracker_address, fileserver_port, add_types, my_peer_id, mode
|
|
||||||
)
|
|
||||||
|
|
||||||
s = time.time()
|
|
||||||
|
|
||||||
need_types = ["ip4"]
|
|
||||||
if self.connection_server and self.connection_server.tor_manager and self.connection_server.tor_manager.enabled:
|
|
||||||
need_types.append("onion")
|
|
||||||
|
|
||||||
if mode == "start" or mode == "more": # Single: Announce only this site
|
|
||||||
sites = [self]
|
|
||||||
full_announce = False
|
|
||||||
else: # Multi: Announce all currently serving site
|
|
||||||
full_announce = True
|
|
||||||
if time.time() - time_full_announced.get(tracker_address, 0) < 60 * 5: # No reannounce all sites within 5 minute
|
|
||||||
return True
|
|
||||||
time_full_announced[tracker_address] = time.time()
|
|
||||||
from Site import SiteManager
|
|
||||||
sites = [site for site in SiteManager.site_manager.sites.values() if site.settings["serving"]]
|
|
||||||
|
|
||||||
# Create request
|
|
||||||
request = {
|
|
||||||
"hashes": [], "onions": [], "port": fileserver_port, "need_types": need_types, "need_num": 20, "add": add_types
|
|
||||||
}
|
|
||||||
for site in sites:
|
|
||||||
if "onion" in add_types:
|
|
||||||
onion = self.connection_server.tor_manager.getOnion(site.address)
|
|
||||||
request["onions"].append(onion)
|
|
||||||
request["hashes"].append(hashlib.sha256(site.address).digest())
|
|
||||||
|
|
||||||
# Tracker can remove sites that we don't announce
|
|
||||||
if full_announce:
|
|
||||||
request["delete"] = True
|
|
||||||
|
|
||||||
# Sent request to tracker
|
|
||||||
tracker = connection_pool.get(tracker_address) # Re-use tracker connection if possible
|
|
||||||
if not tracker:
|
|
||||||
tracker_ip, tracker_port = tracker_address.split(":")
|
|
||||||
tracker = Peer(tracker_ip, tracker_port, connection_server=self.connection_server)
|
|
||||||
connection_pool[tracker_address] = tracker
|
|
||||||
res = tracker.request("announce", request)
|
|
||||||
|
|
||||||
if not res or "peers" not in res:
|
|
||||||
self.log.debug("Announce to %s failed: %s" % (tracker_address, res))
|
|
||||||
if full_announce:
|
|
||||||
time_full_announced[tracker_address] = 0
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Add peers from response to site
|
|
||||||
site_index = 0
|
|
||||||
for site_res in res["peers"]:
|
|
||||||
site = sites[site_index]
|
|
||||||
processPeerRes(site, site_res)
|
|
||||||
site_index += 1
|
|
||||||
|
|
||||||
# Check if we need to sign prove the onion addresses
|
|
||||||
if "onion_sign_this" in res:
|
|
||||||
self.log.debug("Signing %s for %s to add %s onions" % (res["onion_sign_this"], tracker_address, len(sites)))
|
|
||||||
request["onion_signs"] = {}
|
|
||||||
request["onion_sign_this"] = res["onion_sign_this"]
|
|
||||||
request["need_num"] = 0
|
|
||||||
for site in sites:
|
|
||||||
onion = self.connection_server.tor_manager.getOnion(site.address)
|
|
||||||
sign = CryptRsa.sign(res["onion_sign_this"], self.connection_server.tor_manager.getPrivatekey(onion))
|
|
||||||
request["onion_signs"][self.connection_server.tor_manager.getPublickey(onion)] = sign
|
|
||||||
res = tracker.request("announce", request)
|
|
||||||
if not res or "onion_sign_this" in res:
|
|
||||||
self.log.debug("Announce onion address to %s failed: %s" % (tracker_address, res))
|
|
||||||
if full_announce:
|
|
||||||
time_full_announced[tracker_address] = 0
|
|
||||||
return False
|
|
||||||
|
|
||||||
if full_announce:
|
|
||||||
tracker.remove() # Close connection, we don't need it in next 5 minute
|
|
||||||
|
|
||||||
return time.time() - s
|
|
|
@ -1 +0,0 @@
|
||||||
import AnnounceZeroPlugin
|
|
|
@ -1,53 +0,0 @@
|
||||||
from lib.pybitcointools import bitcoin as btctools
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
ecc_cache = {}
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt(data, pubkey, ephemcurve=None, ciphername='aes-256-cbc'):
|
|
||||||
from lib import pyelliptic
|
|
||||||
curve, pubkey_x, pubkey_y, i = pyelliptic.ECC._decode_pubkey(pubkey)
|
|
||||||
if ephemcurve is None:
|
|
||||||
ephemcurve = curve
|
|
||||||
ephem = pyelliptic.ECC(curve=ephemcurve)
|
|
||||||
key = hashlib.sha512(ephem.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest()
|
|
||||||
key_e, key_m = key[:32], key[32:]
|
|
||||||
pubkey = ephem.get_pubkey()
|
|
||||||
iv = pyelliptic.OpenSSL.rand(pyelliptic.OpenSSL.get_cipher(ciphername).get_blocksize())
|
|
||||||
ctx = pyelliptic.Cipher(key_e, iv, 1, ciphername)
|
|
||||||
ciphertext = iv + pubkey + ctx.ciphering(data)
|
|
||||||
mac = pyelliptic.hmac_sha256(key_m, ciphertext)
|
|
||||||
return key_e, ciphertext + mac
|
|
||||||
|
|
||||||
|
|
||||||
def split(encrypted):
|
|
||||||
iv = encrypted[0:16]
|
|
||||||
ciphertext = encrypted[16+70:-32]
|
|
||||||
|
|
||||||
return iv, ciphertext
|
|
||||||
|
|
||||||
|
|
||||||
def getEcc(privatekey=None):
|
|
||||||
from lib import pyelliptic
|
|
||||||
global eccs
|
|
||||||
if privatekey not in ecc_cache:
|
|
||||||
if privatekey:
|
|
||||||
publickey_bin = btctools.encode_pubkey(btctools.privtopub(privatekey), "bin")
|
|
||||||
publickey_openssl = toOpensslPublickey(publickey_bin)
|
|
||||||
privatekey_openssl = toOpensslPrivatekey(privatekey)
|
|
||||||
ecc_cache[privatekey] = pyelliptic.ECC(curve='secp256k1', privkey=privatekey_openssl, pubkey=publickey_openssl)
|
|
||||||
else:
|
|
||||||
ecc_cache[None] = pyelliptic.ECC()
|
|
||||||
return ecc_cache[privatekey]
|
|
||||||
|
|
||||||
|
|
||||||
def toOpensslPrivatekey(privatekey):
|
|
||||||
privatekey_bin = btctools.encode_privkey(privatekey, "bin")
|
|
||||||
return '\x02\xca\x00\x20' + privatekey_bin
|
|
||||||
|
|
||||||
|
|
||||||
def toOpensslPublickey(publickey):
|
|
||||||
publickey_bin = btctools.encode_pubkey(publickey, "bin")
|
|
||||||
publickey_bin = publickey_bin[1:]
|
|
||||||
publickey_openssl = '\x02\xca\x00 ' + publickey_bin[:32] + '\x00 ' + publickey_bin[32:]
|
|
||||||
return publickey_openssl
|
|
|
@ -1,149 +0,0 @@
|
||||||
import base64
|
|
||||||
import os
|
|
||||||
|
|
||||||
from Plugin import PluginManager
|
|
||||||
from Crypt import CryptBitcoin
|
|
||||||
from lib.pybitcointools import bitcoin as btctools
|
|
||||||
|
|
||||||
import CryptMessage
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("UiWebsocket")
|
|
||||||
class UiWebsocketPlugin(object):
|
|
||||||
def encrypt(self, text, publickey):
|
|
||||||
encrypted = CryptMessage.encrypt(text, CryptMessage.toOpensslPublickey(publickey))
|
|
||||||
return encrypted
|
|
||||||
|
|
||||||
def decrypt(self, encrypted, privatekey):
|
|
||||||
back = CryptMessage.getEcc(privatekey).decrypt(encrypted)
|
|
||||||
return back.decode("utf8")
|
|
||||||
|
|
||||||
# - Actions -
|
|
||||||
|
|
||||||
# Returns user's public key unique to site
|
|
||||||
# Return: Public key
|
|
||||||
def actionUserPublickey(self, to, index=0):
|
|
||||||
publickey = self.user.getEncryptPublickey(self.site.address, index)
|
|
||||||
self.response(to, publickey)
|
|
||||||
|
|
||||||
# Encrypt a text using the publickey or user's sites unique publickey
|
|
||||||
# Return: Encrypted text using base64 encoding
|
|
||||||
def actionEciesEncrypt(self, to, text, publickey=0, return_aes_key=False):
|
|
||||||
if type(publickey) is int: # Encrypt using user's publickey
|
|
||||||
publickey = self.user.getEncryptPublickey(self.site.address, publickey)
|
|
||||||
aes_key, encrypted = self.encrypt(text.encode("utf8"), publickey.decode("base64"))
|
|
||||||
if return_aes_key:
|
|
||||||
self.response(to, [base64.b64encode(encrypted), base64.b64encode(aes_key)])
|
|
||||||
else:
|
|
||||||
self.response(to, base64.b64encode(encrypted))
|
|
||||||
|
|
||||||
# Decrypt a text using privatekey or the user's site unique private key
|
|
||||||
# Return: Decrypted text or list of decrypted texts
|
|
||||||
def actionEciesDecrypt(self, to, param, privatekey=0):
|
|
||||||
if type(privatekey) is int: # Decrypt using user's privatekey
|
|
||||||
privatekey = self.user.getEncryptPrivatekey(self.site.address, privatekey)
|
|
||||||
|
|
||||||
if type(param) == list:
|
|
||||||
encrypted_texts = param
|
|
||||||
else:
|
|
||||||
encrypted_texts = [param]
|
|
||||||
|
|
||||||
texts = [] # Decoded texts
|
|
||||||
for encrypted_text in encrypted_texts:
|
|
||||||
try:
|
|
||||||
text = self.decrypt(encrypted_text.decode("base64"), privatekey)
|
|
||||||
texts.append(text)
|
|
||||||
except Exception, err:
|
|
||||||
texts.append(None)
|
|
||||||
|
|
||||||
if type(param) == list:
|
|
||||||
self.response(to, texts)
|
|
||||||
else:
|
|
||||||
self.response(to, texts[0])
|
|
||||||
|
|
||||||
# Encrypt a text using AES
|
|
||||||
# Return: Iv, AES key, Encrypted text
|
|
||||||
def actionAesEncrypt(self, to, text, key=None, iv=None):
|
|
||||||
from lib import pyelliptic
|
|
||||||
|
|
||||||
if key:
|
|
||||||
key = key.decode("base64")
|
|
||||||
else:
|
|
||||||
key = os.urandom(32)
|
|
||||||
|
|
||||||
if iv: # Generate new AES key if not definied
|
|
||||||
iv = iv.decode("base64")
|
|
||||||
else:
|
|
||||||
iv = pyelliptic.Cipher.gen_IV('aes-256-cbc')
|
|
||||||
|
|
||||||
if text:
|
|
||||||
encrypted = pyelliptic.Cipher(key, iv, 1, ciphername='aes-256-cbc').ciphering(text.encode("utf8"))
|
|
||||||
else:
|
|
||||||
encrypted = ""
|
|
||||||
|
|
||||||
self.response(to, [base64.b64encode(key), base64.b64encode(iv), base64.b64encode(encrypted)])
|
|
||||||
|
|
||||||
# Decrypt a text using AES
|
|
||||||
# Return: Decrypted text
|
|
||||||
def actionAesDecrypt(self, to, *args):
|
|
||||||
from lib import pyelliptic
|
|
||||||
|
|
||||||
if len(args) == 3: # Single decrypt
|
|
||||||
encrypted_texts = [(args[0], args[1])]
|
|
||||||
keys = [args[2]]
|
|
||||||
else: # Batch decrypt
|
|
||||||
encrypted_texts, keys = args
|
|
||||||
|
|
||||||
texts = [] # Decoded texts
|
|
||||||
for iv, encrypted_text in encrypted_texts:
|
|
||||||
encrypted_text = encrypted_text.decode("base64")
|
|
||||||
iv = iv.decode("base64")
|
|
||||||
text = None
|
|
||||||
for key in keys:
|
|
||||||
ctx = pyelliptic.Cipher(key.decode("base64"), iv, 0, ciphername='aes-256-cbc')
|
|
||||||
try:
|
|
||||||
decrypted = ctx.ciphering(encrypted_text)
|
|
||||||
if decrypted and decrypted.decode("utf8"): # Valid text decoded
|
|
||||||
text = decrypted
|
|
||||||
except Exception, err:
|
|
||||||
pass
|
|
||||||
texts.append(text)
|
|
||||||
|
|
||||||
if len(args) == 3:
|
|
||||||
self.response(to, texts[0])
|
|
||||||
else:
|
|
||||||
self.response(to, texts)
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("User")
|
|
||||||
class UserPlugin(object):
|
|
||||||
def getEncryptPrivatekey(self, address, param_index=0):
|
|
||||||
assert param_index >= 0 and param_index <= 1000
|
|
||||||
site_data = self.getSiteData(address)
|
|
||||||
|
|
||||||
if site_data.get("cert"): # Different privatekey for different cert provider
|
|
||||||
index = param_index + self.getAddressAuthIndex(site_data["cert"])
|
|
||||||
else:
|
|
||||||
index = param_index
|
|
||||||
|
|
||||||
if "encrypt_privatekey_%s" % index not in site_data:
|
|
||||||
address_index = self.getAddressAuthIndex(address)
|
|
||||||
crypt_index = address_index + 1000 + index
|
|
||||||
site_data["encrypt_privatekey_%s" % index] = CryptBitcoin.hdPrivatekey(self.master_seed, crypt_index)
|
|
||||||
self.log.debug("New encrypt privatekey generated for %s:%s" % (address, index))
|
|
||||||
return site_data["encrypt_privatekey_%s" % index]
|
|
||||||
|
|
||||||
def getEncryptPublickey(self, address, param_index=0):
|
|
||||||
assert param_index >= 0 and param_index <= 1000
|
|
||||||
site_data = self.getSiteData(address)
|
|
||||||
|
|
||||||
if site_data.get("cert"): # Different privatekey for different cert provider
|
|
||||||
index = param_index + self.getAddressAuthIndex(site_data["cert"])
|
|
||||||
else:
|
|
||||||
index = param_index
|
|
||||||
|
|
||||||
if "encrypt_publickey_%s" % index not in site_data:
|
|
||||||
privatekey = self.getEncryptPrivatekey(address, param_index)
|
|
||||||
publickey = btctools.encode_pubkey(btctools.privtopub(privatekey), "bin_compressed")
|
|
||||||
site_data["encrypt_publickey_%s" % index] = base64.b64encode(publickey)
|
|
||||||
return site_data["encrypt_publickey_%s" % index]
|
|
|
@ -1,106 +0,0 @@
|
||||||
import pytest
|
|
||||||
from CryptMessage import CryptMessage
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("resetSettings")
|
|
||||||
class TestCrypt:
|
|
||||||
def testPublickey(self, ui_websocket):
|
|
||||||
pub = ui_websocket.testAction("UserPublickey", 0)
|
|
||||||
assert len(pub) == 44 # Compressed, b64 encoded publickey
|
|
||||||
|
|
||||||
# Different pubkey for specificed index
|
|
||||||
assert ui_websocket.testAction("UserPublickey", 1) != ui_websocket.testAction("UserPublickey", 0)
|
|
||||||
|
|
||||||
# Same publickey for same index
|
|
||||||
assert ui_websocket.testAction("UserPublickey", 2) == ui_websocket.testAction("UserPublickey", 2)
|
|
||||||
|
|
||||||
# Different publickey for different cert
|
|
||||||
pub1 = ui_websocket.testAction("UserPublickey", 0)
|
|
||||||
site_data = ui_websocket.user.getSiteData(ui_websocket.site.address)
|
|
||||||
site_data["cert"] = "zeroid.bit"
|
|
||||||
pub2 = ui_websocket.testAction("UserPublickey", 0)
|
|
||||||
assert pub1 != pub2
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def testEcies(self, ui_websocket):
|
|
||||||
ui_websocket.actionUserPublickey(0, 0)
|
|
||||||
pub = ui_websocket.ws.result
|
|
||||||
|
|
||||||
ui_websocket.actionEciesEncrypt(0, "hello", pub)
|
|
||||||
encrypted = ui_websocket.ws.result
|
|
||||||
assert len(encrypted) == 180
|
|
||||||
|
|
||||||
# Don't allow decrypt using other privatekey index
|
|
||||||
ui_websocket.actionEciesDecrypt(0, encrypted, 123)
|
|
||||||
decrypted = ui_websocket.ws.result
|
|
||||||
assert decrypted != "hello"
|
|
||||||
|
|
||||||
# Decrypt using correct privatekey
|
|
||||||
ui_websocket.actionEciesDecrypt(0, encrypted)
|
|
||||||
decrypted = ui_websocket.ws.result
|
|
||||||
assert decrypted == "hello"
|
|
||||||
|
|
||||||
# Decrypt batch
|
|
||||||
ui_websocket.actionEciesDecrypt(0, [encrypted, "baad", encrypted])
|
|
||||||
decrypted = ui_websocket.ws.result
|
|
||||||
assert decrypted == ["hello", None, "hello"]
|
|
||||||
|
|
||||||
|
|
||||||
def testEciesUtf8(self, ui_websocket):
|
|
||||||
# Utf8 test
|
|
||||||
utf8_text = u'\xc1rv\xedzt\xfbr\xf5t\xfck\xf6rf\xfar\xf3g\xe9p'
|
|
||||||
ui_websocket.actionEciesEncrypt(0, utf8_text)
|
|
||||||
encrypted = ui_websocket.ws.result
|
|
||||||
|
|
||||||
ui_websocket.actionEciesDecrypt(0, encrypted)
|
|
||||||
assert ui_websocket.ws.result == utf8_text
|
|
||||||
|
|
||||||
|
|
||||||
def testEciesAes(self, ui_websocket):
|
|
||||||
ui_websocket.actionEciesEncrypt(0, "hello", return_aes_key=True)
|
|
||||||
ecies_encrypted, aes_key = ui_websocket.ws.result
|
|
||||||
|
|
||||||
# Decrypt using Ecies
|
|
||||||
ui_websocket.actionEciesDecrypt(0, ecies_encrypted)
|
|
||||||
assert ui_websocket.ws.result == "hello"
|
|
||||||
|
|
||||||
# Decrypt using AES
|
|
||||||
aes_iv, aes_encrypted = CryptMessage.split(ecies_encrypted.decode("base64"))
|
|
||||||
|
|
||||||
ui_websocket.actionAesDecrypt(0, aes_iv.encode("base64"), aes_encrypted.encode("base64"), aes_key)
|
|
||||||
assert ui_websocket.ws.result == "hello"
|
|
||||||
|
|
||||||
|
|
||||||
def testAes(self, ui_websocket):
|
|
||||||
ui_websocket.actionAesEncrypt(0, "hello")
|
|
||||||
key, iv, encrypted = ui_websocket.ws.result
|
|
||||||
|
|
||||||
assert len(key) == 44
|
|
||||||
assert len(iv) == 24
|
|
||||||
assert len(encrypted) == 24
|
|
||||||
|
|
||||||
# Single decrypt
|
|
||||||
ui_websocket.actionAesDecrypt(0, iv, encrypted, key)
|
|
||||||
assert ui_websocket.ws.result == "hello"
|
|
||||||
|
|
||||||
# Batch decrypt
|
|
||||||
ui_websocket.actionAesEncrypt(0, "hello")
|
|
||||||
key2, iv2, encrypted2 = ui_websocket.ws.result
|
|
||||||
|
|
||||||
assert [key, iv, encrypted] != [key2, iv2, encrypted2]
|
|
||||||
|
|
||||||
# 2 correct key
|
|
||||||
ui_websocket.actionAesDecrypt(0, [[iv, encrypted], [iv, encrypted], [iv, "baad"], [iv2, encrypted2]], [key])
|
|
||||||
assert ui_websocket.ws.result == ["hello", "hello", None, None]
|
|
||||||
|
|
||||||
# 3 key
|
|
||||||
ui_websocket.actionAesDecrypt(0, [[iv, encrypted], [iv, encrypted], [iv, "baad"], [iv2, encrypted2]], [key, key2])
|
|
||||||
assert ui_websocket.ws.result == ["hello", "hello", None, "hello"]
|
|
||||||
|
|
||||||
def testAesUtf8(self, ui_websocket):
|
|
||||||
utf8_text = u'\xc1rv\xedzt\xfbr\xf5t\xfck\xf6rf\xfar\xf3g\xe9'
|
|
||||||
ui_websocket.actionAesEncrypt(0, utf8_text)
|
|
||||||
key, iv, encrypted = ui_websocket.ws.result
|
|
||||||
|
|
||||||
ui_websocket.actionAesDecrypt(0, iv, encrypted, key)
|
|
||||||
assert ui_websocket.ws.result == utf8_text
|
|
|
@ -1 +0,0 @@
|
||||||
from src.Test.conftest import *
|
|
|
@ -1,5 +0,0 @@
|
||||||
[pytest]
|
|
||||||
python_files = Test*.py
|
|
||||||
addopts = -rsxX -v --durations=6
|
|
||||||
markers =
|
|
||||||
webtest: mark a test as a webtest.
|
|
|
@ -1 +0,0 @@
|
||||||
import CryptMessagePlugin
|
|
|
@ -1,342 +0,0 @@
|
||||||
import re
|
|
||||||
import time
|
|
||||||
|
|
||||||
from Plugin import PluginManager
|
|
||||||
from Translate import Translate
|
|
||||||
from util import RateLimit
|
|
||||||
from util import helper
|
|
||||||
from Debug import Debug
|
|
||||||
try:
|
|
||||||
import OptionalManager.UiWebsocketPlugin # To make optioanlFileInfo merger sites compatible
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if "merger_db" not in locals().keys(): # To keep merger_sites between module reloads
|
|
||||||
merger_db = {} # Sites that allowed to list other sites {address: [type1, type2...]}
|
|
||||||
merged_db = {} # Sites that allowed to be merged to other sites {address: type, ...}
|
|
||||||
merged_to_merger = {} # {address: [site1, site2, ...]} cache
|
|
||||||
site_manager = None # Site manager for merger sites
|
|
||||||
|
|
||||||
if "_" not in locals():
|
|
||||||
_ = Translate("plugins/MergerSite/languages/")
|
|
||||||
|
|
||||||
|
|
||||||
# Check if the site has permission to this merger site
|
|
||||||
def checkMergerPath(address, inner_path):
|
|
||||||
merged_match = re.match("^merged-(.*?)/([A-Za-z0-9]{26,35})/", inner_path)
|
|
||||||
if merged_match:
|
|
||||||
merger_type = merged_match.group(1)
|
|
||||||
# Check if merged site is allowed to include other sites
|
|
||||||
if merger_type in merger_db.get(address, []):
|
|
||||||
# Check if included site allows to include
|
|
||||||
merged_address = merged_match.group(2)
|
|
||||||
if merged_db.get(merged_address) == merger_type:
|
|
||||||
inner_path = re.sub("^merged-(.*?)/([A-Za-z0-9]{26,35})/", "", inner_path)
|
|
||||||
return merged_address, inner_path
|
|
||||||
else:
|
|
||||||
raise Exception(
|
|
||||||
"Merger site (%s) does not have permission for merged site: %s (%s)" %
|
|
||||||
(merger_type, merged_address, merged_db.get(merged_address))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise Exception("No merger (%s) permission to load: <br>%s (%s not in %s)" % (
|
|
||||||
address, inner_path, merger_type, merger_db.get(address, []))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise Exception("Invalid merger path: %s" % inner_path)
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("UiWebsocket")
|
|
||||||
class UiWebsocketPlugin(object):
|
|
||||||
# Download new site
|
|
||||||
def actionMergerSiteAdd(self, to, addresses):
|
|
||||||
if type(addresses) != list:
|
|
||||||
# Single site add
|
|
||||||
addresses = [addresses]
|
|
||||||
# Check if the site has merger permission
|
|
||||||
merger_types = merger_db.get(self.site.address)
|
|
||||||
if not merger_types:
|
|
||||||
return self.response(to, {"error": "Not a merger site"})
|
|
||||||
|
|
||||||
if RateLimit.isAllowed(self.site.address + "-MergerSiteAdd", 10) and len(addresses) == 1:
|
|
||||||
# Without confirmation if only one site address and not called in last 10 sec
|
|
||||||
self.cbMergerSiteAdd(to, addresses)
|
|
||||||
else:
|
|
||||||
self.cmd(
|
|
||||||
"confirm",
|
|
||||||
[_["Add <b>%s</b> new site?"] % len(addresses), "Add"],
|
|
||||||
lambda (res): self.cbMergerSiteAdd(to, addresses)
|
|
||||||
)
|
|
||||||
self.response(to, "ok")
|
|
||||||
|
|
||||||
# Callback of adding new site confirmation
|
|
||||||
def cbMergerSiteAdd(self, to, addresses):
|
|
||||||
added = 0
|
|
||||||
for address in addresses:
|
|
||||||
added += 1
|
|
||||||
site_manager.need(address)
|
|
||||||
if added:
|
|
||||||
self.cmd("notification", ["done", _["Added <b>%s</b> new site"] % added, 5000])
|
|
||||||
RateLimit.called(self.site.address + "-MergerSiteAdd")
|
|
||||||
site_manager.updateMergerSites()
|
|
||||||
|
|
||||||
# Delete a merged site
|
|
||||||
def actionMergerSiteDelete(self, to, address):
|
|
||||||
site = self.server.sites.get(address)
|
|
||||||
if not site:
|
|
||||||
return self.response(to, {"error": "No site found: %s" % address})
|
|
||||||
|
|
||||||
merger_types = merger_db.get(self.site.address)
|
|
||||||
if not merger_types:
|
|
||||||
return self.response(to, {"error": "Not a merger site"})
|
|
||||||
if merged_db.get(address) not in merger_types:
|
|
||||||
return self.response(to, {"error": "Merged type (%s) not in %s" % (merged_db.get(address), merger_types)})
|
|
||||||
|
|
||||||
self.cmd("notification", ["done", _["Site deleted: <b>%s</b>"] % address, 5000])
|
|
||||||
self.response(to, "ok")
|
|
||||||
|
|
||||||
# Lists merged sites
|
|
||||||
def actionMergerSiteList(self, to, query_site_info=False):
|
|
||||||
merger_types = merger_db.get(self.site.address)
|
|
||||||
ret = {}
|
|
||||||
if not merger_types:
|
|
||||||
return self.response(to, {"error": "Not a merger site"})
|
|
||||||
for address, merged_type in merged_db.iteritems():
|
|
||||||
if merged_type not in merger_types:
|
|
||||||
continue # Site not for us
|
|
||||||
if query_site_info:
|
|
||||||
site = self.server.sites.get(address)
|
|
||||||
ret[address] = self.formatSiteInfo(site, create_user=False)
|
|
||||||
else:
|
|
||||||
ret[address] = merged_type
|
|
||||||
self.response(to, ret)
|
|
||||||
|
|
||||||
def hasSitePermission(self, address):
|
|
||||||
if super(UiWebsocketPlugin, self).hasSitePermission(address):
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
if self.site.address in [merger_site.address for merger_site in merged_to_merger.get(address, [])]:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Add support merger sites for file commands
|
|
||||||
def mergerFuncWrapper(self, func_name, to, inner_path, *args, **kwargs):
|
|
||||||
func = getattr(super(UiWebsocketPlugin, self), func_name)
|
|
||||||
if inner_path.startswith("merged-"):
|
|
||||||
merged_address, merged_inner_path = checkMergerPath(self.site.address, inner_path)
|
|
||||||
|
|
||||||
# Set the same cert for merged site
|
|
||||||
merger_cert = self.user.getSiteData(self.site.address).get("cert")
|
|
||||||
if merger_cert:
|
|
||||||
self.user.setCert(merged_address, merger_cert)
|
|
||||||
|
|
||||||
site_before = self.site # Save to be able to change it back after we ran the command
|
|
||||||
self.site = self.server.sites.get(merged_address) # Change the site to the merged one
|
|
||||||
try:
|
|
||||||
back = func(to, merged_inner_path, *args, **kwargs)
|
|
||||||
finally:
|
|
||||||
self.site = site_before # Change back to original site
|
|
||||||
return back
|
|
||||||
else:
|
|
||||||
return func(to, inner_path, *args, **kwargs)
|
|
||||||
|
|
||||||
def actionFileGet(self, to, inner_path, *args, **kwargs):
|
|
||||||
return self.mergerFuncWrapper("actionFileGet", to, inner_path, *args, **kwargs)
|
|
||||||
|
|
||||||
def actionFileWrite(self, to, inner_path, *args, **kwargs):
|
|
||||||
return self.mergerFuncWrapper("actionFileWrite", to, inner_path, *args, **kwargs)
|
|
||||||
|
|
||||||
def actionFileDelete(self, to, inner_path, *args, **kwargs):
|
|
||||||
return self.mergerFuncWrapper("actionFileDelete", to, inner_path, *args, **kwargs)
|
|
||||||
|
|
||||||
def actionFileRules(self, to, inner_path, *args, **kwargs):
|
|
||||||
return self.mergerFuncWrapper("actionFileRules", to, inner_path, *args, **kwargs)
|
|
||||||
|
|
||||||
def actionOptionalFileInfo(self, to, inner_path, *args, **kwargs):
|
|
||||||
return self.mergerFuncWrapper("actionOptionalFileInfo", to, inner_path, *args, **kwargs)
|
|
||||||
|
|
||||||
def actionOptionalFileDelete(self, to, inner_path, *args, **kwargs):
|
|
||||||
return self.mergerFuncWrapper("actionOptionalFileDelete", to, inner_path, *args, **kwargs)
|
|
||||||
|
|
||||||
# Add support merger sites for file commands with privatekey parameter
|
|
||||||
def mergerFuncWrapperWithPrivatekey(self, func_name, to, privatekey, inner_path, *args, **kwargs):
|
|
||||||
func = getattr(super(UiWebsocketPlugin, self), func_name)
|
|
||||||
if inner_path.startswith("merged-"):
|
|
||||||
merged_address, merged_inner_path = checkMergerPath(self.site.address, inner_path)
|
|
||||||
merged_site = self.server.sites.get(merged_address)
|
|
||||||
|
|
||||||
# Set the same cert for merged site
|
|
||||||
merger_cert = self.user.getSiteData(self.site.address).get("cert")
|
|
||||||
if merger_cert:
|
|
||||||
self.user.setCert(merged_address, merger_cert)
|
|
||||||
|
|
||||||
site_before = self.site # Save to be able to change it back after we ran the command
|
|
||||||
self.site = merged_site # Change the site to the merged one
|
|
||||||
try:
|
|
||||||
back = func(to, privatekey, merged_inner_path, *args, **kwargs)
|
|
||||||
finally:
|
|
||||||
self.site = site_before # Change back to original site
|
|
||||||
return back
|
|
||||||
else:
|
|
||||||
return func(to, privatekey, inner_path, *args, **kwargs)
|
|
||||||
|
|
||||||
def actionSiteSign(self, to, privatekey=None, inner_path="content.json", *args, **kwargs):
|
|
||||||
return self.mergerFuncWrapperWithPrivatekey("actionSiteSign", to, privatekey, inner_path, *args, **kwargs)
|
|
||||||
|
|
||||||
def actionSitePublish(self, to, privatekey=None, inner_path="content.json", *args, **kwargs):
|
|
||||||
return self.mergerFuncWrapperWithPrivatekey("actionSitePublish", to, privatekey, inner_path, *args, **kwargs)
|
|
||||||
|
|
||||||
def actionPermissionAdd(self, to, permission):
|
|
||||||
super(UiWebsocketPlugin, self).actionPermissionAdd(to, permission)
|
|
||||||
if permission.startswith("Merger"):
|
|
||||||
self.site.storage.rebuildDb()
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("UiRequest")
|
|
||||||
class UiRequestPlugin(object):
|
|
||||||
# Allow to load merged site files using /merged-ZeroMe/address/file.jpg
|
|
||||||
def parsePath(self, path):
|
|
||||||
path_parts = super(UiRequestPlugin, self).parsePath(path)
|
|
||||||
if "merged-" not in path: # Optimization
|
|
||||||
return path_parts
|
|
||||||
path_parts["address"], path_parts["inner_path"] = checkMergerPath(path_parts["address"], path_parts["inner_path"])
|
|
||||||
return path_parts
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("SiteStorage")
|
|
||||||
class SiteStoragePlugin(object):
|
|
||||||
# Also rebuild from merged sites
|
|
||||||
def getDbFiles(self):
|
|
||||||
merger_types = merger_db.get(self.site.address)
|
|
||||||
|
|
||||||
# First return the site's own db files
|
|
||||||
for item in super(SiteStoragePlugin, self).getDbFiles():
|
|
||||||
yield item
|
|
||||||
|
|
||||||
# Not a merger site, that's all
|
|
||||||
if not merger_types:
|
|
||||||
raise StopIteration
|
|
||||||
|
|
||||||
merged_sites = [
|
|
||||||
site_manager.sites[address]
|
|
||||||
for address, merged_type in merged_db.iteritems()
|
|
||||||
if merged_type in merger_types
|
|
||||||
]
|
|
||||||
for merged_site in merged_sites:
|
|
||||||
merged_type = merged_db[merged_site.address]
|
|
||||||
for content_inner_path, content in merged_site.content_manager.contents.iteritems():
|
|
||||||
# content.json file itself
|
|
||||||
if merged_site.storage.isFile(content_inner_path): # Missing content.json file
|
|
||||||
merged_inner_path = "merged-%s/%s/%s" % (merged_type, merged_site.address, content_inner_path)
|
|
||||||
yield merged_inner_path, merged_site.storage.open(content_inner_path)
|
|
||||||
else:
|
|
||||||
merged_site.log.error("[MISSING] %s" % content_inner_path)
|
|
||||||
# Data files in content.json
|
|
||||||
content_inner_path_dir = helper.getDirname(content_inner_path) # Content.json dir relative to site
|
|
||||||
for file_relative_path in content["files"].keys():
|
|
||||||
if not file_relative_path.endswith(".json"):
|
|
||||||
continue # We only interesed in json files
|
|
||||||
file_inner_path = content_inner_path_dir + file_relative_path # File Relative to site dir
|
|
||||||
file_inner_path = file_inner_path.strip("/") # Strip leading /
|
|
||||||
if merged_site.storage.isFile(file_inner_path):
|
|
||||||
merged_inner_path = "merged-%s/%s/%s" % (merged_type, merged_site.address, file_inner_path)
|
|
||||||
yield merged_inner_path, merged_site.storage.open(file_inner_path)
|
|
||||||
else:
|
|
||||||
merged_site.log.error("[MISSING] %s" % file_inner_path)
|
|
||||||
|
|
||||||
# Also notice merger sites on a merged site file change
|
|
||||||
def onUpdated(self, inner_path, file=None):
|
|
||||||
super(SiteStoragePlugin, self).onUpdated(inner_path, file)
|
|
||||||
|
|
||||||
merged_type = merged_db.get(self.site.address)
|
|
||||||
|
|
||||||
for merger_site in merged_to_merger.get(self.site.address, []):
|
|
||||||
if merger_site.address == self.site.address: # Avoid infinite loop
|
|
||||||
continue
|
|
||||||
virtual_path = "merged-%s/%s/%s" % (merged_type, self.site.address, inner_path)
|
|
||||||
if inner_path.endswith(".json"):
|
|
||||||
if file is not None:
|
|
||||||
merger_site.storage.onUpdated(virtual_path, file=file)
|
|
||||||
else:
|
|
||||||
merger_site.storage.onUpdated(virtual_path, file=self.open(inner_path))
|
|
||||||
else:
|
|
||||||
merger_site.storage.onUpdated(virtual_path)
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("Site")
|
|
||||||
class SitePlugin(object):
|
|
||||||
def fileDone(self, inner_path):
|
|
||||||
super(SitePlugin, self).fileDone(inner_path)
|
|
||||||
|
|
||||||
for merger_site in merged_to_merger.get(self.address, []):
|
|
||||||
if merger_site.address == self.address:
|
|
||||||
continue
|
|
||||||
for ws in merger_site.websockets:
|
|
||||||
ws.event("siteChanged", self, {"event": ["file_done", inner_path]})
|
|
||||||
|
|
||||||
def fileFailed(self, inner_path):
|
|
||||||
super(SitePlugin, self).fileFailed(inner_path)
|
|
||||||
|
|
||||||
for merger_site in merged_to_merger.get(self.address, []):
|
|
||||||
if merger_site.address == self.address:
|
|
||||||
continue
|
|
||||||
for ws in merger_site.websockets:
|
|
||||||
ws.event("siteChanged", self, {"event": ["file_failed", inner_path]})
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("SiteManager")
|
|
||||||
class SiteManagerPlugin(object):
|
|
||||||
# Update merger site for site types
|
|
||||||
def updateMergerSites(self):
|
|
||||||
global merger_db, merged_db, merged_to_merger, site_manager
|
|
||||||
s = time.time()
|
|
||||||
merger_db = {}
|
|
||||||
merged_db = {}
|
|
||||||
merged_to_merger = {}
|
|
||||||
site_manager = self
|
|
||||||
if not self.sites:
|
|
||||||
return
|
|
||||||
for site in self.sites.itervalues():
|
|
||||||
# Update merged sites
|
|
||||||
try:
|
|
||||||
merged_type = site.content_manager.contents.get("content.json", {}).get("merged_type")
|
|
||||||
except Exception, err:
|
|
||||||
self.log.error("Error loading site %s: %s" % (site.address, Debug.formatException(err)))
|
|
||||||
continue
|
|
||||||
if merged_type:
|
|
||||||
merged_db[site.address] = merged_type
|
|
||||||
|
|
||||||
# Update merger sites
|
|
||||||
for permission in site.settings["permissions"]:
|
|
||||||
if not permission.startswith("Merger:"):
|
|
||||||
continue
|
|
||||||
if merged_type:
|
|
||||||
self.log.error(
|
|
||||||
"Removing permission %s from %s: Merger and merged at the same time." %
|
|
||||||
(permission, site.address)
|
|
||||||
)
|
|
||||||
site.settings["permissions"].remove(permission)
|
|
||||||
continue
|
|
||||||
merger_type = permission.replace("Merger:", "")
|
|
||||||
if site.address not in merger_db:
|
|
||||||
merger_db[site.address] = []
|
|
||||||
merger_db[site.address].append(merger_type)
|
|
||||||
site_manager.sites[site.address] = site
|
|
||||||
|
|
||||||
# Update merged to merger
|
|
||||||
if merged_type:
|
|
||||||
for merger_site in self.sites.itervalues():
|
|
||||||
if "Merger:" + merged_type in merger_site.settings["permissions"]:
|
|
||||||
if site.address not in merged_to_merger:
|
|
||||||
merged_to_merger[site.address] = []
|
|
||||||
merged_to_merger[site.address].append(merger_site)
|
|
||||||
self.log.debug("Updated merger sites in %.3fs" % (time.time() - s))
|
|
||||||
|
|
||||||
def load(self, *args, **kwags):
|
|
||||||
super(SiteManagerPlugin, self).load(*args, **kwags)
|
|
||||||
self.updateMergerSites()
|
|
||||||
|
|
||||||
def save(self, *args, **kwags):
|
|
||||||
super(SiteManagerPlugin, self).save(*args, **kwags)
|
|
||||||
self.updateMergerSites()
|
|
|
@ -1 +0,0 @@
|
||||||
import MergerSitePlugin
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"Add <b>%s</b> new site?": "Ajouter le site <b>%s</b> ?",
|
|
||||||
"Added <b>%s</b> new site": "Site <b>%s</b> ajouté",
|
|
||||||
"Site deleted: <b>%s</b>": "Site <b>%s</b> supprimé"
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"Add <b>%s</b> new site?": "Új oldal hozzáadása: <b>%s</b>?",
|
|
||||||
"Added <b>%s</b> new site": "Új oldal hozzáadva: <b>%s</b>",
|
|
||||||
"Site deleted: <b>%s</b>": "Oldal törölve: <b>%s</b>"
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"Add <b>%s</b> new site?": "Aggiungere <b>%s</b> nuovo sito ?",
|
|
||||||
"Added <b>%s</b> new site": "Sito <b>%s</b> aggiunto",
|
|
||||||
"Site deleted: <b>%s</b>": "Sito <b>%s</b> eliminato"
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"Add <b>%s</b> new site?": "<b>%s</b> sitesi eklensin mi?",
|
|
||||||
"Added <b>%s</b> new site": "<b>%s</b> sitesi eklendi",
|
|
||||||
"Site deleted: <b>%s</b>": "<b>%s</b> sitesi silindi"
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"Add <b>%s</b> new site?": "添加新網站: <b>%s</b>?",
|
|
||||||
"Added <b>%s</b> new site": "已添加到新網站:<b>%s</b>",
|
|
||||||
"Site deleted: <b>%s</b>": "網站已刪除:<b>%s</b>"
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"Add <b>%s</b> new site?": "添加新站点: <b>%s</b>?",
|
|
||||||
"Added <b>%s</b> new site": "已添加到新站点:<b>%s</b>",
|
|
||||||
"Site deleted: <b>%s</b>": "站点已删除:<b>%s</b>"
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
import time
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
from Plugin import PluginManager
|
|
||||||
from Translate import Translate
|
|
||||||
from Config import config
|
|
||||||
from util import helper
|
|
||||||
|
|
||||||
|
|
||||||
if os.path.isfile("%s/mutes.json" % config.data_dir):
|
|
||||||
try:
|
|
||||||
mutes = json.load(open("%s/mutes.json" % config.data_dir))["mutes"]
|
|
||||||
except Exception, err:
|
|
||||||
mutes = {}
|
|
||||||
else:
|
|
||||||
open("%s/mutes.json" % config.data_dir, "w").write('{"mutes": {}}')
|
|
||||||
mutes = {}
|
|
||||||
|
|
||||||
if "_" not in locals():
|
|
||||||
_ = Translate("plugins/Mute/languages/")
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("UiWebsocket")
|
|
||||||
class UiWebsocketPlugin(object):
|
|
||||||
# Search and remove or readd files of an user
|
|
||||||
def changeDb(self, auth_address, action):
|
|
||||||
self.log.debug("Mute action %s on user %s" % (action, auth_address))
|
|
||||||
res = self.site.content_manager.contents.db.execute(
|
|
||||||
"SELECT * FROM content LEFT JOIN site USING (site_id) WHERE inner_path LIKE :inner_path",
|
|
||||||
{"inner_path": "%%/%s/%%" % auth_address}
|
|
||||||
)
|
|
||||||
for row in res:
|
|
||||||
site = self.server.sites.get(row["address"])
|
|
||||||
if not site:
|
|
||||||
continue
|
|
||||||
dir_inner_path = helper.getDirname(row["inner_path"])
|
|
||||||
for file_name in site.storage.list(dir_inner_path):
|
|
||||||
if action == "remove":
|
|
||||||
site.storage.onUpdated(dir_inner_path + file_name, False)
|
|
||||||
else:
|
|
||||||
site.storage.onUpdated(dir_inner_path + file_name)
|
|
||||||
site.onFileDone(dir_inner_path + file_name)
|
|
||||||
|
|
||||||
def cbMuteAdd(self, to, auth_address, cert_user_id, reason):
|
|
||||||
mutes[auth_address] = {"cert_user_id": cert_user_id, "reason": reason, "source": self.site.address, "date_added": time.time()}
|
|
||||||
self.saveMutes()
|
|
||||||
self.changeDb(auth_address, "remove")
|
|
||||||
self.response(to, "ok")
|
|
||||||
|
|
||||||
def actionMuteAdd(self, to, auth_address, cert_user_id, reason):
|
|
||||||
if "ADMIN" in self.getPermissions(to):
|
|
||||||
self.cbMuteAdd(to, auth_address, cert_user_id, reason)
|
|
||||||
else:
|
|
||||||
self.cmd(
|
|
||||||
"confirm",
|
|
||||||
[_["Hide all content from <b>%s</b>?"] % cert_user_id, _["Mute"]],
|
|
||||||
lambda (res): self.cbMuteAdd(to, auth_address, cert_user_id, reason)
|
|
||||||
)
|
|
||||||
|
|
||||||
def cbMuteRemove(self, to, auth_address):
|
|
||||||
del mutes[auth_address]
|
|
||||||
self.saveMutes()
|
|
||||||
self.changeDb(auth_address, "load")
|
|
||||||
self.response(to, "ok")
|
|
||||||
|
|
||||||
def actionMuteRemove(self, to, auth_address):
|
|
||||||
if "ADMIN" in self.getPermissions(to):
|
|
||||||
self.cbMuteRemove(to, auth_address)
|
|
||||||
else:
|
|
||||||
self.cmd(
|
|
||||||
"confirm",
|
|
||||||
[_["Unmute <b>%s</b>?"] % mutes[auth_address]["cert_user_id"], _["Unmute"]],
|
|
||||||
lambda (res): self.cbMuteRemove(to, auth_address)
|
|
||||||
)
|
|
||||||
|
|
||||||
def actionMuteList(self, to):
|
|
||||||
if "ADMIN" in self.getPermissions(to):
|
|
||||||
self.response(to, mutes)
|
|
||||||
else:
|
|
||||||
return self.response(to, {"error": "Only ADMIN sites can list mutes"})
|
|
||||||
|
|
||||||
def saveMutes(self):
|
|
||||||
helper.atomicWrite("%s/mutes.json" % config.data_dir, json.dumps({"mutes": mutes}, indent=2, sort_keys=True))
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("SiteStorage")
|
|
||||||
class SiteStoragePlugin(object):
|
|
||||||
def updateDbFile(self, inner_path, file=None, cur=None):
|
|
||||||
if file is not False: # File deletion always allowed
|
|
||||||
# Find for bitcoin addresses in file path
|
|
||||||
matches = re.findall("/(1[A-Za-z0-9]{26,35})/", inner_path)
|
|
||||||
# Check if any of the adresses are in the mute list
|
|
||||||
for auth_address in matches:
|
|
||||||
if auth_address in mutes:
|
|
||||||
self.log.debug("Mute match: %s, ignoring %s" % (auth_address, inner_path))
|
|
||||||
return False
|
|
||||||
|
|
||||||
return super(SiteStoragePlugin, self).updateDbFile(inner_path, file=file, cur=cur)
|
|
|
@ -1 +0,0 @@
|
||||||
import MutePlugin
|
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"Hide all content from <b>%s</b>?": "<b>%s</b> tartalmaniak elrejtése?",
|
|
||||||
"Mute": "Elnémítás",
|
|
||||||
"Unmute <b>%s</b>?": "<b>%s</b> tartalmaniak megjelenítése?",
|
|
||||||
"Unmute": "Némítás visszavonása"
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"Hide all content from <b>%s</b>?": "<b>%s</b> Vuoi nascondere i contenuti di questo utente ?",
|
|
||||||
"Mute": "Attiva Silenzia",
|
|
||||||
"Unmute <b>%s</b>?": "<b>%s</b> Vuoi mostrare i contenuti di questo utente ?",
|
|
||||||
"Unmute": "Disattiva Silenzia"
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"Hide all content from <b>%s</b>?": "屏蔽 <b>%s</b> 的所有內容?",
|
|
||||||
"Mute": "屏蔽",
|
|
||||||
"Unmute <b>%s</b>?": "對 <b>%s</b> 解除屏蔽?",
|
|
||||||
"Unmute": "解除屏蔽"
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"Hide all content from <b>%s</b>?": "屏蔽 <b>%s</b> 的所有内容?",
|
|
||||||
"Mute": "屏蔽",
|
|
||||||
"Unmute <b>%s</b>?": "对 <b>%s</b> 解除屏蔽?",
|
|
||||||
"Unmute": "解除屏蔽"
|
|
||||||
}
|
|
|
@ -1,125 +0,0 @@
|
||||||
import time
|
|
||||||
import re
|
|
||||||
|
|
||||||
from Plugin import PluginManager
|
|
||||||
from Db import DbQuery
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("UiWebsocket")
|
|
||||||
class UiWebsocketPlugin(object):
|
|
||||||
def formatSiteInfo(self, site, create_user=True):
|
|
||||||
site_info = super(UiWebsocketPlugin, self).formatSiteInfo(site, create_user=True)
|
|
||||||
feed_following = self.user.sites[site.address].get("follow", None)
|
|
||||||
if feed_following == None:
|
|
||||||
site_info["feed_follow_num"] = None
|
|
||||||
else:
|
|
||||||
site_info["feed_follow_num"] = len(feed_following)
|
|
||||||
return site_info
|
|
||||||
|
|
||||||
def actionFeedFollow(self, to, feeds):
|
|
||||||
self.user.setFeedFollow(self.site.address, feeds)
|
|
||||||
self.user.save()
|
|
||||||
self.response(to, "ok")
|
|
||||||
|
|
||||||
def actionFeedListFollow(self, to):
|
|
||||||
feeds = self.user.sites[self.site.address].get("follow", {})
|
|
||||||
self.response(to, feeds)
|
|
||||||
|
|
||||||
def actionFeedQuery(self, to, limit=10, day_limit=3):
|
|
||||||
if "ADMIN" not in self.site.settings["permissions"]:
|
|
||||||
return self.response(to, "FeedQuery not allowed")
|
|
||||||
|
|
||||||
from Site import SiteManager
|
|
||||||
rows = []
|
|
||||||
for address, site_data in self.user.sites.iteritems():
|
|
||||||
feeds = site_data.get("follow")
|
|
||||||
if not feeds:
|
|
||||||
continue
|
|
||||||
for name, query_set in feeds.iteritems():
|
|
||||||
site = SiteManager.site_manager.get(address)
|
|
||||||
try:
|
|
||||||
query, params = query_set
|
|
||||||
query_parts = query.split("UNION")
|
|
||||||
for i, query_part in enumerate(query_parts):
|
|
||||||
db_query = DbQuery(query_part)
|
|
||||||
if day_limit:
|
|
||||||
where = " WHERE %s > strftime('%%s', 'now', '-%s day')" % (db_query.fields.get("date_added", "date_added"), day_limit)
|
|
||||||
if "WHERE" in query_part:
|
|
||||||
query_part = re.sub("WHERE (.*?)(?=$| GROUP BY)", where+" AND (\\1)", query_part)
|
|
||||||
else:
|
|
||||||
query_part += where
|
|
||||||
query_parts[i] = query_part
|
|
||||||
query = " UNION ".join(query_parts)
|
|
||||||
|
|
||||||
if ":params" in query:
|
|
||||||
query = query.replace(":params", ",".join(["?"] * len(params)))
|
|
||||||
res = site.storage.query(query + " ORDER BY date_added DESC LIMIT %s" % limit, params)
|
|
||||||
else:
|
|
||||||
res = site.storage.query(query + " ORDER BY date_added DESC LIMIT %s" % limit)
|
|
||||||
except Exception, err: # Log error
|
|
||||||
self.log.error("%s feed query %s error: %s" % (address, name, err))
|
|
||||||
continue
|
|
||||||
for row in res:
|
|
||||||
row = dict(row)
|
|
||||||
if "date_added" not in row or row["date_added"] > time.time() + 120:
|
|
||||||
continue # Feed item is in the future, skip it
|
|
||||||
row["site"] = address
|
|
||||||
row["feed_name"] = name
|
|
||||||
rows.append(row)
|
|
||||||
return self.response(to, rows)
|
|
||||||
|
|
||||||
def actionFeedSearch(self, to, search):
|
|
||||||
if "ADMIN" not in self.site.settings["permissions"]:
|
|
||||||
return self.response(to, "FeedSearch not allowed")
|
|
||||||
|
|
||||||
from Site import SiteManager
|
|
||||||
rows = []
|
|
||||||
num_sites = 0
|
|
||||||
s = time.time()
|
|
||||||
for address, site in SiteManager.site_manager.list().iteritems():
|
|
||||||
if not site.storage.has_db:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if site.storage.db: # Database loaded
|
|
||||||
feeds = site.storage.db.schema.get("feeds")
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
feeds = site.storage.loadJson("dbschema.json").get("feeds")
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not feeds:
|
|
||||||
continue
|
|
||||||
|
|
||||||
num_sites += 1
|
|
||||||
|
|
||||||
for name, query in feeds.iteritems():
|
|
||||||
try:
|
|
||||||
db_query = DbQuery(query)
|
|
||||||
db_query.wheres.append("%s LIKE ? OR %s LIKE ?" % (db_query.fields["body"], db_query.fields["title"]))
|
|
||||||
db_query.parts["ORDER BY"] = "date_added DESC"
|
|
||||||
db_query.parts["LIMIT"] = "30"
|
|
||||||
|
|
||||||
search_like = "%" + search.replace(" ", "%") + "%"
|
|
||||||
res = site.storage.query(str(db_query), [search_like, search_like])
|
|
||||||
except Exception, err:
|
|
||||||
self.log.error("%s feed query %s error: %s" % (address, name, err))
|
|
||||||
continue
|
|
||||||
for row in res:
|
|
||||||
row = dict(row)
|
|
||||||
if row["date_added"] > time.time() + 120:
|
|
||||||
continue # Feed item is in the future, skip it
|
|
||||||
row["site"] = address
|
|
||||||
row["feed_name"] = name
|
|
||||||
rows.append(row)
|
|
||||||
return self.response(to, {"rows": rows, "num": len(rows), "sites": num_sites, "taken": time.time() - s})
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("User")
|
|
||||||
class UserPlugin(object):
|
|
||||||
# Set queries that user follows
|
|
||||||
def setFeedFollow(self, address, feeds):
|
|
||||||
site_data = self.getSiteData(address)
|
|
||||||
site_data["follow"] = feeds
|
|
||||||
self.save()
|
|
||||||
return site_data
|
|
|
@ -1 +0,0 @@
|
||||||
import NewsfeedPlugin
|
|
|
@ -1,401 +0,0 @@
|
||||||
import time
|
|
||||||
import collections
|
|
||||||
import itertools
|
|
||||||
import re
|
|
||||||
|
|
||||||
import gevent
|
|
||||||
|
|
||||||
from util import helper
|
|
||||||
from Plugin import PluginManager
|
|
||||||
from Config import config
|
|
||||||
|
|
||||||
if "content_db" not in locals().keys(): # To keep between module reloads
|
|
||||||
content_db = None
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("ContentDb")
|
|
||||||
class ContentDbPlugin(object):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
global content_db
|
|
||||||
content_db = self
|
|
||||||
self.filled = {} # Site addresses that already filled from content.json
|
|
||||||
self.need_filling = False # file_optional table just created, fill data from content.json files
|
|
||||||
self.time_peer_numbers_updated = 0
|
|
||||||
self.my_optional_files = {} # Last 50 site_address/inner_path called by fileWrite (auto-pinning these files)
|
|
||||||
self.optional_files = collections.defaultdict(dict)
|
|
||||||
self.optional_files_loading = False
|
|
||||||
helper.timer(60 * 5, self.checkOptionalLimit)
|
|
||||||
super(ContentDbPlugin, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def getSchema(self):
|
|
||||||
schema = super(ContentDbPlugin, self).getSchema()
|
|
||||||
|
|
||||||
# Need file_optional table
|
|
||||||
schema["tables"]["file_optional"] = {
|
|
||||||
"cols": [
|
|
||||||
["file_id", "INTEGER PRIMARY KEY UNIQUE NOT NULL"],
|
|
||||||
["site_id", "INTEGER REFERENCES site (site_id) ON DELETE CASCADE"],
|
|
||||||
["inner_path", "TEXT"],
|
|
||||||
["hash_id", "INTEGER"],
|
|
||||||
["size", "INTEGER"],
|
|
||||||
["peer", "INTEGER DEFAULT 0"],
|
|
||||||
["uploaded", "INTEGER DEFAULT 0"],
|
|
||||||
["is_downloaded", "INTEGER DEFAULT 0"],
|
|
||||||
["is_pinned", "INTEGER DEFAULT 0"],
|
|
||||||
["time_added", "INTEGER DEFAULT 0"],
|
|
||||||
["time_downloaded", "INTEGER DEFAULT 0"],
|
|
||||||
["time_accessed", "INTEGER DEFAULT 0"]
|
|
||||||
],
|
|
||||||
"indexes": [
|
|
||||||
"CREATE UNIQUE INDEX file_optional_key ON file_optional (site_id, inner_path)",
|
|
||||||
"CREATE INDEX is_downloaded ON file_optional (is_downloaded)"
|
|
||||||
],
|
|
||||||
"schema_changed": 11
|
|
||||||
}
|
|
||||||
|
|
||||||
return schema
|
|
||||||
|
|
||||||
def initSite(self, site):
|
|
||||||
super(ContentDbPlugin, self).initSite(site)
|
|
||||||
if self.need_filling:
|
|
||||||
self.fillTableFileOptional(site)
|
|
||||||
if not self.optional_files_loading:
|
|
||||||
gevent.spawn_later(1, self.loadFilesOptional)
|
|
||||||
self.optional_files_loading = True
|
|
||||||
|
|
||||||
def checkTables(self):
|
|
||||||
changed_tables = super(ContentDbPlugin, self).checkTables()
|
|
||||||
if "file_optional" in changed_tables:
|
|
||||||
self.need_filling = True
|
|
||||||
return changed_tables
|
|
||||||
|
|
||||||
# Load optional files ending
|
|
||||||
def loadFilesOptional(self):
|
|
||||||
s = time.time()
|
|
||||||
num = 0
|
|
||||||
total = 0
|
|
||||||
total_downloaded = 0
|
|
||||||
res = content_db.execute("SELECT site_id, inner_path, size, is_downloaded FROM file_optional")
|
|
||||||
site_sizes = collections.defaultdict(lambda: collections.defaultdict(int))
|
|
||||||
for row in res:
|
|
||||||
self.optional_files[row["site_id"]][row["inner_path"][-8:]] = 1
|
|
||||||
num += 1
|
|
||||||
|
|
||||||
# Update site size stats
|
|
||||||
site_sizes[row["site_id"]]["size_optional"] += row["size"]
|
|
||||||
if row["is_downloaded"]:
|
|
||||||
site_sizes[row["site_id"]]["optional_downloaded"] += row["size"]
|
|
||||||
|
|
||||||
# Site site size stats to sites.json settings
|
|
||||||
site_ids_reverse = {val: key for key, val in self.site_ids.iteritems()}
|
|
||||||
for site_id, stats in site_sizes.iteritems():
|
|
||||||
site_address = site_ids_reverse.get(site_id)
|
|
||||||
if not site_address:
|
|
||||||
self.log.error("Not found site_id: %s" % site_id)
|
|
||||||
continue
|
|
||||||
site = self.sites[site_address]
|
|
||||||
site.settings["size_optional"] = stats["size_optional"]
|
|
||||||
site.settings["optional_downloaded"] = stats["optional_downloaded"]
|
|
||||||
total += stats["size_optional"]
|
|
||||||
total_downloaded += stats["optional_downloaded"]
|
|
||||||
|
|
||||||
self.log.debug(
|
|
||||||
"Loaded %s optional files: %.2fMB, downloaded: %.2fMB in %.3fs" %
|
|
||||||
(num, float(total) / 1024 / 1024, float(total_downloaded) / 1024 / 1024, time.time() - s)
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.need_filling and self.getOptionalLimitBytes() >= 0 and self.getOptionalLimitBytes() < total_downloaded:
|
|
||||||
limit_bytes = self.getOptionalLimitBytes()
|
|
||||||
limit_new = round((float(total_downloaded) / 1024 / 1024 / 1024) * 1.1, 2) # Current limit + 10%
|
|
||||||
self.log.debug(
|
|
||||||
"First startup after update and limit is smaller than downloaded files size (%.2fGB), increasing it from %.2fGB to %.2fGB" %
|
|
||||||
(float(total_downloaded) / 1024 / 1024 / 1024, float(limit_bytes) / 1024 / 1024 / 1024, limit_new)
|
|
||||||
)
|
|
||||||
config.saveValue("optional_limit", limit_new)
|
|
||||||
config.optional_limit = str(limit_new)
|
|
||||||
|
|
||||||
# Predicts if the file is optional
|
|
||||||
def isOptionalFile(self, site_id, inner_path):
|
|
||||||
return self.optional_files[site_id].get(inner_path[-8:])
|
|
||||||
|
|
||||||
# Fill file_optional table with optional files found in sites
|
|
||||||
def fillTableFileOptional(self, site):
|
|
||||||
s = time.time()
|
|
||||||
site_id = self.site_ids.get(site.address)
|
|
||||||
if not site_id:
|
|
||||||
return False
|
|
||||||
cur = self.getCursor()
|
|
||||||
cur.execute("BEGIN")
|
|
||||||
res = cur.execute("SELECT * FROM content WHERE size_files_optional > 0 AND site_id = %s" % site_id)
|
|
||||||
num = 0
|
|
||||||
for row in res.fetchall():
|
|
||||||
content = site.content_manager.contents[row["inner_path"]]
|
|
||||||
try:
|
|
||||||
num += self.setContentFilesOptional(site, row["inner_path"], content, cur=cur)
|
|
||||||
except Exception, err:
|
|
||||||
self.log.error("Error loading %s into file_optional: %s" % (row["inner_path"], err))
|
|
||||||
cur.execute("COMMIT")
|
|
||||||
cur.close()
|
|
||||||
|
|
||||||
# Set my files to pinned
|
|
||||||
from User import UserManager
|
|
||||||
user = UserManager.user_manager.get()
|
|
||||||
if not user:
|
|
||||||
user = UserManager.user_manager.create()
|
|
||||||
auth_address = user.getAuthAddress(site.address)
|
|
||||||
self.execute(
|
|
||||||
"UPDATE file_optional SET is_pinned = 1 WHERE site_id = :site_id AND inner_path LIKE :inner_path",
|
|
||||||
{"site_id": site_id, "inner_path": "%%/%s/%%" % auth_address}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.log.debug(
|
|
||||||
"Filled file_optional table for %s in %.3fs (loaded: %s, is_pinned: %s)" %
|
|
||||||
(site.address, time.time() - s, num, self.cur.cursor.rowcount)
|
|
||||||
)
|
|
||||||
self.filled[site.address] = True
|
|
||||||
|
|
||||||
def setContentFilesOptional(self, site, content_inner_path, content, cur=None):
|
|
||||||
if not cur:
|
|
||||||
cur = self
|
|
||||||
try:
|
|
||||||
cur.execute("BEGIN")
|
|
||||||
except Exception, err:
|
|
||||||
self.log.warning("Transaction begin error %s %s: %s" % (site, content_inner_path, Debug.formatException(err)))
|
|
||||||
|
|
||||||
num = 0
|
|
||||||
site_id = self.site_ids[site.address]
|
|
||||||
content_inner_dir = helper.getDirname(content_inner_path)
|
|
||||||
for relative_inner_path, file in content.get("files_optional", {}).iteritems():
|
|
||||||
file_inner_path = content_inner_dir + relative_inner_path
|
|
||||||
hash_id = int(file["sha512"][0:4], 16)
|
|
||||||
if hash_id in site.content_manager.hashfield:
|
|
||||||
is_downloaded = 1
|
|
||||||
else:
|
|
||||||
is_downloaded = 0
|
|
||||||
if site.address + "/" + file_inner_path in self.my_optional_files:
|
|
||||||
is_pinned = 1
|
|
||||||
else:
|
|
||||||
is_pinned = 0
|
|
||||||
cur.insertOrUpdate("file_optional", {
|
|
||||||
"hash_id": hash_id,
|
|
||||||
"size": int(file["size"]),
|
|
||||||
"is_pinned": is_pinned
|
|
||||||
}, {
|
|
||||||
"site_id": site_id,
|
|
||||||
"inner_path": file_inner_path
|
|
||||||
}, oninsert={
|
|
||||||
"time_added": int(time.time()),
|
|
||||||
"time_downloaded": int(time.time()) if is_downloaded else 0,
|
|
||||||
"is_downloaded": is_downloaded,
|
|
||||||
"peer": is_downloaded
|
|
||||||
})
|
|
||||||
self.optional_files[site_id][file_inner_path[-8:]] = 1
|
|
||||||
num += 1
|
|
||||||
|
|
||||||
if cur == self:
|
|
||||||
try:
|
|
||||||
cur.execute("END")
|
|
||||||
except Exception, err:
|
|
||||||
self.log.warning("Transaction end error %s %s: %s" % (site, content_inner_path, Debug.formatException(err)))
|
|
||||||
return num
|
|
||||||
|
|
||||||
def setContent(self, site, inner_path, content, size=0):
|
|
||||||
super(ContentDbPlugin, self).setContent(site, inner_path, content, size=size)
|
|
||||||
old_content = site.content_manager.contents.get(inner_path, {})
|
|
||||||
if (not self.need_filling or self.filled.get(site.address)) and "files_optional" in content or "files_optional" in old_content:
|
|
||||||
self.setContentFilesOptional(site, inner_path, content)
|
|
||||||
# Check deleted files
|
|
||||||
if old_content:
|
|
||||||
old_files = old_content.get("files_optional", {}).keys()
|
|
||||||
new_files = content.get("files_optional", {}).keys()
|
|
||||||
content_inner_dir = helper.getDirname(inner_path)
|
|
||||||
deleted = [content_inner_dir + key for key in old_files if key not in new_files]
|
|
||||||
if deleted:
|
|
||||||
site_id = self.site_ids[site.address]
|
|
||||||
self.execute("DELETE FROM file_optional WHERE ?", {"site_id": site_id, "inner_path": deleted})
|
|
||||||
|
|
||||||
def deleteContent(self, site, inner_path):
|
|
||||||
content = site.content_manager.contents.get(inner_path)
|
|
||||||
if content and "files_optional" in content:
|
|
||||||
site_id = self.site_ids[site.address]
|
|
||||||
content_inner_dir = helper.getDirname(inner_path)
|
|
||||||
optional_inner_paths = [
|
|
||||||
content_inner_dir + relative_inner_path
|
|
||||||
for relative_inner_path in content.get("files_optional", {}).keys()
|
|
||||||
]
|
|
||||||
self.execute("DELETE FROM file_optional WHERE ?", {"site_id": site_id, "inner_path": optional_inner_paths})
|
|
||||||
super(ContentDbPlugin, self).deleteContent(site, inner_path)
|
|
||||||
|
|
||||||
def updatePeerNumbers(self):
|
|
||||||
s = time.time()
|
|
||||||
num_file = 0
|
|
||||||
num_updated = 0
|
|
||||||
num_site = 0
|
|
||||||
for site in self.sites.values():
|
|
||||||
if not site.content_manager.has_optional_files:
|
|
||||||
continue
|
|
||||||
has_updated_hashfield = next((
|
|
||||||
peer
|
|
||||||
for peer in site.peers.itervalues()
|
|
||||||
if peer.has_hashfield and peer.hashfield.time_changed > self.time_peer_numbers_updated
|
|
||||||
), None)
|
|
||||||
|
|
||||||
if not has_updated_hashfield and site.content_manager.hashfield.time_changed < self.time_peer_numbers_updated:
|
|
||||||
continue
|
|
||||||
|
|
||||||
hashfield_peers = itertools.chain.from_iterable(
|
|
||||||
peer.hashfield.storage
|
|
||||||
for peer in site.peers.itervalues()
|
|
||||||
if peer.has_hashfield
|
|
||||||
)
|
|
||||||
peer_nums = collections.Counter(
|
|
||||||
itertools.chain(
|
|
||||||
hashfield_peers,
|
|
||||||
site.content_manager.hashfield
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
site_id = self.site_ids[site.address]
|
|
||||||
if not site_id:
|
|
||||||
continue
|
|
||||||
|
|
||||||
res = self.execute("SELECT file_id, hash_id, peer FROM file_optional WHERE ?", {"site_id": site_id})
|
|
||||||
updates = {}
|
|
||||||
for row in res:
|
|
||||||
peer_num = peer_nums.get(row["hash_id"], 0)
|
|
||||||
if peer_num != row["peer"]:
|
|
||||||
updates[row["file_id"]] = peer_num
|
|
||||||
|
|
||||||
self.execute("BEGIN")
|
|
||||||
for file_id, peer_num in updates.iteritems():
|
|
||||||
self.execute("UPDATE file_optional SET peer = ? WHERE file_id = ?", (peer_num, file_id))
|
|
||||||
self.execute("END")
|
|
||||||
|
|
||||||
num_updated += len(updates)
|
|
||||||
num_file += len(peer_nums)
|
|
||||||
num_site += 1
|
|
||||||
|
|
||||||
self.time_peer_numbers_updated = time.time()
|
|
||||||
self.log.debug("%s/%s peer number for %s site updated in %.3fs" % (num_updated, num_file, num_site, time.time() - s))
|
|
||||||
|
|
||||||
def queryDeletableFiles(self):
|
|
||||||
# First return the files with atleast 10 seeder and not accessed in last weed
|
|
||||||
query = """
|
|
||||||
SELECT * FROM file_optional
|
|
||||||
WHERE peer > 10 AND is_downloaded = 1 AND is_pinned = 0
|
|
||||||
ORDER BY time_accessed < %s DESC, uploaded / size
|
|
||||||
""" % int(time.time() - 60 * 60 * 7)
|
|
||||||
limit_start = 0
|
|
||||||
while 1:
|
|
||||||
num = 0
|
|
||||||
res = self.execute("%s LIMIT %s, 50" % (query, limit_start))
|
|
||||||
for row in res:
|
|
||||||
yield row
|
|
||||||
num += 1
|
|
||||||
if num < 50:
|
|
||||||
break
|
|
||||||
limit_start += 50
|
|
||||||
|
|
||||||
self.log.debug("queryDeletableFiles returning less-seeded files")
|
|
||||||
|
|
||||||
# Then return files less seeder but still not accessed in last week
|
|
||||||
query = """
|
|
||||||
SELECT * FROM file_optional
|
|
||||||
WHERE is_downloaded = 1 AND peer <= 10 AND is_pinned = 0
|
|
||||||
ORDER BY peer DESC, time_accessed < %s DESC, uploaded / size
|
|
||||||
""" % int(time.time() - 60 * 60 * 7)
|
|
||||||
limit_start = 0
|
|
||||||
while 1:
|
|
||||||
num = 0
|
|
||||||
res = self.execute("%s LIMIT %s, 50" % (query, limit_start))
|
|
||||||
for row in res:
|
|
||||||
yield row
|
|
||||||
num += 1
|
|
||||||
if num < 50:
|
|
||||||
break
|
|
||||||
limit_start += 50
|
|
||||||
|
|
||||||
self.log.debug("queryDeletableFiles returning everyting")
|
|
||||||
|
|
||||||
# At the end return all files
|
|
||||||
query = """
|
|
||||||
SELECT * FROM file_optional
|
|
||||||
WHERE is_downloaded = 1 AND peer <= 10 AND is_pinned = 0
|
|
||||||
ORDER BY peer DESC, time_accessed, uploaded / size
|
|
||||||
"""
|
|
||||||
limit_start = 0
|
|
||||||
while 1:
|
|
||||||
num = 0
|
|
||||||
res = self.execute("%s LIMIT %s, 50" % (query, limit_start))
|
|
||||||
for row in res:
|
|
||||||
yield row
|
|
||||||
num += 1
|
|
||||||
if num < 50:
|
|
||||||
break
|
|
||||||
limit_start += 50
|
|
||||||
|
|
||||||
def getOptionalLimitBytes(self):
|
|
||||||
if config.optional_limit.endswith("%"):
|
|
||||||
limit_percent = float(re.sub("[^0-9.]", "", config.optional_limit))
|
|
||||||
limit_bytes = helper.getFreeSpace() * (limit_percent / 100)
|
|
||||||
else:
|
|
||||||
limit_bytes = float(re.sub("[^0-9.]", "", config.optional_limit)) * 1024 * 1024 * 1024
|
|
||||||
return limit_bytes
|
|
||||||
|
|
||||||
def getOptionalNeedDelete(self, size):
|
|
||||||
if config.optional_limit.endswith("%"):
|
|
||||||
limit_percent = float(re.sub("[^0-9.]", "", config.optional_limit))
|
|
||||||
need_delete = size - ((helper.getFreeSpace() + size) * (limit_percent / 100))
|
|
||||||
else:
|
|
||||||
need_delete = size - self.getOptionalLimitBytes()
|
|
||||||
return need_delete
|
|
||||||
|
|
||||||
def checkOptionalLimit(self, limit=None):
|
|
||||||
if not limit:
|
|
||||||
limit = self.getOptionalLimitBytes()
|
|
||||||
|
|
||||||
if limit < 0:
|
|
||||||
self.log.debug("Invalid limit for optional files: %s" % limit)
|
|
||||||
return False
|
|
||||||
|
|
||||||
size = self.execute("SELECT SUM(size) FROM file_optional WHERE is_downloaded = 1 AND is_pinned = 0").fetchone()[0]
|
|
||||||
if not size:
|
|
||||||
size = 0
|
|
||||||
|
|
||||||
need_delete = self.getOptionalNeedDelete(size)
|
|
||||||
|
|
||||||
self.log.debug(
|
|
||||||
"Optional size: %.1fMB/%.1fMB, Need delete: %.1fMB" %
|
|
||||||
(float(size) / 1024 / 1024, float(limit) / 1024 / 1024, float(need_delete) / 1024 / 1024)
|
|
||||||
)
|
|
||||||
if need_delete <= 0:
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.updatePeerNumbers()
|
|
||||||
|
|
||||||
site_ids_reverse = {val: key for key, val in self.site_ids.iteritems()}
|
|
||||||
deleted_file_ids = []
|
|
||||||
for row in self.queryDeletableFiles():
|
|
||||||
site_address = site_ids_reverse.get(row["site_id"])
|
|
||||||
site = self.sites.get(site_address)
|
|
||||||
if not site:
|
|
||||||
self.log.error("No site found for id: %s" % row["site_id"])
|
|
||||||
continue
|
|
||||||
site.log.debug("Deleting %s %.3f MB left" % (row["inner_path"], float(need_delete) / 1024 / 1024))
|
|
||||||
deleted_file_ids.append(row["file_id"])
|
|
||||||
try:
|
|
||||||
site.content_manager.optionalRemove(row["inner_path"], row["hash_id"], row["size"])
|
|
||||||
site.storage.delete(row["inner_path"])
|
|
||||||
need_delete -= row["size"]
|
|
||||||
except Exception, err:
|
|
||||||
site.log.error("Error deleting %s: %s" % (row["inner_path"], err))
|
|
||||||
|
|
||||||
if need_delete <= 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
cur = self.getCursor()
|
|
||||||
cur.execute("BEGIN")
|
|
||||||
for file_id in deleted_file_ids:
|
|
||||||
cur.execute("UPDATE file_optional SET is_downloaded = 0, is_pinned = 0, peer = peer - 1 WHERE ?", {"file_id": file_id})
|
|
||||||
cur.execute("COMMIT")
|
|
||||||
cur.close()
|
|
|
@ -1,117 +0,0 @@
|
||||||
import time
|
|
||||||
import collections
|
|
||||||
|
|
||||||
from util import helper
|
|
||||||
from Plugin import PluginManager
|
|
||||||
import ContentDbPlugin
|
|
||||||
|
|
||||||
|
|
||||||
def processAccessLog():
|
|
||||||
if access_log:
|
|
||||||
content_db = ContentDbPlugin.content_db
|
|
||||||
now = int(time.time())
|
|
||||||
num = 0
|
|
||||||
for site_id in access_log:
|
|
||||||
content_db.execute(
|
|
||||||
"UPDATE file_optional SET time_accessed = %s WHERE ?" % now,
|
|
||||||
{"site_id": site_id, "inner_path": access_log[site_id].keys()}
|
|
||||||
)
|
|
||||||
num += len(access_log[site_id])
|
|
||||||
access_log.clear()
|
|
||||||
|
|
||||||
|
|
||||||
def processRequestLog():
|
|
||||||
if request_log:
|
|
||||||
content_db = ContentDbPlugin.content_db
|
|
||||||
cur = content_db.getCursor()
|
|
||||||
num = 0
|
|
||||||
cur.execute("BEGIN")
|
|
||||||
for site_id in request_log:
|
|
||||||
for inner_path, uploaded in request_log[site_id].iteritems():
|
|
||||||
content_db.execute(
|
|
||||||
"UPDATE file_optional SET uploaded = uploaded + %s WHERE ?" % uploaded,
|
|
||||||
{"site_id": site_id, "inner_path": inner_path}
|
|
||||||
)
|
|
||||||
num += 1
|
|
||||||
cur.execute("END")
|
|
||||||
request_log.clear()
|
|
||||||
|
|
||||||
|
|
||||||
if "access_log" not in locals().keys(): # To keep between module reloads
|
|
||||||
access_log = collections.defaultdict(dict) # {site_id: {inner_path1: 1, inner_path2: 1...}}
|
|
||||||
request_log = collections.defaultdict(lambda: collections.defaultdict(int)) # {site_id: {inner_path1: 1, inner_path2: 1...}}
|
|
||||||
helper.timer(61, processAccessLog)
|
|
||||||
helper.timer(60, processRequestLog)
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("WorkerManager")
|
|
||||||
class WorkerManagerPlugin(object):
|
|
||||||
def doneTask(self, task):
|
|
||||||
if task["optional_hash_id"]:
|
|
||||||
content_db = self.site.content_manager.contents.db
|
|
||||||
content_db.executeDelayed(
|
|
||||||
"UPDATE file_optional SET time_downloaded = :now, is_downloaded = 1, peer = peer + 1 WHERE site_id = :site_id AND inner_path = :inner_path",
|
|
||||||
{"now": int(time.time()), "site_id": content_db.site_ids[self.site.address], "inner_path": task["inner_path"]}
|
|
||||||
)
|
|
||||||
|
|
||||||
super(WorkerManagerPlugin, self).doneTask(task)
|
|
||||||
|
|
||||||
if task["optional_hash_id"] and not self.tasks:
|
|
||||||
content_db.processDelayed()
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("UiRequest")
|
|
||||||
class UiRequestPlugin(object):
|
|
||||||
def parsePath(self, path):
|
|
||||||
global access_log
|
|
||||||
path_parts = super(UiRequestPlugin, self).parsePath(path)
|
|
||||||
if path_parts:
|
|
||||||
site_id = ContentDbPlugin.content_db.site_ids.get(path_parts["request_address"])
|
|
||||||
if site_id:
|
|
||||||
if ContentDbPlugin.content_db.isOptionalFile(site_id, path_parts["inner_path"]):
|
|
||||||
access_log[site_id][path_parts["inner_path"]] = 1
|
|
||||||
return path_parts
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("FileRequest")
|
|
||||||
class FileRequestPlugin(object):
|
|
||||||
def actionGetFile(self, params):
|
|
||||||
stats = super(FileRequestPlugin, self).actionGetFile(params)
|
|
||||||
self.recordFileRequest(params["site"], params["inner_path"], stats)
|
|
||||||
return stats
|
|
||||||
|
|
||||||
def actionStreamFile(self, params):
|
|
||||||
stats = super(FileRequestPlugin, self).actionStreamFile(params)
|
|
||||||
self.recordFileRequest(params["site"], params["inner_path"], stats)
|
|
||||||
return stats
|
|
||||||
|
|
||||||
def recordFileRequest(self, site_address, inner_path, stats):
|
|
||||||
if not stats:
|
|
||||||
# Only track the last request of files
|
|
||||||
return False
|
|
||||||
site_id = ContentDbPlugin.content_db.site_ids[site_address]
|
|
||||||
if site_id and ContentDbPlugin.content_db.isOptionalFile(site_id, inner_path):
|
|
||||||
request_log[site_id][inner_path] += stats["bytes_sent"]
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("Site")
|
|
||||||
class SitePlugin(object):
|
|
||||||
def isDownloadable(self, inner_path):
|
|
||||||
is_downloadable = super(SitePlugin, self).isDownloadable(inner_path)
|
|
||||||
if is_downloadable:
|
|
||||||
return is_downloadable
|
|
||||||
|
|
||||||
for path in self.settings.get("optional_help", {}).iterkeys():
|
|
||||||
if inner_path.startswith(path):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("ConfigPlugin")
|
|
||||||
class ConfigPlugin(object):
|
|
||||||
def createArguments(self):
|
|
||||||
group = self.parser.add_argument_group("OptionalManager plugin")
|
|
||||||
group.add_argument('--optional_limit', help='Limit total size of optional files', default="10%", metavar="GB or free space %")
|
|
||||||
|
|
||||||
return super(ConfigPlugin, self).createArguments()
|
|
|
@ -1,42 +0,0 @@
|
||||||
import hashlib
|
|
||||||
import os
|
|
||||||
import copy
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from OptionalManager import OptionalManagerPlugin
|
|
||||||
from util import helper
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("resetSettings")
|
|
||||||
class TestOptionalManager:
|
|
||||||
def testDbFill(self, site):
|
|
||||||
contents = site.content_manager.contents
|
|
||||||
assert len(site.content_manager.hashfield) > 0
|
|
||||||
assert contents.db.execute("SELECT COUNT(*) FROM file_optional WHERE is_downloaded = 1").fetchone()[0] == len(site.content_manager.hashfield)
|
|
||||||
|
|
||||||
def testSetContent(self, site):
|
|
||||||
contents = site.content_manager.contents
|
|
||||||
|
|
||||||
# Add new file
|
|
||||||
new_content = copy.deepcopy(contents["content.json"])
|
|
||||||
new_content["files_optional"]["testfile"] = {
|
|
||||||
"size": 1234,
|
|
||||||
"sha512": "aaaabbbbcccc"
|
|
||||||
}
|
|
||||||
num_optional_files_before = contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0]
|
|
||||||
contents["content.json"] = new_content
|
|
||||||
assert contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0] > num_optional_files_before
|
|
||||||
|
|
||||||
# Remove file
|
|
||||||
new_content = copy.deepcopy(contents["content.json"])
|
|
||||||
del new_content["files_optional"]["testfile"]
|
|
||||||
num_optional_files_before = contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0]
|
|
||||||
contents["content.json"] = new_content
|
|
||||||
assert contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0] < num_optional_files_before
|
|
||||||
|
|
||||||
def testDeleteContent(self, site):
|
|
||||||
contents = site.content_manager.contents
|
|
||||||
num_optional_files_before = contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0]
|
|
||||||
del contents["content.json"]
|
|
||||||
assert contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0] < num_optional_files_before
|
|
|
@ -1 +0,0 @@
|
||||||
from src.Test.conftest import *
|
|
|
@ -1,5 +0,0 @@
|
||||||
[pytest]
|
|
||||||
python_files = Test*.py
|
|
||||||
addopts = -rsxX -v --durations=6
|
|
||||||
markers =
|
|
||||||
webtest: mark a test as a webtest.
|
|
|
@ -1,260 +0,0 @@
|
||||||
import re
|
|
||||||
import time
|
|
||||||
import cgi
|
|
||||||
|
|
||||||
import gevent
|
|
||||||
|
|
||||||
from Plugin import PluginManager
|
|
||||||
from Config import config
|
|
||||||
from util import helper
|
|
||||||
from Translate import Translate
|
|
||||||
|
|
||||||
if "_" not in locals():
|
|
||||||
_ = Translate("plugins/OptionalManager/languages/")
|
|
||||||
|
|
||||||
@PluginManager.registerTo("UiWebsocket")
|
|
||||||
class UiWebsocketPlugin(object):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.time_peer_numbers_updated = 0
|
|
||||||
super(UiWebsocketPlugin, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def actionFileWrite(self, to, inner_path, *args, **kwargs):
|
|
||||||
super(UiWebsocketPlugin, self).actionFileWrite(to, inner_path, *args, **kwargs)
|
|
||||||
|
|
||||||
# Add file to content.db and set it as pinned
|
|
||||||
content_db = self.site.content_manager.contents.db
|
|
||||||
content_db.my_optional_files[self.site.address + "/" + inner_path] = time.time()
|
|
||||||
if len(content_db.my_optional_files) > 50: # Keep only last 50
|
|
||||||
oldest_key = min(
|
|
||||||
content_db.my_optional_files.iterkeys(),
|
|
||||||
key=(lambda key: content_db.my_optional_files[key])
|
|
||||||
)
|
|
||||||
del content_db.my_optional_files[oldest_key]
|
|
||||||
|
|
||||||
def updatePeerNumbers(self):
|
|
||||||
content_db = self.site.content_manager.contents.db
|
|
||||||
content_db.updatePeerNumbers()
|
|
||||||
self.site.updateWebsocket(peernumber_updated=True)
|
|
||||||
|
|
||||||
# Optional file functions
|
|
||||||
|
|
||||||
def actionOptionalFileList(self, to, address=None, orderby="time_downloaded DESC", limit=10):
|
|
||||||
if not address:
|
|
||||||
address = self.site.address
|
|
||||||
|
|
||||||
# Update peer numbers if necessary
|
|
||||||
content_db = self.site.content_manager.contents.db
|
|
||||||
if time.time() - content_db.time_peer_numbers_updated > 60 * 1 and time.time() - self.time_peer_numbers_updated > 60 * 5:
|
|
||||||
# Start in new thread to avoid blocking
|
|
||||||
self.time_peer_numbers_updated = time.time()
|
|
||||||
gevent.spawn(self.updatePeerNumbers)
|
|
||||||
|
|
||||||
if not self.hasSitePermission(address):
|
|
||||||
return self.response(to, {"error": "Forbidden"})
|
|
||||||
|
|
||||||
if not all([re.match("^[a-z_*/+-]+( DESC| ASC|)$", part.strip()) for part in orderby.split(",")]):
|
|
||||||
return self.response(to, "Invalid order_by")
|
|
||||||
|
|
||||||
if type(limit) != int:
|
|
||||||
return self.response(to, "Invalid limit")
|
|
||||||
|
|
||||||
back = []
|
|
||||||
content_db = self.site.content_manager.contents.db
|
|
||||||
site_id = content_db.site_ids[address]
|
|
||||||
query = "SELECT * FROM file_optional WHERE site_id = %s AND is_downloaded = 1 ORDER BY %s LIMIT %s" % (site_id, orderby, limit)
|
|
||||||
for row in content_db.execute(query):
|
|
||||||
back.append(dict(row))
|
|
||||||
self.response(to, back)
|
|
||||||
|
|
||||||
def actionOptionalFileInfo(self, to, inner_path):
|
|
||||||
content_db = self.site.content_manager.contents.db
|
|
||||||
site_id = content_db.site_ids[self.site.address]
|
|
||||||
|
|
||||||
# Update peer numbers if necessary
|
|
||||||
if time.time() - content_db.time_peer_numbers_updated > 60 * 1 and time.time() - self.time_peer_numbers_updated > 60 * 5:
|
|
||||||
# Start in new thread to avoid blocking
|
|
||||||
self.time_peer_numbers_updated = time.time()
|
|
||||||
gevent.spawn(self.updatePeerNumbers)
|
|
||||||
|
|
||||||
query = "SELECT * FROM file_optional WHERE site_id = :site_id AND inner_path = :inner_path LIMIT 1"
|
|
||||||
res = content_db.execute(query, {"site_id": site_id, "inner_path": inner_path})
|
|
||||||
row = next(res, None)
|
|
||||||
if row:
|
|
||||||
self.response(to, dict(row))
|
|
||||||
else:
|
|
||||||
self.response(to, None)
|
|
||||||
|
|
||||||
def setPin(self, inner_path, is_pinned, address=None):
|
|
||||||
if not address:
|
|
||||||
address = self.site.address
|
|
||||||
|
|
||||||
if not self.hasSitePermission(address):
|
|
||||||
return {"error": "Forbidden"}
|
|
||||||
|
|
||||||
site = self.server.sites[address]
|
|
||||||
|
|
||||||
content_db = site.content_manager.contents.db
|
|
||||||
site_id = content_db.site_ids[site.address]
|
|
||||||
content_db.execute("UPDATE file_optional SET is_pinned = %s WHERE ?" % is_pinned, {"site_id": site_id, "inner_path": inner_path})
|
|
||||||
|
|
||||||
return "ok"
|
|
||||||
|
|
||||||
def actionOptionalFilePin(self, to, inner_path, address=None):
|
|
||||||
back = self.setPin(inner_path, 1, address)
|
|
||||||
if back == "ok":
|
|
||||||
self.cmd("notification", ["done", _["Pinned %s files"] % len(inner_path) if type(inner_path) is list else 1, 5000])
|
|
||||||
self.response(to, back)
|
|
||||||
|
|
||||||
def actionOptionalFileUnpin(self, to, inner_path, address=None):
|
|
||||||
back = self.setPin(inner_path, 0, address)
|
|
||||||
if back == "ok":
|
|
||||||
self.cmd("notification", ["done", _["Removed pin from %s files"] % len(inner_path) if type(inner_path) is list else 1, 5000])
|
|
||||||
self.response(to, back)
|
|
||||||
|
|
||||||
def actionOptionalFileDelete(self, to, inner_path, address=None):
|
|
||||||
if not address:
|
|
||||||
address = self.site.address
|
|
||||||
|
|
||||||
if not self.hasSitePermission(address):
|
|
||||||
return self.response(to, {"error": "Forbidden"})
|
|
||||||
|
|
||||||
site = self.server.sites[address]
|
|
||||||
|
|
||||||
content_db = site.content_manager.contents.db
|
|
||||||
site_id = content_db.site_ids[site.address]
|
|
||||||
|
|
||||||
res = content_db.execute("SELECT * FROM file_optional WHERE ? LIMIT 1", {"site_id": site_id, "inner_path": inner_path})
|
|
||||||
row = next(res, None)
|
|
||||||
|
|
||||||
if not row:
|
|
||||||
return self.response(to, {"error": "Not found in content.db"})
|
|
||||||
|
|
||||||
removed = site.content_manager.optionalRemove(inner_path, row["hash_id"], row["size"])
|
|
||||||
# if not removed:
|
|
||||||
# return self.response(to, {"error": "Not found in hash_id: %s" % row["hash_id"]})
|
|
||||||
|
|
||||||
content_db.execute("UPDATE file_optional SET is_downloaded = 0, is_pinned = 0, peer = peer - 1 WHERE ?", {"site_id": site_id, "inner_path": inner_path})
|
|
||||||
|
|
||||||
try:
|
|
||||||
site.storage.delete(inner_path)
|
|
||||||
except Exception, err:
|
|
||||||
return self.response(to, {"error": "File delete error: %s" % err})
|
|
||||||
|
|
||||||
self.response(to, "ok")
|
|
||||||
|
|
||||||
|
|
||||||
# Limit functions
|
|
||||||
|
|
||||||
def actionOptionalLimitStats(self, to):
|
|
||||||
if "ADMIN" not in self.site.settings["permissions"]:
|
|
||||||
return self.response(to, "Forbidden")
|
|
||||||
|
|
||||||
back = {}
|
|
||||||
back["limit"] = config.optional_limit
|
|
||||||
back["used"] = self.site.content_manager.contents.db.execute(
|
|
||||||
"SELECT SUM(size) FROM file_optional WHERE is_downloaded = 1 AND is_pinned = 0"
|
|
||||||
).fetchone()[0]
|
|
||||||
back["free"] = helper.getFreeSpace()
|
|
||||||
|
|
||||||
self.response(to, back)
|
|
||||||
|
|
||||||
def actionOptionalLimitSet(self, to, limit):
|
|
||||||
if "ADMIN" not in self.site.settings["permissions"]:
|
|
||||||
return self.response(to, {"error": "Forbidden"})
|
|
||||||
config.optional_limit = re.sub("\.0+$", "", limit) # Remove unnecessary digits from end
|
|
||||||
config.saveValue("optional_limit", limit)
|
|
||||||
self.response(to, "ok")
|
|
||||||
|
|
||||||
# Distribute help functions
|
|
||||||
|
|
||||||
def actionOptionalHelpList(self, to, address=None):
|
|
||||||
if not address:
|
|
||||||
address = self.site.address
|
|
||||||
|
|
||||||
if not self.hasSitePermission(address):
|
|
||||||
return self.response(to, {"error": "Forbidden"})
|
|
||||||
|
|
||||||
site = self.server.sites[address]
|
|
||||||
|
|
||||||
self.response(to, site.settings.get("optional_help", {}))
|
|
||||||
|
|
||||||
def actionOptionalHelp(self, to, directory, title, address=None):
|
|
||||||
if not address:
|
|
||||||
address = self.site.address
|
|
||||||
|
|
||||||
if not self.hasSitePermission(address):
|
|
||||||
return self.response(to, {"error": "Forbidden"})
|
|
||||||
|
|
||||||
site = self.server.sites[address]
|
|
||||||
content_db = site.content_manager.contents.db
|
|
||||||
site_id = content_db.site_ids[address]
|
|
||||||
|
|
||||||
if "optional_help" not in site.settings:
|
|
||||||
site.settings["optional_help"] = {}
|
|
||||||
|
|
||||||
stats = content_db.execute(
|
|
||||||
"SELECT COUNT(*) AS num, SUM(size) AS size FROM file_optional WHERE site_id = :site_id AND inner_path LIKE :inner_path",
|
|
||||||
{"site_id": site_id, "inner_path": directory + "%"}
|
|
||||||
).fetchone()
|
|
||||||
stats = dict(stats)
|
|
||||||
|
|
||||||
if not stats["size"]:
|
|
||||||
stats["size"] = 0
|
|
||||||
if not stats["num"]:
|
|
||||||
stats["num"] = 0
|
|
||||||
|
|
||||||
self.cmd("notification", [
|
|
||||||
"done",
|
|
||||||
_["You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>"] %
|
|
||||||
(cgi.escape(title), cgi.escape(directory)),
|
|
||||||
10000
|
|
||||||
])
|
|
||||||
|
|
||||||
site.settings["optional_help"][directory] = title
|
|
||||||
|
|
||||||
self.response(to, dict(stats))
|
|
||||||
|
|
||||||
def actionOptionalHelpRemove(self, to, directory, address=None):
|
|
||||||
if not address:
|
|
||||||
address = self.site.address
|
|
||||||
|
|
||||||
if not self.hasSitePermission(address):
|
|
||||||
return self.response(to, {"error": "Forbidden"})
|
|
||||||
|
|
||||||
site = self.server.sites[address]
|
|
||||||
|
|
||||||
try:
|
|
||||||
del site.settings["optional_help"][directory]
|
|
||||||
self.response(to, "ok")
|
|
||||||
except Exception:
|
|
||||||
self.response(to, {"error": "Not found"})
|
|
||||||
|
|
||||||
def cbOptionalHelpAll(self, to, site, value):
|
|
||||||
site.settings["autodownloadoptional"] = value
|
|
||||||
self.response(to, value)
|
|
||||||
|
|
||||||
def actionOptionalHelpAll(self, to, value, address=None):
|
|
||||||
if not address:
|
|
||||||
address = self.site.address
|
|
||||||
|
|
||||||
if not self.hasSitePermission(address):
|
|
||||||
return self.response(to, {"error": "Forbidden"})
|
|
||||||
|
|
||||||
site = self.server.sites[address]
|
|
||||||
|
|
||||||
if value:
|
|
||||||
if "ADMIN" in self.site.settings["permissions"]:
|
|
||||||
self.cbOptionalHelpAll(to, site, True)
|
|
||||||
else:
|
|
||||||
site_title = site.content_manager.contents["content.json"].get("title", address)
|
|
||||||
self.cmd(
|
|
||||||
"confirm",
|
|
||||||
[
|
|
||||||
_["Help distribute all new optional files on site <b>%s</b>"] % cgi.escape(site_title),
|
|
||||||
_["Yes, I want to help!"]
|
|
||||||
],
|
|
||||||
lambda (res): self.cbOptionalHelpAll(to, site, True)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
site.settings["autodownloadoptional"] = False
|
|
||||||
self.response(to, False)
|
|
|
@ -1 +0,0 @@
|
||||||
import OptionalManagerPlugin
|
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"Pinned %s files": "Fichiers %s épinglés",
|
|
||||||
"Removed pin from %s files": "Fichiers %s ne sont plus épinglés",
|
|
||||||
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "Vous avez commencé à aider à distribuer <b>%s</b>.<br><small>Dossier : %s</small>",
|
|
||||||
"Help distribute all new optional files on site <b>%s</b>": "Aider à distribuer tous les fichiers optionnels du site <b>%s</b>",
|
|
||||||
"Yes, I want to help!": "Oui, je veux aider !"
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"Pinned %s files": "%s fájl rögzítve",
|
|
||||||
"Removed pin from %s files": "%s fájl rögzítés eltávolítva",
|
|
||||||
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "Új segítség a terjesztésben: <b>%s</b>.<br><small>Könyvtár: %s</small>",
|
|
||||||
"Help distribute all new optional files on site <b>%s</b>": "Segítség az összes új opcionális fájl terjesztésében az <b>%s</b> oldalon",
|
|
||||||
"Yes, I want to help!": "Igen, segíteni akarok!"
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"Pinned %s files": "已固定 %s 個檔",
|
|
||||||
"Removed pin from %s files": "已解除固定 %s 個檔",
|
|
||||||
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "你已經開始幫助分發 <b>%s</b> 。<br><small>目錄:%s</small>",
|
|
||||||
"Help distribute all new optional files on site <b>%s</b>": "你想要幫助分發 <b>%s</b> 網站的所有檔嗎?",
|
|
||||||
"Yes, I want to help!": "是,我想要幫助!"
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"Pinned %s files": "已固定 %s 个文件",
|
|
||||||
"Removed pin from %s files": "已解除固定 %s 个文件",
|
|
||||||
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "你已经开始帮助分发 <b>%s</b> 。<br><small>目录:%s</small>",
|
|
||||||
"Help distribute all new optional files on site <b>%s</b>": "你想要帮助分发 <b>%s</b> 站点的所有文件吗?",
|
|
||||||
"Yes, I want to help!": "是,我想要帮助!"
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
import time
|
|
||||||
import sqlite3
|
|
||||||
import random
|
|
||||||
import atexit
|
|
||||||
|
|
||||||
import gevent
|
|
||||||
from Plugin import PluginManager
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("ContentDb")
|
|
||||||
class ContentDbPlugin(object):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
atexit.register(self.saveAllPeers)
|
|
||||||
super(ContentDbPlugin, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def getSchema(self):
|
|
||||||
schema = super(ContentDbPlugin, self).getSchema()
|
|
||||||
|
|
||||||
schema["tables"]["peer"] = {
|
|
||||||
"cols": [
|
|
||||||
["site_id", "INTEGER REFERENCES site (site_id) ON DELETE CASCADE"],
|
|
||||||
["address", "TEXT NOT NULL"],
|
|
||||||
["port", "INTEGER NOT NULL"],
|
|
||||||
["hashfield", "BLOB"],
|
|
||||||
["time_added", "INTEGER NOT NULL"]
|
|
||||||
],
|
|
||||||
"indexes": [
|
|
||||||
"CREATE UNIQUE INDEX peer_key ON peer (site_id, address, port)"
|
|
||||||
],
|
|
||||||
"schema_changed": 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return schema
|
|
||||||
|
|
||||||
def loadPeers(self, site):
|
|
||||||
s = time.time()
|
|
||||||
site_id = self.site_ids.get(site.address)
|
|
||||||
res = self.execute("SELECT * FROM peer WHERE site_id = :site_id", {"site_id": site_id})
|
|
||||||
num = 0
|
|
||||||
num_hashfield = 0
|
|
||||||
for row in res:
|
|
||||||
peer = site.addPeer(row["address"], row["port"])
|
|
||||||
if not peer: # Already exist
|
|
||||||
continue
|
|
||||||
if row["hashfield"]:
|
|
||||||
peer.hashfield.replaceFromString(row["hashfield"])
|
|
||||||
num_hashfield += 1
|
|
||||||
peer.time_added = row["time_added"]
|
|
||||||
num += 1
|
|
||||||
site.log.debug("%s peers (%s with hashfield) loaded in %.3fs" % (num, num_hashfield, time.time() - s))
|
|
||||||
|
|
||||||
def iteratePeers(self, site):
|
|
||||||
site_id = self.site_ids.get(site.address)
|
|
||||||
for key, peer in site.peers.iteritems():
|
|
||||||
address, port = key.split(":")
|
|
||||||
if peer.has_hashfield:
|
|
||||||
hashfield = sqlite3.Binary(peer.hashfield.tostring())
|
|
||||||
else:
|
|
||||||
hashfield = ""
|
|
||||||
yield (site_id, address, port, hashfield, int(peer.time_added))
|
|
||||||
|
|
||||||
def savePeers(self, site, spawn=False):
|
|
||||||
if spawn:
|
|
||||||
# Save peers every hour (+random some secs to not update very site at same time)
|
|
||||||
gevent.spawn_later(60 * 60 + random.randint(0, 60), self.savePeers, site, spawn=True)
|
|
||||||
if not site.peers:
|
|
||||||
site.log.debug("Peers not saved: No peers found")
|
|
||||||
return
|
|
||||||
s = time.time()
|
|
||||||
site_id = self.site_ids.get(site.address)
|
|
||||||
cur = self.getCursor()
|
|
||||||
cur.execute("BEGIN")
|
|
||||||
self.execute("DELETE FROM peer WHERE site_id = :site_id", {"site_id": site_id})
|
|
||||||
self.cur.cursor.executemany(
|
|
||||||
"INSERT INTO peer (site_id, address, port, hashfield, time_added) VALUES (?, ?, ?, ?, ?)",
|
|
||||||
self.iteratePeers(site)
|
|
||||||
)
|
|
||||||
cur.execute("END")
|
|
||||||
site.log.debug("Peers saved in %.3fs" % (time.time() - s))
|
|
||||||
|
|
||||||
def initSite(self, site):
|
|
||||||
super(ContentDbPlugin, self).initSite(site)
|
|
||||||
gevent.spawn_later(0.5, self.loadPeers, site)
|
|
||||||
gevent.spawn_later(60*60, self.savePeers, site, spawn=True)
|
|
||||||
|
|
||||||
def saveAllPeers(self):
|
|
||||||
for site in self.sites.values():
|
|
||||||
try:
|
|
||||||
self.savePeers(site)
|
|
||||||
except Exception, err:
|
|
||||||
site.log.error("Save peer error: %s" % err)
|
|
|
@ -1,2 +0,0 @@
|
||||||
import PeerDbPlugin
|
|
||||||
|
|
|
@ -1,605 +0,0 @@
|
||||||
import re
|
|
||||||
import os
|
|
||||||
import cgi
|
|
||||||
import sys
|
|
||||||
import math
|
|
||||||
import time
|
|
||||||
import json
|
|
||||||
try:
|
|
||||||
import cStringIO as StringIO
|
|
||||||
except:
|
|
||||||
import StringIO
|
|
||||||
|
|
||||||
import gevent
|
|
||||||
|
|
||||||
from Config import config
|
|
||||||
from Plugin import PluginManager
|
|
||||||
from Debug import Debug
|
|
||||||
from Translate import Translate
|
|
||||||
from util import helper
|
|
||||||
|
|
||||||
plugin_dir = "plugins/Sidebar"
|
|
||||||
media_dir = plugin_dir + "/media"
|
|
||||||
sys.path.append(plugin_dir) # To able to load geoip lib
|
|
||||||
|
|
||||||
loc_cache = {}
|
|
||||||
if "_" not in locals():
|
|
||||||
_ = Translate(plugin_dir + "/languages/")
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("UiRequest")
|
|
||||||
class UiRequestPlugin(object):
|
|
||||||
# Inject our resources to end of original file streams
|
|
||||||
def actionUiMedia(self, path):
|
|
||||||
if path == "/uimedia/all.js" or path == "/uimedia/all.css":
|
|
||||||
# First yield the original file and header
|
|
||||||
body_generator = super(UiRequestPlugin, self).actionUiMedia(path)
|
|
||||||
for part in body_generator:
|
|
||||||
yield part
|
|
||||||
|
|
||||||
# Append our media file to the end
|
|
||||||
ext = re.match(".*(js|css)$", path).group(1)
|
|
||||||
plugin_media_file = "%s/all.%s" % (media_dir, ext)
|
|
||||||
if config.debug:
|
|
||||||
# If debugging merge *.css to all.css and *.js to all.js
|
|
||||||
from Debug import DebugMedia
|
|
||||||
DebugMedia.merge(plugin_media_file)
|
|
||||||
if ext == "js":
|
|
||||||
yield _.translateData(open(plugin_media_file).read())
|
|
||||||
else:
|
|
||||||
for part in self.actionFile(plugin_media_file, send_header=False):
|
|
||||||
yield part
|
|
||||||
elif path.startswith("/uimedia/globe/"): # Serve WebGL globe files
|
|
||||||
file_name = re.match(".*/(.*)", path).group(1)
|
|
||||||
plugin_media_file = "%s-globe/%s" % (media_dir, file_name)
|
|
||||||
if config.debug and path.endswith("all.js"):
|
|
||||||
# If debugging merge *.css to all.css and *.js to all.js
|
|
||||||
from Debug import DebugMedia
|
|
||||||
DebugMedia.merge(plugin_media_file)
|
|
||||||
for part in self.actionFile(plugin_media_file):
|
|
||||||
yield part
|
|
||||||
else:
|
|
||||||
for part in super(UiRequestPlugin, self).actionUiMedia(path):
|
|
||||||
yield part
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("UiWebsocket")
|
|
||||||
class UiWebsocketPlugin(object):
|
|
||||||
def sidebarRenderPeerStats(self, body, site):
|
|
||||||
connected = len([peer for peer in site.peers.values() if peer.connection and peer.connection.connected])
|
|
||||||
connectable = len([peer_id for peer_id in site.peers.keys() if not peer_id.endswith(":0")])
|
|
||||||
onion = len([peer_id for peer_id in site.peers.keys() if ".onion" in peer_id])
|
|
||||||
peers_total = len(site.peers)
|
|
||||||
if peers_total:
|
|
||||||
percent_connected = float(connected) / peers_total
|
|
||||||
percent_connectable = float(connectable) / peers_total
|
|
||||||
percent_onion = float(onion) / peers_total
|
|
||||||
else:
|
|
||||||
percent_connectable = percent_connected = percent_onion = 0
|
|
||||||
|
|
||||||
body.append(_(u"""
|
|
||||||
<li>
|
|
||||||
<label>{_[Peers]}</label>
|
|
||||||
<ul class='graph'>
|
|
||||||
<li style='width: 100%' class='total back-black' title="{_[Total peers]}"></li>
|
|
||||||
<li style='width: {percent_connectable:.0%}' class='connectable back-blue' title='{_[Connectable peers]}'></li>
|
|
||||||
<li style='width: {percent_onion:.0%}' class='connected back-purple' title='{_[Onion]}'></li>
|
|
||||||
<li style='width: {percent_connected:.0%}' class='connected back-green' title='{_[Connected peers]}'></li>
|
|
||||||
</ul>
|
|
||||||
<ul class='graph-legend'>
|
|
||||||
<li class='color-green'><span>{_[Connected]}:</span><b>{connected}</b></li>
|
|
||||||
<li class='color-blue'><span>{_[Connectable]}:</span><b>{connectable}</b></li>
|
|
||||||
<li class='color-purple'><span>{_[Onion]}:</span><b>{onion}</b></li>
|
|
||||||
<li class='color-black'><span>{_[Total]}:</span><b>{peers_total}</b></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
"""))
|
|
||||||
|
|
||||||
def sidebarRenderTransferStats(self, body, site):
|
|
||||||
recv = float(site.settings.get("bytes_recv", 0)) / 1024 / 1024
|
|
||||||
sent = float(site.settings.get("bytes_sent", 0)) / 1024 / 1024
|
|
||||||
transfer_total = recv + sent
|
|
||||||
if transfer_total:
|
|
||||||
percent_recv = recv / transfer_total
|
|
||||||
percent_sent = sent / transfer_total
|
|
||||||
else:
|
|
||||||
percent_recv = 0.5
|
|
||||||
percent_sent = 0.5
|
|
||||||
|
|
||||||
body.append(_(u"""
|
|
||||||
<li>
|
|
||||||
<label>{_[Data transfer]}</label>
|
|
||||||
<ul class='graph graph-stacked'>
|
|
||||||
<li style='width: {percent_recv:.0%}' class='received back-yellow' title="{_[Received bytes]}"></li>
|
|
||||||
<li style='width: {percent_sent:.0%}' class='sent back-green' title="{_[Sent bytes]}"></li>
|
|
||||||
</ul>
|
|
||||||
<ul class='graph-legend'>
|
|
||||||
<li class='color-yellow'><span>{_[Received]}:</span><b>{recv:.2f}MB</b></li>
|
|
||||||
<li class='color-green'<span>{_[Sent]}:</span><b>{sent:.2f}MB</b></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
"""))
|
|
||||||
|
|
||||||
def sidebarRenderFileStats(self, body, site):
|
|
||||||
body.append(_(u"<li><label>{_[Files]}</label><ul class='graph graph-stacked'>"))
|
|
||||||
|
|
||||||
extensions = (
|
|
||||||
("html", "yellow"),
|
|
||||||
("css", "orange"),
|
|
||||||
("js", "purple"),
|
|
||||||
("Image", "green"),
|
|
||||||
("json", "darkblue"),
|
|
||||||
("User data", "blue"),
|
|
||||||
("Other", "white"),
|
|
||||||
("Total", "black")
|
|
||||||
)
|
|
||||||
# Collect stats
|
|
||||||
size_filetypes = {}
|
|
||||||
size_total = 0
|
|
||||||
contents = site.content_manager.listContents() # Without user files
|
|
||||||
for inner_path in contents:
|
|
||||||
content = site.content_manager.contents[inner_path]
|
|
||||||
if "files" not in content:
|
|
||||||
continue
|
|
||||||
for file_name, file_details in content["files"].items():
|
|
||||||
size_total += file_details["size"]
|
|
||||||
ext = file_name.split(".")[-1]
|
|
||||||
size_filetypes[ext] = size_filetypes.get(ext, 0) + file_details["size"]
|
|
||||||
|
|
||||||
# Get user file sizes
|
|
||||||
size_user_content = site.content_manager.contents.execute(
|
|
||||||
"SELECT SUM(size) + SUM(size_files) AS size FROM content WHERE ?",
|
|
||||||
{"not__inner_path": contents}
|
|
||||||
).fetchone()["size"]
|
|
||||||
if not size_user_content:
|
|
||||||
size_user_content = 0
|
|
||||||
size_filetypes["User data"] = size_user_content
|
|
||||||
size_total += size_user_content
|
|
||||||
|
|
||||||
# The missing difference is content.json sizes
|
|
||||||
if "json" in size_filetypes:
|
|
||||||
size_filetypes["json"] += max(0, site.settings["size"] - size_total)
|
|
||||||
size_total = size_other = site.settings["size"]
|
|
||||||
|
|
||||||
# Bar
|
|
||||||
for extension, color in extensions:
|
|
||||||
if extension == "Total":
|
|
||||||
continue
|
|
||||||
if extension == "Other":
|
|
||||||
size = max(0, size_other)
|
|
||||||
elif extension == "Image":
|
|
||||||
size = size_filetypes.get("jpg", 0) + size_filetypes.get("png", 0) + size_filetypes.get("gif", 0)
|
|
||||||
size_other -= size
|
|
||||||
else:
|
|
||||||
size = size_filetypes.get(extension, 0)
|
|
||||||
size_other -= size
|
|
||||||
if size_total == 0:
|
|
||||||
percent = 0
|
|
||||||
else:
|
|
||||||
percent = 100 * (float(size) / size_total)
|
|
||||||
percent = math.floor(percent * 100) / 100 # Floor to 2 digits
|
|
||||||
body.append(
|
|
||||||
u"""<li style='width: %.2f%%' class='%s back-%s' title="%s"></li>""" %
|
|
||||||
(percent, _[extension], color, _[extension])
|
|
||||||
)
|
|
||||||
|
|
||||||
# Legend
|
|
||||||
body.append("</ul><ul class='graph-legend'>")
|
|
||||||
for extension, color in extensions:
|
|
||||||
if extension == "Other":
|
|
||||||
size = max(0, size_other)
|
|
||||||
elif extension == "Image":
|
|
||||||
size = size_filetypes.get("jpg", 0) + size_filetypes.get("png", 0) + size_filetypes.get("gif", 0)
|
|
||||||
elif extension == "Total":
|
|
||||||
size = size_total
|
|
||||||
else:
|
|
||||||
size = size_filetypes.get(extension, 0)
|
|
||||||
|
|
||||||
if extension == "js":
|
|
||||||
title = "javascript"
|
|
||||||
else:
|
|
||||||
title = extension
|
|
||||||
|
|
||||||
if size > 1024 * 1024 * 10: # Format as mB is more than 10mB
|
|
||||||
size_formatted = "%.0fMB" % (size / 1024 / 1024)
|
|
||||||
else:
|
|
||||||
size_formatted = "%.0fkB" % (size / 1024)
|
|
||||||
|
|
||||||
body.append(u"<li class='color-%s'><span>%s:</span><b>%s</b></li>" % (color, _[title], size_formatted))
|
|
||||||
|
|
||||||
body.append("</ul></li>")
|
|
||||||
|
|
||||||
def sidebarRenderSizeLimit(self, body, site):
|
|
||||||
free_space = helper.getFreeSpace() / 1024 / 1024
|
|
||||||
size = float(site.settings["size"]) / 1024 / 1024
|
|
||||||
size_limit = site.getSizeLimit()
|
|
||||||
percent_used = size / size_limit
|
|
||||||
|
|
||||||
body.append(_(u"""
|
|
||||||
<li>
|
|
||||||
<label>{_[Size limit]} <small>({_[limit used]}: {percent_used:.0%}, {_[free space]}: {free_space:,d}MB)</small></label>
|
|
||||||
<input type='text' class='text text-num' value="{size_limit}" id='input-sitelimit'/><span class='text-post'>MB</span>
|
|
||||||
<a href='#Set' class='button' id='button-sitelimit'>{_[Set]}</a>
|
|
||||||
</li>
|
|
||||||
"""))
|
|
||||||
|
|
||||||
def sidebarRenderOptionalFileStats(self, body, site):
|
|
||||||
size_total = float(site.settings["size_optional"])
|
|
||||||
size_downloaded = float(site.settings["optional_downloaded"])
|
|
||||||
|
|
||||||
if not size_total:
|
|
||||||
return False
|
|
||||||
|
|
||||||
percent_downloaded = size_downloaded / size_total
|
|
||||||
|
|
||||||
size_formatted_total = size_total / 1024 / 1024
|
|
||||||
size_formatted_downloaded = size_downloaded / 1024 / 1024
|
|
||||||
|
|
||||||
body.append(_(u"""
|
|
||||||
<li>
|
|
||||||
<label>{_[Optional files]}</label>
|
|
||||||
<ul class='graph'>
|
|
||||||
<li style='width: 100%' class='total back-black' title="{_[Total size]}"></li>
|
|
||||||
<li style='width: {percent_downloaded:.0%}' class='connected back-green' title='{_[Downloaded files]}'></li>
|
|
||||||
</ul>
|
|
||||||
<ul class='graph-legend'>
|
|
||||||
<li class='color-green'><span>{_[Downloaded]}:</span><b>{size_formatted_downloaded:.2f}MB</b></li>
|
|
||||||
<li class='color-black'><span>{_[Total]}:</span><b>{size_formatted_total:.2f}MB</b></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
"""))
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def sidebarRenderOptionalFileSettings(self, body, site):
|
|
||||||
if self.site.settings.get("autodownloadoptional"):
|
|
||||||
checked = "checked='checked'"
|
|
||||||
else:
|
|
||||||
checked = ""
|
|
||||||
|
|
||||||
body.append(_(u"""
|
|
||||||
<li>
|
|
||||||
<label>{_[Download and help distribute all files]}</label>
|
|
||||||
<input type="checkbox" class="checkbox" id="checkbox-autodownloadoptional" {checked}/><div class="checkbox-skin"></div>
|
|
||||||
</li>
|
|
||||||
"""))
|
|
||||||
|
|
||||||
def sidebarRenderBadFiles(self, body, site):
|
|
||||||
body.append(_(u"""
|
|
||||||
<li>
|
|
||||||
<label>{_[Missing files]}:</label>
|
|
||||||
<ul class='filelist'>
|
|
||||||
"""))
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
for bad_file, tries in site.bad_files.iteritems():
|
|
||||||
i += 1
|
|
||||||
body.append(_(u"""<li class='color-red' title="{bad_file} ({tries})">{bad_file}</li>""", {
|
|
||||||
"bad_file": cgi.escape(bad_file, True), "tries": _.pluralize(tries, "{} try", "{} tries")
|
|
||||||
}))
|
|
||||||
if i > 30:
|
|
||||||
break
|
|
||||||
|
|
||||||
if len(site.bad_files) > 30:
|
|
||||||
num_bad_files = len(site.bad_files) - 30
|
|
||||||
body.append(_(u"""<li class='color-red'>{_[+ {num_bad_files} more]}</li>""", nested=True))
|
|
||||||
|
|
||||||
body.append("""
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
""")
|
|
||||||
|
|
||||||
def sidebarRenderDbOptions(self, body, site):
|
|
||||||
if site.storage.db:
|
|
||||||
inner_path = site.storage.getInnerPath(site.storage.db.db_path)
|
|
||||||
size = float(site.storage.getSize(inner_path)) / 1024
|
|
||||||
feeds = len(site.storage.db.schema.get("feeds", {}))
|
|
||||||
else:
|
|
||||||
inner_path = _[u"No database found"]
|
|
||||||
size = 0.0
|
|
||||||
feeds = 0
|
|
||||||
|
|
||||||
body.append(_(u"""
|
|
||||||
<li>
|
|
||||||
<label>{_[Database]} <small>({size:.2f}kB, {_[search feeds]}: {_[{feeds} query]})</small></label>
|
|
||||||
<div class='flex'>
|
|
||||||
<input type='text' class='text disabled' value="{inner_path}" disabled='disabled'/>
|
|
||||||
<a href='#Reload' id="button-dbreload" class='button'>{_[Reload]}</a>
|
|
||||||
<a href='#Rebuild' id="button-dbrebuild" class='button'>{_[Rebuild]}</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
""", nested=True))
|
|
||||||
|
|
||||||
def sidebarRenderIdentity(self, body, site):
|
|
||||||
auth_address = self.user.getAuthAddress(self.site.address)
|
|
||||||
rules = self.site.content_manager.getRules("data/users/%s/content.json" % auth_address)
|
|
||||||
if rules and rules.get("max_size"):
|
|
||||||
quota = rules["max_size"] / 1024
|
|
||||||
try:
|
|
||||||
content = site.content_manager.contents["data/users/%s/content.json" % auth_address]
|
|
||||||
used = len(json.dumps(content)) + sum([file["size"] for file in content["files"].values()])
|
|
||||||
except:
|
|
||||||
used = 0
|
|
||||||
used = used / 1024
|
|
||||||
else:
|
|
||||||
quota = used = 0
|
|
||||||
|
|
||||||
body.append(_(u"""
|
|
||||||
<li>
|
|
||||||
<label>{_[Identity address]} <small>({_[limit used]}: {used:.2f}kB / {quota:.2f}kB)</small></label>
|
|
||||||
<div class='flex'>
|
|
||||||
<span class='input text disabled'>{auth_address}</span>
|
|
||||||
<a href='#Change' class='button' id='button-identity'>{_[Change]}</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
"""))
|
|
||||||
|
|
||||||
def sidebarRenderControls(self, body, site):
|
|
||||||
auth_address = self.user.getAuthAddress(self.site.address)
|
|
||||||
if self.site.settings["serving"]:
|
|
||||||
class_pause = ""
|
|
||||||
class_resume = "hidden"
|
|
||||||
else:
|
|
||||||
class_pause = "hidden"
|
|
||||||
class_resume = ""
|
|
||||||
|
|
||||||
body.append(_(u"""
|
|
||||||
<li>
|
|
||||||
<label>{_[Site control]}</label>
|
|
||||||
<a href='#Update' class='button noupdate' id='button-update'>{_[Update]}</a>
|
|
||||||
<a href='#Pause' class='button {class_pause}' id='button-pause'>{_[Pause]}</a>
|
|
||||||
<a href='#Resume' class='button {class_resume}' id='button-resume'>{_[Resume]}</a>
|
|
||||||
<a href='#Delete' class='button noupdate' id='button-delete'>{_[Delete]}</a>
|
|
||||||
</li>
|
|
||||||
"""))
|
|
||||||
|
|
||||||
site_address = self.site.address
|
|
||||||
body.append(_(u"""
|
|
||||||
<li>
|
|
||||||
<label>{_[Site address]}</label><br>
|
|
||||||
<div class='flex'>
|
|
||||||
<span class='input text disabled'>{site_address}</span>
|
|
||||||
<a href='bitcoin:{site_address}' class='button' id='button-donate'>{_[Donate]}</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
"""))
|
|
||||||
|
|
||||||
def sidebarRenderOwnedCheckbox(self, body, site):
|
|
||||||
if self.site.settings["own"]:
|
|
||||||
checked = "checked='checked'"
|
|
||||||
else:
|
|
||||||
checked = ""
|
|
||||||
|
|
||||||
body.append(_(u"""
|
|
||||||
<h2 class='owned-title'>{_[This is my site]}</h2>
|
|
||||||
<input type="checkbox" class="checkbox" id="checkbox-owned" {checked}/><div class="checkbox-skin"></div>
|
|
||||||
"""))
|
|
||||||
|
|
||||||
def sidebarRenderOwnSettings(self, body, site):
|
|
||||||
title = cgi.escape(site.content_manager.contents.get("content.json", {}).get("title", ""), True)
|
|
||||||
description = cgi.escape(site.content_manager.contents.get("content.json", {}).get("description", ""), True)
|
|
||||||
privatekey = cgi.escape(self.user.getSiteData(site.address, create=False).get("privatekey", ""))
|
|
||||||
|
|
||||||
body.append(_(u"""
|
|
||||||
<li>
|
|
||||||
<label for='settings-title'>{_[Site title]}</label>
|
|
||||||
<input type='text' class='text' value="{title}" id='settings-title'/>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<label for='settings-description'>{_[Site description]}</label>
|
|
||||||
<input type='text' class='text' value="{description}" id='settings-description'/>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li style='display: none'>
|
|
||||||
<label>{_[Private key]}</label>
|
|
||||||
<input type='text' class='text long' value="{privatekey}" placeholder='{_[Ask when signing]}'/>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a href='#Save' class='button' id='button-settings'>{_[Save site settings]}</a>
|
|
||||||
</li>
|
|
||||||
"""))
|
|
||||||
|
|
||||||
def sidebarRenderContents(self, body, site):
|
|
||||||
body.append(_(u"""
|
|
||||||
<li>
|
|
||||||
<label>{_[Content publishing]}</label>
|
|
||||||
"""))
|
|
||||||
|
|
||||||
# Choose content you want to sign
|
|
||||||
contents = ["content.json"]
|
|
||||||
contents += site.content_manager.contents.get("content.json", {}).get("includes", {}).keys()
|
|
||||||
body.append(_(u"<div class='contents'>{_[Choose]}: "))
|
|
||||||
for content in contents:
|
|
||||||
content = cgi.escape(content, True)
|
|
||||||
body.append(_("<a href='#{content}' onclick='$(\"#input-contents\").val(\"{content}\"); return false'>{content}</a> "))
|
|
||||||
body.append("</div>")
|
|
||||||
|
|
||||||
body.append(_(u"""
|
|
||||||
<div class='flex'>
|
|
||||||
<input type='text' class='text' value="content.json" id='input-contents'/>
|
|
||||||
<a href='#Sign' class='button' id='button-sign'>{_[Sign]}</a>
|
|
||||||
<a href='#Publish' class='button' id='button-publish'>{_[Publish]}</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
"""))
|
|
||||||
|
|
||||||
def actionSidebarGetHtmlTag(self, to):
|
|
||||||
site = self.site
|
|
||||||
|
|
||||||
body = []
|
|
||||||
|
|
||||||
body.append("<div>")
|
|
||||||
body.append("<h1>%s</h1>" % cgi.escape(site.content_manager.contents.get("content.json", {}).get("title", ""), True))
|
|
||||||
|
|
||||||
body.append("<div class='globe loading'></div>")
|
|
||||||
|
|
||||||
body.append("<ul class='fields'>")
|
|
||||||
|
|
||||||
self.sidebarRenderPeerStats(body, site)
|
|
||||||
self.sidebarRenderTransferStats(body, site)
|
|
||||||
self.sidebarRenderFileStats(body, site)
|
|
||||||
self.sidebarRenderSizeLimit(body, site)
|
|
||||||
has_optional = self.sidebarRenderOptionalFileStats(body, site)
|
|
||||||
if has_optional:
|
|
||||||
self.sidebarRenderOptionalFileSettings(body, site)
|
|
||||||
self.sidebarRenderDbOptions(body, site)
|
|
||||||
self.sidebarRenderIdentity(body, site)
|
|
||||||
self.sidebarRenderControls(body, site)
|
|
||||||
if site.bad_files:
|
|
||||||
self.sidebarRenderBadFiles(body, site)
|
|
||||||
|
|
||||||
self.sidebarRenderOwnedCheckbox(body, site)
|
|
||||||
body.append("<div class='settings-owned'>")
|
|
||||||
self.sidebarRenderOwnSettings(body, site)
|
|
||||||
self.sidebarRenderContents(body, site)
|
|
||||||
body.append("</div>")
|
|
||||||
body.append("</ul>")
|
|
||||||
body.append("</div>")
|
|
||||||
|
|
||||||
self.response(to, "".join(body))
|
|
||||||
|
|
||||||
def downloadGeoLiteDb(self, db_path):
|
|
||||||
import urllib
|
|
||||||
import gzip
|
|
||||||
import shutil
|
|
||||||
from util import helper
|
|
||||||
|
|
||||||
self.log.info("Downloading GeoLite2 City database...")
|
|
||||||
self.cmd("notification", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], 0])
|
|
||||||
db_urls = [
|
|
||||||
"https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz",
|
|
||||||
"https://raw.githubusercontent.com/texnikru/GeoLite2-Database/master/GeoLite2-City.mmdb.gz"
|
|
||||||
]
|
|
||||||
for db_url in db_urls:
|
|
||||||
try:
|
|
||||||
# Download
|
|
||||||
response = helper.httpRequest(db_url)
|
|
||||||
|
|
||||||
data = StringIO.StringIO()
|
|
||||||
while True:
|
|
||||||
buff = response.read(1024 * 512)
|
|
||||||
if not buff:
|
|
||||||
break
|
|
||||||
data.write(buff)
|
|
||||||
self.log.info("GeoLite2 City database downloaded (%s bytes), unpacking..." % data.tell())
|
|
||||||
data.seek(0)
|
|
||||||
|
|
||||||
# Unpack
|
|
||||||
with gzip.GzipFile(fileobj=data) as gzip_file:
|
|
||||||
shutil.copyfileobj(gzip_file, open(db_path, "wb"))
|
|
||||||
|
|
||||||
self.cmd("notification", ["geolite-done", _["GeoLite2 City database downloaded!"], 5000])
|
|
||||||
time.sleep(2) # Wait for notify animation
|
|
||||||
return True
|
|
||||||
except Exception, err:
|
|
||||||
self.log.error("Error downloading %s: %s" % (db_url, err))
|
|
||||||
pass
|
|
||||||
self.cmd("notification", [
|
|
||||||
"geolite-error",
|
|
||||||
_["GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}"].format(err, db_urls[0]),
|
|
||||||
0
|
|
||||||
])
|
|
||||||
|
|
||||||
def actionSidebarGetPeers(self, to):
|
|
||||||
permissions = self.getPermissions(to)
|
|
||||||
if "ADMIN" not in permissions:
|
|
||||||
return self.response(to, "You don't have permission to run this command")
|
|
||||||
try:
|
|
||||||
import maxminddb
|
|
||||||
db_path = config.data_dir + '/GeoLite2-City.mmdb'
|
|
||||||
if not os.path.isfile(db_path) or os.path.getsize(db_path) == 0:
|
|
||||||
if not self.downloadGeoLiteDb(db_path):
|
|
||||||
return False
|
|
||||||
geodb = maxminddb.open_database(db_path)
|
|
||||||
|
|
||||||
peers = self.site.peers.values()
|
|
||||||
# Find avg ping
|
|
||||||
ping_times = [
|
|
||||||
peer.connection.last_ping_delay
|
|
||||||
for peer in peers
|
|
||||||
if peer.connection and peer.connection.last_ping_delay and peer.connection.last_ping_delay
|
|
||||||
]
|
|
||||||
if ping_times:
|
|
||||||
ping_avg = sum(ping_times) / float(len(ping_times))
|
|
||||||
else:
|
|
||||||
ping_avg = 0
|
|
||||||
# Place bars
|
|
||||||
globe_data = []
|
|
||||||
placed = {} # Already placed bars here
|
|
||||||
for peer in peers:
|
|
||||||
# Height of bar
|
|
||||||
if peer.connection and peer.connection.last_ping_delay:
|
|
||||||
ping = min(0.20, math.log(1 + peer.connection.last_ping_delay / ping_avg, 300))
|
|
||||||
else:
|
|
||||||
ping = -0.03
|
|
||||||
|
|
||||||
# Query and cache location
|
|
||||||
if peer.ip in loc_cache:
|
|
||||||
loc = loc_cache[peer.ip]
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
loc = geodb.get(peer.ip)
|
|
||||||
except:
|
|
||||||
loc = None
|
|
||||||
loc_cache[peer.ip] = loc
|
|
||||||
if not loc or "location" not in loc:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Create position array
|
|
||||||
lat, lon = (loc["location"]["latitude"], loc["location"]["longitude"])
|
|
||||||
latlon = "%s,%s" % (lat, lon)
|
|
||||||
if latlon in placed: # Dont place more than 1 bar to same place, fake repos using ip address last two part
|
|
||||||
lat += float(128 - int(peer.ip.split(".")[-2])) / 50
|
|
||||||
lon += float(128 - int(peer.ip.split(".")[-1])) / 50
|
|
||||||
latlon = "%s,%s" % (lat, lon)
|
|
||||||
placed[latlon] = True
|
|
||||||
|
|
||||||
globe_data += (lat, lon, ping)
|
|
||||||
# Append myself
|
|
||||||
loc = geodb.get(config.ip_external)
|
|
||||||
if loc and loc.get("location"):
|
|
||||||
lat, lon = (loc["location"]["latitude"], loc["location"]["longitude"])
|
|
||||||
globe_data += (lat, lon, -0.135)
|
|
||||||
|
|
||||||
self.response(to, globe_data)
|
|
||||||
except Exception, err:
|
|
||||||
self.log.debug("sidebarGetPeers error: %s" % Debug.formatException(err))
|
|
||||||
self.response(to, {"error": err})
|
|
||||||
|
|
||||||
def actionSiteSetOwned(self, to, owned):
|
|
||||||
permissions = self.getPermissions(to)
|
|
||||||
if "ADMIN" not in permissions:
|
|
||||||
return self.response(to, "You don't have permission to run this command")
|
|
||||||
|
|
||||||
self.site.settings["own"] = bool(owned)
|
|
||||||
|
|
||||||
def actionSiteSetAutodownloadoptional(self, to, owned):
|
|
||||||
permissions = self.getPermissions(to)
|
|
||||||
if "ADMIN" not in permissions:
|
|
||||||
return self.response(to, "You don't have permission to run this command")
|
|
||||||
|
|
||||||
self.site.settings["autodownloadoptional"] = bool(owned)
|
|
||||||
self.site.bad_files = {}
|
|
||||||
gevent.spawn(self.site.update, check_files=True)
|
|
||||||
self.site.worker_manager.removeGoodFileTasks()
|
|
||||||
|
|
||||||
def actionDbReload(self, to):
|
|
||||||
permissions = self.getPermissions(to)
|
|
||||||
if "ADMIN" not in permissions:
|
|
||||||
return self.response(to, "You don't have permission to run this command")
|
|
||||||
|
|
||||||
self.site.storage.closeDb()
|
|
||||||
self.site.storage.getDb()
|
|
||||||
|
|
||||||
return self.response(to, "ok")
|
|
||||||
|
|
||||||
def actionDbRebuild(self, to):
|
|
||||||
permissions = self.getPermissions(to)
|
|
||||||
if "ADMIN" not in permissions:
|
|
||||||
return self.response(to, "You don't have permission to run this command")
|
|
||||||
|
|
||||||
self.site.storage.rebuildDb()
|
|
||||||
|
|
||||||
return self.response(to, "ok")
|
|
|
@ -1 +0,0 @@
|
||||||
import SidebarPlugin
|
|
|
@ -1,81 +0,0 @@
|
||||||
{
|
|
||||||
"Peers": "Klienter",
|
|
||||||
"Connected": "Forbundet",
|
|
||||||
"Connectable": "Mulige",
|
|
||||||
"Connectable peers": "Mulige klienter",
|
|
||||||
|
|
||||||
"Data transfer": "Data overførsel",
|
|
||||||
"Received": "Modtaget",
|
|
||||||
"Received bytes": "Bytes modtaget",
|
|
||||||
"Sent": "Sendt",
|
|
||||||
"Sent bytes": "Bytes sendt",
|
|
||||||
|
|
||||||
"Files": "Filer",
|
|
||||||
"Total": "I alt",
|
|
||||||
"Image": "Image",
|
|
||||||
"Other": "Andet",
|
|
||||||
"User data": "Bruger data",
|
|
||||||
|
|
||||||
"Size limit": "Side max størrelse",
|
|
||||||
"limit used": "brugt",
|
|
||||||
"free space": "fri",
|
|
||||||
"Set": "Opdater",
|
|
||||||
|
|
||||||
"Optional files": "Valgfri filer",
|
|
||||||
"Downloaded": "Downloadet",
|
|
||||||
"Download and help distribute all files": "Download og hjælp med at dele filer",
|
|
||||||
"Total size": "Størrelse i alt",
|
|
||||||
"Downloaded files": "Filer downloadet",
|
|
||||||
|
|
||||||
"Database": "Database",
|
|
||||||
"search feeds": "søgninger",
|
|
||||||
"{feeds} query": "{feeds} søgninger",
|
|
||||||
"Reload": "Genindlæs",
|
|
||||||
"Rebuild": "Genopbyg",
|
|
||||||
"No database found": "Ingen database fundet",
|
|
||||||
|
|
||||||
"Identity address": "Autorisations ID",
|
|
||||||
"Change": "Skift",
|
|
||||||
|
|
||||||
"Update": "Opdater",
|
|
||||||
"Pause": "Pause",
|
|
||||||
"Resume": "Aktiv",
|
|
||||||
"Delete": "Slet",
|
|
||||||
"Are you sure?": "Er du sikker?",
|
|
||||||
|
|
||||||
"Site address": "Side addresse",
|
|
||||||
"Donate": "Doner penge",
|
|
||||||
|
|
||||||
"Missing files": "Manglende filer",
|
|
||||||
"{} try": "{} forsøg",
|
|
||||||
"{} tries": "{} forsøg",
|
|
||||||
"+ {num_bad_files} more": "+ {num_bad_files} mere",
|
|
||||||
|
|
||||||
"This is my site": "Dette er min side",
|
|
||||||
"Site title": "Side navn",
|
|
||||||
"Site description": "Side beskrivelse",
|
|
||||||
"Save site settings": "Gem side opsætning",
|
|
||||||
|
|
||||||
"Content publishing": "Indhold offentliggøres",
|
|
||||||
"Choose": "Vælg",
|
|
||||||
"Sign": "Signer",
|
|
||||||
"Publish": "Offentliggør",
|
|
||||||
|
|
||||||
"This function is disabled on this proxy": "Denne funktion er slået fra på denne ZeroNet proxyEz a funkció ki van kapcsolva ezen a proxy-n",
|
|
||||||
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 City database kunne ikke downloades: {}!<br>Download venligst databasen manuelt og udpak i data folder:<br>{}",
|
|
||||||
"Downloading GeoLite2 City database (one time only, ~20MB)...": "GeoLite2 város adatbázis letöltése (csak egyszer kell, kb 20MB)...",
|
|
||||||
"GeoLite2 City database downloaded!": "GeoLite2 City database downloadet!",
|
|
||||||
|
|
||||||
"Are you sure?": "Er du sikker?",
|
|
||||||
"Site storage limit modified!": "Side max størrelse ændret!",
|
|
||||||
"Database schema reloaded!": "Database definition genindlæst!",
|
|
||||||
"Database rebuilding....": "Genopbygger database...",
|
|
||||||
"Database rebuilt!": "Database genopbygget!",
|
|
||||||
"Site updated!": "Side opdateret!",
|
|
||||||
"Delete this site": "Slet denne side",
|
|
||||||
"File write error: ": "Fejl ved skrivning af fil: ",
|
|
||||||
"Site settings saved!": "Side opsætning gemt!",
|
|
||||||
"Enter your private key:": "Indtast din private nøgle:",
|
|
||||||
" Signed!": " Signeret!",
|
|
||||||
"WebGL not supported": "WebGL er ikke supporteret"
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
{
|
|
||||||
"Peers": "Peers",
|
|
||||||
"Connected": "Verbunden",
|
|
||||||
"Connectable": "Verbindbar",
|
|
||||||
"Connectable peers": "Verbindbare Peers",
|
|
||||||
|
|
||||||
"Data transfer": "Datei Transfer",
|
|
||||||
"Received": "Empfangen",
|
|
||||||
"Received bytes": "Empfangene Bytes",
|
|
||||||
"Sent": "Gesendet",
|
|
||||||
"Sent bytes": "Gesendete Bytes",
|
|
||||||
|
|
||||||
"Files": "Dateien",
|
|
||||||
"Total": "Gesamt",
|
|
||||||
"Image": "Bilder",
|
|
||||||
"Other": "Sonstiges",
|
|
||||||
"User data": "Nutzer Daten",
|
|
||||||
|
|
||||||
"Size limit": "Speicher Limit",
|
|
||||||
"limit used": "Limit benutzt",
|
|
||||||
"free space": "freier Speicher",
|
|
||||||
"Set": "Setzten",
|
|
||||||
|
|
||||||
"Optional files": "Optionale Dateien",
|
|
||||||
"Downloaded": "Heruntergeladen",
|
|
||||||
"Download and help distribute all files": "Herunterladen und helfen alle Dateien zu verteilen",
|
|
||||||
"Total size": "Gesamte Größe",
|
|
||||||
"Downloaded files": "Heruntergeladene Dateien",
|
|
||||||
|
|
||||||
"Database": "Datenbank",
|
|
||||||
"search feeds": "Feeds durchsuchen",
|
|
||||||
"{feeds} query": "{feeds} Abfrage",
|
|
||||||
"Reload": "Neu laden",
|
|
||||||
"Rebuild": "Neu bauen",
|
|
||||||
"No database found": "Keine Datenbank gefunden",
|
|
||||||
|
|
||||||
"Identity address": "Identitäts Adresse",
|
|
||||||
"Change": "Ändern",
|
|
||||||
|
|
||||||
"Update": "Aktualisieren",
|
|
||||||
"Pause": "Pausieren",
|
|
||||||
"Resume": "Fortsetzen",
|
|
||||||
"Delete": "Löschen",
|
|
||||||
"Are you sure?": "Bist du sicher?",
|
|
||||||
|
|
||||||
"Site address": "Seiten Adresse",
|
|
||||||
"Donate": "Spenden",
|
|
||||||
|
|
||||||
"Missing files": "Fehlende Dateien",
|
|
||||||
"{} try": "{} versuch",
|
|
||||||
"{} tries": "{} versuche",
|
|
||||||
"+ {num_bad_files} more": "+ {num_bad_files} mehr",
|
|
||||||
|
|
||||||
"This is my site": "Das ist meine Seite",
|
|
||||||
"Site title": "Seiten Titel",
|
|
||||||
"Site description": "Seiten Beschreibung",
|
|
||||||
"Save site settings": "Einstellungen der Seite speichern",
|
|
||||||
|
|
||||||
"Content publishing": "Inhaltsveröffentlichung",
|
|
||||||
"Choose": "Wähle",
|
|
||||||
"Sign": "Signieren",
|
|
||||||
"Publish": "Veröffentlichen",
|
|
||||||
|
|
||||||
"This function is disabled on this proxy": "Diese Funktion ist auf dieser Proxy deaktiviert",
|
|
||||||
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 City Datenbank Download Fehler: {}!<br>Bitte manuell herunterladen und die Datei in das Datei Verzeichnis extrahieren:<br>{}",
|
|
||||||
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Herunterladen der GeoLite2 City Datenbank (einmalig, ~20MB)...",
|
|
||||||
"GeoLite2 City database downloaded!": "GeoLite2 City Datenbank heruntergeladen!",
|
|
||||||
|
|
||||||
"Are you sure?": "Bist du sicher?",
|
|
||||||
"Site storage limit modified!": "Speicher Limit der Seite modifiziert!",
|
|
||||||
"Database schema reloaded!": "Datebank Schema neu geladen!",
|
|
||||||
"Database rebuilding....": "Datenbank neu bauen...",
|
|
||||||
"Database rebuilt!": "Datenbank neu gebaut!",
|
|
||||||
"Site updated!": "Seite aktualisiert!",
|
|
||||||
"Delete this site": "Diese Seite löschen",
|
|
||||||
"File write error: ": "Datei schreib fehler:",
|
|
||||||
"Site settings saved!": "Seiten Einstellungen gespeichert!",
|
|
||||||
"Enter your private key:": "Gib deinen privaten Schlüssel ein:",
|
|
||||||
" Signed!": " Signiert!",
|
|
||||||
"WebGL not supported": "WebGL nicht unterstützt"
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
{
|
|
||||||
"Peers": "Pares",
|
|
||||||
"Connected": "Conectados",
|
|
||||||
"Connectable": "Conectables",
|
|
||||||
"Connectable peers": "Pares conectables",
|
|
||||||
|
|
||||||
"Data transfer": "Transferencia de datos",
|
|
||||||
"Received": "Recibidos",
|
|
||||||
"Received bytes": "Bytes recibidos",
|
|
||||||
"Sent": "Enviados",
|
|
||||||
"Sent bytes": "Bytes envidados",
|
|
||||||
|
|
||||||
"Files": "Ficheros",
|
|
||||||
"Total": "Total",
|
|
||||||
"Image": "Imagen",
|
|
||||||
"Other": "Otro",
|
|
||||||
"User data": "Datos del usuario",
|
|
||||||
|
|
||||||
"Size limit": "Límite de tamaño",
|
|
||||||
"limit used": "Límite utilizado",
|
|
||||||
"free space": "Espacio libre",
|
|
||||||
"Set": "Establecer",
|
|
||||||
|
|
||||||
"Optional files": "Ficheros opcionales",
|
|
||||||
"Downloaded": "Descargado",
|
|
||||||
"Download and help distribute all files": "Descargar y ayudar a distribuir todos los ficheros",
|
|
||||||
"Total size": "Tamaño total",
|
|
||||||
"Downloaded files": "Ficheros descargados",
|
|
||||||
|
|
||||||
"Database": "Base de datos",
|
|
||||||
"search feeds": "Fuentes de búsqueda",
|
|
||||||
"{feeds} query": "{feeds} consulta",
|
|
||||||
"Reload": "Recargar",
|
|
||||||
"Rebuild": "Reconstruir",
|
|
||||||
"No database found": "No se ha encontrado la base de datos",
|
|
||||||
|
|
||||||
"Identity address": "Dirección de la identidad",
|
|
||||||
"Change": "Cambiar",
|
|
||||||
|
|
||||||
"Update": "Actualizar",
|
|
||||||
"Pause": "Pausar",
|
|
||||||
"Resume": "Reanudar",
|
|
||||||
"Delete": "Borrar",
|
|
||||||
|
|
||||||
"Site address": "Dirección del sitio",
|
|
||||||
"Donate": "Donar",
|
|
||||||
|
|
||||||
"Missing files": "Ficheros perdidos",
|
|
||||||
"{} try": "{} intento",
|
|
||||||
"{} tries": "{} intentos",
|
|
||||||
"+ {num_bad_files} more": "+ {num_bad_files} más",
|
|
||||||
|
|
||||||
"This is my site": "Este es mi sitio",
|
|
||||||
"Site title": "Título del sitio",
|
|
||||||
"Site description": "Descripción del sitio",
|
|
||||||
"Save site settings": "Guardar la configuración del sitio",
|
|
||||||
|
|
||||||
"Content publishing": "Publicación del contenido",
|
|
||||||
"Choose": "Elegir",
|
|
||||||
"Sign": "Firmar",
|
|
||||||
"Publish": "Publicar",
|
|
||||||
"This function is disabled on this proxy": "Esta función está deshabilitada en este proxy",
|
|
||||||
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "¡Error de la base de datos GeoLite2: {}!<br>Por favor, descárgalo manualmente y descomprime al directorio de datos:<br>{}",
|
|
||||||
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Descargando la base de datos de GeoLite2 (una única vez, ~20MB)...",
|
|
||||||
"GeoLite2 City database downloaded!": "¡Base de datos de GeoLite2 descargada!",
|
|
||||||
|
|
||||||
"Are you sure?": "¿Estás seguro?",
|
|
||||||
"Site storage limit modified!": "¡Límite de almacenamiento del sitio modificado!",
|
|
||||||
"Database schema reloaded!": "¡Esquema de la base de datos recargado!",
|
|
||||||
"Database rebuilding....": "Reconstruyendo la base de datos...",
|
|
||||||
"Database rebuilt!": "¡Base de datos reconstruida!",
|
|
||||||
"Site updated!": "¡Sitio actualizado!",
|
|
||||||
"Delete this site": "Borrar este sitio",
|
|
||||||
"File write error: ": "Error de escritura de fichero:",
|
|
||||||
"Site settings saved!": "¡Configuración del sitio guardada!",
|
|
||||||
"Enter your private key:": "Introduce tu clave privada:",
|
|
||||||
" Signed!": " ¡firmado!",
|
|
||||||
"WebGL not supported": "WebGL no está soportado"
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
{
|
|
||||||
"Peers": "Pairs",
|
|
||||||
"Connected": "Connectés",
|
|
||||||
"Connectable": "Accessibles",
|
|
||||||
"Connectable peers": "Pairs accessibles",
|
|
||||||
|
|
||||||
"Data transfer": "Données transférées",
|
|
||||||
"Received": "Reçues",
|
|
||||||
"Received bytes": "Bytes reçus",
|
|
||||||
"Sent": "Envoyées",
|
|
||||||
"Sent bytes": "Bytes envoyés",
|
|
||||||
|
|
||||||
"Files": "Fichiers",
|
|
||||||
"Total": "Total",
|
|
||||||
"Image": "Image",
|
|
||||||
"Other": "Autre",
|
|
||||||
"User data": "Utilisateurs",
|
|
||||||
|
|
||||||
"Size limit": "Taille maximale",
|
|
||||||
"limit used": "utlisé",
|
|
||||||
"free space": "libre",
|
|
||||||
"Set": "Modifier",
|
|
||||||
|
|
||||||
"Optional files": "Fichiers optionnels",
|
|
||||||
"Downloaded": "Téléchargé",
|
|
||||||
"Download and help distribute all files": "Télécharger et distribuer tous les fichiers",
|
|
||||||
"Total size": "Taille totale",
|
|
||||||
"Downloaded files": "Fichiers téléchargés",
|
|
||||||
|
|
||||||
"Database": "Base de données",
|
|
||||||
"search feeds": "recherche",
|
|
||||||
"{feeds} query": "{feeds} requête",
|
|
||||||
"Reload": "Recharger",
|
|
||||||
"Rebuild": "Reconstruire",
|
|
||||||
"No database found": "Aucune base de données trouvée",
|
|
||||||
|
|
||||||
"Identity address": "Adresse d'identité",
|
|
||||||
"Change": "Modifier",
|
|
||||||
|
|
||||||
"Site control": "Opérations",
|
|
||||||
"Update": "Mettre à jour",
|
|
||||||
"Pause": "Suspendre",
|
|
||||||
"Resume": "Reprendre",
|
|
||||||
"Delete": "Supprimer",
|
|
||||||
"Are you sure?": "Êtes-vous certain?",
|
|
||||||
|
|
||||||
"Site address": "Adresse du site",
|
|
||||||
"Donate": "Faire un don",
|
|
||||||
|
|
||||||
"Missing files": "Fichiers manquants",
|
|
||||||
"{} try": "{} essai",
|
|
||||||
"{} tries": "{} essais",
|
|
||||||
"+ {num_bad_files} more": "+ {num_bad_files} manquants",
|
|
||||||
|
|
||||||
"This is my site": "Ce site m'appartient",
|
|
||||||
"Site title": "Nom du site",
|
|
||||||
"Site description": "Description du site",
|
|
||||||
"Save site settings": "Enregistrer les paramètres",
|
|
||||||
|
|
||||||
"Content publishing": "Publication du contenu",
|
|
||||||
"Choose": "Sélectionner",
|
|
||||||
"Sign": "Signer",
|
|
||||||
"Publish": "Publier",
|
|
||||||
|
|
||||||
"This function is disabled on this proxy": "Cette fonction est désactivé sur ce proxy",
|
|
||||||
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Erreur au téléchargement de la base de données GeoLite2: {}!<br>Téléchargez et décompressez dans le dossier data:<br>{}",
|
|
||||||
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Téléchargement de la base de données GeoLite2 (une seule fois, ~20MB)...",
|
|
||||||
"GeoLite2 City database downloaded!": "Base de données GeoLite2 téléchargée!",
|
|
||||||
|
|
||||||
"Are you sure?": "Êtes-vous certain?",
|
|
||||||
"Site storage limit modified!": "Taille maximale modifiée!",
|
|
||||||
"Database schema reloaded!": "Base de données rechargée!",
|
|
||||||
"Database rebuilding....": "Reconstruction de la base de données...",
|
|
||||||
"Database rebuilt!": "Base de données reconstruite!",
|
|
||||||
"Site updated!": "Site mis à jour!",
|
|
||||||
"Delete this site": "Supprimer ce site",
|
|
||||||
"File write error: ": "Erreur à l'écriture du fichier: ",
|
|
||||||
"Site settings saved!": "Paramètres du site enregistrés!",
|
|
||||||
"Enter your private key:": "Entrez votre clé privée:",
|
|
||||||
" Signed!": " Signé!",
|
|
||||||
"WebGL not supported": "WebGL n'est pas supporté"
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
{
|
|
||||||
"Peers": "Csatlakozási pontok",
|
|
||||||
"Connected": "Csaltakozva",
|
|
||||||
"Connectable": "Csatlakozható",
|
|
||||||
"Connectable peers": "Csatlakozható peer-ek",
|
|
||||||
|
|
||||||
"Data transfer": "Adatátvitel",
|
|
||||||
"Received": "Fogadott",
|
|
||||||
"Received bytes": "Fogadott byte-ok",
|
|
||||||
"Sent": "Küldött",
|
|
||||||
"Sent bytes": "Küldött byte-ok",
|
|
||||||
|
|
||||||
"Files": "Fájlok",
|
|
||||||
"Total": "Összesen",
|
|
||||||
"Image": "Kép",
|
|
||||||
"Other": "Egyéb",
|
|
||||||
"User data": "Felh. adat",
|
|
||||||
|
|
||||||
"Size limit": "Méret korlát",
|
|
||||||
"limit used": "felhasznált",
|
|
||||||
"free space": "szabad hely",
|
|
||||||
"Set": "Beállít",
|
|
||||||
|
|
||||||
"Optional files": "Opcionális fájlok",
|
|
||||||
"Downloaded": "Letöltött",
|
|
||||||
"Download and help distribute all files": "Minden opcionális fájl letöltése",
|
|
||||||
"Total size": "Teljes méret",
|
|
||||||
"Downloaded files": "Letöltve",
|
|
||||||
|
|
||||||
"Database": "Adatbázis",
|
|
||||||
"search feeds": "Keresés források",
|
|
||||||
"{feeds} query": "{feeds} lekérdezés",
|
|
||||||
"Reload": "Újratöltés",
|
|
||||||
"Rebuild": "Újraépítés",
|
|
||||||
"No database found": "Adatbázis nem található",
|
|
||||||
|
|
||||||
"Identity address": "Azonosító cím",
|
|
||||||
"Change": "Módosít",
|
|
||||||
|
|
||||||
"Site control": "Oldal műveletek",
|
|
||||||
"Update": "Frissít",
|
|
||||||
"Pause": "Szünteltet",
|
|
||||||
"Resume": "Folytat",
|
|
||||||
"Delete": "Töröl",
|
|
||||||
"Are you sure?": "Biztos vagy benne?",
|
|
||||||
|
|
||||||
"Site address": "Oldal címe",
|
|
||||||
"Donate": "Támogatás",
|
|
||||||
|
|
||||||
"Missing files": "Hiányzó fájlok",
|
|
||||||
"{} try": "{} próbálkozás",
|
|
||||||
"{} tries": "{} próbálkozás",
|
|
||||||
"+ {num_bad_files} more": "+ még {num_bad_files} darab",
|
|
||||||
|
|
||||||
"This is my site": "Ez az én oldalam",
|
|
||||||
"Site title": "Oldal neve",
|
|
||||||
"Site description": "Oldal leírása",
|
|
||||||
"Save site settings": "Oldal beállítások mentése",
|
|
||||||
|
|
||||||
"Content publishing": "Tartalom publikálás",
|
|
||||||
"Choose": "Válassz",
|
|
||||||
"Sign": "Aláírás",
|
|
||||||
"Publish": "Publikálás",
|
|
||||||
|
|
||||||
"This function is disabled on this proxy": "Ez a funkció ki van kapcsolva ezen a proxy-n",
|
|
||||||
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 város adatbázis letöltési hiba: {}!<br>A térképhez töltsd le és csomagold ki a data könyvtárba:<br>{}",
|
|
||||||
"Downloading GeoLite2 City database (one time only, ~20MB)...": "GeoLite2 város adatbázis letöltése (csak egyszer kell, kb 20MB)...",
|
|
||||||
"GeoLite2 City database downloaded!": "GeoLite2 város adatbázis letöltve!",
|
|
||||||
|
|
||||||
"Are you sure?": "Biztos vagy benne?",
|
|
||||||
"Site storage limit modified!": "Az oldalt méret korlát módosítva!",
|
|
||||||
"Database schema reloaded!": "Adatbázis séma újratöltve!",
|
|
||||||
"Database rebuilding....": "Adatbázis újraépítés...",
|
|
||||||
"Database rebuilt!": "Adatbázis újraépítve!",
|
|
||||||
"Site updated!": "Az oldal frissítve!",
|
|
||||||
"Delete this site": "Az oldal törlése",
|
|
||||||
"File write error: ": "Fájl írási hiba: ",
|
|
||||||
"Site settings saved!": "Az oldal beállításai elmentve!",
|
|
||||||
"Enter your private key:": "Add meg a prviát kulcsod:",
|
|
||||||
" Signed!": " Aláírva!",
|
|
||||||
"WebGL not supported": "WebGL nem támogatott"
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
{
|
|
||||||
"Peers": "Peer",
|
|
||||||
"Connected": "Connessi",
|
|
||||||
"Connectable": "Collegabili",
|
|
||||||
"Connectable peers": "Peer collegabili",
|
|
||||||
|
|
||||||
"Data transfer": "Trasferimento dati",
|
|
||||||
"Received": "Ricevuti",
|
|
||||||
"Received bytes": "Byte ricevuti",
|
|
||||||
"Sent": "Inviati",
|
|
||||||
"Sent bytes": "Byte inviati",
|
|
||||||
|
|
||||||
"Files": "File",
|
|
||||||
"Total": "Totale",
|
|
||||||
"Image": "Imagine",
|
|
||||||
"Other": "Altro",
|
|
||||||
"User data": "Dati utente",
|
|
||||||
|
|
||||||
"Size limit": "Limite dimensione",
|
|
||||||
"limit used": "limite usato",
|
|
||||||
"free space": "spazio libero",
|
|
||||||
"Set": "Imposta",
|
|
||||||
|
|
||||||
"Optional files": "File facoltativi",
|
|
||||||
"Downloaded": "Scaricati",
|
|
||||||
"Download and help distribute all files": "Scarica e aiuta a distribuire tutti i file",
|
|
||||||
"Total size": "Dimensione totale",
|
|
||||||
"Downloaded files": "File scaricati",
|
|
||||||
|
|
||||||
"Database": "Database",
|
|
||||||
"search feeds": "ricerca di feed",
|
|
||||||
"{feeds} query": "{feeds} interrogazione",
|
|
||||||
"Reload": "Ricaricare",
|
|
||||||
"Rebuild": "Ricostruire",
|
|
||||||
"No database found": "Nessun database trovato",
|
|
||||||
|
|
||||||
"Identity address": "Indirizzo di identità",
|
|
||||||
"Change": "Cambia",
|
|
||||||
|
|
||||||
"Update": "Aggiorna",
|
|
||||||
"Pause": "Sospendi",
|
|
||||||
"Resume": "Riprendi",
|
|
||||||
"Delete": "Cancella",
|
|
||||||
"Are you sure?": "Sei sicuro?",
|
|
||||||
|
|
||||||
"Site address": "Indirizzo sito",
|
|
||||||
"Donate": "Dona",
|
|
||||||
|
|
||||||
"Missing files": "File mancanti",
|
|
||||||
"{} try": "{} tenta",
|
|
||||||
"{} tries": "{} prova",
|
|
||||||
"+ {num_bad_files} more": "+ {num_bad_files} altri",
|
|
||||||
|
|
||||||
"This is my site": "Questo è il mio sito",
|
|
||||||
"Site title": "Titolo sito",
|
|
||||||
"Site description": "Descrizione sito",
|
|
||||||
"Save site settings": "Salva impostazioni sito",
|
|
||||||
|
|
||||||
"Content publishing": "Pubblicazione contenuto",
|
|
||||||
"Choose": "Scegli",
|
|
||||||
"Sign": "Firma",
|
|
||||||
"Publish": "Pubblica",
|
|
||||||
|
|
||||||
"This function is disabled on this proxy": "Questa funzione è disabilitata su questo proxy",
|
|
||||||
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Errore scaricamento database GeoLite2 City: {}!<br>Si prega di scaricarlo manualmente e spacchetarlo nella cartella dir:<br>{}",
|
|
||||||
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Scaricamento database GeoLite2 City (solo una volta, ~20MB)...",
|
|
||||||
"GeoLite2 City database downloaded!": "Database GeoLite2 City scaricato!",
|
|
||||||
|
|
||||||
"Are you sure?": "Sei sicuro?",
|
|
||||||
"Site storage limit modified!": "Limite di archiviazione del sito modificato!",
|
|
||||||
"Database schema reloaded!": "Schema database ricaricato!",
|
|
||||||
"Database rebuilding....": "Ricostruzione database...",
|
|
||||||
"Database rebuilt!": "Database ricostruito!",
|
|
||||||
"Site updated!": "Sito aggiornato!",
|
|
||||||
"Delete this site": "Cancella questo sito",
|
|
||||||
"File write error: ": "Errore scrittura file:",
|
|
||||||
"Site settings saved!": "Impostazioni sito salvate!",
|
|
||||||
"Enter your private key:": "Inserisci la tua chiave privata:",
|
|
||||||
" Signed!": " Firmato!",
|
|
||||||
"WebGL not supported": "WebGL non supportato"
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
{
|
|
||||||
"Peers": "Użytkownicy równorzędni",
|
|
||||||
"Connected": "Połączony",
|
|
||||||
"Connectable": "Możliwy do podłączenia",
|
|
||||||
"Connectable peers": "Połączeni użytkownicy równorzędni",
|
|
||||||
|
|
||||||
"Data transfer": "Transfer danych",
|
|
||||||
"Received": "Odebrane",
|
|
||||||
"Received bytes": "Odebrany bajty",
|
|
||||||
"Sent": "Wysłane",
|
|
||||||
"Sent bytes": "Wysłane bajty",
|
|
||||||
|
|
||||||
"Files": "Pliki",
|
|
||||||
"Total": "Sumarycznie",
|
|
||||||
"Image": "Obraz",
|
|
||||||
"Other": "Inne",
|
|
||||||
"User data": "Dane użytkownika",
|
|
||||||
|
|
||||||
"Size limit": "Rozmiar limitu",
|
|
||||||
"limit used": "zużyty limit",
|
|
||||||
"free space": "wolna przestrzeń",
|
|
||||||
"Set": "Ustaw",
|
|
||||||
|
|
||||||
"Optional files": "Pliki opcjonalne",
|
|
||||||
"Downloaded": "Ściągnięte",
|
|
||||||
"Download and help distribute all files": "Ściągnij i pomóż rozpowszechniać wszystkie pliki",
|
|
||||||
"Total size": "Rozmiar sumaryczny",
|
|
||||||
"Downloaded files": "Ściągnięte pliki",
|
|
||||||
|
|
||||||
"Database": "Baza danych",
|
|
||||||
"search feeds": "przeszukaj zasoby",
|
|
||||||
"{feeds} query": "{feeds} pytanie",
|
|
||||||
"Reload": "Odśwież",
|
|
||||||
"Rebuild": "Odbuduj",
|
|
||||||
"No database found": "Nie odnaleziono bazy danych",
|
|
||||||
|
|
||||||
"Identity address": "Adres identyfikacyjny",
|
|
||||||
"Change": "Zmień",
|
|
||||||
|
|
||||||
"Site control": "Kontrola strony",
|
|
||||||
"Update": "Zaktualizuj",
|
|
||||||
"Pause": "Wstrzymaj",
|
|
||||||
"Resume": "Wznów",
|
|
||||||
"Delete": "Skasuj",
|
|
||||||
"Are you sure?": "Jesteś pewien?",
|
|
||||||
|
|
||||||
"Site address": "Adres strony",
|
|
||||||
"Donate": "Wspomóż",
|
|
||||||
|
|
||||||
"Missing files": "Brakujące pliki",
|
|
||||||
"{} try": "{} próba",
|
|
||||||
"{} tries": "{} próby",
|
|
||||||
"+ {num_bad_files} more": "+ {num_bad_files} więcej",
|
|
||||||
|
|
||||||
"This is my site": "To moja strona",
|
|
||||||
"Site title": "Tytuł strony",
|
|
||||||
"Site description": "Opis strony",
|
|
||||||
"Save site settings": "Zapisz ustawienia strony",
|
|
||||||
|
|
||||||
"Content publishing": "Publikowanie treści",
|
|
||||||
"Choose": "Wybierz",
|
|
||||||
"Sign": "Podpisz",
|
|
||||||
"Publish": "Opublikuj",
|
|
||||||
|
|
||||||
"This function is disabled on this proxy": "Ta funkcja jest zablokowana w tym proxy",
|
|
||||||
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Błąd ściągania bazy danych GeoLite2 City: {}!<br>Proszę ściągnąć ją recznie i wypakować do katalogu danych:<br>{}",
|
|
||||||
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Ściąganie bazy danych GeoLite2 City (tylko jednorazowo, ok. 20MB)...",
|
|
||||||
"GeoLite2 City database downloaded!": "Baza danych GeoLite2 City ściagnięta!",
|
|
||||||
|
|
||||||
"Are you sure?": "Jesteś pewien?",
|
|
||||||
"Site storage limit modified!": "Limit pamięci strony zmodyfikowany!",
|
|
||||||
"Database schema reloaded!": "Schemat bazy danych załadowany ponownie!",
|
|
||||||
"Database rebuilding....": "Przebudowywanie bazy danych...",
|
|
||||||
"Database rebuilt!": "Baza danych przebudowana!",
|
|
||||||
"Site updated!": "Strona zaktualizowana!",
|
|
||||||
"Delete this site": "Usuń tę stronę",
|
|
||||||
"File write error: ": "Błąd zapisu pliku: ",
|
|
||||||
"Site settings saved!": "Ustawienia strony zapisane!",
|
|
||||||
"Enter your private key:": "Wpisz swój prywatny klucz:",
|
|
||||||
" Signed!": " Podpisane!",
|
|
||||||
"WebGL not supported": "WebGL nie jest obsługiwany"
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
{
|
|
||||||
"Peers": "Пиры",
|
|
||||||
"Connected": "Подключенные",
|
|
||||||
"Connectable": "Доступные",
|
|
||||||
"Connectable peers": "Пиры доступны для подключения",
|
|
||||||
|
|
||||||
"Data transfer": "Передача данных",
|
|
||||||
"Received": "Получено",
|
|
||||||
"Received bytes": "Получено байн",
|
|
||||||
"Sent": "Отправлено",
|
|
||||||
"Sent bytes": "Отправлено байт",
|
|
||||||
|
|
||||||
"Files": "Файлы",
|
|
||||||
"Total": "Всего",
|
|
||||||
"Image": "Изображений",
|
|
||||||
"Other": "Другое",
|
|
||||||
"User data": "Ваш контент",
|
|
||||||
|
|
||||||
"Size limit": "Ограничение по размеру",
|
|
||||||
"limit used": "Использовано",
|
|
||||||
"free space": "Доступно",
|
|
||||||
"Set": "Установить",
|
|
||||||
|
|
||||||
"Optional files": "Опциональные файлы",
|
|
||||||
"Downloaded": "Загружено",
|
|
||||||
"Download and help distribute all files": "Загрузить опциональные файлы для помощи сайту",
|
|
||||||
"Total size": "Объём",
|
|
||||||
"Downloaded files": "Загруженные файлы",
|
|
||||||
|
|
||||||
"Database": "База данных",
|
|
||||||
"search feeds": "поиск подписок",
|
|
||||||
"{feeds} query": "{feeds} запрос",
|
|
||||||
"Reload": "Перезагрузить",
|
|
||||||
"Rebuild": "Перестроить",
|
|
||||||
"No database found": "База данных не найдена",
|
|
||||||
|
|
||||||
"Identity address": "Уникальный адрес",
|
|
||||||
"Change": "Изменить",
|
|
||||||
|
|
||||||
"Site control": "Управление сайтом",
|
|
||||||
"Update": "Обновить",
|
|
||||||
"Pause": "Пауза",
|
|
||||||
"Resume": "Продолжить",
|
|
||||||
"Delete": "Удалить",
|
|
||||||
"Are you sure?": "Вы уверены?",
|
|
||||||
|
|
||||||
"Site address": "Адрес сайта",
|
|
||||||
"Donate": "Пожертвовать",
|
|
||||||
|
|
||||||
"Missing files": "Отсутствующие файлы",
|
|
||||||
"{} try": "{} попробовать",
|
|
||||||
"{} tries": "{} попыток",
|
|
||||||
"+ {num_bad_files} more": "+ {num_bad_files} ещё",
|
|
||||||
|
|
||||||
"This is my site": "Это мой сайт",
|
|
||||||
"Site title": "Название сайта",
|
|
||||||
"Site description": "Описание сайта",
|
|
||||||
"Save site settings": "Сохранить настройки сайта",
|
|
||||||
|
|
||||||
"Content publishing": "Публикация контента",
|
|
||||||
"Choose": "Выбрать",
|
|
||||||
"Sign": "Подписать",
|
|
||||||
"Publish": "Опубликовать",
|
|
||||||
|
|
||||||
"This function is disabled on this proxy": "Эта функция отключена на этом прокси",
|
|
||||||
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Ошибка загрузки базы городов GeoLite2: {}!<br>Пожалуйста, загрузите её вручную и распакуйте в папку:<br>{}",
|
|
||||||
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Загрузка базы городов GeoLite2 (это делается только 1 раз, ~20MB)...",
|
|
||||||
"GeoLite2 City database downloaded!": "База GeoLite2 успешно загружена!",
|
|
||||||
|
|
||||||
"Are you sure?": "Вы уверены?",
|
|
||||||
"Site storage limit modified!": "Лимит хранилища для сайта изменен!",
|
|
||||||
"Database schema reloaded!": "Схема базы данных перезагружена!",
|
|
||||||
"Database rebuilding....": "Перестройка базы данных...",
|
|
||||||
"Database rebuilt!": "База данных перестроена!",
|
|
||||||
"Site updated!": "Сайт обновлён!",
|
|
||||||
"Delete this site": "Удалить этот сайт",
|
|
||||||
"File write error: ": "Ошибка записи файла:",
|
|
||||||
"Site settings saved!": "Настройки сайта сохранены!",
|
|
||||||
"Enter your private key:": "Введите свой приватный ключ:",
|
|
||||||
" Signed!": " Подписано!",
|
|
||||||
"WebGL not supported": "WebGL не поддерживается"
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
{
|
|
||||||
"Peers": "Eşler",
|
|
||||||
"Connected": "Bağlı",
|
|
||||||
"Connectable": "Erişilebilir",
|
|
||||||
"Connectable peers": "Bağlanılabilir eşler",
|
|
||||||
|
|
||||||
"Data transfer": "Veri aktarımı",
|
|
||||||
"Received": "Alınan",
|
|
||||||
"Received bytes": "Bayt alındı",
|
|
||||||
"Sent": "Gönderilen",
|
|
||||||
"Sent bytes": "Bayt gönderildi",
|
|
||||||
|
|
||||||
"Files": "Dosyalar",
|
|
||||||
"Total": "Toplam",
|
|
||||||
"Image": "Resim",
|
|
||||||
"Other": "Diğer",
|
|
||||||
"User data": "Kullanıcı verisi",
|
|
||||||
|
|
||||||
"Size limit": "Boyut sınırı",
|
|
||||||
"limit used": "kullanılan",
|
|
||||||
"free space": "boş",
|
|
||||||
"Set": "Ayarla",
|
|
||||||
|
|
||||||
"Optional files": "İsteğe bağlı dosyalar",
|
|
||||||
"Downloaded": "İndirilen",
|
|
||||||
"Download and help distribute all files": "Tüm dosyaları indir ve yayılmalarına yardım et",
|
|
||||||
"Total size": "Toplam boyut",
|
|
||||||
"Downloaded files": "İndirilen dosyalar",
|
|
||||||
|
|
||||||
"Database": "Veritabanı",
|
|
||||||
"search feeds": "kaynak ara",
|
|
||||||
"{feeds} query": "{feeds} sorgu",
|
|
||||||
"Reload": "Yenile",
|
|
||||||
"Rebuild": "Yapılandır",
|
|
||||||
"No database found": "Veritabanı yok",
|
|
||||||
|
|
||||||
"Identity address": "Kimlik adresi",
|
|
||||||
"Change": "Değiştir",
|
|
||||||
|
|
||||||
"Site control": "Site kontrolü",
|
|
||||||
"Update": "Güncelle",
|
|
||||||
"Pause": "Duraklat",
|
|
||||||
"Resume": "Sürdür",
|
|
||||||
"Delete": "Sil",
|
|
||||||
"Are you sure?": "Emin misin?",
|
|
||||||
|
|
||||||
"Site address": "Site adresi",
|
|
||||||
"Donate": "Bağış yap",
|
|
||||||
|
|
||||||
"Missing files": "Eksik dosyalar",
|
|
||||||
"{} try": "{} deneme",
|
|
||||||
"{} tries": "{} deneme",
|
|
||||||
"+ {num_bad_files} more": "+ {num_bad_files} tane daha",
|
|
||||||
|
|
||||||
"This is my site": "Bu benim sitem",
|
|
||||||
"Site title": "Site başlığı",
|
|
||||||
"Site description": "Site açıklaması",
|
|
||||||
"Save site settings": "Site ayarlarını kaydet",
|
|
||||||
|
|
||||||
"Content publishing": "İçerik yayımlanıyor",
|
|
||||||
"Choose": "Seç",
|
|
||||||
"Sign": "İmzala",
|
|
||||||
"Publish": "Yayımla",
|
|
||||||
|
|
||||||
"This function is disabled on this proxy": "Bu özellik bu vekilde kullanılamaz",
|
|
||||||
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 Şehir veritabanı indirme hatası: {}!<br>Lütfen kendiniz indirip aşağıdaki konuma açınınız:<br>{}",
|
|
||||||
"Downloading GeoLite2 City database (one time only, ~20MB)...": "GeoLite2 Şehir veritabanı indiriliyor (sadece bir kere, ~20MB)...",
|
|
||||||
"GeoLite2 City database downloaded!": "GeoLite2 Şehir veritabanı indirildi!",
|
|
||||||
|
|
||||||
"Are you sure?": "Emin misiniz?",
|
|
||||||
"Site storage limit modified!": "Site saklama sınırı değiştirildi!",
|
|
||||||
"Database schema reloaded!": "Veritabanı şeması yeniden yüklendi!",
|
|
||||||
"Database rebuilding....": "Veritabanı yeniden inşa ediliyor...",
|
|
||||||
"Database rebuilt!": "Veritabanı yeniden inşa edildi!",
|
|
||||||
"Site updated!": "Site güncellendi!",
|
|
||||||
"Delete this site": "Bu siteyi sil",
|
|
||||||
"File write error: ": "Dosya yazma hatası: ",
|
|
||||||
"Site settings saved!": "Site ayarları kaydedildi!",
|
|
||||||
"Enter your private key:": "Özel anahtarınızı giriniz:",
|
|
||||||
" Signed!": " İmzala!",
|
|
||||||
"WebGL not supported": "WebGL desteklenmiyor"
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
{
|
|
||||||
"Peers": "節點數",
|
|
||||||
"Connected": "已連線",
|
|
||||||
"Connectable": "可連線",
|
|
||||||
"Connectable peers": "可連線節點",
|
|
||||||
|
|
||||||
"Data transfer": "數據傳輸",
|
|
||||||
"Received": "已收到",
|
|
||||||
"Received bytes": "收到字節",
|
|
||||||
"Sent": "已傳送",
|
|
||||||
"Sent bytes": "傳送字節",
|
|
||||||
|
|
||||||
"Files": "檔",
|
|
||||||
"Total": "共計",
|
|
||||||
"Image": "圖片",
|
|
||||||
"Other": "其他",
|
|
||||||
"User data": "用戶數據",
|
|
||||||
|
|
||||||
"Size limit": "大小限制",
|
|
||||||
"limit used": "限额",
|
|
||||||
"free space": "可用空間",
|
|
||||||
"Set": "設定",
|
|
||||||
|
|
||||||
"Optional files": "可選文件",
|
|
||||||
"Downloaded": "已下載",
|
|
||||||
"Download and help distribute all files": "下載並幫助分發所有文件",
|
|
||||||
"Total size": "總大小",
|
|
||||||
"Downloaded files": "下載的文件",
|
|
||||||
|
|
||||||
"Database": "資料庫",
|
|
||||||
"search feeds": "搜尋供稿",
|
|
||||||
"{feeds} query": "{feeds} 查詢 ",
|
|
||||||
"Reload": "重載",
|
|
||||||
"Rebuild": "重建",
|
|
||||||
"No database found": "未找到資料庫",
|
|
||||||
|
|
||||||
"Identity address": "身份地址",
|
|
||||||
"Change": "改變",
|
|
||||||
|
|
||||||
"Site control": "網站控制",
|
|
||||||
"Update": "更新",
|
|
||||||
"Pause": "暫停",
|
|
||||||
"Resume": "恢復",
|
|
||||||
"Delete": "刪除",
|
|
||||||
"Are you sure?": "你確定?",
|
|
||||||
|
|
||||||
"Site address": "網站地址",
|
|
||||||
"Donate": "捐贈",
|
|
||||||
|
|
||||||
"Missing files": "缺少的檔",
|
|
||||||
"{} try": "{} 嘗試",
|
|
||||||
"{} tries": "{} 已嘗試",
|
|
||||||
"+ {num_bad_files} more": "+ {num_bad_files} 更多",
|
|
||||||
|
|
||||||
"This is my site": "這是我的網站",
|
|
||||||
"Site title": "網站標題",
|
|
||||||
"Site description": "網站描寫",
|
|
||||||
"Save site settings": "存儲網站設定",
|
|
||||||
|
|
||||||
"Content publishing": "內容髮布",
|
|
||||||
"Choose": "選擇",
|
|
||||||
"Sign": "簽署",
|
|
||||||
"Publish": "發佈",
|
|
||||||
"This function is disabled on this proxy": "此代理上禁用此功能",
|
|
||||||
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 地理位置資料庫下載錯誤:{}!<br>請手動下載並解壓到數據目錄:<br>{}",
|
|
||||||
"Downloading GeoLite2 City database (one time only, ~20MB)...": "正在下載 GeoLite2 地理位置資料庫 (僅一次,約 20MB )...",
|
|
||||||
"GeoLite2 City database downloaded!": "GeoLite2 地理位置資料庫已下載!",
|
|
||||||
|
|
||||||
"Are you sure?": "你確定?",
|
|
||||||
"Site storage limit modified!": "網站存儲限制已改變!",
|
|
||||||
"Database schema reloaded!": "資料庫架構重新加載!",
|
|
||||||
"Database rebuilding....": "資料庫重建中...",
|
|
||||||
"Database rebuilt!": "資料庫已重建!",
|
|
||||||
"Site updated!": "網站已更新!",
|
|
||||||
"Delete this site": "刪除此網站",
|
|
||||||
"File write error: ": "檔寫入錯誤:",
|
|
||||||
"Site settings saved!": "網站設置已保存!",
|
|
||||||
"Enter your private key:": "輸入您的私鑰:",
|
|
||||||
" Signed!": " 已簽署!",
|
|
||||||
"WebGL not supported": "不支持 WebGL"
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
{
|
|
||||||
"Peers": "节点数",
|
|
||||||
"Connected": "已连接",
|
|
||||||
"Connectable": "可连接",
|
|
||||||
"Connectable peers": "可连接节点",
|
|
||||||
|
|
||||||
"Data transfer": "数据传输",
|
|
||||||
"Received": "已接收",
|
|
||||||
"Received bytes": "已接收字节",
|
|
||||||
"Sent": "已发送",
|
|
||||||
"Sent bytes": "已发送字节",
|
|
||||||
|
|
||||||
"Files": "文件",
|
|
||||||
"Total": "总计",
|
|
||||||
"Image": "图像",
|
|
||||||
"Other": "其他",
|
|
||||||
"User data": "用户数据",
|
|
||||||
|
|
||||||
"Size limit": "大小限制",
|
|
||||||
"limit used": "限额",
|
|
||||||
"free space": "剩余空间",
|
|
||||||
"Set": "设置",
|
|
||||||
|
|
||||||
"Optional files": "可选文件",
|
|
||||||
"Downloaded": "已下载",
|
|
||||||
"Download and help distribute all files": "下载并帮助分发所有文件",
|
|
||||||
"Total size": "总大小",
|
|
||||||
"Downloaded files": "已下载文件",
|
|
||||||
|
|
||||||
"Database": "数据库",
|
|
||||||
"search feeds": "搜索数据源",
|
|
||||||
"{feeds} query": "{feeds} 请求",
|
|
||||||
"Reload": "重载",
|
|
||||||
"Rebuild": "重建",
|
|
||||||
"No database found": "没有找到数据库",
|
|
||||||
|
|
||||||
"Identity address": "身份地址",
|
|
||||||
"Change": "更改",
|
|
||||||
|
|
||||||
"Site control": "站点控制",
|
|
||||||
"Update": "更新",
|
|
||||||
"Pause": "暂停",
|
|
||||||
"Resume": "恢复",
|
|
||||||
"Delete": "删除",
|
|
||||||
"Are you sure?": "你确定吗?",
|
|
||||||
|
|
||||||
"Site address": "站点地址",
|
|
||||||
"Donate": "捐赠",
|
|
||||||
|
|
||||||
"Missing files": "丢失的文件",
|
|
||||||
"{} try": "{} 尝试",
|
|
||||||
"{} tries": "{} 已尝试",
|
|
||||||
"+ {num_bad_files} more": "+ {num_bad_files} 更多",
|
|
||||||
|
|
||||||
"This is my site": "这是我的站点",
|
|
||||||
"Site title": "站点标题",
|
|
||||||
"Site description": "站点描述",
|
|
||||||
"Save site settings": "保存站点设置",
|
|
||||||
|
|
||||||
"Content publishing": "内容发布",
|
|
||||||
"Choose": "选择",
|
|
||||||
"Sign": "签名",
|
|
||||||
"Publish": "发布",
|
|
||||||
"This function is disabled on this proxy": "此功能在代理上被禁用",
|
|
||||||
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 地理位置数据库下载错误:{}!<br>请手动下载并解压在数据目录:<br>{}",
|
|
||||||
"Downloading GeoLite2 City database (one time only, ~20MB)...": "正在下载 GeoLite2 地理位置数据库 (仅需一次,约 20MB )...",
|
|
||||||
"GeoLite2 City database downloaded!": "GeoLite2 地理位置数据库已下载!",
|
|
||||||
|
|
||||||
"Are you sure?": "你确定吗?",
|
|
||||||
"Site storage limit modified!": "站点存储限制已更改!",
|
|
||||||
"Database schema reloaded!": "数据库模式已重新加载!",
|
|
||||||
"Database rebuilding....": "数据库重建中...",
|
|
||||||
"Database rebuilt!": "数据库已重建!",
|
|
||||||
"Site updated!": "站点已更新!",
|
|
||||||
"Delete this site": "删除这个站点",
|
|
||||||
"File write error: ": "文件写入错误:",
|
|
||||||
"Site settings saved!": "站点设置已保存!",
|
|
||||||
"Enter your private key:": "输入你的私钥:",
|
|
||||||
" Signed!": " 已签名!",
|
|
||||||
"WebGL not supported": "不支持 WebGL"
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
# pylint:disable=C0111
|
|
||||||
import os
|
|
||||||
|
|
||||||
import maxminddb.reader
|
|
||||||
|
|
||||||
try:
|
|
||||||
import maxminddb.extension
|
|
||||||
except ImportError:
|
|
||||||
maxminddb.extension = None
|
|
||||||
|
|
||||||
from maxminddb.const import (MODE_AUTO, MODE_MMAP, MODE_MMAP_EXT, MODE_FILE,
|
|
||||||
MODE_MEMORY)
|
|
||||||
from maxminddb.decoder import InvalidDatabaseError
|
|
||||||
|
|
||||||
|
|
||||||
def open_database(database, mode=MODE_AUTO):
|
|
||||||
"""Open a Maxmind DB database
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
database -- A path to a valid MaxMind DB file such as a GeoIP2
|
|
||||||
database file.
|
|
||||||
mode -- mode to open the database with. Valid mode are:
|
|
||||||
* MODE_MMAP_EXT - use the C extension with memory map.
|
|
||||||
* MODE_MMAP - read from memory map. Pure Python.
|
|
||||||
* MODE_FILE - read database as standard file. Pure Python.
|
|
||||||
* MODE_MEMORY - load database into memory. Pure Python.
|
|
||||||
* MODE_AUTO - tries MODE_MMAP_EXT, MODE_MMAP, MODE_FILE in that
|
|
||||||
order. Default mode.
|
|
||||||
"""
|
|
||||||
if (mode == MODE_AUTO and maxminddb.extension and
|
|
||||||
hasattr(maxminddb.extension, 'Reader')) or mode == MODE_MMAP_EXT:
|
|
||||||
return maxminddb.extension.Reader(database)
|
|
||||||
elif mode in (MODE_AUTO, MODE_MMAP, MODE_FILE, MODE_MEMORY):
|
|
||||||
return maxminddb.reader.Reader(database, mode)
|
|
||||||
raise ValueError('Unsupported open mode: {0}'.format(mode))
|
|
||||||
|
|
||||||
|
|
||||||
def Reader(database): # pylint: disable=invalid-name
|
|
||||||
"""This exists for backwards compatibility. Use open_database instead"""
|
|
||||||
return open_database(database)
|
|
||||||
|
|
||||||
__title__ = 'maxminddb'
|
|
||||||
__version__ = '1.2.0'
|
|
||||||
__author__ = 'Gregory Oschwald'
|
|
||||||
__license__ = 'Apache License, Version 2.0'
|
|
||||||
__copyright__ = 'Copyright 2014 Maxmind, Inc.'
|
|
|
@ -1,28 +0,0 @@
|
||||||
import sys
|
|
||||||
|
|
||||||
# pylint: skip-file
|
|
||||||
|
|
||||||
if sys.version_info[0] == 2:
|
|
||||||
import ipaddr as ipaddress # pylint:disable=F0401
|
|
||||||
ipaddress.ip_address = ipaddress.IPAddress
|
|
||||||
|
|
||||||
int_from_byte = ord
|
|
||||||
|
|
||||||
FileNotFoundError = IOError
|
|
||||||
|
|
||||||
def int_from_bytes(b):
|
|
||||||
if b:
|
|
||||||
return int(b.encode("hex"), 16)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
byte_from_int = chr
|
|
||||||
else:
|
|
||||||
import ipaddress # pylint:disable=F0401
|
|
||||||
|
|
||||||
int_from_byte = lambda x: x
|
|
||||||
|
|
||||||
FileNotFoundError = FileNotFoundError
|
|
||||||
|
|
||||||
int_from_bytes = lambda x: int.from_bytes(x, 'big')
|
|
||||||
|
|
||||||
byte_from_int = lambda x: bytes([x])
|
|
|
@ -1,7 +0,0 @@
|
||||||
"""Constants used in the API"""
|
|
||||||
|
|
||||||
MODE_AUTO = 0
|
|
||||||
MODE_MMAP_EXT = 1
|
|
||||||
MODE_MMAP = 2
|
|
||||||
MODE_FILE = 4
|
|
||||||
MODE_MEMORY = 8
|
|
|
@ -1,173 +0,0 @@
|
||||||
"""
|
|
||||||
maxminddb.decoder
|
|
||||||
~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This package contains code for decoding the MaxMind DB data section.
|
|
||||||
|
|
||||||
"""
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import struct
|
|
||||||
|
|
||||||
from maxminddb.compat import byte_from_int, int_from_bytes
|
|
||||||
from maxminddb.errors import InvalidDatabaseError
|
|
||||||
|
|
||||||
|
|
||||||
class Decoder(object): # pylint: disable=too-few-public-methods
|
|
||||||
|
|
||||||
"""Decoder for the data section of the MaxMind DB"""
|
|
||||||
|
|
||||||
def __init__(self, database_buffer, pointer_base=0, pointer_test=False):
|
|
||||||
"""Created a Decoder for a MaxMind DB
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
database_buffer -- an mmap'd MaxMind DB file.
|
|
||||||
pointer_base -- the base number to use when decoding a pointer
|
|
||||||
pointer_test -- used for internal unit testing of pointer code
|
|
||||||
"""
|
|
||||||
self._pointer_test = pointer_test
|
|
||||||
self._buffer = database_buffer
|
|
||||||
self._pointer_base = pointer_base
|
|
||||||
|
|
||||||
def _decode_array(self, size, offset):
|
|
||||||
array = []
|
|
||||||
for _ in range(size):
|
|
||||||
(value, offset) = self.decode(offset)
|
|
||||||
array.append(value)
|
|
||||||
return array, offset
|
|
||||||
|
|
||||||
def _decode_boolean(self, size, offset):
|
|
||||||
return size != 0, offset
|
|
||||||
|
|
||||||
def _decode_bytes(self, size, offset):
|
|
||||||
new_offset = offset + size
|
|
||||||
return self._buffer[offset:new_offset], new_offset
|
|
||||||
|
|
||||||
# pylint: disable=no-self-argument
|
|
||||||
# |-> I am open to better ways of doing this as long as it doesn't involve
|
|
||||||
# lots of code duplication.
|
|
||||||
def _decode_packed_type(type_code, type_size, pad=False):
|
|
||||||
# pylint: disable=protected-access, missing-docstring
|
|
||||||
def unpack_type(self, size, offset):
|
|
||||||
if not pad:
|
|
||||||
self._verify_size(size, type_size)
|
|
||||||
new_offset = offset + type_size
|
|
||||||
packed_bytes = self._buffer[offset:new_offset]
|
|
||||||
if pad:
|
|
||||||
packed_bytes = packed_bytes.rjust(type_size, b'\x00')
|
|
||||||
(value,) = struct.unpack(type_code, packed_bytes)
|
|
||||||
return value, new_offset
|
|
||||||
return unpack_type
|
|
||||||
|
|
||||||
def _decode_map(self, size, offset):
|
|
||||||
container = {}
|
|
||||||
for _ in range(size):
|
|
||||||
(key, offset) = self.decode(offset)
|
|
||||||
(value, offset) = self.decode(offset)
|
|
||||||
container[key] = value
|
|
||||||
return container, offset
|
|
||||||
|
|
||||||
_pointer_value_offset = {
|
|
||||||
1: 0,
|
|
||||||
2: 2048,
|
|
||||||
3: 526336,
|
|
||||||
4: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _decode_pointer(self, size, offset):
|
|
||||||
pointer_size = ((size >> 3) & 0x3) + 1
|
|
||||||
new_offset = offset + pointer_size
|
|
||||||
pointer_bytes = self._buffer[offset:new_offset]
|
|
||||||
packed = pointer_bytes if pointer_size == 4 else struct.pack(
|
|
||||||
b'!c', byte_from_int(size & 0x7)) + pointer_bytes
|
|
||||||
unpacked = int_from_bytes(packed)
|
|
||||||
pointer = unpacked + self._pointer_base + \
|
|
||||||
self._pointer_value_offset[pointer_size]
|
|
||||||
if self._pointer_test:
|
|
||||||
return pointer, new_offset
|
|
||||||
(value, _) = self.decode(pointer)
|
|
||||||
return value, new_offset
|
|
||||||
|
|
||||||
def _decode_uint(self, size, offset):
|
|
||||||
new_offset = offset + size
|
|
||||||
uint_bytes = self._buffer[offset:new_offset]
|
|
||||||
return int_from_bytes(uint_bytes), new_offset
|
|
||||||
|
|
||||||
def _decode_utf8_string(self, size, offset):
|
|
||||||
new_offset = offset + size
|
|
||||||
return self._buffer[offset:new_offset].decode('utf-8'), new_offset
|
|
||||||
|
|
||||||
_type_decoder = {
|
|
||||||
1: _decode_pointer,
|
|
||||||
2: _decode_utf8_string,
|
|
||||||
3: _decode_packed_type(b'!d', 8), # double,
|
|
||||||
4: _decode_bytes,
|
|
||||||
5: _decode_uint, # uint16
|
|
||||||
6: _decode_uint, # uint32
|
|
||||||
7: _decode_map,
|
|
||||||
8: _decode_packed_type(b'!i', 4, pad=True), # int32
|
|
||||||
9: _decode_uint, # uint64
|
|
||||||
10: _decode_uint, # uint128
|
|
||||||
11: _decode_array,
|
|
||||||
14: _decode_boolean,
|
|
||||||
15: _decode_packed_type(b'!f', 4), # float,
|
|
||||||
}
|
|
||||||
|
|
||||||
def decode(self, offset):
|
|
||||||
"""Decode a section of the data section starting at offset
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
offset -- the location of the data structure to decode
|
|
||||||
"""
|
|
||||||
new_offset = offset + 1
|
|
||||||
(ctrl_byte,) = struct.unpack(b'!B', self._buffer[offset:new_offset])
|
|
||||||
type_num = ctrl_byte >> 5
|
|
||||||
# Extended type
|
|
||||||
if not type_num:
|
|
||||||
(type_num, new_offset) = self._read_extended(new_offset)
|
|
||||||
|
|
||||||
if not type_num in self._type_decoder:
|
|
||||||
raise InvalidDatabaseError('Unexpected type number ({type}) '
|
|
||||||
'encountered'.format(type=type_num))
|
|
||||||
|
|
||||||
(size, new_offset) = self._size_from_ctrl_byte(
|
|
||||||
ctrl_byte, new_offset, type_num)
|
|
||||||
return self._type_decoder[type_num](self, size, new_offset)
|
|
||||||
|
|
||||||
def _read_extended(self, offset):
|
|
||||||
(next_byte,) = struct.unpack(b'!B', self._buffer[offset:offset + 1])
|
|
||||||
type_num = next_byte + 7
|
|
||||||
if type_num < 7:
|
|
||||||
raise InvalidDatabaseError(
|
|
||||||
'Something went horribly wrong in the decoder. An '
|
|
||||||
'extended type resolved to a type number < 8 '
|
|
||||||
'({type})'.format(type=type_num))
|
|
||||||
return type_num, offset + 1
|
|
||||||
|
|
||||||
def _verify_size(self, expected, actual):
|
|
||||||
if expected != actual:
|
|
||||||
raise InvalidDatabaseError(
|
|
||||||
'The MaxMind DB file\'s data section contains bad data '
|
|
||||||
'(unknown data type or corrupt data)'
|
|
||||||
)
|
|
||||||
|
|
||||||
def _size_from_ctrl_byte(self, ctrl_byte, offset, type_num):
|
|
||||||
size = ctrl_byte & 0x1f
|
|
||||||
if type_num == 1:
|
|
||||||
return size, offset
|
|
||||||
bytes_to_read = 0 if size < 29 else size - 28
|
|
||||||
|
|
||||||
new_offset = offset + bytes_to_read
|
|
||||||
size_bytes = self._buffer[offset:new_offset]
|
|
||||||
|
|
||||||
# Using unpack rather than int_from_bytes as it is about 200 lookups
|
|
||||||
# per second faster here.
|
|
||||||
if size == 29:
|
|
||||||
size = 29 + struct.unpack(b'!B', size_bytes)[0]
|
|
||||||
elif size == 30:
|
|
||||||
size = 285 + struct.unpack(b'!H', size_bytes)[0]
|
|
||||||
elif size > 30:
|
|
||||||
size = struct.unpack(
|
|
||||||
b'!I', size_bytes.rjust(4, b'\x00'))[0] + 65821
|
|
||||||
|
|
||||||
return size, new_offset
|
|
|
@ -1,11 +0,0 @@
|
||||||
"""
|
|
||||||
maxminddb.errors
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module contains custom errors for the MaxMind DB reader
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidDatabaseError(RuntimeError):
|
|
||||||
|
|
||||||
"""This error is thrown when unexpected data is found in the database."""
|
|
|
@ -1,570 +0,0 @@
|
||||||
#include <Python.h>
|
|
||||||
#include <maxminddb.h>
|
|
||||||
#include "structmember.h"
|
|
||||||
|
|
||||||
#define __STDC_FORMAT_MACROS
|
|
||||||
#include <inttypes.h>
|
|
||||||
|
|
||||||
static PyTypeObject Reader_Type;
|
|
||||||
static PyTypeObject Metadata_Type;
|
|
||||||
static PyObject *MaxMindDB_error;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
PyObject_HEAD /* no semicolon */
|
|
||||||
MMDB_s *mmdb;
|
|
||||||
} Reader_obj;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
PyObject_HEAD /* no semicolon */
|
|
||||||
PyObject *binary_format_major_version;
|
|
||||||
PyObject *binary_format_minor_version;
|
|
||||||
PyObject *build_epoch;
|
|
||||||
PyObject *database_type;
|
|
||||||
PyObject *description;
|
|
||||||
PyObject *ip_version;
|
|
||||||
PyObject *languages;
|
|
||||||
PyObject *node_count;
|
|
||||||
PyObject *record_size;
|
|
||||||
} Metadata_obj;
|
|
||||||
|
|
||||||
static PyObject *from_entry_data_list(MMDB_entry_data_list_s **entry_data_list);
|
|
||||||
static PyObject *from_map(MMDB_entry_data_list_s **entry_data_list);
|
|
||||||
static PyObject *from_array(MMDB_entry_data_list_s **entry_data_list);
|
|
||||||
static PyObject *from_uint128(const MMDB_entry_data_list_s *entry_data_list);
|
|
||||||
|
|
||||||
#if PY_MAJOR_VERSION >= 3
|
|
||||||
#define MOD_INIT(name) PyMODINIT_FUNC PyInit_ ## name(void)
|
|
||||||
#define RETURN_MOD_INIT(m) return (m)
|
|
||||||
#define FILE_NOT_FOUND_ERROR PyExc_FileNotFoundError
|
|
||||||
#else
|
|
||||||
#define MOD_INIT(name) PyMODINIT_FUNC init ## name(void)
|
|
||||||
#define RETURN_MOD_INIT(m) return
|
|
||||||
#define PyInt_FromLong PyLong_FromLong
|
|
||||||
#define FILE_NOT_FOUND_ERROR PyExc_IOError
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef __GNUC__
|
|
||||||
# define UNUSED(x) UNUSED_ ## x __attribute__((__unused__))
|
|
||||||
#else
|
|
||||||
# define UNUSED(x) UNUSED_ ## x
|
|
||||||
#endif
|
|
||||||
|
|
||||||
static int Reader_init(PyObject *self, PyObject *args, PyObject *kwds)
|
|
||||||
{
|
|
||||||
char *filename;
|
|
||||||
int mode = 0;
|
|
||||||
|
|
||||||
static char *kwlist[] = {"database", "mode", NULL};
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|i", kwlist, &filename, &mode)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode != 0 && mode != 1) {
|
|
||||||
PyErr_Format(PyExc_ValueError, "Unsupported open mode (%i). Only "
|
|
||||||
"MODE_AUTO and MODE_MMAP_EXT are supported by this extension.",
|
|
||||||
mode);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (0 != access(filename, R_OK)) {
|
|
||||||
PyErr_Format(FILE_NOT_FOUND_ERROR,
|
|
||||||
"No such file or directory: '%s'",
|
|
||||||
filename);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
MMDB_s *mmdb = (MMDB_s *)malloc(sizeof(MMDB_s));
|
|
||||||
if (NULL == mmdb) {
|
|
||||||
PyErr_NoMemory();
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Reader_obj *mmdb_obj = (Reader_obj *)self;
|
|
||||||
if (!mmdb_obj) {
|
|
||||||
free(mmdb);
|
|
||||||
PyErr_NoMemory();
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t status = MMDB_open(filename, MMDB_MODE_MMAP, mmdb);
|
|
||||||
|
|
||||||
if (MMDB_SUCCESS != status) {
|
|
||||||
free(mmdb);
|
|
||||||
PyErr_Format(
|
|
||||||
MaxMindDB_error,
|
|
||||||
"Error opening database file (%s). Is this a valid MaxMind DB file?",
|
|
||||||
filename
|
|
||||||
);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
mmdb_obj->mmdb = mmdb;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static PyObject *Reader_get(PyObject *self, PyObject *args)
|
|
||||||
{
|
|
||||||
char *ip_address = NULL;
|
|
||||||
|
|
||||||
Reader_obj *mmdb_obj = (Reader_obj *)self;
|
|
||||||
if (!PyArg_ParseTuple(args, "s", &ip_address)) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
MMDB_s *mmdb = mmdb_obj->mmdb;
|
|
||||||
|
|
||||||
if (NULL == mmdb) {
|
|
||||||
PyErr_SetString(PyExc_ValueError,
|
|
||||||
"Attempt to read from a closed MaxMind DB.");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
int gai_error = 0;
|
|
||||||
int mmdb_error = MMDB_SUCCESS;
|
|
||||||
MMDB_lookup_result_s result =
|
|
||||||
MMDB_lookup_string(mmdb, ip_address, &gai_error,
|
|
||||||
&mmdb_error);
|
|
||||||
|
|
||||||
if (0 != gai_error) {
|
|
||||||
PyErr_Format(PyExc_ValueError,
|
|
||||||
"'%s' does not appear to be an IPv4 or IPv6 address.",
|
|
||||||
ip_address);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MMDB_SUCCESS != mmdb_error) {
|
|
||||||
PyObject *exception;
|
|
||||||
if (MMDB_IPV6_LOOKUP_IN_IPV4_DATABASE_ERROR == mmdb_error) {
|
|
||||||
exception = PyExc_ValueError;
|
|
||||||
} else {
|
|
||||||
exception = MaxMindDB_error;
|
|
||||||
}
|
|
||||||
PyErr_Format(exception, "Error looking up %s. %s",
|
|
||||||
ip_address, MMDB_strerror(mmdb_error));
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.found_entry) {
|
|
||||||
Py_RETURN_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
MMDB_entry_data_list_s *entry_data_list = NULL;
|
|
||||||
int status = MMDB_get_entry_data_list(&result.entry, &entry_data_list);
|
|
||||||
if (MMDB_SUCCESS != status) {
|
|
||||||
PyErr_Format(MaxMindDB_error,
|
|
||||||
"Error while looking up data for %s. %s",
|
|
||||||
ip_address, MMDB_strerror(status));
|
|
||||||
MMDB_free_entry_data_list(entry_data_list);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
MMDB_entry_data_list_s *original_entry_data_list = entry_data_list;
|
|
||||||
PyObject *py_obj = from_entry_data_list(&entry_data_list);
|
|
||||||
MMDB_free_entry_data_list(original_entry_data_list);
|
|
||||||
return py_obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
static PyObject *Reader_metadata(PyObject *self, PyObject *UNUSED(args))
|
|
||||||
{
|
|
||||||
Reader_obj *mmdb_obj = (Reader_obj *)self;
|
|
||||||
|
|
||||||
if (NULL == mmdb_obj->mmdb) {
|
|
||||||
PyErr_SetString(PyExc_IOError,
|
|
||||||
"Attempt to read from a closed MaxMind DB.");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
MMDB_entry_data_list_s *entry_data_list;
|
|
||||||
MMDB_get_metadata_as_entry_data_list(mmdb_obj->mmdb, &entry_data_list);
|
|
||||||
MMDB_entry_data_list_s *original_entry_data_list = entry_data_list;
|
|
||||||
|
|
||||||
PyObject *metadata_dict = from_entry_data_list(&entry_data_list);
|
|
||||||
MMDB_free_entry_data_list(original_entry_data_list);
|
|
||||||
if (NULL == metadata_dict || !PyDict_Check(metadata_dict)) {
|
|
||||||
PyErr_SetString(MaxMindDB_error,
|
|
||||||
"Error decoding metadata.");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
PyObject *args = PyTuple_New(0);
|
|
||||||
if (NULL == args) {
|
|
||||||
Py_DECREF(metadata_dict);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
PyObject *metadata = PyObject_Call((PyObject *)&Metadata_Type, args,
|
|
||||||
metadata_dict);
|
|
||||||
|
|
||||||
Py_DECREF(metadata_dict);
|
|
||||||
return metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
static PyObject *Reader_close(PyObject *self, PyObject *UNUSED(args))
|
|
||||||
{
|
|
||||||
Reader_obj *mmdb_obj = (Reader_obj *)self;
|
|
||||||
|
|
||||||
if (NULL != mmdb_obj->mmdb) {
|
|
||||||
MMDB_close(mmdb_obj->mmdb);
|
|
||||||
free(mmdb_obj->mmdb);
|
|
||||||
mmdb_obj->mmdb = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
Py_RETURN_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void Reader_dealloc(PyObject *self)
|
|
||||||
{
|
|
||||||
Reader_obj *obj = (Reader_obj *)self;
|
|
||||||
if (NULL != obj->mmdb) {
|
|
||||||
Reader_close(self, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
PyObject_Del(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int Metadata_init(PyObject *self, PyObject *args, PyObject *kwds)
|
|
||||||
{
|
|
||||||
|
|
||||||
PyObject
|
|
||||||
*binary_format_major_version,
|
|
||||||
*binary_format_minor_version,
|
|
||||||
*build_epoch,
|
|
||||||
*database_type,
|
|
||||||
*description,
|
|
||||||
*ip_version,
|
|
||||||
*languages,
|
|
||||||
*node_count,
|
|
||||||
*record_size;
|
|
||||||
|
|
||||||
static char *kwlist[] = {
|
|
||||||
"binary_format_major_version",
|
|
||||||
"binary_format_minor_version",
|
|
||||||
"build_epoch",
|
|
||||||
"database_type",
|
|
||||||
"description",
|
|
||||||
"ip_version",
|
|
||||||
"languages",
|
|
||||||
"node_count",
|
|
||||||
"record_size",
|
|
||||||
NULL
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOOOO", kwlist,
|
|
||||||
&binary_format_major_version,
|
|
||||||
&binary_format_minor_version,
|
|
||||||
&build_epoch,
|
|
||||||
&database_type,
|
|
||||||
&description,
|
|
||||||
&ip_version,
|
|
||||||
&languages,
|
|
||||||
&node_count,
|
|
||||||
&record_size)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Metadata_obj *obj = (Metadata_obj *)self;
|
|
||||||
|
|
||||||
obj->binary_format_major_version = binary_format_major_version;
|
|
||||||
obj->binary_format_minor_version = binary_format_minor_version;
|
|
||||||
obj->build_epoch = build_epoch;
|
|
||||||
obj->database_type = database_type;
|
|
||||||
obj->description = description;
|
|
||||||
obj->ip_version = ip_version;
|
|
||||||
obj->languages = languages;
|
|
||||||
obj->node_count = node_count;
|
|
||||||
obj->record_size = record_size;
|
|
||||||
|
|
||||||
Py_INCREF(obj->binary_format_major_version);
|
|
||||||
Py_INCREF(obj->binary_format_minor_version);
|
|
||||||
Py_INCREF(obj->build_epoch);
|
|
||||||
Py_INCREF(obj->database_type);
|
|
||||||
Py_INCREF(obj->description);
|
|
||||||
Py_INCREF(obj->ip_version);
|
|
||||||
Py_INCREF(obj->languages);
|
|
||||||
Py_INCREF(obj->node_count);
|
|
||||||
Py_INCREF(obj->record_size);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void Metadata_dealloc(PyObject *self)
|
|
||||||
{
|
|
||||||
Metadata_obj *obj = (Metadata_obj *)self;
|
|
||||||
Py_DECREF(obj->binary_format_major_version);
|
|
||||||
Py_DECREF(obj->binary_format_minor_version);
|
|
||||||
Py_DECREF(obj->build_epoch);
|
|
||||||
Py_DECREF(obj->database_type);
|
|
||||||
Py_DECREF(obj->description);
|
|
||||||
Py_DECREF(obj->ip_version);
|
|
||||||
Py_DECREF(obj->languages);
|
|
||||||
Py_DECREF(obj->node_count);
|
|
||||||
Py_DECREF(obj->record_size);
|
|
||||||
PyObject_Del(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
static PyObject *from_entry_data_list(MMDB_entry_data_list_s **entry_data_list)
|
|
||||||
{
|
|
||||||
if (NULL == entry_data_list || NULL == *entry_data_list) {
|
|
||||||
PyErr_SetString(
|
|
||||||
MaxMindDB_error,
|
|
||||||
"Error while looking up data. Your database may be corrupt or you have found a bug in libmaxminddb."
|
|
||||||
);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ((*entry_data_list)->entry_data.type) {
|
|
||||||
case MMDB_DATA_TYPE_MAP:
|
|
||||||
return from_map(entry_data_list);
|
|
||||||
case MMDB_DATA_TYPE_ARRAY:
|
|
||||||
return from_array(entry_data_list);
|
|
||||||
case MMDB_DATA_TYPE_UTF8_STRING:
|
|
||||||
return PyUnicode_FromStringAndSize(
|
|
||||||
(*entry_data_list)->entry_data.utf8_string,
|
|
||||||
(*entry_data_list)->entry_data.data_size
|
|
||||||
);
|
|
||||||
case MMDB_DATA_TYPE_BYTES:
|
|
||||||
return PyByteArray_FromStringAndSize(
|
|
||||||
(const char *)(*entry_data_list)->entry_data.bytes,
|
|
||||||
(Py_ssize_t)(*entry_data_list)->entry_data.data_size);
|
|
||||||
case MMDB_DATA_TYPE_DOUBLE:
|
|
||||||
return PyFloat_FromDouble((*entry_data_list)->entry_data.double_value);
|
|
||||||
case MMDB_DATA_TYPE_FLOAT:
|
|
||||||
return PyFloat_FromDouble((*entry_data_list)->entry_data.float_value);
|
|
||||||
case MMDB_DATA_TYPE_UINT16:
|
|
||||||
return PyLong_FromLong( (*entry_data_list)->entry_data.uint16);
|
|
||||||
case MMDB_DATA_TYPE_UINT32:
|
|
||||||
return PyLong_FromLong((*entry_data_list)->entry_data.uint32);
|
|
||||||
case MMDB_DATA_TYPE_BOOLEAN:
|
|
||||||
return PyBool_FromLong((*entry_data_list)->entry_data.boolean);
|
|
||||||
case MMDB_DATA_TYPE_UINT64:
|
|
||||||
return PyLong_FromUnsignedLongLong(
|
|
||||||
(*entry_data_list)->entry_data.uint64);
|
|
||||||
case MMDB_DATA_TYPE_UINT128:
|
|
||||||
return from_uint128(*entry_data_list);
|
|
||||||
case MMDB_DATA_TYPE_INT32:
|
|
||||||
return PyLong_FromLong((*entry_data_list)->entry_data.int32);
|
|
||||||
default:
|
|
||||||
PyErr_Format(MaxMindDB_error,
|
|
||||||
"Invalid data type arguments: %d",
|
|
||||||
(*entry_data_list)->entry_data.type);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
static PyObject *from_map(MMDB_entry_data_list_s **entry_data_list)
|
|
||||||
{
|
|
||||||
PyObject *py_obj = PyDict_New();
|
|
||||||
if (NULL == py_obj) {
|
|
||||||
PyErr_NoMemory();
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint32_t map_size = (*entry_data_list)->entry_data.data_size;
|
|
||||||
|
|
||||||
uint i;
|
|
||||||
// entry_data_list cannot start out NULL (see from_entry_data_list). We
|
|
||||||
// check it in the loop because it may become NULL.
|
|
||||||
// coverity[check_after_deref]
|
|
||||||
for (i = 0; i < map_size && entry_data_list; i++) {
|
|
||||||
*entry_data_list = (*entry_data_list)->next;
|
|
||||||
|
|
||||||
PyObject *key = PyUnicode_FromStringAndSize(
|
|
||||||
(char *)(*entry_data_list)->entry_data.utf8_string,
|
|
||||||
(*entry_data_list)->entry_data.data_size
|
|
||||||
);
|
|
||||||
|
|
||||||
*entry_data_list = (*entry_data_list)->next;
|
|
||||||
|
|
||||||
PyObject *value = from_entry_data_list(entry_data_list);
|
|
||||||
if (NULL == value) {
|
|
||||||
Py_DECREF(key);
|
|
||||||
Py_DECREF(py_obj);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
PyDict_SetItem(py_obj, key, value);
|
|
||||||
Py_DECREF(value);
|
|
||||||
Py_DECREF(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return py_obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
static PyObject *from_array(MMDB_entry_data_list_s **entry_data_list)
|
|
||||||
{
|
|
||||||
const uint32_t size = (*entry_data_list)->entry_data.data_size;
|
|
||||||
|
|
||||||
PyObject *py_obj = PyList_New(size);
|
|
||||||
if (NULL == py_obj) {
|
|
||||||
PyErr_NoMemory();
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint i;
|
|
||||||
// entry_data_list cannot start out NULL (see from_entry_data_list). We
|
|
||||||
// check it in the loop because it may become NULL.
|
|
||||||
// coverity[check_after_deref]
|
|
||||||
for (i = 0; i < size && entry_data_list; i++) {
|
|
||||||
*entry_data_list = (*entry_data_list)->next;
|
|
||||||
PyObject *value = from_entry_data_list(entry_data_list);
|
|
||||||
if (NULL == value) {
|
|
||||||
Py_DECREF(py_obj);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
// PyList_SetItem 'steals' the reference
|
|
||||||
PyList_SetItem(py_obj, i, value);
|
|
||||||
}
|
|
||||||
return py_obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
static PyObject *from_uint128(const MMDB_entry_data_list_s *entry_data_list)
|
|
||||||
{
|
|
||||||
uint64_t high = 0;
|
|
||||||
uint64_t low = 0;
|
|
||||||
#if MMDB_UINT128_IS_BYTE_ARRAY
|
|
||||||
int i;
|
|
||||||
for (i = 0; i < 8; i++) {
|
|
||||||
high = (high << 8) | entry_data_list->entry_data.uint128[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i = 8; i < 16; i++) {
|
|
||||||
low = (low << 8) | entry_data_list->entry_data.uint128[i];
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
high = entry_data_list->entry_data.uint128 >> 64;
|
|
||||||
low = (uint64_t)entry_data_list->entry_data.uint128;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
char *num_str = malloc(33);
|
|
||||||
if (NULL == num_str) {
|
|
||||||
PyErr_NoMemory();
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
snprintf(num_str, 33, "%016" PRIX64 "%016" PRIX64, high, low);
|
|
||||||
|
|
||||||
PyObject *py_obj = PyLong_FromString(num_str, NULL, 16);
|
|
||||||
|
|
||||||
free(num_str);
|
|
||||||
return py_obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
static PyMethodDef Reader_methods[] = {
|
|
||||||
{ "get", Reader_get, METH_VARARGS,
|
|
||||||
"Get record for IP address" },
|
|
||||||
{ "metadata", Reader_metadata, METH_NOARGS,
|
|
||||||
"Returns metadata object for database" },
|
|
||||||
{ "close", Reader_close, METH_NOARGS, "Closes database"},
|
|
||||||
{ NULL, NULL, 0, NULL }
|
|
||||||
};
|
|
||||||
|
|
||||||
static PyTypeObject Reader_Type = {
|
|
||||||
PyVarObject_HEAD_INIT(NULL, 0)
|
|
||||||
.tp_basicsize = sizeof(Reader_obj),
|
|
||||||
.tp_dealloc = Reader_dealloc,
|
|
||||||
.tp_doc = "Reader object",
|
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
|
||||||
.tp_methods = Reader_methods,
|
|
||||||
.tp_name = "Reader",
|
|
||||||
.tp_init = Reader_init,
|
|
||||||
};
|
|
||||||
|
|
||||||
static PyMethodDef Metadata_methods[] = {
|
|
||||||
{ NULL, NULL, 0, NULL }
|
|
||||||
};
|
|
||||||
|
|
||||||
/* *INDENT-OFF* */
|
|
||||||
static PyMemberDef Metadata_members[] = {
|
|
||||||
{ "binary_format_major_version", T_OBJECT, offsetof(
|
|
||||||
Metadata_obj, binary_format_major_version), READONLY, NULL },
|
|
||||||
{ "binary_format_minor_version", T_OBJECT, offsetof(
|
|
||||||
Metadata_obj, binary_format_minor_version), READONLY, NULL },
|
|
||||||
{ "build_epoch", T_OBJECT, offsetof(Metadata_obj, build_epoch),
|
|
||||||
READONLY, NULL },
|
|
||||||
{ "database_type", T_OBJECT, offsetof(Metadata_obj, database_type),
|
|
||||||
READONLY, NULL },
|
|
||||||
{ "description", T_OBJECT, offsetof(Metadata_obj, description),
|
|
||||||
READONLY, NULL },
|
|
||||||
{ "ip_version", T_OBJECT, offsetof(Metadata_obj, ip_version),
|
|
||||||
READONLY, NULL },
|
|
||||||
{ "languages", T_OBJECT, offsetof(Metadata_obj, languages), READONLY,
|
|
||||||
NULL },
|
|
||||||
{ "node_count", T_OBJECT, offsetof(Metadata_obj, node_count),
|
|
||||||
READONLY, NULL },
|
|
||||||
{ "record_size", T_OBJECT, offsetof(Metadata_obj, record_size),
|
|
||||||
READONLY, NULL },
|
|
||||||
{ NULL, 0, 0, 0, NULL }
|
|
||||||
};
|
|
||||||
/* *INDENT-ON* */
|
|
||||||
|
|
||||||
static PyTypeObject Metadata_Type = {
|
|
||||||
PyVarObject_HEAD_INIT(NULL, 0)
|
|
||||||
.tp_basicsize = sizeof(Metadata_obj),
|
|
||||||
.tp_dealloc = Metadata_dealloc,
|
|
||||||
.tp_doc = "Metadata object",
|
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
|
||||||
.tp_members = Metadata_members,
|
|
||||||
.tp_methods = Metadata_methods,
|
|
||||||
.tp_name = "Metadata",
|
|
||||||
.tp_init = Metadata_init
|
|
||||||
};
|
|
||||||
|
|
||||||
static PyMethodDef MaxMindDB_methods[] = {
|
|
||||||
{ NULL, NULL, 0, NULL }
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
#if PY_MAJOR_VERSION >= 3
|
|
||||||
static struct PyModuleDef MaxMindDB_module = {
|
|
||||||
PyModuleDef_HEAD_INIT,
|
|
||||||
.m_name = "extension",
|
|
||||||
.m_doc = "This is a C extension to read MaxMind DB file format",
|
|
||||||
.m_methods = MaxMindDB_methods,
|
|
||||||
};
|
|
||||||
#endif
|
|
||||||
|
|
||||||
MOD_INIT(extension){
|
|
||||||
PyObject *m;
|
|
||||||
|
|
||||||
#if PY_MAJOR_VERSION >= 3
|
|
||||||
m = PyModule_Create(&MaxMindDB_module);
|
|
||||||
#else
|
|
||||||
m = Py_InitModule("extension", MaxMindDB_methods);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (!m) {
|
|
||||||
RETURN_MOD_INIT(NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
Reader_Type.tp_new = PyType_GenericNew;
|
|
||||||
if (PyType_Ready(&Reader_Type)) {
|
|
||||||
RETURN_MOD_INIT(NULL);
|
|
||||||
}
|
|
||||||
Py_INCREF(&Reader_Type);
|
|
||||||
PyModule_AddObject(m, "Reader", (PyObject *)&Reader_Type);
|
|
||||||
|
|
||||||
Metadata_Type.tp_new = PyType_GenericNew;
|
|
||||||
if (PyType_Ready(&Metadata_Type)) {
|
|
||||||
RETURN_MOD_INIT(NULL);
|
|
||||||
}
|
|
||||||
PyModule_AddObject(m, "extension", (PyObject *)&Metadata_Type);
|
|
||||||
|
|
||||||
PyObject* error_mod = PyImport_ImportModule("maxminddb.errors");
|
|
||||||
if (error_mod == NULL) {
|
|
||||||
RETURN_MOD_INIT(NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
MaxMindDB_error = PyObject_GetAttrString(error_mod, "InvalidDatabaseError");
|
|
||||||
Py_DECREF(error_mod);
|
|
||||||
|
|
||||||
if (MaxMindDB_error == NULL) {
|
|
||||||
RETURN_MOD_INIT(NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
Py_INCREF(MaxMindDB_error);
|
|
||||||
|
|
||||||
/* We primarily add it to the module for backwards compatibility */
|
|
||||||
PyModule_AddObject(m, "InvalidDatabaseError", MaxMindDB_error);
|
|
||||||
|
|
||||||
RETURN_MOD_INIT(m);
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
"""For internal use only. It provides a slice-like file reader."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
try:
|
|
||||||
from multiprocessing import Lock
|
|
||||||
except ImportError:
|
|
||||||
from threading import Lock
|
|
||||||
|
|
||||||
|
|
||||||
class FileBuffer(object):
|
|
||||||
|
|
||||||
"""A slice-able file reader"""
|
|
||||||
|
|
||||||
def __init__(self, database):
|
|
||||||
self._handle = open(database, 'rb')
|
|
||||||
self._size = os.fstat(self._handle.fileno()).st_size
|
|
||||||
if not hasattr(os, 'pread'):
|
|
||||||
self._lock = Lock()
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
if isinstance(key, slice):
|
|
||||||
return self._read(key.stop - key.start, key.start)
|
|
||||||
elif isinstance(key, int):
|
|
||||||
return self._read(1, key)
|
|
||||||
else:
|
|
||||||
raise TypeError("Invalid argument type.")
|
|
||||||
|
|
||||||
def rfind(self, needle, start):
|
|
||||||
"""Reverse find needle from start"""
|
|
||||||
pos = self._read(self._size - start - 1, start).rfind(needle)
|
|
||||||
if pos == -1:
|
|
||||||
return pos
|
|
||||||
return start + pos
|
|
||||||
|
|
||||||
def size(self):
|
|
||||||
"""Size of file"""
|
|
||||||
return self._size
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""Close file"""
|
|
||||||
self._handle.close()
|
|
||||||
|
|
||||||
if hasattr(os, 'pread'):
|
|
||||||
|
|
||||||
def _read(self, buffersize, offset):
|
|
||||||
"""read that uses pread"""
|
|
||||||
# pylint: disable=no-member
|
|
||||||
return os.pread(self._handle.fileno(), buffersize, offset)
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
def _read(self, buffersize, offset):
|
|
||||||
"""read with a lock
|
|
||||||
|
|
||||||
This lock is necessary as after a fork, the different processes
|
|
||||||
will share the same file table entry, even if we dup the fd, and
|
|
||||||
as such the same offsets. There does not appear to be a way to
|
|
||||||
duplicate the file table entry and we cannot re-open based on the
|
|
||||||
original path as that file may have replaced with another or
|
|
||||||
unlinked.
|
|
||||||
"""
|
|
||||||
with self._lock:
|
|
||||||
self._handle.seek(offset)
|
|
||||||
return self._handle.read(buffersize)
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,221 +0,0 @@
|
||||||
"""
|
|
||||||
maxminddb.reader
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module contains the pure Python database reader and related classes.
|
|
||||||
|
|
||||||
"""
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
try:
|
|
||||||
import mmap
|
|
||||||
except ImportError:
|
|
||||||
# pylint: disable=invalid-name
|
|
||||||
mmap = None
|
|
||||||
|
|
||||||
import struct
|
|
||||||
|
|
||||||
from maxminddb.compat import byte_from_int, int_from_byte, ipaddress
|
|
||||||
from maxminddb.const import MODE_AUTO, MODE_MMAP, MODE_FILE, MODE_MEMORY
|
|
||||||
from maxminddb.decoder import Decoder
|
|
||||||
from maxminddb.errors import InvalidDatabaseError
|
|
||||||
from maxminddb.file import FileBuffer
|
|
||||||
|
|
||||||
|
|
||||||
class Reader(object):
|
|
||||||
|
|
||||||
"""
|
|
||||||
Instances of this class provide a reader for the MaxMind DB format. IP
|
|
||||||
addresses can be looked up using the ``get`` method.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_DATA_SECTION_SEPARATOR_SIZE = 16
|
|
||||||
_METADATA_START_MARKER = b"\xAB\xCD\xEFMaxMind.com"
|
|
||||||
|
|
||||||
_ipv4_start = None
|
|
||||||
|
|
||||||
def __init__(self, database, mode=MODE_AUTO):
|
|
||||||
"""Reader for the MaxMind DB file format
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
database -- A path to a valid MaxMind DB file such as a GeoIP2
|
|
||||||
database file.
|
|
||||||
mode -- mode to open the database with. Valid mode are:
|
|
||||||
* MODE_MMAP - read from memory map.
|
|
||||||
* MODE_FILE - read database as standard file.
|
|
||||||
* MODE_MEMORY - load database into memory.
|
|
||||||
* MODE_AUTO - tries MODE_MMAP and then MODE_FILE. Default.
|
|
||||||
"""
|
|
||||||
if (mode == MODE_AUTO and mmap) or mode == MODE_MMAP:
|
|
||||||
with open(database, 'rb') as db_file:
|
|
||||||
self._buffer = mmap.mmap(
|
|
||||||
db_file.fileno(), 0, access=mmap.ACCESS_READ)
|
|
||||||
self._buffer_size = self._buffer.size()
|
|
||||||
elif mode in (MODE_AUTO, MODE_FILE):
|
|
||||||
self._buffer = FileBuffer(database)
|
|
||||||
self._buffer_size = self._buffer.size()
|
|
||||||
elif mode == MODE_MEMORY:
|
|
||||||
with open(database, 'rb') as db_file:
|
|
||||||
self._buffer = db_file.read()
|
|
||||||
self._buffer_size = len(self._buffer)
|
|
||||||
else:
|
|
||||||
raise ValueError('Unsupported open mode ({0}). Only MODE_AUTO, '
|
|
||||||
' MODE_FILE, and MODE_MEMORY are support by the pure Python '
|
|
||||||
'Reader'.format(mode))
|
|
||||||
|
|
||||||
metadata_start = self._buffer.rfind(self._METADATA_START_MARKER,
|
|
||||||
max(0, self._buffer_size
|
|
||||||
- 128 * 1024))
|
|
||||||
|
|
||||||
if metadata_start == -1:
|
|
||||||
self.close()
|
|
||||||
raise InvalidDatabaseError('Error opening database file ({0}). '
|
|
||||||
'Is this a valid MaxMind DB file?'
|
|
||||||
''.format(database))
|
|
||||||
|
|
||||||
metadata_start += len(self._METADATA_START_MARKER)
|
|
||||||
metadata_decoder = Decoder(self._buffer, metadata_start)
|
|
||||||
(metadata, _) = metadata_decoder.decode(metadata_start)
|
|
||||||
self._metadata = Metadata(
|
|
||||||
**metadata) # pylint: disable=bad-option-value
|
|
||||||
|
|
||||||
self._decoder = Decoder(self._buffer, self._metadata.search_tree_size
|
|
||||||
+ self._DATA_SECTION_SEPARATOR_SIZE)
|
|
||||||
|
|
||||||
def metadata(self):
|
|
||||||
"""Return the metadata associated with the MaxMind DB file"""
|
|
||||||
return self._metadata
|
|
||||||
|
|
||||||
def get(self, ip_address):
|
|
||||||
"""Return the record for the ip_address in the MaxMind DB
|
|
||||||
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
ip_address -- an IP address in the standard string notation
|
|
||||||
"""
|
|
||||||
address = ipaddress.ip_address(ip_address)
|
|
||||||
|
|
||||||
if address.version == 6 and self._metadata.ip_version == 4:
|
|
||||||
raise ValueError('Error looking up {0}. You attempted to look up '
|
|
||||||
'an IPv6 address in an IPv4-only database.'.format(
|
|
||||||
ip_address))
|
|
||||||
pointer = self._find_address_in_tree(address)
|
|
||||||
|
|
||||||
return self._resolve_data_pointer(pointer) if pointer else None
|
|
||||||
|
|
||||||
def _find_address_in_tree(self, ip_address):
|
|
||||||
packed = ip_address.packed
|
|
||||||
|
|
||||||
bit_count = len(packed) * 8
|
|
||||||
node = self._start_node(bit_count)
|
|
||||||
|
|
||||||
for i in range(bit_count):
|
|
||||||
if node >= self._metadata.node_count:
|
|
||||||
break
|
|
||||||
bit = 1 & (int_from_byte(packed[i >> 3]) >> 7 - (i % 8))
|
|
||||||
node = self._read_node(node, bit)
|
|
||||||
if node == self._metadata.node_count:
|
|
||||||
# Record is empty
|
|
||||||
return 0
|
|
||||||
elif node > self._metadata.node_count:
|
|
||||||
return node
|
|
||||||
|
|
||||||
raise InvalidDatabaseError('Invalid node in search tree')
|
|
||||||
|
|
||||||
def _start_node(self, length):
|
|
||||||
if self._metadata.ip_version != 6 or length == 128:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# We are looking up an IPv4 address in an IPv6 tree. Skip over the
|
|
||||||
# first 96 nodes.
|
|
||||||
if self._ipv4_start:
|
|
||||||
return self._ipv4_start
|
|
||||||
|
|
||||||
node = 0
|
|
||||||
for _ in range(96):
|
|
||||||
if node >= self._metadata.node_count:
|
|
||||||
break
|
|
||||||
node = self._read_node(node, 0)
|
|
||||||
self._ipv4_start = node
|
|
||||||
return node
|
|
||||||
|
|
||||||
def _read_node(self, node_number, index):
|
|
||||||
base_offset = node_number * self._metadata.node_byte_size
|
|
||||||
|
|
||||||
record_size = self._metadata.record_size
|
|
||||||
if record_size == 24:
|
|
||||||
offset = base_offset + index * 3
|
|
||||||
node_bytes = b'\x00' + self._buffer[offset:offset + 3]
|
|
||||||
elif record_size == 28:
|
|
||||||
(middle,) = struct.unpack(
|
|
||||||
b'!B', self._buffer[base_offset + 3:base_offset + 4])
|
|
||||||
if index:
|
|
||||||
middle &= 0x0F
|
|
||||||
else:
|
|
||||||
middle = (0xF0 & middle) >> 4
|
|
||||||
offset = base_offset + index * 4
|
|
||||||
node_bytes = byte_from_int(
|
|
||||||
middle) + self._buffer[offset:offset + 3]
|
|
||||||
elif record_size == 32:
|
|
||||||
offset = base_offset + index * 4
|
|
||||||
node_bytes = self._buffer[offset:offset + 4]
|
|
||||||
else:
|
|
||||||
raise InvalidDatabaseError(
|
|
||||||
'Unknown record size: {0}'.format(record_size))
|
|
||||||
return struct.unpack(b'!I', node_bytes)[0]
|
|
||||||
|
|
||||||
def _resolve_data_pointer(self, pointer):
|
|
||||||
resolved = pointer - self._metadata.node_count + \
|
|
||||||
self._metadata.search_tree_size
|
|
||||||
|
|
||||||
if resolved > self._buffer_size:
|
|
||||||
raise InvalidDatabaseError(
|
|
||||||
"The MaxMind DB file's search tree is corrupt")
|
|
||||||
|
|
||||||
(data, _) = self._decoder.decode(resolved)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""Closes the MaxMind DB file and returns the resources to the system"""
|
|
||||||
# pylint: disable=unidiomatic-typecheck
|
|
||||||
if type(self._buffer) not in (str, bytes):
|
|
||||||
self._buffer.close()
|
|
||||||
|
|
||||||
|
|
||||||
class Metadata(object):
|
|
||||||
|
|
||||||
"""Metadata for the MaxMind DB reader"""
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
"""Creates new Metadata object. kwargs are key/value pairs from spec"""
|
|
||||||
# Although I could just update __dict__, that is less obvious and it
|
|
||||||
# doesn't work well with static analysis tools and some IDEs
|
|
||||||
self.node_count = kwargs['node_count']
|
|
||||||
self.record_size = kwargs['record_size']
|
|
||||||
self.ip_version = kwargs['ip_version']
|
|
||||||
self.database_type = kwargs['database_type']
|
|
||||||
self.languages = kwargs['languages']
|
|
||||||
self.binary_format_major_version = kwargs[
|
|
||||||
'binary_format_major_version']
|
|
||||||
self.binary_format_minor_version = kwargs[
|
|
||||||
'binary_format_minor_version']
|
|
||||||
self.build_epoch = kwargs['build_epoch']
|
|
||||||
self.description = kwargs['description']
|
|
||||||
|
|
||||||
@property
|
|
||||||
def node_byte_size(self):
|
|
||||||
"""The size of a node in bytes"""
|
|
||||||
return self.record_size // 4
|
|
||||||
|
|
||||||
@property
|
|
||||||
def search_tree_size(self):
|
|
||||||
"""The size of the search tree"""
|
|
||||||
return self.node_count * self.node_byte_size
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
args = ', '.join('%s=%r' % x for x in self.__dict__.items())
|
|
||||||
return '{module}.{class_name}({data})'.format(
|
|
||||||
module=self.__module__,
|
|
||||||
class_name=self.__class__.__name__,
|
|
||||||
data=args)
|
|
|
@ -1,60 +0,0 @@
|
||||||
/**
|
|
||||||
* @author alteredq / http://alteredqualia.com/
|
|
||||||
* @author mr.doob / http://mrdoob.com/
|
|
||||||
*/
|
|
||||||
|
|
||||||
Detector = {
|
|
||||||
|
|
||||||
canvas : !! window.CanvasRenderingContext2D,
|
|
||||||
webgl : ( function () { try { return !! window.WebGLRenderingContext && !! document.createElement( 'canvas' ).getContext( 'experimental-webgl' ); } catch( e ) { return false; } } )(),
|
|
||||||
workers : !! window.Worker,
|
|
||||||
fileapi : window.File && window.FileReader && window.FileList && window.Blob,
|
|
||||||
|
|
||||||
getWebGLErrorMessage : function () {
|
|
||||||
|
|
||||||
var domElement = document.createElement( 'div' );
|
|
||||||
|
|
||||||
domElement.style.fontFamily = 'monospace';
|
|
||||||
domElement.style.fontSize = '13px';
|
|
||||||
domElement.style.textAlign = 'center';
|
|
||||||
domElement.style.background = '#eee';
|
|
||||||
domElement.style.color = '#000';
|
|
||||||
domElement.style.padding = '1em';
|
|
||||||
domElement.style.width = '475px';
|
|
||||||
domElement.style.margin = '5em auto 0';
|
|
||||||
|
|
||||||
if ( ! this.webgl ) {
|
|
||||||
|
|
||||||
domElement.innerHTML = window.WebGLRenderingContext ? [
|
|
||||||
'Sorry, your graphics card doesn\'t support <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation">WebGL</a>'
|
|
||||||
].join( '\n' ) : [
|
|
||||||
'Sorry, your browser doesn\'t support <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation">WebGL</a><br/>',
|
|
||||||
'Please try with',
|
|
||||||
'<a href="http://www.google.com/chrome">Chrome</a>, ',
|
|
||||||
'<a href="http://www.mozilla.com/en-US/firefox/new/">Firefox 4</a> or',
|
|
||||||
'<a href="http://nightly.webkit.org/">Webkit Nightly (Mac)</a>'
|
|
||||||
].join( '\n' );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return domElement;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
addGetWebGLMessage : function ( parameters ) {
|
|
||||||
|
|
||||||
var parent, id, domElement;
|
|
||||||
|
|
||||||
parameters = parameters || {};
|
|
||||||
|
|
||||||
parent = parameters.parent !== undefined ? parameters.parent : document.body;
|
|
||||||
id = parameters.id !== undefined ? parameters.id : 'oldie';
|
|
||||||
|
|
||||||
domElement = Detector.getWebGLErrorMessage();
|
|
||||||
domElement.id = id;
|
|
||||||
|
|
||||||
parent.appendChild( domElement );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
|
@ -1,12 +0,0 @@
|
||||||
// Tween.js - http://github.com/sole/tween.js
|
|
||||||
var TWEEN=TWEEN||function(){var a,e,c,d,f=[];return{start:function(g){c=setInterval(this.update,1E3/(g||60))},stop:function(){clearInterval(c)},add:function(g){f.push(g)},remove:function(g){a=f.indexOf(g);a!==-1&&f.splice(a,1)},update:function(){a=0;e=f.length;for(d=(new Date).getTime();a<e;)if(f[a].update(d))a++;else{f.splice(a,1);e--}}}}();
|
|
||||||
TWEEN.Tween=function(a){var e={},c={},d={},f=1E3,g=0,j=null,n=TWEEN.Easing.Linear.EaseNone,k=null,l=null,m=null;this.to=function(b,h){if(h!==null)f=h;for(var i in b)if(a[i]!==null)d[i]=b[i];return this};this.start=function(){TWEEN.add(this);j=(new Date).getTime()+g;for(var b in d)if(a[b]!==null){e[b]=a[b];c[b]=d[b]-a[b]}return this};this.stop=function(){TWEEN.remove(this);return this};this.delay=function(b){g=b;return this};this.easing=function(b){n=b;return this};this.chain=function(b){k=b};this.onUpdate=
|
|
||||||
function(b){l=b;return this};this.onComplete=function(b){m=b;return this};this.update=function(b){var h,i;if(b<j)return true;b=(b-j)/f;b=b>1?1:b;i=n(b);for(h in c)a[h]=e[h]+c[h]*i;l!==null&&l.call(a,i);if(b==1){m!==null&&m.call(a);k!==null&&k.start();return false}return true}};TWEEN.Easing={Linear:{},Quadratic:{},Cubic:{},Quartic:{},Quintic:{},Sinusoidal:{},Exponential:{},Circular:{},Elastic:{},Back:{},Bounce:{}};TWEEN.Easing.Linear.EaseNone=function(a){return a};
|
|
||||||
TWEEN.Easing.Quadratic.EaseIn=function(a){return a*a};TWEEN.Easing.Quadratic.EaseOut=function(a){return-a*(a-2)};TWEEN.Easing.Quadratic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a;return-0.5*(--a*(a-2)-1)};TWEEN.Easing.Cubic.EaseIn=function(a){return a*a*a};TWEEN.Easing.Cubic.EaseOut=function(a){return--a*a*a+1};TWEEN.Easing.Cubic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*a;return 0.5*((a-=2)*a*a+2)};TWEEN.Easing.Quartic.EaseIn=function(a){return a*a*a*a};
|
|
||||||
TWEEN.Easing.Quartic.EaseOut=function(a){return-(--a*a*a*a-1)};TWEEN.Easing.Quartic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*a*a;return-0.5*((a-=2)*a*a*a-2)};TWEEN.Easing.Quintic.EaseIn=function(a){return a*a*a*a*a};TWEEN.Easing.Quintic.EaseOut=function(a){return(a-=1)*a*a*a*a+1};TWEEN.Easing.Quintic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*a*a*a;return 0.5*((a-=2)*a*a*a*a+2)};TWEEN.Easing.Sinusoidal.EaseIn=function(a){return-Math.cos(a*Math.PI/2)+1};
|
|
||||||
TWEEN.Easing.Sinusoidal.EaseOut=function(a){return Math.sin(a*Math.PI/2)};TWEEN.Easing.Sinusoidal.EaseInOut=function(a){return-0.5*(Math.cos(Math.PI*a)-1)};TWEEN.Easing.Exponential.EaseIn=function(a){return a==0?0:Math.pow(2,10*(a-1))};TWEEN.Easing.Exponential.EaseOut=function(a){return a==1?1:-Math.pow(2,-10*a)+1};TWEEN.Easing.Exponential.EaseInOut=function(a){if(a==0)return 0;if(a==1)return 1;if((a*=2)<1)return 0.5*Math.pow(2,10*(a-1));return 0.5*(-Math.pow(2,-10*(a-1))+2)};
|
|
||||||
TWEEN.Easing.Circular.EaseIn=function(a){return-(Math.sqrt(1-a*a)-1)};TWEEN.Easing.Circular.EaseOut=function(a){return Math.sqrt(1- --a*a)};TWEEN.Easing.Circular.EaseInOut=function(a){if((a/=0.5)<1)return-0.5*(Math.sqrt(1-a*a)-1);return 0.5*(Math.sqrt(1-(a-=2)*a)+1)};TWEEN.Easing.Elastic.EaseIn=function(a){var e,c=0.1,d=0.4;if(a==0)return 0;if(a==1)return 1;d||(d=0.3);if(!c||c<1){c=1;e=d/4}else e=d/(2*Math.PI)*Math.asin(1/c);return-(c*Math.pow(2,10*(a-=1))*Math.sin((a-e)*2*Math.PI/d))};
|
|
||||||
TWEEN.Easing.Elastic.EaseOut=function(a){var e,c=0.1,d=0.4;if(a==0)return 0;if(a==1)return 1;d||(d=0.3);if(!c||c<1){c=1;e=d/4}else e=d/(2*Math.PI)*Math.asin(1/c);return c*Math.pow(2,-10*a)*Math.sin((a-e)*2*Math.PI/d)+1};
|
|
||||||
TWEEN.Easing.Elastic.EaseInOut=function(a){var e,c=0.1,d=0.4;if(a==0)return 0;if(a==1)return 1;d||(d=0.3);if(!c||c<1){c=1;e=d/4}else e=d/(2*Math.PI)*Math.asin(1/c);if((a*=2)<1)return-0.5*c*Math.pow(2,10*(a-=1))*Math.sin((a-e)*2*Math.PI/d);return c*Math.pow(2,-10*(a-=1))*Math.sin((a-e)*2*Math.PI/d)*0.5+1};TWEEN.Easing.Back.EaseIn=function(a){return a*a*(2.70158*a-1.70158)};TWEEN.Easing.Back.EaseOut=function(a){return(a-=1)*a*(2.70158*a+1.70158)+1};
|
|
||||||
TWEEN.Easing.Back.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*(3.5949095*a-2.5949095);return 0.5*((a-=2)*a*(3.5949095*a+2.5949095)+2)};TWEEN.Easing.Bounce.EaseIn=function(a){return 1-TWEEN.Easing.Bounce.EaseOut(1-a)};TWEEN.Easing.Bounce.EaseOut=function(a){return(a/=1)<1/2.75?7.5625*a*a:a<2/2.75?7.5625*(a-=1.5/2.75)*a+0.75:a<2.5/2.75?7.5625*(a-=2.25/2.75)*a+0.9375:7.5625*(a-=2.625/2.75)*a+0.984375};
|
|
||||||
TWEEN.Easing.Bounce.EaseInOut=function(a){if(a<0.5)return TWEEN.Easing.Bounce.EaseIn(a*2)*0.5;return TWEEN.Easing.Bounce.EaseOut(a*2-1)*0.5+0.5};
|
|
File diff suppressed because one or more lines are too long
|
@ -1,435 +0,0 @@
|
||||||
/**
|
|
||||||
* dat.globe Javascript WebGL Globe Toolkit
|
|
||||||
* http://dataarts.github.com/dat.globe
|
|
||||||
*
|
|
||||||
* Copyright 2011 Data Arts Team, Google Creative Lab
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
var DAT = DAT || {};
|
|
||||||
|
|
||||||
DAT.Globe = function(container, opts) {
|
|
||||||
opts = opts || {};
|
|
||||||
|
|
||||||
var colorFn = opts.colorFn || function(x) {
|
|
||||||
var c = new THREE.Color();
|
|
||||||
c.setHSL( ( 0.5 - (x * 2) ), Math.max(0.8, 1.0 - (x * 3)), 0.5 );
|
|
||||||
return c;
|
|
||||||
};
|
|
||||||
var imgDir = opts.imgDir || '/globe/';
|
|
||||||
|
|
||||||
var Shaders = {
|
|
||||||
'earth' : {
|
|
||||||
uniforms: {
|
|
||||||
'texture': { type: 't', value: null }
|
|
||||||
},
|
|
||||||
vertexShader: [
|
|
||||||
'varying vec3 vNormal;',
|
|
||||||
'varying vec2 vUv;',
|
|
||||||
'void main() {',
|
|
||||||
'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
|
|
||||||
'vNormal = normalize( normalMatrix * normal );',
|
|
||||||
'vUv = uv;',
|
|
||||||
'}'
|
|
||||||
].join('\n'),
|
|
||||||
fragmentShader: [
|
|
||||||
'uniform sampler2D texture;',
|
|
||||||
'varying vec3 vNormal;',
|
|
||||||
'varying vec2 vUv;',
|
|
||||||
'void main() {',
|
|
||||||
'vec3 diffuse = texture2D( texture, vUv ).xyz;',
|
|
||||||
'float intensity = 1.05 - dot( vNormal, vec3( 0.0, 0.0, 1.0 ) );',
|
|
||||||
'vec3 atmosphere = vec3( 1.0, 1.0, 1.0 ) * pow( intensity, 3.0 );',
|
|
||||||
'gl_FragColor = vec4( diffuse + atmosphere, 1.0 );',
|
|
||||||
'}'
|
|
||||||
].join('\n')
|
|
||||||
},
|
|
||||||
'atmosphere' : {
|
|
||||||
uniforms: {},
|
|
||||||
vertexShader: [
|
|
||||||
'varying vec3 vNormal;',
|
|
||||||
'void main() {',
|
|
||||||
'vNormal = normalize( normalMatrix * normal );',
|
|
||||||
'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
|
|
||||||
'}'
|
|
||||||
].join('\n'),
|
|
||||||
fragmentShader: [
|
|
||||||
'varying vec3 vNormal;',
|
|
||||||
'void main() {',
|
|
||||||
'float intensity = pow( 0.8 - dot( vNormal, vec3( 0, 0, 1.0 ) ), 12.0 );',
|
|
||||||
'gl_FragColor = vec4( 1.0, 1.0, 1.0, 1.0 ) * intensity;',
|
|
||||||
'}'
|
|
||||||
].join('\n')
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var camera, scene, renderer, w, h;
|
|
||||||
var mesh, atmosphere, point, running;
|
|
||||||
|
|
||||||
var overRenderer;
|
|
||||||
var running = true;
|
|
||||||
|
|
||||||
var curZoomSpeed = 0;
|
|
||||||
var zoomSpeed = 50;
|
|
||||||
|
|
||||||
var mouse = { x: 0, y: 0 }, mouseOnDown = { x: 0, y: 0 };
|
|
||||||
var rotation = { x: 0, y: 0 },
|
|
||||||
target = { x: Math.PI*3/2, y: Math.PI / 6.0 },
|
|
||||||
targetOnDown = { x: 0, y: 0 };
|
|
||||||
|
|
||||||
var distance = 100000, distanceTarget = 100000;
|
|
||||||
var padding = 10;
|
|
||||||
var PI_HALF = Math.PI / 2;
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
|
|
||||||
container.style.color = '#fff';
|
|
||||||
container.style.font = '13px/20px Arial, sans-serif';
|
|
||||||
|
|
||||||
var shader, uniforms, material;
|
|
||||||
w = container.offsetWidth || window.innerWidth;
|
|
||||||
h = container.offsetHeight || window.innerHeight;
|
|
||||||
|
|
||||||
camera = new THREE.PerspectiveCamera(30, w / h, 1, 10000);
|
|
||||||
camera.position.z = distance;
|
|
||||||
|
|
||||||
scene = new THREE.Scene();
|
|
||||||
|
|
||||||
var geometry = new THREE.SphereGeometry(200, 40, 30);
|
|
||||||
|
|
||||||
shader = Shaders['earth'];
|
|
||||||
uniforms = THREE.UniformsUtils.clone(shader.uniforms);
|
|
||||||
|
|
||||||
uniforms['texture'].value = THREE.ImageUtils.loadTexture(imgDir+'world.jpg');
|
|
||||||
|
|
||||||
material = new THREE.ShaderMaterial({
|
|
||||||
|
|
||||||
uniforms: uniforms,
|
|
||||||
vertexShader: shader.vertexShader,
|
|
||||||
fragmentShader: shader.fragmentShader
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
mesh = new THREE.Mesh(geometry, material);
|
|
||||||
mesh.rotation.y = Math.PI;
|
|
||||||
scene.add(mesh);
|
|
||||||
|
|
||||||
shader = Shaders['atmosphere'];
|
|
||||||
uniforms = THREE.UniformsUtils.clone(shader.uniforms);
|
|
||||||
|
|
||||||
material = new THREE.ShaderMaterial({
|
|
||||||
|
|
||||||
uniforms: uniforms,
|
|
||||||
vertexShader: shader.vertexShader,
|
|
||||||
fragmentShader: shader.fragmentShader,
|
|
||||||
side: THREE.BackSide,
|
|
||||||
blending: THREE.AdditiveBlending,
|
|
||||||
transparent: true
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
mesh = new THREE.Mesh(geometry, material);
|
|
||||||
mesh.scale.set( 1.1, 1.1, 1.1 );
|
|
||||||
scene.add(mesh);
|
|
||||||
|
|
||||||
geometry = new THREE.BoxGeometry(2.75, 2.75, 1);
|
|
||||||
geometry.applyMatrix(new THREE.Matrix4().makeTranslation(0,0,-0.5));
|
|
||||||
|
|
||||||
point = new THREE.Mesh(geometry);
|
|
||||||
|
|
||||||
renderer = new THREE.WebGLRenderer({antialias: true});
|
|
||||||
renderer.setSize(w, h);
|
|
||||||
renderer.setClearColor( 0x212121, 1 );
|
|
||||||
|
|
||||||
renderer.domElement.style.position = 'relative';
|
|
||||||
|
|
||||||
container.appendChild(renderer.domElement);
|
|
||||||
|
|
||||||
container.addEventListener('mousedown', onMouseDown, false);
|
|
||||||
|
|
||||||
if ('onwheel' in document) {
|
|
||||||
container.addEventListener('wheel', onMouseWheel, false);
|
|
||||||
} else {
|
|
||||||
container.addEventListener('mousewheel', onMouseWheel, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('keydown', onDocumentKeyDown, false);
|
|
||||||
|
|
||||||
window.addEventListener('resize', onWindowResize, false);
|
|
||||||
|
|
||||||
container.addEventListener('mouseover', function() {
|
|
||||||
overRenderer = true;
|
|
||||||
}, false);
|
|
||||||
|
|
||||||
container.addEventListener('mouseout', function() {
|
|
||||||
overRenderer = false;
|
|
||||||
}, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addData(data, opts) {
|
|
||||||
var lat, lng, size, color, i, step, colorFnWrapper;
|
|
||||||
|
|
||||||
opts.animated = opts.animated || false;
|
|
||||||
this.is_animated = opts.animated;
|
|
||||||
opts.format = opts.format || 'magnitude'; // other option is 'legend'
|
|
||||||
if (opts.format === 'magnitude') {
|
|
||||||
step = 3;
|
|
||||||
colorFnWrapper = function(data, i) { return colorFn(data[i+2]); }
|
|
||||||
} else if (opts.format === 'legend') {
|
|
||||||
step = 4;
|
|
||||||
colorFnWrapper = function(data, i) { return colorFn(data[i+3]); }
|
|
||||||
} else if (opts.format === 'peer') {
|
|
||||||
colorFnWrapper = function(data, i) { return colorFn(data[i+2]); }
|
|
||||||
} else {
|
|
||||||
throw('error: format not supported: '+opts.format);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.animated) {
|
|
||||||
if (this._baseGeometry === undefined) {
|
|
||||||
this._baseGeometry = new THREE.Geometry();
|
|
||||||
for (i = 0; i < data.length; i += step) {
|
|
||||||
lat = data[i];
|
|
||||||
lng = data[i + 1];
|
|
||||||
// size = data[i + 2];
|
|
||||||
color = colorFnWrapper(data,i);
|
|
||||||
size = 0;
|
|
||||||
addPoint(lat, lng, size, color, this._baseGeometry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(this._morphTargetId === undefined) {
|
|
||||||
this._morphTargetId = 0;
|
|
||||||
} else {
|
|
||||||
this._morphTargetId += 1;
|
|
||||||
}
|
|
||||||
opts.name = opts.name || 'morphTarget'+this._morphTargetId;
|
|
||||||
}
|
|
||||||
var subgeo = new THREE.Geometry();
|
|
||||||
for (i = 0; i < data.length; i += step) {
|
|
||||||
lat = data[i];
|
|
||||||
lng = data[i + 1];
|
|
||||||
color = colorFnWrapper(data,i);
|
|
||||||
size = data[i + 2];
|
|
||||||
size = size*200;
|
|
||||||
addPoint(lat, lng, size, color, subgeo);
|
|
||||||
}
|
|
||||||
if (opts.animated) {
|
|
||||||
this._baseGeometry.morphTargets.push({'name': opts.name, vertices: subgeo.vertices});
|
|
||||||
} else {
|
|
||||||
this._baseGeometry = subgeo;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
function createPoints() {
|
|
||||||
if (this._baseGeometry !== undefined) {
|
|
||||||
if (this.is_animated === false) {
|
|
||||||
this.points = new THREE.Mesh(this._baseGeometry, new THREE.MeshBasicMaterial({
|
|
||||||
color: 0xffffff,
|
|
||||||
vertexColors: THREE.FaceColors,
|
|
||||||
morphTargets: false
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
if (this._baseGeometry.morphTargets.length < 8) {
|
|
||||||
console.log('t l',this._baseGeometry.morphTargets.length);
|
|
||||||
var padding = 8-this._baseGeometry.morphTargets.length;
|
|
||||||
console.log('padding', padding);
|
|
||||||
for(var i=0; i<=padding; i++) {
|
|
||||||
console.log('padding',i);
|
|
||||||
this._baseGeometry.morphTargets.push({'name': 'morphPadding'+i, vertices: this._baseGeometry.vertices});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.points = new THREE.Mesh(this._baseGeometry, new THREE.MeshBasicMaterial({
|
|
||||||
color: 0xffffff,
|
|
||||||
vertexColors: THREE.FaceColors,
|
|
||||||
morphTargets: true
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
scene.add(this.points);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addPoint(lat, lng, size, color, subgeo) {
|
|
||||||
|
|
||||||
var phi = (90 - lat) * Math.PI / 180;
|
|
||||||
var theta = (180 - lng) * Math.PI / 180;
|
|
||||||
|
|
||||||
point.position.x = 200 * Math.sin(phi) * Math.cos(theta);
|
|
||||||
point.position.y = 200 * Math.cos(phi);
|
|
||||||
point.position.z = 200 * Math.sin(phi) * Math.sin(theta);
|
|
||||||
|
|
||||||
point.lookAt(mesh.position);
|
|
||||||
|
|
||||||
point.scale.z = Math.max( size, 0.1 ); // avoid non-invertible matrix
|
|
||||||
point.updateMatrix();
|
|
||||||
|
|
||||||
for (var i = 0; i < point.geometry.faces.length; i++) {
|
|
||||||
|
|
||||||
point.geometry.faces[i].color = color;
|
|
||||||
|
|
||||||
}
|
|
||||||
if(point.matrixAutoUpdate){
|
|
||||||
point.updateMatrix();
|
|
||||||
}
|
|
||||||
subgeo.merge(point.geometry, point.matrix);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMouseDown(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
container.addEventListener('mousemove', onMouseMove, false);
|
|
||||||
container.addEventListener('mouseup', onMouseUp, false);
|
|
||||||
container.addEventListener('mouseout', onMouseOut, false);
|
|
||||||
|
|
||||||
mouseOnDown.x = - event.clientX;
|
|
||||||
mouseOnDown.y = event.clientY;
|
|
||||||
|
|
||||||
targetOnDown.x = target.x;
|
|
||||||
targetOnDown.y = target.y;
|
|
||||||
|
|
||||||
container.style.cursor = 'move';
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMouseMove(event) {
|
|
||||||
mouse.x = - event.clientX;
|
|
||||||
mouse.y = event.clientY;
|
|
||||||
|
|
||||||
var zoomDamp = distance/1000;
|
|
||||||
|
|
||||||
target.x = targetOnDown.x + (mouse.x - mouseOnDown.x) * 0.005 * zoomDamp;
|
|
||||||
target.y = targetOnDown.y + (mouse.y - mouseOnDown.y) * 0.005 * zoomDamp;
|
|
||||||
|
|
||||||
target.y = target.y > PI_HALF ? PI_HALF : target.y;
|
|
||||||
target.y = target.y < - PI_HALF ? - PI_HALF : target.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMouseUp(event) {
|
|
||||||
container.removeEventListener('mousemove', onMouseMove, false);
|
|
||||||
container.removeEventListener('mouseup', onMouseUp, false);
|
|
||||||
container.removeEventListener('mouseout', onMouseOut, false);
|
|
||||||
container.style.cursor = 'auto';
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMouseOut(event) {
|
|
||||||
container.removeEventListener('mousemove', onMouseMove, false);
|
|
||||||
container.removeEventListener('mouseup', onMouseUp, false);
|
|
||||||
container.removeEventListener('mouseout', onMouseOut, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMouseWheel(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
if (overRenderer) {
|
|
||||||
if (event.deltaY) {
|
|
||||||
zoom(-event.deltaY * (event.deltaMode == 0 ? 1 : 50));
|
|
||||||
} else {
|
|
||||||
zoom(event.wheelDeltaY * 0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDocumentKeyDown(event) {
|
|
||||||
switch (event.keyCode) {
|
|
||||||
case 38:
|
|
||||||
zoom(100);
|
|
||||||
event.preventDefault();
|
|
||||||
break;
|
|
||||||
case 40:
|
|
||||||
zoom(-100);
|
|
||||||
event.preventDefault();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onWindowResize( event ) {
|
|
||||||
camera.aspect = container.offsetWidth / container.offsetHeight;
|
|
||||||
camera.updateProjectionMatrix();
|
|
||||||
renderer.setSize( container.offsetWidth, container.offsetHeight );
|
|
||||||
}
|
|
||||||
|
|
||||||
function zoom(delta) {
|
|
||||||
distanceTarget -= delta;
|
|
||||||
distanceTarget = distanceTarget > 855 ? 855 : distanceTarget;
|
|
||||||
distanceTarget = distanceTarget < 350 ? 350 : distanceTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
function animate() {
|
|
||||||
if (!running) return
|
|
||||||
requestAnimationFrame(animate);
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
zoom(curZoomSpeed);
|
|
||||||
|
|
||||||
rotation.x += (target.x - rotation.x) * 0.1;
|
|
||||||
rotation.y += (target.y - rotation.y) * 0.1;
|
|
||||||
distance += (distanceTarget - distance) * 0.3;
|
|
||||||
|
|
||||||
camera.position.x = distance * Math.sin(rotation.x) * Math.cos(rotation.y);
|
|
||||||
camera.position.y = distance * Math.sin(rotation.y);
|
|
||||||
camera.position.z = distance * Math.cos(rotation.x) * Math.cos(rotation.y);
|
|
||||||
|
|
||||||
camera.lookAt(mesh.position);
|
|
||||||
|
|
||||||
renderer.render(scene, camera);
|
|
||||||
}
|
|
||||||
|
|
||||||
function unload() {
|
|
||||||
running = false
|
|
||||||
container.removeEventListener('mousedown', onMouseDown, false);
|
|
||||||
if ('onwheel' in document) {
|
|
||||||
container.removeEventListener('wheel', onMouseWheel, false);
|
|
||||||
} else {
|
|
||||||
container.removeEventListener('mousewheel', onMouseWheel, false);
|
|
||||||
}
|
|
||||||
document.removeEventListener('keydown', onDocumentKeyDown, false);
|
|
||||||
window.removeEventListener('resize', onWindowResize, false);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
|
||||||
this.animate = animate;
|
|
||||||
this.unload = unload;
|
|
||||||
|
|
||||||
|
|
||||||
this.__defineGetter__('time', function() {
|
|
||||||
return this._time || 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.__defineSetter__('time', function(t) {
|
|
||||||
var validMorphs = [];
|
|
||||||
var morphDict = this.points.morphTargetDictionary;
|
|
||||||
for(var k in morphDict) {
|
|
||||||
if(k.indexOf('morphPadding') < 0) {
|
|
||||||
validMorphs.push(morphDict[k]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
validMorphs.sort();
|
|
||||||
var l = validMorphs.length-1;
|
|
||||||
var scaledt = t*l+1;
|
|
||||||
var index = Math.floor(scaledt);
|
|
||||||
for (i=0;i<validMorphs.length;i++) {
|
|
||||||
this.points.morphTargetInfluences[validMorphs[i]] = 0;
|
|
||||||
}
|
|
||||||
var lastIndex = index - 1;
|
|
||||||
var leftover = scaledt - index;
|
|
||||||
if (lastIndex >= 0) {
|
|
||||||
this.points.morphTargetInfluences[lastIndex] = 1 - leftover;
|
|
||||||
}
|
|
||||||
this.points.morphTargetInfluences[index] = leftover;
|
|
||||||
this._time = t;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addData = addData;
|
|
||||||
this.createPoints = createPoints;
|
|
||||||
this.renderer = renderer;
|
|
||||||
this.scene = scene;
|
|
||||||
|
|
||||||
return this;
|
|
||||||
|
|
||||||
};
|
|
814
plugins/Sidebar/media-globe/three.min.js
vendored
814
plugins/Sidebar/media-globe/three.min.js
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 93 KiB |
|
@ -1,23 +0,0 @@
|
||||||
class Class
|
|
||||||
trace: true
|
|
||||||
|
|
||||||
log: (args...) ->
|
|
||||||
return unless @trace
|
|
||||||
return if typeof console is 'undefined'
|
|
||||||
args.unshift("[#{@.constructor.name}]")
|
|
||||||
console.log(args...)
|
|
||||||
@
|
|
||||||
|
|
||||||
logStart: (name, args...) ->
|
|
||||||
return unless @trace
|
|
||||||
@logtimers or= {}
|
|
||||||
@logtimers[name] = +(new Date)
|
|
||||||
@log "#{name}", args..., "(started)" if args.length > 0
|
|
||||||
@
|
|
||||||
|
|
||||||
logEnd: (name, args...) ->
|
|
||||||
ms = +(new Date)-@logtimers[name]
|
|
||||||
@log "#{name}", args..., "(Done in #{ms}ms)"
|
|
||||||
@
|
|
||||||
|
|
||||||
window.Class = Class
|
|
|
@ -1,91 +0,0 @@
|
||||||
/* via http://jsfiddle.net/elGrecode/00dgurnn/ */
|
|
||||||
|
|
||||||
window.initScrollable = function () {
|
|
||||||
|
|
||||||
var scrollContainer = document.querySelector('.scrollable'),
|
|
||||||
scrollContentWrapper = document.querySelector('.scrollable .content-wrapper'),
|
|
||||||
scrollContent = document.querySelector('.scrollable .content'),
|
|
||||||
contentPosition = 0,
|
|
||||||
scrollerBeingDragged = false,
|
|
||||||
scroller,
|
|
||||||
topPosition,
|
|
||||||
scrollerHeight;
|
|
||||||
|
|
||||||
function calculateScrollerHeight() {
|
|
||||||
// *Calculation of how tall scroller should be
|
|
||||||
var visibleRatio = scrollContainer.offsetHeight / scrollContentWrapper.scrollHeight;
|
|
||||||
if (visibleRatio == 1)
|
|
||||||
scroller.style.display = "none";
|
|
||||||
else
|
|
||||||
scroller.style.display = "block";
|
|
||||||
return visibleRatio * scrollContainer.offsetHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveScroller(evt) {
|
|
||||||
// Move Scroll bar to top offset
|
|
||||||
var scrollPercentage = evt.target.scrollTop / scrollContentWrapper.scrollHeight;
|
|
||||||
topPosition = scrollPercentage * (scrollContainer.offsetHeight - 5); // 5px arbitrary offset so scroll bar doesn't move too far beyond content wrapper bounding box
|
|
||||||
scroller.style.top = topPosition + 'px';
|
|
||||||
}
|
|
||||||
|
|
||||||
function startDrag(evt) {
|
|
||||||
normalizedPosition = evt.pageY;
|
|
||||||
contentPosition = scrollContentWrapper.scrollTop;
|
|
||||||
scrollerBeingDragged = true;
|
|
||||||
window.addEventListener('mousemove', scrollBarScroll);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopDrag(evt) {
|
|
||||||
scrollerBeingDragged = false;
|
|
||||||
window.removeEventListener('mousemove', scrollBarScroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollBarScroll(evt) {
|
|
||||||
if (scrollerBeingDragged === true) {
|
|
||||||
evt.preventDefault();
|
|
||||||
var mouseDifferential = evt.pageY - normalizedPosition;
|
|
||||||
var scrollEquivalent = mouseDifferential * (scrollContentWrapper.scrollHeight / scrollContainer.offsetHeight);
|
|
||||||
scrollContentWrapper.scrollTop = contentPosition + scrollEquivalent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateHeight() {
|
|
||||||
scrollerHeight = calculateScrollerHeight() - 10;
|
|
||||||
scroller.style.height = scrollerHeight + 'px';
|
|
||||||
}
|
|
||||||
|
|
||||||
function createScroller() {
|
|
||||||
// *Creates scroller element and appends to '.scrollable' div
|
|
||||||
// create scroller element
|
|
||||||
scroller = document.createElement("div");
|
|
||||||
scroller.className = 'scroller';
|
|
||||||
|
|
||||||
// determine how big scroller should be based on content
|
|
||||||
scrollerHeight = calculateScrollerHeight() - 10;
|
|
||||||
|
|
||||||
if (scrollerHeight / scrollContainer.offsetHeight < 1) {
|
|
||||||
// *If there is a need to have scroll bar based on content size
|
|
||||||
scroller.style.height = scrollerHeight + 'px';
|
|
||||||
|
|
||||||
// append scroller to scrollContainer div
|
|
||||||
scrollContainer.appendChild(scroller);
|
|
||||||
|
|
||||||
// show scroll path divot
|
|
||||||
scrollContainer.className += ' showScroll';
|
|
||||||
|
|
||||||
// attach related draggable listeners
|
|
||||||
scroller.addEventListener('mousedown', startDrag);
|
|
||||||
window.addEventListener('mouseup', stopDrag);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
createScroller();
|
|
||||||
|
|
||||||
|
|
||||||
// *** Listeners ***
|
|
||||||
scrollContentWrapper.addEventListener('scroll', moveScroller);
|
|
||||||
|
|
||||||
return updateHeight;
|
|
||||||
};
|
|
|
@ -1,44 +0,0 @@
|
||||||
.scrollable {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollable.showScroll::after {
|
|
||||||
position: absolute;
|
|
||||||
content: '';
|
|
||||||
top: 5%;
|
|
||||||
right: 7px;
|
|
||||||
height: 90%;
|
|
||||||
width: 3px;
|
|
||||||
background: rgba(224, 224, 255, .3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollable .content-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding-right: 50%;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
.scroller {
|
|
||||||
margin-top: 5px;
|
|
||||||
z-index: 5;
|
|
||||||
cursor: pointer;
|
|
||||||
position: absolute;
|
|
||||||
width: 7px;
|
|
||||||
border-radius: 5px;
|
|
||||||
background: #151515;
|
|
||||||
top: 0px;
|
|
||||||
left: 395px;
|
|
||||||
-webkit-transition: top .08s;
|
|
||||||
-moz-transition: top .08s;
|
|
||||||
-ms-transition: top .08s;
|
|
||||||
-o-transition: top .08s;
|
|
||||||
transition: top .08s;
|
|
||||||
}
|
|
||||||
.scroller {
|
|
||||||
-webkit-touch-callout: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-khtml-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
|
@ -1,426 +0,0 @@
|
||||||
class Sidebar extends Class
|
|
||||||
constructor: ->
|
|
||||||
@tag = null
|
|
||||||
@container = null
|
|
||||||
@opened = false
|
|
||||||
@width = 410
|
|
||||||
@fixbutton = $(".fixbutton")
|
|
||||||
@fixbutton_addx = 0
|
|
||||||
@fixbutton_initx = 0
|
|
||||||
@fixbutton_targetx = 0
|
|
||||||
@page_width = $(window).width()
|
|
||||||
@frame = $("#inner-iframe")
|
|
||||||
@initFixbutton()
|
|
||||||
@dragStarted = 0
|
|
||||||
@globe = null
|
|
||||||
@preload_html = null
|
|
||||||
|
|
||||||
@original_set_site_info = wrapper.setSiteInfo # We going to override this, save the original
|
|
||||||
|
|
||||||
# Start in opened state for debugging
|
|
||||||
if false
|
|
||||||
@startDrag()
|
|
||||||
@moved()
|
|
||||||
@fixbutton_targetx = @fixbutton_initx - @width
|
|
||||||
@stopDrag()
|
|
||||||
|
|
||||||
|
|
||||||
initFixbutton: ->
|
|
||||||
###
|
|
||||||
@fixbutton.on "mousedown touchstart", (e) =>
|
|
||||||
if not @opened
|
|
||||||
@logStart("Preloading")
|
|
||||||
wrapper.ws.cmd "sidebarGetHtmlTag", {}, (res) =>
|
|
||||||
@logEnd("Preloading")
|
|
||||||
@preload_html = res
|
|
||||||
###
|
|
||||||
|
|
||||||
# Detect dragging
|
|
||||||
@fixbutton.on "mousedown touchstart", (e) =>
|
|
||||||
if e.button > 0 # Right or middle click
|
|
||||||
return
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
# Disable previous listeners
|
|
||||||
@fixbutton.off "click touchstop touchcancel"
|
|
||||||
@fixbutton.off "mousemove touchmove"
|
|
||||||
|
|
||||||
# Make sure its not a click
|
|
||||||
@dragStarted = (+ new Date)
|
|
||||||
@fixbutton.one "mousemove touchmove", (e) =>
|
|
||||||
mousex = e.pageX
|
|
||||||
if not mousex
|
|
||||||
mousex = e.originalEvent.touches[0].pageX
|
|
||||||
|
|
||||||
@fixbutton_addx = @fixbutton.offset().left-mousex
|
|
||||||
@startDrag()
|
|
||||||
@fixbutton.parent().on "click touchstop touchcancel", (e) =>
|
|
||||||
@stopDrag()
|
|
||||||
@resized()
|
|
||||||
$(window).on "resize", @resized
|
|
||||||
|
|
||||||
resized: =>
|
|
||||||
@page_width = $(window).width()
|
|
||||||
@fixbutton_initx = @page_width - 75 # Initial x position
|
|
||||||
if @opened
|
|
||||||
@fixbutton.css
|
|
||||||
left: @fixbutton_initx - @width
|
|
||||||
else
|
|
||||||
@fixbutton.css
|
|
||||||
left: @fixbutton_initx
|
|
||||||
|
|
||||||
# Start dragging the fixbutton
|
|
||||||
startDrag: ->
|
|
||||||
@log "startDrag"
|
|
||||||
@fixbutton_targetx = @fixbutton_initx # Fallback x position
|
|
||||||
|
|
||||||
@fixbutton.addClass("dragging")
|
|
||||||
|
|
||||||
# Fullscreen drag bg to capture mouse events over iframe
|
|
||||||
$("<div class='drag-bg'></div>").appendTo(document.body)
|
|
||||||
|
|
||||||
# IE position wrap fix
|
|
||||||
if navigator.userAgent.indexOf('MSIE') != -1 or navigator.appVersion.indexOf('Trident/') > 0
|
|
||||||
@fixbutton.css("pointer-events", "none")
|
|
||||||
|
|
||||||
# Don't go to homepage
|
|
||||||
@fixbutton.one "click", (e) =>
|
|
||||||
@stopDrag()
|
|
||||||
@fixbutton.removeClass("dragging")
|
|
||||||
if Math.abs(@fixbutton.offset().left - @fixbutton_initx) > 5
|
|
||||||
# If moved more than some pixel the button then don't go to homepage
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
# Animate drag
|
|
||||||
@fixbutton.parents().on "mousemove touchmove", @animDrag
|
|
||||||
@fixbutton.parents().on "mousemove touchmove" ,@waitMove
|
|
||||||
|
|
||||||
# Stop dragging listener
|
|
||||||
@fixbutton.parents().on "mouseup touchstop touchend touchcancel", (e) =>
|
|
||||||
e.preventDefault()
|
|
||||||
@stopDrag()
|
|
||||||
|
|
||||||
|
|
||||||
# Wait for moving the fixbutton
|
|
||||||
waitMove: (e) =>
|
|
||||||
if Math.abs(@fixbutton.offset().left - @fixbutton_targetx) > 10 and (+ new Date)-@dragStarted > 100
|
|
||||||
@moved()
|
|
||||||
@fixbutton.parents().off "mousemove touchmove" ,@waitMove
|
|
||||||
|
|
||||||
moved: ->
|
|
||||||
@log "Moved"
|
|
||||||
@createHtmltag()
|
|
||||||
$(document.body).css("perspective", "1000px").addClass("body-sidebar")
|
|
||||||
$(window).off "resize"
|
|
||||||
$(window).on "resize", =>
|
|
||||||
$(document.body).css "height", $(window).height()
|
|
||||||
@scrollable()
|
|
||||||
@resized()
|
|
||||||
$(window).trigger "resize"
|
|
||||||
|
|
||||||
# Override setsiteinfo to catch changes
|
|
||||||
wrapper.setSiteInfo = (site_info) =>
|
|
||||||
@setSiteInfo(site_info)
|
|
||||||
@original_set_site_info.apply(wrapper, arguments)
|
|
||||||
|
|
||||||
# Preload world.jpg
|
|
||||||
img = new Image();
|
|
||||||
img.src = "/uimedia/globe/world.jpg";
|
|
||||||
|
|
||||||
setSiteInfo: (site_info) ->
|
|
||||||
RateLimit 1500, =>
|
|
||||||
@updateHtmlTag()
|
|
||||||
RateLimit 30000, =>
|
|
||||||
@displayGlobe()
|
|
||||||
|
|
||||||
# Create the sidebar html tag
|
|
||||||
createHtmltag: ->
|
|
||||||
@when_loaded = $.Deferred()
|
|
||||||
if not @container
|
|
||||||
@container = $("""
|
|
||||||
<div class="sidebar-container"><div class="sidebar scrollable"><div class="content-wrapper"><div class="content">
|
|
||||||
</div></div></div></div>
|
|
||||||
""")
|
|
||||||
@container.appendTo(document.body)
|
|
||||||
@tag = @container.find(".sidebar")
|
|
||||||
@updateHtmlTag()
|
|
||||||
@scrollable = window.initScrollable()
|
|
||||||
|
|
||||||
|
|
||||||
updateHtmlTag: ->
|
|
||||||
if @preload_html
|
|
||||||
@setHtmlTag(@preload_html)
|
|
||||||
@preload_html = null
|
|
||||||
else
|
|
||||||
wrapper.ws.cmd "sidebarGetHtmlTag", {}, @setHtmlTag
|
|
||||||
|
|
||||||
setHtmlTag: (res) =>
|
|
||||||
if @tag.find(".content").children().length == 0 # First update
|
|
||||||
@log "Creating content"
|
|
||||||
@container.addClass("loaded")
|
|
||||||
morphdom(@tag.find(".content")[0], '<div class="content">'+res+'</div>')
|
|
||||||
# @scrollable()
|
|
||||||
@when_loaded.resolve()
|
|
||||||
|
|
||||||
else # Not first update, patch the html to keep unchanged dom elements
|
|
||||||
@log "Patching content"
|
|
||||||
morphdom @tag.find(".content")[0], '<div class="content">'+res+'</div>', {
|
|
||||||
onBeforeMorphEl: (from_el, to_el) -> # Ignore globe loaded state
|
|
||||||
if from_el.className == "globe" or from_el.className.indexOf("noupdate") >= 0
|
|
||||||
return false
|
|
||||||
else
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
animDrag: (e) =>
|
|
||||||
mousex = e.pageX
|
|
||||||
if not mousex
|
|
||||||
mousex = e.originalEvent.touches[0].pageX
|
|
||||||
|
|
||||||
overdrag = @fixbutton_initx-@width-mousex
|
|
||||||
if overdrag > 0 # Overdragged
|
|
||||||
overdrag_percent = 1+overdrag/300
|
|
||||||
mousex = (mousex + (@fixbutton_initx-@width)*overdrag_percent)/(1+overdrag_percent)
|
|
||||||
targetx = @fixbutton_initx-mousex-@fixbutton_addx
|
|
||||||
|
|
||||||
@fixbutton[0].style.left = (mousex+@fixbutton_addx)+"px"
|
|
||||||
|
|
||||||
if @tag
|
|
||||||
@tag[0].style.transform = "translateX(#{0-targetx}px)"
|
|
||||||
|
|
||||||
# Check if opened
|
|
||||||
if (not @opened and targetx > @width/3) or (@opened and targetx > @width*0.9)
|
|
||||||
@fixbutton_targetx = @fixbutton_initx - @width # Make it opened
|
|
||||||
else
|
|
||||||
@fixbutton_targetx = @fixbutton_initx
|
|
||||||
|
|
||||||
|
|
||||||
# Stop dragging the fixbutton
|
|
||||||
stopDrag: ->
|
|
||||||
@fixbutton.parents().off "mousemove touchmove"
|
|
||||||
@fixbutton.off "mousemove touchmove"
|
|
||||||
@fixbutton.css("pointer-events", "")
|
|
||||||
$(".drag-bg").remove()
|
|
||||||
if not @fixbutton.hasClass("dragging")
|
|
||||||
return
|
|
||||||
@fixbutton.removeClass("dragging")
|
|
||||||
|
|
||||||
# Move back to initial position
|
|
||||||
if @fixbutton_targetx != @fixbutton.offset().left
|
|
||||||
# Animate fixbutton
|
|
||||||
@fixbutton.stop().animate {"left": @fixbutton_targetx}, 500, "easeOutBack", =>
|
|
||||||
# Switch back to auto align
|
|
||||||
if @fixbutton_targetx == @fixbutton_initx # Closed
|
|
||||||
@fixbutton.css("left", "auto")
|
|
||||||
else # Opened
|
|
||||||
@fixbutton.css("left", @fixbutton_targetx)
|
|
||||||
|
|
||||||
$(".fixbutton-bg").trigger "mouseout" # Switch fixbutton back to normal status
|
|
||||||
|
|
||||||
# Animate sidebar and iframe
|
|
||||||
if @fixbutton_targetx == @fixbutton_initx
|
|
||||||
# Closed
|
|
||||||
targetx = 0
|
|
||||||
@opened = false
|
|
||||||
else
|
|
||||||
# Opened
|
|
||||||
targetx = @width
|
|
||||||
if not @opened
|
|
||||||
@when_loaded.done =>
|
|
||||||
@onOpened()
|
|
||||||
@opened = true
|
|
||||||
|
|
||||||
# Revent sidebar transitions
|
|
||||||
if @tag
|
|
||||||
@tag.css("transition", "0.4s ease-out")
|
|
||||||
@tag.css("transform", "translateX(-#{targetx}px)").one transitionEnd, =>
|
|
||||||
@tag.css("transition", "")
|
|
||||||
if not @opened
|
|
||||||
@container.remove()
|
|
||||||
@container = null
|
|
||||||
@tag.remove()
|
|
||||||
@tag = null
|
|
||||||
|
|
||||||
# Revert body transformations
|
|
||||||
@log "stopdrag", "opened:", @opened
|
|
||||||
if not @opened
|
|
||||||
@onClosed()
|
|
||||||
|
|
||||||
|
|
||||||
onOpened: ->
|
|
||||||
@log "Opened"
|
|
||||||
@scrollable()
|
|
||||||
|
|
||||||
# Re-calculate height when site admin opened or closed
|
|
||||||
@tag.find("#checkbox-owned").off("click").on "click", =>
|
|
||||||
setTimeout (=>
|
|
||||||
@scrollable()
|
|
||||||
), 300
|
|
||||||
|
|
||||||
# Site limit button
|
|
||||||
@tag.find("#button-sitelimit").off("click").on "click", =>
|
|
||||||
wrapper.ws.cmd "siteSetLimit", $("#input-sitelimit").val(), =>
|
|
||||||
wrapper.notifications.add "done-sitelimit", "done", "Site storage limit modified!", 5000
|
|
||||||
@updateHtmlTag()
|
|
||||||
return false
|
|
||||||
|
|
||||||
# Database reload
|
|
||||||
@tag.find("#button-dbreload").off("click").on "click", =>
|
|
||||||
wrapper.ws.cmd "dbReload", [], =>
|
|
||||||
wrapper.notifications.add "done-dbreload", "done", "Database schema reloaded!", 5000
|
|
||||||
@updateHtmlTag()
|
|
||||||
return false
|
|
||||||
|
|
||||||
# Database rebuild
|
|
||||||
@tag.find("#button-dbrebuild").off("click").on "click", =>
|
|
||||||
wrapper.notifications.add "done-dbrebuild", "info", "Database rebuilding...."
|
|
||||||
wrapper.ws.cmd "dbRebuild", [], =>
|
|
||||||
wrapper.notifications.add "done-dbrebuild", "done", "Database rebuilt!", 5000
|
|
||||||
@updateHtmlTag()
|
|
||||||
return false
|
|
||||||
|
|
||||||
# Update site
|
|
||||||
@tag.find("#button-update").off("click").on "click", =>
|
|
||||||
@tag.find("#button-update").addClass("loading")
|
|
||||||
wrapper.ws.cmd "siteUpdate", wrapper.site_info.address, =>
|
|
||||||
wrapper.notifications.add "done-updated", "done", "Site updated!", 5000
|
|
||||||
@tag.find("#button-update").removeClass("loading")
|
|
||||||
return false
|
|
||||||
|
|
||||||
# Pause site
|
|
||||||
@tag.find("#button-pause").off("click").on "click", =>
|
|
||||||
@tag.find("#button-pause").addClass("hidden")
|
|
||||||
wrapper.ws.cmd "sitePause", wrapper.site_info.address
|
|
||||||
return false
|
|
||||||
|
|
||||||
# Resume site
|
|
||||||
@tag.find("#button-resume").off("click").on "click", =>
|
|
||||||
@tag.find("#button-resume").addClass("hidden")
|
|
||||||
wrapper.ws.cmd "siteResume", wrapper.site_info.address
|
|
||||||
return false
|
|
||||||
|
|
||||||
# Delete site
|
|
||||||
@tag.find("#button-delete").off("click").on "click", =>
|
|
||||||
wrapper.displayConfirm "Are you sure?", "Delete this site", =>
|
|
||||||
@tag.find("#button-delete").addClass("loading")
|
|
||||||
wrapper.ws.cmd "siteDelete", wrapper.site_info.address, ->
|
|
||||||
document.location = $(".fixbutton-bg").attr("href")
|
|
||||||
return false
|
|
||||||
|
|
||||||
# Owned checkbox
|
|
||||||
@tag.find("#checkbox-owned").off("click").on "click", =>
|
|
||||||
wrapper.ws.cmd "siteSetOwned", [@tag.find("#checkbox-owned").is(":checked")]
|
|
||||||
|
|
||||||
# Owned checkbox
|
|
||||||
@tag.find("#checkbox-autodownloadoptional").off("click").on "click", =>
|
|
||||||
wrapper.ws.cmd "siteSetAutodownloadoptional", [@tag.find("#checkbox-autodownloadoptional").is(":checked")]
|
|
||||||
|
|
||||||
# Change identity button
|
|
||||||
@tag.find("#button-identity").off("click").on "click", =>
|
|
||||||
wrapper.ws.cmd "certSelect"
|
|
||||||
return false
|
|
||||||
|
|
||||||
# Owned checkbox
|
|
||||||
@tag.find("#checkbox-owned").off("click").on "click", =>
|
|
||||||
wrapper.ws.cmd "siteSetOwned", [@tag.find("#checkbox-owned").is(":checked")]
|
|
||||||
|
|
||||||
# Save settings
|
|
||||||
@tag.find("#button-settings").off("click").on "click", =>
|
|
||||||
wrapper.ws.cmd "fileGet", "content.json", (res) =>
|
|
||||||
data = JSON.parse(res)
|
|
||||||
data["title"] = $("#settings-title").val()
|
|
||||||
data["description"] = $("#settings-description").val()
|
|
||||||
json_raw = unescape(encodeURIComponent(JSON.stringify(data, undefined, '\t')))
|
|
||||||
wrapper.ws.cmd "fileWrite", ["content.json", btoa(json_raw), true], (res) =>
|
|
||||||
if res != "ok" # fileWrite failed
|
|
||||||
wrapper.notifications.add "file-write", "error", "File write error: #{res}"
|
|
||||||
else
|
|
||||||
wrapper.notifications.add "file-write", "done", "Site settings saved!", 5000
|
|
||||||
@updateHtmlTag()
|
|
||||||
return false
|
|
||||||
|
|
||||||
# Sign content.json
|
|
||||||
@tag.find("#button-sign").off("click").on "click", =>
|
|
||||||
inner_path = @tag.find("#input-contents").val()
|
|
||||||
|
|
||||||
if wrapper.site_info.privatekey
|
|
||||||
# Privatekey stored in users.json
|
|
||||||
wrapper.ws.cmd "siteSign", {privatekey: "stored", inner_path: inner_path, update_changed_files: true}, (res) =>
|
|
||||||
wrapper.notifications.add "sign", "done", "#{inner_path} Signed!", 5000
|
|
||||||
|
|
||||||
else
|
|
||||||
# Ask the user for privatekey
|
|
||||||
wrapper.displayPrompt "Enter your private key:", "password", "Sign", (privatekey) => # Prompt the private key
|
|
||||||
wrapper.ws.cmd "siteSign", {privatekey: privatekey, inner_path: inner_path, update_changed_files: true}, (res) =>
|
|
||||||
if res == "ok"
|
|
||||||
wrapper.notifications.add "sign", "done", "#{inner_path} Signed!", 5000
|
|
||||||
|
|
||||||
return false
|
|
||||||
|
|
||||||
# Publish content.json
|
|
||||||
@tag.find("#button-publish").off("click").on "click", =>
|
|
||||||
inner_path = @tag.find("#input-contents").val()
|
|
||||||
@tag.find("#button-publish").addClass "loading"
|
|
||||||
wrapper.ws.cmd "sitePublish", {"inner_path": inner_path, "sign": false}, =>
|
|
||||||
@tag.find("#button-publish").removeClass "loading"
|
|
||||||
|
|
||||||
@loadGlobe()
|
|
||||||
|
|
||||||
|
|
||||||
onClosed: ->
|
|
||||||
$(window).off "resize"
|
|
||||||
$(window).on "resize", @resized
|
|
||||||
$(document.body).css("transition", "0.6s ease-in-out").removeClass("body-sidebar").on transitionEnd, (e) =>
|
|
||||||
if e.target == document.body
|
|
||||||
$(document.body).css("height", "auto").css("perspective", "").css("transition", "").off transitionEnd
|
|
||||||
@unloadGlobe()
|
|
||||||
|
|
||||||
# We dont need site info anymore
|
|
||||||
wrapper.setSiteInfo = @original_set_site_info
|
|
||||||
|
|
||||||
|
|
||||||
loadGlobe: =>
|
|
||||||
console.log "loadGlobe", @tag.find(".globe").hasClass("loading")
|
|
||||||
if @tag.find(".globe").hasClass("loading")
|
|
||||||
setTimeout (=>
|
|
||||||
if typeof(DAT) == "undefined" # Globe script not loaded, do it first
|
|
||||||
$.getScript("/uimedia/globe/all.js", @displayGlobe)
|
|
||||||
else
|
|
||||||
@displayGlobe()
|
|
||||||
), 600
|
|
||||||
|
|
||||||
|
|
||||||
displayGlobe: =>
|
|
||||||
img = new Image();
|
|
||||||
img.src = "/uimedia/globe/world.jpg";
|
|
||||||
img.onload = =>
|
|
||||||
wrapper.ws.cmd "sidebarGetPeers", [], (globe_data) =>
|
|
||||||
if @globe
|
|
||||||
@globe.scene.remove(@globe.points)
|
|
||||||
@globe.addData( globe_data, {format: 'magnitude', name: "hello", animated: false} )
|
|
||||||
@globe.createPoints()
|
|
||||||
else if typeof(DAT) != "undefined"
|
|
||||||
try
|
|
||||||
@globe = new DAT.Globe( @tag.find(".globe")[0], {"imgDir": "/uimedia/globe/"} )
|
|
||||||
@globe.addData( globe_data, {format: 'magnitude', name: "hello"} )
|
|
||||||
@globe.createPoints()
|
|
||||||
@globe.animate()
|
|
||||||
catch e
|
|
||||||
console.log "WebGL error", e
|
|
||||||
@tag?.find(".globe").addClass("error").text("WebGL not supported")
|
|
||||||
|
|
||||||
@tag?.find(".globe").removeClass("loading")
|
|
||||||
|
|
||||||
|
|
||||||
unloadGlobe: =>
|
|
||||||
if not @globe
|
|
||||||
return false
|
|
||||||
@globe.unload()
|
|
||||||
@globe = null
|
|
||||||
|
|
||||||
|
|
||||||
setTimeout ( ->
|
|
||||||
window.sidebar = new Sidebar()
|
|
||||||
), 500
|
|
||||||
window.transitionEnd = 'transitionend webkitTransitionEnd oTransitionEnd otransitionend'
|
|
|
@ -1,124 +0,0 @@
|
||||||
.drag-bg { width: 100%; height: 100%; position: fixed; }
|
|
||||||
.fixbutton.dragging { cursor: -webkit-grabbing; }
|
|
||||||
.fixbutton-bg:active { cursor: -webkit-grabbing; }
|
|
||||||
|
|
||||||
|
|
||||||
.body-sidebar { background-color: #666 !important; }
|
|
||||||
#inner-iframe { transition: 0.3s ease-in-out; transform-origin: left; outline: 1px solid transparent; }
|
|
||||||
.body-sidebar iframe { transform: rotateY(5deg); opacity: 0.8; pointer-events: none } /* translateX(-200px) scale(0.95)*/
|
|
||||||
|
|
||||||
/* SIDEBAR */
|
|
||||||
|
|
||||||
.sidebar-container { width: 100%; height: 100%; overflow: hidden; position: fixed; }
|
|
||||||
.sidebar { background-color: #212121; position: fixed; backface-visibility: hidden; right: -1200px; height: 100%; width: 1200px; } /*box-shadow: inset 0px 0px 10px #000*/
|
|
||||||
.sidebar .content { margin: 30px; font-family: "Segoe UI Light", "Segoe UI", "Helvetica Neue"; color: white; width: 375px; height: 300px; font-weight: 200; transition: all 1s; opacity: 0 }
|
|
||||||
.sidebar-container.loaded .content { opacity: 1; transform: none }
|
|
||||||
.sidebar h1, .sidebar h2 { font-weight: lighter; }
|
|
||||||
.sidebar .button { margin: 0px; display: inline-block; transition: all 0.3s; box-sizing: border-box; max-width: 260px }
|
|
||||||
.sidebar .button.hidden { padding: 0px; max-width: 0px; opacity: 0; pointer-events: none }
|
|
||||||
.sidebar #button-delete { background-color: transparent; border: 1px solid #333; color: #AAA; margin-left: 10px }
|
|
||||||
.sidebar #button-delete:hover { border: 1px solid #666; color: white }
|
|
||||||
|
|
||||||
.sidebar .flex { display: flex }
|
|
||||||
.sidebar .flex .input.text, .sidebar .flex input.text { width: 100%; }
|
|
||||||
.sidebar .flex .button { margin-left: 4px; white-space: nowrap; }
|
|
||||||
|
|
||||||
/* FIELDS */
|
|
||||||
|
|
||||||
.sidebar .fields { padding: 0px; list-style-type: none; width: 355px; }
|
|
||||||
.sidebar .fields > li, .sidebar .fields .settings-owned > li { margin-bottom: 30px }
|
|
||||||
.sidebar .fields > li:after, .sidebar .fields .settings-owned > li:after { clear: both; content: ''; display: block }
|
|
||||||
.sidebar .fields label {
|
|
||||||
font-family: Consolas, monospace; text-transform: uppercase; font-size: 13px; color: #ACACAC; display: inline-block; margin-bottom: 10px;
|
|
||||||
vertical-align: text-bottom; margin-right: 10px; width: 100%
|
|
||||||
}
|
|
||||||
.sidebar .fields label small { font-weight: normal; color: white; text-transform: none; }
|
|
||||||
.sidebar .fields .text { background-color: black; border: 0px; padding: 10px; color: white; border-radius: 3px; width: 260px; font-family: Consolas, monospace; }
|
|
||||||
.sidebar .fields .text.long { width: 330px; font-size: 72%; }
|
|
||||||
.sidebar .fields .disabled { color: #AAA; background-color: #3B3B3B; }
|
|
||||||
.sidebar .fields .text-num { width: 30px; text-align: right; padding-right: 30px; }
|
|
||||||
.sidebar .fields .text-post { color: white; font-family: Consolas, monospace; display: inline-block; font-size: 13px; margin-left: -25px; width: 25px; }
|
|
||||||
|
|
||||||
/* Select */
|
|
||||||
.sidebar .fields select {
|
|
||||||
width: 225px; background-color: #3B3B3B; color: white; font-family: Consolas, monospace; appearance: none;
|
|
||||||
padding: 5px; padding-right: 25px; border: 0px; border-radius: 3px; height: 35px; vertical-align: 1px; box-shadow: 0px 1px 2px rgba(0,0,0,0.5);
|
|
||||||
}
|
|
||||||
.sidebar .fields .select-down { margin-left: -39px; width: 34px; display: inline-block; transform: rotateZ(90deg); height: 35px; vertical-align: -8px; pointer-events: none; font-weight: bold }
|
|
||||||
|
|
||||||
/* Checkbox */
|
|
||||||
.sidebar .fields .checkbox { width: 50px; height: 24px; position: relative; z-index: 999; opacity: 0; }
|
|
||||||
.sidebar .fields .checkbox-skin { background-color: #CCC; width: 50px; height: 24px; border-radius: 15px; transition: all 0.3s ease-in-out; display: inline-block; margin-left: -59px; }
|
|
||||||
.sidebar .fields .checkbox-skin:before {
|
|
||||||
content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; border-radius: 100%; margin-top: 2px; margin-left: 2px;
|
|
||||||
transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86);
|
|
||||||
}
|
|
||||||
.sidebar .fields .checkbox:checked ~ .checkbox-skin:before { margin-left: 27px; }
|
|
||||||
.sidebar .fields .checkbox:checked ~ .checkbox-skin { background-color: #2ECC71; }
|
|
||||||
|
|
||||||
/* Fake input */
|
|
||||||
.sidebar .input { font-size: 13px; width: 250px; display: inline-block; overflow: hidden; text-overflow: ellipsis; vertical-align: top }
|
|
||||||
|
|
||||||
/* GRAPH */
|
|
||||||
|
|
||||||
.graph { padding: 0px; list-style-type: none; width: 351px; background-color: black; height: 10px; border-radius: 8px; overflow: hidden; position: relative; font-size: 0 }
|
|
||||||
.graph li { height: 100%; position: absolute; transition: all 0.3s; }
|
|
||||||
.graph-stacked { white-space: nowrap; }
|
|
||||||
.graph-stacked li { position: static; display: inline-block; height: 20px }
|
|
||||||
|
|
||||||
.graph-legend { padding: 0px; list-style-type: none; margin-top: 13px; font-family: Consolas, "Andale Mono", monospace; font-size: 13px; text-transform: capitalize; }
|
|
||||||
.sidebar .graph-legend li { margin: 0px; margin-top: 5px; margin-left: 0px; width: 160px; float: left; position: relative; }
|
|
||||||
.sidebar .graph-legend li:nth-child(odd) { margin-right: 29px }
|
|
||||||
.graph-legend span { position: absolute; }
|
|
||||||
.graph-legend b { text-align: right; display: inline-block; width: 50px; float: right; font-weight: normal; }
|
|
||||||
.graph-legend li:before { content: '\2022'; font-size: 23px; line-height: 0px; vertical-align: -3px; margin-right: 5px; }
|
|
||||||
|
|
||||||
.filelist { font-size: 12px; font-family: monospace; margin: 0px; padding: 0px; list-style-type: none; line-height: 1.5em; }
|
|
||||||
.filelist li:before { content: '\2022'; font-size: 11px; line-height: 0px; vertical-align: 0px; margin-right: 5px; color: #FFBE00; }
|
|
||||||
.filelist li { overflow: hidden; text-overflow: ellipsis; }
|
|
||||||
|
|
||||||
/* COLORS */
|
|
||||||
|
|
||||||
.back-green { background-color: #2ECC71 }
|
|
||||||
.color-green:before { color: #2ECC71 }
|
|
||||||
.back-blue { background-color: #3BAFDA }
|
|
||||||
.color-blue:before { color: #3BAFDA }
|
|
||||||
.back-darkblue { background-color: #156fb7 }
|
|
||||||
.color-darkblue:before { color: #156fb7 }
|
|
||||||
.back-purple { background-color: #B10DC9 }
|
|
||||||
.color-purple:before { color: #B10DC9 }
|
|
||||||
.back-yellow { background-color: #FFDC00 }
|
|
||||||
.color-yellow:before { color: #FFDC00 }
|
|
||||||
.back-orange { background-color: #FF9800 }
|
|
||||||
.color-orange:before { color: #FF9800 }
|
|
||||||
.back-gray { background-color: #ECF0F1 }
|
|
||||||
.color-gray:before { color: #ECF0F1 }
|
|
||||||
.back-black { background-color: #34495E }
|
|
||||||
.color-black:before { color: #34495E }
|
|
||||||
.back-red { background-color: #5E4934 }
|
|
||||||
.color-red:before { color: #5E4934 }
|
|
||||||
.back-gray { background-color: #9e9e9e }
|
|
||||||
.color-gray:before { color: #9e9e9e }
|
|
||||||
.back-white { background-color: #EEE }
|
|
||||||
.color-white:before { color: #EEE }
|
|
||||||
.back-red { background-color: #E91E63 }
|
|
||||||
.color-red:before { color: #E91E63 }
|
|
||||||
|
|
||||||
|
|
||||||
/* Settings owned */
|
|
||||||
|
|
||||||
.owned-title { float: left }
|
|
||||||
#checkbox-owned { margin-bottom: 25px; margin-top: 26px; margin-left: 11px; }
|
|
||||||
.settings-owned { clear: both }
|
|
||||||
#checkbox-owned ~ .settings-owned { opacity: 0; max-height: 0px; transition: all 0.3s linear; overflow: hidden }
|
|
||||||
#checkbox-owned:checked ~ .settings-owned { opacity: 1; max-height: 400px }
|
|
||||||
|
|
||||||
/* Globe */
|
|
||||||
.globe { width: 360px; height: 360px }
|
|
||||||
.globe.loading { background: url(/uimedia/img/loading-circle.gif) center center no-repeat }
|
|
||||||
.globe.error { text-align: center; padding-top: 156px; box-sizing: border-box; opacity: 0.2; }
|
|
||||||
|
|
||||||
/* Sign publish */
|
|
||||||
.contents { background-color: #3B3B3B; color: white; padding: 7px 10px; font-family: Consolas; font-size: 11px; display: inline-block; margin-bottom: 6px; }
|
|
||||||
.contents a { color: white }
|
|
||||||
.contents a:active { background-color: #6B6B6B }
|
|
|
@ -1,178 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
/* ---- plugins/Sidebar/media/Scrollbable.css ---- */
|
|
||||||
|
|
||||||
|
|
||||||
.scrollable {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollable.showScroll::after {
|
|
||||||
position: absolute;
|
|
||||||
content: '';
|
|
||||||
top: 5%;
|
|
||||||
right: 7px;
|
|
||||||
height: 90%;
|
|
||||||
width: 3px;
|
|
||||||
background: rgba(224, 224, 255, .3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollable .content-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding-right: 50%;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
.scroller {
|
|
||||||
margin-top: 5px;
|
|
||||||
z-index: 5;
|
|
||||||
cursor: pointer;
|
|
||||||
position: absolute;
|
|
||||||
width: 7px;
|
|
||||||
-webkit-border-radius: 5px; -moz-border-radius: 5px; -o-border-radius: 5px; -ms-border-radius: 5px; border-radius: 5px ;
|
|
||||||
background: #151515;
|
|
||||||
top: 0px;
|
|
||||||
left: 395px;
|
|
||||||
-webkit-transition: top .08s;
|
|
||||||
-moz-transition: top .08s;
|
|
||||||
-ms-transition: top .08s;
|
|
||||||
-o-transition: top .08s;
|
|
||||||
-webkit-transition: top .08s; -moz-transition: top .08s; -o-transition: top .08s; -ms-transition: top .08s; transition: top .08s ;
|
|
||||||
}
|
|
||||||
.scroller {
|
|
||||||
-webkit-touch-callout: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-khtml-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ---- plugins/Sidebar/media/Sidebar.css ---- */
|
|
||||||
|
|
||||||
|
|
||||||
.drag-bg { width: 100%; height: 100%; position: fixed; }
|
|
||||||
.fixbutton.dragging { cursor: -webkit-grabbing; }
|
|
||||||
.fixbutton-bg:active { cursor: -webkit-grabbing; }
|
|
||||||
|
|
||||||
|
|
||||||
.body-sidebar { background-color: #666 !important; }
|
|
||||||
#inner-iframe { -webkit-transition: 0.3s ease-in-out; -moz-transition: 0.3s ease-in-out; -o-transition: 0.3s ease-in-out; -ms-transition: 0.3s ease-in-out; transition: 0.3s ease-in-out ; transform-origin: left; outline: 1px solid transparent; }
|
|
||||||
.body-sidebar iframe { -webkit-transform: rotateY(5deg); -moz-transform: rotateY(5deg); -o-transform: rotateY(5deg); -ms-transform: rotateY(5deg); transform: rotateY(5deg) ; opacity: 0.8; pointer-events: none } /* translateX(-200px) scale(0.95)*/
|
|
||||||
|
|
||||||
/* SIDEBAR */
|
|
||||||
|
|
||||||
.sidebar-container { width: 100%; height: 100%; overflow: hidden; position: fixed; }
|
|
||||||
.sidebar { background-color: #212121; position: fixed; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; right: -1200px; height: 100%; width: 1200px; } /*box-shadow: inset 0px 0px 10px #000*/
|
|
||||||
.sidebar .content { margin: 30px; font-family: "Segoe UI Light", "Segoe UI", "Helvetica Neue"; color: white; width: 375px; height: 300px; font-weight: 200; -webkit-transition: all 1s; -moz-transition: all 1s; -o-transition: all 1s; -ms-transition: all 1s; transition: all 1s ; opacity: 0 }
|
|
||||||
.sidebar-container.loaded .content { opacity: 1; -webkit-transform: none ; -moz-transform: none ; -o-transform: none ; -ms-transform: none ; transform: none }
|
|
||||||
.sidebar h1, .sidebar h2 { font-weight: lighter; }
|
|
||||||
.sidebar .button { margin: 0px; display: inline-block; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -o-transition: all 0.3s; -ms-transition: all 0.3s; transition: all 0.3s ; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; max-width: 260px }
|
|
||||||
.sidebar .button.hidden { padding: 0px; max-width: 0px; opacity: 0; pointer-events: none }
|
|
||||||
.sidebar #button-delete { background-color: transparent; border: 1px solid #333; color: #AAA; margin-left: 10px }
|
|
||||||
.sidebar #button-delete:hover { border: 1px solid #666; color: white }
|
|
||||||
|
|
||||||
.sidebar .flex { display: flex }
|
|
||||||
.sidebar .flex .input.text, .sidebar .flex input.text { width: 100%; }
|
|
||||||
.sidebar .flex .button { margin-left: 4px; white-space: nowrap; }
|
|
||||||
|
|
||||||
/* FIELDS */
|
|
||||||
|
|
||||||
.sidebar .fields { padding: 0px; list-style-type: none; width: 355px; }
|
|
||||||
.sidebar .fields > li, .sidebar .fields .settings-owned > li { margin-bottom: 30px }
|
|
||||||
.sidebar .fields > li:after, .sidebar .fields .settings-owned > li:after { clear: both; content: ''; display: block }
|
|
||||||
.sidebar .fields label {
|
|
||||||
font-family: Consolas, monospace; text-transform: uppercase; font-size: 13px; color: #ACACAC; display: inline-block; margin-bottom: 10px;
|
|
||||||
vertical-align: text-bottom; margin-right: 10px; width: 100%
|
|
||||||
}
|
|
||||||
.sidebar .fields label small { font-weight: normal; color: white; text-transform: none; }
|
|
||||||
.sidebar .fields .text { background-color: black; border: 0px; padding: 10px; color: white; -webkit-border-radius: 3px; -moz-border-radius: 3px; -o-border-radius: 3px; -ms-border-radius: 3px; border-radius: 3px ; width: 260px; font-family: Consolas, monospace; }
|
|
||||||
.sidebar .fields .text.long { width: 330px; font-size: 72%; }
|
|
||||||
.sidebar .fields .disabled { color: #AAA; background-color: #3B3B3B; }
|
|
||||||
.sidebar .fields .text-num { width: 30px; text-align: right; padding-right: 30px; }
|
|
||||||
.sidebar .fields .text-post { color: white; font-family: Consolas, monospace; display: inline-block; font-size: 13px; margin-left: -25px; width: 25px; }
|
|
||||||
|
|
||||||
/* Select */
|
|
||||||
.sidebar .fields select {
|
|
||||||
width: 225px; background-color: #3B3B3B; color: white; font-family: Consolas, monospace; -webkit-appearance: none; -moz-appearance: none; -o-appearance: none; -ms-appearance: none; appearance: none ;
|
|
||||||
padding: 5px; padding-right: 25px; border: 0px; -webkit-border-radius: 3px; -moz-border-radius: 3px; -o-border-radius: 3px; -ms-border-radius: 3px; border-radius: 3px ; height: 35px; vertical-align: 1px; -webkit-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); -moz-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); -o-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); -ms-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); box-shadow: 0px 1px 2px rgba(0,0,0,0.5) ;
|
|
||||||
}
|
|
||||||
.sidebar .fields .select-down { margin-left: -39px; width: 34px; display: inline-block; -webkit-transform: rotateZ(90deg); -moz-transform: rotateZ(90deg); -o-transform: rotateZ(90deg); -ms-transform: rotateZ(90deg); transform: rotateZ(90deg) ; height: 35px; vertical-align: -8px; pointer-events: none; font-weight: bold }
|
|
||||||
|
|
||||||
/* Checkbox */
|
|
||||||
.sidebar .fields .checkbox { width: 50px; height: 24px; position: relative; z-index: 999; opacity: 0; }
|
|
||||||
.sidebar .fields .checkbox-skin { background-color: #CCC; width: 50px; height: 24px; -webkit-border-radius: 15px; -moz-border-radius: 15px; -o-border-radius: 15px; -ms-border-radius: 15px; border-radius: 15px ; -webkit-transition: all 0.3s ease-in-out; -moz-transition: all 0.3s ease-in-out; -o-transition: all 0.3s ease-in-out; -ms-transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out ; display: inline-block; margin-left: -59px; }
|
|
||||||
.sidebar .fields .checkbox-skin:before {
|
|
||||||
content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; -webkit-border-radius: 100%; -moz-border-radius: 100%; -o-border-radius: 100%; -ms-border-radius: 100%; border-radius: 100% ; margin-top: 2px; margin-left: 2px;
|
|
||||||
-webkit-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -moz-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -o-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -ms-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86) ;
|
|
||||||
}
|
|
||||||
.sidebar .fields .checkbox:checked ~ .checkbox-skin:before { margin-left: 27px; }
|
|
||||||
.sidebar .fields .checkbox:checked ~ .checkbox-skin { background-color: #2ECC71; }
|
|
||||||
|
|
||||||
/* Fake input */
|
|
||||||
.sidebar .input { font-size: 13px; width: 250px; display: inline-block; overflow: hidden; text-overflow: ellipsis; vertical-align: top }
|
|
||||||
|
|
||||||
/* GRAPH */
|
|
||||||
|
|
||||||
.graph { padding: 0px; list-style-type: none; width: 351px; background-color: black; height: 10px; -webkit-border-radius: 8px; -moz-border-radius: 8px; -o-border-radius: 8px; -ms-border-radius: 8px; border-radius: 8px ; overflow: hidden; position: relative; font-size: 0 }
|
|
||||||
.graph li { height: 100%; position: absolute; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -o-transition: all 0.3s; -ms-transition: all 0.3s; transition: all 0.3s ; }
|
|
||||||
.graph-stacked { white-space: nowrap; }
|
|
||||||
.graph-stacked li { position: static; display: inline-block; height: 20px }
|
|
||||||
|
|
||||||
.graph-legend { padding: 0px; list-style-type: none; margin-top: 13px; font-family: Consolas, "Andale Mono", monospace; font-size: 13px; text-transform: capitalize; }
|
|
||||||
.sidebar .graph-legend li { margin: 0px; margin-top: 5px; margin-left: 0px; width: 160px; float: left; position: relative; }
|
|
||||||
.sidebar .graph-legend li:nth-child(odd) { margin-right: 29px }
|
|
||||||
.graph-legend span { position: absolute; }
|
|
||||||
.graph-legend b { text-align: right; display: inline-block; width: 50px; float: right; font-weight: normal; }
|
|
||||||
.graph-legend li:before { content: '\2022'; font-size: 23px; line-height: 0px; vertical-align: -3px; margin-right: 5px; }
|
|
||||||
|
|
||||||
.filelist { font-size: 12px; font-family: monospace; margin: 0px; padding: 0px; list-style-type: none; line-height: 1.5em; }
|
|
||||||
.filelist li:before { content: '\2022'; font-size: 11px; line-height: 0px; vertical-align: 0px; margin-right: 5px; color: #FFBE00; }
|
|
||||||
.filelist li { overflow: hidden; text-overflow: ellipsis; }
|
|
||||||
|
|
||||||
/* COLORS */
|
|
||||||
|
|
||||||
.back-green { background-color: #2ECC71 }
|
|
||||||
.color-green:before { color: #2ECC71 }
|
|
||||||
.back-blue { background-color: #3BAFDA }
|
|
||||||
.color-blue:before { color: #3BAFDA }
|
|
||||||
.back-darkblue { background-color: #156fb7 }
|
|
||||||
.color-darkblue:before { color: #156fb7 }
|
|
||||||
.back-purple { background-color: #B10DC9 }
|
|
||||||
.color-purple:before { color: #B10DC9 }
|
|
||||||
.back-yellow { background-color: #FFDC00 }
|
|
||||||
.color-yellow:before { color: #FFDC00 }
|
|
||||||
.back-orange { background-color: #FF9800 }
|
|
||||||
.color-orange:before { color: #FF9800 }
|
|
||||||
.back-gray { background-color: #ECF0F1 }
|
|
||||||
.color-gray:before { color: #ECF0F1 }
|
|
||||||
.back-black { background-color: #34495E }
|
|
||||||
.color-black:before { color: #34495E }
|
|
||||||
.back-red { background-color: #5E4934 }
|
|
||||||
.color-red:before { color: #5E4934 }
|
|
||||||
.back-gray { background-color: #9e9e9e }
|
|
||||||
.color-gray:before { color: #9e9e9e }
|
|
||||||
.back-white { background-color: #EEE }
|
|
||||||
.color-white:before { color: #EEE }
|
|
||||||
.back-red { background-color: #E91E63 }
|
|
||||||
.color-red:before { color: #E91E63 }
|
|
||||||
|
|
||||||
|
|
||||||
/* Settings owned */
|
|
||||||
|
|
||||||
.owned-title { float: left }
|
|
||||||
#checkbox-owned { margin-bottom: 25px; margin-top: 26px; margin-left: 11px; }
|
|
||||||
.settings-owned { clear: both }
|
|
||||||
#checkbox-owned ~ .settings-owned { opacity: 0; max-height: 0px; -webkit-transition: all 0.3s linear; -moz-transition: all 0.3s linear; -o-transition: all 0.3s linear; -ms-transition: all 0.3s linear; transition: all 0.3s linear ; overflow: hidden }
|
|
||||||
#checkbox-owned:checked ~ .settings-owned { opacity: 1; max-height: 400px }
|
|
||||||
|
|
||||||
/* Globe */
|
|
||||||
.globe { width: 360px; height: 360px }
|
|
||||||
.globe.loading { background: url(/uimedia/img/loading-circle.gif) center center no-repeat }
|
|
||||||
.globe.error { text-align: center; padding-top: 156px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; opacity: 0.2; }
|
|
||||||
|
|
||||||
/* Sign publish */
|
|
||||||
.contents { background-color: #3B3B3B; color: white; padding: 7px 10px; font-family: Consolas; font-size: 11px; display: inline-block; margin-bottom: 6px; }
|
|
||||||
.contents a { color: white }
|
|
||||||
.contents a:active { background-color: #6B6B6B }
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,340 +0,0 @@
|
||||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.morphdom = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
|
||||||
var specialElHandlers = {
|
|
||||||
/**
|
|
||||||
* Needed for IE. Apparently IE doesn't think
|
|
||||||
* that "selected" is an attribute when reading
|
|
||||||
* over the attributes using selectEl.attributes
|
|
||||||
*/
|
|
||||||
OPTION: function(fromEl, toEl) {
|
|
||||||
if ((fromEl.selected = toEl.selected)) {
|
|
||||||
fromEl.setAttribute('selected', '');
|
|
||||||
} else {
|
|
||||||
fromEl.removeAttribute('selected', '');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* The "value" attribute is special for the <input> element
|
|
||||||
* since it sets the initial value. Changing the "value"
|
|
||||||
* attribute without changing the "value" property will have
|
|
||||||
* no effect since it is only used to the set the initial value.
|
|
||||||
* Similar for the "checked" attribute.
|
|
||||||
*/
|
|
||||||
/*INPUT: function(fromEl, toEl) {
|
|
||||||
fromEl.checked = toEl.checked;
|
|
||||||
fromEl.value = toEl.value;
|
|
||||||
|
|
||||||
if (!toEl.hasAttribute('checked')) {
|
|
||||||
fromEl.removeAttribute('checked');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!toEl.hasAttribute('value')) {
|
|
||||||
fromEl.removeAttribute('value');
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
};
|
|
||||||
|
|
||||||
function noop() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loop over all of the attributes on the target node and make sure the
|
|
||||||
* original DOM node has the same attributes. If an attribute
|
|
||||||
* found on the original node is not on the new node then remove it from
|
|
||||||
* the original node
|
|
||||||
* @param {HTMLElement} fromNode
|
|
||||||
* @param {HTMLElement} toNode
|
|
||||||
*/
|
|
||||||
function morphAttrs(fromNode, toNode) {
|
|
||||||
var attrs = toNode.attributes;
|
|
||||||
var i;
|
|
||||||
var attr;
|
|
||||||
var attrName;
|
|
||||||
var attrValue;
|
|
||||||
var foundAttrs = {};
|
|
||||||
|
|
||||||
for (i=attrs.length-1; i>=0; i--) {
|
|
||||||
attr = attrs[i];
|
|
||||||
if (attr.specified !== false) {
|
|
||||||
attrName = attr.name;
|
|
||||||
attrValue = attr.value;
|
|
||||||
foundAttrs[attrName] = true;
|
|
||||||
|
|
||||||
if (fromNode.getAttribute(attrName) !== attrValue) {
|
|
||||||
fromNode.setAttribute(attrName, attrValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete any extra attributes found on the original DOM element that weren't
|
|
||||||
// found on the target element.
|
|
||||||
attrs = fromNode.attributes;
|
|
||||||
|
|
||||||
for (i=attrs.length-1; i>=0; i--) {
|
|
||||||
attr = attrs[i];
|
|
||||||
if (attr.specified !== false) {
|
|
||||||
attrName = attr.name;
|
|
||||||
if (!foundAttrs.hasOwnProperty(attrName)) {
|
|
||||||
fromNode.removeAttribute(attrName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies the children of one DOM element to another DOM element
|
|
||||||
*/
|
|
||||||
function moveChildren(from, to) {
|
|
||||||
var curChild = from.firstChild;
|
|
||||||
while(curChild) {
|
|
||||||
var nextChild = curChild.nextSibling;
|
|
||||||
to.appendChild(curChild);
|
|
||||||
curChild = nextChild;
|
|
||||||
}
|
|
||||||
return to;
|
|
||||||
}
|
|
||||||
|
|
||||||
function morphdom(fromNode, toNode, options) {
|
|
||||||
if (!options) {
|
|
||||||
options = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof toNode === 'string') {
|
|
||||||
var newBodyEl = document.createElement('body');
|
|
||||||
newBodyEl.innerHTML = toNode;
|
|
||||||
toNode = newBodyEl.childNodes[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
var savedEls = {}; // Used to save off DOM elements with IDs
|
|
||||||
var unmatchedEls = {};
|
|
||||||
var onNodeDiscarded = options.onNodeDiscarded || noop;
|
|
||||||
var onBeforeMorphEl = options.onBeforeMorphEl || noop;
|
|
||||||
var onBeforeMorphElChildren = options.onBeforeMorphElChildren || noop;
|
|
||||||
|
|
||||||
function removeNodeHelper(node, nestedInSavedEl) {
|
|
||||||
var id = node.id;
|
|
||||||
// If the node has an ID then save it off since we will want
|
|
||||||
// to reuse it in case the target DOM tree has a DOM element
|
|
||||||
// with the same ID
|
|
||||||
if (id) {
|
|
||||||
savedEls[id] = node;
|
|
||||||
} else if (!nestedInSavedEl) {
|
|
||||||
// If we are not nested in a saved element then we know that this node has been
|
|
||||||
// completely discarded and will not exist in the final DOM.
|
|
||||||
onNodeDiscarded(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.nodeType === 1) {
|
|
||||||
var curChild = node.firstChild;
|
|
||||||
while(curChild) {
|
|
||||||
removeNodeHelper(curChild, nestedInSavedEl || id);
|
|
||||||
curChild = curChild.nextSibling;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function walkDiscardedChildNodes(node) {
|
|
||||||
if (node.nodeType === 1) {
|
|
||||||
var curChild = node.firstChild;
|
|
||||||
while(curChild) {
|
|
||||||
|
|
||||||
|
|
||||||
if (!curChild.id) {
|
|
||||||
// We only want to handle nodes that don't have an ID to avoid double
|
|
||||||
// walking the same saved element.
|
|
||||||
|
|
||||||
onNodeDiscarded(curChild);
|
|
||||||
|
|
||||||
// Walk recursively
|
|
||||||
walkDiscardedChildNodes(curChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
curChild = curChild.nextSibling;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeNode(node, parentNode, alreadyVisited) {
|
|
||||||
parentNode.removeChild(node);
|
|
||||||
|
|
||||||
if (alreadyVisited) {
|
|
||||||
if (!node.id) {
|
|
||||||
onNodeDiscarded(node);
|
|
||||||
walkDiscardedChildNodes(node);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
removeNodeHelper(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function morphEl(fromNode, toNode, alreadyVisited) {
|
|
||||||
if (toNode.id) {
|
|
||||||
// If an element with an ID is being morphed then it is will be in the final
|
|
||||||
// DOM so clear it out of the saved elements collection
|
|
||||||
delete savedEls[toNode.id];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onBeforeMorphEl(fromNode, toNode) === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
morphAttrs(fromNode, toNode);
|
|
||||||
|
|
||||||
if (onBeforeMorphElChildren(fromNode, toNode) === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var curToNodeChild = toNode.firstChild;
|
|
||||||
var curFromNodeChild = fromNode.firstChild;
|
|
||||||
var curToNodeId;
|
|
||||||
|
|
||||||
var fromNextSibling;
|
|
||||||
var toNextSibling;
|
|
||||||
var savedEl;
|
|
||||||
var unmatchedEl;
|
|
||||||
|
|
||||||
outer: while(curToNodeChild) {
|
|
||||||
toNextSibling = curToNodeChild.nextSibling;
|
|
||||||
curToNodeId = curToNodeChild.id;
|
|
||||||
|
|
||||||
while(curFromNodeChild) {
|
|
||||||
var curFromNodeId = curFromNodeChild.id;
|
|
||||||
fromNextSibling = curFromNodeChild.nextSibling;
|
|
||||||
|
|
||||||
if (!alreadyVisited) {
|
|
||||||
if (curFromNodeId && (unmatchedEl = unmatchedEls[curFromNodeId])) {
|
|
||||||
unmatchedEl.parentNode.replaceChild(curFromNodeChild, unmatchedEl);
|
|
||||||
morphEl(curFromNodeChild, unmatchedEl, alreadyVisited);
|
|
||||||
curFromNodeChild = fromNextSibling;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var curFromNodeType = curFromNodeChild.nodeType;
|
|
||||||
|
|
||||||
if (curFromNodeType === curToNodeChild.nodeType) {
|
|
||||||
var isCompatible = false;
|
|
||||||
|
|
||||||
if (curFromNodeType === 1) { // Both nodes being compared are Element nodes
|
|
||||||
if (curFromNodeChild.tagName === curToNodeChild.tagName) {
|
|
||||||
// We have compatible DOM elements
|
|
||||||
if (curFromNodeId || curToNodeId) {
|
|
||||||
// If either DOM element has an ID then we handle
|
|
||||||
// those differently since we want to match up
|
|
||||||
// by ID
|
|
||||||
if (curToNodeId === curFromNodeId) {
|
|
||||||
isCompatible = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isCompatible = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCompatible) {
|
|
||||||
// We found compatible DOM elements so add a
|
|
||||||
// task to morph the compatible DOM elements
|
|
||||||
morphEl(curFromNodeChild, curToNodeChild, alreadyVisited);
|
|
||||||
}
|
|
||||||
} else if (curFromNodeType === 3) { // Both nodes being compared are Text nodes
|
|
||||||
isCompatible = true;
|
|
||||||
curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCompatible) {
|
|
||||||
curToNodeChild = toNextSibling;
|
|
||||||
curFromNodeChild = fromNextSibling;
|
|
||||||
continue outer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No compatible match so remove the old node from the DOM
|
|
||||||
removeNode(curFromNodeChild, fromNode, alreadyVisited);
|
|
||||||
|
|
||||||
curFromNodeChild = fromNextSibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (curToNodeId) {
|
|
||||||
if ((savedEl = savedEls[curToNodeId])) {
|
|
||||||
morphEl(savedEl, curToNodeChild, true);
|
|
||||||
curToNodeChild = savedEl; // We want to append the saved element instead
|
|
||||||
} else {
|
|
||||||
// The current DOM element in the target tree has an ID
|
|
||||||
// but we did not find a match in any of the corresponding
|
|
||||||
// siblings. We just put the target element in the old DOM tree
|
|
||||||
// but if we later find an element in the old DOM tree that has
|
|
||||||
// a matching ID then we will replace the target element
|
|
||||||
// with the corresponding old element and morph the old element
|
|
||||||
unmatchedEls[curToNodeId] = curToNodeChild;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we got this far then we did not find a candidate match for our "to node"
|
|
||||||
// and we exhausted all of the children "from" nodes. Therefore, we will just
|
|
||||||
// append the current "to node" to the end
|
|
||||||
fromNode.appendChild(curToNodeChild);
|
|
||||||
|
|
||||||
curToNodeChild = toNextSibling;
|
|
||||||
curFromNodeChild = fromNextSibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have processed all of the "to nodes". If curFromNodeChild is non-null then
|
|
||||||
// we still have some from nodes left over that need to be removed
|
|
||||||
while(curFromNodeChild) {
|
|
||||||
fromNextSibling = curFromNodeChild.nextSibling;
|
|
||||||
removeNode(curFromNodeChild, fromNode, alreadyVisited);
|
|
||||||
curFromNodeChild = fromNextSibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
var specialElHandler = specialElHandlers[fromNode.tagName];
|
|
||||||
if (specialElHandler) {
|
|
||||||
specialElHandler(fromNode, toNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var morphedNode = fromNode;
|
|
||||||
var morphedNodeType = morphedNode.nodeType;
|
|
||||||
var toNodeType = toNode.nodeType;
|
|
||||||
|
|
||||||
// Handle the case where we are given two DOM nodes that are not
|
|
||||||
// compatible (e.g. <div> --> <span> or <div> --> TEXT)
|
|
||||||
if (morphedNodeType === 1) {
|
|
||||||
if (toNodeType === 1) {
|
|
||||||
if (morphedNode.tagName !== toNode.tagName) {
|
|
||||||
onNodeDiscarded(fromNode);
|
|
||||||
morphedNode = moveChildren(morphedNode, document.createElement(toNode.tagName));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Going from an element node to a text node
|
|
||||||
return toNode;
|
|
||||||
}
|
|
||||||
} else if (morphedNodeType === 3) { // Text node
|
|
||||||
if (toNodeType === 3) {
|
|
||||||
morphedNode.nodeValue = toNode.nodeValue;
|
|
||||||
return morphedNode;
|
|
||||||
} else {
|
|
||||||
onNodeDiscarded(fromNode);
|
|
||||||
// Text node to something else
|
|
||||||
return toNode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
morphEl(morphedNode, toNode, false);
|
|
||||||
|
|
||||||
// Fire the "onNodeDiscarded" event for any saved elements
|
|
||||||
// that never found a new home in the morphed DOM
|
|
||||||
for (var savedElId in savedEls) {
|
|
||||||
if (savedEls.hasOwnProperty(savedElId)) {
|
|
||||||
var savedEl = savedEls[savedElId];
|
|
||||||
onNodeDiscarded(savedEl);
|
|
||||||
walkDiscardedChildNodes(savedEl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (morphedNode !== fromNode && fromNode.parentNode) {
|
|
||||||
fromNode.parentNode.replaceChild(morphedNode, fromNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
return morphedNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = morphdom;
|
|
||||||
},{}]},{},[1])(1)
|
|
||||||
});
|
|
File diff suppressed because one or more lines are too long
|
@ -1 +0,0 @@
|
||||||
import StatsPlugin
|
|
|
@ -1,67 +0,0 @@
|
||||||
import time
|
|
||||||
|
|
||||||
from Plugin import PluginManager
|
|
||||||
from Translate import translate
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("UiRequest")
|
|
||||||
class UiRequestPlugin(object):
|
|
||||||
def actionSiteMedia(self, path, header_length=True):
|
|
||||||
file_name = path.split("/")[-1]
|
|
||||||
if not file_name: # Path ends with /
|
|
||||||
file_name = "index.html"
|
|
||||||
extension = file_name.split(".")[-1]
|
|
||||||
if translate.lang != "en" and extension in ["js", "html"]:
|
|
||||||
path_parts = self.parsePath(path)
|
|
||||||
file_generator = super(UiRequestPlugin, self).actionSiteMedia(path, header_length=False)
|
|
||||||
if "next" in dir(file_generator): # File found and generator returned
|
|
||||||
site = self.server.sites.get(path_parts["address"])
|
|
||||||
return self.actionPatchFile(site, path_parts["inner_path"], file_generator)
|
|
||||||
else:
|
|
||||||
return file_generator
|
|
||||||
|
|
||||||
else:
|
|
||||||
return super(UiRequestPlugin, self).actionSiteMedia(path)
|
|
||||||
|
|
||||||
def actionUiMedia(self, path):
|
|
||||||
file_generator = super(UiRequestPlugin, self).actionUiMedia(path)
|
|
||||||
if translate.lang != "en" and path.endswith(".js"):
|
|
||||||
s = time.time()
|
|
||||||
data = "".join(list(file_generator))
|
|
||||||
data = translate.translateData(data)
|
|
||||||
self.log.debug("Patched %s (%s bytes) in %.3fs" % (path, len(data), time.time() - s))
|
|
||||||
return iter([data])
|
|
||||||
else:
|
|
||||||
return file_generator
|
|
||||||
|
|
||||||
def actionPatchFile(self, site, inner_path, file_generator):
|
|
||||||
content_json = site.content_manager.contents["content.json"]
|
|
||||||
lang_file = "languages/%s.json" % translate.lang
|
|
||||||
lang_file_exist = False
|
|
||||||
if site.settings.get("own"): # My site, check if the file is exist (allow to add new lang without signing)
|
|
||||||
if site.storage.isFile(lang_file):
|
|
||||||
lang_file_exist = True
|
|
||||||
else: # Not my site the reference in content.json is enough (will wait for download later)
|
|
||||||
if lang_file in content_json.get("files", {}):
|
|
||||||
lang_file_exist = True
|
|
||||||
|
|
||||||
if not lang_file_exist or inner_path not in content_json.get("translate", []):
|
|
||||||
for part in file_generator:
|
|
||||||
if inner_path.endswith(".html"):
|
|
||||||
yield part.replace("lang={lang}", "lang=" + str(translate.lang)) # lang get parameter to .js file to avoid cache
|
|
||||||
else:
|
|
||||||
yield part
|
|
||||||
else:
|
|
||||||
s = time.time()
|
|
||||||
data = "".join(list(file_generator))
|
|
||||||
|
|
||||||
# if site.content_manager.contents["content.json"]["files"].get(lang_file):
|
|
||||||
site.needFile(lang_file, priority=10)
|
|
||||||
if inner_path.endswith("js"):
|
|
||||||
data = translate.translateData(data, site.storage.loadJson(lang_file), "js")
|
|
||||||
else:
|
|
||||||
data = translate.translateData(data, site.storage.loadJson(lang_file), "html")
|
|
||||||
data = data.replace("lang={lang}", "lang=" + str(translate.lang)) # lang get parameter to .js file to avoid cache
|
|
||||||
|
|
||||||
self.log.debug("Patched %s (%s bytes) in %.3fs" % (inner_path, len(data), time.time() - s))
|
|
||||||
yield data
|
|
|
@ -1 +0,0 @@
|
||||||
import TranslateSitePlugin
|
|
|
@ -1,163 +0,0 @@
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import atexit
|
|
||||||
|
|
||||||
from Plugin import PluginManager
|
|
||||||
from Config import config
|
|
||||||
from Translate import Translate
|
|
||||||
|
|
||||||
allow_reload = False # No source reload supported in this plugin
|
|
||||||
|
|
||||||
if "_" not in locals():
|
|
||||||
_ = Translate("plugins/Trayicon/languages/")
|
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.registerTo("Actions")
|
|
||||||
class ActionsPlugin(object):
|
|
||||||
|
|
||||||
def main(self):
|
|
||||||
global notificationicon, winfolders
|
|
||||||
from lib import notificationicon, winfolders
|
|
||||||
import gevent.threadpool
|
|
||||||
|
|
||||||
self.main = sys.modules["main"]
|
|
||||||
|
|
||||||
fs_encoding = sys.getfilesystemencoding()
|
|
||||||
|
|
||||||
icon = notificationicon.NotificationIcon(
|
|
||||||
os.path.join(os.path.dirname(os.path.abspath(__file__).decode(fs_encoding)), 'trayicon.ico'),
|
|
||||||
"ZeroNet %s" % config.version
|
|
||||||
)
|
|
||||||
self.icon = icon
|
|
||||||
|
|
||||||
if not config.debug: # Hide console if not in debug mode
|
|
||||||
notificationicon.hideConsole()
|
|
||||||
self.console = False
|
|
||||||
else:
|
|
||||||
self.console = True
|
|
||||||
|
|
||||||
@atexit.register
|
|
||||||
def hideIcon():
|
|
||||||
icon.die()
|
|
||||||
|
|
||||||
ui_ip = config.ui_ip if config.ui_ip != "*" else "127.0.0.1"
|
|
||||||
|
|
||||||
icon.items = [
|
|
||||||
(self.titleIp, False),
|
|
||||||
(self.titleConnections, False),
|
|
||||||
(self.titleTransfer, False),
|
|
||||||
(self.titleConsole, self.toggleConsole),
|
|
||||||
(self.titleAutorun, self.toggleAutorun),
|
|
||||||
"--",
|
|
||||||
(_["ZeroNet Twitter"], lambda: self.opensite("https://twitter.com/HelloZeroNet")),
|
|
||||||
(_["ZeroNet Reddit"], lambda: self.opensite("http://www.reddit.com/r/zeronet/")),
|
|
||||||
(_["ZeroNet Github"], lambda: self.opensite("https://github.com/HelloZeroNet/ZeroNet")),
|
|
||||||
(_["Report bug/request feature"], lambda: self.opensite("https://github.com/HelloZeroNet/ZeroNet/issues")),
|
|
||||||
"--",
|
|
||||||
(_["!Open ZeroNet"], lambda: self.opensite("http://%s:%s/%s" % (ui_ip, config.ui_port, config.homepage))),
|
|
||||||
"--",
|
|
||||||
(_["Quit"], self.quit),
|
|
||||||
]
|
|
||||||
|
|
||||||
if not notificationicon.hasConsole():
|
|
||||||
del icon.items[3]
|
|
||||||
|
|
||||||
icon.clicked = lambda: self.opensite("http://%s:%s/%s" % (ui_ip, config.ui_port, config.homepage))
|
|
||||||
self.quit_servers_event = gevent.threadpool.ThreadResult(
|
|
||||||
lambda res: gevent.spawn_later(0.1, self.quitServers)
|
|
||||||
) # Fix gevent thread switch error
|
|
||||||
gevent.threadpool.start_new_thread(icon._run, ()) # Start in real thread (not gevent compatible)
|
|
||||||
super(ActionsPlugin, self).main()
|
|
||||||
icon._die = True
|
|
||||||
|
|
||||||
def quit(self):
|
|
||||||
self.icon.die()
|
|
||||||
self.quit_servers_event.set(True)
|
|
||||||
|
|
||||||
def quitServers(self):
|
|
||||||
self.main.ui_server.stop()
|
|
||||||
self.main.file_server.stop()
|
|
||||||
|
|
||||||
def opensite(self, url):
|
|
||||||
import webbrowser
|
|
||||||
webbrowser.open(url, new=0)
|
|
||||||
|
|
||||||
def titleIp(self):
|
|
||||||
title = "!IP: %s " % config.ip_external
|
|
||||||
if self.main.file_server.port_opened:
|
|
||||||
title += _["(active)"]
|
|
||||||
else:
|
|
||||||
title += _["(passive)"]
|
|
||||||
return title
|
|
||||||
|
|
||||||
def titleConnections(self):
|
|
||||||
title = _["Connections: %s"] % len(self.main.file_server.connections)
|
|
||||||
return title
|
|
||||||
|
|
||||||
def titleTransfer(self):
|
|
||||||
title = _["Received: %.2f MB | Sent: %.2f MB"] % (
|
|
||||||
float(self.main.file_server.bytes_recv) / 1024 / 1024,
|
|
||||||
float(self.main.file_server.bytes_sent) / 1024 / 1024
|
|
||||||
)
|
|
||||||
return title
|
|
||||||
|
|
||||||
def titleConsole(self):
|
|
||||||
translate = _["Show console window"]
|
|
||||||
if self.console:
|
|
||||||
return "+" + translate
|
|
||||||
else:
|
|
||||||
return translate
|
|
||||||
|
|
||||||
def toggleConsole(self):
|
|
||||||
if self.console:
|
|
||||||
notificationicon.hideConsole()
|
|
||||||
self.console = False
|
|
||||||
else:
|
|
||||||
notificationicon.showConsole()
|
|
||||||
self.console = True
|
|
||||||
|
|
||||||
def getAutorunPath(self):
|
|
||||||
return "%s\\zeronet.cmd" % winfolders.get(winfolders.STARTUP)
|
|
||||||
|
|
||||||
def formatAutorun(self):
|
|
||||||
args = sys.argv[:]
|
|
||||||
|
|
||||||
if not getattr(sys, 'frozen', False): # Not frozen
|
|
||||||
args.insert(0, sys.executable)
|
|
||||||
cwd = os.getcwd().decode(sys.getfilesystemencoding())
|
|
||||||
else:
|
|
||||||
cwd = os.path.dirname(sys.executable).decode(sys.getfilesystemencoding())
|
|
||||||
|
|
||||||
if sys.platform == 'win32':
|
|
||||||
args = ['"%s"' % arg for arg in args if arg]
|
|
||||||
cmd = " ".join(args)
|
|
||||||
|
|
||||||
# Dont open browser on autorun
|
|
||||||
cmd = cmd.replace("start.py", "zeronet.py").replace('"--open_browser"', "").replace('"default_browser"', "").strip()
|
|
||||||
cmd += ' --open_browser ""'
|
|
||||||
cmd = cmd.decode(sys.getfilesystemencoding())
|
|
||||||
|
|
||||||
return u"""
|
|
||||||
@echo off
|
|
||||||
chcp 65001
|
|
||||||
set PYTHONIOENCODING=utf-8
|
|
||||||
cd /D \"%s\"
|
|
||||||
%s
|
|
||||||
""" % (cwd, cmd)
|
|
||||||
|
|
||||||
def isAutorunEnabled(self):
|
|
||||||
path = self.getAutorunPath()
|
|
||||||
return os.path.isfile(path) and open(path).read().decode("utf8") == self.formatAutorun()
|
|
||||||
|
|
||||||
def titleAutorun(self):
|
|
||||||
translate = _["Start ZeroNet when Windows starts"]
|
|
||||||
if self.isAutorunEnabled():
|
|
||||||
return "+" + translate
|
|
||||||
else:
|
|
||||||
return translate
|
|
||||||
|
|
||||||
def toggleAutorun(self):
|
|
||||||
if self.isAutorunEnabled():
|
|
||||||
os.unlink(self.getAutorunPath())
|
|
||||||
else:
|
|
||||||
open(self.getAutorunPath(), "w").write(self.formatAutorun().encode("utf8"))
|
|
|
@ -1,4 +0,0 @@
|
||||||
import sys
|
|
||||||
|
|
||||||
if sys.platform == 'win32':
|
|
||||||
import TrayiconPlugin
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue