copyparty 1.17.2__py3-none-any.whl → 1.18.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
@@ -1294,6 +1294,9 @@ class HttpCli(object):
1294
1294
  if "ru" in self.uparam:
1295
1295
  return self.tx_rups()
1296
1296
 
1297
+ if "idp" in self.uparam:
1298
+ return self.tx_idp()
1299
+
1297
1300
  if "h" in self.uparam:
1298
1301
  return self.tx_mounts()
1299
1302
 
@@ -1407,7 +1410,13 @@ class HttpCli(object):
1407
1410
  except:
1408
1411
  pass
1409
1412
 
1413
+ ap = ""
1414
+ use_magic = "rmagic" in self.vn.flags
1415
+
1410
1416
  for i in hits:
1417
+ if use_magic:
1418
+ ap = os.path.join(self.vn.realpath, i["rp"])
1419
+
1411
1420
  iurl = html_escape("%s%s" % (baseurl, i["rp"]), True, True)
1412
1421
  title = unquotep(i["rp"].split("?")[0].split("/")[-1])
1413
1422
  title = html_escape(title, True, True)
@@ -1415,7 +1424,7 @@ class HttpCli(object):
1415
1424
  tag_a = str(i["tags"].get("artist") or "")
1416
1425
  desc = "%s - %s" % (tag_a, tag_t) if tag_t and tag_a else (tag_t or tag_a)
1417
1426
  desc = html_escape(desc, True, True) if desc else title
1418
- mime = html_escape(guess_mime(title))
1427
+ mime = html_escape(guess_mime(title, ap))
1419
1428
  lmod = formatdate(max(0, i["ts"]))
1420
1429
  zsa = (iurl, iurl, title, desc, lmod, iurl, mime, i["sz"])
1421
1430
  zs = (
@@ -1568,6 +1577,9 @@ class HttpCli(object):
1568
1577
  None, 207, "text/xml; charset=" + enc, {"Transfer-Encoding": "chunked"}
1569
1578
  )
1570
1579
 
1580
+ ap = ""
1581
+ use_magic = "rmagic" in vn.flags
1582
+
1571
1583
  ret = '<?xml version="1.0" encoding="{}"?>\n<D:multistatus xmlns:D="DAV:">'
1572
1584
  ret = ret.format(uenc)
1573
1585
  for x in fgen:
@@ -1594,7 +1606,9 @@ class HttpCli(object):
1594
1606
  "supportedlock": '<D:lockentry xmlns:D="DAV:"><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockentry>',
1595
1607
  }
1596
1608
  if not isdir:
1597
- pvs["getcontenttype"] = html_escape(guess_mime(rp))
1609
+ if use_magic:
1610
+ ap = os.path.join(tap, x["vp"])
1611
+ pvs["getcontenttype"] = html_escape(guess_mime(rp, ap))
1598
1612
  pvs["getcontentlength"] = str(st.st_size)
1599
1613
 
1600
1614
  for k, v in pvs.items():
@@ -2705,6 +2719,7 @@ class HttpCli(object):
2705
2719
  locked = chashes # remaining chunks to be received in this request
2706
2720
  written = [] # chunks written to disk, but not yet released by up2k
2707
2721
  num_left = -1 # num chunks left according to most recent up2k release
2722
+ bail1 = False # used in sad path to avoid contradicting error-text
2708
2723
  treport = time.time() # ratelimit up2k reporting to reduce overhead
2709
2724
 
2710
2725
  if "x-up2k-subc" in self.headers:
@@ -2843,7 +2858,6 @@ class HttpCli(object):
2843
2858
  except:
2844
2859
  # maybe busted handle (eg. disk went full)
2845
2860
  f.close()
2846
- chashes = [] # exception flag
2847
2861
  raise
2848
2862
  finally:
2849
2863
  if locked:
@@ -2852,13 +2866,14 @@ class HttpCli(object):
2852
2866
  num_left, t = x.get()
2853
2867
  if num_left < 0:
2854
2868
  self.loud_reply(t, status=500)
2855
- if chashes: # kills exception bubbling otherwise
2856
- return False
2869
+ bail1 = True
2857
2870
  else:
2858
2871
  t = "got %d more chunks, %d left"
2859
2872
  self.log(t % (len(written), num_left), 6)
2860
2873
 
2861
2874
  if num_left < 0:
2875
+ if bail1:
2876
+ return False
2862
2877
  raise Pebkac(500, "unconfirmed; see serverlog")
2863
2878
 
2864
2879
  if not num_left and fpool:
@@ -3795,6 +3810,20 @@ class HttpCli(object):
3795
3810
 
3796
3811
  return txt
3797
3812
 
3813
+ def _can_tail(self, volflags ) :
3814
+ zp = self.args.ua_nodoc
3815
+ if zp and zp.search(self.ua):
3816
+ t = "this URL contains no valuable information for bots/crawlers"
3817
+ raise Pebkac(403, t)
3818
+ lvl = volflags["tail_who"]
3819
+ if "notail" in volflags or not lvl:
3820
+ raise Pebkac(400, "tail is disabled in server config")
3821
+ elif lvl <= 1 and not self.can_admin:
3822
+ raise Pebkac(400, "tail is admin-only on this server")
3823
+ elif lvl <= 2 and self.uname in ("", "*"):
3824
+ raise Pebkac(400, "you must be authenticated to use ?tail on this server")
3825
+ return True
3826
+
3798
3827
  def _can_zip(self, volflags ) :
3799
3828
  lvl = volflags["zip_who"]
3800
3829
  if self.args.no_zip or not lvl:
@@ -3939,6 +3968,8 @@ class HttpCli(object):
3939
3968
  logmsg = "{:4} {} ".format("", self.req)
3940
3969
  logtail = ""
3941
3970
 
3971
+ is_tail = "tail" in self.uparam and self._can_tail(self.vn.flags)
3972
+
3942
3973
  if ptop is not None:
3943
3974
  ap_data = "<%s>" % (req_path,)
3944
3975
  try:
@@ -4051,6 +4082,7 @@ class HttpCli(object):
4051
4082
  and can_range
4052
4083
  and file_sz
4053
4084
  and "," not in hrange
4085
+ and not is_tail
4054
4086
  ):
4055
4087
  try:
4056
4088
  if not hrange.lower().startswith("bytes"):
@@ -4119,6 +4151,8 @@ class HttpCli(object):
4119
4151
  mime = "text/plain; charset={}".format(self.uparam["txt"] or "utf-8")
4120
4152
  elif "mime" in self.uparam:
4121
4153
  mime = str(self.uparam.get("mime"))
4154
+ elif "rmagic" in self.vn.flags:
4155
+ mime = guess_mime(req_path, fs_path)
4122
4156
  else:
4123
4157
  mime = guess_mime(req_path)
4124
4158
 
@@ -4136,13 +4170,18 @@ class HttpCli(object):
4136
4170
  return True
4137
4171
 
4138
4172
  dls = self.conn.hsrv.dls
4173
+ if is_tail:
4174
+ upper = 1 << 30
4175
+ if len(dls) > self.args.tail_cmax:
4176
+ raise Pebkac(400, "too many active downloads to start a new tail")
4177
+
4139
4178
  if upper - lower > 0x400000: # 4m
4140
4179
  now = time.time()
4141
4180
  self.dl_id = "%s:%s" % (self.ip, self.addr[1])
4142
4181
  dls[self.dl_id] = (now, 0)
4143
4182
  self.conn.hsrv.dli[self.dl_id] = (
4144
4183
  now,
4145
- upper - lower,
4184
+ 0 if is_tail else upper - lower,
4146
4185
  self.vn,
4147
4186
  self.vpath,
4148
4187
  self.uname,
@@ -4152,6 +4191,9 @@ class HttpCli(object):
4152
4191
  return self.tx_pipe(
4153
4192
  ptop, req_path, ap_data, job, lower, upper, status, mime, logmsg
4154
4193
  )
4194
+ elif is_tail:
4195
+ self.tx_tail(open_args, status, mime)
4196
+ return False
4155
4197
 
4156
4198
  ret = True
4157
4199
  with open_func(*open_args) as f:
@@ -4181,6 +4223,131 @@ class HttpCli(object):
4181
4223
 
4182
4224
  return ret
4183
4225
 
4226
+ def tx_tail(
4227
+ self,
4228
+ open_args ,
4229
+ status ,
4230
+ mime ,
4231
+ ) :
4232
+ vf = self.vn.flags
4233
+ self.send_headers(length=None, status=status, mime=mime)
4234
+ abspath = open_args[0]
4235
+ sec_rate = vf["tail_rate"]
4236
+ sec_max = vf["tail_tmax"]
4237
+ sec_fd = vf["tail_fd"]
4238
+ sec_ka = self.args.tail_ka
4239
+ wr_slp = self.args.s_wr_slp
4240
+ wr_sz = self.args.s_wr_sz
4241
+ dls = self.conn.hsrv.dls
4242
+ dl_id = self.dl_id
4243
+
4244
+ # non-numeric = full file from start
4245
+ # positive = absolute offset from start
4246
+ # negative = start that many bytes from eof
4247
+ try:
4248
+ ofs = int(self.uparam["tail"])
4249
+ except:
4250
+ ofs = 0
4251
+
4252
+ t0 = time.time()
4253
+ ofs0 = ofs
4254
+ f = None
4255
+ try:
4256
+ st = os.stat(abspath)
4257
+ f = open(*open_args)
4258
+ f.seek(0, os.SEEK_END)
4259
+ eof = f.tell()
4260
+ f.seek(0)
4261
+ if ofs < 0:
4262
+ ofs = max(0, ofs + eof)
4263
+
4264
+ self.log("tailing from byte %d: %r" % (ofs, abspath), 6)
4265
+
4266
+ # send initial data asap
4267
+ remains = sendfile_py(
4268
+ self.log, # d/c
4269
+ ofs,
4270
+ eof,
4271
+ f,
4272
+ self.s,
4273
+ wr_sz,
4274
+ wr_slp,
4275
+ False, # d/c
4276
+ dls,
4277
+ dl_id,
4278
+ )
4279
+ sent = (eof - ofs) - remains
4280
+ ofs = eof - remains
4281
+ f.seek(ofs)
4282
+
4283
+ try:
4284
+ st2 = os.stat(open_args[0])
4285
+ if st.st_ino == st2.st_ino:
4286
+ st = st2 # for filesize
4287
+ except:
4288
+ pass
4289
+
4290
+ gone = 0
4291
+ t_fd = t_ka = time.time()
4292
+ while True:
4293
+ buf = f.read(4096)
4294
+ now = time.time()
4295
+
4296
+ if sec_max and now - t0 >= sec_max:
4297
+ self.log("max duration exceeded; kicking client", 6)
4298
+ zb = b"\n\n*** max duration exceeded; disconnecting ***\n"
4299
+ self.s.sendall(zb)
4300
+ break
4301
+
4302
+ if buf:
4303
+ t_fd = t_ka = now
4304
+ self.s.sendall(buf)
4305
+ sent += len(buf)
4306
+ dls[dl_id] = (time.time(), sent)
4307
+ continue
4308
+
4309
+ time.sleep(sec_rate)
4310
+ if t_ka < now - sec_ka:
4311
+ t_ka = now
4312
+ self.s.send(b"\x00")
4313
+ if t_fd < now - sec_fd:
4314
+ try:
4315
+ st2 = os.stat(open_args[0])
4316
+ if (
4317
+ st2.st_ino != st.st_ino
4318
+ or st2.st_size < sent
4319
+ or st2.st_size < st.st_size
4320
+ ):
4321
+ # open new file before closing previous to avoid toctous (open may fail; cannot null f before)
4322
+ f2 = open(*open_args)
4323
+ f.close()
4324
+ f = f2
4325
+ f.seek(0, os.SEEK_END)
4326
+ eof = f.tell()
4327
+ if eof < sent:
4328
+ ofs = sent = 0 # shrunk; send from start
4329
+ zb = b"\n\n*** file size decreased -- rewinding to the start of the file ***\n\n"
4330
+ self.s.sendall(zb)
4331
+ if ofs0 < 0 and eof > -ofs0:
4332
+ ofs = eof + ofs0
4333
+ else:
4334
+ ofs = sent # just new fd? resume from same ofs
4335
+ f.seek(ofs)
4336
+ self.log("reopened at byte %d: %r" % (ofs, abspath), 6)
4337
+ gone = 0
4338
+ st = st2
4339
+ except:
4340
+ gone += 1
4341
+ if gone > 3:
4342
+ self.log("file deleted; disconnecting")
4343
+ break
4344
+ except IOError as ex:
4345
+ if ex.errno not in (errno.EPIPE, errno.ESHUTDOWN, errno.EBADFD):
4346
+ raise
4347
+ finally:
4348
+ if f:
4349
+ f.close()
4350
+
4184
4351
  def tx_pipe(
4185
4352
  self,
4186
4353
  ptop ,
@@ -4740,7 +4907,6 @@ class HttpCli(object):
4740
4907
  if zi == 2 or (zi == 1 and self.avol):
4741
4908
  dl_list = self.get_dls()
4742
4909
  for t0, t1, sent, sz, vp, dl_id, uname in dl_list:
4743
- rem = sz - sent
4744
4910
  td = max(0.1, now - t0)
4745
4911
  rd, fn = vsplit(vp)
4746
4912
  if not rd:
@@ -4914,15 +5080,24 @@ class HttpCli(object):
4914
5080
  return "" # unhandled / fallthrough
4915
5081
 
4916
5082
  def scanvol(self) :
4917
- if not self.can_admin:
4918
- raise Pebkac(403, "'scanvol' not allowed for user " + self.uname)
4919
-
4920
5083
  if self.args.no_rescan:
4921
5084
  raise Pebkac(403, "the rescan feature is disabled in server config")
4922
5085
 
4923
- vn, _ = self.asrv.vfs.get(self.vpath, self.uname, True, True)
5086
+ vpaths = self.uparam["scan"].split(",/")
5087
+ if vpaths == [""]:
5088
+ vpaths = [self.vpath]
5089
+
5090
+ vols = []
5091
+ for vpath in vpaths:
5092
+ vn, _ = self.asrv.vfs.get(vpath, self.uname, True, True)
5093
+ vols.append(vn.vpath)
5094
+ if self.uname not in vn.axs.uadmin:
5095
+ self.log("rejected scanning [%s] => [%s];" % (vpath, vn.vpath), 3)
5096
+ raise Pebkac(403, "'scanvol' not allowed for user " + self.uname)
5097
+
5098
+ self.log("trying to rescan %d volumes: %r" % (len(vols), vols))
4924
5099
 
4925
- args = [self.asrv.vfs.all_vols, [vn.vpath], False, True]
5100
+ args = [self.asrv.vfs.all_vols, vols, False, True]
4926
5101
 
4927
5102
  x = self.conn.hsrv.broker.ask("up2k.rescan", *args)
4928
5103
  err = x.get()
@@ -5341,6 +5516,32 @@ class HttpCli(object):
5341
5516
  self.reply(html.encode("utf-8"), status=200)
5342
5517
  return True
5343
5518
 
5519
+ def tx_idp(self) :
5520
+ if self.uname.lower() not in self.args.idp_adm_set:
5521
+ raise Pebkac(403, "'idp' not allowed for user " + self.uname)
5522
+
5523
+ cmd = self.uparam["idp"]
5524
+ if cmd.startswith("rm="):
5525
+ import sqlite3
5526
+
5527
+ db = sqlite3.connect(self.args.idp_db)
5528
+ db.execute("delete from us where un=?", (cmd[3:],))
5529
+ db.commit()
5530
+ db.close()
5531
+
5532
+ self.conn.hsrv.broker.ask("reload", False, False).get()
5533
+
5534
+ self.redirect("", "?idp")
5535
+ return True
5536
+
5537
+ rows = [
5538
+ [k, "[%s]" % ("], [".join(v))]
5539
+ for k, v in sorted(self.asrv.idp_accs.items())
5540
+ ]
5541
+ html = self.j2s("idp", this=self, rows=rows, now=int(time.time()))
5542
+ self.reply(html.encode("utf-8"), status=200)
5543
+ return True
5544
+
5344
5545
  def tx_shares(self) :
5345
5546
  if self.uname == "*":
5346
5547
  self.loud_reply("you're not logged in")
@@ -5415,7 +5616,7 @@ class HttpCli(object):
5415
5616
  self.conn.hsrv.broker.ask("reload", False, False).get()
5416
5617
  self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
5417
5618
 
5418
- self.redirect(self.args.SRS + "?shares")
5619
+ self.redirect("", "?shares")
5419
5620
  return True
5420
5621
 
5421
5622
  def handle_share(self, req ) :
copyparty/httpconn.py CHANGED
@@ -219,3 +219,6 @@ class HttpConn(object):
219
219
  if self.u2idx:
220
220
  self.hsrv.put_u2idx(str(self.addr), self.u2idx)
221
221
  self.u2idx = None
222
+
223
+ if self.rproxy:
224
+ self.set_rproxy()
copyparty/httpsrv.py CHANGED
@@ -171,6 +171,7 @@ class HttpSrv(object):
171
171
  "browser",
172
172
  "browser2",
173
173
  "cf",
174
+ "idp",
174
175
  "md",
175
176
  "mde",
176
177
  "msg",
@@ -308,6 +309,8 @@ class HttpSrv(object):
308
309
 
309
310
  Daemon(self.broker.say, "sig-hsrv-up1", ("cb_httpsrv_up",))
310
311
 
312
+ saddr = ("", 0) # fwd-decl for `except TypeError as ex:`
313
+
311
314
  while not self.stopping:
312
315
  if self.args.log_conn:
313
316
  self.log(self.name, "|%sC-ncli" % ("-" * 1,), c="90")
@@ -389,6 +392,19 @@ class HttpSrv(object):
389
392
  self.log(self.name, "accept({}): {}".format(fno, ex), c=6)
390
393
  time.sleep(0.02)
391
394
  continue
395
+ except TypeError as ex:
396
+ # on macOS, accept() may return a None saddr if blocked by LittleSnitch;
397
+ # unicode(saddr[0]) ==> TypeError: 'NoneType' object is not subscriptable
398
+ if tcp and not saddr:
399
+ t = "accept(%s): failed to accept connection from client due to firewall or network issue"
400
+ self.log(self.name, t % (fno,), c=3)
401
+ try:
402
+ sck.close() # type: ignore
403
+ except:
404
+ pass
405
+ time.sleep(0.02)
406
+ continue
407
+ raise
392
408
 
393
409
  if self.args.log_conn:
394
410
  t = "|{}C-acc2 \033[0;36m{} \033[3{}m{}".format(
copyparty/svchub.py CHANGED
@@ -82,6 +82,7 @@ if PY2:
82
82
  range = xrange # type: ignore
83
83
 
84
84
 
85
+ VER_IDP_DB = 1
85
86
  VER_SESSION_DB = 1
86
87
  VER_SHARES_DB = 2
87
88
 
@@ -252,11 +253,15 @@ class SvcHub(object):
252
253
  self.log("root", "effective %s is %s" % (zs, getattr(args, zs)))
253
254
 
254
255
  if args.ah_cli or args.ah_gen:
256
+ args.idp_store = 0
255
257
  args.no_ses = True
256
258
  args.shr = ""
257
259
 
260
+ if args.idp_store and args.idp_h_usr:
261
+ self.setup_db("idp")
262
+
258
263
  if not self.args.no_ses:
259
- self.setup_session_db()
264
+ self.setup_db("ses")
260
265
 
261
266
  args.shr1 = ""
262
267
  if args.shr:
@@ -415,25 +420,57 @@ class SvcHub(object):
415
420
  except:
416
421
  pass
417
422
 
418
- def setup_session_db(self) :
423
+ def _db_onfail_ses(self) :
424
+ self.args.no_ses = True
425
+
426
+ def _db_onfail_idp(self) :
427
+ self.args.idp_store = 0
428
+
429
+ def setup_db(self, which ) :
430
+ """
431
+ the "non-mission-critical" databases; if something looks broken then just nuke it
432
+ """
433
+ if which == "ses":
434
+ native_ver = VER_SESSION_DB
435
+ db_path = self.args.ses_db
436
+ desc = "sessions-db"
437
+ pathopt = "ses-db"
438
+ sanchk_q = "select count(*) from us"
439
+ createfun = self._create_session_db
440
+ failfun = self._db_onfail_ses
441
+ elif which == "idp":
442
+ native_ver = VER_IDP_DB
443
+ db_path = self.args.idp_db
444
+ desc = "idp-db"
445
+ pathopt = "idp-db"
446
+ sanchk_q = "select count(*) from us"
447
+ createfun = self._create_idp_db
448
+ failfun = self._db_onfail_idp
449
+ else:
450
+ raise Exception("unknown cachetype")
451
+
452
+ if not db_path.endswith(".db"):
453
+ zs = "config option --%s (the %s) was configured to [%s] which is invalid; must be a filepath ending with .db"
454
+ self.log("root", zs % (pathopt, desc, db_path), 1)
455
+ raise Exception(BAD_CFG)
456
+
419
457
  if not HAVE_SQLITE3:
420
- self.args.no_ses = True
421
- t = "WARNING: sqlite3 not available; disabling sessions, will use plaintext passwords in cookies"
422
- self.log("root", t, 3)
458
+ failfun()
459
+ if which == "ses":
460
+ zs = "disabling sessions, will use plaintext passwords in cookies"
461
+ elif which == "idp":
462
+ zs = "disabling idp-db, will be unable to remember IdP-volumes after a restart"
463
+ self.log("root", "WARNING: sqlite3 not available; %s" % (zs,), 3)
423
464
  return
424
465
 
425
466
 
426
- # policy:
427
- # the sessions-db is whatever, if something looks broken then just nuke it
428
-
429
- db_path = self.args.ses_db
430
467
  db_lock = db_path + ".lock"
431
468
  try:
432
469
  create = not os.path.getsize(db_path)
433
470
  except:
434
471
  create = True
435
472
  zs = "creating new" if create else "opening"
436
- self.log("root", "%s sessions-db %s" % (zs, db_path))
473
+ self.log("root", "%s %s %s" % (zs, desc, db_path))
437
474
 
438
475
  for tries in range(2):
439
476
  sver = 0
@@ -443,17 +480,19 @@ class SvcHub(object):
443
480
  try:
444
481
  zs = "select v from kv where k='sver'"
445
482
  sver = cur.execute(zs).fetchall()[0][0]
446
- if sver > VER_SESSION_DB:
447
- zs = "this version of copyparty only understands session-db v%d and older; the db is v%d"
448
- raise Exception(zs % (VER_SESSION_DB, sver))
483
+ if sver > native_ver:
484
+ zs = "this version of copyparty only understands %s v%d and older; the db is v%d"
485
+ raise Exception(zs % (desc, native_ver, sver))
449
486
 
450
- cur.execute("select count(*) from us").fetchone()
487
+ cur.execute(sanchk_q).fetchone()
451
488
  except:
452
489
  if sver:
453
490
  raise
454
- sver = 1
455
- self._create_session_db(cur)
456
- err = self._verify_session_db(cur, sver, db_path)
491
+ sver = createfun(cur)
492
+
493
+ err = self._verify_db(
494
+ cur, which, pathopt, db_path, desc, sver, native_ver
495
+ )
457
496
  if err:
458
497
  tries = 99
459
498
  self.args.no_ses = True
@@ -461,10 +500,10 @@ class SvcHub(object):
461
500
  break
462
501
 
463
502
  except Exception as ex:
464
- if tries or sver > VER_SESSION_DB:
503
+ if tries or sver > native_ver:
465
504
  raise
466
- t = "sessions-db is unusable; deleting and recreating: %r"
467
- self.log("root", t % (ex,), 3)
505
+ t = "%s is unusable; deleting and recreating: %r"
506
+ self.log("root", t % (desc, ex), 3)
468
507
  try:
469
508
  cur.close() # type: ignore
470
509
  except:
@@ -492,8 +531,31 @@ class SvcHub(object):
492
531
  for cmd in sch:
493
532
  cur.execute(cmd)
494
533
  self.log("root", "created new sessions-db")
534
+ return 1
495
535
 
496
- def _verify_session_db(self, cur , sver , db_path ) :
536
+ def _create_idp_db(self, cur ) :
537
+ sch = [
538
+ r"create table kv (k text, v int)",
539
+ r"create table us (un text, gs text)",
540
+ # username, groups
541
+ r"create index us_un on us(un)",
542
+ r"insert into kv values ('sver', 1)",
543
+ ]
544
+ for cmd in sch:
545
+ cur.execute(cmd)
546
+ self.log("root", "created new idp-db")
547
+ return 1
548
+
549
+ def _verify_db(
550
+ self,
551
+ cur ,
552
+ which ,
553
+ pathopt ,
554
+ db_path ,
555
+ desc ,
556
+ sver ,
557
+ native_ver ,
558
+ ) :
497
559
  # ensure writable (maybe owned by other user)
498
560
  db = cur.connection
499
561
 
@@ -505,9 +567,16 @@ class SvcHub(object):
505
567
  except:
506
568
  owner = 0
507
569
 
570
+ if which == "ses":
571
+ cons = "Will now disable sessions and instead use plaintext passwords in cookies."
572
+ elif which == "idp":
573
+ cons = "Each IdP-volume will not become available until its associated user sends their first request."
574
+ else:
575
+ raise Exception()
576
+
508
577
  if not lock_file(db_path + ".lock"):
509
- t = "the sessions-db [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --ses-db or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. Will now disable sessions and instead use plaintext passwords in cookies."
510
- return t % (db_path, owner)
578
+ t = "the %s [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --%s or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. %s"
579
+ return t % (desc, db_path, owner, pathopt, cons)
511
580
 
512
581
  vars = (("pid", os.getpid()), ("ts", int(time.time() * 1000)))
513
582
  if owner:
@@ -519,9 +588,9 @@ class SvcHub(object):
519
588
  for k, v in vars:
520
589
  cur.execute("insert into kv values(?, ?)", (k, v))
521
590
 
522
- if sver < VER_SESSION_DB:
591
+ if sver < native_ver:
523
592
  cur.execute("delete from kv where k='sver'")
524
- cur.execute("insert into kv values('sver',?)", (VER_SESSION_DB,))
593
+ cur.execute("insert into kv values('sver',?)", (native_ver,))
525
594
 
526
595
  db.commit()
527
596
  cur.close()
@@ -870,6 +939,12 @@ class SvcHub(object):
870
939
  vs = os.path.expandvars(os.path.expanduser(vs))
871
940
  setattr(al, k, vs)
872
941
 
942
+ for k in "idp_adm".split(" "):
943
+ vs = getattr(al, k)
944
+ vsa = [x.strip() for x in vs.split(",")]
945
+ vsa = [x.lower() for x in vsa if x]
946
+ setattr(al, k + "_set", set(vsa))
947
+
873
948
  zs = "dav_ua1 sus_urls nonsus_urls ua_nodoc ua_nozip"
874
949
  for k in zs.split(" "):
875
950
  vs = getattr(al, k)
copyparty/th_srv.py CHANGED
@@ -93,6 +93,10 @@ try:
93
93
  if os.environ.get("PRTY_NO_PIL_AVIF"):
94
94
  raise Exception()
95
95
 
96
+ if ".avif" in Image.registered_extensions():
97
+ HAVE_AVIF = True
98
+ raise Exception()
99
+
96
100
  import pillow_avif # noqa: F401 # pylint: disable=unused-import
97
101
 
98
102
  HAVE_AVIF = True
copyparty/util.py CHANGED
@@ -153,9 +153,15 @@ try:
153
153
  except:
154
154
  HAVE_PSUTIL = False
155
155
 
156
- if TYPE_CHECKING:
156
+ try:
157
+ if os.environ.get("PRTY_NO_MAGIC"):
158
+ raise Exception()
159
+
157
160
  import magic
161
+ except:
162
+ pass
158
163
 
164
+ if TYPE_CHECKING:
159
165
  from .authsrv import VFS
160
166
  from .broker_util import BrokerCli
161
167
  from .up2k import Up2k
@@ -1174,8 +1180,6 @@ class Magician(object):
1174
1180
  self.magic = None
1175
1181
 
1176
1182
  def ext(self, fpath ) :
1177
- import magic
1178
-
1179
1183
  try:
1180
1184
  if self.bad_magic:
1181
1185
  raise Exception()
@@ -3065,11 +3069,13 @@ def unescape_cookie(orig ) :
3065
3069
  return "".join(ret)
3066
3070
 
3067
3071
 
3068
- def guess_mime(url , fallback = "application/octet-stream") :
3072
+ def guess_mime(
3073
+ url , path = "", fallback = "application/octet-stream"
3074
+ ) :
3069
3075
  try:
3070
3076
  ext = url.rsplit(".", 1)[1].lower()
3071
3077
  except:
3072
- return fallback
3078
+ ext = ""
3073
3079
 
3074
3080
  ret = MIMES.get(ext)
3075
3081
 
@@ -3077,6 +3083,16 @@ def guess_mime(url , fallback = "application/octet-stream") :
3077
3083
  x = mimetypes.guess_type(url)
3078
3084
  ret = "application/{}".format(x[1]) if x[1] else x[0]
3079
3085
 
3086
+ if not ret and path:
3087
+ try:
3088
+ with open(fsenc(path), "rb", 0) as f:
3089
+ ret = magic.from_buffer(f.read(4096), mime=True)
3090
+ if ret.startswith("text/htm"):
3091
+ # avoid serving up HTML content unless there was actually a .html extension
3092
+ ret = "text/plain"
3093
+ except Exception as ex:
3094
+ pass
3095
+
3080
3096
  if not ret:
3081
3097
  ret = fallback
3082
3098