copyparty 1.19.16__py3-none-any.whl → 1.19.18__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 (55) hide show
  1. copyparty/__init__.py +25 -1
  2. copyparty/__main__.py +32 -9
  3. copyparty/__version__.py +2 -2
  4. copyparty/authsrv.py +78 -31
  5. copyparty/bos/bos.py +5 -1
  6. copyparty/cfg.py +20 -0
  7. copyparty/ftpd.py +6 -4
  8. copyparty/httpcli.py +166 -45
  9. copyparty/httpsrv.py +2 -2
  10. copyparty/mtag.py +2 -2
  11. copyparty/smbd.py +1 -1
  12. copyparty/svchub.py +3 -0
  13. copyparty/tftpd.py +1 -1
  14. copyparty/up2k.py +39 -15
  15. copyparty/util.py +15 -5
  16. copyparty/web/a/partyfuse.py.gz +0 -0
  17. copyparty/web/a/u2c.py.gz +0 -0
  18. copyparty/web/a/webdav-cfg.txt.gz +0 -0
  19. copyparty/web/baguettebox.js.gz +0 -0
  20. copyparty/web/browser.css.gz +0 -0
  21. copyparty/web/browser.html +3 -0
  22. copyparty/web/browser.js.gz +0 -0
  23. copyparty/web/splash.html +3 -0
  24. copyparty/web/splash.js.gz +0 -0
  25. copyparty/web/svcs.html +1 -1
  26. copyparty/web/tl/chi.js.gz +0 -0
  27. copyparty/web/tl/cze.js.gz +0 -0
  28. copyparty/web/tl/deu.js.gz +0 -0
  29. copyparty/web/tl/epo.js.gz +0 -0
  30. copyparty/web/tl/fin.js.gz +0 -0
  31. copyparty/web/tl/fra.js.gz +0 -0
  32. copyparty/web/tl/grc.js.gz +0 -0
  33. copyparty/web/tl/ita.js.gz +0 -0
  34. copyparty/web/tl/kor.js.gz +0 -0
  35. copyparty/web/tl/nld.js.gz +0 -0
  36. copyparty/web/tl/nno.js.gz +0 -0
  37. copyparty/web/tl/nor.js.gz +0 -0
  38. copyparty/web/tl/pol.js.gz +0 -0
  39. copyparty/web/tl/por.js.gz +0 -0
  40. copyparty/web/tl/rus.js.gz +0 -0
  41. copyparty/web/tl/spa.js.gz +0 -0
  42. copyparty/web/tl/swe.js.gz +0 -0
  43. copyparty/web/tl/tur.js.gz +0 -0
  44. copyparty/web/tl/ukr.js.gz +0 -0
  45. copyparty/web/up2k.js.gz +0 -0
  46. copyparty/web/util.js.gz +0 -0
  47. {copyparty-1.19.16.dist-info → copyparty-1.19.18.dist-info}/METADATA +26 -4
  48. {copyparty-1.19.16.dist-info → copyparty-1.19.18.dist-info}/RECORD +52 -33
  49. copyparty/web/a/partyfuse.py +0 -947
  50. copyparty/web/a/u2c.py +0 -1718
  51. copyparty/web/a/webdav-cfg.bat +0 -45
  52. {copyparty-1.19.16.dist-info → copyparty-1.19.18.dist-info}/WHEEL +0 -0
  53. {copyparty-1.19.16.dist-info → copyparty-1.19.18.dist-info}/entry_points.txt +0 -0
  54. {copyparty-1.19.16.dist-info → copyparty-1.19.18.dist-info}/licenses/LICENSE +0 -0
  55. {copyparty-1.19.16.dist-info → copyparty-1.19.18.dist-info}/top_level.txt +0 -0
copyparty/httpcli.py CHANGED
@@ -30,7 +30,7 @@ try:
30
30
  except:
31
31
  pass
32
32
 
33
- from .__init__ import ANYWIN, RES, TYPE_CHECKING, EnvParams, unicode
33
+ from .__init__ import ANYWIN, RES, RESM, TYPE_CHECKING, EnvParams, unicode
34
34
  from .__version__ import S_VERSION
35
35
  from .authsrv import LEELOO_DALLAS, VFS # typechk
36
36
  from .bos import bos
@@ -161,6 +161,12 @@ RE_MDV = re.compile(r"(.*)\.([0-9]+\.[0-9]{3})(\.[Mm][Dd])$")
161
161
 
162
162
  UPARAM_CC_OK = set("doc move tree".split())
163
163
 
164
+ PERMS_rwh = [
165
+ [True, False],
166
+ [False, True],
167
+ [False, False, False, False, False, False, True],
168
+ ]
169
+
164
170
 
165
171
  class HttpCli(object):
166
172
  """
@@ -224,6 +230,7 @@ class HttpCli(object):
224
230
  self.can_delete = False
225
231
  self.can_get = False
226
232
  self.can_upget = False
233
+ self.can_html = False
227
234
  self.can_admin = False
228
235
  self.can_dot = False
229
236
  self.out_headerlist = []
@@ -270,7 +277,7 @@ class HttpCli(object):
270
277
  tpl = self.conn.hsrv.j2[name]
271
278
  ka["r"] = self.args.SR if self.is_vproxied else ""
272
279
  ka["ts"] = self.conn.hsrv.cachebuster()
273
- ka["lang"] = self.args.lang
280
+ ka["lang"] = self.cookies.get("cplng") or self.args.lang
274
281
  ka["favico"] = self.args.favico
275
282
  ka["s_doctitle"] = self.args.doctitle
276
283
  ka["tcolor"] = self.vn.flags["tcolor"]
@@ -732,18 +739,21 @@ class HttpCli(object):
732
739
  if "bcasechk" in vn.flags and not vn.casechk(rem, True):
733
740
  return self.tx_404() and False
734
741
 
735
- (
736
- self.can_read,
737
- self.can_write,
738
- self.can_move,
739
- self.can_delete,
740
- self.can_get,
741
- self.can_upget,
742
- self.can_admin,
743
- self.can_dot,
744
- ) = (
745
- avn.can_access("", self.uname) if avn else [False] * 8
746
- )
742
+ try:
743
+ (
744
+ self.can_read,
745
+ self.can_write,
746
+ self.can_move,
747
+ self.can_delete,
748
+ self.can_get,
749
+ self.can_upget,
750
+ self.can_html,
751
+ self.can_admin,
752
+ self.can_dot,
753
+ ) = avn.uaxs[self.uname]
754
+ except:
755
+ pass # default is all-false
756
+
747
757
  self.avn = avn
748
758
  self.vn = vn # note: do not dbv due to walk/zipgen
749
759
  self.rem = rem
@@ -857,6 +867,16 @@ class HttpCli(object):
857
867
  return self.conn.iphash.s(self.ip)
858
868
 
859
869
  def cbonk(self, g , v , reason , descr ) :
870
+ cond = self.args.dont_ban
871
+ if (
872
+ cond == "any"
873
+ or (cond == "auth" and self.uname != "*")
874
+ or (cond == "aa" and self.avol)
875
+ or (cond == "av" and self.can_admin)
876
+ or (cond == "rw" and self.can_read and self.can_write)
877
+ ):
878
+ return False
879
+
860
880
  self.conn.hsrv.nsus += 1
861
881
  if not g.lim:
862
882
  return False
@@ -881,7 +901,7 @@ class HttpCli(object):
881
901
  0,
882
902
  self.ip,
883
903
  time.time(),
884
- reason,
904
+ [reason, reason],
885
905
  ):
886
906
  self.log("client banned: %s" % (descr,), 1)
887
907
  self.conn.hsrv.bans[ip] = bonk
@@ -1260,6 +1280,20 @@ class HttpCli(object):
1260
1280
  else:
1261
1281
  return self.tx_res(res_path)
1262
1282
 
1283
+ if res_path in RESM:
1284
+ ap = self.E.mod_ + RESM[res_path]
1285
+ if (
1286
+ "txt" not in self.uparam
1287
+ and "mime" not in self.uparam
1288
+ and not self.ouparam.get("dl")
1289
+ ):
1290
+ # return mimetype matching request extension
1291
+ self.ouparam["dl"] = res_path.split("/")[-1]
1292
+ if bos.path.exists(ap) or bos.path.exists(ap + ".gz"):
1293
+ return self.tx_file(ap)
1294
+ else:
1295
+ return self.tx_res(res_path)
1296
+
1263
1297
  self.tx_404()
1264
1298
  return False
1265
1299
 
@@ -1516,6 +1550,64 @@ class HttpCli(object):
1516
1550
  self.log("rss: %d hits, %d bytes" % (len(hits), len(bret)))
1517
1551
  return True
1518
1552
 
1553
+ def tx_zls(self, abspath) :
1554
+ if self.do_log:
1555
+ self.log("zls %s @%s" % (self.req, self.uname))
1556
+ if self.args.no_zls:
1557
+ raise Pebkac(405, "zip browsing is disabled in server config")
1558
+
1559
+ import zipfile
1560
+
1561
+ try:
1562
+ with zipfile.ZipFile(abspath, "r") as zf:
1563
+ filelist = [{"fn": f.filename} for f in zf.infolist()]
1564
+ ret = json.dumps(filelist).encode("utf-8", "replace")
1565
+ self.reply(ret, mime="application/json")
1566
+ return True
1567
+ except (zipfile.BadZipfile, RuntimeError):
1568
+ raise Pebkac(404, "requested file is not a valid zip file")
1569
+
1570
+ def tx_zget(self, abspath) :
1571
+ maxsz = 1024 * 1024 * 64
1572
+
1573
+ inner_path = self.uparam.get("zget")
1574
+ if not inner_path:
1575
+ raise Pebkac(405, "inner path is required")
1576
+ if self.do_log:
1577
+ self.log(
1578
+ "zget %s \033[35m%s\033[0m @%s" % (self.req, inner_path, self.uname)
1579
+ )
1580
+ if self.args.no_zls:
1581
+ raise Pebkac(405, "zip browsing is disabled in server config")
1582
+
1583
+ import zipfile
1584
+
1585
+ try:
1586
+ with zipfile.ZipFile(abspath, "r") as zf:
1587
+ zi = zf.getinfo(inner_path)
1588
+ if zi.file_size >= maxsz:
1589
+ raise Pebkac(404, "zip bomb defused")
1590
+ with zf.open(zi, "r") as fi:
1591
+ self.send_headers(length=zi.file_size, mime=guess_mime(inner_path))
1592
+
1593
+ sendfile_py(
1594
+ self.log,
1595
+ 0,
1596
+ zi.file_size,
1597
+ fi,
1598
+ self.s,
1599
+ self.args.s_wr_sz,
1600
+ self.args.s_wr_slp,
1601
+ not self.args.no_poll,
1602
+ {},
1603
+ "",
1604
+ )
1605
+ except KeyError:
1606
+ raise Pebkac(404, "no such file in archive")
1607
+ except (zipfile.BadZipfile, RuntimeError):
1608
+ raise Pebkac(404, "requested file is not a valid zip file")
1609
+ return True
1610
+
1519
1611
  def handle_propfind(self) :
1520
1612
  if self.do_log:
1521
1613
  self.log("PFIND %s @%s" % (self.req, self.uname))
@@ -2079,7 +2171,7 @@ class HttpCli(object):
2079
2171
  t = "urlform_raw %d @ %r\n %r\n"
2080
2172
  self.log(t % (len(orig), "/" + self.vpath, orig))
2081
2173
  try:
2082
- zb = unquote(buf.replace(b"+", b" "))
2174
+ zb = unquote(buf.replace(b"+", b" ").replace(b"&", b"\n"))
2083
2175
  plain = zb.decode("utf-8", "replace")
2084
2176
  if buf.startswith(b"msg="):
2085
2177
  plain = plain[4:]
@@ -2100,7 +2192,7 @@ class HttpCli(object):
2100
2192
  len(buf),
2101
2193
  self.ip,
2102
2194
  time.time(),
2103
- plain,
2195
+ [plain, orig],
2104
2196
  )
2105
2197
 
2106
2198
  t = "urlform_dec %d @ %r\n %r\n"
@@ -2259,7 +2351,7 @@ class HttpCli(object):
2259
2351
  remains,
2260
2352
  self.ip,
2261
2353
  at,
2262
- "",
2354
+ None,
2263
2355
  )
2264
2356
  t = hr.get("rejectmsg") or ""
2265
2357
  if t or not hr:
@@ -2394,7 +2486,7 @@ class HttpCli(object):
2394
2486
  post_sz,
2395
2487
  self.ip,
2396
2488
  at,
2397
- "",
2489
+ None,
2398
2490
  )
2399
2491
  t = hr.get("rejectmsg") or ""
2400
2492
  if t or not hr:
@@ -3226,7 +3318,7 @@ class HttpCli(object):
3226
3318
  0,
3227
3319
  self.ip,
3228
3320
  time.time(),
3229
- "",
3321
+ None,
3230
3322
  )
3231
3323
  t = hr.get("rejectmsg") or ""
3232
3324
  if t or not hr:
@@ -3398,7 +3490,7 @@ class HttpCli(object):
3398
3490
  0,
3399
3491
  self.ip,
3400
3492
  at,
3401
- "",
3493
+ None,
3402
3494
  )
3403
3495
  t = hr.get("rejectmsg") or ""
3404
3496
  if t or not hr:
@@ -3505,7 +3597,7 @@ class HttpCli(object):
3505
3597
  sz,
3506
3598
  self.ip,
3507
3599
  at,
3508
- "",
3600
+ None,
3509
3601
  )
3510
3602
  t = hr.get("rejectmsg") or ""
3511
3603
  if t or not hr:
@@ -3816,7 +3908,7 @@ class HttpCli(object):
3816
3908
  0,
3817
3909
  self.ip,
3818
3910
  time.time(),
3819
- "",
3911
+ None,
3820
3912
  )
3821
3913
  t = hr.get("rejectmsg") or ""
3822
3914
  if t or not hr:
@@ -3864,7 +3956,7 @@ class HttpCli(object):
3864
3956
  sz,
3865
3957
  self.ip,
3866
3958
  new_lastmod,
3867
- "",
3959
+ None,
3868
3960
  )
3869
3961
  t = hr.get("rejectmsg") or ""
3870
3962
  if t or not hr:
@@ -4093,8 +4185,11 @@ class HttpCli(object):
4093
4185
  # force download
4094
4186
 
4095
4187
  if "dl" in self.ouparam:
4096
- cdis = gen_content_disposition(os.path.basename(req_path))
4097
- self.out_headers["Content-Disposition"] = cdis
4188
+ cdis = self.ouparam["dl"] or req_path
4189
+ zs = gen_content_disposition(os.path.basename(cdis))
4190
+ self.out_headers["Content-Disposition"] = zs
4191
+ else:
4192
+ cdis = req_path
4098
4193
 
4099
4194
  #
4100
4195
  # if-modified
@@ -4160,7 +4255,7 @@ class HttpCli(object):
4160
4255
  elif "mime" in self.uparam:
4161
4256
  mime = str(self.uparam.get("mime"))
4162
4257
  else:
4163
- mime = guess_mime(req_path)
4258
+ mime = guess_mime(cdis)
4164
4259
 
4165
4260
  logmsg += unicode(status) + logtail
4166
4261
 
@@ -4267,8 +4362,11 @@ class HttpCli(object):
4267
4362
  # force download
4268
4363
 
4269
4364
  if "dl" in self.ouparam:
4270
- cdis = gen_content_disposition(os.path.basename(req_path))
4271
- self.out_headers["Content-Disposition"] = cdis
4365
+ cdis = self.ouparam["dl"] or req_path
4366
+ zs = gen_content_disposition(os.path.basename(cdis))
4367
+ self.out_headers["Content-Disposition"] = zs
4368
+ else:
4369
+ cdis = req_path
4272
4370
 
4273
4371
  #
4274
4372
  # if-modified
@@ -4396,7 +4494,7 @@ class HttpCli(object):
4396
4494
  elif "rmagic" in self.vn.flags:
4397
4495
  mime = guess_mime(req_path, fs_path)
4398
4496
  else:
4399
- mime = guess_mime(req_path)
4497
+ mime = guess_mime(cdis)
4400
4498
 
4401
4499
  if "nohtml" in self.vn.flags and "html" in mime:
4402
4500
  mime = "text/plain; charset=utf-8"
@@ -4998,7 +5096,7 @@ class HttpCli(object):
4998
5096
  "edit": "edit" in self.uparam,
4999
5097
  "title": html_escape(self.vpath, crlf=True),
5000
5098
  "lastmod": int(ts_md * 1000),
5001
- "lang": self.args.lang,
5099
+ "lang": self.cookies.get("cplng") or self.args.lang,
5002
5100
  "favico": self.args.favico,
5003
5101
  "have_emp": int(self.args.emp),
5004
5102
  "md_no_br": int(vn.flags.get("md_no_br") or 0),
@@ -5170,7 +5268,7 @@ class HttpCli(object):
5170
5268
  dls.append((perc, hsent, spd, eta, idle, usr, erd, rds, fn))
5171
5269
 
5172
5270
  if self.args.have_unlistc:
5173
- allvols = self.asrv.vfs.all_vols
5271
+ allvols = self.asrv.vfs.all_nodes
5174
5272
  rvol = [x for x in rvol if "unlistcr" not in allvols[x[1:-1]].flags]
5175
5273
  wvol = [x for x in wvol if "unlistcw" not in allvols[x[1:-1]].flags]
5176
5274
 
@@ -5383,13 +5481,20 @@ class HttpCli(object):
5383
5481
  return self.redirect("", "?h", x.get(), "return to", False)
5384
5482
 
5385
5483
  def tx_stack(self) :
5386
- if not self.avol and not [x for x in self.wvol if x in self.rvol]:
5484
+ zs = self.args.stack_who
5485
+ if zs == "all" or (
5486
+ (zs == "a" and self.avol)
5487
+ or (zs == "rw" and [x for x in self.wvol if x in self.rvol])
5488
+ ):
5489
+ pass
5490
+ else:
5387
5491
  raise Pebkac(403, "'stack' not allowed for user " + self.uname)
5388
5492
 
5389
- if self.args.no_stack:
5390
- raise Pebkac(403, "the stackdump feature is disabled in server config")
5391
-
5392
- ret = "<pre>{}\n{}".format(time.time(), html_escape(alltrace()))
5493
+ ret = html_escape(alltrace(self.args.stack_v))
5494
+ if self.args.stack_v:
5495
+ ret = "<pre>%s\n%s" % (time.time(), ret)
5496
+ else:
5497
+ ret = "<pre>%s" % (ret,)
5393
5498
  self.reply(ret.encode("utf-8"))
5394
5499
  return True
5395
5500
 
@@ -5442,7 +5547,7 @@ class HttpCli(object):
5442
5547
  rem,
5443
5548
  self.uname,
5444
5549
  not self.args.no_scandir,
5445
- [[True, False], [False, True]],
5550
+ PERMS_rwh,
5446
5551
  )
5447
5552
  dots = self.uname in vn.axs.udot
5448
5553
  dk_sz = vn.flags.get("dk")
@@ -5474,7 +5579,13 @@ class HttpCli(object):
5474
5579
  for x in vfs_virt:
5475
5580
  if x != excl:
5476
5581
  try:
5477
- dvn, drem = vfs.get(vjoin(top, x), self.uname, True, False)
5582
+ dvn, drem = vfs.get(vjoin(top, x), self.uname, False, False)
5583
+ if (
5584
+ self.uname not in dvn.axs.uread
5585
+ and self.uname not in dvn.axs.uwrite
5586
+ and self.uname not in dvn.axs.uhtml
5587
+ ):
5588
+ raise Exception()
5478
5589
  bos.stat(dvn.canonical(drem, False))
5479
5590
  except:
5480
5591
  x += "\n"
@@ -6399,8 +6510,7 @@ class HttpCli(object):
6399
6510
  return self.tx_svg("upload\nonly")
6400
6511
 
6401
6512
  if not self.can_read and self.can_get and self.avn:
6402
- axs = self.avn.axs
6403
- if self.uname not in axs.uhtml:
6513
+ if not self.can_html:
6404
6514
  pass
6405
6515
  elif is_dir:
6406
6516
  for fn in ("index.htm", "index.html"):
@@ -6421,6 +6531,7 @@ class HttpCli(object):
6421
6531
 
6422
6532
  fk_pass = True
6423
6533
  is_dir = False
6534
+ add_og = False
6424
6535
  rem = vjoin(rem, fn)
6425
6536
  vrem = vjoin(vrem, fn)
6426
6537
  abspath = ap2
@@ -6456,14 +6567,23 @@ class HttpCli(object):
6456
6567
  ):
6457
6568
  return self.tx_md(vn, abspath)
6458
6569
 
6570
+ if "zls" in self.uparam:
6571
+ return self.tx_zls(abspath)
6572
+ if "zget" in self.uparam:
6573
+ return self.tx_zget(abspath)
6574
+
6459
6575
  if not add_og or not og_fn:
6460
- return self.tx_file(
6461
- abspath, None if st.st_size or "nopipe" in vn.flags else vn.realpath
6462
- )
6576
+ if st.st_size or "nopipe" in vn.flags:
6577
+ return self.tx_file(abspath, None)
6578
+ else:
6579
+ return self.tx_file(abspath, vn.get_dbv("")[0].realpath)
6463
6580
 
6464
6581
  elif is_dir and not self.can_read:
6465
6582
  if use_dirkey:
6466
6583
  is_dk = True
6584
+ elif self.can_get and "doc" in self.uparam:
6585
+ zs = vjoin(self.vpath, self.uparam["doc"]) + "?v"
6586
+ return self.redirect(zs, flavor="redirecting to", use302=True)
6467
6587
  elif not self.can_write:
6468
6588
  return self.tx_404(True)
6469
6589
 
@@ -6545,6 +6665,7 @@ class HttpCli(object):
6545
6665
  "acct": self.uname,
6546
6666
  "perms": perms,
6547
6667
  }
6668
+ # also see `js_htm` in authsrv.py
6548
6669
  j2a = {
6549
6670
  "cgv1": vn.js_htm,
6550
6671
  "cgv": cgv,
@@ -6604,7 +6725,7 @@ class HttpCli(object):
6604
6725
  rem,
6605
6726
  self.uname,
6606
6727
  not self.args.no_scandir,
6607
- [[True, False], [False, True]],
6728
+ PERMS_rwh,
6608
6729
  lstat="lt" in self.uparam,
6609
6730
  throw=True,
6610
6731
  )
copyparty/httpsrv.py CHANGED
@@ -378,8 +378,8 @@ class HttpSrv(object):
378
378
  if nloris < nconn / 2:
379
379
  continue
380
380
 
381
- t = "slowloris (idle-conn): {} banned for {} min"
382
- self.log(self.name, t.format(ip, self.args.loris, nclose), 1)
381
+ t = "slow%s (idle-conn): %s banned for %d min" # slowloris
382
+ self.log(self.name, t % ("loris", ip, self.args.loris), 1)
383
383
  self.bans[ip] = int(time.time() + self.args.loris * 60)
384
384
 
385
385
  if self.args.log_conn:
copyparty/mtag.py CHANGED
@@ -162,12 +162,12 @@ def au_unpk(
162
162
  znil = [x for x in znil if "cover" in x[0]] or znil
163
163
  znil = [x for x in znil if CBZ_01.search(x[0])] or znil
164
164
  t = "cbz: %d files, %d hits" % (nf, len(znil))
165
+ if not znil:
166
+ raise Exception("no images inside cbz")
165
167
  using = sorted(znil)[0][1].filename
166
168
  if znil:
167
169
  t += ", using " + using
168
170
  log(t)
169
- if not znil:
170
- raise Exception("no images inside cbz")
171
171
  fi = zf.open(using)
172
172
 
173
173
  elif pk == "epub":
copyparty/smbd.py CHANGED
@@ -259,7 +259,7 @@ class SMB(object):
259
259
  0,
260
260
  "1.7.6.2",
261
261
  time.time(),
262
- "",
262
+ None,
263
263
  )
264
264
  t = hr.get("rejectmsg") or ""
265
265
  if t or not hr:
copyparty/svchub.py CHANGED
@@ -285,6 +285,9 @@ class SvcHub(object):
285
285
  ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)]
286
286
  args.theme = "{0}{1} {0} {1}".format(ch, bri)
287
287
 
288
+ if args.no_stack:
289
+ args.stack_who = "no"
290
+
288
291
  if args.nid:
289
292
  args.du_who = "no"
290
293
  args.du_iwho = n_du_who(args.du_who)
copyparty/tftpd.py CHANGED
@@ -376,7 +376,7 @@ class Tftpd(object):
376
376
  0,
377
377
  "8.3.8.7",
378
378
  time.time(),
379
- "",
379
+ None,
380
380
  )
381
381
  t = hr.get("rejectmsg") or ""
382
382
  if t or not hr:
copyparty/up2k.py CHANGED
@@ -10,6 +10,7 @@ import re
10
10
  import shutil
11
11
  import stat
12
12
  import subprocess as sp
13
+ import sys
13
14
  import tempfile
14
15
  import threading
15
16
  import time
@@ -27,6 +28,7 @@ from .mtag import MParser, MTag
27
28
  from .util import (
28
29
  E_FS_CRIT,
29
30
  E_FS_MEH,
31
+ HAVE_FICLONE,
30
32
  HAVE_SQLITE3,
31
33
  SYMTIME,
32
34
  VF_CAREFUL,
@@ -85,6 +87,10 @@ DB_VER = 6
85
87
  if TYPE_CHECKING:
86
88
  from .svchub import SvcHub
87
89
 
90
+ USE_FICLONE = HAVE_FICLONE and sys.version_info < (3, 14)
91
+ if USE_FICLONE:
92
+ import fcntl
93
+
88
94
  zsg = "avif,avifs,bmp,gif,heic,heics,heif,heifs,ico,j2p,j2k,jp2,jpeg,jpg,jpx,png,tga,tif,tiff,webp"
89
95
  ICV_EXTS = set(zsg.split(","))
90
96
 
@@ -3282,7 +3288,7 @@ class Up2k(object):
3282
3288
  job["size"],
3283
3289
  job["addr"],
3284
3290
  job["at"],
3285
- "",
3291
+ None,
3286
3292
  )
3287
3293
  t = hr.get("rejectmsg") or ""
3288
3294
  if t or not hr:
@@ -3514,11 +3520,26 @@ class Up2k(object):
3514
3520
 
3515
3521
  linked = False
3516
3522
  try:
3517
- if "reflink" in flags:
3518
- raise Exception("reflink")
3523
+ if rm and bos.path.exists(dst):
3524
+ wunlink(self.log, dst, flags)
3525
+
3519
3526
  if not is_mv and not flags.get("dedup"):
3520
3527
  raise Exception("dedup is disabled in config")
3521
3528
 
3529
+ if "reflink" in flags:
3530
+ if not USE_FICLONE:
3531
+ raise Exception("reflink") # python 3.14 or newer; no need
3532
+ try:
3533
+ with open(fsenc(src), "rb") as fi, open(fsenc(dst), "wb") as fo:
3534
+ fcntl.ioctl(fo.fileno(), fcntl.FICLONE, fi.fileno())
3535
+ except:
3536
+ if bos.path.exists(dst):
3537
+ wunlink(self.log, dst, flags)
3538
+ raise
3539
+ if lmod:
3540
+ bos.utime_c(self.log, dst, int(lmod), False)
3541
+ return
3542
+
3522
3543
  lsrc = src
3523
3544
  ldst = dst
3524
3545
  fs1 = bos.stat(os.path.dirname(src)).st_dev
@@ -3545,9 +3566,6 @@ class Up2k(object):
3545
3566
  lsrc = lsrc.replace("/", "\\")
3546
3567
  ldst = ldst.replace("/", "\\")
3547
3568
 
3548
- if rm and bos.path.exists(dst):
3549
- wunlink(self.log, dst, flags)
3550
-
3551
3569
  try:
3552
3570
  if "hardlink" in flags:
3553
3571
  os.link(fsenc(absreal(src)), fsenc(dst))
@@ -3962,7 +3980,7 @@ class Up2k(object):
3962
3980
  sz,
3963
3981
  ip,
3964
3982
  at or time.time(),
3965
- "",
3983
+ None,
3966
3984
  )
3967
3985
  t = hr.get("rejectmsg") or ""
3968
3986
  if t or not hr:
@@ -4197,7 +4215,7 @@ class Up2k(object):
4197
4215
  st.st_size,
4198
4216
  ip,
4199
4217
  time.time(),
4200
- "",
4218
+ None,
4201
4219
  ):
4202
4220
  t = "delete blocked by xbd server config: %r"
4203
4221
  self.log(t % (abspath,), 1)
@@ -4237,7 +4255,7 @@ class Up2k(object):
4237
4255
  st.st_size,
4238
4256
  ip,
4239
4257
  time.time(),
4240
- "",
4258
+ None,
4241
4259
  )
4242
4260
 
4243
4261
  if is_dir:
@@ -4365,7 +4383,7 @@ class Up2k(object):
4365
4383
  fsize,
4366
4384
  ip,
4367
4385
  time.time(),
4368
- "",
4386
+ None,
4369
4387
  ):
4370
4388
  t = "copy blocked by xbr server config: %r" % (svp,)
4371
4389
  self.log(t, 1)
@@ -4465,7 +4483,7 @@ class Up2k(object):
4465
4483
  fsize,
4466
4484
  ip,
4467
4485
  time.time(),
4468
- "",
4486
+ None,
4469
4487
  )
4470
4488
 
4471
4489
  return "k"
@@ -4616,7 +4634,7 @@ class Up2k(object):
4616
4634
  fsize,
4617
4635
  ip,
4618
4636
  time.time(),
4619
- "",
4637
+ None,
4620
4638
  ):
4621
4639
  t = "move blocked by xbr server config: %r" % (svp,)
4622
4640
  self.log(t, 1)
@@ -4656,7 +4674,7 @@ class Up2k(object):
4656
4674
  fsize,
4657
4675
  ip,
4658
4676
  time.time(),
4659
- "",
4677
+ None,
4660
4678
  )
4661
4679
 
4662
4680
  return "k"
@@ -4667,6 +4685,12 @@ class Up2k(object):
4667
4685
  has_dupes = False
4668
4686
  if w:
4669
4687
  if c2 and c2 != c1:
4688
+ if "nodupem" in dvn.flags:
4689
+ q = "select w from up where substr(w,1,16) = ?"
4690
+ for (w2,) in c2.execute(q, (w[:16],)):
4691
+ if w == w2:
4692
+ t = "file exists in target volume, and dupes are forbidden in config"
4693
+ raise Pebkac(400, t)
4670
4694
  self._copy_tags(c1, c2, w)
4671
4695
 
4672
4696
  xlink = bool(svn.flags.get("xlink"))
@@ -4775,7 +4799,7 @@ class Up2k(object):
4775
4799
  fsize,
4776
4800
  ip,
4777
4801
  time.time(),
4778
- "",
4802
+ None,
4779
4803
  )
4780
4804
 
4781
4805
  return "k"
@@ -5112,7 +5136,7 @@ class Up2k(object):
5112
5136
  job["size"],
5113
5137
  job["addr"],
5114
5138
  job["t0"],
5115
- "",
5139
+ None,
5116
5140
  )
5117
5141
  t = hr.get("rejectmsg") or ""
5118
5142
  if t or not hr: