copyparty 1.19.5__py3-none-any.whl → 1.19.6__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
@@ -190,35 +190,41 @@ def init_E(EE ) :
190
190
  (unicode, "/tmp"),
191
191
  ]
192
192
  errs = []
193
- for chk in [os.listdir, os.mkdir]:
194
- for npath, (pf, pa) in enumerate(paths):
195
- p = ""
196
- try:
197
- p = pf(pa)
198
- # print(chk.__name__, p, pa)
199
- if not p or p.startswith("~"):
200
- continue
201
-
202
- p = os.path.normpath(p)
203
- chk(p) # type: ignore
204
- p = os.path.join(p, "copyparty")
205
- if not os.path.isdir(p):
206
- os.mkdir(p)
207
-
208
- if npath > 1:
209
- t = "Using [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/"
210
- errs.append(t % (p,))
211
- elif errs:
212
- errs.append("Using [%s] instead" % (p,))
213
-
214
- if errs:
215
- warn(". ".join(errs))
216
-
217
- return p # type: ignore
218
- except Exception as ex:
219
- if p and npath < 2:
220
- t = "Unable to store config in [%s] due to %r"
221
- errs.append(t % (p, ex))
193
+ for npath, (pf, pa) in enumerate(paths):
194
+ p = ""
195
+ try:
196
+ p = pf(pa)
197
+ if not p or p.startswith("~"):
198
+ continue
199
+
200
+ p = os.path.normpath(p)
201
+ if os.path.isdir(p) and os.listdir(p):
202
+ mkdir = False
203
+ else:
204
+ mkdir = True
205
+ os.mkdir(p)
206
+
207
+ p = os.path.join(p, "copyparty")
208
+ if not os.path.isdir(p):
209
+ os.mkdir(p)
210
+
211
+ if npath > 1:
212
+ t = "Using %s/copyparty [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/"
213
+ errs.append(t % (pa, p))
214
+ elif mkdir:
215
+ t = "Using %s/copyparty [%s] for config%s (Warning: %s did not exist and was created just now)"
216
+ errs.append(t % (pa, p, " instead" if npath else "", pa))
217
+ elif errs:
218
+ errs.append("Using %s/copyparty [%s] instead" % (pa, p))
219
+
220
+ if errs:
221
+ warn(". ".join(errs))
222
+
223
+ return p # type: ignore
224
+ except Exception as ex:
225
+ if p and npath < 2:
226
+ t = "Unable to store config in %s [%s] due to %r"
227
+ errs.append(t % (pa, p, ex))
222
228
 
223
229
  raise Exception("could not find a writable path for config")
224
230
 
@@ -662,6 +668,42 @@ def get_sects():
662
668
  """
663
669
  ),
664
670
  ],
671
+ [
672
+ "auth-ord",
673
+ "authentication precedence",
674
+ dedent(
675
+ """
676
+ \033[33m--auth-ord\033[0m is a comma-separated list of auth options
677
+ (one or more of the [\033[35moptions\033[0m] below); first one wins
678
+
679
+ [\033[35mpw\033[0m] is conventional login, for example the "\033[36mPW\033[0m" header,
680
+ or the \033[36m?pw=\033[0m[...] URL-suffix, or a valid session cookie
681
+ (see \033[33m--help-auth\033[0m)
682
+
683
+ [\033[35midp\033[0m] is a username provided in the http-request-header
684
+ defined by \033[33m--idp-h-usr\033[0m and/or \033[33m--idp-hm-usr\033[0m, which is
685
+ provided by an authentication middleware such as
686
+ authentik, authelia, tailscale, ... (see \033[33m--help-idp\033[0m)
687
+
688
+ [\033[35midp-h\033[0m] is specifically an \033[33m--idp-h-usr\033[0m header,
689
+ [\033[35midp-hm\033[0m] is specifically an \033[33m--idp-hm-usr\033[0m header;
690
+ [\033[35midp\033[0m] is the same as [\033[35midp-hm,idp-h\033[0m]
691
+
692
+ [\033[35mipu\033[0m] is a mapping from an IP-address to a username,
693
+ auto-authing that client-IP to that account
694
+ (see the description of \033[36m--ipu\033[0m in \033[33m--help\033[0m)
695
+
696
+ NOTE: even if an option (\033[35mpw\033[0m/\033[35mipu\033[0m/...) is not in the list,
697
+ it may still be enabled and can still take effect if
698
+ none of the other alternatives identify the user
699
+
700
+ NOTE: if [\033[35mipu\033[0m] is in the list, it must be FIRST or LAST
701
+
702
+ NOTE: if [\033[35mpw\033[0m] is not in the list, the logout-button
703
+ will be hidden when any idp feature is enabled
704
+ """
705
+ ),
706
+ ],
665
707
  [
666
708
  "flags",
667
709
  "list of volflags",
@@ -1107,6 +1149,8 @@ def add_qr(ap, tty):
1107
1149
  ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)")
1108
1150
  ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr")
1109
1151
  ap2.add_argument("--qr-wait", metavar="SEC", type=float, default=0, help="wait \033[33mSEC\033[0m before printing the qr-code to the log")
1152
+ ap2.add_argument("--qr-every", metavar="SEC", type=float, default=0, help="print the qr-code every \033[33mSEC\033[0m (try this with/without --qr-pin in case of issues)")
1153
+ ap2.add_argument("--qr-winch", metavar="SEC", type=float, default=0, help="when --qr-pin is enabled, check for terminal size change every \033[33mSEC\033[0m")
1110
1154
  ap2.add_argument("--qr-file", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m write qr-code to file.\n └─To create txt or svg, \033[33mTXT\033[0m is Filepath:Zoom:Pad, for example [\033[32mqr.txt:1:2\033[0m]\n └─To create png or gif, \033[33mTXT\033[0m is Filepath:Zoom:Pad:Foreground:Background, for example [\033[32mqr.png:8:2:333333:ffcc55\033[0m], or [\033[32mqr.png:8:2::ffcc55\033[0m] for transparent")
1111
1155
 
1112
1156
 
@@ -1232,7 +1276,7 @@ def add_auth(ap):
1232
1276
  ses_db = os.path.join(E.cfg, "sessions.db")
1233
1277
  ap2 = ap.add_argument_group("IdP / identity provider / user authentication options")
1234
1278
  ap2.add_argument("--idp-h-usr", metavar="HN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
1235
- ap2.add_argument("--idp-hm-usr", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp")
1279
+ ap2.add_argument("--idp-hm-usr", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mT\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp")
1236
1280
  ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control")
1237
1281
  ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present")
1238
1282
  ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m")
@@ -1240,6 +1284,7 @@ def add_auth(ap):
1240
1284
  ap2.add_argument("--idp-store", metavar="N", type=int, default=1, help="how to use \033[33m--idp-db\033[0m; [\033[32m0\033[0m] = entirely disable, [\033[32m1\033[0m] = write-only (effectively disabled), [\033[32m2\033[0m] = remember users, [\033[32m3\033[0m] = remember users and groups.\nNOTE: Will remember and restore the IdP-volumes of all users for all eternity if set to 2 or 3, even when user is deleted from your IdP")
1241
1285
  ap2.add_argument("--idp-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to use /?idp (the cache management UI)")
1242
1286
  ap2.add_argument("--idp-cookie", metavar="S", type=int, default=0, help="generate a session-token for IdP users which is written to cookie \033[33mcppws\033[0m (or \033[33mcppwd\033[0m if plaintext), to reduce the load on the IdP server, lifetime \033[33mS\033[0m seconds.\n └─note: The expiration time is a client hint only; the actual lifetime of the session-token is infinite (until next restart with \033[33m--ses-db\033[0m wiped)")
1287
+ ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see --help-auth-ord")
1243
1288
  ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app")
1244
1289
  ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins")
1245
1290
  ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)")
@@ -1250,6 +1295,10 @@ def add_auth(ap):
1250
1295
  ap2.add_argument("--ipr", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m username \033[33mUSR\033[0m can only connect from an IP matching one or more \033[33mCIDR\033[0m (comma-sep.); example: [\033[32m192.168.123.0/24,172.16.0.0/16=dave]")
1251
1296
  ap2.add_argument("--have-idp-hdrs", type=u, default="", help=argparse.SUPPRESS)
1252
1297
  ap2.add_argument("--have-ipu-or-ipr", type=u, default="", help=argparse.SUPPRESS)
1298
+ ap2.add_argument("--ao-idp-before-pw", type=u, default="", help=argparse.SUPPRESS)
1299
+ ap2.add_argument("--ao-h-before-hm", type=u, default="", help=argparse.SUPPRESS)
1300
+ ap2.add_argument("--ao-ipu-wins", type=u, default="", help=argparse.SUPPRESS)
1301
+ ap2.add_argument("--ao-has-pw", type=u, default="", help=argparse.SUPPRESS)
1253
1302
 
1254
1303
 
1255
1304
  def add_chpw(ap):
@@ -1489,6 +1538,7 @@ def add_logging(ap):
1489
1538
  ap2.add_argument("--log-utc", action="store_true", help="do not use local timezone; assume the TZ env-var is UTC (tiny bit faster)")
1490
1539
  ap2.add_argument("--log-tdec", metavar="N", type=int, default=3, help="timestamp resolution / number of timestamp decimals")
1491
1540
  ap2.add_argument("--log-badpwd", metavar="N", type=int, default=2, help="log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed")
1541
+ ap2.add_argument("--log-badxml", action="store_true", help="log any invalid XML received from a client")
1492
1542
  ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs")
1493
1543
  ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling")
1494
1544
  ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
@@ -1626,6 +1676,7 @@ def add_db_metadata(ap):
1626
1676
 
1627
1677
  def add_txt(ap):
1628
1678
  ap2 = ap.add_argument_group("textfile options")
1679
+ ap2.add_argument("--md-no-br", action="store_true", help="markdown: disable newline-is-newline; will only render a newline into the html given two trailing spaces or a double-newline (volflag=md_no_br)")
1629
1680
  ap2.add_argument("--md-hist", metavar="TXT", type=u, default="s", help="where to store old version of markdown files; [\033[32ms\033[0m]=subfolder, [\033[32mv\033[0m]=volume-histpath, [\033[32mn\033[0m]=nope/disabled (volflag=md_hist)")
1630
1681
  ap2.add_argument("--txt-eol", metavar="TYPE", type=u, default="", help="enable EOL conversion when writing documents; supported: CRLF, LF (volflag=txt_eol)")
1631
1682
  ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="the textfile editor will check for serverside changes every \033[33mSEC\033[0m seconds")
@@ -1959,7 +2010,7 @@ def main(argv = None) :
1959
2010
  if not HAVE_IPV6 and al.i == "::":
1960
2011
  al.i = "0.0.0.0"
1961
2012
 
1962
- al.i = al.i.split(",")
2013
+ al.i = [x.strip() for x in al.i.split(",")]
1963
2014
  try:
1964
2015
  if "-" in al.p:
1965
2016
  lo, hi = [int(x) for x in al.p.split("-")]
copyparty/__version__.py CHANGED
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 19, 5)
3
+ VERSION = (1, 19, 6)
4
4
  CODENAME = "usernames"
5
- BUILD_DT = (2025, 8, 21)
5
+ BUILD_DT = (2025, 8, 27)
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
@@ -1651,6 +1651,7 @@ class AuthSrv(object):
1651
1651
  # accept both , and : as separators between usernames
1652
1652
  zs1, zs2 = x.replace("=", ":").split(":", 1)
1653
1653
  grps[zs1] = zs2.replace(":", ",").split(",")
1654
+ grps[zs1] = [x.strip() for x in grps[zs1]]
1654
1655
  except:
1655
1656
  t = '\n invalid value "{}" for argument --grp, must be groupname:username1,username2,...'
1656
1657
  raise Exception(t.format(x))
@@ -1704,6 +1705,7 @@ class AuthSrv(object):
1704
1705
 
1705
1706
  self.args.have_idp_hdrs = bool(self.args.idp_h_usr or self.args.idp_hm_usr)
1706
1707
  self.args.have_ipu_or_ipr = bool(self.args.ipu or self.args.ipr)
1708
+ self.setup_auth_ord()
1707
1709
 
1708
1710
  self.setup_pwhash(acct)
1709
1711
  defpw = acct.copy()
@@ -2802,7 +2804,8 @@ class AuthSrv(object):
2802
2804
  "have_mv": not self.args.no_mv,
2803
2805
  "have_del": not self.args.no_del,
2804
2806
  "have_unpost": int(self.args.unpost),
2805
- "have_emp": self.args.emp,
2807
+ "have_emp": int(self.args.emp),
2808
+ "md_no_br": int(vf.get("md_no_br") or 0),
2806
2809
  "ext_th": vf.get("ext_th_d") or {},
2807
2810
  "sb_md": "" if "no_sb_md" in vf else (vf.get("md_sbf") or "y"),
2808
2811
  "sba_md": vf.get("md_sba") or "",
@@ -2852,6 +2855,18 @@ class AuthSrv(object):
2852
2855
  zs = str(vol.flags.get("tcolor") or self.args.tcolor)
2853
2856
  vol.flags["tcolor"] = zs.lstrip("#")
2854
2857
 
2858
+ def setup_auth_ord(self) :
2859
+ ao = [x.strip() for x in self.args.auth_ord.split(",")]
2860
+ if "idp" in ao:
2861
+ zi = ao.index("idp")
2862
+ ao = ao[:zi] + ["idp-hm", "idp-h"] + ao[zi:]
2863
+ zsl = "pw idp-h idp-hm ipu".split()
2864
+ pw, h, hm, ipu = [ao.index(x) if x in ao else 99 for x in zsl]
2865
+ self.args.ao_idp_before_pw = min(h, hm) < pw
2866
+ self.args.ao_h_before_hm = h < hm
2867
+ self.args.ao_ipu_wins = ipu == 0
2868
+ self.args.ao_have_pw = pw < 99
2869
+
2855
2870
  def load_idp_db(self, quiet=False) :
2856
2871
  # mutex me
2857
2872
  level = self.args.idp_store
copyparty/broker_util.py CHANGED
@@ -2,7 +2,6 @@
2
2
  from __future__ import print_function, unicode_literals
3
3
 
4
4
  import argparse
5
- import traceback
6
5
 
7
6
  from queue import Queue
8
7
 
copyparty/cert.py CHANGED
@@ -126,6 +126,7 @@ def _gen_srv(log , args, netdevs ):
126
126
  nlog = lambda msg, c=0: log("cert-gen-srv", msg, c)
127
127
 
128
128
  names = args.crt_ns.split(",") if args.crt_ns else []
129
+ names = [x.strip() for x in names]
129
130
  if not args.crt_exact:
130
131
  for n in names[:]:
131
132
  names.append("*.{}".format(n))
copyparty/cfg.py CHANGED
@@ -44,6 +44,7 @@ def vf_bmap() :
44
44
  "gsel",
45
45
  "hardlink",
46
46
  "magic",
47
+ "md_no_br",
47
48
  "no_db_ip",
48
49
  "no_sb_md",
49
50
  "no_sb_lg",
@@ -324,6 +325,7 @@ flagcats = {
324
325
  "og_ua": "if defined: only send OG html if useragent matches this regex",
325
326
  },
326
327
  "textfiles": {
328
+ "md_no_br": "newline only on double-newline or two tailing spaces",
327
329
  "md_hist": "where to put markdown backups; s=subfolder, v=volHist, n=nope",
328
330
  "exp": "enable textfile expansion; see --help-exp",
329
331
  "exp_md": "placeholders to expand in markdown files; see --help",
copyparty/ftpd.py CHANGED
@@ -382,7 +382,7 @@ class FtpFs(AbstractedFS):
382
382
  svp = join(self.cwd, src).lstrip("/")
383
383
  dvp = join(self.cwd, dst).lstrip("/")
384
384
  try:
385
- self.hub.up2k.handle_mv(self.uname, self.h.cli_ip, svp, dvp)
385
+ self.hub.up2k.handle_mv("", self.uname, self.h.cli_ip, svp, dvp)
386
386
  except Exception as ex:
387
387
  raise FSE(str(ex))
388
388
 
copyparty/httpcli.py CHANGED
@@ -12,7 +12,6 @@ import random
12
12
  import re
13
13
  import socket
14
14
  import stat
15
- import string
16
15
  import sys
17
16
  import threading # typechk
18
17
  import time
@@ -31,7 +30,7 @@ try:
31
30
  except:
32
31
  pass
33
32
 
34
- from .__init__ import ANYWIN, PY2, RES, TYPE_CHECKING, EnvParams, unicode
33
+ from .__init__ import ANYWIN, RES, TYPE_CHECKING, EnvParams, unicode
35
34
  from .__version__ import S_VERSION
36
35
  from .authsrv import LEELOO_DALLAS, VFS # typechk
37
36
  from .bos import bos
@@ -66,6 +65,7 @@ from .util import (
66
65
  exclude_dotfiles,
67
66
  formatdate,
68
67
  fsenc,
68
+ gen_content_disposition,
69
69
  gen_filekey,
70
70
  gen_filekey_dbg,
71
71
  gencookie,
@@ -619,7 +619,9 @@ class HttpCli(object):
619
619
  or "*"
620
620
  )
621
621
 
622
- if self.args.have_idp_hdrs:
622
+ if self.args.have_idp_hdrs and (
623
+ self.uname == "*" or self.args.ao_idp_before_pw
624
+ ):
623
625
  idp_usr = ""
624
626
  if self.args.idp_hm_usr:
625
627
  for hn, hmv in self.args.idp_hm_usr_p.items():
@@ -632,9 +634,9 @@ class HttpCli(object):
632
634
  if idp_usr:
633
635
  break
634
636
  for hn in self.args.idp_h_usr:
635
- if idp_usr:
637
+ if idp_usr and not self.args.ao_h_before_hm:
636
638
  break
637
- idp_usr = self.headers.get(hn)
639
+ idp_usr = self.headers.get(hn) or idp_usr
638
640
  if idp_usr:
639
641
  idp_grp = (
640
642
  self.headers.get(self.args.idp_h_grp) or ""
@@ -683,7 +685,10 @@ class HttpCli(object):
683
685
  if idp_usr in self.asrv.vfs.aread:
684
686
  self.pw = ""
685
687
  self.uname = idp_usr
686
- self.html_head += "<script>var is_idp=1</script>\n"
688
+ if self.args.ao_have_pw:
689
+ self.html_head += "<script>var is_idp=1</script>\n"
690
+ else:
691
+ self.html_head += "<script>var is_idp=2</script>\n"
687
692
  zs = self.asrv.ases.get(idp_usr)
688
693
  if zs:
689
694
  self.set_idp_cookie(zs)
@@ -691,7 +696,7 @@ class HttpCli(object):
691
696
  self.log("unknown username: %r" % (idp_usr,), 1)
692
697
 
693
698
  if self.args.have_ipu_or_ipr:
694
- if self.args.ipu and self.uname == "*":
699
+ if self.args.ipu and (self.uname == "*" or self.args.ao_ipu_wins):
695
700
  self.uname = self.conn.ipu_iu[self.conn.ipu_nm.map(self.ip)]
696
701
  ipr = self.conn.hsrv.ipr
697
702
  if ipr and self.uname in ipr:
@@ -809,6 +814,15 @@ class HttpCli(object):
809
814
  6 if em.startswith("client d/c ") else 3,
810
815
  )
811
816
 
817
+ if self.hint and self.hint.startswith("<xml> "):
818
+ if self.args.log_badxml:
819
+ t = "invalid XML received from client: %r"
820
+ self.log(t % (self.hint[6:],), 6)
821
+ else:
822
+ t = "received invalid XML from client; enable --log-badxml to see the whole XML in the log"
823
+ self.log(t, 6)
824
+ self.hint = ""
825
+
812
826
  msg = "%s\r\nURL: %s\r\n" % (em, self.vpath)
813
827
  if self.hint:
814
828
  msg += "hint: %s\r\n" % (self.hint,)
@@ -1525,7 +1539,9 @@ class HttpCli(object):
1525
1539
  if not rbuf or len(buf) >= 32768:
1526
1540
  break
1527
1541
 
1528
- xroot = parse_xml(buf.decode(enc, "replace"))
1542
+ sbuf = buf.decode(enc, "replace")
1543
+ self.hint = "<xml> " + sbuf
1544
+ xroot = parse_xml(sbuf)
1529
1545
  xtag = next((x for x in xroot if x.tag.split("}")[-1] == "prop"), None)
1530
1546
  if xtag is not None:
1531
1547
  props = set([y.tag.split("}")[-1] for y in xtag])
@@ -1731,6 +1747,7 @@ class HttpCli(object):
1731
1747
  uenc = enc.upper()
1732
1748
 
1733
1749
  txt = buf.decode(enc, "replace")
1750
+ self.hint = "<xml> " + txt
1734
1751
  ET.register_namespace("D", "DAV:")
1735
1752
  xroot = mkenod("D:orz")
1736
1753
  xroot.insert(0, parse_xml(txt))
@@ -1788,6 +1805,7 @@ class HttpCli(object):
1788
1805
  uenc = enc.upper()
1789
1806
 
1790
1807
  txt = buf.decode(enc, "replace")
1808
+ self.hint = "<xml> " + txt
1791
1809
  ET.register_namespace("D", "DAV:")
1792
1810
  lk = parse_xml(txt)
1793
1811
  assert lk.tag == "{DAV:}lockinfo"
@@ -3995,6 +4013,13 @@ class HttpCli(object):
3995
4013
  if not editions:
3996
4014
  return self.tx_404()
3997
4015
 
4016
+ #
4017
+ # force download
4018
+
4019
+ if "dl" in self.ouparam:
4020
+ cdis = gen_content_disposition(os.path.basename(req_path))
4021
+ self.out_headers["Content-Disposition"] = cdis
4022
+
3998
4023
  #
3999
4024
  # if-modified
4000
4025
 
@@ -4162,6 +4187,13 @@ class HttpCli(object):
4162
4187
  if not editions:
4163
4188
  return self.tx_404()
4164
4189
 
4190
+ #
4191
+ # force download
4192
+
4193
+ if "dl" in self.ouparam:
4194
+ cdis = gen_content_disposition(os.path.basename(req_path))
4195
+ self.out_headers["Content-Disposition"] = cdis
4196
+
4165
4197
  #
4166
4198
  # if-modified
4167
4199
 
@@ -4707,24 +4739,7 @@ class HttpCli(object):
4707
4739
  if maxn < nf:
4708
4740
  raise Pebkac(400, t)
4709
4741
 
4710
- safe = (string.ascii_letters + string.digits).replace("%", "")
4711
- afn = "".join([x if x in safe.replace('"', "") else "_" for x in fn])
4712
- bascii = unicode(safe).encode("utf-8")
4713
- zb = fn.encode("utf-8", "xmlcharrefreplace")
4714
- if not PY2:
4715
- zbl = [
4716
- chr(x).encode("utf-8")
4717
- if x in bascii
4718
- else "%{:02x}".format(x).encode("ascii")
4719
- for x in zb
4720
- ]
4721
- else:
4722
- zbl = [unicode(x) if x in bascii else "%{:02x}".format(ord(x)) for x in zb]
4723
-
4724
- ufn = b"".join(zbl).decode("ascii")
4725
-
4726
- cdis = "attachment; filename=\"{}.{}\"; filename*=UTF-8''{}.{}"
4727
- cdis = cdis.format(afn, ext, ufn, ext)
4742
+ cdis = gen_content_disposition("%s.%s" % (fn, ext))
4728
4743
  self.log(repr(cdis))
4729
4744
  self.send_headers(None, mime=mime, headers={"Content-Disposition": cdis})
4730
4745
 
@@ -4911,7 +4926,8 @@ class HttpCli(object):
4911
4926
  "lastmod": int(ts_md * 1000),
4912
4927
  "lang": self.args.lang,
4913
4928
  "favico": self.args.favico,
4914
- "have_emp": self.args.emp,
4929
+ "have_emp": int(self.args.emp),
4930
+ "md_no_br": int(vn.flags.get("md_no_br") or 0),
4915
4931
  "md_chk_rate": self.args.mcr,
4916
4932
  "md": boundary,
4917
4933
  "arg_base": arg_base,
copyparty/mdns.py CHANGED
@@ -27,7 +27,7 @@ from .stolen.dnslib import (
27
27
  DNSRecord,
28
28
  set_avahi_379,
29
29
  )
30
- from .util import CachedSet, Daemon, Netdev, list_ips, min_ex
30
+ from .util import IP6_LL, CachedSet, Daemon, Netdev, list_ips, min_ex
31
31
 
32
32
  if TYPE_CHECKING:
33
33
  from .svchub import SvcHub
@@ -371,7 +371,7 @@ class MDNS(MCast):
371
371
  cip = addr[0]
372
372
  v6 = ":" in cip
373
373
  if (cip.startswith("169.254") and not self.ll_ok) or (
374
- v6 and not cip.startswith("fe80")
374
+ v6 and not cip.startswith(IP6_LL)
375
375
  ):
376
376
  return
377
377
 
copyparty/multicast.py CHANGED
@@ -15,7 +15,7 @@ from ipaddress import (
15
15
  )
16
16
 
17
17
  from .__init__ import MACOS, TYPE_CHECKING
18
- from .util import Daemon, Netdev, find_prefix, min_ex, spack
18
+ from .util import IP6_LL, IP64_LL, Daemon, Netdev, find_prefix, min_ex, spack
19
19
 
20
20
  if TYPE_CHECKING:
21
21
  from .svchub import SvcHub
@@ -142,7 +142,7 @@ class MCast(object):
142
142
  all_selected = ips[:]
143
143
 
144
144
  # discard non-linklocal ipv6
145
- ips = [x for x in ips if ":" not in x or x.startswith("fe80")]
145
+ ips = [x for x in ips if ":" not in x or x.startswith(IP6_LL)]
146
146
 
147
147
  if not ips:
148
148
  raise NoIPs()
@@ -180,7 +180,7 @@ class MCast(object):
180
180
  srv.ips[oth_ip.split("/")[0]] = ipaddress.ip_network(oth_ip, False)
181
181
 
182
182
  # gvfs breaks if a linklocal ip appears in a dns reply
183
- ll = {k: v for k, v in srv.ips.items() if k.startswith(("169.254", "fe80"))}
183
+ ll = {k: v for k, v in srv.ips.items() if k.startswith(IP64_LL)}
184
184
  rt = {k: v for k, v in srv.ips.items() if k not in ll}
185
185
 
186
186
  if self.args.ll or not rt:
copyparty/pwhash.py CHANGED
@@ -25,6 +25,7 @@ class PWHash(object):
25
25
  self.args = args
26
26
 
27
27
  zsl = args.ah_alg.split(",")
28
+ zsl = [x.strip() for x in zsl]
28
29
  alg = zsl[0]
29
30
  if alg == "none":
30
31
  alg = ""
copyparty/smbd.py CHANGED
@@ -315,7 +315,7 @@ class SMB(object):
315
315
  t = "blocked rename (no-move-acc %s): /%s @%s"
316
316
  yeet(t % (vfs1.axs.umove, vp1, uname))
317
317
 
318
- self.hub.up2k.handle_mv(uname, "1.7.6.2", vp1, vp2)
318
+ self.hub.up2k.handle_mv("", uname, "1.7.6.2", vp1, vp2)
319
319
  try:
320
320
  bos.makedirs(ap2, vf=vfs2.flags)
321
321
  except:
copyparty/svchub.py CHANGED
@@ -128,6 +128,7 @@ class SvcHub(object):
128
128
  self.nsigs = 3
129
129
  self.retcode = 0
130
130
  self.httpsrv_up = 0
131
+ self.qr_tsz = None
131
132
 
132
133
  self.log_mutex = threading.Lock()
133
134
  self.cday = 0
@@ -319,7 +320,7 @@ class SvcHub(object):
319
320
 
320
321
  self._feature_test()
321
322
 
322
- decs = {k: 1 for k in self.args.th_dec.split(",")}
323
+ decs = {k.strip(): 1 for k in self.args.th_dec.split(",")}
323
324
  if not HAVE_VIPS:
324
325
  decs.pop("vips", None)
325
326
  if not HAVE_PIL:
@@ -777,7 +778,27 @@ class SvcHub(object):
777
778
  self.signal_handler(signal.SIGTERM, None)
778
779
 
779
780
  def sticky_qr(self) :
780
- tw, th = termsize()
781
+ self._sticky_qr()
782
+
783
+ def _unsticky_qr(self, flush=True) :
784
+ print("\033[s\033[J\033[r\033[u", file=sys.stderr, end="")
785
+ if flush:
786
+ sys.stderr.flush()
787
+
788
+ def _sticky_qr(self, force = False) :
789
+ sz = termsize()
790
+ if self.qr_tsz == sz:
791
+ if not force:
792
+ return
793
+ else:
794
+ force = False
795
+
796
+ if self.qr_tsz:
797
+ self._unsticky_qr(False)
798
+ else:
799
+ atexit.register(self._unsticky_qr)
800
+
801
+ tw, th = self.qr_tsz = sz
781
802
  zs1, qr = self.tcpsrv.qr.split("\n", 1)
782
803
  url, colr = zs1.split(" ", 1)
783
804
  nl = len(qr.split("\n")) # numlines
@@ -801,17 +822,34 @@ class SvcHub(object):
801
822
  url = "%s\033[%d;%dH%s\033[0m" % (colr, sh + 1, (nl + lp) * 2, url)
802
823
  qr = colr + qr
803
824
 
804
- def unlock():
805
- print("\033[s\033[r\033[u", file=sys.stderr)
806
-
807
- atexit.register(unlock)
808
825
  t = "%s\033[%dA" % ("\n" * nl, nl)
809
826
  t = "%s\033[s\033[1;%dr\033[%dH%s%s\033[u" % (t, sh - 1, sh, qr, url)
810
- self.pr(t, file=sys.stderr)
811
-
812
- def sleepy_qr(self):
813
- time.sleep(self.args.qr_wait)
814
- self.log("qr-code", self.tcpsrv.qr)
827
+ if not force:
828
+ self.log("qr", "sticky-qrcode %sx%s,%s" % (tw, th, sh), 6)
829
+ self.pr(t, file=sys.stderr, end="")
830
+
831
+ def _qr_thr(self):
832
+ qr = self.tcpsrv.qr
833
+ w8 = self.args.qr_wait
834
+ if w8:
835
+ time.sleep(w8)
836
+ self.log("qr-code", qr)
837
+ w8 = self.args.qr_every
838
+ msg = "%s\033[%dA" % (qr, len(qr.split("\n")))
839
+ while w8:
840
+ time.sleep(w8)
841
+ if self.stopping:
842
+ break
843
+ if self.args.qr_pin:
844
+ self._sticky_qr(True)
845
+ else:
846
+ self.log("qr-code", msg)
847
+ w8 = self.args.qr_winch
848
+ while w8:
849
+ time.sleep(w8)
850
+ if self.stopping:
851
+ break
852
+ self._sticky_qr()
815
853
 
816
854
  def cb_httpsrv_up(self) :
817
855
  self.httpsrv_up += 1
@@ -827,11 +865,10 @@ class SvcHub(object):
827
865
  if self.tcpsrv.qr:
828
866
  if self.args.qr_pin:
829
867
  self.sticky_qr()
830
- else:
831
- if self.args.qr_wait:
832
- Daemon(self.sleepy_qr, "qr_w8")
833
- else:
834
- self.log("qr-code", self.tcpsrv.qr)
868
+ if self.args.qr_wait or self.args.qr_every or self.args.qr_winch:
869
+ Daemon(self._qr_thr, "qr")
870
+ elif not self.args.qr_pin:
871
+ self.log("qr-code", self.tcpsrv.qr)
835
872
  else:
836
873
  self.log("root", "workers OK\n")
837
874
 
@@ -1085,7 +1122,7 @@ class SvcHub(object):
1085
1122
  al.tcolor = "".join([x * 2 for x in al.tcolor])
1086
1123
 
1087
1124
  zs = al.u2sz
1088
- zsl = zs.split(",")
1125
+ zsl = [x.strip() for x in zs.split(",")]
1089
1126
  if len(zsl) not in (1, 3):
1090
1127
  t = "invalid --u2sz; must be either one number, or a comma-separated list of three numbers (min,default,max)"
1091
1128
  raise Exception(t)
copyparty/tcpsrv.py CHANGED
@@ -16,6 +16,7 @@ from .util import (
16
16
  E_ADDR_NOT_AVAIL,
17
17
  E_UNREACH,
18
18
  HAVE_IPV6,
19
+ IP6_LL,
19
20
  IP6ALL,
20
21
  VF_CAREFUL,
21
22
  Netdev,
@@ -137,12 +138,12 @@ class TcpSrv(object):
137
138
  # keep IPv6 LL-only nics
138
139
  ll_ok = set()
139
140
  for ip, nd in self.netdevs.items():
140
- if not ip.startswith("fe80"):
141
+ if not ip.startswith(IP6_LL):
141
142
  continue
142
143
 
143
144
  just_ll = True
144
145
  for ip2, nd2 in self.netdevs.items():
145
- if nd == nd2 and ":" in ip2 and not ip2.startswith("fe80"):
146
+ if nd == nd2 and ":" in ip2 and not ip2.startswith(IP6_LL):
146
147
  just_ll = False
147
148
 
148
149
  if just_ll or self.args.ll:
@@ -161,7 +162,7 @@ class TcpSrv(object):
161
162
  title_vars = [x[1:] for x in self.args.wintitle.split(" ") if x.startswith("$")]
162
163
  t = "available @ {}://{}:{}/ (\033[33m{}\033[0m)"
163
164
  for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
164
- if ip.startswith("fe80") and ip not in ll_ok:
165
+ if ip.startswith(IP6_LL) and ip not in ll_ok:
165
166
  continue
166
167
 
167
168
  for port in sorted(self.args.p):
copyparty/up2k.py CHANGED
@@ -88,6 +88,9 @@ ICV_EXTS = set(zsg.split(","))
88
88
  zsg = "3gp,asf,av1,avc,avi,flv,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,vob,webm,wmv"
89
89
  VCV_EXTS = set(zsg.split(","))
90
90
 
91
+ zsg = "aif,aiff,alac,ape,flac,m4a,mp3,oga,ogg,opus,tak,tta,wav,wma,wv"
92
+ ACV_EXTS = set(zsg.split(","))
93
+
91
94
  zsg = "nohash noidx xdev xvol"
92
95
  VF_AFFECTS_INDEXING = set(zsg.split(" "))
93
96
 
@@ -918,6 +921,12 @@ class Up2k(object):
918
921
  with self.mutex, self.reg_mutex:
919
922
  # only need to protect register_vpath but all in one go feels right
920
923
  for vol in vols:
924
+ if bos.path.isfile(vol.realpath):
925
+ self.volstate[vol.vpath] = "online (just-a-file)"
926
+ t = "NOTE: volume [/%s] is a file, not a folder"
927
+ self.log(t % (vol.vpath,))
928
+ continue
929
+
921
930
  try:
922
931
  # mkdir gonna happen at snap anyways;
923
932
  bos.makedirs(vol.realpath, vf=vol.flags)
@@ -1475,7 +1484,7 @@ class Up2k(object):
1475
1484
  unreg = []
1476
1485
  files = []
1477
1486
  fat32 = True
1478
- cv = vcv = ""
1487
+ cv = vcv = acv = ""
1479
1488
 
1480
1489
  th_cvd = self.args.th_coversd
1481
1490
  th_cvds = self.args.th_coversd_set
@@ -1584,9 +1593,11 @@ class Up2k(object):
1584
1593
  cv = iname
1585
1594
  elif not vcv and ext in VCV_EXTS and not iname.startswith("."):
1586
1595
  vcv = iname
1596
+ elif not acv and ext in ACV_EXTS and not iname.startswith("."):
1597
+ acv = iname
1587
1598
 
1588
1599
  if not cv:
1589
- cv = vcv
1600
+ cv = vcv or acv
1590
1601
 
1591
1602
  if not self.args.no_dirsz:
1592
1603
  tnf += len(files)
copyparty/util.py CHANGED
@@ -52,6 +52,7 @@ from .__init__ import (
52
52
  VT100,
53
53
  WINDOWS,
54
54
  EnvParams,
55
+ unicode,
55
56
  )
56
57
  from .__version__ import S_BUILD_DT, S_VERSION
57
58
  from .stolen import surrogateescape
@@ -112,7 +113,13 @@ E_ACCESS = _ens("EACCES WSAEACCES")
112
113
  E_UNREACH = _ens("EHOSTUNREACH WSAEHOSTUNREACH ENETUNREACH WSAENETUNREACH")
113
114
 
114
115
  IP6ALL = "0:0:0:0:0:0:0:0"
116
+ IP6_LL = ("fe8", "fe9", "fea", "feb")
117
+ IP64_LL = ("fe8", "fe9", "fea", "feb", "169.254")
115
118
 
119
+ UC_CDISP = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._"
120
+ BC_CDISP = UC_CDISP.encode("ascii")
121
+ UC_CDISP_SET = set(UC_CDISP)
122
+ BC_CDISP_SET = set(BC_CDISP)
116
123
 
117
124
  try:
118
125
  import fcntl
@@ -1985,6 +1992,29 @@ def gencookie(
1985
1992
  )
1986
1993
 
1987
1994
 
1995
+ def gen_content_disposition(fn ) :
1996
+ safe = UC_CDISP_SET
1997
+ bsafe = BC_CDISP_SET
1998
+ fn = fn.replace("/", "_").replace("\\", "_")
1999
+ zb = fn.encode("utf-8", "xmlcharrefreplace")
2000
+ if not PY2:
2001
+ zbl = [
2002
+ chr(x).encode("utf-8")
2003
+ if x in bsafe
2004
+ else "%{:02X}".format(x).encode("ascii")
2005
+ for x in zb
2006
+ ]
2007
+ else:
2008
+ zbl = [unicode(x) if x in bsafe else "%{:02X}".format(ord(x)) for x in zb]
2009
+
2010
+ ufn = b"".join(zbl).decode("ascii")
2011
+ afn = "".join([x if x in safe else "_" for x in fn]).lstrip(".")
2012
+ while ".." in afn:
2013
+ afn = afn.replace("..", ".")
2014
+
2015
+ return "attachment; filename=\"%s\"; filename*=UTF-8''%s" % (afn, ufn)
2016
+
2017
+
1988
2018
  def humansize(sz , terse = False) :
1989
2019
  for unit in HUMANSIZE_UNITS:
1990
2020
  if sz < 1024:
copyparty/web/a/u2c.py CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
  from __future__ import print_function, unicode_literals
3
3
 
4
- S_VERSION = "2.11"
5
- S_BUILD_DT = "2025-05-18"
4
+ S_VERSION = "2.12"
5
+ S_BUILD_DT = "2025-08-26"
6
6
 
7
7
  """
8
8
  u2c.py: upload to copyparty
@@ -10,7 +10,7 @@ u2c.py: upload to copyparty
10
10
  https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py
11
11
 
12
12
  - dependencies: no
13
- - supports python 2.6, 2.7, and 3.3 through 3.12
13
+ - supports python 2.6, 2.7, and 3.3 through 3.14
14
14
  - if something breaks just try again and it'll autoresume
15
15
  """
16
16
 
@@ -675,7 +675,7 @@ def walkdirs(err, tops, excl):
675
675
  yield stop, ap[len(stop) :].lstrip(sep), inf
676
676
  else:
677
677
  d, n = top.rsplit(sep, 1)
678
- yield d, n, os.stat(top)
678
+ yield d or b"/", n, os.stat(top)
679
679
 
680
680
 
681
681
  # mostly from copyparty/util.py
@@ -1525,10 +1525,10 @@ def main():
1525
1525
 
1526
1526
  # fmt: off
1527
1527
  ap = app = argparse.ArgumentParser(formatter_class=APF, description="copyparty up2k uploader / filesearch tool " + ver, epilog="""
1528
- NOTE:
1529
- source file/folder selection uses rsync syntax, meaning that:
1528
+ NOTE: source file/folder selection uses rsync syntax, meaning that:
1530
1529
  "foo" uploads the entire folder to URL/foo/
1531
1530
  "foo/" uploads the CONTENTS of the folder into URL/
1531
+ NOTE: if server has --usernames enabled, then password is "username:password"
1532
1532
  """)
1533
1533
 
1534
1534
  ap.add_argument("url", type=unicode, help="server url, including destination folder")
Binary file
copyparty/web/md.html CHANGED
@@ -130,7 +130,8 @@ write markdown (most html is 🙆 too)
130
130
 
131
131
  var SR = "{{ r }}",
132
132
  last_modified = {{ lastmod }},
133
- have_emp = {{ "true" if have_emp else "false" }},
133
+ have_emp = {{ have_emp }},
134
+ md_no_br = {{ md_no_br }},
134
135
  dfavico = "{{ favico }}";
135
136
 
136
137
  var md_opt = {
copyparty/web/md.js.gz CHANGED
Binary file
copyparty/web/md2.js.gz CHANGED
Binary file
copyparty/web/mde.html CHANGED
@@ -28,7 +28,8 @@
28
28
 
29
29
  var SR = "{{ r }}",
30
30
  last_modified = {{ lastmod }},
31
- have_emp = {{ "true" if have_emp else "false" }},
31
+ have_emp = {{ have_emp }},
32
+ md_no_br = {{ md_no_br }},
32
33
  dfavico = "{{ favico }}";
33
34
 
34
35
  var md_opt = {
Binary file
copyparty/web/ui.css.gz CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: copyparty
3
- Version: 1.19.5
3
+ Version: 1.19.6
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
@@ -637,6 +637,8 @@ for example `-v /mnt::r -v /var/empty:web/certs:r` mounts the server folder `/mn
637
637
 
638
638
  the example config file right above this section may explain this better; the first volume `/` is mapped to `/srv` which means http://127.0.0.1:3923/music would try to read `/srv/music` on the server filesystem, but since there's another volume at `/music` mapped to `/mnt/music` then it'll go to `/mnt/music` instead
639
639
 
640
+ > ℹ️ this also works for single files, because files can also be volumes
641
+
640
642
 
641
643
  ## dotfiles
642
644
 
@@ -1494,6 +1496,7 @@ and some minor issues,
1494
1496
  * win10 onwards does not allow connecting anonymously / without accounts
1495
1497
  * python3 only
1496
1498
  * slow (the builtin webdav support in windows is 5x faster, and rclone-webdav is 30x faster)
1499
+ * those numbers are specifically for copyparty's smb-server (because it sucks); other smb-servers should be similar to webdav
1497
1500
 
1498
1501
  known client bugs:
1499
1502
  * on win7 only, `--smb1` is much faster than smb2 (default) because it keeps rescanning folders on smb2
@@ -2679,9 +2682,16 @@ NOTE: full bidirectional sync, like what [nextcloud](https://docs.nextcloud.com/
2679
2682
 
2680
2683
  the commandline uploader [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy) with `--dr` is the best way to sync a folder to copyparty; verifies checksums and does files in parallel, and deletes unexpected files on the server after upload has finished which makes file-renames really cheap (it'll rename serverside and skip uploading)
2681
2684
 
2685
+ if you want to sync with `u2c.py` then:
2686
+ * the `e2dsa` option (either globally or volflag) must be enabled on the server for the volumes you're syncing into
2687
+ * ...but DON'T enable global-options `no-hash` or `no-idx` (or volflags `nohash` / `noidx`), or at least make sure they are configured so they do not affect anything you are syncing into
2688
+ * ...and u2c needs the delete-permission, so either `rwd` at minimum, or just `A` which is the same as `rwmd.a`
2689
+ * quick reminder that `a` and `A` are different permissions, and `.` is very useful for sync
2690
+
2682
2691
  alternatively there is [rclone](./docs/rclone.md) which allows for bidirectional sync and is *way* more flexible (stream files straight from sftp/s3/gcs to copyparty, ...), although there is no integrity check and it won't work with files over 100 MiB if copyparty is behind cloudflare
2683
2692
 
2684
2693
  * starting from rclone v1.63, rclone is faster than u2c.py on low-latency connections
2694
+ * but this is only true for the initial upload; u2c will be faster for periodic syncing
2685
2695
 
2686
2696
 
2687
2697
  ## mount as drive
@@ -1,38 +1,38 @@
1
1
  copyparty/__init__.py,sha256=SJtQjM-9PP9K-IaoM9M3iNKvRApp0omOrAN6YtXTPNM,2599
2
- copyparty/__main__.py,sha256=_u7Z2u3E6Aonw8m-wqGqAxbFmZFE8TvriQUQByFEEe4,136752
3
- copyparty/__version__.py,sha256=a01oYLp_GGtSVF39k8jOpCDJ28AnfX88Sni6OfYoQEw,251
4
- copyparty/authsrv.py,sha256=M8oeX_foEreCeftnRNxam4yvVq2hVBrI58u4rytp26c,125592
2
+ copyparty/__main__.py,sha256=l9zhoDdwkKH33EJFoDoDfX2zpf-qjKwMWQVPuIkrmTg,139985
3
+ copyparty/__version__.py,sha256=UAYgCM1TKjFBQYsu5L3qncXYlq_Poy2878QCRjvhDME,251
4
+ copyparty/authsrv.py,sha256=yQHc2Lrl9yVg8bE_ZA6OukKLIS8wukm8Uvo6ANmOY8Q,126249
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
- copyparty/broker_util.py,sha256=76mfnFOpX1gUUvtjm8UQI7jpTIaVINX10QonM-B7ggc,1680
9
- copyparty/cert.py,sha256=pSSeVYticrDsnsrdRtfpUQN-8WRObsqrYtSRroXmgxo,7992
10
- copyparty/cfg.py,sha256=i5HQzxqAtMfJac6DJxulvCNKEFPdqzxZps5yYseLJt4,16108
8
+ copyparty/broker_util.py,sha256=oZ3dGdJXLYvmdV4KSFdUv_PGErkgU-7kgr1SIglkjCU,1663
9
+ copyparty/cert.py,sha256=OGTUBxhqPbseG0Bd4cHD6e5T5T8JdGqp3q0KAYqX0Cc,8031
10
+ copyparty/cfg.py,sha256=SBS3o0fQTnNy-hm97e0dzFJFcjmY8O-jiqZh6sJ3cyE,16204
11
11
  copyparty/dxml.py,sha256=VZADJS9z18LalENSvVfjk29ddnpaDQ-v8AVm_THwS1c,2607
12
12
  copyparty/fsutil.py,sha256=NC_CJC4TDag399vVDH9_uQfdfpTMwRFLNxERSWhlVvs,4594
13
- copyparty/ftpd.py,sha256=K4EClq4dVk6qh0QO8ire1O1Gv_8KzMAxiPBPAM5lfek,18793
14
- copyparty/httpcli.py,sha256=aXLSvoS4bc5SOzUIrOJ7hlDROpTd4QeEoOhiMQ1VEgU,237807
13
+ copyparty/ftpd.py,sha256=QHyrtREOD6s3P9XghZKbq0nlE1KFbpcfLhQk2pIDXGw,18797
14
+ copyparty/httpcli.py,sha256=dFcDgywGgQV0AZ_LxB9Oc2N7sKrISWQIt43PuFigbdc,238501
15
15
  copyparty/httpconn.py,sha256=IA9fdCjigawZ4kWhgvVN3nSiy5pb3W2qaE6rFqUYdq0,6943
16
16
  copyparty/httpsrv.py,sha256=12j76CpAlJEeZU17CbWLnoVqoAPdv4xN48prQtE0IRs,19051
17
17
  copyparty/ico.py,sha256=-7QjF_jIxnPo4Vr0oUPksQ_U_Ef0HRsSPm3s71idOz8,3879
18
- copyparty/mdns.py,sha256=XWh41aBrfnXSF2pFanY20QVk_8nNpk4QmKnCEnR2CfU,18405
18
+ copyparty/mdns.py,sha256=usQLqWfFCz8Nqr5Z1x7dtxrnx_gORWpxAGAQ9tPMS_w,18413
19
19
  copyparty/metrics.py,sha256=1dim0ShnsD5cfspRbeN9Mt14wOIxPRtxCZY2IScikKw,8974
20
20
  copyparty/mtag.py,sha256=yePCWUwiW7G90gd8AsqtDTcyRZ54wfu-ayLRu0i7Hys,22832
21
- copyparty/multicast.py,sha256=ix78aoFs9xZxg_zFqCmIfz0U4nDve3S0_V0CQR4IwrM,12317
22
- copyparty/pwhash.py,sha256=s80IBmdmtwz51yjY6hY9LuYPsMODIZSH2XGS3JEJS7c,4387
23
- copyparty/smbd.py,sha256=Czo8SRkkl4ndCwEUe9Cbr8v0YOnyQHzubGSguPizuTc,14651
21
+ copyparty/multicast.py,sha256=xjCBHbbFI7XEohXxgBCAeExkzXJede6TLc5oR7ov5vM,12322
22
+ copyparty/pwhash.py,sha256=58txP8GXIHOtWd__Tni8qkilHEGpOFsKIPuzjdFLc6M,4426
23
+ copyparty/smbd.py,sha256=x6Y0alKOoMb8GmO0HNqwvucFwUJW9ct1HKL2Bx_b6MY,14655
24
24
  copyparty/ssdp.py,sha256=R1Z61GZOxBMF2Sk4RTxKWMOemogmcjEWG-CvLihd45k,7023
25
25
  copyparty/star.py,sha256=tV5BbX6AiQ7N4UU8DYtSTckNYeoeey4DBqq4LjfymbY,3818
26
26
  copyparty/sutil.py,sha256=E65jAaOzHlJYnqsOKDMPVT8kALPUVexpkSStWBdItkY,3231
27
- copyparty/svchub.py,sha256=NKpJ4LYZz6WRVDbGkdHk7p5aJkfBXzo5ShM9gWJvsBs,51334
27
+ copyparty/svchub.py,sha256=GC8yKpbnYzI-ubRH8JqC4vDXsAMGgaA_-_HOMv3kw0U,52434
28
28
  copyparty/szip.py,sha256=9srQzjsTBrBadf6QMh4YRAL70rkZLevAOHqXWK5jvr8,8846
29
- copyparty/tcpsrv.py,sha256=z1k8wo7xIqNbUJisuW0LH7S_2rjaSYgvZ2ACsC5bzi8,21913
29
+ copyparty/tcpsrv.py,sha256=6SH5ZxBXtQF8hHgReNbvKtqgTFgLFdpgFL_lXL1khqI,21925
30
30
  copyparty/tftpd.py,sha256=fXW0w2_fSbzKVQGTLgr4DbtxaIEfwB591bl0pYhwy6k,14272
31
31
  copyparty/th_cli.py,sha256=n3MMbnN7HTVSBHkTI2qLIjgwVvHkGsfTW7-aPJ-2o4s,5545
32
32
  copyparty/th_srv.py,sha256=iAWBHgtAaYAshXdCFrUx7IWq4y3NXrH79yzHChsy2dc,38335
33
33
  copyparty/u2idx.py,sha256=mTtUKyIv9putnkDel8cttqMlZXDxb3IAga8J7e0kHls,13666
34
- copyparty/up2k.py,sha256=7rxPNOPkRPhm30eGEnSp3IiGhTo3TsiOhokoLgw4j6Y,181037
35
- copyparty/util.py,sha256=1MbLGXCz1r8N8oyuEyPTlJMdy0sgqHVXPfXpdDcr9Cc,106889
34
+ copyparty/up2k.py,sha256=qVPVN6qKAT4H3eeWyirUpgF4D7yH6N-B6m-NhwdJlSw,181539
35
+ copyparty/util.py,sha256=CBSe4e4-vd2zaSfjXd4H_J0tDFFwEy-DI9OjvjpTf7Y,107866
36
36
  copyparty/bos/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
37
  copyparty/bos/bos.py,sha256=DYt5mJJNt-935rU7HRm8kt_whpcVSI0uSphvD7PXrJo,2247
38
38
  copyparty/bos/path.py,sha256=yEjCq2ki9CvxA5sCT8pS0keEXwugs0ZeUyUhdBziOCI,777
@@ -57,18 +57,18 @@ copyparty/stolen/ifaddr/_win32.py,sha256=EE-QyoBgeB7lYQ6z62VjXNaRozaYfCkaJBHGNA8
57
57
  copyparty/web/baguettebox.js.gz,sha256=VR-MdQ11CZ1PkMW0VTv36RPLY5L1w2rDj2CY-TPfvRM,8269
58
58
  copyparty/web/browser.css.gz,sha256=8zQ0rWJvNPUOgOOY4EMCFh20q8S3-bDhiFi4sYx135M,15661
59
59
  copyparty/web/browser.html,sha256=lhelkXI8_HGfuqo_5b6XEGzf8VNodOMXE9kbv31JtbM,4790
60
- copyparty/web/browser.js.gz,sha256=E6jFuEeusjqb_mYo5400iDhq8asUHUlfmENSX3_IEh4,300849
60
+ copyparty/web/browser.js.gz,sha256=kU3y_olayBkx_c8L07St0Ci9cKoLOy16xbLPaZHHDKQ,314574
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
64
64
  copyparty/web/idp.html,sha256=qOjjvZz6fVk0W8cXiP8fXeTp5KMa_dDV7BRzJNUModo,1481
65
65
  copyparty/web/md.css.gz,sha256=UZpN0J7ubVM05CZkbZYkQRJeGgJt_GNDEzKTGSQd8h4,2032
66
- copyparty/web/md.html,sha256=hz-xJVfKtaeTUQn3tGh7ebIMvLbOjVKkrMhsCTr3lGM,4200
67
- copyparty/web/md.js.gz,sha256=94jN-dH86i5r9u1xAvvKjX9It5lzRl-8TUKRSPVL8LM,4164
66
+ copyparty/web/md.html,sha256=jJ-aE6vbQiWlksAPpkqDYWNnTo8tJW1RhEXrMMmcwRg,4205
67
+ copyparty/web/md.js.gz,sha256=moH1ul86OFDTfUKmN09aLVVul3RN-gjI2UxuZGcGjjI,4169
68
68
  copyparty/web/md2.css.gz,sha256=uIVHKScThdbcfhXNSHgKZnALYpxbnXC-WuEzOJ20Lpc,699
69
- copyparty/web/md2.js.gz,sha256=Km7nX6pnZs0GWaqCj54FYu-aYrkxa73C9yCGGOcG4dE,8368
69
+ copyparty/web/md2.js.gz,sha256=xiYIwvKscN4HxefrjkEvc2JtBUUQyEodbKDX5T26ev8,8407
70
70
  copyparty/web/mde.css.gz,sha256=2SkAEDKIRPqywNJ8t_heQaeBQ_R73Rf-pQI_bDoKF6o,942
71
- copyparty/web/mde.html,sha256=fRGUlnNhK6ra8P4jskQLUw6Aaef6g6Heh4EEgYhJkxU,1770
71
+ copyparty/web/mde.html,sha256=JnvToXsrUHwPJUkrZvt6pitrficWjIAbfBofQAVgGyU,1775
72
72
  copyparty/web/mde.js.gz,sha256=FQplRzzMJqC_xNb8gmzj79PrlCfcPdKIm85lM6cR-xA,2220
73
73
  copyparty/web/msg.css.gz,sha256=u90fXYAVrMD-jqwf5XFVC1ptSpSHZUe8Mez6PX101P8,300
74
74
  copyparty/web/msg.html,sha256=w9CM3hkLLGJX9fWEaG4gSbTOPe2GcPqW8BpSCDiFzOI,977
@@ -80,16 +80,16 @@ copyparty/web/shares.html,sha256=ZZ9BIuzhbVtJCAZOb_PAaEY_z9jo8i93QEJolNDHX3g,257
80
80
  copyparty/web/shares.js.gz,sha256=QgtzZ6oKJqGVlAEbhVCn-35F6mnwLwOmndRqlgtJd74,942
81
81
  copyparty/web/splash.css.gz,sha256=rumglcBttk2HVhAEoS65m2GZIQtXd4c9HOHvd82OJeg,1097
82
82
  copyparty/web/splash.html,sha256=91KdrAfoBZgeM8qap5J-KeTVs_O4WvtwiZYpjVBTj1o,6884
83
- copyparty/web/splash.js.gz,sha256=NSTEWUqvpXMHmhdUfbJQkPsqMQzaac9euXEQEdTgwzk,15273
83
+ copyparty/web/splash.js.gz,sha256=9h1yRUYteu7cVxcYWYG06A78YGXN5OS1AgKx3ELQg-8,15888
84
84
  copyparty/web/svcs.html,sha256=wGW7bwY3EIDRpp9apzJsED_UMlHX31X2H3ZnN13tWCI,14913
85
85
  copyparty/web/svcs.js.gz,sha256=_dvatVBMVI_iy5MnYQeZumFGAnu9lAP_WKrhOA1TYxs,852
86
- copyparty/web/ui.css.gz,sha256=e3iIflzddmjoyPrun_1jsu9j7fbdonNQLyhEE2oKKOQ,2819
86
+ copyparty/web/ui.css.gz,sha256=0NOzup4KnBwcKGV7MZJvHaglEvLti6tgwnY4o21wMTo,2848
87
87
  copyparty/web/up2k.js.gz,sha256=pgj3drYXEiHOkSpcZp4ttWCqD72BpFp16cuHObl0zpo,24994
88
88
  copyparty/web/util.js.gz,sha256=A28FamE9n03YkSE09G4IOLvFaDZK-j5Mi-xl124JcC8,15569
89
89
  copyparty/web/w.hash.js.gz,sha256=cFH6Xo4YRgH9Wr7RmHMSEfpuTmmIvEmzmSvv4RLmyPU,1193
90
90
  copyparty/web/a/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
91
91
  copyparty/web/a/partyfuse.py,sha256=9p5Hpg_IBiSimv7j9kmPhCGpy-FLXSRUOYnLjJ5JifU,28049
92
- copyparty/web/a/u2c.py,sha256=f_KR1ZhOjJYBnyYlJbBXY-TnNITeT7HOf0R3pR_9DIM,53248
92
+ copyparty/web/a/u2c.py,sha256=v7YDWBh_d9NbT_-zYZABjPTSqMBCG2naF8PaLZZ2W8o,53334
93
93
  copyparty/web/a/webdav-cfg.bat,sha256=Y4NoGZlksAIg4cBMb7KdJrpKC6Nx97onaTl6yMjaimk,1449
94
94
  copyparty/web/deps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
95
95
  copyparty/web/deps/busy.mp3.gz,sha256=EVphk1_HYyRKJmtpeK99vbAstF7ub1f9ndu020H8PQ8,106
@@ -105,9 +105,9 @@ copyparty/web/deps/prismd.css.gz,sha256=ObUlksQVr-OuYlTz-I4B23TeBg2QDVVGRnWBz8cV
105
105
  copyparty/web/deps/scp.woff2,sha256=w99BDU5i8MukkMEL-iW0YO9H4vFFZSPWxbkH70ytaAg,8612
106
106
  copyparty/web/deps/sha512.ac.js.gz,sha256=lFZaCLumgWxrvEuDr4bqdKHsqjX82AbVAb7_F45Yk88,7033
107
107
  copyparty/web/deps/sha512.hw.js.gz,sha256=UAed2_ocklZCnIzcSYz2h4P1ycztlCLj-ewsRTud2lU,7939
108
- copyparty-1.19.5.dist-info/licenses/LICENSE,sha256=gOr4h33pCsBEg9uIy9AYmb7qlocL4V9t2uPJS5wllr0,1072
109
- copyparty-1.19.5.dist-info/METADATA,sha256=3Ov6qPwDkVJlVCvBQxJLjo5KKCGfAfxYTDi7rnceBME,175971
110
- copyparty-1.19.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
111
- copyparty-1.19.5.dist-info/entry_points.txt,sha256=4zw6a3rqASywQomiYLObjjlxybaI65LYYOTJwgKz7b0,128
112
- copyparty-1.19.5.dist-info/top_level.txt,sha256=LnYUPsDyk-8kFgM6YJLG4h820DQekn81cObKSu9g-sI,10
113
- copyparty-1.19.5.dist-info/RECORD,,
108
+ copyparty-1.19.6.dist-info/licenses/LICENSE,sha256=gOr4h33pCsBEg9uIy9AYmb7qlocL4V9t2uPJS5wllr0,1072
109
+ copyparty-1.19.6.dist-info/METADATA,sha256=1lwqgItGYrevYV6bWbGNj4KsFZfgqeKQmlfL5Km7meA,176818
110
+ copyparty-1.19.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
111
+ copyparty-1.19.6.dist-info/entry_points.txt,sha256=4zw6a3rqASywQomiYLObjjlxybaI65LYYOTJwgKz7b0,128
112
+ copyparty-1.19.6.dist-info/top_level.txt,sha256=LnYUPsDyk-8kFgM6YJLG4h820DQekn81cObKSu9g-sI,10
113
+ copyparty-1.19.6.dist-info/RECORD,,