copyparty 1.16.16__py3-none-any.whl → 1.16.17__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/__main__.py +15 -40
- copyparty/__version__.py +2 -2
- copyparty/authsrv.py +137 -56
- copyparty/cfg.py +8 -0
- copyparty/fsutil.py +7 -5
- copyparty/httpcli.py +68 -32
- copyparty/svchub.py +2 -2
- copyparty/up2k.py +8 -5
- copyparty/util.py +32 -0
- copyparty/web/browser.js.gz +0 -0
- copyparty/web/up2k.js.gz +0 -0
- copyparty/web/util.js.gz +0 -0
- {copyparty-1.16.16.dist-info → copyparty-1.16.17.dist-info}/METADATA +12 -2
- {copyparty-1.16.16.dist-info → copyparty-1.16.17.dist-info}/RECORD +18 -18
- {copyparty-1.16.16.dist-info → copyparty-1.16.17.dist-info}/WHEEL +1 -1
- {copyparty-1.16.16.dist-info → copyparty-1.16.17.dist-info}/LICENSE +0 -0
- {copyparty-1.16.16.dist-info → copyparty-1.16.17.dist-info}/entry_points.txt +0 -0
- {copyparty-1.16.16.dist-info → copyparty-1.16.17.dist-info}/top_level.txt +0 -0
copyparty/__main__.py
CHANGED
@@ -65,6 +65,7 @@ from .util import (
|
|
65
65
|
load_resource,
|
66
66
|
min_ex,
|
67
67
|
pybin,
|
68
|
+
read_utf8,
|
68
69
|
termsize,
|
69
70
|
wrap,
|
70
71
|
)
|
@@ -249,8 +250,7 @@ def get_srvname(verbose) :
|
|
249
250
|
if verbose:
|
250
251
|
lprint("using hostname from {}\n".format(fp))
|
251
252
|
try:
|
252
|
-
|
253
|
-
ret = f.read().decode("utf-8", "replace").strip()
|
253
|
+
return read_utf8(None, fp, True).strip()
|
254
254
|
except:
|
255
255
|
ret = ""
|
256
256
|
namelen = 5
|
@@ -259,47 +259,18 @@ def get_srvname(verbose) :
|
|
259
259
|
ret = re.sub("[234567=]", "", ret)[:namelen]
|
260
260
|
with open(fp, "wb") as f:
|
261
261
|
f.write(ret.encode("utf-8") + b"\n")
|
262
|
-
|
263
|
-
return ret
|
264
|
-
|
265
|
-
|
266
|
-
def get_fk_salt() :
|
267
|
-
fp = os.path.join(E.cfg, "fk-salt.txt")
|
268
|
-
try:
|
269
|
-
with open(fp, "rb") as f:
|
270
|
-
ret = f.read().strip()
|
271
|
-
except:
|
272
|
-
ret = b64enc(os.urandom(18))
|
273
|
-
with open(fp, "wb") as f:
|
274
|
-
f.write(ret + b"\n")
|
275
|
-
|
276
|
-
return ret.decode("utf-8")
|
277
|
-
|
278
|
-
|
279
|
-
def get_dk_salt() :
|
280
|
-
fp = os.path.join(E.cfg, "dk-salt.txt")
|
281
|
-
try:
|
282
|
-
with open(fp, "rb") as f:
|
283
|
-
ret = f.read().strip()
|
284
|
-
except:
|
285
|
-
ret = b64enc(os.urandom(30))
|
286
|
-
with open(fp, "wb") as f:
|
287
|
-
f.write(ret + b"\n")
|
288
|
-
|
289
|
-
return ret.decode("utf-8")
|
262
|
+
return ret
|
290
263
|
|
291
264
|
|
292
|
-
def
|
293
|
-
fp = os.path.join(E.cfg, "
|
265
|
+
def get_salt(name , nbytes ) :
|
266
|
+
fp = os.path.join(E.cfg, "%s-salt.txt" % (name,))
|
294
267
|
try:
|
295
|
-
|
296
|
-
ret = f.read().strip()
|
268
|
+
return read_utf8(None, fp, True).strip()
|
297
269
|
except:
|
298
|
-
ret = b64enc(os.urandom(
|
270
|
+
ret = b64enc(os.urandom(nbytes))
|
299
271
|
with open(fp, "wb") as f:
|
300
272
|
f.write(ret + b"\n")
|
301
|
-
|
302
|
-
return ret.decode("utf-8")
|
273
|
+
return ret.decode("utf-8")
|
303
274
|
|
304
275
|
|
305
276
|
def ensure_locale() :
|
@@ -1257,6 +1228,10 @@ def add_optouts(ap):
|
|
1257
1228
|
ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI")
|
1258
1229
|
ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI")
|
1259
1230
|
ap2.add_argument("-nb", action="store_true", help="no powered-by-copyparty branding in UI")
|
1231
|
+
ap2.add_argument("--zipmaxn", metavar="N", type=u, default="0", help="reject download-as-zip if more than \033[33mN\033[0m files in total; optionally takes a unit suffix: [\033[32m256\033[0m], [\033[32m9K\033[0m], [\033[32m4G\033[0m] (volflag=zipmaxn)")
|
1232
|
+
ap2.add_argument("--zipmaxs", metavar="SZ", type=u, default="0", help="reject download-as-zip if total download size exceeds \033[33mSZ\033[0m bytes; optionally takes a unit suffix: [\033[32m256M\033[0m], [\033[32m4G\033[0m], [\033[32m2T\033[0m] (volflag=zipmaxs)")
|
1233
|
+
ap2.add_argument("--zipmaxt", metavar="TXT", type=u, default="", help="custom errormessage when download size exceeds max (volflag=zipmaxt)")
|
1234
|
+
ap2.add_argument("--zipmaxu", action="store_true", help="authenticated users bypass the zip size limit (volflag=zipmaxu)")
|
1260
1235
|
ap2.add_argument("--zip-who", metavar="LVL", type=int, default=3, help="who can download as zip/tar? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=authenticated-with-read-access, [\033[32m3\033[0m]=everyone-with-read-access (volflag=zip_who)\n\033[1;31mWARNING:\033[0m if a nested volume has a more restrictive value than a parent volume, then this will be \033[33mignored\033[0m if the download is initiated from the parent, more lenient volume")
|
1261
1236
|
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar; same as \033[33m--zip-who=0\033[0m")
|
1262
1237
|
ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)")
|
@@ -1544,9 +1519,9 @@ def run_argparse(
|
|
1544
1519
|
|
1545
1520
|
cert_path = os.path.join(E.cfg, "cert.pem")
|
1546
1521
|
|
1547
|
-
fk_salt =
|
1548
|
-
dk_salt =
|
1549
|
-
ah_salt =
|
1522
|
+
fk_salt = get_salt("fk", 18)
|
1523
|
+
dk_salt = get_salt("dk", 30)
|
1524
|
+
ah_salt = get_salt("ah", 18)
|
1550
1525
|
|
1551
1526
|
# alpine peaks at 5 threads for some reason,
|
1552
1527
|
# all others scale past that (but try to avoid SMT),
|
copyparty/__version__.py
CHANGED
copyparty/authsrv.py
CHANGED
@@ -33,6 +33,7 @@ from .util import (
|
|
33
33
|
get_df,
|
34
34
|
humansize,
|
35
35
|
odfusion,
|
36
|
+
read_utf8,
|
36
37
|
relchk,
|
37
38
|
statdir,
|
38
39
|
ub64enc,
|
@@ -64,6 +65,8 @@ SSEELOG = " ({})".format(SEE_LOG)
|
|
64
65
|
BAD_CFG = "invalid config; {}".format(SEE_LOG)
|
65
66
|
SBADCFG = " ({})".format(BAD_CFG)
|
66
67
|
|
68
|
+
PTN_U_GRP = re.compile(r"\$\{u%([+-])([^}]+)\}")
|
69
|
+
|
67
70
|
|
68
71
|
class CfgEx(Exception):
|
69
72
|
pass
|
@@ -335,22 +338,26 @@ class VFS(object):
|
|
335
338
|
log ,
|
336
339
|
realpath ,
|
337
340
|
vpath ,
|
341
|
+
vpath0 ,
|
338
342
|
axs ,
|
339
343
|
flags ,
|
340
344
|
) :
|
341
345
|
self.log = log
|
342
346
|
self.realpath = realpath # absolute path on host filesystem
|
343
347
|
self.vpath = vpath # absolute path in the virtual filesystem
|
348
|
+
self.vpath0 = vpath0 # original vpath (before idp expansion)
|
344
349
|
self.axs = axs
|
345
350
|
self.flags = flags # config options
|
346
351
|
self.root = self
|
347
352
|
self.dev = 0 # st_dev
|
353
|
+
self.badcfg1 = False
|
348
354
|
self.nodes = {} # child nodes
|
349
355
|
self.histtab = {} # all realpath->histpath
|
350
356
|
self.dbv = None # closest full/non-jump parent
|
351
357
|
self.lim = None # upload limits; only set for dbv
|
352
358
|
self.shr_src = None # source vfs+rem of a share
|
353
359
|
self.shr_files = set() # filenames to include from shr_src
|
360
|
+
self.shr_owner = "" # uname
|
354
361
|
self.aread = {}
|
355
362
|
self.awrite = {}
|
356
363
|
self.amove = {}
|
@@ -368,7 +375,7 @@ class VFS(object):
|
|
368
375
|
vp = vpath + ("/" if vpath else "")
|
369
376
|
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
|
370
377
|
self.all_vols = {vpath: self} # flattened recursive
|
371
|
-
self.all_nodes = {vpath: self} # also jumpvols
|
378
|
+
self.all_nodes = {vpath: self} # also jumpvols/shares
|
372
379
|
self.all_aps = [(rp, self)]
|
373
380
|
self.all_vps = [(vp, self)]
|
374
381
|
else:
|
@@ -408,7 +415,7 @@ class VFS(object):
|
|
408
415
|
for v in self.nodes.values():
|
409
416
|
v.get_all_vols(vols, nodes, aps, vps)
|
410
417
|
|
411
|
-
def add(self, src , dst ) :
|
418
|
+
def add(self, src , dst , dst0 ) :
|
412
419
|
"""get existing, or add new path to the vfs"""
|
413
420
|
assert src == "/" or not src.endswith("/") # nosec
|
414
421
|
assert not dst.endswith("/") # nosec
|
@@ -416,20 +423,22 @@ class VFS(object):
|
|
416
423
|
if "/" in dst:
|
417
424
|
# requires breadth-first population (permissions trickle down)
|
418
425
|
name, dst = dst.split("/", 1)
|
426
|
+
name0, dst0 = dst0.split("/", 1)
|
419
427
|
if name in self.nodes:
|
420
428
|
# exists; do not manipulate permissions
|
421
|
-
return self.nodes[name].add(src, dst)
|
429
|
+
return self.nodes[name].add(src, dst, dst0)
|
422
430
|
|
423
431
|
vn = VFS(
|
424
432
|
self.log,
|
425
433
|
os.path.join(self.realpath, name) if self.realpath else "",
|
426
434
|
"{}/{}".format(self.vpath, name).lstrip("/"),
|
435
|
+
"{}/{}".format(self.vpath0, name0).lstrip("/"),
|
427
436
|
self.axs,
|
428
437
|
self._copy_flags(name),
|
429
438
|
)
|
430
439
|
vn.dbv = self.dbv or self
|
431
440
|
self.nodes[name] = vn
|
432
|
-
return vn.add(src, dst)
|
441
|
+
return vn.add(src, dst, dst0)
|
433
442
|
|
434
443
|
if dst in self.nodes:
|
435
444
|
# leaf exists; return as-is
|
@@ -437,7 +446,8 @@ class VFS(object):
|
|
437
446
|
|
438
447
|
# leaf does not exist; create and keep permissions blank
|
439
448
|
vp = "{}/{}".format(self.vpath, dst).lstrip("/")
|
440
|
-
|
449
|
+
vp0 = "{}/{}".format(self.vpath0, dst0).lstrip("/")
|
450
|
+
vn = VFS(self.log, src, vp, vp0, AXS(), {})
|
441
451
|
vn.dbv = self.dbv or self
|
442
452
|
self.nodes[dst] = vn
|
443
453
|
return vn
|
@@ -854,7 +864,7 @@ class AuthSrv(object):
|
|
854
864
|
self.indent = ""
|
855
865
|
|
856
866
|
# fwd-decl
|
857
|
-
self.vfs = VFS(log_func, "", "", AXS(), {})
|
867
|
+
self.vfs = VFS(log_func, "", "", "", AXS(), {})
|
858
868
|
self.acct = {} # uname->pw
|
859
869
|
self.iacct = {} # pw->uname
|
860
870
|
self.ases = {} # uname->session
|
@@ -922,7 +932,7 @@ class AuthSrv(object):
|
|
922
932
|
self,
|
923
933
|
src ,
|
924
934
|
dst ,
|
925
|
-
mount
|
935
|
+
mount ,
|
926
936
|
daxs ,
|
927
937
|
mflags ,
|
928
938
|
un_gns ,
|
@@ -938,12 +948,24 @@ class AuthSrv(object):
|
|
938
948
|
un_gn = [("", "")]
|
939
949
|
|
940
950
|
for un, gn in un_gn:
|
951
|
+
m = PTN_U_GRP.search(dst0)
|
952
|
+
if m:
|
953
|
+
req, gnc = m.groups()
|
954
|
+
hit = gnc in (un_gns.get(un) or [])
|
955
|
+
if req == "+":
|
956
|
+
if not hit:
|
957
|
+
continue
|
958
|
+
elif hit:
|
959
|
+
continue
|
960
|
+
|
941
961
|
# if ap/vp has a user/group placeholder, make sure to keep
|
942
962
|
# track so the same user/group is mapped when setting perms;
|
943
963
|
# otherwise clear un/gn to indicate it's a regular volume
|
944
964
|
|
945
965
|
src1 = src0.replace("${u}", un or "\n")
|
946
966
|
dst1 = dst0.replace("${u}", un or "\n")
|
967
|
+
src1 = PTN_U_GRP.sub(un or "\n", src1)
|
968
|
+
dst1 = PTN_U_GRP.sub(un or "\n", dst1)
|
947
969
|
if src0 == src1 and dst0 == dst1:
|
948
970
|
un = ""
|
949
971
|
|
@@ -960,7 +982,7 @@ class AuthSrv(object):
|
|
960
982
|
continue
|
961
983
|
visited.add(label)
|
962
984
|
|
963
|
-
src, dst = self._map_volume(src, dst, mount, daxs, mflags)
|
985
|
+
src, dst = self._map_volume(src, dst, dst0, mount, daxs, mflags)
|
964
986
|
if src:
|
965
987
|
ret.append((src, dst, un, gn))
|
966
988
|
if un or gn:
|
@@ -972,7 +994,8 @@ class AuthSrv(object):
|
|
972
994
|
self,
|
973
995
|
src ,
|
974
996
|
dst ,
|
975
|
-
|
997
|
+
dst0 ,
|
998
|
+
mount ,
|
976
999
|
daxs ,
|
977
1000
|
mflags ,
|
978
1001
|
) :
|
@@ -982,13 +1005,13 @@ class AuthSrv(object):
|
|
982
1005
|
|
983
1006
|
if dst in mount:
|
984
1007
|
t = "multiple filesystem-paths mounted at [/{}]:\n [{}]\n [{}]"
|
985
|
-
self.log(t.format(dst, mount[dst], src), c=1)
|
1008
|
+
self.log(t.format(dst, mount[dst][0], src), c=1)
|
986
1009
|
raise Exception(BAD_CFG)
|
987
1010
|
|
988
1011
|
if src in mount.values():
|
989
1012
|
t = "filesystem-path [{}] mounted in multiple locations:"
|
990
1013
|
t = t.format(src)
|
991
|
-
for v in [k for k, v in mount.items() if v == src] + [dst]:
|
1014
|
+
for v in [k for k, v in mount.items() if v[0] == src] + [dst]:
|
992
1015
|
t += "\n /{}".format(v)
|
993
1016
|
|
994
1017
|
self.log(t, c=3)
|
@@ -997,7 +1020,7 @@ class AuthSrv(object):
|
|
997
1020
|
if not bos.path.isdir(src):
|
998
1021
|
self.log("warning: filesystem-path does not exist: {}".format(src), 3)
|
999
1022
|
|
1000
|
-
mount[dst] = src
|
1023
|
+
mount[dst] = (src, dst0)
|
1001
1024
|
daxs[dst] = AXS()
|
1002
1025
|
mflags[dst] = {}
|
1003
1026
|
return (src, dst)
|
@@ -1058,7 +1081,7 @@ class AuthSrv(object):
|
|
1058
1081
|
grps ,
|
1059
1082
|
daxs ,
|
1060
1083
|
mflags ,
|
1061
|
-
mount
|
1084
|
+
mount ,
|
1062
1085
|
) :
|
1063
1086
|
self.line_ctr = 0
|
1064
1087
|
|
@@ -1083,7 +1106,7 @@ class AuthSrv(object):
|
|
1083
1106
|
grps ,
|
1084
1107
|
daxs ,
|
1085
1108
|
mflags ,
|
1086
|
-
mount
|
1109
|
+
mount ,
|
1087
1110
|
npass ,
|
1088
1111
|
) :
|
1089
1112
|
self.line_ctr = 0
|
@@ -1442,8 +1465,8 @@ class AuthSrv(object):
|
|
1442
1465
|
acct = {} # username:password
|
1443
1466
|
grps = {} # groupname:usernames
|
1444
1467
|
daxs = {}
|
1445
|
-
mflags = {} #
|
1446
|
-
mount
|
1468
|
+
mflags = {} # vpath:flags
|
1469
|
+
mount = {} # dst:src (vp:(ap,vp0))
|
1447
1470
|
|
1448
1471
|
self.idp_vols = {} # yolo
|
1449
1472
|
|
@@ -1522,8 +1545,8 @@ class AuthSrv(object):
|
|
1522
1545
|
# case-insensitive; normalize
|
1523
1546
|
if WINDOWS:
|
1524
1547
|
cased = {}
|
1525
|
-
for
|
1526
|
-
cased[
|
1548
|
+
for vp, (ap, vp0) in mount.items():
|
1549
|
+
cased[vp] = (absreal(ap), vp0)
|
1527
1550
|
|
1528
1551
|
mount = cased
|
1529
1552
|
|
@@ -1538,25 +1561,28 @@ class AuthSrv(object):
|
|
1538
1561
|
t = "Read-access has been disabled due to failsafe: No volumes were defined by the config-file. This failsafe is to prevent unintended access if this is due to accidental loss of config. You can override this safeguard and allow read/write to the working-directory by adding the following arguments: -v .::rw"
|
1539
1562
|
self.log(t, 1)
|
1540
1563
|
axs = AXS()
|
1541
|
-
vfs = VFS(self.log_func, absreal("."), "", axs, {})
|
1564
|
+
vfs = VFS(self.log_func, absreal("."), "", "", axs, {})
|
1565
|
+
if not axs.uread:
|
1566
|
+
vfs.badcfg1 = True
|
1542
1567
|
elif "" not in mount:
|
1543
1568
|
# there's volumes but no root; make root inaccessible
|
1544
1569
|
zsd = {"d2d": True, "tcolor": self.args.tcolor}
|
1545
|
-
vfs = VFS(self.log_func, "", "", AXS(), zsd)
|
1570
|
+
vfs = VFS(self.log_func, "", "", "", AXS(), zsd)
|
1546
1571
|
|
1547
1572
|
maxdepth = 0
|
1548
1573
|
for dst in sorted(mount.keys(), key=lambda x: (x.count("/"), len(x))):
|
1549
1574
|
depth = dst.count("/")
|
1550
1575
|
assert maxdepth <= depth # nosec
|
1551
1576
|
maxdepth = depth
|
1577
|
+
src, dst0 = mount[dst]
|
1552
1578
|
|
1553
1579
|
if dst == "":
|
1554
1580
|
# rootfs was mapped; fully replaces the default CWD vfs
|
1555
|
-
vfs = VFS(self.log_func,
|
1581
|
+
vfs = VFS(self.log_func, src, dst, dst0, daxs[dst], mflags[dst])
|
1556
1582
|
continue
|
1557
1583
|
|
1558
1584
|
assert vfs # type: ignore
|
1559
|
-
zv = vfs.add(
|
1585
|
+
zv = vfs.add(src, dst, dst0)
|
1560
1586
|
zv.axs = daxs[dst]
|
1561
1587
|
zv.flags = mflags[dst]
|
1562
1588
|
zv.dbv = None
|
@@ -1590,7 +1616,8 @@ class AuthSrv(object):
|
|
1590
1616
|
if enshare:
|
1591
1617
|
import sqlite3
|
1592
1618
|
|
1593
|
-
|
1619
|
+
zsd = {"d2d": True, "tcolor": self.args.tcolor}
|
1620
|
+
shv = VFS(self.log_func, "", shr, shr, AXS(), zsd)
|
1594
1621
|
|
1595
1622
|
db_path = self.args.shr_db
|
1596
1623
|
db = sqlite3.connect(db_path)
|
@@ -1624,9 +1651,8 @@ class AuthSrv(object):
|
|
1624
1651
|
|
1625
1652
|
# don't know the abspath yet + wanna ensure the user
|
1626
1653
|
# still has the privs they granted, so nullmap it
|
1627
|
-
|
1628
|
-
|
1629
|
-
)
|
1654
|
+
vp = "%s/%s" % (shr, s_k)
|
1655
|
+
shv.nodes[s_k] = VFS(self.log_func, "", vp, vp, s_axs, shv.flags.copy())
|
1630
1656
|
|
1631
1657
|
vfs.nodes[shr] = vfs.all_vols[shr] = shv
|
1632
1658
|
for vol in shv.nodes.values():
|
@@ -1787,6 +1813,24 @@ class AuthSrv(object):
|
|
1787
1813
|
rhisttab[histp] = zv
|
1788
1814
|
vfs.histtab[zv.realpath] = histp
|
1789
1815
|
|
1816
|
+
for vol in vfs.all_vols.values():
|
1817
|
+
use = False
|
1818
|
+
for k in ["zipmaxn", "zipmaxs"]:
|
1819
|
+
try:
|
1820
|
+
zs = vol.flags[k]
|
1821
|
+
except:
|
1822
|
+
zs = getattr(self.args, k)
|
1823
|
+
if zs in ("", "0"):
|
1824
|
+
vol.flags[k] = 0
|
1825
|
+
continue
|
1826
|
+
|
1827
|
+
zf = unhumanize(zs)
|
1828
|
+
vol.flags[k + "_v"] = zf
|
1829
|
+
if zf:
|
1830
|
+
use = True
|
1831
|
+
if use:
|
1832
|
+
vol.flags["zipmax"] = True
|
1833
|
+
|
1790
1834
|
for vol in vfs.all_vols.values():
|
1791
1835
|
lim = Lim(self.log_func)
|
1792
1836
|
use = False
|
@@ -2269,22 +2313,56 @@ class AuthSrv(object):
|
|
2269
2313
|
except Pebkac:
|
2270
2314
|
self.warn_anonwrite = True
|
2271
2315
|
|
2272
|
-
|
2316
|
+
self.idp_warn = []
|
2317
|
+
self.idp_err = []
|
2273
2318
|
for idp_vp in self.idp_vols:
|
2274
|
-
|
2275
|
-
|
2276
|
-
|
2277
|
-
|
2278
|
-
|
2279
|
-
|
2280
|
-
|
2281
|
-
|
2282
|
-
|
2283
|
-
|
2284
|
-
|
2285
|
-
|
2286
|
-
|
2287
|
-
|
2319
|
+
idp_vn, _ = vfs.get(idp_vp, "*", False, False)
|
2320
|
+
idp_vp0 = idp_vn.vpath0
|
2321
|
+
|
2322
|
+
sigils = set(re.findall(r"(\${[ug][}%])", idp_vp0))
|
2323
|
+
if len(sigils) > 1:
|
2324
|
+
t = '\nWARNING: IdP-volume "/%s" created by "/%s" has multiple IdP placeholders: %s'
|
2325
|
+
self.idp_warn.append(t % (idp_vp, idp_vp0, list(sigils)))
|
2326
|
+
continue
|
2327
|
+
|
2328
|
+
sigil = sigils.pop()
|
2329
|
+
par_vp = idp_vp
|
2330
|
+
while par_vp:
|
2331
|
+
par_vp = vsplit(par_vp)[0]
|
2332
|
+
par_vn, _ = vfs.get(par_vp, "*", False, False)
|
2333
|
+
if sigil in par_vn.vpath0:
|
2334
|
+
continue # parent was spawned for and by same user
|
2335
|
+
|
2336
|
+
oth_read = []
|
2337
|
+
oth_write = []
|
2338
|
+
for usr in par_vn.axs.uread:
|
2339
|
+
if usr not in idp_vn.axs.uread:
|
2340
|
+
oth_read.append(usr)
|
2341
|
+
for usr in par_vn.axs.uwrite:
|
2342
|
+
if usr not in idp_vn.axs.uwrite:
|
2343
|
+
oth_write.append(usr)
|
2344
|
+
|
2345
|
+
if "*" in oth_read:
|
2346
|
+
taxs = "WORLD-READABLE"
|
2347
|
+
elif "*" in oth_write:
|
2348
|
+
taxs = "WORLD-WRITABLE"
|
2349
|
+
elif oth_read:
|
2350
|
+
taxs = "READABLE BY %r" % (oth_read,)
|
2351
|
+
elif oth_write:
|
2352
|
+
taxs = "WRITABLE BY %r" % (oth_write,)
|
2353
|
+
else:
|
2354
|
+
break # no sigil; not idp; safe to stop
|
2355
|
+
|
2356
|
+
t = '\nWARNING: IdP-volume "/%s" created by "/%s" has parent/grandparent "/%s" and would be %s'
|
2357
|
+
self.idp_err.append(t % (idp_vp, idp_vp0, par_vn.vpath, taxs))
|
2358
|
+
|
2359
|
+
if self.idp_warn:
|
2360
|
+
t = "WARNING! Some IdP volumes include multiple IdP placeholders; this is too complex to automatically determine if safe or not. To ensure that no users gain unintended access, please use only a single placeholder for each IdP volume."
|
2361
|
+
self.log(t + "".join(self.idp_warn), 1)
|
2362
|
+
|
2363
|
+
if self.idp_err:
|
2364
|
+
t = "WARNING! The following IdP volumes are mounted below another volume where other users can read and/or write files. This is a SECURITY HAZARD!! When copyparty is restarted, it will not know about these IdP volumes yet. These volumes will then be accessible by an unexpected set of permissions UNTIL one of the users associated with their volume sends a request to the server. RECOMMENDATION: You should create a restricted volume where nobody can read/write files, and make sure that all IdP volumes are configured to appear somewhere below that volume."
|
2365
|
+
self.log(t + "".join(self.idp_err), 1)
|
2288
2366
|
|
2289
2367
|
self.vfs = vfs
|
2290
2368
|
self.acct = acct
|
@@ -2319,11 +2397,6 @@ class AuthSrv(object):
|
|
2319
2397
|
for x, y in vfs.all_vols.items()
|
2320
2398
|
if x != shr and not x.startswith(shrs)
|
2321
2399
|
}
|
2322
|
-
vfs.all_nodes = {
|
2323
|
-
x: y
|
2324
|
-
for x, y in vfs.all_nodes.items()
|
2325
|
-
if x != shr and not x.startswith(shrs)
|
2326
|
-
}
|
2327
2400
|
|
2328
2401
|
assert db and cur and cur2 and shv # type: ignore
|
2329
2402
|
for row in cur.execute("select * from sh"):
|
@@ -2353,6 +2426,7 @@ class AuthSrv(object):
|
|
2353
2426
|
else:
|
2354
2427
|
shn.ls = shn._ls
|
2355
2428
|
|
2429
|
+
shn.shr_owner = s_un
|
2356
2430
|
shn.shr_src = (s_vfs, s_rem)
|
2357
2431
|
shn.realpath = s_vfs.canonical(s_rem)
|
2358
2432
|
|
@@ -2370,7 +2444,7 @@ class AuthSrv(object):
|
|
2370
2444
|
continue # also fine
|
2371
2445
|
for zs in svn.nodes.keys():
|
2372
2446
|
# hide subvolume
|
2373
|
-
vn.nodes[zs] = VFS(self.log_func, "", "", AXS(), {})
|
2447
|
+
vn.nodes[zs] = VFS(self.log_func, "", "", "", AXS(), {})
|
2374
2448
|
|
2375
2449
|
cur2.close()
|
2376
2450
|
cur.close()
|
@@ -2378,7 +2452,9 @@ class AuthSrv(object):
|
|
2378
2452
|
|
2379
2453
|
self.js_ls = {}
|
2380
2454
|
self.js_htm = {}
|
2381
|
-
for vn in self.vfs.all_nodes.
|
2455
|
+
for vp, vn in self.vfs.all_nodes.items():
|
2456
|
+
if enshare and vp.startswith(shrs):
|
2457
|
+
continue # propagates later in this func
|
2382
2458
|
vf = vn.flags
|
2383
2459
|
vn.js_ls = {
|
2384
2460
|
"idx": "e2d" in vf,
|
@@ -2435,8 +2511,12 @@ class AuthSrv(object):
|
|
2435
2511
|
|
2436
2512
|
vols = list(vfs.all_nodes.values())
|
2437
2513
|
if enshare:
|
2438
|
-
|
2439
|
-
|
2514
|
+
for vol in shv.nodes.values():
|
2515
|
+
if vol.vpath not in vfs.all_nodes:
|
2516
|
+
self.log("BUG: /%s not in all_nodes" % (vol.vpath,), 1)
|
2517
|
+
vols.append(vol)
|
2518
|
+
if shr in vfs.all_nodes:
|
2519
|
+
self.log("BUG: %s found in all_nodes" % (shr,), 1)
|
2440
2520
|
|
2441
2521
|
for vol in vols:
|
2442
2522
|
dbv = vol.get_dbv("")[0]
|
@@ -2539,8 +2619,8 @@ class AuthSrv(object):
|
|
2539
2619
|
if not bos.path.exists(ap):
|
2540
2620
|
pwdb = {}
|
2541
2621
|
else:
|
2542
|
-
|
2543
|
-
|
2622
|
+
jtxt = read_utf8(self.log, ap, True)
|
2623
|
+
pwdb = json.loads(jtxt)
|
2544
2624
|
|
2545
2625
|
pwdb = [x for x in pwdb if x[0] != uname]
|
2546
2626
|
pwdb.append((uname, self.defpw[uname], hpw))
|
@@ -2563,8 +2643,8 @@ class AuthSrv(object):
|
|
2563
2643
|
if not self.args.chpw or not bos.path.exists(ap):
|
2564
2644
|
return
|
2565
2645
|
|
2566
|
-
|
2567
|
-
|
2646
|
+
jtxt = read_utf8(self.log, ap, True)
|
2647
|
+
pwdb = json.loads(jtxt)
|
2568
2648
|
|
2569
2649
|
useen = set()
|
2570
2650
|
urst = set()
|
@@ -3060,8 +3140,9 @@ def expand_config_file(
|
|
3060
3140
|
ipath += " -> " + fp
|
3061
3141
|
ret.append("#\033[36m opening cfg file{}\033[0m".format(ipath))
|
3062
3142
|
|
3063
|
-
|
3064
|
-
|
3143
|
+
cfg_lines = read_utf8(log, fp, True).split("\n")
|
3144
|
+
if True: # diff-golf
|
3145
|
+
for oln in [x.rstrip() for x in cfg_lines]:
|
3065
3146
|
ln = oln.split(" #")[0].strip()
|
3066
3147
|
if ln.startswith("% "):
|
3067
3148
|
pad = " " * len(oln.split("%")[0])
|
copyparty/cfg.py
CHANGED
@@ -55,6 +55,7 @@ def vf_bmap() :
|
|
55
55
|
"xdev",
|
56
56
|
"xlink",
|
57
57
|
"xvol",
|
58
|
+
"zipmaxu",
|
58
59
|
):
|
59
60
|
ret[k] = k
|
60
61
|
return ret
|
@@ -101,6 +102,9 @@ def vf_vmap() :
|
|
101
102
|
"u2ts",
|
102
103
|
"ups_who",
|
103
104
|
"zip_who",
|
105
|
+
"zipmaxn",
|
106
|
+
"zipmaxs",
|
107
|
+
"zipmaxt",
|
104
108
|
):
|
105
109
|
ret[k] = k
|
106
110
|
return ret
|
@@ -299,6 +303,10 @@ flagcats = {
|
|
299
303
|
"rss": "allow '?rss' URL suffix (experimental)",
|
300
304
|
"ups_who=2": "restrict viewing the list of recent uploads",
|
301
305
|
"zip_who=2": "restrict access to download-as-zip/tar",
|
306
|
+
"zipmaxn=9k": "reject download-as-zip if more than 9000 files",
|
307
|
+
"zipmaxs=2g": "reject download-as-zip if size over 2 GiB",
|
308
|
+
"zipmaxt=no": "reply with 'no' if download-as-zip exceeds max",
|
309
|
+
"zipmaxu": "zip-size-limit does not apply to authenticated users",
|
302
310
|
"nopipe": "disable race-the-beam (download unfinished uploads)",
|
303
311
|
"mv_retry": "ms-windows: timeout for renaming busy files",
|
304
312
|
"rm_retry": "ms-windows: timeout for deleting busy files",
|
copyparty/fsutil.py
CHANGED
@@ -72,7 +72,7 @@ class Fstab(object):
|
|
72
72
|
return vid
|
73
73
|
|
74
74
|
def build_fallback(self) :
|
75
|
-
self.tab = VFS(self.log_func, "idk", "/", AXS(), {})
|
75
|
+
self.tab = VFS(self.log_func, "idk", "/", "/", AXS(), {})
|
76
76
|
self.trusted = False
|
77
77
|
|
78
78
|
def build_tab(self) :
|
@@ -105,9 +105,10 @@ class Fstab(object):
|
|
105
105
|
|
106
106
|
tab1.sort(key=lambda x: (len(x[0]), x[0]))
|
107
107
|
path1, fs1 = tab1[0]
|
108
|
-
tab = VFS(self.log_func, fs1, path1, AXS(), {})
|
108
|
+
tab = VFS(self.log_func, fs1, path1, path1, AXS(), {})
|
109
109
|
for path, fs in tab1[1:]:
|
110
|
-
|
110
|
+
zs = path.lstrip("/")
|
111
|
+
tab.add(fs, zs, zs)
|
111
112
|
|
112
113
|
self.tab = tab
|
113
114
|
self.srctab = srctab
|
@@ -123,9 +124,10 @@ class Fstab(object):
|
|
123
124
|
if not self.trusted:
|
124
125
|
# no mtab access; have to build as we go
|
125
126
|
if "/" in rem:
|
126
|
-
|
127
|
+
zs = os.path.join(vn.vpath, rem.split("/")[0])
|
128
|
+
self.tab.add("idk", zs, zs)
|
127
129
|
if rem:
|
128
|
-
self.tab.add(nval, path)
|
130
|
+
self.tab.add(nval, path, path)
|
129
131
|
else:
|
130
132
|
vn.realpath = nval
|
131
133
|
|
copyparty/httpcli.py
CHANGED
@@ -22,6 +22,7 @@ from datetime import datetime
|
|
22
22
|
from operator import itemgetter
|
23
23
|
|
24
24
|
import jinja2 # typechk
|
25
|
+
from ipaddress import IPv6Network
|
25
26
|
|
26
27
|
try:
|
27
28
|
if os.environ.get("PRTY_NO_LZMA"):
|
@@ -89,6 +90,7 @@ from .util import (
|
|
89
90
|
read_socket,
|
90
91
|
read_socket_chunked,
|
91
92
|
read_socket_unbounded,
|
93
|
+
read_utf8,
|
92
94
|
relchk,
|
93
95
|
ren_open,
|
94
96
|
runhook,
|
@@ -382,11 +384,12 @@ class HttpCli(object):
|
|
382
384
|
t += ' Note: if you are behind cloudflare, then this default header is not a good choice; please first make sure your local reverse-proxy (if any) does not allow non-cloudflare IPs from providing cf-* headers, and then add this additional global setting: "--xff-hdr=cf-connecting-ip"'
|
383
385
|
else:
|
384
386
|
t += ' Note: depending on your reverse-proxy, and/or WAF, and/or other intermediates, you may want to read the true client IP from another header by also specifying "--xff-hdr=SomeOtherHeader"'
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
387
|
+
|
388
|
+
if "." in pip:
|
389
|
+
zs = ".".join(pip.split(".")[:2]) + ".0.0/16"
|
390
|
+
else:
|
391
|
+
zs = IPv6Network(pip + "/64", False).compressed
|
392
|
+
|
390
393
|
zs2 = ' or "--xff-src=lan"' if self.conn.xff_lan.map(pip) else ""
|
391
394
|
self.log(t % (self.args.xff_hdr, pip, cli_ip, zso, zs, zs2), 3)
|
392
395
|
self.bad_xff = True
|
@@ -863,8 +866,7 @@ class HttpCli(object):
|
|
863
866
|
html = html.replace("%", "", 1)
|
864
867
|
|
865
868
|
if html.startswith("@"):
|
866
|
-
|
867
|
-
html = f.read().decode("utf-8")
|
869
|
+
html = read_utf8(self.log, html[1:], True)
|
868
870
|
|
869
871
|
if html.startswith("%"):
|
870
872
|
html = html[1:]
|
@@ -1231,14 +1233,7 @@ class HttpCli(object):
|
|
1231
1233
|
return self.tx_404(True)
|
1232
1234
|
else:
|
1233
1235
|
vfs = self.asrv.vfs
|
1234
|
-
if
|
1235
|
-
not vfs.nodes
|
1236
|
-
and not vfs.axs.uread
|
1237
|
-
and not vfs.axs.uwrite
|
1238
|
-
and not vfs.axs.uget
|
1239
|
-
and not vfs.axs.uhtml
|
1240
|
-
and not vfs.axs.uadmin
|
1241
|
-
):
|
1236
|
+
if vfs.badcfg1:
|
1242
1237
|
t = "<h2>access denied due to failsafe; check server log</h2>"
|
1243
1238
|
html = self.j2s("splash", this=self, msg=t)
|
1244
1239
|
self.reply(html.encode("utf-8", "replace"), 500)
|
@@ -3720,8 +3715,7 @@ class HttpCli(object):
|
|
3720
3715
|
continue
|
3721
3716
|
fn = "%s/%s" % (abspath, fn)
|
3722
3717
|
if bos.path.isfile(fn):
|
3723
|
-
|
3724
|
-
logues[n] = f.read().decode("utf-8")
|
3718
|
+
logues[n] = read_utf8(self.log, fsenc(fn), False)
|
3725
3719
|
if "exp" in vn.flags:
|
3726
3720
|
logues[n] = self._expand(
|
3727
3721
|
logues[n], vn.flags.get("exp_lg") or []
|
@@ -3742,9 +3736,8 @@ class HttpCli(object):
|
|
3742
3736
|
for fn in fns:
|
3743
3737
|
fn = "%s/%s" % (abspath, fn)
|
3744
3738
|
if bos.path.isfile(fn):
|
3745
|
-
|
3746
|
-
|
3747
|
-
break
|
3739
|
+
txt = read_utf8(self.log, fsenc(fn), False)
|
3740
|
+
break
|
3748
3741
|
|
3749
3742
|
if txt and "exp" in vn.flags:
|
3750
3743
|
txt = self._expand(txt, vn.flags.get("exp_md") or [])
|
@@ -3777,6 +3770,16 @@ class HttpCli(object):
|
|
3777
3770
|
|
3778
3771
|
return txt
|
3779
3772
|
|
3773
|
+
def _can_zip(self, volflags ) :
|
3774
|
+
lvl = volflags["zip_who"]
|
3775
|
+
if self.args.no_zip or not lvl:
|
3776
|
+
return "download-as-zip/tar is disabled in server config"
|
3777
|
+
elif lvl <= 1 and not self.can_admin:
|
3778
|
+
return "download-as-zip/tar is admin-only on this server"
|
3779
|
+
elif lvl <= 2 and self.uname in ("", "*"):
|
3780
|
+
return "you must be authenticated to download-as-zip/tar on this server"
|
3781
|
+
return ""
|
3782
|
+
|
3780
3783
|
def tx_res(self, req_path ) :
|
3781
3784
|
status = 200
|
3782
3785
|
logmsg = "{:4} {} ".format("", self.req)
|
@@ -4307,13 +4310,8 @@ class HttpCli(object):
|
|
4307
4310
|
rem ,
|
4308
4311
|
items ,
|
4309
4312
|
) :
|
4310
|
-
|
4311
|
-
if
|
4312
|
-
raise Pebkac(400, "download-as-zip/tar is disabled in server config")
|
4313
|
-
elif lvl <= 1 and not self.can_admin:
|
4314
|
-
raise Pebkac(400, "download-as-zip/tar is admin-only on this server")
|
4315
|
-
elif lvl <= 2 and self.uname in ("", "*"):
|
4316
|
-
t = "you must be authenticated to download-as-zip/tar on this server"
|
4313
|
+
t = self._can_zip(vn.flags)
|
4314
|
+
if t:
|
4317
4315
|
raise Pebkac(400, t)
|
4318
4316
|
|
4319
4317
|
logmsg = "{:4} {} ".format("", self.req)
|
@@ -4346,6 +4344,33 @@ class HttpCli(object):
|
|
4346
4344
|
else:
|
4347
4345
|
fn = self.host.split(":")[0]
|
4348
4346
|
|
4347
|
+
if vn.flags.get("zipmax") and (not self.uname or not "zipmaxu" in vn.flags):
|
4348
|
+
maxs = vn.flags.get("zipmaxs_v") or 0
|
4349
|
+
maxn = vn.flags.get("zipmaxn_v") or 0
|
4350
|
+
nf = 0
|
4351
|
+
nb = 0
|
4352
|
+
fgen = vn.zipgen(
|
4353
|
+
vpath, rem, set(items), self.uname, False, not self.args.no_scandir
|
4354
|
+
)
|
4355
|
+
t = "total size exceeds a limit specified in server config"
|
4356
|
+
t = vn.flags.get("zipmaxt") or t
|
4357
|
+
if maxs and maxn:
|
4358
|
+
for zd in fgen:
|
4359
|
+
nf += 1
|
4360
|
+
nb += zd["st"].st_size
|
4361
|
+
if maxs < nb or maxn < nf:
|
4362
|
+
raise Pebkac(400, t)
|
4363
|
+
elif maxs:
|
4364
|
+
for zd in fgen:
|
4365
|
+
nb += zd["st"].st_size
|
4366
|
+
if maxs < nb:
|
4367
|
+
raise Pebkac(400, t)
|
4368
|
+
elif maxn:
|
4369
|
+
for zd in fgen:
|
4370
|
+
nf += 1
|
4371
|
+
if maxn < nf:
|
4372
|
+
raise Pebkac(400, t)
|
4373
|
+
|
4349
4374
|
safe = (string.ascii_letters + string.digits).replace("%", "")
|
4350
4375
|
afn = "".join([x if x in safe.replace('"', "") else "_" for x in fn])
|
4351
4376
|
bascii = unicode(safe).encode("utf-8")
|
@@ -4991,6 +5016,8 @@ class HttpCli(object):
|
|
4991
5016
|
def get_dls(self) :
|
4992
5017
|
ret = []
|
4993
5018
|
dls = self.conn.hsrv.tdls
|
5019
|
+
enshare = self.args.shr
|
5020
|
+
shrs = enshare[1:]
|
4994
5021
|
for dl_id, (t0, sz, vn, vp, uname) in self.conn.hsrv.tdli.items():
|
4995
5022
|
t1, sent = dls[dl_id]
|
4996
5023
|
if sent > 0x100000: # 1m; buffers 2~4
|
@@ -4999,6 +5026,15 @@ class HttpCli(object):
|
|
4999
5026
|
vp = ""
|
5000
5027
|
elif self.uname not in vn.axs.udot and (vp.startswith(".") or "/." in vp):
|
5001
5028
|
vp = ""
|
5029
|
+
elif (
|
5030
|
+
enshare
|
5031
|
+
and vp.startswith(shrs)
|
5032
|
+
and self.uname != vn.shr_owner
|
5033
|
+
and self.uname not in vn.axs.uadmin
|
5034
|
+
and self.uname not in self.args.shr_adm
|
5035
|
+
and not dl_id.startswith(self.ip + ":")
|
5036
|
+
):
|
5037
|
+
vp = ""
|
5002
5038
|
if self.uname not in vn.axs.uadmin:
|
5003
5039
|
dl_id = uname = ""
|
5004
5040
|
|
@@ -5980,6 +6016,8 @@ class HttpCli(object):
|
|
5980
6016
|
zs = self.gen_fk(2, self.args.dk_salt, abspath, 0, 0)[:add_dk]
|
5981
6017
|
ls_ret["dk"] = cgv["dk"] = zs
|
5982
6018
|
|
6019
|
+
no_zip = bool(self._can_zip(vf))
|
6020
|
+
|
5983
6021
|
dirs = []
|
5984
6022
|
files = []
|
5985
6023
|
ptn_hr = RE_HR
|
@@ -6005,7 +6043,7 @@ class HttpCli(object):
|
|
6005
6043
|
is_dir = stat.S_ISDIR(inf.st_mode)
|
6006
6044
|
if is_dir:
|
6007
6045
|
href += "/"
|
6008
|
-
if
|
6046
|
+
if no_zip:
|
6009
6047
|
margin = "DIR"
|
6010
6048
|
elif add_dk:
|
6011
6049
|
zs = absreal(fspath)
|
@@ -6018,7 +6056,7 @@ class HttpCli(object):
|
|
6018
6056
|
quotep(href),
|
6019
6057
|
)
|
6020
6058
|
elif fn in hist:
|
6021
|
-
margin = '<a href="%s.hist/%s">#%s</a>' % (
|
6059
|
+
margin = '<a href="%s.hist/%s" rel="nofollow">#%s</a>' % (
|
6022
6060
|
base,
|
6023
6061
|
html_escape(hist[fn][2], quot=True, crlf=True),
|
6024
6062
|
hist[fn][0],
|
@@ -6229,9 +6267,7 @@ class HttpCli(object):
|
|
6229
6267
|
docpath = os.path.join(abspath, doc)
|
6230
6268
|
sz = bos.path.getsize(docpath)
|
6231
6269
|
if sz < 1024 * self.args.txt_max:
|
6232
|
-
|
6233
|
-
doctxt = f.read().decode("utf-8", "replace")
|
6234
|
-
|
6270
|
+
doctxt = read_utf8(self.log, fsenc(docpath), False)
|
6235
6271
|
if doc.lower().endswith(".md") and "exp" in vn.flags:
|
6236
6272
|
doctxt = self._expand(doctxt, vn.flags.get("exp_md") or [])
|
6237
6273
|
else:
|
copyparty/svchub.py
CHANGED
@@ -1250,7 +1250,7 @@ class SvcHub(object):
|
|
1250
1250
|
raise
|
1251
1251
|
|
1252
1252
|
def check_mp_support(self) :
|
1253
|
-
if MACOS:
|
1253
|
+
if MACOS and not os.environ.get("PRTY_FORCE_MP"):
|
1254
1254
|
return "multiprocessing is wonky on mac osx;"
|
1255
1255
|
elif sys.version_info < (3, 3):
|
1256
1256
|
return "need python 3.3 or newer for multiprocessing;"
|
@@ -1270,7 +1270,7 @@ class SvcHub(object):
|
|
1270
1270
|
return False
|
1271
1271
|
|
1272
1272
|
try:
|
1273
|
-
if mp.cpu_count() <= 1:
|
1273
|
+
if mp.cpu_count() <= 1 and not os.environ.get("PRTY_FORCE_MP"):
|
1274
1274
|
raise Exception()
|
1275
1275
|
except:
|
1276
1276
|
self.log("svchub", "only one CPU detected; multiprocessing disabled")
|
copyparty/up2k.py
CHANGED
@@ -1112,7 +1112,7 @@ class Up2k(object):
|
|
1112
1112
|
ft = "\033[0;32m{}{:.0}"
|
1113
1113
|
ff = "\033[0;35m{}{:.0}"
|
1114
1114
|
fv = "\033[0;36m{}:\033[90m{}"
|
1115
|
-
zs = "ext_th_d html_head mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot"
|
1115
|
+
zs = "ext_th_d html_head mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
|
1116
1116
|
fx = set(zs.split())
|
1117
1117
|
fd = vf_bmap()
|
1118
1118
|
fd.update(vf_cmap())
|
@@ -3410,6 +3410,7 @@ class Up2k(object):
|
|
3410
3410
|
rm = False,
|
3411
3411
|
lmod = 0,
|
3412
3412
|
fsrc = None,
|
3413
|
+
is_mv = False,
|
3413
3414
|
) :
|
3414
3415
|
if src == dst or (fsrc and fsrc == dst):
|
3415
3416
|
t = "symlinking a file to itself?? orig(%s) fsrc(%s) link(%s)"
|
@@ -3426,7 +3427,7 @@ class Up2k(object):
|
|
3426
3427
|
|
3427
3428
|
linked = False
|
3428
3429
|
try:
|
3429
|
-
if not flags.get("dedup"):
|
3430
|
+
if not is_mv and not flags.get("dedup"):
|
3430
3431
|
raise Exception("dedup is disabled in config")
|
3431
3432
|
|
3432
3433
|
lsrc = src
|
@@ -4578,7 +4579,7 @@ class Up2k(object):
|
|
4578
4579
|
dlink = bos.readlink(sabs)
|
4579
4580
|
dlink = os.path.join(os.path.dirname(sabs), dlink)
|
4580
4581
|
dlink = bos.path.abspath(dlink)
|
4581
|
-
self._symlink(dlink, dabs, dvn.flags, lmod=ftime)
|
4582
|
+
self._symlink(dlink, dabs, dvn.flags, lmod=ftime, is_mv=True)
|
4582
4583
|
wunlink(self.log, sabs, svn.flags)
|
4583
4584
|
else:
|
4584
4585
|
atomic_move(self.log, sabs, dabs, svn.flags)
|
@@ -4796,7 +4797,7 @@ class Up2k(object):
|
|
4796
4797
|
flags = self.flags.get(ptop) or {}
|
4797
4798
|
atomic_move(self.log, sabs, slabs, flags)
|
4798
4799
|
bos.utime(slabs, (int(time.time()), int(mt)), False)
|
4799
|
-
self._symlink(slabs, sabs, flags, False)
|
4800
|
+
self._symlink(slabs, sabs, flags, False, is_mv=True)
|
4800
4801
|
full[slabs] = (ptop, rem)
|
4801
4802
|
sabs = slabs
|
4802
4803
|
|
@@ -4855,7 +4856,9 @@ class Up2k(object):
|
|
4855
4856
|
# (for example a volume with symlinked dupes but no --dedup);
|
4856
4857
|
# fsrc=sabs is then a source that currently resolves to copy
|
4857
4858
|
|
4858
|
-
self._symlink(
|
4859
|
+
self._symlink(
|
4860
|
+
dabs, alink, flags, False, lmod=lmod or 0, fsrc=sabs, is_mv=True
|
4861
|
+
)
|
4859
4862
|
|
4860
4863
|
return len(full) + len(links)
|
4861
4864
|
|
copyparty/util.py
CHANGED
@@ -572,6 +572,38 @@ except Exception as ex:
|
|
572
572
|
print("using fallback base64 codec due to %r" % (ex,))
|
573
573
|
|
574
574
|
|
575
|
+
class NotUTF8(Exception):
|
576
|
+
pass
|
577
|
+
|
578
|
+
|
579
|
+
def read_utf8(log , ap , strict ) :
|
580
|
+
with open(ap, "rb") as f:
|
581
|
+
buf = f.read()
|
582
|
+
|
583
|
+
try:
|
584
|
+
return buf.decode("utf-8", "strict")
|
585
|
+
except UnicodeDecodeError as ex:
|
586
|
+
eo = ex.start
|
587
|
+
eb = buf[eo : eo + 1]
|
588
|
+
|
589
|
+
if not strict:
|
590
|
+
t = "WARNING: The file [%s] is not using the UTF-8 character encoding; some characters in the file will be skipped/ignored. The first unreadable character was byte %r at offset %d. Please convert this file to UTF-8 by opening the file in your text-editor and saving it as UTF-8."
|
591
|
+
t = t % (ap, eb, eo)
|
592
|
+
if log:
|
593
|
+
log(t, 3)
|
594
|
+
else:
|
595
|
+
print(t)
|
596
|
+
return buf.decode("utf-8", "replace")
|
597
|
+
|
598
|
+
t = "ERROR: The file [%s] is not using the UTF-8 character encoding, and cannot be loaded. The first unreadable character was byte %r at offset %d. Please convert this file to UTF-8 by opening the file in your text-editor and saving it as UTF-8."
|
599
|
+
t = t % (ap, eb, eo)
|
600
|
+
if log:
|
601
|
+
log(t, 3)
|
602
|
+
else:
|
603
|
+
print(t)
|
604
|
+
raise NotUTF8(t)
|
605
|
+
|
606
|
+
|
575
607
|
class Daemon(threading.Thread):
|
576
608
|
def __init__(
|
577
609
|
self,
|
copyparty/web/browser.js.gz
CHANGED
Binary file
|
copyparty/web/up2k.js.gz
CHANGED
Binary file
|
copyparty/web/util.js.gz
CHANGED
Binary file
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: copyparty
|
3
|
-
Version: 1.16.
|
3
|
+
Version: 1.16.17
|
4
4
|
Summary: Portable file server with accelerated resumable uploads, deduplication, WebDAV, FTP, zeroconf, media indexer, video thumbnails, audio transcoding, and write-only folders
|
5
5
|
Author-email: ed <copyparty@ocv.me>
|
6
6
|
License: MIT
|
@@ -157,6 +157,7 @@ turn almost any device into a file server with resumable uploads/downloads using
|
|
157
157
|
* [custom mimetypes](#custom-mimetypes) - change the association of a file extension
|
158
158
|
* [GDPR compliance](#GDPR-compliance) - imagine using copyparty professionally...
|
159
159
|
* [feature chickenbits](#feature-chickenbits) - buggy feature? rip it out
|
160
|
+
* [feature beefybits](#feature-beefybits) - force-enable incompatible features
|
160
161
|
* [packages](#packages) - the party might be closer than you think
|
161
162
|
* [arch package](#arch-package) - now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
|
162
163
|
* [fedora package](#fedora-package) - does not exist yet
|
@@ -1893,7 +1894,7 @@ tell search engines you don't wanna be indexed, either using the good old [robo
|
|
1893
1894
|
* volflag `[...]:c,norobots` does the same thing for that single volume
|
1894
1895
|
* volflag `[...]:c,robots` ALLOWS search-engine crawling for that volume, even if `--no-robots` is set globally
|
1895
1896
|
|
1896
|
-
also, `--force-js` disables the plain HTML folder listing, making things harder to parse for search engines
|
1897
|
+
also, `--force-js` disables the plain HTML folder listing, making things harder to parse for *some* search engines -- note that crawlers which understand javascript (such as google) will not be affected
|
1897
1898
|
|
1898
1899
|
|
1899
1900
|
## themes
|
@@ -2194,6 +2195,15 @@ buggy feature? rip it out by setting any of the following environment variables
|
|
2194
2195
|
example: `PRTY_NO_IFADDR=1 python3 copyparty-sfx.py`
|
2195
2196
|
|
2196
2197
|
|
2198
|
+
### feature beefybits
|
2199
|
+
|
2200
|
+
force-enable features with known issues on your OS/env by setting any of the following environment variables, also affectionately known as `fuckitbits` or `hail-mary-bits`
|
2201
|
+
|
2202
|
+
| env-var | what it does |
|
2203
|
+
| ------------------------ | ------------ |
|
2204
|
+
| `PRTY_FORCE_MP` | force-enable multiprocessing (real multithreading) on MacOS and other broken platforms |
|
2205
|
+
|
2206
|
+
|
2197
2207
|
# packages
|
2198
2208
|
|
2199
2209
|
the party might be closer than you think
|
@@ -1,17 +1,17 @@
|
|
1
1
|
copyparty/__init__.py,sha256=VR6ZZhB9IxaK5TDXDTBM_OIP5ydkrdbaEnstktLM__s,2649
|
2
|
-
copyparty/__main__.py,sha256=
|
3
|
-
copyparty/__version__.py,sha256=
|
4
|
-
copyparty/authsrv.py,sha256=
|
2
|
+
copyparty/__main__.py,sha256=hlYwQuq_c1ML_Mkn4U3YSOllU-SavqtkMt72VMdi9os,117346
|
3
|
+
copyparty/__version__.py,sha256=ouQegJYN1YmTy_TtphfQsiIWHiCpzBMamgsfT-_sPxU,252
|
4
|
+
copyparty/authsrv.py,sha256=nZ27CC3HxwVbbCLPMEO9v6NZV7pLYP3euwrQ9qwT3gg,111163
|
5
5
|
copyparty/broker_mp.py,sha256=QdOXXvV2Xn6J0CysEqyY3GZbqxQMyWnTpnba-a5lMc0,4987
|
6
6
|
copyparty/broker_mpw.py,sha256=PpSS4SK3pItlpfD8OwVr3QmJEPKlUgaf2nuMOozixgU,3347
|
7
7
|
copyparty/broker_thr.py,sha256=fjoYtpSscUA7-nMl4r1n2R7UK3J9lrvLS3rUZ-iJzKQ,1721
|
8
8
|
copyparty/broker_util.py,sha256=76mfnFOpX1gUUvtjm8UQI7jpTIaVINX10QonM-B7ggc,1680
|
9
9
|
copyparty/cert.py,sha256=0ZAPeXeMR164vWn9GQU3JDKooYXEq_NOQkDeg543ivg,8009
|
10
|
-
copyparty/cfg.py,sha256=
|
10
|
+
copyparty/cfg.py,sha256=cL2_gysqgLR6kU_sqaq6XheED0jbbTcqBFGEnYj_pA4,13996
|
11
11
|
copyparty/dxml.py,sha256=vu5uZQtwvwoqnFHbULs2Zh_y2DETu0T-ENpMZ1i2CV4,2505
|
12
|
-
copyparty/fsutil.py,sha256=
|
12
|
+
copyparty/fsutil.py,sha256=NC_CJC4TDag399vVDH9_uQfdfpTMwRFLNxERSWhlVvs,4594
|
13
13
|
copyparty/ftpd.py,sha256=T97SFS7JFtvRLbJX8C4fJSYwe13vhN3-E6emtlVmqLA,17608
|
14
|
-
copyparty/httpcli.py,sha256=
|
14
|
+
copyparty/httpcli.py,sha256=O7iCliCPY57KzD_QI64cu7kS8UQIv1fo4BpmXo783kg,220444
|
15
15
|
copyparty/httpconn.py,sha256=mQSgljh0Q-jyWjF4tQLrHbRKRe9WKl19kGqsGMsJpWo,6880
|
16
16
|
copyparty/httpsrv.py,sha256=pxH_Eh8ElBLvOEDejgpP9Bvk65HNEou-03aYIcgXhrs,18090
|
17
17
|
copyparty/ico.py,sha256=eWSxEae4wOCfheHl-m-wchYvFRAR_97kJDb4NGaB-Z8,3561
|
@@ -24,15 +24,15 @@ copyparty/smbd.py,sha256=dixFl2wlWymq_Cycc8a4cVB4gY8RSg2e3tE7Xr-aDa0,14614
|
|
24
24
|
copyparty/ssdp.py,sha256=R1Z61GZOxBMF2Sk4RTxKWMOemogmcjEWG-CvLihd45k,7023
|
25
25
|
copyparty/star.py,sha256=tV5BbX6AiQ7N4UU8DYtSTckNYeoeey4DBqq4LjfymbY,3818
|
26
26
|
copyparty/sutil.py,sha256=6zEEGl4hRe6bTB83Y_RtnBGxr2JcUa__GdiAMqNJZnY,3208
|
27
|
-
copyparty/svchub.py,sha256=
|
27
|
+
copyparty/svchub.py,sha256=EPkbchicmP5Acq64rhpIshL-zvGEltcRJelkzW_dx5s,41542
|
28
28
|
copyparty/szip.py,sha256=HFtnwOiBgx0HMLUf-h_T84zSlRijPxmhRo5PM613kRA,8602
|
29
29
|
copyparty/tcpsrv.py,sha256=2q18dGR8jnezA4SMfUXa-wrGRGX3nHIwkxkWvkTzF2A,19889
|
30
30
|
copyparty/tftpd.py,sha256=PXgG4rTmiaU_TavSyZWD5cFphdfChs9YvNY21qfExt8,13611
|
31
31
|
copyparty/th_cli.py,sha256=PxDAmUvO_8Vm5edXiWtsCft0Fw69QL9rCHf9zLmUNeA,4800
|
32
32
|
copyparty/th_srv.py,sha256=tHbh_Ve3v8tYclWH2thLs5oFufeXgJi1duUMveKIx9k,30725
|
33
33
|
copyparty/u2idx.py,sha256=G6MDbD4I_sJSOwaNFZ6XLTQhnEDrB12pVKuKhzQ_leE,13676
|
34
|
-
copyparty/up2k.py,sha256=
|
35
|
-
copyparty/util.py,sha256=
|
34
|
+
copyparty/up2k.py,sha256=abULiz0KK3dcuw9hsapJBnueaqrCWry6rQe8VgsJITM,177330
|
35
|
+
copyparty/util.py,sha256=QOl_gNH8eHYNrnvoNPpGgkGAq8vV-r_Kr5Tp30qZo-M,100520
|
36
36
|
copyparty/bos/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
37
37
|
copyparty/bos/bos.py,sha256=Wb7eWsXJgR5AFlBR9ZOyKrLTwy-Kct9RrGiOu4Jo37Y,1622
|
38
38
|
copyparty/bos/path.py,sha256=yEjCq2ki9CvxA5sCT8pS0keEXwugs0ZeUyUhdBziOCI,777
|
@@ -57,7 +57,7 @@ copyparty/stolen/ifaddr/_win32.py,sha256=EE-QyoBgeB7lYQ6z62VjXNaRozaYfCkaJBHGNA8
|
|
57
57
|
copyparty/web/baguettebox.js.gz,sha256=r2c_hOZV_RTyl4CqWWX14FDWP8nnDVwGkDl4Sfk0rU4,8239
|
58
58
|
copyparty/web/browser.css.gz,sha256=_HiFW5vPUusWadoqdY8ZihuWizY9UECAc5nIamBPRi4,11654
|
59
59
|
copyparty/web/browser.html,sha256=auvhLVE_t0aIN0q-nk0zOWFqITgDhroMAAviBNLoFfc,4788
|
60
|
-
copyparty/web/browser.js.gz,sha256=
|
60
|
+
copyparty/web/browser.js.gz,sha256=48fDqIqQKCkrH0VsYVj03sDOx9gEZ-DfiHDaqEUuyr0,92341
|
61
61
|
copyparty/web/browser2.html,sha256=NRUZ08GH-e2YcGXcoz0UjYg6JIVF42u4IMX4HHwWTmg,1587
|
62
62
|
copyparty/web/cf.html,sha256=lJThtNFNAQT1ClCHHlivAkDGE0LutedwopXD62Z8Nys,589
|
63
63
|
copyparty/web/dbg-audio.js.gz,sha256=Ma-KZtK8LnmiwNvNKFKXMPYl_Nn_3U7GsJ6-DRWC2HE,688
|
@@ -83,8 +83,8 @@ copyparty/web/splash.js.gz,sha256=4VqNznN10-bT33IJm3VWzBEJ1s08XZyxFB1TYPUkuAo,27
|
|
83
83
|
copyparty/web/svcs.html,sha256=dnE1fG15zOpq7u0GYt8ij6BUv_LTwsiipFeneVYlMsM,14140
|
84
84
|
copyparty/web/svcs.js.gz,sha256=lMXEP9W-VlXyANlva4q0ASSxvvHYlE2CrmxGgZXZop0,713
|
85
85
|
copyparty/web/ui.css.gz,sha256=0sHIwGsL3_xH8Uu6N0Ag3bKBTjf-e_yfFbKynEZXAnk,2800
|
86
|
-
copyparty/web/up2k.js.gz,sha256=
|
87
|
-
copyparty/web/util.js.gz,sha256=
|
86
|
+
copyparty/web/up2k.js.gz,sha256=VQVWBXK2gEz1b8if_ujXHNHnfBO7cdrKoSjqX397VUI,24519
|
87
|
+
copyparty/web/util.js.gz,sha256=rD9iLfVLKRhxC8hmakal-s18xN_rs6GuOqyRPii6HQ8,15110
|
88
88
|
copyparty/web/w.hash.js.gz,sha256=JhJagnqIkcKng_hs6otEgzcuQE7keToG_r5dd2o3EfU,1108
|
89
89
|
copyparty/web/a/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
90
90
|
copyparty/web/a/partyfuse.py,sha256=9p5Hpg_IBiSimv7j9kmPhCGpy-FLXSRUOYnLjJ5JifU,28049
|
@@ -109,9 +109,9 @@ copyparty/web/deps/prismd.css.gz,sha256=ObUlksQVr-OuYlTz-I4B23TeBg2QDVVGRnWBz8cV
|
|
109
109
|
copyparty/web/deps/scp.woff2,sha256=w99BDU5i8MukkMEL-iW0YO9H4vFFZSPWxbkH70ytaAg,8612
|
110
110
|
copyparty/web/deps/sha512.ac.js.gz,sha256=lFZaCLumgWxrvEuDr4bqdKHsqjX82AbVAb7_F45Yk88,7033
|
111
111
|
copyparty/web/deps/sha512.hw.js.gz,sha256=UAed2_ocklZCnIzcSYz2h4P1ycztlCLj-ewsRTud2lU,7939
|
112
|
-
copyparty-1.16.
|
113
|
-
copyparty-1.16.
|
114
|
-
copyparty-1.16.
|
115
|
-
copyparty-1.16.
|
116
|
-
copyparty-1.16.
|
117
|
-
copyparty-1.16.
|
112
|
+
copyparty-1.16.17.dist-info/LICENSE,sha256=gOr4h33pCsBEg9uIy9AYmb7qlocL4V9t2uPJS5wllr0,1072
|
113
|
+
copyparty-1.16.17.dist-info/METADATA,sha256=mv4zOlv-ElE_ldsed5zneXRYtUjsFF5L9vCh7cTSi7Q,158632
|
114
|
+
copyparty-1.16.17.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
115
|
+
copyparty-1.16.17.dist-info/entry_points.txt,sha256=4zw6a3rqASywQomiYLObjjlxybaI65LYYOTJwgKz7b0,128
|
116
|
+
copyparty-1.16.17.dist-info/top_level.txt,sha256=LnYUPsDyk-8kFgM6YJLG4h820DQekn81cObKSu9g-sI,10
|
117
|
+
copyparty-1.16.17.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|