Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe700dcf1a | ||
|
|
c8e3ed3aae | ||
|
|
b8733653a3 | ||
|
|
b772a4f8bb | ||
|
|
9e5253ef87 | ||
|
|
7b94e4edf3 | ||
|
|
da26ec36ca | ||
|
|
443acf2f8b | ||
|
|
6c90e3893d |
12
README.md
12
README.md
@@ -15,6 +15,7 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
|
||||
* top
|
||||
* [quickstart](#quickstart) - just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** -- that's it! 🎉
|
||||
* [at home](#at-home) - make it accessible over the internet
|
||||
* [on servers](#on-servers) - you may also want these, especially on servers
|
||||
* [features](#features)
|
||||
* [testimonials](#testimonials) - small collection of user feedback
|
||||
@@ -147,6 +148,17 @@ some recommended options:
|
||||
* see [accounts and volumes](#accounts-and-volumes) (or `--help-accounts`) for the syntax and other permissions
|
||||
|
||||
|
||||
### at home
|
||||
|
||||
make it accessible over the internet by starting a [cloudflare quicktunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/) like so:
|
||||
|
||||
first download [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) and then start the tunnel with `cloudflared tunnel --url http://127.0.0.1:3923`
|
||||
|
||||
as the tunnel starts, it will show a URL which you can share to let anyone browse your stash or upload files to you
|
||||
|
||||
since people will be connecting through cloudflare, run copyparty with `--xff-hdr cf-connecting-ip` to detect client IPs correctly
|
||||
|
||||
|
||||
### on servers
|
||||
|
||||
you may also want these, especially on servers:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: icxes <dev.null@need.moe>
|
||||
pkgname=copyparty
|
||||
pkgver="1.12.0"
|
||||
pkgver="1.12.1"
|
||||
pkgrel=1
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||
arch=("any")
|
||||
@@ -21,7 +21,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
|
||||
)
|
||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||
backup=("etc/${pkgname}.d/init" )
|
||||
sha256sums=("20d06469924b77f80f104d75eaebbb6ffa8aa4e5701b78f7264c4ce435768e5e")
|
||||
sha256sums=("c247de98727bc28aef4f696850c52b4816530847d839f5ff8451a8313bb7f983")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.12.0/copyparty-sfx.py",
|
||||
"version": "1.12.0",
|
||||
"hash": "sha256-Y18pCX/U0POgEH+wcs8Sf7c4lSUYu6bCtKGzIXa/X0M="
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.12.1/copyparty-sfx.py",
|
||||
"version": "1.12.1",
|
||||
"hash": "sha256-EkkLOaGZPbu9NJyCoSTj6yso9wcAbNAk23TiT96QYJ8="
|
||||
}
|
||||
@@ -858,6 +858,7 @@ def add_fs(ap):
|
||||
ap2 = ap.add_argument_group("filesystem options")
|
||||
rm_re_def = "5/0.1" if ANYWIN else "0/0"
|
||||
ap2.add_argument("--rm-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be deleted because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=rm_retry)")
|
||||
ap2.add_argument("--mv-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be renamed because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=mv_retry)")
|
||||
ap2.add_argument("--iobuf", metavar="BYTES", type=int, default=256*1024, help="file I/O buffer-size; if your volumes are on a network drive, try increasing to \033[32m524288\033[0m or even \033[32m4194304\033[0m (and let me know if that improves your performance)")
|
||||
|
||||
|
||||
@@ -949,6 +950,8 @@ def add_auth(ap):
|
||||
ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control")
|
||||
ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present")
|
||||
ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m")
|
||||
ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app")
|
||||
ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins")
|
||||
|
||||
|
||||
def add_zeroconf(ap):
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (1, 12, 1)
|
||||
VERSION = (1, 12, 2)
|
||||
CODENAME = "locksmith"
|
||||
BUILD_DT = (2024, 4, 9)
|
||||
BUILD_DT = (2024, 4, 12)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
@@ -1764,13 +1764,14 @@ class AuthSrv(object):
|
||||
if k in vol.flags:
|
||||
vol.flags[k] = float(vol.flags[k])
|
||||
|
||||
try:
|
||||
zs1, zs2 = vol.flags["rm_retry"].split("/")
|
||||
vol.flags["rm_re_t"] = float(zs1)
|
||||
vol.flags["rm_re_r"] = float(zs2)
|
||||
except:
|
||||
t = 'volume "/%s" has invalid rm_retry [%s]'
|
||||
raise Exception(t % (vol.vpath, vol.flags.get("rm_retry")))
|
||||
for k in ("mv_re", "rm_re"):
|
||||
try:
|
||||
zs1, zs2 = vol.flags[k + "try"].split("/")
|
||||
vol.flags[k + "_t"] = float(zs1)
|
||||
vol.flags[k + "_r"] = float(zs2)
|
||||
except:
|
||||
t = 'volume "/%s" has invalid %stry [%s]'
|
||||
raise Exception(t % (vol.vpath, k, vol.flags.get(k + "try")))
|
||||
|
||||
for k1, k2 in IMPLICATIONS:
|
||||
if k1 in vol.flags:
|
||||
|
||||
@@ -6,7 +6,8 @@ import os
|
||||
import shutil
|
||||
import time
|
||||
|
||||
from .util import Netdev, runcmd
|
||||
from .__init__ import ANYWIN
|
||||
from .util import Netdev, runcmd, wrename, wunlink
|
||||
|
||||
HAVE_CFSSL = True
|
||||
|
||||
@@ -14,6 +15,12 @@ if True: # pylint: disable=using-constant-test
|
||||
from .util import RootLogger
|
||||
|
||||
|
||||
if ANYWIN:
|
||||
VF = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1}
|
||||
else:
|
||||
VF = {"mv_re_t": 0, "rm_re_t": 0}
|
||||
|
||||
|
||||
def ensure_cert(log: "RootLogger", args) -> None:
|
||||
"""
|
||||
the default cert (and the entire TLS support) is only here to enable the
|
||||
@@ -105,8 +112,12 @@ def _gen_ca(log: "RootLogger", args):
|
||||
raise Exception("failed to translate ca-cert: {}, {}".format(rc, se), 3)
|
||||
|
||||
bname = os.path.join(args.crt_dir, "ca")
|
||||
os.rename(bname + "-key.pem", bname + ".key")
|
||||
os.unlink(bname + ".csr")
|
||||
try:
|
||||
wunlink(log, bname + ".key", VF)
|
||||
except:
|
||||
pass
|
||||
wrename(log, bname + "-key.pem", bname + ".key", VF)
|
||||
wunlink(log, bname + ".csr", VF)
|
||||
|
||||
log("cert", "new ca OK", 2)
|
||||
|
||||
@@ -185,11 +196,11 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
||||
|
||||
bname = os.path.join(args.crt_dir, "srv")
|
||||
try:
|
||||
os.unlink(bname + ".key")
|
||||
wunlink(log, bname + ".key", VF)
|
||||
except:
|
||||
pass
|
||||
os.rename(bname + "-key.pem", bname + ".key")
|
||||
os.unlink(bname + ".csr")
|
||||
wrename(log, bname + "-key.pem", bname + ".key", VF)
|
||||
wunlink(log, bname + ".csr", VF)
|
||||
|
||||
with open(os.path.join(args.crt_dir, "ca.pem"), "rb") as f:
|
||||
ca = f.read()
|
||||
|
||||
@@ -63,6 +63,7 @@ def vf_vmap() -> dict[str, str]:
|
||||
"lg_sbf",
|
||||
"md_sbf",
|
||||
"nrand",
|
||||
"mv_retry",
|
||||
"rm_retry",
|
||||
"sort",
|
||||
"unlist",
|
||||
@@ -214,6 +215,7 @@ flagcats = {
|
||||
"dots": "allow all users with read-access to\nenable the option to show dotfiles in listings",
|
||||
"fk=8": 'generates per-file accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes',
|
||||
"fka=8": 'generates slightly weaker per-file accesskeys,\nwhich are then required at the "g" permission;\nnot affected by filesize or inode numbers',
|
||||
"mv_retry": "ms-windows: timeout for renaming busy files",
|
||||
"rm_retry": "ms-windows: timeout for deleting busy files",
|
||||
"davauth": "ask webdav clients to login for all folders",
|
||||
"davrt": "show lastmod time of symlink destination, not the link itself\n(note: this option is always enabled for recursive listings)",
|
||||
|
||||
@@ -89,6 +89,7 @@ from .util import (
|
||||
vjoin,
|
||||
vol_san,
|
||||
vsplit,
|
||||
wrename,
|
||||
wunlink,
|
||||
yieldfile,
|
||||
)
|
||||
@@ -443,7 +444,11 @@ class HttpCli(object):
|
||||
|
||||
zso = self.headers.get("authorization")
|
||||
bauth = ""
|
||||
if zso:
|
||||
if (
|
||||
zso
|
||||
and not self.args.no_bauth
|
||||
and (not cookie_pw or not self.args.bauth_last)
|
||||
):
|
||||
try:
|
||||
zb = zso.split(" ")[1].encode("ascii")
|
||||
zs = base64.b64decode(zb).decode("utf-8")
|
||||
@@ -1800,7 +1805,7 @@ class HttpCli(object):
|
||||
f, fn = zfw["orz"]
|
||||
|
||||
path2 = os.path.join(fdir, fn2)
|
||||
atomic_move(path, path2)
|
||||
atomic_move(self.log, path, path2, vfs.flags)
|
||||
fn = fn2
|
||||
path = path2
|
||||
|
||||
@@ -1881,7 +1886,9 @@ class HttpCli(object):
|
||||
self.reply(t.encode("utf-8"), 201, headers=h)
|
||||
return True
|
||||
|
||||
def bakflip(self, f: typing.BinaryIO, ofs: int, sz: int, sha: str) -> None:
|
||||
def bakflip(
|
||||
self, f: typing.BinaryIO, ofs: int, sz: int, sha: str, flags: dict[str, Any]
|
||||
) -> None:
|
||||
if not self.args.bak_flips or self.args.nw:
|
||||
return
|
||||
|
||||
@@ -1909,7 +1916,7 @@ class HttpCli(object):
|
||||
|
||||
if nrem:
|
||||
self.log("bakflip truncated; {} remains".format(nrem), 1)
|
||||
atomic_move(fp, fp + ".trunc")
|
||||
atomic_move(self.log, fp, fp + ".trunc", flags)
|
||||
else:
|
||||
self.log("bakflip ok", 2)
|
||||
|
||||
@@ -2175,7 +2182,7 @@ class HttpCli(object):
|
||||
|
||||
if sha_b64 != chash:
|
||||
try:
|
||||
self.bakflip(f, cstart[0], post_sz, sha_b64)
|
||||
self.bakflip(f, cstart[0], post_sz, sha_b64, vfs.flags)
|
||||
except:
|
||||
self.log("bakflip failed: " + min_ex())
|
||||
|
||||
@@ -2527,7 +2534,7 @@ class HttpCli(object):
|
||||
raise
|
||||
|
||||
if not nullwrite:
|
||||
atomic_move(tabspath, abspath)
|
||||
atomic_move(self.log, tabspath, abspath, vfs.flags)
|
||||
|
||||
tabspath = ""
|
||||
|
||||
@@ -2767,7 +2774,7 @@ class HttpCli(object):
|
||||
hidedir(dp)
|
||||
except:
|
||||
pass
|
||||
bos.rename(fp, os.path.join(mdir, ".hist", mfile2))
|
||||
wrename(self.log, fp, os.path.join(mdir, ".hist", mfile2), vfs.flags)
|
||||
|
||||
assert self.parser.gen
|
||||
p_field, _, p_data = next(self.parser.gen)
|
||||
|
||||
@@ -424,6 +424,12 @@ class SvcHub(object):
|
||||
t = "WARNING: found config files in [%s]: %s\n config files are not expected here, and will NOT be loaded (unless your setup is intentionally hella funky)"
|
||||
self.log("root", t % (E.cfg, ", ".join(hits)), 3)
|
||||
|
||||
if self.args.no_bauth:
|
||||
t = "WARNING: --no-bauth disables support for the Android app; you may want to use --bauth-last instead"
|
||||
self.log("root", t, 3)
|
||||
if self.args.bauth_last:
|
||||
self.log("root", "WARNING: ignoring --bauth-last due to --no-bauth", 3)
|
||||
|
||||
def _process_config(self) -> bool:
|
||||
al = self.args
|
||||
|
||||
@@ -544,6 +550,13 @@ class SvcHub(object):
|
||||
except:
|
||||
raise Exception("invalid --rm-retry [%s]" % (self.args.rm_retry,))
|
||||
|
||||
try:
|
||||
zf1, zf2 = self.args.mv_retry.split("/")
|
||||
self.args.mv_re_t = float(zf1)
|
||||
self.args.mv_re_r = float(zf2)
|
||||
except:
|
||||
raise Exception("invalid --mv-retry [%s]" % (self.args.mv_retry,))
|
||||
|
||||
return True
|
||||
|
||||
def _ipa2re(self, txt) -> Optional[re.Pattern]:
|
||||
|
||||
@@ -28,6 +28,7 @@ from .util import (
|
||||
runcmd,
|
||||
statdir,
|
||||
vsplit,
|
||||
wrename,
|
||||
wunlink,
|
||||
)
|
||||
|
||||
@@ -346,7 +347,7 @@ class ThumbSrv(object):
|
||||
pass
|
||||
|
||||
try:
|
||||
bos.rename(ttpath, tpath)
|
||||
wrename(self.log, ttpath, tpath, vn.flags)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@@ -91,6 +91,9 @@ CV_EXTS = set(zsg.split(","))
|
||||
HINT_HISTPATH = "you could try moving the database to another location (preferably an SSD or NVME drive) using either the --hist argument (global option for all volumes), or the hist volflag (just for this volume)"
|
||||
|
||||
|
||||
VF_CAREFUL = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1}
|
||||
|
||||
|
||||
class Dbw(object):
|
||||
def __init__(self, c: "sqlite3.Cursor", n: int, t: float) -> None:
|
||||
self.c = c
|
||||
@@ -869,7 +872,7 @@ class Up2k(object):
|
||||
ft = "\033[0;32m{}{:.0}"
|
||||
ff = "\033[0;35m{}{:.0}"
|
||||
fv = "\033[0;36m{}:\033[90m{}"
|
||||
fx = set(("html_head", "rm_re_t", "rm_re_r"))
|
||||
fx = set(("html_head", "rm_re_t", "rm_re_r", "mv_re_t", "mv_re_r"))
|
||||
fd = vf_bmap()
|
||||
fd.update(vf_cmap())
|
||||
fd.update(vf_vmap())
|
||||
@@ -3044,12 +3047,11 @@ class Up2k(object):
|
||||
t = "finish_upload {} with remaining chunks {}"
|
||||
raise Pebkac(500, t.format(wark, job["need"]))
|
||||
|
||||
# self.log("--- " + wark + " " + dst + " finish_upload atomic " + dst, 4)
|
||||
atomic_move(src, dst)
|
||||
|
||||
upt = job.get("at") or time.time()
|
||||
vflags = self.flags[ptop]
|
||||
|
||||
atomic_move(self.log, src, dst, vflags)
|
||||
|
||||
times = (int(time.time()), int(job["lmod"]))
|
||||
self.log(
|
||||
"no more chunks, setting times {} ({}) on {}".format(
|
||||
@@ -3653,7 +3655,7 @@ class Up2k(object):
|
||||
self._symlink(dlink, dabs, dvn.flags, lmod=ftime)
|
||||
wunlink(self.log, sabs, svn.flags)
|
||||
else:
|
||||
atomic_move(sabs, dabs)
|
||||
atomic_move(self.log, sabs, dabs, svn.flags)
|
||||
|
||||
except OSError as ex:
|
||||
if ex.errno != errno.EXDEV:
|
||||
@@ -3830,8 +3832,7 @@ class Up2k(object):
|
||||
self.log("linkswap [{}] and [{}]".format(sabs, slabs))
|
||||
mt = bos.path.getmtime(slabs, False)
|
||||
flags = self.flags.get(ptop) or {}
|
||||
wunlink(self.log, slabs, flags)
|
||||
bos.rename(sabs, slabs)
|
||||
atomic_move(self.log, sabs, slabs, flags)
|
||||
bos.utime(slabs, (int(time.time()), int(mt)), False)
|
||||
self._symlink(slabs, sabs, flags, False)
|
||||
full[slabs] = (ptop, rem)
|
||||
@@ -4142,7 +4143,7 @@ class Up2k(object):
|
||||
with gzip.GzipFile(path2, "wb") as f:
|
||||
f.write(j)
|
||||
|
||||
atomic_move(path2, path)
|
||||
atomic_move(self.log, path2, path, VF_CAREFUL)
|
||||
|
||||
self.log("snap: {} |{}|".format(path, len(reg.keys())))
|
||||
self.snap_prev[ptop] = etag
|
||||
|
||||
@@ -2125,26 +2125,29 @@ def lsof(log: "NamedLogger", abspath: str) -> None:
|
||||
log("lsof failed; " + min_ex(), 3)
|
||||
|
||||
|
||||
def atomic_move(usrc: str, udst: str) -> None:
|
||||
src = fsenc(usrc)
|
||||
dst = fsenc(udst)
|
||||
if not PY2:
|
||||
os.replace(src, dst)
|
||||
def _fs_mvrm(
|
||||
log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any]
|
||||
) -> bool:
|
||||
bsrc = fsenc(src)
|
||||
bdst = fsenc(dst)
|
||||
if atomic:
|
||||
k = "mv_re_"
|
||||
act = "atomic-rename"
|
||||
osfun = os.replace
|
||||
args = [bsrc, bdst]
|
||||
elif dst:
|
||||
k = "mv_re_"
|
||||
act = "rename"
|
||||
osfun = os.rename
|
||||
args = [bsrc, bdst]
|
||||
else:
|
||||
if os.path.exists(dst):
|
||||
os.unlink(dst)
|
||||
k = "rm_re_"
|
||||
act = "delete"
|
||||
osfun = os.unlink
|
||||
args = [bsrc]
|
||||
|
||||
os.rename(src, dst)
|
||||
|
||||
|
||||
def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
|
||||
maxtime = flags.get("rm_re_t", 0.0)
|
||||
bpath = fsenc(abspath)
|
||||
if not maxtime:
|
||||
os.unlink(bpath)
|
||||
return True
|
||||
|
||||
chill = flags.get("rm_re_r", 0.0)
|
||||
maxtime = flags.get(k + "t", 0.0)
|
||||
chill = flags.get(k + "r", 0.0)
|
||||
if chill < 0.001:
|
||||
chill = 0.1
|
||||
|
||||
@@ -2152,14 +2155,19 @@ def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
|
||||
t0 = now = time.time()
|
||||
for attempt in range(90210):
|
||||
try:
|
||||
if ino and os.stat(bpath).st_ino != ino:
|
||||
log("inode changed; aborting delete")
|
||||
if ino and os.stat(bsrc).st_ino != ino:
|
||||
t = "src inode changed; aborting %s %s"
|
||||
log(t % (act, src), 1)
|
||||
return False
|
||||
os.unlink(bpath)
|
||||
if (dst and not atomic) and os.path.exists(bdst):
|
||||
t = "something appeared at dst; aborting rename [%s] ==> [%s]"
|
||||
log(t % (src, dst), 1)
|
||||
return False
|
||||
osfun(*args)
|
||||
if attempt:
|
||||
now = time.time()
|
||||
t = "deleted in %.2f sec, attempt %d"
|
||||
log(t % (now - t0, attempt + 1))
|
||||
t = "%sd in %.2f sec, attempt %d: %s"
|
||||
log(t % (act, now - t0, attempt + 1, src))
|
||||
return True
|
||||
except OSError as ex:
|
||||
now = time.time()
|
||||
@@ -2169,15 +2177,45 @@ def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
|
||||
raise
|
||||
if not attempt:
|
||||
if not PY2:
|
||||
ino = os.stat(bpath).st_ino
|
||||
t = "delete failed (err.%d); retrying for %d sec: %s"
|
||||
log(t % (ex.errno, maxtime + 0.99, abspath))
|
||||
ino = os.stat(bsrc).st_ino
|
||||
t = "%s failed (err.%d); retrying for %d sec: [%s]"
|
||||
log(t % (act, ex.errno, maxtime + 0.99, src))
|
||||
|
||||
time.sleep(chill)
|
||||
|
||||
return False # makes pylance happy
|
||||
|
||||
|
||||
def atomic_move(log: "NamedLogger", src: str, dst: str, flags: dict[str, Any]) -> None:
|
||||
bsrc = fsenc(src)
|
||||
bdst = fsenc(dst)
|
||||
if PY2:
|
||||
if os.path.exists(bdst):
|
||||
_fs_mvrm(log, dst, "", False, flags) # unlink
|
||||
|
||||
_fs_mvrm(log, src, dst, False, flags) # rename
|
||||
elif flags.get("mv_re_t"):
|
||||
_fs_mvrm(log, src, dst, True, flags)
|
||||
else:
|
||||
os.replace(bsrc, bdst)
|
||||
|
||||
|
||||
def wrename(log: "NamedLogger", src: str, dst: str, flags: dict[str, Any]) -> bool:
|
||||
if not flags.get("mv_re_t"):
|
||||
os.rename(fsenc(src), fsenc(dst))
|
||||
return True
|
||||
|
||||
return _fs_mvrm(log, src, dst, False, flags)
|
||||
|
||||
|
||||
def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
|
||||
if not flags.get("rm_re_t"):
|
||||
os.unlink(fsenc(abspath))
|
||||
return True
|
||||
|
||||
return _fs_mvrm(log, abspath, "", False, flags)
|
||||
|
||||
|
||||
def get_df(abspath: str) -> tuple[Optional[int], Optional[int]]:
|
||||
try:
|
||||
# some fuses misbehave
|
||||
|
||||
@@ -1600,6 +1600,7 @@ html {
|
||||
padding: .2em .4em;
|
||||
font-size: 1.2em;
|
||||
margin: .2em;
|
||||
display: inline-block;
|
||||
white-space: pre;
|
||||
position: relative;
|
||||
top: -.12em;
|
||||
@@ -1773,9 +1774,6 @@ html.y #tree.nowrap .ntree a+a:hover {
|
||||
margin: .7em 0 .7em .5em;
|
||||
padding-left: .5em;
|
||||
}
|
||||
.opwide>div>div>a {
|
||||
line-height: 2em;
|
||||
}
|
||||
.opwide>div>h3 {
|
||||
color: var(--fg-weak);
|
||||
margin: 0 .4em;
|
||||
|
||||
@@ -1539,16 +1539,18 @@ var mpl = (function () {
|
||||
set_tint();
|
||||
|
||||
r.acode = function (url) {
|
||||
var c = true;
|
||||
var c = true,
|
||||
cs = url.split('?')[0];
|
||||
|
||||
if (!have_acode)
|
||||
c = false;
|
||||
else if (/\.(wav|flac)$/i.exec(url))
|
||||
else if (/\.(wav|flac)$/i.exec(cs))
|
||||
c = r.ac_flac;
|
||||
else if (/\.(aac|m4a)$/i.exec(url))
|
||||
else if (/\.(aac|m4a)$/i.exec(cs))
|
||||
c = r.ac_aac;
|
||||
else if (/\.(ogg|opus)$/i.exec(url) && !can_ogg)
|
||||
else if (/\.(ogg|opus)$/i.exec(cs) && !can_ogg)
|
||||
c = true;
|
||||
else if (re_au_native.exec(url))
|
||||
else if (re_au_native.exec(cs))
|
||||
c = false;
|
||||
|
||||
if (!c)
|
||||
|
||||
@@ -190,11 +190,21 @@ input {
|
||||
padding: .5em .7em;
|
||||
margin: 0 .5em 0 0;
|
||||
}
|
||||
input::placeholder {
|
||||
font-size: 1.2em;
|
||||
font-style: italic;
|
||||
letter-spacing: .04em;
|
||||
opacity: 0.64;
|
||||
color: #930;
|
||||
}
|
||||
html.z input {
|
||||
color: #fff;
|
||||
background: #626;
|
||||
border-color: #c2c;
|
||||
}
|
||||
html.z input::placeholder {
|
||||
color: #fff;
|
||||
}
|
||||
html.z .num {
|
||||
border-color: #777;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
<div>
|
||||
<form method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}">
|
||||
<input type="hidden" name="act" value="login" />
|
||||
<input type="password" name="cppwd" />
|
||||
<input type="password" name="cppwd" placeholder=" password" />
|
||||
<input type="submit" value="Login" />
|
||||
{% if ahttps %}
|
||||
<a id="w" href="{{ ahttps }}">switch to https</a>
|
||||
|
||||
@@ -148,6 +148,8 @@ try {
|
||||
hook('error');
|
||||
}
|
||||
catch (ex) {
|
||||
if (console.stdlog)
|
||||
console.log = console.stdlog;
|
||||
console.log('console capture failed', ex);
|
||||
}
|
||||
var crashed = false, ignexd = {}, evalex_fatal = false;
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0409-2334 `v1.12.1` scrolling stuff
|
||||
|
||||
## new features
|
||||
|
||||
* while viewing pictures/videos, the scrollwheel can be used to view the prev/next file 844d16b9
|
||||
|
||||
## bugfixes
|
||||
|
||||
* #81 (scrolling suddenly getting disabled) properly fixed after @icxes found another way to reproduce it (thx) 4f0cad54
|
||||
* and fixed at least one javascript glitch introduced in v1.12.0 while adding dirkeys 989cc613
|
||||
* directory tree sidebar could fail to render when popping browser history into the lightbox
|
||||
|
||||
## other changes
|
||||
|
||||
* music preloader is slightly less hyper f89de6b3
|
||||
* u2c.exe: updated TLS-certs and deps ab18893c
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0406-2011 `v1.12.0` locksmith
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
* [complete release](#complete-release)
|
||||
* [debugging](#debugging)
|
||||
* [music playback halting on phones](#music-playback-halting-on-phones) - mostly fine on android
|
||||
* [todo](#todo) - roughly sorted by priority
|
||||
* [discarded ideas](#discarded-ideas)
|
||||
|
||||
|
||||
@@ -312,17 +311,11 @@ mostly fine on android, but still haven't find a way to massage iphones into be
|
||||
* conditionally starting/stopping mp.fau according to mp.au.readyState <3 or <4 doesn't help
|
||||
* loop=true doesn't work, and manually looping mp.fau from an onended also doesn't work (it does nothing)
|
||||
* assigning fau.currentTime in a timer doesn't work, as safari merely pretends to assign it
|
||||
* on ios 16.7.7, mp.fau can sometimes make everything visibly work correctly, but no audio is actually hitting the speakers
|
||||
|
||||
can be reproduced with `--no-sendfile --s-wr-sz 8192 --s-wr-slp 0.3 --rsp-slp 6` and then play a collection of small audio files with the screen off, `ffmpeg -i track01.cdda.flac -c:a libopus -b:a 128k -segment_time 12 -f segment smol-%02d.opus`
|
||||
|
||||
|
||||
# todo
|
||||
|
||||
roughly sorted by priority
|
||||
|
||||
* nothing! currently
|
||||
|
||||
|
||||
## discarded ideas
|
||||
|
||||
* optimization attempts which didn't improve performance
|
||||
|
||||
129
docs/nuitka.txt
129
docs/nuitka.txt
@@ -1,82 +1,71 @@
|
||||
# recipe for building an exe with nuitka (extreme jank edition)
|
||||
#
|
||||
# NOTE: win7 and win10 builds both work on win10 but
|
||||
# on win7 they immediately c0000005 in kernelbase.dll
|
||||
#
|
||||
# first install python-3.6.8-amd64.exe
|
||||
# [x] add to path
|
||||
#
|
||||
|
||||
NOTE: copyparty runs SLOWER when compiled with nuitka;
|
||||
just use copyparty-sfx.py and/or pyinstaller instead
|
||||
|
||||
( the sfx and the pyinstaller EXEs are equally fast if you
|
||||
have the latest jinja2 installed, but the older jinja that
|
||||
comes bundled with the sfx is slightly faster yet )
|
||||
|
||||
roughly, copyparty-sfx.py is 6% faster than copyparty.exe
|
||||
(win10-pyinstaller), and copyparty.exe is 10% faster than
|
||||
nuitka, making copyparty-sfx.py 17% faster than nuitka
|
||||
|
||||
NOTE: every time a nuitka-compiled copyparty.exe is launched,
|
||||
it will show the windows firewall prompt since nuitka will
|
||||
pick a new unique location in %TEMP% to unpack an exe into,
|
||||
unlike pyinstaller which doesn't fork itself on startup...
|
||||
might be fixable by configuring nuitka differently, idk
|
||||
|
||||
NOTE: nuitka EXEs are larger than pyinstaller ones;
|
||||
a minimal nuitka build of just the sfx (with its bundled
|
||||
dependencies) was already the same size as the pyinstaller
|
||||
copyparty.exe which also includes Mutagen and Pillow
|
||||
|
||||
NOTE: nuitka takes a lot longer to build than pyinstaller
|
||||
(due to actual compilation of course, but still)
|
||||
|
||||
NOTE: binaries built with nuitka cannot run on windows7,
|
||||
even when compiled with python 3.6 on windows 7 itself
|
||||
|
||||
NOTE: `--python-flags=-m` is the magic sauce to
|
||||
correctly compile `from .util import Daemon`
|
||||
(which otherwise only explodes at runtime)
|
||||
|
||||
NOTE: `--deployment` doesn't seem to affect performance
|
||||
|
||||
########################################################################
|
||||
# copypaste the rest of this file into cmd
|
||||
|
||||
rem from pypi
|
||||
cd \users\ed\downloads
|
||||
python -m pip install --user Nuitka-0.6.14.7.tar.gz
|
||||
|
||||
rem https://github.com/brechtsanders/winlibs_mingw/releases/download/10.2.0-11.0.0-8.0.0-r5/winlibs-x86_64-posix-seh-gcc-10.2.0-llvm-11.0.0-mingw-w64-8.0.0-r5.zip
|
||||
mkdir C:\Users\ed\AppData\Local\Nuitka\
|
||||
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\
|
||||
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\gcc\
|
||||
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\gcc\x86_64\
|
||||
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\gcc\x86_64\10.2.0-11.0.0-8.0.0-r5\
|
||||
copy c:\users\ed\downloads\winlibs-x86_64-posix-seh-gcc-10.2.0-llvm-11.0.0-mingw-w64-8.0.0-r5.zip C:\Users\ed\AppData\Local\Nuitka\Nuitka\gcc\x86_64\10.2.0-11.0.0-8.0.0-r5\winlibs-x86_64-posix-seh-gcc-10.2.0-llvm-11.0.0-mingw-w64-8.0.0-r5.zip
|
||||
|
||||
rem https://github.com/ccache/ccache/releases/download/v3.7.12/ccache-3.7.12-windows-32.zip
|
||||
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\ccache\
|
||||
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\ccache\v3.7.12\
|
||||
copy c:\users\ed\downloads\ccache-3.7.12-windows-32.zip C:\Users\ed\AppData\Local\Nuitka\Nuitka\ccache\v3.7.12\ccache-3.7.12-windows-32.zip
|
||||
python -m pip install --user -U nuitka
|
||||
|
||||
rem https://dependencywalker.com/depends22_x64.zip
|
||||
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\depends\
|
||||
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\depends\x86_64\
|
||||
copy c:\users\ed\downloads\depends22_x64.zip C:\Users\ed\AppData\Local\Nuitka\Nuitka\depends\x86_64\depends22_x64.zip
|
||||
cd %homedrive%
|
||||
cd %homepath%\downloads
|
||||
|
||||
cd \
|
||||
rd /s /q %appdata%\..\local\temp\pe-copyparty
|
||||
cd \users\ed\downloads
|
||||
python copyparty-sfx.py -h
|
||||
cd %appdata%\..\local\temp\pe-copyparty\copyparty
|
||||
rd /s /q copypuitka
|
||||
mkdir copypuitka
|
||||
cd copypuitka
|
||||
|
||||
python
|
||||
import os, re
|
||||
os.rename('../dep-j2/jinja2', '../jinja2')
|
||||
os.rename('../dep-j2/markupsafe', '../markupsafe')
|
||||
rd /s /q %temp%\pe-copyparty
|
||||
python ..\copyparty-sfx.py --version
|
||||
|
||||
print("# nuitka dies if .__init__.stuff is imported")
|
||||
with open('__init__.py','r',encoding='utf-8') as f:
|
||||
t1 = f.read()
|
||||
move %temp%\pe-copyparty\copyparty .\
|
||||
move %temp%\pe-copyparty\partftpy .\
|
||||
move %temp%\pe-copyparty\ftp\pyftpdlib .\
|
||||
move %temp%\pe-copyparty\j2\jinja2 .\
|
||||
move %temp%\pe-copyparty\j2\markupsafe .\
|
||||
|
||||
with open('util.py','r',encoding='utf-8') as f:
|
||||
t2 = f.read().split('\n')[3:]
|
||||
rd /s /q %temp%\pe-copyparty
|
||||
|
||||
t2 = [x for x in t2 if 'from .__init__' not in x]
|
||||
t = t1 + '\n'.join(t2)
|
||||
with open('__init__.py','w',encoding='utf-8') as f:
|
||||
f.write('\n')
|
||||
python -m nuitka ^
|
||||
--onefile --deployment --python-flag=-m ^
|
||||
--include-package=markupsafe ^
|
||||
--include-package=jinja2 ^
|
||||
--include-package=partftpy ^
|
||||
--include-package=pyftpdlib ^
|
||||
--include-data-dir=copyparty\web=copyparty\web ^
|
||||
--include-data-dir=copyparty\res=copyparty\res ^
|
||||
--run copyparty
|
||||
|
||||
with open('util.py','w',encoding='utf-8') as f:
|
||||
f.write(t)
|
||||
|
||||
print("# local-imports fail, prefix module names")
|
||||
ptn = re.compile(r'^( *from )(\.[^ ]+ import .*)')
|
||||
for d, _, fs in os.walk('.'):
|
||||
for f in fs:
|
||||
fp = os.path.join(d, f)
|
||||
if not fp.endswith('.py'):
|
||||
continue
|
||||
t = ''
|
||||
with open(fp,'r',encoding='utf-8') as f:
|
||||
for ln in [x.rstrip('\r\n') for x in f]:
|
||||
m = ptn.match(ln)
|
||||
if not m:
|
||||
t += ln + '\n'
|
||||
continue
|
||||
p1, p2 = m.groups()
|
||||
t += "{}copyparty{}\n".format(p1, p2).replace("__init__", "util")
|
||||
with open(fp,'w',encoding='utf-8') as f:
|
||||
f.write(t)
|
||||
|
||||
exit()
|
||||
|
||||
cd ..
|
||||
|
||||
rd /s /q bout & python -m nuitka --standalone --onefile --windows-onefile-tempdir --python-flag=no_site --assume-yes-for-downloads --include-data-dir=copyparty\web=copyparty\web --include-data-dir=copyparty\res=copyparty\res --run --output-dir=bout --mingw64 --include-package=markupsafe --include-package=jinja2 copyparty
|
||||
|
||||
@@ -155,6 +155,7 @@ class Cfg(Namespace):
|
||||
mte={"a": True},
|
||||
mth={},
|
||||
mtp=[],
|
||||
mv_retry="0/0",
|
||||
rm_retry="0/0",
|
||||
s_rd_sz=256 * 1024,
|
||||
s_wr_sz=256 * 1024,
|
||||
|
||||
Reference in New Issue
Block a user