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/__init__.py +1 -0
- copyparty/__main__.py +18 -0
- copyparty/__version__.py +3 -3
- copyparty/authsrv.py +93 -17
- copyparty/cfg.py +14 -0
- copyparty/httpcli.py +214 -13
- copyparty/httpconn.py +3 -0
- copyparty/httpsrv.py +16 -0
- copyparty/svchub.py +100 -25
- copyparty/th_srv.py +4 -0
- copyparty/util.py +21 -5
- copyparty/web/browser.css.gz +0 -0
- copyparty/web/browser.js.gz +0 -0
- copyparty/web/deps/marked.js.gz +0 -0
- copyparty/web/idp.html +55 -0
- copyparty/web/splash.html +4 -0
- copyparty/web/splash.js.gz +0 -0
- copyparty/web/ui.css.gz +0 -0
- copyparty/web/up2k.js.gz +0 -0
- copyparty/web/util.js.gz +0 -0
- {copyparty-1.17.2.dist-info → copyparty-1.18.1.dist-info}/METADATA +32 -7
- {copyparty-1.17.2.dist-info → copyparty-1.18.1.dist-info}/RECORD +26 -25
- {copyparty-1.17.2.dist-info → copyparty-1.18.1.dist-info}/WHEEL +0 -0
- {copyparty-1.17.2.dist-info → copyparty-1.18.1.dist-info}/entry_points.txt +0 -0
- {copyparty-1.17.2.dist-info → copyparty-1.18.1.dist-info}/licenses/LICENSE +0 -0
- {copyparty-1.17.2.dist-info → copyparty-1.18.1.dist-info}/top_level.txt +0 -0
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
|
-
|
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
|
-
|
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
|
-
|
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,
|
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(
|
5619
|
+
self.redirect("", "?shares")
|
5419
5620
|
return True
|
5420
5621
|
|
5421
5622
|
def handle_share(self, req ) :
|
copyparty/httpconn.py
CHANGED
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.
|
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
|
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
|
-
|
421
|
-
|
422
|
-
|
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
|
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 >
|
447
|
-
zs = "this version of copyparty only understands
|
448
|
-
raise Exception(zs % (
|
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(
|
487
|
+
cur.execute(sanchk_q).fetchone()
|
451
488
|
except:
|
452
489
|
if sver:
|
453
490
|
raise
|
454
|
-
sver =
|
455
|
-
|
456
|
-
err = self.
|
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 >
|
503
|
+
if tries or sver > native_ver:
|
465
504
|
raise
|
466
|
-
t = "
|
467
|
-
self.log("root", t % (ex
|
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
|
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
|
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 <
|
591
|
+
if sver < native_ver:
|
523
592
|
cur.execute("delete from kv where k='sver'")
|
524
|
-
cur.execute("insert into kv values('sver',?)", (
|
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
|
-
|
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(
|
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
|
-
|
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
|
|