copyparty 1.17.1__py3-none-any.whl → 1.18.0__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 -0
- copyparty/__version__.py +3 -3
- copyparty/authsrv.py +49 -19
- copyparty/cfg.py +14 -0
- copyparty/httpcli.py +189 -17
- 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/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/web/w.hash.js.gz +0 -0
- {copyparty-1.17.1.dist-info → copyparty-1.18.0.dist-info}/METADATA +37 -4
- {copyparty-1.17.1.dist-info → copyparty-1.18.0.dist-info}/RECORD +20 -20
- {copyparty-1.17.1.dist-info → copyparty-1.18.0.dist-info}/WHEEL +1 -1
- {copyparty-1.17.1.dist-info → copyparty-1.18.0.dist-info}/entry_points.txt +0 -0
- {copyparty-1.17.1.dist-info → copyparty-1.18.0.dist-info}/licenses/LICENSE +0 -0
- {copyparty-1.17.1.dist-info → copyparty-1.18.0.dist-info}/top_level.txt +0 -0
copyparty/__main__.py
CHANGED
@@ -956,6 +956,7 @@ def add_general(ap, nc, srvname):
|
|
956
956
|
ap2.add_argument("--name", metavar="TXT", type=u, default=srvname, help="server name (displayed topleft in browser and in mDNS)")
|
957
957
|
ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]")
|
958
958
|
ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit")
|
959
|
+
ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)")
|
959
960
|
ap2.add_argument("--license", action="store_true", help="show licenses and exit")
|
960
961
|
ap2.add_argument("--version", action="store_true", help="show versions and exit")
|
961
962
|
|
@@ -1020,6 +1021,7 @@ def add_upload(ap):
|
|
1020
1021
|
ap2.add_argument("--df", metavar="GiB", type=u, default="0", help="ensure \033[33mGiB\033[0m free disk space by rejecting upload requests; assumes gigabytes unless a unit suffix is given: [\033[32m256m\033[0m], [\033[32m4\033[0m], [\033[32m2T\033[0m] (volflag=df)")
|
1021
1022
|
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
|
1022
1023
|
ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; [\033[32m-1\033[0m] = forbidden/always-off, [\033[32m0\033[0m] = default-off and warn if enabled, [\033[32m1\033[0m] = default-off, [\033[32m2\033[0m] = on, [\033[32m3\033[0m] = on and disable datecheck")
|
1024
|
+
ap2.add_argument("--nosubtle", metavar="N", type=int, default=0, help="when to use a wasm-hasher instead of the browser's builtin; faster on chrome, but buggy in older chrome versions. [\033[32m0\033[0m] = only when necessary (non-https), [\033[32m1\033[0m] = always (all browsers), [\033[32m2\033[0m] = always on chrome/firefox, [\033[32m3\033[0m] = always on chrome, [\033[32mN\033[0m] = chrome-version N and newer (recommendation: 137)")
|
1023
1025
|
ap2.add_argument("--u2j", metavar="JOBS", type=int, default=2, help="web-client: number of file chunks to upload in parallel; 1 or 2 is good when latency is low (same-country), 2~4 for android-clients, 2~6 for cross-atlantic. Max is 6 in most browsers. Big values increase network-speed but may reduce HDD-speed")
|
1024
1026
|
ap2.add_argument("--u2sz", metavar="N,N,N", type=u, default="1,64,96", help="web-client: default upload chunksize (MiB); sets \033[33mmin,default,max\033[0m in the settings gui. Each HTTP POST will aim for \033[33mdefault\033[0m, and never exceed \033[33mmax\033[0m. Cloudflare max is 96. Big values are good for cross-atlantic but may increase HDD fragmentation on some FS. Disable this optimization with [\033[32m1,1,1\033[0m]")
|
1025
1027
|
ap2.add_argument("--u2ow", metavar="NUM", type=int, default=0, help="web-client: default setting for when to replace/overwrite existing files; [\033[32m0\033[0m]=never, [\033[32m1\033[0m]=if-client-newer, [\033[32m2\033[0m]=always (volflag=u2ow)")
|
@@ -1261,6 +1263,7 @@ def add_optouts(ap):
|
|
1261
1263
|
ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)")
|
1262
1264
|
ap2.add_argument("--no-lifetime", action="store_true", help="do not allow clients (or server config) to schedule an upload to be deleted after a given time")
|
1263
1265
|
ap2.add_argument("--no-pipe", action="store_true", help="disable race-the-beam (lockstep download of files which are currently being uploaded) (volflag=nopipe)")
|
1266
|
+
ap2.add_argument("--no-tail", action="store_true", help="disable streaming a growing files with ?tail (volflag=notail)")
|
1264
1267
|
ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader-IP into the database; will also disable unpost, you may want \033[32m--forget-ip\033[0m instead (volflag=no_db_ip)")
|
1265
1268
|
|
1266
1269
|
|
@@ -1390,6 +1393,16 @@ def add_transcoding(ap):
|
|
1390
1393
|
ap2.add_argument("--ac-maxage", metavar="SEC", type=int, default=86400, help="delete cached transcode output after \033[33mSEC\033[0m seconds")
|
1391
1394
|
|
1392
1395
|
|
1396
|
+
def add_tail(ap):
|
1397
|
+
ap2 = ap.add_argument_group('tailing options (realtime streaming of a growing file)')
|
1398
|
+
ap2.add_argument("--tail-who", metavar="LVL", type=int, default=2, help="who can tail? [\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=tail_who)")
|
1399
|
+
ap2.add_argument("--tail-cmax", metavar="N", type=int, default=64, help="do not allow starting a new tail if more than \033[33mN\033[0m active downloads")
|
1400
|
+
ap2.add_argument("--tail-tmax", metavar="SEC", type=float, default=0, help="terminate connection after \033[33mSEC\033[0m seconds; [\033[32m0\033[0m]=never (volflag=tail_tmax)")
|
1401
|
+
ap2.add_argument("--tail-rate", metavar="SEC", type=float, default=0.2, help="check for new data every \033[33mSEC\033[0m seconds (volflag=tail_rate)")
|
1402
|
+
ap2.add_argument("--tail-ka", metavar="SEC", type=float, default=3.0, help="send a zerobyte if connection is idle for \033[33mSEC\033[0m seconds to prevent disconnect")
|
1403
|
+
ap2.add_argument("--tail-fd", metavar="SEC", type=float, default=1.0, help="check if file was replaced (new fd) if idle for \033[33mSEC\033[0m seconds (volflag=tail_fd)")
|
1404
|
+
|
1405
|
+
|
1393
1406
|
def add_rss(ap):
|
1394
1407
|
ap2 = ap.add_argument_group('RSS options')
|
1395
1408
|
ap2.add_argument("--rss", action="store_true", help="enable RSS output (experimental) (volflag=rss)")
|
@@ -1485,6 +1498,7 @@ def add_ui(ap, retry):
|
|
1485
1498
|
ap2.add_argument("--sort", metavar="C,C,C", type=u, default="href", help="default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: \033[32mhref -href ext sz ts tags/Album tags/.tn\033[0m (volflag=sort)")
|
1486
1499
|
ap2.add_argument("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)")
|
1487
1500
|
ap2.add_argument("--hsortn", metavar="N", type=int, default=2, help="number of sorting rules to include in media URLs by default (volflag=hsortn)")
|
1501
|
+
ap2.add_argument("--see-dots", action="store_true", help="default-enable seeing dotfiles; only takes effect if user has the necessary permissions")
|
1488
1502
|
ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files matching \033[33mREGEX\033[0m in file list. Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)")
|
1489
1503
|
ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable")
|
1490
1504
|
ap2.add_argument("--ext-th", metavar="E=VP", type=u, action="append", help="use thumbnail-image \033[33mVP\033[0m for file-extension \033[33mE\033[0m, example: [\033[32mexe=/.res/exe.png\033[0m] (volflag=ext_th)")
|
@@ -1594,6 +1608,7 @@ def run_argparse(
|
|
1594
1608
|
add_hooks(ap)
|
1595
1609
|
add_stats(ap)
|
1596
1610
|
add_txt(ap)
|
1611
|
+
add_tail(ap)
|
1597
1612
|
add_og(ap)
|
1598
1613
|
add_ui(ap, retry)
|
1599
1614
|
add_admin(ap)
|
copyparty/__version__.py
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
|
3
|
-
VERSION = (1,
|
4
|
-
CODENAME = "
|
5
|
-
BUILD_DT = (2025,
|
3
|
+
VERSION = (1, 18, 0)
|
4
|
+
CODENAME = "logtail"
|
5
|
+
BUILD_DT = (2025, 6, 22)
|
6
6
|
|
7
7
|
S_VERSION = ".".join(map(str, VERSION))
|
8
8
|
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
copyparty/authsrv.py
CHANGED
@@ -65,7 +65,9 @@ SSEELOG = " ({})".format(SEE_LOG)
|
|
65
65
|
BAD_CFG = "invalid config; {}".format(SEE_LOG)
|
66
66
|
SBADCFG = " ({})".format(BAD_CFG)
|
67
67
|
|
68
|
-
PTN_U_GRP = re.compile(r"\$\{u%
|
68
|
+
PTN_U_GRP = re.compile(r"\$\{u(%[+-][^}]+)\}")
|
69
|
+
PTN_G_GRP = re.compile(r"\$\{g(%[+-][^}]+)\}")
|
70
|
+
PTN_SIGIL = re.compile(r"(\${[ug][}%])")
|
69
71
|
|
70
72
|
|
71
73
|
class CfgEx(Exception):
|
@@ -350,7 +352,6 @@ class VFS(object):
|
|
350
352
|
self.flags = flags # config options
|
351
353
|
self.root = self
|
352
354
|
self.dev = 0 # st_dev
|
353
|
-
self.badcfg1 = False
|
354
355
|
self.nodes = {} # child nodes
|
355
356
|
self.histtab = {} # all realpath->histpath
|
356
357
|
self.dbpaths = {} # all realpath->dbpath
|
@@ -870,6 +871,7 @@ class AuthSrv(object):
|
|
870
871
|
self.warn_anonwrite = warn_anonwrite
|
871
872
|
self.line_ctr = 0
|
872
873
|
self.indent = ""
|
874
|
+
self.is_lxc = args.c == ["/z/initcfg"]
|
873
875
|
|
874
876
|
# fwd-decl
|
875
877
|
self.vfs = VFS(log_func, "", "", "", AXS(), {})
|
@@ -880,6 +882,8 @@ class AuthSrv(object):
|
|
880
882
|
self.defpw = {}
|
881
883
|
self.grps = {}
|
882
884
|
self.re_pwd = None
|
885
|
+
self.cfg_files_loaded = []
|
886
|
+
self.badcfg1 = False
|
883
887
|
|
884
888
|
# all volumes observed since last restart
|
885
889
|
self.idp_vols = {} # vpath->abspath
|
@@ -956,15 +960,27 @@ class AuthSrv(object):
|
|
956
960
|
un_gn = [("", "")]
|
957
961
|
|
958
962
|
for un, gn in un_gn:
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
if req == "+":
|
964
|
-
if not hit:
|
965
|
-
continue
|
966
|
-
elif hit:
|
963
|
+
rejected = False
|
964
|
+
for ptn in [PTN_U_GRP, PTN_G_GRP]:
|
965
|
+
m = ptn.search(dst0)
|
966
|
+
if not m:
|
967
967
|
continue
|
968
|
+
zs = m.group(1)
|
969
|
+
zs = zs.replace(",%+", "\n%+")
|
970
|
+
zs = zs.replace(",%-", "\n%-")
|
971
|
+
for rule in zs.split("\n"):
|
972
|
+
gnc = rule[2:]
|
973
|
+
if ptn == PTN_U_GRP:
|
974
|
+
# is user member of group?
|
975
|
+
hit = gnc in (un_gns.get(un) or [])
|
976
|
+
else:
|
977
|
+
# is it this specific group?
|
978
|
+
hit = gn == gnc
|
979
|
+
|
980
|
+
if rule.startswith("%+") != hit:
|
981
|
+
rejected = True
|
982
|
+
if rejected:
|
983
|
+
continue
|
968
984
|
|
969
985
|
# if ap/vp has a user/group placeholder, make sure to keep
|
970
986
|
# track so the same user/group is mapped when setting perms;
|
@@ -979,6 +995,8 @@ class AuthSrv(object):
|
|
979
995
|
|
980
996
|
src = src1.replace("${g}", gn or "\n")
|
981
997
|
dst = dst1.replace("${g}", gn or "\n")
|
998
|
+
src = PTN_G_GRP.sub(gn or "\n", src)
|
999
|
+
dst = PTN_G_GRP.sub(gn or "\n", dst)
|
982
1000
|
if src == src1 and dst == dst1:
|
983
1001
|
gn = ""
|
984
1002
|
|
@@ -1475,8 +1493,10 @@ class AuthSrv(object):
|
|
1475
1493
|
daxs = {}
|
1476
1494
|
mflags = {} # vpath:flags
|
1477
1495
|
mount = {} # dst:src (vp:(ap,vp0))
|
1496
|
+
cfg_files_loaded = []
|
1478
1497
|
|
1479
1498
|
self.idp_vols = {} # yolo
|
1499
|
+
self.badcfg1 = False
|
1480
1500
|
|
1481
1501
|
if self.args.a:
|
1482
1502
|
# list of username:password
|
@@ -1537,6 +1557,7 @@ class AuthSrv(object):
|
|
1537
1557
|
zst = [(max(0, len(x) - 2) * " ") + "└" + x[-1] for x in zstt]
|
1538
1558
|
t = "loaded {} config files:\n{}"
|
1539
1559
|
self.log(t.format(len(zst), "\n".join(zst)))
|
1560
|
+
cfg_files_loaded = zst
|
1540
1561
|
|
1541
1562
|
except:
|
1542
1563
|
lns = lns[: self.line_ctr]
|
@@ -1561,9 +1582,14 @@ class AuthSrv(object):
|
|
1561
1582
|
if not mount and not self.args.idp_h_usr:
|
1562
1583
|
# -h says our defaults are CWD at root and read/write for everyone
|
1563
1584
|
axs = AXS(["*"], ["*"], None, None)
|
1564
|
-
if
|
1565
|
-
t = "Read-access has been disabled due to failsafe: Docker detected, but
|
1566
|
-
|
1585
|
+
if self.is_lxc:
|
1586
|
+
t = "Read-access has been disabled due to failsafe: Docker detected, but %s. 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 all of /w/ by adding the following arguments to the docker container: -v .::rw"
|
1587
|
+
if len(cfg_files_loaded) == 1:
|
1588
|
+
self.log(t % ("no config-file was provided",), 1)
|
1589
|
+
t = "it is strongly recommended to add a config-file instead, for example based on https://github.com/9001/copyparty/blob/hovudstraum/docs/examples/docker/basic-docker-compose/copyparty.conf"
|
1590
|
+
self.log(t, 3)
|
1591
|
+
else:
|
1592
|
+
self.log(t % ("the config does not define any volumes",), 1)
|
1567
1593
|
axs = AXS()
|
1568
1594
|
elif self.args.c:
|
1569
1595
|
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"
|
@@ -1571,7 +1597,7 @@ class AuthSrv(object):
|
|
1571
1597
|
axs = AXS()
|
1572
1598
|
vfs = VFS(self.log_func, absreal("."), "", "", axs, {})
|
1573
1599
|
if not axs.uread:
|
1574
|
-
|
1600
|
+
self.badcfg1 = True
|
1575
1601
|
elif "" not in mount:
|
1576
1602
|
# there's volumes but no root; make root inaccessible
|
1577
1603
|
zsd = {"d2d": True, "tcolor": self.args.tcolor}
|
@@ -1845,7 +1871,7 @@ class AuthSrv(object):
|
|
1845
1871
|
is_shr = shr and zv.vpath.split("/")[0] == shr
|
1846
1872
|
if histp and not is_shr and histp in rhisttab:
|
1847
1873
|
zv2 = rhisttab[histp]
|
1848
|
-
t = "invalid config; multiple volumes share the same histpath (database+thumbnails location):\n histpath: %s\n volume 1: /%s [%s]\n volume 2:
|
1874
|
+
t = "invalid config; multiple volumes share the same histpath (database+thumbnails location):\n histpath: %s\n volume 1: /%s [%s]\n volume 2: /%s [%s]"
|
1849
1875
|
t = t % (histp, zv2.vpath, zv2.realpath, zv.vpath, zv.realpath)
|
1850
1876
|
self.log(t, 1)
|
1851
1877
|
raise Exception(t)
|
@@ -1859,7 +1885,7 @@ class AuthSrv(object):
|
|
1859
1885
|
is_shr = shr and zv.vpath.split("/")[0] == shr
|
1860
1886
|
if dbp and not is_shr and dbp in rdbpaths:
|
1861
1887
|
zv2 = rdbpaths[dbp]
|
1862
|
-
t = "invalid config; multiple volumes share the same dbpath (database location):\n dbpath: %s\n volume 1: /%s [%s]\n volume 2:
|
1888
|
+
t = "invalid config; multiple volumes share the same dbpath (database location):\n dbpath: %s\n volume 1: /%s [%s]\n volume 2: /%s [%s]"
|
1863
1889
|
t = t % (dbp, zv2.vpath, zv2.realpath, zv.vpath, zv.realpath)
|
1864
1890
|
self.log(t, 1)
|
1865
1891
|
raise Exception(t)
|
@@ -2042,12 +2068,13 @@ class AuthSrv(object):
|
|
2042
2068
|
if vf not in vol.flags:
|
2043
2069
|
vol.flags[vf] = getattr(self.args, ga)
|
2044
2070
|
|
2045
|
-
zs = "forget_ip nrand u2abort u2ow ups_who zip_who"
|
2071
|
+
zs = "forget_ip nrand tail_who u2abort u2ow ups_who zip_who"
|
2046
2072
|
for k in zs.split():
|
2047
2073
|
if k in vol.flags:
|
2048
2074
|
vol.flags[k] = int(vol.flags[k])
|
2049
2075
|
|
2050
|
-
|
2076
|
+
zs = "convt tail_fd tail_rate tail_tmax"
|
2077
|
+
for k in zs.split():
|
2051
2078
|
if k in vol.flags:
|
2052
2079
|
vol.flags[k] = float(vol.flags[k])
|
2053
2080
|
|
@@ -2376,7 +2403,7 @@ class AuthSrv(object):
|
|
2376
2403
|
idp_vn, _ = vfs.get(idp_vp, "*", False, False)
|
2377
2404
|
idp_vp0 = idp_vn.vpath0
|
2378
2405
|
|
2379
|
-
sigils = set(
|
2406
|
+
sigils = set(PTN_SIGIL.findall(idp_vp0))
|
2380
2407
|
if len(sigils) > 1:
|
2381
2408
|
t = '\nWARNING: IdP-volume "/%s" created by "/%s" has multiple IdP placeholders: %s'
|
2382
2409
|
self.idp_warn.append(t % (idp_vp, idp_vp0, list(sigils)))
|
@@ -2426,6 +2453,7 @@ class AuthSrv(object):
|
|
2426
2453
|
self.defpw = defpw
|
2427
2454
|
self.grps = grps
|
2428
2455
|
self.iacct = {v: k for k, v in acct.items()}
|
2456
|
+
self.cfg_files_loaded = cfg_files_loaded
|
2429
2457
|
|
2430
2458
|
self.load_sessions()
|
2431
2459
|
|
@@ -2545,6 +2573,7 @@ class AuthSrv(object):
|
|
2545
2573
|
"txt_ext": self.args.textfiles.replace(",", " "),
|
2546
2574
|
"def_hcols": list(vf.get("mth") or []),
|
2547
2575
|
"unlist0": vf.get("unlist") or "",
|
2576
|
+
"see_dots": self.args.see_dots,
|
2548
2577
|
"dgrid": "grid" in vf,
|
2549
2578
|
"dgsel": "gsel" in vf,
|
2550
2579
|
"dnsort": "nsort" in vf,
|
@@ -2556,6 +2585,7 @@ class AuthSrv(object):
|
|
2556
2585
|
"idxh": int(self.args.ih),
|
2557
2586
|
"themes": self.args.themes,
|
2558
2587
|
"turbolvl": self.args.turbo,
|
2588
|
+
"nosubtle": self.args.nosubtle,
|
2559
2589
|
"u2j": self.args.u2j,
|
2560
2590
|
"u2sz": self.args.u2sz,
|
2561
2591
|
"u2ts": vf["u2ts"],
|
copyparty/cfg.py
CHANGED
@@ -22,6 +22,7 @@ def vf_bmap() :
|
|
22
22
|
"no_forget": "noforget",
|
23
23
|
"no_pipe": "nopipe",
|
24
24
|
"no_robots": "norobots",
|
25
|
+
"no_tail": "notail",
|
25
26
|
"no_thumb": "dthumb",
|
26
27
|
"no_vthumb": "dvthumb",
|
27
28
|
"no_athumb": "dathumb",
|
@@ -51,6 +52,7 @@ def vf_bmap() :
|
|
51
52
|
"og_no_head",
|
52
53
|
"og_s_title",
|
53
54
|
"rand",
|
55
|
+
"rmagic",
|
54
56
|
"rss",
|
55
57
|
"wo_up_readme",
|
56
58
|
"xdev",
|
@@ -101,6 +103,10 @@ def vf_vmap() :
|
|
101
103
|
"mv_retry",
|
102
104
|
"rm_retry",
|
103
105
|
"sort",
|
106
|
+
"tail_fd",
|
107
|
+
"tail_rate",
|
108
|
+
"tail_tmax",
|
109
|
+
"tail_who",
|
104
110
|
"tcolor",
|
105
111
|
"unlist",
|
106
112
|
"u2abort",
|
@@ -304,6 +310,13 @@ flagcats = {
|
|
304
310
|
"exp_md": "placeholders to expand in markdown files; see --help",
|
305
311
|
"exp_lg": "placeholders to expand in prologue/epilogue; see --help",
|
306
312
|
},
|
313
|
+
"tailing": {
|
314
|
+
"notail": "disable ?tail (download a growing file continuously)",
|
315
|
+
"tail_fd=1": "check if file was replaced (new fd) every 1 sec",
|
316
|
+
"tail_rate=0.2": "check for new data every 0.2 sec",
|
317
|
+
"tail_tmax=30": "kill connection after 30 sec",
|
318
|
+
"tail_who=2": "restrict ?tail access (1=admins,2=authed,3=everyone)",
|
319
|
+
},
|
307
320
|
"others": {
|
308
321
|
"dots": "allow all users with read-access to\nenable the option to show dotfiles in listings",
|
309
322
|
"fk=8": 'generates per-file accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes',
|
@@ -312,6 +325,7 @@ flagcats = {
|
|
312
325
|
"dks": "per-directory accesskeys allow browsing into subdirs",
|
313
326
|
"dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so',
|
314
327
|
"rss": "allow '?rss' URL suffix (experimental)",
|
328
|
+
"rmagic": "expensive analysis for mimetype accuracy",
|
315
329
|
"ups_who=2": "restrict viewing the list of recent uploads",
|
316
330
|
"zip_who=2": "restrict access to download-as-zip/tar",
|
317
331
|
"zipmaxn=9k": "reject download-as-zip if more than 9000 files",
|
copyparty/httpcli.py
CHANGED
@@ -184,11 +184,11 @@ class HttpCli(object):
|
|
184
184
|
self.log_src = conn.log_src # mypy404
|
185
185
|
self.gen_fk = self._gen_fk if self.args.log_fk else gen_filekey
|
186
186
|
self.tls = hasattr(self.s, "cipher")
|
187
|
+
self.is_vproxied = bool(self.args.R)
|
187
188
|
|
188
189
|
# placeholders; assigned by run()
|
189
190
|
self.keepalive = False
|
190
191
|
self.is_https = False
|
191
|
-
self.is_vproxied = False
|
192
192
|
self.in_hdr_recv = True
|
193
193
|
self.headers = {}
|
194
194
|
self.mode = " " # http verb
|
@@ -396,7 +396,6 @@ class HttpCli(object):
|
|
396
396
|
self.bad_xff = True
|
397
397
|
else:
|
398
398
|
self.ip = cli_ip
|
399
|
-
self.is_vproxied = bool(self.args.R)
|
400
399
|
self.log_src = self.conn.set_rproxy(self.ip)
|
401
400
|
self.host = self.headers.get("x-forwarded-host") or self.host
|
402
401
|
trusted_xff = True
|
@@ -529,6 +528,7 @@ class HttpCli(object):
|
|
529
528
|
else:
|
530
529
|
t = "incorrect --rp-loc or webserver config; expected vpath starting with %r but got %r"
|
531
530
|
self.log(t % (self.args.R, vpath), 1)
|
531
|
+
self.is_vproxied = False
|
532
532
|
|
533
533
|
self.ouparam = uparam.copy()
|
534
534
|
|
@@ -1228,10 +1228,19 @@ class HttpCli(object):
|
|
1228
1228
|
else:
|
1229
1229
|
return self.tx_404(True)
|
1230
1230
|
else:
|
1231
|
-
|
1232
|
-
|
1233
|
-
|
1234
|
-
|
1231
|
+
if (
|
1232
|
+
self.asrv.badcfg1
|
1233
|
+
and "h" not in self.ouparam
|
1234
|
+
and "hc" not in self.ouparam
|
1235
|
+
):
|
1236
|
+
zs1 = "copyparty refused to start due to a failsafe: invalid server config; check server log"
|
1237
|
+
zs2 = 'you may <a href="/?h">access the controlpanel</a> but nothing will work until you shutdown the copyparty container and %s config-file (or provide the configuration as command-line arguments)'
|
1238
|
+
if self.asrv.is_lxc and len(self.asrv.cfg_files_loaded) == 1:
|
1239
|
+
zs2 = zs2 % ("add a",)
|
1240
|
+
else:
|
1241
|
+
zs2 = zs2 % ("fix the",)
|
1242
|
+
|
1243
|
+
html = self.j2s("msg", h1=zs1, h2=zs2)
|
1235
1244
|
self.reply(html.encode("utf-8", "replace"), 500)
|
1236
1245
|
return True
|
1237
1246
|
|
@@ -1398,7 +1407,13 @@ class HttpCli(object):
|
|
1398
1407
|
except:
|
1399
1408
|
pass
|
1400
1409
|
|
1410
|
+
ap = ""
|
1411
|
+
use_magic = "rmagic" in self.vn.flags
|
1412
|
+
|
1401
1413
|
for i in hits:
|
1414
|
+
if use_magic:
|
1415
|
+
ap = os.path.join(self.vn.realpath, i["rp"])
|
1416
|
+
|
1402
1417
|
iurl = html_escape("%s%s" % (baseurl, i["rp"]), True, True)
|
1403
1418
|
title = unquotep(i["rp"].split("?")[0].split("/")[-1])
|
1404
1419
|
title = html_escape(title, True, True)
|
@@ -1406,7 +1421,7 @@ class HttpCli(object):
|
|
1406
1421
|
tag_a = str(i["tags"].get("artist") or "")
|
1407
1422
|
desc = "%s - %s" % (tag_a, tag_t) if tag_t and tag_a else (tag_t or tag_a)
|
1408
1423
|
desc = html_escape(desc, True, True) if desc else title
|
1409
|
-
mime = html_escape(guess_mime(title))
|
1424
|
+
mime = html_escape(guess_mime(title, ap))
|
1410
1425
|
lmod = formatdate(max(0, i["ts"]))
|
1411
1426
|
zsa = (iurl, iurl, title, desc, lmod, iurl, mime, i["sz"])
|
1412
1427
|
zs = (
|
@@ -1559,6 +1574,9 @@ class HttpCli(object):
|
|
1559
1574
|
None, 207, "text/xml; charset=" + enc, {"Transfer-Encoding": "chunked"}
|
1560
1575
|
)
|
1561
1576
|
|
1577
|
+
ap = ""
|
1578
|
+
use_magic = "rmagic" in vn.flags
|
1579
|
+
|
1562
1580
|
ret = '<?xml version="1.0" encoding="{}"?>\n<D:multistatus xmlns:D="DAV:">'
|
1563
1581
|
ret = ret.format(uenc)
|
1564
1582
|
for x in fgen:
|
@@ -1585,7 +1603,9 @@ class HttpCli(object):
|
|
1585
1603
|
"supportedlock": '<D:lockentry xmlns:D="DAV:"><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockentry>',
|
1586
1604
|
}
|
1587
1605
|
if not isdir:
|
1588
|
-
|
1606
|
+
if use_magic:
|
1607
|
+
ap = os.path.join(tap, x["vp"])
|
1608
|
+
pvs["getcontenttype"] = html_escape(guess_mime(rp, ap))
|
1589
1609
|
pvs["getcontentlength"] = str(st.st_size)
|
1590
1610
|
|
1591
1611
|
for k, v in pvs.items():
|
@@ -2576,10 +2596,6 @@ class HttpCli(object):
|
|
2576
2596
|
x = self.conn.hsrv.broker.ask("up2k.handle_json", body, self.u2fh.aps)
|
2577
2597
|
ret = x.get()
|
2578
2598
|
|
2579
|
-
if self.is_vproxied:
|
2580
|
-
if "purl" in ret:
|
2581
|
-
ret["purl"] = self.args.SR + ret["purl"]
|
2582
|
-
|
2583
2599
|
if self.args.shr and self.vpath.startswith(self.args.shr1):
|
2584
2600
|
# strip common suffix (uploader's folder structure)
|
2585
2601
|
vp_req, vp_vfs = vroots(self.vpath, vjoin(dbv.vpath, vrem))
|
@@ -2589,6 +2605,10 @@ class HttpCli(object):
|
|
2589
2605
|
raise Pebkac(500, t % zt)
|
2590
2606
|
ret["purl"] = vp_req + ret["purl"][len(vp_vfs) :]
|
2591
2607
|
|
2608
|
+
if self.is_vproxied:
|
2609
|
+
if "purl" in ret:
|
2610
|
+
ret["purl"] = self.args.SR + ret["purl"]
|
2611
|
+
|
2592
2612
|
ret = json.dumps(ret)
|
2593
2613
|
self.log(ret)
|
2594
2614
|
self.reply(ret.encode("utf-8"), mime="application/json")
|
@@ -2696,6 +2716,7 @@ class HttpCli(object):
|
|
2696
2716
|
locked = chashes # remaining chunks to be received in this request
|
2697
2717
|
written = [] # chunks written to disk, but not yet released by up2k
|
2698
2718
|
num_left = -1 # num chunks left according to most recent up2k release
|
2719
|
+
bail1 = False # used in sad path to avoid contradicting error-text
|
2699
2720
|
treport = time.time() # ratelimit up2k reporting to reduce overhead
|
2700
2721
|
|
2701
2722
|
if "x-up2k-subc" in self.headers:
|
@@ -2834,7 +2855,6 @@ class HttpCli(object):
|
|
2834
2855
|
except:
|
2835
2856
|
# maybe busted handle (eg. disk went full)
|
2836
2857
|
f.close()
|
2837
|
-
chashes = [] # exception flag
|
2838
2858
|
raise
|
2839
2859
|
finally:
|
2840
2860
|
if locked:
|
@@ -2843,13 +2863,14 @@ class HttpCli(object):
|
|
2843
2863
|
num_left, t = x.get()
|
2844
2864
|
if num_left < 0:
|
2845
2865
|
self.loud_reply(t, status=500)
|
2846
|
-
|
2847
|
-
return False
|
2866
|
+
bail1 = True
|
2848
2867
|
else:
|
2849
2868
|
t = "got %d more chunks, %d left"
|
2850
2869
|
self.log(t % (len(written), num_left), 6)
|
2851
2870
|
|
2852
2871
|
if num_left < 0:
|
2872
|
+
if bail1:
|
2873
|
+
return False
|
2853
2874
|
raise Pebkac(500, "unconfirmed; see serverlog")
|
2854
2875
|
|
2855
2876
|
if not num_left and fpool:
|
@@ -3786,6 +3807,20 @@ class HttpCli(object):
|
|
3786
3807
|
|
3787
3808
|
return txt
|
3788
3809
|
|
3810
|
+
def _can_tail(self, volflags ) :
|
3811
|
+
zp = self.args.ua_nodoc
|
3812
|
+
if zp and zp.search(self.ua):
|
3813
|
+
t = "this URL contains no valuable information for bots/crawlers"
|
3814
|
+
raise Pebkac(403, t)
|
3815
|
+
lvl = volflags["tail_who"]
|
3816
|
+
if "notail" in volflags or not lvl:
|
3817
|
+
raise Pebkac(400, "tail is disabled in server config")
|
3818
|
+
elif lvl <= 1 and not self.can_admin:
|
3819
|
+
raise Pebkac(400, "tail is admin-only on this server")
|
3820
|
+
elif lvl <= 2 and self.uname in ("", "*"):
|
3821
|
+
raise Pebkac(400, "you must be authenticated to use ?tail on this server")
|
3822
|
+
return True
|
3823
|
+
|
3789
3824
|
def _can_zip(self, volflags ) :
|
3790
3825
|
lvl = volflags["zip_who"]
|
3791
3826
|
if self.args.no_zip or not lvl:
|
@@ -3930,6 +3965,8 @@ class HttpCli(object):
|
|
3930
3965
|
logmsg = "{:4} {} ".format("", self.req)
|
3931
3966
|
logtail = ""
|
3932
3967
|
|
3968
|
+
is_tail = "tail" in self.uparam and self._can_tail(self.vn.flags)
|
3969
|
+
|
3933
3970
|
if ptop is not None:
|
3934
3971
|
ap_data = "<%s>" % (req_path,)
|
3935
3972
|
try:
|
@@ -4042,6 +4079,7 @@ class HttpCli(object):
|
|
4042
4079
|
and can_range
|
4043
4080
|
and file_sz
|
4044
4081
|
and "," not in hrange
|
4082
|
+
and not is_tail
|
4045
4083
|
):
|
4046
4084
|
try:
|
4047
4085
|
if not hrange.lower().startswith("bytes"):
|
@@ -4110,6 +4148,8 @@ class HttpCli(object):
|
|
4110
4148
|
mime = "text/plain; charset={}".format(self.uparam["txt"] or "utf-8")
|
4111
4149
|
elif "mime" in self.uparam:
|
4112
4150
|
mime = str(self.uparam.get("mime"))
|
4151
|
+
elif "rmagic" in self.vn.flags:
|
4152
|
+
mime = guess_mime(req_path, fs_path)
|
4113
4153
|
else:
|
4114
4154
|
mime = guess_mime(req_path)
|
4115
4155
|
|
@@ -4127,13 +4167,18 @@ class HttpCli(object):
|
|
4127
4167
|
return True
|
4128
4168
|
|
4129
4169
|
dls = self.conn.hsrv.dls
|
4170
|
+
if is_tail:
|
4171
|
+
upper = 1 << 30
|
4172
|
+
if len(dls) > self.args.tail_cmax:
|
4173
|
+
raise Pebkac(400, "too many active downloads to start a new tail")
|
4174
|
+
|
4130
4175
|
if upper - lower > 0x400000: # 4m
|
4131
4176
|
now = time.time()
|
4132
4177
|
self.dl_id = "%s:%s" % (self.ip, self.addr[1])
|
4133
4178
|
dls[self.dl_id] = (now, 0)
|
4134
4179
|
self.conn.hsrv.dli[self.dl_id] = (
|
4135
4180
|
now,
|
4136
|
-
upper - lower,
|
4181
|
+
0 if is_tail else upper - lower,
|
4137
4182
|
self.vn,
|
4138
4183
|
self.vpath,
|
4139
4184
|
self.uname,
|
@@ -4143,6 +4188,9 @@ class HttpCli(object):
|
|
4143
4188
|
return self.tx_pipe(
|
4144
4189
|
ptop, req_path, ap_data, job, lower, upper, status, mime, logmsg
|
4145
4190
|
)
|
4191
|
+
elif is_tail:
|
4192
|
+
self.tx_tail(open_args, status, mime)
|
4193
|
+
return False
|
4146
4194
|
|
4147
4195
|
ret = True
|
4148
4196
|
with open_func(*open_args) as f:
|
@@ -4172,6 +4220,131 @@ class HttpCli(object):
|
|
4172
4220
|
|
4173
4221
|
return ret
|
4174
4222
|
|
4223
|
+
def tx_tail(
|
4224
|
+
self,
|
4225
|
+
open_args ,
|
4226
|
+
status ,
|
4227
|
+
mime ,
|
4228
|
+
) :
|
4229
|
+
vf = self.vn.flags
|
4230
|
+
self.send_headers(length=None, status=status, mime=mime)
|
4231
|
+
abspath = open_args[0]
|
4232
|
+
sec_rate = vf["tail_rate"]
|
4233
|
+
sec_max = vf["tail_tmax"]
|
4234
|
+
sec_fd = vf["tail_fd"]
|
4235
|
+
sec_ka = self.args.tail_ka
|
4236
|
+
wr_slp = self.args.s_wr_slp
|
4237
|
+
wr_sz = self.args.s_wr_sz
|
4238
|
+
dls = self.conn.hsrv.dls
|
4239
|
+
dl_id = self.dl_id
|
4240
|
+
|
4241
|
+
# non-numeric = full file from start
|
4242
|
+
# positive = absolute offset from start
|
4243
|
+
# negative = start that many bytes from eof
|
4244
|
+
try:
|
4245
|
+
ofs = int(self.uparam["tail"])
|
4246
|
+
except:
|
4247
|
+
ofs = 0
|
4248
|
+
|
4249
|
+
t0 = time.time()
|
4250
|
+
ofs0 = ofs
|
4251
|
+
f = None
|
4252
|
+
try:
|
4253
|
+
st = os.stat(abspath)
|
4254
|
+
f = open(*open_args)
|
4255
|
+
f.seek(0, os.SEEK_END)
|
4256
|
+
eof = f.tell()
|
4257
|
+
f.seek(0)
|
4258
|
+
if ofs < 0:
|
4259
|
+
ofs = max(0, ofs + eof)
|
4260
|
+
|
4261
|
+
self.log("tailing from byte %d: %r" % (ofs, abspath), 6)
|
4262
|
+
|
4263
|
+
# send initial data asap
|
4264
|
+
remains = sendfile_py(
|
4265
|
+
self.log, # d/c
|
4266
|
+
ofs,
|
4267
|
+
eof,
|
4268
|
+
f,
|
4269
|
+
self.s,
|
4270
|
+
wr_sz,
|
4271
|
+
wr_slp,
|
4272
|
+
False, # d/c
|
4273
|
+
dls,
|
4274
|
+
dl_id,
|
4275
|
+
)
|
4276
|
+
sent = (eof - ofs) - remains
|
4277
|
+
ofs = eof - remains
|
4278
|
+
f.seek(ofs)
|
4279
|
+
|
4280
|
+
try:
|
4281
|
+
st2 = os.stat(open_args[0])
|
4282
|
+
if st.st_ino == st2.st_ino:
|
4283
|
+
st = st2 # for filesize
|
4284
|
+
except:
|
4285
|
+
pass
|
4286
|
+
|
4287
|
+
gone = 0
|
4288
|
+
t_fd = t_ka = time.time()
|
4289
|
+
while True:
|
4290
|
+
buf = f.read(4096)
|
4291
|
+
now = time.time()
|
4292
|
+
|
4293
|
+
if sec_max and now - t0 >= sec_max:
|
4294
|
+
self.log("max duration exceeded; kicking client", 6)
|
4295
|
+
zb = b"\n\n*** max duration exceeded; disconnecting ***\n"
|
4296
|
+
self.s.sendall(zb)
|
4297
|
+
break
|
4298
|
+
|
4299
|
+
if buf:
|
4300
|
+
t_fd = t_ka = now
|
4301
|
+
self.s.sendall(buf)
|
4302
|
+
sent += len(buf)
|
4303
|
+
dls[dl_id] = (time.time(), sent)
|
4304
|
+
continue
|
4305
|
+
|
4306
|
+
time.sleep(sec_rate)
|
4307
|
+
if t_ka < now - sec_ka:
|
4308
|
+
t_ka = now
|
4309
|
+
self.s.send(b"\x00")
|
4310
|
+
if t_fd < now - sec_fd:
|
4311
|
+
try:
|
4312
|
+
st2 = os.stat(open_args[0])
|
4313
|
+
if (
|
4314
|
+
st2.st_ino != st.st_ino
|
4315
|
+
or st2.st_size < sent
|
4316
|
+
or st2.st_size < st.st_size
|
4317
|
+
):
|
4318
|
+
# open new file before closing previous to avoid toctous (open may fail; cannot null f before)
|
4319
|
+
f2 = open(*open_args)
|
4320
|
+
f.close()
|
4321
|
+
f = f2
|
4322
|
+
f.seek(0, os.SEEK_END)
|
4323
|
+
eof = f.tell()
|
4324
|
+
if eof < sent:
|
4325
|
+
ofs = sent = 0 # shrunk; send from start
|
4326
|
+
zb = b"\n\n*** file size decreased -- rewinding to the start of the file ***\n\n"
|
4327
|
+
self.s.sendall(zb)
|
4328
|
+
if ofs0 < 0 and eof > -ofs0:
|
4329
|
+
ofs = eof + ofs0
|
4330
|
+
else:
|
4331
|
+
ofs = sent # just new fd? resume from same ofs
|
4332
|
+
f.seek(ofs)
|
4333
|
+
self.log("reopened at byte %d: %r" % (ofs, abspath), 6)
|
4334
|
+
gone = 0
|
4335
|
+
st = st2
|
4336
|
+
except:
|
4337
|
+
gone += 1
|
4338
|
+
if gone > 3:
|
4339
|
+
self.log("file deleted; disconnecting")
|
4340
|
+
break
|
4341
|
+
except IOError as ex:
|
4342
|
+
if ex.errno not in (errno.EPIPE, errno.ESHUTDOWN, errno.EBADFD):
|
4343
|
+
raise
|
4344
|
+
finally:
|
4345
|
+
if f:
|
4346
|
+
f.close()
|
4347
|
+
|
4175
4348
|
def tx_pipe(
|
4176
4349
|
self,
|
4177
4350
|
ptop ,
|
@@ -4731,7 +4904,6 @@ class HttpCli(object):
|
|
4731
4904
|
if zi == 2 or (zi == 1 and self.avol):
|
4732
4905
|
dl_list = self.get_dls()
|
4733
4906
|
for t0, t1, sent, sz, vp, dl_id, uname in dl_list:
|
4734
|
-
rem = sz - sent
|
4735
4907
|
td = max(0.1, now - t0)
|
4736
4908
|
rd, fn = vsplit(vp)
|
4737
4909
|
if not rd:
|
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
|
|
copyparty/web/browser.css.gz
CHANGED
Binary file
|
copyparty/web/browser.js.gz
CHANGED
Binary file
|
copyparty/web/deps/marked.js.gz
CHANGED
Binary file
|
copyparty/web/splash.js.gz
CHANGED
Binary file
|
copyparty/web/ui.css.gz
CHANGED
Binary file
|
copyparty/web/up2k.js.gz
CHANGED
Binary file
|
copyparty/web/util.js.gz
CHANGED
Binary file
|
copyparty/web/w.hash.js.gz
CHANGED
Binary file
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: copyparty
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.18.0
|
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
|
@@ -66,12 +66,14 @@ turn almost any device into a file server with resumable uploads/downloads using
|
|
66
66
|
* 🔌 protocols: [http](#the-browser) // [webdav](#webdav-server) // [ftp](#ftp-server) // [tftp](#tftp-server) // [smb/cifs](#smb-server)
|
67
67
|
* 📱 [android app](#android-app) // [iPhone shortcuts](#ios-shortcuts)
|
68
68
|
|
69
|
-
👉 **[Get started](#quickstart)!** or visit the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running
|
69
|
+
👉 **[Get started](#quickstart)!** or visit the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running on a nuc in my basement
|
70
70
|
|
71
71
|
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer)
|
72
72
|
|
73
73
|
🎬 **videos:** [upload](https://a.ocv.me/pub/demo/pics-vids/up2k.webm) // [cli-upload](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm) // [race-the-beam](https://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm)
|
74
74
|
|
75
|
+
made in Norway 🇳🇴
|
76
|
+
|
75
77
|
|
76
78
|
## readme toc
|
77
79
|
|
@@ -112,6 +114,7 @@ turn almost any device into a file server with resumable uploads/downloads using
|
|
112
114
|
* [creating a playlist](#creating-a-playlist) - with a standalone mediaplayer or copyparty
|
113
115
|
* [audio equalizer](#audio-equalizer) - and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression)
|
114
116
|
* [fix unreliable playback on android](#fix-unreliable-playback-on-android) - due to phone / app settings
|
117
|
+
* [textfile viewer](#textfile-viewer) - with realtime streaming of logfiles and such ([demo](https://a.ocv.me/pub/demo/logtail/))
|
115
118
|
* [markdown viewer](#markdown-viewer) - and there are *two* editors
|
116
119
|
* [markdown vars](#markdown-vars) - dynamic docs with serverside variable expansion
|
117
120
|
* [other tricks](#other-tricks)
|
@@ -313,7 +316,8 @@ also see [comparison to similar software](./docs/versus.md)
|
|
313
316
|
* ☑ play video files as audio (converted on server)
|
314
317
|
* ☑ create and play [m3u8 playlists](#playlists)
|
315
318
|
* ☑ image gallery with webm player
|
316
|
-
* ☑ textfile browser with syntax hilighting
|
319
|
+
* ☑ [textfile browser](#textfile-viewer) with syntax hilighting
|
320
|
+
* ☑ realtime streaming of growing files (logfiles and such)
|
317
321
|
* ☑ [thumbnails](#thumbnails)
|
318
322
|
* ☑ ...of images using Pillow, pyvips, or FFmpeg
|
319
323
|
* ☑ ...of videos using FFmpeg
|
@@ -617,6 +621,8 @@ a client can request to see dotfiles in directory listings if global option `-ed
|
|
617
621
|
|
618
622
|
dotfiles do not appear in search results unless one of the above is true, **and** the global option / volflag `dotsrch` is set
|
619
623
|
|
624
|
+
> even if user has permission to see dotfiles, they are default-hidden unless `--see-dots` is set, and/or user has enabled the `dotfiles` option in the settings tab
|
625
|
+
|
620
626
|
config file example, where the same permission to see dotfiles is given in two different ways just for reference:
|
621
627
|
|
622
628
|
```yaml
|
@@ -753,7 +759,10 @@ enabling `multiselect` lets you click files to select them, and then shift-click
|
|
753
759
|
* `multiselect` is mostly intended for phones/tablets, but the `sel` option in the `[⚙️] settings` tab is better suited for desktop use, allowing selection by CTRL-clicking and range-selection with SHIFT-click, all without affecting regular clicking
|
754
760
|
* the `sel` option can be made default globally with `--gsel` or per-volume with volflag `gsel`
|
755
761
|
|
756
|
-
to show `/icons/exe.png` as the thumbnail for all
|
762
|
+
to show `/icons/exe.png` and `/icons/elf.gif` as the thumbnail for all `.exe` and `.elf` files respectively, do this: `--ext-th=exe=/icons/exe.png --ext-th=elf=/icons/elf.gif`
|
763
|
+
* optionally as separate volflags for each mapping; see config file example below
|
764
|
+
* the supported image formats are [jpg, png, gif, webp, ico](https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types)
|
765
|
+
* be careful with svg; chrome will crash if you have too many unique svg files showing on the same page (the limit is 250 or so) -- showing the same handful of svg files thousands of times is ok however
|
757
766
|
|
758
767
|
config file example:
|
759
768
|
|
@@ -770,6 +779,7 @@ config file example:
|
|
770
779
|
dthumb # disable ALL thumbnails and audio transcoding
|
771
780
|
dvthumb # only disable video thumbnails
|
772
781
|
ext-th: exe=/ico/exe.png # /ico/exe.png is the thumbnail of *.exe
|
782
|
+
ext-th: elf=/ico/elf.gif # ...and /ico/elf.gif is used for *.elf
|
773
783
|
th-covers: folder.png,folder.jpg,cover.png,cover.jpg # the default
|
774
784
|
```
|
775
785
|
|
@@ -1177,6 +1187,18 @@ not available on iPhones / iPads because AudioContext currently breaks backgroun
|
|
1177
1187
|
due to phone / app settings, android phones may randomly stop playing music when the power saver kicks in, especially at the end of an album -- you can fix it by [disabling power saving](https://user-images.githubusercontent.com/241032/235262123-c328cca9-3930-4948-bd18-3949b9fd3fcf.png) in the [app settings](https://user-images.githubusercontent.com/241032/235262121-2ffc51ae-7821-4310-a322-c3b7a507890c.png) of the browser you use for music streaming (preferably a dedicated one)
|
1178
1188
|
|
1179
1189
|
|
1190
|
+
## textfile viewer
|
1191
|
+
|
1192
|
+
with realtime streaming of logfiles and such ([demo](https://a.ocv.me/pub/demo/logtail/)) , and terminal colors work too
|
1193
|
+
|
1194
|
+
click `-txt-` next to a textfile to open the viewer, which has the following toolbar buttons:
|
1195
|
+
|
1196
|
+
* `✏️ edit` opens the textfile editor
|
1197
|
+
* `📡 follow` starts monitoring the file for changes, streaming new lines in realtime
|
1198
|
+
* similar to `tail -f`
|
1199
|
+
* [link directly](https://a.ocv.me/pub/demo/logtail/?doc=lipsum.txt&tail) to a file with tailing enabled by adding `&tail` to the textviewer URL
|
1200
|
+
|
1201
|
+
|
1180
1202
|
## markdown viewer
|
1181
1203
|
|
1182
1204
|
and there are *two* editors
|
@@ -2475,6 +2497,9 @@ interact with copyparty using non-browser clients
|
|
2475
2497
|
* and for screenshots on macos, see [./contrib/ishare.iscu](./contrib/#ishareiscu)
|
2476
2498
|
* and for screenshots on linux, see [./contrib/flameshot.sh](./contrib/flameshot.sh)
|
2477
2499
|
|
2500
|
+
* [Custom Uploader](https://f-droid.org/en/packages/com.nyx.custom_uploader/) (an Android app) as an alternative to copyparty's own [PartyUP!](#android-app)
|
2501
|
+
* works if you set UploadURL to `https://your.com/foo/?want=url&pw=hunter2` and FormDataName `f`
|
2502
|
+
|
2478
2503
|
* contextlet (web browser integration); see [contrib contextlet](contrib/#send-to-cppcontextletjson)
|
2479
2504
|
|
2480
2505
|
* [igloo irc](https://iglooirc.com/): Method: `post` Host: `https://you.com/up/?want=url&pw=hunter2` Multipart: `yes` File parameter: `f`
|
@@ -2576,6 +2601,11 @@ below are some tweaks roughly ordered by usefulness:
|
|
2576
2601
|
|
2577
2602
|
when uploading files,
|
2578
2603
|
|
2604
|
+
* when uploading from very fast storage (NVMe SSD) with chrome/firefox, enable `[wasm]` in the `[⚙️] settings` tab to more effectively use all CPU-cores for hashing
|
2605
|
+
* don't do this on Safari (runs faster without)
|
2606
|
+
* don't do this on older browsers; likely to provoke browser-bugs (browser eats all RAM and crashes)
|
2607
|
+
* can be made default-enabled serverside with `--nosubtle 137` (chrome v137+) or `--nosubtle 2` (chrome+firefox)
|
2608
|
+
|
2579
2609
|
* chrome is recommended (unfortunately), at least compared to firefox:
|
2580
2610
|
* up to 90% faster when hashing, especially on SSDs
|
2581
2611
|
* up to 40% faster when uploading over extremely fast internets
|
@@ -2783,6 +2813,7 @@ set any of the following environment variables to disable its associated optiona
|
|
2783
2813
|
| `PRTY_NO_CFSSL` | never attempt to generate self-signed certificates using [cfssl](https://github.com/cloudflare/cfssl) |
|
2784
2814
|
| `PRTY_NO_FFMPEG` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips |
|
2785
2815
|
| `PRTY_NO_FFPROBE` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips, **metadata-scanning** must be handled by mutagen |
|
2816
|
+
| `PRTY_NO_MAGIC` | do not use [magic](https://pypi.org/project/python-magic/) for filetype detection |
|
2786
2817
|
| `PRTY_NO_MUTAGEN` | do not use [mutagen](https://pypi.org/project/mutagen/) for reading metadata from media files; will fallback to ffprobe |
|
2787
2818
|
| `PRTY_NO_PIL` | disable all [Pillow](https://pypi.org/project/pillow/)-based thumbnail support; will fallback to libvips or ffmpeg |
|
2788
2819
|
| `PRTY_NO_PILF` | disable Pillow `ImageFont` text rendering, used for folder thumbnails |
|
@@ -2883,5 +2914,7 @@ if there's a wall of base64 in the log (thread stacks) then please include that,
|
|
2883
2914
|
|
2884
2915
|
for build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md)
|
2885
2916
|
|
2917
|
+
specifically you may want to [build the sfx](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#just-the-sfx) or [build from scratch](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#build-from-scratch)
|
2918
|
+
|
2886
2919
|
see [./docs/TODO.md](./docs/TODO.md) for planned features / fixes / changes
|
2887
2920
|
|
@@ -1,17 +1,17 @@
|
|
1
1
|
copyparty/__init__.py,sha256=TnFSStmHlwlRIClWW8jSHxZpt3dl_kN6_pEnqBqh3mE,2638
|
2
|
-
copyparty/__main__.py,sha256=
|
3
|
-
copyparty/__version__.py,sha256=
|
4
|
-
copyparty/authsrv.py,sha256=
|
2
|
+
copyparty/__main__.py,sha256=gJlKxpBmV39xfiWp8sx5iUZsoJPhFcCAWY5RVpWsrz0,122959
|
3
|
+
copyparty/__version__.py,sha256=QNl11l-yovzXttFgMOYiqG-Rd-5GJHs6EcPWg-ZqCSI,249
|
4
|
+
copyparty/authsrv.py,sha256=K3dIZxJvNhrcPbXJpVIkoxWaDrQf3CYz3X-FYaVlrYw,114905
|
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=pSSeVYticrDsnsrdRtfpUQN-8WRObsqrYtSRroXmgxo,7992
|
10
|
-
copyparty/cfg.py,sha256=
|
10
|
+
copyparty/cfg.py,sha256=6o6aLzLxZ59lusAn_PMGgldc3rEb9yyGnUnGIP5D9M8,15077
|
11
11
|
copyparty/dxml.py,sha256=vu5uZQtwvwoqnFHbULs2Zh_y2DETu0T-ENpMZ1i2CV4,2505
|
12
12
|
copyparty/fsutil.py,sha256=NC_CJC4TDag399vVDH9_uQfdfpTMwRFLNxERSWhlVvs,4594
|
13
13
|
copyparty/ftpd.py,sha256=G7PApVIFeSzRo4-D-9uRb8NxYgz6nFwTEbrOk1ErYCU,17969
|
14
|
-
copyparty/httpcli.py,sha256=
|
14
|
+
copyparty/httpcli.py,sha256=r_M67eEl-qKLR6pf79TTcf_G1o0fJiOcU4fTTll7B34,228170
|
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=-7QjF_jIxnPo4Vr0oUPksQ_U_Ef0HRsSPm3s71idOz8,3879
|
@@ -32,7 +32,7 @@ copyparty/th_cli.py,sha256=IEX5tCb0gw9Z2aRIDL9bwdvJ6g5jhWZ8OEAAz16_xN4,5426
|
|
32
32
|
copyparty/th_srv.py,sha256=2omGprnKGWim6U7fjJAXI41UCZBSxRwZfS0rCDsUDps,32556
|
33
33
|
copyparty/u2idx.py,sha256=4Y5OOPyVkc-pS0z6e3p4StXAMnjHobSOMmMsvNUTD34,13674
|
34
34
|
copyparty/up2k.py,sha256=nnR_ZKaopSNBuAjSN3Q5G_UVe6GmYD1NkxJt5QQf02o,178079
|
35
|
-
copyparty/util.py,sha256=
|
35
|
+
copyparty/util.py,sha256=S-dEme_1rKf3qYgxWWahEUQUzgq_kpoO_nPbuKCwLsY,103948
|
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
|
@@ -55,9 +55,9 @@ copyparty/stolen/ifaddr/_posix.py,sha256=-67NdfGrCktfQPakT2fLbjl2U00QMvyBGkSvrUu
|
|
55
55
|
copyparty/stolen/ifaddr/_shared.py,sha256=uNC4SdEIgdSLKvuUzsf1aM-H1Xrc_9mpLoOT43YukGs,6206
|
56
56
|
copyparty/stolen/ifaddr/_win32.py,sha256=EE-QyoBgeB7lYQ6z62VjXNaRozaYfCkaJBHGNA8QtZM,4026
|
57
57
|
copyparty/web/baguettebox.js.gz,sha256=r2c_hOZV_RTyl4CqWWX14FDWP8nnDVwGkDl4Sfk0rU4,8239
|
58
|
-
copyparty/web/browser.css.gz,sha256
|
58
|
+
copyparty/web/browser.css.gz,sha256=cjHv6i6VDjCG66W9h0S37l2-Gv7-qKBH-HpTP7kZSr4,11769
|
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=qgL276U3d4GAZmkdq7YaWTBxGulnDvkevXDoCi9_GJk,96443
|
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
|
@@ -79,13 +79,13 @@ copyparty/web/shares.html,sha256=YctvUrKuBYu42kxVylyW2_DEHm7Ik6uHqzfzVZ4N0ac,254
|
|
79
79
|
copyparty/web/shares.js.gz,sha256=emeY2-wjkh8x1JgaW6ny5fcC7XpZzZzfE1f-sEyntQ4,940
|
80
80
|
copyparty/web/splash.css.gz,sha256=S8_A7JJl71xACRBYGzafeaD82OacW6Fa7oKPiNyrhAs,1087
|
81
81
|
copyparty/web/splash.html,sha256=ouFB1P9g0K-S3LZQtOLfNz3GLXIRjyETkxo9aJZhiYk,6249
|
82
|
-
copyparty/web/splash.js.gz,sha256=
|
82
|
+
copyparty/web/splash.js.gz,sha256=Qh0KoPWKoJ77cyzOwnhUaCTI5XUoPVV3YJURKklqpBg,2739
|
83
83
|
copyparty/web/svcs.html,sha256=cxgrhX9wD0Z_kvidry3aS9ubuGXYDj2f4ehq1X8T1EA,14227
|
84
84
|
copyparty/web/svcs.js.gz,sha256=lMXEP9W-VlXyANlva4q0ASSxvvHYlE2CrmxGgZXZop0,713
|
85
|
-
copyparty/web/ui.css.gz,sha256=
|
86
|
-
copyparty/web/up2k.js.gz,sha256=
|
87
|
-
copyparty/web/util.js.gz,sha256=
|
88
|
-
copyparty/web/w.hash.js.gz,sha256=
|
85
|
+
copyparty/web/ui.css.gz,sha256=e3iIflzddmjoyPrun_1jsu9j7fbdonNQLyhEE2oKKOQ,2819
|
86
|
+
copyparty/web/up2k.js.gz,sha256=vf9Kth2JQ4F-XmWhjcGXqy6gA4eOkg0-EfVjAGQsW5o,24794
|
87
|
+
copyparty/web/util.js.gz,sha256=vQj__zSM0cbBN4AoZgwhFrSCh82ZTv4CbzeufXo8tw4,15248
|
88
|
+
copyparty/web/w.hash.js.gz,sha256=cFH6Xo4YRgH9Wr7RmHMSEfpuTmmIvEmzmSvv4RLmyPU,1193
|
89
89
|
copyparty/web/a/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
90
90
|
copyparty/web/a/partyfuse.py,sha256=9p5Hpg_IBiSimv7j9kmPhCGpy-FLXSRUOYnLjJ5JifU,28049
|
91
91
|
copyparty/web/a/u2c.py,sha256=auXzLj04dt_lw4H70PhNUK0GjrQEThrybo2-77SLsUg,53165
|
@@ -100,7 +100,7 @@ copyparty/web/deps/busy.mp3.gz,sha256=EVphk1_HYyRKJmtpeK99vbAstF7ub1f9ndu020H8PQ
|
|
100
100
|
copyparty/web/deps/easymde.css.gz,sha256=vWxfueI64rPikuqFj69wJBtGisqf93AheQtOZqgUI_c,3041
|
101
101
|
copyparty/web/deps/easymde.js.gz,sha256=rHBs4XWQe2bmv7ZzDIk43oxnTwrwpq5laYHhV5sKQQo,77014
|
102
102
|
copyparty/web/deps/fuse.py,sha256=6j4Zy3VpQg629pwwIW77v2LJ1hy-qlyrxwhXfKl9B7I,33426
|
103
|
-
copyparty/web/deps/marked.js.gz,sha256=
|
103
|
+
copyparty/web/deps/marked.js.gz,sha256=UdxHVVlpRf9k1UivWpLKVQzpzZxfK_O3pleTi_B5f8k,22704
|
104
104
|
copyparty/web/deps/mini-fa.css.gz,sha256=CTPrNaH8OTVmxajrGP88E2MkjadY9_81TBVnd9sw9Y8,572
|
105
105
|
copyparty/web/deps/mini-fa.woff,sha256=L9DNncV2TIyvsrspMbJouvnnt7F068Hbn7YZYvN76AU,2784
|
106
106
|
copyparty/web/deps/prism.css.gz,sha256=Z_A6rJ3MN5KWnjvXaV787aTW_5DT-xjFd0YZ7_W-Krk,1468
|
@@ -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.
|
113
|
-
copyparty-1.
|
114
|
-
copyparty-1.
|
115
|
-
copyparty-1.
|
116
|
-
copyparty-1.
|
117
|
-
copyparty-1.
|
112
|
+
copyparty-1.18.0.dist-info/licenses/LICENSE,sha256=gOr4h33pCsBEg9uIy9AYmb7qlocL4V9t2uPJS5wllr0,1072
|
113
|
+
copyparty-1.18.0.dist-info/METADATA,sha256=AgVke4zkEUcHf9Iw-CNV1myTB72KmxPHCua7tI3DYwg,165863
|
114
|
+
copyparty-1.18.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
115
|
+
copyparty-1.18.0.dist-info/entry_points.txt,sha256=4zw6a3rqASywQomiYLObjjlxybaI65LYYOTJwgKz7b0,128
|
116
|
+
copyparty-1.18.0.dist-info/top_level.txt,sha256=LnYUPsDyk-8kFgM6YJLG4h820DQekn81cObKSu9g-sI,10
|
117
|
+
copyparty-1.18.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|