copyparty 1.18.9__py3-none-any.whl → 1.19.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.
Files changed (42) hide show
  1. copyparty/__init__.py +0 -4
  2. copyparty/__main__.py +85 -69
  3. copyparty/__version__.py +3 -3
  4. copyparty/authsrv.py +31 -2
  5. copyparty/cfg.py +2 -0
  6. copyparty/ftpd.py +7 -2
  7. copyparty/httpcli.py +87 -31
  8. copyparty/httpsrv.py +2 -1
  9. copyparty/mtag.py +5 -1
  10. copyparty/multicast.py +1 -5
  11. copyparty/pwhash.py +4 -0
  12. copyparty/svchub.py +2 -11
  13. copyparty/tcpsrv.py +16 -6
  14. copyparty/tftpd.py +1 -1
  15. copyparty/th_cli.py +2 -2
  16. copyparty/th_srv.py +68 -2
  17. copyparty/up2k.py +21 -18
  18. copyparty/util.py +11 -0
  19. copyparty/web/browser.css.gz +0 -0
  20. copyparty/web/browser.js.gz +0 -0
  21. copyparty/web/md2.js.gz +0 -0
  22. copyparty/web/mde.js.gz +0 -0
  23. copyparty/web/rups.html +1 -8
  24. copyparty/web/rups.js.gz +0 -0
  25. copyparty/web/shares.js.gz +0 -0
  26. copyparty/web/splash.html +6 -1
  27. copyparty/web/splash.js.gz +0 -0
  28. copyparty/web/svcs.html +1 -0
  29. copyparty/web/svcs.js.gz +0 -0
  30. copyparty/web/up2k.js.gz +0 -0
  31. copyparty/web/util.js.gz +0 -0
  32. {copyparty-1.18.9.dist-info → copyparty-1.19.0.dist-info}/METADATA +50 -9
  33. {copyparty-1.18.9.dist-info → copyparty-1.19.0.dist-info}/RECORD +37 -42
  34. copyparty/web/dd/2.png +0 -0
  35. copyparty/web/dd/3.png +0 -0
  36. copyparty/web/dd/4.png +0 -0
  37. copyparty/web/dd/5.png +0 -0
  38. copyparty/web/dd/__init__.py +0 -0
  39. {copyparty-1.18.9.dist-info → copyparty-1.19.0.dist-info}/WHEEL +0 -0
  40. {copyparty-1.18.9.dist-info → copyparty-1.19.0.dist-info}/entry_points.txt +0 -0
  41. {copyparty-1.18.9.dist-info → copyparty-1.19.0.dist-info}/licenses/LICENSE +0 -0
  42. {copyparty-1.18.9.dist-info → copyparty-1.19.0.dist-info}/top_level.txt +0 -0
copyparty/cfg.py CHANGED
@@ -111,6 +111,7 @@ def vf_vmap() :
111
111
  "tail_tmax",
112
112
  "tail_who",
113
113
  "tcolor",
114
+ "txt_eol",
114
115
  "unlist",
115
116
  "u2abort",
116
117
  "u2ts",
@@ -322,6 +323,7 @@ flagcats = {
322
323
  "exp": "enable textfile expansion; see --help-exp",
323
324
  "exp_md": "placeholders to expand in markdown files; see --help",
324
325
  "exp_lg": "placeholders to expand in prologue/epilogue; see --help",
326
+ "txt_eol=lf": "enable EOL conversion when writing docs (LF or CRLF)",
325
327
  },
326
328
  "tailing": {
327
329
  "notail": "disable ?tail (download a growing file continuously)",
copyparty/ftpd.py CHANGED
@@ -79,7 +79,12 @@ class FtpAuth(DummyAuthorizer):
79
79
  uname = "*"
80
80
  if username != "anonymous":
81
81
  uname = ""
82
- for zs in (password, username):
82
+ if args.usernames:
83
+ alts = ["%s:%s" % (username, password)]
84
+ else:
85
+ alts = password, username
86
+
87
+ for zs in alts:
83
88
  zs = asrv.iacct.get(asrv.ah.hash(zs), "")
84
89
  if zs:
85
90
  uname = zs
@@ -603,7 +608,7 @@ class Ftpd(object):
603
608
  if "::" in ips:
604
609
  ips.append("0.0.0.0")
605
610
 
606
- ips = [x for x in ips if "unix:" not in x]
611
+ ips = [x for x in ips if not x.startswith(("unix:", "fd:"))]
607
612
 
608
613
  if self.args.ftp4:
609
614
  ips = [x for x in ips if ":" not in x]
copyparty/httpcli.py CHANGED
@@ -62,6 +62,7 @@ from .util import (
62
62
  alltrace,
63
63
  atomic_move,
64
64
  b64dec,
65
+ eol_conv,
65
66
  exclude_dotfiles,
66
67
  formatdate,
67
68
  fsenc,
@@ -257,7 +258,8 @@ class HttpCli(object):
257
258
 
258
259
  def _assert_safe_rem(self, rem ) :
259
260
  # sanity check to prevent any disasters
260
- if rem.startswith("/") or rem.startswith("../") or "/../" in rem:
261
+ # (this function hopefully serves no purpose; validation has already happened at this point, this only exists as a last-ditch effort just in case)
262
+ if rem.startswith(("/", "../")) or "/../" in rem:
261
263
  raise Exception("that was close")
262
264
 
263
265
  def _gen_fk(self, alg , salt , fspath , fsize , inode ) :
@@ -378,9 +380,20 @@ class HttpCli(object):
378
380
  try:
379
381
  cli_ip = zsl[n].strip()
380
382
  except:
381
- cli_ip = zsl[0].strip()
382
- t = "rproxy={} oob x-fwd {}"
383
- self.log(t.format(self.args.rproxy, zso), c=3)
383
+ cli_ip = self.ip
384
+ self.bad_xff = True
385
+ if self.args.rproxy != 9999999:
386
+ t = "global-option --rproxy %d could not be used (out-of-bounds) for the received header [%s]"
387
+ self.log(t % (self.args.rproxy, zso), c=3)
388
+ else:
389
+ zsl = [
390
+ " rproxy: %d if this client's IP-address is [%s]"
391
+ % (-1 - zd, zs.strip())
392
+ for zd, zs in enumerate(zsl)
393
+ ]
394
+ t = 'could not determine the client\'s IP-address because the global-option --rproxy has not been configured, so the request-header [%s] specified by global-option --xff-hdr cannot be used safely! Please see the "reverse-proxy" section in the readme. The best approach is to configure your reverse-proxy to give copyparty the exact IP-address to assume (perhaps in another header), but you may also try the following:'
395
+ t = t % (self.args.xff_hdr,)
396
+ self.log("%s\n\n%s\n" % (t, "\n".join(zsl)), 3)
384
397
 
385
398
  pip = self.conn.addr[0]
386
399
  xffs = self.conn.xff_nm
@@ -653,6 +666,9 @@ class HttpCli(object):
653
666
  self.pw = ""
654
667
  self.uname = idp_usr
655
668
  self.html_head += "<script>var is_idp=1</script>\n"
669
+ zs = self.asrv.ases.get(idp_usr)
670
+ if zs:
671
+ self.set_idp_cookie(zs)
656
672
  else:
657
673
  self.log("unknown username: %r" % (idp_usr,), 1)
658
674
 
@@ -1191,15 +1207,6 @@ class HttpCli(object):
1191
1207
  self.reply(b"ssdp is disabled in server config", 404)
1192
1208
  return False
1193
1209
 
1194
- if self.vpath.startswith(".cpr/dd/") and self.args.mpmc:
1195
- if self.args.mpmc == ".":
1196
- raise Pebkac(404)
1197
-
1198
- loc = self.args.mpmc.rstrip("/") + self.vpath[self.vpath.rfind("/") :]
1199
- h = {"Location": loc, "Cache-Control": "max-age=39"}
1200
- self.reply(b"", 301, headers=h)
1201
- return True
1202
-
1203
1210
  if self.vpath == ".cpr/metrics":
1204
1211
  return self.conn.hsrv.metrics.tx(self)
1205
1212
 
@@ -2075,16 +2082,16 @@ class HttpCli(object):
2075
2082
  rnd, lifetime, xbu, xau = self.upload_flags(vfs)
2076
2083
  lim = vfs.get_dbv(rem)[0].lim
2077
2084
  fdir = vfs.canonical(rem)
2078
- if lim:
2079
- fdir, rem = lim.all(
2080
- self.ip, rem, remains, vfs.realpath, fdir, self.conn.hsrv.broker
2081
- )
2082
-
2083
2085
  fn = None
2084
2086
  if rem and not self.trailing_slash and not bos.path.isdir(fdir):
2085
2087
  fdir, fn = os.path.split(fdir)
2086
2088
  rem, _ = vsplit(rem)
2087
2089
 
2090
+ if lim:
2091
+ fdir, rem = lim.all(
2092
+ self.ip, rem, remains, vfs.realpath, fdir, self.conn.hsrv.broker
2093
+ )
2094
+
2088
2095
  bos.makedirs(fdir, vf=vfs.flags)
2089
2096
 
2090
2097
  open_ka = {"fun": open}
@@ -2918,12 +2925,16 @@ class HttpCli(object):
2918
2925
  return True
2919
2926
 
2920
2927
  def handle_chpw(self) :
2928
+ if self.args.usernames:
2929
+ self.parser.require("uname", 64)
2921
2930
  pwd = self.parser.require("pw", 64)
2922
2931
  self.parser.drop()
2923
2932
 
2924
2933
  ok, msg = self.asrv.chpw(self.conn.hsrv.broker, self.uname, pwd)
2925
2934
  if ok:
2926
2935
  self.cbonk(self.conn.hsrv.gpwc, pwd, "pw", "too many password changes")
2936
+ if self.args.usernames:
2937
+ pwd = "%s:%s" % (self.uname, pwd)
2927
2938
  ok, msg = self.get_pwd_cookie(pwd)
2928
2939
  if ok:
2929
2940
  msg = "new password OK"
@@ -2935,6 +2946,15 @@ class HttpCli(object):
2935
2946
  return True
2936
2947
 
2937
2948
  def handle_login(self) :
2949
+ if self.args.usernames and not (
2950
+ self.args.shr and self.vpath.startswith(self.args.shr1)
2951
+ ):
2952
+ try:
2953
+ un = self.parser.require("uname", 64)
2954
+ except:
2955
+ un = ""
2956
+ else:
2957
+ un = ""
2938
2958
  pwd = self.parser.require("cppwd", 64)
2939
2959
  try:
2940
2960
  uhash = self.parser.require("uhash", 256)
@@ -2945,6 +2965,9 @@ class HttpCli(object):
2945
2965
  if not pwd:
2946
2966
  raise Pebkac(422, "password cannot be blank")
2947
2967
 
2968
+ if un:
2969
+ pwd = "%s:%s" % (un, pwd)
2970
+
2948
2971
  dst = self.args.SRS
2949
2972
  if self.vpath:
2950
2973
  dst += quotep(self.vpaths)
@@ -3024,6 +3047,19 @@ class HttpCli(object):
3024
3047
 
3025
3048
  return dur > 0, msg
3026
3049
 
3050
+ def set_idp_cookie(self, ases) :
3051
+ k = "cppws" if self.is_https else "cppwd"
3052
+ ck = gencookie(
3053
+ k,
3054
+ ases,
3055
+ self.args.R,
3056
+ self.args.cookie_lax,
3057
+ self.is_https,
3058
+ self.args.idp_cookie,
3059
+ "; HttpOnly",
3060
+ )
3061
+ self.out_headers["Set-Cookie"] = ck
3062
+
3027
3063
  def handle_mkdir(self) :
3028
3064
  new_dir = self.parser.require("name", 512)
3029
3065
  self.parser.drop()
@@ -3559,7 +3595,7 @@ class HttpCli(object):
3559
3595
  rem = "{}/{}".format(rp, fn).strip("/")
3560
3596
  dbv, vrem = vfs.get_dbv(rem)
3561
3597
 
3562
- if not rem.endswith(".md") and not self.can_delete:
3598
+ if not rem.lower().endswith(".md") and not self.can_delete:
3563
3599
  raise Pebkac(400, "only markdown pls")
3564
3600
 
3565
3601
  if nullwrite:
@@ -3642,6 +3678,9 @@ class HttpCli(object):
3642
3678
  if p_field != "body":
3643
3679
  raise Pebkac(400, "expected body, got {}".format(p_field))
3644
3680
 
3681
+ if "txt_eol" in vfs.flags:
3682
+ p_data = eol_conv(p_data, vfs.flags["txt_eol"])
3683
+
3645
3684
  xbu = vfs.flags.get("xbu")
3646
3685
  if xbu:
3647
3686
  if not runhook(
@@ -4608,7 +4647,9 @@ class HttpCli(object):
4608
4647
  else:
4609
4648
  fn = self.host.split(":")[0]
4610
4649
 
4611
- if vn.flags.get("zipmax") and (not self.uname or not "zipmaxu" in vn.flags):
4650
+ if vn.flags.get("zipmax") and not (
4651
+ vn.flags.get("zipmaxu") and self.uname != "*"
4652
+ ):
4612
4653
  maxs = vn.flags.get("zipmaxs_v") or 0
4613
4654
  maxn = vn.flags.get("zipmaxn_v") or 0
4614
4655
  nf = 0
@@ -4662,7 +4703,7 @@ class HttpCli(object):
4662
4703
  # for f in fgen: print(repr({k: f[k] for k in ["vp", "ap"]}))
4663
4704
  cfmt = ""
4664
4705
  if self.thumbcli and not self.args.no_bacode:
4665
- for zs in ("opus", "mp3", "w", "j", "p"):
4706
+ for zs in ("opus", "mp3", "flac", "wav", "w", "j", "p"):
4666
4707
  if zs in self.ouparam or uarg == zs:
4667
4708
  cfmt = zs
4668
4709
 
@@ -5001,7 +5042,7 @@ class HttpCli(object):
5001
5042
  wvol = [x for x in wvol if "unlistcw" not in allvols[x[1:-1]].flags]
5002
5043
 
5003
5044
  fmt = self.uparam.get("ls", "")
5004
- if not fmt and (self.ua.startswith("curl/") or self.ua.startswith("fetch")):
5045
+ if not fmt and self.ua.startswith(("curl/", "fetch")):
5005
5046
  fmt = "v"
5006
5047
 
5007
5048
  if fmt in ["v", "t", "txt"]:
@@ -5041,6 +5082,13 @@ class HttpCli(object):
5041
5082
  self.reply(zb, mime="text/plain; charset=utf-8")
5042
5083
  return True
5043
5084
 
5085
+ re_btn = ""
5086
+ nre = self.args.ctl_re
5087
+ if "re" in self.uparam:
5088
+ self.out_headers["Refresh"] = str(nre)
5089
+ elif nre:
5090
+ re_btn = "&re=%s" % (nre,)
5091
+
5044
5092
  html = self.j2s(
5045
5093
  "splash",
5046
5094
  this=self,
@@ -5058,6 +5106,7 @@ class HttpCli(object):
5058
5106
  mtpq=vs["mtpq"],
5059
5107
  dbwt=vs["dbwt"],
5060
5108
  url_suf=suf,
5109
+ re=re_btn,
5061
5110
  k304=self.k304(),
5062
5111
  no304=self.no304(),
5063
5112
  k304vis=self.args.k304 > 0,
@@ -5103,7 +5152,7 @@ class HttpCli(object):
5103
5152
  t = '<h1 id="n">404 not found &nbsp;┐( ´ -`)┌</h1><p><a id="r" href="{}/?h">go home</a></p>'
5104
5153
  pt = "404 not found ┐( ´ -`)┌"
5105
5154
 
5106
- if self.ua.startswith("curl/") or self.ua.startswith("fetch"):
5155
+ if self.ua.startswith(("curl/", "fetch")):
5107
5156
  pt = "# acct: %s\n%s\n" % (self.uname, pt)
5108
5157
  self.reply(pt.encode("utf-8"), status=rc)
5109
5158
  return True
@@ -5417,6 +5466,8 @@ class HttpCli(object):
5417
5466
  elif nfi == 3:
5418
5467
  if not vp.endswith(vfi):
5419
5468
  continue
5469
+ else:
5470
+ continue
5420
5471
 
5421
5472
  n -= 1
5422
5473
  if not n:
@@ -5540,6 +5591,8 @@ class HttpCli(object):
5540
5591
  elif nfi == 3:
5541
5592
  if not vp.endswith(vfi):
5542
5593
  continue
5594
+ else:
5595
+ continue
5543
5596
 
5544
5597
  if not dots and "/." in vp:
5545
5598
  continue
@@ -5970,6 +6023,12 @@ class HttpCli(object):
5970
6023
  else:
5971
6024
  [x.pop(k) for k in ["name", "dt"] for y in [dirs, files] for x in y]
5972
6025
 
6026
+ # nonce (tlnote: norwegian for flake as in snowflake)
6027
+ if self.args.no_fnugg:
6028
+ ls["fnugg"] = "nei"
6029
+ elif "fnugg" in self.headers:
6030
+ ls["fnugg"] = self.headers["fnugg"]
6031
+
5973
6032
  ret = json.dumps(ls)
5974
6033
  mime = "application/json"
5975
6034
 
@@ -6152,7 +6211,8 @@ class HttpCli(object):
6152
6211
  if not use_filekey:
6153
6212
  return self.tx_404(True)
6154
6213
 
6155
- if add_og and not abspath.lower().endswith(".md"):
6214
+ is_md = abspath.lower().endswith(".md")
6215
+ if add_og and not is_md:
6156
6216
  if og_ua or self.host not in self.headers.get("referer", ""):
6157
6217
  self.vpath, og_fn = vsplit(self.vpath)
6158
6218
  vpath = self.vpath
@@ -6164,10 +6224,10 @@ class HttpCli(object):
6164
6224
  vpnodes.pop()
6165
6225
 
6166
6226
  if (
6167
- (abspath.endswith(".md") or self.can_delete)
6227
+ (is_md or self.can_delete)
6168
6228
  and "nohtml" not in vn.flags
6169
6229
  and (
6170
- ("v" in self.uparam and abspath.endswith(".md"))
6230
+ (is_md and "v" in self.uparam)
6171
6231
  or "edit" in self.uparam
6172
6232
  or "edit2" in self.uparam
6173
6233
  )
@@ -6224,11 +6284,7 @@ class HttpCli(object):
6224
6284
  is_ls = "ls" in self.uparam
6225
6285
  is_js = self.args.force_js or self.cookies.get("js") == "y"
6226
6286
 
6227
- if (
6228
- not is_ls
6229
- and not add_og
6230
- and (self.ua.startswith("curl/") or self.ua.startswith("fetch"))
6231
- ):
6287
+ if not is_ls and not add_og and self.ua.startswith(("curl/", "fetch")):
6232
6288
  self.uparam["ls"] = "v"
6233
6289
  is_ls = True
6234
6290
 
copyparty/httpsrv.py CHANGED
@@ -319,7 +319,8 @@ class HttpSrv(object):
319
319
  spins = 0
320
320
  while self.ncli >= self.nclimax:
321
321
  if not spins:
322
- self.log(self.name, "at connection limit; waiting", 3)
322
+ t = "at connection limit (global-option 'nc'); waiting"
323
+ self.log(self.name, t, 3)
323
324
 
324
325
  spins += 1
325
326
  time.sleep(0.1)
copyparty/mtag.py CHANGED
@@ -61,6 +61,8 @@ HAVE_FFPROBE = not os.environ.get("PRTY_NO_FFPROBE") and have_ff("ffprobe")
61
61
  CBZ_PICS = set("png jpg jpeg gif bmp tga tif tiff webp avif".split())
62
62
  CBZ_01 = re.compile(r"(^|[^0-9v])0+[01]\b")
63
63
 
64
+ FMT_AU = set("mp3 ogg flac wav".split())
65
+
64
66
 
65
67
  class MParser(object):
66
68
  def __init__(self, cmdline ) :
@@ -236,7 +238,7 @@ def parse_ffprobe(txt ) :
236
238
  ret = {} # processed
237
239
  md = {} # raw tags
238
240
 
239
- is_audio = fmt.get("format_name") in ["mp3", "ogg", "flac", "wav"]
241
+ is_audio = fmt.get("format_name") in FMT_AU
240
242
  if fmt.get("filename", "").split(".")[-1].lower() in ["m4a", "aac"]:
241
243
  is_audio = True
242
244
 
@@ -264,6 +266,8 @@ def parse_ffprobe(txt ) :
264
266
  ["channel_layout", "chs"],
265
267
  ["sample_rate", ".hz"],
266
268
  ["bit_rate", ".aq"],
269
+ ["bits_per_sample", ".bps"],
270
+ ["bits_per_raw_sample", ".bprs"],
267
271
  ["duration", ".dur"],
268
272
  ]
269
273
 
copyparty/multicast.py CHANGED
@@ -180,11 +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 = {
184
- k: v
185
- for k, v in srv.ips.items()
186
- if k.startswith("169.254") or k.startswith("fe80")
187
- }
183
+ ll = {k: v for k, v in srv.ips.items() if k.startswith(("169.254", "fe80"))}
188
184
  rt = {k: v for k, v in srv.ips.items() if k not in ll}
189
185
 
190
186
  if self.args.ll or not rt:
copyparty/pwhash.py CHANGED
@@ -147,6 +147,10 @@ class PWHash(object):
147
147
  def cli(self) :
148
148
  import getpass
149
149
 
150
+ if self.args.usernames:
151
+ t = "since you have enabled --usernames, please provide username:password"
152
+ print(t)
153
+
150
154
  while True:
151
155
  try:
152
156
  p1 = getpass.getpass("password> ")
copyparty/svchub.py CHANGED
@@ -840,15 +840,6 @@ class SvcHub(object):
840
840
 
841
841
  def _check_env(self) :
842
842
  al = self.args
843
- try:
844
- files = os.listdir(E.cfg)
845
- except:
846
- files = []
847
-
848
- hits = [x for x in files if x.lower().endswith(".conf")]
849
- if hits:
850
- t = "WARNING: found config files in [%s]: %s\n config files are not expected here, and will NOT be loaded (unless your setup is intentionally hella funky)"
851
- self.log("root", t % (E.cfg, ", ".join(hits)), 3)
852
843
 
853
844
  if self.args.no_bauth:
854
845
  t = "WARNING: --no-bauth disables support for the Android app; you may want to use --bauth-last instead"
@@ -858,7 +849,7 @@ class SvcHub(object):
858
849
 
859
850
  have_tcp = False
860
851
  for zs in al.i:
861
- if not zs.startswith("unix:"):
852
+ if not zs.startswith(("unix:", "fd:")):
862
853
  have_tcp = True
863
854
  if not have_tcp:
864
855
  zb = False
@@ -868,7 +859,7 @@ class SvcHub(object):
868
859
  setattr(al, zs, False)
869
860
  zb = True
870
861
  if zb:
871
- t = "only listening on unix-sockets; cannot enable zeroconf/mdns/ssdp as requested"
862
+ t = "not listening on any ip-addresses (only unix-sockets and/or FDs); cannot enable zeroconf/mdns/ssdp as requested"
872
863
  self.log("root", t, 3)
873
864
 
874
865
  if not self.args.no_dav:
copyparty/tcpsrv.py CHANGED
@@ -242,8 +242,10 @@ class TcpSrv(object):
242
242
 
243
243
  def _listen(self, ip , port ) :
244
244
  uds_perm = uds_gid = -1
245
+ bound = None
246
+ tcp = False
247
+
245
248
  if "unix:" in ip:
246
- tcp = False
247
249
  ipv = socket.AF_UNIX
248
250
  uds = ip.split(":")
249
251
  ip = uds[-1]
@@ -256,7 +258,12 @@ class TcpSrv(object):
256
258
  import grp
257
259
 
258
260
  uds_gid = grp.getgrnam(uds[2]).gr_gid
261
+ elif "fd:" in ip:
262
+ fd = ip[3:]
263
+ bound = socket.socket(fileno=int(fd))
259
264
 
265
+ tcp = bound.proto == socket.IPPROTO_TCP
266
+ ipv = bound.family
260
267
  elif ":" in ip:
261
268
  tcp = True
262
269
  ipv = socket.AF_INET6
@@ -264,7 +271,7 @@ class TcpSrv(object):
264
271
  tcp = True
265
272
  ipv = socket.AF_INET
266
273
 
267
- srv = socket.socket(ipv, socket.SOCK_STREAM)
274
+ srv = bound or socket.socket(ipv, socket.SOCK_STREAM)
268
275
 
269
276
  if not ANYWIN or self.args.reuseaddr:
270
277
  srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@@ -282,6 +289,10 @@ class TcpSrv(object):
282
289
  if getattr(self.args, "freebind", False):
283
290
  srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1)
284
291
 
292
+ if bound:
293
+ self.srv.append(srv)
294
+ return
295
+
285
296
  try:
286
297
  if tcp:
287
298
  srv.bind((ip, port))
@@ -434,7 +445,7 @@ class TcpSrv(object):
434
445
  def detect_interfaces(self, listen_ips ) :
435
446
  from .stolen.ifaddr import get_adapters
436
447
 
437
- listen_ips = [x for x in listen_ips if "unix:" not in x]
448
+ listen_ips = [x for x in listen_ips if not x.startswith(("unix:", "fd:"))]
438
449
 
439
450
  nics = get_adapters(True)
440
451
  eps = {}
@@ -580,8 +591,7 @@ class TcpSrv(object):
580
591
  if not ip:
581
592
  return ""
582
593
 
583
- if ":" in ip:
584
- ip = "[{}]".format(ip)
594
+ hip = "[%s]" % (ip,) if ":" in ip else ip
585
595
 
586
596
  if self.args.http_only:
587
597
  https = ""
@@ -593,7 +603,7 @@ class TcpSrv(object):
593
603
  ports = t1.get(ip, t2.get(ip, []))
594
604
  dport = 443 if https else 80
595
605
  port = "" if dport in ports or not ports else ":{}".format(ports[0])
596
- txt = "http{}://{}{}/{}".format(https, ip, port, self.args.qrl)
606
+ txt = "http{}://{}{}/{}".format(https, hip, port, self.args.qrl)
597
607
 
598
608
  btxt = txt.encode("utf-8")
599
609
  if PY2:
copyparty/tftpd.py CHANGED
@@ -176,7 +176,7 @@ class Tftpd(object):
176
176
  if "::" in ips:
177
177
  ips.append("0.0.0.0")
178
178
 
179
- ips = [x for x in ips if "unix:" not in x]
179
+ ips = [x for x in ips if not x.startswith(("unix:", "fd:"))]
180
180
 
181
181
  if self.args.tftp4:
182
182
  ips = [x for x in ips if ":" not in x]
copyparty/th_cli.py CHANGED
@@ -85,7 +85,7 @@ class ThumbCli(object):
85
85
  if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg", "png"]:
86
86
  return os.path.join(ptop, rem)
87
87
 
88
- if fmt[:1] in "jw":
88
+ if fmt[:1] in "jw" and fmt != "wav":
89
89
  sfmt = fmt[:1]
90
90
 
91
91
  if sfmt == "j" and self.args.th_no_jpg:
@@ -126,7 +126,7 @@ class ThumbCli(object):
126
126
 
127
127
  tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa)
128
128
  tpaths = [tpath]
129
- if fmt[:1] == "w":
129
+ if fmt[:1] == "w" and fmt != "wav":
130
130
  # also check for jpg (maybe webp is unavailable)
131
131
  tpaths.append(tpath.rsplit(".", 1)[0] + ".jpg")
132
132
 
copyparty/th_srv.py CHANGED
@@ -47,7 +47,7 @@ HAVE_AVIF = False
47
47
  HAVE_WEBP = False
48
48
 
49
49
  EXTS_TH = set(["jpg", "webp", "png"])
50
- EXTS_AC = set(["opus", "owa", "caf", "mp3"])
50
+ EXTS_AC = set(["opus", "owa", "caf", "mp3", "flac", "wav"])
51
51
  EXTS_SPEC_SAFE = set("aif aiff flac mp3 opus wav".split())
52
52
 
53
53
  PTN_TS = re.compile("^-?[0-9a-f]{8,10}$")
@@ -352,8 +352,10 @@ class ThumbSrv(object):
352
352
  tex = tpath.rsplit(".", 1)[-1]
353
353
  want_mp3 = tex == "mp3"
354
354
  want_opus = tex in ("opus", "owa", "caf")
355
+ want_flac = tex == "flac"
356
+ want_wav = tex == "wav"
355
357
  want_png = tex == "png"
356
- want_au = want_mp3 or want_opus
358
+ want_au = want_mp3 or want_opus or want_flac or want_wav
357
359
  for lib in self.args.th_dec:
358
360
  can_au = lib == "ff" and (
359
361
  ext in self.fmt_ffa or ext in self.fmt_ffv
@@ -368,6 +370,10 @@ class ThumbSrv(object):
368
370
  funs.append(self.conv_opus)
369
371
  elif want_mp3:
370
372
  funs.append(self.conv_mp3)
373
+ elif want_flac:
374
+ funs.append(self.conv_flac)
375
+ elif want_wav:
376
+ funs.append(self.conv_wav)
371
377
  elif want_png:
372
378
  funs.append(self.conv_waves)
373
379
  png_ok = True
@@ -803,6 +809,66 @@ class ThumbSrv(object):
803
809
  # fmt: on
804
810
  self._run_ff(cmd, vn, oom=300)
805
811
 
812
+ def conv_flac(self, abspath , tpath , fmt , vn ) :
813
+ if self.args.no_acode or not self.args.allow_flac:
814
+ raise Exception("flac not permitted in server config")
815
+
816
+ self.wait4ram(0.2, tpath)
817
+ tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2))
818
+ if "ac" not in tags:
819
+ raise Exception("not audio")
820
+
821
+ self.log("conv2 flac", 6)
822
+
823
+ # fmt: off
824
+ cmd = [
825
+ b"ffmpeg",
826
+ b"-nostdin",
827
+ b"-v", b"error",
828
+ b"-hide_banner",
829
+ b"-i", fsenc(abspath),
830
+ b"-map", b"0:a:0",
831
+ b"-c:a", b"flac",
832
+ fsenc(tpath)
833
+ ]
834
+ # fmt: on
835
+ self._run_ff(cmd, vn, oom=300)
836
+
837
+ def conv_wav(self, abspath , tpath , fmt , vn ) :
838
+ if self.args.no_acode or not self.args.allow_wav:
839
+ raise Exception("wav not permitted in server config")
840
+
841
+ self.wait4ram(0.2, tpath)
842
+ tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2))
843
+ if "ac" not in tags:
844
+ raise Exception("not audio")
845
+
846
+ bits = tags[".bps"][1]
847
+ if bits == 0.0:
848
+ bits = tags[".bprs"][1]
849
+
850
+ codec = b"pcm_s32le"
851
+ if bits <= 16.0:
852
+ codec = b"pcm_s16le"
853
+ elif bits <= 24.0:
854
+ codec = b"pcm_s24le"
855
+
856
+ self.log("conv2 wav", 6)
857
+
858
+ # fmt: off
859
+ cmd = [
860
+ b"ffmpeg",
861
+ b"-nostdin",
862
+ b"-v", b"error",
863
+ b"-hide_banner",
864
+ b"-i", fsenc(abspath),
865
+ b"-map", b"0:a:0",
866
+ b"-c:a", codec,
867
+ fsenc(tpath)
868
+ ]
869
+ # fmt: on
870
+ self._run_ff(cmd, vn, oom=300)
871
+
806
872
  def conv_opus(self, abspath , tpath , fmt , vn ) :
807
873
  if self.args.no_acode or not self.args.q_opus:
808
874
  raise Exception("disabled in server config")