copyparty 1.15.9__py3-none-any.whl → 1.15.10__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/__main__.py CHANGED
@@ -1299,6 +1299,7 @@ def add_logging(ap):
1299
1299
  ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs")
1300
1300
  ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling")
1301
1301
  ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
1302
+ ap2.add_argument("--ohead", metavar="HEADER", type=u, action='append', help="print response \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
1302
1303
  ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|\?th=[wj]$|/\.(_|ql_|DS_Store$|localized$)", help="dont log URLs matching regex \033[33mRE\033[0m")
1303
1304
 
1304
1305
 
@@ -1452,6 +1453,7 @@ def add_ui(ap, retry):
1452
1453
  ap2.add_argument("--pb-url", metavar="URL", type=u, default="https://github.com/9001/copyparty", help="powered-by link; disable with \033[33m-np\033[0m")
1453
1454
  ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)")
1454
1455
  ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
1456
+ ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
1455
1457
  ap2.add_argument("--md-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to ALLOW for README.md docs (volflag=md_sbf); see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox")
1456
1458
  ap2.add_argument("--lg-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to ALLOW for prologue/epilogue docs (volflag=lg_sbf)")
1457
1459
  ap2.add_argument("--no-sb-md", action="store_true", help="don't sandbox README/PREADME.md documents (volflags: no_sb_md | sb_md)")
@@ -1749,6 +1751,9 @@ def main(argv = None) :
1749
1751
  if al.ihead:
1750
1752
  al.ihead = [x.lower() for x in al.ihead]
1751
1753
 
1754
+ if al.ohead:
1755
+ al.ohead = [x.lower() for x in al.ohead]
1756
+
1752
1757
  if HAVE_SSL:
1753
1758
  if al.ssl_ver:
1754
1759
  configure_ssl_ver(al)
copyparty/__version__.py CHANGED
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 15, 9)
3
+ VERSION = (1, 15, 10)
4
4
  CODENAME = "fill the drives"
5
- BUILD_DT = (2024, 10, 18)
5
+ BUILD_DT = (2024, 10, 26)
6
6
 
7
7
  S_VERSION = ".".join(map(str, VERSION))
8
8
  S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
copyparty/authsrv.py CHANGED
@@ -545,15 +545,14 @@ class VFS(object):
545
545
  return self._get_dbv(vrem)
546
546
 
547
547
  shv, srem = src
548
- return shv, vjoin(srem, vrem)
548
+ return shv._get_dbv(vjoin(srem, vrem))
549
549
 
550
550
  def _get_dbv(self, vrem ) :
551
551
  dbv = self.dbv
552
552
  if not dbv:
553
553
  return self, vrem
554
554
 
555
- tv = [self.vpath[len(dbv.vpath) :].lstrip("/"), vrem]
556
- vrem = "/".join([x for x in tv if x])
555
+ vrem = vjoin(self.vpath[len(dbv.vpath) :].lstrip("/"), vrem)
557
556
  return dbv, vrem
558
557
 
559
558
  def canonical(self, rem , resolve = True) :
@@ -2633,7 +2632,7 @@ class AuthSrv(object):
2633
2632
  ]
2634
2633
 
2635
2634
  csv = set("i p th_covers zm_on zm_off zs_on zs_off".split())
2636
- zs = "c ihead mtm mtp on403 on404 xad xar xau xiu xban xbd xbr xbu xm"
2635
+ zs = "c ihead ohead mtm mtp on403 on404 xad xar xau xiu xban xbd xbr xbu xm"
2637
2636
  lst = set(zs.split())
2638
2637
  askip = set("a v c vc cgen exp_lg exp_md theme".split())
2639
2638
  fskip = set("exp_lg exp_md mv_re_r mv_re_t rm_re_r rm_re_t".split())
copyparty/httpcli.py CHANGED
@@ -2,7 +2,6 @@
2
2
  from __future__ import print_function, unicode_literals
3
3
 
4
4
  import argparse # typechk
5
- import calendar
6
5
  import copy
7
6
  import errno
8
7
  import gzip
@@ -19,7 +18,6 @@ import threading # typechk
19
18
  import time
20
19
  import uuid
21
20
  from datetime import datetime
22
- from email.utils import parsedate
23
21
  from operator import itemgetter
24
22
 
25
23
  import jinja2 # typechk
@@ -107,6 +105,7 @@ from .util import (
107
105
  unquotep,
108
106
  vjoin,
109
107
  vol_san,
108
+ vroots,
110
109
  vsplit,
111
110
  wrename,
112
111
  wunlink,
@@ -123,6 +122,11 @@ _ = (argparse, threading)
123
122
 
124
123
  NO_CACHE = {"Cache-Control": "no-cache"}
125
124
 
125
+ ALL_COOKIES = "k304 no304 js idxh dots cppwd cppws".split()
126
+
127
+ H_CONN_KEEPALIVE = "Connection: Keep-Alive"
128
+ H_CONN_CLOSE = "Connection: Close"
129
+
126
130
  LOGUES = [[0, ".prologue.html"], [1, ".epilogue.html"]]
127
131
 
128
132
  READMES = [[0, ["preadme.md", "PREADME.md"]], [1, ["readme.md", "README.md"]]]
@@ -787,11 +791,11 @@ class HttpCli(object):
787
791
 
788
792
  def k304(self) :
789
793
  k304 = self.cookies.get("k304")
790
- return (
791
- k304 == "y"
792
- or (self.args.k304 == 2 and k304 != "n")
793
- or ("; Trident/" in self.ua and not k304)
794
- )
794
+ return k304 == "y" or (self.args.k304 == 2 and k304 != "n")
795
+
796
+ def no304(self) :
797
+ no304 = self.cookies.get("no304")
798
+ return no304 == "y" or (self.args.no304 == 2 and no304 != "n")
795
799
 
796
800
  def _build_html_head(self, maybe_html , kv ) :
797
801
  html = str(maybe_html)
@@ -826,25 +830,28 @@ class HttpCli(object):
826
830
  ) :
827
831
  response = ["%s %s %s" % (self.http_ver, status, HTTPCODE[status])]
828
832
 
829
- if length is not None:
830
- response.append("Content-Length: " + unicode(length))
831
-
832
- if status == 304 and self.k304():
833
- self.keepalive = False
834
-
835
- # close if unknown length, otherwise take client's preference
836
- response.append("Connection: " + ("Keep-Alive" if self.keepalive else "Close"))
837
- response.append("Date: " + formatdate())
838
-
839
833
  # headers{} overrides anything set previously
840
834
  if headers:
841
835
  self.out_headers.update(headers)
842
836
 
843
- # default to utf8 html if no content-type is set
844
- if not mime:
845
- mime = self.out_headers.get("Content-Type") or "text/html; charset=utf-8"
837
+ if status == 304:
838
+ self.out_headers.pop("Content-Length", None)
839
+ self.out_headers.pop("Content-Type", None)
840
+ self.out_headerlist.clear()
841
+ if self.k304():
842
+ self.keepalive = False
843
+ else:
844
+ if length is not None:
845
+ response.append("Content-Length: " + unicode(length))
846
+
847
+ if mime:
848
+ self.out_headers["Content-Type"] = mime
849
+ elif "Content-Type" not in self.out_headers:
850
+ self.out_headers["Content-Type"] = "text/html; charset=utf-8"
846
851
 
847
- self.out_headers["Content-Type"] = mime
852
+ # close if unknown length, otherwise take client's preference
853
+ response.append(H_CONN_KEEPALIVE if self.keepalive else H_CONN_CLOSE)
854
+ response.append("Date: " + formatdate())
848
855
 
849
856
  for k, zs in list(self.out_headers.items()) + self.out_headerlist:
850
857
  response.append("%s: %s" % (k, zs))
@@ -858,6 +865,19 @@ class HttpCli(object):
858
865
  self.cbonk(self.conn.hsrv.gmal, zs, "cc_hdr", "Cc in out-hdr")
859
866
  raise Pebkac(999)
860
867
 
868
+ if self.args.ohead and self.do_log:
869
+ keys = self.args.ohead
870
+ if "*" in keys:
871
+ lines = response[1:]
872
+ else:
873
+ lines = []
874
+ for zs in response[1:]:
875
+ if zs.split(":")[0].lower() in keys:
876
+ lines.append(zs)
877
+ for zs in lines:
878
+ hk, hv = zs.split(": ")
879
+ self.log("[O] {}: \033[33m[{}]".format(hk, hv), 5)
880
+
861
881
  response.append("\r\n")
862
882
  try:
863
883
  self.s.sendall("\r\n".join(response).encode("utf-8"))
@@ -937,13 +957,14 @@ class HttpCli(object):
937
957
 
938
958
  lines = [
939
959
  "%s %s %s" % (self.http_ver or "HTTP/1.1", status, HTTPCODE[status]),
940
- "Connection: Close",
960
+ H_CONN_CLOSE,
941
961
  ]
942
962
 
943
963
  if body:
944
964
  lines.append("Content-Length: " + unicode(len(body)))
945
965
 
946
- self.s.sendall("\r\n".join(lines).encode("utf-8") + b"\r\n\r\n" + body)
966
+ lines.append("\r\n")
967
+ self.s.sendall("\r\n".join(lines).encode("utf-8") + body)
947
968
 
948
969
  def urlq(self, add , rm ) :
949
970
  """
@@ -1177,12 +1198,6 @@ class HttpCli(object):
1177
1198
  if "stack" in self.uparam:
1178
1199
  return self.tx_stack()
1179
1200
 
1180
- if "ups" in self.uparam:
1181
- return self.tx_ups()
1182
-
1183
- if "k304" in self.uparam:
1184
- return self.set_k304()
1185
-
1186
1201
  if "setck" in self.uparam:
1187
1202
  return self.setck()
1188
1203
 
@@ -1198,6 +1213,10 @@ class HttpCli(object):
1198
1213
  if "h" in self.uparam:
1199
1214
  return self.tx_mounts()
1200
1215
 
1216
+ if "ups" in self.uparam:
1217
+ # vpath is used for share translation
1218
+ return self.tx_ups()
1219
+
1201
1220
  if "rss" in self.uparam:
1202
1221
  return self.tx_rss()
1203
1222
 
@@ -2380,6 +2399,15 @@ class HttpCli(object):
2380
2399
  if "purl" in ret:
2381
2400
  ret["purl"] = self.args.SR + ret["purl"]
2382
2401
 
2402
+ if self.args.shr and self.vpath.startswith(self.args.shr1):
2403
+ # strip common suffix (uploader's folder structure)
2404
+ vp_req, vp_vfs = vroots(self.vpath, vjoin(dbv.vpath, vrem))
2405
+ if not ret["purl"].startswith(vp_vfs):
2406
+ t = "share-mapping failed; req=[%s] dbv=[%s] vrem=[%s] n1=[%s] n2=[%s] purl=[%s]"
2407
+ zt = (self.vpath, dbv.vpath, vrem, vp_req, vp_vfs, ret["purl"])
2408
+ raise Pebkac(500, t % zt)
2409
+ ret["purl"] = vp_req + ret["purl"][len(vp_vfs) :]
2410
+
2383
2411
  ret = json.dumps(ret)
2384
2412
  self.log(ret)
2385
2413
  self.reply(ret.encode("utf-8"), mime="application/json")
@@ -2472,7 +2500,11 @@ class HttpCli(object):
2472
2500
  chashes.append(siblings[n : n + clen])
2473
2501
 
2474
2502
  vfs, _ = self.asrv.vfs.get(self.vpath, self.uname, False, True)
2475
- ptop = (vfs.dbv or vfs).realpath
2503
+ ptop = vfs.get_dbv("")[0].realpath
2504
+ # if this is a share, then get_dbv has been overridden to return
2505
+ # the dbv (which does not exist as a property). And its realpath
2506
+ # could point into the middle of its origin vfs node, meaning it
2507
+ # is not necessarily registered with up2k, so get_dbv is crucial
2476
2508
 
2477
2509
  broker = self.conn.hsrv.broker
2478
2510
  x = broker.ask("up2k.handle_chunks", ptop, wark, chashes)
@@ -3378,25 +3410,29 @@ class HttpCli(object):
3378
3410
  self.reply(response.encode("utf-8"))
3379
3411
  return True
3380
3412
 
3381
- def _chk_lastmod(self, file_ts ) :
3413
+ def _chk_lastmod(self, file_ts ) :
3414
+ # ret: lastmod, do_send, can_range
3382
3415
  file_lastmod = formatdate(file_ts)
3383
- cli_lastmod = self.headers.get("if-modified-since")
3384
- if cli_lastmod:
3385
- try:
3386
- # some browser append "; length=573"
3387
- cli_lastmod = cli_lastmod.split(";")[0].strip()
3388
- cli_dt = parsedate(cli_lastmod)
3389
- cli_ts = calendar.timegm(cli_dt)
3390
- return file_lastmod, int(file_ts) > int(cli_ts)
3391
- except Exception as ex:
3392
- self.log(
3393
- "lastmod {}\nremote: [{}]\n local: [{}]".format(
3394
- repr(ex), cli_lastmod, file_lastmod
3395
- )
3396
- )
3397
- return file_lastmod, file_lastmod != cli_lastmod
3416
+ c_ifrange = self.headers.get("if-range")
3417
+ c_lastmod = self.headers.get("if-modified-since")
3418
+
3419
+ if not c_ifrange and not c_lastmod:
3420
+ return file_lastmod, True, True
3421
+
3422
+ if c_ifrange and c_ifrange != file_lastmod:
3423
+ t = "sending entire file due to If-Range; cli(%s) file(%s)"
3424
+ self.log(t % (c_ifrange, file_lastmod), 6)
3425
+ return file_lastmod, True, False
3426
+
3427
+ do_send = c_lastmod != file_lastmod
3428
+ if do_send and c_lastmod:
3429
+ t = "sending body due to If-Modified-Since cli(%s) file(%s)"
3430
+ self.log(t % (c_lastmod, file_lastmod), 6)
3431
+ elif not do_send and self.no304():
3432
+ do_send = True
3433
+ self.log("sending body due to no304")
3398
3434
 
3399
- return file_lastmod, True
3435
+ return file_lastmod, do_send, True
3400
3436
 
3401
3437
  def _use_dirkey(self, vn , ap ) :
3402
3438
  if self.can_read or not self.can_get:
@@ -3546,7 +3582,7 @@ class HttpCli(object):
3546
3582
  # if-modified
3547
3583
 
3548
3584
  if file_ts > 0:
3549
- file_lastmod, do_send = self._chk_lastmod(int(file_ts))
3585
+ file_lastmod, do_send, _ = self._chk_lastmod(int(file_ts))
3550
3586
  self.out_headers["Last-Modified"] = file_lastmod
3551
3587
  if not do_send:
3552
3588
  status = 304
@@ -3708,7 +3744,7 @@ class HttpCli(object):
3708
3744
  #
3709
3745
  # if-modified
3710
3746
 
3711
- file_lastmod, do_send = self._chk_lastmod(int(file_ts))
3747
+ file_lastmod, do_send, can_range = self._chk_lastmod(int(file_ts))
3712
3748
  self.out_headers["Last-Modified"] = file_lastmod
3713
3749
  if not do_send:
3714
3750
  status = 304
@@ -3752,7 +3788,14 @@ class HttpCli(object):
3752
3788
 
3753
3789
  # let's not support 206 with compression
3754
3790
  # and multirange / multipart is also not-impl (mostly because calculating contentlength is a pain)
3755
- if do_send and not is_compressed and hrange and file_sz and "," not in hrange:
3791
+ if (
3792
+ do_send
3793
+ and not is_compressed
3794
+ and hrange
3795
+ and can_range
3796
+ and file_sz
3797
+ and "," not in hrange
3798
+ ):
3756
3799
  try:
3757
3800
  if not hrange.lower().startswith("bytes"):
3758
3801
  raise Exception()
@@ -4223,7 +4266,7 @@ class HttpCli(object):
4223
4266
  sz_md = len(lead) + len(fullfile)
4224
4267
 
4225
4268
  file_ts = int(max(ts_md, self.E.t0))
4226
- file_lastmod, do_send = self._chk_lastmod(file_ts)
4269
+ file_lastmod, do_send, _ = self._chk_lastmod(file_ts)
4227
4270
  self.out_headers["Last-Modified"] = file_lastmod
4228
4271
  self.out_headers.update(NO_CACHE)
4229
4272
  status = 200 if do_send else 304
@@ -4409,7 +4452,7 @@ class HttpCli(object):
4409
4452
  rvol=rvol,
4410
4453
  wvol=wvol,
4411
4454
  avol=avol,
4412
- in_shr=self.args.shr and self.vpath.startswith(self.args.shr[1:]),
4455
+ in_shr=self.args.shr and self.vpath.startswith(self.args.shr1),
4413
4456
  vstate=vstate,
4414
4457
  ups=ups,
4415
4458
  scanning=vs["scanning"],
@@ -4419,7 +4462,9 @@ class HttpCli(object):
4419
4462
  dbwt=vs["dbwt"],
4420
4463
  url_suf=suf,
4421
4464
  k304=self.k304(),
4465
+ no304=self.no304(),
4422
4466
  k304vis=self.args.k304 > 0,
4467
+ no304vis=self.args.no304 > 0,
4423
4468
  ver=S_VERSION if self.args.ver else "",
4424
4469
  chpw=self.args.chpw and self.uname != "*",
4425
4470
  ahttps="" if self.is_https else "https://" + self.host + self.req,
@@ -4427,29 +4472,21 @@ class HttpCli(object):
4427
4472
  self.reply(html.encode("utf-8"))
4428
4473
  return True
4429
4474
 
4430
- def set_k304(self) :
4431
- v = self.uparam["k304"].lower()
4432
- if v in "yn":
4433
- dur = 86400 * 299
4434
- else:
4435
- dur = 0
4436
- v = "x"
4437
-
4438
- ck = gencookie("k304", v, self.args.R, False, dur)
4439
- self.out_headerlist.append(("Set-Cookie", ck))
4440
- self.redirect("", "?h#cc")
4441
- return True
4442
-
4443
4475
  def setck(self) :
4444
4476
  k, v = self.uparam["setck"].split("=", 1)
4445
- t = 0 if v == "" else 86400 * 299
4477
+ t = 0 if v in ("", "x") else 86400 * 299
4446
4478
  ck = gencookie(k, v, self.args.R, False, t)
4447
4479
  self.out_headerlist.append(("Set-Cookie", ck))
4448
- self.reply(b"o7\n")
4480
+ if "cc" in self.ouparam:
4481
+ self.redirect("", "?h#cc")
4482
+ else:
4483
+ self.reply(b"o7\n")
4449
4484
  return True
4450
4485
 
4451
4486
  def set_cfg_reset(self) :
4452
- for k in ("k304", "js", "idxh", "dots", "cppwd", "cppws"):
4487
+ for k in ALL_COOKIES:
4488
+ if k not in self.cookies:
4489
+ continue
4453
4490
  cookie = gencookie(k, "x", self.args.R, False)
4454
4491
  self.out_headerlist.append(("Set-Cookie", cookie))
4455
4492
 
@@ -4479,7 +4516,7 @@ class HttpCli(object):
4479
4516
 
4480
4517
  t = t.format(self.args.SR)
4481
4518
  qv = quotep(self.vpaths) + self.ourlq()
4482
- in_shr = self.args.shr and self.vpath.startswith(self.args.shr[1:])
4519
+ in_shr = self.args.shr and self.vpath.startswith(self.args.shr1)
4483
4520
  html = self.j2s("splash", this=self, qvpath=qv, in_shr=in_shr, msg=t)
4484
4521
  self.reply(html.encode("utf-8"), status=rc)
4485
4522
  return True
@@ -4642,6 +4679,11 @@ class HttpCli(object):
4642
4679
  lm = "ups [{}]".format(filt)
4643
4680
  self.log(lm)
4644
4681
 
4682
+ if self.args.shr and self.vpath.startswith(self.args.shr1):
4683
+ shr_dbv, shr_vrem = self.vn.get_dbv(self.rem)
4684
+ else:
4685
+ shr_dbv = None
4686
+
4645
4687
  ret = []
4646
4688
  t0 = time.time()
4647
4689
  lim = time.time() - self.args.unpost
@@ -4662,7 +4704,12 @@ class HttpCli(object):
4662
4704
  else:
4663
4705
  allvols = list(self.asrv.vfs.all_vols.values())
4664
4706
 
4665
- allvols = [x for x in allvols if "e2d" in x.flags]
4707
+ allvols = [
4708
+ x
4709
+ for x in allvols
4710
+ if "e2d" in x.flags
4711
+ and ("*" in x.axs.uwrite or self.uname in x.axs.uwrite or x == shr_dbv)
4712
+ ]
4666
4713
 
4667
4714
  for vol in allvols:
4668
4715
  cur = idx.get_cur(vol)
@@ -4712,6 +4759,15 @@ class HttpCli(object):
4712
4759
 
4713
4760
  ret = ret[:2000]
4714
4761
 
4762
+ if shr_dbv:
4763
+ # translate vpaths from share-target to share-url
4764
+ # to satisfy access checks
4765
+ vp_shr, vp_vfs = vroots(self.vpath, vjoin(shr_dbv.vpath, shr_vrem))
4766
+ for v in ret:
4767
+ vp = v["vp"]
4768
+ if vp.startswith(vp_vfs):
4769
+ v["vp"] = vp_shr + vp[len(vp_vfs) :]
4770
+
4715
4771
  if self.is_vproxied:
4716
4772
  for v in ret:
4717
4773
  v["vp"] = self.args.SR + v["vp"]
@@ -4841,7 +4897,7 @@ class HttpCli(object):
4841
4897
  if m:
4842
4898
  raise Pebkac(400, "sharekey has illegal character [%s]" % (m[1],))
4843
4899
 
4844
- if vp.startswith(self.args.shr[1:]):
4900
+ if vp.startswith(self.args.shr1):
4845
4901
  raise Pebkac(400, "yo dawg...")
4846
4902
 
4847
4903
  cur = idx.get_shr()
copyparty/svchub.py CHANGED
@@ -217,13 +217,14 @@ class SvcHub(object):
217
217
  args.chpw_no = noch
218
218
 
219
219
  if args.ipu:
220
- iu, nm = load_ipu(self.log, args.ipu)
220
+ iu, nm = load_ipu(self.log, args.ipu, True)
221
221
  setattr(args, "ipu_iu", iu)
222
222
  setattr(args, "ipu_nm", nm)
223
223
 
224
224
  if not self.args.no_ses:
225
225
  self.setup_session_db()
226
226
 
227
+ args.shr1 = ""
227
228
  if args.shr:
228
229
  self.setup_share_db()
229
230
 
@@ -372,6 +373,14 @@ class SvcHub(object):
372
373
 
373
374
  self.broker = Broker(self)
374
375
 
376
+ # create netmaps early to avoid firewall gaps,
377
+ # but the mutex blocks multiprocessing startup
378
+ for zs in "ipu_iu ftp_ipa_nm tftp_ipa_nm".split():
379
+ try:
380
+ getattr(args, zs).mutex = threading.Lock()
381
+ except:
382
+ pass
383
+
375
384
  def setup_session_db(self) :
376
385
  if not HAVE_SQLITE3:
377
386
  self.args.no_ses = True
@@ -444,6 +453,7 @@ class SvcHub(object):
444
453
  raise Exception(t)
445
454
 
446
455
  al.shr = "/%s/" % (al.shr,)
456
+ al.shr1 = al.shr[1:]
447
457
 
448
458
  create = True
449
459
  modified = False
@@ -751,8 +761,8 @@ class SvcHub(object):
751
761
  al.idp_h_grp = al.idp_h_grp.lower()
752
762
  al.idp_h_key = al.idp_h_key.lower()
753
763
 
754
- al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa)
755
- al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa)
764
+ al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa, True)
765
+ al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa, True)
756
766
 
757
767
  mte = ODict.fromkeys(DEF_MTE.split(","), True)
758
768
  al.mte = odfusion(mte, al.mte)
@@ -799,6 +809,24 @@ class SvcHub(object):
799
809
  if len(al.tcolor) == 3: # fc5 => ffcc55
800
810
  al.tcolor = "".join([x * 2 for x in al.tcolor])
801
811
 
812
+ zs = al.u2sz
813
+ zsl = zs.split(",")
814
+ if len(zsl) not in (1, 3):
815
+ t = "invalid --u2sz; must be either one number, or a comma-separated list of three numbers (min,default,max)"
816
+ raise Exception(t)
817
+ if len(zsl) < 3:
818
+ zsl = ["1", zs, zs]
819
+ zi2 = 1
820
+ for zs in zsl:
821
+ zi = int(zs)
822
+ # arbitrary constraint (anything above 2 GiB is probably unintended)
823
+ if zi < 1 or zi > 2047:
824
+ raise Exception("invalid --u2sz; minimum is 1, max is 2047")
825
+ if zi < zi2:
826
+ raise Exception("invalid --u2sz; values must be equal or ascending")
827
+ zi2 = zi
828
+ al.u2sz = ",".join(zsl)
829
+
802
830
  return True
803
831
 
804
832
  def _ipa2re(self, txt) :
copyparty/up2k.py CHANGED
@@ -3886,11 +3886,9 @@ class Up2k(object):
3886
3886
  if unpost:
3887
3887
  raise Pebkac(400, "cannot unpost folders")
3888
3888
  elif stat.S_ISLNK(st.st_mode) or stat.S_ISREG(st.st_mode):
3889
- dbv, vrem = self.asrv.vfs.get(vpath, uname, *permsets[0])
3890
- dbv, vrem = dbv.get_dbv(vrem)
3891
- voldir = vsplit(vrem)[0]
3889
+ voldir = vsplit(rem)[0]
3892
3890
  vpath_dir = vsplit(vpath)[0]
3893
- g = [(dbv, voldir, vpath_dir, adir, [(fn, 0)], [], {})] # type: ignore
3891
+ g = [(vn, voldir, vpath_dir, adir, [(fn, 0)], [], {})] # type: ignore
3894
3892
  else:
3895
3893
  self.log("rm: skip type-{:x} file [{}]".format(st.st_mode, atop))
3896
3894
  return 0, [], []
@@ -3917,7 +3915,10 @@ class Up2k(object):
3917
3915
  volpath = ("%s/%s" % (vrem, fn)).strip("/")
3918
3916
  vpath = ("%s/%s" % (dbv.vpath, volpath)).strip("/")
3919
3917
  self.log("rm %s\n %s" % (vpath, abspath))
3920
- _ = dbv.get(volpath, uname, *permsets[0])
3918
+ if not unpost:
3919
+ # recursion-only sanchk
3920
+ _ = dbv.get(volpath, uname, *permsets[0])
3921
+
3921
3922
  if xbd:
3922
3923
  if not runhook(
3923
3924
  self.log,
copyparty/util.py CHANGED
@@ -647,13 +647,20 @@ class HLog(logging.Handler):
647
647
 
648
648
  class NetMap(object):
649
649
  def __init__(
650
- self, ips , cidrs , keep_lo=False, strict_cidr=False
650
+ self,
651
+ ips ,
652
+ cidrs ,
653
+ keep_lo=False,
654
+ strict_cidr=False,
655
+ defer_mutex=False,
651
656
  ) :
652
657
  """
653
658
  ips: list of plain ipv4/ipv6 IPs, not cidr
654
659
  cidrs: list of cidr-notation IPs (ip/prefix)
655
660
  """
656
- self.mutex = threading.Lock()
661
+
662
+ # fails multiprocessing; defer assignment
663
+ self.mutex = None if defer_mutex else threading.Lock()
657
664
 
658
665
  if "::" in ips:
659
666
  ips = [x for x in ips if x != "::"] + list(
@@ -692,6 +699,8 @@ class NetMap(object):
692
699
  try:
693
700
  return self.cache[ip]
694
701
  except:
702
+ # intentionally crash the calling thread if unset:
703
+
695
704
  with self.mutex:
696
705
  return self._map(ip)
697
706
 
@@ -2109,6 +2118,23 @@ def unquotep(txt ) :
2109
2118
  return w8dec(unq2)
2110
2119
 
2111
2120
 
2121
+ def vroots(vp1 , vp2 ) :
2122
+ """
2123
+ input("q/w/e/r","a/s/d/e/r") output("/q/w/","/a/s/d/")
2124
+ """
2125
+ while vp1 and vp2:
2126
+ zt1 = vp1.rsplit("/", 1) if "/" in vp1 else ("", vp1)
2127
+ zt2 = vp2.rsplit("/", 1) if "/" in vp2 else ("", vp2)
2128
+ if zt1[1] != zt2[1]:
2129
+ break
2130
+ vp1 = zt1[0]
2131
+ vp2 = zt2[0]
2132
+ return (
2133
+ "/%s/" % (vp1,) if vp1 else "/",
2134
+ "/%s/" % (vp2,) if vp2 else "/",
2135
+ )
2136
+
2137
+
2112
2138
  def vsplit(vpath ) :
2113
2139
  if "/" not in vpath:
2114
2140
  return "", vpath
@@ -2568,7 +2594,7 @@ def list_ips() :
2568
2594
  return list(ret)
2569
2595
 
2570
2596
 
2571
- def build_netmap(csv ):
2597
+ def build_netmap(csv , defer_mutex = False):
2572
2598
  csv = csv.lower().strip()
2573
2599
 
2574
2600
  if csv in ("any", "all", "no", ",", ""):
@@ -2603,10 +2629,12 @@ def build_netmap(csv ):
2603
2629
  cidrs.append(zs)
2604
2630
 
2605
2631
  ips = [x.split("/")[0] for x in cidrs]
2606
- return NetMap(ips, cidrs, True)
2632
+ return NetMap(ips, cidrs, True, False, defer_mutex)
2607
2633
 
2608
2634
 
2609
- def load_ipu(log , ipus ) :
2635
+ def load_ipu(
2636
+ log , ipus , defer_mutex = False
2637
+ ) :
2610
2638
  ip_u = {"": "*"}
2611
2639
  cidr_u = {}
2612
2640
  for ipu in ipus:
@@ -2623,7 +2651,7 @@ def load_ipu(log , ipus ) :
2623
2651
  cidr_u[cidr] = uname
2624
2652
  ip_u[cip] = uname
2625
2653
  try:
2626
- nm = NetMap(["::"], list(cidr_u.keys()), True, True)
2654
+ nm = NetMap(["::"], list(cidr_u.keys()), True, True, defer_mutex)
2627
2655
  except Exception as ex:
2628
2656
  t = "failed to translate --ipu into netmap, probably due to invalid config: %r"
2629
2657
  log("root", t % (ex,), 1)
@@ -844,7 +844,7 @@ def main():
844
844
 
845
845
  # dircache is always a boost,
846
846
  # only want to disable it for tests etc,
847
- cdn = 9 # max num dirs; 0=disable
847
+ cdn = 24 # max num dirs; keep larger than max dir depth; 0=disable
848
848
  cds = 1 # numsec until an entry goes stale
849
849
 
850
850
  where = "local directory"
Binary file
copyparty/web/splash.html CHANGED
@@ -129,11 +129,20 @@
129
129
 
130
130
  {% if k304 or k304vis %}
131
131
  {% if k304 %}
132
- <li><a id="h" href="{{ r }}/?k304=n">disable k304</a> (currently enabled)
132
+ <li><a id="h" href="{{ r }}/?cc&setck=k304=n">disable k304</a> (currently enabled)
133
133
  {%- else %}
134
- <li><a id="i" href="{{ r }}/?k304=y" class="r">enable k304</a> (currently disabled)
134
+ <li><a id="i" href="{{ r }}/?cc&setck=k304=y" class="r">enable k304</a> (currently disabled)
135
135
  {% endif %}
136
- <blockquote id="j">enabling this will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general</blockquote></li>
136
+ <blockquote id="j">enabling k304 will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general</blockquote></li>
137
+ {% endif %}
138
+
139
+ {% if no304 or no304vis %}
140
+ {% if no304 %}
141
+ <li><a id="ab" href="{{ r }}/?cc&setck=no304=n">disable no304</a> (currently enabled)
142
+ {%- else %}
143
+ <li><a id="ac" href="{{ r }}/?cc&setck=no304=y" class="r">enable no304</a> (currently disabled)
144
+ {% endif %}
145
+ <blockquote id="ad">enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!</blockquote></li>
137
146
  {% endif %}
138
147
 
139
148
  <li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li>
Binary file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: copyparty
3
- Version: 1.15.9
3
+ Version: 1.15.10
4
4
  Summary: Portable file server with accelerated resumable uploads, deduplication, WebDAV, FTP, zeroconf, media indexer, video thumbnails, audio transcoding, and write-only folders
5
5
  Author-email: ed <copyparty@ocv.me>
6
6
  License: MIT
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.9
21
21
  Classifier: Programming Language :: Python :: 3.10
22
22
  Classifier: Programming Language :: Python :: 3.11
23
23
  Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
24
25
  Classifier: Programming Language :: Python :: Implementation :: CPython
25
26
  Classifier: Programming Language :: Python :: Implementation :: Jython
26
27
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -1987,6 +1988,9 @@ quick summary of more eccentric web-browsers trying to view a directory index:
1987
1988
  | **ie4** and **netscape** 4.0 | can browse, upload with `?b=u`, auth with `&pw=wark` |
1988
1989
  | **ncsa mosaic** 2.7 | does not get a pass, [pic1](https://user-images.githubusercontent.com/241032/174189227-ae816026-cf6f-4be5-a26e-1b3b072c1b2f.png) - [pic2](https://user-images.githubusercontent.com/241032/174189225-5651c059-5152-46e9-ac26-7e98e497901b.png) |
1989
1990
  | **SerenityOS** (7e98457) | hits a page fault, works with `?b=u`, file upload not-impl |
1991
+ | **nintendo 3ds** | can browse, upload, view thumbnails (thx bnjmn) |
1992
+
1993
+ <p align="center"><img src="https://github.com/user-attachments/assets/88deab3d-6cad-4017-8841-2f041472b853" /></p>
1990
1994
 
1991
1995
 
1992
1996
  # client examples
@@ -1,7 +1,7 @@
1
1
  copyparty/__init__.py,sha256=Chqw7uXX4r_-a2p6-xthrrqVHFI4aZdW45sWU7UvqeE,2597
2
- copyparty/__main__.py,sha256=rx8OlvcX3M-SQOvkrYT9C-HN7GmlPUx5p_2Vqv_LEo4,110949
3
- copyparty/__version__.py,sha256=PMcIJPKN9PvxPb2mIFEiFAsp9HLavA8qaM4ckTc7cNE,258
4
- copyparty/authsrv.py,sha256=Iw_4lJUhRU9q3qCQucGWRtJOFKYrYd5omL5nmwGvw-k,98979
2
+ copyparty/__main__.py,sha256=S9jBx-HMqtUusAjpA64akmN4SKeRMGS5dNfzS4-miC4,111480
3
+ copyparty/__version__.py,sha256=fkcFvGlCD_NUIpsBFycKnSiV9ebZj5bDCWyy5H38XJw,259
4
+ copyparty/authsrv.py,sha256=FNb0l3o57R9xnYrZsqjTOqPwWkXGgP9YdX7NWx0ZMow,98955
5
5
  copyparty/broker_mp.py,sha256=jsHUM2BSfRVRyZT869iPCqYEHSqedk6VkwvygZwbEZE,4017
6
6
  copyparty/broker_mpw.py,sha256=PYFgQfssOCfdI6qayW1ZjO1j1-7oez094muhYMbPOz0,3339
7
7
  copyparty/broker_thr.py,sha256=MXrwjusP0z1LPURUhi5jx_TL3jrXhYcDrJPDSKu6EEU,1705
@@ -11,7 +11,7 @@ copyparty/cfg.py,sha256=E9iBGNjIUrDAPLFRgKsVOmAknP9bDE27xh0gkmNdH1s,10127
11
11
  copyparty/dxml.py,sha256=lZpg-kn-kQsXRtNY1n6fRaS-b7uXzMCyv8ovKnhZcZc,1548
12
12
  copyparty/fsutil.py,sha256=5CshJWO7CflfaRRNOb3JxghUH7W5rmS_HWNmKfx42MM,4538
13
13
  copyparty/ftpd.py,sha256=G_h1urfIikzfCWGXnW9p-rioWdNM_Je6vWYq0-QSbC8,17580
14
- copyparty/httpcli.py,sha256=irIxsAI0KpNvxoenK16zkezttl4sUSNUB59yBr8L6VA,199053
14
+ copyparty/httpcli.py,sha256=rb-QxygFrRM7LEmTadMKn4FZ2bl5ddoPfJXsRrItKDg,201389
15
15
  copyparty/httpconn.py,sha256=mQSgljh0Q-jyWjF4tQLrHbRKRe9WKl19kGqsGMsJpWo,6880
16
16
  copyparty/httpsrv.py,sha256=d_UiGnQKniBoEV68lNFgnYm-byda7uj56mFf-YC7piI,17223
17
17
  copyparty/ico.py,sha256=eWSxEae4wOCfheHl-m-wchYvFRAR_97kJDb4NGaB-Z8,3561
@@ -24,15 +24,15 @@ copyparty/smbd.py,sha256=Or7RF13cl1r3ncnpVh8BqyAGqH2Oa04O9iPZWCoB0Bo,14609
24
24
  copyparty/ssdp.py,sha256=R1Z61GZOxBMF2Sk4RTxKWMOemogmcjEWG-CvLihd45k,7023
25
25
  copyparty/star.py,sha256=tV5BbX6AiQ7N4UU8DYtSTckNYeoeey4DBqq4LjfymbY,3818
26
26
  copyparty/sutil.py,sha256=JTMrQwcWH85hXB_cKG206eDZ967WZDGaP00AWvl_gB0,3214
27
- copyparty/svchub.py,sha256=FuQGFBm-lJfe28M-SMBLieHy8jdWIQMkONK2GcWZU_E,40105
27
+ copyparty/svchub.py,sha256=sfA_2BIXf0GP6-vNJGsttcvuVTjMek7TXWoGhYSa1bg,41178
28
28
  copyparty/szip.py,sha256=sDypi1_yR6-62fIZ_3D0L9PfIzCUiK_3JqcaJCvTBCs,8601
29
29
  copyparty/tcpsrv.py,sha256=l_vb9FoF0AJur0IoqHNUSBDqMgBO_MRUZeDszi1UNfY,19881
30
30
  copyparty/tftpd.py,sha256=jZbf2JpeJmkuQWJErmAPG-dKhtYNvIUHbkAgodSXw9Y,13582
31
31
  copyparty/th_cli.py,sha256=o6FMkerYvAXS455z3DUossVztu_nzFlYSQhs6qN6Jt8,4636
32
32
  copyparty/th_srv.py,sha256=hI9wY1E_9N9Cgqvtr8zADeVqqiLGTiTdAnYAA7WFvJw,29346
33
33
  copyparty/u2idx.py,sha256=HLO49L1zmpJtBcJiXgD12a6pAlQdnf2pFelHMA7habw,13462
34
- copyparty/up2k.py,sha256=2K21-Scz4c3_Y9MzAPmvq3_LEOIkFnx7KLR6hqJkP18,165419
35
- copyparty/util.py,sha256=eJU7a5O-bZf_MEplCVmLoS3r5mjPC3-2khqZVTYGw-c,92653
34
+ copyparty/up2k.py,sha256=PYAcSuiEJAikQSDGNodyv-jXNtGxmN-KB8_hZ4989Qg,165385
35
+ copyparty/util.py,sha256=bV_zxOiG3GIxt91EkTR7r-YjvR7WOM9IIvg0zuq0s7k,93378
36
36
  copyparty/bos/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
37
  copyparty/bos/bos.py,sha256=Wb7eWsXJgR5AFlBR9ZOyKrLTwy-Kct9RrGiOu4Jo37Y,1622
38
38
  copyparty/bos/path.py,sha256=yEjCq2ki9CvxA5sCT8pS0keEXwugs0ZeUyUhdBziOCI,777
@@ -57,7 +57,7 @@ copyparty/stolen/ifaddr/_win32.py,sha256=EE-QyoBgeB7lYQ6z62VjXNaRozaYfCkaJBHGNA8
57
57
  copyparty/web/baguettebox.js.gz,sha256=YIaxFDsubJfGIdzzxA-cL6GwJVmpWZyaPhW9hHcOIIw,7964
58
58
  copyparty/web/browser.css.gz,sha256=4bAS9Xkl2fflhaxRSRSVoYQcpXsg1mCWxsYjId7phbU,11610
59
59
  copyparty/web/browser.html,sha256=ISpfvWEawufJCYZIqvuXiyUgiXgjmOTtScz4zrEaypI,4870
60
- copyparty/web/browser.js.gz,sha256=7rubbEoqFlNn7FPF0mz3L2LQeWuPzw95MYpIaZmcczE,84985
60
+ copyparty/web/browser.js.gz,sha256=yDhq0t-6R9OCv--ca_dCCsg7ZkhJjN3Y-9-PQdZPRpg,85071
61
61
  copyparty/web/browser2.html,sha256=NRUZ08GH-e2YcGXcoz0UjYg6JIVF42u4IMX4HHwWTmg,1587
62
62
  copyparty/web/cf.html,sha256=lJThtNFNAQT1ClCHHlivAkDGE0LutedwopXD62Z8Nys,589
63
63
  copyparty/web/dbg-audio.js.gz,sha256=Ma-KZtK8LnmiwNvNKFKXMPYl_Nn_3U7GsJ6-DRWC2HE,688
@@ -75,8 +75,8 @@ copyparty/web/shares.css.gz,sha256=T2fSezuluDVIiNIERAuUREByhHFlIwwNyx7EBOAVVyQ,4
75
75
  copyparty/web/shares.html,sha256=ZNHtLBM-Y4BX2qa9AGTrZzZp_IP5PLM3QvFMYKolFfM,2494
76
76
  copyparty/web/shares.js.gz,sha256=814O61mxLSWs0AO2fbGJ8d4BSPB7pE9NdkKiVf1gj6E,926
77
77
  copyparty/web/splash.css.gz,sha256=RjdNoIT5BSxXRFu0ldMUH4ghRNUMCTs2mGKzstrpI6o,1033
78
- copyparty/web/splash.html,sha256=pUbsso_W3Q7bso8fy8qxh-fHDrrLm39mBBTIlTeH63w,5235
79
- copyparty/web/splash.js.gz,sha256=Xoccku-2vE3tABo-88q3Cl4koHs_AE76T8QvMy4u6T8,2540
78
+ copyparty/web/splash.html,sha256=_09d2C79S4sIyaks5pPdq1PBgabdlEFhB6Z-KiynwD8,5699
79
+ copyparty/web/splash.js.gz,sha256=f9aSzI0vw0teK52MBi8Y9FsSMURDfRdZFEvpzqNBon8,2694
80
80
  copyparty/web/svcs.html,sha256=P5YZimYLeQMT0uz6u3clQSNZRc5Zs0Ok-ffcbcGSYuc,11762
81
81
  copyparty/web/svcs.js.gz,sha256=k81ZvZ3I-f4fMHKrNGGOgOlvXnCBz0mVjD-8mieoWCA,520
82
82
  copyparty/web/ui.css.gz,sha256=wloSacrHgP722hy4XiOvVY2GI9-V4zvfvzu84LLWS_o,2779
@@ -84,7 +84,7 @@ copyparty/web/up2k.js.gz,sha256=lGR1Xb0RkIZ1eHmncsSwWRuFc6FC2rZalvjo3oNTV1s,2329
84
84
  copyparty/web/util.js.gz,sha256=NvjPYhIa0-C_NhUyW-Ra-XinUCRjj8G3pYq1zJHYWEk,14805
85
85
  copyparty/web/w.hash.js.gz,sha256=l3GpSJD6mcU-1CRWkIj7PybgbjlfSr8oeO3vortIrQk,1105
86
86
  copyparty/web/a/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
87
- copyparty/web/a/partyfuse.py,sha256=fa9bBYNJHvtWpNVjQyyFzx6JOK7MJL1u0zj80PBYQKs,27960
87
+ copyparty/web/a/partyfuse.py,sha256=efBOupuGVCaGVGylJ-mcx0kz6paqc2sk8kHIprfGUQU,27993
88
88
  copyparty/web/a/u2c.py,sha256=ZmLcGuOWB66ZkAMpJBiR1Xpa75PgpzdFRTt3GGVCorc,49533
89
89
  copyparty/web/a/webdav-cfg.bat,sha256=Y4NoGZlksAIg4cBMb7KdJrpKC6Nx97onaTl6yMjaimk,1449
90
90
  copyparty/web/dd/2.png,sha256=gJ14XFPzaw95L6z92fSq9eMPikSQyu-03P1lgiGe0_I,258
@@ -106,9 +106,9 @@ copyparty/web/deps/prismd.css.gz,sha256=ObUlksQVr-OuYlTz-I4B23TeBg2QDVVGRnWBz8cV
106
106
  copyparty/web/deps/scp.woff2,sha256=w99BDU5i8MukkMEL-iW0YO9H4vFFZSPWxbkH70ytaAg,8612
107
107
  copyparty/web/deps/sha512.ac.js.gz,sha256=lFZaCLumgWxrvEuDr4bqdKHsqjX82AbVAb7_F45Yk88,7033
108
108
  copyparty/web/deps/sha512.hw.js.gz,sha256=vqoXeracj-99Z5MfY3jK2N4WiSzYQdfjy0RnUlQDhSU,8110
109
- copyparty-1.15.9.dist-info/LICENSE,sha256=gOr4h33pCsBEg9uIy9AYmb7qlocL4V9t2uPJS5wllr0,1072
110
- copyparty-1.15.9.dist-info/METADATA,sha256=1GMM2x8ZO0z3TgiQgCB6xEiiahXsn3PdeiA7Uni_D_4,139807
111
- copyparty-1.15.9.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
112
- copyparty-1.15.9.dist-info/entry_points.txt,sha256=4zw6a3rqASywQomiYLObjjlxybaI65LYYOTJwgKz7b0,128
113
- copyparty-1.15.9.dist-info/top_level.txt,sha256=LnYUPsDyk-8kFgM6YJLG4h820DQekn81cObKSu9g-sI,10
114
- copyparty-1.15.9.dist-info/RECORD,,
109
+ copyparty-1.15.10.dist-info/LICENSE,sha256=gOr4h33pCsBEg9uIy9AYmb7qlocL4V9t2uPJS5wllr0,1072
110
+ copyparty-1.15.10.dist-info/METADATA,sha256=DhJZfQ1d9CPtx3LXk0ga7thnke1xWg3GJI_HlTPXZLw,140056
111
+ copyparty-1.15.10.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
112
+ copyparty-1.15.10.dist-info/entry_points.txt,sha256=4zw6a3rqASywQomiYLObjjlxybaI65LYYOTJwgKz7b0,128
113
+ copyparty-1.15.10.dist-info/top_level.txt,sha256=LnYUPsDyk-8kFgM6YJLG4h820DQekn81cObKSu9g-sI,10
114
+ copyparty-1.15.10.dist-info/RECORD,,