copyparty 1.19.6__py3-none-any.whl → 1.19.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. copyparty/__init__.py +2 -0
  2. copyparty/__main__.py +48 -19
  3. copyparty/__version__.py +2 -2
  4. copyparty/authsrv.py +96 -8
  5. copyparty/bos/bos.py +38 -2
  6. copyparty/cfg.py +4 -0
  7. copyparty/ftpd.py +4 -25
  8. copyparty/httpcli.py +62 -24
  9. copyparty/httpsrv.py +1 -1
  10. copyparty/smbd.py +1 -1
  11. copyparty/svchub.py +27 -1
  12. copyparty/tcpsrv.py +4 -0
  13. copyparty/up2k.py +49 -25
  14. copyparty/util.py +8 -5
  15. copyparty/web/a/partyfuse.py +19 -11
  16. copyparty/web/a/u2c.py +6 -4
  17. copyparty/web/baguettebox.js.gz +0 -0
  18. copyparty/web/browser.css.gz +0 -0
  19. copyparty/web/browser.html +1 -1
  20. copyparty/web/browser.js.gz +0 -0
  21. copyparty/web/idp.html +5 -5
  22. copyparty/web/md.html +1 -1
  23. copyparty/web/md.js.gz +0 -0
  24. copyparty/web/md2.js.gz +0 -0
  25. copyparty/web/mde.html +1 -1
  26. copyparty/web/msg.html +1 -1
  27. copyparty/web/rups.html +1 -1
  28. copyparty/web/shares.html +5 -5
  29. copyparty/web/splash.css.gz +0 -0
  30. copyparty/web/splash.html +54 -38
  31. copyparty/web/splash.js.gz +0 -0
  32. copyparty/web/svcs.html +80 -39
  33. copyparty/web/svcs.js.gz +0 -0
  34. copyparty/web/up2k.js.gz +0 -0
  35. copyparty/web/util.js.gz +0 -0
  36. {copyparty-1.19.6.dist-info → copyparty-1.19.8.dist-info}/METADATA +8 -1
  37. {copyparty-1.19.6.dist-info → copyparty-1.19.8.dist-info}/RECORD +41 -41
  38. {copyparty-1.19.6.dist-info → copyparty-1.19.8.dist-info}/WHEEL +0 -0
  39. {copyparty-1.19.6.dist-info → copyparty-1.19.8.dist-info}/entry_points.txt +0 -0
  40. {copyparty-1.19.6.dist-info → copyparty-1.19.8.dist-info}/licenses/LICENSE +0 -0
  41. {copyparty-1.19.6.dist-info → copyparty-1.19.8.dist-info}/top_level.txt +0 -0
copyparty/httpcli.py CHANGED
@@ -562,7 +562,7 @@ class HttpCli(object):
562
562
  return False
563
563
  zsll = [x.split("=", 1) for x in zso.split(";") if "=" in x]
564
564
  cookies = {k.strip(): unescape_cookie(zs) for k, zs in zsll}
565
- cookie_pw = cookies.get("cppws") or cookies.get("cppwd") or ""
565
+ cookie_pw = cookies.get("cppws" if self.is_https else "cppwd") or ""
566
566
  if "b" in cookies and "b" not in uparam:
567
567
  uparam["b"] = cookies["b"]
568
568
  if len(cookies) > self.args.cookie_nmax:
@@ -685,7 +685,7 @@ class HttpCli(object):
685
685
  if idp_usr in self.asrv.vfs.aread:
686
686
  self.pw = ""
687
687
  self.uname = idp_usr
688
- if self.args.ao_have_pw:
688
+ if self.args.ao_have_pw or self.args.idp_logout:
689
689
  self.html_head += "<script>var is_idp=1</script>\n"
690
690
  else:
691
691
  self.html_head += "<script>var is_idp=2</script>\n"
@@ -1250,7 +1250,7 @@ class HttpCli(object):
1250
1250
 
1251
1251
  res_path = "web/" + self.vpath[5:]
1252
1252
  if res_path in RES:
1253
- ap = os.path.join(self.E.mod, res_path)
1253
+ ap = self.E.mod_ + res_path
1254
1254
  if bos.path.exists(ap) or bos.path.exists(ap + ".gz"):
1255
1255
  return self.tx_file(ap)
1256
1256
  else:
@@ -1627,7 +1627,14 @@ class HttpCli(object):
1627
1627
  self.log("inaccessible: %r" % ("/" + self.vpath,))
1628
1628
  raise Pebkac(401, "authenticate")
1629
1629
 
1630
- if "quota-available-bytes" in props and not self.args.nid:
1630
+ zi = vn.flags["du_iwho"] if "quota-available-bytes" in props else 0
1631
+ if zi and (
1632
+ zi == 9
1633
+ or (zi == 7 and self.uname != "*")
1634
+ or (zi == 5 and self.can_write)
1635
+ or (zi == 4 and self.can_write and self.can_read)
1636
+ or (zi == 3 and self.can_admin)
1637
+ ):
1631
1638
  bfree, btot, _ = get_df(vn.realpath, False)
1632
1639
  if btot:
1633
1640
  df = {
@@ -2319,12 +2326,7 @@ class HttpCli(object):
2319
2326
  at = mt = time.time() - lifetime
2320
2327
  cli_mt = self.headers.get("x-oc-mtime")
2321
2328
  if cli_mt:
2322
- try:
2323
- mt = int(cli_mt)
2324
- times = (int(time.time()), mt)
2325
- bos.utime(path, times, False)
2326
- except:
2327
- pass
2329
+ bos.utime_c(self.log, path, int(cli_mt), False)
2328
2330
 
2329
2331
  if nameless and "magic" in vfs.flags:
2330
2332
  try:
@@ -3042,7 +3044,7 @@ class HttpCli(object):
3042
3044
  self.asrv.forget_session(self.conn.hsrv.broker, self.uname)
3043
3045
  self.get_pwd_cookie("x")
3044
3046
 
3045
- dst = self.args.SRS + "?h"
3047
+ dst = self.args.idp_logout or (self.args.SRS + "?h")
3046
3048
  h2 = '<a href="' + dst + '">continue</a>'
3047
3049
  html = self.j2s("msg", h1="ok bye", h2=h2, redir=dst)
3048
3050
  self.reply(html.encode("utf-8"))
@@ -3055,6 +3057,11 @@ class HttpCli(object):
3055
3057
  uname = self.asrv.iacct.get(hpwd)
3056
3058
  if uname:
3057
3059
  pwd = self.asrv.ases.get(uname) or pwd
3060
+ if uname and self.conn.hsrv.ipr:
3061
+ znm = self.conn.hsrv.ipr.get(uname)
3062
+ if znm and not znm.map(self.ip):
3063
+ self.log("username [%s] rejected by --ipr" % (self.uname,), 3)
3064
+ uname = ""
3058
3065
  if uname:
3059
3066
  msg = "hi " + uname
3060
3067
  dur = int(60 * 60 * self.args.logout)
@@ -4995,10 +5002,20 @@ class HttpCli(object):
4995
5002
  else:
4996
5003
  rip = host
4997
5004
 
5005
+ defpw = "dave:hunter2" if self.args.usernames else "hunter2"
5006
+
4998
5007
  vp = (self.uparam["hc"] or "").lstrip("/")
4999
- pw = self.ouparam.get("pw") or "hunter2"
5008
+ pw = self.ouparam.get("pw") or defpw
5000
5009
  if pw in self.asrv.sesa:
5001
- pw = "hunter2"
5010
+ pw = defpw
5011
+
5012
+ unpw = pw
5013
+ try:
5014
+ un, pw = unpw.split(":")
5015
+ except:
5016
+ un = ""
5017
+ if self.args.usernames:
5018
+ un = "dave"
5002
5019
 
5003
5020
  html = self.j2s(
5004
5021
  "svcs",
@@ -5012,7 +5029,10 @@ class HttpCli(object):
5012
5029
  host=html_sh_esc(host),
5013
5030
  hport=html_sh_esc(hport),
5014
5031
  aname=aname,
5032
+ b_un=("<b>%s</b>" % (html_sh_esc(un),)) if un else "k",
5033
+ un=html_sh_esc(un),
5015
5034
  pw=html_sh_esc(pw),
5035
+ unpw=html_sh_esc(unpw),
5016
5036
  )
5017
5037
  self.reply(html.encode("utf-8"))
5018
5038
  return True
@@ -5136,6 +5156,11 @@ class HttpCli(object):
5136
5156
  elif nre:
5137
5157
  re_btn = "&re=%s" % (nre,)
5138
5158
 
5159
+ zi = self.args.ver_iwho
5160
+ show_ver = zi and (
5161
+ zi == 9 or (zi == 6 and self.uname != "*") or (zi == 3 and avol)
5162
+ )
5163
+
5139
5164
  html = self.j2s(
5140
5165
  "splash",
5141
5166
  this=self,
@@ -5158,7 +5183,7 @@ class HttpCli(object):
5158
5183
  no304=self.no304(),
5159
5184
  k304vis=self.args.k304 > 0,
5160
5185
  no304vis=self.args.no304 > 0,
5161
- ver=S_VERSION if self.args.ver else "",
5186
+ ver=S_VERSION if show_ver else "",
5162
5187
  chpw=self.args.chpw and self.uname != "*",
5163
5188
  ahttps="" if self.is_https else "https://" + self.host + self.req,
5164
5189
  )
@@ -5373,8 +5398,9 @@ class HttpCli(object):
5373
5398
 
5374
5399
  if dk_sz and fsroot:
5375
5400
  kdirs = []
5401
+ fsroot_ = os.path.join(fsroot, "")
5376
5402
  for dn in dirs:
5377
- ap = os.path.join(fsroot, dn)
5403
+ ap = fsroot_ + dn
5378
5404
  zs = self.gen_fk(2, self.args.dk_salt, ap, 0, 0)[:dk_sz]
5379
5405
  kdirs.append(dn + "?k=" + zs)
5380
5406
  dirs = kdirs
@@ -5905,6 +5931,14 @@ class HttpCli(object):
5905
5931
  except:
5906
5932
  raise Pebkac(400, "you dont have all the perms you tried to grant")
5907
5933
 
5934
+ zs = vfs.flags["shr_who"]
5935
+ if zs == "auth" and self.uname != "*":
5936
+ pass
5937
+ elif zs == "a" and self.uname in vfs.axs.uadmin:
5938
+ pass
5939
+ else:
5940
+ raise Pebkac(400, "you dont have perms to create shares from this volume")
5941
+
5908
5942
  ap, reals, _ = vfs.ls(
5909
5943
  rem, self.uname, not self.args.no_scandir, [[s_rd, s_wr, s_mv, s_del]]
5910
5944
  )
@@ -6166,16 +6200,13 @@ class HttpCli(object):
6166
6200
  add_og = "og" in vn.flags
6167
6201
  if add_og:
6168
6202
  if "th" in self.uparam or "raw" in self.uparam:
6169
- og_ua = add_og = False
6170
- elif self.args.og_ua:
6171
- og_ua = add_og = self.args.og_ua.search(self.ua)
6172
- else:
6173
- og_ua = False
6174
- add_og = True
6203
+ add_og = False
6204
+ elif vn.flags["og_ua"]:
6205
+ add_og = vn.flags["og_ua"].search(self.ua)
6175
6206
  og_fn = ""
6176
6207
 
6177
6208
  if "v" in self.uparam:
6178
- add_og = og_ua = True
6209
+ add_og = True
6179
6210
 
6180
6211
  if "b" in self.uparam:
6181
6212
  self.out_headers["X-Robots-Tag"] = "noindex, nofollow"
@@ -6296,7 +6327,7 @@ class HttpCli(object):
6296
6327
 
6297
6328
  is_md = abspath.lower().endswith(".md")
6298
6329
  if add_og and not is_md:
6299
- if og_ua or self.host not in self.headers.get("referer", ""):
6330
+ if self.host not in self.headers.get("referer", ""):
6300
6331
  self.vpath, og_fn = vsplit(self.vpath)
6301
6332
  vpath = self.vpath
6302
6333
  vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
@@ -6336,7 +6367,14 @@ class HttpCli(object):
6336
6367
  except:
6337
6368
  self.log("#wow #whoa")
6338
6369
 
6339
- if not self.args.nid:
6370
+ zi = vn.flags["du_iwho"]
6371
+ if zi and (
6372
+ zi == 9
6373
+ or (zi == 7 and self.uname != "*")
6374
+ or (zi == 5 and self.can_write)
6375
+ or (zi == 4 and self.can_write and self.can_read)
6376
+ or (zi == 3 and self.can_admin)
6377
+ ):
6340
6378
  free, total, zs = get_df(abspath, False)
6341
6379
  if total:
6342
6380
  h1 = humansize(free or 0)
copyparty/httpsrv.py CHANGED
@@ -565,7 +565,7 @@ class HttpSrv(object):
565
565
 
566
566
  v = self.E.t0
567
567
  try:
568
- with os.scandir(os.path.join(self.E.mod, "web")) as dh:
568
+ with os.scandir(self.E.mod_ + "web") as dh:
569
569
  for fh in dh:
570
570
  inf = fh.stat()
571
571
  v = max(v, inf.st_mtime)
copyparty/smbd.py CHANGED
@@ -370,7 +370,7 @@ class SMB(object):
370
370
  t = "blocked utime (no-write-acc %s): /%s @%s"
371
371
  yeet(t % (vfs.axs.uwrite, vpath, uname))
372
372
 
373
- return bos.utime(ap, times)
373
+ bos.utime_c(info, ap, int(times[1]), False)
374
374
 
375
375
  def _p_exists(self, vpath ) :
376
376
  # ap = "?"
copyparty/svchub.py CHANGED
@@ -21,7 +21,7 @@ from datetime import datetime
21
21
 
22
22
 
23
23
  from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode
24
- from .authsrv import BAD_CFG, AuthSrv
24
+ from .authsrv import BAD_CFG, AuthSrv, n_du_who, n_ver_who
25
25
  from .bos import bos
26
26
  from .cert import ensure_cert
27
27
  from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN
@@ -283,6 +283,14 @@ class SvcHub(object):
283
283
  ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)]
284
284
  args.theme = "{0}{1} {0} {1}".format(ch, bri)
285
285
 
286
+ if args.nid:
287
+ args.du_who = "no"
288
+ args.du_iwho = n_du_who(args.du_who)
289
+
290
+ if args.ver and args.ver_who == "no":
291
+ args.ver_who = "all"
292
+ args.ver_iwho = n_ver_who(args.ver_who)
293
+
286
294
  if args.nih:
287
295
  args.vname = ""
288
296
  args.doctitle = args.doctitle.replace(" @ --name", "")
@@ -966,6 +974,24 @@ class SvcHub(object):
966
974
  t = "WARNING:\nDisabling WebDAV support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n"
967
975
  self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1)
968
976
 
977
+ if not E.scfg and not al.unsafe_state and not os.getenv("PRTY_UNSAFE_STATE"):
978
+ t = "because runtime config is currently being stored in an untrusted emergency-fallback location. Please fix your environment so either XDG_CONFIG_HOME or ~/.config can be used instead, or disable this safeguard with --unsafe-state or env-var PRTY_UNSAFE_STATE=1."
979
+ if not al.no_ses:
980
+ al.no_ses = True
981
+ t2 = "A consequence of this misconfiguration is that passwords will now be sent in the HTTP-header of every request!"
982
+ self.log("root", "WARNING:\nWill disable sessions %s %s" % (t, t2), 1)
983
+ if al.idp_store == 1:
984
+ al.idp_store = 0
985
+ self.log("root", "WARNING:\nDisabling --idp-store %s" % (t,), 3)
986
+ if al.idp_store:
987
+ t2 = "ERROR: Cannot enable --idp-store %s" % (t,)
988
+ self.log("root", t2, 1)
989
+ raise Exception(t2)
990
+ if al.shr:
991
+ t2 = "ERROR: Cannot enable shares %s" % (t,)
992
+ self.log("root", t2, 1)
993
+ raise Exception(t2)
994
+
969
995
  def _process_config(self) :
970
996
  al = self.args
971
997
 
copyparty/tcpsrv.py CHANGED
@@ -302,6 +302,10 @@ class TcpSrv(object):
302
302
  if os.path.exists(ip):
303
303
  os.unlink(ip)
304
304
  srv.bind(ip)
305
+ if uds_gid != -1:
306
+ os.chown(ip, -1, uds_gid)
307
+ if uds_perm != -1:
308
+ os.chmod(ip, uds_perm)
305
309
  else:
306
310
  tf = "%s.%d" % (ip, os.getpid())
307
311
  if os.path.exists(tf):
copyparty/up2k.py CHANGED
@@ -410,10 +410,11 @@ class Up2k(object):
410
410
 
411
411
  ret = []
412
412
  userset = set([(uname or "\n"), "*"])
413
+ e_d = {}
413
414
  n = 1000
414
415
  try:
415
416
  for ptop, tab2 in self.registry.items():
416
- cfg = self.flags.get(ptop, {}).get("u2abort", 1)
417
+ cfg = self.flags.get(ptop, e_d).get("u2abort", 1)
417
418
  if not cfg:
418
419
  continue
419
420
  addr = (ip or "\n") if cfg in (1, 2) else ""
@@ -1131,7 +1132,7 @@ class Up2k(object):
1131
1132
  ft = "\033[0;32m{}{:.0}"
1132
1133
  ff = "\033[0;35m{}{:.0}"
1133
1134
  fv = "\033[0;36m{}:\033[90m{}"
1134
- zs = "ext_th_d html_head put_name2 mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
1135
+ zs = "du_iwho ext_th_d html_head put_name2 mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
1135
1136
  fx = set(zs.split())
1136
1137
  fd = vf_bmap()
1137
1138
  fd.update(vf_cmap())
@@ -1485,6 +1486,7 @@ class Up2k(object):
1485
1486
  files = []
1486
1487
  fat32 = True
1487
1488
  cv = vcv = acv = ""
1489
+ e_d = {}
1488
1490
 
1489
1491
  th_cvd = self.args.th_coversd
1490
1492
  th_cvds = self.args.th_coversd_set
@@ -1684,7 +1686,7 @@ class Up2k(object):
1684
1686
 
1685
1687
  t = "reindex %r => %r mtime(%s/%s) size(%s/%s)"
1686
1688
  self.log(t % (top, rp, dts, lmod, dsz, sz))
1687
- self.db_rm(db.c, rd, fn, 0)
1689
+ self.db_rm(db.c, e_d, rd, fn, 0)
1688
1690
  tfa += 1
1689
1691
  db.n += 1
1690
1692
  in_db = []
@@ -1721,7 +1723,7 @@ class Up2k(object):
1721
1723
  un = ""
1722
1724
 
1723
1725
  # skip upload hooks by not providing vflags
1724
- self.db_add(db.c, {}, rd, fn, lmod, sz, "", "", wark, wark, "", un, ip, at)
1726
+ self.db_add(db.c, e_d, rd, fn, lmod, sz, "", "", wark, wark, "", un, ip, at)
1725
1727
  db.n += 1
1726
1728
  db.nf += 1
1727
1729
  tfa += 1
@@ -1780,7 +1782,7 @@ class Up2k(object):
1780
1782
  rm_files = [x for x in hits if x not in seen_files]
1781
1783
  n_rm = len(rm_files)
1782
1784
  for fn in rm_files:
1783
- self.db_rm(db.c, rd, fn, 0)
1785
+ self.db_rm(db.c, e_d, rd, fn, 0)
1784
1786
 
1785
1787
  if n_rm:
1786
1788
  self.log("forgot {} deleted files".format(n_rm))
@@ -3126,7 +3128,7 @@ class Up2k(object):
3126
3128
  for cur, dp_dir, dp_fn in lost:
3127
3129
  t = "forgetting desynced db entry: %r"
3128
3130
  self.log(t % ("/" + vjoin(vjoin(vfs.vpath, dp_dir), dp_fn)))
3129
- self.db_rm(cur, dp_dir, dp_fn, cj["size"])
3131
+ self.db_rm(cur, vfs.flags, dp_dir, dp_fn, cj["size"])
3130
3132
  if c2 and c2 != cur:
3131
3133
  c2.connection.commit()
3132
3134
 
@@ -3419,10 +3421,9 @@ class Up2k(object):
3419
3421
  cur.connection.commit()
3420
3422
 
3421
3423
  ap = djoin(job["ptop"], job["prel"], job["name"])
3422
- times = (int(time.time()), int(cj["lmod"]))
3423
- bos.utime(ap, times, False)
3424
+ mt = bos.utime_c(self.log, ap, int(cj["lmod"]), False, True)
3424
3425
 
3425
- self.log("touched %r from %d to %d" % (ap, job["lmod"], cj["lmod"]))
3426
+ self.log("touched %r from %d to %d" % (ap, job["lmod"], mt))
3426
3427
  except Exception as ex:
3427
3428
  self.log("umod failed, %r" % (ex,), 3)
3428
3429
 
@@ -3456,7 +3457,7 @@ class Up2k(object):
3456
3457
  vrel = vjoin(job["prel"], fname)
3457
3458
  xlink = bool(vf.get("xlink"))
3458
3459
  cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, vrel)
3459
- self._forget_file(ptop, vrel, cur, wark, True, st.st_size, xlink)
3460
+ self._forget_file(ptop, vrel, vf, cur, wark, True, st.st_size, xlink)
3460
3461
  except Exception as ex:
3461
3462
  self.log("skipping replace-relink: %r" % (ex,))
3462
3463
  finally:
@@ -3577,8 +3578,7 @@ class Up2k(object):
3577
3578
  shutil.copy2(fsenc(csrc), fsenc(dst))
3578
3579
 
3579
3580
  if lmod and (not linked or SYMTIME):
3580
- times = (int(time.time()), int(lmod))
3581
- bos.utime(dst, times, False)
3581
+ bos.utime_c(self.log, dst, int(lmod), False)
3582
3582
 
3583
3583
  def handle_chunks(
3584
3584
  self, ptop , wark , chashes
@@ -3750,10 +3750,8 @@ class Up2k(object):
3750
3750
  times = (int(time.time()), int(job["lmod"]))
3751
3751
  t = "no more chunks, setting times %s (%d) on %r"
3752
3752
  self.log(t % (times, bos.path.getsize(dst), dst))
3753
- try:
3754
- bos.utime(dst, times)
3755
- except:
3756
- self.log("failed to utime (%r, %s)" % (dst, times))
3753
+ bos.utime_c(self.log, dst, times[1], False)
3754
+ # the above logmsg (and associated logic) is retained due to unforget.py
3757
3755
 
3758
3756
  zs = "prel name lmod size ptop vtop wark dwrk host user addr"
3759
3757
  z2 = [job[x] for x in zs.split()]
@@ -3871,16 +3869,31 @@ class Up2k(object):
3871
3869
 
3872
3870
  return True
3873
3871
 
3874
- def db_rm(self, db , rd , fn , sz ) :
3872
+ def db_rm(
3873
+ self, db , vflags , rd , fn , sz
3874
+ ) :
3875
3875
  sql = "delete from up where rd = ? and fn = ?"
3876
3876
  try:
3877
3877
  r = db.execute(sql, (rd, fn))
3878
3878
  except:
3879
3879
  r = db.execute(sql, s3enc(self.mem_cur, rd, fn))
3880
3880
 
3881
- if r.rowcount:
3882
- self.volsize[db] -= sz
3883
- self.volnfiles[db] -= 1
3881
+ if not r.rowcount:
3882
+ return
3883
+
3884
+ self.volsize[db] -= sz
3885
+ self.volnfiles[db] -= 1
3886
+
3887
+ if "nodirsz" not in vflags:
3888
+ try:
3889
+ q = "update ds set nf=nf-1, sz=sz-? where rd=?"
3890
+ while True:
3891
+ db.execute(q, (sz, rd))
3892
+ if not rd:
3893
+ break
3894
+ rd = rd.rsplit("/", 1)[0] if "/" in rd else ""
3895
+ except:
3896
+ pass
3884
3897
 
3885
3898
  def db_add(
3886
3899
  self,
@@ -3901,7 +3914,7 @@ class Up2k(object):
3901
3914
  skip_xau = False,
3902
3915
  ) :
3903
3916
  """mutex(main) me"""
3904
- self.db_rm(db, rd, fn, sz)
3917
+ self.db_rm(db, vflags, rd, fn, sz)
3905
3918
 
3906
3919
  if not ip:
3907
3920
  db_ip = ""
@@ -4182,7 +4195,7 @@ class Up2k(object):
4182
4195
  xlink = bool(dbv.flags.get("xlink"))
4183
4196
  cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, volpath)
4184
4197
  self._forget_file(
4185
- ptop, volpath, cur, wark, True, st.st_size, xlink
4198
+ ptop, volpath, dbv.flags, cur, wark, True, st.st_size, xlink
4186
4199
  )
4187
4200
  finally:
4188
4201
  if cur:
@@ -4638,7 +4651,14 @@ class Up2k(object):
4638
4651
 
4639
4652
  with self.reg_mutex:
4640
4653
  has_dupes = self._forget_file(
4641
- svn.realpath, srem, c1, w, is_xvol, fsize_ or fsize, xlink
4654
+ svn.realpath,
4655
+ srem,
4656
+ svn.flags,
4657
+ c1,
4658
+ w,
4659
+ is_xvol,
4660
+ fsize_ or fsize,
4661
+ xlink,
4642
4662
  )
4643
4663
 
4644
4664
  if not is_xvol:
@@ -4782,6 +4802,7 @@ class Up2k(object):
4782
4802
  self,
4783
4803
  ptop ,
4784
4804
  vrem ,
4805
+ vflags ,
4785
4806
  cur ,
4786
4807
  wark ,
4787
4808
  drop_tags ,
@@ -4806,7 +4827,7 @@ class Up2k(object):
4806
4827
  q = "delete from mt where w=?"
4807
4828
  cur.execute(q, (wark[:16],))
4808
4829
 
4809
- self.db_rm(cur, srd, sfn, sz)
4830
+ self.db_rm(cur, vflags, srd, sfn, sz)
4810
4831
 
4811
4832
  reg = self.registry.get(ptop)
4812
4833
  if reg:
@@ -4895,7 +4916,10 @@ class Up2k(object):
4895
4916
  mt = bos.path.getmtime(slabs, False)
4896
4917
  flags = self.flags.get(ptop) or {}
4897
4918
  atomic_move(self.log, sabs, slabs, flags)
4898
- bos.utime(slabs, (int(time.time()), int(mt)), False)
4919
+ try:
4920
+ bos.utime(slabs, (int(time.time()), int(mt)), False)
4921
+ except:
4922
+ self.log("relink: failed to utime(%r, %s)" % (slabs, mt), 3)
4899
4923
  self._symlink(slabs, sabs, flags, False, is_mv=True)
4900
4924
  full[slabs] = (ptop, rem)
4901
4925
  sabs = slabs
copyparty/util.py CHANGED
@@ -3102,8 +3102,9 @@ def statdir(
3102
3102
  else:
3103
3103
  src = "listdir"
3104
3104
  fun = os.lstat if lstat else os.stat
3105
+ btop_ = os.path.join(btop, b"")
3105
3106
  for name in os.listdir(btop):
3106
- abspath = os.path.join(btop, name)
3107
+ abspath = btop_ + name
3107
3108
  try:
3108
3109
  yield (fsdec(name), fun(abspath))
3109
3110
  except Exception as ex:
@@ -3142,7 +3143,9 @@ def rmdirs(
3142
3143
 
3143
3144
  stats = statdir(logger, scandir, lstat, top, False)
3144
3145
  dirs = [x[0] for x in stats if stat.S_ISDIR(x[1].st_mode)]
3145
- dirs = [os.path.join(top, x) for x in dirs]
3146
+ if dirs:
3147
+ top_ = os.path.join(top, "")
3148
+ dirs = [top_ + x for x in dirs]
3146
3149
  ok = []
3147
3150
  ng = []
3148
3151
  for d in reversed(dirs):
@@ -4113,7 +4116,7 @@ def _pkg_resource_exists(pkg , name ) :
4113
4116
 
4114
4117
 
4115
4118
  def stat_resource(E , name ):
4116
- path = os.path.join(E.mod, name)
4119
+ path = E.mod_ + name
4117
4120
  if os.path.exists(path):
4118
4121
  return os.stat(fsenc(path))
4119
4122
  return None
@@ -4158,7 +4161,7 @@ def _has_resource(name ):
4158
4161
 
4159
4162
 
4160
4163
  def has_resource(E , name ):
4161
- return _has_resource(name) or os.path.exists(os.path.join(E.mod, name))
4164
+ return _has_resource(name) or os.path.exists(E.mod_ + name)
4162
4165
 
4163
4166
 
4164
4167
  def load_resource(E , name , mode="rb") :
@@ -4181,7 +4184,7 @@ def load_resource(E , name , mode="rb") :
4181
4184
  stream = codecs.getreader(enc)(stream)
4182
4185
  return stream
4183
4186
 
4184
- ap = os.path.join(E.mod, name)
4187
+ ap = E.mod_ + name
4185
4188
 
4186
4189
  if PY2:
4187
4190
  return codecs.open(ap, "r", encoding=enc) # type: ignore
@@ -6,8 +6,8 @@ __copyright__ = 2019
6
6
  __license__ = "MIT"
7
7
  __url__ = "https://github.com/9001/copyparty/"
8
8
 
9
- S_VERSION = "2.0"
10
- S_BUILD_DT = "2024-10-01"
9
+ S_VERSION = "2.1"
10
+ S_BUILD_DT = "2025-09-06"
11
11
 
12
12
  """
13
13
  mount a copyparty server (local or remote) as a filesystem
@@ -95,7 +95,7 @@ except:
95
95
  elif MACOS:
96
96
  libfuse = "install https://osxfuse.github.io/"
97
97
  else:
98
- libfuse = "apt install libfuse3-3\n modprobe fuse"
98
+ libfuse = "apt install libfuse2\n modprobe fuse"
99
99
 
100
100
  m = """\033[33m
101
101
  could not import fuse; these may help:
@@ -324,7 +324,7 @@ class Gateway(object):
324
324
  def sendreq(self, meth, path, headers, **kwargs):
325
325
  tid = get_tid()
326
326
  if self.password:
327
- headers["Cookie"] = "=".join(["cppwd", self.password])
327
+ headers["PW"] = self.password
328
328
 
329
329
  try:
330
330
  c = self.getconn(tid)
@@ -682,9 +682,7 @@ class CPPF(Operations):
682
682
  return ret
683
683
 
684
684
  def _readdir(self, path, fh=None):
685
- path = path.strip("/")
686
- dbg("readdir %r [%s]", path, fh)
687
-
685
+ dbg("dircache miss")
688
686
  ret = self.gw.listdir(path)
689
687
  if not self.n_dircache:
690
688
  return ret
@@ -694,11 +692,17 @@ class CPPF(Operations):
694
692
  self.dircache.append(cn)
695
693
  self.clean_dircache()
696
694
 
697
- # import pprint; pprint.pprint(ret)
698
695
  return ret
699
696
 
700
697
  def readdir(self, path, fh=None):
701
- return [".", ".."] + list(self._readdir(path, fh))
698
+ dbg("readdir %r [%s]", path, fh)
699
+ path = path.strip("/")
700
+ cn = self.get_cached_dir(path)
701
+ if cn:
702
+ ret = cn.data
703
+ else:
704
+ ret = self._readdir(path, fh)
705
+ return [".", ".."] + list(ret)
702
706
 
703
707
  def read(self, path, length, offset, fh=None):
704
708
  req_max = 1024 * 1024 * 8
@@ -749,7 +753,6 @@ class CPPF(Operations):
749
753
  if cn:
750
754
  dents = cn.data
751
755
  else:
752
- dbg("cache miss")
753
756
  dents = self._readdir(dirpath)
754
757
 
755
758
  try:
@@ -857,10 +860,15 @@ def main():
857
860
  if WINDOWS:
858
861
  examples.append("http://192.168.1.69:3923/music/ M:")
859
862
 
863
+ epi = "example:" + ex_pre + ex_pre.join(examples)
864
+ epi += """\n
865
+ NOTE: if server has --usernames enabled, then password is "username:password"
866
+ """
867
+
860
868
  ap = argparse.ArgumentParser(
861
869
  formatter_class=TheArgparseFormatter,
862
870
  description="mount a copyparty server as a local filesystem -- " + ver,
863
- epilog="example:" + ex_pre + ex_pre.join(examples),
871
+ epilog=epi,
864
872
  )
865
873
  # fmt: off
866
874
  ap.add_argument("base_url", type=str, help="remote copyparty URL to mount")
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.12"
5
- S_BUILD_DT = "2025-08-26"
4
+ S_VERSION = "2.13"
5
+ S_BUILD_DT = "2025-09-05"
6
6
 
7
7
  """
8
8
  u2c.py: upload to copyparty
@@ -588,9 +588,10 @@ def undns(url):
588
588
 
589
589
  def _scd(err, top):
590
590
  """non-recursive listing of directory contents, along with stat() info"""
591
+ top_ = os.path.join(top, b"")
591
592
  with os.scandir(top) as dh:
592
593
  for fh in dh:
593
- abspath = os.path.join(top, fh.name)
594
+ abspath = top_ + fh.name
594
595
  try:
595
596
  yield [abspath, fh.stat()]
596
597
  except Exception as ex:
@@ -599,8 +600,9 @@ def _scd(err, top):
599
600
 
600
601
  def _lsd(err, top):
601
602
  """non-recursive listing of directory contents, along with stat() info"""
603
+ top_ = os.path.join(top, b"")
602
604
  for name in os.listdir(top):
603
- abspath = os.path.join(top, name)
605
+ abspath = top_ + name
604
606
  try:
605
607
  yield [abspath, os.stat(abspath)]
606
608
  except Exception as ex:
Binary file
Binary file
@@ -9,7 +9,7 @@
9
9
  <meta name="theme-color" content="#{{ tcolor }}">
10
10
  <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
11
11
  <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/browser.css?_={{ ts }}">
12
- {{ html_head }}
12
+ {{- html_head }}
13
13
  {%- if css %}
14
14
  <link rel="stylesheet" media="screen" href="{{ css }}_={{ ts }}">
15
15
  {%- endif %}
Binary file