Compare commits

...

17 Commits

Author SHA1 Message Date
ed
4dca1cf8f4 v1.18.4 2025-07-25 18:41:05 +00:00
ed
edba7fffd3 add landmarks (#182) 2025-07-25 18:35:28 +00:00
ed
21a96bcfe8 add quickdelete option; closes #183
togglebutton in the ui switches between 2 (off/default) and
1 (on/quick) confirmations; global-option `--qdel` sets the default

setting `--qdel=0` changes the togglebutton to switch
between 1 (off/default) confirmations and 0 (on)

in other words, when the ui-button is enabled, it
always reduces the number of confirmations by one
2025-07-25 18:31:49 +00:00
ed
2d322dd48e fix unpost in new shares 2025-07-25 15:12:05 +00:00
ed
df6d4df4f8 fix filekeys on windows 2025-07-24 23:07:04 +00:00
ed
5aa893973c update pkgs to 1.18.3 2025-07-21 23:30:16 +00:00
ed
be0dd555a6 v1.18.3 2025-07-21 23:07:00 +00:00
ed
9921c43e3a add options to set default chmod (#181)
the unix-permissions of new files/folders can now be changed

* global-option --chmod-f, volflag chmod_f for files
* global-option --chmod-d, volflag chmod_d for directories

the expected value is a standard three-digit octal value
(User/Group/Other) such as 755, 750, 644, 640, etc
2025-07-21 22:46:28 +00:00
ed
14fa369fae macos fixes 2025-07-21 00:04:38 +02:00
ed
0f0f8d90c1 support --shr with --xvol; closes #179 2025-07-20 23:49:36 +02:00
ed
1afbff7335 fix some error-messages failing to render
would show a jinja-panic instead of explaining what went wrong
2025-07-20 23:39:08 +02:00
ed
8c32b0e7bb bbox: hide buttons fully; closes #180 2025-07-20 23:31:38 +02:00
ed
9bc4c5d2e6 mediaplayer: stay within search-results 2025-07-20 23:30:27 +02:00
ed
1534b7cb55 fix hotkey-help on macos 2025-07-20 23:27:44 +02:00
ed
56d3bcf515 rss: fix --rp-loc;
some rss links were malformed when combined with rp-loc
2025-07-14 03:48:27 +02:00
ed
78605d9a79 ios: force video embed
default on all other platforms, but apple thinks different
2025-07-09 14:11:45 +00:00
ed
d46a40fed8 update pkgs to 1.18.2 2025-07-07 14:29:38 +00:00
26 changed files with 681 additions and 116 deletions

View File

@@ -1605,7 +1605,7 @@ config file example:
w: * # anyone can upload here
rw: ed # only user "ed" can read-write
flags:
e2ds: # filesystem indexing is required for many of these:
e2ds # filesystem indexing is required for many of these:
sz: 1k-3m # accept upload only if filesize in this range
df: 4g # free disk space cannot go lower than this
vmaxb: 1g # volume can never exceed 1 GiB
@@ -1662,6 +1662,8 @@ this can instead be kept in a single place using the `--hist` argument, or the `
by default, the per-volume `up2k.db` sqlite3-database for `-e2d` and `-e2t` is stored next to the thumbnails according to the `--hist` option, but the global-option `--dbpath` and/or volflag `dbpath` can be used to put the database somewhere else
if your storage backend is unreliable (NFS or bad HDDs), you can specify one or more "landmarks" to look for before doing anything database-related. A landmark is a file which is always expected to exist inside the volume. This avoids spurious filesystem rescans in the event of an outage. One line per landmark (see example below)
note:
* putting the hist-folders on an SSD is strongly recommended for performance
* markdown edits are always stored in a local `.hist` subdirectory
@@ -1679,6 +1681,8 @@ config file example:
flags:
hist: - # restore the default (/mnt/nas/pics/.hist/)
hist: /mnt/nas/cache/pics/ # can be absolute path
landmark: me.jpg # /mnt/nas/pics/me.jpg must be readable to enable db
landmark: info/a.txt^=ok # and this textfile must start with "ok"
```
@@ -2364,8 +2368,10 @@ TLDR: yes
| send message | yep | yep | yep | yep | yep | yep | yep | yep |
| set sort order | - | yep | yep | yep | yep | yep | yep | yep |
| zip selection | - | yep | yep | yep | yep | yep | yep | yep |
| file search | - | yep | yep | yep | yep | yep | yep | yep |
| file rename | - | yep | yep | yep | yep | yep | yep | yep |
| file cut/paste | - | yep | yep | yep | yep | yep | yep | yep |
| unpost uploads | - | - | yep | yep | yep | yep | yep | yep |
| navpane | - | yep | yep | yep | yep | yep | yep | yep |
| image viewer | - | yep | yep | yep | yep | yep | yep | yep |
| video player | - | yep | yep | yep | yep | yep | yep | yep |

View File

@@ -1,6 +1,6 @@
# Maintainer: icxes <dev.null@need.moe>
pkgname=copyparty
pkgver="1.18.1"
pkgver="1.18.3"
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any")
@@ -22,7 +22,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=("4f8069987574a580017531ffe7d30bcfc573979cca419f9becebae6b4228525a")
sha256sums=("aa12f4779cf5c014cc9503798ac63872dac840ca91ddf122daa6befb4c883d48")
build() {
cd "${srcdir}/${pkgname}-${pkgver}"

View File

@@ -1,5 +1,5 @@
{
"url": "https://github.com/9001/copyparty/releases/download/v1.18.1/copyparty-sfx.py",
"version": "1.18.1",
"hash": "sha256-Dzz5JXvVH+nCVc8Bc0DV3UdZanzDHRnwGKJYx7N8WZc="
"url": "https://github.com/9001/copyparty/releases/download/v1.18.3/copyparty-sfx.py",
"version": "1.18.3",
"hash": "sha256-INqErls4gyhBAlDlY1vfNboKrrqHmeiyB+RAuuYRISQ="
}

View File

@@ -863,6 +863,43 @@ def get_sects():
"""
),
],
[
"chmod",
"file/folder permissions",
dedent(
"""
global-option \033[33m--chmod-f\033[0m and volflag \033[33mchmod_f\033[0m specifies the unix-permission to use when creating a new file
similarly, \033[33m--chmod-d\033[0m and \033[33mchmod_d\033[0m sets the directory/folder perm
the value is a three-digit octal number such as 755, 750, 644, etc.
first digit = "User"; permission for the unix-user
second digit = "Group"; permission for the unix-group
third digit = "Other"; permission for all other users/groups
for files:
0 = --- = no access
1 = --x = can execute the file as a program
2 = -w- = can write
3 = -wx = can write and execute
4 = r-- = can read
5 = r-x = can read and execute
6 = rw- = can read and write
7 = rwx = can read, write, execute
for directories/folders:
0 = --- = no access
1 = --x = can read files in folder but not list contents
2 = -w- = n/a
3 = -wx = can create files but not list
4 = r-- = can list, but not read/write
5 = r-x = can list and read files
6 = rw- = n/a
7 = rwx = can read, write, list
"""
),
],
[
"pwhash",
"password hashing",
@@ -1013,6 +1050,8 @@ def add_upload(ap):
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600")
ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload (bad idea to enable this on windows and/or cow filesystems)")
ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even when it might be dangerous (multiprocessing, filesystems lacking sparse-files support, ...)")
ap2.add_argument("--chmod-f", metavar="UGO", type=u, default="", help="unix file permissions to use when creating files; default is probably 644 (OS-decided), see --help-chmod. Examples: [\033[32m644\033[0m] = owner-RW + all-R, [\033[32m755\033[0m] = owner-RWX + all-RX, [\033[32m777\033[0m] = full-yolo (volflag=chmod_f)")
ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)")
ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)")
ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)")
ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)")
@@ -1049,7 +1088,7 @@ def add_network(ap):
ap2.add_argument("--rp-loc", metavar="PATH", type=u, default="", help="if reverse-proxying on a location instead of a dedicated domain/subdomain, provide the base location here; example: [\033[32m/foo/bar\033[0m]")
if ANYWIN:
ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances")
else:
elif not MACOS:
ap2.add_argument("--freebind", action="store_true", help="allow listening on IPs which do not yet exist, for example if the network interfaces haven't finished going up. Only makes sense for IPs other than '0.0.0.0', '127.0.0.1', '::', and '::1'. May require running as root (unless net.ipv6.ip_nonlocal_bind)")
ap2.add_argument("--wr-h-eps", metavar="PATH", type=u, default="", help="write list of listening-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started")
ap2.add_argument("--wr-h-aon", metavar="PATH", type=u, default="", help="write list of accessible-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started")
@@ -1511,6 +1550,7 @@ def add_ui(ap, retry):
ap2.add_argument("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)")
ap2.add_argument("--hsortn", metavar="N", type=int, default=2, help="number of sorting rules to include in media URLs by default (volflag=hsortn)")
ap2.add_argument("--see-dots", action="store_true", help="default-enable seeing dotfiles; only takes effect if user has the necessary permissions")
ap2.add_argument("--qdel", metavar="LVL", type=int, default=2, help="number of confirmations to show when deleting files (2/1/0)")
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)")
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")
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)")

View File

@@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (1, 18, 2)
VERSION = (1, 18, 4)
CODENAME = "logtail"
BUILD_DT = (2025, 7, 7)
BUILD_DT = (2025, 7, 25)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View File

@@ -120,6 +120,8 @@ class Lim(object):
self.reg: Optional[dict[str, dict[str, Any]]] = None # up2k registry
self.chmod_d = 0o755
self.nups: dict[str, list[float]] = {} # num tracker
self.bups: dict[str, list[tuple[float, int]]] = {} # byte tracker list
self.bupc: dict[str, int] = {} # byte tracker cache
@@ -280,7 +282,7 @@ class Lim(object):
if not dirs:
# no branches yet; make one
sub = os.path.join(path, "0")
bos.mkdir(sub)
bos.mkdir(sub, self.chmod_d)
else:
# try newest branch only
sub = os.path.join(path, str(dirs[-1]))
@@ -295,7 +297,7 @@ class Lim(object):
# make a branch
sub = os.path.join(path, str(dirs[-1] + 1))
bos.mkdir(sub)
bos.mkdir(sub, self.chmod_d)
ret = self.dive(sub, lvs - 1)
if ret is None:
raise Pebkac(500, "rotation bug")
@@ -372,6 +374,7 @@ class VFS(object):
self.shr_src: Optional[tuple[VFS, str]] = None # source vfs+rem of a share
self.shr_files: set[str] = set() # filenames to include from shr_src
self.shr_owner: str = "" # uname
self.shr_all_aps: list[tuple[str, list[VFS]]] = []
self.aread: dict[str, list[str]] = {}
self.awrite: dict[str, list[str]] = {}
self.amove: dict[str, list[str]] = {}
@@ -383,20 +386,20 @@ class VFS(object):
self.adot: dict[str, list[str]] = {}
self.js_ls = {}
self.js_htm = ""
self.all_vols: dict[str, VFS] = {} # flattened recursive
self.all_nodes: dict[str, VFS] = {} # also jumpvols/shares
if realpath:
rp = realpath + ("" if realpath.endswith(os.sep) else os.sep)
vp = vpath + ("/" if vpath else "")
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
self.dbpath = self.histpath
self.all_vols = {vpath: self} # flattened recursive
self.all_nodes = {vpath: self} # also jumpvols/shares
self.all_aps = [(rp, self)]
self.all_vols[vpath] = self
self.all_nodes[vpath] = self
self.all_aps = [(rp, [self])]
self.all_vps = [(vp, self)]
else:
self.histpath = self.dbpath = ""
self.all_vols = {}
self.all_nodes = {}
self.all_aps = []
self.all_vps = []
@@ -415,7 +418,7 @@ class VFS(object):
self,
vols: dict[str, "VFS"],
nodes: dict[str, "VFS"],
aps: list[tuple[str, "VFS"]],
aps: list[tuple[str, list["VFS"]]],
vps: list[tuple[str, "VFS"]],
) -> None:
nodes[self.vpath] = self
@@ -424,7 +427,11 @@ class VFS(object):
rp = self.realpath
rp += "" if rp.endswith(os.sep) else os.sep
vp = self.vpath + ("/" if self.vpath else "")
aps.append((rp, self))
hit = next((x[1] for x in aps if x[0] == rp), None)
if hit:
hit.append(self)
else:
aps.append((rp, [self]))
vps.append((vp, self))
for v in self.nodes.values():
@@ -848,9 +855,11 @@ class VFS(object):
return None
if "xvol" in self.flags:
for vap, vn in self.root.all_aps:
all_aps = self.shr_all_aps or self.root.all_aps
for vap, vns in all_aps:
if aps.startswith(vap):
return vn
return self if self in vns else vns[0]
if self.log:
self.log("vfs", "xvol: %r" % (ap,), 3)
@@ -859,6 +868,53 @@ class VFS(object):
return self
def check_landmarks(self) -> bool:
if self.dbv:
return True
vps = self.flags.get("landmark") or []
if not vps:
return True
failed = ""
for vp in vps:
if "^=" in vp:
vp, zs = vp.split("^=", 1)
expect = zs.encode("utf-8")
else:
expect = b""
if self.log:
t = "checking [/%s] landmark [%s]"
self.log("vfs", t % (self.vpath, vp), 6)
ap = "?"
try:
ap = self.canonical(vp)
with open(ap, "rb") as f:
buf = f.read(4096)
if not buf.startswith(expect):
t = "file [%s] does not start with the expected bytes %s"
failed = t % (ap, expect)
break
except Exception as ex:
t = "%r while trying to read [%s] => [%s]"
failed = t % (ex, vp, ap)
break
if not failed:
return True
if self.log:
t = "WARNING: landmark verification failed; %s; will now disable up2k database for volume [/%s]"
self.log("vfs", t % (failed, self.vpath), 3)
for rm in "e2d e2t e2v".split():
self.flags = {k: v for k, v in self.flags.items() if not k.startswith(rm)}
self.flags["d2d"] = True
self.flags["d2t"] = True
return False
if WINDOWS:
re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$")
@@ -922,6 +978,9 @@ class AuthSrv(object):
yield prev, True
def vf0(self):
return {"d2d": True, "tcolor": self.args.tcolor}
def idp_checkin(
self, broker: Optional["BrokerCli"], uname: str, gname: str
) -> bool:
@@ -1489,7 +1548,7 @@ class AuthSrv(object):
flags[name] = True
return
zs = "ext_th mtp on403 on404 xbu xau xiu xbc xac xbr xar xbd xad xm xban"
zs = "ext_th landmark mtp on403 on404 xbu xau xiu xbc xac xbr xar xbd xad xm xban"
if name not in zs.split():
if value is True:
t = "└─add volflag [{}] = {} ({})"
@@ -1628,13 +1687,12 @@ class AuthSrv(object):
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"
self.log(t, 1)
axs = AXS()
vfs = VFS(self.log_func, absreal("."), "", "", axs, {})
vfs = VFS(self.log_func, absreal("."), "", "", axs, self.vf0())
if not axs.uread:
self.badcfg1 = True
elif "" not in mount:
# there's volumes but no root; make root inaccessible
zsd = {"d2d": True, "tcolor": self.args.tcolor}
vfs = VFS(self.log_func, "", "", "", AXS(), zsd)
vfs = VFS(self.log_func, "", "", "", AXS(), self.vf0())
maxdepth = 0
for dst in sorted(mount.keys(), key=lambda x: (x.count("/"), len(x))):
@@ -1683,8 +1741,7 @@ class AuthSrv(object):
if enshare:
assert sqlite3 # type: ignore # !rm
zsd = {"d2d": True, "tcolor": self.args.tcolor}
shv = VFS(self.log_func, "", shr, shr, AXS(), zsd)
shv = VFS(self.log_func, "", shr, shr, AXS(), self.vf0())
db_path = self.args.shr_db
db = sqlite3.connect(db_path)
@@ -2066,6 +2123,7 @@ class AuthSrv(object):
all_mte = {}
errors = False
free_umask = False
for vol in vfs.all_nodes.values():
if (self.args.e2ds and vol.axs.uwrite) or self.args.e2dsa:
vol.flags["e2ds"] = True
@@ -2122,6 +2180,27 @@ class AuthSrv(object):
t = 'volume "/%s" has invalid %stry [%s]'
raise Exception(t % (vol.vpath, k, vol.flags.get(k + "try")))
for k in ("chmod_d", "chmod_f"):
is_d = k == "chmod_d"
zs = vol.flags.get(k, "")
if not zs and is_d:
zs = "755"
if not zs:
vol.flags.pop(k, None)
continue
if not re.match("^[0-7]{3}$", zs):
t = "config-option '%s' must be a three-digit octal value such as [755] or [644] but the value was [%s]"
t = t % (k, zs)
self.log(t, 1)
raise Exception(t)
zi = int(zs, 8)
vol.flags[k] = zi
if (is_d and zi != 0o755) or not is_d:
free_umask = True
if vol.lim:
vol.lim.chmod_d = vol.flags["chmod_d"]
if vol.flags.get("og"):
self.args.uqe = True
@@ -2205,6 +2284,8 @@ class AuthSrv(object):
t = "WARNING: volume [/%s]: invalid value specified for ext-th: %s"
self.log(t % (vol.vpath, etv), 3)
vol.check_landmarks()
# d2d drops all database features for a volume
for grp, rm in [["d2d", "e2d"], ["d2t", "e2t"], ["d2d", "e2v"]]:
if not vol.flags.get(grp, False):
@@ -2351,6 +2432,10 @@ class AuthSrv(object):
if errors:
sys.exit(1)
setattr(self.args, "free_umask", free_umask)
if free_umask:
os.umask(0)
vfs.bubble_flags()
have_e2d = False
@@ -2553,6 +2638,28 @@ class AuthSrv(object):
shn.shr_src = (s_vfs, s_rem)
shn.realpath = s_vfs.canonical(s_rem)
# root.all_aps doesn't include any shares, so make a copy where the
# share appears in all abspaths it can provide (for example for chk_ap)
ap = shn.realpath
if not ap.endswith(os.sep):
ap += os.sep
shn.shr_all_aps = [(x, y[:]) for x, y in vfs.all_aps]
exact = False
for ap2, vns in shn.shr_all_aps:
if ap == ap2:
exact = True
if ap2.startswith(ap):
try:
vp2 = vjoin(s_rem, ap2[len(ap) :])
vn2, _ = s_vfs.get(vp2, "*", False, False)
if vn2 == s_vfs or vn2.dbv == s_vfs:
vns.append(shn)
except:
pass
if not exact:
shn.shr_all_aps.append((ap, [shn]))
shn.shr_all_aps.sort(key=lambda x: len(x[0]), reverse=True)
if self.args.shr_v:
t = "mapped %s share [%s] by [%s] => [%s] => [%s]"
self.log(t % (s_pr, s_k, s_un, s_vp, shn.realpath))
@@ -2567,7 +2674,7 @@ class AuthSrv(object):
continue # also fine
for zs in svn.nodes.keys():
# hide subvolume
vn.nodes[zs] = VFS(self.log_func, "", "", "", AXS(), {})
vn.nodes[zs] = VFS(self.log_func, "", "", "", AXS(), self.vf0())
cur2.close()
cur.close()
@@ -2612,6 +2719,7 @@ class AuthSrv(object):
"def_hcols": list(vf.get("mth") or []),
"unlist0": vf.get("unlist") or "",
"see_dots": self.args.see_dots,
"dqdel": self.args.qdel,
"dgrid": "grid" in vf,
"dgsel": "gsel" in vf,
"dnsort": "nsort" in vf,

View File

@@ -25,14 +25,26 @@ def listdir(p: str = ".") -> list[str]:
def makedirs(name: str, mode: int = 0o755, exist_ok: bool = True) -> bool:
# os.makedirs does 777 for all but leaf; this does mode on all
todo = []
bname = fsenc(name)
try:
os.makedirs(bname, mode)
return True
except:
if not exist_ok or not os.path.isdir(bname):
raise
while bname:
if os.path.isdir(bname):
break
todo.append(bname)
bname = os.path.dirname(bname)
if not todo:
if not exist_ok:
os.mkdir(bname) # to throw
return False
for zb in todo[::-1]:
try:
os.mkdir(zb, mode)
except:
if os.path.isdir(zb):
continue
raise
return True
def mkdir(p: str, mode: int = 0o755) -> None:

View File

@@ -78,6 +78,8 @@ def vf_vmap() -> dict[str, str]:
}
for k in (
"bup_ck",
"chmod_d",
"chmod_f",
"dbd",
"forget_ip",
"hsortn",
@@ -169,6 +171,8 @@ flagcats = {
"safededup": "verify on-disk data before using it for dedup",
"noclone": "take dupe data from clients, even if available on HDD",
"nodupe": "rejects existing files (instead of linking/cloning them)",
"chmod_d=755": "unix-permission for new dirs/folders",
"chmod_f=644": "unix-permission for new files",
"sparse": "force use of sparse files, mainly for s3-backed storage",
"nosparse": "deny use of sparse files, mainly for slow storage",
"daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
@@ -218,6 +222,7 @@ flagcats = {
"d2d": "disables all database stuff, overrides -e2*",
"hist=/tmp/cdb": "puts thumbnails and indexes at that location",
"dbpath=/tmp/cdb": "puts indexes at that location",
"landmark=foo": "disable db if file foo doesn't exist",
"scan=60": "scan for new files every 60sec, same as --re-maxage",
"nohash=\\.iso$": "skips hashing file contents if path matches *.iso",
"noidx=\\.iso$": "fully ignores the contents at paths matching *.iso",

View File

@@ -229,7 +229,7 @@ class FtpFs(AbstractedFS):
r = "r" in mode
w = "w" in mode or "a" in mode or "+" in mode
ap = self.rv2a(filename, r, w)[0]
ap, vfs, _ = self.rv2a(filename, r, w)
self.validpath(ap)
if w:
try:
@@ -261,7 +261,11 @@ class FtpFs(AbstractedFS):
wunlink(self.log, ap, VF_CAREFUL)
return open(fsenc(ap), mode, self.args.iobuf)
ret = open(fsenc(ap), mode, self.args.iobuf)
if w and "chmod_f" in vfs.flags:
os.fchmod(ret.fileno(), vfs.flags["chmod_f"])
return ret
def chdir(self, path: str) -> None:
nwd = join(self.cwd, path)
@@ -292,8 +296,9 @@ class FtpFs(AbstractedFS):
) = avfs.can_access("", self.h.uname)
def mkdir(self, path: str) -> None:
ap = self.rv2a(path, w=True)[0]
bos.makedirs(ap) # filezilla expects this
ap, vfs, _ = self.rv2a(path, w=True)
chmod = vfs.flags["chmod_d"]
bos.makedirs(ap, chmod) # filezilla expects this
def listdir(self, path: str) -> list[str]:
vpath = join(self.cwd, path)

View File

@@ -45,6 +45,7 @@ from .util import (
APPLESAN_RE,
BITNESS,
DAV_ALLPROPS,
E_SCK_WR,
FN_EMB,
HAVE_SQLITE3,
HTTPCODE,
@@ -1374,12 +1375,13 @@ class HttpCli(object):
title = self.uparam.get("title") or self.vpath.split("/")[-1]
etitle = html_escape(title, True, True)
baseurl = "%s://%s%s" % (
baseurl = "%s://%s/" % (
"https" if self.is_https else "http",
self.host,
self.args.SRS,
)
feed = "%s%s" % (baseurl, self.req[1:])
feed = baseurl + self.req[1:]
if self.is_vproxied:
baseurl += self.args.RS
efeed = html_escape(feed, True, True)
edirlink = efeed.split("?")[0] + q_pw
@@ -1392,7 +1394,7 @@ class HttpCli(object):
\t\t<title>%s</title>
\t\t<description></description>
\t\t<link>%s</link>
\t\t<generator>copyparty-1</generator>
\t\t<generator>copyparty-2</generator>
"""
% (efeed, etitle, edirlink)
]
@@ -2066,7 +2068,7 @@ class HttpCli(object):
fdir, fn = os.path.split(fdir)
rem, _ = vsplit(rem)
bos.makedirs(fdir)
bos.makedirs(fdir, vfs.flags["chmod_d"])
open_ka: dict[str, Any] = {"fun": open}
open_a = ["wb", self.args.iobuf]
@@ -2125,6 +2127,8 @@ class HttpCli(object):
fn = vfs.flags["put_name2"].format(now=time.time(), cip=self.dip())
params = {"suffix": suffix, "fdir": fdir}
if "chmod_f" in vfs.flags:
params["chmod"] = vfs.flags["chmod_f"]
if self.args.nw:
params = {}
fn = os.devnull
@@ -2173,7 +2177,7 @@ class HttpCli(object):
if self.args.nw:
fn = os.devnull
else:
bos.makedirs(fdir)
bos.makedirs(fdir, vfs.flags["chmod_d"])
path = os.path.join(fdir, fn)
if not nameless:
self.vpath = vjoin(self.vpath, fn)
@@ -2305,7 +2309,7 @@ class HttpCli(object):
if self.args.hook_v:
log_reloc(self.log, hr["reloc"], x, path, vp, fn, vfs, rem)
fdir, self.vpath, fn, (vfs, rem) = x
bos.makedirs(fdir)
bos.makedirs(fdir, vfs.flags["chmod_d"])
path2 = os.path.join(fdir, fn)
atomic_move(self.log, path, path2, vfs.flags)
path = path2
@@ -2591,7 +2595,7 @@ class HttpCli(object):
dst = vfs.canonical(rem)
try:
if not bos.path.isdir(dst):
bos.makedirs(dst)
bos.makedirs(dst, vfs.flags["chmod_d"])
except OSError as ex:
self.log("makedirs failed %r" % (dst,))
if not bos.path.isdir(dst):
@@ -3026,7 +3030,7 @@ class HttpCli(object):
raise Pebkac(405, 'folder "/%s" already exists' % (vpath,))
try:
bos.makedirs(fn)
bos.makedirs(fn, vfs.flags["chmod_d"])
except OSError as ex:
if ex.errno == errno.EACCES:
raise Pebkac(500, "the server OS denied write-access")
@@ -3068,6 +3072,8 @@ class HttpCli(object):
with open(fsenc(fn), "wb") as f:
f.write(b"`GRUNNUR`\n")
if "chmod_f" in vfs.flags:
os.fchmod(f.fileno(), vfs.flags["chmod_f"])
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
self.redirect(vpath, "?edit")
@@ -3141,7 +3147,7 @@ class HttpCli(object):
)
upload_vpath = "{}/{}".format(vfs.vpath, rem).strip("/")
if not nullwrite:
bos.makedirs(fdir_base)
bos.makedirs(fdir_base, vfs.flags["chmod_d"])
rnd, lifetime, xbu, xau = self.upload_flags(vfs)
zs = self.uparam.get("want") or self.headers.get("accept") or ""
@@ -3236,8 +3242,11 @@ class HttpCli(object):
else:
open_args["fdir"] = fdir
if "chmod_f" in vfs.flags:
open_args["chmod"] = vfs.flags["chmod_f"]
if p_file and not nullwrite:
bos.makedirs(fdir)
bos.makedirs(fdir, vfs.flags["chmod_d"])
# reserve destination filename
f, fname = ren_open(fname, "wb", fdir=fdir, suffix=suffix)
@@ -3341,7 +3350,7 @@ class HttpCli(object):
if nullwrite:
fdir = ap2 = ""
else:
bos.makedirs(fdir)
bos.makedirs(fdir, vfs.flags["chmod_d"])
atomic_move(self.log, abspath, ap2, vfs.flags)
abspath = ap2
sz = bos.path.getsize(abspath)
@@ -3462,6 +3471,8 @@ class HttpCli(object):
ft = "{}:{}".format(self.ip, self.addr[1])
ft = "{}\n{}\n{}\n".format(ft, msg.rstrip(), errmsg)
f.write(ft.encode("utf-8"))
if "chmod_f" in vfs.flags:
os.fchmod(f.fileno(), vfs.flags["chmod_f"])
except Exception as ex:
suf = "\nfailed to write the upload report: {}".format(ex)
@@ -3512,7 +3523,7 @@ class HttpCli(object):
lim = vfs.get_dbv(rem)[0].lim
if lim:
fp, rp = lim.all(self.ip, rp, clen, vfs.realpath, fp, self.conn.hsrv.broker)
bos.makedirs(fp)
bos.makedirs(fp, vfs.flags["chmod_d"])
fp = os.path.join(fp, fn)
rem = "{}/{}".format(rp, fn).strip("/")
@@ -3580,13 +3591,15 @@ class HttpCli(object):
zs = ub64enc(zb).decode("ascii")[:24].lower()
dp = "%s/md/%s/%s/%s" % (dbv.histpath, zs[:2], zs[2:4], zs)
self.log("moving old version to %s/%s" % (dp, mfile2))
if bos.makedirs(dp):
if bos.makedirs(dp, vfs.flags["chmod_d"]):
with open(os.path.join(dp, "dir.txt"), "wb") as f:
f.write(afsenc(vrd))
if "chmod_f" in vfs.flags:
os.fchmod(f.fileno(), vfs.flags["chmod_f"])
elif hist_cfg == "s":
dp = os.path.join(mdir, ".hist")
try:
bos.mkdir(dp)
bos.mkdir(dp, vfs.flags["chmod_d"])
hidedir(dp)
except:
pass
@@ -3625,6 +3638,8 @@ class HttpCli(object):
wunlink(self.log, fp, vfs.flags)
with open(fsenc(fp), "wb", self.args.iobuf) as f:
if "chmod_f" in vfs.flags:
os.fchmod(f.fileno(), vfs.flags["chmod_f"])
sz, sha512, _ = hashcopy(p_data, f, None, 0, self.args.s_wr_slp)
if lim:
@@ -4364,7 +4379,7 @@ class HttpCli(object):
self.log("file deleted; disconnecting")
break
except IOError as ex:
if ex.errno not in (errno.EPIPE, errno.ESHUTDOWN, errno.EBADFD):
if ex.errno not in E_SCK_WR:
raise
finally:
if f:
@@ -5558,7 +5573,7 @@ class HttpCli(object):
db.commit()
db.close()
self.conn.hsrv.broker.ask("reload", False, False).get()
self.conn.hsrv.broker.ask("reload", False, True).get()
self.redirect("", "?idp")
return True
@@ -5642,7 +5657,7 @@ class HttpCli(object):
cur.connection.commit()
if reload:
self.conn.hsrv.broker.ask("reload", False, False).get()
self.conn.hsrv.broker.ask("reload", False, True).get()
self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
self.redirect("", "?shares")
@@ -5734,7 +5749,7 @@ class HttpCli(object):
cur.execute(q, (skey, fn))
cur.connection.commit()
self.conn.hsrv.broker.ask("reload", False, False).get()
self.conn.hsrv.broker.ask("reload", False, True).get()
self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
fn = quotep(fns[0]) if len(fns) == 1 else ""

View File

@@ -320,7 +320,7 @@ class SMB(object):
self.hub.up2k.handle_mv(uname, "1.7.6.2", vp1, vp2)
try:
bos.makedirs(ap2)
bos.makedirs(ap2, vfs2.flags["chmod_d"])
except:
pass
@@ -334,7 +334,7 @@ class SMB(object):
t = "blocked mkdir (no-write-acc %s): /%s @%s"
yeet(t % (vfs.axs.uwrite, vpath, uname))
return bos.mkdir(ap)
return bos.mkdir(ap, vfs.flags["chmod_d"])
def _stat(self, vpath: str, *a: Any, **ka: Any) -> os.stat_result:
try:

View File

@@ -27,6 +27,7 @@ if True: # pylint: disable=using-constant-test
from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode
from .authsrv import BAD_CFG, AuthSrv
from .bos import bos
from .cert import ensure_cert
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN
from .pwhash import HAVE_ARGON2
@@ -1118,7 +1119,7 @@ class SvcHub(object):
fn = sel_fn
try:
os.makedirs(os.path.dirname(fn))
bos.makedirs(os.path.dirname(fn))
except:
pass
@@ -1135,6 +1136,9 @@ class SvcHub(object):
lh = codecs.open(fn, "w", encoding="utf-8", errors="replace")
if getattr(self.args, "free_umask", False):
os.fchmod(lh.fileno(), 0o644)
argv = [pybin] + self.argv
if hasattr(shlex, "quote"):
argv = [shlex.quote(x) for x in argv]

View File

@@ -282,7 +282,7 @@ class TcpSrv(object):
except:
pass # will create another ipv4 socket instead
if not ANYWIN and self.args.freebind:
if getattr(self.args, "freebind", False):
srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1)
try:

View File

@@ -387,14 +387,18 @@ class Tftpd(object):
if not a:
a = (self.args.iobuf,)
return open(ap, mode, *a, **ka)
ret = open(ap, mode, *a, **ka)
if wr and "chmod_f" in vfs.flags:
os.fchmod(ret.fileno(), vfs.flags["chmod_f"])
return ret
def _mkdir(self, vpath: str, *a) -> None:
vfs, _, ap = self._v2a("mkdir", vpath, [False, True])
if "*" not in vfs.axs.uwrite:
yeet("blocked mkdir; folder not world-writable: /%s" % (vpath,))
return bos.mkdir(ap)
return bos.mkdir(ap, vfs.flags["chmod_d"])
def _unlink(self, vpath: str) -> None:
# return bos.unlink(self._v2a("stat", vpath, *a)[1])

View File

@@ -269,7 +269,8 @@ class ThumbSrv(object):
self.log("joined waiting room for %r" % (tpath,))
except:
thdir = os.path.dirname(tpath)
bos.makedirs(os.path.join(thdir, "w"))
chmod = 0o700 if self.args.free_umask else 0o755
bos.makedirs(os.path.join(thdir, "w"), chmod)
inf_path = os.path.join(thdir, "dir.txt")
if not bos.path.exists(inf_path):
@@ -284,7 +285,7 @@ class ThumbSrv(object):
vn = next((x for x in allvols if x.realpath == ptop), None)
if not vn:
self.log("ptop %r not in %s" % (ptop, allvols), 3)
vn = self.asrv.vfs.all_aps[0][1]
vn = self.asrv.vfs.all_aps[0][1][0]
self.q.put((abspath, tpath, fmt, vn))
self.log("conv %r :%s \033[0m%r" % (tpath, fmt, abspath), 6)

View File

@@ -915,7 +915,8 @@ class Up2k(object):
# only need to protect register_vpath but all in one go feels right
for vol in vols:
try:
bos.makedirs(vol.realpath) # gonna happen at snap anyways
# mkdir gonna happen at snap anyways;
bos.makedirs(vol.realpath, vol.flags["chmod_d"])
dir_is_empty(self.log_func, not self.args.no_scandir, vol.realpath)
except Exception as ex:
self.volstate[vol.vpath] = "OFFLINE (cannot access folder)"
@@ -1141,6 +1142,20 @@ class Up2k(object):
del fl[k1]
else:
fl[k1] = ",".join(x for x in fl[k1])
if fl["chmod_d"] == int(self.args.chmod_d, 8):
fl.pop("chmod_d")
try:
if fl["chmod_f"] == int(self.args.chmod_f or "-1", 8):
fl.pop("chmod_f")
except:
pass
for k in ("chmod_f", "chmod_d"):
try:
fl[k] = "%o" % (fl[k])
except:
pass
a = [
(ft if v is True else ff if v is False else fv).format(k, str(v))
for k, v in fl.items()
@@ -1364,6 +1379,10 @@ class Up2k(object):
t = "volume /%s at [%s] is empty; will not be indexed as this could be due to an offline filesystem"
self.log(t % (vol.vpath, rtop), 6)
return True, False
if not vol.check_landmarks():
t = "volume /%s at [%s] will not be indexed due to bad landmarks"
self.log(t % (vol.vpath, rtop), 6)
return True, False
n_add, _, _ = self._build_dir(
db,
@@ -3290,7 +3309,7 @@ class Up2k(object):
reg,
"up2k._get_volsize",
)
bos.makedirs(ap2)
bos.makedirs(ap2, vfs.flags["chmod_d"])
vfs.lim.nup(cj["addr"])
vfs.lim.bup(cj["addr"], cj["size"])
@@ -3397,11 +3416,11 @@ class Up2k(object):
self.log(t % (mts - mtc, mts, mtc, fp))
ow = False
ptop = job["ptop"]
vf = self.flags.get(ptop) or {}
if ow:
self.log("replacing existing file at %r" % (fp,))
cur = None
ptop = job["ptop"]
vf = self.flags.get(ptop) or {}
st = bos.stat(fp)
try:
vrel = vjoin(job["prel"], fname)
@@ -3421,8 +3440,13 @@ class Up2k(object):
else:
dip = self.hub.iphash.s(ip)
suffix = "-%.6f-%s" % (ts, dip)
f, ret = ren_open(fname, "wb", fdir=fdir, suffix=suffix)
f, ret = ren_open(
fname,
"wb",
fdir=fdir,
suffix="-%.6f-%s" % (ts, dip),
chmod=vf.get("chmod_f", -1),
)
f.close()
return ret
@@ -4277,7 +4301,7 @@ class Up2k(object):
self.log(t, 1)
raise Pebkac(405, t)
bos.makedirs(os.path.dirname(dabs))
bos.makedirs(os.path.dirname(dabs), dvn.flags["chmod_d"])
c1, w, ftime_, fsize_, ip, at = self._find_from_vpath(
svn_dbv.realpath, srem_dbv
@@ -4453,7 +4477,7 @@ class Up2k(object):
vp = vjoin(dvp, rem)
try:
dvn, drem = self.vfs.get(vp, uname, False, True)
bos.mkdir(dvn.canonical(drem))
bos.mkdir(dvn.canonical(drem), dvn.flags["chmod_d"])
except:
pass
@@ -4523,7 +4547,7 @@ class Up2k(object):
is_xvol = svn.realpath != dvn.realpath
bos.makedirs(os.path.dirname(dabs))
bos.makedirs(os.path.dirname(dabs), dvn.flags["chmod_d"])
if is_dirlink:
dlabs = absreal(sabs)
@@ -5030,8 +5054,13 @@ class Up2k(object):
else:
dip = self.hub.iphash.s(job["addr"])
suffix = "-%.6f-%s" % (job["t0"], dip)
f, job["tnam"] = ren_open(tnam, "wb", fdir=pdir, suffix=suffix)
f, job["tnam"] = ren_open(
tnam,
"wb",
fdir=pdir,
suffix="-%.6f-%s" % (job["t0"], dip),
chmod=vf.get("chmod_f", -1),
)
try:
abspath = djoin(pdir, job["tnam"])
sprs = job["sprs"]

View File

@@ -105,6 +105,7 @@ def _ens(want: str) -> tuple[int, ...]:
# WSAENOTSOCK - no longer a socket
# EUNATCH - can't assign requested address (wifi down)
E_SCK = _ens("ENOTCONN EUNATCH EBADF WSAENOTSOCK WSAECONNRESET")
E_SCK_WR = _ens("EPIPE ESHUTDOWN EBADFD")
E_ADDR_NOT_AVAIL = _ens("EADDRNOTAVAIL WSAEADDRNOTAVAIL")
E_ADDR_IN_USE = _ens("EADDRINUSE WSAEADDRINUSE")
E_ACCESS = _ens("EACCES WSAEACCES")
@@ -1584,6 +1585,7 @@ def ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str
fun = kwargs.pop("fun", open)
fdir = kwargs.pop("fdir", None)
suffix = kwargs.pop("suffix", None)
chmod = kwargs.pop("chmod", -1)
if fname == os.devnull:
return fun(fname, *args, **kwargs), fname
@@ -1627,6 +1629,11 @@ def ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str
fp2 = os.path.join(fdir, fp2)
with open(fsenc(fp2), "wb") as f2:
f2.write(orig_name.encode("utf-8"))
if chmod >= 0:
os.fchmod(f2.fileno(), chmod)
if chmod >= 0:
os.fchmod(f.fileno(), chmod)
return f, fname
@@ -1967,7 +1974,7 @@ def rand_name(fdir: str, fn: str, rnd: int) -> str:
return fn
def gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:
def _gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:
if alg == 1:
zs = "%s %s %s %s" % (salt, fspath, fsize, inode)
else:
@@ -1977,6 +1984,13 @@ def gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str
return ub64enc(hashlib.sha512(zb).digest()).decode("ascii")
def _gen_filekey_w(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:
return _gen_filekey(alg, salt, fspath.replace("/", "\\"), fsize, inode)
gen_filekey = _gen_filekey_w if ANYWIN else _gen_filekey
def gen_filekey_dbg(
alg: int,
salt: str,
@@ -2400,11 +2414,11 @@ def pathmod(
# try to map abspath to vpath
np = np.replace("/", os.sep)
for vn_ap, vn in vfs.all_aps:
for vn_ap, vns in vfs.all_aps:
if not np.startswith(vn_ap):
continue
zs = np[len(vn_ap) :].replace(os.sep, "/")
nvp = vjoin(vn.vpath, zs)
nvp = vjoin(vns[0].vpath, zs)
break
if nvp == "\n":

View File

@@ -592,9 +592,7 @@ window.baguetteBox = (function () {
preloadPrev(currentIndex);
});
clmod(ebi('bbox-btns'), 'off');
clmod(btnPrev, 'off');
clmod(btnNext, 'off');
show_buttons(0);
updateOffset();
overlay.style.display = 'block';
@@ -776,6 +774,8 @@ window.baguetteBox = (function () {
if (is_vid) {
image.volume = clamp(fcfg_get('vol', dvol / 100), 0, 1);
image.setAttribute('controls', 'controls');
image.setAttribute('playsinline', '1');
// ios ignores poster
image.onended = vidEnd;
image.onplay = function () { show_buttons(1); };
image.onpause = function () { show_buttons(); };

View File

@@ -2035,6 +2035,9 @@ a.btn,
font-family: 'scp', monospace, monospace;
font-family: var(--font-mono), 'scp', monospace, monospace;
}
#hkhelp b {
text-shadow: 1px 0 0 var(--fg), -1px 0 0 var(--fg), 0 -1px 0 var(--fg);
}
html.noscroll,
html.noscroll .sbar {
scrollbar-width: none;
@@ -2196,18 +2199,25 @@ html.y #bbox-overlay figcaption a {
top: calc(50% - 30px);
width: 44px;
height: 60px;
transition: background-color .3s ease, color .3s ease, left .3s ease, right .3s ease;
}
#bbox-btns button {
transition: background-color .3s ease, color .3s ease;
}
#bbox-btns {
transition: top .3s ease;
}
.bbox-btn {
position: fixed;
}
.bbox-btn,
#bbox-btns {
opacity: 1;
animation: opacity .2s infinite ease-in-out;
#bbox-next.off {
right: -2.6em;
}
#bbox-prev.off {
left: -2.6em;
}
.bbox-btn.off,
#bbox-btns.off {
opacity: 0;
top: -2.2em;
}
#bbox-overlay button {
cursor: pointer;
@@ -2218,8 +2228,6 @@ html.y #bbox-overlay figcaption a {
border-radius: 15%;
background: rgba(50, 50, 50, 0.5);
color: rgba(255,255,255,0.7);
transition: background-color .3s ease;
transition: color .3s ease;
font-size: 1.4em;
line-height: 1.4em;
vertical-align: top;
@@ -3264,4 +3272,9 @@ html.d #treepar {
.dropdesc>div>div {
transition: none;
}
#bbox-next,
#bbox-prev,
#bbox-btns {
transition: background-color .3s ease, color .3s ease;
}
}

View File

@@ -35,7 +35,7 @@ var Ls = {
"file-manager",
["G", "toggle list / grid view"],
["T", "toggle thumbnails / icons"],
["🡅 A/D", "thumbnail size"],
[" A/D", "thumbnail size"],
["ctrl-K", "delete selected"],
["ctrl-X", "cut selection to clipboard"],
["ctrl-C", "copy selection to clipboard"],
@@ -45,9 +45,9 @@ var Ls = {
"file-list-sel",
["space", "toggle file selection"],
["🡑/🡓", "move selection cursor"],
["ctrl 🡑/🡓", "move cursor and viewport"],
["🡅 🡑/🡓", "select prev/next file"],
["↑/↓", "move selection cursor"],
["ctrl ↑/↓", "move cursor and viewport"],
["⇧ ↑/↓", "select prev/next file"],
["ctrl-A", "select all files / folders"],
], [
"navigation",
@@ -70,7 +70,7 @@ var Ls = {
["Home/End", "first/last pic"],
["F", "fullscreen"],
["R", "rotate clockwise"],
["🡅 R", "rotate ccw"],
[" R", "rotate ccw"],
["S", "select pic"],
["Y", "download pic"],
], [
@@ -226,6 +226,7 @@ var Ls = {
"ct_csel": 'use CTRL and SHIFT for file selection in grid-view">sel',
"ct_ihop": 'when the image viewer is closed, scroll down to the last viewed file">g⮯',
"ct_dots": 'show hidden files (if server permits)">dotfiles',
"ct_qdel": 'when deleting files, only ask for confirmation once">qdel',
"ct_dir1st": 'sort folders before files">📁 first',
"ct_nsort": 'natural sort (for filenames with leading digits)">nsort',
"ct_readme": 'show README.md in folder listings">📜 readme',
@@ -658,7 +659,7 @@ var Ls = {
"filbehandler",
["G", "listevisning eller ikoner"],
["T", "miniatyrbilder på/av"],
["🡅 A/D", "ikonstørrelse"],
[" A/D", "ikonstørrelse"],
["ctrl-K", "slett valgte"],
["ctrl-X", "klipp ut valgte"],
["ctrl-C", "kopiér til utklippstavle"],
@@ -668,9 +669,9 @@ var Ls = {
"filmarkering",
["space", "marker fil"],
["🡑/🡓", "flytt markør"],
["ctrl 🡑/🡓", "flytt markør og scroll"],
["🡅 🡑/🡓", "velg forr./neste fil"],
["↑/↓", "flytt markør"],
["ctrl ↑/↓", "flytt markør og scroll"],
["⇧ ↑/↓", "velg forr./neste fil"],
["ctrl-A", "velg alle filer / mapper"],
], [
"navigering",
@@ -693,7 +694,7 @@ var Ls = {
["Home/End", "første/siste bilde"],
["F", "fullskjermvisning"],
["R", "rotere mot høyre"],
["🡅 R", "rotere mot venstre"],
[" R", "rotere mot venstre"],
["S", "marker bilde"],
["Y", "last ned bilde"],
], [
@@ -850,6 +851,7 @@ var Ls = {
"ct_csel": 'bruk tastene CTRL og SHIFT for markering av filer i ikonvisning">merk',
"ct_ihop": 'bla ned til sist viste bilde når bildeviseren lukkes">g⮯',
"ct_dots": 'vis skjulte filer (gitt at serveren tillater det)">.synlig',
"ct_qdel": 'sletteknappen spør bare én gang om bekreftelse">hurtig🗑',
"ct_dir1st": 'sorter slik at mapper kommer foran filer">📁 først',
"ct_nsort": 'naturlig sortering (forstår tall i filnavn)">nsort',
"ct_readme": 'vis README.md nedenfor filene">📜 readme',
@@ -1283,7 +1285,7 @@ var Ls = {
"file-manager",
["G", "切换列表 / 网格视图"],
["T", "切换缩略图 / 图标"],
["🡅 A/D", "缩略图大小"],
[" A/D", "缩略图大小"],
["ctrl-K", "删除选中项"],
["ctrl-X", "剪切选中项"],
["ctrl-C", "复制选中项"], //m
@@ -1293,9 +1295,9 @@ var Ls = {
"file-list-sel",
["space", "切换文件选择"],
["🡑/🡓", "移动选择光标"],
["ctrl 🡑/🡓", "移动光标和视图"],
["🡅 🡑/🡓", "选择上一个/下一个文件"],
["↑/↓", "移动选择光标"],
["ctrl ↑/↓", "移动光标和视图"],
["⇧ ↑/↓", "选择上一个/下一个文件"],
["ctrl-A", "选择所有文件 / 文件夹"]
], [
"navigation",
@@ -1318,7 +1320,7 @@ var Ls = {
["Home/End", "第一张/最后一张图片"],
["F", "全屏"],
["R", "顺时针旋转"],
["🡅 R", "逆时针旋转"],
[" R", "逆时针旋转"],
["S", "选择图片"], //m
["Y", "下载图片"]
], [
@@ -1474,6 +1476,7 @@ var Ls = {
"ct_csel": '在网格视图中使用 CTRL 和 SHIFT 进行文件选择">CTRL',
"ct_ihop": '当图像查看器关闭时,滚动到最后查看的文件">滚动',
"ct_dots": '显示隐藏文件(如果服务器允许)">隐藏文件',
"ct_qdel": '删除文件时,只需确认一次">快删', //m
"ct_dir1st": '在文件之前排序文件夹">📁 排序',
"ct_nsort": '正确排序以数字开头的文件名">数字排序', //m
"ct_readme": '在文件夹列表中显示 README.md">📜 readme',
@@ -2090,6 +2093,7 @@ ebi('op_cfg').innerHTML = (
' <a id="csel" class="tgl btn" href="#" tt="' + L.ct_csel + '</a>\n' +
' <a id="ihop" class="tgl btn" href="#" tt="' + L.ct_ihop + '</a>\n' +
' <a id="dotfiles" class="tgl btn" href="#" tt="' + L.ct_dots + '</a>\n' +
' <a id="qdel" class="tgl btn" href="#" tt="' + L.ct_qdel + '</a>\n' +
' <a id="dir1st" class="tgl btn" href="#" tt="' + L.ct_dir1st + '</a>\n' +
' <a id="nsort" class="tgl btn" href="#" tt="' + L.ct_nsort + '</a>\n' +
' <a id="ireadme" class="tgl btn" href="#" tt="' + L.ct_readme + '</a>\n' +
@@ -3683,7 +3687,7 @@ var mpui = (function () {
var oi = mp.order.indexOf(mp.au.tid) + 1,
evp = get_evpath();
if (mpl.pb_mode == 'loop' || mp.au.evp != evp)
if (mpl.pb_mode == 'loop' || mp.au.evp != evp || ebi('unsearch'))
oi = 0;
if (oi >= mp.order.length) {
@@ -5453,7 +5457,16 @@ var fileman = (function () {
deleter();
}
var asks = r.qdel ? 1 : 2;
if (dqdel === 0)
asks -= 1;
if (!asks)
return deleter();
modal.confirm('<h6 style="color:#900">' + L.danger + '</h6>\n<b>' + L.fd_warn1.format(vps.length) + '</b><ul>' + uricom_adec(vps, true).join('') + '</ul>', function () {
if (asks === 1)
return deleter();
modal.confirm(L.fd_warn2, deleter, null);
}, null);
};
@@ -5814,6 +5827,8 @@ var fileman = (function () {
r.bus.onmessage();
};
bcfg_bind(r, 'qdel', 'qdel', dqdel == 1);
bren.onclick = r.rename;
bdel.onclick = r.delete;
bcut.onclick = r.cut;
@@ -6885,8 +6900,10 @@ function hkhelp() {
try {
if (c[a].length != 2)
html.push('<tr><th colspan="2">' + esc(c[a]) + '</th></tr>');
else
html.push('<tr><td>{0}</td><td>{1}</td></tr>'.format(c[a][0], c[a][1]));
else {
var t1 = c[a][0].replace('⇧', '<b>⇧</b>');
html.push('<tr><td>{0}</td><td>{1}</td></tr>'.format(t1, c[a][1]));
}
}
catch (ex) {
html.push(">>> " + c[a]);

View File

@@ -1,3 +1,61 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0721-2307 `v1.18.3` drop the umask
## 🧪 new features
* #181 the default chmod (unix-permissions) of new files and folders can now be changed 9921c43e
* `--chmod-d` or volflag `chmod_d` sets directory permissions; default is 755
* `--chmod-f` or volflag `chmod_f` sets file permissions; default is usually 644 (OS-defined)
* see `--help-chmod` which explains the numbers
## 🩹 bugfixes
* #179 couldn't combine `--shr` (shares) and `--xvol` (symlink-guard) 0f0f8d90
* #180 gallery buttons could still be clicked when faded-out 8c32b0e7
* rss-feeds were slightly busted when combined with rp-loc (location-based proxying) 56d3bcf5
* music-playback within search-results no longer jumps into the next folder at end-of-list 9bc4c5d2
* video-playback on iOS now behaves like on all other platforms 78605d9a
* (it would force-switch into fullscreen because that's their default)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0707-1419 `v1.18.2` idp-vol persistence
## 🧪 new features
* IdP-volumes can optionally be persisted across restarts d162502c
* there is a UI to manage the cached users/groups 4f264a0a
* only available to users listed in the new option `--idp-adm`
* api for manually rescanning several volumes at once 42c199e7
* `/some/path/?scan` does that one volume like before
* `/any/path/?scan=/vol1,/another/vol2` rescans `/vol1` and `/another/vol2`
* volflag to hide volume from listing in controlpanel fd7c71d6
## 🩹 bugfixes
* macos: fix confusing crash when blocked by [Little Snitch](https://www.obdev.at/products/littlesnitch/) bf11b2a4
* unpost could break in some hairy reverseproxy setups 1b2d3985
* copyparty32.exe: fix segfault on win7 c9fafb20
* ui: fix navpane overlapping the scrollbar (still a bit jank but eh) 7ef6fd13
* usb-eject: support all volume names ed908b98
* docker: ensure clean slate deb6711b
* fix up2k on ie11 d2714434
## 🔧 other changes
* update buildscript for keyfinder to support llvm 65c4e035
* #175 add `python-magic` into the `iv` and `dj` docker flavors (thx @Morganamilo) 77274e9d
* properly killed the experimental docker flavors to avoid confusion 8306e3d9
* copyparty.exe: updated pillow 299cff3f f6be3905
* avif support was removed to save 2 MiB
## 🌠 fun facts
* this release was slightly delayed due to a [norwegian traffic jam](https://a.ocv.me/pub/g/2025/07/PXL_20250706_143558381.jpg)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0622-0020 `v1.18.0` Logtail

View File

@@ -7,7 +7,7 @@ import subprocess as sp
# to convert the copyparty --help to html, run this in xfce4-terminal @ 140x43:
_ = r""""
echo; for a in '' -bind -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -pwhash -zm; do
echo; for a in '' -bind -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -chmod -pwhash -zm; do
./copyparty-sfx.py --help$a 2>/dev/null; printf '\n\n\n%0139d\n\n\n'; done # xfce4-terminal @ 140x43
"""
# click [edit] => [select all]

View File

@@ -23,7 +23,7 @@ exit 0
# first open an infinitely wide console (this is why you own an ultrawide) and copypaste this into it:
for a in '' -bind -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -pwhash -zm; do
for a in '' -bind -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -chmod -pwhash -zm; do
./copyparty-sfx.py --help$a 2>/dev/null; printf '\n\n\n%0255d\n\n\n'; done
# then copypaste all of the output by pressing ctrl-shift-a, ctrl-shift-c

View File

@@ -121,7 +121,7 @@ var tl_browser = {
"file-manager",
["G", "toggle list / grid view"],
["T", "toggle thumbnails / icons"],
["🡅 A/D", "thumbnail size"],
[" A/D", "thumbnail size"],
["ctrl-K", "delete selected"],
["ctrl-X", "cut selection to clipboard"],
["ctrl-C", "copy selection to clipboard"],
@@ -131,9 +131,9 @@ var tl_browser = {
"file-list-sel",
["space", "toggle file selection"],
["🡑/🡓", "move selection cursor"],
["ctrl 🡑/🡓", "move cursor and viewport"],
["🡅 🡑/🡓", "select prev/next file"],
["↑/↓", "move selection cursor"],
["ctrl ↑/↓", "move cursor and viewport"],
["⇧ ↑/↓", "select prev/next file"],
["ctrl-A", "select all files / folders"],
], [
"navigation",
@@ -156,7 +156,7 @@ var tl_browser = {
["Home/End", "first/last pic"],
["F", "fullscreen"],
["R", "rotate clockwise"],
["🡅 R", "rotate ccw"],
[" R", "rotate ccw"],
["S", "select pic"],
["Y", "download pic"],
], [
@@ -312,6 +312,7 @@ var tl_browser = {
"ct_csel": 'use CTRL and SHIFT for file selection in grid-view">sel',
"ct_ihop": 'when the image viewer is closed, scroll down to the last viewed file">g⮯',
"ct_dots": 'show hidden files (if server permits)">dotfiles',
"ct_qdel": 'when deleting files, only ask for confirmation once">qdel',
"ct_dir1st": 'sort folders before files">📁 first',
"ct_nsort": 'natural sort (for filenames with leading digits)">nsort',
"ct_readme": 'show README.md in folder listings">📜 readme',

229
tests/test_shr.py Normal file
View File

@@ -0,0 +1,229 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
import json
import os
import shutil
import sqlite3
import tempfile
import unittest
from copyparty.__init__ import ANYWIN
from copyparty.authsrv import AuthSrv
from copyparty.httpcli import HttpCli
from copyparty.util import absreal
from tests import util as tu
from tests.util import Cfg
class TestShr(unittest.TestCase):
def log(self, src, msg, c=0):
m = "%s" % (msg,)
if (
"warning: filesystem-path does not exist:" in m
or "you are sharing a system directory:" in m
or "symlink-based deduplication is enabled" in m
or m.startswith("hint: argument")
):
return
print(("[%s] %s" % (src, msg)).encode("ascii", "replace").decode("ascii"))
def assertLD(self, url, auth, els, edl):
ls = self.ls(url, auth)
self.assertEqual(ls[0], len(els) == 2)
if not ls[0]:
return
a = [list(sorted(els[0])), list(sorted(els[1]))]
b = [list(sorted(ls[1])), list(sorted(ls[2]))]
self.assertEqual(a, b)
if edl is None:
edl = els[1]
can_dl = []
for fn in b[1]:
if fn == "a.db":
continue
furl = url + "/" + fn
if auth:
furl += "?pw=p1"
h, zb = self.curl(furl, True)
if h.startswith("HTTP/1.1 200 "):
can_dl.append(fn)
self.assertEqual(edl, can_dl)
def setUp(self):
self.td = tu.get_ramdisk()
td = os.path.join(self.td, "vfs")
os.mkdir(td)
os.chdir(td)
os.mkdir("d1")
os.mkdir("d2")
os.mkdir("d2/d3")
for zs in ("d1/f1", "d2/f2", "d2/d3/f3"):
with open(zs, "wb") as f:
f.write(zs.encode("utf-8"))
for dst in ("d1", "d2", "d2/d3"):
src, fn = zs.rsplit("/", 1)
os.symlink(absreal(zs), dst + "/l" + fn[-1:])
db = sqlite3.connect("a.db")
with db:
zs = r"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int)"
db.execute(zs)
db.close()
def tearDown(self):
os.chdir(tempfile.gettempdir())
shutil.rmtree(self.td)
def cinit(self):
self.asrv = AuthSrv(self.args, self.log)
self.conn = tu.VHttpConn(self.args, self.asrv, self.log, b"", True)
def test1(self):
self.args = Cfg(
a=["u1:p1"],
v=["::A,u1", "d1:v1:A,u1", "d2/d3:d2/d3:A,u1"],
shr="/shr/",
shr1="shr/",
shr_db="a.db",
shr_v=False,
)
self.cinit()
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
self.assertLD("d3", True, [], [])
jt = {
"k": "r",
"vp": ["/"],
"pw": "",
"exp": "99",
"perms": ["read"],
}
print(self.post_json("?pw=p1&share", jt)[1])
jt = {
"k": "d2",
"vp": ["/d2/"],
"pw": "",
"exp": "99",
"perms": ["read"],
}
print(self.post_json("?pw=p1&share", jt)[1])
self.conn.shutdown()
self.cinit()
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
self.assertLD("d3", True, [], [])
self.assertLD("shr/d2", False, [[], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("shr/d2/d3", False, [], None)
self.assertLD("shr/r", False, [["d1"], ["a.db"]], [])
self.assertLD("shr/r/d1", False, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("shr/r/d2", False, [], None) # unfortunate
self.assertLD("shr/r/d2/d3", False, [], None)
self.conn.shutdown()
def test2(self):
self.args = Cfg(
a=["u1:p1"],
v=["::A,u1", "d1:v1:A,u1", "d2/d3:d2/d3:A,u1"],
shr="/shr/",
shr1="shr/",
shr_db="a.db",
shr_v=False,
xvol=True,
)
self.cinit()
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
self.assertLD("d3", True, [], [])
jt = {
"k": "r",
"vp": ["/"],
"pw": "",
"exp": "99",
"perms": ["read"],
}
print(self.post_json("?pw=p1&share", jt)[1])
jt = {
"k": "d2",
"vp": ["/d2/"],
"pw": "",
"exp": "99",
"perms": ["read"],
}
print(self.post_json("?pw=p1&share", jt)[1])
self.conn.shutdown()
self.cinit()
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
self.assertLD("d3", True, [], [])
self.assertLD("shr/d2", False, [[], ["f2", "l1", "l2", "l3"]], ["f2", "l2"])
self.assertLD("shr/d2/d3", False, [], [])
self.assertLD("shr/r", False, [["d1"], ["a.db"]], [])
self.assertLD(
"shr/r/d1", False, [[], ["f1", "l1", "l2", "l3"]], ["f1", "l1", "l2"]
)
self.assertLD("shr/r/d2", False, [], []) # unfortunate
self.assertLD("shr/r/d2/d3", False, [], [])
self.conn.shutdown()
def ls(self, url: str, auth: bool):
zs = url + "?ls" + ("&pw=p1" if auth else "")
h, b = self.curl(zs)
if not h.startswith("HTTP/1.1 200 "):
return (False, [], [])
jo = json.loads(b)
return (
True,
[x["href"].rstrip("/") for x in jo.get("dirs") or {}],
[x["href"] for x in jo.get("files") or {}],
)
def curl(self, url: str, binary=False):
h = "GET /%s HTTP/1.1\r\nConnection: close\r\n\r\n"
HttpCli(self.conn.setbuf((h % (url,)).encode("utf-8"))).run()
if binary:
h, b = self.conn.s._reply.split(b"\r\n\r\n", 1)
return [h.decode("utf-8"), b]
return self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
def post_json(self, url: str, data):
buf = json.dumps(data).encode("utf-8")
msg = [
"POST /%s HTTP/1.1" % (url,),
"Connection: close",
"Content-Type: application/json",
"Content-Length: %d" % (len(buf),),
"\r\n",
]
buf = "\r\n".join(msg).encode("utf-8") + buf
print("PUT -->", buf)
HttpCli(self.conn.setbuf(buf)).run()
return self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)

View File

@@ -152,7 +152,7 @@ class Cfg(Namespace):
ex = "ah_cli ah_gen css_browser dbpath hist ipu js_browser js_other mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua ua_nodoc ua_nozip"
ka.update(**{k: None for k in ex.split()})
ex = "hash_mt hsortn safe_dedup srch_time tail_fd tail_rate u2abort u2j u2sz"
ex = "hash_mt hsortn qdel safe_dedup srch_time tail_fd tail_rate u2abort u2j u2sz"
ka.update(**{k: 1 for k in ex.split()})
ex = "au_vol dl_list mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who zip_who"
@@ -161,7 +161,7 @@ class Cfg(Namespace):
ex = "db_act forget_ip idp_store k304 loris no304 nosubtle re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs"
ka.update(**{k: 0 for k in ex.split()})
ex = "ah_alg bname chpw_db doctitle df exit favico idp_h_usr ipa html_head lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles unlist vname xff_src zipmaxt R RS SR"
ex = "ah_alg bname chmod_f chpw_db doctitle df exit favico idp_h_usr ipa html_head lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles unlist vname xff_src zipmaxt R RS SR"
ka.update(**{k: "" for k in ex.split()})
ex = "ban_403 ban_404 ban_422 ban_pw ban_url spinner"
@@ -181,6 +181,7 @@ class Cfg(Namespace):
c=c,
E=E,
bup_ck="sha512",
chmod_d="755",
dbd="wal",
dk_salt="b" * 16,
fk_salt="a" * 16,
@@ -260,6 +261,9 @@ class VHub(object):
self.is_dut = True
self.up2k = Up2k(self)
def reload(self, a, b):
pass
class VBrokerThr(BrokerThr):
def __init__(self, hub):