copyparty 1.19.16__py3-none-any.whl → 1.19.18__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.
Files changed (55) hide show
  1. copyparty/__init__.py +25 -1
  2. copyparty/__main__.py +32 -9
  3. copyparty/__version__.py +2 -2
  4. copyparty/authsrv.py +78 -31
  5. copyparty/bos/bos.py +5 -1
  6. copyparty/cfg.py +20 -0
  7. copyparty/ftpd.py +6 -4
  8. copyparty/httpcli.py +166 -45
  9. copyparty/httpsrv.py +2 -2
  10. copyparty/mtag.py +2 -2
  11. copyparty/smbd.py +1 -1
  12. copyparty/svchub.py +3 -0
  13. copyparty/tftpd.py +1 -1
  14. copyparty/up2k.py +39 -15
  15. copyparty/util.py +15 -5
  16. copyparty/web/a/partyfuse.py.gz +0 -0
  17. copyparty/web/a/u2c.py.gz +0 -0
  18. copyparty/web/a/webdav-cfg.txt.gz +0 -0
  19. copyparty/web/baguettebox.js.gz +0 -0
  20. copyparty/web/browser.css.gz +0 -0
  21. copyparty/web/browser.html +3 -0
  22. copyparty/web/browser.js.gz +0 -0
  23. copyparty/web/splash.html +3 -0
  24. copyparty/web/splash.js.gz +0 -0
  25. copyparty/web/svcs.html +1 -1
  26. copyparty/web/tl/chi.js.gz +0 -0
  27. copyparty/web/tl/cze.js.gz +0 -0
  28. copyparty/web/tl/deu.js.gz +0 -0
  29. copyparty/web/tl/epo.js.gz +0 -0
  30. copyparty/web/tl/fin.js.gz +0 -0
  31. copyparty/web/tl/fra.js.gz +0 -0
  32. copyparty/web/tl/grc.js.gz +0 -0
  33. copyparty/web/tl/ita.js.gz +0 -0
  34. copyparty/web/tl/kor.js.gz +0 -0
  35. copyparty/web/tl/nld.js.gz +0 -0
  36. copyparty/web/tl/nno.js.gz +0 -0
  37. copyparty/web/tl/nor.js.gz +0 -0
  38. copyparty/web/tl/pol.js.gz +0 -0
  39. copyparty/web/tl/por.js.gz +0 -0
  40. copyparty/web/tl/rus.js.gz +0 -0
  41. copyparty/web/tl/spa.js.gz +0 -0
  42. copyparty/web/tl/swe.js.gz +0 -0
  43. copyparty/web/tl/tur.js.gz +0 -0
  44. copyparty/web/tl/ukr.js.gz +0 -0
  45. copyparty/web/up2k.js.gz +0 -0
  46. copyparty/web/util.js.gz +0 -0
  47. {copyparty-1.19.16.dist-info → copyparty-1.19.18.dist-info}/METADATA +26 -4
  48. {copyparty-1.19.16.dist-info → copyparty-1.19.18.dist-info}/RECORD +52 -33
  49. copyparty/web/a/partyfuse.py +0 -947
  50. copyparty/web/a/u2c.py +0 -1718
  51. copyparty/web/a/webdav-cfg.bat +0 -45
  52. {copyparty-1.19.16.dist-info → copyparty-1.19.18.dist-info}/WHEEL +0 -0
  53. {copyparty-1.19.16.dist-info → copyparty-1.19.18.dist-info}/entry_points.txt +0 -0
  54. {copyparty-1.19.16.dist-info → copyparty-1.19.18.dist-info}/licenses/LICENSE +0 -0
  55. {copyparty-1.19.16.dist-info → copyparty-1.19.18.dist-info}/top_level.txt +0 -0
@@ -1,947 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- """partyfuse: remote copyparty as a local filesystem"""
4
- __author__ = "ed <copyparty@ocv.me>"
5
- __copyright__ = 2019
6
- __license__ = "MIT"
7
- __url__ = "https://github.com/9001/copyparty/"
8
-
9
- S_VERSION = "2.1"
10
- S_BUILD_DT = "2025-09-06"
11
-
12
- """
13
- mount a copyparty server (local or remote) as a filesystem
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
-
20
- usage:
21
- python partyfuse.py http://192.168.1.69:3923/ ./music
22
-
23
- dependencies:
24
- python3 -m pip install --user fusepy # or grab it from the connect page
25
- + on Linux: sudo apk add fuse
26
- + on Macos: https://osxfuse.github.io/
27
- + on Windows: https://github.com/billziss-gh/winfsp/releases/latest
28
-
29
- note:
30
- you probably want to run this on windows clients:
31
- https://github.com/9001/copyparty/blob/hovudstraum/contrib/explorer-nothumbs-nofoldertypes.reg
32
-
33
- get server cert:
34
- awk '/-BEGIN CERTIFICATE-/ {a=1} a; /-END CERTIFICATE-/{exit}' <(openssl s_client -connect 127.0.0.1:3923 </dev/null 2>/dev/null) >cert.pem
35
- """
36
-
37
-
38
- import argparse
39
- import calendar
40
- import codecs
41
- import errno
42
- import json
43
- import os
44
- import platform
45
- import re
46
- import stat
47
- import struct
48
- import sys
49
- import threading
50
- import time
51
- import traceback
52
- import urllib.parse
53
- from datetime import datetime, timezone
54
- from urllib.parse import quote_from_bytes as quote
55
- from urllib.parse import unquote_to_bytes as unquote
56
-
57
- import builtins
58
- import http.client
59
-
60
- WINDOWS = sys.platform == "win32"
61
- MACOS = platform.system() == "Darwin"
62
- UTC = timezone.utc
63
-
64
-
65
-
66
- def print(*args, **kwargs):
67
- try:
68
- builtins.print(*list(args), **kwargs)
69
- except:
70
- builtins.print(termsafe(" ".join(str(x) for x in args)), **kwargs)
71
-
72
-
73
- print(
74
- "{} v{} @ {}".format(
75
- platform.python_implementation(),
76
- ".".join([str(x) for x in sys.version_info]),
77
- sys.executable,
78
- )
79
- )
80
-
81
-
82
- def nullfun(*a):
83
- pass
84
-
85
-
86
- info = dbg = nullfun
87
- is_dbg = False
88
-
89
-
90
- try:
91
- from fuse import FUSE, FuseOSError, Operations
92
- except:
93
- if WINDOWS:
94
- libfuse = "install https://github.com/billziss-gh/winfsp/releases/latest"
95
- elif MACOS:
96
- libfuse = "install https://osxfuse.github.io/"
97
- else:
98
- libfuse = "apt install libfuse2\n modprobe fuse"
99
-
100
- m = """\033[33m
101
- could not import fuse; these may help:
102
- {} -m pip install --user fusepy
103
- {}
104
- \033[0m"""
105
- print(m.format(sys.executable, libfuse))
106
- raise
107
-
108
-
109
- def termsafe(txt):
110
- enc = sys.stdout.encoding
111
- try:
112
- return txt.encode(enc, "backslashreplace").decode(enc)
113
- except:
114
- return txt.encode(enc, "replace").decode(enc)
115
-
116
-
117
- def threadless_log(fmt, *a):
118
- fmt += "\n"
119
- print(fmt % a if a else fmt, end="")
120
-
121
-
122
- riced_tids = {}
123
-
124
-
125
- def rice_tid():
126
- tid = threading.current_thread().ident
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
134
-
135
-
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="")
139
-
140
-
141
- def register_wtf8():
142
- def wtf8_enc(text):
143
- return str(text).encode("utf-8", "surrogateescape"), len(text)
144
-
145
- def wtf8_dec(binary):
146
- return bytes(binary).decode("utf-8", "surrogateescape"), len(binary)
147
-
148
- def wtf8_search(encoding_name):
149
- return codecs.CodecInfo(wtf8_enc, wtf8_dec, name="wtf-8")
150
-
151
- codecs.register(wtf8_search)
152
-
153
-
154
- bad_good = {}
155
- good_bad = {}
156
-
157
-
158
- def enwin(txt):
159
- return "".join([bad_good.get(x, x) for x in txt])
160
-
161
-
162
- def dewin(txt):
163
- return "".join([good_bad.get(x, x) for x in txt])
164
-
165
-
166
- class RecentLog(object):
167
- def __init__(self, ar):
168
- self.ar = ar
169
- self.mtx = threading.Lock()
170
- self.f = open(ar.logf, "wb") if ar.logf else None
171
- self.q = []
172
-
173
- thr = threading.Thread(target=self.printer)
174
- thr.daemon = True
175
- thr.start()
176
-
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)
180
- if self.f:
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
- )
189
- self.f.write(fmsg.encode("utf-8"))
190
-
191
- with self.mtx:
192
- self.q.append(msg)
193
- if len(self.q) > 200:
194
- self.q = self.q[-50:]
195
-
196
- def printer(self):
197
- while True:
198
- time.sleep(0.05)
199
- with self.mtx:
200
- q = self.q
201
- if not q:
202
- continue
203
-
204
- self.q = []
205
-
206
- print("".join(q), end="")
207
-
208
-
209
- # [windows/cmd/cpy3] python dev\copyparty\bin\partyfuse.py q: http://192.168.1.159:1234/
210
- # [windows/cmd/msys2] C:\msys64\mingw64\bin\python3 dev\copyparty\bin\partyfuse.py q: http://192.168.1.159:1234/
211
- # [windows/mty/msys2] /mingw64/bin/python3 /c/Users/ed/dev/copyparty/bin/partyfuse.py q: http://192.168.1.159:1234/
212
- #
213
- # [windows] find /q/music/albums/Phant*24bit -printf '%s %p\n' | sort -n | tail -n 8 | sed -r 's/^[0-9]+ //' | while IFS= read -r x; do dd if="$x" of=/dev/null bs=4k count=8192 & done
214
- # [alpine] ll t; for x in t/2020_0724_16{2,3}*; do dd if="$x" of=/dev/null bs=4k count=10240 & done
215
- #
216
- # 72.4983 windows mintty msys2 fancy_log
217
- # 219.5781 windows cmd msys2 fancy_log
218
- # nope.avi windows cmd cpy3 fancy_log
219
- # 9.8817 windows mintty msys2 RecentLog 200 50 0.1
220
- # 10.2241 windows cmd cpy3 RecentLog 200 50 0.1
221
- # 9.8494 windows cmd msys2 RecentLog 200 50 0.1
222
- # 7.8061 windows mintty msys2 fancy_log <info-only>
223
- # 7.9961 windows mintty msys2 RecentLog <info-only>
224
- # 4.2603 alpine xfce4 cpy3 RecentLog
225
- # 4.1538 alpine xfce4 cpy3 fancy_log
226
- # 3.1742 alpine urxvt cpy3 fancy_log
227
-
228
-
229
- def get_tid():
230
- return threading.current_thread().ident
231
-
232
-
233
- def html_dec(txt):
234
- return (
235
- txt.replace("&lt;", "<")
236
- .replace("&gt;", ">")
237
- .replace("&quot;", '"')
238
- .replace("&#13;", "\r")
239
- .replace("&#10;", "\n")
240
- .replace("&amp;", "&")
241
- )
242
-
243
-
244
- class CacheNode(object):
245
- def __init__(self, tag, data):
246
- self.tag = tag
247
- self.data = data
248
- self.ts = time.time()
249
-
250
-
251
- class Gateway(object):
252
- def __init__(self, ar):
253
- zs = ar.base_url
254
- if "://" not in zs:
255
- zs = "http://" + zs
256
-
257
- self.base_url = zs
258
- self.password = ar.a
259
-
260
- ui = urllib.parse.urlparse(zs)
261
- self.web_root = ui.path.strip("/")
262
- self.SRS = "/%s/" % (self.web_root,) if self.web_root else "/"
263
- try:
264
- self.web_host, self.web_port = ui.netloc.split(":")
265
- self.web_port = int(self.web_port)
266
- except:
267
- self.web_host = ui.netloc
268
- if ui.scheme == "http":
269
- self.web_port = 80
270
- elif ui.scheme == "https":
271
- self.web_port = 443
272
- else:
273
- raise Exception("bad url?")
274
-
275
- self.ssl_context = None
276
- self.use_tls = ui.scheme.lower() == "https"
277
- if self.use_tls:
278
- import ssl
279
-
280
- if ar.td:
281
- self.ssl_context = ssl._create_unverified_context()
282
- elif ar.te:
283
- self.ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS)
284
- self.ssl_context.load_verify_locations(ar.te)
285
-
286
- self.conns = {}
287
-
288
- self.fsuf = "?raw"
289
- self.dsuf = "?ls&lt&dots"
290
-
291
-
292
- def quotep(self, path):
293
- path = path.encode("wtf-8")
294
- return quote(path, safe="/")
295
-
296
- def getconn(self, tid=None):
297
- tid = tid or get_tid()
298
- try:
299
- return self.conns[tid]
300
- except:
301
- info("new conn [{}] [{}]".format(self.web_host, self.web_port))
302
-
303
- args = {}
304
- if not self.use_tls:
305
- C = http.client.HTTPConnection
306
- else:
307
- C = http.client.HTTPSConnection
308
- if self.ssl_context:
309
- args = {"context": self.ssl_context}
310
-
311
- conn = C(self.web_host, self.web_port, timeout=260, **args)
312
-
313
- self.conns[tid] = conn
314
- return conn
315
-
316
- def closeconn(self, tid=None):
317
- tid = tid or get_tid()
318
- try:
319
- self.conns[tid].close()
320
- del self.conns[tid]
321
- except:
322
- pass
323
-
324
- def sendreq(self, meth, path, headers, **kwargs):
325
- tid = get_tid()
326
- if self.password:
327
- headers["PW"] = self.password
328
-
329
- try:
330
- c = self.getconn(tid)
331
- c.request(meth, path, headers=headers, **kwargs)
332
- return c.getresponse()
333
- except Exception as ex:
334
- info("HTTP %r", ex)
335
-
336
- self.closeconn(tid)
337
- try:
338
- c = self.getconn(tid)
339
- c.request(meth, path, headers=headers, **kwargs)
340
- return c.getresponse()
341
- except:
342
- info("http connection failed:\n" + traceback.format_exc())
343
- if self.use_tls and not self.ssl_context:
344
- import ssl
345
-
346
- cert = ssl.get_server_certificate((self.web_host, self.web_port))
347
- info("server certificate probably not trusted:\n" + cert)
348
-
349
- raise
350
-
351
- def listdir(self, path):
352
- if bad_good:
353
- path = dewin(path)
354
-
355
- zs = "%s%s/" if path else "%s%s"
356
- web_path = self.quotep(zs % (self.SRS, path)) + self.dsuf
357
- r = self.sendreq("GET", web_path, {})
358
- if r.status != 200:
359
- self.closeconn()
360
- info("http error %s reading dir %r", r.status, web_path)
361
- err = errno.ENOENT if r.status == 404 else errno.EIO
362
- raise FuseOSError(err)
363
-
364
- ctype = r.getheader("Content-Type", "")
365
- if ctype == "application/json":
366
- parser = self.parse_jls
367
- else:
368
- info("listdir on file (%s): %r", ctype, path)
369
- raise FuseOSError(errno.ENOENT)
370
-
371
- try:
372
- return parser(r)
373
- except:
374
- info("parser: %r\n%s", path, traceback.format_exc())
375
- raise FuseOSError(errno.EIO)
376
-
377
- def download_file_range(self, path, ofs1, ofs2):
378
- if bad_good:
379
- path = dewin(path)
380
-
381
- web_path = self.quotep("%s%s" % (self.SRS, path)) + self.fsuf
382
- hdr_range = "bytes=%d-%d" % (ofs1, ofs2 - 1)
383
-
384
- t = "DL %4.0fK\033[36m%9d-%-9d\033[0m%r"
385
- info(t, (ofs2 - ofs1) / 1024.0, ofs1, ofs2 - 1, path)
386
-
387
- r = self.sendreq("GET", web_path, {"Range": hdr_range})
388
- if r.status != http.client.PARTIAL_CONTENT:
389
- t = "http error %d reading file %r range %s in %s"
390
- info(t, r.status, web_path, hdr_range, rice_tid())
391
- self.closeconn()
392
- raise FuseOSError(errno.EIO)
393
-
394
- return r.read()
395
-
396
- def parse_jls(self, sck):
397
- rsp = b""
398
- while True:
399
- buf = sck.read(1024 * 32)
400
- if not buf:
401
- break
402
- rsp += buf
403
-
404
- rsp = json.loads(rsp.decode("utf-8"))
405
- ret = {}
406
- for statfun, nodes in [
407
- [self.stat_dir, rsp["dirs"]],
408
- [self.stat_file, rsp["files"]],
409
- ]:
410
- for n in nodes:
411
- fname = unquote(n["href"].split("?")[0]).rstrip(b"/").decode("wtf-8")
412
- if bad_good:
413
- fname = enwin(fname)
414
-
415
- ret[fname] = statfun(n["ts"], n["sz"])
416
-
417
- return ret
418
-
419
-
420
- def stat_dir(self, ts, sz):
421
- return {
422
- "st_mode": stat.S_IFDIR | 0o555,
423
- "st_uid": 1000,
424
- "st_gid": 1000,
425
- "st_size": sz,
426
- "st_atime": ts,
427
- "st_mtime": ts,
428
- "st_ctime": ts,
429
- "st_blocks": int((sz + 511) / 512),
430
- }
431
-
432
- def stat_file(self, ts, sz):
433
- return {
434
- "st_mode": stat.S_IFREG | 0o444,
435
- "st_uid": 1000,
436
- "st_gid": 1000,
437
- "st_size": sz,
438
- "st_atime": ts,
439
- "st_mtime": ts,
440
- "st_ctime": ts,
441
- "st_blocks": int((sz + 511) / 512),
442
- }
443
-
444
-
445
- class CPPF(Operations):
446
- def __init__(self, ar):
447
- self.gw = Gateway(ar)
448
- self.junk_fh_ctr = 3
449
- self.t_dircache = ar.cds
450
- self.n_dircache = ar.cdn
451
- self.n_filecache = ar.cf
452
-
453
- self.dircache = []
454
- self.dircache_mtx = threading.Lock()
455
-
456
- self.filecache = []
457
- self.filecache_mtx = threading.Lock()
458
-
459
- info("up")
460
-
461
- def _describe(self):
462
- msg = []
463
- with self.filecache_mtx:
464
- for n, cn in enumerate(self.filecache):
465
- cache_path, cache1 = cn.tag
466
- cache2 = cache1 + len(cn.data)
467
- t = "\n{:<2} {:>7} {:>10}:{:<9} {}".format(
468
- n,
469
- len(cn.data),
470
- cache1,
471
- cache2,
472
- cache_path.replace("\r", "\\r").replace("\n", "\\n"),
473
- )
474
- msg.append(t)
475
- return "".join(msg)
476
-
477
- def clean_dircache(self):
478
- """not threadsafe"""
479
- now = time.time()
480
- cutoff = 0
481
- for cn in self.dircache:
482
- if now - cn.ts <= self.t_dircache:
483
- break
484
- cutoff += 1
485
-
486
- if cutoff > 0:
487
- self.dircache = self.dircache[cutoff:]
488
- elif len(self.dircache) > self.n_dircache:
489
- self.dircache.pop(0)
490
-
491
- def get_cached_dir(self, dirpath):
492
- with self.dircache_mtx:
493
- for cn in self.dircache:
494
- if cn.tag == dirpath:
495
- if time.time() - cn.ts <= self.t_dircache:
496
- return cn
497
- break
498
- return None
499
-
500
-
501
- def get_cached_file(self, path, get1, get2, file_sz):
502
- car = None
503
- cdr = None
504
- ncn = -1
505
- if is_dbg:
506
- dbg("cache request %d:%d |%d|%s", get1, get2, file_sz, self._describe())
507
- with self.filecache_mtx:
508
- for cn in self.filecache:
509
- ncn += 1
510
-
511
- cache_path, cache1 = cn.tag
512
- if cache_path != path:
513
- continue
514
-
515
- cache2 = cache1 + len(cn.data)
516
- if get2 <= cache1 or get1 >= cache2:
517
- # request does not overlap with cached area at all
518
- continue
519
-
520
- if get1 < cache1 and get2 > cache2:
521
- # cached area does overlap, but must specifically contain
522
- # either the first or last byte in the requested range
523
- continue
524
-
525
- if get1 >= cache1 and get2 <= cache2:
526
- # keep cache entry alive by moving it to the end
527
- self.filecache = (
528
- self.filecache[:ncn] + self.filecache[ncn + 1 :] + [cn]
529
- )
530
- buf_ofs = get1 - cache1
531
- buf_end = buf_ofs + (get2 - get1)
532
- dbg(
533
- "found all (#%d %d:%d |%d|) [%d:%d] = %d",
534
- ncn,
535
- cache1,
536
- cache2,
537
- len(cn.data),
538
- buf_ofs,
539
- buf_end,
540
- buf_end - buf_ofs,
541
- )
542
- return cn.data[buf_ofs:buf_end]
543
-
544
- if get2 <= cache2:
545
- x = cn.data[: get2 - cache1]
546
- if not cdr or len(cdr) < len(x):
547
- dbg(
548
- "found cdr (#%d %d:%d |%d|) [:%d-%d] = [:%d] = %d",
549
- ncn,
550
- cache1,
551
- cache2,
552
- len(cn.data),
553
- get2,
554
- cache1,
555
- get2 - cache1,
556
- len(x),
557
- )
558
- cdr = x
559
-
560
- continue
561
-
562
- if get1 >= cache1:
563
- x = cn.data[-(max(0, cache2 - get1)) :]
564
- if not car or len(car) < len(x):
565
- dbg(
566
- "found car (#%d %d:%d |%d|) [-(%d-%d):] = [-%d:] = %d",
567
- ncn,
568
- cache1,
569
- cache2,
570
- len(cn.data),
571
- cache2,
572
- get1,
573
- cache2 - get1,
574
- len(x),
575
- )
576
- car = x
577
-
578
- continue
579
-
580
- msg = "cache fallthrough\n%d %d %d\n%d %d %d\n%d %d --\n%s" % (
581
- get1,
582
- get2,
583
- get2 - get1,
584
- cache1,
585
- cache2,
586
- cache2 - cache1,
587
- get1 - cache1,
588
- get2 - cache2,
589
- self._describe(),
590
- )
591
- info(msg)
592
- raise FuseOSError(errno.EIO)
593
-
594
- if car and cdr and len(car) + len(cdr) == get2 - get1:
595
- dbg("<cache> have both")
596
- return car + cdr
597
-
598
- elif cdr and (not car or len(car) < len(cdr)):
599
- h_end = get1 + (get2 - get1) - len(cdr)
600
- h_ofs = min(get1, h_end - 0x80000) # 512k
601
-
602
- if h_ofs < 0:
603
- h_ofs = 0
604
-
605
- buf_ofs = get1 - h_ofs
606
-
607
- if dbg:
608
- t = "<cache> cdr %d, car %d:%d |%d| [%d:]"
609
- dbg(t, len(cdr), h_ofs, h_end, h_end - h_ofs, buf_ofs)
610
-
611
- buf = self.gw.download_file_range(path, h_ofs, h_end)
612
- if len(buf) == h_end - h_ofs:
613
- ret = buf[buf_ofs:] + cdr
614
- else:
615
- ret = buf[get1 - h_ofs :]
616
- t = "remote truncated %d:%d to |%d|, will return |%d|"
617
- info(t, h_ofs, h_end, len(buf), len(ret))
618
-
619
- elif car:
620
- h_ofs = get1 + len(car)
621
- if get2 < 0x100000:
622
- # already cached from 0 to 64k, now do ~64k plus 1 MiB
623
- h_end = max(get2, h_ofs + 0x100000) # 1m
624
- else:
625
- # after 1 MiB, bump window to 8 MiB
626
- h_end = max(get2, h_ofs + 0x800000) # 8m
627
-
628
- if h_end > file_sz:
629
- h_end = file_sz
630
-
631
- buf_ofs = (get2 - get1) - len(car)
632
-
633
- t = "<cache> car %d, cdr %d:%d |%d| [:%d]"
634
- dbg(t, len(car), h_ofs, h_end, h_end - h_ofs, buf_ofs)
635
-
636
- buf = self.gw.download_file_range(path, h_ofs, h_end)
637
- ret = car + buf[:buf_ofs]
638
-
639
- else:
640
- if get2 - get1 < 0x500000: # 5m
641
- # unless the request is for the last n bytes of the file,
642
- # grow the start to cache some stuff around the range
643
- if get2 < file_sz - 1:
644
- h_ofs = get1 - 0x40000 # 256k
645
- else:
646
- h_ofs = get1 - 0x10000 # 64k
647
-
648
- # likewise grow the end unless start is 0
649
- if get1 >= 0x100000:
650
- h_end = get2 + 0x400000 # 4m
651
- elif get1 > 0:
652
- h_end = get2 + 0x100000 # 1m
653
- else:
654
- h_end = get2 + 0x10000 # 64k
655
- else:
656
- # big enough, doesn't need pads
657
- h_ofs = get1
658
- h_end = get2
659
-
660
- if h_ofs < 0:
661
- h_ofs = 0
662
-
663
- if h_end > file_sz:
664
- h_end = file_sz
665
-
666
- buf_ofs = get1 - h_ofs
667
- buf_end = buf_ofs + get2 - get1
668
-
669
- t = "<cache> %d:%d |%d| [%d:%d]"
670
- dbg(t, h_ofs, h_end, h_end - h_ofs, buf_ofs, buf_end)
671
-
672
- buf = self.gw.download_file_range(path, h_ofs, h_end)
673
- ret = buf[buf_ofs:buf_end]
674
-
675
- cn = CacheNode([path, h_ofs], buf)
676
- with self.filecache_mtx:
677
- if len(self.filecache) >= self.n_filecache:
678
- self.filecache = self.filecache[1:] + [cn]
679
- else:
680
- self.filecache.append(cn)
681
-
682
- return ret
683
-
684
- def _readdir(self, path, fh=None):
685
- dbg("dircache miss")
686
- ret = self.gw.listdir(path)
687
- if not self.n_dircache:
688
- return ret
689
-
690
- with self.dircache_mtx:
691
- cn = CacheNode(path, ret)
692
- self.dircache.append(cn)
693
- self.clean_dircache()
694
-
695
- return ret
696
-
697
- def readdir(self, path, fh=None):
698
- dbg("readdir %r [%s]", path, fh)
699
- path = path.strip("/")
700
- cn = self.get_cached_dir(path)
701
- if cn:
702
- ret = cn.data
703
- else:
704
- ret = self._readdir(path, fh)
705
- return [".", ".."] + list(ret)
706
-
707
- def read(self, path, length, offset, fh=None):
708
- req_max = 1024 * 1024 * 8
709
- cache_max = 1024 * 1024 * 2
710
- if length > req_max:
711
- # windows actually doing 240 MiB read calls, sausage
712
- info("truncate |%d| to %dMiB", length, req_max >> 20)
713
- length = req_max
714
-
715
- path = path.strip("/")
716
- ofs2 = offset + length
717
- file_sz = self.getattr(path)["st_size"]
718
- dbg("read %r |%d| %d:%d max %d", path, length, offset, ofs2, file_sz)
719
-
720
- if ofs2 > file_sz:
721
- ofs2 = file_sz
722
- dbg("truncate to |%d| :%d", ofs2 - offset, ofs2)
723
-
724
- if file_sz == 0 or offset >= ofs2:
725
- return b""
726
-
727
- if self.n_filecache and length <= cache_max:
728
- ret = self.get_cached_file(path, offset, ofs2, file_sz)
729
- else:
730
- ret = self.gw.download_file_range(path, offset, ofs2)
731
-
732
- return ret
733
-
734
-
735
- def getattr(self, path, fh=None):
736
- dbg("getattr %r", path)
737
- if WINDOWS:
738
- path = enwin(path) # windows occasionally decodes f0xx to xx
739
-
740
- path = path.strip("/")
741
- if not path:
742
- ret = self.gw.stat_dir(time.time(), 4096)
743
- dbg("/=%r", ret)
744
- return ret
745
-
746
- try:
747
- dirpath, fname = path.rsplit("/", 1)
748
- except:
749
- dirpath = ""
750
- fname = path
751
-
752
- cn = self.get_cached_dir(dirpath)
753
- if cn:
754
- dents = cn.data
755
- else:
756
- dents = self._readdir(dirpath)
757
-
758
- try:
759
- ret = dents[fname]
760
- dbg("s=%r", ret)
761
- return ret
762
- except:
763
- pass
764
-
765
- fun = info
766
- if MACOS and path.split("/")[-1].startswith("._"):
767
- fun = dbg
768
-
769
- fun("=ENOENT %r", path)
770
- raise FuseOSError(errno.ENOENT)
771
-
772
- access = None
773
- flush = None
774
- getxattr = None
775
- listxattr = None
776
- open = None
777
- opendir = None
778
- release = None
779
- releasedir = None
780
- statfs = None
781
-
782
-
783
- if sys.platform == "win32":
784
- # quick compat for /mingw64/bin/python3 (msys2)
785
- def _open(self, path):
786
- try:
787
- x = self.getattr(path)
788
- if x["st_mode"] <= 0:
789
- raise Exception()
790
-
791
- self.junk_fh_ctr += 1
792
- if self.junk_fh_ctr > 32000: # TODO untested
793
- self.junk_fh_ctr = 4
794
-
795
- return self.junk_fh_ctr
796
-
797
- except Exception as ex:
798
- info("open ERR %r", ex)
799
- raise FuseOSError(errno.ENOENT)
800
-
801
- def open(self, path, flags):
802
- dbg("open %r [%s]", path, flags)
803
- return self._open(path)
804
-
805
- def opendir(self, path):
806
- dbg("opendir %r", path)
807
- return self._open(path)
808
-
809
- def flush(self, path, fh):
810
- dbg("flush %r [%s]", path, fh)
811
-
812
- def release(self, ino, fi):
813
- dbg("release %r [%s]", ino, fi)
814
-
815
- def releasedir(self, ino, fi):
816
- dbg("releasedir %r [%s]", ino, fi)
817
-
818
- def access(self, path, mode):
819
- dbg("access %r [%s]", path, mode)
820
- try:
821
- x = self.getattr(path)
822
- if x["st_mode"] <= 0:
823
- raise Exception()
824
- except:
825
- raise FuseOSError(errno.ENOENT)
826
-
827
-
828
- class TheArgparseFormatter(
829
- argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter
830
- ):
831
- pass
832
-
833
-
834
- def main():
835
- global info, dbg, is_dbg
836
- time.strptime("19970815", "%Y%m%d") # python#7980
837
-
838
- ver = "{0}, v{1}".format(S_BUILD_DT, S_VERSION)
839
- if "--version" in sys.argv:
840
- print("partyfuse", ver)
841
- return
842
-
843
- # filecache helps for reads that are ~64k or smaller;
844
- # windows likes to use 4k and 64k so cache is important,
845
- # linux generally does 128k so the cache is still nice,
846
- # value is numChunks (1~8M each) to keep in the cache
847
- nf = 12
848
-
849
- # dircache is always a boost,
850
- # only want to disable it for tests etc,
851
- cdn = 24 # max num dirs; keep larger than max dir depth; 0=disable
852
- cds = 1 # numsec until an entry goes stale
853
-
854
- where = "local directory"
855
- if WINDOWS:
856
- where += " or DRIVE:"
857
-
858
- ex_pre = "\n " + os.path.basename(__file__) + " "
859
- examples = ["http://192.168.1.69:3923/music/ ./music"]
860
- if WINDOWS:
861
- examples.append("http://192.168.1.69:3923/music/ M:")
862
-
863
- epi = "example:" + ex_pre + ex_pre.join(examples)
864
- epi += """\n
865
- NOTE: if server has --usernames enabled, then password is "username:password"
866
- """
867
-
868
- ap = argparse.ArgumentParser(
869
- formatter_class=TheArgparseFormatter,
870
- description="mount a copyparty server as a local filesystem -- " + ver,
871
- epilog=epi,
872
- )
873
- # fmt: off
874
- ap.add_argument("base_url", type=str, help="remote copyparty URL to mount")
875
- ap.add_argument("local_path", type=str, help=where + " to mount it on")
876
- ap.add_argument("-a", metavar="PASSWORD", help="password or $filepath")
877
-
878
-
879
- ap2 = ap.add_argument_group("https/TLS")
880
- ap2.add_argument("-te", metavar="PEMFILE", help="certificate to expect/verify")
881
- ap2.add_argument("-td", action="store_true", help="disable certificate check")
882
-
883
- ap2 = ap.add_argument_group("cache/perf")
884
- ap2.add_argument("-cdn", metavar="DIRS", type=float, default=cdn, help="directory-cache, max num dirs; 0=disable")
885
- ap2.add_argument("-cds", metavar="SECS", type=float, default=cds, help="directory-cache, expiration time")
886
- ap2.add_argument("-cf", metavar="BLOCKS", type=int, default=nf, help="file cache; each block is <= 1 MiB")
887
-
888
- ap2 = ap.add_argument_group("logging")
889
- ap2.add_argument("-q", action="store_true", help="quiet")
890
- ap2.add_argument("-d", action="store_true", help="debug/verbose")
891
- ap2.add_argument("--slowterm", action="store_true", help="only most recent msgs; good for windows")
892
- ap2.add_argument("--logf", metavar="FILE", type=str, default="", help="log to FILE; enables --slowterm")
893
-
894
- ap2 = ap.add_argument_group("fuse")
895
- ap2.add_argument("--oth", action="store_true", help="tell FUSE to '-o allow_other'")
896
- ap2.add_argument("--nonempty", action="store_true", help="tell FUSE to '-o nonempty'")
897
-
898
- ar = ap.parse_args()
899
- # fmt: on
900
-
901
- if ar.logf:
902
- ar.slowterm = True
903
-
904
- # windows terminals are slow (cmd.exe, mintty)
905
- # otoh fancy_log beats RecentLog on linux
906
- logger = RecentLog(ar).put if ar.slowterm else fancy_log
907
- if ar.d:
908
- info = logger
909
- dbg = logger
910
- is_dbg = True
911
- elif not ar.q:
912
- info = logger
913
-
914
- if ar.a and ar.a.startswith("$"):
915
- fn = ar.a[1:]
916
- info("reading password from file %r", fn)
917
- with open(fn, "rb") as f:
918
- ar.a = f.read().decode("utf-8").strip()
919
-
920
- if WINDOWS:
921
- os.system("rem")
922
-
923
- for ch in '<>:"\\|?*':
924
- # microsoft maps illegal characters to f0xx
925
- # (e000 to f8ff is basic-plane private-use)
926
- bad_good[ch] = chr(ord(ch) + 0xF000)
927
-
928
- for n in range(0, 0x100):
929
- # map surrogateescape to another private-use area
930
- bad_good[chr(n + 0xDC00)] = chr(n + 0xF100)
931
-
932
- for k, v in bad_good.items():
933
- good_bad[v] = k
934
-
935
- register_wtf8()
936
-
937
- args = {"foreground": True, "nothreads": True}
938
- if ar.oth:
939
- args["allow_other"] = True
940
- if ar.nonempty:
941
- args["nonempty"] = True
942
-
943
- FUSE(CPPF(ar), ar.local_path, encoding="wtf-8", **args)
944
-
945
-
946
- if __name__ == "__main__":
947
- main()