mirror of
https://github.com/9001/copyparty.git
synced 2026-11-14 08:38:22 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e00e11225f | ||
|
|
d21242fc62 | ||
|
|
31b2801fd0 | ||
|
|
9951e0333d | ||
|
|
ab8bd0178b | ||
|
|
7be73721fb | ||
|
|
1cc2c4e2fe | ||
|
|
1bec91d11c | ||
|
|
f5e70c7f67 | ||
|
|
c2b71dab25 | ||
|
|
6f1d664754 | ||
|
|
f1a433a617 | ||
|
|
d68028890d | ||
|
|
f37c3b9608 | ||
|
|
eefb181b33 | ||
|
|
5a7504fdbf | ||
|
|
c39d149378 | ||
|
|
d067d2a638 | ||
|
|
e1c22eaf0e | ||
|
|
914f84ce5e | ||
|
|
ddd5ca84c1 | ||
|
|
0ca4c1bda4 | ||
|
|
3c1bade452 | ||
|
|
27ae2e1e66 | ||
|
|
dc99c872e0 | ||
|
|
40e13419e5 | ||
|
|
756494a1c3 | ||
|
|
bf01ca4891 | ||
|
|
cd927e7837 | ||
|
|
17145ba074 | ||
|
|
3089896be7 | ||
|
|
c8dfcc86f8 | ||
|
|
418bf2f979 | ||
|
|
84e687a00d | ||
|
|
d44ea24530 | ||
|
|
a9a7c7e746 | ||
|
|
e5d0a0572d | ||
|
|
0e8913c2d0 | ||
|
|
48c1017843 | ||
|
|
fef34b5465 | ||
|
|
31e7d2d622 | ||
|
|
e8609b87af | ||
|
|
d692838095 | ||
|
|
fd09638567 | ||
|
|
312f48e17c | ||
|
|
f02e9cf6d3 | ||
|
|
fc8a4b8ea2 | ||
|
|
76041fdba8 | ||
|
|
a368fc66b3 | ||
|
|
4cb4e820f6 | ||
|
|
6c96c64605 | ||
|
|
dfadb5a7ba | ||
|
|
16403d8c6c | ||
|
|
59de5e2ca8 | ||
|
|
a7a25debe4 | ||
|
|
a65a30b1b4 | ||
|
|
93ffc65c14 | ||
|
|
720c83b2ff | ||
|
|
b2711e05b6 | ||
|
|
6694998985 | ||
|
|
598df44e87 | ||
|
|
96aeb898ae | ||
|
|
9b436eb52e | ||
|
|
ff2200ff85 | ||
|
|
0a41d0c03b | ||
|
|
72224d29d6 | ||
|
|
95b827f1a5 | ||
|
|
32c11f142e | ||
|
|
e2dc6d33bf | ||
|
|
9c14972da6 | ||
|
|
530afddc6a | ||
|
|
08b0abdbdd | ||
|
|
ed6a8d5a73 | ||
|
|
73d06eaf84 | ||
|
|
22cdc0f8c9 | ||
|
|
79273a7d97 | ||
|
|
2f57228fd4 | ||
|
|
b6bf6d5f7a | ||
|
|
24141b494b | ||
|
|
8e046fb6a8 | ||
|
|
296362fc84 | ||
|
|
b20d32593e | ||
|
|
69fa1d10bc | ||
|
|
5c4ba376a0 | ||
|
|
bef30ac04d | ||
|
|
d734786c1d | ||
|
|
59e2d77df9 | ||
|
|
d750f3f0d2 | ||
|
|
c249ee457b | ||
|
|
b4fddbc3d2 | ||
|
|
81e5eb7b27 | ||
|
|
1142ac2563 | ||
|
|
ffca67f25a | ||
|
|
e1eff21623 | ||
|
|
dc8c229bcd | ||
|
|
c41ee3fc27 | ||
|
|
6dcb1efb7c | ||
|
|
0a3a80725a | ||
|
|
ba67b27946 | ||
|
|
b4df8fa23c | ||
|
|
78f6855f08 | ||
|
|
cab9feb225 | ||
|
|
e752005543 | ||
|
|
206b1752e6 | ||
|
|
df8b395380 | ||
|
|
67c5d8dae4 | ||
|
|
c17c3be008 | ||
|
|
22b9c283d1 | ||
|
|
60ceea4b42 | ||
|
|
826e84c8ec | ||
|
|
d90d021969 | ||
|
|
d925553810 | ||
|
|
41d3bae929 | ||
|
|
ffb2560322 | ||
|
|
40fb263097 | ||
|
|
4e67b467a9 | ||
|
|
72c59405e7 | ||
|
|
5d7cf80ff0 | ||
|
|
3e3228e0f6 | ||
|
|
930e864239 | ||
|
|
bef07720d9 | ||
|
|
5b3191b495 | ||
|
|
d32704ed37 | ||
|
|
04f612ff63 | ||
|
|
66750391ae | ||
|
|
c46cd7f57a | ||
|
|
8240ef6151 | ||
|
|
bc24604a83 | ||
|
|
266489113a | ||
|
|
d5a8a34bca | ||
|
|
9d223d6ca7 | ||
|
|
caf831fc28 | ||
|
|
ae56f3bdae | ||
|
|
25a8b96fd1 | ||
|
|
7357d46f43 | ||
|
|
3aebfabd5c | ||
|
|
8914f0af70 | ||
|
|
a9ae6d5131 | ||
|
|
ecd4fcc1b3 | ||
|
|
738a419b2b | ||
|
|
3a16d3461d | ||
|
|
3a52096205 | ||
|
|
87a5c22a34 | ||
|
|
7d6e59f347 | ||
|
|
b918b592ca | ||
|
|
cbeb439ad2 | ||
|
|
74a6ce1c1e | ||
|
|
2f4a30b620 | ||
|
|
6c41bac6d2 | ||
|
|
8c9e1016de | ||
|
|
9030828494 | ||
|
|
13055c6451 | ||
|
|
038af50777 | ||
|
|
7ffb1fd0ca | ||
|
|
364f74a1bd | ||
|
|
feabbf3e6a | ||
|
|
5b2c84752d | ||
|
|
cce1210792 | ||
|
|
e55e5a4585 | ||
|
|
af3f777ec9 | ||
|
|
80a3749238 | ||
|
|
2f3591d036 | ||
|
|
65391a9d8c | ||
|
|
fd141c61b5 | ||
|
|
3ee91dffad | ||
|
|
ec7ea30951 | ||
|
|
4e9cf95e8d | ||
|
|
3bf80c8152 | ||
|
|
5b89a2e3b2 | ||
|
|
f81d80bcad | ||
|
|
f08cb25ccc | ||
|
|
7d7a1510fb | ||
|
|
63d8e5a033 | ||
|
|
ec51d3241c | ||
|
|
85639ad2cd | ||
|
|
05a4472075 | ||
|
|
82c496092f | ||
|
|
8551472bf0 | ||
|
|
d1ddcb19f5 | ||
|
|
39c3ccc2eb | ||
|
|
4714c2fa5a | ||
|
|
120fdfb257 | ||
|
|
4642d32366 | ||
|
|
2c26aecd87 | ||
|
|
d8c732469b | ||
|
|
4c73704ce7 | ||
|
|
e0845b2363 | ||
|
|
511dc01615 | ||
|
|
fa32e15958 | ||
|
|
2d1d295a4d | ||
|
|
c82a3cb226 | ||
|
|
485c60cf25 | ||
|
|
c0e167fd97 | ||
|
|
7bfd370b6c | ||
|
|
1f6e811674 | ||
|
|
0e6b167167 | ||
|
|
519bfe1f0b | ||
|
|
d006cecaef | ||
|
|
b6c2ec15db | ||
|
|
0b6d2d2424 | ||
|
|
8d46cf1823 | ||
|
|
c8f3b4ef05 | ||
|
|
9c64788d43 | ||
|
|
5e1d9a58d8 | ||
|
|
336842192c | ||
|
|
d12cf9aee1 | ||
|
|
db38cb2f79 | ||
|
|
d4a9787c6c | ||
|
|
b60eb3f01a | ||
|
|
3476d5e6d9 | ||
|
|
a3eec23cef | ||
|
|
2e7390a4c5 | ||
|
|
e0b04d9c16 | ||
|
|
9e64fe02f9 | ||
|
|
67ddc64171 | ||
|
|
56e15009c7 | ||
|
|
08474dbe14 | ||
|
|
5a1f0a330c | ||
|
|
3bc0bf19b0 | ||
|
|
efc6a09dd3 | ||
|
|
921954037b | ||
|
|
fecc3fd507 | ||
|
|
5e85e3d628 | ||
|
|
965a4a6949 | ||
|
|
7f82189da9 | ||
|
|
14bef85b87 | ||
|
|
594ec39481 | ||
|
|
ba017f7b53 | ||
|
|
3bbed1bc46 | ||
|
|
4b0064b209 | ||
|
|
e440578cae | ||
|
|
1a9d4c04d5 | ||
|
|
8e2fb05ab8 | ||
|
|
ca6c4deaac | ||
|
|
a1cbac0252 | ||
|
|
1b222fb576 | ||
|
|
ce2eeba226 | ||
|
|
ad45de9441 | ||
|
|
7d526eaba3 | ||
|
|
1b0eb45032 | ||
|
|
a86983928c | ||
|
|
c5c5f9b4b8 | ||
|
|
fa918228d5 | ||
|
|
29925dc22b | ||
|
|
cdffde7813 | ||
|
|
b314e30db8 | ||
|
|
d9f76882e7 | ||
|
|
89cab5b520 | ||
|
|
fd8c5bfcbc | ||
|
|
f7f8716aa8 | ||
|
|
b9aacba130 | ||
|
|
2e47074627 | ||
|
|
e469bc9445 | ||
|
|
945b2276a0 | ||
|
|
a9378a8e9c | ||
|
|
a9174e5dee | ||
|
|
fcc1bdfbf5 | ||
|
|
278a0d8548 | ||
|
|
04ac7fbd21 | ||
|
|
ba7387209a | ||
|
|
a31bfe6b2b | ||
|
|
dba7c5d4d5 | ||
|
|
acde21d484 | ||
|
|
cedfc44420 | ||
|
|
f4d67ff031 | ||
|
|
889bd3242a | ||
|
|
ca6d3a5c16 | ||
|
|
98701b78e6 | ||
|
|
a28503e805 | ||
|
|
ec00dc18ca | ||
|
|
7f9c139ead | ||
|
|
3f4b79ffb8 | ||
|
|
867237d06a | ||
|
|
c7345308f5 | ||
|
|
fb9f0441c9 | ||
|
|
a359b89edd | ||
|
|
77f74ddb2f | ||
|
|
f7e7b03f6f | ||
|
|
7a291403ca | ||
|
|
b427d7802a | ||
|
|
c424a55d6f | ||
|
|
7d62335c13 | ||
|
|
9385daeae8 | ||
|
|
79e1078671 | ||
|
|
cad15fbf60 | ||
|
|
9f08efcabd | ||
|
|
ac085b8149 | ||
|
|
1c15c0d5d1 | ||
|
|
904c984bda | ||
|
|
3242145e52 | ||
|
|
dff6aa2435 | ||
|
|
450cd86dc1 | ||
|
|
db60951d9f | ||
|
|
c00314a292 | ||
|
|
2cc53ea151 | ||
|
|
e9ab040ce8 | ||
|
|
e005930cd0 | ||
|
|
4fcd2c4193 | ||
|
|
cec44aa1dd | ||
|
|
e3524d85bd | ||
|
|
1963ed1795 | ||
|
|
f0297777eb | ||
|
|
36ab77e0bf | ||
|
|
db87ea5ce1 | ||
|
|
b1efc0065c | ||
|
|
a325353b1b | ||
|
|
1c17b63b76 | ||
|
|
d3dd34569a | ||
|
|
cdd5e78adf | ||
|
|
fff7291dcf | ||
|
|
7f5810f1a7 | ||
|
|
b624a38747 | ||
|
|
c9e45c12d8 | ||
|
|
5c42ad1c78 | ||
|
|
547a7ab1cc | ||
|
|
a04570ff5e | ||
|
|
93eb862c60 | ||
|
|
81881a449b | ||
|
|
e2a15a3a92 | ||
|
|
51e6099c58 | ||
|
|
f73742b6a4 | ||
|
|
5d96862c45 | ||
|
|
98da5cc509 | ||
|
|
d9cd7ec3cf | ||
|
|
dbb78705f7 | ||
|
|
b4fda5f1d6 | ||
|
|
1eb016c975 | ||
|
|
f55d8341f1 | ||
|
|
9746b4e21e | ||
|
|
ad85713693 | ||
|
|
66dc8b5c9f | ||
|
|
740d22d3ba | ||
|
|
6f02812af1 | ||
|
|
3364448858 | ||
|
|
677fd8eef1 | ||
|
|
cca1f9b22b | ||
|
|
6c53c75ab2 | ||
|
|
6c024dbf80 | ||
|
|
d099e5e84e | ||
|
|
ff01723c6d | ||
|
|
02ba9ea7ec | ||
|
|
5232ce6a53 | ||
|
|
f5425a8839 | ||
|
|
3b009d97b3 | ||
|
|
50109f7631 | ||
|
|
14bd4cf5c1 | ||
|
|
28b9323890 | ||
|
|
b61b910e2a | ||
|
|
07ace41640 | ||
|
|
748aaa950c | ||
|
|
6de6aa4bb8 | ||
|
|
0fa862e145 | ||
|
|
bb166c98c9 | ||
|
|
52446bb507 | ||
|
|
5a29df6ba9 | ||
|
|
b9d7ede3d6 | ||
|
|
c2e03bf6bc | ||
|
|
a85ad20142 | ||
|
|
98086948df | ||
|
|
a7cdc5de08 | ||
|
|
a7840bebe7 | ||
|
|
d6433b784b | ||
|
|
33b0cd5a34 | ||
|
|
f2caab6119 | ||
|
|
e65ec75e22 | ||
|
|
7be67ab474 | ||
|
|
8ef6dda74b | ||
|
|
46c205dd60 | ||
|
|
dbd8f837e8 | ||
|
|
20ac117c32 | ||
|
|
cd3feaac86 | ||
|
|
f8e19815e1 | ||
|
|
76e9f23a6d | ||
|
|
4542ad3c01 | ||
|
|
50276c0cfa | ||
|
|
c5f1229685 | ||
|
|
73ec2d296f | ||
|
|
a499648291 | ||
|
|
4bb5baf1b8 | ||
|
|
efd19af7ca | ||
|
|
aace711eb9 | ||
|
|
39bd4e5b58 | ||
|
|
08ebb0b4c9 | ||
|
|
656f0a6c39 | ||
|
|
805a7054e9 | ||
|
|
01709691f2 | ||
|
|
41ed559faa | ||
|
|
a0f8f794a8 | ||
|
|
fbe5fa582e | ||
|
|
2248705e1a | ||
|
|
eb173be4f1 | ||
|
|
d05a88d2ee | ||
|
|
09e6f29e5e | ||
|
|
2ce32e4fb6 | ||
|
|
9b7f933b78 | ||
|
|
38cc809822 | ||
|
|
e9b6e645d3 | ||
|
|
0f9a239078 | ||
|
|
0453b7ac53 | ||
|
|
1bcdf8c9e3 | ||
|
|
4177c1d9ed | ||
|
|
171ca985c8 | ||
|
|
dacc64dd2e | ||
|
|
31f1b535b2 | ||
|
|
7fc379abc8 | ||
|
|
a8f53d5ef0 | ||
|
|
3f59710294 | ||
|
|
24e01221c5 | ||
|
|
daba1ab7bd | ||
|
|
1bca86c6e1 | ||
|
|
fc2754cba5 | ||
|
|
470b504843 | ||
|
|
435db14798 | ||
|
|
d08e872062 | ||
|
|
f91a653bde | ||
|
|
7d86f39a23 | ||
|
|
456addf26f | ||
|
|
4e38e4087e | ||
|
|
f0ecb08347 | ||
|
|
1193f9ba6c | ||
|
|
234eddec90 | ||
|
|
e3baf932f3 | ||
|
|
eb5d767b01 | ||
|
|
ec7418734d | ||
|
|
a3d9506783 | ||
|
|
57650a218f | ||
|
|
983865d96c | ||
|
|
6f6b70ad04 | ||
|
|
e187df28f2 | ||
|
|
df0fa9d1b7 | ||
|
|
397ed5653b | ||
|
|
9f46e4dbd7 | ||
|
|
6912e86747 | ||
|
|
80ca78516e | ||
|
|
a493cd6530 | ||
|
|
c72b62ad86 | ||
|
|
fdcd92bac8 | ||
|
|
b00dac997a | ||
|
|
9d066414c6 | ||
|
|
5e4ff90b1c | ||
|
|
deb8a4a86e | ||
|
|
733e85c040 | ||
|
|
892a452446 | ||
|
|
38df223b8f | ||
|
|
b136a5b042 | ||
|
|
377eddcd06 | ||
|
|
549fe33f51 | ||
|
|
c214a93caa | ||
|
|
0941fd4ec1 | ||
|
|
6dbd9901b2 | ||
|
|
e9ca36fa88 | ||
|
|
a053a663b4 | ||
|
|
1923a25879 | ||
|
|
4cce799012 | ||
|
|
1460fe97ac | ||
|
|
ca872c4055 | ||
|
|
3ddb4c042a | ||
|
|
15d3c2fbff | ||
|
|
6a24432019 | ||
|
|
8f6194fe77 | ||
|
|
260da2f45c | ||
|
|
a9e02ce753 | ||
|
|
70c088aeca | ||
|
|
280815f158 | ||
|
|
e1ea9852c6 | ||
|
|
2ee9c80d3b | ||
|
|
4b2ff3a196 | ||
|
|
538a205ce4 | ||
|
|
6559152882 | ||
|
|
669b10754d | ||
|
|
478f1c764e | ||
|
|
a043d7cfb6 | ||
|
|
ee5f31908f | ||
|
|
35326a6fb8 | ||
|
|
59a0122179 | ||
|
|
5996a58b20 | ||
|
|
fd331a545d | ||
|
|
e7ef31ee33 | ||
|
|
49ce67e9cd | ||
|
|
8b66874b85 | ||
|
|
3a2381ff2d | ||
|
|
83bd197438 | ||
|
|
c03b332ec0 | ||
|
|
17b4f905a7 | ||
|
|
8f587627e1 | ||
|
|
14b7e5143f | ||
|
|
3e97a77784 | ||
|
|
3f45492725 | ||
|
|
9c9e4057e0 | ||
|
|
3d09bec1bb | ||
|
|
d7887f3d55 | ||
|
|
ecd18adc3c | ||
|
|
e2aa8fc1a4 | ||
|
|
dfd9e007ee | ||
|
|
5c1a43c711 | ||
|
|
52438bcc0d | ||
|
|
e09f3c9e2c | ||
|
|
25749b4b5f | ||
|
|
75b0b312a4 | ||
|
|
c47c708433 | ||
|
|
e0a92ba72d | ||
|
|
98386f28f0 | ||
|
|
422f8f624e | ||
|
|
edafa1586a | ||
|
|
e270fe60ed | ||
|
|
ab56238249 | ||
|
|
3bdef75e88 | ||
|
|
67ba5b0252 | ||
|
|
06d2654b3f | ||
|
|
1cdb388090 | ||
|
|
f7e08ed007 | ||
|
|
b049631169 | ||
|
|
aaeec11f81 | ||
|
|
96b109b0d6 | ||
|
|
74821a38ad | ||
|
|
19a4c45389 | ||
|
|
09f22993be | ||
|
|
c2be664e96 | ||
|
|
7a4ee4dbc8 | ||
|
|
bd6d1f961d | ||
|
|
eeb7738b53 | ||
|
|
e6755aa8a1 | ||
|
|
230a146209 | ||
|
|
c71128fd72 | ||
|
|
b59b915962 | ||
|
|
f0caf88185 | ||
|
|
bfcb6eac41 | ||
|
|
e798a9a53a | ||
|
|
09e3018bf9 | ||
|
|
87539800e8 | ||
|
|
0469b5a29e | ||
|
|
3e90abbf6f | ||
|
|
26a29797a6 | ||
|
|
14555d5832 | ||
|
|
d1f75229b5 | ||
|
|
01cf20a029 | ||
|
|
6f0871173e | ||
|
|
914686ec7c | ||
|
|
0d96786e68 | ||
|
|
4c3792de07 | ||
|
|
200eaa92d7 | ||
|
|
d40f061a79 | ||
|
|
28b93d7961 | ||
|
|
aa1c921302 | ||
|
|
2848941e01 | ||
|
|
f4f702c39d | ||
|
|
4c042b3c82 | ||
|
|
0b50fde305 | ||
|
|
0b3939002d | ||
|
|
543b7ea959 | ||
|
|
d30240b431 | ||
|
|
599e82f24d | ||
|
|
abffda5474 | ||
|
|
35472557cb | ||
|
|
48d6224ec8 | ||
|
|
68503444c7 | ||
|
|
0491123bb2 | ||
|
|
8f235be66f | ||
|
|
4b8c22159a | ||
|
|
59f142cd19 | ||
|
|
6cd0a396df | ||
|
|
6413ad3e8d | ||
|
|
ad0e6c7fde | ||
|
|
978801d020 | ||
|
|
d39c74c126 | ||
|
|
5c250c2c19 | ||
|
|
ceaf133d9d | ||
|
|
202ddeac0d | ||
|
|
cc4f4aef99 | ||
|
|
f9cb2c15e3 | ||
|
|
cc65b1b551 | ||
|
|
6d76254c88 | ||
|
|
0de07d8e8b | ||
|
|
ca98d54fda | ||
|
|
6c76614eb1 | ||
|
|
63d4ec64cd | ||
|
|
cd8771fa52 | ||
|
|
15c5b50a36 | ||
|
|
7a4973fa56 | ||
|
|
3259367007 | ||
|
|
5e36f02595 | ||
|
|
c51371c71d | ||
|
|
20ef74cdac | ||
|
|
b5c6b4fa99 | ||
|
|
377f7732de | ||
|
|
b2fb0c26ad | ||
|
|
da4ae66173 | ||
|
|
bf1fdcab09 | ||
|
|
e7f2c6d806 | ||
|
|
a113d3b925 | ||
|
|
96cb5abf53 | ||
|
|
55c85d0984 | ||
|
|
782e2f1de3 | ||
|
|
f4727f8ea3 | ||
|
|
d4cf42e760 | ||
|
|
98d117b8ad | ||
|
|
d9046f7e01 | ||
|
|
dcc6b1b4ef | ||
|
|
274c074775 | ||
|
|
187cae25bf | ||
|
|
43a19779c1 | ||
|
|
23ea1c8a14 | ||
|
|
e3c7d6776e | ||
|
|
4df033ecc3 | ||
|
|
1228b5510b | ||
|
|
62e072a2ed | ||
|
|
a4649d1e71 | ||
|
|
f4a3fba29c | ||
|
|
3aa8b7aa2d | ||
|
|
d56230573d | ||
|
|
af8620da92 | ||
|
|
2961dea5bb | ||
|
|
4e878d2f1e | ||
|
|
7f44875061 | ||
|
|
68907eaf48 | ||
|
|
c4a4fddd27 | ||
|
|
5b62742512 | ||
|
|
554cc2f3ee | ||
|
|
6303effe59 | ||
|
|
659f351c65 | ||
|
|
d676a86f3f | ||
|
|
715d374ee4 | ||
|
|
c9fd608732 | ||
|
|
c32a672a68 | ||
|
|
69d9878acd | ||
|
|
d8662aeb0e | ||
|
|
a407eb9269 | ||
|
|
1ebe06f51e | ||
|
|
88243ac8d6 | ||
|
|
6ccc9224f3 | ||
|
|
0177a9b402 | ||
|
|
9435e6b2e2 | ||
|
|
0da93659a4 | ||
|
|
db2a03409c | ||
|
|
c2cee222bd | ||
|
|
b87f8f1b01 | ||
|
|
a01870b744 | ||
|
|
3560eeb10e | ||
|
|
03acd65e96 | ||
|
|
e5e822951d | ||
|
|
347cf6a546 | ||
|
|
8ba98877ee | ||
|
|
3c78c6a880 | ||
|
|
7aa21483c5 | ||
|
|
074e106e24 | ||
|
|
e9ddfccfb6 | ||
|
|
91ce7a29aa | ||
|
|
392a4db55b | ||
|
|
3931bc2779 | ||
|
|
bd514f0666 | ||
|
|
f8a7c02f23 | ||
|
|
0dd5987250 | ||
|
|
4eca4885f3 | ||
|
|
e9ecb2edc5 | ||
|
|
f0b1c82b44 | ||
|
|
c955658332 | ||
|
|
a98360f213 | ||
|
|
33497e6b11 | ||
|
|
36ab323d08 | ||
|
|
1bf23fabc6 | ||
|
|
d9e3f998d1 | ||
|
|
1b71294aab | ||
|
|
346515ccf1 | ||
|
|
3c42a34f7b | ||
|
|
13499d2846 | ||
|
|
8b31ed8816 | ||
|
|
bcc3b1568e | ||
|
|
2943c7f2d5 | ||
|
|
34d98e9980 | ||
|
|
9e980bb552 | ||
|
|
3f8cb7e877 | ||
|
|
4a04356814 | ||
|
|
8a0746c6af | ||
|
|
54caf63f6a | ||
|
|
9b9d2a92ca | ||
|
|
a9ee4f24d5 | ||
|
|
29a4e54799 | ||
|
|
3b26884c69 | ||
|
|
392abd0675 | ||
|
|
50f46187f1 | ||
|
|
7ae84dea1a | ||
|
|
a57f7cc2f8 | ||
|
|
0f55a1ae86 | ||
|
|
00cb1f74e2 | ||
|
|
b664ebb01f | ||
|
|
c2ac57a2a8 | ||
|
|
0df1901fc0 | ||
|
|
8c000fd683 | ||
|
|
d4397e7217 | ||
|
|
d7e7e77f93 | ||
|
|
715f8424b3 | ||
|
|
40d56bb3f0 | ||
|
|
f9502c3df3 | ||
|
|
ae5eefc528 | ||
|
|
6eaf8af15a | ||
|
|
848315c009 | ||
|
|
47fa4a9299 | ||
|
|
9db8037e39 | ||
|
|
39e5582496 | ||
|
|
16bbcce51b | ||
|
|
e85a71070e | ||
|
|
d0499257c8 | ||
|
|
7d3a5c1e97 | ||
|
|
153d240d0d | ||
|
|
66a5bf365b | ||
|
|
0d09fb6818 | ||
|
|
b2d48c646f | ||
|
|
b469db3c62 | ||
|
|
a38e6e65d5 | ||
|
|
3798e19a26 | ||
|
|
c805c60f40 | ||
|
|
c69c7c8ac0 | ||
|
|
50f1629355 | ||
|
|
0bc1b8f715 | ||
|
|
a68d5b03f1 | ||
|
|
971360e914 | ||
|
|
7e3825f8f5 | ||
|
|
b700072107 | ||
|
|
ca22cd8853 | ||
|
|
09910ba807 | ||
|
|
3c6f0b17d0 | ||
|
|
4fa7be2a48 | ||
|
|
941761e6e7 | ||
|
|
c160428810 | ||
|
|
ad23b253dc | ||
|
|
d0d2f206a9 | ||
|
|
7ecedb2ce2 | ||
|
|
fee1416cbc | ||
|
|
6d6d79fcbc | ||
|
|
9c19753546 | ||
|
|
4e8b88d8f6 | ||
|
|
1ee89ec21d | ||
|
|
9dcb45133b | ||
|
|
1a5b7d40a8 | ||
|
|
6e35171c88 | ||
|
|
af34fbf1a4 | ||
|
|
be729fe557 | ||
|
|
a1dfd0be33 | ||
|
|
d357ff0d16 | ||
|
|
e965dc9c74 | ||
|
|
f401fa7f6c | ||
|
|
b69d590176 | ||
|
|
1f966bb9d5 | ||
|
|
3222ba3acd | ||
|
|
0e35f37638 | ||
|
|
edb5c2bdce | ||
|
|
714744f73e | ||
|
|
1c86b64a4e | ||
|
|
a2faf4e1e9 | ||
|
|
b46b5c35e3 | ||
|
|
fea45e451d | ||
|
|
0b05c726de | ||
|
|
cd460902b0 | ||
|
|
dccef40f3d | ||
|
|
c17ce4892e | ||
|
|
5df2cbe5d7 | ||
|
|
daa44be1a5 | ||
|
|
13d5631b48 | ||
|
|
a8705e611d | ||
|
|
b7ca6f4a66 | ||
|
|
4f1eb89382 | ||
|
|
9d32564c68 | ||
|
|
6016ec9388 | ||
|
|
fb7cbc423b | ||
|
|
e9684d402e | ||
|
|
6069bc9b19 | ||
|
|
f195998865 | ||
|
|
a9d07c63ed | ||
|
|
053de61907 | ||
|
|
c3cc2ddeae | ||
|
|
4988a55ea5 | ||
|
|
5c6341e99f | ||
|
|
fbf17be203 | ||
|
|
3cde1f3be2 | ||
|
|
4915b14be1 | ||
|
|
735d9f9391 | ||
|
|
cd40adccdb | ||
|
|
0f2c623599 | ||
|
|
4adbe1b517 | ||
|
|
4f013f64fe | ||
|
|
a9d1310296 | ||
|
|
43e6da3454 | ||
|
|
542a1de1ba | ||
|
|
03d23daecb | ||
|
|
cb019afecf | ||
|
|
5b98e104f2 | ||
|
|
df9feabcf8 | ||
|
|
674fc1fe08 | ||
|
|
a2601fd6ad | ||
|
|
025942a7d6 | ||
|
|
510100c86b | ||
|
|
161bbc7d26 | ||
|
|
7c9c962b79 | ||
|
|
cbdbaf1938 | ||
|
|
cdfceb483e | ||
|
|
2228f81f94 | ||
|
|
895880aeb0 | ||
|
|
6bb27e6091 | ||
|
|
d197e754b9 | ||
|
|
b0dec83aad | ||
|
|
e2c2dd18cf | ||
|
|
ca6d0b8d5e | ||
|
|
48705a74c6 | ||
|
|
b419984709 | ||
|
|
e00b97eee0 | ||
|
|
4dca1cf8f4 | ||
|
|
edba7fffd3 | ||
|
|
21a96bcfe8 | ||
|
|
2d322dd48e | ||
|
|
df6d4df4f8 | ||
|
|
5aa893973c | ||
|
|
be0dd555a6 | ||
|
|
9921c43e3a | ||
|
|
14fa369fae | ||
|
|
0f0f8d90c1 | ||
|
|
1afbff7335 | ||
|
|
8c32b0e7bb | ||
|
|
9bc4c5d2e6 | ||
|
|
1534b7cb55 | ||
|
|
56d3bcf515 | ||
|
|
78605d9a79 | ||
|
|
d46a40fed8 | ||
|
|
ce4e489802 | ||
|
|
fd7c71d6a3 | ||
|
|
fad2268566 | ||
|
|
a95ea03cd0 | ||
|
|
f6be390579 | ||
|
|
4f264a0a9c | ||
|
|
d27144340f | ||
|
|
299cff3ff7 | ||
|
|
42c199e78e | ||
|
|
1b2d39857b | ||
|
|
ed908b9868 | ||
|
|
d162502c38 | ||
|
|
bf11b2a421 | ||
|
|
77274e9d59 | ||
|
|
8306e3d9de | ||
|
|
deb6711b51 | ||
|
|
7ef6fd13cf | ||
|
|
65c4e03574 | ||
|
|
c9fafb202d | ||
|
|
d4d9069130 | ||
|
|
7eca90cc21 | ||
|
|
6ecf4fdceb | ||
|
|
8cae7a715b | ||
|
|
c75b0c25a6 | ||
|
|
9dd5dec093 | ||
|
|
ec05f8ccd5 | ||
|
|
a1c7a095ee | ||
|
|
77df17d191 | ||
|
|
fa5845ff5f | ||
|
|
17fa490687 | ||
|
|
1eff87c3bd | ||
|
|
d123d2bff0 | ||
|
|
5ac3864874 | ||
|
|
c599e2aaa3 | ||
|
|
2e53f7979a | ||
|
|
f61511d8c8 | ||
|
|
47415a7120 | ||
|
|
db7becacd2 | ||
|
|
28b63e587b | ||
|
|
9cb93ae1ed | ||
|
|
e3e51fb83a | ||
|
|
49c7124776 | ||
|
|
60fb1207fc | ||
|
|
48470f6b50 | ||
|
|
1d308eeb4c | ||
|
|
84f5f41747 | ||
|
|
19189afb34 | ||
|
|
23e77a3389 | ||
|
|
ecced0c4f2 | ||
|
|
d4a8071de5 | ||
|
|
261236e302 | ||
|
|
0de09860f6 | ||
|
|
bfb39969a4 | ||
|
|
256dad8cc0 | ||
|
|
a247ba9ca3 | ||
|
|
0a9a807772 | ||
|
|
41fa6b2552 | ||
|
|
f425ff51ae | ||
|
|
7cde9a2976 | ||
|
|
5dcd88a6c8 | ||
|
|
c3ef3fdc1f | ||
|
|
b9ba783c1c | ||
|
|
d1bca1f52f | ||
|
|
94352f278b | ||
|
|
4fb87ebe32 | ||
|
|
3cbb7243ab | ||
|
|
fff45552da | ||
|
|
95157d02c9 | ||
|
|
3090c74832 | ||
|
|
4195762d2a | ||
|
|
dc3b7a2720 | ||
|
|
ad200f2b97 | ||
|
|
897f9d328d | ||
|
|
efbe34f29d | ||
|
|
dbfc899d79 | ||
|
|
74fb4b0cb8 | ||
|
|
68e7000275 | ||
|
|
38c2dcce3e | ||
|
|
5b3a5fe76b | ||
|
|
d5a9bd80b2 | ||
|
|
71c5565949 | ||
|
|
db33d68d42 | ||
|
|
e1c20c7a18 | ||
|
|
d3f1b45ce3 | ||
|
|
c7aa1a3558 | ||
|
|
7b2bd6da83 | ||
|
|
2bd955ba9f | ||
|
|
98dcaee210 | ||
|
|
361aebf877 | ||
|
|
ffc1610980 | ||
|
|
233075aee7 | ||
|
|
d1a4d335df | ||
|
|
96acbd3593 | ||
|
|
4b876dd133 | ||
|
|
a06c5eb048 | ||
|
|
c9cdc3e1c1 | ||
|
|
c0becc6418 | ||
|
|
b17ccc38ee | ||
|
|
acfaacbd46 | ||
|
|
8e0364efad | ||
|
|
e3043004ba | ||
|
|
b2aaf40a3e | ||
|
|
21db8833dc | ||
|
|
ec14c3944e | ||
|
|
20920e844f | ||
|
|
f9954bc4e5 | ||
|
|
d450f61534 | ||
|
|
2b50fc2010 | ||
|
|
c2034f7bc5 | ||
|
|
cec3bee020 | ||
|
|
e1b9ac631f | ||
|
|
19ee64e5e3 | ||
|
|
4f397b9b5b | ||
|
|
71775dcccb | ||
|
|
b383c08cc3 | ||
|
|
fc88341820 | ||
|
|
43bbd566d7 | ||
|
|
e1dea7ef3e | ||
|
|
de2fedd2cd | ||
|
|
6aaafeee6d | ||
|
|
99f63adf58 | ||
|
|
de2c978842 | ||
|
|
3c90cec0cd | ||
|
|
57a56073d8 | ||
|
|
2525d594c5 | ||
|
|
a0ecc4d88e | ||
|
|
accd003d15 | ||
|
|
9c2c423761 | ||
|
|
999789c742 | ||
|
|
14bb299918 | ||
|
|
0a33336dd4 | ||
|
|
6a2644fece | ||
|
|
5ab09769e1 | ||
|
|
782084056d | ||
|
|
494179bd1c | ||
|
|
29a17ae2b7 | ||
|
|
815d46f2c4 | ||
|
|
8417098c68 | ||
|
|
25974d660d | ||
|
|
12fcb42201 | ||
|
|
16462ee573 | ||
|
|
540664e0c2 | ||
|
|
b5cb763ab1 | ||
|
|
c24a0ec364 |
No files matched your search
@@ -4,3 +4,4 @@
|
||||
|
||||
*.png binary
|
||||
*.gif binary
|
||||
*.gz binary
|
||||
@@ -7,9 +7,11 @@ assignees: '9001'
|
||||
|
||||
---
|
||||
|
||||
NOTE:
|
||||
all of the below are optional, consider them as inspiration, delete and rewrite at will, thx md
|
||||
|
||||
<!-- NOTE:
|
||||
**please use english, or include an english translation.** aside from that,
|
||||
include all of the information that might possibly be necessary to help diagnose your issue,
|
||||
especially things like the server config and server logs; that said,
|
||||
any parts below which are obviously and definitely irrelevant can be skipped -->
|
||||
|
||||
### Describe the bug
|
||||
a description of what the bug is
|
||||
@@ -33,7 +35,7 @@ remove the ones that are not relevant:
|
||||
### Server details (if you're NOT using docker/podman)
|
||||
remove the ones that are not relevant:
|
||||
* **server OS / version:**
|
||||
* **what copyparty did you grab:** (sfx/exe/pip/aur/...)
|
||||
* **what copyparty did you grab:** (sfx/exe/pip/arch/...)
|
||||
* **how you're running it:** (in a terminal, as a systemd-service, ...)
|
||||
* run copyparty with `--version` and grab the last 3 lines (they start with `copyparty`, `CPython`, `sqlite`) and paste them below this line:
|
||||
* **copyparty arguments and/or config-file:**
|
||||
@@ -44,5 +46,11 @@ if the issue is possibly on the client-side, then mention some of the following:
|
||||
* OS version:
|
||||
* browser version:
|
||||
|
||||
### The rest of the stack
|
||||
if you are connecting directly to copyparty then that's cool, otherwise please mention everything else between copyparty and the browser (reverseproxy, tunnels, etc.)
|
||||
|
||||
### Server log
|
||||
if the issue might be server-related, include everything that appears in the copyparty log during startup, and also anything else you think might be relevant
|
||||
|
||||
### Additional context
|
||||
any other context about the problem here
|
||||
@@ -7,7 +7,9 @@ assignees: '9001'
|
||||
|
||||
---
|
||||
|
||||
all of the below are optional, consider them as inspiration, delete and rewrite at will
|
||||
<!-- NOTE:
|
||||
**please use english, or include an english translation.** aside from that,
|
||||
all of the below are optional, consider them as inspiration, delete and rewrite at will -->
|
||||
|
||||
**is your feature request related to a problem? Please describe.**
|
||||
a description of what the problem is, for example, `I'm always frustrated when [...]` or `Why is it not possible to [...]`
|
||||
|
||||
@@ -29,6 +29,10 @@ copyparty.egg-info/
|
||||
copyparty/res/COPYING.txt
|
||||
copyparty/web/deps/
|
||||
srv/
|
||||
scripts/docker/base/b/
|
||||
scripts/docker/base/cver*
|
||||
scripts/docker/base/test-aac/
|
||||
scripts/docker/base/whl/
|
||||
scripts/docker/i/
|
||||
scripts/deps-docker/uncomment.py
|
||||
contrib/package/arch/pkg/
|
||||
@@ -43,3 +47,7 @@ scripts/docker/*.err
|
||||
|
||||
# nix build output link
|
||||
result
|
||||
result-*
|
||||
|
||||
# IDEA config
|
||||
.idea/
|
||||
Vendored
+34
-18
@@ -3,7 +3,7 @@
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run copyparty",
|
||||
"type": "python",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "copyparty",
|
||||
"console": "integratedTerminal",
|
||||
@@ -11,30 +11,46 @@
|
||||
"justMyCode": false,
|
||||
"env": {
|
||||
"PYDEVD_DISABLE_FILE_VALIDATION": "1",
|
||||
"PYTHONWARNINGS": "always", //error
|
||||
"PYTHONWARNINGS": "always" //error
|
||||
},
|
||||
"args": [
|
||||
//"-nw",
|
||||
"-ed",
|
||||
"-emp",
|
||||
//"-nw", // no-write; for testing uploads without writing to disk
|
||||
//"-q", // quiet; speedboost when console output is not needed
|
||||
|
||||
// # increase debugger performance:
|
||||
//"no-htp",
|
||||
//"hash-mt=0",
|
||||
//"mtag-mt=1",
|
||||
//"th-mt=1",
|
||||
|
||||
// # listen for FTP and TFTP
|
||||
"--ftp=3921",
|
||||
"--ftp-pr=12000-12099",
|
||||
"--tftp=3969",
|
||||
|
||||
// # listen on all IPv6, all IPv4, and unix-socket
|
||||
"-i::,unix:777:a.sock",
|
||||
|
||||
// # misc
|
||||
"--dedup",
|
||||
"-e2dsa",
|
||||
"-e2ts",
|
||||
"-mtp=.bpm=f,bin/mtag/audio-bpm.py",
|
||||
"--rss",
|
||||
"--shr=/shr",
|
||||
"--stats",
|
||||
"-z",
|
||||
|
||||
// # users + volumes
|
||||
"-aed:wark",
|
||||
"-vsrv::r:rw,ed:c,dupe",
|
||||
"-vdist:dist:r"
|
||||
"-vdist:dist:r",
|
||||
"-vsrv::r:rw,ed",
|
||||
"-vsrv/junk:junk:r:A,ed",
|
||||
"--ver"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "No debug",
|
||||
"preLaunchTask": "no_dbg",
|
||||
"type": "python",
|
||||
//"request": "attach", "port": 42069
|
||||
// fork: nc -l 42069 </dev/null
|
||||
},
|
||||
{
|
||||
"name": "Run active unit test",
|
||||
"type": "python",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "unittest",
|
||||
"console": "integratedTerminal",
|
||||
@@ -51,6 +67,6 @@
|
||||
"program": "${file}",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": false
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+17
-4
@@ -1,8 +1,21 @@
|
||||
* do something cool
|
||||
* **found a bug?** [create an issue!](https://github.com/9001/copyparty/issues) or let me know in the [discord](https://discord.gg/25J8CdTT6G) :>
|
||||
* **fixed a bug?** create a PR or post a patch! big thx in advance :>
|
||||
* **have a cool idea?** let's discuss it! anywhere's fine, you choose.
|
||||
|
||||
really tho, send a PR or an issue or whatever, all appreciated, anything goes, just behave aight 👍👍
|
||||
but please:
|
||||
|
||||
|
||||
|
||||
# do not use AI / LLM when writing code
|
||||
|
||||
copyparty is 100% organic, free-range, human-written software!
|
||||
|
||||
> ⚠ you are now entering a no-copilot zone
|
||||
|
||||
the *only* place where LLM/AI *may* be accepted is for [localization](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) if you are fluent and have confirmed that the translation is accurate.
|
||||
|
||||
sorry for the harsh tone, but this is important to me 🙏
|
||||
|
||||
but to be more specific,
|
||||
|
||||
|
||||
# contribution ideas
|
||||
@@ -26,7 +39,7 @@ if you wanna have a go at coding it up yourself then maybe mention the idea on d
|
||||
|
||||
aside from documentation and ideas, some other things that would be cool to have some help with is:
|
||||
|
||||
* **translations** -- the copyparty web-UI has translations for english and norwegian at the top of [browser.js](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/web/browser.js); if you'd like to add a translation for another language then that'd be welcome! and if that language has a grammar that doesn't fit into the way the strings are assembled, then we'll fix that as we go :>
|
||||
* **translations** -- the copyparty web-UI has translations in [copyparty/web/tl](https://github.com/9001/copyparty/tree/hovudstraum/copyparty/web/tl); if you'd like to [add a translation](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) for another language then that'd be welcome! and if that language has a grammar that doesn't fit into the way the strings are assembled, then we'll fix that as we go :>
|
||||
|
||||
* but please note that support for [RTL (Right-to-Left) languages](https://en.wikipedia.org/wiki/Right-to-left_script) is currently not planned, since the javascript is a bit too jank for that
|
||||
|
||||
|
||||
+1
-3
@@ -1,9 +1,7 @@
|
||||
# Security Policy
|
||||
|
||||
if you hit something extra juicy pls let me know on either of the following
|
||||
if you hit something extra juicy pls let me know on one of the following:
|
||||
* email -- `copyparty@ocv.ze` except `ze` should be `me`
|
||||
* [mastodon dm](https://layer8.space/@tripflag) -- `@tripflag@layer8.space`
|
||||
* [github private vulnerability report](https://github.com/9001/copyparty/security/advisories/new), wow that form is complicated
|
||||
* [twitter dm](https://twitter.com/tripflag) (if im somehow not banned yet)
|
||||
|
||||
no bug bounties sorry! all i can offer is greetz in the release notes
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# [`u2c.py`](u2c.py)
|
||||
* command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm)
|
||||
* file uploads, file-search, autoresume of aborted/broken uploads
|
||||
* sync local folder to server
|
||||
* [sync local folder to server](https://github.com/9001/copyparty/#folder-sync)
|
||||
* generally faster than browsers
|
||||
* if something breaks just restart it
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
# usage: ./bubbleparty.sh ./copyparty-sfx.py ....
|
||||
bwrap \
|
||||
--unshare-all \
|
||||
@@ -9,7 +9,7 @@ bwrap \
|
||||
--dev-bind /dev /dev \
|
||||
--dir /tmp \
|
||||
--dir /var \
|
||||
--bind $(pwd) $(pwd) \
|
||||
--bind "$(pwd)" "$(pwd)" \
|
||||
--share-net \
|
||||
--die-with-parent \
|
||||
--file 11 /etc/passwd \
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ import sqlite3
|
||||
import argparse
|
||||
|
||||
DB_VER1 = 3
|
||||
DB_VER2 = 5
|
||||
DB_VER2 = 6
|
||||
|
||||
BY_PATH = None
|
||||
NC = None
|
||||
@@ -39,7 +39,7 @@ def ls(db):
|
||||
print(f"{nfiles} files")
|
||||
print(f"{ntags} tags\n")
|
||||
|
||||
print("number of occurences for each tag,")
|
||||
print("number of occurrences for each tag,")
|
||||
print(" 'x' = file has no tags")
|
||||
print(" 't:mtp' = the mtp flag (file not mtp processed yet)")
|
||||
print()
|
||||
|
||||
@@ -46,7 +46,7 @@ def main(cli, vn, rem):
|
||||
|
||||
# uncomment one of these:
|
||||
send_http_302_temporary_redirect(cli, new_path)
|
||||
#send_http_301_permanent_redirect(cli, new_path)
|
||||
#send_errorpage_with_redirect_link(cli, new_path)
|
||||
# send_http_301_permanent_redirect(cli, new_path)
|
||||
# send_errorpage_with_redirect_link(cli, new_path)
|
||||
|
||||
return "true"
|
||||
@@ -4,6 +4,11 @@ these programs either take zero arguments, or a filepath (the affected file), or
|
||||
|
||||
run copyparty with `--help-hooks` for usage details / hook type explanations (xm/xbu/xau/xiu/xbc/xac/xbr/xar/xbd/xad/xban)
|
||||
|
||||
in particular, if a hook is loaded into copyparty with the hook-flag `c` ("check") then its exit-code controls the action that launched the hook:
|
||||
* exit-code `0` = allow the action, and/or continue running the next hook
|
||||
* exit-code `100` = allow the action, and stop running any remaining consecutive hooks
|
||||
* anything else = reject/prevent the original action, and don't run the remaining hooks
|
||||
|
||||
> **note:** in addition to event hooks (the stuff described here), copyparty has another api to run your programs/scripts while providing way more information such as audio tags / video codecs / etc and optionally daisychaining data between scripts in a processing pipeline; if that's what you want then see [mtp plugins](../mtag/) instead
|
||||
|
||||
|
||||
@@ -14,6 +19,8 @@ run copyparty with `--help-hooks` for usage details / hook type explanations (xm
|
||||
* [discord-announce.py](discord-announce.py) announces new uploads on discord using webhooks ([example](https://user-images.githubusercontent.com/241032/215304439-1c1cb3c8-ec6f-4c17-9f27-81f969b1811a.png))
|
||||
* [reject-mimetype.py](reject-mimetype.py) rejects uploads unless the mimetype is acceptable
|
||||
* [into-the-cache-it-goes.py](into-the-cache-it-goes.py) avoids bugs in caching proxies by immediately downloading each file that is uploaded
|
||||
* [podcast-normalizer.py](podcast-normalizer.py) creates a second file with dynamic-range-compression whenever an audio file is uploaded
|
||||
* good example of the `idx` [hook effect](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hook-effects) to tell copyparty about additional files to scan/index
|
||||
|
||||
|
||||
# upload batches
|
||||
@@ -25,10 +32,20 @@ these are `--xiu` hooks; unlike `xbu` and `xau` (which get executed on every sin
|
||||
# before upload
|
||||
* [reject-extension.py](reject-extension.py) rejects uploads if they match a list of file extensions
|
||||
* [reloc-by-ext.py](reloc-by-ext.py) redirects an upload to another destination based on the file extension
|
||||
* good example of the `reloc` [hook effect](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hook-effects)
|
||||
* [reject-and-explain.py](reject-and-explain.py) shows a custom error-message when it rejects an upload
|
||||
* [reject-ramdisk.py](reject-ramdisk.py) rejects the upload if the destination is a ramdisk
|
||||
* this hook uses the `I` flag which makes it 140x faster, but if the plugin has a bug it may crash copyparty
|
||||
|
||||
|
||||
# on message
|
||||
* [wget.py](wget.py) lets you download files by POSTing URLs to copyparty
|
||||
* [wget-i.py](wget-i.py) is an import-safe modification of this hook (starts 140x faster, but higher chance of bugs)
|
||||
* [qbittorrent-magnet.py](qbittorrent-magnet.py) starts downloading a torrent if you post a magnet url
|
||||
* [usb-eject.py](usb-eject.py) adds web-UI buttons to safe-remove usb flashdrives shared through copyparty
|
||||
* [msg-log.py](msg-log.py) is a guestbook; logs messages to a doc in the same folder
|
||||
|
||||
|
||||
# general concept demos
|
||||
* [import-me.py](import-me.py) shows how the `I` flag makes the hook 140x faster (but you need to be Very Careful when writing the plugin)
|
||||
* [wget-i.py](wget-i.py) is an import-safe modification of [wget.py](wget.py)
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from typing import Any
|
||||
|
||||
_ = r"""
|
||||
the fastest hook in the west
|
||||
(runs directly inside copyparty, not as a subprocess)
|
||||
|
||||
example usage as global config:
|
||||
--xbu I,bin/hooks/import-me.py
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:r:rw,ed:c,xbu=I,bin/hooks/import-me.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all uploads with the params listed below)
|
||||
|
||||
example usage as a volflag in a copyparty config file:
|
||||
[/inc]
|
||||
srv/inc
|
||||
accs:
|
||||
r: *
|
||||
rw: ed
|
||||
flags:
|
||||
xbu: I,bin/hooks/import-me.py
|
||||
|
||||
parameters explained,
|
||||
I = import; do not fork / subprocess
|
||||
|
||||
IMPORTANT NOTE:
|
||||
because this hook is running inside copyparty, you need to
|
||||
be EXCEPTIONALLY CAREFUL to avoid side-effects, for example
|
||||
DO NOT os.chdir() or anything like that, and also make sure
|
||||
that the name of this file is unique (cannot be the same as
|
||||
an existing python module/library)
|
||||
"""
|
||||
|
||||
|
||||
def main(ka: dict[str, Any]) -> dict[str, Any]:
|
||||
# "ka" is a dictionary with info from copyparty...
|
||||
|
||||
# but because we are running inside copyparty, we don't need such courtesies;
|
||||
import inspect
|
||||
|
||||
cf = inspect.currentframe().f_back.f_back.f_back
|
||||
t = "hello from hook; I am able to peek into copyparty's memory like so:\n function name: %s\n variables:\n %s\n"
|
||||
t2 = "\n ".join([("%r: %r" % (k, v))[:99] for k, v in cf.f_locals.items()][:9])
|
||||
logger = ka["log"]
|
||||
logger(t % (cf.f_code, t2))
|
||||
|
||||
# must return a dictionary with:
|
||||
# "rc": the retcode; 0 is ok
|
||||
return {"rc": 0}
|
||||
+1
-1
@@ -9,7 +9,7 @@ from plyer import notification
|
||||
_ = r"""
|
||||
show os notification on upload; works on windows, linux, macos, android
|
||||
|
||||
depdencies:
|
||||
dependencies:
|
||||
windows: python3 -m pip install --user -U plyer
|
||||
linux: python3 -m pip install --user -U plyer
|
||||
macos: python3 -m pip install --user -U plyer pyobjus
|
||||
|
||||
Executable
+121
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import subprocess as sp
|
||||
|
||||
|
||||
_ = r"""
|
||||
sends all uploaded audio files through an aggressive
|
||||
dynamic-range-compressor to even out the volume levels
|
||||
|
||||
dependencies:
|
||||
ffmpeg
|
||||
|
||||
being an xau hook, this gets eXecuted After Upload completion
|
||||
but before copyparty has started hashing/indexing the file, so
|
||||
we'll create a second normalized copy in a subfolder and tell
|
||||
copyparty to hash/index that additional file as well
|
||||
|
||||
example usage as global config:
|
||||
-e2d -e2t --xau j,c1,bin/hooks/podcast-normalizer.py
|
||||
|
||||
parameters explained,
|
||||
e2d/e2t = enable database and metadata indexing
|
||||
xau = execute after upload
|
||||
j = this hook needs upload information as json (not just the filename)
|
||||
c1 = this hook returns json on stdout, so tell copyparty to read that
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc/pods:inc/pods:r:rw,ed:c,xau=j,c1,bin/hooks/podcast-normalizer.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share fs-path srv/inc/pods at URL /inc/pods,
|
||||
readable by all, read-write for user ed,
|
||||
running this xau (exec-after-upload) plugin for all uploaded files)
|
||||
|
||||
example usage as a volflag in a copyparty config file:
|
||||
[/inc/pods]
|
||||
srv/inc/pods
|
||||
accs:
|
||||
r: *
|
||||
rw: ed
|
||||
flags:
|
||||
e2d # enables file indexing
|
||||
e2t # metadata tags too
|
||||
xau: j,c1,bin/hooks/podcast-normalizer.py
|
||||
|
||||
"""
|
||||
|
||||
########################################################################
|
||||
### CONFIG
|
||||
|
||||
# filetypes to process; ignores everything else
|
||||
EXTS = "mp3 flac ogg oga opus m4a aac wav wma"
|
||||
|
||||
# the name of the subdir to put the normalized files in
|
||||
SUBDIR = "normalized"
|
||||
|
||||
########################################################################
|
||||
|
||||
|
||||
# try to enable support for crazy filenames
|
||||
try:
|
||||
from copyparty.util import fsenc
|
||||
except:
|
||||
|
||||
def fsenc(p):
|
||||
return p.encode("utf-8")
|
||||
|
||||
|
||||
def main():
|
||||
# read info from copyparty
|
||||
inf = json.loads(sys.argv[1])
|
||||
vpath = inf["vp"]
|
||||
abspath = inf["ap"]
|
||||
|
||||
# check if the file-extension is on the to-be-processed list
|
||||
ext = abspath.lower().split(".")[-1]
|
||||
if ext not in EXTS.split():
|
||||
return
|
||||
|
||||
# jump into the folder where the file was uploaded
|
||||
# and create the subfolder to place the normalized copy inside
|
||||
dirpath, filename = os.path.split(abspath)
|
||||
os.chdir(fsenc(dirpath))
|
||||
os.makedirs(SUBDIR, exist_ok=True)
|
||||
|
||||
# the input and output filenames to give ffmpeg
|
||||
fname_in = fsenc(f"./{filename}")
|
||||
fname_out = fsenc(f"{SUBDIR}/{filename}.opus")
|
||||
|
||||
# fmt: off
|
||||
# create and run the ffmpeg command
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
b"-nostdin",
|
||||
b"-hide_banner",
|
||||
b"-i", fname_in,
|
||||
b"-af", b"dynaudnorm=f=100:g=9", # the normalizer config
|
||||
b"-c:a", b"libopus",
|
||||
b"-b:a", b"128k",
|
||||
fname_out,
|
||||
]
|
||||
# fmt: on
|
||||
sp.check_output(cmd)
|
||||
|
||||
# and finally, tell copyparty about the new file
|
||||
# so it appears in the database and rss-feed:
|
||||
vpath = f"{SUBDIR}/{filename}.opus"
|
||||
print(json.dumps({"idx": {"vp": [vpath]}}))
|
||||
|
||||
# (it's fine to give it a relative path like that; it gets
|
||||
# resolved relative to the folder the file was uploaded into)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception as ex:
|
||||
print("podcast-normalizer failed; %r" % (ex,))
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
_ = r"""
|
||||
reject file upload (with a nice explanation why)
|
||||
|
||||
example usage as global config:
|
||||
--xbu j,c1,bin/hooks/reject-and-explain.py
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:r:rw,ed:c,xbu=j,c1,bin/hooks/reject-and-explain.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all uploads with the params listed below)
|
||||
|
||||
example usage as a volflag in a copyparty config file:
|
||||
[/inc]
|
||||
srv/inc
|
||||
accs:
|
||||
r: *
|
||||
rw: ed
|
||||
flags:
|
||||
xbu: j,c1,bin/hooks/reject-and-explain.py
|
||||
|
||||
parameters explained,
|
||||
xbu = execute-before-upload (can also be xau, execute-after-upload)
|
||||
j = this hook needs upload information as json (not just the filename)
|
||||
c1 = this hook returns json on stdout, so tell copyparty to read that
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
inf = json.loads(sys.argv[1])
|
||||
vdir, fn = os.path.split(inf["vp"])
|
||||
print("inf[vp] = %r" % (inf["vp"],), file=sys.stderr)
|
||||
|
||||
# the following is what decides if we'll accept the upload or reject it:
|
||||
# we check if the upload-folder url matches the following regex-pattern:
|
||||
ok = re.search(r"(^|/)day[0-9]+$", vdir, re.IGNORECASE)
|
||||
|
||||
if ok:
|
||||
# allow the upload
|
||||
print("{}")
|
||||
return
|
||||
|
||||
# the upload was rejected; display the following errortext:
|
||||
errmsg = "Files can only be uploaded into a folder named 'DayN' where N is a number, for example 'Day573'. This file was REJECTED: "
|
||||
errmsg += inf["vp"] # if you want to mention the file's url
|
||||
print(json.dumps({"rejectmsg": errmsg}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import threading
|
||||
from argparse import Namespace
|
||||
|
||||
from jinja2.nodes import Name
|
||||
from copyparty.fsutil import Fstab
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
_ = r"""
|
||||
reject an upload if the target folder is on a ramdisk; useful when you
|
||||
have a volume where some folders inside are ramdisks but others aren't
|
||||
|
||||
example usage as global config:
|
||||
--xbu I,bin/hooks/reject-ramdisk.py
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:r:rw,ed:c,xbu=I,bin/hooks/reject-ramdisk.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all uploads with the params listed below)
|
||||
|
||||
example usage as a volflag in a copyparty config file:
|
||||
[/inc]
|
||||
srv/inc
|
||||
accs:
|
||||
r: *
|
||||
rw: ed
|
||||
flags:
|
||||
xbu: I,bin/hooks/reject-ramdisk.py
|
||||
|
||||
parameters explained,
|
||||
I = import; do not fork / subprocess
|
||||
|
||||
IMPORTANT NOTE:
|
||||
because this hook is imported inside copyparty, you need to
|
||||
be EXCEPTIONALLY CAREFUL to avoid side-effects, for example
|
||||
DO NOT os.chdir() or anything like that, and also make sure
|
||||
that the name of this file is unique (cannot be the same as
|
||||
an existing python module/library)
|
||||
"""
|
||||
|
||||
|
||||
mutex = threading.Lock()
|
||||
fstab: Optional[Fstab] = None
|
||||
|
||||
|
||||
def main(ka: dict[str, Any]) -> dict[str, Any]:
|
||||
global fstab
|
||||
with mutex:
|
||||
log = ka["log"] # this is a copyparty NamedLogger function
|
||||
if not fstab:
|
||||
log("<HOOK:RAMDISK> creating fstab", 6)
|
||||
args = Namespace()
|
||||
args.mtab_age = 1 # cache the filesystem info for 1 sec
|
||||
fstab = Fstab(log, args, False)
|
||||
|
||||
ap = ka["ap"] # abspath the upload is going to
|
||||
fs, mp = fstab.get(ap) # figure out what the filesystem is
|
||||
ramdisk = fs in ("tmpfs", "overlay") # looks like a ramdisk?
|
||||
|
||||
# log("<HOOK:RAMDISK> fs=%r" % (fs,))
|
||||
|
||||
if ramdisk:
|
||||
t = "Upload REJECTED because destination is a ramdisk"
|
||||
return {"rc": 1, "rejectmsg": t}
|
||||
|
||||
return {"rc": 0}
|
||||
@@ -71,6 +71,9 @@ def main():
|
||||
## selecting it inside the print at the end:
|
||||
##
|
||||
|
||||
# move all uploads to one specific folder
|
||||
into_junk = {"vp": "/junk"}
|
||||
|
||||
# create a subfolder named after the filetype and move it into there
|
||||
into_subfolder = {"vp": ext}
|
||||
|
||||
@@ -92,8 +95,8 @@ def main():
|
||||
by_category = {} # no action
|
||||
|
||||
# now choose the default effect to apply; can be any of these:
|
||||
# into_subfolder into_toplevel into_sibling by_category
|
||||
effect = {"vp": "/junk"}
|
||||
# into_junk into_subfolder into_toplevel into_sibling by_category
|
||||
effect = into_sibling
|
||||
|
||||
##
|
||||
## but we can keep going, adding more speicifc rules
|
||||
|
||||
+11
-6
@@ -1,11 +1,13 @@
|
||||
// see usb-eject.py for usage
|
||||
|
||||
function usbclick() {
|
||||
QS('#treeul a[href="/usb/"]').click();
|
||||
var o = QS('#treeul a[dst="/usb/"]') || QS('#treepar a[dst="/usb/"]');
|
||||
if (o)
|
||||
o.click();
|
||||
}
|
||||
|
||||
function eject_cb() {
|
||||
var t = this.responseText;
|
||||
var t = ('' + this.responseText).trim();
|
||||
if (t.indexOf('can be safely unplugged') < 0 && t.indexOf('Device can be removed') < 0)
|
||||
return toast.err(30, 'usb eject failed:\n\n' + t);
|
||||
|
||||
@@ -19,11 +21,14 @@ function add_eject_2(a) {
|
||||
return;
|
||||
|
||||
var v = aw[2],
|
||||
k = 'umount_' + v,
|
||||
o = ebi(k);
|
||||
k = 'umount_' + v;
|
||||
|
||||
if (o)
|
||||
for (var b = 0; b < 9; b++) {
|
||||
var o = ebi(k);
|
||||
if (!o)
|
||||
break;
|
||||
o.parentNode.removeChild(o);
|
||||
}
|
||||
|
||||
a.appendChild(mknod('span', k, '⏏'), a);
|
||||
o = ebi(k);
|
||||
@@ -40,7 +45,7 @@ function add_eject_2(a) {
|
||||
};
|
||||
|
||||
function add_eject() {
|
||||
var o = QSA('#treeul a[href^="/usb/"]');
|
||||
var o = QSA('#treeul a[href^="/usb/"]') || QSA('#treepar a[href^="/usb/"]');
|
||||
for (var a = o.length - 1; a > 0; a--)
|
||||
add_eject_2(o[a]);
|
||||
};
|
||||
|
||||
+11
-4
@@ -4,6 +4,7 @@ import os
|
||||
import stat
|
||||
import subprocess as sp
|
||||
import sys
|
||||
from urllib.parse import unquote_to_bytes as unquote
|
||||
|
||||
|
||||
"""
|
||||
@@ -28,14 +29,20 @@ which does the following respectively,
|
||||
"""
|
||||
|
||||
|
||||
MOUNT_BASE = b"/run/media/egon/"
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
label = sys.argv[1].split(":usb-eject:")[1].split(":")[0]
|
||||
mp = "/run/media/egon/" + label
|
||||
msg = sys.argv[1]
|
||||
if msg.startswith("upload-queue-empty;"):
|
||||
return
|
||||
label = msg.split(":usb-eject:")[1].split(":")[0]
|
||||
mp = MOUNT_BASE + unquote(label)
|
||||
# print("ejecting [%s]... " % (mp,), end="")
|
||||
mp = os.path.abspath(os.path.realpath(mp.encode("utf-8")))
|
||||
mp = os.path.abspath(os.path.realpath(mp))
|
||||
st = os.lstat(mp)
|
||||
if not stat.S_ISDIR(st.st_mode):
|
||||
if not stat.S_ISDIR(st.st_mode) or not mp.startswith(MOUNT_BASE):
|
||||
raise Exception("not a regular directory")
|
||||
|
||||
# if you're running copyparty as root (thx for the faith)
|
||||
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import threading
|
||||
import subprocess as sp
|
||||
|
||||
|
||||
_ = r"""
|
||||
use copyparty as a file downloader by POSTing URLs as
|
||||
application/x-www-form-urlencoded (for example using the
|
||||
📟 message-to-server-log in the web-ui)
|
||||
|
||||
this hook is a modified copy of wget.py, modified to
|
||||
make it import-safe so it can be run with the 'I' flag,
|
||||
which speeds up the startup time of the hook by 140x
|
||||
|
||||
example usage as global config:
|
||||
--xm aw,I,bin/hooks/wget-i.py
|
||||
|
||||
parameters explained,
|
||||
xm = execute on message-to-server-log
|
||||
aw = only users with write-access can use this
|
||||
I = import; do not fork / subprocess
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:r:rw,ed:c,xm=aw,I,bin/hooks/wget.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all messages with the params explained above)
|
||||
|
||||
example usage as a volflag in a copyparty config file:
|
||||
[/inc]
|
||||
srv/inc
|
||||
accs:
|
||||
r: *
|
||||
rw: ed
|
||||
flags:
|
||||
xm: aw,I,bin/hooks/wget.py
|
||||
|
||||
the volflag examples only kicks in if you send the message
|
||||
while you're in the /inc folder (or any folder below there)
|
||||
|
||||
IMPORTANT NOTE:
|
||||
because this hook uses the 'I' flag to run inside copyparty,
|
||||
many other flags will not work (f,j,c3,t3600 as seen in the
|
||||
original wget.py), and furthermore + more importantly we
|
||||
need to be EXCEPTIONALLY CAREFUL to avoid side-effects, so
|
||||
the os.chdir has been replaced with cwd=dirpath for example
|
||||
"""
|
||||
|
||||
|
||||
def do_stuff(inf):
|
||||
"""
|
||||
worker function which is executed in another thread to
|
||||
avoid blocking copyparty while the download is running,
|
||||
since we cannot use the 'f,t3600' hook-flags with 'I'
|
||||
"""
|
||||
|
||||
# first things first; grab the logger-function which copyparty is letting us borrow
|
||||
log = inf["log"]
|
||||
|
||||
url = inf["txt"]
|
||||
if url.startswith("upload-queue-empty;"):
|
||||
return
|
||||
|
||||
if "://" not in url:
|
||||
url = "https://" + url
|
||||
|
||||
proto = url.split("://")[0].lower()
|
||||
if proto not in ("http", "https", "ftp", "ftps"):
|
||||
raise Exception("bad proto {}".format(proto))
|
||||
|
||||
dirpath = inf["ap"]
|
||||
|
||||
name = url.split("?")[0].split("/")[-1]
|
||||
msg = "-- DOWNLOADING " + name
|
||||
log(msg)
|
||||
tfn = os.path.join(dirpath, msg)
|
||||
open(tfn, "wb").close()
|
||||
|
||||
cmd = ["wget", "--trust-server-names", "-nv", "--", url]
|
||||
|
||||
try:
|
||||
# two things to note here:
|
||||
# - cannot use the `c3` hook-flag with `I` so mute output with stdout=sp.DEVNULL instead;
|
||||
# - MUST NOT use os.chdir with 'I' so use cwd=dirpath instead
|
||||
sp.check_call(cmd, cwd=dirpath, stdout=sp.DEVNULL)
|
||||
except:
|
||||
t = "-- FAILED TO DOWNLOAD " + name
|
||||
log(t, 3) # 3=yellow=warning
|
||||
open(os.path.join(dirpath, t), "wb").close()
|
||||
raise # have copyparty scream about the details in the log
|
||||
|
||||
os.unlink(tfn)
|
||||
|
||||
|
||||
def main(inf):
|
||||
threading.Thread(target=do_stuff, args=(inf,), daemon=True).start()
|
||||
+4
-1
@@ -47,6 +47,9 @@ while you're in the /inc folder (or any folder below there)
|
||||
def main():
|
||||
inf = json.loads(sys.argv[1])
|
||||
url = inf["txt"]
|
||||
if url.startswith("upload-queue-empty;"):
|
||||
return
|
||||
|
||||
if "://" not in url:
|
||||
url = "https://" + url
|
||||
|
||||
@@ -66,7 +69,7 @@ def main():
|
||||
try:
|
||||
sp.check_call(cmd)
|
||||
except:
|
||||
t = "-- FAILED TO DONWLOAD " + name
|
||||
t = "-- FAILED TO DOWNLOAD " + name
|
||||
print(f"{t}\n", end="")
|
||||
open(t, "wb").close()
|
||||
|
||||
|
||||
@@ -68,3 +68,8 @@ instead of affecting all volumes, you can set the options for just one volume li
|
||||
* `:c,mtp=key=f,audio-key.py`
|
||||
* `:c,mtp=.bpm=f,audio-bpm.py`
|
||||
* `:c,mtp=ahash,vhash=f,media-hash.py`
|
||||
|
||||
|
||||
# tips & tricks
|
||||
|
||||
* to delete tags for all files below `blog*` and rescan that, `sqlite3 .hist/up2k.db "delete from mt where w in (select substr(w,1,16) from up where rd like 'blog%')";`
|
||||
+19
-3
@@ -4,13 +4,19 @@ import os
|
||||
import sys
|
||||
import tempfile
|
||||
import subprocess as sp
|
||||
import keyfinder
|
||||
|
||||
try:
|
||||
import keyfinder
|
||||
|
||||
PKF = True
|
||||
except:
|
||||
PKF = False
|
||||
|
||||
from copyparty.util import fsenc
|
||||
|
||||
"""
|
||||
dep: github/mixxxdj/libkeyfinder
|
||||
dep: pypi/keyfinder
|
||||
dep: pypi/keyfinder -OR- EvanPurkhiser/keyfinder-cli
|
||||
dep: ffmpeg
|
||||
"""
|
||||
|
||||
@@ -35,7 +41,17 @@ def det(tf):
|
||||
])
|
||||
# fmt: on
|
||||
|
||||
print(keyfinder.key(tf).camelot())
|
||||
if PKF:
|
||||
print(keyfinder.key(tf).camelot())
|
||||
else:
|
||||
# fmt: off
|
||||
sp.check_call([
|
||||
b"keyfinder-cli",
|
||||
b"-n",
|
||||
b"camelot",
|
||||
fsenc(tf)
|
||||
])
|
||||
# fmt: on
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
+5
-1
@@ -2,11 +2,15 @@
|
||||
|
||||
import sys
|
||||
import json
|
||||
import zlib
|
||||
import struct
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
try:
|
||||
from zlib_ng import zlib_ng as zlib
|
||||
except:
|
||||
import zlib
|
||||
|
||||
try:
|
||||
from copyparty.util import fsenc
|
||||
except:
|
||||
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
from copyparty.util import fsenc, runcmd
|
||||
|
||||
|
||||
"""
|
||||
uses exiftool to geotag images based on embedded gps coordinates in exif data
|
||||
|
||||
adds four new metadata keys:
|
||||
.gps_lat = latitute
|
||||
.gps_lon = longitude
|
||||
.masl = meters above sea level
|
||||
city = "city, subregion, region"
|
||||
|
||||
usage: -mtp .masl,.gps_lat,.gps_lon,city=ad,t10,bin/mtag/geotag.py
|
||||
|
||||
example: https://a.ocv.me/pub/blog/j7/8/?grid=0
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
cmd = b"exiftool -api geolocation -n".split(b" ")
|
||||
rc, so, se = runcmd(cmd + [fsenc(sys.argv[1])])
|
||||
ptn = re.compile("([^:]*[^ :]) *: (.*)")
|
||||
city = ["", "", ""]
|
||||
ret = {}
|
||||
for ln in so.split("\n"):
|
||||
m = ptn.match(ln)
|
||||
if not m:
|
||||
continue
|
||||
k, v = m.groups()
|
||||
if k == "Geolocation City":
|
||||
city[2] = v
|
||||
elif k == "Geolocation Subregion":
|
||||
city[1] = v
|
||||
elif k == "Geolocation Region":
|
||||
city[0] = v
|
||||
elif k == "GPS Latitude":
|
||||
ret[".gps_lat"] = "%.04f" % (float(v),)
|
||||
elif k == "GPS Longitude":
|
||||
ret[".gps_lon"] = "%.04f" % (float(v),)
|
||||
elif k == "GPS Altitude":
|
||||
ret[".masl"] = str(int(float(v)))
|
||||
v = ", ".join(city).strip(", ")
|
||||
if v:
|
||||
ret["city"] = v
|
||||
print(json.dumps(ret))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -7,7 +7,7 @@ example copyparty config to use this:
|
||||
--urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=guestbook=t10,ad,p,bin/mtag/guestbook-read.py:mte=+guestbook
|
||||
|
||||
explained:
|
||||
for realpath srv/hello (served at /hello), write-only for eveyrone,
|
||||
for realpath srv/hello (served at /hello), write-only for everyone,
|
||||
enable file analysis on upload (e2ts),
|
||||
use mtp plugin "bin/mtag/guestbook-read.py" to provide metadata tag "guestbook",
|
||||
do this on all uploads regardless of extension,
|
||||
|
||||
@@ -11,7 +11,7 @@ example copyparty config to use this:
|
||||
--urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=xgb=ebin,t10,ad,p,bin/mtag/guestbook.py:mte=+xgb
|
||||
|
||||
explained:
|
||||
for realpath srv/hello (served at /hello),write-only for eveyrone,
|
||||
for realpath srv/hello (served at /hello),write-only for everyone,
|
||||
enable file analysis on upload (e2ts),
|
||||
use mtp plugin "bin/mtag/guestbook.py" to provide metadata tag "xgb",
|
||||
do this on all uploads with the file extension "bin",
|
||||
|
||||
@@ -22,6 +22,8 @@ set -e
|
||||
# modifies the keyfinder python lib to load the .so in ~/pe
|
||||
|
||||
|
||||
export FORCE_COLOR=1
|
||||
|
||||
linux=1
|
||||
|
||||
win=
|
||||
@@ -153,6 +155,11 @@ install_keyfinder() {
|
||||
return
|
||||
}
|
||||
|
||||
(cat /etc/alpine-release || echo a) 2>&1 | grep -E '3\.2[3-9]' && {
|
||||
echo "alpine too new; ffmpeg8 is keyfinder-py incompat; giving up"
|
||||
return
|
||||
}
|
||||
|
||||
cd "$td"
|
||||
github_tarball https://api.github.com/repos/mixxxdj/libkeyfinder/releases/latest
|
||||
ls -al
|
||||
@@ -186,12 +193,15 @@ install_keyfinder() {
|
||||
echo "so not found at $sop"
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
x=${-//[^x]/}; set -x; cat /etc/alpine-release || true
|
||||
# rm -rf /Users/ed/Library/Python/3.9/lib/python/site-packages/*keyfinder*
|
||||
CFLAGS="-I$h/pe/keyfinder/include -I/opt/local/include -I/usr/include/ffmpeg" \
|
||||
CXXFLAGS="-I$h/pe/keyfinder/include -I/opt/local/include -I/usr/include/ffmpeg" \
|
||||
LDFLAGS="-L$h/pe/keyfinder/lib -L$h/pe/keyfinder/lib64 -L/opt/local/lib" \
|
||||
PKG_CONFIG_PATH=/c/msys64/mingw64/lib/pkgconfig \
|
||||
PKG_CONFIG_PATH="/c/msys64/mingw64/lib/pkgconfig:$h/pe/keyfinder/lib/pkgconfig" \
|
||||
$pybin -m pip install --user keyfinder
|
||||
[ "$x" ] || set +x
|
||||
|
||||
pypath="$($pybin -c 'import keyfinder; print(keyfinder.__file__)')"
|
||||
for pyso in "${pypath%/*}"/*.so; do
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ def main():
|
||||
# on success, delete the .bin file which contains the URL
|
||||
os.unlink(fp)
|
||||
except:
|
||||
open("-- FAILED TO DONWLOAD " + name, "wb").close()
|
||||
open("-- FAILED TO DOWNLOAD " + name, "wb").close()
|
||||
|
||||
os.unlink(tfn)
|
||||
print(url)
|
||||
|
||||
+22
-13
@@ -6,8 +6,8 @@ __copyright__ = 2019
|
||||
__license__ = "MIT"
|
||||
__url__ = "https://github.com/9001/copyparty/"
|
||||
|
||||
S_VERSION = "2.0"
|
||||
S_BUILD_DT = "2024-10-01"
|
||||
S_VERSION = "2.2"
|
||||
S_BUILD_DT = "2025-12-16"
|
||||
|
||||
"""
|
||||
mount a copyparty server (local or remote) as a filesystem
|
||||
@@ -99,7 +99,7 @@ except:
|
||||
elif MACOS:
|
||||
libfuse = "install https://osxfuse.github.io/"
|
||||
else:
|
||||
libfuse = "apt install libfuse3-3\n modprobe fuse"
|
||||
libfuse = "apt install libfuse2\n modprobe fuse"
|
||||
|
||||
m = """\033[33m
|
||||
could not import fuse; these may help:
|
||||
@@ -284,8 +284,8 @@ class Gateway(object):
|
||||
if ar.td:
|
||||
self.ssl_context = ssl._create_unverified_context()
|
||||
elif ar.te:
|
||||
self.ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS)
|
||||
self.ssl_context.load_verify_locations(ar.te)
|
||||
self.ssl_context = ssl.create_default_context(cafile=ar.te)
|
||||
self.ssl_context.check_hostname = ar.teh
|
||||
|
||||
self.conns = {}
|
||||
|
||||
@@ -359,7 +359,7 @@ class Gateway(object):
|
||||
def sendreq(self, meth, path, headers, **kwargs):
|
||||
tid = get_tid()
|
||||
if self.password:
|
||||
headers["Cookie"] = "=".join(["cppwd", self.password])
|
||||
headers["PW"] = self.password
|
||||
|
||||
try:
|
||||
c = self.getconn(tid)
|
||||
@@ -902,9 +902,7 @@ class CPPF(Operations):
|
||||
return ret
|
||||
|
||||
def _readdir(self, path, fh=None):
|
||||
path = path.strip("/")
|
||||
dbg("readdir %r [%s]", path, fh)
|
||||
|
||||
dbg("dircache miss")
|
||||
ret = self.gw.listdir(path)
|
||||
if not self.n_dircache:
|
||||
return ret
|
||||
@@ -914,11 +912,17 @@ class CPPF(Operations):
|
||||
self.dircache.append(cn)
|
||||
self.clean_dircache()
|
||||
|
||||
# import pprint; pprint.pprint(ret)
|
||||
return ret
|
||||
|
||||
def readdir(self, path, fh=None):
|
||||
return [".", ".."] + list(self._readdir(path, fh))
|
||||
dbg("readdir %r [%s]", path, fh)
|
||||
path = path.strip("/")
|
||||
cn = self.get_cached_dir(path)
|
||||
if cn:
|
||||
ret = cn.data
|
||||
else:
|
||||
ret = self._readdir(path, fh)
|
||||
return [".", ".."] + list(ret)
|
||||
|
||||
def read(self, path, length, offset, fh=None):
|
||||
req_max = 1024 * 1024 * 8
|
||||
@@ -993,7 +997,6 @@ class CPPF(Operations):
|
||||
if cn:
|
||||
dents = cn.data
|
||||
else:
|
||||
dbg("cache miss")
|
||||
dents = self._readdir(dirpath)
|
||||
|
||||
try:
|
||||
@@ -1141,10 +1144,15 @@ def main():
|
||||
if WINDOWS:
|
||||
examples.append("http://192.168.1.69:3923/music/ M:")
|
||||
|
||||
epi = "example:" + ex_pre + ex_pre.join(examples)
|
||||
epi += """\n
|
||||
NOTE: if server has --usernames enabled, then password is "username:password"
|
||||
"""
|
||||
|
||||
ap = argparse.ArgumentParser(
|
||||
formatter_class=TheArgparseFormatter,
|
||||
description="mount a copyparty server as a local filesystem -- " + ver,
|
||||
epilog="example:" + ex_pre + ex_pre.join(examples),
|
||||
epilog=epi,
|
||||
)
|
||||
# fmt: off
|
||||
ap.add_argument("base_url", type=str, help="remote copyparty URL to mount")
|
||||
@@ -1157,6 +1165,7 @@ def main():
|
||||
|
||||
ap2 = ap.add_argument_group("https/TLS")
|
||||
ap2.add_argument("-te", metavar="PEMFILE", help="certificate to expect/verify")
|
||||
ap2.add_argument("-teh", action="store_true", help="require correct hostname in -te cert")
|
||||
ap2.add_argument("-td", action="store_true", help="disable certificate check")
|
||||
|
||||
ap2 = ap.add_argument_group("cache/perf")
|
||||
|
||||
+1
-1
@@ -141,7 +141,7 @@ chmod 777 "$jail/tmp"
|
||||
|
||||
|
||||
# create a dev
|
||||
(cd $jail; mkdir -p dev; cd dev
|
||||
(cd "$jail"; mkdir -p dev; cd dev
|
||||
[ -e null ] || mknod -m 666 null c 1 3
|
||||
[ -e zero ] || mknod -m 666 zero c 1 5
|
||||
[ -e random ] || mknod -m 444 random c 1 8
|
||||
|
||||
+59
-29
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
S_VERSION = "2.10"
|
||||
S_BUILD_DT = "2025-02-19"
|
||||
S_VERSION = "2.19"
|
||||
S_BUILD_DT = "2026-01-18"
|
||||
|
||||
"""
|
||||
u2c.py: upload to copyparty
|
||||
@@ -10,7 +10,7 @@ u2c.py: upload to copyparty
|
||||
https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py
|
||||
|
||||
- dependencies: no
|
||||
- supports python 2.6, 2.7, and 3.3 through 3.12
|
||||
- supports python 2.6, 2.7, and 3.3 through 3.14
|
||||
- if something breaks just try again and it'll autoresume
|
||||
"""
|
||||
|
||||
@@ -52,6 +52,7 @@ if PY2:
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
bytes = str
|
||||
files_decoder = lambda s: unicode(s, "utf8")
|
||||
else:
|
||||
from urllib.parse import quote_from_bytes as quote
|
||||
from urllib.parse import unquote_to_bytes as unquote
|
||||
@@ -61,6 +62,7 @@ else:
|
||||
from queue import Queue
|
||||
|
||||
unicode = str
|
||||
files_decoder = unicode
|
||||
|
||||
|
||||
WTF8 = "replace" if PY2 else "surrogateescape"
|
||||
@@ -98,7 +100,7 @@ except:
|
||||
ub64enc = base64.urlsafe_b64encode
|
||||
|
||||
|
||||
class BadAuth(Exception):
|
||||
class Fatal(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@@ -163,8 +165,8 @@ class HCli(object):
|
||||
elif self.verify is True:
|
||||
self.ctx = None
|
||||
else:
|
||||
self.ctx = ssl.SSLContext(ssl.PROTOCOL_TLS)
|
||||
self.ctx.load_verify_locations(self.verify)
|
||||
self.ctx = ssl.create_default_context(cafile=self.verify)
|
||||
self.ctx.check_hostname = ar.teh
|
||||
|
||||
self.base_hdrs = {
|
||||
"Accept": "*/*",
|
||||
@@ -194,6 +196,8 @@ class HCli(object):
|
||||
hdrs.update(self.base_hdrs)
|
||||
if self.ar.a:
|
||||
hdrs["PW"] = self.ar.a
|
||||
if self.ar.ba:
|
||||
hdrs["Authorization"] = self.ar.ba
|
||||
if ctype:
|
||||
hdrs["Content-Type"] = ctype
|
||||
if meth == "POST" and CLEN not in hdrs:
|
||||
@@ -230,6 +234,7 @@ class HCli(object):
|
||||
|
||||
MJ = "application/json"
|
||||
MO = "application/octet-stream"
|
||||
MM = "application/x-www-form-urlencoded"
|
||||
CLEN = "Content-Length"
|
||||
|
||||
web = None # type: HCli
|
||||
@@ -489,6 +494,12 @@ print = safe_print if VT100 else flushing_print
|
||||
|
||||
|
||||
def termsize():
|
||||
try:
|
||||
w, h = os.get_terminal_size()
|
||||
return w, h
|
||||
except:
|
||||
pass
|
||||
|
||||
env = os.environ
|
||||
|
||||
def ioctl_GWINSZ(fd):
|
||||
@@ -588,9 +599,10 @@ def undns(url):
|
||||
|
||||
def _scd(err, top):
|
||||
"""non-recursive listing of directory contents, along with stat() info"""
|
||||
top_ = os.path.join(top, b"")
|
||||
with os.scandir(top) as dh:
|
||||
for fh in dh:
|
||||
abspath = os.path.join(top, fh.name)
|
||||
abspath = top_ + fh.name
|
||||
try:
|
||||
yield [abspath, fh.stat()]
|
||||
except Exception as ex:
|
||||
@@ -599,8 +611,9 @@ def _scd(err, top):
|
||||
|
||||
def _lsd(err, top):
|
||||
"""non-recursive listing of directory contents, along with stat() info"""
|
||||
top_ = os.path.join(top, b"")
|
||||
for name in os.listdir(top):
|
||||
abspath = os.path.join(top, name)
|
||||
abspath = top_ + name
|
||||
try:
|
||||
yield [abspath, os.stat(abspath)]
|
||||
except Exception as ex:
|
||||
@@ -675,7 +688,7 @@ def walkdirs(err, tops, excl):
|
||||
yield stop, ap[len(stop) :].lstrip(sep), inf
|
||||
else:
|
||||
d, n = top.rsplit(sep, 1)
|
||||
yield d, n, os.stat(top)
|
||||
yield d or b"/", n, os.stat(top)
|
||||
|
||||
|
||||
# mostly from copyparty/util.py
|
||||
@@ -822,10 +835,15 @@ def handshake(ar, file, search):
|
||||
url = ""
|
||||
url = ar.vtop + url
|
||||
|
||||
t0 = time.time()
|
||||
tmax = t0 + ar.t_hs
|
||||
while True:
|
||||
sc = 600
|
||||
txt = ""
|
||||
t0 = time.time()
|
||||
t1 = time.time()
|
||||
if t1 >= tmax:
|
||||
print("\nERROR: server offline for longer than --t-hs; giving up")
|
||||
raise Fatal()
|
||||
try:
|
||||
zs = json.dumps(req, separators=(",\n", ": "))
|
||||
sc, txt = web.req("POST", url, {}, zs.encode("utf-8"), MJ)
|
||||
@@ -846,13 +864,13 @@ def handshake(ar, file, search):
|
||||
return [], False
|
||||
elif sc == 409 or "<pre>upload rejected, file already exists" in txt:
|
||||
return [], False
|
||||
elif sc == 403:
|
||||
elif sc == 403 or sc == 401:
|
||||
print("\nERROR: login required, or wrong password:\n%s" % (txt,))
|
||||
raise BadAuth()
|
||||
raise Fatal()
|
||||
|
||||
t = "handshake failed, retrying: %s\n t0=%.3f t1=%.3f td=%.3f\n %s\n\n"
|
||||
t = "handshake failed, retrying: %s\n t0=%.3f t1=%.3f t2=%.3f td1=%.3f td2=%.3f\n %s\n\n"
|
||||
now = time.time()
|
||||
eprint(t % (file.name, t0, now, now - t0, em))
|
||||
eprint(t % (file.name, t0, t1, now, now - t0, now - t1, em))
|
||||
time.sleep(ar.cd)
|
||||
|
||||
try:
|
||||
@@ -975,6 +993,7 @@ class Ctl(object):
|
||||
self.nfiles, self.nbytes = self.stats
|
||||
self.filegen = walkdirs([], ar.files, ar.x)
|
||||
self.recheck = [] # type: list[File]
|
||||
self.last_file = None
|
||||
|
||||
if ar.safe:
|
||||
self._safe()
|
||||
@@ -1011,6 +1030,11 @@ class Ctl(object):
|
||||
|
||||
self._fancy()
|
||||
|
||||
file = self.last_file
|
||||
if self.up_br and file:
|
||||
zs = quotep(file.name.encode("utf-8", WTF8))
|
||||
web.req("POST", file.url, {}, b"msg=upload-queue-empty;" + zs, MM)
|
||||
|
||||
self.ok = not self.errs
|
||||
|
||||
def _safe(self):
|
||||
@@ -1032,7 +1056,7 @@ class Ctl(object):
|
||||
print(" hs...")
|
||||
try:
|
||||
hs, _ = handshake(self.ar, file, search)
|
||||
except BadAuth:
|
||||
except Fatal:
|
||||
sys.exit(1)
|
||||
|
||||
if search:
|
||||
@@ -1221,9 +1245,7 @@ class Ctl(object):
|
||||
while req:
|
||||
print("DELETING ~%s#%s" % (srd, len(req)))
|
||||
body = json.dumps(req).encode("utf-8")
|
||||
sc, txt = web.req(
|
||||
"POST", self.ar.url + "?delete", {}, body, MJ
|
||||
)
|
||||
sc, txt = web.req("POST", "/?delete", {}, body, MJ)
|
||||
if sc == 413 and "json 2big" in txt:
|
||||
print(" (delete request too big; slicing...)")
|
||||
req = req[: len(req) // 2]
|
||||
@@ -1289,7 +1311,7 @@ class Ctl(object):
|
||||
if self.ar.jw:
|
||||
print("%s %s" % (wark, vp))
|
||||
else:
|
||||
zd = datetime.datetime.fromtimestamp(file.lmod, UTC)
|
||||
zd = datetime.datetime.fromtimestamp(max(0, file.lmod), UTC)
|
||||
dt = "%04d-%02d-%02d %02d:%02d:%02d" % (
|
||||
zd.year,
|
||||
zd.month,
|
||||
@@ -1339,7 +1361,7 @@ class Ctl(object):
|
||||
|
||||
try:
|
||||
hs, sprs = handshake(self.ar, file, search)
|
||||
except BadAuth:
|
||||
except Fatal:
|
||||
self.panik = 1
|
||||
break
|
||||
|
||||
@@ -1451,6 +1473,7 @@ class Ctl(object):
|
||||
|
||||
file = fsl.file
|
||||
cids = fsl.cids
|
||||
self.last_file = file
|
||||
|
||||
with self.mutex:
|
||||
if not self.uploader_busy:
|
||||
@@ -1525,16 +1548,17 @@ def main():
|
||||
|
||||
# fmt: off
|
||||
ap = app = argparse.ArgumentParser(formatter_class=APF, description="copyparty up2k uploader / filesearch tool " + ver, epilog="""
|
||||
NOTE:
|
||||
source file/folder selection uses rsync syntax, meaning that:
|
||||
NOTE: source file/folder selection uses rsync syntax, meaning that:
|
||||
"foo" uploads the entire folder to URL/foo/
|
||||
"foo/" uploads the CONTENTS of the folder into URL/
|
||||
NOTE: if server has --usernames enabled, then password is "username:password"
|
||||
""")
|
||||
|
||||
ap.add_argument("url", type=unicode, help="server url, including destination folder")
|
||||
ap.add_argument("files", type=unicode, nargs="+", help="files and/or folders to process")
|
||||
ap.add_argument("files", type=files_decoder, nargs="+", help="files and/or folders to process")
|
||||
ap.add_argument("-v", action="store_true", help="verbose")
|
||||
ap.add_argument("-a", metavar="PASSWD", help="password or $filepath")
|
||||
ap.add_argument("-a", metavar="PASSWD", default="", help="password (or $filepath) for copyparty (is sent in header 'PW')")
|
||||
ap.add_argument("--ba", metavar="PASSWD", default="", help="password (or $filepath) for basic-auth (usually not necessary)")
|
||||
ap.add_argument("-s", action="store_true", help="file-search (disables upload)")
|
||||
ap.add_argument("-x", type=unicode, metavar="REGEX", action="append", help="skip file if filesystem-abspath matches REGEX (option can be repeated), example: '.*/\\.hist/.*'")
|
||||
ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible")
|
||||
@@ -1572,11 +1596,13 @@ source file/folder selection uses rsync syntax, meaning that:
|
||||
ap.add_argument("-ns", action="store_true", help="no status panel (for slow consoles and macos)")
|
||||
ap.add_argument("--cxp", type=float, metavar="SEC", default=57, help="assume http connections expired after SEConds")
|
||||
ap.add_argument("--cd", type=float, metavar="SEC", default=5, help="delay before reattempting a failed handshake/upload")
|
||||
ap.add_argument("--t-hs", type=float, metavar="SEC", default=186, help="crash if handshakes fail due to server-offline for this long")
|
||||
ap.add_argument("--safe", action="store_true", help="use simple fallback approach")
|
||||
ap.add_argument("-z", action="store_true", help="ZOOMIN' (skip uploading files if they exist at the destination with the ~same last-modified timestamp, so same as yolo / turbo with date-chk but even faster)")
|
||||
|
||||
ap = app.add_argument_group("tls")
|
||||
ap.add_argument("-te", metavar="PATH", help="path to ca.pem or cert.pem to expect/verify")
|
||||
ap.add_argument("-teh", action="store_true", help="require correct hostname in -te cert")
|
||||
ap.add_argument("-td", action="store_true", help="disable certificate check")
|
||||
# fmt: on
|
||||
|
||||
@@ -1672,11 +1698,15 @@ source file/folder selection uses rsync syntax, meaning that:
|
||||
print("\n\n %s\n\n" % (t,))
|
||||
raise
|
||||
|
||||
if ar.a and ar.a.startswith("$"):
|
||||
fn = ar.a[1:]
|
||||
print("reading password from file [{0}]".format(fn))
|
||||
with open(fn, "rb") as f:
|
||||
ar.a = f.read().decode("utf-8").strip()
|
||||
for k in ("a", "ba"):
|
||||
zs = getattr(ar, k)
|
||||
if zs.startswith("$"):
|
||||
print("reading password from file [%s]" % (zs[1:],))
|
||||
with open(zs[1:], "rb") as f:
|
||||
setattr(ar, k, f.read().decode("utf-8").strip())
|
||||
|
||||
if ar.ba:
|
||||
ar.ba = "Basic " + base64.b64encode(ar.ba.encode("utf-8")).decode("utf-8")
|
||||
|
||||
for n in range(ar.rh):
|
||||
try:
|
||||
|
||||
@@ -61,6 +61,8 @@ def rep_server():
|
||||
print("copyparty says %r" % (sck.recv_string(),))
|
||||
reply = b"thx"
|
||||
# reply = b"return 1" # non-zero to block an upload
|
||||
# reply = b'{"rc":1}' # or as json, that's fine too
|
||||
# reply = b'{"rejectmsg":"naw dude"}' # or custom message
|
||||
sck.send(reply)
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
* works on windows, linux and macos
|
||||
* assumes `copyparty-sfx.py` was renamed to `copyparty.py` in the same folder as `copyparty.bat`
|
||||
|
||||
### [`setup-ashell.sh`](setup-ashell.sh)
|
||||
* run copyparty on an iPhone/iPad using [a-Shell](https://holzschu.github.io/a-Shell_iOS/)
|
||||
* not very useful due to limitations in iOS:
|
||||
* not able to share all of your phone's storage
|
||||
* cannot run in the background
|
||||
|
||||
### [`index.html`](index.html)
|
||||
* drop-in redirect from an httpd to copyparty
|
||||
* assumes the webserver and copyparty is running on the same server/IP
|
||||
@@ -50,6 +56,9 @@
|
||||
* give a 3rd argument to install it to your copyparty config
|
||||
* systemd service at [`systemd/cfssl.service`](systemd/cfssl.service)
|
||||
|
||||
### [`zfs-tune.py`](zfs-tune.py)
|
||||
* optimizes databases for optimal performance when stored on a zfs filesystem; also see [openzfs docs](https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Workload%20Tuning.html#database-workloads) and specifically the SQLite subsection
|
||||
|
||||
# OS integration
|
||||
init-scripts to start copyparty as a service
|
||||
* [`systemd/copyparty.service`](systemd/copyparty.service) runs the sfx normally
|
||||
|
||||
@@ -2,19 +2,38 @@
|
||||
# not accept more consecutive clients than what copyparty is able to;
|
||||
# nginx default is 512 (worker_processes 1, worker_connections 512)
|
||||
#
|
||||
# ======================================================================
|
||||
#
|
||||
# to reverse-proxy a specific path/subpath/location below a domain
|
||||
# (rather than a complete subdomain), for example "/qw/er", you must
|
||||
# run copyparty with --rp-loc /qw/er and also change the following:
|
||||
# location / {
|
||||
# proxy_pass http://cpp_tcp;
|
||||
# to this:
|
||||
# location /qw/er/ {
|
||||
# proxy_pass http://cpp_tcp/qw/er/;
|
||||
#
|
||||
# ======================================================================
|
||||
#
|
||||
# rarely, in some extreme usecases, it can be good to add -j0
|
||||
# (40'000 requests per second, or 20gbps upload/download in parallel)
|
||||
# but this is usually counterproductive and slightly buggy
|
||||
#
|
||||
# ======================================================================
|
||||
#
|
||||
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
|
||||
#
|
||||
# if you are behind cloudflare (or another protection service),
|
||||
# ======================================================================
|
||||
#
|
||||
# if you are behind cloudflare (or another CDN/WAF/protection service),
|
||||
# remember to reject all connections which are not coming from your
|
||||
# protection service -- for cloudflare in particular, you can
|
||||
# generate the list of permitted IP ranges like so:
|
||||
# (curl -s https://www.cloudflare.com/ips-v{4,6} | sed 's/^/allow /; s/$/;/'; echo; echo "deny all;") > /etc/nginx/cloudflare-only.conf
|
||||
#
|
||||
# and then enable it below by uncomenting the cloudflare-only.conf line
|
||||
# and then enable it below by uncommenting the cloudflare-only.conf line
|
||||
#
|
||||
# ======================================================================
|
||||
|
||||
|
||||
upstream cpp_tcp {
|
||||
@@ -66,13 +85,13 @@ server {
|
||||
proxy_buffer_size 16k;
|
||||
proxy_busy_buffers_size 24k;
|
||||
|
||||
proxy_set_header Connection "Keep-Alive";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# NOTE: with cloudflare you want this instead:
|
||||
#proxy_set_header X-Forwarded-For $http_cf_connecting_ip;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Connection "Keep-Alive";
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# NOTE: with cloudflare you want this X-Forwarded-For instead:
|
||||
#proxy_set_header X-Forwarded-For $http_cf_connecting_ip;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+293
-167
@@ -1,23 +1,28 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
with lib;
|
||||
|
||||
let
|
||||
mkKeyValue = key: value:
|
||||
mkKeyValue =
|
||||
key: value:
|
||||
if value == true then
|
||||
# sets with a true boolean value are coerced to just the key name
|
||||
# sets with a true boolean value are coerced to just the key name
|
||||
key
|
||||
else if value == false then
|
||||
# or omitted completely when false
|
||||
# or omitted completely when false
|
||||
""
|
||||
else
|
||||
(generators.mkKeyValueDefault { inherit mkValueString; } ": " key value);
|
||||
|
||||
mkAttrsString = value: (generators.toKeyValue { inherit mkKeyValue; } value);
|
||||
|
||||
mkValueString = value:
|
||||
mkValueString =
|
||||
value:
|
||||
if isList value then
|
||||
(concatStringsSep ", " (map mkValueString value))
|
||||
(concatStringsSep "," (map mkValueString value))
|
||||
else if isAttrs value then
|
||||
"\n" + (mkAttrsString value)
|
||||
else
|
||||
@@ -43,31 +48,62 @@ let
|
||||
|
||||
accountsWithPlaceholders = mapAttrs (name: attrs: passwordPlaceholder name);
|
||||
|
||||
volumesWithoutVariables = filterAttrs (k: v: !(hasInfix "\${" v.path)) cfg.volumes;
|
||||
|
||||
configStr = ''
|
||||
${mkSection "global" cfg.settings}
|
||||
${cfg.globalExtraConfig}
|
||||
${mkSection "accounts" (accountsWithPlaceholders cfg.accounts)}
|
||||
${mkSection "groups" cfg.groups}
|
||||
${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)}
|
||||
'';
|
||||
|
||||
name = "copyparty";
|
||||
cfg = config.services.copyparty;
|
||||
configFile = pkgs.writeText "${name}.conf" configStr;
|
||||
runtimeConfigPath = "/run/${name}/${name}.conf";
|
||||
home = "/var/lib/${name}";
|
||||
defaultShareDir = "${home}/data";
|
||||
in {
|
||||
configFile = pkgs.writeText "copyparty.conf" configStr;
|
||||
runtimeConfigPath = "/run/copyparty/copyparty.conf";
|
||||
externalCacheDir = "/var/cache/copyparty";
|
||||
externalStateDir = "/var/lib/copyparty";
|
||||
defaultShareDir = "${externalStateDir}/data";
|
||||
in
|
||||
{
|
||||
options.services.copyparty = {
|
||||
enable = mkEnableOption "web-based file manager";
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.copyparty;
|
||||
defaultText = "pkgs.copyparty";
|
||||
description = ''
|
||||
package = mkPackageOption pkgs "copyparty" {
|
||||
extraDescription = ''
|
||||
Package of the application to run, exposed for overriding purposes.
|
||||
'';
|
||||
};
|
||||
|
||||
mkHashWrapper = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Make a shell script wrapper called {command}`copyparty-hash` with all options set here,
|
||||
that launches the hashing cli.
|
||||
'';
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "copyparty";
|
||||
description = ''
|
||||
The user that copyparty will run under.
|
||||
|
||||
If changed from default, you are responsible for making sure the user exists.
|
||||
'';
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "copyparty";
|
||||
description = ''
|
||||
The group that copyparty will run under.
|
||||
|
||||
If changed from default, you are responsible for making sure the user exists.
|
||||
'';
|
||||
};
|
||||
|
||||
openFilesLimit = mkOption {
|
||||
default = 4096;
|
||||
type = types.either types.int types.str;
|
||||
@@ -78,34 +114,48 @@ in {
|
||||
type = types.attrs;
|
||||
description = ''
|
||||
Global settings to apply.
|
||||
Directly maps to values in the [global] section of the copyparty config.
|
||||
See `${getExe cfg.package} --help` for more details.
|
||||
Directly maps to values in the `[global]` section of the copyparty config.
|
||||
Cannot set "c" or "hist", those are set by this module.
|
||||
See {command}`copyparty --help` for more details.
|
||||
'';
|
||||
default = {
|
||||
i = "127.0.0.1";
|
||||
no-reload = true;
|
||||
hist = externalCacheDir;
|
||||
};
|
||||
example = literalExpression ''
|
||||
{
|
||||
i = "0.0.0.0";
|
||||
no-reload = true;
|
||||
hist = ${externalCacheDir};
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
globalExtraConfig = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "Appended to the end of the `[global]` section verbatim. This is useful for flags which are used in a repeating manner (e.g. `ipu: 255.255.255.1=user`) which can't be repeated in the settings = {} attribute set.";
|
||||
};
|
||||
|
||||
accounts = mkOption {
|
||||
type = types.attrsOf (types.submodule ({ ... }: {
|
||||
options = {
|
||||
passwordFile = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
Runtime file path to a file containing the user password.
|
||||
Must be readable by the copyparty user.
|
||||
'';
|
||||
example = "/run/keys/copyparty/ed";
|
||||
};
|
||||
};
|
||||
}));
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
{ ... }:
|
||||
{
|
||||
options = {
|
||||
passwordFile = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
Runtime file path to a file containing the user password.
|
||||
Must be readable by the copyparty user.
|
||||
'';
|
||||
example = "/run/keys/copyparty/ed";
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
description = ''
|
||||
A set of copyparty accounts to create.
|
||||
'';
|
||||
@@ -117,75 +167,95 @@ in {
|
||||
'';
|
||||
};
|
||||
|
||||
volumes = mkOption {
|
||||
type = types.attrsOf (types.submodule ({ ... }: {
|
||||
options = {
|
||||
path = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
Path of a directory to share.
|
||||
'';
|
||||
};
|
||||
access = mkOption {
|
||||
type = types.attrs;
|
||||
description = ''
|
||||
Attribute list of permissions and the users to apply them to.
|
||||
|
||||
The key must be a string containing any combination of allowed permission:
|
||||
"r" (read): list folder contents, download files
|
||||
"w" (write): upload files; need "r" to see the uploads
|
||||
"m" (move): move files and folders; need "w" at destination
|
||||
"d" (delete): permanently delete files and folders
|
||||
"g" (get): download files, but cannot see folder contents
|
||||
"G" (upget): "get", but can see filekeys of their own uploads
|
||||
"h" (html): "get", but folders return their index.html
|
||||
"a" (admin): can see uploader IPs, config-reload
|
||||
|
||||
For example: "rwmd"
|
||||
|
||||
The value must be one of:
|
||||
an account name, defined in `accounts`
|
||||
a list of account names
|
||||
"*", which means "any account"
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{
|
||||
# wG = write-upget = see your own uploads only
|
||||
wG = "*";
|
||||
# read-write-modify-delete for users "ed" and "k"
|
||||
rwmd = ["ed" "k"];
|
||||
};
|
||||
'';
|
||||
};
|
||||
flags = mkOption {
|
||||
type = types.attrs;
|
||||
description = ''
|
||||
Attribute list of volume flags to apply.
|
||||
See `${getExe cfg.package} --help-flags` for more details.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{
|
||||
# "fk" enables filekeys (necessary for upget permission) (4 chars long)
|
||||
fk = 4;
|
||||
# scan for new files every 60sec
|
||||
scan = 60;
|
||||
# volflag "e2d" enables the uploads database
|
||||
e2d = true;
|
||||
# "d2t" disables multimedia parsers (in case the uploads are malicious)
|
||||
d2t = true;
|
||||
# skips hashing file contents if path matches *.iso
|
||||
nohash = "\.iso$";
|
||||
};
|
||||
'';
|
||||
default = { };
|
||||
};
|
||||
groups = mkOption {
|
||||
type = types.attrsOf (types.listOf types.str);
|
||||
description = ''
|
||||
A set of copyparty groups to create and the users that should be part of each group.
|
||||
'';
|
||||
default = { };
|
||||
example = literalExpression ''
|
||||
{
|
||||
group_name = [ "user1" "user2" ];
|
||||
};
|
||||
}));
|
||||
'';
|
||||
};
|
||||
|
||||
volumes = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
{ ... }:
|
||||
{
|
||||
options = {
|
||||
path = mkOption {
|
||||
type = types.path;
|
||||
description = ''
|
||||
Path of a directory to share.
|
||||
'';
|
||||
};
|
||||
access = mkOption {
|
||||
type = types.attrs;
|
||||
description = ''
|
||||
Attribute list of permissions and the users to apply them to.
|
||||
|
||||
The key must be a string containing any combination of allowed permission:
|
||||
* "r" (read): list folder contents, download files
|
||||
* "w" (write): upload files; need "r" to see the uploads
|
||||
* "m" (move): move files and folders; need "w" at destination
|
||||
* "d" (delete): permanently delete files and folders
|
||||
* "g" (get): download files, but cannot see folder contents
|
||||
* "G" (upget): "get", but can see filekeys of their own uploads
|
||||
* "h" (html): "get", but folders return their index.html
|
||||
* "a" (admin): can see uploader IPs, config-reload
|
||||
|
||||
For example: "rwmd"
|
||||
|
||||
The value must be one of:
|
||||
* an account name, defined in `accounts`
|
||||
* a list of account names
|
||||
* "*", which means "any account"
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{
|
||||
# wG = write-upget = see your own uploads only
|
||||
wG = "*";
|
||||
# read-write-modify-delete for users "ed" and "k"
|
||||
rwmd = ["ed" "k"];
|
||||
};
|
||||
'';
|
||||
};
|
||||
flags = mkOption {
|
||||
type = types.attrs;
|
||||
description = ''
|
||||
Attribute list of volume flags to apply.
|
||||
See {command}`copyparty --help-flags` for more details.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{
|
||||
# "fk" enables filekeys (necessary for upget permission) (4 chars long)
|
||||
fk = 4;
|
||||
# scan for new files every 60sec
|
||||
scan = 60;
|
||||
# volflag "e2d" enables the uploads database
|
||||
e2d = true;
|
||||
# "d2t" disables multimedia parsers (in case the uploads are malicious)
|
||||
d2t = true;
|
||||
# skips hashing file contents if path matches *.iso
|
||||
nohash = "\.iso$";
|
||||
};
|
||||
'';
|
||||
default = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
description = "A set of copyparty volumes to create";
|
||||
default = {
|
||||
"/" = {
|
||||
path = defaultShareDir;
|
||||
access = { r = "*"; };
|
||||
access = {
|
||||
r = "*";
|
||||
};
|
||||
};
|
||||
};
|
||||
example = literalExpression ''
|
||||
@@ -204,80 +274,136 @@ in {
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.copyparty = {
|
||||
description = "http file sharing hub";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
config = mkIf cfg.enable (
|
||||
let
|
||||
command = "${getExe cfg.package} -c ${runtimeConfigPath}";
|
||||
in
|
||||
{
|
||||
systemd.services.copyparty = {
|
||||
description = "http file sharing hub";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
environment = {
|
||||
PYTHONUNBUFFERED = "true";
|
||||
XDG_CONFIG_HOME = "${home}/.config";
|
||||
environment = {
|
||||
PYTHONUNBUFFERED = "true";
|
||||
XDG_CONFIG_HOME = externalStateDir;
|
||||
};
|
||||
|
||||
preStart =
|
||||
let
|
||||
replaceSecretCommand =
|
||||
name: attrs:
|
||||
"${getExe pkgs.replace-secret} '${passwordPlaceholder name}' '${attrs.passwordFile}' ${runtimeConfigPath}";
|
||||
in
|
||||
''
|
||||
set -euo pipefail
|
||||
install -m 600 ${configFile} ${runtimeConfigPath}
|
||||
${concatStringsSep "\n" (mapAttrsToList replaceSecretCommand cfg.accounts)}
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = command;
|
||||
# Hardening options
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
RuntimeDirectory = [ "copyparty" ];
|
||||
RuntimeDirectoryMode = "0700";
|
||||
StateDirectory = [ "copyparty" ];
|
||||
StateDirectoryMode = "0700";
|
||||
CacheDirectory = lib.mkIf (cfg.settings ? hist) [ "copyparty" ];
|
||||
CacheDirectoryMode = lib.mkIf (cfg.settings ? hist) "0700";
|
||||
WorkingDirectory = externalStateDir;
|
||||
BindReadOnlyPaths = [
|
||||
"/nix/store"
|
||||
"-/etc/resolv.conf"
|
||||
"-/etc/nsswitch.conf"
|
||||
"-/etc/group"
|
||||
"-/etc/hosts"
|
||||
"-/etc/localtime"
|
||||
] ++ (mapAttrsToList (k: v: "-${v.passwordFile}") cfg.accounts);
|
||||
BindPaths =
|
||||
(if cfg.settings ? hist then [ cfg.settings.hist ] else [ ])
|
||||
++ [ externalStateDir ]
|
||||
++ (mapAttrsToList (k: v: v.path) volumesWithoutVariables);
|
||||
# ProtectSystem = "strict";
|
||||
# Note that unlike what 'ro' implies,
|
||||
# this actually makes it impossible to read anything in the root FS,
|
||||
# except for things explicitly mounted via `RuntimeDirectory`, `StateDirectory`, `CacheDirectory`, and `BindReadOnlyPaths`.
|
||||
# This is because TemporaryFileSystem creates a *new* *empty* filesystem for the process, so only bindmounts are visible.
|
||||
TemporaryFileSystem = "/:ro";
|
||||
PrivateTmp = true;
|
||||
PrivateDevices = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectControlGroups = true;
|
||||
RestrictSUIDSGID = true;
|
||||
PrivateMounts = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectHostname = true;
|
||||
ProtectClock = true;
|
||||
ProtectProc = "invisible";
|
||||
ProcSubset = "pid";
|
||||
RestrictNamespaces = true;
|
||||
RemoveIPC = true;
|
||||
UMask = "0077";
|
||||
LimitNOFILE = cfg.openFilesLimit;
|
||||
NoNewPrivileges = true;
|
||||
LockPersonality = true;
|
||||
RestrictRealtime = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
};
|
||||
};
|
||||
|
||||
preStart = let
|
||||
replaceSecretCommand = name: attrs:
|
||||
"${getExe pkgs.replace-secret} '${
|
||||
passwordPlaceholder name
|
||||
}' '${attrs.passwordFile}' ${runtimeConfigPath}";
|
||||
in ''
|
||||
set -euo pipefail
|
||||
install -m 600 ${configFile} ${runtimeConfigPath}
|
||||
${concatStringsSep "\n"
|
||||
(mapAttrsToList replaceSecretCommand cfg.accounts)}
|
||||
'';
|
||||
# ensure volumes exist:
|
||||
systemd.tmpfiles.settings."copyparty" = (
|
||||
lib.attrsets.mapAttrs' (
|
||||
name: value:
|
||||
lib.attrsets.nameValuePair (value.path) {
|
||||
d = {
|
||||
#: in front of things means it wont change it if the directory already exists.
|
||||
group = ":${cfg.group}";
|
||||
user = ":${cfg.user}";
|
||||
mode = ":${
|
||||
# Use volume permissions if set
|
||||
if (value.flags ? chmod_d) then
|
||||
value.flags.chmod_d
|
||||
# Else, use global permission if set
|
||||
else if (cfg.settings ? chmod-d) then
|
||||
cfg.settings.chmod-d
|
||||
# Else, use the default permission
|
||||
else
|
||||
"755"
|
||||
}";
|
||||
};
|
||||
}
|
||||
) volumesWithoutVariables
|
||||
);
|
||||
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = "${getExe cfg.package} -c ${runtimeConfigPath}";
|
||||
|
||||
# Hardening options
|
||||
User = "copyparty";
|
||||
Group = "copyparty";
|
||||
RuntimeDirectory = name;
|
||||
RuntimeDirectoryMode = "0700";
|
||||
StateDirectory = [ name "${name}/data" "${name}/.config" ];
|
||||
StateDirectoryMode = "0700";
|
||||
WorkingDirectory = home;
|
||||
TemporaryFileSystem = "/:ro";
|
||||
BindReadOnlyPaths = [
|
||||
"/nix/store"
|
||||
"-/etc/resolv.conf"
|
||||
"-/etc/nsswitch.conf"
|
||||
"-/etc/hosts"
|
||||
"-/etc/localtime"
|
||||
] ++ (mapAttrsToList (k: v: "-${v.passwordFile}") cfg.accounts);
|
||||
BindPaths = [ home ] ++ (mapAttrsToList (k: v: v.path) cfg.volumes);
|
||||
# Would re-mount paths ignored by temporary root
|
||||
#ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
PrivateTmp = true;
|
||||
PrivateDevices = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectControlGroups = true;
|
||||
RestrictSUIDSGID = true;
|
||||
PrivateMounts = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectHostname = true;
|
||||
ProtectClock = true;
|
||||
ProtectProc = "invisible";
|
||||
ProcSubset = "pid";
|
||||
RestrictNamespaces = true;
|
||||
RemoveIPC = true;
|
||||
UMask = "0077";
|
||||
LimitNOFILE = cfg.openFilesLimit;
|
||||
NoNewPrivileges = true;
|
||||
LockPersonality = true;
|
||||
RestrictRealtime = true;
|
||||
users.groups = lib.mkIf (cfg.group == "copyparty") {
|
||||
copyparty = { };
|
||||
};
|
||||
};
|
||||
users.users = lib.mkIf (cfg.user == "copyparty") {
|
||||
copyparty = {
|
||||
description = "Service user for copyparty";
|
||||
group = cfg.group;
|
||||
home = externalStateDir;
|
||||
isSystemUser = true;
|
||||
};
|
||||
};
|
||||
environment.systemPackages = lib.mkIf cfg.mkHashWrapper [
|
||||
(pkgs.writeShellScriptBin "copyparty-hash" ''
|
||||
set -a # automatically export variables
|
||||
# set same environment variables as the systemd service
|
||||
${lib.pipe config.systemd.services.copyparty.environment [
|
||||
(lib.filterAttrs (n: v: v != null && n != "PATH"))
|
||||
(lib.mapAttrs (_: v: "${v}"))
|
||||
(lib.toShellVars)
|
||||
]}
|
||||
PATH=${config.systemd.services.copyparty.environment.PATH}:$PATH
|
||||
|
||||
users.groups.copyparty = { };
|
||||
users.users.copyparty = {
|
||||
description = "Service user for copyparty";
|
||||
group = "copyparty";
|
||||
home = home;
|
||||
isSystemUser = true;
|
||||
};
|
||||
};
|
||||
exec ${command} --ah-cli
|
||||
'')
|
||||
];
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -9,10 +9,19 @@
|
||||
# you may want to:
|
||||
# change '/usr/bin/python' to another interpreter
|
||||
# change '/mnt::rw' to another location or permission-set
|
||||
# use a config file instead of command arguments, e.g.:
|
||||
# command_args="-c /etc/copyparty.conf"
|
||||
|
||||
name="$SVCNAME"
|
||||
command_background=true
|
||||
extra_commands="checkconfig"
|
||||
pidfile="/var/run/$SVCNAME.pid"
|
||||
|
||||
command="/usr/bin/python3 /usr/local/bin/copyparty-sfx.py"
|
||||
command_args="-q -v /mnt::rw"
|
||||
|
||||
checkconfig() {
|
||||
ebegin "Checking $RC_SVCNAME configuration"
|
||||
$command $command_args --exit cfg
|
||||
eend $?
|
||||
}
|
||||
@@ -1,57 +1,49 @@
|
||||
# Maintainer: icxes <dev.null@need.moe>
|
||||
# Contributor: Morgan Adamiec <morganamilo@archlinux.org>
|
||||
# NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead.
|
||||
|
||||
pkgname=copyparty
|
||||
pkgver="1.16.15"
|
||||
pkgver="1.20.8"
|
||||
pkgrel=1
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||
arch=("any")
|
||||
url="https://github.com/9001/${pkgname}"
|
||||
license=('MIT')
|
||||
depends=("python" "lsof" "python-jinja")
|
||||
depends=("bash" "python" "lsof" "python-jinja")
|
||||
makedepends=("python-wheel" "python-setuptools" "python-build" "python-installer" "make" "pigz")
|
||||
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
|
||||
"cfssl: generate TLS certificates on startup (pointless when reverse-proxied)"
|
||||
"python-mutagen: music tags (alternative)"
|
||||
"python-pillow: thumbnails for images"
|
||||
"python-pyvips: thumbnails for images (higher quality, faster, uses more ram)"
|
||||
"libkeyfinder-git: detection of musical keys"
|
||||
"qm-vamp-plugins: BPM detection"
|
||||
"python-pyopenssl: ftps functionality"
|
||||
"python-pyzmq: send zeromq messages from event-hooks"
|
||||
"python-argon2-cffi: hashed passwords in config"
|
||||
"python-impacket-git: smb support (bad idea)"
|
||||
"cfssl: generate TLS certificates on startup"
|
||||
"python-mutagen: music tags (alternative)"
|
||||
"python-paramiko: sftp server",
|
||||
"python-pillow: thumbnails for images"
|
||||
"python-pyvips: thumbnails for images (higher quality, faster, uses more ram)"
|
||||
"libkeyfinder: detection of musical keys"
|
||||
"python-pyopenssl: ftps functionality"
|
||||
"python-pyzmq: send zeromq messages from event-hooks"
|
||||
"python-argon2-cffi: hashed passwords in config"
|
||||
)
|
||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||
backup=("etc/${pkgname}.d/init" )
|
||||
sha256sums=("080844a7458073c86714dde5fa659a64120909568005420ff81a27e353e91a72")
|
||||
backup=("etc/${pkgname}/copyparty.conf" )
|
||||
sha256sums=("6b1d8d3043b8b1899cb1fb140dcac87b7bcda52ba6566f0d8ec8c0c143966ef0")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
|
||||
make
|
||||
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
|
||||
pushd copyparty/web
|
||||
make -j$(nproc)
|
||||
rm Makefile
|
||||
popd
|
||||
|
||||
python3 -m build -wn
|
||||
python -m build --wheel --no-isolation
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
python3 -m installer -d "$pkgdir" dist/*.whl
|
||||
python -m installer --destdir="$pkgdir" dist/*.whl
|
||||
|
||||
install -dm755 "${pkgdir}/etc/${pkgname}.d"
|
||||
install -dm755 "${pkgdir}/etc/${pkgname}"
|
||||
install -Dm755 "bin/prisonparty.sh" "${pkgdir}/usr/bin/prisonparty"
|
||||
install -Dm644 "contrib/package/arch/${pkgname}.conf" "${pkgdir}/etc/${pkgname}.d/init"
|
||||
install -Dm644 "contrib/package/arch/${pkgname}.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}.service"
|
||||
install -Dm644 "contrib/package/arch/prisonparty.service" "${pkgdir}/usr/lib/systemd/system/prisonparty.service"
|
||||
install -Dm644 "contrib/package/arch/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md"
|
||||
install -Dm644 "contrib/systemd/${pkgname}.conf" "${pkgdir}/etc/${pkgname}/copyparty.conf"
|
||||
install -Dm644 "contrib/systemd/${pkgname}@.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}@.service"
|
||||
install -Dm644 "contrib/systemd/${pkgname}-user.service" "${pkgdir}/usr/lib/systemd/user/${pkgname}.service"
|
||||
install -Dm644 "contrib/systemd/prisonparty@.service" "${pkgdir}/usr/lib/systemd/system/prisonparty@.service"
|
||||
install -Dm644 "contrib/systemd/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md"
|
||||
install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
|
||||
find /etc/${pkgname}.d -iname '*.conf' 2>/dev/null | grep -qE . && return
|
||||
echo "┏━━━━━━━━━━━━━━━──-"
|
||||
echo "┃ Configure ${pkgname} by adding .conf files into /etc/${pkgname}.d/"
|
||||
echo "┃ and maybe copy+edit one of the following to /etc/systemd/system/:"
|
||||
echo "┣━♦ /usr/lib/systemd/system/${pkgname}.service (standard)"
|
||||
echo "┣━♦ /usr/lib/systemd/system/prisonparty.service (chroot)"
|
||||
echo "┗━━━━━━━━━━━━━━━──-"
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
# Contributor: Beethoven <beethovenisadog@protonmail.com>
|
||||
|
||||
|
||||
pkgname=copyparty
|
||||
pkgver=1.20.8
|
||||
pkgrel=1
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||
arch=("any")
|
||||
url="https://github.com/9001/${pkgname}"
|
||||
license=('MIT')
|
||||
depends=("bash" "python3" "lsof" "python3-jinja2")
|
||||
makedepends=("python3-wheel" "python3-setuptools" "python3-build" "python3-installer" "make" "pigz")
|
||||
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
|
||||
"golang-cfssl: generate TLS certificates on startup"
|
||||
"python3-mutagen: music tags (alternative)"
|
||||
"python3-paramiko: sftp server"
|
||||
"python3-pil: thumbnails for images"
|
||||
"python3-openssl: ftps functionality"
|
||||
"python3-zmq: send zeromq messages from event-hooks"
|
||||
"python3-argon2: hashed passwords in config"
|
||||
)
|
||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||
backup=("/etc/${pkgname}.d/init" )
|
||||
sha256sums=("6b1d8d3043b8b1899cb1fb140dcac87b7bcda52ba6566f0d8ec8c0c143966ef0")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
|
||||
make
|
||||
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
python -m build --wheel --no-isolation
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
python -m installer --destdir="$pkgdir" dist/*.whl
|
||||
|
||||
install -dm755 "${pkgdir}/etc/${pkgname}.d"
|
||||
install -Dm755 "bin/prisonparty.sh" "${pkgdir}/usr/bin/prisonparty"
|
||||
install -Dm644 "contrib/package/makedeb-mpr/${pkgname}.conf" "${pkgdir}/etc/${pkgname}.d/init"
|
||||
install -Dm644 "contrib/package/makedeb-mpr/${pkgname}.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}.service"
|
||||
install -Dm644 "contrib/package/makedeb-mpr/prisonparty.service" "${pkgdir}/usr/lib/systemd/system/prisonparty.service"
|
||||
install -Dm644 "contrib/package/makedeb-mpr/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md"
|
||||
install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
}
|
||||
File renamed without changes.
+1
-1
@@ -26,7 +26,7 @@ Environment=XDG_CONFIG_HOME=/home/cpp/.config
|
||||
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
||||
|
||||
# run copyparty
|
||||
ExecStart=/usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init
|
||||
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty -c /etc/copyparty.d/init
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
File renamed without changes.
File renamed without changes.
@@ -1,45 +1,144 @@
|
||||
{ lib, stdenv, makeWrapper, fetchurl, utillinux, python, jinja2, impacket, pyftpdlib, pyopenssl, argon2-cffi, pillow, pyvips, pyzmq, ffmpeg, mutagen,
|
||||
{
|
||||
lib,
|
||||
buildPythonApplication,
|
||||
fetchurl,
|
||||
util-linux,
|
||||
python,
|
||||
setuptools,
|
||||
jinja2,
|
||||
impacket,
|
||||
pyopenssl,
|
||||
cfssl,
|
||||
argon2-cffi,
|
||||
pillow,
|
||||
pyvips,
|
||||
pyzmq,
|
||||
ffmpeg,
|
||||
mutagen,
|
||||
paramiko,
|
||||
pyftpdlib,
|
||||
magic,
|
||||
partftpy,
|
||||
fusepy, # for partyfuse
|
||||
|
||||
# use argon2id-hashed passwords in config files (sha2 is always available)
|
||||
withHashedPasswords ? true,
|
||||
# use argon2id-hashed passwords in config files (sha2 is always available)
|
||||
withHashedPasswords ? true,
|
||||
|
||||
# generate TLS certificates on startup (pointless when reverse-proxied)
|
||||
withCertgen ? false,
|
||||
# generate TLS certificates on startup (pointless when reverse-proxied)
|
||||
withCertgen ? false,
|
||||
|
||||
# create thumbnails with Pillow; faster than FFmpeg / MediaProcessing
|
||||
withThumbnails ? true,
|
||||
# create thumbnails with Pillow; faster than FFmpeg / MediaProcessing
|
||||
withThumbnails ? true,
|
||||
|
||||
# create thumbnails with PyVIPS; even faster, uses more memory
|
||||
# -- can be combined with Pillow to support more filetypes
|
||||
withFastThumbnails ? false,
|
||||
# create thumbnails with PyVIPS; even faster, uses more memory
|
||||
# -- can be combined with Pillow to support more filetypes
|
||||
withFastThumbnails ? false,
|
||||
|
||||
# enable FFmpeg; thumbnails for most filetypes (also video and audio), extract audio metadata, transcode audio to opus
|
||||
# -- possibly dangerous if you allow anonymous uploads, since FFmpeg has a huge attack surface
|
||||
# -- can be combined with Thumbnails and/or FastThumbnails, since FFmpeg is slower than both
|
||||
withMediaProcessing ? true,
|
||||
# enable FFmpeg; thumbnails for most filetypes (also video and audio), extract audio metadata, transcode audio to opus
|
||||
# -- possibly dangerous if you allow anonymous uploads, since FFmpeg has a huge attack surface
|
||||
# -- can be combined with Thumbnails and/or FastThumbnails, since FFmpeg is slower than both
|
||||
withMediaProcessing ? true,
|
||||
|
||||
# if MediaProcessing is not enabled, you probably want this instead (less accurate, but much safer and faster)
|
||||
withBasicAudioMetadata ? false,
|
||||
# if MediaProcessing is not enabled, you probably want this instead (less accurate, but much safer and faster)
|
||||
withBasicAudioMetadata ? false,
|
||||
|
||||
# send ZeroMQ messages from event-hooks
|
||||
withZeroMQ ? true,
|
||||
# send ZeroMQ messages from event-hooks
|
||||
withZeroMQ ? true,
|
||||
|
||||
# enable FTPS support in the FTP server
|
||||
withFTPS ? false,
|
||||
# enable SFTP server
|
||||
withSFTP ? false,
|
||||
|
||||
# samba/cifs server; dangerous and buggy, enable if you really need it
|
||||
withSMB ? false,
|
||||
# enable FTP server
|
||||
withFTP ? true,
|
||||
|
||||
# enable FTPS support in the FTP server
|
||||
withFTPS ? false,
|
||||
|
||||
# enable TFTP server
|
||||
withTFTP ? false,
|
||||
|
||||
# samba/cifs server; dangerous and buggy, enable if you really need it
|
||||
withSMB ? false,
|
||||
|
||||
# enables filetype detection for nameless uploads
|
||||
withMagic ? false,
|
||||
|
||||
# extra packages to add to the PATH
|
||||
extraPackages ? [ ],
|
||||
|
||||
# function that accepts a python packageset and returns a list of packages to
|
||||
# be added to the python venv. useful for scripts and such that require
|
||||
# additional dependencies
|
||||
extraPythonPackages ? (_p: [ ]),
|
||||
|
||||
# to build stable + unstable with the same file
|
||||
stable ? true,
|
||||
|
||||
# for commit date, only used when stable = false
|
||||
copypartyFlake ? null,
|
||||
|
||||
nix-gitignore,
|
||||
}:
|
||||
|
||||
let
|
||||
pinData = lib.importJSON ./pin.json;
|
||||
pyEnv = python.withPackages (ps:
|
||||
with ps; [
|
||||
runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg);
|
||||
inherit (copypartyFlake) lastModifiedDate;
|
||||
# ex: "1970" "01" "01"
|
||||
dateStringsZeroPrefixed = {
|
||||
year = builtins.substring 0 4 lastModifiedDate;
|
||||
month = builtins.substring 4 2 lastModifiedDate;
|
||||
day = builtins.substring 6 2 lastModifiedDate;
|
||||
};
|
||||
# ex: "1970" "1" "1"
|
||||
dateStringsShort = builtins.mapAttrs (_: val: toString (lib.toIntBase10 val)) dateStringsZeroPrefixed;
|
||||
unstableVersion =
|
||||
if copypartyFlake == null then
|
||||
"${pinData.version}-unstable"
|
||||
else
|
||||
with dateStringsZeroPrefixed; "${pinData.version}-unstable-${year}-${month}-${day}"
|
||||
;
|
||||
version = if stable then pinData.version else unstableVersion;
|
||||
stableSrc = fetchurl {
|
||||
inherit (pinData) url hash;
|
||||
};
|
||||
root = ../../../..;
|
||||
unstableSrc = nix-gitignore.gitignoreSource [] root;
|
||||
src = if stable then stableSrc else unstableSrc;
|
||||
rev = copypartyFlake.shortRev or copypartyFlake.dirtyShortRev or "unknown";
|
||||
unstableCodename = "unstable" + (lib.optionalString (copypartyFlake != null) "-${rev}");
|
||||
in
|
||||
buildPythonApplication {
|
||||
pname = "copyparty";
|
||||
inherit version src;
|
||||
postPatch = lib.optionalString (!stable) ''
|
||||
old_src="$(mktemp -d)"
|
||||
tar -C "$old_src" -xf ${stableSrc}
|
||||
declare -a folders
|
||||
folders=("$old_src"/*)
|
||||
count_folders="''${#folders[@]}"
|
||||
if [[ $count_folders != 1 ]]; then
|
||||
declare -p folders
|
||||
echo "Expected 1 folder, found $count_folders" >&2
|
||||
exit 1
|
||||
fi
|
||||
old_src_folder="''${folders[0]}"
|
||||
cp -r "$old_src_folder"/copyparty/web/deps copyparty/web/deps
|
||||
sed -i 's/^CODENAME =.*$/CODENAME = "${unstableCodename}"/' copyparty/__version__.py
|
||||
${lib.optionalString (copypartyFlake != null) (with dateStringsShort; ''
|
||||
sed -i 's/^BUILD_DT =.*$/BUILD_DT = (${year}, ${month}, ${day})/' copyparty/__version__.py
|
||||
'')}
|
||||
'';
|
||||
dependencies =
|
||||
[
|
||||
jinja2
|
||||
fusepy
|
||||
]
|
||||
++ lib.optional withSMB impacket
|
||||
++ lib.optional withSFTP paramiko
|
||||
++ lib.optional withFTP pyftpdlib
|
||||
++ lib.optional withFTPS pyopenssl
|
||||
++ lib.optional withTFTP partftpy
|
||||
++ lib.optional withCertgen cfssl
|
||||
++ lib.optional withThumbnails pillow
|
||||
++ lib.optional withFastThumbnails pyvips
|
||||
@@ -47,22 +146,24 @@ let
|
||||
++ lib.optional withBasicAudioMetadata mutagen
|
||||
++ lib.optional withHashedPasswords argon2-cffi
|
||||
++ lib.optional withZeroMQ pyzmq
|
||||
);
|
||||
in stdenv.mkDerivation {
|
||||
pname = "copyparty";
|
||||
version = pinData.version;
|
||||
src = fetchurl {
|
||||
url = pinData.url;
|
||||
hash = pinData.hash;
|
||||
++ lib.optional withMagic magic
|
||||
++ (extraPythonPackages python.pkgs);
|
||||
makeWrapperArgs = [ "--prefix PATH : ${lib.makeBinPath runtimeDeps}" ];
|
||||
|
||||
pyproject = true;
|
||||
build-system = [
|
||||
setuptools
|
||||
];
|
||||
meta = {
|
||||
description = "Turn almost any device into a file server";
|
||||
longDescription = ''
|
||||
Portable file server with accelerated resumable uploads, dedup, WebDAV, SFTP,
|
||||
FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps
|
||||
'';
|
||||
homepage = "https://github.com/9001/copyparty";
|
||||
changelog = "https://github.com/9001/copyparty/releases/tag/v${pinData.version}";
|
||||
license = lib.licenses.mit;
|
||||
mainProgram = "copyparty";
|
||||
sourceProvenance = [ lib.sourceTypes.fromSource ];
|
||||
};
|
||||
buildInputs = [ makeWrapper ];
|
||||
dontUnpack = true;
|
||||
dontBuild = true;
|
||||
installPhase = ''
|
||||
install -Dm755 $src $out/share/copyparty-sfx.py
|
||||
makeWrapper ${pyEnv.interpreter} $out/bin/copyparty \
|
||||
--set PATH '${lib.makeBinPath ([ utillinux ] ++ lib.optional withMediaProcessing ffmpeg)}:$PATH' \
|
||||
--add-flags "$out/share/copyparty-sfx.py"
|
||||
'';
|
||||
meta.mainProgram = "copyparty";
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.16.15/copyparty-sfx.py",
|
||||
"version": "1.16.15",
|
||||
"hash": "sha256-vpI/N01pCeCiAv3haE1Zac1X9Oyp2vqPAC1WEcl5phA="
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.20.8/copyparty-1.20.8.tar.gz",
|
||||
"version": "1.20.8",
|
||||
"hash": "sha256-ax2NMEO4sYmcsfsUDcrIe3vNpSumVm8NjsjAwUOWbvA="
|
||||
}
|
||||
@@ -11,14 +11,14 @@ import base64
|
||||
import json
|
||||
import hashlib
|
||||
import sys
|
||||
import re
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
OUTPUT_FILE = Path("pin.json")
|
||||
TARGET_ASSET = "copyparty-sfx.py"
|
||||
TARGET_ASSET = lambda version: f"copyparty-{version}.tar.gz"
|
||||
HASH_TYPE = "sha256"
|
||||
LATEST_RELEASE_URL = "https://api.github.com/repos/9001/copyparty/releases/latest"
|
||||
DOWNLOAD_URL = lambda version: f"https://github.com/9001/copyparty/releases/download/v{version}/{TARGET_ASSET}"
|
||||
DOWNLOAD_URL = lambda version: f"https://github.com/9001/copyparty/releases/download/v{version}/{TARGET_ASSET(version)}"
|
||||
|
||||
|
||||
def get_formatted_hash(binary):
|
||||
@@ -29,11 +29,13 @@ def get_formatted_hash(binary):
|
||||
return f"{HASH_TYPE}-{encoded_hash}"
|
||||
|
||||
|
||||
def version_from_sfx(binary):
|
||||
result = re.search(b'^VER = "(.*)"$', binary, re.MULTILINE)
|
||||
if result:
|
||||
return result.groups(1)[0].decode("ascii")
|
||||
def version_from_tar_gz(path):
|
||||
with tarfile.open(path) as tarball:
|
||||
release_name = tarball.getmembers()[0].name
|
||||
prefix = "copyparty-"
|
||||
|
||||
if release_name.startswith(prefix):
|
||||
return release_name.replace(prefix, "")
|
||||
raise ValueError("version not found in provided file")
|
||||
|
||||
|
||||
@@ -42,7 +44,7 @@ def remote_release_pin():
|
||||
|
||||
response = requests.get(LATEST_RELEASE_URL).json()
|
||||
version = response["tag_name"].lstrip("v")
|
||||
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET][0]
|
||||
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET(version)][0]
|
||||
download_url = asset_info["browser_download_url"]
|
||||
asset = requests.get(download_url)
|
||||
formatted_hash = get_formatted_hash(asset.content)
|
||||
@@ -52,10 +54,9 @@ def remote_release_pin():
|
||||
|
||||
|
||||
def local_release_pin(path):
|
||||
asset = path.read_bytes()
|
||||
version = version_from_sfx(asset)
|
||||
version = version_from_tar_gz(path)
|
||||
download_url = DOWNLOAD_URL(version)
|
||||
formatted_hash = get_formatted_hash(asset)
|
||||
formatted_hash = get_formatted_hash(path.read_bytes())
|
||||
|
||||
result = {"url": download_url, "version": version, "hash": formatted_hash}
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
final: prev:
|
||||
let
|
||||
fullAttrs = {
|
||||
withHashedPasswords = true;
|
||||
withCertgen = true;
|
||||
withThumbnails = true;
|
||||
withFastThumbnails = true;
|
||||
withMediaProcessing = true;
|
||||
withBasicAudioMetadata = true;
|
||||
withZeroMQ = true;
|
||||
withSFTP = true;
|
||||
withFTP = true;
|
||||
withFTPS = true;
|
||||
withTFTP = true;
|
||||
withSMB = true;
|
||||
withMagic = true;
|
||||
};
|
||||
|
||||
call = attrs: final.python3.pkgs.callPackage ./copyparty ({ ffmpeg = final.ffmpeg-full; } // attrs);
|
||||
in
|
||||
{
|
||||
copyparty = call { stable = true; };
|
||||
copyparty-unstable = call { stable = false; };
|
||||
copyparty-full = call (fullAttrs // { stable = true; });
|
||||
copyparty-unstable-full = call (fullAttrs // { stable = false; });
|
||||
|
||||
python3 = prev.python3.override {
|
||||
packageOverrides = pyFinal: pyPrev: {
|
||||
partftpy = pyFinal.callPackage ./partftpy { };
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchurl,
|
||||
setuptools,
|
||||
}:
|
||||
let
|
||||
pinData = lib.importJSON ./pin.json;
|
||||
in
|
||||
|
||||
buildPythonPackage rec {
|
||||
pname = "partftpy";
|
||||
inherit (pinData) version;
|
||||
pyproject = true;
|
||||
|
||||
src = fetchurl {
|
||||
inherit (pinData) url hash;
|
||||
};
|
||||
|
||||
build-system = [ setuptools ];
|
||||
|
||||
pythonImportsCheck = [ "partftpy.TftpServer" ];
|
||||
|
||||
meta = {
|
||||
description = "Pure Python TFTP library (copyparty edition)";
|
||||
homepage = "https://github.com/9001/partftpy";
|
||||
changelog = "https://github.com/9001/partftpy/releases/tag/${version}";
|
||||
license = lib.licenses.mit;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"url": "https://github.com/9001/partftpy/releases/download/v0.4.0/partftpy-0.4.0.tar.gz",
|
||||
"version": "0.4.0",
|
||||
"hash": "sha256-5Q2zyuJ892PGZmb+YXg0ZPW/DK8RDL1uE0j5HPd4We0="
|
||||
}
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Update the Nix package pin
|
||||
#
|
||||
# Usage: ./update.sh
|
||||
|
||||
import base64
|
||||
import json
|
||||
import hashlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
OUTPUT_FILE = Path("pin.json")
|
||||
TARGET_ASSET = lambda version: f"partftpy-{version}.tar.gz"
|
||||
HASH_TYPE = "sha256"
|
||||
LATEST_RELEASE_URL = "https://api.github.com/repos/9001/partftpy/releases/latest"
|
||||
|
||||
|
||||
def get_formatted_hash(binary):
|
||||
hasher = hashlib.new("sha256")
|
||||
hasher.update(binary)
|
||||
asset_hash = hasher.digest()
|
||||
encoded_hash = base64.b64encode(asset_hash).decode("ascii")
|
||||
return f"{HASH_TYPE}-{encoded_hash}"
|
||||
|
||||
|
||||
def remote_release_pin():
|
||||
import requests
|
||||
|
||||
response = requests.get(LATEST_RELEASE_URL).json()
|
||||
version = response["tag_name"].lstrip("v")
|
||||
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET(version)][0]
|
||||
download_url = asset_info["browser_download_url"]
|
||||
asset = requests.get(download_url)
|
||||
formatted_hash = get_formatted_hash(asset.content)
|
||||
|
||||
result = {"url": download_url, "version": version, "hash": formatted_hash}
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
result = remote_release_pin()
|
||||
|
||||
print(result)
|
||||
json_result = json.dumps(result, indent=4)
|
||||
OUTPUT_FILE.write_text(json_result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,62 @@
|
||||
Name: copyparty
|
||||
Version: $pkgver
|
||||
Release: $pkgrel
|
||||
License: MIT
|
||||
Group: Utilities
|
||||
URL: https://github.com/9001/copyparty
|
||||
Source0: copyparty-$pkgver.tar.gz
|
||||
Summary: File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++
|
||||
BuildArch: noarch
|
||||
BuildRequires: python3, python3-devel, pyproject-rpm-macros, python-setuptools, python-wheel, make
|
||||
Requires: python3, (python3-jinja2 or python-jinja2), lsof
|
||||
Recommends: ffmpeg, (golang-github-cloudflare-cfssl or cfssl), python-mutagen, python-pillow, python-pyvips
|
||||
Recommends: qm-vamp-plugins, python-argon2-cffi, (python-pyopenssl or pyopenssl), python-paramiko, python-impacket
|
||||
|
||||
%description
|
||||
Portable file server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps
|
||||
|
||||
See release at https://github.com/9001/copyparty/releases
|
||||
|
||||
%global debug_package %{nil}
|
||||
|
||||
%generate_buildrequires
|
||||
%pyproject_buildrequires
|
||||
|
||||
%prep
|
||||
%setup -q
|
||||
|
||||
%build
|
||||
cd "copyparty/web"
|
||||
make
|
||||
cd -
|
||||
%pyproject_wheel
|
||||
|
||||
%install
|
||||
mkdir -p %{buildroot}%{_bindir}
|
||||
mkdir -p %{buildroot}%{_libdir}/systemd/{system,user}
|
||||
mkdir -p %{buildroot}/etc/%{name}
|
||||
mkdir -p %{buildroot}/var/lib/%{name}-jail
|
||||
mkdir -p %{buildroot}%{_datadir}/licenses/%{name}
|
||||
|
||||
%pyproject_install
|
||||
%pyproject_save_files copyparty
|
||||
|
||||
install -m 0755 bin/prisonparty.sh %{buildroot}%{_bindir}/prisonpary.sh
|
||||
install -m 0644 contrib/systemd/%{name}.conf %{buildroot}/etc/%{name}/%{name}.conf
|
||||
install -m 0644 contrib/systemd/%{name}@.service %{buildroot}%{_libdir}/systemd/system/%{name}@.service
|
||||
install -m 0644 contrib/systemd/%{name}-user.service %{buildroot}%{_libdir}/systemd/user/%{name}.service
|
||||
install -m 0644 contrib/systemd/prisonparty@.service %{buildroot}%{_libdir}/systemd/system/prisonparty@.service
|
||||
install -m 0644 contrib/systemd/index.md %{buildroot}/var/lib/%{name}-jail/README.md
|
||||
install -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE
|
||||
|
||||
%files -n copyparty -f %{pyproject_files}
|
||||
%license LICENSE
|
||||
%{_bindir}/copyparty
|
||||
%{_bindir}/partyfuse
|
||||
%{_bindir}/u2c
|
||||
%{_bindir}/prisonpary.sh
|
||||
/etc/%{name}/%{name}.conf
|
||||
%{_libdir}/systemd/system/%{name}@.service
|
||||
%{_libdir}/systemd/user/%{name}.service
|
||||
%{_libdir}/systemd/system/prisonparty@.service
|
||||
/var/lib/%{name}-jail/README.md
|
||||
@@ -39,3 +39,9 @@ point `--css-browser` to one of these by URL:
|
||||
|
||||
* turns copyparty into chromecast just more flexible (and probably way more buggy)
|
||||
* usage: put the js somewhere in the webroot and `--js-browser /memes/meadup.js`
|
||||
|
||||
|
||||
|
||||
# junk
|
||||
|
||||
* [**rave.js**](./rave.js): april-fools joke, [demo (epilepsy warning)](https://cd.ocv.me/b/d2/d21/#af-9b927c42,sorthref), not maintained, very buggy
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
for (var a = 0; a < files.length; a++) {
|
||||
var file = files[a],
|
||||
is_pic = /\.(jpe?g|png|gif|webp)$/i.exec(file.vp),
|
||||
is_pic = /\.(jpe?g|png|gif|webp|jxl)$/i.exec(file.vp),
|
||||
is_audio = re_au_all.exec(file.vp),
|
||||
basename = file.vp.replace(/\.[^\.]+$/, ""),
|
||||
entry = pairs[basename];
|
||||
|
||||
@@ -12,6 +12,23 @@ almost the same as minimal-up2k.html except this one...:
|
||||
|
||||
-- looks slightly better
|
||||
|
||||
|
||||
========================
|
||||
== USAGE INSTRUCTIONS ==
|
||||
|
||||
1. create a volume which anyone can read from (if you haven't already)
|
||||
2. copy this file into that volume, so anyone can download it
|
||||
3. enable the plugin by telling the webbrowser to load this file;
|
||||
assuming the URL to the public volume is /res/, and
|
||||
assuming you're using config-files, then add this to your config:
|
||||
|
||||
[global]
|
||||
js-browser: /res/minimal-up2k.js
|
||||
|
||||
alternatively, if you're not using config-files, then
|
||||
add the following commandline argument instead:
|
||||
--js-browser=/res/minimal-up2k.js
|
||||
|
||||
*/
|
||||
|
||||
var u2min = `
|
||||
|
||||
@@ -101,10 +101,10 @@
|
||||
|
||||
function our_hotkey_handler(e) {
|
||||
// bail if either ALT, CTRL, or SHIFT is pressed
|
||||
if (e.altKey || e.shiftKey || e.isComposing || ctrl(e))
|
||||
if (anymod(e))
|
||||
return main_hotkey_handler(e); // let copyparty handle this keystroke
|
||||
|
||||
var key_name = (e.code || e.key) + '',
|
||||
var keycode = (e.key || e.code) + '',
|
||||
ae = document.activeElement,
|
||||
aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : '';
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
if (aet && !/^(a|button|tr|td|div|pre)$/.test(aet))
|
||||
return main_hotkey_handler(e); // let copyparty handle this keystroke
|
||||
|
||||
if (key_name == 'KeyW') {
|
||||
if (keycode == 'w' || keycode == 'KeyW') {
|
||||
// okay, this one's for us... do the thing
|
||||
action_to_perform();
|
||||
return ev(e);
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
# copyparty with Podman and Systemd
|
||||
|
||||
Use this configuration if you want to run copyparty in a Podman container, with the reliability of running the container under a systemd service.
|
||||
|
||||
Documentation for `.container` files can be found in the [Container unit](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#container-units-container) docs. Systemd does not understand `.container` files natively, so Podman converts these to `.service` files with a [systemd-generator](https://www.freedesktop.org/software/systemd/man/latest/systemd.generator.html). This process is transparent, but sometimes needs to be debugged in case your `.container` file is malformed. There are instructions to debug the systemd generator in the Troubleshooting section below.
|
||||
|
||||
To run copyparty in this way, you must already have podman installed. To install Podman, see: https://podman.io/docs/installation
|
||||
|
||||
There is a sample configuration file in the same directory as this file (`copyparty.conf`).
|
||||
|
||||
## Run the container as root
|
||||
|
||||
Running the container as the root user is easy to set up, but less secure. There are instructions in the next section to run the container as a rootless user if you'd rather run the container like that.
|
||||
|
||||
First, change this line in the `copyparty.container` file to reflect the directory you want to share. By default, it shares `/mnt/` but you'll probably want to change that.
|
||||
|
||||
```
|
||||
# Change /mnt to something you want to share
|
||||
Volume=/mnt:/w:z
|
||||
```
|
||||
|
||||
Note that you can select the owner and group of this volume by changing the `uid:` and `gid:` of the volume in `copyparty.conf`, but for simplicity let's assume you want it to be owned by `root:root`.
|
||||
|
||||
To install and start copyparty with Podman and systemd as the root user, run the following:
|
||||
|
||||
```shell
|
||||
sudo mkdir -pv /etc/containers/systemd/ /etc/copyparty/
|
||||
sudo cp -v copyparty.container /etc/containers/systemd/
|
||||
sudo cp -v copyparty.conf /etc/copyparty/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl start copyparty
|
||||
```
|
||||
|
||||
Note: You can't "enable" this kind of Podman service. The `[Install]` section of the `.container` file effectively handles enabling the service so that it starts when the server reboots.
|
||||
|
||||
You can see the status of the service with:
|
||||
|
||||
```shell
|
||||
sudo systemctl status -a copyparty
|
||||
```
|
||||
|
||||
You can see (and follow) the logs with either of these commands:
|
||||
|
||||
```shell
|
||||
sudo podman logs -f copyparty
|
||||
|
||||
# -a is required or else you'll get output like: copyparty[549025]: [649B blob data]
|
||||
sudo journalctl -a -f -u copyparty
|
||||
```
|
||||
|
||||
## Run the container as a non-root user
|
||||
|
||||
This configuration is more secure, but is more involved and requires ensuring files have proper permissions. You will need a root user account to do some of this setup.
|
||||
|
||||
First, you need a user to run the container as. In this example we'll create a "podman" user with UID=1001 and GID=1001.
|
||||
|
||||
```shell
|
||||
sudo groupadd -g 1001 podman
|
||||
sudo useradd -u 1001 -m podman
|
||||
sudo usermod -aG podman podman
|
||||
sudo loginctl enable-linger podman
|
||||
# Set a strong password for this user
|
||||
sudo -u podman passwd
|
||||
```
|
||||
|
||||
The `enable-linger` command allows the podman user to run systemd user services that persist even when the user is not logged in. You could use a user that already exists in the system to run this service as, just make sure to run `loginctl enable-linger USERNAME` for that user.
|
||||
|
||||
Next, change these lines in the `copyparty.container` file to reflect the config directory and the directory you want to share. By default, the config shares `/home/podman/copyparty/sharing/` but you'll probably want to change this:
|
||||
|
||||
```
|
||||
# Change to reflect your non-root user's home directory
|
||||
Volume=/home/podman/copyparty/config:/cfg:z
|
||||
|
||||
# Change to the directory you want to share
|
||||
Volume=/home/podman/copyparty/sharing:/w:z
|
||||
```
|
||||
|
||||
Make sure the podman user has read/write access to both of these directories.
|
||||
|
||||
Next, **log in to the server as the podman user**.
|
||||
|
||||
To install and start copyparty as the non-root podman user, run the following:
|
||||
|
||||
```shell
|
||||
mkdir -pv /home/podman/.config/containers/systemd/ /home/podman/copyparty/config
|
||||
cp -v copyparty.container /home/podman/.config/containers/systemd/copyparty.container
|
||||
cp -v copyparty.conf /home/podman/copyparty/config
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user start copyparty
|
||||
```
|
||||
|
||||
**Important note: Never use `sudo` with `systemctl --user`!**
|
||||
|
||||
You can check the status of the user service with:
|
||||
|
||||
```shell
|
||||
systemctl --user status -a copyparty
|
||||
```
|
||||
|
||||
You can see (and follow) the logs with:
|
||||
|
||||
```shell
|
||||
podman logs -f copyparty
|
||||
|
||||
journalctl --user -a -f -u copyparty
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the container fails to start, and you've modified the `.container` service, it's likely that your `.container` file failed to be translated into a `.service` file. You can debug the podman service generator with this command:
|
||||
|
||||
```shell
|
||||
sudo /usr/lib/systemd/system-generators/podman-system-generator --dryrun
|
||||
```
|
||||
|
||||
## Allowing Traffic from Outside your Server
|
||||
|
||||
To allow traffic on port 3923 of your server, you should run:
|
||||
|
||||
```shell
|
||||
sudo firewall-cmd --permanent --add-port=3923/tcp
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
Otherwise, you won't be able to access the copyparty server from anywhere other than the server itself.
|
||||
|
||||
## Updating copyparty
|
||||
|
||||
To update the version of copyparty used in the container, you can:
|
||||
|
||||
```shell
|
||||
# If root:
|
||||
sudo podman pull docker.io/copyparty/ac:latest
|
||||
sudo systemctl restart copyparty
|
||||
|
||||
# If non-root:
|
||||
podman pull docker.io/copyparty/ac:latest
|
||||
systemctl --user restart copyparty
|
||||
```
|
||||
|
||||
Or, you can change the pinned version of the image in the `[Container]` section of the `.container` file and run:
|
||||
|
||||
```shell
|
||||
# If root:
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart copyparty
|
||||
|
||||
# If non-root:
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user restart copyparty
|
||||
```
|
||||
|
||||
Podman will pull the image you've specified when restarting. If you have it set to `:latest`, Podman does not know to re-pull the container.
|
||||
|
||||
### Enabling auto-update
|
||||
|
||||
Alternatively, you can enable auto-updates by un-commenting this line:
|
||||
|
||||
```
|
||||
# AutoUpdate=registry
|
||||
```
|
||||
|
||||
You will also need to enable the [podman auto-updater service](https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html) with:
|
||||
|
||||
```shell
|
||||
# If root:
|
||||
sudo systemctl enable podman-auto-update.timer podman-auto-update.service
|
||||
|
||||
# If non-root:
|
||||
systemctl --user enable podman-auto-update.timer podman-auto-update.service
|
||||
```
|
||||
|
||||
This works best if you always want the latest version of copyparty. The auto-updater runs once every 24 hours.
|
||||
@@ -0,0 +1,36 @@
|
||||
[global]
|
||||
e2dsa # enable file indexing and filesystem scanning
|
||||
e2ts # and enable multimedia indexing
|
||||
ansi # and colors in log messages
|
||||
|
||||
# uncomment the line starting with q, lo: to log to a file instead of stdout/journalctl;
|
||||
# $LOGS_DIRECTORY is usually /var/log/copyparty (comes from systemd)
|
||||
# and copyparty replaces %Y-%m%d with Year-MonthDay, so the
|
||||
# full path will be something like /var/log/copyparty/2023-1130.txt
|
||||
# (note: enable compression by adding .xz at the end)
|
||||
# q, lo: $LOGS_DIRECTORY/%Y-%m%d.log
|
||||
|
||||
# p: 80,443,3923 # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE)
|
||||
# i: 127.0.0.1 # only allow connections from localhost (reverse-proxies)
|
||||
# ftp: 3921 # enable ftp server on port 3921
|
||||
# p: 3939 # listen on another port
|
||||
# df: 16 # stop accepting uploads if less than 16 GB free disk space
|
||||
# ver # show copyparty version in the controlpanel
|
||||
# grid # show thumbnails/grid-view by default
|
||||
# theme: 2 # monokai
|
||||
# name: datasaver # change the server-name that's displayed in the browser
|
||||
# stats, nos-dup # enable the prometheus endpoint, but disable the dupes counter (too slow)
|
||||
# no-robots, force-js # make it harder for search engines to read your server
|
||||
|
||||
|
||||
[accounts]
|
||||
ed: wark # username: password
|
||||
|
||||
|
||||
[/] # create a volume at "/" (the webroot), which will
|
||||
/w # share the contents of the "/w" folder
|
||||
accs:
|
||||
rw: * # everyone gets read-write access, but
|
||||
rwmda: ed # the user "ed" gets read-write-move-delete-admin
|
||||
# uid: 1000 # If you're running as root, you can change the owner of this volume here
|
||||
# gid: 1000 # If you're running as root, you can change the group of this volume here
|
||||
@@ -0,0 +1,55 @@
|
||||
[Container]
|
||||
# It's recommended to replace :latest with a specific version
|
||||
# for example: docker.io/copyparty/ac:1.19.15
|
||||
Image=docker.io/copyparty/ac:latest
|
||||
ContainerName=copyparty
|
||||
|
||||
# Uncomment to enable auto-updates
|
||||
# AutoUpdate=registry
|
||||
|
||||
# Environment variables
|
||||
# enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram)
|
||||
Environment=LD_PRELOAD=/usr/lib/libmimalloc-secure.so.NOPE
|
||||
# ensures log-messages are not delayed (but can reduce speed a tiny bit)
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
# Ports
|
||||
PublishPort=3923:3923
|
||||
|
||||
|
||||
# Volumes (PLEASE LOOK!)
|
||||
|
||||
# Rootful setup:
|
||||
# Leave as-is
|
||||
# Non-root setup:
|
||||
# Change /etc/copyparty to /home/<USER>/copyparty/config
|
||||
Volume=/etc/copyparty:/cfg:z
|
||||
|
||||
# Rootful setup:
|
||||
# Change /mnt to the directory you want to share
|
||||
# Non-root setup:
|
||||
# Change /mnt to something owned by your user, e.g., /home/<USER>/copyparty/sharing:/w:z
|
||||
Volume=/mnt:/w:z
|
||||
|
||||
|
||||
# Give the container time to stop in case the thumbnailer is still running.
|
||||
# It's allowed to continue finishing up for 10s after the shutdown signal, give it a 5s buffer
|
||||
StopTimeout=15
|
||||
|
||||
# hide it from logs with "/._" so it matches the default --lf-url filter
|
||||
HealthCmd="wget --spider -q 127.0.0.1:3923/?reset=/._"
|
||||
HealthInterval=1m
|
||||
HealthTimeout=2s
|
||||
HealthRetries=5
|
||||
HealthStartPeriod=15s
|
||||
|
||||
[Unit]
|
||||
After=default.target
|
||||
|
||||
[Install]
|
||||
# Start by default on boot
|
||||
WantedBy=default.target
|
||||
|
||||
[Service]
|
||||
# Give the container time to start in case it needs to pull the image
|
||||
TimeoutStartSec=600
|
||||
@@ -10,7 +10,7 @@ name="copyparty"
|
||||
rcvar="copyparty_enable"
|
||||
copyparty_user="copyparty"
|
||||
copyparty_args="-e2dsa -v /storage:/storage:r" # change as you see fit
|
||||
copyparty_command="/usr/local/bin/python3.9 /usr/local/copyparty/copyparty-sfx.py ${copyparty_args}"
|
||||
copyparty_command="/usr/local/bin/python3.11 /usr/local/copyparty/copyparty-sfx.py ${copyparty_args}"
|
||||
pidfile="/var/run/copyparty/${name}.pid"
|
||||
command="/usr/sbin/daemon"
|
||||
command_args="-P ${pidfile} -r -f ${copyparty_command}"
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# this script will install copyparty onto an iOS device (iPhone/iPad)
|
||||
#
|
||||
# step 1: install a-Shell:
|
||||
# https://apps.apple.com/us/app/a-shell/id1473805438
|
||||
#
|
||||
# step 2: copypaste the following command into a-Shell:
|
||||
# curl -L https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh | sh
|
||||
#
|
||||
# step 3: launch copyparty with this command: cpp
|
||||
#
|
||||
# if you ever want to upgrade copyparty, just repeat step 2
|
||||
|
||||
|
||||
|
||||
cd "$HOME/Documents"
|
||||
curl -Locopyparty https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py
|
||||
|
||||
|
||||
|
||||
# create the config file? (cannot use heredoc because body too large)
|
||||
[ -e cpc ] || {
|
||||
echo '[global]' >cpc
|
||||
echo ' p: 80, 443, 3923 # enable http and https on these ports' >>cpc
|
||||
echo ' e2dsa # enable file indexing and filesystem scanning' >>cpc
|
||||
echo ' e2ts # and enable multimedia indexing' >>cpc
|
||||
echo ' ver # show copyparty version in the controlpanel' >>cpc
|
||||
echo ' qrz: 2 # enable qr-code and make it big' >>cpc
|
||||
echo ' qrp: 1 # reduce qr-code padding' >>cpc
|
||||
echo ' qr-fg: -1 # optimize for basic/simple terminals' >>cpc
|
||||
echo ' qr-wait: 0.3 # less chance of getting scrolled away' >>cpc
|
||||
echo '' >>cpc
|
||||
echo ' # enable these by uncommenting them:' >>cpc
|
||||
echo ' # ftp: 21 # enable ftp server on port 21' >>cpc
|
||||
echo ' # tftp: 69 # enable tftp server on port 69' >>cpc
|
||||
echo '' >>cpc
|
||||
echo '[/]' >>cpc
|
||||
echo ' ~/Documents' >>cpc
|
||||
echo ' accs:' >>cpc
|
||||
echo ' A: *' >>cpc
|
||||
}
|
||||
|
||||
|
||||
|
||||
# create the launcher?
|
||||
[ -e cpp ] || {
|
||||
echo '#!/bin/sh' >cpp
|
||||
echo '' >>cpp
|
||||
echo '# change the font so the qr-code draws correctly:' >>cpp
|
||||
echo 'config -n "Menlo" # name' >>cpp
|
||||
echo 'config -s 8 # size' >>cpp
|
||||
echo '' >>cpp
|
||||
echo '# launch copyparty' >>cpp
|
||||
echo 'exec copyparty -c cpc "$@"' >>cpp
|
||||
}
|
||||
|
||||
|
||||
|
||||
chmod 755 copyparty cpp
|
||||
echo
|
||||
echo =================================
|
||||
echo
|
||||
echo 'okay, all done!'
|
||||
echo
|
||||
echo 'you can edit your config'
|
||||
echo 'with this command: vim cpc'
|
||||
echo
|
||||
echo 'you can run copyparty'
|
||||
echo 'with this command: cpp'
|
||||
echo
|
||||
@@ -0,0 +1,26 @@
|
||||
# this will start `/usr/bin/copyparty`
|
||||
# and read config from `$HOME/.config/copyparty.conf`
|
||||
#
|
||||
# unless you add -q to disable logging, you may want to remove the
|
||||
# following line to allow buffering (slightly better performance):
|
||||
# Environment=PYTHONUNBUFFERED=x
|
||||
|
||||
[Unit]
|
||||
Description=copyparty file server
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
SyslogIdentifier=copyparty
|
||||
WorkingDirectory=/var/lib/copyparty-jail
|
||||
Environment=PYTHONUNBUFFERED=x
|
||||
Environment=PRTY_CONFIG=%h/.config/copyparty/copyparty.conf
|
||||
ExecReload=/bin/kill -s USR1 $MAINPID
|
||||
|
||||
# ensure there is a config
|
||||
ExecStartPre=/bin/bash -c 'if [[ ! -f %h/.config/copyparty/copyparty.conf ]]; then mkdir -p %h/.config/copyparty; cp /etc/copyparty/copyparty.conf %h/.config/copyparty/copyparty.conf; fi'
|
||||
|
||||
# run copyparty
|
||||
ExecStart=/usr/bin/python3 /usr/bin/copyparty
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,42 +1,13 @@
|
||||
# not actually YAML but lets pretend:
|
||||
# -*- mode: yaml -*-
|
||||
# vim: ft=yaml:
|
||||
|
||||
|
||||
# put this file in /etc/
|
||||
|
||||
|
||||
[global]
|
||||
e2dsa # enable file indexing and filesystem scanning
|
||||
e2ts # and enable multimedia indexing
|
||||
ansi # and colors in log messages
|
||||
|
||||
# disable logging to stdout/journalctl and log to a file instead;
|
||||
# $LOGS_DIRECTORY is usually /var/log/copyparty (comes from systemd)
|
||||
# and copyparty replaces %Y-%m%d with Year-MonthDay, so the
|
||||
# full path will be something like /var/log/copyparty/2023-1130.txt
|
||||
# (note: enable compression by adding .xz at the end)
|
||||
q, lo: $LOGS_DIRECTORY/%Y-%m%d.log
|
||||
|
||||
# p: 80,443,3923 # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE)
|
||||
# i: 127.0.0.1 # only allow connections from localhost (reverse-proxies)
|
||||
# ftp: 3921 # enable ftp server on port 3921
|
||||
# p: 3939 # listen on another port
|
||||
# df: 16 # stop accepting uploads if less than 16 GB free disk space
|
||||
# ver # show copyparty version in the controlpanel
|
||||
# grid # show thumbnails/grid-view by default
|
||||
# theme: 2 # monokai
|
||||
# name: datasaver # change the server-name that's displayed in the browser
|
||||
# stats, nos-dup # enable the prometheus endpoint, but disable the dupes counter (too slow)
|
||||
# no-robots, force-js # make it harder for search engines to read your server
|
||||
|
||||
i: 127.0.0.1
|
||||
|
||||
[accounts]
|
||||
ed: wark # username: password
|
||||
user: password
|
||||
|
||||
|
||||
[/] # create a volume at "/" (the webroot), which will
|
||||
/mnt # share the contents of the "/mnt" folder
|
||||
[/]
|
||||
/var/lib/copyparty-jail
|
||||
accs:
|
||||
rw: * # everyone gets read-write access, but
|
||||
rwmda: ed # the user "ed" gets read-write-move-delete-admin
|
||||
r: *
|
||||
rwdma: user
|
||||
flags:
|
||||
grid
|
||||
@@ -0,0 +1,42 @@
|
||||
# not actually YAML but lets pretend:
|
||||
# -*- mode: yaml -*-
|
||||
# vim: ft=yaml:
|
||||
|
||||
|
||||
# put this file in /etc/
|
||||
|
||||
|
||||
[global]
|
||||
e2dsa # enable file indexing and filesystem scanning
|
||||
e2ts # and enable multimedia indexing
|
||||
ansi # and colors in log messages
|
||||
|
||||
# disable logging to stdout/journalctl and log to a file instead;
|
||||
# $LOGS_DIRECTORY is usually /var/log/copyparty (comes from systemd)
|
||||
# and copyparty replaces %Y-%m%d with Year-MonthDay, so the
|
||||
# full path will be something like /var/log/copyparty/2023-1130.txt
|
||||
# (note: enable compression by adding .xz at the end)
|
||||
q, lo: $LOGS_DIRECTORY/%Y-%m%d.log
|
||||
|
||||
# p: 80,443,3923 # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE)
|
||||
# i: 127.0.0.1 # only allow connections from localhost (reverse-proxies)
|
||||
# ftp: 3921 # enable ftp server on port 3921
|
||||
# p: 3939 # listen on another port
|
||||
# df: 16 # stop accepting uploads if less than 16 GB free disk space
|
||||
# ver # show copyparty version in the controlpanel
|
||||
# grid # show thumbnails/grid-view by default
|
||||
# theme: 2 # monokai
|
||||
# name: datasaver # change the server-name that's displayed in the browser
|
||||
# stats, nos-dup # enable the prometheus endpoint, but disable the dupes counter (too slow)
|
||||
# no-robots, force-js # make it harder for search engines to read your server
|
||||
|
||||
|
||||
[accounts]
|
||||
ed: wark # username: password
|
||||
|
||||
|
||||
[/] # create a volume at "/" (the webroot), which will
|
||||
/mnt # share the contents of the "/mnt" folder
|
||||
accs:
|
||||
rw: * # everyone gets read-write access, but
|
||||
rwmda: ed # the user "ed" gets read-write-move-delete-admin
|
||||
@@ -0,0 +1,30 @@
|
||||
# this will start `/usr/bin/copyparty`
|
||||
# and read config from `/etc/copyparty/copyparty.conf`
|
||||
#
|
||||
# the %i refers to whatever you put after the copyparty@
|
||||
# so with copyparty@foo.service, %i == foo
|
||||
#
|
||||
# unless you add -q to disable logging, you may want to remove the
|
||||
# following line to allow buffering (slightly better performance):
|
||||
# Environment=PYTHONUNBUFFERED=x
|
||||
|
||||
[Unit]
|
||||
Description=copyparty file server
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
SyslogIdentifier=copyparty
|
||||
WorkingDirectory=/var/lib/copyparty-jail
|
||||
Environment=PYTHONUNBUFFERED=x
|
||||
Environment=PRTY_CONFIG=/etc/copyparty/copyparty.conf
|
||||
ExecReload=/bin/kill -s USR1 $MAINPID
|
||||
|
||||
# user to run as + where the TLS certificate is (if any)
|
||||
User=%i
|
||||
Environment=XDG_CONFIG_HOME=/home/%i/.config
|
||||
|
||||
# run copyparty
|
||||
ExecStart=/usr/bin/python3 /usr/bin/copyparty
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,10 @@
|
||||
this is `/var/lib/copyparty-jail`, the fallback webroot when copyparty has not yet been configured
|
||||
|
||||
please edit `/etc/copyparty/copyparty.conf` (if running as a system service)
|
||||
or `$HOME/.config/copyparty/copyparty.conf` if running as a user service
|
||||
|
||||
a basic configuration example is available at https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf
|
||||
a configuration example that explains most flags is available at https://github.com/9001/copyparty/blob/hovudstraum/docs/chungus.conf
|
||||
|
||||
the full list of configuration options can be seen at https://ocv.me/copyparty/helptext.html
|
||||
or by running `copyparty --help`
|
||||
@@ -0,0 +1,38 @@
|
||||
# this will start `/usr/bin/copyparty`
|
||||
# in a chroot, preventing accidental access elsewhere,
|
||||
# and read copyparty config from `/etc/copyparty/copyparty.conf`
|
||||
#
|
||||
# expose additional filesystem locations to copyparty
|
||||
# by listing them between the last `%i` and `--`
|
||||
#
|
||||
# `%i %i` = user/group to run copyparty as; can be IDs (1000 1000)
|
||||
# the %i refers to whatever you put after the prisonparty@
|
||||
# so with prisonparty@foo.service, %i == foo
|
||||
#
|
||||
# unless you add -q to disable logging, you may want to remove the
|
||||
# following line to allow buffering (slightly better performance):
|
||||
# Environment=PYTHONUNBUFFERED=x
|
||||
|
||||
[Unit]
|
||||
Description=copyparty file server
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
SyslogIdentifier=prisonparty
|
||||
WorkingDirectory=/var/lib/copyparty-jail
|
||||
Environment=PYTHONUNBUFFERED=x
|
||||
Environment=PRTY_CONFIG=/etc/copyparty/copyparty.conf
|
||||
ExecReload=/bin/kill -s USR1 $MAINPID
|
||||
|
||||
# user to run as + where the TLS certificate is (if any)
|
||||
User=%i
|
||||
Environment=XDG_CONFIG_HOME=/home/%i/.config
|
||||
|
||||
# run copyparty
|
||||
ExecStart=/bin/bash /usr/bin/prisonparty /var/lib/copyparty-jail %i %i \
|
||||
/etc/copyparty \
|
||||
-- \
|
||||
/usr/bin/python3 /usr/bin/copyparty
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
|
||||
"""
|
||||
when the up2k-database is stored on a zfs volume, this may give
|
||||
slightly higher performance (actual gains not measured yet)
|
||||
|
||||
NOTE: must be applied in combination with the related advice in the openzfs documentation;
|
||||
https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Workload%20Tuning.html#database-workloads
|
||||
and see specifically the SQLite subsection
|
||||
|
||||
it is assumed that all databases are stored in a single location,
|
||||
for example with `--hist /var/store/hists`
|
||||
|
||||
three alternatives for running this script:
|
||||
|
||||
1. copy it into /var/store/hists and run "python3 zfs-tune.py s"
|
||||
(s = modify all databases below folder containing script)
|
||||
|
||||
2. cd into /var/store/hists and run "python3 ~/zfs-tune.py w"
|
||||
(w = modify all databases below current working directory)
|
||||
|
||||
3. python3 ~/zfs-tune.py /var/store/hists
|
||||
|
||||
if you use docker, run copyparty with `--hist /cfg/hists`, copy this script into /cfg, and run this:
|
||||
podman run --rm -it --entrypoint /usr/bin/python3 ghcr.io/9001/copyparty-ac /cfg/zfs-tune.py s
|
||||
|
||||
"""
|
||||
|
||||
|
||||
PAGESIZE = 65536
|
||||
|
||||
|
||||
# borrowed from copyparty; short efficient stacktrace for errors
|
||||
def min_ex(max_lines: int = 8, reverse: bool = False) -> str:
|
||||
et, ev, tb = sys.exc_info()
|
||||
stb = traceback.extract_tb(tb) if tb else traceback.extract_stack()[:-1]
|
||||
fmt = "%s:%d <%s>: %s"
|
||||
ex = [fmt % (fp.split(os.sep)[-1], ln, fun, txt) for fp, ln, fun, txt in stb]
|
||||
if et or ev or tb:
|
||||
ex.append("[%s] %s" % (et.__name__ if et else "(anonymous)", ev))
|
||||
return "\n".join(ex[-max_lines:][:: -1 if reverse else 1])
|
||||
|
||||
|
||||
def set_pagesize(db_path):
|
||||
try:
|
||||
# check current page_size
|
||||
with sqlite3.connect(db_path) as db:
|
||||
v = db.execute("pragma page_size").fetchone()[0]
|
||||
if v == PAGESIZE:
|
||||
print(" `-- OK")
|
||||
return
|
||||
|
||||
# https://www.sqlite.org/pragma.html#pragma_page_size
|
||||
# `- disable wal; set pagesize; vacuum
|
||||
# (copyparty will reenable wal if necessary)
|
||||
|
||||
with sqlite3.connect(db_path) as db:
|
||||
db.execute("pragma journal_mode=delete")
|
||||
db.commit()
|
||||
|
||||
with sqlite3.connect(db_path) as db:
|
||||
db.execute(f"pragma page_size = {PAGESIZE}")
|
||||
db.execute("vacuum")
|
||||
|
||||
print(" `-- new pagesize OK")
|
||||
|
||||
except Exception:
|
||||
err = min_ex().replace("\n", "\n -- ")
|
||||
print(f"FAILED: {db_path}\n -- {err}")
|
||||
|
||||
|
||||
def main():
|
||||
top = os.path.dirname(os.path.abspath(__file__))
|
||||
cwd = os.path.abspath(os.getcwd())
|
||||
try:
|
||||
x = sys.argv[1]
|
||||
except:
|
||||
print(f"""
|
||||
this script takes one mandatory argument:
|
||||
specify 's' to start recursing from folder containing this script file ({top})
|
||||
specify 'w' to start recursing from the current working directory ({cwd})
|
||||
specify a path to start recursing from there
|
||||
""")
|
||||
sys.exit(1)
|
||||
|
||||
if x.lower() == "w":
|
||||
top = cwd
|
||||
elif x.lower() != "s":
|
||||
top = x
|
||||
|
||||
for dirpath, dirs, files in os.walk(top):
|
||||
for fname in files:
|
||||
if not fname.endswith(".db"):
|
||||
continue
|
||||
db_path = os.path.join(dirpath, fname)
|
||||
print(db_path)
|
||||
set_pagesize(db_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+34
-6
@@ -44,6 +44,8 @@ ANYWIN = WINDOWS or sys.platform in ["msys", "cygwin"]
|
||||
|
||||
MACOS = platform.system() == "Darwin"
|
||||
|
||||
GRAAL = platform.python_implementation() == "GraalVM"
|
||||
|
||||
EXE = bool(getattr(sys, "frozen", False))
|
||||
|
||||
try:
|
||||
@@ -55,7 +57,7 @@ except:
|
||||
zs = """
|
||||
web/a/partyfuse.py
|
||||
web/a/u2c.py
|
||||
web/a/webdav-cfg.bat
|
||||
web/a/webdav-cfg.txt
|
||||
web/baguettebox.js
|
||||
web/browser.css
|
||||
web/browser.html
|
||||
@@ -63,10 +65,6 @@ web/browser.js
|
||||
web/browser2.html
|
||||
web/cf.html
|
||||
web/copyparty.gif
|
||||
web/dd/2.png
|
||||
web/dd/3.png
|
||||
web/dd/4.png
|
||||
web/dd/5.png
|
||||
web/deps/busy.mp3
|
||||
web/deps/easymde.css
|
||||
web/deps/easymde.js
|
||||
@@ -80,6 +78,7 @@ web/deps/prismd.css
|
||||
web/deps/scp.woff2
|
||||
web/deps/sha512.ac.js
|
||||
web/deps/sha512.hw.js
|
||||
web/idp.html
|
||||
web/iiam.gif
|
||||
web/md.css
|
||||
web/md.html
|
||||
@@ -89,8 +88,8 @@ web/md2.js
|
||||
web/mde.css
|
||||
web/mde.html
|
||||
web/mde.js
|
||||
web/msg.css
|
||||
web/msg.html
|
||||
web/opds.xml
|
||||
web/rups.css
|
||||
web/rups.html
|
||||
web/rups.js
|
||||
@@ -102,19 +101,48 @@ web/splash.html
|
||||
web/splash.js
|
||||
web/svcs.html
|
||||
web/svcs.js
|
||||
web/tl/chi.js
|
||||
web/tl/cze.js
|
||||
web/tl/deu.js
|
||||
web/tl/epo.js
|
||||
web/tl/fin.js
|
||||
web/tl/fra.js
|
||||
web/tl/grc.js
|
||||
web/tl/hun.js
|
||||
web/tl/ita.js
|
||||
web/tl/jpn.js
|
||||
web/tl/kor.js
|
||||
web/tl/nld.js
|
||||
web/tl/nno.js
|
||||
web/tl/nor.js
|
||||
web/tl/pol.js
|
||||
web/tl/por.js
|
||||
web/tl/rus.js
|
||||
web/tl/spa.js
|
||||
web/tl/swe.js
|
||||
web/tl/tur.js
|
||||
web/tl/ukr.js
|
||||
web/tl/vie.js
|
||||
web/ui.css
|
||||
web/up2k.js
|
||||
web/util.js
|
||||
web/w.hash.js
|
||||
"""
|
||||
RES = set(zs.strip().split("\n"))
|
||||
RESM = {
|
||||
"web/a/partyfuse.txt": "web/a/partyfuse.py",
|
||||
"web/a/u2c.txt": "web/a/u2c.py",
|
||||
"web/a/webdav-cfg.bat": "web/a/webdav-cfg.txt",
|
||||
}
|
||||
|
||||
|
||||
class EnvParams(object):
|
||||
def __init__(self) -> None:
|
||||
self.t0 = time.time()
|
||||
self.mod = ""
|
||||
self.mod_ = ""
|
||||
self.cfg = ""
|
||||
self.scfg = True
|
||||
|
||||
|
||||
E = EnvParams()
|
||||
+642
-221
File diff suppressed because it is too large.
Load diff
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (1, 16, 16)
|
||||
CODENAME = "COPYparty"
|
||||
BUILD_DT = (2025, 2, 28)
|
||||
VERSION = (1, 20, 9)
|
||||
CODENAME = "sftp is fine too"
|
||||
BUILD_DT = (2026, 2, 25)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
+1077
-160
File diff suppressed because it is too large.
Load diff
+75
-10
@@ -2,15 +2,22 @@
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
from ..util import SYMTIME, fsdec, fsenc
|
||||
from . import path as path
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
_ = (path,)
|
||||
__all__ = ["path"]
|
||||
from ..util import NamedLogger
|
||||
|
||||
MKD_755 = {"chmod_d": 0o755}
|
||||
MKD_700 = {"chmod_d": 0o700}
|
||||
UTIME_CLAMPS = ((max, -2147483647), (max, 1), (min, 4294967294), (min, 2147483646))
|
||||
|
||||
_ = (path, MKD_755, MKD_700, UTIME_CLAMPS)
|
||||
__all__ = ["path", "MKD_755", "MKD_700", "UTIME_CLAMPS"]
|
||||
|
||||
# grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c
|
||||
# printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')"
|
||||
@@ -20,19 +27,39 @@ def chmod(p: str, mode: int) -> None:
|
||||
return os.chmod(fsenc(p), mode)
|
||||
|
||||
|
||||
def chown(p: str, uid: int, gid: int) -> None:
|
||||
return os.chown(fsenc(p), uid, gid)
|
||||
|
||||
|
||||
def listdir(p: str = ".") -> list[str]:
|
||||
return [fsdec(x) for x in os.listdir(fsenc(p))]
|
||||
|
||||
|
||||
def makedirs(name: str, mode: int = 0o755, exist_ok: bool = True) -> bool:
|
||||
def makedirs(name: str, vf: dict[str, Any] = MKD_755, exist_ok: bool = True) -> bool:
|
||||
# os.makedirs does 777 for all but leaf; this does mode on all
|
||||
todo = []
|
||||
bname = fsenc(name)
|
||||
try:
|
||||
os.makedirs(bname, mode)
|
||||
return True
|
||||
except:
|
||||
if not exist_ok or not os.path.isdir(bname):
|
||||
raise
|
||||
while bname:
|
||||
if os.path.isdir(bname) or bname in todo:
|
||||
break
|
||||
todo.append(bname)
|
||||
bname = os.path.dirname(bname)
|
||||
if not todo:
|
||||
if not exist_ok:
|
||||
os.mkdir(bname) # to throw
|
||||
return False
|
||||
mode = vf["chmod_d"]
|
||||
chown = "chown" in vf
|
||||
for zb in todo[::-1]:
|
||||
try:
|
||||
os.mkdir(zb, mode)
|
||||
if chown:
|
||||
os.chown(zb, vf["uid"], vf["gid"])
|
||||
except:
|
||||
if os.path.isdir(zb):
|
||||
continue
|
||||
raise
|
||||
return True
|
||||
|
||||
|
||||
def mkdir(p: str, mode: int = 0o755) -> None:
|
||||
@@ -76,6 +103,44 @@ def utime(
|
||||
return os.utime(fsenc(p), times)
|
||||
|
||||
|
||||
def utime_c(
|
||||
log: Union["NamedLogger", Any],
|
||||
p: str,
|
||||
ts: float,
|
||||
follow_symlinks: bool = True,
|
||||
throw: bool = False,
|
||||
) -> Optional[float]:
|
||||
clamp = 0
|
||||
ov = ts
|
||||
bp = fsenc(p)
|
||||
now = time.time()
|
||||
while True:
|
||||
try:
|
||||
if SYMTIME:
|
||||
os.utime(bp, (now, ts), follow_symlinks=follow_symlinks)
|
||||
else:
|
||||
os.utime(bp, (now, ts))
|
||||
if clamp:
|
||||
t = "filesystem rejected utime(%r); clamped %s to %s"
|
||||
log(t % (p, ov, ts))
|
||||
return ts
|
||||
except Exception as ex:
|
||||
pv = ts
|
||||
while clamp < len(UTIME_CLAMPS):
|
||||
fun, cv = UTIME_CLAMPS[clamp]
|
||||
ts = fun(ts, cv)
|
||||
clamp += 1
|
||||
if ts != pv:
|
||||
break
|
||||
if clamp >= len(UTIME_CLAMPS):
|
||||
if throw:
|
||||
raise
|
||||
else:
|
||||
t = "could not utime(%r) to %s; %s, %r"
|
||||
log(t % (p, ov, ex, ex))
|
||||
return None
|
||||
|
||||
|
||||
if hasattr(os, "lstat"):
|
||||
|
||||
def lstat(p: str) -> os.stat_result:
|
||||
|
||||
@@ -12,6 +12,7 @@ import queue
|
||||
from .__init__ import ANYWIN
|
||||
from .authsrv import AuthSrv
|
||||
from .broker_util import BrokerCli, ExceptionalQueue, NotExQueue
|
||||
from .fsutil import ramdisk_chk
|
||||
from .httpsrv import HttpSrv
|
||||
from .util import FAKE_MP, Daemon, HMaccas
|
||||
|
||||
@@ -56,6 +57,7 @@ class MpWorker(BrokerCli):
|
||||
|
||||
# starting to look like a good idea
|
||||
self.asrv = AuthSrv(args, None, False)
|
||||
ramdisk_chk(self.asrv)
|
||||
|
||||
# instantiate all services here (TODO: inheritance?)
|
||||
self.iphash = HMaccas(os.path.join(self.args.E.cfg, "iphash"), 8)
|
||||
@@ -99,6 +101,7 @@ class MpWorker(BrokerCli):
|
||||
if dest == "reload":
|
||||
self.logw("mpw.asrv reloading")
|
||||
self.asrv.reload()
|
||||
ramdisk_chk(self.asrv)
|
||||
self.logw("mpw.asrv reloaded")
|
||||
continue
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import traceback
|
||||
|
||||
from queue import Queue
|
||||
|
||||
|
||||
+21
-8
@@ -1,13 +1,12 @@
|
||||
import calendar
|
||||
import errno
|
||||
import filecmp
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
|
||||
from .__init__ import ANYWIN
|
||||
from .util import Netdev, load_resource, runcmd, wrename, wunlink
|
||||
from .util import Netdev, atomic_move, load_resource, runcmd, wunlink
|
||||
|
||||
HAVE_CFSSL = not os.environ.get("PRTY_NO_CFSSL")
|
||||
|
||||
@@ -21,6 +20,19 @@ else:
|
||||
VF = {"mv_re_t": 0, "rm_re_t": 0}
|
||||
|
||||
|
||||
def _sp_err(exe, what, rc, so, se, sin):
|
||||
try:
|
||||
zs = shutil.which(exe)
|
||||
except:
|
||||
zs = "<?>"
|
||||
try:
|
||||
zi = os.path.getsize(zs)
|
||||
except:
|
||||
zi = 0
|
||||
t = "failed to %s; error %s using %s (%s):\n STDOUT: %s\n STDERR: %s\n STDIN: %s\n"
|
||||
raise Exception(t % (what, rc, zs, zi, so, se, sin.decode("utf-8")))
|
||||
|
||||
|
||||
def ensure_cert(log: "RootLogger", args) -> None:
|
||||
"""
|
||||
the default cert (and the entire TLS support) is only here to enable the
|
||||
@@ -109,20 +121,20 @@ def _gen_ca(log: "RootLogger", args):
|
||||
cmd = "cfssl gencert -initca -"
|
||||
rc, so, se = runcmd(cmd.split(), 30, sin=sin)
|
||||
if rc:
|
||||
raise Exception("failed to create ca-cert: {}, {}".format(rc, se), 3)
|
||||
_sp_err("cfssl", "create ca-cert", rc, so, se, sin)
|
||||
|
||||
cmd = "cfssljson -bare ca"
|
||||
sin = so.encode("utf-8")
|
||||
rc, so, se = runcmd(cmd.split(), 10, sin=sin, cwd=args.crt_dir)
|
||||
if rc:
|
||||
raise Exception("failed to translate ca-cert: {}, {}".format(rc, se), 3)
|
||||
_sp_err("cfssljson", "translate ca-cert", rc, so, se, sin)
|
||||
|
||||
bname = os.path.join(args.crt_dir, "ca")
|
||||
try:
|
||||
wunlink(nlog, bname + ".key", VF)
|
||||
except:
|
||||
pass
|
||||
wrename(nlog, bname + "-key.pem", bname + ".key", VF)
|
||||
atomic_move(nlog, bname + "-key.pem", bname + ".key", VF)
|
||||
wunlink(nlog, bname + ".csr", VF)
|
||||
|
||||
log("cert", "new ca OK", 2)
|
||||
@@ -132,6 +144,7 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
||||
nlog: "NamedLogger" = lambda msg, c=0: log("cert-gen-srv", msg, c)
|
||||
|
||||
names = args.crt_ns.split(",") if args.crt_ns else []
|
||||
names = [x.strip() for x in names]
|
||||
if not args.crt_exact:
|
||||
for n in names[:]:
|
||||
names.append("*.{}".format(n))
|
||||
@@ -202,20 +215,20 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
||||
acmd = cmd.split() + ["-hostname=" + ",".join(names), "-"]
|
||||
rc, so, se = runcmd(acmd, 30, sin=sin, cwd=args.crt_dir)
|
||||
if rc:
|
||||
raise Exception("failed to create cert: {}, {}".format(rc, se))
|
||||
_sp_err("cfssl", "create cert", rc, so, se, sin)
|
||||
|
||||
cmd = "cfssljson -bare srv"
|
||||
sin = so.encode("utf-8")
|
||||
rc, so, se = runcmd(cmd.split(), 10, sin=sin, cwd=args.crt_dir)
|
||||
if rc:
|
||||
raise Exception("failed to translate cert: {}, {}".format(rc, se))
|
||||
_sp_err("cfssljson", "translate cert", rc, so, se, sin)
|
||||
|
||||
bname = os.path.join(args.crt_dir, "srv")
|
||||
try:
|
||||
wunlink(nlog, bname + ".key", VF)
|
||||
except:
|
||||
pass
|
||||
wrename(nlog, bname + "-key.pem", bname + ".key", VF)
|
||||
atomic_move(nlog, bname + "-key.pem", bname + ".key", VF)
|
||||
wunlink(nlog, bname + ".csr", VF)
|
||||
|
||||
with open(os.path.join(args.crt_dir, "ca.pem"), "rb") as f:
|
||||
|
||||
+135
-2
@@ -19,15 +19,20 @@ def vf_bmap() -> dict[str, str]:
|
||||
"no_clone": "noclone",
|
||||
"no_dirsz": "nodirsz",
|
||||
"no_dupe": "nodupe",
|
||||
"no_dupe_m": "nodupem",
|
||||
"no_forget": "noforget",
|
||||
"no_pipe": "nopipe",
|
||||
"no_robots": "norobots",
|
||||
"no_tail": "notail",
|
||||
"no_thumb": "dthumb",
|
||||
"no_vthumb": "dvthumb",
|
||||
"no_athumb": "dathumb",
|
||||
"vol_nospawn": "nospawn",
|
||||
"vol_or_crash": "assert_root",
|
||||
}
|
||||
for k in (
|
||||
"dedup",
|
||||
"dlni",
|
||||
"dotsrch",
|
||||
"e2d",
|
||||
"e2ds",
|
||||
@@ -43,6 +48,7 @@ def vf_bmap() -> dict[str, str]:
|
||||
"gsel",
|
||||
"hardlink",
|
||||
"magic",
|
||||
"md_no_br",
|
||||
"no_db_ip",
|
||||
"no_sb_md",
|
||||
"no_sb_lg",
|
||||
@@ -50,11 +56,27 @@ def vf_bmap() -> dict[str, str]:
|
||||
"og",
|
||||
"og_no_head",
|
||||
"og_s_title",
|
||||
"opds",
|
||||
"rand",
|
||||
"reflink",
|
||||
"rm_partial",
|
||||
"rmagic",
|
||||
"rss",
|
||||
"ui_noacci",
|
||||
"ui_nocpla",
|
||||
"ui_nolbar",
|
||||
"ui_nombar",
|
||||
"ui_nonav",
|
||||
"ui_notree",
|
||||
"ui_norepl",
|
||||
"ui_nosrvi",
|
||||
"ui_noctxb",
|
||||
"wo_up_readme",
|
||||
"wram",
|
||||
"xdev",
|
||||
"xlink",
|
||||
"xvol",
|
||||
"zipmaxu",
|
||||
):
|
||||
ret[k] = k
|
||||
return ret
|
||||
@@ -63,6 +85,7 @@ def vf_bmap() -> dict[str, str]:
|
||||
def vf_vmap() -> dict[str, str]:
|
||||
"""argv-to-volflag: simple values"""
|
||||
ret = {
|
||||
"ac_convt": "aconvt",
|
||||
"no_hash": "nohash",
|
||||
"no_idx": "noidx",
|
||||
"re_maxage": "scan",
|
||||
@@ -73,14 +96,27 @@ def vf_vmap() -> dict[str, str]:
|
||||
"th_x3": "th3x",
|
||||
}
|
||||
for k in (
|
||||
"apnd_who",
|
||||
"bup_ck",
|
||||
"cachectl",
|
||||
"casechk",
|
||||
"chmod_d",
|
||||
"chmod_f",
|
||||
"dbd",
|
||||
"db_xattr",
|
||||
"du_who",
|
||||
"epilogues",
|
||||
"ufavico",
|
||||
"forget_ip",
|
||||
"fsnt",
|
||||
"hsortn",
|
||||
"html_head",
|
||||
"html_head_s",
|
||||
"lg_sbf",
|
||||
"md_sbf",
|
||||
"lg_sba",
|
||||
"md_sba",
|
||||
"md_hist",
|
||||
"nrand",
|
||||
"u2ow",
|
||||
"og_desc",
|
||||
@@ -92,15 +128,40 @@ def vf_vmap() -> dict[str, str]:
|
||||
"og_title_i",
|
||||
"og_tpl",
|
||||
"og_ua",
|
||||
"opds_exts",
|
||||
"prologues",
|
||||
"preadmes",
|
||||
"put_ck",
|
||||
"put_name",
|
||||
"readmes",
|
||||
"mv_retry",
|
||||
"rm_retry",
|
||||
"rss_sort",
|
||||
"rss_fmt_t",
|
||||
"rss_fmt_d",
|
||||
"rw_edit",
|
||||
"shr_who",
|
||||
"sort",
|
||||
"tail_fd",
|
||||
"tail_rate",
|
||||
"tail_tmax",
|
||||
"tail_who",
|
||||
"tcolor",
|
||||
"th_qv",
|
||||
"th_qvx",
|
||||
"th_spec_p",
|
||||
"txt_eol",
|
||||
"unlist",
|
||||
"u2abort",
|
||||
"u2ts",
|
||||
"uid",
|
||||
"gid",
|
||||
"unp_who",
|
||||
"ups_who",
|
||||
"zip_who",
|
||||
"zipmaxn",
|
||||
"zipmaxs",
|
||||
"zipmaxt",
|
||||
):
|
||||
ret[k] = k
|
||||
return ret
|
||||
@@ -151,24 +212,39 @@ flagcats = {
|
||||
"dedup": "enable symlink-based file deduplication",
|
||||
"hardlink": "enable hardlink-based file deduplication,\nwith fallback on symlinks when that is impossible",
|
||||
"hardlinkonly": "dedup with hardlink only, never symlink;\nmake a full copy if hardlink is impossible",
|
||||
"reflink": "enable reflink-based file deduplication,\nwith fallback on full copy when that is impossible",
|
||||
"safededup": "verify on-disk data before using it for dedup",
|
||||
"noclone": "take dupe data from clients, even if available on HDD",
|
||||
"nodupe": "rejects existing files (instead of linking/cloning them)",
|
||||
"nodupem": "rejects existing files during moves as well",
|
||||
"casechk=auto": "actively prevent case-insensitive filesystem? y/n",
|
||||
"chmod_d=755": "unix-permission for new dirs/folders",
|
||||
"chmod_f=644": "unix-permission for new files",
|
||||
"uid=573": "change owner of new files/folders to unix-user 573",
|
||||
"gid=999": "change owner of new files/folders to unix-group 999",
|
||||
"fsnt=auto": "filesystem filename traits (lin/win/mac/auto)",
|
||||
"wram": "allow uploading into ramdisks",
|
||||
"sparse": "force use of sparse files, mainly for s3-backed storage",
|
||||
"nosparse": "deny use of sparse files, mainly for slow storage",
|
||||
"rm_partial": "delete unfinished uploads from HDD when they timeout",
|
||||
"daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
|
||||
"nosub": "forces all uploads into the top folder of the vfs",
|
||||
"magic": "enables filetype detection for nameless uploads",
|
||||
"put_name": "fallback filename for nameless uploads",
|
||||
"put_ck": "default checksum-hasher for PUT/WebDAV uploads",
|
||||
"bup_ck": "default checksum-hasher for bup/basic uploads",
|
||||
"gz": "allows server-side gzip compression of uploads with ?gz",
|
||||
"xz": "allows server-side lzma compression of uploads with ?xz",
|
||||
"pk": "forces server-side compression, optional arg: xz,9",
|
||||
},
|
||||
"upload rules": {
|
||||
"apnd_who=dw": "who can append? (aw/dw/w/no)",
|
||||
"maxn=250,600": "max 250 uploads over 15min",
|
||||
"maxb=1g,300": "max 1 GiB over 5min (suffixes: b, k, m, g, t)",
|
||||
"vmaxb=1g": "total volume size max 1 GiB (suffixes: b, k, m, g, t)",
|
||||
"vmaxn=4k": "max 4096 files in volume (suffixes: b, k, m, g, t)",
|
||||
"medialinks": "return medialinks for non-up2k uploads (not hotlinks)",
|
||||
"wo_up_readme": "write-only users can upload logues without getting renamed",
|
||||
"rand": "force randomized filenames, 9 chars long by default",
|
||||
"nrand=N": "randomized filenames are N chars long",
|
||||
"u2ow=N": "overwrite existing files? 0=no 1=if-older 2=always",
|
||||
@@ -180,6 +256,7 @@ flagcats = {
|
||||
"upload rotation\n(moves all uploads into the specified folder structure)": {
|
||||
"rotn=100,3": "3 levels of subfolders with 100 entries in each",
|
||||
"rotf=%Y-%m/%d-%H": "date-formatted organizing",
|
||||
"rotf_tz=Europe/Oslo": "timezone (default=UTC)",
|
||||
"lifetime=3600": "uploads are deleted after 1 hour",
|
||||
},
|
||||
"database, general": {
|
||||
@@ -198,6 +275,8 @@ flagcats = {
|
||||
"d2v": "disables file verification, overrides -e2v*",
|
||||
"d2d": "disables all database stuff, overrides -e2*",
|
||||
"hist=/tmp/cdb": "puts thumbnails and indexes at that location",
|
||||
"dbpath=/tmp/cdb": "puts indexes at that location",
|
||||
"landmark=foo": "disable db if file foo doesn't exist",
|
||||
"scan=60": "scan for new files every 60sec, same as --re-maxage",
|
||||
"nohash=\\.iso$": "skips hashing file contents if path matches *.iso",
|
||||
"noidx=\\.iso$": "fully ignores the contents at paths matching *.iso",
|
||||
@@ -212,6 +291,7 @@ flagcats = {
|
||||
"dotsrch": "show dotfiles in search results",
|
||||
"nodotsrch": "hide dotfiles in search results (default)",
|
||||
"srch_excl": "exclude search results with URL matching this regex",
|
||||
"db_xattr=user.foo,user.bar": "index file xattrs as media-tags",
|
||||
},
|
||||
'database, audio tags\n"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ...': {
|
||||
"mte=artist,title": "media-tags to index/display",
|
||||
@@ -228,7 +308,11 @@ flagcats = {
|
||||
"thsize": "thumbnail res; WxH",
|
||||
"crop": "center-cropping (y/n/fy/fn)",
|
||||
"th3x": "3x resolution (y/n/fy/fn)",
|
||||
"convt": "conversion timeout in seconds",
|
||||
"th_qv=40": "webp/jpg thumbnail quality (10~90)",
|
||||
"th_qvx=40": "jxl thumbnail quality (10~90)",
|
||||
"convt": "convert-to-image timeout in seconds",
|
||||
"aconvt": "convert-to-audio timeout in seconds",
|
||||
"th_spec_p=1": "make spectrograms? 0=never 1=fallback 2=always",
|
||||
"ext_th=s=/b.png": "use /b.png as thumbnail for file-extension s",
|
||||
},
|
||||
"handlers\n(better explained in --help-handlers)": {
|
||||
@@ -254,12 +338,22 @@ flagcats = {
|
||||
"sort": "default sort order",
|
||||
"nsort": "natural-sort of leading digits in filenames",
|
||||
"hsortn": "number of sort-rules to add to media URLs",
|
||||
"ufavico=URL": "per-volume favicon (.ico/png/gif/svg)",
|
||||
"unlist": "dont list files matching REGEX",
|
||||
"dlni": "force-download (no-inline) files on click",
|
||||
"html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH",
|
||||
"html_head_s=TXT": "additional static text in the html <head>",
|
||||
"tcolor=#fc0": "theme color (a hint for webbrowsers, discord, etc.)",
|
||||
"nodirsz": "don't show total folder size",
|
||||
"du_who=all": "show disk-usage info to everyone",
|
||||
"robots": "allows indexing by search engines (default)",
|
||||
"norobots": "kindly asks search engines to leave",
|
||||
"unlistcr": "don't list read-access in controlpanel",
|
||||
"unlistcw": "don't list write-access in controlpanel",
|
||||
"prologues=.prologue.html": "files to embed above/before files",
|
||||
"epilogues=.epilogue.html": "files to embed below/after files",
|
||||
"readmes=readme.md,README.md": "files to embed as readmes",
|
||||
"preadmes=preadme.md,PREADME.md": "files to embed as preadmes",
|
||||
"no_sb_md": "disable js sandbox for markdown files",
|
||||
"no_sb_lg": "disable js sandbox for prologue/epilogue",
|
||||
"sb_md": "enable js sandbox for markdown files (default)",
|
||||
@@ -269,6 +363,15 @@ flagcats = {
|
||||
"md_sba": "value of iframe allow-prop for markdown-sandbox",
|
||||
"lg_sba": "value of iframe allow-prop for *logue-sandbox",
|
||||
"nohtml": "return html and markdown as text/html",
|
||||
"ui_noacci": "hide account-info in the UI",
|
||||
"ui_nocpla": "hide cpanel-link in the UI",
|
||||
"ui_nolbar": "hide link-bar in the UI",
|
||||
"ui_nombar": "hide top-menu in the UI",
|
||||
"ui_nonav": "hide navpane+breadcrumbs in the UI",
|
||||
"ui_notree": "hide navpane in the UI",
|
||||
"ui_norepl": "hide repl-button in the UI",
|
||||
"ui_nosrvi": "hide server-info in the UI",
|
||||
"ui_noctxb": "hide context-buttons in the UI",
|
||||
},
|
||||
"opengraph (discord embeds)": {
|
||||
"og": "enable OG (disables hotlinking)",
|
||||
@@ -284,10 +387,31 @@ flagcats = {
|
||||
"og_no_head": "you want to add tags manually with og_tpl",
|
||||
"og_ua": "if defined: only send OG html if useragent matches this regex",
|
||||
},
|
||||
"opds": {
|
||||
"opds": "enable OPDS",
|
||||
"opds_exts": "file formats to list in OPDS feeds; leave empty to show everything",
|
||||
},
|
||||
"textfiles": {
|
||||
"rw_edit=md,txt": "only require read+write to edit .md and .txt",
|
||||
"md_no_br": "newline only on double-newline or two tailing spaces",
|
||||
"md_hist": "where to put markdown backups; s=subfolder, v=volHist, n=nope",
|
||||
"exp": "enable textfile expansion; see --help-exp",
|
||||
"exp_md": "placeholders to expand in markdown files; see --help",
|
||||
"exp_lg": "placeholders to expand in prologue/epilogue; see --help",
|
||||
"txt_eol=lf": "enable EOL conversion when writing docs (LF or CRLF)",
|
||||
},
|
||||
"tailing": {
|
||||
"notail": "disable ?tail (download a growing file continuously)",
|
||||
"tail_fd=1": "check if file was replaced (new fd) every 1 sec",
|
||||
"tail_rate=0.2": "check for new data every 0.2 sec",
|
||||
"tail_tmax=30": "kill connection after 30 sec",
|
||||
"tail_who=2": "restrict ?tail access (1=admins,2=authed,3=everyone)",
|
||||
},
|
||||
"rss": {
|
||||
"rss": "allow '?rss' URL suffix (experimental)",
|
||||
"rss_sort=m": "default sort-order (m/u/n/s)",
|
||||
"rss_fmt_t={fname}": "default title-format",
|
||||
"rss_fmt_d={album},{.tn}": "default description-format",
|
||||
},
|
||||
"others": {
|
||||
"dots": "allow all users with read-access to\nenable the option to show dotfiles in listings",
|
||||
@@ -296,12 +420,21 @@ flagcats = {
|
||||
"dk=8": 'generates per-directory accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes',
|
||||
"dks": "per-directory accesskeys allow browsing into subdirs",
|
||||
"dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so',
|
||||
"rss": "allow '?rss' URL suffix (experimental)",
|
||||
"rmagic": "expensive analysis for mimetype accuracy",
|
||||
"shr_who=auth": "who can create shares? no/auth/a",
|
||||
"unp_who=2": "unpost only if same... 1=ip+name, 2=ip, 3=name",
|
||||
"ups_who=2": "restrict viewing the list of recent uploads",
|
||||
"zip_who=2": "restrict access to download-as-zip/tar",
|
||||
"zipmaxn=9k": "reject download-as-zip if more than 9000 files",
|
||||
"zipmaxs=2g": "reject download-as-zip if size over 2 GiB",
|
||||
"zipmaxt=no": "reply with 'no' if download-as-zip exceeds max",
|
||||
"zipmaxu": "zip-size-limit does not apply to authenticated users",
|
||||
"nopipe": "disable race-the-beam (download unfinished uploads)",
|
||||
"cachectl=no-cache": "controls caching in webbrowsers",
|
||||
"mv_retry": "ms-windows: timeout for renaming busy files",
|
||||
"rm_retry": "ms-windows: timeout for deleting busy files",
|
||||
"nospawn": "don't create volume's folder if not exist",
|
||||
"assert_root": "crash on startup if volume's folder not exist",
|
||||
"davauth": "ask webdav clients to login for all folders",
|
||||
"davrt": "show lastmod time of symlink destination, not the link itself\n(note: this option is always enabled for recursive listings)",
|
||||
},
|
||||
|
||||
@@ -65,6 +65,9 @@ DXMLParser = _DXMLParser
|
||||
|
||||
|
||||
def parse_xml(txt: str) -> ET.Element:
|
||||
"""
|
||||
Parse XML into an xml.etree.ElementTree.Element while defusing some unsafe parts.
|
||||
"""
|
||||
parser = DXMLParser()
|
||||
parser.feed(txt)
|
||||
return parser.close() # type: ignore
|
||||
|
||||
+122
-29
@@ -2,14 +2,15 @@
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
from .__init__ import ANYWIN, MACOS
|
||||
from .authsrv import AXS, VFS
|
||||
from .authsrv import AXS, VFS, AuthSrv
|
||||
from .bos import bos
|
||||
from .util import chkcmd, min_ex, undot
|
||||
from .util import chkcmd, json_hesc, min_ex, undot
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Optional, Union
|
||||
@@ -18,22 +19,25 @@ if True: # pylint: disable=using-constant-test
|
||||
|
||||
|
||||
class Fstab(object):
|
||||
def __init__(self, log: "RootLogger", args: argparse.Namespace):
|
||||
def __init__(self, log: "RootLogger", args: argparse.Namespace, verbose: bool):
|
||||
self.log_func = log
|
||||
self.verbose = verbose
|
||||
|
||||
self.warned = False
|
||||
self.trusted = False
|
||||
self.tab: Optional[VFS] = None
|
||||
self.oldtab: Optional[VFS] = None
|
||||
self.srctab = "a"
|
||||
self.cache: dict[str, str] = {}
|
||||
self.cache: dict[str, tuple[str, str]] = {}
|
||||
self.age = 0.0
|
||||
self.maxage = args.mtab_age
|
||||
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
if not c or self.verbose:
|
||||
return
|
||||
self.log_func("fstab", msg, c)
|
||||
|
||||
def get(self, path: str) -> str:
|
||||
def get(self, path: str) -> tuple[str, str]:
|
||||
now = time.time()
|
||||
if now - self.age > self.maxage or len(self.cache) > 9000:
|
||||
self.age = now
|
||||
@@ -41,6 +45,7 @@ class Fstab(object):
|
||||
self.tab = None
|
||||
self.cache = {}
|
||||
|
||||
mp = ""
|
||||
fs = "ext4"
|
||||
msg = "failed to determine filesystem at %r; assuming %s\n%s"
|
||||
|
||||
@@ -50,7 +55,7 @@ class Fstab(object):
|
||||
path = self._winpath(path)
|
||||
except:
|
||||
self.log(msg % (path, fs, min_ex()), 3)
|
||||
return fs
|
||||
return fs, ""
|
||||
|
||||
path = undot(path)
|
||||
try:
|
||||
@@ -59,14 +64,14 @@ class Fstab(object):
|
||||
pass
|
||||
|
||||
try:
|
||||
fs = self.get_w32(path) if ANYWIN else self.get_unix(path)
|
||||
fs, mp = self.get_w32(path) if ANYWIN else self.get_unix(path)
|
||||
except:
|
||||
self.log(msg % (path, fs, min_ex()), 3)
|
||||
|
||||
fs = fs.lower()
|
||||
self.cache[path] = fs
|
||||
self.log("found %s at %r" % (fs, path))
|
||||
return fs
|
||||
self.cache[path] = (fs, mp)
|
||||
self.log("found %s at %r, %r" % (fs, mp, path))
|
||||
return fs, mp
|
||||
|
||||
def _winpath(self, path: str) -> str:
|
||||
# try to combine volume-label + st_dev (vsn)
|
||||
@@ -78,42 +83,76 @@ class Fstab(object):
|
||||
return vid
|
||||
|
||||
def build_fallback(self) -> None:
|
||||
self.tab = VFS(self.log_func, "idk", "/", AXS(), {})
|
||||
self.tab = VFS(self.log_func, "idk", "/", "/", AXS(), {})
|
||||
self.trusted = False
|
||||
|
||||
def build_tab(self) -> None:
|
||||
self.log("inspecting mtab for changes")
|
||||
|
||||
def _from_sp_mount(self) -> dict[str, str]:
|
||||
sptn = r"^.*? on (.*) type ([^ ]+) \(.*"
|
||||
if MACOS:
|
||||
sptn = r"^.*? on (.*) \(([^ ]+), .*"
|
||||
|
||||
ptn = re.compile(sptn)
|
||||
so, _ = chkcmd(["mount"])
|
||||
tab1: list[tuple[str, str]] = []
|
||||
atab = []
|
||||
dtab: dict[str, str] = {}
|
||||
for ln in so.split("\n"):
|
||||
m = ptn.match(ln)
|
||||
if not m:
|
||||
continue
|
||||
|
||||
zs1, zs2 = m.groups()
|
||||
tab1.append((str(zs1), str(zs2)))
|
||||
atab.append(ln)
|
||||
dtab[str(zs1)] = str(zs2)
|
||||
|
||||
return dtab
|
||||
|
||||
def _from_proc(self) -> dict[str, str]:
|
||||
ret: dict[str, str] = {}
|
||||
with open("/proc/self/mounts", "rb", 262144) as f:
|
||||
src = f.read(262144).decode("utf-8", "replace").split("\n")
|
||||
for zsl in [x.split(" ") for x in src]:
|
||||
if len(zsl) < 3:
|
||||
continue
|
||||
zs = zsl[1]
|
||||
zs = zs.replace("\\011", "\t").replace("\\040", " ").replace("\\134", "\\")
|
||||
ret[zs] = zsl[2]
|
||||
return ret
|
||||
|
||||
def build_tab(self) -> None:
|
||||
self.log("inspecting mtab for changes")
|
||||
dtab = self._from_sp_mount() if MACOS else self._from_proc()
|
||||
|
||||
# keep empirically-correct values if mounttab unchanged
|
||||
srctab = "\n".join(sorted(atab))
|
||||
srctab = str(sorted(dtab.items()))
|
||||
if srctab == self.srctab:
|
||||
self.tab = self.oldtab
|
||||
return
|
||||
|
||||
self.log("mtab has changed; reevaluating support for sparse files")
|
||||
|
||||
try:
|
||||
fuses = [mp for mp, fs in dtab.items() if fs == "fuseblk"]
|
||||
if not fuses or MACOS:
|
||||
raise Exception()
|
||||
try:
|
||||
so, _ = chkcmd(["lsblk", "-nrfo", "FSTYPE,MOUNTPOINT"]) # centos6
|
||||
except:
|
||||
so, _ = chkcmd(["lsblk", "-nrfo", "FSTYPE,MOUNTPOINTS"]) # future
|
||||
for ln in so.split("\n"):
|
||||
zsl = ln.split(" ", 1)
|
||||
if len(zsl) != 2:
|
||||
continue
|
||||
fs, mp = zsl
|
||||
if mp in fuses:
|
||||
dtab[mp] = fs
|
||||
except:
|
||||
pass
|
||||
|
||||
tab1 = list(dtab.items())
|
||||
tab1.sort(key=lambda x: (len(x[0]), x[0]))
|
||||
path1, fs1 = tab1[0]
|
||||
tab = VFS(self.log_func, fs1, path1, AXS(), {})
|
||||
tab = VFS(self.log_func, fs1, path1, path1, AXS(), {})
|
||||
for path, fs in tab1[1:]:
|
||||
tab.add(fs, path.lstrip("/"))
|
||||
zs = path.lstrip("/")
|
||||
tab.add(fs, zs, zs)
|
||||
|
||||
self.tab = tab
|
||||
self.srctab = srctab
|
||||
@@ -130,9 +169,10 @@ class Fstab(object):
|
||||
if not self.trusted:
|
||||
# no mtab access; have to build as we go
|
||||
if "/" in rem:
|
||||
self.tab.add("idk", os.path.join(vn.vpath, rem.split("/")[0]))
|
||||
zs = os.path.join(vn.vpath, rem.split("/")[0])
|
||||
self.tab.add("idk", zs, zs)
|
||||
if rem:
|
||||
self.tab.add(nval, path)
|
||||
self.tab.add(nval, path, path)
|
||||
else:
|
||||
vn.realpath = nval
|
||||
|
||||
@@ -144,7 +184,7 @@ class Fstab(object):
|
||||
vn.realpath = ptn.sub(nval, vn.realpath)
|
||||
visit.extend(list(vn.nodes.values()))
|
||||
|
||||
def get_unix(self, path: str) -> str:
|
||||
def get_unix(self, path: str) -> tuple[str, str]:
|
||||
if not self.tab:
|
||||
try:
|
||||
self.build_tab()
|
||||
@@ -153,20 +193,73 @@ class Fstab(object):
|
||||
# prisonparty or other restrictive environment
|
||||
if not self.warned:
|
||||
self.warned = True
|
||||
self.log("failed to build tab:\n{}".format(min_ex()), 3)
|
||||
t = "failed to associate fs-mounts with the VFS (this is fine):\n%s"
|
||||
self.log(t % (min_ex(),), 6)
|
||||
self.build_fallback()
|
||||
|
||||
assert self.tab # !rm
|
||||
ret = self.tab._find(path)[0]
|
||||
if self.trusted or path == ret.vpath:
|
||||
return ret.realpath.split("/")[0]
|
||||
return ret.realpath.split("/")[0], ret.vpath
|
||||
else:
|
||||
return "idk"
|
||||
return "idk", ""
|
||||
|
||||
def get_w32(self, path: str) -> str:
|
||||
def get_w32(self, path: str) -> tuple[str, str]:
|
||||
if not self.tab:
|
||||
self.build_fallback()
|
||||
|
||||
assert self.tab # !rm
|
||||
ret = self.tab._find(path)[0]
|
||||
return ret.realpath
|
||||
return ret.realpath, ""
|
||||
|
||||
|
||||
_fstab: Optional[Fstab] = None
|
||||
winfs = set(("msdos", "vfat", "ntfs", "exfat"))
|
||||
# "msdos" = vfat on macos
|
||||
|
||||
|
||||
def ramdisk_chk(asrv: AuthSrv) -> None:
|
||||
# should have been in authsrv but that's a circular import
|
||||
global _fstab
|
||||
mods = []
|
||||
ramfs = ("tmpfs", "overlay")
|
||||
log = asrv.log_func or print
|
||||
if not _fstab:
|
||||
_fstab = Fstab(log, asrv.args, False)
|
||||
for vn in asrv.vfs.all_nodes.values():
|
||||
if not vn.axs.uwrite or "wram" in vn.flags:
|
||||
continue
|
||||
ap = vn.realpath
|
||||
if not ap or os.path.isfile(ap):
|
||||
continue
|
||||
fs, mp = _fstab.get(ap)
|
||||
mp = "/" + mp.strip("/")
|
||||
if fs == "tmpfs" or (mp == "/" and fs in ramfs):
|
||||
mods.append((vn.vpath, ap, fs, mp))
|
||||
vn.axs.uwrite.clear()
|
||||
vn.axs.umove.clear()
|
||||
for un, ztsp in list(vn.uaxs.items()):
|
||||
zsl = list(ztsp)
|
||||
zsl[1] = False
|
||||
zsl[2] = False
|
||||
vn.uaxs[un] = tuple(zsl) # type: ignore
|
||||
if mods:
|
||||
t = "WARNING: write-access was removed from the following volumes because they are not mapped to an actual HDD for storage! All uploaded data would live in RAM only, and all uploaded files would be LOST on next reboot. To allow uploading and ignore this hazard, enable the 'wram' option (global/volflag). List of affected volumes:"
|
||||
t2 = ["\n volume=[/%s], abspath=%r, type=%s, root=%r" % x for x in mods]
|
||||
log("vfs", t + "".join(t2) + "\n", 1)
|
||||
|
||||
assume = "mac" if MACOS else "lin"
|
||||
for vol in asrv.vfs.all_nodes.values():
|
||||
if not vol.realpath or vol.flags.get("is_file"):
|
||||
continue
|
||||
zs = vol.flags["fsnt"].strip()[:3].lower()
|
||||
if ANYWIN and not zs:
|
||||
zs = "win"
|
||||
if zs in ("lin", "win", "mac"):
|
||||
vol.flags["fsnt"] = zs
|
||||
continue
|
||||
fs = _fstab.get(vol.realpath)[0]
|
||||
fs = "win" if fs in winfs else assume
|
||||
htm = json.loads(vol.js_htm)
|
||||
vol.flags["fsnt"] = vol.js_ls["fsnt"] = htm["fsnt"] = fs
|
||||
vol.js_htm = json_hesc(json.dumps(htm))
|
||||
+109
-56
@@ -30,6 +30,7 @@ from .util import (
|
||||
relchk,
|
||||
runhook,
|
||||
sanitize_fn,
|
||||
set_fperms,
|
||||
vjoin,
|
||||
wunlink,
|
||||
)
|
||||
@@ -66,13 +67,13 @@ class FtpAuth(DummyAuthorizer):
|
||||
if ip.startswith("::ffff:"):
|
||||
ip = ip[7:]
|
||||
|
||||
ip = ipnorm(ip)
|
||||
ipn = ipnorm(ip)
|
||||
bans = self.hub.bans
|
||||
if ip in bans:
|
||||
rt = bans[ip] - time.time()
|
||||
if ipn in bans:
|
||||
rt = bans[ipn] - time.time()
|
||||
if rt < 0:
|
||||
logging.info("client unbanned")
|
||||
del bans[ip]
|
||||
del bans[ipn]
|
||||
else:
|
||||
raise AuthenticationFailed("banned")
|
||||
|
||||
@@ -81,7 +82,12 @@ class FtpAuth(DummyAuthorizer):
|
||||
uname = "*"
|
||||
if username != "anonymous":
|
||||
uname = ""
|
||||
for zs in (password, username):
|
||||
if args.usernames:
|
||||
alts = ["%s:%s" % (username, password)]
|
||||
else:
|
||||
alts = [password, username]
|
||||
|
||||
for zs in alts:
|
||||
zs = asrv.iacct.get(asrv.ah.hash(zs), "")
|
||||
if zs:
|
||||
uname = zs
|
||||
@@ -89,6 +95,10 @@ class FtpAuth(DummyAuthorizer):
|
||||
|
||||
if args.ipu and uname == "*":
|
||||
uname = args.ipu_iu[args.ipu_nm.map(ip)]
|
||||
if args.ipr and uname in args.ipr_u:
|
||||
if not args.ipr_u[uname].map(ip):
|
||||
logging.warning("username [%s] rejected by --ipr", uname)
|
||||
uname = "*"
|
||||
|
||||
if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):
|
||||
g = self.hub.gpwd
|
||||
@@ -141,10 +151,6 @@ class FtpFs(AbstractedFS):
|
||||
self.cwd = "/" # pyftpdlib convention of leading slash
|
||||
self.root = "/var/lib/empty"
|
||||
|
||||
self.can_read = self.can_write = self.can_move = False
|
||||
self.can_delete = self.can_get = self.can_upget = False
|
||||
self.can_admin = self.can_dot = False
|
||||
|
||||
self.listdirinfo = self.listdir
|
||||
self.chdir(".")
|
||||
|
||||
@@ -167,9 +173,19 @@ class FtpFs(AbstractedFS):
|
||||
t = "Unsupported characters in [{}]"
|
||||
raise FSE(t.format(vpath), 1)
|
||||
|
||||
fn = sanitize_fn(fn or "", "")
|
||||
fn = sanitize_fn(fn or "")
|
||||
vpath = vjoin(rd, fn)
|
||||
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
|
||||
if (
|
||||
w
|
||||
and fn.lower() in vfs.flags["emb_all"]
|
||||
and self.h.uname not in vfs.axs.uread
|
||||
and "wo_up_readme" not in vfs.flags
|
||||
):
|
||||
fn = "_wo_" + fn
|
||||
vpath = vjoin(rd, fn)
|
||||
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
|
||||
|
||||
if not vfs.realpath:
|
||||
t = "No filesystem mounted at [{}]"
|
||||
raise FSE(t.format(vpath))
|
||||
@@ -181,10 +197,13 @@ class FtpFs(AbstractedFS):
|
||||
if not avfs:
|
||||
raise FSE(t.format(vpath), 1)
|
||||
|
||||
cr, cw, cm, cd, _, _, _, _ = avfs.can_access("", self.h.uname)
|
||||
cr, cw, cm, cd, _, _, _, _, _ = avfs.uaxs[self.h.uname]
|
||||
if r and not cr or w and not cw or m and not cm or d and not cd:
|
||||
raise FSE(t.format(vpath), 1)
|
||||
|
||||
if "bcasechk" in vfs.flags and not vfs.casechk(rem, True):
|
||||
raise FSE("No such file or directory", 1)
|
||||
|
||||
return os.path.join(vfs.realpath, rem), vfs, rem
|
||||
except Pebkac as ex:
|
||||
raise FSE(str(ex))
|
||||
@@ -197,7 +216,7 @@ class FtpFs(AbstractedFS):
|
||||
m: bool = False,
|
||||
d: bool = False,
|
||||
) -> tuple[str, VFS, str]:
|
||||
return self.v2a(os.path.join(self.cwd, vpath), r, w, m, d)
|
||||
return self.v2a(join(self.cwd, vpath), r, w, m, d)
|
||||
|
||||
def ftp2fs(self, ftppath: str) -> str:
|
||||
# return self.v2a(ftppath)
|
||||
@@ -218,7 +237,7 @@ class FtpFs(AbstractedFS):
|
||||
r = "r" in mode
|
||||
w = "w" in mode or "a" in mode or "+" in mode
|
||||
|
||||
ap = self.rv2a(filename, r, w)[0]
|
||||
ap, vfs, _ = self.rv2a(filename, r, w)
|
||||
self.validpath(ap)
|
||||
if w:
|
||||
try:
|
||||
@@ -229,9 +248,36 @@ class FtpFs(AbstractedFS):
|
||||
need_unlink = False
|
||||
td = 0
|
||||
|
||||
if w and need_unlink:
|
||||
xbu = vfs.flags.get("xbu")
|
||||
if xbu:
|
||||
hr = runhook(
|
||||
self.log,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.ftp",
|
||||
xbu,
|
||||
ap,
|
||||
filename,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
"1.3.8.7",
|
||||
time.time(),
|
||||
None,
|
||||
)
|
||||
t = hr.get("rejectmsg") or ""
|
||||
if t or hr.get("rc") != 0:
|
||||
if not t:
|
||||
t = "upload blocked by xbu server config: %r" % (filename,)
|
||||
self.log(t, 3)
|
||||
raise FSE(t)
|
||||
|
||||
if w and need_unlink: # type: ignore # !rm
|
||||
assert td # type: ignore # !rm
|
||||
if td >= -1 and td <= self.args.ftp_wt:
|
||||
# within permitted timeframe; unlink and accept
|
||||
# within permitted timeframe; allow overwrite or resume
|
||||
do_it = True
|
||||
elif self.args.no_del or self.args.ftp_no_ow:
|
||||
# file too old, or overwrite not allowed; reject
|
||||
@@ -248,13 +294,23 @@ class FtpFs(AbstractedFS):
|
||||
if not do_it:
|
||||
raise FSE("File already exists")
|
||||
|
||||
wunlink(self.log, ap, VF_CAREFUL)
|
||||
# Don't unlink file for append mode
|
||||
elif "a" not in mode:
|
||||
wunlink(self.log, ap, VF_CAREFUL)
|
||||
|
||||
return open(fsenc(ap), mode, self.args.iobuf)
|
||||
ret = open(fsenc(ap), mode, self.args.iobuf)
|
||||
if w and "fperms" in vfs.flags:
|
||||
set_fperms(ret, vfs.flags)
|
||||
|
||||
return ret
|
||||
|
||||
def chdir(self, path: str) -> None:
|
||||
nwd = join(self.cwd, path)
|
||||
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
|
||||
if not vfs.realpath:
|
||||
self.cwd = nwd
|
||||
return
|
||||
|
||||
ap = vfs.canonical(rem)
|
||||
try:
|
||||
st = bos.stat(ap)
|
||||
@@ -269,20 +325,10 @@ class FtpFs(AbstractedFS):
|
||||
raise FSE("Permission denied", 1)
|
||||
|
||||
self.cwd = nwd
|
||||
(
|
||||
self.can_read,
|
||||
self.can_write,
|
||||
self.can_move,
|
||||
self.can_delete,
|
||||
self.can_get,
|
||||
self.can_upget,
|
||||
self.can_admin,
|
||||
self.can_dot,
|
||||
) = avfs.can_access("", self.h.uname)
|
||||
|
||||
def mkdir(self, path: str) -> None:
|
||||
ap = self.rv2a(path, w=True)[0]
|
||||
bos.makedirs(ap) # filezilla expects this
|
||||
ap, vfs, _ = self.rv2a(path, w=True)
|
||||
bos.makedirs(ap, vf=vfs.flags) # filezilla expects this
|
||||
|
||||
def listdir(self, path: str) -> list[str]:
|
||||
vpath = join(self.cwd, path)
|
||||
@@ -301,7 +347,7 @@ class FtpFs(AbstractedFS):
|
||||
vfs_ls = [x[0] for x in vfs_ls1]
|
||||
vfs_ls.extend(vfs_virt.keys())
|
||||
|
||||
if not self.can_dot:
|
||||
if self.uname not in vfs.axs.udot:
|
||||
vfs_ls = exclude_dotfiles(vfs_ls)
|
||||
|
||||
vfs_ls.sort()
|
||||
@@ -349,16 +395,13 @@ class FtpFs(AbstractedFS):
|
||||
raise FSE(str(ex))
|
||||
|
||||
def rename(self, src: str, dst: str) -> None:
|
||||
if not self.can_move:
|
||||
raise FSE("Not allowed for user " + self.h.uname)
|
||||
|
||||
if self.args.no_mv:
|
||||
raise FSE("The rename/move feature is disabled in server config")
|
||||
|
||||
svp = join(self.cwd, src).lstrip("/")
|
||||
dvp = join(self.cwd, dst).lstrip("/")
|
||||
try:
|
||||
self.hub.up2k.handle_mv(self.uname, self.h.cli_ip, svp, dvp)
|
||||
self.hub.up2k.handle_mv("", self.uname, self.h.cli_ip, svp, dvp)
|
||||
except Exception as ex:
|
||||
raise FSE(str(ex))
|
||||
|
||||
@@ -382,7 +425,7 @@ class FtpFs(AbstractedFS):
|
||||
|
||||
def utime(self, path: str, timeval: float) -> None:
|
||||
ap = self.rv2a(path, w=True)[0]
|
||||
return bos.utime(ap, (timeval, timeval))
|
||||
bos.utime_c(logging.warning, ap, int(timeval), False)
|
||||
|
||||
def lstat(self, path: str) -> os.stat_result:
|
||||
ap = self.rv2a(path)[0]
|
||||
@@ -471,27 +514,37 @@ class FtpHandler(FTPHandler):
|
||||
def ftp_STOR(self, file: str, mode: str = "w") -> Any:
|
||||
# Optional[str]
|
||||
vp = join(self.fs.cwd, file).lstrip("/")
|
||||
ap, vfs, rem = self.fs.v2a(vp, w=True)
|
||||
try:
|
||||
ap, vfs, rem = self.fs.v2a(vp, w=True)
|
||||
except Exception as ex:
|
||||
self.respond("550 %s" % (ex,), logging.info)
|
||||
return
|
||||
self.vfs_map[ap] = vp
|
||||
xbu = vfs.flags.get("xbu")
|
||||
if xbu and not runhook(
|
||||
None,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.ftpd",
|
||||
xbu,
|
||||
ap,
|
||||
vp,
|
||||
"",
|
||||
self.uname,
|
||||
self.hub.asrv.vfs.get_perms(vp, self.uname),
|
||||
0,
|
||||
0,
|
||||
self.cli_ip,
|
||||
time.time(),
|
||||
"",
|
||||
):
|
||||
raise FSE("Upload blocked by xbu server config")
|
||||
if xbu:
|
||||
hr = runhook(
|
||||
None,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.ftpd",
|
||||
xbu,
|
||||
ap,
|
||||
vp,
|
||||
"",
|
||||
self.uname,
|
||||
self.hub.asrv.vfs.get_perms(vp, self.uname),
|
||||
0,
|
||||
0,
|
||||
self.cli_ip,
|
||||
time.time(),
|
||||
None,
|
||||
)
|
||||
t = hr.get("rejectmsg") or ""
|
||||
if t or hr.get("rc") != 0:
|
||||
if not t:
|
||||
t = "Upload blocked by xbu server config: %r" % (vp,)
|
||||
self.respond("550 %s" % (t,), logging.info)
|
||||
return
|
||||
|
||||
# print("ftp_STOR: {} {} => {}".format(vp, mode, ap))
|
||||
ret = FTPHandler.ftp_STOR(self, file, mode)
|
||||
@@ -587,11 +640,11 @@ class Ftpd(object):
|
||||
lgr = logging.getLogger("pyftpdlib")
|
||||
lgr.setLevel(logging.DEBUG if self.args.ftpv else logging.INFO)
|
||||
|
||||
ips = self.args.i
|
||||
ips = self.args.ftp_i
|
||||
if "::" in ips:
|
||||
ips.append("0.0.0.0")
|
||||
|
||||
ips = [x for x in ips if "unix:" not in x]
|
||||
ips = [x for x in ips if not x.startswith(("unix:", "fd:"))]
|
||||
|
||||
if self.args.ftp4:
|
||||
ips = [x for x in ips if ":" not in x]
|
||||
|
||||
+1954
-688
File diff suppressed because it is too large.
Load diff
@@ -62,6 +62,7 @@ class HttpConn(object):
|
||||
self.ipu_iu: Optional[dict[str, str]] = hsrv.ipu_iu
|
||||
self.ipu_nm: Optional[NetMap] = hsrv.ipu_nm
|
||||
self.ipa_nm: Optional[NetMap] = hsrv.ipa_nm
|
||||
self.ipar_nm: Optional[NetMap] = hsrv.ipar_nm
|
||||
self.xff_nm: Optional[NetMap] = hsrv.xff_nm
|
||||
self.xff_lan: NetMap = hsrv.xff_lan # type: ignore
|
||||
self.iphash: HMaccas = hsrv.broker.iphash
|
||||
@@ -221,6 +222,24 @@ class HttpConn(object):
|
||||
if not self.cli.run():
|
||||
return
|
||||
|
||||
if self.sr.te == 1:
|
||||
self.log("closing socket (leftover TE)", "90")
|
||||
return
|
||||
|
||||
if (
|
||||
"content-length" in self.cli.headers
|
||||
and int(self.cli.headers["content-length"]) != self.sr.nb
|
||||
):
|
||||
self.log("closing socket (CL mismatch)", "90")
|
||||
return
|
||||
|
||||
# note: proxies reject PUT sans Content-Length; illegal for HTTP/1.1
|
||||
|
||||
self.sr.nb = self.sr.te = 0
|
||||
|
||||
if self.u2idx:
|
||||
self.hsrv.put_u2idx(str(self.addr), self.u2idx)
|
||||
self.u2idx = None
|
||||
|
||||
if self.rproxy:
|
||||
self.set_rproxy()
|
||||
+31
-4
@@ -70,6 +70,7 @@ from .util import (
|
||||
build_netmap,
|
||||
has_resource,
|
||||
ipnorm,
|
||||
load_ipr,
|
||||
load_ipu,
|
||||
load_resource,
|
||||
min_ex,
|
||||
@@ -123,6 +124,7 @@ class HttpSrv(object):
|
||||
self.nm = NetMap([], [])
|
||||
self.ssdp: Optional["SSDPr"] = None
|
||||
self.gpwd = Garda(self.args.ban_pw)
|
||||
self.gpwc = Garda(self.args.ban_pwc)
|
||||
self.g404 = Garda(self.args.ban_404)
|
||||
self.g403 = Garda(self.args.ban_403)
|
||||
self.g422 = Garda(self.args.ban_422, False)
|
||||
@@ -175,6 +177,7 @@ class HttpSrv(object):
|
||||
"browser",
|
||||
"browser2",
|
||||
"cf",
|
||||
"idp",
|
||||
"md",
|
||||
"mde",
|
||||
"msg",
|
||||
@@ -184,6 +187,8 @@ class HttpSrv(object):
|
||||
"svcs",
|
||||
]
|
||||
self.j2 = {x: env.get_template(x + ".html") for x in jn}
|
||||
self.j2["opds"] = env.get_template("opds.xml")
|
||||
self.j2["opds_osd"] = env.get_template("opds_osd.xml")
|
||||
self.prism = has_resource(self.E, "web/deps/prism.js.gz")
|
||||
|
||||
if self.args.ipu:
|
||||
@@ -191,7 +196,13 @@ class HttpSrv(object):
|
||||
else:
|
||||
self.ipu_iu = self.ipu_nm = None
|
||||
|
||||
if self.args.ipr:
|
||||
self.ipr = load_ipr(self.log, self.args.ipr)
|
||||
else:
|
||||
self.ipr = None
|
||||
|
||||
self.ipa_nm = build_netmap(self.args.ipa)
|
||||
self.ipar_nm = build_netmap(self.args.ipar)
|
||||
self.xff_nm = build_netmap(self.args.xff_src)
|
||||
self.xff_lan = build_netmap("lan")
|
||||
|
||||
@@ -313,6 +324,8 @@ class HttpSrv(object):
|
||||
|
||||
Daemon(self.broker.say, "sig-hsrv-up1", ("cb_httpsrv_up",))
|
||||
|
||||
saddr = ("", 0) # fwd-decl for `except TypeError as ex:`
|
||||
|
||||
while not self.stopping:
|
||||
if self.args.log_conn:
|
||||
self.log(self.name, "|%sC-ncli" % ("-" * 1,), c="90")
|
||||
@@ -320,7 +333,8 @@ class HttpSrv(object):
|
||||
spins = 0
|
||||
while self.ncli >= self.nclimax:
|
||||
if not spins:
|
||||
self.log(self.name, "at connection limit; waiting", 3)
|
||||
t = "at connection limit (global-option 'nc'); waiting"
|
||||
self.log(self.name, t, 3)
|
||||
|
||||
spins += 1
|
||||
time.sleep(0.1)
|
||||
@@ -371,8 +385,8 @@ class HttpSrv(object):
|
||||
if nloris < nconn / 2:
|
||||
continue
|
||||
|
||||
t = "slowloris (idle-conn): {} banned for {} min"
|
||||
self.log(self.name, t.format(ip, self.args.loris, nclose), 1)
|
||||
t = "slow%s (idle-conn): %s banned for %d min" # slowloris
|
||||
self.log(self.name, t % ("loris", ip, self.args.loris), 1)
|
||||
self.bans[ip] = int(time.time() + self.args.loris * 60)
|
||||
|
||||
if self.args.log_conn:
|
||||
@@ -394,6 +408,19 @@ class HttpSrv(object):
|
||||
self.log(self.name, "accept({}): {}".format(fno, ex), c=6)
|
||||
time.sleep(0.02)
|
||||
continue
|
||||
except TypeError as ex:
|
||||
# on macOS, accept() may return a None saddr if blocked by LittleSnitch;
|
||||
# unicode(saddr[0]) ==> TypeError: 'NoneType' object is not subscriptable
|
||||
if tcp and not saddr:
|
||||
t = "accept(%s): failed to accept connection from client due to firewall or network issue"
|
||||
self.log(self.name, t % (fno,), c=3)
|
||||
try:
|
||||
sck.close() # type: ignore
|
||||
except:
|
||||
pass
|
||||
time.sleep(0.02)
|
||||
continue
|
||||
raise
|
||||
|
||||
if self.args.log_conn:
|
||||
t = "|{}C-acc2 \033[0;36m{} \033[3{}m{}".format(
|
||||
@@ -547,7 +574,7 @@ class HttpSrv(object):
|
||||
|
||||
v = self.E.t0
|
||||
try:
|
||||
with os.scandir(os.path.join(self.E.mod, "web")) as dh:
|
||||
with os.scandir(self.E.mod_ + "web") as dh:
|
||||
for fh in dh:
|
||||
inf = fh.stat()
|
||||
v = max(v, inf.st_mtime)
|
||||
|
||||
+13
-2
@@ -94,10 +94,21 @@ class Ico(object):
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" viewBox="0 0 100 {}" xmlns="http://www.w3.org/2000/svg"><g>
|
||||
<rect width="100%" height="100%" fill="#{}" />
|
||||
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" xml:space="preserve"
|
||||
<text x="50%" y="{}" dominant-baseline="middle" text-anchor="middle" xml:space="preserve"
|
||||
fill="#{}" font-family="monospace" font-size="14px" style="letter-spacing:.5px">{}</text>
|
||||
</g></svg>
|
||||
"""
|
||||
svg = svg.format(h, c[:6], c[6:], html_escape(ext, True))
|
||||
|
||||
txt = html_escape(ext, True)
|
||||
if "\n" in txt:
|
||||
lines = txt.split("\n")
|
||||
n = len(lines)
|
||||
y = "20%" if n == 2 else "10%" if n == 3 else "0"
|
||||
zs = '<tspan x="50%%" dy="1.2em">%s</tspan>'
|
||||
txt = "".join([zs % (x,) for x in lines])
|
||||
else:
|
||||
y = "50%"
|
||||
|
||||
svg = svg.format(h, c[:6], y, c[6:], txt)
|
||||
|
||||
return "image/svg+xml", svg.encode("utf-8")
|
||||
+68
-26
@@ -2,6 +2,7 @@
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import errno
|
||||
import os
|
||||
import random
|
||||
import select
|
||||
import socket
|
||||
@@ -12,22 +13,52 @@ from ipaddress import IPv4Network, IPv6Network
|
||||
from .__init__ import TYPE_CHECKING
|
||||
from .__init__ import unicode as U
|
||||
from .multicast import MC_Sck, MCast
|
||||
from .stolen.dnslib import AAAA
|
||||
from .stolen.dnslib import CLASS as DC
|
||||
from .stolen.dnslib import (
|
||||
NSEC,
|
||||
PTR,
|
||||
QTYPE,
|
||||
RR,
|
||||
SRV,
|
||||
TXT,
|
||||
A,
|
||||
DNSHeader,
|
||||
DNSQuestion,
|
||||
DNSRecord,
|
||||
set_avahi_379,
|
||||
)
|
||||
from .util import CachedSet, Daemon, Netdev, list_ips, min_ex
|
||||
from .util import IP6_LL, CachedSet, Daemon, Netdev, list_ips, min_ex
|
||||
|
||||
try:
|
||||
if os.environ.get("PRTY_SYS_ALL") or os.environ.get("PRTY_SYS_DNSLIB"):
|
||||
raise ImportError()
|
||||
from .stolen.dnslib import (
|
||||
AAAA,
|
||||
)
|
||||
from .stolen.dnslib import CLASS as DC
|
||||
from .stolen.dnslib import (
|
||||
NSEC,
|
||||
PTR,
|
||||
QTYPE,
|
||||
RR,
|
||||
SRV,
|
||||
TXT,
|
||||
A,
|
||||
DNSHeader,
|
||||
DNSQuestion,
|
||||
DNSRecord,
|
||||
set_avahi_379,
|
||||
)
|
||||
|
||||
DNS_VND = True
|
||||
except ImportError:
|
||||
DNS_VND = False
|
||||
from dnslib import (
|
||||
AAAA,
|
||||
)
|
||||
from dnslib import CLASS as DC
|
||||
from dnslib import (
|
||||
NSEC,
|
||||
PTR,
|
||||
QTYPE,
|
||||
RR,
|
||||
SRV,
|
||||
TXT,
|
||||
A,
|
||||
Bimap,
|
||||
DNSHeader,
|
||||
DNSQuestion,
|
||||
DNSRecord,
|
||||
)
|
||||
|
||||
DC.forward[0x8001] = "F_IN"
|
||||
DC.reverse["F_IN"] = 0x8001
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .svchub import SvcHub
|
||||
@@ -35,6 +66,11 @@ if TYPE_CHECKING:
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
if os.environ.get("PRTY_MODSPEC"):
|
||||
from inspect import getsourcefile
|
||||
|
||||
print("PRTY_MODSPEC: dnslib:", getsourcefile(A))
|
||||
|
||||
|
||||
MDNS4 = "224.0.0.251"
|
||||
MDNS6 = "ff02::fb"
|
||||
@@ -73,10 +109,11 @@ class MDNS(MCast):
|
||||
self.ngen = ngen
|
||||
self.ttl = 300
|
||||
|
||||
if not self.args.zm_nwa_1:
|
||||
set_avahi_379()
|
||||
if not self.args.zm_nwa_1 and DNS_VND:
|
||||
set_avahi_379() # type: ignore
|
||||
|
||||
zs = self.args.name + ".local."
|
||||
zs = self.args.zm_fqdn or (self.args.name + ".local")
|
||||
zs = zs.replace("--name", self.args.name).rstrip(".") + "."
|
||||
zs = zs.encode("ascii", "replace").decode("ascii", "replace")
|
||||
self.hn = "-".join(x for x in zs.split("?") if x) or (
|
||||
"vault-{}".format(random.randint(1, 255))
|
||||
@@ -99,9 +136,14 @@ class MDNS(MCast):
|
||||
self.log_func(self.logsrc, msg, c)
|
||||
|
||||
def build_svcs(self) -> tuple[dict[str, dict[str, Any]], set[str]]:
|
||||
ar = self.args
|
||||
zms = self.args.zms
|
||||
http = {"port": 80 if 80 in self.args.p else self.args.p[0]}
|
||||
https = {"port": 443 if 443 in self.args.p else self.args.p[0]}
|
||||
|
||||
zi = ar.zm_http
|
||||
http = {"port": zi if zi != -1 else 80 if 80 in ar.p else ar.p[0]}
|
||||
zi = ar.zm_https
|
||||
https = {"port": zi if zi != -1 else 443 if 443 in ar.p else ar.p[0]}
|
||||
|
||||
webdav = http.copy()
|
||||
webdavs = https.copy()
|
||||
webdav["u"] = webdavs["u"] = "u" # KDE requires username
|
||||
@@ -126,16 +168,16 @@ class MDNS(MCast):
|
||||
|
||||
svcs: dict[str, dict[str, Any]] = {}
|
||||
|
||||
if "d" in zms:
|
||||
if "d" in zms and http["port"]:
|
||||
svcs["_webdav._tcp.local."] = webdav
|
||||
|
||||
if "D" in zms:
|
||||
if "D" in zms and https["port"]:
|
||||
svcs["_webdavs._tcp.local."] = webdavs
|
||||
|
||||
if "h" in zms:
|
||||
if "h" in zms and http["port"]:
|
||||
svcs["_http._tcp.local."] = http
|
||||
|
||||
if "H" in zms:
|
||||
if "H" in zms and https["port"]:
|
||||
svcs["_https._tcp.local."] = https
|
||||
|
||||
if "f" in zms.lower():
|
||||
@@ -374,7 +416,7 @@ class MDNS(MCast):
|
||||
cip = addr[0]
|
||||
v6 = ":" in cip
|
||||
if (cip.startswith("169.254") and not self.ll_ok) or (
|
||||
v6 and not cip.startswith("fe80")
|
||||
v6 and not cip.startswith(IP6_LL)
|
||||
):
|
||||
return
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ class Metrics(object):
|
||||
self.hsrv = hsrv
|
||||
|
||||
def tx(self, cli: "HttpCli") -> bool:
|
||||
if not cli.avol:
|
||||
args = cli.args
|
||||
if not cli.avol and cli.uname.lower() not in args.stats_u_set:
|
||||
raise Pebkac(403, "'stats' not allowed for user " + cli.uname)
|
||||
|
||||
args = cli.args
|
||||
if not args.stats:
|
||||
raise Pebkac(403, "the stats feature is not enabled in server config")
|
||||
|
||||
|
||||
+155
-19
@@ -18,6 +18,7 @@ from .util import (
|
||||
REKOBO_LKEY,
|
||||
VF_CAREFUL,
|
||||
fsenc,
|
||||
gzip,
|
||||
min_ex,
|
||||
pybin,
|
||||
retchk,
|
||||
@@ -28,7 +29,7 @@ from .util import (
|
||||
)
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Optional, Union
|
||||
from typing import IO, Any, Optional, Union
|
||||
|
||||
from .util import NamedLogger, RootLogger
|
||||
|
||||
@@ -63,9 +64,11 @@ def have_ff(scmd: str) -> bool:
|
||||
HAVE_FFMPEG = not os.environ.get("PRTY_NO_FFMPEG") and have_ff("ffmpeg")
|
||||
HAVE_FFPROBE = not os.environ.get("PRTY_NO_FFPROBE") and have_ff("ffprobe")
|
||||
|
||||
CBZ_PICS = set("png jpg jpeg gif bmp tga tif tiff webp avif".split())
|
||||
CBZ_PICS = set("png jpg jpeg gif bmp tga tif tiff webp avif jxl".split())
|
||||
CBZ_01 = re.compile(r"(^|[^0-9v])0+[01]\b")
|
||||
|
||||
FMT_AU = set("mp3 ogg flac wav".split())
|
||||
|
||||
|
||||
class MParser(object):
|
||||
def __init__(self, cmdline: str) -> None:
|
||||
@@ -138,8 +141,6 @@ def au_unpk(
|
||||
fd, ret = tempfile.mkstemp("." + au)
|
||||
|
||||
if pk == "gz":
|
||||
import gzip
|
||||
|
||||
fi = gzip.GzipFile(abspath, mode="rb")
|
||||
|
||||
elif pk == "xz":
|
||||
@@ -167,12 +168,17 @@ def au_unpk(
|
||||
znil = [x for x in znil if "cover" in x[0]] or znil
|
||||
znil = [x for x in znil if CBZ_01.search(x[0])] or znil
|
||||
t = "cbz: %d files, %d hits" % (nf, len(znil))
|
||||
if znil:
|
||||
t += ", using " + znil[0][1].filename
|
||||
log(t)
|
||||
if not znil:
|
||||
raise Exception("no images inside cbz")
|
||||
fi = zf.open(znil[0][1])
|
||||
using = sorted(znil)[0][1].filename
|
||||
if znil:
|
||||
t += ", using " + using
|
||||
log(t)
|
||||
fi = zf.open(using)
|
||||
|
||||
elif pk == "epub":
|
||||
fi = get_cover_from_epub(log, abspath)
|
||||
assert fi # !rm
|
||||
|
||||
else:
|
||||
raise Exception("unknown compression %s" % (pk,))
|
||||
@@ -194,16 +200,17 @@ def au_unpk(
|
||||
|
||||
except Exception as ex:
|
||||
if ret:
|
||||
t = "failed to decompress audio file %r: %r"
|
||||
t = "failed to decompress file %r: %r"
|
||||
log(t % (abspath, ex))
|
||||
wunlink(log, ret, vn.flags if vn else VF_CAREFUL)
|
||||
return ""
|
||||
|
||||
return abspath
|
||||
|
||||
|
||||
def ffprobe(
|
||||
abspath: str, timeout: int = 60
|
||||
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]:
|
||||
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:
|
||||
cmd = [
|
||||
b"ffprobe",
|
||||
b"-hide_banner",
|
||||
@@ -217,8 +224,17 @@ def ffprobe(
|
||||
return parse_ffprobe(so)
|
||||
|
||||
|
||||
def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]:
|
||||
"""ffprobe -show_format -show_streams"""
|
||||
def parse_ffprobe(
|
||||
txt: str,
|
||||
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:
|
||||
"""
|
||||
txt: output from ffprobe -show_format -show_streams
|
||||
returns:
|
||||
* normalized tags
|
||||
* original/raw tags
|
||||
* list of streams
|
||||
* format props
|
||||
"""
|
||||
streams = []
|
||||
fmt = {}
|
||||
g = {}
|
||||
@@ -242,7 +258,7 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
|
||||
ret: dict[str, Any] = {} # processed
|
||||
md: dict[str, list[Any]] = {} # raw tags
|
||||
|
||||
is_audio = fmt.get("format_name") in ["mp3", "ogg", "flac", "wav"]
|
||||
is_audio = fmt.get("format_name") in FMT_AU
|
||||
if fmt.get("filename", "").split(".")[-1].lower() in ["m4a", "aac"]:
|
||||
is_audio = True
|
||||
|
||||
@@ -270,6 +286,8 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
|
||||
["channel_layout", "chs"],
|
||||
["sample_rate", ".hz"],
|
||||
["bit_rate", ".aq"],
|
||||
["bits_per_sample", ".bps"],
|
||||
["bits_per_raw_sample", ".bprs"],
|
||||
["duration", ".dur"],
|
||||
]
|
||||
|
||||
@@ -309,7 +327,7 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
|
||||
ret[rk] = v1
|
||||
|
||||
if ret.get("vc") == "ansi": # shellscript
|
||||
return {}, {}
|
||||
return {}, {}, [], {}
|
||||
|
||||
for strm in streams:
|
||||
for sk, sv in strm.items():
|
||||
@@ -358,7 +376,83 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
|
||||
zero = int("0")
|
||||
zd = {k: (zero, v) for k, v in ret.items()}
|
||||
|
||||
return zd, md
|
||||
return zd, md, streams, fmt
|
||||
|
||||
|
||||
def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]:
|
||||
import zipfile
|
||||
|
||||
from .dxml import parse_xml
|
||||
|
||||
try:
|
||||
from urlparse import urljoin # type: ignore # Python2
|
||||
except ImportError:
|
||||
from urllib.parse import urljoin # Python3
|
||||
|
||||
with zipfile.ZipFile(abspath, "r") as z:
|
||||
# First open the container file to find the package document (.opf file)
|
||||
try:
|
||||
container_root = parse_xml(z.read("META-INF/container.xml").decode())
|
||||
except KeyError:
|
||||
log("epub: no container file found in %s" % (abspath,))
|
||||
return None
|
||||
|
||||
# https://www.w3.org/TR/epub-33/#sec-container.xml-rootfile-elem
|
||||
container_ns = {"": "urn:oasis:names:tc:opendocument:xmlns:container"}
|
||||
# One file could contain multiple package documents, default to the first one
|
||||
rootfile_path = container_root.find("./rootfiles/rootfile", container_ns).get(
|
||||
"full-path"
|
||||
)
|
||||
|
||||
# Then open the first package document to find the path of the cover image
|
||||
try:
|
||||
package_root = parse_xml(z.read(rootfile_path).decode())
|
||||
except KeyError:
|
||||
log("epub: no package document found in %s" % (abspath,))
|
||||
return None
|
||||
|
||||
# https://www.w3.org/TR/epub-33/#sec-package-doc
|
||||
package_ns = {"": "http://www.idpf.org/2007/opf"}
|
||||
# https://www.w3.org/TR/epub-33/#sec-cover-image
|
||||
coverimage_path_node = package_root.find(
|
||||
"./manifest/item[@properties='cover-image']", package_ns
|
||||
)
|
||||
if coverimage_path_node is not None:
|
||||
coverimage_path = coverimage_path_node.get("href")
|
||||
else:
|
||||
# This might be an EPUB2 file, try the legacy way of specifying covers
|
||||
coverimage_path = _get_cover_from_epub2(log, package_root, package_ns)
|
||||
|
||||
if not coverimage_path:
|
||||
raise Exception("no cover inside epub")
|
||||
|
||||
# This url is either absolute (in the .epub) or relative to the package document
|
||||
adjusted_cover_path = urljoin(rootfile_path, coverimage_path)
|
||||
|
||||
try:
|
||||
return z.open(adjusted_cover_path)
|
||||
except KeyError:
|
||||
t = "epub: cover specified in package document, but doesn't exist: %s"
|
||||
log(t % (adjusted_cover_path,))
|
||||
|
||||
|
||||
def _get_cover_from_epub2(
|
||||
log: "NamedLogger", package_root, package_ns
|
||||
) -> Optional[str]:
|
||||
# <meta name="cover" content="id-to-cover-image"> in <metadata>, then
|
||||
# <item> in <manifest>
|
||||
xn = package_root.find("./metadata/meta[@name='cover']", package_ns)
|
||||
cover_id = xn.get("content") if xn is not None else None
|
||||
|
||||
if not cover_id:
|
||||
return None
|
||||
|
||||
for node in package_root.iterfind("./manifest/item", package_ns):
|
||||
if node.get("id") == cover_id:
|
||||
cover_path = node.get("href")
|
||||
return cover_path
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class MTag(object):
|
||||
@@ -371,6 +465,8 @@ class MTag(object):
|
||||
"ffprobe" if args.no_mutagen or (HAVE_FFPROBE and EXE) else "mutagen"
|
||||
)
|
||||
self.can_ffprobe = HAVE_FFPROBE and not args.no_mtag_ff
|
||||
self.read_xattrs = args.have_db_xattr
|
||||
self.get = self._get_xattr if self.read_xattrs else self._get_main
|
||||
mappings = args.mtm
|
||||
or_ffprobe = " or FFprobe"
|
||||
|
||||
@@ -392,7 +488,13 @@ class MTag(object):
|
||||
msg = "found FFprobe but it was disabled by --no-mtag-ff"
|
||||
self.log(msg, c=3)
|
||||
|
||||
if self.read_xattrs and not self.usable:
|
||||
t = "don't have the necessary dependencies to read conventional media tags, but will read xattrs"
|
||||
self.log(t)
|
||||
self.usable = True
|
||||
|
||||
if not self.usable:
|
||||
self._get = None
|
||||
if EXE:
|
||||
t = "copyparty.exe cannot use mutagen; need ffprobe.exe to read media tags: "
|
||||
self.log(t + FFMPEG_URL)
|
||||
@@ -424,13 +526,12 @@ class MTag(object):
|
||||
"album-artist",
|
||||
"tpe2",
|
||||
"aart",
|
||||
"conductor",
|
||||
"organization",
|
||||
"band",
|
||||
],
|
||||
".tn": ["tracknumber", "trck", "trkn", "track"],
|
||||
"genre": ["genre", "tcon", "\u00a9gen"],
|
||||
"date": [
|
||||
"tdate": [
|
||||
"original-release-date",
|
||||
"release-date",
|
||||
"date",
|
||||
@@ -552,12 +653,44 @@ class MTag(object):
|
||||
|
||||
return r1
|
||||
|
||||
def get(self, abspath: str) -> dict[str, Union[str, float]]:
|
||||
def _get_xattr(
|
||||
self, abspath: str, vf: dict[str, Any]
|
||||
) -> dict[str, Union[str, float]]:
|
||||
ret = self._get_main(abspath, vf) if self._get else {}
|
||||
if "db_xattr_no" in vf:
|
||||
try:
|
||||
neg = vf["db_xattr_no"]
|
||||
zsl = os.listxattr(abspath)
|
||||
zsl = [x for x in zsl if x not in neg]
|
||||
for xattr in zsl:
|
||||
zb = os.getxattr(abspath, xattr)
|
||||
ret[xattr] = zb.decode("utf-8", "replace")
|
||||
except:
|
||||
self.log("failed to read xattrs from [%s]\n%s", abspath, min_ex(), 3)
|
||||
elif "db_xattr_yes" in vf:
|
||||
for xattr in vf["db_xattr_yes"]:
|
||||
if "=" in xattr:
|
||||
xattr, name = xattr.split("=", 1)
|
||||
else:
|
||||
name = xattr
|
||||
try:
|
||||
zs = os.getxattr(abspath, xattr)
|
||||
ret[name] = zs.decode("utf-8", "replace")
|
||||
except:
|
||||
pass
|
||||
return ret
|
||||
|
||||
def _get_main(
|
||||
self, abspath: str, vf: dict[str, Any]
|
||||
) -> dict[str, Union[str, float]]:
|
||||
ext = abspath.split(".")[-1].lower()
|
||||
if ext not in self.args.au_unpk:
|
||||
return self._get(abspath)
|
||||
|
||||
ap = au_unpk(self.log, self.args.au_unpk, abspath)
|
||||
if not ap:
|
||||
return {}
|
||||
|
||||
ret = self._get(ap)
|
||||
if ap != abspath:
|
||||
wunlink(self.log, ap, VF_CAREFUL)
|
||||
@@ -629,7 +762,7 @@ class MTag(object):
|
||||
if not bos.path.isfile(abspath):
|
||||
return {}
|
||||
|
||||
ret, md = ffprobe(abspath, self.args.mtag_to)
|
||||
ret, md, _, _ = ffprobe(abspath, self.args.mtag_to)
|
||||
|
||||
if self.args.mtag_vv:
|
||||
for zd in (ret, dict(md)):
|
||||
@@ -663,6 +796,9 @@ class MTag(object):
|
||||
ap = abspath
|
||||
|
||||
ret: dict[str, Any] = {}
|
||||
if not ap:
|
||||
return ret
|
||||
|
||||
for tagname, parser in sorted(parsers.items(), key=lambda x: (x[1].pri, x[0])):
|
||||
try:
|
||||
cmd = [parser.bin, ap]
|
||||
|
||||
@@ -15,7 +15,7 @@ from ipaddress import (
|
||||
)
|
||||
|
||||
from .__init__ import MACOS, TYPE_CHECKING
|
||||
from .util import Daemon, Netdev, find_prefix, min_ex, spack
|
||||
from .util import IP6_LL, IP64_LL, Daemon, Netdev, find_prefix, min_ex, spack
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .svchub import SvcHub
|
||||
@@ -96,7 +96,10 @@ class MCast(object):
|
||||
def create_servers(self) -> list[str]:
|
||||
bound: list[str] = []
|
||||
netdevs = self.hub.tcpsrv.netdevs
|
||||
ips = [x[0] for x in self.hub.tcpsrv.bound]
|
||||
blist = self.hub.tcpsrv.bound
|
||||
if self.args.http_no_tcp:
|
||||
blist = self.hub.tcpsrv.seen_eps
|
||||
ips = [x[0] for x in blist]
|
||||
|
||||
if "::" in ips:
|
||||
ips = [x for x in ips if x != "::"] + list(
|
||||
@@ -145,7 +148,7 @@ class MCast(object):
|
||||
all_selected = ips[:]
|
||||
|
||||
# discard non-linklocal ipv6
|
||||
ips = [x for x in ips if ":" not in x or x.startswith("fe80")]
|
||||
ips = [x for x in ips if ":" not in x or x.startswith(IP6_LL)]
|
||||
|
||||
if not ips:
|
||||
raise NoIPs()
|
||||
@@ -183,11 +186,7 @@ class MCast(object):
|
||||
srv.ips[oth_ip.split("/")[0]] = ipaddress.ip_network(oth_ip, False)
|
||||
|
||||
# gvfs breaks if a linklocal ip appears in a dns reply
|
||||
ll = {
|
||||
k: v
|
||||
for k, v in srv.ips.items()
|
||||
if k.startswith("169.254") or k.startswith("fe80")
|
||||
}
|
||||
ll = {k: v for k, v in srv.ips.items() if k.startswith(IP64_LL)}
|
||||
rt = {k: v for k, v in srv.ips.items() if k not in ll}
|
||||
|
||||
if self.args.ll or not rt:
|
||||
|
||||
+6
-1
@@ -15,7 +15,7 @@ try:
|
||||
raise Exception()
|
||||
|
||||
HAVE_ARGON2 = True
|
||||
from argon2 import __version__ as argon2ver
|
||||
from argon2 import exceptions as argon2ex
|
||||
except:
|
||||
HAVE_ARGON2 = False
|
||||
|
||||
@@ -25,6 +25,7 @@ class PWHash(object):
|
||||
self.args = args
|
||||
|
||||
zsl = args.ah_alg.split(",")
|
||||
zsl = [x.strip() for x in zsl]
|
||||
alg = zsl[0]
|
||||
if alg == "none":
|
||||
alg = ""
|
||||
@@ -147,6 +148,10 @@ class PWHash(object):
|
||||
def cli(self) -> None:
|
||||
import getpass
|
||||
|
||||
if self.args.usernames:
|
||||
t = "since you have enabled --usernames, please provide username:password"
|
||||
print(t)
|
||||
|
||||
while True:
|
||||
try:
|
||||
p1 = getpass.getpass("password> ")
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
try:
|
||||
if os.environ.get("PRTY_SYS_ALL") or os.environ.get("PRTY_SYS_QRCG"):
|
||||
raise ImportError()
|
||||
from .stolen.qrcodegen import QrCode
|
||||
|
||||
qrgen = QrCode.encode_binary
|
||||
VENDORED = True
|
||||
except ImportError:
|
||||
VENDORED = False
|
||||
from qrcodegen import QrCode
|
||||
|
||||
if os.environ.get("PRTY_MODSPEC"):
|
||||
from inspect import getsourcefile
|
||||
|
||||
print("PRTY_MODSPEC: qrcode:", getsourcefile(QrCode))
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
import typing
|
||||
from typing import Any, Optional, Sequence, Union
|
||||
|
||||
|
||||
if not VENDORED:
|
||||
|
||||
def _qrgen(data: Union[bytes, Sequence[int]]) -> "QrCode":
|
||||
ret = None
|
||||
V = QrCode.Ecc
|
||||
for e in [V.HIGH, V.QUARTILE, V.MEDIUM, V.LOW]:
|
||||
qr = QrCode.encode_binary(data, e)
|
||||
qr.size = qr._size
|
||||
qr.modules = qr._modules
|
||||
if not ret or ret.size > qr.size:
|
||||
ret = qr
|
||||
return ret
|
||||
|
||||
qrgen = _qrgen
|
||||
|
||||
|
||||
def qr2txt(qr: QrCode, zoom: int = 1, pad: int = 4) -> str:
|
||||
tab = qr.modules
|
||||
sz = qr.size
|
||||
if sz % 2 and zoom == 1:
|
||||
tab.append([False] * sz)
|
||||
|
||||
tab = [[False] * sz] * pad + tab + [[False] * sz] * pad
|
||||
tab = [[False] * pad + x + [False] * pad for x in tab]
|
||||
|
||||
rows: list[str] = []
|
||||
if zoom == 1:
|
||||
for y in range(0, len(tab), 2):
|
||||
row = ""
|
||||
for x in range(len(tab[y])):
|
||||
v = 2 if tab[y][x] else 0
|
||||
v += 1 if tab[y + 1][x] else 0
|
||||
row += " ▄▀█"[v]
|
||||
rows.append(row)
|
||||
else:
|
||||
for tr in tab:
|
||||
row = ""
|
||||
for zb in tr:
|
||||
row += " █"[int(zb)] * 2
|
||||
rows.append(row)
|
||||
|
||||
return "\n".join(rows)
|
||||
|
||||
|
||||
def qr2png(
|
||||
qr: QrCode,
|
||||
zoom: int,
|
||||
pad: int,
|
||||
bg: Optional[tuple[int, int, int]],
|
||||
fg: Optional[tuple[int, int, int]],
|
||||
ap: str,
|
||||
) -> None:
|
||||
from PIL import Image
|
||||
|
||||
tab = qr.modules
|
||||
sz = qr.size
|
||||
psz = sz + pad * 2
|
||||
if bg:
|
||||
img = Image.new("RGB", (psz, psz), bg)
|
||||
else:
|
||||
img = Image.new("RGBA", (psz, psz), (0, 0, 0, 0))
|
||||
fg = (fg[0], fg[1], fg[2], 255)
|
||||
for y in range(sz):
|
||||
for x in range(sz):
|
||||
if tab[y][x]:
|
||||
img.putpixel((x + pad, y + pad), fg)
|
||||
if zoom != 1:
|
||||
img = img.resize((sz * zoom, sz * zoom), Image.Resampling.NEAREST)
|
||||
img.save(ap)
|
||||
|
||||
|
||||
def qr2svg(qr: QrCode, border: int) -> str:
|
||||
parts: list[str] = []
|
||||
for y in range(qr.size):
|
||||
sy = border + y
|
||||
for x in range(qr.size):
|
||||
if qr.modules[y][x]:
|
||||
parts.append("M%d,%dh1v1h-1z" % (border + x, sy))
|
||||
t = """\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 {0} {0}" stroke="none">
|
||||
<rect width="100%" height="100%" fill="#F7F7F7"/>
|
||||
<path d="{1}" fill="#111111"/>
|
||||
</svg>
|
||||
"""
|
||||
return t.format(qr.size + border * 2, " ".join(parts))
|
||||
@@ -0,0 +1,873 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import errno
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import select
|
||||
import socket
|
||||
import time
|
||||
from threading import ExceptHookArgs
|
||||
|
||||
import paramiko
|
||||
import paramiko.common
|
||||
import paramiko.sftp_attr
|
||||
from paramiko.common import AUTH_FAILED, AUTH_SUCCESSFUL
|
||||
from paramiko.sftp import (
|
||||
SFTP_FAILURE,
|
||||
SFTP_NO_SUCH_FILE,
|
||||
SFTP_OK,
|
||||
SFTP_OP_UNSUPPORTED,
|
||||
SFTP_PERMISSION_DENIED,
|
||||
)
|
||||
|
||||
from .__init__ import ANYWIN, TYPE_CHECKING
|
||||
from .authsrv import LEELOO_DALLAS, VFS, AuthSrv
|
||||
from .bos import bos
|
||||
from .util import (
|
||||
VF_CAREFUL,
|
||||
Daemon,
|
||||
ODict,
|
||||
Pebkac,
|
||||
ipnorm,
|
||||
min_ex,
|
||||
read_utf8,
|
||||
relchk,
|
||||
runhook,
|
||||
sanitize_fn,
|
||||
ub64enc,
|
||||
undot,
|
||||
vjoin,
|
||||
wunlink,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .svchub import SvcHub
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, BinaryIO, Optional, Union
|
||||
|
||||
SATTR = paramiko.sftp_attr.SFTPAttributes
|
||||
|
||||
|
||||
class SSH_Srv(paramiko.ServerInterface):
|
||||
def __init__(self, hub: "SvcHub", addr: Any):
|
||||
self.hub = hub
|
||||
self.args = args = hub.args
|
||||
self.log_func = hub.log
|
||||
self.uname = "*"
|
||||
|
||||
self.addr = addr
|
||||
self.ip = addr[0]
|
||||
if self.ip.startswith("::ffff:"):
|
||||
self.ip = self.ip[7:]
|
||||
|
||||
zsl = []
|
||||
if args.sftp_anon:
|
||||
zsl.append("none")
|
||||
if args.sftp_key2u:
|
||||
zsl.append("publickey")
|
||||
if args.sftp_pw or args.sftp_anon:
|
||||
zsl.append("password")
|
||||
self._auths = ",".join(zsl)
|
||||
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.hub.log("sftp:%s" % (self.ip,), msg, c)
|
||||
|
||||
def get_allowed_auths(self, username: str) -> str:
|
||||
return self._auths
|
||||
|
||||
def get_banner(self) -> tuple[Optional[str], Optional[str]]:
|
||||
if self.args.sftpv:
|
||||
self.log("get_banner")
|
||||
t = self.args.sftp_banner
|
||||
if not t:
|
||||
return (None, None)
|
||||
if t.startswith("@"):
|
||||
t = read_utf8(self.log, t[1:], False)
|
||||
if t and not t.endswith("\n"):
|
||||
t += "\n"
|
||||
return (t, "en-US")
|
||||
|
||||
def check_channel_request(self, kind: str, chanid: int) -> int:
|
||||
if self.args.sftpv:
|
||||
self.log("channel-request: %r, %r" % (kind, chanid))
|
||||
if kind == "session":
|
||||
return paramiko.common.OPEN_SUCCEEDED
|
||||
return paramiko.common.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
|
||||
|
||||
def check_auth_none(self, username: str) -> int:
|
||||
try:
|
||||
return self._check_auth_none(username)
|
||||
except:
|
||||
self.log("unhandled exception: %s" % (min_ex(),), 1)
|
||||
return AUTH_FAILED
|
||||
|
||||
def _check_auth_none(self, uname: str) -> int:
|
||||
args = self.args
|
||||
if uname != args.sftp_anon or not uname:
|
||||
return AUTH_FAILED
|
||||
|
||||
ipn = ipnorm(self.ip)
|
||||
bans = self.hub.bans
|
||||
if ipn in bans:
|
||||
rt = bans[ipn] - time.time()
|
||||
if rt < 0:
|
||||
self.log("client unbanned")
|
||||
del bans[ipn]
|
||||
else:
|
||||
self.log("client is banned")
|
||||
return AUTH_FAILED
|
||||
|
||||
self.uname = "*"
|
||||
self.log("auth-none OK: *")
|
||||
return AUTH_SUCCESSFUL
|
||||
|
||||
def check_auth_password(self, username: str, password: str) -> int:
|
||||
try:
|
||||
return self._check_auth_password(username, password)
|
||||
except:
|
||||
self.log("unhandled exception: %s" % (min_ex(),), 1)
|
||||
return AUTH_FAILED
|
||||
|
||||
def _check_auth_password(self, uname: str, pw: str) -> int:
|
||||
args = self.args
|
||||
if args.sftpv:
|
||||
logpw = pw
|
||||
if args.log_badpwd == 0:
|
||||
logpw = ""
|
||||
elif args.log_badpwd == 2:
|
||||
zb = hashlib.sha512(pw.encode("utf-8", "replace")).digest()
|
||||
logpw = "%" + ub64enc(zb[:12]).decode("ascii")
|
||||
self.log("auth-pw: %r, %r" % (uname, logpw))
|
||||
|
||||
ipn = ipnorm(self.ip)
|
||||
bans = self.hub.bans
|
||||
if ipn in bans:
|
||||
rt = bans[ipn] - time.time()
|
||||
if rt < 0:
|
||||
self.log("client unbanned")
|
||||
del bans[ipn]
|
||||
else:
|
||||
self.log("client is banned")
|
||||
return AUTH_FAILED
|
||||
|
||||
anon = args.sftp_anon
|
||||
if anon and uname == anon:
|
||||
self.uname = "*"
|
||||
self.log("auth-pw OK: *")
|
||||
return AUTH_SUCCESSFUL
|
||||
|
||||
if not args.sftp_pw:
|
||||
return AUTH_FAILED
|
||||
|
||||
if args.usernames:
|
||||
alts = ["%s:%s" % (uname, pw)]
|
||||
else:
|
||||
alts = [pw, uname]
|
||||
|
||||
attempt = "%s:%s" % (uname, pw)
|
||||
uname = ""
|
||||
asrv = self.hub.asrv
|
||||
for zs in alts:
|
||||
zs = asrv.iacct.get(asrv.ah.hash(zs), "")
|
||||
if zs:
|
||||
uname = zs
|
||||
break
|
||||
|
||||
if args.ipu and uname == "*":
|
||||
uname = args.ipu_iu[args.ipu_nm.map(self.ip)]
|
||||
if args.ipr and uname in args.ipr_u:
|
||||
if not args.ipr_u[uname].map(self.ip):
|
||||
logging.warning("username [%s] rejected by --ipr", uname)
|
||||
return AUTH_FAILED
|
||||
|
||||
if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):
|
||||
g = self.hub.gpwd
|
||||
if g.lim:
|
||||
bonk, ip = g.bonk(self.ip, attempt)
|
||||
if bonk:
|
||||
logging.warning("client banned: invalid passwords")
|
||||
bans[self.ip] = bonk
|
||||
try:
|
||||
# only possible if multiprocessing disabled
|
||||
self.hub.broker.httpsrv.bans[ip] = bonk # type: ignore
|
||||
self.hub.broker.httpsrv.nban += 1 # type: ignore
|
||||
except:
|
||||
pass
|
||||
return AUTH_FAILED
|
||||
|
||||
self.uname = uname
|
||||
self.log("auth-pw OK: %s" % (uname,))
|
||||
return AUTH_SUCCESSFUL
|
||||
|
||||
def check_auth_publickey(self, username: str, key: paramiko.PKey) -> int:
|
||||
try:
|
||||
return self._check_auth_publickey(username, key)
|
||||
except:
|
||||
self.log("unhandled exception: %s" % (min_ex(),), 1)
|
||||
return AUTH_FAILED
|
||||
|
||||
def _check_auth_publickey(self, uname: str, key: paramiko.PKey) -> int:
|
||||
args = self.args
|
||||
if args.sftpv:
|
||||
zs = key.get_name() + "," + key.get_base64()[:32]
|
||||
self.log("auth-key: %r, %r" % (uname, zs))
|
||||
|
||||
ipn = ipnorm(self.ip)
|
||||
bans = self.hub.bans
|
||||
if ipn in bans:
|
||||
rt = bans[ipn] - time.time()
|
||||
if rt < 0:
|
||||
self.log("client unbanned")
|
||||
del bans[ipn]
|
||||
else:
|
||||
self.log("client is banned")
|
||||
return AUTH_FAILED
|
||||
|
||||
anon = args.sftp_anon
|
||||
if anon and uname == anon:
|
||||
self.uname = "*"
|
||||
self.log("auth-key OK: *")
|
||||
return AUTH_SUCCESSFUL
|
||||
|
||||
attempt = "%s %s" % (key.get_name(), key.get_base64())
|
||||
ok = args.sftp_key2u.get(attempt) == uname
|
||||
|
||||
if ok and args.ipr and uname in args.ipr_u:
|
||||
if not args.ipr_u[uname].map(self.ip):
|
||||
logging.warning("username [%s] rejected by --ipr", uname)
|
||||
return AUTH_FAILED
|
||||
|
||||
asrv = self.hub.asrv
|
||||
if not ok or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):
|
||||
self.log("auth-key REJECTED: %s" % (uname,))
|
||||
return AUTH_FAILED
|
||||
|
||||
self.uname = uname
|
||||
self.log("auth-key OK: %s" % (uname,))
|
||||
return AUTH_SUCCESSFUL
|
||||
|
||||
|
||||
class SFTP_FH(paramiko.SFTPHandle):
|
||||
def __init__(self, flags: int = 0) -> None:
|
||||
self.filename = ""
|
||||
self.readfile: Optional[BinaryIO] = None
|
||||
self.writefile: Optional[BinaryIO] = None
|
||||
super(SFTP_FH, self).__init__(flags)
|
||||
|
||||
def stat(self):
|
||||
try:
|
||||
f = self.readfile or self.writefile
|
||||
return SATTR.from_stat(os.fstat(f.fileno()))
|
||||
except OSError as ex:
|
||||
return paramiko.SFTPServer.convert_errno(ex.errno)
|
||||
|
||||
def chattr(self, attr):
|
||||
# python doesn't have equivalents to fchown or fchmod, so we have to
|
||||
# use the stored filename
|
||||
if not self.writefile:
|
||||
return SFTP_PERMISSION_DENIED
|
||||
try:
|
||||
paramiko.SFTPServer.set_file_attr(self.filename, attr)
|
||||
return SFTP_OK
|
||||
except OSError as ex:
|
||||
return paramiko.SFTPServer.convert_errno(ex.errno)
|
||||
|
||||
|
||||
class SFTP_Srv(paramiko.SFTPServerInterface):
|
||||
def __init__(self, ssh: paramiko.ServerInterface, *a, **ka):
|
||||
super(SFTP_Srv, self).__init__(ssh, *a, **ka)
|
||||
self.ssh = ssh
|
||||
self.ip: str = ssh.ip # type: ignore
|
||||
self.hub: "SvcHub" = ssh.hub # type: ignore
|
||||
self.uname: str = ssh.uname # type: ignore
|
||||
self.args = self.hub.args
|
||||
self.asrv: "AuthSrv" = self.hub.asrv
|
||||
self.v = self.args.sftpv
|
||||
self.vv = self.args.sftpvv
|
||||
|
||||
if self.uname == LEELOO_DALLAS:
|
||||
raise Exception("send her back")
|
||||
|
||||
self.vols = [
|
||||
vp
|
||||
for vp, vn in self.asrv.vfs.all_vols.items()
|
||||
if self.uname in vn.axs.uread
|
||||
or self.uname in vn.axs.uwrite
|
||||
or self.uname in vn.axs.uget
|
||||
]
|
||||
self.vis = set()
|
||||
for zs in self.vols:
|
||||
self.vis.add(zs)
|
||||
while zs:
|
||||
zs = zs.rsplit("/", 1)[0] if "/" in zs else ""
|
||||
self.vis.add(zs)
|
||||
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.hub.log("sftp:%s" % (self.ip,), msg, c)
|
||||
|
||||
def v2a(
|
||||
self,
|
||||
vpath: str,
|
||||
r: bool = False,
|
||||
w: bool = False,
|
||||
m: bool = False,
|
||||
d: bool = False,
|
||||
) -> tuple[str, VFS, str]:
|
||||
vpath = vpath.replace(os.sep, "/").strip("/")
|
||||
rd, fn = os.path.split(vpath)
|
||||
if relchk(rd):
|
||||
self.log("malicious vpath: %s", vpath)
|
||||
raise Exception("Unsupported characters in [%s]" % (vpath,))
|
||||
|
||||
fn = sanitize_fn(fn or "")
|
||||
vpath = vjoin(rd, fn)
|
||||
vn, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
|
||||
if (
|
||||
w
|
||||
and fn.lower() in vn.flags["emb_all"]
|
||||
and self.uname not in vn.axs.uread
|
||||
and "wo_up_readme" not in vn.flags
|
||||
):
|
||||
fn = "_wo_" + fn
|
||||
vpath = vjoin(rd, fn)
|
||||
vn, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
|
||||
|
||||
if not vn.realpath:
|
||||
# return "", vn, rem
|
||||
raise OSError(errno.ENOENT, "no filesystem mounted at [/%s]" % (vpath,))
|
||||
|
||||
if "xdev" in vn.flags or "xvol" in vn.flags:
|
||||
ap = vn.canonical(rem)
|
||||
avn = vn.chk_ap(ap)
|
||||
t = "Permission denied in [{}]"
|
||||
if not avn:
|
||||
raise OSError(errno.EPERM, "permission denied in [/%s]" % (vpath,))
|
||||
|
||||
cr, cw, cm, cd, _, _, _, _, _ = avn.uaxs[self.uname]
|
||||
if r and not cr or w and not cw or m and not cm or d and not cd:
|
||||
raise OSError(errno.EPERM, "permission denied in [/%s]" % (vpath,))
|
||||
|
||||
if "bcasechk" in vn.flags and not vn.casechk(rem, True):
|
||||
raise OSError(errno.ENOENT, "file does not exist case-sensitively")
|
||||
|
||||
return os.path.join(vn.realpath, rem), vn, rem
|
||||
|
||||
def list_folder(self, path: str) -> list[SATTR] | int:
|
||||
try:
|
||||
return self._list_folder(path)
|
||||
except Pebkac as ex:
|
||||
if ex.code == 404:
|
||||
self.log("folder 404: %s" % (path,))
|
||||
return SFTP_NO_SUCH_FILE
|
||||
return SFTP_PERMISSION_DENIED
|
||||
except:
|
||||
self.log("unhandled exception: %s" % (min_ex(),), 1)
|
||||
return SFTP_FAILURE
|
||||
|
||||
def _list_folder(self, path: str) -> list[SATTR] | int:
|
||||
if self.v:
|
||||
self.log("ls(%s):" % (path,))
|
||||
path = path.strip("/")
|
||||
try:
|
||||
ap, vn, rem = self.v2a(path, r=True)
|
||||
except Pebkac:
|
||||
try:
|
||||
self.v2a(path, w=True)
|
||||
self.log("ls(%s): [] (write-only)" % (path,))
|
||||
return [] # display write-only folders as empty
|
||||
except:
|
||||
pass
|
||||
if path not in self.vis:
|
||||
self.log("ls(%s): EPERM" % (path,))
|
||||
return SFTP_PERMISSION_DENIED
|
||||
# list of accessible volumes
|
||||
ret = []
|
||||
zi = int(time.time())
|
||||
vst = os.stat_result((16877, -1, -1, 1, 1000, 1000, 8, zi, zi, zi))
|
||||
prefix = path + "/"
|
||||
for vn in self.asrv.vfs.all_nodes.values():
|
||||
if path and not vn.vpath.startswith(prefix):
|
||||
continue # vn is parent
|
||||
vname = vn.vpath[len(prefix) :]
|
||||
if "/" in vname or not vname:
|
||||
continue # only include vols at current level
|
||||
ret.append(SATTR.from_stat(vst, filename=vn.vpath))
|
||||
ret.sort(key=lambda x: x.filename)
|
||||
self.log("ls(%s): vfs-vols; |%d|" % (path, len(ret)))
|
||||
return ret
|
||||
|
||||
_, vfs_ls, vfs_virt = vn.ls(
|
||||
rem,
|
||||
self.uname,
|
||||
not self.args.no_scandir,
|
||||
[[True, False], [False, True]],
|
||||
throw=True,
|
||||
)
|
||||
ret = [SATTR.from_stat(x[1], filename=x[0]) for x in vfs_ls]
|
||||
for zs, vn2 in vfs_virt.items():
|
||||
if not vn2.realpath:
|
||||
continue
|
||||
st = bos.stat(vn2.realpath)
|
||||
ret.append(SATTR.from_stat(st, filename=zs))
|
||||
if self.uname not in vn.axs.udot:
|
||||
ret = [x for x in ret if not x.filename.split("/")[-1].startswith(".")]
|
||||
ret.sort(key=lambda x: x.filename)
|
||||
self.log("ls(%s): |%d|" % (path, len(ret)))
|
||||
return ret
|
||||
|
||||
def stat(self, path: str) -> SATTR | int:
|
||||
try:
|
||||
return self._stat(path)
|
||||
except:
|
||||
self.log("unhandled exception: %s" % (min_ex(),), 1)
|
||||
return SFTP_FAILURE
|
||||
|
||||
def lstat(self, path: str) -> SATTR | int:
|
||||
try:
|
||||
return self._stat(path)
|
||||
except:
|
||||
self.log("unhandled exception: %s" % (min_ex(),), 1)
|
||||
return SFTP_FAILURE
|
||||
|
||||
def _stat(self, vp: str) -> SATTR | int:
|
||||
vp = vp.strip("/")
|
||||
try:
|
||||
ap, vn, _ = self.v2a(vp)
|
||||
if (
|
||||
self.uname not in vn.axs.uread
|
||||
and self.uname not in vn.axs.uwrite
|
||||
and self.uname not in vn.axs.uget
|
||||
):
|
||||
self.log("stat(%s): EPERM" % (vp,))
|
||||
return SFTP_PERMISSION_DENIED
|
||||
st = bos.stat(ap)
|
||||
self.log("stat(%s): %s" % (vp, st))
|
||||
except:
|
||||
if vp not in self.vis:
|
||||
self.log("stat(%s): ENOENT" % (vp,))
|
||||
return SFTP_NO_SUCH_FILE
|
||||
zi = int(time.time())
|
||||
st = os.stat_result((16877, -1, -1, 1, 1000, 1000, 8, zi, zi, zi))
|
||||
self.log("stat(%s): vfs-vols")
|
||||
return SATTR.from_stat(st)
|
||||
|
||||
def open(self, path: str, flags: int, attr: SATTR) -> paramiko.SFTPHandle | int:
|
||||
try:
|
||||
return self._open(path, flags, attr)
|
||||
except:
|
||||
self.log("unhandled exception: %s" % (min_ex(),), 1)
|
||||
return SFTP_FAILURE
|
||||
|
||||
def _open(self, vp: str, iflag: int, attr: SATTR) -> paramiko.SFTPHandle | int:
|
||||
if ANYWIN:
|
||||
iflag |= os.O_BINARY
|
||||
if iflag & os.O_WRONLY:
|
||||
rd = False
|
||||
wr = True
|
||||
if iflag & os.O_APPEND:
|
||||
smode = "ab"
|
||||
else:
|
||||
smode = "wb"
|
||||
elif iflag & os.O_RDWR:
|
||||
rd = wr = True
|
||||
if iflag & os.O_APPEND:
|
||||
smode = "a+b"
|
||||
else:
|
||||
smode = "r+b"
|
||||
else:
|
||||
rd = True
|
||||
wr = False
|
||||
smode = "rb"
|
||||
|
||||
try:
|
||||
vn, rem = self.asrv.vfs.get(vp, self.uname, rd, wr)
|
||||
ap = os.path.join(vn.realpath, rem)
|
||||
vf = vn.flags
|
||||
except Pebkac as ex:
|
||||
t = "denied open file [%s], iflag=%s, read=%s, write=%s: %s"
|
||||
self.log(t % (vp, iflag, rd, wr, ex))
|
||||
return SFTP_PERMISSION_DENIED
|
||||
|
||||
self.log("open(%s, %x, %s)" % (vp, iflag, smode))
|
||||
|
||||
if wr:
|
||||
try:
|
||||
st = bos.stat(ap)
|
||||
td = time.time() - st.st_mtime
|
||||
need_unlink = True
|
||||
except:
|
||||
need_unlink = False
|
||||
td = 0
|
||||
|
||||
xbu = vn.flags.get("xbu")
|
||||
if xbu:
|
||||
hr = runhook(
|
||||
self.log,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.sftp",
|
||||
xbu,
|
||||
ap,
|
||||
vp,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
"7.3.8.7",
|
||||
time.time(),
|
||||
None,
|
||||
)
|
||||
t = hr.get("rejectmsg") or ""
|
||||
if t or hr.get("rc") != 0:
|
||||
if not t:
|
||||
t = "upload blocked by xbu server config: %r" % (vp,)
|
||||
self.log(t, 3)
|
||||
return SFTP_PERMISSION_DENIED
|
||||
|
||||
self.log("writing to [%s] => [%s]" % (vp, ap))
|
||||
|
||||
if wr and need_unlink: # type: ignore # !rm
|
||||
assert td # type: ignore # !rm
|
||||
if td >= -1 and td <= self.args.ftp_wt:
|
||||
# within permitted timeframe; allow overwrite or resume
|
||||
do_it = True
|
||||
elif self.args.no_del or self.args.ftp_no_ow:
|
||||
# file too old, or overwrite not allowed; reject
|
||||
do_it = False
|
||||
else:
|
||||
# allow overwrite if user has delete permission
|
||||
do_it = self.uname in vn.axs.udel
|
||||
|
||||
if not do_it:
|
||||
t = "file already exists and no permission to overwrite: %s"
|
||||
self.log(t % (vp,))
|
||||
return SFTP_PERMISSION_DENIED
|
||||
|
||||
# Don't unlink file for append mode
|
||||
elif "a" not in smode:
|
||||
wunlink(self.log, ap, VF_CAREFUL)
|
||||
|
||||
chmod = getattr(attr, "st_mode", None)
|
||||
if chmod is None:
|
||||
chmod = vf.get("chmod_f", 0o644)
|
||||
self.log("open(%s, %x): client did not chmod" % (vp, iflag))
|
||||
else:
|
||||
self.log("open(%s, %x): client set chmod 0%o" % (vp, iflag, chmod))
|
||||
|
||||
try:
|
||||
fd = os.open(ap, iflag, chmod)
|
||||
except OSError as ex:
|
||||
t = "failed to os.open [%s] -> [%s] with iflag [%s] and chmod [%s]: %r"
|
||||
self.log(t % (vp, ap, iflag, chmod, ex), 3)
|
||||
return paramiko.SFTPServer.convert_errno(ex.errno)
|
||||
|
||||
if iflag & os.O_CREAT:
|
||||
paramiko.SFTPServer.set_file_attr(ap, attr)
|
||||
|
||||
try:
|
||||
f = os.fdopen(fd, smode)
|
||||
except OSError as ex:
|
||||
t = "failed to os.fdpen [%s] -> [%s] with smode [%s]: %r"
|
||||
self.log(t % (vp, ap, smode, ex), 3)
|
||||
return paramiko.SFTPServer.convert_errno(ex.errno)
|
||||
|
||||
ret = SFTP_FH(iflag)
|
||||
ret.filename = ap
|
||||
ret.readfile = f if rd else None
|
||||
ret.writefile = f if wr else None
|
||||
return ret
|
||||
|
||||
def remove(self, path: str) -> int:
|
||||
try:
|
||||
return self._remove(path)
|
||||
except:
|
||||
self.log("unhandled exception: %s" % (min_ex(),), 1)
|
||||
return SFTP_FAILURE
|
||||
|
||||
def _remove(self, vp: str) -> int:
|
||||
self.log("rm(%s)" % (vp,))
|
||||
if self.args.no_del:
|
||||
self.log("The delete feature is disabled in server config")
|
||||
return SFTP_PERMISSION_DENIED
|
||||
try:
|
||||
self.hub.up2k.handle_rm(self.uname, self.ip, [vp], [], False, False)
|
||||
self.log("rm(%s): ok" % (vp,))
|
||||
return SFTP_OK
|
||||
except Pebkac as ex:
|
||||
t = "denied delete [%s]: %s"
|
||||
self.log(t % (vp, ex))
|
||||
if str(ex).startswith("file not found"):
|
||||
return SFTP_NO_SUCH_FILE
|
||||
try:
|
||||
# write-only client trying to rm before upload?
|
||||
ap, vn, _ = self.v2a(vp)
|
||||
if (
|
||||
self.uname not in vn.axs.uread
|
||||
and self.uname not in vn.axs.uwrite
|
||||
and self.uname not in vn.axs.uget
|
||||
):
|
||||
self.log("rm(%s): EPERM" % (vp,))
|
||||
return SFTP_PERMISSION_DENIED
|
||||
if not bos.path.exists(ap):
|
||||
self.log(" `- file didn't exist; returning ENOENT")
|
||||
return SFTP_NO_SUCH_FILE
|
||||
except:
|
||||
pass
|
||||
return SFTP_PERMISSION_DENIED
|
||||
except OSError as ex:
|
||||
self.log("failed: rm(%s): %r" % (vp, ex))
|
||||
return paramiko.SFTPServer.convert_errno(ex.errno)
|
||||
|
||||
def rename(self, oldpath: str, newpath: str) -> int:
|
||||
try:
|
||||
return self._rename(oldpath, newpath)
|
||||
except:
|
||||
self.log("unhandled exception: %s" % (min_ex(),), 1)
|
||||
return SFTP_FAILURE
|
||||
|
||||
def _rename(self, svp: str, dvp: str) -> int:
|
||||
self.log("mv(%s, %s)" % (svp, dvp))
|
||||
if self.args.no_mv:
|
||||
self.log("The rename/move feature is disabled in server config")
|
||||
svp = svp.strip("/")
|
||||
dvp = dvp.strip("/")
|
||||
try:
|
||||
self.hub.up2k.handle_mv("", self.uname, self.ip, svp, dvp)
|
||||
return SFTP_OK
|
||||
except Pebkac as ex:
|
||||
t = "denied rename [%s] to [%s]: %s"
|
||||
self.log(t % (svp, dvp, ex))
|
||||
return SFTP_PERMISSION_DENIED
|
||||
except OSError as ex:
|
||||
self.log("mv(%s, %s): %r" % (svp, dvp, ex))
|
||||
return paramiko.SFTPServer.convert_errno(ex.errno)
|
||||
|
||||
def mkdir(self, path: str, attr: SATTR) -> int:
|
||||
try:
|
||||
return self._mkdir(path, attr)
|
||||
except:
|
||||
self.log("unhandled exception: %s" % (min_ex(),), 1)
|
||||
return SFTP_FAILURE
|
||||
|
||||
def _mkdir(self, vp: str, attr: SATTR) -> int:
|
||||
self.log("mkdir(%s)" % (vp,))
|
||||
try:
|
||||
vn, rem = self.asrv.vfs.get(vp, self.uname, False, True)
|
||||
ap = os.path.join(vn.realpath, rem)
|
||||
bos.makedirs(ap, vf=vn.flags) # filezilla expects this
|
||||
if attr is not None:
|
||||
paramiko.SFTPServer.set_file_attr(ap, attr)
|
||||
return SFTP_OK
|
||||
except Pebkac as ex:
|
||||
t = "denied mkdir [%s]: %s"
|
||||
self.log(t % (vp, ex))
|
||||
return SFTP_PERMISSION_DENIED
|
||||
except OSError as ex:
|
||||
self.log("mkdir(%s): %r" % (vp, ex))
|
||||
return paramiko.SFTPServer.convert_errno(ex.errno)
|
||||
|
||||
def rmdir(self, path: str) -> int:
|
||||
try:
|
||||
return self._rmdir(path)
|
||||
except:
|
||||
self.log("unhandled exception: %s" % (min_ex(),), 1)
|
||||
return SFTP_FAILURE
|
||||
|
||||
def _rmdir(self, vp: str) -> int:
|
||||
self.log("rmdir(%s)" % (vp,))
|
||||
try:
|
||||
vn, rem = self.asrv.vfs.get(vp, self.uname, False, False, will_del=True)
|
||||
ap = os.path.join(vn.realpath, rem)
|
||||
bos.rmdir(ap)
|
||||
return SFTP_OK
|
||||
except Pebkac as ex:
|
||||
t = "denied rmdir [%s]: %s"
|
||||
self.log(t % (vp, ex))
|
||||
return SFTP_PERMISSION_DENIED
|
||||
except OSError as ex:
|
||||
self.log("rmdir(%s): %r" % (vp, ex))
|
||||
return paramiko.SFTPServer.convert_errno(ex.errno)
|
||||
|
||||
def chattr(self, path: str, attr: SATTR) -> int:
|
||||
try:
|
||||
return self._chattr(path, attr)
|
||||
except:
|
||||
self.log("unhandled exception: %s" % (min_ex(),), 1)
|
||||
return SFTP_FAILURE
|
||||
|
||||
def _chattr(self, vp: str, attr: SATTR) -> int:
|
||||
self.log("chattr(%s, %s)" % (vp, attr))
|
||||
try:
|
||||
vn, rem = self.asrv.vfs.get(vp, self.uname, False, True, will_del=True)
|
||||
ap = os.path.join(vn.realpath, rem)
|
||||
paramiko.SFTPServer.set_file_attr(ap, attr)
|
||||
return SFTP_OK
|
||||
except Pebkac as ex:
|
||||
t = "denied chattr [%s]: %s"
|
||||
self.log(t % (vp, ex))
|
||||
return SFTP_PERMISSION_DENIED
|
||||
except OSError as ex:
|
||||
self.log("chattr(%s): %r" % (vp, ex))
|
||||
return paramiko.SFTPServer.convert_errno(ex.errno)
|
||||
|
||||
def symlink(self, target_path: str, path: str) -> int:
|
||||
return SFTP_OP_UNSUPPORTED
|
||||
|
||||
def readlink(self, path: str) -> str | int:
|
||||
return path
|
||||
|
||||
def canonicalize(self, path: str) -> str:
|
||||
return "/%s" % (undot(path),)
|
||||
|
||||
|
||||
class Sftpd(object):
|
||||
def __init__(self, hub: "SvcHub") -> None:
|
||||
self.hub = hub
|
||||
self.args = args = hub.args
|
||||
self.log_func = hub.log
|
||||
self.srv: list[socket.socket] = []
|
||||
self.bound: list[str] = []
|
||||
self.sessions = {}
|
||||
|
||||
ips = args.sftp_i
|
||||
if "::" in ips:
|
||||
ips.append("0.0.0.0")
|
||||
|
||||
ips = [x for x in ips if not x.startswith(("unix:", "fd:"))]
|
||||
|
||||
if args.sftp4:
|
||||
ips = [x for x in ips if ":" not in x]
|
||||
|
||||
if not ips:
|
||||
self.log("cannot start sftp-server; no compatible IPs in -i", 1)
|
||||
return
|
||||
|
||||
self.hostkeys = []
|
||||
hostkeytypes = (
|
||||
("ed25519", "Ed25519Key", {}), # best
|
||||
("ecdsa", "ECDSAKey", {"bits": 384}),
|
||||
("rsa", "RSAKey", {"bits": 4096}),
|
||||
("dsa", "DSSKey", {}), # worst
|
||||
)
|
||||
for fname, aname, opts in hostkeytypes:
|
||||
fpath = "%s/ssh_host_%s_key" % (args.sftp_hostk, fname.lower())
|
||||
try:
|
||||
pkey = getattr(paramiko, aname).from_private_key_file(fpath)
|
||||
except Exception as ex:
|
||||
try:
|
||||
genfun = getattr(paramiko, aname).generate
|
||||
except Exception as ex2:
|
||||
if args.sftpv or fname not in ("dsa", "ed25519"):
|
||||
# dsa dropped in 4.0
|
||||
# ed25519 not supported yet
|
||||
self.log("cannot generate %s hostkey: %r" % (aname, ex2), 3)
|
||||
continue
|
||||
self.log("generating hostkey [%s] due to %r" % (fpath, ex))
|
||||
pkey = genfun(**opts)
|
||||
pkey.write_private_key_file(fpath)
|
||||
pkey = getattr(paramiko, aname).from_private_key_file(fpath)
|
||||
self.hostkeys.append(pkey)
|
||||
if args.sftpv:
|
||||
self.log("loaded hostkey %r" % (pkey,))
|
||||
|
||||
ips = list(ODict.fromkeys(ips)) # dedup
|
||||
|
||||
for ip in ips:
|
||||
self._bind(ip)
|
||||
|
||||
self.log("listening @ %s port %s" % (self.bound, args.sftp))
|
||||
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.hub.log("sftp", msg, c)
|
||||
|
||||
def _bind(self, ip: str) -> None:
|
||||
port = self.args.sftp
|
||||
try:
|
||||
ipv = socket.AF_INET6 if ":" in ip else socket.AF_INET
|
||||
srv = socket.socket(ipv, socket.SOCK_STREAM)
|
||||
if not ANYWIN or self.args.reuseaddr:
|
||||
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
srv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
srv.settimeout(0) # == srv.setblocking(False)
|
||||
try:
|
||||
srv.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False)
|
||||
except:
|
||||
pass # will create another ipv4 socket instead
|
||||
if getattr(self.args, "freebind", False):
|
||||
srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1)
|
||||
srv.bind((ip, port))
|
||||
srv.listen(10)
|
||||
self.srv.append(srv)
|
||||
self.bound.append(ip)
|
||||
except Exception as ex:
|
||||
if ip == "0.0.0.0" and "::" in self.bound:
|
||||
try:
|
||||
srv.close() # type: ignore
|
||||
except:
|
||||
pass
|
||||
return # dualstack
|
||||
self.log("could not listen on (%s,%s): %r" % (ip, port, ex), 3)
|
||||
|
||||
def _accept(self, srv: socket.socket) -> None:
|
||||
cli, addr = srv.accept()
|
||||
# cli.settimeout(0) # == srv.setblocking(False)
|
||||
self.log("%r is connecting" % (addr,))
|
||||
zs = "sftp-%s" % (addr[0],)
|
||||
# Daemon(self._accept2, zs, (cli, addr))
|
||||
self._accept2(cli, addr)
|
||||
|
||||
def _accept2(self, cli, addr) -> None:
|
||||
tra = paramiko.Transport(cli)
|
||||
for hkey in self.hostkeys:
|
||||
tra.add_server_key(hkey)
|
||||
tra.set_subsystem_handler("sftp", paramiko.SFTPServer, SFTP_Srv)
|
||||
psrv = SSH_Srv(self.hub, addr)
|
||||
try:
|
||||
tra.start_server(server=psrv)
|
||||
except Exception as ex:
|
||||
self.log("%r could not establish connection: %r" % (addr, ex), 3)
|
||||
cli.close()
|
||||
return
|
||||
|
||||
chan = tra.accept()
|
||||
if chan is None:
|
||||
self.log("%r did not open an sftp channel" % (addr,), 3)
|
||||
cli.close()
|
||||
return
|
||||
|
||||
self.sessions[addr] = (chan, tra, psrv)
|
||||
# tra.join()
|
||||
# self.log("%r disconnected" % (addr,))
|
||||
|
||||
def run(self):
|
||||
lgr = logging.getLogger("paramiko.transport")
|
||||
lgr.setLevel(logging.DEBUG if self.args.sftpvv else logging.INFO)
|
||||
|
||||
if self.args.no_poll:
|
||||
fun = self._run_select
|
||||
else:
|
||||
fun = self._run_poll
|
||||
Daemon(fun, "sftpd")
|
||||
|
||||
def _run_select(self):
|
||||
while not self.hub.stopping:
|
||||
rx, _, _ = select.select(self.srv, [], [], 180)
|
||||
for sck in rx:
|
||||
self._accept(sck)
|
||||
|
||||
def _run_poll(self):
|
||||
fd2sck = {}
|
||||
poll = select.poll()
|
||||
for sck in self.srv:
|
||||
fd = sck.fileno()
|
||||
fd2sck[fd] = sck
|
||||
poll.register(fd, select.POLLIN)
|
||||
while not self.hub.stopping:
|
||||
pr = poll.poll(180 * 1000)
|
||||
rx = [fd2sck[x[0]] for x in pr if x[1] & select.POLLIN]
|
||||
for sck in rx:
|
||||
self._accept(sck)
|
||||
+28
-23
@@ -89,7 +89,7 @@ class SMB(object):
|
||||
smbserver.isInFileJail = self._is_in_file_jail
|
||||
self._disarm()
|
||||
|
||||
ip = next((x for x in self.args.i if ":" not in x), None)
|
||||
ip = next((x for x in self.args.smb_i if ":" not in x), None)
|
||||
if not ip:
|
||||
self.log("smb", "IPv6 not supported for SMB; listening on 0.0.0.0", 3)
|
||||
ip = "0.0.0.0"
|
||||
@@ -246,24 +246,29 @@ class SMB(object):
|
||||
|
||||
ap = absreal(ap)
|
||||
xbu = vfs.flags.get("xbu")
|
||||
if xbu and not runhook(
|
||||
self.nlog,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.smb",
|
||||
xbu,
|
||||
ap,
|
||||
vpath,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
"1.7.6.2",
|
||||
time.time(),
|
||||
"",
|
||||
):
|
||||
yeet("blocked by xbu server config: %r" % (vpath,))
|
||||
if xbu:
|
||||
hr = runhook(
|
||||
self.nlog,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.smb",
|
||||
xbu,
|
||||
ap,
|
||||
vpath,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
"1.7.6.2",
|
||||
time.time(),
|
||||
None,
|
||||
)
|
||||
t = hr.get("rejectmsg") or ""
|
||||
if t or hr.get("rc") != 0:
|
||||
if not t:
|
||||
t = "blocked by xbu server config: %r" % (vpath,)
|
||||
yeet(t)
|
||||
|
||||
ret = bos.open(ap, flags, *a, mode=chmod, **ka)
|
||||
if wr:
|
||||
@@ -318,9 +323,9 @@ class SMB(object):
|
||||
t = "blocked rename (no-move-acc %s): /%s @%s"
|
||||
yeet(t % (vfs1.axs.umove, vp1, uname))
|
||||
|
||||
self.hub.up2k.handle_mv(uname, "1.7.6.2", vp1, vp2)
|
||||
self.hub.up2k.handle_mv("", uname, "1.7.6.2", vp1, vp2)
|
||||
try:
|
||||
bos.makedirs(ap2)
|
||||
bos.makedirs(ap2, vf=vfs2.flags)
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -334,7 +339,7 @@ class SMB(object):
|
||||
t = "blocked mkdir (no-write-acc %s): /%s @%s"
|
||||
yeet(t % (vfs.axs.uwrite, vpath, uname))
|
||||
|
||||
return bos.mkdir(ap)
|
||||
return bos.mkdir(ap, vfs.flags["chmod_d"])
|
||||
|
||||
def _stat(self, vpath: str, *a: Any, **ka: Any) -> os.stat_result:
|
||||
try:
|
||||
@@ -373,7 +378,7 @@ class SMB(object):
|
||||
t = "blocked utime (no-write-acc %s): /%s @%s"
|
||||
yeet(t % (vfs.axs.uwrite, vpath, uname))
|
||||
|
||||
return bos.utime(ap, times)
|
||||
bos.utime_c(info, ap, int(times[1]), False)
|
||||
|
||||
def _p_exists(self, vpath: str) -> bool:
|
||||
# ap = "?"
|
||||
|
||||
@@ -8,6 +8,7 @@ https://github.com/pydron/ifaddr/tree/0.2.0
|
||||
"""
|
||||
|
||||
import os
|
||||
import platform
|
||||
|
||||
from ._shared import IP, Adapter
|
||||
|
||||
@@ -16,20 +17,28 @@ def nope(include_unconfigured=False):
|
||||
return []
|
||||
|
||||
|
||||
try:
|
||||
S390X = os.uname().machine == "s390x"
|
||||
except:
|
||||
S390X = False
|
||||
host_os = platform.system()
|
||||
machine = platform.machine()
|
||||
py_impl = platform.python_implementation()
|
||||
|
||||
|
||||
if os.environ.get("PRTY_NO_IFADDR") or S390X:
|
||||
if os.environ.get("PRTY_NO_IFADDR"):
|
||||
get_adapters = nope
|
||||
elif machine in ("s390x",) or host_os in ("IRIX32",):
|
||||
# s390x deadlocks at libc.getifaddrs
|
||||
# irix libc does not have getifaddrs at all
|
||||
print("ifaddr unavailable; can't determine LAN IP: unsupported OS")
|
||||
get_adapters = nope
|
||||
elif py_impl in ("GraalVM",):
|
||||
print("ifaddr unavailable; can't determine LAN IP: unsupported interpreter")
|
||||
get_adapters = nope
|
||||
elif os.name == "nt":
|
||||
from ._win32 import get_adapters
|
||||
elif os.name == "posix":
|
||||
from ._posix import get_adapters
|
||||
else:
|
||||
raise RuntimeError("Unsupported Operating System: %s" % os.name)
|
||||
print("ifaddr unavailable; can't determine LAN IP: unsupported OS")
|
||||
get_adapters = nope
|
||||
|
||||
|
||||
__all__ = ["Adapter", "IP", "get_adapters"]
|
||||
@@ -4,7 +4,7 @@
|
||||
# https://github.com/nayuki/QR-Code-generator/blob/daa3114/python/qrcodegen.py
|
||||
# the original ^ is extremely well commented so refer to that for explanations
|
||||
|
||||
# hacks: binary-only, auto-ecc, render, py2-compat
|
||||
# hacks: binary-only, auto-ecc, py2-compat
|
||||
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
@@ -173,33 +173,6 @@ class QrCode(object):
|
||||
self._apply_mask(msk) # Apply the final choice of mask
|
||||
self._draw_format_bits(msk) # Overwrite old format bits
|
||||
|
||||
def render(self, zoom=1, pad=4) -> str:
|
||||
tab = self.modules
|
||||
sz = self.size
|
||||
if sz % 2 and zoom == 1:
|
||||
tab.append([False] * sz)
|
||||
|
||||
tab = [[False] * sz] * pad + tab + [[False] * sz] * pad
|
||||
tab = [[False] * pad + x + [False] * pad for x in tab]
|
||||
|
||||
rows: list[str] = []
|
||||
if zoom == 1:
|
||||
for y in range(0, len(tab), 2):
|
||||
row = ""
|
||||
for x in range(len(tab[y])):
|
||||
v = 2 if tab[y][x] else 0
|
||||
v += 1 if tab[y + 1][x] else 0
|
||||
row += " ▄▀█"[v]
|
||||
rows.append(row)
|
||||
else:
|
||||
for tr in tab:
|
||||
row = ""
|
||||
for zb in tr:
|
||||
row += " █"[int(zb)] * 2
|
||||
rows.append(row)
|
||||
|
||||
return "\n".join(rows)
|
||||
|
||||
def _draw_function_patterns(self) -> None:
|
||||
# Draw horizontal and vertical timing patterns
|
||||
for i in range(self.size):
|
||||
@@ -594,20 +567,3 @@ def _get_bit(x: int, i: int) -> bool:
|
||||
|
||||
class DataTooLongError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def qr2svg(qr: QrCode, border: int) -> str:
|
||||
parts: list[str] = []
|
||||
for y in range(qr.size):
|
||||
sy = border + y
|
||||
for x in range(qr.size):
|
||||
if qr.modules[y][x]:
|
||||
parts.append("M%d,%dh1v1h-1z" % (border + x, sy))
|
||||
t = """\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 {0} {0}" stroke="none">
|
||||
<rect width="100%" height="100%" fill="#F7F7F7"/>
|
||||
<path d="{1}" fill="#111111"/>
|
||||
</svg>
|
||||
"""
|
||||
return t.format(qr.size + border * 2, " ".join(parts))
|
||||
+6
-4
@@ -9,6 +9,7 @@ from .__init__ import CORES
|
||||
from .authsrv import VFS, AuthSrv
|
||||
from .bos import bos
|
||||
from .th_cli import ThumbCli
|
||||
from .th_srv import TH_CH
|
||||
from .util import UTC, vjoin, vol_san
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
@@ -17,6 +18,9 @@ if True: # pylint: disable=using-constant-test
|
||||
from .util import NamedLogger
|
||||
|
||||
|
||||
TAR_NO_OPUS = set("aac|m4a|mp3|oga|ogg|opus|wma".split("|"))
|
||||
|
||||
|
||||
class StreamArc(object):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -82,9 +86,7 @@ def enthumb(
|
||||
) -> dict[str, Any]:
|
||||
rem = f["vp"]
|
||||
ext = rem.rsplit(".", 1)[-1].lower()
|
||||
if (fmt == "mp3" and ext == "mp3") or (
|
||||
fmt == "opus" and ext in "aac|m4a|mp3|ogg|opus|wma".split("|")
|
||||
):
|
||||
if (fmt == "mp3" and ext == "mp3") or (fmt == "opus" and ext in TAR_NO_OPUS):
|
||||
raise Exception()
|
||||
|
||||
vp = vjoin(vtop, rem.split("/", 1)[1])
|
||||
@@ -94,7 +96,7 @@ def enthumb(
|
||||
if not thp:
|
||||
raise Exception()
|
||||
|
||||
ext = "jpg" if fmt == "j" else "webp" if fmt == "w" else fmt
|
||||
ext = fmt if fmt == "wav" else TH_CH.get(fmt[:1], fmt)
|
||||
sz = bos.path.getsize(thp)
|
||||
st: os.stat_result = f["st"]
|
||||
ts = st.st_mtime
|
||||
|
||||
+573
-128
File diff suppressed because it is too large.
Load diff
+12
-9
@@ -4,12 +4,11 @@ from __future__ import print_function, unicode_literals
|
||||
import calendar
|
||||
import stat
|
||||
import time
|
||||
import zlib
|
||||
|
||||
from .authsrv import AuthSrv
|
||||
from .bos import bos
|
||||
from .sutil import StreamArc, errdesc
|
||||
from .util import min_ex, sanitize_fn, spack, sunpack, yieldfile
|
||||
from .util import VPTL_WIN, min_ex, sanitize_to, spack, sunpack, yieldfile, zlib
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Generator, Optional
|
||||
@@ -55,6 +54,7 @@ def gen_fdesc(sz: int, crc32: int, z64: bool) -> bytes:
|
||||
|
||||
def gen_hdr(
|
||||
h_pos: Optional[int],
|
||||
z64: bool,
|
||||
fn: str,
|
||||
sz: int,
|
||||
lastmod: int,
|
||||
@@ -71,7 +71,6 @@ def gen_hdr(
|
||||
# appnote 4.5 / zip 3.0 (2008) / unzip 6.0 (2009) says to add z64
|
||||
# extinfo for values which exceed H, but that becomes an off-by-one
|
||||
# (can't tell if it was clamped or exactly maxval), make it obvious
|
||||
z64 = sz >= 0xFFFFFFFF
|
||||
z64v = [sz, sz] if z64 else []
|
||||
if h_pos and h_pos >= 0xFFFFFFFF:
|
||||
# central, also consider ptr to original header
|
||||
@@ -105,7 +104,7 @@ def gen_hdr(
|
||||
ret += spack(b"<LL", vsz, vsz)
|
||||
|
||||
# windows support (the "?" replace below too)
|
||||
fn = sanitize_fn(fn, "/")
|
||||
fn = sanitize_to(fn, VPTL_WIN)
|
||||
bfn = fn.encode("utf-8" if utf8 else "cp437", "replace").replace(b"?", b"_")
|
||||
|
||||
# add ntfs (0x24) and/or unix (0x10) extrafields for utc, add z64 if requested
|
||||
@@ -245,6 +244,7 @@ class StreamZip(StreamArc):
|
||||
|
||||
sz = st.st_size
|
||||
ts = st.st_mtime
|
||||
h_pos = self.pos
|
||||
|
||||
crc = 0
|
||||
if self.pre_crc:
|
||||
@@ -253,8 +253,12 @@ class StreamZip(StreamArc):
|
||||
|
||||
crc &= 0xFFFFFFFF
|
||||
|
||||
h_pos = self.pos
|
||||
buf = gen_hdr(None, name, sz, ts, self.utf8, crc, self.pre_crc)
|
||||
# some unzip-programs expect a 64bit data-descriptor
|
||||
# even if the only 32bit-exceeding value is the offset,
|
||||
# so force that by placeholdering the filesize too
|
||||
z64 = h_pos >= 0xFFFFFFFF or sz >= 0xFFFFFFFF
|
||||
|
||||
buf = gen_hdr(None, z64, name, sz, ts, self.utf8, crc, self.pre_crc)
|
||||
yield self._ct(buf)
|
||||
|
||||
for buf in yieldfile(src, self.args.iobuf):
|
||||
@@ -267,8 +271,6 @@ class StreamZip(StreamArc):
|
||||
|
||||
self.items.append((name, sz, ts, crc, h_pos))
|
||||
|
||||
z64 = sz >= 4 * 1024 * 1024 * 1024
|
||||
|
||||
if z64 or not self.pre_crc:
|
||||
buf = gen_fdesc(sz, crc, z64)
|
||||
yield self._ct(buf)
|
||||
@@ -307,7 +309,8 @@ class StreamZip(StreamArc):
|
||||
|
||||
cdir_pos = self.pos
|
||||
for name, sz, ts, crc, h_pos in self.items:
|
||||
buf = gen_hdr(h_pos, name, sz, ts, self.utf8, crc, self.pre_crc)
|
||||
z64 = h_pos >= 0xFFFFFFFF or sz >= 0xFFFFFFFF
|
||||
buf = gen_hdr(h_pos, z64, name, sz, ts, self.utf8, crc, self.pre_crc)
|
||||
mbuf += self._ct(buf)
|
||||
if len(mbuf) >= 16384:
|
||||
yield mbuf
|
||||
|
||||
+94
-18
@@ -9,24 +9,26 @@ import time
|
||||
|
||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
|
||||
from .cert import gencert
|
||||
from .stolen.qrcodegen import QrCode
|
||||
from .qrkode import QrCode, qr2png, qr2svg, qr2txt, qrgen
|
||||
from .util import (
|
||||
E_ACCESS,
|
||||
E_ADDR_IN_USE,
|
||||
E_ADDR_NOT_AVAIL,
|
||||
E_UNREACH,
|
||||
HAVE_IPV6,
|
||||
IP6_LL,
|
||||
IP6ALL,
|
||||
VF_CAREFUL,
|
||||
Netdev,
|
||||
atomic_move,
|
||||
get_adapters,
|
||||
min_ex,
|
||||
sunpack,
|
||||
termsize,
|
||||
)
|
||||
|
||||
if True:
|
||||
from typing import Generator, Union
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Generator, Optional, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .svchub import SvcHub
|
||||
@@ -58,6 +60,7 @@ class TcpSrv(object):
|
||||
self.stopping = False
|
||||
self.srv: list[socket.socket] = []
|
||||
self.bound: list[tuple[str, int]] = []
|
||||
self.seen_eps: list[tuple[str, int]] = [] # also skipped by uds-only
|
||||
self.netdevs: dict[str, Netdev] = {}
|
||||
self.netlist = ""
|
||||
self.nsrv = 0
|
||||
@@ -140,25 +143,31 @@ class TcpSrv(object):
|
||||
# keep IPv6 LL-only nics
|
||||
ll_ok: set[str] = set()
|
||||
for ip, nd in self.netdevs.items():
|
||||
if not ip.startswith("fe80"):
|
||||
if not ip.startswith(IP6_LL):
|
||||
continue
|
||||
|
||||
just_ll = True
|
||||
for ip2, nd2 in self.netdevs.items():
|
||||
if nd == nd2 and ":" in ip2 and not ip2.startswith("fe80"):
|
||||
if nd == nd2 and ":" in ip2 and not ip2.startswith(IP6_LL):
|
||||
just_ll = False
|
||||
|
||||
if just_ll or self.args.ll:
|
||||
ll_ok.add(ip.split("/")[0])
|
||||
|
||||
listening_on = []
|
||||
for ip, ports in sorted(ok.items()):
|
||||
for port in sorted(ports):
|
||||
listening_on.append("%s %s" % (ip, port))
|
||||
|
||||
qr1: dict[str, list[int]] = {}
|
||||
qr2: dict[str, list[int]] = {}
|
||||
msgs = []
|
||||
accessible_on = []
|
||||
title_tab: dict[str, dict[str, int]] = {}
|
||||
title_vars = [x[1:] for x in self.args.wintitle.split(" ") if x.startswith("$")]
|
||||
t = "available @ {}://{}:{}/ (\033[33m{}\033[0m)"
|
||||
for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
|
||||
if ip.startswith("fe80") and ip not in ll_ok:
|
||||
if ip.startswith(IP6_LL) and ip not in ll_ok:
|
||||
continue
|
||||
|
||||
for port in sorted(self.args.p):
|
||||
@@ -169,6 +178,10 @@ class TcpSrv(object):
|
||||
):
|
||||
continue
|
||||
|
||||
zs = "%s %s" % (ip, port)
|
||||
if zs not in accessible_on:
|
||||
accessible_on.append(zs)
|
||||
|
||||
proto = " http"
|
||||
if self.args.http_only:
|
||||
pass
|
||||
@@ -219,6 +232,14 @@ class TcpSrv(object):
|
||||
else:
|
||||
print("\n", end="")
|
||||
|
||||
for fn, ls in (
|
||||
(self.args.wr_h_eps, listening_on),
|
||||
(self.args.wr_h_aon, accessible_on),
|
||||
):
|
||||
if fn:
|
||||
with open(fn, "wb") as f:
|
||||
f.write(("\n".join(ls)).encode("utf-8"))
|
||||
|
||||
if self.args.qr or self.args.qrs:
|
||||
self.qr = self._qr(qr1, qr2)
|
||||
|
||||
@@ -227,8 +248,10 @@ class TcpSrv(object):
|
||||
|
||||
def _listen(self, ip: str, port: int) -> None:
|
||||
uds_perm = uds_gid = -1
|
||||
bound: Optional[socket.socket] = None
|
||||
tcp = False
|
||||
|
||||
if "unix:" in ip:
|
||||
tcp = False
|
||||
ipv = socket.AF_UNIX
|
||||
uds = ip.split(":")
|
||||
ip = uds[-1]
|
||||
@@ -241,7 +264,12 @@ class TcpSrv(object):
|
||||
import grp
|
||||
|
||||
uds_gid = grp.getgrnam(uds[2]).gr_gid
|
||||
elif "fd:" in ip:
|
||||
fd = ip[3:]
|
||||
bound = socket.socket(fileno=int(fd))
|
||||
|
||||
tcp = bound.proto == socket.IPPROTO_TCP
|
||||
ipv = bound.family
|
||||
elif ":" in ip:
|
||||
tcp = True
|
||||
ipv = socket.AF_INET6
|
||||
@@ -249,7 +277,7 @@ class TcpSrv(object):
|
||||
tcp = True
|
||||
ipv = socket.AF_INET
|
||||
|
||||
srv = socket.socket(ipv, socket.SOCK_STREAM)
|
||||
srv = bound or socket.socket(ipv, socket.SOCK_STREAM)
|
||||
|
||||
if not ANYWIN or self.args.reuseaddr:
|
||||
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
@@ -264,17 +292,28 @@ class TcpSrv(object):
|
||||
except:
|
||||
pass # will create another ipv4 socket instead
|
||||
|
||||
if not ANYWIN and self.args.freebind:
|
||||
if getattr(self.args, "freebind", False):
|
||||
srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1)
|
||||
|
||||
if bound:
|
||||
self.srv.append(srv)
|
||||
return
|
||||
|
||||
try:
|
||||
if tcp:
|
||||
if self.args.http_no_tcp:
|
||||
self.seen_eps.append((ip, port))
|
||||
return
|
||||
srv.bind((ip, port))
|
||||
else:
|
||||
if ANYWIN or self.args.rm_sck:
|
||||
if os.path.exists(ip):
|
||||
os.unlink(ip)
|
||||
srv.bind(ip)
|
||||
if uds_gid != -1:
|
||||
os.chown(ip, -1, uds_gid)
|
||||
if uds_perm != -1:
|
||||
os.chmod(ip, uds_perm)
|
||||
else:
|
||||
tf = "%s.%d" % (ip, os.getpid())
|
||||
if os.path.exists(tf):
|
||||
@@ -375,6 +414,7 @@ class TcpSrv(object):
|
||||
|
||||
self.srv = srvs
|
||||
self.bound = bound
|
||||
self.seen_eps = list(set(self.seen_eps + bound))
|
||||
self.nsrv = len(srvs)
|
||||
self._distribute_netdevs()
|
||||
|
||||
@@ -382,6 +422,7 @@ class TcpSrv(object):
|
||||
self.hub.broker.say("httpsrv.set_netdevs", self.netdevs)
|
||||
self.hub.start_zeroconf()
|
||||
gencert(self.log, self.args, self.netdevs)
|
||||
self.hub.restart_sftpd()
|
||||
self.hub.restart_ftpd()
|
||||
self.hub.restart_tftpd()
|
||||
|
||||
@@ -417,9 +458,7 @@ class TcpSrv(object):
|
||||
self._distribute_netdevs()
|
||||
|
||||
def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]:
|
||||
from .stolen.ifaddr import get_adapters
|
||||
|
||||
listen_ips = [x for x in listen_ips if "unix:" not in x]
|
||||
listen_ips = [x for x in listen_ips if not x.startswith(("unix:", "fd:"))]
|
||||
|
||||
nics = get_adapters(True)
|
||||
eps: dict[str, Netdev] = {}
|
||||
@@ -548,7 +587,7 @@ class TcpSrv(object):
|
||||
ip = None
|
||||
ips = list(t1) + list(t2)
|
||||
qri = self.args.qri
|
||||
if self.args.zm and not qri:
|
||||
if self.args.zm and not qri and ips:
|
||||
name = self.args.name + ".local"
|
||||
t1[name] = next(v for v in (t1 or t2).values())
|
||||
ips = [name] + ips
|
||||
@@ -565,8 +604,7 @@ class TcpSrv(object):
|
||||
if not ip:
|
||||
return ""
|
||||
|
||||
if ":" in ip:
|
||||
ip = "[{}]".format(ip)
|
||||
hip = "[%s]" % (ip,) if ":" in ip else ip
|
||||
|
||||
if self.args.http_only:
|
||||
https = ""
|
||||
@@ -578,7 +616,7 @@ class TcpSrv(object):
|
||||
ports = t1.get(ip, t2.get(ip, []))
|
||||
dport = 443 if https else 80
|
||||
port = "" if dport in ports or not ports else ":{}".format(ports[0])
|
||||
txt = "http{}://{}{}/{}".format(https, ip, port, self.args.qrl)
|
||||
txt = "http{}://{}{}/{}".format(https, hip, port, self.args.qrl)
|
||||
|
||||
btxt = txt.encode("utf-8")
|
||||
if PY2:
|
||||
@@ -586,9 +624,17 @@ class TcpSrv(object):
|
||||
|
||||
fg = self.args.qr_fg
|
||||
bg = self.args.qr_bg
|
||||
nocolor = fg == -1
|
||||
if nocolor:
|
||||
fg = 0
|
||||
|
||||
pad = self.args.qrp
|
||||
zoom = self.args.qrz
|
||||
qrc = QrCode.encode_binary(btxt)
|
||||
qrc = qrgen(btxt)
|
||||
|
||||
for zs in self.args.qr_file or []:
|
||||
self._qr2file(qrc, zs)
|
||||
|
||||
if zoom == 0:
|
||||
try:
|
||||
tw, th = termsize()
|
||||
@@ -597,13 +643,15 @@ class TcpSrv(object):
|
||||
except:
|
||||
zoom = 1
|
||||
|
||||
qr = qrc.render(zoom, pad)
|
||||
qr = qr2txt(qrc, zoom, pad)
|
||||
if self.args.no_ansi:
|
||||
return "{}\n{}".format(txt, qr)
|
||||
|
||||
halfc = "\033[40;48;5;{0}m{1}\033[47;48;5;{2}m"
|
||||
if not fg:
|
||||
halfc = "\033[0;40m{1}\033[0;47m"
|
||||
if nocolor:
|
||||
halfc = "\033[0;7m{1}\033[0m"
|
||||
|
||||
def ansify(m: re.Match) -> str:
|
||||
return halfc.format(fg, " " * len(m.group(1)), bg)
|
||||
@@ -613,6 +661,8 @@ class TcpSrv(object):
|
||||
|
||||
qr = qr.replace("\n", "\033[K\n") + "\033[K" # win10do
|
||||
cc = " \033[0;38;5;{0};47;48;5;{1}m" if fg else " \033[0;30;47m"
|
||||
if nocolor:
|
||||
cc = " \033[0m"
|
||||
t = cc + "\n{2}\033[999G\033[0m\033[J"
|
||||
t = t.format(fg, bg, qr)
|
||||
if ANYWIN:
|
||||
@@ -620,3 +670,29 @@ class TcpSrv(object):
|
||||
t = t.replace("\n", "`\n`")
|
||||
|
||||
return txt + t
|
||||
|
||||
def _qr2file(self, qrc: QrCode, txt: str):
|
||||
if ".txt:" in txt or ".svg:" in txt:
|
||||
ap, zs1, zs2 = txt.rsplit(":", 2)
|
||||
bg = fg = ""
|
||||
else:
|
||||
ap, zs1, zs2, bg, fg = txt.rsplit(":", 4)
|
||||
zoom = int(zs1)
|
||||
pad = int(zs2)
|
||||
|
||||
if ap.endswith(".txt"):
|
||||
if zoom not in (1, 2):
|
||||
raise Exception("invalid zoom for qr.txt; must be 1 or 2")
|
||||
with open(ap, "wb") as f:
|
||||
f.write(qr2txt(qrc, zoom, pad).encode("utf-8"))
|
||||
elif ap.endswith(".svg"):
|
||||
with open(ap, "wb") as f:
|
||||
f.write(qr2svg(qrc, pad).encode("utf-8"))
|
||||
else:
|
||||
qr2png(qrc, zoom, pad, self._h2i(bg), self._h2i(fg), ap)
|
||||
|
||||
def _h2i(self, hs):
|
||||
try:
|
||||
return tuple(int(hs[i : i + 2], 16) for i in (0, 2, 4))
|
||||
except:
|
||||
return None
|
||||
+64
-31
@@ -36,7 +36,19 @@ from partftpy.TftpShared import TftpException
|
||||
from .__init__ import EXE, PY2, TYPE_CHECKING
|
||||
from .authsrv import VFS
|
||||
from .bos import bos
|
||||
from .util import UTC, BytesIO, Daemon, ODict, exclude_dotfiles, min_ex, runhook, undot
|
||||
from .util import (
|
||||
UTC,
|
||||
BytesIO,
|
||||
Daemon,
|
||||
ODict,
|
||||
exclude_dotfiles,
|
||||
min_ex,
|
||||
runhook,
|
||||
set_fperms,
|
||||
undot,
|
||||
vjoin,
|
||||
vsplit,
|
||||
)
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Union
|
||||
@@ -162,11 +174,11 @@ class Tftpd(object):
|
||||
p1, p2 = [int(x) for x in self.args.tftp_pr.split("-")]
|
||||
ports = list(range(p1, p2 + 1))
|
||||
|
||||
ips = self.args.i
|
||||
ips = self.args.tftp_i
|
||||
if "::" in ips:
|
||||
ips.append("0.0.0.0")
|
||||
|
||||
ips = [x for x in ips if "unix:" not in x]
|
||||
ips = [x for x in ips if not x.startswith(("unix:", "fd:"))]
|
||||
|
||||
if self.args.tftp4:
|
||||
ips = [x for x in ips if ":" not in x]
|
||||
@@ -244,16 +256,25 @@ class Tftpd(object):
|
||||
for srv in srvs:
|
||||
srv.stop()
|
||||
|
||||
def _v2a(self, caller: str, vpath: str, perms: list, *a: Any) -> tuple[VFS, str]:
|
||||
def _v2a(
|
||||
self, caller: str, vpath: str, perms: list, *a: Any
|
||||
) -> tuple[VFS, str, str]:
|
||||
vpath = vpath.replace("\\", "/").lstrip("/")
|
||||
if not perms:
|
||||
perms = [True, True]
|
||||
|
||||
debug('%s("%s", %s) %s\033[K\033[0m', caller, vpath, str(a), perms)
|
||||
vfs, rem = self.asrv.vfs.get(vpath, "*", *perms)
|
||||
if perms[1] and "*" not in vfs.axs.uread and "wo_up_readme" not in vfs.flags:
|
||||
zs, fn = vsplit(vpath)
|
||||
if fn.lower() in vfs.flags["emb_all"]:
|
||||
vpath = vjoin(zs, "_wo_" + fn)
|
||||
vfs, rem = self.asrv.vfs.get(vpath, "*", *perms)
|
||||
|
||||
if not vfs.realpath:
|
||||
raise Exception("unmapped vfs")
|
||||
return vfs, vfs.canonical(rem)
|
||||
|
||||
return vfs, vpath, vfs.canonical(rem)
|
||||
|
||||
def _ls(self, vpath: str, raddress: str, rport: int, force=False) -> Any:
|
||||
# generate file listing if vpath is dir.txt and return as file object
|
||||
@@ -263,6 +284,7 @@ class Tftpd(object):
|
||||
if not ptn or not ptn.match(fn.lower()):
|
||||
return None
|
||||
|
||||
tsdt = datetime.fromtimestamp
|
||||
vn, rem = self.asrv.vfs.get(vpath, "*", True, False)
|
||||
fsroot, vfs_ls, vfs_virt = vn.ls(
|
||||
rem,
|
||||
@@ -275,7 +297,7 @@ class Tftpd(object):
|
||||
dirs1 = [(v.st_mtime, v.st_size, k + "/") for k, v in vfs_ls if k in dnames]
|
||||
fils1 = [(v.st_mtime, v.st_size, k) for k, v in vfs_ls if k not in dnames]
|
||||
real1 = dirs1 + fils1
|
||||
realt = [(datetime.fromtimestamp(mt, UTC), sz, fn) for mt, sz, fn in real1]
|
||||
realt = [(tsdt(max(0, mt), UTC), sz, fn) for mt, sz, fn in real1]
|
||||
reals = [
|
||||
(
|
||||
"%04d-%02d-%02d %02d:%02d:%02d"
|
||||
@@ -331,7 +353,7 @@ class Tftpd(object):
|
||||
else:
|
||||
raise Exception("bad mode %s" % (mode,))
|
||||
|
||||
vfs, ap = self._v2a("open", vpath, [rd, wr])
|
||||
vfs, vpath, ap = self._v2a("open", vpath, [rd, wr])
|
||||
if wr:
|
||||
if "*" not in vfs.axs.uwrite:
|
||||
yeet("blocked write; folder not world-writable: /%s" % (vpath,))
|
||||
@@ -340,24 +362,29 @@ class Tftpd(object):
|
||||
yeet("blocked write; folder not world-deletable: /%s" % (vpath,))
|
||||
|
||||
xbu = vfs.flags.get("xbu")
|
||||
if xbu and not runhook(
|
||||
self.nlog,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.tftpd",
|
||||
xbu,
|
||||
ap,
|
||||
vpath,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
"8.3.8.7",
|
||||
time.time(),
|
||||
"",
|
||||
):
|
||||
yeet("blocked by xbu server config: %r" % (vpath,))
|
||||
if xbu:
|
||||
hr = runhook(
|
||||
self.nlog,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.tftpd",
|
||||
xbu,
|
||||
ap,
|
||||
vpath,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
"8.3.8.7",
|
||||
time.time(),
|
||||
None,
|
||||
)
|
||||
t = hr.get("rejectmsg") or ""
|
||||
if t or hr.get("rc") != 0:
|
||||
if not t:
|
||||
t = "upload blocked by xbu server config: %r" % (vpath,)
|
||||
yeet(t)
|
||||
|
||||
if not self.args.tftp_nols and bos.path.isdir(ap):
|
||||
return self._ls(vpath, "", 0, True)
|
||||
@@ -365,18 +392,24 @@ class Tftpd(object):
|
||||
if not a:
|
||||
a = (self.args.iobuf,)
|
||||
|
||||
return open(ap, mode, *a, **ka)
|
||||
ret = open(ap, mode, *a, **ka)
|
||||
if wr and "fperms" in vfs.flags:
|
||||
set_fperms(ret, vfs.flags)
|
||||
|
||||
return ret
|
||||
|
||||
def _mkdir(self, vpath: str, *a) -> None:
|
||||
vfs, ap = self._v2a("mkdir", vpath, [])
|
||||
vfs, _, ap = self._v2a("mkdir", vpath, [False, True])
|
||||
if "*" not in vfs.axs.uwrite:
|
||||
yeet("blocked mkdir; folder not world-writable: /%s" % (vpath,))
|
||||
|
||||
return bos.mkdir(ap)
|
||||
bos.mkdir(ap, vfs.flags["chmod_d"])
|
||||
if "chown" in vfs.flags:
|
||||
bos.chown(ap, vfs.flags["uid"], vfs.flags["gid"])
|
||||
|
||||
def _unlink(self, vpath: str) -> None:
|
||||
# return bos.unlink(self._v2a("stat", vpath, *a)[1])
|
||||
vfs, ap = self._v2a("delete", vpath, [True, False, False, True])
|
||||
vfs, _, ap = self._v2a("delete", vpath, [True, False, False, True])
|
||||
|
||||
try:
|
||||
inf = bos.stat(ap)
|
||||
@@ -400,7 +433,7 @@ class Tftpd(object):
|
||||
|
||||
def _p_exists(self, vpath: str) -> bool:
|
||||
try:
|
||||
ap = self._v2a("p.exists", vpath, [False, False])[1]
|
||||
ap = self._v2a("p.exists", vpath, [False, False])[2]
|
||||
bos.stat(ap)
|
||||
return True
|
||||
except:
|
||||
@@ -408,7 +441,7 @@ class Tftpd(object):
|
||||
|
||||
def _p_isdir(self, vpath: str) -> bool:
|
||||
try:
|
||||
st = bos.stat(self._v2a("p.isdir", vpath, [False, False])[1])
|
||||
st = bos.stat(self._v2a("p.isdir", vpath, [False, False])[2])
|
||||
ret = stat.S_ISDIR(st.st_mode)
|
||||
return ret
|
||||
except:
|
||||
|
||||
Loaded 100 of 264 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user