diff --git a/README.md b/README.md
index d8c88bac..c8d50dbf 100644
--- a/README.md
+++ b/README.md
@@ -120,6 +120,7 @@ built in Norway 🇳🇴 with contributions from [not-norway](https://github.com
* [packages](#packages) - the party might be closer than you think
* [arch package](#arch-package) - `pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/))
* [fedora package](#fedora-package) - does not exist yet
+ * [gentoo ::guru package](#gentoo-guru-package) - `emerge www-servers/copyparty::guru` (in [::guru](https://wiki.gentoo.org/wiki/Project:GURU))
* [homebrew formulae](#homebrew-formulae) - `brew install copyparty ffmpeg`
* [nix package](#nix-package) - `nix profile install github:9001/copyparty`
* [nixos module](#nixos-module)
@@ -183,7 +184,7 @@ enable thumbnails (images/audio/video), media indexing, and audio transcoding by
* **Alpine:** `apk add py3-pillow ffmpeg`
* **Debian:** `apt install --no-install-recommends python3-pil ffmpeg`
* **Fedora:** rpmfusion + `dnf install python3-pillow ffmpeg --allowerasing`
-* **FreeBSD:** `pkg install py39-sqlite3 py39-pillow ffmpeg`
+* **FreeBSD:** `pkg install py311-sqlite3 py311-pillow ffmpeg`
* **MacOS:** `port install py-Pillow ffmpeg`
* **MacOS** (alternative): `brew install pillow ffmpeg`
* **Windows:** `python -m pip install --user -U Pillow`
@@ -607,10 +608,12 @@ and if you want to use config files instead of commandline args (good!) then her
hiding specific subfolders by mounting another volume on top of them
-for example `-v /mnt::r -v /var/empty:web/certs:r` mounts the server folder `/mnt` as the webroot, but another volume is mounted at `/web/certs` -- so visitors can only see the contents of `/mnt` and `/mnt/web` (at URLs `/` and `/web`), but not `/mnt/web/certs` because URL `/web/certs` is mapped to `/var/empty`
+for example `-v /mnt::r -v /var/empty:web/certs:` (note: no permissions) mounts the server folder `/mnt` as the webroot, but another volume is mounted at `/web/certs` -- so visitors can only see the contents of `/mnt` and `/mnt/web` (at URLs `/` and `/web`), but not `/mnt/web/certs` because URL `/web/certs` is mapped to `/var/empty`
the example config file right above this section may explain this better; the first volume `/` is mapped to `/srv` which means http://127.0.0.1:3923/music would try to read `/srv/music` on the server filesystem, but since there's another volume at `/music` mapped to `/mnt/music` then it'll go to `/mnt/music` instead
+so, to shadow a file/folder, define a volume but leave out the `accs:` section
+
> ℹ️ this also works for single files, because files can also be volumes
@@ -2539,6 +2542,7 @@ buggy feature? rip it out by setting any of the following environment variables
| -------------------- | ------------ |
| `PRTY_NO_CTYPES` | do not use features from external libraries such as kernel32 |
| `PRTY_NO_DB_LOCK` | do not lock session/shares-databases for exclusive access |
+| `PRTY_NO_ENVEXPAND` | do not expand environment-variables in configs and args |
| `PRTY_NO_IFADDR` | disable ip/nic discovery by poking into your OS with ctypes |
| `PRTY_NO_IMPRESO` | do not try to load js/css files using `importlib.resources` |
| `PRTY_NO_IPV6` | disable some ipv6 support (should not be necessary since windows 2000) |
@@ -2585,6 +2589,23 @@ after installing, start either the system service or the user service and naviga
does not exist yet; there are rumours that it is being packaged! keep an eye on this space...
+## gentoo ::guru package
+
+`emerge www-servers/copyparty::guru` (in [::guru](https://wiki.gentoo.org/wiki/Project:GURU))
+
+but first enable the `::guru` repo;
+
+```bash
+emerge -an app-eselect/eselect-repository
+eselect repository enable guru
+emerge --sync guru
+```
+
+to start the service as a user:
+* OpenRC: `rc-service -U copyparty start && rc-update -U add copyparty default`
+* systemd: [todo]
+
+
## homebrew formulae
`brew install copyparty ffmpeg` -- https://formulae.brew.sh/formula/copyparty
@@ -2729,6 +2750,12 @@ services.copyparty = {
};
# you may increase the open file limit for the process
openFilesLimit = 8192;
+
+ # override the package used by the module to add dependencies, e.g. for hooks
+ package = pkgs.copyparty.override {
+ # provides exiftool for bin/hooks/image-noexif.py
+ extraPackages = [ pkgs.exiftool ];
+ };
};
```
diff --git a/contrib/podman-systemd/copyparty.conf b/contrib/podman-systemd/copyparty.conf
index aa3d4b2e..001835c6 100644
--- a/contrib/podman-systemd/copyparty.conf
+++ b/contrib/podman-systemd/copyparty.conf
@@ -8,7 +8,7 @@
# and copyparty replaces %Y-%m%d with Year-MonthDay, so the
# full path will be something like /var/log/copyparty/2023-1130.txt
# (note: enable compression by adding .xz at the end)
- # q, lo: $LOGS_DIRECTORY/%Y-%m%d.log
+ # q, lo: ${LOGS_DIRECTORY}/%Y-%m%d.log
# p: 80,443,3923 # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE)
# i: 127.0.0.1 # only allow connections from localhost (reverse-proxies)
diff --git a/contrib/systemd/copyparty.example.conf b/contrib/systemd/copyparty.example.conf
index de220e9f..b85ec770 100644
--- a/contrib/systemd/copyparty.example.conf
+++ b/contrib/systemd/copyparty.example.conf
@@ -16,7 +16,7 @@
# and copyparty replaces %Y-%m%d with Year-MonthDay, so the
# full path will be something like /var/log/copyparty/2023-1130.txt
# (note: enable compression by adding .xz at the end)
- q, lo: $LOGS_DIRECTORY/%Y-%m%d.log
+ q, lo: ${LOGS_DIRECTORY}/%Y-%m%d.log
# enable version-checker by uncommenting one of the 'vc-url' lines below; this will
# periodically check if your copyparty version has a known security vulnerability,
diff --git a/copyparty/__init__.py b/copyparty/__init__.py
index 55befeb7..8bffed86 100644
--- a/copyparty/__init__.py
+++ b/copyparty/__init__.py
@@ -44,6 +44,14 @@ ANYWIN = WINDOWS or sys.platform in ["msys", "cygwin"]
MACOS = platform.system() == "Darwin"
+FREEBSD = platform.system() == "FreeBSD"
+
+OPENBSD = platform.system() == "OpenBSD"
+
+ANYBSD = FREEBSD or OPENBSD
+
+UNIX = MACOS or ANYBSD
+
GRAAL = platform.python_implementation() == "GraalVM"
EXE = bool(getattr(sys, "frozen", False))
diff --git a/copyparty/__main__.py b/copyparty/__main__.py
index 71b9a74e..c14e8d8b 100644
--- a/copyparty/__main__.py
+++ b/copyparty/__main__.py
@@ -65,6 +65,10 @@ from .util import (
b64enc,
ctypes,
dedent,
+ expand_osenv_c,
+ expand_osenv_cs,
+ expand_osenv_noop,
+ expand_osenv_s,
has_resource,
load_resource,
min_ex,
@@ -427,9 +431,22 @@ def configure_ssl_ciphers(al: argparse.Namespace) -> None:
sys.exit(0)
+def expand_cvars(argv) -> list[str]:
+ n = 0
+ for v in argv:
+ if "=" in v:
+ a, b = v.split("=", 1)
+ v = "%s=%s" % (a, os.path.expanduser(expand_osenv_c(b)))
+ else:
+ v = os.path.expanduser(expand_osenv_c(v))
+ argv[n] = v
+ n += 1
+ return argv
+
+
def args_from_cfg(cfg_path: str) -> list[str]:
lines: list[str] = []
- expand_config_file(None, lines, cfg_path, "")
+ expand_config_file(None, expand_osenv_c, lines, cfg_path, "")
lines = upgrade_cfg_fmt(None, argparse.Namespace(vc=False), lines, "")
ret: list[str] = []
@@ -453,10 +470,12 @@ def args_from_cfg(cfg_path: str) -> list[str]:
else:
ret.append(prefix + k + "=" + v)
- return ret
+ return expand_cvars(ret)
def expand_cfg(argv) -> list[str]:
+ argv = expand_cvars(argv)
+
if CFG_DEF:
supp = args_from_cfg(CFG_DEF[0])
argv = argv[:1] + supp + argv[1:]
@@ -1197,6 +1216,7 @@ def add_general(ap, nc, srvname):
ap2.add_argument("--name-url", metavar="TXT", type=u, help="URL for server name hyperlink (displayed topleft in browser)")
ap2.add_argument("--name-html", type=u, help=argparse.SUPPRESS)
ap2.add_argument("--site", metavar="URL", type=u, default="", help="public URL to assume when creating links; example: [\033[32mhttps://example.com/\033[0m]")
+ ap2.add_argument("--env-expand", metavar="N", type=int, default=-1, help="syntax to expect for environment-variables to expand in config-files; [\033[32m0\033[0m]=disable, [\033[32m1\033[0m]=$VAR (old syntax (scary)), [\033[32m2\033[0m]=${VAR} (new syntax (recommended))")
ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="\033[34mREPEATABLE:\033[0m map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]")
ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit")
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)")
@@ -1520,6 +1540,7 @@ def add_smb(ap):
ap2.add_argument("--smb-nwa-1", action="store_true", help="truncate directory listings to 64kB (~400 files); avoids impacket-0.11 bug, fixes impacket-0.12 performance")
ap2.add_argument("--smb-nwa-2", action="store_true", help="disable impacket workaround for filecopy globs")
ap2.add_argument("--smba", action="store_true", help="small performance boost: disable per-account permissions, enables account coalescing instead (if one user has write/delete-access, then everyone does)")
+ ap2.add_argument("--smb6", action="store_true", help="enable IPv6")
ap2.add_argument("--smbv", action="store_true", help="verbose")
ap2.add_argument("--smbvv", action="store_true", help="verboser")
ap2.add_argument("--smbvvv", action="store_true", help="verbosest")
@@ -1740,7 +1761,7 @@ def add_thumbnail(ap):
ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="3fr,arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mdc,mef,mos,mrw,nef,nrw,orf,pef,raf,raw,sr2,srf,srw,x3f", help="image formats to decode using rawpy")
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
- ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,m4b,m4r,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
+ ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bcstm,bfstm,brstm,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,m4b,m4r,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)")
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz, epub=jpg.epub", help="audio/image formats to decompress before passing to ffmpeg")
@@ -1868,8 +1889,10 @@ def add_ui(ap, retry: int):
ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)")
ap2.add_argument("--localtime", action="store_true", help="default to local timezone instead of UTC")
ap2.add_argument("--ui-filesz", metavar="FMT", type=u, default="4", help="default filesize format; one of these: 0, 1, 2, 2c, 3, 3c, 4, 4c, 5, 5c, fuzzy (see UI)")
+ ap2.add_argument("--gauto", metavar="PERCENT", type=int, default=0, help="switch to gridview if more than \033[33mPERCENT\033[0m of files are pics/vids; 0=disabled")
ap2.add_argument("--rcm", metavar="TXT", default="yy", help="rightclick-menu; two yes/no options: 1st y/n is enable-custom-menu, 2nd y/n is enable-double")
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language, for example \033[32meng\033[0m / \033[32mnor\033[0m / ...")
+ ap2.add_argument("--glang", action="store_true", help="guess the browser's default language, otherwise fall back to \033[33m--lang\033[0m")
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..%d)" % (THEMES - 1,))
ap2.add_argument("--themes", metavar="NUM", type=int, default=THEMES, help="number of themes installed")
ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent")
@@ -2176,6 +2199,15 @@ def main(argv: Optional[list[str]] = None) -> None:
quotecheck(al)
+ if al.env_expand == 2:
+ al.shenvexp = expand_osenv_c
+ elif al.env_expand == 1:
+ al.shenvexp = expand_osenv_s
+ elif al.env_expand == 0:
+ al.shenvexp = expand_osenv_noop
+ else:
+ al.shenvexp = expand_osenv_cs
+
if al.chdir:
os.chdir(al.chdir)
diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py
index 61cf1da9..7dde17f4 100644
--- a/copyparty/authsrv.py
+++ b/copyparty/authsrv.py
@@ -56,7 +56,7 @@ if HAVE_SQLITE3:
if True: # pylint: disable=using-constant-test
from collections.abc import Iterable
- from typing import Any, Generator, Optional, Sequence, Union
+ from typing import Any, Callable, Generator, Optional, Sequence, Union
from .util import NamedLogger, RootLogger
@@ -541,13 +541,11 @@ class VFS(object):
hist = flags.get("hist")
if hist and hist != "-":
- zs = "{}/{}".format(hist.rstrip("/"), name)
- flags["hist"] = os.path.expandvars(os.path.expanduser(zs))
+ flags["hist"] = "%s/%s" % (hist.rstrip("/"), name)
dbp = flags.get("dbpath")
if dbp and dbp != "-":
- zs = "{}/{}".format(dbp.rstrip("/"), name)
- flags["dbpath"] = os.path.expandvars(os.path.expanduser(zs))
+ flags["dbpath"] = "%s/%s" % (dbp.rstrip("/"), name)
return flags
@@ -1279,7 +1277,7 @@ class AuthSrv(object):
daxs: dict[str, AXS],
mflags: dict[str, dict[str, Any]],
) -> tuple[str, str]:
- src = os.path.expandvars(os.path.expanduser(src))
+ src = os.path.expanduser(self.args.shenvexp(src))
src = absreal(src)
dst = dst.strip("/")
@@ -1372,7 +1370,7 @@ class AuthSrv(object):
) -> None:
self.line_ctr = 0
- expand_config_file(self.log, cfg_lines, fp, "")
+ expand_config_file(self.log, self.args.shenvexp, cfg_lines, fp, "")
if self.args.vc:
lns = ["{:4}: {}".format(n, s) for n, s in enumerate(cfg_lines, 1)]
self.log("expanded config file (unprocessed):\n" + "\n".join(lns))
@@ -2174,7 +2172,7 @@ class AuthSrv(object):
if vflag == "-":
pass
elif vflag:
- vflag = os.path.expandvars(os.path.expanduser(vflag))
+ vflag = os.path.expanduser(self.args.shenvexp(vflag))
vol.histpath = vol.dbpath = uncyg(vflag) if WINDOWS else vflag
elif self.args.hist:
for nch in range(len(hid)):
@@ -2209,7 +2207,7 @@ class AuthSrv(object):
if vflag == "-":
pass
elif vflag:
- vflag = os.path.expandvars(os.path.expanduser(vflag))
+ vflag = os.path.expanduser(self.args.shenvexp(vflag))
vol.dbpath = uncyg(vflag) if WINDOWS else vflag
elif self.args.dbpath:
for nch in range(len(hid)):
@@ -3258,6 +3256,7 @@ class AuthSrv(object):
"idxh": int(self.args.ih),
"dutc": not self.args.localtime,
"dfszf": self.args.ui_filesz.strip("-"),
+ "dgauto": self.args.gauto,
"themes": self.args.themes,
"turbolvl": self.args.turbo,
"nosubtle": self.args.nosubtle,
@@ -3273,7 +3272,7 @@ class AuthSrv(object):
for zs in zs.split():
if vf.get(zs):
js_htm[zs] = 1
- zs = "notooltips"
+ zs = "glang notooltips"
for zs in zs.split():
if getattr(self.args, zs, False):
js_htm[zs] = 1
@@ -3963,10 +3962,14 @@ def split_cfg_ln(ln: str) -> dict[str, Any]:
def expand_config_file(
- log: Optional["NamedLogger"], ret: list[str], fp: str, ipath: str
+ log: Optional["NamedLogger"],
+ shenvexp: "Callable[[str], str]",
+ ret: list[str],
+ fp: str,
+ ipath: str,
) -> None:
"""expand all % file includes"""
- fp = absreal(fp)
+ fp = absreal(os.path.expanduser(shenvexp(fp)))
if len(ipath.split(" -> ")) > 64:
raise Exception("hit max depth of 64 includes")
@@ -3997,7 +4000,7 @@ def expand_config_file(
if fp2 in ipath:
continue
- expand_config_file(log, ret, fp2, ipath)
+ expand_config_file(log, shenvexp, ret, fp2, ipath)
return
@@ -4022,7 +4025,7 @@ def expand_config_file(
fp2 = ln[1:].strip()
fp2 = os.path.join(os.path.dirname(fp), fp2)
ofs = len(ret)
- expand_config_file(log, ret, fp2, ipath)
+ expand_config_file(log, shenvexp, ret, fp2, ipath)
for n in range(ofs, len(ret)):
ret[n] = pad + ret[n]
continue
diff --git a/copyparty/fsutil.py b/copyparty/fsutil.py
index 9b2540c2..b0b1e8a4 100644
--- a/copyparty/fsutil.py
+++ b/copyparty/fsutil.py
@@ -7,7 +7,7 @@ import os
import re
import time
-from .__init__ import ANYWIN, MACOS
+from .__init__ import ANYWIN, FREEBSD, MACOS, UNIX
from .authsrv import AXS, VFS, AuthSrv
from .bos import bos
from .util import chkcmd, json_hesc, min_ex, undot
@@ -88,7 +88,7 @@ class Fstab(object):
def _from_sp_mount(self) -> dict[str, str]:
sptn = r"^.*? on (.*) type ([^ ]+) \(.*"
- if MACOS:
+ if MACOS or FREEBSD:
sptn = r"^.*? on (.*) \(([^ ]+), .*"
ptn = re.compile(sptn)
@@ -118,7 +118,7 @@ class Fstab(object):
def build_tab(self) -> None:
self.log("inspecting mtab for changes")
- dtab = self._from_sp_mount() if MACOS else self._from_proc()
+ dtab = self._from_sp_mount() if UNIX else self._from_proc()
# keep empirically-correct values if mounttab unchanged
srctab = str(sorted(dtab.items()))
@@ -130,7 +130,7 @@ class Fstab(object):
try:
fuses = [mp for mp, fs in dtab.items() if fs == "fuseblk"]
- if not fuses or MACOS:
+ if not fuses or UNIX:
raise Exception()
try:
so, _ = chkcmd(["lsblk", "-nrfo", "FSTYPE,MOUNTPOINT"]) # centos6
diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py
index fd3802b5..8b95507b 100644
--- a/copyparty/httpcli.py
+++ b/copyparty/httpcli.py
@@ -154,7 +154,7 @@ _ = (argparse, threading)
USED4SEC = {"usedforsecurity": False} if sys.version_info > (3, 9) else {}
-ALL_COOKIES = "k304 no304 js idxh dots cppwd cppws".split()
+ALL_COOKIES = "cplng cppwd cppws dots idxh js k304 no304".split()
BADXFF = " due to dangerous misconfiguration (the http-header specified by --xff-hdr was received from an untrusted reverse-proxy)"
BADXFF2 = ". Some copyparty features are now disabled as a safety measure.\n\n\n"
@@ -1800,7 +1800,11 @@ class HttpCli(object):
topdir = {"vp": "", "st": st}
fgen: Iterable[dict[str, Any]] = []
- depth = self.headers.get("depth", "infinity").lower()
+ if stat.S_ISDIR(st.st_mode):
+ depth = self.headers.get("depth", "infinity").lower()
+ else:
+ depth = "0"
+
if depth == "infinity":
# allow depth:0 from unmapped root, but require read-axs otherwise
if not self.can_read and (self.vpath or self.asrv.vfs.realpath):
@@ -1809,12 +1813,6 @@ class HttpCli(object):
self.log(t, 3)
raise Pebkac(401, t)
- if not stat.S_ISDIR(topdir["st"].st_mode):
- t = "depth:infinity can only be used on folders; %r is 0o%o"
- t = t % ("/" + self.vpath, topdir["st"])
- self.log(t, 3)
- raise Pebkac(400, t)
-
if not self.args.dav_inf:
self.log("client wants --dav-inf", 3)
zb = b'\n'
@@ -1835,7 +1833,7 @@ class HttpCli(object):
wrap=False,
)
- elif depth == "0" or not stat.S_ISDIR(st.st_mode):
+ elif depth == "0":
if depth == "0" and not self.vpath and not vn.realpath:
# rootless server; give dummy listing
self.can_read = True
@@ -3721,12 +3719,12 @@ class HttpCli(object):
fdir = fdir_base
fname = sanitize_fn(p_file or "")
- abspath = os.path.join(fdir, fname)
suffix = "-%.6f-%s" % (time.time(), dip)
if p_file and not nullwrite:
if rnd:
fname = rand_name(fdir, fname, rnd)
+ abspath = os.path.join(fdir, fname)
open_args = {"fdir": fdir, "suffix": suffix, "vf": vfs.flags}
if "replace" in self.uparam or "replace" in self.headers:
@@ -3744,7 +3742,7 @@ class HttpCli(object):
tnam = fname = os.devnull
fdir = abspath = ""
- if xbu:
+ if xbu and abspath:
at = time.time() - lifetime
hr = runhook(
self.log,
@@ -3792,7 +3790,7 @@ class HttpCli(object):
else:
open_args["fdir"] = fdir
- if p_file and not nullwrite:
+ if abspath:
bos.makedirs(fdir, vf=vfs.flags)
# reserve destination filename
@@ -3830,6 +3828,14 @@ class HttpCli(object):
finally:
f.close()
+ self.conn.nbyte += sz
+ if not abspath:
+ files.append(
+ (sz, sha_hex, sha_b64, p_file or "(discarded)", fname, "")
+ )
+ tabspath = ""
+ continue
+
if lim:
lim.nup(self.ip)
lim.bup(self.ip, sz)
@@ -3840,15 +3846,12 @@ class HttpCli(object):
lim.chk_bup(self.ip)
lim.chk_nup(self.ip)
except:
- if not nullwrite:
- wunlink(self.log, tabspath, vfs.flags)
- wunlink(self.log, abspath, vfs.flags)
+ wunlink(self.log, tabspath, vfs.flags)
+ wunlink(self.log, abspath, vfs.flags)
fname = os.devnull
raise
- if not nullwrite:
- atomic_move(self.log, tabspath, abspath, vfs.flags)
-
+ atomic_move(self.log, tabspath, abspath, vfs.flags)
tabspath = ""
at = time.time() - lifetime
@@ -3903,9 +3906,7 @@ class HttpCli(object):
abspath = ap2
sz = bos.path.getsize(abspath)
- files.append(
- (sz, sha_hex, sha_b64, p_file or "(discarded)", fname, abspath)
- )
+ files.append((sz, sha_hex, sha_b64, p_file, fname, abspath))
dbv, vrem = vfs.get_dbv(rem)
self.conn.hsrv.broker.say(
"up2k.hash_file",
@@ -3919,7 +3920,6 @@ class HttpCli(object):
self.uname,
True,
)
- self.conn.nbyte += sz
except Pebkac:
self.parser.drop()
diff --git a/copyparty/httpconn.py b/copyparty/httpconn.py
index 03d33199..30d9d87d 100644
--- a/copyparty/httpconn.py
+++ b/copyparty/httpconn.py
@@ -22,7 +22,7 @@ from .__init__ import TYPE_CHECKING, EnvParams
from .authsrv import AuthSrv # typechk
from .httpcli import HttpCli
from .u2idx import U2idx
-from .util import HMaccas, NetMap, shut_socket
+from .util import HMaccas, NetMap, min_ex, shut_socket
if True: # pylint: disable=using-constant-test
from typing import Optional, Pattern, Union
@@ -194,12 +194,12 @@ class HttpConn(object):
except Exception as ex:
em = str(ex)
- if "ALERT_CERTIFICATE_UNKNOWN" in em:
- # android-chrome keeps doing this
- pass
+ if "ALERT_" in em:
+ self.log("client refused our TLS cert or config: " + em, c=6)
else:
- self.log("handshake\033[0m " + em, c=5)
+ t = "https-handshake failed, probably due to client:\n"
+ self.log(t + min_ex(), c=5)
return
diff --git a/copyparty/mtag.py b/copyparty/mtag.py
index 1d4df60e..65dcd9b8 100644
--- a/copyparty/mtag.py
+++ b/copyparty/mtag.py
@@ -17,6 +17,7 @@ from .util import (
FFMPEG_URL,
REKOBO_LKEY,
VF_CAREFUL,
+ expand_osenv_c,
fsenc,
gzip,
min_ex,
@@ -86,7 +87,7 @@ class MParser(object):
while True:
try:
- bp = os.path.expanduser(args)
+ bp = os.path.expanduser(expand_osenv_c(args))
if WINDOWS:
bp = uncyg(bp)
@@ -216,6 +217,7 @@ def au_unpk(
def ffprobe(
abspath: str, timeout: int = 60
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:
+ # ffprobe -hide_banner -show_streams -show_format --
cmd = [
b"ffprobe",
b"-hide_banner",
diff --git a/copyparty/smbd.py b/copyparty/smbd.py
index e0ca920c..3ae41091 100644
--- a/copyparty/smbd.py
+++ b/copyparty/smbd.py
@@ -89,13 +89,15 @@ class SMB(object):
smbserver.isInFileJail = self._is_in_file_jail
self._disarm()
- ip = next((x for x in self.args.smb_i if ":" not in x), None)
+ zs = " " if self.args.smb6 else ":"
+ ip = next((x for x in self.args.smb_i if zs not in x), None)
if not ip:
- self.log("smb", "IPv6 not supported for SMB; listening on 0.0.0.0", 3)
+ self.log("smb", "IPv6 not enabled with --smb6; listening on 0.0.0.0", 3)
ip = "0.0.0.0"
port = int(self.args.smb_port)
- srv = smbserver.SimpleSMBServer(listenAddress=ip, listenPort=port)
+ kw = {"ipv6": True} if ":" in ip else {}
+ srv = smbserver.SimpleSMBServer(listenAddress=ip, listenPort=port, **kw)
try:
if self.accs:
srv.setAuthCallback(self._auth_cb)
@@ -121,6 +123,7 @@ class SMB(object):
self.srv = srv
self.stop = srv.stop
+ ip = "[%s]" % (ip,) if kw else ip
self.log("smb", "listening @ {}:{}".format(ip, port))
def nlog(self, msg: str, c: Union[int, str] = 0) -> None:
diff --git a/copyparty/svchub.py b/copyparty/svchub.py
index d5a59722..5f7441c3 100644
--- a/copyparty/svchub.py
+++ b/copyparty/svchub.py
@@ -1133,17 +1133,23 @@ class SvcHub(object):
al.th_coversd_set = set(al.th_coversd)
for k in "c".split(" "):
+ if self.args.env_expand in (0, 2):
+ break
+
vl = getattr(al, k)
if not vl:
continue
- vl = [os.path.expandvars(os.path.expanduser(x)) for x in vl]
+ vl = [os.path.expanduser(self.args.shenvexp(x)) for x in vl]
setattr(al, k, vl)
for k in "lo hist dbpath ssl_log".split(" "):
+ if self.args.env_expand in (0, 2):
+ break
+
vs = getattr(al, k)
if vs:
- vs = os.path.expandvars(os.path.expanduser(vs))
+ vs = os.path.expanduser(self.args.shenvexp(vs))
setattr(al, k, vs)
for k in "idp_adm stats_u".split(" "):
diff --git a/copyparty/tcpsrv.py b/copyparty/tcpsrv.py
index c28406bf..73fc7448 100644
--- a/copyparty/tcpsrv.py
+++ b/copyparty/tcpsrv.py
@@ -7,7 +7,7 @@ import socket
import sys
import time
-from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
+from .__init__ import ANYWIN, OPENBSD, PY2, TYPE_CHECKING, UNIX, unicode
from .cert import gencert
from .qrkode import QrCode, qr2png, qr2svg, qr2txt, qrgen
from .util import (
@@ -21,6 +21,7 @@ from .util import (
VF_CAREFUL,
Netdev,
atomic_move,
+ chkcmd,
get_adapters,
min_ex,
sunpack,
@@ -510,6 +511,13 @@ class TcpSrv(object):
return eps
def _extdevs_nix(self) -> Generator[str, None, None]:
+ if UNIX:
+ so, _ = chkcmd(["netstat", "-nrf", "inet"])
+ for ln in so.split("\n"):
+ if not ln.startswith("default"):
+ continue
+ yield ln.split()[7] if OPENBSD else ln.split()[3]
+ return
with open("/proc/net/route", "rb") as f:
next(f)
for ln in f:
diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py
index 93ce8c22..eb039e3f 100644
--- a/copyparty/th_srv.py
+++ b/copyparty/th_srv.py
@@ -1219,6 +1219,13 @@ class ThumbSrv(object):
self.log("conv2 %s [%s]" % (container, enc), 6)
benc = enc.encode("ascii").split(b" ")
+ ac = b"2"
+ try:
+ if tags["chs"][1] in ("mono", "1", "1.0"):
+ ac = b"1"
+ except:
+ pass
+
# fmt: off
cmd = [
b"ffmpeg",
@@ -1228,6 +1235,7 @@ class ThumbSrv(object):
b"-i", fsenc(abspath),
] + tagset + [
b"-map", b"0:a:0",
+ b"-ac", ac,
] + benc + [
b"-f", container,
fsenc(tpath)
@@ -1268,6 +1276,7 @@ class ThumbSrv(object):
b"-i", fsenc(abspath),
b"-map_metadata", b"-1",
b"-map", b"0:a:0",
+ b"-ac", b"2",
] + benc + [
b"-f", b"opus",
fsenc(tmp_opus)
diff --git a/copyparty/util.py b/copyparty/util.py
index 656a329f..c8a8dfc1 100644
--- a/copyparty/util.py
+++ b/copyparty/util.py
@@ -491,13 +491,12 @@ font woff woff2 otf ttf
for v in vs.strip().split():
MIMES[v] = "{}/{}".format(k, v)
- for ln in """text md=plain txt=plain js=javascript
+ for ln in """text md=plain js=javascript ass=plain ssa=plain txt=plain
application 7z=x-7z-compressed tar=x-tar bz2=x-bzip2 gz=gzip rar=x-rar-compressed zst=zstd xz=x-xz lz=lzip cpio=x-cpio
application msi=x-ms-installer cab=vnd.ms-cab-compressed rpm=x-rpm crx=x-chrome-extension
application epub=epub+zip mobi=x-mobipocket-ebook lit=x-ms-reader rss=rss+xml atom=atom+xml torrent=x-bittorrent
application p7s=pkcs7-signature dcm=dicom shx=vnd.shx shp=vnd.shp dbf=x-dbf gml=gml+xml gpx=gpx+xml amf=x-amf
application swf=x-shockwave-flash m3u=vnd.apple.mpegurl db3=vnd.sqlite3 sqlite=vnd.sqlite3
-text ass=plain ssa=plain
image jpg=jpeg xpm=x-xpixmap psd=vnd.adobe.photoshop jpf=jpx tif=tiff ico=x-icon djvu=vnd.djvu
image heics=heic-sequence heifs=heif-sequence hdr=vnd.radiance svg=svg+xml
image arw=x-sony-arw cr2=x-canon-cr2 crw=x-canon-crw dcr=x-kodak-dcr dng=x-adobe-dng erf=x-epson-erf
@@ -1502,8 +1501,7 @@ class Garda(object):
return 0, ip
if ":" in ip:
- # assume /64 clients; drop 4 groups
- ip = IPv6Address(ip).exploded[:-20]
+ ip = ipnorm(ip)
if prev and self.uniq:
if self.prev.get(ip) == prev:
@@ -1564,6 +1562,43 @@ def dedent(txt: str) -> str:
return "\n".join([ln[pad:] for ln in lns])
+def expand_osenv_noop(txt) -> str:
+ return txt
+
+
+def _expand_osenv_c(txt) -> str:
+ if "${" not in txt:
+ return txt
+ zsl = txt.split("${")
+ ret = zsl[0]
+ for v in zsl[1:]:
+ if "}" not in v:
+ raise Exception("missing '}' after %r in config-value %r" % (v, txt))
+ a, b = v.split("}", 1)
+ try:
+ ret += os.environ[a] + b
+ except:
+ raise Exception("env-var %r not defined; config-value %r" % (a, txt))
+ return ret
+
+
+if os.environ.get("PRTY_NO_ENVEXPAND"):
+ expand_osenv_c = expand_osenv_noop
+ expand_osenv_s = expand_osenv_noop
+else:
+ expand_osenv_c = _expand_osenv_c
+ expand_osenv_s = os.path.expandvars
+
+
+def expand_osenv_cs(txt) -> str:
+ a = expand_osenv_c(txt)
+ b = expand_osenv_s(txt)
+ if a == b:
+ return a
+ t = "config-value %r is using the old syntax for environment-variables; choose one of the following options:\noption 1: update the config-value to the new syntax, ${VAR} instead of $VAR or %%VAR%%\noption 2: tell copyparty to allow the old syntax with global-option --env-expand 1 (risky)\noption 3: tell copyparty to only use the new syntax (and not expand this variable) with global-option --env-expand 2\noption 4: disable all environment-variable expansions with PRTY_NO_ENVEXPAND=1 or global-option --env-expand 0"
+ raise Exception(t % (txt,))
+
+
def rice_tid() -> str:
tid = threading.current_thread().ident
c = sunpack(b"B" * 5, spack(b">Q", tid)[-5:])
@@ -2446,8 +2481,8 @@ def odfusion(
def ipnorm(ip: str) -> str:
if ":" in ip:
- # assume /64 clients; drop 4 groups
- return IPv6Address(ip).exploded[:-20]
+ # assume /56 clients; drop final 72 bits
+ return str(IPv6Network(ip + "/56", strict=False).network_address)
return ip
@@ -3849,7 +3884,7 @@ def _parsehook(
argv = cmd.split(",") if "," in cmd else [cmd]
- argv[0] = os.path.expandvars(os.path.expanduser(argv[0]))
+ argv[0] = os.path.expanduser(expand_osenv_c(argv[0]))
return areq, chk, imp, fork, sin, jtxt, wait, sp_ka, argv
@@ -4192,7 +4227,7 @@ def loadpy(ap: str, hot: bool) -> Any:
depending on what other inconveniently named files happen
to be in the same folder
"""
- ap = os.path.expandvars(os.path.expanduser(ap))
+ ap = os.path.expanduser(expand_osenv_c(ap))
mdir, mfile = os.path.split(absreal(ap))
mname = mfile.rsplit(".", 1)[0]
sys.path.insert(0, mdir)
diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js
index 0b72145f..bcc2ae28 100644
--- a/copyparty/web/browser.js
+++ b/copyparty/web/browser.js
@@ -2,7 +2,7 @@
var J_BRW = 1;
-if (window.rw_edit === undefined)
+if (window.dgauto === undefined)
alert('FATAL ERROR: receiving stale data from the server; this may be due to a broken reverse-proxy (stuck cache). Try restarting copyparty and press CTRL-SHIFT-R in the browser');
var XHR = XMLHttpRequest;
@@ -232,6 +232,7 @@ if (1)
"cl_hpick": "tap on column headers to hide in the table below",
"cl_hcancel": "column hiding aborted",
"cl_rcm": "right-click menu",
+ "cl_gauto": "autogrid",
"ct_grid": '田 the grid',
"ct_ttips": '◔ ◡ ◔">ℹ️ tooltips',
@@ -287,6 +288,8 @@ if (1)
"tt_dynt": "autogrow as tree expands",
"tt_wrap": "word wrap",
"tt_hover": "reveal overflowing lines on hover$N( breaks scrolling unless mouse $N cursor is in the left gutter )",
+ "tt_gauto": "display as grid or list depending on folder contents",
+ "tt_gathr": "use grid if this percentage of files are pics/vids",
"ml_pmode": "at end of folder...",
"ml_btns": "cmds",
@@ -720,6 +723,54 @@ var L = Ls[lang] || Ls.eng, LANGS = [];
for (var a = 0; a < LANGN.length; a++)
LANGS.push(LANGN[a][0]);
+if (window.glang && navigator.languages && !/\bcplng=/.test(document.cookie))
+ (function() {
+ var lmap = [
+ ["eng", /^en/i],
+ ["nor", /^n[ob]/i],
+ ["chi", /^zh-cn/i],
+ ["cze", /^cs/i],
+ ["deu", /^de/i],
+ ["epo", /^eo/i],
+ ["fin", /^fi/i],
+ ["fra", /^fr/i],
+ ["grc", /^el/i],
+ ["hun", /^hu/i],
+ ["ita", /^it/i],
+ ["jpn", /^ja/i],
+ ["kor", /^ko/i],
+ ["nld", /^nl/i],
+ ["nno", /^nn/i],
+ ["pol", /^pl/i],
+ ["por", /^pt/i],
+ ["rus", /^ru/i],
+ ["spa", /^es/i],
+ ["swe", /^sv/i],
+ ["tur", /^tr/i],
+ ["ukr", /^uk/i],
+ ["vie", /^vi/i],
+ ];
+ for (var a = 0; a < navigator.languages.length; a++) {
+ for (var b = 0; b < lmap.length; b++) {
+ var n = lmap[b][0];
+ if (!lmap[b][1].test(navigator.languages[a]) || !has(LANGS, n))
+ continue;
+
+ if (Ls[n]) {
+ lang = n;
+ L = Ls[n];
+ return;
+ }
+ if (window.stop)
+ window.stop();
+ document.body.innerHTML = 'Loading ' + n;
+ setck("cplng=" + n, location.reload.bind(location));
+ crashed = true;
+ throw 1;
+ }
+ }
+ })();
+
function langtest() {
var n = LANGS.length - 1;
@@ -728,7 +779,9 @@ function langtest() {
}
function langtest2() {
for (var a = 0; a < LANGS.length; a++) {
+ if (!Ls[LANGS[a]]) continue;
for (var b = a + 1; b < LANGS.length; b++) {
+ if (!Ls[LANGS[b]]) continue;
var i1 = Object.keys(Ls[LANGS[a]]).length > Object.keys(Ls[LANGS[b]]).length ? a : b,
i2 = i1 == a ? b : a,
t1 = Ls[LANGS[i1]],
@@ -742,6 +795,7 @@ for (var a = 0; a < LANGS.length; a++) {
}
}
}
+langtest2();
@@ -809,12 +863,7 @@ function mktemp(is_dir) {
sendit(input.value);
// Chrome blurs elements when calling remove for some reason
input.onblur = null;
- try{
- row.remove();
- }
- catch(e){
- console.log(e);
- }
+ row.remove();
};
input.onkeydown = function(e) {
if (e.key == "Enter")
@@ -1172,6 +1221,13 @@ ebi('op_cfg').innerHTML = (
'\n' +
(!MOBILE ? '
🖱️ ' + L.cl_rcm + '
\n' +
+ '
\n' +
'
🔢 ' + L.cl_hfsz + '
\n' +
'