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.
@@ -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 re
33
- import os
34
- import sys
35
- import time
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 codecs
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 null_log(msg):
82
+ def nullfun(*a):
75
83
  pass
76
84
 
77
85
 
78
- info = log = dbg = null_log
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(sys.stdout.encoding, "backslashreplace").decode(
103
- sys.stdout.encoding
104
- )
112
+ return txt.encode(enc, "backslashreplace").decode(enc)
105
113
  except:
106
- return txt.encode(sys.stdout.encoding, "replace").decode(sys.stdout.encoding)
114
+ return txt.encode(enc, "replace").decode(enc)
107
115
 
108
116
 
109
- def threadless_log(msg):
110
- print(msg + "\n", end="")
117
+ def threadless_log(fmt, *a):
118
+ fmt += "\n"
119
+ print(fmt % a if a else fmt, end="")
111
120
 
112
121
 
113
- def boring_log(msg):
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
- c = struct.unpack(b"B" * 5, struct.pack(b">Q", tid)[-5:])
121
- return "".join("\033[1;37;48;5;{}m{:02x}".format(x, x) for x in c) + "\033[0m"
122
-
123
-
124
- def fancy_log(msg):
125
- print("{:10.6f} {} {}\n".format(time.time() % 900, rice_tid(), msg), end="")
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 hexler(binary):
129
- return binary.replace("\r", "\\r").replace("\n", "\\n")
130
- return " ".join(["{}\033[36m{:02x}\033[0m".format(b, ord(b)) for b in binary])
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 = None # open("partyfuse.log", "wb")
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, msg):
180
- msg = "{:10.6f} {} {}\n".format(time.time() % 900, rice_tid(), 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
- fmsg = " ".join([datetime.now(UTC).strftime("%H%M%S.%f"), str(msg)])
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
- self.base_url = ar.base_url
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(self.base_url)
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&lt&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
- dbg("bad conn")
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
- web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?dots&ls"
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
- log(
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
- log("listdir on file: {}".format(path))
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(repr(path) + "\n" + traceback.format_exc())
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("/" + "/".join([self.web_root, path])) + "?raw"
371
- hdr_range = "bytes={}-{}".format(ofs1, ofs2 - 1)
372
- info(
373
- "DL {:4.0f}K\033[36m{:>9}-{:<9}\033[0m{}".format(
374
- (ofs2 - ofs1) / 1024.0, ofs1, ofs2 - 1, hexler(path)
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 Exception(
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, datasrc):
395
+ def parse_jls(self, sck):
390
396
  rsp = b""
391
397
  while True:
392
- buf = datasrc.read(1024 * 32)
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.append([fname, statfun(n["ts"], n["sz"]), 0])
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
- remainder = b""
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.n_dircache = ar.cd
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
- msg += "\n{:<2} {:>7} {:>10}:{:<9} {}".format(
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
- return msg
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 > self.n_dircache:
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
- return cn
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
- dbg("cache request {}:{} |{}|".format(get1, get2, file_sz) + self._describe())
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 (#{} {}:{} |{}|) [{}:{}] = {}".format(
596
- ncn,
597
- cache1,
598
- cache2,
599
- len(cn.data),
600
- buf_ofs,
601
- buf_end,
602
- buf_end - buf_ofs,
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 (#{} {}:{} |{}|) [:{}-{}] = [:{}] = {}".format(
612
- ncn,
613
- cache1,
614
- cache2,
615
- len(cn.data),
616
- get2,
617
- cache1,
618
- get2 - cache1,
619
- len(x),
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 (#{} {}:{} |{}|) [-({}-{}):] = [-{}:] = {}".format(
631
- ncn,
632
- cache1,
633
- cache2,
634
- len(cn.data),
635
- cache2,
636
- get1,
637
- cache2 - get1,
638
- len(x),
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{} {} {}\n{} {} {}\n{} {} --\n".format(
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 += self._describe()
656
- raise Exception(msg)
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 - 512 * 1024)
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 {}, car {}:{} |{}| [{}:]".format(
673
- len(cdr), h_ofs, h_end, h_end - h_ofs, buf_ofs
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
- info(
683
- "remote truncated {}:{} to |{}|, will return |{}|".format(
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
- h_end = max(get2, h_ofs + 1024 * 1024)
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
- dbg(
698
- "<cache> car {}, cdr {}:{} |{}| [:{}]".format(
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 <= 1024 * 1024:
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 - 1024 * 256
643
+ h_ofs = get1 - 0x40000 # 256k
712
644
  else:
713
- h_ofs = get1 - 1024 * 32
645
+ h_ofs = get1 - 0x10000 # 64k
714
646
 
715
647
  # likewise grow the end unless start is 0
716
- if get1 > 0:
717
- h_end = get2 + 1024 * 1024
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 + 1024 * 64
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
- dbg(
735
- "<cache> {}:{} |{}| [{}:{}]".format(
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
- log("readdir [{}] [{}]".format(hexler(path), fh))
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 |{}| to {}MiB".format(length, req_max >> 20))
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
- log(
783
- "read {} |{}| {}:{} max {}".format(
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
- log("truncate to |{}| :{}".format(ofs2 - offset, ofs2))
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
- log("getattr [{}]".format(hexler(path)))
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
- for cache_name, cache_stat, _ in dents:
850
- # if "qw" in cache_name and "qw" in fname:
851
- # info(
852
- # "cmp\n [{}]\n [{}]\n\n{}\n".format(
853
- # hexler(cache_name),
854
- # hexler(fname),
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 ({})".format(hexler(path)))
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
- log("open ERR {}".format(repr(ex)))
794
+ info("open ERR %r", ex)
934
795
  raise FuseOSError(errno.ENOENT)
935
796
 
936
797
  def open(self, path, flags):
937
- dbg("open [{}] [{}]".format(hexler(path), flags))
798
+ dbg("open %r [%s]", path, flags)
938
799
  return self._open(path)
939
800
 
940
801
  def opendir(self, path):
941
- dbg("opendir [{}]".format(hexler(path)))
802
+ dbg("opendir %r", path)
942
803
  return self._open(path)
943
804
 
944
805
  def flush(self, path, fh):
945
- dbg("flush [{}] [{}]".format(hexler(path), fh))
806
+ dbg("flush %r [%s]", path, fh)
946
807
 
947
808
  def release(self, ino, fi):
948
- dbg("release [{}] [{}]".format(hexler(ino), fi))
809
+ dbg("release %r [%s]", ino, fi)
949
810
 
950
811
  def releasedir(self, ino, fi):
951
- dbg("releasedir [{}] [{}]".format(hexler(ino), fi))
812
+ dbg("releasedir %r [%s]", ino, fi)
952
813
 
953
814
  def access(self, path, mode):
954
- dbg("access [{}] [{}]".format(hexler(path), mode))
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, log, dbg
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
- # linux generally does 128k so the cache is a slowdown,
975
- # windows likes to use 4k and 64k so cache is required,
976
- # value is numChunks (1~3M each) to keep in the cache
977
- nf = 24
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
- # value is numSec until an entry goes stale
982
- nd = 1
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
- ap.add_argument(
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.d:
1012
- # windows terminals are slow (cmd.exe, mintty)
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
- else:
1020
- # debug=off, speed is dontcare
1021
- info = fancy_log
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
- log("reading password from file [{}]".format(fn))
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
- try:
1049
- with open("/etc/fuse.conf", "rb") as f:
1050
- allow_other = b"\nuser_allow_other" in f.read()
1051
- except:
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)