copyparty 1.15.2__py3-none-any.whl → 1.15.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- copyparty/__init__.py +55 -0
- copyparty/__main__.py +15 -12
- copyparty/__version__.py +2 -2
- copyparty/cert.py +20 -8
- copyparty/cfg.py +1 -1
- copyparty/ftpd.py +1 -1
- copyparty/httpcli.py +227 -68
- copyparty/httpconn.py +0 -3
- copyparty/httpsrv.py +10 -15
- copyparty/metrics.py +1 -1
- copyparty/smbd.py +6 -3
- copyparty/stolen/qrcodegen.py +17 -0
- copyparty/szip.py +1 -1
- copyparty/u2idx.py +1 -0
- copyparty/up2k.py +31 -13
- copyparty/util.py +145 -64
- copyparty/web/a/partyfuse.py +233 -357
- copyparty/web/a/u2c.py +249 -153
- copyparty/web/browser.css.gz +0 -0
- copyparty/web/browser.html +3 -6
- copyparty/web/browser.js.gz +0 -0
- copyparty/web/deps/fuse.py +1064 -0
- copyparty/web/deps/marked.js.gz +0 -0
- copyparty/web/shares.css.gz +0 -0
- copyparty/web/shares.html +7 -4
- copyparty/web/shares.js.gz +0 -0
- copyparty/web/splash.html +12 -12
- copyparty/web/splash.js.gz +0 -0
- copyparty/web/svcs.html +1 -1
- copyparty/web/ui.css.gz +0 -0
- copyparty/web/util.js.gz +0 -0
- {copyparty-1.15.2.dist-info → copyparty-1.15.4.dist-info}/METADATA +9 -8
- {copyparty-1.15.2.dist-info → copyparty-1.15.4.dist-info}/RECORD +37 -36
- {copyparty-1.15.2.dist-info → copyparty-1.15.4.dist-info}/WHEEL +1 -1
- {copyparty-1.15.2.dist-info → copyparty-1.15.4.dist-info}/LICENSE +0 -0
- {copyparty-1.15.2.dist-info → copyparty-1.15.4.dist-info}/entry_points.txt +0 -0
- {copyparty-1.15.2.dist-info → copyparty-1.15.4.dist-info}/top_level.txt +0 -0
copyparty/web/a/partyfuse.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
-
from __future__ import print_function, unicode_literals
|
3
2
|
|
4
3
|
"""partyfuse: remote copyparty as a local filesystem"""
|
5
4
|
__author__ = "ed <copyparty@ocv.me>"
|
@@ -7,15 +6,22 @@ __copyright__ = 2019
|
|
7
6
|
__license__ = "MIT"
|
8
7
|
__url__ = "https://github.com/9001/copyparty/"
|
9
8
|
|
9
|
+
S_VERSION = "2.0"
|
10
|
+
S_BUILD_DT = "2024-10-01"
|
10
11
|
|
11
12
|
"""
|
12
13
|
mount a copyparty server (local or remote) as a filesystem
|
13
14
|
|
15
|
+
speeds:
|
16
|
+
1 GiB/s reading large files
|
17
|
+
27'000 files/sec: copy small files
|
18
|
+
700 folders/sec: copy small folders
|
19
|
+
|
14
20
|
usage:
|
15
21
|
python partyfuse.py http://192.168.1.69:3923/ ./music
|
16
22
|
|
17
23
|
dependencies:
|
18
|
-
python3 -m pip install --user fusepy
|
24
|
+
python3 -m pip install --user fusepy # or grab it from the connect page
|
19
25
|
+ on Linux: sudo apk add fuse
|
20
26
|
+ on Macos: https://osxfuse.github.io/
|
21
27
|
+ on Windows: https://github.com/billziss-gh/winfsp/releases/latest
|
@@ -29,32 +35,34 @@ get server cert:
|
|
29
35
|
"""
|
30
36
|
|
31
37
|
|
32
|
-
import
|
33
|
-
import
|
34
|
-
import
|
35
|
-
import
|
38
|
+
import argparse
|
39
|
+
import calendar
|
40
|
+
import codecs
|
41
|
+
import errno
|
36
42
|
import json
|
43
|
+
import os
|
44
|
+
import platform
|
45
|
+
import re
|
37
46
|
import stat
|
38
|
-
import errno
|
39
47
|
import struct
|
40
|
-
import
|
41
|
-
import builtins
|
42
|
-
import platform
|
43
|
-
import argparse
|
48
|
+
import sys
|
44
49
|
import threading
|
50
|
+
import time
|
45
51
|
import traceback
|
46
|
-
import http.client # py2: httplib
|
47
52
|
import urllib.parse
|
48
|
-
import calendar
|
49
53
|
from datetime import datetime, timezone
|
50
54
|
from urllib.parse import quote_from_bytes as quote
|
51
55
|
from urllib.parse import unquote_to_bytes as unquote
|
52
56
|
|
57
|
+
import builtins
|
58
|
+
import http.client
|
59
|
+
|
53
60
|
WINDOWS = sys.platform == "win32"
|
54
61
|
MACOS = platform.system() == "Darwin"
|
55
62
|
UTC = timezone.utc
|
56
63
|
|
57
64
|
|
65
|
+
|
58
66
|
def print(*args, **kwargs):
|
59
67
|
try:
|
60
68
|
builtins.print(*list(args), **kwargs)
|
@@ -71,11 +79,12 @@ print(
|
|
71
79
|
)
|
72
80
|
|
73
81
|
|
74
|
-
def
|
82
|
+
def nullfun(*a):
|
75
83
|
pass
|
76
84
|
|
77
85
|
|
78
|
-
info =
|
86
|
+
info = dbg = nullfun
|
87
|
+
is_dbg = False
|
79
88
|
|
80
89
|
|
81
90
|
try:
|
@@ -98,37 +107,35 @@ except:
|
|
98
107
|
|
99
108
|
|
100
109
|
def termsafe(txt):
|
110
|
+
enc = sys.stdout.encoding
|
101
111
|
try:
|
102
|
-
return txt.encode(
|
103
|
-
sys.stdout.encoding
|
104
|
-
)
|
112
|
+
return txt.encode(enc, "backslashreplace").decode(enc)
|
105
113
|
except:
|
106
|
-
return txt.encode(
|
114
|
+
return txt.encode(enc, "replace").decode(enc)
|
107
115
|
|
108
116
|
|
109
|
-
def threadless_log(
|
110
|
-
|
117
|
+
def threadless_log(fmt, *a):
|
118
|
+
fmt += "\n"
|
119
|
+
print(fmt % a if a else fmt, end="")
|
111
120
|
|
112
121
|
|
113
|
-
|
114
|
-
msg = "\033[36m{:012x}\033[0m {}\n".format(threading.current_thread().ident, msg)
|
115
|
-
print(msg[4:], end="")
|
122
|
+
riced_tids = {}
|
116
123
|
|
117
124
|
|
118
125
|
def rice_tid():
|
119
126
|
tid = threading.current_thread().ident
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
127
|
+
try:
|
128
|
+
return riced_tids[tid]
|
129
|
+
except:
|
130
|
+
c = struct.unpack(b"B" * 5, struct.pack(b">Q", tid)[-5:])
|
131
|
+
ret = "".join("\033[1;37;48;5;%dm%02x" % (x, x) for x in c) + "\033[0m"
|
132
|
+
riced_tids[tid] = ret
|
133
|
+
return ret
|
126
134
|
|
127
135
|
|
128
|
-
def
|
129
|
-
|
130
|
-
|
131
|
-
return " ".join(map(lambda b: format(ord(b), "02x"), binary))
|
136
|
+
def fancy_log(fmt, *a):
|
137
|
+
msg = fmt % a if a else fmt
|
138
|
+
print("%10.6f %s %s\n" % (time.time() % 900, rice_tid(), msg), end="")
|
132
139
|
|
133
140
|
|
134
141
|
def register_wtf8():
|
@@ -151,35 +158,34 @@ good_bad = {}
|
|
151
158
|
def enwin(txt):
|
152
159
|
return "".join([bad_good.get(x, x) for x in txt])
|
153
160
|
|
154
|
-
for bad, good in bad_good.items():
|
155
|
-
txt = txt.replace(bad, good)
|
156
|
-
|
157
|
-
return txt
|
158
|
-
|
159
161
|
|
160
162
|
def dewin(txt):
|
161
163
|
return "".join([good_bad.get(x, x) for x in txt])
|
162
164
|
|
163
|
-
for bad, good in bad_good.items():
|
164
|
-
txt = txt.replace(good, bad)
|
165
|
-
|
166
|
-
return txt
|
167
|
-
|
168
165
|
|
169
166
|
class RecentLog(object):
|
170
|
-
def __init__(self):
|
167
|
+
def __init__(self, ar):
|
168
|
+
self.ar = ar
|
171
169
|
self.mtx = threading.Lock()
|
172
|
-
self.f =
|
170
|
+
self.f = open(ar.logf, "wb") if ar.logf else None
|
173
171
|
self.q = []
|
174
172
|
|
175
173
|
thr = threading.Thread(target=self.printer)
|
176
174
|
thr.daemon = True
|
177
175
|
thr.start()
|
178
176
|
|
179
|
-
def put(self,
|
180
|
-
msg =
|
177
|
+
def put(self, fmt, *a):
|
178
|
+
msg = fmt % a if a else fmt
|
179
|
+
msg = "%10.6f %s %s\n" % (time.time() % 900, rice_tid(), msg)
|
181
180
|
if self.f:
|
182
|
-
|
181
|
+
zd = datetime.now(UTC)
|
182
|
+
fmsg = "%d-%04d-%06d.%06d %s" % (
|
183
|
+
zd.year,
|
184
|
+
zd.month * 100 + zd.day,
|
185
|
+
(zd.hour * 100 + zd.minute) * 100 + zd.second,
|
186
|
+
zd.microsecond,
|
187
|
+
msg,
|
188
|
+
)
|
183
189
|
self.f.write(fmsg.encode("utf-8"))
|
184
190
|
|
185
191
|
with self.mtx:
|
@@ -244,11 +250,16 @@ class CacheNode(object):
|
|
244
250
|
|
245
251
|
class Gateway(object):
|
246
252
|
def __init__(self, ar):
|
247
|
-
|
253
|
+
zs = ar.base_url
|
254
|
+
if "://" not in zs:
|
255
|
+
zs = "http://" + zs
|
256
|
+
|
257
|
+
self.base_url = zs
|
248
258
|
self.password = ar.a
|
249
259
|
|
250
|
-
ui = urllib.parse.urlparse(
|
260
|
+
ui = urllib.parse.urlparse(zs)
|
251
261
|
self.web_root = ui.path.strip("/")
|
262
|
+
self.SRS = "/%s/" % (self.web_root,) if self.web_root else "/"
|
252
263
|
try:
|
253
264
|
self.web_host, self.web_port = ui.netloc.split(":")
|
254
265
|
self.web_port = int(self.web_port)
|
@@ -274,6 +285,10 @@ class Gateway(object):
|
|
274
285
|
|
275
286
|
self.conns = {}
|
276
287
|
|
288
|
+
self.fsuf = "?raw"
|
289
|
+
self.dsuf = "?ls<&dots"
|
290
|
+
|
291
|
+
|
277
292
|
def quotep(self, path):
|
278
293
|
path = path.encode("wtf-8")
|
279
294
|
return quote(path, safe="/")
|
@@ -315,8 +330,8 @@ class Gateway(object):
|
|
315
330
|
c = self.getconn(tid)
|
316
331
|
c.request(meth, path, headers=headers, **kwargs)
|
317
332
|
return c.getresponse()
|
318
|
-
except:
|
319
|
-
|
333
|
+
except Exception as ex:
|
334
|
+
info("HTTP %r", ex)
|
320
335
|
|
321
336
|
self.closeconn(tid)
|
322
337
|
try:
|
@@ -337,66 +352,56 @@ class Gateway(object):
|
|
337
352
|
if bad_good:
|
338
353
|
path = dewin(path)
|
339
354
|
|
340
|
-
|
355
|
+
zs = "%s%s/" if path else "%s%s"
|
356
|
+
web_path = self.quotep(zs % (self.SRS, path)) + self.dsuf
|
341
357
|
r = self.sendreq("GET", web_path, {})
|
342
358
|
if r.status != 200:
|
343
359
|
self.closeconn()
|
344
|
-
|
345
|
-
"http error {} reading dir {} in {}".format(
|
346
|
-
r.status, web_path, rice_tid()
|
347
|
-
)
|
348
|
-
)
|
360
|
+
info("http error %s reading dir %r", r.status, web_path)
|
349
361
|
raise FuseOSError(errno.ENOENT)
|
350
362
|
|
351
363
|
ctype = r.getheader("Content-Type", "")
|
352
364
|
if ctype == "application/json":
|
353
365
|
parser = self.parse_jls
|
354
|
-
elif ctype.startswith("text/html"):
|
355
|
-
parser = self.parse_html
|
356
366
|
else:
|
357
|
-
|
367
|
+
info("listdir on file (%s): %r", ctype, path)
|
358
368
|
raise FuseOSError(errno.ENOENT)
|
359
369
|
|
360
370
|
try:
|
361
371
|
return parser(r)
|
362
372
|
except:
|
363
|
-
info(
|
364
|
-
raise
|
373
|
+
info("parser: %r\n%s", path, traceback.format_exc())
|
374
|
+
raise FuseOSError(errno.EIO)
|
365
375
|
|
366
376
|
def download_file_range(self, path, ofs1, ofs2):
|
367
377
|
if bad_good:
|
368
378
|
path = dewin(path)
|
369
379
|
|
370
|
-
web_path = self.quotep("
|
371
|
-
hdr_range = "bytes
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
)
|
376
|
-
)
|
380
|
+
web_path = self.quotep("%s%s" % (self.SRS, path)) + self.fsuf
|
381
|
+
hdr_range = "bytes=%d-%d" % (ofs1, ofs2 - 1)
|
382
|
+
|
383
|
+
t = "DL %4.0fK\033[36m%9d-%-9d\033[0m%r"
|
384
|
+
info(t, (ofs2 - ofs1) / 1024.0, ofs1, ofs2 - 1, path)
|
377
385
|
|
378
386
|
r = self.sendreq("GET", web_path, {"Range": hdr_range})
|
379
387
|
if r.status != http.client.PARTIAL_CONTENT:
|
388
|
+
t = "http error %d reading file %r range %s in %s"
|
389
|
+
info(t, r.status, web_path, hdr_range, rice_tid())
|
380
390
|
self.closeconn()
|
381
|
-
raise
|
382
|
-
"http error {} reading file {} range {} in {}".format(
|
383
|
-
r.status, web_path, hdr_range, rice_tid()
|
384
|
-
)
|
385
|
-
)
|
391
|
+
raise FuseOSError(errno.EIO)
|
386
392
|
|
387
393
|
return r.read()
|
388
394
|
|
389
|
-
def parse_jls(self,
|
395
|
+
def parse_jls(self, sck):
|
390
396
|
rsp = b""
|
391
397
|
while True:
|
392
|
-
buf =
|
398
|
+
buf = sck.read(1024 * 32)
|
393
399
|
if not buf:
|
394
400
|
break
|
395
|
-
|
396
401
|
rsp += buf
|
397
402
|
|
398
403
|
rsp = json.loads(rsp.decode("utf-8"))
|
399
|
-
ret =
|
404
|
+
ret = {}
|
400
405
|
for statfun, nodes in [
|
401
406
|
[self.stat_dir, rsp["dirs"]],
|
402
407
|
[self.stat_file, rsp["files"]],
|
@@ -406,60 +411,12 @@ class Gateway(object):
|
|
406
411
|
if bad_good:
|
407
412
|
fname = enwin(fname)
|
408
413
|
|
409
|
-
ret
|
414
|
+
ret[fname] = statfun(n["ts"], n["sz"])
|
410
415
|
|
411
416
|
return ret
|
412
417
|
|
413
|
-
def parse_html(self, datasrc):
|
414
|
-
ret = []
|
415
|
-
remainder = b""
|
416
|
-
ptn = re.compile(
|
417
|
-
r'^<tr><td>(-|DIR|<a [^<]+</a>)</td><td><a[^>]* href="([^"]+)"[^>]*>([^<]+)</a></td><td>([^<]+)</td><td>[^<]+</td><td>([^<]+)</td></tr>$'
|
418
|
-
)
|
419
|
-
|
420
|
-
while True:
|
421
|
-
buf = remainder + datasrc.read(4096)
|
422
|
-
# print('[{}]'.format(buf.decode('utf-8')))
|
423
|
-
if not buf:
|
424
|
-
break
|
425
418
|
|
426
|
-
|
427
|
-
endpos = buf.rfind(b"\n")
|
428
|
-
if endpos >= 0:
|
429
|
-
remainder = buf[endpos + 1 :]
|
430
|
-
buf = buf[:endpos]
|
431
|
-
|
432
|
-
lines = buf.decode("utf-8").split("\n")
|
433
|
-
for line in lines:
|
434
|
-
m = ptn.match(line)
|
435
|
-
if not m:
|
436
|
-
# print(line)
|
437
|
-
continue
|
438
|
-
|
439
|
-
ftype, furl, fname, fsize, fdate = m.groups()
|
440
|
-
fname = furl.rstrip("/").split("/")[-1]
|
441
|
-
fname = unquote(fname)
|
442
|
-
fname = fname.decode("wtf-8")
|
443
|
-
if bad_good:
|
444
|
-
fname = enwin(fname)
|
445
|
-
|
446
|
-
sz = 1
|
447
|
-
ts = 60 * 60 * 24 * 2
|
448
|
-
try:
|
449
|
-
sz = int(fsize)
|
450
|
-
ts = calendar.timegm(time.strptime(fdate, "%Y-%m-%d %H:%M:%S"))
|
451
|
-
except:
|
452
|
-
info("bad HTML or OS [{}] [{}]".format(fdate, fsize))
|
453
|
-
# python cannot strptime(1959-01-01) on windows
|
454
|
-
|
455
|
-
if ftype != "DIR":
|
456
|
-
ret.append([fname, self.stat_file(ts, sz), 0])
|
457
|
-
else:
|
458
|
-
ret.append([fname, self.stat_dir(ts, sz), 0])
|
459
|
-
|
460
|
-
return ret
|
461
|
-
|
462
|
-
def stat_dir(self, ts, sz=4096):
|
419
|
+
def stat_dir(self, ts, sz):
|
463
420
|
return {
|
464
421
|
"st_mode": stat.S_IFDIR | 0o555,
|
465
422
|
"st_uid": 1000,
|
@@ -488,7 +445,8 @@ class CPPF(Operations):
|
|
488
445
|
def __init__(self, ar):
|
489
446
|
self.gw = Gateway(ar)
|
490
447
|
self.junk_fh_ctr = 3
|
491
|
-
self.
|
448
|
+
self.t_dircache = ar.cds
|
449
|
+
self.n_dircache = ar.cdn
|
492
450
|
self.n_filecache = ar.cf
|
493
451
|
|
494
452
|
self.dircache = []
|
@@ -500,72 +458,51 @@ class CPPF(Operations):
|
|
500
458
|
info("up")
|
501
459
|
|
502
460
|
def _describe(self):
|
503
|
-
msg =
|
461
|
+
msg = []
|
504
462
|
with self.filecache_mtx:
|
505
463
|
for n, cn in enumerate(self.filecache):
|
506
464
|
cache_path, cache1 = cn.tag
|
507
465
|
cache2 = cache1 + len(cn.data)
|
508
|
-
|
466
|
+
t = "\n{:<2} {:>7} {:>10}:{:<9} {}".format(
|
509
467
|
n,
|
510
468
|
len(cn.data),
|
511
469
|
cache1,
|
512
470
|
cache2,
|
513
471
|
cache_path.replace("\r", "\\r").replace("\n", "\\n"),
|
514
472
|
)
|
515
|
-
|
473
|
+
msg.append(t)
|
474
|
+
return "".join(msg)
|
516
475
|
|
517
476
|
def clean_dircache(self):
|
518
477
|
"""not threadsafe"""
|
519
478
|
now = time.time()
|
520
479
|
cutoff = 0
|
521
480
|
for cn in self.dircache:
|
522
|
-
if now - cn.ts
|
523
|
-
cutoff += 1
|
524
|
-
else:
|
481
|
+
if now - cn.ts <= self.t_dircache:
|
525
482
|
break
|
483
|
+
cutoff += 1
|
526
484
|
|
527
485
|
if cutoff > 0:
|
528
486
|
self.dircache = self.dircache[cutoff:]
|
487
|
+
elif len(self.dircache) > self.n_dircache:
|
488
|
+
self.dircache.pop(0)
|
529
489
|
|
530
490
|
def get_cached_dir(self, dirpath):
|
531
491
|
with self.dircache_mtx:
|
532
|
-
self.clean_dircache()
|
533
492
|
for cn in self.dircache:
|
534
493
|
if cn.tag == dirpath:
|
535
|
-
|
536
|
-
|
494
|
+
if time.time() - cn.ts <= self.t_dircache:
|
495
|
+
return cn
|
496
|
+
break
|
537
497
|
return None
|
538
498
|
|
539
|
-
"""
|
540
|
-
,-------------------------------, g1>=c1, g2<=c2
|
541
|
-
|cache1 cache2| buf[g1-c1:(g1-c1)+(g2-g1)]
|
542
|
-
`-------------------------------'
|
543
|
-
,---------------,
|
544
|
-
|get1 get2|
|
545
|
-
`---------------'
|
546
|
-
__________________________________________________________________________
|
547
|
-
|
548
|
-
,-------------------------------, g2<=c2, (g2>=c1)
|
549
|
-
|cache1 cache2| cdr=buf[:g2-c1]
|
550
|
-
`-------------------------------' dl car; g1-512K:c1
|
551
|
-
,---------------,
|
552
|
-
|get1 get2|
|
553
|
-
`---------------'
|
554
|
-
__________________________________________________________________________
|
555
|
-
|
556
|
-
,-------------------------------, g1>=c1, (g1<=c2)
|
557
|
-
|cache1 cache2| car=buf[c2-g1:]
|
558
|
-
`-------------------------------' dl cdr; c2:c2+1M
|
559
|
-
,---------------,
|
560
|
-
|get1 get2|
|
561
|
-
`---------------'
|
562
|
-
"""
|
563
499
|
|
564
500
|
def get_cached_file(self, path, get1, get2, file_sz):
|
565
501
|
car = None
|
566
502
|
cdr = None
|
567
503
|
ncn = -1
|
568
|
-
|
504
|
+
if is_dbg:
|
505
|
+
dbg("cache request %d:%d |%d|%s", get1, get2, file_sz, self._describe())
|
569
506
|
with self.filecache_mtx:
|
570
507
|
for cn in self.filecache:
|
571
508
|
ncn += 1
|
@@ -592,15 +529,14 @@ class CPPF(Operations):
|
|
592
529
|
buf_ofs = get1 - cache1
|
593
530
|
buf_end = buf_ofs + (get2 - get1)
|
594
531
|
dbg(
|
595
|
-
"found all (
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
)
|
532
|
+
"found all (#%d %d:%d |%d|) [%d:%d] = %d",
|
533
|
+
ncn,
|
534
|
+
cache1,
|
535
|
+
cache2,
|
536
|
+
len(cn.data),
|
537
|
+
buf_ofs,
|
538
|
+
buf_end,
|
539
|
+
buf_end - buf_ofs,
|
604
540
|
)
|
605
541
|
return cn.data[buf_ofs:buf_end]
|
606
542
|
|
@@ -608,16 +544,15 @@ class CPPF(Operations):
|
|
608
544
|
x = cn.data[: get2 - cache1]
|
609
545
|
if not cdr or len(cdr) < len(x):
|
610
546
|
dbg(
|
611
|
-
"found cdr (
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
)
|
547
|
+
"found cdr (#%d %d:%d |%d|) [:%d-%d] = [:%d] = %d",
|
548
|
+
ncn,
|
549
|
+
cache1,
|
550
|
+
cache2,
|
551
|
+
len(cn.data),
|
552
|
+
get2,
|
553
|
+
cache1,
|
554
|
+
get2 - cache1,
|
555
|
+
len(x),
|
621
556
|
)
|
622
557
|
cdr = x
|
623
558
|
|
@@ -627,22 +562,21 @@ class CPPF(Operations):
|
|
627
562
|
x = cn.data[-(max(0, cache2 - get1)) :]
|
628
563
|
if not car or len(car) < len(x):
|
629
564
|
dbg(
|
630
|
-
"found car (
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
)
|
565
|
+
"found car (#%d %d:%d |%d|) [-(%d-%d):] = [-%d:] = %d",
|
566
|
+
ncn,
|
567
|
+
cache1,
|
568
|
+
cache2,
|
569
|
+
len(cn.data),
|
570
|
+
cache2,
|
571
|
+
get1,
|
572
|
+
cache2 - get1,
|
573
|
+
len(x),
|
640
574
|
)
|
641
575
|
car = x
|
642
576
|
|
643
577
|
continue
|
644
578
|
|
645
|
-
msg = "cache fallthrough\n
|
579
|
+
msg = "cache fallthrough\n%d %d %d\n%d %d %d\n%d %d --\n%s" % (
|
646
580
|
get1,
|
647
581
|
get2,
|
648
582
|
get2 - get1,
|
@@ -651,9 +585,10 @@ class CPPF(Operations):
|
|
651
585
|
cache2 - cache1,
|
652
586
|
get1 - cache1,
|
653
587
|
get2 - cache2,
|
588
|
+
self._describe(),
|
654
589
|
)
|
655
|
-
msg
|
656
|
-
raise
|
590
|
+
info(msg)
|
591
|
+
raise FuseOSError(errno.EIO)
|
657
592
|
|
658
593
|
if car and cdr and len(car) + len(cdr) == get2 - get1:
|
659
594
|
dbg("<cache> have both")
|
@@ -661,62 +596,61 @@ class CPPF(Operations):
|
|
661
596
|
|
662
597
|
elif cdr and (not car or len(car) < len(cdr)):
|
663
598
|
h_end = get1 + (get2 - get1) - len(cdr)
|
664
|
-
h_ofs = min(get1, h_end -
|
599
|
+
h_ofs = min(get1, h_end - 0x80000) # 512k
|
665
600
|
|
666
601
|
if h_ofs < 0:
|
667
602
|
h_ofs = 0
|
668
603
|
|
669
604
|
buf_ofs = get1 - h_ofs
|
670
605
|
|
671
|
-
dbg
|
672
|
-
"<cache> cdr
|
673
|
-
|
674
|
-
)
|
675
|
-
)
|
606
|
+
if dbg:
|
607
|
+
t = "<cache> cdr %d, car %d:%d |%d| [%d:]"
|
608
|
+
dbg(t, len(cdr), h_ofs, h_end, h_end - h_ofs, buf_ofs)
|
676
609
|
|
677
610
|
buf = self.gw.download_file_range(path, h_ofs, h_end)
|
678
611
|
if len(buf) == h_end - h_ofs:
|
679
612
|
ret = buf[buf_ofs:] + cdr
|
680
613
|
else:
|
681
614
|
ret = buf[get1 - h_ofs :]
|
682
|
-
|
683
|
-
|
684
|
-
h_ofs, h_end, len(buf), len(ret)
|
685
|
-
)
|
686
|
-
)
|
615
|
+
t = "remote truncated %d:%d to |%d|, will return |%d|"
|
616
|
+
info(t, h_ofs, h_end, len(buf), len(ret))
|
687
617
|
|
688
618
|
elif car:
|
689
619
|
h_ofs = get1 + len(car)
|
690
|
-
|
620
|
+
if get2 < 0x100000:
|
621
|
+
# already cached from 0 to 64k, now do ~64k plus 1 MiB
|
622
|
+
h_end = max(get2, h_ofs + 0x100000) # 1m
|
623
|
+
else:
|
624
|
+
# after 1 MiB, bump window to 8 MiB
|
625
|
+
h_end = max(get2, h_ofs + 0x800000) # 8m
|
691
626
|
|
692
627
|
if h_end > file_sz:
|
693
628
|
h_end = file_sz
|
694
629
|
|
695
630
|
buf_ofs = (get2 - get1) - len(car)
|
696
631
|
|
697
|
-
|
698
|
-
|
699
|
-
len(car), h_ofs, h_end, h_end - h_ofs, buf_ofs
|
700
|
-
)
|
701
|
-
)
|
632
|
+
t = "<cache> car %d, cdr %d:%d |%d| [:%d]"
|
633
|
+
dbg(t, len(car), h_ofs, h_end, h_end - h_ofs, buf_ofs)
|
702
634
|
|
703
635
|
buf = self.gw.download_file_range(path, h_ofs, h_end)
|
704
636
|
ret = car + buf[:buf_ofs]
|
705
637
|
|
706
638
|
else:
|
707
|
-
if get2 - get1
|
639
|
+
if get2 - get1 < 0x500000: # 5m
|
708
640
|
# unless the request is for the last n bytes of the file,
|
709
641
|
# grow the start to cache some stuff around the range
|
710
642
|
if get2 < file_sz - 1:
|
711
|
-
h_ofs = get1 -
|
643
|
+
h_ofs = get1 - 0x40000 # 256k
|
712
644
|
else:
|
713
|
-
h_ofs = get1 -
|
645
|
+
h_ofs = get1 - 0x10000 # 64k
|
714
646
|
|
715
647
|
# likewise grow the end unless start is 0
|
716
|
-
if get1
|
717
|
-
h_end = get2 +
|
648
|
+
if get1 >= 0x100000:
|
649
|
+
h_end = get2 + 0x400000 # 4m
|
650
|
+
elif get1 > 0:
|
651
|
+
h_end = get2 + 0x100000 # 1m
|
718
652
|
else:
|
719
|
-
h_end = get2 +
|
653
|
+
h_end = get2 + 0x10000 # 64k
|
720
654
|
else:
|
721
655
|
# big enough, doesn't need pads
|
722
656
|
h_ofs = get1
|
@@ -731,11 +665,8 @@ class CPPF(Operations):
|
|
731
665
|
buf_ofs = get1 - h_ofs
|
732
666
|
buf_end = buf_ofs + get2 - get1
|
733
667
|
|
734
|
-
|
735
|
-
|
736
|
-
h_ofs, h_end, h_end - h_ofs, buf_ofs, buf_end
|
737
|
-
)
|
738
|
-
)
|
668
|
+
t = "<cache> %d:%d |%d| [%d:%d]"
|
669
|
+
dbg(t, h_ofs, h_end, h_end - h_ofs, buf_ofs, buf_end)
|
739
670
|
|
740
671
|
buf = self.gw.download_file_range(path, h_ofs, h_end)
|
741
672
|
ret = buf[buf_ofs:buf_end]
|
@@ -751,7 +682,7 @@ class CPPF(Operations):
|
|
751
682
|
|
752
683
|
def _readdir(self, path, fh=None):
|
753
684
|
path = path.strip("/")
|
754
|
-
|
685
|
+
dbg("readdir %r [%s]", path, fh)
|
755
686
|
|
756
687
|
ret = self.gw.listdir(path)
|
757
688
|
if not self.n_dircache:
|
@@ -766,27 +697,24 @@ class CPPF(Operations):
|
|
766
697
|
return ret
|
767
698
|
|
768
699
|
def readdir(self, path, fh=None):
|
769
|
-
return [".", ".."] + self._readdir(path, fh)
|
700
|
+
return [".", ".."] + list(self._readdir(path, fh))
|
770
701
|
|
771
702
|
def read(self, path, length, offset, fh=None):
|
772
703
|
req_max = 1024 * 1024 * 8
|
773
704
|
cache_max = 1024 * 1024 * 2
|
774
705
|
if length > req_max:
|
775
706
|
# windows actually doing 240 MiB read calls, sausage
|
776
|
-
info("truncate |
|
707
|
+
info("truncate |%d| to %dMiB", length, req_max >> 20)
|
777
708
|
length = req_max
|
778
709
|
|
779
710
|
path = path.strip("/")
|
780
711
|
ofs2 = offset + length
|
781
712
|
file_sz = self.getattr(path)["st_size"]
|
782
|
-
|
783
|
-
|
784
|
-
hexler(path), length, offset, ofs2, file_sz
|
785
|
-
)
|
786
|
-
)
|
713
|
+
dbg("read %r |%d| %d:%d max %d", path, length, offset, ofs2, file_sz)
|
714
|
+
|
787
715
|
if ofs2 > file_sz:
|
788
716
|
ofs2 = file_sz
|
789
|
-
|
717
|
+
dbg("truncate to |%d| :%d", ofs2 - offset, ofs2)
|
790
718
|
|
791
719
|
if file_sz == 0 or offset >= ofs2:
|
792
720
|
return b""
|
@@ -798,73 +726,43 @@ class CPPF(Operations):
|
|
798
726
|
|
799
727
|
return ret
|
800
728
|
|
801
|
-
fn = "cppf-{}-{}-{}".format(time.time(), offset, length)
|
802
|
-
if False:
|
803
|
-
with open(fn, "wb", len(ret)) as f:
|
804
|
-
f.write(ret)
|
805
|
-
elif self.n_filecache:
|
806
|
-
ret2 = self.gw.download_file_range(path, offset, ofs2)
|
807
|
-
if ret != ret2:
|
808
|
-
info(fn)
|
809
|
-
for v in [ret, ret2]:
|
810
|
-
try:
|
811
|
-
info(len(v))
|
812
|
-
except:
|
813
|
-
info("uhh " + repr(v))
|
814
|
-
|
815
|
-
with open(fn + ".bad", "wb") as f:
|
816
|
-
f.write(ret)
|
817
|
-
with open(fn + ".good", "wb") as f:
|
818
|
-
f.write(ret2)
|
819
|
-
|
820
|
-
raise Exception("cache bork")
|
821
|
-
|
822
|
-
return ret
|
823
729
|
|
824
730
|
def getattr(self, path, fh=None):
|
825
|
-
|
731
|
+
dbg("getattr %r", path)
|
826
732
|
if WINDOWS:
|
827
733
|
path = enwin(path) # windows occasionally decodes f0xx to xx
|
828
734
|
|
829
735
|
path = path.strip("/")
|
736
|
+
if not path:
|
737
|
+
ret = self.gw.stat_dir(time.time(), 4096)
|
738
|
+
dbg("/=%r", ret)
|
739
|
+
return ret
|
740
|
+
|
830
741
|
try:
|
831
742
|
dirpath, fname = path.rsplit("/", 1)
|
832
743
|
except:
|
833
744
|
dirpath = ""
|
834
745
|
fname = path
|
835
746
|
|
836
|
-
if not path:
|
837
|
-
ret = self.gw.stat_dir(time.time())
|
838
|
-
# dbg("=" + repr(ret))
|
839
|
-
return ret
|
840
|
-
|
841
747
|
cn = self.get_cached_dir(dirpath)
|
842
748
|
if cn:
|
843
|
-
log("cache ok")
|
844
749
|
dents = cn.data
|
845
750
|
else:
|
846
751
|
dbg("cache miss")
|
847
752
|
dents = self._readdir(dirpath)
|
848
753
|
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
# "\n".join(traceback.format_stack()[:-1]),
|
856
|
-
# )
|
857
|
-
# )
|
858
|
-
|
859
|
-
if cache_name == fname:
|
860
|
-
# dbg("=" + repr(cache_stat))
|
861
|
-
return cache_stat
|
754
|
+
try:
|
755
|
+
ret = dents[fname]
|
756
|
+
dbg("s=%r", ret)
|
757
|
+
return ret
|
758
|
+
except:
|
759
|
+
pass
|
862
760
|
|
863
761
|
fun = info
|
864
762
|
if MACOS and path.split("/")[-1].startswith("._"):
|
865
763
|
fun = dbg
|
866
764
|
|
867
|
-
fun("=ENOENT
|
765
|
+
fun("=ENOENT %r", path)
|
868
766
|
raise FuseOSError(errno.ENOENT)
|
869
767
|
|
870
768
|
access = None
|
@@ -877,43 +775,6 @@ class CPPF(Operations):
|
|
877
775
|
releasedir = None
|
878
776
|
statfs = None
|
879
777
|
|
880
|
-
if False:
|
881
|
-
# incorrect semantics but good for debugging stuff like samba and msys2
|
882
|
-
def access(self, path, mode):
|
883
|
-
log("@@ access [{}] [{}]".format(path, mode))
|
884
|
-
return 1 if self.getattr(path) else 0
|
885
|
-
|
886
|
-
def flush(self, path, fh):
|
887
|
-
log("@@ flush [{}] [{}]".format(path, fh))
|
888
|
-
return True
|
889
|
-
|
890
|
-
def getxattr(self, *args):
|
891
|
-
log("@@ getxattr [{}]".format("] [".join(str(x) for x in args)))
|
892
|
-
return False
|
893
|
-
|
894
|
-
def listxattr(self, *args):
|
895
|
-
log("@@ listxattr [{}]".format("] [".join(str(x) for x in args)))
|
896
|
-
return False
|
897
|
-
|
898
|
-
def open(self, path, flags):
|
899
|
-
log("@@ open [{}] [{}]".format(path, flags))
|
900
|
-
return 42
|
901
|
-
|
902
|
-
def opendir(self, fh):
|
903
|
-
log("@@ opendir [{}]".format(fh))
|
904
|
-
return 69
|
905
|
-
|
906
|
-
def release(self, ino, fi):
|
907
|
-
log("@@ release [{}] [{}]".format(ino, fi))
|
908
|
-
return True
|
909
|
-
|
910
|
-
def releasedir(self, ino, fi):
|
911
|
-
log("@@ releasedir [{}] [{}]".format(ino, fi))
|
912
|
-
return True
|
913
|
-
|
914
|
-
def statfs(self, path):
|
915
|
-
log("@@ statfs [{}]".format(path))
|
916
|
-
return {}
|
917
778
|
|
918
779
|
if sys.platform == "win32":
|
919
780
|
# quick compat for /mingw64/bin/python3 (msys2)
|
@@ -930,28 +791,28 @@ class CPPF(Operations):
|
|
930
791
|
return self.junk_fh_ctr
|
931
792
|
|
932
793
|
except Exception as ex:
|
933
|
-
|
794
|
+
info("open ERR %r", ex)
|
934
795
|
raise FuseOSError(errno.ENOENT)
|
935
796
|
|
936
797
|
def open(self, path, flags):
|
937
|
-
dbg("open
|
798
|
+
dbg("open %r [%s]", path, flags)
|
938
799
|
return self._open(path)
|
939
800
|
|
940
801
|
def opendir(self, path):
|
941
|
-
dbg("opendir
|
802
|
+
dbg("opendir %r", path)
|
942
803
|
return self._open(path)
|
943
804
|
|
944
805
|
def flush(self, path, fh):
|
945
|
-
dbg("flush
|
806
|
+
dbg("flush %r [%s]", path, fh)
|
946
807
|
|
947
808
|
def release(self, ino, fi):
|
948
|
-
dbg("release
|
809
|
+
dbg("release %r [%s]", ino, fi)
|
949
810
|
|
950
811
|
def releasedir(self, ino, fi):
|
951
|
-
dbg("releasedir
|
812
|
+
dbg("releasedir %r [%s]", ino, fi)
|
952
813
|
|
953
814
|
def access(self, path, mode):
|
954
|
-
dbg("access
|
815
|
+
dbg("access %r [%s]", path, mode)
|
955
816
|
try:
|
956
817
|
x = self.getattr(path)
|
957
818
|
if x["st_mode"] <= 0:
|
@@ -967,19 +828,24 @@ class TheArgparseFormatter(
|
|
967
828
|
|
968
829
|
|
969
830
|
def main():
|
970
|
-
global info,
|
831
|
+
global info, dbg, is_dbg
|
971
832
|
time.strptime("19970815", "%Y%m%d") # python#7980
|
972
833
|
|
834
|
+
ver = "{0}, v{1}".format(S_BUILD_DT, S_VERSION)
|
835
|
+
if "--version" in sys.argv:
|
836
|
+
print("partyfuse", ver)
|
837
|
+
return
|
838
|
+
|
973
839
|
# filecache helps for reads that are ~64k or smaller;
|
974
|
-
#
|
975
|
-
#
|
976
|
-
# value is numChunks (1~
|
977
|
-
nf =
|
840
|
+
# windows likes to use 4k and 64k so cache is important,
|
841
|
+
# linux generally does 128k so the cache is still nice,
|
842
|
+
# value is numChunks (1~8M each) to keep in the cache
|
843
|
+
nf = 12
|
978
844
|
|
979
845
|
# dircache is always a boost,
|
980
846
|
# only want to disable it for tests etc,
|
981
|
-
|
982
|
-
|
847
|
+
cdn = 9 # max num dirs; 0=disable
|
848
|
+
cds = 1 # numsec until an entry goes stale
|
983
849
|
|
984
850
|
where = "local directory"
|
985
851
|
if WINDOWS:
|
@@ -992,39 +858,53 @@ def main():
|
|
992
858
|
|
993
859
|
ap = argparse.ArgumentParser(
|
994
860
|
formatter_class=TheArgparseFormatter,
|
861
|
+
description="mount a copyparty server as a local filesystem -- " + ver,
|
995
862
|
epilog="example:" + ex_pre + ex_pre.join(examples),
|
996
863
|
)
|
997
|
-
|
998
|
-
"-cd", metavar="NUM_SECONDS", type=float, default=nd, help="directory cache"
|
999
|
-
)
|
1000
|
-
ap.add_argument(
|
1001
|
-
"-cf", metavar="NUM_BLOCKS", type=int, default=nf, help="file cache"
|
1002
|
-
)
|
1003
|
-
ap.add_argument("-a", metavar="PASSWORD", help="password or $filepath")
|
1004
|
-
ap.add_argument("-d", action="store_true", help="enable debug")
|
1005
|
-
ap.add_argument("-te", metavar="PEM_FILE", help="certificate to expect/verify")
|
1006
|
-
ap.add_argument("-td", action="store_true", help="disable certificate check")
|
864
|
+
# fmt: off
|
1007
865
|
ap.add_argument("base_url", type=str, help="remote copyparty URL to mount")
|
1008
866
|
ap.add_argument("local_path", type=str, help=where + " to mount it on")
|
867
|
+
ap.add_argument("-a", metavar="PASSWORD", help="password or $filepath")
|
868
|
+
|
869
|
+
|
870
|
+
ap2 = ap.add_argument_group("https/TLS")
|
871
|
+
ap2.add_argument("-te", metavar="PEMFILE", help="certificate to expect/verify")
|
872
|
+
ap2.add_argument("-td", action="store_true", help="disable certificate check")
|
873
|
+
|
874
|
+
ap2 = ap.add_argument_group("cache/perf")
|
875
|
+
ap2.add_argument("-cdn", metavar="DIRS", type=float, default=cdn, help="directory-cache, max num dirs; 0=disable")
|
876
|
+
ap2.add_argument("-cds", metavar="SECS", type=float, default=cds, help="directory-cache, expiration time")
|
877
|
+
ap2.add_argument("-cf", metavar="BLOCKS", type=int, default=nf, help="file cache; each block is <= 1 MiB")
|
878
|
+
|
879
|
+
ap2 = ap.add_argument_group("logging")
|
880
|
+
ap2.add_argument("-q", action="store_true", help="quiet")
|
881
|
+
ap2.add_argument("-d", action="store_true", help="debug/verbose")
|
882
|
+
ap2.add_argument("--slowterm", action="store_true", help="only most recent msgs; good for windows")
|
883
|
+
ap2.add_argument("--logf", metavar="FILE", type=str, default="", help="log to FILE; enables --slowterm")
|
884
|
+
|
885
|
+
ap2 = ap.add_argument_group("fuse")
|
886
|
+
ap2.add_argument("--oth", action="store_true", help="tell FUSE to '-o allow_other'")
|
887
|
+
ap2.add_argument("--nonempty", action="store_true", help="tell FUSE to '-o nonempty'")
|
888
|
+
|
1009
889
|
ar = ap.parse_args()
|
890
|
+
# fmt: on
|
1010
891
|
|
1011
|
-
if ar.
|
1012
|
-
|
1013
|
-
# otoh fancy_log beats RecentLog on linux
|
1014
|
-
logger = RecentLog().put if WINDOWS else fancy_log
|
892
|
+
if ar.logf:
|
893
|
+
ar.slowterm = True
|
1015
894
|
|
895
|
+
# windows terminals are slow (cmd.exe, mintty)
|
896
|
+
# otoh fancy_log beats RecentLog on linux
|
897
|
+
logger = RecentLog(ar).put if ar.slowterm else fancy_log
|
898
|
+
if ar.d:
|
1016
899
|
info = logger
|
1017
|
-
log = logger
|
1018
900
|
dbg = logger
|
1019
|
-
|
1020
|
-
|
1021
|
-
info =
|
1022
|
-
log = null_log
|
1023
|
-
dbg = null_log
|
901
|
+
is_dbg = True
|
902
|
+
elif not ar.q:
|
903
|
+
info = logger
|
1024
904
|
|
1025
905
|
if ar.a and ar.a.startswith("$"):
|
1026
906
|
fn = ar.a[1:]
|
1027
|
-
|
907
|
+
info("reading password from file %r", fn)
|
1028
908
|
with open(fn, "rb") as f:
|
1029
909
|
ar.a = f.read().decode("utf-8").strip()
|
1030
910
|
|
@@ -1045,14 +925,10 @@ def main():
|
|
1045
925
|
|
1046
926
|
register_wtf8()
|
1047
927
|
|
1048
|
-
|
1049
|
-
|
1050
|
-
|
1051
|
-
|
1052
|
-
allow_other = WINDOWS or MACOS
|
1053
|
-
|
1054
|
-
args = {"foreground": True, "nothreads": True, "allow_other": allow_other}
|
1055
|
-
if not MACOS:
|
928
|
+
args = {"foreground": True, "nothreads": True}
|
929
|
+
if ar.oth:
|
930
|
+
args["allow_other"] = True
|
931
|
+
if ar.nonempty:
|
1056
932
|
args["nonempty"] = True
|
1057
933
|
|
1058
934
|
FUSE(CPPF(ar), ar.local_path, encoding="wtf-8", **args)
|