Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4accef00fb | ||
|
|
d779525500 | ||
|
|
65a7706f77 | ||
|
|
5e12abbb9b | ||
|
|
e0fe2b97be | ||
|
|
bd33863f9f | ||
|
|
a011139894 | ||
|
|
36866f1d36 | ||
|
|
407531bcb1 | ||
|
|
3adbb2ff41 | ||
|
|
499ae1c7a1 | ||
|
|
438ea6ccb0 | ||
|
|
598a29a733 | ||
|
|
6d102fc826 | ||
|
|
fca07fbb62 | ||
|
|
cdedcc24b8 | ||
|
|
60d5f27140 | ||
|
|
cb413bae49 | ||
|
|
e9f78ea70c | ||
|
|
6858cb066f | ||
|
|
4be0d426f4 | ||
|
|
7d7d5d6c3c | ||
|
|
0422387e90 | ||
|
|
2ed5fd9ac4 | ||
|
|
2beb2acc24 | ||
|
|
56ce591908 | ||
|
|
b190e676b4 | ||
|
|
19520b2ec9 | ||
|
|
eeb96ae8b5 | ||
|
|
cddedd37d5 | ||
|
|
4d6626b099 | ||
|
|
7a55833bb2 | ||
|
|
7e4702cf09 | ||
|
|
685f08697a | ||
|
|
a255db706d | ||
|
|
9d76902710 | ||
|
|
62ee7f6980 | ||
|
|
2f6707825a | ||
|
|
7dda77dcb4 |
64
README.md
64
README.md
@@ -94,9 +94,11 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
* [reverse-proxy](#reverse-proxy) - running copyparty next to other websites
|
||||
* [real-ip](#real-ip) - teaching copyparty how to see client IPs
|
||||
* [reverse-proxy performance](#reverse-proxy-performance)
|
||||
* [permanent cloudflare tunnel](#permanent-cloudflare-tunnel) - if you have a domain and want to get your copyparty online real quick
|
||||
* [prometheus](#prometheus) - metrics/stats can be enabled
|
||||
* [other extremely specific features](#other-extremely-specific-features) - you'll never find a use for these
|
||||
* [custom mimetypes](#custom-mimetypes) - change the association of a file extension
|
||||
* [GDPR compliance](#GDPR-compliance) - imagine using copyparty professionally...
|
||||
* [feature chickenbits](#feature-chickenbits) - buggy feature? rip it out
|
||||
* [packages](#packages) - the party might be closer than you think
|
||||
* [arch package](#arch-package) - now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
|
||||
@@ -159,8 +161,8 @@ enable thumbnails (images/audio/video), media indexing, and audio transcoding by
|
||||
* **MacOS:** `port install py-Pillow ffmpeg`
|
||||
* **MacOS** (alternative): `brew install pillow ffmpeg`
|
||||
* **Windows:** `python -m pip install --user -U Pillow`
|
||||
* install python and ffmpeg manually; do not use `winget` or `Microsoft Store` (it breaks $PATH)
|
||||
* copyparty.exe comes with `Pillow` and only needs `ffmpeg`
|
||||
* install [python](https://www.python.org/downloads/windows/) and [ffmpeg](#optional-dependencies) manually; do not use `winget` or `Microsoft Store` (it breaks $PATH)
|
||||
* copyparty.exe comes with `Pillow` and only needs [ffmpeg](#optional-dependencies) for mediatags/videothumbs
|
||||
* see [optional dependencies](#optional-dependencies) to enable even more features
|
||||
|
||||
running copyparty without arguments (for example doubleclicking it on Windows) will give everyone read/write access to the current folder; you may want [accounts and volumes](#accounts-and-volumes)
|
||||
@@ -183,6 +185,8 @@ first download [cloudflared](https://developers.cloudflare.com/cloudflare-one/co
|
||||
|
||||
as the tunnel starts, it will show a URL which you can share to let anyone browse your stash or upload files to you
|
||||
|
||||
but if you have a domain, then you probably want to skip the random autogenerated URL and instead make a [permanent cloudflare tunnel](#permanent-cloudflare-tunnel)
|
||||
|
||||
since people will be connecting through cloudflare, run copyparty with `--xff-hdr cf-connecting-ip` to detect client IPs correctly
|
||||
|
||||
|
||||
@@ -224,6 +228,7 @@ also see [comparison to similar software](./docs/versus.md)
|
||||
* ☑ [upnp / zeroconf / mdns / ssdp](#zeroconf)
|
||||
* ☑ [event hooks](#event-hooks) / script runner
|
||||
* ☑ [reverse-proxy support](https://github.com/9001/copyparty#reverse-proxy)
|
||||
* ☑ cross-platform (Windows, Linux, Macos, Android, FreeBSD, arm32/arm64, ppc64le, s390x, risc-v/riscv64)
|
||||
* upload
|
||||
* ☑ basic: plain multipart, ie6 support
|
||||
* ☑ [up2k](#uploading): js, resumable, multithreaded
|
||||
@@ -401,6 +406,9 @@ upgrade notes
|
||||
|
||||
"frequently" asked questions
|
||||
|
||||
* can I change the 🌲 spinning pine-tree loading animation?
|
||||
* [yeah...](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#boring-loader-spinner) :-(
|
||||
|
||||
* is it possible to block read-access to folders unless you know the exact URL for a particular file inside?
|
||||
* yes, using the [`g` permission](#accounts-and-volumes), see the examples there
|
||||
* you can also do this with linux filesystem permissions; `chmod 111 music` will make it possible to access files and folders inside the `music` folder but not list the immediate contents -- also works with other software, not just copyparty
|
||||
@@ -423,6 +431,14 @@ upgrade notes
|
||||
* copyparty seems to think I am using http, even though the URL is https
|
||||
* your reverse-proxy is not sending the `X-Forwarded-Proto: https` header; this could be because your reverse-proxy itself is confused. Ensure that none of the intermediates (such as cloudflare) are terminating https before the traffic hits your entrypoint
|
||||
|
||||
* thumbnails are broken (you get a colorful square which says the filetype instead)
|
||||
* you need to install `FFmpeg` or `Pillow`; see [thumbnails](#thumbnails)
|
||||
|
||||
* thumbnails are broken (some images appear, but other files just get a blank box, and/or the broken-image placeholder)
|
||||
* probably due to a reverse-proxy messing with the request URLs and stripping the query parameters (`?th=w`), so check your URL rewrite rules
|
||||
* could also be due to incorrect caching settings in reverse-proxies and/or CDNs, so make sure that nothing is set to ignore the query string
|
||||
* could also be due to misbehaving privacy-related browser extensions, so try to disable those
|
||||
|
||||
* i want to learn python and/or programming and am considering looking at the copyparty source code in that occasion
|
||||
* ```bash
|
||||
_| _ __ _ _|_
|
||||
@@ -653,6 +669,7 @@ press `g` or `田` to toggle grid-view instead of the file listing and `t` togg
|
||||
it does static images with Pillow / pyvips / FFmpeg, and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how dangerous your users are
|
||||
* pyvips is 3x faster than Pillow, Pillow is 3x faster than FFmpeg
|
||||
* disable thumbnails for specific volumes with volflag `dthumb` for all, or `dvthumb` / `dathumb` / `dithumb` for video/audio/images only
|
||||
* for installing FFmpeg on windows, see [optional dependencies](#optional-dependencies)
|
||||
|
||||
audio files are converted into spectrograms using FFmpeg unless you `--no-athumb` (and some FFmpeg builds may need `--th-ff-swr`)
|
||||
|
||||
@@ -664,6 +681,8 @@ enabling `multiselect` lets you click files to select them, and then shift-click
|
||||
* `multiselect` is mostly intended for phones/tablets, but the `sel` option in the `[⚙️] settings` tab is better suited for desktop use, allowing selection by CTRL-clicking and range-selection with SHIFT-click, all without affecting regular clicking
|
||||
* the `sel` option can be made default globally with `--gsel` or per-volume with volflag `gsel`
|
||||
|
||||
to show `/icons/exe.png` as the thumbnail for all .exe files, `--ext-th=exe=/icons/exe.png` (optionally as a volflag)
|
||||
|
||||
config file example:
|
||||
|
||||
```yaml
|
||||
@@ -678,6 +697,7 @@ config file example:
|
||||
flags:
|
||||
dthumb # disable ALL thumbnails and audio transcoding
|
||||
dvthumb # only disable video thumbnails
|
||||
ext-th: exe=/ico/exe.png # /ico/exe.png is the thumbnail of *.exe
|
||||
th-covers: folder.png,folder.jpg,cover.png,cover.jpg # the default
|
||||
```
|
||||
|
||||
@@ -761,8 +781,11 @@ the up2k UI is the epitome of polished intuitive experiences:
|
||||
* "parallel uploads" specifies how many chunks to upload at the same time
|
||||
* `[🏃]` analysis of other files should continue while one is uploading
|
||||
* `[🥔]` shows a simpler UI for faster uploads from slow devices
|
||||
* `[🛡️]` decides when to overwrite existing files on the server
|
||||
* `🛡️` = never (generate a new filename instead)
|
||||
* `🕒` = overwrite if the server-file is older
|
||||
* `♻️` = always overwrite if the files are different
|
||||
* `[🎲]` generate random filenames during upload
|
||||
* `[📅]` preserve last-modified timestamps; server times will match yours
|
||||
* `[🔎]` switch between upload and [file-search](#file-search) mode
|
||||
* ignore `[🔎]` if you add files by dragging them into the browser
|
||||
|
||||
@@ -1979,6 +2002,26 @@ in summary, `haproxy > caddy > traefik > nginx > apache > lighttpd`, and use uds
|
||||
* if these results are bullshit because my config exampels are bad, please submit corrections!
|
||||
|
||||
|
||||
## permanent cloudflare tunnel
|
||||
|
||||
if you have a domain and want to get your copyparty online real quick, either from your home-PC behind a CGNAT or from a server without an existing [reverse-proxy](#reverse-proxy) setup, one approach is to create a [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/) (formerly "Argo Tunnel")
|
||||
|
||||
I'd recommend making a `Locally-managed tunnel` for more control, but if you prefer to make a `Remotely-managed tunnel` then this is currently how:
|
||||
|
||||
* `cloudflare dashboard` » `zero trust` » `networks` » `tunnels` » `create a tunnel` » `cloudflared` » choose a cool `subdomain` and leave the `path` blank, and use `service type` = `http` and `URL` = `127.0.0.1:3923`
|
||||
|
||||
* and if you want to just run the tunnel without installing it, skip the `cloudflared service install BASE64` step and instead do `cloudflared --no-autoupdate tunnel run --token BASE64`
|
||||
|
||||
NOTE: since people will be connecting through cloudflare, as mentioned in [real-ip](#real-ip) you should run copyparty with `--xff-hdr cf-connecting-ip` to detect client IPs correctly
|
||||
|
||||
config file example:
|
||||
|
||||
```yaml
|
||||
[global]
|
||||
xff-hdr: cf-connecting-ip
|
||||
```
|
||||
|
||||
|
||||
## prometheus
|
||||
|
||||
metrics/stats can be enabled at URL `/.cpr/metrics` for grafana / prometheus / etc (openmetrics 1.0.0)
|
||||
@@ -2065,6 +2108,18 @@ in a config file, this is the same as:
|
||||
run copyparty with `--mimes` to list all the default mappings
|
||||
|
||||
|
||||
### GDPR compliance
|
||||
|
||||
imagine using copyparty professionally... **TINLA/IANAL; EU laws are hella confusing**
|
||||
|
||||
* remember to disable logging, or configure logrotation to an acceptable timeframe with `-lo cpp-%Y-%m%d.txt.xz` or similar
|
||||
|
||||
* if running with the database enabled (recommended), then have it forget uploader-IPs after some time using `--forget-ip 43200`
|
||||
* don't set it too low; [unposting](#unpost) a file is no longer possible after this takes effect
|
||||
|
||||
* if you actually *are* a lawyer then I'm open for feedback, would be fun
|
||||
|
||||
|
||||
### feature chickenbits
|
||||
|
||||
buggy feature? rip it out by setting any of the following environment variables to disable its associated bell or whistle,
|
||||
@@ -2252,6 +2307,7 @@ quick summary of more eccentric web-browsers trying to view a directory index:
|
||||
| **ie4** and **netscape** 4.0 | can browse, upload with `?b=u`, auth with `&pw=wark` |
|
||||
| **ncsa mosaic** 2.7 | does not get a pass, [pic1](https://user-images.githubusercontent.com/241032/174189227-ae816026-cf6f-4be5-a26e-1b3b072c1b2f.png) - [pic2](https://user-images.githubusercontent.com/241032/174189225-5651c059-5152-46e9-ac26-7e98e497901b.png) |
|
||||
| **SerenityOS** (7e98457) | hits a page fault, works with `?b=u`, file upload not-impl |
|
||||
| **sony psp** 5.50 | can browse, upload/mkdir/msg (thx dwarf) [screenshot](https://github.com/user-attachments/assets/9d21f020-1110-4652-abeb-6fc09c533d4f) |
|
||||
| **nintendo 3ds** | can browse, upload, view thumbnails (thx bnjmn) |
|
||||
|
||||
<p align="center"><img src="https://github.com/user-attachments/assets/88deab3d-6cad-4017-8841-2f041472b853" /></p>
|
||||
@@ -2582,6 +2638,8 @@ enable [smb](#smb-server) support (**not** recommended): `impacket==0.12.0`
|
||||
|
||||
`pyvips` gives higher quality thumbnails than `Pillow` and is 320% faster, using 270% more ram: `sudo apt install libvips42 && python3 -m pip install --user -U pyvips`
|
||||
|
||||
to install FFmpeg on Windows, grab [a recent build](https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z) -- you need `ffmpeg.exe` and `ffprobe.exe` from inside the `bin` folder; copy them into `C:\Windows\System32` or any other folder that's in your `%PATH%`
|
||||
|
||||
|
||||
### dependency chickenbits
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ each plugin must define a `main()` which takes 3 arguments;
|
||||
## on404
|
||||
|
||||
* [redirect.py](redirect.py) sends an HTTP 301 or 302, redirecting the client to another page/file
|
||||
* [randpic.py](randpic.py) redirects `/foo/bar/randpic.jpg` to a random pic in `/foo/bar/`
|
||||
* [sorry.py](answer.py) replies with a custom message instead of the usual 404
|
||||
* [nooo.py](nooo.py) replies with an endless noooooooooooooo
|
||||
* [never404.py](never404.py) 100% guarantee that 404 will never be a thing again as it automatically creates dummy files whenever necessary
|
||||
|
||||
35
bin/handlers/randpic.py
Normal file
35
bin/handlers/randpic.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import os
|
||||
import random
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
# assuming /foo/bar/ is a valid URL but /foo/bar/randpic.png does not exist,
|
||||
# hijack the 404 with a redirect to a random pic in that folder
|
||||
#
|
||||
# thx to lia & kipu for the idea
|
||||
|
||||
|
||||
def main(cli, vn, rem):
|
||||
req_fn = rem.split("/")[-1]
|
||||
if not cli.can_read or not req_fn.startswith("randpic"):
|
||||
return
|
||||
|
||||
req_abspath = vn.canonical(rem)
|
||||
req_ap_dir = os.path.dirname(req_abspath)
|
||||
files_in_dir = os.listdir(req_ap_dir)
|
||||
|
||||
if "." in req_fn:
|
||||
file_ext = "." + req_fn.split(".")[-1]
|
||||
files_in_dir = [x for x in files_in_dir if x.lower().endswith(file_ext)]
|
||||
|
||||
if not files_in_dir:
|
||||
return
|
||||
|
||||
selected_file = random.choice(files_in_dir)
|
||||
|
||||
req_url = "/".join([vn.vpath, rem]).strip("/")
|
||||
req_dir = req_url.rsplit("/", 1)[0]
|
||||
new_url = "/".join([req_dir, quote(selected_file)]).strip("/")
|
||||
|
||||
cli.reply(b"redirecting...", 302, headers={"Location": "/" + new_url})
|
||||
return "true"
|
||||
@@ -9,7 +9,7 @@ function eject_cb() {
|
||||
if (t.indexOf('can be safely unplugged') < 0 && t.indexOf('Device can be removed') < 0)
|
||||
return toast.err(30, 'usb eject failed:\n\n' + t);
|
||||
|
||||
toast.ok(5, esc(t.replace(/ - /g, '\n\n')));
|
||||
toast.ok(5, esc(t.replace(/ - /g, '\n\n')).trim());
|
||||
usbclick(); setTimeout(usbclick, 10);
|
||||
};
|
||||
|
||||
@@ -19,12 +19,14 @@ function add_eject_2(a) {
|
||||
return;
|
||||
|
||||
var v = aw[2],
|
||||
k = 'umount_' + v;
|
||||
k = 'umount_' + v,
|
||||
o = ebi(k);
|
||||
|
||||
if (o)
|
||||
o.parentNode.removeChild(o);
|
||||
|
||||
qsr('#' + k);
|
||||
a.appendChild(mknod('span', k, '⏏'), a);
|
||||
|
||||
var o = ebi(k);
|
||||
o = ebi(k);
|
||||
o.style.cssText = 'position:absolute; right:1em; margin-top:-.2em; font-size:1.3em';
|
||||
o.onclick = function (e) {
|
||||
ev(e);
|
||||
@@ -38,8 +40,9 @@ function add_eject_2(a) {
|
||||
};
|
||||
|
||||
function add_eject() {
|
||||
for (var a of QSA('#treeul a[href^="/usb/"]'))
|
||||
add_eject_2(a);
|
||||
var o = QSA('#treeul a[href^="/usb/"]');
|
||||
for (var a = o.length - 1; a > 0; a--)
|
||||
add_eject_2(o[a]);
|
||||
};
|
||||
|
||||
(function() {
|
||||
|
||||
@@ -14,13 +14,13 @@ remove those flashdrives, then boy howdy are you in the right place :D
|
||||
put usb-eject.js in the webroot (or somewhere else http-accessible)
|
||||
then run copyparty with these args:
|
||||
|
||||
-v /run/media/ed:/usb:A:c,hist=/tmp/junk
|
||||
-v /run/media/egon:/usb:A:c,hist=/tmp/junk
|
||||
--xm=c1,bin/hooks/usb-eject.py
|
||||
--js-browser=/usb-eject.js
|
||||
|
||||
which does the following respectively,
|
||||
|
||||
* share all of /run/media/ed as /usb with admin for everyone
|
||||
* share all of /run/media/egon as /usb with admin for everyone
|
||||
and put the histpath somewhere it won't cause trouble
|
||||
* run the usb-eject hook with stdout redirect to the web-ui
|
||||
* add the complementary usb-eject.js to the browser
|
||||
@@ -31,15 +31,24 @@ which does the following respectively,
|
||||
def main():
|
||||
try:
|
||||
label = sys.argv[1].split(":usb-eject:")[1].split(":")[0]
|
||||
mp = "/run/media/ed/" + label
|
||||
mp = "/run/media/egon/" + label
|
||||
# print("ejecting [%s]... " % (mp,), end="")
|
||||
mp = os.path.abspath(os.path.realpath(mp.encode("utf-8")))
|
||||
st = os.lstat(mp)
|
||||
if not stat.S_ISDIR(st.st_mode):
|
||||
raise Exception("not a regular directory")
|
||||
|
||||
cmd = [b"gio", b"mount", b"-e", mp]
|
||||
print(sp.check_output(cmd).decode("utf-8", "replace").strip())
|
||||
# if you're running copyparty as root (thx for the faith)
|
||||
# you'll need something like this to make dbus talkative
|
||||
cmd = b"sudo -u egon DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus gio mount -e"
|
||||
|
||||
# but if copyparty and the ui-session is running
|
||||
# as the same user (good) then this is plenty
|
||||
cmd = b"gio mount -e"
|
||||
|
||||
cmd = cmd.split(b" ") + [mp]
|
||||
ret = sp.check_output(cmd).decode("utf-8", "replace")
|
||||
print(ret.strip() or (label + " can be safely unplugged"))
|
||||
|
||||
except Exception as ex:
|
||||
print("unmount failed: %r" % (ex,))
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
S_VERSION = "2.9"
|
||||
S_BUILD_DT = "2025-01-27"
|
||||
S_VERSION = "2.10"
|
||||
S_BUILD_DT = "2025-02-19"
|
||||
|
||||
"""
|
||||
u2c.py: upload to copyparty
|
||||
@@ -807,7 +807,9 @@ def handshake(ar, file, search):
|
||||
else:
|
||||
if ar.touch:
|
||||
req["umod"] = True
|
||||
if ar.ow:
|
||||
if ar.owo:
|
||||
req["replace"] = "mt"
|
||||
elif ar.ow:
|
||||
req["replace"] = True
|
||||
|
||||
file.recheck = False
|
||||
@@ -1538,6 +1540,7 @@ source file/folder selection uses rsync syntax, meaning that:
|
||||
ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible")
|
||||
ap.add_argument("--touch", action="store_true", help="if last-modified timestamps differ, push local to server (need write+delete perms)")
|
||||
ap.add_argument("--ow", action="store_true", help="overwrite existing files instead of autorenaming")
|
||||
ap.add_argument("--owo", action="store_true", help="overwrite existing files if server-file is older")
|
||||
ap.add_argument("--spd", action="store_true", help="print speeds for each file")
|
||||
ap.add_argument("--version", action="store_true", help="show version and exit")
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: icxes <dev.null@need.moe>
|
||||
pkgname=copyparty
|
||||
pkgver="1.16.11"
|
||||
pkgver="1.16.15"
|
||||
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=("c8b594e9ca22d81a316ee8b10a9f5f4ebb11f0e4767738e0cb1b1704b13b2d70")
|
||||
sha256sums=("080844a7458073c86714dde5fa659a64120909568005420ff81a27e353e91a72")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
|
||||
@@ -64,4 +64,5 @@ in stdenv.mkDerivation {
|
||||
--set PATH '${lib.makeBinPath ([ utillinux ] ++ lib.optional withMediaProcessing ffmpeg)}:$PATH' \
|
||||
--add-flags "$out/share/copyparty-sfx.py"
|
||||
'';
|
||||
meta.mainProgram = "copyparty";
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.16.11/copyparty-sfx.py",
|
||||
"version": "1.16.11",
|
||||
"hash": "sha256-Zl8f6mzWamwDeTnL5T5i7NdgIqYHy601oEWw8JPrPrI="
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.16.15/copyparty-sfx.py",
|
||||
"version": "1.16.15",
|
||||
"hash": "sha256-vpI/N01pCeCiAv3haE1Zac1X9Oyp2vqPAC1WEcl5phA="
|
||||
}
|
||||
@@ -1039,6 +1039,7 @@ def add_upload(ap):
|
||||
ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; [\033[32m-1\033[0m] = forbidden/always-off, [\033[32m0\033[0m] = default-off and warn if enabled, [\033[32m1\033[0m] = default-off, [\033[32m2\033[0m] = on, [\033[32m3\033[0m] = on and disable datecheck")
|
||||
ap2.add_argument("--u2j", metavar="JOBS", type=int, default=2, help="web-client: number of file chunks to upload in parallel; 1 or 2 is good for low-latency (same-country) connections, 4-8 for android clients, 16 for cross-atlantic (max=64)")
|
||||
ap2.add_argument("--u2sz", metavar="N,N,N", type=u, default="1,64,96", help="web-client: default upload chunksize (MiB); sets \033[33mmin,default,max\033[0m in the settings gui. Each HTTP POST will aim for \033[33mdefault\033[0m, and never exceed \033[33mmax\033[0m. Cloudflare max is 96. Big values are good for cross-atlantic but may increase HDD fragmentation on some FS. Disable this optimization with [\033[32m1,1,1\033[0m]")
|
||||
ap2.add_argument("--u2ow", metavar="NUM", type=int, default=0, help="web-client: default setting for when to overwrite existing files; [\033[32m0\033[0m]=never, [\033[32m1\033[0m]=if-client-newer, [\033[32m2\033[0m]=always (volflag=u2ow)")
|
||||
ap2.add_argument("--u2sort", metavar="TXT", type=u, default="s", help="upload order; [\033[32ms\033[0m]=smallest-first, [\033[32mn\033[0m]=alphabetical, [\033[32mfs\033[0m]=force-s, [\033[32mfn\033[0m]=force-n -- alphabetical is a bit slower on fiber/LAN but makes it easier to eyeball if everything went fine")
|
||||
ap2.add_argument("--write-uplog", action="store_true", help="write POST reports to textfiles in working-directory")
|
||||
|
||||
@@ -1183,6 +1184,7 @@ def add_webdav(ap):
|
||||
ap2.add_argument("--dav-mac", action="store_true", help="disable apple-garbage filter -- allow macos to create junk files (._* and .DS_Store, .Spotlight-*, .fseventsd, .Trashes, .AppleDouble, __MACOS)")
|
||||
ap2.add_argument("--dav-rt", action="store_true", help="show symlink-destination's lastmodified instead of the link itself; always enabled for recursive listings (volflag=davrt)")
|
||||
ap2.add_argument("--dav-auth", action="store_true", help="force auth for all folders (required by davfs2 when only some folders are world-readable) (volflag=davauth)")
|
||||
ap2.add_argument("--dav-ua1", metavar="PTN", type=u, default=r" kioworker/", help="regex of tricky user-agents which expect 401 from GET requests; disable with [\033[32mno\033[0m] or blank")
|
||||
|
||||
|
||||
def add_tftp(ap):
|
||||
@@ -1268,7 +1270,7 @@ def add_optouts(ap):
|
||||
ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)")
|
||||
ap2.add_argument("--no-lifetime", action="store_true", help="do not allow clients (or server config) to schedule an upload to be deleted after a given time")
|
||||
ap2.add_argument("--no-pipe", action="store_true", help="disable race-the-beam (lockstep download of files which are currently being uploaded) (volflag=nopipe)")
|
||||
ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader IPs into the database")
|
||||
ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader-IP into the database; will also disable unpost, you may want \033[32m--forget-ip\033[0m instead (volflag=no_db_ip)")
|
||||
|
||||
|
||||
def add_safety(ap):
|
||||
@@ -1395,7 +1397,7 @@ def add_transcoding(ap):
|
||||
|
||||
def add_rss(ap):
|
||||
ap2 = ap.add_argument_group('RSS options')
|
||||
ap2.add_argument("--rss", action="store_true", help="enable RSS output (experimental)")
|
||||
ap2.add_argument("--rss", action="store_true", help="enable RSS output (experimental) (volflag=rss)")
|
||||
ap2.add_argument("--rss-nf", metavar="HITS", type=int, default=250, help="default number of files to return (url-param 'nf')")
|
||||
ap2.add_argument("--rss-fext", metavar="E,E", type=u, default="", help="default list of file extensions to include (url-param 'fext'); blank=all")
|
||||
ap2.add_argument("--rss-sort", metavar="ORD", type=u, default="m", help="default sort order (url-param 'sort'); [\033[32mm\033[0m]=last-modified [\033[32mu\033[0m]=upload-time [\033[32mn\033[0m]=filename [\033[32ms\033[0m]=filesize; Uppercase=oldest-first. Note that upload-time is 0 for non-uploaded files")
|
||||
@@ -1418,6 +1420,7 @@ def add_db_general(ap, hcores):
|
||||
ap2.add_argument("--no-dhash", action="store_true", help="disable rescan acceleration; do full database integrity check -- makes the db ~5%% smaller and bootup/rescans 3~10x slower")
|
||||
ap2.add_argument("--re-dhash", action="store_true", help="force a cache rebuild on startup; enable this once if it gets out of sync (should never be necessary)")
|
||||
ap2.add_argument("--no-forget", action="store_true", help="never forget indexed files, even when deleted from disk -- makes it impossible to ever upload the same file twice -- only useful for offloading uploads to a cloud service or something (volflag=noforget)")
|
||||
ap2.add_argument("--forget-ip", metavar="MIN", type=int, default=0, help="remove uploader-IP from database (and make unpost impossible) \033[33mMIN\033[0m minutes after upload, for GDPR reasons. Default [\033[32m0\033[0m] is never-forget. [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month. (volflag=forget_ip)")
|
||||
ap2.add_argument("--dbd", metavar="PROFILE", default="wal", help="database durability profile; sets the tradeoff between robustness and speed, see \033[33m--help-dbd\033[0m (volflag=dbd)")
|
||||
ap2.add_argument("--xlink", action="store_true", help="on upload: check all volumes for dupes, not just the target volume (probably buggy, not recommended) (volflag=xlink)")
|
||||
ap2.add_argument("--hash-mt", metavar="CORES", type=int, default=hcores, help="num cpu cores to use for file hashing; set 0 or 1 for single-core hashing")
|
||||
@@ -1486,7 +1489,9 @@ def add_ui(ap, retry):
|
||||
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("--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)")
|
||||
ap2.add_argument("--mpmc", metavar="URL", type=u, default="", help="change the mediaplayer-toggle mouse cursor; URL to a folder with {2..5}.png inside (or disable with [\033[32m.\033[0m])")
|
||||
ap2.add_argument("--spinner", metavar="TXT", type=u, default="🌲", help="\033[33memoji\033[0m or \033[33memoji,css\033[0m Example: [\033[32m🥖,padding:0\033[0m]")
|
||||
ap2.add_argument("--css-browser", metavar="L", type=u, default="", help="URL to additional CSS to include in the filebrowser html")
|
||||
ap2.add_argument("--js-browser", metavar="L", type=u, default="", help="URL to additional JS to include in the filebrowser html")
|
||||
ap2.add_argument("--js-other", metavar="L", type=u, default="", help="URL to additional JS to include in all other pages")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (1, 16, 12)
|
||||
VERSION = (1, 16, 16)
|
||||
CODENAME = "COPYparty"
|
||||
BUILD_DT = (2025, 2, 9)
|
||||
BUILD_DT = (2025, 2, 28)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
@@ -1289,10 +1289,10 @@ class AuthSrv(object):
|
||||
# one or more bools before the final flag; eat them
|
||||
n1, uname = uname.split(",", 1)
|
||||
for _, vp, _, _ in vols:
|
||||
self._read_volflag(flags[vp], n1, True, False)
|
||||
self._read_volflag(vp, flags[vp], n1, True, False)
|
||||
|
||||
for _, vp, _, _ in vols:
|
||||
self._read_volflag(flags[vp], uname, cval, False)
|
||||
self._read_volflag(vp, flags[vp], uname, cval, False)
|
||||
|
||||
return
|
||||
|
||||
@@ -1379,20 +1379,42 @@ class AuthSrv(object):
|
||||
|
||||
def _read_volflag(
|
||||
self,
|
||||
vpath: str,
|
||||
flags: dict[str, Any],
|
||||
name: str,
|
||||
value: Union[str, bool, list[str]],
|
||||
is_list: bool,
|
||||
) -> None:
|
||||
if name not in flagdescs:
|
||||
name = name.lower()
|
||||
|
||||
# volflags are snake_case, but a leading dash is the removal operator
|
||||
stripped = name.lstrip("-")
|
||||
zi = len(name) - len(stripped)
|
||||
if zi > 1:
|
||||
t = "WARNING: the config for volume [/%s] specified a volflag with multiple leading hyphens (%s); use one hyphen to remove, or zero hyphens to add a flag. Will now enable flag [%s]"
|
||||
self.log(t % (vpath, name, stripped), 3)
|
||||
name = stripped
|
||||
zi = 0
|
||||
|
||||
if stripped not in flagdescs and "-" in stripped:
|
||||
name = ("-" * zi) + stripped.replace("-", "_")
|
||||
|
||||
desc = flagdescs.get(name.lstrip("-"), "?").replace("\n", " ")
|
||||
|
||||
if not name:
|
||||
self._e("└─unreadable-line")
|
||||
t = "WARNING: the config for volume [/%s] indicated that a volflag was to be defined, but the volflag name was blank"
|
||||
self.log(t % (vpath,), 3)
|
||||
return
|
||||
|
||||
if re.match("^-[^-]+$", name):
|
||||
t = "└─unset volflag [{}] ({})"
|
||||
self._e(t.format(name[1:], desc))
|
||||
flags[name] = True
|
||||
return
|
||||
|
||||
zs = "mtp on403 on404 xbu xau xiu xbc xac xbr xar xbd xad xm xban"
|
||||
zs = "ext_th 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 [{}] = {} ({})"
|
||||
@@ -1557,6 +1579,18 @@ class AuthSrv(object):
|
||||
vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
|
||||
vol.root = vfs
|
||||
|
||||
zs = "neversymlink"
|
||||
k_ign = set(zs.split())
|
||||
for vol in vfs.all_vols.values():
|
||||
unknown_flags = set()
|
||||
for k, v in vol.flags.items():
|
||||
ks = k.lstrip("-")
|
||||
if ks not in flagdescs and ks not in k_ign:
|
||||
unknown_flags.add(k)
|
||||
if unknown_flags:
|
||||
t = "WARNING: the config for volume [/%s] has unrecognized volflags; will ignore: '%s'"
|
||||
self.log(t % (vol.vpath, "', '".join(unknown_flags)), 3)
|
||||
|
||||
enshare = self.args.shr
|
||||
shr = enshare[1:-1]
|
||||
shrs = enshare[1:]
|
||||
@@ -1918,11 +1952,8 @@ class AuthSrv(object):
|
||||
if vf not in vol.flags:
|
||||
vol.flags[vf] = getattr(self.args, ga)
|
||||
|
||||
for k in ("nrand",):
|
||||
if k not in vol.flags:
|
||||
vol.flags[k] = getattr(self.args, k)
|
||||
|
||||
for k in ("nrand", "u2abort", "ups_who", "zip_who"):
|
||||
zs = "forget_ip nrand u2abort u2ow ups_who zip_who"
|
||||
for k in zs.split():
|
||||
if k in vol.flags:
|
||||
vol.flags[k] = int(vol.flags[k])
|
||||
|
||||
@@ -1974,8 +2005,10 @@ class AuthSrv(object):
|
||||
|
||||
# append additive args from argv to volflags
|
||||
hooks = "xbu xau xiu xbc xac xbr xar xbd xad xm xban".split()
|
||||
for name in "mtp on404 on403".split() + hooks:
|
||||
self._read_volflag(vol.flags, name, getattr(self.args, name), True)
|
||||
for name in "ext_th mtp on404 on403".split() + hooks:
|
||||
self._read_volflag(
|
||||
vol.vpath, vol.flags, name, getattr(self.args, name), True
|
||||
)
|
||||
|
||||
for hn in hooks:
|
||||
cmds = vol.flags.get(hn)
|
||||
@@ -2003,6 +2036,16 @@ class AuthSrv(object):
|
||||
ncmds.append(ocmd)
|
||||
vol.flags[hn] = ncmds
|
||||
|
||||
ext_th = vol.flags["ext_th_d"] = {}
|
||||
etv = "(?)"
|
||||
try:
|
||||
for etv in vol.flags.get("ext_th") or []:
|
||||
k, v = etv.split("=")
|
||||
ext_th[k] = v
|
||||
except:
|
||||
t = "WARNING: volume [/%s]: invalid value specified for ext-th: %s"
|
||||
self.log(t % (vol.vpath, etv), 3)
|
||||
|
||||
# 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):
|
||||
@@ -2134,8 +2177,13 @@ class AuthSrv(object):
|
||||
for vol in vfs.all_nodes.values():
|
||||
for k in list(vol.flags.keys()):
|
||||
if re.match("^-[^-]+$", k):
|
||||
vol.flags.pop(k[1:], None)
|
||||
vol.flags.pop(k)
|
||||
zs = k[1:]
|
||||
if zs in vol.flags:
|
||||
vol.flags.pop(k[1:])
|
||||
else:
|
||||
t = "WARNING: the config for volume [/%s] tried to remove volflag [%s] by specifying [%s] but that volflag was not already set"
|
||||
self.log(t % (vol.vpath, zs, k), 3)
|
||||
|
||||
if vol.flags.get("dots"):
|
||||
for name in vol.axs.uread:
|
||||
@@ -2354,6 +2402,7 @@ class AuthSrv(object):
|
||||
"sb_lg": "" if "no_sb_lg" in vf else (vf.get("lg_sbf") or "y"),
|
||||
}
|
||||
js_htm = {
|
||||
"SPINNER": self.args.spinner,
|
||||
"s_name": self.args.bname,
|
||||
"have_up2k_idx": "e2d" in vf,
|
||||
"have_acode": not self.args.no_acode,
|
||||
@@ -2363,6 +2412,7 @@ class AuthSrv(object):
|
||||
"have_del": not self.args.no_del,
|
||||
"have_unpost": int(self.args.unpost),
|
||||
"have_emp": self.args.emp,
|
||||
"ext_th": vf.get("ext_th_d") or {},
|
||||
"sb_md": "" if "no_sb_md" in vf else (vf.get("md_sbf") or "y"),
|
||||
"sba_md": vf.get("md_sba") or "",
|
||||
"sba_lg": vf.get("lg_sba") or "",
|
||||
@@ -2383,6 +2433,7 @@ class AuthSrv(object):
|
||||
"u2j": self.args.u2j,
|
||||
"u2sz": self.args.u2sz,
|
||||
"u2ts": vf["u2ts"],
|
||||
"u2ow": vf["u2ow"],
|
||||
"frand": bool(vf.get("rand")),
|
||||
"lifetime": vn.js_ls["lifetime"],
|
||||
"u2sort": self.args.u2sort,
|
||||
@@ -2767,7 +2818,9 @@ class AuthSrv(object):
|
||||
zs = "c ihead ohead mtm mtp on403 on404 xac xad xar xau xiu xban xbc xbd xbr xbu xm"
|
||||
lst = set(zs.split())
|
||||
askip = set("a v c vc cgen exp_lg exp_md theme".split())
|
||||
fskip = set("exp_lg exp_md mv_re_r mv_re_t rm_re_r rm_re_t".split())
|
||||
|
||||
t = "exp_lg exp_md ext_th_d mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot"
|
||||
fskip = set(t.split())
|
||||
|
||||
# keymap from argv to vflag
|
||||
amap = vf_bmap()
|
||||
|
||||
@@ -5,6 +5,9 @@ from __future__ import print_function, unicode_literals
|
||||
zs = "a c e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vp e2vu ed emp i j lo mcr mte mth mtm mtp nb nc nid nih nth nw p q s ss sss v z zv"
|
||||
onedash = set(zs.split())
|
||||
|
||||
# verify that all volflags are documented here:
|
||||
# grep volflag= __main__.py | sed -r 's/.*volflag=//;s/\).*//' | sort | uniq | while IFS= read -r x; do grep -E "\"$x(=[^ \"]+)?\": \"" cfg.py || printf '%s\n' "$x"; done
|
||||
|
||||
|
||||
def vf_bmap() -> dict[str, str]:
|
||||
"""argv-to-volflag: simple bools"""
|
||||
@@ -40,6 +43,7 @@ def vf_bmap() -> dict[str, str]:
|
||||
"gsel",
|
||||
"hardlink",
|
||||
"magic",
|
||||
"no_db_ip",
|
||||
"no_sb_md",
|
||||
"no_sb_lg",
|
||||
"nsort",
|
||||
@@ -70,6 +74,7 @@ def vf_vmap() -> dict[str, str]:
|
||||
}
|
||||
for k in (
|
||||
"dbd",
|
||||
"forget_ip",
|
||||
"hsortn",
|
||||
"html_head",
|
||||
"lg_sbf",
|
||||
@@ -77,6 +82,7 @@ def vf_vmap() -> dict[str, str]:
|
||||
"lg_sba",
|
||||
"md_sba",
|
||||
"nrand",
|
||||
"u2ow",
|
||||
"og_desc",
|
||||
"og_site",
|
||||
"og_th",
|
||||
@@ -106,6 +112,7 @@ def vf_cmap() -> dict[str, str]:
|
||||
for k in (
|
||||
"exp_lg",
|
||||
"exp_md",
|
||||
"ext_th",
|
||||
"mte",
|
||||
"mth",
|
||||
"mtp",
|
||||
@@ -152,7 +159,8 @@ flagcats = {
|
||||
"daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
|
||||
"nosub": "forces all uploads into the top folder of the vfs",
|
||||
"magic": "enables filetype detection for nameless uploads",
|
||||
"gz": "allows server-side gzip of uploads with ?gz (also c,xz)",
|
||||
"gz": "allows server-side gzip compression of uploads with ?gz",
|
||||
"xz": "allows server-side lzma compression of uploads with ?xz",
|
||||
"pk": "forces server-side compression, optional arg: xz,9",
|
||||
},
|
||||
"upload rules": {
|
||||
@@ -163,6 +171,7 @@ flagcats = {
|
||||
"medialinks": "return medialinks for non-up2k uploads (not hotlinks)",
|
||||
"rand": "force randomized filenames, 9 chars long by default",
|
||||
"nrand=N": "randomized filenames are N chars long",
|
||||
"u2ow=N": "overwrite existing files? 0=no 1=if-older 2=always",
|
||||
"u2ts=fc": "[f]orce [c]lient-last-modified or [u]pload-time",
|
||||
"u2abort=1": "allow aborting unfinished uploads? 0=no 1=strict 2=ip-chk 3=acct-chk",
|
||||
"sz=1k-3m": "allow filesizes between 1 KiB and 3MiB",
|
||||
@@ -179,8 +188,11 @@ flagcats = {
|
||||
"e2dsa": "scans all folders for new files on startup; also sets -e2d",
|
||||
"e2t": "enable multimedia indexing; makes it possible to search for tags",
|
||||
"e2ts": "scan existing files for tags on startup; also sets -e2t",
|
||||
"e2tsa": "delete all metadata from DB (full rescan); also sets -e2ts",
|
||||
"e2tsr": "delete all metadata from DB (full rescan); also sets -e2ts",
|
||||
"d2ts": "disables metadata collection for existing files",
|
||||
"e2v": "verify integrity on startup by hashing files and comparing to db",
|
||||
"e2vu": "when e2v fails, update the db (assume on-disk files are good)",
|
||||
"e2vp": "when e2v fails, panic and quit copyparty",
|
||||
"d2ds": "disables onboot indexing, overrides -e2ds*",
|
||||
"d2t": "disables metadata collection, overrides -e2t*",
|
||||
"d2v": "disables file verification, overrides -e2v*",
|
||||
@@ -190,6 +202,8 @@ flagcats = {
|
||||
"nohash=\\.iso$": "skips hashing file contents if path matches *.iso",
|
||||
"noidx=\\.iso$": "fully ignores the contents at paths matching *.iso",
|
||||
"noforget": "don't forget files when deleted from disk",
|
||||
"forget_ip=43200": "forget uploader-IP after 30 days (GDPR)",
|
||||
"no_db_ip": "never store uploader-IP in the db; disables unpost",
|
||||
"fat32": "avoid excessive reindexing on android sdcardfs",
|
||||
"dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
|
||||
"xlink": "cross-volume dupe detection / linking (dangerous)",
|
||||
@@ -200,6 +214,8 @@ flagcats = {
|
||||
"srch_excl": "exclude search results with URL matching this regex",
|
||||
},
|
||||
'database, audio tags\n"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ...': {
|
||||
"mte=artist,title": "media-tags to index/display",
|
||||
"mth=fmt,res,ac": "media-tags to hide by default",
|
||||
"mtp=.bpm=f,audio-bpm.py": 'uses the "audio-bpm.py" program to\ngenerate ".bpm" tags from uploads (f = overwrite tags)',
|
||||
"mtp=ahash,vhash=media-hash.py": "collects two tags at once",
|
||||
},
|
||||
@@ -213,6 +229,7 @@ flagcats = {
|
||||
"crop": "center-cropping (y/n/fy/fn)",
|
||||
"th3x": "3x resolution (y/n/fy/fn)",
|
||||
"convt": "conversion timeout in seconds",
|
||||
"ext_th=s=/b.png": "use /b.png as thumbnail for file-extension s",
|
||||
},
|
||||
"handlers\n(better explained in --help-handlers)": {
|
||||
"on404=PY": "handle 404s by executing PY file",
|
||||
@@ -235,8 +252,12 @@ flagcats = {
|
||||
"grid": "show grid/thumbnails by default",
|
||||
"gsel": "select files in grid by ctrl-click",
|
||||
"sort": "default sort order",
|
||||
"nsort": "natural-sort of leading digits in filenames",
|
||||
"hsortn": "number of sort-rules to add to media URLs",
|
||||
"unlist": "dont list files matching REGEX",
|
||||
"html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH",
|
||||
"tcolor=#fc0": "theme color (a hint for webbrowsers, discord, etc.)",
|
||||
"nodirsz": "don't show total folder size",
|
||||
"robots": "allows indexing by search engines (default)",
|
||||
"norobots": "kindly asks search engines to leave",
|
||||
"no_sb_md": "disable js sandbox for markdown files",
|
||||
@@ -249,12 +270,36 @@ flagcats = {
|
||||
"lg_sba": "value of iframe allow-prop for *logue-sandbox",
|
||||
"nohtml": "return html and markdown as text/html",
|
||||
},
|
||||
"opengraph (discord embeds)": {
|
||||
"og": "enable OG (disables hotlinking)",
|
||||
"og_site": "sitename; defaults to --name, disable with '-'",
|
||||
"og_desc": "description text for all files; disable with '-'",
|
||||
"og_th=jf": "thumbnail format; j / jf / jf3 / w / w3 / ...",
|
||||
"og_title_a": "audio title format; default: {{ artist }} - {{ title }}",
|
||||
"og_title_v": "video title format; default: {{ title }}",
|
||||
"og_title_i": "image title format; default: {{ title }}",
|
||||
"og_title=foo": "fallback title if there's nothing in the db",
|
||||
"og_s_title": "force default title; do not read from tags",
|
||||
"og_tpl": "custom html; see --og-tpl in --help",
|
||||
"og_no_head": "you want to add tags manually with og_tpl",
|
||||
"og_ua": "if defined: only send OG html if useragent matches this regex",
|
||||
},
|
||||
"textfiles": {
|
||||
"exp": "enable textfile expansion; see --help-exp",
|
||||
"exp_md": "placeholders to expand in markdown files; see --help",
|
||||
"exp_lg": "placeholders to expand in prologue/epilogue; see --help",
|
||||
},
|
||||
"others": {
|
||||
"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',
|
||||
"dk=8": 'generates per-directory accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes',
|
||||
"dks": "per-directory accesskeys allow browsing into subdirs",
|
||||
"dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so',
|
||||
"rss": "allow '?rss' URL suffix (experimental)",
|
||||
"ups_who=2": "restrict viewing the list of recent uploads",
|
||||
"zip_who=2": "restrict access to download-as-zip/tar",
|
||||
"nopipe": "disable race-the-beam (download unfinished uploads)",
|
||||
"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",
|
||||
@@ -264,3 +309,10 @@ flagcats = {
|
||||
|
||||
|
||||
flagdescs = {k.split("=")[0]: v for tab in flagcats.values() for k, v in tab.items()}
|
||||
|
||||
|
||||
if True: # so it gets removed in release-builds
|
||||
for fun in [vf_bmap, vf_cmap, vf_vmap]:
|
||||
for k in fun().values():
|
||||
if k not in flagdescs:
|
||||
raise Exception("undocumented volflag: " + k)
|
||||
|
||||
@@ -152,6 +152,8 @@ RE_HSAFE = re.compile(r"[\x00-\x1f<>\"'&]") # search always much faster
|
||||
RE_HOST = re.compile(r"[^][0-9a-zA-Z.:_-]") # search faster <=17ch
|
||||
RE_MHOST = re.compile(r"^[][0-9a-zA-Z.:_-]+$") # match faster >=18ch
|
||||
RE_K = re.compile(r"[^0-9a-zA-Z_-]") # search faster <=17ch
|
||||
RE_HR = re.compile(r"[<>\"'&]")
|
||||
RE_MDV = re.compile(r"(.*)\.([0-9]+\.[0-9]{3})(\.[Mm][Dd])$")
|
||||
|
||||
UPARAM_CC_OK = set("doc move tree".split())
|
||||
|
||||
@@ -191,7 +193,7 @@ class HttpCli(object):
|
||||
self.is_vproxied = False
|
||||
self.in_hdr_recv = True
|
||||
self.headers: dict[str, str] = {}
|
||||
self.mode = " "
|
||||
self.mode = " " # http verb
|
||||
self.req = " "
|
||||
self.http_ver = ""
|
||||
self.hint = ""
|
||||
@@ -731,10 +733,10 @@ class HttpCli(object):
|
||||
return self.handle_unlock() and self.keepalive
|
||||
elif self.mode == "MKCOL":
|
||||
return self.handle_mkcol() and self.keepalive
|
||||
elif self.mode == "MOVE":
|
||||
return self.handle_move() and self.keepalive
|
||||
elif self.mode in ("MOVE", "COPY"):
|
||||
return self.handle_cpmv() and self.keepalive
|
||||
else:
|
||||
raise Pebkac(400, 'invalid HTTP mode "{0}"'.format(self.mode))
|
||||
raise Pebkac(400, 'invalid HTTP verb "{0}"'.format(self.mode))
|
||||
|
||||
except Exception as ex:
|
||||
if not isinstance(ex, Pebkac):
|
||||
@@ -1776,6 +1778,12 @@ class HttpCli(object):
|
||||
if "%" in self.req:
|
||||
self.log(" `-- %r" % (self.vpath,))
|
||||
|
||||
if self.args.no_dav:
|
||||
raise Pebkac(405, "WebDAV is disabled in server config")
|
||||
|
||||
if not self.can_write:
|
||||
raise Pebkac(401, "authenticate")
|
||||
|
||||
try:
|
||||
return self._mkdir(self.vpath, True)
|
||||
except Pebkac as ex:
|
||||
@@ -1785,14 +1793,36 @@ class HttpCli(object):
|
||||
self.reply(b"", ex.code)
|
||||
return True
|
||||
|
||||
def handle_move(self) -> bool:
|
||||
def handle_cpmv(self) -> bool:
|
||||
dst = self.headers["destination"]
|
||||
dst = re.sub("^https?://[^/]+", "", dst).lstrip()
|
||||
dst = unquotep(dst)
|
||||
if not self._mv(self.vpath, dst.lstrip("/")):
|
||||
return False
|
||||
|
||||
return True
|
||||
# dolphin (kioworker/6.10) "webdav://127.0.0.1:3923/a/b.txt"
|
||||
dst = re.sub("^[a-zA-Z]+://[^/]+", "", dst).lstrip()
|
||||
|
||||
if self.is_vproxied and dst.startswith(self.args.SRS):
|
||||
dst = dst[len(self.args.RS) :]
|
||||
|
||||
if self.do_log:
|
||||
self.log("%s %s --//> %s @%s" % (self.mode, self.req, dst, self.uname))
|
||||
if "%" in self.req:
|
||||
self.log(" `-- %r" % (self.vpath,))
|
||||
|
||||
if self.args.no_dav:
|
||||
raise Pebkac(405, "WebDAV is disabled in server config")
|
||||
|
||||
dst = unquotep(dst)
|
||||
|
||||
# overwrite=True is default; rfc4918 9.8.4
|
||||
zs = self.headers.get("overwrite", "").lower()
|
||||
overwrite = zs not in ["f", "false"]
|
||||
|
||||
try:
|
||||
fun = self._cp if self.mode == "COPY" else self._mv
|
||||
return fun(self.vpath, dst.lstrip("/"), overwrite)
|
||||
except Pebkac as ex:
|
||||
if ex.code == 403:
|
||||
ex.code = 401
|
||||
raise
|
||||
|
||||
def _applesan(self) -> bool:
|
||||
if self.args.dav_mac or "Darwin/" not in self.ua:
|
||||
@@ -4809,9 +4839,12 @@ class HttpCli(object):
|
||||
# that the client is not a graphical browser
|
||||
if (
|
||||
rc == 403
|
||||
and not self.pw
|
||||
and not self.ua.startswith("Mozilla/")
|
||||
and self.uname == "*"
|
||||
and "sec-fetch-site" not in self.headers
|
||||
and (
|
||||
not self.ua.startswith("Mozilla/")
|
||||
or (self.args.dav_ua1 and self.args.dav_ua1.search(self.ua))
|
||||
)
|
||||
):
|
||||
rc = 401
|
||||
self.out_headers["WWW-Authenticate"] = 'Basic realm="a"'
|
||||
@@ -5441,6 +5474,8 @@ class HttpCli(object):
|
||||
|
||||
def handle_rm(self, req: list[str]) -> bool:
|
||||
if not req and not self.can_delete:
|
||||
if self.mode == "DELETE" and self.uname == "*":
|
||||
raise Pebkac(401, "authenticate") # webdav
|
||||
raise Pebkac(403, "'delete' not allowed for user " + self.uname)
|
||||
|
||||
if self.args.no_del:
|
||||
@@ -5475,14 +5510,22 @@ class HttpCli(object):
|
||||
if not dst:
|
||||
raise Pebkac(400, "need dst vpath")
|
||||
|
||||
return self._mv(self.vpath, dst.lstrip("/"))
|
||||
return self._mv(self.vpath, dst.lstrip("/"), False)
|
||||
|
||||
def _mv(self, vsrc: str, vdst: str) -> bool:
|
||||
def _mv(self, vsrc: str, vdst: str, overwrite: bool) -> bool:
|
||||
if self.args.no_mv:
|
||||
raise Pebkac(403, "the rename/move feature is disabled in server config")
|
||||
|
||||
self.asrv.vfs.get(vsrc, self.uname, True, False, True)
|
||||
self.asrv.vfs.get(vdst, self.uname, False, True)
|
||||
# `handle_cpmv` will catch 403 from these and raise 401
|
||||
svn, srem = self.asrv.vfs.get(vsrc, self.uname, True, False, True)
|
||||
dvn, drem = self.asrv.vfs.get(vdst, self.uname, False, True)
|
||||
|
||||
if overwrite:
|
||||
dabs = dvn.canonical(drem)
|
||||
if bos.path.exists(dabs):
|
||||
self.log("overwriting %s" % (dabs,))
|
||||
self.asrv.vfs.get(vdst, self.uname, False, True, False, True)
|
||||
wunlink(self.log, dabs, dvn.flags)
|
||||
|
||||
x = self.conn.hsrv.broker.ask("up2k.handle_mv", self.uname, self.ip, vsrc, vdst)
|
||||
self.loud_reply(x.get(), status=201)
|
||||
@@ -5498,14 +5541,21 @@ class HttpCli(object):
|
||||
if not dst:
|
||||
raise Pebkac(400, "need dst vpath")
|
||||
|
||||
return self._cp(self.vpath, dst.lstrip("/"))
|
||||
return self._cp(self.vpath, dst.lstrip("/"), False)
|
||||
|
||||
def _cp(self, vsrc: str, vdst: str) -> bool:
|
||||
def _cp(self, vsrc: str, vdst: str, overwrite: bool) -> bool:
|
||||
if self.args.no_cp:
|
||||
raise Pebkac(403, "the copy feature is disabled in server config")
|
||||
|
||||
self.asrv.vfs.get(vsrc, self.uname, True, False)
|
||||
self.asrv.vfs.get(vdst, self.uname, False, True)
|
||||
svn, srem = self.asrv.vfs.get(vsrc, self.uname, True, False)
|
||||
dvn, drem = self.asrv.vfs.get(vdst, self.uname, False, True)
|
||||
|
||||
if overwrite:
|
||||
dabs = dvn.canonical(drem)
|
||||
if bos.path.exists(dabs):
|
||||
self.log("overwriting %s" % (dabs,))
|
||||
self.asrv.vfs.get(vdst, self.uname, False, True, False, True)
|
||||
wunlink(self.log, dabs, dvn.flags)
|
||||
|
||||
x = self.conn.hsrv.broker.ask("up2k.handle_cp", self.uname, self.ip, vsrc, vdst)
|
||||
self.loud_reply(x.get(), status=201)
|
||||
@@ -5921,7 +5971,7 @@ class HttpCli(object):
|
||||
# [num-backups, most-recent, hist-path]
|
||||
hist: dict[str, tuple[int, float, str]] = {}
|
||||
histdir = os.path.join(fsroot, ".hist")
|
||||
ptn = re.compile(r"(.*)\.([0-9]+\.[0-9]{3})(\.[^\.]+)$")
|
||||
ptn = RE_MDV
|
||||
try:
|
||||
for hfn in bos.listdir(histdir):
|
||||
m = ptn.match(hfn)
|
||||
@@ -5954,6 +6004,7 @@ class HttpCli(object):
|
||||
|
||||
dirs = []
|
||||
files = []
|
||||
ptn_hr = RE_HR
|
||||
for fn in ls_names:
|
||||
base = ""
|
||||
href = fn
|
||||
@@ -6008,11 +6059,13 @@ class HttpCli(object):
|
||||
zd.second,
|
||||
)
|
||||
|
||||
try:
|
||||
ext = "---" if is_dir else fn.rsplit(".", 1)[1]
|
||||
if is_dir:
|
||||
ext = "---"
|
||||
elif "." in fn:
|
||||
ext = ptn_hr.sub("@", fn.rsplit(".", 1)[1])
|
||||
if len(ext) > 16:
|
||||
ext = ext[:16]
|
||||
except:
|
||||
else:
|
||||
ext = "%"
|
||||
|
||||
if add_fk and not is_dir:
|
||||
|
||||
@@ -769,7 +769,7 @@ class SvcHub(object):
|
||||
vs = os.path.expandvars(os.path.expanduser(vs))
|
||||
setattr(al, k, vs)
|
||||
|
||||
for k in "sus_urls nonsus_urls".split(" "):
|
||||
for k in "dav_ua1 sus_urls nonsus_urls".split(" "):
|
||||
vs = getattr(al, k)
|
||||
if not vs or vs == "no":
|
||||
setattr(al, k, None)
|
||||
|
||||
@@ -557,6 +557,7 @@ class Up2k(object):
|
||||
else:
|
||||
# important; not deferred by db_act
|
||||
timeout = self._check_lifetimes()
|
||||
timeout = min(self._check_forget_ip(), timeout)
|
||||
try:
|
||||
if self.args.shr:
|
||||
timeout = min(self._check_shares(), timeout)
|
||||
@@ -617,6 +618,43 @@ class Up2k(object):
|
||||
for v in vols:
|
||||
volage[v] = now
|
||||
|
||||
def _check_forget_ip(self) -> float:
|
||||
now = time.time()
|
||||
timeout = now + 9001
|
||||
for vp, vol in sorted(self.vfs.all_vols.items()):
|
||||
maxage = vol.flags["forget_ip"]
|
||||
if not maxage:
|
||||
continue
|
||||
|
||||
cur = self.cur.get(vol.realpath)
|
||||
if not cur:
|
||||
continue
|
||||
|
||||
cutoff = now - maxage * 60
|
||||
|
||||
for _ in range(2):
|
||||
q = "select ip, at from up where ip > '' order by +at limit 1"
|
||||
hits = cur.execute(q).fetchall()
|
||||
if not hits:
|
||||
break
|
||||
|
||||
remains = hits[0][1] - cutoff
|
||||
if remains > 0:
|
||||
timeout = min(timeout, now + remains)
|
||||
break
|
||||
|
||||
q = "update up set ip = '' where ip > '' and at <= %d"
|
||||
cur.execute(q % (cutoff,))
|
||||
zi = cur.rowcount
|
||||
cur.connection.commit()
|
||||
|
||||
t = "forget-ip(%d) removed %d IPs from db [/%s]"
|
||||
self.log(t % (maxage, zi, vol.vpath))
|
||||
|
||||
timeout = min(timeout, now + 900)
|
||||
|
||||
return timeout
|
||||
|
||||
def _check_lifetimes(self) -> float:
|
||||
now = time.time()
|
||||
timeout = now + 9001
|
||||
@@ -1081,7 +1119,7 @@ class Up2k(object):
|
||||
ft = "\033[0;32m{}{:.0}"
|
||||
ff = "\033[0;35m{}{:.0}"
|
||||
fv = "\033[0;36m{}:\033[90m{}"
|
||||
zs = "html_head mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot"
|
||||
zs = "ext_th_d html_head mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot"
|
||||
fx = set(zs.split())
|
||||
fd = vf_bmap()
|
||||
fd.update(vf_cmap())
|
||||
@@ -2916,9 +2954,14 @@ class Up2k(object):
|
||||
self.salt, cj["size"], cj["lmod"], cj["prel"], cj["name"]
|
||||
)
|
||||
|
||||
if vfs.flags.get("up_ts", "") == "fu" or not cj["lmod"]:
|
||||
zi = cj["lmod"]
|
||||
bad_mt = zi <= 0 or zi > 0xAAAAAAAA
|
||||
if bad_mt or vfs.flags.get("up_ts", "") == "fu":
|
||||
# force upload time rather than last-modified
|
||||
cj["lmod"] = int(time.time())
|
||||
if zi and bad_mt:
|
||||
t = "ignoring impossible last-modified time from client: %s"
|
||||
self.log(t % (zi,), 6)
|
||||
|
||||
alts: list[tuple[int, int, dict[str, Any], "sqlite3.Cursor", str, str]] = []
|
||||
for ptop, cur in vols:
|
||||
@@ -3335,7 +3378,17 @@ class Up2k(object):
|
||||
return fname
|
||||
|
||||
fp = djoin(fdir, fname)
|
||||
if job.get("replace") and bos.path.exists(fp):
|
||||
|
||||
ow = job.get("replace") and bos.path.exists(fp)
|
||||
if ow and "mt" in str(job["replace"]).lower():
|
||||
mts = bos.stat(fp).st_mtime
|
||||
mtc = job["lmod"]
|
||||
if mtc < mts:
|
||||
t = "will not overwrite; server %d sec newer than client; %d > %d %r"
|
||||
self.log(t % (mts - mtc, mts, mtc, fp))
|
||||
ow = False
|
||||
|
||||
if ow:
|
||||
self.log("replacing existing file at %r" % (fp,))
|
||||
cur = None
|
||||
ptop = job["ptop"]
|
||||
@@ -3789,7 +3842,7 @@ class Up2k(object):
|
||||
db_ip = ""
|
||||
else:
|
||||
# plugins may expect this to look like an actual IP
|
||||
db_ip = "1.1.1.1" if self.args.no_db_ip else ip
|
||||
db_ip = "1.1.1.1" if "no_db_ip" in vflags else ip
|
||||
|
||||
sql = "insert into up values (?,?,?,?,?,?,?)"
|
||||
v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0))
|
||||
@@ -4628,12 +4681,12 @@ class Up2k(object):
|
||||
Optional[str],
|
||||
Optional[int],
|
||||
Optional[int],
|
||||
Optional[str],
|
||||
str,
|
||||
Optional[int],
|
||||
]:
|
||||
cur = self.cur.get(ptop)
|
||||
if not cur:
|
||||
return None, None, None, None, None, None
|
||||
return None, None, None, None, "", None
|
||||
|
||||
rd, fn = vsplit(vrem)
|
||||
q = "select w, mt, sz, ip, at from up where rd=? and fn=? limit 1"
|
||||
@@ -4647,7 +4700,7 @@ class Up2k(object):
|
||||
if hit:
|
||||
wark, ftime, fsize, ip, at = hit
|
||||
return cur, wark, ftime, fsize, ip, at
|
||||
return cur, None, None, None, None, None
|
||||
return cur, None, None, None, "", None
|
||||
|
||||
def _forget_file(
|
||||
self,
|
||||
|
||||
@@ -1695,15 +1695,24 @@ html.y #tree.nowrap .ntree a+a:hover {
|
||||
line-height: 0;
|
||||
}
|
||||
.dumb_loader_thing {
|
||||
display: inline-block;
|
||||
display: block;
|
||||
margin: 1em .3em 1em 1em;
|
||||
padding: 0 1.2em 0 0;
|
||||
font-size: 4em;
|
||||
min-width: 1em;
|
||||
min-height: 1em;
|
||||
opacity: 0;
|
||||
animation: 1s linear .15s infinite forwards spin, .2s ease .15s 1 forwards fadein;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: .3em;
|
||||
z-index: 9;
|
||||
}
|
||||
#dlt_t {
|
||||
left: 0;
|
||||
}
|
||||
#dlt_f {
|
||||
right: .5em;
|
||||
}
|
||||
#files .cfg {
|
||||
display: none;
|
||||
font-size: 2em;
|
||||
|
||||
@@ -151,7 +151,8 @@ var Ls = {
|
||||
|
||||
"ul_par": "parallel uploads:",
|
||||
"ut_rand": "randomize filenames",
|
||||
"ut_u2ts": "copy the last-modified timestamp$Nfrom your filesystem to the server",
|
||||
"ut_u2ts": "copy the last-modified timestamp$Nfrom your filesystem to the server\">📅",
|
||||
"ut_ow": "overwrite existing files on the server?$N🛡️: never (will generate a new filename instead)$N🕒: overwrite if server-file is older than yours$N♻️: always overwrite if the files are different",
|
||||
"ut_mt": "continue hashing other files while uploading$N$Nmaybe disable if your CPU or HDD is a bottleneck",
|
||||
"ut_ask": 'ask for confirmation before upload starts">💭',
|
||||
"ut_pot": "improve upload speed on slow devices$Nby making the UI less complex",
|
||||
@@ -538,6 +539,7 @@ var Ls = {
|
||||
"u_ewrite": 'you do not have write-access to this folder',
|
||||
"u_eread": 'you do not have read-access to this folder',
|
||||
"u_enoi": 'file-search is not enabled in server config',
|
||||
"u_enoow": "overwrite will not work here; need Delete-permission",
|
||||
"u_badf": 'These {0} files (of {1} total) were skipped, possibly due to filesystem permissions:\n\n',
|
||||
"u_blankf": 'These {0} files (of {1} total) are blank / empty; upload them anyways?\n\n',
|
||||
"u_just1": '\nMaybe it works better if you select just one file',
|
||||
@@ -751,7 +753,8 @@ var Ls = {
|
||||
|
||||
"ul_par": "samtidige handl.:",
|
||||
"ut_rand": "finn opp nye tilfeldige filnavn",
|
||||
"ut_u2ts": "gi filen på serveren samme$Ntidsstempel som lokalt hos deg",
|
||||
"ut_u2ts": "gi filen på serveren samme$Ntidsstempel som lokalt hos deg\">📅",
|
||||
"ut_ow": "overskrive eksisterende filer på serveren?$N🛡️: aldri (finner på et nytt filnavn istedenfor)$N🕒: overskriv hvis serverens fil er eldre$N♻️: alltid, gitt at innholdet er forskjellig",
|
||||
"ut_mt": "fortsett å befare køen mens opplastning foregår$N$Nskru denne av dersom du har en$Ntreg prosessor eller harddisk",
|
||||
"ut_ask": 'bekreft filutvalg før opplastning starter">💭',
|
||||
"ut_pot": "forbedre ytelsen på trege enheter ved å$Nforenkle brukergrensesnittet",
|
||||
@@ -1138,6 +1141,7 @@ var Ls = {
|
||||
"u_ewrite": 'du har ikke skrivetilgang i denne mappen',
|
||||
"u_eread": 'du har ikke lesetilgang i denne mappen',
|
||||
"u_enoi": 'filsøk er deaktivert i serverkonfigurasjonen',
|
||||
"u_enoow": "kan ikke overskrive filer her (Delete-rettigheten er nødvendig)",
|
||||
"u_badf": 'Disse {0} filene (av totalt {1}) kan ikke leses, kanskje pga rettighetsproblemer i filsystemet på datamaskinen din:\n\n',
|
||||
"u_blankf": 'Disse {0} filene (av totalt {1}) er blanke / uten innhold; ønsker du å laste dem opp uansett?\n\n',
|
||||
"u_just1": '\nFunker kanskje bedre hvis du bare tar én fil om gangen',
|
||||
@@ -1351,7 +1355,8 @@ var Ls = {
|
||||
|
||||
"ul_par": "并行上传:",
|
||||
"ut_rand": "随机化文件名",
|
||||
"ut_u2ts": "将最后修改的时间戳$N从你的文件系统复制到服务器",
|
||||
"ut_u2ts": "将最后修改的时间戳$N从你的文件系统复制到服务器\">📅",
|
||||
"ut_ow": "覆盖服务器上的现有文件?$N🛡️: 从不(会生成一个新文件名)$N🕒: 服务器文件较旧则覆盖$N♻️: 总是覆盖,如果文件内容不同", //m
|
||||
"ut_mt": "在上传时继续哈希其他文件$N$N如果你的 CPU 或硬盘是瓶颈,可能需要禁用",
|
||||
"ut_ask": '上传开始前询问确认">💭',
|
||||
"ut_pot": "通过简化 UI 来$N提高慢设备上的上传速度",
|
||||
@@ -1738,6 +1743,7 @@ var Ls = {
|
||||
"u_ewrite": '你对这个文件夹没有写入权限',
|
||||
"u_eread": '你对这个文件夹没有读取权限',
|
||||
"u_enoi": '文件搜索在服务器配置中未启用',
|
||||
"u_enoow": "无法覆盖此处的文件;需要删除权限", //m
|
||||
"u_badf": '这些 {0} 个文件(共 {1} 个)被跳过,可能是由于文件系统权限:\n\n',
|
||||
"u_blankf": '这些 {0} 个文件(共 {1} 个)是空白的;是否仍然上传?\n\n',
|
||||
"u_just1": '\n也许如果你只选择一个文件会更好',
|
||||
@@ -1918,8 +1924,8 @@ ebi('op_up2k').innerHTML = (
|
||||
' <label for="u2rand" tt="' + L.ut_rand + '">🎲</label>\n' +
|
||||
' </td>\n' +
|
||||
' <td class="c" rowspan="2">\n' +
|
||||
' <input type="checkbox" id="u2ts" />\n' +
|
||||
' <label for="u2ts" tt="' + L.ut_u2ts + '">📅</a>\n' +
|
||||
' <input type="checkbox" id="u2ow" />\n' +
|
||||
' <label for="u2ow" tt="' + L.ut_ow + '">?</a>\n' +
|
||||
' </td>\n' +
|
||||
' <td class="c" data-perm="read" data-dep="idx" rowspan="2">\n' +
|
||||
' <input type="checkbox" id="fsearch" />\n' +
|
||||
@@ -2037,6 +2043,7 @@ ebi('op_cfg').innerHTML = (
|
||||
' <h3>' + L.cl_uopts + '</h3>\n' +
|
||||
' <div>\n' +
|
||||
' <a id="ask_up" class="tgl btn" href="#" tt="' + L.ut_ask + '</a>\n' +
|
||||
' <a id="u2ts" class="tgl btn" href="#" tt="' + L.ut_u2ts + '</a>\n' +
|
||||
' <a id="umod" class="tgl btn" href="#" tt="' + L.cut_umod + '</a>\n' +
|
||||
' <a id="hashw" class="tgl btn" href="#" tt="' + L.cut_mt + '</a>\n' +
|
||||
' <a id="u2turbo" class="tgl btn ttb" href="#" tt="' + L.cut_turbo + '</a>\n' +
|
||||
@@ -2173,6 +2180,11 @@ function goto(dest) {
|
||||
}
|
||||
|
||||
|
||||
var m = SPINNER.split(','),
|
||||
SPINNER_CSS = SPINNER.slice(1 + m[0].length);
|
||||
SPINNER = m[0];
|
||||
|
||||
|
||||
var SBW, SBH; // scrollbar size
|
||||
(function () {
|
||||
var el = mknod('div');
|
||||
@@ -4426,7 +4438,7 @@ function read_dsort(txt) {
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
toast.warn(10, 'failed to apply default sort order [' + txt + ']:\n' + ex);
|
||||
toast.warn(10, 'failed to apply default sort order [' + esc('' + txt) + ']:\n' + ex);
|
||||
dsort = [['href', 1, '']];
|
||||
}
|
||||
}
|
||||
@@ -6294,19 +6306,40 @@ var thegrid = (function () {
|
||||
var html = [],
|
||||
svgs = new Set(),
|
||||
max_svgs = CHROME ? 500 : 5000,
|
||||
need_ext = !r.thumbs || !!ext_th,
|
||||
use_ext_th = r.thumbs && ext_th,
|
||||
files = QSA('#files>tbody>tr>td:nth-child(2) a[id]');
|
||||
|
||||
for (var a = 0, aa = files.length; a < aa; a++) {
|
||||
var ao = files[a],
|
||||
ohref = esc(ao.getAttribute('href')),
|
||||
href = ohref.split('?')[0],
|
||||
ext = '',
|
||||
name = uricom_dec(vsplit(href)[1]),
|
||||
ref = ao.getAttribute('id'),
|
||||
isdir = href.endsWith('/'),
|
||||
ac = isdir ? ' class="dir"' : '',
|
||||
ihref = ohref;
|
||||
|
||||
if (r.thumbs) {
|
||||
if (need_ext && href != "#") {
|
||||
var ar = href.split('.');
|
||||
if (ar.length > 1)
|
||||
ar.shift();
|
||||
|
||||
ar.reverse();
|
||||
for (var b = 0; b < Math.min(2, ar.length); b++) {
|
||||
if (ar[b].length > 7)
|
||||
break;
|
||||
|
||||
ext = ar[b] + '.' + ext;
|
||||
}
|
||||
ext = (ext || 'unk.').slice(0, -1);
|
||||
}
|
||||
|
||||
if (use_ext_th && ext_th[ext]) {
|
||||
ihref = ext_th[ext];
|
||||
}
|
||||
else if (r.thumbs) {
|
||||
ihref = addq(ihref, 'th=' + (have_webp ? 'w' : 'j'));
|
||||
if (!r.crop)
|
||||
ihref += 'f';
|
||||
@@ -6319,22 +6352,6 @@ var thegrid = (function () {
|
||||
ihref = SR + '/.cpr/ico/folder';
|
||||
}
|
||||
else {
|
||||
var ar = href.split('.');
|
||||
if (ar.length > 1)
|
||||
ar = ar.slice(1);
|
||||
|
||||
ihref = '';
|
||||
ar.reverse();
|
||||
for (var b = 0; b < ar.length; b++) {
|
||||
if (ar[b].length > 7)
|
||||
break;
|
||||
|
||||
ihref = ar[b] + '.' + ihref;
|
||||
}
|
||||
if (!ihref) {
|
||||
ihref = 'unk.';
|
||||
}
|
||||
var ext = ihref.slice(0, -1);
|
||||
if (!svgs.has(ext)) {
|
||||
if (svgs.size < max_svgs)
|
||||
svgs.add(ext);
|
||||
@@ -7493,7 +7510,7 @@ var treectl = (function () {
|
||||
xhr.open('GET', addq(dst, 'tree=' + top + (r.dots ? '&dots' : '') + k), true);
|
||||
xhr.onload = xhr.onerror = r.recvtree;
|
||||
xhr.send();
|
||||
enspin('#tree');
|
||||
enspin('t');
|
||||
}
|
||||
|
||||
r.recvtree = function () {
|
||||
@@ -7541,7 +7558,7 @@ var treectl = (function () {
|
||||
}
|
||||
}
|
||||
}
|
||||
despin('#tree');
|
||||
qsr('#dlt_t');
|
||||
|
||||
try {
|
||||
QS('#treeul>li>a+a').textContent = '[root]';
|
||||
@@ -7701,8 +7718,8 @@ var treectl = (function () {
|
||||
r.sb_msg = false;
|
||||
r.nextdir = xhr.top;
|
||||
clearTimeout(mpl.t_eplay);
|
||||
enspin('#tree');
|
||||
enspin(thegrid.en ? '#gfiles' : '#files');
|
||||
enspin('t');
|
||||
enspin('f');
|
||||
window.removeEventListener('scroll', r.tscroll);
|
||||
}
|
||||
|
||||
@@ -7797,9 +7814,8 @@ var treectl = (function () {
|
||||
}
|
||||
|
||||
r.gentab(this.top, res);
|
||||
despin('#tree');
|
||||
despin('#files');
|
||||
despin('#gfiles');
|
||||
qsr('#dlt_t');
|
||||
qsr('#dlt_f');
|
||||
|
||||
var lg0 = res.logues ? res.logues[0] || "" : "",
|
||||
lg1 = res.logues ? res.logues[1] || "" : "",
|
||||
@@ -8188,20 +8204,15 @@ var treectl = (function () {
|
||||
})();
|
||||
|
||||
|
||||
function enspin(sel) {
|
||||
despin(sel);
|
||||
var d = mknod('div');
|
||||
function enspin(i) {
|
||||
i = 'dlt_' + i;
|
||||
if (ebi(i))
|
||||
return;
|
||||
var d = mknod('div', i, SPINNER);
|
||||
d.className = 'dumb_loader_thing';
|
||||
d.innerHTML = '🌲';
|
||||
var tgt = QS(sel);
|
||||
tgt.insertBefore(d, tgt.childNodes[0]);
|
||||
}
|
||||
|
||||
|
||||
function despin(sel) {
|
||||
var o = QSA(sel + '>.dumb_loader_thing');
|
||||
for (var a = o.length - 1; a >= 0; a--)
|
||||
o[a].parentNode.removeChild(o[a]);
|
||||
if (SPINNER_CSS)
|
||||
d.style.cssText = SPINNER_CSS;
|
||||
document.body.appendChild(d);
|
||||
}
|
||||
|
||||
|
||||
@@ -8368,7 +8379,7 @@ function mk_files_header(taglist) {
|
||||
var tag = taglist[a],
|
||||
c1 = tag.slice(0, 1).toUpperCase();
|
||||
|
||||
tag = c1 + tag.slice(1);
|
||||
tag = esc(c1 + tag.slice(1));
|
||||
if (c1 == '.')
|
||||
tag = '<th name="tags/' + tag + '" sort="int"><span>' + tag.slice(1);
|
||||
else
|
||||
@@ -8685,7 +8696,17 @@ var mukey = (function () {
|
||||
|
||||
var light, theme, themen;
|
||||
var settheme = (function () {
|
||||
var ax = 'abcdefghijklmnopqrstuvwx';
|
||||
var r = {},
|
||||
ax = 'abcdefghijklmnopqrstuvwx',
|
||||
tre = '🌲',
|
||||
chldr = !SPINNER_CSS && SPINNER == tre;
|
||||
|
||||
r.ldr = {
|
||||
'4':['🌴'],
|
||||
'5':['🌭', 'padding:0 0 .7em .7em;filter:saturate(3)'],
|
||||
'6':['📞', 'padding:0;filter:brightness(2) sepia(1) saturate(3) hue-rotate(60deg)'],
|
||||
'7':['▲', 'font-size:3em'], //cp437
|
||||
};
|
||||
|
||||
theme = sread('cpp_thm') || 'a';
|
||||
if (!/^[a-x][yz]/.exec(theme))
|
||||
@@ -8715,13 +8736,19 @@ var settheme = (function () {
|
||||
ebi('themes').innerHTML = html.join('');
|
||||
var btns = QSA('#themes a');
|
||||
for (var a = 0; a < themes; a++)
|
||||
btns[a].onclick = settheme;
|
||||
btns[a].onclick = r.go;
|
||||
|
||||
if (chldr) {
|
||||
var x = r.ldr[itheme] || [tre];
|
||||
SPINNER = x[0];
|
||||
SPINNER_CSS = x[1];
|
||||
}
|
||||
|
||||
bcfg_set('light', light);
|
||||
tt.att(ebi('themes'));
|
||||
}
|
||||
|
||||
function settheme(e) {
|
||||
r.go = function (e) {
|
||||
var i = e;
|
||||
try { ev(e); i = e.target.textContent; } catch (ex) { }
|
||||
light = i % 2 == 1;
|
||||
@@ -8734,7 +8761,7 @@ var settheme = (function () {
|
||||
}
|
||||
|
||||
freshen();
|
||||
return settheme;
|
||||
return r;
|
||||
})();
|
||||
|
||||
|
||||
|
||||
@@ -1078,26 +1078,28 @@ action_stack = (function () {
|
||||
var p1 = from.length,
|
||||
p2 = to.length;
|
||||
|
||||
while (p1-- > 0 && p2-- > 0)
|
||||
while (p1 --> 0 && p2 --> 0)
|
||||
if (from[p1] != to[p2])
|
||||
break;
|
||||
|
||||
if (car > ++p1) {
|
||||
if (car > ++p1)
|
||||
car = p1;
|
||||
}
|
||||
|
||||
var txt = from.substring(car, p1)
|
||||
return {
|
||||
car: car,
|
||||
cdr: ++p2,
|
||||
cdr: p2 + (car && 1),
|
||||
txt: txt,
|
||||
cpos: cpos
|
||||
};
|
||||
}
|
||||
|
||||
var undiff = function (from, change) {
|
||||
var t1 = from.substring(0, change.car),
|
||||
t2 = from.substring(change.cdr);
|
||||
|
||||
return {
|
||||
txt: from.substring(0, change.car) + change.txt + from.substring(change.cdr),
|
||||
txt: t1 + change.txt + t2,
|
||||
cpos: change.cpos
|
||||
};
|
||||
}
|
||||
|
||||
@@ -885,6 +885,25 @@ function up2k_init(subtle) {
|
||||
bcfg_bind(uc, 'upnag', 'upnag', false, set_upnag);
|
||||
bcfg_bind(uc, 'upsfx', 'upsfx', false, set_upsfx);
|
||||
|
||||
uc.ow = parseInt(sread('u2ow', ['0', '1', '2']) || u2ow);
|
||||
uc.owt = ['🛡️', '🕒', '♻️'];
|
||||
function set_ow() {
|
||||
QS('label[for="u2ow"]').innerHTML = uc.owt[uc.ow];
|
||||
ebi('u2ow').checked = true; //cosmetic
|
||||
}
|
||||
ebi('u2ow').onclick = function (e) {
|
||||
ev(e);
|
||||
if (++uc.ow > 2)
|
||||
uc.ow = 0;
|
||||
swrite('u2ow', uc.ow);
|
||||
set_ow();
|
||||
if (uc.ow && !has(perms, 'delete'))
|
||||
toast.warn(10, L.u_enoow, 'noow');
|
||||
else if (toast.tag == 'noow')
|
||||
toast.hide();
|
||||
};
|
||||
set_ow();
|
||||
|
||||
var st = {
|
||||
"files": [],
|
||||
"nfile": {
|
||||
@@ -1300,7 +1319,7 @@ function up2k_init(subtle) {
|
||||
if (bad_files.length) {
|
||||
var msg = L.u_badf.format(bad_files.length, ntot);
|
||||
for (var a = 0, aa = Math.min(20, bad_files.length); a < aa; a++)
|
||||
msg += '-- ' + bad_files[a][1] + '\n';
|
||||
msg += '-- ' + esc(bad_files[a][1]) + '\n';
|
||||
|
||||
msg += L.u_just1;
|
||||
return modal.alert(msg, function () {
|
||||
@@ -1312,7 +1331,7 @@ function up2k_init(subtle) {
|
||||
if (nil_files.length) {
|
||||
var msg = L.u_blankf.format(nil_files.length, ntot);
|
||||
for (var a = 0, aa = Math.min(20, nil_files.length); a < aa; a++)
|
||||
msg += '-- ' + nil_files[a][1] + '\n';
|
||||
msg += '-- ' + esc(nil_files[a][1]) + '\n';
|
||||
|
||||
msg += L.u_just1;
|
||||
return modal.confirm(msg, function () {
|
||||
@@ -1390,7 +1409,7 @@ function up2k_init(subtle) {
|
||||
name = good_files[a][1],
|
||||
fdir = evpath,
|
||||
now = Date.now(),
|
||||
lmod = uc.u2ts ? (fobj.lastModified || now) : 0,
|
||||
lmod = (uc.u2ts && fobj.lastModified) || 0,
|
||||
ofs = name.lastIndexOf('/') + 1;
|
||||
|
||||
if (ofs) {
|
||||
@@ -2054,8 +2073,8 @@ function up2k_init(subtle) {
|
||||
try { orz(e); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); }
|
||||
};
|
||||
reader.onerror = function () {
|
||||
var err = reader.error + '';
|
||||
var handled = false;
|
||||
var err = esc('' + reader.error),
|
||||
handled = false;
|
||||
|
||||
if (err.indexOf('NotReadableError') !== -1 || // win10-chrome defender
|
||||
err.indexOf('NotFoundError') !== -1 // macos-firefox permissions
|
||||
@@ -2279,7 +2298,7 @@ function up2k_init(subtle) {
|
||||
xhr.onerror = xhr.ontimeout = function () {
|
||||
console.log('head onerror, retrying', t.name, t);
|
||||
if (!toast.visible)
|
||||
toast.warn(9.98, L.u_enethd + "\n\nfile: " + t.name, t);
|
||||
toast.warn(9.98, L.u_enethd + "\n\nfile: " + esc(t.name), t);
|
||||
|
||||
apop(st.busy.head, t);
|
||||
st.todo.head.unshift(t);
|
||||
@@ -2354,7 +2373,7 @@ function up2k_init(subtle) {
|
||||
return console.log('zombie handshake onerror', t.name, t);
|
||||
|
||||
if (!toast.visible)
|
||||
toast.warn(9.98, L.u_eneths + "\n\nfile: " + t.name, t);
|
||||
toast.warn(9.98, L.u_eneths + "\n\nfile: " + esc(t.name), t);
|
||||
|
||||
console.log('handshake onerror, retrying', t.name, t);
|
||||
apop(st.busy.handshake, t);
|
||||
@@ -2459,7 +2478,7 @@ function up2k_init(subtle) {
|
||||
var idx = t.hash.indexOf(missing[a]);
|
||||
if (idx < 0)
|
||||
return modal.alert('wtf negative index for hash "{0}" in task:\n{1}'.format(
|
||||
missing[a], JSON.stringify(t)));
|
||||
missing[a], esc(JSON.stringify(t))));
|
||||
|
||||
t.postlist.push(idx);
|
||||
cbd[idx] = 0;
|
||||
@@ -2613,7 +2632,7 @@ function up2k_init(subtle) {
|
||||
return toast.err(0, L.u_ehsdf + "\n\n" + rsp.replace(/.*; /, ''));
|
||||
|
||||
err = t.t_uploading ? L.u_ehsfin : t.srch ? L.u_ehssrch : L.u_ehsinit;
|
||||
xhrchk(xhr, err + "\n\nfile: " + t.name + "\n\nerror ", "404, target folder not found", "warn", t);
|
||||
xhrchk(xhr, err + "\n\nfile: " + esc(t.name) + "\n\nerror ", "404, target folder not found", "warn", t);
|
||||
}
|
||||
}
|
||||
xhr.onload = function (e) {
|
||||
@@ -2634,6 +2653,13 @@ function up2k_init(subtle) {
|
||||
else if (t.umod)
|
||||
req.umod = true;
|
||||
|
||||
if (!t.srch) {
|
||||
if (uc.ow == 1)
|
||||
req.replace = 'mt';
|
||||
if (uc.ow == 2)
|
||||
req.replace = true;
|
||||
}
|
||||
|
||||
xhr.open('POST', t.purl, true);
|
||||
xhr.responseType = 'text';
|
||||
xhr.timeout = 42000 + (t.srch || t.t_uploaded ? 0 :
|
||||
@@ -2763,7 +2789,7 @@ function up2k_init(subtle) {
|
||||
toast.inf(10, L.u_cbusy);
|
||||
}
|
||||
else {
|
||||
xhrchk(xhr, L.u_cuerr2.format(snpart, Math.ceil(t.size / chunksize), t.name), "404, target folder not found (???)", "warn", t);
|
||||
xhrchk(xhr, L.u_cuerr2.format(snpart, Math.ceil(t.size / chunksize), esc(t.name)), "404, target folder not found (???)", "warn", t);
|
||||
chill(t);
|
||||
}
|
||||
orz2(xhr);
|
||||
@@ -2807,7 +2833,7 @@ function up2k_init(subtle) {
|
||||
xhr.bsent = 0;
|
||||
|
||||
if (!toast.visible)
|
||||
toast.warn(9.98, L.u_cuerr.format(snpart, Math.ceil(t.size / chunksize), t.name), t);
|
||||
toast.warn(9.98, L.u_cuerr.format(snpart, Math.ceil(t.size / chunksize), esc(t.name)), t);
|
||||
|
||||
t.nojoin = t.nojoin || t.postlist.length; // maybe rproxy postsize limit
|
||||
console.log('chunkpit onerror,', t.name, t);
|
||||
|
||||
@@ -64,7 +64,7 @@ onmessage = (d) => {
|
||||
};
|
||||
reader.onerror = function () {
|
||||
busy = false;
|
||||
var err = reader.error + '';
|
||||
var err = esc('' + reader.error);
|
||||
|
||||
if (err.indexOf('NotReadableError') !== -1 || // win10-chrome defender
|
||||
err.indexOf('NotFoundError') !== -1 // macos-firefox permissions
|
||||
|
||||
@@ -1,3 +1,167 @@
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0225-0017 `v1.16.15` fix low-severity vuln
|
||||
|
||||
<img src="https://github.com/9001/copyparty/raw/hovudstraum/docs/logo.svg" width="250" align="right"/>
|
||||
|
||||
* read-only demo server at https://a.ocv.me/pub/demo/
|
||||
* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) ╱ [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) ╱ [client testbed](https://cd.ocv.me/b/)
|
||||
|
||||
## ⚠️ this fixes a minor vulnerability; CVE-score `3.6`/`10`
|
||||
|
||||
[GHSA-m2jw-cj8v-937r](https://github.com/9001/copyparty/security/advisories/GHSA-m2jw-cj8v-937r) aka [CVE-2025-27145](https://www.cve.org/CVERecord?id=CVE-2025-27145) could let an attacker run arbitrary javascript by tricking an authenticated user into uploading files with malicious filenames
|
||||
|
||||
* ...but it required some clever social engineering, and is **not likely** to be a cause for concern... ah, better safe than sorry
|
||||
|
||||
there is a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone` in case of future important updates, such as [vulnerabilities](https://github.com/9001/copyparty/security) (most recently 2025-02-25)
|
||||
|
||||
## recent important news
|
||||
|
||||
* [v1.15.0 (2024-09-08)](https://github.com/9001/copyparty/releases/tag/v1.15.0) changed upload deduplication to be default-disabled
|
||||
* [v1.14.3 (2024-08-30)](https://github.com/9001/copyparty/releases/tag/v1.14.3) fixed a bug that was introduced in v1.13.8 (2024-08-13); this bug could lead to **data loss** -- see the v1.14.3 release-notes for details
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* nothing this time
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* fix [GHSA-m2jw-cj8v-937r](https://github.com/9001/copyparty/security/advisories/GHSA-m2jw-cj8v-937r) / [CVE-2025-27145](https://www.cve.org/CVERecord?id=CVE-2025-27145) in 438ea6cc
|
||||
* when trying to upload an empty files by dragging it into the browser, the filename would be rendered as HTML, allowing javascript injection if the filename was malicious
|
||||
* issue discovered and reported by @JayPatel48 (thx!)
|
||||
* related issues in errorhandling of uploads 499ae1c7 36866f1d
|
||||
* these all had the same consequences as the GHSA above, but a network outage was necessary to trigger them
|
||||
* which would probably have the lucky side-effect of blocking the javascript download, nice
|
||||
* paranoid fixing of probably-not-even-issues 3adbb2ff
|
||||
* fix some markdown / texteditor bugs 407531bc
|
||||
* only indicate file-versions for markdown files in listings, since it's tricky to edit non-textfiles otherwise
|
||||
* CTRL-C followed by CTRL-V and CTRL-Z in a single-line file would make a character fall off
|
||||
* ensure safety of extensions
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* readme:
|
||||
* mention support for running the server on risc-v 6d102fc8
|
||||
* mention that the [sony psp](https://github.com/user-attachments/assets/9d21f020-1110-4652-abeb-6fc09c533d4f) can browse and upload 598a29a7
|
||||
|
||||
----
|
||||
|
||||
# 💾 what to download?
|
||||
| download link | is it good? | description |
|
||||
| -- | -- | -- |
|
||||
| **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** | ✅ the best 👍 | runs anywhere! only needs python |
|
||||
| [a docker image](https://github.com/9001/copyparty/blob/hovudstraum/scripts/docker/README.md) | it's ok | good if you prefer docker 🐋 |
|
||||
| [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) | ⚠️ [acceptable](https://github.com/9001/copyparty#copypartyexe) | for [win8](https://user-images.githubusercontent.com/241032/221445946-1e328e56-8c5b-44a9-8b9f-dee84d942535.png) or later; built-in thumbnailer |
|
||||
| [u2c.exe](https://github.com/9001/copyparty/releases/download/v1.16.14/u2c.exe) | ⚠️ acceptable | [CLI uploader](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) as a win7+ exe ([video](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm)) |
|
||||
| [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz) | ⚠️ acceptable | similar to the regular sfx, [mostly worse](https://github.com/9001/copyparty#zipapp) |
|
||||
| [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) | ⛔️ [dangerous](https://github.com/9001/copyparty#copypartyexe) | for [win7](https://user-images.githubusercontent.com/241032/221445944-ae85d1f4-d351-4837-b130-82cab57d6cca.png) -- never expose to the internet! |
|
||||
| [cpp-winpe64.exe](https://github.com/9001/copyparty/releases/download/v1.16.5/copyparty-winpe64.exe) | ⛔️ dangerous | runs on [64bit WinPE](https://user-images.githubusercontent.com/241032/205454984-e6b550df-3c49-486d-9267-1614078dd0dd.png), otherwise useless |
|
||||
|
||||
* except for [u2c.exe](https://github.com/9001/copyparty/releases/download/v1.16.14/u2c.exe), all of the options above are mostly equivalent
|
||||
* the zip and tar.gz files below are just source code
|
||||
* python packages are available at [PyPI](https://pypi.org/project/copyparty/#files)
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0219-2309 `v1.16.14` overwrite by upload
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #139 overwrite existing files by uploading over them e9f78ea7
|
||||
* default-disabled; a new togglebutton in the upload-UI configures it
|
||||
* can optionally compare last-modified-time and only overwrite older files
|
||||
* [GDPR compliance](https://github.com/9001/copyparty#GDPR-compliance) (maybe/probably) 4be0d426
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* some cosmetic volflag stuff, all harmless b190e676
|
||||
* disabling a volflag `foo` with `-foo` shows a warning that `-foo` was not a recognized volflag, but it still does the right thing
|
||||
* some volflags give the *"unrecognized volflag, will ignore"* warning, but not to worry, they still work just fine:
|
||||
* `xz` to allow serverside xz-compression of uploaded files
|
||||
* the option to customize the loader-spinner would glitch out during the initial page load 7d7d5d6c
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* [randpic.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/handlers/randpic.py), new 404-handler example, returns a random pic from a folder 60d5f271
|
||||
* readme: [howto permanent cloudflare tunnel](https://github.com/9001/copyparty#permanent-cloudflare-tunnel) for easy hosting from home 2beb2acc
|
||||
* [synology-dsm](https://github.com/9001/copyparty/blob/hovudstraum/docs/synology-dsm.md): mention how to update the docker image 56ce5919
|
||||
* spinner improvements 6858cb06
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0213-2057 `v1.16.13` configure with confidence
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* make the config-parser more helpful regarding volflags a255db70
|
||||
* if an unrecognized volflag is specified, print a warning instead of silently ignoring it
|
||||
* understand volflag-names with Uppercase and/or kebab-case (dashes), and not just snake_case (underscores)
|
||||
* improve `--help-flags` to mention and explain all available flags
|
||||
* #136 WebDAV: support COPY 62ee7f69
|
||||
* also support overwrite of existing target files (default-enabled according to the spec)
|
||||
* the user must have the delete-permission to actually replace files
|
||||
* option to specify custom icons for certain file extensions 7e4702cf
|
||||
* see `--ext-th` mentioned briefly in the [thumbnails section](https://github.com/9001/copyparty/#thumbnails)
|
||||
* option to replace the loading-spinner animation 685f0869
|
||||
* including how to [make it exceptionally normal-looking](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#boring-loader-spinner)
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* #136 WebDAV fixes 62ee7f69
|
||||
* COPY/MOVE/MKCOL: challenge clients to provide the password as necessary
|
||||
* most clients only need this in PROPFIND, but KDE-Dolphin is more picky
|
||||
* MOVE: support `webdav://` Destination prefix as used by Dolphin, probably others
|
||||
* #136 WebDAV: improve support for KDE-Dolphin as client 9d769027
|
||||
* it masquerades as a graphical browser yet still expects 401, so special-case it with a useragent scan
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* Docker-only: quick hacky fix for the [musl CVE](https://www.openwall.com/lists/musl/2025/02/13/1) until the official fix is out 4d6626b0
|
||||
* the docker images will be rebuilt when `musl-1.2.5-r9.apk` is released, in 6~24h or so
|
||||
* until then, there is no support for reading korean XML files when running in docker
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0209-2331 `v1.16.12` RTT
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* show rtt (network latency to server, including request processing time) in the top status text d27f1104
|
||||
* and log the client-reported RTT to serverlog 20ddeb6e
|
||||
* remember file selection when changing folders c7db08ed
|
||||
* good for when you accidentally navigate elsewhere
|
||||
* option to restrict download-as-zip/tar to admins-only c87af9e8
|
||||
* #135 add [bubbleparty](https://github.com/9001/copyparty/blob/hovudstraum/bin/README.md#bubblepartysh), thx @coderofsalvation! 3582a100
|
||||
* runs copyparty in a [sandbox](https://github.com/containers/bubblewrap), making it harder to gain unintended access through bugs in python or copyparty
|
||||
* better alternative to [prisonparty](https://github.com/9001/copyparty/tree/hovudstraum/bin#prisonpartysh), more similar to [the sandboxing in the nixos package](https://github.com/9001/copyparty/blob/7dda77dcb/contrib/nixos/modules/copyparty.nix#L232-L272)
|
||||
* new plugin: [quickmove](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/quickmove.js) 46f9e9ef
|
||||
* adds hotkey `W` to quickly move selected files into a subfolder
|
||||
* #133 new plugin: [graft-thumbs.js](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/graft-thumbs.js) 6c202eff
|
||||
* in folders with foobar.mp3 and foobar.png, can copy the thumbnail from the png to the jpg (and then hide the png)
|
||||
* handlers: add [http-redirect example](https://github.com/9001/copyparty/blob/hovudstraum/bin/handlers/redirect.py) 22cbd2db
|
||||
* add [ping.html](https://github.com/9001/copyparty/blob/hovudstraum/srv/ping.html) 7de9d15a 910797cc
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* improve iPad detection so they get opus instead of mp3 12dcea4f
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* safeguard against accidental config loss cd71b505
|
||||
* while no copyparty servers have ended up in this unfortunate situation yet (afaik), be proactive and borrow some experience from other docker-based services
|
||||
* readme: improve config examples 32e90859
|
||||
* improve serverlog entries regarding 403s b020fd4a
|
||||
* #132 mention fuse permissions in readme d9d2a092
|
||||
* traefik-example: fix disconnect during big uploads 6a9ffe7e
|
||||
* try to show an appropriate warning for media that the browser doesn't support playing 4ef35263
|
||||
* was an attempt at detecting iphones failing to play high-color-precision webm files, but safari doesn't seem to realize itself that playback has failed, ah well
|
||||
* copyparty.exe: update to python 3.12.9
|
||||
* update deps: dompurify 3.2.4
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0127-0140 `v1.16.11` fix no-acode
|
||||
|
||||
|
||||
@@ -48,6 +48,20 @@ and if you want to have a monospace font in the fancy markdown editor, do this:
|
||||
NB: `<textarea id="mt">` and `<div id="mtr">` in the regular markdown editor must have the same font; none of the suggestions above will cause any issues but keep it in mind if you're getting creative
|
||||
|
||||
|
||||
# boring loader spinner
|
||||
|
||||
replace the 🌲 with a spinning circle using commandline args:
|
||||
|
||||
`--spinner ',padding:0;border-radius:9em;border:.2em solid #444;border-top:.2em solid #fc0'`
|
||||
|
||||
or config file example:
|
||||
|
||||
```yaml
|
||||
[global]
|
||||
spinner: ,padding:0;border-radius:9em;border:.2em solid #444;border-top:.2em solid #fc0
|
||||
```
|
||||
|
||||
|
||||
# `<head>`
|
||||
|
||||
to add stuff to the html `<head>`, for example a css `<link>` or `<meta>` tags, use either the global-option `--html-head` or the volflag `html_head`
|
||||
|
||||
@@ -115,6 +115,16 @@ note that if you only want to share some folders inside your data volume, and no
|
||||
|
||||
|
||||
|
||||
## updating
|
||||
|
||||
to update to a new copyparty version: `Container Manager` » `Images` » `Update available` » `Update`
|
||||
|
||||
* DSM checks for updates every 12h; you can force a check with `sudo /var/packages/ContainerManager/target/tool/image_upgradable_checker`
|
||||
|
||||
* there is no auto-update feature, and beware that watchtower does not support DSM
|
||||
|
||||
|
||||
|
||||
## regarding ram usage
|
||||
|
||||
the ram usage indicator in both `Docker` and `Container Manager` is misleading because it also counts the kernel disk cache which makes the number insanely high -- the synology resource monitor shows the correct values, usually less than 100 MiB
|
||||
|
||||
@@ -131,6 +131,7 @@ symbol legend,
|
||||
| runs on Linux | █ | ╱ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| runs on Macos | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | |
|
||||
| runs on FreeBSD | █ | | | • | █ | █ | █ | • | █ | █ | | █ | |
|
||||
| runs on Risc-V | █ | | | █ | █ | █ | | • | | █ | | | |
|
||||
| portable binary | █ | █ | █ | | | █ | █ | | | █ | | █ | █ |
|
||||
| zero setup, just go | █ | █ | █ | | | ╱ | █ | | | █ | | ╱ | █ |
|
||||
| android app | ╱ | | | █ | █ | | | | | | | | |
|
||||
|
||||
@@ -144,16 +144,16 @@ class Cfg(Namespace):
|
||||
ex = "au_vol dl_list mtab_age reg_cap s_thead s_tbody th_convt ups_who zip_who"
|
||||
ka.update(**{k: 9 for k in ex.split()})
|
||||
|
||||
ex = "db_act k304 loris no304 re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo"
|
||||
ex = "db_act forget_ip k304 loris no304 re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow"
|
||||
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 R RS SR"
|
||||
ka.update(**{k: "" for k in ex.split()})
|
||||
|
||||
ex = "ban_403 ban_404 ban_422 ban_pw ban_url"
|
||||
ex = "ban_403 ban_404 ban_422 ban_pw ban_url spinner"
|
||||
ka.update(**{k: "no" for k in ex.split()})
|
||||
|
||||
ex = "grp on403 on404 xac xad xar xau xban xbc xbd xbr xbu xiu xm"
|
||||
ex = "ext_th grp on403 on404 xac xad xar xau xban xbc xbd xbr xbu xiu xm"
|
||||
ka.update(**{k: [] for k in ex.split()})
|
||||
|
||||
ex = "exp_lg exp_md"
|
||||
|
||||
Reference in New Issue
Block a user