Merge branch 'tr_localization' of github.com:NandeMD/copyparty into tr_localization

This commit is contained in:
NandeMD 2025-09-22 17:01:58 +03:00
commit 24f5c39f59
109 changed files with 7309 additions and 934 deletions

3
.gitignore vendored
View file

@ -43,3 +43,6 @@ scripts/docker/*.err
# nix build output link
result
# IDEA config
.idea/

View file

@ -6,13 +6,13 @@ but please:
# do not use AI / LMM when writing code
# do not use AI / LLM when writing code
copyparty is 100% organic, free-range, human-written software!
> ⚠ you are now entering a no-copilot zone
the *only* place where LMM/AI *may* be accepted is for [localization](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) if you are fluent and have confirmed that the translation is accurate.
the *only* place where LLM/AI *may* be accepted is for [localization](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) if you are fluent and have confirmed that the translation is accurate.
sorry for the harsh tone, but this is important to me 🙏

147
README.md
View file

@ -90,7 +90,9 @@ made in Norway 🇳🇴
* [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/))
* [handlers](#handlers) - redefine behavior with plugins ([examples](./bin/handlers/))
* [ip auth](#ip-auth) - autologin based on IP range (CIDR)
* [restrict to ip](#restrict-to-ip) - limit a user to certain IP ranges (CIDR)
* [identity providers](#identity-providers) - replace copyparty passwords with oauth and such
* [generic header auth](#generic-header-auth) - other ways to auth by header
* [user-changeable passwords](#user-changeable-passwords) - if permitted, users can change their own passwords
* [using the cloud as storage](#using-the-cloud-as-storage) - connecting to an aws s3 bucket and similar
* [hiding from google](#hiding-from-google) - tell search engines you don't wanna be indexed
@ -110,6 +112,7 @@ made in Norway 🇳🇴
* [packages](#packages) - the party might be closer than you think
* [arch package](#arch-package) - `pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/))
* [fedora package](#fedora-package) - does not exist yet
* [homebrew formulae](#homebrew-formulae) - `brew install copyparty ffmpeg`
* [nix package](#nix-package) - `nix profile install github:9001/copyparty`
* [nixos module](#nixos-module)
* [browser support](#browser-support) - TLDR: yes
@ -139,6 +142,7 @@ made in Norway 🇳🇴
* [copyparty.exe](#copypartyexe) - download [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) (win8+) or [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) (win7+)
* [zipapp](#zipapp) - another emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz)
* [install on android](#install-on-android)
* [install on iOS](#install-on-iOS)
* [reporting bugs](#reporting-bugs) - ideas for context to include, and where to submit them
* [devnotes](#devnotes) - for build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md)
@ -153,6 +157,7 @@ just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/
* or if you cannot install python, you can use [copyparty.exe](#copypartyexe) instead
* or install [on arch](#arch-package) [on NixOS](#nixos-module) [through nix](#nix-package)
* or if you are on android, [install copyparty in termux](#install-on-android)
* or maybe an iPhone or iPad? [install in a-Shell on iOS](#install-on-iOS)
* or maybe you have a [synology nas / dsm](./docs/synology-dsm.md)
* or if you have [uv](https://docs.astral.sh/uv/) installed, run `uv tool run copyparty`
* or if your computer is messed up and nothing else works, [try the pyz](#zipapp)
@ -239,7 +244,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)
* ☑ cross-platform (Windows, Linux, Macos, Android, iOS, FreeBSD, arm32/arm64, ppc64le, s390x, risc-v/riscv64)
* upload
* ☑ basic: plain multipart, ie6 support
* ☑ [up2k](#uploading): js, resumable, multithreaded
@ -262,10 +267,11 @@ also see [comparison to similar software](./docs/versus.md)
* ☑ play video files as audio (converted on server)
* ☑ create and play [m3u8 playlists](#playlists)
* ☑ image gallery with webm player
* ☑ [textfile browser](#textfile-viewer) with syntax hilighting
* ☑ [textfile browser](#textfile-viewer) with syntax highlighting
* ☑ realtime streaming of growing files (logfiles and such)
* ☑ [thumbnails](#thumbnails)
* ☑ ...of images using Pillow, pyvips, or FFmpeg
* ☑ ...of RAW images using rawpy
* ☑ ...of videos using FFmpeg
* ☑ ...of audio (spectrograms) using FFmpeg
* ☑ cache eviction (max-age; maybe max-size eventually)
@ -512,6 +518,8 @@ examples:
* replacing the `g` permission with `wg` would let anonymous users upload files, but not see the required filekey to access it
* replacing the `g` permission with `wG` would let anonymous users upload files, receiving a working direct link in return
if you want to grant access to all users who are logged in, the group `acct` will always contain all known users, so for example `-v /mnt/music:music:r,@acct`
anyone trying to bruteforce a password gets banned according to `--ban-pw`; default is 24h ban for 9 failed attempts in 1 hour
and if you want to use config files instead of commandline args (good!) then here's the same examples as a configfile; save it as `foobar.conf` and use it like this: `python copyparty-sfx.py -c foobar.conf`
@ -537,6 +545,7 @@ and if you want to use config files instead of commandline args (good!) then her
accs:
r: u1, u2 # only these accounts can read,
r: @g1 # (exactly the same, just with a group instead)
r: @acct # (alternatively, ALL users who are logged in)
rw: u3 # and only u3 can read-write
[/inc]
@ -563,6 +572,8 @@ for example `-v /mnt::r -v /var/empty:web/certs:r` mounts the server folder `/mn
the example config file right above this section may explain this better; the first volume `/` is mapped to `/srv` which means http://127.0.0.1:3923/music would try to read `/srv/music` on the server filesystem, but since there's another volume at `/music` mapped to `/mnt/music` then it'll go to `/mnt/music` instead
> this also works for single files, because files can also be volumes
## dotfiles
@ -825,7 +836,7 @@ the up2k UI is the epitome of polished intuitive experiences:
* `[🔎]` switch between upload and [file-search](#file-search) mode
* ignore `[🔎]` if you add files by dragging them into the browser
and then theres the tabs below it,
and then there's the tabs below it,
* `[ok]` is the files which completed successfully
* `[ng]` is the ones that failed / got rejected (already exists, ...)
* `[done]` shows a combined list of `[ok]` and `[ng]`, chronological order
@ -1057,7 +1068,7 @@ plays almost every audio format there is (if the server has FFmpeg installed fo
the following audio formats are usually always playable, even without FFmpeg: `aac|flac|m4a|mp3|ogg|opus|wav`
some hilights:
some highlights:
* OS integration; control playback from your phone's lockscreen ([windows](https://user-images.githubusercontent.com/241032/233213022-298a98ba-721a-4cf1-a3d4-f62634bc53d5.png) // [iOS](https://user-images.githubusercontent.com/241032/142711926-0700be6c-3e31-47b3-9928-53722221f722.png) // [android](https://user-images.githubusercontent.com/241032/233212311-a7368590-08c7-4f9f-a1af-48ccf3f36fad.png))
* shows the audio waveform in the seekbar
* not perfectly gapless but can get really close (see settings + eq below); good enough to enjoy gapless albums as intended
@ -1288,6 +1299,12 @@ print a qr-code [(screenshot)](https://user-images.githubusercontent.com/241032/
* `--qrl lootbox/?pw=hunter2` appends to the url, linking to the `lootbox` folder with password `hunter2`
* `--qrz 1` forces 1x zoom instead of autoscaling to fit the terminal size
* 1x may render incorrectly on some terminals/fonts, but 2x should always work
* `--qr-pin 1` makes the qr-code stick to the bottom of the console (never scrolls away)
* `--qr-file qr.txt:1:2` writes a small qr-code to `qr.txt`
* `--qr-file qr.txt:2:2` writes a big qr-code to `qr.txt`
* `--qr-file qr.svg:1:2` writes a vector-graphics qr-code to `qr.svg`
* `--qr-file qr.png:8:4:333333:ffcc55` writes an 8x-magnified yellow-on-gray `qr.png`
* `--qr-file qr.png:8:4::ffffff` writes an 8x-magnified white-on-transparent `qr.png`
it uses the server hostname if [mdns](#mdns) is enabled, otherwise it'll use your external ip (default route) unless `--qri` specifies a specific ip-prefix or domain
@ -1311,6 +1328,16 @@ some recommended FTP / FTPS clients; `wark` = example password:
* https://rclone.org/ does FTPS with `tls=false explicit_tls=true`
* `lftp -u k,wark -p 3921 127.0.0.1 -e ls`
* `lftp -u k,wark -p 3990 127.0.0.1 -e 'set ssl:verify-certificate no; ls'`
* `curl ftp://127.0.0.1:3921/` (plaintext ftp)
* `curl --ssl-reqd ftp://127.0.0.1:3990/` (encrypted ftps)
config file example, which restricts FTP to only use ports 3921 and 12000-12099 so all of those ports must be opened in your firewall:
```yaml
[global]
ftp: 3921
ftp-pr: 12000-12099
```
## webdav server
@ -1406,6 +1433,7 @@ and some minor issues,
* win10 onwards does not allow connecting anonymously / without accounts
* python3 only
* slow (the builtin webdav support in windows is 5x faster, and rclone-webdav is 30x faster)
* those numbers are specifically for copyparty's smb-server (because it sucks); other smb-servers should be similar to webdav
known client bugs:
* on win7 only, `--smb1` is much faster than smb2 (default) because it keeps rescanning folders on smb2
@ -1892,6 +1920,20 @@ repeat the option to map additional subnets
**be careful with this one!** if you have a reverseproxy, then you definitely want to make sure you have [real-ip](#real-ip) configured correctly, and it's probably a good idea to nullmap the reverseproxy's IP just in case; so if your reverseproxy is sending requests from `172.24.27.9` then that would be `--ipu=172.24.27.9/32=`
### restrict to ip
limit a user to certain IP ranges (CIDR) , using the global-option `--ipr`
for example, if the user `spartacus` should get rejected if they're not connecting from an IP that starts with `192.168.123` or `172.16`, then you can either specify `--ipr=192.168.123.0/24,172.16.0.0/16=spartacus` as a commandline option, or put this in a config file:
```yaml
[global]
ipr: 192.168.123.0/24,172.16.0.0/16=spartacus
```
repeat the option to map additional users
## identity providers
replace copyparty passwords with oauth and such
@ -1900,6 +1942,10 @@ you can disable the built-in password-based login system, and instead replace it
* the regular config-defined users will be used as a fallback for requests which don't include a valid (trusted) IdP username header
* `--auth-ord` configured auth precedence, for example to allow overriding the IdP with a copyparty password
* the login/logout links/buttons can be replaced with links to your IdP with `--idp-login` and `--idp-logout` , for example `--idp-login /idp/login/?redir={dst}` will expand `{dst}` to the page the user was on when clicking Login
* if your IdP-server is slow, consider `--idp-cookie` and let requests with the cookie `cppws` bypass the IdP; experimental sessions-based feature added for a party
some popular identity providers are [Authelia](https://www.authelia.com/) (config-file based) and [authentik](https://goauthentik.io/) (GUI-based, more complex)
@ -1911,6 +1957,20 @@ a more complete example of the copyparty configuration options [look like this](
but if you just want to let users change their own passwords, then you probably want [user-changeable passwords](#user-changeable-passwords) instead
### generic header auth
other ways to auth by header
if you have a middleware which adds a header with a user identifier, for example tailscale's `Tailscale-User-Login: alice.m@forest.net` then you can automatically auth as `alice` by defining that mapping with `--idp-hm-usr '^Tailscale-User-Login^alice.m@forest.net^alice'` or the following config file:
```yaml
[global]
idp-hm-usr: ^Tailscale-User-Login^alice.m@forest.net^alice
```
repeat the whole `idp-hm-usr` option to add more mappings
## user-changeable passwords
if permitted, users can change their own passwords in the control-panel
@ -2126,7 +2186,7 @@ when connecting the reverse-proxy to `127.0.0.1` instead (the basic and/or old-f
in summary, `haproxy > caddy > traefik > nginx > apache > lighttpd`, and use uds when possible (traefik does not support it yet)
* if these results are bullshit because my config exampels are bad, please submit corrections!
* if these results are bullshit because my config examples are bad, please submit corrections!
## permanent cloudflare tunnel
@ -2262,6 +2322,7 @@ buggy feature? rip it out by setting any of the following environment variables
| `PRTY_NO_SQLITE` | disable all database-related functionality (file indexing, metadata indexing, most file deduplication logic) |
| `PRTY_NO_TLS` | disable native HTTPS support; if you still want to accept HTTPS connections then TLS must now be terminated by a reverse-proxy |
| `PRTY_NO_TPOKE` | disable systemd-tmpfilesd avoider |
| `PRTY_UNSAFE_STATE` | allow storing secrets into emergency-fallback locations |
example: `PRTY_NO_IFADDR=1 python3 copyparty-sfx.py`
@ -2297,6 +2358,15 @@ after installing, start either the system service or the user service and naviga
does not exist yet; there are rumours that it is being packaged! keep an eye on this space...
## homebrew formulae
`brew install copyparty ffmpeg` -- https://formulae.brew.sh/formula/copyparty
should work on all macs (both intel and apple silicon) and all relevant macos versions
the homebrew package is maintained by the homebrew team (thanks!)
## nix package
`nix profile install github:9001/copyparty`
@ -2310,7 +2380,7 @@ some recommended dependencies are enabled by default; [override the package](htt
## nixos module
for this setup, you will need a [flake-enabled](https://nixos.wiki/wiki/Flakes) installation of NixOS.
for [flake-enabled](https://nixos.wiki/wiki/Flakes) installations of NixOS:
```nix
{
@ -2337,6 +2407,33 @@ for this setup, you will need a [flake-enabled](https://nixos.wiki/wiki/Flakes)
}
```
if you don't use a flake in your configuration, you can use other dependency management tools like [npins](https://github.com/andir/npins), [niv](https://github.com/nmattia/niv), or even plain [`fetchTarball`](https://nix.dev/manual/nix/stable/language/builtins#builtins-fetchTarball), like so:
```nix
{ pkgs, ... }:
let
# npins example, adjust for your setup. copyparty should be a path to the downloaded repo
# for niv, just replace the npins folder import with the sources.nix file
copyparty = (import ./npins).copyparty;
# or with fetchTarball:
copyparty = fetchTarball "https://github.com/9001/copyparty/archive/hovudstraum.tar.gz";
in
{
# load the copyparty NixOS module
imports = [ "${copyparty}/contrib/nixos/modules/copyparty.nix" ];
# add the copyparty overlay to expose the package to the module
nixpkgs.overlays = [ (import "${copyparty}/contrib/package/nix/overlay.nix") ];
# (optional) install the package globally
environment.systemPackages = [ pkgs.copyparty ];
# configure the copyparty module
services.copyparty.enable = true;
}
```
copyparty on NixOS is configured via `services.copyparty` options, for example:
```nix
services.copyparty = {
@ -2523,11 +2620,20 @@ sync folders to/from copyparty
NOTE: full bidirectional sync, like what [nextcloud](https://docs.nextcloud.com/server/latest/user_manual/sv/files/desktop_mobile_sync.html) and [syncthing](https://syncthing.net/) does, will never be supported! Only single-direction sync (server-to-client, or client-to-server) is possible with copyparty
* if you want bidirectional sync, then copyparty and syncthing *should* be entirely safe to combine; they should be able to collaborate on the same folders without causing any trouble for eachother. Many people do this, and there have been no issues so far. But, if you *do* encounter any problems, please [file a copyparty bug](https://github.com/9001/copyparty/issues/new/choose) and I'll try to help -- just keep in mind I've never used syncthing before :-)
the commandline uploader [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy) with `--dr` is the best way to sync a folder to copyparty; verifies checksums and does files in parallel, and deletes unexpected files on the server after upload has finished which makes file-renames really cheap (it'll rename serverside and skip uploading)
if you want to sync with `u2c.py` then:
* the `e2dsa` option (either globally or volflag) must be enabled on the server for the volumes you're syncing into
* ...but DON'T enable global-options `no-hash` or `no-idx` (or volflags `nohash` / `noidx`), or at least make sure they are configured so they do not affect anything you are syncing into
* ...and u2c needs the delete-permission, so either `rwd` at minimum, or just `A` which is the same as `rwmd.a`
* quick reminder that `a` and `A` are different permissions, and `.` is very useful for sync
alternatively there is [rclone](./docs/rclone.md) which allows for bidirectional sync and is *way* more flexible (stream files straight from sftp/s3/gcs to copyparty, ...), although there is no integrity check and it won't work with files over 100 MiB if copyparty is behind cloudflare
* starting from rclone v1.63, rclone is faster than u2c.py on low-latency connections
* but this is only true for the initial upload; u2c will be faster for periodic syncing
## mount as drive
@ -2569,6 +2675,8 @@ there is no iPhone app, but the following shortcuts are almost as good:
* can download links and rehost the target file on copyparty (see first comment inside the shortcut)
* pics become lowres if you share from gallery to shortcut, so better to launch the shortcut and pick stuff from there
if you want to run the copyparty server on your iPhone or iPad, see [install on iOS](#install-on-iOS)
# performance
@ -2792,9 +2900,10 @@ enable [music tags](#metadata-from-audio-files):
enable [thumbnails](#thumbnails) of...
* **images:** `Pillow` and/or `pyvips` and/or `ffmpeg` (requires py2.7 or py3.5+)
* **videos/audio:** `ffmpeg` and `ffprobe` somewhere in `$PATH`
* **HEIF pictures:** `pyvips` or `ffmpeg` or `pyheif-pillow-opener` (requires Linux or a C compiler)
* **HEIF pictures:** `pyvips` or `ffmpeg` or `pillow-heif`
* **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin` or pillow v11.3+
* **JPEG XL pictures:** `pyvips` or `ffmpeg`
* **RAW images:** `rawpy`, plus one of `pyvips` or `Pillow` (for some formats)
enable sending [zeromq messages](#zeromq) from event-hooks: `pyzmq`
@ -2825,9 +2934,10 @@ set any of the following environment variables to disable its associated optiona
| `PRTY_NO_PIL` | disable all [Pillow](https://pypi.org/project/pillow/)-based thumbnail support; will fallback to libvips or ffmpeg |
| `PRTY_NO_PILF` | disable Pillow `ImageFont` text rendering, used for folder thumbnails |
| `PRTY_NO_PIL_AVIF` | disable Pillow avif support (internal and/or [plugin](https://pypi.org/project/pillow-avif-plugin/)) |
| `PRTY_NO_PIL_HEIF` | disable 3rd-party Pillow plugin for [HEIF support](https://pypi.org/project/pyheif-pillow-opener/) |
| `PRTY_NO_PIL_HEIF` | disable 3rd-party Pillow plugin for [HEIF support](https://pypi.org/project/pillow-heif/) |
| `PRTY_NO_PIL_WEBP` | disable use of native webp support in Pillow |
| `PRTY_NO_PSUTIL` | do not use [psutil](https://pypi.org/project/psutil/) for reaping stuck hooks and plugins on Windows |
| `PRTY_NO_RAW` | disable all [rawpy](https://pypi.org/project/rawpy/)-based thumbnail support for RAW images |
| `PRTY_NO_VIPS` | disable all [libvips](https://pypi.org/project/pyvips/)-based thumbnail support; will fallback to Pillow or ffmpeg |
example: `PRTY_NO_PIL=1 python3 copyparty-sfx.py`
@ -2900,6 +3010,27 @@ if you want thumbnails (photos+videos) and you're okay with spending another 132
* or if you want to use `vips` for photo-thumbs instead, `pkg install libvips && python -m pip install --user -U wheel && python -m pip install --user -U pyvips && (cd /data/data/com.termux/files/usr/lib/; ln -s libgobject-2.0.so{,.0}; ln -s libvips.so{,.42})`
# install on iOS
first install one of the following:
* [a-Shell mini](https://apps.apple.com/us/app/a-shell-mini/id1543537943) gives you the essential features
* [a-Shell](https://apps.apple.com/us/app/a-shell/id1473805438) also enables audio transcoding and better thubmnails
and then copypaste the following command into `a-Shell`:
```sh
curl https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh | sh
```
what this does:
* creates a basic [config file](#accounts-and-volumes) named `cpc` which you can edit with `vim cpc`
* adds the command `cpp` to launch copyparty with that config file
known issues:
* cannot run in the background; it needs to be on-screen to accept connections / uploads / downloads
* the best way to exit copyparty is to swipe away the app
# reporting bugs
ideas for context to include, and where to submit them

View file

@ -1,7 +1,7 @@
# [`u2c.py`](u2c.py)
* command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm)
* file uploads, file-search, autoresume of aborted/broken uploads
* sync local folder to server
* [sync local folder to server](https://github.com/9001/copyparty/#folder-sync)
* generally faster than browsers
* if something breaks just restart it

View file

@ -1,4 +1,4 @@
#!/bin/sh
#!/bin/bash
# usage: ./bubbleparty.sh ./copyparty-sfx.py ....
bwrap \
--unshare-all \
@ -9,7 +9,7 @@ bwrap \
--dev-bind /dev /dev \
--dir /tmp \
--dir /var \
--bind $(pwd) $(pwd) \
--bind "$(pwd)" "$(pwd)" \
--share-net \
--die-with-parent \
--file 11 /etc/passwd \

View file

@ -8,7 +8,7 @@ import sqlite3
import argparse
DB_VER1 = 3
DB_VER2 = 5
DB_VER2 = 6
BY_PATH = None
NC = None
@ -39,7 +39,7 @@ def ls(db):
print(f"{nfiles} files")
print(f"{ntags} tags\n")
print("number of occurences for each tag,")
print("number of occurrences for each tag,")
print(" 'x' = file has no tags")
print(" 't:mtp' = the mtp flag (file not mtp processed yet)")
print()

View file

@ -46,7 +46,7 @@ def main(cli, vn, rem):
# uncomment one of these:
send_http_302_temporary_redirect(cli, new_path)
#send_http_301_permanent_redirect(cli, new_path)
#send_errorpage_with_redirect_link(cli, new_path)
# send_http_301_permanent_redirect(cli, new_path)
# send_errorpage_with_redirect_link(cli, new_path)
return "true"

View file

@ -9,7 +9,7 @@ from plyer import notification
_ = r"""
show os notification on upload; works on windows, linux, macos, android
depdencies:
dependencies:
windows: python3 -m pip install --user -U plyer
linux: python3 -m pip install --user -U plyer
macos: python3 -m pip install --user -U plyer pyobjus

View file

@ -66,7 +66,7 @@ def main():
try:
sp.check_call(cmd)
except:
t = "-- FAILED TO DONWLOAD " + name
t = "-- FAILED TO DOWNLOAD " + name
print(f"{t}\n", end="")
open(t, "wb").close()

View file

@ -7,7 +7,7 @@ example copyparty config to use this:
--urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=guestbook=t10,ad,p,bin/mtag/guestbook-read.py:mte=+guestbook
explained:
for realpath srv/hello (served at /hello), write-only for eveyrone,
for realpath srv/hello (served at /hello), write-only for everyone,
enable file analysis on upload (e2ts),
use mtp plugin "bin/mtag/guestbook-read.py" to provide metadata tag "guestbook",
do this on all uploads regardless of extension,

View file

@ -11,7 +11,7 @@ example copyparty config to use this:
--urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=xgb=ebin,t10,ad,p,bin/mtag/guestbook.py:mte=+xgb
explained:
for realpath srv/hello (served at /hello),write-only for eveyrone,
for realpath srv/hello (served at /hello),write-only for everyone,
enable file analysis on upload (e2ts),
use mtp plugin "bin/mtag/guestbook.py" to provide metadata tag "xgb",
do this on all uploads with the file extension "bin",

View file

@ -84,7 +84,7 @@ def main():
# on success, delete the .bin file which contains the URL
os.unlink(fp)
except:
open("-- FAILED TO DONWLOAD " + name, "wb").close()
open("-- FAILED TO DOWNLOAD " + name, "wb").close()
os.unlink(tfn)
print(url)

View file

@ -6,8 +6,8 @@ __copyright__ = 2019
__license__ = "MIT"
__url__ = "https://github.com/9001/copyparty/"
S_VERSION = "2.0"
S_BUILD_DT = "2024-10-01"
S_VERSION = "2.1"
S_BUILD_DT = "2025-09-06"
"""
mount a copyparty server (local or remote) as a filesystem
@ -99,7 +99,7 @@ except:
elif MACOS:
libfuse = "install https://osxfuse.github.io/"
else:
libfuse = "apt install libfuse3-3\n modprobe fuse"
libfuse = "apt install libfuse2\n modprobe fuse"
m = """\033[33m
could not import fuse; these may help:
@ -359,7 +359,7 @@ class Gateway(object):
def sendreq(self, meth, path, headers, **kwargs):
tid = get_tid()
if self.password:
headers["Cookie"] = "=".join(["cppwd", self.password])
headers["PW"] = self.password
try:
c = self.getconn(tid)
@ -902,9 +902,7 @@ class CPPF(Operations):
return ret
def _readdir(self, path, fh=None):
path = path.strip("/")
dbg("readdir %r [%s]", path, fh)
dbg("dircache miss")
ret = self.gw.listdir(path)
if not self.n_dircache:
return ret
@ -914,11 +912,17 @@ class CPPF(Operations):
self.dircache.append(cn)
self.clean_dircache()
# import pprint; pprint.pprint(ret)
return ret
def readdir(self, path, fh=None):
return [".", ".."] + list(self._readdir(path, fh))
dbg("readdir %r [%s]", path, fh)
path = path.strip("/")
cn = self.get_cached_dir(path)
if cn:
ret = cn.data
else:
ret = self._readdir(path, fh)
return [".", ".."] + list(ret)
def read(self, path, length, offset, fh=None):
req_max = 1024 * 1024 * 8
@ -993,7 +997,6 @@ class CPPF(Operations):
if cn:
dents = cn.data
else:
dbg("cache miss")
dents = self._readdir(dirpath)
try:
@ -1141,10 +1144,15 @@ def main():
if WINDOWS:
examples.append("http://192.168.1.69:3923/music/ M:")
epi = "example:" + ex_pre + ex_pre.join(examples)
epi += """\n
NOTE: if server has --usernames enabled, then password is "username:password"
"""
ap = argparse.ArgumentParser(
formatter_class=TheArgparseFormatter,
description="mount a copyparty server as a local filesystem -- " + ver,
epilog="example:" + ex_pre + ex_pre.join(examples),
epilog=epi,
)
# fmt: off
ap.add_argument("base_url", type=str, help="remote copyparty URL to mount")

View file

@ -141,7 +141,7 @@ chmod 777 "$jail/tmp"
# create a dev
(cd $jail; mkdir -p dev; cd dev
(cd "$jail"; mkdir -p dev; cd dev
[ -e null ] || mknod -m 666 null c 1 3
[ -e zero ] || mknod -m 666 zero c 1 5
[ -e random ] || mknod -m 444 random c 1 8

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python3
from __future__ import print_function, unicode_literals
S_VERSION = "2.11"
S_BUILD_DT = "2025-05-18"
S_VERSION = "2.13"
S_BUILD_DT = "2025-09-05"
"""
u2c.py: upload to copyparty
@ -10,7 +10,7 @@ u2c.py: upload to copyparty
https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py
- dependencies: no
- supports python 2.6, 2.7, and 3.3 through 3.12
- supports python 2.6, 2.7, and 3.3 through 3.14
- if something breaks just try again and it'll autoresume
"""
@ -590,9 +590,10 @@ def undns(url):
def _scd(err, top):
"""non-recursive listing of directory contents, along with stat() info"""
top_ = os.path.join(top, b"")
with os.scandir(top) as dh:
for fh in dh:
abspath = os.path.join(top, fh.name)
abspath = top_ + fh.name
try:
yield [abspath, fh.stat()]
except Exception as ex:
@ -601,8 +602,9 @@ def _scd(err, top):
def _lsd(err, top):
"""non-recursive listing of directory contents, along with stat() info"""
top_ = os.path.join(top, b"")
for name in os.listdir(top):
abspath = os.path.join(top, name)
abspath = top_ + name
try:
yield [abspath, os.stat(abspath)]
except Exception as ex:
@ -677,7 +679,7 @@ def walkdirs(err, tops, excl):
yield stop, ap[len(stop) :].lstrip(sep), inf
else:
d, n = top.rsplit(sep, 1)
yield d, n, os.stat(top)
yield d or b"/", n, os.stat(top)
# mostly from copyparty/util.py
@ -1527,10 +1529,10 @@ def main():
# fmt: off
ap = app = argparse.ArgumentParser(formatter_class=APF, description="copyparty up2k uploader / filesearch tool " + ver, epilog="""
NOTE:
source file/folder selection uses rsync syntax, meaning that:
NOTE: source file/folder selection uses rsync syntax, meaning that:
"foo" uploads the entire folder to URL/foo/
"foo/" uploads the CONTENTS of the folder into URL/
NOTE: if server has --usernames enabled, then password is "username:password"
""")
ap.add_argument("url", type=unicode, help="server url, including destination folder")

View file

@ -31,7 +31,7 @@
# generate the list of permitted IP ranges like so:
# (curl -s https://www.cloudflare.com/ips-v{4,6} | sed 's/^/allow /; s/$/;/'; echo; echo "deny all;") > /etc/nginx/cloudflare-only.conf
#
# and then enable it below by uncomenting the cloudflare-only.conf line
# and then enable it below by uncommenting the cloudflare-only.conf line
#
# ======================================================================

View file

@ -50,6 +50,7 @@ let
configStr = ''
${mkSection "global" cfg.settings}
${cfg.globalExtraConfig}
${mkSection "accounts" (accountsWithPlaceholders cfg.accounts)}
${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)}
'';
@ -131,6 +132,12 @@ in
'';
};
globalExtraConfig = mkOption {
type = types.str;
default = "";
description = "Appended to the end of the [global] section verbatim. This is useful for flags which are used in a repeating manner (e.g. ipu: 255.255.255.1=user) which can't be repeated in the settings = {} attribute set.";
};
accounts = mkOption {
type = types.attrsOf (
types.submodule (
@ -373,3 +380,4 @@ in
}
);
}

View file

@ -3,7 +3,7 @@
# NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead.
pkgname=copyparty
pkgver="1.19.0"
pkgver="1.19.8"
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any")
@ -23,7 +23,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}/copyparty.conf" )
sha256sums=("179b027d51e4fe7ebdab2b18c07475d52c57e2ce69256292b157a8efacd82118")
sha256sums=("3143ba5216c8d4cf1fbc58fa08c6ecef955de04b5e34b3910ab0b71cffec88ef")
build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"

View file

@ -2,7 +2,7 @@
pkgname=copyparty
pkgver=1.19.0
pkgver=1.19.8
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any")
@ -20,7 +20,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=("179b027d51e4fe7ebdab2b18c07475d52c57e2ce69256292b157a8efacd82118")
sha256sums=("3143ba5216c8d4cf1fbc58fa08c6ecef955de04b5e34b3910ab0b71cffec88ef")
build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"

View file

@ -1,10 +1,10 @@
{
lib,
stdenv,
makeWrapper,
buildPythonApplication,
fetchurl,
util-linux,
python,
setuptools,
jinja2,
impacket,
pyopenssl,
@ -15,6 +15,10 @@
pyzmq,
ffmpeg,
mutagen,
pyftpdlib,
magic,
partftpy,
fusepy, # for partyfuse
# use argon2id-hashed passwords in config files (sha2 is always available)
withHashedPasswords ? true,
@ -40,12 +44,21 @@
# send ZeroMQ messages from event-hooks
withZeroMQ ? true,
# enable FTP server
withFTP ? true,
# enable FTPS support in the FTP server
withFTPS ? false,
# enable TFTP server
withTFTP ? false,
# samba/cifs server; dangerous and buggy, enable if you really need it
withSMB ? false,
# enables filetype detection for nameless uploads
withMagic ? false,
# extra packages to add to the PATH
extraPackages ? [ ],
@ -58,14 +71,23 @@
let
pinData = lib.importJSON ./pin.json;
pyEnv = python.withPackages (
ps:
with ps;
runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg);
in
buildPythonApplication {
pname = "copyparty";
inherit (pinData) version;
src = fetchurl {
inherit (pinData) url hash;
};
dependencies =
[
jinja2
fusepy
]
++ lib.optional withSMB impacket
++ lib.optional withFTP pyftpdlib
++ lib.optional withFTPS pyopenssl
++ lib.optional withTFTP partftpy
++ lib.optional withCertgen cfssl
++ lib.optional withThumbnails pillow
++ lib.optional withFastThumbnails pyvips
@ -73,25 +95,14 @@ let
++ lib.optional withBasicAudioMetadata mutagen
++ lib.optional withHashedPasswords argon2-cffi
++ lib.optional withZeroMQ pyzmq
++ (extraPythonPackages ps)
);
++ lib.optional withMagic magic
++ (extraPythonPackages python.pkgs);
makeWrapperArgs = [ "--prefix PATH : ${lib.makeBinPath runtimeDeps}" ];
runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg);
in
stdenv.mkDerivation {
pname = "copyparty";
inherit (pinData) version;
src = fetchurl {
inherit (pinData) url hash;
};
nativeBuildInputs = [ makeWrapper ];
dontUnpack = true;
installPhase = ''
install -Dm755 $src $out/share/copyparty-sfx.py
makeWrapper ${pyEnv.interpreter} $out/bin/copyparty \
--prefix PATH : ${lib.makeBinPath runtimeDeps} \
--add-flag $out/share/copyparty-sfx.py
'';
pyproject = true;
build-system = [
setuptools
];
meta = {
description = "Turn almost any device into a file server";
longDescription = ''
@ -101,8 +112,7 @@ stdenv.mkDerivation {
homepage = "https://github.com/9001/copyparty";
changelog = "https://github.com/9001/copyparty/releases/tag/v${pinData.version}";
license = lib.licenses.mit;
inherit (python.meta) platforms;
mainProgram = "copyparty";
sourceProvenance = [ lib.sourceTypes.binaryBytecode ];
sourceProvenance = [ lib.sourceTypes.fromSource ];
};
}

View file

@ -1,5 +1,5 @@
{
"url": "https://github.com/9001/copyparty/releases/download/v1.19.0/copyparty-sfx.py",
"version": "1.19.0",
"hash": "sha256-9A+zPtkVtUuGHB/JJV3fhVtJderLUGxHqvuJQz0/1+Q="
"url": "https://github.com/9001/copyparty/releases/download/v1.19.8/copyparty-1.19.8.tar.gz",
"version": "1.19.8",
"hash": "sha256-MUO6UhbI1M8fvFj6CMbs75Vd4EteNLORCrC3HP/siO8="
}

View file

@ -11,14 +11,14 @@ import base64
import json
import hashlib
import sys
import re
import tarfile
from pathlib import Path
OUTPUT_FILE = Path("pin.json")
TARGET_ASSET = "copyparty-sfx.py"
TARGET_ASSET = lambda version: f"copyparty-{version}.tar.gz"
HASH_TYPE = "sha256"
LATEST_RELEASE_URL = "https://api.github.com/repos/9001/copyparty/releases/latest"
DOWNLOAD_URL = lambda version: f"https://github.com/9001/copyparty/releases/download/v{version}/{TARGET_ASSET}"
DOWNLOAD_URL = lambda version: f"https://github.com/9001/copyparty/releases/download/v{version}/{TARGET_ASSET(version)}"
def get_formatted_hash(binary):
@ -29,11 +29,13 @@ def get_formatted_hash(binary):
return f"{HASH_TYPE}-{encoded_hash}"
def version_from_sfx(binary):
result = re.search(b'^VER = "(.*)"$', binary, re.MULTILINE)
if result:
return result.groups(1)[0].decode("ascii")
def version_from_tar_gz(path):
with tarfile.open(path) as tarball:
release_name = tarball.getmembers()[0].name
prefix = "copyparty-"
if release_name.startswith(prefix):
return release_name.replace(prefix, "")
raise ValueError("version not found in provided file")
@ -42,7 +44,7 @@ def remote_release_pin():
response = requests.get(LATEST_RELEASE_URL).json()
version = response["tag_name"].lstrip("v")
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET][0]
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET(version)][0]
download_url = asset_info["browser_download_url"]
asset = requests.get(download_url)
formatted_hash = get_formatted_hash(asset.content)
@ -52,10 +54,9 @@ def remote_release_pin():
def local_release_pin(path):
asset = path.read_bytes()
version = version_from_sfx(asset)
version = version_from_tar_gz(path)
download_url = DOWNLOAD_URL(version)
formatted_hash = get_formatted_hash(asset)
formatted_hash = get_formatted_hash(path.read_bytes())
result = {"url": download_url, "version": version, "hash": formatted_hash}
return result

View file

@ -0,0 +1,11 @@
final: prev: {
copyparty = final.python3.pkgs.callPackage ./copyparty {
ffmpeg = final.ffmpeg-full;
};
python3 = prev.python3.override {
packageOverrides = pyFinal: pyPrev: {
partftpy = pyFinal.callPackage ./partftpy { };
};
};
}

View file

@ -0,0 +1,30 @@
{
lib,
buildPythonPackage,
fetchurl,
setuptools,
}:
let
pinData = lib.importJSON ./pin.json;
in
buildPythonPackage rec {
pname = "partftpy";
inherit (pinData) version;
pyproject = true;
src = fetchurl {
inherit (pinData) url hash;
};
build-system = [ setuptools ];
pythonImportsCheck = [ "partftpy.TftpServer" ];
meta = {
description = "Pure Python TFTP library (copyparty edition)";
homepage = "https://github.com/9001/partftpy";
changelog = "https://github.com/9001/partftpy/releases/tag/${version}";
license = lib.licenses.mit;
};
}

View file

@ -0,0 +1,5 @@
{
"url": "https://github.com/9001/partftpy/releases/download/v0.4.0/partftpy-0.4.0.tar.gz",
"version": "0.4.0",
"hash": "sha256-5Q2zyuJ892PGZmb+YXg0ZPW/DK8RDL1uE0j5HPd4We0="
}

View file

@ -0,0 +1,50 @@
#!/usr/bin/env python3
# Update the Nix package pin
#
# Usage: ./update.sh
import base64
import json
import hashlib
import sys
from pathlib import Path
OUTPUT_FILE = Path("pin.json")
TARGET_ASSET = lambda version: f"partftpy-{version}.tar.gz"
HASH_TYPE = "sha256"
LATEST_RELEASE_URL = "https://api.github.com/repos/9001/partftpy/releases/latest"
def get_formatted_hash(binary):
hasher = hashlib.new("sha256")
hasher.update(binary)
asset_hash = hasher.digest()
encoded_hash = base64.b64encode(asset_hash).decode("ascii")
return f"{HASH_TYPE}-{encoded_hash}"
def remote_release_pin():
import requests
response = requests.get(LATEST_RELEASE_URL).json()
version = response["tag_name"].lstrip("v")
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET(version)][0]
download_url = asset_info["browser_download_url"]
asset = requests.get(download_url)
formatted_hash = get_formatted_hash(asset.content)
result = {"url": download_url, "version": version, "hash": formatted_hash}
return result
def main():
result = remote_release_pin()
print(result)
json_result = json.dumps(result, indent=4)
OUTPUT_FILE.write_text(json_result)
if __name__ == "__main__":
main()

View file

@ -1,26 +0,0 @@
{
stdenvNoCC,
copyparty,
python3,
makeBinaryWrapper,
}:
let
python = python3.withPackages (p: [ p.fusepy ]);
in
stdenvNoCC.mkDerivation {
pname = "partyfuse";
inherit (copyparty) version meta;
src = ../../../..;
nativeBuildInputs = [ makeBinaryWrapper ];
installPhase = ''
runHook preInstall
install -Dm444 bin/partyfuse.py -t $out/share/copyparty
makeWrapper ${python.interpreter} $out/bin/partyfuse \
--add-flag $out/share/copyparty/partyfuse.py
runHook postInstall
'';
}

View file

@ -1,24 +0,0 @@
{
stdenvNoCC,
copyparty,
python312,
makeBinaryWrapper,
}:
stdenvNoCC.mkDerivation {
pname = "u2c";
inherit (copyparty) version meta;
src = ../../../..;
nativeBuildInputs = [ makeBinaryWrapper ];
installPhase = ''
runHook preInstall
install -Dm444 bin/u2c.py -t $out/share/copyparty
mkdir $out/bin
makeWrapper ${python312.interpreter} $out/bin/u2c \
--add-flag $out/share/copyparty/u2c.py
runHook postInstall
'';
}

View file

@ -0,0 +1,62 @@
Name: copyparty
Version: $pkgver
Release: $pkgrel
License: MIT
Group: Utilities
URL: https://github.com/9001/copyparty
Source0: copyparty-$pkgver.tar.gz
Summary: File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++
BuildArch: noarch
BuildRequires: python3, python3-devel, pyproject-rpm-macros, python-setuptools, python-wheel, make
Requires: python3, (python3-jinja2 or python-jinja2), lsof
Recommends: ffmpeg, (golang-github-cloudflare-cfssl or cfssl), python-mutagen, python-pillow, python-pyvips
Recommends: qm-vamp-plugins, python-argon2-cffi, (python-pyopenssl or pyopenssl), python-impacket
%description
Portable file server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps
See release at https://github.com/9001/copyparty/releases
%global debug_package %{nil}
%generate_buildrequires
%pyproject_buildrequires
%prep
%setup -q
%build
cd "copyparty/web"
make
cd -
%pyproject_wheel
%install
mkdir -p %{buildroot}%{_bindir}
mkdir -p %{buildroot}%{_libdir}/systemd/{system,user}
mkdir -p %{buildroot}/etc/%{name}
mkdir -p %{buildroot}/var/lib/%{name}-jail
mkdir -p %{buildroot}%{_datadir}/licenses/%{name}
%pyproject_install
%pyproject_save_files copyparty
install -m 0755 bin/prisonparty.sh %{buildroot}%{_bindir}/prisonpary.sh
install -m 0644 contrib/systemd/%{name}.conf %{buildroot}/etc/%{name}/%{name}.conf
install -m 0644 contrib/systemd/%{name}@.service %{buildroot}%{_libdir}/systemd/system/%{name}@.service
install -m 0644 contrib/systemd/%{name}-user.service %{buildroot}%{_libdir}/systemd/user/%{name}.service
install -m 0644 contrib/systemd/prisonparty@.service %{buildroot}%{_libdir}/systemd/system/prisonparty@.service
install -m 0644 contrib/systemd/index.md %{buildroot}/var/lib/%{name}-jail/README.md
install -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE
%files -n copyparty -f %{pyproject_files}
%license LICENSE
%{_bindir}/copyparty
%{_bindir}/partyfuse
%{_bindir}/u2c
%{_bindir}/prisonpary.sh
/etc/%{name}/%{name}.conf
%{_libdir}/systemd/system/%{name}@.service
%{_libdir}/systemd/user/%{name}.service
%{_libdir}/systemd/system/prisonparty@.service
/var/lib/%{name}-jail/README.md

View file

@ -101,10 +101,10 @@
function our_hotkey_handler(e) {
// bail if either ALT, CTRL, or SHIFT is pressed
if (e.altKey || e.shiftKey || e.isComposing || ctrl(e))
if (anymod(e))
return main_hotkey_handler(e); // let copyparty handle this keystroke
var key_name = (e.code || e.key) + '',
var keycode = (e.key || e.code) + '',
ae = document.activeElement,
aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : '';
@ -114,7 +114,7 @@
if (aet && !/^(a|button|tr|td|div|pre)$/.test(aet))
return main_hotkey_handler(e); // let copyparty handle this keystroke
if (key_name == 'KeyW') {
if (keycode == 'w' || keycode == 'KeyW') {
// okay, this one's for us... do the thing
action_to_perform();
return ev(e);

71
contrib/setup-ashell.sh Normal file
View file

@ -0,0 +1,71 @@
#!/bin/bash
#
# this script will install copyparty onto an iOS device (iPhone/iPad)
#
# step 1: install a-Shell:
# https://apps.apple.com/us/app/a-shell/id1473805438
#
# step 2: copypaste the following command into a-Shell:
# curl https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh
#
# step 3: launch copyparty with this command: cpp
#
# if you ever want to upgrade copyparty, just repeat step 2
cd "$HOME/Documents"
curl -Locopyparty https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py
# create the config file? (cannot use heredoc because body too large)
[ -e cpc ] || {
echo '[global]' >cpc
echo ' p: 80, 443, 3923 # enable http and https on these ports' >>cpc
echo ' e2dsa # enable file indexing and filesystem scanning' >>cpc
echo ' e2ts # and enable multimedia indexing' >>cpc
echo ' ver # show copyparty version in the controlpanel' >>cpc
echo ' qrz: 2 # enable qr-code and make it big' >>cpc
echo ' qrp: 1 # reduce qr-code padding' >>cpc
echo ' qr-fg: -1 # optimize for basic/simple terminals' >>cpc
echo ' qr-wait: 0.3 # less chance of getting scrolled away' >>cpc
echo '' >>cpc
echo ' # enable these by uncommenting them:' >>cpc
echo ' # ftp: 21 # enable ftp server on port 21' >>cpc
echo ' # tftp: 69 # enable tftp server on port 69' >>cpc
echo '' >>cpc
echo '[/]' >>cpc
echo ' ~/Documents' >>cpc
echo ' accs:' >>cpc
echo ' A: *' >>cpc
}
# create the launcher?
[ -e cpp ] || {
echo '#!/bin/sh' >cpp
echo '' >>cpp
echo '# change the font so the qr-code draws correctly:' >>cpp
echo 'config -n "Menlo" # name' >>cpp
echo 'config -s 8 # size' >>cpp
echo '' >>cpp
echo '# launch copyparty' >>cpp
echo 'exec copyparty -c cpc "$@"' >>cpp
}
chmod 755 copyparty cpp
echo
echo =================================
echo
echo 'okay, all done!'
echo
echo 'you can edit your config'
echo 'with this command: vim cpc'
echo
echo 'you can run copyparty'
echo 'with this command: cpp'
echo

View file

@ -111,7 +111,9 @@ class EnvParams(object):
def __init__(self) -> None:
self.t0 = time.time()
self.mod = ""
self.mod_ = ""
self.cfg = ""
self.scfg = True
E = EnvParams()

View file

@ -36,6 +36,7 @@ from .__init__ import (
)
from .__version__ import CODENAME, S_BUILD_DT, S_VERSION
from .authsrv import expand_config_file, split_cfg_ln, upgrade_cfg_fmt
from .bos import bos
from .cfg import flagcats, onedash
from .svchub import SvcHub
from .util import (
@ -186,7 +187,7 @@ def init_E(EE: EnvParams) -> None:
E = EE # pylint: disable=redefined-outer-name
def get_unixdir() -> str:
def get_unixdir() -> tuple[str, bool]:
paths: list[tuple[Callable[..., Any], str]] = [
(os.environ.get, "XDG_CONFIG_HOME"),
(os.path.expanduser, "~/.config"),
@ -196,41 +197,57 @@ def init_E(EE: EnvParams) -> None:
(unicode, "/tmp"),
]
errs = []
for chk in [os.listdir, os.mkdir]:
for npath, (pf, pa) in enumerate(paths):
priv = npath < 2 # private/trusted location
ram = npath > 1 # "nonvolatile"; not semantically same as `not priv`
p = ""
try:
p = pf(pa)
# print(chk.__name__, p, pa)
if not p or p.startswith("~"):
continue
p = os.path.normpath(p)
chk(p) # type: ignore
p = os.path.join(p, "copyparty")
if not os.path.isdir(p):
os.mkdir(p)
mkdir = not os.path.isdir(p)
if mkdir:
os.mkdir(p, 0o700)
if npath > 1:
t = "Using [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/"
errs.append(t % (p,))
p = os.path.join(p, "copyparty")
if not priv and os.path.isdir(p):
uid = os.geteuid()
if os.stat(p).st_uid != uid:
p += ".%s" % (uid,)
if os.path.isdir(p) and os.stat(p).st_uid != uid:
raise Exception("filesystem has broken unix permissions")
try:
os.listdir(p)
except:
os.mkdir(p, 0o700)
if ram:
t = "Using %s/copyparty [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/"
errs.append(t % (pa, p))
elif mkdir:
t = "Using %s/copyparty [%s] for config%s (Warning: %s did not exist and was created just now)"
errs.append(t % (pa, p, " instead" if npath else "", pa))
elif errs:
errs.append("Using [%s] instead" % (p,))
errs.append("Using %s/copyparty [%s] instead" % (pa, p))
if errs:
warn(". ".join(errs))
return p # type: ignore
return p, priv
except Exception as ex:
if p and npath < 2:
t = "Unable to store config in [%s] due to %r"
errs.append(t % (p, ex))
if p:
t = "Unable to store config in %s [%s] due to %r"
errs.append(t % (pa, p, ex))
raise Exception("could not find a writable path for config")
t = "could not find a writable path for runtime state:\n> %s"
raise Exception(t % ("\n> ".join(errs)))
E.mod = os.path.dirname(os.path.realpath(__file__))
if E.mod.endswith("__init__"):
E.mod = os.path.dirname(E.mod)
E.mod_ = os.path.join(E.mod, "")
try:
p = os.environ.get("XDG_CONFIG_HOME")
@ -241,7 +258,7 @@ def init_E(EE: EnvParams) -> None:
p = os.path.abspath(os.path.realpath(p))
p = os.path.join(p, "copyparty")
if not os.path.isdir(p):
os.mkdir(p)
os.mkdir(p, 0o700)
os.listdir(p)
except:
p = ""
@ -254,11 +271,11 @@ def init_E(EE: EnvParams) -> None:
elif sys.platform == "darwin":
E.cfg = os.path.expanduser("~/Library/Preferences/copyparty")
else:
E.cfg = get_unixdir()
E.cfg, E.scfg = get_unixdir()
E.cfg = E.cfg.replace("\\", "/")
try:
os.makedirs(E.cfg)
bos.makedirs(E.cfg, bos.MKD_700)
except:
if not os.path.isdir(E.cfg):
raise
@ -436,6 +453,29 @@ def args_from_cfg(cfg_path: str) -> list[str]:
return ret
def expand_cfg(argv) -> list[str]:
if CFG_DEF:
supp = args_from_cfg(CFG_DEF[0])
argv = argv[:1] + supp + argv[1:]
n = 0
while n < len(argv):
v1 = argv[n]
v1v = v1[2:].lstrip("=")
try:
v2 = argv[n + 1]
except:
v2 = ""
n += 1
if v1 == "-c" and v2 and os.path.isfile(v2):
n += 1
argv = argv[:n] + args_from_cfg(v2) + argv[n:]
elif v1.startswith("-c") and v1v and os.path.isfile(v1v):
argv = argv[:n] + args_from_cfg(v1v) + argv[n:]
return argv
def sighandler(sig: Optional[int] = None, frame: Optional[FrameType] = None) -> None:
msg = [""] * 5
for th in threading.enumerate():
@ -609,8 +649,77 @@ def get_sects():
if no accounts or volumes are configured,
current folder will be read/write for everyone
the group @acct will always have every user with an account
(the name of that group can be changed with --grp-all)
consider the config file for more flexible account/volume management,
including dynamic reload at runtime (and being more readable w)
see \033[32m--help-auth\033[0m for ways to provide the password in requests;
see \033[32m--help-idp\033[0m for replacing it with SSO and auth-middlewares
"""
),
],
[
"auth",
"how to login from a client",
dedent(
"""
different ways to provide the password so you become authenticated:
login with the ui:
go to \033[36mhttp://127.0.0.1:3923/?h\033[0m and login there
send the password in the '\033[36mPW\033[0m' http-header:
\033[36mPW: \033[35mhunter2\033[0m
or if you have \033[33m--accounts\033[0m enabled,
\033[36mPW: \033[35med:hunter2\033[0m
send the password in the URL itself:
\033[36mhttp://127.0.0.1:3923/\033[35m?pw=hunter2\033[0m
or if you have \033[33m--accounts\033[0m enabled,
\033[36mhttp://127.0.0.1:3923/\033[35m?pw=ed:hunter2\033[0m
use basic-authentication:
\033[36mhttp://\033[35med:hunter2\033[36m@127.0.0.1:3923/\033[0m
which should be the same as this header:
\033[36mAuthorization: Basic \033[35mZWQ6aHVudGVyMg==\033[0m
"""
),
],
[
"auth-ord",
"authentication precedence",
dedent(
"""
\033[33m--auth-ord\033[0m is a comma-separated list of auth options
(one or more of the [\033[35moptions\033[0m] below); first one wins
[\033[35mpw\033[0m] is conventional login, for example the "\033[36mPW\033[0m" header,
or the \033[36m?pw=\033[0m[...] URL-suffix, or a valid session cookie
(see \033[33m--help-auth\033[0m)
[\033[35midp\033[0m] is a username provided in the http-request-header
defined by \033[33m--idp-h-usr\033[0m and/or \033[33m--idp-hm-usr\033[0m, which is
provided by an authentication middleware such as
authentik, authelia, tailscale, ... (see \033[33m--help-idp\033[0m)
[\033[35midp-h\033[0m] is specifically an \033[33m--idp-h-usr\033[0m header,
[\033[35midp-hm\033[0m] is specifically an \033[33m--idp-hm-usr\033[0m header;
[\033[35midp\033[0m] is the same as [\033[35midp-hm,idp-h\033[0m]
[\033[35mipu\033[0m] is a mapping from an IP-address to a username,
auto-authing that client-IP to that account
(see the description of \033[36m--ipu\033[0m in \033[33m--help\033[0m)
NOTE: even if an option (\033[35mpw\033[0m/\033[35mipu\033[0m/...) is not in the list,
it may still be enabled and can still take effect if
none of the other alternatives identify the user
NOTE: if [\033[35mipu\033[0m] is in the list, it must be FIRST or LAST
NOTE: if [\033[35mpw\033[0m] is not in the list, the logout-button
will be hidden when any idp feature is enabled
"""
),
],
@ -719,7 +828,7 @@ def get_sects():
\033[36mc0\033[35m show all process output (default)
\033[36mc1\033[35m show only stderr
\033[36mc2\033[35m show only stdout
\033[36mc3\033[35m mute all process otput
\033[36mc3\033[35m mute all process output
\033[0m
examples:
@ -762,6 +871,41 @@ def get_sects():
the upload speed can easily drop to 10% for small files)"""
),
],
[
"idp",
"replacing the login system with fancy middleware",
dedent(
"""
if you already have a centralized service which handles
user-authentication for other services already, you can
integrate copyparty with that for automatic login
if the middleware is providing the username in an http-header
named '\033[35mtheUsername\033[0m' then do this: \033[36m--idp-h-usr theUsername\033[0m
if the middleware is providing a list of groups in the header
named '\033[35mtheGroups\033[0m' then do this: \033[36m--idp-h-grp theGroup\033[0m
if the list of groups is separated by '\033[35m%\033[0m' then \033[36m--idp-gsep %\033[0m
if the middleware is providing a header named '\033[35mAccount\033[0m'
and the value is '\033[35malice@forest.net\033[0m' but the username is
actually '\033[35mmarisa\033[0m' then do this for each user:
\033[36m--idp-hm-usr ^Account^alice@forest.net^marisa\033[0m
(the separator '\033[35m^\033[0m' can be any character)
make ABSOLUTELY SURE that the header can only be set by your
middleware and not by clients! and, as an extra precaution,
send a header named '\033[36mfinalmasterspark\033[0m' (a secret keyword)
and then \033[36m--idp-h-key finalmasterspark\033[0m to require that
the login/logout links/buttons can be replaced with links
going to your IdP's UI; \033[36m--idp-login /login/?redir={dst}\033[0m
will expand \033[36m{dst}\033[0m to the URL of the current page, so
the IdP can redirect the user back to where they were
"""
),
],
[
"urlform",
"how to handle url-form POSTs",
@ -1006,6 +1150,7 @@ def add_general(ap, nc, srvname):
ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add volume, \033[33mSRC\033[0m:\033[33mDST\033[0m:\033[33mFLAG\033[0m; examples [\033[32m.::r\033[0m], [\033[32m/mnt/nas/music:/music:r:aed\033[0m], see --help-accounts")
ap2.add_argument("--grp", metavar="G:N,N", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add group, \033[33mNAME\033[0m:\033[33mUSER1\033[0m,\033[33mUSER2\033[0m,\033[33m...\033[0m; example [\033[32madmins:ed,foo,bar\033[0m]")
ap2.add_argument("--usernames", action="store_true", help="require username and password for login; default is just password")
ap2.add_argument("--chdir", metavar="PATH", type=u, help="change working-directory to \033[33mPATH\033[0m before mapping volumes")
ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files (volflag=dots)")
ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,xm", help="how to handle url-form POSTs; see \033[33m--help-urlform\033[0m")
ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="server terminal title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]")
@ -1019,14 +1164,19 @@ def add_general(ap, nc, srvname):
def add_qr(ap, tty):
ap2 = ap.add_argument_group("qr options")
ap2.add_argument("--qr", action="store_true", help="show http:// QR-code on startup")
ap2.add_argument("--qrs", action="store_true", help="show https:// QR-code on startup")
ap2.add_argument("--qr", action="store_true", help="show QR-code on startup")
ap2.add_argument("--qrs", action="store_true", help="change the QR-code URL to https://")
ap2.add_argument("--qrl", metavar="PATH", type=u, default="", help="location to include in the url, for example [\033[32mpriv/?pw=hunter2\033[0m]")
ap2.add_argument("--qri", metavar="PREFIX", type=u, default="", help="select IP which starts with \033[33mPREFIX\033[0m; [\033[32m.\033[0m] to force default IP when mDNS URL would have been used instead")
ap2.add_argument("--qr-fg", metavar="COLOR", type=int, default=0 if tty else 16, help="foreground; try [\033[32m0\033[0m] if the qr-code is unreadable")
ap2.add_argument("--qr-fg", metavar="COLOR", type=int, default=0 if tty else 16, help="foreground; try [\033[32m0\033[0m] or [\033[32m-1\033[0m] if the qr-code is unreadable")
ap2.add_argument("--qr-bg", metavar="COLOR", type=int, default=229, help="background (white=255)")
ap2.add_argument("--qrp", metavar="CELLS", type=int, default=4, help="padding (spec says 4 or more, but 1 is usually fine)")
ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)")
ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr")
ap2.add_argument("--qr-wait", metavar="SEC", type=float, default=0, help="wait \033[33mSEC\033[0m before printing the qr-code to the log")
ap2.add_argument("--qr-every", metavar="SEC", type=float, default=0, help="print the qr-code every \033[33mSEC\033[0m (try this with/without --qr-pin in case of issues)")
ap2.add_argument("--qr-winch", metavar="SEC", type=float, default=0, help="when --qr-pin is enabled, check for terminal size change every \033[33mSEC\033[0m")
ap2.add_argument("--qr-file", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m write qr-code to file.\n └─To create txt or svg, \033[33mTXT\033[0m is Filepath:Zoom:Pad, for example [\033[32mqr.txt:1:2\033[0m]\n └─To create png or gif, \033[33mTXT\033[0m is Filepath:Zoom:Pad:Foreground:Background, for example [\033[32mqr.png:8:2:333333:ffcc55\033[0m], or [\033[32mqr.png:8:2::ffcc55\033[0m] for transparent")
def add_fs(ap):
@ -1043,6 +1193,7 @@ def add_share(ap):
ap2 = ap.add_argument_group("share-url options")
ap2.add_argument("--shr", metavar="DIR", type=u, default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]")
ap2.add_argument("--shr-db", metavar="FILE", type=u, default=db_path, help="database to store shares in")
ap2.add_argument("--shr-who", metavar="TXT", type=u, default="auth", help="who can create a share? [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mauth\033[0m]=authenticated (volflag=shr_who)")
ap2.add_argument("--shr-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to view/delete any share")
ap2.add_argument("--shr-rt", metavar="MIN", type=int, default=1440, help="shares can be revived by their owner if they expired less than MIN minutes ago; [\033[32m60\033[0m]=hour, [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week")
ap2.add_argument("--shr-v", action="store_true", help="debug")
@ -1056,6 +1207,7 @@ def add_upload(ap):
ap2.add_argument("--put-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for PUT/WebDAV uploads: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=put_ck)")
ap2.add_argument("--bup-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for bup/basic-uploader: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=bup_ck)")
ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled, default=12h")
ap2.add_argument("--unp-who", metavar="NUM", type=int, default=1, help="clients can undo recent uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=unp_who)")
ap2.add_argument("--u2abort", metavar="NUM", type=int, default=1, help="clients can abort incomplete uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=u2abort)")
ap2.add_argument("--blank-wt", metavar="SEC", type=int, default=300, help="file write grace period (any client can write to a blank file last-modified more recently than \033[33mSEC\033[0m seconds ago)")
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")
@ -1149,7 +1301,8 @@ def add_auth(ap):
idp_db = os.path.join(E.cfg, "idp.db")
ses_db = os.path.join(E.cfg, "sessions.db")
ap2 = ap.add_argument_group("IdP / identity provider / user authentication options")
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, default="", help="bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
ap2.add_argument("--idp-hm-usr", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mT\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp")
ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control")
ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present")
ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m")
@ -1157,12 +1310,24 @@ def add_auth(ap):
ap2.add_argument("--idp-store", metavar="N", type=int, default=1, help="how to use \033[33m--idp-db\033[0m; [\033[32m0\033[0m] = entirely disable, [\033[32m1\033[0m] = write-only (effectively disabled), [\033[32m2\033[0m] = remember users, [\033[32m3\033[0m] = remember users and groups.\nNOTE: Will remember and restore the IdP-volumes of all users for all eternity if set to 2 or 3, even when user is deleted from your IdP")
ap2.add_argument("--idp-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to use /?idp (the cache management UI)")
ap2.add_argument("--idp-cookie", metavar="S", type=int, default=0, help="generate a session-token for IdP users which is written to cookie \033[33mcppws\033[0m (or \033[33mcppwd\033[0m if plaintext), to reduce the load on the IdP server, lifetime \033[33mS\033[0m seconds.\n └─note: The expiration time is a client hint only; the actual lifetime of the session-token is infinite (until next restart with \033[33m--ses-db\033[0m wiped)")
ap2.add_argument("--idp-login", metavar="L", type=u, default="", help="replace all login-buttons with a link to URL \033[33mL\033[0m (unless \033[32mpw\033[0m is in \033[33m--auth-ord\033[0m then both will be shown); [\033[32m{dst}\033[0m] expands to url of current page")
ap2.add_argument("--idp-login-t", metavar="T", type=u, default="Login with SSO", help="the label/text for the idp-login button")
ap2.add_argument("--idp-logout", metavar="L", type=u, default="", help="replace all logout-buttons with a link to URL \033[33mL\033[0m")
ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see --help-auth-ord")
ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app")
ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins")
ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)")
ap2.add_argument("--ses-len", metavar="CHARS", type=int, default=20, help="session key length; default is 120 bits ((20//4)*4*6)")
ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies")
ap2.add_argument("--grp-all", metavar="NAME", type=u, default="acct", help="the name of the auto-generated group which contains every username which is known")
ap2.add_argument("--ipu", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m users with IP matching \033[33mCIDR\033[0m are auto-authenticated as username \033[33mUSR\033[0m; example: [\033[32m172.16.24.0/24=dave]")
ap2.add_argument("--ipr", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m username \033[33mUSR\033[0m can only connect from an IP matching one or more \033[33mCIDR\033[0m (comma-sep.); example: [\033[32m192.168.123.0/24,172.16.0.0/16=dave]")
ap2.add_argument("--have-idp-hdrs", type=u, default="", help=argparse.SUPPRESS)
ap2.add_argument("--have-ipu-or-ipr", type=u, default="", help=argparse.SUPPRESS)
ap2.add_argument("--ao-idp-before-pw", type=u, default="", help=argparse.SUPPRESS)
ap2.add_argument("--ao-h-before-hm", type=u, default="", help=argparse.SUPPRESS)
ap2.add_argument("--ao-ipu-wins", type=u, default="", help=argparse.SUPPRESS)
ap2.add_argument("--ao-have-pw", type=u, default="", help=argparse.SUPPRESS)
def add_chpw(ap):
@ -1201,6 +1366,7 @@ def add_zc_mdns(ap):
ap2.add_argument("--zm-lh", metavar="PATH", type=u, default="", help="link a specific folder for http shares")
ap2.add_argument("--zm-lf", metavar="PATH", type=u, default="", help="link a specific folder for ftp shares")
ap2.add_argument("--zm-ls", metavar="PATH", type=u, default="", help="link a specific folder for smb shares")
ap2.add_argument("--zm-fqdn", metavar="FQDN", type=u, default="--name.local", help="the domain to announce; NOTE: using anything other than .local is nonstandard and could cause problems")
ap2.add_argument("--zm-mnic", action="store_true", help="merge NICs which share subnets; assume that same subnet means same network")
ap2.add_argument("--zm-msub", action="store_true", help="merge subnets on each NIC -- always enabled for ipv6 -- reduces network load, but gnome-gvfs clients may stop working, and clients cannot be in subnets that the server is not")
ap2.add_argument("--zm-noneg", action="store_true", help="disable NSEC replies -- try this if some clients don't see copyparty")
@ -1307,6 +1473,7 @@ def add_yolo(ap):
ap2.add_argument("--no-fnugg", action="store_true", help="disable the smoketest for caching-related issues in the web-UI")
ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET")
ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)")
ap2.add_argument("--unsafe-state", action="store_true", help="when one of the emergency fallback locations are used for runtime state ($TMPDIR, /tmp), certain features will be force-disabled for security reasons by default. This option overrides that safeguard and allows unsafe storage of secrets")
def add_optouts(ap):
@ -1317,9 +1484,10 @@ def add_optouts(ap):
ap2.add_argument("--no-del", action="store_true", help="disable delete operations")
ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations")
ap2.add_argument("--no-cp", action="store_true", help="disable copy operations")
ap2.add_argument("--no-fs-abrt", action="store_true", help="disable ability to abort ongoing copy/move")
ap2.add_argument("-nth", action="store_true", help="no title hostname; don't show \033[33m--name\033[0m in <title>")
ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI")
ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI")
ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI. This is the same as --du-who no")
ap2.add_argument("-nb", action="store_true", help="no powered-by-copyparty branding in UI")
ap2.add_argument("--zipmaxn", metavar="N", type=u, default="0", help="reject download-as-zip if more than \033[33mN\033[0m files in total; optionally takes a unit suffix: [\033[32m256\033[0m], [\033[32m9K\033[0m], [\033[32m4G\033[0m] (volflag=zipmaxn)")
ap2.add_argument("--zipmaxs", metavar="SZ", type=u, default="0", help="reject download-as-zip if total download size exceeds \033[33mSZ\033[0m bytes; optionally takes a unit suffix: [\033[32m256M\033[0m], [\033[32m4G\033[0m], [\033[32m2T\033[0m] (volflag=zipmaxs)")
@ -1338,7 +1506,7 @@ def add_optouts(ap):
def add_safety(ap):
ap2 = ap.add_argument_group("safety options")
ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js")
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --vague-403 -nih")
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav requires login, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --dav-auth --vague-403 -nih")
ap2.add_argument("-sss", action="store_true", help="further increase safety: Enable logging to disk, scan for dangerous symlinks.\n └─Alias of\033[32m -ss --no-dav --no-logues --no-readme -lo=cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz --ls=**,*,ln,p,r")
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, default="", help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m (see \033[33m--help-ls\033[0m); example [\033[32m**,*,ln,p,r\033[0m]")
ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)")
@ -1360,6 +1528,8 @@ def add_safety(ap):
ap2.add_argument("--sus-urls", metavar="R", type=u, default=r"\.php$|(^|/)wp-(admin|content|includes)/", help="URLs which are considered sus / eligible for banning; disable with blank or [\033[32mno\033[0m]")
ap2.add_argument("--nonsus-urls", metavar="R", type=u, default=r"^(favicon\.ico|robots\.txt)$|^apple-touch-icon|^\.well-known", help="harmless URLs ignored from 404-bans; disable with blank or [\033[32mno\033[0m]")
ap2.add_argument("--early-ban", action="store_true", help="if a client is banned, reject its connection as soon as possible; not a good idea to enable when proxied behind cloudflare since it could ban your reverse-proxy")
ap2.add_argument("--cookie-nmax", metavar="N", type=int, default=50, help="reject HTTP-request from client if they send more than N cookies")
ap2.add_argument("--cookie-cmax", metavar="N", type=int, default=8192, help="reject HTTP-request from client if more than N characters in Cookie header")
ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for \033[33mMIN\033[0m minutes (and also kill its active connections) -- disable with 0")
ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]")
ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives")
@ -1390,7 +1560,7 @@ def add_shutdown(ap):
def add_logging(ap):
ap2 = ap.add_argument_group("logging options")
ap2.add_argument("-q", action="store_true", help="quiet; disable most STDOUT messages")
ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile, example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)")
ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile; use .txt for plaintext or .xz for compressed. Example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)")
ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR")
ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR")
ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster")
@ -1398,6 +1568,7 @@ def add_logging(ap):
ap2.add_argument("--log-utc", action="store_true", help="do not use local timezone; assume the TZ env-var is UTC (tiny bit faster)")
ap2.add_argument("--log-tdec", metavar="N", type=int, default=3, help="timestamp resolution / number of timestamp decimals")
ap2.add_argument("--log-badpwd", metavar="N", type=int, default=2, help="log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed")
ap2.add_argument("--log-badxml", action="store_true", help="log any invalid XML received from a client")
ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs")
ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling")
ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
@ -1426,11 +1597,12 @@ def add_thumbnail(ap):
ap2.add_argument("--no-athumb", action="store_true", help="disable audio thumbnails (spectrograms) (volflag=dathumb)")
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)")
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails")
ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="conversion timeout in seconds (volflag=convt)")
ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="convert-to-image timeout in seconds (volflag=convt)")
ap2.add_argument("--ac-convt", metavar="SEC", type=float, default=150.0, help="convert-to-audio timeout in seconds (volflag=aconvt)")
ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=th_ram, help="max memory usage (GiB) permitted by thumbnailer; not very accurate")
ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32my\033[0m]=crop, [\033[32mn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)")
ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)")
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference")
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,raw,ff", help="image decoders, in order of preference")
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg output for video thumbs (avoids issues on some FFmpeg builds)")
@ -1439,16 +1611,19 @@ def add_thumbnail(ap):
ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled")
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than \033[33m--th-poke\033[0m seconds will get deleted every \033[33m--th-clean\033[0m seconds")
ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern")
ap2.add_argument("--th-spec-p", metavar="N", type=u, default=1, help="for music, do spectrograms or embedded coverart? [\033[32m0\033[0m]=only-art, [\033[32m1\033[0m]=prefer-art, [\033[32m2\033[0m]=only-spec")
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
# https://github.com/libvips/libvips
# https://stackoverflow.com/a/47612661
# ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:'
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow")
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips")
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow")
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips")
ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mrw,nef,orf,pef,raf,raw,sr2,srf,x3f", help="image formats to decode using rawpy")
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)")
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz", help="audio/image formats to decompress before passing to ffmpeg")
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz, epub=jpg.epub", help="audio/image formats to decompress before passing to ffmpeg")
def add_transcoding(ap):
@ -1493,8 +1668,8 @@ def add_db_general(ap, hcores):
ap2.add_argument("-e2vp", action="store_true", help="on hash mismatch: panic and quit copyparty")
ap2.add_argument("--hist", metavar="PATH", type=u, default="", help="where to store volume data (db, thumbs); default is a folder named \".hist\" inside each volume (volflag=hist)")
ap2.add_argument("--dbpath", metavar="PATH", type=u, default="", help="override where the volume databases are to be placed; default is the same as \033[33m--hist\033[0m (volflag=dbpath)")
ap2.add_argument("--no-hash", metavar="PTN", type=u, default="", help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (volflag=nohash)")
ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scans (volflag=noidx)")
ap2.add_argument("--no-hash", metavar="PTN", type=u, default="", help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (must be specified as one big regex, not multiple times) (volflag=nohash)")
ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scan (must be specified as one big regex, not multiple times) (volflag=noidx)")
ap2.add_argument("--no-dirsz", action="store_true", help="do not show total recursive size of folders in listings, show inode size instead; slightly faster (volflag=nodirsz)")
ap2.add_argument("--re-dirsz", action="store_true", help="if the directory-sizes in the UI are bonkers, use this along with \033[33m-e2dsa\033[0m to rebuild the index from scratch")
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")
@ -1531,6 +1706,7 @@ def add_db_metadata(ap):
def add_txt(ap):
ap2 = ap.add_argument_group("textfile options")
ap2.add_argument("--md-no-br", action="store_true", help="markdown: disable newline-is-newline; will only render a newline into the html given two trailing spaces or a double-newline (volflag=md_no_br)")
ap2.add_argument("--md-hist", metavar="TXT", type=u, default="s", help="where to store old version of markdown files; [\033[32ms\033[0m]=subfolder, [\033[32mv\033[0m]=volume-histpath, [\033[32mn\033[0m]=nope/disabled (volflag=md_hist)")
ap2.add_argument("--txt-eol", metavar="TYPE", type=u, default="", help="enable EOL conversion when writing documents; supported: CRLF, LF (volflag=txt_eol)")
ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="the textfile editor will check for serverside changes every \033[33mSEC\033[0m seconds")
@ -1560,13 +1736,14 @@ def add_og(ap):
def add_ui(ap, retry):
THEMES = 10
ap2 = ap.add_argument_group("ui options")
ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)")
ap2.add_argument("--localtime", action="store_true", help="default to local timezone instead of UTC")
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language; one of the following: \033[32meng nor chi\033[0m")
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..7)")
ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed")
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language, for example \033[32meng\033[0m / \033[32mnor\033[0m / ...")
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..%d)" % (THEMES - 1,))
ap2.add_argument("--themes", metavar="NUM", type=int, default=THEMES, help="number of themes installed")
ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent")
ap2.add_argument("--sort", metavar="C,C,C", type=u, default="href", help="default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: \033[32mhref -href ext sz ts tags/Album tags/.tn\033[0m (volflag=sort)")
ap2.add_argument("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)")
@ -1588,7 +1765,11 @@ def add_ui(ap, retry):
ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-nb\033[0m")
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)")
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m). This is the same as --ver-who all")
ap2.add_argument("--ver-who", metavar="TXT", type=u, default="no", help="only show version for: [\033[32ma\033[0m]=admin-permission-anywhere, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone")
ap2.add_argument("--du-who", metavar="TXT", type=u, default="all", help="only show disk usage for: [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mrw\033[0m]=read-write, [\033[32mw\033[0m]=write, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone (volflag=du_who)")
ap2.add_argument("--ver-iwho", type=int, default=0, help=argparse.SUPPRESS)
ap2.add_argument("--du-iwho", type=int, default=0, help=argparse.SUPPRESS)
ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
ap2.add_argument("--ctl-re", metavar="SEC", type=int, default=1, help="the controlpanel Refresh-button will autorefresh every SEC; [\033[32m0\033[0m] = just once")
@ -1767,16 +1948,7 @@ def main(argv: Optional[list[str]] = None) -> None:
ensure_webdeps()
for k, v in zip(argv[1:], argv[2:]):
if k == "-c" and os.path.isfile(v):
supp = args_from_cfg(v)
argv.extend(supp)
for k in argv[1:]:
v = k[2:]
if k.startswith("-c") and v and os.path.isfile(v):
supp = args_from_cfg(v)
argv.extend(supp)
argv = expand_cfg(argv)
deprecated: list[tuple[str, str]] = [
("--salt", "--warksalt"),
@ -1845,6 +2017,9 @@ def main(argv: Optional[list[str]] = None) -> None:
except:
sys.exit(1)
if al.chdir:
os.chdir(al.chdir)
if al.ansi:
al.no_ansi = False
elif not al.no_ansi:
@ -1872,7 +2047,7 @@ def main(argv: Optional[list[str]] = None) -> None:
if not HAVE_IPV6 and al.i == "::":
al.i = "0.0.0.0"
al.i = al.i.split(",")
al.i = [x.strip() for x in al.i.split(",")]
try:
if "-" in al.p:
lo, hi = [int(x) for x in al.p.split("-")]

View file

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

View file

@ -431,6 +431,8 @@ class VFS(object):
self.get_dbv = self._get_dbv
self.ls = self._ls
self.canonical = self._canonical
self.dcanonical = self._dcanonical
def __repr__(self) -> str:
return "VFS(%s)" % (
@ -624,7 +626,7 @@ class VFS(object):
vrem = vjoin(self.vpath[len(dbv.vpath) :].lstrip("/"), vrem)
return dbv, vrem
def canonical(self, rem: str, resolve: bool = True) -> str:
def _canonical(self, rem: str, resolve: bool = True) -> str:
"""returns the canonical path (fully-resolved absolute fs path)"""
ap = self.realpath
if rem:
@ -632,7 +634,7 @@ class VFS(object):
return absreal(ap) if resolve else ap
def dcanonical(self, rem: str) -> str:
def _dcanonical(self, rem: str) -> str:
"""resolves until the final component (filename)"""
ap = self.realpath
if rem:
@ -641,6 +643,44 @@ class VFS(object):
ad, fn = os.path.split(ap)
return os.path.join(absreal(ad), fn)
def _canonical_shr(self, rem: str, resolve: bool = True) -> str:
"""returns the canonical path (fully-resolved absolute fs path)"""
ap = self.realpath
if rem:
ap += "/" + rem
rap = absreal(ap)
if self.shr_files:
assert self.shr_src # !rm
vn, rem = self.shr_src
chk = absreal(os.path.join(vn.realpath, rem))
if chk != rap:
# not the dir itself; assert file allowed
ad, fn = os.path.split(rap)
if chk != ad or fn not in self.shr_files:
return "\n\n"
return rap if resolve else ap
def _dcanonical_shr(self, rem: str) -> str:
"""resolves until the final component (filename)"""
ap = self.realpath
if rem:
ap += "/" + rem
ad, fn = os.path.split(ap)
ad = absreal(ad)
if self.shr_files:
assert self.shr_src # !rm
vn, rem = self.shr_src
chk = absreal(os.path.join(vn.realpath, rem))
if chk != absreal(ap):
# not the dir itself; assert file allowed
if ad != chk or fn not in self.shr_files:
return "\n\n"
return os.path.join(ad, fn)
def _ls_nope(
self, *a, **ka
) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]:
@ -881,6 +921,15 @@ class VFS(object):
return None
if "xvol" in self.flags:
self_ap = self.realpath + os.sep
if aps.startswith(self_ap):
vp = aps[len(self_ap) :]
if ANYWIN:
vp = vp.replace(os.sep, "/")
vn2, _ = self._find(vp)
if self == vn2:
return self
all_aps = self.shr_all_aps or self.root.all_aps
for vap, vns in all_aps:
@ -967,6 +1016,14 @@ class AuthSrv(object):
self.indent = ""
self.is_lxc = args.c == ["/z/initcfg"]
self._vf0b = {
"tcolor": self.args.tcolor,
"du_iwho": self.args.du_iwho,
"shr_who": self.args.shr_who if self.args.shr else "no",
}
self._vf0 = self._vf0b.copy()
self._vf0["d2d"] = True
# fwd-decl
self.vfs = VFS(log_func, "", "", "", AXS(), {})
self.acct: dict[str, str] = {} # uname->pw
@ -1005,7 +1062,10 @@ class AuthSrv(object):
yield prev, True
def vf0(self):
return {"d2d": True, "tcolor": self.args.tcolor}
return self._vf0.copy()
def vf0b(self):
return self._vf0b.copy()
def idp_checkin(
self, broker: Optional["BrokerCli"], uname: str, gname: str
@ -1099,6 +1159,9 @@ class AuthSrv(object):
if rejected:
continue
if gn == self.args.grp_all:
gn = ""
# if ap/vp has a user/group placeholder, make sure to keep
# track so the same user/group is mapped when setting perms;
# otherwise clear un/gn to indicate it's a regular volume
@ -1208,6 +1271,7 @@ class AuthSrv(object):
self.load_idp_db(bool(self.idp_accs))
ret = {un: gns[:] for un, gns in self.idp_accs.items()}
ret.update({zs: [""] for zs in acct if zs not in ret})
grps[self.args.grp_all] = list(ret.keys())
for gn, uns in grps.items():
for un in uns:
try:
@ -1315,6 +1379,10 @@ class AuthSrv(object):
zt = split_cfg_ln(ln)
for zs, za in zt.items():
zs = zs.lstrip("-")
if "=" in zs:
t = "WARNING: found an option named [%s] in your [global] config; did you mean to say [%s: %s] instead?"
zs1, zs2 = zs.split("=", 1)
self.log(t % (zs, zs1, zs2), 3)
if za is True:
self._e("└─argument [{}]".format(zs))
else:
@ -1324,6 +1392,10 @@ class AuthSrv(object):
if cat == cata:
try:
u, p = [zs.strip() for zs in ln.split(":", 1)]
if "=" in u and not p:
t = "WARNING: found username [%s] in your [accounts] config; did you mean to say [%s: %s] instead?"
zs1, zs2 = u.split("=", 1)
self.log(t % (u, zs1, zs2), 3)
self._l(ln, 5, "account [{}], password [{}]".format(u, p))
acct[u] = p
except:
@ -1394,6 +1466,10 @@ class AuthSrv(object):
zd = split_cfg_ln(ln)
fstr = ""
for sk, sv in zd.items():
if "=" in sk:
t = "WARNING: found a volflag named [%s] in your config; did you mean to say [%s: %s] instead?"
zs1, zs2 = sk.split("=", 1)
self.log(t % (sk, zs1, zs2), 3)
bad = re.sub(r"[a-z0-9_-]", "", sk).lstrip("-")
if bad:
err = "bad characters [{}] in volflag name [{}]; "
@ -1634,6 +1710,7 @@ class AuthSrv(object):
# accept both , and : as separators between usernames
zs1, zs2 = x.replace("=", ":").split(":", 1)
grps[zs1] = zs2.replace(":", ",").split(",")
grps[zs1] = [x.strip() for x in grps[zs1]]
except:
t = '\n invalid value "{}" for argument --grp, must be groupname:username1,username2,...'
raise Exception(t.format(x))
@ -1685,6 +1762,10 @@ class AuthSrv(object):
self.log("\n{0}\n{1}{0}".format(t, "\n".join(slns)))
raise
self.args.have_idp_hdrs = bool(self.args.idp_h_usr or self.args.idp_hm_usr)
self.args.have_ipu_or_ipr = bool(self.args.ipu or self.args.ipr)
self.setup_auth_ord()
self.setup_pwhash(acct)
defpw = acct.copy()
self.setup_chpw(acct)
@ -1697,7 +1778,7 @@ class AuthSrv(object):
mount = cased
if not mount and not self.args.idp_h_usr:
if not mount and not self.args.have_idp_hdrs:
# -h says our defaults are CWD at root and read/write for everyone
axs = AXS(["*"], ["*"], None, None)
ehint = ""
@ -1721,12 +1802,15 @@ class AuthSrv(object):
files = os.listdir(E.cfg)
except:
files = []
hits = [x for x in files if x.lower().endswith(".conf")]
hits = [
x
for x in files
if x.lower().endswith(".conf") and not x.startswith(".")
]
if hits:
t = "Hint: Found some config files in [%s], but these were not automatically loaded because they are in the wrong place%s %s\n"
self.log(t % (E.cfg, ehint, ", ".join(hits)), 3)
zvf = {"tcolor": self.args.tcolor}
vfs = VFS(self.log_func, absreal("."), "", "", axs, zvf)
vfs = VFS(self.log_func, absreal("."), "", "", axs, self.vf0b())
if not axs.uread:
self.badcfg1 = True
elif "" not in mount:
@ -1870,7 +1954,7 @@ class AuthSrv(object):
if missing_users:
zs = ", ".join(k for k in sorted(missing_users))
if self.args.idp_h_usr:
if self.args.have_idp_hdrs:
t = "the following users are unknown, and assumed to come from IdP: "
self.log(t + zs, c=6)
else:
@ -1881,6 +1965,16 @@ class AuthSrv(object):
if LEELOO_DALLAS in all_users:
raise Exception("sorry, reserved username: " + LEELOO_DALLAS)
zsl = []
for usr in list(acct)[:]:
zs = acct[usr].strip()
if not zs:
zs = ub64enc(os.urandom(48)).decode("ascii")
zsl.append(usr)
acct[usr] = zs
if zsl:
self.log("generated random passwords for users %r" % (zsl,), 6)
seenpwds = {}
for usr, pwd in acct.items():
if pwd in seenpwds:
@ -2201,12 +2295,12 @@ class AuthSrv(object):
if vf not in vol.flags:
vol.flags[vf] = getattr(self.args, ga)
zs = "forget_ip gid nrand tail_who u2abort u2ow uid ups_who zip_who"
zs = "forget_ip gid nrand tail_who th_spec_p u2abort u2ow uid unp_who ups_who zip_who"
for k in zs.split():
if k in vol.flags:
vol.flags[k] = int(vol.flags[k])
zs = "convt tail_fd tail_rate tail_tmax"
zs = "aconvt convt tail_fd tail_rate tail_tmax"
for k in zs.split():
if k in vol.flags:
vol.flags[k] = float(vol.flags[k])
@ -2250,6 +2344,11 @@ class AuthSrv(object):
vol.lim.uid = vol.flags["uid"]
vol.lim.gid = vol.flags["gid"]
vol.flags["du_iwho"] = n_du_who(vol.flags["du_who"])
if not enshare:
vol.flags["shr_who"] = "no"
if vol.flags.get("og"):
self.args.uqe = True
@ -2537,7 +2636,7 @@ class AuthSrv(object):
if not self.args.no_voldump:
self.log(t)
if have_e2d or self.args.idp_h_usr:
if have_e2d or self.args.have_idp_hdrs:
t = self.chk_sqlite_threadsafe()
if t:
self.log("\n\033[{}\033[0m\n".format(t))
@ -2692,6 +2791,8 @@ class AuthSrv(object):
shn.shr_files = set(fns)
shn.ls = shn._ls_shr
shn.canonical = shn._canonical_shr
shn.dcanonical = shn._dcanonical_shr
else:
shn.ls = shn._ls
@ -2756,6 +2857,7 @@ class AuthSrv(object):
"dcrop": vf["crop"],
"dth3x": vf["th3x"],
"u2ts": vf["u2ts"],
"shr_who": vf["shr_who"],
"frand": bool(vf.get("rand")),
"lifetime": vf.get("lifetime") or 0,
"unlist": vf.get("unlist") or "",
@ -2764,16 +2866,19 @@ class AuthSrv(object):
js_htm = {
"SPINNER": self.args.spinner,
"s_name": self.args.bname,
"idp_login": self.args.idp_login,
"have_up2k_idx": "e2d" in vf,
"have_acode": not self.args.no_acode,
"have_c2flac": self.args.allow_flac,
"have_c2wav": self.args.allow_wav,
"have_shr": self.args.shr,
"shr_who": vf["shr_who"],
"have_zip": not self.args.no_zip,
"have_mv": not self.args.no_mv,
"have_del": not self.args.no_del,
"have_unpost": int(self.args.unpost),
"have_emp": self.args.emp,
"have_emp": int(self.args.emp),
"md_no_br": int(vf.get("md_no_br") or 0),
"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 "",
@ -2824,10 +2929,22 @@ class AuthSrv(object):
zs = str(vol.flags.get("tcolor") or self.args.tcolor)
vol.flags["tcolor"] = zs.lstrip("#")
def setup_auth_ord(self) -> None:
ao = [x.strip() for x in self.args.auth_ord.split(",")]
if "idp" in ao:
zi = ao.index("idp")
ao = ao[:zi] + ["idp-hm", "idp-h"] + ao[zi:]
zsl = "pw idp-h idp-hm ipu".split()
pw, h, hm, ipu = [ao.index(x) if x in ao else 99 for x in zsl]
self.args.ao_idp_before_pw = min(h, hm) < pw
self.args.ao_h_before_hm = h < hm
self.args.ao_ipu_wins = ipu == 0
self.args.ao_have_pw = pw < 99 or not self.args.have_idp_hdrs
def load_idp_db(self, quiet=False) -> None:
# mutex me
level = self.args.idp_store
if level < 2 or not self.args.idp_h_usr:
if level < 2 or not self.args.have_idp_hdrs:
return
assert sqlite3 # type: ignore # !rm
@ -2884,7 +3001,7 @@ class AuthSrv(object):
n = []
q = "insert into us values (?,?,?)"
accs = list(self.acct)
if self.args.idp_h_usr and self.args.idp_cookie:
if self.args.have_idp_hdrs and self.args.idp_cookie:
accs.extend(self.idp_accs.keys())
for uname in accs:
if uname not in ases:
@ -3416,6 +3533,30 @@ class AuthSrv(object):
self.log("generated config:\n\n" + "\n".join(ret))
def n_du_who(s: str) -> int:
if s == "all":
return 9
if s == "auth":
return 7
if s == "w":
return 5
if s == "rw":
return 4
if s == "a":
return 3
return 0
def n_ver_who(s: str) -> int:
if s == "all":
return 9
if s == "auth":
return 6
if s == "a":
return 3
return 0
def split_cfg_ln(ln: str) -> dict[str, Any]:
# "a, b, c: 3" => {a:true, b:true, c:3}
ret = {}
@ -3448,7 +3589,9 @@ def expand_config_file(
if os.path.isdir(fp):
names = list(sorted(os.listdir(fp)))
cnames = [x for x in names if x.lower().endswith(".conf")]
cnames = [
x for x in names if x.lower().endswith(".conf") and not x.startswith(".")
]
if not cnames:
t = "warning: tried to read config-files from folder '%s' but it does not contain any "
if names:

View file

@ -2,18 +2,22 @@
from __future__ import print_function, unicode_literals
import os
import time
from ..util import SYMTIME, fsdec, fsenc
from . import path as path
if True: # pylint: disable=using-constant-test
from typing import Any, Optional
from typing import Any, Optional, Union
from ..util import NamedLogger
MKD_755 = {"chmod_d": 0o755}
MKD_700 = {"chmod_d": 0o700}
UTIME_CLAMPS = ((max, -2147483647), (max, 1), (min, 4294967294), (min, 2147483646))
_ = (path, MKD_755, MKD_700)
__all__ = ["path", "MKD_755", "MKD_700"]
_ = (path, MKD_755, MKD_700, UTIME_CLAMPS)
__all__ = ["path", "MKD_755", "MKD_700", "UTIME_CLAMPS"]
# grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c
# printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')"
@ -99,6 +103,40 @@ def utime(
return os.utime(fsenc(p), times)
def utime_c(
log: Union["NamedLogger", Any], p: str, ts: int, follow_symlinks: bool = True, throw: bool = False
) -> Optional[int]:
clamp = 0
ov = ts
bp = fsenc(p)
now = int(time.time())
while True:
try:
if SYMTIME:
os.utime(bp, (now, ts), follow_symlinks=follow_symlinks)
else:
os.utime(bp, (now, ts))
if clamp:
t = "filesystem rejected utime(%r); clamped %s to %s"
log(t % (p, ov, ts))
return ts
except Exception as ex:
pv = ts
while clamp < len(UTIME_CLAMPS):
fun, cv = UTIME_CLAMPS[clamp]
ts = fun(ts, cv)
clamp += 1
if ts != pv:
break
if clamp >= len(UTIME_CLAMPS):
if throw:
raise
else:
t = "could not utime(%r) to %s; %s, %r"
log(t % (p, ov, ex, ex))
return None
if hasattr(os, "lstat"):
def lstat(p: str) -> os.stat_result:

View file

@ -2,7 +2,6 @@
from __future__ import print_function, unicode_literals
import argparse
import traceback
from queue import Queue

View file

@ -130,6 +130,7 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
nlog: "NamedLogger" = lambda msg, c=0: log("cert-gen-srv", msg, c)
names = args.crt_ns.split(",") if args.crt_ns else []
names = [x.strip() for x in names]
if not args.crt_exact:
for n in names[:]:
names.append("*.{}".format(n))

View file

@ -44,6 +44,7 @@ def vf_bmap() -> dict[str, str]:
"gsel",
"hardlink",
"magic",
"md_no_br",
"no_db_ip",
"no_sb_md",
"no_sb_lg",
@ -68,6 +69,7 @@ def vf_bmap() -> dict[str, str]:
def vf_vmap() -> dict[str, str]:
"""argv-to-volflag: simple values"""
ret = {
"ac_convt": "aconvt",
"no_hash": "nohash",
"no_idx": "noidx",
"re_maxage": "scan",
@ -82,6 +84,7 @@ def vf_vmap() -> dict[str, str]:
"chmod_d",
"chmod_f",
"dbd",
"du_who",
"forget_ip",
"hsortn",
"html_head",
@ -105,18 +108,21 @@ def vf_vmap() -> dict[str, str]:
"put_name",
"mv_retry",
"rm_retry",
"shr_who",
"sort",
"tail_fd",
"tail_rate",
"tail_tmax",
"tail_who",
"tcolor",
"th_spec_p",
"txt_eol",
"unlist",
"u2abort",
"u2ts",
"uid",
"gid",
"unp_who",
"ups_who",
"zip_who",
"zipmaxn",
@ -260,7 +266,9 @@ flagcats = {
"thsize": "thumbnail res; WxH",
"crop": "center-cropping (y/n/fy/fn)",
"th3x": "3x resolution (y/n/fy/fn)",
"convt": "conversion timeout in seconds",
"convt": "convert-to-image timeout in seconds",
"aconvt": "convert-to-audio timeout in seconds",
"th_spec_p=1": "make spectrograms? 0=never 1=fallback 2=always",
"ext_th=s=/b.png": "use /b.png as thumbnail for file-extension s",
},
"handlers\n(better explained in --help-handlers)": {
@ -290,6 +298,7 @@ flagcats = {
"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",
"du_who=all": "show disk-usage info to everyone",
"robots": "allows indexing by search engines (default)",
"norobots": "kindly asks search engines to leave",
"unlistcr": "don't list read-access in controlpanel",
@ -319,6 +328,7 @@ flagcats = {
"og_ua": "if defined: only send OG html if useragent matches this regex",
},
"textfiles": {
"md_no_br": "newline only on double-newline or two tailing spaces",
"md_hist": "where to put markdown backups; s=subfolder, v=volHist, n=nope",
"exp": "enable textfile expansion; see --help-exp",
"exp_md": "placeholders to expand in markdown files; see --help",
@ -341,6 +351,8 @@ flagcats = {
"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)",
"rmagic": "expensive analysis for mimetype accuracy",
"shr_who=auth": "who can create shares? no/auth/a",
"unp_who=2": "unpost only if same... 1=ip+name, 2=ip, 3=name",
"ups_who=2": "restrict viewing the list of recent uploads",
"zip_who=2": "restrict access to download-as-zip/tar",
"zipmaxn=9k": "reject download-as-zip if more than 9000 files",

View file

@ -65,6 +65,9 @@ DXMLParser = _DXMLParser
def parse_xml(txt: str) -> ET.Element:
"""
Parse XML into an xml.etree.ElementTree.Element while defusing some unsafe parts.
"""
parser = DXMLParser()
parser.feed(txt)
return parser.close() # type: ignore

View file

@ -68,13 +68,13 @@ class FtpAuth(DummyAuthorizer):
if ip.startswith("::ffff:"):
ip = ip[7:]
ip = ipnorm(ip)
ipn = ipnorm(ip)
bans = self.hub.bans
if ip in bans:
rt = bans[ip] - time.time()
if ipn in bans:
rt = bans[ipn] - time.time()
if rt < 0:
logging.info("client unbanned")
del bans[ip]
del bans[ipn]
else:
raise AuthenticationFailed("banned")
@ -96,6 +96,10 @@ class FtpAuth(DummyAuthorizer):
if args.ipu and uname == "*":
uname = args.ipu_iu[args.ipu_nm.map(ip)]
if args.ipr and uname in args.ipr_u:
if not args.ipr_u[uname].map(ip):
logging.warning("username [%s] rejected by --ipr", uname)
uname = "*"
if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):
g = self.hub.gpwd
@ -148,10 +152,6 @@ class FtpFs(AbstractedFS):
self.cwd = "/" # pyftpdlib convention of leading slash
self.root = "/var/lib/empty"
self.can_read = self.can_write = self.can_move = False
self.can_delete = self.can_get = self.can_upget = False
self.can_admin = self.can_dot = False
self.listdirinfo = self.listdir
self.chdir(".")
@ -214,7 +214,7 @@ class FtpFs(AbstractedFS):
m: bool = False,
d: bool = False,
) -> tuple[str, VFS, str]:
return self.v2a(os.path.join(self.cwd, vpath), r, w, m, d)
return self.v2a(join(self.cwd, vpath), r, w, m, d)
def ftp2fs(self, ftppath: str) -> str:
# return self.v2a(ftppath)
@ -285,21 +285,14 @@ class FtpFs(AbstractedFS):
# returning 550 is library-default and suitable
raise FSE("No such file or directory")
if vfs.realpath:
avfs = vfs.chk_ap(ap, st)
if not avfs:
raise FSE("Permission denied", 1)
else:
avfs = vfs
self.cwd = nwd
(
self.can_read,
self.can_write,
self.can_move,
self.can_delete,
self.can_get,
self.can_upget,
self.can_admin,
self.can_dot,
) = avfs.can_access("", self.h.uname)
def mkdir(self, path: str) -> None:
ap, vfs, _ = self.rv2a(path, w=True)
@ -322,7 +315,7 @@ class FtpFs(AbstractedFS):
vfs_ls = [x[0] for x in vfs_ls1]
vfs_ls.extend(vfs_virt.keys())
if not self.can_dot:
if self.uname not in vfs.axs.udot:
vfs_ls = exclude_dotfiles(vfs_ls)
vfs_ls.sort()
@ -370,16 +363,13 @@ class FtpFs(AbstractedFS):
raise FSE(str(ex))
def rename(self, src: str, dst: str) -> None:
if not self.can_move:
raise FSE("Not allowed for user " + self.h.uname)
if self.args.no_mv:
raise FSE("The rename/move feature is disabled in server config")
svp = join(self.cwd, src).lstrip("/")
dvp = join(self.cwd, dst).lstrip("/")
try:
self.hub.up2k.handle_mv(self.uname, self.h.cli_ip, svp, dvp)
self.hub.up2k.handle_mv("", self.uname, self.h.cli_ip, svp, dvp)
except Exception as ex:
raise FSE(str(ex))
@ -403,7 +393,7 @@ class FtpFs(AbstractedFS):
def utime(self, path: str, timeval: float) -> None:
ap = self.rv2a(path, w=True)[0]
return bos.utime(ap, (timeval, timeval))
bos.utime_c(logging.warning, ap, int(timeval), False)
def lstat(self, path: str) -> os.stat_result:
ap = self.rv2a(path)[0]
@ -492,7 +482,11 @@ class FtpHandler(FTPHandler):
def ftp_STOR(self, file: str, mode: str = "w") -> Any:
# Optional[str]
vp = join(self.fs.cwd, file).lstrip("/")
try:
ap, vfs, rem = self.fs.v2a(vp, w=True)
except Exception as ex:
self.respond("550 %s" % (ex,), logging.info)
return
self.vfs_map[ap] = vp
xbu = vfs.flags.get("xbu")
if xbu and not runhook(

View file

@ -12,7 +12,6 @@ import random
import re
import socket
import stat
import string
import sys
import threading # typechk
import time
@ -31,7 +30,7 @@ try:
except:
pass
from .__init__ import ANYWIN, PY2, RES, TYPE_CHECKING, EnvParams, unicode
from .__init__ import ANYWIN, RES, TYPE_CHECKING, EnvParams, unicode
from .__version__ import S_VERSION
from .authsrv import LEELOO_DALLAS, VFS # typechk
from .bos import bos
@ -66,6 +65,7 @@ from .util import (
exclude_dotfiles,
formatdate,
fsenc,
gen_content_disposition,
gen_filekey,
gen_filekey_dbg,
gencookie,
@ -394,10 +394,10 @@ class HttpCli(object):
zsl = [
" rproxy: %d if this client's IP-address is [%s]"
% (-1 - zd, zs.strip())
for zd, zs in enumerate(zsl)
for zd, zs in enumerate(zsl[::-1])
]
t = 'could not determine the client\'s IP-address because the global-option --rproxy has not been configured, so the request-header [%s] specified by global-option --xff-hdr cannot be used safely! Please see the "reverse-proxy" section in the readme. The best approach is to configure your reverse-proxy to give copyparty the exact IP-address to assume (perhaps in another header), but you may also try the following:'
t = t % (self.args.xff_hdr,)
t = 'could not determine the client\'s IP-address because the global-option --rproxy has not been configured, so the request-header [%s] specified by global-option --xff-hdr cannot be used safely! The raw header value was [%s]. Please see the "reverse-proxy" section in the readme. The best approach is to configure your reverse-proxy to give copyparty the exact IP-address to assume (perhaps in another header), but you may also try the following:'
t = t % (self.args.xff_hdr, zso)
self.log("%s\n\n%s\n" % (t, "\n".join(zsl)), 3)
pip = self.conn.addr[0]
@ -562,19 +562,23 @@ class HttpCli(object):
zso = self.headers.get("cookie")
if zso:
if len(zso) > 8192:
if len(zso) > self.args.cookie_cmax:
self.loud_reply("cookie header too big", status=400)
return False
zsll = [x.split("=", 1) for x in zso.split(";") if "=" in x]
cookies = {k.strip(): unescape_cookie(zs) for k, zs in zsll}
cookie_pw = cookies.get("cppws") or cookies.get("cppwd") or ""
cookie_pw = cookies.get("cppws" if self.is_https else "cppwd") or ""
if "b" in cookies and "b" not in uparam:
uparam["b"] = cookies["b"]
if len(cookies) > self.args.cookie_nmax:
self.loud_reply("too many cookies", status=400)
else:
cookies = {}
cookie_pw = ""
if len(uparam) > 10 or len(cookies) > 50:
if len(uparam) > 12:
t = "http-request rejected; num.params: %d %r"
self.log(t % (len(uparam), self.req), 3)
self.loud_reply("u wot m8", status=400)
return False
@ -620,8 +624,24 @@ class HttpCli(object):
or "*"
)
if self.args.idp_h_usr:
idp_usr = self.headers.get(self.args.idp_h_usr) or ""
if self.args.have_idp_hdrs and (
self.uname == "*" or self.args.ao_idp_before_pw
):
idp_usr = ""
if self.args.idp_hm_usr:
for hn, hmv in self.args.idp_hm_usr_p.items():
zs = self.headers.get(hn)
if zs:
for zs1, zs2 in hmv.items():
if zs == zs1:
idp_usr = zs2
break
if idp_usr:
break
for hn in self.args.idp_h_usr:
if idp_usr and not self.args.ao_h_before_hm:
break
idp_usr = self.headers.get(hn) or idp_usr
if idp_usr:
idp_grp = (
self.headers.get(self.args.idp_h_grp) or ""
@ -670,15 +690,24 @@ class HttpCli(object):
if idp_usr in self.asrv.vfs.aread:
self.pw = ""
self.uname = idp_usr
if self.args.ao_have_pw or self.args.idp_logout:
self.html_head += "<script>var is_idp=1</script>\n"
else:
self.html_head += "<script>var is_idp=2</script>\n"
zs = self.asrv.ases.get(idp_usr)
if zs:
self.set_idp_cookie(zs)
else:
self.log("unknown username: %r" % (idp_usr,), 1)
if self.args.ipu and self.uname == "*":
if self.args.have_ipu_or_ipr:
if self.args.ipu and (self.uname == "*" or self.args.ao_ipu_wins):
self.uname = self.conn.ipu_iu[self.conn.ipu_nm.map(self.ip)]
ipr = self.conn.hsrv.ipr
if ipr and self.uname in ipr:
if not ipr[self.uname].map(self.ip):
self.log("username [%s] rejected by --ipr" % (self.uname,), 3)
self.uname = "*"
self.rvol = self.asrv.vfs.aread[self.uname]
self.wvol = self.asrv.vfs.awrite[self.uname]
@ -700,7 +729,7 @@ class HttpCli(object):
cookies["b"] = ""
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
if "xdev" in vn.flags or "xvol" in vn.flags:
if vn.realpath and ("xdev" in vn.flags or "xvol" in vn.flags):
ap = vn.canonical(rem)
avn = vn.chk_ap(ap)
else:
@ -790,6 +819,15 @@ class HttpCli(object):
6 if em.startswith("client d/c ") else 3,
)
if self.hint and self.hint.startswith("<xml> "):
if self.args.log_badxml:
t = "invalid XML received from client: %r"
self.log(t % (self.hint[6:],), 6)
else:
t = "received invalid XML from client; enable --log-badxml to see the whole XML in the log"
self.log(t, 6)
self.hint = ""
msg = "%s\r\nURL: %s\r\n" % (em, self.vpath)
if self.hint:
msg += "hint: %s\r\n" % (self.hint,)
@ -1217,7 +1255,7 @@ class HttpCli(object):
res_path = "web/" + self.vpath[5:]
if res_path in RES:
ap = os.path.join(self.E.mod, res_path)
ap = self.E.mod_ + res_path
if bos.path.exists(ap) or bos.path.exists(ap + ".gz"):
return self.tx_file(ap)
else:
@ -1506,7 +1544,9 @@ class HttpCli(object):
if not rbuf or len(buf) >= 32768:
break
xroot = parse_xml(buf.decode(enc, "replace"))
sbuf = buf.decode(enc, "replace")
self.hint = "<xml> " + sbuf
xroot = parse_xml(sbuf)
xtag = next((x for x in xroot if x.tag.split("}")[-1] == "prop"), None)
if xtag is not None:
props = set([y.tag.split("}")[-1] for y in xtag])
@ -1592,13 +1632,24 @@ class HttpCli(object):
self.log("inaccessible: %r" % ("/" + self.vpath,))
raise Pebkac(401, "authenticate")
if "quota-available-bytes" in props and not self.args.nid:
zi = vn.flags["du_iwho"] if "quota-available-bytes" in props else 0
if zi and (
zi == 9
or (zi == 7 and self.uname != "*")
or (zi == 5 and self.can_write)
or (zi == 4 and self.can_write and self.can_read)
or (zi == 3 and self.can_admin)
):
bfree, btot, _ = get_df(vn.realpath, False)
if btot:
df = {
"quota-available-bytes": str(bfree),
"quota-used-bytes": str(btot - bfree),
}
if "quotaused" in props: # macos finder crazytalk
df["quotaused"] = df["quota-used-bytes"]
if "quota" in props:
df["quota"] = df["quota-available-bytes"] # idk, makes it happy
else:
df = {}
else:
@ -1708,6 +1759,7 @@ class HttpCli(object):
uenc = enc.upper()
txt = buf.decode(enc, "replace")
self.hint = "<xml> " + txt
ET.register_namespace("D", "DAV:")
xroot = mkenod("D:orz")
xroot.insert(0, parse_xml(txt))
@ -1768,6 +1820,7 @@ class HttpCli(object):
uenc = enc.upper()
txt = buf.decode(enc, "replace")
self.hint = "<xml> " + txt
ET.register_namespace("D", "DAV:")
lk = parse_xml(txt)
assert lk.tag == "{DAV:}lockinfo"
@ -1983,6 +2036,9 @@ class HttpCli(object):
if "eshare" in self.uparam:
return self.handle_eshare()
if "fs_abrt" in self.uparam:
return self.handle_fs_abrt()
if "application/octet-stream" in ctype:
return self.handle_post_binary()
@ -2280,12 +2336,7 @@ class HttpCli(object):
at = mt = time.time() - lifetime
cli_mt = self.headers.get("x-oc-mtime")
if cli_mt:
try:
mt = int(cli_mt)
times = (int(time.time()), mt)
bos.utime(path, times, False)
except:
pass
bos.utime_c(self.log, path, int(cli_mt), False)
if nameless and "magic" in vfs.flags:
try:
@ -3007,7 +3058,7 @@ class HttpCli(object):
self.asrv.forget_session(self.conn.hsrv.broker, self.uname)
self.get_pwd_cookie("x")
dst = self.args.SRS + "?h"
dst = self.args.idp_logout or (self.args.SRS + "?h")
h2 = '<a href="' + dst + '">continue</a>'
html = self.j2s("msg", h1="ok bye", h2=h2, redir=dst)
self.reply(html.encode("utf-8"))
@ -3020,6 +3071,11 @@ class HttpCli(object):
uname = self.asrv.iacct.get(hpwd)
if uname:
pwd = self.asrv.ases.get(uname) or pwd
if uname and self.conn.hsrv.ipr:
znm = self.conn.hsrv.ipr.get(uname)
if znm and not znm.map(self.ip):
self.log("username [%s] rejected by --ipr" % (self.uname,), 3)
uname = ""
if uname:
msg = "hi " + uname
dur = int(60 * 60 * self.args.logout)
@ -3363,8 +3419,6 @@ class HttpCli(object):
sz, sha_hex, sha_b64 = copier(
p_data, f, hasher, max_sz, self.args.s_wr_slp
)
if sz == 0:
raise Pebkac(400, "empty files in post")
finally:
f.close()
@ -3982,6 +4036,13 @@ class HttpCli(object):
if not editions:
return self.tx_404()
#
# force download
if "dl" in self.ouparam:
cdis = gen_content_disposition(os.path.basename(req_path))
self.out_headers["Content-Disposition"] = cdis
#
# if-modified
@ -4150,6 +4211,13 @@ class HttpCli(object):
if not editions:
return self.tx_404()
#
# force download
if "dl" in self.ouparam:
cdis = gen_content_disposition(os.path.basename(req_path))
self.out_headers["Content-Disposition"] = cdis
#
# if-modified
@ -4698,24 +4766,7 @@ class HttpCli(object):
if maxn < nf:
raise Pebkac(400, t)
safe = (string.ascii_letters + string.digits).replace("%", "")
afn = "".join([x if x in safe.replace('"', "") else "_" for x in fn])
bascii = unicode(safe).encode("utf-8")
zb = fn.encode("utf-8", "xmlcharrefreplace")
if not PY2:
zbl = [
chr(x).encode("utf-8")
if x in bascii
else "%{:02x}".format(x).encode("ascii")
for x in zb
]
else:
zbl = [unicode(x) if x in bascii else "%{:02x}".format(ord(x)) for x in zb]
ufn = b"".join(zbl).decode("ascii")
cdis = "attachment; filename=\"{}.{}\"; filename*=UTF-8''{}.{}"
cdis = cdis.format(afn, ext, ufn, ext)
cdis = gen_content_disposition("%s.%s" % (fn, ext))
self.log(repr(cdis))
self.send_headers(None, mime=mime, headers={"Content-Disposition": cdis})
@ -4902,7 +4953,8 @@ class HttpCli(object):
"lastmod": int(ts_md * 1000),
"lang": self.args.lang,
"favico": self.args.favico,
"have_emp": self.args.emp,
"have_emp": int(self.args.emp),
"md_no_br": int(vn.flags.get("md_no_br") or 0),
"md_chk_rate": self.args.mcr,
"md": boundary,
"arg_base": arg_base,
@ -4970,10 +5022,20 @@ class HttpCli(object):
else:
rip = host
defpw = "dave:hunter2" if self.args.usernames else "hunter2"
vp = (self.uparam["hc"] or "").lstrip("/")
pw = self.pw or "hunter2"
pw = self.ouparam.get("pw") or defpw
if pw in self.asrv.sesa:
pw = "hunter2"
pw = defpw
unpw = pw
try:
un, pw = unpw.split(":")
except:
un = ""
if self.args.usernames:
un = "dave"
html = self.j2s(
"svcs",
@ -4987,7 +5049,10 @@ class HttpCli(object):
host=html_sh_esc(host),
hport=html_sh_esc(hport),
aname=aname,
b_un=("<b>%s</b>" % (html_sh_esc(un),)) if un else "k",
un=html_sh_esc(un),
pw=html_sh_esc(pw),
unpw=html_sh_esc(unpw),
)
self.reply(html.encode("utf-8"))
return True
@ -5112,6 +5177,11 @@ class HttpCli(object):
elif nre:
re_btn = "&re=%s" % (nre,)
zi = self.args.ver_iwho
show_ver = zi and (
zi == 9 or (zi == 6 and self.uname != "*") or (zi == 3 and avol)
)
html = self.j2s(
"splash",
this=self,
@ -5134,7 +5204,7 @@ class HttpCli(object):
no304=self.no304(),
k304vis=self.args.k304 > 0,
no304vis=self.args.no304 > 0,
ver=S_VERSION if self.args.ver else "",
ver=S_VERSION if show_ver else "",
chpw=self.args.chpw and self.uname != "*",
ahttps="" if self.is_https else "https://" + self.host + self.req,
)
@ -5349,8 +5419,9 @@ class HttpCli(object):
if dk_sz and fsroot:
kdirs = []
fsroot_ = os.path.join(fsroot, "")
for dn in dirs:
ap = os.path.join(fsroot, dn)
ap = fsroot_ + dn
zs = self.gen_fk(2, self.args.dk_salt, ap, 0, 0)[:dk_sz]
kdirs.append(dn + "?k=" + zs)
dirs = kdirs
@ -5470,6 +5541,10 @@ class HttpCli(object):
and ("*" in x.axs.uwrite or self.uname in x.axs.uwrite or x == shr_dbv)
]
q = ""
qp = (0,)
q_c = -1
for vol in allvols:
cur = idx.get_cur(vol)
if not cur:
@ -5477,17 +5552,31 @@ class HttpCli(object):
nfk, fk_alg = fk_vols.get(vol) or (0, 0)
zi = vol.flags["unp_who"]
if q_c != zi:
q_c = zi
q = "select sz, rd, fn, at from up where "
if zi == 1:
q += "ip=? and un=?"
qp = (self.ip, self.uname, lim)
elif zi == 2:
q += "ip=?"
qp = (self.ip, lim)
if zi == 3:
q += "un=?"
qp = (self.uname, lim)
q += " and at>? order by at desc"
n = 2000
q = "select sz, rd, fn, at from up where ip=? and at>? order by at desc"
for sz, rd, fn, at in cur.execute(q, (self.ip, lim)):
for sz, rd, fn, at in cur.execute(q, qp):
vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x)
if nfi == 0 or (nfi == 1 and vfi in vp):
if nfi == 0 or (nfi == 1 and vfi in vp.lower()):
pass
elif nfi == 2:
if not vp.startswith(vfi):
if not vp.lower().startswith(vfi):
continue
elif nfi == 3:
if not vp.endswith(vfi):
if not vp.lower().endswith(vfi):
continue
else:
continue
@ -5604,16 +5693,16 @@ class HttpCli(object):
continue
n = 1000
q = "select sz, rd, fn, ip, at from up where at>0 order by at desc"
for sz, rd, fn, ip, at in cur.execute(q):
q = "select sz, rd, fn, ip, at, un from up where at>0 order by at desc"
for sz, rd, fn, ip, at, un in cur.execute(q):
vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x)
if nfi == 0 or (nfi == 1 and vfi in vp):
if nfi == 0 or (nfi == 1 and vfi in vp.lower()):
pass
elif nfi == 2:
if not vp.startswith(vfi):
if not vp.lower().startswith(vfi):
continue
elif nfi == 3:
if not vp.endswith(vfi):
if not vp.lower().endswith(vfi):
continue
else:
continue
@ -5626,6 +5715,7 @@ class HttpCli(object):
"sz": sz,
"ip": ip,
"at": at,
"un": un,
"nfk": nfk,
"adm": adm,
}
@ -5670,12 +5760,16 @@ class HttpCli(object):
adm = rv.pop("adm")
if not adm:
rv["ip"] = "(You)" if rv["ip"] == self.ip else "(?)"
if rv["un"] not in ("*", self.uname):
rv["un"] = "(?)"
else:
for rv in ret:
adm = rv.pop("adm")
if not adm:
rv["ip"] = "(You)" if rv["ip"] == self.ip else "(?)"
rv["at"] = 0
if rv["un"] not in ("*", self.uname):
rv["un"] = "(?)"
if self.is_vproxied:
for v in ret:
@ -5859,6 +5953,14 @@ class HttpCli(object):
except:
raise Pebkac(400, "you dont have all the perms you tried to grant")
zs = vfs.flags["shr_who"]
if zs == "auth" and self.uname != "*":
pass
elif zs == "a" and self.uname in vfs.axs.uadmin:
pass
else:
raise Pebkac(400, "you dont have perms to create shares from this volume")
ap, reals, _ = vfs.ls(
rem, self.uname, not self.args.no_scandir, [[s_rd, s_wr, s_mv, s_del]]
)
@ -5954,7 +6056,9 @@ class HttpCli(object):
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)
x = self.conn.hsrv.broker.ask(
"up2k.handle_mv", self.ouparam.get("akey"), self.uname, self.ip, vsrc, vdst
)
self.loud_reply(x.get(), status=201)
return True
@ -5984,10 +6088,21 @@ class HttpCli(object):
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)
x = self.conn.hsrv.broker.ask(
"up2k.handle_cp", self.ouparam.get("akey"), self.uname, self.ip, vsrc, vdst
)
self.loud_reply(x.get(), status=201)
return True
def handle_fs_abrt(self):
if self.args.no_fs_abrt:
t = "aborting an ongoing copy/move is disabled in server config"
raise Pebkac(403, t)
self.conn.hsrv.broker.say("up2k.handle_fs_abrt", self.uparam["fs_abrt"])
self.loud_reply("aborting", status=200)
return True
def tx_ls(self, ls: dict[str, Any]) -> bool:
dirs = ls["dirs"]
files = ls["files"]
@ -6107,16 +6222,13 @@ class HttpCli(object):
add_og = "og" in vn.flags
if add_og:
if "th" in self.uparam or "raw" in self.uparam:
og_ua = add_og = False
elif self.args.og_ua:
og_ua = add_og = self.args.og_ua.search(self.ua)
else:
og_ua = False
add_og = True
add_og = False
elif vn.flags["og_ua"]:
add_og = vn.flags["og_ua"].search(self.ua)
og_fn = ""
if "v" in self.uparam:
add_og = og_ua = True
add_og = True
if "b" in self.uparam:
self.out_headers["X-Robots-Tag"] = "noindex, nofollow"
@ -6237,7 +6349,7 @@ class HttpCli(object):
is_md = abspath.lower().endswith(".md")
if add_og and not is_md:
if og_ua or self.host not in self.headers.get("referer", ""):
if self.host not in self.headers.get("referer", ""):
self.vpath, og_fn = vsplit(self.vpath)
vpath = self.vpath
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
@ -6277,7 +6389,14 @@ class HttpCli(object):
except:
self.log("#wow #whoa")
if not self.args.nid:
zi = vn.flags["du_iwho"]
if zi and (
zi == 9
or (zi == 7 and self.uname != "*")
or (zi == 5 and self.can_write)
or (zi == 4 and self.can_write and self.can_read)
or (zi == 3 and self.can_admin)
):
free, total, zs = get_df(abspath, False)
if total:
h1 = humansize(free or 0)
@ -6584,13 +6703,15 @@ class HttpCli(object):
tags = {k: v for k, v in r}
if is_admin:
q = "select ip, at from up where rd=? and fn=?"
q = "select ip, at, un from up where rd=? and fn=?"
try:
zs1, zs2 = icur.execute(q, erd_efn).fetchone()
zs1, zs2, zs3 = icur.execute(q, erd_efn).fetchone()
if zs1:
tags["up_ip"] = zs1
if zs2:
tags[".up_at"] = zs2
if zs3:
tags["up_by"] = zs3
except:
pass
elif add_up_at:
@ -6611,7 +6732,7 @@ class HttpCli(object):
lmte = list(mte)
if self.can_admin:
lmte.extend(("up_ip", ".up_at"))
lmte.extend(("up_by", "up_ip", ".up_at"))
if "nodirsz" not in vf:
tagset.add(".files")

View file

@ -70,6 +70,7 @@ from .util import (
build_netmap,
has_resource,
ipnorm,
load_ipr,
load_ipu,
load_resource,
min_ex,
@ -193,6 +194,11 @@ class HttpSrv(object):
else:
self.ipu_iu = self.ipu_nm = None
if self.args.ipr:
self.ipr = load_ipr(self.log, self.args.ipr)
else:
self.ipr = None
self.ipa_nm = build_netmap(self.args.ipa)
self.xff_nm = build_netmap(self.args.xff_src)
self.xff_lan = build_netmap("lan")
@ -565,7 +571,7 @@ class HttpSrv(object):
v = self.E.t0
try:
with os.scandir(os.path.join(self.E.mod, "web")) as dh:
with os.scandir(self.E.mod_ + "web") as dh:
for fh in dh:
inf = fh.stat()
v = max(v, inf.st_mtime)

View file

@ -27,7 +27,7 @@ from .stolen.dnslib import (
DNSRecord,
set_avahi_379,
)
from .util import CachedSet, Daemon, Netdev, list_ips, min_ex
from .util import IP6_LL, CachedSet, Daemon, Netdev, list_ips, min_ex
if TYPE_CHECKING:
from .svchub import SvcHub
@ -76,7 +76,8 @@ class MDNS(MCast):
if not self.args.zm_nwa_1:
set_avahi_379()
zs = self.args.name + ".local."
zs = self.args.zm_fqdn or (self.args.name + ".local")
zs = zs.replace("--name", self.args.name).rstrip(".") + "."
zs = zs.encode("ascii", "replace").decode("ascii", "replace")
self.hn = "-".join(x for x in zs.split("?") if x) or (
"vault-{}".format(random.randint(1, 255))
@ -374,7 +375,7 @@ class MDNS(MCast):
cip = addr[0]
v6 = ":" in cip
if (cip.startswith("169.254") and not self.ll_ok) or (
v6 and not cip.startswith("fe80")
v6 and not cip.startswith(IP6_LL)
):
return

View file

@ -29,7 +29,7 @@ from .util import (
)
if True: # pylint: disable=using-constant-test
from typing import Any, Optional, Union
from typing import IO, Any, Optional, Union
from .util import NamedLogger, RootLogger
@ -176,6 +176,9 @@ def au_unpk(
raise Exception("no images inside cbz")
fi = zf.open(using)
elif pk == "epub":
fi = get_cover_from_epub(log, abspath)
else:
raise Exception("unknown compression %s" % (pk,))
@ -205,7 +208,7 @@ def au_unpk(
def ffprobe(
abspath: str, timeout: int = 60
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]:
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:
cmd = [
b"ffprobe",
b"-hide_banner",
@ -219,8 +222,17 @@ def ffprobe(
return parse_ffprobe(so)
def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]:
"""ffprobe -show_format -show_streams"""
def parse_ffprobe(
txt: str,
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:
"""
txt: output from ffprobe -show_format -show_streams
returns:
* normalized tags
* original/raw tags
* list of streams
* format props
"""
streams = []
fmt = {}
g = {}
@ -313,7 +325,7 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
ret[rk] = v1
if ret.get("vc") == "ansi": # shellscript
return {}, {}
return {}, {}, [], {}
for strm in streams:
for sk, sv in strm.items():
@ -362,7 +374,77 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
zero = int("0")
zd = {k: (zero, v) for k, v in ret.items()}
return zd, md
return zd, md, streams, fmt
def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]:
import zipfile
from .dxml import parse_xml
try:
from urlparse import urljoin # Python2
except ImportError:
from urllib.parse import urljoin # Python3
with zipfile.ZipFile(abspath, "r") as z:
# First open the container file to find the package document (.opf file)
try:
container_root = parse_xml(z.read("META-INF/container.xml").decode())
except KeyError:
log("epub: no container file found in %s" % (abspath,))
return None
# https://www.w3.org/TR/epub-33/#sec-container.xml-rootfile-elem
container_ns = {"": "urn:oasis:names:tc:opendocument:xmlns:container"}
# One file could contain multiple package documents, default to the first one
rootfile_path = container_root.find("./rootfiles/rootfile", container_ns).get(
"full-path"
)
# Then open the first package document to find the path of the cover image
try:
package_root = parse_xml(z.read(rootfile_path).decode())
except KeyError:
log("epub: no package document found in %s" % (abspath,))
return None
# https://www.w3.org/TR/epub-33/#sec-package-doc
package_ns = {"": "http://www.idpf.org/2007/opf"}
# https://www.w3.org/TR/epub-33/#sec-cover-image
coverimage_path_node = package_root.find(
"./manifest/item[@properties='cover-image']", package_ns
)
if coverimage_path_node is not None:
coverimage_path = coverimage_path_node.get("href")
else:
# This might be an EPUB2 file, try the legacy way of specifying covers
coverimage_path = _get_cover_from_epub2(log, package_root, package_ns)
# This url is either absolute (in the .epub) or relative to the package document
adjusted_cover_path = urljoin(rootfile_path, coverimage_path)
return z.open(adjusted_cover_path)
def _get_cover_from_epub2(
log: "NamedLogger", package_root, package_ns
) -> Optional[str]:
# <meta name="cover" content="id-to-cover-image"> in <metadata>, then
# <item> in <manifest>
cover_id = package_root.find("./metadata/meta[@name='cover']", package_ns).get(
"content"
)
if not cover_id:
return None
for node in package_root.iterfind("./manifest/item", package_ns):
if node.get("id") == cover_id:
cover_path = node.get("href")
return cover_path
return None
class MTag(object):
@ -633,7 +715,7 @@ class MTag(object):
if not bos.path.isfile(abspath):
return {}
ret, md = ffprobe(abspath, self.args.mtag_to)
ret, md, _, _ = ffprobe(abspath, self.args.mtag_to)
if self.args.mtag_vv:
for zd in (ret, dict(md)):

View file

@ -15,7 +15,7 @@ from ipaddress import (
)
from .__init__ import MACOS, TYPE_CHECKING
from .util import Daemon, Netdev, find_prefix, min_ex, spack
from .util import IP6_LL, IP64_LL, Daemon, Netdev, find_prefix, min_ex, spack
if TYPE_CHECKING:
from .svchub import SvcHub
@ -145,7 +145,7 @@ class MCast(object):
all_selected = ips[:]
# discard non-linklocal ipv6
ips = [x for x in ips if ":" not in x or x.startswith("fe80")]
ips = [x for x in ips if ":" not in x or x.startswith(IP6_LL)]
if not ips:
raise NoIPs()
@ -183,7 +183,7 @@ class MCast(object):
srv.ips[oth_ip.split("/")[0]] = ipaddress.ip_network(oth_ip, False)
# gvfs breaks if a linklocal ip appears in a dns reply
ll = {k: v for k, v in srv.ips.items() if k.startswith(("169.254", "fe80"))}
ll = {k: v for k, v in srv.ips.items() if k.startswith(IP64_LL)}
rt = {k: v for k, v in srv.ips.items() if k not in ll}
if self.args.ll or not rt:

View file

@ -25,6 +25,7 @@ class PWHash(object):
self.args = args
zsl = args.ah_alg.split(",")
zsl = [x.strip() for x in zsl]
alg = zsl[0]
if alg == "none":
alg = ""

View file

@ -318,7 +318,7 @@ class SMB(object):
t = "blocked rename (no-move-acc %s): /%s @%s"
yeet(t % (vfs1.axs.umove, vp1, uname))
self.hub.up2k.handle_mv(uname, "1.7.6.2", vp1, vp2)
self.hub.up2k.handle_mv("", uname, "1.7.6.2", vp1, vp2)
try:
bos.makedirs(ap2, vf=vfs2.flags)
except:
@ -373,7 +373,7 @@ class SMB(object):
t = "blocked utime (no-write-acc %s): /%s @%s"
yeet(t % (vfs.axs.uwrite, vpath, uname))
return bos.utime(ap, times)
bos.utime_c(info, ap, int(times[1]), False)
def _p_exists(self, vpath: str) -> bool:
# ap = "?"

View file

@ -200,6 +200,25 @@ class QrCode(object):
return "\n".join(rows)
def to_png(self, zoom, pad, bg, fg, ap) -> None:
from PIL import Image
tab = self.modules
sz = self.size
psz = sz + pad * 2
if bg:
img = Image.new("RGB", (psz, psz), bg)
else:
img = Image.new("RGBA", (psz, psz), (0, 0, 0, 0))
fg = (fg[0], fg[1], fg[2], 255)
for y in range(sz):
for x in range(sz):
if tab[y][x]:
img.putpixel((x + pad, y + pad), fg)
if zoom != 1:
img = img.resize((sz * zoom, sz * zoom), Image.Resampling.NEAREST)
img.save(ap)
def _draw_function_patterns(self) -> None:
# Draw horizontal and vertical timing patterns
for i in range(self.size):

View file

@ -2,6 +2,7 @@
from __future__ import print_function, unicode_literals
import argparse
import atexit
import errno
import logging
import os
@ -26,7 +27,7 @@ if True: # pylint: disable=using-constant-test
from typing import Any, Optional, Union
from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode
from .authsrv import BAD_CFG, AuthSrv
from .authsrv import BAD_CFG, AuthSrv, n_du_who, n_ver_who
from .bos import bos
from .cert import ensure_cert
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN
@ -38,6 +39,7 @@ from .th_srv import (
HAVE_FFPROBE,
HAVE_HEIF,
HAVE_PIL,
HAVE_RAW,
HAVE_VIPS,
HAVE_WEBP,
ThumbSrv,
@ -64,6 +66,7 @@ from .util import (
build_netmap,
expat_ver,
gzip,
load_ipr,
load_ipu,
lock_file,
min_ex,
@ -72,6 +75,7 @@ from .util import (
pybin,
start_log_thrs,
start_stackmon,
termsize,
ub64enc,
)
@ -130,6 +134,7 @@ class SvcHub(object):
self.nsigs = 3
self.retcode = 0
self.httpsrv_up = 0
self.qr_tsz = None
self.log_mutex = threading.Lock()
self.cday = 0
@ -152,6 +157,7 @@ class SvcHub(object):
args.no_del = True
args.no_mv = True
args.hardlink = True
args.dav_auth = True
args.vague_403 = True
args.nih = True
@ -240,7 +246,7 @@ class SvcHub(object):
t = "WARNING: --th-ram-max is very small (%.2f GiB); will not be able to %s"
self.log("root", t % (args.th_ram_max, zs), 3)
if args.chpw and args.idp_h_usr:
if args.chpw and args.have_idp_hdrs:
t = "ERROR: user-changeable passwords is incompatible with IdP/identity-providers; you must disable either --chpw or --idp-h-usr"
self.log("root", t, 1)
raise Exception(t)
@ -256,6 +262,10 @@ class SvcHub(object):
setattr(args, "ipu_iu", iu)
setattr(args, "ipu_nm", nm)
if args.ipr:
ipr = load_ipr(self.log, args.ipr, True)
setattr(args, "ipr_u", ipr)
for zs in "ah_salt fk_salt dk_salt".split():
if getattr(args, "show_%s" % (zs,)):
self.log("root", "effective %s is %s" % (zs, getattr(args, zs)))
@ -265,7 +275,7 @@ class SvcHub(object):
args.no_ses = True
args.shr = ""
if args.idp_store and args.idp_h_usr:
if args.idp_store and args.have_idp_hdrs:
self.setup_db("idp")
if not self.args.no_ses:
@ -279,6 +289,14 @@ class SvcHub(object):
ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)]
args.theme = "{0}{1} {0} {1}".format(ch, bri)
if args.nid:
args.du_who = "no"
args.du_iwho = n_du_who(args.du_who)
if args.ver and args.ver_who == "no":
args.ver_who = "all"
args.ver_iwho = n_ver_who(args.ver_who)
if args.nih:
args.vname = ""
args.doctitle = args.doctitle.replace(" @ --name", "")
@ -316,11 +334,13 @@ class SvcHub(object):
self._feature_test()
decs = {k: 1 for k in self.args.th_dec.split(",")}
decs = {k.strip(): 1 for k in self.args.th_dec.split(",")}
if not HAVE_VIPS:
decs.pop("vips", None)
if not HAVE_PIL:
decs.pop("pil", None)
if not HAVE_RAW:
decs.pop("raw", None)
if not HAVE_FFMPEG or not HAVE_FFPROBE:
decs.pop("ff", None)
@ -422,11 +442,14 @@ class SvcHub(object):
# create netmaps early to avoid firewall gaps,
# but the mutex blocks multiprocessing startup
for zs in "ipu_iu ftp_ipa_nm tftp_ipa_nm".split():
for zs in "ipu_nm ftp_ipa_nm tftp_ipa_nm".split():
try:
getattr(args, zs).mutex = threading.Lock()
except:
pass
if args.ipr:
for nm in args.ipr_u.values():
nm.mutex = threading.Lock()
def _db_onfail_ses(self) -> None:
self.args.no_ses = True
@ -772,6 +795,80 @@ class SvcHub(object):
def sigterm(self) -> None:
self.signal_handler(signal.SIGTERM, None)
def sticky_qr(self) -> None:
self._sticky_qr()
def _unsticky_qr(self, flush=True) -> None:
print("\033[s\033[J\033[r\033[u", file=sys.stderr, end="")
if flush:
sys.stderr.flush()
def _sticky_qr(self, force: bool = False) -> None:
sz = termsize()
if self.qr_tsz == sz:
if not force:
return
else:
force = False
if self.qr_tsz:
self._unsticky_qr(False)
else:
atexit.register(self._unsticky_qr)
tw, th = self.qr_tsz = sz
zs1, qr = self.tcpsrv.qr.split("\n", 1)
url, colr = zs1.split(" ", 1)
nl = len(qr.split("\n")) # numlines
lp = 3 if nl * 2 + 4 < tw else 0 # leftpad
lp0 = lp
if self.args.qr_pin == 2:
url = ""
else:
while lp and (nl + lp) * 2 + len(url) + 1 > tw:
lp -= 1
if (nl + lp) * 2 + len(url) + 1 > tw:
qr = url + "\n" + qr
url = ""
nl += 1
lp = lp0
sh = 1 + th - nl
if lp:
zs = " " * lp
qr = zs + qr.replace("\n", "\n" + zs)
if url:
url = "%s\033[%d;%dH%s\033[0m" % (colr, sh + 1, (nl + lp) * 2, url)
qr = colr + qr
t = "%s\033[%dA" % ("\n" * nl, nl)
t = "%s\033[s\033[1;%dr\033[%dH%s%s\033[u" % (t, sh - 1, sh, qr, url)
if not force:
self.log("qr", "sticky-qrcode %sx%s,%s" % (tw, th, sh), 6)
self.pr(t, file=sys.stderr, end="")
def _qr_thr(self):
qr = self.tcpsrv.qr
w8 = self.args.qr_wait
if w8:
time.sleep(w8)
self.log("qr-code", qr)
w8 = self.args.qr_every
msg = "%s\033[%dA" % (qr, len(qr.split("\n")))
while w8:
time.sleep(w8)
if self.stopping:
break
if self.args.qr_pin:
self._sticky_qr(True)
else:
self.log("qr-code", msg)
w8 = self.args.qr_winch
while w8:
time.sleep(w8)
if self.stopping:
break
self._sticky_qr()
def cb_httpsrv_up(self) -> None:
self.httpsrv_up += 1
if self.httpsrv_up != self.broker.num_workers:
@ -784,6 +881,11 @@ class SvcHub(object):
break
if self.tcpsrv.qr:
if self.args.qr_pin:
self.sticky_qr()
if self.args.qr_wait or self.args.qr_every or self.args.qr_winch:
Daemon(self._qr_thr, "qr")
elif not self.args.qr_pin:
self.log("qr-code", self.tcpsrv.qr)
else:
self.log("root", "workers OK\n")
@ -811,6 +913,7 @@ class SvcHub(object):
(HAVE_ZMQ, "pyzmq", "send zeromq messages from event-hooks"),
(HAVE_HEIF, "pillow-heif", "read .heif images with pillow (rarely useful)"),
(HAVE_AVIF, "pillow-avif", "read .avif images with pillow (rarely useful)"),
(HAVE_RAW, "rawpy", "read RAW images"),
]
if ANYWIN:
to_check += [
@ -881,6 +984,24 @@ class SvcHub(object):
t = "WARNING:\nDisabling WebDAV support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n"
self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1)
if not E.scfg and not al.unsafe_state and not os.getenv("PRTY_UNSAFE_STATE"):
t = "because runtime config is currently being stored in an untrusted emergency-fallback location. Please fix your environment so either XDG_CONFIG_HOME or ~/.config can be used instead, or disable this safeguard with --unsafe-state or env-var PRTY_UNSAFE_STATE=1."
if not al.no_ses:
al.no_ses = True
t2 = "A consequence of this misconfiguration is that passwords will now be sent in the HTTP-header of every request!"
self.log("root", "WARNING:\nWill disable sessions %s %s" % (t, t2), 1)
if al.idp_store == 1:
al.idp_store = 0
self.log("root", "WARNING:\nDisabling --idp-store %s" % (t,), 3)
if al.idp_store:
t2 = "ERROR: Cannot enable --idp-store %s" % (t,)
self.log("root", t2, 1)
raise Exception(t2)
if al.shr:
t2 = "ERROR: Cannot enable shares %s" % (t,)
self.log("root", t2, 1)
raise Exception(t2)
def _process_config(self) -> bool:
al = self.args
@ -969,10 +1090,23 @@ class SvcHub(object):
al.sus_urls = None
al.xff_hdr = al.xff_hdr.lower()
al.idp_h_usr = al.idp_h_usr.lower()
al.idp_h_usr = [x.lower() for x in al.idp_h_usr or []]
al.idp_h_grp = al.idp_h_grp.lower()
al.idp_h_key = al.idp_h_key.lower()
al.idp_hm_usr_p = {}
for zs0 in al.idp_hm_usr or []:
try:
sep = zs0[:1]
hn, zs1, zs2 = zs0[1:].split(sep)
hn = hn.lower()
if hn in al.idp_hm_usr_p:
al.idp_hm_usr_p[hn][zs1] = zs2
else:
al.idp_hm_usr_p[hn] = {zs1: zs2}
except:
raise Exception("invalid --idp-hm-usr [%s]" % (zs0,))
al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa, True)
al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa, True)
@ -1024,7 +1158,7 @@ class SvcHub(object):
al.tcolor = "".join([x * 2 for x in al.tcolor])
zs = al.u2sz
zsl = zs.split(",")
zsl = [x.strip() for x in zs.split(",")]
if len(zsl) not in (1, 3):
t = "invalid --u2sz; must be either one number, or a comma-separated list of three numbers (min,default,max)"
raise Exception(t)
@ -1400,7 +1534,14 @@ class SvcHub(object):
fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n"
if self.no_ansi:
fmt = "%s %-21s %s\n"
if c == 1:
fmt = "%s %-21s CRIT: %s\n"
elif c == 3:
fmt = "%s %-21s WARN: %s\n"
elif c == 6:
fmt = "%s %-21s BTW: %s\n"
else:
fmt = "%s %-21s LOG: %s\n"
if "\033" in msg:
msg = RE_ANSI.sub("", msg)
if "\033" in src:

View file

@ -9,13 +9,14 @@ import time
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
from .cert import gencert
from .stolen.qrcodegen import QrCode
from .stolen.qrcodegen import QrCode, qr2svg
from .util import (
E_ACCESS,
E_ADDR_IN_USE,
E_ADDR_NOT_AVAIL,
E_UNREACH,
HAVE_IPV6,
IP6_LL,
IP6ALL,
VF_CAREFUL,
Netdev,
@ -140,12 +141,12 @@ class TcpSrv(object):
# keep IPv6 LL-only nics
ll_ok: set[str] = set()
for ip, nd in self.netdevs.items():
if not ip.startswith("fe80"):
if not ip.startswith(IP6_LL):
continue
just_ll = True
for ip2, nd2 in self.netdevs.items():
if nd == nd2 and ":" in ip2 and not ip2.startswith("fe80"):
if nd == nd2 and ":" in ip2 and not ip2.startswith(IP6_LL):
just_ll = False
if just_ll or self.args.ll:
@ -164,7 +165,7 @@ class TcpSrv(object):
title_vars = [x[1:] for x in self.args.wintitle.split(" ") if x.startswith("$")]
t = "available @ {}://{}:{}/ (\033[33m{}\033[0m)"
for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
if ip.startswith("fe80") and ip not in ll_ok:
if ip.startswith(IP6_LL) and ip not in ll_ok:
continue
for port in sorted(self.args.p):
@ -304,6 +305,10 @@ class TcpSrv(object):
if os.path.exists(ip):
os.unlink(ip)
srv.bind(ip)
if uds_gid != -1:
os.chown(ip, -1, uds_gid)
if uds_perm != -1:
os.chmod(ip, uds_perm)
else:
tf = "%s.%d" % (ip, os.getpid())
if os.path.exists(tf):
@ -614,9 +619,17 @@ class TcpSrv(object):
fg = self.args.qr_fg
bg = self.args.qr_bg
nocolor = fg == -1
if nocolor:
fg = 0
pad = self.args.qrp
zoom = self.args.qrz
qrc = QrCode.encode_binary(btxt)
for zs in self.args.qr_file or []:
self._qr2file(qrc, zs)
if zoom == 0:
try:
tw, th = termsize()
@ -632,6 +645,8 @@ class TcpSrv(object):
halfc = "\033[40;48;5;{0}m{1}\033[47;48;5;{2}m"
if not fg:
halfc = "\033[0;40m{1}\033[0;47m"
if nocolor:
halfc = "\033[0;7m{1}\033[0m"
def ansify(m: re.Match) -> str:
return halfc.format(fg, " " * len(m.group(1)), bg)
@ -641,6 +656,8 @@ class TcpSrv(object):
qr = qr.replace("\n", "\033[K\n") + "\033[K" # win10do
cc = " \033[0;38;5;{0};47;48;5;{1}m" if fg else " \033[0;30;47m"
if nocolor:
cc = " \033[0m"
t = cc + "\n{2}\033[999G\033[0m\033[J"
t = t.format(fg, bg, qr)
if ANYWIN:
@ -648,3 +665,29 @@ class TcpSrv(object):
t = t.replace("\n", "`\n`")
return txt + t
def _qr2file(self, qrc: QrCode, txt: str):
if ".txt:" in txt or ".svg:" in txt:
ap, zs1, zs2 = txt.rsplit(":", 2)
bg = fg = ""
else:
ap, zs1, zs2, bg, fg = txt.rsplit(":", 4)
zoom = int(zs1)
pad = int(zs2)
if ap.endswith(".txt"):
if zoom not in (1, 2):
raise Exception("invalid zoom for qr.txt; must be 1 or 2")
with open(ap, "wb") as f:
f.write(qrc.render(zoom, pad).encode("utf-8"))
elif ap.endswith(".svg"):
with open(ap, "wb") as f:
f.write(qr2svg(qrc, pad).encode("utf-8"))
else:
qrc.to_png(zoom, pad, self._h2i(bg), self._h2i(fg), ap)
def _h2i(self, hs):
try:
return tuple(int(hs[i : i + 2], 16) for i in (0, 2, 4))
except:
return None

View file

@ -36,11 +36,15 @@ class ThumbCli(object):
if not c:
raise Exception()
except:
c = {k: set() for k in ["thumbable", "pil", "vips", "ffi", "ffv", "ffa"]}
c = {
k: set()
for k in ["thumbable", "pil", "vips", "raw", "ffi", "ffv", "ffa"]
}
self.thumbable = c["thumbable"]
self.fmt_pil = c["pil"]
self.fmt_vips = c["vips"]
self.fmt_raw = c["raw"]
self.fmt_ffi = c["ffi"]
self.fmt_ffv = c["ffv"]
self.fmt_ffa = c["ffa"]

View file

@ -2,6 +2,7 @@
from __future__ import print_function, unicode_literals
import hashlib
import io
import logging
import os
import re
@ -85,6 +86,9 @@ try:
if os.environ.get("PRTY_NO_PIL_HEIF"):
raise Exception()
try:
from pillow_heif import register_heif_opener
except ImportError:
from pyheif_pillow_opener import register_heif_opener
register_heif_opener()
@ -112,14 +116,28 @@ except:
try:
if os.environ.get("PRTY_NO_VIPS"):
raise Exception()
raise ImportError()
HAVE_VIPS = True
import pyvips
logging.getLogger("pyvips").setLevel(logging.WARNING)
except:
except Exception as e:
HAVE_VIPS = False
if not isinstance(e, ImportError):
logging.warning("libvips found, but failed to load: " + str(e))
try:
if os.environ.get("PRTY_NO_RAW"):
raise Exception()
HAVE_RAW = True
import rawpy
logging.getLogger("rawpy").setLevel(logging.WARNING)
except:
HAVE_RAW = False
th_dir_cache = {}
@ -205,11 +223,19 @@ class ThumbSrv(object):
if self.args.th_clean:
Daemon(self.cleaner, "thumb.cln")
self.fmt_pil, self.fmt_vips, self.fmt_ffi, self.fmt_ffv, self.fmt_ffa = [
(
self.fmt_pil,
self.fmt_vips,
self.fmt_raw,
self.fmt_ffi,
self.fmt_ffv,
self.fmt_ffa,
) = [
set(y.split(","))
for y in [
self.args.th_r_pil,
self.args.th_r_vips,
self.args.th_r_raw,
self.args.th_r_ffi,
self.args.th_r_ffv,
self.args.th_r_ffa,
@ -232,6 +258,9 @@ class ThumbSrv(object):
if "vips" in self.args.th_dec:
self.thumbable |= self.fmt_vips
if "raw" in self.args.th_dec:
self.thumbable |= self.fmt_raw
if "ff" in self.args.th_dec:
for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]:
self.thumbable |= zss
@ -313,6 +342,7 @@ class ThumbSrv(object):
"thumbable": self.thumbable,
"pil": self.fmt_pil,
"vips": self.fmt_vips,
"raw": self.fmt_raw,
"ffi": self.fmt_ffi,
"ffv": self.fmt_ffv,
"ffa": self.fmt_ffa,
@ -368,6 +398,8 @@ class ThumbSrv(object):
funs.append(self.conv_pil)
elif lib == "vips" and ext in self.fmt_vips:
funs.append(self.conv_vips)
elif lib == "raw" and ext in self.fmt_raw:
funs.append(self.conv_raw)
elif can_au and (want_png or want_au):
if want_opus:
funs.append(self.conv_opus)
@ -480,9 +512,7 @@ class ThumbSrv(object):
return im
def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath)
with Image.open(fsenc(abspath)) as im:
def conv_image_pil(self, im: "Image.Image", tpath: str, fmt: str, vn: VFS) -> None:
try:
im = self.fancy_pillow(im, fmt, vn)
except Exception as ex:
@ -510,6 +540,11 @@ class ThumbSrv(object):
im.save(tpath, **args)
def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath)
with Image.open(fsenc(abspath)) as im:
self.conv_image_pil(im, tpath, fmt, vn)
def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath)
crops = ["centre", "none"]
@ -531,9 +566,53 @@ class ThumbSrv(object):
assert img # type: ignore # !rm
img.write_to_file(tpath, Q=40)
def conv_raw(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath)
with rawpy.imread(abspath) as raw:
thumb = raw.extract_thumb()
if thumb.format == rawpy.ThumbFormat.JPEG and tpath.endswith(".jpg"):
# if we have a jpg thumbnail and no webp output is available,
# just write the jpg directly (it'll be the wrong size, but it's fast)
with open(tpath, "wb") as f:
f.write(thumb.data)
if HAVE_VIPS:
crops = ["centre", "none"]
if "f" in fmt:
crops = ["none"]
w, h = self.getres(vn, fmt)
kw = {"height": h, "size": "down", "intent": "relative"}
for c in crops:
try:
kw["crop"] = c
if thumb.format == rawpy.ThumbFormat.BITMAP:
img = pyvips.Image.new_from_array(
thumb.data, interpretation="rgb"
)
img = img.thumbnail_image(w, **kw)
else:
img = pyvips.Image.thumbnail_buffer(thumb.data, w, **kw)
break
except:
if c == crops[-1]:
raise
assert img # type: ignore # !rm
img.write_to_file(tpath, Q=40)
elif HAVE_PIL:
if thumb.format == rawpy.ThumbFormat.BITMAP:
im = Image.fromarray(thumb.data, "RGB")
else:
im = Image.open(io.BytesIO(thumb.data))
self.conv_image_pil(im, tpath, fmt, vn)
else:
raise Exception(
"either pil or vips is needed to process embedded bitmap thumbnails in raw files"
)
def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath)
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if not ret:
return
@ -544,6 +623,17 @@ class ThumbSrv(object):
dur = ret[".dur"][1] if ".dur" in ret else 4
seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")]
self._ffmpeg_im(abspath, tpath, fmt, vn, seek, b"0:v:0")
def _ffmpeg_im(
self,
abspath: str,
tpath: str,
fmt: str,
vn: VFS,
seek: list[bytes],
imap: bytes,
) -> None:
scale = "scale={0}:{1}:force_original_aspect_ratio="
if "f" in fmt:
scale += "decrease,setsar=1:1"
@ -562,7 +652,7 @@ class ThumbSrv(object):
cmd += seek
cmd += [
b"-i", fsenc(abspath),
b"-map", b"0:v:0",
b"-map", imap,
b"-vf", bscale,
b"-frames:v", b"1",
b"-metadata:s:v:0", b"rotate=0",
@ -583,11 +673,11 @@ class ThumbSrv(object):
]
cmd += [fsenc(tpath)]
self._run_ff(cmd, vn)
self._run_ff(cmd, vn, "convt")
def _run_ff(self, cmd: list[bytes], vn: VFS, oom: int = 400) -> None:
def _run_ff(self, cmd: list[bytes], vn: VFS, kto: str, oom: int = 400) -> None:
# self.log((b" ".join(cmd)).decode("utf-8"))
ret, _, serr = runcmd(cmd, timeout=vn.flags["convt"], nice=True, oom=oom)
ret, _, serr = runcmd(cmd, timeout=vn.flags[kto], nice=True, oom=oom)
if not ret:
return
@ -631,7 +721,7 @@ class ThumbSrv(object):
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
def conv_waves(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in ret:
raise Exception("not audio")
@ -669,7 +759,7 @@ class ThumbSrv(object):
# fmt: on
cmd += [fsenc(tpath)]
self._run_ff(cmd, vn)
self._run_ff(cmd, vn, "convt")
if "pngquant" in vn.flags:
wtpath = tpath + ".png"
@ -690,11 +780,31 @@ class ThumbSrv(object):
else:
atomic_move(self.log, wtpath, tpath, vn.flags)
def conv_emb_cv(
self, abspath: str, tpath: str, fmt: str, vn: VFS, strm: dict[str, Any]
) -> None:
self.wait4ram(0.2, tpath)
self._ffmpeg_im(
abspath, tpath, fmt, vn, [], b"0:" + strm["index"].encode("ascii")
)
def conv_spec(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
ret, raw, strms, ctnr = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in ret:
raise Exception("not audio")
want_spec = vn.flags.get("th_spec_p", 1)
if want_spec < 2:
for strm in strms:
if (
strm.get("codec_type") == "video"
and strm.get("DISPOSITION:attached_pic") == "1"
):
return self.conv_emb_cv(abspath, tpath, fmt, vn, strm)
if not want_spec:
raise Exception("spectrograms forbidden by volflag")
fext = abspath.split(".")[-1].lower()
# https://trac.ffmpeg.org/ticket/10797
@ -730,7 +840,7 @@ class ThumbSrv(object):
b"-y", fsenc(infile),
]
# fmt: on
self._run_ff(cmd, vn)
self._run_ff(cmd, vn, "convt")
fc = "[0:a:0]aresample=48000{},showspectrumpic=s="
if "3" in fmt:
@ -772,7 +882,7 @@ class ThumbSrv(object):
]
cmd += [fsenc(tpath)]
self._run_ff(cmd, vn)
self._run_ff(cmd, vn, "convt")
def conv_mp3(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
quality = self.args.q_mp3.lower()
@ -780,7 +890,7 @@ class ThumbSrv(object):
raise Exception("disabled in server config")
self.wait4ram(0.2, tpath)
tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2))
tags, rawtags, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in tags:
raise Exception("not audio")
@ -811,14 +921,14 @@ class ThumbSrv(object):
fsenc(tpath)
]
# fmt: on
self._run_ff(cmd, vn, oom=300)
self._run_ff(cmd, vn, "aconvt", oom=300)
def conv_flac(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
if self.args.no_acode or not self.args.allow_flac:
raise Exception("flac not permitted in server config")
self.wait4ram(0.2, tpath)
tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2))
tags, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in tags:
raise Exception("not audio")
@ -836,14 +946,14 @@ class ThumbSrv(object):
fsenc(tpath)
]
# fmt: on
self._run_ff(cmd, vn, oom=300)
self._run_ff(cmd, vn, "aconvt", oom=300)
def conv_wav(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
if self.args.no_acode or not self.args.allow_wav:
raise Exception("wav not permitted in server config")
self.wait4ram(0.2, tpath)
tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2))
tags, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in tags:
raise Exception("not audio")
@ -871,14 +981,14 @@ class ThumbSrv(object):
fsenc(tpath)
]
# fmt: on
self._run_ff(cmd, vn, oom=300)
self._run_ff(cmd, vn, "aconvt", oom=300)
def conv_opus(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
if self.args.no_acode or not self.args.q_opus:
raise Exception("disabled in server config")
self.wait4ram(0.2, tpath)
tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2))
tags, rawtags, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in tags:
raise Exception("not audio")
@ -927,7 +1037,7 @@ class ThumbSrv(object):
fsenc(tpath)
]
# fmt: on
self._run_ff(cmd, vn, oom=300)
self._run_ff(cmd, vn, "aconvt", oom=300)
def _conv_caf(
self,
@ -967,7 +1077,7 @@ class ThumbSrv(object):
fsenc(tmp_opus)
]
# fmt: on
self._run_ff(cmd, vn, oom=300)
self._run_ff(cmd, vn, "aconvt", oom=300)
# iOS fails to play some "insufficiently complex" files
# (average file shorter than 8 seconds), so of course we
@ -994,7 +1104,7 @@ class ThumbSrv(object):
fsenc(tpath)
]
# fmt: on
self._run_ff(cmd, vn, oom=300)
self._run_ff(cmd, vn, "aconvt", oom=300)
else:
# simple remux should be safe
@ -1013,7 +1123,7 @@ class ThumbSrv(object):
fsenc(tpath)
]
# fmt: on
self._run_ff(cmd, vn, oom=300)
self._run_ff(cmd, vn, "aconvt", oom=300)
try:
wunlink(self.log, tmp_opus, vn.flags)

View file

@ -391,7 +391,7 @@ class U2idx(object):
fk_alg = 2 if "fka" in flags else 1
c = cur.execute(uq, tuple(vuv))
for hit in c:
w, ts, sz, rd, fn, ip, at = hit[:7]
w, ts, sz, rd, fn = hit[:5]
if rd.startswith("//") or fn.startswith("//"):
rd, fn = s3dec(rd, fn)

View file

@ -60,6 +60,7 @@ from .util import (
sfsenc,
spack,
statdir,
trystat_shutil_copy2,
ub64enc,
unhumanize,
vjoin,
@ -77,7 +78,7 @@ except:
if HAVE_SQLITE3:
import sqlite3
DB_VER = 5
DB_VER = 6
if True: # pylint: disable=using-constant-test
from typing import Any, Optional, Pattern, Union
@ -91,6 +92,9 @@ ICV_EXTS = set(zsg.split(","))
zsg = "3gp,asf,av1,avc,avi,flv,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,vob,webm,wmv"
VCV_EXTS = set(zsg.split(","))
zsg = "aif,aiff,alac,ape,flac,m4a,mp3,oga,ogg,opus,tak,tta,wav,wma,wv"
ACV_EXTS = set(zsg.split(","))
zsg = "nohash noidx xdev xvol"
VF_AFFECTS_INDEXING = set(zsg.split(" "))
@ -144,6 +148,7 @@ class Up2k(object):
self.salt = self.args.warksalt
self.r_hash = re.compile("^[0-9a-zA-Z_-]{44}$")
self.abrt_key = ""
self.gid = 0
self.gt0 = 0
@ -410,10 +415,11 @@ class Up2k(object):
ret: list[tuple[int, str, int, int, int]] = []
userset = set([(uname or "\n"), "*"])
e_d = {}
n = 1000
try:
for ptop, tab2 in self.registry.items():
cfg = self.flags.get(ptop, {}).get("u2abort", 1)
cfg = self.flags.get(ptop, e_d).get("u2abort", 1)
if not cfg:
continue
addr = (ip or "\n") if cfg in (1, 2) else ""
@ -902,7 +908,7 @@ class Up2k(object):
self.iacct = self.asrv.iacct
self.grps = self.asrv.grps
have_e2d = self.args.idp_h_usr or self.args.chpw or self.args.shr
have_e2d = self.args.have_idp_hdrs or self.args.chpw or self.args.shr
vols = list(all_vols.values())
t0 = time.time()
@ -922,6 +928,12 @@ class Up2k(object):
with self.mutex, self.reg_mutex:
# only need to protect register_vpath but all in one go feels right
for vol in vols:
if bos.path.isfile(vol.realpath):
self.volstate[vol.vpath] = "online (just-a-file)"
t = "NOTE: volume [/%s] is a file, not a folder"
self.log(t % (vol.vpath,))
continue
try:
# mkdir gonna happen at snap anyways;
bos.makedirs(vol.realpath, vf=vol.flags)
@ -1128,7 +1140,7 @@ class Up2k(object):
ft = "\033[0;32m{}{:.0}"
ff = "\033[0;35m{}{:.0}"
fv = "\033[0;36m{}:\033[90m{}"
zs = "ext_th_d html_head put_name2 mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
zs = "du_iwho ext_th_d html_head put_name2 mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
fx = set(zs.split())
fd = vf_bmap()
fd.update(vf_cmap())
@ -1482,7 +1494,8 @@ class Up2k(object):
unreg: list[str] = []
files: list[tuple[int, int, str]] = []
fat32 = True
cv = vcv = ""
cv = vcv = acv = ""
e_d = {}
th_cvd = self.args.th_coversd
th_cvds = self.args.th_coversd_set
@ -1592,9 +1605,11 @@ class Up2k(object):
cv = iname
elif not vcv and ext in VCV_EXTS and not iname.startswith("."):
vcv = iname
elif not acv and ext in ACV_EXTS and not iname.startswith("."):
acv = iname
if not cv:
cv = vcv
cv = vcv or acv
if not self.args.no_dirsz:
tnf += len(files)
@ -1654,7 +1669,7 @@ class Up2k(object):
abspath = cdirs + fn
nohash = reh.search(abspath) if reh else False
sql = "select w, mt, sz, ip, at from up where rd = ? and fn = ?"
sql = "select w, mt, sz, ip, at, un from up where rd = ? and fn = ?"
try:
c = db.c.execute(sql, (rd, fn))
except:
@ -1663,7 +1678,7 @@ class Up2k(object):
in_db = list(c.fetchall())
if in_db:
self.pp.n -= 1
dw, dts, dsz, ip, at = in_db[0]
dw, dts, dsz, ip, at, un = in_db[0]
if len(in_db) > 1:
t = "WARN: multiple entries: %r => %r |%d|\n%r"
rep_db = "\n".join([repr(x) for x in in_db])
@ -1676,9 +1691,12 @@ class Up2k(object):
if dts == lmod and dsz == sz and (nohash or dw[0] != "#" or not sz):
continue
if un is None:
un = ""
t = "reindex %r => %r mtime(%s/%s) size(%s/%s)"
self.log(t % (top, rp, dts, lmod, dsz, sz))
self.db_rm(db.c, rd, fn, 0)
self.db_rm(db.c, e_d, rd, fn, 0)
tfa += 1
db.n += 1
in_db = []
@ -1686,6 +1704,7 @@ class Up2k(object):
dw = ""
ip = ""
at = 0
un = ""
self.pp.msg = "a%d %s" % (self.pp.n, abspath)
@ -1711,9 +1730,10 @@ class Up2k(object):
if dw and dw != wark:
ip = ""
at = 0
un = ""
# skip upload hooks by not providing vflags
self.db_add(db.c, {}, rd, fn, lmod, sz, "", "", wark, wark, "", "", ip, at)
self.db_add(db.c, e_d, rd, fn, lmod, sz, "", "", wark, wark, "", un, ip, at)
db.n += 1
db.nf += 1
tfa += 1
@ -1773,7 +1793,7 @@ class Up2k(object):
rm_files = [x for x in hits if x not in seen_files]
n_rm = len(rm_files)
for fn in rm_files:
self.db_rm(db.c, rd, fn, 0)
self.db_rm(db.c, e_d, rd, fn, 0)
if n_rm:
self.log("forgot {} deleted files".format(n_rm))
@ -2150,8 +2170,8 @@ class Up2k(object):
with self.mutex:
try:
q = "select rd, fn, ip, at from up where substr(w,1,16)=? and +w=?"
rd, fn, ip, at = cur.execute(q, (w16, w)).fetchone()
q = "select rd, fn, ip, at, un from up where substr(w,1,16)=? and +w=?"
rd, fn, ip, at, un = cur.execute(q, (w16, w)).fetchone()
except:
# file modified/deleted since spooling
continue
@ -2170,12 +2190,15 @@ class Up2k(object):
abspath = djoin(ptop, rd, fn)
self.pp.msg = "c%d %s" % (nq, abspath)
if not mpool:
n_tags = self._tagscan_file(cur, entags, w, abspath, ip, at)
else:
if ip:
oth_tags = {"up_ip": ip, "up_at": at}
n_tags = self._tagscan_file(cur, entags, w, abspath, ip, at, un)
else:
oth_tags = {}
if ip:
oth_tags["up_ip"] = ip
if at:
oth_tags["up_at"] = at
if un:
oth_tags["up_by"] = un
mpool.put(Mpqe({}, entags, w, abspath, oth_tags))
with self.mutex:
@ -2331,8 +2354,8 @@ class Up2k(object):
if w in in_progress:
continue
q = "select rd, fn, ip, at from up where substr(w,1,16)=? limit 1"
rd, fn, ip, at = cur.execute(q, (w,)).fetchone()
q = "select rd, fn, ip, at, un from up where substr(w,1,16)=? limit 1"
rd, fn, ip, at, un = cur.execute(q, (w,)).fetchone()
rd, fn = s3dec(rd, fn)
abspath = djoin(ptop, rd, fn)
@ -2356,7 +2379,10 @@ class Up2k(object):
if ip:
oth_tags["up_ip"] = ip
if at:
oth_tags["up_at"] = at
if un:
oth_tags["up_by"] = un
jobs.append(Mpqe(parsers, set(), w, abspath, oth_tags))
in_progress[w] = True
@ -2545,6 +2571,7 @@ class Up2k(object):
abspath: str,
ip: str,
at: float,
un: Optional[str],
) -> int:
"""will mutex(main)"""
assert self.mtag # !rm
@ -2565,7 +2592,10 @@ class Up2k(object):
if ip:
tags["up_ip"] = ip
if at:
tags["up_at"] = at
if un:
tags["up_by"] = un
with self.mutex:
return self._tag_file(write_cur, entags, wark, abspath, tags)
@ -2669,16 +2699,19 @@ class Up2k(object):
if not existed and ver is None:
return self._try_create_db(db_path, cur)
if ver == 4:
for upver in (4, 5):
if ver != upver:
continue
try:
t = "creating backup before upgrade: "
cur = self._backup_db(db_path, cur, ver, t)
self._upgrade_v4(cur)
ver = 5
getattr(self, "_upgrade_v%d" % (upver,))(cur)
ver += 1 # type: ignore
except:
self.log("WARN: failed to upgrade from v4", 3)
self.log("WARN: failed to upgrade from v%d" % (ver,), 3)
if ver == DB_VER:
# these no longer serve their intended purpose but they're great as additional sanchks
self._add_dhash_tab(cur)
self._add_xiu_tab(cur)
self._add_cv_tab(cur)
@ -2736,7 +2769,7 @@ class Up2k(object):
cur.close()
db.close()
shutil.copy2(fsenc(db_path), fsenc(bak))
trystat_shutil_copy2(self.log, fsenc(db_path), fsenc(bak))
return self._orz(db_path)
def _read_ver(self, cur: "sqlite3.Cursor") -> Optional[int]:
@ -2780,7 +2813,7 @@ class Up2k(object):
idx = r"create index up_w on up(w)"
for cmd in [
r"create table up (w text, mt int, sz int, rd text, fn text, ip text, at int)",
r"create table up (w text, mt int, sz int, rd text, fn text, ip text, at int, un text)",
r"create index up_vp on up(rd, fn)",
r"create index up_fn on up(fn)",
r"create index up_ip on up(ip)",
@ -2813,6 +2846,15 @@ class Up2k(object):
cur.connection.commit()
def _upgrade_v5(self, cur: "sqlite3.Cursor") -> None:
for cmd in [
r"alter table up add column un text",
r"update kv set v=6 where k='sver'",
]:
cur.execute(cmd)
cur.connection.commit()
def _add_dhash_tab(self, cur: "sqlite3.Cursor") -> None:
# v5 -> v5a
try:
@ -3010,7 +3052,7 @@ class Up2k(object):
argv = [dwark[:16], dwark]
c2 = cur.execute(q, tuple(argv))
for _, dtime, dsize, dp_dir, dp_fn, ip, at in c2:
for _, dtime, dsize, dp_dir, dp_fn, ip, at, _ in c2:
if dp_dir.startswith("//") or dp_fn.startswith("//"):
dp_dir, dp_fn = s3dec(dp_dir, dp_fn)
@ -3102,7 +3144,7 @@ class Up2k(object):
for cur, dp_dir, dp_fn in lost:
t = "forgetting desynced db entry: %r"
self.log(t % ("/" + vjoin(vjoin(vfs.vpath, dp_dir), dp_fn)))
self.db_rm(cur, dp_dir, dp_fn, cj["size"])
self.db_rm(cur, vfs.flags, dp_dir, dp_fn, cj["size"])
if c2 and c2 != cur:
c2.connection.commit()
@ -3396,10 +3438,9 @@ class Up2k(object):
cur.connection.commit()
ap = djoin(job["ptop"], job["prel"], job["name"])
times = (int(time.time()), int(cj["lmod"]))
bos.utime(ap, times, False)
mt = bos.utime_c(self.log, ap, int(cj["lmod"]), False, True)
self.log("touched %r from %d to %d" % (ap, job["lmod"], cj["lmod"]))
self.log("touched %r from %d to %d" % (ap, job["lmod"], mt))
except Exception as ex:
self.log("umod failed, %r" % (ex,), 3)
@ -3432,8 +3473,8 @@ class Up2k(object):
try:
vrel = vjoin(job["prel"], fname)
xlink = bool(vf.get("xlink"))
cur, wark, _, _, _, _ = self._find_from_vpath(ptop, vrel)
self._forget_file(ptop, vrel, cur, wark, True, st.st_size, xlink)
cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, vrel)
self._forget_file(ptop, vrel, vf, cur, wark, True, st.st_size, xlink)
except Exception as ex:
self.log("skipping replace-relink: %r" % (ex,))
finally:
@ -3551,11 +3592,10 @@ class Up2k(object):
t = "BUG: no valid sources to link from! orig(%r) fsrc(%r) link(%r)"
self.log(t, 1)
raise Exception(t % (src, fsrc, dst))
shutil.copy2(fsenc(csrc), fsenc(dst))
trystat_shutil_copy2(self.log, fsenc(csrc), fsenc(dst))
if lmod and (not linked or SYMTIME):
times = (int(time.time()), int(lmod))
bos.utime(dst, times, False)
bos.utime_c(self.log, dst, int(lmod), False)
def handle_chunks(
self, ptop: str, wark: str, chashes: list[str]
@ -3728,10 +3768,8 @@ class Up2k(object):
times = (int(time.time()), int(job["lmod"]))
t = "no more chunks, setting times %s (%d) on %r"
self.log(t % (times, bos.path.getsize(dst), dst))
try:
bos.utime(dst, times)
except:
self.log("failed to utime (%r, %s)" % (dst, times))
bos.utime_c(self.log, dst, times[1], False)
# the above logmsg (and associated logic) is retained due to unforget.py
zs = "prel name lmod size ptop vtop wark dwrk host user addr"
z2 = [job[x] for x in zs.split()]
@ -3850,7 +3888,9 @@ class Up2k(object):
return True
def db_rm(self, db: "sqlite3.Cursor", rd: str, fn: str, sz: int) -> None:
def db_rm(
self, db: "sqlite3.Cursor", vflags: dict[str, Any], rd: str, fn: str, sz: int
) -> None:
sql = "delete from up where rd = ? and fn = ?"
try:
r = db.execute(sql, (rd, fn))
@ -3858,10 +3898,23 @@ class Up2k(object):
assert self.mem_cur # !rm
r = db.execute(sql, s3enc(self.mem_cur, rd, fn))
if r.rowcount:
if not r.rowcount:
return
self.volsize[db] -= sz
self.volnfiles[db] -= 1
if "nodirsz" not in vflags:
try:
q = "update ds set nf=nf-1, sz=sz-? where rd=?"
while True:
db.execute(q, (sz, rd))
if not rd:
break
rd = rd.rsplit("/", 1)[0] if "/" in rd else ""
except:
pass
def db_add(
self,
db: "sqlite3.Cursor",
@ -3881,7 +3934,7 @@ class Up2k(object):
skip_xau: bool = False,
) -> None:
"""mutex(main) me"""
self.db_rm(db, rd, fn, sz)
self.db_rm(db, vflags, rd, fn, sz)
if not ip:
db_ip = ""
@ -3889,14 +3942,14 @@ class Up2k(object):
# plugins may expect this to look like an actual 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))
sql = "insert into up values (?,?,?,?,?,?,?,?)"
v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0), usr)
try:
db.execute(sql, v)
except:
assert self.mem_cur # !rm
rd, fn = s3enc(self.mem_cur, rd, fn)
v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0))
v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0), usr)
db.execute(sql, v)
self.volsize[db] += sz
@ -3988,6 +4041,9 @@ class Up2k(object):
except:
pass
def handle_fs_abrt(self, akey: str) -> None:
self.abrt_key = akey
def handle_rm(
self,
uname: str,
@ -4034,7 +4090,7 @@ class Up2k(object):
vn, rem = vn0.get_dbv(rem0)
ptop = vn.realpath
with self.mutex, self.reg_mutex:
abrt_cfg = self.flags.get(ptop, {}).get("u2abort", 1)
abrt_cfg = vn.flags.get("u2abort", 1)
addr = (ip or "\n") if abrt_cfg in (1, 2) else ""
user = ((uname or "\n"), "*") if abrt_cfg in (1, 3) else None
reg = self.registry.get(ptop, {}) if abrt_cfg else {}
@ -4055,17 +4111,22 @@ class Up2k(object):
if partial:
dip = ip
dat = time.time()
dun = uname
un_cfg = 1
else:
if not self.args.unpost:
un_cfg = vn.flags["unp_who"]
if not self.args.unpost or not un_cfg:
t = "the unpost feature is disabled in server config"
raise Pebkac(400, t)
_, _, _, _, dip, dat = self._find_from_vpath(ptop, rem)
_, _, _, _, dip, dat, dun = self._find_from_vpath(ptop, rem)
t = "you cannot delete this: "
if not dip:
t += "file not found"
elif dip != ip:
elif dip != ip and un_cfg in (1, 2):
t += "not uploaded by (You)"
elif dun != uname and un_cfg in (1, 3):
t += "not uploaded by (You)"
elif dat < time.time() - self.args.unpost:
t += "uploaded too long ago"
@ -4154,9 +4215,9 @@ class Up2k(object):
try:
ptop = dbv.realpath
xlink = bool(dbv.flags.get("xlink"))
cur, wark, _, _, _, _ = self._find_from_vpath(ptop, volpath)
cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, volpath)
self._forget_file(
ptop, volpath, cur, wark, True, st.st_size, xlink
ptop, volpath, dbv.flags, cur, wark, True, st.st_size, xlink
)
finally:
if cur:
@ -4197,7 +4258,7 @@ class Up2k(object):
return n_files, ok + ok2, ng + ng2
def handle_cp(self, uname: str, ip: str, svp: str, dvp: str) -> str:
def handle_cp(self, abrt: str, uname: str, ip: str, svp: str, dvp: str) -> str:
if svp == dvp or dvp.startswith(svp + "/"):
raise Pebkac(400, "cp: cannot copy parent into subfolder")
@ -4244,6 +4305,8 @@ class Up2k(object):
dvpf = dvp + svpf[len(svp) :]
self._cp_file(uname, ip, svpf, dvpf, curs)
if abrt and abrt == self.abrt_key:
raise Pebkac(400, "filecopy aborted by http-api")
for v in curs:
v.connection.commit()
@ -4313,7 +4376,7 @@ class Up2k(object):
bos.makedirs(os.path.dirname(dabs), vf=dvn.flags)
c1, w, ftime_, fsize_, ip, at = self._find_from_vpath(
c1, w, ftime_, fsize_, ip, at, un = self._find_from_vpath(
svn_dbv.realpath, srem_dbv
)
c2 = self.cur.get(dvn.realpath)
@ -4338,7 +4401,7 @@ class Up2k(object):
w,
w,
"",
"",
un or "",
ip or "",
at or 0,
)
@ -4365,7 +4428,7 @@ class Up2k(object):
b1, b2 = fsenc(sabs), fsenc(dabs)
is_link = os.path.islink(b1) # due to _relink
try:
shutil.copy2(b1, b2)
trystat_shutil_copy2(self.log, b1, b2)
except:
try:
wunlink(self.log, dabs, dvn.flags)
@ -4411,7 +4474,7 @@ class Up2k(object):
return "k"
def handle_mv(self, uname: str, ip: str, svp: str, dvp: str) -> str:
def handle_mv(self, abrt: str, uname: str, ip: str, svp: str, dvp: str) -> str:
if svp == dvp or dvp.startswith(svp + "/"):
raise Pebkac(400, "mv: cannot move parent into subfolder")
@ -4466,6 +4529,8 @@ class Up2k(object):
dvpf = dvp + svpf[len(svp) :]
self._mv_file(uname, ip, svpf, dvpf, curs)
if abrt and abrt == self.abrt_key:
raise Pebkac(400, "filemove aborted by http-api")
for v in curs:
v.connection.commit()
@ -4597,7 +4662,7 @@ class Up2k(object):
return "k"
c1, w, ftime_, fsize_, ip, at = self._find_from_vpath(svn.realpath, srem)
c1, w, ftime_, fsize_, ip, at, un = self._find_from_vpath(svn.realpath, srem)
c2 = self.cur.get(dvn.realpath)
has_dupes = False
@ -4610,7 +4675,14 @@ class Up2k(object):
with self.reg_mutex:
has_dupes = self._forget_file(
svn.realpath, srem, c1, w, is_xvol, fsize_ or fsize, xlink
svn.realpath,
srem,
svn.flags,
c1,
w,
is_xvol,
fsize_ or fsize,
xlink,
)
if not is_xvol:
@ -4631,7 +4703,7 @@ class Up2k(object):
w,
w,
"",
"",
un or "",
ip or "",
at or 0,
)
@ -4662,7 +4734,7 @@ class Up2k(object):
b1, b2 = fsenc(sabs), fsenc(dabs)
is_link = os.path.islink(b1) # due to _relink
try:
shutil.copy2(b1, b2)
trystat_shutil_copy2(self.log, b1, b2)
except:
try:
wunlink(self.log, dabs, dvn.flags)
@ -4731,13 +4803,14 @@ class Up2k(object):
Optional[int],
str,
Optional[int],
str,
]:
cur = self.cur.get(ptop)
if not cur:
return 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"
q = "select w, mt, sz, ip, at, un from up where rd=? and fn=? limit 1"
try:
c = cur.execute(q, (rd, fn))
except:
@ -4746,14 +4819,15 @@ class Up2k(object):
hit = c.fetchone()
if hit:
wark, ftime, fsize, ip, at = hit
return cur, wark, ftime, fsize, ip, at
return cur, None, None, None, "", None
wark, ftime, fsize, ip, at, un = hit
return cur, wark, ftime, fsize, ip, at, un
return cur, None, None, None, "", None, ""
def _forget_file(
self,
ptop: str,
vrem: str,
vflags: dict[str, Any],
cur: Optional["sqlite3.Cursor"],
wark: Optional[str],
drop_tags: bool,
@ -4778,7 +4852,7 @@ class Up2k(object):
q = "delete from mt where w=?"
cur.execute(q, (wark[:16],))
self.db_rm(cur, srd, sfn, sz)
self.db_rm(cur, vflags, srd, sfn, sz)
reg = self.registry.get(ptop)
if reg:
@ -4867,7 +4941,10 @@ class Up2k(object):
mt = bos.path.getmtime(slabs, False)
flags = self.flags.get(ptop) or {}
atomic_move(self.log, sabs, slabs, flags)
try:
bos.utime(slabs, (int(time.time()), int(mt)), False)
except:
self.log("relink: failed to utime(%r, %s)" % (slabs, mt), 3)
self._symlink(slabs, sabs, flags, False, is_mv=True)
full[slabs] = (ptop, rem)
sabs = slabs

View file

@ -52,6 +52,7 @@ from .__init__ import (
VT100,
WINDOWS,
EnvParams,
unicode,
)
from .__version__ import S_BUILD_DT, S_VERSION
from .stolen import surrogateescape
@ -112,7 +113,13 @@ E_ACCESS = _ens("EACCES WSAEACCES")
E_UNREACH = _ens("EHOSTUNREACH WSAEHOSTUNREACH ENETUNREACH WSAENETUNREACH")
IP6ALL = "0:0:0:0:0:0:0:0"
IP6_LL = ("fe8", "fe9", "fea", "feb")
IP64_LL = ("fe8", "fe9", "fea", "feb", "169.254")
UC_CDISP = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._"
BC_CDISP = UC_CDISP.encode("ascii")
UC_CDISP_SET = set(UC_CDISP)
BC_CDISP_SET = set(BC_CDISP)
try:
import fcntl
@ -399,6 +406,9 @@ application swf=x-shockwave-flash m3u=vnd.apple.mpegurl db3=vnd.sqlite3 sqlite=v
text ass=plain ssa=plain
image jpg=jpeg xpm=x-xpixmap psd=vnd.adobe.photoshop jpf=jpx tif=tiff ico=x-icon djvu=vnd.djvu
image heic=heic-sequence heif=heif-sequence hdr=vnd.radiance svg=svg+xml
image arw=x-sony-arw cr2=x-canon-cr2 crw=x-canon-crw dcr=x-kodak-dcr dng=x-adobe-dng erf=x-epson-erf
image k25=x-kodak-k25 kdc=x-kodak-kdc mrw=x-minolta-mrw nef=x-nikon-nef orf=x-olympus-orf
image pef=x-pentax-pef raf=x-fuji-raf raw=x-panasonic-raw sr2=x-sony-sr2 srf=x-sony-srf x3f=x-sigma-x3f
audio caf=x-caf mp3=mpeg m4a=mp4 mid=midi mpc=musepack aif=aiff au=basic qcp=qcelp
video mkv=x-matroska mov=quicktime avi=x-msvideo m4v=x-m4v ts=mp2t
video asf=x-ms-asf flv=x-flv 3gp=3gpp 3g2=3gpp2 rmvb=vnd.rn-realmedia-vbr
@ -2068,6 +2078,29 @@ def gencookie(
)
def gen_content_disposition(fn: str) -> str:
safe = UC_CDISP_SET
bsafe = BC_CDISP_SET
fn = fn.replace("/", "_").replace("\\", "_")
zb = fn.encode("utf-8", "xmlcharrefreplace")
if not PY2:
zbl = [
chr(x).encode("utf-8")
if x in bsafe
else "%{:02X}".format(x).encode("ascii")
for x in zb
]
else:
zbl = [unicode(x) if x in bsafe else "%{:02X}".format(ord(x)) for x in zb]
ufn = b"".join(zbl).decode("ascii")
afn = "".join([x if x in safe else "_" for x in fn]).lstrip(".")
while ".." in afn:
afn = afn.replace("..", ".")
return "attachment; filename=\"%s\"; filename*=UTF-8''%s" % (afn, ufn)
def humansize(sz: float, terse: bool = False) -> str:
for unit in HUMANSIZE_UNITS:
if sz < 1024:
@ -2606,6 +2639,24 @@ def set_fperms(f: Union[typing.BinaryIO, typing.IO[Any]], vf: dict[str, Any]) ->
os.fchown(fno, vf["uid"], vf["gid"])
def trystat_shutil_copy2(log: "NamedLogger", src: bytes, dst: bytes) -> bytes:
try:
return shutil.copy2(src, dst)
except:
# ignore failed mtime on linux+ntfs; for example:
# shutil.py:437 <copy2>: copystat(src, dst, follow_symlinks=follow_symlinks)
# shutil.py:376 <copystat>: lookup("utime")(dst, ns=(st.st_atime_ns, st.st_mtime_ns),
# [PermissionError] [Errno 1] Operation not permitted, '/windows/_videos'
_, _, tb = sys.exc_info()
for _, _, fun, _ in traceback.extract_tb(tb):
if fun == "copystat":
if log:
t = "warning: failed to retain some file attributes (timestamp and/or permissions) during copy from %r to %r:\n%s"
log(t % (src, dst, min_ex()), 3)
return dst # close enough
raise
def _fs_mvrm(
log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any]
) -> bool:
@ -2951,6 +3002,27 @@ def load_ipu(
return ip_u, nm
def load_ipr(
log: "RootLogger", iprs: list[str], defer_mutex: bool = False
) -> dict[str, NetMap]:
ret = {}
for ipr in iprs:
try:
zs, uname = ipr.split("=")
cidrs = zs.split(",")
except:
t = "\n invalid value %r for argument --ipr; must be CIDR[,CIDR[,...]]=UNAME (192.168.0.0/16=amelia)"
raise Exception(t % (ipr,))
try:
nm = NetMap(["::"], cidrs, True, True, defer_mutex)
except Exception as ex:
t = "failed to translate --ipr into netmap, probably due to invalid config: %r"
log("root", t % (ex,), 1)
raise
ret[uname] = nm
return ret
def yieldfile(fn: str, bufsz: int) -> Generator[bytes, None, None]:
readsz = min(bufsz, 128 * 1024)
with open(fsenc(fn), "rb", bufsz) as f:
@ -3135,8 +3207,9 @@ def statdir(
else:
src = "listdir"
fun: Any = os.lstat if lstat else os.stat
btop_ = os.path.join(btop, b"")
for name in os.listdir(btop):
abspath = os.path.join(btop, name)
abspath = btop_ + name
try:
yield (fsdec(name), fun(abspath))
except Exception as ex:
@ -3175,7 +3248,9 @@ def rmdirs(
stats = statdir(logger, scandir, lstat, top, False)
dirs = [x[0] for x in stats if stat.S_ISDIR(x[1].st_mode)]
dirs = [os.path.join(top, x) for x in dirs]
if dirs:
top_ = os.path.join(top, "")
dirs = [top_ + x for x in dirs]
ok = []
ng = []
for d in reversed(dirs):
@ -3578,7 +3653,7 @@ def runihook(
verbose: bool,
cmd: str,
vol: "VFS",
ups: list[tuple[str, int, int, str, str, str, int]],
ups: list[tuple[str, int, int, str, str, str, int, str]],
) -> bool:
_, chk, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd)
bcmd = [sfsenc(x) for x in acmd]
@ -4151,7 +4226,7 @@ def _pkg_resource_exists(pkg: str, name: str) -> bool:
def stat_resource(E: EnvParams, name: str):
path = os.path.join(E.mod, name)
path = E.mod_ + name
if os.path.exists(path):
return os.stat(fsenc(path))
return None
@ -4198,7 +4273,7 @@ def _has_resource(name: str):
def has_resource(E: EnvParams, name: str):
return _has_resource(name) or os.path.exists(os.path.join(E.mod, name))
return _has_resource(name) or os.path.exists(E.mod_ + name)
def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]:
@ -4223,7 +4298,7 @@ def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]:
stream = codecs.getreader(enc)(stream)
return stream
ap = os.path.join(E.mod, name)
ap = E.mod_ + name
if PY2:
return codecs.open(ap, "r", encoding=enc) # type: ignore

View file

@ -44,10 +44,12 @@ window.baguetteBox = (function () {
loopA = null,
loopB = null,
url_ts = null,
un_pp = 0,
resume_mp = false;
var onFSC = function (e) {
isFullscreen = !!document.fullscreenElement;
clmod(document.documentElement, 'bb_fsc', isFullscreen);
};
var overlayClickHandler = function (e) {
@ -277,23 +279,30 @@ window.baguetteBox = (function () {
if (modal.busy)
return;
if (e.key == '?')
return halp();
if (anymod(e, true))
return;
var k = (e.code || e.key) + '', v = vid(), pos = -1;
var k = (e.key || e.code) + '', v = vid();
if (k == "BracketLeft")
if (k.startsWith('Key'))
k = k.slice(3);
else if (k.startsWith('Digit'))
k = k.slice(5);
var kl = k.toLowerCase();
if (k == '?')
return halp();
if (k == "[" || k == "BracketLeft")
setloop(1);
else if (k == "BracketRight")
else if (k == "]" || k == "BracketRight")
setloop(2);
else if (e.shiftKey && k != "KeyR" && k != "R")
else if (e.shiftKey && kl != "r")
return;
else if (k == "ArrowLeft" || k == "KeyJ" || k == "Left" || k == "j")
else if (k == "ArrowLeft" || k == "Left" || kl == "j")
showPreviousImage();
else if (k == "ArrowRight" || k == "KeyL" || k == "Right" || k == "l")
else if (k == "ArrowRight" || k == "Right" || kl == "l")
showNextImage();
else if (k == "Escape" || k == "Esc")
hideOverlay();
@ -301,34 +310,37 @@ window.baguetteBox = (function () {
showFirstImage(e);
else if (k == "End")
showLastImage(e);
else if (k == "Space" || k == "KeyP" || k == "KeyK")
else if (k == "Space" || k == "Spacebar" || kl == " " || kl == "p" || kl == "k")
playpause();
else if (k == "KeyU" || k == "KeyO")
relseek(k == "KeyU" ? -10 : 10);
else if (k.indexOf('Digit') === 0 && v)
v.currentTime = v.duration * parseInt(k.slice(-1)) * 0.1;
else if (k == "KeyM" && v) {
else if (kl == "u" || kl == "o")
relseek(kl == "u" ? -10 : 10);
else if (v && /^[0-9]$/.test(k))
v.currentTime = v.duration * parseInt(k) * 0.1;
else if (kl == "m" && v) {
v.muted = vmute = !vmute;
mp_ctl();
}
else if (k == "KeyV" && v) {
else if (kl == "v" && v) {
vloop = !vloop;
vnext = vnext && !vloop;
setVmode();
}
else if (k == "KeyC" && v) {
else if (kl == "c" && v) {
vnext = !vnext;
vloop = vloop && !vnext;
setVmode();
}
else if (k == "KeyF")
else if (kl == "f")
tglfull();
else if (k == "KeyS" || k == "s")
else if (kl == "s")
tglsel();
else if (k == "KeyR" || k == "r" || k == "R")
else if (kl == "r")
rotn(e.shiftKey ? -1 : 1);
else if (k == "KeyY")
else if (kl == "y")
dlpic();
else
return;
return ev(e);
}
function anim() {
@ -402,7 +414,7 @@ window.baguetteBox = (function () {
if (isFullscreen)
document.exitFullscreen();
else
(vid() || ebi('bbox-overlay')).requestFullscreen();
ebi('bbox-overlay').requestFullscreen();
}
catch (ex) {
if (IPHONE)
@ -449,10 +461,12 @@ window.baguetteBox = (function () {
if (anymod(e))
return;
var k = e.code + '';
var k = (e.key || e.code) + '';
if (k == "Space")
ev(e);
if (k == "Space" || k == "Spacebar" || k == " ") {
un_pp = Date.now();
return ev(e);
}
}
var passiveSupp = false;
@ -777,8 +791,7 @@ window.baguetteBox = (function () {
image.setAttribute('playsinline', '1');
// ios ignores poster
image.onended = vidEnd;
image.onplay = function () { show_buttons(1); };
image.onpause = function () { show_buttons(); };
image.onplay = image.onpause = ppHandler;
}
image.alt = thumbnailElement ? thumbnailElement.alt || '' : '';
if (options.titleTag && imageCaption)
@ -793,6 +806,15 @@ window.baguetteBox = (function () {
callback();
}
function ppHandler() {
var now = Date.now();
if (now - un_pp < 50) {
un_pp = 0;
return playpause(); // browser undid space hotkey
}
show_buttons(this.paused ? 1 : 0);
}
function showNextImage(e) {
ev(e);
return show(currentIndex + 1);

View file

@ -571,6 +571,7 @@ pre, code, tt, #doc, #doc>code {
overflow: hidden;
width: 0;
height: 0;
left: -10em;
color: var(--bg);
}
html .ayjump:focus {
@ -881,6 +882,9 @@ html.y #path a:hover {
#flogout {
display: inline;
}
html.dz #flogout {
margin-left: 1em;
}
#goh+span {
color: var(--bg-u5);
padding-left: .5em;
@ -1388,6 +1392,7 @@ html.y #ops svg circle {
.opview select {
padding: .3em;
margin: .2em .4em;
background: var(--bg-u3);
}
.opview input.err {
color: var(--err-fg);
@ -1553,11 +1558,13 @@ html {
#treepar {
z-index: 1;
position: fixed;
background: #fff;
background: var(--tree-bg);
left: -.96em;
width: calc(.3em + var(--nav-sz) - var(--sbw));
border-bottom: 1px solid var(--bg-u5);
overflow: hidden;
border-right: .5em solid #999\9;
}
#treepar.off {
display: none;
@ -1930,6 +1937,11 @@ html.y #tree.nowrap .ntree a+a:hover {
padding: 0;
font-size: 1.5em;
}
#fs_abrt {
margin-top: 1em;
text-shadow: 0;
box-shadow: 1px 1px 0 var(--bg-d3);
}
#doc {
overflow: visible;
background: #fff;
@ -1988,6 +2000,7 @@ html.y #doc .line-highlight {
}
#seldoc.sel {
color: var(--fg2-max);
background: #f0f;
background: var(--g-sel-b1);
}
#pvol,
@ -2007,6 +2020,7 @@ a.btn,
user-select: none;
}
#hkhelp {
background: #fff;
background: var(--bg);
}
#hkhelp table {
@ -2120,6 +2134,13 @@ html.noscroll .sbar::-webkit-scrollbar {
vertical-align: middle;
transition: transform .23s, left .23s, top .23s, width .23s, height .23s;
}
html.bb_fsc .full-image img,
html.bb_fsc .full-image video {
max-height: 100%;
}
html.bb_fsc figcaption {
display: none;
}
.full-image img.nt,
.full-image video.nt {
transition: none;
@ -3272,3 +3293,793 @@ html.d #treepar {
transition: background-color .3s ease, color .3s ease;
}
}
html.ey {
--negative-space: 0em; /* Use this to change the global spacing of the 95 theme */
--font-main: consolas;
--font-serif: consolas;
--font-mono: consolas;
--w: #fff;
--w2: #dfdfdf;
--w3: grey;
--fg: #000;
--fg-max: #0000ff;
--fg-weak: #0000ff;
--bg: #c6c3c6;
--bg-d3: #ff0;
--bg-d2: var(--w3);
--bg-d1: var(--bg);
--bg-u2: var(--bg);
--bg-u3: var(--bg);
--bg-u5: var(--shadow-color-2);
--tab-alt: #00f;
--g-fsel-bg: #00f;
--g-sel-bg: #00f;
--g-fsel-b1: #fff;
--row-alt: var(--w);
--scroll: var(--silver);
--f-sel-sh: transparent;
--a: #000;
--a-b: #fff;
--a-hil: #fff;
--a-h-bg: var(--bg);
--a-dark: var(--a);
--a-gray: var(--fg-weak);
--btn-fg: var(--fg);
--btn-bg: var(--bg);
--btn-h-fg: var(--fg);
--btn-h-bg: var(--bg);
--btn-1-fg: var(--fg);
--btn-1-bg: var(--bg);
--btn-1h-bg: var(--bg-d3);
--txt-sh: a;
--txt-bg: var(--white);
--u2-b1-bg: var(--w2);
--u2-b2-bg: var(--w2);
--u2-txt-bg: var(--w2);
--u2-tab-bg: a;
--u2-tab-1-bg: var(--w2);
--sort-1: var(--fg-weak);
--tree-bg: var(--w);
--g-b1: a;
--g-b2: a;
--g-f-bg: var(--w2);
--f-sh1: 0.1;
--f-sh2: 0.02;
--f-sh3: 0.1;
--f-h-b1: a;
--srv-1: var(--w);
--srv-3: var(--a);
--mp-sh: a;
--black: #000;
--white: #fff;
--grey: grey;
--silver: silver;
--transparent: transparent;
--shadow-color-1: #0a0a0a;
--shadow-color-2: #808080;
--border-dashed-black: 1px dashed var(--black);
--radius: 0;
--focus-outline: 1px dashed var(--black);
--hover-outline: 1px dotted var(--black);
--fm-off: var(--w3);
--ttlbar: linear-gradient(90deg, navy, #1084d0);
--inset-bg: var(--white);
--scroll-bkg: var(--white);
/*All sides*/
--shadow-outset: inset -1px -1px var(--shadow-color-1),
inset 1px 1px var(--white), inset -2px -2px var(--grey),
inset 2px 2px var(--w2);
--shadow-inset: inset -1px -1px var(--white),
inset 1px 1px var(--shadow-color-1), inset -2px -2px var(--w2),
inset 2px 2px var(--shadow-color-2);
--shadow-input: inset -1px -1px var(--white), inset 1px 1px var(--grey),
inset -2px -2px var(--w2), inset 2px 2px var(--shadow-color-1);
/*Indiv sides*/
--shadow-outset-bottom: inset 0 -1px var(--shadow-color-1),
inset 0 -2px var(--grey);
--shadow-outset-right: inset -1px 0 var(--shadow-color-1),
inset -2px 0 var(--grey);
--shadow-outset-left: inset 1px 0 var(--white), inset 2px 0 var(--w2);
--shadow-outset-top: inset 0 1px var(--white), inset 0 2px var(--w2);
--shadow-inset-bottom: inset 0 -1px var(--white), inset 0 -2px var(--w2);
--shadow-inset-right: inset -1px 0 var(--white), inset -2px 0 var(--w2);
--shadow-inset-left: inset 1px 0 var(--shadow-color-1),
inset 2px 0 var(--shadow-color-2);
--shadow-inset-top: inset 0 1px var(--shadow-color-1),
inset 0 2px var(--shadow-color-2);
}
html.ez {
--negative-space: 0em; /* Use this to change the global spacing of your theme :) */
--font-main: consolas;
--font-serif: consolas;
--font-mono: consolas;
--w: #fff;
--w2: var(--inset-bg);
--w3: grey;
--fg: #cfcfcf;
--fg-max: #47b8ff;
--fg-weak: #47b8ff;
--bg: #383838;
--bg-d3: #600000;
--bg-d2: var(--shadow-color-1);
--bg-d1: var(--bg);
--u2-tab-1-fg: #ff0;
--bg-u2: var(--bg);
--bg-u3: var(--bg);
--bg-u5: var(--shadow-color-2);
--tab-alt: #47b8ff;
--g-fsel-bg: #0000b7;
--g-sel-bg: #00f;
--g-fsel-b1: #fff;
--row-alt: #555555;
--scroll: #555555;
--f-sel-sh: transparent;
--a: var(--fg);
--a-b: var(--fg);
--a-hil: var(--fg);
--btn-1h-bg: var(--bg-d3);
--a-h-bg: var(--bg);
--a-dark: var(--a);
--a-gray: var(--fg-weak);
--btn-fg: var(--white);
--btn-bg: var(--bg);
--btn-h-fg: var(--white);
--btn-h-bg: var(--bg);
--btn-1-fg: var(--white);
--btn-1-bg: var(--bg);
--txt-sh: a;
--u2-b1-bg: var(--w2);
--u2-b2-bg: var(--w2);
--u2-txt-bg: var(--w2);
--u2-tab-bg: a;
--u2-tab-1-bg: var(--w2);
--sort-1: var(--fg-weak);
--g-b1: a;
--g-b2: a;
--g-f-bg: var(--w2);
--f-sh1: 0.1;
--f-sh2: 0.02;
--f-sh3: 0.1;
--f-h-b1: a;
--srv-1: var(--w);
--srv-3: var(--a);
--mp-sh: a;
--black: #000;
--white: #fff;
--grey: grey;
--silver: #858585;
--transparent: transparent;
--shadow-color-1: #101010;
--shadow-color-2: #1f1f1f;
--border-dashed-black: 1px dashed var(--shadow-color-1);
--radius: 0;
--focus-outline: 1px dashed var(--white);
--hover-outline: 1px dotted var(--white);
--fm-off: var(--w3);
--ttlbar: linear-gradient(90deg, var(--shadow-color-1) 20%, #888888);
--inset-bg: #3f3f3f;
--tree-bg: var(--inset-bg);
--txt-bg: var(--inset-bg);
--scroll-bkg: var(--black);
/*All sides*/
--shadow-outset: inset -1px -1px var(--shadow-color-1), inset 1px 1px #878787,
inset -2px -2px var(--shadow-color-2), inset 2px 2px #575757;
--shadow-inset: inset -1px -1px #878787, inset 1px 1px var(--shadow-color-1),
inset -2px -2px #575757, inset 2px 2px var(--shadow-color-2);
--shadow-input: inset -1px -1px var(--white),
inset 1px 1px var(--shadow-color-2), inset -2px -2px #575757,
inset 2px 2px var(--shadow-color-1);
--shadow-outset-bottom: inset 0 -1px var(--shadow-color-1),
inset 0 -2px var(--shadow-color-2);
--shadow-outset-right: inset -1px 0 var(--shadow-color-1),
inset -2px 0 var(--shadow-color-2);
--shadow-outset-left: inset 1px 0 #878787, inset 2px 0 #575757;
--shadow-outset-top: inset 0 1px #878787, inset 0 2px #575757;
--shadow-inset-bottom: inset 0 -1px #878787, inset 0 -2px #575757;
--shadow-inset-right: inset -1px 0 #878787, inset -2px 0 #575757;
--shadow-inset-left: inset 1px 0 var(--shadow-color-1),
inset 2px 0 var(--shadow-color-2);
--shadow-inset-top: inset 0 1px var(--shadow-color-1),
inset 0 2px var(--shadow-color-2);
}
html.e {
text-shadow: none;
}
html.e #files,
html.e #u2conf input[type="checkbox"]:hover + label,
html.e .tgl.btn.on:hover,
html.e body {
background: var(--bg);
}
html.e #pctl a,
html.e #repl,
html.e #u2conf a,
html.e #u2conf input[type="checkbox"] + label,
html.e #wfp a,
html.e .btn,
html.e .eq_step,
html.e input[type="submit"] {
box-shadow: var(--shadow-outset);
border-radius: var(--radius);
background: var(--bg);
border: 0;
}
a.s0r,
html.e #ghead a.s0,
html.e #u2conf input[type="checkbox"]:checked + label,
html.e .tgl.btn.on,
html.e input[type="submit"]:active {
box-shadow: var(--shadow-inset) !important;
}
html.e #ops a:hover,
html.e #pctl a:hover,
html.e #repl:hover,
html.e #u2conf a:hover,
html.e #u2conf input[type="checkbox"]:hover + label,
html.e #wfp a:hover,
html.e .btn:hover,
html.e .eq_step:hover,
html.e input[type="submit"]:hover {
outline: var(--hover-outline);
outline-offset: -4px;
}
html.e .ntree a:hover,
html.e :focus,
html.e :focus + label,
html.e a:active,
html.e tr:focus,
input[type="text"]:focus {
outline: var(--focus-outline) !important;
}
html.e tr:focus {
box-shadow: none;
}
html.e #pctl a:focus,
html.e #repl:hover,
html.e #u2conf input[type="checkbox"]:focus + label,
html.e #wfp a:focus,
html.e .btn:focus,
html.e .eq_step:focus {
border: 0 !important;
outline: var(--focus-outline) !important;
outline-offset: 2px;
box-shadow: var(--shadow-outset) !important;
}
html.e #files tbody,
html.e #u2cards a.act {
box-shadow: var(--shadow-inset);
}
html.e #files {
border: 2px groove var(--transparent);
box-sizing: border-box;
width: 100%;
padding: 0.3em;
top: 0;
border: 0;
}
html.e #files tbody tr td,
html.e #files thead th {
border-radius: var(--radius);
}
#files td {
background: var(--w2);
}
html.e #files tr {
background-color: var(--black);
}
html.e #srv_info span,
html.e label {
color: var(--btn-fg) !important;
}
html.e #acc_info {
background: var(--transparent);
color: var(--white);
height: 2em;
left: 1em;
width: fit-content;
}
html.e #acc_info,
html.e #ops,
html.e #srv_info {
display: flex;
align-items: center;
}
html.e #acc_info span.warn,
html.e #acc_info a {
color: var(--white);
}
html.e #flogout:before {
padding-left: 0.2em;
padding-right: 0.4em;
content: " | ";
}
html.e #blogout {
color: var(--w);
box-shadow: none;
background: transparent;
}
html.e .opwide > div {
border-left: 1px solid var(--fg);
}
html.e #srv_info {
background: var(--transparent);
color: var(--white);
height: fit-content;
top: 3.2em;
left: 1em;
gap: 0.2em;
}
html.e #u2cards a.act {
padding: 0.2em 1em;
}
html.e #u2btn {
border: var(--border-dashed-black);
border-radius: var(--border-radius);
transform: translateY(30%);
}
html.e #ops,
html.e #ops a {
border-radius: var(--radius);
}
@media only screen and (max-width: 600px) {
html.e #acc_info {
background: var(--transparent);
color: var(--white);
height: fit-content;
align-items: center;
top: 3.2em;
right: 1em;
left: auto;
display: flex;
gap: 0.2em;
}
html.e #u2btn {
transform: none;
}
}
html.e #ops {
background: var(--ttlbar);
/*HC*/
box-shadow: inset 0-1px grey, inset 0-2px var(--shadow-color-1);
height: 2em;
gap: 0.6em;
padding: 0.2em;
flex-direction: row-reverse;
margin-bottom: 1.2em;
}
html.e #srch_form,
html.e .opbox {
padding-bottom: 1em;
padding-top: 1em;
max-width: 100vw;
}
html.e #ghead,
html.e #ops a {
align-items: center;
display: flex;
}
html.e #ops a {
text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5);
height: 1.4em;
padding: 0;
box-shadow: var(--shadow-outset);
background: var(--bg);
aspect-ratio: 1/1;
justify-content: center;
font-size: 1.25em;
z-index: 4;
}
html.e #blogout:focus,
html.e #ops a:focus {
outline: 1px dashed var(--w) !important;
}
html.e #blogout:hover {
text-decoration: underline;
}
html.e #ops > a:not(:first-child).act {
height: 1.4em;
width: 1.4em;
padding-bottom: 0.3em;
margin-top: 0.3em;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
box-shadow: var(--shadow-inset-left), var(--shadow-inset-top),
var(--shadow-inset-right);
z-index: 6;
}
html.e #ops a.act {
box-shadow: var(--shadow-inset);
border-bottom: 0;
}
html.e a:active {
border: 0;
}
html.e :focus,
html.e :focus + label {
border: 0 !important;
outline-offset: 1px;
border-radius: var(--radius) !important;
box-shadow: inherit;
}
html.e #opa_x {
text-shadow: 0 0 0 var(--transparent) !important;
color: var(--bg) !important;
display: flex;
}
html.e #opa_x:before {
content: "";
color: var(--fg) !important;
margin-top: -0.1em;
font-size: 1.75em;
position: absolute;
}
html.e .opbox {
margin: -1.2em 0 0;
box-shadow: var(--shadow-inset-bottom), var(--shadow-inset-left),
var(--shadow-inset-right);
border-radius: var(--radius);
z-index: 5;
background: var(--bg);
}
html.e #srch_form {
margin: 0;
border-radius: var(--radius);
}
html.e #op_unpost {
max-width: 100vw;
margin: 0;
}
html.e label:focus {
box-shadow: 0 0;
}
html.e #tree {
box-shadow: none;
padding-right: 5px;
}
html.e #tt {
background: var(--w2);
}
html.e .mdo a {
background: 0 0;
text-decoration: underline;
}
html.e .mdo code,
html.e .mdo pre {
color: var(--white);
background: var(--w2);
border: 0;
}
html.e .mdo h1,
html.e .mdo h2 {
background: 0 0;
border-color: var(--w2);
}
html.e #tt,
html.e .mdo ol ol,
html.e .mdo ol ul,
html.e .mdo ul ol,
html.e .mdo ul ul {
border-color: var(--w2);
}
html.e .mdo li > em,
html.e .mdo p > em,
html.e .mdo td > em {
color: #fd0;
}
html.e input.txtbox,
html.e input[type="text"],
html.e select {
background-color: var(--txt-bg);
box-shadow: var(--shadow-input) !important;
box-sizing: border-box;
padding: 3px 4px;
border-radius: var(--radius);
border: 0;
}
html.e #gfiles {
box-shadow: var(--shadow-outset);
background: var(--bg);
padding: 0.4em;
display: flex;
flex-direction: column;
gap: 0.3em;
}
html.e #ggrid {
background-color: var(--inset-bg);
box-shadow: var(--shadow-input);
padding: 1.5em;
margin: 0;
overflow-x: scroll;
}
html.e #ghead {
margin: 0;
justify-content: flex-end;
gap: 0.4em;
padding: 0;
overflow: auto;
top: 0px;
border-radius: 0px;
}
html.e #ghead a {
margin: 0;
border-radius: var(--radius);
}
html.e ::-webkit-scrollbar,
html.e::-webkit-scrollbar {
width: 16px !important;
height: 16px !important;
background: var(--transparent) !important;
}
html.e ::-webkit-scrollbar-button,
html.e ::-webkit-scrollbar-thumb,
html.e::-webkit-scrollbar-button,
html.e::-webkit-scrollbar-thumb {
width: 16px !important;
height: 16px !important;
background: var(--scroll) !important;
/*HC*/
box-shadow: var(--shadow-outset);
border: 1px solid !important;
border-color: var(--silver) var(--black) var(--black) var(--silver) !important;
}
html.e ::-webkit-scrollbar-track,
html.e::-webkit-scrollbar-track {
image-rendering: optimize-contrast !important;
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAyIDIiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyI+CjxtZXRhZGF0YT5NYWRlIHdpdGggUGl4ZWxzIHRvIFN2ZyBodHRwczovL2NvZGVwZW4uaW8vc2hzaGF3L3Blbi9YYnh2Tmo8L21ldGFkYXRhPgo8cGF0aCBzdHJva2U9IiNjMGMwYzAiIGQ9Ik0wIDBoMU0xIDFoMSIgLz4KPC9zdmc+) !important;
background-position: 0 0 !important;
background-repeat: repeat !important;
background-size: 2px !important;
background: var(--scroll-bkg);
}
#tree::-webkit-scrollbar,
#tree::-webkit-scrollbar-track {
background: var(--scroll-bkg);
}
html.e ::-webkit-scrollbar-button,
html.e::-webkit-scrollbar-button {
background-repeat: no-repeat !important;
background-size: 16px !important;
}
html.e ::-webkit-scrollbar-button:single-button:vertical:decrement,
html.e::-webkit-scrollbar-button:single-button:vertical:decrement {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTcgNWgxTTYgNmgzTTUgN2g1TTQgOGg3IiAvPgo8L3N2Zz4=) !important;
}
html.e ::-webkit-scrollbar-button:single-button:vertical:increment,
html.e::-webkit-scrollbar-button:single-button:vertical:increment {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTQgNWg3TTUgNmg1TTYgN2gzTTcgOGgxIiAvPgo8L3N2Zz4=) !important;
}
html.e ::-webkit-scrollbar-button:single-button:horizontal:decrement,
html.e::-webkit-scrollbar-button:single-button:horizontal:decrement {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTggM2gxTTcgNGgyTTYgNWgzTTUgNmg0TTYgN2gzTTcgOGgyTTggOWgxIiAvPgo8L3N2Zz4=) !important;
}
html.e ::-webkit-scrollbar-button:single-button:horizontal:increment,
html.e::-webkit-scrollbar-button:single-button:horizontal:increment {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTYgM2gxTTYgNGgyTTYgNWgzTTYgNmg0TTYgN2gzTTYgOGgyTTYgOWgxIiAvPgo8L3N2Zz4=) !important;
}
html.e ::-webkit-scrollbar-corner,
html.e::-webkit-scrollbar-corner {
background: var(--silver) !important;
}
html,
html.e #tree {
scrollbar-color: inherit !important;
}
html.e #tree {
background: var(--bg);
padding-left: 0.4em;
padding-top: 0;
margin-left: var(--negative-space);
}
html.e.noscroll #tree {
/*HC*/
box-shadow: 1px 1px var(--grey), 2px 2px var(--shadow-color-1),
var(--shadow-outset-bottom);
}
html.e #treeh {
background: var(--bg);
box-shadow: var(--shadow-outset-top), var(--shadow-outset-bottom);
width: calc(1.5em + var(--nav-sz) - var(--sbw));
height: 2.4em;
border: none;
top: -2px;
display: flex;
align-items: center;
gap: 0.6em;
}
html.e #treeh .btn {
margin: 0px;
top: auto;
}
html.e #tree ul {
border-left: var(--border-dashed-black);
margin-left: 2.15em;
}
html.e .ntree a:first-child {
font-family: scp, monospace, monospace;
font-size: 1.2em;
line-height: 0;
background: var(--inset-bg);
aspect-ratio: 1/1;
text-align: center;
align-content: center;
border-radius: var(--radius) !important;
padding: 0.057em;
border: 1px solid var(--black);
}
html.e .ntree a:first-child:after {
content: ".";
position: absolute;
border-top: var(--border-dashed-black);
color: var(--transparent);
font-size: 0.9em;
margin-left: 0.13em;
}
html.e #treeul {
border: 0 !important;
position: static;
margin: 0 !important;
min-height: 100%;
height: max-content;
}
html.e .ntree a:last-of-type:before {
content: "📁";
margin-left: 0.3em;
}
html.e .ntree {
padding-left: 1em !important;
padding-top: 0.3em !important;
background: var(--inset-bg);
box-shadow: var(--shadow-inset-left), var(--shadow-inset-bottom);
}
html.e #tree li {
margin-left: -0.5em;
border-top: 0;
}
html.e .ntree a:hover {
outline-offset: -2px;
color: var(--fg);
border-radius: var(--radius) !important;
}
html.e #treepar {
width: calc(-1em + var(--nav-sz) - var(--sbw));
overflow: hidden;
left: -0.7em;
box-shadow: var(--shadow-inset-left), var(--shadow-inset-top);
border-left: 0 !important;
border-bottom: var(--border-dashed-black);
margin-left: calc(2.1em - (1em - var(--negative-space))) !important;
}
html.e #path,
html.e #widgeti,
html.e #wtoggle,
html.e #wtoggle a,
html.e #files,
html.e #files thead th,
html.e #ghead a,
html.e #tree {
box-shadow: var(--shadow-outset);
}
html.e.noscroll #treepar {
width: calc(var(--nav-sz) - 1em);
}
html.e #docul {
border-left: 0 !important;
margin-left: 0 !important;
}
html.e #wrap {
transform: translateX(calc((var(--negative-space) * 2) - 1.2em));
padding-right: var(--negative-space);
position: relative;
margin-right: calc((var(--negative-space) * 2) - 1.2em);
margin-top: var(--negative-space);
margin-left: 1.2em;
/*overflow-x: auto; fix for OOB table when screen space is limited (mobile), but removes sticky header*/
}
html.e input[type="radio"] {
accent-color: #232323;
}
html.e #path {
width: calc(100% - 0.4em);
display: flex;
align-items: center;
margin: 0;
padding: 0.2em;
overflow-x: auto;
}
html.e #path i {
border: 1px solid var(--w);
border-color: var(--w);
margin: 0;
border-width: 0.1em 0.1em 0 0;
height: 0.5em;
width: 0.5em;
}/*
html.e #hovertree:after {
color: red;
content: "BUGGY";
html.ez #hovertree:after {
color: rgb(255 98 98);
content: "BUGGY";
}
}*/
html.e #widget {
box-shadow: 0 0;
border: 0 !important;
}
html.e #wtico,
html.e #zip1 {
box-shadow: 0 0 !important;
}
html.e #wtgrid {
top: -0.09em;
}
html.e #wfs,
html.e #wm3u,
html.e #wnp,
html.e #wzip {
border-width: 0 1px 0 0;
}
html.e #wfm.act + #wzip1 + #wzip,
html.e #wfm.act + #wzip1 + #wzip + #wnp {
border-left-width: 1px;
}
html.e #barpos {
/* border-radius: var(--radius); */
box-shadow: var(--shadow-inset);
}
html.e #goh + span {
border-left: 0.1em solid var(--bg-u5);
}
html.e #wfp {
margin: var(--negative-space);
font-size: 0;
display: inline-block;
}
html.e #wfp a {
font-size: large;
display: inline-block;
}
html.e #repl {
font-size: large;
padding: 0.33em;
right: calc(var(--negative-space) * 0.89);
position: absolute;
}
html.e #epi {
text-align: center;
text-wrap-mode: nowrap;
margin: 0px;
}
html.e #epi.logue:not(.mdo) {
padding: 0.8em;
box-shadow: var(--shadow-outset);
}
html.e #epi.logue.mdo {
padding-left: 3px;
}
html.e #doc {
box-shadow: var(--shadow-inset);
background: var(--inset-bg);
margin: 0.2em;
border-radius: var(--radius);
}
html.e #detree {
padding: 0px;
}

View file

@ -9,7 +9,7 @@
<meta name="theme-color" content="#{{ tcolor }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/browser.css?_={{ ts }}">
{{ html_head }}
{{- html_head }}
{%- if css %}
<link rel="stylesheet" media="screen" href="{{ css }}_={{ ts }}">
{%- endif %}

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@
<meta name="theme-color" content="#{{ tcolor }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/shares.css?_={{ ts }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
{{ html_head }}
{{- html_head }}
</head>
<body>
@ -23,17 +23,17 @@
<th>user</th>
<th>groups</th>
</tr></thead><tbody>
{% for un, gn in rows %}
{%- for un, gn in rows %}
<tr>
<td><a href="{{ r }}/?idp=rm={{ un|e }}">forget</a></td>
<td>{{ un|e }}</td>
<td>{{ gn|e }}</td>
</tr>
{% endfor %}
{%- endfor %}
</tbody></table>
{% if not rows %}
{%- if not rows %}
(there are no IdP users in the cache)
{% endif %}
{%- endif %}
</div>
<a href="#" id="repl">π</a>
<script>

View file

@ -9,7 +9,7 @@
{%- if edit %}
<link rel="stylesheet" href="{{ r }}/.cpr/md2.css?_={{ ts }}">
{%- endif %}
{{ html_head }}
{{- html_head }}
</head>
<body>
<div id="mn"></div>
@ -130,7 +130,8 @@ write markdown (most html is 🙆 too)
var SR = "{{ r }}",
last_modified = {{ lastmod }},
have_emp = {{ "true" if have_emp else "false" }},
have_emp = {{ have_emp }},
md_no_br = {{ md_no_br }},
dfavico = "{{ favico }}";
var md_opt = {

View file

@ -201,7 +201,7 @@ function convert_markdown(md_text, dest_dom) {
var marked_opts = {
//headerPrefix: 'h-',
breaks: true,
breaks: !md_no_br,
gfm: true
};
@ -217,7 +217,7 @@ function convert_markdown(md_text, dest_dom) {
catch (ex) {
if (IE) {
dest_dom.innerHTML = 'IE cannot into markdown ;_;';
return;
return false;
}
if (ext)
@ -344,6 +344,8 @@ function convert_markdown(md_text, dest_dom) {
}
catch (ex) { }
}, 1);
return true;
}
@ -422,7 +424,7 @@ function init_toc() {
}
}
// hilight the correct toc items + scroll into view
// highlight the correct toc items + scroll into view
function freshen_toclist() {
if (anchors.length == 0)
return;

View file

@ -1,6 +1,10 @@
"use strict";
var sloc0 = '' + location,
dbg_kbd = /[?&]dbgkbd\b/.exec(sloc0);
// server state
var server_md = dom_src.value;
@ -697,8 +701,14 @@ function reLastIndexOf(txt, ptn, end) {
// table formatter
function fmt_table(e) {
if (e) e.preventDefault();
//dom_tbox.className = '';
try {
fmt_table2();
}
catch (ex) {
return toast.err(7, 'table-format (CTRL-K) failed:\n' + ex);
}
}
function fmt_table2() {
var txt = dom_src.value,
ofs = dom_src.selectionStart,
//o0 = txt.lastIndexOf('\n\n', ofs),
@ -930,23 +940,30 @@ var set_lno = (function () {
// hotkeys / toolbar
(function () {
var keydown = function (ev) {
if (!ev && window.event) {
ev = window.event;
var keydown = function (e) {
if (!e && window.event) {
e = window.event;
if (dev_fbw == 1) {
toast.warn(10, 'hello from fallback code ;_;\ncheck console trace');
console.error('using window.event');
}
}
var kc = ev.code || ev.keyCode || ev.which,
var k = (e.key || e.code) + '',
editing = document.activeElement == dom_src;
//console.log(ev.key, ev.code, ev.keyCode, ev.which);
if (ctrl(ev) && (ev.code == "KeyS" || kc == 83)) {
if (k.startsWith('Key'))
k = k.slice(3);
var kl = k.toLowerCase();
if (dbg_kbd)
console.log('KBD', k, kl, e.key, e.code, e.keyCode, e.which);
if (ctrl(e) && kl == "s") {
save();
return false;
}
if (ev.code == "Escape" || kc == 27) {
if (k == "Escape" || k == "Esc") {
var d = ebi('helpclose');
if (d)
d.click();
@ -954,46 +971,44 @@ var set_lno = (function () {
if (editing)
set_lno();
if (ctrl(ev)) {
if (ev.code == "KeyE") {
if (ctrl(e)) {
if (kl == "e") {
dom_nsbs.click();
return false;
}
if (!editing)
return true;
if (ev.code == "KeyH" || kc == 72) {
md_header(ev.shiftKey);
if (kl == "h") {
md_header(e.shiftKey);
return false;
}
if (ev.code == "KeyZ" || kc == 90) {
if (ev.shiftKey)
if (kl == "z") {
if (e.shiftKey)
action_stack.redo();
else
action_stack.undo();
return false;
}
if (ev.code == "KeyY" || kc == 89) {
if (kl == "y") {
action_stack.redo();
return false;
}
if (ev.code == "KeyK") {
if (kl == "k") {
fmt_table();
return false;
}
if (ev.code == "KeyU") {
if (kl == "u") {
iter_uni();
return false;
}
var up = ev.code == "ArrowUp" || kc == 38;
var dn = ev.code == "ArrowDown" || kc == 40;
if (up || dn) {
md_p_jump(dn);
if (k == "ArrowUp" || k == "ArrowDown") {
md_p_jump(k == "ArrowDown");
return false;
}
if (ev.code == "KeyX" || ev.code == "KeyC") {
md_cut(ev.code == "KeyX");
if (kl == "x" || kl == "c") {
md_cut(kl == "x");
return true; //sic
}
}
@ -1001,18 +1016,18 @@ var set_lno = (function () {
if (!editing)
return true;
if (ev.code == "Tab" || kc == 9) {
md_indent(ev.shiftKey);
if (k == "Tab") {
md_indent(e.shiftKey);
return false;
}
if (ev.code == "Home" || kc == 36) {
md_home(ev.shiftKey);
if (k == "Home") {
md_home(e.shiftKey);
return false;
}
if (!ev.shiftKey && ((ev.code + '').endsWith("Enter") || kc == 13)) {
if (!e.shiftKey && k.endsWith("Enter")) {
return md_newline();
}
if (!ev.shiftKey && kc == 8) {
if (!e.shiftKey && k == "Backspace") {
return md_backspace();
}
}
@ -1036,7 +1051,9 @@ ebi('help').onclick = function (e) {
var dom = ebi('helpbox');
var dtxt = dom.getElementsByTagName('textarea');
if (dtxt.length > 0) {
convert_markdown(dtxt[0].value, dom);
var txt = dtxt[0].value;
if (!convert_markdown(txt, dom))
dom.innerText = txt.split('## markdown')[0];
dom.innerHTML = '<a href="#" id="helpclose">close</a>' + dom.innerHTML;
}

View file

@ -8,7 +8,7 @@
<link rel="stylesheet" href="{{ r }}/.cpr/mde.css?_={{ ts }}">
<link rel="stylesheet" href="{{ r }}/.cpr/deps/mini-fa.css?_={{ ts }}">
<link rel="stylesheet" href="{{ r }}/.cpr/deps/easymde.css?_={{ ts }}">
{{ html_head }}
{{- html_head }}
</head>
<body>
<div id="mw">
@ -28,7 +28,8 @@
var SR = "{{ r }}",
last_modified = {{ lastmod }},
have_emp = {{ "true" if have_emp else "false" }},
have_emp = {{ have_emp }},
md_no_br = {{ md_no_br }},
dfavico = "{{ favico }}";
var md_opt = {

View file

@ -8,7 +8,7 @@
<meta name="viewport" content="width=device-width, initial-scale=0.8">
<meta name="theme-color" content="#{{ tcolor }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/msg.css?_={{ ts }}">
{{ html_head }}
{{- html_head }}
</head>
<body>

View file

@ -10,7 +10,7 @@
<meta name="theme-color" content="#{{ tcolor }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/rups.css?_={{ ts }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
{{ html_head }}
{{- html_head }}
</head>
<body>

View file

@ -1,5 +1,5 @@
function render() {
var html = ['<table id="tab"><thead><tr><th>size</th><th>who</th><th>when</th><th>age</th><th>dir</th><th>file</th></tr></thead><tbody>'];
var html = ['<table id="tab"><thead><tr><th>size</th><th>who</th><th>ip</th><th>when</th><th>age</th><th>dir</th><th>file</th></tr></thead><tbody>'];
var ups = V.ups, now = V.now;
ebi('filter').value = V.filter;
ebi('hits').innerHTML = 'showing ' + ups.length + ' files';
@ -16,6 +16,7 @@ function render() {
sz = ('' + f.sz).replace(/\B(?=(\d{3})+(?!\d))/g, " ");
html.push('<tr><td>' + sz +
'</td><td>' + (f.un || '') +
'</td><td>' + f.ip +
'</td><td>' + ts +
'</td><td>' + sa +

View file

@ -10,7 +10,7 @@
<meta name="theme-color" content="#{{ tcolor }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/shares.css?_={{ ts }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
{{ html_head }}
{{- html_head }}
</head>
<body>
@ -36,7 +36,7 @@
<th>hrs</th>
<th>add time</th>
</tr></thead><tbody>
{% for k, pw, vp, pr, st, un, t0, t1 in rows %}
{%- for k, pw, vp, pr, st, un, t0, t1 in rows %}
<tr>
<td>
<a href="{{ r }}{{ shr }}{{ k }}?qr">qr</a>
@ -54,11 +54,11 @@
<td>{{ "inf" if not t1 else "dead" if t1 < now else ((t1 - now) / 3600) | round(1) }}</td>
<td></td>
</tr>
{% endfor %}
{%- endfor %}
</tbody></table>
{% if not rows %}
{%- if not rows %}
(you don't have any active shares btw)
{% endif %}
{%- endif %}
</div>
<a href="#" id="repl">π</a>
<script>

View file

@ -24,6 +24,7 @@ h1 {
li {
margin: 1em 0;
}
#lo,
a {
color: #047;
background: #fff;
@ -37,6 +38,7 @@ a {
td a {
margin: 0;
}
#wb,
#w {
color: #fff;
background: #940;
@ -47,6 +49,7 @@ td a {
float: right;
margin: -.2em 0 0 .8em;
}
#lo,
.logout,
a.r {
color: #c04;
@ -176,12 +179,14 @@ html.z {
html.z h1 {
border-color: #777;
}
html.z #lo,
html.z a {
color: #fff;
background: #057;
border-color: #37a;
}
html.z .logout,
html.z #lo,
html.z a.r {
background: #804;
border-color: #c28;

View file

@ -9,7 +9,7 @@
<meta name="theme-color" content="#{{ tcolor }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/splash.css?_={{ ts }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
{{ html_head }}
{{- html_head }}
</head>
<body>
@ -21,8 +21,12 @@
{%- if this.uname == '*' %}
<p id="b">howdy stranger &nbsp; <small>(you're not logged in)</small></p>
{%- else %}
{%- if this.args.idp_logout %}
<a id="c" href="{{ this.args.idp_logout }}" class="logout">logout</a>
{%- else %}
<a id="c" href="{{ r }}/?pw=x" class="logout">logout</a>
<p><span id="m">welcome back,</span> <strong>{{ this.uname|e }}</strong></p>
{%- endif %}
<p><span id="m">welcome back,</span> <strong id="un">{{ this.uname|e }}</strong></p>
{%- endif %}
{%- endif %}
@ -37,9 +41,9 @@
<table class="vols">
<thead><tr><th>%</th><th>speed</th><th>eta</th><th>idle</th><th>dir</th><th>file</th></tr></thead>
<tbody>
{% for u in ups %}
{%- for u in ups %}
<tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td><a href="{{ u[4] }}">{{ u[5]|e }}</a></td><td>{{ u[6]|e }}</td></tr>
{% endfor %}
{%- endfor %}
</tbody>
</table>
{%- endif %}
@ -49,9 +53,9 @@
<table class="vols">
<thead><tr><th>%</th><th>sent</th><th>speed</th><th>eta</th><th>idle</th><th></th><th>dir</th><th>file</th></tr></thead>
<tbody>
{% for u in dls %}
{%- for u in dls %}
<tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td>{{ u[4] }}</td><td>{{ u[5] }}</td><td><a href="{{ u[6] }}">{{ u[7]|e }}</a></td><td>{{ u[8] }}</td></tr>
{% endfor %}
{%- endfor %}
</tbody>
</table>
{%- endif %}
@ -70,11 +74,11 @@
<table class="vols">
<thead><tr><th>vol</th><th id="t">action</th><th>status</th></tr></thead>
<tbody>
{% for mp in avol %}
{%- for mp in avol %}
{%- if mp in vstate and vstate[mp] %}
<tr><td><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></td><td><a class="s" href="{{ r }}{{ mp }}?scan">rescan</a></td><td>{{ vstate[mp] }}</td></tr>
{%- endif %}
{% endfor %}
{%- endfor %}
</tbody>
</table>
</td></tr></table>
@ -87,18 +91,18 @@
{%- if rvol %}
<h1 id="f">you can browse:</h1>
<ul>
{% for mp in rvol %}
{%- for mp in rvol %}
<li><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></li>
{% endfor %}
{%- endfor %}
</ul>
{%- endif %}
{%- if wvol %}
<h1 id="g">you can upload to:</h1>
<ul>
{% for mp in wvol %}
{%- for mp in wvol %}
<li><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></li>
{% endfor %}
{%- endfor %}
</ul>
{%- endif %}
@ -110,64 +114,83 @@
<input type="password" id="lp" name="cppwd" placeholder=" password" />
<input type="hidden" name="uhash" id="uhash" value="x" />
<input type="submit" id="ls" value="Unlock" />
{% if ahttps %}
{%- if ahttps %}
<a id="w" href="{{ ahttps }}">switch to https</a>
{% endif %}
{%- endif %}
</form>
</div>
{%- else %}
<h1 id="l">login for more:</h1>
<div>
{%- if this.args.idp_login %}
<ul><li>
<a href="{{ this.args.idp_login | replace("{dst}",r+"/"+qvpath) }}">{{ this.args.idp_login_t }}</a>
{%- if this.args.ao_have_pw %}or alternatively:{%- endif %}
</li></ul>
{%- endif %}
{%- if this.args.ao_have_pw %}
<form id="lf" method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}">
<input type="hidden" id="la" name="act" value="login" />
{% if this.args.usernames %}
{%- if this.args.usernames %}
<input type="text" id="lu" name="uname" placeholder=" username" size="12" />
<input type="password" id="lp" name="cppwd" placeholder=" password" size="12" />
{% else %}
{%- else %}
<input type="password" id="lp" name="cppwd" placeholder=" password" />
{% endif %}
{%- endif %}
<input type="hidden" name="uhash" id="uhash" value="x" />
<input type="submit" id="ls" value="login" />
{% if chpw %}
{%- if chpw %}
<a id="x" href="#">change password</a>
{% endif %}
{% if ahttps %}
{%- endif %}
{%- if ahttps %}
<a id="w" href="{{ ahttps }}">switch to https</a>
{% endif %}
{%- endif %}
</form>
{%- endif %}
</div>
{%- endif %}
<h1 id="cc">other stuff:</h1>
<ul>
{%- if ahttps %}
<li><a id="wb" href="{{ ahttps }}">switch to https</a></li>
{%- endif %}
{%- if this.uname in this.args.idp_adm_set %}
<li><a id="ag" href="{{ r }}/?idp">view idp cache</a></li>
{% endif %}
{%- endif %}
{%- if this.uname != '*' and this.args.shr %}
<li><a id="y" href="{{ r }}/?shares">edit shares</a></li>
{% endif %}
{%- endif %}
{% if k304 or k304vis %}
{% if k304 %}
{%- if k304 or k304vis %}
{%- if k304 %}
<li><a id="h" href="{{ r }}/?cc&setck=k304=n">disable k304</a> (currently enabled)
{%- else %}
<li><a id="i" href="{{ r }}/?cc&setck=k304=y" class="r">enable k304</a> (currently disabled)
{% endif %}
{%- endif %}
<blockquote id="j">enabling k304 will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general</blockquote></li>
{% endif %}
{%- endif %}
{% if no304 or no304vis %}
{% if no304 %}
{%- if no304 or no304vis %}
{%- if no304 %}
<li><a id="ab" href="{{ r }}/?cc&setck=no304=n">disable no304</a> (currently enabled)
{%- else %}
<li><a id="ac" href="{{ r }}/?cc&setck=no304=y" class="r">enable no304</a> (currently disabled)
{% endif %}
{%- endif %}
<blockquote id="ad">enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!</blockquote></li>
{% endif %}
{%- endif %}
<li><a id="af" href="{{ r }}/?ru">show recent uploads</a></li>
<li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li>
{%- if this.uname != '*' and not in_shr %}
<li><form method="post" enctype="multipart/form-data">
<input type="hidden" name="act" value="logout" />
<input type="submit" id="lo" value="logout “{{ this.uname|e }}” everywhere" />
</form></li>
{%- endif %}
</ul>
</div>

View file

@ -17,6 +17,11 @@ var Ls = {
"j1": "k304 bryter tilkoplingen for hver HTTP 304. Dette hjelper mot visse mellomtjenere som kan sette seg fast / plutselig slutter å laste sider, men det reduserer også ytelsen betydelig",
"k1": "nullstill innstillinger",
"l1": "logg inn:",
"ls3": "logg inn",
"lu4": "brukernavn",
"lp4": "passord",
"lo3": "logg ut “{0}” overalt",
"lo2": "avslutter økten på alle nettlesere",
"m1": "velkommen tilbake,",
"n1": "404: filen finnes ikke &nbsp;┐( ´ -`)┌",
"o1": 'eller kanskje du ikke har tilgang? prøv et passord eller <a href="' + SR + '/?h">gå hjem</a>',
@ -46,6 +51,7 @@ var Ls = {
"eng": {
"d2": "shows the state of all active threads",
"e2": "reload config files (accounts/volumes/volflags),$Nand rescan all e2ds volumes$N$Nnote: any changes to global settings$Nrequire a full restart to take effect",
"lo2": "ends the session on all browsers",
"u2": "time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds",
"v2": "use this server as a local HDD",
"ta1": "fill in your new password first",
@ -68,6 +74,11 @@ var Ls = {
"j1": "k304 会在每个 HTTP 304 时断开连接。这有助于避免某些代理服务器卡住或突然停止加载页面,但也会显著降低性能。",
"k1": "重置设置",
"l1": "登录:",
"ls3": "登录", //m
"lu4": "用户名", //m
"lp4": "密码", //m
"lo3": "在所有地方注销 {0}", //m
"lo2": "这将结束在所有浏览器中的会话", //m
"m1": "欢迎回来,",
"n1": "404: 文件不存在 &nbsp;┐( ´ -`)┌",
"o1": '或者你可能没有权限?尝试输入密码或 <a href="' + SR + '/?h">回家</a>',
@ -98,33 +109,38 @@ var Ls = {
"a1": "obnovit",
"b1": "ahoj cizinče &nbsp; <small>(nejsi přihlášen)</small>",
"c1": "odhlásit se",
"d1": "vypsat zásobníku", // TLNote: "d2" is the tooltip for this button
"d1": "vypsat zásobníku",
"d2": "zobrazit stav všech aktivních vláken",
"e1": "znovu načíst konfiguraci",
"e2": "znovu načíst konfigurační soubory (accounts/volumes/volflags),$Na prohledat všechny e2ds úložiště$N$Npoznámka: všechny změny globálních nastavení$Nvyžadují úplné restartování, aby se projevily",
"f1": "můžeš procházet:",
"g1": "můžeš nahrávat do:",
"cc1": "další věci:",
"h1": "zakázat k304", // TLNote: "j1" explains what k304 is
"h1": "zakázat k304",
"i1": "povolit k304",
"j1": "povolení k304 odpojí vašeho klienta při každém HTTP 304, což může zabránit některým chybovým proxy serverům, aby se zasekly (náhle nenačítaly stránky), <em>ale</em> také to obecně zpomalí věci",
"k1": "resetovat nastavení klienta",
"l1": "přihlaste se pro více:",
"m1": "vítej zpět,", // TLNote: "welcome back, USERNAME"
"ls3": "přihlásit se", //m
"lu4": "uživatelské jméno", //m
"lp4": "heslo", //m
"lo3": "odhlásit “{0}” všude", //m
"lo2": "tímto ukončíte relaci ve všech prohlížečích", //m
"m1": "vítej zpět,",
"n1": "404 nenalezeno &nbsp;┐( ´ -`)┌",
"o1": 'nebo možná nemáš přístup -- zkus heslo nebo <a href="' + SR + '/?h">jdi domů</a>',
"p1": "403 zakázáno &nbsp;~┻━┻",
"q1": 'použij heslo nebo <a href="' + SR + '/?h">jdi domů</a>',
"r1": "jdi domů",
".s1": "znovu prohledat",
"t1": "akce", // TLNote: this is the header above the "rescan" buttons
"t1": "akce",
"u2": "čas od posledního zápisu na server$N( upload / rename / ... )$N$N17d = 17 dní$N1h23 = 1 hodina 23 minut$N4m56 = 4 minuty 56 sekund",
"v1": "připojit",
"v2": "použít tento server jako místní HDD",
"w1": "přepnout na https",
"x1": "změnit heslo",
"y1": "upravit sdílení", // TLNote: shows the list of folders that the user has decided to share
"z1": "odblokovat toto sdílení:", // TLNote: the password prompt to see a hidden share
"y1": "upravit sdílení",
"z1": "odblokovat toto sdílení:",
"ta1": "nejprve vyplňte své nové heslo",
"ta2": "zopakujte pro potvrzení nového hesla:",
"ta3": "nalezen překlep; zkuste to prosím znovu",
@ -151,6 +167,11 @@ var Ls = {
"j1": "k304 trennt die Clientverbindung bei jedem HTTP 304, was Bugs mit problematischen Proxies vorbeugen kann (z.B. nicht ladenden Seiten), macht Dinge aber generell langsamer",
"k1": "Client-Einstellungen zurücksetzen",
"l1": "Melde dich an für mehr:",
"ls3": "Anmelden", //m
"lu4": "Benutzername", //m
"lp4": "Passwort", //m
"lo3": "“{0}” überall abmelden", //m
"lo2": "Dies beendet die Sitzung in allen Browsern", //m
"m1": "Willkommen zurück,",
"n1": "404 Nicht gefunden &nbsp;┐( ´ -`)┌",
"o1": 'or maybe you don\'t have access -- try a password or <a href="' + SR + '/?h">go home</a>',
@ -192,6 +213,11 @@ var Ls = {
"j1": "k304 katkaisee yhteytesi jokaisella HTTP 304:llä, mikä voi estää joitain bugisia välityspalvelimia jumittumasta/lopettamasta sivujen lataamista, <em>mutta</em> se myös vähentää suorituskykyä",
"k1": "nollaa asetukset",
"l1": "kirjaudu sisään:",
"ls3": "kirjaudu sisään", //m
"lu4": "käyttäjätunnus", //m
"lp4": "salasana", //m
"lo3": "kirjaa “{0}” ulos kaikkialta", //m
"lo2": "tämä lopettaa istunnon kaikissa selaimissa", //m
"m1": "tervetuloa takaisin,",
"n1": "404: ei löytynyt mitään &nbsp;┐( ´ -`)┌",
"o1": 'tai ehkä sinulla ei vain ole käyttöoikeuksia? kokeile salasanaa tai <a href="' + SR + '/?h">mene kotiin</a>',
@ -218,6 +244,52 @@ var Ls = {
"af1": "näytä viimeaikaiset lataukset",
"ag1": "näytä tunnetut IdP-käyttäjät",
},
"fra": {
"a1": "rafraîchir",
"b1": "salut étranger &nbsp; <small>(vous n'êtes pas connecté.)</small>",
"c1": "déconnexion",
"d1": "vidange de la pile",
"d2": "affiche l'état de tous les threads actifs",
"e1": "recharger la configuration",
"e2": "recharger le fichier de configuration (comptes/volumes/indicateurs de volume),$Net rescanner tous les volumes e2ds$N$Nnote : n'importe quel changement aux paramètres globaux$Nnécessite un redémarrage complet pour prendre effet",
"f1": "vous pouvez naviguer :",
"g1": "vous pouvez télécharger sur :",
"cc1": "autres choses :",
"h1": "désactiver k304",
"i1": "activer k304",
"j1": "activer k304 va déconnecter votre client sur chaque HTTP 304, ce qui peut éviter à certains proxies défectueux de rester bloqués (les pages ne se chargent soudainement plus), <em>mais</em> cela ralentira également les choses en général",
"k1": "réinitialiser les paramètres du client",
"l1": "connectez-vous pour en savoir plus :",
"ls3": "se connecter", //m
"lu4": "nom d'utilisateur", //m
"lp4": "mot de passe", //m
"lo3": "déconnecter “{0}” partout", //m
"lo2": "cela mettra fin à la session sur tous les navigateurs", //m
"m1": "heureux de vous revoir,",
"n1": "404 introuvable &nbsp;┐( ´ -`)┌",
"o1": 'ou peut-être que vous n\'y avez pas accès -- essayer un mot de passe ou <a href="' + SR + '/?h">aller à la page d\'accueil</a>',
"p1": "403 interdit &nbsp;~┻━┻",
"q1": 'utiliser un mot de passe ou <a href="' + SR + '/?h">aller à la page d\'accueil</a>',
"r1": "aller à la page d\'accueil",
".s1": "rescanner",
"t1": "action",
"u2": "temps écoulé depuis la dernière écriture sur le serveur$N(téléchargement/renommage/...)$N$N17j = 17 jours$N1h23 = 1 heure 23 minutes$N4m56 = 4 minutes 56 secondes",
"v1": "connecter",
"v2": "utilisez ce serveur en tant que disque dur local",
"w1": "passer à https",
"x1": "changer mot de passe",
"y1": "modifier les partages",
"z1": "déverrouiller ce partage :",
"ta1": "entrez d'abord votre nouveau mot de passe",
"ta2": "répétez pour confirmer le nouveau mot de passe :",
"ta3": "une faute de frappe a été détectée ; veuillez réessayer.",
"aa1": "fichiers entrants :",
"ab1": "désactiver no304",
"ac1": "activer no304",
"ad1": "l'activation de no304 désactivera toute mise en cache ; essayez ceci si k304 n'était pas suffisant. Cela va générer un trafic réseau considérable !",
"ae1": "téléchargements actifs :",
"af1": "afficher les derniers téléchargements",
},
"grc": {
"a1": "ανανέωση",
"b1": "γεια σου ξένε! &nbsp; <small>(δεν είσαι συνδεδεμένος)</small>",
@ -234,6 +306,11 @@ var Ls = {
"j1": "η ενεργοποίηση του k304 θα αποσυνδέσει το πρόγραμμα πελάτη σου σε κάθε HTTP 304, κάτι που μπορεί να αποτρέψει κάποια προβληματικά proxies από το να κολλάνε (να μην φορτώνουν ξαφνικά σελίδες), <em>αλλά</em> θα κάνει τα πράγματα, γενικά πιο αργά",
"k1": "επαναφορά ρυθμίσεων στο πρόγραμμα πελάτη",
"l1": "συνδέσου για περισσότερα:",
"ls3": "σύνδεση", //m
"lu4": "όνομα χρήστη", //m
"lp4": "κωδικός πρόσβασης", //m
"lo3": "αποσύνδεση του “{0}” από παντού", //m
"lo2": "αυτό θα τερματίσει τη συνεδρία σε όλους τους περιηγητές", //m
"m1": "καλώς ήρθες,",
"n1": "404 δεν βρέθηκε &nbsp;┐( ´ -`)┌",
"o1": '´η μήπως δεν έχεις πρόσβαση -- δοκίμασε έναν κωδικό <a href="' + SR + '/?h">πήγαινε στην αρχική</a>',
@ -275,6 +352,11 @@ var Ls = {
"j1": "k304 interrompe la connessione per ogni HTTP 304. Questo aiuta contro alcuni proxy difettosi che possono bloccarsi o smettere improvvisamente di caricare pagine, ma riduce notevolmente le prestazioni",
"k1": "resetta impostazioni",
"l1": "accedi:",
"ls3": "accedi", //m
"lu4": "nome utente", //m
"lp4": "password", //m
"lo3": "disconnetti “{0}” ovunque", //m
"lo2": "questo terminerà la sessione su tutti i browser", //m
"m1": "bentornato,",
"n1": "404: file non trovato &nbsp;┐( ´ -`)┌",
"o1": "oppure forse non hai accesso? prova una password o <a href=\"SR/?h\">torna alla home</a>",
@ -301,6 +383,53 @@ var Ls = {
"af1": "mostra i file caricati di recente",
"ag1": "mostra utenti IdP conosciuti"
},
"kor": {
"a1": "새로고침",
"b1": "어이 친구! 처음 보는 얼굴인데? &nbsp; <small>(로그인되어 있지 않습니다)</small>",
"c1": "로그아웃",
"d1": "스택 덤프하기",
"d2": "모든 활성 스레드의 상태를 표시합니다",
"e1": "설정 다시 불러오기",
"e2": "설정 파일(계정/볼륨/볼륨 플래그)을 다시 불러오고,$N모든 e2ds 볼륨을 다시 스캔합니다$N$N참고: 전역 설정에 대한 변경 사항은$N적용하려면 전체 재시작이 필요합니다",
"f1": "탐색 가능한 곳:",
"g1": "업로드 가능한 곳:",
"cc1": "기타 항목:",
"h1": "k304 비활성화",
"i1": "k304 활성화",
"j1": "k304를 활성화하면 모든 HTTP 304 응답 시 클라이언트 연결이 끊어집니다. 이는 일부 프록시가 멈추는 현상(갑자기 페이지가 로드되지 않음)을 방지할 수 있지만, <em>대신 전반적인 속도는 느려집니다.</em>",
"k1": "클라이언트 설정 초기화",
"l1": "로그인하기:",
"ls3": "로그인", //m
"lu4": "사용자 이름", //m
"lp4": "비밀번호", //m
"lo3": "{0}을(를) 모든 곳에서 로그아웃", //m
"lo2": "이 작업은 모든 브라우저에서 세션을 종료합니다", //m
"m1": "또 오셨네요,",
"n1": "404 찾을 수 없음 &nbsp;┐( ´ -`)┌",
"o1": "또는 접근 권한이 없을 수 있습니다. 비밀번호를 입력하거나 <a href=\"' + SR + '/?h\">홈으로 이동</a>하세요",
"p1": "403 접근 금지 &nbsp;~┻━┻",
"q1": "비밀번호를 입력하거나 <a href=\"' + SR + '/?h\">홈으로 이동</a>하세요",
"r1": "홈으로 이동",
".s1": "다시 스캔",
"t1": "작업",
"u2": "서버에 마지막으로 쓰기 작업을 한 후 경과된 시간$N(업로드 / 이름 변경 / 등등...)$N$N17d = 17일$N1h23 = 1시간 23분$N4m56 = 4분 56초",
"v1": "연결",
"v2": "이 서버를 로컬 하드디스크처럼 사용하기",
"w1": "HTTPS로 전환",
"x1": "비밀번호 변경",
"y1": "공유 설정",
"z1": "이 공유 잠금해제:",
"ta1": "새 비밀번호를 먼저 입력하세요",
"ta2": "새 비밀번호 확인을 위해 다시 입력하세요:",
"ta3": "오타가 있습니다. 다시 시도해주세요",
"aa1": "수신 중인 파일:",
"ab1": "no304 비활성화",
"ac1": "no304 활성화",
"ad1": "no304를 활성화하면 모든 캐싱이 비활성화됩니다. k304로 충분하지 않은 경우 시도해보세요. 네트워크 트래픽이 대량으로 낭비됩니다!",
"ae1": "활성 다운로드:",
"af1": "최근 업로드 보기",
"ag1": "IdP 캐시 보기"
},
"nld": {
"a1": "Update",
"b1": "Hallo, hoe gaat het met jou? &nbsp; <small>(Je bent niet ingelogd)</small>",
@ -317,6 +446,11 @@ var Ls = {
"j1": "k304 verbreekt de verbinding voor elke HTTP 304. Dit helpt tegen bepaalde proxy servers die kunnen vastlopen/plotseling stoppen met het laden van pagina's, maar het vermindert ook de prestaties aanzienlijk",
"k1": "Instellingen resetten",
"l1": "Inloggen:",
"ls3": "inloggen", //m
"lu4": "gebruikersnaam", //m
"lp4": "wachtwoord", //m
"lo3": "“{0}” overal afmelden", //m
"lo2": "dit zal de sessie in alle browsers beëindigen", //m
"m1": "Welkom terug,",
"n1": "404: bestand bestaat niet &nbsp;┐( ´ -`)┌",
"o1": 'of misschien heb je geen toegang? probeer een wachtwoord of <a href="' + SR + '/?h">ga naar startscherm</a>',
@ -343,6 +477,147 @@ var Ls = {
"af1": "Recent geüploade bestanden weergeven",
"ag1": "Bekende IdP-gebruikers weergeven",
},
"nno": {
"a1": "oppdatér",
"b1": "heisann &nbsp; <small>(du er ikkje logga inn)</small>",
"c1": "logg ut",
"d1": "tilstand",
"d2": "vis tilstanden åt alle trådar",
"e1": "last innst.",
"e2": "les inn konfigurasjonsfiler på nytt$N(kontoer, volum, volumbrytarar)$Nog kartlegg alle e2ds-volum$N$Nmerk: endringer i globale parametrar$Nkrev ein full restart for å gjelde",
"f1": "du kan sjå på:",
"g1": "du kan laste opp åt:",
"cc1": "brytarar og slikt:",
"h1": "skru av k304",
"i1": "skru på k304",
"j1": "k304 bryt tilkoplinga for kvar HTTP 304. Dette hjelp mot visse mellomtjenarar som kan sette seg fast / plutselig sluttar å laste sider, men det sett óg ytinga ned betydelig",
"k1": "nullstill innstillinger",
"l1": "logg inn:",
"ls3": "logg inn",
"lu4": "brukarnamn",
"lp4": "passord",
"lo3": "logg ut “{0}” overalt",
"lo2": "avslutt økta på alle nettlesarar",
"m1": "velkomen attende,",
"n1": "404: filen finnast ikkje &nbsp;┐( ´ -`)┌",
"o1": 'eller kanskje du ikkje har høve? prøv eit passord eller <a href="' + SR + '/?h">gå heim</a>',
"p1": "403: tilgang nektet &nbsp;~┻━┻",
"q1": 'prøv eit passord eller <a href="' + SR + '/?h">gå heim</a>',
"r1": "gå heim",
".s1": "kartlegg",
"t1": "handling",
"u2": "tid sidan nokon sist skreiv åt serveren$N( opplastning / namnendring / ... )$N$N17d = 17 dagar$N1h23 = 1 time 23 minutt$N4m56 = 4 minutt 56 sekund",
"v1": "kople åt",
"v2": "bruk denne serveren som ein lokal harddisk",
"w1": "bytt åt https",
"x1": "bytt passord",
"y1": "dine delinger",
"z1": "lås opp område:",
"ta1": "du må skrive eit nytt passord først",
"ta2": "gjenta for å stadfeste nytt passord:",
"ta3": "fant ein skrivefeil; vennligst prøv igjen",
"aa1": "innkommande:",
"ab1": "skru av no304",
"ac1": "skru på no304",
"ad1": "no304 stoppar all bruk av cache. Hvis ikkje k304 var nok, prøv denne. Vil mangedoble dataforbruk!",
"ae1": "utgående:",
"af1": "vis nylig opplasta filer",
"ag1": "vis kjente IdP-brukarar",
},
"pol": {
"a1": "odśwież",
"b1": "witaj, nieznajomy &nbsp; <small>(nie jesteś zalogowany)</small>",
"c1": "wyloguj się",
"d1": "zrzut stosu",
"d2": "pokazuje status wszystkich aktywnych wątków",
"e1": "przeładuj konfigurację",
"e2": "przeładuj pliki konfiguracyjne (konta/wolumeny/flagi wolumenów),$Ni przeskanuje wszystkie wolumeny e2ds$N$Nnotka: zmiany konfiguracji globalnej$Nwymagają pełnego uruchomienia ponownie serwera, aby zaczęły obowiązywać",
"f1": "możesz przeglądać:",
"g1": "możesz przesyłać do:",
"cc1": "inne:",
"h1": "wyłącz k304",
"i1": "włącz k304",
"j1": "włączenie k304 będzie odłączało klienta przy każdorazowym otrzymaniu kodu HTTP 304, co może zapobiec wieszaniu się wadliwych proxy, <em>ale</em> spowolni ogólne działanie",
"k1": "zresetuj ustawienia klienta",
"l1": "zaloguj się po więcej:",
"ls3": "zaloguj się", //m
"lu4": "nazwa użytkownika", //m
"lp4": "hasło", //m
"lo3": "wyloguj “{0}” wszędzie", //m
"lo2": "spowoduje to zakończenie sesji we wszystkich przeglądarkach", //m
"m1": "Witaj,",
"n1": "404 nie znaleziono &nbsp;┐( ´ -`)┌",
"o1": 'lub możesz nie mieć dostępu -- spróbuj wprowadzić hasło lub <a href="' + SR + '/?h">przejdź do strony głównej</a>',
"p1": "403 odmowa dostępu &nbsp;~┻━┻",
"q1": 'użyj hasła lub <a href="' + SR + '/?h">przejdź do strony głównej</a>',
"r1": "idź do strony głównej",
".s1": "przeskanuj ponownie",
"t1": "akcje",
"u2": "czas od ostatniej interakcji z serwerem$N( przesyłania / zmiany nazwy / ... )$N$N17d = 17 dni$N1h23 = 1 godzina 23 minuty$N4m56 = 4 minuty 56 sekund",
"v1": "połącz",
"v2": "używaj tego serwera jako dysku lokalnego",
"w1": "przejdź na HTTPS",
"x1": "zmień hasło",
"y1": "edytuj udostępnione",
"z1": "odblokuj udostępnienie:",
"ta1": "najpierw wprowadź nowe hasło",
"ta2": "powtórz hasło dla potwierdzenia:",
"ta3": "znaleziono literówkę, spróbuj ponownie",
"aa1": "pliki przychodzące:",
"ab1": "wyłącz no304",
"ac1": "włącz no304",
"ad1": "włączenie no304 wyłączy przechowywanie jakiejkolwiek pamięci podręcznej. Zmarnuje to olbrzymią ilość ruchu sieciowego!",
"ae1": "trwające pobierania:",
"af1": "pokaż ostatnio przesłane pliki",
"ag1": "pokaż znanych użytkowników IdP",
},
"por": {
"a1": "atualizar",
"b1": "olá &nbsp; <small>(você não está logado)</small>",
"c1": "encerrar sessão",
"d1": "despejar o estado da pilha",
"d2": "mostra o estado de todos os threads ativos",
"e1": "recarregar configuração",
"e2": "recarregar arquivos de configuração (contas/volumes/indicadores de volume),$N e reescanear todos os volumes e2ds$N$Nnota: qualquer alteração na configuração global$N requer uma reinicialização completa para ter efeito",
"f1": "você pode navegar:",
"g1": "você pode fazer upload para:",
"cc1": "outras coisas:",
"h1": "desativar k304",
"i1": "ativar k304",
"j1": "ativar k304 irá desconectar seu cliente em cada HTTP 304, o que pode evitar que alguns proxies com erros fiquem presos (parando de carregar páginas de repente), <em>mas</em> também irá desacelerar as coisas em geral",
"k1": "redefinir config. de cliente",
"l1": "faça login para mais:",
"ls3": "fazer login",
"lu4": "nome de usuário",
"lp4": "senha",
"lo3": "encerrar sessão de \"{0}\" em todos os lugares",
"lo2": "isso irá encerrar a sessão em todos os navegadores",
"m1": "bem-vindo de volta,",
"n1": "404 não encontrado &nbsp;┐( ´ -`)┌",
"o1": "ou talvez você não tenha acesso? -- tente com uma senha ou volte para o início",
"p1": "403 proibido &nbsp;~┻━┻",
"q1": "use uma senha ou volte para o início",
"r1": "ir para o início",
".s1": "reescanear",
"t1": "ação",
"u2": "tempo desde a última gravação no servidor$N( upload / renomear / ... )$N$N17d = 17 dias$N1h23 = 1 hora 23 minutos$N4m56 = 4 minutos 56 segundos",
"v1": "conectar",
"v2": "usar este servidor como um disco rígido local",
"w1": "mudar para https",
"x1": "mudar senha",
"y1": "editar recursos compartilhados",
"z1": "desbloquear este recurso compartilhado:",
"ta1": "primeiro digite sua nova senha",
"ta2": "repita para confirmar a nova senha:",
"ta3": "há um erro; por favor, tente novamente",
"aa1": "arquivos de entrada:",
"ab1": "desativar no304",
"ac1": "ativar no304",
"ad1": "ativar no304 irá desabilitar todo o armazenamento em cache; tente isso se k304 não for suficiente. Isso irá desperdiçar uma grande quantidade de tráfego de rede!",
"ae1": "downloads ativos:",
"af1": "mostrar uploads recentes",
"ag1": "mostrar usuários IdP conhecidos"
},
"spa": {
"a1": "actualizar",
"b1": "hola &nbsp; <small>(no has iniciado sesión)</small>",
@ -359,6 +634,11 @@ var Ls = {
"j1": "activar k304 desconectará tu cliente en cada HTTP 304, lo que puede evitar que algunos proxies con errores se atasquen (dejando de cargar páginas de repente), <em>pero</em> también ralentizará las cosas en general",
"k1": "restablecer config. de cliente",
"l1": "inicia sesión para más:",
"ls3": "iniciar sesión", //m
"lu4": "nombre de usuario", //m
"lp4": "contraseña", //m
"lo3": "cerrar sesión de “{0}” en todas partes", //m
"lo2": "esto finalizará la sesión en todos los navegadores", //m
"m1": "bienvenido de nuevo,",
"n1": "404 no encontrado &nbsp;┐( ´ -`)┌",
"o1": '¿o quizás no tienes acceso? -- prueba con una contraseña o <a href=\"' + SR + '/?h\">vuelve al inicio</a>',
@ -385,6 +665,53 @@ var Ls = {
"af1": "mostrar subidas recientes",
"ag1": "mostrar usuarios IdP conocidos"
},
"swe": {
"a1": "uppdatera",
"b1": "tjena främling &nbsp; <small>(du är inte inloggad)</small>",
"c1": "logga ut",
"d1": "dumpa stacken",
"d2": "visar tillståndet på alla aktiva trådar",
"e1": "ladda om konfig.",
"e2": "ladda om konfigurationsfiler (konton/volymer/volflaggor),$Noch skanna om alla e2ds-volymer$N$Nobs.: ändrade globala inställningar$Nkräver en fullständig omstart",
"f1": "du kan bläddra:",
"g1": "du kan ladda upp till:",
"cc1": "annat:",
"h1": "avaktivera k304",
"i1": "aktivera k304",
"j1": "med k304 aktiverad kommer klienten att koppla bort sig vid varje HTTP 304-fel, vilket kan hindra vissa buggiga proxyservrar från att fastna (sidor slutar ladda), <em>men</em> saker kommer också att bli långsammare i allmänhet",
"k1": "återställ klientinställningar",
"l1": "logga in för att se mer:",
"ls3": "logga in", //m
"lu4": "användarnamn", //m
"lp4": "lösenord", //m
"lo3": "logga ut “{0}” överallt", //m
"lo2": "avsluta sessionen i alla webbläsare", //m
"m1": "välkommen tillbaka,",
"n1": "404 hittades inte &nbsp;┐( ´ -`)┌",
"o1": 'eller så har du kanske inte tillgång -- prova ett lösenord eller <a href="' + SR + '/?h">åk hem</a>',
"p1": "403 nekat &nbsp;~┻━┻",
"q1": 'använd ett lösenord eller <a href="' + SR + '/?h">åk hem</a>',
"r1": "åk hem",
".s1": "skanna om",
"t1": "åtgärd",
"u2": "tid sedan senaste serverskrivning$N( uppladdning / namnbyte / ... )$N$N17d = 17 dagar$N1h23 = 1 timme 23 minuter$N4m56 = 4 minuter 56 sekunder",
"v1": "koppla upp",
"v2": "använd denna server som en lokal disk",
"w1": "byt till https",
"x1": "byt lösenord",
"y1": "redigera utdelningar",
"z1": "lås upp denna utdelning:",
"ta1": "fyll i ditt nya lösenord",
"ta2": "upprepa det nya lösenordet:",
"ta3": "det blev fel; vänligen försök igen",
"aa1": "inkommande filer:",
"ab1": "avaktivera no304",
"ac1": "aktivera no304",
"ad1": "detta stänger av all cachning; prova detta om k304 inte räckte till. Detta kommer att slösa enorma mängder nätverkstrafik!",
"ae1": "aktiva nedladdningar:",
"af1": "visa senaste uppladdningar",
"ag1": "visa idp-cache"
},
"ukr": {
"a1": "оновити",
"b1": "привітик, незнайомцю &nbsp; <small>(ви не авторизовані)</small>",
@ -401,6 +728,11 @@ var Ls = {
"j1": "увімкнення k304 буде відключати ваш клієнт при кожному HTTP 304, що може запобігти зависанню деяких глючних проксі (раптово перестають завантажувати сторінки), <em>але</em> це також зробить усе повільнішим загалом",
"k1": "скинути налаштування клієнта",
"l1": "авторизуйтесь для інших опцій:",
"ls3": "увійти", //m
"lu4": "ім'я користувача", //m
"lp4": "пароль", //m
"lo3": "вийти з облікового запису “{0}” всюди", //m
"lo2": "це завершить сеанс у всіх браузерах", //m
"m1": "з поверненням,",
"n1": "404 не знайдено &nbsp;┐( ´ -`)┌",
"o1": 'або у вас немає доступу -- спробуйте авторизуватися або <a href="' + SR + '/?h">повернутися на головну</a>',
@ -443,6 +775,11 @@ var Ls = {
"j1": "включённый k304 будет отключать вас при получении HTTP 304, что может помочь при работе с некоторыми глючными прокси (перестают загружаться страницы), <em>но</em> это также сделает работу клиента медленнее",
"k1": "сбросить локальные настройки",
"l1": "авторизуйтесь для других опций:",
"ls3": "войти", //m
"lu4": "имя пользователя", //m
"lp4": "пароль", //m
"lo3": "выйти из “{0}” везде", //m
"lo2": "это завершит сеанс во всех браузерах", //m
"m1": "с возвращением,",
"n1": "404 не найдено &nbsp;┐( ´ -`)┌",
"o1": 'или у вас нет доступа -- попробуйте авторизоваться или <a href="' + SR + '/?h">вернуться на главную</a>',
@ -518,6 +855,8 @@ if (window.langmod)
var d = Ls[sread("cpp_lang", Object.keys(Ls)) || lang] ||
Ls.eng || Ls.nor || Ls.chi;
d.wb = d.w;
for (var k in (d || {})) {
var f = k.slice(-1),
i = k.slice(0, -1),
@ -528,10 +867,17 @@ for (var k in (d || {})) {
o[a].innerHTML = d[k];
else if (f == 2)
o[a].setAttribute("tt", d[k]);
else if (f == 3)
o[a].setAttribute("value", d[k]);
else if (f == 4)
o[a].setAttribute("placeholder", " " + d[k]);
}
var o1 = ebi('lo'), o2 = ebi('un');
if (o1 && o2 && d.lo3)
o1.setAttribute("value", d.lo3.format(o2.textContent));
try {
if (is_idp) {
if (is_idp > 1) {
var z = ['#l+div', '#l', '#c'];
for (var a = 0; a < z.length; a++)
QS(z[a]).style.display = 'none';
@ -541,14 +887,16 @@ catch (ex) { }
tt.init();
var o = QS('input[name="uname"]') || QS('input[name="cppwd"]');
if (!ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight)
if (o && !MOBILE && !ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight)
o.focus();
o = ebi('u');
if (o && /[0-9]+$/.exec(o.innerHTML))
o.innerHTML = shumantime(o.innerHTML);
ebi('uhash').value = '' + location.hash;
o = ebi('uhash')
if (o)
o.value = '' + location.hash;
if (/\&re=/.test('' + location))
ebi('a').className = 'af g';

View file

@ -10,7 +10,7 @@
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/splash.css?_={{ ts }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
<style>ul{padding-left:1.3em}li{margin:.4em 0}.txa{float:right;margin:0 0 0 1em}</style>
{{ html_head }}
{{- html_head }}
</head>
<body>
@ -31,10 +31,10 @@
<br />
<span class="os win lin mac">placeholders:</span>
<span class="os win">
{% if accs %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>W:</b></code>=mountpoint
{% if accs %}{% if un %}<code><b id="un0">{{ un }}</b></code>=username, <code><b id="up0">{{ unpw }}</b></code>=username:password, {% endif %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>W:</b></code>=mountpoint
</span>
<span class="os lin mac">
{% if accs %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint
{% if accs %}{% if un %}<code><b id="un0">{{ un }}</b></code>=username, <code><b id="up0">{{ unpw }}</b></code>=username:password, {% endif %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint
</span>
{% if accs %}<a href="#" id="setpw">use real password</a>{% endif %}
<a href="#" id="qr">show qr</a>
@ -42,9 +42,9 @@
{% if args.idp_h_usr %}
{% if args.have_idp_hdrs %}
<p style="line-height:2em"><b>WARNING:</b> this server is using IdP-based authentication, so this stuff may not work as advertised. Depending on server config, these commands can probably only be used to access areas which don't require authentication, unless you auth using any non-IdP accounts defined in the copyparty config. Please see <a href="https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients">the IdP docs</a></p>
{% endif %}
{%- endif %}
@ -54,50 +54,64 @@
<div class="os win">
<p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p>
<pre>
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>W:</b>
</pre>
<ul>
{% if s %}
{%- if s %}
<li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li>
{% endif %}
{%- endif %}
<li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>
</ul>
<p>if you want to use the native WebDAV client in windows instead (slow and buggy), first run <a href="{{ r }}/.cpr/a/webdav-cfg.bat">webdav-cfg.bat</a> to remove the 47 MiB filesize limit (also fixes latency and password login), then connect:</p>
<pre>
{%- if un %}
net use <b>w:</b> http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} <b>{{ pw }}</b> /user:{{ b_un }}{% endif %}
{%- else %}
net use <b>w:</b> http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} k /user:<b>{{ pw }}</b>{% endif %}
{%- endif %}
</pre>
</div>
<div class="os lin">
<p>rclone (v1.63 or later) is recommended:</p>
<pre>
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>mp</b>
</pre>
<ul>
{% if s %}
{%- if s %}
<li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li>
{% endif %}
{%- endif %}
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
<li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>
</ul>
<p>alternatively use davfs2 (requires root, is slower, forgets lastmodified-timestamp on upload):</p>
<pre>
yum install davfs2
{%- if un %}
{% if accs %}printf '%s\n' {{ b_un }} <b>{{ pw }}</b> | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b>
{%- else %}
{% if accs %}printf '%s\n' <b>{{ pw }}</b> k | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b>
{%- endif %}
</pre>
{%- if accs %}
<p>make davfs2 automount on boot:</p>
<pre>
{%- if un %}
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} {{ b_un }} <b>{{ pw }}</b>" >> /etc/davfs2/secrets
{%- else %}
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>{{ pw }}</b> k" >> /etc/davfs2/secrets
{%- endif %}
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b> davfs rw,user,uid=1000,noauto 0 0" >> /etc/fstab
</pre>
{%- endif %}
<p>or the emergency alternative (gnome/gui-only):</p>
<!-- gnome-bug: ignores vp -->
<pre>
{%- if accs %}
echo <b>{{ pw }}</b> | gio mount dav{{ s }}://k@{{ ep }}/{{ rvp }}
echo <b>{{ pw }}</b> | gio mount dav{{ s }}://{{ b_un }}@{{ ep }}/{{ rvp }}
{%- else %}
gio mount -a dav{{ s }}://{{ ep }}/{{ rvp }}
{%- endif %}
@ -107,18 +121,18 @@
<div class="os mac">
<pre>
osascript -e ' mount volume "http{{ s }}://k:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}" '
osascript -e ' mount volume "http{{ s }}://{{ b_un }}:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}" '
</pre>
<p>or you can open up a Finder, press command-K and paste this instead:</p>
<pre>
http{{ s }}://k:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}
http{{ s }}://{{ b_un }}:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}
</pre>
{% if s %}
{%- if s %}
<p><em>replace <code>https</code> with <code>http</code> if it doesn't work</em></p>
{% endif %}
{%- endif %}
</div>
{% endif %}
{%- endif %}
@ -127,51 +141,71 @@
<div class="os win">
<p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p>
{% if args.ftp %}
{%- if args.ftp %}
<p>connect with plaintext FTP:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
{%- else %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>W:</b>
</pre>
{% endif %}
{% if args.ftps %}
{%- endif %}
{%- if args.ftps %}
<p>connect with TLS-encrypted FTPS:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
{%- else %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>W:</b>
</pre>
{% endif %}
{%- endif %}
<ul>
{% if args.ftps %}
{%- if args.ftps %}
<li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li>
{% endif %}
{%- endif %}
<li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>
</ul>
<p>if you want to use the native FTP client in windows instead (please dont), press <code>win+R</code> and run this command:</p>
<pre>
{%- if un %}
explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}{{ b_un }}:<b>{{ pw }}</b>@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
{%- else %}
explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}<b>{{ pw }}</b>:k@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
{%- endif %}
</pre>
</div>
<div class="os lin">
{% if args.ftp %}
{%- if args.ftp %}
<p>connect with plaintext FTP:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
{%- else %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>mp</b>
</pre>
{% endif %}
{% if args.ftps %}
{%- endif %}
{%- if args.ftps %}
<p>connect with TLS-encrypted FTPS:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
{%- else %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>mp</b>
</pre>
{% endif %}
{%- endif %}
<ul>
{% if args.ftps %}
{%- if args.ftps %}
<li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li>
{% endif %}
{%- endif %}
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
<li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>
</ul>
@ -179,7 +213,7 @@
<!-- gnome-bug: ignores vp -->
<pre>
{%- if accs %}
echo <b>{{ pw }}</b> | gio mount ftp{{ "" if args.ftp else "s" }}://k@{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
echo <b>{{ pw }}</b> | gio mount ftp{{ "" if args.ftp else "s" }}://{{ b_un }}@{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
{%- else %}
gio mount -a ftp{{ "" if args.ftp else "s" }}://{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
{%- endif %}
@ -189,10 +223,10 @@
<div class="os mac">
<p>note: FTP is read-only on macos; please use WebDAV instead</p>
<pre>
open {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}k:<b>{{ pw }}</b>@{% else %}anonymous:@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
open {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}{{ b_un }}:<b>{{ pw }}</b>@{% else %}anonymous:@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
</pre>
</div>
{% endif %}
{%- endif %}
@ -204,11 +238,11 @@
<span class="os lin">doesn't need root</span>
</p>
<pre>
partyfuse.py{% if accs %} -a <b>{{ pw }}</b>{% endif %} http{{ s }}://{{ ep }}/{{ rvp }} <b><span class="os win">W:</span><span class="os lin mac">mp</span></b>
partyfuse.py{% if accs %} -a <b>{{ unpw }}</b>{% endif %} http{{ s }}://{{ ep }}/{{ rvp }} <b><span class="os win">W:</span><span class="os lin mac">mp</span></b>
</pre>
{% if s %}
{%- if s %}
<ul><li>if you are on LAN (or just dont have valid certificates), add <code>-td</code></li></ul>
{% endif %}
{%- endif %}
<p>
you can use <a href="{{ r }}/.cpr/a/u2c.py">u2c.py</a> to upload (sometimes faster than web-browsers)
</p>
@ -217,6 +251,10 @@
{% if args.smb %}
<h1>SMB / CIFS</h1>
{%- if un %}
<h2>not available on this server because <code>--usernames</code> is enabled in the server config</h2>
{%- else %}
<div class="os win">
<pre>
net use <b>w:</b> \\{{ host }}\a{% if accs %} k /user:<b>{{ pw }}</b>{% endif %}
@ -234,7 +272,8 @@
<pre class="os mac">
open 'smb://<b>{{ pw }}</b>:k@{{ host }}/a'
</pre>
{% endif %}
{%- endif %}
{%- endif %}
@ -247,7 +286,7 @@
{ "Version": "15.0.0", "Name": "copyparty",
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
"Headers": {
{% if accs %}"pw": "<b>{{ pw }}</b>", {% endif %}"accept": "url"
{% if accs %}"pw": "<b>{{ unpw }}</b>", {% endif %}"accept": "url"
},
"DestinationType": "ImageUploader, TextUploader, FileUploader",
"Body": "MultipartFormData", "URL": "{response}",
@ -260,7 +299,7 @@
{ "Name": "copyparty",
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
"Headers": {
{% if accs %}"pw": "<b>{{ pw }}</b>", {% endif %}"accept": "url"
{% if accs %}"pw": "<b>{{ unpw }}</b>", {% endif %}"accept": "url"
},
"DestinationType": "ImageUploader, TextUploader, FileUploader",
"FileFormName": "f" }
@ -278,7 +317,9 @@
{ "Name": "copyparty",
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
"Headers": {
{% if accs %}"pw": "<b>{{ pw }}</b>",{% endif %}
{%- if accs %}
"pw": "<b>{{ unpw }}</b>",
{%- endif %}
"accept": "json"
},
"ResponseURL": "{{ '{{fileurl}}' }}",
@ -295,7 +336,7 @@
<pre class="dl" name="flameshot.sh">
#!/bin/bash
pw="<b>{{ pw }}</b>"
pw="<b>{{ unpw }}</b>"
url="http{{ s }}://{{ ep }}/{{ rvp }}"
filename="$(date +%Y-%m%d-%H%M%S).png"
flameshot gui -s -r | curl -sT- "$url$filename?want=url&pw=$pw" | xsel -ib

View file

@ -49,21 +49,47 @@ function setos(os) {
setos(WINDOWS ? 'win' : LINUX ? 'lin' : MACOS ? 'mac' : 'idk');
var pw = '';
var un, un0, pw, pw0, unpw, up0;
function setpw(e) {
ev(e);
if (!ebi('un0'))
return askpw();
modal.prompt('username:', '', function (v) {
if (!v)
return;
un = v;
un0 = ebi('un0').innerHTML;
var oa = QSA('b');
for (var a = 0; a < oa.length; a++)
if (oa[a].innerHTML == un0)
oa[a].textContent = un;
askpw();
});
}
function askpw() {
modal.prompt('password:', '', function (v) {
if (!v)
return;
pw = v;
var pw0 = ebi('pw0').innerHTML,
oa = QSA('b');
pw0 = ebi('pw0').innerHTML;
var oa = QSA('b');
for (var a = 0; a < oa.length; a++)
if (oa[a].innerHTML == pw0)
oa[a].textContent = v;
oa[a].textContent = pw;
if (un) {
unpw = un ? (un+':'+pw) : pw;
up0 = ebi('up0').innerHTML;
for (var a = 0; a < oa.length; a++)
if (oa[a].innerHTML == up0)
oa[a].textContent = unpw;
}
add_dls();
});
}

View file

@ -430,6 +430,15 @@ html.y textarea:focus {
.mdo code {
font-size: .96em;
}
html.z .mdo a>code,
html.y .mdo a>code {
color: inherit;
background: inherit;
background: rgba(0, 0, 0, 0.2);
padding-top: 0;
padding-bottom: 0;
border: none;
}
.mdo h1,
.mdo h2 {
line-height: 1.5em;

View file

@ -50,7 +50,7 @@ catch (ex) {
}
catch (ex) {
console.log('up2k init failed:', ex);
toast.err(10, 'could not initialze up2k\n\n' + basenames(ex));
toast.err(10, 'could not initialize up2k\n\n' + basenames(ex));
}
}
treectl.onscroll();
@ -732,7 +732,7 @@ function Donut(uc, st) {
tstrober = setInterval(strobe, 300);
if (uc.upsfx && actx && actx.state != 'suspended')
sfx();
sfx_nice();
// firefox may forget that filedrops are user-gestures so it can skip this:
if (uc.upnag && Notification && Notification.permission == 'granted')
@ -745,8 +745,10 @@ function Donut(uc, st) {
if (!txt)
clearInterval(tstrober);
}
}
function sfx() {
function sfx_nice() {
if (true) {
var osc = actx.createOscillator(),
gain = actx.createGain(),
gg = gain.gain,
@ -2831,7 +2833,7 @@ function up2k_init(subtle) {
if (!t.t_uploading)
t.t_uploading = Date.now();
pvis.seth(t.n, 1, "🚀 send");
pvis.seth(t.n, 1, "🚀 " + L.ul_send);
var chunksize = get_chunksize(t.size),
car = pcar * chunksize,
@ -3037,10 +3039,12 @@ function up2k_init(subtle) {
if (anymod(e))
return;
if (e.code == 'ArrowUp')
var k = e.key || e.code;
if (k == 'ArrowUp')
bumpthread(1);
if (e.code == 'ArrowDown')
if (k == 'ArrowDown')
bumpthread(-1);
}
@ -3103,7 +3107,8 @@ function up2k_init(subtle) {
ebi('u2szg').addEventListener('blur', read_u2sz);
ebi('u2szg').onkeydown = function (e) {
if (anymod(e)) return;
var n = e.code == 'ArrowUp' ? 1 : e.code == 'ArrowDown' ? -1 : 0;
var k = e.key || e.code,
n = k == 'ArrowUp' ? 1 : k == 'ArrowDown' ? -1 : 0;
if (!n) return;
this.value = parseInt(this.value) + n;
read_u2sz();
@ -3180,7 +3185,8 @@ function up2k_init(subtle) {
function kd_life(e) {
var el = e.target,
d = e.code == 'ArrowUp' ? 1 : e.code == 'ArrowDown' ? -1 : 0;
k = e.key || e.code,
d = k == 'ArrowUp' ? 1 : k == 'ArrowDown' ? -1 : 0;
if (anymod(e) || !d)
return;
@ -3421,6 +3427,7 @@ if (QS('#op_up2k.act'))
goto_up2k();
apply_perms({ "perms": perms, "frand": frand, "u2ts": u2ts });
fileman.render();
(function () {

View file

@ -1263,10 +1263,13 @@ function sethash(hv) {
function dl_file(url) {
console.log('DL [%s]', url);
var o = mknod('a');
qsr('#dlfth');
var o = mknod('a', 'dlfth');
o.setAttribute('href', url);
o.setAttribute('download', '');
o.click();
document.body.appendChild(o);
ebi('dlfth').click();
qsr('#dlfth');
}
@ -1821,12 +1824,12 @@ var modal = (function () {
};
var onkey = function (e) {
var k = (e.code || e.key) + '',
var k = (e.key || e.code) + '',
eok = ebi('modal-ok'),
eng = ebi('modal-ng'),
ae = document.activeElement;
if (k == 'Space' && ae && (ae === eok || ae === eng))
if ((k == 'Space' || k == 'Spacebar' || k == ' ') && ae && (ae === eok || ae === eng))
k = 'Enter';
if (k.endsWith('Enter')) {

View file

@ -1,3 +1,209 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0828-2014 `v1.19.7` chdir
## 🧪 new features
* new option `chdir` to change the PWD (process working-directory) before volumes are mapped 14555d58
## 🩹 bugfixes
* fix using empty folders as statefile storage ([v1.19.6](https://github.com/9001/copyparty/releases/tag/v1.19.6) made this a bit too strict) 0d96786e
* holding I/K to scroll through folders quickly now works better 914686ec
## 🔧 other changes
* #717 docker: fix the image repo metadata (thx @EmilyxFox!) 6f087117
* docker: change `$HOME` to `/state` 01cf20a0 d1f75229
* and use the new `chdir` option to preserve old config-file semantics 14555d58
* helps avoid statefiles accidentally landing in `/w` as a consequence of misconfiguration
## 🌠 fun facts
* this release was made at [RevSpace NL](https://a.ocv.me/pub/g/nerd-stuff/PXL_20250828_202820075.jpg?cache)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0827-2038 `v1.19.6` auth-precedence
## 🧪 new features
* #673 add Portuguese translation (thx anonymous!) 4b8c2215
* ...and enable the Polish translation (whoops) 8f235be6
* #689 add option to control authentication priority/precedence 543b7ea9
* url-parameter `?dl` forces file download instead of displaying in-browser 48d6224e
* #533 more ways to make the QR-code always-visible in the console 2848941e
* #695 option to log invalid xml from clients 28b93d79
* #552 configurable markdown newline behavior 0491123b
* and tweak the styling of monospace in links 68503444
## 🩹 bugfixes
* #628 FTP-server now accepts connections from IPv6 link-local addresses 978801d0
* incorrect assumption that all IPv6 link-local addresses start with `fe80` d39c74c1
* ftp: fix file rename d40f061a
* u2c: couldn't upload files located at the very top of the unix file hierarchy 599e82f2
* #699 markdown-editor: fix panic if the table-formatter is executed on something that isn't a table 4c042b3c
## 🔧 other changes
* #696 a volume can be one single file, not just folders aa1c9213
* #442 strongly prefer XDG_CONFIG_HOME as config location 35472557
* #691 album-art collected from audio-files can now become folder thumbnails 0b50fde3
* allow spaces in more of the comma-separated options d30240b4
* docs:
* mention config requirements for [syncing folders](https://github.com/9001/copyparty/#folder-sync) with u2c 6cd0a396 59f142cd
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0821-2319 `v1.19.5` it runs on iOS
## 🧪 new features
* #328 run copyparty on iPhones; see [install on iOS](https://github.com/9001/copyparty#install-on-iOS) in the readme ca98d54f
* cannot run in the background, doesn't have full access to your files, and is slightly buggy, but it *works*
* [running on android](https://github.com/9001/copyparty#install-on-android) gives you a much better experience
* save the qr-code to a file (txt/svg/png) 202ddeac
## 🩹 bugfixes
* #661 fix incorrect `rproxy` hint in the logs 6c76614e
* #649 fix js-crash when tapping in the exactly correct place (thx @hahaslav for debugging!) 0de07d8e
* #628 ftpd: fix banning IPv6 clients 6d76254c
## 🔧 other changes
* #296 nixos: support non-flake setups (thx @Sorixelle!) 20ef74cd 32593670
* config-parser catches and explains a few more common mistakes cc65b1b5
* docs:
* #490, #199: readme: confirm that combining copyparty and syncthing is safe c51371c7
* #377 improved authelia docker example (thx @xFuture603!) cd8771fa
* mention the homebrew formulae f9cb2c15
* #651 versus.md: fix hfs3 comparison (thx @rejetto!) 7a4973fa
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0817-1556 `v1.19.4` take two (fix cfg vols)
## this upgrade is a one-way ticket
* your up2k database (`.hist/up2k.db`), used by the `e2d` filesystem indexing feature, **will be upgraded to a new format** which older copyparty versions cannot read. A backup of each database will be created automatically, named `up2k.db.bak.SOMETHING.v5`. If you need to downgrade to a previous version: Shutdown copyparty, delete these files: `up2k.db up2k.db-shm up2k.db-wal` and then copy `up2k.db.bak.*.v5` to `up2k.db`
## 🧪 new features
* new translations:
* #551 Swedish (thx @Bevinsky!) d676a86f
* #551 Korean (thx @nyqui!) 4e878d2f
* #581 new theme: phi95 (thx @varphi-online!) d8662aeb
* #567 .raw image thumbnails (thx @ar-nelson!) 0177a9b4
* available in docker-images `iv` and `dj`
* #561 epub thumbnails (thx @Scotsguy!) 9435e6b2
* #252 music thumbnails use embdded coverart if available 98d117b8
* thumbnails folder `.hist/th` must be deleted to take effect
* #530 show username of uploaders in file listings; requires `a` (admin) permission 4df033ec
* #604 a new group `@acct` which automatically contains all known usernames 68907eaf
* controlpanel has a dedicated "logout all sessions" button, similar to the logout-link in the browser f4a3fba2
* #397 accounts can be restricted to certian IPs 62e072a2
* #504 automatic login through tailscale auth a4649d1e
* #533 sticky qr-code with `--qr-pin 1` 1ebe06f5
* #572 button to abort copy/move 715d374e
* #618 "download selected files" didn't work on firefox 52 (winxp) dcc6b1b4
* max number of cookies to allow can be configured 6303effe
* good if you have too many selfhosted services on one domain (but will beware of the spec-mandataed max length of the cookie field!)
## 🩹 bugfixes
* fix xvol/xdev edgecases:
* #603 rootless vfs 554cc2f3
* false-positive with overlapping volumes d9046f7e
* #573 ftp: attempting an upload into read-only folder no longer kills the connection 3aa8b7aa
* #306 adjust navpane for `--rp-loc` (location-based proxying)
* #556 more sensible config expansion order f4727f8e
* #624 ...which broke things bf1fdcab
* the video player now stays fullscreen between videos 782e2f1d
* heif thumbnailing with libvips
## 🔧 other changes
* #253 build nix-packages from source (thx @toast003, @chinponya!) 187cae25
* #616 logfiles will have a plaintext severity column if `--no-ansi` d4cf42e7
* #598 separate option `--ac-convt` for audio transcoding timeout d5623057
* #596 users with a blank password gets a strong random-generated one 7f448750
* copyparty.exe: upgrade to python 3.13.7
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0810-1226 `v1.19.1` archlinux fix
## 🧪 new features
* new translations:
* #486 French (thx @Tr3yWay996, @Packingdustry, @Alee14, @jakubiakfr, @Equinoxs!) e9ddfccf 7aa21483 b87f8f1b
* #463 Polish (thx @pufereq and @daimond113!) 392a4db5
* #537 Nynorsk (thx @chinatsu!) 3931bc27
* #549 custom mdns domain 3c78c6a8
## 🩹 bugfixes
* #539 FTP glitches when running on windows 8ba98877
* #555 global-config didn't load through PRTY_CONFIG (thx @icxes!) 074e106e
* macos: could take a while to establish webdav connection from finder a01870b7
* ux:
* dropdown colors 347cf6a5
* case-sensitivity in filters e5e82295
* iOS being too enthusiastic about using saved passwords 03acd65e
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0807-2213 `v1.19.0` usernames
## 🧪 new features
* #511 login with username and password (not just password) can now optionally be enabled with `--usernames` 346515cc
* if you have enabled password hashing (`ah-alg: argon2` or similar) then you will need to hash your passwords again after enabling usernames, hashing them as `username:password:`
* #468 add Greek translation (thx @chamdim!) 50f46187 392abd06
* #471 add Czech translation (thx @kubakubakuba!) c9556583
* #515 support systemd socket acivation (thx @mati1210!) 9b9d2a92
* #523 add QR-code to the connectpage bcc3b156
* #513 optional EOL-conversion for texteditor 8b31ed88
* controlpanel refresh-button now toggles automatic refresh 7ae84dea
## 🩹 bugfixes
* fix stuck uploads when the up2k database (`e2d`) is not enabled 4a043568
* if more than 60'000 files were uploaded and there were several dupes of some files, they could get stuck and never upload
* upload performance is improved remarkably by enabling `e2d` so such huge uploads non-e2d had not been tested in a long time
* #467 #470 fix ui-crash when exporting links of all uploaded files to clipboard (thx @geekalaa!) 0df1901f
* #487 fix ui-crash when the location url-part is `//` 0f55a1ae
* fix viewing `.MD` files (8a0746c6)
## 🔧 other changes
* when a reverse-proxy is detected, force explicit configuration of `--rproxy` to obtain correct client IP 3f8cb7e8
* a bit inconvenient, but helps prevent potentially-dangerous misconfiguration
* the necessary configuration changes are explained in the serverlog (you can't miss it)
* thanks to @person4268 for pointing out that there was room for improvements!
* failed login attempts now only log a sha512 hash of the provided password
* to see login-attempts with incorrect passwords as plaintext like before, `log-badpwd: 1`
* #502 add systemd user services and templated services (thx @icxes!) 34d98e99
* #475 improve helptext for multivalue global-options c2ac57a2
* #475 add [chungus.conf](https://github.com/9001/copyparty/blob/hovudstraum/docs/chungus.conf), massive extensive nonsensical demo config b664ebb0
* try to detect proxies with incorrect caching behavior 9e980bb5
* recent-uploads now support ie9 a57f7cc2
* languages and themes are now dropdowns a9ee4f24
* copyparty.exe: upgrade python to 3.13.6 a98360f2
* introduce [copyparty-en.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-en.py), english-only edition of copyparty-sfx.py to save space 33497e6b
## 🗿 known issues
* the `copyparty.pyz` in this release is english-only, and does not include the translations -- they got lost in transit while adjusting the buildscripts to make `copyparty-en.py`
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0804-0013 `v1.18.10` idp speedboost

View file

@ -1083,7 +1083,7 @@
th-x3: n # default
# image decoders, in order of preference
th-dec: vips,pil,ff # default
th-dec: vips,pil,raw,ff # default
# disable jpg output
th-no-jpg
@ -1115,6 +1115,9 @@
# image formats to decode using pyvips
th-r-vips: a,very,long,list,of,file,extensions # hint
# image formats to decode using rawpy
th-r-raw: a,very,long,list,of,file,extensions # hint
# image formats to decode using ffmpeg
th-r-ffi: a,very,long,list,of,file,extensions # hint

View file

@ -160,6 +160,7 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
| method | params | result |
|--|--|--|
| GET | `?dl` | download file (don't show in-browser) |
| GET | `?ls` | list files/folders at URL as JSON |
| GET | `?ls&dots` | list files/folders at URL as JSON, including dotfiles |
| GET | `?ls=t` | list files/folders at URL as plaintext |
@ -328,7 +329,7 @@ if you don't need all the features, you can repack the sfx and save a bunch of s
the features you can opt to drop are
* `cm`/easymde, the "fancy" markdown editor, saves ~89k
* `hl`, prism, the syntax hilighter, saves ~41k
* `hl`, prism, the syntax highlighter, saves ~41k
* `fnt`, source-code-pro, the monospace font, saves ~9k
for the `re`pack to work, first run one of the sfx'es once to unpack it
@ -354,7 +355,7 @@ pip install mutagen # audio metadata
pip install pyftpdlib # ftp server
pip install partftpy # tftp server
pip install impacket # smb server -- disable Windows Defender if you REALLY need this on windows
pip install Pillow pyheif-pillow-opener # thumbnails
pip install Pillow pillow-heif # thumbnails
pip install pyvips # faster thumbnails
pip install psutil # better cleanup of stuck metadata parsers on windows
pip install black==21.12b0 click==8.0.2 bandit pylint flake8 isort mypy # vscode tooling

View file

@ -6,7 +6,7 @@
[global]
e2dsa # enable file indexing and filesystem scanning
e2ts # enable multimedia indexing
ansi # enable colors in log messages
ansi # enable colors in log messages (both in logfiles and stdout)
# q, lo: /cfg/log/%Y-%m%d.log # log to file instead of docker

View file

@ -1,5 +1,6 @@
services:
---
services:
copyparty:
image: copyparty/ac:latest
container_name: copyparty

View file

@ -8,7 +8,7 @@ to try this out with minimal adjustments:
* login to https://fs.example.com/ with username `authelia` password `authelia`
to use this in a safe and secure manner:
* follow a guide on setting up authelia properly (TODO:link) and use the copyparty-specific parts of this folder as inspiration for your own config; namely the `cpp` subfolder and the `copyparty` service in `docker-compose.yml`
* follow a guide on setting up [authelia](https://www.authelia.com/integration/proxies/traefik/#docker-compose) properly and use the copyparty-specific parts of this folder as inspiration for your own config; namely the `cpp` subfolder and the `copyparty` service in `docker-compose.yml`
this folder is based on:
* https://github.com/authelia/authelia/tree/39763aaed24c4abdecd884b47357a052b235942d/examples/compose/lite
@ -16,20 +16,18 @@ this folder is based on:
incomplete list of modifications made:
* support for running with podman as root on fedora (`:z` volumes, `label:disable`)
* explicitly using authelia `v4.38.0-beta3` because config syntax changed since last stable release
* disabled automatic letsencrypt certificate signing
* reduced logging from debug to info
* added a warning that traefik is given access to the docker socket (as recommended by traefik docs) which means traefik is able to break out of the container and has full root access on the host machine
* implemented a docker socket-proxy to not bind the docker.socket directly to traefik
* using valkey instead of redis for caching
# security
there is probably/definitely room for improvement in this example setup. Some ideas taken from [github issue #62](https://github.com/9001/copyparty/issues/62):
* Add in a redis password to limit attacker lateral movement in the system
* Move redis to a private network shared with just authelia
* Pin to image hashes (or go all in on updates and add `watchtower`)
* Move valkey to a private network shared with just authelia
* Add `watchtower` to manage your image version updates
* Drop bridge networking for just exposing traefik's public ports
* Configure docker for non-root access to docker socket and then move traefik to use [non-root perms](https://docs.docker.com/engine/security/rootless/)
if you manage to improve on any of this, especially in a way that might be useful for other people, consider sending a PR :>
@ -47,4 +45,4 @@ currently **not optimal,** at least when compared to running the python sfx outs
authelia is behaving strangely, handling 340 requests per second for a while, but then it suddenly drops to 75 and stays there...
I'm assuming all of the performance issues is due to a misconfiguration of authelia/traefik/docker on my end, but I don't relly know where to start
I'm assuming all of the performance issues is due to a misconfiguration of authelia/traefik/docker on my end, but I don't really know where to start

View file

@ -1,15 +1,14 @@
# based on https://github.com/authelia/authelia/blob/39763aaed24c4abdecd884b47357a052b235942d/examples/compose/lite/authelia/configuration.yml
# Authelia configuration
# This secret can also be set using the env variables AUTHELIA_JWT_SECRET_FILE
jwt_secret: a_very_important_secret
identity_validation:
reset_password:
jwt_secret: 'a_very_important_secret_so_please_change_this'
server:
address: 'tcp://:9091'
log:
level: info # debug
level: info
totp:
issuer: authelia.com
@ -21,29 +20,26 @@ authentication_backend:
access_control:
default_policy: deny
rules:
# Rules applied to everyone
- domain: traefik.example.com
policy: one_factor
- domain: auth.example.com
policy: bypass # Allow access to the login UI
- domain: fs.example.com
policy: one_factor
session:
# This secret can also be set using the env variables AUTHELIA_SESSION_SECRET_FILE
secret: unsecure_session_secret
cookies:
- name: authelia_session
domain: example.com # Should match whatever your root protected domain is
domain: example.com # this should match whatever your root protected domain is
default_redirection_url: https://fs.example.com
authelia_url: https://authelia.example.com/
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
redis:
host: redis
host: valkey
port: 6379
# This secret can also be set using the env variables AUTHELIA_SESSION_REDIS_PASSWORD_FILE
# password: authelia
password: your_secure_password_here
regulation:
max_retries: 3
@ -58,9 +54,7 @@ storage:
notifier:
disable_startup_check: true
smtp:
username: test
# This secret can also be set using the env variables AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE
password: password
host: mail.example.com
port: 25
sender: admin@example.com
address: 'smtp://127.0.0.1:25'
username: 'test'
password: 'password'
sender: "Authelia <admin@example.com>"

View file

@ -1,4 +1,4 @@
version: '3.3'
---
networks:
net:
@ -6,7 +6,7 @@ networks:
services:
copyparty:
image: copyparty/ac
image: copyparty/ac:latest
container_name: idp_copyparty
user: "1000:1000" # should match the user/group of your fileshare volumes
volumes:
@ -19,19 +19,19 @@ services:
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.copyparty.rule=Host(`fs.example.com`)'
- 'traefik.http.routers.copyparty.entrypoints=https'
- 'traefik.http.routers.copyparty.entrypoints=websecure'
- 'traefik.http.routers.copyparty.tls=true'
- 'traefik.http.routers.copyparty.tls.certresolver=letsencrypt' # ← THIS IS CRUCIAL
- 'traefik.http.routers.copyparty.middlewares=authelia@docker'
stop_grace_period: 15s # thumbnailer is allowed to continue finishing up for 10s after the shutdown signal
environment:
LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE
# enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram)
PYTHONUNBUFFERED: 1
# ensures log-messages are not delayed (but can reduce speed a tiny bit)
authelia:
image: authelia/authelia:v4.38.0-beta3 # the config files in the authelia folder use the new syntax
image: authelia/authelia:4.39.5@sha256:023e02e5203dfa0ebaee7a48b5bae34f393d1f9cada4a9df7fbf87eb1759c671
container_name: idp_authelia
volumes:
- ./authelia:/config:z
@ -40,25 +40,23 @@ services:
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.authelia.rule=Host(`authelia.example.com`)'
- 'traefik.http.routers.authelia.entrypoints=https'
- 'traefik.http.routers.authelia.entrypoints=websecure'
- 'traefik.http.routers.authelia.tls=true'
#- 'traefik.http.routers.authelia.tls.certresolver=letsencrypt' # uncomment this to enable automatic certificate signing (1/2)
- 'traefik.http.routers.authelia.tls.certresolver=letsencrypt'
- 'traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/authz/forward-auth?authelia_url=https://authelia.example.com'
- 'traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true'
- 'traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email'
expose:
- 9091
restart: unless-stopped
healthcheck:
disable: true
environment:
- TZ=Etc/UTC
redis:
image: redis:7.2.4-alpine3.19
container_name: idp_redis
valkey:
image: valkey/valkey:8.1.3-alpine3.22@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4
container_name: idp_valkey
volumes:
- ./redis:/data:z
- ./valkey:/data:z
networks:
- net
expose:
@ -66,40 +64,55 @@ services:
restart: unless-stopped
environment:
- TZ=Etc/UTC
- VALKEY_EXTRA_FLAGS=--requirepass your_secure_password_here
socket-proxy:
image: lscr.io/linuxserver/socket-proxy:3.2.3@sha256:63d2e0ce6bb0d12dfdbde5c3af31d08fee343ec3801a050c8197a3f5ffae8bed
container_name: idp_socket_proxy
environment:
- CONTAINERS=1
- NETWORKS=1
- EVENTS=1
- PING=1
- VERSION=1
- LOG_LEVEL=warning
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /run
networks:
- net
restart: unless-stopped
expose:
- 2375
traefik:
image: traefik:2.11.0
image: traefik:3.5.0@sha256:4e7175cfe19be83c6b928cae49dde2f2788fb307189a4dc9550b67acf30c11a5
container_name: idp_traefik
volumes:
- ./traefik:/etc/traefik:z
- /var/run/docker.sock:/var/run/docker.sock # WARNING: this gives traefik full root-access to the host OS, but is recommended/required(?) by traefik
security_opt:
- label:disable # disable selinux because it (rightly) blocks access to docker.sock
networks:
- net
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.api.rule=Host(`traefik.example.com`)'
- 'traefik.http.routers.api.entrypoints=https'
- 'traefik.http.routers.api.service=api@internal'
- 'traefik.http.routers.api.tls=true'
#- 'traefik.http.routers.api.tls.certresolver=letsencrypt' # uncomment this to enable automatic certificate signing (2/2)
- 'traefik.http.routers.api.middlewares=authelia@docker'
ports:
- '80:80'
- '443:443'
command:
- '--api'
- '--providers.docker=true'
- '--global.sendAnonymousUsage=false'
- '--providers.docker.endpoint=tcp://socket-proxy:2375'
- '--providers.docker.exposedByDefault=false'
- '--entrypoints.http=true'
- '--entrypoints.http.address=:80'
- '--entrypoints.http.http.redirections.entrypoint.to=https'
- '--entrypoints.http.http.redirections.entrypoint.scheme=https'
- '--entrypoints.https=true'
- '--entrypoints.https.address=:443'
- '--entrypoints.web.address=:80'
- '--entrypoints.web.http.redirections.entrypoint.to=websecure'
- '--entrypoints.web.http.redirections.entrypoint.scheme=https'
- '--entrypoints.websecure.address=:443'
- '--certificatesResolvers.letsencrypt.acme.email=your-email@your-domain.com'
- '--certificatesResolvers.letsencrypt.acme.storage=/etc/traefik/acme.json'
- '--certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=http'
- '--log=true'
- '--log.level=WARNING' # DEBUG
- '--certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=web'
- '--log.level=INFO'
depends_on:
- socket-proxy

View file

@ -71,7 +71,7 @@ avg() { awk 'function pr(ncsz) {if (nsmp>0) {printf "%3s %s\n", csz, sum/nsmp} c
python3 -um copyparty -nw -v srv::rw -i 127.0.0.1 2>&1 | tee log
cat log | awk '!/"purl"/{next} {s=$1;sub(/[^m]+m/,"");gsub(/:/," ");t=60*(60*$1+$2)+$3} t<p{t+=86400} !a{a=t;sa=s} {b=t;sb=s} END {print b-a,sa,sb}'
# or if the client youre measuring dies for ~15sec every once ina while and you wanna filter those out,
# or if the client you're measuring dies for ~15sec every once ina while and you wanna filter those out,
cat log | awk '!/"purl"/{next} {s=$1;sub(/[^m]+m/,"");gsub(/:/," ");t=60*(60*$1+$2)+$3} t<p{t+=86400} !p{a=t;p=t;r=0;next} t-p>1{printf "%.3f += %.3f - %.3f (%.3f) # %.3f -> %.3f\n",r,p,a,p-a,p,t;r+=p-a;a=t} {p=t} END {print r+p-a}'
@ -337,3 +337,5 @@ mk && t0="$(date)" && while true; do date -s "$(date '+ 1 hour')"; systemd-tmpfi
mk && sudo -u ed flock /tmp/foo sleep 40 & sleep 1; ps aux | grep -E 'sleep 40$' && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; done; echo "$t0"
mk && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; tar -cf/dev/null /tmp/foo; done; echo "$t0"
# number of megabytes downloaded since some date
awk </var/log/wjaycore.out '/^..36m2025-05-20/{o=1} !o{next} !/ plain 20[06](,| \[[^,]+\],) +[0-9.]+.\[33m[KM] .* n[0-9]+$/{next} {v=$0;sub(/.* plain 20[06](,| \[[^,]+\],) +/,"",v);sub(/ .*/,"",v);u=v;sub(/.\[.*/,"",v);sub(/.*m/,"",u);$0=u} /[KMG]/{v*=1024} /[MG]/{v*=1024} /G/{v*=1024} {t+=v} END{printf "%d\n",t/(1024*1024)}'

View file

@ -68,7 +68,7 @@ currently up to date with [awesome-selfhosted](https://github.com/awesome-selfho
* [kodbox](https://github.com/kalcaddle/kodbox) ([review](#kodbox)) appears to be a fantastic alternative if you're not worried about running chinese software, with several advantages over copyparty
* but anything you want to share must be moved into the kodbox filesystem
* [seafile](https://github.com/haiwen/seafile) ([review](#seafile)) and [nextcloud](https://github.com/nextcloud/server) ([review](#nextcloud)) could be decent alternatives if you need something heavier than copyparty
* but their [license](https://snyk.io/learn/agpl-license/) is [problematic](https://opensource.google/documentation/reference/using/agpl-policy)
* but their [license (AGPL)](https://snyk.io/learn/agpl-license/) is [thorny](https://opensource.google/documentation/reference/using/agpl-policy)
* and copyparty is way better at uploads in particular (resumable, accelerated)
* and anything you want to share must be moved into the respective filesystems
* [filebrowser](https://github.com/filebrowser/filebrowser) ([review](#filebrowser)) and [dufs](https://github.com/sigoden/dufs) ([review](#dufs)) are simpler copyparties but with a settings gui
@ -123,14 +123,14 @@ symbol legend,
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
| intuitive UX | | | █ | █ | █ | | █ | █ | █ | █ | █ | █ | █ |
| config GUI | | █ | █ | █ | █ | | | █ | █ | █ | | █ | █ |
| good documentation | | | | █ | █ | █ | █ | | | █ | █ | | |
| good documentation | | | | █ | █ | █ | █ | | | █ | █ | | |
| runs on iOS | | | | | | | | | | | | | |
| runs on Android | █ | | | | | █ | | | | | | | |
| runs on Android | █ | | | | | █ | | | | | | | |
| runs on WinXP | █ | █ | | | | █ | | | | | | | |
| runs on Windows | █ | █ | █ | █ | █ | █ | █ | | █ | █ | █ | █ | |
| runs on Linux | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
| runs on Macos | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | |
| runs on FreeBSD | █ | | | • | █ | █ | █ | • | █ | █ | | █ | |
| runs on FreeBSD | █ | | | • | █ | █ | █ | • | █ | █ | | █ | |
| runs on Risc-V | █ | | | █ | █ | █ | | • | | █ | | | |
| portable binary | █ | █ | █ | | | █ | █ | | | █ | | █ | █ |
| zero setup, just go | █ | █ | █ | | | | █ | | | █ | | | █ |
@ -140,7 +140,7 @@ symbol legend,
* `zero setup` = you can get a mostly working setup by just launching the app, without having to install any software or configure whatever
* `a`/copyparty remarks:
* no gui for server settings; only for client-side stuff
* can theoretically run on iOS / iPads using [iSH](https://ish.app/), but only the iPad will offer sufficient multitasking i think
* runs on iOS / iPads using [a-Shell](https://holzschu.github.io/a-Shell_iOS/) (pretty good) or [iSH](https://ish.app/) (very slow) but cannot run in the background and is not able to share all of your phone storage (just a separate dedicated folder)
* [android app](https://f-droid.org/en/packages/me.ocv.partyup/) is for uploading only
* no iOS app but has [shortcuts](https://github.com/9001/copyparty#ios-shortcuts) for easy uploading
* `b`/hfs2 runs on linux through wine
@ -161,7 +161,7 @@ symbol legend,
| upload | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | | █ | █ |
| parallel uploads | █ | | | █ | █ | | • | | █ | █ | █ | | █ |
| resumable uploads | █ | | █ | | | | | | █ | █ | █ | | |
| upload segmenting | █ | | | █ | | | | █ | █ | █ | █ | | █ |
| upload segmenting | █ | | | █ | | | | █ | █ | █ | █ | | █ |
| upload acceleration | █ | | | | | | | | █ | | █ | | |
| upload verification | █ | | | █ | █ | | | | █ | | | | |
| upload deduplication | █ | | | | █ | | | | █ | | | | |
@ -169,7 +169,7 @@ symbol legend,
| CTRL-V from device | █ | | | █ | | | | | | | | | |
| race the beam ("p2p") | █ | | | | | | | | | | | | |
| "tail -f" streaming | █ | | | | | | | | | | | | |
| keep last-modified time | █ | | | █ | █ | █ | | | | | | █ | |
| keep last-modified time | █ | | | █ | █ | █ | | | | | | █ | |
| upload rules | | | | | | | | | | | | | |
| ┗ max disk usage | █ | █ | █ | | █ | | | | █ | | | █ | █ |
| ┗ max filesize | █ | | | | | | | █ | | | █ | █ | █ |
@ -251,7 +251,7 @@ symbol legend,
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
| config from cmd args | █ | | | | | █ | █ | | | █ | | | |
| config from cmd args | █ | | | | | █ | █ | | | █ | | | |
| config files | █ | █ | █ | | | █ | | █ | | █ | • | | |
| runtime config reload | █ | █ | █ | | | | | █ | █ | █ | █ | | █ |
| same-port http / https | █ | | | | | | | | | | | | |
@ -276,10 +276,10 @@ symbol legend,
| per-account chroot | | | | | | | | | | | | █ | |
| single-sign-on | | | | █ | █ | | | | • | | | | |
| token auth | | | | █ | █ | | | █ | | | | | █ |
| 2fa | | | | █ | █ | | | | | | | █ | |
| 2fa | | | / | █ | █ | | | | | | | █ | |
| per-volume permissions | █ | █ | █ | █ | █ | █ | █ | | █ | █ | | █ | █ |
| per-folder permissions | | | | █ | █ | | █ | | █ | █ | | █ | █ |
| per-file permissions | | | | █ | █ | | █ | | █ | | | | █ |
| per-folder permissions | | | | █ | █ | | █ | | █ | █ | | █ | █ |
| per-file permissions | | | | █ | █ | | █ | | █ | | | | █ |
| per-file passwords | █ | | | █ | █ | | █ | | █ | | | | █ |
| unmap subfolders | █ | | █ | | | | █ | | | █ | | • | |
| index.html blocks list | | | | | | | █ | | | • | | | |
@ -297,13 +297,13 @@ symbol legend,
| full sync | | | | █ | █ | | | | | | | | |
| speed throttle | | █ | █ | | | █ | | | █ | | | █ | |
| anti-bruteforce | █ | █ | █ | █ | █ | | | | • | | | █ | • |
| dyndns updater | | █ | | | | | | | | | | | |
| dyndns updater | | █ | | | | | | | | | | | |
| self-updater | | | █ | | | | | | | | | | █ |
| log rotation | █ | | █ | █ | █ | | | • | █ | | | █ | • |
| upload tracking / log | █ | █ | • | █ | █ | | | █ | █ | | | | █ |
| prometheus metrics | █ | | | █ | | | | | | | | █ | |
| curl-friendly ls | █ | | | | | | | | | | | | |
| curl-friendly upload | █ | | | | | █ | █ | • | | | | | |
| curl-friendly upload | █ | | | | | █ | █ | • | | | | | |
* `unmap subfolders` = "shadowing"; mounting a local folder in the middle of an existing filesystem tree in order to disable access below that path
* `files stored as-is` = uploaded files are trivially readable from the server HDD, not sliced into chunks or in weird folder structures or anything like that
@ -332,7 +332,8 @@ symbol legend,
* `upload tracking / log` in main logfile
* `m`/arozos:
* `2fa` maybe possible through LDAP/Oauth
* `c`/hfs3
* `2fa` available by installing a plugin
## client features
@ -342,18 +343,18 @@ symbol legend,
| themes | █ | █ | █ | █ | | | | | █ | | | | |
| directory tree nav | █ | | | | █ | | | | █ | | | | |
| multi-column sorting | █ | | | | | | | | | | | | |
| thumbnails | █ | | | | | | | █ | █ | | | | █ |
| ┗ image thumbnails | █ | | | █ | █ | | | █ | █ | █ | | | █ |
| thumbnails | █ | | / | | | | | █ | █ | | | | █ |
| ┗ image thumbnails | █ | | / | █ | █ | | | █ | █ | █ | | | █ |
| ┗ video thumbnails | █ | | | █ | █ | | | | █ | | | | █ |
| ┗ audio spectrograms | █ | | | | | | | | | | | | |
| audio player | █ | | | █ | █ | | | | █ | | | | █ |
| ┗ gapless playback | █ | | | | | | | | • | | | | |
| ┗ audio equalizer | █ | | | | | | | | | | | | |
| ┗ waveform seekbar | █ | | | | | | | | | | | | |
| ┗ OS integration | █ | | | | | | | | | | | | |
| ┗ OS integration | █ | | | | | | | | | | | | |
| ┗ transcode to lossy | █ | | | | | | | | | | | | |
| video player | █ | | | █ | █ | | | | █ | █ | | | █ |
| ┗ video transcoding | | | | | | | | | █ | | | | |
| video player | █ | | | █ | █ | | | | █ | █ | | | █ |
| ┗ video transcoding | | | / | | | | | | █ | | | | |
| audio BPM detector | █ | | | | | | | | | | | | |
| audio key detector | █ | | | | | | | | | | | | |
| search by path / name | █ | █ | █ | █ | █ | | █ | | █ | █ | | | |
@ -366,15 +367,15 @@ symbol legend,
| undo recent uploads | █ | | | | | | | | | | | | |
| create directories | █ | | █ | █ | █ | | █ | █ | █ | █ | █ | █ | █ |
| image viewer | █ | | █ | █ | █ | | | | █ | █ | █ | | █ |
| markdown viewer | █ | | | | █ | | | | █ | | | | █ |
| markdown viewer | █ | | / | | █ | | | | █ | | | | █ |
| markdown editor | █ | | | | █ | | | | █ | | | | █ |
| readme.md in listing | █ | | | █ | | | | | | | | | |
| readme.md in listing | █ | | / | █ | | | | | | | | | |
| rename files | █ | █ | █ | █ | █ | | █ | | █ | █ | █ | █ | █ |
| batch rename | █ | | | | | | | | █ | | | | |
| cut / paste files | █ | █ | | █ | █ | | | | █ | | | | █ |
| cut / paste files | █ | █ | | █ | █ | | | | █ | | | | █ |
| move files | █ | █ | █ | █ | █ | | █ | | █ | █ | █ | | █ |
| delete files | █ | █ | █ | █ | █ | | █ | █ | █ | █ | █ | █ | █ |
| copy files | | | | | █ | | | | █ | █ | █ | | █ |
| copy files | | | / | | █ | | | | █ | █ | █ | | █ |
* `single-page app` = multitasking; possible to continue navigating while uploading
* `audio player » os-integration` = use the [lockscreen](https://user-images.githubusercontent.com/241032/142711926-0700be6c-3e31-47b3-9928-53722221f722.png) or [media hotkeys](https://user-images.githubusercontent.com/241032/215347492-b4250797-6c90-4e09-9a4c-721edf2fb15c.png) to play/pause, prev/next song
@ -383,8 +384,6 @@ symbol legend,
* `undo recent uploads` = accounts without delete permissions have a time window where they can undo their own uploads
* `a`/copyparty has teeny-tiny skips playing gapless albums depending on audio codec (opus best)
* `b`/hfs2 has a very basic directory tree view, not showing sibling folders
* `c`/hfs3 remarks:
* audio playback does not continue into next song
* `f`/rclone can do some file management (mkdir, rename, delete) when hosting througn webdav
* `j`/filebrowser remarks:
* audio playback does not continue into next song
@ -396,9 +395,9 @@ symbol legend,
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
| OS alert on upload | | | | | | | | | | | | | |
| discord | | | | | | | | | | | | | |
| ┗ announce uploads | | | | | | | | | | | | | |
| OS alert on upload | | | | | | | | | | | | | |
| discord | | | | | | | | | | | | | |
| ┗ announce uploads | | | | | | | | | | | | | |
| ┗ custom embeds | | | | | | | | | | | | | |
| sharex | █ | | | █ | | █ | | █ | | | | | |
| flameshot | | | | | | █ | | | | | | | |
@ -471,10 +470,8 @@ symbol legend,
* vfs with gui config, per-volume permissions
* tested locally, v0.53.2 on archlinux
* 🔵 uploads are resumable
* ⚠️ uploads are not segmented; max upload size 100 MiB on cloudflare
* ⚠️ uploads are not accelerated (copyparty is 3x faster across the atlantic)
* ⚠️ uploads are not integrity-checked
* ⚠️ copies the file after upload; need twice filesize free disk space
* ⚠️ uploading small files is decent; `107` files per sec (copyparty does `670`/sec, 6x faster)
* ⚠️ doesn't support crazy filenames
* ✅ config GUI
@ -575,7 +572,7 @@ symbol legend,
* ✅ file tags; file discussions!?
* ✅ video transcoding
* ✅ unzip uploaded archives
* ✅ IDE with syntax hilighting
* ✅ IDE with syntax highlighting
* ✅ wysiwyg editor for openoffice files
## [filebrowser](https://github.com/filebrowser/filebrowser)

View file

@ -12,19 +12,7 @@
}:
{
nixosModules.default = ./contrib/nixos/modules/copyparty.nix;
overlays.default = final: prev: rec {
copyparty = final.python3.pkgs.callPackage ./contrib/package/nix/copyparty {
ffmpeg = final.ffmpeg-full;
};
partyfuse = prev.callPackage ./contrib/package/nix/partyfuse {
inherit copyparty;
};
u2c = prev.callPackage ./contrib/package/nix/u2c {
inherit copyparty;
};
};
overlays.default = import ./contrib/package/nix/overlay.nix;
}
// flake-utils.lib.eachDefaultSystem (
system:
@ -54,8 +42,6 @@
packages = {
inherit (pkgs)
copyparty
partyfuse
u2c
;
default = self.packages.${system}.copyparty;
};

View file

@ -1,7 +1,7 @@
FROM alpine:latest
WORKDIR /z
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
org.opencontainers.image.source="https://github.com/9001/copyparty" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="copyparty-ac" \
org.opencontainers.image.description="copyparty with Pillow and FFmpeg (image/audio/video thumbnails, audio transcoding, media tags)"
@ -16,6 +16,6 @@ COPY i/dist/copyparty-sfx.py innvikler.sh ./
ADD base ./base
RUN ash innvikler.sh ac
WORKDIR /w
WORKDIR /state
EXPOSE 3923
ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"]
ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"]

View file

@ -1,7 +1,7 @@
FROM alpine:latest
WORKDIR /z
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
org.opencontainers.image.source="https://github.com/9001/copyparty" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="copyparty-dj" \
org.opencontainers.image.description="copyparty with all optional dependencies, including musical key / bpm detection"
@ -19,13 +19,16 @@ RUN apk add -U !pyc \
vips-jxl vips-heif vips-poppler vips-magick \
py3-numpy fftw libsndfile \
vamp-sdk vamp-sdk-libs \
libraw py3-numpy cython \
&& apk add -t .bd \
bash wget gcc g++ make cmake patchelf \
python3-dev ffmpeg-dev fftw-dev libsndfile-dev \
py3-wheel py3-numpy-dev libffi-dev \
vamp-sdk-dev \
libraw-dev py3-numpy-dev \
&& rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \
&& python3 -m pip install pyvips \
&& python3 -m pip install "$(wget -O- https://api.github.com/repos/letmaik/rawpy/releases/latest | awk -F\" '$2=="tarball_url"{print$4}')" \
&& bash install-deps.sh \
&& apk del py3-pip .bd \
&& chmod 777 /root \
@ -35,6 +38,6 @@ COPY i/dist/copyparty-sfx.py innvikler.sh ./
ADD base ./base
RUN ash innvikler.sh dj
WORKDIR /w
WORKDIR /state
EXPOSE 3923
ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"]
ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"]

View file

@ -1,7 +1,7 @@
FROM alpine:latest
WORKDIR /z
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
org.opencontainers.image.source="https://github.com/9001/copyparty" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="copyparty-im" \
org.opencontainers.image.description="copyparty with Pillow and Mutagen (image thumbnails, media tags)"
@ -15,6 +15,6 @@ COPY i/dist/copyparty-sfx.py innvikler.sh ./
ADD base ./base
RUN ash innvikler.sh im
WORKDIR /w
WORKDIR /state
EXPOSE 3923
ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"]
ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"]

View file

@ -1,7 +1,7 @@
FROM alpine:latest
WORKDIR /z
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
org.opencontainers.image.source="https://github.com/9001/copyparty" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="copyparty-iv" \
org.opencontainers.image.description="copyparty with Pillow, FFmpeg, libvips (image/audio/video thumbnails, audio transcoding, media tags)"
@ -14,17 +14,20 @@ RUN apk add -U !pyc \
ffmpeg \
py3-magic \
vips-jxl vips-heif vips-poppler vips-magick \
libraw py3-numpy cython \
&& apk add -t .bd \
bash wget gcc g++ make cmake patchelf \
python3-dev py3-wheel libffi-dev \
libraw-dev py3-numpy-dev \
&& rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \
&& python3 -m pip install pyvips \
&& python3 -m pip install "$(wget -O- https://api.github.com/repos/letmaik/rawpy/releases/latest | awk -F\" '$2=="tarball_url"{print$4}')" \
&& apk del py3-pip .bd
COPY i/dist/copyparty-sfx.py innvikler.sh ./
ADD base ./base
RUN ash innvikler.sh iv
WORKDIR /w
WORKDIR /state
EXPOSE 3923
ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"]
ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"]

View file

@ -1,7 +1,7 @@
FROM alpine:latest
WORKDIR /z
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
org.opencontainers.image.source="https://github.com/9001/copyparty" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="copyparty-min" \
org.opencontainers.image.description="just copyparty, no thumbnails / media tags / audio transcoding"
@ -13,6 +13,6 @@ RUN apk --no-cache add !pyc \
COPY i/dist/copyparty-sfx.py innvikler.sh ./
RUN ash innvikler.sh min
WORKDIR /w
WORKDIR /state
EXPOSE 3923
ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "--no-thumb", "-c", "/z/initcfg"]
ENTRYPOINT ["python3", "-m", "copyparty", "--no-thumb", "-c", "/z/initcfg"]

View file

@ -15,9 +15,15 @@ rm -rf /z/base
rm -rf /var/cache/apk/* /root/.cache
# initial config; common for all flavors
mkdir /cfg /w
chmod 777 /cfg /w
echo % /cfg > initcfg
mkdir /state /cfg /w
chmod 777 /state /cfg /w
cat >initcfg <<'EOF'
[global]
chdir: /w
no-crt
% /cfg
EOF
# unpack sfx and dive in
python3 copyparty-sfx.py --version

View file

@ -102,12 +102,18 @@ filt=
# arm takes forever so make it top priority
[ ${a::3} == arm ] && nice= || nice=-n20
# not sure if this is necessary or if inherit-annotations=false was enough, but won't hurt
readarray -t annot < <(awk <Dockerfile.$i '/org.opencontainers.image/{sub(/[^\.]+/,"");sub(/[" \\]+$/,"");sub(/"/,"");print"--annotation";print"org"$0}')
annot+=( --annotation "org.opencontainers.image.created=$( date -u +%Y-%m-%dT%H:%M:%SZ )" )
# --pull=never does nothing at all btw
(set -x
nice $nice podman build \
--squash \
--pull=never \
--from localhost/alpine-$a \
--inherit-annotations=false \
"${annot[@]}" \
-t copyparty-$i-$a$suf \
-f Dockerfile.$i . ||
(echo $? $i-$a >> err; printf '%096d\n' $(seq 1 42))

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 -chmod -pwhash -zm; do
echo; for a in '' -bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -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 -chmod -pwhash -zm; do
for a in '' -bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -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

66
scripts/make-rpm.sh Executable file
View file

@ -0,0 +1,66 @@
#!/bin/bash
set -e
#--localbuild to build webdeps and tar locally; otherwise just download prebuilt
#--pm change packagemanager; otherwise default to dnf
while [ ! -z "$1" ]; do
case $1 in
local-build) local_build=1 ; ;;
pm) shift;packagemanager="$1"; ;;
esac
shift
done
[ -e copyparty/__main__.py ] || cd ..
[ -e copyparty/__main__.py ] ||
{
echo "run me from within the project root folder"
echo
exit 1
}
packagemanager=${packagemanager:-dnf}
ver=$(awk '/^VERSION/{gsub(/[^0-9]/," ");printf "%d.%d.%d\n",$1,$2,$3}' copyparty/__version__.py)
releasedir="dist/temp_copyparty_$ver"
sourcepkg="copyparty-$ver.tar.gz"
#make temporary directory to build rpm in
mkdir -p $releasedir/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
trap "rm -rf $releasedir" EXIT
# make/get tarball
if [ $local_build ]; then
if [ ! -f "copyparty/web/deps/mini-fa.woff" ]; then
sudo $packagemanager update
sudo $packagemanager install podman-docker docker
make -C deps-docker
fi
if [ ! -f "dist/$sourcepkg" ]; then
./$cppdir/scripts/make-sfx.sh gz fast # pulls some build-deps + good smoketest
./$cppdir/scripts/make-tgz-release.sh "$ver"
fi
else
if [ ! -f "dist/$sourcepkg" ]; then
curl -OL https://github.com/9001/copyparty/releases/download/v$ver/$sourcepkg --output-dir dist
fi
fi
cp dist/$sourcepkg "$releasedir/SOURCES/$sourcepkg"
cp "contrib/package/rpm/copyparty.spec" "$releasedir/SPECS/"
sed -i "s/\$pkgver/$ver/g" "$releasedir/SPECS/copyparty.spec"
sed -i "s/\$pkgrel/1/g" "$releasedir/SPECS/copyparty.spec"
sudo $packagemanager update
sudo $packagemanager install \
rpmdevtools python-devel pyproject-rpm-macros \
python-wheel python-setuptools python-jinja2 \
make pigz
cd "$releasedir/"
rpmbuild --define "_topdir `pwd`" -bb SPECS/copyparty.spec
cd -
rpm="copyparty-$ver-1.noarch.rpm"
mv "$releasedir/RPMS/noarch/$rpm" dist/$rpm

View file

@ -41,7 +41,7 @@ help() { exec cat <<'EOF'
# `no-cm` saves ~89k by removing easymde/codemirror
# (the fancy markdown editor)
#
# `no-hl` saves ~41k by removing syntax hilighting in the text viewer
# `no-hl` saves ~41k by removing syntax highlighting in the text viewer
#
# `no-fnt` saves ~9k by removing the source-code-pro font
# (browsers will try to use 'Consolas' instead)
@ -217,6 +217,8 @@ necho() {
tar -zxf $f
mv pyftpdlib-*/pyftpdlib .
rm -rf pyftpdlib-* pyftpdlib/test
patch -s -p1 <../scripts/patches/pyftpdlib-win313.patch
patch -s -p1 <../scripts/patches/pyftpdlib-fe80.patch
for f in pyftpdlib/_async{hat,ore}.py; do
[ -e "$f" ] || continue;
iawk 'NR<4||NR>27||!/^#/;NR==4{print"# license: https://opensource.org/licenses/ISC\n"}' $f

View file

@ -0,0 +1,37 @@
accept connections from IPv6 link-local addresses
diff -NarU1 a/pyftpdlib/handlers.py b/pyftpdlib2/handlers.py
--- a/pyftpdlib/handlers.py 2024-06-23 14:03:38
+++ b/pyftpdlib/handlers.py 2025-08-22 21:59:40
@@ -451,3 +451,4 @@
- local_ip = self.cmd_channel.socket.getsockname()[0]
+ sockname = list(self.cmd_channel.socket.getsockname())
+ local_ip = sockname[0]
if local_ip in self.cmd_channel.masquerade_address_map:
@@ -459,3 +460,5 @@
- if self.cmd_channel.server.socket.family != socket.AF_INET:
+ if local_ip.startswith('fe') and local_ip[2:3] in "89ab":
+ af = socket.AF_INET6 # link-local
+ elif self.cmd_channel.server.socket.family != socket.AF_INET:
# dual stack IPv4/IPv6 support
@@ -472,3 +475,4 @@
# free unprivileged random port.
- self.bind((local_ip, 0))
+ sockname[1] = 0
+ self.bind(tuple(sockname))
else:
@@ -478,4 +482,5 @@
self.set_reuse_addr()
+ sockname[1] = port
try:
- self.bind((local_ip, port))
+ self.bind(tuple(sockname))
except PermissionError:
@@ -495,3 +500,4 @@
else:
- self.bind((local_ip, 0))
+ sockname[1] = 0
+ self.bind(tuple(sockname))
self.cmd_channel.log(

Some files were not shown because too many files have changed in this diff Show more