copyparty 1.15.8__py3-none-any.whl → 1.15.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
copyparty/__main__.py CHANGED
@@ -1029,7 +1029,7 @@ def add_network(ap):
1029
1029
  else:
1030
1030
  ap2.add_argument("--freebind", action="store_true", help="allow listening on IPs which do not yet exist, for example if the network interfaces haven't finished going up. Only makes sense for IPs other than '0.0.0.0', '127.0.0.1', '::', and '::1'. May require running as root (unless net.ipv6.ip_nonlocal_bind)")
1031
1031
  ap2.add_argument("--s-thead", metavar="SEC", type=int, default=120, help="socket timeout (read request header)")
1032
- ap2.add_argument("--s-tbody", metavar="SEC", type=float, default=186.0, help="socket timeout (read/write request/response bodies). Use 60 on fast servers (default is extremely safe). Disable with 0 if reverse-proxied for a 2%% speed boost")
1032
+ ap2.add_argument("--s-tbody", metavar="SEC", type=float, default=128.0, help="socket timeout (read/write request/response bodies). Use 60 on fast servers (default is extremely safe). Disable with 0 if reverse-proxied for a 2%% speed boost")
1033
1033
  ap2.add_argument("--s-rd-sz", metavar="B", type=int, default=256*1024, help="socket read size in bytes (indirectly affects filesystem writes; recommendation: keep equal-to or lower-than \033[33m--iobuf\033[0m)")
1034
1034
  ap2.add_argument("--s-wr-sz", metavar="B", type=int, default=256*1024, help="socket write size in bytes")
1035
1035
  ap2.add_argument("--s-wr-slp", metavar="SEC", type=float, default=0.0, help="debug: socket write delay in seconds")
@@ -1299,6 +1299,7 @@ def add_logging(ap):
1299
1299
  ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs")
1300
1300
  ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling")
1301
1301
  ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
1302
+ ap2.add_argument("--ohead", metavar="HEADER", type=u, action='append', help="print response \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
1302
1303
  ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|\?th=[wj]$|/\.(_|ql_|DS_Store$|localized$)", help="dont log URLs matching regex \033[33mRE\033[0m")
1303
1304
 
1304
1305
 
@@ -1349,6 +1350,14 @@ def add_transcoding(ap):
1349
1350
  ap2.add_argument("--ac-maxage", metavar="SEC", type=int, default=86400, help="delete cached transcode output after \033[33mSEC\033[0m seconds")
1350
1351
 
1351
1352
 
1353
+ def add_rss(ap):
1354
+ ap2 = ap.add_argument_group('RSS options')
1355
+ ap2.add_argument("--rss", action="store_true", help="enable RSS output (experimental)")
1356
+ ap2.add_argument("--rss-nf", metavar="HITS", type=int, default=250, help="default number of files to return (url-param 'nf')")
1357
+ ap2.add_argument("--rss-fext", metavar="E,E", type=u, default="", help="default list of file extensions to include (url-param 'fext'); blank=all")
1358
+ ap2.add_argument("--rss-sort", metavar="ORD", type=u, default="m", help="default sort order (url-param 'sort'); [\033[32mm\033[0m]=last-modified [\033[32mu\033[0m]=upload-time [\033[32mn\033[0m]=filename [\033[32ms\033[0m]=filesize; Uppercase=oldest-first. Note that upload-time is 0 for non-uploaded files")
1359
+
1360
+
1352
1361
  def add_db_general(ap, hcores):
1353
1362
  noidx = APPLESAN_TXT if MACOS else ""
1354
1363
  ap2 = ap.add_argument_group('general db options')
@@ -1444,6 +1453,7 @@ def add_ui(ap, retry):
1444
1453
  ap2.add_argument("--pb-url", metavar="URL", type=u, default="https://github.com/9001/copyparty", help="powered-by link; disable with \033[33m-np\033[0m")
1445
1454
  ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)")
1446
1455
  ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
1456
+ ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
1447
1457
  ap2.add_argument("--md-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to ALLOW for README.md docs (volflag=md_sbf); see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox")
1448
1458
  ap2.add_argument("--lg-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to ALLOW for prologue/epilogue docs (volflag=lg_sbf)")
1449
1459
  ap2.add_argument("--no-sb-md", action="store_true", help="don't sandbox README/PREADME.md documents (volflags: no_sb_md | sb_md)")
@@ -1518,6 +1528,7 @@ def run_argparse(
1518
1528
  add_db_metadata(ap)
1519
1529
  add_thumbnail(ap)
1520
1530
  add_transcoding(ap)
1531
+ add_rss(ap)
1521
1532
  add_ftp(ap)
1522
1533
  add_webdav(ap)
1523
1534
  add_tftp(ap)
@@ -1740,6 +1751,9 @@ def main(argv = None) :
1740
1751
  if al.ihead:
1741
1752
  al.ihead = [x.lower() for x in al.ihead]
1742
1753
 
1754
+ if al.ohead:
1755
+ al.ohead = [x.lower() for x in al.ohead]
1756
+
1743
1757
  if HAVE_SSL:
1744
1758
  if al.ssl_ver:
1745
1759
  configure_ssl_ver(al)
copyparty/__version__.py CHANGED
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 15, 8)
3
+ VERSION = (1, 15, 10)
4
4
  CODENAME = "fill the drives"
5
- BUILD_DT = (2024, 10, 16)
5
+ BUILD_DT = (2024, 10, 26)
6
6
 
7
7
  S_VERSION = ".".join(map(str, VERSION))
8
8
  S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
copyparty/authsrv.py CHANGED
@@ -158,8 +158,11 @@ class Lim(object):
158
158
  self.chk_rem(rem)
159
159
  if sz != -1:
160
160
  self.chk_sz(sz)
161
- self.chk_vsz(broker, ptop, sz, volgetter)
162
- self.chk_df(abspath, sz) # side effects; keep last-ish
161
+ else:
162
+ sz = 0
163
+
164
+ self.chk_vsz(broker, ptop, sz, volgetter)
165
+ self.chk_df(abspath, sz) # side effects; keep last-ish
163
166
 
164
167
  ap2, vp2 = self.rot(abspath)
165
168
  if abspath == ap2:
@@ -199,7 +202,15 @@ class Lim(object):
199
202
 
200
203
  if self.dft < time.time():
201
204
  self.dft = int(time.time()) + 300
202
- self.dfv = get_df(abspath)[0] or 0
205
+
206
+ df, du, err = get_df(abspath, True)
207
+ if err:
208
+ t = "failed to read disk space usage for [%s]: %s"
209
+ self.log(t % (abspath, err), 3)
210
+ self.dfv = 0xAAAAAAAAA # 42.6 GiB
211
+ else:
212
+ self.dfv = df or 0
213
+
203
214
  for j in list(self.reg.values()) if self.reg else []:
204
215
  self.dfv -= int(j["size"] / (len(j["hash"]) or 999) * len(j["need"]))
205
216
 
@@ -534,15 +545,14 @@ class VFS(object):
534
545
  return self._get_dbv(vrem)
535
546
 
536
547
  shv, srem = src
537
- return shv, vjoin(srem, vrem)
548
+ return shv._get_dbv(vjoin(srem, vrem))
538
549
 
539
550
  def _get_dbv(self, vrem ) :
540
551
  dbv = self.dbv
541
552
  if not dbv:
542
553
  return self, vrem
543
554
 
544
- tv = [self.vpath[len(dbv.vpath) :].lstrip("/"), vrem]
545
- vrem = "/".join([x for x in tv if x])
555
+ vrem = vjoin(self.vpath[len(dbv.vpath) :].lstrip("/"), vrem)
546
556
  return dbv, vrem
547
557
 
548
558
  def canonical(self, rem , resolve = True) :
@@ -2622,7 +2632,7 @@ class AuthSrv(object):
2622
2632
  ]
2623
2633
 
2624
2634
  csv = set("i p th_covers zm_on zm_off zs_on zs_off".split())
2625
- zs = "c ihead mtm mtp on403 on404 xad xar xau xiu xban xbd xbr xbu xm"
2635
+ zs = "c ihead ohead mtm mtp on403 on404 xad xar xau xiu xban xbd xbr xbu xm"
2626
2636
  lst = set(zs.split())
2627
2637
  askip = set("a v c vc cgen exp_lg exp_md theme".split())
2628
2638
  fskip = set("exp_lg exp_md mv_re_r mv_re_t rm_re_r rm_re_t".split())
copyparty/cfg.py CHANGED
@@ -46,6 +46,7 @@ def vf_bmap() :
46
46
  "og_no_head",
47
47
  "og_s_title",
48
48
  "rand",
49
+ "rss",
49
50
  "xdev",
50
51
  "xlink",
51
52
  "xvol",
copyparty/httpcli.py CHANGED
@@ -2,7 +2,6 @@
2
2
  from __future__ import print_function, unicode_literals
3
3
 
4
4
  import argparse # typechk
5
- import calendar
6
5
  import copy
7
6
  import errno
8
7
  import gzip
@@ -19,7 +18,6 @@ import threading # typechk
19
18
  import time
20
19
  import uuid
21
20
  from datetime import datetime
22
- from email.utils import parsedate
23
21
  from operator import itemgetter
24
22
 
25
23
  import jinja2 # typechk
@@ -107,6 +105,7 @@ from .util import (
107
105
  unquotep,
108
106
  vjoin,
109
107
  vol_san,
108
+ vroots,
110
109
  vsplit,
111
110
  wrename,
112
111
  wunlink,
@@ -123,10 +122,17 @@ _ = (argparse, threading)
123
122
 
124
123
  NO_CACHE = {"Cache-Control": "no-cache"}
125
124
 
125
+ ALL_COOKIES = "k304 no304 js idxh dots cppwd cppws".split()
126
+
127
+ H_CONN_KEEPALIVE = "Connection: Keep-Alive"
128
+ H_CONN_CLOSE = "Connection: Close"
129
+
126
130
  LOGUES = [[0, ".prologue.html"], [1, ".epilogue.html"]]
127
131
 
128
132
  READMES = [[0, ["preadme.md", "PREADME.md"]], [1, ["readme.md", "README.md"]]]
129
133
 
134
+ RSS_SORT = {"m": "mt", "u": "at", "n": "fn", "s": "sz"}
135
+
130
136
 
131
137
  class HttpCli(object):
132
138
  """
@@ -785,11 +791,11 @@ class HttpCli(object):
785
791
 
786
792
  def k304(self) :
787
793
  k304 = self.cookies.get("k304")
788
- return (
789
- k304 == "y"
790
- or (self.args.k304 == 2 and k304 != "n")
791
- or ("; Trident/" in self.ua and not k304)
792
- )
794
+ return k304 == "y" or (self.args.k304 == 2 and k304 != "n")
795
+
796
+ def no304(self) :
797
+ no304 = self.cookies.get("no304")
798
+ return no304 == "y" or (self.args.no304 == 2 and no304 != "n")
793
799
 
794
800
  def _build_html_head(self, maybe_html , kv ) :
795
801
  html = str(maybe_html)
@@ -824,25 +830,28 @@ class HttpCli(object):
824
830
  ) :
825
831
  response = ["%s %s %s" % (self.http_ver, status, HTTPCODE[status])]
826
832
 
827
- if length is not None:
828
- response.append("Content-Length: " + unicode(length))
829
-
830
- if status == 304 and self.k304():
831
- self.keepalive = False
832
-
833
- # close if unknown length, otherwise take client's preference
834
- response.append("Connection: " + ("Keep-Alive" if self.keepalive else "Close"))
835
- response.append("Date: " + formatdate())
836
-
837
833
  # headers{} overrides anything set previously
838
834
  if headers:
839
835
  self.out_headers.update(headers)
840
836
 
841
- # default to utf8 html if no content-type is set
842
- if not mime:
843
- mime = self.out_headers.get("Content-Type") or "text/html; charset=utf-8"
837
+ if status == 304:
838
+ self.out_headers.pop("Content-Length", None)
839
+ self.out_headers.pop("Content-Type", None)
840
+ self.out_headerlist.clear()
841
+ if self.k304():
842
+ self.keepalive = False
843
+ else:
844
+ if length is not None:
845
+ response.append("Content-Length: " + unicode(length))
846
+
847
+ if mime:
848
+ self.out_headers["Content-Type"] = mime
849
+ elif "Content-Type" not in self.out_headers:
850
+ self.out_headers["Content-Type"] = "text/html; charset=utf-8"
844
851
 
845
- self.out_headers["Content-Type"] = mime
852
+ # close if unknown length, otherwise take client's preference
853
+ response.append(H_CONN_KEEPALIVE if self.keepalive else H_CONN_CLOSE)
854
+ response.append("Date: " + formatdate())
846
855
 
847
856
  for k, zs in list(self.out_headers.items()) + self.out_headerlist:
848
857
  response.append("%s: %s" % (k, zs))
@@ -856,6 +865,19 @@ class HttpCli(object):
856
865
  self.cbonk(self.conn.hsrv.gmal, zs, "cc_hdr", "Cc in out-hdr")
857
866
  raise Pebkac(999)
858
867
 
868
+ if self.args.ohead and self.do_log:
869
+ keys = self.args.ohead
870
+ if "*" in keys:
871
+ lines = response[1:]
872
+ else:
873
+ lines = []
874
+ for zs in response[1:]:
875
+ if zs.split(":")[0].lower() in keys:
876
+ lines.append(zs)
877
+ for zs in lines:
878
+ hk, hv = zs.split(": ")
879
+ self.log("[O] {}: \033[33m[{}]".format(hk, hv), 5)
880
+
859
881
  response.append("\r\n")
860
882
  try:
861
883
  self.s.sendall("\r\n".join(response).encode("utf-8"))
@@ -935,13 +957,14 @@ class HttpCli(object):
935
957
 
936
958
  lines = [
937
959
  "%s %s %s" % (self.http_ver or "HTTP/1.1", status, HTTPCODE[status]),
938
- "Connection: Close",
960
+ H_CONN_CLOSE,
939
961
  ]
940
962
 
941
963
  if body:
942
964
  lines.append("Content-Length: " + unicode(len(body)))
943
965
 
944
- self.s.sendall("\r\n".join(lines).encode("utf-8") + b"\r\n\r\n" + body)
966
+ lines.append("\r\n")
967
+ self.s.sendall("\r\n".join(lines).encode("utf-8") + body)
945
968
 
946
969
  def urlq(self, add , rm ) :
947
970
  """
@@ -1175,12 +1198,6 @@ class HttpCli(object):
1175
1198
  if "stack" in self.uparam:
1176
1199
  return self.tx_stack()
1177
1200
 
1178
- if "ups" in self.uparam:
1179
- return self.tx_ups()
1180
-
1181
- if "k304" in self.uparam:
1182
- return self.set_k304()
1183
-
1184
1201
  if "setck" in self.uparam:
1185
1202
  return self.setck()
1186
1203
 
@@ -1196,8 +1213,150 @@ class HttpCli(object):
1196
1213
  if "h" in self.uparam:
1197
1214
  return self.tx_mounts()
1198
1215
 
1216
+ if "ups" in self.uparam:
1217
+ # vpath is used for share translation
1218
+ return self.tx_ups()
1219
+
1220
+ if "rss" in self.uparam:
1221
+ return self.tx_rss()
1222
+
1199
1223
  return self.tx_browser()
1200
1224
 
1225
+ def tx_rss(self) :
1226
+ if self.do_log:
1227
+ self.log("RSS %s @%s" % (self.req, self.uname))
1228
+
1229
+ if not self.can_read:
1230
+ return self.tx_404()
1231
+
1232
+ vn = self.vn
1233
+ if not vn.flags.get("rss"):
1234
+ raise Pebkac(405, "RSS is disabled in server config")
1235
+
1236
+ rem = self.rem
1237
+ idx = self.conn.get_u2idx()
1238
+ if not idx or not hasattr(idx, "p_end"):
1239
+ if not HAVE_SQLITE3:
1240
+ raise Pebkac(500, "sqlite3 not found on server; rss is disabled")
1241
+ raise Pebkac(500, "server busy, cannot generate rss; please retry in a bit")
1242
+
1243
+ uv = [rem]
1244
+ if "recursive" in self.uparam:
1245
+ uq = "up.rd like ?||'%'"
1246
+ else:
1247
+ uq = "up.rd == ?"
1248
+
1249
+ zs = str(self.uparam.get("fext", self.args.rss_fext))
1250
+ if zs in ("True", "False"):
1251
+ zs = ""
1252
+ if zs:
1253
+ zsl = []
1254
+ for ext in zs.split(","):
1255
+ zsl.append("+up.fn like '%.'||?")
1256
+ uv.append(ext)
1257
+ uq += " and ( %s )" % (" or ".join(zsl),)
1258
+
1259
+ zs1 = self.uparam.get("sort", self.args.rss_sort)
1260
+ zs2 = zs1.lower()
1261
+ zs = RSS_SORT.get(zs2)
1262
+ if not zs:
1263
+ raise Pebkac(400, "invalid sort key; must be m/u/n/s")
1264
+
1265
+ uq += " order by up." + zs
1266
+ if zs1 == zs2:
1267
+ uq += " desc"
1268
+
1269
+ nmax = int(self.uparam.get("nf") or self.args.rss_nf)
1270
+
1271
+ hits = idx.run_query(self.uname, [self.vn], uq, uv, False, False, nmax)[0]
1272
+
1273
+ pw = self.ouparam.get("pw")
1274
+ if pw:
1275
+ q_pw = "?pw=%s" % (pw,)
1276
+ a_pw = "&pw=%s" % (pw,)
1277
+ for i in hits:
1278
+ i["rp"] += a_pw if "?" in i["rp"] else q_pw
1279
+ else:
1280
+ q_pw = a_pw = ""
1281
+
1282
+ title = self.uparam.get("title") or self.vpath.split("/")[-1]
1283
+ etitle = html_escape(title, True, True)
1284
+
1285
+ baseurl = "%s://%s%s" % (
1286
+ "https" if self.is_https else "http",
1287
+ self.host,
1288
+ self.args.SRS,
1289
+ )
1290
+ feed = "%s%s" % (baseurl, self.req[1:])
1291
+ efeed = html_escape(feed, True, True)
1292
+ edirlink = efeed.split("?")[0] + q_pw
1293
+
1294
+ ret = [
1295
+ """\
1296
+ <?xml version="1.0" encoding="UTF-8"?>
1297
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/">
1298
+ \t<channel>
1299
+ \t\t<atom:link href="%s" rel="self" type="application/rss+xml" />
1300
+ \t\t<title>%s</title>
1301
+ \t\t<description></description>
1302
+ \t\t<link>%s</link>
1303
+ \t\t<generator>copyparty-1</generator>
1304
+ """
1305
+ % (efeed, etitle, edirlink)
1306
+ ]
1307
+
1308
+ q = "select fn from cv where rd=? and dn=?"
1309
+ crd, cdn = rem.rsplit("/", 1) if "/" in rem else ("", rem)
1310
+ try:
1311
+ cfn = idx.cur[self.vn.realpath].execute(q, (crd, cdn)).fetchone()[0]
1312
+ bos.stat(os.path.join(vn.canonical(rem), cfn))
1313
+ cv_url = "%s%s?th=jf%s" % (baseurl, vjoin(self.vpath, cfn), a_pw)
1314
+ cv_url = html_escape(cv_url, True, True)
1315
+ zs = """\
1316
+ \t\t<image>
1317
+ \t\t\t<url>%s</url>
1318
+ \t\t\t<title>%s</title>
1319
+ \t\t\t<link>%s</link>
1320
+ \t\t</image>
1321
+ """
1322
+ ret.append(zs % (cv_url, etitle, edirlink))
1323
+ except:
1324
+ pass
1325
+
1326
+ for i in hits:
1327
+ iurl = html_escape("%s%s" % (baseurl, i["rp"]), True, True)
1328
+ title = unquotep(i["rp"].split("?")[0].split("/")[-1])
1329
+ title = html_escape(title, True, True)
1330
+ tag_t = str(i["tags"].get("title") or "")
1331
+ tag_a = str(i["tags"].get("artist") or "")
1332
+ desc = "%s - %s" % (tag_a, tag_t) if tag_t and tag_a else (tag_t or tag_a)
1333
+ desc = html_escape(desc, True, True) if desc else title
1334
+ mime = html_escape(guess_mime(title))
1335
+ lmod = formatdate(i["ts"])
1336
+ zsa = (iurl, iurl, title, desc, lmod, iurl, mime, i["sz"])
1337
+ zs = (
1338
+ """\
1339
+ \t\t<item>
1340
+ \t\t\t<guid>%s</guid>
1341
+ \t\t\t<link>%s</link>
1342
+ \t\t\t<title>%s</title>
1343
+ \t\t\t<description>%s</description>
1344
+ \t\t\t<pubDate>%s</pubDate>
1345
+ \t\t\t<enclosure url="%s" type="%s" length="%d"/>
1346
+ """
1347
+ % zsa
1348
+ )
1349
+ dur = i["tags"].get(".dur")
1350
+ if dur:
1351
+ zs += "\t\t\t<itunes:duration>%d</itunes:duration>\n" % (dur,)
1352
+ ret.append(zs + "\t\t</item>\n")
1353
+
1354
+ ret.append("\t</channel>\n</rss>\n")
1355
+ bret = "".join(ret).encode("utf-8", "replace")
1356
+ self.reply(bret, 200, "text/xml; charset=utf-8")
1357
+ self.log("rss: %d hits, %d bytes" % (len(hits), len(bret)))
1358
+ return True
1359
+
1201
1360
  def handle_propfind(self) :
1202
1361
  if self.do_log:
1203
1362
  self.log("PFIND %s @%s" % (self.req, self.uname))
@@ -2240,6 +2399,15 @@ class HttpCli(object):
2240
2399
  if "purl" in ret:
2241
2400
  ret["purl"] = self.args.SR + ret["purl"]
2242
2401
 
2402
+ if self.args.shr and self.vpath.startswith(self.args.shr1):
2403
+ # strip common suffix (uploader's folder structure)
2404
+ vp_req, vp_vfs = vroots(self.vpath, vjoin(dbv.vpath, vrem))
2405
+ if not ret["purl"].startswith(vp_vfs):
2406
+ t = "share-mapping failed; req=[%s] dbv=[%s] vrem=[%s] n1=[%s] n2=[%s] purl=[%s]"
2407
+ zt = (self.vpath, dbv.vpath, vrem, vp_req, vp_vfs, ret["purl"])
2408
+ raise Pebkac(500, t % zt)
2409
+ ret["purl"] = vp_req + ret["purl"][len(vp_vfs) :]
2410
+
2243
2411
  ret = json.dumps(ret)
2244
2412
  self.log(ret)
2245
2413
  self.reply(ret.encode("utf-8"), mime="application/json")
@@ -2332,7 +2500,11 @@ class HttpCli(object):
2332
2500
  chashes.append(siblings[n : n + clen])
2333
2501
 
2334
2502
  vfs, _ = self.asrv.vfs.get(self.vpath, self.uname, False, True)
2335
- ptop = (vfs.dbv or vfs).realpath
2503
+ ptop = vfs.get_dbv("")[0].realpath
2504
+ # if this is a share, then get_dbv has been overridden to return
2505
+ # the dbv (which does not exist as a property). And its realpath
2506
+ # could point into the middle of its origin vfs node, meaning it
2507
+ # is not necessarily registered with up2k, so get_dbv is crucial
2336
2508
 
2337
2509
  broker = self.conn.hsrv.broker
2338
2510
  x = broker.ask("up2k.handle_chunks", ptop, wark, chashes)
@@ -2481,6 +2653,7 @@ class HttpCli(object):
2481
2653
  except:
2482
2654
  # maybe busted handle (eg. disk went full)
2483
2655
  f.close()
2656
+ chashes = [] # exception flag
2484
2657
  raise
2485
2658
  finally:
2486
2659
  if locked:
@@ -2489,9 +2662,11 @@ class HttpCli(object):
2489
2662
  num_left, t = x.get()
2490
2663
  if num_left < 0:
2491
2664
  self.loud_reply(t, status=500)
2492
- return False
2493
- t = "got %d more chunks, %d left"
2494
- self.log(t % (len(written), num_left), 6)
2665
+ if chashes: # kills exception bubbling otherwise
2666
+ return False
2667
+ else:
2668
+ t = "got %d more chunks, %d left"
2669
+ self.log(t % (len(written), num_left), 6)
2495
2670
 
2496
2671
  if num_left < 0:
2497
2672
  raise Pebkac(500, "unconfirmed; see serverlog")
@@ -3235,25 +3410,29 @@ class HttpCli(object):
3235
3410
  self.reply(response.encode("utf-8"))
3236
3411
  return True
3237
3412
 
3238
- def _chk_lastmod(self, file_ts ) :
3413
+ def _chk_lastmod(self, file_ts ) :
3414
+ # ret: lastmod, do_send, can_range
3239
3415
  file_lastmod = formatdate(file_ts)
3240
- cli_lastmod = self.headers.get("if-modified-since")
3241
- if cli_lastmod:
3242
- try:
3243
- # some browser append "; length=573"
3244
- cli_lastmod = cli_lastmod.split(";")[0].strip()
3245
- cli_dt = parsedate(cli_lastmod)
3246
- cli_ts = calendar.timegm(cli_dt)
3247
- return file_lastmod, int(file_ts) > int(cli_ts)
3248
- except Exception as ex:
3249
- self.log(
3250
- "lastmod {}\nremote: [{}]\n local: [{}]".format(
3251
- repr(ex), cli_lastmod, file_lastmod
3252
- )
3253
- )
3254
- return file_lastmod, file_lastmod != cli_lastmod
3416
+ c_ifrange = self.headers.get("if-range")
3417
+ c_lastmod = self.headers.get("if-modified-since")
3418
+
3419
+ if not c_ifrange and not c_lastmod:
3420
+ return file_lastmod, True, True
3421
+
3422
+ if c_ifrange and c_ifrange != file_lastmod:
3423
+ t = "sending entire file due to If-Range; cli(%s) file(%s)"
3424
+ self.log(t % (c_ifrange, file_lastmod), 6)
3425
+ return file_lastmod, True, False
3426
+
3427
+ do_send = c_lastmod != file_lastmod
3428
+ if do_send and c_lastmod:
3429
+ t = "sending body due to If-Modified-Since cli(%s) file(%s)"
3430
+ self.log(t % (c_lastmod, file_lastmod), 6)
3431
+ elif not do_send and self.no304():
3432
+ do_send = True
3433
+ self.log("sending body due to no304")
3255
3434
 
3256
- return file_lastmod, True
3435
+ return file_lastmod, do_send, True
3257
3436
 
3258
3437
  def _use_dirkey(self, vn , ap ) :
3259
3438
  if self.can_read or not self.can_get:
@@ -3403,7 +3582,7 @@ class HttpCli(object):
3403
3582
  # if-modified
3404
3583
 
3405
3584
  if file_ts > 0:
3406
- file_lastmod, do_send = self._chk_lastmod(int(file_ts))
3585
+ file_lastmod, do_send, _ = self._chk_lastmod(int(file_ts))
3407
3586
  self.out_headers["Last-Modified"] = file_lastmod
3408
3587
  if not do_send:
3409
3588
  status = 304
@@ -3565,7 +3744,7 @@ class HttpCli(object):
3565
3744
  #
3566
3745
  # if-modified
3567
3746
 
3568
- file_lastmod, do_send = self._chk_lastmod(int(file_ts))
3747
+ file_lastmod, do_send, can_range = self._chk_lastmod(int(file_ts))
3569
3748
  self.out_headers["Last-Modified"] = file_lastmod
3570
3749
  if not do_send:
3571
3750
  status = 304
@@ -3609,7 +3788,14 @@ class HttpCli(object):
3609
3788
 
3610
3789
  # let's not support 206 with compression
3611
3790
  # and multirange / multipart is also not-impl (mostly because calculating contentlength is a pain)
3612
- if do_send and not is_compressed and hrange and file_sz and "," not in hrange:
3791
+ if (
3792
+ do_send
3793
+ and not is_compressed
3794
+ and hrange
3795
+ and can_range
3796
+ and file_sz
3797
+ and "," not in hrange
3798
+ ):
3613
3799
  try:
3614
3800
  if not hrange.lower().startswith("bytes"):
3615
3801
  raise Exception()
@@ -4080,7 +4266,7 @@ class HttpCli(object):
4080
4266
  sz_md = len(lead) + len(fullfile)
4081
4267
 
4082
4268
  file_ts = int(max(ts_md, self.E.t0))
4083
- file_lastmod, do_send = self._chk_lastmod(file_ts)
4269
+ file_lastmod, do_send, _ = self._chk_lastmod(file_ts)
4084
4270
  self.out_headers["Last-Modified"] = file_lastmod
4085
4271
  self.out_headers.update(NO_CACHE)
4086
4272
  status = 200 if do_send else 304
@@ -4266,7 +4452,7 @@ class HttpCli(object):
4266
4452
  rvol=rvol,
4267
4453
  wvol=wvol,
4268
4454
  avol=avol,
4269
- in_shr=self.args.shr and self.vpath.startswith(self.args.shr[1:]),
4455
+ in_shr=self.args.shr and self.vpath.startswith(self.args.shr1),
4270
4456
  vstate=vstate,
4271
4457
  ups=ups,
4272
4458
  scanning=vs["scanning"],
@@ -4276,7 +4462,9 @@ class HttpCli(object):
4276
4462
  dbwt=vs["dbwt"],
4277
4463
  url_suf=suf,
4278
4464
  k304=self.k304(),
4465
+ no304=self.no304(),
4279
4466
  k304vis=self.args.k304 > 0,
4467
+ no304vis=self.args.no304 > 0,
4280
4468
  ver=S_VERSION if self.args.ver else "",
4281
4469
  chpw=self.args.chpw and self.uname != "*",
4282
4470
  ahttps="" if self.is_https else "https://" + self.host + self.req,
@@ -4284,29 +4472,21 @@ class HttpCli(object):
4284
4472
  self.reply(html.encode("utf-8"))
4285
4473
  return True
4286
4474
 
4287
- def set_k304(self) :
4288
- v = self.uparam["k304"].lower()
4289
- if v in "yn":
4290
- dur = 86400 * 299
4291
- else:
4292
- dur = 0
4293
- v = "x"
4294
-
4295
- ck = gencookie("k304", v, self.args.R, False, dur)
4296
- self.out_headerlist.append(("Set-Cookie", ck))
4297
- self.redirect("", "?h#cc")
4298
- return True
4299
-
4300
4475
  def setck(self) :
4301
4476
  k, v = self.uparam["setck"].split("=", 1)
4302
- t = 0 if v == "" else 86400 * 299
4477
+ t = 0 if v in ("", "x") else 86400 * 299
4303
4478
  ck = gencookie(k, v, self.args.R, False, t)
4304
4479
  self.out_headerlist.append(("Set-Cookie", ck))
4305
- self.reply(b"o7\n")
4480
+ if "cc" in self.ouparam:
4481
+ self.redirect("", "?h#cc")
4482
+ else:
4483
+ self.reply(b"o7\n")
4306
4484
  return True
4307
4485
 
4308
4486
  def set_cfg_reset(self) :
4309
- for k in ("k304", "js", "idxh", "dots", "cppwd", "cppws"):
4487
+ for k in ALL_COOKIES:
4488
+ if k not in self.cookies:
4489
+ continue
4310
4490
  cookie = gencookie(k, "x", self.args.R, False)
4311
4491
  self.out_headerlist.append(("Set-Cookie", cookie))
4312
4492
 
@@ -4336,7 +4516,7 @@ class HttpCli(object):
4336
4516
 
4337
4517
  t = t.format(self.args.SR)
4338
4518
  qv = quotep(self.vpaths) + self.ourlq()
4339
- in_shr = self.args.shr and self.vpath.startswith(self.args.shr[1:])
4519
+ in_shr = self.args.shr and self.vpath.startswith(self.args.shr1)
4340
4520
  html = self.j2s("splash", this=self, qvpath=qv, in_shr=in_shr, msg=t)
4341
4521
  self.reply(html.encode("utf-8"), status=rc)
4342
4522
  return True
@@ -4499,6 +4679,11 @@ class HttpCli(object):
4499
4679
  lm = "ups [{}]".format(filt)
4500
4680
  self.log(lm)
4501
4681
 
4682
+ if self.args.shr and self.vpath.startswith(self.args.shr1):
4683
+ shr_dbv, shr_vrem = self.vn.get_dbv(self.rem)
4684
+ else:
4685
+ shr_dbv = None
4686
+
4502
4687
  ret = []
4503
4688
  t0 = time.time()
4504
4689
  lim = time.time() - self.args.unpost
@@ -4519,7 +4704,12 @@ class HttpCli(object):
4519
4704
  else:
4520
4705
  allvols = list(self.asrv.vfs.all_vols.values())
4521
4706
 
4522
- allvols = [x for x in allvols if "e2d" in x.flags]
4707
+ allvols = [
4708
+ x
4709
+ for x in allvols
4710
+ if "e2d" in x.flags
4711
+ and ("*" in x.axs.uwrite or self.uname in x.axs.uwrite or x == shr_dbv)
4712
+ ]
4523
4713
 
4524
4714
  for vol in allvols:
4525
4715
  cur = idx.get_cur(vol)
@@ -4569,6 +4759,15 @@ class HttpCli(object):
4569
4759
 
4570
4760
  ret = ret[:2000]
4571
4761
 
4762
+ if shr_dbv:
4763
+ # translate vpaths from share-target to share-url
4764
+ # to satisfy access checks
4765
+ vp_shr, vp_vfs = vroots(self.vpath, vjoin(shr_dbv.vpath, shr_vrem))
4766
+ for v in ret:
4767
+ vp = v["vp"]
4768
+ if vp.startswith(vp_vfs):
4769
+ v["vp"] = vp_shr + vp[len(vp_vfs) :]
4770
+
4572
4771
  if self.is_vproxied:
4573
4772
  for v in ret:
4574
4773
  v["vp"] = self.args.SR + v["vp"]
@@ -4698,7 +4897,7 @@ class HttpCli(object):
4698
4897
  if m:
4699
4898
  raise Pebkac(400, "sharekey has illegal character [%s]" % (m[1],))
4700
4899
 
4701
- if vp.startswith(self.args.shr[1:]):
4900
+ if vp.startswith(self.args.shr1):
4702
4901
  raise Pebkac(400, "yo dawg...")
4703
4902
 
4704
4903
  cur = idx.get_shr()
@@ -5082,7 +5281,7 @@ class HttpCli(object):
5082
5281
  self.log("#wow #whoa")
5083
5282
 
5084
5283
  if not self.args.nid:
5085
- free, total = get_df(abspath)
5284
+ free, total, _ = get_df(abspath, False)
5086
5285
  if total is not None:
5087
5286
  h1 = humansize(free or 0)
5088
5287
  h2 = humansize(total)
copyparty/metrics.py CHANGED
@@ -128,7 +128,7 @@ class Metrics(object):
128
128
  addbh("cpp_disk_size_bytes", "total HDD size of volume")
129
129
  addbh("cpp_disk_free_bytes", "free HDD space in volume")
130
130
  for vpath, vol in allvols:
131
- free, total = get_df(vol.realpath)
131
+ free, total, _ = get_df(vol.realpath, False)
132
132
  if free is None or total is None:
133
133
  continue
134
134
 
copyparty/svchub.py CHANGED
@@ -217,13 +217,14 @@ class SvcHub(object):
217
217
  args.chpw_no = noch
218
218
 
219
219
  if args.ipu:
220
- iu, nm = load_ipu(self.log, args.ipu)
220
+ iu, nm = load_ipu(self.log, args.ipu, True)
221
221
  setattr(args, "ipu_iu", iu)
222
222
  setattr(args, "ipu_nm", nm)
223
223
 
224
224
  if not self.args.no_ses:
225
225
  self.setup_session_db()
226
226
 
227
+ args.shr1 = ""
227
228
  if args.shr:
228
229
  self.setup_share_db()
229
230
 
@@ -372,6 +373,14 @@ class SvcHub(object):
372
373
 
373
374
  self.broker = Broker(self)
374
375
 
376
+ # create netmaps early to avoid firewall gaps,
377
+ # but the mutex blocks multiprocessing startup
378
+ for zs in "ipu_iu ftp_ipa_nm tftp_ipa_nm".split():
379
+ try:
380
+ getattr(args, zs).mutex = threading.Lock()
381
+ except:
382
+ pass
383
+
375
384
  def setup_session_db(self) :
376
385
  if not HAVE_SQLITE3:
377
386
  self.args.no_ses = True
@@ -444,6 +453,7 @@ class SvcHub(object):
444
453
  raise Exception(t)
445
454
 
446
455
  al.shr = "/%s/" % (al.shr,)
456
+ al.shr1 = al.shr[1:]
447
457
 
448
458
  create = True
449
459
  modified = False
@@ -751,8 +761,8 @@ class SvcHub(object):
751
761
  al.idp_h_grp = al.idp_h_grp.lower()
752
762
  al.idp_h_key = al.idp_h_key.lower()
753
763
 
754
- al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa)
755
- al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa)
764
+ al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa, True)
765
+ al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa, True)
756
766
 
757
767
  mte = ODict.fromkeys(DEF_MTE.split(","), True)
758
768
  al.mte = odfusion(mte, al.mte)
@@ -799,6 +809,24 @@ class SvcHub(object):
799
809
  if len(al.tcolor) == 3: # fc5 => ffcc55
800
810
  al.tcolor = "".join([x * 2 for x in al.tcolor])
801
811
 
812
+ zs = al.u2sz
813
+ zsl = zs.split(",")
814
+ if len(zsl) not in (1, 3):
815
+ t = "invalid --u2sz; must be either one number, or a comma-separated list of three numbers (min,default,max)"
816
+ raise Exception(t)
817
+ if len(zsl) < 3:
818
+ zsl = ["1", zs, zs]
819
+ zi2 = 1
820
+ for zs in zsl:
821
+ zi = int(zs)
822
+ # arbitrary constraint (anything above 2 GiB is probably unintended)
823
+ if zi < 1 or zi > 2047:
824
+ raise Exception("invalid --u2sz; minimum is 1, max is 2047")
825
+ if zi < zi2:
826
+ raise Exception("invalid --u2sz; values must be equal or ascending")
827
+ zi2 = zi
828
+ al.u2sz = ",".join(zsl)
829
+
802
830
  return True
803
831
 
804
832
  def _ipa2re(self, txt) :
copyparty/u2idx.py CHANGED
@@ -91,7 +91,7 @@ class U2idx(object):
91
91
  uv = [wark[:16], wark]
92
92
 
93
93
  try:
94
- return self.run_query(uname, vols, uq, uv, False, 99999)[0]
94
+ return self.run_query(uname, vols, uq, uv, False, True, 99999)[0]
95
95
  except:
96
96
  raise Pebkac(500, min_ex())
97
97
 
@@ -295,7 +295,7 @@ class U2idx(object):
295
295
  q += " lower({}) {} ? ) ".format(field, oper)
296
296
 
297
297
  try:
298
- return self.run_query(uname, vols, q, va, have_mt, lim)
298
+ return self.run_query(uname, vols, q, va, have_mt, True, lim)
299
299
  except Exception as ex:
300
300
  raise Pebkac(500, repr(ex))
301
301
 
@@ -306,6 +306,7 @@ class U2idx(object):
306
306
  uq ,
307
307
  uv ,
308
308
  have_mt ,
309
+ sort ,
309
310
  lim ,
310
311
  ) :
311
312
  if self.args.srch_dbg:
@@ -452,7 +453,8 @@ class U2idx(object):
452
453
  done_flag.append(True)
453
454
  self.active_id = ""
454
455
 
455
- ret.sort(key=itemgetter("rp"))
456
+ if sort:
457
+ ret.sort(key=itemgetter("rp"))
456
458
 
457
459
  return ret, list(taglist.keys()), lim < 0 and not clamped
458
460
 
copyparty/up2k.py CHANGED
@@ -3481,6 +3481,7 @@ class Up2k(object):
3481
3481
  for chash in written:
3482
3482
  job["need"].remove(chash)
3483
3483
  except Exception as ex:
3484
+ # dead tcp connections can get here by timeout (OK)
3484
3485
  return -2, "confirm_chunk, chash(%s) %r" % (chash, ex) # type: ignore
3485
3486
 
3486
3487
  ret = len(job["need"])
@@ -3885,11 +3886,9 @@ class Up2k(object):
3885
3886
  if unpost:
3886
3887
  raise Pebkac(400, "cannot unpost folders")
3887
3888
  elif stat.S_ISLNK(st.st_mode) or stat.S_ISREG(st.st_mode):
3888
- dbv, vrem = self.asrv.vfs.get(vpath, uname, *permsets[0])
3889
- dbv, vrem = dbv.get_dbv(vrem)
3890
- voldir = vsplit(vrem)[0]
3889
+ voldir = vsplit(rem)[0]
3891
3890
  vpath_dir = vsplit(vpath)[0]
3892
- g = [(dbv, voldir, vpath_dir, adir, [(fn, 0)], [], {})] # type: ignore
3891
+ g = [(vn, voldir, vpath_dir, adir, [(fn, 0)], [], {})] # type: ignore
3893
3892
  else:
3894
3893
  self.log("rm: skip type-{:x} file [{}]".format(st.st_mode, atop))
3895
3894
  return 0, [], []
@@ -3916,7 +3915,10 @@ class Up2k(object):
3916
3915
  volpath = ("%s/%s" % (vrem, fn)).strip("/")
3917
3916
  vpath = ("%s/%s" % (dbv.vpath, volpath)).strip("/")
3918
3917
  self.log("rm %s\n %s" % (vpath, abspath))
3919
- _ = dbv.get(volpath, uname, *permsets[0])
3918
+ if not unpost:
3919
+ # recursion-only sanchk
3920
+ _ = dbv.get(volpath, uname, *permsets[0])
3921
+
3920
3922
  if xbd:
3921
3923
  if not runhook(
3922
3924
  self.log,
copyparty/util.py CHANGED
@@ -192,6 +192,9 @@ except:
192
192
  ansi_re = re.compile("\033\\[[^mK]*[mK]")
193
193
 
194
194
 
195
+ BOS_SEP = ("%s" % (os.sep,)).encode("ascii")
196
+
197
+
195
198
  surrogateescape.register_surrogateescape()
196
199
  if WINDOWS and PY2:
197
200
  FS_ENCODING = "utf-8"
@@ -644,13 +647,20 @@ class HLog(logging.Handler):
644
647
 
645
648
  class NetMap(object):
646
649
  def __init__(
647
- self, ips , cidrs , keep_lo=False, strict_cidr=False
650
+ self,
651
+ ips ,
652
+ cidrs ,
653
+ keep_lo=False,
654
+ strict_cidr=False,
655
+ defer_mutex=False,
648
656
  ) :
649
657
  """
650
658
  ips: list of plain ipv4/ipv6 IPs, not cidr
651
659
  cidrs: list of cidr-notation IPs (ip/prefix)
652
660
  """
653
- self.mutex = threading.Lock()
661
+
662
+ # fails multiprocessing; defer assignment
663
+ self.mutex = None if defer_mutex else threading.Lock()
654
664
 
655
665
  if "::" in ips:
656
666
  ips = [x for x in ips if x != "::"] + list(
@@ -689,6 +699,8 @@ class NetMap(object):
689
699
  try:
690
700
  return self.cache[ip]
691
701
  except:
702
+ # intentionally crash the calling thread if unset:
703
+
692
704
  with self.mutex:
693
705
  return self._map(ip)
694
706
 
@@ -2106,6 +2118,23 @@ def unquotep(txt ) :
2106
2118
  return w8dec(unq2)
2107
2119
 
2108
2120
 
2121
+ def vroots(vp1 , vp2 ) :
2122
+ """
2123
+ input("q/w/e/r","a/s/d/e/r") output("/q/w/","/a/s/d/")
2124
+ """
2125
+ while vp1 and vp2:
2126
+ zt1 = vp1.rsplit("/", 1) if "/" in vp1 else ("", vp1)
2127
+ zt2 = vp2.rsplit("/", 1) if "/" in vp2 else ("", vp2)
2128
+ if zt1[1] != zt2[1]:
2129
+ break
2130
+ vp1 = zt1[0]
2131
+ vp2 = zt2[0]
2132
+ return (
2133
+ "/%s/" % (vp1,) if vp1 else "/",
2134
+ "/%s/" % (vp2,) if vp2 else "/",
2135
+ )
2136
+
2137
+
2109
2138
  def vsplit(vpath ) :
2110
2139
  if "/" not in vpath:
2111
2140
  return "", vpath
@@ -2407,22 +2436,27 @@ def wunlink(log , abspath , flags ) :
2407
2436
  return _fs_mvrm(log, abspath, "", False, flags)
2408
2437
 
2409
2438
 
2410
- def get_df(abspath ) :
2439
+ def get_df(abspath , prune ) :
2411
2440
  try:
2412
- # some fuses misbehave
2441
+ ap = fsenc(abspath)
2442
+ while prune and not os.path.isdir(ap) and BOS_SEP in ap:
2443
+ # strip leafs until it hits an existing folder
2444
+ ap = ap.rsplit(BOS_SEP, 1)[0]
2445
+
2413
2446
  if ANYWIN:
2447
+ abspath = fsdec(ap)
2414
2448
  bfree = ctypes.c_ulonglong(0)
2415
2449
  ctypes.windll.kernel32.GetDiskFreeSpaceExW( # type: ignore
2416
2450
  ctypes.c_wchar_p(abspath), None, None, ctypes.pointer(bfree)
2417
2451
  )
2418
- return (bfree.value, None)
2452
+ return (bfree.value, None, "")
2419
2453
  else:
2420
- sv = os.statvfs(fsenc(abspath))
2454
+ sv = os.statvfs(ap)
2421
2455
  free = sv.f_frsize * sv.f_bfree
2422
2456
  total = sv.f_frsize * sv.f_blocks
2423
- return (free, total)
2424
- except:
2425
- return (None, None)
2457
+ return (free, total, "")
2458
+ except Exception as ex:
2459
+ return (None, None, repr(ex))
2426
2460
 
2427
2461
 
2428
2462
  if not ANYWIN and not MACOS:
@@ -2560,7 +2594,7 @@ def list_ips() :
2560
2594
  return list(ret)
2561
2595
 
2562
2596
 
2563
- def build_netmap(csv ):
2597
+ def build_netmap(csv , defer_mutex = False):
2564
2598
  csv = csv.lower().strip()
2565
2599
 
2566
2600
  if csv in ("any", "all", "no", ",", ""):
@@ -2595,10 +2629,12 @@ def build_netmap(csv ):
2595
2629
  cidrs.append(zs)
2596
2630
 
2597
2631
  ips = [x.split("/")[0] for x in cidrs]
2598
- return NetMap(ips, cidrs, True)
2632
+ return NetMap(ips, cidrs, True, False, defer_mutex)
2599
2633
 
2600
2634
 
2601
- def load_ipu(log , ipus ) :
2635
+ def load_ipu(
2636
+ log , ipus , defer_mutex = False
2637
+ ) :
2602
2638
  ip_u = {"": "*"}
2603
2639
  cidr_u = {}
2604
2640
  for ipu in ipus:
@@ -2615,7 +2651,7 @@ def load_ipu(log , ipus ) :
2615
2651
  cidr_u[cidr] = uname
2616
2652
  ip_u[cip] = uname
2617
2653
  try:
2618
- nm = NetMap(["::"], list(cidr_u.keys()), True, True)
2654
+ nm = NetMap(["::"], list(cidr_u.keys()), True, True, defer_mutex)
2619
2655
  except Exception as ex:
2620
2656
  t = "failed to translate --ipu into netmap, probably due to invalid config: %r"
2621
2657
  log("root", t % (ex,), 1)
@@ -844,7 +844,7 @@ def main():
844
844
 
845
845
  # dircache is always a boost,
846
846
  # only want to disable it for tests etc,
847
- cdn = 9 # max num dirs; 0=disable
847
+ cdn = 24 # max num dirs; keep larger than max dir depth; 0=disable
848
848
  cds = 1 # numsec until an entry goes stale
849
849
 
850
850
  where = "local directory"
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.4"
5
- S_BUILD_DT = "2024-10-16"
4
+ S_VERSION = "2.5"
5
+ S_BUILD_DT = "2024-10-18"
6
6
 
7
7
  """
8
8
  u2c.py: upload to copyparty
@@ -154,6 +154,7 @@ class HCli(object):
154
154
  self.tls = tls
155
155
  self.verify = ar.te or not ar.td
156
156
  self.conns = []
157
+ self.hconns = []
157
158
  if tls:
158
159
  import ssl
159
160
 
@@ -173,7 +174,7 @@ class HCli(object):
173
174
  "User-Agent": "u2c/%s" % (S_VERSION,),
174
175
  }
175
176
 
176
- def _connect(self):
177
+ def _connect(self, timeout):
177
178
  args = {}
178
179
  if PY37:
179
180
  args["blocksize"] = 1048576
@@ -185,7 +186,7 @@ class HCli(object):
185
186
  if self.ctx:
186
187
  args = {"context": self.ctx}
187
188
 
188
- return C(self.addr, self.port, timeout=999, **args)
189
+ return C(self.addr, self.port, timeout=timeout, **args)
189
190
 
190
191
  def req(self, meth, vpath, hdrs, body=None, ctype=None):
191
192
  hdrs.update(self.base_hdrs)
@@ -198,7 +199,9 @@ class HCli(object):
198
199
  0 if not body else body.len if hasattr(body, "len") else len(body)
199
200
  )
200
201
 
201
- c = self.conns.pop() if self.conns else self._connect()
202
+ # large timeout for handshakes (safededup)
203
+ conns = self.hconns if ctype == MJ else self.conns
204
+ c = conns.pop() if conns else self._connect(999 if ctype == MJ else 128)
202
205
  try:
203
206
  c.request(meth, vpath, body, hdrs)
204
207
  if PY27:
@@ -207,7 +210,7 @@ class HCli(object):
207
210
  rsp = c.getresponse()
208
211
 
209
212
  data = rsp.read()
210
- self.conns.append(c)
213
+ conns.append(c)
211
214
  return rsp.status, data.decode("utf-8")
212
215
  except:
213
216
  c.close()
@@ -868,9 +871,10 @@ def upload(fsl, stats, maxsz):
868
871
  if sc >= 400:
869
872
  raise Exception("http %s: %s" % (sc, txt))
870
873
  finally:
871
- fsl.f.close()
872
- if nsub != -1:
873
- fsl.unsub()
874
+ if fsl.f:
875
+ fsl.f.close()
876
+ if nsub != -1:
877
+ fsl.unsub()
874
878
 
875
879
 
876
880
  class Ctl(object):
Binary file
copyparty/web/splash.html CHANGED
@@ -129,11 +129,20 @@
129
129
 
130
130
  {% if k304 or k304vis %}
131
131
  {% if k304 %}
132
- <li><a id="h" href="{{ r }}/?k304=n">disable k304</a> (currently enabled)
132
+ <li><a id="h" href="{{ r }}/?cc&setck=k304=n">disable k304</a> (currently enabled)
133
133
  {%- else %}
134
- <li><a id="i" href="{{ r }}/?k304=y" class="r">enable k304</a> (currently disabled)
134
+ <li><a id="i" href="{{ r }}/?cc&setck=k304=y" class="r">enable k304</a> (currently disabled)
135
135
  {% endif %}
136
- <blockquote id="j">enabling this will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general</blockquote></li>
136
+ <blockquote id="j">enabling k304 will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general</blockquote></li>
137
+ {% endif %}
138
+
139
+ {% if no304 or no304vis %}
140
+ {% if no304 %}
141
+ <li><a id="ab" href="{{ r }}/?cc&setck=no304=n">disable no304</a> (currently enabled)
142
+ {%- else %}
143
+ <li><a id="ac" href="{{ r }}/?cc&setck=no304=y" class="r">enable no304</a> (currently disabled)
144
+ {% endif %}
145
+ <blockquote id="ad">enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!</blockquote></li>
137
146
  {% endif %}
138
147
 
139
148
  <li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li>
Binary file
copyparty/web/up2k.js.gz CHANGED
Binary file
Binary file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: copyparty
3
- Version: 1.15.8
3
+ Version: 1.15.10
4
4
  Summary: Portable file server with accelerated resumable uploads, deduplication, WebDAV, FTP, zeroconf, media indexer, video thumbnails, audio transcoding, and write-only folders
5
5
  Author-email: ed <copyparty@ocv.me>
6
6
  License: MIT
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.9
21
21
  Classifier: Programming Language :: Python :: 3.10
22
22
  Classifier: Programming Language :: Python :: 3.11
23
23
  Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
24
25
  Classifier: Programming Language :: Python :: Implementation :: CPython
25
26
  Classifier: Programming Language :: Python :: Implementation :: Jython
26
27
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -101,6 +102,7 @@ turn almost any device into a file server with resumable uploads/downloads using
101
102
  * [file manager](#file-manager) - cut/paste, rename, and delete files/folders (if you have permission)
102
103
  * [shares](#shares) - share a file or folder by creating a temporary link
103
104
  * [batch rename](#batch-rename) - select some files and press `F2` to bring up the rename UI
105
+ * [rss feeds](#rss-feeds) - monitor a folder with your RSS reader
104
106
  * [media player](#media-player) - plays almost every audio format there is
105
107
  * [audio equalizer](#audio-equalizer) - and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression)
106
108
  * [fix unreliable playback on android](#fix-unreliable-playback-on-android) - due to phone / app settings
@@ -899,6 +901,30 @@ or a mix of both:
899
901
  the metadata keys you can use in the format field are the ones in the file-browser table header (whatever is collected with `-mte` and `-mtp`)
900
902
 
901
903
 
904
+ ## rss feeds
905
+
906
+ monitor a folder with your RSS reader , optionally recursive
907
+
908
+ must be enabled per-volume with volflag `rss` or globally with `--rss`
909
+
910
+ the feed includes itunes metadata for use with podcast readers such as [AntennaPod](https://antennapod.org/)
911
+
912
+ a feed example: https://cd.ocv.me/a/d2/d22/?rss&fext=mp3
913
+
914
+ url parameters:
915
+
916
+ * `pw=hunter2` for password auth
917
+ * `recursive` to also include subfolders
918
+ * `title=foo` changes the feed title (default: folder name)
919
+ * `fext=mp3,opus` only include mp3 and opus files (default: all)
920
+ * `nf=30` only show the first 30 results (default: 250)
921
+ * `sort=m` sort by mtime (file last-modified), newest first (default)
922
+ * `u` = upload-time; NOTE: non-uploaded files have upload-time `0`
923
+ * `n` = filename
924
+ * `a` = filesize
925
+ * uppercase = reverse-sort; `M` = oldest file first
926
+
927
+
902
928
  ## media player
903
929
 
904
930
  plays almost every audio format there is (if the server has FFmpeg installed for on-demand transcoding)
@@ -1962,6 +1988,9 @@ quick summary of more eccentric web-browsers trying to view a directory index:
1962
1988
  | **ie4** and **netscape** 4.0 | can browse, upload with `?b=u`, auth with `&pw=wark` |
1963
1989
  | **ncsa mosaic** 2.7 | does not get a pass, [pic1](https://user-images.githubusercontent.com/241032/174189227-ae816026-cf6f-4be5-a26e-1b3b072c1b2f.png) - [pic2](https://user-images.githubusercontent.com/241032/174189225-5651c059-5152-46e9-ac26-7e98e497901b.png) |
1964
1990
  | **SerenityOS** (7e98457) | hits a page fault, works with `?b=u`, file upload not-impl |
1991
+ | **nintendo 3ds** | can browse, upload, view thumbnails (thx bnjmn) |
1992
+
1993
+ <p align="center"><img src="https://github.com/user-attachments/assets/88deab3d-6cad-4017-8841-2f041472b853" /></p>
1965
1994
 
1966
1995
 
1967
1996
  # client examples
@@ -1,22 +1,22 @@
1
1
  copyparty/__init__.py,sha256=Chqw7uXX4r_-a2p6-xthrrqVHFI4aZdW45sWU7UvqeE,2597
2
- copyparty/__main__.py,sha256=oTzWyuZotTwi-dHpD41mVp_EW2mPSWDMKP8o_nxp9Yk,110180
3
- copyparty/__version__.py,sha256=V6jLFRwMXDp4buHI89fYcQoINvqpP0XYLPVMn_QHEDg,258
4
- copyparty/authsrv.py,sha256=-lImrFH6pm3gcI76vZiFfEgrQx3_STYTSS2soYX5y1Y,98711
2
+ copyparty/__main__.py,sha256=S9jBx-HMqtUusAjpA64akmN4SKeRMGS5dNfzS4-miC4,111480
3
+ copyparty/__version__.py,sha256=fkcFvGlCD_NUIpsBFycKnSiV9ebZj5bDCWyy5H38XJw,259
4
+ copyparty/authsrv.py,sha256=FNb0l3o57R9xnYrZsqjTOqPwWkXGgP9YdX7NWx0ZMow,98955
5
5
  copyparty/broker_mp.py,sha256=jsHUM2BSfRVRyZT869iPCqYEHSqedk6VkwvygZwbEZE,4017
6
6
  copyparty/broker_mpw.py,sha256=PYFgQfssOCfdI6qayW1ZjO1j1-7oez094muhYMbPOz0,3339
7
7
  copyparty/broker_thr.py,sha256=MXrwjusP0z1LPURUhi5jx_TL3jrXhYcDrJPDSKu6EEU,1705
8
8
  copyparty/broker_util.py,sha256=76mfnFOpX1gUUvtjm8UQI7jpTIaVINX10QonM-B7ggc,1680
9
9
  copyparty/cert.py,sha256=0ZAPeXeMR164vWn9GQU3JDKooYXEq_NOQkDeg543ivg,8009
10
- copyparty/cfg.py,sha256=33nLatBUmzRFKQ4KpoQei3ZY6EqRrlaHpQnvCNFXcHI,10112
10
+ copyparty/cfg.py,sha256=E9iBGNjIUrDAPLFRgKsVOmAknP9bDE27xh0gkmNdH1s,10127
11
11
  copyparty/dxml.py,sha256=lZpg-kn-kQsXRtNY1n6fRaS-b7uXzMCyv8ovKnhZcZc,1548
12
12
  copyparty/fsutil.py,sha256=5CshJWO7CflfaRRNOb3JxghUH7W5rmS_HWNmKfx42MM,4538
13
13
  copyparty/ftpd.py,sha256=G_h1urfIikzfCWGXnW9p-rioWdNM_Je6vWYq0-QSbC8,17580
14
- copyparty/httpcli.py,sha256=xhM8unCDyo7Gj3hmLdhncXUCgKHgb6a300bolO1WF4U,194152
14
+ copyparty/httpcli.py,sha256=rb-QxygFrRM7LEmTadMKn4FZ2bl5ddoPfJXsRrItKDg,201389
15
15
  copyparty/httpconn.py,sha256=mQSgljh0Q-jyWjF4tQLrHbRKRe9WKl19kGqsGMsJpWo,6880
16
16
  copyparty/httpsrv.py,sha256=d_UiGnQKniBoEV68lNFgnYm-byda7uj56mFf-YC7piI,17223
17
17
  copyparty/ico.py,sha256=eWSxEae4wOCfheHl-m-wchYvFRAR_97kJDb4NGaB-Z8,3561
18
18
  copyparty/mdns.py,sha256=vC078llnL1v0pvL3mnwacuStFHPJUQuxo9Opj-IbHL4,18155
19
- copyparty/metrics.py,sha256=aV09nntEmKMIyde8xoPtj1ehDOQVQOHchRF4uMMNzqM,8855
19
+ copyparty/metrics.py,sha256=-1Rkk44gBh_1YJbdzGZHaqR4pEwkbno6fSdsRb5wDIk,8865
20
20
  copyparty/mtag.py,sha256=8WGjEn0T0Ri9ww1yBpLUnFHZiTQMye1BMXL6SkK3MRo,18893
21
21
  copyparty/multicast.py,sha256=Ha27l2oATEa-Qo2WOzkeRgjAm6G_YDCfbVJWR-ao2UE,12319
22
22
  copyparty/pwhash.py,sha256=AdLMLyIi2IDhGtbKIQOswKUxWvO7ARYYRF_ThsryOoc,4124
@@ -24,15 +24,15 @@ copyparty/smbd.py,sha256=Or7RF13cl1r3ncnpVh8BqyAGqH2Oa04O9iPZWCoB0Bo,14609
24
24
  copyparty/ssdp.py,sha256=R1Z61GZOxBMF2Sk4RTxKWMOemogmcjEWG-CvLihd45k,7023
25
25
  copyparty/star.py,sha256=tV5BbX6AiQ7N4UU8DYtSTckNYeoeey4DBqq4LjfymbY,3818
26
26
  copyparty/sutil.py,sha256=JTMrQwcWH85hXB_cKG206eDZ967WZDGaP00AWvl_gB0,3214
27
- copyparty/svchub.py,sha256=FuQGFBm-lJfe28M-SMBLieHy8jdWIQMkONK2GcWZU_E,40105
27
+ copyparty/svchub.py,sha256=sfA_2BIXf0GP6-vNJGsttcvuVTjMek7TXWoGhYSa1bg,41178
28
28
  copyparty/szip.py,sha256=sDypi1_yR6-62fIZ_3D0L9PfIzCUiK_3JqcaJCvTBCs,8601
29
29
  copyparty/tcpsrv.py,sha256=l_vb9FoF0AJur0IoqHNUSBDqMgBO_MRUZeDszi1UNfY,19881
30
30
  copyparty/tftpd.py,sha256=jZbf2JpeJmkuQWJErmAPG-dKhtYNvIUHbkAgodSXw9Y,13582
31
31
  copyparty/th_cli.py,sha256=o6FMkerYvAXS455z3DUossVztu_nzFlYSQhs6qN6Jt8,4636
32
32
  copyparty/th_srv.py,sha256=hI9wY1E_9N9Cgqvtr8zADeVqqiLGTiTdAnYAA7WFvJw,29346
33
- copyparty/u2idx.py,sha256=JjgqwgJBNj6sTn4PJfuqM3VEHqlmoyGC5bk4_92K2h0,13414
34
- copyparty/up2k.py,sha256=Zn33f8EkFpAnK_RTDyVNA97CKlS9MS3k5mnnvYh1w1k,165351
35
- copyparty/util.py,sha256=7_-17F94TsLw644xLT8FfO_fH0DwViD54-grej3f8sY,92379
33
+ copyparty/u2idx.py,sha256=HLO49L1zmpJtBcJiXgD12a6pAlQdnf2pFelHMA7habw,13462
34
+ copyparty/up2k.py,sha256=PYAcSuiEJAikQSDGNodyv-jXNtGxmN-KB8_hZ4989Qg,165385
35
+ copyparty/util.py,sha256=bV_zxOiG3GIxt91EkTR7r-YjvR7WOM9IIvg0zuq0s7k,93378
36
36
  copyparty/bos/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
37
  copyparty/bos/bos.py,sha256=Wb7eWsXJgR5AFlBR9ZOyKrLTwy-Kct9RrGiOu4Jo37Y,1622
38
38
  copyparty/bos/path.py,sha256=yEjCq2ki9CvxA5sCT8pS0keEXwugs0ZeUyUhdBziOCI,777
@@ -57,7 +57,7 @@ copyparty/stolen/ifaddr/_win32.py,sha256=EE-QyoBgeB7lYQ6z62VjXNaRozaYfCkaJBHGNA8
57
57
  copyparty/web/baguettebox.js.gz,sha256=YIaxFDsubJfGIdzzxA-cL6GwJVmpWZyaPhW9hHcOIIw,7964
58
58
  copyparty/web/browser.css.gz,sha256=4bAS9Xkl2fflhaxRSRSVoYQcpXsg1mCWxsYjId7phbU,11610
59
59
  copyparty/web/browser.html,sha256=ISpfvWEawufJCYZIqvuXiyUgiXgjmOTtScz4zrEaypI,4870
60
- copyparty/web/browser.js.gz,sha256=7rubbEoqFlNn7FPF0mz3L2LQeWuPzw95MYpIaZmcczE,84985
60
+ copyparty/web/browser.js.gz,sha256=yDhq0t-6R9OCv--ca_dCCsg7ZkhJjN3Y-9-PQdZPRpg,85071
61
61
  copyparty/web/browser2.html,sha256=NRUZ08GH-e2YcGXcoz0UjYg6JIVF42u4IMX4HHwWTmg,1587
62
62
  copyparty/web/cf.html,sha256=lJThtNFNAQT1ClCHHlivAkDGE0LutedwopXD62Z8Nys,589
63
63
  copyparty/web/dbg-audio.js.gz,sha256=Ma-KZtK8LnmiwNvNKFKXMPYl_Nn_3U7GsJ6-DRWC2HE,688
@@ -75,17 +75,17 @@ copyparty/web/shares.css.gz,sha256=T2fSezuluDVIiNIERAuUREByhHFlIwwNyx7EBOAVVyQ,4
75
75
  copyparty/web/shares.html,sha256=ZNHtLBM-Y4BX2qa9AGTrZzZp_IP5PLM3QvFMYKolFfM,2494
76
76
  copyparty/web/shares.js.gz,sha256=814O61mxLSWs0AO2fbGJ8d4BSPB7pE9NdkKiVf1gj6E,926
77
77
  copyparty/web/splash.css.gz,sha256=RjdNoIT5BSxXRFu0ldMUH4ghRNUMCTs2mGKzstrpI6o,1033
78
- copyparty/web/splash.html,sha256=pUbsso_W3Q7bso8fy8qxh-fHDrrLm39mBBTIlTeH63w,5235
79
- copyparty/web/splash.js.gz,sha256=Xoccku-2vE3tABo-88q3Cl4koHs_AE76T8QvMy4u6T8,2540
78
+ copyparty/web/splash.html,sha256=_09d2C79S4sIyaks5pPdq1PBgabdlEFhB6Z-KiynwD8,5699
79
+ copyparty/web/splash.js.gz,sha256=f9aSzI0vw0teK52MBi8Y9FsSMURDfRdZFEvpzqNBon8,2694
80
80
  copyparty/web/svcs.html,sha256=P5YZimYLeQMT0uz6u3clQSNZRc5Zs0Ok-ffcbcGSYuc,11762
81
81
  copyparty/web/svcs.js.gz,sha256=k81ZvZ3I-f4fMHKrNGGOgOlvXnCBz0mVjD-8mieoWCA,520
82
82
  copyparty/web/ui.css.gz,sha256=wloSacrHgP722hy4XiOvVY2GI9-V4zvfvzu84LLWS_o,2779
83
- copyparty/web/up2k.js.gz,sha256=iMaZ2joij8ndkA2iFCTri6MnYRxXzQbRu9uwAnxZBj8,23096
83
+ copyparty/web/up2k.js.gz,sha256=lGR1Xb0RkIZ1eHmncsSwWRuFc6FC2rZalvjo3oNTV1s,23291
84
84
  copyparty/web/util.js.gz,sha256=NvjPYhIa0-C_NhUyW-Ra-XinUCRjj8G3pYq1zJHYWEk,14805
85
- copyparty/web/w.hash.js.gz,sha256=5weFAxkcfHhu90tUKnVnTu04U_PQjhzUVRfvir199I0,1093
85
+ copyparty/web/w.hash.js.gz,sha256=l3GpSJD6mcU-1CRWkIj7PybgbjlfSr8oeO3vortIrQk,1105
86
86
  copyparty/web/a/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
87
- copyparty/web/a/partyfuse.py,sha256=fa9bBYNJHvtWpNVjQyyFzx6JOK7MJL1u0zj80PBYQKs,27960
88
- copyparty/web/a/u2c.py,sha256=QA0t_k422uLRh8Kt2eQIpcGYaaUZA1CGz_S-z23Yk-4,49343
87
+ copyparty/web/a/partyfuse.py,sha256=efBOupuGVCaGVGylJ-mcx0kz6paqc2sk8kHIprfGUQU,27993
88
+ copyparty/web/a/u2c.py,sha256=ZmLcGuOWB66ZkAMpJBiR1Xpa75PgpzdFRTt3GGVCorc,49533
89
89
  copyparty/web/a/webdav-cfg.bat,sha256=Y4NoGZlksAIg4cBMb7KdJrpKC6Nx97onaTl6yMjaimk,1449
90
90
  copyparty/web/dd/2.png,sha256=gJ14XFPzaw95L6z92fSq9eMPikSQyu-03P1lgiGe0_I,258
91
91
  copyparty/web/dd/3.png,sha256=4lho8Koz5tV7jJ4ODo6GMTScZfkqsT05yp48EDFIlyg,252
@@ -106,9 +106,9 @@ copyparty/web/deps/prismd.css.gz,sha256=ObUlksQVr-OuYlTz-I4B23TeBg2QDVVGRnWBz8cV
106
106
  copyparty/web/deps/scp.woff2,sha256=w99BDU5i8MukkMEL-iW0YO9H4vFFZSPWxbkH70ytaAg,8612
107
107
  copyparty/web/deps/sha512.ac.js.gz,sha256=lFZaCLumgWxrvEuDr4bqdKHsqjX82AbVAb7_F45Yk88,7033
108
108
  copyparty/web/deps/sha512.hw.js.gz,sha256=vqoXeracj-99Z5MfY3jK2N4WiSzYQdfjy0RnUlQDhSU,8110
109
- copyparty-1.15.8.dist-info/LICENSE,sha256=gOr4h33pCsBEg9uIy9AYmb7qlocL4V9t2uPJS5wllr0,1072
110
- copyparty-1.15.8.dist-info/METADATA,sha256=R5tW0crtzFZ4vKzGny4gQ6-IOERgBgj7LZtP14Huung,138915
111
- copyparty-1.15.8.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
112
- copyparty-1.15.8.dist-info/entry_points.txt,sha256=4zw6a3rqASywQomiYLObjjlxybaI65LYYOTJwgKz7b0,128
113
- copyparty-1.15.8.dist-info/top_level.txt,sha256=LnYUPsDyk-8kFgM6YJLG4h820DQekn81cObKSu9g-sI,10
114
- copyparty-1.15.8.dist-info/RECORD,,
109
+ copyparty-1.15.10.dist-info/LICENSE,sha256=gOr4h33pCsBEg9uIy9AYmb7qlocL4V9t2uPJS5wllr0,1072
110
+ copyparty-1.15.10.dist-info/METADATA,sha256=DhJZfQ1d9CPtx3LXk0ga7thnke1xWg3GJI_HlTPXZLw,140056
111
+ copyparty-1.15.10.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
112
+ copyparty-1.15.10.dist-info/entry_points.txt,sha256=4zw6a3rqASywQomiYLObjjlxybaI65LYYOTJwgKz7b0,128
113
+ copyparty-1.15.10.dist-info/top_level.txt,sha256=LnYUPsDyk-8kFgM6YJLG4h820DQekn81cObKSu9g-sI,10
114
+ copyparty-1.15.10.dist-info/RECORD,,