copyparty 1.15.0__py3-none-any.whl → 1.15.1__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
@@ -1061,6 +1061,7 @@ def add_cert(ap, cert_path):
1061
1061
 
1062
1062
 
1063
1063
  def add_auth(ap):
1064
+ ses_db = os.path.join(E.cfg, "sessions.db")
1064
1065
  ap2 = ap.add_argument_group('IdP / identity provider / user authentication options')
1065
1066
  ap2.add_argument("--idp-h-usr", metavar="HN", type=u, default="", help="bypass the copyparty authentication checks and assume the request-header \033[33mHN\033[0m contains the username of the requesting user (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")
1066
1067
  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")
@@ -1068,6 +1069,9 @@ def add_auth(ap):
1068
1069
  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")
1069
1070
  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")
1070
1071
  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")
1072
+ 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)")
1073
+ ap2.add_argument("--ses-len", metavar="CHARS", type=int, default=20, help="session key length; default is 120 bits ((20//4)*4*6)")
1074
+ ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies")
1071
1075
 
1072
1076
 
1073
1077
  def add_chpw(ap):
@@ -1465,6 +1469,7 @@ def run_argparse(
1465
1469
  ) :
1466
1470
  ap = argparse.ArgumentParser(
1467
1471
  formatter_class=formatter,
1472
+ usage=argparse.SUPPRESS,
1468
1473
  prog="copyparty",
1469
1474
  description="http file sharing hub v{} ({})".format(S_VERSION, S_BUILD_DT),
1470
1475
  )
copyparty/__version__.py CHANGED
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 15, 0)
3
+ VERSION = (1, 15, 1)
4
4
  CODENAME = "fill the drives"
5
- BUILD_DT = (2024, 9, 8)
5
+ BUILD_DT = (2024, 9, 9)
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
@@ -833,8 +833,10 @@ class AuthSrv(object):
833
833
 
834
834
  # fwd-decl
835
835
  self.vfs = VFS(log_func, "", "", AXS(), {})
836
- self.acct = {}
837
- self.iacct = {}
836
+ self.acct = {} # uname->pw
837
+ self.iacct = {} # pw->uname
838
+ self.ases = {} # uname->session
839
+ self.sesa = {} # session->uname
838
840
  self.defpw = {}
839
841
  self.grps = {}
840
842
  self.re_pwd = None
@@ -2174,8 +2176,11 @@ class AuthSrv(object):
2174
2176
  self.grps = grps
2175
2177
  self.iacct = {v: k for k, v in acct.items()}
2176
2178
 
2179
+ self.load_sessions()
2180
+
2177
2181
  self.re_pwd = None
2178
2182
  pwds = [re.escape(x) for x in self.iacct.keys()]
2183
+ pwds.extend(list(self.sesa))
2179
2184
  if pwds:
2180
2185
  if self.ah.on:
2181
2186
  zs = r"(\[H\] pw:.*|[?&]pw=)([^&]+)"
@@ -2250,6 +2255,72 @@ class AuthSrv(object):
2250
2255
  cur.close()
2251
2256
  db.close()
2252
2257
 
2258
+ def load_sessions(self, quiet=False) :
2259
+ # mutex me
2260
+ if self.args.no_ses:
2261
+ self.ases = {}
2262
+ self.sesa = {}
2263
+ return
2264
+
2265
+ import sqlite3
2266
+
2267
+ ases = {}
2268
+ blen = (self.args.ses_len // 4) * 4 # 3 bytes in 4 chars
2269
+ blen = (blen * 3) // 4 # bytes needed for ses_len chars
2270
+
2271
+ db = sqlite3.connect(self.args.ses_db)
2272
+ cur = db.cursor()
2273
+
2274
+ for uname, sid in cur.execute("select un, si from us"):
2275
+ if uname in self.acct:
2276
+ ases[uname] = sid
2277
+
2278
+ n = []
2279
+ q = "insert into us values (?,?,?)"
2280
+ for uname in self.acct:
2281
+ if uname not in ases:
2282
+ sid = ub64enc(os.urandom(blen)).decode("utf-8")
2283
+ cur.execute(q, (uname, sid, int(time.time())))
2284
+ ases[uname] = sid
2285
+ n.append(uname)
2286
+
2287
+ if n:
2288
+ db.commit()
2289
+
2290
+ cur.close()
2291
+ db.close()
2292
+
2293
+ self.ases = ases
2294
+ self.sesa = {v: k for k, v in ases.items()}
2295
+ if n and not quiet:
2296
+ t = ", ".join(n[:3])
2297
+ if len(n) > 3:
2298
+ t += "..."
2299
+ self.log("added %d new sessions (%s)" % (len(n), t))
2300
+
2301
+ def forget_session(self, broker , uname ) :
2302
+ with self.mutex:
2303
+ self._forget_session(uname)
2304
+
2305
+ if broker:
2306
+ broker.ask("_reload_sessions").get()
2307
+
2308
+ def _forget_session(self, uname ) :
2309
+ if self.args.no_ses:
2310
+ return
2311
+
2312
+ import sqlite3
2313
+
2314
+ db = sqlite3.connect(self.args.ses_db)
2315
+ cur = db.cursor()
2316
+ cur.execute("delete from us where un = ?", (uname,))
2317
+ db.commit()
2318
+ cur.close()
2319
+ db.close()
2320
+
2321
+ self.sesa.pop(self.ases.get(uname, ""), "")
2322
+ self.ases.pop(uname, "")
2323
+
2253
2324
  def chpw(self, broker , uname, pw) :
2254
2325
  if not self.args.chpw:
2255
2326
  return False, "feature disabled in server config"
@@ -2269,7 +2340,7 @@ class AuthSrv(object):
2269
2340
  if hpw == self.acct[uname]:
2270
2341
  return False, "that's already your password my dude"
2271
2342
 
2272
- if hpw in self.iacct:
2343
+ if hpw in self.iacct or hpw in self.sesa:
2273
2344
  return False, "password is taken"
2274
2345
 
2275
2346
  with self.mutex:
copyparty/broker_mp.py CHANGED
@@ -72,6 +72,10 @@ class BrokerMp(object):
72
72
  for _, proc in enumerate(self.procs):
73
73
  proc.q_pend.put((0, "reload", []))
74
74
 
75
+ def reload_sessions(self) :
76
+ for _, proc in enumerate(self.procs):
77
+ proc.q_pend.put((0, "reload_sessions", []))
78
+
75
79
  def collector(self, proc ) :
76
80
  """receive message from hub in other process"""
77
81
  while True:
copyparty/broker_mpw.py CHANGED
@@ -88,6 +88,10 @@ class MpWorker(BrokerCli):
88
88
  self.asrv.reload()
89
89
  self.logw("mpw.asrv reloaded")
90
90
 
91
+ elif dest == "reload_sessions":
92
+ with self.asrv.mutex:
93
+ self.asrv.load_sessions()
94
+
91
95
  elif dest == "listen":
92
96
  self.httpsrv.listen(args[0], args[1])
93
97
 
copyparty/broker_thr.py CHANGED
@@ -30,6 +30,7 @@ class BrokerThr(BrokerCli):
30
30
  self.iphash = HMaccas(os.path.join(self.args.E.cfg, "iphash"), 8)
31
31
  self.httpsrv = HttpSrv(self, None)
32
32
  self.reload = self.noop
33
+ self.reload_sessions = self.noop
33
34
 
34
35
  def shutdown(self) :
35
36
  # self.log("broker", "shutting down")
copyparty/httpcli.py CHANGED
@@ -201,7 +201,8 @@ class HttpCli(object):
201
201
 
202
202
  def unpwd(self, m ) :
203
203
  a, b, c = m.groups()
204
- return "%s\033[7m %s \033[27m%s" % (a, self.asrv.iacct[b], c)
204
+ uname = self.asrv.iacct.get(b) or self.asrv.sesa.get(b)
205
+ return "%s\033[7m %s \033[27m%s" % (a, uname, c)
205
206
 
206
207
  def _check_nonfatal(self, ex , post ) :
207
208
  if post:
@@ -500,6 +501,8 @@ class HttpCli(object):
500
501
  zs = base64.b64decode(zb).decode("utf-8")
501
502
  # try "pwd", "x:pwd", "pwd:x"
502
503
  for bauth in [zs] + zs.split(":", 1)[::-1]:
504
+ if bauth in self.asrv.sesa:
505
+ break
503
506
  hpw = self.asrv.ah.hash(bauth)
504
507
  if self.asrv.iacct.get(hpw):
505
508
  break
@@ -561,7 +564,11 @@ class HttpCli(object):
561
564
  self.uname = "*"
562
565
  else:
563
566
  self.pw = uparam.get("pw") or self.headers.get("pw") or bauth or cookie_pw
564
- self.uname = self.asrv.iacct.get(self.asrv.ah.hash(self.pw)) or "*"
567
+ self.uname = (
568
+ self.asrv.sesa.get(self.pw)
569
+ or self.asrv.iacct.get(self.asrv.ah.hash(self.pw))
570
+ or "*"
571
+ )
565
572
 
566
573
  self.rvol = self.asrv.vfs.aread[self.uname]
567
574
  self.wvol = self.asrv.vfs.awrite[self.uname]
@@ -2084,6 +2091,9 @@ class HttpCli(object):
2084
2091
  if act == "chpw":
2085
2092
  return self.handle_chpw()
2086
2093
 
2094
+ if act == "logout":
2095
+ return self.handle_logout()
2096
+
2087
2097
  raise Pebkac(422, 'invalid action "{}"'.format(act))
2088
2098
 
2089
2099
  def handle_zip_post(self) :
@@ -2405,7 +2415,8 @@ class HttpCli(object):
2405
2415
  msg = "new password OK"
2406
2416
 
2407
2417
  redir = (self.args.SRS + "?h") if ok else ""
2408
- html = self.j2s("msg", h1=msg, h2='<a href="/?h">ack</a>', redir=redir)
2418
+ h2 = '<a href="' + self.args.SRS + '?h">ack</a>'
2419
+ html = self.j2s("msg", h1=msg, h2=h2, redir=redir)
2409
2420
  self.reply(html.encode("utf-8"))
2410
2421
  return True
2411
2422
 
@@ -2418,9 +2429,8 @@ class HttpCli(object):
2418
2429
  uhash = ""
2419
2430
  self.parser.drop()
2420
2431
 
2421
- self.out_headerlist = [
2422
- x for x in self.out_headerlist if x[0] != "Set-Cookie" or "cppw" != x[1][:4]
2423
- ]
2432
+ if not pwd:
2433
+ raise Pebkac(422, "password cannot be blank")
2424
2434
 
2425
2435
  dst = self.args.SRS
2426
2436
  if self.vpath:
@@ -2438,9 +2448,27 @@ class HttpCli(object):
2438
2448
  self.reply(html.encode("utf-8"))
2439
2449
  return True
2440
2450
 
2451
+ def handle_logout(self) :
2452
+ assert self.parser
2453
+ self.parser.drop()
2454
+
2455
+ self.log("logout " + self.uname)
2456
+ self.asrv.forget_session(self.conn.hsrv.broker, self.uname)
2457
+ self.get_pwd_cookie("x")
2458
+
2459
+ dst = self.args.SRS + "?h"
2460
+ h2 = '<a href="' + dst + '">ack</a>'
2461
+ html = self.j2s("msg", h1="ok bye", h2=h2, redir=dst)
2462
+ self.reply(html.encode("utf-8"))
2463
+ return True
2464
+
2441
2465
  def get_pwd_cookie(self, pwd ) :
2442
- hpwd = self.asrv.ah.hash(pwd)
2443
- uname = self.asrv.iacct.get(hpwd)
2466
+ uname = self.asrv.sesa.get(pwd)
2467
+ if not uname:
2468
+ hpwd = self.asrv.ah.hash(pwd)
2469
+ uname = self.asrv.iacct.get(hpwd)
2470
+ if uname:
2471
+ pwd = self.asrv.ases.get(uname) or pwd
2444
2472
  if uname:
2445
2473
  msg = "hi " + uname
2446
2474
  dur = int(60 * 60 * self.args.logout)
@@ -2452,8 +2480,9 @@ class HttpCli(object):
2452
2480
  zb = hashlib.sha512(pwd.encode("utf-8", "replace")).digest()
2453
2481
  logpwd = "%" + base64.b64encode(zb[:12]).decode("utf-8")
2454
2482
 
2455
- self.log("invalid password: {}".format(logpwd), 3)
2456
- self.cbonk(self.conn.hsrv.gpwd, pwd, "pw", "invalid passwords")
2483
+ if pwd != "x":
2484
+ self.log("invalid password: {}".format(logpwd), 3)
2485
+ self.cbonk(self.conn.hsrv.gpwd, pwd, "pw", "invalid passwords")
2457
2486
 
2458
2487
  msg = "naw dude"
2459
2488
  pwd = "x" # nosec
@@ -2465,10 +2494,11 @@ class HttpCli(object):
2465
2494
  for k in ("cppwd", "cppws") if self.is_https else ("cppwd",):
2466
2495
  ck = gencookie(k, pwd, self.args.R, False)
2467
2496
  self.out_headerlist.append(("Set-Cookie", ck))
2497
+ self.out_headers.pop("Set-Cookie", None) # drop keepalive
2468
2498
  else:
2469
2499
  k = "cppws" if self.is_https else "cppwd"
2470
2500
  ck = gencookie(k, pwd, self.args.R, self.is_https, dur, "; HttpOnly")
2471
- self.out_headerlist.append(("Set-Cookie", ck))
2501
+ self.out_headers["Set-Cookie"] = ck
2472
2502
 
2473
2503
  return dur > 0, msg
2474
2504
 
copyparty/svchub.py CHANGED
@@ -215,6 +215,9 @@ class SvcHub(object):
215
215
  noch.update([x for x in zsl if x])
216
216
  args.chpw_no = noch
217
217
 
218
+ if not self.args.no_ses:
219
+ self.setup_session_db()
220
+
218
221
  if args.shr:
219
222
  self.setup_share_db()
220
223
 
@@ -363,6 +366,64 @@ class SvcHub(object):
363
366
 
364
367
  self.broker = Broker(self)
365
368
 
369
+ def setup_session_db(self) :
370
+ if not HAVE_SQLITE3:
371
+ self.args.no_ses = True
372
+ t = "WARNING: sqlite3 not available; disabling sessions, will use plaintext passwords in cookies"
373
+ self.log("root", t, 3)
374
+ return
375
+
376
+ import sqlite3
377
+
378
+ create = True
379
+ db_path = self.args.ses_db
380
+ self.log("root", "opening sessions-db %s" % (db_path,))
381
+ for n in range(2):
382
+ try:
383
+ db = sqlite3.connect(db_path)
384
+ cur = db.cursor()
385
+ try:
386
+ cur.execute("select count(*) from us").fetchone()
387
+ create = False
388
+ break
389
+ except:
390
+ pass
391
+ except Exception as ex:
392
+ if n:
393
+ raise
394
+ t = "sessions-db corrupt; deleting and recreating: %r"
395
+ self.log("root", t % (ex,), 3)
396
+ try:
397
+ cur.close() # type: ignore
398
+ except:
399
+ pass
400
+ try:
401
+ db.close() # type: ignore
402
+ except:
403
+ pass
404
+ os.unlink(db_path)
405
+
406
+ sch = [
407
+ r"create table kv (k text, v int)",
408
+ r"create table us (un text, si text, t0 int)",
409
+ # username, session-id, creation-time
410
+ r"create index us_un on us(un)",
411
+ r"create index us_si on us(si)",
412
+ r"create index us_t0 on us(t0)",
413
+ r"insert into kv values ('sver', 1)",
414
+ ]
415
+
416
+ assert db # type: ignore
417
+ assert cur # type: ignore
418
+ if create:
419
+ for cmd in sch:
420
+ cur.execute(cmd)
421
+ self.log("root", "created new sessions-db")
422
+ db.commit()
423
+
424
+ cur.close()
425
+ db.close()
426
+
366
427
  def setup_share_db(self) :
367
428
  al = self.args
368
429
  if not HAVE_SQLITE3:
@@ -539,7 +600,7 @@ class SvcHub(object):
539
600
  fng = []
540
601
  t_ff = "transcode audio, create spectrograms, video thumbnails"
541
602
  to_check = [
542
- (HAVE_SQLITE3, "sqlite", "file and media indexing"),
603
+ (HAVE_SQLITE3, "sqlite", "sessions and file/media indexing"),
543
604
  (HAVE_PIL, "pillow", "image thumbnails (plenty fast)"),
544
605
  (HAVE_VIPS, "vips", "image thumbnails (faster, eats more ram)"),
545
606
  (HAVE_WEBP, "pillow-webp", "create thumbnails as webp files"),
@@ -939,6 +1000,11 @@ class SvcHub(object):
939
1000
 
940
1001
  self._reload(rescan_all_vols=rescan_all_vols, up2k=up2k)
941
1002
 
1003
+ def _reload_sessions(self) :
1004
+ with self.asrv.mutex:
1005
+ self.asrv.load_sessions(True)
1006
+ self.broker.reload_sessions()
1007
+
942
1008
  def stop_thr(self) :
943
1009
  while not self.stop_req:
944
1010
  with self.stop_cond:
copyparty/up2k.py CHANGED
@@ -2843,7 +2843,7 @@ class Up2k(object):
2843
2843
  self.log(t)
2844
2844
  del reg[wark]
2845
2845
 
2846
- elif inc_ap != orig_ap and not data_ok:
2846
+ elif inc_ap != orig_ap and not data_ok and "done" in reg[wark]:
2847
2847
  self.log("asserting contents of %s" % (orig_ap,))
2848
2848
  dhashes, _ = self._hashlist_from_file(orig_ap)
2849
2849
  dwark = up2k_wark_from_hashlist(self.salt, st.st_size, dhashes)
@@ -3104,7 +3104,22 @@ class Up2k(object):
3104
3104
  fp = djoin(fdir, fname)
3105
3105
  if job.get("replace") and bos.path.exists(fp):
3106
3106
  self.log("replacing existing file at {}".format(fp))
3107
- wunlink(self.log, fp, self.flags.get(job["ptop"]) or {})
3107
+ cur = None
3108
+ ptop = job["ptop"]
3109
+ vf = self.flags.get(ptop) or {}
3110
+ st = bos.stat(fp)
3111
+ try:
3112
+ vrel = vjoin(job["prel"], fname)
3113
+ xlink = bool(vf.get("xlink"))
3114
+ cur, wark, _, _, _, _ = self._find_from_vpath(ptop, vrel)
3115
+ self._forget_file(ptop, vrel, cur, wark, True, st.st_size, xlink)
3116
+ except Exception as ex:
3117
+ self.log("skipping replace-relink: %r" % (ex,))
3118
+ finally:
3119
+ if cur:
3120
+ cur.connection.commit()
3121
+
3122
+ wunlink(self.log, fp, vf)
3108
3123
 
3109
3124
  if self.args.plain_ip:
3110
3125
  dip = ip.replace(":", ".")
Binary file
Binary file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: copyparty
3
- Version: 1.15.0
3
+ Version: 1.15.1
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
@@ -1,17 +1,17 @@
1
1
  copyparty/__init__.py,sha256=fUINM1abqDGzCCH_JcXdOnLdKOV-SrTI2Xo2QgQW2P4,1703
2
- copyparty/__main__.py,sha256=snMG7GjXZ54Zssycrb5jiZSiyo04-Q84XtLHILOnmVw,108558
3
- copyparty/__version__.py,sha256=P2ttoiozX2VCs4PraPoVx64v13JfCOKOL5vl61l3aTE,256
4
- copyparty/authsrv.py,sha256=7_gMjcUSm7igR4NjZv34TegBKizmBvWjeRChT5MfF_s,96535
5
- copyparty/broker_mp.py,sha256=YFe1S6Zziht8Qc__dCLj_ff8z0DDny9lqk_Mi5ajsJk,3868
6
- copyparty/broker_mpw.py,sha256=4ZI7bJYOwUibeAJVv9_FPGNmHrr9eOtkj_Kz0JEppTU,3197
7
- copyparty/broker_thr.py,sha256=eKr--HJGig5zqvNGwH9UoBG9Nvi9mT2axrRmJwknd0s,1759
2
+ copyparty/__main__.py,sha256=M8o_4CGNuteh7j_3iiFN_FTu0IIHKrjRScZOBFKqXis,109083
3
+ copyparty/__version__.py,sha256=BCJou8sLX4iTHVA4RubRxWxlC8xKh9t-i3XAUnB-aq8,256
4
+ copyparty/authsrv.py,sha256=pX-Dax35ClxftNnN50UslvZZbPTFI73v-EFLGSqtAA4,98516
5
+ copyparty/broker_mp.py,sha256=krsUWduBULlZ1LnroHhWLGnUBWYtWc_kohZXgHsE458,4004
6
+ copyparty/broker_mpw.py,sha256=9kb_3BYUduwtmluuqdm0OhY6T4DwpUSX2xW7jo6veQ8,3326
7
+ copyparty/broker_thr.py,sha256=BErWJkpd1bnRCcUHowAPNHCfo3CSqVmD-5yVRKjIIhU,1800
8
8
  copyparty/broker_util.py,sha256=w0E-GhoOgq8ow7mEWi3GOyqraux6VG9yk1tif1yo0jc,1474
9
9
  copyparty/cert.py,sha256=kRFkMwBUCV_Vo7BYweD-yJ7Hpp5BCpaXneyBWxlu1PM,7759
10
10
  copyparty/cfg.py,sha256=6cj2xJnBa9vRubM5U_mkA87zG2Ug11vnyk2hYz0XfxI,9965
11
11
  copyparty/dxml.py,sha256=lZpg-kn-kQsXRtNY1n6fRaS-b7uXzMCyv8ovKnhZcZc,1548
12
12
  copyparty/fsutil.py,sha256=hnEHgySI43-XJJKbI8n6t1A6oVHzR_nYdsBcAwtreBk,4610
13
13
  copyparty/ftpd.py,sha256=1vD-KTy07xfEEEk1dx37pUYModpNO2gIhVXvFUr205M,17497
14
- copyparty/httpcli.py,sha256=QTOrZOuVh__C_r5REpTiUeGEhrRAke2ejEOOi4zyfbo,182619
14
+ copyparty/httpcli.py,sha256=T-bISKn73-zt-q3C4dbRDgfB1tspze7dhM3gUY0ztX0,183570
15
15
  copyparty/httpconn.py,sha256=mwIDup85cBowIfJOse8rla5bqTz7nf-ChgfR-5-V0JM,6938
16
16
  copyparty/httpsrv.py,sha256=8_1Ivg3eco7HJDjqL_rUB58IOUaUnoXGhO62bOMXLBk,17242
17
17
  copyparty/ico.py,sha256=eWSxEae4wOCfheHl-m-wchYvFRAR_97kJDb4NGaB-Z8,3561
@@ -24,14 +24,14 @@ copyparty/smbd.py,sha256=8zkC9BjVtGiKXMLajbdakxoKeFzACdM75SW0_SvqXJA,14490
24
24
  copyparty/ssdp.py,sha256=8iyF5sqIjATJLWcAtnJa8eadHosOn0CP4ywltzJ7bVY,7023
25
25
  copyparty/star.py,sha256=tV5BbX6AiQ7N4UU8DYtSTckNYeoeey4DBqq4LjfymbY,3818
26
26
  copyparty/sutil.py,sha256=JTMrQwcWH85hXB_cKG206eDZ967WZDGaP00AWvl_gB0,3214
27
- copyparty/svchub.py,sha256=gxvBZ3LTeBCtJnrGTSRl908z6zKj0SY5vJOmFMbMXYk,38017
27
+ copyparty/svchub.py,sha256=mhELkl2XCvaTrxLcXbfGSPbRtXA1wE66ap73wEWcvFI,40087
28
28
  copyparty/szip.py,sha256=tor4yjdHhEL4Ox-Xg7-cuUFrMO0IwQD29aRX5Cp8MYs,8605
29
29
  copyparty/tcpsrv.py,sha256=jM_Za64O8LEMfMrU4irJluIJZrU494e2b759r_KhaUQ,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=27IftjIXUQzRRiUytt-CgXkybEoP3HHHoXaDAvxEmLo,29217
33
33
  copyparty/u2idx.py,sha256=t4mzjj2GDrkjIHt0RM68y1EgT5qOBoz6mkYgjMbqA38,13526
34
- copyparty/up2k.py,sha256=kDc6GwMGSyvF8hgx2digIIy2LX8RKGj1diRSFrBD0EY,159274
34
+ copyparty/up2k.py,sha256=USxHI5IN-9DQp_P7Yl4683yxlvaWTslZT90h6sfqpuk,159853
35
35
  copyparty/util.py,sha256=qkwrCRqDI7iCiO3X2RQ1LdGVblnkIQe1YTuJPlF27u4,88666
36
36
  copyparty/bos/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
37
  copyparty/bos/bos.py,sha256=Wb7eWsXJgR5AFlBR9ZOyKrLTwy-Kct9RrGiOu4Jo37Y,1622
@@ -55,9 +55,9 @@ copyparty/stolen/ifaddr/_posix.py,sha256=-67NdfGrCktfQPakT2fLbjl2U00QMvyBGkSvrUu
55
55
  copyparty/stolen/ifaddr/_shared.py,sha256=uNC4SdEIgdSLKvuUzsf1aM-H1Xrc_9mpLoOT43YukGs,6206
56
56
  copyparty/stolen/ifaddr/_win32.py,sha256=EE-QyoBgeB7lYQ6z62VjXNaRozaYfCkaJBHGNA8QtZM,4026
57
57
  copyparty/web/baguettebox.js.gz,sha256=4dS8-r4si84ca71l98672ahnRI86Aq95MU-bc5knykk,7962
58
- copyparty/web/browser.css.gz,sha256=-y8OT0wvGrZnZvFxas0_Nob4pf9_1zNABX2nUaAj6Hs,11579
58
+ copyparty/web/browser.css.gz,sha256=TM9gYp-MFSjj_2kV-mKrLFit0Ayz2e7AxI4L0LJmrBE,11608
59
59
  copyparty/web/browser.html,sha256=vvfWiu_aOFRar8u5lridMRKQSPF4R0YkA41zrsh82Qs,4878
60
- copyparty/web/browser.js.gz,sha256=CzvjCOUR_faGx5PLA4rR7XDNdp-N-ZCHJ3rJLUim9kw,84721
60
+ copyparty/web/browser.js.gz,sha256=EZhEAuKeZH9qO1gfRzyARG-PFmP7rFtQfHRr0ZnUEYs,84762
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
@@ -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=vqoXeracj-99Z5MfY3jK2N4WiSzYQdfjy0RnUlQDhSU,8110
108
- copyparty-1.15.0.dist-info/LICENSE,sha256=gOr4h33pCsBEg9uIy9AYmb7qlocL4V9t2uPJS5wllr0,1072
109
- copyparty-1.15.0.dist-info/METADATA,sha256=DL4nbPuSPVsK8XTadsayPyqsiCkgDbOhhbMYKearcqg,134165
110
- copyparty-1.15.0.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
111
- copyparty-1.15.0.dist-info/entry_points.txt,sha256=4zw6a3rqASywQomiYLObjjlxybaI65LYYOTJwgKz7b0,128
112
- copyparty-1.15.0.dist-info/top_level.txt,sha256=LnYUPsDyk-8kFgM6YJLG4h820DQekn81cObKSu9g-sI,10
113
- copyparty-1.15.0.dist-info/RECORD,,
108
+ copyparty-1.15.1.dist-info/LICENSE,sha256=gOr4h33pCsBEg9uIy9AYmb7qlocL4V9t2uPJS5wllr0,1072
109
+ copyparty-1.15.1.dist-info/METADATA,sha256=sfkSpMV_LX0ITSPAYWbMeaYs8Zz1LC0ZSuv5GSpTjLw,134165
110
+ copyparty-1.15.1.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
111
+ copyparty-1.15.1.dist-info/entry_points.txt,sha256=4zw6a3rqASywQomiYLObjjlxybaI65LYYOTJwgKz7b0,128
112
+ copyparty-1.15.1.dist-info/top_level.txt,sha256=LnYUPsDyk-8kFgM6YJLG4h820DQekn81cObKSu9g-sI,10
113
+ copyparty-1.15.1.dist-info/RECORD,,