copyparty 1.16.16__py3-none-any.whl → 1.16.17__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
@@ -65,6 +65,7 @@ from .util import (
65
65
  load_resource,
66
66
  min_ex,
67
67
  pybin,
68
+ read_utf8,
68
69
  termsize,
69
70
  wrap,
70
71
  )
@@ -249,8 +250,7 @@ def get_srvname(verbose) :
249
250
  if verbose:
250
251
  lprint("using hostname from {}\n".format(fp))
251
252
  try:
252
- with open(fp, "rb") as f:
253
- ret = f.read().decode("utf-8", "replace").strip()
253
+ return read_utf8(None, fp, True).strip()
254
254
  except:
255
255
  ret = ""
256
256
  namelen = 5
@@ -259,47 +259,18 @@ def get_srvname(verbose) :
259
259
  ret = re.sub("[234567=]", "", ret)[:namelen]
260
260
  with open(fp, "wb") as f:
261
261
  f.write(ret.encode("utf-8") + b"\n")
262
-
263
- return ret
264
-
265
-
266
- def get_fk_salt() :
267
- fp = os.path.join(E.cfg, "fk-salt.txt")
268
- try:
269
- with open(fp, "rb") as f:
270
- ret = f.read().strip()
271
- except:
272
- ret = b64enc(os.urandom(18))
273
- with open(fp, "wb") as f:
274
- f.write(ret + b"\n")
275
-
276
- return ret.decode("utf-8")
277
-
278
-
279
- def get_dk_salt() :
280
- fp = os.path.join(E.cfg, "dk-salt.txt")
281
- try:
282
- with open(fp, "rb") as f:
283
- ret = f.read().strip()
284
- except:
285
- ret = b64enc(os.urandom(30))
286
- with open(fp, "wb") as f:
287
- f.write(ret + b"\n")
288
-
289
- return ret.decode("utf-8")
262
+ return ret
290
263
 
291
264
 
292
- def get_ah_salt() :
293
- fp = os.path.join(E.cfg, "ah-salt.txt")
265
+ def get_salt(name , nbytes ) :
266
+ fp = os.path.join(E.cfg, "%s-salt.txt" % (name,))
294
267
  try:
295
- with open(fp, "rb") as f:
296
- ret = f.read().strip()
268
+ return read_utf8(None, fp, True).strip()
297
269
  except:
298
- ret = b64enc(os.urandom(18))
270
+ ret = b64enc(os.urandom(nbytes))
299
271
  with open(fp, "wb") as f:
300
272
  f.write(ret + b"\n")
301
-
302
- return ret.decode("utf-8")
273
+ return ret.decode("utf-8")
303
274
 
304
275
 
305
276
  def ensure_locale() :
@@ -1257,6 +1228,10 @@ def add_optouts(ap):
1257
1228
  ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI")
1258
1229
  ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI")
1259
1230
  ap2.add_argument("-nb", action="store_true", help="no powered-by-copyparty branding in UI")
1231
+ ap2.add_argument("--zipmaxn", metavar="N", type=u, default="0", help="reject download-as-zip if more than \033[33mN\033[0m files in total; optionally takes a unit suffix: [\033[32m256\033[0m], [\033[32m9K\033[0m], [\033[32m4G\033[0m] (volflag=zipmaxn)")
1232
+ ap2.add_argument("--zipmaxs", metavar="SZ", type=u, default="0", help="reject download-as-zip if total download size exceeds \033[33mSZ\033[0m bytes; optionally takes a unit suffix: [\033[32m256M\033[0m], [\033[32m4G\033[0m], [\033[32m2T\033[0m] (volflag=zipmaxs)")
1233
+ ap2.add_argument("--zipmaxt", metavar="TXT", type=u, default="", help="custom errormessage when download size exceeds max (volflag=zipmaxt)")
1234
+ ap2.add_argument("--zipmaxu", action="store_true", help="authenticated users bypass the zip size limit (volflag=zipmaxu)")
1260
1235
  ap2.add_argument("--zip-who", metavar="LVL", type=int, default=3, help="who can download as zip/tar? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=authenticated-with-read-access, [\033[32m3\033[0m]=everyone-with-read-access (volflag=zip_who)\n\033[1;31mWARNING:\033[0m if a nested volume has a more restrictive value than a parent volume, then this will be \033[33mignored\033[0m if the download is initiated from the parent, more lenient volume")
1261
1236
  ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar; same as \033[33m--zip-who=0\033[0m")
1262
1237
  ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)")
@@ -1544,9 +1519,9 @@ def run_argparse(
1544
1519
 
1545
1520
  cert_path = os.path.join(E.cfg, "cert.pem")
1546
1521
 
1547
- fk_salt = get_fk_salt()
1548
- dk_salt = get_dk_salt()
1549
- ah_salt = get_ah_salt()
1522
+ fk_salt = get_salt("fk", 18)
1523
+ dk_salt = get_salt("dk", 30)
1524
+ ah_salt = get_salt("ah", 18)
1550
1525
 
1551
1526
  # alpine peaks at 5 threads for some reason,
1552
1527
  # all others scale past that (but try to avoid SMT),
copyparty/__version__.py CHANGED
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 16, 16)
3
+ VERSION = (1, 16, 17)
4
4
  CODENAME = "COPYparty"
5
- BUILD_DT = (2025, 2, 28)
5
+ BUILD_DT = (2025, 3, 16)
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
@@ -33,6 +33,7 @@ from .util import (
33
33
  get_df,
34
34
  humansize,
35
35
  odfusion,
36
+ read_utf8,
36
37
  relchk,
37
38
  statdir,
38
39
  ub64enc,
@@ -64,6 +65,8 @@ SSEELOG = " ({})".format(SEE_LOG)
64
65
  BAD_CFG = "invalid config; {}".format(SEE_LOG)
65
66
  SBADCFG = " ({})".format(BAD_CFG)
66
67
 
68
+ PTN_U_GRP = re.compile(r"\$\{u%([+-])([^}]+)\}")
69
+
67
70
 
68
71
  class CfgEx(Exception):
69
72
  pass
@@ -335,22 +338,26 @@ class VFS(object):
335
338
  log ,
336
339
  realpath ,
337
340
  vpath ,
341
+ vpath0 ,
338
342
  axs ,
339
343
  flags ,
340
344
  ) :
341
345
  self.log = log
342
346
  self.realpath = realpath # absolute path on host filesystem
343
347
  self.vpath = vpath # absolute path in the virtual filesystem
348
+ self.vpath0 = vpath0 # original vpath (before idp expansion)
344
349
  self.axs = axs
345
350
  self.flags = flags # config options
346
351
  self.root = self
347
352
  self.dev = 0 # st_dev
353
+ self.badcfg1 = False
348
354
  self.nodes = {} # child nodes
349
355
  self.histtab = {} # all realpath->histpath
350
356
  self.dbv = None # closest full/non-jump parent
351
357
  self.lim = None # upload limits; only set for dbv
352
358
  self.shr_src = None # source vfs+rem of a share
353
359
  self.shr_files = set() # filenames to include from shr_src
360
+ self.shr_owner = "" # uname
354
361
  self.aread = {}
355
362
  self.awrite = {}
356
363
  self.amove = {}
@@ -368,7 +375,7 @@ class VFS(object):
368
375
  vp = vpath + ("/" if vpath else "")
369
376
  self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
370
377
  self.all_vols = {vpath: self} # flattened recursive
371
- self.all_nodes = {vpath: self} # also jumpvols
378
+ self.all_nodes = {vpath: self} # also jumpvols/shares
372
379
  self.all_aps = [(rp, self)]
373
380
  self.all_vps = [(vp, self)]
374
381
  else:
@@ -408,7 +415,7 @@ class VFS(object):
408
415
  for v in self.nodes.values():
409
416
  v.get_all_vols(vols, nodes, aps, vps)
410
417
 
411
- def add(self, src , dst ) :
418
+ def add(self, src , dst , dst0 ) :
412
419
  """get existing, or add new path to the vfs"""
413
420
  assert src == "/" or not src.endswith("/") # nosec
414
421
  assert not dst.endswith("/") # nosec
@@ -416,20 +423,22 @@ class VFS(object):
416
423
  if "/" in dst:
417
424
  # requires breadth-first population (permissions trickle down)
418
425
  name, dst = dst.split("/", 1)
426
+ name0, dst0 = dst0.split("/", 1)
419
427
  if name in self.nodes:
420
428
  # exists; do not manipulate permissions
421
- return self.nodes[name].add(src, dst)
429
+ return self.nodes[name].add(src, dst, dst0)
422
430
 
423
431
  vn = VFS(
424
432
  self.log,
425
433
  os.path.join(self.realpath, name) if self.realpath else "",
426
434
  "{}/{}".format(self.vpath, name).lstrip("/"),
435
+ "{}/{}".format(self.vpath0, name0).lstrip("/"),
427
436
  self.axs,
428
437
  self._copy_flags(name),
429
438
  )
430
439
  vn.dbv = self.dbv or self
431
440
  self.nodes[name] = vn
432
- return vn.add(src, dst)
441
+ return vn.add(src, dst, dst0)
433
442
 
434
443
  if dst in self.nodes:
435
444
  # leaf exists; return as-is
@@ -437,7 +446,8 @@ class VFS(object):
437
446
 
438
447
  # leaf does not exist; create and keep permissions blank
439
448
  vp = "{}/{}".format(self.vpath, dst).lstrip("/")
440
- vn = VFS(self.log, src, vp, AXS(), {})
449
+ vp0 = "{}/{}".format(self.vpath0, dst0).lstrip("/")
450
+ vn = VFS(self.log, src, vp, vp0, AXS(), {})
441
451
  vn.dbv = self.dbv or self
442
452
  self.nodes[dst] = vn
443
453
  return vn
@@ -854,7 +864,7 @@ class AuthSrv(object):
854
864
  self.indent = ""
855
865
 
856
866
  # fwd-decl
857
- self.vfs = VFS(log_func, "", "", AXS(), {})
867
+ self.vfs = VFS(log_func, "", "", "", AXS(), {})
858
868
  self.acct = {} # uname->pw
859
869
  self.iacct = {} # pw->uname
860
870
  self.ases = {} # uname->session
@@ -922,7 +932,7 @@ class AuthSrv(object):
922
932
  self,
923
933
  src ,
924
934
  dst ,
925
- mount ,
935
+ mount ,
926
936
  daxs ,
927
937
  mflags ,
928
938
  un_gns ,
@@ -938,12 +948,24 @@ class AuthSrv(object):
938
948
  un_gn = [("", "")]
939
949
 
940
950
  for un, gn in un_gn:
951
+ m = PTN_U_GRP.search(dst0)
952
+ if m:
953
+ req, gnc = m.groups()
954
+ hit = gnc in (un_gns.get(un) or [])
955
+ if req == "+":
956
+ if not hit:
957
+ continue
958
+ elif hit:
959
+ continue
960
+
941
961
  # if ap/vp has a user/group placeholder, make sure to keep
942
962
  # track so the same user/group is mapped when setting perms;
943
963
  # otherwise clear un/gn to indicate it's a regular volume
944
964
 
945
965
  src1 = src0.replace("${u}", un or "\n")
946
966
  dst1 = dst0.replace("${u}", un or "\n")
967
+ src1 = PTN_U_GRP.sub(un or "\n", src1)
968
+ dst1 = PTN_U_GRP.sub(un or "\n", dst1)
947
969
  if src0 == src1 and dst0 == dst1:
948
970
  un = ""
949
971
 
@@ -960,7 +982,7 @@ class AuthSrv(object):
960
982
  continue
961
983
  visited.add(label)
962
984
 
963
- src, dst = self._map_volume(src, dst, mount, daxs, mflags)
985
+ src, dst = self._map_volume(src, dst, dst0, mount, daxs, mflags)
964
986
  if src:
965
987
  ret.append((src, dst, un, gn))
966
988
  if un or gn:
@@ -972,7 +994,8 @@ class AuthSrv(object):
972
994
  self,
973
995
  src ,
974
996
  dst ,
975
- mount ,
997
+ dst0 ,
998
+ mount ,
976
999
  daxs ,
977
1000
  mflags ,
978
1001
  ) :
@@ -982,13 +1005,13 @@ class AuthSrv(object):
982
1005
 
983
1006
  if dst in mount:
984
1007
  t = "multiple filesystem-paths mounted at [/{}]:\n [{}]\n [{}]"
985
- self.log(t.format(dst, mount[dst], src), c=1)
1008
+ self.log(t.format(dst, mount[dst][0], src), c=1)
986
1009
  raise Exception(BAD_CFG)
987
1010
 
988
1011
  if src in mount.values():
989
1012
  t = "filesystem-path [{}] mounted in multiple locations:"
990
1013
  t = t.format(src)
991
- for v in [k for k, v in mount.items() if v == src] + [dst]:
1014
+ for v in [k for k, v in mount.items() if v[0] == src] + [dst]:
992
1015
  t += "\n /{}".format(v)
993
1016
 
994
1017
  self.log(t, c=3)
@@ -997,7 +1020,7 @@ class AuthSrv(object):
997
1020
  if not bos.path.isdir(src):
998
1021
  self.log("warning: filesystem-path does not exist: {}".format(src), 3)
999
1022
 
1000
- mount[dst] = src
1023
+ mount[dst] = (src, dst0)
1001
1024
  daxs[dst] = AXS()
1002
1025
  mflags[dst] = {}
1003
1026
  return (src, dst)
@@ -1058,7 +1081,7 @@ class AuthSrv(object):
1058
1081
  grps ,
1059
1082
  daxs ,
1060
1083
  mflags ,
1061
- mount ,
1084
+ mount ,
1062
1085
  ) :
1063
1086
  self.line_ctr = 0
1064
1087
 
@@ -1083,7 +1106,7 @@ class AuthSrv(object):
1083
1106
  grps ,
1084
1107
  daxs ,
1085
1108
  mflags ,
1086
- mount ,
1109
+ mount ,
1087
1110
  npass ,
1088
1111
  ) :
1089
1112
  self.line_ctr = 0
@@ -1442,8 +1465,8 @@ class AuthSrv(object):
1442
1465
  acct = {} # username:password
1443
1466
  grps = {} # groupname:usernames
1444
1467
  daxs = {}
1445
- mflags = {} # moutpoint:flags
1446
- mount = {} # dst:src (mountpoint:realpath)
1468
+ mflags = {} # vpath:flags
1469
+ mount = {} # dst:src (vp:(ap,vp0))
1447
1470
 
1448
1471
  self.idp_vols = {} # yolo
1449
1472
 
@@ -1522,8 +1545,8 @@ class AuthSrv(object):
1522
1545
  # case-insensitive; normalize
1523
1546
  if WINDOWS:
1524
1547
  cased = {}
1525
- for k, v in mount.items():
1526
- cased[k] = absreal(v)
1548
+ for vp, (ap, vp0) in mount.items():
1549
+ cased[vp] = (absreal(ap), vp0)
1527
1550
 
1528
1551
  mount = cased
1529
1552
 
@@ -1538,25 +1561,28 @@ class AuthSrv(object):
1538
1561
  t = "Read-access has been disabled due to failsafe: No volumes were defined by the config-file. This failsafe is to prevent unintended access if this is due to accidental loss of config. You can override this safeguard and allow read/write to the working-directory by adding the following arguments: -v .::rw"
1539
1562
  self.log(t, 1)
1540
1563
  axs = AXS()
1541
- vfs = VFS(self.log_func, absreal("."), "", axs, {})
1564
+ vfs = VFS(self.log_func, absreal("."), "", "", axs, {})
1565
+ if not axs.uread:
1566
+ vfs.badcfg1 = True
1542
1567
  elif "" not in mount:
1543
1568
  # there's volumes but no root; make root inaccessible
1544
1569
  zsd = {"d2d": True, "tcolor": self.args.tcolor}
1545
- vfs = VFS(self.log_func, "", "", AXS(), zsd)
1570
+ vfs = VFS(self.log_func, "", "", "", AXS(), zsd)
1546
1571
 
1547
1572
  maxdepth = 0
1548
1573
  for dst in sorted(mount.keys(), key=lambda x: (x.count("/"), len(x))):
1549
1574
  depth = dst.count("/")
1550
1575
  assert maxdepth <= depth # nosec
1551
1576
  maxdepth = depth
1577
+ src, dst0 = mount[dst]
1552
1578
 
1553
1579
  if dst == "":
1554
1580
  # rootfs was mapped; fully replaces the default CWD vfs
1555
- vfs = VFS(self.log_func, mount[dst], dst, daxs[dst], mflags[dst])
1581
+ vfs = VFS(self.log_func, src, dst, dst0, daxs[dst], mflags[dst])
1556
1582
  continue
1557
1583
 
1558
1584
  assert vfs # type: ignore
1559
- zv = vfs.add(mount[dst], dst)
1585
+ zv = vfs.add(src, dst, dst0)
1560
1586
  zv.axs = daxs[dst]
1561
1587
  zv.flags = mflags[dst]
1562
1588
  zv.dbv = None
@@ -1590,7 +1616,8 @@ class AuthSrv(object):
1590
1616
  if enshare:
1591
1617
  import sqlite3
1592
1618
 
1593
- shv = VFS(self.log_func, "", shr, AXS(), {})
1619
+ zsd = {"d2d": True, "tcolor": self.args.tcolor}
1620
+ shv = VFS(self.log_func, "", shr, shr, AXS(), zsd)
1594
1621
 
1595
1622
  db_path = self.args.shr_db
1596
1623
  db = sqlite3.connect(db_path)
@@ -1624,9 +1651,8 @@ class AuthSrv(object):
1624
1651
 
1625
1652
  # don't know the abspath yet + wanna ensure the user
1626
1653
  # still has the privs they granted, so nullmap it
1627
- shv.nodes[s_k] = VFS(
1628
- self.log_func, "", "%s/%s" % (shr, s_k), s_axs, shv.flags.copy()
1629
- )
1654
+ vp = "%s/%s" % (shr, s_k)
1655
+ shv.nodes[s_k] = VFS(self.log_func, "", vp, vp, s_axs, shv.flags.copy())
1630
1656
 
1631
1657
  vfs.nodes[shr] = vfs.all_vols[shr] = shv
1632
1658
  for vol in shv.nodes.values():
@@ -1787,6 +1813,24 @@ class AuthSrv(object):
1787
1813
  rhisttab[histp] = zv
1788
1814
  vfs.histtab[zv.realpath] = histp
1789
1815
 
1816
+ for vol in vfs.all_vols.values():
1817
+ use = False
1818
+ for k in ["zipmaxn", "zipmaxs"]:
1819
+ try:
1820
+ zs = vol.flags[k]
1821
+ except:
1822
+ zs = getattr(self.args, k)
1823
+ if zs in ("", "0"):
1824
+ vol.flags[k] = 0
1825
+ continue
1826
+
1827
+ zf = unhumanize(zs)
1828
+ vol.flags[k + "_v"] = zf
1829
+ if zf:
1830
+ use = True
1831
+ if use:
1832
+ vol.flags["zipmax"] = True
1833
+
1790
1834
  for vol in vfs.all_vols.values():
1791
1835
  lim = Lim(self.log_func)
1792
1836
  use = False
@@ -2269,22 +2313,56 @@ class AuthSrv(object):
2269
2313
  except Pebkac:
2270
2314
  self.warn_anonwrite = True
2271
2315
 
2272
- idp_err = "WARNING! The following IdP volumes are mounted directly below another volume where anonymous users can read and/or write files. This is a SECURITY HAZARD!! When copyparty is restarted, it will not know about these IdP volumes yet. These volumes will then be accessible by anonymous users UNTIL one of the users associated with their volume sends a request to the server. RECOMMENDATION: You should create a restricted volume where nobody can read/write files, and make sure that all IdP volumes are configured to appear somewhere below that volume."
2316
+ self.idp_warn = []
2317
+ self.idp_err = []
2273
2318
  for idp_vp in self.idp_vols:
2274
- parent_vp = vsplit(idp_vp)[0]
2275
- vn, _ = vfs.get(parent_vp, "*", False, False)
2276
- zs = (
2277
- "READABLE"
2278
- if "*" in vn.axs.uread
2279
- else "WRITABLE"
2280
- if "*" in vn.axs.uwrite
2281
- else ""
2282
- )
2283
- if zs:
2284
- t = '\nWARNING: Volume "/%s" appears below "/%s" and would be WORLD-%s'
2285
- idp_err += t % (idp_vp, vn.vpath, zs)
2286
- if "\n" in idp_err:
2287
- self.log(idp_err, 1)
2319
+ idp_vn, _ = vfs.get(idp_vp, "*", False, False)
2320
+ idp_vp0 = idp_vn.vpath0
2321
+
2322
+ sigils = set(re.findall(r"(\${[ug][}%])", idp_vp0))
2323
+ if len(sigils) > 1:
2324
+ t = '\nWARNING: IdP-volume "/%s" created by "/%s" has multiple IdP placeholders: %s'
2325
+ self.idp_warn.append(t % (idp_vp, idp_vp0, list(sigils)))
2326
+ continue
2327
+
2328
+ sigil = sigils.pop()
2329
+ par_vp = idp_vp
2330
+ while par_vp:
2331
+ par_vp = vsplit(par_vp)[0]
2332
+ par_vn, _ = vfs.get(par_vp, "*", False, False)
2333
+ if sigil in par_vn.vpath0:
2334
+ continue # parent was spawned for and by same user
2335
+
2336
+ oth_read = []
2337
+ oth_write = []
2338
+ for usr in par_vn.axs.uread:
2339
+ if usr not in idp_vn.axs.uread:
2340
+ oth_read.append(usr)
2341
+ for usr in par_vn.axs.uwrite:
2342
+ if usr not in idp_vn.axs.uwrite:
2343
+ oth_write.append(usr)
2344
+
2345
+ if "*" in oth_read:
2346
+ taxs = "WORLD-READABLE"
2347
+ elif "*" in oth_write:
2348
+ taxs = "WORLD-WRITABLE"
2349
+ elif oth_read:
2350
+ taxs = "READABLE BY %r" % (oth_read,)
2351
+ elif oth_write:
2352
+ taxs = "WRITABLE BY %r" % (oth_write,)
2353
+ else:
2354
+ break # no sigil; not idp; safe to stop
2355
+
2356
+ t = '\nWARNING: IdP-volume "/%s" created by "/%s" has parent/grandparent "/%s" and would be %s'
2357
+ self.idp_err.append(t % (idp_vp, idp_vp0, par_vn.vpath, taxs))
2358
+
2359
+ if self.idp_warn:
2360
+ t = "WARNING! Some IdP volumes include multiple IdP placeholders; this is too complex to automatically determine if safe or not. To ensure that no users gain unintended access, please use only a single placeholder for each IdP volume."
2361
+ self.log(t + "".join(self.idp_warn), 1)
2362
+
2363
+ if self.idp_err:
2364
+ t = "WARNING! The following IdP volumes are mounted below another volume where other users can read and/or write files. This is a SECURITY HAZARD!! When copyparty is restarted, it will not know about these IdP volumes yet. These volumes will then be accessible by an unexpected set of permissions UNTIL one of the users associated with their volume sends a request to the server. RECOMMENDATION: You should create a restricted volume where nobody can read/write files, and make sure that all IdP volumes are configured to appear somewhere below that volume."
2365
+ self.log(t + "".join(self.idp_err), 1)
2288
2366
 
2289
2367
  self.vfs = vfs
2290
2368
  self.acct = acct
@@ -2319,11 +2397,6 @@ class AuthSrv(object):
2319
2397
  for x, y in vfs.all_vols.items()
2320
2398
  if x != shr and not x.startswith(shrs)
2321
2399
  }
2322
- vfs.all_nodes = {
2323
- x: y
2324
- for x, y in vfs.all_nodes.items()
2325
- if x != shr and not x.startswith(shrs)
2326
- }
2327
2400
 
2328
2401
  assert db and cur and cur2 and shv # type: ignore
2329
2402
  for row in cur.execute("select * from sh"):
@@ -2353,6 +2426,7 @@ class AuthSrv(object):
2353
2426
  else:
2354
2427
  shn.ls = shn._ls
2355
2428
 
2429
+ shn.shr_owner = s_un
2356
2430
  shn.shr_src = (s_vfs, s_rem)
2357
2431
  shn.realpath = s_vfs.canonical(s_rem)
2358
2432
 
@@ -2370,7 +2444,7 @@ class AuthSrv(object):
2370
2444
  continue # also fine
2371
2445
  for zs in svn.nodes.keys():
2372
2446
  # hide subvolume
2373
- vn.nodes[zs] = VFS(self.log_func, "", "", AXS(), {})
2447
+ vn.nodes[zs] = VFS(self.log_func, "", "", "", AXS(), {})
2374
2448
 
2375
2449
  cur2.close()
2376
2450
  cur.close()
@@ -2378,7 +2452,9 @@ class AuthSrv(object):
2378
2452
 
2379
2453
  self.js_ls = {}
2380
2454
  self.js_htm = {}
2381
- for vn in self.vfs.all_nodes.values():
2455
+ for vp, vn in self.vfs.all_nodes.items():
2456
+ if enshare and vp.startswith(shrs):
2457
+ continue # propagates later in this func
2382
2458
  vf = vn.flags
2383
2459
  vn.js_ls = {
2384
2460
  "idx": "e2d" in vf,
@@ -2435,8 +2511,12 @@ class AuthSrv(object):
2435
2511
 
2436
2512
  vols = list(vfs.all_nodes.values())
2437
2513
  if enshare:
2438
- vols.append(shv)
2439
- vols.extend(list(shv.nodes.values()))
2514
+ for vol in shv.nodes.values():
2515
+ if vol.vpath not in vfs.all_nodes:
2516
+ self.log("BUG: /%s not in all_nodes" % (vol.vpath,), 1)
2517
+ vols.append(vol)
2518
+ if shr in vfs.all_nodes:
2519
+ self.log("BUG: %s found in all_nodes" % (shr,), 1)
2440
2520
 
2441
2521
  for vol in vols:
2442
2522
  dbv = vol.get_dbv("")[0]
@@ -2539,8 +2619,8 @@ class AuthSrv(object):
2539
2619
  if not bos.path.exists(ap):
2540
2620
  pwdb = {}
2541
2621
  else:
2542
- with open(ap, "r", encoding="utf-8") as f:
2543
- pwdb = json.load(f)
2622
+ jtxt = read_utf8(self.log, ap, True)
2623
+ pwdb = json.loads(jtxt)
2544
2624
 
2545
2625
  pwdb = [x for x in pwdb if x[0] != uname]
2546
2626
  pwdb.append((uname, self.defpw[uname], hpw))
@@ -2563,8 +2643,8 @@ class AuthSrv(object):
2563
2643
  if not self.args.chpw or not bos.path.exists(ap):
2564
2644
  return
2565
2645
 
2566
- with open(ap, "r", encoding="utf-8") as f:
2567
- pwdb = json.load(f)
2646
+ jtxt = read_utf8(self.log, ap, True)
2647
+ pwdb = json.loads(jtxt)
2568
2648
 
2569
2649
  useen = set()
2570
2650
  urst = set()
@@ -3060,8 +3140,9 @@ def expand_config_file(
3060
3140
  ipath += " -> " + fp
3061
3141
  ret.append("#\033[36m opening cfg file{}\033[0m".format(ipath))
3062
3142
 
3063
- with open(fp, "rb") as f:
3064
- for oln in [x.decode("utf-8").rstrip() for x in f]:
3143
+ cfg_lines = read_utf8(log, fp, True).split("\n")
3144
+ if True: # diff-golf
3145
+ for oln in [x.rstrip() for x in cfg_lines]:
3065
3146
  ln = oln.split(" #")[0].strip()
3066
3147
  if ln.startswith("% "):
3067
3148
  pad = " " * len(oln.split("%")[0])
copyparty/cfg.py CHANGED
@@ -55,6 +55,7 @@ def vf_bmap() :
55
55
  "xdev",
56
56
  "xlink",
57
57
  "xvol",
58
+ "zipmaxu",
58
59
  ):
59
60
  ret[k] = k
60
61
  return ret
@@ -101,6 +102,9 @@ def vf_vmap() :
101
102
  "u2ts",
102
103
  "ups_who",
103
104
  "zip_who",
105
+ "zipmaxn",
106
+ "zipmaxs",
107
+ "zipmaxt",
104
108
  ):
105
109
  ret[k] = k
106
110
  return ret
@@ -299,6 +303,10 @@ flagcats = {
299
303
  "rss": "allow '?rss' URL suffix (experimental)",
300
304
  "ups_who=2": "restrict viewing the list of recent uploads",
301
305
  "zip_who=2": "restrict access to download-as-zip/tar",
306
+ "zipmaxn=9k": "reject download-as-zip if more than 9000 files",
307
+ "zipmaxs=2g": "reject download-as-zip if size over 2 GiB",
308
+ "zipmaxt=no": "reply with 'no' if download-as-zip exceeds max",
309
+ "zipmaxu": "zip-size-limit does not apply to authenticated users",
302
310
  "nopipe": "disable race-the-beam (download unfinished uploads)",
303
311
  "mv_retry": "ms-windows: timeout for renaming busy files",
304
312
  "rm_retry": "ms-windows: timeout for deleting busy files",
copyparty/fsutil.py CHANGED
@@ -72,7 +72,7 @@ class Fstab(object):
72
72
  return vid
73
73
 
74
74
  def build_fallback(self) :
75
- self.tab = VFS(self.log_func, "idk", "/", AXS(), {})
75
+ self.tab = VFS(self.log_func, "idk", "/", "/", AXS(), {})
76
76
  self.trusted = False
77
77
 
78
78
  def build_tab(self) :
@@ -105,9 +105,10 @@ class Fstab(object):
105
105
 
106
106
  tab1.sort(key=lambda x: (len(x[0]), x[0]))
107
107
  path1, fs1 = tab1[0]
108
- tab = VFS(self.log_func, fs1, path1, AXS(), {})
108
+ tab = VFS(self.log_func, fs1, path1, path1, AXS(), {})
109
109
  for path, fs in tab1[1:]:
110
- tab.add(fs, path.lstrip("/"))
110
+ zs = path.lstrip("/")
111
+ tab.add(fs, zs, zs)
111
112
 
112
113
  self.tab = tab
113
114
  self.srctab = srctab
@@ -123,9 +124,10 @@ class Fstab(object):
123
124
  if not self.trusted:
124
125
  # no mtab access; have to build as we go
125
126
  if "/" in rem:
126
- self.tab.add("idk", os.path.join(vn.vpath, rem.split("/")[0]))
127
+ zs = os.path.join(vn.vpath, rem.split("/")[0])
128
+ self.tab.add("idk", zs, zs)
127
129
  if rem:
128
- self.tab.add(nval, path)
130
+ self.tab.add(nval, path, path)
129
131
  else:
130
132
  vn.realpath = nval
131
133
 
copyparty/httpcli.py CHANGED
@@ -22,6 +22,7 @@ from datetime import datetime
22
22
  from operator import itemgetter
23
23
 
24
24
  import jinja2 # typechk
25
+ from ipaddress import IPv6Network
25
26
 
26
27
  try:
27
28
  if os.environ.get("PRTY_NO_LZMA"):
@@ -89,6 +90,7 @@ from .util import (
89
90
  read_socket,
90
91
  read_socket_chunked,
91
92
  read_socket_unbounded,
93
+ read_utf8,
92
94
  relchk,
93
95
  ren_open,
94
96
  runhook,
@@ -382,11 +384,12 @@ class HttpCli(object):
382
384
  t += ' Note: if you are behind cloudflare, then this default header is not a good choice; please first make sure your local reverse-proxy (if any) does not allow non-cloudflare IPs from providing cf-* headers, and then add this additional global setting: "--xff-hdr=cf-connecting-ip"'
383
385
  else:
384
386
  t += ' Note: depending on your reverse-proxy, and/or WAF, and/or other intermediates, you may want to read the true client IP from another header by also specifying "--xff-hdr=SomeOtherHeader"'
385
- zs = (
386
- ".".join(pip.split(".")[:2]) + "."
387
- if "." in pip
388
- else ":".join(pip.split(":")[:4]) + ":"
389
- ) + "0.0/16"
387
+
388
+ if "." in pip:
389
+ zs = ".".join(pip.split(".")[:2]) + ".0.0/16"
390
+ else:
391
+ zs = IPv6Network(pip + "/64", False).compressed
392
+
390
393
  zs2 = ' or "--xff-src=lan"' if self.conn.xff_lan.map(pip) else ""
391
394
  self.log(t % (self.args.xff_hdr, pip, cli_ip, zso, zs, zs2), 3)
392
395
  self.bad_xff = True
@@ -863,8 +866,7 @@ class HttpCli(object):
863
866
  html = html.replace("%", "", 1)
864
867
 
865
868
  if html.startswith("@"):
866
- with open(html[1:], "rb") as f:
867
- html = f.read().decode("utf-8")
869
+ html = read_utf8(self.log, html[1:], True)
868
870
 
869
871
  if html.startswith("%"):
870
872
  html = html[1:]
@@ -1231,14 +1233,7 @@ class HttpCli(object):
1231
1233
  return self.tx_404(True)
1232
1234
  else:
1233
1235
  vfs = self.asrv.vfs
1234
- if (
1235
- not vfs.nodes
1236
- and not vfs.axs.uread
1237
- and not vfs.axs.uwrite
1238
- and not vfs.axs.uget
1239
- and not vfs.axs.uhtml
1240
- and not vfs.axs.uadmin
1241
- ):
1236
+ if vfs.badcfg1:
1242
1237
  t = "<h2>access denied due to failsafe; check server log</h2>"
1243
1238
  html = self.j2s("splash", this=self, msg=t)
1244
1239
  self.reply(html.encode("utf-8", "replace"), 500)
@@ -3720,8 +3715,7 @@ class HttpCli(object):
3720
3715
  continue
3721
3716
  fn = "%s/%s" % (abspath, fn)
3722
3717
  if bos.path.isfile(fn):
3723
- with open(fsenc(fn), "rb") as f:
3724
- logues[n] = f.read().decode("utf-8")
3718
+ logues[n] = read_utf8(self.log, fsenc(fn), False)
3725
3719
  if "exp" in vn.flags:
3726
3720
  logues[n] = self._expand(
3727
3721
  logues[n], vn.flags.get("exp_lg") or []
@@ -3742,9 +3736,8 @@ class HttpCli(object):
3742
3736
  for fn in fns:
3743
3737
  fn = "%s/%s" % (abspath, fn)
3744
3738
  if bos.path.isfile(fn):
3745
- with open(fsenc(fn), "rb") as f:
3746
- txt = f.read().decode("utf-8")
3747
- break
3739
+ txt = read_utf8(self.log, fsenc(fn), False)
3740
+ break
3748
3741
 
3749
3742
  if txt and "exp" in vn.flags:
3750
3743
  txt = self._expand(txt, vn.flags.get("exp_md") or [])
@@ -3777,6 +3770,16 @@ class HttpCli(object):
3777
3770
 
3778
3771
  return txt
3779
3772
 
3773
+ def _can_zip(self, volflags ) :
3774
+ lvl = volflags["zip_who"]
3775
+ if self.args.no_zip or not lvl:
3776
+ return "download-as-zip/tar is disabled in server config"
3777
+ elif lvl <= 1 and not self.can_admin:
3778
+ return "download-as-zip/tar is admin-only on this server"
3779
+ elif lvl <= 2 and self.uname in ("", "*"):
3780
+ return "you must be authenticated to download-as-zip/tar on this server"
3781
+ return ""
3782
+
3780
3783
  def tx_res(self, req_path ) :
3781
3784
  status = 200
3782
3785
  logmsg = "{:4} {} ".format("", self.req)
@@ -4307,13 +4310,8 @@ class HttpCli(object):
4307
4310
  rem ,
4308
4311
  items ,
4309
4312
  ) :
4310
- lvl = vn.flags["zip_who"]
4311
- if self.args.no_zip or not lvl:
4312
- raise Pebkac(400, "download-as-zip/tar is disabled in server config")
4313
- elif lvl <= 1 and not self.can_admin:
4314
- raise Pebkac(400, "download-as-zip/tar is admin-only on this server")
4315
- elif lvl <= 2 and self.uname in ("", "*"):
4316
- t = "you must be authenticated to download-as-zip/tar on this server"
4313
+ t = self._can_zip(vn.flags)
4314
+ if t:
4317
4315
  raise Pebkac(400, t)
4318
4316
 
4319
4317
  logmsg = "{:4} {} ".format("", self.req)
@@ -4346,6 +4344,33 @@ class HttpCli(object):
4346
4344
  else:
4347
4345
  fn = self.host.split(":")[0]
4348
4346
 
4347
+ if vn.flags.get("zipmax") and (not self.uname or not "zipmaxu" in vn.flags):
4348
+ maxs = vn.flags.get("zipmaxs_v") or 0
4349
+ maxn = vn.flags.get("zipmaxn_v") or 0
4350
+ nf = 0
4351
+ nb = 0
4352
+ fgen = vn.zipgen(
4353
+ vpath, rem, set(items), self.uname, False, not self.args.no_scandir
4354
+ )
4355
+ t = "total size exceeds a limit specified in server config"
4356
+ t = vn.flags.get("zipmaxt") or t
4357
+ if maxs and maxn:
4358
+ for zd in fgen:
4359
+ nf += 1
4360
+ nb += zd["st"].st_size
4361
+ if maxs < nb or maxn < nf:
4362
+ raise Pebkac(400, t)
4363
+ elif maxs:
4364
+ for zd in fgen:
4365
+ nb += zd["st"].st_size
4366
+ if maxs < nb:
4367
+ raise Pebkac(400, t)
4368
+ elif maxn:
4369
+ for zd in fgen:
4370
+ nf += 1
4371
+ if maxn < nf:
4372
+ raise Pebkac(400, t)
4373
+
4349
4374
  safe = (string.ascii_letters + string.digits).replace("%", "")
4350
4375
  afn = "".join([x if x in safe.replace('"', "") else "_" for x in fn])
4351
4376
  bascii = unicode(safe).encode("utf-8")
@@ -4991,6 +5016,8 @@ class HttpCli(object):
4991
5016
  def get_dls(self) :
4992
5017
  ret = []
4993
5018
  dls = self.conn.hsrv.tdls
5019
+ enshare = self.args.shr
5020
+ shrs = enshare[1:]
4994
5021
  for dl_id, (t0, sz, vn, vp, uname) in self.conn.hsrv.tdli.items():
4995
5022
  t1, sent = dls[dl_id]
4996
5023
  if sent > 0x100000: # 1m; buffers 2~4
@@ -4999,6 +5026,15 @@ class HttpCli(object):
4999
5026
  vp = ""
5000
5027
  elif self.uname not in vn.axs.udot and (vp.startswith(".") or "/." in vp):
5001
5028
  vp = ""
5029
+ elif (
5030
+ enshare
5031
+ and vp.startswith(shrs)
5032
+ and self.uname != vn.shr_owner
5033
+ and self.uname not in vn.axs.uadmin
5034
+ and self.uname not in self.args.shr_adm
5035
+ and not dl_id.startswith(self.ip + ":")
5036
+ ):
5037
+ vp = ""
5002
5038
  if self.uname not in vn.axs.uadmin:
5003
5039
  dl_id = uname = ""
5004
5040
 
@@ -5980,6 +6016,8 @@ class HttpCli(object):
5980
6016
  zs = self.gen_fk(2, self.args.dk_salt, abspath, 0, 0)[:add_dk]
5981
6017
  ls_ret["dk"] = cgv["dk"] = zs
5982
6018
 
6019
+ no_zip = bool(self._can_zip(vf))
6020
+
5983
6021
  dirs = []
5984
6022
  files = []
5985
6023
  ptn_hr = RE_HR
@@ -6005,7 +6043,7 @@ class HttpCli(object):
6005
6043
  is_dir = stat.S_ISDIR(inf.st_mode)
6006
6044
  if is_dir:
6007
6045
  href += "/"
6008
- if self.args.no_zip:
6046
+ if no_zip:
6009
6047
  margin = "DIR"
6010
6048
  elif add_dk:
6011
6049
  zs = absreal(fspath)
@@ -6018,7 +6056,7 @@ class HttpCli(object):
6018
6056
  quotep(href),
6019
6057
  )
6020
6058
  elif fn in hist:
6021
- margin = '<a href="%s.hist/%s">#%s</a>' % (
6059
+ margin = '<a href="%s.hist/%s" rel="nofollow">#%s</a>' % (
6022
6060
  base,
6023
6061
  html_escape(hist[fn][2], quot=True, crlf=True),
6024
6062
  hist[fn][0],
@@ -6229,9 +6267,7 @@ class HttpCli(object):
6229
6267
  docpath = os.path.join(abspath, doc)
6230
6268
  sz = bos.path.getsize(docpath)
6231
6269
  if sz < 1024 * self.args.txt_max:
6232
- with open(fsenc(docpath), "rb") as f:
6233
- doctxt = f.read().decode("utf-8", "replace")
6234
-
6270
+ doctxt = read_utf8(self.log, fsenc(docpath), False)
6235
6271
  if doc.lower().endswith(".md") and "exp" in vn.flags:
6236
6272
  doctxt = self._expand(doctxt, vn.flags.get("exp_md") or [])
6237
6273
  else:
copyparty/svchub.py CHANGED
@@ -1250,7 +1250,7 @@ class SvcHub(object):
1250
1250
  raise
1251
1251
 
1252
1252
  def check_mp_support(self) :
1253
- if MACOS:
1253
+ if MACOS and not os.environ.get("PRTY_FORCE_MP"):
1254
1254
  return "multiprocessing is wonky on mac osx;"
1255
1255
  elif sys.version_info < (3, 3):
1256
1256
  return "need python 3.3 or newer for multiprocessing;"
@@ -1270,7 +1270,7 @@ class SvcHub(object):
1270
1270
  return False
1271
1271
 
1272
1272
  try:
1273
- if mp.cpu_count() <= 1:
1273
+ if mp.cpu_count() <= 1 and not os.environ.get("PRTY_FORCE_MP"):
1274
1274
  raise Exception()
1275
1275
  except:
1276
1276
  self.log("svchub", "only one CPU detected; multiprocessing disabled")
copyparty/up2k.py CHANGED
@@ -1112,7 +1112,7 @@ class Up2k(object):
1112
1112
  ft = "\033[0;32m{}{:.0}"
1113
1113
  ff = "\033[0;35m{}{:.0}"
1114
1114
  fv = "\033[0;36m{}:\033[90m{}"
1115
- zs = "ext_th_d html_head mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot"
1115
+ zs = "ext_th_d html_head mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
1116
1116
  fx = set(zs.split())
1117
1117
  fd = vf_bmap()
1118
1118
  fd.update(vf_cmap())
@@ -3410,6 +3410,7 @@ class Up2k(object):
3410
3410
  rm = False,
3411
3411
  lmod = 0,
3412
3412
  fsrc = None,
3413
+ is_mv = False,
3413
3414
  ) :
3414
3415
  if src == dst or (fsrc and fsrc == dst):
3415
3416
  t = "symlinking a file to itself?? orig(%s) fsrc(%s) link(%s)"
@@ -3426,7 +3427,7 @@ class Up2k(object):
3426
3427
 
3427
3428
  linked = False
3428
3429
  try:
3429
- if not flags.get("dedup"):
3430
+ if not is_mv and not flags.get("dedup"):
3430
3431
  raise Exception("dedup is disabled in config")
3431
3432
 
3432
3433
  lsrc = src
@@ -4578,7 +4579,7 @@ class Up2k(object):
4578
4579
  dlink = bos.readlink(sabs)
4579
4580
  dlink = os.path.join(os.path.dirname(sabs), dlink)
4580
4581
  dlink = bos.path.abspath(dlink)
4581
- self._symlink(dlink, dabs, dvn.flags, lmod=ftime)
4582
+ self._symlink(dlink, dabs, dvn.flags, lmod=ftime, is_mv=True)
4582
4583
  wunlink(self.log, sabs, svn.flags)
4583
4584
  else:
4584
4585
  atomic_move(self.log, sabs, dabs, svn.flags)
@@ -4796,7 +4797,7 @@ class Up2k(object):
4796
4797
  flags = self.flags.get(ptop) or {}
4797
4798
  atomic_move(self.log, sabs, slabs, flags)
4798
4799
  bos.utime(slabs, (int(time.time()), int(mt)), False)
4799
- self._symlink(slabs, sabs, flags, False)
4800
+ self._symlink(slabs, sabs, flags, False, is_mv=True)
4800
4801
  full[slabs] = (ptop, rem)
4801
4802
  sabs = slabs
4802
4803
 
@@ -4855,7 +4856,9 @@ class Up2k(object):
4855
4856
  # (for example a volume with symlinked dupes but no --dedup);
4856
4857
  # fsrc=sabs is then a source that currently resolves to copy
4857
4858
 
4858
- self._symlink(dabs, alink, flags, False, lmod=lmod or 0, fsrc=sabs)
4859
+ self._symlink(
4860
+ dabs, alink, flags, False, lmod=lmod or 0, fsrc=sabs, is_mv=True
4861
+ )
4859
4862
 
4860
4863
  return len(full) + len(links)
4861
4864
 
copyparty/util.py CHANGED
@@ -572,6 +572,38 @@ except Exception as ex:
572
572
  print("using fallback base64 codec due to %r" % (ex,))
573
573
 
574
574
 
575
+ class NotUTF8(Exception):
576
+ pass
577
+
578
+
579
+ def read_utf8(log , ap , strict ) :
580
+ with open(ap, "rb") as f:
581
+ buf = f.read()
582
+
583
+ try:
584
+ return buf.decode("utf-8", "strict")
585
+ except UnicodeDecodeError as ex:
586
+ eo = ex.start
587
+ eb = buf[eo : eo + 1]
588
+
589
+ if not strict:
590
+ t = "WARNING: The file [%s] is not using the UTF-8 character encoding; some characters in the file will be skipped/ignored. The first unreadable character was byte %r at offset %d. Please convert this file to UTF-8 by opening the file in your text-editor and saving it as UTF-8."
591
+ t = t % (ap, eb, eo)
592
+ if log:
593
+ log(t, 3)
594
+ else:
595
+ print(t)
596
+ return buf.decode("utf-8", "replace")
597
+
598
+ t = "ERROR: The file [%s] is not using the UTF-8 character encoding, and cannot be loaded. The first unreadable character was byte %r at offset %d. Please convert this file to UTF-8 by opening the file in your text-editor and saving it as UTF-8."
599
+ t = t % (ap, eb, eo)
600
+ if log:
601
+ log(t, 3)
602
+ else:
603
+ print(t)
604
+ raise NotUTF8(t)
605
+
606
+
575
607
  class Daemon(threading.Thread):
576
608
  def __init__(
577
609
  self,
Binary file
copyparty/web/up2k.js.gz CHANGED
Binary file
copyparty/web/util.js.gz CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: copyparty
3
- Version: 1.16.16
3
+ Version: 1.16.17
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
@@ -157,6 +157,7 @@ turn almost any device into a file server with resumable uploads/downloads using
157
157
  * [custom mimetypes](#custom-mimetypes) - change the association of a file extension
158
158
  * [GDPR compliance](#GDPR-compliance) - imagine using copyparty professionally...
159
159
  * [feature chickenbits](#feature-chickenbits) - buggy feature? rip it out
160
+ * [feature beefybits](#feature-beefybits) - force-enable incompatible features
160
161
  * [packages](#packages) - the party might be closer than you think
161
162
  * [arch package](#arch-package) - now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
162
163
  * [fedora package](#fedora-package) - does not exist yet
@@ -1893,7 +1894,7 @@ tell search engines you don't wanna be indexed, either using the good old [robo
1893
1894
  * volflag `[...]:c,norobots` does the same thing for that single volume
1894
1895
  * volflag `[...]:c,robots` ALLOWS search-engine crawling for that volume, even if `--no-robots` is set globally
1895
1896
 
1896
- also, `--force-js` disables the plain HTML folder listing, making things harder to parse for search engines
1897
+ also, `--force-js` disables the plain HTML folder listing, making things harder to parse for *some* search engines -- note that crawlers which understand javascript (such as google) will not be affected
1897
1898
 
1898
1899
 
1899
1900
  ## themes
@@ -2194,6 +2195,15 @@ buggy feature? rip it out by setting any of the following environment variables
2194
2195
  example: `PRTY_NO_IFADDR=1 python3 copyparty-sfx.py`
2195
2196
 
2196
2197
 
2198
+ ### feature beefybits
2199
+
2200
+ force-enable features with known issues on your OS/env by setting any of the following environment variables, also affectionately known as `fuckitbits` or `hail-mary-bits`
2201
+
2202
+ | env-var | what it does |
2203
+ | ------------------------ | ------------ |
2204
+ | `PRTY_FORCE_MP` | force-enable multiprocessing (real multithreading) on MacOS and other broken platforms |
2205
+
2206
+
2197
2207
  # packages
2198
2208
 
2199
2209
  the party might be closer than you think
@@ -1,17 +1,17 @@
1
1
  copyparty/__init__.py,sha256=VR6ZZhB9IxaK5TDXDTBM_OIP5ydkrdbaEnstktLM__s,2649
2
- copyparty/__main__.py,sha256=JDjMsCiOMMBPs8HR8vpgEJMgnxq-qjX-hcgxKHF9WX4,117137
3
- copyparty/__version__.py,sha256=k9LRMLdD2c_LzklrEl5kBzwA4LXuQlPPCEKasLrNXqw,252
4
- copyparty/authsrv.py,sha256=iWMMp4hqejVDyK5g3hwK4xZ9oZIqWBa_OBMr8pPAM2g,107740
2
+ copyparty/__main__.py,sha256=hlYwQuq_c1ML_Mkn4U3YSOllU-SavqtkMt72VMdi9os,117346
3
+ copyparty/__version__.py,sha256=ouQegJYN1YmTy_TtphfQsiIWHiCpzBMamgsfT-_sPxU,252
4
+ copyparty/authsrv.py,sha256=nZ27CC3HxwVbbCLPMEO9v6NZV7pLYP3euwrQ9qwT3gg,111163
5
5
  copyparty/broker_mp.py,sha256=QdOXXvV2Xn6J0CysEqyY3GZbqxQMyWnTpnba-a5lMc0,4987
6
6
  copyparty/broker_mpw.py,sha256=PpSS4SK3pItlpfD8OwVr3QmJEPKlUgaf2nuMOozixgU,3347
7
7
  copyparty/broker_thr.py,sha256=fjoYtpSscUA7-nMl4r1n2R7UK3J9lrvLS3rUZ-iJzKQ,1721
8
8
  copyparty/broker_util.py,sha256=76mfnFOpX1gUUvtjm8UQI7jpTIaVINX10QonM-B7ggc,1680
9
9
  copyparty/cert.py,sha256=0ZAPeXeMR164vWn9GQU3JDKooYXEq_NOQkDeg543ivg,8009
10
- copyparty/cfg.py,sha256=7LDVvUkhcqArE3Jmz8JVjcJC6xPvAjf2g0lUhw6p1k4,13634
10
+ copyparty/cfg.py,sha256=cL2_gysqgLR6kU_sqaq6XheED0jbbTcqBFGEnYj_pA4,13996
11
11
  copyparty/dxml.py,sha256=vu5uZQtwvwoqnFHbULs2Zh_y2DETu0T-ENpMZ1i2CV4,2505
12
- copyparty/fsutil.py,sha256=IVOFG8zBQPMQDDv7RIStSJHwHiAnVNROZS37O5k465A,4524
12
+ copyparty/fsutil.py,sha256=NC_CJC4TDag399vVDH9_uQfdfpTMwRFLNxERSWhlVvs,4594
13
13
  copyparty/ftpd.py,sha256=T97SFS7JFtvRLbJX8C4fJSYwe13vhN3-E6emtlVmqLA,17608
14
- copyparty/httpcli.py,sha256=DT2ZV-Hrxg1itfmeTkkHIctJHdG8nxtnIZJ8Whufxxo,219291
14
+ copyparty/httpcli.py,sha256=O7iCliCPY57KzD_QI64cu7kS8UQIv1fo4BpmXo783kg,220444
15
15
  copyparty/httpconn.py,sha256=mQSgljh0Q-jyWjF4tQLrHbRKRe9WKl19kGqsGMsJpWo,6880
16
16
  copyparty/httpsrv.py,sha256=pxH_Eh8ElBLvOEDejgpP9Bvk65HNEou-03aYIcgXhrs,18090
17
17
  copyparty/ico.py,sha256=eWSxEae4wOCfheHl-m-wchYvFRAR_97kJDb4NGaB-Z8,3561
@@ -24,15 +24,15 @@ copyparty/smbd.py,sha256=dixFl2wlWymq_Cycc8a4cVB4gY8RSg2e3tE7Xr-aDa0,14614
24
24
  copyparty/ssdp.py,sha256=R1Z61GZOxBMF2Sk4RTxKWMOemogmcjEWG-CvLihd45k,7023
25
25
  copyparty/star.py,sha256=tV5BbX6AiQ7N4UU8DYtSTckNYeoeey4DBqq4LjfymbY,3818
26
26
  copyparty/sutil.py,sha256=6zEEGl4hRe6bTB83Y_RtnBGxr2JcUa__GdiAMqNJZnY,3208
27
- copyparty/svchub.py,sha256=gBp7x1hGF4b6_nanW5QUQcz8UmMCad6DzdE2KD5cLvE,41462
27
+ copyparty/svchub.py,sha256=EPkbchicmP5Acq64rhpIshL-zvGEltcRJelkzW_dx5s,41542
28
28
  copyparty/szip.py,sha256=HFtnwOiBgx0HMLUf-h_T84zSlRijPxmhRo5PM613kRA,8602
29
29
  copyparty/tcpsrv.py,sha256=2q18dGR8jnezA4SMfUXa-wrGRGX3nHIwkxkWvkTzF2A,19889
30
30
  copyparty/tftpd.py,sha256=PXgG4rTmiaU_TavSyZWD5cFphdfChs9YvNY21qfExt8,13611
31
31
  copyparty/th_cli.py,sha256=PxDAmUvO_8Vm5edXiWtsCft0Fw69QL9rCHf9zLmUNeA,4800
32
32
  copyparty/th_srv.py,sha256=tHbh_Ve3v8tYclWH2thLs5oFufeXgJi1duUMveKIx9k,30725
33
33
  copyparty/u2idx.py,sha256=G6MDbD4I_sJSOwaNFZ6XLTQhnEDrB12pVKuKhzQ_leE,13676
34
- copyparty/up2k.py,sha256=c8llviRN10hoFHTE-z-APwGnzQ6mVC4KrNIcngusIU0,177199
35
- copyparty/util.py,sha256=Y_znSn3hBNYaaduwcCB7mmBYsi6vv9CYC1zJ9rq9yeQ,99435
34
+ copyparty/up2k.py,sha256=abULiz0KK3dcuw9hsapJBnueaqrCWry6rQe8VgsJITM,177330
35
+ copyparty/util.py,sha256=QOl_gNH8eHYNrnvoNPpGgkGAq8vV-r_Kr5Tp30qZo-M,100520
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=r2c_hOZV_RTyl4CqWWX14FDWP8nnDVwGkDl4Sfk0rU4,8239
58
58
  copyparty/web/browser.css.gz,sha256=_HiFW5vPUusWadoqdY8ZihuWizY9UECAc5nIamBPRi4,11654
59
59
  copyparty/web/browser.html,sha256=auvhLVE_t0aIN0q-nk0zOWFqITgDhroMAAviBNLoFfc,4788
60
- copyparty/web/browser.js.gz,sha256=dZ0sELnKXMnfQ_hrYb19xZ0SE9wFlYbDOCy-TEnFono,92091
60
+ copyparty/web/browser.js.gz,sha256=48fDqIqQKCkrH0VsYVj03sDOx9gEZ-DfiHDaqEUuyr0,92341
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
@@ -83,8 +83,8 @@ copyparty/web/splash.js.gz,sha256=4VqNznN10-bT33IJm3VWzBEJ1s08XZyxFB1TYPUkuAo,27
83
83
  copyparty/web/svcs.html,sha256=dnE1fG15zOpq7u0GYt8ij6BUv_LTwsiipFeneVYlMsM,14140
84
84
  copyparty/web/svcs.js.gz,sha256=lMXEP9W-VlXyANlva4q0ASSxvvHYlE2CrmxGgZXZop0,713
85
85
  copyparty/web/ui.css.gz,sha256=0sHIwGsL3_xH8Uu6N0Ag3bKBTjf-e_yfFbKynEZXAnk,2800
86
- copyparty/web/up2k.js.gz,sha256=0XPd3HafOgJe2TlCc2VIgpJCcR0SI0adwM-MjpT08qo,24071
87
- copyparty/web/util.js.gz,sha256=wD3tP5j1iE5Uj5AvLW5zZbQJXDIFDlqgBTGdXeRVqo0,15110
86
+ copyparty/web/up2k.js.gz,sha256=VQVWBXK2gEz1b8if_ujXHNHnfBO7cdrKoSjqX397VUI,24519
87
+ copyparty/web/util.js.gz,sha256=rD9iLfVLKRhxC8hmakal-s18xN_rs6GuOqyRPii6HQ8,15110
88
88
  copyparty/web/w.hash.js.gz,sha256=JhJagnqIkcKng_hs6otEgzcuQE7keToG_r5dd2o3EfU,1108
89
89
  copyparty/web/a/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
90
90
  copyparty/web/a/partyfuse.py,sha256=9p5Hpg_IBiSimv7j9kmPhCGpy-FLXSRUOYnLjJ5JifU,28049
@@ -109,9 +109,9 @@ copyparty/web/deps/prismd.css.gz,sha256=ObUlksQVr-OuYlTz-I4B23TeBg2QDVVGRnWBz8cV
109
109
  copyparty/web/deps/scp.woff2,sha256=w99BDU5i8MukkMEL-iW0YO9H4vFFZSPWxbkH70ytaAg,8612
110
110
  copyparty/web/deps/sha512.ac.js.gz,sha256=lFZaCLumgWxrvEuDr4bqdKHsqjX82AbVAb7_F45Yk88,7033
111
111
  copyparty/web/deps/sha512.hw.js.gz,sha256=UAed2_ocklZCnIzcSYz2h4P1ycztlCLj-ewsRTud2lU,7939
112
- copyparty-1.16.16.dist-info/LICENSE,sha256=gOr4h33pCsBEg9uIy9AYmb7qlocL4V9t2uPJS5wllr0,1072
113
- copyparty-1.16.16.dist-info/METADATA,sha256=n_4WfJgIHsqlaYd8Mq5G0e9bK9Z-v4fpIiayX6tmXfM,158045
114
- copyparty-1.16.16.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
115
- copyparty-1.16.16.dist-info/entry_points.txt,sha256=4zw6a3rqASywQomiYLObjjlxybaI65LYYOTJwgKz7b0,128
116
- copyparty-1.16.16.dist-info/top_level.txt,sha256=LnYUPsDyk-8kFgM6YJLG4h820DQekn81cObKSu9g-sI,10
117
- copyparty-1.16.16.dist-info/RECORD,,
112
+ copyparty-1.16.17.dist-info/LICENSE,sha256=gOr4h33pCsBEg9uIy9AYmb7qlocL4V9t2uPJS5wllr0,1072
113
+ copyparty-1.16.17.dist-info/METADATA,sha256=mv4zOlv-ElE_ldsed5zneXRYtUjsFF5L9vCh7cTSi7Q,158632
114
+ copyparty-1.16.17.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
115
+ copyparty-1.16.17.dist-info/entry_points.txt,sha256=4zw6a3rqASywQomiYLObjjlxybaI65LYYOTJwgKz7b0,128
116
+ copyparty-1.16.17.dist-info/top_level.txt,sha256=LnYUPsDyk-8kFgM6YJLG4h820DQekn81cObKSu9g-sI,10
117
+ copyparty-1.16.17.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.2)
2
+ Generator: setuptools (76.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5