copyparty 1.13.8__py3-none-any.whl → 1.14.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/httpcli.py CHANGED
@@ -45,6 +45,7 @@ from .util import unquote # type: ignore
45
45
  from .util import (
46
46
  APPLESAN_RE,
47
47
  BITNESS,
48
+ HAVE_SQLITE3,
48
49
  HTTPCODE,
49
50
  META_NOBOTS,
50
51
  UTC,
@@ -450,7 +451,7 @@ class HttpCli(object):
450
451
  t = "incorrect --rp-loc or webserver config; expected vpath starting with [{}] but got [{}]"
451
452
  self.log(t.format(self.args.R, vpath), 1)
452
453
 
453
- self.ouparam = {k: zs for k, zs in uparam.items()}
454
+ self.ouparam = uparam.copy()
454
455
 
455
456
  if self.args.rsp_slp:
456
457
  time.sleep(self.args.rsp_slp)
@@ -459,6 +460,9 @@ class HttpCli(object):
459
460
 
460
461
  zso = self.headers.get("cookie")
461
462
  if zso:
463
+ if len(zso) > 8192:
464
+ self.loud_reply("cookie header too big", status=400)
465
+ return False
462
466
  zsll = [x.split("=", 1) for x in zso.split(";") if "=" in x]
463
467
  cookies = {k.strip(): unescape_cookie(zs) for k, zs in zsll}
464
468
  cookie_pw = cookies.get("cppws") or cookies.get("cppwd") or ""
@@ -964,10 +968,10 @@ class HttpCli(object):
964
968
  status = 200,
965
969
  use302 = False,
966
970
  ) :
967
- vp = self.args.RS + vpath
971
+ vp = self.args.SRS + vpath
968
972
  html = self.j2s(
969
973
  "msg",
970
- h2='<a href="/{}">{} /{}</a>'.format(
974
+ h2='<a href="{}">{} {}</a>'.format(
971
975
  quotep(vp) + suf, flavor, html_escape(vp, crlf=True) + suf
972
976
  ),
973
977
  pre=msg,
@@ -975,7 +979,7 @@ class HttpCli(object):
975
979
  ).encode("utf-8", "replace")
976
980
 
977
981
  if use302:
978
- self.reply(html, status=302, headers={"Location": "/" + vpath})
982
+ self.reply(html, status=302, headers={"Location": vp})
979
983
  else:
980
984
  self.reply(html, status=status)
981
985
 
@@ -1137,7 +1141,7 @@ class HttpCli(object):
1137
1141
  if "move" in self.uparam:
1138
1142
  return self.handle_mv()
1139
1143
 
1140
- if not self.vpath:
1144
+ if not self.vpath and self.ouparam:
1141
1145
  if "reload" in self.uparam:
1142
1146
  return self.handle_reload()
1143
1147
 
@@ -1159,23 +1163,12 @@ class HttpCli(object):
1159
1163
  if "hc" in self.uparam:
1160
1164
  return self.tx_svcs()
1161
1165
 
1166
+ if "shares" in self.uparam:
1167
+ return self.tx_shares()
1168
+
1162
1169
  if "h" in self.uparam:
1163
1170
  return self.tx_mounts()
1164
1171
 
1165
- # conditional redirect to single volumes
1166
- if not self.vpath and not self.ouparam:
1167
- nread = len(self.rvol)
1168
- nwrite = len(self.wvol)
1169
- if nread + nwrite == 1 or (self.rvol == self.wvol and nread == 1):
1170
- if nread == 1:
1171
- vpath = self.rvol[0]
1172
- else:
1173
- vpath = self.wvol[0]
1174
-
1175
- if self.vpath != vpath:
1176
- self.redirect(vpath, flavor="redirecting to", use302=True)
1177
- return True
1178
-
1179
1172
  return self.tx_browser()
1180
1173
 
1181
1174
  def handle_propfind(self) :
@@ -1614,6 +1607,9 @@ class HttpCli(object):
1614
1607
  if "delete" in self.uparam:
1615
1608
  return self.handle_rm([])
1616
1609
 
1610
+ if "unshare" in self.uparam:
1611
+ return self.handle_unshare()
1612
+
1617
1613
  if "application/octet-stream" in ctype:
1618
1614
  return self.handle_post_binary()
1619
1615
 
@@ -2085,6 +2081,9 @@ class HttpCli(object):
2085
2081
  if act == "zip":
2086
2082
  return self.handle_zip_post()
2087
2083
 
2084
+ if act == "chpw":
2085
+ return self.handle_chpw()
2086
+
2088
2087
  raise Pebkac(422, 'invalid action "{}"'.format(act))
2089
2088
 
2090
2089
  def handle_zip_post(self) :
@@ -2143,6 +2142,9 @@ class HttpCli(object):
2143
2142
  if "srch" in self.uparam or "srch" in body:
2144
2143
  return self.handle_search(body)
2145
2144
 
2145
+ if "share" in self.uparam:
2146
+ return self.handle_share(body)
2147
+
2146
2148
  if "delete" in self.uparam:
2147
2149
  return self.handle_rm(body)
2148
2150
 
@@ -2199,7 +2201,9 @@ class HttpCli(object):
2199
2201
  def handle_search(self, body ) :
2200
2202
  idx = self.conn.get_u2idx()
2201
2203
  if not idx or not hasattr(idx, "p_end"):
2202
- raise Pebkac(500, "server busy, or sqlite3 not available; cannot search")
2204
+ if not HAVE_SQLITE3:
2205
+ raise Pebkac(500, "sqlite3 not found on server; search is disabled")
2206
+ raise Pebkac(500, "server busy, cannot search; please retry in a bit")
2203
2207
 
2204
2208
  vols = []
2205
2209
  seen = {}
@@ -2389,6 +2393,22 @@ class HttpCli(object):
2389
2393
  self.reply(b"thank")
2390
2394
  return True
2391
2395
 
2396
+ def handle_chpw(self) :
2397
+ assert self.parser
2398
+ pwd = self.parser.require("pw", 64)
2399
+ self.parser.drop()
2400
+
2401
+ ok, msg = self.asrv.chpw(self.conn.hsrv.broker, self.uname, pwd)
2402
+ if ok:
2403
+ ok, msg = self.get_pwd_cookie(pwd)
2404
+ if ok:
2405
+ msg = "new password OK"
2406
+
2407
+ redir = (self.args.SRS + "?h") if ok else ""
2408
+ html = self.j2s("msg", h1=msg, h2='<a href="/?h">ack</a>', redir=redir)
2409
+ self.reply(html.encode("utf-8"))
2410
+ return True
2411
+
2392
2412
  def handle_login(self) :
2393
2413
  assert self.parser
2394
2414
  pwd = self.parser.require("cppwd", 64)
@@ -2413,12 +2433,12 @@ class HttpCli(object):
2413
2433
  dst += "&" if "?" in dst else "?"
2414
2434
  dst += "_=1#" + html_escape(uhash, True, True)
2415
2435
 
2416
- msg = self.get_pwd_cookie(pwd)
2436
+ _, msg = self.get_pwd_cookie(pwd)
2417
2437
  html = self.j2s("msg", h1=msg, h2='<a href="' + dst + '">ack</a>', redir=dst)
2418
2438
  self.reply(html.encode("utf-8"))
2419
2439
  return True
2420
2440
 
2421
- def get_pwd_cookie(self, pwd ) :
2441
+ def get_pwd_cookie(self, pwd ) :
2422
2442
  hpwd = self.asrv.ah.hash(pwd)
2423
2443
  uname = self.asrv.iacct.get(hpwd)
2424
2444
  if uname:
@@ -2450,7 +2470,7 @@ class HttpCli(object):
2450
2470
  ck = gencookie(k, pwd, self.args.R, self.is_https, dur, "; HttpOnly")
2451
2471
  self.out_headerlist.append(("Set-Cookie", ck))
2452
2472
 
2453
- return msg
2473
+ return dur > 0, msg
2454
2474
 
2455
2475
  def handle_mkdir(self) :
2456
2476
  assert self.parser
@@ -2489,7 +2509,7 @@ class HttpCli(object):
2489
2509
  except:
2490
2510
  raise Pebkac(500, min_ex())
2491
2511
 
2492
- self.out_headers["X-New-Dir"] = quotep(vpath)
2512
+ self.out_headers["X-New-Dir"] = quotep(self.args.RS + vpath)
2493
2513
 
2494
2514
  if dav:
2495
2515
  self.reply(b"", 201)
@@ -3944,6 +3964,7 @@ class HttpCli(object):
3944
3964
  k304=self.k304(),
3945
3965
  k304vis=self.args.k304 > 0,
3946
3966
  ver=S_VERSION if self.args.ver else "",
3967
+ chpw=self.args.chpw and self.uname != "*",
3947
3968
  ahttps="" if self.is_https else "https://" + self.host + self.req,
3948
3969
  )
3949
3970
  self.reply(html.encode("utf-8"))
@@ -4078,7 +4099,9 @@ class HttpCli(object):
4078
4099
  dst = dst[len(top) + 1 :]
4079
4100
 
4080
4101
  ret = self.gen_tree(top, dst, self.uparam.get("k", ""))
4081
- if self.is_vproxied:
4102
+ if self.is_vproxied and not self.uparam["tree"]:
4103
+ # uparam is '' on initial load, which is
4104
+ # the only time we gotta fill in the blanks
4082
4105
  parents = self.args.R.split("/")
4083
4106
  for parent in reversed(parents):
4084
4107
  ret = {"k%s" % (parent,): ret, "a": []}
@@ -4153,7 +4176,9 @@ class HttpCli(object):
4153
4176
  def tx_ups(self) :
4154
4177
  idx = self.conn.get_u2idx()
4155
4178
  if not idx or not hasattr(idx, "p_end"):
4156
- raise Pebkac(500, "sqlite3 is not available on the server; cannot unpost")
4179
+ if not HAVE_SQLITE3:
4180
+ raise Pebkac(500, "sqlite3 not found on server; unpost is disabled")
4181
+ raise Pebkac(500, "server busy, cannot unpost; please retry in a bit")
4157
4182
 
4158
4183
  filt = self.uparam.get("filter") or ""
4159
4184
  lm = "ups [{}]".format(filt)
@@ -4242,6 +4267,137 @@ class HttpCli(object):
4242
4267
  self.reply(jtxt.encode("utf-8", "replace"), mime="application/json")
4243
4268
  return True
4244
4269
 
4270
+ def tx_shares(self) :
4271
+ if self.uname == "*":
4272
+ self.loud_reply("you're not logged in")
4273
+ return True
4274
+
4275
+ idx = self.conn.get_u2idx()
4276
+ if not idx or not hasattr(idx, "p_end"):
4277
+ if not HAVE_SQLITE3:
4278
+ raise Pebkac(500, "sqlite3 not found on server; sharing is disabled")
4279
+ raise Pebkac(500, "server busy, cannot list shares; please retry in a bit")
4280
+
4281
+ cur = idx.get_shr()
4282
+ if not cur:
4283
+ raise Pebkac(400, "huh, sharing must be disabled in the server config...")
4284
+
4285
+ rows = cur.execute("select * from sh").fetchall()
4286
+ rows = [list(x) for x in rows]
4287
+
4288
+ if self.uname != self.args.shr_adm:
4289
+ rows = [x for x in rows if x[5] == self.uname]
4290
+
4291
+ for x in rows:
4292
+ x[1] = "yes" if x[1] else ""
4293
+
4294
+ html = self.j2s(
4295
+ "shares", this=self, shr=self.args.shr, rows=rows, now=int(time.time())
4296
+ )
4297
+ self.reply(html.encode("utf-8"), status=200)
4298
+ return True
4299
+
4300
+ def handle_unshare(self) :
4301
+ idx = self.conn.get_u2idx()
4302
+ if not idx or not hasattr(idx, "p_end"):
4303
+ if not HAVE_SQLITE3:
4304
+ raise Pebkac(500, "sqlite3 not found on server; sharing is disabled")
4305
+ raise Pebkac(500, "server busy, cannot create share; please retry in a bit")
4306
+
4307
+ if self.args.shr_v:
4308
+ self.log("handle_unshare: " + self.req)
4309
+
4310
+ cur = idx.get_shr()
4311
+ if not cur:
4312
+ raise Pebkac(400, "huh, sharing must be disabled in the server config...")
4313
+
4314
+ skey = self.vpath.split("/")[-1]
4315
+
4316
+ uns = cur.execute("select un from sh where k = ?", (skey,)).fetchall()
4317
+ un = uns[0][0] if uns and uns[0] else ""
4318
+
4319
+ if not un:
4320
+ raise Pebkac(400, "that sharekey didn't match anything")
4321
+
4322
+ if un != self.uname and self.uname != self.args.shr_adm:
4323
+ t = "your username (%r) does not match the sharekey's owner (%r) and you're not admin"
4324
+ raise Pebkac(400, t % (self.uname, un))
4325
+
4326
+ cur.execute("delete from sh where k = ?", (skey,))
4327
+ cur.connection.commit()
4328
+
4329
+ self.redirect(self.args.SRS + "?shares")
4330
+ return True
4331
+
4332
+ def handle_share(self, req ) :
4333
+ idx = self.conn.get_u2idx()
4334
+ if not idx or not hasattr(idx, "p_end"):
4335
+ if not HAVE_SQLITE3:
4336
+ raise Pebkac(500, "sqlite3 not found on server; sharing is disabled")
4337
+ raise Pebkac(500, "server busy, cannot create share; please retry in a bit")
4338
+
4339
+ if self.args.shr_v:
4340
+ self.log("handle_share: " + json.dumps(req, indent=4))
4341
+
4342
+ skey = req["k"]
4343
+ vp = req["vp"].strip("/")
4344
+ if self.is_vproxied and (vp == self.args.R or vp.startswith(self.args.RS)):
4345
+ vp = vp[len(self.args.RS) :]
4346
+
4347
+ m = re.search(r"([^0-9a-zA-Z_\.-]|\.\.|^\.)", skey)
4348
+ if m:
4349
+ raise Pebkac(400, "sharekey has illegal character [%s]" % (m[1],))
4350
+
4351
+ if vp.startswith(self.args.shr[1:]):
4352
+ raise Pebkac(400, "yo dawg...")
4353
+
4354
+ cur = idx.get_shr()
4355
+ if not cur:
4356
+ raise Pebkac(400, "huh, sharing must be disabled in the server config...")
4357
+
4358
+ q = "select * from sh where k = ?"
4359
+ qr = cur.execute(q, (skey,)).fetchall()
4360
+ if qr and qr[0]:
4361
+ self.log("sharekey taken by %r" % (qr,))
4362
+ raise Pebkac(400, "sharekey [%s] is already in use" % (skey,))
4363
+
4364
+ # ensure user has requested perms
4365
+ s_rd = "read" in req["perms"]
4366
+ s_wr = "write" in req["perms"]
4367
+ s_mv = "move" in req["perms"]
4368
+ s_del = "delete" in req["perms"]
4369
+ try:
4370
+ vfs, rem = self.asrv.vfs.get(vp, self.uname, s_rd, s_wr, s_mv, s_del)
4371
+ except:
4372
+ raise Pebkac(400, "you dont have all the perms you tried to grant")
4373
+
4374
+ ap = vfs.canonical(rem)
4375
+ st = bos.stat(ap)
4376
+ ist = 2 if stat.S_ISDIR(st.st_mode) else 1
4377
+
4378
+ pw = req.get("pw") or ""
4379
+ now = int(time.time())
4380
+ sexp = req["exp"]
4381
+ exp = now + int(sexp) * 60 if sexp else 0
4382
+ pr = "".join(zc for zc, zb in zip("rwmd", (s_rd, s_wr, s_mv, s_del)) if zb)
4383
+
4384
+ q = "insert into sh values (?,?,?,?,?,?,?,?)"
4385
+ cur.execute(q, (skey, pw, vp, pr, ist, self.uname, now, exp))
4386
+ cur.connection.commit()
4387
+
4388
+ self.conn.hsrv.broker.ask("_reload_blocking", False, False).get()
4389
+ self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
4390
+
4391
+ surl = "%s://%s%s%s%s" % (
4392
+ "https" if self.is_https else "http",
4393
+ self.host,
4394
+ self.args.SR,
4395
+ self.args.shr,
4396
+ skey,
4397
+ )
4398
+ self.loud_reply(surl, status=201)
4399
+ return True
4400
+
4245
4401
  def handle_rm(self, req ) :
4246
4402
  if not req and not self.can_delete:
4247
4403
  raise Pebkac(403, "not allowed for user " + self.uname)
@@ -4640,6 +4796,7 @@ class HttpCli(object):
4640
4796
  "have_mv": (not self.args.no_mv),
4641
4797
  "have_del": (not self.args.no_del),
4642
4798
  "have_zip": (not self.args.no_zip),
4799
+ "have_shr": self.args.shr,
4643
4800
  "have_unpost": int(self.args.unpost),
4644
4801
  "sb_md": "" if "no_sb_md" in vf else (vf.get("md_sbf") or "y"),
4645
4802
  "dgrid": "grid" in vf,
copyparty/httpsrv.py CHANGED
@@ -151,7 +151,17 @@ class HttpSrv(object):
151
151
 
152
152
  env = jinja2.Environment()
153
153
  env.loader = jinja2.FileSystemLoader(os.path.join(self.E.mod, "web"))
154
- jn = ["splash", "svcs", "browser", "browser2", "msg", "md", "mde", "cf"]
154
+ jn = [
155
+ "splash",
156
+ "shares",
157
+ "svcs",
158
+ "browser",
159
+ "browser2",
160
+ "msg",
161
+ "md",
162
+ "mde",
163
+ "cf",
164
+ ]
155
165
  self.j2 = {x: env.get_template(x + ".html") for x in jn}
156
166
  zs = os.path.join(self.E.mod, "web", "deps", "prism.js.gz")
157
167
  self.prism = os.path.exists(zs)
@@ -362,7 +372,7 @@ class HttpSrv(object):
362
372
  cip = cip[7:]
363
373
  addr = (cip, saddr[1])
364
374
  else:
365
- addr = (ip, sck.fileno())
375
+ addr = ("127.8.3.7", sck.fileno())
366
376
  except (OSError, socket.error) as ex:
367
377
  if self.stopping:
368
378
  break
copyparty/svchub.py CHANGED
@@ -202,6 +202,20 @@ class SvcHub(object):
202
202
  t = "WARNING: --s-rd-sz (%d) is larger than --iobuf (%d); this may lead to reduced performance"
203
203
  self.log("root", t % (args.s_rd_sz, args.iobuf), 3)
204
204
 
205
+ if args.chpw and args.idp_h_usr:
206
+ t = "ERROR: user-changeable passwords is incompatible with IdP/identity-providers; you must disable either --chpw or --idp-h-usr"
207
+ self.log("root", t, 1)
208
+ raise Exception(t)
209
+
210
+ noch = set()
211
+ for zs in args.chpw_no or []:
212
+ zsl = [x.strip() for x in zs.split(",")]
213
+ noch.update([x for x in zsl if x])
214
+ args.chpw_no = noch
215
+
216
+ if args.shr:
217
+ self.setup_share_db()
218
+
205
219
  bri = "zy"[args.theme % 2 :][:1]
206
220
  ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)]
207
221
  args.theme = "{0}{1} {0} {1}".format(ch, bri)
@@ -347,6 +361,61 @@ class SvcHub(object):
347
361
 
348
362
  self.broker = Broker(self)
349
363
 
364
+ def setup_share_db(self) :
365
+ al = self.args
366
+ if not HAVE_SQLITE3:
367
+ self.log("root", "sqlite3 not available; disabling --shr", 1)
368
+ al.shr = ""
369
+ return
370
+
371
+ import sqlite3
372
+
373
+ al.shr = "/%s/" % (al.shr.strip("/"))
374
+
375
+ create = True
376
+ db_path = self.args.shr_db
377
+ self.log("root", "initializing shares-db %s" % (db_path,))
378
+ for n in range(2):
379
+ try:
380
+ db = sqlite3.connect(db_path)
381
+ cur = db.cursor()
382
+ try:
383
+ cur.execute("select count(*) from sh").fetchone()
384
+ create = False
385
+ break
386
+ except:
387
+ pass
388
+ except Exception as ex:
389
+ if n:
390
+ raise
391
+ t = "shares-db corrupt; deleting and recreating: %r"
392
+ self.log("root", t % (ex,), 3)
393
+ try:
394
+ cur.close() # type: ignore
395
+ except:
396
+ pass
397
+ try:
398
+ db.close() # type: ignore
399
+ except:
400
+ pass
401
+ os.unlink(db_path)
402
+
403
+ assert db # type: ignore
404
+ assert cur # type: ignore
405
+ if create:
406
+ for cmd in [
407
+ # sharekey, password, src, perms, type, owner, created, expires
408
+ r"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int)",
409
+ r"create table kv (k text, v int)",
410
+ r"insert into kv values ('sver', {})".format(1),
411
+ ]:
412
+ cur.execute(cmd)
413
+ db.commit()
414
+ self.log("root", "created new shares-db")
415
+
416
+ cur.close()
417
+ db.close()
418
+
350
419
  def start_ftpd(self) :
351
420
  time.sleep(30)
352
421
 
@@ -809,18 +878,21 @@ class SvcHub(object):
809
878
  Daemon(self._reload, "reloading")
810
879
  return "reload initiated"
811
880
 
812
- def _reload(self, rescan_all_vols = True) :
881
+ def _reload(self, rescan_all_vols = True, up2k = True) :
813
882
  with self.up2k.mutex:
814
883
  if self.reloading != 1:
815
884
  return
816
885
  self.reloading = 2
817
886
  self.log("root", "reloading config")
818
- self.asrv.reload()
819
- self.up2k.reload(rescan_all_vols)
887
+ self.asrv.reload(9 if up2k else 4)
888
+ if up2k:
889
+ self.up2k.reload(rescan_all_vols)
890
+ else:
891
+ self.log("root", "reload done")
820
892
  self.broker.reload()
821
893
  self.reloading = 0
822
894
 
823
- def _reload_blocking(self, rescan_all_vols = True) :
895
+ def _reload_blocking(self, rescan_all_vols = True, up2k = True) :
824
896
  while True:
825
897
  with self.up2k.mutex:
826
898
  if self.reloading < 2:
@@ -831,7 +903,7 @@ class SvcHub(object):
831
903
  # try to handle multiple pending IdP reloads at once:
832
904
  time.sleep(0.2)
833
905
 
834
- self._reload(rescan_all_vols=rescan_all_vols)
906
+ self._reload(rescan_all_vols=rescan_all_vols, up2k=up2k)
835
907
 
836
908
  def stop_thr(self) :
837
909
  while not self.stop_req:
copyparty/tcpsrv.py CHANGED
@@ -223,10 +223,22 @@ class TcpSrv(object):
223
223
  self.log("tcpsrv", msg, c)
224
224
 
225
225
  def _listen(self, ip , port ) :
226
+ uds_perm = uds_gid = -1
226
227
  if "unix:" in ip:
227
228
  tcp = False
228
229
  ipv = socket.AF_UNIX
229
- ip = ip.split("unix:")[1]
230
+ uds = ip.split(":")
231
+ ip = uds[-1]
232
+ if len(uds) > 2:
233
+ uds_perm = int(uds[1], 8)
234
+ if len(uds) > 3:
235
+ try:
236
+ uds_gid = int(uds[2])
237
+ except:
238
+ import grp
239
+
240
+ uds_gid = grp.getgrnam(uds[2]).gr_gid
241
+
230
242
  elif ":" in ip:
231
243
  tcp = True
232
244
  ipv = socket.AF_INET6
@@ -262,7 +274,13 @@ class TcpSrv(object):
262
274
  srv.bind(ip)
263
275
  else:
264
276
  tf = "%s.%d" % (ip, os.getpid())
277
+ if os.path.exists(tf):
278
+ os.unlink(tf)
265
279
  srv.bind(tf)
280
+ if uds_gid != -1:
281
+ os.chown(tf, -1, uds_gid)
282
+ if uds_perm != -1:
283
+ os.chmod(tf, uds_perm)
266
284
  atomic_move(self.nlog, tf, ip, VF_CAREFUL)
267
285
 
268
286
  sport = srv.getsockname()[1] if tcp else port
copyparty/u2idx.py CHANGED
@@ -56,6 +56,8 @@ class U2idx(object):
56
56
  self.mem_cur = sqlite3.connect(":memory:", check_same_thread=False).cursor()
57
57
  self.mem_cur.execute(r"create table a (b text)")
58
58
 
59
+ self.sh_cur = None
60
+
59
61
  self.p_end = 0.0
60
62
  self.p_dur = 0.0
61
63
 
@@ -92,17 +94,31 @@ class U2idx(object):
92
94
  except:
93
95
  raise Pebkac(500, min_ex())
94
96
 
95
- def get_cur(self, vn ) :
96
- if not HAVE_SQLITE3:
97
+ def get_shr(self) :
98
+ if self.sh_cur:
99
+ return self.sh_cur
100
+
101
+ if not HAVE_SQLITE3 or not self.args.shr:
97
102
  return None
98
103
 
104
+ assert sqlite3 # type: ignore
105
+
106
+ db = sqlite3.connect(self.args.shr_db, timeout=2, check_same_thread=False)
107
+ cur = db.cursor()
108
+ cur.execute('pragma table_info("sh")').fetchall()
109
+ self.sh_cur = cur
110
+ return cur
111
+
112
+ def get_cur(self, vn ) :
99
113
  cur = self.cur.get(vn.realpath)
100
114
  if cur:
101
115
  return cur
102
116
 
103
- if "e2d" not in vn.flags:
117
+ if not HAVE_SQLITE3 or "e2d" not in vn.flags:
104
118
  return None
105
119
 
120
+ assert sqlite3 # type: ignore
121
+
106
122
  ptop = vn.realpath
107
123
  histpath = self.asrv.vfs.histtab.get(ptop)
108
124
  if not histpath: