Compare commits

...

45 commits

Author SHA1 Message Date
ed a5d859d2b1 smb: add ipv6 support; closes #1417 2026-04-11 00:17:05 +00:00
AppleTheGolden 0b16e875da
nixos: add override example (#1406) 2026-04-10 20:02:50 +00:00
chilledfrogs f5613187b4
improve *BSD compat (#1425)
reuse some macOS stuff since lsblk and /proc doesn't apply to *BSD
2026-04-10 20:02:01 +00:00
ed 003c68d027 readme: shadowing 2026-04-10 19:52:43 +00:00
ed 961a273764 autogrid global-option 2026-04-10 19:37:00 +00:00
ed 55d37364a6 indentation 2026-04-10 19:36:06 +00:00
ed 6b3419b38f mtl new strings 2026-04-10 19:11:09 +00:00
ed 660ed7a92e tl new strings 2026-04-10 19:09:59 +00:00
exci 822fa71800
add autogrid (#1407)
Co-authored-by: ed <s@ocv.me>
2026-04-10 18:33:28 +00:00
AppleTheGolden ed516ddc20
make .txt the default extension for text/plain (#1428)
`MIMES.items()` iterates in insertion order, so the last-inserted entry
had priority, meaning that `text/plain` got `ssa` as default extension.
2026-04-09 23:20:53 +00:00
ed ede692925e reset language too 2026-04-04 20:28:16 +00:00
stackxp ec3e0e7e1d
add --glang to use browser language (#1410)
Signed-off-by: ed <s@ocv.me>
Co-authored-by: ed <s@ocv.me>
2026-04-04 20:24:11 +00:00
ed d1517d0c65 catch the server-hdd phasing out of existence (and equally unexpected stuff) 2026-04-04 18:19:49 +00:00
mid-kid fb5384f412
readme: add gentoo packaging (#1387) 2026-04-02 20:36:52 +00:00
ed 971f8ef944 devnotes: vendored deps 2026-04-02 20:28:16 +00:00
exci 198f631ac8
fix playlist error on re-sorted filelists (#1403)
m3u files would get added (not good)
2026-03-31 19:02:11 +00:00
ed 5aaa4ff15b docs: volflags examples 2026-03-30 08:43:05 +00:00
Lomain 26e663d111
add fail2ban handler (#1352) 2026-03-25 04:08:35 +00:00
ed dcbdd9e665 versus: update pydio; closes #1385 2026-03-24 21:57:57 +00:00
ed 6a9e6da864 update pkgs to 1.20.13 2026-03-23 03:33:52 +00:00
ed aa23777385 v1.20.13 2026-03-23 03:28:27 +00:00
ed abdbd69a68 libvips jxl tweaks 2026-03-23 02:02:15 +00:00
ed edc201752b it is easter,, my dudes 2026-03-23 02:01:13 +00:00
ed 7d6b037dcc thumbnail pregen; closes #1381 2026-03-23 01:57:32 +00:00
ed 5f3b76c8f8 nohtml / noscript as global options 2026-03-22 17:11:45 +00:00
ed 66f9c95097 shares: dotfiles can be granted 2026-03-22 16:56:07 +00:00
ed 134e378e3a docs: dothidden, unlist, descript.ion 2026-03-22 16:22:22 +00:00
ed b4faff8e4a save some cycles 2026-03-22 15:44:36 +00:00
ed df78fc5732 throw ValueError for clarity 2026-03-22 15:43:21 +00:00
ed 23545116af sftp: .hidden 2026-03-22 15:40:04 +00:00
NecRaul beb634dc54
support .hidden file for dotfiles exclusion (#1351)
similar to many desktop file managers (Dolphin, Nautilus, Thunar), the
filenames in `.hidden` are cosmetically hidden in directory listings

the files are still directly accessible, and will be included in
download-as-zip/tar, and still appear in many other places in the UI

---------

Co-authored-by: ed <s@ocv.me>
2026-03-22 15:34:14 +00:00
ed 1f8ebd57dc macos/HFS+ cannot into floats 2026-03-21 23:49:09 +01:00
ed 311eb698a0 nginx: client_max_body_size is global 2026-03-21 21:18:32 +00:00
ed bb92ce5fc8 optimize connection:close 2026-03-21 19:47:44 +00:00
ed 1afe48b85d thumbs: fix th=x on non-jxl-capable servers;
if FFmpeg was compiled without jxl support, then every
request for a jxl thumbnail would result in a placeholder svg

detect this and switch to webp as fallback; the initial request
will still fail but successive ones are fine

closes #1333, closes #1372
2026-03-21 19:28:06 +00:00
ed bbe58105e7 shares: fix qr for huge links 2026-03-21 19:04:48 +00:00
ed ac60a1da46 shares: fix mkdir too 2026-03-21 19:04:14 +00:00
ed 2cebda3297 sfx.py: add MIT license; closes #1379 2026-03-20 00:32:10 +00:00
TWhiteShadow 4688410f8f
readme: mention --shr-who (#1367)
Signed-off-by: TWhiteShadow <123359049+TWhiteShadow@users.noreply.github.com>
2026-03-19 01:15:51 +00:00
Evgeny e71e1900b4
versus: 2-letter headers (#1360) 2026-03-19 01:13:06 +00:00
ed 8c6d8a3c22 logfile colors 2026-03-19 00:57:09 +00:00
ed dcb0ffa72c cosmetic 2026-03-19 00:47:15 +00:00
ed eb028c92f8 fix js mimetype with noscript webroot 2026-03-17 23:51:25 +00:00
ed 6eb4f0ad9c ver-chk: fix admin-filter (closes #1363);
also drop cache if json invalid
2026-03-12 00:33:26 +01:00
ed 8a9066c35c update pkgs to 1.20.12 2026-03-11 00:46:49 +00:00
63 changed files with 983 additions and 220 deletions

113
README.md
View file

@ -65,6 +65,7 @@ built in Norway 🇳🇴 with contributions from [not-norway](https://github.com
* [searching](#searching) - search by size, date, path/name, mp3-tags, ... * [searching](#searching) - search by size, date, path/name, mp3-tags, ...
* [server config](#server-config) - using arguments or config files, or a mix of both * [server config](#server-config) - using arguments or config files, or a mix of both
* [version-checker](#version-checker) - sleep better at night * [version-checker](#version-checker) - sleep better at night
* [logging](#logging) - serverlog is sent to stdout by default
* [zeroconf](#zeroconf) - announce enabled services on the LAN ([pic](https://user-images.githubusercontent.com/241032/215344737-0eae8d98-9496-4256-9aa8-cd2f6971810d.png)) * [zeroconf](#zeroconf) - announce enabled services on the LAN ([pic](https://user-images.githubusercontent.com/241032/215344737-0eae8d98-9496-4256-9aa8-cd2f6971810d.png))
* [mdns](#mdns) - LAN domain-name and feature announcer * [mdns](#mdns) - LAN domain-name and feature announcer
* [ssdp](#ssdp) - windows-explorer announcer * [ssdp](#ssdp) - windows-explorer announcer
@ -86,6 +87,8 @@ built in Norway 🇳🇴 with contributions from [not-norway](https://github.com
* [compress uploads](#compress-uploads) - files can be autocompressed on upload * [compress uploads](#compress-uploads) - files can be autocompressed on upload
* [chmod and chown](#chmod-and-chown) - per-volume filesystem-permissions and ownership * [chmod and chown](#chmod-and-chown) - per-volume filesystem-permissions and ownership
* [other flags](#other-flags) * [other flags](#other-flags)
* [descript.ion](#description) - add a description to each file in a folder
* [dothidden](#dothidden) - cosmetically hide specific files in a folder
* [database location](#database-location) - in-volume (`.hist/up2k.db`, default) or somewhere else * [database location](#database-location) - in-volume (`.hist/up2k.db`, default) or somewhere else
* [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload * [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload
* [metadata from xattrs](#metadata-from-xattrs) - unix extended file attributes * [metadata from xattrs](#metadata-from-xattrs) - unix extended file attributes
@ -117,6 +120,7 @@ built in Norway 🇳🇴 with contributions from [not-norway](https://github.com
* [packages](#packages) - the party might be closer than you think * [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/)) * [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 * [fedora package](#fedora-package) - does not exist yet
* [gentoo ::guru package](#gentoo-guru-package) - `emerge www-servers/copyparty::guru` (in [::guru](https://wiki.gentoo.org/wiki/Project:GURU))
* [homebrew formulae](#homebrew-formulae) - `brew install copyparty ffmpeg` * [homebrew formulae](#homebrew-formulae) - `brew install copyparty ffmpeg`
* [nix package](#nix-package) - `nix profile install github:9001/copyparty` * [nix package](#nix-package) - `nix profile install github:9001/copyparty`
* [nixos module](#nixos-module) * [nixos module](#nixos-module)
@ -604,10 +608,12 @@ and if you want to use config files instead of commandline args (good!) then her
hiding specific subfolders by mounting another volume on top of them hiding specific subfolders by mounting another volume on top of them
for example `-v /mnt::r -v /var/empty:web/certs:r` mounts the server folder `/mnt` as the webroot, but another volume is mounted at `/web/certs` -- so visitors can only see the contents of `/mnt` and `/mnt/web` (at URLs `/` and `/web`), but not `/mnt/web/certs` because URL `/web/certs` is mapped to `/var/empty` for example `-v /mnt::r -v /var/empty:web/certs:` (note: no permissions) mounts the server folder `/mnt` as the webroot, but another volume is mounted at `/web/certs` -- so visitors can only see the contents of `/mnt` and `/mnt/web` (at URLs `/` and `/web`), but not `/mnt/web/certs` because URL `/web/certs` is mapped to `/var/empty`
the example config file right above this section may explain this better; the first volume `/` is mapped to `/srv` which means http://127.0.0.1:3923/music would try to read `/srv/music` on the server filesystem, but since there's another volume at `/music` mapped to `/mnt/music` then it'll go to `/mnt/music` instead the example config file right above this section may explain this better; the first volume `/` is mapped to `/srv` which means http://127.0.0.1:3923/music would try to read `/srv/music` on the server filesystem, but since there's another volume at `/music` mapped to `/mnt/music` then it'll go to `/mnt/music` instead
so, to shadow a file/folder, define a volume but leave out the `accs:` section
> this also works for single files, because files can also be volumes > this also works for single files, because files can also be volumes
@ -619,6 +625,8 @@ anyone can access these if they know the name, but they normally don't appear in
a client can request to see dotfiles in directory listings if global option `-ed` is specified, or the volume has volflag `dots`, or the user has permission `.` a client can request to see dotfiles in directory listings if global option `-ed` is specified, or the volume has volflag `dots`, or the user has permission `.`
> for [shares](#shares), the `dots` volflag is ignored
dotfiles do not appear in search results unless one of the above is true, **and** the global option / volflag `dotsrch` is set dotfiles do not appear in search results unless one of the above is true, **and** the global option / volflag `dotsrch` is set
> even if user has permission to see dotfiles, they are default-hidden unless `--see-dots` is set, and/or user has enabled the `dotfiles` option in the settings tab > even if user has permission to see dotfiles, they are default-hidden unless `--see-dots` is set, and/or user has enabled the `dotfiles` option in the settings tab
@ -766,6 +774,7 @@ to show `/icons/exe.png` and `/icons/elf.gif` as the thumbnail for all `.exe` an
note: note:
* heif/heifs/heic/heics images usually require the `libvips` [optional dependency](#optional-dependencies) but this is not possible with the docker-images due to [legal reasons](docs/bad-codecs.md) * heif/heifs/heic/heics images usually require the `libvips` [optional dependency](#optional-dependencies) but this is not possible with the docker-images due to [legal reasons](docs/bad-codecs.md)
* if you do not want thumbnails to be generated on-the-fly, and instead wish to generate all of them on server startup, then see [thumbnail pregen](#thumbnail-pregen)
config file example: config file example:
@ -824,6 +833,7 @@ cool trick: download a folder by appending url-params `?tar&opus` or `?tar&mp3`
* and url-param `&nodot` skips dotfiles/dotfolders; they are included by default if your account has permission to see them * and url-param `&nodot` skips dotfiles/dotfolders; they are included by default if your account has permission to see them
* and url-params `&j` / `&w` produce jpeg/webm thumbnails/spectrograms instead of the original audio/video/images (`&p` for audio waveforms) * and url-params `&j` / `&w` produce jpeg/webm thumbnails/spectrograms instead of the original audio/video/images (`&p` for audio waveforms)
* can also be used to pregenerate thumbnails; combine with `--th-maxage=9999999` or `--th-clean=0` * can also be used to pregenerate thumbnails; combine with `--th-maxage=9999999` or `--th-clean=0`
* but now there is also a real [thumbnail pregen](#thumbnail-pregen) so just use that
## uploading ## uploading
@ -1005,6 +1015,8 @@ specify `--shr /foobar` to enable this feature; a toplevel virtual folder named
users can delete their own shares in the controlpanel, and a list of privileged users (`--shr-adm`) are allowed to see and/or delet any share on the server users can delete their own shares in the controlpanel, and a list of privileged users (`--shr-adm`) are allowed to see and/or delet any share on the server
the volflag `--shr-who` lets you control who can create a share from that volume, either `no` (nobody), `a` (people with admin permission), or `auth` (people who are logged in)
after a share has expired, it remains visible in the controlpanel for `--shr-rt` minutes (default is 1 day), and the owner can revive it by extending the expiration time there after a share has expired, it remains visible in the controlpanel for `--shr-rt` minutes (default is 1 day), and the owner can revive it by extending the expiration time there
**security note:** using this feature does not mean that you can skip the [accounts and volumes](#accounts-and-volumes) section -- you still need to restrict access to volumes that you do not intend to share with unauthenticated users! it is not sufficient to use rules in the reverseproxy to restrict access to just the `/share` folder. **security note:** using this feature does not mean that you can skip the [accounts and volumes](#accounts-and-volumes) section -- you still need to restrict access to volumes that you do not intend to share with unauthenticated users! it is not sufficient to use rules in the reverseproxy to restrict access to just the `/share` folder.
@ -1339,6 +1351,33 @@ config file example:
``` ```
## logging
serverlog is sent to stdout by default (but logging to a file is also possible)
"stdout" usually means either the terminal, or journalctl, or whatever is collecting logs from your docker containers, so that depends on your setup
* [-q](https://copyparty.eu/cli/#g-q) disables logging to stdout, and may improve performance a little bit
* combine it with `-lo logfolder/cpp-%Y-%m-%d.txt` to log to a file instead
* the `%Y-%m-%d` makes it create a new logfile every day, with the date as filename
* `-lo whatever.txt` can be used without `-q` to log to both at the same time
* by default, the logfile will have colors if the terminal does (usually the case)
* use the [textfile-viewer](https://github.com/user-attachments/assets/8a828947-2fae-4df9-bd2a-3de46f42d478) or `less -R` in a terminal to see colors correctly
* if you want [no colors](https://youtu.be/biW5UVGkPMA?t=148):
* `--flo 2` disables colors for just the logfile
* `--no-ansi` disables colors for both the terminal and logfile
config file example:
```yaml
[global]
log-date: %Y-%m-%d # show dates on stdout too
lo: /var/log/cpp/%Y-%m-%d.txt # logfile path
flo: 2 # just text (no colors) in logfile
q # disable stdout; use logfile only
```
## zeroconf ## zeroconf
announce enabled services on the LAN ([pic](https://user-images.githubusercontent.com/241032/215344737-0eae8d98-9496-4256-9aa8-cd2f6971810d.png)) -- `-z` enables both [mdns](#mdns) and [ssdp](#ssdp) announce enabled services on the LAN ([pic](https://user-images.githubusercontent.com/241032/215344737-0eae8d98-9496-4256-9aa8-cd2f6971810d.png)) -- `-z` enables both [mdns](#mdns) and [ssdp](#ssdp)
@ -1847,6 +1886,55 @@ notes:
* adding `?cache` to a link will override this with "fully cache this for 69 seconds"; `?cache=321` is 321 seconds, and `?cache=i` is 7 days * adding `?cache` to a link will override this with "fully cache this for 69 seconds"; `?cache=321` is 321 seconds, and `?cache=i` is 7 days
## descript.ion
add a description to each file in a folder by adding them to a textfile named `descript.ion`
see https://copyparty.eu/beta/ for an example -- here's a basic `descript.ion` file:
```
bookmark.mp3 Taishi feat. Rita - Bookmark Memories
slowstep.mp3 Taishi feat. 向日葵 - Slow Step -F.L.C.A-
prsnlzr.mp3 Taishi feat. みとせのりこ - Personalizer
cosmos.mp3 Taishi feat. Rita - Into the cosmos
```
## dothidden
cosmetically hide specific files in a folder by adding them to a textfile named `.hidden`
this option is default-disabled; enable the volflag and/or global-option `dothidden`
this is **cosmetic only!** the files are still easily accessible in many ways, for example with download-as-zip/tar, so **do not** rely on this for security.
> also see the [--unlist](https://copyparty.eu/cli/#g-unlist) option which is somewhat similar -- `unlist` applies to the whole volume instead of just one folder; however, while dothidden also affects sftp and ftp, the `unlist` option is http/https-only
## thumbnail pregen
if you want to pre-generate everything on startup (usually a bad idea);
by default, thumbnails are created on-the-fly when a client needs it, and then cached on the server for [--th-maxage](https://copyparty.eu/cli/#g-th-maxage) seconds (default is one week), so most thumbnails only need to be created once, and are then eventually deleted from the cache to preserver disk space
but if you need every thumbnail instantly available when a folder is viewed, then first increase the thumbnail expiration time to something really big, and then set global-option `th-pregen` and volflag `th_pregen` to a comma-separated list of thumbnail formats to automatically generate on server startup;
the full list of all possible formats is: `j,jf,jf3,j3,w,wf,wf3,w3,x,xf,xf3,x3,opus,mp3,flac,wav` and I'll explain what those mean soon
* `j` = jpeg cropped, `jf` = jpeg uncropped, `jf3` = jpeg uncropped triplesize, `j3` jpeg cropped triplesize
* `w` = webm cropped, `wf` = webm uncropped, ..., `x` = jxl cropped, `xf` = jxl uncropped, ...
* and yes, audio-transcodes are technically thumbnails according to copyparty -- don't think too much about it ( ゚ ヮ゚)
* unlike thumbnails, the expiry time for audio-transcodes is configured with [--ac-maxage](https://copyparty.eu/cli/#g-ac-maxage)
anyways, obviously you **do not** want to pregenerate flac/wav because they're HUGE, and everything else also gets pretty big because it all adds up;
* each regular thumbnail ( j, jf, w, wf, x, xf ) takes about 16 KiB of disk space
* each triplesize thumb ( j3, jf3, w3, wf3, x3, xf3 ) takes about 96 KiB
* each opus / mp3 audiotranscode takes... idk, 6 MiB? depends on song length
so a thousand pictures converted to every possible regular-size image format (`j,jf,w,wf,x,xf`) takes **96 MiB,** and every possible 3x-size (`jf3,j3,wf3,w3,xf3,x3`) takes **562 MiB,** alternatively **658 MiB** in total for all, so that's why the default is to *not* pregenerate on startup, but instead do on-demand with a cache
## database location ## database location
in-volume (`.hist/up2k.db`, default) or somewhere else in-volume (`.hist/up2k.db`, default) or somewhere else
@ -2500,6 +2588,23 @@ after installing, start either the system service or the user service and naviga
does not exist yet; there are rumours that it is being packaged! keep an eye on this space... does not exist yet; there are rumours that it is being packaged! keep an eye on this space...
## gentoo ::guru package
`emerge www-servers/copyparty::guru` (in [::guru](https://wiki.gentoo.org/wiki/Project:GURU))
but first enable the `::guru` repo;
```bash
emerge -an app-eselect/eselect-repository
eselect repository enable guru
emerge --sync guru
```
to start the service as a user:
* OpenRC: `rc-service -U copyparty start && rc-update -U add copyparty default`
* systemd: [todo]
## homebrew formulae ## homebrew formulae
`brew install copyparty ffmpeg` -- https://formulae.brew.sh/formula/copyparty `brew install copyparty ffmpeg` -- https://formulae.brew.sh/formula/copyparty
@ -2644,6 +2749,12 @@ services.copyparty = {
}; };
# you may increase the open file limit for the process # you may increase the open file limit for the process
openFilesLimit = 8192; openFilesLimit = 8192;
# override the package used by the module to add dependencies, e.g. for hooks
package = pkgs.copyparty.override {
# provides exiftool for bin/hooks/image-noexif.py
extraPackages = [ pkgs.exiftool ];
};
}; };
``` ```

36
bin/handlers/404-to-fail2ban.py Executable file
View file

@ -0,0 +1,36 @@
# /!\ Warning: be careful, as webdav clients often generate a large number of 404 requets.
# In your `jail.local`, add:
# [copyparty]
# enabled = true
# logtimezone = UTC
# logpath = /path/to/log/file # or keep the default value if you're using systemd
# Create the `copyparty.conf` file in `filter.d` with the following:
# [Definition]
# failregex = ^ <ADDR>$
# ignoreregex =
# datepattern = ^fail2ban: %%Y-%%m-%%dT%%H:%%M:%%S
# First check `--dont-ban`, and if it doesn't match, log the line to be intercepted by fail2ban.
from datetime import datetime, UTC
def main(cli, vn="", rem=""):
now = datetime.now(UTC).isoformat()[:19]
msg = "\nfail2ban: %s %s"
if not vn and not rem:
# got called by --xban
cli["log"](msg % (now, cli["ip"]), 3)
return {"rc": 0}
cond = cli.args.dont_ban
if (
cond == "any"
or (cond == "auth" and cli.uname != "*")
or (cond == "aa" and cli.avol)
or (cond == "av" and cli.can_admin)
or (cond == "rw" and cli.can_read and cli.can_write)
):
return ""
cli.log(msg % (now, cli.ip), 3)
return ""

View file

@ -22,10 +22,11 @@ each plugin must define a `main()` which takes 3 arguments;
* [redirect.py](redirect.py) sends an HTTP 301 or 302, redirecting the client to another page/file * [redirect.py](redirect.py) sends an HTTP 301 or 302, redirecting the client to another page/file
* [randpic.py](randpic.py) redirects `/foo/bar/randpic.jpg` to a random pic in `/foo/bar/` * [randpic.py](randpic.py) redirects `/foo/bar/randpic.jpg` to a random pic in `/foo/bar/`
* [sorry.py](answer.py) replies with a custom message instead of the usual 404 * [sorry.py](sorry.py) replies with a custom message instead of the usual 404
* [nooo.py](nooo.py) replies with an endless noooooooooooooo * [nooo.py](nooo.py) replies with an endless noooooooooooooo
* [never404.py](never404.py) 100% guarantee that 404 will never be a thing again as it automatically creates dummy files whenever necessary * [never404.py](never404.py) 100% guarantee that 404 will never be a thing again as it automatically creates dummy files whenever necessary
* [caching-proxy.py](caching-proxy.py) transforms copyparty into a squid/varnish knockoff * [caching-proxy.py](caching-proxy.py) transforms copyparty into a squid/varnish knockoff
* [404-to-fail2ban.py](404-to-fail2ban.py) creates 404 logs for fail2ban
## on403 ## on403

View file

@ -75,9 +75,8 @@ server {
# recommendation: replace cpp_tcp with cpp_uds below # recommendation: replace cpp_tcp with cpp_uds below
proxy_pass http://cpp_tcp; proxy_pass http://cpp_tcp;
proxy_redirect off; proxy_redirect off;
# disable buffering (next 4 lines) # disable buffering (next 3 lines)
proxy_http_version 1.1; proxy_http_version 1.1;
client_max_body_size 0;
proxy_buffering off; proxy_buffering off;
proxy_request_buffering off; proxy_request_buffering off;
# improve download speed from 600 to 1500 MiB/s # improve download speed from 600 to 1500 MiB/s

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. # NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead.
pkgname=copyparty pkgname=copyparty
pkgver="1.20.11" pkgver="1.20.13"
pkgrel=1 pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++" pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any") arch=("any")
@ -24,7 +24,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") source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("etc/${pkgname}/copyparty.conf" ) backup=("etc/${pkgname}/copyparty.conf" )
sha256sums=("4ad301adf2cd1ff0d65573e4e93af97fa85c1b9eed1207625e99afc5a5ca6213") sha256sums=("b2af9250f7ef97a5df26df412ee082c6d2be0f0cd31d579b4fbb6aa2f3e5c271")
build() { build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"

View file

@ -2,7 +2,7 @@
pkgname=copyparty pkgname=copyparty
pkgver=1.20.11 pkgver=1.20.13
pkgrel=1 pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++" pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any") arch=("any")
@ -21,7 +21,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
) )
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("/etc/${pkgname}.d/init" ) backup=("/etc/${pkgname}.d/init" )
sha256sums=("4ad301adf2cd1ff0d65573e4e93af97fa85c1b9eed1207625e99afc5a5ca6213") sha256sums=("b2af9250f7ef97a5df26df412ee082c6d2be0f0cd31d579b4fbb6aa2f3e5c271")
build() { build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"

View file

@ -1,5 +1,5 @@
{ {
"url": "https://github.com/9001/copyparty/releases/download/v1.20.11/copyparty-1.20.11.tar.gz", "url": "https://github.com/9001/copyparty/releases/download/v1.20.13/copyparty-1.20.13.tar.gz",
"version": "1.20.11", "version": "1.20.13",
"hash": "sha256-StMBrfLNH/DWVXPk6Tr5f6hcG57tEgdiXpmvxaXKYhM=" "hash": "sha256-sq+SUPfvl6XfJt9BLuCCxtK+DwzTHVebT7tqovPlwnE="
} }

View file

@ -38,5 +38,7 @@
accs: accs:
rw: * # everyone gets read-write access, but rw: * # everyone gets read-write access, but
rwmda: ed # the user "ed" gets read-write-move-delete-admin rwmda: ed # the user "ed" gets read-write-move-delete-admin
flags:
e2ds # enable filesystem-scanning for this volume only
# uid: 1000 # If you're running as root, you can change the owner of this volume here # uid: 1000 # If you're running as root, you can change the owner of this volume here
# gid: 1000 # If you're running as root, you can change the group of this volume here # gid: 1000 # If you're running as root, you can change the group of this volume here

View file

@ -49,3 +49,5 @@
accs: accs:
rw: * # everyone gets read-write access, but rw: * # everyone gets read-write access, but
rwmda: ed # the user "ed" gets read-write-move-delete-admin rwmda: ed # the user "ed" gets read-write-move-delete-admin
flags:
e2ds # enable filesystem-scanning for this volume only

View file

@ -44,6 +44,14 @@ ANYWIN = WINDOWS or sys.platform in ["msys", "cygwin"]
MACOS = platform.system() == "Darwin" MACOS = platform.system() == "Darwin"
FREEBSD = platform.system() == "FreeBSD"
OPENBSD = platform.system() == "OpenBSD"
ANYBSD = FREEBSD or OPENBSD
UNIX = MACOS or ANYBSD
GRAAL = platform.python_implementation() == "GraalVM" GRAAL = platform.python_implementation() == "GraalVM"
EXE = bool(getattr(sys, "frozen", False)) EXE = bool(getattr(sys, "frozen", False))

View file

@ -1520,6 +1520,7 @@ def add_smb(ap):
ap2.add_argument("--smb-nwa-1", action="store_true", help="truncate directory listings to 64kB (~400 files); avoids impacket-0.11 bug, fixes impacket-0.12 performance") ap2.add_argument("--smb-nwa-1", action="store_true", help="truncate directory listings to 64kB (~400 files); avoids impacket-0.11 bug, fixes impacket-0.12 performance")
ap2.add_argument("--smb-nwa-2", action="store_true", help="disable impacket workaround for filecopy globs") ap2.add_argument("--smb-nwa-2", action="store_true", help="disable impacket workaround for filecopy globs")
ap2.add_argument("--smba", action="store_true", help="small performance boost: disable per-account permissions, enables account coalescing instead (if one user has write/delete-access, then everyone does)") ap2.add_argument("--smba", action="store_true", help="small performance boost: disable per-account permissions, enables account coalescing instead (if one user has write/delete-access, then everyone does)")
ap2.add_argument("--smb6", action="store_true", help="enable IPv6")
ap2.add_argument("--smbv", action="store_true", help="verbose") ap2.add_argument("--smbv", action="store_true", help="verbose")
ap2.add_argument("--smbvv", action="store_true", help="verboser") ap2.add_argument("--smbvv", action="store_true", help="verboser")
ap2.add_argument("--smbvvv", action="store_true", help="verbosest") ap2.add_argument("--smbvvv", action="store_true", help="verbosest")
@ -1606,7 +1607,7 @@ def add_optouts(ap):
def add_safety(ap): def add_safety(ap):
ap2 = ap.add_argument_group("safety options") 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("-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 requires login, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --reflink --dav-auth --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 --no-html --no-readme --no-logues --unpost=0 --no-del --no-mv --reflink --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("-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("--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)") 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)")
@ -1617,6 +1618,8 @@ def add_safety(ap):
ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to turn something into a dotfile") ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to turn something into a dotfile")
ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings") ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings")
ap2.add_argument("--no-readme", action="store_true", help="disable rendering readme/preadme.md into directory listings") ap2.add_argument("--no-readme", action="store_true", help="disable rendering readme/preadme.md into directory listings")
ap2.add_argument("--no-script", action="store_true", help="disables javascript in html files; helps prevent XSS but kills interactive websites (volflag=noscript)")
ap2.add_argument("--no-html", action="store_true", help="show html-files as plain text; helps prevent XSS but kills websites/blogs, also enables --no-script (volflag=nohtml)")
ap2.add_argument("--vague-403", action="store_true", help="send 404 instead of 403 (security through ambiguity, very enterprise). \033[1;31mWARNING:\033[0m Not compatible with WebDAV") ap2.add_argument("--vague-403", action="store_true", help="send 404 instead of 403 (security through ambiguity, very enterprise). \033[1;31mWARNING:\033[0m Not compatible with WebDAV")
ap2.add_argument("--force-js", action="store_true", help="don't send folder listings as HTML, force clients to use the embedded json instead -- slight protection against misbehaving search engines which ignore \033[33m--no-robots\033[0m") ap2.add_argument("--force-js", action="store_true", help="don't send folder listings as HTML, force clients to use the embedded json instead -- slight protection against misbehaving search engines which ignore \033[33m--no-robots\033[0m")
ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)") ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)")
@ -1658,7 +1661,7 @@ def add_shutdown(ap):
ap2 = ap.add_argument_group("shutdown options") ap2 = ap.add_argument_group("shutdown options")
ap2.add_argument("--ign-ebind", action="store_true", help="continue running even if it's impossible to listen on some of the requested endpoints") ap2.add_argument("--ign-ebind", action="store_true", help="continue running even if it's impossible to listen on some of the requested endpoints")
ap2.add_argument("--ign-ebind-all", action="store_true", help="continue running even if it's impossible to receive connections at all") ap2.add_argument("--ign-ebind-all", action="store_true", help="continue running even if it's impossible to receive connections at all")
ap2.add_argument("--exit", metavar="WHEN", type=u, default="", help="shutdown after \033[33mWHEN\033[0m has finished; [\033[32mcfg\033[0m] config parsing, [\033[32midx\033[0m] volscan + multimedia indexing") ap2.add_argument("--exit", metavar="WHEN", type=u, default="", help="shutdown after \033[33mWHEN\033[0m has finished; [\033[32mcfg\033[0m] config parsing, [\033[32midx\033[0m] volscan + multimedia indexing, [\033[32mthgen\033[0m] thumbnail-pregen")
def add_logging(ap): def add_logging(ap):
@ -1721,9 +1724,12 @@ def add_thumbnail(ap):
ap2.add_argument("--th-no-jxl", action="store_true", help="disable jpeg-xl output") ap2.add_argument("--th-no-jxl", action="store_true", help="disable jpeg-xl output")
ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg output for video thumbs (avoids issues on some FFmpeg builds)") ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg output for video thumbs (avoids issues on some FFmpeg builds)")
ap2.add_argument("--th-ff-swr", action="store_true", help="use swresample instead of soxr for audio thumbs (faster, lower accuracy, avoids issues on some FFmpeg builds)") ap2.add_argument("--th-ff-swr", action="store_true", help="use swresample instead of soxr for audio thumbs (faster, lower accuracy, avoids issues on some FFmpeg builds)")
ap2.add_argument("--th-vips-jxl", metavar="N", type=int, default=1, help="when to allow generating jxl thumbnails with libvips; 0=never, 1=musl-mallocng, 2=always")
ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown -- avoids doing keepalive pokes (updating the mtime) on thumbnail folders more often than \033[33mSEC\033[0m seconds") ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown -- avoids doing keepalive pokes (updating the mtime) on thumbnail folders more often than \033[33mSEC\033[0m seconds")
ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled") 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-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-pregen", metavar="F,F", type=u, default="", help="pregenerate thumbnails on startup; \033[33mF,F\033[0m is comma-separated list of formats; example: [\033[32mj,jf,w,w3,wf,wf3,x,xf\033[0m] NOTE: remember to set \033[33m--th-maxage 123456789\033[0m (volflag=th_pregen)")
ap2.add_argument("--th-pre-rl", metavar="SEC", type=int, default=30, help="while pregen is running, ratelimit the thumbnailer logger to one message every \033[33mSEC\033[0m seconds (only works with \033[33m-j1\033[0m); set 0 to disable ratelimit")
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-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") 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://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
@ -1863,8 +1869,10 @@ def add_ui(ap, retry: int):
ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)") ap2.add_argument("--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("--localtime", action="store_true", help="default to local timezone instead of UTC")
ap2.add_argument("--ui-filesz", metavar="FMT", type=u, default="1", help="default filesize format; one of these: 0, 1, 2, 2c, 3, 3c, 4, 4c, 5, 5c, fuzzy (see UI)") ap2.add_argument("--ui-filesz", metavar="FMT", type=u, default="1", help="default filesize format; one of these: 0, 1, 2, 2c, 3, 3c, 4, 4c, 5, 5c, fuzzy (see UI)")
ap2.add_argument("--gauto", metavar="PERCENT", type=int, default=0, help="switch to gridview if more than \033[33mPERCENT\033[0m of files are pics/vids; 0=disabled")
ap2.add_argument("--rcm", metavar="TXT", default="yy", help="rightclick-menu; two yes/no options: 1st y/n is enable-custom-menu, 2nd y/n is enable-double") ap2.add_argument("--rcm", metavar="TXT", default="yy", help="rightclick-menu; two yes/no options: 1st y/n is enable-custom-menu, 2nd y/n is enable-double")
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language, for example \033[32meng\033[0m / \033[32mnor\033[0m / ...") ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language, for example \033[32meng\033[0m / \033[32mnor\033[0m / ...")
ap2.add_argument("--glang", action="store_true", help="guess the browser's default language, otherwise fall back to \033[33m--lang\033[0m")
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..%d)" % (THEMES - 1,)) ap2.add_argument("--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("--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("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent")
@ -1875,6 +1883,7 @@ def add_ui(ap, retry: int):
ap2.add_argument("--qdel", metavar="LVL", type=int, default=2, help="number of confirmations to show when deleting files (2/1/0)") ap2.add_argument("--qdel", metavar="LVL", type=int, default=2, help="number of confirmations to show when deleting files (2/1/0)")
ap2.add_argument("--dlni", action="store_true", help="force download (don't show inline) when files are clicked (volflag:dlni)") ap2.add_argument("--dlni", action="store_true", help="force download (don't show inline) when files are clicked (volflag:dlni)")
ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files/folders matching \033[33mREGEX\033[0m in file list. WARNING: Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)") ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files/folders matching \033[33mREGEX\033[0m in file list. WARNING: Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)")
ap2.add_argument("--dothidden", action="store_true", help="hide specific files in a folder by listing them in a file named .hidden -- WARNING: Mostly cosmetic! Download-as-zip/tar will still download them. Do not rely on this for security (volflag=dothidden)")
ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable") ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable")
ap2.add_argument("--ufavico", metavar="TXT", type=u, default="", help="URL to .ico/png/gif/svg file; \033[33m--favico\033[0m takes precedence unless disabled (volflag=ufavico)") ap2.add_argument("--ufavico", metavar="TXT", type=u, default="", help="URL to .ico/png/gif/svg file; \033[33m--favico\033[0m takes precedence unless disabled (volflag=ufavico)")
ap2.add_argument("--ext-th", metavar="E=VP", type=u, action="append", help="\033[34mREPEATABLE:\033[0m use thumbnail-image \033[33mVP\033[0m for file-extension \033[33mE\033[0m, example: [\033[32mexe=/.res/exe.png\033[0m] (volflag=ext_th)") ap2.add_argument("--ext-th", metavar="E=VP", type=u, action="append", help="\033[34mREPEATABLE:\033[0m use thumbnail-image \033[33mVP\033[0m for file-extension \033[33mE\033[0m, example: [\033[32mexe=/.res/exe.png\033[0m] (volflag=ext_th)")

View file

@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
VERSION = (1, 20, 12) VERSION = (1, 20, 13)
CODENAME = "sftp is fine too" CODENAME = "sftp is fine too"
BUILD_DT = (2026, 3, 11) BUILD_DT = (2026, 3, 23)
S_VERSION = ".".join(map(str, VERSION)) S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View file

@ -709,7 +709,7 @@ class VFS(object):
if self.shr_files: if self.shr_files:
assert self.shr_src # !rm assert self.shr_src # !rm
if rem and rem not in self.shr_files: if rem and rem not in self.shr_files:
return "\n\n" return "\n\n\0\n\n"
if resolve: if resolve:
rap = absreal(ap) rap = absreal(ap)
vn, rem = self.shr_src vn, rem = self.shr_src
@ -718,7 +718,7 @@ class VFS(object):
# not the dir itself; assert file allowed # not the dir itself; assert file allowed
ad, fn = os.path.split(rap) ad, fn = os.path.split(rap)
if chk != ad or fn not in self.shr_files: if chk != ad or fn not in self.shr_files:
return "\n\n" return "\n\n\0\n\n"
return (rap or absreal(ap)) if resolve else ap return (rap or absreal(ap)) if resolve else ap
@ -737,7 +737,7 @@ class VFS(object):
if chk != absreal(ap): if chk != absreal(ap):
# not the dir itself; assert file allowed # not the dir itself; assert file allowed
if ad != chk or fn not in self.shr_files: if ad != chk or fn not in self.shr_files:
return "\n\n" return "\n\n\0\n\n"
return os.path.join(ad, fn) return os.path.join(ad, fn)
@ -880,9 +880,6 @@ class VFS(object):
yield dbv, vrem, rel, fsroot, rfiles, rdirs, vfs_virt yield dbv, vrem, rel, fsroot, rfiles, rdirs, vfs_virt
for rdir, _ in rdirs: for rdir, _ in rdirs:
if not dots_ok and rdir.startswith("."):
continue
wrel = (rel + "/" + rdir).lstrip("/") wrel = (rel + "/" + rdir).lstrip("/")
wrem = (rem + "/" + rdir).lstrip("/") wrem = (rem + "/" + rdir).lstrip("/")
for x in self.walk( for x in self.walk(
@ -1939,7 +1936,7 @@ class AuthSrv(object):
vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True) vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
vol.root = vfs vol.root = vfs
zs = "du_iwho emb_all ls_q_m neversymlink" zs = "du_iwho emb_all ls_q_m neversymlink oh_f oh_g"
k_ign = set(zs.split()) k_ign = set(zs.split())
for vol in vfs.all_vols.values(): for vol in vfs.all_vols.values():
unknown_flags = set() unknown_flags = set()
@ -1988,6 +1985,10 @@ class AuthSrv(object):
[sun] if "m" in s_pr else [], [sun] if "m" in s_pr else [],
[sun] if "d" in s_pr else [], [sun] if "d" in s_pr else [],
[sun] if "g" in s_pr else [], [sun] if "g" in s_pr else [],
[], # G
[], # h
[], # a
[sun] if "." in s_pr or self.args.ed else [],
) )
# don't know the abspath yet + wanna ensure the user # don't know the abspath yet + wanna ensure the user
@ -3257,6 +3258,7 @@ class AuthSrv(object):
"idxh": int(self.args.ih), "idxh": int(self.args.ih),
"dutc": not self.args.localtime, "dutc": not self.args.localtime,
"dfszf": self.args.ui_filesz.strip("-"), "dfszf": self.args.ui_filesz.strip("-"),
"dgauto": self.args.gauto,
"themes": self.args.themes, "themes": self.args.themes,
"turbolvl": self.args.turbo, "turbolvl": self.args.turbo,
"nosubtle": self.args.nosubtle, "nosubtle": self.args.nosubtle,
@ -3272,7 +3274,7 @@ class AuthSrv(object):
for zs in zs.split(): for zs in zs.split():
if vf.get(zs): if vf.get(zs):
js_htm[zs] = 1 js_htm[zs] = 1
zs = "notooltips" zs = "glang notooltips"
for zs in zs.split(): for zs in zs.split():
if getattr(self.args, zs, False): if getattr(self.args, zs, False):
js_htm[zs] = 1 js_htm[zs] = 1

View file

@ -146,24 +146,27 @@ class BrokerMp(object):
returns a Queue object which eventually contains the response if want_retval returns a Queue object which eventually contains the response if want_retval
(not-impl here since nothing uses it yet) (not-impl here since nothing uses it yet)
""" """
if dest == "httpsrv.listen": if dest.startswith("httpsrv."):
for p in self.procs: if dest == "httpsrv.listen":
p.q_pend.put((0, dest, [args[0], len(self.procs)])) for p in self.procs:
p.q_pend.put((0, dest, [args[0], len(self.procs)]))
elif dest == "httpsrv.set_netdevs": else:
for p in self.procs: for p in self.procs:
p.q_pend.put((0, dest, list(args))) p.q_pend.put((0, dest, list(args)))
elif dest == "cb_httpsrv_up": elif dest == "cb_httpsrv_up":
self.hub.cb_httpsrv_up() self.hub.cb_httpsrv_up()
elif dest == "httpsrv.set_bad_ver":
for p in self.procs:
p.q_pend.put((0, dest, list(args)))
else: else:
raise Exception("what is " + str(dest)) raise Exception("what is " + str(dest))
def say1(self, dest: str, *args: Any) -> None:
"""
send message to one lucky recipient
"""
p = self.procs[0]
p.q_pend.put((0, dest, list(args)))
def periodic(self) -> None: def periodic(self) -> None:
while True: while True:
time.sleep(1) time.sleep(1)

View file

@ -58,13 +58,8 @@ class BrokerThr(BrokerCli):
self.httpsrv.listen(args[0], 1) self.httpsrv.listen(args[0], 1)
return return
if dest == "httpsrv.set_netdevs": getattr(self.httpsrv, dest[8:])(*args)
self.httpsrv.set_netdevs(args[0]) return
return
if dest == "httpsrv.set_bad_ver":
self.httpsrv.set_bad_ver()
return
# new ipc invoking managed service in hub # new ipc invoking managed service in hub
obj = self.hub obj = self.hub

View file

@ -21,8 +21,10 @@ def vf_bmap() -> dict[str, str]:
"no_dupe": "nodupe", "no_dupe": "nodupe",
"no_dupe_m": "nodupem", "no_dupe_m": "nodupem",
"no_forget": "noforget", "no_forget": "noforget",
"no_html": "nohtml",
"no_pipe": "nopipe", "no_pipe": "nopipe",
"no_robots": "norobots", "no_robots": "norobots",
"no_script": "noscript",
"no_tail": "notail", "no_tail": "notail",
"no_thumb": "dthumb", "no_thumb": "dthumb",
"no_vthumb": "dvthumb", "no_vthumb": "dvthumb",
@ -33,6 +35,7 @@ def vf_bmap() -> dict[str, str]:
for k in ( for k in (
"dedup", "dedup",
"dlni", "dlni",
"dothidden",
"dotsrch", "dotsrch",
"e2d", "e2d",
"e2ds", "e2ds",
@ -147,6 +150,7 @@ def vf_vmap() -> dict[str, str]:
"tail_tmax", "tail_tmax",
"tail_who", "tail_who",
"tcolor", "tcolor",
"th_pregen",
"th_qv", "th_qv",
"th_qvx", "th_qvx",
"th_spec_p", "th_spec_p",
@ -314,6 +318,7 @@ flagcats = {
"aconvt": "convert-to-audio timeout in seconds", "aconvt": "convert-to-audio timeout in seconds",
"th_spec_p=1": "make spectrograms? 0=never 1=fallback 2=always", "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", "ext_th=s=/b.png": "use /b.png as thumbnail for file-extension s",
"th_pregen=w,wf": "pregenerate thumbs for these formats",
}, },
"handlers\n(better explained in --help-handlers)": { "handlers\n(better explained in --help-handlers)": {
"on404=PY": "handle 404s by executing PY file", "on404=PY": "handle 404s by executing PY file",
@ -340,6 +345,7 @@ flagcats = {
"hsortn": "number of sort-rules to add to media URLs", "hsortn": "number of sort-rules to add to media URLs",
"ufavico=URL": "per-volume favicon (.ico/png/gif/svg)", "ufavico=URL": "per-volume favicon (.ico/png/gif/svg)",
"unlist": "dont list files matching REGEX", "unlist": "dont list files matching REGEX",
"dothidden": "enable support for .hidden files",
"dlni": "force-download (no-inline) files on click", "dlni": "force-download (no-inline) files on click",
"html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH", "html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH",
"html_head_s=TXT": "additional static text in the html <head>", "html_head_s=TXT": "additional static text in the html <head>",

View file

@ -7,7 +7,7 @@ import os
import re import re
import time import time
from .__init__ import ANYWIN, MACOS from .__init__ import ANYWIN, FREEBSD, MACOS, UNIX
from .authsrv import AXS, VFS, AuthSrv from .authsrv import AXS, VFS, AuthSrv
from .bos import bos from .bos import bos
from .util import chkcmd, json_hesc, min_ex, undot from .util import chkcmd, json_hesc, min_ex, undot
@ -88,7 +88,7 @@ class Fstab(object):
def _from_sp_mount(self) -> dict[str, str]: def _from_sp_mount(self) -> dict[str, str]:
sptn = r"^.*? on (.*) type ([^ ]+) \(.*" sptn = r"^.*? on (.*) type ([^ ]+) \(.*"
if MACOS: if MACOS or FREEBSD:
sptn = r"^.*? on (.*) \(([^ ]+), .*" sptn = r"^.*? on (.*) \(([^ ]+), .*"
ptn = re.compile(sptn) ptn = re.compile(sptn)
@ -118,7 +118,7 @@ class Fstab(object):
def build_tab(self) -> None: def build_tab(self) -> None:
self.log("inspecting mtab for changes") self.log("inspecting mtab for changes")
dtab = self._from_sp_mount() if MACOS else self._from_proc() dtab = self._from_sp_mount() if UNIX else self._from_proc()
# keep empirically-correct values if mounttab unchanged # keep empirically-correct values if mounttab unchanged
srctab = str(sorted(dtab.items())) srctab = str(sorted(dtab.items()))
@ -130,7 +130,7 @@ class Fstab(object):
try: try:
fuses = [mp for mp, fs in dtab.items() if fs == "fuseblk"] fuses = [mp for mp, fs in dtab.items() if fs == "fuseblk"]
if not fuses or MACOS: if not fuses or UNIX:
raise Exception() raise Exception()
try: try:
so, _ = chkcmd(["lsblk", "-nrfo", "FSTYPE,MOUNTPOINT"]) # centos6 so, _ = chkcmd(["lsblk", "-nrfo", "FSTYPE,MOUNTPOINT"]) # centos6

View file

@ -24,6 +24,7 @@ from .util import (
ODict, ODict,
Pebkac, Pebkac,
exclude_dotfiles, exclude_dotfiles,
exclude_dothidden,
fsenc, fsenc,
ipnorm, ipnorm,
pybin, pybin,
@ -350,7 +351,10 @@ class FtpFs(AbstractedFS):
vfs_ls.extend(vfs_virt.keys()) vfs_ls.extend(vfs_virt.keys())
if self.uname not in vfs.axs.udot: if self.uname not in vfs.axs.udot:
vfs_ls = exclude_dotfiles(vfs_ls) if "dothidden" in vfs.flags and ".hidden" in [x[0] for x in vfs_ls]:
vfs_ls = exclude_dothidden(vfs_ls, fsroot)
else:
vfs_ls = exclude_dotfiles(vfs_ls)
vfs_ls.sort() vfs_ls.sort()
return vfs_ls return vfs_ls

View file

@ -66,6 +66,8 @@ from .util import (
eol_conv, eol_conv,
exclude_dotfiles, exclude_dotfiles,
exclude_dotfiles_ls, exclude_dotfiles_ls,
exclude_dothidden,
exclude_dothidden_ls,
formatdate, formatdate,
fsenc, fsenc,
gen_content_disposition, gen_content_disposition,
@ -152,7 +154,7 @@ _ = (argparse, threading)
USED4SEC = {"usedforsecurity": False} if sys.version_info > (3, 9) else {} USED4SEC = {"usedforsecurity": False} if sys.version_info > (3, 9) else {}
ALL_COOKIES = "k304 no304 js idxh dots cppwd cppws".split() ALL_COOKIES = "cplng cppwd cppws dots idxh js k304 no304".split()
BADXFF = " due to dangerous misconfiguration (the http-header specified by --xff-hdr was received from an untrusted reverse-proxy)" BADXFF = " due to dangerous misconfiguration (the http-header specified by --xff-hdr was received from an untrusted reverse-proxy)"
BADXFF2 = ". Some copyparty features are now disabled as a safety measure.\n\n\n" BADXFF2 = ". Some copyparty features are now disabled as a safety measure.\n\n\n"
@ -228,8 +230,7 @@ class HttpCli(object):
self.args = conn.args # mypy404 self.args = conn.args # mypy404
self.E: EnvParams = self.args.E self.E: EnvParams = self.args.E
self.asrv = conn.asrv # mypy404 self.asrv = conn.asrv # mypy404
self.ico = conn.ico # mypy404 self.thumbcli = conn.hsrv.thumbcli
self.thumbcli = conn.thumbcli # mypy404
self.u2fh = conn.u2fh # mypy404 self.u2fh = conn.u2fh # mypy404
self.pipes = conn.pipes # mypy404 self.pipes = conn.pipes # mypy404
self.log_func = conn.log_func # mypy404 self.log_func = conn.log_func # mypy404
@ -534,7 +535,7 @@ class HttpCli(object):
else: else:
self.keepalive = False self.keepalive = False
ptn: Optional[Pattern[str]] = self.conn.lf_url # mypy404 ptn = self.conn.lf_url
self.do_log = not ptn or not ptn.search(self.req) self.do_log = not ptn or not ptn.search(self.req)
if self.args.ihead and self.do_log: if self.args.ihead and self.do_log:
@ -1844,7 +1845,7 @@ class HttpCli(object):
raise Pebkac(401, "authenticate") raise Pebkac(401, "authenticate")
elif depth == "1": elif depth == "1":
_, vfs_ls, vfs_virt = vn.ls( fsroot, vfs_ls, vfs_virt = vn.ls(
rem, rem,
self.uname, self.uname,
not self.args.no_scandir, not self.args.no_scandir,
@ -1855,13 +1856,20 @@ class HttpCli(object):
if not self.can_read: if not self.can_read:
vfs_ls = [] vfs_ls = []
if not self.can_dot: if not self.can_dot:
vfs_ls = exclude_dotfiles_ls(vfs_ls) if "dothidden" in vn.flags and ".hidden" in [x[0] for x in vfs_ls]:
vfs_ls = exclude_dothidden_ls(vfs_ls, fsroot)
self.dothid = True
else:
vfs_ls = exclude_dotfiles_ls(vfs_ls)
fgen = [{"vp": vp, "st": st} for vp, st in vfs_ls] fgen = [{"vp": vp, "st": st} for vp, st in vfs_ls]
if vfs_virt: if vfs_virt:
zsl = list(vfs_virt) zsl = list(vfs_virt)
if not self.can_dot: if not self.can_dot:
zsl = exclude_dotfiles(zsl) if "dothidden" in vn.flags and getattr(self, "dothid", False):
zsl = exclude_dothidden(zsl, fsroot)
else:
zsl = exclude_dotfiles(zsl)
fgen += [{"vp": v, "st": vst} for v in zsl] fgen += [{"vp": v, "st": vst} for v in zsl]
else: else:
@ -4767,7 +4775,7 @@ class HttpCli(object):
else: else:
mime = guess_mime(cdis) mime = guess_mime(cdis)
if mime not in SAFE_MIMES and "nohtml" in self.vn.flags: if mime not in SAFE_MIMES and "nohtml" in self.vn.flags and oh_k != "oh_g":
mime = safe_mime(mime) mime = safe_mime(mime)
self.out_headers["Accept-Ranges"] = "bytes" self.out_headers["Accept-Ranges"] = "bytes"
@ -5285,7 +5293,7 @@ class HttpCli(object):
# chrome cannot handle more than ~2000 unique SVGs # chrome cannot handle more than ~2000 unique SVGs
# so url-param "raster" returns a png/webp instead # so url-param "raster" returns a png/webp instead
# (useragent-sniffing kinshi due to caching proxies) # (useragent-sniffing kinshi due to caching proxies)
mime, ico = self.ico.get(txt, not small, "raster" in self.uparam) mime, ico = self.conn.hsrv.ico.get(txt, not small, "raster" in self.uparam)
lm = formatdate(self.E.t0) lm = formatdate(self.E.t0)
self.reply(ico, mime=mime, headers={"Last-Modified": lm}) self.reply(ico, mime=mime, headers={"Last-Modified": lm})
@ -5644,7 +5652,7 @@ class HttpCli(object):
no304vis=self.args.no304 > 0, no304vis=self.args.no304 > 0,
msg=( msg=(
BADVER BADVER
if self.conn.hsrv.bad_ver and self.can_admin if self.conn.hsrv.bad_ver and avol
else BADXFFB else BADXFFB
if hasattr(self, "bad_xff") if hasattr(self, "bad_xff")
else "" else ""
@ -5860,6 +5868,7 @@ class HttpCli(object):
dk_sz = vn.flags.get("dk") dk_sz = vn.flags.get("dk")
except: except:
dk_sz = None dk_sz = None
vn = vfs
vfs_ls = [] vfs_ls = []
vfs_virt = {} vfs_virt = {}
for v in self.rvol: for v in self.rvol:
@ -5870,7 +5879,11 @@ class HttpCli(object):
dirs = [x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)] dirs = [x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]
if not dots: if not dots:
dirs = exclude_dotfiles(dirs) if "dothidden" in vn.flags and ".hidden" in [x[0] for x in vfs_ls]:
dirs = exclude_dothidden(dirs, fsroot)
self.dothid = True
else:
dirs = exclude_dotfiles(dirs)
dirs = [quotep(x) for x in dirs if x != excl] dirs = [quotep(x) for x in dirs if x != excl]
@ -5900,7 +5913,10 @@ class HttpCli(object):
x += "\n" x += "\n"
dirs.append(quotep(x)) dirs.append(quotep(x))
if not dots: if not dots:
dirs = exclude_dotfiles(dirs) if "dothidden" in vn.flags and getattr(self, "dothid", False):
dirs = exclude_dothidden(dirs, fsroot)
else:
dirs = exclude_dotfiles(dirs)
ret["a"] = dirs ret["a"] = dirs
return ret return ret
@ -6436,13 +6452,18 @@ class HttpCli(object):
s_rd = "read" in req["perms"] s_rd = "read" in req["perms"]
s_wr = "write" in req["perms"] s_wr = "write" in req["perms"]
s_get = "get" in req["perms"] s_get = "get" in req["perms"]
s_dot = "dot" in req["perms"]
s_axs = [s_rd, s_wr, False, False, s_get] s_axs = [s_rd, s_wr, False, False, s_get]
s_axsd = s_axs + [s_dot]
if s_axs == [False] * 5: if s_axs == [False] * 5:
raise Pebkac(400, "select at least one permission") raise Pebkac(400, "select at least one permission")
try: try:
vfs, rem = self.asrv.vfs.get(vp, self.uname, *s_axs) vfs, rem = self.asrv.vfs.get(vp, self.uname, *s_axs)
can_dot = self.uname in vfs.axs.udot
if s_dot and not can_dot:
raise Exception()
except: except:
raise Pebkac(400, "you dont have all the perms you tried to grant") raise Pebkac(400, "you dont have all the perms you tried to grant")
@ -6455,7 +6476,10 @@ class HttpCli(object):
raise Pebkac(400, "you dont have perms to create shares from this volume") 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_axs]) ap, reals, _ = vfs.ls(rem, self.uname, not self.args.no_scandir, [s_axs])
rfns = set([x[0] for x in reals]) zsl = [x[0] for x in reals]
if not can_dot:
zsl = exclude_dotfiles(zsl)
rfns = set(zsl)
for fn in fns: for fn in fns:
if fn not in rfns: if fn not in rfns:
raise Pebkac(400, "selected file not found on disk: %r" % (fn,)) raise Pebkac(400, "selected file not found on disk: %r" % (fn,))
@ -6466,7 +6490,7 @@ class HttpCli(object):
sexp = req["exp"] sexp = req["exp"]
exp = int(sexp) if sexp else 0 exp = int(sexp) if sexp else 0
exp = now + exp * 60 if exp else 0 exp = now + exp * 60 if exp else 0
pr = "".join(zc for zc, zb in zip("rwmdg", s_axs) if zb) pr = "".join(zc for zc, zb in zip("rwmdg.", s_axsd) if zb)
q = "insert into sh values (?,?,?,?,?,?,?,?)" q = "insert into sh values (?,?,?,?,?,?,?,?)"
cur.execute(q, (skey, pw, vp, pr, len(fns), self.uname, now, exp)) cur.execute(q, (skey, pw, vp, pr, len(fns), self.uname, now, exp))
@ -7002,6 +7026,8 @@ class HttpCli(object):
perms.append("move") perms.append("move")
if self.can_delete: if self.can_delete:
perms.append("delete") perms.append("delete")
if self.can_dot:
perms.append("dot")
if self.can_get: if self.can_get:
perms.append("get") perms.append("get")
if self.can_upget: if self.can_upget:
@ -7150,7 +7176,10 @@ class HttpCli(object):
if not self.can_dot or ( if not self.can_dot or (
"dots" not in self.uparam and (is_ls or "dots" not in self.cookies) "dots" not in self.uparam and (is_ls or "dots" not in self.cookies)
): ):
ls_names = exclude_dotfiles(ls_names) if "dothidden" in vf and ".hidden" in ls_names:
ls_names = exclude_dothidden(ls_names, fsroot)
else:
ls_names = exclude_dotfiles(ls_names)
add_dk = vf.get("dk") add_dk = vf.get("dk")
add_fk = vf.get("fk") add_fk = vf.get("fk")

View file

@ -21,12 +21,8 @@ from . import util as Util
from .__init__ import TYPE_CHECKING, EnvParams from .__init__ import TYPE_CHECKING, EnvParams
from .authsrv import AuthSrv # typechk from .authsrv import AuthSrv # typechk
from .httpcli import HttpCli from .httpcli import HttpCli
from .ico import Ico
from .mtag import HAVE_FFMPEG
from .th_cli import ThumbCli
from .th_srv import HAVE_PIL, HAVE_VIPS
from .u2idx import U2idx from .u2idx import U2idx
from .util import HMaccas, NetMap, shut_socket from .util import HMaccas, NetMap, min_ex, shut_socket
if True: # pylint: disable=using-constant-test if True: # pylint: disable=using-constant-test
from typing import Optional, Pattern, Union from typing import Optional, Pattern, Union
@ -69,21 +65,15 @@ class HttpConn(object):
self.bans: dict[str, int] = hsrv.bans self.bans: dict[str, int] = hsrv.bans
self.aclose: dict[str, int] = hsrv.aclose self.aclose: dict[str, int] = hsrv.aclose
enth = (HAVE_PIL or HAVE_VIPS or HAVE_FFMPEG) and not self.args.no_thumb
self.thumbcli: Optional[ThumbCli] = ThumbCli(hsrv) if enth else None # mypy404
self.ico: Ico = Ico(self.args) # mypy404
self.t0: float = time.time() # mypy404 self.t0: float = time.time() # mypy404
self.freshen_pwd: float = 0.0 self.freshen_pwd: float = 0.0
self.stopping = False self.stopping = False
self.nreq: int = -1 # mypy404 self.nreq: int = -1 # mypy404
self.nbyte: int = 0 # mypy404 self.nbyte: int = 0 # mypy404
self.u2idx: Optional[U2idx] = None self.u2idx: Optional[U2idx] = None
self.lf_url: Optional[Pattern[str]] = self.args.lf_url
self.log_func: "Util.RootLogger" = hsrv.log # mypy404 self.log_func: "Util.RootLogger" = hsrv.log # mypy404
self.log_src: str = "httpconn" # mypy404 self.log_src: str = "httpconn" # mypy404
self.lf_url: Optional[Pattern[str]] = (
re.compile(self.args.lf_url) if self.args.lf_url else None
) # mypy404
self.set_rproxy() self.set_rproxy()
def shutdown(self) -> None: def shutdown(self) -> None:
@ -204,12 +194,12 @@ class HttpConn(object):
except Exception as ex: except Exception as ex:
em = str(ex) em = str(ex)
if "ALERT_CERTIFICATE_UNKNOWN" in em: if "ALERT_" in em:
# android-chrome keeps doing this self.log("client refused our TLS cert or config: " + em, c=6)
pass
else: else:
self.log("handshake\033[0m " + em, c=5) t = "https-handshake failed, probably due to client:\n"
self.log(t + min_ex(), c=5)
return return

View file

@ -55,8 +55,14 @@ except SyntaxError:
) )
sys.exit(1) sys.exit(1)
from .authsrv import LEELOO_DALLAS
from .httpconn import HttpConn from .httpconn import HttpConn
from .ico import Ico
from .metrics import Metrics from .metrics import Metrics
from .mtag import HAVE_FFMPEG
from .sutil import gfilter2
from .th_cli import ThumbCli
from .th_srv import HAVE_PIL, HAVE_VIPS
from .u2idx import U2idx from .u2idx import U2idx
from .util import ( from .util import (
E_SCK, E_SCK,
@ -133,6 +139,9 @@ class HttpSrv(object):
self.bans: dict[str, int] = {} self.bans: dict[str, int] = {}
self.aclose: dict[str, int] = {} self.aclose: dict[str, int] = {}
self.thumbcli: Optional[ThumbCli] = None
self.ico: Ico = Ico(self.args)
dli: dict[str, tuple[float, int, "VFS", str, str]] = {} # info dli: dict[str, tuple[float, int, "VFS", str, str]] = {} # info
dls: dict[str, tuple[float, int]] = {} # state dls: dict[str, tuple[float, int]] = {} # state
self.dli = self.tdli = dli self.dli = self.tdli = dli
@ -230,16 +239,20 @@ class HttpSrv(object):
if self.args.log_thrs: if self.args.log_thrs:
start_log_thrs(self.log, self.args.log_thrs, nid) start_log_thrs(self.log, self.args.log_thrs, nid)
self.th_cfg: dict[str, set[str]] = {} if (HAVE_PIL or HAVE_VIPS or HAVE_FFMPEG) and not self.args.no_thumb:
Daemon(self.post_init, "hsrv-init2") Daemon(self.post_init, "hsrv-init2")
def post_init(self) -> None: def post_init(self) -> None:
try: try:
x = self.broker.ask("thumbsrv.getcfg") x = self.broker.ask("thumbsrv.getcfg")
self.th_cfg = x.get() self.thumbcli = ThumbCli(self, x.get())
except: except:
pass pass
def set_th_cfg(self, c: dict[str, set[str]], opts: tuple[bool]) -> None:
self.args.th_no_jxl = opts[0]
self.thumbcli = ThumbCli(self, c)
def set_bad_ver(self) -> None: def set_bad_ver(self) -> None:
self.bad_ver = True self.bad_ver = True
@ -646,3 +659,73 @@ class HttpSrv(object):
self.tdli = dli self.tdli = dli
self.tdls = dls self.tdls = dls
def pregen_thumbs(self) -> None:
Daemon(self._pregen_thumbs, "th_pregen")
def _pregen_thumbs(self) -> None:
def log(msg, n):
self.log("thumb-pregen", msg, n)
if getattr(self, "_pregen", False):
log("already running", 1)
return
self._pregen = True
for n in range(9999999):
x = self.broker.ask("up2k.is_busy")
zb, zi = x.get()
if zi:
break
if not n:
log("waiting for up2k to finish initializing", 6)
time.sleep(1 if n < 10 else 5 if n < 300 else 15)
if not self.thumbcli:
log("no thumbcli", 1)
return
if self.args.th_pre_rl:
try:
self.broker.hub.thumbsrv.log = self.broker.hub.thumbsrv._slog
except:
pass
nfiles = 0
t0 = time.time()
scandir = not self.args.no_scandir
for vn in self.asrv.vfs.all_nodes.values():
fmts = vn.flags.get("th_pregen", "")
if not fmts:
continue
log("starting for volume /%s" % (vn.vpath,), 6)
g = vn.walk("x", "/", [], LEELOO_DALLAS, [[True]], 2, scandir, False, False)
g = gfilter2(g, self, vn.vpath, fmts.split(","))
for f in g:
nfiles += 1
if not nfiles % 256:
now = time.time()
for n in range(9999999):
x = self.broker.ask("up2k.is_busy")
zb, zi = x.get()
if not zb:
if n:
t0 += time.time() - now
break
if not n:
log("waiting for up2k to finish indexing", 6)
time.sleep(5)
if self.args.th_pre_rl:
try:
self.broker.hub.thumbsrv.log = self.broker.hub.thumbsrv._log
except:
pass
t = "finished; %d files in %d seconds"
log(t % (nfiles, time.time() - t0), 6)
self._pregen = False
if self.args.exit == "thgen":
self.broker.say("sigterm")

View file

@ -30,6 +30,10 @@ from .util import (
Daemon, Daemon,
ODict, ODict,
Pebkac, Pebkac,
exclude_dotfiles,
exclude_dotfiles_ls,
exclude_dothidden,
exclude_dothidden_ls,
ipnorm, ipnorm,
min_ex, min_ex,
read_utf8, read_utf8,
@ -401,21 +405,27 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
self.log("ls(%s): vfs-vols; |%d|" % (path, len(ret))) self.log("ls(%s): vfs-vols; |%d|" % (path, len(ret)))
return ret return ret
_, vfs_ls, vfs_virt = vn.ls( fsroot, vfs_ls, vfs_virt = vn.ls(
rem, rem,
self.uname, self.uname,
not self.args.no_scandir, not self.args.no_scandir,
[[True, False], [False, True]], [[True, False], [False, True]],
throw=True, throw=True,
) )
vnames = list(vfs_virt)
if self.uname not in vn.axs.udot:
if "dothidden" in vn.flags and ".hidden" in [x[0] for x in vfs_ls]:
vfs_ls = exclude_dothidden_ls(vfs_ls, fsroot)
vnames = exclude_dothidden(vnames, fsroot)
else:
vfs_ls = exclude_dotfiles_ls(vfs_ls)
vnames = exclude_dotfiles(vnames)
ret = [SATTR.from_stat(x[1], filename=x[0]) for x in vfs_ls] ret = [SATTR.from_stat(x[1], filename=x[0]) for x in vfs_ls]
for zs, vn2 in vfs_virt.items(): for zs, vn2 in vfs_virt.items():
if not vn2.realpath: if not vn2.realpath or zs not in vnames:
continue continue
st = bos.stat(vn2.realpath) st = bos.stat(vn2.realpath)
ret.append(SATTR.from_stat(st, filename=zs)) ret.append(SATTR.from_stat(st, filename=zs))
if self.uname not in vn.axs.udot:
ret = [x for x in ret if not x.filename.split("/")[-1].startswith(".")]
ret.sort(key=lambda x: x.filename) ret.sort(key=lambda x: x.filename)
self.log("ls(%s): |%d|" % (path, len(ret))) self.log("ls(%s): |%d|" % (path, len(ret)))
return ret return ret
@ -659,7 +669,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
self.log("mkdir(%s)" % (vp,)) self.log("mkdir(%s)" % (vp,))
try: try:
vn, rem = self.asrv.vfs.get(vp, self.uname, False, True) vn, rem = self.asrv.vfs.get(vp, self.uname, False, True)
ap = os.path.join(vn.realpath, rem) ap = vn.canonical(rem, False)
bos.makedirs(ap, vf=vn.flags) # filezilla expects this bos.makedirs(ap, vf=vn.flags) # filezilla expects this
if attr is not None: if attr is not None:
paramiko.SFTPServer.set_file_attr(ap, attr) paramiko.SFTPServer.set_file_attr(ap, attr)

View file

@ -89,13 +89,15 @@ class SMB(object):
smbserver.isInFileJail = self._is_in_file_jail smbserver.isInFileJail = self._is_in_file_jail
self._disarm() self._disarm()
ip = next((x for x in self.args.smb_i if ":" not in x), None) zs = " " if self.args.smb6 else ":"
ip = next((x for x in self.args.smb_i if zs not in x), None)
if not ip: if not ip:
self.log("smb", "IPv6 not supported for SMB; listening on 0.0.0.0", 3) self.log("smb", "IPv6 not enabled with --smb6; listening on 0.0.0.0", 3)
ip = "0.0.0.0" ip = "0.0.0.0"
port = int(self.args.smb_port) port = int(self.args.smb_port)
srv = smbserver.SimpleSMBServer(listenAddress=ip, listenPort=port) kw = {"ipv6": True} if ":" in ip else {}
srv = smbserver.SimpleSMBServer(listenAddress=ip, listenPort=port, **kw)
try: try:
if self.accs: if self.accs:
srv.setAuthCallback(self._auth_cb) srv.setAuthCallback(self._auth_cb)
@ -121,6 +123,7 @@ class SMB(object):
self.srv = srv self.srv = srv
self.stop = srv.stop self.stop = srv.stop
ip = "[%s]" % (ip,) if kw else ip
self.log("smb", "listening @ {}:{}".format(ip, port)) self.log("smb", "listening @ {}:{}".format(ip, port))
def nlog(self, msg: str, c: Union[int, str] = 0) -> None: def nlog(self, msg: str, c: Union[int, str] = 0) -> None:

View file

@ -5,18 +5,21 @@ import os
import tempfile import tempfile
from datetime import datetime from datetime import datetime
from .__init__ import CORES from .__init__ import CORES, TYPE_CHECKING
from .authsrv import VFS, AuthSrv from .authsrv import LEELOO_DALLAS, VFS, AuthSrv
from .bos import bos from .bos import bos
from .th_cli import ThumbCli from .th_cli import ThumbCli
from .th_srv import TH_CH from .th_srv import TH_CH
from .util import UTC, vjoin, vol_san from .util import UTC, sigblock, vjoin, vol_san
if True: # pylint: disable=using-constant-test if True: # pylint: disable=using-constant-test
from typing import Any, Generator, Optional from typing import Any, Generator, Optional
from .util import NamedLogger from .util import NamedLogger
if TYPE_CHECKING:
from httpsrv import HttpSrv
TAR_NO_OPUS = set("aac|m4a|m4b|m4r|mp3|oga|ogg|opus|wma".split("|")) TAR_NO_OPUS = set("aac|m4a|m4b|m4r|mp3|oga|ogg|opus|wma".split("|"))
@ -42,6 +45,17 @@ class StreamArc(object):
self.stopped = True self.stopped = True
_pools = {}
def close_pools() -> None:
for p in list(_pools):
try:
p.shutdown(wait=False, cancel_futures=True)
except:
pass
def gfilter( def gfilter(
fgen: Generator[dict[str, Any], None, None], fgen: Generator[dict[str, Any], None, None],
thumbcli: ThumbCli, thumbcli: ThumbCli,
@ -52,7 +66,8 @@ def gfilter(
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
pend = [] pend = []
with ThreadPoolExecutor(max_workers=CORES) as tp: with ThreadPoolExecutor(max_workers=CORES, initializer=sigblock) as tp:
_pools[tp] = 1
try: try:
for f in fgen: for f in fgen:
task = tp.submit(enthumb, thumbcli, uname, vtop, f, fmt) task = tp.submit(enthumb, thumbcli, uname, vtop, f, fmt)
@ -79,6 +94,61 @@ def gfilter(
except: except:
pass pass
thumbcli.log("gfilter flushed") thumbcli.log("gfilter flushed")
_pools.pop(tp, None)
def gfilter2(
fgen: Generator[
tuple[
"VFS",
str,
str,
str,
list[tuple[str, os.stat_result]],
list[tuple[str, os.stat_result]],
dict[str, "VFS"],
],
None,
None,
],
hsrv: "HttpSrv",
vtop: str,
fmts: list[str],
) -> Generator[dict[str, Any], None, None]:
from concurrent.futures import ThreadPoolExecutor
pend = []
with ThreadPoolExecutor(max_workers=CORES, initializer=sigblock) as tp:
_pools[tp] = 1
for _, _, vpath, apath, files, rd, vd in fgen:
if "/.hist/" in vpath:
continue
fnames = [n[0] for n in files]
vpaths = [vpath + "/" + n for n in fnames] if vpath else fnames
for vp, fi in zip(vpaths, files):
for fmt in fmts:
try:
f = {"vp": vp, "st": fi[1]}
task = tp.submit(
enthumb, hsrv.thumbcli, LEELOO_DALLAS, vtop, f, fmt
)
pend.append((task, f))
if pend[0][0].done() or len(pend) > CORES * 4:
task, f = pend.pop(0)
try:
f = task.result(600)
except:
pass
yield f
except:
pass
for task, f in pend:
try:
f = task.result(600)
except:
pass
yield f
_pools.pop(tp, None)
def enthumb( def enthumb(

View file

@ -35,6 +35,7 @@ from .cert import ensure_cert
from .fsutil import ramdisk_chk from .fsutil import ramdisk_chk
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN
from .pwhash import HAVE_ARGON2 from .pwhash import HAVE_ARGON2
from .sutil import close_pools as sutil_close_pools
from .tcpsrv import TcpSrv from .tcpsrv import TcpSrv
from .th_srv import ( from .th_srv import (
H_PIL_AVIF, H_PIL_AVIF,
@ -171,6 +172,9 @@ class SvcHub(object):
args.reflink = True args.reflink = True
args.dav_auth = True args.dav_auth = True
args.vague_403 = True args.vague_403 = True
args.no_html = True
args.no_readme = True
args.no_logues = True
args.nih = True args.nih = True
if args.s: if args.s:
@ -196,7 +200,14 @@ class SvcHub(object):
self.log_div = 10 ** (6 - args.log_tdec) self.log_div = 10 ** (6 - args.log_tdec)
self.log_efmt = "%02d:%02d:%02d.%0{}d".format(args.log_tdec) self.log_efmt = "%02d:%02d:%02d.%0{}d".format(args.log_tdec)
self.log_dfmt = "%04d-%04d-%06d.%0{}d".format(args.log_tdec) self.log_dfmt = "%04d-%04d-%06d.%0{}d".format(args.log_tdec)
self.log = self._log_disabled if args.q else self._log_enabled
if args.q:
self.log = self._log_disabled
elif args.lo and args.flo == 2 and not self.no_ansi:
self.log = self._log_en_f2
else:
self.log = self._log_enabled
if args.lo: if args.lo:
self._setup_logfile(printed) self._setup_logfile(printed)
@ -253,6 +264,13 @@ class SvcHub(object):
t = "WARNING: --s-rd-sz (%d) is larger than --iobuf (%d); this may lead to reduced performance" t = "WARNING: --s-rd-sz (%d) is larger than --iobuf (%d); this may lead to reduced performance"
self.log("root", t % (args.s_rd_sz, args.iobuf), 3) self.log("root", t % (args.s_rd_sz, args.iobuf), 3)
if args.vc_url:
zi = max(1, int(args.vc_age))
if zi < 3 and "api.copyparty.eu" in args.vc_url:
zi = 3
self.log("root", "vc-age too low for copyparty.eu; will use 3 hours")
args.vc_age = zi
zs = "" zs = ""
if args.th_ram_max < 0.22: if args.th_ram_max < 0.22:
zs = "generate thumbnails" zs = "generate thumbnails"
@ -479,6 +497,8 @@ class SvcHub(object):
for nm in args.ipr_u.values(): for nm in args.ipr_u.values():
nm.mutex = threading.Lock() nm.mutex = threading.Lock()
self._reload_thumbsrv()
def _db_onfail_ses(self) -> None: def _db_onfail_ses(self) -> None:
self.args.no_ses = True self.args.no_ses = True
@ -1138,7 +1158,7 @@ class SvcHub(object):
vsa = [x.upper() for x in vsa if x] vsa = [x.upper() for x in vsa if x]
setattr(al, k + "_set", set(vsa)) setattr(al, k + "_set", set(vsa))
zs = "dav_ua1 sus_urls nonsus_urls ua_nodav ua_nodoc ua_nozip" zs = "dav_ua1 lf_url sus_urls nonsus_urls ua_nodav ua_nodoc ua_nozip"
for k in zs.split(" "): for k in zs.split(" "):
vs = getattr(al, k) vs = getattr(al, k)
if not vs or vs == "no": if not vs or vs == "no":
@ -1244,13 +1264,6 @@ class SvcHub(object):
except: except:
raise Exception("invalid --mv-retry [%s]" % (self.args.mv_retry,)) raise Exception("invalid --mv-retry [%s]" % (self.args.mv_retry,))
if self.args.vc_url:
zi = max(1, int(self.args.vc_age))
if zi < 3 and "api.copyparty.eu" in self.args.vc_url:
zi = 3
self.log("root", "vc-age too low for copyparty.eu; will use 3 hours")
self.args.vc_age = zi
al.js_utc = "false" if al.localtime else "true" al.js_utc = "false" if al.localtime else "true"
al.tcolor = al.tcolor.lstrip("#") al.tcolor = al.tcolor.lstrip("#")
@ -1468,8 +1481,17 @@ class SvcHub(object):
self.log("root", "reload done") self.log("root", "reload done")
t += "\n\nchanges to global options (if any) require a restart of copyparty to take effect" t += "\n\nchanges to global options (if any) require a restart of copyparty to take effect"
self.broker.reload() self.broker.reload()
self._reload_thumbsrv()
return t return t
def _reload_thumbsrv(self) -> None:
if not self.thumbsrv:
return
vols = list(self.asrv.vfs.all_nodes.values())
if next((x for x in vols if x.flags.get("th_pregen", "")), None):
fun = getattr(self.broker, "say1", self.broker.say)
fun("httpsrv.pregen_thumbs")
def _reload_sessions(self) -> None: def _reload_sessions(self) -> None:
with self.asrv.mutex: with self.asrv.mutex:
self.asrv.load_sessions(True) self.asrv.load_sessions(True)
@ -1554,6 +1576,7 @@ class SvcHub(object):
if self.thumbsrv: if self.thumbsrv:
self.thumbsrv.shutdown() self.thumbsrv.shutdown()
sutil_close_pools()
for n in range(200): # 10s for n in range(200): # 10s
time.sleep(0.05) time.sleep(0.05)
@ -1714,6 +1737,69 @@ class SvcHub(object):
if not self.args.no_logflush: if not self.args.no_logflush:
self.logf.flush() self.logf.flush()
def _log_en_f2(self, src: str, msg: str, c: Union[int, str] = 0) -> None:
with self.log_mutex:
dt = datetime.now(self.tz)
if dt.day != self.cday or dt.month != self.cmon:
if self.args.log_date:
zs = dt.strftime(self.args.log_date)
self.log_efmt = "%s %s" % (zs, self.log_efmt.split(" ")[-1])
zs = "{}\n" if self.no_ansi else "\033[36m{}\033[0m\n"
zs = zs.format(dt.strftime("%Y-%m-%d"))
print(zs, end="")
self._set_next_day(dt)
if self.logf:
self.logf.write(zs)
ts = self.log_efmt % (
dt.hour,
dt.minute,
dt.second,
dt.microsecond // self.log_div,
)
# logfile:
if not c:
fmt = "%s %-21s LOG: %s\n"
elif 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"
fsrc = RE_ANSI.sub("", src) if "\033" in src else src
fmsg = RE_ANSI.sub("", msg) if "\033" in msg else msg
fmsg = fmt % (ts, fsrc, fmsg)
# stdout ansi:
fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n"
if not c:
pass
elif isinstance(c, int):
msg = "\033[3%sm%s\033[0m" % (c, msg)
elif "\033" not in c:
msg = "\033[%sm%s\033[0m" % (c, msg)
else:
msg = "%s%s\033[0m" % (c, msg)
msg = fmt % (ts, src, msg)
try:
print(msg, end="")
except UnicodeEncodeError:
try:
print(msg.encode("utf-8", "replace").decode(), end="")
except:
print(msg.encode("ascii", "replace").decode(), end="")
except OSError as ex:
if ex.errno != errno.EPIPE:
raise
self.logf.write(fmsg)
if not self.args.no_logflush:
self.logf.flush()
def pr(self, *a: Any, **ka: Any) -> None: def pr(self, *a: Any, **ka: Any) -> None:
try: try:
with self.log_mutex: with self.log_mutex:
@ -1871,3 +1957,7 @@ class SvcHub(object):
except Exception as e: except Exception as e:
t = "failed to process vulnerability advisory; %s" t = "failed to process vulnerability advisory; %s"
self.log("ver-chk", t % (min_ex()), 1) self.log("ver-chk", t % (min_ex()), 1)
try:
os.unlink(fpath)
except:
pass

View file

@ -7,7 +7,7 @@ import socket
import sys import sys
import time import time
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode from .__init__ import ANYWIN, OPENBSD, PY2, TYPE_CHECKING, UNIX, unicode
from .cert import gencert from .cert import gencert
from .qrkode import QrCode, qr2png, qr2svg, qr2txt, qrgen from .qrkode import QrCode, qr2png, qr2svg, qr2txt, qrgen
from .util import ( from .util import (
@ -510,6 +510,13 @@ class TcpSrv(object):
return eps return eps
def _extdevs_nix(self) -> Generator[str, None, None]: def _extdevs_nix(self) -> Generator[str, None, None]:
if UNIX:
so, _ = chkcmd(["netstat", "-nrf", "inet"])
for ln in so.split("\n"):
if not ln.startswith("default"):
continue
yield ln.split()[7] if OPENBSD else ln.split()[3]
return
with open("/proc/net/route", "rb") as f: with open("/proc/net/route", "rb") as f:
next(f) next(f)
for ln in f: for ln in f:

View file

@ -42,6 +42,7 @@ from .util import (
Daemon, Daemon,
ODict, ODict,
exclude_dotfiles, exclude_dotfiles,
exclude_dothidden,
min_ex, min_ex,
runhook, runhook,
set_fperms, set_fperms,
@ -318,7 +319,11 @@ class Tftpd(object):
ls = virs + reals ls = virs + reals
if "*" not in vn.axs.udot: if "*" not in vn.axs.udot:
names = set(exclude_dotfiles([x[2] for x in ls])) zsl = [x[2] for x in ls]
if "dothidden" in vn.flags and ".hidden" in zsl:
names = set(exclude_dothidden(zsl, fsroot))
else:
names = set(exclude_dotfiles(zsl))
ls = [x for x in ls if x[2] in names] ls = [x for x in ls if x[2] in names]
try: try:

View file

@ -24,7 +24,7 @@ IMG_EXTS = set(["webp", "jpg", "png", "jxl"])
class ThumbCli(object): class ThumbCli(object):
def __init__(self, hsrv: "HttpSrv") -> None: def __init__(self, hsrv: "HttpSrv", c: dict[str, set[str]]) -> None:
self.broker = hsrv.broker self.broker = hsrv.broker
self.log_func = hsrv.log self.log_func = hsrv.log
self.args = hsrv.args self.args = hsrv.args
@ -33,16 +33,6 @@ class ThumbCli(object):
# cache on both sides for less broker spam # cache on both sides for less broker spam
self.cooldown = Cooldown(self.args.th_poke) self.cooldown = Cooldown(self.args.th_poke)
try:
c = hsrv.th_cfg
if not c:
raise Exception()
except:
c = {
k: set()
for k in ["thumbable", "pil", "vips", "raw", "ffi", "ffv", "ffa"]
}
self.thumbable = c["thumbable"] self.thumbable = c["thumbable"]
self.fmt_pil = c["pil"] self.fmt_pil = c["pil"]
self.fmt_vips = c["vips"] self.fmt_vips = c["vips"]
@ -53,8 +43,8 @@ class ThumbCli(object):
# defer args.th_ff_jpg, can change at runtime # defer args.th_ff_jpg, can change at runtime
nonpil = next((x for x in self.args.th_dec if x in ("vips", "ff")), None) nonpil = next((x for x in self.args.th_dec if x in ("vips", "ff")), None)
self.can_webp = H_PIL_WEBP or nonpil self.can_webp = (H_PIL_WEBP or nonpil) and not self.args.th_no_webp
self.can_jxl = H_PIL_JXL or nonpil self.can_jxl = (H_PIL_JXL or nonpil) and not self.args.th_no_jxl
def log(self, msg: str, c: Union[int, str] = 0) -> None: def log(self, msg: str, c: Union[int, str] = 0) -> None:
self.log_func("thumbcli", msg, c) self.log_func("thumbcli", msg, c)
@ -95,23 +85,20 @@ class ThumbCli(object):
if rem.startswith(".hist/th/") and rem.split(".")[-1] in IMG_EXTS: if rem.startswith(".hist/th/") and rem.split(".")[-1] in IMG_EXTS:
return os.path.join(ptop, rem) return os.path.join(ptop, rem)
if fmt[:1] in "jwx" and fmt != "wav": sfmt = fmt[:1]
sfmt = fmt[:1] if sfmt in "jwx" and fmt != "wav":
if sfmt == "j" and self.args.th_no_jpg: if sfmt == "j" and self.args.th_no_jpg:
sfmt = "w" sfmt = "w"
if sfmt == "w": if sfmt == "w":
if ( if not self.can_webp or (
self.args.th_no_webp self.args.th_ff_jpg and (not is_img or preferred == "ff")
or not self.can_webp
or (self.args.th_ff_jpg and (not is_img or preferred == "ff"))
): ):
sfmt = "j" sfmt = "j"
if sfmt == "x": if sfmt == "x" and not self.can_jxl:
if self.args.th_no_jxl or not self.can_jxl: sfmt = "w"
sfmt = "w"
vf_crop = dbv.flags["crop"] vf_crop = dbv.flags["crop"]
vf_th3x = dbv.flags["th3x"] vf_th3x = dbv.flags["th3x"]
@ -128,7 +115,7 @@ class ThumbCli(object):
fmt = sfmt fmt = sfmt
elif fmt[:1] == "p" and not is_au and not is_vid: elif sfmt == "p" and not is_au and not is_vid:
t = "cannot thumbnail %r: png only allowed for waveforms" t = "cannot thumbnail %r: png only allowed for waveforms"
self.log(t % (rem,), 6) self.log(t % (rem,), 6)
return None return None

View file

@ -186,6 +186,10 @@ try:
if os.environ.get("PRTY_NO_VIPS"): if os.environ.get("PRTY_NO_VIPS"):
raise ImportError() raise ImportError()
if "VIPS_CONCURRENCY" not in os.environ:
# reduces glibc RAM usage from 4.7 to 3.5 GiB ...yep, still bonkers
os.environ["VIPS_CONCURRENCY"] = "1"
HAVE_VIPS = True HAVE_VIPS = True
import pyvips import pyvips
@ -256,6 +260,9 @@ class ThumbSrv(object):
self.args = hub.args self.args = hub.args
self.log_func = hub.log self.log_func = hub.log
self.log = self._log
self.nextlog = 0
self.poke_cd = Cooldown(self.args.th_poke) self.poke_cd = Cooldown(self.args.th_poke)
self.mutex = threading.Lock() self.mutex = threading.Lock()
@ -269,6 +276,18 @@ class ThumbSrv(object):
self.exts_spec_unsafe = set(self.args.th_spec_cnv.split(",")) self.exts_spec_unsafe = set(self.args.th_spec_cnv.split(","))
# libvips can easily gobble up 4 GiB of RAM when generating JXL thumbnails on glibc so let's not
self.vips_jxl = False
if HAVE_VIPS and self.args.th_vips_jxl == 2:
self.vips_jxl = True
elif HAVE_VIPS and self.args.th_vips_jxl == 1:
try:
with open("/proc/self/maps", "rb") as f:
zb = f.read()
self.vips_jxl = b"/ld-musl-" in zb and b"mimalloc" not in zb
except:
pass
self.q: Queue[Optional[tuple[str, str, str, VFS]]] = Queue(self.nthr * 4) self.q: Queue[Optional[tuple[str, str, str, VFS]]] = Queue(self.nthr * 4)
for n in range(self.nthr): for n in range(self.nthr):
Daemon(self.worker, "thumb-{}-{}".format(n, self.nthr)) Daemon(self.worker, "thumb-{}-{}".format(n, self.nthr))
@ -327,6 +346,10 @@ class ThumbSrv(object):
self.fmt_pil.discard(f) self.fmt_pil.discard(f)
self.thumbable: set[str] = set() self.thumbable: set[str] = set()
self._build_thumbable()
def _build_thumbable(self) -> None:
self.thumbable.clear()
if "pil" in self.args.th_dec: if "pil" in self.args.th_dec:
self.thumbable |= self.fmt_pil self.thumbable |= self.fmt_pil
@ -341,7 +364,14 @@ class ThumbSrv(object):
for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]: for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]:
self.thumbable |= zss self.thumbable |= zss
def log(self, msg: str, c: Union[int, str] = 0) -> None: def _log(self, msg: str, c: Union[int, str] = 0) -> None:
self.log_func("thumb", msg, c)
def _slog(self, msg: str, c: Union[int, str] = 0) -> None:
now = time.time()
if c in (0, 6) and now < self.nextlog:
return
self.nextlog = now + self.args.th_pre_rl
self.log_func("thumb", msg, c) self.log_func("thumb", msg, c)
def shutdown(self) -> None: def shutdown(self) -> None:
@ -417,6 +447,10 @@ class ThumbSrv(object):
return None return None
def _rebuild_thumbable(self) -> None:
self._build_thumbable()
self.hub.broker.say("httpsrv.set_th_cfg", self.getcfg(), (self.args.th_no_jxl,))
def getcfg(self) -> dict[str, set[str]]: def getcfg(self) -> dict[str, set[str]]:
return { return {
"thumbable": self.thumbable, "thumbable": self.thumbable,
@ -517,7 +551,11 @@ class ThumbSrv(object):
if lib == "pil" and ext in self.fmt_pil and tex in self.fmt_pil: if lib == "pil" and ext in self.fmt_pil and tex in self.fmt_pil:
funs.append(self.conv_pil) funs.append(self.conv_pil)
elif lib == "vips" and ext in self.fmt_vips: elif (
lib == "vips"
and ext in self.fmt_vips
and (tex != "jxl" or self.vips_jxl)
):
funs.append(self.conv_vips) funs.append(self.conv_vips)
elif lib == "raw" and ext in self.fmt_raw: elif lib == "raw" and ext in self.fmt_raw:
funs.append(self.conv_raw) funs.append(self.conv_raw)
@ -807,7 +845,7 @@ class ThumbSrv(object):
b"-q:v", b"-q:v",
unicode(vn.flags["th_qvx"]).encode("ascii"), # default=?? unicode(vn.flags["th_qvx"]).encode("ascii"), # default=??
b"-effort:v", b"-effort:v",
b"8", # default=7, 1=fast, 9=max, 9~=8 but slower b"7", # default=7, 1=fast, 9=max, 9~=8 but slower
] ]
else: else:
cmd += [ cmd += [
@ -834,6 +872,22 @@ class ThumbSrv(object):
ret = 321 ret = 321
c = 3 c = 3
elif cmd[-1].lower().endswith(b".jxl") and (
"Error selecting an encoder" in serr
or "find a suitable output format" in serr
or "Automatic encoder selection failed" in serr
or "Default encoder for format webp" in serr
or "Unrecognized option 'effort:v" in serr
or "Please choose an encoder manually" in serr
):
self.args.th_no_jxl = True
self.fmt_ffi.discard("jxl")
self.fmt_ffv.discard("jxl")
self._rebuild_thumbable()
t = "FFmpeg failed because it was compiled without jpegxl; enabling --th-no-jxl to force webp output:\n"
ret = 321
c = 1
elif ( elif (
(not self.args.th_ff_jpg or time.time() - int(self.args.th_ff_jpg) < 60) (not self.args.th_ff_jpg or time.time() - int(self.args.th_ff_jpg) < 60)
and cmd[-1].lower().endswith(b".webp") and cmd[-1].lower().endswith(b".webp")

View file

@ -337,6 +337,10 @@ class Up2k(object):
if not self.stop: if not self.stop:
self.log("uploads are now possible", 2) self.log("uploads are now possible", 2)
def is_busy(self) -> tuple[bool, float]:
# returns ( currently-busy , have-finished-at-least-once )
return bool(self.pp), self.gt1
def get_state(self, get_q: bool, uname: str) -> str: def get_state(self, get_q: bool, uname: str) -> str:
mtpq: Union[int, str] = 0 mtpq: Union[int, str] = 0
ups = [] ups = []

View file

@ -413,6 +413,7 @@ IMPLICATIONS = [
["tftpvv", "tftpv"], ["tftpvv", "tftpv"],
["nodupem", "nodupe"], ["nodupem", "nodupe"],
["no_dupe_m", "no_dupe"], ["no_dupe_m", "no_dupe"],
["no_html", "no_script"],
["nohtml", "noscript"], ["nohtml", "noscript"],
["sftpvv", "sftpv"], ["sftpvv", "sftpv"],
["smbw", "smb"], ["smbw", "smb"],
@ -490,13 +491,12 @@ font woff woff2 otf ttf
for v in vs.strip().split(): for v in vs.strip().split():
MIMES[v] = "{}/{}".format(k, v) MIMES[v] = "{}/{}".format(k, v)
for ln in """text md=plain txt=plain js=javascript for ln in """text md=plain js=javascript ass=plain ssa=plain txt=plain
application 7z=x-7z-compressed tar=x-tar bz2=x-bzip2 gz=gzip rar=x-rar-compressed zst=zstd xz=x-xz lz=lzip cpio=x-cpio application 7z=x-7z-compressed tar=x-tar bz2=x-bzip2 gz=gzip rar=x-rar-compressed zst=zstd xz=x-xz lz=lzip cpio=x-cpio
application msi=x-ms-installer cab=vnd.ms-cab-compressed rpm=x-rpm crx=x-chrome-extension application msi=x-ms-installer cab=vnd.ms-cab-compressed rpm=x-rpm crx=x-chrome-extension
application epub=epub+zip mobi=x-mobipocket-ebook lit=x-ms-reader rss=rss+xml atom=atom+xml torrent=x-bittorrent application epub=epub+zip mobi=x-mobipocket-ebook lit=x-ms-reader rss=rss+xml atom=atom+xml torrent=x-bittorrent
application p7s=pkcs7-signature dcm=dicom shx=vnd.shx shp=vnd.shp dbf=x-dbf gml=gml+xml gpx=gpx+xml amf=x-amf application p7s=pkcs7-signature dcm=dicom shx=vnd.shx shp=vnd.shp dbf=x-dbf gml=gml+xml gpx=gpx+xml amf=x-amf
application swf=x-shockwave-flash m3u=vnd.apple.mpegurl db3=vnd.sqlite3 sqlite=vnd.sqlite3 application swf=x-shockwave-flash m3u=vnd.apple.mpegurl db3=vnd.sqlite3 sqlite=vnd.sqlite3
text ass=plain ssa=plain
image jpg=jpeg xpm=x-xpixmap psd=vnd.adobe.photoshop jpf=jpx tif=tiff ico=x-icon djvu=vnd.djvu image jpg=jpeg xpm=x-xpixmap psd=vnd.adobe.photoshop jpf=jpx tif=tiff ico=x-icon djvu=vnd.djvu
image heics=heic-sequence heifs=heif-sequence hdr=vnd.radiance svg=svg+xml image heics=heic-sequence heifs=heif-sequence hdr=vnd.radiance svg=svg+xml
image arw=x-sony-arw cr2=x-canon-cr2 crw=x-canon-crw dcr=x-kodak-dcr dng=x-adobe-dng erf=x-epson-erf 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
@ -2396,6 +2396,33 @@ def exclude_dotfiles_ls(
return [x for x in vfs_ls if not x[0].split("/")[-1].startswith(".")] return [x for x in vfs_ls if not x[0].split("/")[-1].startswith(".")]
def exclude_dothidden(filepaths: list[str], fsroot: Any) -> list[str]:
ret = [x for x in filepaths if not x.split("/")[-1].startswith(".")]
filt = load_dothidden(fsroot)
if filt:
ret = [x for x in ret if x.split("/")[-1] not in filt]
return ret
def exclude_dothidden_ls(
vfs_ls: list[tuple[str, os.stat_result]], fsroot: Any
) -> list[tuple[str, os.stat_result]]:
ret = [x for x in vfs_ls if not x[0].split("/")[-1].startswith(".")]
filt = load_dothidden(fsroot)
if filt:
ret = [x for x in ret if x[0].split("/")[-1] not in filt]
return ret
def load_dothidden(dpath: str) -> list[str]:
try:
with open(os.path.join(dpath, ".hidden"), "rb") as f:
zsl = f.read().decode("utf-8").splitlines()
return [x.strip() for x in zsl]
except OSError:
return []
def odfusion( def odfusion(
base: Union[ODict[str, bool], ODict["LiteralString", bool]], oth: str base: Union[ODict[str, bool], ODict["LiteralString", bool]], oth: str
) -> ODict[str, bool]: ) -> ODict[str, bool]:

View file

@ -2,7 +2,7 @@
var J_BRW = 1; var J_BRW = 1;
if (window.rw_edit === undefined) if (window.dgauto === undefined)
alert('FATAL ERROR: receiving stale data from the server; this may be due to a broken reverse-proxy (stuck cache). Try restarting copyparty and press CTRL-SHIFT-R in the browser'); alert('FATAL ERROR: receiving stale data from the server; this may be due to a broken reverse-proxy (stuck cache). Try restarting copyparty and press CTRL-SHIFT-R in the browser');
var XHR = XMLHttpRequest; var XHR = XMLHttpRequest;
@ -228,6 +228,7 @@ if (1)
"cl_hpick": "tap on column headers to hide in the table below", "cl_hpick": "tap on column headers to hide in the table below",
"cl_hcancel": "column hiding aborted", "cl_hcancel": "column hiding aborted",
"cl_rcm": "right-click menu", "cl_rcm": "right-click menu",
"cl_gauto": "autogrid",
"ct_grid": '田 the grid', "ct_grid": '田 the grid',
"ct_ttips": '◔ ◡ ◔"> tooltips', "ct_ttips": '◔ ◡ ◔"> tooltips',
@ -282,6 +283,8 @@ if (1)
"tt_dynt": "autogrow as tree expands", "tt_dynt": "autogrow as tree expands",
"tt_wrap": "word wrap", "tt_wrap": "word wrap",
"tt_hover": "reveal overflowing lines on hover$N( breaks scrolling unless mouse $N&nbsp; cursor is in the left gutter )", "tt_hover": "reveal overflowing lines on hover$N( breaks scrolling unless mouse $N&nbsp; cursor is in the left gutter )",
"tt_gauto": "display as grid or list depending on folder contents",
"tt_gathr": "use grid if this percentage of files are pics/vids",
"ml_pmode": "at end of folder...", "ml_pmode": "at end of folder...",
"ml_btns": "cmds", "ml_btns": "cmds",
@ -713,6 +716,54 @@ var L = Ls[lang] || Ls.eng, LANGS = [];
for (var a = 0; a < LANGN.length; a++) for (var a = 0; a < LANGN.length; a++)
LANGS.push(LANGN[a][0]); LANGS.push(LANGN[a][0]);
if (window.glang && navigator.languages && !/\bcplng=/.test(document.cookie))
(function() {
var lmap = [
["eng", /^en/i],
["nor", /^n[ob]/i],
["chi", /^zh-cn/i],
["cze", /^cs/i],
["deu", /^de/i],
["epo", /^eo/i],
["fin", /^fi/i],
["fra", /^fr/i],
["grc", /^el/i],
["hun", /^hu/i],
["ita", /^it/i],
["jpn", /^ja/i],
["kor", /^ko/i],
["nld", /^nl/i],
["nno", /^nn/i],
["pol", /^pl/i],
["por", /^pt/i],
["rus", /^ru/i],
["spa", /^es/i],
["swe", /^sv/i],
["tur", /^tr/i],
["ukr", /^uk/i],
["vie", /^vi/i],
];
for (var a = 0; a < navigator.languages.length; a++) {
for (var b = 0; b < lmap.length; b++) {
var n = lmap[b][0];
if (!lmap[b][1].test(navigator.languages[a]) || !has(LANGS, n))
continue;
if (Ls[n]) {
lang = n;
L = Ls[n];
return;
}
if (window.stop)
window.stop();
document.body.innerHTML = 'Loading ' + n;
setck("cplng=" + n, location.reload.bind(location));
crashed = true;
throw 1;
}
}
})();
function langtest() { function langtest() {
var n = LANGS.length - 1; var n = LANGS.length - 1;
@ -721,7 +772,9 @@ function langtest() {
} }
function langtest2() { function langtest2() {
for (var a = 0; a < LANGS.length; a++) { for (var a = 0; a < LANGS.length; a++) {
if (!Ls[LANGS[a]]) continue;
for (var b = a + 1; b < LANGS.length; b++) { for (var b = a + 1; b < LANGS.length; b++) {
if (!Ls[LANGS[b]]) continue;
var i1 = Object.keys(Ls[LANGS[a]]).length > Object.keys(Ls[LANGS[b]]).length ? a : b, var i1 = Object.keys(Ls[LANGS[a]]).length > Object.keys(Ls[LANGS[b]]).length ? a : b,
i2 = i1 == a ? b : a, i2 = i1 == a ? b : a,
t1 = Ls[LANGS[i1]], t1 = Ls[LANGS[i1]],
@ -735,6 +788,7 @@ for (var a = 0; a < LANGS.length; a++) {
} }
} }
} }
langtest2();
@ -936,6 +990,13 @@ ebi('op_cfg').innerHTML = (
' </div>\n' + ' </div>\n' +
'</div>\n' + '</div>\n' +
'<div>\n' + '<div>\n' +
' <h3>' + L.cl_gauto + '</h3>\n' +
' <div>\n' +
' <a id="gauto" class="tgl btn" href="#" tt="' + L.tt_gauto + '">' + L.enable + '</a>\n' +
' <input type="text" id="ga_thresh" value="" ' + NOAC + ' style="width:1.5em" tt="' + L.tt_gathr + '" />' +
' </div>\n' +
'</div>\n' +
'<div>\n' +
' <h3>' + L.cl_hfsz + '</h3>\n' + ' <h3>' + L.cl_hfsz + '</h3>\n' +
' <div><select id="fszfmt">\n' + ' <div><select id="fszfmt">\n' +
' <option value="0">0 ┃ 1234567</option>\n' + ' <option value="0">0 ┃ 1234567</option>\n' +
@ -1155,9 +1216,7 @@ onresize100.add(read_sbw, true);
function check_image_support(format, uri) { function check_image_support(format, uri) {
var cached var cached = window['have_' + format] = sread('have_' + format);
= window['have_' + format]
= sread('have_' + format);
if (cached !== null) if (cached !== null)
return; return;
@ -1719,7 +1778,9 @@ function MPlayer() {
if (!tid || tid.indexOf('af-') !== 0) if (!tid || tid.indexOf('af-') !== 0)
continue; continue;
order.push(tid.slice(1)); tid = tid.slice(1);
if (r.tracks[tid])
order.push(tid);
} }
r.order = order; r.order = order;
r.shuffle(); r.shuffle();
@ -5348,8 +5409,8 @@ var showfile = (function () {
Prism.highlightElement(el); Prism.highlightElement(el);
} }
catch (ex) { } catch (ex) { }
btn.setAttribute('download', ebi('docname').innerHTML); btn.setAttribute('download', ebi('docname').innerHTML);
btn.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(jt)); btn.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(jt));
}; };
r.mktree = function () { r.mktree = function () {
@ -5557,6 +5618,16 @@ var thegrid = (function () {
r.setvis(); r.setvis();
}; };
r.autogrid = function (res) {
var ni = 0;
var nf = res.files.length;
for (var a = 0; a < nf; a++)
if (img_re.test('.' + res.files[a].ext))
ni++;
if (nf)
thegrid.en = 100 * ni / nf >= r.gathr;
};
function setln(v) { function setln(v) {
if (v) { if (v) {
r.ln += v; r.ln += v;
@ -5779,7 +5850,7 @@ var thegrid = (function () {
ihref = addq(ihref, 'th=' + ( ihref = addq(ihref, 'th=' + (
have_jxl ? 'x' : have_jxl ? 'x' :
have_webp ? 'w' : have_webp ? 'w' :
'j' 'j'
)); ));
if (!r.crop) if (!r.crop)
ihref += 'f'; ihref += 'f';
@ -5944,6 +6015,17 @@ var thegrid = (function () {
pbar.onresize(); pbar.onresize();
vbar.onresize(); vbar.onresize();
}); });
bcfg_bind(r, 'gaen', 'gauto', !!dgauto, function(v) {
if (r.en && sread("griden") != 1) {
r.en = false;
r.setvis(true);
}
});
ebi('ga_thresh').value = r.gathr = icfg_get('ga_thresh', dgauto || 70);
ebi('ga_thresh').oninput = function (e) {
var n = parseInt(this.value);
swrite('ga_thresh', r.gathr = (isNum(n) ? n : 0) || 70);
};
ebi('wtgrid').onclick = ebi('griden').onclick; ebi('wtgrid').onclick = ebi('griden').onclick;
return r; return r;
@ -7656,6 +7738,9 @@ var treectl = (function () {
} }
} }
if (thegrid.gaen && sread('griden') != 1)
thegrid.autogrid(res);
if (url) setTimeout(asdf, 1); else asdf(); if (url) setTimeout(asdf, 1); else asdf();
} }
@ -9626,7 +9711,7 @@ var rcm = (function () {
var row = mknod('tr', 'rcm_tmp', var row = mknod('tr', 'rcm_tmp',
'<td>-new-</td><td colspan="' + (QSA("#files thead th").length - 1) + '"><input id="tempname" class="i" type="text" placeholder="' + (is_dir ? 'Folder' : 'File') + ' Name"></td>'); '<td>-new-</td><td colspan="' + (QSA("#files thead th").length - 1) + '"><input id="tempname" class="i" type="text" placeholder="' + (is_dir ? 'Folder' : 'File') + ' Name"></td>');
QS("#files tbody").appendChild(row); QS("#files tbody").appendChild(row);
} }
else { else {
var row = mknod('a', 'rcm_tmp', var row = mknod('a', 'rcm_tmp',
'<span class="dir" style="align-self:end"><input id="tempname" class="dir" type="text" placeholder="' + (is_dir ? 'Folder' : 'File') + ' Name"></span>'); '<span class="dir" style="align-self:end"><input id="tempname" class="dir" type="text" placeholder="' + (is_dir ? 'Folder' : 'File') + ' Name"></span>');

View file

@ -65,13 +65,21 @@ o = ebi('u');
if (o && /[0-9]+$/.exec(o.innerHTML)) if (o && /[0-9]+$/.exec(o.innerHTML))
o.innerHTML = shumantime(o.innerHTML); o.innerHTML = shumantime(o.innerHTML);
o = ebi('uhash') o = ebi('uhash');
if (o) if (o)
o.value = '' + location.hash; o.value = '' + location.hash;
if (/\&re=/.test('' + location)) if (/\&re=/.test('' + location))
ebi('a').className = 'af g'; ebi('a').className = 'af g';
o = ebi('v');
if (o) o.onclick = function (e) {
if (e && e.shiftKey) {
ev(e);
document.location = '//youtu.be/8Ok5Sey1MoU';
}
};
(function() { (function() {
if (!ebi('x')) if (!ebi('x'))
return; return;

View file

@ -221,6 +221,7 @@ Ls.chi = {
"cl_hpick": "在下方文件列表中点击某列表头即可从表中隐去该列", "cl_hpick": "在下方文件列表中点击某列表头即可从表中隐去该列",
"cl_hcancel": "列隐藏操作已中止", "cl_hcancel": "列隐藏操作已中止",
"cl_rcm": "右键菜单", "cl_rcm": "右键菜单",
"cl_gauto": "自动网格", //m
"ct_grid": '田 网格', "ct_grid": '田 网格',
"ct_ttips": '◔ ◡ ◔"> 提示', "ct_ttips": '◔ ◡ ◔"> 提示',
@ -275,6 +276,8 @@ Ls.chi = {
"tt_dynt": "自动随着目录树展开而变宽", "tt_dynt": "自动随着目录树展开而变宽",
"tt_wrap": "自动换行", "tt_wrap": "自动换行",
"tt_hover": "悬停时完整显示出写不下的文字$N启用后鼠标光标只有$N&nbsp; 位于左边线上才滚得动)", "tt_hover": "悬停时完整显示出写不下的文字$N启用后鼠标光标只有$N&nbsp; 位于左边线上才滚得动)",
"tt_gauto": "根据文件夹内容以网格或列表显示", //m
"tt_gathr": "当此比例的文件为图片/视频时使用网格", //m
"ml_pmode": "文件夹播完后", "ml_pmode": "文件夹播完后",
"ml_btns": "命令", "ml_btns": "命令",

View file

@ -225,6 +225,7 @@ Ls.cze = {
"cl_hpick": "klepněte na záhlaví sloupců pro skrytí v tabulce níže", "cl_hpick": "klepněte na záhlaví sloupců pro skrytí v tabulce níže",
"cl_hcancel": "skrývání sloupců zrušeno", "cl_hcancel": "skrývání sloupců zrušeno",
"cl_rcm": "kontextová nabídka", //m "cl_rcm": "kontextová nabídka", //m
"cl_gauto": "auto mřížka", //m
"ct_grid": '田 mřížka', "ct_grid": '田 mřížka',
"ct_ttips": '◔ ◡ ◔"> nápovědy', "ct_ttips": '◔ ◡ ◔"> nápovědy',
@ -279,6 +280,8 @@ Ls.cze = {
"tt_dynt": "automaticky rozrůstat jak se strom rozšiřuje", "tt_dynt": "automaticky rozrůstat jak se strom rozšiřuje",
"tt_wrap": "zalomení řádků", "tt_wrap": "zalomení řádků",
"tt_hover": "odhalit přetékající řádky při najetí$N( ruší posun pokud kurzor myši $N&nbsp; není v levém okraji )", "tt_hover": "odhalit přetékající řádky při najetí$N( ruší posun pokud kurzor myši $N&nbsp; není v levém okraji )",
"tt_gauto": "zobrazit jako mřížku nebo seznam podle obsahu složky", //m
"tt_gathr": "použít mřížku, pokud toto procento souborů tvoří obrázky/videa", //m
"ml_pmode": "na konci složky...", "ml_pmode": "na konci složky...",
"ml_btns": "příkazy", "ml_btns": "příkazy",

View file

@ -221,6 +221,7 @@ Ls.deu = {
"cl_hpick": "zum Verstecken, tippe auf Spaltenüberschriften in der Tabelle unten", "cl_hpick": "zum Verstecken, tippe auf Spaltenüberschriften in der Tabelle unten",
"cl_hcancel": "Spaltenbearbeitung abgebrochen", "cl_hcancel": "Spaltenbearbeitung abgebrochen",
"cl_rcm": "Rechtsklick-Menü", "cl_rcm": "Rechtsklick-Menü",
"cl_gauto": "auto-raster", //m
"ct_grid": '田 Das Raster&trade;', "ct_grid": '田 Das Raster&trade;',
"ct_ttips": '◔ ◡ ◔"> Tooltips', "ct_ttips": '◔ ◡ ◔"> Tooltips',
@ -275,6 +276,8 @@ Ls.deu = {
"tt_dynt": "autom. wachsen wenn Baum wächst", "tt_dynt": "autom. wachsen wenn Baum wächst",
"tt_wrap": "Zeilenumbruch", "tt_wrap": "Zeilenumbruch",
"tt_hover": "Beim Hovern überlange Zeilen anzeigen$N(Scrollen funktioniert nicht ausser $N&nbsp; Cursor ist im linken Gutter)", "tt_hover": "Beim Hovern überlange Zeilen anzeigen$N(Scrollen funktioniert nicht ausser $N&nbsp; Cursor ist im linken Gutter)",
"tt_gauto": "je nach ordnerinhalt als raster oder liste anzeigen", //m
"tt_gathr": "raster verwenden, wenn dieser prozentsatz der dateien bilder/videos sind", //m
"ml_pmode": "am Ende des Ordners...", "ml_pmode": "am Ende des Ordners...",
"ml_btns": "cmds", "ml_btns": "cmds",

View file

@ -221,6 +221,7 @@ Ls.epo = {
"cl_hpick": "alklaki la kapojn de kolumnoj por kasi en la suban tabelon", "cl_hpick": "alklaki la kapojn de kolumnoj por kasi en la suban tabelon",
"cl_hcancel": "kaŝado de kolumno nuligita", "cl_hcancel": "kaŝado de kolumno nuligita",
"cl_rcm": "dekstra-klaka menuo", "cl_rcm": "dekstra-klaka menuo",
"cl_gauto": "aŭto田", //m
"ct_grid": '田 krado', "ct_grid": '田 krado',
"ct_ttips": '◔ ◡ ◔"> ŝpruchelpiloj', "ct_ttips": '◔ ◡ ◔"> ŝpruchelpiloj',
@ -275,6 +276,8 @@ Ls.epo = {
"tt_dynt": "aŭtomate pligrandigi panelon", "tt_dynt": "aŭtomate pligrandigi panelon",
"tt_wrap": "linifaldo", "tt_wrap": "linifaldo",
"tt_hover": "montri kompletajn nomojn sur musumo$N( paneas rulumadon, se la kursoro de muso $N&nbsp; ne estas en la maldekstra malplenaĵo )", "tt_hover": "montri kompletajn nomojn sur musumo$N( paneas rulumadon, se la kursoro de muso $N&nbsp; ne estas en la maldekstra malplenaĵo )",
"tt_gauto": "montri kiel krado aŭ listo laŭ dosieruja enhavo", //m
"tt_gathr": "uzi kradon se ĉi tiu procento de dosieroj estas bildoj/filmetoj", //m
"ml_pmode": "je la fino de dosierujo...", "ml_pmode": "je la fino de dosierujo...",
"ml_btns": "komandoj", "ml_btns": "komandoj",

View file

@ -221,6 +221,7 @@ Ls.fin = {
"cl_hpick": "napauta sarakeotsikoita piilottaaksesi alla olevassa taulukossa", "cl_hpick": "napauta sarakeotsikoita piilottaaksesi alla olevassa taulukossa",
"cl_hcancel": "sarakkeiden piilotus peruttu", "cl_hcancel": "sarakkeiden piilotus peruttu",
"cl_rcm": "hiiren pikavalikko", "cl_rcm": "hiiren pikavalikko",
"cl_gauto": "auto田", //m
"ct_grid": '田 kuvanäkymä', "ct_grid": '田 kuvanäkymä',
"ct_ttips": '◔ ◡ ◔"> vihjelaatikot', "ct_ttips": '◔ ◡ ◔"> vihjelaatikot',
@ -275,6 +276,8 @@ Ls.fin = {
"tt_dynt": "kasvata automaattisesti hakemistosyvyyden kasvaessa", "tt_dynt": "kasvata automaattisesti hakemistosyvyyden kasvaessa",
"tt_wrap": "rivitys", "tt_wrap": "rivitys",
"tt_hover": "paljasta ylivuotavat rivit leijutettaessa$N( rikkoo vierityksen ellei hiiri $N&nbsp; ole vasemmassa marginaalissa )", "tt_hover": "paljasta ylivuotavat rivit leijutettaessa$N( rikkoo vierityksen ellei hiiri $N&nbsp; ole vasemmassa marginaalissa )",
"tt_gauto": "näytä ruudukkona tai listana kansion sisällön mukaan", //m
"tt_gathr": "käytä ruudukkoa jos tämä prosentti tiedostoista on kuvia/videoita", //m
"ml_pmode": "hakemiston lopussa...", "ml_pmode": "hakemiston lopussa...",
"ml_btns": "komennot", "ml_btns": "komennot",

View file

@ -221,6 +221,7 @@ Ls.fra = {
"cl_hpick": "cliquez sur les en-têtes de colonnes pour les masquer dans le tableau ci-dessous", "cl_hpick": "cliquez sur les en-têtes de colonnes pour les masquer dans le tableau ci-dessous",
"cl_hcancel": "masquage des colonnes annulé", "cl_hcancel": "masquage des colonnes annulé",
"cl_rcm": "menu contextuel", //m "cl_rcm": "menu contextuel", //m
"cl_gauto": "auto-grille", //m
"ct_grid": '田 grille', "ct_grid": '田 grille',
"ct_ttips": '◔ ◡ ◔"> infobulles', "ct_ttips": '◔ ◡ ◔"> infobulles',
@ -275,6 +276,8 @@ Ls.fra = {
"tt_dynt": "croissance automatique à mesure que l'arborescence s'étend", "tt_dynt": "croissance automatique à mesure que l'arborescence s'étend",
"tt_wrap": "retour à la ligne", "tt_wrap": "retour à la ligne",
"tt_hover": "révéler les lignes débordantes au survol$N( interrompt le défilement à moins que le curseur de la souris ne soit dans la gouttière gauche )", "tt_hover": "révéler les lignes débordantes au survol$N( interrompt le défilement à moins que le curseur de la souris ne soit dans la gouttière gauche )",
"tt_gauto": "afficher en grille ou liste selon le contenu du dossier", //m
"tt_gathr": "utiliser la grille si ce pourcentage de fichiers sont des images/vidéos", //m
"ml_pmode": "à la fin du dossier…", "ml_pmode": "à la fin du dossier…",
"ml_btns": "cmds", "ml_btns": "cmds",

View file

@ -221,6 +221,7 @@ Ls.grc = {
"cl_hpick": "πάτησε στις κεφαλίδες στηλών για να τις κρύψεις στον πίνακα παρακάτω", "cl_hpick": "πάτησε στις κεφαλίδες στηλών για να τις κρύψεις στον πίνακα παρακάτω",
"cl_hcancel": "η απόκρυψη στηλών ακυρώθηκε", "cl_hcancel": "η απόκρυψη στηλών ακυρώθηκε",
"cl_rcm": "μενού δεξιού κλικ", //m "cl_rcm": "μενού δεξιού κλικ", //m
"cl_gauto": "αυτόματο田", //m
"ct_grid": '田 το πλέγμα', "ct_grid": '田 το πλέγμα',
"ct_ttips": '◔ ◡ ◔"> συμβουλές εργαλείων', "ct_ttips": '◔ ◡ ◔"> συμβουλές εργαλείων',
@ -275,6 +276,8 @@ Ls.grc = {
"tt_dynt": "αυτόματη επέκταση καθώς επεκτείνεται το δέντρο διαδρομών", "tt_dynt": "αυτόματη επέκταση καθώς επεκτείνεται το δέντρο διαδρομών",
"tt_wrap": "αναδίπλωση λέξεων", "tt_wrap": "αναδίπλωση λέξεων",
"tt_hover": "αποκάλυψη των γραμμών που ξεπερνούν το πλάτος με το ποντίκι πάνω τους$N( σπάει το scroll εκτός αν το ποντίκι $N&nbsp; είναι στην αριστερή στήλη )", "tt_hover": "αποκάλυψη των γραμμών που ξεπερνούν το πλάτος με το ποντίκι πάνω τους$N( σπάει το scroll εκτός αν το ποντίκι $N&nbsp; είναι στην αριστερή στήλη )",
"tt_gauto": "εμφάνιση ως πλέγμα ή λίστα ανάλογα με τα περιεχόμενα του φακέλου", //m
"tt_gathr": "χρήση πλέγματος αν αυτό το ποσοστό αρχείων είναι εικόνες/βίντεο", //m
"ml_pmode": "στο τέλος του φακέλου...", "ml_pmode": "στο τέλος του φακέλου...",
"ml_btns": "εντολές", "ml_btns": "εντολές",

View file

@ -222,6 +222,7 @@ Ls.hun = {
"cl_hpick": 'kattints az oszlopfejlécre az elrejtéshez', "cl_hpick": 'kattints az oszlopfejlécre az elrejtéshez',
"cl_hcancel": 'elrejtés megszakítva', "cl_hcancel": 'elrejtés megszakítva',
"cl_rcm": 'jobb-klikkes menü', "cl_rcm": 'jobb-klikkes menü',
"cl_gauto": "auto田", //m
"ct_grid": '田 rács nézet', "ct_grid": '田 rács nézet',
"ct_ttips": '◔ ◡ ◔"> segítő szövegek', "ct_ttips": '◔ ◡ ◔"> segítő szövegek',
@ -276,6 +277,8 @@ Ls.hun = {
"tt_dynt": 'automatikus méretezés nyitáskor', "tt_dynt": 'automatikus méretezés nyitáskor',
"tt_wrap": 'sortörés', "tt_wrap": 'sortörés',
"tt_hover": 'túl hosszú sorok mutatása rámutatáskor', "tt_hover": 'túl hosszú sorok mutatása rámutatáskor',
"tt_gauto": "megjelenítés rácsban vagy listában a mappa tartalmától függően", //m
"tt_gathr": "rács használata, ha a fájlok ezen százaléka kép/videó", //m
"ml_pmode": 'mappa végén...', "ml_pmode": 'mappa végén...',
"ml_btns": 'gombok', "ml_btns": 'gombok',

View file

@ -221,6 +221,7 @@ Ls.ita = {
"cl_hpick": "tocca le intestazioni delle colonne per nascondere nella tabella sottostante", "cl_hpick": "tocca le intestazioni delle colonne per nascondere nella tabella sottostante",
"cl_hcancel": "nascondere colonne annullato", "cl_hcancel": "nascondere colonne annullato",
"cl_rcm": "menu contestuale", //m "cl_rcm": "menu contestuale", //m
"cl_gauto": "auto田", //m
"ct_grid": '田 griglia', "ct_grid": '田 griglia',
"ct_ttips": '◔ ◡ ◔"> tooltip', "ct_ttips": '◔ ◡ ◔"> tooltip',
@ -275,6 +276,8 @@ Ls.ita = {
"tt_dynt": "crescita automatica mentre l'albero si espande", "tt_dynt": "crescita automatica mentre l'albero si espande",
"tt_wrap": "a capo parola", "tt_wrap": "a capo parola",
"tt_hover": "rivela righe che traboccano al passaggio del mouse$N( interrompe lo scorrimento a meno che il cursore $N&nbsp; del mouse non sia nella grondaia sinistra )", "tt_hover": "rivela righe che traboccano al passaggio del mouse$N( interrompe lo scorrimento a meno che il cursore $N&nbsp; del mouse non sia nella grondaia sinistra )",
"tt_gauto": "mostra come griglia o lista in base al contenuto della cartella", //m
"tt_gathr": "usa la griglia se questa percentuale di file sono immagini/video", //m
"ml_pmode": "alla fine della cartella...", "ml_pmode": "alla fine della cartella...",
"ml_btns": "comandi", "ml_btns": "comandi",

View file

@ -221,6 +221,7 @@ Ls.jpn = {
"cl_hpick": "下の表で非表示にするには列ヘッダーをタップします", "cl_hpick": "下の表で非表示にするには列ヘッダーをタップします",
"cl_hcancel": "列の非表示を解除", "cl_hcancel": "列の非表示を解除",
"cl_rcm": "右クリックメニュー", "cl_rcm": "右クリックメニュー",
"cl_gauto": "自動グリッド", //m
"ct_grid": '田 グリッド', "ct_grid": '田 グリッド',
"ct_ttips": '◔ ◡ ◔"> ツールチップ', "ct_ttips": '◔ ◡ ◔"> ツールチップ',
@ -275,6 +276,8 @@ Ls.jpn = {
"tt_dynt": "ツリーが拡大するにつれて自動的に増加", "tt_dynt": "ツリーが拡大するにつれて自動的に増加",
"tt_wrap": "単語の折り返し", "tt_wrap": "単語の折り返し",
"tt_hover": "ホバーすると溢れた線を表示する$N( マウスを押さない限りスクロールが中断されます $N&nbsp; カーソルは左余白です )", "tt_hover": "ホバーすると溢れた線を表示する$N( マウスを押さない限りスクロールが中断されます $N&nbsp; カーソルは左余白です )",
"tt_gauto": "フォルダー内容に応じてグリッドまたはリスト表示", //m
"tt_gathr": "この割合のファイルが画像/動画ならグリッドを使用", //m
"ml_pmode": "フォルダの末尾...", "ml_pmode": "フォルダの末尾...",
"ml_btns": "コマンド", "ml_btns": "コマンド",

View file

@ -221,6 +221,7 @@ Ls.kor = {
"cl_hpick": "아래 테이블에서 숨기고 싶은 열의 헤더를 탭하세요", "cl_hpick": "아래 테이블에서 숨기고 싶은 열의 헤더를 탭하세요",
"cl_hcancel": "열 숨기기가 중단되었습니다", "cl_hcancel": "열 숨기기가 중단되었습니다",
"cl_rcm": "우클릭 메뉴", //m "cl_rcm": "우클릭 메뉴", //m
"cl_gauto": "자동 田", //m
"ct_grid": "田 그리드", "ct_grid": "田 그리드",
"ct_ttips": '◔ ◡ ◔"> 도움말', "ct_ttips": '◔ ◡ ◔"> 도움말',
@ -275,6 +276,8 @@ Ls.kor = {
"tt_dynt": "트리가 확장될 때 자동으로 너비 증가", "tt_dynt": "트리가 확장될 때 자동으로 너비 증가",
"tt_wrap": "자동 줄 바꿈", "tt_wrap": "자동 줄 바꿈",
"tt_hover": "마우스를 올리면 넘어가는 줄 표시$N(마우스 커서가 왼쪽 여백에$N&nbsp; 있지 않으면 스크롤이 깨짐)", "tt_hover": "마우스를 올리면 넘어가는 줄 표시$N(마우스 커서가 왼쪽 여백에$N&nbsp; 있지 않으면 스크롤이 깨짐)",
"tt_gauto": "폴더 내용에 따라 그리드 또는 목록으로 표시", //m
"tt_gathr": "파일 중 이 비율이 이미지/동영상이면 그리드 사용", //m
"ml_pmode": "폴더 끝에서...", "ml_pmode": "폴더 끝에서...",
"ml_btns": "명령", "ml_btns": "명령",

View file

@ -221,6 +221,7 @@ Ls.nld = {
"cl_hpick": "Tik op de kolomkoppen om ze in de onderstaande tabel te verbergen", "cl_hpick": "Tik op de kolomkoppen om ze in de onderstaande tabel te verbergen",
"cl_hcancel": "Kolumn verbergen geannuleerd", "cl_hcancel": "Kolumn verbergen geannuleerd",
"cl_rcm": "Rechtermuisknopmenu", //m "cl_rcm": "Rechtermuisknopmenu", //m
"cl_gauto": "auto田", //m
"ct_grid": '田 grid', "ct_grid": '田 grid',
"ct_ttips": '◔ ◡ ◔"> tooltips', "ct_ttips": '◔ ◡ ◔"> tooltips',
@ -275,6 +276,8 @@ Ls.nld = {
"tt_dynt": "Automatisch groeien naarmate de directoryboom zich uitbreidt", "tt_dynt": "Automatisch groeien naarmate de directoryboom zich uitbreidt",
"tt_wrap": "Automatische terugloop", "tt_wrap": "Automatische terugloop",
"tt_hover": "Laat overlopenden lijnen zien bij zweven$N(stopt het scrollen tenzij de muis in de linker gedeelte van het scherm is)", "tt_hover": "Laat overlopenden lijnen zien bij zweven$N(stopt het scrollen tenzij de muis in de linker gedeelte van het scherm is)",
"tt_gauto": "weergeven als grid of lijst afhankelijk van mapinhoud", //m
"tt_gathr": "gebruik grid als dit percentage bestanden afbeeldingen/video's zijn", //m
"ml_pmode": "Aan het einde van de map...", "ml_pmode": "Aan het einde van de map...",
"ml_btns": "Cmds", "ml_btns": "Cmds",

View file

@ -218,6 +218,7 @@ Ls.nno = {
"cl_hpick": "klikk på overskrifta åt kolonnene du ønskjer å skjule i tabellen nedanfor", "cl_hpick": "klikk på overskrifta åt kolonnene du ønskjer å skjule i tabellen nedanfor",
"cl_hcancel": "kolonne-skjuling avbrote", "cl_hcancel": "kolonne-skjuling avbrote",
"cl_rcm": "høgreklikkmeny", "cl_rcm": "høgreklikkmeny",
"cl_gauto": "auto田",
"ct_grid": '田 ikon', "ct_grid": '田 ikon',
"ct_ttips": 'vis hjelpetekst ved å holde musa over ting"> tips', "ct_ttips": 'vis hjelpetekst ved å holde musa over ting"> tips',
@ -272,6 +273,8 @@ Ls.nno = {
"tt_dynt": "øk bredda på panelet ettersom treet utvider seg", "tt_dynt": "øk bredda på panelet ettersom treet utvider seg",
"tt_wrap": "linjebryting", "tt_wrap": "linjebryting",
"tt_hover": "vis heile mappenamnet når musepeikaren treff mappa$N( gjer diverre at scrollhjulet fusker dersom musepeikaren ikkje finn seg i grøfta )", "tt_hover": "vis heile mappenamnet når musepeikaren treff mappa$N( gjer diverre at scrollhjulet fusker dersom musepeikaren ikkje finn seg i grøfta )",
"tt_gauto": "byt visingsmodus (liste/ikon) avhengig av mappeinnhald",
"tt_gathr": "vis som ikon når denne prosentdelen er bilete/videoar",
"ml_pmode": "ved enden av mappa", "ml_pmode": "ved enden av mappa",
"ml_btns": "knapper", "ml_btns": "knapper",

View file

@ -218,6 +218,7 @@ Ls.nor = {
"cl_hpick": "klikk på overskriften til kolonnene du ønsker å skjule i tabellen nedenfor", "cl_hpick": "klikk på overskriften til kolonnene du ønsker å skjule i tabellen nedenfor",
"cl_hcancel": "kolonne-skjuling avbrutt", "cl_hcancel": "kolonne-skjuling avbrutt",
"cl_rcm": "høyreklikkmeny", "cl_rcm": "høyreklikkmeny",
"cl_gauto": "auto田",
"ct_grid": '田 ikoner', "ct_grid": '田 ikoner',
"ct_ttips": 'vis hjelpetekst ved å holde musen over ting"> tips', "ct_ttips": 'vis hjelpetekst ved å holde musen over ting"> tips',
@ -272,6 +273,8 @@ Ls.nor = {
"tt_dynt": "øk bredden på panelet ettersom treet utvider seg", "tt_dynt": "øk bredden på panelet ettersom treet utvider seg",
"tt_wrap": "linjebryting", "tt_wrap": "linjebryting",
"tt_hover": "vis hele mappenavnet når musepekeren treffer mappen$N( gjør dessverre at scrollhjulet fusker dersom musepekeren ikke befinner seg i grøfta )", "tt_hover": "vis hele mappenavnet når musepekeren treffer mappen$N( gjør dessverre at scrollhjulet fusker dersom musepekeren ikke befinner seg i grøfta )",
"tt_gauto": "bytt visningsmodus (liste/ikoner) avhengig av mappeinnhold",
"tt_gathr": "vis som ikoner når denne prosentandelen er bilder/videoer",
"ml_pmode": "ved enden av mappen", "ml_pmode": "ved enden av mappen",
"ml_btns": "knapper", "ml_btns": "knapper",

View file

@ -224,6 +224,7 @@ Ls.pol = {
"cl_hpick": "kliknij nagłówki kolumn, aby ukryć je w tabeli niżej", "cl_hpick": "kliknij nagłówki kolumn, aby ukryć je w tabeli niżej",
"cl_hcancel": "ukrywanie kolumn przerwane", "cl_hcancel": "ukrywanie kolumn przerwane",
"cl_rcm": "menu kontekstowe", //m "cl_rcm": "menu kontekstowe", //m
"cl_gauto": "auto田", //m
"ct_grid": '田 siatka', "ct_grid": '田 siatka',
"ct_ttips": '◔ ◡ ◔"> podpowiedzi', "ct_ttips": '◔ ◡ ◔"> podpowiedzi',
@ -278,6 +279,8 @@ Ls.pol = {
"tt_dynt": "rozszerzaj panel wraz z drzewem", "tt_dynt": "rozszerzaj panel wraz z drzewem",
"tt_wrap": "zawijaj tekst", "tt_wrap": "zawijaj tekst",
"tt_hover": "pokazuj za długie linie po najechaniu kursorem$N( psuje przewijanie gdy $N&nbsp; kursor nie jest w lewym marginesie )", "tt_hover": "pokazuj za długie linie po najechaniu kursorem$N( psuje przewijanie gdy $N&nbsp; kursor nie jest w lewym marginesie )",
"tt_gauto": "wyświetl jako siatkę lub listę w zależności od zawartości folderu", //m
"tt_gathr": "użyj siatki, jeśli ten procent plików to obrazy/wideo", //m
"ml_pmode": "na końcu folderu...", "ml_pmode": "na końcu folderu...",
"ml_btns": "komendy", "ml_btns": "komendy",

View file

@ -221,6 +221,7 @@ Ls.por = {
"cl_hpick": "toque nos cabeçalhos das colunas para ocultá-los na tabela abaixo", "cl_hpick": "toque nos cabeçalhos das colunas para ocultá-los na tabela abaixo",
"cl_hcancel": "ocultar coluna abortado", "cl_hcancel": "ocultar coluna abortado",
"cl_rcm": "menu de clique direito", "cl_rcm": "menu de clique direito",
"cl_gauto": "auto田", //m
"ct_grid": '田 a grade', "ct_grid": '田 a grade',
"ct_ttips": '◔ ◡ ◔"> dicas de ferramentas', "ct_ttips": '◔ ◡ ◔"> dicas de ferramentas',
@ -275,6 +276,8 @@ Ls.por = {
"tt_dynt": "crescer automaticamente à medida que a árvore se expande", "tt_dynt": "crescer automaticamente à medida que a árvore se expande",
"tt_wrap": "quebra de linha", "tt_wrap": "quebra de linha",
"tt_hover": "revelar linhas transbordando ao passar o mouse$N( quebra a rolagem a menos que o cursor do mouse $N&nbsp; esteja na margem esquerda )", "tt_hover": "revelar linhas transbordando ao passar o mouse$N( quebra a rolagem a menos que o cursor do mouse $N&nbsp; esteja na margem esquerda )",
"tt_gauto": "exibir como grade ou lista dependendo do conteúdo da pasta", //m
"tt_gathr": "usar grade se esta porcentagem de arquivos for imagens/vídeos", //m
"ml_pmode": "ao final da pasta...", "ml_pmode": "ao final da pasta...",
"ml_btns": "comandos", "ml_btns": "comandos",

View file

@ -221,6 +221,7 @@ Ls.rus = {
"cl_hpick": "нажмите на заголовки столбцов, чтобы скрыть их в таблице ниже", "cl_hpick": "нажмите на заголовки столбцов, чтобы скрыть их в таблице ниже",
"cl_hcancel": "скрытие столбца отменено", "cl_hcancel": "скрытие столбца отменено",
"cl_rcm": "контекстное меню", //m "cl_rcm": "контекстное меню", //m
"cl_gauto": "авто田", //m
"ct_grid": '田 сетка', "ct_grid": '田 сетка',
"ct_ttips": '◔ ◡ ◔"> подсказки', "ct_ttips": '◔ ◡ ◔"> подсказки',
@ -275,6 +276,8 @@ Ls.rus = {
"tt_dynt": "автоматическое расширение панели", "tt_dynt": "автоматическое расширение панели",
"tt_wrap": "перенос слов", "tt_wrap": "перенос слов",
"tt_hover": "раскрывать обрезанные строки при наведении$N( ломает скроллинг, если $N&nbsp; курсор не в пустоте слева )", "tt_hover": "раскрывать обрезанные строки при наведении$N( ломает скроллинг, если $N&nbsp; курсор не в пустоте слева )",
"tt_gauto": "показывать как сетку или список в зависимости от содержимого папки", //m
"tt_gathr": "использовать сетку, если этот процент файлов — изображения/видео", //m
"ml_pmode": "в конце папки...", "ml_pmode": "в конце папки...",
"ml_btns": "команды", "ml_btns": "команды",

View file

@ -220,6 +220,7 @@ Ls.spa = {
"cl_hpick": "toca en las cabeceras de columna para ocultarlas en la tabla de abajo", "cl_hpick": "toca en las cabeceras de columna para ocultarlas en la tabla de abajo",
"cl_hcancel": "ocultación de columna cancelada", "cl_hcancel": "ocultación de columna cancelada",
"cl_rcm": "menú contextual", //m "cl_rcm": "menú contextual", //m
"cl_gauto": "auto田", //m
"ct_grid": '田 cuadrícula', "ct_grid": '田 cuadrícula',
"ct_ttips": '◔ ◡ ◔"> tooltips', "ct_ttips": '◔ ◡ ◔"> tooltips',
@ -274,6 +275,8 @@ Ls.spa = {
"tt_dynt": "crecimiento automático a medida que el árbol se expande", "tt_dynt": "crecimiento automático a medida que el árbol se expande",
"tt_wrap": "ajuste de línea", "tt_wrap": "ajuste de línea",
"tt_hover": "revelar líneas que se desbordan al pasar el ratón$N( rompe el desplazamiento a menos que el $N&nbsp; cursor esté en el margen izquierdo )", "tt_hover": "revelar líneas que se desbordan al pasar el ratón$N( rompe el desplazamiento a menos que el $N&nbsp; cursor esté en el margen izquierdo )",
"tt_gauto": "mostrar como cuadrícula o lista según el contenido de la carpeta", //m
"tt_gathr": "usar cuadrícula si este porcentaje de archivos son imágenes/videos", //m
"ml_pmode": "al final de la carpeta...", "ml_pmode": "al final de la carpeta...",
"ml_btns": "acciones", "ml_btns": "acciones",

View file

@ -221,6 +221,7 @@ Ls.swe = {
"cl_hpick": "tryck på en kolumntitel för att dölja den i filvyn", "cl_hpick": "tryck på en kolumntitel för att dölja den i filvyn",
"cl_hcancel": "kolumndöljning avbruten", "cl_hcancel": "kolumndöljning avbruten",
"cl_rcm": "högerklicksmeny", //m "cl_rcm": "högerklicksmeny", //m
"cl_gauto": "auto田", //m
"ct_grid": '田 rutnätet', "ct_grid": '田 rutnätet',
"ct_ttips": '◔ ◡ ◔"> tips', "ct_ttips": '◔ ◡ ◔"> tips',
@ -275,6 +276,8 @@ Ls.swe = {
"tt_dynt": "väx vyn när trädet expanderar", "tt_dynt": "väx vyn när trädet expanderar",
"tt_wrap": "automatisk radbrytning", "tt_wrap": "automatisk radbrytning",
"tt_hover": "visa överlånga rader när muspekaren hovrar över dem$N( skrollhjulet fungerar ej såvida inte pekaren$Nstår till vänster )", "tt_hover": "visa överlånga rader när muspekaren hovrar över dem$N( skrollhjulet fungerar ej såvida inte pekaren$Nstår till vänster )",
"tt_gauto": "visa som rutnät eller lista beroende på mappens innehåll", //m
"tt_gathr": "använd rutnät om denna andel filer är bilder/videor", //m
"ml_pmode": "vid mappens slut...", "ml_pmode": "vid mappens slut...",
"ml_btns": "komm.", "ml_btns": "komm.",

View file

@ -221,6 +221,7 @@ Ls.tur = {
"cl_hpick": "aşağıdaki tabloda gizlemek için sütun başlıklarına dokunun", "cl_hpick": "aşağıdaki tabloda gizlemek için sütun başlıklarına dokunun",
"cl_hcancel": "sütun gizleme iptal edildi", "cl_hcancel": "sütun gizleme iptal edildi",
"cl_rcm": "sağ tık menüsü", //m "cl_rcm": "sağ tık menüsü", //m
"cl_gauto": "otomatik田", //m
"ct_grid": '田 ızgara', "ct_grid": '田 ızgara',
"ct_ttips": '◔ ◡ ◔"> ipuçları', "ct_ttips": '◔ ◡ ◔"> ipuçları',
@ -275,6 +276,8 @@ Ls.tur = {
"tt_dynt": "ağaç genişledikçe otomatik büyüt", "tt_dynt": "ağaç genişledikçe otomatik büyüt",
"tt_wrap": "kelime sarma", "tt_wrap": "kelime sarma",
"tt_hover": "fare ile üzerine gelindiğinde taşan satırları göster$N( fare imleci sol kenarda değilse kaydırmayı bozar )", "tt_hover": "fare ile üzerine gelindiğinde taşan satırları göster$N( fare imleci sol kenarda değilse kaydırmayı bozar )",
"tt_gauto": "klasör içeriğine bağlı olarak ızgara veya liste olarak göster", //m
"tt_gathr": "dosyaların bu yüzdesi resim/video ise ızgara kullan", //m
"ml_pmode": "klasör sonunda...", "ml_pmode": "klasör sonunda...",
"ml_btns": "komutlar", "ml_btns": "komutlar",

View file

@ -221,6 +221,7 @@ Ls.ukr = {
"cl_hpick": "натисніть на заголовки стовпців, щоб приховати їх у таблиці нижче", "cl_hpick": "натисніть на заголовки стовпців, щоб приховати їх у таблиці нижче",
"cl_hcancel": "приховання стовпців скасовано", "cl_hcancel": "приховання стовпців скасовано",
"cl_rcm": "контекстне меню", //m "cl_rcm": "контекстне меню", //m
"cl_gauto": "авто田", //m
"ct_grid": '田 сітка', "ct_grid": '田 сітка',
"ct_ttips": '◔ ◡ ◔"> підказки', "ct_ttips": '◔ ◡ ◔"> підказки',
@ -275,6 +276,8 @@ Ls.ukr = {
"tt_dynt": "автоматично збільшуватися при розширенні дерева", "tt_dynt": "автоматично збільшуватися при розширенні дерева",
"tt_wrap": "перенесення слів", "tt_wrap": "перенесення слів",
"tt_hover": "показувати переповнені рядки при наведенні$N( порушує прокрутку, якщо курсор $N&nbsp; миші не знаходиться в лівому відступі )", "tt_hover": "показувати переповнені рядки при наведенні$N( порушує прокрутку, якщо курсор $N&nbsp; миші не знаходиться в лівому відступі )",
"tt_gauto": "показувати як сітку або список залежно від вмісту папки", //m
"tt_gathr": "використовувати сітку, якщо цей відсоток файлів — зображення/відео", //m
"ml_pmode": "в кінці папки...", "ml_pmode": "в кінці папки...",
"ml_btns": "команди", "ml_btns": "команди",

View file

@ -221,6 +221,7 @@ Ls.vie = {
"cl_hpick": "chạm vào tiêu đề cột để ẩn trong bảng bên dưới", "cl_hpick": "chạm vào tiêu đề cột để ẩn trong bảng bên dưới",
"cl_hcancel": "đã hủy việc ẩn cột", "cl_hcancel": "đã hủy việc ẩn cột",
"cl_rcm": "menu chuột phải", //m "cl_rcm": "menu chuột phải", //m
"cl_gauto": "lưới tự động", //m
// settings / tuỳ chọn // settings / tuỳ chọn
"ct_grid": '田 chế độ lưới', "ct_grid": '田 chế độ lưới',
@ -279,6 +280,8 @@ Ls.vie = {
"tt_dynt": "tự mở rộng khi cây mở rộng", "tt_dynt": "tự mở rộng khi cây mở rộng",
"tt_wrap": "ngắt dòng", "tt_wrap": "ngắt dòng",
"tt_hover": "hiện thị dòng tràn khi rê chuột$N( không cuộn được nếu $N&nbsp; con trỏ chuột nằm ngoài cột trái )", "tt_hover": "hiện thị dòng tràn khi rê chuột$N( không cuộn được nếu $N&nbsp; con trỏ chuột nằm ngoài cột trái )",
"tt_gauto": "hiển thị dạng lưới hoặc danh sách tùy theo nội dung thư mục", //m
"tt_gathr": "dùng lưới nếu tỷ lệ tệp này là ảnh/video", //m
"ml_pmode": "ở cuối thư mục...", "ml_pmode": "ở cuối thư mục...",
"ml_btns": "lệnh", "ml_btns": "lệnh",

View file

@ -322,6 +322,7 @@ html.y #tth {
margin: .1em auto; margin: .1em auto;
width: 60%; width: 60%;
height: 60%; height: 60%;
max-height: 22em;
background: #999; background: #999;
background: rgba(128,128,128,0.2); background: rgba(128,128,128,0.2);
} }

View file

@ -359,6 +359,48 @@ for the `re`pack to work, first run one of the sfx'es once to unpack it
**note:** you can also just download and run [/scripts/copyparty-repack.sh](https://github.com/9001/copyparty/blob/hovudstraum/scripts/copyparty-repack.sh) -- this will grab the latest copyparty release from github and do a few repacks; works on linux/macos (and windows with msys2 or WSL) **note:** you can also just download and run [/scripts/copyparty-repack.sh](https://github.com/9001/copyparty/blob/hovudstraum/scripts/copyparty-repack.sh) -- this will grab the latest copyparty release from github and do a few repacks; works on linux/macos (and windows with msys2 or WSL)
# dependencies
## vendored dependencies
some third-party code has been vendored into the git repo; some for convenience, some because they have been lightly hacked to fit copyparty's usecase better:
* inside the folder [/copyparty/stolen](https://github.com/9001/copyparty/tree/hovudstraum/copyparty/stolen) is python-libraries which runs on the serverside:
* `surrogateescape.py` (BSD2) can be removed; only needed for python2 support
* `qrcodegen.py` (MIT) can be removed and replaced with a systemwide install of the original [qrcodegen.py](https://github.com/nayuki/QR-Code-generator/blob/daa3114/python/qrcodegen.py);
* modifications: removed code/features that copyparty does not need/use
* `ifaddr` (BSD2) can be removed and replaced with a systemwide install of the original [ifaddr](https://github.com/ifaddr/ifaddr);
* modifications: support python2, support s390x / irix32 / graal
* `dnslib` (MIT) may be deleted and replaced with a systemwide install of the original [dnslib](https://github.com/paulc/dnslib/), HOWEVER:
* will cause problems for mDNS in some network environments; 6c1cf68bca7376c6291c3cfe710ebd5bd5ed3e6c + 94d1924fa97e5faaf1ebfd85cae73faebcb89fa1
* inside the folder `/copyparty/web/deps` (only in distributed archives/builds) is [fuse.py](https://github.com/fusepy/fusepy/blob/master/fuse.py), to make it downloadable from the connect-page on the web-ui
* inside the folder `/copyparty/web` (only in distributed archives/builds) is a collection of javascript libraries (produced by [deps-docker](https://github.com/9001/copyparty/tree/hovudstraum/scripts/deps-docker)) which are used clientside by the web-UI:
* [marked.js](https://github.com/markedjs/marked/releases) (MIT) powers the markdown editor, and has been [patched](https://github.com/9001/copyparty/blob/hovudstraum/scripts/deps-docker/marked-ln.patch) to include the line-numbers of each input line, to enable scroll-sync between the editor and the preview-pane. This patch is [not strictly necessary anymore](https://github.com/markedjs/marked/issues/2134) but I haven't gotten around to making the change yet
* [easyMDE](https://github.com/Ionaru/easy-markdown-editor/) (MIT), the alternative markdown editor, has the same [patch](https://github.com/9001/copyparty/blob/hovudstraum/scripts/deps-docker/easymde-ln.patch) to enable scroll-sync, and also some [size-golfing](https://github.com/9001/copyparty/blob/hovudstraum/scripts/deps-docker/easymde.patch)
* [codemirror5](https://github.com/codemirror/codemirror5/) (MIT) has no noteworthy changes, and has only been [size-golfed](https://github.com/9001/copyparty/blob/hovudstraum/scripts/deps-docker/codemirror.patch), could have been used as-is
* [DOMPurify](https://github.com/cure53/DOMPurify) (Apache2) is used as-is
* [hash-wasm](https://github.com/Daninet/hash-wasm/) (MIT) is used entirely as-is
* [asmcrypto.js](https://github.com/openpgpjs/asmcrypto.js/) (MIT) is abandoned software, and used almost as-is (slightly golfed for size); it is probably fine to exclude/remove this, since it will only break support for uploading from really old browsers (IE10/IE11) using up2k (the "fancy uploader")
* [prism.js](https://github.com/PrismJS/prism/) (MIT) is built with a [selection of languages](https://github.com/9001/copyparty/blob/hovudstraum/scripts/deps-docker/genprism.py); there is an assumption about the exact subset of languages elsewhere in copyparty, but there shouldn't be any big consequences of replacing it with a different build if that exists in Fedora
* an old version of [SourceCodePro](https://github.com/adobe-fonts/source-code-pro) (OFL-1.1), is size-reduced to [only the necessary characters](https://github.com/9001/copyparty/blob/41ed559faabdc180efc37fd027e7f1bb2d14d174/scripts/deps-docker/mini-fa.sh#L30-L31). There will be subtle layout issues if this is replaced with a newer version, because they changed some line-heights or something in later versions, but shouldn't be a big issue
* an old version of [font-awesome](https://github.com/FortAwesome/Font-Awesome) (OFL-1.1), size-reduced to [only the necessary icons](https://github.com/9001/copyparty/blob/hovudstraum/scripts/deps-docker/mini-fa.sh). I believe a newer version should also work.
## optional dependencies
explained in the [main readme](https://github.com/9001/copyparty/tree/hovudstraum#optional-dependencies), but a quick recap:
* recommended python libraries: `argon2-cffi paramiko pyftpdlib pyopenssl pillow rawpy pyzmq` [python-magic](https://pypi.org/project/python-magic/)
* only recommended on Windows: `psutil` (not very useful on Linux)
* NOT recommended: `impacket` because the feature it enables is a security nightmare
* NOT recommended: `mutagen` because ffmpeg produces better results (albeit slower)
* NOT recommended: `pyvips` because converting to jxl is extremely RAM-heavy
* NOT recommended: `pillow-heif` due to [legal reasons](https://github.com/9001/copyparty/blob/hovudstraum/docs/bad-codecs.md)
* recommended programs: `ffmpeg ffprobe cfssl cfssljson cfssl-certinfo`
* FFmpeg powers audio transcoding, and thumbnails of formats not covered by pillow/pyvips
# building # building
## dev env setup ## dev env setup

View file

@ -38,3 +38,5 @@
accs: accs:
rw: * # everyone gets read-write access, but rw: * # everyone gets read-write access, but
rwmda: ed # the user "ed" gets read-write-move-delete-admin rwmda: ed # the user "ed" gets read-write-move-delete-admin
flags:
e2ds # enable filesystem-scanning for this volume only

View file

@ -86,33 +86,33 @@ the table headers in the matrixes below are the different softwares, with a quic
the softwares, the softwares,
[a]: https://github.com/9001/copyparty "copyparty" [C]: https://github.com/9001/copyparty "copyparty"
[b]: https://github.com/rejetto/hfs2/ "hfs2" [h2]: https://github.com/rejetto/hfs2/ "hfs2"
[c]: https://rejetto.com/hfs/ "hfs3" [h3]: https://rejetto.com/hfs/ "hfs3"
[d]: https://github.com/nextcloud/server "nextcloud" [nc]: https://github.com/nextcloud/server "nextcloud"
[e]: https://github.com/haiwen/seafile "seafile" [sf]: https://github.com/haiwen/seafile "seafile"
[f]: https://github.com/rclone/rclone "rclone" [rc]: https://github.com/rclone/rclone "rclone"
[g]: https://github.com/sigoden/dufs "dufs" [df]: https://github.com/sigoden/dufs "dufs"
[h]: https://github.com/chibisafe/chibisafe "chibisafe" [cs]: https://github.com/chibisafe/chibisafe "chibisafe"
[i]: https://github.com/kalcaddle/kodbox "kodbox" [kb]: https://github.com/kalcaddle/kodbox "kodbox"
[j]: https://github.com/filebrowser/filebrowser "filebrowser" [fb]: https://github.com/filebrowser/filebrowser "filebrowser"
[k]: https://github.com/filegator/filegator "filegator" [fg]: https://github.com/filegator/filegator "filegator"
[l]: https://github.com/drakkan/sftpgo "sftpgo" [sg]: https://github.com/drakkan/sftpgo "sftpgo"
[m]: https://github.com/tobychui/arozos "arozos" [az]: https://github.com/tobychui/arozos "arozos"
* `a` = [copyparty][a] * `C` = [copyparty][C]
* `b` = [hfs2][b] 🔥 * `h2` = [hfs2][h2] 🔥
* `c` = [hfs3][c] * `h3` = [hfs3][h3]
* `d` = [nextcloud][d] * `nc` = [nextcloud][nc]
* `e` = [seafile][e] * `sf` = [seafile][sf]
* `f` = [rclone][f], specifically `rclone serve webdav .` * `rc` = [rclone][rc], specifically `rclone serve webdav .`
* `g` = [dufs][g] * `df` = [dufs][df]
* `h` = [chibisafe][h] * `cs` = [chibisafe][cs]
* `i` = [kodbox][i] * `kb` = [kodbox][kb]
* `j` = [filebrowser][j] * `fb` = [filebrowser][fb]
* `k` = [filegator][k] * `fg` = [filegator][fg]
* `l` = [sftpgo][l] * `sg` = [sftpgo][sg]
* `m` = [arozos][m] * `az` = [arozos][az]
some softwares not in the matrixes, some softwares not in the matrixes,
* [updog](#updog) * [updog](#updog)
@ -134,8 +134,8 @@ symbol legend,
## general ## general
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]| | feature / software |[C]|[h2]|[h3]|[nc]|[sf]|[rc]|[df]|[cs]|[kb]|[fb]|[fg]|[sg]|[az]|
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - | | ----------------------- |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| intuitive UX | | | █ | █ | █ | | █ | █ | █ | █ | █ | █ | █ | | intuitive UX | | | █ | █ | █ | | █ | █ | █ | █ | █ | █ | █ |
| config GUI | | █ | █ | █ | █ | | | █ | █ | █ | | █ | █ | | config GUI | | █ | █ | █ | █ | | | █ | █ | █ | | █ | █ |
| good documentation | | | █ | █ | █ | █ | █ | | | █ | █ | | | | good documentation | | | █ | █ | █ | █ | █ | | | █ | █ | | |
@ -155,28 +155,28 @@ symbol legend,
| iOS app | | | | █ | █ | | | | | | | | | | iOS app | | | | █ | █ | | | | | | | | |
* `zero setup` = you can get a mostly working setup by just launching the app, without having to install any software or configure whatever * `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: * `C`/copyparty remarks:
* no gui for server settings; only for client-side stuff * no gui for server settings; only for client-side stuff
* 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) * 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 * [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 * no iOS app but has [shortcuts](https://github.com/9001/copyparty#ios-shortcuts) for easy uploading
* validated on aarch64-BE by [Øl Telecom](http://ol-tele.com/) during eth0:2025; [photo1](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/aallwinner.jpg?cache) and [diploma](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/be-ready.png?cache) * validated on aarch64-BE by [Øl Telecom](http://ol-tele.com/) during eth0:2025; [photo1](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/aallwinner.jpg?cache) and [diploma](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/be-ready.png?cache)
* validated on [SGI IRIX](https://en.wikipedia.org/wiki/IRIX) ([an O2](https://en.wikipedia.org/wiki/SGI_O2)) by [Øl Telecom](http://ol-tele.com/) during 39c3; [photo1](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/sgi-o2.jpg?cache) and [screenshot](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/sgi-o2.png?cache) * validated on [SGI IRIX](https://en.wikipedia.org/wiki/IRIX) ([an O2](https://en.wikipedia.org/wiki/SGI_O2)) by [Øl Telecom](http://ol-tele.com/) during 39c3; [photo1](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/sgi-o2.jpg?cache) and [screenshot](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/sgi-o2.png?cache)
* `b`/hfs2 runs on linux through wine * `h2`/hfs2 runs on linux through wine
* `f`/rclone must be started with the command `rclone serve webdav .` or similar * `rc`/rclone must be started with the command `rclone serve webdav .` or similar
* `h`/chibisafe has undocumented windows support * `cs`/chibisafe has undocumented windows support
* `l`/sftpgo: * `sg`/sftpgo:
* Must be launched with a command * Must be launched with a command
* On Termux, just run `pkg in sftpgo` * On Termux, just run `pkg in sftpgo`
* `m`/arozos has partial windows support * `az`/arozos has partial windows support
## file transfer ## file transfer
*the thing that copyparty is actually kinda good at* *the thing that copyparty is actually kinda good at*
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]| | feature / software |[C]|[h2]|[h3]|[nc]|[sf]|[rc]|[df]|[cs]|[kb]|[fb]|[fg]|[sg]|[az]|
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - | | ----------------------- |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| download folder as zip | █ | █ | █ | █ | | | █ | | █ | █ | | █ | | | download folder as zip | █ | █ | █ | █ | | | █ | | █ | █ | | █ | |
| download folder as tar | █ | | | | | | | | | | | | | | download folder as tar | █ | | | | | | | | | | | | |
| upload | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | | █ | █ | | upload | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | | █ | █ |
@ -224,23 +224,23 @@ symbol legend,
* `cloud storage backend` = able to serve files from (and write to) s3 or similar cloud services; `` means the software can do this with some help from `rclone mount` as a bridge * `cloud storage backend` = able to serve files from (and write to) s3 or similar cloud services; `` means the software can do this with some help from `rclone mount` as a bridge
* `a`/copyparty can reject uploaded files (based on complex conditions), for example [by extension](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/reject-extension.py) or [mimetype](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/reject-mimetype.py) * `C`/copyparty can reject uploaded files (based on complex conditions), for example [by extension](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/reject-extension.py) or [mimetype](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/reject-mimetype.py)
* `e`/seafile download-as-zip is not streaming; it creates the full zipfile before download can start, and fails on big folders * `sf`/seafile download-as-zip is not streaming; it creates the full zipfile before download can start, and fails on big folders
* `j`/filebrowser remarks: * `fg`/filebrowser remarks:
* can provide checksums for single files on request * can provide checksums for single files on request
* can probably do extension/mimetype rejection similar to copyparty * can probably do extension/mimetype rejection similar to copyparty
* `k`/filegator download-as-zip is not streaming; it creates the full zipfile before download can start * `fg`/filegator download-as-zip is not streaming; it creates the full zipfile before download can start
* `l`/sftpgo: * `sg`/sftpgo:
* resumable/segmented uploads only over SFTP, not over HTTP * resumable/segmented uploads only over SFTP, not over HTTP
* upload rules are totals only, not over time * upload rules are totals only, not over time
* can probably do extension/mimetype rejection similar to copyparty * can probably do extension/mimetype rejection similar to copyparty
* `m`/arozos download-as-zip is not streaming; it creates the full zipfile before download can start, and fails on big folders * `az`/arozos download-as-zip is not streaming; it creates the full zipfile before download can start, and fails on big folders
## protocols and client support ## protocols and client support
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]| | feature / software |[C]|[h2]|[h3]|[nc]|[sf]|[rc]|[df]|[cs]|[kb]|[fb]|[fg]|[sg]|[az]|
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - | | ----------------------- |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| serve https | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | | serve https | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
| serve webdav | █ | | | █ | █ | █ | █ | | █ | | | █ | █ | | serve webdav | █ | | | █ | █ | █ | █ | | █ | | | █ | █ |
| serve ftp (tcp) | █ | | | | | █ | | | | | | █ | █ | | serve ftp (tcp) | █ | | | | | █ | | | | | | █ | █ |
@ -261,17 +261,17 @@ symbol legend,
* `mojibake filenames` = filenames decoded with the wrong codec and then reencoded (usually to utf-8), so `宇多田ヒカル` might look like `ëFæ╜ôcâqâJâï` * `mojibake filenames` = filenames decoded with the wrong codec and then reencoded (usually to utf-8), so `宇多田ヒカル` might look like `ëFæ╜ôcâqâJâï`
* `undecodable filenames` = pure binary garbage which cannot be parsed as utf-8 * `undecodable filenames` = pure binary garbage which cannot be parsed as utf-8
* you can successfully play `$'\355\221'` with mpv through mounting a remote copyparty server with rclone, pog * you can successfully play `$'\355\221'` with mpv through mounting a remote copyparty server with rclone, pog
* `a`/copyparty remarks: * `C`/copyparty remarks:
* extremely minimal samba/cifs server * extremely minimal samba/cifs server
* netscape 4 / ie6 support is mostly listed as a joke altho some people have actually found it useful ([ie4 tho](https://user-images.githubusercontent.com/241032/118192791-fb31fe00-b446-11eb-9647-898ea8efc1f7.png)) * netscape 4 / ie6 support is mostly listed as a joke altho some people have actually found it useful ([ie4 tho](https://user-images.githubusercontent.com/241032/118192791-fb31fe00-b446-11eb-9647-898ea8efc1f7.png))
* `l`/sftpgo translates mojibake filenames into valid utf-8 (information loss) * `sg`/sftpgo translates mojibake filenames into valid utf-8 (information loss)
* `m`/arozos has readonly-support for older browsers; no uploading * `az`/arozos has readonly-support for older browsers; no uploading
## server configuration ## server configuration
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]| | feature / software |[C]|[h2]|[h3]|[nc]|[sf]|[rc]|[df]|[cs]|[kb]|[fb]|[fg]|[sg]|[az]|
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - | | ----------------------- |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| config from cmd args | █ | | █ | | | █ | █ | | | █ | | | | | config from cmd args | █ | | █ | | | █ | █ | | | █ | | | |
| config files | █ | █ | █ | | | █ | | █ | | █ | • | | | | config files | █ | █ | █ | | | █ | | █ | | █ | • | | |
| runtime config reload | █ | █ | █ | | | | | █ | █ | █ | █ | | █ | | runtime config reload | █ | █ | █ | | | | | █ | █ | █ | █ | | █ |
@ -282,17 +282,17 @@ symbol legend,
| folder-rproxy ok | █ | | █ | | █ | █ | | • | • | █ | • | | • | | folder-rproxy ok | █ | | █ | | █ | █ | | • | • | █ | • | | • |
* `folder-rproxy` = reverse-proxying without dedicating an entire (sub)domain, using a subfolder instead * `folder-rproxy` = reverse-proxying without dedicating an entire (sub)domain, using a subfolder instead
* `l`/sftpgo: * `sg`/sftpgo:
* config: user can be added by cmd command in [Portable mode](https://docs.sftpgo.com/2.6/cli/#portable-mode); if not in Portable mode users must be added through gui / api calls * config: user can be added by cmd command in [Portable mode](https://docs.sftpgo.com/2.6/cli/#portable-mode); if not in Portable mode users must be added through gui / api calls
* `m`/arozos: * `az`/arozos:
* configuration is primarily through GUI * configuration is primarily through GUI
* reverse-proxy is not guaranteed to see the correct client IP * reverse-proxy is not guaranteed to see the correct client IP
## server capabilities ## server capabilities
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]| | feature / software |[C]|[h2]|[h3]|[nc]|[sf]|[rc]|[df]|[cs]|[kb]|[fb]|[fg]|[sg]|[az]|
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - | | ----------------------- |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| accounts | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | | accounts | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
| per-account chroot | | | | | | | | | | | | █ | | | per-account chroot | | | | | | | | | | | | █ | |
| single-sign-on | | | | █ | █ | | | | • | | | | | | single-sign-on | | | | █ | █ | | | | • | | | | |
@ -337,7 +337,7 @@ symbol legend,
* `speed throttle` = rate limiting (per ip, per user, per connection, anything like that) * `speed throttle` = rate limiting (per ip, per user, per connection, anything like that)
* `curl-friendly ls` = returns a [sortable plaintext folder listing](https://user-images.githubusercontent.com/241032/215322619-ea5fd606-3654-40ad-94ee-2bc058647bb2.png) when curled * `curl-friendly ls` = returns a [sortable plaintext folder listing](https://user-images.githubusercontent.com/241032/215322619-ea5fd606-3654-40ad-94ee-2bc058647bb2.png) when curled
* `curl-friendly upload` = uploading with curl is just `curl -T some.bin http://.../` * `curl-friendly upload` = uploading with curl is just `curl -T some.bin http://.../`
* `a`/copyparty remarks: * `C`/copyparty remarks:
* single-sign-on, token-auth, and 2fa is *possible* through authelia/authentik or similar, but nobody's made an example yet * single-sign-on, token-auth, and 2fa is *possible* through authelia/authentik or similar, but nobody's made an example yet
* one-way folder sync from local to server can be done efficiently with [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy), or with webdav and conventional rsync * one-way folder sync from local to server can be done efficiently with [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy), or with webdav and conventional rsync
* can hot-reload config files (with just a few exceptions) * can hot-reload config files (with just a few exceptions)
@ -346,21 +346,21 @@ symbol legend,
* [version-checker](https://github.com/9001/copyparty/#version-checker) can check if the current version has a known vulnerability and immediately exit/shutdown, but automatic self-updating is **not** available * [version-checker](https://github.com/9001/copyparty/#version-checker) can check if the current version has a known vulnerability and immediately exit/shutdown, but automatic self-updating is **not** available
* [event hooks](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks) ([discord](https://user-images.githubusercontent.com/241032/215304439-1c1cb3c8-ec6f-4c17-9f27-81f969b1811a.png), [desktop](https://user-images.githubusercontent.com/241032/215335767-9c91ed24-d36e-4b6b-9766-fb95d12d163f.png)) inspired by filebrowser, as well as the more complex [media parser](https://github.com/9001/copyparty/tree/hovudstraum/bin/mtag) alternative * [event hooks](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks) ([discord](https://user-images.githubusercontent.com/241032/215304439-1c1cb3c8-ec6f-4c17-9f27-81f969b1811a.png), [desktop](https://user-images.githubusercontent.com/241032/215335767-9c91ed24-d36e-4b6b-9766-fb95d12d163f.png)) inspired by filebrowser, as well as the more complex [media parser](https://github.com/9001/copyparty/tree/hovudstraum/bin/mtag) alternative
* upload history can be visualized using [partyjournal](https://github.com/9001/copyparty/blob/hovudstraum/bin/partyjournal.py) * upload history can be visualized using [partyjournal](https://github.com/9001/copyparty/blob/hovudstraum/bin/partyjournal.py)
* `k`/filegator remarks: * `fg`/filegator remarks:
* `per-* permissions` -- can limit a user to one folder and its subfolders * `per-* permissions` -- can limit a user to one folder and its subfolders
* `unmap subfolders` -- can globally filter a list of paths * `unmap subfolders` -- can globally filter a list of paths
* `l`/sftpgo: * `sg`/sftpgo:
* `file action event hooks` also include on-download triggers * `file action event hooks` also include on-download triggers
* `upload tracking / log` in main logfile * `upload tracking / log` in main logfile
* `m`/arozos: * `az`/arozos:
* `2fa` maybe possible through LDAP/Oauth * `2fa` maybe possible through LDAP/Oauth
* `c`/hfs3 * `h3`/hfs3
* `2fa` available by installing a plugin * `2fa` available by installing a plugin
## client features ## client features
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]| | feature / software |[C]|[h2]|[h3]|[nc]|[sf]|[rc]|[df]|[cs]|[kb]|[fb]|[fg]|[sg]|[az]|
| ---------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - | | ---------------------- |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| single-page app | █ | | █ | █ | █ | | | █ | █ | █ | █ | | █ | | single-page app | █ | | █ | █ | █ | | | █ | █ | █ | █ | | █ |
| themes | █ | █ | █ | █ | | | | | █ | | | | | | themes | █ | █ | █ | █ | | | | | █ | | | | |
| directory tree nav | █ | | | | █ | | | | █ | | | | | | directory tree nav | █ | | | | █ | | | | █ | | | | |
@ -404,22 +404,22 @@ symbol legend,
* `search by custom tags` = ability to tag files through the UI and search by those * `search by custom tags` = ability to tag files through the UI and search by those
* `find local file` = drop a file into the browser to see if it exists on the server * `find local file` = drop a file into the browser to see if it exists on the server
* `undo recent uploads` = accounts without delete permissions have a time window where they can undo their own uploads * `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) * `C`/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 * `h2`/hfs2 has a very basic directory tree view, not showing sibling folders
* `f`/rclone can do some file management (mkdir, rename, delete) when hosting througn webdav * `rc`/rclone can do some file management (mkdir, rename, delete) when hosting througn webdav
* `j`/filebrowser remarks: * `fg`/filebrowser remarks:
* audio playback does not continue into next song * audio playback does not continue into next song
* plaintext viewer/editor * plaintext viewer/editor
* `k`/filegator directory tree is a modal window * `fg`/filegator directory tree is a modal window
* `l`/sftpgo remarks: * `sg`/sftpgo remarks:
* audio/video playback does not continue into next song/video * audio/video playback does not continue into next song/video
* plaintext viewer/editor * plaintext viewer/editor
## integration ## integration
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]| | feature / software |[C]|[h2]|[h3]|[nc]|[sf]|[rc]|[df]|[cs]|[kb]|[fb]|[fg]|[sg]|[az]|
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - | | ----------------------- |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| OS alert on upload | | | | | | | | | | | | | | | OS alert on upload | | | | | | | | | | | | | |
| discord | | | | | | | | | | | | | | | discord | | | | | | | | | | | | | |
| ┗ announce uploads | | | | | | | | | | | | | | | ┗ announce uploads | | | | | | | | | | | | | |
@ -428,11 +428,11 @@ symbol legend,
| flameshot | | | | | | █ | | | | | | | | | flameshot | | | | | | █ | | | | | | | |
* sharex `` = yes, but does not provide example sharex config * sharex `` = yes, but does not provide example sharex config
* `a`/copyparty remarks: * `C`/copyparty remarks:
* `OS alert on upload` available as [a plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/notify.py) * `OS alert on upload` available as [a plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/notify.py)
* `discord » announce uploads` available as [a plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/discord-announce.py) * `discord » announce uploads` available as [a plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/discord-announce.py)
* `j`/filebrowser can probably pull those off with command runners similar to copyparty * `fg`/filebrowser can probably pull those off with command runners similar to copyparty
* `l`/sftpgo has nothing built-in but is very extensible * `sg`/sftpgo has nothing built-in but is very extensible
## another matrix ## another matrix
@ -777,7 +777,7 @@ symbol legend,
# briefly considered # briefly considered
* [pydio](https://github.com/pydio/cells): python/agpl3, looks great, fantastic ux -- but needs mariadb, systemwide install * [pydio](https://github.com/pydio/cells): go/agpl3, looks great, fantastic ux -- but needs mariadb, systemwide install, SSO is 3280€/year
* [gossa](https://github.com/pldubouilh/gossa): go/mit, minimalistic, basic file upload, text editor, mkdir and rename (no delete/move) * [gossa](https://github.com/pldubouilh/gossa): go/mit, minimalistic, basic file upload, text editor, mkdir and rename (no delete/move)

View file

@ -2,7 +2,7 @@ FROM alpine:3.23
WORKDIR /z WORKDIR /z
ENV ver_hashwasm=4.12.0 \ ENV ver_hashwasm=4.12.0 \
ver_marked=4.3.0 \ ver_marked=4.3.0 \
ver_dompf=3.3.1 \ ver_dompf=3.3.3 \
ver_mde=2.18.0 \ ver_mde=2.18.0 \
ver_codemirror=5.65.18 \ ver_codemirror=5.65.18 \
ver_fontawesome=5.13.0 \ ver_fontawesome=5.13.0 \

View file

@ -5,6 +5,25 @@ import re, os, sys, time, shutil, signal, tarfile, hashlib, platform, tempfile,
import subprocess as sp import subprocess as sp
# skip 1
#
# py2sfx (sfx.py) - bundle python-modules into an executable sfx.py
# (c)2020, ed <oss@ocv.me>, MIT-licensed, originally from copyparty:
# https://github.com/9001/copyparty/blob/hovudstraum/scripts/sfx.py
#
# ( no need to include the remaining lines of this comment-block
# for attribution; the rest is just for context )
#
# to create an sfx, use this:
# https://github.com/9001/copyparty/blob/hovudstraum/scripts/make-sfx.sh
#
# wanna steal this for your own project? then maybe
# see the r0c version, since that's slightly simpler:
# https://github.com/9001/r0c/blob/master/scripts/sfx.py
#
# skip 0
""" """
to edit this file, use HxD or "vim -b" to edit this file, use HxD or "vim -b"
(there is compressed stuff at the end) (there is compressed stuff at the end)

View file

@ -8,6 +8,7 @@ import tempfile
import time import time
import unittest import unittest
from copyparty.__init__ import MACOS
from copyparty.authsrv import AuthSrv from copyparty.authsrv import AuthSrv
from copyparty.httpcli import HttpCli from copyparty.httpcli import HttpCli
from tests import util as tu from tests import util as tu
@ -218,7 +219,8 @@ class TestHttpCli(TC):
# float x-oc-mtime should be accepted # float x-oc-mtime should be accepted
h, b = self.req(RCLONE_PUT_FLOAT % ("a/fb",)) h, b = self.req(RCLONE_PUT_FLOAT % ("a/fb",))
self.assertStart("HTTP/1.1 201 Created\r", h) self.assertStart("HTTP/1.1 201 Created\r", h)
self.assertAlmostEqual(os.path.getmtime("a/fb"), 1689453578.123, places=3) zi = 0 if MACOS else 3 # its okay macos you tried your best
self.assertAlmostEqual(os.path.getmtime("a/fb"), 1689453578.123, places=zi)
# then it does a propfind to confirm # then it does a propfind to confirm
h, b = self.req(RCLONE_PROPFIND % ("a/fa",)) h, b = self.req(RCLONE_PROPFIND % ("a/fa",))

View file

@ -143,13 +143,13 @@ class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None, **ka0): def __init__(self, a=None, v=None, c=None, **ka0):
ka = {} ka = {}
ex = "allow_flac allow_wav chpw cookie_lax daw dav_auth dav_mac dav_rt dlni e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only http_no_tcp ih ihead localtime log_badxml magic md_no_br nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_dupe_m no_fnugg no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_u2abrt no_zip no_zls nrand nsort nw og og_no_head og_s_title ohead opds q rand re_dirsz reflink rm_partial rmagic rss smb srch_dbg srch_excl srch_icase stats ui_noacci ui_nocpla ui_noctxb ui_nolbar ui_nombar ui_nonav ui_notree ui_norepl ui_nosrvi uqe usernames vague_403 vc ver vol_nospawn vol_or_crash wo_up_readme write_uplog xdev xlink xvol zipmaxu zs" ex = "allow_flac allow_wav chpw cookie_lax daw dav_auth dav_mac dav_rt dlni dothidden e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only http_no_tcp ih ihead localtime log_badxml magic md_no_br nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_dupe_m no_fnugg no_html no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_script no_tail no_tarcmp no_thumb no_vthumb no_u2abrt no_zip no_zls nrand nsort nw og og_no_head og_s_title ohead opds q rand re_dirsz reflink rm_partial rmagic rss smb srch_dbg srch_excl srch_icase stats ui_noacci ui_nocpla ui_noctxb ui_nolbar ui_nombar ui_nonav ui_notree ui_norepl ui_nosrvi uqe usernames vague_403 vc ver vol_nospawn vol_or_crash wo_up_readme write_uplog xdev xlink xvol zipmaxu zs"
ka.update(**{k: False for k in ex.split()}) ka.update(**{k: False for k in ex.split()})
ex = "dav_inf dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump wram re_dhash see_dots plain_ip" ex = "dav_inf dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump wram re_dhash see_dots plain_ip"
ka.update(**{k: True for k in ex.split()}) ka.update(**{k: True for k in ex.split()})
ex = "ah_cli ah_gen css_browser dbpath hist ipu js_browser js_other mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua ua_nodoc ua_nozip" ex = "ah_cli ah_gen css_browser dbpath hist ipu js_browser js_other lf_url mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua ua_nodoc ua_nozip"
ka.update(**{k: None for k in ex.split()}) ka.update(**{k: None for k in ex.split()})
ex = "gid uid" ex = "gid uid"
@ -161,10 +161,10 @@ class Cfg(Namespace):
ex = "ac_convt au_vol dl_list du_iwho mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt th_qv th_qvx ups_who ver_iwho zip_who" ex = "ac_convt au_vol dl_list du_iwho mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt th_qv th_qvx ups_who ver_iwho zip_who"
ka.update(**{k: 9 for k in ex.split()}) ka.update(**{k: 9 for k in ex.split()})
ex = "ctl_re db_act forget_ip idp_cookie idp_store k304 loris no304 nosubtle qr_pin qr_wait re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs" ex = "ctl_re db_act forget_ip gauto idp_cookie idp_store k304 loris no304 nosubtle qr_pin qr_wait re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs"
ka.update(**{k: 0 for k in ex.split()}) ka.update(**{k: 0 for k in ex.split()})
ex = "ah_alg bname chdir chmod_f chpw_db db_xattr doctitle df epilogues exit favico fika ipa ipar html_head html_head_d html_head_s idp_login idp_logout lg_sba lg_sbf log_date log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i opds_exts preadmes prologues readmes shr shr1 shr_site site smsg tcolor textfiles txt_eol ufavico ufavico_h unlist up_site vc_url vname xff_src zipmaxt R RS SR" ex = "ah_alg bname chdir chmod_f chpw_db db_xattr doctitle df epilogues exit favico fika ipa ipar html_head html_head_d html_head_s idp_login idp_logout lg_sba lg_sbf log_date log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i opds_exts preadmes prologues readmes shr shr1 shr_site site smsg tcolor textfiles th_pregen txt_eol ufavico ufavico_h unlist up_site vc_url vname xff_src zipmaxt R RS SR"
ka.update(**{k: "" for k in ex.split()}) ka.update(**{k: "" for k in ex.split()})
ex = "apnd_who ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url dont_ban cachectl http_vary rcm rss_fmt_d rss_fmt_t spinner" ex = "apnd_who ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url dont_ban cachectl http_vary rcm rss_fmt_d rss_fmt_t spinner"
@ -316,6 +316,8 @@ class VHttpSrv(object):
self.g403 = Garda("") self.g403 = Garda("")
self.gurl = Garda("") self.gurl = Garda("")
self.ico = Ico(args)
self.thumbcli = None
self.u2idx = None self.u2idx = None
def cachebuster(self): def cachebuster(self):
@ -356,7 +358,6 @@ class VHttpConn(object):
Ctor = VHttpSrvUp2k if use_up2k else VHttpSrv Ctor = VHttpSrvUp2k if use_up2k else VHttpSrv
self.hsrv = Ctor(args, asrv, log) self.hsrv = Ctor(args, asrv, log)
self.ico = Ico(args)
self.ipr = None self.ipr = None
self.ipa_nm = None self.ipa_nm = None
self.ipar_nm = None self.ipar_nm = None