copyparty 1.15.0__py3-none-any.whl → 1.15.2__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
@@ -16,6 +16,7 @@ except:
16
16
  TYPE_CHECKING = False
17
17
 
18
18
  PY2 = sys.version_info < (3,)
19
+ PY36 = sys.version_info > (3, 6)
19
20
  if not PY2:
20
21
  unicode = str
21
22
  else:
copyparty/__main__.py CHANGED
@@ -27,6 +27,7 @@ from .__init__ import (
27
27
  EXE,
28
28
  MACOS,
29
29
  PY2,
30
+ PY36,
30
31
  VT100,
31
32
  WINDOWS,
32
33
  E,
@@ -54,6 +55,7 @@ from .util import (
54
55
  Daemon,
55
56
  align_tab,
56
57
  ansi_re,
58
+ b64enc,
57
59
  dedent,
58
60
  min_ex,
59
61
  pybin,
@@ -198,7 +200,7 @@ def init_E(EE ) :
198
200
  errs.append("Using [%s] instead" % (p,))
199
201
 
200
202
  if errs:
201
- print("WARNING: " + ". ".join(errs))
203
+ warn(". ".join(errs))
202
204
 
203
205
  return p # type: ignore
204
206
  except Exception as ex:
@@ -228,7 +230,7 @@ def init_E(EE ) :
228
230
  raise
229
231
 
230
232
 
231
- def get_srvname() :
233
+ def get_srvname(verbose) :
232
234
  try:
233
235
  ret = unicode(socket.gethostname()).split(".")[0]
234
236
  except:
@@ -238,7 +240,8 @@ def get_srvname() :
238
240
  return ret
239
241
 
240
242
  fp = os.path.join(E.cfg, "name.txt")
241
- lprint("using hostname from {}\n".format(fp))
243
+ if verbose:
244
+ lprint("using hostname from {}\n".format(fp))
242
245
  try:
243
246
  with open(fp, "rb") as f:
244
247
  ret = f.read().decode("utf-8", "replace").strip()
@@ -260,7 +263,7 @@ def get_fk_salt() :
260
263
  with open(fp, "rb") as f:
261
264
  ret = f.read().strip()
262
265
  except:
263
- ret = base64.b64encode(os.urandom(18))
266
+ ret = b64enc(os.urandom(18))
264
267
  with open(fp, "wb") as f:
265
268
  f.write(ret + b"\n")
266
269
 
@@ -273,7 +276,7 @@ def get_dk_salt() :
273
276
  with open(fp, "rb") as f:
274
277
  ret = f.read().strip()
275
278
  except:
276
- ret = base64.b64encode(os.urandom(30))
279
+ ret = b64enc(os.urandom(30))
277
280
  with open(fp, "wb") as f:
278
281
  f.write(ret + b"\n")
279
282
 
@@ -286,7 +289,7 @@ def get_ah_salt() :
286
289
  with open(fp, "rb") as f:
287
290
  ret = f.read().strip()
288
291
  except:
289
- ret = base64.b64encode(os.urandom(18))
292
+ ret = b64enc(os.urandom(18))
290
293
  with open(fp, "wb") as f:
291
294
  f.write(ret + b"\n")
292
295
 
@@ -344,7 +347,6 @@ def configure_ssl_ver(al ) :
344
347
  # oh man i love openssl
345
348
  # check this out
346
349
  # hold my beer
347
- assert ssl # type: ignore
348
350
  ptn = re.compile(r"^OP_NO_(TLS|SSL)v")
349
351
  sslver = terse_sslver(al.ssl_ver).split(",")
350
352
  flags = [k for k in ssl.__dict__ if ptn.match(k)]
@@ -378,7 +380,6 @@ def configure_ssl_ver(al ) :
378
380
 
379
381
 
380
382
  def configure_ssl_ciphers(al ) :
381
- assert ssl # type: ignore
382
383
  ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
383
384
  if al.ssl_ver:
384
385
  ctx.options &= ~al.ssl_flags_en
@@ -1061,6 +1062,7 @@ def add_cert(ap, cert_path):
1061
1062
 
1062
1063
 
1063
1064
  def add_auth(ap):
1065
+ ses_db = os.path.join(E.cfg, "sessions.db")
1064
1066
  ap2 = ap.add_argument_group('IdP / identity provider / user authentication options')
1065
1067
  ap2.add_argument("--idp-h-usr", metavar="HN", type=u, default="", help="bypass the copyparty authentication checks and assume the request-header \033[33mHN\033[0m contains the username of the requesting user (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
1066
1068
  ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control")
@@ -1068,6 +1070,9 @@ def add_auth(ap):
1068
1070
  ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m")
1069
1071
  ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app")
1070
1072
  ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins")
1073
+ ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)")
1074
+ ap2.add_argument("--ses-len", metavar="CHARS", type=int, default=20, help="session key length; default is 120 bits ((20//4)*4*6)")
1075
+ ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies")
1071
1076
 
1072
1077
 
1073
1078
  def add_chpw(ap):
@@ -1220,6 +1225,7 @@ def add_optouts(ap):
1220
1225
  ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
1221
1226
  ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)")
1222
1227
  ap2.add_argument("--no-lifetime", action="store_true", help="do not allow clients (or server config) to schedule an upload to be deleted after a given time")
1228
+ ap2.add_argument("--no-up-list", action="store_true", help="don't show list of incoming files in controlpanel")
1223
1229
  ap2.add_argument("--no-pipe", action="store_true", help="disable race-the-beam (lockstep download of files which are currently being uploaded) (volflag=nopipe)")
1224
1230
  ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader IPs into the database")
1225
1231
 
@@ -1348,6 +1354,8 @@ def add_db_general(ap, hcores):
1348
1354
  ap2.add_argument("--hist", metavar="PATH", type=u, default="", help="where to store volume data (db, thumbs); default is a folder named \".hist\" inside each volume (volflag=hist)")
1349
1355
  ap2.add_argument("--no-hash", metavar="PTN", type=u, default="", help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (volflag=nohash)")
1350
1356
  ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scans (volflag=noidx)")
1357
+ ap2.add_argument("--no-dirsz", action="store_true", help="do not show total recursive size of folders in listings, show inode size instead; slightly faster (volflag=nodirsz)")
1358
+ ap2.add_argument("--re-dirsz", action="store_true", help="if the directory-sizes in the UI are bonkers, use this along with \033[33m-e2dsa\033[0m to rebuild the index from scratch")
1351
1359
  ap2.add_argument("--no-dhash", action="store_true", help="disable rescan acceleration; do full database integrity check -- makes the db ~5%% smaller and bootup/rescans 3~10x slower")
1352
1360
  ap2.add_argument("--re-dhash", action="store_true", help="force a cache rebuild on startup; enable this once if it gets out of sync (should never be necessary)")
1353
1361
  ap2.add_argument("--no-forget", action="store_true", help="never forget indexed files, even when deleted from disk -- makes it impossible to ever upload the same file twice -- only useful for offloading uploads to a cloud service or something (volflag=noforget)")
@@ -1461,10 +1469,11 @@ def add_debug(ap):
1461
1469
 
1462
1470
 
1463
1471
  def run_argparse(
1464
- argv , formatter , retry , nc
1472
+ argv , formatter , retry , nc , verbose=True
1465
1473
  ) :
1466
1474
  ap = argparse.ArgumentParser(
1467
1475
  formatter_class=formatter,
1476
+ usage=argparse.SUPPRESS,
1468
1477
  prog="copyparty",
1469
1478
  description="http file sharing hub v{} ({})".format(S_VERSION, S_BUILD_DT),
1470
1479
  )
@@ -1482,7 +1491,7 @@ def run_argparse(
1482
1491
 
1483
1492
  tty = os.environ.get("TERM", "").lower() == "linux"
1484
1493
 
1485
- srvname = get_srvname()
1494
+ srvname = get_srvname(verbose)
1486
1495
 
1487
1496
  add_general(ap, nc, srvname)
1488
1497
  add_network(ap)
@@ -1662,7 +1671,7 @@ def main(argv = None, rsrc = None) :
1662
1671
  for fmtr in [RiceFormatter, RiceFormatter, Dodge11874, BasicDodge11874]:
1663
1672
  try:
1664
1673
  al = run_argparse(argv, fmtr, retry, nc)
1665
- dal = run_argparse([], fmtr, retry, nc)
1674
+ dal = run_argparse([], fmtr, retry, nc, False)
1666
1675
  break
1667
1676
  except SystemExit:
1668
1677
  raise
@@ -1746,7 +1755,7 @@ def main(argv = None, rsrc = None) :
1746
1755
  print("error: python2 cannot --smb")
1747
1756
  return
1748
1757
 
1749
- if sys.version_info < (3, 6):
1758
+ if not PY36:
1750
1759
  al.no_scandir = True
1751
1760
 
1752
1761
  if not hasattr(os, "sendfile"):
copyparty/__version__.py CHANGED
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 15, 0)
3
+ VERSION = (1, 15, 2)
4
4
  CODENAME = "fill the drives"
5
- BUILD_DT = (2024, 9, 8)
5
+ BUILD_DT = (2024, 9, 16)
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
@@ -833,8 +833,10 @@ class AuthSrv(object):
833
833
 
834
834
  # fwd-decl
835
835
  self.vfs = VFS(log_func, "", "", AXS(), {})
836
- self.acct = {}
837
- self.iacct = {}
836
+ self.acct = {} # uname->pw
837
+ self.iacct = {} # pw->uname
838
+ self.ases = {} # uname->session
839
+ self.sesa = {} # session->uname
838
840
  self.defpw = {}
839
841
  self.grps = {}
840
842
  self.re_pwd = None
@@ -846,6 +848,7 @@ class AuthSrv(object):
846
848
  self.idp_accs = {} # username->groupnames
847
849
  self.idp_usr_gh = {} # username->group-header-value (cache)
848
850
 
851
+ self.hid_cache = {}
849
852
  self.mutex = threading.Lock()
850
853
  self.reload()
851
854
 
@@ -1522,7 +1525,7 @@ class AuthSrv(object):
1522
1525
  if enshare:
1523
1526
  import sqlite3
1524
1527
 
1525
- shv = VFS(self.log_func, "", shr, AXS(), {"d2d": True})
1528
+ shv = VFS(self.log_func, "", shr, AXS(), {})
1526
1529
 
1527
1530
  db_path = self.args.shr_db
1528
1531
  db = sqlite3.connect(db_path)
@@ -1541,8 +1544,8 @@ class AuthSrv(object):
1541
1544
  if s_pw:
1542
1545
  # gotta reuse the "account" for all shares with this pw,
1543
1546
  # so do a light scramble as this appears in the web-ui
1544
- zs = ub64enc(hashlib.sha512(s_pw.encode("utf-8")).digest())[4:16]
1545
- sun = "s_%s" % (zs.decode("utf-8"),)
1547
+ zb = hashlib.sha512(s_pw.encode("utf-8")).digest()
1548
+ sun = "s_%s" % (ub64enc(zb)[4:16].decode("ascii"),)
1546
1549
  acct[sun] = s_pw
1547
1550
  else:
1548
1551
  sun = "*"
@@ -1647,8 +1650,12 @@ class AuthSrv(object):
1647
1650
  promote = []
1648
1651
  demote = []
1649
1652
  for vol in vfs.all_vols.values():
1650
- zb = hashlib.sha512(afsenc(vol.realpath)).digest()
1651
- hid = base64.b32encode(zb).decode("ascii").lower()
1653
+ hid = self.hid_cache.get(vol.realpath)
1654
+ if not hid:
1655
+ zb = hashlib.sha512(afsenc(vol.realpath)).digest()
1656
+ hid = base64.b32encode(zb).decode("ascii").lower()
1657
+ self.hid_cache[vol.realpath] = hid
1658
+
1652
1659
  vflag = vol.flags.get("hist")
1653
1660
  if vflag == "-":
1654
1661
  pass
@@ -2174,8 +2181,11 @@ class AuthSrv(object):
2174
2181
  self.grps = grps
2175
2182
  self.iacct = {v: k for k, v in acct.items()}
2176
2183
 
2184
+ self.load_sessions()
2185
+
2177
2186
  self.re_pwd = None
2178
2187
  pwds = [re.escape(x) for x in self.iacct.keys()]
2188
+ pwds.extend(list(self.sesa))
2179
2189
  if pwds:
2180
2190
  if self.ah.on:
2181
2191
  zs = r"(\[H\] pw:.*|[?&]pw=)([^&]+)"
@@ -2250,6 +2260,72 @@ class AuthSrv(object):
2250
2260
  cur.close()
2251
2261
  db.close()
2252
2262
 
2263
+ def load_sessions(self, quiet=False) :
2264
+ # mutex me
2265
+ if self.args.no_ses:
2266
+ self.ases = {}
2267
+ self.sesa = {}
2268
+ return
2269
+
2270
+ import sqlite3
2271
+
2272
+ ases = {}
2273
+ blen = (self.args.ses_len // 4) * 4 # 3 bytes in 4 chars
2274
+ blen = (blen * 3) // 4 # bytes needed for ses_len chars
2275
+
2276
+ db = sqlite3.connect(self.args.ses_db)
2277
+ cur = db.cursor()
2278
+
2279
+ for uname, sid in cur.execute("select un, si from us"):
2280
+ if uname in self.acct:
2281
+ ases[uname] = sid
2282
+
2283
+ n = []
2284
+ q = "insert into us values (?,?,?)"
2285
+ for uname in self.acct:
2286
+ if uname not in ases:
2287
+ sid = ub64enc(os.urandom(blen)).decode("ascii")
2288
+ cur.execute(q, (uname, sid, int(time.time())))
2289
+ ases[uname] = sid
2290
+ n.append(uname)
2291
+
2292
+ if n:
2293
+ db.commit()
2294
+
2295
+ cur.close()
2296
+ db.close()
2297
+
2298
+ self.ases = ases
2299
+ self.sesa = {v: k for k, v in ases.items()}
2300
+ if n and not quiet:
2301
+ t = ", ".join(n[:3])
2302
+ if len(n) > 3:
2303
+ t += "..."
2304
+ self.log("added %d new sessions (%s)" % (len(n), t))
2305
+
2306
+ def forget_session(self, broker , uname ) :
2307
+ with self.mutex:
2308
+ self._forget_session(uname)
2309
+
2310
+ if broker:
2311
+ broker.ask("_reload_sessions").get()
2312
+
2313
+ def _forget_session(self, uname ) :
2314
+ if self.args.no_ses:
2315
+ return
2316
+
2317
+ import sqlite3
2318
+
2319
+ db = sqlite3.connect(self.args.ses_db)
2320
+ cur = db.cursor()
2321
+ cur.execute("delete from us where un = ?", (uname,))
2322
+ db.commit()
2323
+ cur.close()
2324
+ db.close()
2325
+
2326
+ self.sesa.pop(self.ases.get(uname, ""), "")
2327
+ self.ases.pop(uname, "")
2328
+
2253
2329
  def chpw(self, broker , uname, pw) :
2254
2330
  if not self.args.chpw:
2255
2331
  return False, "feature disabled in server config"
@@ -2269,7 +2345,7 @@ class AuthSrv(object):
2269
2345
  if hpw == self.acct[uname]:
2270
2346
  return False, "that's already your password my dude"
2271
2347
 
2272
- if hpw in self.iacct:
2348
+ if hpw in self.iacct or hpw in self.sesa:
2273
2349
  return False, "password is taken"
2274
2350
 
2275
2351
  with self.mutex:
copyparty/broker_mp.py CHANGED
@@ -9,7 +9,7 @@ import queue
9
9
 
10
10
  from .__init__ import CORES, TYPE_CHECKING
11
11
  from .broker_mpw import MpWorker
12
- from .broker_util import ExceptionalQueue, try_exec
12
+ from .broker_util import ExceptionalQueue, NotExQueue, try_exec
13
13
  from .util import Daemon, mp
14
14
 
15
15
  if TYPE_CHECKING:
@@ -72,6 +72,10 @@ class BrokerMp(object):
72
72
  for _, proc in enumerate(self.procs):
73
73
  proc.q_pend.put((0, "reload", []))
74
74
 
75
+ def reload_sessions(self) :
76
+ for _, proc in enumerate(self.procs):
77
+ proc.q_pend.put((0, "reload_sessions", []))
78
+
75
79
  def collector(self, proc ) :
76
80
  """receive message from hub in other process"""
77
81
  while True:
@@ -100,7 +104,7 @@ class BrokerMp(object):
100
104
  if retq_id:
101
105
  proc.q_pend.put((retq_id, "retq", rv))
102
106
 
103
- def ask(self, dest , *args ) :
107
+ def ask(self, dest , *args ) :
104
108
 
105
109
  # new non-ipc invoking managed service in hub
106
110
  obj = self.hub
copyparty/broker_mpw.py CHANGED
@@ -11,7 +11,7 @@ import queue
11
11
 
12
12
  from .__init__ import ANYWIN
13
13
  from .authsrv import AuthSrv
14
- from .broker_util import BrokerCli, ExceptionalQueue
14
+ from .broker_util import BrokerCli, ExceptionalQueue, NotExQueue
15
15
  from .httpsrv import HttpSrv
16
16
  from .util import FAKE_MP, Daemon, HMaccas
17
17
 
@@ -88,6 +88,10 @@ class MpWorker(BrokerCli):
88
88
  self.asrv.reload()
89
89
  self.logw("mpw.asrv reloaded")
90
90
 
91
+ elif dest == "reload_sessions":
92
+ with self.asrv.mutex:
93
+ self.asrv.load_sessions()
94
+
91
95
  elif dest == "listen":
92
96
  self.httpsrv.listen(args[0], args[1])
93
97
 
@@ -104,7 +108,7 @@ class MpWorker(BrokerCli):
104
108
  else:
105
109
  raise Exception("what is " + str(dest))
106
110
 
107
- def ask(self, dest , *args ) :
111
+ def ask(self, dest , *args ) :
108
112
  retq = ExceptionalQueue(1)
109
113
  retq_id = id(retq)
110
114
  with self.retpend_mutex:
copyparty/broker_thr.py CHANGED
@@ -5,7 +5,7 @@ import os
5
5
  import threading
6
6
 
7
7
  from .__init__ import TYPE_CHECKING
8
- from .broker_util import BrokerCli, ExceptionalQueue, try_exec
8
+ from .broker_util import BrokerCli, ExceptionalQueue, NotExQueue
9
9
  from .httpsrv import HttpSrv
10
10
  from .util import HMaccas
11
11
 
@@ -30,6 +30,7 @@ class BrokerThr(BrokerCli):
30
30
  self.iphash = HMaccas(os.path.join(self.args.E.cfg, "iphash"), 8)
31
31
  self.httpsrv = HttpSrv(self, None)
32
32
  self.reload = self.noop
33
+ self.reload_sessions = self.noop
33
34
 
34
35
  def shutdown(self) :
35
36
  # self.log("broker", "shutting down")
@@ -38,19 +39,14 @@ class BrokerThr(BrokerCli):
38
39
  def noop(self) :
39
40
  pass
40
41
 
41
- def ask(self, dest , *args ) :
42
+ def ask(self, dest , *args ) :
42
43
 
43
44
  # new ipc invoking managed service in hub
44
45
  obj = self.hub
45
46
  for node in dest.split("."):
46
47
  obj = getattr(obj, node)
47
48
 
48
- rv = try_exec(True, obj, *args)
49
-
50
- # pretend we're broker_mp
51
- retq = ExceptionalQueue(1)
52
- retq.put(rv)
53
- return retq
49
+ return NotExQueue(obj(*args)) # type: ignore
54
50
 
55
51
  def say(self, dest , *args ) :
56
52
  if dest == "listen":
@@ -66,4 +62,4 @@ class BrokerThr(BrokerCli):
66
62
  for node in dest.split("."):
67
63
  obj = getattr(obj, node)
68
64
 
69
- try_exec(False, obj, *args)
65
+ obj(*args) # type: ignore
copyparty/broker_util.py CHANGED
@@ -28,6 +28,18 @@ class ExceptionalQueue(Queue, object):
28
28
  return rv
29
29
 
30
30
 
31
+ class NotExQueue(object):
32
+ """
33
+ BrokerThr uses this instead of ExceptionalQueue; 7x faster
34
+ """
35
+
36
+ def __init__(self, rv ) :
37
+ self.rv = rv
38
+
39
+ def get(self) :
40
+ return self.rv
41
+
42
+
31
43
  class BrokerCli(object):
32
44
  """
33
45
  helps mypy understand httpsrv.broker but still fails a few levels deeper,
@@ -43,7 +55,7 @@ class BrokerCli(object):
43
55
  def __init__(self) :
44
56
  pass
45
57
 
46
- def ask(self, dest , *args ) :
58
+ def ask(self, dest , *args ) :
47
59
  return ExceptionalQueue(1)
48
60
 
49
61
  def say(self, dest , *args ) :
copyparty/cfg.py CHANGED
@@ -13,6 +13,7 @@ def vf_bmap() :
13
13
  "dav_rt": "davrt",
14
14
  "ed": "dots",
15
15
  "hardlink_only": "hardlinkonly",
16
+ "no_dirsz": "nodirsz",
16
17
  "no_dupe": "nodupe",
17
18
  "no_forget": "noforget",
18
19
  "no_pipe": "nopipe",
copyparty/fsutil.py CHANGED
@@ -113,7 +113,6 @@ class Fstab(object):
113
113
  self.srctab = srctab
114
114
 
115
115
  def relabel(self, path , nval ) :
116
- assert self.tab
117
116
  self.cache = {}
118
117
  if ANYWIN:
119
118
  path = self._winpath(path)
@@ -150,7 +149,6 @@ class Fstab(object):
150
149
  self.log("failed to build tab:\n{}".format(min_ex()), 3)
151
150
  self.build_fallback()
152
151
 
153
- assert self.tab
154
152
  ret = self.tab._find(path)[0]
155
153
  if self.trusted or path == ret.vpath:
156
154
  return ret.realpath.split("/")[0]
@@ -161,6 +159,5 @@ class Fstab(object):
161
159
  if not self.tab:
162
160
  self.build_fallback()
163
161
 
164
- assert self.tab
165
162
  ret = self.tab._find(path)[0]
166
163
  return ret.realpath