copyparty 1.14.4__py3-none-any.whl → 1.15.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
copyparty/svchub.py CHANGED
@@ -3,7 +3,6 @@ from __future__ import print_function, unicode_literals
3
3
 
4
4
  import argparse
5
5
  import base64
6
- import calendar
7
6
  import errno
8
7
  import gzip
9
8
  import logging
@@ -16,7 +15,7 @@ import string
16
15
  import sys
17
16
  import threading
18
17
  import time
19
- from datetime import datetime, timedelta
18
+ from datetime import datetime
20
19
 
21
20
  # from inspect import currentframe
22
21
  # print(currentframe().f_lineno)
@@ -98,6 +97,7 @@ class SvcHub(object):
98
97
  self.argv = argv
99
98
  self.E = args.E
100
99
  self.no_ansi = args.no_ansi
100
+ self.tz = UTC if args.log_utc else None
101
101
  self.logf = None
102
102
  self.logf_base_fn = ""
103
103
  self.is_dut = False # running in unittest; always False
@@ -112,7 +112,8 @@ class SvcHub(object):
112
112
  self.httpsrv_up = 0
113
113
 
114
114
  self.log_mutex = threading.Lock()
115
- self.next_day = 0
115
+ self.cday = 0
116
+ self.cmon = 0
116
117
  self.tstack = 0.0
117
118
 
118
119
  self.iphash = HMaccas(os.path.join(self.E.cfg, "iphash"), 8)
@@ -214,6 +215,9 @@ class SvcHub(object):
214
215
  noch.update([x for x in zsl if x])
215
216
  args.chpw_no = noch
216
217
 
218
+ if not self.args.no_ses:
219
+ self.setup_session_db()
220
+
217
221
  if args.shr:
218
222
  self.setup_share_db()
219
223
 
@@ -362,6 +366,64 @@ class SvcHub(object):
362
366
 
363
367
  self.broker = Broker(self)
364
368
 
369
+ def setup_session_db(self) :
370
+ if not HAVE_SQLITE3:
371
+ self.args.no_ses = True
372
+ t = "WARNING: sqlite3 not available; disabling sessions, will use plaintext passwords in cookies"
373
+ self.log("root", t, 3)
374
+ return
375
+
376
+ import sqlite3
377
+
378
+ create = True
379
+ db_path = self.args.ses_db
380
+ self.log("root", "opening sessions-db %s" % (db_path,))
381
+ for n in range(2):
382
+ try:
383
+ db = sqlite3.connect(db_path)
384
+ cur = db.cursor()
385
+ try:
386
+ cur.execute("select count(*) from us").fetchone()
387
+ create = False
388
+ break
389
+ except:
390
+ pass
391
+ except Exception as ex:
392
+ if n:
393
+ raise
394
+ t = "sessions-db corrupt; deleting and recreating: %r"
395
+ self.log("root", t % (ex,), 3)
396
+ try:
397
+ cur.close() # type: ignore
398
+ except:
399
+ pass
400
+ try:
401
+ db.close() # type: ignore
402
+ except:
403
+ pass
404
+ os.unlink(db_path)
405
+
406
+ sch = [
407
+ r"create table kv (k text, v int)",
408
+ r"create table us (un text, si text, t0 int)",
409
+ # username, session-id, creation-time
410
+ r"create index us_un on us(un)",
411
+ r"create index us_si on us(si)",
412
+ r"create index us_t0 on us(t0)",
413
+ r"insert into kv values ('sver', 1)",
414
+ ]
415
+
416
+ assert db # type: ignore
417
+ assert cur # type: ignore
418
+ if create:
419
+ for cmd in sch:
420
+ cur.execute(cmd)
421
+ self.log("root", "created new sessions-db")
422
+ db.commit()
423
+
424
+ cur.close()
425
+ db.close()
426
+
365
427
  def setup_share_db(self) :
366
428
  al = self.args
367
429
  if not HAVE_SQLITE3:
@@ -538,7 +600,7 @@ class SvcHub(object):
538
600
  fng = []
539
601
  t_ff = "transcode audio, create spectrograms, video thumbnails"
540
602
  to_check = [
541
- (HAVE_SQLITE3, "sqlite", "file and media indexing"),
603
+ (HAVE_SQLITE3, "sqlite", "sessions and file/media indexing"),
542
604
  (HAVE_PIL, "pillow", "image thumbnails (plenty fast)"),
543
605
  (HAVE_VIPS, "vips", "image thumbnails (faster, eats more ram)"),
544
606
  (HAVE_WEBP, "pillow-webp", "create thumbnails as webp files"),
@@ -785,7 +847,7 @@ class SvcHub(object):
785
847
  self.args.nc = min(self.args.nc, soft // 2)
786
848
 
787
849
  def _logname(self) :
788
- dt = datetime.now(UTC)
850
+ dt = datetime.now(self.tz)
789
851
  fn = str(self.args.lo)
790
852
  for fs in "YmdHMS":
791
853
  fs = "%" + fs
@@ -938,6 +1000,11 @@ class SvcHub(object):
938
1000
 
939
1001
  self._reload(rescan_all_vols=rescan_all_vols, up2k=up2k)
940
1002
 
1003
+ def _reload_sessions(self) :
1004
+ with self.asrv.mutex:
1005
+ self.asrv.load_sessions(True)
1006
+ self.broker.reload_sessions()
1007
+
941
1008
  def stop_thr(self) :
942
1009
  while not self.stop_req:
943
1010
  with self.stop_cond:
@@ -1058,12 +1125,12 @@ class SvcHub(object):
1058
1125
  return
1059
1126
 
1060
1127
  with self.log_mutex:
1061
- zd = datetime.now(UTC)
1128
+ dt = datetime.now(self.tz)
1062
1129
  ts = self.log_dfmt % (
1063
- zd.year,
1064
- zd.month * 100 + zd.day,
1065
- (zd.hour * 100 + zd.minute) * 100 + zd.second,
1066
- zd.microsecond // self.log_div,
1130
+ dt.year,
1131
+ dt.month * 100 + dt.day,
1132
+ (dt.hour * 100 + dt.minute) * 100 + dt.second,
1133
+ dt.microsecond // self.log_div,
1067
1134
  )
1068
1135
 
1069
1136
  if c and not self.args.no_ansi:
@@ -1084,41 +1151,26 @@ class SvcHub(object):
1084
1151
  if not self.args.no_logflush:
1085
1152
  self.logf.flush()
1086
1153
 
1087
- now = time.time()
1088
- if int(now) >= self.next_day:
1089
- self._set_next_day()
1154
+ if dt.day != self.cday or dt.month != self.cmon:
1155
+ self._set_next_day(dt)
1090
1156
 
1091
- def _set_next_day(self) :
1092
- if self.next_day and self.logf and self.logf_base_fn != self._logname():
1157
+ def _set_next_day(self, dt ) :
1158
+ if self.cday and self.logf and self.logf_base_fn != self._logname():
1093
1159
  self.logf.close()
1094
1160
  self._setup_logfile("")
1095
1161
 
1096
- dt = datetime.now(UTC)
1097
-
1098
- # unix timestamp of next 00:00:00 (leap-seconds safe)
1099
- day_now = dt.day
1100
- while dt.day == day_now:
1101
- dt += timedelta(hours=12)
1102
-
1103
- dt = dt.replace(hour=0, minute=0, second=0)
1104
- try:
1105
- tt = dt.utctimetuple()
1106
- except:
1107
- # still makes me hella uncomfortable
1108
- tt = dt.timetuple()
1109
-
1110
- self.next_day = calendar.timegm(tt)
1162
+ self.cday = dt.day
1163
+ self.cmon = dt.month
1111
1164
 
1112
1165
  def _log_enabled(self, src , msg , c = 0) :
1113
1166
  """handles logging from all components"""
1114
1167
  with self.log_mutex:
1115
- now = time.time()
1116
- if int(now) >= self.next_day:
1117
- dt = datetime.fromtimestamp(now, UTC)
1168
+ dt = datetime.now(self.tz)
1169
+ if dt.day != self.cday or dt.month != self.cmon:
1118
1170
  zs = "{}\n" if self.no_ansi else "\033[36m{}\033[0m\n"
1119
1171
  zs = zs.format(dt.strftime("%Y-%m-%d"))
1120
1172
  print(zs, end="")
1121
- self._set_next_day()
1173
+ self._set_next_day(dt)
1122
1174
  if self.logf:
1123
1175
  self.logf.write(zs)
1124
1176
 
@@ -1137,12 +1189,11 @@ class SvcHub(object):
1137
1189
  else:
1138
1190
  msg = "%s%s\033[0m" % (c, msg)
1139
1191
 
1140
- zd = datetime.fromtimestamp(now, UTC)
1141
1192
  ts = self.log_efmt % (
1142
- zd.hour,
1143
- zd.minute,
1144
- zd.second,
1145
- zd.microsecond // self.log_div,
1193
+ dt.hour,
1194
+ dt.minute,
1195
+ dt.second,
1196
+ dt.microsecond // self.log_div,
1146
1197
  )
1147
1198
  msg = fmt % (ts, src, msg)
1148
1199
  try:
copyparty/up2k.py CHANGED
@@ -1459,7 +1459,7 @@ class Up2k(object):
1459
1459
  self.log("file: {}".format(abspath))
1460
1460
 
1461
1461
  try:
1462
- hashes = self._hashlist_from_file(
1462
+ hashes, _ = self._hashlist_from_file(
1463
1463
  abspath, "a{}, ".format(self.pp.n)
1464
1464
  )
1465
1465
  except Exception as ex:
@@ -1653,6 +1653,7 @@ class Up2k(object):
1653
1653
  qex = " where " + qex
1654
1654
 
1655
1655
  rewark = []
1656
+ f404 = []
1656
1657
 
1657
1658
  with self.mutex:
1658
1659
  b_left = 0
@@ -1669,7 +1670,8 @@ class Up2k(object):
1669
1670
  if self.stop:
1670
1671
  return -1
1671
1672
 
1672
- w, drd, dfn = zb[:-1].decode("utf-8").split("\x00")
1673
+ zs = zb[:-1].decode("utf-8").replace("\x00\x02", "\n")
1674
+ w, drd, dfn = zs.split("\x00\x01")
1673
1675
  with self.mutex:
1674
1676
  q = "select mt, sz from up where rd=? and fn=? and +w=?"
1675
1677
  try:
@@ -1695,9 +1697,14 @@ class Up2k(object):
1695
1697
  pf = "v{}, {:.0f}+".format(n_left, b_left / 1024 / 1024)
1696
1698
  self.pp.msg = pf + abspath
1697
1699
 
1698
- # throws on broken symlinks (always did)
1699
- stl = bos.lstat(abspath)
1700
- st = bos.stat(abspath) if stat.S_ISLNK(stl.st_mode) else stl
1700
+ try:
1701
+ stl = bos.lstat(abspath)
1702
+ st = bos.stat(abspath) if stat.S_ISLNK(stl.st_mode) else stl
1703
+ except Exception as ex:
1704
+ self.log("missing file: %s" % (abspath,), 3)
1705
+ f404.append((drd, dfn, w))
1706
+ continue
1707
+
1701
1708
  mt2 = int(stl.st_mtime)
1702
1709
  sz2 = st.st_size
1703
1710
 
@@ -1708,7 +1715,7 @@ class Up2k(object):
1708
1715
  self.log("file: {}".format(abspath))
1709
1716
 
1710
1717
  try:
1711
- hashes = self._hashlist_from_file(abspath, pf)
1718
+ hashes, _ = self._hashlist_from_file(abspath, pf)
1712
1719
  except Exception as ex:
1713
1720
  self.log("hash: {} @ [{}]".format(repr(ex), abspath))
1714
1721
  continue
@@ -1734,12 +1741,15 @@ class Up2k(object):
1734
1741
  t = t.format(abspath, w, sz, mt, w2, sz2, mt2)
1735
1742
  self.log(t, 1)
1736
1743
 
1737
- if e2vp and rewark:
1744
+ if e2vp and (rewark or f404):
1738
1745
  self.hub.retcode = 1
1739
1746
  Daemon(self.hub.sigterm)
1740
- raise Exception("{} files have incorrect hashes".format(len(rewark)))
1747
+ t = "in volume /%s: %s files missing, %s files have incorrect hashes"
1748
+ t = t % (vol.vpath, len(f404), len(rewark))
1749
+ self.log(t, 1)
1750
+ raise Exception(t)
1741
1751
 
1742
- if not e2vu or not rewark:
1752
+ if not e2vu or (not rewark and not f404):
1743
1753
  return 0
1744
1754
 
1745
1755
  with self.mutex:
@@ -1747,9 +1757,13 @@ class Up2k(object):
1747
1757
  q = "update up set w = ?, sz = ?, mt = ? where rd = ? and fn = ? limit 1"
1748
1758
  cur.execute(q, (w, sz, int(mt), rd, fn))
1749
1759
 
1760
+ for _, _, w in f404:
1761
+ q = "delete from up where w = ? limit 1"
1762
+ cur.execute(q, (w,))
1763
+
1750
1764
  cur.connection.commit()
1751
1765
 
1752
- return len(rewark)
1766
+ return len(rewark) + len(f404)
1753
1767
 
1754
1768
  def _build_tags_index(self, vol ) :
1755
1769
  ptop = vol.realpath
@@ -1964,7 +1978,8 @@ class Up2k(object):
1964
1978
  if c2.execute(q, (row[0][:16],)).fetchone():
1965
1979
  continue
1966
1980
 
1967
- gf.write(("%s\n" % ("\x00".join(row),)).encode("utf-8"))
1981
+ zs = "\x00\x01".join(row).replace("\n", "\x00\x02")
1982
+ gf.write((zs + "\n").encode("utf-8"))
1968
1983
  n += 1
1969
1984
 
1970
1985
  c2.close()
@@ -2663,10 +2678,13 @@ class Up2k(object):
2663
2678
  jcur = self.cur.get(ptop)
2664
2679
  reg = self.registry[ptop]
2665
2680
  vfs = self.asrv.vfs.all_vols[cj["vtop"]]
2666
- n4g = vfs.flags.get("noforget")
2681
+ n4g = bool(vfs.flags.get("noforget"))
2667
2682
  rand = vfs.flags.get("rand") or cj.get("rand")
2668
2683
  lost = []
2669
2684
 
2685
+ safe_dedup = vfs.flags.get("safededup") or 50
2686
+ data_ok = safe_dedup < 10 or n4g
2687
+
2670
2688
  vols = [(ptop, jcur)] if jcur else []
2671
2689
  if vfs.flags.get("xlink"):
2672
2690
  vols += [(k, v) for k, v in self.cur.items() if k != ptop]
@@ -2674,7 +2692,7 @@ class Up2k(object):
2674
2692
  # force upload time rather than last-modified
2675
2693
  cj["lmod"] = int(time.time())
2676
2694
 
2677
- alts = []
2695
+ alts = []
2678
2696
  for ptop, cur in vols:
2679
2697
  allv = self.asrv.vfs.all_vols
2680
2698
  cvfs = next((v for v in allv.values() if v.realpath == ptop), vfs)
@@ -2704,13 +2722,12 @@ class Up2k(object):
2704
2722
  wark, st.st_size, dsize, st.st_mtime, dtime, dp_abs
2705
2723
  )
2706
2724
  self.log(t)
2707
- raise Exception("desync")
2725
+ raise Exception()
2708
2726
  except Exception as ex:
2709
2727
  if n4g:
2710
2728
  st = os.stat_result((0, -1, -1, 0, 0, 0, 0, 0, 0, 0))
2711
2729
  else:
2712
- if str(ex) != "desync":
2713
- lost.append((cur, dp_dir, dp_fn))
2730
+ lost.append((cur, dp_dir, dp_fn))
2714
2731
  continue
2715
2732
 
2716
2733
  j = {
@@ -2733,18 +2750,42 @@ class Up2k(object):
2733
2750
  if k in cj:
2734
2751
  j[k] = cj[k]
2735
2752
 
2753
+ # offset of 1st diff in vpaths
2754
+ zig = (
2755
+ n + 1
2756
+ for n, (c1, c2) in enumerate(
2757
+ zip(dp_dir + "\r", cj["prel"] + "\n")
2758
+ )
2759
+ if c1 != c2
2760
+ )
2736
2761
  score = (
2737
- (3 if st.st_dev == dev else 0)
2738
- + (2 if dp_dir == cj["prel"] else 0)
2762
+ (6969 if st.st_dev == dev else 0)
2763
+ + (3210 if dp_dir == cj["prel"] else next(zig))
2739
2764
  + (1 if dp_fn == cj["name"] else 0)
2740
2765
  )
2741
- alts.append((score, -len(alts), j))
2742
-
2743
- if alts:
2744
- best = sorted(alts, reverse=True)[0]
2745
- job = best[2]
2746
- else:
2747
- job = None
2766
+ alts.append((score, -len(alts), j, cur, dp_dir, dp_fn))
2767
+
2768
+ job = None
2769
+ inc_ap = djoin(cj["ptop"], cj["prel"], cj["name"])
2770
+ for dupe in sorted(alts, reverse=True):
2771
+ rj = dupe[2]
2772
+ orig_ap = djoin(rj["ptop"], rj["prel"], rj["name"])
2773
+ if data_ok or inc_ap == orig_ap:
2774
+ data_ok = True
2775
+ job = rj
2776
+ break
2777
+ else:
2778
+ self.log("asserting contents of %s" % (orig_ap,))
2779
+ dhashes, st = self._hashlist_from_file(orig_ap)
2780
+ dwark = up2k_wark_from_hashlist(self.salt, st.st_size, dhashes)
2781
+ if wark != dwark:
2782
+ t = "will not dedup (fs index desync): fs=%s, db=%s, file: %s"
2783
+ self.log(t % (dwark, wark, orig_ap))
2784
+ lost.append(dupe[3:])
2785
+ continue
2786
+ data_ok = True
2787
+ job = rj
2788
+ break
2748
2789
 
2749
2790
  if job and wark in reg:
2750
2791
  # self.log("pop " + wark + " " + job["name"] + " handle_json db", 4)
@@ -2753,7 +2794,7 @@ class Up2k(object):
2753
2794
  if lost:
2754
2795
  c2 = None
2755
2796
  for cur, dp_dir, dp_fn in lost:
2756
- t = "forgetting deleted file: /{}"
2797
+ t = "forgetting desynced db entry: /{}"
2757
2798
  self.log(t.format(vjoin(vjoin(vfs.vpath, dp_dir), dp_fn)))
2758
2799
  self.db_rm(cur, dp_dir, dp_fn, cj["size"])
2759
2800
  if c2 and c2 != cur:
@@ -2788,7 +2829,13 @@ class Up2k(object):
2788
2829
  del reg[wark]
2789
2830
  break
2790
2831
 
2791
- if st and not self.args.nw and not n4g and st.st_size != rj["size"]:
2832
+ inc_ap = djoin(cj["ptop"], cj["prel"], cj["name"])
2833
+ orig_ap = djoin(rj["ptop"], rj["prel"], rj["name"])
2834
+
2835
+ if self.args.nw or n4g or not st:
2836
+ pass
2837
+
2838
+ elif st.st_size != rj["size"]:
2792
2839
  t = "will not dedup (fs index desync): {}, size fs={} db={}, mtime fs={} db={}, file: {}"
2793
2840
  t = t.format(
2794
2841
  wark, st.st_size, rj["size"], st.st_mtime, rj["lmod"], path
@@ -2796,6 +2843,15 @@ class Up2k(object):
2796
2843
  self.log(t)
2797
2844
  del reg[wark]
2798
2845
 
2846
+ elif inc_ap != orig_ap and not data_ok and "done" in reg[wark]:
2847
+ self.log("asserting contents of %s" % (orig_ap,))
2848
+ dhashes, _ = self._hashlist_from_file(orig_ap)
2849
+ dwark = up2k_wark_from_hashlist(self.salt, st.st_size, dhashes)
2850
+ if wark != dwark:
2851
+ t = "will not dedup (fs index desync): fs=%s, idx=%s, file: %s"
2852
+ self.log(t % (dwark, wark, orig_ap))
2853
+ del reg[wark]
2854
+
2799
2855
  if job or wark in reg:
2800
2856
  job = job or reg[wark]
2801
2857
  if (
@@ -3048,7 +3104,22 @@ class Up2k(object):
3048
3104
  fp = djoin(fdir, fname)
3049
3105
  if job.get("replace") and bos.path.exists(fp):
3050
3106
  self.log("replacing existing file at {}".format(fp))
3051
- wunlink(self.log, fp, self.flags.get(job["ptop"]) or {})
3107
+ cur = None
3108
+ ptop = job["ptop"]
3109
+ vf = self.flags.get(ptop) or {}
3110
+ st = bos.stat(fp)
3111
+ try:
3112
+ vrel = vjoin(job["prel"], fname)
3113
+ xlink = bool(vf.get("xlink"))
3114
+ cur, wark, _, _, _, _ = self._find_from_vpath(ptop, vrel)
3115
+ self._forget_file(ptop, vrel, cur, wark, True, st.st_size, xlink)
3116
+ except Exception as ex:
3117
+ self.log("skipping replace-relink: %r" % (ex,))
3118
+ finally:
3119
+ if cur:
3120
+ cur.connection.commit()
3121
+
3122
+ wunlink(self.log, fp, vf)
3052
3123
 
3053
3124
  if self.args.plain_ip:
3054
3125
  dip = ip.replace(":", ".")
@@ -3067,17 +3138,25 @@ class Up2k(object):
3067
3138
  verbose = True,
3068
3139
  rm = False,
3069
3140
  lmod = 0,
3141
+ fsrc = None,
3070
3142
  ) :
3143
+ if src == dst or (fsrc and fsrc == dst):
3144
+ t = "symlinking a file to itself?? orig(%s) fsrc(%s) link(%s)"
3145
+ raise Exception(t % (src, fsrc, dst))
3146
+
3071
3147
  if verbose:
3072
- self.log("linking dupe:\n {0}\n {1}".format(src, dst))
3148
+ t = "linking dupe:\n point-to: {0}\n link-loc: {1}"
3149
+ if fsrc:
3150
+ t += "\n data-src: {2}"
3151
+ self.log(t.format(src, dst, fsrc))
3073
3152
 
3074
3153
  if self.args.nw:
3075
3154
  return
3076
3155
 
3077
3156
  linked = False
3078
3157
  try:
3079
- if "copydupes" in flags:
3080
- raise Exception("disabled in config")
3158
+ if not flags.get("dedup"):
3159
+ raise Exception("dedup is disabled in config")
3081
3160
 
3082
3161
  lsrc = src
3083
3162
  ldst = dst
@@ -3114,7 +3193,7 @@ class Up2k(object):
3114
3193
  linked = True
3115
3194
  except Exception as ex:
3116
3195
  self.log("cannot hardlink: " + repr(ex))
3117
- if "neversymlink" in flags:
3196
+ if "hardlinkonly" in flags:
3118
3197
  raise Exception("symlink-fallback disabled in cfg")
3119
3198
 
3120
3199
  if not linked:
@@ -3133,7 +3212,15 @@ class Up2k(object):
3133
3212
  linked = True
3134
3213
  except Exception as ex:
3135
3214
  self.log("cannot link; creating copy: " + repr(ex))
3136
- shutil.copy2(fsenc(src), fsenc(dst))
3215
+ if bos.path.isfile(src):
3216
+ csrc = src
3217
+ elif fsrc and bos.path.isfile(fsrc):
3218
+ csrc = fsrc
3219
+ else:
3220
+ t = "BUG: no valid sources to link from! orig(%s) fsrc(%s) link(%s)"
3221
+ self.log(t, 1)
3222
+ raise Exception(t % (src, fsrc, dst))
3223
+ shutil.copy2(fsenc(csrc), fsenc(dst))
3137
3224
 
3138
3225
  if lmod and (not linked or SYMTIME):
3139
3226
  times = (int(time.time()), int(lmod))
@@ -3695,8 +3782,11 @@ class Up2k(object):
3695
3782
  cur = None
3696
3783
  try:
3697
3784
  ptop = dbv.realpath
3785
+ xlink = bool(dbv.flags.get("xlink"))
3698
3786
  cur, wark, _, _, _, _ = self._find_from_vpath(ptop, volpath)
3699
- self._forget_file(ptop, volpath, cur, wark, True, st.st_size)
3787
+ self._forget_file(
3788
+ ptop, volpath, cur, wark, True, st.st_size, xlink
3789
+ )
3700
3790
  finally:
3701
3791
  if cur:
3702
3792
  cur.connection.commit()
@@ -3920,13 +4010,15 @@ class Up2k(object):
3920
4010
  if c2 and c2 != c1:
3921
4011
  self._copy_tags(c1, c2, w)
3922
4012
 
4013
+ xlink = bool(svn.flags.get("xlink"))
4014
+
3923
4015
  with self.reg_mutex:
3924
4016
  has_dupes = self._forget_file(
3925
- svn.realpath, srem, c1, w, is_xvol, fsize_ or fsize
4017
+ svn.realpath, srem, c1, w, is_xvol, fsize_ or fsize, xlink
3926
4018
  )
3927
4019
 
3928
4020
  if not is_xvol:
3929
- has_dupes = self._relink(w, svn.realpath, srem, dabs)
4021
+ has_dupes = self._relink(w, svn.realpath, srem, dabs, c1, xlink)
3930
4022
 
3931
4023
  curs.add(c1)
3932
4024
 
@@ -4069,6 +4161,7 @@ class Up2k(object):
4069
4161
  wark ,
4070
4162
  drop_tags ,
4071
4163
  sz ,
4164
+ xlink ,
4072
4165
  ) :
4073
4166
  """
4074
4167
  mutex(main,reg) me
@@ -4080,7 +4173,7 @@ class Up2k(object):
4080
4173
  if wark and cur:
4081
4174
  self.log("found {} in db".format(wark))
4082
4175
  if drop_tags:
4083
- if self._relink(wark, ptop, vrem, ""):
4176
+ if self._relink(wark, ptop, vrem, "", cur, xlink):
4084
4177
  has_dupes = True
4085
4178
  drop_tags = False
4086
4179
 
@@ -4112,7 +4205,15 @@ class Up2k(object):
4112
4205
 
4113
4206
  return has_dupes
4114
4207
 
4115
- def _relink(self, wark , sptop , srem , dabs ) :
4208
+ def _relink(
4209
+ self,
4210
+ wark ,
4211
+ sptop ,
4212
+ srem ,
4213
+ dabs ,
4214
+ vcur ,
4215
+ xlink ,
4216
+ ) :
4116
4217
  """
4117
4218
  update symlinks from file at svn/srem to dabs (rename),
4118
4219
  or to first remaining full if no dabs (delete)
@@ -4128,6 +4229,8 @@ class Up2k(object):
4128
4229
  argv = (wark[:16], wark)
4129
4230
 
4130
4231
  for ptop, cur in self.cur.items():
4232
+ if not xlink and cur and cur != vcur:
4233
+ continue
4131
4234
  for rd, fn in cur.execute(q, argv):
4132
4235
  if rd.startswith("//") or fn.startswith("//"):
4133
4236
  rd, fn = s3dec(rd, fn)
@@ -4214,7 +4317,13 @@ class Up2k(object):
4214
4317
  except:
4215
4318
  pass
4216
4319
 
4217
- self._symlink(dabs, alink, flags, False, lmod=lmod or 0)
4320
+ # this creates a link pointing from dabs to alink; alink may
4321
+ # not exist yet, which becomes problematic if the symlinking
4322
+ # fails and it has to fall back on hardlinking/copying files
4323
+ # (for example a volume with symlinked dupes but no --dedup);
4324
+ # fsrc=sabs is then a source that currently resolves to copy
4325
+
4326
+ self._symlink(dabs, alink, flags, False, lmod=lmod or 0, fsrc=sabs)
4218
4327
 
4219
4328
  return len(full) + len(links)
4220
4329
 
@@ -4243,8 +4352,11 @@ class Up2k(object):
4243
4352
 
4244
4353
  return wark
4245
4354
 
4246
- def _hashlist_from_file(self, path , prefix = "") :
4247
- fsz = bos.path.getsize(path)
4355
+ def _hashlist_from_file(
4356
+ self, path , prefix = ""
4357
+ ) :
4358
+ st = bos.stat(path)
4359
+ fsz = st.st_size
4248
4360
  csz = up2k_chunksize(fsz)
4249
4361
  ret = []
4250
4362
  suffix = " MB, {}".format(path)
@@ -4257,7 +4369,7 @@ class Up2k(object):
4257
4369
  while fsz > 0:
4258
4370
  # same as `hash_at` except for `imutex` / bufsz
4259
4371
  if self.stop:
4260
- return []
4372
+ return [], st
4261
4373
 
4262
4374
  if self.pp:
4263
4375
  mb = fsz // (1024 * 1024)
@@ -4278,7 +4390,7 @@ class Up2k(object):
4278
4390
  digest = base64.urlsafe_b64encode(digest)
4279
4391
  ret.append(digest.decode("utf-8"))
4280
4392
 
4281
- return ret
4393
+ return ret, st
4282
4394
 
4283
4395
  def _new_upload(self, job , vfs , depth ) :
4284
4396
  pdir = djoin(job["ptop"], job["prel"])
@@ -4579,7 +4691,7 @@ class Up2k(object):
4579
4691
  self.salt, inf.st_size, int(inf.st_mtime), rd, fn
4580
4692
  )
4581
4693
  else:
4582
- hashes = self._hashlist_from_file(abspath)
4694
+ hashes, _ = self._hashlist_from_file(abspath)
4583
4695
  if not hashes:
4584
4696
  return False
4585
4697
 
copyparty/util.py CHANGED
@@ -239,6 +239,8 @@ IMPLICATIONS = [
239
239
  ["e2vu", "e2v"],
240
240
  ["e2vp", "e2v"],
241
241
  ["e2v", "e2d"],
242
+ ["hardlink_only", "hardlink"],
243
+ ["hardlink", "dedup"],
242
244
  ["tftpvv", "tftpv"],
243
245
  ["smbw", "smb"],
244
246
  ["smb1", "smb"],