copyparty 1.17.1__py3-none-any.whl → 1.18.0__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
@@ -956,6 +956,7 @@ def add_general(ap, nc, srvname):
956
956
  ap2.add_argument("--name", metavar="TXT", type=u, default=srvname, help="server name (displayed topleft in browser and in mDNS)")
957
957
  ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]")
958
958
  ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit")
959
+ ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)")
959
960
  ap2.add_argument("--license", action="store_true", help="show licenses and exit")
960
961
  ap2.add_argument("--version", action="store_true", help="show versions and exit")
961
962
 
@@ -1020,6 +1021,7 @@ def add_upload(ap):
1020
1021
  ap2.add_argument("--df", metavar="GiB", type=u, default="0", help="ensure \033[33mGiB\033[0m free disk space by rejecting upload requests; assumes gigabytes unless a unit suffix is given: [\033[32m256m\033[0m], [\033[32m4\033[0m], [\033[32m2T\033[0m] (volflag=df)")
1021
1022
  ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
1022
1023
  ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; [\033[32m-1\033[0m] = forbidden/always-off, [\033[32m0\033[0m] = default-off and warn if enabled, [\033[32m1\033[0m] = default-off, [\033[32m2\033[0m] = on, [\033[32m3\033[0m] = on and disable datecheck")
1024
+ ap2.add_argument("--nosubtle", metavar="N", type=int, default=0, help="when to use a wasm-hasher instead of the browser's builtin; faster on chrome, but buggy in older chrome versions. [\033[32m0\033[0m] = only when necessary (non-https), [\033[32m1\033[0m] = always (all browsers), [\033[32m2\033[0m] = always on chrome/firefox, [\033[32m3\033[0m] = always on chrome, [\033[32mN\033[0m] = chrome-version N and newer (recommendation: 137)")
1023
1025
  ap2.add_argument("--u2j", metavar="JOBS", type=int, default=2, help="web-client: number of file chunks to upload in parallel; 1 or 2 is good when latency is low (same-country), 2~4 for android-clients, 2~6 for cross-atlantic. Max is 6 in most browsers. Big values increase network-speed but may reduce HDD-speed")
1024
1026
  ap2.add_argument("--u2sz", metavar="N,N,N", type=u, default="1,64,96", help="web-client: default upload chunksize (MiB); sets \033[33mmin,default,max\033[0m in the settings gui. Each HTTP POST will aim for \033[33mdefault\033[0m, and never exceed \033[33mmax\033[0m. Cloudflare max is 96. Big values are good for cross-atlantic but may increase HDD fragmentation on some FS. Disable this optimization with [\033[32m1,1,1\033[0m]")
1025
1027
  ap2.add_argument("--u2ow", metavar="NUM", type=int, default=0, help="web-client: default setting for when to replace/overwrite existing files; [\033[32m0\033[0m]=never, [\033[32m1\033[0m]=if-client-newer, [\033[32m2\033[0m]=always (volflag=u2ow)")
@@ -1261,6 +1263,7 @@ def add_optouts(ap):
1261
1263
  ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)")
1262
1264
  ap2.add_argument("--no-lifetime", action="store_true", help="do not allow clients (or server config) to schedule an upload to be deleted after a given time")
1263
1265
  ap2.add_argument("--no-pipe", action="store_true", help="disable race-the-beam (lockstep download of files which are currently being uploaded) (volflag=nopipe)")
1266
+ ap2.add_argument("--no-tail", action="store_true", help="disable streaming a growing files with ?tail (volflag=notail)")
1264
1267
  ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader-IP into the database; will also disable unpost, you may want \033[32m--forget-ip\033[0m instead (volflag=no_db_ip)")
1265
1268
 
1266
1269
 
@@ -1390,6 +1393,16 @@ def add_transcoding(ap):
1390
1393
  ap2.add_argument("--ac-maxage", metavar="SEC", type=int, default=86400, help="delete cached transcode output after \033[33mSEC\033[0m seconds")
1391
1394
 
1392
1395
 
1396
+ def add_tail(ap):
1397
+ ap2 = ap.add_argument_group('tailing options (realtime streaming of a growing file)')
1398
+ ap2.add_argument("--tail-who", metavar="LVL", type=int, default=2, help="who can tail? [\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=tail_who)")
1399
+ ap2.add_argument("--tail-cmax", metavar="N", type=int, default=64, help="do not allow starting a new tail if more than \033[33mN\033[0m active downloads")
1400
+ ap2.add_argument("--tail-tmax", metavar="SEC", type=float, default=0, help="terminate connection after \033[33mSEC\033[0m seconds; [\033[32m0\033[0m]=never (volflag=tail_tmax)")
1401
+ ap2.add_argument("--tail-rate", metavar="SEC", type=float, default=0.2, help="check for new data every \033[33mSEC\033[0m seconds (volflag=tail_rate)")
1402
+ ap2.add_argument("--tail-ka", metavar="SEC", type=float, default=3.0, help="send a zerobyte if connection is idle for \033[33mSEC\033[0m seconds to prevent disconnect")
1403
+ ap2.add_argument("--tail-fd", metavar="SEC", type=float, default=1.0, help="check if file was replaced (new fd) if idle for \033[33mSEC\033[0m seconds (volflag=tail_fd)")
1404
+
1405
+
1393
1406
  def add_rss(ap):
1394
1407
  ap2 = ap.add_argument_group('RSS options')
1395
1408
  ap2.add_argument("--rss", action="store_true", help="enable RSS output (experimental) (volflag=rss)")
@@ -1485,6 +1498,7 @@ def add_ui(ap, retry):
1485
1498
  ap2.add_argument("--sort", metavar="C,C,C", type=u, default="href", help="default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: \033[32mhref -href ext sz ts tags/Album tags/.tn\033[0m (volflag=sort)")
1486
1499
  ap2.add_argument("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)")
1487
1500
  ap2.add_argument("--hsortn", metavar="N", type=int, default=2, help="number of sorting rules to include in media URLs by default (volflag=hsortn)")
1501
+ ap2.add_argument("--see-dots", action="store_true", help="default-enable seeing dotfiles; only takes effect if user has the necessary permissions")
1488
1502
  ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files matching \033[33mREGEX\033[0m in file list. Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)")
1489
1503
  ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable")
1490
1504
  ap2.add_argument("--ext-th", metavar="E=VP", type=u, action="append", help="use thumbnail-image \033[33mVP\033[0m for file-extension \033[33mE\033[0m, example: [\033[32mexe=/.res/exe.png\033[0m] (volflag=ext_th)")
@@ -1594,6 +1608,7 @@ def run_argparse(
1594
1608
  add_hooks(ap)
1595
1609
  add_stats(ap)
1596
1610
  add_txt(ap)
1611
+ add_tail(ap)
1597
1612
  add_og(ap)
1598
1613
  add_ui(ap, retry)
1599
1614
  add_admin(ap)
copyparty/__version__.py CHANGED
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 17, 1)
4
- CODENAME = "mixtape.m3u"
5
- BUILD_DT = (2025, 5, 18)
3
+ VERSION = (1, 18, 0)
4
+ CODENAME = "logtail"
5
+ BUILD_DT = (2025, 6, 22)
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
@@ -65,7 +65,9 @@ SSEELOG = " ({})".format(SEE_LOG)
65
65
  BAD_CFG = "invalid config; {}".format(SEE_LOG)
66
66
  SBADCFG = " ({})".format(BAD_CFG)
67
67
 
68
- PTN_U_GRP = re.compile(r"\$\{u%([+-])([^}]+)\}")
68
+ PTN_U_GRP = re.compile(r"\$\{u(%[+-][^}]+)\}")
69
+ PTN_G_GRP = re.compile(r"\$\{g(%[+-][^}]+)\}")
70
+ PTN_SIGIL = re.compile(r"(\${[ug][}%])")
69
71
 
70
72
 
71
73
  class CfgEx(Exception):
@@ -350,7 +352,6 @@ class VFS(object):
350
352
  self.flags = flags # config options
351
353
  self.root = self
352
354
  self.dev = 0 # st_dev
353
- self.badcfg1 = False
354
355
  self.nodes = {} # child nodes
355
356
  self.histtab = {} # all realpath->histpath
356
357
  self.dbpaths = {} # all realpath->dbpath
@@ -870,6 +871,7 @@ class AuthSrv(object):
870
871
  self.warn_anonwrite = warn_anonwrite
871
872
  self.line_ctr = 0
872
873
  self.indent = ""
874
+ self.is_lxc = args.c == ["/z/initcfg"]
873
875
 
874
876
  # fwd-decl
875
877
  self.vfs = VFS(log_func, "", "", "", AXS(), {})
@@ -880,6 +882,8 @@ class AuthSrv(object):
880
882
  self.defpw = {}
881
883
  self.grps = {}
882
884
  self.re_pwd = None
885
+ self.cfg_files_loaded = []
886
+ self.badcfg1 = False
883
887
 
884
888
  # all volumes observed since last restart
885
889
  self.idp_vols = {} # vpath->abspath
@@ -956,15 +960,27 @@ class AuthSrv(object):
956
960
  un_gn = [("", "")]
957
961
 
958
962
  for un, gn in un_gn:
959
- m = PTN_U_GRP.search(dst0)
960
- if m:
961
- req, gnc = m.groups()
962
- hit = gnc in (un_gns.get(un) or [])
963
- if req == "+":
964
- if not hit:
965
- continue
966
- elif hit:
963
+ rejected = False
964
+ for ptn in [PTN_U_GRP, PTN_G_GRP]:
965
+ m = ptn.search(dst0)
966
+ if not m:
967
967
  continue
968
+ zs = m.group(1)
969
+ zs = zs.replace(",%+", "\n%+")
970
+ zs = zs.replace(",%-", "\n%-")
971
+ for rule in zs.split("\n"):
972
+ gnc = rule[2:]
973
+ if ptn == PTN_U_GRP:
974
+ # is user member of group?
975
+ hit = gnc in (un_gns.get(un) or [])
976
+ else:
977
+ # is it this specific group?
978
+ hit = gn == gnc
979
+
980
+ if rule.startswith("%+") != hit:
981
+ rejected = True
982
+ if rejected:
983
+ continue
968
984
 
969
985
  # if ap/vp has a user/group placeholder, make sure to keep
970
986
  # track so the same user/group is mapped when setting perms;
@@ -979,6 +995,8 @@ class AuthSrv(object):
979
995
 
980
996
  src = src1.replace("${g}", gn or "\n")
981
997
  dst = dst1.replace("${g}", gn or "\n")
998
+ src = PTN_G_GRP.sub(gn or "\n", src)
999
+ dst = PTN_G_GRP.sub(gn or "\n", dst)
982
1000
  if src == src1 and dst == dst1:
983
1001
  gn = ""
984
1002
 
@@ -1475,8 +1493,10 @@ class AuthSrv(object):
1475
1493
  daxs = {}
1476
1494
  mflags = {} # vpath:flags
1477
1495
  mount = {} # dst:src (vp:(ap,vp0))
1496
+ cfg_files_loaded = []
1478
1497
 
1479
1498
  self.idp_vols = {} # yolo
1499
+ self.badcfg1 = False
1480
1500
 
1481
1501
  if self.args.a:
1482
1502
  # list of username:password
@@ -1537,6 +1557,7 @@ class AuthSrv(object):
1537
1557
  zst = [(max(0, len(x) - 2) * " ") + "└" + x[-1] for x in zstt]
1538
1558
  t = "loaded {} config files:\n{}"
1539
1559
  self.log(t.format(len(zst), "\n".join(zst)))
1560
+ cfg_files_loaded = zst
1540
1561
 
1541
1562
  except:
1542
1563
  lns = lns[: self.line_ctr]
@@ -1561,9 +1582,14 @@ class AuthSrv(object):
1561
1582
  if not mount and not self.args.idp_h_usr:
1562
1583
  # -h says our defaults are CWD at root and read/write for everyone
1563
1584
  axs = AXS(["*"], ["*"], None, None)
1564
- if os.path.exists("/z/initcfg"):
1565
- t = "Read-access has been disabled due to failsafe: Docker detected, but the config does not define any volumes. 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 all of /w/ by adding the following arguments to the docker container: -v .::rw"
1566
- self.log(t, 1)
1585
+ if self.is_lxc:
1586
+ t = "Read-access has been disabled due to failsafe: Docker detected, but %s. 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 all of /w/ by adding the following arguments to the docker container: -v .::rw"
1587
+ if len(cfg_files_loaded) == 1:
1588
+ self.log(t % ("no config-file was provided",), 1)
1589
+ t = "it is strongly recommended to add a config-file instead, for example based on https://github.com/9001/copyparty/blob/hovudstraum/docs/examples/docker/basic-docker-compose/copyparty.conf"
1590
+ self.log(t, 3)
1591
+ else:
1592
+ self.log(t % ("the config does not define any volumes",), 1)
1567
1593
  axs = AXS()
1568
1594
  elif self.args.c:
1569
1595
  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"
@@ -1571,7 +1597,7 @@ class AuthSrv(object):
1571
1597
  axs = AXS()
1572
1598
  vfs = VFS(self.log_func, absreal("."), "", "", axs, {})
1573
1599
  if not axs.uread:
1574
- vfs.badcfg1 = True
1600
+ self.badcfg1 = True
1575
1601
  elif "" not in mount:
1576
1602
  # there's volumes but no root; make root inaccessible
1577
1603
  zsd = {"d2d": True, "tcolor": self.args.tcolor}
@@ -1845,7 +1871,7 @@ class AuthSrv(object):
1845
1871
  is_shr = shr and zv.vpath.split("/")[0] == shr
1846
1872
  if histp and not is_shr and histp in rhisttab:
1847
1873
  zv2 = rhisttab[histp]
1848
- t = "invalid config; multiple volumes share the same histpath (database+thumbnails location):\n histpath: %s\n volume 1: /%s [%s]\n volume 2: %s [%s]"
1874
+ t = "invalid config; multiple volumes share the same histpath (database+thumbnails location):\n histpath: %s\n volume 1: /%s [%s]\n volume 2: /%s [%s]"
1849
1875
  t = t % (histp, zv2.vpath, zv2.realpath, zv.vpath, zv.realpath)
1850
1876
  self.log(t, 1)
1851
1877
  raise Exception(t)
@@ -1859,7 +1885,7 @@ class AuthSrv(object):
1859
1885
  is_shr = shr and zv.vpath.split("/")[0] == shr
1860
1886
  if dbp and not is_shr and dbp in rdbpaths:
1861
1887
  zv2 = rdbpaths[dbp]
1862
- t = "invalid config; multiple volumes share the same dbpath (database location):\n dbpath: %s\n volume 1: /%s [%s]\n volume 2: %s [%s]"
1888
+ t = "invalid config; multiple volumes share the same dbpath (database location):\n dbpath: %s\n volume 1: /%s [%s]\n volume 2: /%s [%s]"
1863
1889
  t = t % (dbp, zv2.vpath, zv2.realpath, zv.vpath, zv.realpath)
1864
1890
  self.log(t, 1)
1865
1891
  raise Exception(t)
@@ -2042,12 +2068,13 @@ class AuthSrv(object):
2042
2068
  if vf not in vol.flags:
2043
2069
  vol.flags[vf] = getattr(self.args, ga)
2044
2070
 
2045
- zs = "forget_ip nrand u2abort u2ow ups_who zip_who"
2071
+ zs = "forget_ip nrand tail_who u2abort u2ow ups_who zip_who"
2046
2072
  for k in zs.split():
2047
2073
  if k in vol.flags:
2048
2074
  vol.flags[k] = int(vol.flags[k])
2049
2075
 
2050
- for k in ("convt",):
2076
+ zs = "convt tail_fd tail_rate tail_tmax"
2077
+ for k in zs.split():
2051
2078
  if k in vol.flags:
2052
2079
  vol.flags[k] = float(vol.flags[k])
2053
2080
 
@@ -2376,7 +2403,7 @@ class AuthSrv(object):
2376
2403
  idp_vn, _ = vfs.get(idp_vp, "*", False, False)
2377
2404
  idp_vp0 = idp_vn.vpath0
2378
2405
 
2379
- sigils = set(re.findall(r"(\${[ug][}%])", idp_vp0))
2406
+ sigils = set(PTN_SIGIL.findall(idp_vp0))
2380
2407
  if len(sigils) > 1:
2381
2408
  t = '\nWARNING: IdP-volume "/%s" created by "/%s" has multiple IdP placeholders: %s'
2382
2409
  self.idp_warn.append(t % (idp_vp, idp_vp0, list(sigils)))
@@ -2426,6 +2453,7 @@ class AuthSrv(object):
2426
2453
  self.defpw = defpw
2427
2454
  self.grps = grps
2428
2455
  self.iacct = {v: k for k, v in acct.items()}
2456
+ self.cfg_files_loaded = cfg_files_loaded
2429
2457
 
2430
2458
  self.load_sessions()
2431
2459
 
@@ -2545,6 +2573,7 @@ class AuthSrv(object):
2545
2573
  "txt_ext": self.args.textfiles.replace(",", " "),
2546
2574
  "def_hcols": list(vf.get("mth") or []),
2547
2575
  "unlist0": vf.get("unlist") or "",
2576
+ "see_dots": self.args.see_dots,
2548
2577
  "dgrid": "grid" in vf,
2549
2578
  "dgsel": "gsel" in vf,
2550
2579
  "dnsort": "nsort" in vf,
@@ -2556,6 +2585,7 @@ class AuthSrv(object):
2556
2585
  "idxh": int(self.args.ih),
2557
2586
  "themes": self.args.themes,
2558
2587
  "turbolvl": self.args.turbo,
2588
+ "nosubtle": self.args.nosubtle,
2559
2589
  "u2j": self.args.u2j,
2560
2590
  "u2sz": self.args.u2sz,
2561
2591
  "u2ts": vf["u2ts"],
copyparty/cfg.py CHANGED
@@ -22,6 +22,7 @@ def vf_bmap() :
22
22
  "no_forget": "noforget",
23
23
  "no_pipe": "nopipe",
24
24
  "no_robots": "norobots",
25
+ "no_tail": "notail",
25
26
  "no_thumb": "dthumb",
26
27
  "no_vthumb": "dvthumb",
27
28
  "no_athumb": "dathumb",
@@ -51,6 +52,7 @@ def vf_bmap() :
51
52
  "og_no_head",
52
53
  "og_s_title",
53
54
  "rand",
55
+ "rmagic",
54
56
  "rss",
55
57
  "wo_up_readme",
56
58
  "xdev",
@@ -101,6 +103,10 @@ def vf_vmap() :
101
103
  "mv_retry",
102
104
  "rm_retry",
103
105
  "sort",
106
+ "tail_fd",
107
+ "tail_rate",
108
+ "tail_tmax",
109
+ "tail_who",
104
110
  "tcolor",
105
111
  "unlist",
106
112
  "u2abort",
@@ -304,6 +310,13 @@ flagcats = {
304
310
  "exp_md": "placeholders to expand in markdown files; see --help",
305
311
  "exp_lg": "placeholders to expand in prologue/epilogue; see --help",
306
312
  },
313
+ "tailing": {
314
+ "notail": "disable ?tail (download a growing file continuously)",
315
+ "tail_fd=1": "check if file was replaced (new fd) every 1 sec",
316
+ "tail_rate=0.2": "check for new data every 0.2 sec",
317
+ "tail_tmax=30": "kill connection after 30 sec",
318
+ "tail_who=2": "restrict ?tail access (1=admins,2=authed,3=everyone)",
319
+ },
307
320
  "others": {
308
321
  "dots": "allow all users with read-access to\nenable the option to show dotfiles in listings",
309
322
  "fk=8": 'generates per-file accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes',
@@ -312,6 +325,7 @@ flagcats = {
312
325
  "dks": "per-directory accesskeys allow browsing into subdirs",
313
326
  "dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so',
314
327
  "rss": "allow '?rss' URL suffix (experimental)",
328
+ "rmagic": "expensive analysis for mimetype accuracy",
315
329
  "ups_who=2": "restrict viewing the list of recent uploads",
316
330
  "zip_who=2": "restrict access to download-as-zip/tar",
317
331
  "zipmaxn=9k": "reject download-as-zip if more than 9000 files",
copyparty/httpcli.py CHANGED
@@ -184,11 +184,11 @@ class HttpCli(object):
184
184
  self.log_src = conn.log_src # mypy404
185
185
  self.gen_fk = self._gen_fk if self.args.log_fk else gen_filekey
186
186
  self.tls = hasattr(self.s, "cipher")
187
+ self.is_vproxied = bool(self.args.R)
187
188
 
188
189
  # placeholders; assigned by run()
189
190
  self.keepalive = False
190
191
  self.is_https = False
191
- self.is_vproxied = False
192
192
  self.in_hdr_recv = True
193
193
  self.headers = {}
194
194
  self.mode = " " # http verb
@@ -396,7 +396,6 @@ class HttpCli(object):
396
396
  self.bad_xff = True
397
397
  else:
398
398
  self.ip = cli_ip
399
- self.is_vproxied = bool(self.args.R)
400
399
  self.log_src = self.conn.set_rproxy(self.ip)
401
400
  self.host = self.headers.get("x-forwarded-host") or self.host
402
401
  trusted_xff = True
@@ -529,6 +528,7 @@ class HttpCli(object):
529
528
  else:
530
529
  t = "incorrect --rp-loc or webserver config; expected vpath starting with %r but got %r"
531
530
  self.log(t % (self.args.R, vpath), 1)
531
+ self.is_vproxied = False
532
532
 
533
533
  self.ouparam = uparam.copy()
534
534
 
@@ -1228,10 +1228,19 @@ class HttpCli(object):
1228
1228
  else:
1229
1229
  return self.tx_404(True)
1230
1230
  else:
1231
- vfs = self.asrv.vfs
1232
- if vfs.badcfg1:
1233
- t = "<h2>access denied due to failsafe; check server log</h2>"
1234
- html = self.j2s("splash", this=self, msg=t)
1231
+ if (
1232
+ self.asrv.badcfg1
1233
+ and "h" not in self.ouparam
1234
+ and "hc" not in self.ouparam
1235
+ ):
1236
+ zs1 = "copyparty refused to start due to a failsafe: invalid server config; check server log"
1237
+ zs2 = 'you may <a href="/?h">access the controlpanel</a> but nothing will work until you shutdown the copyparty container and %s config-file (or provide the configuration as command-line arguments)'
1238
+ if self.asrv.is_lxc and len(self.asrv.cfg_files_loaded) == 1:
1239
+ zs2 = zs2 % ("add a",)
1240
+ else:
1241
+ zs2 = zs2 % ("fix the",)
1242
+
1243
+ html = self.j2s("msg", h1=zs1, h2=zs2)
1235
1244
  self.reply(html.encode("utf-8", "replace"), 500)
1236
1245
  return True
1237
1246
 
@@ -1398,7 +1407,13 @@ class HttpCli(object):
1398
1407
  except:
1399
1408
  pass
1400
1409
 
1410
+ ap = ""
1411
+ use_magic = "rmagic" in self.vn.flags
1412
+
1401
1413
  for i in hits:
1414
+ if use_magic:
1415
+ ap = os.path.join(self.vn.realpath, i["rp"])
1416
+
1402
1417
  iurl = html_escape("%s%s" % (baseurl, i["rp"]), True, True)
1403
1418
  title = unquotep(i["rp"].split("?")[0].split("/")[-1])
1404
1419
  title = html_escape(title, True, True)
@@ -1406,7 +1421,7 @@ class HttpCli(object):
1406
1421
  tag_a = str(i["tags"].get("artist") or "")
1407
1422
  desc = "%s - %s" % (tag_a, tag_t) if tag_t and tag_a else (tag_t or tag_a)
1408
1423
  desc = html_escape(desc, True, True) if desc else title
1409
- mime = html_escape(guess_mime(title))
1424
+ mime = html_escape(guess_mime(title, ap))
1410
1425
  lmod = formatdate(max(0, i["ts"]))
1411
1426
  zsa = (iurl, iurl, title, desc, lmod, iurl, mime, i["sz"])
1412
1427
  zs = (
@@ -1559,6 +1574,9 @@ class HttpCli(object):
1559
1574
  None, 207, "text/xml; charset=" + enc, {"Transfer-Encoding": "chunked"}
1560
1575
  )
1561
1576
 
1577
+ ap = ""
1578
+ use_magic = "rmagic" in vn.flags
1579
+
1562
1580
  ret = '<?xml version="1.0" encoding="{}"?>\n<D:multistatus xmlns:D="DAV:">'
1563
1581
  ret = ret.format(uenc)
1564
1582
  for x in fgen:
@@ -1585,7 +1603,9 @@ class HttpCli(object):
1585
1603
  "supportedlock": '<D:lockentry xmlns:D="DAV:"><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockentry>',
1586
1604
  }
1587
1605
  if not isdir:
1588
- pvs["getcontenttype"] = html_escape(guess_mime(rp))
1606
+ if use_magic:
1607
+ ap = os.path.join(tap, x["vp"])
1608
+ pvs["getcontenttype"] = html_escape(guess_mime(rp, ap))
1589
1609
  pvs["getcontentlength"] = str(st.st_size)
1590
1610
 
1591
1611
  for k, v in pvs.items():
@@ -2576,10 +2596,6 @@ class HttpCli(object):
2576
2596
  x = self.conn.hsrv.broker.ask("up2k.handle_json", body, self.u2fh.aps)
2577
2597
  ret = x.get()
2578
2598
 
2579
- if self.is_vproxied:
2580
- if "purl" in ret:
2581
- ret["purl"] = self.args.SR + ret["purl"]
2582
-
2583
2599
  if self.args.shr and self.vpath.startswith(self.args.shr1):
2584
2600
  # strip common suffix (uploader's folder structure)
2585
2601
  vp_req, vp_vfs = vroots(self.vpath, vjoin(dbv.vpath, vrem))
@@ -2589,6 +2605,10 @@ class HttpCli(object):
2589
2605
  raise Pebkac(500, t % zt)
2590
2606
  ret["purl"] = vp_req + ret["purl"][len(vp_vfs) :]
2591
2607
 
2608
+ if self.is_vproxied:
2609
+ if "purl" in ret:
2610
+ ret["purl"] = self.args.SR + ret["purl"]
2611
+
2592
2612
  ret = json.dumps(ret)
2593
2613
  self.log(ret)
2594
2614
  self.reply(ret.encode("utf-8"), mime="application/json")
@@ -2696,6 +2716,7 @@ class HttpCli(object):
2696
2716
  locked = chashes # remaining chunks to be received in this request
2697
2717
  written = [] # chunks written to disk, but not yet released by up2k
2698
2718
  num_left = -1 # num chunks left according to most recent up2k release
2719
+ bail1 = False # used in sad path to avoid contradicting error-text
2699
2720
  treport = time.time() # ratelimit up2k reporting to reduce overhead
2700
2721
 
2701
2722
  if "x-up2k-subc" in self.headers:
@@ -2834,7 +2855,6 @@ class HttpCli(object):
2834
2855
  except:
2835
2856
  # maybe busted handle (eg. disk went full)
2836
2857
  f.close()
2837
- chashes = [] # exception flag
2838
2858
  raise
2839
2859
  finally:
2840
2860
  if locked:
@@ -2843,13 +2863,14 @@ class HttpCli(object):
2843
2863
  num_left, t = x.get()
2844
2864
  if num_left < 0:
2845
2865
  self.loud_reply(t, status=500)
2846
- if chashes: # kills exception bubbling otherwise
2847
- return False
2866
+ bail1 = True
2848
2867
  else:
2849
2868
  t = "got %d more chunks, %d left"
2850
2869
  self.log(t % (len(written), num_left), 6)
2851
2870
 
2852
2871
  if num_left < 0:
2872
+ if bail1:
2873
+ return False
2853
2874
  raise Pebkac(500, "unconfirmed; see serverlog")
2854
2875
 
2855
2876
  if not num_left and fpool:
@@ -3786,6 +3807,20 @@ class HttpCli(object):
3786
3807
 
3787
3808
  return txt
3788
3809
 
3810
+ def _can_tail(self, volflags ) :
3811
+ zp = self.args.ua_nodoc
3812
+ if zp and zp.search(self.ua):
3813
+ t = "this URL contains no valuable information for bots/crawlers"
3814
+ raise Pebkac(403, t)
3815
+ lvl = volflags["tail_who"]
3816
+ if "notail" in volflags or not lvl:
3817
+ raise Pebkac(400, "tail is disabled in server config")
3818
+ elif lvl <= 1 and not self.can_admin:
3819
+ raise Pebkac(400, "tail is admin-only on this server")
3820
+ elif lvl <= 2 and self.uname in ("", "*"):
3821
+ raise Pebkac(400, "you must be authenticated to use ?tail on this server")
3822
+ return True
3823
+
3789
3824
  def _can_zip(self, volflags ) :
3790
3825
  lvl = volflags["zip_who"]
3791
3826
  if self.args.no_zip or not lvl:
@@ -3930,6 +3965,8 @@ class HttpCli(object):
3930
3965
  logmsg = "{:4} {} ".format("", self.req)
3931
3966
  logtail = ""
3932
3967
 
3968
+ is_tail = "tail" in self.uparam and self._can_tail(self.vn.flags)
3969
+
3933
3970
  if ptop is not None:
3934
3971
  ap_data = "<%s>" % (req_path,)
3935
3972
  try:
@@ -4042,6 +4079,7 @@ class HttpCli(object):
4042
4079
  and can_range
4043
4080
  and file_sz
4044
4081
  and "," not in hrange
4082
+ and not is_tail
4045
4083
  ):
4046
4084
  try:
4047
4085
  if not hrange.lower().startswith("bytes"):
@@ -4110,6 +4148,8 @@ class HttpCli(object):
4110
4148
  mime = "text/plain; charset={}".format(self.uparam["txt"] or "utf-8")
4111
4149
  elif "mime" in self.uparam:
4112
4150
  mime = str(self.uparam.get("mime"))
4151
+ elif "rmagic" in self.vn.flags:
4152
+ mime = guess_mime(req_path, fs_path)
4113
4153
  else:
4114
4154
  mime = guess_mime(req_path)
4115
4155
 
@@ -4127,13 +4167,18 @@ class HttpCli(object):
4127
4167
  return True
4128
4168
 
4129
4169
  dls = self.conn.hsrv.dls
4170
+ if is_tail:
4171
+ upper = 1 << 30
4172
+ if len(dls) > self.args.tail_cmax:
4173
+ raise Pebkac(400, "too many active downloads to start a new tail")
4174
+
4130
4175
  if upper - lower > 0x400000: # 4m
4131
4176
  now = time.time()
4132
4177
  self.dl_id = "%s:%s" % (self.ip, self.addr[1])
4133
4178
  dls[self.dl_id] = (now, 0)
4134
4179
  self.conn.hsrv.dli[self.dl_id] = (
4135
4180
  now,
4136
- upper - lower,
4181
+ 0 if is_tail else upper - lower,
4137
4182
  self.vn,
4138
4183
  self.vpath,
4139
4184
  self.uname,
@@ -4143,6 +4188,9 @@ class HttpCli(object):
4143
4188
  return self.tx_pipe(
4144
4189
  ptop, req_path, ap_data, job, lower, upper, status, mime, logmsg
4145
4190
  )
4191
+ elif is_tail:
4192
+ self.tx_tail(open_args, status, mime)
4193
+ return False
4146
4194
 
4147
4195
  ret = True
4148
4196
  with open_func(*open_args) as f:
@@ -4172,6 +4220,131 @@ class HttpCli(object):
4172
4220
 
4173
4221
  return ret
4174
4222
 
4223
+ def tx_tail(
4224
+ self,
4225
+ open_args ,
4226
+ status ,
4227
+ mime ,
4228
+ ) :
4229
+ vf = self.vn.flags
4230
+ self.send_headers(length=None, status=status, mime=mime)
4231
+ abspath = open_args[0]
4232
+ sec_rate = vf["tail_rate"]
4233
+ sec_max = vf["tail_tmax"]
4234
+ sec_fd = vf["tail_fd"]
4235
+ sec_ka = self.args.tail_ka
4236
+ wr_slp = self.args.s_wr_slp
4237
+ wr_sz = self.args.s_wr_sz
4238
+ dls = self.conn.hsrv.dls
4239
+ dl_id = self.dl_id
4240
+
4241
+ # non-numeric = full file from start
4242
+ # positive = absolute offset from start
4243
+ # negative = start that many bytes from eof
4244
+ try:
4245
+ ofs = int(self.uparam["tail"])
4246
+ except:
4247
+ ofs = 0
4248
+
4249
+ t0 = time.time()
4250
+ ofs0 = ofs
4251
+ f = None
4252
+ try:
4253
+ st = os.stat(abspath)
4254
+ f = open(*open_args)
4255
+ f.seek(0, os.SEEK_END)
4256
+ eof = f.tell()
4257
+ f.seek(0)
4258
+ if ofs < 0:
4259
+ ofs = max(0, ofs + eof)
4260
+
4261
+ self.log("tailing from byte %d: %r" % (ofs, abspath), 6)
4262
+
4263
+ # send initial data asap
4264
+ remains = sendfile_py(
4265
+ self.log, # d/c
4266
+ ofs,
4267
+ eof,
4268
+ f,
4269
+ self.s,
4270
+ wr_sz,
4271
+ wr_slp,
4272
+ False, # d/c
4273
+ dls,
4274
+ dl_id,
4275
+ )
4276
+ sent = (eof - ofs) - remains
4277
+ ofs = eof - remains
4278
+ f.seek(ofs)
4279
+
4280
+ try:
4281
+ st2 = os.stat(open_args[0])
4282
+ if st.st_ino == st2.st_ino:
4283
+ st = st2 # for filesize
4284
+ except:
4285
+ pass
4286
+
4287
+ gone = 0
4288
+ t_fd = t_ka = time.time()
4289
+ while True:
4290
+ buf = f.read(4096)
4291
+ now = time.time()
4292
+
4293
+ if sec_max and now - t0 >= sec_max:
4294
+ self.log("max duration exceeded; kicking client", 6)
4295
+ zb = b"\n\n*** max duration exceeded; disconnecting ***\n"
4296
+ self.s.sendall(zb)
4297
+ break
4298
+
4299
+ if buf:
4300
+ t_fd = t_ka = now
4301
+ self.s.sendall(buf)
4302
+ sent += len(buf)
4303
+ dls[dl_id] = (time.time(), sent)
4304
+ continue
4305
+
4306
+ time.sleep(sec_rate)
4307
+ if t_ka < now - sec_ka:
4308
+ t_ka = now
4309
+ self.s.send(b"\x00")
4310
+ if t_fd < now - sec_fd:
4311
+ try:
4312
+ st2 = os.stat(open_args[0])
4313
+ if (
4314
+ st2.st_ino != st.st_ino
4315
+ or st2.st_size < sent
4316
+ or st2.st_size < st.st_size
4317
+ ):
4318
+ # open new file before closing previous to avoid toctous (open may fail; cannot null f before)
4319
+ f2 = open(*open_args)
4320
+ f.close()
4321
+ f = f2
4322
+ f.seek(0, os.SEEK_END)
4323
+ eof = f.tell()
4324
+ if eof < sent:
4325
+ ofs = sent = 0 # shrunk; send from start
4326
+ zb = b"\n\n*** file size decreased -- rewinding to the start of the file ***\n\n"
4327
+ self.s.sendall(zb)
4328
+ if ofs0 < 0 and eof > -ofs0:
4329
+ ofs = eof + ofs0
4330
+ else:
4331
+ ofs = sent # just new fd? resume from same ofs
4332
+ f.seek(ofs)
4333
+ self.log("reopened at byte %d: %r" % (ofs, abspath), 6)
4334
+ gone = 0
4335
+ st = st2
4336
+ except:
4337
+ gone += 1
4338
+ if gone > 3:
4339
+ self.log("file deleted; disconnecting")
4340
+ break
4341
+ except IOError as ex:
4342
+ if ex.errno not in (errno.EPIPE, errno.ESHUTDOWN, errno.EBADFD):
4343
+ raise
4344
+ finally:
4345
+ if f:
4346
+ f.close()
4347
+
4175
4348
  def tx_pipe(
4176
4349
  self,
4177
4350
  ptop ,
@@ -4731,7 +4904,6 @@ class HttpCli(object):
4731
4904
  if zi == 2 or (zi == 1 and self.avol):
4732
4905
  dl_list = self.get_dls()
4733
4906
  for t0, t1, sent, sz, vp, dl_id, uname in dl_list:
4734
- rem = sz - sent
4735
4907
  td = max(0.1, now - t0)
4736
4908
  rd, fn = vsplit(vp)
4737
4909
  if not rd:
copyparty/util.py CHANGED
@@ -153,9 +153,15 @@ try:
153
153
  except:
154
154
  HAVE_PSUTIL = False
155
155
 
156
- if TYPE_CHECKING:
156
+ try:
157
+ if os.environ.get("PRTY_NO_MAGIC"):
158
+ raise Exception()
159
+
157
160
  import magic
161
+ except:
162
+ pass
158
163
 
164
+ if TYPE_CHECKING:
159
165
  from .authsrv import VFS
160
166
  from .broker_util import BrokerCli
161
167
  from .up2k import Up2k
@@ -1174,8 +1180,6 @@ class Magician(object):
1174
1180
  self.magic = None
1175
1181
 
1176
1182
  def ext(self, fpath ) :
1177
- import magic
1178
-
1179
1183
  try:
1180
1184
  if self.bad_magic:
1181
1185
  raise Exception()
@@ -3065,11 +3069,13 @@ def unescape_cookie(orig ) :
3065
3069
  return "".join(ret)
3066
3070
 
3067
3071
 
3068
- def guess_mime(url , fallback = "application/octet-stream") :
3072
+ def guess_mime(
3073
+ url , path = "", fallback = "application/octet-stream"
3074
+ ) :
3069
3075
  try:
3070
3076
  ext = url.rsplit(".", 1)[1].lower()
3071
3077
  except:
3072
- return fallback
3078
+ ext = ""
3073
3079
 
3074
3080
  ret = MIMES.get(ext)
3075
3081
 
@@ -3077,6 +3083,16 @@ def guess_mime(url , fallback = "application/octet-stream") :
3077
3083
  x = mimetypes.guess_type(url)
3078
3084
  ret = "application/{}".format(x[1]) if x[1] else x[0]
3079
3085
 
3086
+ if not ret and path:
3087
+ try:
3088
+ with open(fsenc(path), "rb", 0) as f:
3089
+ ret = magic.from_buffer(f.read(4096), mime=True)
3090
+ if ret.startswith("text/htm"):
3091
+ # avoid serving up HTML content unless there was actually a .html extension
3092
+ ret = "text/plain"
3093
+ except Exception as ex:
3094
+ pass
3095
+
3080
3096
  if not ret:
3081
3097
  ret = fallback
3082
3098
 
Binary file
Binary file
Binary file
Binary file
copyparty/web/ui.css.gz CHANGED
Binary file
copyparty/web/up2k.js.gz CHANGED
Binary file
copyparty/web/util.js.gz CHANGED
Binary file
Binary file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: copyparty
3
- Version: 1.17.1
3
+ Version: 1.18.0
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
@@ -66,12 +66,14 @@ turn almost any device into a file server with resumable uploads/downloads using
66
66
  * 🔌 protocols: [http](#the-browser) // [webdav](#webdav-server) // [ftp](#ftp-server) // [tftp](#tftp-server) // [smb/cifs](#smb-server)
67
67
  * 📱 [android app](#android-app) // [iPhone shortcuts](#ios-shortcuts)
68
68
 
69
- 👉 **[Get started](#quickstart)!** or visit the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running from a basement in finland
69
+ 👉 **[Get started](#quickstart)!** or visit the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running on a nuc in my basement
70
70
 
71
71
  📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer)
72
72
 
73
73
  🎬 **videos:** [upload](https://a.ocv.me/pub/demo/pics-vids/up2k.webm) // [cli-upload](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm) // [race-the-beam](https://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm)
74
74
 
75
+ made in Norway 🇳🇴
76
+
75
77
 
76
78
  ## readme toc
77
79
 
@@ -112,6 +114,7 @@ turn almost any device into a file server with resumable uploads/downloads using
112
114
  * [creating a playlist](#creating-a-playlist) - with a standalone mediaplayer or copyparty
113
115
  * [audio equalizer](#audio-equalizer) - and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression)
114
116
  * [fix unreliable playback on android](#fix-unreliable-playback-on-android) - due to phone / app settings
117
+ * [textfile viewer](#textfile-viewer) - with realtime streaming of logfiles and such ([demo](https://a.ocv.me/pub/demo/logtail/))
115
118
  * [markdown viewer](#markdown-viewer) - and there are *two* editors
116
119
  * [markdown vars](#markdown-vars) - dynamic docs with serverside variable expansion
117
120
  * [other tricks](#other-tricks)
@@ -313,7 +316,8 @@ also see [comparison to similar software](./docs/versus.md)
313
316
  * ☑ play video files as audio (converted on server)
314
317
  * ☑ create and play [m3u8 playlists](#playlists)
315
318
  * ☑ image gallery with webm player
316
- * ☑ textfile browser with syntax hilighting
319
+ * ☑ [textfile browser](#textfile-viewer) with syntax hilighting
320
+ * ☑ realtime streaming of growing files (logfiles and such)
317
321
  * ☑ [thumbnails](#thumbnails)
318
322
  * ☑ ...of images using Pillow, pyvips, or FFmpeg
319
323
  * ☑ ...of videos using FFmpeg
@@ -617,6 +621,8 @@ a client can request to see dotfiles in directory listings if global option `-ed
617
621
 
618
622
  dotfiles do not appear in search results unless one of the above is true, **and** the global option / volflag `dotsrch` is set
619
623
 
624
+ > even if user has permission to see dotfiles, they are default-hidden unless `--see-dots` is set, and/or user has enabled the `dotfiles` option in the settings tab
625
+
620
626
  config file example, where the same permission to see dotfiles is given in two different ways just for reference:
621
627
 
622
628
  ```yaml
@@ -753,7 +759,10 @@ enabling `multiselect` lets you click files to select them, and then shift-click
753
759
  * `multiselect` is mostly intended for phones/tablets, but the `sel` option in the `[⚙️] settings` tab is better suited for desktop use, allowing selection by CTRL-clicking and range-selection with SHIFT-click, all without affecting regular clicking
754
760
  * the `sel` option can be made default globally with `--gsel` or per-volume with volflag `gsel`
755
761
 
756
- to show `/icons/exe.png` as the thumbnail for all .exe files, `--ext-th=exe=/icons/exe.png` (optionally as a volflag)
762
+ to show `/icons/exe.png` and `/icons/elf.gif` as the thumbnail for all `.exe` and `.elf` files respectively, do this: `--ext-th=exe=/icons/exe.png --ext-th=elf=/icons/elf.gif`
763
+ * optionally as separate volflags for each mapping; see config file example below
764
+ * the supported image formats are [jpg, png, gif, webp, ico](https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types)
765
+ * be careful with svg; chrome will crash if you have too many unique svg files showing on the same page (the limit is 250 or so) -- showing the same handful of svg files thousands of times is ok however
757
766
 
758
767
  config file example:
759
768
 
@@ -770,6 +779,7 @@ config file example:
770
779
  dthumb # disable ALL thumbnails and audio transcoding
771
780
  dvthumb # only disable video thumbnails
772
781
  ext-th: exe=/ico/exe.png # /ico/exe.png is the thumbnail of *.exe
782
+ ext-th: elf=/ico/elf.gif # ...and /ico/elf.gif is used for *.elf
773
783
  th-covers: folder.png,folder.jpg,cover.png,cover.jpg # the default
774
784
  ```
775
785
 
@@ -1177,6 +1187,18 @@ not available on iPhones / iPads because AudioContext currently breaks backgroun
1177
1187
  due to phone / app settings, android phones may randomly stop playing music when the power saver kicks in, especially at the end of an album -- you can fix it by [disabling power saving](https://user-images.githubusercontent.com/241032/235262123-c328cca9-3930-4948-bd18-3949b9fd3fcf.png) in the [app settings](https://user-images.githubusercontent.com/241032/235262121-2ffc51ae-7821-4310-a322-c3b7a507890c.png) of the browser you use for music streaming (preferably a dedicated one)
1178
1188
 
1179
1189
 
1190
+ ## textfile viewer
1191
+
1192
+ with realtime streaming of logfiles and such ([demo](https://a.ocv.me/pub/demo/logtail/)) , and terminal colors work too
1193
+
1194
+ click `-txt-` next to a textfile to open the viewer, which has the following toolbar buttons:
1195
+
1196
+ * `✏️ edit` opens the textfile editor
1197
+ * `📡 follow` starts monitoring the file for changes, streaming new lines in realtime
1198
+ * similar to `tail -f`
1199
+ * [link directly](https://a.ocv.me/pub/demo/logtail/?doc=lipsum.txt&tail) to a file with tailing enabled by adding `&tail` to the textviewer URL
1200
+
1201
+
1180
1202
  ## markdown viewer
1181
1203
 
1182
1204
  and there are *two* editors
@@ -2475,6 +2497,9 @@ interact with copyparty using non-browser clients
2475
2497
  * and for screenshots on macos, see [./contrib/ishare.iscu](./contrib/#ishareiscu)
2476
2498
  * and for screenshots on linux, see [./contrib/flameshot.sh](./contrib/flameshot.sh)
2477
2499
 
2500
+ * [Custom Uploader](https://f-droid.org/en/packages/com.nyx.custom_uploader/) (an Android app) as an alternative to copyparty's own [PartyUP!](#android-app)
2501
+ * works if you set UploadURL to `https://your.com/foo/?want=url&pw=hunter2` and FormDataName `f`
2502
+
2478
2503
  * contextlet (web browser integration); see [contrib contextlet](contrib/#send-to-cppcontextletjson)
2479
2504
 
2480
2505
  * [igloo irc](https://iglooirc.com/): Method: `post` Host: `https://you.com/up/?want=url&pw=hunter2` Multipart: `yes` File parameter: `f`
@@ -2576,6 +2601,11 @@ below are some tweaks roughly ordered by usefulness:
2576
2601
 
2577
2602
  when uploading files,
2578
2603
 
2604
+ * when uploading from very fast storage (NVMe SSD) with chrome/firefox, enable `[wasm]` in the `[⚙️] settings` tab to more effectively use all CPU-cores for hashing
2605
+ * don't do this on Safari (runs faster without)
2606
+ * don't do this on older browsers; likely to provoke browser-bugs (browser eats all RAM and crashes)
2607
+ * can be made default-enabled serverside with `--nosubtle 137` (chrome v137+) or `--nosubtle 2` (chrome+firefox)
2608
+
2579
2609
  * chrome is recommended (unfortunately), at least compared to firefox:
2580
2610
  * up to 90% faster when hashing, especially on SSDs
2581
2611
  * up to 40% faster when uploading over extremely fast internets
@@ -2783,6 +2813,7 @@ set any of the following environment variables to disable its associated optiona
2783
2813
  | `PRTY_NO_CFSSL` | never attempt to generate self-signed certificates using [cfssl](https://github.com/cloudflare/cfssl) |
2784
2814
  | `PRTY_NO_FFMPEG` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips |
2785
2815
  | `PRTY_NO_FFPROBE` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips, **metadata-scanning** must be handled by mutagen |
2816
+ | `PRTY_NO_MAGIC` | do not use [magic](https://pypi.org/project/python-magic/) for filetype detection |
2786
2817
  | `PRTY_NO_MUTAGEN` | do not use [mutagen](https://pypi.org/project/mutagen/) for reading metadata from media files; will fallback to ffprobe |
2787
2818
  | `PRTY_NO_PIL` | disable all [Pillow](https://pypi.org/project/pillow/)-based thumbnail support; will fallback to libvips or ffmpeg |
2788
2819
  | `PRTY_NO_PILF` | disable Pillow `ImageFont` text rendering, used for folder thumbnails |
@@ -2883,5 +2914,7 @@ if there's a wall of base64 in the log (thread stacks) then please include that,
2883
2914
 
2884
2915
  for build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md)
2885
2916
 
2917
+ specifically you may want to [build the sfx](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#just-the-sfx) or [build from scratch](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#build-from-scratch)
2918
+
2886
2919
  see [./docs/TODO.md](./docs/TODO.md) for planned features / fixes / changes
2887
2920
 
@@ -1,17 +1,17 @@
1
1
  copyparty/__init__.py,sha256=TnFSStmHlwlRIClWW8jSHxZpt3dl_kN6_pEnqBqh3mE,2638
2
- copyparty/__main__.py,sha256=FZs3IMHTFyeR_-Ftx-M-61_gOwmQCM3bD5b9jo6nrbs,120810
3
- copyparty/__version__.py,sha256=OCUlop4ig08TTAg6PnNAKigP9eAnhhG-82RUqzvbE-w,253
4
- copyparty/authsrv.py,sha256=Z7VH7nO-Yd2X33d8qdMemb6bsAGZqgtaRPFgSAF7rl0,113463
2
+ copyparty/__main__.py,sha256=gJlKxpBmV39xfiWp8sx5iUZsoJPhFcCAWY5RVpWsrz0,122959
3
+ copyparty/__version__.py,sha256=QNl11l-yovzXttFgMOYiqG-Rd-5GJHs6EcPWg-ZqCSI,249
4
+ copyparty/authsrv.py,sha256=K3dIZxJvNhrcPbXJpVIkoxWaDrQf3CYz3X-FYaVlrYw,114905
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=pSSeVYticrDsnsrdRtfpUQN-8WRObsqrYtSRroXmgxo,7992
10
- copyparty/cfg.py,sha256=H2p2bHRWk2e4rOx5NZWUVHUcoETy3dtzDkeC8TdosRY,14522
10
+ copyparty/cfg.py,sha256=6o6aLzLxZ59lusAn_PMGgldc3rEb9yyGnUnGIP5D9M8,15077
11
11
  copyparty/dxml.py,sha256=vu5uZQtwvwoqnFHbULs2Zh_y2DETu0T-ENpMZ1i2CV4,2505
12
12
  copyparty/fsutil.py,sha256=NC_CJC4TDag399vVDH9_uQfdfpTMwRFLNxERSWhlVvs,4594
13
13
  copyparty/ftpd.py,sha256=G7PApVIFeSzRo4-D-9uRb8NxYgz6nFwTEbrOk1ErYCU,17969
14
- copyparty/httpcli.py,sha256=9yCKMqUD3J2m1tmvx9VIF_RkGi2LvbBHkUx4xxBzVL4,221960
14
+ copyparty/httpcli.py,sha256=r_M67eEl-qKLR6pf79TTcf_G1o0fJiOcU4fTTll7B34,228170
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=-7QjF_jIxnPo4Vr0oUPksQ_U_Ef0HRsSPm3s71idOz8,3879
@@ -32,7 +32,7 @@ copyparty/th_cli.py,sha256=IEX5tCb0gw9Z2aRIDL9bwdvJ6g5jhWZ8OEAAz16_xN4,5426
32
32
  copyparty/th_srv.py,sha256=2omGprnKGWim6U7fjJAXI41UCZBSxRwZfS0rCDsUDps,32556
33
33
  copyparty/u2idx.py,sha256=4Y5OOPyVkc-pS0z6e3p4StXAMnjHobSOMmMsvNUTD34,13674
34
34
  copyparty/up2k.py,sha256=nnR_ZKaopSNBuAjSN3Q5G_UVe6GmYD1NkxJt5QQf02o,178079
35
- copyparty/util.py,sha256=Keb-mlTq4rtWjv3utaGsKqwujHYPZLkaBZOeivnGxKc,103485
35
+ copyparty/util.py,sha256=S-dEme_1rKf3qYgxWWahEUQUzgq_kpoO_nPbuKCwLsY,103948
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
@@ -55,9 +55,9 @@ copyparty/stolen/ifaddr/_posix.py,sha256=-67NdfGrCktfQPakT2fLbjl2U00QMvyBGkSvrUu
55
55
  copyparty/stolen/ifaddr/_shared.py,sha256=uNC4SdEIgdSLKvuUzsf1aM-H1Xrc_9mpLoOT43YukGs,6206
56
56
  copyparty/stolen/ifaddr/_win32.py,sha256=EE-QyoBgeB7lYQ6z62VjXNaRozaYfCkaJBHGNA8QtZM,4026
57
57
  copyparty/web/baguettebox.js.gz,sha256=r2c_hOZV_RTyl4CqWWX14FDWP8nnDVwGkDl4Sfk0rU4,8239
58
- copyparty/web/browser.css.gz,sha256=-w-OUbKy0UdL9vYKAC95ayl4MesDAFjqrq60uAxPj4U,11728
58
+ copyparty/web/browser.css.gz,sha256=cjHv6i6VDjCG66W9h0S37l2-Gv7-qKBH-HpTP7kZSr4,11769
59
59
  copyparty/web/browser.html,sha256=auvhLVE_t0aIN0q-nk0zOWFqITgDhroMAAviBNLoFfc,4788
60
- copyparty/web/browser.js.gz,sha256=wdxJBHlMCo1Oh9v1YZsj6rucul7qhQts1-iVfmFPjr8,94639
60
+ copyparty/web/browser.js.gz,sha256=qgL276U3d4GAZmkdq7YaWTBxGulnDvkevXDoCi9_GJk,96443
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
@@ -79,13 +79,13 @@ copyparty/web/shares.html,sha256=YctvUrKuBYu42kxVylyW2_DEHm7Ik6uHqzfzVZ4N0ac,254
79
79
  copyparty/web/shares.js.gz,sha256=emeY2-wjkh8x1JgaW6ny5fcC7XpZzZzfE1f-sEyntQ4,940
80
80
  copyparty/web/splash.css.gz,sha256=S8_A7JJl71xACRBYGzafeaD82OacW6Fa7oKPiNyrhAs,1087
81
81
  copyparty/web/splash.html,sha256=ouFB1P9g0K-S3LZQtOLfNz3GLXIRjyETkxo9aJZhiYk,6249
82
- copyparty/web/splash.js.gz,sha256=4VqNznN10-bT33IJm3VWzBEJ1s08XZyxFB1TYPUkuAo,2739
82
+ copyparty/web/splash.js.gz,sha256=Qh0KoPWKoJ77cyzOwnhUaCTI5XUoPVV3YJURKklqpBg,2739
83
83
  copyparty/web/svcs.html,sha256=cxgrhX9wD0Z_kvidry3aS9ubuGXYDj2f4ehq1X8T1EA,14227
84
84
  copyparty/web/svcs.js.gz,sha256=lMXEP9W-VlXyANlva4q0ASSxvvHYlE2CrmxGgZXZop0,713
85
- copyparty/web/ui.css.gz,sha256=iDjrmq32aDN6l2S5AjCQdKjD6bxmzP6ji2WjM1FjKiU,2819
86
- copyparty/web/up2k.js.gz,sha256=7AKmoJOtFh9tx3Ha7w2F-z69-XZo_LzyR3ilWnBO_D8,24524
87
- copyparty/web/util.js.gz,sha256=Ha2u-RG4HAYOL_QZnt426man0BdhucVNYOQjG5v69AA,15254
88
- copyparty/web/w.hash.js.gz,sha256=JhJagnqIkcKng_hs6otEgzcuQE7keToG_r5dd2o3EfU,1108
85
+ copyparty/web/ui.css.gz,sha256=e3iIflzddmjoyPrun_1jsu9j7fbdonNQLyhEE2oKKOQ,2819
86
+ copyparty/web/up2k.js.gz,sha256=vf9Kth2JQ4F-XmWhjcGXqy6gA4eOkg0-EfVjAGQsW5o,24794
87
+ copyparty/web/util.js.gz,sha256=vQj__zSM0cbBN4AoZgwhFrSCh82ZTv4CbzeufXo8tw4,15248
88
+ copyparty/web/w.hash.js.gz,sha256=cFH6Xo4YRgH9Wr7RmHMSEfpuTmmIvEmzmSvv4RLmyPU,1193
89
89
  copyparty/web/a/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
90
90
  copyparty/web/a/partyfuse.py,sha256=9p5Hpg_IBiSimv7j9kmPhCGpy-FLXSRUOYnLjJ5JifU,28049
91
91
  copyparty/web/a/u2c.py,sha256=auXzLj04dt_lw4H70PhNUK0GjrQEThrybo2-77SLsUg,53165
@@ -100,7 +100,7 @@ copyparty/web/deps/busy.mp3.gz,sha256=EVphk1_HYyRKJmtpeK99vbAstF7ub1f9ndu020H8PQ
100
100
  copyparty/web/deps/easymde.css.gz,sha256=vWxfueI64rPikuqFj69wJBtGisqf93AheQtOZqgUI_c,3041
101
101
  copyparty/web/deps/easymde.js.gz,sha256=rHBs4XWQe2bmv7ZzDIk43oxnTwrwpq5laYHhV5sKQQo,77014
102
102
  copyparty/web/deps/fuse.py,sha256=6j4Zy3VpQg629pwwIW77v2LJ1hy-qlyrxwhXfKl9B7I,33426
103
- copyparty/web/deps/marked.js.gz,sha256=6GrdpSikQ-tt9GrcAl7wA7mp_Il_2T2ZvBeACVX1j1k,22665
103
+ copyparty/web/deps/marked.js.gz,sha256=UdxHVVlpRf9k1UivWpLKVQzpzZxfK_O3pleTi_B5f8k,22704
104
104
  copyparty/web/deps/mini-fa.css.gz,sha256=CTPrNaH8OTVmxajrGP88E2MkjadY9_81TBVnd9sw9Y8,572
105
105
  copyparty/web/deps/mini-fa.woff,sha256=L9DNncV2TIyvsrspMbJouvnnt7F068Hbn7YZYvN76AU,2784
106
106
  copyparty/web/deps/prism.css.gz,sha256=Z_A6rJ3MN5KWnjvXaV787aTW_5DT-xjFd0YZ7_W-Krk,1468
@@ -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.17.1.dist-info/licenses/LICENSE,sha256=gOr4h33pCsBEg9uIy9AYmb7qlocL4V9t2uPJS5wllr0,1072
113
- copyparty-1.17.1.dist-info/METADATA,sha256=ApeqUFduyJZInDL2s46CSphR3JT7XsAmrTvoOlLCa20,163313
114
- copyparty-1.17.1.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
115
- copyparty-1.17.1.dist-info/entry_points.txt,sha256=4zw6a3rqASywQomiYLObjjlxybaI65LYYOTJwgKz7b0,128
116
- copyparty-1.17.1.dist-info/top_level.txt,sha256=LnYUPsDyk-8kFgM6YJLG4h820DQekn81cObKSu9g-sI,10
117
- copyparty-1.17.1.dist-info/RECORD,,
112
+ copyparty-1.18.0.dist-info/licenses/LICENSE,sha256=gOr4h33pCsBEg9uIy9AYmb7qlocL4V9t2uPJS5wllr0,1072
113
+ copyparty-1.18.0.dist-info/METADATA,sha256=AgVke4zkEUcHf9Iw-CNV1myTB72KmxPHCua7tI3DYwg,165863
114
+ copyparty-1.18.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
115
+ copyparty-1.18.0.dist-info/entry_points.txt,sha256=4zw6a3rqASywQomiYLObjjlxybaI65LYYOTJwgKz7b0,128
116
+ copyparty-1.18.0.dist-info/top_level.txt,sha256=LnYUPsDyk-8kFgM6YJLG4h820DQekn81cObKSu9g-sI,10
117
+ copyparty-1.18.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.7.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5