copyparty 1.14.1__py3-none-any.whl → 1.14.3__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
@@ -969,9 +969,10 @@ def add_fs(ap):
969
969
  def add_share(ap):
970
970
  db_path = os.path.join(E.cfg, "shares.db")
971
971
  ap2 = ap.add_argument_group('share-url options')
972
- ap2.add_argument("--shr", metavar="URL", default="", help="base url for shared files, for example [\033[32m/share\033[0m] (must be a toplevel subfolder)")
973
- ap2.add_argument("--shr-db", metavar="PATH", default=db_path, help="database to store shares in")
974
- ap2.add_argument("--shr-adm", metavar="U,U", default="", help="comma-separated list of users allowed to view/delete any share")
972
+ ap2.add_argument("--shr", metavar="DIR", type=u, default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]")
973
+ ap2.add_argument("--shr-db", metavar="FILE", type=u, default=db_path, help="database to store shares in")
974
+ ap2.add_argument("--shr-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to view/delete any share")
975
+ ap2.add_argument("--shr-rt", metavar="MIN", type=int, default=1440, help="shares can be revived by their owner if they expired less than MIN minutes ago; [\033[32m60\033[0m]=hour, [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week")
975
976
  ap2.add_argument("--shr-v", action="store_true", help="debug")
976
977
 
977
978
 
@@ -1406,7 +1407,7 @@ def add_ui(ap, retry):
1406
1407
  ap2 = ap.add_argument_group('ui options')
1407
1408
  ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
1408
1409
  ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)")
1409
- ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language; one of the following: \033[32meng nor\033[0m")
1410
+ ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language; one of the following: \033[32meng nor chi\033[0m")
1410
1411
  ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..7)")
1411
1412
  ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed")
1412
1413
  ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent")
copyparty/__version__.py CHANGED
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 14, 1)
3
+ VERSION = (1, 14, 3)
4
4
  CODENAME = "one step forward"
5
- BUILD_DT = (2024, 8, 19)
5
+ BUILD_DT = (2024, 8, 30)
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
@@ -35,6 +35,7 @@ from .util import (
35
35
  odfusion,
36
36
  relchk,
37
37
  statdir,
38
+ ub64enc,
38
39
  uncyg,
39
40
  undot,
40
41
  unhumanize,
@@ -337,6 +338,7 @@ class VFS(object):
337
338
  self.dbv = None # closest full/non-jump parent
338
339
  self.lim = None # upload limits; only set for dbv
339
340
  self.shr_src = None # source vfs+rem of a share
341
+ self.shr_files = set() # filenames to include from shr_src
340
342
  self.aread = {}
341
343
  self.awrite = {}
342
344
  self.amove = {}
@@ -362,6 +364,7 @@ class VFS(object):
362
364
  self.all_vps = []
363
365
 
364
366
  self.get_dbv = self._get_dbv
367
+ self.ls = self._ls
365
368
 
366
369
  def __repr__(self) :
367
370
  return "VFS(%s)" % (
@@ -558,7 +561,26 @@ class VFS(object):
558
561
  ad, fn = os.path.split(ap)
559
562
  return os.path.join(absreal(ad), fn)
560
563
 
561
- def ls(
564
+ def _ls_nope(
565
+ self, *a, **ka
566
+ ) :
567
+ raise Pebkac(500, "nope.avi")
568
+
569
+ def _ls_shr(
570
+ self,
571
+ rem ,
572
+ uname ,
573
+ scandir ,
574
+ permsets ,
575
+ lstat = False,
576
+ ) :
577
+ """replaces _ls for certain shares (single-file, or file selection)"""
578
+ vn, rem = self.shr_src # type: ignore
579
+ abspath, real, _ = vn.ls(rem, "\n", scandir, permsets, lstat)
580
+ real = [x for x in real if os.path.basename(x[0]) in self.shr_files]
581
+ return abspath, real, {}
582
+
583
+ def _ls(
562
584
  self,
563
585
  rem ,
564
586
  uname ,
@@ -1501,14 +1523,14 @@ class AuthSrv(object):
1501
1523
  import sqlite3
1502
1524
 
1503
1525
  shv = VFS(self.log_func, "", shr, AXS(), {"d2d": True})
1504
- par = vfs.all_vols[""]
1505
1526
 
1506
1527
  db_path = self.args.shr_db
1507
1528
  db = sqlite3.connect(db_path)
1508
1529
  cur = db.cursor()
1530
+ cur2 = db.cursor()
1509
1531
  now = time.time()
1510
1532
  for row in cur.execute("select * from sh"):
1511
- s_k, s_pw, s_vp, s_pr, s_st, s_un, s_t0, s_t1 = row
1533
+ s_k, s_pw, s_vp, s_pr, s_nf, s_un, s_t0, s_t1 = row
1512
1534
  if s_t1 and s_t1 < now:
1513
1535
  continue
1514
1536
 
@@ -1517,7 +1539,10 @@ class AuthSrv(object):
1517
1539
  self.log(t % (s_pr, s_k, s_un, s_vp))
1518
1540
 
1519
1541
  if s_pw:
1520
- sun = "s_%s" % (s_k,)
1542
+ # gotta reuse the "account" for all shares with this pw,
1543
+ # so do a light scramble as this appears in the web-ui
1544
+ zs = ub64enc(hashlib.sha512(s_pw.encode("utf-8")).digest())[4:16]
1545
+ sun = "s_%s" % (zs.decode("utf-8"),)
1521
1546
  acct[sun] = s_pw
1522
1547
  else:
1523
1548
  sun = "*"
@@ -1532,13 +1557,14 @@ class AuthSrv(object):
1532
1557
  # don't know the abspath yet + wanna ensure the user
1533
1558
  # still has the privs they granted, so nullmap it
1534
1559
  shv.nodes[s_k] = VFS(
1535
- self.log_func, "", "%s/%s" % (shr, s_k), s_axs, par.flags.copy()
1560
+ self.log_func, "", "%s/%s" % (shr, s_k), s_axs, shv.flags.copy()
1536
1561
  )
1537
1562
 
1538
1563
  vfs.nodes[shr] = vfs.all_vols[shr] = shv
1539
1564
  for vol in shv.nodes.values():
1540
1565
  vfs.all_vols[vol.vpath] = vol
1541
1566
  vol.get_dbv = vol._get_share_src
1567
+ vol.ls = vol._ls_nope
1542
1568
 
1543
1569
  zss = set(acct)
1544
1570
  zss.update(self.idp_accs)
@@ -2048,6 +2074,9 @@ class AuthSrv(object):
2048
2074
  if not self.warn_anonwrite or verbosity < 5:
2049
2075
  break
2050
2076
 
2077
+ if enshare and (zv.vpath == shr or zv.vpath.startswith(shrs)):
2078
+ continue
2079
+
2051
2080
  t += '\n\033[36m"/{}" \033[33m{}\033[0m'.format(zv.vpath, zv.realpath)
2052
2081
  for txt, attr in [
2053
2082
  [" read", "uread"],
@@ -2154,10 +2183,9 @@ class AuthSrv(object):
2154
2183
  if x != shr and not x.startswith(shrs)
2155
2184
  }
2156
2185
 
2157
- assert cur # type: ignore
2158
- assert shv # type: ignore
2186
+ assert db and cur and cur2 and shv # type: ignore
2159
2187
  for row in cur.execute("select * from sh"):
2160
- s_k, s_pw, s_vp, s_pr, s_st, s_un, s_t0, s_t1 = row
2188
+ s_k, s_pw, s_vp, s_pr, s_nf, s_un, s_t0, s_t1 = row
2161
2189
  shn = shv.nodes.get(s_k, None)
2162
2190
  if not shn:
2163
2191
  continue
@@ -2172,6 +2200,17 @@ class AuthSrv(object):
2172
2200
  shv.nodes.pop(s_k)
2173
2201
  continue
2174
2202
 
2203
+ fns = []
2204
+ if s_nf:
2205
+ q = "select vp from sf where k = ?"
2206
+ for (s_fn,) in cur2.execute(q, (s_k,)):
2207
+ fns.append(s_fn)
2208
+
2209
+ shn.shr_files = set(fns)
2210
+ shn.ls = shn._ls_shr
2211
+ else:
2212
+ shn.ls = shn._ls
2213
+
2175
2214
  shn.shr_src = (s_vfs, s_rem)
2176
2215
  shn.realpath = s_vfs.canonical(s_rem)
2177
2216
 
@@ -2191,6 +2230,10 @@ class AuthSrv(object):
2191
2230
  # hide subvolume
2192
2231
  vn.nodes[zs] = VFS(self.log_func, "", "", AXS(), {})
2193
2232
 
2233
+ cur2.close()
2234
+ cur.close()
2235
+ db.close()
2236
+
2194
2237
  def chpw(self, broker , uname, pw) :
2195
2238
  if not self.args.chpw:
2196
2239
  return False, "feature disabled in server config"
copyparty/httpcli.py CHANGED
@@ -1607,8 +1607,8 @@ class HttpCli(object):
1607
1607
  if "delete" in self.uparam:
1608
1608
  return self.handle_rm([])
1609
1609
 
1610
- if "unshare" in self.uparam:
1611
- return self.handle_unshare()
1610
+ if "eshare" in self.uparam:
1611
+ return self.handle_eshare()
1612
1612
 
1613
1613
  if "application/octet-stream" in ctype:
1614
1614
  return self.handle_post_binary()
@@ -3262,7 +3262,8 @@ class HttpCli(object):
3262
3262
  raise Exception("not found in registry")
3263
3263
  self.pipes.set(req_path, job)
3264
3264
  except Exception as ex:
3265
- self.log("will not pipe [%s]; %s" % (ap_data, ex), 6)
3265
+ if getattr(ex, "errno", 0) != errno.ENOENT:
3266
+ self.log("will not pipe [%s]; %s" % (ap_data, ex), 6)
3266
3267
  ptop = None
3267
3268
 
3268
3269
  #
@@ -3954,6 +3955,7 @@ class HttpCli(object):
3954
3955
  rvol=rvol,
3955
3956
  wvol=wvol,
3956
3957
  avol=avol,
3958
+ in_shr=self.args.shr and self.vpath.startswith(self.args.shr[1:]),
3957
3959
  vstate=vstate,
3958
3960
  scanning=vs["scanning"],
3959
3961
  hashq=vs["hashq"],
@@ -4002,10 +4004,10 @@ class HttpCli(object):
4002
4004
  def tx_404(self, is_403 = False) :
4003
4005
  rc = 404
4004
4006
  if self.args.vague_403:
4005
- t = '<h1 id="n">404 not found &nbsp;┐( ´ -`)┌</h1><p id="o">or maybe you don\'t have access -- try logging in or <a href="{}/?h">go home</a></p>'
4006
- pt = "404 not found ┐( ´ -`)┌ (or maybe you don't have access -- try logging in)"
4007
+ t = '<h1 id="n">404 not found &nbsp;┐( ´ -`)┌</h1><p id="o">or maybe you don\'t have access -- try a password or <a href="{}/?h">go home</a></p>'
4008
+ pt = "404 not found ┐( ´ -`)┌ (or maybe you don't have access -- try a password)"
4007
4009
  elif is_403:
4008
- t = '<h1 id="p">403 forbiddena &nbsp;~┻━┻</h1><p id="q">you\'ll have to log in or <a href="{}/?h">go home</a></p>'
4010
+ t = '<h1 id="p">403 forbiddena &nbsp;~┻━┻</h1><p id="q">use a password or <a href="{}/?h">go home</a></p>'
4009
4011
  pt = "403 forbiddena ~┻━┻ (you'll have to log in)"
4010
4012
  rc = 403
4011
4013
  else:
@@ -4022,7 +4024,8 @@ class HttpCli(object):
4022
4024
 
4023
4025
  t = t.format(self.args.SR)
4024
4026
  qv = quotep(self.vpaths) + self.ourlq()
4025
- html = self.j2s("splash", this=self, qvpath=qv, msg=t)
4027
+ in_shr = self.args.shr and self.vpath.startswith(self.args.shr[1:])
4028
+ html = self.j2s("splash", this=self, qvpath=qv, in_shr=in_shr, msg=t)
4026
4029
  self.reply(html.encode("utf-8"), status=rc)
4027
4030
  return True
4028
4031
 
@@ -4297,7 +4300,7 @@ class HttpCli(object):
4297
4300
  self.reply(html.encode("utf-8"), status=200)
4298
4301
  return True
4299
4302
 
4300
- def handle_unshare(self) :
4303
+ def handle_eshare(self) :
4301
4304
  idx = self.conn.get_u2idx()
4302
4305
  if not idx or not hasattr(idx, "p_end"):
4303
4306
  if not HAVE_SQLITE3:
@@ -4305,7 +4308,7 @@ class HttpCli(object):
4305
4308
  raise Pebkac(500, "server busy, cannot create share; please retry in a bit")
4306
4309
 
4307
4310
  if self.args.shr_v:
4308
- self.log("handle_unshare: " + self.req)
4311
+ self.log("handle_eshare: " + self.req)
4309
4312
 
4310
4313
  cur = idx.get_shr()
4311
4314
  if not cur:
@@ -4313,18 +4316,36 @@ class HttpCli(object):
4313
4316
 
4314
4317
  skey = self.vpath.split("/")[-1]
4315
4318
 
4316
- uns = cur.execute("select un from sh where k = ?", (skey,)).fetchall()
4317
- un = uns[0][0] if uns and uns[0] else ""
4319
+ rows = cur.execute("select un, t1 from sh where k = ?", (skey,)).fetchall()
4320
+ un = rows[0][0] if rows and rows[0] else ""
4318
4321
 
4319
4322
  if not un:
4320
4323
  raise Pebkac(400, "that sharekey didn't match anything")
4321
4324
 
4325
+ expiry = rows[0][1]
4326
+
4322
4327
  if un != self.uname and self.uname != self.args.shr_adm:
4323
4328
  t = "your username (%r) does not match the sharekey's owner (%r) and you're not admin"
4324
4329
  raise Pebkac(400, t % (self.uname, un))
4325
4330
 
4326
- cur.execute("delete from sh where k = ?", (skey,))
4331
+ reload = False
4332
+ act = self.uparam["eshare"]
4333
+ if act == "rm":
4334
+ cur.execute("delete from sh where k = ?", (skey,))
4335
+ if skey in self.asrv.vfs.nodes[self.args.shr.strip("/")].nodes:
4336
+ reload = True
4337
+ else:
4338
+ now = time.time()
4339
+ if expiry < now:
4340
+ expiry = now
4341
+ reload = True
4342
+ expiry += int(act) * 60
4343
+ cur.execute("update sh set t1 = ? where k = ?", (expiry, skey))
4344
+
4327
4345
  cur.connection.commit()
4346
+ if reload:
4347
+ self.conn.hsrv.broker.ask("_reload_blocking", False, False).get()
4348
+ self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
4328
4349
 
4329
4350
  self.redirect(self.args.SRS + "?shares")
4330
4351
  return True
@@ -4340,11 +4361,31 @@ class HttpCli(object):
4340
4361
  self.log("handle_share: " + json.dumps(req, indent=4))
4341
4362
 
4342
4363
  skey = req["k"]
4343
- vp = req["vp"].strip("/")
4364
+ vps = req["vp"]
4365
+ fns = []
4366
+ if len(vps) == 1:
4367
+ vp = vps[0]
4368
+ if not vp.endswith("/"):
4369
+ vp, zs = vp.rsplit("/", 1)
4370
+ fns = [zs]
4371
+ else:
4372
+ for zs in vps:
4373
+ if zs.endswith("/"):
4374
+ t = "you cannot select more than one folder, or mix flies and folders in one selection"
4375
+ raise Pebkac(400, t)
4376
+ vp = vps[0].rsplit("/", 1)[0]
4377
+ for zs in vps:
4378
+ vp2, fn = zs.rsplit("/", 1)
4379
+ fns.append(fn)
4380
+ if vp != vp2:
4381
+ t = "mismatching base paths in selection:\n [%s]\n [%s]"
4382
+ raise Pebkac(400, t % (vp, vp2))
4383
+
4384
+ vp = vp.strip("/")
4344
4385
  if self.is_vproxied and (vp == self.args.R or vp.startswith(self.args.RS)):
4345
4386
  vp = vp[len(self.args.RS) :]
4346
4387
 
4347
- m = re.search(r"([^0-9a-zA-Z_\.-]|\.\.|^\.)", skey)
4388
+ m = re.search(r"([^0-9a-zA-Z_-])", skey)
4348
4389
  if m:
4349
4390
  raise Pebkac(400, "sharekey has illegal character [%s]" % (m[1],))
4350
4391
 
@@ -4371,29 +4412,41 @@ class HttpCli(object):
4371
4412
  except:
4372
4413
  raise Pebkac(400, "you dont have all the perms you tried to grant")
4373
4414
 
4374
- ap = vfs.canonical(rem)
4375
- st = bos.stat(ap)
4376
- ist = 2 if stat.S_ISDIR(st.st_mode) else 1
4415
+ ap, reals, _ = vfs.ls(
4416
+ rem, self.uname, not self.args.no_scandir, [[s_rd, s_wr, s_mv, s_del]]
4417
+ )
4418
+ rfns = set([x[0] for x in reals])
4419
+ for fn in fns:
4420
+ if fn not in rfns:
4421
+ raise Pebkac(400, "selected file not found on disk: [%s]" % (fn,))
4377
4422
 
4378
4423
  pw = req.get("pw") or ""
4379
4424
  now = int(time.time())
4380
4425
  sexp = req["exp"]
4381
- exp = now + int(sexp) * 60 if sexp else 0
4426
+ exp = int(sexp) if sexp else 0
4427
+ exp = now + exp * 60 if exp else 0
4382
4428
  pr = "".join(zc for zc, zb in zip("rwmd", (s_rd, s_wr, s_mv, s_del)) if zb)
4383
4429
 
4384
4430
  q = "insert into sh values (?,?,?,?,?,?,?,?)"
4385
- cur.execute(q, (skey, pw, vp, pr, ist, self.uname, now, exp))
4386
- cur.connection.commit()
4431
+ cur.execute(q, (skey, pw, vp, pr, len(fns), self.uname, now, exp))
4432
+
4433
+ q = "insert into sf values (?,?)"
4434
+ for fn in fns:
4435
+ cur.execute(q, (skey, fn))
4387
4436
 
4437
+ cur.connection.commit()
4388
4438
  self.conn.hsrv.broker.ask("_reload_blocking", False, False).get()
4389
4439
  self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
4390
4440
 
4391
- surl = "%s://%s%s%s%s" % (
4441
+ fn = quotep(fns[0]) if len(fns) == 1 else ""
4442
+
4443
+ surl = "created share: %s://%s%s%s%s/%s" % (
4392
4444
  "https" if self.is_https else "http",
4393
4445
  self.host,
4394
4446
  self.args.SR,
4395
4447
  self.args.shr,
4396
4448
  skey,
4449
+ fn,
4397
4450
  )
4398
4451
  self.loud_reply(surl, status=201)
4399
4452
  return True
copyparty/svchub.py CHANGED
@@ -100,6 +100,7 @@ class SvcHub(object):
100
100
  self.no_ansi = args.no_ansi
101
101
  self.logf = None
102
102
  self.logf_base_fn = ""
103
+ self.is_dut = False # running in unittest; always False
103
104
  self.stop_req = False
104
105
  self.stopping = False
105
106
  self.stopped = False
@@ -370,11 +371,18 @@ class SvcHub(object):
370
371
 
371
372
  import sqlite3
372
373
 
373
- al.shr = "/%s/" % (al.shr.strip("/"))
374
+ al.shr = al.shr.strip("/")
375
+ if "/" in al.shr or not al.shr:
376
+ t = "config error: --shr must be the name of a virtual toplevel directory to put shares inside"
377
+ self.log("root", t, 1)
378
+ raise Exception(t)
379
+
380
+ al.shr = "/%s/" % (al.shr,)
374
381
 
375
382
  create = True
383
+ modified = False
376
384
  db_path = self.args.shr_db
377
- self.log("root", "initializing shares-db %s" % (db_path,))
385
+ self.log("root", "opening shares-db %s" % (db_path,))
378
386
  for n in range(2):
379
387
  try:
380
388
  db = sqlite3.connect(db_path)
@@ -400,18 +408,43 @@ class SvcHub(object):
400
408
  pass
401
409
  os.unlink(db_path)
402
410
 
411
+ sch1 = [
412
+ r"create table kv (k text, v int)",
413
+ r"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int)",
414
+ # sharekey, password, src, perms, numFiles, owner, created, expires
415
+ ]
416
+ sch2 = [
417
+ r"create table sf (k text, vp text)",
418
+ r"create index sf_k on sf(k)",
419
+ r"create index sh_k on sh(k)",
420
+ r"create index sh_t1 on sh(t1)",
421
+ ]
422
+
403
423
  assert db # type: ignore
404
424
  assert cur # type: ignore
405
425
  if create:
426
+ dver = 2
427
+ modified = True
428
+ for cmd in sch1 + sch2:
429
+ cur.execute(cmd)
430
+ self.log("root", "created new shares-db")
431
+ else:
432
+ (dver,) = cur.execute("select v from kv where k = 'sver'").fetchall()[0]
433
+
434
+ if dver == 1:
435
+ modified = True
436
+ for cmd in sch2:
437
+ cur.execute(cmd)
438
+ cur.execute("update sh set st = 0")
439
+ self.log("root", "shares-db schema upgrade ok")
440
+
441
+ if modified:
406
442
  for cmd in [
407
- # sharekey, password, src, perms, type, owner, created, expires
408
- r"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int)",
409
- r"create table kv (k text, v int)",
410
- r"insert into kv values ('sver', {})".format(1),
443
+ r"delete from kv where k = 'sver'",
444
+ r"insert into kv values ('sver', %d)" % (2,),
411
445
  ]:
412
446
  cur.execute(cmd)
413
447
  db.commit()
414
- self.log("root", "created new shares-db")
415
448
 
416
449
  cur.close()
417
450
  db.close()
copyparty/tftpd.py CHANGED
@@ -400,7 +400,7 @@ class Tftpd(object):
400
400
  bos.stat(ap)
401
401
  return True
402
402
  except:
403
- return False
403
+ return vpath == "/"
404
404
 
405
405
  def _p_isdir(self, vpath ) :
406
406
  try:
@@ -408,7 +408,7 @@ class Tftpd(object):
408
408
  ret = stat.S_ISDIR(st.st_mode)
409
409
  return ret
410
410
  except:
411
- return False
411
+ return vpath == "/"
412
412
 
413
413
  def _hook(self, *a , **ka ) :
414
414
  src = inspect.currentframe().f_back.f_code.co_name
copyparty/up2k.py CHANGED
@@ -233,6 +233,9 @@ class Up2k(object):
233
233
  if not self.pp and self.args.exit == "idx":
234
234
  return self.hub.sigterm()
235
235
 
236
+ if self.hub.is_dut:
237
+ return
238
+
236
239
  Daemon(self._snapshot, "up2k-snapshot")
237
240
  if have_e2d:
238
241
  Daemon(self._hasher, "up2k-hasher")
@@ -429,7 +432,7 @@ class Up2k(object):
429
432
  def _sched_rescan(self) :
430
433
  volage = {}
431
434
  cooldown = timeout = time.time() + 3.0
432
- while True:
435
+ while not self.stop:
433
436
  now = time.time()
434
437
  timeout = max(timeout, cooldown)
435
438
  wait = timeout - time.time()
@@ -437,6 +440,9 @@ class Up2k(object):
437
440
  with self.rescan_cond:
438
441
  self.rescan_cond.wait(wait)
439
442
 
443
+ if self.stop:
444
+ return
445
+
440
446
  now = time.time()
441
447
  if now < cooldown:
442
448
  # self.log("SR: cd - now = {:.2f}".format(cooldown - now), 5)
@@ -460,6 +466,7 @@ class Up2k(object):
460
466
  if self.args.shr:
461
467
  timeout = min(self._check_shares(), timeout)
462
468
  except Exception as ex:
469
+ timeout = min(timeout, now + 60)
463
470
  t = "could not check for expiring shares: %r"
464
471
  self.log(t % (ex,), 1)
465
472
 
@@ -569,27 +576,53 @@ class Up2k(object):
569
576
 
570
577
  now = time.time()
571
578
  timeout = now + 9001
579
+ maxage = self.args.shr_rt * 60
580
+ low = now - maxage
581
+
582
+ vn = self.asrv.vfs.nodes.get(self.args.shr.strip("/"))
583
+ active = vn and vn.nodes
572
584
 
573
585
  db = sqlite3.connect(self.args.shr_db, timeout=2)
574
586
  cur = db.cursor()
575
587
 
576
588
  q = "select k from sh where t1 and t1 <= ?"
577
- rm = [x[0] for x in cur.execute(q, (now,))]
589
+ rm = [x[0] for x in cur.execute(q, (now,))] if active else []
590
+ if rm:
591
+ assert vn and vn.nodes # type: ignore
592
+ # self.log("chk_shr: %d" % (len(rm),))
593
+ zss = set(rm)
594
+ rm = [zs for zs in vn.nodes if zs in zss]
595
+ reload = bool(rm)
596
+ if reload:
597
+ self.log("disabling expired shares %s" % (rm,))
598
+
599
+ rm = [x[0] for x in cur.execute(q, (low,))]
578
600
  if rm:
579
601
  self.log("forgetting expired shares %s" % (rm,))
580
- q = "delete from sh where k=?"
581
- cur.executemany(q, [(x,) for x in rm])
602
+ cur.executemany("delete from sh where k=?", [(x,) for x in rm])
603
+ cur.executemany("delete from sf where k=?", [(x,) for x in rm])
582
604
  db.commit()
605
+
606
+ if reload:
583
607
  Daemon(self.hub._reload_blocking, "sharedrop", (False, False))
584
608
 
585
- q = "select min(t1) from sh where t1 > 1"
586
- (earliest,) = cur.execute(q).fetchone()
609
+ q = "select min(t1) from sh where t1 > ?"
610
+ (earliest,) = cur.execute(q, (1,)).fetchone()
587
611
  if earliest:
588
- timeout = earliest - now
612
+ # deadline for revoking regular access
613
+ timeout = min(timeout, earliest + maxage)
614
+
615
+ (earliest,) = cur.execute(q, (now - 2,)).fetchone()
616
+ if earliest:
617
+ # deadline for revival; drop entirely
618
+ timeout = min(timeout, earliest)
589
619
 
590
620
  cur.close()
591
621
  db.close()
592
622
 
623
+ if self.args.shr_v:
624
+ self.log("next shr_chk = %d (%d)" % (timeout, timeout - time.time()))
625
+
593
626
  return timeout
594
627
 
595
628
  def _check_xiu(self) :
@@ -1402,7 +1435,7 @@ class Up2k(object):
1402
1435
  if dts == lmod and dsz == sz and (nohash or dw[0] != "#" or not sz):
1403
1436
  continue
1404
1437
 
1405
- t = "reindex [{}] => [{}] ({}/{}) ({}/{})".format(
1438
+ t = "reindex [{}] => [{}] mtime({}/{}) size({}/{})".format(
1406
1439
  top, rp, dts, lmod, dsz, sz
1407
1440
  )
1408
1441
  self.log(t)
@@ -2661,11 +2694,19 @@ class Up2k(object):
2661
2694
  if stat.S_ISLNK(st.st_mode):
2662
2695
  # broken symlink
2663
2696
  raise Exception()
2664
- except:
2697
+ if st.st_size != dsize:
2698
+ t = "candidate ignored (db/fs desync): {}, size fs={} db={}, mtime fs={} db={}, file: {}"
2699
+ t = t.format(
2700
+ wark, st.st_size, dsize, st.st_mtime, dtime, dp_abs
2701
+ )
2702
+ self.log(t)
2703
+ raise Exception("desync")
2704
+ except Exception as ex:
2665
2705
  if n4g:
2666
2706
  st = os.stat_result((0, -1, -1, 0, 0, 0, 0, 0, 0, 0))
2667
2707
  else:
2668
- lost.append((cur, dp_dir, dp_fn))
2708
+ if str(ex) != "desync":
2709
+ lost.append((cur, dp_dir, dp_fn))
2669
2710
  continue
2670
2711
 
2671
2712
  j = {
@@ -2723,13 +2764,16 @@ class Up2k(object):
2723
2764
  ptop = None # use cj or job as appropriate
2724
2765
 
2725
2766
  if not job and wark in reg:
2726
- # ensure the files haven't been deleted manually
2767
+ # ensure the files haven't been edited or deleted
2768
+ path = ""
2769
+ st = None
2727
2770
  rj = reg[wark]
2728
2771
  names = [rj[x] for x in ["name", "tnam"] if x in rj]
2729
2772
  for fn in names:
2730
2773
  path = djoin(rj["ptop"], rj["prel"], fn)
2731
2774
  try:
2732
- if bos.path.getsize(path) > 0 or not rj["need"]:
2775
+ st = bos.stat(path)
2776
+ if st.st_size > 0 or not rj["need"]:
2733
2777
  # upload completed or both present
2734
2778
  break
2735
2779
  except:
@@ -2740,6 +2784,14 @@ class Up2k(object):
2740
2784
  del reg[wark]
2741
2785
  break
2742
2786
 
2787
+ if st and not self.args.nw and not n4g and st.st_size != rj["size"]:
2788
+ t = "will not dedup (fs index desync): {}, size fs={} db={}, mtime fs={} db={}, file: {}"
2789
+ t = t.format(
2790
+ wark, st.st_size, rj["size"], st.st_mtime, rj["lmod"], path
2791
+ )
2792
+ self.log(t)
2793
+ del reg[wark]
2794
+
2743
2795
  if job or wark in reg:
2744
2796
  job = job or reg[wark]
2745
2797
  if (
@@ -2847,6 +2899,7 @@ class Up2k(object):
2847
2899
  return self._handle_json(job, depth + 1)
2848
2900
 
2849
2901
  job["name"] = self._untaken(pdir, job, now)
2902
+ dst = djoin(job["ptop"], job["prel"], job["name"])
2850
2903
 
2851
2904
  if not self.args.nw:
2852
2905
  dvf = vfs.flags
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 = "1.22"
5
- S_BUILD_DT = "2024-08-08"
4
+ S_VERSION = "1.23"
5
+ S_BUILD_DT = "2024-08-22"
6
6
 
7
7
  """
8
8
  u2c.py: upload to copyparty
@@ -1236,7 +1236,7 @@ source file/folder selection uses rsync syntax, meaning that:
1236
1236
  ap.add_argument("-v", action="store_true", help="verbose")
1237
1237
  ap.add_argument("-a", metavar="PASSWD", help="password or $filepath")
1238
1238
  ap.add_argument("-s", action="store_true", help="file-search (disables upload)")
1239
- ap.add_argument("-x", type=unicode, metavar="REGEX", default="", help="skip file if filesystem-abspath matches REGEX, example: '.*/\\.hist/.*'")
1239
+ ap.add_argument("-x", type=unicode, metavar="REGEX", action="append", help="skip file if filesystem-abspath matches REGEX (option can be repeated), example: '.*/\\.hist/.*'")
1240
1240
  ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible")
1241
1241
  ap.add_argument("--touch", action="store_true", help="if last-modified timestamps differ, push local to server (need write+delete perms)")
1242
1242
  ap.add_argument("--ow", action="store_true", help="overwrite existing files instead of autorenaming")
@@ -1283,6 +1283,8 @@ source file/folder selection uses rsync syntax, meaning that:
1283
1283
  if ar.dr:
1284
1284
  ar.ow = True
1285
1285
 
1286
+ ar.x = "|".join(ar.x or [])
1287
+
1286
1288
  for k in "dl dr drd".split():
1287
1289
  errs = []
1288
1290
  if ar.safe and getattr(ar, k):
Binary file
Binary file
Binary file
copyparty/web/shares.html CHANGED
@@ -15,25 +15,26 @@
15
15
  <body>
16
16
  <div id="wrap">
17
17
  <a id="a" href="{{ r }}/?shares" class="af">refresh</a>
18
- <a id="a" href="{{ r }}/?h" class="af">controlpanel</a>
18
+ <a id="a" href="{{ r }}/?h" class="af">control-panel</a>
19
19
 
20
20
  <span>axs = perms (read,write,move,delet)</span>
21
- <span>st 1=file 2=dir</span>
21
+ <span>nf = numFiles (0=dir)</span>
22
22
  <span>min/hrs = time left</span>
23
23
 
24
- <table><tr>
24
+ <table id="tab"><thead><tr>
25
25
  <th>delete</th>
26
26
  <th>sharekey</th>
27
27
  <th>pw</th>
28
28
  <th>source</th>
29
29
  <th>axs</th>
30
- <th>st</th>
30
+ <th>nf</th>
31
31
  <th>user</th>
32
32
  <th>created</th>
33
33
  <th>expires</th>
34
34
  <th>min</th>
35
35
  <th>hrs</th>
36
- </tr>
36
+ <th>add time</th>
37
+ </tr></thead><tbody>
37
38
  {% for k, pw, vp, pr, st, un, t0, t1 in rows %}
38
39
  <tr>
39
40
  <td><a href="#" k="{{ k }}">delete</a></td>
@@ -45,11 +46,12 @@
45
46
  <td>{{ un|e }}</td>
46
47
  <td>{{ t0 }}</td>
47
48
  <td>{{ t1 }}</td>
48
- <td>{{ (t1 - now) // 60 if t1 else "never" }}</td>
49
- <td>{{ (t1 - now) // 3600 if t1 else "never" }}</td>
49
+ <td>{{ "inf" if not t1 else "dead" if t1 < now else ((t1 - now) / 60) | round(1) }}</td>
50
+ <td>{{ "inf" if not t1 else "dead" if t1 < now else ((t1 - now) / 3600) | round(1) }}</td>
51
+ <td></td>
50
52
  </tr>
51
53
  {% endfor %}
52
- </table>
54
+ </tbody></table>
53
55
  {% if not rows %}
54
56
  (you don't have any active shares btw)
55
57
  {% endif %}
Binary file
Binary file
copyparty/web/splash.html CHANGED
@@ -14,6 +14,7 @@
14
14
 
15
15
  <body>
16
16
  <div id="wrap">
17
+ {%- if not in_shr %}
17
18
  <a id="a" href="{{ r }}/?h" class="af">refresh</a>
18
19
  <a id="v" href="{{ r }}/?hc" class="af">connect</a>
19
20
 
@@ -23,6 +24,7 @@
23
24
  <a id="c" href="{{ r }}/?pw=x" class="logout">logout</a>
24
25
  <p><span id="m">welcome back,</span> <strong>{{ this.uname|e }}</strong></p>
25
26
  {%- endif %}
27
+ {%- endif %}
26
28
 
27
29
  {%- if msg %}
28
30
  <div id="msg">
@@ -76,6 +78,37 @@
76
78
  </ul>
77
79
  {%- endif %}
78
80
 
81
+ {%- if in_shr %}
82
+ <h1 id="z">unlock this share:</h1>
83
+ <div>
84
+ <form id="lf" method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}">
85
+ <input type="hidden" id="la" name="act" value="login" />
86
+ <input type="password" id="lp" name="cppwd" placeholder=" password" />
87
+ <input type="hidden" name="uhash" id="uhash" value="x" />
88
+ <input type="submit" id="ls" value="Unlock" />
89
+ {% if ahttps %}
90
+ <a id="w" href="{{ ahttps }}">switch to https</a>
91
+ {% endif %}
92
+ </form>
93
+ </div>
94
+ {%- else %}
95
+ <h1 id="l">login for more:</h1>
96
+ <div>
97
+ <form id="lf" method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}">
98
+ <input type="hidden" id="la" name="act" value="login" />
99
+ <input type="password" id="lp" name="cppwd" placeholder=" password" />
100
+ <input type="hidden" name="uhash" id="uhash" value="x" />
101
+ <input type="submit" id="ls" value="Login" />
102
+ {% if chpw %}
103
+ <a id="x" href="#">change password</a>
104
+ {% endif %}
105
+ {% if ahttps %}
106
+ <a id="w" href="{{ ahttps }}">switch to https</a>
107
+ {% endif %}
108
+ </form>
109
+ </div>
110
+ {%- endif %}
111
+
79
112
  <h1 id="cc">other stuff:</h1>
80
113
  <ul>
81
114
  {%- if this.uname != '*' and this.args.shr %}
@@ -94,21 +127,6 @@
94
127
  <li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li>
95
128
  </ul>
96
129
 
97
- <h1 id="l">login for more:</h1>
98
- <div>
99
- <form id="lf" method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}">
100
- <input type="hidden" id="la" name="act" value="login" />
101
- <input type="password" id="lp" name="cppwd" placeholder=" password" />
102
- <input type="hidden" name="uhash" id="uhash" value="x" />
103
- <input type="submit" id="ls" value="Login" />
104
- {% if chpw %}
105
- <a id="x" href="#">change password</a>
106
- {% endif %}
107
- {% if ahttps %}
108
- <a id="w" href="{{ ahttps }}">switch to https</a>
109
- {% endif %}
110
- </form>
111
- </div>
112
130
  </div>
113
131
  <a href="#" id="repl">π</a>
114
132
  {%- if not this.args.nb %}
Binary file
copyparty/web/ui.css.gz CHANGED
Binary file
copyparty/web/util.js.gz CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: copyparty
3
- Version: 1.14.1
3
+ Version: 1.14.3
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
@@ -52,7 +52,9 @@ Requires-Dist: Pillow; extra == "thumbnails"
52
52
  Provides-Extra: thumbnails2
53
53
  Requires-Dist: pyvips; extra == "thumbnails2"
54
54
 
55
- # 💾🎉 copyparty
55
+ <img src="docs/logo.svg" width="250" align="right"/>
56
+
57
+ ### 💾🎉 copyparty
56
58
 
57
59
  turn almost any device into a file server with resumable uploads/downloads using [*any*](#browser-support) web browser
58
60
 
@@ -804,14 +806,16 @@ you can move files across browser tabs (cut in one tab, paste in another)
804
806
 
805
807
  share a file or folder by creating a temporary link
806
808
 
807
- when enabled in the server settings (`--shr`), click the bottom-right `share` button to share the folder you're currently in, or select a file first to share only that file
809
+ when enabled in the server settings (`--shr`), click the bottom-right `share` button to share the folder you're currently in, or alternatively:
810
+ * select a folder first to share that folder instead
811
+ * select one or more files to share only those files
808
812
 
809
813
  this feature was made with [identity providers](#identity-providers) in mind -- configure your reverseproxy to skip the IdP's access-control for a given URL prefix and use that to safely share specific files/folders sans the usual auth checks
810
814
 
811
815
  when creating a share, the creator can choose any of the following options:
812
816
 
813
817
  * password-protection
814
- * expire after a certain time
818
+ * expire after a certain time; `0` or blank means infinite
815
819
  * allow visitors to upload (if the user who creates the share has write-access)
816
820
 
817
821
  semi-intentional limitations:
@@ -822,10 +826,17 @@ semi-intentional limitations:
822
826
  * when linking something to discord (for example) it'll get accessed by their scraper and that would count as a hit
823
827
  * browsers wouldn't be able to resume a broken download unless the requester's IP gets allowlisted for X minutes (ref. tricky)
824
828
 
825
- the links are created inside a specific toplevel folder which must be specified with server-config `--shr`, for example `--shr /share/` (this also enables the feature)
829
+ specify `--shr /foobar` to enable this feature; a toplevel virtual folder named `foobar` is then created, and that's where all the shares will be served from
830
+
831
+ * you can name it whatever, `foobar` is just an example
832
+ * if you're using config files, put `shr: /foobar` inside the `[global]` section instead
826
833
 
827
834
  users can delete their own shares in the controlpanel, and a list of privileged users (`--shr-adm`) are allowed to see and/or delet any share on the server
828
835
 
836
+ after a share has expired, it remains visible in the controlpanel for `--shr-rt` minutes (default is 1 day), and the owner can revive it by extending the expiration time there
837
+
838
+ **security note:** using this feature does not mean that you can skip the [accounts and volumes](#accounts-and-volumes) section -- you still need to restrict access to volumes that you do not intend to share with unauthenticated users! it is not sufficient to use rules in the reverseproxy to restrict access to just the `/share` folder.
839
+
829
840
 
830
841
  ## batch rename
831
842
 
@@ -1,7 +1,7 @@
1
1
  copyparty/__init__.py,sha256=fUINM1abqDGzCCH_JcXdOnLdKOV-SrTI2Xo2QgQW2P4,1703
2
- copyparty/__main__.py,sha256=JBSVuRhMEnI_LMdRe5VLs09Li7WRJFcLlYGStcu0F90,107832
3
- copyparty/__version__.py,sha256=dTu12M9eISQGxeta4ssAuHyXgdmB0G1mXgoIPbzdzt8,258
4
- copyparty/authsrv.py,sha256=5LEz9yBB4PGEgbVsnJne-XevAsHbolFP-yfanOsDsUs,94305
2
+ copyparty/__main__.py,sha256=0fQcGqHkcYXRqtkh9as-xyBfV_5R6z2fQf41WFtjRe8,108093
3
+ copyparty/__version__.py,sha256=4Eh8URLJCrO8OCohdoeL5DfefMT0SlqqmOBPsnjPgJE,258
4
+ copyparty/authsrv.py,sha256=jWXTjZLT8cGymfa9wBwGJqyBIi80aXcvzAMgI74G8iA,95750
5
5
  copyparty/broker_mp.py,sha256=YFe1S6Zziht8Qc__dCLj_ff8z0DDny9lqk_Mi5ajsJk,3868
6
6
  copyparty/broker_mpw.py,sha256=4ZI7bJYOwUibeAJVv9_FPGNmHrr9eOtkj_Kz0JEppTU,3197
7
7
  copyparty/broker_thr.py,sha256=eKr--HJGig5zqvNGwH9UoBG9Nvi9mT2axrRmJwknd0s,1759
@@ -11,7 +11,7 @@ copyparty/cfg.py,sha256=i8-bjWgbguQooxiA172RcptqR_SEOwDHJ4cqldrZ8oQ,9792
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=xk8ZjiZkb3Cko-_A95bgFgm68rpHrmASLwqRmlOss1U,180620
14
+ copyparty/httpcli.py,sha256=QTOrZOuVh__C_r5REpTiUeGEhrRAke2ejEOOi4zyfbo,182619
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=eAvlDGhKAXUYxAHd03lHY-iNat32Bbt1rZHZwF46Kzg,37367
27
+ copyparty/svchub.py,sha256=v0f8KU65dj2MXOjrd-7kckgiFZSu8kpnTUMyLfn9NIM,38429
28
28
  copyparty/szip.py,sha256=tor4yjdHhEL4Ox-Xg7-cuUFrMO0IwQD29aRX5Cp8MYs,8605
29
29
  copyparty/tcpsrv.py,sha256=jM_Za64O8LEMfMrU4irJluIJZrU494e2b759r_KhaUQ,19881
30
- copyparty/tftpd.py,sha256=i1-oZ05DJq2_nDOW3g3PfTkMoUCr2lAcDYFMWArwtKA,13568
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=Ys3tbEm4RW4FLxaFwrdgG7RSsDd0Hr2832I0nCvLtfM,153049
34
+ copyparty/up2k.py,sha256=kfzsgd-dHjoWUETP79rq46n8oZDU0uDlDlIL3tHbUis,155260
35
35
  copyparty/util.py,sha256=S7FuQBPbl2FX7ULIH9VNgiB7Z_rceqTJssXz4SGErwA,88625
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=tE4SKTvB2l6t91Mb_bhadKbV0QMLrYbAcVE-JQ7MPUk,11494
58
+ copyparty/web/browser.css.gz,sha256=PoW_IIwFigZaMo3atpPU0o05Jj5Flbsm1bhW_KfcX-U,11491
59
59
  copyparty/web/browser.html,sha256=vvfWiu_aOFRar8u5lridMRKQSPF4R0YkA41zrsh82Qs,4878
60
- copyparty/web/browser.js.gz,sha256=lF2coe19rlOIwgDZlcqdnascDjqCJDVzVxtyZopQE2Y,71016
60
+ copyparty/web/browser.js.gz,sha256=vbgNK0skbOyQHqUeYbLxi0qkdAXzWBTGxhgFaKBhxx8,80863
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
@@ -71,21 +71,21 @@ copyparty/web/mde.html,sha256=ImBhQAaEUCke2M85QU_fl4X2XQExRLcEzgCEN8RNe9o,1759
71
71
  copyparty/web/mde.js.gz,sha256=kN2eUSvr4mFuksfK4-4LimJmWdwsao39Sea2lWtu8L0,2224
72
72
  copyparty/web/msg.css.gz,sha256=u90fXYAVrMD-jqwf5XFVC1ptSpSHZUe8Mez6PX101P8,300
73
73
  copyparty/web/msg.html,sha256=w9CM3hkLLGJX9fWEaG4gSbTOPe2GcPqW8BpSCDiFzOI,977
74
- copyparty/web/shares.css.gz,sha256=4OqsDr0aby14LPy61nU6JdEsgDyBw3TMQiClLvOPFds,477
75
- copyparty/web/shares.html,sha256=sogrhA6HCzG413qSrHar8-RFSocuBxACTWArO_HOP28,2208
76
- copyparty/web/shares.js.gz,sha256=PYOZ2jjzDTy_g7lali1kozUjUoI-97LsjXxKUzoThiU,319
77
- copyparty/web/splash.css.gz,sha256=lCleyzwvc5KdkFtuLhImeOeRZKYi_ISiLppCDcmRK7Y,1023
78
- copyparty/web/splash.html,sha256=FDCcWzO8E_mm_D4EaCWgeMyFSkMTAnGxWwPh9dn1e4Y,4221
79
- copyparty/web/splash.js.gz,sha256=RUH5mxrYtCAQ4eLsOUfLl0ryHa1f6-kf_I2j8FitMB0,1840
74
+ copyparty/web/shares.css.gz,sha256=m-nRqTGPiU3ohZxvGaROzFr98F_jmohQnjieqEAyjBo,496
75
+ copyparty/web/shares.html,sha256=d--9tyg6u3JzszpEtMmU5S4XdUF_mfUAhzCvwl-XAXw,2384
76
+ copyparty/web/shares.js.gz,sha256=296fTZV4sW7CxT-YNnDufUZL-aIy4E4r8q-XtSy6bHs,652
77
+ copyparty/web/splash.css.gz,sha256=4DOtEKBWyaDKel7fdnwvnc9FrKlkht-ec7R2nRlruPU,1023
78
+ copyparty/web/splash.html,sha256=dAo4KXKmXUMGcIwetZkFtVxk-mCMNkscD36BxLwRdow,4804
79
+ copyparty/web/splash.js.gz,sha256=pxEHaRDpxTnW6WdRWpKlRux8jtI7B5RImRjUVs9gdQQ,2582
80
80
  copyparty/web/svcs.html,sha256=v0C3cOFWXYlvp3GEifz1Qj0W3MD8JANT3WTON05GZ9o,11797
81
81
  copyparty/web/svcs.js.gz,sha256=k81ZvZ3I-f4fMHKrNGGOgOlvXnCBz0mVjD-8mieoWCA,520
82
- copyparty/web/ui.css.gz,sha256=GnR_PxnZGcNs2IJnb5hFffnhlW3cUHkPad3tNIm-7DQ,2637
82
+ copyparty/web/ui.css.gz,sha256=ae1JosPYS8d2F9e_b95bTwa7qYwk8Ur_UhoVpRYEp0Y,2658
83
83
  copyparty/web/up2k.js.gz,sha256=KufMtRViAZQo2rVj67iEWbdPxlVeXW85emRYVJoY3aA,22946
84
- copyparty/web/util.js.gz,sha256=9LeqbO0j_Z4pEXH2pl9FxWxv5eG7d-SE997AFGboK74,14682
84
+ copyparty/web/util.js.gz,sha256=dPuhXEBJ_T-d2tYUUufGTUul4FYIbuh6GQmtK7iBkEo,14682
85
85
  copyparty/web/w.hash.js.gz,sha256=7wP9EZQNXQxwZnCCFUVsi_-6TM9PLZJeZ9krutXRRj8,1060
86
86
  copyparty/web/a/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
87
87
  copyparty/web/a/partyfuse.py,sha256=MuRkaSuYsdfWfBFMOkbPwDXqSvNTw3sd7QhhlKCDZ8I,32311
88
- copyparty/web/a/u2c.py,sha256=eJlcAN70lKeh46eNq9ae7CO-kgA8oGrMfiOBJivzCXk,42419
88
+ copyparty/web/a/u2c.py,sha256=WG9njRxY9g9xjO93-fas9Wo-AM8vtrWrUTwpJ5Afmvk,42482
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
@@ -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.14.1.dist-info/LICENSE,sha256=gOr4h33pCsBEg9uIy9AYmb7qlocL4V9t2uPJS5wllr0,1072
109
- copyparty-1.14.1.dist-info/METADATA,sha256=AXs4o0mUOTU-WVhn3Kh5QyI-swMa36Rqrk3a4v1Gp38,130962
110
- copyparty-1.14.1.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
111
- copyparty-1.14.1.dist-info/entry_points.txt,sha256=4zw6a3rqASywQomiYLObjjlxybaI65LYYOTJwgKz7b0,128
112
- copyparty-1.14.1.dist-info/top_level.txt,sha256=LnYUPsDyk-8kFgM6YJLG4h820DQekn81cObKSu9g-sI,10
113
- copyparty-1.14.1.dist-info/RECORD,,
108
+ copyparty-1.14.3.dist-info/LICENSE,sha256=gOr4h33pCsBEg9uIy9AYmb7qlocL4V9t2uPJS5wllr0,1072
109
+ copyparty-1.14.3.dist-info/METADATA,sha256=qu9Hp-ogYR98wuQgVMJNJOhOzNAIRKgGO7erkv72ty0,131776
110
+ copyparty-1.14.3.dist-info/WHEEL,sha256=UvcQYKBHoFqaQd6LKyqHw9fxEolWLQnlzP0h_LgJAfI,91
111
+ copyparty-1.14.3.dist-info/entry_points.txt,sha256=4zw6a3rqASywQomiYLObjjlxybaI65LYYOTJwgKz7b0,128
112
+ copyparty-1.14.3.dist-info/top_level.txt,sha256=LnYUPsDyk-8kFgM6YJLG4h820DQekn81cObKSu9g-sI,10
113
+ copyparty-1.14.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (72.2.0)
2
+ Generator: setuptools (74.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5