copyparty 1.11.1__py3-none-any.whl → 1.12.0__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/__init__.py CHANGED
@@ -53,7 +53,6 @@ class EnvParams(object):
53
53
  self.t0 = time.time()
54
54
  self.mod = ""
55
55
  self.cfg = ""
56
- self.ox = getattr(sys, "oxidized", None)
57
56
 
58
57
 
59
58
  E = EnvParams()
copyparty/__main__.py CHANGED
@@ -151,7 +151,8 @@ def warn(msg ) :
151
151
 
152
152
 
153
153
  def init_E(EE ) :
154
- # __init__ runs 18 times when oxidized; do expensive stuff here
154
+ # some cpython alternatives (such as pyoxidizer) can
155
+ # __init__ several times, so do expensive stuff here
155
156
 
156
157
  E = EE # pylint: disable=redefined-outer-name
157
158
 
@@ -184,34 +185,9 @@ def init_E(EE ) :
184
185
 
185
186
  raise Exception("could not find a writable path for config")
186
187
 
187
- def _unpack() :
188
- import atexit
189
- import tarfile
190
- import tempfile
191
- from importlib.resources import open_binary
192
-
193
- td = tempfile.TemporaryDirectory(prefix="")
194
- atexit.register(td.cleanup)
195
- tdn = td.name
196
-
197
- with open_binary("copyparty", "z.tar") as tgz:
198
- with tarfile.open(fileobj=tgz) as tf:
199
- try:
200
- tf.extractall(tdn, filter="tar")
201
- except TypeError:
202
- tf.extractall(tdn) # nosec (archive is safe)
203
-
204
- return tdn
205
-
206
- try:
207
- E.mod = os.path.dirname(os.path.realpath(__file__))
208
- if E.mod.endswith("__init__"):
209
- E.mod = os.path.dirname(E.mod)
210
- except:
211
- if not E.ox:
212
- raise
213
-
214
- E.mod = _unpack()
188
+ E.mod = os.path.dirname(os.path.realpath(__file__))
189
+ if E.mod.endswith("__init__"):
190
+ E.mod = os.path.dirname(E.mod)
215
191
 
216
192
  if sys.platform == "win32":
217
193
  bdir = os.environ.get("APPDATA") or os.environ.get("TEMP") or "."
@@ -268,6 +244,19 @@ def get_fk_salt() :
268
244
  return ret.decode("utf-8")
269
245
 
270
246
 
247
+ def get_dk_salt() :
248
+ fp = os.path.join(E.cfg, "dk-salt.txt")
249
+ try:
250
+ with open(fp, "rb") as f:
251
+ ret = f.read().strip()
252
+ except:
253
+ ret = base64.b64encode(os.urandom(30))
254
+ with open(fp, "wb") as f:
255
+ f.write(ret + b"\n")
256
+
257
+ return ret.decode("utf-8")
258
+
259
+
271
260
  def get_ah_salt() :
272
261
  fp = os.path.join(E.cfg, "ah-salt.txt")
273
262
  try:
@@ -863,6 +852,7 @@ def add_fs(ap):
863
852
  ap2 = ap.add_argument_group("filesystem options")
864
853
  rm_re_def = "5/0.1" if ANYWIN else "0/0"
865
854
  ap2.add_argument("--rm-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be deleted because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=rm_retry)")
855
+ ap2.add_argument("--iobuf", metavar="BYTES", type=int, default=256*1024, help="file I/O buffer-size; if your volumes are on a network drive, try increasing to \033[32m524288\033[0m or even \033[32m4194304\033[0m (and let me know if that improves your performance)")
866
856
 
867
857
 
868
858
  def add_upload(ap):
@@ -910,6 +900,7 @@ def add_network(ap):
910
900
  ap2.add_argument("--freebind", action="store_true", help="allow listening on IPs which do not yet exist, for example if the network interfaces haven't finished going up. Only makes sense for IPs other than '0.0.0.0', '127.0.0.1', '::', and '::1'. May require running as root (unless net.ipv6.ip_nonlocal_bind)")
911
901
  ap2.add_argument("--s-thead", metavar="SEC", type=int, default=120, help="socket timeout (read request header)")
912
902
  ap2.add_argument("--s-tbody", metavar="SEC", type=float, default=186, help="socket timeout (read/write request/response bodies). Use 60 on fast servers (default is extremely safe). Disable with 0 if reverse-proxied for a 2%% speed boost")
903
+ ap2.add_argument("--s-rd-sz", metavar="B", type=int, default=256*1024, help="socket read size in bytes (indirectly affects filesystem writes; recommendation: keep equal-to or lower-than \033[33m--iobuf\033[0m)")
913
904
  ap2.add_argument("--s-wr-sz", metavar="B", type=int, default=256*1024, help="socket write size in bytes")
914
905
  ap2.add_argument("--s-wr-slp", metavar="SEC", type=float, default=0, help="debug: socket write delay in seconds")
915
906
  ap2.add_argument("--rsp-slp", metavar="SEC", type=float, default=0, help="debug: response delay in seconds")
@@ -1123,13 +1114,14 @@ def add_safety(ap):
1123
1114
  ap2.add_argument("--acam", metavar="V[,V]", type=u, default="GET,HEAD", help="Access-Control-Allow-Methods; list of methods to accept from offsite ('*' behaves like \033[33m--acao\033[0m's description)")
1124
1115
 
1125
1116
 
1126
- def add_salt(ap, fk_salt, ah_salt):
1117
+ def add_salt(ap, fk_salt, dk_salt, ah_salt):
1127
1118
  ap2 = ap.add_argument_group('salting options')
1128
1119
  ap2.add_argument("--ah-alg", metavar="ALG", type=u, default="none", help="account-pw hashing algorithm; one of these, best to worst: \033[32margon2 scrypt sha2 none\033[0m (each optionally followed by alg-specific comma-sep. config)")
1129
1120
  ap2.add_argument("--ah-salt", metavar="SALT", type=u, default=ah_salt, help="account-pw salt; ignored if \033[33m--ah-alg\033[0m is none (default)")
1130
1121
  ap2.add_argument("--ah-gen", metavar="PW", type=u, default="", help="generate hashed password for \033[33mPW\033[0m, or read passwords from STDIN if \033[33mPW\033[0m is [\033[32m-\033[0m]")
1131
1122
  ap2.add_argument("--ah-cli", action="store_true", help="launch an interactive shell which hashes passwords without ever storing or displaying the original passwords")
1132
1123
  ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files")
1124
+ ap2.add_argument("--dk-salt", metavar="SALT", type=u, default=dk_salt, help="per-directory accesskey salt; used to generate unpredictable URLs to share folders with users who only have the 'get' permission")
1133
1125
  ap2.add_argument("--warksalt", metavar="SALT", type=u, default="hunter2", help="up2k file-hash salt; serves no purpose, no reason to change this (but delete all databases if you do)")
1134
1126
 
1135
1127
 
@@ -1195,6 +1187,8 @@ def add_thumbnail(ap):
1195
1187
 
1196
1188
  def add_transcoding(ap):
1197
1189
  ap2 = ap.add_argument_group('transcoding options')
1190
+ ap2.add_argument("--q-opus", metavar="KBPS", type=int, default=128, help="target bitrate for transcoding to opus; set 0 to disable")
1191
+ ap2.add_argument("--q-mp3", metavar="QUALITY", type=u, default="q2", help="target quality for transcoding to mp3, for example [\033[32m192k\033[0m] (CBR) or [\033[32mq0\033[0m] (CQ/CRF, q0=maxquality, q9=smallest); set 0 to disable")
1198
1192
  ap2.add_argument("--no-acode", action="store_true", help="disable audio transcoding")
1199
1193
  ap2.add_argument("--no-bacode", action="store_true", help="disable batch audio transcoding by folder download (zip/tar)")
1200
1194
  ap2.add_argument("--ac-maxage", metavar="SEC", type=int, default=86400, help="delete cached transcode output after \033[33mSEC\033[0m seconds")
@@ -1311,6 +1305,7 @@ def run_argparse(
1311
1305
  cert_path = os.path.join(E.cfg, "cert.pem")
1312
1306
 
1313
1307
  fk_salt = get_fk_salt()
1308
+ dk_salt = get_dk_salt()
1314
1309
  ah_salt = get_ah_salt()
1315
1310
 
1316
1311
  # alpine peaks at 5 threads for some reason,
@@ -1342,7 +1337,7 @@ def run_argparse(
1342
1337
  add_tftp(ap)
1343
1338
  add_smb(ap)
1344
1339
  add_safety(ap)
1345
- add_salt(ap, fk_salt, ah_salt)
1340
+ add_salt(ap, fk_salt, dk_salt, ah_salt)
1346
1341
  add_optouts(ap)
1347
1342
  add_shutdown(ap)
1348
1343
  add_yolo(ap)
copyparty/__version__.py CHANGED
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 11, 1)
4
- CODENAME = "You Can (Not) Proceed"
5
- BUILD_DT = (2024, 3, 18)
3
+ VERSION = (1, 12, 0)
4
+ CODENAME = "locksmith"
5
+ BUILD_DT = (2024, 4, 6)
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
@@ -548,7 +548,12 @@ class VFS(object):
548
548
  # no vfs nodes in the list of real inodes
549
549
  real = [x for x in real if x[0] not in self.nodes]
550
550
 
551
+ dbv = self.dbv or self
551
552
  for name, vn2 in sorted(self.nodes.items()):
553
+ if vn2.dbv == dbv and self.flags.get("dk"):
554
+ virt_vis[name] = vn2
555
+ continue
556
+
552
557
  ok = False
553
558
  zx = vn2.axs
554
559
  axs = [zx.uread, zx.uwrite, zx.umove, zx.udel, zx.uget]
@@ -1217,7 +1222,9 @@ class AuthSrv(object):
1217
1222
  if un.startswith("@"):
1218
1223
  grp = un[1:]
1219
1224
  uns = [x[0] for x in un_gns.items() if grp in x[1]]
1220
- if not uns and grp != "${g}" and not self.args.idp_h_grp:
1225
+ if grp == "${g}":
1226
+ unames.append(un)
1227
+ elif not uns and not self.args.idp_h_grp:
1221
1228
  t = "group [%s] must be defined with --grp argument (or in a [groups] config section)"
1222
1229
  raise CfgEx(t % (grp,))
1223
1230
 
@@ -1227,31 +1234,28 @@ class AuthSrv(object):
1227
1234
 
1228
1235
  # unames may still contain ${u} and ${g} so now expand those;
1229
1236
  un_gn = [(un, gn) for un, gns in un_gns.items() for gn in gns]
1230
- if "*" not in un_gns:
1231
- # need ("*","") to match "*" in unames
1232
- un_gn.append(("*", ""))
1233
-
1234
- for _, dst, vu, vg in vols:
1235
- unames2 = set()
1236
- for un, gn in un_gn:
1237
- # if vu/vg (volume user/group) is non-null,
1238
- # then each non-null value corresponds to
1239
- # ${u}/${g}; consider this a filter to
1240
- # apply to unames, as well as un_gn
1241
- if (vu and vu != un) or (vg and vg != gn):
1242
- continue
1243
1237
 
1244
- for uname in unames + ([un] if vu or vg else []):
1245
- if uname == "${u}":
1246
- uname = vu or un
1247
- elif uname in ("${g}", "@${g}"):
1248
- uname = vg or gn
1238
+ for src, dst, vu, vg in vols:
1239
+ unames2 = set(unames)
1249
1240
 
1250
- if vu and vu != uname:
1251
- continue
1241
+ if "${u}" in unames:
1242
+ if not vu:
1243
+ t = "cannot use ${u} in accs of volume [%s] because the volume url does not contain ${u}"
1244
+ raise CfgEx(t % (src,))
1245
+ unames2.add(vu)
1246
+
1247
+ if "@${g}" in unames:
1248
+ if not vg:
1249
+ t = "cannot use @${g} in accs of volume [%s] because the volume url does not contain @${g}"
1250
+ raise CfgEx(t % (src,))
1251
+ unames2.update([un for un, gn in un_gn if gn == vg])
1252
1252
 
1253
- if uname:
1254
- unames2.add(uname)
1253
+ if "${g}" in unames:
1254
+ t = 'the accs of volume [%s] contains "${g}" but the only supported way of specifying that is "@${g}"'
1255
+ raise CfgEx(t % (src,))
1256
+
1257
+ unames2.discard("${u}")
1258
+ unames2.discard("@${g}")
1255
1259
 
1256
1260
  self._read_vol_str(lvl, list(unames2), axs[dst])
1257
1261
 
@@ -1675,6 +1679,20 @@ class AuthSrv(object):
1675
1679
  vol.flags["fk"] = int(fk) if fk is not True else 8
1676
1680
  have_fk = True
1677
1681
 
1682
+ dk = vol.flags.get("dk")
1683
+ dks = vol.flags.get("dks")
1684
+ dky = vol.flags.get("dky")
1685
+ if dks is not None and dky is not None:
1686
+ t = "WARNING: volume /%s has both dks and dky enabled; this is too yolo and not permitted"
1687
+ raise Exception(t % (vol.vpath,))
1688
+
1689
+ if dks and not dk:
1690
+ dk = dks
1691
+ if dky and not dk:
1692
+ dk = dky
1693
+ if dk:
1694
+ vol.flags["dk"] = int(dk) if dk is not True else 8
1695
+
1678
1696
  if have_fk and re.match(r"^[0-9\.]+$", self.args.fk_salt):
1679
1697
  self.log("filekey salt: {}".format(self.args.fk_salt))
1680
1698
 
copyparty/ftpd.py CHANGED
@@ -213,7 +213,7 @@ class FtpFs(AbstractedFS):
213
213
  raise FSE("Cannot open existing file for writing")
214
214
 
215
215
  self.validpath(ap)
216
- return open(fsenc(ap), mode)
216
+ return open(fsenc(ap), mode, self.args.iobuf)
217
217
 
218
218
  def chdir(self, path ) :
219
219
  nwd = join(self.cwd, path)
copyparty/httpcli.py CHANGED
@@ -36,6 +36,7 @@ from .bos import bos
36
36
  from .star import StreamTar
37
37
  from .sutil import StreamArc, gfilter
38
38
  from .szip import StreamZip
39
+ from .util import unquote # type: ignore
39
40
  from .util import (
40
41
  APPLESAN_RE,
41
42
  BITNESS,
@@ -84,7 +85,6 @@ from .util import (
84
85
  sendfile_py,
85
86
  undot,
86
87
  unescape_cookie,
87
- unquote, # type: ignore
88
88
  unquotep,
89
89
  vjoin,
90
90
  vol_san,
@@ -170,7 +170,6 @@ class HttpCli(object):
170
170
  self.parser = None
171
171
  # end placeholders
172
172
 
173
- self.bufsz = 1024 * 32
174
173
  self.html_head = ""
175
174
 
176
175
  def log(self, msg , c = 0) :
@@ -1607,15 +1606,16 @@ class HttpCli(object):
1607
1606
  return enc or "utf-8"
1608
1607
 
1609
1608
  def get_body_reader(self) :
1609
+ bufsz = self.args.s_rd_sz
1610
1610
  if "chunked" in self.headers.get("transfer-encoding", "").lower():
1611
- return read_socket_chunked(self.sr), -1
1611
+ return read_socket_chunked(self.sr, bufsz), -1
1612
1612
 
1613
1613
  remains = int(self.headers.get("content-length", -1))
1614
1614
  if remains == -1:
1615
1615
  self.keepalive = False
1616
- return read_socket_unbounded(self.sr), remains
1616
+ return read_socket_unbounded(self.sr, bufsz), remains
1617
1617
  else:
1618
- return read_socket(self.sr, remains), remains
1618
+ return read_socket(self.sr, bufsz, remains), remains
1619
1619
 
1620
1620
  def dump_to_file(self, is_put ) :
1621
1621
  # post_sz, sha_hex, sha_b64, remains, path, url
@@ -1637,7 +1637,7 @@ class HttpCli(object):
1637
1637
  bos.makedirs(fdir)
1638
1638
 
1639
1639
  open_ka = {"fun": open}
1640
- open_a = ["wb", 512 * 1024]
1640
+ open_a = ["wb", self.args.iobuf]
1641
1641
 
1642
1642
  # user-request || config-force
1643
1643
  if ("gz" in vfs.flags or "xz" in vfs.flags) and (
@@ -1896,7 +1896,7 @@ class HttpCli(object):
1896
1896
  f.seek(ofs)
1897
1897
  with open(fp, "wb") as fo:
1898
1898
  while nrem:
1899
- buf = f.read(min(nrem, 512 * 1024))
1899
+ buf = f.read(min(nrem, self.args.iobuf))
1900
1900
  if not buf:
1901
1901
  break
1902
1902
 
@@ -1918,7 +1918,7 @@ class HttpCli(object):
1918
1918
  return "%s %s n%s" % (spd1, spd2, self.conn.nreq)
1919
1919
 
1920
1920
  def handle_post_multipart(self) :
1921
- self.parser = MultipartParser(self.log, self.sr, self.headers)
1921
+ self.parser = MultipartParser(self.log, self.args, self.sr, self.headers)
1922
1922
  self.parser.parse()
1923
1923
 
1924
1924
  file0 = []
@@ -1962,7 +1962,12 @@ class HttpCli(object):
1962
1962
 
1963
1963
  v = self.uparam[k]
1964
1964
 
1965
- vn, rem = self.asrv.vfs.get(self.vpath, self.uname, True, False)
1965
+ if self._use_dirkey():
1966
+ vn = self.vn
1967
+ rem = self.rem
1968
+ else:
1969
+ vn, rem = self.asrv.vfs.get(self.vpath, self.uname, True, False)
1970
+
1966
1971
  zs = self.parser.require("files", 1024 * 1024)
1967
1972
  if not zs:
1968
1973
  raise Pebkac(422, "need files list")
@@ -2147,7 +2152,7 @@ class HttpCli(object):
2147
2152
 
2148
2153
  self.log("writing {} #{} @{} len {}".format(path, chash, cstart, remains))
2149
2154
 
2150
- reader = read_socket(self.sr, remains)
2155
+ reader = read_socket(self.sr, self.args.s_rd_sz, remains)
2151
2156
 
2152
2157
  f = None
2153
2158
  fpool = not self.args.no_fpool and sprs
@@ -2158,7 +2163,7 @@ class HttpCli(object):
2158
2163
  except:
2159
2164
  pass
2160
2165
 
2161
- f = f or open(fsenc(path), "rb+", 512 * 1024)
2166
+ f = f or open(fsenc(path), "rb+", self.args.iobuf)
2162
2167
 
2163
2168
  try:
2164
2169
  f.seek(cstart[0])
@@ -2181,7 +2186,8 @@ class HttpCli(object):
2181
2186
  )
2182
2187
  ofs = 0
2183
2188
  while ofs < chunksize:
2184
- bufsz = min(chunksize - ofs, 4 * 1024 * 1024)
2189
+ bufsz = max(4 * 1024 * 1024, self.args.iobuf)
2190
+ bufsz = min(chunksize - ofs, bufsz)
2185
2191
  f.seek(cstart[0] + ofs)
2186
2192
  buf = f.read(bufsz)
2187
2193
  for wofs in cstart[1:]:
@@ -2434,6 +2440,18 @@ class HttpCli(object):
2434
2440
  suffix = "-{:.6f}-{}".format(time.time(), dip)
2435
2441
  open_args = {"fdir": fdir, "suffix": suffix}
2436
2442
 
2443
+ if "replace" in self.uparam:
2444
+ abspath = os.path.join(fdir, fname)
2445
+ if not self.can_delete:
2446
+ self.log("user not allowed to overwrite with ?replace")
2447
+ elif bos.path.exists(abspath):
2448
+ try:
2449
+ wunlink(self.log, abspath, vfs.flags)
2450
+ t = "overwriting file with new upload: %s"
2451
+ except:
2452
+ t = "toctou while deleting for ?replace: %s"
2453
+ self.log(t % (abspath,))
2454
+
2437
2455
  # reserve destination filename
2438
2456
  with ren_open(fname, "wb", fdir=fdir, suffix=suffix) as zfw:
2439
2457
  fname = zfw["orz"][1]
@@ -2478,7 +2496,7 @@ class HttpCli(object):
2478
2496
  v2 = lim.dfv - lim.dfl
2479
2497
  max_sz = min(v1, v2) if v1 and v2 else v1 or v2
2480
2498
 
2481
- with ren_open(tnam, "wb", 512 * 1024, **open_args) as zfw:
2499
+ with ren_open(tnam, "wb", self.args.iobuf, **open_args) as zfw:
2482
2500
  f, tnam = zfw["orz"]
2483
2501
  tabspath = os.path.join(fdir, tnam)
2484
2502
  self.log("writing to {}".format(tabspath))
@@ -2774,7 +2792,7 @@ class HttpCli(object):
2774
2792
  if bos.path.exists(fp):
2775
2793
  wunlink(self.log, fp, vfs.flags)
2776
2794
 
2777
- with open(fsenc(fp), "wb", 512 * 1024) as f:
2795
+ with open(fsenc(fp), "wb", self.args.iobuf) as f:
2778
2796
  sz, sha512, _ = hashcopy(p_data, f, self.args.s_wr_slp)
2779
2797
 
2780
2798
  if lim:
@@ -2853,6 +2871,30 @@ class HttpCli(object):
2853
2871
 
2854
2872
  return file_lastmod, True
2855
2873
 
2874
+ def _use_dirkey(self, ap = "") :
2875
+ if self.can_read or not self.can_get:
2876
+ return False
2877
+
2878
+ if self.vn.flags.get("dky"):
2879
+ return True
2880
+
2881
+ req = self.uparam.get("k") or ""
2882
+ if not req:
2883
+ return False
2884
+
2885
+ dk_len = self.vn.flags.get("dk")
2886
+ if not dk_len:
2887
+ return False
2888
+
2889
+ ap = ap or self.vn.canonical(self.rem)
2890
+ zs = self.gen_fk(2, self.args.dk_salt, ap, 0, 0)[:dk_len]
2891
+ if req == zs:
2892
+ return True
2893
+
2894
+ t = "wrong dirkey, want %s, got %s\n vp: %s\n ap: %s"
2895
+ self.log(t % (zs, req, self.req, ap), 6)
2896
+ return False
2897
+
2856
2898
  def _expand(self, txt , phs ) :
2857
2899
  for ph in phs:
2858
2900
  if ph.startswith("hdr."):
@@ -3006,8 +3048,7 @@ class HttpCli(object):
3006
3048
  upper = gzip_orig_sz(fs_path)
3007
3049
  else:
3008
3050
  open_func = open
3009
- # 512 kB is optimal for huge files, use 64k
3010
- open_args = [fsenc(fs_path), "rb", 64 * 1024]
3051
+ open_args = [fsenc(fs_path), "rb", self.args.iobuf]
3011
3052
  use_sendfile = (
3012
3053
  # fmt: off
3013
3054
  not self.tls
@@ -3132,7 +3173,7 @@ class HttpCli(object):
3132
3173
  # for f in fgen: print(repr({k: f[k] for k in ["vp", "ap"]}))
3133
3174
  cfmt = ""
3134
3175
  if self.thumbcli and not self.args.no_bacode:
3135
- for zs in ("opus", "w", "j"):
3176
+ for zs in ("opus", "mp3", "w", "j"):
3136
3177
  if zs in self.ouparam or uarg == zs:
3137
3178
  cfmt = zs
3138
3179
 
@@ -3142,6 +3183,7 @@ class HttpCli(object):
3142
3183
 
3143
3184
  bgen = packer(
3144
3185
  self.log,
3186
+ self.args,
3145
3187
  fgen,
3146
3188
  utf8="utf" in uarg,
3147
3189
  pre_crc="crc" in uarg,
@@ -3219,7 +3261,7 @@ class HttpCli(object):
3219
3261
  sz_md = 0
3220
3262
  lead = b""
3221
3263
  fullfile = b""
3222
- for buf in yieldfile(fs_path):
3264
+ for buf in yieldfile(fs_path, self.args.iobuf):
3223
3265
  if sz_md < max_sz:
3224
3266
  fullfile += buf
3225
3267
  else:
@@ -3292,7 +3334,7 @@ class HttpCli(object):
3292
3334
  if fullfile:
3293
3335
  self.s.sendall(fullfile)
3294
3336
  else:
3295
- for buf in yieldfile(fs_path):
3337
+ for buf in yieldfile(fs_path, self.args.iobuf):
3296
3338
  self.s.sendall(html_bescape(buf))
3297
3339
 
3298
3340
  self.s.sendall(html[1])
@@ -3540,7 +3582,7 @@ class HttpCli(object):
3540
3582
 
3541
3583
  dst = dst[len(top) + 1 :]
3542
3584
 
3543
- ret = self.gen_tree(top, dst)
3585
+ ret = self.gen_tree(top, dst, self.uparam.get("k", ""))
3544
3586
  if self.is_vproxied:
3545
3587
  parents = self.args.R.split("/")
3546
3588
  for parent in reversed(parents):
@@ -3550,18 +3592,25 @@ class HttpCli(object):
3550
3592
  self.reply(zs.encode("utf-8"), mime="application/json")
3551
3593
  return True
3552
3594
 
3553
- def gen_tree(self, top , target ) :
3595
+ def gen_tree(self, top , target , dk ) :
3554
3596
  ret = {}
3555
3597
  excl = None
3556
3598
  if target:
3557
3599
  excl, target = (target.split("/", 1) + [""])[:2]
3558
- sub = self.gen_tree("/".join([top, excl]).strip("/"), target)
3600
+ sub = self.gen_tree("/".join([top, excl]).strip("/"), target, dk)
3559
3601
  ret["k" + quotep(excl)] = sub
3560
3602
 
3561
3603
  vfs = self.asrv.vfs
3604
+ dk_sz = False
3605
+ if dk:
3606
+ vn, rem = vfs.get(top, self.uname, False, False)
3607
+ if vn.flags.get("dks") and self._use_dirkey(vn.canonical(rem)):
3608
+ dk_sz = vn.flags.get("dk")
3609
+
3562
3610
  dots = False
3611
+ fsroot = ""
3563
3612
  try:
3564
- vn, rem = vfs.get(top, self.uname, True, False)
3613
+ vn, rem = vfs.get(top, self.uname, not dk_sz, False)
3565
3614
  fsroot, vfs_ls, vfs_virt = vn.ls(
3566
3615
  rem,
3567
3616
  self.uname,
@@ -3569,7 +3618,9 @@ class HttpCli(object):
3569
3618
  [[True, False], [False, True]],
3570
3619
  )
3571
3620
  dots = self.uname in vn.axs.udot
3621
+ dk_sz = vn.flags.get("dk")
3572
3622
  except:
3623
+ dk_sz = None
3573
3624
  vfs_ls = []
3574
3625
  vfs_virt = {}
3575
3626
  for v in self.rvol:
@@ -3584,6 +3635,14 @@ class HttpCli(object):
3584
3635
 
3585
3636
  dirs = [quotep(x) for x in dirs if x != excl]
3586
3637
 
3638
+ if dk_sz and fsroot:
3639
+ kdirs = []
3640
+ for dn in dirs:
3641
+ ap = os.path.join(fsroot, dn)
3642
+ zs = self.gen_fk(2, self.args.dk_salt, ap, 0, 0)[:dk_sz]
3643
+ kdirs.append(dn + "?k=" + zs)
3644
+ dirs = kdirs
3645
+
3587
3646
  for x in vfs_virt:
3588
3647
  if x != excl:
3589
3648
  try:
@@ -3848,6 +3907,7 @@ class HttpCli(object):
3848
3907
  self.out_headers["X-Robots-Tag"] = "noindex, nofollow"
3849
3908
 
3850
3909
  is_dir = stat.S_ISDIR(st.st_mode)
3910
+ is_dk = False
3851
3911
  fk_pass = False
3852
3912
  icur = None
3853
3913
  if is_dir and (e2t or e2d):
@@ -3856,7 +3916,7 @@ class HttpCli(object):
3856
3916
  icur = idx.get_cur(dbv.realpath)
3857
3917
 
3858
3918
  th_fmt = self.uparam.get("th")
3859
- if self.can_read:
3919
+ if self.can_read or (self.can_get and vn.flags.get("dk")):
3860
3920
  if th_fmt is not None:
3861
3921
  nothumb = "dthumb" in dbv.flags
3862
3922
  if is_dir:
@@ -3962,8 +4022,11 @@ class HttpCli(object):
3962
4022
 
3963
4023
  return self.tx_file(abspath)
3964
4024
 
3965
- elif is_dir and not self.can_read and not self.can_write:
3966
- return self.tx_404(True)
4025
+ elif is_dir and not self.can_read:
4026
+ if self._use_dirkey(abspath):
4027
+ is_dk = True
4028
+ elif not self.can_write:
4029
+ return self.tx_404(True)
3967
4030
 
3968
4031
  srv_info = []
3969
4032
 
@@ -3985,7 +4048,7 @@ class HttpCli(object):
3985
4048
  srv_infot = "</span> // <span>".join(srv_info)
3986
4049
 
3987
4050
  perms = []
3988
- if self.can_read:
4051
+ if self.can_read or is_dk:
3989
4052
  perms.append("read")
3990
4053
  if self.can_write:
3991
4054
  perms.append("write")
@@ -4113,7 +4176,7 @@ class HttpCli(object):
4113
4176
  if not self.conn.hsrv.prism:
4114
4177
  j2a["no_prism"] = True
4115
4178
 
4116
- if not self.can_read:
4179
+ if not self.can_read and not is_dk:
4117
4180
  if is_ls:
4118
4181
  return self.tx_ls(ls_ret)
4119
4182
 
@@ -4166,8 +4229,15 @@ class HttpCli(object):
4166
4229
  ):
4167
4230
  ls_names = exclude_dotfiles(ls_names)
4168
4231
 
4232
+ add_dk = vf.get("dk")
4169
4233
  add_fk = vf.get("fk")
4170
4234
  fk_alg = 2 if "fka" in vf else 1
4235
+ if add_dk:
4236
+ if vf.get("dky"):
4237
+ add_dk = False
4238
+ else:
4239
+ zs = self.gen_fk(2, self.args.dk_salt, abspath, 0, 0)[:add_dk]
4240
+ ls_ret["dk"] = cgv["dk"] = zs
4171
4241
 
4172
4242
  dirs = []
4173
4243
  files = []
@@ -4195,6 +4265,12 @@ class HttpCli(object):
4195
4265
  href += "/"
4196
4266
  if self.args.no_zip:
4197
4267
  margin = "DIR"
4268
+ elif add_dk:
4269
+ zs = absreal(fspath)
4270
+ margin = '<a href="%s?k=%s&zip" rel="nofollow">zip</a>' % (
4271
+ quotep(href),
4272
+ self.gen_fk(2, self.args.dk_salt, zs, 0, 0)[:add_dk],
4273
+ )
4198
4274
  else:
4199
4275
  margin = '<a href="%s?zip" rel="nofollow">zip</a>' % (quotep(href),)
4200
4276
  elif fn in hist:
@@ -4235,6 +4311,11 @@ class HttpCli(object):
4235
4311
  0 if ANYWIN else inf.st_ino,
4236
4312
  )[:add_fk],
4237
4313
  )
4314
+ elif add_dk and is_dir:
4315
+ href = "%s?k=%s" % (
4316
+ quotep(href),
4317
+ self.gen_fk(2, self.args.dk_salt, fspath, 0, 0)[:add_dk],
4318
+ )
4238
4319
  else:
4239
4320
  href = quotep(href)
4240
4321
 
@@ -4253,6 +4334,9 @@ class HttpCli(object):
4253
4334
  files.append(item)
4254
4335
  item["rd"] = rem
4255
4336
 
4337
+ if is_dk and not vf.get("dks"):
4338
+ dirs = []
4339
+
4256
4340
  if (
4257
4341
  self.cookies.get("idxh") == "y"
4258
4342
  and "ls" not in self.uparam
copyparty/mtag.py CHANGED
@@ -545,8 +545,7 @@ class MTag(object):
545
545
  pypath = str(os.pathsep.join(zsl))
546
546
  env["PYTHONPATH"] = pypath
547
547
  except:
548
- if not E.ox and not EXE:
549
- raise
548
+ raise # might be expected outside cpython
550
549
 
551
550
  ret = {}
552
551
  for tagname, parser in sorted(parsers.items(), key=lambda x: (x[1].pri, x[0])):
copyparty/star.py CHANGED
@@ -1,6 +1,7 @@
1
1
  # coding: utf-8
2
2
  from __future__ import print_function, unicode_literals
3
3
 
4
+ import argparse
4
5
  import re
5
6
  import stat
6
7
  import tarfile
@@ -38,11 +39,12 @@ class StreamTar(StreamArc):
38
39
  def __init__(
39
40
  self,
40
41
  log ,
42
+ args ,
41
43
  fgen ,
42
44
  cmp = "",
43
45
  **kwargs
44
46
  ):
45
- super(StreamTar, self).__init__(log, fgen)
47
+ super(StreamTar, self).__init__(log, args, fgen)
46
48
 
47
49
  self.ci = 0
48
50
  self.co = 0
@@ -120,7 +122,7 @@ class StreamTar(StreamArc):
120
122
  inf.gid = 0
121
123
 
122
124
  self.ci += inf.size
123
- with open(fsenc(src), "rb", 512 * 1024) as fo:
125
+ with open(fsenc(src), "rb", self.args.iobuf) as fo:
124
126
  self.tar.addfile(inf, fo)
125
127
 
126
128
  def _gen(self) :