mirror of
https://github.com/9001/copyparty.git
synced 2026-11-14 07:48:21 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ccebb6948 | ||
|
|
ed0fcd608f | ||
|
|
9811cd3101 | ||
|
|
fa8913c712 | ||
|
|
4bd64d7456 | ||
|
|
7ce483e970 | ||
|
|
82eab3d2f8 | ||
|
|
0dc1cb3291 | ||
|
|
1a87dc3218 | ||
|
|
dc3adc0f4d | ||
|
|
5f00c1631c | ||
|
|
e59fc05e73 | ||
|
|
e8de421174 | ||
|
|
36aaa24974 | ||
|
|
5e0a88e355 | ||
|
|
4b283b4bfd | ||
|
|
64f574fe94 | ||
|
|
a31870cbd3 | ||
|
|
74b17374c2 | ||
|
|
670e86228c | ||
|
|
1341b3421f | ||
|
|
5961228c93 | ||
|
|
28f9f6f002 | ||
|
|
186cbf939f | ||
|
|
080935788b | ||
|
|
c6918c2ce6 | ||
|
|
ab2299e3db | ||
|
|
334dccc55e | ||
|
|
8d7aa4f860 | ||
|
|
f73b37c6fa | ||
|
|
731dbbff84 | ||
|
|
23e00c0a04 | ||
|
|
107dec9ed1 | ||
|
|
f445bfa862 | ||
|
|
aff083fb28 | ||
|
|
5ccc999f39 | ||
|
|
16a3f5d51a | ||
|
|
35af7c9fef | ||
|
|
4d1f2a9301 | ||
|
|
229159027b | ||
|
|
ec4a95e1a5 | ||
|
|
eb4f1dddce | ||
|
|
097b83056e | ||
|
|
c688bb2e46 | ||
|
|
c92435c2d2 | ||
|
|
22afaa7586 | ||
|
|
5ddbce31fc | ||
|
|
ec8ec0f1a6 | ||
|
|
558f6aee44 | ||
|
|
6867ccc511 | ||
|
|
947d73eeb6 | ||
|
|
735d6e00de | ||
|
|
fd4f434192 | ||
|
|
971a62cc89 | ||
|
|
bfc3cff848 | ||
|
|
8967726f61 | ||
|
|
ad9994bef3 | ||
|
|
e5e2e97910 | ||
|
|
e42e01563e | ||
|
|
de21c210c6 | ||
|
|
55969b8e54 | ||
|
|
b80a210e0f | ||
|
|
8016512064 | ||
|
|
72eb03bf43 | ||
|
|
f870cf3c05 | ||
|
|
e6c9c6fbeb | ||
|
|
dd9cf6456d | ||
|
|
185087ee05 | ||
|
|
3e3401f4ed | ||
|
|
a78611d8c5 | ||
|
|
370a05ab3c | ||
|
|
d7d30f268e | ||
|
|
34229337cc | ||
|
|
0767d95674 | ||
|
|
f00d19c5d3 | ||
|
|
ab791921ea | ||
|
|
2cebd8ce16 | ||
|
|
662e3c5d59 | ||
|
|
3ea671ff9a | ||
|
|
f792a9529c | ||
|
|
4e318d5165 | ||
|
|
80cf7bfdc0 | ||
|
|
704af697c7 | ||
|
|
b854b8f596 | ||
|
|
936e8a3bf3 | ||
|
|
7796c2d06a | ||
|
|
adf5f639fa | ||
|
|
d4a1ce48e4 | ||
|
|
0a957bf264 | ||
|
|
bdf8b793d9 | ||
|
|
69c022f565 | ||
|
|
7984ef9232 | ||
|
|
9de090265f | ||
|
|
6115eb452d | ||
|
|
32eadc0f14 | ||
|
|
566de65f17 | ||
|
|
78a4ee49a2 | ||
|
|
9ff6a71a2a | ||
|
|
21c2c72889 | ||
|
|
18791c5768 | ||
|
|
2be85b67a9 | ||
|
|
eeb399e3c9 | ||
|
|
dcc0abd696 | ||
|
|
48d9de03fe | ||
|
|
dbc6df812b | ||
|
|
1864805072 | ||
|
|
47297475bf | ||
|
|
3098cf59cb | ||
|
|
efcf96e3e3 | ||
|
|
92c3f32e13 | ||
|
|
14e2d79b08 | ||
|
|
825f1c9456 | ||
|
|
f19ef033ec | ||
|
|
8b6e8972fe | ||
|
|
fbcb773307 | ||
|
|
3eb7b46d83 | ||
|
|
4f20738b71 | ||
|
|
ef75220faf | ||
|
|
a3acbfcd5b | ||
|
|
fdf52a8c4e | ||
|
|
b9405a3e25 | ||
|
|
6f924829ee | ||
|
|
24a4090fc7 | ||
|
|
c0c638c917 | ||
|
|
2c6497b644 | ||
|
|
2c4033831f | ||
|
|
2880117265 | ||
|
|
63c61b6729 | ||
|
|
5c157be955 | ||
|
|
9f5f9d8884 | ||
|
|
4003a47723 | ||
|
|
398fcf1d18 | ||
|
|
ac4be555f6 | ||
|
|
75c7502c34 | ||
|
|
9912a95166 | ||
|
|
b331bb1c8c | ||
|
|
c122e103bd | ||
|
|
6226858b89 | ||
|
|
cea97ac6fd | ||
|
|
bc45299b86 | ||
|
|
bb40804f2d | ||
|
|
c70dc7ac56 | ||
|
|
b6abc33fb3 | ||
|
|
537a99df52 | ||
|
|
cdb474c8ec | ||
|
|
0f2040c383 | ||
|
|
e49f6cca17 | ||
|
|
10db4236bf | ||
|
|
d57bb0c701 | ||
|
|
fface524c3 | ||
|
|
bae77b9078 | ||
|
|
b2e4b1c46c | ||
|
|
4543ab31d3 | ||
|
|
856fada3c7 | ||
|
|
aa86235360 | ||
|
|
6a9437b77c | ||
|
|
5952401815 | ||
|
|
ed0be42a3b | ||
|
|
daf144af61 | ||
|
|
a7c99094dd | ||
|
|
daee3e8500 | ||
|
|
96bafb8f4a | ||
|
|
e700e4c421 | ||
|
|
e15f292766 | ||
|
|
8884606138 | ||
|
|
12878e6c1c | ||
|
|
29e145f5e4 | ||
|
|
85a8cc99ba | ||
|
|
a523ba231f | ||
|
|
f0feab0889 | ||
|
|
e40755331b | ||
|
|
89040874f5 | ||
|
|
9fa950bcfa | ||
|
|
a3fbba10f7 | ||
|
|
741b00f342 | ||
|
|
2c8325a770 | ||
|
|
db68353e22 | ||
|
|
68568b7743 | ||
|
|
3888048755 | ||
|
|
379c0aa6b4 | ||
|
|
8c4e931300 | ||
|
|
32d074ffc5 | ||
|
|
85be3b8de2 | ||
|
|
7b21e9f9ce | ||
|
|
56b6db0875 | ||
|
|
fe006073a9 | ||
|
|
8242e69359 | ||
|
|
34c856e838 | ||
|
|
fa1499ae15 | ||
|
|
be31a74485 | ||
|
|
12d877b0d0 | ||
|
|
d3b9599421 | ||
|
|
9a7f474750 | ||
|
|
52c42b83e4 | ||
|
|
4c8203016f | ||
|
|
a4f259dafb | ||
|
|
d33d11321f | ||
|
|
e017b1bc6e | ||
|
|
efa43f891b | ||
|
|
90639de984 | ||
|
|
5dbff4af41 | ||
|
|
5beecd6622 | ||
|
|
5a8949d595 | ||
|
|
22c3a3dd46 | ||
|
|
a00bc93fe1 | ||
|
|
a48afe87b9 | ||
|
|
c3eb9ecec2 | ||
|
|
6e75faa623 | ||
|
|
06af60276b | ||
|
|
cc80ecc341 | ||
|
|
ce33a88e25 | ||
|
|
62dc833273 | ||
|
|
f0f6933b4e | ||
|
|
e32718303c | ||
|
|
c7b80acd0d | ||
|
|
cc5420a324 | ||
|
|
c28aa08b35 | ||
|
|
6183540c61 | ||
|
|
5e806ec124 | ||
|
|
9068ec6a8e | ||
|
|
27031f73be | ||
|
|
926c6e814d | ||
|
|
4e9ad781b6 | ||
|
|
f4f97b6cc3 | ||
|
|
f23ec5d9f8 | ||
|
|
348b4bb5c7 | ||
|
|
2c778e0828 | ||
|
|
7d81b9e837 | ||
|
|
ca406472f4 | ||
|
|
30b23c6ae8 | ||
|
|
9b0268970c | ||
|
|
8c201b844e | ||
|
|
b2401ff15a | ||
|
|
1f4246c6bb | ||
|
|
83dc20f33e | ||
|
|
f432ef6d51 | ||
|
|
d7eb556cdd | ||
|
|
0bb80e9294 | ||
|
|
3b53a228b0 | ||
|
|
139ef1851e | ||
|
|
2a4c82c742 | ||
|
|
b009c585a6 | ||
|
|
da6e2ddca9 | ||
|
|
6e25d648a9 | ||
|
|
a09a0eadbb | ||
|
|
1066dc3908 | ||
|
|
8d4363d147 | ||
|
|
8b986888a9 | ||
|
|
1e7de5d14f | ||
|
|
e52bbed871 | ||
|
|
46bd386a55 | ||
|
|
8c7cdf8583 | ||
|
|
43773f2c7e | ||
|
|
228c3dfa79 | ||
|
|
ac05b4f1e6 | ||
|
|
238887c774 | ||
|
|
9a724b0124 | ||
|
|
8173018926 | ||
|
|
37e68f60a6 | ||
|
|
cbd82b654a | ||
|
|
b31f29024a | ||
|
|
3a9ff67ab0 | ||
|
|
874e0e7a01 | ||
|
|
a997455b5a | ||
|
|
6fb1287e7f | ||
|
|
e00f2b46eb | ||
|
|
745d82faf8 | ||
|
|
f6dc1e2996 | ||
|
|
d93fadd87e | ||
|
|
a5d859d2b1 | ||
|
|
0b16e875da | ||
|
|
f5613187b4 | ||
|
|
003c68d027 | ||
|
|
961a273764 | ||
|
|
55d37364a6 | ||
|
|
6b3419b38f | ||
|
|
660ed7a92e | ||
|
|
822fa71800 | ||
|
|
ed516ddc20 | ||
|
|
ede692925e | ||
|
|
ec3e0e7e1d | ||
|
|
d1517d0c65 | ||
|
|
fb5384f412 | ||
|
|
971f8ef944 | ||
|
|
198f631ac8 | ||
|
|
5aaa4ff15b | ||
|
|
26e663d111 | ||
|
|
dcbdd9e665 | ||
|
|
6a9e6da864 | ||
|
|
aa23777385 | ||
|
|
abdbd69a68 | ||
|
|
edc201752b | ||
|
|
7d6b037dcc | ||
|
|
5f3b76c8f8 | ||
|
|
66f9c95097 | ||
|
|
134e378e3a | ||
|
|
b4faff8e4a | ||
|
|
df78fc5732 | ||
|
|
23545116af | ||
|
|
beb634dc54 | ||
|
|
1f8ebd57dc | ||
|
|
311eb698a0 | ||
|
|
bb92ce5fc8 | ||
|
|
1afe48b85d | ||
|
|
bbe58105e7 | ||
|
|
ac60a1da46 | ||
|
|
2cebda3297 | ||
|
|
4688410f8f | ||
|
|
e71e1900b4 | ||
|
|
8c6d8a3c22 | ||
|
|
dcb0ffa72c | ||
|
|
eb028c92f8 | ||
|
|
6eb4f0ad9c | ||
|
|
8a9066c35c |
No files matched your search
@@ -4,4 +4,5 @@
|
||||
|
||||
*.png binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.gz binary
|
||||
@@ -35,6 +35,7 @@ scripts/docker/base/test-aac/
|
||||
scripts/docker/base/whl/
|
||||
scripts/docker/i/
|
||||
scripts/deps-docker/uncomment.py
|
||||
scripts/deps-docker/unhint.py
|
||||
contrib/package/arch/pkg/
|
||||
contrib/package/arch/src/
|
||||
|
||||
|
||||
@@ -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, ...
|
||||
* [server config](#server-config) - using arguments or config files, or a mix of both
|
||||
* [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))
|
||||
* [mdns](#mdns) - LAN domain-name and feature announcer
|
||||
* [ssdp](#ssdp) - windows-explorer announcer
|
||||
@@ -75,6 +76,7 @@ built in Norway 🇳🇴 with contributions from [not-norway](https://github.com
|
||||
* [connecting to webdav from windows](#connecting-to-webdav-from-windows) - using the GUI
|
||||
* [tftp server](#tftp-server) - a TFTP server (read/write) can be started using `--tftp 3969`
|
||||
* [smb server](#smb-server) - unsafe, slow, not recommended for wan
|
||||
* [wopi server](#wopi-server) - edit office documents in the web-ui
|
||||
* [browser ux](#browser-ux) - tweaking the ui
|
||||
* [opengraph](#opengraph) - discord and social-media embeds
|
||||
* [file deduplication](#file-deduplication) - enable symlink-based upload deduplication
|
||||
@@ -82,10 +84,14 @@ built in Norway 🇳🇴 with contributions from [not-norway](https://github.com
|
||||
* [exclude-patterns](#exclude-patterns) - to save some time
|
||||
* [filesystem guards](#filesystem-guards) - avoid traversing into other filesystems
|
||||
* [periodic rescan](#periodic-rescan) - filesystem monitoring
|
||||
* [u2sz](#u2sz) - configure upload-chunking
|
||||
* [upload rules](#upload-rules) - set upload rules using volflags
|
||||
* [compress uploads](#compress-uploads) - files can be autocompressed on upload
|
||||
* [chmod and chown](#chmod-and-chown) - per-volume filesystem-permissions and ownership
|
||||
* [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
|
||||
* [thumbnail pregen](#thumbnail-pregen) - if you want to pre-generate everything on startup
|
||||
* [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 xattrs](#metadata-from-xattrs) - unix extended file attributes
|
||||
@@ -117,6 +123,7 @@ built in Norway 🇳🇴 with contributions from [not-norway](https://github.com
|
||||
* [packages](#packages) - the party might be closer than you think
|
||||
* [arch package](#arch-package) - `pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/))
|
||||
* [fedora package](#fedora-package) - does not exist yet
|
||||
* [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`
|
||||
* [nix package](#nix-package) - `nix profile install github:9001/copyparty`
|
||||
* [nixos module](#nixos-module)
|
||||
@@ -131,6 +138,7 @@ built in Norway 🇳🇴 with contributions from [not-norway](https://github.com
|
||||
* [client-side](#client-side) - when uploading files
|
||||
* [security](#security) - there is a [discord server](https://discord.gg/25J8CdTT6G) with announcements
|
||||
* [gotchas](#gotchas) - behavior that might be unexpected
|
||||
* [csp nonce](#csp-nonce) - unauthorized javascript is blocked
|
||||
* [cors](#cors) - cross-site request config
|
||||
* [filekeys](#filekeys) - prevent filename bruteforcing
|
||||
* [dirkeys](#dirkeys) - share specific folders in a volume
|
||||
@@ -180,7 +188,7 @@ enable thumbnails (images/audio/video), media indexing, and audio transcoding by
|
||||
* **Alpine:** `apk add py3-pillow ffmpeg`
|
||||
* **Debian:** `apt install --no-install-recommends python3-pil ffmpeg`
|
||||
* **Fedora:** rpmfusion + `dnf install python3-pillow ffmpeg --allowerasing`
|
||||
* **FreeBSD:** `pkg install py39-sqlite3 py39-pillow ffmpeg`
|
||||
* **FreeBSD:** `pkg install py311-sqlite3 py311-pillow ffmpeg`
|
||||
* **MacOS:** `port install py-Pillow ffmpeg`
|
||||
* **MacOS** (alternative): `brew install pillow ffmpeg`
|
||||
* **Windows:** `python -m pip install --user -U Pillow`
|
||||
@@ -211,6 +219,8 @@ other places to download copyparty from (non-github links):
|
||||
* https://copyparty.eu/enz = https://copyparty.eu/copyparty-en.pyz = the enterprise pyz
|
||||
* https://copyparty.eu/cli = online cli helptext
|
||||
|
||||
DO NOT download from [sourceforge](https://copyparty.eu/unsanctioned/sourceforge.txt)
|
||||
|
||||
|
||||
### at home
|
||||
|
||||
@@ -237,6 +247,11 @@ you may also want these, especially on servers:
|
||||
* [nixos module](#nixos-module) to run copyparty on NixOS hosts
|
||||
* [contrib/nginx/copyparty.conf](contrib/nginx/copyparty.conf) to [reverse-proxy](#reverse-proxy) behind nginx (for better https)
|
||||
|
||||
because the following environment variables are commonly used in service-scripts, they are understood by copyparty:
|
||||
|
||||
* `NOTIFY_SOCKET` as provided by systemd with service type=notify (see systemd/copyparty.service above)
|
||||
* `S6_NOTIFY_FD` for dinit [`ready-notification = pipevar:S6_NOTIFY_FD`](https://davmac.org/projects/dinit/man-pages-html/dinit-service.5.html#ready) and [s6 supervision suite notification](https://skarnet.org/software/s6/notifywhenup.html) support
|
||||
|
||||
and remember to open the ports you want; here's a complete example including every feature copyparty has to offer:
|
||||
```
|
||||
firewall-cmd --permanent --add-port={80,443,3921,3922,3923,3945,3990}/tcp # --zone=libvirt
|
||||
@@ -292,7 +307,7 @@ also see [comparison to similar software](./docs/versus.md)
|
||||
* ☑ realtime streaming of growing files (logfiles and such)
|
||||
* ☑ [thumbnails](#thumbnails)
|
||||
* ☑ ...of images using Pillow, pyvips, or FFmpeg
|
||||
* ☑ ...of RAW images using rawpy
|
||||
* ☑ ...of RAW images using libraw-dcraw_emu or rawpy
|
||||
* ☑ ...of videos using FFmpeg
|
||||
* ☑ ...of audio (spectrograms) using FFmpeg
|
||||
* ☑ cache eviction (max-age; maybe max-size eventually)
|
||||
@@ -513,6 +528,7 @@ upgrade notes
|
||||
per-folder, per-user permissions - if your setup is getting complex, consider making a [config file](./docs/example.conf) instead of using arguments
|
||||
* much easier to manage, and you can modify the config at runtime with `systemctl reload copyparty` or more conveniently using the `[reload cfg]` button in the control-panel (if the user has `a`/admin in any volume)
|
||||
* changes to the `[global]` config section requires a restart to take effect
|
||||
* when using config-files, ignore all the `-a` and `-v` stuff below, and CTRL-F `config file example` on this page instead
|
||||
|
||||
a quick summary can be seen using [`--help-accounts`](https://copyparty.eu/cli/#accounts-help-page)
|
||||
|
||||
@@ -556,12 +572,14 @@ if you want to grant access to all users who are logged in, the group `acct` wil
|
||||
* to do the opposite, granting access to everyone who is NOT logged in. `*,-@acct` does the trick, for example `-v /srv/welcome:welcome:r,*,-@acct`
|
||||
* single users can also be subtracted from a group: `@admins,-james`
|
||||
|
||||
anyone trying to bruteforce a password gets banned according to `--ban-pw`; default is 24h ban for 9 failed attempts in 1 hour
|
||||
anyone trying to bruteforce a password gets banned according to `--ban-pw` (assuming [real-ip](#real-ip) is configured correctly); default is 24h ban for 9 failed attempts in 1 hour
|
||||
|
||||
and if you want to use config files instead of commandline args (good!) then here's the same examples as a configfile; save it as `foobar.conf` and use it like this: `python copyparty-sfx.py -c foobar.conf`
|
||||
|
||||
* you can also `PRTY_CONFIG=foobar.conf python copyparty-sfx.py` (convenient in docker etc)
|
||||
|
||||
config file example:
|
||||
|
||||
```yaml
|
||||
[accounts]
|
||||
u1: p1 # create account "u1" with password "p1"
|
||||
@@ -604,12 +622,30 @@ 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
|
||||
|
||||
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`
|
||||
> **WARNING:** shadowing is NOT bulletproof when the underlying filesystem is casefolding (case-insensitive), in other words DON'T rely on this when copyparty is running on Windows and/or MacOS. This is also unsafe when combined with certain filesystems on Linux, especially FAT32/exFAT. Pay attention to the volume-listing in the startup log; `casechk:n` indicates probably-OK, but if it says `casechk:y` then DO NOT trust shadowing to fully prevent access to a file/subfolder.
|
||||
|
||||
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`
|
||||
|
||||
to fully unmap it from the filesystem, specify `//NULL` instead of a real path such as `/var/empty`, so for example `-v /mnt::r -v //NULL:web/certs:` ensures `/web/certs` will never be accessible by anyone
|
||||
|
||||
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
|
||||
|
||||
config file example for unmapping folders by shadowing:
|
||||
|
||||
```yaml
|
||||
[/drives]
|
||||
/mnt # url "/drives" goes to "/mnt"
|
||||
accs:
|
||||
r: * # everyone can read
|
||||
|
||||
[/drives/foo/bar]
|
||||
//NULL # blocks access to "/mnt/foo/bar"
|
||||
```
|
||||
|
||||
|
||||
## dotfiles
|
||||
|
||||
@@ -619,6 +655,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 `.`
|
||||
|
||||
> 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
|
||||
|
||||
> 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
|
||||
@@ -745,7 +783,7 @@ press `g` or `田` to toggle grid-view instead of the file listing and `t` togg
|
||||

|
||||
|
||||
it does static images with Pillow / pyvips / FFmpeg, and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how dangerous your users are
|
||||
* pyvips is 3x faster than Pillow, Pillow is 3x faster than FFmpeg
|
||||
* Pillow is 3x faster (and safer) than FFmpeg
|
||||
* disable thumbnails for specific volumes with volflag `dthumb` for all, or `dvthumb` / `dathumb` / `dithumb` for video/audio/images only
|
||||
* for installing FFmpeg on windows, see [optional dependencies](#optional-dependencies)
|
||||
|
||||
@@ -754,6 +792,7 @@ audio files are converted into spectrograms using FFmpeg unless you `--no-athumb
|
||||
images with the following names (see `--th-covers`) become the thumbnail of the folder they're in: `folder.png`, `folder.jpg`, `cover.png`, `cover.jpg`
|
||||
* the order is significant, so if both `cover.png` and `folder.jpg` exist in a folder, it will pick the first matching `--th-covers` entry (`folder.jpg`)
|
||||
* and, if you enable [file indexing](#file-indexing), it will also try those names as dotfiles (`.folder.jpg` and so), and then fallback on the first picture in the folder (if it has any pictures at all)
|
||||
* disable folderthumbs with `--th-covers no`
|
||||
|
||||
enabling `multiselect` lets you click files to select them, and then shift-click another file for range-select
|
||||
* `multiselect` is mostly intended for phones/tablets, but the `sel` option in the `[⚙️] settings` tab is better suited for desktop use, allowing selection by CTRL-clicking and range-selection with SHIFT-click, all without affecting regular clicking
|
||||
@@ -766,6 +805,7 @@ to show `/icons/exe.png` and `/icons/elf.gif` as the thumbnail for all `.exe` an
|
||||
|
||||
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)
|
||||
* 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:
|
||||
|
||||
@@ -821,9 +861,11 @@ you can also zip a selection of files or folders by clicking them in the browser
|
||||
|
||||
cool trick: download a folder by appending url-params `?tar&opus` or `?tar&mp3` to transcode all audio files (except aac|m4a|mp3|ogg|opus|wma) to opus/mp3 before they're added to the archive
|
||||
* super useful if you're 5 minutes away from takeoff and realize you don't have any music on your phone but your server only has flac files and downloading those will burn through all your data + there wouldn't be enough time anyways
|
||||
* and url-param `&name=foo` changes the name of the toplevel folder in the archive to `foo`, and just `&name` removes the folder entirely
|
||||
* 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)
|
||||
* 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
|
||||
@@ -888,7 +930,7 @@ note that since up2k has to read each file twice, `[🎈] bup` can *theoreticall
|
||||
|
||||
if you are resuming a massive upload and want to skip hashing the files which already finished, you can enable `turbo` in the `[⚙️] config` tab, but please read the tooltip on that button
|
||||
|
||||
if the server is behind a proxy which imposes a request-size limit, you can configure up2k to sneak below the limit with server-option `--u2sz` (the default is 96 MiB to support Cloudflare)
|
||||
if the server is behind a proxy which imposes a request-size limit, you can configure up2k to sneak below the limit with server-option [--u2sz](#u2sz)
|
||||
|
||||
if you want to replace existing files on the server with new uploads by default, run with `--u2ow 2` (only works if users have the delete-permission, and can still be disabled with `🛡️` in the UI)
|
||||
|
||||
@@ -1005,6 +1047,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
|
||||
|
||||
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
|
||||
|
||||
**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.
|
||||
@@ -1168,7 +1212,7 @@ open the `[🎺]` media-player-settings tab to configure it,
|
||||
* `[flac]` converts `flac` and `wav` files into opus (if supported by browser) or mp3
|
||||
* `[aac]` converts `aac` and `m4a` files into opus (if supported by browser) or mp3
|
||||
* `[oth]` converts all other known formats into opus (if supported by browser) or mp3
|
||||
* `aac|ac3|aif|aiff|alac|alaw|amr|ape|au|dfpwm|dts|flac|gsm|it|m4a|m4b|m4r|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|ogg|okt|opus|ra|s3m|tak|tta|ulaw|wav|wma|wv|xm|xpk`
|
||||
* `aac|ac3|aif|aiff|alac|alaw|amr|ape|au|dfpwm|dts|flac|gsm|it|m4a|m4b|m4r|mka|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|ogg|okt|opus|ra|s3m|tak|tta|ulaw|wav|wma|wv|xm|xpk`
|
||||
* "transcode to":
|
||||
* `[opus]` produces an `opus` whenever transcoding is necessary (the best choice on Android and PCs)
|
||||
* `[awo]` is `opus` in a `weba` file, good for iPhones (iOS 17.5 and newer) but Apple is still fixing some state-confusion bugs as of iOS 18.2.1
|
||||
@@ -1304,7 +1348,8 @@ for the above example to work, add the commandline argument `-e2ts` to also scan
|
||||
# server config
|
||||
|
||||
using arguments or config files, or a mix of both:
|
||||
* config files (`-c some.conf`) can set additional commandline arguments; see [./docs/example.conf](docs/example.conf) and [./docs/example2.conf](docs/example2.conf)
|
||||
* using a config-file (`-c some.conf`) is best, easier to read/maintain; see [./docs/example.conf](docs/example.conf)
|
||||
* for complex and intricate setups, see how [./docs/example2.conf](docs/example2.conf) includes additional config-files/folders with `% copyparty.d` which loads [./docs/copyparty.d/some.conf](docs/copyparty.d/some.conf)
|
||||
* `kill -s USR1` (same as `systemctl reload copyparty`) to reload accounts and volumes from config files without restarting
|
||||
* or click the `[reload cfg]` button in the control-panel if the user has `a`/admin in any volume
|
||||
* changes to the `[global]` config section requires a restart to take effect
|
||||
@@ -1318,9 +1363,15 @@ using arguments or config files, or a mix of both:
|
||||
|
||||
sleep better at night by telling copyparty to periodically check whether your version has a [known vulnerability](https://github.com/9001/copyparty/security/advisories)
|
||||
|
||||
this feature can be enabled by setting the global-option `--vc-url` to one of the following URLs; all of them provide the same information, so which one you choose is whatever
|
||||
* `https://api.copyparty.eu/advisories`
|
||||
* `https://api.github.com/repos/9001/copyparty/security-advisories?per_page=9`
|
||||
this feature can be enabled by setting the global-option `--vc-url` to one of the following URLs; choose what severity level you want to be notified for:
|
||||
* `https://api.copyparty.eu/advisories-panic` -- only really bad stuff, the "UPGRADE NOW" kind
|
||||
* `https://api.copyparty.eu/advisories` -- everything important / noteworthy, "upgrade when you can"
|
||||
* `https://api.copyparty.eu/advisories-all` -- *everything*, including stuff that's unlikely to affect anyone
|
||||
* `https://api.github.com/repos/9001/copyparty/security-advisories?per_page=9` -- same as `advisories-all`
|
||||
|
||||
note that `https://api.copyparty.eu/advisories` may (for example) skip some advisories rated `High` but include some `Low`; that's because an easily-reachable `Low` in a default-enabled feature is more severe than a `High` which is a theoretical bug in a contrived use of a fringe feature, but the CVE calculator would still classify that as `High`
|
||||
|
||||
if you want to use the github advisory feed but only care about advisories rated `medium`/`moderate` or higher, then global-option `--vc-sev medium` does that, but see previous paragraph
|
||||
|
||||
> to see what happens when a bad version is detected, try `--vc-url https://api.copyparty.eu/advisories-test`
|
||||
|
||||
@@ -1336,6 +1387,35 @@ config file example:
|
||||
vc-url: https://api.copyparty.eu/advisories
|
||||
vc-age: 3 # how many hours to wait between each check
|
||||
vc-exit # emergency-exit if current version is vulnerable
|
||||
vc-sev: medium # only care about severity 'Medium'/'Moderate' or higher (github-only; don't use this with api.copyparty.eu)
|
||||
```
|
||||
|
||||
|
||||
## 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
|
||||
* global-option [--rlo](https://copyparty.eu/cli/#rlo-help-page) decides what happens if the filename is taken
|
||||
* `-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
|
||||
```
|
||||
|
||||
|
||||
@@ -1561,6 +1641,15 @@ authenticate with one of the following:
|
||||
* username `$password`, password `k`
|
||||
|
||||
|
||||
## wopi server
|
||||
|
||||
edit office documents in the web-ui
|
||||
|
||||
needs a wopi client to work, and the only supported wopi client is a selfhosted collabora online server; eventually onlyoffice will probably also be supported: [issue #1574](https://github.com/9001/copyparty/issues/1574)
|
||||
|
||||
no further documentation or examples yet, [issue #1575](https://github.com/9001/copyparty/issues/1575)
|
||||
|
||||
|
||||
## browser ux
|
||||
|
||||
tweaking the ui
|
||||
@@ -1716,12 +1805,17 @@ avoid traversing into other filesystems using `--xdev` / volflag `:c,xdev`, ski
|
||||
|
||||
and/or you can `--xvol` / `:c,xvol` to ignore all symlinks leaving the volume's top directory, but still allow bind-mounts pointing elsewhere
|
||||
|
||||
* symlinks are permitted with `xvol` if they point into another volume where the user has the same level of access
|
||||
* symlinks are permitted with `xvol` if they point into another volume where the user also has some sort of access
|
||||
* usually keeps permissions from outer/initial volume, but don't take that for granted
|
||||
|
||||
these options will reduce performance; unlikely worst-case estimates are 14% reduction for directory listings, 35% for download-as-tar
|
||||
these options will reduce performance; unlikely worst-case estimates are 27% reduction for directory listings, 35% for download-as-tar
|
||||
|
||||
* if the underlying storage is [a network disk](#using-the-cloud-as-storage) then xvol/xdev will be **very** slow (not to mention pointless/useless)
|
||||
|
||||
as of copyparty v1.7.0 these options also prevent file access at runtime -- in previous versions it was just hints for the indexer
|
||||
|
||||
> **WARNING:** the `xvol` and `xdev` options should NOT be relied on for security; these are originally and primarily intended for the [e2ds indexer](#file-indexing). Certain underlying filesystems (especially FUSE-based and casefolding ones) will be problematic, and symlinks inherently have various TOCTOUs, and there's probably also bugs in copyparty where xvol/xdev is not fully enforced for certain APIs. The developer does not care about these options and they do not receive much testing.
|
||||
|
||||
### periodic rescan
|
||||
|
||||
filesystem monitoring; if copyparty is not the only software doing stuff on your filesystem, you may want to enable periodic rescans to keep the index up to date
|
||||
@@ -1745,6 +1839,23 @@ config file example:
|
||||
```
|
||||
|
||||
|
||||
## u2sz
|
||||
|
||||
configure upload-chunking; the default `64,96` is optimal for nearly every scenario, however:
|
||||
|
||||
* if you are seeing the message `bad server config; see "u2sz" in readme` then you have a proxy/CDN/whatever with a `max-request-body-size` which is too small, and you must either increase the limit in the reverseproxy config to at least 96 MiB (recommended) or reduce the target chunksize in copyparty (continue reading)
|
||||
|
||||
the global-option `--u2sz` is two numbers, `target,max` MiB of each chunk to send, aiming for `target` MiB and allowing up-to-and-including `max` MiB [depending on filesize](./docs/devnotes.md#list-of-chunk-sizes), and if you only specify one number then it sets that for both
|
||||
|
||||
cloudflare max is 96 MiB, but if you have a reverseproxy (or CDN/WAF/whatever) which is more restrictive, then try some of the following:
|
||||
|
||||
* `u2sz: 32` is still acceptable performance in most cases
|
||||
* `u2sz: 8` is the lowest recommended value; anything lower will reduce performance over long physical distances and/or shitty routes
|
||||
* `u2sz: 1` is the absolute minimum, and will be slow in many cases
|
||||
|
||||
the setting can also be changed in the web-UI; the textbox next to `date-chk` in the settings tab
|
||||
|
||||
|
||||
## upload rules
|
||||
|
||||
set upload rules using volflags, some examples:
|
||||
@@ -1847,6 +1958,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
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
in-volume (`.hist/up2k.db`, default) or somewhere else
|
||||
@@ -2047,7 +2207,7 @@ as for client-side stuff, there is [plugins for modifying UI/UX](./contrib/plugi
|
||||
|
||||
autologin based on IP range (CIDR) , using the global-option `--ipu`
|
||||
|
||||
for example, if everyone with an IP that starts with `192.168.123` should automatically log in as the user `spartacus`, then you can either specify `--ipu=192.168.123.0/24=spartacus` as a commandline option, or put this in a config file:
|
||||
for example, if everyone with an IP that starts with `192.168.123` should automatically log in as the user `spartacus`, then you can either specify `--ipu=192.168.123.0/24=spartacus` as a commandline option, same as this config file example:
|
||||
|
||||
```yaml
|
||||
[global]
|
||||
@@ -2063,7 +2223,7 @@ repeat the option to map additional subnets
|
||||
|
||||
limit a user to certain IP ranges (CIDR) , using the global-option `--ipr`
|
||||
|
||||
for example, if the user `spartacus` should get rejected if they're not connecting from an IP that starts with `192.168.123` or `172.16`, then you can either specify `--ipr=192.168.123.0/24,172.16.0.0/16=spartacus` as a commandline option, or put this in a config file:
|
||||
for example, if the user `spartacus` should get rejected if they're not connecting from an IP that starts with `192.168.123` or `172.16`, then you can either specify `--ipr=192.168.123.0/24,172.16.0.0/16=spartacus` as a commandline option, same as this config file example:
|
||||
|
||||
```yaml
|
||||
[global]
|
||||
@@ -2100,7 +2260,7 @@ but if you just want to let users change their own passwords, then you probably
|
||||
|
||||
other ways to auth by header
|
||||
|
||||
if you have a middleware which adds a header with a user identifier, for example tailscale's `Tailscale-User-Login: alice.m@forest.net` then you can automatically auth as `alice` by defining that mapping with `--idp-hm-usr '^Tailscale-User-Login^alice.m@forest.net^alice'` or the following config file:
|
||||
if you have a middleware which adds a header with a user identifier, for example tailscale's `Tailscale-User-Login: alice.m@forest.net` then you can automatically auth as `alice` by defining that mapping with `--idp-hm-usr '^Tailscale-User-Login^alice.m@forest.net^alice'` or the following config file example:
|
||||
|
||||
```yaml
|
||||
[global]
|
||||
@@ -2231,9 +2391,7 @@ if you want to change the fonts, see [./docs/rice/](./docs/rice/)
|
||||
|
||||
become a *real* webserver which people can access by just going to your IP or domain without specifying a port
|
||||
|
||||
**if you're on windows,** then you just need to add the commandline argument `-p 80,443` and you're done! nice
|
||||
|
||||
**if you're on macos,** sorry, I don't know
|
||||
**if you're on windows or macos,** then you just need to add the commandline argument `-p 80,443` and you're done! nice
|
||||
|
||||
**if you're on Linux,** you have the following 4 options:
|
||||
|
||||
@@ -2423,7 +2581,7 @@ change the association of a file extension
|
||||
|
||||
using commandline args, you can do something like `--mime gif=image/jif` and `--mime ts=text/x.typescript` (can be specified multiple times)
|
||||
|
||||
in a config file, this is the same as:
|
||||
that's the same as this config file example:
|
||||
|
||||
```yaml
|
||||
[global]
|
||||
@@ -2454,11 +2612,13 @@ buggy feature? rip it out by setting any of the following environment variables
|
||||
| -------------------- | ------------ |
|
||||
| `PRTY_NO_CTYPES` | do not use features from external libraries such as kernel32 |
|
||||
| `PRTY_NO_DB_LOCK` | do not lock session/shares-databases for exclusive access |
|
||||
| `PRTY_NO_ENVEXPAND` | do not expand environment-variables in configs and args |
|
||||
| `PRTY_NO_IFADDR` | disable ip/nic discovery by poking into your OS with ctypes |
|
||||
| `PRTY_NO_IMPRESO` | do not try to load js/css files using `importlib.resources` |
|
||||
| `PRTY_NO_IPV6` | disable some ipv6 support (should not be necessary since windows 2000) |
|
||||
| `PRTY_NO_LZMA` | disable streaming xz compression of incoming uploads |
|
||||
| `PRTY_NO_MP` | disable all use of the python `multiprocessing` module (actual multithreading, cpu-count for parsers/thumbnailers) |
|
||||
| `PRTY_NO_UNIDATA` | disable loading the unicodedata c-extension; saves 400k ram, breaks `--srch-nfkc` |
|
||||
| `PRTY_NO_SQLITE` | disable all database-related functionality (file indexing, metadata indexing, most file deduplication logic) |
|
||||
| `PRTY_NO_TLS` | disable native HTTPS support; if you still want to accept HTTPS connections then TLS must now be terminated by a reverse-proxy |
|
||||
| `PRTY_NO_TPOKE` | disable systemd-tmpfilesd avoider |
|
||||
@@ -2488,7 +2648,7 @@ if your distro/OS is not mentioned below, there might be some hints in the [«on
|
||||
|
||||
`pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/))
|
||||
|
||||
it comes with a [systemd service](./contrib/systemd/copyparty@.service) as well as a [user service](./contrib/systemd/copyparty-user.service), and expects to find a [config file](./contrib/systemd/copyparty.example.conf) in `/etc/copyparty/copyparty.conf` or `~/.config/copyparty/copyparty.conf`
|
||||
it comes with a [systemd service](./contrib/systemd/copyparty@.service) as well as a [user service](./contrib/systemd/copyparty-user.service), and expects to find a [config file](./contrib/systemd/copyparty.example.conf) in `/etc/copyparty.conf` or `~/.config/copyparty/copyparty.conf`
|
||||
|
||||
after installing, start either the system service or the user service and navigate to http://127.0.0.1:3923 for further instructions (unless you already edited the config files, in which case you are good to go, probably)
|
||||
|
||||
@@ -2500,6 +2660,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...
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
`brew install copyparty ffmpeg` -- https://formulae.brew.sh/formula/copyparty
|
||||
@@ -2517,8 +2694,6 @@ requires a [flake-enabled](https://nixos.wiki/wiki/Flakes) installation of nix
|
||||
|
||||
some recommended dependencies are enabled by default; [override the package](https://github.com/9001/copyparty/blob/hovudstraum/contrib/package/nix/copyparty/default.nix#L3-L22) if you want to add/remove some features/deps
|
||||
|
||||
`ffmpeg-full` was chosen over `ffmpeg-headless` mainly because we need `withWebp` (and `withOpenmpt` is also nice) and being able to use a cached build felt more important than optimizing for size at the time -- PRs welcome if you disagree 👍
|
||||
|
||||
|
||||
## nixos module
|
||||
|
||||
@@ -2644,6 +2819,12 @@ services.copyparty = {
|
||||
};
|
||||
# you may increase the open file limit for the process
|
||||
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 ];
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
@@ -2873,7 +3054,7 @@ below are some tweaks roughly ordered by usefulness:
|
||||
* and pypy can sometimes crash on startup with `-j0` (TODO make issue)
|
||||
|
||||
* if you are running the copyparty server **on Windows or Macos:**
|
||||
* `--casechk=n` makes it much faster, but also awakens [the usual surprises](https://github.com/9001/copyparty/issues/781) you expect from a case-insensitive filesystem
|
||||
* `--casechk=n` makes it much faster, but [reduces security](https://github.com/9001/copyparty/security/advisories/GHSA-3c88-p299-7hvr) and awakens [the usual surprises](https://github.com/9001/copyparty/issues/781) you expect from a case-insensitive filesystem
|
||||
* this is the same as `casechk: n` in a config-file
|
||||
|
||||
|
||||
@@ -2911,6 +3092,9 @@ some notes on hardening
|
||||
* the `nohtml` volflag also enables `noscript` which, on its own, prevents *most* javascript from running; enabling just `noscript` without `nohtml` makes it probably-safe (see below) to view html and svg files, but `nohtml` is necessary to block javascript in markdown documents
|
||||
* "probably-safe" because it relies on `Content-Security-Policy` so it depends on the reverseproxy to forward it, and the browser to understand it, but `nohtml` (the nuclear option) always works
|
||||
* when running behind a reverse-proxy, listen on a unix-socket for tighter access control (and more performance); see [reverse-proxy](#reverse-proxy) or [`--help-bind`](https://copyparty.eu/cli/#bind-help-page)
|
||||
* put ffmpeg into a sandbox (helps against future ffmpeg vulns) by configuring [--th-bwrap](https://copyparty.eu/cli/#g-th-bwrap) and enable it with `--use-bwrap y`
|
||||
* usually impossible when running in docker or podman
|
||||
* the default value of `--th-bwrap` (see `--help` on your own server) is autogenerated best-effort guess based on your setup; installing `bwrap` and configuring this is up to you (good luck)
|
||||
|
||||
safety profiles:
|
||||
|
||||
@@ -2954,6 +3138,23 @@ behavior that might be unexpected
|
||||
* or eliminate the problem entirely by only giving write-access to trustworthy people :^)
|
||||
|
||||
|
||||
## csp nonce
|
||||
|
||||
unauthorized javascript is blocked by means of the default [content-security-policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) which enforces "script nonces", to help prevent XSS and similar
|
||||
|
||||
usually this Just Works and is correctly configured by default; you can stop reading this section and move on, nothing to see here
|
||||
|
||||
but if you are the unlucky 0.1% with a hosting-setup funky enough for this to not work as intended, then you will also be seeing this message: `Looks like javascript is broken; see "csp nonce" in the readme`
|
||||
|
||||
there is two possible reasons why this might happen:
|
||||
|
||||
1. your browser is simply too old to understand the javascript, which is not likely, because just about every browser is supported, even internet explorer 11
|
||||
|
||||
2. something in your hosting-setup is tampering with the responses from copyparty and destroying this security feature; the culprit is probably a misconfigured reverseproxy, a CDN, a WAF, or some other mitm, but a popular offender is **"cloudflare rocket loader"**
|
||||
|
||||
you can try to **temporarily** set global-options `--csp-ui no` and `--csp-dl no` and restart copyparty to check whether it is due to reason #2. Remember to refresh the webbrowser with CTRL-SHIFT-R to check properly. If that makes the problem go away, then turn them back off (**never** keep them enabled), restart copyparty, open the `Network` tab in the webbrowser, and refresh the website; you should see the `content-security-policy` header including a nonce, for example `nonce-BxcJbvdu1z3S/ycPBjVWuA==` which should also appear inside the HTML of that response like this: `<script nonce="BxcJbvdu1z3S/ycPBjVWuA==">` -- if it doesn't then that's the problem; something in your hosting stack is dangerously misconfigured
|
||||
|
||||
|
||||
## cors
|
||||
|
||||
cross-site request config
|
||||
@@ -2980,6 +3181,8 @@ permissions `wG` (write + upget) lets users upload files and receive their own f
|
||||
|
||||
share specific folders in a volume without giving away full read-access to the rest -- the visitor only needs the `g` (get) permission to view the link
|
||||
|
||||
> ℹ️ a better alternative to this is [shares](#shares)
|
||||
|
||||
volflag `dk` generates dirkeys (per-directory accesskeys) for all folders, granting read-access to that folder; by default only that folder itself, no subfolders
|
||||
|
||||
volflag `dky` disables the actual key-check, meaning anyone can see the contents of a folder where they have `g` access, but not its subdirectories
|
||||
@@ -3015,9 +3218,10 @@ when generating hashes using `--ah-cli` for docker or systemd services, make sur
|
||||
|
||||
## https
|
||||
|
||||
both HTTP and HTTPS are accepted by default, but letting a [reverse proxy](#reverse-proxy) handle the https/tls/ssl would be better (probably more secure by default)
|
||||
both HTTP and HTTPS are accepted by default, but please ignore copyparty's built-in https/tls support and instead use a [reverse proxy](#reverse-proxy) to handle https/tls/ssl
|
||||
|
||||
copyparty doesn't speak HTTP/2 or QUIC, so using a reverse proxy would solve that as well -- but note that HTTP/1 is usually faster than both HTTP/2 and HTTP/3
|
||||
* reverseproxies do a better job following [best practices](https://cipherlist.eu/) meaning they are more secure, and probably also have higher performance
|
||||
* also, copyparty doesn't speak HTTP/2 or QUIC, so using a reverse proxy would solve that as well -- but note that HTTP/1 is usually faster than both HTTP/2 and HTTP/3
|
||||
|
||||
if [cfssl](https://github.com/cloudflare/cfssl/releases/latest) is installed, copyparty will automatically create a CA and server-cert on startup
|
||||
* the certs are written to `--crt-dir` for distribution, see `--help` for the other `--crt` options
|
||||
@@ -3029,6 +3233,11 @@ to install cfssl on windows:
|
||||
* rename them to `cfssl.exe`, `cfssljson.exe`, `cfssl-certinfo.exe`
|
||||
* put them in PATH, for example inside `c:\windows\system32`
|
||||
|
||||
if you really wanna give copyparty an existing TLS certificate then do one of the following:
|
||||
* `--no-crt --cert server.pem` where `server.pem` is a concatenation of key + cert + chain (in that order), or...
|
||||
* `--no-crt --cert server.crt --certkey server.key` where `server.key` is the key, and `server.crt` is a concatenation of cert + chain (in that order)
|
||||
* file-extensions don't matter, but all files are expected to be [PEM-style](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/res/insecure.pem)
|
||||
|
||||
|
||||
# recovering from crashes
|
||||
|
||||
@@ -3083,18 +3292,20 @@ enable [thumbnails](#thumbnails) of...
|
||||
* **HEIF pictures:** `pyvips` or `ffmpeg` or `pillow-heif`
|
||||
* **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin` or pillow v11.3+
|
||||
* **JPEG XL pictures:** `pyvips` or `ffmpeg`
|
||||
* **RAW images:** `rawpy`, plus one of `pyvips` or `Pillow` (for some formats)
|
||||
* **RAW photos:** either `libraw dcraw_emu` or `rawpy`, plus either `pyvips` or `Pillow`
|
||||
|
||||
enable sending [zeromq messages](#zeromq) from event-hooks: `pyzmq`
|
||||
|
||||
enable [smb](#smb-server) support (**not** recommended): `impacket==0.13.0`
|
||||
|
||||
`pyvips` gives higher quality thumbnails than `Pillow` and is 320% faster, using 270% more ram
|
||||
`pyvips` adds support for some additional image formats, but can use extreme amounts of RAM
|
||||
* to install `pyvips` on Linux: `sudo apt install libvips42 && python3 -m pip install --user -U pyvips`
|
||||
* to install `pyvips` on windows: `pip install --user -U "pyvips[binary]"`
|
||||
|
||||
to install FFmpeg on Windows, grab [a recent build](https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z) -- you need `ffmpeg.exe` and `ffprobe.exe` from inside the `bin` folder; copy them into `C:\Windows\System32` or any other folder that's in your `%PATH%`
|
||||
|
||||
if your ffmpeg/ffprobe binaries have nonstandard names -- such as `ffmpeg8` (macports) -- set environment variables `PRTY_FFMPEG_BIN` and `PRTY_FFPROBE_BIN` to the corret name (or full path)
|
||||
|
||||
|
||||
### dependency chickenbits
|
||||
|
||||
@@ -3109,6 +3320,7 @@ set any of the following environment variables to disable its associated optiona
|
||||
| -------------------- | ------------ |
|
||||
| `PRTY_NO_ARGON2` | disable argon2-cffi password hashing |
|
||||
| `PRTY_NO_CFSSL` | never attempt to generate self-signed certificates using [cfssl](https://github.com/cloudflare/cfssl) |
|
||||
| `PRTY_NO_DCRAW` | disable all [libraw](https://www.libraw.org/homepage)-based thumbnail support for RAW images |
|
||||
| `PRTY_NO_FFMPEG` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips |
|
||||
| `PRTY_NO_FFPROBE` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips, **metadata-scanning** must be handled by mutagen |
|
||||
| `PRTY_NO_MAGIC` | do not use [magic](https://pypi.org/project/python-magic/) for filetype detection |
|
||||
@@ -3123,7 +3335,8 @@ set any of the following environment variables to disable its associated optiona
|
||||
| `PRTY_NO_PIL_WEBP` | disable use of native webp support in Pillow |
|
||||
| `PRTY_NO_PSUTIL` | do not use [psutil](https://pypi.org/project/psutil/) for reaping stuck hooks and plugins on Windows |
|
||||
| `PRTY_NO_PYFTPD` | disable ftp(s) server ([pyftpdlib](https://pypi.org/project/pyftpdlib/)-based) |
|
||||
| `PRTY_NO_RAW` | disable all [rawpy](https://pypi.org/project/rawpy/)-based thumbnail support for RAW images |
|
||||
| `PRTY_NO_RAW` | same as `PRTY_NO_DCRAW` plus `PRTY_NO_RAWPY` |
|
||||
| `PRTY_NO_RAWPY` | disable all [rawpy](https://pypi.org/project/rawpy/)-based thumbnail support for RAW images |
|
||||
| `PRTY_NO_VIPS` | disable all [libvips](https://pypi.org/project/pyvips/)-based thumbnail support; will fallback to Pillow or ffmpeg |
|
||||
|
||||
example: `PRTY_NO_PIL=1 python3 copyparty-sfx.py`
|
||||
|
||||
+3
-2
@@ -26,12 +26,12 @@ and consider using [../docs/rclone.md](../docs/rclone.md) instead; usually a bit
|
||||
## to run this on windows:
|
||||
* install [winfsp](https://github.com/billziss-gh/winfsp/releases/latest) and [python 3](https://www.python.org/downloads/)
|
||||
* [x] add python 3.x to PATH (it asks during install)
|
||||
* `python -m pip install --user fusepy` (or grab a copy of `fuse.py` from the `connect` page on your copyparty, and keep it in the same folder)
|
||||
* `python -m pip install --user mfusepy` (or grab a copy of `mfusepy.py` from the `connect` page on your copyparty, and keep it in the same folder)
|
||||
* `python ./partyfuse.py n: http://192.168.1.69:3923/`
|
||||
|
||||
10% faster in [msys2](https://www.msys2.org/), 700% faster if debug prints are enabled:
|
||||
* `pacman -S mingw64/mingw-w64-x86_64-python{,-pip}`
|
||||
* `/mingw64/bin/python3 -m pip install --user fusepy`
|
||||
* `/mingw64/bin/python3 -m pip install --user mfusepy`
|
||||
* `/mingw64/bin/python3 ./partyfuse.py [...]`
|
||||
|
||||
you could replace winfsp with [dokan](https://github.com/dokan-dev/dokany/releases/latest), let me know if you [figure out how](https://github.com/dokan-dev/dokany/wiki/FUSE)
|
||||
@@ -78,6 +78,7 @@ cd /mnt/nas/music/.hist
|
||||
# [`prisonparty.sh`](prisonparty.sh)
|
||||
* run copyparty in a chroot, preventing any accidental file access
|
||||
* creates bindmounts for /bin, /lib, and so on, see `sysdirs=`
|
||||
* **not compatible with:** `--th-bwrap`
|
||||
|
||||
# [`bubbleparty.sh`](bubbleparty.sh)
|
||||
* run copyparty in an isolated process, preventing any accidental file access and more
|
||||
Executable
+36
@@ -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 ""
|
||||
@@ -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
|
||||
* [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
|
||||
* [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
|
||||
* [404-to-fail2ban.py](404-to-fail2ban.py) creates 404 logs for fail2ban
|
||||
|
||||
## on403
|
||||
|
||||
|
||||
@@ -36,6 +36,11 @@ these are `--xiu` hooks; unlike `xbu` and `xau` (which get executed on every sin
|
||||
* [reject-and-explain.py](reject-and-explain.py) shows a custom error-message when it rejects an upload
|
||||
* [reject-ramdisk.py](reject-ramdisk.py) rejects the upload if the destination is a ramdisk
|
||||
* this hook uses the `I` flag which makes it 140x faster, but if the plugin has a bug it may crash copyparty
|
||||
* [phonecam-sorter.py](phonecam-sorter.py) is for syncing backups of phone gallery into year/month folders
|
||||
|
||||
|
||||
# more upload stuff
|
||||
* combine [reloc-by-wark-xbu.py](reloc-by-wark-xbu.py) and [reloc-by-wark-xau.py](reloc-by-wark-xau.py) to rename uploads to the checksum of the file contents
|
||||
|
||||
|
||||
# on message
|
||||
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
_ = r"""
|
||||
purpose:
|
||||
sync/backup phone gallery (pics/vids) to a nas which runs copyparty
|
||||
|
||||
what this hook does:
|
||||
redirect photos/videos into date-based folders, named "%Y%m" according
|
||||
to filename; that is, "/cam/sort/DCIM/D027/PXL_20250828_202820075.jpg"
|
||||
will be redirected to "/cam/pics/ts/202508/PXL_20250828_202820075.jpg"
|
||||
|
||||
requires this setup on the server:
|
||||
* /cam/ is a volume with all your pics/vids
|
||||
* the phone must upload to somewhere below /cam/sort/
|
||||
* photo/video filenames match the following regex:
|
||||
^[a-z]{3,4}_([0-9]{6})[0-9]{2}_[0-9]{6}.*\.(jpg|mp4)$
|
||||
* so for example these filenames are OK:
|
||||
* PXL_20250828_202820075.jpg
|
||||
* IMG_20251123_202405.jpg
|
||||
* PANO_20180421_121245_03.t8o5.jpg
|
||||
* VID_20250307_220250-crf34.mp4
|
||||
|
||||
example server config as a volflag:
|
||||
-v /mnt/thedisk/gallery:cam:rwa,ed:c,e2ds,xbu=I,~/hooks/phonecam-sorter.py
|
||||
|
||||
example server config in a copyparty config file:
|
||||
[/cam]
|
||||
/mnt/thedisk/gallery
|
||||
accs:
|
||||
rwa: ed
|
||||
flags:
|
||||
e2ds
|
||||
xbu: I,~/hooks/phonecam-sorter.py
|
||||
|
||||
command to run on phone to sync to server, sending new files only:
|
||||
~/bin/u2c.py -te homenas.ca -a '$homenas.pw' https://10.0.0.2:3923/cam/sort/ ~/storage/dcim/Camera
|
||||
|
||||
the phone-command above assumes this setup on the phone:
|
||||
* termux or another linux-like thing with access to gallery files
|
||||
* homenas.ca is a textfile with the server's CA certificate
|
||||
* homenas.pw is a textfile with the copyparty password inside
|
||||
* ~/storage/dcim/ exists; if not, run termux-setup-storage
|
||||
* ~/bin/u2c.py exists; if not, curl -LO https://10.0.0.2:3923/.cpr/a/u2c.py && chmod 755 u2c.py
|
||||
|
||||
unfortunate consequences:
|
||||
* due to the redirection, u2c -z doesn't do anything, so every local
|
||||
file is hashed every time, wasting some time and cpu on the phone
|
||||
but otoh certainly avoids corruption on either side (phone/server)
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def main(inf):
|
||||
log = inf["log"]
|
||||
vdir, fn = os.path.split(inf["vp"])
|
||||
if not vdir.startswith("cam/sort"):
|
||||
return {"rc": 0} # 2nd call after 1st reloc; already done
|
||||
|
||||
m = re.match(r"^[a-z]{3,4}_([0-9]{6})[0-9]{2}_[0-9]{6}.*\.(jpg|mp4)$", fn.lower())
|
||||
if not m:
|
||||
return {"rc": 0} # not recognized; write to original path in /cam/sort/
|
||||
|
||||
ym, ext = m.groups()
|
||||
if ext == "jpg":
|
||||
dst = f"/cam/pics/ts/{ym}/"
|
||||
elif ext == "mp4":
|
||||
dst = f"/cam/vids/ts/{ym}/"
|
||||
else:
|
||||
return {"rc": 0}
|
||||
|
||||
ap = "%s%s" % (inf["ap"].split("/cam/sort/")[0], dst)
|
||||
if not os.path.isdir(ap):
|
||||
os.makedirs(ap)
|
||||
|
||||
ret = {"rc": 0, "reloc": {"vp": dst}}
|
||||
return ret
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
_ = r"""
|
||||
rename incoming uploads according to the "wark" (the file identifier)
|
||||
which is basically but not exactly a sha512 hash of the file contents
|
||||
|
||||
NOTE: this does NOT work with up2k uploads (dragdrop into browser);
|
||||
combine this hook with reloc-by-wark-xbu.py to fix that
|
||||
|
||||
example usage as global config:
|
||||
-e2d --xau I,c,bin/hooks/reloc-by-wark-xau.py
|
||||
|
||||
parameters explained,
|
||||
e2d = enable up2k database (mandatory for xau hooks)
|
||||
xau = execute before upload
|
||||
I = import this hook for performance; do not fork / subprocess
|
||||
c = "check"; reject upload if this hook crashes due to a bug
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:r:rw,ed:c,e2d,xau=I,c,bin/hooks/reloc-by-wark-xau.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all uploads with the params explained above)
|
||||
|
||||
example usage as a volflag in a copyparty config file:
|
||||
[/inc]
|
||||
srv/inc
|
||||
accs:
|
||||
r: *
|
||||
rw: ed
|
||||
flags:
|
||||
e2d, xau: I,c,bin/hooks/reloc-by-wark-xau.py
|
||||
"""
|
||||
|
||||
|
||||
def main(inf):
|
||||
if inf.get("wark"):
|
||||
# this is an up2k upload; nothing to be done, just bail
|
||||
return {}
|
||||
|
||||
abspath = inf["ap"] # filesystem path to the uploaded file
|
||||
|
||||
# we don't have the wark yet so need to calculate it;
|
||||
# generating a regular sha512 would of course be much easier,
|
||||
# but then filenames would be different depending on how the
|
||||
# file was uploaded (laaame) so let's do it the hard way
|
||||
|
||||
# use the standard up2k-salt which nobody ever changes:
|
||||
salt = "hunter2"
|
||||
|
||||
# to generate the wark we'll need some functions from copyparty;
|
||||
# follow the trail to the copyparty module and grab them from there:
|
||||
|
||||
import inspect
|
||||
|
||||
libpath = inspect.getfile(inf["log"])
|
||||
libpath = os.path.dirname(os.path.dirname(libpath))
|
||||
sys.path.insert(0, libpath)
|
||||
|
||||
from copyparty.up2k import up2k_hashlist_from_file, up2k_wark_from_hashlist
|
||||
|
||||
chunklist, st = up2k_hashlist_from_file(abspath)
|
||||
wark = up2k_wark_from_hashlist(salt, st.st_size, chunklist)
|
||||
|
||||
# okay nice
|
||||
# the rest of the code below is just copied from reloc-by-wark-xbu.py
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# grab the original filename from the vpath...
|
||||
vdir, fn = os.path.split(inf["vp"])
|
||||
|
||||
# ...to retain the original file extension, if any
|
||||
try:
|
||||
fn, ext = fn.rsplit(".", 1)
|
||||
except:
|
||||
ext = ""
|
||||
|
||||
# use the first 16 characters; 12 bytes of entropy,
|
||||
# roughly one collision for every 26 million files
|
||||
fn = wark[:16]
|
||||
|
||||
if ext:
|
||||
ext = ext.lower()
|
||||
fn += "." + ext
|
||||
|
||||
return {"reloc": {"fn": fn}}
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
|
||||
|
||||
_ = r"""
|
||||
rename incoming uploads according to the "wark" (the file identifier)
|
||||
which is basically but not exactly a sha512 hash of the file contents
|
||||
|
||||
NOTE: this only works for up2k uploads (dragdrop into browser);
|
||||
combine this with reloc-by-wark-xau.py to cover the other protocols
|
||||
|
||||
example usage as global config:
|
||||
-e2d --xbu I,c,bin/hooks/reloc-by-wark-xbu.py
|
||||
|
||||
parameters explained,
|
||||
e2d = enable up2k database (mandatory for xbu hooks)
|
||||
xbu = execute before upload
|
||||
I = import this hook for performance; do not fork / subprocess
|
||||
c = "check"; reject upload if this hook crashes due to a bug
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:r:rw,ed:c,e2d,xbu=I,c,bin/hooks/reloc-by-wark-xbu.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all uploads with the params explained above)
|
||||
|
||||
example usage as a volflag in a copyparty config file:
|
||||
[/inc]
|
||||
srv/inc
|
||||
accs:
|
||||
r: *
|
||||
rw: ed
|
||||
flags:
|
||||
e2d, xbu: I,c,bin/hooks/reloc-by-wark-xbu.py
|
||||
"""
|
||||
|
||||
|
||||
def main(inf):
|
||||
wark = inf.get("wark")
|
||||
if not wark:
|
||||
# not an up2k upload, so we don't have the hash;
|
||||
|
||||
# option 1: let upload proceed with original filename
|
||||
return {}
|
||||
|
||||
# option 2: reject the upload
|
||||
return {"rejectmsg": "only up2k uploads are allowed in this volume"}
|
||||
|
||||
# grab the original filename from the vpath...
|
||||
vdir, fn = os.path.split(inf["vp"])
|
||||
|
||||
# ...to retain the original file extension, if any
|
||||
try:
|
||||
fn, ext = fn.rsplit(".", 1)
|
||||
except:
|
||||
ext = ""
|
||||
|
||||
# use the first 16 characters; 12 bytes of entropy,
|
||||
# roughly one collision for every 26 million files
|
||||
fn = wark[:16]
|
||||
|
||||
if ext:
|
||||
ext = ext.lower()
|
||||
fn += "." + ext
|
||||
|
||||
return {"reloc": {"fn": fn}}
|
||||
+16
-10
@@ -21,7 +21,7 @@ usage:
|
||||
python partyfuse.py http://192.168.1.69:3923/ ./music
|
||||
|
||||
dependencies:
|
||||
python3 -m pip install --user fusepy # or grab it from the connect page
|
||||
python3 -m pip install --user mfusepy # or grab it from the connect page
|
||||
+ on Linux: sudo apk add fuse
|
||||
+ on Macos: https://osxfuse.github.io/
|
||||
+ on Windows: https://github.com/billziss-gh/winfsp/releases/latest
|
||||
@@ -92,8 +92,13 @@ is_dbg = False
|
||||
|
||||
|
||||
try:
|
||||
from fuse import FUSE, FuseOSError, Operations
|
||||
from mfusepy import FUSE, FuseOSError, Operations
|
||||
except:
|
||||
try:
|
||||
from fuse import FUSE, FuseOSError, Operations
|
||||
except:
|
||||
FUSE = None
|
||||
|
||||
if WINDOWS:
|
||||
libfuse = "install https://github.com/billziss-gh/winfsp/releases/latest"
|
||||
elif MACOS:
|
||||
@@ -102,12 +107,13 @@ except:
|
||||
libfuse = "apt install libfuse2\n modprobe fuse"
|
||||
|
||||
m = """\033[33m
|
||||
could not import fuse; these may help:
|
||||
{} -m pip install --user fusepy
|
||||
could not import mfusepy; these may help:
|
||||
{} -m pip install --user mfusepy
|
||||
{}
|
||||
\033[0m"""
|
||||
print(m.format(sys.executable, libfuse))
|
||||
raise
|
||||
if not FUSE:
|
||||
print(m.format(sys.executable, libfuse))
|
||||
raise
|
||||
|
||||
|
||||
def termsafe(txt):
|
||||
@@ -143,10 +149,10 @@ def fancy_log(fmt, *a):
|
||||
|
||||
|
||||
def register_wtf8():
|
||||
def wtf8_enc(text):
|
||||
def wtf8_enc(text, errors=""):
|
||||
return str(text).encode("utf-8", "surrogateescape"), len(text)
|
||||
|
||||
def wtf8_dec(binary):
|
||||
def wtf8_dec(binary, errors=""):
|
||||
return bytes(binary).decode("utf-8", "surrogateescape"), len(binary)
|
||||
|
||||
def wtf8_search(encoding_name):
|
||||
@@ -620,7 +626,7 @@ class Gateway(object):
|
||||
"st_atime": ts,
|
||||
"st_mtime": ts,
|
||||
"st_ctime": ts,
|
||||
"st_blocks": int((sz + 511) / 512),
|
||||
"st_blocks": -int(-sz // 512),
|
||||
}
|
||||
|
||||
def stat_file(self, ts, sz):
|
||||
@@ -632,7 +638,7 @@ class Gateway(object):
|
||||
"st_atime": ts,
|
||||
"st_mtime": ts,
|
||||
"st_ctime": ts,
|
||||
"st_blocks": int((sz + 511) / 512),
|
||||
"st_blocks": -int(-sz // 512),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ example:
|
||||
example for running straight from source (instead of using an sfx):
|
||||
PYTHONPATH=$PWD ./prisonparty.sh /var/lib/copyparty-jail cpp cpp /mnt/nas/music -- python3 -um copyparty -v /mnt/nas/music::rwmd
|
||||
|
||||
NOT COMPATIBLE WITH: --th-bwrap
|
||||
|
||||
note that if you have python modules installed as --user (such as bpm/key detectors),
|
||||
you should add /home/foo/.local as a VOLDIR
|
||||
|
||||
@@ -97,6 +99,7 @@ jail="${jail%/}"
|
||||
|
||||
|
||||
# bind-mount system directories and volumes
|
||||
mkdir -p "$jail"
|
||||
for a in {1..30}; do mkdir "$jail/.prisonlock" && break; sleep 0.1; done
|
||||
printf '%s\n' "${sysdirs[@]}" "${vols[@]}" | sed -r 's`/$``' | LC_ALL=C sort | uniq |
|
||||
while IFS= read -r v; do
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# thumbnail extractors
|
||||
|
||||
extract (or generate) thumbnails for custom file formats or override default thumbnail extraction for standard media files
|
||||
|
||||
|
||||
## usage
|
||||
|
||||
* load plugins with `--th-extract baz=~/dev/copyparty/bin/thumbs/randomcolor.py`
|
||||
* `baz` is the file extension (may be comma-separated list of extensions)
|
||||
* add multiple different extractor plugins by repeating the `--th-extract` argument
|
||||
|
||||
|
||||
## api
|
||||
|
||||
### in
|
||||
|
||||
each plugin must define a function `main(abspath, **kwargs)`:
|
||||
* `abspath` is the path to a file to extract thumbnail for
|
||||
* `kwargs` currently receives these keyword arguments:
|
||||
* `vn` is the VFS which contains the requested file
|
||||
* `th_srv` is an instance of [copyparty/th_srv](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/th_srv.py)
|
||||
|
||||
### out
|
||||
|
||||
the `main()` function must
|
||||
* return `None` or raise an `Exception` in case it is unable to extract thumbnail
|
||||
* return tuple `fmt, stream, offset, whence, size` otherwise:
|
||||
* `fmt: str` – format (filename extension) of the thumbnail; should be one of the image formats supported by copyparty (see `--th-r-*` options)
|
||||
* `stream: IO[bytes]` – binary file-like object supporting `seek()`, `read()`, `close()` methods; should contain extracted thumbnail image
|
||||
* `offset: int, whence: int` – arguments to `stream.seek()`; copyparty will call `stream.seek(offset, whence)` once before starting reading the stream contents
|
||||
* `size: int | None` – number of bytes to read from the `stream`; if `size` is negative or `None`, copyparty will read until the end of the stream
|
||||
|
||||
|
||||
### notes
|
||||
|
||||
it is possible to extract or generate custom thumbnails for standard media files too,
|
||||
e.g. run copyparty with `--th-extract mp3,m4a,aac,flac,opus=~/dev/copyparty/bin/thumbs/my_custom_mp3_th_extractor.py` and do your magic with audio file thumbnails
|
||||
|
||||
in case copyparty doesn't like the `fmt` option, it will `close()` the `stream` without ever calling `seek()` or `read()`
|
||||
|
||||
if your scenario involves heavy computation or i/o, it is better to defer the heavy parts until the first `seek()` or `read()` to avoid redundant work in case the result gets rejected based on `fmt` value
|
||||
|
||||
if you provide custom `stream` object:
|
||||
copyparty performs buffered reading, so expect multiple `read()` calls, respect `size` argument in `read(size)` calls, and maintain correct seek position
|
||||
|
||||
|
||||
## examples
|
||||
|
||||
* [randomcolor](https://github.com/9001/copyparty/blob/hovudstraum/bin/thumbs/randomcolor.py) generates a random .gif image
|
||||
* [collabora](https://github.com/9001/copyparty/blob/hovudstraum/bin/thumbs/collabora.py) uses external self-hosted service (collabora online) to generate previews for various office documents, pdf and fb2 books, html, markdown, text files and some image formats; collabora is also supported as office editor in copyparty, see [wopi](https://github.com/9001/copyparty#wopi-server)
|
||||
|
||||
|
||||
## some other known plugins seen on the internets
|
||||
|
||||
* [fpkg_thumb](https://github.com/kamaeff/fpkg-vault/blob/1c248de62a9ff9c4088d4b3e7962023cde9d6426/old/fpkg_thumb.py) extracts cover images from playstation4 software installation packages ("pkg" and "fpkg" files)
|
||||
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
CAUTION!
|
||||
looks like collabora spawns separate jailed libreoffice instance to do the conversion
|
||||
this may eat up too much ram in folders with many docs or with --th-pregen enabled
|
||||
|
||||
consider limiting resources available for collabora
|
||||
e.g. for me these lines in docker-compose.yml helped alot:
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2'
|
||||
memory: '2G'
|
||||
/CAUTION!
|
||||
|
||||
if you use collabora office editor with copyparty, you already have a thumbnailer for office formats:
|
||||
https://sdk.collaboraonline.com/docs/conversion_api.html
|
||||
the 'convert-to' api accepts files in multipart/form-data format
|
||||
|
||||
just add this extractor to copyparty with an argument like this:
|
||||
--th-extract=docx,xlsx,pptx,odt,ods,pdf,fb2=~/dev/copyparty/bin/thumbs/collabora.py
|
||||
and set COLLABORA_INSTANCE_URL below to your collabora instace url
|
||||
|
||||
full list of supported file types:
|
||||
https://sdk.collaboraonline.com/docs/conversion_api.html#supported-input-formats
|
||||
|
||||
this is also an example of deferred IO:
|
||||
no files opened and no network requests made if copyparty doesn't like the file type
|
||||
"""
|
||||
|
||||
import os
|
||||
import string
|
||||
import random
|
||||
import mimetypes
|
||||
from urllib.error import HTTPError
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
COLLABORA_INSTANCE_URL = "http://collabora:9980"
|
||||
BOUNDARY_CHARS = string.ascii_letters + string.digits
|
||||
|
||||
|
||||
def main(abspath, **kwargs):
|
||||
boundary = "CopypartyFormData" + "".join(random.choices(BOUNDARY_CHARS, k=53))
|
||||
start, end = make_headers(abspath, boundary)
|
||||
size = len(start) + os.path.getsize(abspath) + len(end)
|
||||
request = Request(
|
||||
COLLABORA_INSTANCE_URL + "/cool/convert-to/png",
|
||||
method="POST",
|
||||
data=iterate(start, abspath, end),
|
||||
headers={
|
||||
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
||||
"Content-Length": str(size),
|
||||
},
|
||||
)
|
||||
|
||||
file_like = Wrapper(request)
|
||||
offset, whence, length = 0, 0, None
|
||||
|
||||
return "png", file_like, offset, whence, length
|
||||
|
||||
|
||||
def make_headers(abspath, boundary):
|
||||
"""stuff to wrap file contents by for multipart"""
|
||||
|
||||
fname = "file." + abspath.rsplit(".", 1)[-1]
|
||||
ftype = mimetypes.guess_file_type(fname)[0] or "application/octet-stream"
|
||||
start = (
|
||||
f"--{boundary}\r\n"
|
||||
f'Content-Disposition: form-data; name="data"; filename="{fname}"\r\n'
|
||||
f"Content-Type: {ftype}\r\n\r\n"
|
||||
)
|
||||
end = f"\r\n--{boundary}--"
|
||||
return bytes(start, "ascii"), bytes(end, "ascii")
|
||||
|
||||
|
||||
def iterate(start, fpath, end):
|
||||
"""docs may be big so read and send them in chunks"""
|
||||
|
||||
yield start
|
||||
with open(fpath, "rb") as f:
|
||||
while True:
|
||||
b = f.read(32768)
|
||||
if not b:
|
||||
break
|
||||
yield b
|
||||
yield end
|
||||
|
||||
|
||||
class Wrapper:
|
||||
"""
|
||||
This file-like wrapper defers expensive work until first .seek() call
|
||||
Useful to not waste resources in case copyparty decides
|
||||
to throw our thumbnail away based on returned file extension
|
||||
"""
|
||||
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
self.response = None
|
||||
|
||||
def seek(self, offset, whence=0):
|
||||
if self.response:
|
||||
return 0
|
||||
|
||||
try:
|
||||
# the heavy part:
|
||||
# - reading probably big file
|
||||
# - sending it over the network
|
||||
# - making collabora do some work
|
||||
self.response = urlopen(self.request, timeout=10)
|
||||
except HTTPError as e:
|
||||
e.close()
|
||||
raise e
|
||||
except Exception as e:
|
||||
self.close()
|
||||
raise e
|
||||
|
||||
# actual response is not seekable
|
||||
# just return 0
|
||||
return 0
|
||||
|
||||
def read(self, amount):
|
||||
return self.response.read(amount)
|
||||
|
||||
def close(self):
|
||||
if self.response:
|
||||
self.response.close()
|
||||
@@ -0,0 +1,29 @@
|
||||
import io
|
||||
import os
|
||||
import struct
|
||||
from base64 import b64decode
|
||||
|
||||
|
||||
def create_gif() -> bytes:
|
||||
head = r"R0lGODdhIAAgAIAB"
|
||||
tail = r"AAD/LAAAAAAgACAAAAIehI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC5sFADs="
|
||||
#color = struct.pack(">L", random.randrange(0xffffff)) # wow, way too stable
|
||||
color = b"\x00" + os.urandom(3)
|
||||
return b64decode(head) + color + b64decode(tail)
|
||||
|
||||
|
||||
def main(abspath, **ka) -> Optional[tuple[str, IO[bytes], int, int, Optional[int]]]:
|
||||
print("hello from randomcolor.py pretending to extract a thumb from " + abspath)
|
||||
|
||||
# need to return a file-like object
|
||||
gif_bytes = create_gif()
|
||||
file_obj = io.BytesIO(gif_bytes)
|
||||
|
||||
# where copyparty should seek to in the "file" to start reading from
|
||||
seek_offset = 0
|
||||
seek_whence = 0
|
||||
|
||||
filesize = None # auto; read until end
|
||||
|
||||
# "gif" is the file-extension to decode the file as
|
||||
return "gif", file_obj, seek_offset, seek_whence, filesize
|
||||
+59
-20
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
S_VERSION = "2.19"
|
||||
S_BUILD_DT = "2026-01-18"
|
||||
S_VERSION = "2.25"
|
||||
S_BUILD_DT = "2026-10-05"
|
||||
|
||||
"""
|
||||
u2c.py: upload to copyparty
|
||||
@@ -67,6 +67,8 @@ else:
|
||||
|
||||
WTF8 = "replace" if PY2 else "surrogateescape"
|
||||
|
||||
SEP = "{0}".format(os.sep).encode("ascii")
|
||||
|
||||
VT100 = platform.system() != "Windows"
|
||||
|
||||
|
||||
@@ -249,7 +251,7 @@ class File(object):
|
||||
|
||||
def __init__(self, top, rel, size, lmod):
|
||||
self.top = top # type: bytes
|
||||
self.rel = rel.replace(b"\\", b"/") # type: bytes
|
||||
self.rel = rel.replace(SEP, b"/") # type: bytes
|
||||
self.size = size # type: int
|
||||
self.lmod = lmod # type: float
|
||||
|
||||
@@ -267,6 +269,7 @@ class File(object):
|
||||
self.wark = "" # type: str
|
||||
self.url = "" # type: str
|
||||
self.nhs = 0 # type: int
|
||||
self.t_hs = 0.0 # type: float
|
||||
|
||||
# set by upload
|
||||
self.t0_up = 0.0 # type: float
|
||||
@@ -398,7 +401,7 @@ class MTHash(object):
|
||||
self.csz = chunksz
|
||||
|
||||
chunks = {}
|
||||
nchunks = int(math.ceil(fsz / chunksz))
|
||||
nchunks = -int(-fsz // chunksz)
|
||||
for nch in range(nchunks):
|
||||
self.work_q.put(nch)
|
||||
|
||||
@@ -652,7 +655,7 @@ def walkdir(err, top, excl, seen):
|
||||
|
||||
def walkdirs(err, tops, excl):
|
||||
"""recursive statdir for a list of tops, yields [top, relpath, stat]"""
|
||||
sep = "{0}".format(os.sep).encode("ascii")
|
||||
sep = SEP
|
||||
if not VT100:
|
||||
excl = excl.replace("/", r"\\")
|
||||
za = []
|
||||
@@ -725,7 +728,7 @@ def up2k_chunksize(filesize):
|
||||
stepsize = 512 * 1024
|
||||
while True:
|
||||
for mul in [1, 2]:
|
||||
nchunks = math.ceil(filesize * 1.0 / chunksize)
|
||||
nchunks = -int(-filesize // chunksize)
|
||||
if nchunks <= 256 or (chunksize >= 32 * 1024 * 1024 and nchunks <= 4096):
|
||||
return chunksize
|
||||
|
||||
@@ -733,16 +736,22 @@ def up2k_chunksize(filesize):
|
||||
stepsize *= mul
|
||||
|
||||
|
||||
# mostly from copyparty/up2k.py
|
||||
def get_hashlist(file, pcb, mth):
|
||||
# type: (File, Any, Any) -> None
|
||||
with open(file.abs, "rb", 512 * 1024) as f:
|
||||
get_hashlist_f(file, pcb, mth, f)
|
||||
|
||||
|
||||
# mostly from copyparty/up2k.py
|
||||
def get_hashlist_f(file, pcb, mth, f):
|
||||
# type: (File, Any, Any, Any) -> None
|
||||
"""generates the up2k hashlist from file contents, inserts it into `file`"""
|
||||
|
||||
chunk_sz = up2k_chunksize(file.size)
|
||||
file_rem = file.size
|
||||
file_ofs = 0
|
||||
ret = []
|
||||
with open(file.abs, "rb", 512 * 1024) as f:
|
||||
if True:
|
||||
t0 = time.time()
|
||||
|
||||
if mth and file.size >= 1024 * 512:
|
||||
@@ -778,6 +787,18 @@ def get_hashlist(file, pcb, mth):
|
||||
file.kchunks[k] = [v1, v2]
|
||||
|
||||
|
||||
def wark_stdin(ar):
|
||||
file = File(b"", b"", int(ar.files[0]), 0)
|
||||
get_hashlist_f(file, None, False, sys.stdin if PY2 else sys.stdin.buffer)
|
||||
if ar.chs:
|
||||
zsl = ["%s %d %d" % (zsii[0], n, zsii[1]) for n, zsii in enumerate(file.cids)]
|
||||
print("chs:\n" + "\n".join(zsl))
|
||||
zsl = [ar.wsalt, str(file.size)] + [x[0] for x in file.cids]
|
||||
zb = hashlib.sha512("\n".join(zsl).encode("utf-8")).digest()[:33]
|
||||
wark = ub64enc(zb).decode("utf-8")
|
||||
print(wark)
|
||||
|
||||
|
||||
def printlink(ar, purl, name, fk):
|
||||
if not name:
|
||||
url = purl # srch
|
||||
@@ -886,6 +907,7 @@ def handshake(ar, file, search):
|
||||
file.url = quotep(r["purl"].encode("utf-8", WTF8)).decode("utf-8")
|
||||
file.name = r["name"]
|
||||
file.wark = r["wark"]
|
||||
file.t_hs = time.time()
|
||||
|
||||
if ar.uon and not r["hash"]:
|
||||
printlink(ar, file.url, r["name"], r.get("fk"))
|
||||
@@ -1009,7 +1031,7 @@ class Ctl(object):
|
||||
self.up_b = 0 # num bytes handled
|
||||
self.up_br = 0 # num bytes actually transferred
|
||||
self.uploader_busy = 0
|
||||
self.serialized = False
|
||||
self.oneconn = False
|
||||
|
||||
self.t0 = time.time()
|
||||
self.t0_up = None
|
||||
@@ -1184,7 +1206,7 @@ class Ctl(object):
|
||||
handshake(self.ar, file, False)
|
||||
|
||||
def cleanup_vt100(self):
|
||||
if VT100:
|
||||
if VT100 and not self.ar.ns:
|
||||
ss.scroll_region(None)
|
||||
else:
|
||||
eprint("\033]9;4;0\033\\")
|
||||
@@ -1195,14 +1217,15 @@ class Ctl(object):
|
||||
|
||||
def hasher(self):
|
||||
ptn = re.compile(self.ar.x.encode("utf-8"), re.I) if self.ar.x else None
|
||||
sep = "{0}".format(os.sep).encode("ascii")
|
||||
usep = os.sep
|
||||
sep = SEP
|
||||
prd = None
|
||||
ls = {}
|
||||
for top, rel, inf in self.filegen:
|
||||
isdir = stat.S_ISDIR(inf.st_mode)
|
||||
if self.ar.z or self.ar.drd:
|
||||
rd = rel if isdir else os.path.dirname(rel)
|
||||
srd = rd.decode("utf-8", "replace").replace("\\", "/").rstrip("/")
|
||||
srd = rd.decode("utf-8", "replace").replace(usep, "/").rstrip("/")
|
||||
if srd:
|
||||
srd += "/"
|
||||
if prd != rd:
|
||||
@@ -1212,7 +1235,7 @@ class Ctl(object):
|
||||
print(" ls ~{0}".format(srd))
|
||||
zt = (
|
||||
self.ar.vtop,
|
||||
quotep(rd.replace(b"\\", b"/")).decode("utf-8"),
|
||||
quotep(rd.replace(sep, b"/")).decode("utf-8"),
|
||||
)
|
||||
sc, txt = web.req("GET", "%s%s?ls<&dots" % zt, {})
|
||||
if sc >= 400:
|
||||
@@ -1311,7 +1334,8 @@ class Ctl(object):
|
||||
if self.ar.jw:
|
||||
print("%s %s" % (wark, vp))
|
||||
else:
|
||||
zd = datetime.datetime.fromtimestamp(max(0, file.lmod), UTC)
|
||||
tsdt = datetime.datetime.fromtimestamp
|
||||
zd = tsdt(max(0, min(2 << 36, file.lmod)), UTC)
|
||||
dt = "%04d-%02d-%02d %02d:%02d:%02d" % (
|
||||
zd.year,
|
||||
zd.month,
|
||||
@@ -1384,10 +1408,10 @@ class Ctl(object):
|
||||
self.recheck.append(file)
|
||||
|
||||
with self.mutex:
|
||||
if hs and not sprs and not self.serialized:
|
||||
t = "server filesystem does not support sparse files; serializing uploads\n"
|
||||
if hs and not sprs and not self.oneconn:
|
||||
t = "server filesystem does not support sparse files; cannot parallelize uploads\n"
|
||||
eprint(t)
|
||||
self.serialized = True
|
||||
self.oneconn = True
|
||||
for _ in range(self.ar.j - 1):
|
||||
self.q_upload.put(None)
|
||||
if not hs:
|
||||
@@ -1399,7 +1423,7 @@ class Ctl(object):
|
||||
if not file.recheck:
|
||||
self.up_done(file)
|
||||
|
||||
if hs and file.up_c:
|
||||
if hs and file.up_c and not self.oneconn:
|
||||
# some chunks failed
|
||||
self.up_c -= len(hs)
|
||||
file.up_c -= len(hs)
|
||||
@@ -1457,6 +1481,7 @@ class Ctl(object):
|
||||
self.q_upload.put(fsl)
|
||||
|
||||
def uploader(self):
|
||||
drop_hs = (0.0, "")
|
||||
while True:
|
||||
fsl = self.q_upload.get()
|
||||
if not fsl:
|
||||
@@ -1475,6 +1500,9 @@ class Ctl(object):
|
||||
cids = fsl.cids
|
||||
self.last_file = file
|
||||
|
||||
if self.oneconn and drop_hs == (file.t_hs, file.wark):
|
||||
continue
|
||||
|
||||
with self.mutex:
|
||||
if not self.uploader_busy:
|
||||
self.at_upr = time.time()
|
||||
@@ -1500,6 +1528,9 @@ class Ctl(object):
|
||||
t = "upload failed, retrying: %s #%s+%d (%s)\n"
|
||||
eprint(t % (file.name, cids[0][:8], len(cids) - 1, ex))
|
||||
file.cd = time.time() + self.ar.cd
|
||||
if self.oneconn:
|
||||
file.ucids = []
|
||||
drop_hs = (file.t_hs, file.wark)
|
||||
# handshake will fix it
|
||||
|
||||
with self.mutex:
|
||||
@@ -1557,7 +1588,7 @@ NOTE: if server has --usernames enabled, then password is "username:password"
|
||||
ap.add_argument("url", type=unicode, help="server url, including destination folder")
|
||||
ap.add_argument("files", type=files_decoder, nargs="+", help="files and/or folders to process")
|
||||
ap.add_argument("-v", action="store_true", help="verbose")
|
||||
ap.add_argument("-a", metavar="PASSWD", default="", help="password (or $filepath) for copyparty (is sent in header 'PW')")
|
||||
ap.add_argument("-a", metavar="PASSWD", default="", help="password (or $filepath) for copyparty (is sent in header 'PW'), default is env.var U2C_PW")
|
||||
ap.add_argument("--ba", metavar="PASSWD", default="", help="password (or $filepath) for basic-auth (usually not necessary)")
|
||||
ap.add_argument("-s", action="store_true", help="file-search (disables upload)")
|
||||
ap.add_argument("-x", type=unicode, metavar="REGEX", action="append", help="skip file if filesystem-abspath matches REGEX (option can be repeated), example: '.*/\\.hist/.*'")
|
||||
@@ -1586,6 +1617,7 @@ NOTE: if server has --usernames enabled, then password is "username:password"
|
||||
ap.add_argument("--wsalt", type=unicode, metavar="S", default="hunter2", help="salt to use when creating warks; must match server config")
|
||||
ap.add_argument("--chs", action="store_true", help="verbose (print the hash/offset of each chunk in each file)")
|
||||
ap.add_argument("--jw", action="store_true", help="just identifier+filepath, not mtime/size too")
|
||||
ap.add_argument("--stdin", action="store_true", help="calculate file-ID of stdin; u2c.py --stdin - $LEN < a.mkv")
|
||||
|
||||
ap = app.add_argument_group("performance tweaks")
|
||||
ap.add_argument("-j", type=int, metavar="CONNS", default=2, help="parallel connections")
|
||||
@@ -1616,6 +1648,10 @@ NOTE: if server has --usernames enabled, then password is "username:password"
|
||||
except:
|
||||
pass
|
||||
|
||||
if ar.stdin:
|
||||
wark_stdin(ar)
|
||||
return
|
||||
|
||||
# msys2 doesn't uncygpath absolute paths with whitespace
|
||||
if not VT100:
|
||||
zsl = []
|
||||
@@ -1705,6 +1741,9 @@ NOTE: if server has --usernames enabled, then password is "username:password"
|
||||
with open(zs[1:], "rb") as f:
|
||||
setattr(ar, k, f.read().decode("utf-8").strip())
|
||||
|
||||
if not ar.a:
|
||||
ar.a = os.environ.get("U2C_PW", "")
|
||||
|
||||
if ar.ba:
|
||||
ar.ba = "Basic " + base64.b64encode(ar.ba.encode("utf-8")).decode("utf-8")
|
||||
|
||||
|
||||
@@ -17,8 +17,12 @@ defaults
|
||||
|
||||
listen foo1
|
||||
bind *:8081
|
||||
http-request add-header X-Forwarded-Proto https if { ssl_fc }
|
||||
http-request add-header X-Forwarded-Proto http unless { ssl_fc }
|
||||
server srv1 127.0.0.1:3923 maxconn 512
|
||||
|
||||
listen foo2
|
||||
bind *:8082
|
||||
http-request add-header X-Forwarded-Proto https if { ssl_fc }
|
||||
http-request add-header X-Forwarded-Proto http unless { ssl_fc }
|
||||
server srv1 /dev/shm/party.sock maxconn 512
|
||||
@@ -75,9 +75,8 @@ server {
|
||||
# recommendation: replace cpp_tcp with cpp_uds below
|
||||
proxy_pass http://cpp_tcp;
|
||||
proxy_redirect off;
|
||||
# disable buffering (next 4 lines)
|
||||
# disable buffering (next 3 lines)
|
||||
proxy_http_version 1.1;
|
||||
client_max_body_size 0;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
# improve download speed from 600 to 1500 MiB/s
|
||||
|
||||
@@ -285,6 +285,7 @@ in
|
||||
|
||||
environment = {
|
||||
PYTHONUNBUFFERED = "true";
|
||||
NIX_SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
|
||||
XDG_CONFIG_HOME = externalStateDir;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead.
|
||||
|
||||
pkgname=copyparty
|
||||
pkgver="1.20.11"
|
||||
pkgver="1.20.25"
|
||||
pkgrel=1
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||
arch=("any")
|
||||
@@ -13,23 +13,22 @@ depends=("bash" "python" "lsof" "python-jinja")
|
||||
makedepends=("python-wheel" "python-setuptools" "python-build" "python-installer" "make" "pigz")
|
||||
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
|
||||
"cfssl: generate TLS certificates on startup"
|
||||
"python-mutagen: music tags (alternative)"
|
||||
"python-paramiko: sftp server",
|
||||
"python-mutagen: music tags (fast/inaccurate)"
|
||||
"python-pillow: thumbnails for images"
|
||||
"python-pyvips: thumbnails for images (higher quality, faster, uses more ram)"
|
||||
"python-pyvips: thumbnails for images (eats ram, prefer pillow)"
|
||||
"python-paramiko: sftp server"
|
||||
"python-pyftpdlib: ftp server"
|
||||
"python-pyopenssl: ftps server (needs pyftpdlib too)"
|
||||
"libkeyfinder: detection of musical keys"
|
||||
"python-pyopenssl: ftps functionality"
|
||||
"python-pyzmq: send zeromq messages from event-hooks"
|
||||
"python-argon2-cffi: hashed passwords in config"
|
||||
)
|
||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||
backup=("etc/${pkgname}/copyparty.conf" )
|
||||
sha256sums=("4ad301adf2cd1ff0d65573e4e93af97fa85c1b9eed1207625e99afc5a5ca6213")
|
||||
backup=("etc/copyparty.conf")
|
||||
install=copyparty.install
|
||||
sha256sums=("82c68dfa42e08a59757bbfa0b1d140032141175347906e056bef83904f5c2c70")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
|
||||
make
|
||||
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
python -m build --wheel --no-isolation
|
||||
}
|
||||
@@ -38,12 +37,11 @@ package() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
python -m installer --destdir="$pkgdir" dist/*.whl
|
||||
|
||||
install -dm755 "${pkgdir}/etc/${pkgname}"
|
||||
install -Dm755 "bin/prisonparty.sh" "${pkgdir}/usr/bin/prisonparty"
|
||||
install -Dm644 "contrib/systemd/${pkgname}.conf" "${pkgdir}/etc/${pkgname}/copyparty.conf"
|
||||
install -Dm644 "contrib/systemd/${pkgname}.conf" "${pkgdir}/etc/copyparty.conf"
|
||||
install -Dm644 "contrib/systemd/${pkgname}@.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}@.service"
|
||||
install -Dm644 "contrib/systemd/${pkgname}-user.service" "${pkgdir}/usr/lib/systemd/user/${pkgname}.service"
|
||||
install -Dm644 "contrib/systemd/prisonparty@.service" "${pkgdir}/usr/lib/systemd/system/prisonparty@.service"
|
||||
install -Dm644 "contrib/systemd/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md"
|
||||
install -Dm644 "contrib/systemd/index.md" "${pkgdir}/var/lib/${pkgname}-jail/srv/README.md"
|
||||
install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
post_upgrade() {
|
||||
|
||||
# v1.20.22 moved the config-file, affecting the system-service only
|
||||
if [[ $(vercmp "$2" 1.20.22) -lt 0 ]]; then
|
||||
|
||||
oldpath="/etc/copyparty/copyparty.conf"
|
||||
newpath="/etc/copyparty.conf"
|
||||
|
||||
echo "==> Migrating config-file from $oldpath to $newpath"
|
||||
|
||||
if [[ ! -f "$oldpath" ]]; then
|
||||
oldpath="$oldpath.pacsave"
|
||||
fi
|
||||
|
||||
if [[ -f "$newpath.pacnew" ]]; then
|
||||
echo "==> WARNING: A config-file already existed in the new location [$newpath]. The existing file will be used as-is."
|
||||
|
||||
elif [[ -f "$oldpath" ]]; then
|
||||
mv -v "$oldpath" "$newpath"
|
||||
rmdir /etc/copyparty 2>/dev/null || true
|
||||
fi
|
||||
|
||||
systemctl cat copyparty@.service | awk '/=PRTY_CONFIG=/ && !/=\/etc\/copyparty.conf/ {exit 1}' || {
|
||||
echo "==> WARNING: The copyparty system-service has been edited by hand since installation, so it will not read config from the new default config path [$newpath]. Please use PRTY_CONFIG=$newpath if that is correct for your setup."
|
||||
}
|
||||
fi
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
pkgname=copyparty
|
||||
pkgver=1.20.11
|
||||
pkgver=1.20.25
|
||||
pkgrel=1
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||
arch=("any")
|
||||
@@ -21,12 +21,9 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
|
||||
)
|
||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||
backup=("/etc/${pkgname}.d/init" )
|
||||
sha256sums=("4ad301adf2cd1ff0d65573e4e93af97fa85c1b9eed1207625e99afc5a5ca6213")
|
||||
sha256sums=("82c68dfa42e08a59757bbfa0b1d140032141175347906e056bef83904f5c2c70")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
|
||||
make
|
||||
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
python -m build --wheel --no-isolation
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ Description=copyparty file server
|
||||
Type=notify
|
||||
SyslogIdentifier=copyparty
|
||||
Environment=PYTHONUNBUFFERED=x
|
||||
WorkingDirectory=/var/lib/copyparty-jail
|
||||
WorkingDirectory=/var/lib/copyparty-jail/srv
|
||||
ExecReload=/bin/kill -s USR1 $MAINPID
|
||||
|
||||
# user to run as + where the TLS certificate is (if any)
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
this is `/var/lib/copyparty-jail`, the fallback webroot when copyparty has not yet been configured
|
||||
this is `/var/lib/copyparty-jail/srv/`, the fallback webroot when copyparty has not yet been configured
|
||||
|
||||
please add some `*.conf` files to `/etc/copyparty.d/`
|
||||
please edit the config-file and restart copyparty;
|
||||
* if running as a system service: `vim /etc/copyparty.conf`
|
||||
* if running as a user service: `vim $HOME/.config/copyparty/copyparty.conf`
|
||||
|
||||
some inspiration:
|
||||
* a basic example config: https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf
|
||||
* another example with focus on syntax: https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf
|
||||
* and CTRL-F `config file example` here: https://github.com/9001/copyparty
|
||||
|
||||
the full list of configuration options can be seen at https://copyparty.eu/cli/
|
||||
or by running `copyparty --help`
|
||||
@@ -17,7 +17,7 @@ Description=copyparty file server
|
||||
[Service]
|
||||
SyslogIdentifier=prisonparty
|
||||
Environment=PYTHONUNBUFFERED=x
|
||||
WorkingDirectory=/var/lib/copyparty-jail
|
||||
WorkingDirectory=/var/lib/copyparty-jail/srv
|
||||
ExecReload=/bin/kill -s USR1 $MAINPID
|
||||
|
||||
# stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
# create thumbnails with Pillow; faster than FFmpeg / MediaProcessing
|
||||
withThumbnails ? true,
|
||||
|
||||
# create thumbnails with PyVIPS; even faster, uses more memory
|
||||
# create thumbnails with PyVIPS; uses more memory, pillow is preferred
|
||||
# -- can be combined with Pillow to support more filetypes
|
||||
withFastThumbnails ? false,
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.20.11/copyparty-1.20.11.tar.gz",
|
||||
"version": "1.20.11",
|
||||
"hash": "sha256-StMBrfLNH/DWVXPk6Tr5f6hcG57tEgdiXpmvxaXKYhM="
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.20.25/copyparty-1.20.25.tar.gz",
|
||||
"version": "1.20.25",
|
||||
"hash": "sha256-gsaN+kLgill1e7+gsdFAAyFBF1NHkG4Fa++DkE9cLHA="
|
||||
}
|
||||
@@ -16,7 +16,7 @@ let
|
||||
withMagic = true;
|
||||
};
|
||||
|
||||
call = attrs: final.python3.pkgs.callPackage ./copyparty ({ ffmpeg = final.ffmpeg-full; } // attrs);
|
||||
call = attrs: final.python3.pkgs.callPackage ./copyparty ({ ffmpeg = final.ffmpeg-headless; } // attrs);
|
||||
in
|
||||
{
|
||||
copyparty = call { stable = true; };
|
||||
|
||||
@@ -26,9 +26,6 @@ See release at https://github.com/9001/copyparty/releases
|
||||
%setup -q
|
||||
|
||||
%build
|
||||
cd "copyparty/web"
|
||||
make
|
||||
cd -
|
||||
%pyproject_wheel
|
||||
|
||||
%install
|
||||
|
||||
@@ -66,11 +66,14 @@ var u2min = `
|
||||
|
||||
</style>
|
||||
|
||||
<a href="#" onclick="this.parentNode.innerHTML='';">show advanced options</a>
|
||||
<a href="#" id="u2min_off">show advanced options</a>
|
||||
`;
|
||||
|
||||
if (!has(perms, 'read')) {
|
||||
var e2 = mknod('div');
|
||||
e2.innerHTML = u2min;
|
||||
ebi('wrap').insertBefore(e2, QS('#wfp'));
|
||||
ebi('u2min_off').onclick = function () {
|
||||
this.parentNode.innerHTML='';
|
||||
};
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
# and copyparty replaces %Y-%m%d with Year-MonthDay, so the
|
||||
# full path will be something like /var/log/copyparty/2023-1130.txt
|
||||
# (note: enable compression by adding .xz at the end)
|
||||
# q, lo: $LOGS_DIRECTORY/%Y-%m%d.log
|
||||
# q, lo: ${LOGS_DIRECTORY}/%Y-%m%d.log
|
||||
|
||||
# p: 80,443,3923 # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE)
|
||||
# i: 127.0.0.1 # only allow connections from localhost (reverse-proxies)
|
||||
@@ -38,5 +38,7 @@
|
||||
accs:
|
||||
rw: * # everyone gets read-write access, but
|
||||
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
|
||||
# gid: 1000 # If you're running as root, you can change the group of this volume here
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# PROVIDE: copyparty
|
||||
# REQUIRE: networking
|
||||
# REQUIRE: networking DAEMON FILESYSTEMS mountd
|
||||
# KEYWORD:
|
||||
|
||||
. /etc/rc.subr
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# this will start `/usr/bin/copyparty`
|
||||
# and read config from `$HOME/.config/copyparty.conf`
|
||||
# and read config from `$HOME/.config/copyparty/copyparty.conf`
|
||||
#
|
||||
# unless you add -q to disable logging, you may want to remove the
|
||||
# following line to allow buffering (slightly better performance):
|
||||
@@ -11,13 +11,13 @@ Description=copyparty file server
|
||||
[Service]
|
||||
Type=notify
|
||||
SyslogIdentifier=copyparty
|
||||
WorkingDirectory=/var/lib/copyparty-jail
|
||||
WorkingDirectory=/var/lib/copyparty-jail/srv
|
||||
Environment=PYTHONUNBUFFERED=x
|
||||
Environment=PRTY_CONFIG=%h/.config/copyparty/copyparty.conf
|
||||
ExecReload=/bin/kill -s USR1 $MAINPID
|
||||
|
||||
# ensure there is a config
|
||||
ExecStartPre=/bin/bash -c 'if [[ ! -f %h/.config/copyparty/copyparty.conf ]]; then mkdir -p %h/.config/copyparty; cp /etc/copyparty/copyparty.conf %h/.config/copyparty/copyparty.conf; fi'
|
||||
ExecStartPre=/bin/bash -c 'if [[ ! -f %h/.config/copyparty/copyparty.conf ]]; then mkdir -p %h/.config/copyparty; cp -v /etc/copyparty.conf %h/.config/copyparty/; fi'
|
||||
|
||||
# run copyparty
|
||||
ExecStart=/usr/bin/python3 /usr/bin/copyparty
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
[global]
|
||||
i: 127.0.0.1
|
||||
|
||||
[accounts]
|
||||
user: password
|
||||
|
||||
[/]
|
||||
/var/lib/copyparty-jail
|
||||
/var/lib/copyparty-jail/srv/
|
||||
accs:
|
||||
r: *
|
||||
rwdma: user
|
||||
flags:
|
||||
grid
|
||||
r: * # everyone can [r]ead (browse/download)
|
||||
|
||||
|
||||
# above is a very simple config;
|
||||
# the webroot is /var/lib/copyparty-jail/srv/
|
||||
# readable by everyone, no login required
|
||||
#
|
||||
# you'll want to replace this file; some inspiration:
|
||||
# - a basic example config: https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf
|
||||
# - another example with focus on syntax: https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf
|
||||
# - and CTRL-F "config file example" here: https://github.com/9001/copyparty
|
||||
# - full list of options: https://copyparty.eu/cli/
|
||||
@@ -3,9 +3,6 @@
|
||||
# vim: ft=yaml:
|
||||
|
||||
|
||||
# put this file in /etc/
|
||||
|
||||
|
||||
[global]
|
||||
e2dsa # enable file indexing and filesystem scanning
|
||||
e2ts # and enable multimedia indexing
|
||||
@@ -16,7 +13,7 @@
|
||||
# and copyparty replaces %Y-%m%d with Year-MonthDay, so the
|
||||
# full path will be something like /var/log/copyparty/2023-1130.txt
|
||||
# (note: enable compression by adding .xz at the end)
|
||||
q, lo: $LOGS_DIRECTORY/%Y-%m%d.log
|
||||
q, lo: ${LOGS_DIRECTORY}/%Y-%m%d.log
|
||||
|
||||
# enable version-checker by uncommenting one of the 'vc-url' lines below; this will
|
||||
# periodically check if your copyparty version has a known security vulnerability,
|
||||
@@ -49,3 +46,5 @@
|
||||
accs:
|
||||
rw: * # everyone gets read-write access, but
|
||||
rwmda: ed # the user "ed" gets read-write-move-delete-admin
|
||||
flags:
|
||||
e2ds # enable filesystem-scanning for this volume only
|
||||
@@ -1,5 +1,5 @@
|
||||
# this will start `/usr/bin/copyparty`
|
||||
# and read config from `/etc/copyparty/copyparty.conf`
|
||||
# and read config from `/etc/copyparty.conf`
|
||||
#
|
||||
# the %i refers to whatever you put after the copyparty@
|
||||
# so with copyparty@foo.service, %i == foo
|
||||
@@ -14,9 +14,9 @@ Description=copyparty file server
|
||||
[Service]
|
||||
Type=notify
|
||||
SyslogIdentifier=copyparty
|
||||
WorkingDirectory=/var/lib/copyparty-jail
|
||||
WorkingDirectory=/var/lib/copyparty-jail/srv
|
||||
Environment=PYTHONUNBUFFERED=x
|
||||
Environment=PRTY_CONFIG=/etc/copyparty/copyparty.conf
|
||||
Environment=PRTY_CONFIG=/etc/copyparty.conf
|
||||
ExecReload=/bin/kill -s USR1 $MAINPID
|
||||
|
||||
# user to run as + where the TLS certificate is (if any)
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
this is `/var/lib/copyparty-jail`, the fallback webroot when copyparty has not yet been configured
|
||||
this is `/var/lib/copyparty-jail/srv/`, the fallback webroot when copyparty has not yet been configured
|
||||
|
||||
please edit `/etc/copyparty/copyparty.conf` (if running as a system service)
|
||||
or `$HOME/.config/copyparty/copyparty.conf` if running as a user service
|
||||
please edit the config-file and restart copyparty;
|
||||
* if running as a system service: `vim /etc/copyparty.conf`
|
||||
* if running as a user service: `vim $HOME/.config/copyparty/copyparty.conf`
|
||||
|
||||
a basic configuration example is available at https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf
|
||||
a configuration example that explains most flags is available at https://github.com/9001/copyparty/blob/hovudstraum/docs/chungus.conf
|
||||
some inspiration:
|
||||
* a basic example config: https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf
|
||||
* another example with focus on syntax: https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf
|
||||
* and CTRL-F `config file example` here: https://github.com/9001/copyparty
|
||||
|
||||
the full list of configuration options can be seen at https://ocv.me/copyparty/helptext.html
|
||||
the full list of configuration options can be seen at https://copyparty.eu/cli/
|
||||
or by running `copyparty --help`
|
||||
@@ -1,6 +1,6 @@
|
||||
# this will start `/usr/bin/copyparty`
|
||||
# in a chroot, preventing accidental access elsewhere,
|
||||
# and read copyparty config from `/etc/copyparty/copyparty.conf`
|
||||
# and read copyparty config from `/etc/copyparty.conf`
|
||||
#
|
||||
# expose additional filesystem locations to copyparty
|
||||
# by listing them between the last `%i` and `--`
|
||||
@@ -19,9 +19,9 @@ Description=copyparty file server
|
||||
[Service]
|
||||
Type=notify
|
||||
SyslogIdentifier=prisonparty
|
||||
WorkingDirectory=/var/lib/copyparty-jail
|
||||
WorkingDirectory=/var/lib/copyparty-jail/srv
|
||||
Environment=PYTHONUNBUFFERED=x
|
||||
Environment=PRTY_CONFIG=/etc/copyparty/copyparty.conf
|
||||
Environment=PRTY_CONFIG=/etc/copyparty.conf
|
||||
ExecReload=/bin/kill -s USR1 $MAINPID
|
||||
|
||||
# user to run as + where the TLS certificate is (if any)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# docker-compose.yml for copyparty
|
||||
services:
|
||||
copyparty:
|
||||
image: copyparty/ac:latest
|
||||
container_name: copyparty
|
||||
user: "1000:1000"
|
||||
volumes:
|
||||
- ./:/cfg:z
|
||||
- /path/to/your/fileshare/top/folder:/w:z
|
||||
|
||||
environment:
|
||||
LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE
|
||||
# enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram)
|
||||
|
||||
PYTHONUNBUFFERED: 1
|
||||
# ensures log-messages are not delayed (but can reduce speed a tiny bit)
|
||||
|
||||
stop_grace_period: 15s # thumbnailer is allowed to continue finishing up for 10s after the shutdown signal
|
||||
healthcheck:
|
||||
# hide it from logs with "/._" so it matches the default --lf-url filter
|
||||
test: ["CMD-SHELL", "wget --spider -q 127.0.0.1:3923/?reset=/._"]
|
||||
interval: 1m
|
||||
timeout: 2s
|
||||
retries: 5
|
||||
start_period: 15s
|
||||
networks:
|
||||
- traefik_default
|
||||
labels:
|
||||
- traefik.http.routers.copyparty.rule=Host(`<DOMAIN>`)
|
||||
- traefik.http.services.copyparty.loadbalancer.server.port=3923
|
||||
- traefik.http.routers.copyparty.tls.certresolver=le
|
||||
- traefik.http.routers.copyparty.entrypoints=websecure
|
||||
|
||||
networks:
|
||||
traefik_default:
|
||||
external: true
|
||||
@@ -0,0 +1,43 @@
|
||||
# note: copied from copyparty/docs/examples/docker/basic-docker-compose/copyparty.conf
|
||||
# not actually YAML but lets pretend:
|
||||
# -*- mode: yaml -*-
|
||||
# vim: ft=yaml:
|
||||
|
||||
|
||||
[global]
|
||||
e2dsa # enable file indexing and filesystem scanning
|
||||
e2ts # enable multimedia indexing
|
||||
ansi # enable colors in log messages (both in logfiles and stdout)
|
||||
|
||||
# q, lo: /cfg/log/%Y-%m%d.log # log to file instead of docker
|
||||
|
||||
# p: 3939 # listen on another port
|
||||
# ipa: 10.89. # only allow connections from 10.89.*
|
||||
# ipa: 172.16.4.0/23 # ...or only 172.16.4.* and 172.16.5.*
|
||||
# ipa: lan # ...or allow LAN only; reject internet IPs
|
||||
# df: 16 # stop accepting uploads if less than 16 GB free disk space
|
||||
# ver # show copyparty version in the controlpanel
|
||||
# grid # show thumbnails/grid-view by default
|
||||
# theme: 2 # monokai
|
||||
# name: datasaver # change the server-name that's displayed in the browser
|
||||
# stats, nos-dup # enable the prometheus endpoint, but disable the dupes counter (too slow)
|
||||
# no-robots, force-js # make it harder for search engines to read your server
|
||||
|
||||
# enable version-checking by uncommenting one of the vc-url lines below;
|
||||
# shows a warning-banner in the controlpanel if your version has a known vulnerability
|
||||
#vc-url: https://api.github.com/repos/9001/copyparty/security-advisories?per_page=9
|
||||
#vc-url: https://api.copyparty.eu/advisories
|
||||
vc-exit # panic and shutdown instead of just showing the warning
|
||||
|
||||
|
||||
[accounts]
|
||||
ed: wark # username: password
|
||||
|
||||
|
||||
[/] # create a volume at "/" (the webroot), which will
|
||||
/w # share /w (the docker data volume)
|
||||
accs:
|
||||
rw: * # everyone gets read-write access, but
|
||||
rwmda: ed # the user "ed" gets read-write-move-delete-admin
|
||||
flags:
|
||||
e2ds # enable filesystem-scanning for this volume only
|
||||
@@ -0,0 +1,34 @@
|
||||
# docker-compose.yml for traefik
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:latest
|
||||
command:
|
||||
- --api.insecure=false
|
||||
- --api.dashboard=true
|
||||
- --providers.docker=true
|
||||
- --entrypoints.web.address=:80
|
||||
- --entrypoints.websecure.address=:443
|
||||
- --entryPoints.websecure.http.tls=true
|
||||
- --entryPoints.web.http.redirections.entryPoint.to=websecure
|
||||
- --entryPoints.web.http.redirections.entryPoint.scheme=https
|
||||
- --certificatesresolvers.le.acme.email=<your email here>
|
||||
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
|
||||
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./letsencrypt:/letsencrypt
|
||||
labels:
|
||||
# OPTIONAL: host your traefik dashboard on <DOMAIN>
|
||||
# - traefik.http.routers.traefik-dash.rule=Host(`<DOMAIN>`)
|
||||
# - traefik.http.routers.traefik-dash.entrypoints=websecure
|
||||
# - traefik.http.routers.traefik-dash.service=api@internal
|
||||
# - traefik.http.routers.traefik-dash.tls.certresolver=le
|
||||
# - traefik.http.routers.traefik-dash.middlewares=traefik-dash-auth,traefik-dash-redirect
|
||||
# - traefik.http.middlewares.traefik-dash-auth.basicauth.users=<USERNAME>:<HTPASSWD_ENCODED_PASSWD>
|
||||
# - traefik.http.middlewares.traefik-dash-redirect.redirectregex.regex=^https://<SUBDOMAIN>\.<DOMAIN>\.TLD/?$
|
||||
# - traefik.http.middlewares.traefik-dash-redirect.redirectregex.replacement=https://<DOMAIN>/dashboard/
|
||||
|
||||
networks: {}
|
||||
+15
-3
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import platform
|
||||
@@ -40,10 +40,21 @@ VT100 = "--ansi" in sys.argv or (
|
||||
)
|
||||
# introduced in anniversary update
|
||||
|
||||
zs = os.environ.get("PRTY_CONFIG", "")
|
||||
CFG_DEF = [zs] if zs else []
|
||||
|
||||
ANYWIN = WINDOWS or sys.platform in ["msys", "cygwin"]
|
||||
|
||||
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"
|
||||
|
||||
EXE = bool(getattr(sys, "frozen", False))
|
||||
@@ -69,9 +80,10 @@ web/deps/busy.mp3
|
||||
web/deps/easymde.css
|
||||
web/deps/easymde.js
|
||||
web/deps/marked.js
|
||||
web/deps/fuse.py
|
||||
web/deps/mfusepy.py
|
||||
web/deps/mini-fa.css
|
||||
web/deps/mini-fa.woff
|
||||
web/deps/orbitron.woff2
|
||||
web/deps/prism.css
|
||||
web/deps/prism.js
|
||||
web/deps/prismd.css
|
||||
@@ -126,7 +138,7 @@ web/tl/vie.js
|
||||
web/ui.css
|
||||
web/up2k.js
|
||||
web/util.js
|
||||
web/w.hash.js
|
||||
web/wopi.html
|
||||
"""
|
||||
RES = set(zs.strip().split("\n"))
|
||||
RESM = {
|
||||
|
||||
+409
-65
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
"""copyparty: http file sharing hub (py2/py3)"""
|
||||
__author__ = "ed <copyparty@ocv.me>"
|
||||
@@ -14,6 +14,7 @@ import locale
|
||||
import os
|
||||
import re
|
||||
import select
|
||||
import shutil
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
@@ -23,11 +24,13 @@ import uuid
|
||||
|
||||
from .__init__ import (
|
||||
ANYWIN,
|
||||
CFG_DEF,
|
||||
CORES,
|
||||
EXE,
|
||||
MACOS,
|
||||
PY2,
|
||||
PY36,
|
||||
UNIX,
|
||||
VT100,
|
||||
WINDOWS,
|
||||
E,
|
||||
@@ -37,7 +40,10 @@ from .__init__ import (
|
||||
from .__version__ import CODENAME, S_BUILD_DT, S_VERSION
|
||||
from .authsrv import expand_config_file, split_cfg_ln, upgrade_cfg_fmt
|
||||
from .bos import bos
|
||||
from .cert import NO_TLS
|
||||
from .cfg import flagcats, onedash
|
||||
from .mdns import DNS_VND
|
||||
from .qrkode import VENDORED as QR_VND
|
||||
from .svchub import SvcHub
|
||||
from .util import (
|
||||
APPLESAN_TXT,
|
||||
@@ -45,7 +51,10 @@ from .util import (
|
||||
DEF_EXP,
|
||||
DEF_MTE,
|
||||
DEF_MTH,
|
||||
HAVE_BWRAP,
|
||||
HAVE_IPV6,
|
||||
HAVE_MAGIC,
|
||||
IFADDR_VND,
|
||||
IMPLICATIONS,
|
||||
JINJA_VER,
|
||||
MIKO_VER,
|
||||
@@ -61,12 +70,20 @@ from .util import (
|
||||
URL_BUG,
|
||||
URL_PRJ,
|
||||
Daemon,
|
||||
absreal,
|
||||
align_tab,
|
||||
b64enc,
|
||||
ctypes,
|
||||
dedent,
|
||||
expand_osenv_c,
|
||||
expand_osenv_cs,
|
||||
expand_osenv_noop,
|
||||
expand_osenv_s,
|
||||
has_resource,
|
||||
list_ips,
|
||||
list_nics,
|
||||
load_resource,
|
||||
lprint,
|
||||
min_ex,
|
||||
pybin,
|
||||
read_utf8,
|
||||
@@ -85,21 +102,19 @@ if PY2:
|
||||
range = xrange # type: ignore
|
||||
|
||||
try:
|
||||
if os.environ.get("PRTY_NO_TLS"):
|
||||
if NO_TLS:
|
||||
raise Exception()
|
||||
|
||||
HAVE_SSL = True
|
||||
import ssl
|
||||
|
||||
HAVE_SSL = True
|
||||
except:
|
||||
HAVE_SSL = False
|
||||
|
||||
u = unicode
|
||||
printed: list[str] = []
|
||||
zsid = uuid.uuid4().urn[4:]
|
||||
|
||||
CFG_DEF = [os.environ.get("PRTY_CONFIG", "")]
|
||||
if not CFG_DEF[0]:
|
||||
CFG_DEF.pop()
|
||||
FULL_HELP = os.environ.get("PRTY_FULL_HELP", "")
|
||||
|
||||
|
||||
class RiceFormatter(argparse.HelpFormatter):
|
||||
@@ -170,16 +185,6 @@ class BasicDodge11874(
|
||||
super(BasicDodge11874, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
def lprint(*a: Any, **ka: Any) -> None:
|
||||
eol = ka.pop("end", "\n")
|
||||
txt: str = " ".join(unicode(x) for x in a) + eol
|
||||
printed.append(txt)
|
||||
if not VT100:
|
||||
txt = RE_ANSI.sub("", txt)
|
||||
|
||||
print(txt, end="", **ka)
|
||||
|
||||
|
||||
def warn(msg: str) -> None:
|
||||
lprint("\033[1mwarning:\033[0;33m {}\033[0m\n".format(msg))
|
||||
|
||||
@@ -427,9 +432,22 @@ def configure_ssl_ciphers(al: argparse.Namespace) -> None:
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def expand_cvars(argv) -> list[str]:
|
||||
n = 0
|
||||
for v in argv:
|
||||
if "=" in v:
|
||||
a, b = v.split("=", 1)
|
||||
v = "%s=%s" % (a, os.path.expanduser(expand_osenv_c(b)))
|
||||
else:
|
||||
v = os.path.expanduser(expand_osenv_c(v))
|
||||
argv[n] = v
|
||||
n += 1
|
||||
return argv
|
||||
|
||||
|
||||
def args_from_cfg(cfg_path: str) -> list[str]:
|
||||
lines: list[str] = []
|
||||
expand_config_file(None, lines, cfg_path, "")
|
||||
expand_config_file(None, expand_osenv_c, lines, cfg_path, "")
|
||||
lines = upgrade_cfg_fmt(None, argparse.Namespace(vc=False), lines, "")
|
||||
|
||||
ret: list[str] = []
|
||||
@@ -453,10 +471,12 @@ def args_from_cfg(cfg_path: str) -> list[str]:
|
||||
else:
|
||||
ret.append(prefix + k + "=" + v)
|
||||
|
||||
return ret
|
||||
return expand_cvars(ret)
|
||||
|
||||
|
||||
def expand_cfg(argv) -> list[str]:
|
||||
argv = expand_cvars(argv)
|
||||
|
||||
if CFG_DEF:
|
||||
supp = args_from_cfg(CFG_DEF[0])
|
||||
argv = argv[:1] + supp + argv[1:]
|
||||
@@ -571,6 +591,17 @@ def sfx_tpoke(top: str):
|
||||
lprint("<TPOKE> [%s] %r" % (f, ex))
|
||||
files.remove(f)
|
||||
|
||||
r = os.path.dirname(top)
|
||||
for d in os.listdir(r):
|
||||
try:
|
||||
p = r + "/" + d
|
||||
f = p + "/copyparty/up2k.py"
|
||||
t2 = os.path.getmtime(f)
|
||||
if t - t2 > 777777 and d.startswith("pe-copyparty"):
|
||||
shutil.rmtree(p)
|
||||
except:
|
||||
pass
|
||||
|
||||
time.sleep(78123)
|
||||
|
||||
|
||||
@@ -581,12 +612,64 @@ def showlic() -> None:
|
||||
except:
|
||||
buf = b""
|
||||
|
||||
if buf:
|
||||
print(buf.decode("utf-8", "replace"))
|
||||
else:
|
||||
if not buf:
|
||||
print("no relevant license info to display")
|
||||
return
|
||||
|
||||
lic = buf.decode("utf-8", "replace")
|
||||
|
||||
try:
|
||||
import site
|
||||
from importlib import import_module
|
||||
from inspect import getsourcefile
|
||||
|
||||
bases = site.PREFIXES + [site.USER_BASE]
|
||||
bases = [x.replace(os.sep, "/") + "/" for x in list(set(bases)) if x]
|
||||
except:
|
||||
bases = []
|
||||
|
||||
# remove missing and/or non-vendored
|
||||
for url, zi, zx in (
|
||||
("/dnslib/", 1, DNS_VND),
|
||||
("/ifaddr/", 1, IFADDR_VND),
|
||||
("/nayuki/QR", 1, QR_VND),
|
||||
("/partftpy", 2, PARTFTPY_VER),
|
||||
("/jinja/", 3, "jinja2"),
|
||||
("/markupsafe/", 3, "jinja2"), # jinja2 sic
|
||||
("/pyftpdlib/", 2, PYFTPD_VER), # heed PRTY_NO
|
||||
("/pyftpdlib/", 3, "pyftpdlib"),
|
||||
("/python-magic/", 1, HAVE_MAGIC), # heed PRTY_NO
|
||||
("/python-magic/", 3, "magic"),
|
||||
):
|
||||
if url not in lic:
|
||||
continue
|
||||
elif zi == 1:
|
||||
if zx:
|
||||
continue
|
||||
elif zi == 2:
|
||||
if zx != "(None)":
|
||||
continue
|
||||
elif zi == 3:
|
||||
mod = None
|
||||
try:
|
||||
assert bases and getsourcefile and import_module # type: ignore # !rm
|
||||
mod = import_module(zx) # type: ignore
|
||||
mp = getsourcefile(mod) or ""
|
||||
for zs in bases:
|
||||
if mp.startswith(zs):
|
||||
mp = ""
|
||||
if mp:
|
||||
continue
|
||||
except:
|
||||
if mod:
|
||||
continue
|
||||
zs1, zs2 = lic.split(url, 1)
|
||||
zs1 = zs1.rsplit("\n", 1)[0]
|
||||
zs2 = zs2.split("\n\n", 1)[1]
|
||||
lic = zs1 + "\n" + zs2
|
||||
|
||||
print(lic)
|
||||
|
||||
|
||||
def get_sects():
|
||||
return [
|
||||
@@ -604,6 +687,7 @@ def get_sects():
|
||||
\033[32m-i 127.0.0.1\033[0m listens on IPv4 localhost only
|
||||
\033[32m-i 127.1\033[0m listens on IPv4 localhost only
|
||||
\033[32m-i 127.1,192.168.123.1\033[0m = IPv4 localhost and 192.168.123.1
|
||||
\033[32m-i no\033[0m disable webserver
|
||||
|
||||
\033[33m-p\033[0m takes a comma-separated list of tcp ports to listen on;
|
||||
the default is \033[32m-p 3923\033[0m but as root you can \033[32m-p 80,443,3923\033[0m
|
||||
@@ -625,6 +709,9 @@ def get_sects():
|
||||
\033[32m-i fd:\033[33m3\033[0m uses the socket passed to copyparty on file descriptor 3
|
||||
|
||||
\033[33m-p\033[0m (tcp ports) is ignored for unix-sockets and FDs
|
||||
|
||||
\033[33m--list-nics\033[0m shows all network adapters (also offline ones);
|
||||
\033[33m--list-ips\033[0m shows all LAN IPs
|
||||
"""
|
||||
),
|
||||
],
|
||||
@@ -761,6 +848,122 @@ def get_sects():
|
||||
).rstrip()
|
||||
+ build_flags_desc(),
|
||||
],
|
||||
[
|
||||
"dedup",
|
||||
"file deduplication on upload",
|
||||
dedent(
|
||||
"""
|
||||
when just \033[36m--dedup\033[0m or volflag "\033[36mdedup\033[0m" is enabled, files will be
|
||||
deduplicated by means of \033[1msymlinks,\033[0m which demand some caution:
|
||||
|
||||
the available dedup approaches:
|
||||
|
||||
* symlinks work on most filesystems, but if another software
|
||||
moves either the symlink itself or the file that it points to,
|
||||
the link will break and the file "disappears";
|
||||
only safe if only copyparty is used to rename/move/delete
|
||||
|
||||
* hardlinks work on Linux filesystems and \033[1mlook\033[0m like regular files,
|
||||
except if the contents of one copy of that file is edited, then
|
||||
this will affect \033[1mall the dupes too!\033[0m
|
||||
and they share the same last-modified-time
|
||||
|
||||
* reflinks only work on btrfs/zfs/xfs but is the safest/best choice;
|
||||
deduplicated files behave just like regular files,
|
||||
and can be modified without affecting other copies/dupes
|
||||
|
||||
to change approach, add one of the following in addition to "\033[36mdedup\033[0m":
|
||||
|
||||
\033[36m--reflink\033[0m or volflag "\033[36mreflink\033[0m"
|
||||
if reflink is not possible (unsupp/cross-fs), a full copy is made
|
||||
|
||||
\033[36m--hardlink-only\033[0m or volflag "\033[36mhardlinkonly\033[0m"
|
||||
if hardlink is not possible (unsupp/cross-fs), a full copy is made
|
||||
|
||||
\033[36m--hardlink\033[0m or volflag "\033[36mhardlink\033[0m"
|
||||
if hardlink is not possible (unsupp/cross-fs), a symlink is made
|
||||
|
||||
note: dedup works best when combined with e2ds or e2dsa
|
||||
|
||||
to deduplicate existing files on-disk, see \033[36m--help-redup\033[0m
|
||||
"""
|
||||
),
|
||||
],
|
||||
[
|
||||
"redup",
|
||||
"deduplicating existing files on disk",
|
||||
dedent(
|
||||
"""
|
||||
\033[36m--redup\033[0m can convert on-disk files between deduplication-approaches,
|
||||
for example turning redundant full copies into reflinks to save space
|
||||
|
||||
converts \033[1mall files\033[0m in each volume! not just copyparty-created ones
|
||||
|
||||
example: --redup \033[32mno,sym,hard\033[0m=\033[35mref\033[0m will find all files of type
|
||||
\033[32mfullcopy/symlink/hardlink\033[0m, and convert them into \033[35mreflinks\033[0m
|
||||
(see \033[36m--help-dedup\033[0m for a summary on the types)
|
||||
|
||||
converting \033[1mfrom\033[0m symlinks/hardlinks is fairly fast and simple;
|
||||
in this case, the obvious target type/approach is reflinks
|
||||
(which only works on btrfs/zfs/xfs)
|
||||
|
||||
converting \033[1mto\033[0m symlinks or hardlinks is \033[3mpossible\033[0m, but
|
||||
not-recommended due to risks explained in \033[36m--help-dedup\033[0m
|
||||
(interaction with other softwares can result in data loss)
|
||||
|
||||
converting \033[1mfrom\033[0m 'no' (full redundant copies) is hella slow;
|
||||
checksums each file to avoid corruption from bad RAM
|
||||
"""
|
||||
),
|
||||
],
|
||||
[
|
||||
"thumb-ex",
|
||||
"extract thumbnails from custom file formats with python scripts",
|
||||
dedent(
|
||||
"""
|
||||
copyparty is able to extract thumbnails from various media formats:
|
||||
images, audio, video, ebooks
|
||||
|
||||
if you want to extract thumbnails from unsupported file formats,
|
||||
you can load a plugin, which extracts image data from custom formats
|
||||
|
||||
load the plugin using --args; for example \033[36m
|
||||
--tx-extract iso,mdf,dmg=~/party-thumb-extractors/th_diskimg.py
|
||||
\033[0m
|
||||
the file must define the function \033[35mmain(abspath, **kwargs)\033[0m:
|
||||
\033[35mabspath\033[0m: path to the file to extract thumbnail from
|
||||
\033[35mkwargs\033[0m currently have:
|
||||
\033[35mvn\033[0m: the VFS which contains the requested file
|
||||
\033[35mrth_srv\033[0m: the copyparty ThumbSrv instance
|
||||
|
||||
if `main` couldn't extract a thumbnail, it must return None;
|
||||
otherwise the return value must be a tuple:
|
||||
\033[35mreturn fmt, stream, offset, whence, size\033[0m
|
||||
|
||||
> \033[32mfmt\033[0m: thumbnail format, should be one of the formats
|
||||
supported by copyparty; e.g. "png", "jpg"
|
||||
|
||||
> \033[32mstream\033[0m: binary file-like object containing the thumbnail,
|
||||
supporting seek(), read(), and close() methods
|
||||
|
||||
> \033[32moffset, whence\033[0m: arguments to seek() method;
|
||||
stream.seek(offset, whence) will be called once before copyparty
|
||||
starts reading from the stream
|
||||
|
||||
> \033[32msize\033[0m: number of bytes to read ftom the stream;
|
||||
if negative, copyparty will read through the end of the stream;
|
||||
|
||||
you can also supply extractors for standard formats to override default
|
||||
thumbnail extraction behaviour; for example \033[36m
|
||||
--tx-extract mp3=~/party-thumb-extractors/th_mp3.py
|
||||
\033[0mwill let you extract or generate custom thumbnails for mp3 files
|
||||
|
||||
\033[1;35mPS!\033[0m the folder that contains the python file should ideally
|
||||
not contain many other python files, and especially nothing
|
||||
with filenames that overlap with modules used by copyparty
|
||||
"""
|
||||
),
|
||||
],
|
||||
[
|
||||
"handlers",
|
||||
"use plugins to handle certain events",
|
||||
@@ -950,6 +1153,7 @@ def get_sects():
|
||||
values for --urlform:
|
||||
\033[36mstash\033[35m dumps the data to file and returns length + checksum
|
||||
\033[36msave,get\033[35m dumps to file and returns the page like a GET
|
||||
\033[36mget\033[35m ignores the message and returns the page like a GET
|
||||
\033[36mprint \033[35m prints the data to log and returns an error
|
||||
\033[36mprint,xm \033[35m prints the data to log and returns --xm output
|
||||
\033[36mprint,get\033[35m prints the data to log and returns GET\033[0m
|
||||
@@ -1010,6 +1214,28 @@ def get_sects():
|
||||
"""
|
||||
),
|
||||
],
|
||||
[
|
||||
"rlo",
|
||||
"logrotate format",
|
||||
dedent(
|
||||
"""
|
||||
a logrotate-counter is added if the logfile filename is taken;
|
||||
by default at the end, unless \033[32m%R\033[0m is somewhere in the \033[36m-lo\033[0m pattern,
|
||||
for example: -lo /var/log/cpp/%Y-%m-%d%R.txt
|
||||
|
||||
\033[36m--rlo\033[0m configures the logrotate format; examples:
|
||||
.1 = when necessary, append a dot followed by a single digit
|
||||
.1! = counter is always added, even when not necessary
|
||||
-3 = a hyphen followed by three-digit counter
|
||||
no = disable counter; append to existing file
|
||||
del = delete existing logfile and create new
|
||||
over = overwrite existing logfile
|
||||
|
||||
append is not possible for .xz-compressed logfiles;
|
||||
if logfile is *.xz and rlo is no then rlo will be .1
|
||||
"""
|
||||
),
|
||||
],
|
||||
[
|
||||
"ls",
|
||||
"volume inspection",
|
||||
@@ -1055,9 +1281,9 @@ def get_sects():
|
||||
"file/folder permissions",
|
||||
dedent(
|
||||
"""
|
||||
global-option \033[33m--chmod-f\033[0m and volflag \033[33mchmod_f\033[0m specifies the unix-permission to use when creating a new file
|
||||
global-option \033[33m--chmod-f\033[0m and volflag "\033[33mchmod_f\033[0m" specifies the unix-permission to use when creating a new file
|
||||
|
||||
similarly, \033[33m--chmod-d\033[0m and \033[33mchmod_d\033[0m sets the directory/folder perm
|
||||
similarly, \033[33m--chmod-d\033[0m and "\033[33mchmod_d\033[0m" sets the directory/folder perm
|
||||
|
||||
the value is a three- or four-digit octal number such as \033[32m755\033[0m, \033[32m0644\033[0m, \033[32m2750\033[0m, etc.
|
||||
|
||||
@@ -1197,12 +1423,15 @@ def add_general(ap, nc, srvname):
|
||||
ap2.add_argument("--name-url", metavar="TXT", type=u, help="URL for server name hyperlink (displayed topleft in browser)")
|
||||
ap2.add_argument("--name-html", type=u, help=argparse.SUPPRESS)
|
||||
ap2.add_argument("--site", metavar="URL", type=u, default="", help="public URL to assume when creating links; example: [\033[32mhttps://example.com/\033[0m]")
|
||||
ap2.add_argument("--env-expand", metavar="N", type=int, default=-1, help="expand environment-variables in config-files? [\033[32m0\033[0m]=no, [\033[32m1\033[0m]=$VAR (old scary syntax), [\033[32m2\033[0m]=${VAR} (new recommended syntax); default is new-syntax with panic if old-syntax is seen")
|
||||
ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="\033[34mREPEATABLE:\033[0m map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]")
|
||||
ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit")
|
||||
ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)")
|
||||
ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="num cpu-cores for uploads/downloads (0=all); keeping the default is almost always best")
|
||||
ap2.add_argument("--reload-sig", metavar="S", type=u, default=("" if ANYWIN else "USR1"), help="reload server config when unix-signal \033[33mS\033[0m is received; examples: [\033[32mSIGUSR1\033[0m], [\033[32mUSR1\033[0m], [\033[32m10\033[0m]")
|
||||
ap2.add_argument("--vc-url", metavar="URL", type=u, default="", help="URL to check for vulnerable versions (default-disabled)")
|
||||
ap2.add_argument("--vc-age", metavar="HOURS", type=int, default=3, help="how many hours to wait between vulnerability checks")
|
||||
ap2.add_argument("--vc-sev", metavar="LEVEL", type=u, default="low", help="minimum severity to care about; one of these: \033[32mlow medium high critical\033[0m")
|
||||
ap2.add_argument("--vc-exit", action="store_true", help="panic and exit if current version is vulnerable")
|
||||
ap2.add_argument("--license", action="store_true", help="show licenses and exit")
|
||||
ap2.add_argument("--version", action="store_true", help="show versions and exit")
|
||||
@@ -1221,8 +1450,8 @@ def add_qr(ap, tty):
|
||||
ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)")
|
||||
ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr")
|
||||
ap2.add_argument("--qr-wait", metavar="SEC", type=float, default=0, help="wait \033[33mSEC\033[0m before printing the qr-code to the log")
|
||||
ap2.add_argument("--qr-every", metavar="SEC", type=float, default=0, help="print the qr-code every \033[33mSEC\033[0m (try this with/without --qr-pin in case of issues)")
|
||||
ap2.add_argument("--qr-winch", metavar="SEC", type=float, default=0, help="when --qr-pin is enabled, check for terminal size change every \033[33mSEC\033[0m")
|
||||
ap2.add_argument("--qr-every", metavar="SEC", type=float, default=0, help="print the qr-code every \033[33mSEC\033[0m (try this with/without \033[33m--qr-pin\033[0m in case of issues)")
|
||||
ap2.add_argument("--qr-winch", metavar="SEC", type=float, default=0, help="when \033[33m--qr-pin\033[0m is enabled, check for terminal size change every \033[33mSEC\033[0m")
|
||||
ap2.add_argument("--qr-file", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m write qr-code to file.\n └─To create txt or svg, \033[33mTXT\033[0m is Filepath:Zoom:Pad, for example [\033[32mqr.txt:1:2\033[0m]\n └─To create png or gif, \033[33mTXT\033[0m is Filepath:Zoom:Pad:Foreground:Background, for example [\033[32mqr.png:8:2:333333:ffcc55\033[0m], or [\033[32mqr.png:8:2::ffcc55\033[0m] for transparent")
|
||||
ap2.add_argument("--qr-stdout", action="store_true", help="always display the QR-code on STDOUT in the terminal, even if \033[33m-q\033[0m")
|
||||
ap2.add_argument("--qr-stderr", action="store_true", help="always display the QR-code on STDERR in the terminal, even if \033[33m-q\033[0m")
|
||||
@@ -1267,16 +1496,18 @@ def add_upload(ap):
|
||||
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600")
|
||||
ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload (bad idea to enable this on windows and/or cow filesystems)")
|
||||
ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even when it might be dangerous (multiprocessing, filesystems lacking sparse-files support, ...)")
|
||||
ap2.add_argument("--chmod-f", metavar="UGO", type=u, default="", help="unix file permissions to use when creating files; default is probably 644 (OS-decided), see --help-chmod. Examples: [\033[32m644\033[0m] = owner-RW + all-R, [\033[32m755\033[0m] = owner-RWX + all-RX, [\033[32m777\033[0m] = full-yolo (volflag=chmod_f)")
|
||||
ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m2750\033[0m] = setgid + owner-RW + group-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)")
|
||||
ap2.add_argument("--chmod-f", metavar="UGO", type=u, default="", help="unix file permissions to use when creating files; default is probably 644 (OS-decided), see \033[33m--help-chmod\033[0m. Examples: [\033[32m644\033[0m] = owner-RW + all-R, [\033[32m755\033[0m] = owner-RWX + all-RX, [\033[32m777\033[0m] = full-yolo (volflag=chmod_f)")
|
||||
ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see \033[33m--help-chmod\033[0m. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m2750\033[0m] = setgid + owner-RW + group-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)")
|
||||
ap2.add_argument("--uid", metavar="N", type=int, default=-1, help="unix user-id to chown new files/folders to; default = -1 = do-not-change (volflag=uid)")
|
||||
ap2.add_argument("--gid", metavar="N", type=int, default=-1, help="unix group-id to chown new files/folders to; default = -1 = do-not-change (volflag=gid)")
|
||||
ap2.add_argument("--wram", action="store_true", help="allow uploading even if a volume is inside a ramdisk, meaning that all data will be lost on the next server reboot (volflag=wram)")
|
||||
ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)")
|
||||
ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication; see \033[33m--help-dedup\033[0m (volflag=dedup)")
|
||||
ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)")
|
||||
ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)")
|
||||
ap2.add_argument("--hardlink-only", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made (volflag=hardlinkonly)")
|
||||
ap2.add_argument("--reflink", action="store_true", help="enable reflink-based dedup; will fallback on full copies when that is impossible (non-CoW filesystem) (volflag=reflink)")
|
||||
ap2.add_argument("--redup", metavar="A[,B]=T", help="convert dedup-type for existing files; \033[33mA,B\033[0m is dedup-types to convert from (no/ref/sym/hard), \033[33mT\033[0m is target type; converting from sym/hard is fast-ish, from no/ref is slooow, to sym/hard is jank; see \033[33m--help-redup\033[0m. Example: [\033[32msym,hard=ref\033[0m] (volflag=redup)")
|
||||
ap2.add_argument("--redup-dry", action="store_true", help="dry-run; makes \033[33m--redup\033[0m not apply changes (volflag=redup_dry)")
|
||||
ap2.add_argument("--no-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)")
|
||||
ap2.add_argument("--no-dupe-m", action="store_true", help="also reject dupes when moving a file into another volume (volflag=nodupem)")
|
||||
ap2.add_argument("--no-clone", action="store_true", help="do not use existing data on disk to satisfy dupe uploads; reduces server HDD reads in exchange for much more network load (volflag=noclone)")
|
||||
@@ -1290,11 +1521,11 @@ def add_upload(ap):
|
||||
ap2.add_argument("--nrand", metavar="NUM", type=int, default=9, help="randomized filenames length (volflag=nrand)")
|
||||
ap2.add_argument("--magic", action="store_true", help="enable filetype detection on nameless uploads (volflag=magic)")
|
||||
ap2.add_argument("--df", metavar="GiB", type=u, default="0", help="ensure \033[33mGiB\033[0m free disk space by rejecting upload requests; assumes gigabytes unless a unit suffix is given: [\033[32m256m\033[0m], [\033[32m4\033[0m], [\033[32m2T\033[0m] (volflag=df)")
|
||||
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
|
||||
ap2.add_argument("--sparse", metavar="MiB", type=int, default=1, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
|
||||
ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; [\033[32m-1\033[0m] = forbidden/always-off, [\033[32m0\033[0m] = default-off and warn if enabled, [\033[32m1\033[0m] = default-off, [\033[32m2\033[0m] = on, [\033[32m3\033[0m] = on and disable datecheck")
|
||||
ap2.add_argument("--nosubtle", metavar="N", type=int, default=0, help="when to use a wasm-hasher instead of the browser's builtin; faster on chrome, but buggy in older chrome versions. [\033[32m0\033[0m] = only when necessary (non-https), [\033[32m1\033[0m] = always (all browsers), [\033[32m2\033[0m] = always on chrome/firefox, [\033[32m3\033[0m] = always on chrome, [\033[32mN\033[0m] = chrome-version N and newer (recommendation: 137)")
|
||||
ap2.add_argument("--u2j", metavar="JOBS", type=int, default=2, help="web-client: number of file chunks to upload in parallel; 1 or 2 is good when latency is low (same-country), 2~4 for android-clients, 2~6 for cross-atlantic. Max is 6 in most browsers. Big values increase network-speed but may reduce HDD-speed")
|
||||
ap2.add_argument("--u2sz", metavar="N,N,N", type=u, default="1,64,96", help="web-client: default upload chunksize (MiB); sets \033[33mmin,default,max\033[0m in the settings gui. Each HTTP POST will aim for \033[33mdefault\033[0m, and never exceed \033[33mmax\033[0m. Cloudflare max is 96. Big values are good for cross-atlantic but may increase HDD fragmentation on some FS. Disable this optimization with [\033[32m1,1,1\033[0m]")
|
||||
ap2.add_argument("--u2sz", metavar="N,N", type=u, default="64,96", help="web-client: default upload chunksize in MiB, either \033[33mtarget,max\033[0m or just \033[33mtarget\033[0m (sets both); this is \033[33mtarget\033[0m in the web-UI. Each HTTP POST will aim for \033[33mtarget\033[0m and never exceed \033[33mmax\033[0m. Cloudflare max is 96. Minimum is [\033[32m1\033[0m] and will be slow. Lowest recommended is [\033[32m8\033[0m]. Big values are good for cross-atlantic but may increase HDD fragmentation on some FS. Disable this optimization with [\033[32mno\033[0m]")
|
||||
ap2.add_argument("--u2ow", metavar="NUM", type=int, default=0, help="web-client: default setting for when to replace/overwrite existing files; [\033[32m0\033[0m]=never, [\033[32m1\033[0m]=if-client-newer, [\033[32m2\033[0m]=always (volflag=u2ow)")
|
||||
ap2.add_argument("--u2sort", metavar="TXT", type=u, default="s", help="upload order; [\033[32ms\033[0m]=smallest-first, [\033[32mn\033[0m]=alphabetical, [\033[32mfs\033[0m]=force-s, [\033[32mfn\033[0m]=force-n -- alphabetical is a bit slower on fiber/LAN but makes it easier to eyeball if everything went fine")
|
||||
ap2.add_argument("--write-uplog", action="store_true", help="write POST reports to textfiles in working-directory")
|
||||
@@ -1317,9 +1548,9 @@ def add_network(ap):
|
||||
ap2.add_argument("--cachectl", metavar="TXT", default="no-cache", help="default-value of the 'Cache-Control' response-header (controls caching in webbrowsers). Default prevents repeated downloading of the same file unless necessary (browser will ask copyparty if the file has changed). Examples: [\033[32mmax-age=604869\033[0m] will cache for 7 days, [\033[32mno-store, max-age=0\033[0m] will always redownload. (volflag=cachectl)")
|
||||
ap2.add_argument("--http-vary", metavar="TXT", type=u, default="Origin, PW, Cookie", help="value of the 'Vary' response-header; a hint for caching proxies")
|
||||
ap2.add_argument("--http-no-tcp", action="store_true", help="do not listen on TCP/IP for http/https; only listen on unix-domain-sockets")
|
||||
if ANYWIN:
|
||||
ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances")
|
||||
elif not MACOS:
|
||||
if FULL_HELP or ANYWIN:
|
||||
ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets (windows-only); allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances")
|
||||
if FULL_HELP or (not ANYWIN and not MACOS):
|
||||
ap2.add_argument("--freebind", action="store_true", help="allow listening on IPs which do not yet exist, for example if the network interfaces haven't finished going up. Only makes sense for IPs other than '0.0.0.0', '127.0.0.1', '::', and '::1'. May require running as root (unless net.ipv6.ip_nonlocal_bind)")
|
||||
ap2.add_argument("--wr-h-eps", metavar="PATH", type=u, default="", help="write list of listening-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started")
|
||||
ap2.add_argument("--wr-h-aon", metavar="PATH", type=u, default="", help="write list of accessible-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started")
|
||||
@@ -1330,13 +1561,16 @@ def add_network(ap):
|
||||
ap2.add_argument("--s-wr-slp", metavar="SEC", type=float, default=0.0, help="debug: socket write delay in seconds")
|
||||
ap2.add_argument("--rsp-slp", metavar="SEC", type=float, default=0.0, help="debug: response delay in seconds")
|
||||
ap2.add_argument("--rsp-jtr", metavar="SEC", type=float, default=0.0, help="debug: response delay, random duration 0..\033[33mSEC\033[0m")
|
||||
ap2.add_argument("--list-nics", action="store_true", help="debug: list detected network adapters")
|
||||
ap2.add_argument("--list-ips", action="store_true", help="debug: list detected LAN IPs")
|
||||
|
||||
|
||||
def add_tls(ap, cert_path):
|
||||
ap2 = ap.add_argument_group("SSL/TLS options")
|
||||
ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls -- force plaintext")
|
||||
ap2.add_argument("--https-only", action="store_true", help="disable plaintext -- force tls")
|
||||
ap2.add_argument("--cert", metavar="PATH", type=u, default=cert_path, help="path to file containing a concatenation of TLS key and certificate chain")
|
||||
ap2.add_argument("--cert", metavar="PATH", type=u, default=cert_path, help="path to file containing a concatenation of TLS key and certificate chain (if \033[33m--certkey\033[0m is not set), or just the certificate chain (if \033[33m--certkey\033[0m is set)")
|
||||
ap2.add_argument("--certkey", metavar="PATH", type=u, default="", help="path to file containing just the certificate key; if this is set, then \033[33m--cert\033[0m should only contain the certificate chain")
|
||||
ap2.add_argument("--ssl-ver", metavar="LIST", type=u, default="", help="set allowed ssl/tls versions; [\033[32mhelp\033[0m] shows available versions; default is what your python version considers safe")
|
||||
ap2.add_argument("--ciphers", metavar="LIST", type=u, default="", help="set allowed ssl/tls ciphers; [\033[32mhelp\033[0m] shows available ciphers")
|
||||
ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info")
|
||||
@@ -1367,7 +1601,7 @@ def add_auth(ap):
|
||||
ses_db = os.path.join(E.cfg, "sessions.db")
|
||||
ap2 = ap.add_argument_group("IdP / identity provider / user authentication options")
|
||||
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
|
||||
ap2.add_argument("--idp-hm-usr", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mT\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp")
|
||||
ap2.add_argument("--idp-hm-usr", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mT\033[0m is provided, and its value exists in a mapping defined by this option; see \033[33m--help-idp\033[0m")
|
||||
ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control")
|
||||
ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present")
|
||||
ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m")
|
||||
@@ -1379,13 +1613,13 @@ def add_auth(ap):
|
||||
ap2.add_argument("--idp-login", metavar="L", type=u, default="", help="replace all login-buttons with a link to URL \033[33mL\033[0m (unless \033[32mpw\033[0m is in \033[33m--auth-ord\033[0m then both will be shown); [\033[32m{dst}\033[0m] expands to url of current page")
|
||||
ap2.add_argument("--idp-login-t", metavar="T", type=u, default="Login with SSO", help="the label/text for the idp-login button")
|
||||
ap2.add_argument("--idp-logout", metavar="L", type=u, default="", help="replace all logout-buttons with a link to URL \033[33mL\033[0m")
|
||||
ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see --help-auth-ord")
|
||||
ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see \033[33m--help-auth-ord\033[0m")
|
||||
ap2.add_argument("--pw-hdr", metavar="NAME", type=u, default="pw", help="lowercase name of password-header (NAME: foo); \033[1;31mWARNING:\033[0m Changing this will break support for many clients")
|
||||
ap2.add_argument("--pw-urlp", metavar="NAME", type=u, default="pw", help="lowercase name of password url-param (?NAME=foo); \033[1;31mWARNING:\033[0m Changing this will break support for many clients")
|
||||
ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app")
|
||||
ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins")
|
||||
ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)")
|
||||
ap2.add_argument("--ses-len", metavar="CHARS", type=int, default=20, help="session key length; default is 120 bits ((20//4)*4*6)")
|
||||
ap2.add_argument("--ses-len", metavar="CHARS", type=int, default=24, help="session key length; default is 144 bits ((24//4)*4*6)")
|
||||
ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies")
|
||||
ap2.add_argument("--grp-all", metavar="NAME", type=u, default="acct", help="the name of the auto-generated group which contains every username which is known")
|
||||
ap2.add_argument("--ipu", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m users with IP matching \033[33mCIDR\033[0m are auto-authenticated as username \033[33mUSR\033[0m; example: [\033[32m172.16.24.0/24=dave]")
|
||||
@@ -1465,8 +1699,12 @@ def add_sftp(ap):
|
||||
ap2.add_argument("--sftp-pw", action="store_true", help="allow password-authentication with sftp (not just ssh-keys)")
|
||||
ap2.add_argument("--sftp-anon", metavar="TXT", type=u, default="", help="allow anonymous/unauthenticated connections with \033[33mTXT\033[0m as username")
|
||||
ap2.add_argument("--sftp-hostk", metavar="FP", type=u, default=E.cfg, help="path to folder with hostkeys, for example 'ssh_host_rsa_key'; missing keys will be generated")
|
||||
ap2.add_argument("--sftp-fastldr", action="store_true", help="speedhack for 'loading hostkeys' on old CPUs; only use with known-good hostkeys")
|
||||
ap2.add_argument("--sftp-chattr", metavar="T", default="s", help="which attributes can be modified with chattr? [\033[32ms\033[0m]=size, [\033[32mt\033[0m]=mtime, [\033[32mp\033[0m]=permissions, [\033[32mu\033[0m]=uid/gid. Example: [\033[32mstpu\033[0m]")
|
||||
ap2.add_argument("--sftp-banner", metavar="T", type=u, default="", help="bannertext to send when someone connects; can be @filepath")
|
||||
ap2.add_argument("--sftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m / \033[33m--ipar\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]")
|
||||
ap2.add_argument("--sftp-hs-t", metavar="SEC", type=int, default=15, help="connection handshake timeout in seconds")
|
||||
ap2.add_argument("--sftp-hs-n", metavar="NUM", type=int, default=4, help="max num ongoing/incomplete handshakes")
|
||||
|
||||
|
||||
def add_ftp(ap):
|
||||
@@ -1481,6 +1719,7 @@ def add_ftp(ap):
|
||||
ap2.add_argument("--ftp-wt", metavar="SEC", type=int, default=7, help="grace period for resuming interrupted uploads (any client can write to any file last-modified more recently than \033[33mSEC\033[0m seconds ago)")
|
||||
ap2.add_argument("--ftp-nat", metavar="ADDR", type=u, default="", help="the NAT address to use for passive connections")
|
||||
ap2.add_argument("--ftp-pr", metavar="P-P", type=u, default="", help="the range of TCP ports to use for passive connections, for example \033[32m12000-13000")
|
||||
ap2.add_argument("--ftp-banner", metavar="T", type=u, default="welcome to the party", help="bannertext to send when someone connects; \\n is newline, can be @filepath (loaded into memory on startup); if it has a newline then banner must be longer than 75 chars")
|
||||
|
||||
|
||||
def add_webdav(ap):
|
||||
@@ -1520,6 +1759,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-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("--smb6", action="store_true", help="enable IPv6")
|
||||
ap2.add_argument("--smbv", action="store_true", help="verbose")
|
||||
ap2.add_argument("--smbvv", action="store_true", help="verboser")
|
||||
ap2.add_argument("--smbvvv", action="store_true", help="verbosest")
|
||||
@@ -1531,6 +1771,19 @@ def add_opds(ap):
|
||||
ap2.add_argument("--opds-exts", metavar="T,T", type=u, default="epub,cbz,pdf", help="file formats to list in OPDS feeds; leave empty to show everything (volflag=opds_exts)")
|
||||
|
||||
|
||||
def add_wopi(ap):
|
||||
ap2 = ap.add_argument_group("WOPI options")
|
||||
ap2.add_argument("--wopi", action="store_true", help="enable integration with office suites using WOPI")
|
||||
ap2.add_argument("--wopi-accs", metavar="TXT", type=u, default="", help="restrict wopi to this comma-separated list of usernames; default is everyone")
|
||||
ap2.add_argument("--wopi-api", metavar="URL", type=u, default="", help="URL that the WOPI-client should use to communicate with copyparty; default is same as user's webbrowser. Example: [\033[32mhttps://party.example.com/\033[0m]")
|
||||
ap2.add_argument("--wopi-url", metavar="URL", type=u, default="", help="URL to your WOPI client; the host of e.g. Collabora Online. Example: [\033[32mhttps://code.example.com/\033[0m]")
|
||||
ap2.add_argument("--wopi-urls", metavar="H=U", type=u, action="append", help="\033[34mREPEATABLE:\033[0m maps http \033[33mH\033[0mOST copyparty being accessed by to specific WOPI client instance \033[33mU\033[0mRL; falls back to \033[33m--wopi-url\033[0m; examples: [\033[32mparty.public.com=https://office.public.com/\033[0m], [\033[32mparty.internal.net:8443=https://office.internal.net:8443/\033[0m]")
|
||||
ap2.add_argument("--wopi-crt", metavar="TXT", type=u, default="", help="if \033[33m--wopi-url\033[0m is selfsigned: path to ca.pem or cert.pem to expect/verify (can be [\033[32mno\033[0m] for full-yolo)")
|
||||
ap2.add_argument("--wopi-crt-icn", action="store_true", help="if \033[33m--wopi-url\033[0m is selfsigned: ignore the CN (server ip/name) in cert")
|
||||
ap2.add_argument("--wopi-ttl", metavar="SEC", type=int, default=1800, help="session lifetime; allow editing for this many seconds (default is 30 min)")
|
||||
ap2.add_argument("--wopi-wdel", action="store_true", help="require permissions read+write+delete for writing to file; default is just read+write")
|
||||
|
||||
|
||||
def add_handlers(ap):
|
||||
ap2 = ap.add_argument_group("handlers (see --help-handlers)")
|
||||
ap2.add_argument("--on404", metavar="PY", type=u, action="append", help="\033[34mREPEATABLE:\033[0m handle 404s by executing \033[33mPY\033[0m file")
|
||||
@@ -1567,10 +1820,12 @@ def add_stats(ap):
|
||||
|
||||
def add_yolo(ap):
|
||||
ap2 = ap.add_argument_group("yolo options")
|
||||
ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests")
|
||||
ap2.add_argument("--hack-me-bro", action="store_true", help="allow enabling dangerously buggy protocols such as \033[33m--smb\033[0m / \033[33m--tftp\033[0m")
|
||||
ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests; \033[1;31mDANGEROUS\033[0m / LAN-only")
|
||||
ap2.add_argument("--cookie-lax", action="store_true", help="allow cookies from other domains (if you follow a link from another website into your server, you will arrive logged-in); this reduces protection against CSRF")
|
||||
ap2.add_argument("--allow-svg-js", action="store_true", help="allow svg images to execute javascript; default-disabled because ~nobody wants it (volflag=allow_svg_js)")
|
||||
ap2.add_argument("--no-fnugg", action="store_true", help="disable the smoketest for caching-related issues in the web-UI")
|
||||
ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET")
|
||||
ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET -- \033[1;31mDANGEROUS\033[0m, removes csrf protection")
|
||||
ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)")
|
||||
ap2.add_argument("--unsafe-state", action="store_true", help="when one of the emergency fallback locations are used for runtime state ($TMPDIR, /tmp), certain features will be force-disabled for security reasons by default. This option overrides that safeguard and allows unsafe storage of secrets")
|
||||
|
||||
@@ -1604,19 +1859,47 @@ def add_optouts(ap):
|
||||
|
||||
|
||||
def add_safety(ap):
|
||||
th_bwrap = ""
|
||||
if HAVE_BWRAP:
|
||||
zsl = [
|
||||
HAVE_BWRAP,
|
||||
"--proc /proc",
|
||||
"--tmpfs /tmp",
|
||||
"--tmpfs /var",
|
||||
"--tmpfs /run",
|
||||
"--dev-bind /dev/null /dev/null",
|
||||
"--dev-bind /dev/random /dev/random",
|
||||
"--dev-bind /dev/urandom /dev/urandom",
|
||||
"--chdir /tmp",
|
||||
"--clearenv",
|
||||
"--unshare-all",
|
||||
"--cap-drop ALL",
|
||||
"--die-with-parent",
|
||||
"--new-session",
|
||||
]
|
||||
for d in ("/lib", "/lib64", "/usr/lib", "/usr/lib64"):
|
||||
if os.path.isdir(d):
|
||||
zsl.append(" --ro-bind %s %s" % (d, d))
|
||||
th_bwrap = " ".join(zsl)
|
||||
|
||||
ap2 = ap.add_argument_group("safety options")
|
||||
ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js")
|
||||
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav 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("--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 also has access (keeps permissions from the outer/initial volume) (volflag=xvol)")
|
||||
ap2.add_argument("--xdev", action="store_true", help="stay within the filesystem of the volume root; do not descend into other devices (symlink or bind-mount to another HDD, ...) (volflag=xdev)")
|
||||
ap2.add_argument("--vol-nospawn", action="store_true", help="if a volume's folder does not exist on the HDD, then do not create it (continue with warning) (volflag=nospawn)")
|
||||
ap2.add_argument("--vol-or-crash", action="store_true", help="if a volume's folder does not exist on the HDD, then burst into flames (volflag=assert_root)")
|
||||
ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles")
|
||||
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-readme", action="store_true", help="disable rendering readme/preadme.md into directory listings")
|
||||
ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings (volflag=no_logues)")
|
||||
ap2.add_argument("--no-readme", action="store_true", help="disable rendering readme/preadme.md into directory listings (volflag=no_readme)")
|
||||
ap2.add_argument("--csp-ui", metavar="TXT", default="script-src 'unsafe-eval' 'nonce-{{ js_nonce }}'; worker-src 'self'", help="content-security-policy to apply for the web-UI; default helps prevent XSS by blocking <script> / onclick / ... (volflag=csp_ui)")
|
||||
ap2.add_argument("--csp-dl", metavar="TXT", default="", help="content-security-policy to apply for static files (volflag=csp_dl)")
|
||||
ap2.add_argument("--no-script", action="store_true", help="disables javascript in html files; helps prevent XSS but kills interactive websites; this will override \033[33m--csp-dl\033[0m with [\033[32mscript-src 'none'\033[0m] (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 \033[33m--no-script\033[0m (volflag=nohtml)")
|
||||
ap2.add_argument("--no-mime", action="store_true", help="disallow changing the response mimetype with url-parameter ?mime=... (volflag=nomime)")
|
||||
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("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)")
|
||||
@@ -1637,10 +1920,18 @@ def add_safety(ap):
|
||||
ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for \033[33mMIN\033[0m minutes (and also kill its active connections) -- disable with 0")
|
||||
ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]")
|
||||
ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives")
|
||||
ap2.add_argument("--acao-re", metavar="PTN", type=u, default="", help="Access-Control-Allow-Origin; supplement to \033[33m--acao\033[0m; allow request if http-header 'Origin' matches either \033[33m--acao\033[0m or regex-\033[33mPTN\033[0m; ominous example: [\033[32m^moz-extension://.*\033[0m]")
|
||||
ap2.add_argument("--acam", metavar="V[,V]", type=u, default="GET,HEAD", help="Access-Control-Allow-Methods; list of methods to accept from offsite ('*' behaves like \033[33m--acao\033[0m's description)")
|
||||
if FULL_HELP or ANYWIN:
|
||||
ap2.add_argument("--unsafe-tools", action="store_true", help="windows-only: allow running programs (ffmpeg, dcraw_emu, ...) when their location is inside a folder that can be uploaded to (dangerous due to DLL-hijacking)")
|
||||
if FULL_HELP or (not ANYWIN and not UNIX):
|
||||
ap2.add_argument("--th-bwrap", metavar="CMD", type=u, default=th_bwrap, help="optional bwrap sandbox command for FFmpeg and dcraw (Linux-only)")
|
||||
ap2.add_argument("--use-bwrap", metavar="TXT", type=u, default="n", help="a/n/f; [\033[32ma\033[0m]=auto (yes if the program 'bwrap' exists (assumes it works)), [\033[32mn\033[0m]=no (assumes bwrap is broken), [\033[32mf\033[0m]=force (disables FFmpeg if bwrap unavailable)")
|
||||
else:
|
||||
ap2.add_argument("--use-bwrap", metavar="TXT", type=u, default="n", help=argparse.SUPPRESS)
|
||||
|
||||
|
||||
def add_salt(ap, fk_salt, dk_salt, ah_salt):
|
||||
def add_salt(ap, fk_salt, dk_salt, ah_salt, wopi_salt):
|
||||
ap2 = ap.add_argument_group("salting options")
|
||||
ap2.add_argument("--ah-alg", metavar="ALG", type=u, default="none", help="account-pw hashing algorithm; one of these, best to worst: \033[32margon2 scrypt sha2 none\033[0m (each optionally followed by alg-specific comma-sep. config)")
|
||||
ap2.add_argument("--ah-salt", metavar="SALT", type=u, default=ah_salt, help="account-pw salt; ignored if \033[33m--ah-alg\033[0m is none (default)")
|
||||
@@ -1649,6 +1940,7 @@ def add_salt(ap, fk_salt, dk_salt, ah_salt):
|
||||
ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files")
|
||||
ap2.add_argument("--dk-salt", metavar="SALT", type=u, default=dk_salt, help="per-directory accesskey salt; used to generate unpredictable URLs to share folders with users who only have the 'get' permission")
|
||||
ap2.add_argument("--warksalt", metavar="SALT", type=u, default="hunter2", help="up2k file-hash salt; serves no purpose, no reason to change this (but delete all databases if you do)")
|
||||
ap2.add_argument("--wopi-salt", metavar="TXT", type=u, default=wopi_salt, help="WOPI file ID salt; used by \033[33m--wopi\033[0m for persistent file IDs")
|
||||
ap2.add_argument("--show-ah-salt", action="store_true", help="on startup, print the effective value of \033[33m--ah-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)")
|
||||
ap2.add_argument("--show-fk-salt", action="store_true", help="on startup, print the effective value of \033[33m--fk-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)")
|
||||
ap2.add_argument("--show-dk-salt", action="store_true", help="on startup, print the effective value of \033[33m--dk-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)")
|
||||
@@ -1658,7 +1950,7 @@ def add_shutdown(ap):
|
||||
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-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):
|
||||
@@ -1666,6 +1958,8 @@ def add_logging(ap):
|
||||
ap2.add_argument("-q", action="store_true", help="quiet; disable most STDOUT messages")
|
||||
ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile; use .txt for plaintext or .xz for compressed. Example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)")
|
||||
ap2.add_argument("--flo", metavar="N", type=int, default=1, help="log format for \033[33m-lo\033[0m; [\033[32m1\033[0m]=classic/colors, [\033[32m2\033[0m]=no-color")
|
||||
ap2.add_argument("--rlo", metavar="TXT", type=u, default="no", help="logrotate counter format; see \033[33m--help-rlo\033[0m")
|
||||
ap2.add_argument("--logrot-sig", metavar="S", type=u, default="", help="immediately logrotate when unix-signal \033[33mS\033[0m is received; examples: [\033[32mSIGHUP\033[0m], [\033[32mHUP\033[0m], [\033[32m1\033[0m]")
|
||||
ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR")
|
||||
ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR")
|
||||
ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster")
|
||||
@@ -1715,29 +2009,44 @@ def add_thumbnail(ap):
|
||||
ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)")
|
||||
ap2.add_argument("--th-qv", metavar="N", type=int, default=40, help="webp/jpg thumbnail quality (10~90); higher is larger filesize and better quality (volflag=th_qv)")
|
||||
ap2.add_argument("--th-qvx", metavar="N", type=int, default=64, help="jxl thumbnail quality (10~90); higher is larger filesize and better quality (volflag=th_qvx)")
|
||||
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,raw,pil,ff", help="image decoders, in order of preference")
|
||||
ap2.add_argument("--th-dec", metavar="LIBS", default="raw,pil,ff,vips", help="image decoders, in order of preference")
|
||||
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
|
||||
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
|
||||
ap2.add_argument("--th-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-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-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-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; 0=no-keepalive")
|
||||
ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled")
|
||||
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than \033[33m--th-poke\033[0m seconds will get deleted every \033[33m--th-clean\033[0m seconds")
|
||||
ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern")
|
||||
ap2.add_argument("--th-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 (volflag=th_covers)")
|
||||
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-fl", action="store_true", help="generate spectrograms with logarithmic frequency scale instead of linear")
|
||||
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
|
||||
# https://github.com/libvips/libvips
|
||||
# https://stackoverflow.com/a/47612661
|
||||
# ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:'
|
||||
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow")
|
||||
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="3fr,avif,cr2,cr3,crw,dcr,dng,erf,exr,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,jp2,jpeg,jpg,jpx,jxl,k25,mdc,mef,mrw,nef,nii,pfm,pgm,png,ppm,raf,raw,sr2,srf,svg,tif,tiff,webp,x3f", help="image formats to decode using pyvips")
|
||||
ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="3fr,arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mdc,mef,mos,mrw,nef,nrw,orf,pef,raf,raw,sr2,srf,srw,x3f", help="image formats to decode using rawpy")
|
||||
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,jxl,kra,ora,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow")
|
||||
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="3fr,arw,avif,cr2,cr3,crw,dcr,dng,erf,exr,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,jp2,jpeg,jpg,jpx,jxl,k25,kdc,mdc,mef,mrw,nef,nii,nrw,orf,pfm,pgm,png,ppm,raf,raw,rw2,sr2,srf,srw,svg,tif,tiff,webp,x3f", help="image formats to decode using pyvips")
|
||||
ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="3fr,arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mdc,mef,mos,mrw,nef,nrw,orf,pef,raf,raw,rw2,sr2,srf,srw,x3f", help="image formats to decode using rawpy (if available) or libraw's dcraw_emu")
|
||||
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,kra,ora,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,m4b,m4r,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bcstm,bfstm,brstm,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,m4b,m4r,mdgz,mdxz,mdz,mka,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
|
||||
|
||||
ap2.add_argument("--th-pil-add", metavar="T", type=u, default="", help="add or remove image formats to decode using pillow listed in \033[33m--th-r-pil\033[0m; example: [\033[32mfb2,pdf,-psd,-bmp\033[0m] (add fb2 and pdf, remove psd and bmp)")
|
||||
ap2.add_argument("--th-vips-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for pyvips")
|
||||
ap2.add_argument("--th-raw-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for rawpy/libraw")
|
||||
ap2.add_argument("--th-ffi-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for ffmpeg images")
|
||||
ap2.add_argument("--th-ffv-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for ffmpeg videos")
|
||||
ap2.add_argument("--th-ffa-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for ffmpeg audio")
|
||||
|
||||
ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)")
|
||||
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz, epub=jpg.epub", help="audio/image formats to decompress before passing to ffmpeg")
|
||||
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz, epub=jpg.epub, kra=png.kra, ora=png.ora", help="audio/image formats to decompress before passing to ffmpeg")
|
||||
ap2.add_argument("--th-extract", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m list of file extensions to thumbnail using a custom plugin (a python script); example: [\033[32mmdf,iso,dmg=/thumbs/diskimg.py\033[0m], see \033[33m--help-thumb-ex\033[0m")
|
||||
ap2.add_argument("--th-extr-sz", metavar="M", type=int, default=16, help="max num megabytes to allow \033[33m--th-extract\033[0m plugins to extract from each file")
|
||||
ap2.add_argument("--hot-th-extr", action="store_true", help="recompile extractors on each thumbnail extraction -- expensive but convenient when hacking on stuff")
|
||||
|
||||
|
||||
def add_transcoding(ap):
|
||||
@@ -1794,11 +2103,13 @@ def add_db_general(ap, hcores):
|
||||
ap2.add_argument("--no-forget", action="store_true", help="never forget indexed files, even when deleted from disk -- makes it impossible to ever upload the same file twice -- only useful for offloading uploads to a cloud service or something (volflag=noforget)")
|
||||
ap2.add_argument("--forget-ip", metavar="MIN", type=int, default=0, help="remove uploader-IP from database (and make unpost impossible) \033[33mMIN\033[0m minutes after upload, for GDPR reasons. Default [\033[32m0\033[0m] is never-forget. [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month. (volflag=forget_ip)")
|
||||
ap2.add_argument("--dbd", metavar="PROFILE", default="wal", help="database durability profile; sets the tradeoff between robustness and speed, see \033[33m--help-dbd\033[0m (volflag=dbd)")
|
||||
ap2.add_argument("--hist-cow", action="store_true", help="btrfs-only: histpaths (db/thumbs) are nocow by default; if you really need Copy-on-write then enable this (volflag=hist_cow)")
|
||||
ap2.add_argument("--xlink", action="store_true", help="on upload: check all volumes for dupes, not just the target volume (probably buggy, not recommended) (volflag=xlink)")
|
||||
ap2.add_argument("--hash-mt", metavar="CORES", type=int, default=hcores, help="num cpu cores to use for file hashing; set 0 or 1 for single-core hashing")
|
||||
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="rescan filesystem for changes every \033[33mSEC\033[0m seconds; 0=off (volflag=scan)")
|
||||
ap2.add_argument("--db-act", metavar="SEC", type=float, default=10.0, help="defer any scheduled volume reindexing until \033[33mSEC\033[0m seconds after last db write (uploads, renames, ...)")
|
||||
ap2.add_argument("--srch-icase", action="store_true", help="case-insensitive search for all unicode characters (the default is icase for just ascii). NOTE: will make searches much slower (around 4x), and NOTE: only applies to filenames/paths, not tags")
|
||||
ap2.add_argument("--srch-icase", action="store_true", help="case-insensitive search for all unicode characters (the default is icase for just ascii). NOTE: will make searches much slower (around 3x), and NOTE: only applies to filenames/paths, not tags")
|
||||
ap2.add_argument("--srch-nfkc", action="store_true", help="case-insensitive and unicode-normalization-insensitive search (NFC/NFD) for filenames/paths; slightly slower than \033[33m--srch-icase\033[0m (about 15%% slower / 87%% as fast)")
|
||||
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=45, help="search deadline -- terminate searches running for more than \033[33mSEC\033[0m seconds")
|
||||
ap2.add_argument("--srch-hits", metavar="N", type=int, default=7999, help="max search results to allow clients to fetch; 125 results will be shown initially")
|
||||
ap2.add_argument("--srch-excl", metavar="PTN", type=u, default="", help="regex: exclude files from search results if the file-URL matches \033[33mPTN\033[0m (case-sensitive). Example: [\033[32mpassword|logs/[0-9]\033[0m] any URL containing 'password' or 'logs/DIGIT' (volflag=srch_excl)")
|
||||
@@ -1828,7 +2139,9 @@ def add_txt(ap):
|
||||
ap2 = ap.add_argument_group("textfile options")
|
||||
ap2.add_argument("--rw-edit", metavar="T,T", type=u, default="md", help="comma-sep. list of file-extensions to allow editing with permissions read+write; all others require read+write+delete (volflag=rw_edit)")
|
||||
ap2.add_argument("--md-no-br", action="store_true", help="markdown: disable newline-is-newline; will only render a newline into the html given two trailing spaces or a double-newline (volflag=md_no_br)")
|
||||
ap2.add_argument("--md-hist", metavar="TXT", type=u, default="s", help="where to store old version of markdown files; [\033[32ms\033[0m]=subfolder, [\033[32mv\033[0m]=volume-histpath, [\033[32mn\033[0m]=nope/disabled (volflag=md_hist)")
|
||||
ap2.add_argument("--md-hist", metavar="TXT", type=u, default="s", help="where to store old version of textfiles; [\033[32ms\033[0m]=subfolder, [\033[32mv\033[0m]=volume-histpath, [\033[32mn\033[0m]=nope/disabled (volflag=md_hist)")
|
||||
ap2.add_argument("--md-nhist", metavar="TXT", type=int, default=99, help="how many old versions of textfiles to keep; 0=infinite (volflag=md_nhist)")
|
||||
ap2.add_argument("--show-hist", action="store_true", help="allow browsing the .hist folder; enable to view old markdown files (volflag=show_hist)")
|
||||
ap2.add_argument("--txt-eol", metavar="TYPE", type=u, default="", help="enable EOL conversion when writing documents; supported: CRLF, LF (volflag=txt_eol)")
|
||||
ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="the textfile editor will check for serverside changes every \033[33mSEC\033[0m seconds")
|
||||
ap2.add_argument("-emp", action="store_true", help="enable markdown plugins -- neat but dangerous, big XSS risk")
|
||||
@@ -1862,9 +2175,11 @@ def add_ui(ap, retry: int):
|
||||
ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
|
||||
ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)")
|
||||
ap2.add_argument("--localtime", action="store_true", help="default to local timezone instead of UTC")
|
||||
ap2.add_argument("--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, 6, 6c, 7, 7c, 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("--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("--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")
|
||||
@@ -1875,6 +2190,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("--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("--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("--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)")
|
||||
@@ -1884,8 +2200,8 @@ def add_ui(ap, retry: int):
|
||||
ap2.add_argument("--css-browser", metavar="L", type=u, default="", help="URL to additional CSS to include in the filebrowser html")
|
||||
ap2.add_argument("--js-browser", metavar="L", type=u, default="", help="URL to additional JS to include in the filebrowser html")
|
||||
ap2.add_argument("--js-other", metavar="L", type=u, default="", help="URL to additional JS to include in all other pages")
|
||||
ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); can be @PATH to send the contents of a file at PATH, and/or begin with %% to render as jinja2 template (volflag=html_head)")
|
||||
ap2.add_argument("--html-head-s", metavar="T", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); similar to (and can be combined with) --html-head but only accepts static text (volflag=html_head_s)")
|
||||
ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); can be @PATH to send the contents of a file at PATH, and/or begin with %% to render as jinja2 template; \033[32m<script>\033[0m will not work, use \033[32m<script nonce=\"{{ js_nonce }}\">\033[0m (volflag=html_head)")
|
||||
ap2.add_argument("--html-head-s", metavar="T", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); similar to (and can be combined with) \033[33m--html-head\033[0m but only accepts static text (volflag=html_head_s)")
|
||||
ap2.add_argument("--ih", action="store_true", help="if a folder contains index.html, show that instead of the directory listing by default (can be changed in the client settings UI, or add ?v to URL for override)")
|
||||
ap2.add_argument("--textfiles", metavar="CSV", type=u, default="txt,nfo,diz,cue,readme", help="file extensions to present as plaintext")
|
||||
ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)")
|
||||
@@ -1896,7 +2212,7 @@ def add_ui(ap, retry: int):
|
||||
ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
|
||||
ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
|
||||
ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-nb\033[0m")
|
||||
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m). This is the same as --ver-who all")
|
||||
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m). This is the same as \033[33m--ver-who all\033[0m")
|
||||
ap2.add_argument("--ver-who", metavar="TXT", type=u, default="no", help="only show version for: [\033[32ma\033[0m]=admin-permission-anywhere, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone")
|
||||
ap2.add_argument("--du-who", metavar="TXT", type=u, default="all", help="only show disk usage for: [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mrw\033[0m]=read-write, [\033[32mw\033[0m]=write, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone (volflag=du_who)")
|
||||
ap2.add_argument("--ver-iwho", type=int, default=0, help=argparse.SUPPRESS)
|
||||
@@ -1927,16 +2243,21 @@ def add_debug(ap):
|
||||
ap2.add_argument("--vc", action="store_true", help="verbose config file parser (explain config)")
|
||||
ap2.add_argument("--cgen", action="store_true", help="generate config file from current config (best-effort; probably buggy)")
|
||||
ap2.add_argument("--deps", action="store_true", help="list information about detected optional dependencies")
|
||||
if hasattr(select, "poll"):
|
||||
if FULL_HELP or hasattr(select, "poll"):
|
||||
ap2.add_argument("--no-poll", action="store_true", help="kernel-bug workaround: disable poll; use select instead (limits max num clients to ~700)")
|
||||
ap2.add_argument("--no-sendfile", action="store_true", help="kernel-bug workaround: disable sendfile; do a safe and slow read-send-loop instead")
|
||||
ap2.add_argument("--no-scandir", action="store_true", help="kernel-bug workaround: disable scandir; do a listdir + stat on each file instead")
|
||||
ap2.add_argument("--no-fastboot", action="store_true", help="wait for initial filesystem indexing before accepting client requests")
|
||||
ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead")
|
||||
if sys.version_info > (3, 7) and not ANYWIN:
|
||||
ap2.add_argument("--sig-thr", action="store_true", help="start separate thread for OS-signals (try this if CTRL-C is busted)")
|
||||
else:
|
||||
ap2.add_argument("--sig-thr", action="store_true", default=True, help=argparse.SUPPRESS)
|
||||
ap2.add_argument("--rm-sck", action="store_true", help="when listening on unix-sockets, do a basic delete+bind instead of the default atomic bind")
|
||||
ap2.add_argument("--srch-dbg", action="store_true", help="explain search processing, and do some extra expensive sanity checks")
|
||||
ap2.add_argument("--rclone-mdns", action="store_true", help="use mdns-domain instead of server-ip on /?hc")
|
||||
ap2.add_argument("--stackmon", metavar="P,S", type=u, default="", help="write stacktrace to \033[33mP\033[0math every \033[33mS\033[0m second, for example --stackmon=\033[32m./st/%%Y-%%m/%%d/%%H%%M.xz,60")
|
||||
ap2.add_argument("--stack-sig", metavar="S", type=u, default="", help="show stacktrace when unix-signal \033[33mS\033[0m is received; examples: [\033[32mSIGUSR2\033[0m], [\033[32mUSR2\033[0m], [\033[32m12\033[0m]")
|
||||
ap2.add_argument("--log-thrs", metavar="SEC", type=float, default=0.0, help="list active threads every \033[33mSEC\033[0m")
|
||||
ap2.add_argument("--log-fk", metavar="REGEX", type=u, default="", help="log filekey params for files where path matches \033[33mREGEX\033[0m; [\033[32m.\033[0m] (a single dot) = all files")
|
||||
ap2.add_argument("--bak-flips", action="store_true", help="[up2k] if a client uploads a bitflipped/corrupted chunk, store a copy according to \033[33m--bf-nc\033[0m and \033[33m--bf-dir\033[0m")
|
||||
@@ -1964,6 +2285,7 @@ def run_argparse(
|
||||
fk_salt = get_salt("fk", 18)
|
||||
dk_salt = get_salt("dk", 30)
|
||||
ah_salt = get_salt("ah", 18)
|
||||
wopi_salt = get_salt("wopi", 18)
|
||||
|
||||
# alpine peaks at 5 threads for some reason,
|
||||
# all others scale past that (but try to avoid SMT),
|
||||
@@ -1998,8 +2320,9 @@ def run_argparse(
|
||||
add_tftp(ap)
|
||||
add_smb(ap)
|
||||
add_opds(ap)
|
||||
add_wopi(ap)
|
||||
add_safety(ap)
|
||||
add_salt(ap, fk_salt, dk_salt, ah_salt)
|
||||
add_salt(ap, fk_salt, dk_salt, ah_salt, wopi_salt)
|
||||
add_optouts(ap)
|
||||
add_shutdown(ap)
|
||||
add_yolo(ap)
|
||||
@@ -2085,6 +2408,14 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
print("\n".join("%8s %s" % (k, v) for k, v in sorted(MIMES.items())))
|
||||
sys.exit(0)
|
||||
|
||||
if "--list-ips" in argv:
|
||||
print("\n".join(str(x) for x in sorted(list_ips())))
|
||||
sys.exit(0)
|
||||
|
||||
if "--list-nics" in argv:
|
||||
print("\n".join(str(x) for x in sorted(list_nics(True).items())))
|
||||
sys.exit(0)
|
||||
|
||||
if EXE:
|
||||
print("pybin: {}\n".format(pybin), end="")
|
||||
|
||||
@@ -2094,6 +2425,9 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
argv.pop(n)
|
||||
break
|
||||
|
||||
if CFG_DEF:
|
||||
CFG_DEF[0] = absreal(CFG_DEF[0])
|
||||
|
||||
ensure_locale()
|
||||
|
||||
ensure_webdeps()
|
||||
@@ -2170,6 +2504,15 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
|
||||
quotecheck(al)
|
||||
|
||||
if al.env_expand == 2:
|
||||
al.shenvexp = expand_osenv_c
|
||||
elif al.env_expand == 1:
|
||||
al.shenvexp = expand_osenv_s
|
||||
elif al.env_expand == 0:
|
||||
al.shenvexp = expand_osenv_noop
|
||||
else:
|
||||
al.shenvexp = expand_osenv_cs
|
||||
|
||||
if al.chdir:
|
||||
os.chdir(al.chdir)
|
||||
|
||||
@@ -2239,7 +2582,8 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
if al.ciphers:
|
||||
configure_ssl_ciphers(al)
|
||||
else:
|
||||
warn("ssl module does not exist; cannot enable https")
|
||||
if not al.http_only and not NO_TLS:
|
||||
warn("ssl module does not exist; cannot enable https")
|
||||
al.http_only = True
|
||||
|
||||
if PY2 and WINDOWS and al.e2d:
|
||||
@@ -2263,7 +2607,7 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
|
||||
# signal.signal(signal.SIGINT, sighandler)
|
||||
|
||||
SvcHub(al, dal, argv, "".join(printed)).run()
|
||||
SvcHub(al, dal, argv).run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (1, 20, 12)
|
||||
VERSION = (1, 20, 26)
|
||||
CODENAME = "sftp is fine too"
|
||||
BUILD_DT = (2026, 3, 11)
|
||||
BUILD_DT = (2026, 10, 14)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
+335
-60
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
@@ -13,7 +13,7 @@ import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, WINDOWS, E
|
||||
from .__init__ import ANYWIN, CFG_DEF, MACOS, PY2, TYPE_CHECKING, WINDOWS, E
|
||||
from .bos import bos
|
||||
from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap
|
||||
from .pwhash import PWHash
|
||||
@@ -25,6 +25,7 @@ from .util import (
|
||||
FN_EMB,
|
||||
HAVE_SQLITE3,
|
||||
IMPLICATIONS,
|
||||
LOG,
|
||||
META_NOBOTS,
|
||||
MIMES,
|
||||
SQLITE_VER,
|
||||
@@ -41,6 +42,7 @@ from .util import (
|
||||
odfusion,
|
||||
read_utf8,
|
||||
relchk,
|
||||
sanitize_fn,
|
||||
statdir,
|
||||
ub64enc,
|
||||
uncyg,
|
||||
@@ -56,7 +58,7 @@ if HAVE_SQLITE3:
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from collections.abc import Iterable
|
||||
|
||||
from typing import Any, Generator, Optional, Sequence, Union
|
||||
from typing import Any, Callable, Generator, Optional, Sequence, Union
|
||||
|
||||
from .util import NamedLogger, RootLogger
|
||||
|
||||
@@ -106,6 +108,7 @@ SEESLOG = " (see fileserver log for details)"
|
||||
SSEELOG = " ({})".format(SEE_LOG)
|
||||
BAD_CFG = "invalid config; {}".format(SEE_LOG)
|
||||
SBADCFG = " ({})".format(BAD_CFG)
|
||||
REDUP_E2 = "WARNING: [/%s] needs 'e2ds' for redup from 'ref' and/or to 'sym'"
|
||||
|
||||
PTN_U_GRP = re.compile(r"\$\{u(%[+-][^}]+)\}")
|
||||
PTN_G_GRP = re.compile(r"\$\{g(%[+-][^}]+)\}")
|
||||
@@ -424,6 +427,7 @@ class VFS(object):
|
||||
self.root = self
|
||||
self.dev = 0 # st_dev
|
||||
self.nodes: dict[str, VFS] = {} # child nodes
|
||||
self.lnodes: Optional[dict[bytes, VFS]] = None # lowercase
|
||||
self.histtab: dict[str, str] = {} # all realpath->histpath
|
||||
self.dbpaths: dict[str, str] = {} # all realpath->dbpath
|
||||
self.dbv: Optional[VFS] = None # closest full/non-jump parent
|
||||
@@ -443,6 +447,7 @@ class VFS(object):
|
||||
self.adot: dict[str, list[str]] = {}
|
||||
self.js_ls = {}
|
||||
self.js_htm = ""
|
||||
self.md_htm = ""
|
||||
self.all_vols: dict[str, VFS] = {} # flattened recursive
|
||||
self.all_nodes: dict[str, VFS] = {} # also jumpvols/shares
|
||||
self.all_fvols: dict[str, VFS] = {} # volumes which are files
|
||||
@@ -465,6 +470,7 @@ class VFS(object):
|
||||
self.canonical = self._canonical_null
|
||||
self.dcanonical = self._dcanonical_null
|
||||
|
||||
self.get = self._get
|
||||
self.get_dbv = self._get_dbv
|
||||
self.ls = self._ls
|
||||
|
||||
@@ -541,13 +547,11 @@ class VFS(object):
|
||||
|
||||
hist = flags.get("hist")
|
||||
if hist and hist != "-":
|
||||
zs = "{}/{}".format(hist.rstrip("/"), name)
|
||||
flags["hist"] = os.path.expandvars(os.path.expanduser(zs))
|
||||
flags["hist"] = "%s/%s" % (hist.rstrip("/"), name)
|
||||
|
||||
dbp = flags.get("dbpath")
|
||||
if dbp and dbp != "-":
|
||||
zs = "{}/{}".format(dbp.rstrip("/"), name)
|
||||
flags["dbpath"] = os.path.expandvars(os.path.expanduser(zs))
|
||||
flags["dbpath"] = "%s/%s" % (dbp.rstrip("/"), name)
|
||||
|
||||
return flags
|
||||
|
||||
@@ -574,6 +578,11 @@ class VFS(object):
|
||||
if name in self.nodes:
|
||||
return self.nodes[name]._find(rem)
|
||||
|
||||
if self.lnodes:
|
||||
zs = name.casefold().encode("ascii", "ignore")
|
||||
if zs in self.lnodes:
|
||||
return self.lnodes[zs]._find(rem)
|
||||
|
||||
return self, vpath
|
||||
|
||||
def can_access(
|
||||
@@ -595,7 +604,7 @@ class VFS(object):
|
||||
ret += "A"
|
||||
return ret
|
||||
|
||||
def get(
|
||||
def _get(
|
||||
self,
|
||||
vpath: str,
|
||||
uname: str,
|
||||
@@ -629,11 +638,50 @@ class VFS(object):
|
||||
t = "%s has no %s in %r => %r => %r"
|
||||
self.log("vfs", t % (uname, msg, vpath, cvpath, ap), 6)
|
||||
|
||||
if not err:
|
||||
return None
|
||||
|
||||
t = "you don't have %s-access in %r or below %r"
|
||||
raise Pebkac(err, t % (msg, "/" + cvpath, "/" + vn.vpath))
|
||||
|
||||
return vn, rem
|
||||
|
||||
def _get_xvd(
|
||||
self,
|
||||
vpath: str,
|
||||
uname: str,
|
||||
will_read: bool,
|
||||
will_write: bool,
|
||||
will_move: bool = False,
|
||||
will_del: bool = False,
|
||||
will_get: bool = False,
|
||||
err: int = 403,
|
||||
) -> tuple["VFS", str]:
|
||||
zvr = self._get(
|
||||
vpath, uname, will_read, will_write, will_move, will_del, will_get, err
|
||||
)
|
||||
if not zvr:
|
||||
return None
|
||||
vn, rem = zvr
|
||||
if "xvd" in vn.flags and vn.realpath:
|
||||
avn = vn.chk_ap(vn.canonical(rem))
|
||||
if avn and avn.vpath == vn.vpath:
|
||||
return vn, rem
|
||||
if (
|
||||
not avn
|
||||
or (will_read and uname not in avn.axs.uread)
|
||||
or (will_write and uname not in avn.axs.uwrite)
|
||||
or (will_move and uname not in avn.axs.umove)
|
||||
or (will_del and uname not in avn.axs.udel)
|
||||
or (will_get and uname not in avn.axs.uget)
|
||||
):
|
||||
if not err:
|
||||
return None
|
||||
raise Pebkac(
|
||||
err, "permission denied for [/%s]" % (vjoin(vn.vpath, rem))
|
||||
)
|
||||
return vn, rem
|
||||
|
||||
def _get_share_src(self, vrem: str) -> tuple["VFS", str]:
|
||||
src = self.shr_src
|
||||
if not src:
|
||||
@@ -669,8 +717,6 @@ class VFS(object):
|
||||
if lfn == zs.lower():
|
||||
hit = zs
|
||||
break
|
||||
if not hit:
|
||||
return True # NFC/NFD or something, can't be helped either way
|
||||
if self.log:
|
||||
t = "returning 404 due to underlying case-insensitive filesystem:\n http-req: %r\n local-fs: %r"
|
||||
self.log("vfs", t % (fn, hit))
|
||||
@@ -709,7 +755,7 @@ class VFS(object):
|
||||
if self.shr_files:
|
||||
assert self.shr_src # !rm
|
||||
if rem and rem not in self.shr_files:
|
||||
return "\n\n"
|
||||
return "\n\n\0\n\n"
|
||||
if resolve:
|
||||
rap = absreal(ap)
|
||||
vn, rem = self.shr_src
|
||||
@@ -718,7 +764,7 @@ class VFS(object):
|
||||
# not the dir itself; assert file allowed
|
||||
ad, fn = os.path.split(rap)
|
||||
if chk != ad or fn not in self.shr_files:
|
||||
return "\n\n"
|
||||
return "\n\n\0\n\n"
|
||||
|
||||
return (rap or absreal(ap)) if resolve else ap
|
||||
|
||||
@@ -737,7 +783,7 @@ class VFS(object):
|
||||
if chk != absreal(ap):
|
||||
# not the dir itself; assert file allowed
|
||||
if ad != chk or fn not in self.shr_files:
|
||||
return "\n\n"
|
||||
return "\n\n\0\n\n"
|
||||
|
||||
return os.path.join(ad, fn)
|
||||
|
||||
@@ -761,6 +807,32 @@ class VFS(object):
|
||||
real = [x for x in real if os.path.basename(x[0]) in self.shr_files]
|
||||
return abspath, real, {}
|
||||
|
||||
def _ls_xvd(
|
||||
self,
|
||||
rem: str,
|
||||
uname: str,
|
||||
scandir: bool,
|
||||
permsets: list[list[bool]],
|
||||
lstat: bool = False,
|
||||
throw: bool = False,
|
||||
) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]:
|
||||
dirpath, real0, virt_vis = self._ls(rem, uname, scandir, permsets, True, throw)
|
||||
lns = [x for x in real0 if stat.S_ISLNK(x[1].st_mode)]
|
||||
if not lns:
|
||||
return dirpath, real0, virt_vis
|
||||
real = [x for x in real0 if not stat.S_ISLNK(x[1].st_mode)]
|
||||
ps = (permsets[0] + [False] * 5)[:5] # rwmdg
|
||||
for fn, _ in lns:
|
||||
ap = absreal(os.path.join(dirpath, fn))
|
||||
vn = self.chk_ap(ap)
|
||||
try:
|
||||
if vn.get("", uname, *ps):
|
||||
real.append((fn, bos.stat(ap)))
|
||||
except:
|
||||
pass
|
||||
real.sort()
|
||||
return dirpath, real, virt_vis
|
||||
|
||||
def _ls(
|
||||
self,
|
||||
rem: str,
|
||||
@@ -782,6 +854,12 @@ class VFS(object):
|
||||
if not rem:
|
||||
# no vfs nodes in the list of real inodes
|
||||
real = [x for x in real if x[0] not in self.nodes]
|
||||
if self.lnodes:
|
||||
real = [
|
||||
x
|
||||
for x in real
|
||||
if x[0].casefold().encode("ascii", "ignore") not in self.lnodes
|
||||
]
|
||||
|
||||
dbv = self.dbv or self
|
||||
for name, vn2 in sorted(self.nodes.items()):
|
||||
@@ -855,12 +933,16 @@ class VFS(object):
|
||||
self.log("vfs.walk", t % (seen[-1], fsroot, self.vpath, rem), 3)
|
||||
return
|
||||
|
||||
if "xdev" in self.flags or "xvol" in self.flags:
|
||||
if "xvd" in self.flags:
|
||||
ps = (permsets[0] + [False] * 5)[:5] # rwmdg
|
||||
rm1 = []
|
||||
for le in vfs_ls:
|
||||
ap = absreal(os.path.join(fsroot, le[0]))
|
||||
vn2 = self.chk_ap(ap)
|
||||
if not vn2 or not vn2.get("", uname, True, False):
|
||||
try:
|
||||
if not vn2.get("", uname, *ps):
|
||||
raise Exception()
|
||||
except:
|
||||
rm1.append(le)
|
||||
_ = [vfs_ls.remove(x) for x in rm1] # type: ignore
|
||||
|
||||
@@ -880,9 +962,6 @@ class VFS(object):
|
||||
yield dbv, vrem, rel, fsroot, rfiles, rdirs, vfs_virt
|
||||
|
||||
for rdir, _ in rdirs:
|
||||
if not dots_ok and rdir.startswith("."):
|
||||
continue
|
||||
|
||||
wrel = (rel + "/" + rdir).lstrip("/")
|
||||
wrem = (rem + "/" + rdir).lstrip("/")
|
||||
for x in self.walk(
|
||||
@@ -905,20 +984,14 @@ class VFS(object):
|
||||
|
||||
def zipgen(
|
||||
self,
|
||||
vpath: str,
|
||||
folder: str,
|
||||
vrem: str,
|
||||
flt: set[str],
|
||||
uname: str,
|
||||
dirs: bool,
|
||||
dots: int,
|
||||
scandir: bool,
|
||||
wrap: bool = True,
|
||||
) -> Generator[dict[str, Any], None, None]:
|
||||
|
||||
# if multiselect: add all items to archive root
|
||||
# if single folder: the folder itself is the top-level item
|
||||
folder = "" if flt or not wrap else (vpath.split("/")[-1].lstrip(".") or "top")
|
||||
|
||||
g = self.walk(folder, vrem, [], uname, [[True, False]], dots, scandir, False)
|
||||
for _, _, vpath, apath, files, rd, vd in g:
|
||||
if flt:
|
||||
@@ -956,6 +1029,22 @@ class VFS(object):
|
||||
for d in [{"vp": v, "ap": a, "st": n} for v, a, n in ret2]:
|
||||
yield d
|
||||
|
||||
def sanitize_fn(self, fn: str, rem: Any = None) -> str:
|
||||
fn = sanitize_fn(fn, self.flags["fsnt"])
|
||||
if rem:
|
||||
return fn
|
||||
if fn in self.nodes or (
|
||||
self.lnodes and fn.casefold().encode("ascii", "ignore") in self.lnodes
|
||||
):
|
||||
raise Pebkac(400, "bad filename")
|
||||
return fn
|
||||
|
||||
def chk_fn(self, fn: str) -> None:
|
||||
if fn in self.nodes or (
|
||||
self.lnodes and fn.casefold().encode("ascii", "ignore") in self.lnodes
|
||||
):
|
||||
raise Pebkac(400, "bad filename")
|
||||
|
||||
def chk_ap(self, ap: str, st: Optional[os.stat_result] = None) -> Optional["VFS"]:
|
||||
aps = ap + os.sep
|
||||
if "xdev" in self.flags and not ANYWIN:
|
||||
@@ -1202,7 +1291,7 @@ class AuthSrv(object):
|
||||
for ptn, sigil in ((PTN_U_ANY, "${u}"), (PTN_G_ANY, "${g}")):
|
||||
if bool(ptn.search(src)) != bool(ptn.search(dst)):
|
||||
zsl.append(sigil)
|
||||
if zsl:
|
||||
if zsl and src != "//NULL":
|
||||
t = "ERROR: if %s is mentioned in a volume definition, it must be included in both the filesystem-path [%s] and the volume-url [/%s]"
|
||||
t = "\n".join([t % (x, src, dst) for x in zsl])
|
||||
self.log(t, 1)
|
||||
@@ -1282,8 +1371,16 @@ class AuthSrv(object):
|
||||
daxs: dict[str, AXS],
|
||||
mflags: dict[str, dict[str, Any]],
|
||||
) -> tuple[str, str]:
|
||||
src = os.path.expandvars(os.path.expanduser(src))
|
||||
src = absreal(src)
|
||||
if src == "//NULL":
|
||||
src = ""
|
||||
else:
|
||||
src = os.path.expanduser(self.args.shenvexp(src))
|
||||
src = absreal(src)
|
||||
|
||||
if dst == "//NULL":
|
||||
t = "//NULL given as URL instead of filesystem-path"
|
||||
self.log(t, 1)
|
||||
raise Exception(t)
|
||||
dst = dst.strip("/")
|
||||
|
||||
if dst in mount:
|
||||
@@ -1291,7 +1388,7 @@ class AuthSrv(object):
|
||||
self.log(t.format(dst, mount[dst][0], src), c=1)
|
||||
raise Exception(BAD_CFG)
|
||||
|
||||
if src in mount.values():
|
||||
if src and src in mount.values():
|
||||
t = "filesystem-path [{}] mounted in multiple locations:"
|
||||
t = t.format(src)
|
||||
for v in [k for k, v in mount.items() if v[0] == src] + [dst]:
|
||||
@@ -1300,7 +1397,7 @@ class AuthSrv(object):
|
||||
self.log(t, c=3)
|
||||
raise Exception(BAD_CFG)
|
||||
|
||||
if not bos.path.exists(src):
|
||||
if src and not bos.path.exists(src):
|
||||
self.log("warning: filesystem-path did not exist: %r" % (src,), 3)
|
||||
|
||||
vf = {}
|
||||
@@ -1375,7 +1472,7 @@ class AuthSrv(object):
|
||||
) -> None:
|
||||
self.line_ctr = 0
|
||||
|
||||
expand_config_file(self.log, cfg_lines, fp, "")
|
||||
expand_config_file(self.log, self.args.shenvexp, cfg_lines, fp, "")
|
||||
if self.args.vc:
|
||||
lns = ["{:4}: {}".format(n, s) for n, s in enumerate(cfg_lines, 1)]
|
||||
self.log("expanded config file (unprocessed):\n" + "\n".join(lns))
|
||||
@@ -1668,7 +1765,8 @@ class AuthSrv(object):
|
||||
for alias, mapping in [
|
||||
("h", "gh"),
|
||||
("G", "gG"),
|
||||
("A", "rwmda.A"),
|
||||
("r", "g"),
|
||||
("A", "rgwmda.A"),
|
||||
]:
|
||||
expanded = ""
|
||||
for ch in mapping:
|
||||
@@ -1867,7 +1965,7 @@ class AuthSrv(object):
|
||||
if WINDOWS:
|
||||
cased = {}
|
||||
for vp, (ap, vp0) in mount.items():
|
||||
cased[vp] = (absreal(ap), vp0)
|
||||
cased[vp] = (absreal(ap) if ap else "", vp0)
|
||||
|
||||
mount = cased
|
||||
|
||||
@@ -1939,7 +2037,7 @@ class AuthSrv(object):
|
||||
vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
|
||||
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())
|
||||
for vol in vfs.all_vols.values():
|
||||
unknown_flags = set()
|
||||
@@ -1988,6 +2086,10 @@ class AuthSrv(object):
|
||||
[sun] if "m" in s_pr else [],
|
||||
[sun] if "d" 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
|
||||
@@ -2033,6 +2135,15 @@ class AuthSrv(object):
|
||||
umap[usr].sort()
|
||||
setattr(vfs, "a" + perm, umap)
|
||||
|
||||
if self.args.wopi:
|
||||
zsl = unames[:]
|
||||
if self.args.wopi_accs:
|
||||
zsl = [x.strip() for x in self.args.wopi_accs.split(",")]
|
||||
axs = AXS(zsl, zsl, None, zsl)
|
||||
vn = VFS(self.log_func, "", "wopi", "", axs, self.vf0())
|
||||
vn.flags["unlistcr"] = vn.flags["unlistcw"] = True
|
||||
vfs.nodes["wopi"] = vfs.all_nodes["wopi"] = vn
|
||||
|
||||
for vol in vfs.all_nodes.values():
|
||||
za = vol.axs
|
||||
vol.uaxs = {
|
||||
@@ -2173,7 +2284,7 @@ class AuthSrv(object):
|
||||
if vflag == "-":
|
||||
pass
|
||||
elif vflag:
|
||||
vflag = os.path.expandvars(os.path.expanduser(vflag))
|
||||
vflag = os.path.expanduser(self.args.shenvexp(vflag))
|
||||
vol.histpath = vol.dbpath = uncyg(vflag) if WINDOWS else vflag
|
||||
elif self.args.hist:
|
||||
for nch in range(len(hid)):
|
||||
@@ -2208,7 +2319,7 @@ class AuthSrv(object):
|
||||
if vflag == "-":
|
||||
pass
|
||||
elif vflag:
|
||||
vflag = os.path.expandvars(os.path.expanduser(vflag))
|
||||
vflag = os.path.expanduser(self.args.shenvexp(vflag))
|
||||
vol.dbpath = uncyg(vflag) if WINDOWS else vflag
|
||||
elif self.args.dbpath:
|
||||
for nch in range(len(hid)):
|
||||
@@ -2331,7 +2442,7 @@ class AuthSrv(object):
|
||||
zs = vol.flags.get("rotf")
|
||||
if zs:
|
||||
use = True
|
||||
lim.set_rotf(zs, vol.flags.get("rotf_tz") or "UTC")
|
||||
lim.set_rotf(zs, vol.flags.get("rotf_tz", self.args.rotf_tz) or "UTC")
|
||||
|
||||
zs = vol.flags.get("maxn")
|
||||
if zs:
|
||||
@@ -2372,6 +2483,7 @@ class AuthSrv(object):
|
||||
vol.flags["dvthumb"] = True
|
||||
vol.flags["dathumb"] = True
|
||||
vol.flags["dithumb"] = True
|
||||
vol.flags["dethumb"] = True
|
||||
|
||||
have_fk = False
|
||||
for vol in vfs.all_nodes.values():
|
||||
@@ -2466,7 +2578,7 @@ class AuthSrv(object):
|
||||
if vf not in vol.flags:
|
||||
vol.flags[vf] = getattr(self.args, ga)
|
||||
|
||||
zs = "forget_ip gid nrand tail_who th_qv th_qvx th_spec_p u2abort u2ow uid unp_who ups_who zip_who"
|
||||
zs = "forget_ip gid md_nhist nrand tail_who th_qv th_qvx th_spec_p u2abort u2ow uid unp_who ups_who zip_who"
|
||||
for k in zs.split():
|
||||
if k in vol.flags:
|
||||
vol.flags[k] = int(vol.flags[k])
|
||||
@@ -2606,6 +2718,13 @@ class AuthSrv(object):
|
||||
t = "WARNING: volume [/%s]: invalid value specified for ext-th: %s"
|
||||
self.log(t % (vol.vpath, etv), 3)
|
||||
|
||||
zsl = [x.strip() for x in vol.flags["th_covers"].split(",")]
|
||||
zsl = [x for x in zsl if x not in ("", "no")]
|
||||
vol.flags["th_coversl"] = zsl
|
||||
vol.flags["th_coversd"] = zsl + ["." + x for x in zsl]
|
||||
vol.flags["th_covers_set"] = set(vol.flags["th_coversl"])
|
||||
vol.flags["th_coversd_set"] = set(vol.flags["th_coversd"])
|
||||
|
||||
zsl = [x.strip() for x in vol.flags["rw_edit"].split(",")]
|
||||
zsl = [x for x in zsl if x]
|
||||
vol.flags["rw_edit"] = ",".join(zsl)
|
||||
@@ -2619,6 +2738,8 @@ class AuthSrv(object):
|
||||
zsl4 = list(set([x.lower() for x in zsl2]))
|
||||
emb_all.update(zsl3)
|
||||
emb_all.update(zsl4)
|
||||
if "no_readme" in vol.flags:
|
||||
zsl1 = zsl2 = zsl3 = zsl4 = []
|
||||
vol.flags["emb_mds"] = [[0, zsl1, zsl3], [1, zsl2, zsl4]]
|
||||
|
||||
zsl1 = [x for x in vol.flags["prologues"].split(",") if x]
|
||||
@@ -2627,6 +2748,8 @@ class AuthSrv(object):
|
||||
zsl4 = list(set([x.lower() for x in zsl2]))
|
||||
emb_all.update(zsl3)
|
||||
emb_all.update(zsl4)
|
||||
if "no_logues" in vol.flags:
|
||||
zsl1 = zsl2 = zsl3 = zsl4 = []
|
||||
vol.flags["emb_lgs"] = [[0, zsl1, zsl3], [1, zsl2, zsl4]]
|
||||
|
||||
zs = str(vol.flags.get("html_head") or "")
|
||||
@@ -2643,17 +2766,23 @@ class AuthSrv(object):
|
||||
if head_s and not head_s.endswith("\n"):
|
||||
head_s += "\n"
|
||||
|
||||
zs2 = "Content-Security-Policy: %s\r\n"
|
||||
zs = vol.flags.get("csp_ui", "")
|
||||
csp_ui = zs2 % (zs,) if zs.lower() not in ("", "no") else ""
|
||||
zs = vol.flags.get("csp_dl", "")
|
||||
csp_dl = zs2 % (zs,) if zs.lower() not in ("", "no") else ""
|
||||
|
||||
zs = "X-Content-Type-Options: nosniff\r\n"
|
||||
if "norobots" in vol.flags:
|
||||
head_s += META_NOBOTS
|
||||
zs += "X-Robots-Tag: noindex, nofollow\r\n"
|
||||
if self.args.http_vary:
|
||||
zs += "Vary: %s\r\n" % (self.args.http_vary,)
|
||||
vol.flags["oh_g"] = zs + "\r\n"
|
||||
vol.flags["oh_g"] = zs + csp_ui + "\r\n"
|
||||
|
||||
if "noscript" in vol.flags:
|
||||
zs += "Content-Security-Policy: script-src 'none';\r\n"
|
||||
vol.flags["oh_f"] = zs + "\r\n"
|
||||
csp_dl = "Content-Security-Policy: script-src 'none';\r\n"
|
||||
vol.flags["oh_f"] = zs + csp_dl + "\r\n"
|
||||
|
||||
ico_url = vol.flags.get("ufavico")
|
||||
if ico_url:
|
||||
@@ -2739,7 +2868,8 @@ class AuthSrv(object):
|
||||
|
||||
for k in drop:
|
||||
t = 'cannot enable [%s] for volume "/%s" because this requires one of the following: e2d / e2ds / e2dsa (either as volflag or global-option)'
|
||||
self.log(t % (k, vol.vpath), 1)
|
||||
if not (enshare and vol.vpath.startswith(shrs)):
|
||||
self.log(t % (k, vol.vpath), 1)
|
||||
vol.flags.pop(k)
|
||||
|
||||
zi = vol.flags.get("lifetime") or 0
|
||||
@@ -2799,6 +2929,34 @@ class AuthSrv(object):
|
||||
zs = "select %s from up where rd=? and fn=?" % (", ".join(up_q),)
|
||||
vol.flags["ls_q_m"] = (zs if up_m else "", up_m)
|
||||
|
||||
zltss = [] # recheck
|
||||
for vn1 in vfs.all_nodes.values():
|
||||
if "show_hist" in vn1.flags or not vn1.realpath:
|
||||
continue
|
||||
ap = vn1.realpath.replace(os.sep, "/")
|
||||
apS = ap.rstrip("/") + "/"
|
||||
for vn2 in vfs.all_nodes.values():
|
||||
if not vn2.realpath:
|
||||
continue
|
||||
haps = [
|
||||
vn2.histpath.replace(os.sep, "/"),
|
||||
vn2.dbpath.replace(os.sep, "/"),
|
||||
]
|
||||
for hap in list(set(haps)):
|
||||
if not hap.startswith(apS):
|
||||
continue
|
||||
zs = hap[len(apS) :]
|
||||
if "/" in zs:
|
||||
zltss.append((vjoin(vn1.vpath, zs), hap))
|
||||
else:
|
||||
vn1.add("", zs, zs)
|
||||
for vp, hap in zltss:
|
||||
zss = vfs.get(vp, "", False, False)[0].axs.uget
|
||||
if zss:
|
||||
t = "note: /%s gives access to %r for %s"
|
||||
self.log(t % (vp, hap, zss), 3)
|
||||
zltss[:] = []
|
||||
|
||||
vfs.all_fvols = {
|
||||
zs: vol for zs, vol in vfs.all_vols.items() if "is_file" in vol.flags
|
||||
}
|
||||
@@ -2820,12 +2978,13 @@ class AuthSrv(object):
|
||||
continue
|
||||
try:
|
||||
bos.makedirs(vol.realpath, vf=vol.flags)
|
||||
files = os.listdir(vol.realpath)
|
||||
for fn in files:
|
||||
for fn, _ in statdir(
|
||||
self.log_func, not self.args.no_scandir, False, vol.realpath, True
|
||||
):
|
||||
fn2 = fn.lower()
|
||||
if fn == fn2:
|
||||
fn2 = fn.upper()
|
||||
if fn == fn2 or fn2 in files:
|
||||
if fn == fn2:
|
||||
continue
|
||||
is_ci = os.path.exists(os.path.join(vol.realpath, fn2))
|
||||
ccs = "y" if is_ci else "n"
|
||||
@@ -2851,6 +3010,17 @@ class AuthSrv(object):
|
||||
if ccs == "y":
|
||||
vol.flags["bcasechk"] = True
|
||||
|
||||
ptn = re.compile(r"^(,no|,sym|,hard|,ref)+=(sym|hard|ref)$")
|
||||
ptn2 = re.compile(r"(no|ref).*=|=sym")
|
||||
for vol in vfs.all_nodes.values():
|
||||
zs = vol.flags.get("redup")
|
||||
if zs and not ptn.match("," + zs):
|
||||
t = "invalid redup for volume /%s: %r"
|
||||
self.log(t % (vol.vpath, zs), 1)
|
||||
errors = True
|
||||
if zs and "e2ds" not in vol.flags and ptn2.search(zs):
|
||||
self.log(REDUP_E2 % (vol.vpath,), 3)
|
||||
|
||||
tags = self.args.mtp or []
|
||||
tags = [x.split("=")[0] for x in tags]
|
||||
tags = [y for x in tags for y in x.split(",")]
|
||||
@@ -2876,6 +3046,14 @@ class AuthSrv(object):
|
||||
excl.extend([r"^\.", r"/\."])
|
||||
vol.flags["srch_re_nodot"] = re.compile("|".join(excl))
|
||||
|
||||
for vol in list(vfs.all_nodes.values()):
|
||||
if "xvd" in vol.flags:
|
||||
for vol in vfs.all_nodes.values():
|
||||
vol.get = vol._get_xvd
|
||||
if vol.ls.__name__ == "_ls":
|
||||
vol.ls = vol._ls_xvd
|
||||
break
|
||||
|
||||
have_daw = False
|
||||
for vol in vfs.all_nodes.values():
|
||||
daw = vol.flags.get("daw") or self.args.daw
|
||||
@@ -2893,6 +3071,9 @@ class AuthSrv(object):
|
||||
errors = True
|
||||
|
||||
for vol in vfs.all_nodes.values():
|
||||
for zs in vf_bmap().values():
|
||||
if vol.flags.get(zs) in (None, False, ""):
|
||||
vol.flags.pop(zs, None)
|
||||
for k in list(vol.flags.keys()):
|
||||
if re.match("^-[^-]+$", k):
|
||||
vol.flags.pop(k)
|
||||
@@ -2907,6 +3088,22 @@ class AuthSrv(object):
|
||||
for name in vol.axs.uread:
|
||||
vol.axs.udot.add(name)
|
||||
|
||||
zi = 1
|
||||
zs = "; you must also enable --hack-me-bro to accept the total loss of security"
|
||||
if self.args.hack_me_bro:
|
||||
zi = 3
|
||||
zs = ""
|
||||
|
||||
if self.args.smb:
|
||||
t = "smb is enabled; this has serious security issues which will not be fixed"
|
||||
self.log(t + zs, zi)
|
||||
if zs:
|
||||
errors = True
|
||||
|
||||
if self.args.tftp:
|
||||
t = "tftp is enabled; primitive protocol with primitive access-restrictions"
|
||||
self.log(t, 3)
|
||||
|
||||
if errors:
|
||||
sys.exit(1)
|
||||
|
||||
@@ -2994,6 +3191,10 @@ class AuthSrv(object):
|
||||
t = "hint: enable upload deduplication with --dedup (but see readme for consequences)"
|
||||
self.log(t, 6)
|
||||
|
||||
if MACOS and not self.args.srch_nfkc:
|
||||
t = "hint: enable --srch-nfkc to improve unicode filename/path search on MacOS"
|
||||
self.log(t, 6)
|
||||
|
||||
zv, _ = vfs.get("/", "*", False, False)
|
||||
zs = zv.realpath.lower()
|
||||
if zs in ("/", "c:\\") or zs.startswith(r"c:\windows"):
|
||||
@@ -3004,6 +3205,8 @@ class AuthSrv(object):
|
||||
zv, _ = vfs.get("", "*", False, True, err=999)
|
||||
if self.warn_anonwrite and verbosity > 4 and os.getcwd() == zv.realpath:
|
||||
t = "anyone can write to the current directory: {}\n"
|
||||
if ANYWIN:
|
||||
t += "/!\\ NOTE: because you are using Windows, this is kinda dangerous (DLL-hijacking); you should configure accounts and volumes if this is accessible from an untrusted network\n"
|
||||
self.log(t.format(zv.realpath), c=1)
|
||||
|
||||
self.warn_anonwrite = False
|
||||
@@ -3082,10 +3285,10 @@ class AuthSrv(object):
|
||||
pwds.extend([x.split(":", 1)[1] for x in pwds if ":" in x])
|
||||
if pwds:
|
||||
if self.ah.on:
|
||||
zs = r"(\[H\] %s:.*|[?&]%s=)([^&]+)"
|
||||
zs = r"(\[[HO]\] %s:.*|[?&]%s=)([^&]+)"
|
||||
zs = zs % (self.args.pw_hdr, self.args.pw_urlp)
|
||||
else:
|
||||
zs = r"(\[H\] %s:.*|=)(" % (self.args.pw_hdr,)
|
||||
zs = r"(\[[HO]\] %s:.*|=)(" % (self.args.pw_hdr,)
|
||||
zs += "|".join(pwds) + r")([]&; ]|$)"
|
||||
|
||||
self.re_pwd = re.compile(zs)
|
||||
@@ -3114,7 +3317,13 @@ class AuthSrv(object):
|
||||
|
||||
try:
|
||||
s_vfs, s_rem = vfs.get(
|
||||
s_vp, s_un, "r" in s_pr, "w" in s_pr, "m" in s_pr, "d" in s_pr
|
||||
s_vp,
|
||||
s_un,
|
||||
"r" in s_pr,
|
||||
"w" in s_pr,
|
||||
"m" in s_pr,
|
||||
"d" in s_pr,
|
||||
"g" in s_pr,
|
||||
)
|
||||
except Exception as ex:
|
||||
t = "removing share [%s] by [%s] to [%s] due to %r"
|
||||
@@ -3147,6 +3356,13 @@ class AuthSrv(object):
|
||||
shn.dbpath = o_vn.dbpath
|
||||
shn.histpath = o_vn.histpath
|
||||
|
||||
zs = "assert_root daw dk dks dky e2ds e2dsa e2ts e2tsr fk fka landmark opds rss scan th_pregen"
|
||||
for zs2 in zs.split(" "):
|
||||
shn.flags.pop(zs2, None)
|
||||
zs = "srch_excl srch_re_dots srch_re_nodot th_coversd th_coversl"
|
||||
for zs2 in zs.split(" "):
|
||||
shn.flags[zs2] = []
|
||||
|
||||
# root.all_aps doesn't include any shares, so make a copy where the
|
||||
# share appears in all abspaths it can provide (for example for chk_ap)
|
||||
ap = shn.realpath
|
||||
@@ -3176,6 +3392,9 @@ class AuthSrv(object):
|
||||
# transplant shadowing into shares
|
||||
for vn in shv.nodes.values():
|
||||
svn, srem = vn.shr_src # type: ignore
|
||||
if "show_hist" not in vn.flags:
|
||||
zvn = VFS(self.log_func, "", "", "", AXS(), self.vf0())
|
||||
vn.nodes[".hist"] = zvn
|
||||
if srem:
|
||||
continue # free branch, safe
|
||||
ap = svn.canonical(srem)
|
||||
@@ -3189,8 +3408,42 @@ class AuthSrv(object):
|
||||
cur.close()
|
||||
db.close()
|
||||
|
||||
zb1 = zb2 = False
|
||||
nodes = [self.vfs]
|
||||
while nodes:
|
||||
vn = nodes.pop()
|
||||
nodes += list(vn.nodes.values())
|
||||
if (
|
||||
not vn.realpath
|
||||
or "bcasechk" not in vn.flags
|
||||
or (not vn.axs.uread and not vn.axs.uwrite and not vn.axs.uget)
|
||||
):
|
||||
continue
|
||||
if ".hist" in vn.nodes and "e2d" in vn.flags:
|
||||
zb1 = True
|
||||
for zs, vnn in vn.nodes.items():
|
||||
if zs != ".hist" and (vnn.realpath or "").startswith(vn.realpath):
|
||||
t = "nested volume on casefolding filesystem: [/%s] inside [/%s]"
|
||||
self.log(t % (vnn.vpath, vn.vpath), 3)
|
||||
zb2 = True
|
||||
if PY2:
|
||||
continue
|
||||
vn.lnodes = {
|
||||
k.casefold().encode("ascii", "ignore"): v for k, v in vn.nodes.items()
|
||||
}
|
||||
if zb2 and PY2:
|
||||
t = "SECURITY WARNING: due to python2, shadowing will NOT provide much security:"
|
||||
self.log(t, 1)
|
||||
if zb2:
|
||||
t = "SECURITY WARNING: you have a case-insensitive / casefolding filesystem, and you also have nested volumes (subvolumes). This combination is NOT 100% bulletproof; it *might* be possible for someone to sneakily access a subvolume they're not supposed to have access to, if they have access to the parent volume."
|
||||
self.log(t, 3)
|
||||
if zb1:
|
||||
t = "you have a case-insensitive / casefolding filesystem, so it is recommended to set --hist C:\\copyparty-data\\ or another safe place"
|
||||
self.log(t, 3)
|
||||
|
||||
self.js_ls = {}
|
||||
self.js_htm = {}
|
||||
self.js_htm = ""
|
||||
self.md_htm = ""
|
||||
for vp, vn in self.vfs.all_nodes.items():
|
||||
if enshare and vp.startswith(shrs):
|
||||
continue # propagates later in this func
|
||||
@@ -3207,7 +3460,7 @@ class AuthSrv(object):
|
||||
"dth3x": vf["th3x"],
|
||||
"u2ts": vf["u2ts"],
|
||||
"shr_who": vf["shr_who"],
|
||||
"frand": bool(vf.get("rand")),
|
||||
"frand": "rand" in vf,
|
||||
"lifetime": vf.get("lifetime") or 0,
|
||||
"unlist": vf.get("unlist") or "",
|
||||
"sb_lg": "" if "no_sb_lg" in vf else (vf.get("lg_sbf") or "y"),
|
||||
@@ -3257,6 +3510,7 @@ class AuthSrv(object):
|
||||
"idxh": int(self.args.ih),
|
||||
"dutc": not self.args.localtime,
|
||||
"dfszf": self.args.ui_filesz.strip("-"),
|
||||
"dgauto": self.args.gauto,
|
||||
"themes": self.args.themes,
|
||||
"turbolvl": self.args.turbo,
|
||||
"nosubtle": self.args.nosubtle,
|
||||
@@ -3264,7 +3518,7 @@ class AuthSrv(object):
|
||||
"u2sz": self.args.u2sz,
|
||||
"u2ts": vf["u2ts"],
|
||||
"u2ow": vf["u2ow"],
|
||||
"frand": bool(vf.get("rand")),
|
||||
"frand": "rand" in vf,
|
||||
"lifetime": vn.js_ls["lifetime"],
|
||||
"u2sort": self.args.u2sort,
|
||||
}
|
||||
@@ -3272,7 +3526,7 @@ class AuthSrv(object):
|
||||
for zs in zs.split():
|
||||
if vf.get(zs):
|
||||
js_htm[zs] = 1
|
||||
zs = "notooltips"
|
||||
zs = "glang notooltips"
|
||||
for zs in zs.split():
|
||||
if getattr(self.args, zs, False):
|
||||
js_htm[zs] = 1
|
||||
@@ -3281,7 +3535,15 @@ class AuthSrv(object):
|
||||
zs2 = getattr(self.args, zs, "")
|
||||
if zs2:
|
||||
js_htm[zs] = zs2
|
||||
if self.args.wopi:
|
||||
js_htm["have_wopi"] = True
|
||||
|
||||
zs = "have_emp md_no_br"
|
||||
md_htm = {x: js_htm[x] for x in zs.split(" ")}
|
||||
md_htm["modpoll_freq"] = self.args.mcr
|
||||
|
||||
vn.js_htm = json_hesc(json.dumps(js_htm))
|
||||
vn.md_htm = json_hesc(json.dumps(md_htm))
|
||||
|
||||
vols = list(vfs.all_nodes.values())
|
||||
if enshare:
|
||||
@@ -3300,6 +3562,7 @@ class AuthSrv(object):
|
||||
dbv = vol.get_dbv("")[0]
|
||||
vol.js_ls = vol.js_ls or dbv.js_ls or {}
|
||||
vol.js_htm = vol.js_htm or dbv.js_htm or "{}"
|
||||
vol.md_htm = vol.md_htm or dbv.md_htm or "{}"
|
||||
|
||||
zs = str(vol.flags.get("tcolor") or self.args.tcolor)
|
||||
vol.flags["tcolor"] = zs.lstrip("#")
|
||||
@@ -3727,12 +3990,12 @@ class AuthSrv(object):
|
||||
"",
|
||||
]
|
||||
|
||||
csv = set("i p th_covers zm_on zm_off zs_on zs_off".split())
|
||||
csv = set("i p zm_on zm_off zs_on zs_off".split())
|
||||
zs = "c ihead ohead mtm mtp on403 on404 xac xad xar xau xiu xban xbc xbd xbr xbu xm"
|
||||
lst = set(zs.split())
|
||||
askip = set("a v c vc cgen exp_lg exp_md theme".split())
|
||||
|
||||
t = "exp_lg exp_md ext_th_d mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot"
|
||||
t = "emb_all emb_lgs emb_mds exp_lg exp_md ext_th_d ls_q_m mv_re_r mv_re_t rm_re_r rm_re_t oh_f oh_g rw_edit_set srch_re_dots srch_re_nodot th_coversd th_coversl th_covers_set th_coversd_set"
|
||||
fskip = set(t.split())
|
||||
|
||||
# keymap from argv to vflag
|
||||
@@ -3859,6 +4122,9 @@ class AuthSrv(object):
|
||||
except:
|
||||
pass
|
||||
|
||||
if k in ("chmod_d", "chmod_f"):
|
||||
v = "%o" % (v,)
|
||||
|
||||
try:
|
||||
ak = vmap[k]
|
||||
v2 = getattr(self.args, ak)
|
||||
@@ -3868,7 +4134,7 @@ class AuthSrv(object):
|
||||
except:
|
||||
pass
|
||||
|
||||
if v2 is v:
|
||||
if v2 == v:
|
||||
continue
|
||||
except:
|
||||
pass
|
||||
@@ -3912,7 +4178,7 @@ class AuthSrv(object):
|
||||
|
||||
def derive_args(args: argparse.Namespace) -> None:
|
||||
args.have_idp_hdrs = bool(args.idp_h_usr or args.idp_hm_usr)
|
||||
args.have_ipu_or_ipr = bool(args.ipu or args.ipr)
|
||||
args.have_ipu_or_ipr = bool(args.ipu or args.ipr or args.wopi)
|
||||
|
||||
|
||||
def n_du_who(s: str) -> int:
|
||||
@@ -3962,10 +4228,14 @@ def split_cfg_ln(ln: str) -> dict[str, Any]:
|
||||
|
||||
|
||||
def expand_config_file(
|
||||
log: Optional["NamedLogger"], ret: list[str], fp: str, ipath: str
|
||||
log: Optional["NamedLogger"],
|
||||
shenvexp: "Callable[[str], str]",
|
||||
ret: list[str],
|
||||
fp: str,
|
||||
ipath: str,
|
||||
) -> None:
|
||||
"""expand all % file includes"""
|
||||
fp = absreal(fp)
|
||||
fp = absreal(os.path.expanduser(shenvexp(fp)))
|
||||
if len(ipath.split(" -> ")) > 64:
|
||||
raise Exception("hit max depth of 64 includes")
|
||||
|
||||
@@ -3996,7 +4266,12 @@ def expand_config_file(
|
||||
if fp2 in ipath:
|
||||
continue
|
||||
|
||||
expand_config_file(log, ret, fp2, ipath)
|
||||
if CFG_DEF and CFG_DEF[0] == fp2:
|
||||
t = "ignoring PRTY_CONFIG because it was also autodiscovered by %s -> %s"
|
||||
LOG[0]("root", t % (ipath, fp))
|
||||
continue
|
||||
|
||||
expand_config_file(log, shenvexp, ret, fp2, ipath)
|
||||
|
||||
return
|
||||
|
||||
@@ -4021,7 +4296,7 @@ def expand_config_file(
|
||||
fp2 = ln[1:].strip()
|
||||
fp2 = os.path.join(os.path.dirname(fp), fp2)
|
||||
ofs = len(ret)
|
||||
expand_config_file(log, ret, fp2, ipath)
|
||||
expand_config_file(log, shenvexp, ret, fp2, ipath)
|
||||
for n in range(ofs, len(ret)):
|
||||
ret[n] = pad + ret[n]
|
||||
continue
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
|
||||
+15
-12
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import threading
|
||||
import time
|
||||
@@ -146,24 +146,27 @@ class BrokerMp(object):
|
||||
returns a Queue object which eventually contains the response if want_retval
|
||||
(not-impl here since nothing uses it yet)
|
||||
"""
|
||||
if dest == "httpsrv.listen":
|
||||
for p in self.procs:
|
||||
p.q_pend.put((0, dest, [args[0], len(self.procs)]))
|
||||
|
||||
elif dest == "httpsrv.set_netdevs":
|
||||
for p in self.procs:
|
||||
p.q_pend.put((0, dest, list(args)))
|
||||
if dest.startswith("httpsrv."):
|
||||
if dest == "httpsrv.listen":
|
||||
for p in self.procs:
|
||||
p.q_pend.put((0, dest, [args[0], len(self.procs)]))
|
||||
else:
|
||||
for p in self.procs:
|
||||
p.q_pend.put((0, dest, list(args)))
|
||||
|
||||
elif dest == "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:
|
||||
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:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import os
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import threading
|
||||
@@ -58,13 +58,8 @@ class BrokerThr(BrokerCli):
|
||||
self.httpsrv.listen(args[0], 1)
|
||||
return
|
||||
|
||||
if dest == "httpsrv.set_netdevs":
|
||||
self.httpsrv.set_netdevs(args[0])
|
||||
return
|
||||
|
||||
if dest == "httpsrv.set_bad_ver":
|
||||
self.httpsrv.set_bad_ver()
|
||||
return
|
||||
getattr(self.httpsrv, dest[8:])(*args)
|
||||
return
|
||||
|
||||
# new ipc invoking managed service in hub
|
||||
obj = self.hub
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
|
||||
@@ -16,6 +16,7 @@ if True: # pylint: disable=using-constant-test
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .httpsrv import HttpSrv
|
||||
from .svchub import SvcHub
|
||||
|
||||
|
||||
class ExceptionalQueue(Queue, object):
|
||||
@@ -50,6 +51,7 @@ class BrokerCli(object):
|
||||
for example resolving httpconn.* in httpcli -- see lines tagged #mypy404
|
||||
"""
|
||||
|
||||
hub: "SvcHub"
|
||||
log: "RootLogger"
|
||||
args: argparse.Namespace
|
||||
asrv: AuthSrv
|
||||
|
||||
+13
-5
@@ -1,3 +1,6 @@
|
||||
# coding: utf-8
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import calendar
|
||||
import errno
|
||||
import json
|
||||
@@ -8,6 +11,7 @@ import time
|
||||
from .__init__ import ANYWIN
|
||||
from .util import Netdev, atomic_move, load_resource, runcmd, wunlink
|
||||
|
||||
NO_TLS = bool(os.environ.get("PRTY_NO_TLS"))
|
||||
HAVE_CFSSL = not os.environ.get("PRTY_NO_CFSSL")
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
@@ -51,9 +55,12 @@ def ensure_cert(log: "RootLogger", args) -> None:
|
||||
with open(args.cert, "wb") as f:
|
||||
f.write(cert_insec)
|
||||
|
||||
if args.certkey and not os.path.isfile(args.certkey):
|
||||
raise Exception("certificate-key file does not exist: " + args.certkey)
|
||||
|
||||
with open(args.cert, "rb") as f:
|
||||
buf = f.read()
|
||||
o1 = buf.find(b" PRIVATE KEY-")
|
||||
o1 = buf.find(b" PRIVATE KEY-") if not args.certkey else 0
|
||||
o2 = buf.find(b" CERTIFICATE-")
|
||||
m = "unsupported certificate format: "
|
||||
if o1 < 0:
|
||||
@@ -67,8 +74,9 @@ def ensure_cert(log: "RootLogger", args) -> None:
|
||||
with open(args.cert, "rb") as f:
|
||||
active_cert = f.read()
|
||||
if active_cert == cert_insec:
|
||||
t = "using default TLS certificate; https will be insecure:\033[36m {}"
|
||||
log("cert", t.format(args.cert), 3)
|
||||
t = "using default TLS certificate; %s will be insecure:\033[36m %s"
|
||||
t2 = "https" if not args.ftps else "ftps"
|
||||
log("cert", t % (t2, args.cert), 3)
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -249,10 +257,10 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
||||
def gencert(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
||||
global HAVE_CFSSL
|
||||
|
||||
if args.http_only:
|
||||
if args.http_only and not args.ftps:
|
||||
return
|
||||
|
||||
if args.no_crt or not HAVE_CFSSL:
|
||||
if args.no_crt or args.certkey or not HAVE_CFSSL:
|
||||
ensure_cert(log, args)
|
||||
return
|
||||
|
||||
|
||||
+36
-2
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
# awk -F\" '/add_argument\("-[^-]/{print(substr($2,2))}' copyparty/__main__.py | sort | tr '\n' ' '
|
||||
zs = "a c e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vp e2vu ed emp i j lo mcr mte mth mtm mtp nb nc nid nih nth nw p q s ss sss v z zv"
|
||||
@@ -21,8 +21,11 @@ def vf_bmap() -> dict[str, str]:
|
||||
"no_dupe": "nodupe",
|
||||
"no_dupe_m": "nodupem",
|
||||
"no_forget": "noforget",
|
||||
"no_html": "nohtml",
|
||||
"no_mime": "nomime",
|
||||
"no_pipe": "nopipe",
|
||||
"no_robots": "norobots",
|
||||
"no_script": "noscript",
|
||||
"no_tail": "notail",
|
||||
"no_thumb": "dthumb",
|
||||
"no_vthumb": "dvthumb",
|
||||
@@ -31,8 +34,10 @@ def vf_bmap() -> dict[str, str]:
|
||||
"vol_or_crash": "assert_root",
|
||||
}
|
||||
for k in (
|
||||
"allow_svg_js",
|
||||
"dedup",
|
||||
"dlni",
|
||||
"dothidden",
|
||||
"dotsrch",
|
||||
"e2d",
|
||||
"e2ds",
|
||||
@@ -47,9 +52,12 @@ def vf_bmap() -> dict[str, str]:
|
||||
"grid",
|
||||
"gsel",
|
||||
"hardlink",
|
||||
"hist_cow",
|
||||
"magic",
|
||||
"md_no_br",
|
||||
"no_db_ip",
|
||||
"no_logues",
|
||||
"no_readme",
|
||||
"no_sb_md",
|
||||
"no_sb_lg",
|
||||
"nsort",
|
||||
@@ -58,10 +66,12 @@ def vf_bmap() -> dict[str, str]:
|
||||
"og_s_title",
|
||||
"opds",
|
||||
"rand",
|
||||
"redup_dry",
|
||||
"reflink",
|
||||
"rm_partial",
|
||||
"rmagic",
|
||||
"rss",
|
||||
"show_hist",
|
||||
"ui_noacci",
|
||||
"ui_nocpla",
|
||||
"ui_nolbar",
|
||||
@@ -102,6 +112,8 @@ def vf_vmap() -> dict[str, str]:
|
||||
"casechk",
|
||||
"chmod_d",
|
||||
"chmod_f",
|
||||
"csp_ui",
|
||||
"csp_dl",
|
||||
"dbd",
|
||||
"db_xattr",
|
||||
"du_who",
|
||||
@@ -117,6 +129,7 @@ def vf_vmap() -> dict[str, str]:
|
||||
"lg_sba",
|
||||
"md_sba",
|
||||
"md_hist",
|
||||
"md_nhist",
|
||||
"nrand",
|
||||
"u2ow",
|
||||
"og_desc",
|
||||
@@ -134,6 +147,8 @@ def vf_vmap() -> dict[str, str]:
|
||||
"put_ck",
|
||||
"put_name",
|
||||
"readmes",
|
||||
"redup",
|
||||
"rotf_tz",
|
||||
"mv_retry",
|
||||
"rm_retry",
|
||||
"rss_sort",
|
||||
@@ -147,6 +162,8 @@ def vf_vmap() -> dict[str, str]:
|
||||
"tail_tmax",
|
||||
"tail_who",
|
||||
"tcolor",
|
||||
"th_covers",
|
||||
"th_pregen",
|
||||
"th_qv",
|
||||
"th_qvx",
|
||||
"th_spec_p",
|
||||
@@ -214,6 +231,8 @@ flagcats = {
|
||||
"hardlinkonly": "dedup with hardlink only, never symlink;\nmake a full copy if hardlink is impossible",
|
||||
"reflink": "enable reflink-based file deduplication,\nwith fallback on full copy when that is impossible",
|
||||
"safededup": "verify on-disk data before using it for dedup",
|
||||
"redup=A,B=T": "convert dedup-type for existing files",
|
||||
"redup_dry": "dry-run mode for redup",
|
||||
"noclone": "take dupe data from clients, even if available on HDD",
|
||||
"nodupe": "rejects existing files (instead of linking/cloning them)",
|
||||
"nodupem": "rejects existing files during moves as well",
|
||||
@@ -285,6 +304,7 @@ flagcats = {
|
||||
"no_db_ip": "never store uploader-IP in the db; disables unpost",
|
||||
"fat32": "avoid excessive reindexing on android sdcardfs",
|
||||
"dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
|
||||
"hist_cow": "btrfs: keep copy-on-write enabled on db/thumbs",
|
||||
"xlink": "cross-volume dupe detection / linking (dangerous)",
|
||||
"xdev": "do not descend into other filesystems",
|
||||
"xvol": "do not follow symlinks leaving the volume root",
|
||||
@@ -304,16 +324,19 @@ flagcats = {
|
||||
"dvthumb": "disables video thumbnails",
|
||||
"dathumb": "disables audio thumbnails (spectrograms)",
|
||||
"dithumb": "disables image thumbnails",
|
||||
"dethumb": "disables custom thumbnails (--th-extract)",
|
||||
"pngquant": "compress audio waveforms 33% better",
|
||||
"thsize": "thumbnail res; WxH",
|
||||
"crop": "center-cropping (y/n/fy/fn)",
|
||||
"th3x": "3x resolution (y/n/fy/fn)",
|
||||
"th_qv=40": "webp/jpg thumbnail quality (10~90)",
|
||||
"th_qvx=40": "jxl thumbnail quality (10~90)",
|
||||
"th_covers=a.jpg,a.png": "filenames to prefer as folder-thumbs",
|
||||
"convt": "convert-to-image timeout in seconds",
|
||||
"aconvt": "convert-to-audio timeout in seconds",
|
||||
"th_spec_p=1": "make spectrograms? 0=never 1=fallback 2=always",
|
||||
"ext_th=s=/b.png": "use /b.png as thumbnail for file-extension s",
|
||||
"th_pregen=w,wf": "pregenerate thumbs for these formats",
|
||||
},
|
||||
"handlers\n(better explained in --help-handlers)": {
|
||||
"on404=PY": "handle 404s by executing PY file",
|
||||
@@ -340,6 +363,7 @@ flagcats = {
|
||||
"hsortn": "number of sort-rules to add to media URLs",
|
||||
"ufavico=URL": "per-volume favicon (.ico/png/gif/svg)",
|
||||
"unlist": "dont list files matching REGEX",
|
||||
"dothidden": "enable support for .hidden files",
|
||||
"dlni": "force-download (no-inline) files on click",
|
||||
"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>",
|
||||
@@ -354,6 +378,10 @@ flagcats = {
|
||||
"epilogues=.epilogue.html": "files to embed below/after files",
|
||||
"readmes=readme.md,README.md": "files to embed as readmes",
|
||||
"preadmes=preadme.md,PREADME.md": "files to embed as preadmes",
|
||||
"plainreadme": "embed readme/preadme as monospace plaintext",
|
||||
"plainlogues": "embed prologue/epilogue as sans-serif plaintext",
|
||||
"no_readme": "disable embedding readme/preadme",
|
||||
"no_logues": "disable embedding prologue/epilogue",
|
||||
"no_sb_md": "disable js sandbox for markdown files",
|
||||
"no_sb_lg": "disable js sandbox for prologue/epilogue",
|
||||
"sb_md": "enable js sandbox for markdown files (default)",
|
||||
@@ -362,8 +390,12 @@ flagcats = {
|
||||
"lg_sbf": "list of *logue-sandbox safeguards to disable",
|
||||
"md_sba": "value of iframe allow-prop for markdown-sandbox",
|
||||
"lg_sba": "value of iframe allow-prop for *logue-sandbox",
|
||||
"csp_ui": "content-security-policy for the web-UI",
|
||||
"csp_dl": "content-security-policy for static files",
|
||||
"nohtml": "return html and markdown as text/html",
|
||||
"nomime": "disallow ?mime to change response type",
|
||||
"noscript": "disable most javascript by use of CSP",
|
||||
"allow_svg_js": "allow javascript in svg images",
|
||||
"ui_noacci": "hide account-info in the UI",
|
||||
"ui_nocpla": "hide cpanel-link in the UI",
|
||||
"ui_nolbar": "hide link-bar in the UI",
|
||||
@@ -395,7 +427,9 @@ flagcats = {
|
||||
"textfiles": {
|
||||
"rw_edit=md,txt": "only require read+write to edit .md and .txt",
|
||||
"md_no_br": "newline only on double-newline or two tailing spaces",
|
||||
"md_hist": "where to put markdown backups; s=subfolder, v=volHist, n=nope",
|
||||
"md_hist": "where to put text-editor backups; s=subfolder, v=volHist, n=nope",
|
||||
"md_nhist=99": "how many text-editor backups to keep",
|
||||
"show_hist": "allow browsing the .hist folder (old textfiles)",
|
||||
"exp": "enable textfile expansion; see --help-exp",
|
||||
"exp_md": "placeholders to expand in markdown files; see --help",
|
||||
"exp_lg": "placeholders to expand in prologue/epilogue; see --help",
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
+27
-7
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import json
|
||||
@@ -7,10 +7,10 @@ import os
|
||||
import re
|
||||
import time
|
||||
|
||||
from .__init__ import ANYWIN, MACOS
|
||||
from .__init__ import ANYWIN, FREEBSD, MACOS, UNIX
|
||||
from .authsrv import AXS, VFS, AuthSrv
|
||||
from .bos import bos
|
||||
from .util import chkcmd, json_hesc, min_ex, undot
|
||||
from .util import chkcmd, fsenc, json_hesc, min_ex, runcmd, undot
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Optional, Union
|
||||
@@ -18,6 +18,11 @@ if True: # pylint: disable=using-constant-test
|
||||
from .util import RootLogger, undot
|
||||
|
||||
|
||||
PTN_FILEFRAG = re.compile(
|
||||
r"^\s*[0-9]+:\s*[0-9]+\.\.\s*[0-9]+:\s*([0-9]+)\.\.\s*([0-9]+):\s*[0-9]+:\s*(.*)$"
|
||||
)
|
||||
|
||||
|
||||
class Fstab(object):
|
||||
def __init__(self, log: "RootLogger", args: argparse.Namespace, verbose: bool):
|
||||
self.log_func = log
|
||||
@@ -88,7 +93,7 @@ class Fstab(object):
|
||||
|
||||
def _from_sp_mount(self) -> dict[str, str]:
|
||||
sptn = r"^.*? on (.*) type ([^ ]+) \(.*"
|
||||
if MACOS:
|
||||
if MACOS or FREEBSD:
|
||||
sptn = r"^.*? on (.*) \(([^ ]+), .*"
|
||||
|
||||
ptn = re.compile(sptn)
|
||||
@@ -118,7 +123,7 @@ class Fstab(object):
|
||||
|
||||
def build_tab(self) -> None:
|
||||
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
|
||||
srctab = str(sorted(dtab.items()))
|
||||
@@ -130,7 +135,7 @@ class Fstab(object):
|
||||
|
||||
try:
|
||||
fuses = [mp for mp, fs in dtab.items() if fs == "fuseblk"]
|
||||
if not fuses or MACOS:
|
||||
if not fuses or UNIX:
|
||||
raise Exception()
|
||||
try:
|
||||
so, _ = chkcmd(["lsblk", "-nrfo", "FSTYPE,MOUNTPOINT"]) # centos6
|
||||
@@ -248,7 +253,7 @@ def ramdisk_chk(asrv: AuthSrv) -> None:
|
||||
t2 = ["\n volume=[/%s], abspath=%r, type=%s, root=%r" % x for x in mods]
|
||||
log("vfs", t + "".join(t2) + "\n", 1)
|
||||
|
||||
assume = "mac" if MACOS else "lin"
|
||||
assume = "mac" if MACOS else "win" if ANYWIN else "lin"
|
||||
for vol in asrv.vfs.all_nodes.values():
|
||||
if not vol.realpath or vol.flags.get("is_file"):
|
||||
continue
|
||||
@@ -263,3 +268,18 @@ def ramdisk_chk(asrv: AuthSrv) -> None:
|
||||
htm = json.loads(vol.js_htm)
|
||||
vol.flags["fsnt"] = vol.js_ls["fsnt"] = htm["fsnt"] = fs
|
||||
vol.js_htm = json_hesc(json.dumps(htm))
|
||||
# md_htm is dontcare; only relevant for up2k request body
|
||||
|
||||
|
||||
def filefrag(ap: str) -> list[str]:
|
||||
rc, so, se = runcmd([b"filefrag", b"-v", fsenc(ap)], timeout=10)
|
||||
ptn = PTN_FILEFRAG
|
||||
ret = []
|
||||
for ln in so.split("\n"):
|
||||
m = ptn.match(ln)
|
||||
if not m:
|
||||
continue
|
||||
a, b, s = m.groups()
|
||||
if "shared" in s.strip().split(","):
|
||||
ret.append((a, b))
|
||||
return ret
|
||||
+71
-32
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import errno
|
||||
@@ -15,7 +15,7 @@ from pyftpdlib.handlers import FTPHandler
|
||||
from pyftpdlib.ioloop import IOLoop
|
||||
from pyftpdlib.servers import FTPServer
|
||||
|
||||
from .__init__ import PY2, TYPE_CHECKING
|
||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING
|
||||
from .authsrv import VFS
|
||||
from .bos import bos
|
||||
from .util import (
|
||||
@@ -24,10 +24,11 @@ from .util import (
|
||||
ODict,
|
||||
Pebkac,
|
||||
exclude_dotfiles,
|
||||
exclude_dothidden,
|
||||
fsenc,
|
||||
ipnorm,
|
||||
pybin,
|
||||
relchk,
|
||||
read_utf8,
|
||||
runhook,
|
||||
sanitize_fn,
|
||||
set_fperms,
|
||||
@@ -147,6 +148,7 @@ class FtpFs(AbstractedFS):
|
||||
self.hub: "SvcHub" = cmd_channel.hub
|
||||
self.args = cmd_channel.args
|
||||
self.uname = cmd_channel.uname
|
||||
self.log_src = "%s:FTP" % (cmd_channel.cli_ip,)
|
||||
|
||||
self.cwd = "/" # pyftpdlib convention of leading slash
|
||||
self.root = "/var/lib/empty"
|
||||
@@ -154,8 +156,8 @@ class FtpFs(AbstractedFS):
|
||||
self.listdirinfo = self.listdir
|
||||
self.chdir(".")
|
||||
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.hub.log("ftpd", msg, c)
|
||||
def clog(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.hub.log(self.log_src, msg, c)
|
||||
|
||||
def v2a(
|
||||
self,
|
||||
@@ -166,16 +168,23 @@ class FtpFs(AbstractedFS):
|
||||
d: bool = False,
|
||||
) -> tuple[str, VFS, str]:
|
||||
try:
|
||||
vpath = vpath.replace("\\", "/").strip("/")
|
||||
rd, fn = os.path.split(vpath)
|
||||
if relchk(rd):
|
||||
logging.warning("malicious vpath: %s", vpath)
|
||||
t = "Unsupported characters in [{}]"
|
||||
raise FSE(t.format(vpath), 1)
|
||||
# because:
|
||||
# * ftp-spec doesn't define sep
|
||||
# * windows filename limitations
|
||||
# need to allow/translate illegal fn,
|
||||
# but require filesystem-correct rd
|
||||
if ANYWIN or ("\\" in vpath and "/" not in vpath):
|
||||
vpath = vpath.replace("\\", "/")
|
||||
vpath = vpath.strip("/\\")
|
||||
if ANYWIN:
|
||||
rd, fn = os.path.split(vpath)
|
||||
fn = sanitize_fn(fn, "win")
|
||||
vpath = vjoin(rd, fn)
|
||||
# end of windows jank
|
||||
|
||||
fn = sanitize_fn(fn or "")
|
||||
vpath = vjoin(rd, fn)
|
||||
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
|
||||
vpath = vjoin(vfs.vpath, rem)
|
||||
rd, fn = os.path.split(vpath)
|
||||
if (
|
||||
w
|
||||
and fn.lower() in vfs.flags["emb_all"]
|
||||
@@ -190,18 +199,7 @@ class FtpFs(AbstractedFS):
|
||||
t = "No filesystem mounted at [{}]"
|
||||
raise FSE(t.format(vpath))
|
||||
|
||||
if "xdev" in vfs.flags or "xvol" in vfs.flags:
|
||||
ap = vfs.canonical(rem)
|
||||
avfs = vfs.chk_ap(ap)
|
||||
t = "Permission denied in [{}]"
|
||||
if not avfs:
|
||||
raise FSE(t.format(vpath), 1)
|
||||
|
||||
cr, cw, cm, cd, _, _, _, _, _ = avfs.uaxs[self.h.uname]
|
||||
if r and not cr or w and not cw or m and not cm or d and not cd:
|
||||
raise FSE(t.format(vpath), 1)
|
||||
else:
|
||||
ap = vfs.canonical(rem, False)
|
||||
ap = vfs.canonical(rem, False)
|
||||
|
||||
if "bcasechk" in vfs.flags and not vfs.casechk(rem, True):
|
||||
raise FSE("No such file or directory", 1)
|
||||
@@ -236,6 +234,8 @@ class FtpFs(AbstractedFS):
|
||||
return True
|
||||
|
||||
def open(self, filename: str, mode: str) -> typing.IO[Any]:
|
||||
self.clog("open(%r,%r) @%s" % (filename, mode, self.uname))
|
||||
|
||||
r = "r" in mode
|
||||
w = "w" in mode or "a" in mode or "+" in mode
|
||||
|
||||
@@ -253,7 +253,7 @@ class FtpFs(AbstractedFS):
|
||||
xbu = vfs.flags.get("xbu")
|
||||
if xbu:
|
||||
hr = runhook(
|
||||
self.log,
|
||||
self.clog,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.ftp",
|
||||
@@ -273,7 +273,7 @@ class FtpFs(AbstractedFS):
|
||||
if t or hr.get("rc") != 0:
|
||||
if not t:
|
||||
t = "upload blocked by xbu server config: %r" % (filename,)
|
||||
self.log(t, 3)
|
||||
self.clog(t, 3)
|
||||
raise FSE(t)
|
||||
|
||||
if w and need_unlink: # type: ignore # !rm
|
||||
@@ -298,17 +298,22 @@ class FtpFs(AbstractedFS):
|
||||
|
||||
# Don't unlink file for append mode
|
||||
elif "a" not in mode:
|
||||
wunlink(self.log, ap, VF_CAREFUL)
|
||||
wunlink(self.clog, ap, VF_CAREFUL)
|
||||
|
||||
ret = open(fsenc(ap), mode, self.args.iobuf)
|
||||
if w and "fperms" in vfs.flags:
|
||||
set_fperms(ret, vfs.flags)
|
||||
|
||||
self.clog(" `-- %r" % (ap,))
|
||||
return ret
|
||||
|
||||
def chdir(self, path: str) -> None:
|
||||
nwd = join(self.cwd, path)
|
||||
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
|
||||
try:
|
||||
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
|
||||
except Exception as ex:
|
||||
raise FSE(str(ex))
|
||||
|
||||
if not vfs.realpath:
|
||||
self.cwd = nwd
|
||||
return
|
||||
@@ -329,10 +334,12 @@ class FtpFs(AbstractedFS):
|
||||
self.cwd = nwd
|
||||
|
||||
def mkdir(self, path: str) -> None:
|
||||
self.clog("mkdir(%r) @%s" % (path, self.uname))
|
||||
ap, vfs, _ = self.rv2a(path, w=True)
|
||||
bos.makedirs(ap, vf=vfs.flags) # filezilla expects this
|
||||
|
||||
def listdir(self, path: str) -> list[str]:
|
||||
self.clog("ls(%r) @%s" % (path, self.uname))
|
||||
vpath = join(self.cwd, path)
|
||||
try:
|
||||
ap, vfs, rem = self.v2a(vpath, True, False)
|
||||
@@ -350,7 +357,10 @@ class FtpFs(AbstractedFS):
|
||||
vfs_ls.extend(vfs_virt.keys())
|
||||
|
||||
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()
|
||||
return vfs_ls
|
||||
@@ -379,6 +389,7 @@ class FtpFs(AbstractedFS):
|
||||
return ret
|
||||
|
||||
def rmdir(self, path: str) -> None:
|
||||
self.clog("rmdir(%r) @%s" % (path, self.uname))
|
||||
ap = self.rv2a(path, d=True)[0]
|
||||
try:
|
||||
bos.rmdir(ap)
|
||||
@@ -387,6 +398,7 @@ class FtpFs(AbstractedFS):
|
||||
raise
|
||||
|
||||
def remove(self, path: str) -> None:
|
||||
self.clog("rm(%r) @%s" % (path, self.uname))
|
||||
if self.args.no_del:
|
||||
raise FSE("The delete feature is disabled in server config")
|
||||
|
||||
@@ -397,6 +409,7 @@ class FtpFs(AbstractedFS):
|
||||
raise FSE(str(ex))
|
||||
|
||||
def rename(self, src: str, dst: str) -> None:
|
||||
self.clog("mv(%r,%r) @%s" % (src, dst, self.uname))
|
||||
if self.args.no_mv:
|
||||
raise FSE("The rename/move feature is disabled in server config")
|
||||
|
||||
@@ -426,6 +439,7 @@ class FtpFs(AbstractedFS):
|
||||
return st
|
||||
|
||||
def utime(self, path: str, timeval: float) -> None:
|
||||
self.clog("utime(%r,%r) @%s" % (path, timeval, self.uname))
|
||||
ap = self.rv2a(path, w=True)[0]
|
||||
bos.utime_c(logging.warning, ap, int(timeval), False)
|
||||
|
||||
@@ -433,6 +447,9 @@ class FtpFs(AbstractedFS):
|
||||
ap = self.rv2a(path)[0]
|
||||
return bos.stat(ap)
|
||||
|
||||
def readlink(self, *a) -> None:
|
||||
raise OSError(errno.EINVAL, "nope")
|
||||
|
||||
def isfile(self, path: str) -> bool:
|
||||
try:
|
||||
st = self.stat(path)
|
||||
@@ -475,9 +492,13 @@ class FtpFs(AbstractedFS):
|
||||
def get_user_by_uid(self, uid: int) -> str:
|
||||
return "root"
|
||||
|
||||
def get_group_by_uid(self, gid: int) -> str:
|
||||
def get_group_by_gid(self, gid: int) -> str:
|
||||
return "root"
|
||||
|
||||
def mkstemp(self, *a, **ka):
|
||||
self.clog("mkstemp%r @%s" % (a, self.uname))
|
||||
raise FSE("nope")
|
||||
|
||||
|
||||
class FtpHandler(FTPHandler):
|
||||
abstracted_fs = FtpFs
|
||||
@@ -506,6 +527,7 @@ class FtpHandler(FTPHandler):
|
||||
return
|
||||
|
||||
self.cli_ip = cip
|
||||
self.log_src = "%s:FTP" % (cip,)
|
||||
|
||||
# abspath->vpath mapping to resolve log_transfer paths
|
||||
self.vfs_map: dict[str, str] = {}
|
||||
@@ -513,6 +535,15 @@ class FtpHandler(FTPHandler):
|
||||
# reduce non-debug logging
|
||||
self.log_cmds_list = [x for x in self.log_cmds_list if x not in ("CWD", "XCWD")]
|
||||
|
||||
def clog(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.hub.log(self.log_src, msg, c)
|
||||
|
||||
def ftp_NOOP(self, *a) -> None:
|
||||
self.respond("200 pong", logging.debug)
|
||||
|
||||
def ftp_STOU(self, *a) -> None:
|
||||
self.respond("550 nope", logging.warning)
|
||||
|
||||
def ftp_STOR(self, file: str, mode: str = "w") -> Any:
|
||||
# Optional[str]
|
||||
vp = join(self.fs.cwd, file).lstrip("/")
|
||||
@@ -525,7 +556,7 @@ class FtpHandler(FTPHandler):
|
||||
xbu = vfs.flags.get("xbu")
|
||||
if xbu:
|
||||
hr = runhook(
|
||||
None,
|
||||
self.clog,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.ftpd",
|
||||
@@ -612,17 +643,25 @@ class Ftpd(object):
|
||||
print(t.format(pybin))
|
||||
sys.exit(1)
|
||||
|
||||
if self.args.certkey:
|
||||
h1.keyfile = self.args.certkey
|
||||
h1.certfile = self.args.cert
|
||||
h1.tls_control_required = True
|
||||
h1.tls_data_required = True
|
||||
|
||||
hs.append([h1, self.args.ftps])
|
||||
|
||||
zs = self.args.ftp_banner.replace("\\n", "\n")
|
||||
if zs.startswith("@"):
|
||||
zs = read_utf8(None, zs[1:], False)
|
||||
banner = zs.replace("\r", "").replace("\n", "\r\n").strip()
|
||||
|
||||
for h_lp in hs:
|
||||
h2, lp = h_lp
|
||||
FtpHandler.hub = h2.hub = hub
|
||||
FtpHandler.args = h2.args = hub.args
|
||||
FtpHandler.authorizer = h2.authorizer = FtpAuth(hub)
|
||||
FtpHandler.banner = banner
|
||||
|
||||
if self.args.ftp_pr:
|
||||
p1, p2 = [int(x) for x in self.args.ftp_pr.split("-")]
|
||||
|
||||
+547
-162
File diff suppressed because it is too large.
Load diff
+19
-28
@@ -1,32 +1,29 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse # typechk
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import threading # typechk
|
||||
import time
|
||||
|
||||
try:
|
||||
if os.environ.get("PRTY_NO_TLS"):
|
||||
raise Exception()
|
||||
|
||||
HAVE_SSL = True
|
||||
import ssl
|
||||
except:
|
||||
HAVE_SSL = False
|
||||
|
||||
from . import util as Util
|
||||
from .__init__ import TYPE_CHECKING, EnvParams
|
||||
from .authsrv import AuthSrv # typechk
|
||||
from .cert import NO_TLS
|
||||
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 .util import HMaccas, NetMap, shut_socket
|
||||
from .util import HMaccas, NetMap, min_ex, shut_socket
|
||||
|
||||
try:
|
||||
if NO_TLS:
|
||||
raise Exception()
|
||||
|
||||
import ssl
|
||||
|
||||
HAVE_SSL = True
|
||||
except:
|
||||
HAVE_SSL = False
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Optional, Pattern, Union
|
||||
@@ -69,21 +66,15 @@ class HttpConn(object):
|
||||
self.bans: dict[str, int] = hsrv.bans
|
||||
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.freshen_pwd: float = 0.0
|
||||
self.stopping = False
|
||||
self.nreq: int = -1 # mypy404
|
||||
self.nbyte: int = 0 # mypy404
|
||||
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_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()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
@@ -167,7 +158,7 @@ class HttpConn(object):
|
||||
try:
|
||||
assert ssl # type: ignore # !rm
|
||||
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
ctx.load_cert_chain(self.args.cert)
|
||||
ctx.load_cert_chain(self.args.cert, self.args.certkey)
|
||||
if self.args.ssl_ver:
|
||||
ctx.options &= ~self.args.ssl_flags_en
|
||||
ctx.options |= self.args.ssl_flags_de
|
||||
@@ -204,12 +195,12 @@ class HttpConn(object):
|
||||
except Exception as ex:
|
||||
em = str(ex)
|
||||
|
||||
if "ALERT_CERTIFICATE_UNKNOWN" in em:
|
||||
# android-chrome keeps doing this
|
||||
pass
|
||||
if "ALERT_" in em:
|
||||
self.log("client refused our TLS cert or config: " + em, c=6)
|
||||
|
||||
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
|
||||
|
||||
|
||||
+91
-6
@@ -1,8 +1,7 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import hashlib
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
@@ -55,8 +54,14 @@ except SyntaxError:
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
from .authsrv import LEELOO_DALLAS
|
||||
from .httpconn import HttpConn
|
||||
from .ico import Ico
|
||||
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 .util import (
|
||||
E_SCK,
|
||||
@@ -133,6 +138,9 @@ class HttpSrv(object):
|
||||
self.bans: 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
|
||||
dls: dict[str, tuple[float, int]] = {} # state
|
||||
self.dli = self.tdli = dli
|
||||
@@ -171,6 +179,8 @@ class HttpSrv(object):
|
||||
self.u2idx_free: dict[str, U2idx] = {}
|
||||
self.u2idx_n = 0
|
||||
|
||||
self.wopi_files: dict[str, dict[str, Any]] = {}
|
||||
|
||||
assert jinja2 # type: ignore # !rm
|
||||
env = jinja2.Environment()
|
||||
env.loader = jinja2.FunctionLoader(lambda f: load_jinja2_resource(self.E, f))
|
||||
@@ -186,6 +196,7 @@ class HttpSrv(object):
|
||||
"shares",
|
||||
"splash",
|
||||
"svcs",
|
||||
"wopi",
|
||||
]
|
||||
self.j2 = {x: env.get_template(x + ".html") for x in jn}
|
||||
self.j2["opds"] = env.get_template("opds.xml")
|
||||
@@ -230,16 +241,20 @@ class HttpSrv(object):
|
||||
if self.args.log_thrs:
|
||||
start_log_thrs(self.log, self.args.log_thrs, nid)
|
||||
|
||||
self.th_cfg: dict[str, set[str]] = {}
|
||||
Daemon(self.post_init, "hsrv-init2")
|
||||
if (HAVE_PIL or HAVE_VIPS or HAVE_FFMPEG) and not self.args.no_thumb:
|
||||
Daemon(self.post_init, "hsrv-init2")
|
||||
|
||||
def post_init(self) -> None:
|
||||
try:
|
||||
x = self.broker.ask("thumbsrv.getcfg")
|
||||
self.th_cfg = x.get()
|
||||
self.thumbcli = ThumbCli(self, x.get())
|
||||
except:
|
||||
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:
|
||||
self.bad_ver = True
|
||||
|
||||
@@ -302,7 +317,7 @@ class HttpSrv(object):
|
||||
|
||||
self.srvs.append(sck)
|
||||
self.bound.add((ip, port))
|
||||
self.nclimax = math.ceil(self.args.nc * 1.0 / nlisteners)
|
||||
self.nclimax = -int(-self.args.nc // nlisteners)
|
||||
Daemon(
|
||||
self.thr_listen,
|
||||
"httpsrv-n{}-listen-{}-{}".format(self.nid or "0", ip, port),
|
||||
@@ -646,3 +661,73 @@ class HttpSrv(object):
|
||||
|
||||
self.tdli = dli
|
||||
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("", "/", [], 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")
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse # typechk
|
||||
import colorsys
|
||||
|
||||
+1
-2
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import errno
|
||||
import os
|
||||
@@ -51,7 +51,6 @@ except ImportError:
|
||||
SRV,
|
||||
TXT,
|
||||
A,
|
||||
Bimap,
|
||||
DNSHeader,
|
||||
DNSQuestion,
|
||||
DNSRecord,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import json
|
||||
import time
|
||||
|
||||
+71
-23
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import json
|
||||
@@ -15,8 +15,12 @@ from .authsrv import VFS
|
||||
from .bos import bos
|
||||
from .util import (
|
||||
FFMPEG_URL,
|
||||
LOG,
|
||||
REKOBO_LKEY,
|
||||
SCWD,
|
||||
VF_CAREFUL,
|
||||
absreal,
|
||||
expand_osenv_c,
|
||||
fsenc,
|
||||
gzip,
|
||||
min_ex,
|
||||
@@ -26,6 +30,9 @@ from .util import (
|
||||
sfsenc,
|
||||
uncyg,
|
||||
wunlink,
|
||||
zip_fi,
|
||||
zip_lim,
|
||||
zip_readf,
|
||||
)
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
@@ -45,24 +52,36 @@ except:
|
||||
HAVE_MUTAGEN = False
|
||||
|
||||
|
||||
def have_ff(scmd: str) -> bool:
|
||||
if ANYWIN:
|
||||
def have_ff(name: str) -> list[bytes]:
|
||||
uname = name.upper()
|
||||
if os.environ.get("PRTY_NO_" + uname):
|
||||
return []
|
||||
|
||||
ebin = os.environ.get("PRTY_%s_BIN" % (uname,))
|
||||
try:
|
||||
scmd = (ebin or name).decode("utf-8")
|
||||
except:
|
||||
scmd: str = ebin or name
|
||||
|
||||
if ANYWIN and not ebin:
|
||||
scmd += ".exe"
|
||||
|
||||
if PY2:
|
||||
print("# checking {}".format(scmd))
|
||||
acmd = (scmd + " -version").encode("ascii").split(b" ")
|
||||
print("# checking %s" % (scmd,))
|
||||
bcmd = scmd.encode("utf-8")
|
||||
try:
|
||||
sp.Popen(acmd, stdout=sp.PIPE, stderr=sp.PIPE).communicate()
|
||||
return True
|
||||
sp.Popen([bcmd, b"-version"], stdout=sp.PIPE, stderr=sp.PIPE).communicate()
|
||||
return [bcmd]
|
||||
except:
|
||||
return False
|
||||
return []
|
||||
else:
|
||||
return bool(shutil.which(scmd))
|
||||
ret = shutil.which(scmd)
|
||||
return [absreal(ret).encode("utf-8")] if ret else []
|
||||
|
||||
|
||||
HAVE_FFMPEG = not os.environ.get("PRTY_NO_FFMPEG") and have_ff("ffmpeg")
|
||||
HAVE_FFPROBE = not os.environ.get("PRTY_NO_FFPROBE") and have_ff("ffprobe")
|
||||
HAVE_FFMPEG = have_ff("ffmpeg")
|
||||
HAVE_FFPROBE = have_ff("ffprobe")
|
||||
TH_BWRAP = []
|
||||
|
||||
CBZ_PICS = set("png jpg jpeg gif bmp tga tif tiff webp avif jxl".split())
|
||||
CBZ_01 = re.compile(r"(^|[^0-9v])0+[01]\b")
|
||||
@@ -86,7 +105,7 @@ class MParser(object):
|
||||
|
||||
while True:
|
||||
try:
|
||||
bp = os.path.expanduser(args)
|
||||
bp = os.path.expanduser(expand_osenv_c(args))
|
||||
if WINDOWS:
|
||||
bp = uncyg(bp)
|
||||
|
||||
@@ -158,7 +177,7 @@ def au_unpk(
|
||||
zil = [x for x in zil if x.filename.lower().split(".")[-1] == au]
|
||||
if not zil:
|
||||
raise Exception("no audio inside zip")
|
||||
fi = zf.open(zil[0])
|
||||
fi = zf.open(zip_lim(zil[0], maxsz))
|
||||
|
||||
elif pk == "cbz":
|
||||
import zipfile
|
||||
@@ -172,11 +191,18 @@ def au_unpk(
|
||||
t = "cbz: %d files, %d hits" % (nf, len(znil))
|
||||
if not znil:
|
||||
raise Exception("no images inside cbz")
|
||||
using = sorted(znil)[0][1].filename
|
||||
zi = sorted(znil)[0][1]
|
||||
if znil:
|
||||
t += ", using " + using
|
||||
t += ", using " + zi.filename
|
||||
log(t)
|
||||
fi = zf.open(using)
|
||||
fi = zf.open(zip_lim(zi, maxsz))
|
||||
|
||||
elif pk == "kra" or pk == "ora":
|
||||
import zipfile
|
||||
|
||||
zf = zipfile.ZipFile(abspath, "r")
|
||||
zi = zip_fi(zf, "mergedimage.png", maxsz)
|
||||
fi = zf.open(zi)
|
||||
|
||||
elif pk == "epub":
|
||||
fi = get_cover_from_epub(log, abspath)
|
||||
@@ -213,18 +239,37 @@ def au_unpk(
|
||||
return abspath
|
||||
|
||||
|
||||
def bwrap(prog: bytes, ap_in: bytes, ap_out: bytes) -> list[bytes]:
|
||||
if not TH_BWRAP:
|
||||
return [prog]
|
||||
ret = TH_BWRAP + [b"--ro-bind", prog, prog, b"--ro-bind", ap_in, ap_in]
|
||||
if ap_out:
|
||||
zs = ap_out.rsplit(b"/", 1)[0]
|
||||
ret += [b"--bind", zs, zs]
|
||||
ret.append(prog)
|
||||
return ret
|
||||
|
||||
|
||||
def bwrap_fail(serr: str) -> None:
|
||||
if "bwrap:" in serr:
|
||||
LOG[0]("root", "ffmpeg failed due to --th-bwrap;\n " + serr, 3)
|
||||
|
||||
|
||||
def ffprobe(
|
||||
abspath: str, timeout: int = 60
|
||||
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:
|
||||
cmd = [
|
||||
b"ffprobe",
|
||||
# ffprobe -hide_banner -show_streams -show_format --
|
||||
bap = fsenc(abspath)
|
||||
cmd = bwrap(HAVE_FFPROBE[0], bap, b"") + [
|
||||
b"-hide_banner",
|
||||
b"-show_streams",
|
||||
b"-show_format",
|
||||
b"--",
|
||||
fsenc(abspath),
|
||||
bap,
|
||||
]
|
||||
rc, so, se = runcmd(cmd, timeout=timeout, nice=True, oom=200)
|
||||
rc, so, se = runcmd(cmd, cwd=SCWD, timeout=timeout, nice=True, oom=200)
|
||||
if rc and TH_BWRAP:
|
||||
bwrap_fail(se)
|
||||
retchk(rc, cmd, se)
|
||||
return parse_ffprobe(so)
|
||||
|
||||
@@ -397,7 +442,8 @@ def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]
|
||||
with zipfile.ZipFile(abspath, "r") as z:
|
||||
# First open the container file to find the package document (.opf file)
|
||||
try:
|
||||
container_root = parse_xml(z.read("META-INF/container.xml").decode())
|
||||
zb = zip_readf(z, "META-INF/container.xml", 2 << 20)
|
||||
container_root = parse_xml(zb.decode())
|
||||
except KeyError:
|
||||
log("epub: no container file found in %s" % (abspath,))
|
||||
return None
|
||||
@@ -411,7 +457,8 @@ def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]
|
||||
|
||||
# Then open the first package document to find the path of the cover image
|
||||
try:
|
||||
package_root = parse_xml(z.read(rootfile_path).decode())
|
||||
zb = zip_readf(z, rootfile_path, 2 << 20)
|
||||
package_root = parse_xml(zb.decode())
|
||||
except KeyError:
|
||||
log("epub: no package document found in %s" % (abspath,))
|
||||
return None
|
||||
@@ -435,7 +482,8 @@ def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]
|
||||
adjusted_cover_path = urljoin(rootfile_path, coverimage_path)
|
||||
|
||||
try:
|
||||
return z.open(adjusted_cover_path)
|
||||
zi = zip_fi(z, adjusted_cover_path, 2 << 24)
|
||||
return z.open(zi)
|
||||
except KeyError:
|
||||
t = "epub: cover specified in package document, but doesn't exist: %s"
|
||||
log(t % (adjusted_cover_path,))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import socket
|
||||
import time
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
@@ -14,8 +14,9 @@ try:
|
||||
if os.environ.get("PRTY_NO_ARGON2"):
|
||||
raise Exception()
|
||||
|
||||
HAVE_ARGON2 = True
|
||||
from argon2 import exceptions as argon2ex
|
||||
|
||||
HAVE_ARGON2 = True
|
||||
except:
|
||||
HAVE_ARGON2 = False
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
|
||||
+104
-40
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import errno
|
||||
import hashlib
|
||||
@@ -8,7 +8,6 @@ import os
|
||||
import select
|
||||
import socket
|
||||
import time
|
||||
from threading import ExceptHookArgs
|
||||
|
||||
import paramiko
|
||||
import paramiko.common
|
||||
@@ -21,6 +20,7 @@ from paramiko.sftp import (
|
||||
SFTP_OP_UNSUPPORTED,
|
||||
SFTP_PERMISSION_DENIED,
|
||||
)
|
||||
from queue import Queue
|
||||
|
||||
from .__init__ import ANYWIN, TYPE_CHECKING
|
||||
from .authsrv import LEELOO_DALLAS, VFS, AuthSrv
|
||||
@@ -30,10 +30,13 @@ from .util import (
|
||||
Daemon,
|
||||
ODict,
|
||||
Pebkac,
|
||||
exclude_dotfiles,
|
||||
exclude_dotfiles_ls,
|
||||
exclude_dothidden,
|
||||
exclude_dothidden_ls,
|
||||
ipnorm,
|
||||
min_ex,
|
||||
read_utf8,
|
||||
relchk,
|
||||
runhook,
|
||||
sanitize_fn,
|
||||
ub64enc,
|
||||
@@ -51,6 +54,23 @@ if True: # pylint: disable=using-constant-test
|
||||
SATTR = paramiko.sftp_attr.SFTPAttributes
|
||||
|
||||
|
||||
try:
|
||||
from paramiko.rsakey import serialization
|
||||
|
||||
_load_der_pk = serialization.load_der_private_key
|
||||
|
||||
def fastloader(*a, **ka):
|
||||
ka0 = ka.copy()
|
||||
try:
|
||||
ka["unsafe_skip_rsa_key_validation"] = True
|
||||
return _load_der_pk(*a, **ka)
|
||||
except:
|
||||
return _load_der_pk(*a, **ka0)
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class SSH_Srv(paramiko.ServerInterface):
|
||||
def __init__(self, hub: "SvcHub", addr: Any):
|
||||
self.hub = hub
|
||||
@@ -291,6 +311,17 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
|
||||
if self.uname == LEELOO_DALLAS:
|
||||
raise Exception("send her back")
|
||||
|
||||
cm = 0
|
||||
for zs, zi in (
|
||||
("s", SATTR.FLAG_SIZE),
|
||||
("t", SATTR.FLAG_AMTIME),
|
||||
("p", SATTR.FLAG_PERMISSIONS),
|
||||
("u", SATTR.FLAG_UIDGID),
|
||||
):
|
||||
if zs in self.args.sftp_chattr:
|
||||
cm |= zi
|
||||
self.chattr_mask = cm
|
||||
|
||||
self.vols = [
|
||||
vp
|
||||
for vp, vn in self.asrv.vfs.all_vols.items()
|
||||
@@ -316,15 +347,23 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
|
||||
m: bool = False,
|
||||
d: bool = False,
|
||||
) -> tuple[str, VFS, str]:
|
||||
vpath = vpath.replace(os.sep, "/").strip("/")
|
||||
rd, fn = os.path.split(vpath)
|
||||
if relchk(rd):
|
||||
self.log("malicious vpath: %s", vpath)
|
||||
raise Exception("Unsupported characters in [%s]" % (vpath,))
|
||||
# because:
|
||||
# * maybe some clients use \
|
||||
# * windows filename limitations
|
||||
# need to allow/translate illegal fn,
|
||||
# but require filesystem-correct rd
|
||||
if ANYWIN or ("\\" in vpath and "/" not in vpath):
|
||||
vpath = vpath.replace("\\", "/")
|
||||
vpath = vpath.strip("/\\")
|
||||
if ANYWIN:
|
||||
rd, fn = os.path.split(vpath)
|
||||
fn = sanitize_fn(fn, "win")
|
||||
vpath = vjoin(rd, fn)
|
||||
# end of windows jank
|
||||
|
||||
fn = sanitize_fn(fn or "")
|
||||
vpath = vjoin(rd, fn)
|
||||
vn, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
|
||||
vpath = vjoin(vn.vpath, rem)
|
||||
rd, fn = os.path.split(vpath)
|
||||
if (
|
||||
w
|
||||
and fn.lower() in vn.flags["emb_all"]
|
||||
@@ -339,18 +378,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
|
||||
# return "", vn, rem
|
||||
raise OSError(errno.ENOENT, "no filesystem mounted at [/%s]" % (vpath,))
|
||||
|
||||
if "xdev" in vn.flags or "xvol" in vn.flags:
|
||||
ap = vn.canonical(rem)
|
||||
avn = vn.chk_ap(ap)
|
||||
t = "Permission denied in [{}]"
|
||||
if not avn:
|
||||
raise OSError(errno.EPERM, "permission denied in [/%s]" % (vpath,))
|
||||
|
||||
cr, cw, cm, cd, _, _, _, _, _ = avn.uaxs[self.uname]
|
||||
if r and not cr or w and not cw or m and not cm or d and not cd:
|
||||
raise OSError(errno.EPERM, "permission denied in [/%s]" % (vpath,))
|
||||
else:
|
||||
ap = vn.canonical(rem, False)
|
||||
ap = vn.canonical(rem, False)
|
||||
|
||||
if "bcasechk" in vn.flags and not vn.casechk(rem, True):
|
||||
raise OSError(errno.ENOENT, "file does not exist case-sensitively")
|
||||
@@ -401,21 +429,27 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
|
||||
self.log("ls(%s): vfs-vols; |%d|" % (path, len(ret)))
|
||||
return ret
|
||||
|
||||
_, vfs_ls, vfs_virt = vn.ls(
|
||||
fsroot, vfs_ls, vfs_virt = vn.ls(
|
||||
rem,
|
||||
self.uname,
|
||||
not self.args.no_scandir,
|
||||
[[True, False], [False, 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]
|
||||
for zs, vn2 in vfs_virt.items():
|
||||
if not vn2.realpath:
|
||||
if not vn2.realpath or zs not in vnames:
|
||||
continue
|
||||
st = bos.stat(vn2.realpath)
|
||||
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)
|
||||
self.log("ls(%s): |%d|" % (path, len(ret)))
|
||||
return ret
|
||||
@@ -485,8 +519,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
|
||||
smode = "rb"
|
||||
|
||||
try:
|
||||
vn, rem = self.asrv.vfs.get(vp, self.uname, rd, wr)
|
||||
ap = vn.canonical(rem, False)
|
||||
ap, vn, rem = self.v2a(vp, rd, wr)
|
||||
vf = vn.flags
|
||||
except Pebkac as ex:
|
||||
t = "denied open file [%s], iflag=%s, read=%s, write=%s: %s"
|
||||
@@ -658,8 +691,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
|
||||
def _mkdir(self, vp: str, attr: SATTR) -> int:
|
||||
self.log("mkdir(%s)" % (vp,))
|
||||
try:
|
||||
vn, rem = self.asrv.vfs.get(vp, self.uname, False, True)
|
||||
ap = os.path.join(vn.realpath, rem)
|
||||
ap, vn, _ = self.v2a(vp, w=True)
|
||||
bos.makedirs(ap, vf=vn.flags) # filezilla expects this
|
||||
if attr is not None:
|
||||
paramiko.SFTPServer.set_file_attr(ap, attr)
|
||||
@@ -682,8 +714,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
|
||||
def _rmdir(self, vp: str) -> int:
|
||||
self.log("rmdir(%s)" % (vp,))
|
||||
try:
|
||||
vn, rem = self.asrv.vfs.get(vp, self.uname, False, False, will_del=True)
|
||||
ap = os.path.join(vn.realpath, rem)
|
||||
ap, _, _ = self.v2a(vp, d=True)
|
||||
bos.rmdir(ap)
|
||||
return SFTP_OK
|
||||
except Pebkac as ex:
|
||||
@@ -704,8 +735,8 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
|
||||
def _chattr(self, vp: str, attr: SATTR) -> int:
|
||||
self.log("chattr(%s, %s)" % (vp, attr))
|
||||
try:
|
||||
vn, rem = self.asrv.vfs.get(vp, self.uname, False, True, will_del=True)
|
||||
ap = os.path.join(vn.realpath, rem)
|
||||
attr._flags &= self.chattr_mask
|
||||
ap, _, _ = self.v2a(vp, w=True, d=True)
|
||||
paramiko.SFTPServer.set_file_attr(ap, attr)
|
||||
return SFTP_OK
|
||||
except Pebkac as ex:
|
||||
@@ -726,6 +757,22 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
|
||||
return "/%s" % (undot(path),)
|
||||
|
||||
|
||||
class Transport2(paramiko.Transport):
|
||||
def __init__(self, cli, addr):
|
||||
self.addr = addr
|
||||
super(Transport2, self).__init__(cli)
|
||||
|
||||
def _check_banner(self):
|
||||
try:
|
||||
return super()._check_banner()
|
||||
except Exception as ex:
|
||||
if "Error reading SSH protocol bann" in str(ex):
|
||||
self.logger.warning("%s rejected: %s" % (self.addr, ex))
|
||||
self.addr = None
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
class Sftpd(object):
|
||||
def __init__(self, hub: "SvcHub") -> None:
|
||||
self.hub = hub
|
||||
@@ -733,6 +780,7 @@ class Sftpd(object):
|
||||
self.log_func = hub.log
|
||||
self.srv: list[socket.socket] = []
|
||||
self.bound: list[str] = []
|
||||
self.hs_q = Queue()
|
||||
self.sessions = {}
|
||||
|
||||
ips = args.sftp_i
|
||||
@@ -748,6 +796,10 @@ class Sftpd(object):
|
||||
self.log("cannot start sftp-server; no compatible IPs in -i", 1)
|
||||
return
|
||||
|
||||
if args.sftp_fastldr:
|
||||
serialization.load_der_private_key = fastloader # type: ignore
|
||||
else:
|
||||
self.log("loading hostkeys...")
|
||||
self.hostkeys = []
|
||||
hostkeytypes = (
|
||||
("ed25519", "Ed25519Key", {}), # best
|
||||
@@ -775,6 +827,8 @@ class Sftpd(object):
|
||||
self.hostkeys.append(pkey)
|
||||
if args.sftpv:
|
||||
self.log("loaded hostkey %r" % (pkey,))
|
||||
if args.sftp_fastldr:
|
||||
serialization.load_der_private_key = _load_der_pk # type: ignore
|
||||
|
||||
ips = list(ODict.fromkeys(ips)) # dedup
|
||||
|
||||
@@ -783,6 +837,9 @@ class Sftpd(object):
|
||||
|
||||
self.log("listening @ %s port %s" % (self.bound, args.sftp))
|
||||
|
||||
for n in range(self.args.sftp_hs_n):
|
||||
Daemon(self.acceptor, "sftp-hs-%d" % (n,))
|
||||
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.hub.log("sftp", msg, c)
|
||||
|
||||
@@ -818,12 +875,18 @@ class Sftpd(object):
|
||||
cli, addr = srv.accept()
|
||||
# cli.settimeout(0) # == srv.setblocking(False)
|
||||
self.log("%r is connecting" % (addr,))
|
||||
zs = "sftp-%s" % (addr[0],)
|
||||
# Daemon(self._accept2, zs, (cli, addr))
|
||||
self._accept2(cli, addr)
|
||||
self.hs_q.put((cli, addr))
|
||||
|
||||
def acceptor(self) -> None:
|
||||
while True:
|
||||
try:
|
||||
cli, addr = self.hs_q.get()
|
||||
self._accept2(cli, addr)
|
||||
except:
|
||||
time.sleep(1)
|
||||
|
||||
def _accept2(self, cli, addr) -> None:
|
||||
tra = paramiko.Transport(cli)
|
||||
tra = Transport2(cli, addr)
|
||||
for hkey in self.hostkeys:
|
||||
tra.add_server_key(hkey)
|
||||
tra.set_subsystem_handler("sftp", paramiko.SFTPServer, SFTP_Srv)
|
||||
@@ -831,11 +894,12 @@ class Sftpd(object):
|
||||
try:
|
||||
tra.start_server(server=psrv)
|
||||
except Exception as ex:
|
||||
self.log("%r could not establish connection: %r" % (addr, ex), 3)
|
||||
if tra.addr:
|
||||
self.log("%r could not establish connection: %r" % (addr, ex), 3)
|
||||
cli.close()
|
||||
return
|
||||
|
||||
chan = tra.accept()
|
||||
chan = tra.accept(timeout=self.args.sftp_hs_t)
|
||||
if chan is None:
|
||||
self.log("%r did not open an sftp channel" % (addr,), 3)
|
||||
cli.close()
|
||||
|
||||
+7
-4
@@ -12,7 +12,7 @@ from types import SimpleNamespace
|
||||
from .__init__ import ANYWIN, EXE, TYPE_CHECKING
|
||||
from .authsrv import LEELOO_DALLAS, VFS
|
||||
from .bos import bos
|
||||
from .util import Daemon, absreal, min_ex, pybin, runhook, vjoin
|
||||
from .util import Daemon, absreal, min_ex, pybin, runhook
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Union
|
||||
@@ -89,13 +89,15 @@ class SMB(object):
|
||||
smbserver.isInFileJail = self._is_in_file_jail
|
||||
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:
|
||||
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"
|
||||
|
||||
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:
|
||||
if self.accs:
|
||||
srv.setAuthCallback(self._auth_cb)
|
||||
@@ -121,6 +123,7 @@ class SMB(object):
|
||||
|
||||
self.srv = srv
|
||||
self.stop = srv.stop
|
||||
ip = "[%s]" % (ip,) if kw else ip
|
||||
self.log("smb", "listening @ {}:{}".format(ip, port))
|
||||
|
||||
def nlog(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
|
||||
+1
-2
@@ -1,11 +1,10 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import errno
|
||||
import re
|
||||
import select
|
||||
import socket
|
||||
import time
|
||||
|
||||
from .__init__ import TYPE_CHECKING
|
||||
from .multicast import MC_Sck, MCast
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import re
|
||||
import stat
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import ctypes
|
||||
import sys
|
||||
from ctypes import wintypes
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
@@ -9,6 +10,9 @@ if True: # pylint: disable=using-constant-test
|
||||
|
||||
from . import _shared as shared
|
||||
|
||||
WINVER = sys.getwindowsversion()
|
||||
LE_XP = WINVER.major < 6
|
||||
|
||||
NO_ERROR = 0
|
||||
ERROR_BUFFER_OVERFLOW = 111
|
||||
MAX_ADAPTER_NAME_LENGTH = 256
|
||||
@@ -83,7 +87,10 @@ def enumerate_interfaces_of_adapter(
|
||||
t = "sockaddr_to_ip({}) returned None"
|
||||
raise Exception(t.format(address.Address.lpSockaddr))
|
||||
|
||||
network_prefix = address.OnLinkPrefixLength
|
||||
if LE_XP:
|
||||
network_prefix = 16 if str(ip).startswith("169.254") else 24
|
||||
else:
|
||||
network_prefix = address.OnLinkPrefixLength
|
||||
yield shared.IP(ip, network_prefix, nice_name)
|
||||
|
||||
|
||||
|
||||
+82
-8
@@ -1,22 +1,25 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
|
||||
from .__init__ import CORES
|
||||
from .authsrv import VFS, AuthSrv
|
||||
from .__init__ import CORES, TYPE_CHECKING
|
||||
from .authsrv import LEELOO_DALLAS, VFS, AuthSrv
|
||||
from .bos import bos
|
||||
from .th_cli import ThumbCli
|
||||
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
|
||||
from typing import Any, Generator, Optional
|
||||
|
||||
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("|"))
|
||||
|
||||
@@ -42,20 +45,33 @@ class StreamArc(object):
|
||||
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(
|
||||
fgen: Generator[dict[str, Any], None, None],
|
||||
thumbcli: ThumbCli,
|
||||
uname: str,
|
||||
vtop: str,
|
||||
vname: str,
|
||||
fmt: str,
|
||||
) -> Generator[dict[str, Any], None, None]:
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
pend = []
|
||||
with ThreadPoolExecutor(max_workers=CORES) as tp:
|
||||
with ThreadPoolExecutor(max_workers=CORES, initializer=sigblock) as tp:
|
||||
_pools[tp] = 1
|
||||
try:
|
||||
for f in fgen:
|
||||
task = tp.submit(enthumb, thumbcli, uname, vtop, f, fmt)
|
||||
task = tp.submit(enthumb, thumbcli, uname, vtop, vname, f, fmt)
|
||||
pend.append((task, f))
|
||||
if pend[0][0].done() or len(pend) > CORES * 4:
|
||||
task, f = pend.pop(0)
|
||||
@@ -79,17 +95,75 @@ def gfilter(
|
||||
except:
|
||||
pass
|
||||
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(
|
||||
thumbcli: ThumbCli, uname: str, vtop: str, f: dict[str, Any], fmt: str
|
||||
thumbcli: ThumbCli, uname: str, vtop: str, vname: str, f: dict[str, Any], fmt: str
|
||||
) -> dict[str, Any]:
|
||||
rem = f["vp"]
|
||||
ext = rem.rsplit(".", 1)[-1].lower()
|
||||
if (fmt == "mp3" and ext == "mp3") or (fmt == "opus" and ext in TAR_NO_OPUS):
|
||||
raise Exception()
|
||||
|
||||
vp = vjoin(vtop, rem.split("/", 1)[1])
|
||||
if vname:
|
||||
vp = vjoin(vtop, rem.split("/", 1)[1])
|
||||
else:
|
||||
vp = vjoin(vtop, rem)
|
||||
vn, rem = thumbcli.asrv.vfs.get(vp, uname, True, False)
|
||||
dbv, vrem = vn.get_dbv(rem)
|
||||
thp = thumbcli.get(dbv, vrem, f["st"].st_mtime, fmt)
|
||||
|
||||
+401
-139
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
@@ -27,35 +27,48 @@ if True: # pylint: disable=using-constant-test
|
||||
import typing
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode
|
||||
from .__init__ import (
|
||||
ANYWIN,
|
||||
EXE,
|
||||
MACOS,
|
||||
PY2,
|
||||
TYPE_CHECKING,
|
||||
UNIX,
|
||||
E,
|
||||
EnvParams,
|
||||
unicode,
|
||||
)
|
||||
from .__version__ import S_VERSION, VERSION
|
||||
from .authsrv import BAD_CFG, AuthSrv, derive_args, n_du_who, n_ver_who
|
||||
from .bos import bos
|
||||
from .cert import ensure_cert
|
||||
from .fsutil import ramdisk_chk
|
||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN
|
||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN, TH_BWRAP
|
||||
from .pwhash import HAVE_ARGON2
|
||||
from .sutil import close_pools as sutil_close_pools
|
||||
from .tcpsrv import TcpSrv
|
||||
from .th_srv import (
|
||||
H_PIL_AVIF,
|
||||
H_PIL_HEIF,
|
||||
H_PIL_WEBP,
|
||||
HAVE_FFMPEG,
|
||||
HAVE_FFPROBE,
|
||||
HAVE_DCRAW,
|
||||
HAVE_PIL,
|
||||
HAVE_RAW,
|
||||
HAVE_RAWPY,
|
||||
HAVE_VIPS,
|
||||
ThumbSrv,
|
||||
)
|
||||
from .up2k import Up2k
|
||||
from .util import (
|
||||
BLOCK_SIGS,
|
||||
DEF_EXP,
|
||||
DEF_MTE,
|
||||
DEF_MTH,
|
||||
FFMPEG_URL,
|
||||
HAVE_BWRAP,
|
||||
HAVE_PSUTIL,
|
||||
HAVE_SQLITE3,
|
||||
HAVE_ZMQ,
|
||||
LOG,
|
||||
RE_ANSI,
|
||||
URL_BUG,
|
||||
UTC,
|
||||
@@ -73,11 +86,13 @@ from .util import (
|
||||
load_ipr,
|
||||
load_ipu,
|
||||
lock_file,
|
||||
lprinted,
|
||||
min_ex,
|
||||
mp,
|
||||
odfusion,
|
||||
pybin,
|
||||
read_utf8,
|
||||
signame2int,
|
||||
start_log_thrs,
|
||||
start_stackmon,
|
||||
termsize,
|
||||
@@ -103,11 +118,32 @@ if PY2:
|
||||
else:
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
try:
|
||||
from queue import SimpleQueue
|
||||
except:
|
||||
# yuul b. alwright
|
||||
from queue import Queue as SimpleQueue
|
||||
|
||||
|
||||
VER_IDP_DB = 1
|
||||
VER_SESSION_DB = 1
|
||||
VER_SHARES_DB = 2
|
||||
|
||||
CVE_SEVS = {"low": 1, "medium": 2, "moderate": 2, "high": 3, "critical": 4}
|
||||
|
||||
|
||||
def _build_th_fset(f: str, fadd: str) -> set[str]:
|
||||
fset = {x for x in f.replace(" ", "").split(",")}
|
||||
rmset = {""}
|
||||
|
||||
for x in fadd.replace(" ", "").split(","):
|
||||
if x.startswith("-"):
|
||||
rmset.add(x.lstrip("-"))
|
||||
else:
|
||||
fset.add(x)
|
||||
|
||||
return fset - rmset
|
||||
|
||||
|
||||
class SvcHub(object):
|
||||
"""
|
||||
@@ -125,7 +161,6 @@ class SvcHub(object):
|
||||
args: argparse.Namespace,
|
||||
dargs: argparse.Namespace,
|
||||
argv: list[str],
|
||||
printed: str,
|
||||
) -> None:
|
||||
self.args = args
|
||||
self.dargs = dargs
|
||||
@@ -137,13 +172,10 @@ class SvcHub(object):
|
||||
self.logf: Optional[typing.TextIO] = None
|
||||
self.logf_base_fn = ""
|
||||
self.is_dut = False # running in unittest; always False
|
||||
self.stop_req = False
|
||||
self.stopping = False
|
||||
self.stopped = False
|
||||
self.reload_req = False
|
||||
self.init_mutex = threading.Lock()
|
||||
self.reload_mutex = threading.Lock()
|
||||
self.stop_cond = threading.Condition()
|
||||
self.nsigs = 3
|
||||
self.retcode = 0
|
||||
self.httpsrv_up = 0
|
||||
self.qr_tsz = None
|
||||
@@ -153,6 +185,12 @@ class SvcHub(object):
|
||||
self.cmon = 0
|
||||
self.tstack = 0.0
|
||||
|
||||
self.sig_logrot = -999
|
||||
self.sig_reload = -999
|
||||
self.sig_stack = -999
|
||||
self.nsigs = 7
|
||||
self.sig = SimpleQueue()
|
||||
|
||||
self.iphash = HMaccas(os.path.join(self.E.cfg, "iphash"), 8)
|
||||
|
||||
if args.sss or args.s >= 3:
|
||||
@@ -171,6 +209,9 @@ class SvcHub(object):
|
||||
args.reflink = True
|
||||
args.dav_auth = True
|
||||
args.vague_403 = True
|
||||
args.no_html = True
|
||||
args.no_readme = True
|
||||
args.no_logues = True
|
||||
args.nih = True
|
||||
|
||||
if args.s:
|
||||
@@ -196,9 +237,37 @@ class SvcHub(object):
|
||||
self.log_div = 10 ** (6 - 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 = 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
|
||||
|
||||
self.lo1 = self.lo2 = ""
|
||||
if args.lo:
|
||||
self._setup_logfile(printed)
|
||||
do_xz = args.lo.replace("%R", "").lower().endswith(".xz")
|
||||
if "%R" not in args.lo:
|
||||
args.lo += "%R"
|
||||
if args.rlo in ("", "no"):
|
||||
args.rlo = ""
|
||||
if do_xz and not args.rlo:
|
||||
args.rlo = ".1"
|
||||
if args.rlo in ("", "del", "over"):
|
||||
args.lo = args.lo.replace("%R", "")
|
||||
try:
|
||||
self.lo1, self.lo2 = args.lo.split("%R")
|
||||
except:
|
||||
self.lo1 = args.lo
|
||||
try:
|
||||
self.rot_fmt = "%%s%s%%0%sd%s" % (args.rlo[:1], args.rlo[1:2], self.lo2)
|
||||
except:
|
||||
self.rot_fmt = "%s.%d"
|
||||
self._setup_logfile()
|
||||
|
||||
LOG[0] = self.log
|
||||
lprinted[:] = []
|
||||
|
||||
lg = logging.getLogger()
|
||||
lh = HLog(self.log)
|
||||
@@ -253,6 +322,16 @@ class SvcHub(object):
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
if args.vc_sev and args.vc_sev not in CVE_SEVS:
|
||||
self.log("root", "vc-sev %r invalid; will use 'low'" % (args.vc_sev,), 3)
|
||||
|
||||
zs = ""
|
||||
if args.th_ram_max < 0.22:
|
||||
zs = "generate thumbnails"
|
||||
@@ -335,6 +414,9 @@ class SvcHub(object):
|
||||
self.asrv = AuthSrv(self.args, self.log, dargs=self.dargs)
|
||||
ramdisk_chk(self.asrv)
|
||||
|
||||
if ANYWIN and not self.args.unsafe_tools:
|
||||
self._check_toolpaths()
|
||||
|
||||
if args.cgen:
|
||||
self.asrv.cgen()
|
||||
|
||||
@@ -363,7 +445,7 @@ class SvcHub(object):
|
||||
decs.pop("vips", None)
|
||||
if not HAVE_PIL:
|
||||
decs.pop("pil", None)
|
||||
if not HAVE_RAW:
|
||||
if not HAVE_RAWPY and not HAVE_DCRAW:
|
||||
decs.pop("raw", None)
|
||||
if not HAVE_FFMPEG or not HAVE_FFPROBE:
|
||||
decs.pop("ff", None)
|
||||
@@ -372,6 +454,20 @@ class SvcHub(object):
|
||||
zlss = [x.strip().lower().split("=", 1) for x in args.au_unpk.split(",")]
|
||||
args.au_unpk = {x[0]: x[1] for x in zlss}
|
||||
|
||||
args.th_r_pil = _build_th_fset(args.th_r_pil, args.th_pil_add)
|
||||
args.th_r_vips = _build_th_fset(args.th_r_vips, args.th_vips_add)
|
||||
args.th_r_raw = _build_th_fset(args.th_r_raw, args.th_raw_add)
|
||||
args.th_r_ffi = _build_th_fset(args.th_r_ffi, args.th_ffi_add)
|
||||
args.th_r_ffv = _build_th_fset(args.th_r_ffv, args.th_ffv_add)
|
||||
args.th_r_ffa = _build_th_fset(args.th_r_ffa, args.th_ffa_add)
|
||||
|
||||
th_extract = args.th_extract or []
|
||||
args.th_extract = {}
|
||||
for arg in th_extract:
|
||||
exts, script = arg.split("=", 1)
|
||||
for ext in exts.split(","):
|
||||
args.th_extract[ext.strip().lower()] = script
|
||||
|
||||
self.args.th_dec = list(decs.keys())
|
||||
self.thumbsrv = None
|
||||
want_ff = False
|
||||
@@ -409,12 +505,14 @@ class SvcHub(object):
|
||||
t = "invalid mp3 transcoding quality [%s] specified; only supports [0] to disable, a CBR value such as [192k], or a CQ/CRF value such as [v2]"
|
||||
raise Exception(t % (args.q_mp3,))
|
||||
else:
|
||||
zss = set(args.th_r_ffa.split(",") + args.th_r_ffv.split(","))
|
||||
zss = args.th_r_ffa | args.th_r_ffv
|
||||
args.au_unpk = {
|
||||
k: v for k, v in args.au_unpk.items() if v.split(".")[0] not in zss
|
||||
}
|
||||
|
||||
args.th_poke = min(args.th_poke, args.th_maxage, args.ac_maxage)
|
||||
if not args.th_clean:
|
||||
args.th_poke = 0
|
||||
|
||||
zms = ""
|
||||
if not args.https_only:
|
||||
@@ -438,9 +536,6 @@ class SvcHub(object):
|
||||
|
||||
self.tftpd: Optional[Tftpd] = None
|
||||
|
||||
if args.sftp or args.ftp or args.ftps or args.tftp:
|
||||
Daemon(self.start_ftpd, "start_tftpd")
|
||||
|
||||
if args.smb:
|
||||
# impacket.dcerpc is noisy about listen timeouts
|
||||
sto = socket.getdefaulttimeout()
|
||||
@@ -479,6 +574,8 @@ class SvcHub(object):
|
||||
for nm in args.ipr_u.values():
|
||||
nm.mutex = threading.Lock()
|
||||
|
||||
self._reload_thumbsrv()
|
||||
|
||||
def _db_onfail_ses(self) -> None:
|
||||
self.args.no_ses = True
|
||||
|
||||
@@ -762,29 +859,19 @@ class SvcHub(object):
|
||||
cur.close()
|
||||
db.close()
|
||||
|
||||
def start_ftpd(self) -> None:
|
||||
time.sleep(30)
|
||||
|
||||
if hasattr(self, "sftpd") and not self.sftpd:
|
||||
self.restart_sftpd()
|
||||
|
||||
if hasattr(self, "ftpd") and not self.ftpd:
|
||||
self.restart_ftpd()
|
||||
|
||||
if hasattr(self, "tftpd") and not self.tftpd:
|
||||
self.restart_tftpd()
|
||||
|
||||
def restart_sftpd(self) -> None:
|
||||
if not hasattr(self, "sftpd"):
|
||||
return
|
||||
|
||||
from .sftpd import Sftpd
|
||||
|
||||
if self.sftpd:
|
||||
return # todo
|
||||
with self.init_mutex:
|
||||
if self.sftpd:
|
||||
return # todo
|
||||
|
||||
self.sftpd = Sftpd(self)
|
||||
self.sftpd.run()
|
||||
|
||||
self.sftpd = Sftpd(self)
|
||||
self.sftpd.run()
|
||||
self.log("root", "started SFTPd")
|
||||
|
||||
def restart_ftpd(self) -> None:
|
||||
@@ -793,13 +880,15 @@ class SvcHub(object):
|
||||
|
||||
from .ftpd import Ftpd
|
||||
|
||||
if self.ftpd:
|
||||
return # todo
|
||||
with self.init_mutex:
|
||||
if self.ftpd:
|
||||
return # todo
|
||||
|
||||
if not os.path.exists(self.args.cert):
|
||||
ensure_cert(self.log, self.args)
|
||||
if not os.path.exists(self.args.cert):
|
||||
ensure_cert(self.log, self.args)
|
||||
|
||||
self.ftpd = Ftpd(self)
|
||||
|
||||
self.ftpd = Ftpd(self)
|
||||
self.log("root", "started FTPd")
|
||||
|
||||
def restart_tftpd(self) -> None:
|
||||
@@ -808,10 +897,11 @@ class SvcHub(object):
|
||||
|
||||
from .tftpd import Tftpd
|
||||
|
||||
if self.tftpd:
|
||||
return # todo
|
||||
with self.init_mutex:
|
||||
if self.tftpd:
|
||||
return # todo
|
||||
|
||||
self.tftpd = Tftpd(self)
|
||||
self.tftpd = Tftpd(self)
|
||||
|
||||
def thr_httpsrv_up(self) -> None:
|
||||
time.sleep(1 if self.args.ign_ebind_all else 5)
|
||||
@@ -948,36 +1038,62 @@ class SvcHub(object):
|
||||
def after_httpsrv_up(self) -> None:
|
||||
self.up2k.init_vols()
|
||||
|
||||
Daemon(self.sd_notify, "sd-notify")
|
||||
zb = os.environ.get("NOTIFY_SOCKET")
|
||||
if zb:
|
||||
Daemon(self.sd_notify, "sd-notify", (zb,))
|
||||
|
||||
zb = os.environ.get("S6_NOTIFY_FD")
|
||||
if zb:
|
||||
Daemon(self.s6_notify, "s6-notify", (zb,))
|
||||
|
||||
def _check_toolpaths(self) -> None:
|
||||
for vol in self.asrv.vfs.all_vols.values():
|
||||
if not vol.realpath or not vol.axs.uwrite:
|
||||
continue
|
||||
ap_vol = (vol.realpath + os.sep).encode("utf-8")
|
||||
for zsl in (HAVE_FFMPEG, HAVE_FFPROBE, HAVE_DCRAW):
|
||||
if zsl and zsl[0].startswith(ap_vol):
|
||||
zs = zsl[0].decode("utf-8", "replace")
|
||||
t = "will not use [%s] because it is inside a writable volume [/%s] => [%s] and --unsafe-tools is not enabled"
|
||||
self.log("root", t % (zs, vol.vpath, vol.realpath), 3)
|
||||
del zsl[0]
|
||||
|
||||
def _feature_test(self) -> None:
|
||||
fok = []
|
||||
fng = []
|
||||
t_ff = "transcode audio, create spectrograms, video thumbnails"
|
||||
|
||||
# fmt: off
|
||||
to_check = [
|
||||
(HAVE_SQLITE3, "sqlite", "sessions and file/media indexing"),
|
||||
(HAVE_PIL, "pillow", "image thumbnails (plenty fast)"),
|
||||
(HAVE_VIPS, "vips", "image thumbnails (faster, eats more ram)"),
|
||||
(HAVE_PIL, "pillow", "image thumbnails (good and fast)"),
|
||||
(HAVE_VIPS, "vips", "image thumbnails (eats ram; will prefer pillow)"),
|
||||
(H_PIL_WEBP, "pillow-webp", "create thumbnails as webp files"),
|
||||
(HAVE_FFMPEG, "ffmpeg", t_ff + ", good-but-slow image thumbnails"),
|
||||
(HAVE_FFPROBE, "ffprobe", t_ff + ", read audio/media tags"),
|
||||
(HAVE_BWRAP, "bwrap", "sandbox to make ffmpeg less dangerous", not ANYWIN and not UNIX),
|
||||
(HAVE_MUTAGEN, "mutagen", "read audio tags (ffprobe is better but slower)"),
|
||||
(HAVE_ARGON2, "argon2", "secure password hashing (advanced users only)"),
|
||||
(HAVE_ZMQ, "pyzmq", "send zeromq messages from event-hooks"),
|
||||
(H_PIL_HEIF, "pillow-heif", "read .heif pics with pillow (rarely useful)"),
|
||||
(H_PIL_AVIF, "pillow-avif", "read .avif pics with pillow (rarely useful)"),
|
||||
(HAVE_RAW, "rawpy", "read RAW images"),
|
||||
(HAVE_RAWPY, "rawpy", "read RAW images"),
|
||||
(HAVE_DCRAW, "libraw", "read RAW images"),
|
||||
(HAVE_PSUTIL, "psutil", "improved plugin cleanup (rarely useful)", ANYWIN),
|
||||
]
|
||||
if ANYWIN:
|
||||
to_check += [
|
||||
(HAVE_PSUTIL, "psutil", "improved plugin cleanup (rarely useful)")
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
verbose = self.args.deps
|
||||
if verbose:
|
||||
self.log("dependencies", "")
|
||||
|
||||
for have, feat, what in to_check:
|
||||
for zc in to_check:
|
||||
try:
|
||||
have, feat, what = zc
|
||||
except:
|
||||
have, feat, what, zb = zc
|
||||
if not zb:
|
||||
continue
|
||||
lst = fok if have else fng
|
||||
lst.append((feat, what))
|
||||
if verbose:
|
||||
@@ -1028,14 +1144,23 @@ class SvcHub(object):
|
||||
t = "not listening on any ip-addresses (only unix-sockets and/or FDs); cannot enable zeroconf/mdns/ssdp as requested"
|
||||
self.log("root", t, 3)
|
||||
|
||||
if not self.args.no_dav:
|
||||
if al.wopi and not al.wopi_url and not al.wopi_urls:
|
||||
self.log("root", "disabling wopi; need either wopi-url or wopi-urls", 1)
|
||||
al.wopi = False
|
||||
|
||||
if self.args.wopi or not self.args.no_dav:
|
||||
from .dxml import DXML_OK
|
||||
|
||||
if not DXML_OK:
|
||||
if not self.args.no_dav:
|
||||
self.args.no_dav = True
|
||||
t = "WARNING:\nDisabling WebDAV support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n"
|
||||
self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1)
|
||||
self.args.wopi = False
|
||||
self.args.no_dav = True
|
||||
t = "WARNING:\nDisabling WebDAV and WOPI support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n"
|
||||
self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1)
|
||||
|
||||
if self.args.wopi and self.args.j != 1:
|
||||
self.args.wopi = False
|
||||
t = "WARNING: Disabling --wopi because -j is not 1 (the default and recommended -j value)"
|
||||
self.log("root", t, 1)
|
||||
|
||||
if (
|
||||
not E.scfg
|
||||
@@ -1104,26 +1229,35 @@ class SvcHub(object):
|
||||
if al.rsp_jtr:
|
||||
al.rsp_slp = 0.000001
|
||||
|
||||
zsl = al.th_covers.split(",")
|
||||
zsl = [x.strip() for x in zsl]
|
||||
zsl = [x for x in zsl if x]
|
||||
al.th_covers = zsl
|
||||
al.th_coversd = zsl + ["." + x for x in zsl]
|
||||
al.th_covers_set = set(al.th_covers)
|
||||
al.th_coversd_set = set(al.th_coversd)
|
||||
zs = al.use_bwrap.strip().lower()
|
||||
if zs == "a":
|
||||
if not HAVE_BWRAP:
|
||||
al.th_bwrap = ""
|
||||
elif zs == "n":
|
||||
al.th_bwrap = ""
|
||||
elif zs == "f":
|
||||
al.th_bwrap = al.th_bwrap or "bwrap-not-available"
|
||||
else:
|
||||
raise Exception("--use-bwrap must be a/n/f")
|
||||
|
||||
for k in "c".split(" "):
|
||||
if self.args.env_expand in (0, 2):
|
||||
break
|
||||
|
||||
vl = getattr(al, k)
|
||||
if not vl:
|
||||
continue
|
||||
|
||||
vl = [os.path.expandvars(os.path.expanduser(x)) for x in vl]
|
||||
vl = [os.path.expanduser(self.args.shenvexp(x)) for x in vl]
|
||||
setattr(al, k, vl)
|
||||
|
||||
for k in "lo hist dbpath ssl_log".split(" "):
|
||||
if self.args.env_expand in (0, 2):
|
||||
break
|
||||
|
||||
vs = getattr(al, k)
|
||||
if vs:
|
||||
vs = os.path.expandvars(os.path.expanduser(vs))
|
||||
vs = os.path.expanduser(self.args.shenvexp(vs))
|
||||
setattr(al, k, vs)
|
||||
|
||||
for k in "idp_adm stats_u".split(" "):
|
||||
@@ -1138,7 +1272,16 @@ class SvcHub(object):
|
||||
vsa = [x.upper() for x in vsa if x]
|
||||
setattr(al, k + "_set", set(vsa))
|
||||
|
||||
zs = "dav_ua1 sus_urls nonsus_urls ua_nodav ua_nodoc ua_nozip"
|
||||
zs = "th_bwrap"
|
||||
for k in zs.split(" "):
|
||||
zsl = [x for x in str(getattr(al, k, "")).split(" ") if x]
|
||||
zbl = [x.encode("ascii", "replace") for x in zsl]
|
||||
setattr(al, k + "_s", zsl)
|
||||
setattr(al, k + "_b", zbl)
|
||||
|
||||
TH_BWRAP[:] = al.th_bwrap_b
|
||||
|
||||
zs = "acao_re dav_ua1 lf_url sus_urls nonsus_urls ua_nodav ua_nodoc ua_nozip"
|
||||
for k in zs.split(" "):
|
||||
vs = getattr(al, k)
|
||||
if not vs or vs == "no":
|
||||
@@ -1203,6 +1346,9 @@ class SvcHub(object):
|
||||
for x in [x.split(" ") for x in al.sftp_key or []]
|
||||
}
|
||||
|
||||
if not al.certkey:
|
||||
al.certkey = None
|
||||
|
||||
mte = ODict.fromkeys(DEF_MTE.split(","), True)
|
||||
al.mte = odfusion(mte, al.mte)
|
||||
|
||||
@@ -1244,13 +1390,6 @@ class SvcHub(object):
|
||||
except:
|
||||
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.tcolor = al.tcolor.lstrip("#")
|
||||
@@ -1265,18 +1404,20 @@ class SvcHub(object):
|
||||
self.args.name_html = zs
|
||||
|
||||
zs = al.u2sz
|
||||
if zs in ("no", "1,1,1"):
|
||||
zs = "0"
|
||||
zsl = [x.strip() for x in zs.split(",")]
|
||||
if len(zsl) not in (1, 3):
|
||||
t = "invalid --u2sz; must be either one number, or a comma-separated list of three numbers (min,default,max)"
|
||||
if len(zsl) not in (1, 2):
|
||||
t = "invalid --u2sz; must be either one number, or a comma-separated list of two numbers (target,max)"
|
||||
raise Exception(t)
|
||||
if len(zsl) < 3:
|
||||
zsl = ["1", zs, zs]
|
||||
zi2 = 1
|
||||
if len(zsl) < 2:
|
||||
zsl = [zs, zs]
|
||||
zi2 = 0
|
||||
for zs in zsl:
|
||||
zi = int(zs)
|
||||
# arbitrary constraint (anything above 2 GiB is probably unintended)
|
||||
if zi < 1 or zi > 2047:
|
||||
raise Exception("invalid --u2sz; minimum is 1, max is 2047")
|
||||
if zi < 0 or zi > 2047:
|
||||
raise Exception("invalid --u2sz; minimum is 0, max is 2047")
|
||||
if zi < zi2:
|
||||
raise Exception("invalid --u2sz; values must be equal or ascending")
|
||||
zi2 = zi
|
||||
@@ -1334,7 +1475,7 @@ class SvcHub(object):
|
||||
|
||||
def _logname(self) -> str:
|
||||
dt = datetime.now(self.tz)
|
||||
fn = str(self.args.lo)
|
||||
fn = str(self.lo1)
|
||||
for fs in "YmdHMS":
|
||||
fs = "%" + fs
|
||||
if fs in fn:
|
||||
@@ -1342,16 +1483,18 @@ class SvcHub(object):
|
||||
|
||||
return fn
|
||||
|
||||
def _setup_logfile(self, printed: str) -> None:
|
||||
base_fn = fn = sel_fn = self._logname()
|
||||
do_xz = fn.lower().endswith(".xz")
|
||||
if fn != self.args.lo:
|
||||
ctr = 0
|
||||
def _setup_logfile(self) -> None:
|
||||
base_fn = fn = self._logname()
|
||||
sel_fn = fn + self.lo2
|
||||
do_xz = sel_fn.replace("%R", "").lower().endswith(".xz")
|
||||
if "%R" in self.args.lo:
|
||||
# yup this is a race; if started sufficiently concurrently, two
|
||||
# copyparties can grab the same logfile (considered and ignored)
|
||||
while os.path.exists(sel_fn):
|
||||
ctr += 1
|
||||
sel_fn = "{}.{}".format(fn, ctr)
|
||||
for n in range(9999):
|
||||
if n or "!" in self.args.rlo:
|
||||
sel_fn = self.rot_fmt % (fn, n)
|
||||
if not os.path.exists(sel_fn):
|
||||
break
|
||||
|
||||
fn = sel_fn
|
||||
try:
|
||||
@@ -1359,18 +1502,31 @@ class SvcHub(object):
|
||||
except:
|
||||
pass
|
||||
|
||||
if self.args.rlo in ("del", "over"):
|
||||
tmode = "wt"
|
||||
bmode = "w"
|
||||
else:
|
||||
tmode = "at"
|
||||
bmode = "a"
|
||||
|
||||
if self.args.rlo == "del":
|
||||
try:
|
||||
bos.unlink(fn)
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
if do_xz:
|
||||
import lzma
|
||||
|
||||
lh = lzma.open(fn, "wt", encoding="utf-8", errors="replace", preset=0)
|
||||
lh = lzma.open(fn, tmode, encoding="utf-8", errors="replace", preset=0)
|
||||
self.args.no_logflush = True
|
||||
else:
|
||||
lh = open(fn, "wt", encoding="utf-8", errors="replace")
|
||||
lh = open(fn, tmode, encoding="utf-8", errors="replace")
|
||||
except:
|
||||
import codecs
|
||||
|
||||
lh = codecs.open(fn, "w", encoding="utf-8", errors="replace")
|
||||
lh = codecs.open(fn, bmode, encoding="utf-8", errors="replace")
|
||||
|
||||
if getattr(self.args, "free_umask", False):
|
||||
os.fchmod(lh.fileno(), 0o644)
|
||||
@@ -1382,9 +1538,11 @@ class SvcHub(object):
|
||||
argv = ['"{}"'.format(x) for x in argv]
|
||||
|
||||
msg = "[+] opened logfile [{}]\n".format(fn)
|
||||
printed += msg
|
||||
printed = "".join(lprinted) + msg
|
||||
t = "t0: {:.3f}\nargv: {}\n\n{}"
|
||||
lh.write(t.format(self.E.t0, " ".join(argv), printed))
|
||||
if not self.args.no_logflush:
|
||||
lh.flush()
|
||||
self.logf = lh
|
||||
self.logf_base_fn = base_fn
|
||||
print(msg, end="")
|
||||
@@ -1402,31 +1560,44 @@ class SvcHub(object):
|
||||
|
||||
sigs = [signal.SIGINT, signal.SIGTERM]
|
||||
if not ANYWIN:
|
||||
sigs.append(signal.SIGUSR1)
|
||||
sigs.append(signal.SIGHUP)
|
||||
|
||||
for (opt, mem) in (
|
||||
("logrot_sig", "sig_logrot"),
|
||||
("reload_sig", "sig_reload"),
|
||||
("stack_sig", "sig_stack"),
|
||||
):
|
||||
zs = getattr(self.args, opt)
|
||||
if not zs:
|
||||
continue
|
||||
zi = signame2int(zs)
|
||||
setattr(self, mem, zi)
|
||||
try:
|
||||
sigs.append(signal.Signals(zi))
|
||||
except:
|
||||
t = "using unknown signal %r as %s"
|
||||
self.log("root", t % (zi, mem), 3)
|
||||
sigs.append(zi)
|
||||
|
||||
for sig in sigs:
|
||||
signal.signal(sig, self.signal_handler)
|
||||
if sig not in BLOCK_SIGS and BLOCK_SIGS:
|
||||
BLOCK_SIGS.append(sig)
|
||||
|
||||
# macos hangs after shutdown on sigterm with while-sleep,
|
||||
# windows cannot ^c stop_cond (and win10 does the macos thing but winxp is fine??)
|
||||
# linux is fine with both,
|
||||
# never lucky
|
||||
if ANYWIN:
|
||||
# msys-python probably fine but >msys-python
|
||||
Daemon(self.stop_thr, "svchub-sig")
|
||||
if self.args.sig_thr:
|
||||
Daemon(self._signal_thr, "svchub-sig")
|
||||
|
||||
try:
|
||||
while not self.stop_req:
|
||||
while not self.stopping:
|
||||
time.sleep(1)
|
||||
except:
|
||||
pass
|
||||
|
||||
self.shutdown()
|
||||
# cant join; eats signals on win10
|
||||
while not self.stopped:
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
self.stop_thr()
|
||||
self._signal_thr()
|
||||
|
||||
def start_zeroconf(self) -> None:
|
||||
self.zc_ngen += 1
|
||||
@@ -1468,24 +1639,22 @@ class SvcHub(object):
|
||||
self.log("root", "reload done")
|
||||
t += "\n\nchanges to global options (if any) require a restart of copyparty to take effect"
|
||||
self.broker.reload()
|
||||
self._reload_thumbsrv()
|
||||
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:
|
||||
with self.asrv.mutex:
|
||||
self.asrv.load_sessions(True)
|
||||
self.broker.reload_sessions()
|
||||
|
||||
def stop_thr(self) -> None:
|
||||
while not self.stop_req:
|
||||
with self.stop_cond:
|
||||
self.stop_cond.wait(9001)
|
||||
|
||||
if self.reload_req:
|
||||
self.reload_req = False
|
||||
self.reload(True, True)
|
||||
|
||||
self.shutdown()
|
||||
|
||||
def kill9(self, delay: float = 0.0) -> None:
|
||||
if delay > 0.01:
|
||||
time.sleep(delay)
|
||||
@@ -1498,26 +1667,42 @@ class SvcHub(object):
|
||||
os.kill(os.getpid(), signal.SIGKILL)
|
||||
|
||||
def signal_handler(self, sig: int, frame: Optional[FrameType]) -> None:
|
||||
if self.stopping:
|
||||
if self.nsigs <= 0:
|
||||
if sig in (signal.SIGINT, signal.SIGTERM):
|
||||
self.nsigs -= 1
|
||||
|
||||
if self.nsigs == 0:
|
||||
try:
|
||||
threading.Thread(target=self.pr, args=("OMBO BREAKER",)).start()
|
||||
time.sleep(0.1)
|
||||
except:
|
||||
pass
|
||||
|
||||
if self.nsigs <= 0:
|
||||
self.kill9()
|
||||
else:
|
||||
self.nsigs -= 1
|
||||
return
|
||||
|
||||
if not ANYWIN and sig == signal.SIGUSR1:
|
||||
self.reload_req = True
|
||||
self.sig.put(sig)
|
||||
|
||||
def _signal_thr(self) -> None:
|
||||
while not self.stopping:
|
||||
sig = self.sig.get()
|
||||
self._signal_handler(sig)
|
||||
|
||||
def _signal_handler(self, sig: int) -> None:
|
||||
if sig == self.sig_logrot:
|
||||
self.log("root", "signal: logrotate")
|
||||
dt = datetime.now(self.tz)
|
||||
self.logf_base_fn = "\t"
|
||||
self._set_next_day(dt)
|
||||
|
||||
elif sig == self.sig_reload:
|
||||
self.log("root", "signal: reload")
|
||||
self.reload(True, True)
|
||||
|
||||
elif sig == self.sig_stack:
|
||||
self.log("root", "signal: stack%s" % (alltrace(),))
|
||||
|
||||
else:
|
||||
self.stop_req = True
|
||||
|
||||
with self.stop_cond:
|
||||
self.stop_cond.notify_all()
|
||||
self.shutdown()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
if self.stopping:
|
||||
@@ -1525,10 +1710,8 @@ class SvcHub(object):
|
||||
|
||||
# start_log_thrs(print, 0.1, 1)
|
||||
|
||||
self.nsigs = 3
|
||||
self.stopping = True
|
||||
self.stop_req = True
|
||||
with self.stop_cond:
|
||||
self.stop_cond.notify_all()
|
||||
|
||||
ret = 1
|
||||
try:
|
||||
@@ -1554,6 +1737,7 @@ class SvcHub(object):
|
||||
|
||||
if self.thumbsrv:
|
||||
self.thumbsrv.shutdown()
|
||||
sutil_close_pools()
|
||||
|
||||
for n in range(200): # 10s
|
||||
time.sleep(0.05)
|
||||
@@ -1644,7 +1828,7 @@ class SvcHub(object):
|
||||
def _set_next_day(self, dt: datetime) -> None:
|
||||
if self.cday and self.logf and self.logf_base_fn != self._logname():
|
||||
self.logf.close()
|
||||
self._setup_logfile("")
|
||||
self._setup_logfile()
|
||||
|
||||
self.cday = dt.day
|
||||
self.cmon = dt.month
|
||||
@@ -1714,6 +1898,69 @@ class SvcHub(object):
|
||||
if not self.args.no_logflush:
|
||||
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:
|
||||
try:
|
||||
with self.log_mutex:
|
||||
@@ -1764,12 +2011,8 @@ class SvcHub(object):
|
||||
self.log("svchub", "cannot efficiently use multiple CPU cores")
|
||||
return False
|
||||
|
||||
def sd_notify(self) -> None:
|
||||
def sd_notify(self, zb: bytes) -> None:
|
||||
try:
|
||||
zb = os.environ.get("NOTIFY_SOCKET")
|
||||
if not zb:
|
||||
return
|
||||
|
||||
addr = unicode(zb)
|
||||
if addr.startswith("@"):
|
||||
addr = "\0" + addr[1:]
|
||||
@@ -1781,7 +2024,19 @@ class SvcHub(object):
|
||||
sck.connect(addr)
|
||||
sck.sendall(b"READY=1")
|
||||
except:
|
||||
self.log("sd_notify", min_ex())
|
||||
t = "NOTIFY_SOCKET=%s:\n%s"
|
||||
self.log("sd-notify", t % (zb, min_ex()), 1)
|
||||
|
||||
def s6_notify(self, zb: bytes) -> None:
|
||||
try:
|
||||
fd = int(zb)
|
||||
if fd < 3:
|
||||
raise Exception("value < 3")
|
||||
os.write(fd, b"\n")
|
||||
os.close(fd)
|
||||
except:
|
||||
t = "S6_NOTIFY_FD=%s:\n%s"
|
||||
self.log("s6-notify", t % (zb, min_ex()), 1)
|
||||
|
||||
def log_stacks(self) -> None:
|
||||
td = time.time() - self.tstack
|
||||
@@ -1800,6 +2055,7 @@ class SvcHub(object):
|
||||
next_chk = 0
|
||||
# self.args.vc_age = 2 / 60
|
||||
fpath = os.path.join(self.E.cfg, "vuln_advisory.json")
|
||||
minsev = CVE_SEVS.get(self.args.vc_sev, 0)
|
||||
while not self.stopping:
|
||||
now = time.time()
|
||||
if now < next_chk:
|
||||
@@ -1843,10 +2099,13 @@ class SvcHub(object):
|
||||
continue
|
||||
|
||||
try:
|
||||
sver = "0.1"
|
||||
advisories = json.loads(jtxt)
|
||||
for adv in advisories:
|
||||
if adv.get("state") == "closed":
|
||||
continue
|
||||
if CVE_SEVS.get(adv.get("severity"), 9) < minsev:
|
||||
continue
|
||||
vuln = {}
|
||||
for x in adv["vulnerabilities"]:
|
||||
if x["package"]["name"].lower() == "copyparty":
|
||||
@@ -1864,10 +2123,13 @@ class SvcHub(object):
|
||||
if self.args.vc_exit:
|
||||
self.sigterm()
|
||||
return
|
||||
else:
|
||||
t = "%sok; v%s and newer is safe"
|
||||
self.log("ver-chk", t % (src, sver), 2)
|
||||
t = "%sok; v%s and newer is safe"
|
||||
self.log("ver-chk", t % (src, sver), 2)
|
||||
next_chk = time.time() + self.args.vc_age * 3600 - age
|
||||
except Exception as e:
|
||||
t = "failed to process vulnerability advisory; %s"
|
||||
self.log("ver-chk", t % (min_ex()), 1)
|
||||
try:
|
||||
os.unlink(fpath)
|
||||
except:
|
||||
pass
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import calendar
|
||||
import stat
|
||||
@@ -104,7 +104,7 @@ def gen_hdr(
|
||||
ret += spack(b"<LL", vsz, vsz)
|
||||
|
||||
# windows support (the "?" replace below too)
|
||||
fn = sanitize_to(fn, VPTL_WIN)
|
||||
fn = sanitize_to(fn, VPTL_WIN, "win")
|
||||
bfn = fn.encode("utf-8" if utf8 else "cp437", "replace").replace(b"?", b"_")
|
||||
|
||||
# add ntfs (0x24) and/or unix (0x10) extrafields for utc, add z64 if requested
|
||||
|
||||
+15
-20
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import re
|
||||
@@ -7,7 +7,7 @@ import socket
|
||||
import sys
|
||||
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 .qrkode import QrCode, qr2png, qr2svg, qr2txt, qrgen
|
||||
from .util import (
|
||||
@@ -21,7 +21,8 @@ from .util import (
|
||||
VF_CAREFUL,
|
||||
Netdev,
|
||||
atomic_move,
|
||||
get_adapters,
|
||||
chkcmd,
|
||||
list_nics,
|
||||
min_ex,
|
||||
sunpack,
|
||||
termsize,
|
||||
@@ -86,6 +87,9 @@ class TcpSrv(object):
|
||||
successful_binds = 0
|
||||
try:
|
||||
for ipa in ips:
|
||||
if ipa == "no":
|
||||
self.args.ign_ebind_all = True
|
||||
continue
|
||||
try:
|
||||
self._listen(ipa, port)
|
||||
ok[ipa].append(port)
|
||||
@@ -460,23 +464,7 @@ class TcpSrv(object):
|
||||
def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]:
|
||||
listen_ips = [x for x in listen_ips if not x.startswith(("unix:", "fd:"))]
|
||||
|
||||
nics = get_adapters(True)
|
||||
eps: dict[str, Netdev] = {}
|
||||
for nic in nics:
|
||||
for nip in nic.ips:
|
||||
ipa = nip.ip[0] if ":" in str(nip.ip) else nip.ip
|
||||
sip = "{}/{}".format(ipa, nip.network_prefix)
|
||||
nd = Netdev(sip, nic.index or 0, nic.nice_name, "")
|
||||
eps[sip] = nd
|
||||
try:
|
||||
idx = socket.if_nametoindex(nd.name)
|
||||
if idx and idx != nd.idx:
|
||||
t = "netdev idx mismatch; ifaddr={} cpython={}"
|
||||
self.log("tcpsrv", t.format(nd.idx, idx), 3)
|
||||
nd.idx = idx
|
||||
except:
|
||||
pass
|
||||
|
||||
eps = list_nics()
|
||||
netlist = str(sorted(eps.items()))
|
||||
if netlist == self.netlist and self.netdevs:
|
||||
return {}
|
||||
@@ -510,6 +498,13 @@ class TcpSrv(object):
|
||||
return eps
|
||||
|
||||
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:
|
||||
next(f)
|
||||
for ln in f:
|
||||
|
||||
+12
-3
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
try:
|
||||
from types import SimpleNamespace
|
||||
@@ -42,6 +42,7 @@ from .util import (
|
||||
Daemon,
|
||||
ODict,
|
||||
exclude_dotfiles,
|
||||
exclude_dothidden,
|
||||
min_ex,
|
||||
runhook,
|
||||
set_fperms,
|
||||
@@ -274,7 +275,11 @@ class Tftpd(object):
|
||||
if not vfs.realpath:
|
||||
raise Exception("unmapped vfs")
|
||||
|
||||
return vfs, vpath, vfs.canonical(rem)
|
||||
ap = vfs.canonical(rem)
|
||||
if "bcasechk" in vfs.flags and not vfs.casechk(rem, True):
|
||||
raise Exception("file not found")
|
||||
|
||||
return vfs, vpath, ap
|
||||
|
||||
def _ls(self, vpath: str, raddress: str, rport: int, force=False) -> Any:
|
||||
# generate file listing if vpath is dir.txt and return as file object
|
||||
@@ -318,7 +323,11 @@ class Tftpd(object):
|
||||
ls = virs + reals
|
||||
|
||||
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]
|
||||
|
||||
try:
|
||||
|
||||
+28
-33
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import errno
|
||||
import os
|
||||
@@ -24,26 +24,17 @@ IMG_EXTS = set(["webp", "jpg", "png", "jxl"])
|
||||
|
||||
|
||||
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.log_func = hsrv.log
|
||||
self.args = hsrv.args
|
||||
self.asrv = hsrv.asrv
|
||||
|
||||
# cache on both sides for less broker spam
|
||||
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.cooldown = Cooldown(self.args.th_poke) if self.args.th_poke else None
|
||||
|
||||
self.thumbable = c["thumbable"]
|
||||
self.thumbable_native = c["thumbable_native"]
|
||||
self.fmt_pil = c["pil"]
|
||||
self.fmt_vips = c["vips"]
|
||||
self.fmt_raw = c["raw"]
|
||||
@@ -53,8 +44,8 @@ class ThumbCli(object):
|
||||
|
||||
# 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)
|
||||
self.can_webp = H_PIL_WEBP or nonpil
|
||||
self.can_jxl = H_PIL_JXL 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) and not self.args.th_no_jxl
|
||||
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.log_func("thumbcli", msg, c)
|
||||
@@ -69,6 +60,10 @@ class ThumbCli(object):
|
||||
if is_vid and "dvthumb" in dbv.flags:
|
||||
return None
|
||||
|
||||
is_custom = ext not in self.thumbable_native
|
||||
if is_custom and "dethumb" in dbv.flags:
|
||||
return None
|
||||
|
||||
want_opus = fmt in EXTS_AC
|
||||
is_au = ext in self.fmt_ffa
|
||||
is_vau = want_opus and ext in self.fmt_ffv
|
||||
@@ -86,7 +81,7 @@ class ThumbCli(object):
|
||||
elif want_opus:
|
||||
return None
|
||||
|
||||
is_img = not is_vid and not is_au
|
||||
is_img = not is_vid and not is_au and not is_custom
|
||||
if is_img and "dithumb" in dbv.flags:
|
||||
return None
|
||||
|
||||
@@ -95,23 +90,22 @@ class ThumbCli(object):
|
||||
if rem.startswith(".hist/th/") and rem.split(".")[-1] in IMG_EXTS:
|
||||
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:
|
||||
sfmt = "w"
|
||||
|
||||
if sfmt == "w":
|
||||
if (
|
||||
self.args.th_no_webp
|
||||
or not self.can_webp
|
||||
or (self.args.th_ff_jpg and (not is_img or preferred == "ff"))
|
||||
if not self.can_webp or (
|
||||
self.args.th_ff_jpg and (not is_img or preferred == "ff")
|
||||
):
|
||||
sfmt = "j"
|
||||
|
||||
if sfmt == "x":
|
||||
if self.args.th_no_jxl or not self.can_jxl:
|
||||
sfmt = "w"
|
||||
if sfmt == "x" and (
|
||||
not self.can_jxl or (not H_PIL_JXL and ext in self.fmt_raw)
|
||||
):
|
||||
sfmt = "w"
|
||||
|
||||
vf_crop = dbv.flags["crop"]
|
||||
vf_th3x = dbv.flags["th3x"]
|
||||
@@ -128,7 +122,7 @@ class ThumbCli(object):
|
||||
|
||||
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"
|
||||
self.log(t % (rem,), 6)
|
||||
return None
|
||||
@@ -162,14 +156,15 @@ class ThumbCli(object):
|
||||
pass
|
||||
|
||||
if ret:
|
||||
tdir = os.path.dirname(tpath)
|
||||
if self.cooldown.poke(tdir):
|
||||
self.broker.say("thumbsrv.poke", tdir)
|
||||
if self.cooldown:
|
||||
tdir = os.path.dirname(tpath)
|
||||
if self.cooldown.poke(tdir):
|
||||
self.broker.say("thumbsrv.poke", tdir)
|
||||
|
||||
if want_opus:
|
||||
# audio files expire individually
|
||||
if self.cooldown.poke(tpath):
|
||||
self.broker.say("thumbsrv.poke", tpath)
|
||||
if want_opus:
|
||||
# audio files expire individually
|
||||
if self.cooldown.poke(tpath):
|
||||
self.broker.say("thumbsrv.poke", tpath)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
+318
-104
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
@@ -18,16 +18,27 @@ from queue import Queue
|
||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
|
||||
from .authsrv import VFS
|
||||
from .bos import bos
|
||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, au_unpk, ffprobe
|
||||
from .mtag import (
|
||||
HAVE_FFMPEG,
|
||||
HAVE_FFPROBE,
|
||||
TH_BWRAP,
|
||||
au_unpk,
|
||||
bwrap,
|
||||
bwrap_fail,
|
||||
ffprobe,
|
||||
have_ff,
|
||||
)
|
||||
from .util import BytesIO # type: ignore
|
||||
from .util import (
|
||||
FFMPEG_URL,
|
||||
SCWD,
|
||||
VF_CAREFUL,
|
||||
Cooldown,
|
||||
Daemon,
|
||||
afsenc,
|
||||
atomic_move,
|
||||
fsenc,
|
||||
loadpy,
|
||||
min_ex,
|
||||
runcmd,
|
||||
statdir,
|
||||
@@ -186,27 +197,37 @@ try:
|
||||
if os.environ.get("PRTY_NO_VIPS"):
|
||||
raise ImportError()
|
||||
|
||||
HAVE_VIPS = True
|
||||
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"
|
||||
|
||||
import pyvips
|
||||
|
||||
pyvips.cache_set_max(0)
|
||||
logging.getLogger("pyvips").setLevel(logging.WARNING)
|
||||
HAVE_VIPS = True
|
||||
except Exception as e:
|
||||
HAVE_VIPS = False
|
||||
if not isinstance(e, ImportError):
|
||||
logging.warning("libvips found, but failed to load: " + str(e))
|
||||
|
||||
|
||||
PRTY_NO_RAW = os.environ.get("PRTY_NO_RAW")
|
||||
PRTY_NO_RAWPY = PRTY_NO_RAW or os.environ.get("PRTY_NO_RAWPY")
|
||||
PRTY_NO_DCRAW = PRTY_NO_RAW or os.environ.get("PRTY_NO_DCRAW")
|
||||
try:
|
||||
if os.environ.get("PRTY_NO_RAW"):
|
||||
if PRTY_NO_RAWPY:
|
||||
raise Exception()
|
||||
|
||||
HAVE_RAW = True
|
||||
import rawpy
|
||||
|
||||
logging.getLogger("rawpy").setLevel(logging.WARNING)
|
||||
HAVE_RAWPY = True
|
||||
except:
|
||||
HAVE_RAW = False
|
||||
HAVE_RAWPY = False
|
||||
|
||||
|
||||
HAVE_DCRAW = not PRTY_NO_DCRAW and have_ff("dcraw_emu")
|
||||
|
||||
|
||||
th_dir_cache = {}
|
||||
@@ -220,11 +241,6 @@ def thumb_path(histpath: str, rem: str, mtime: float, fmt: str, ffa: set[str]) -
|
||||
if not rd:
|
||||
rd = "\ntop"
|
||||
|
||||
# spectrograms are never cropped; strip fullsize flag
|
||||
ext = rem.split(".")[-1].lower()
|
||||
if ext in ffa and fmt[:2] in ("wf", "jf", "xf"):
|
||||
fmt = fmt.replace("f", "")
|
||||
|
||||
dcache = th_dir_cache
|
||||
rd_key = rd + "\n" + fmt
|
||||
rd = dcache.get(rd_key)
|
||||
@@ -256,7 +272,10 @@ class ThumbSrv(object):
|
||||
self.args = hub.args
|
||||
self.log_func = hub.log
|
||||
|
||||
self.poke_cd = Cooldown(self.args.th_poke)
|
||||
self.log = self._log
|
||||
self.nextlog = 0
|
||||
|
||||
self.poke_cd = Cooldown(self.args.th_poke) if self.args.th_poke else None
|
||||
|
||||
self.mutex = threading.Lock()
|
||||
self.busy: dict[str, list[threading.Condition]] = {}
|
||||
@@ -269,6 +288,18 @@ class ThumbSrv(object):
|
||||
|
||||
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)
|
||||
for n in range(self.nthr):
|
||||
Daemon(self.worker, "thumb-{}-{}".format(n, self.nthr))
|
||||
@@ -288,27 +319,17 @@ class ThumbSrv(object):
|
||||
if ANYWIN and self.args.no_acode:
|
||||
self.log("download FFmpeg to fix it:\033[0m " + FFMPEG_URL, 3)
|
||||
|
||||
self.conv_raw = self._conv_rawpy if HAVE_RAWPY else self._conv_dcraw
|
||||
|
||||
if self.args.th_clean:
|
||||
Daemon(self.cleaner, "thumb.cln")
|
||||
|
||||
(
|
||||
self.fmt_pil,
|
||||
self.fmt_vips,
|
||||
self.fmt_raw,
|
||||
self.fmt_ffi,
|
||||
self.fmt_ffv,
|
||||
self.fmt_ffa,
|
||||
) = [
|
||||
set(y.split(","))
|
||||
for y in [
|
||||
self.args.th_r_pil,
|
||||
self.args.th_r_vips,
|
||||
self.args.th_r_raw,
|
||||
self.args.th_r_ffi,
|
||||
self.args.th_r_ffv,
|
||||
self.args.th_r_ffa,
|
||||
]
|
||||
]
|
||||
self.fmt_pil = self.args.th_r_pil
|
||||
self.fmt_vips = self.args.th_r_vips
|
||||
self.fmt_raw = self.args.th_r_raw
|
||||
self.fmt_ffi = self.args.th_r_ffi
|
||||
self.fmt_ffv = self.args.th_r_ffv
|
||||
self.fmt_ffa = self.args.th_r_ffa
|
||||
|
||||
if not H_PIL_HEIF:
|
||||
for f in "heif heifs heic heics".split(" "):
|
||||
@@ -327,21 +348,35 @@ class ThumbSrv(object):
|
||||
self.fmt_pil.discard(f)
|
||||
|
||||
self.thumbable: set[str] = set()
|
||||
self.thumbable_native: set[str] = set()
|
||||
self._build_thumbable()
|
||||
|
||||
def _build_thumbable(self) -> None:
|
||||
self.thumbable_native.clear()
|
||||
|
||||
if "pil" in self.args.th_dec:
|
||||
self.thumbable |= self.fmt_pil
|
||||
self.thumbable_native |= self.fmt_pil
|
||||
|
||||
if "vips" in self.args.th_dec:
|
||||
self.thumbable |= self.fmt_vips
|
||||
self.thumbable_native |= self.fmt_vips
|
||||
|
||||
if "raw" in self.args.th_dec:
|
||||
self.thumbable |= self.fmt_raw
|
||||
self.thumbable_native |= self.fmt_raw
|
||||
|
||||
if "ff" in self.args.th_dec:
|
||||
for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]:
|
||||
self.thumbable |= zss
|
||||
self.thumbable_native |= zss
|
||||
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.thumbable = self.thumbable_native | set(self.args.th_extract)
|
||||
|
||||
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)
|
||||
|
||||
def shutdown(self) -> None:
|
||||
@@ -417,9 +452,14 @@ class ThumbSrv(object):
|
||||
|
||||
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]]:
|
||||
return {
|
||||
"thumbable": self.thumbable,
|
||||
"thumbable_native": self.thumbable_native,
|
||||
"pil": self.fmt_pil,
|
||||
"vips": self.fmt_vips,
|
||||
"raw": self.fmt_raw,
|
||||
@@ -433,6 +473,11 @@ class ThumbSrv(object):
|
||||
zs = "th_dec th_no_webp th_no_jpg"
|
||||
for zs in zs.split(" "):
|
||||
ret.append("%s(%s)\n" % (zs, getattr(self.args, zs)))
|
||||
zs = "th_spec_fl"
|
||||
for zs in zs.split(" "):
|
||||
v = getattr(self.args, zs)
|
||||
if v:
|
||||
ret.append("%s(%s)\n" % (zs, v))
|
||||
zs = "th_qv th_qvx thsize th_spec_p convt"
|
||||
for zs in zs.split(" "):
|
||||
ret.append("%s(%s)\n" % (zs, vn.flags.get(zs)))
|
||||
@@ -497,19 +542,36 @@ class ThumbSrv(object):
|
||||
png_ok = False
|
||||
funs = []
|
||||
|
||||
tex = tpath.rsplit(".", 1)[-1]
|
||||
want_mp3 = tex == "mp3"
|
||||
want_opus = tex in ("opus", "owa", "caf")
|
||||
want_flac = tex == "flac"
|
||||
want_wav = tex == "wav"
|
||||
want_png = tex == "png"
|
||||
want_au = want_mp3 or want_opus or want_flac or want_wav
|
||||
|
||||
ap_extr = abspath
|
||||
if (
|
||||
ext in self.args.th_extract
|
||||
and not want_au
|
||||
and not want_png
|
||||
and "dethumb" not in vn.flags
|
||||
):
|
||||
ap_extr = self.run_extractor(self.args.th_extract[ext], abspath, vn)
|
||||
if ap_extr:
|
||||
ext = ap_extr.rsplit(".", 1)[-1]
|
||||
|
||||
if ext in self.args.au_unpk:
|
||||
ap_unpk = au_unpk(self.log, self.args.au_unpk, abspath, vn)
|
||||
ap_unpk = au_unpk(self.log, self.args.au_unpk, ap_extr, vn)
|
||||
elif ext in self.thumbable_native:
|
||||
ap_unpk = ap_extr
|
||||
else:
|
||||
ap_unpk = abspath
|
||||
ap_unpk = ""
|
||||
|
||||
if ap_extr and ap_extr != ap_unpk and ap_extr != abspath:
|
||||
wunlink(self.log, ap_extr, vn.flags)
|
||||
|
||||
if ap_unpk and not bos.path.exists(tpath):
|
||||
tex = tpath.rsplit(".", 1)[-1]
|
||||
want_mp3 = tex == "mp3"
|
||||
want_opus = tex in ("opus", "owa", "caf")
|
||||
want_flac = tex == "flac"
|
||||
want_wav = tex == "wav"
|
||||
want_png = tex == "png"
|
||||
want_au = want_mp3 or want_opus or want_flac or want_wav
|
||||
for lib in self.args.th_dec:
|
||||
can_au = lib == "ff" and (
|
||||
ext in self.fmt_ffa or ext in self.fmt_ffv
|
||||
@@ -517,7 +579,11 @@ class ThumbSrv(object):
|
||||
|
||||
if lib == "pil" and ext in self.fmt_pil and tex in self.fmt_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)
|
||||
elif lib == "raw" and ext in self.fmt_raw:
|
||||
funs.append(self.conv_raw)
|
||||
@@ -613,6 +679,66 @@ class ThumbSrv(object):
|
||||
with self.mutex:
|
||||
self.nthr -= 1
|
||||
|
||||
def run_extractor(self, extr: str, abspath: str, vn: VFS) -> str:
|
||||
try:
|
||||
mod = loadpy(extr, self.args.hot_th_extr)
|
||||
except Exception as ex:
|
||||
self.log("extractor import failed; " + min_ex(), 1)
|
||||
return ""
|
||||
|
||||
fd = 0
|
||||
ret = ""
|
||||
stream = None
|
||||
try:
|
||||
res = mod.main(abspath, vn=vn, th_srv=self)
|
||||
if not res:
|
||||
t = "extractor %r returned no data for file %r"
|
||||
self.log(t % (extr, abspath))
|
||||
return ""
|
||||
|
||||
outext, stream, offset, whence, size = res
|
||||
if (
|
||||
outext not in self.thumbable_native
|
||||
or outext in self.fmt_ffa
|
||||
or outext in self.fmt_ffv
|
||||
):
|
||||
t = "extractor %r returned unsupported format %r"
|
||||
self.log(t % (extr, outext))
|
||||
return ""
|
||||
|
||||
remains = size if size is not None and size >= 0 else -1
|
||||
|
||||
stream.seek(offset, whence)
|
||||
fd, ret = tempfile.mkstemp("." + outext)
|
||||
bufsz = rsz = self.args.iobuf
|
||||
maxsz = self.args.th_extr_sz * 1048576
|
||||
outsz = 0
|
||||
with os.fdopen(fd, "wb", rsz) as f:
|
||||
fd = 0
|
||||
while True:
|
||||
if remains < rsz and remains >= 0:
|
||||
bufsz = remains
|
||||
buf = stream.read(bufsz)
|
||||
if not buf:
|
||||
break
|
||||
remains -= len(buf)
|
||||
outsz += len(buf)
|
||||
if outsz >= maxsz:
|
||||
raise Exception("too large")
|
||||
f.write(buf)
|
||||
return ret
|
||||
except Exception as e:
|
||||
if fd:
|
||||
os.close(fd)
|
||||
if ret:
|
||||
wunlink(self.log, ret, vn.flags)
|
||||
t = "failed to extract thumbnail from %r: %s"
|
||||
self.log(t % (abspath, min_ex()))
|
||||
return ""
|
||||
finally:
|
||||
if stream:
|
||||
stream.close()
|
||||
|
||||
def fancy_pillow(self, im: "Image.Image", fmt: str, vn: VFS) -> "Image.Image":
|
||||
# exif_transpose is expensive (loads full image + unconditional copy)
|
||||
res = self.getres(vn, fmt)
|
||||
@@ -715,8 +841,39 @@ class ThumbSrv(object):
|
||||
|
||||
self.conv_image_vips(_loader, tpath, fmt, vn)
|
||||
|
||||
def conv_raw(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
self.wait4ram(0.2, tpath)
|
||||
def _conv_dcraw(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
self.wait4ram(0.6, tpath)
|
||||
bap = fsenc(abspath)
|
||||
# fmt: off
|
||||
cmd = bwrap(HAVE_DCRAW[0], bap, b"") + [
|
||||
b"-h", # halfsize
|
||||
b"-o", b"1", # srgb
|
||||
b"-s", b"0", # first frame
|
||||
b"-Z", b"-", # to stdout
|
||||
bap,
|
||||
]
|
||||
# fmt: on
|
||||
p = sp.Popen(cmd, stdout=sp.PIPE, cwd=SCWD)
|
||||
try:
|
||||
if HAVE_PIL:
|
||||
self.conv_image_pil(Image.open(p.stdout), tpath, fmt, vn)
|
||||
elif HAVE_VIPS:
|
||||
ppm, _ = p.communicate(timeout=vn.flags["convt"])
|
||||
|
||||
def _loader(w: int, kw: dict) -> Any:
|
||||
return pyvips.Image.thumbnail_buffer(ppm, w, **kw)
|
||||
|
||||
self.conv_image_vips(_loader, tpath, fmt, vn)
|
||||
else:
|
||||
raise Exception(
|
||||
"either pil or vips is needed to process embedded bitmap thumbnails in raw files"
|
||||
)
|
||||
finally:
|
||||
if p and p.poll() is None:
|
||||
p.kill()
|
||||
|
||||
def _conv_rawpy(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
self.wait4ram(0.6, tpath)
|
||||
with rawpy.imread(abspath) as raw:
|
||||
thumb = raw.extract_thumb()
|
||||
if thumb.format == rawpy.ThumbFormat.JPEG and tpath.endswith(".jpg"):
|
||||
@@ -747,10 +904,21 @@ class ThumbSrv(object):
|
||||
|
||||
def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
self.wait4ram(0.2, tpath)
|
||||
ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
ret, raw, strms, ctnr = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
if not ret:
|
||||
return
|
||||
|
||||
if "vc" not in ret and "ac" in ret:
|
||||
# audio in a video trenchcoat
|
||||
return self.conv_spec(abspath, tpath, fmt, vn)
|
||||
|
||||
for strm in strms:
|
||||
if (
|
||||
strm.get("codec_type") == "video"
|
||||
and strm.get("DISPOSITION:attached_pic") == "1"
|
||||
):
|
||||
return self.conv_emb_cv(abspath, tpath, fmt, vn, strm)
|
||||
|
||||
ext = abspath.rsplit(".")[-1].lower()
|
||||
if ext in ["h264", "h265"] or ext in self.fmt_ffi:
|
||||
seek: list[bytes] = []
|
||||
@@ -777,16 +945,15 @@ class ThumbSrv(object):
|
||||
|
||||
res = self.getres(vn, fmt)
|
||||
bscale = scale.format(*list(res)).encode("utf-8")
|
||||
bap_in = fsenc(abspath)
|
||||
bap_out = fsenc(tpath)
|
||||
# fmt: off
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner"
|
||||
]
|
||||
cmd += seek
|
||||
cmd += [
|
||||
b"-i", fsenc(abspath),
|
||||
] + seek + [
|
||||
b"-i", bap_in,
|
||||
b"-map", imap,
|
||||
b"-vf", bscale,
|
||||
b"-frames:v", b"1",
|
||||
@@ -794,20 +961,20 @@ class ThumbSrv(object):
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
self._ffmpeg_im_o(tpath, vn, cmd)
|
||||
self._ffmpeg_im_o(bap_out, vn, cmd)
|
||||
|
||||
def _ffmpeg_im_o(self, tpath: str, vn: VFS, cmd: list[bytes]) -> None:
|
||||
if tpath.endswith(".jpg"):
|
||||
def _ffmpeg_im_o(self, tpath: bytes, vn: VFS, cmd: list[bytes]) -> None:
|
||||
if tpath.endswith(b".jpg"):
|
||||
cmd += [
|
||||
b"-q:v",
|
||||
FF_JPG_Q[vn.flags["th_qv"] // 5], # default=??
|
||||
]
|
||||
elif tpath.endswith(".jxl"):
|
||||
elif tpath.endswith(b".jxl"):
|
||||
cmd += [
|
||||
b"-q:v",
|
||||
unicode(vn.flags["th_qvx"]).encode("ascii"), # default=??
|
||||
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:
|
||||
cmd += [
|
||||
@@ -817,15 +984,18 @@ class ThumbSrv(object):
|
||||
b"6", # default=4, 0=fast, 6=max
|
||||
]
|
||||
|
||||
cmd += [fsenc(tpath)]
|
||||
cmd.append(tpath)
|
||||
self._run_ff(cmd, vn, "convt")
|
||||
|
||||
def _run_ff(self, cmd: list[bytes], vn: VFS, kto: str, oom: int = 400) -> None:
|
||||
# self.log((b" ".join(cmd)).decode("utf-8"))
|
||||
ret, _, serr = runcmd(cmd, timeout=vn.flags[kto], nice=True, oom=oom)
|
||||
ret, _, serr = runcmd(cmd, cwd=SCWD, timeout=vn.flags[kto], nice=True, oom=oom)
|
||||
if not ret:
|
||||
return
|
||||
|
||||
if TH_BWRAP:
|
||||
bwrap_fail(serr)
|
||||
|
||||
c: Union[str, int] = "90"
|
||||
t = "FFmpeg failed (probably a corrupt file):\n"
|
||||
|
||||
@@ -834,6 +1004,22 @@ class ThumbSrv(object):
|
||||
ret = 321
|
||||
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 (
|
||||
(not self.args.th_ff_jpg or time.time() - int(self.args.th_ff_jpg) < 60)
|
||||
and cmd[-1].lower().endswith(b".webp")
|
||||
@@ -901,20 +1087,21 @@ class ThumbSrv(object):
|
||||
b",showwavespic=s=2048x64:colors=white"
|
||||
b",convolution=1 1 1 1 1 1 1 1 1:1 1 1 1 1 1 1 1 1:1 1 1 1 1 1 1 1 1:1 -1 1 -1 5 -1 1 -1 1" # idk what im doing but it looks ok
|
||||
)
|
||||
bap_in = fsenc(abspath)
|
||||
bap_out = fsenc(tpath)
|
||||
|
||||
# fmt: off
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
b"-i", fsenc(abspath),
|
||||
b"-i", bap_in,
|
||||
b"-filter_complex", flt,
|
||||
b"-frames:v", b"1",
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
cmd += [fsenc(tpath)]
|
||||
cmd.append(bap_out)
|
||||
self._run_ff(cmd, vn, "convt")
|
||||
|
||||
if "pngquant" in vn.flags:
|
||||
@@ -981,28 +1168,32 @@ class ThumbSrv(object):
|
||||
except:
|
||||
self.untemp[tpath] = [infile]
|
||||
|
||||
bap_in = fsenc(abspath)
|
||||
bap_out = fsenc(infile)
|
||||
|
||||
# fmt: off
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
b"-i", fsenc(abspath),
|
||||
b"-i", bap_in,
|
||||
b"-map", b"0:a:0",
|
||||
b"-ac", b"1",
|
||||
b"-ar", b"48000",
|
||||
b"-sample_fmt", b"s16",
|
||||
b"-t", b"900",
|
||||
b"-y", fsenc(infile),
|
||||
b"-y", bap_out,
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, "convt")
|
||||
|
||||
fscale = ":fscale=log" if self.args.th_spec_fl else ""
|
||||
|
||||
fc = "[0:a:0]aresample=48000{},showspectrumpic=s="
|
||||
if "3" in fmt:
|
||||
fc += "1280x1024,crop=1420:1056:70:48[o]"
|
||||
fc += "1280x1024%s,crop=1420:1056:70:48[o]" % fscale
|
||||
else:
|
||||
fc += "640x512,crop=780:544:70:48[o]"
|
||||
fc += "640x512%s,crop=780:544:70:48[o]" % fscale
|
||||
|
||||
if self.args.th_ff_swr:
|
||||
fco = ":filter_size=128:cutoff=0.877"
|
||||
@@ -1011,20 +1202,22 @@ class ThumbSrv(object):
|
||||
|
||||
fc = fc.format(fco)
|
||||
|
||||
bap_in = fsenc(infile)
|
||||
bap_out = fsenc(tpath)
|
||||
|
||||
# fmt: off
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
b"-i", fsenc(infile),
|
||||
b"-i", bap_in,
|
||||
b"-filter_complex", fc.encode("utf-8"),
|
||||
b"-map", b"[o]",
|
||||
b"-frames:v", b"1",
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
self._ffmpeg_im_o(tpath, vn, cmd)
|
||||
self._ffmpeg_im_o(bap_out, vn, cmd)
|
||||
|
||||
def conv_mp3(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
quality = self.args.q_mp3.lower()
|
||||
@@ -1043,24 +1236,26 @@ class ThumbSrv(object):
|
||||
qk = b"-q:a"
|
||||
qv = quality[1:].encode("ascii")
|
||||
|
||||
bap_in = fsenc(abspath)
|
||||
bap_out = fsenc(tpath)
|
||||
|
||||
# extremely conservative choices for output format
|
||||
# (always 2ch 44k1) because if a device is old enough
|
||||
# to not support opus then it's probably also super picky
|
||||
|
||||
# fmt: off
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
b"-i", fsenc(abspath),
|
||||
b"-i", bap_in,
|
||||
] + self.big_tags(rawtags) + [
|
||||
b"-map", b"0:a:0",
|
||||
b"-ar", b"44100",
|
||||
b"-ac", b"2",
|
||||
b"-c:a", b"libmp3lame",
|
||||
qk, qv,
|
||||
fsenc(tpath)
|
||||
bap_out,
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
||||
@@ -1076,16 +1271,18 @@ class ThumbSrv(object):
|
||||
|
||||
self.log("conv2 flac", 6)
|
||||
|
||||
bap_in = fsenc(abspath)
|
||||
bap_out = fsenc(tpath)
|
||||
|
||||
# fmt: off
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
b"-i", fsenc(abspath),
|
||||
b"-i", bap_in,
|
||||
b"-map", b"0:a:0",
|
||||
b"-c:a", b"flac",
|
||||
fsenc(tpath)
|
||||
bap_out,
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
||||
@@ -1111,16 +1308,18 @@ class ThumbSrv(object):
|
||||
|
||||
self.log("conv2 wav", 6)
|
||||
|
||||
bap_in = fsenc(abspath)
|
||||
bap_out = fsenc(tpath)
|
||||
|
||||
# fmt: off
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
b"-i", fsenc(abspath),
|
||||
b"-i", bap_in,
|
||||
b"-map", b"0:a:0",
|
||||
b"-c:a", codec,
|
||||
fsenc(tpath)
|
||||
bap_out,
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
||||
@@ -1165,18 +1364,28 @@ class ThumbSrv(object):
|
||||
self.log("conv2 %s [%s]" % (container, enc), 6)
|
||||
benc = enc.encode("ascii").split(b" ")
|
||||
|
||||
ac = b"2"
|
||||
try:
|
||||
if tags["chs"][1] in ("mono", "1", "1.0"):
|
||||
ac = b"1"
|
||||
except:
|
||||
pass
|
||||
|
||||
bap_in = fsenc(abspath)
|
||||
bap_out = fsenc(tpath)
|
||||
|
||||
# fmt: off
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
b"-i", fsenc(abspath),
|
||||
b"-i", bap_in,
|
||||
] + tagset + [
|
||||
b"-map", b"0:a:0",
|
||||
b"-ac", ac,
|
||||
] + benc + [
|
||||
b"-f", container,
|
||||
fsenc(tpath)
|
||||
bap_out,
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
||||
@@ -1205,18 +1414,21 @@ class ThumbSrv(object):
|
||||
self.log("conv2 caf-tmp [%s]" % (enc,), 6)
|
||||
benc = enc.encode("ascii").split(b" ")
|
||||
|
||||
bap_in = fsenc(abspath)
|
||||
bap_out = fsenc(tmp_opus)
|
||||
|
||||
# fmt: off
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
b"-i", fsenc(abspath),
|
||||
b"-i", bap_in,
|
||||
b"-map_metadata", b"-1",
|
||||
b"-map", b"0:a:0",
|
||||
b"-ac", b"2",
|
||||
] + benc + [
|
||||
b"-f", b"opus",
|
||||
fsenc(tmp_opus)
|
||||
bap_out,
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
||||
@@ -1230,20 +1442,21 @@ class ThumbSrv(object):
|
||||
if dur < 20 or sz < 256 * 1024:
|
||||
zs = bq.decode("ascii")
|
||||
self.log("conv2 caf-transcode; dur=%d sz=%d q=%s" % (dur, sz, zs), 6)
|
||||
bap_in = fsenc(abspath)
|
||||
bap_out = fsenc(tpath)
|
||||
# fmt: off
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
b"-i", fsenc(abspath),
|
||||
b"-i", bap_in,
|
||||
b"-filter_complex", b"anoisesrc=a=0.001:d=7:c=pink,asplit[l][r]; [l][r]amerge[s]; [0:a:0][s]amix",
|
||||
b"-map_metadata", b"-1",
|
||||
b"-ac", b"2",
|
||||
b"-c:a", b"libopus",
|
||||
b"-b:a", bq,
|
||||
b"-f", b"caf",
|
||||
fsenc(tpath)
|
||||
bap_out,
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
||||
@@ -1251,18 +1464,19 @@ class ThumbSrv(object):
|
||||
else:
|
||||
# simple remux should be safe
|
||||
self.log("conv2 caf-remux; dur=%d sz=%d" % (dur, sz), 6)
|
||||
bap_in = fsenc(tmp_opus)
|
||||
bap_out = fsenc(tpath)
|
||||
# fmt: off
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
b"-i", fsenc(tmp_opus),
|
||||
b"-i", bap_in,
|
||||
b"-map_metadata", b"-1",
|
||||
b"-map", b"0:a:0",
|
||||
b"-c:a", b"copy",
|
||||
b"-f", b"caf",
|
||||
fsenc(tpath)
|
||||
bap_out,
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
||||
@@ -1283,7 +1497,7 @@ class ThumbSrv(object):
|
||||
return ret
|
||||
|
||||
def poke(self, tdir: str) -> None:
|
||||
if not self.poke_cd.poke(tdir):
|
||||
if not self.poke_cd or not self.poke_cd.poke(tdir):
|
||||
return
|
||||
|
||||
ts = int(time.time())
|
||||
|
||||
+25
-4
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import calendar
|
||||
import os
|
||||
@@ -22,11 +22,20 @@ from .util import (
|
||||
quotep,
|
||||
s3dec,
|
||||
vjoin,
|
||||
vjoins,
|
||||
)
|
||||
|
||||
if HAVE_SQLITE3:
|
||||
import sqlite3
|
||||
|
||||
try:
|
||||
if os.environ.get("PRTY_NO_UNIDATA"):
|
||||
raise Exception()
|
||||
|
||||
import unicodedata
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
from pathlib import Path
|
||||
except:
|
||||
@@ -42,6 +51,14 @@ if PY2:
|
||||
range = xrange # type: ignore
|
||||
|
||||
|
||||
def norm_icase(txt: str) -> str:
|
||||
return txt.casefold() if txt else txt
|
||||
|
||||
|
||||
def norm_nfkc(txt: str) -> str:
|
||||
return unicodedata.normalize("NFKC", txt).casefold() if txt else txt
|
||||
|
||||
|
||||
class U2idx(object):
|
||||
def __init__(self, hsrv: "HttpSrv") -> None:
|
||||
self.log_func = hsrv.log
|
||||
@@ -54,6 +71,10 @@ class U2idx(object):
|
||||
return
|
||||
|
||||
if self.args.srch_icase:
|
||||
if self.args.srch_nfkc:
|
||||
self.normfun = norm_nfkc
|
||||
else:
|
||||
self.normfun = norm_icase
|
||||
self._open_db = self._open_db_icase
|
||||
else:
|
||||
self._open_db = self._open_db_std
|
||||
@@ -81,7 +102,7 @@ class U2idx(object):
|
||||
|
||||
def _open_db_icase(self, *args, **kwargs):
|
||||
db = self._open_db_std(*args, **kwargs)
|
||||
db.create_function("casefold", 1, lambda x: x.casefold() if x else x)
|
||||
db.create_function("casefold", 1, self.normfun)
|
||||
return db
|
||||
|
||||
def shutdown(self) -> None:
|
||||
@@ -322,7 +343,7 @@ class U2idx(object):
|
||||
|
||||
if icase and "casefold(" in q:
|
||||
try:
|
||||
v = unicode(v).casefold()
|
||||
v = self.normfun(unicode(v))
|
||||
except:
|
||||
v = unicode(v).lower()
|
||||
|
||||
@@ -437,7 +458,7 @@ class U2idx(object):
|
||||
if rd.startswith("//") or fn.startswith("//"):
|
||||
rd, fn = s3dec(rd, fn)
|
||||
|
||||
vp = vjoin(vjoin(vtop, rd), fn)
|
||||
vp = vjoins(vtop, rd, fn)
|
||||
|
||||
if vp in seen_rps:
|
||||
continue
|
||||
|
||||
+328
-116
@@ -1,15 +1,12 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import errno
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess as sp
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
@@ -19,11 +16,11 @@ from copy import deepcopy
|
||||
|
||||
from queue import Queue
|
||||
|
||||
from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, WINDOWS, E
|
||||
from .authsrv import LEELOO_DALLAS, SEESLOG, VFS, AuthSrv
|
||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, UNIX, WINDOWS, E
|
||||
from .authsrv import LEELOO_DALLAS, REDUP_E2, SEESLOG, VFS, AuthSrv
|
||||
from .bos import bos
|
||||
from .cfg import vf_bmap, vf_cmap, vf_vmap
|
||||
from .fsutil import Fstab
|
||||
from .fsutil import Fstab, filefrag
|
||||
from .mtag import MParser, MTag
|
||||
from .util import (
|
||||
E_FS_CRIT,
|
||||
@@ -55,6 +52,7 @@ from .util import (
|
||||
ren_open,
|
||||
rmdirs,
|
||||
rmdirs_up,
|
||||
runcmd,
|
||||
runhook,
|
||||
runihook,
|
||||
s2hms,
|
||||
@@ -70,9 +68,11 @@ from .util import (
|
||||
ub64enc,
|
||||
unhumanize,
|
||||
vjoin,
|
||||
vjoins,
|
||||
vsplit,
|
||||
w8b64dec,
|
||||
w8b64enc,
|
||||
winsparse,
|
||||
wunlink,
|
||||
)
|
||||
|
||||
@@ -102,7 +102,7 @@ ICV_EXTS = set(zsg.split(","))
|
||||
zsg = "3gp,asf,av1,avc,avi,flv,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,vob,webm,wmv"
|
||||
VCV_EXTS = set(zsg.split(","))
|
||||
|
||||
zsg = "aif,aiff,alac,ape,flac,m4a,m4b,m4r,mp3,oga,ogg,opus,tak,tta,wav,wma,wv,cbz,epub"
|
||||
zsg = "aif,aiff,alac,ape,flac,m4a,m4b,m4r,mka,mp3,oga,ogg,opus,tak,tta,wav,wma,wv,cbz,epub"
|
||||
ACV_EXTS = set(zsg.split(","))
|
||||
|
||||
zsg = "nohash noidx xdev xvol"
|
||||
@@ -175,6 +175,7 @@ class Up2k(object):
|
||||
self.pp: Optional[ProgressPrinter] = None
|
||||
self.rescan_cond = threading.Condition()
|
||||
self.need_rescan: set[str] = set()
|
||||
self.have_e2d = False
|
||||
self.db_act = 0.0
|
||||
|
||||
self.reg_mutex = threading.Lock()
|
||||
@@ -254,6 +255,10 @@ class Up2k(object):
|
||||
with self.reload_mutex:
|
||||
if self.reload_flag < n:
|
||||
self.reload_flag = n
|
||||
if not self.have_e2d and not self.reloading:
|
||||
self.reloading = True
|
||||
Daemon(self._reload_thr)
|
||||
return
|
||||
with self.rescan_cond:
|
||||
self.rescan_cond.notify_all()
|
||||
|
||||
@@ -291,7 +296,7 @@ class Up2k(object):
|
||||
|
||||
def deferred_init(self) -> None:
|
||||
all_vols = self.asrv.vfs.all_vols
|
||||
have_e2d = self.init_indexes(all_vols, [], False)
|
||||
self.init_indexes(all_vols, [], False)
|
||||
|
||||
if self.stop:
|
||||
# up-mt consistency not guaranteed if init is interrupted;
|
||||
@@ -309,7 +314,7 @@ class Up2k(object):
|
||||
return
|
||||
|
||||
Daemon(self._snapshot, "up2k-snapshot")
|
||||
if have_e2d:
|
||||
if self.have_e2d:
|
||||
Daemon(self._hasher, "up2k-hasher")
|
||||
Daemon(self._sched_rescan, "up2k-rescan")
|
||||
if self.mtag:
|
||||
@@ -337,6 +342,10 @@ class Up2k(object):
|
||||
if not self.stop:
|
||||
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:
|
||||
mtpq: Union[int, str] = 0
|
||||
ups = []
|
||||
@@ -878,17 +887,14 @@ class Up2k(object):
|
||||
|
||||
def _expr_idx_filter(self, flags: dict[str, Any]) -> tuple[bool, dict[str, Any]]:
|
||||
if not self.no_expr_idx:
|
||||
return False, flags
|
||||
return False, dict(flags)
|
||||
|
||||
ret = {k: v for k, v in flags.items() if not k.startswith("e2t")}
|
||||
if len(ret) == len(flags):
|
||||
return False, flags
|
||||
|
||||
return True, ret
|
||||
return len(ret) != len(flags), flags
|
||||
|
||||
def init_indexes(
|
||||
self, all_vols: dict[str, VFS], scan_vols: list[str], fscan: bool, gid: int = 0
|
||||
) -> bool:
|
||||
) -> None:
|
||||
if not gid:
|
||||
with self.mutex:
|
||||
gid = self.gid
|
||||
@@ -903,7 +909,7 @@ class Up2k(object):
|
||||
|
||||
with self.mutex:
|
||||
if gid != self.gid:
|
||||
return False
|
||||
return
|
||||
|
||||
if self.pp:
|
||||
continue
|
||||
@@ -942,6 +948,9 @@ class Up2k(object):
|
||||
with self.mutex, self.reg_mutex:
|
||||
# only need to protect register_vpath but all in one go feels right
|
||||
for vol in vols:
|
||||
if vol.realpath:
|
||||
self.flags[vol.realpath] = dict(vol.flags)
|
||||
|
||||
if bos.path.isfile(vol.realpath):
|
||||
self.volstate[vol.vpath] = "online (just-a-file)"
|
||||
t = "NOTE: volume [/%s] is a file, not a folder"
|
||||
@@ -991,6 +1000,21 @@ class Up2k(object):
|
||||
with self.mutex, self.reg_mutex:
|
||||
self._drop_caches()
|
||||
|
||||
def setstate(vol, n):
|
||||
t = ""
|
||||
if n < 1 and "e2v" in vol.flags:
|
||||
t += "+e2v"
|
||||
if n < 2 and vol.flags.get("redup"):
|
||||
t += "+redup"
|
||||
if n < 3 and "e2ts" in vol.flags:
|
||||
t += "+tags"
|
||||
|
||||
if t:
|
||||
t = "online (%s pending)" % (t[1:],)
|
||||
else:
|
||||
t = "online, idle"
|
||||
self.volstate[vol.vpath] = t
|
||||
|
||||
for vol in vols:
|
||||
if self.stop or gid != self.gid:
|
||||
break
|
||||
@@ -1007,26 +1031,16 @@ class Up2k(object):
|
||||
if vac:
|
||||
need_vac[vol] = True
|
||||
|
||||
if "e2v" in vol.flags:
|
||||
t = "online (integrity-check pending)"
|
||||
elif "e2ts" in vol.flags:
|
||||
t = "online (tags pending)"
|
||||
else:
|
||||
t = "online, idle"
|
||||
|
||||
self.volstate[vol.vpath] = t
|
||||
setstate(vol, 0)
|
||||
|
||||
self._unblock()
|
||||
|
||||
# file contents verification
|
||||
for vol in vols:
|
||||
if self.stop:
|
||||
break
|
||||
|
||||
if "e2v" not in vol.flags:
|
||||
if self.stop or "e2v" not in vol.flags:
|
||||
continue
|
||||
|
||||
t = "online (verifying integrity)"
|
||||
t = "online (doing e2v)"
|
||||
self.volstate[vol.vpath] = t
|
||||
self.log("{} [{}]".format(t, vol.realpath))
|
||||
|
||||
@@ -1035,12 +1049,23 @@ class Up2k(object):
|
||||
self.log("modified {} entries in the db".format(nmod), 3)
|
||||
need_vac[vol] = True
|
||||
|
||||
if "e2ts" in vol.flags:
|
||||
t = "online (tags pending)"
|
||||
else:
|
||||
t = "online, idle"
|
||||
setstate(vol, 1)
|
||||
|
||||
# convert dedup type
|
||||
for vol in vols:
|
||||
if self.stop or not vol.flags.get("redup"):
|
||||
continue
|
||||
|
||||
t = "online (doing redup)"
|
||||
self.volstate[vol.vpath] = t
|
||||
self.log("{} [{}]".format(t, vol.realpath))
|
||||
|
||||
try:
|
||||
with self.mutex, self.reg_mutex:
|
||||
self._redup(vol, vols)
|
||||
except:
|
||||
self.log("redup failed: " + min_ex(), 1)
|
||||
setstate(vol, 2)
|
||||
|
||||
# open the rest + do any e2ts(a)
|
||||
needed_mutagen = False
|
||||
@@ -1075,7 +1100,7 @@ class Up2k(object):
|
||||
cur.execute("vacuum")
|
||||
|
||||
if self.stop:
|
||||
return False
|
||||
return
|
||||
|
||||
for vol in all_vols.values():
|
||||
if vol.flags["dbd"] == "acid":
|
||||
@@ -1105,7 +1130,7 @@ class Up2k(object):
|
||||
self.log("checkpoint failed: {}".format(ex), 3)
|
||||
|
||||
if self.stop:
|
||||
return False
|
||||
return
|
||||
|
||||
self.pp.end = True
|
||||
|
||||
@@ -1125,7 +1150,7 @@ class Up2k(object):
|
||||
else:
|
||||
self.unpp()
|
||||
|
||||
return have_e2d
|
||||
self.have_e2d = self.have_e2d or have_e2d
|
||||
|
||||
def register_vpath(
|
||||
self, ptop: str, flags: dict[str, Any]
|
||||
@@ -1149,12 +1174,12 @@ class Up2k(object):
|
||||
vpath = k
|
||||
|
||||
_, flags = self._expr_idx_filter(flags)
|
||||
n4g = bool(flags.get("noforget"))
|
||||
n4g = "noforget" in flags
|
||||
|
||||
ft = "\033[0;32m{}{:.0}"
|
||||
ff = "\033[0;35m{}{:.0}"
|
||||
fv = "\033[0;36m{}:\033[90m{}"
|
||||
zs = "bcasechk du_iwho emb_all emb_lgs emb_mds ext_th_d html_head html_head_d html_head_s ls_q_m put_name2 mv_re_r mv_re_t rm_re_r rm_re_t oh_f oh_g rw_edit_set srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
|
||||
zs = "bcasechk du_iwho emb_all emb_lgs emb_mds ext_th_d html_head html_head_d html_head_s ls_q_m put_name2 mv_re_r mv_re_t rm_re_r rm_re_t oh_f oh_g rw_edit_set srch_re_dots srch_re_nodot th_coversd th_coversl th_covers_set th_coversd_set xvd zipmax zipmaxn_v zipmaxs_v"
|
||||
fx = set(zs.split())
|
||||
fd = vf_bmap()
|
||||
fd.update(vf_cmap())
|
||||
@@ -1288,6 +1313,17 @@ class Up2k(object):
|
||||
self.log(t % (vpath, ex), 1)
|
||||
return None
|
||||
|
||||
if not (
|
||||
ANYWIN or UNIX or "hist_cow" in flags or "up2k.db" in os.listdir(histpath)
|
||||
):
|
||||
try:
|
||||
if self.fstab.get(histpath)[0] != "btrfs":
|
||||
raise Exception()
|
||||
runcmd([b"chattr", b"-c", fsenc(histpath)], 1) # compression and...
|
||||
runcmd([b"chattr", b"+C", fsenc(histpath)], 1) # no-cow are incompat
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
cur = self._open_db_wd(db_path)
|
||||
|
||||
@@ -1371,14 +1407,15 @@ class Up2k(object):
|
||||
top = vol.realpath
|
||||
rei = vol.flags.get("noidx")
|
||||
reh = vol.flags.get("nohash")
|
||||
n4g = bool(vol.flags.get("noforget"))
|
||||
n4g = "noforget" in vol.flags
|
||||
ffat = "fat32" in vol.flags
|
||||
cst = bos.stat(top)
|
||||
dev = cst.st_dev if vol.flags.get("xdev") else 0
|
||||
dev = cst.st_dev if "xdev" in vol.flags else 0
|
||||
|
||||
with self.mutex:
|
||||
with self.reg_mutex:
|
||||
reg = self.register_vpath(top, vol.flags)
|
||||
self.flags[vol.realpath] = dict(vol.flags)
|
||||
|
||||
assert reg and self.pp # !rm
|
||||
cur, db_path = reg
|
||||
@@ -1426,12 +1463,14 @@ class Up2k(object):
|
||||
rtop,
|
||||
rei,
|
||||
reh,
|
||||
vol.flags["th_coversd"],
|
||||
vol.flags["th_coversd_set"],
|
||||
n4g,
|
||||
ffat,
|
||||
[],
|
||||
cst,
|
||||
dev,
|
||||
bool(vol.flags.get("xvol")),
|
||||
"xvol" in vol.flags,
|
||||
)
|
||||
if not n4g:
|
||||
n_rm = self._drop_lost(db.c, top, excl)
|
||||
@@ -1503,6 +1542,8 @@ class Up2k(object):
|
||||
rcdir: str,
|
||||
rei: Optional[Pattern[str]],
|
||||
reh: Optional[Pattern[str]],
|
||||
th_cvd: list[str],
|
||||
th_cvds: set[str],
|
||||
n4g: bool,
|
||||
ffat: bool,
|
||||
seen: list[str],
|
||||
@@ -1528,8 +1569,6 @@ class Up2k(object):
|
||||
cv = vcv = acv = ""
|
||||
e_d = {}
|
||||
|
||||
th_cvd = self.args.th_coversd
|
||||
th_cvds = self.args.th_coversd_set
|
||||
scan_pr_s = self.args.scan_pr_s
|
||||
|
||||
assert self.pp and self.mem_cur # !rm
|
||||
@@ -1596,6 +1635,8 @@ class Up2k(object):
|
||||
rap,
|
||||
rei,
|
||||
reh,
|
||||
th_cvd,
|
||||
th_cvds,
|
||||
n4g,
|
||||
fat32,
|
||||
seen,
|
||||
@@ -1626,7 +1667,7 @@ class Up2k(object):
|
||||
|
||||
rsz += sz
|
||||
files.append((sz, lmod, iname))
|
||||
if sz:
|
||||
if sz and th_cvd:
|
||||
liname = iname.lower()
|
||||
ext = liname.rsplit(".", 1)[-1]
|
||||
if (
|
||||
@@ -2067,6 +2108,138 @@ class Up2k(object):
|
||||
|
||||
return len(rewark) + len(f404)
|
||||
|
||||
def _redup(self, vol: VFS, vols: list[VFS]) -> None:
|
||||
logmsg = "redup: replacing %r with %r"
|
||||
dry = "redup_dry" in vol.flags
|
||||
if dry:
|
||||
logmsg += " #dry"
|
||||
|
||||
zs, to = vol.flags["redup"].split("=")
|
||||
zsl = zs.split(",")
|
||||
cref = "no" in zsl or "ref" in zsl
|
||||
csym = "sym" in zsl
|
||||
chard = "hard" in zsl
|
||||
chard0 = chard
|
||||
|
||||
vf = vol.flags.copy()
|
||||
vf["dedup"] = 1
|
||||
for zs in "hardlink hardlinkonly reflink".split():
|
||||
vf.pop(zs, None)
|
||||
|
||||
if to == "ref":
|
||||
vf["reflink"] = 1
|
||||
elif to == "hard":
|
||||
vf["hardlinkonly"] = vf["hardlink"] = 1
|
||||
elif to == "sym":
|
||||
pass
|
||||
else:
|
||||
raise Exception("target type not implemented: " + to)
|
||||
|
||||
if (csym or chard) and to != "sym":
|
||||
self.log("redup: stage 1 begin")
|
||||
if to == "hard":
|
||||
chard = False
|
||||
for x in vol.walk("", "", [], LEELOO_DALLAS, [[]], 2, True, True, True):
|
||||
vn, _, _, atop, lsf, lsd, lsv = x
|
||||
if vn is not vol:
|
||||
lsd[:] = []
|
||||
lsv.clear()
|
||||
continue
|
||||
for fn, st in lsf:
|
||||
if (csym and stat.S_ISLNK(st.st_mode)) or (
|
||||
chard and st.st_nlink > 1
|
||||
):
|
||||
ap = os.path.join(atop, fn)
|
||||
self.log(logmsg % (ap, absreal(ap)))
|
||||
if dry:
|
||||
continue
|
||||
ap2 = tempfile.NamedTemporaryFile(
|
||||
prefix="r,", dir=atop, delete=False
|
||||
).name
|
||||
self._symlink(ap, ap2, vf, True, True, st.st_mtime)
|
||||
st1 = bos.stat(ap)
|
||||
st2 = bos.lstat(ap2)
|
||||
if st1.st_size != st2.st_size or stat.S_ISLNK(st2.st_mode):
|
||||
wunlink(self.log, ap2, vf)
|
||||
raise Exception("redup: clone failed; giving up")
|
||||
wunlink(self.log, ap, vf)
|
||||
bos.rename(ap2, ap)
|
||||
|
||||
if cref or to == "sym":
|
||||
self.log("redup: stage 2 begin")
|
||||
if "e2ds" not in vol.flags:
|
||||
self.log(REDUP_E2 % (vol.vpath,), 3)
|
||||
cur = self.cur[vol.realpath]
|
||||
curs = [(vol, cur.connection.cursor())]
|
||||
if to != "sym":
|
||||
for v2 in vols:
|
||||
if vol is not v2 and v2.realpath in self.cur:
|
||||
curs += [(v2, self.cur[v2.realpath])]
|
||||
nrem = cur.execute("select count(w) from up").fetchone()[0]
|
||||
self.log("redup [/%s]: %d files left" % (vol.vpath, nrem))
|
||||
q2 = "select rd, fn from up where substr(w,1,16) = ? and w=?"
|
||||
for w, rd, fn in cur.execute("select w, rd, fn from up"):
|
||||
w16 = w[:16]
|
||||
for v2, c2 in curs:
|
||||
hits = c2.execute(q2, (w16, w)).fetchall()
|
||||
if not hits:
|
||||
continue
|
||||
rd2, fn2 = hits[0]
|
||||
if fn == fn2 and rd == rd2 and vol is v2:
|
||||
continue
|
||||
apt = ""
|
||||
try:
|
||||
rd, fn = s3dec(rd, fn)
|
||||
rd2, fn2 = s3dec(rd2, fn2)
|
||||
fp1 = os.path.join(vol.realpath, rd, fn)
|
||||
fp2 = os.path.join(v2.realpath, rd2, fn2)
|
||||
st1 = bos.lstat(fp1)
|
||||
if fp1 == fp2 or stat.S_ISLNK(st1.st_mode):
|
||||
continue # self(?), or redup failed during walk
|
||||
if to == "hard" and st1.st_nlink >= len(hits):
|
||||
continue
|
||||
ap1 = absreal(fp1)
|
||||
ap2 = absreal(fp2)
|
||||
st2 = bos.lstat(ap2)
|
||||
if stat.S_ISLNK(st2.st_mode):
|
||||
continue # dead hit
|
||||
if to == "ref" and ap1 != ap2:
|
||||
try:
|
||||
frag1 = filefrag(ap1)
|
||||
if frag1:
|
||||
frag2 = filefrag(ap2)
|
||||
if frag1 == frag2:
|
||||
continue
|
||||
except Exception as ex:
|
||||
self.log("redup: filefrag failed (OK); %r" % (ex,))
|
||||
for ap in (ap1,) if ap1 == ap2 else (ap1, ap2):
|
||||
self.log("redup: integrity-checking %r" % (ap,))
|
||||
zsl, st = self._hashlist_from_file(ap)
|
||||
w2 = up2k_wark_from_hashlist(self.salt, st.st_size, zsl)
|
||||
if w2 != w:
|
||||
t = "db desync:\n%s %r\n%s db"
|
||||
raise Exception(t % (w2, ap, w))
|
||||
|
||||
self.log(logmsg % (ap1, ap2))
|
||||
if dry:
|
||||
continue
|
||||
apt = tempfile.NamedTemporaryFile(
|
||||
prefix="r,", dir=os.path.dirname(fp1), delete=False
|
||||
).name
|
||||
self._symlink(ap2, apt, vf, True, True, st1.st_mtime)
|
||||
zsl, st = self._hashlist_from_file(apt)
|
||||
w2 = up2k_wark_from_hashlist(self.salt, st.st_size, zsl)
|
||||
if w != w2:
|
||||
raise Exception("bad checksum after copy?!")
|
||||
wunlink(self.log, ap1, vf)
|
||||
bos.rename(apt, ap1)
|
||||
break
|
||||
except Exception as ex:
|
||||
self.log("redup: skipping match due to " + str(ex))
|
||||
if apt and os.path.exists(apt):
|
||||
os.unlink(apt)
|
||||
continue
|
||||
|
||||
def _build_tags_index(self, vol: VFS) -> tuple[int, int, bool]:
|
||||
ptop = vol.realpath
|
||||
with self.mutex, self.reg_mutex:
|
||||
@@ -2726,6 +2899,17 @@ class Up2k(object):
|
||||
t = "-" * 72
|
||||
raise Exception("%s\n%s\n%s" % (t, txt, t))
|
||||
|
||||
def _sql_ok(self, cur: "sqlite3.Cursor", q: str) -> bool:
|
||||
try:
|
||||
cur.execute(q).fetchone()
|
||||
return True
|
||||
except Exception as ex:
|
||||
if "no such table" not in str(ex):
|
||||
t = "WARNING: database issues, maybe due to server filesystem or hardware (check %sOS-logs):\n %r -- \033[31m%r\n"
|
||||
t2 = "" if ANYWIN or UNIX else "dmesg / "
|
||||
self.log(t % (t2, q, ex), 3)
|
||||
return False
|
||||
|
||||
def _orz(self, db_path: str) -> "sqlite3.Cursor":
|
||||
assert sqlite3 # type: ignore # !rm
|
||||
c = sqlite3.connect(
|
||||
@@ -2899,11 +3083,8 @@ class Up2k(object):
|
||||
|
||||
def _add_dhash_tab(self, cur: "sqlite3.Cursor") -> None:
|
||||
# v5 -> v5a
|
||||
try:
|
||||
cur.execute("select d, h from dh limit 1").fetchone()
|
||||
if self._sql_ok(cur, "select d, h from dh limit 1"):
|
||||
return
|
||||
except:
|
||||
pass
|
||||
|
||||
for cmd in [
|
||||
r"create table dh (d text, h text)",
|
||||
@@ -2917,11 +3098,8 @@ class Up2k(object):
|
||||
def _add_xiu_tab(self, cur: "sqlite3.Cursor") -> None:
|
||||
# v5a -> v5b
|
||||
# store rd+fn rather than warks to support nohash vols
|
||||
try:
|
||||
cur.execute("select c, w, rd, fn from iu limit 1").fetchone()
|
||||
if self._sql_ok(cur, "select c, w, rd, fn from iu limit 1"):
|
||||
return
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
cur.execute("drop table iu")
|
||||
@@ -2939,11 +3117,8 @@ class Up2k(object):
|
||||
|
||||
def _add_cv_tab(self, cur: "sqlite3.Cursor") -> None:
|
||||
# v5b -> v5c
|
||||
try:
|
||||
cur.execute("select rd, dn, fn from cv limit 1").fetchone()
|
||||
if self._sql_ok(cur, "select rd, dn, fn from cv limit 1"):
|
||||
return
|
||||
except:
|
||||
pass
|
||||
|
||||
for cmd in [
|
||||
r"create table cv (rd text, dn text, fn text)",
|
||||
@@ -2978,11 +3153,8 @@ class Up2k(object):
|
||||
|
||||
def _add_ds_tab(self, cur: "sqlite3.Cursor") -> None:
|
||||
# v5d -> v5e
|
||||
try:
|
||||
cur.execute("select rd, sz from ds limit 1").fetchone()
|
||||
if self._sql_ok(cur, "select rd, sz from ds limit 1"):
|
||||
return
|
||||
except:
|
||||
pass
|
||||
|
||||
for cmd in [
|
||||
r"create table ds (rd text, sz int, nf int)",
|
||||
@@ -3033,7 +3205,7 @@ class Up2k(object):
|
||||
raise Pebkac(500, "too many xbu relocs, giving up")
|
||||
|
||||
ptop = cj["ptop"]
|
||||
if not self.register_vpath(ptop, cj["vcfg"]):
|
||||
if not self.register_vpath(ptop, cj.pop("vcfg")):
|
||||
if ptop not in self.registry:
|
||||
raise Pebkac(410, "location unavailable")
|
||||
|
||||
@@ -3055,17 +3227,20 @@ class Up2k(object):
|
||||
jcur = self.cur.get(ptop)
|
||||
reg = self.registry[ptop]
|
||||
vfs = self.vfs.all_vols[cj["vtop"]]
|
||||
n4g = bool(vfs.flags.get("noforget"))
|
||||
noclone = bool(vfs.flags.get("noclone"))
|
||||
rand = vfs.flags.get("rand") or cj.get("rand")
|
||||
n4g = "noforget" in vfs.flags
|
||||
noclone = "noclone" in vfs.flags
|
||||
rand = "rand" in vfs.flags or cj.get("rand")
|
||||
lost: list[tuple["sqlite3.Cursor", str, str]] = []
|
||||
|
||||
safe_dedup = vfs.flags.get("safededup") or 50
|
||||
data_ok = safe_dedup < 10 or n4g
|
||||
|
||||
vols = [(ptop, jcur)] if jcur else []
|
||||
if vfs.flags.get("xlink"):
|
||||
if vols and ("xlink" in vfs.flags or "reflink" in vfs.flags):
|
||||
vols += [(k, v) for k, v in self.cur.items() if k != ptop]
|
||||
if "xlink" not in vfs.flags:
|
||||
zs = self.fstab.get(ptop)[1]
|
||||
vols = [x for x in vols if self.fstab.get(x[0])[1] == zs]
|
||||
|
||||
if noclone:
|
||||
wark = up2k_wark_from_metadata(
|
||||
@@ -3073,7 +3248,7 @@ class Up2k(object):
|
||||
)
|
||||
|
||||
zi = cj["lmod"]
|
||||
bad_mt = zi <= 0 or zi > 0xAAAAAAAA
|
||||
bad_mt = zi <= 0 or zi > (2 << 36)
|
||||
if bad_mt or vfs.flags.get("up_ts", "") == "fu":
|
||||
# force upload time rather than last-modified
|
||||
cj["lmod"] = int(time.time())
|
||||
@@ -3186,7 +3361,7 @@ class Up2k(object):
|
||||
c2 = None
|
||||
for cur, dp_dir, dp_fn in lost:
|
||||
t = "forgetting desynced db entry: %r"
|
||||
self.log(t % ("/" + vjoin(vjoin(vfs.vpath, dp_dir), dp_fn)))
|
||||
self.log(t % ("/" + vjoins(vfs.vpath, dp_dir, dp_fn)))
|
||||
self.db_rm(cur, vfs.flags, dp_dir, dp_fn, cj["size"])
|
||||
if c2 and c2 != cur:
|
||||
c2.connection.commit()
|
||||
@@ -3276,7 +3451,10 @@ class Up2k(object):
|
||||
vfs.lim.nup(cj["addr"])
|
||||
vfs.lim.bup(cj["addr"], cj["size"])
|
||||
|
||||
if "done" not in job:
|
||||
if "rvp0" in job and wark in reg:
|
||||
# xbu reloc; accept wrong path
|
||||
job["addr"] = cj["addr"]
|
||||
elif "done" not in job:
|
||||
self.log("unfinished:\n %r\n %r" % (src, dst))
|
||||
err = "partial upload exists at a different location; please resume uploading here instead:\n"
|
||||
err += "/" + quotep(vsrc) + " "
|
||||
@@ -3353,7 +3531,7 @@ class Up2k(object):
|
||||
job["size"],
|
||||
job["addr"],
|
||||
job["at"],
|
||||
None,
|
||||
[dwark],
|
||||
)
|
||||
t = hr.get("rejectmsg") or ""
|
||||
if t or hr.get("rc") != 0:
|
||||
@@ -3366,18 +3544,29 @@ class Up2k(object):
|
||||
x = pathmod(self.vfs, dst, vp, hr["reloc"])
|
||||
if x:
|
||||
ud1 = (vfs.vpath, job["prel"], job["name"])
|
||||
job["rvp0"] = vjoins(*ud1)
|
||||
pdir, _, job["name"], (vfs, rem) = x
|
||||
dst = os.path.join(pdir, job["name"])
|
||||
job["vcfg"] = vfs.flags
|
||||
job["ptop"] = vfs.realpath
|
||||
job["vtop"] = vfs.vpath
|
||||
job["prel"] = rem
|
||||
job["name"] = sanitize_fn(job["name"])
|
||||
job["name"] = vfs.sanitize_fn(job["name"], rem)
|
||||
ud2 = (vfs.vpath, job["prel"], job["name"])
|
||||
if ud1 != ud2:
|
||||
# print(json.dumps(job, sort_keys=True, indent=4))
|
||||
job["vcfg"] = vfs.flags
|
||||
job["hash"] = cj["hash"]
|
||||
self.log("xbu reloc1:%d..." % (depth,), 6)
|
||||
t = "xbu reloc1=%d ptop=%r vtop=%r prel=%r name=%r"
|
||||
t = t % (
|
||||
depth,
|
||||
job["ptop"],
|
||||
job["vtop"],
|
||||
job["prel"],
|
||||
job["name"],
|
||||
)
|
||||
self.log(t, 6)
|
||||
zs = djoin(job["ptop"], job["prel"])
|
||||
bos.makedirs(zs, vf=vfs.flags)
|
||||
return self._handle_json(job, depth + 1)
|
||||
|
||||
job["name"] = self._untaken(pdir, job, now)
|
||||
@@ -3541,7 +3730,7 @@ class Up2k(object):
|
||||
st = bos.stat(fp)
|
||||
try:
|
||||
vrel = vjoin(job["prel"], fname)
|
||||
xlink = bool(vf.get("xlink"))
|
||||
xlink = "xlink" in vf
|
||||
cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, vrel)
|
||||
self._forget_file(ptop, vrel, vf, cur, wark, True, st.st_size, xlink)
|
||||
except Exception as ex:
|
||||
@@ -3596,7 +3785,7 @@ class Up2k(object):
|
||||
if rm and bos.path.exists(dst):
|
||||
wunlink(self.log, dst, flags)
|
||||
|
||||
if not is_mv and not flags.get("dedup"):
|
||||
if not is_mv and "dedup" not in flags:
|
||||
raise Exception("dedup is disabled in config")
|
||||
|
||||
if "reflink" in flags:
|
||||
@@ -4065,7 +4254,7 @@ class Up2k(object):
|
||||
sz,
|
||||
ip,
|
||||
at or time.time(),
|
||||
None,
|
||||
[dwark],
|
||||
)
|
||||
t = hr.get("rejectmsg") or ""
|
||||
if t or hr.get("rc") != 0:
|
||||
@@ -4099,31 +4288,7 @@ class Up2k(object):
|
||||
with self.rescan_cond:
|
||||
self.rescan_cond.notify_all()
|
||||
|
||||
if rd and sz and fn.lower() in self.args.th_coversd_set:
|
||||
# wasteful; db_add will re-index actual covers
|
||||
# but that won't catch existing files
|
||||
crd, cdn = rd.rsplit("/", 1) if "/" in rd else ("", rd)
|
||||
try:
|
||||
q = "select fn from cv where rd=? and dn=?"
|
||||
db_cv = db.execute(q, (crd, cdn)).fetchone()[0]
|
||||
db_lcv = db_cv.lower()
|
||||
if db_lcv in self.args.th_coversd_set:
|
||||
idx_db = self.args.th_coversd.index(db_lcv)
|
||||
idx_fn = self.args.th_coversd.index(fn.lower())
|
||||
add_cv = idx_fn < idx_db
|
||||
else:
|
||||
add_cv = True
|
||||
except:
|
||||
add_cv = True
|
||||
|
||||
if add_cv:
|
||||
try:
|
||||
db.execute("delete from cv where rd=? and dn=?", (crd, cdn))
|
||||
db.execute("insert into cv values (?,?,?)", (crd, cdn, fn))
|
||||
except:
|
||||
pass
|
||||
|
||||
if "nodirsz" not in vflags:
|
||||
if vflags and "nodirsz" not in vflags:
|
||||
try:
|
||||
q = "update ds set nf=nf+1, sz=sz+? where rd=?"
|
||||
q2 = "insert into ds values(?,?,1)"
|
||||
@@ -4137,6 +4302,9 @@ class Up2k(object):
|
||||
pass
|
||||
|
||||
def handle_fs_abrt(self, akey: str) -> None:
|
||||
if akey == "ping":
|
||||
with self.mutex, self.reg_mutex:
|
||||
return
|
||||
self.abrt_key = akey
|
||||
|
||||
def handle_rm(
|
||||
@@ -4241,7 +4409,7 @@ class Up2k(object):
|
||||
is_dir = stat.S_ISDIR(st.st_mode)
|
||||
except:
|
||||
# NOTE: "file not found" *sftpd
|
||||
raise Pebkac(400, "file not found on disk (already deleted?)")
|
||||
raise Pebkac(404, "file not found on disk (already deleted?)")
|
||||
|
||||
if "bcasechk" in vn.flags and not vn.casechk(rem, False):
|
||||
raise Pebkac(400, "file does not exist case-sensitively")
|
||||
@@ -4318,7 +4486,7 @@ class Up2k(object):
|
||||
cur = None
|
||||
try:
|
||||
ptop = dbv.realpath
|
||||
xlink = bool(dbv.flags.get("xlink"))
|
||||
xlink = "xlink" in dbv.flags
|
||||
cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, volpath)
|
||||
self._forget_file(
|
||||
ptop, volpath, dbv.flags, cur, wark, True, st.st_size, xlink
|
||||
@@ -4522,6 +4690,12 @@ class Up2k(object):
|
||||
self.log("not found in src db: %r" % (svp,))
|
||||
|
||||
try:
|
||||
if svn_dbv != dvn and not (
|
||||
"xlink" in dvn.flags
|
||||
or "reflink" in dvn.flags
|
||||
or "hardlinkonly" in dvn.flags
|
||||
):
|
||||
raise OSError(errno.EXDEV, "different volumes")
|
||||
if is_link and st != stl:
|
||||
# relink non-broken symlinks to still work after the move,
|
||||
# but only resolve 1st level to maintain relativity
|
||||
@@ -4736,7 +4910,7 @@ class Up2k(object):
|
||||
fsize,
|
||||
ip,
|
||||
time.time(),
|
||||
None,
|
||||
[dabs],
|
||||
)
|
||||
t = hr.get("rejectmsg") or ""
|
||||
if t or hr.get("rc") != 0:
|
||||
@@ -4779,7 +4953,7 @@ class Up2k(object):
|
||||
fsize,
|
||||
ip,
|
||||
time.time(),
|
||||
None,
|
||||
[sabs],
|
||||
)
|
||||
|
||||
return "k"
|
||||
@@ -4799,7 +4973,7 @@ class Up2k(object):
|
||||
raise Pebkac(400, t)
|
||||
self._copy_tags(c1, c2, w)
|
||||
|
||||
xlink = bool(svn.flags.get("xlink"))
|
||||
xlink = "xlink" in svn.flags
|
||||
|
||||
with self.reg_mutex:
|
||||
has_dupes = self._forget_file(
|
||||
@@ -4909,7 +5083,7 @@ class Up2k(object):
|
||||
fsize,
|
||||
ip,
|
||||
time.time(),
|
||||
None,
|
||||
[sabs],
|
||||
)
|
||||
|
||||
return "k"
|
||||
@@ -5213,7 +5387,7 @@ class Up2k(object):
|
||||
if eno not in E_FS_CRIT:
|
||||
return self.log("hashing failed; %r @ %r\n%s" % (ex, ap, min_ex()), 3)
|
||||
t = "hashing failed; %r @ %r\n%s\nWARNING: This MAY indicate a serious issue with your harddisk or filesystem! Please investigate %sOS-logs\n"
|
||||
t2 = "" if ANYWIN or MACOS else "dmesg and "
|
||||
t2 = "" if ANYWIN or UNIX else "dmesg and "
|
||||
return self.log(t % (ex, ap, min_ex(), t2), 1)
|
||||
|
||||
def _new_upload(self, job: dict[str, Any], vfs: VFS, depth: int) -> dict[str, str]:
|
||||
@@ -5247,7 +5421,7 @@ class Up2k(object):
|
||||
job["size"],
|
||||
job["addr"],
|
||||
job["t0"],
|
||||
None,
|
||||
[job["dwrk"]],
|
||||
)
|
||||
t = hr.get("rejectmsg") or ""
|
||||
if t or hr.get("rc") != 0:
|
||||
@@ -5259,15 +5433,25 @@ class Up2k(object):
|
||||
x = pathmod(self.vfs, ap_chk, vp_chk, hr["reloc"])
|
||||
if x:
|
||||
ud1 = (vfs.vpath, job["prel"], job["name"])
|
||||
job["rvp0"] = vjoins(*ud1)
|
||||
pdir, _, job["name"], (vfs, rem) = x
|
||||
job["vcfg"] = vf = vfs.flags
|
||||
vf = vfs.flags
|
||||
job["ptop"] = vfs.realpath
|
||||
job["vtop"] = vfs.vpath
|
||||
job["prel"] = rem
|
||||
job["name"] = sanitize_fn(job["name"])
|
||||
job["name"] = vfs.sanitize_fn(job["name"], rem)
|
||||
ud2 = (vfs.vpath, job["prel"], job["name"])
|
||||
if ud1 != ud2:
|
||||
self.log("xbu reloc2:%d..." % (depth,), 6)
|
||||
job["vcfg"] = vf
|
||||
t = "xbu reloc2=%d ptop=%r vtop=%r prel=%r name=%r" % (
|
||||
depth,
|
||||
job["ptop"],
|
||||
job["vtop"],
|
||||
job["prel"],
|
||||
job["name"],
|
||||
)
|
||||
self.log(t, 6)
|
||||
bos.makedirs(djoin(job["ptop"], job["prel"]), vf=vf)
|
||||
return self._handle_json(job, depth + 1)
|
||||
|
||||
job["name"] = self._untaken(pdir, job, job["t0"])
|
||||
@@ -5307,9 +5491,12 @@ class Up2k(object):
|
||||
and self.args.sparse * 1024 * 1024 <= sz
|
||||
):
|
||||
try:
|
||||
sp.check_call(["fsutil", "sparse", "setflag", abspath])
|
||||
except:
|
||||
self.log("could not sparse %r" % (abspath,), 3)
|
||||
winsparse(f)
|
||||
# some kernels/etc are buggy; force sync:
|
||||
f.close()
|
||||
f = open(abspath, "rb+")
|
||||
except Exception as ex:
|
||||
self.log("could not sparse %r: %s" % (abspath, ex), 3)
|
||||
relabel = True
|
||||
sprs = False
|
||||
|
||||
@@ -5696,7 +5883,7 @@ def up2k_chunksize(filesize: int) -> int:
|
||||
stepsize = 512 * 1024
|
||||
while True:
|
||||
for mul in [1, 2]:
|
||||
nchunks = math.ceil(filesize * 1.0 / chunksize)
|
||||
nchunks = -int(-filesize // chunksize)
|
||||
if nchunks <= 256 or (chunksize >= 32 * 1024 * 1024 and nchunks <= 4096):
|
||||
return chunksize
|
||||
|
||||
@@ -5704,6 +5891,31 @@ def up2k_chunksize(filesize: int) -> int:
|
||||
stepsize *= mul
|
||||
|
||||
|
||||
def up2k_hashlist_from_file(path: str) -> tuple[list[str], os.stat_result]:
|
||||
"""not used by copyparty itself, only by some hooks"""
|
||||
st = bos.stat(path)
|
||||
fsz = st.st_size
|
||||
csz = up2k_chunksize(fsz)
|
||||
ret = []
|
||||
with open(fsenc(path), "rb", 256 * 1024) as f:
|
||||
while fsz > 0:
|
||||
hashobj = hashlib.sha512()
|
||||
rem = min(csz, fsz)
|
||||
fsz -= rem
|
||||
while rem > 0:
|
||||
buf = f.read(min(rem, 64 * 1024))
|
||||
if not buf:
|
||||
raise Exception("EOF at " + str(f.tell()))
|
||||
|
||||
hashobj.update(buf)
|
||||
rem -= len(buf)
|
||||
|
||||
digest = hashobj.digest()[:33]
|
||||
ret.append(ub64enc(digest).decode("ascii"))
|
||||
|
||||
return ret, st
|
||||
|
||||
|
||||
def up2k_wark_from_hashlist(salt: str, filesize: int, hashes: list[str]) -> str:
|
||||
"""server-reproducible file identifier, independent of name or location"""
|
||||
values = [salt, str(filesize)] + hashes
|
||||
|
||||
+340
-59
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
@@ -10,7 +10,6 @@ import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import mimetypes
|
||||
import os
|
||||
import platform
|
||||
@@ -62,6 +61,20 @@ def noop(*a, **ka):
|
||||
pass
|
||||
|
||||
|
||||
def lprint(*a: "Any", **ka: "Any") -> None:
|
||||
eol = ka.pop("end", "\n")
|
||||
txt = " ".join(unicode(x) for x in a) + eol
|
||||
lprinted.append(txt)
|
||||
if not VT100 and "\033" in txt:
|
||||
txt = RE_ANSI.sub("", txt)
|
||||
|
||||
print(txt, end="", **ka)
|
||||
|
||||
|
||||
lprinted: list[str] = []
|
||||
LOG: list["Callable[..., None]"] = [lprint]
|
||||
|
||||
|
||||
try:
|
||||
from datetime import datetime, timezone
|
||||
|
||||
@@ -166,12 +179,16 @@ try:
|
||||
raise ImportError()
|
||||
|
||||
from .stolen.ifaddr import get_adapters
|
||||
|
||||
IFADDR_VND = True
|
||||
except ImportError:
|
||||
IFADDR_VND = False
|
||||
from ifaddr import get_adapters
|
||||
|
||||
HAVE_IFADDR = True
|
||||
except:
|
||||
HAVE_IFADDR = False
|
||||
IFADDR_VND = False
|
||||
|
||||
def get_adapters(include_unconfigured=False):
|
||||
return []
|
||||
@@ -181,10 +198,10 @@ try:
|
||||
if os.environ.get("PRTY_NO_SQLITE"):
|
||||
raise Exception()
|
||||
|
||||
HAVE_SQLITE3 = True
|
||||
import sqlite3
|
||||
|
||||
assert hasattr(sqlite3, "connect") # graalpy
|
||||
HAVE_SQLITE3 = True
|
||||
except:
|
||||
HAVE_SQLITE3 = False
|
||||
|
||||
@@ -199,8 +216,9 @@ try:
|
||||
if os.environ.get("PRTY_NO_PSUTIL"):
|
||||
raise Exception()
|
||||
|
||||
HAVE_PSUTIL = True
|
||||
import psutil
|
||||
|
||||
HAVE_PSUTIL = True
|
||||
except:
|
||||
HAVE_PSUTIL = False
|
||||
|
||||
@@ -211,8 +229,10 @@ try:
|
||||
raise Exception()
|
||||
|
||||
import magic
|
||||
|
||||
HAVE_MAGIC = True
|
||||
except:
|
||||
pass
|
||||
HAVE_MAGIC = False
|
||||
|
||||
if os.environ.get("PRTY_MODSPEC"):
|
||||
from inspect import getsourcefile
|
||||
@@ -269,7 +289,6 @@ try:
|
||||
raise Exception()
|
||||
|
||||
socket.inet_pton(socket.AF_INET6, "::1")
|
||||
HAVE_IPV6 = True
|
||||
|
||||
if GRAAL:
|
||||
try:
|
||||
@@ -284,13 +303,15 @@ try:
|
||||
return _inet_pton(fam, ip)
|
||||
|
||||
socket.inet_pton = inet_pton
|
||||
|
||||
HAVE_IPV6 = True
|
||||
except:
|
||||
HAVE_IPV6 = False
|
||||
|
||||
def inet_pton(fam, ip):
|
||||
return socket.inet_aton(ip)
|
||||
|
||||
socket.inet_pton = inet_pton
|
||||
HAVE_IPV6 = False
|
||||
|
||||
|
||||
try:
|
||||
@@ -312,7 +333,14 @@ except:
|
||||
BITNESS = struct.calcsize("P") * 8
|
||||
|
||||
|
||||
CAN_SIGMASK = not (ANYWIN or PY2 or GRAAL)
|
||||
try:
|
||||
if ANYWIN or PY2 or GRAAL or not hasattr(signal, "pthread_sigmask"):
|
||||
raise Exception()
|
||||
BLOCK_SIGS = [signal.SIGINT, signal.SIGTERM, signal.SIGHUP, signal.SIGUSR1]
|
||||
CAN_SIGMASK = True
|
||||
except:
|
||||
BLOCK_SIGS = []
|
||||
CAN_SIGMASK = False
|
||||
|
||||
|
||||
RE_ANSI = re.compile("\033\\[[^mK]*[mK]")
|
||||
@@ -331,19 +359,25 @@ RE_MEMAVAIL = re.compile("^MemAvailable:.* kB")
|
||||
|
||||
if PY2:
|
||||
|
||||
def umktrans(s1, s2):
|
||||
def umktrans(s1: str, s2: str):
|
||||
return {ord(c1): ord(c2) for c1, c2 in zip(s1, s2)}
|
||||
|
||||
else:
|
||||
umktrans = str.maketrans
|
||||
|
||||
FNTL_WIN = umktrans('<>:|?*"\\/', "<>:|?*"\/")
|
||||
VPTL_WIN = umktrans('<>:|?*"\\', "<>:|?*"\")
|
||||
APTL_WIN = umktrans('<>:|?*"/', "<>:|?*"/")
|
||||
FNTL_MAC = VPTL_MAC = APTL_MAC = umktrans(":", ":")
|
||||
FNTL_OS = FNTL_WIN if ANYWIN else FNTL_MAC if MACOS else None
|
||||
VPTL_MAC = umktrans(":", ":")
|
||||
VPTL_OS = VPTL_WIN if ANYWIN else VPTL_MAC if MACOS else None
|
||||
APTL_OS = APTL_WIN if ANYWIN else APTL_MAC if MACOS else None
|
||||
|
||||
|
||||
BADNAMES = []
|
||||
if ANYWIN:
|
||||
BADNAMES += ["con", "prn", "aux", "nul"]
|
||||
BADNAMES += ["com%s" % (n,) for n in range(1, 10)]
|
||||
BADNAMES += ["lpt%s" % (n,) for n in range(1, 10)]
|
||||
BADNAMES += ["com%s" % (n,) for n in ("¹", "²", "³")]
|
||||
BADNAMES += ["lpt%s" % (n,) for n in ("¹", "²", "³")]
|
||||
BADNAMESET = set(BADNAMES)
|
||||
|
||||
|
||||
BOS_SEP = ("%s" % (os.sep,)).encode("ascii")
|
||||
@@ -408,18 +442,35 @@ IMPLICATIONS = [
|
||||
["e2vu", "e2v"],
|
||||
["e2vp", "e2v"],
|
||||
["e2v", "e2d"],
|
||||
["srch_nfkc", "srch_icase"],
|
||||
["hardlink_only", "hardlink"],
|
||||
["hardlink", "dedup"],
|
||||
["tftpvv", "tftpv"],
|
||||
["nodupem", "nodupe"],
|
||||
["no_dupe_m", "no_dupe"],
|
||||
["no_html", "no_script"],
|
||||
["nohtml", "noscript"],
|
||||
["no_html", "nohtml"], # user-typo
|
||||
["no_script", "noscript"], # user-typo
|
||||
["no_readmes", "no_readme"], # user-typo
|
||||
["noreadmes", "no_readme"], # user-typo
|
||||
["noreadme", "no_readme"], # user-typo
|
||||
["no_logue", "no_logues"], # user-typo
|
||||
["nologues", "no_logues"], # user-typo
|
||||
["nologue", "no_logues"], # user-typo
|
||||
["nw", "redup_dry"],
|
||||
["plainlogue", "plainlogues"], # user-typo
|
||||
["plainreadmes", "plainreadme"], # user-typo
|
||||
["noscript", "plainlogues"],
|
||||
["noscript", "plainreadme"],
|
||||
["sftpvv", "sftpv"],
|
||||
["smbw", "smb"],
|
||||
["smb1", "smb"],
|
||||
["smbvvv", "smbvv"],
|
||||
["smbvv", "smbv"],
|
||||
["smbv", "smb"],
|
||||
["xdev", "xvd"],
|
||||
["xvol", "xvd"],
|
||||
["zv", "zmv"],
|
||||
["zv", "zsv"],
|
||||
["z", "zm"],
|
||||
@@ -490,19 +541,18 @@ font woff woff2 otf ttf
|
||||
for v in vs.strip().split():
|
||||
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 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 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
|
||||
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 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 k25=x-kodak-k25 kdc=x-kodak-kdc mrw=x-minolta-mrw nef=x-nikon-nef orf=x-olympus-orf
|
||||
image pef=x-pentax-pef raf=x-fuji-raf raw=x-panasonic-raw sr2=x-sony-sr2 srf=x-sony-srf x3f=x-sigma-x3f
|
||||
audio caf=x-caf mp3=mpeg m4a=mp4 m4b=mp4 m4r=mp4 mid=midi mpc=musepack aif=aiff au=basic qcp=qcelp
|
||||
audio caf=x-caf mp3=mpeg m4a=mp4 m4b=mp4 m4r=mp4 mid=midi mka=x-matroska mpc=musepack aif=aiff au=basic qcp=qcelp
|
||||
video mkv=x-matroska mov=quicktime avi=x-msvideo m4v=x-m4v ts=mp2t
|
||||
video asf=x-ms-asf flv=x-flv 3gp=3gpp 3g2=3gpp2 rmvb=vnd.rn-realmedia-vbr
|
||||
font ttc=collection
|
||||
@@ -636,6 +686,20 @@ if EXE:
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
if PY2 or ANYWIN:
|
||||
raise Exception()
|
||||
HAVE_BWRAP = shutil.which("bwrap")
|
||||
except:
|
||||
HAVE_BWRAP = ""
|
||||
|
||||
|
||||
if ANYWIN:
|
||||
SCWD = os.environ["systemroot"]
|
||||
else:
|
||||
SCWD = None
|
||||
|
||||
|
||||
def py_desc() -> str:
|
||||
interp = platform.python_implementation()
|
||||
py_ver = ".".join([str(x) for x in sys.version_info])
|
||||
@@ -788,7 +852,7 @@ def read_utf8(log: Optional["NamedLogger"], ap: Union[str, bytes], strict: bool)
|
||||
if log:
|
||||
log(t, 3)
|
||||
else:
|
||||
print(t)
|
||||
LOG[0]("#", t)
|
||||
return buf.decode("utf-8", "replace")
|
||||
|
||||
t = "ERROR: The file [%s] is not using the UTF-8 character encoding, and cannot be loaded. The first unreadable character was byte %r at offset %d. Please convert this file to UTF-8 by opening the file in your text-editor and saving it as UTF-8."
|
||||
@@ -796,7 +860,7 @@ def read_utf8(log: Optional["NamedLogger"], ap: Union[str, bytes], strict: bool)
|
||||
if log:
|
||||
log(t, 3)
|
||||
else:
|
||||
print(t)
|
||||
LOG[0]("#", t)
|
||||
raise NotUTF8(t)
|
||||
|
||||
|
||||
@@ -818,10 +882,8 @@ class Daemon(threading.Thread):
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
if CAN_SIGMASK:
|
||||
signal.pthread_sigmask(
|
||||
signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM, signal.SIGUSR1]
|
||||
)
|
||||
if BLOCK_SIGS:
|
||||
signal.pthread_sigmask(signal.SIG_BLOCK, BLOCK_SIGS)
|
||||
|
||||
self.fun(*self.a, **self.ka)
|
||||
|
||||
@@ -1315,7 +1377,7 @@ class MTHash(object):
|
||||
self.csz = chunksz
|
||||
|
||||
chunks: dict[int, tuple[str, int, int]] = {}
|
||||
nchunks = int(math.ceil(fsz / chunksz))
|
||||
nchunks = -int(-fsz // chunksz)
|
||||
for nch in range(nchunks):
|
||||
self.work_q.put(nch)
|
||||
|
||||
@@ -1501,8 +1563,7 @@ class Garda(object):
|
||||
return 0, ip
|
||||
|
||||
if ":" in ip:
|
||||
# assume /64 clients; drop 4 groups
|
||||
ip = IPv6Address(ip).exploded[:-20]
|
||||
ip = ipnorm(ip)
|
||||
|
||||
if prev and self.uniq:
|
||||
if self.prev.get(ip) == prev:
|
||||
@@ -1563,6 +1624,75 @@ def dedent(txt: str) -> str:
|
||||
return "\n".join([ln[pad:] for ln in lns])
|
||||
|
||||
|
||||
def expand_osenv_noop(txt) -> str:
|
||||
return txt
|
||||
|
||||
|
||||
def _expand_osenv_c(txt) -> str:
|
||||
if "${" not in txt:
|
||||
return txt
|
||||
zsl = txt.split("${")
|
||||
ret = zsl[0]
|
||||
for v in zsl[1:]:
|
||||
if "}" not in v:
|
||||
t = "missing '}' after %r in config-value %r" % (v, txt)
|
||||
LOG[0]("ERROR:", t)
|
||||
raise Exception(t)
|
||||
a, b = v.split("}", 1)
|
||||
try:
|
||||
ret += os.environ[a] + b
|
||||
continue
|
||||
except:
|
||||
pass
|
||||
t = "env-var %r not defined; config-value %r" % (a, txt)
|
||||
LOG[0]("ERROR:", t)
|
||||
raise Exception(t)
|
||||
return ret
|
||||
|
||||
|
||||
if os.environ.get("PRTY_NO_ENVEXPAND"):
|
||||
expand_osenv_c = expand_osenv_noop
|
||||
expand_osenv_s = expand_osenv_noop
|
||||
else:
|
||||
expand_osenv_c = _expand_osenv_c
|
||||
expand_osenv_s = os.path.expandvars
|
||||
|
||||
|
||||
def expand_osenv_cs(txt) -> str:
|
||||
a = expand_osenv_c(txt)
|
||||
b = expand_osenv_s(txt)
|
||||
if a == b:
|
||||
return a
|
||||
|
||||
t = "config-value %r is using old syntax for environment-variables; choose one of the following:\noption 1: update the config-value to the new syntax; ${VAR} instead of $VAR or %%VAR%%\noption 2: allow and expand old-syntax with global-option --env-expand 1 (risky)\noption 3: ignore/disable expansion of old-syntax with global-option --env-expand 2\noption 4: disable all env-var expansions by setting env-var PRTY_NO_ENVEXPAND=1"
|
||||
t = t % (txt,)
|
||||
LOG[0]("WARNING:", t)
|
||||
|
||||
try:
|
||||
_, _ = txt.split("$")
|
||||
zs = r"\$(LOGS_DIRECTORY|XDG_[A-Z]+_HOME|XDG_[A-Z]+_DIR)\b"
|
||||
txt = re.sub(zs, r"${\1}", txt)
|
||||
|
||||
a = expand_osenv_c(txt)
|
||||
b = expand_osenv_s(txt)
|
||||
if a == b:
|
||||
return a
|
||||
except:
|
||||
pass
|
||||
|
||||
raise Exception(t)
|
||||
|
||||
|
||||
def signame2int(txt: str) -> int:
|
||||
try:
|
||||
return int(txt)
|
||||
except:
|
||||
txt = txt.upper()
|
||||
if not txt.startswith("SIG"):
|
||||
txt = "SIG" + txt
|
||||
return int(getattr(signal, txt))
|
||||
|
||||
|
||||
def rice_tid() -> str:
|
||||
tid = threading.current_thread().ident
|
||||
c = sunpack(b"B" * 5, spack(b">Q", tid)[-5:])
|
||||
@@ -1696,9 +1826,7 @@ def log_thrs(log: Callable[[str, str, int], None], ival: float, name: str) -> No
|
||||
|
||||
|
||||
def _sigblock():
|
||||
signal.pthread_sigmask(
|
||||
signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM, signal.SIGUSR1]
|
||||
)
|
||||
signal.pthread_sigmask(signal.SIG_BLOCK, BLOCK_SIGS)
|
||||
|
||||
|
||||
sigblock = _sigblock if CAN_SIGMASK else noop
|
||||
@@ -2038,7 +2166,8 @@ class MultipartParser(object):
|
||||
boundary = get_boundary(self.headers)
|
||||
if boundary.startswith('"') and boundary.endswith('"'):
|
||||
boundary = boundary[1:-1] # dillo uses quotes
|
||||
self.log("boundary=%r" % (boundary,))
|
||||
if len(boundary) > 72: # rfc-2046 <=70
|
||||
raise Pebkac(400, "boundary 2big: %d" % (len(boundary),))
|
||||
|
||||
# spec says there might be junk before the first boundary,
|
||||
# can't have the leading \r\n if that's not the case
|
||||
@@ -2104,7 +2233,7 @@ def read_header(sr: Unrecv, t_idle: int, t_tot: int) -> list[str]:
|
||||
raise Pebkac(
|
||||
400,
|
||||
"protocol error while reading headers",
|
||||
log=ret.decode("utf-8", "replace"),
|
||||
log=repr(ret.decode("utf-8", "replace")),
|
||||
)
|
||||
|
||||
ofs = ret.find(b"\r\n\r\n")
|
||||
@@ -2308,7 +2437,7 @@ def uncyg(path: str) -> str:
|
||||
return "%s:\\%s" % (path[1], path[3:])
|
||||
|
||||
|
||||
def undot(path: str) -> str:
|
||||
def _undot(path: str) -> str:
|
||||
ret: list[str] = []
|
||||
for node in path.split("/"):
|
||||
if node == "." or not node:
|
||||
@@ -2324,30 +2453,56 @@ def undot(path: str) -> str:
|
||||
return "/".join(ret)
|
||||
|
||||
|
||||
def sanitize_fn(fn: str) -> str:
|
||||
fn = fn.replace("\\", "/").split("/")[-1]
|
||||
if APTL_OS:
|
||||
fn = sanitize_to(fn, APTL_OS)
|
||||
def _undot_win(path: str) -> str:
|
||||
ret: list[str] = []
|
||||
for node in path.replace("\\", "/").split("/"):
|
||||
if node == "..":
|
||||
if ret:
|
||||
ret.pop()
|
||||
continue
|
||||
|
||||
node = node.rstrip(". ")
|
||||
if node:
|
||||
ret.append(node)
|
||||
|
||||
return "/".join(ret)
|
||||
|
||||
|
||||
if ANYWIN:
|
||||
undot = _undot_win
|
||||
else:
|
||||
undot = _undot
|
||||
|
||||
|
||||
def sanitize_fn(fn: str, fsnt: Optional[str] = None) -> str:
|
||||
fn = fn.replace("\x00", "_").split("/")[-1]
|
||||
if fsnt and fsnt != "lin":
|
||||
fn = fn.translate(VPTL_WIN if fsnt == "win" or ANYWIN else VPTL_MAC)
|
||||
fn2 = fn.replace("\\", "/")
|
||||
if ANYWIN or fsnt == "win" or "/../" in fn2:
|
||||
fn = fn2.split("/")[-1].rstrip(". ")
|
||||
if VPTL_OS:
|
||||
fn = sanitize_to(fn, VPTL_OS)
|
||||
return fn.strip()
|
||||
|
||||
|
||||
def sanitize_to(fn: str, tl: dict[int, int]) -> str:
|
||||
def sanitize_to(fn: str, tl: dict[int, int], fsnt: Optional[str] = None) -> str:
|
||||
fn = fn.translate(tl)
|
||||
if ANYWIN:
|
||||
bad = ["con", "prn", "aux", "nul"]
|
||||
for n in range(1, 10):
|
||||
bad += ("com%s lpt%s" % (n, n)).split(" ")
|
||||
|
||||
if fn.lower().split(".")[0] in bad:
|
||||
if ANYWIN or fsnt == "win":
|
||||
fn = fn.rstrip(". ")
|
||||
if fn.lower().split(".")[0].strip() in BADNAMESET:
|
||||
fn = "_" + fn
|
||||
return fn
|
||||
|
||||
|
||||
def sanitize_vpath(vp: str) -> str:
|
||||
if not APTL_OS:
|
||||
def sanitize_vpath(vp: str, fsnt: Optional[str] = None) -> str:
|
||||
vptl = VPTL_OS
|
||||
if fsnt and fsnt != "lin":
|
||||
vptl = VPTL_WIN if fsnt == "win" or ANYWIN else VPTL_MAC
|
||||
if not vptl:
|
||||
return vp
|
||||
parts = vp.replace(os.sep, "/").split("/")
|
||||
ret = [sanitize_to(x, APTL_OS) for x in parts]
|
||||
ret = [sanitize_to(x, vptl, fsnt) for x in parts]
|
||||
return "/".join(ret)
|
||||
|
||||
|
||||
@@ -2396,6 +2551,33 @@ def exclude_dotfiles_ls(
|
||||
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(
|
||||
base: Union[ODict[str, bool], ODict["LiteralString", bool]], oth: str
|
||||
) -> ODict[str, bool]:
|
||||
@@ -2418,8 +2600,8 @@ def odfusion(
|
||||
|
||||
def ipnorm(ip: str) -> str:
|
||||
if ":" in ip:
|
||||
# assume /64 clients; drop 4 groups
|
||||
return IPv6Address(ip).exploded[:-20]
|
||||
# assume /56 clients; drop final 72 bits
|
||||
return str(IPv6Network(ip + "/56", strict=False).network_address)
|
||||
|
||||
return ip
|
||||
|
||||
@@ -2567,6 +2749,10 @@ def vjoin(rd: str, fn: str) -> str:
|
||||
return rd or fn
|
||||
|
||||
|
||||
def vjoins(*a: str) -> str:
|
||||
return "/".join([x for x in a if x])
|
||||
|
||||
|
||||
# url-join
|
||||
def ujoin(rd: str, fn: str) -> str:
|
||||
if rd and fn:
|
||||
@@ -2811,6 +2997,25 @@ def trystat_shutil_copy2(log: "NamedLogger", src: bytes, dst: bytes) -> bytes:
|
||||
raise
|
||||
|
||||
|
||||
def trystat_shutil_move(log: "NamedLogger", src: bytes, dst: bytes) -> bytes:
|
||||
try:
|
||||
return shutil.move(src, dst)
|
||||
except:
|
||||
# ignore failed mtime on linux+ntfs; for example:
|
||||
# shutil.py:437 <copy2>: copystat(src, dst, follow_symlinks=follow_symlinks)
|
||||
# shutil.py:376 <copystat>: lookup("utime")(dst, ns=(st.st_atime_ns, st.st_mtime_ns),
|
||||
# [PermissionError] [Errno 1] Operation not permitted, '/windows/_videos'
|
||||
_, _, tb = sys.exc_info()
|
||||
for _, _, fun, _ in traceback.extract_tb(tb):
|
||||
if fun == "copystat":
|
||||
if log:
|
||||
t = "warning: failed to retain some file attributes (timestamp and/or permissions) during move from %r to %r:\n%s"
|
||||
log(t % (src, dst, min_ex()), 3)
|
||||
os.unlink(src)
|
||||
return dst # close enough
|
||||
raise
|
||||
|
||||
|
||||
def _fs_mvrm(
|
||||
log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any]
|
||||
) -> bool:
|
||||
@@ -2862,7 +3067,9 @@ def _fs_mvrm(
|
||||
if not attempt and ex.errno == errno.EXDEV:
|
||||
t = "using copy+delete (%s)\n %s\n %s"
|
||||
log(t % (ex.strerror, src, dst))
|
||||
osfun = shutil.move
|
||||
if osfun in (os.replace, os.rename):
|
||||
args.insert(0, log)
|
||||
osfun = trystat_shutil_move
|
||||
continue
|
||||
if now - t0 > maxtime or attempt == 90209:
|
||||
raise
|
||||
@@ -2899,7 +3106,7 @@ def atomic_move(log: "NamedLogger", src: str, dst: str, flags: dict[str, Any]) -
|
||||
os.unlink(bdst)
|
||||
except:
|
||||
pass
|
||||
shutil.move(bsrc, bdst) # type: ignore
|
||||
trystat_shutil_move(log, bsrc, bdst) # type: ignore
|
||||
|
||||
|
||||
def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
|
||||
@@ -3077,6 +3284,31 @@ def list_ips() -> list[str]:
|
||||
return list(ret)
|
||||
|
||||
|
||||
def list_nics(alll: bool = False) -> dict[str, Netdev]:
|
||||
nics = get_adapters(alll)
|
||||
eps: dict[str, Netdev] = {}
|
||||
for nic in nics:
|
||||
name = nic.nice_name
|
||||
try:
|
||||
idx = socket.if_nametoindex(name)
|
||||
if idx and idx != nic.index:
|
||||
LOG[0]("#", "nic-idx mismatch; ifaddr=%r libc=%r" % (nic.index, idx), 3)
|
||||
except:
|
||||
idx = nic.index
|
||||
|
||||
for nip in nic.ips:
|
||||
ipa = nip.ip[0] if ":" in str(nip.ip) else nip.ip
|
||||
sip = "%s/%s" % (ipa, nip.network_prefix)
|
||||
nd = Netdev(sip, idx or 0, name, "")
|
||||
eps[sip] = nd
|
||||
|
||||
if alll and not nic.ips:
|
||||
zs = "no-ip-%s" % (idx,)
|
||||
eps[zs] = Netdev(zs, idx or 0, name, "")
|
||||
|
||||
return eps
|
||||
|
||||
|
||||
def build_netmap(csv: str, defer_mutex: bool = False):
|
||||
csv = csv.lower().strip()
|
||||
|
||||
@@ -3821,7 +4053,7 @@ def _parsehook(
|
||||
|
||||
argv = cmd.split(",") if "," in cmd else [cmd]
|
||||
|
||||
argv[0] = os.path.expandvars(os.path.expanduser(argv[0]))
|
||||
argv[0] = os.path.expanduser(expand_osenv_c(argv[0]))
|
||||
|
||||
return areq, chk, imp, fork, sin, jtxt, wait, sp_ka, argv
|
||||
|
||||
@@ -3854,9 +4086,9 @@ def runihook(
|
||||
}
|
||||
for x, vp, ap in zip(ups, vps, aps)
|
||||
]
|
||||
sp_ka["sin"] = json.dumps(ja).encode("utf-8", "replace")
|
||||
sp_ka["sin"] = json.dumps(ja).encode("utf-8", "replace") + b"\n"
|
||||
else:
|
||||
sp_ka["sin"] = b"\n".join(fsenc(x) for x in aps)
|
||||
sp_ka["sin"] = b"\n".join(fsenc(x) for x in aps) + b"\n"
|
||||
|
||||
if acmd[0].startswith("zmq:"):
|
||||
try:
|
||||
@@ -3871,7 +4103,7 @@ def runihook(
|
||||
|
||||
t0 = time.time()
|
||||
if fork:
|
||||
Daemon(runcmd, cmd, bcmd, ka=sp_ka)
|
||||
Daemon(runcmd, cmd, [bcmd], ka=sp_ka)
|
||||
else:
|
||||
rc, v, err = runcmd(bcmd, **sp_ka) # type: ignore
|
||||
if chk and rc:
|
||||
@@ -4034,15 +4266,22 @@ def _runhook(
|
||||
"src": src,
|
||||
}
|
||||
if txt:
|
||||
ja["txt"] = txt[0]
|
||||
ja["body"] = txt[1]
|
||||
if src in ("xm", "xban"):
|
||||
ja["txt"] = txt[0]
|
||||
ja["body"] = txt[1]
|
||||
elif src == "xbr":
|
||||
ja["ap_to"] = txt[0]
|
||||
elif src in ("xar.ln", "xar.mv"):
|
||||
ja["ap_from"] = txt[0]
|
||||
else:
|
||||
ja["wark"] = txt[0] # acshually the dwark but less confusing
|
||||
if imp:
|
||||
ja["log"] = log
|
||||
mod = loadpy(acmd[0], False)
|
||||
return mod.main(ja)
|
||||
arg = json.dumps(ja)
|
||||
else:
|
||||
arg = txt[0] if txt else ap
|
||||
arg = txt[0] if txt and src in ("xm", "xban") else ap
|
||||
|
||||
if acmd[0].startswith("zmq:"):
|
||||
zi, zs = _zmq_hook(log, verbose, src, acmd[0][4:].lower(), arg, wait, sp_ka)
|
||||
@@ -4148,7 +4387,7 @@ def runhook(
|
||||
else:
|
||||
ret[k] = v
|
||||
except Exception as ex:
|
||||
(log or print)("hook: %r, %s" % (ex, ex))
|
||||
(log or print)("hook failed; %s:\n%s" % (ex, min_ex()))
|
||||
if ",c," in "," + cmd:
|
||||
return {"rc": 1}
|
||||
break
|
||||
@@ -4164,7 +4403,7 @@ def loadpy(ap: str, hot: bool) -> Any:
|
||||
depending on what other inconveniently named files happen
|
||||
to be in the same folder
|
||||
"""
|
||||
ap = os.path.expandvars(os.path.expanduser(ap))
|
||||
ap = os.path.expanduser(expand_osenv_c(ap))
|
||||
mdir, mfile = os.path.split(absreal(ap))
|
||||
mname = mfile.rsplit(".", 1)[0]
|
||||
sys.path.insert(0, mdir)
|
||||
@@ -4197,6 +4436,36 @@ def gzip_file_orig_sz(f) -> int:
|
||||
return sunpack(b"I", rv)[0] # type: ignore
|
||||
|
||||
|
||||
def zip_fi(zf, fp, max_sz):
|
||||
zi = zf.getinfo(fp)
|
||||
if max_sz and zi.file_size > max_sz:
|
||||
raise Pebkac(404, "zip bomb defused")
|
||||
return zi
|
||||
|
||||
|
||||
def zip_lim(zi, max_sz):
|
||||
if zi.file_size > max_sz:
|
||||
raise Pebkac(404, "zip bomb defused")
|
||||
return zi
|
||||
|
||||
|
||||
def zip_read(zf, zi, max_sz):
|
||||
ret = b""
|
||||
with zf.open(zi) as f:
|
||||
while True:
|
||||
buf = f.read(max_sz)
|
||||
if not buf:
|
||||
break
|
||||
ret += buf
|
||||
if len(ret) >= max_sz:
|
||||
raise Pebkac(404, "zip bomb defused")
|
||||
return ret
|
||||
|
||||
|
||||
def zip_readf(zf, zp, max_sz):
|
||||
return zip_read(zf, zip_fi(zf, zp, max_sz), max_sz)
|
||||
|
||||
|
||||
def align_tab(lines: list[str]) -> list[str]:
|
||||
rows = []
|
||||
ncols = 0
|
||||
@@ -4337,6 +4606,18 @@ def hidedir(dp) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def winsparse(f: typing.BinaryIO) -> None:
|
||||
assert ctypes # !rm
|
||||
assert wk32 # !rm
|
||||
import msvcrt
|
||||
|
||||
fh = msvcrt.get_osfhandle(f.fileno())
|
||||
if not wk32.DeviceIoControl(
|
||||
fh, 0x900C4, None, 0, None, 0, ctypes.byref(ctypes.c_ulong()), None
|
||||
):
|
||||
raise ctypes.WinError(ctypes.get_last_error())
|
||||
|
||||
|
||||
_flocks = {}
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ pk: $(addsuffix .gz, $(wildcard tl/*.js *.js *.css) \
|
||||
un: $(addsuffix .un, $(wildcard tl/*.gz *.gz a/*.gz))
|
||||
|
||||
tl/%.js.gz: tl/%.js
|
||||
./Makefile.s1 <$< | pigz -c11 -J 34 -I 573 >$@
|
||||
sh Makefile.s1 <$< | pigz -c11 -J 34 -I 573 >$@
|
||||
pigz -dc <$@ | grep -q ^Ls
|
||||
touch -r $< $@
|
||||
rm $<
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ window.baguetteBox = (function () {
|
||||
onChange: null,
|
||||
readDirRtl: false,
|
||||
},
|
||||
overlay, slider, btnPrev, btnNext, btnHelp, btnAnim, btnRotL, btnRotR, btnSel, btnFull, btnZoom, btnVmode, btnReadDir, btnClose,
|
||||
overlay, slider, btnPrev, btnNext, btnHelp, btnAnim, btnRotL, btnRotR, btnSel, btnFull, btnZoom, btnVmode, btnReadDir, btnGoPage, btnClose,
|
||||
currentGallery = [],
|
||||
currentIndex = 0,
|
||||
isOverlayVisible = false,
|
||||
@@ -46,6 +46,7 @@ window.baguetteBox = (function () {
|
||||
imagesElements = [],
|
||||
documentLastFocus = null,
|
||||
isFullscreen = false,
|
||||
isCbz = false,
|
||||
vmute = false,
|
||||
vloop = sread('vmode') == 'L',
|
||||
vnext = sread('vmode') == 'C',
|
||||
@@ -137,6 +138,9 @@ window.baguetteBox = (function () {
|
||||
};
|
||||
|
||||
var trapFocusInsideOverlay = function (e) {
|
||||
if (modal.busy)
|
||||
return;
|
||||
|
||||
if (overlay.style.display === 'block' && (overlay.contains && !overlay.contains(e.target))) {
|
||||
e.stopPropagation();
|
||||
btnClose.focus();
|
||||
@@ -196,6 +200,16 @@ window.baguetteBox = (function () {
|
||||
return [selectorData.galleries, options];
|
||||
}
|
||||
|
||||
function getCbzPage() {
|
||||
try {
|
||||
var ret = parseInt(/,p([0-9]+)(,|$)/.exec(location.hash)[1]);
|
||||
if (isNum(ret))
|
||||
return Math.max(Math.min(ret, currentGallery.length), 1) - 1;
|
||||
}
|
||||
catch (ex) { }
|
||||
return 0;
|
||||
}
|
||||
|
||||
function bindCbzClickListeners(tagsNodeList, userOptions) {
|
||||
var cbzNodes = [].filter.call(tagsNodeList, function (element) {
|
||||
return re_cbz.test(element.href);
|
||||
@@ -212,10 +226,9 @@ window.baguetteBox = (function () {
|
||||
|
||||
e.preventDefault ? e.preventDefault() : e.returnValue = false;
|
||||
fillCbzGallery(gallery, cbzElement, eventHandler).then(function () {
|
||||
prepareOverlay(gallery, userOptions);
|
||||
showOverlay(0);
|
||||
}
|
||||
).catch(function (reason) {
|
||||
prepareOverlay(gallery, userOptions, true);
|
||||
showOverlay(getCbzPage());
|
||||
}).catch(function (reason) {
|
||||
console.error("cbz-ded", reason);
|
||||
var t;
|
||||
try {
|
||||
@@ -269,6 +282,7 @@ window.baguetteBox = (function () {
|
||||
href: imageHref,
|
||||
imageElement: cbzElement,
|
||||
eventHandler: eventHandler,
|
||||
page: index + 1,
|
||||
};
|
||||
gallery.push(galleryItem);
|
||||
});
|
||||
@@ -317,6 +331,7 @@ window.baguetteBox = (function () {
|
||||
'<button id="bbox-full" type="button" tt="full-screen">⛶</button>' +
|
||||
'<button id="bbzoom" type="button" tt="zoom/stretch">z</button>' +
|
||||
'<button id="bbox-vmode" type="button" tt="a"></button>' +
|
||||
'<button id="bbox-gopage" type="button" tt="go to page">pg</button>' +
|
||||
'<button id="bbox-close" type="button" aria-label="Close">X</button>' +
|
||||
'</div></div>'
|
||||
);
|
||||
@@ -336,6 +351,7 @@ window.baguetteBox = (function () {
|
||||
btnFull = ebi('bbox-full');
|
||||
btnZoom = ebi('bbzoom');
|
||||
btnVmode = ebi('bbox-vmode');
|
||||
btnGoPage = ebi('bbox-gopage');
|
||||
btnClose = ebi('bbox-close');
|
||||
|
||||
bcfg_bind(options, 'bbzoom', 'bbzoom', false, setzoom);
|
||||
@@ -357,6 +373,7 @@ window.baguetteBox = (function () {
|
||||
['F', 'toggle fullscreen'],
|
||||
['Z', 'toggle zoom/stretch'],
|
||||
['S', 'toggle file selection'],
|
||||
['G', 'cbz: go to page'],
|
||||
['space, P, K', 'video: play / pause'],
|
||||
['U', 'video: seek 10sec back'],
|
||||
['O', 'video: seek 10sec ahead'],
|
||||
@@ -442,6 +459,8 @@ window.baguetteBox = (function () {
|
||||
btnZoom.click();
|
||||
else if (kl == "s")
|
||||
tglsel();
|
||||
else if (kl == "g" && isCbz)
|
||||
gotoCbzPage(e);
|
||||
else if (kl == "r")
|
||||
rotn(e.shiftKey ? -1 : 1);
|
||||
else if (kl == "y")
|
||||
@@ -507,6 +526,24 @@ window.baguetteBox = (function () {
|
||||
v.play();
|
||||
}
|
||||
|
||||
function gotoCbzPage(e) {
|
||||
ev(e);
|
||||
|
||||
var cur = currentIndex + 1,
|
||||
max = currentGallery.length;
|
||||
|
||||
modal.prompt('Go to page (1-' + max + ')', '' + cur, function (v) {
|
||||
if (!v)
|
||||
return;
|
||||
|
||||
v = parseInt(v);
|
||||
if (!isNum(v))
|
||||
return toast.warn(4, 'invalid input');
|
||||
|
||||
show(Math.max(Math.min(v, max), 1) - 1);
|
||||
});
|
||||
}
|
||||
|
||||
function tglVmode() {
|
||||
if (vloop) {
|
||||
vnext = true;
|
||||
@@ -636,6 +673,7 @@ window.baguetteBox = (function () {
|
||||
bind(btnRotR, 'click', rotr);
|
||||
bind(btnSel, 'click', tglsel);
|
||||
bind(btnFull, 'click', tglfull);
|
||||
bind(btnGoPage, 'click', gotoCbzPage);
|
||||
bind(slider, 'contextmenu', contextmenuHandler);
|
||||
bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
||||
bind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
||||
@@ -660,18 +698,23 @@ window.baguetteBox = (function () {
|
||||
unbind(btnRotR, 'click', rotr);
|
||||
unbind(btnSel, 'click', tglsel);
|
||||
unbind(btnFull, 'click', tglfull);
|
||||
unbind(btnGoPage, 'click', gotoCbzPage);
|
||||
unbind(slider, 'contextmenu', contextmenuHandler);
|
||||
unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
||||
unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
||||
unbind(overlay, 'touchend', touchendHandler);
|
||||
unbind(document, 'focus', trapFocusInsideOverlay, true);
|
||||
timer.rm(rotn);
|
||||
clearTimeout(unrot_timer);
|
||||
}
|
||||
|
||||
function prepareOverlay(gallery, userOptions) {
|
||||
function prepareOverlay(gallery, userOptions, cbz) {
|
||||
if (currentGallery === gallery)
|
||||
return;
|
||||
|
||||
isCbz = cbz;
|
||||
btnGoPage.style.display = isCbz ? '' : 'none';
|
||||
|
||||
currentGallery = gallery;
|
||||
setOptions(userOptions);
|
||||
slider.innerHTML = '';
|
||||
|
||||
@@ -1768,6 +1768,10 @@ html.y #tree.nowrap .ntree a+a:hover {
|
||||
top: .3em;
|
||||
z-index: 9;
|
||||
}
|
||||
.dumb_loader_thing.ns {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
}
|
||||
#dlt_t {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
@@ -121,6 +121,8 @@
|
||||
|
||||
<h2 id="wfp"><a href="{{ r }}/?h" id="goh">control-panel</a></h2>
|
||||
|
||||
<p id="js_bork">Looks like javascript is broken; see "csp nonce" section in readme</p>
|
||||
|
||||
<a href="#" id="repl">π</a>
|
||||
|
||||
</div>
|
||||
@@ -131,8 +133,9 @@
|
||||
|
||||
<div id="rcm" tabindex="0"></div>
|
||||
|
||||
<script>
|
||||
<script nonce="{{ js_nonce }}">
|
||||
var SR = "{{ r }}",
|
||||
JS_NONCE = "{{ js_nonce }}",
|
||||
CGV1 = {{ cgv1 }},
|
||||
CGV = {{ cgv|tojson }},
|
||||
TS = "{{ ts }}",
|
||||
@@ -146,17 +149,17 @@
|
||||
var STG = window.localStorage;
|
||||
document.documentElement.className = (STG && STG.cpp_thm) || dtheme;
|
||||
</script>
|
||||
<script src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
|
||||
{%- if lang != "eng" %}
|
||||
<script src="{{ r }}/.cpr/w/tl/{{ lang }}.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/tl/{{ lang }}.js?_={{ ts }}"></script>
|
||||
{%- endif %}
|
||||
<script src="{{ r }}/.cpr/w/baguettebox.js?_={{ ts }}"></script>
|
||||
<script src="{{ r }}/.cpr/w/browser.js?_={{ ts }}"></script>
|
||||
<script src="{{ r }}/.cpr/w/up2k.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/baguettebox.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/browser.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/up2k.js?_={{ ts }}"></script>
|
||||
{%- if js %}
|
||||
<script src="{{ js }}_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ js }}_={{ ts }}"></script>
|
||||
{%- endif %}
|
||||
<script>
|
||||
<script nonce="{{ js_nonce }}">
|
||||
Date.now();function jsldp(a,b){2!=window[a]&&alert("FATAL ERROR: cannot load "+b+".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R")}
|
||||
jsldp("J_UTL","util");
|
||||
jsldp("J_BBX","baguettebox");
|
||||
|
||||
+256
-77
@@ -2,7 +2,7 @@
|
||||
|
||||
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');
|
||||
|
||||
var XHR = XMLHttpRequest;
|
||||
@@ -46,6 +46,7 @@ if (1)
|
||||
["ctrl-V", "paste (move/copy) here"],
|
||||
["Y", "download selected"],
|
||||
["F2", "rename selected"],
|
||||
["F4", "update/refresh"],
|
||||
|
||||
"file-list-sel",
|
||||
["space", "toggle file selection"],
|
||||
@@ -228,6 +229,7 @@ if (1)
|
||||
"cl_hpick": "tap on column headers to hide in the table below",
|
||||
"cl_hcancel": "column hiding aborted",
|
||||
"cl_rcm": "right-click menu",
|
||||
"cl_gauto": "autogrid",
|
||||
|
||||
"ct_grid": '田 the grid',
|
||||
"ct_ttips": '◔ ◡ ◔">ℹ️ tooltips',
|
||||
@@ -282,6 +284,8 @@ if (1)
|
||||
"tt_dynt": "autogrow as tree expands",
|
||||
"tt_wrap": "word wrap",
|
||||
"tt_hover": "reveal overflowing lines on hover$N( breaks scrolling unless mouse $N 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_btns": "cmds",
|
||||
@@ -576,9 +580,10 @@ if (1)
|
||||
"u_https1": "you should",
|
||||
"u_https2": "switch to https",
|
||||
"u_https3": "for better performance",
|
||||
"u_ancient": 'your browser is impressively ancient -- maybe you should <a href="#" onclick="goto(\'bup\')">use bup instead</a>',
|
||||
"u_ancient": 'your browser is impressively ancient -- maybe you should <a href="#" id="u2nah">use bup instead</a>',
|
||||
"u_nowork": "need firefox 53+ or chrome 57+ or iOS 11+",
|
||||
"tail_2old": "need firefox 105+ or chrome 71+ or iOS 14.5+",
|
||||
"b2old": 'your browser is too old',
|
||||
"u_nodrop": 'your browser is too old for drag-and-drop uploading',
|
||||
"u_notdir": "that's not a folder!\n\nyour browser is too old,\nplease try dragdrop instead",
|
||||
"u_uri": "to dragdrop images from other browser windows,\nplease drop it onto the big upload button",
|
||||
@@ -662,6 +667,7 @@ if (1)
|
||||
"rc_pla": "play as audio",
|
||||
"rc_txt": "open in textfile viewer",
|
||||
"rc_md": "open in markdown viewer",
|
||||
"rc_wopi": "open in office editor",
|
||||
"rc_dl": "download",
|
||||
"rc_zip": "download as archive",
|
||||
"rc_cpl": "copy link",
|
||||
@@ -713,6 +719,54 @@ var L = Ls[lang] || Ls.eng, LANGS = [];
|
||||
for (var a = 0; a < LANGN.length; a++)
|
||||
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, relod);
|
||||
crashed = true;
|
||||
throw 1;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
function langtest() {
|
||||
var n = LANGS.length - 1;
|
||||
@@ -721,7 +775,9 @@ function langtest() {
|
||||
}
|
||||
function langtest2() {
|
||||
for (var a = 0; a < LANGS.length; a++) {
|
||||
if (!Ls[LANGS[a]]) continue;
|
||||
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,
|
||||
i2 = i1 == a ? b : a,
|
||||
t1 = Ls[LANGS[i1]],
|
||||
@@ -735,6 +791,7 @@ for (var a = 0; a < LANGS.length; a++) {
|
||||
}
|
||||
}
|
||||
}
|
||||
langtest2();
|
||||
|
||||
|
||||
|
||||
@@ -811,7 +868,7 @@ ebi('widget').innerHTML = (
|
||||
|
||||
// up2k ui
|
||||
ebi('op_up2k').innerHTML = (
|
||||
'<form id="u2form" method="post" enctype="multipart/form-data" onsubmit="return false;"></form>\n' +
|
||||
'<form id="u2form" method="post" enctype="multipart/form-data"></form>\n' +
|
||||
|
||||
'<table id="u2conf">\n' +
|
||||
' <tr>\n' +
|
||||
@@ -890,6 +947,9 @@ ebi('op_up2k').innerHTML = (
|
||||
'<div id="u2life"></div>' +
|
||||
'<div id="u2foot"></div>'
|
||||
);
|
||||
ebi('u2form').onsubmit = function () {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
ebi('wrap').insertBefore(mknod('div', 'lazy'), ebi('epi'));
|
||||
@@ -936,18 +996,29 @@ ebi('op_cfg').innerHTML = (
|
||||
' </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' +
|
||||
' <div><select id="fszfmt">\n' +
|
||||
' <option value="0">0 ┃ 1234567</option>\n' +
|
||||
' <option value="1">1 ┃ 1 234 567</option>\n' +
|
||||
' <option value="2">2- ┃ 1.18 M</option>\n' +
|
||||
' <option value="2c">2c ┃ 1.18 M</option>\n' +
|
||||
' <option value="3">3- ┃ 1.2 M</option>\n' +
|
||||
' <option value="3c">3c ┃ 1.2 M</option>\n' +
|
||||
' <option value="4">4- ┃ 1.18 MB</option>\n' +
|
||||
' <option value="4c">4c ┃ 1.18 MB</option>\n' +
|
||||
' <option value="5">5- ┃ 1.2 MB</option>\n' +
|
||||
' <option value="5c">5c ┃ 1.2 MB</option>\n' +
|
||||
' <option value="0">0 ┃ 2345678</option>\n' +
|
||||
' <option value="1">1 ┃ 2 345 678</option>\n' +
|
||||
' <option value="2">2- ┃ 2.24 M /1024</option>\n' +
|
||||
' <option value="2c">2c ┃ 2.24 M /1024</option>\n' +
|
||||
' <option value="3">3- ┃ 2.2 M /1024</option>\n' +
|
||||
' <option value="3c">3c ┃ 2.2 M /1024</option>\n' +
|
||||
' <option value="4">4- ┃ 2.24 Mi /1024</option>\n' +
|
||||
' <option value="4c">4c ┃ 2.24 Mi /1024</option>\n' +
|
||||
' <option value="5">5- ┃ 2.2 Mi /1024</option>\n' +
|
||||
' <option value="5c">5c ┃ 2.2 Mi /1024</option>\n' +
|
||||
' <option value="6">6- ┃ 2.35 MB /1000</option>\n' +
|
||||
' <option value="6c">6c ┃ 2.35 MB /1000</option>\n' +
|
||||
' <option value="7">7- ┃ 2.3 MB /1000</option>\n' +
|
||||
' <option value="7c">7c ┃ 2.3 MB /1000</option>\n' +
|
||||
' <option value="fuzzy">fuzzy</option>\n' +
|
||||
' </select></div>\n' +
|
||||
'</div>\n' +
|
||||
@@ -1043,6 +1114,7 @@ ebi('rcm').innerHTML = (
|
||||
'<a href="#" id="rpla">' + L.rc_pla + '</a>' +
|
||||
'<a href="#" id="rtxt">' + L.rc_txt + '</a>' +
|
||||
'<a href="#" id="rmd">' + L.rc_md + '</a>' +
|
||||
'<a href="#" id="rwopi">' + L.rc_wopi + '</a>' +
|
||||
'<div id="rs1" class="sep"></div>' +
|
||||
'<a href="#" id="rcpl">' + L.rc_cpl + '</a>' +
|
||||
'<a href="#" id="rdl">' + L.rc_dl + '</a>' +
|
||||
@@ -1133,6 +1205,8 @@ function goto(dest) {
|
||||
if (treectl)
|
||||
treectl.onscroll();
|
||||
}
|
||||
function go2bup() { goto('bup'); }
|
||||
function go2up2k() { goto('up2k'); }
|
||||
|
||||
|
||||
var m = SPINNER.split(','),
|
||||
@@ -1155,9 +1229,7 @@ onresize100.add(read_sbw, true);
|
||||
|
||||
|
||||
function check_image_support(format, uri) {
|
||||
var cached
|
||||
= window['have_' + format]
|
||||
= sread('have_' + format);
|
||||
var cached = window['have_' + format] = sread('have_' + format);
|
||||
if (cached !== null)
|
||||
return;
|
||||
|
||||
@@ -1177,8 +1249,11 @@ check_image_support('jxl', "data:image/jxl;base64,/woIAAAMABKIAgC4AF3lEgA=");
|
||||
|
||||
|
||||
var img_re = APPLE ?
|
||||
/\.(a?png|avif|bmp|gif|hei[cf]s?|jpe?g|jfif|svg|webp|webm|mkv|mp4|m4v|mov)(\?|$)/i :
|
||||
/\.(a?png|avif|bmp|gif|jpe?g|jfif|svg|webp|webm|mkv|mp4|m4v|mov)(\?|$)/i;
|
||||
/\.(a?png|avif|bmp|gif|hei[cf]s?|jpe?g|jfif|jxl|svg|webp|webm|mkv|mp4|m4v|mov)(\?|$)/i :
|
||||
/\.(a?png|avif|bmp|gif|jpe?g|jfif|jxl|svg|webp|webm|mkv|mp4|m4v|mov)(\?|$)/i;
|
||||
|
||||
var wopi_set = !window.have_wopi ? null :
|
||||
new Set('odt fodt ott doc docx dotx rtf odm ods fods ots xls xlsx odp fodp otp ppt pptx ppsx odg fodg otg odf'.split(' '));
|
||||
|
||||
|
||||
function set_files_html(html) {
|
||||
@@ -1638,7 +1713,7 @@ mpl.init_ac2();
|
||||
var re_m3u = /\.(m3u8?)$/i;
|
||||
var re_au_native = (can_ogg || have_acode) ? /\.(aac|flac|m4[abr]|mp3|oga|ogg|opus|wav)$/i : /\.(aac|flac|m4[abr]|mp3|wav)$/i,
|
||||
re_au_vid = /\.(3gp|asf|avi|flv|m4v|mkv|mov|mp4|mpeg|mpeg2|mpegts|mpg|mpg2|nut|ogm|ogv|rm|ts|vob|webm|wmv)$/i,
|
||||
re_au_all = /\.(aac|ac3|aif|aiff|alac|alaw|amr|ape|au|dfpwm|dts|flac|gsm|it|itgz|itxz|itz|m4[abr]|mdgz|mdxz|mdz|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|oga|ogg|okt|opus|ra|s3m|s3gz|s3xz|s3z|tak|tta|ulaw|wav|wma|wv|xm|xmgz|xmxz|xmz|xpk|3gp|asf|avi|flv|m4v|mkv|mov|mp4|mpeg|mpeg2|mpegts|mpg|mpg2|nut|ogm|ogv|rm|ts|vob|webm|wmv)$/i;
|
||||
re_au_all = /\.(aac|ac3|aif|aiff|alac|alaw|amr|ape|au|b[cfr]stm|dfpwm|dts|flac|gsm|it|itgz|itxz|itz|m4[abr]|mdgz|mdxz|mdz|mka|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|oga|ogg|okt|opus|ra|s3m|s3gz|s3xz|s3z|tak|tta|ulaw|wav|wma|wv|xm|xmgz|xmxz|xmz|xpk|3gp|asf|avi|flv|m4v|mkv|mov|mp4|mpeg|mpeg2|mpegts|mpg|mpg2|nut|ogm|ogv|rm|ts|vob|webm|wmv)$/i;
|
||||
|
||||
|
||||
// extract songs + add play column
|
||||
@@ -1719,7 +1794,9 @@ function MPlayer() {
|
||||
if (!tid || tid.indexOf('af-') !== 0)
|
||||
continue;
|
||||
|
||||
order.push(tid.slice(1));
|
||||
tid = tid.slice(1);
|
||||
if (r.tracks[tid])
|
||||
order.push(tid);
|
||||
}
|
||||
r.order = order;
|
||||
r.shuffle();
|
||||
@@ -3482,7 +3559,7 @@ function eval_hash() {
|
||||
if (hash0 && window.og_fn) {
|
||||
var all = msel.getall(), mi;
|
||||
for (var a = 0; a < all.length; a++)
|
||||
if (og_fn == uricom_dec(vsplit(all[a].vp)[1].split('?')[0])) {
|
||||
if (og_fn == all[a].fn) {
|
||||
mi = all[a];
|
||||
break;
|
||||
}
|
||||
@@ -3695,7 +3772,7 @@ function sortfiles(nodes) {
|
||||
if ((v + '').indexOf('<a ') === 0)
|
||||
v = v.split('>')[1];
|
||||
else if (name == "href" && v)
|
||||
v = uricom_dec(v);
|
||||
v = uri2txt(v, true);
|
||||
|
||||
nodes[b]._sv = v
|
||||
}
|
||||
@@ -3988,6 +4065,11 @@ var fileman = (function () {
|
||||
}
|
||||
shui.style.display = 'block';
|
||||
|
||||
var wr = has(perms, 'write') && !fns.length;
|
||||
var nope = ['admin', 'move', 'delete', 'upget'];
|
||||
if (!wr)
|
||||
nope.push('write');
|
||||
|
||||
var html = [
|
||||
'<div>',
|
||||
'<table>',
|
||||
@@ -4008,13 +4090,13 @@ var fileman = (function () {
|
||||
'<tr><td>perms</td><td class="sh_axs">',
|
||||
];
|
||||
for (var a = 0; a < perms.length; a++)
|
||||
if (!has(['admin', 'move', 'delete'], perms[a]))
|
||||
if (!has(nope, perms[a]))
|
||||
html.push('<a href="#" class="tgl btn">' + perms[a] + '</a>');
|
||||
|
||||
if (has(perms, 'write'))
|
||||
if (wr)
|
||||
html.push('<a href="#" class="btn">write-only</a>');
|
||||
|
||||
html.push('</td></tr></div');
|
||||
html.push('</td></tr><table></div>');
|
||||
shui.innerHTML = html.join('\n');
|
||||
|
||||
var sh_rand = ebi('sh_rand'),
|
||||
@@ -4080,6 +4162,8 @@ var fileman = (function () {
|
||||
if (this.textContent == 'write-only')
|
||||
for (var a = 0; a < pbtns.length; a++)
|
||||
clmod(pbtns[a], 'on', pbtns[a].textContent == 'write');
|
||||
if (this.textContent == 'get' && clgot(this, 'on') && has(perms, 'read'))
|
||||
clmod(pbtns[0], 'on');
|
||||
}
|
||||
clmod(pbtns[0], 'on', 1);
|
||||
|
||||
@@ -4170,9 +4254,6 @@ var fileman = (function () {
|
||||
s2d[a] = all.indexOf(sel[a]);
|
||||
|
||||
var vp = sel[a].vp;
|
||||
if (vp.endsWith('/'))
|
||||
vp = vp.slice(0, -1);
|
||||
|
||||
var vsp = vsplit(vp);
|
||||
if (base != vsp[0])
|
||||
return toast.err(0, esc('bug:\n' + base + '\n' + vsp[0]));
|
||||
@@ -4466,8 +4547,9 @@ var fileman = (function () {
|
||||
}
|
||||
|
||||
var msg = esc(L.fr_busy.format(f.length, f[0].ofn));
|
||||
msg += '\n<a id="fs_abrt" class="btn" href="#" onclick="fs_abrt()">' + L.fs_abrt + '</a>';
|
||||
msg += '\n<a id="fs_abrt" class="btn" href="#">' + L.fs_abrt + '</a>';
|
||||
toast.show('inf r', 0, msg);
|
||||
ebi('fs_abrt').onclick = fs_abrt;
|
||||
var dst = base + uricom_enc(f[0].inew.value, false);
|
||||
|
||||
function rename_cb() {
|
||||
@@ -4490,6 +4572,8 @@ var fileman = (function () {
|
||||
xhr.onload = xhr.onerror = rename_cb;
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
ire.oninput();
|
||||
};
|
||||
|
||||
r.delete = function (e) {
|
||||
@@ -4516,6 +4600,9 @@ var fileman = (function () {
|
||||
if (err !== 'xbd')
|
||||
toast.ok(2, L.fd_ok);
|
||||
|
||||
if (QS('#seldoc.sel'))
|
||||
thegrid.setvis(true);
|
||||
|
||||
treectl.goto();
|
||||
return;
|
||||
}
|
||||
@@ -4794,8 +4881,9 @@ var fileman = (function () {
|
||||
return paster();
|
||||
|
||||
var msg = esc((r.ccp ? L.fcp_busy : L.fp_busy).format(f.length + 1, uricom_dec(t.src)));
|
||||
msg += '\n<a id="fs_abrt" class="btn" href="#" onclick="fs_abrt()">' + L.fs_abrt + '</a>';
|
||||
msg += '\n<a id="fs_abrt" class="btn" href="#">' + L.fs_abrt + '</a>';
|
||||
toast.show('inf r', 0, msg);
|
||||
ebi('fs_abrt').onclick = fs_abrt;
|
||||
|
||||
var xhr = new XHR(),
|
||||
act = r.ccp ? '?copy=' : '?move=',
|
||||
@@ -4991,11 +5079,22 @@ var showfile = (function () {
|
||||
r.nmap = {
|
||||
'dockerfile': 'docker'
|
||||
};
|
||||
r.pmap = {
|
||||
'sh': 'bash',
|
||||
'ash': 'bash',
|
||||
'dash': 'bash',
|
||||
'luajit': 'lua',
|
||||
'node': 'js',
|
||||
};
|
||||
var x = txt_ext + ' ans c cfg conf cpp cs css diff glsl go html ini java js json jsx kt kts latex less lisp lua makefile md nasm nim nix py r rss rb ruby sass scss sql svg swift tex toml ts vhdl xml yaml zig';
|
||||
x = x.split(/ +/g);
|
||||
for (var a = 0; a < x.length; a++)
|
||||
if (!r.map["." + x[a]])
|
||||
r.map["." + x[a]] = x[a];
|
||||
x = 'awk bash lua make perl python ruby swift';
|
||||
x = x.split(/ +/g);
|
||||
for (var a = 0; a < x.length; a++)
|
||||
r.pmap[x[a]] = x[a];
|
||||
|
||||
r.sname = function (srch) {
|
||||
return srch.split(/[?&]doc=/)[1].split('&')[0];
|
||||
@@ -5004,7 +5103,7 @@ var showfile = (function () {
|
||||
if (window.og_fn) {
|
||||
var ext = og_fn.split(/\./g).pop();
|
||||
if (r.map['.' + ext])
|
||||
hist_replace(get_evpath() + '?doc=' + og_fn);
|
||||
hist_replace(get_evpath() + '?doc=' + uricom_enc(og_fn));
|
||||
}
|
||||
|
||||
window.Prism = { 'manual': true };
|
||||
@@ -5015,7 +5114,7 @@ var showfile = (function () {
|
||||
var m = /[?&]doc=([^&]+)/.exec(location.search);
|
||||
if (m) {
|
||||
setTimeout(function () {
|
||||
r.show(uricom_dec(m[1]), true);
|
||||
r.show(m[1], true);
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
@@ -5035,11 +5134,17 @@ var showfile = (function () {
|
||||
return !!/[?&]doc=/.exec(location.search);
|
||||
};
|
||||
|
||||
r.getlang = function (fn) {
|
||||
r.getlang = function (fn, txt) {
|
||||
fn = fn.toLowerCase();
|
||||
var ext = fn.slice(fn.lastIndexOf('.'));
|
||||
return r.map[ext] || r.nmap[fn];
|
||||
}
|
||||
var prog = txt && txt.startsWith('#!') && txt.split('\n')[0].replace(
|
||||
'#!/usr/bin/env', '').replace(
|
||||
'#!/usr/bin/', '').replace(
|
||||
'#!/bin/', '').trim().split(' ')[0];
|
||||
if (prog)
|
||||
prog = r.pmap[prog] || r.pmap[prog.replace(/[0-9].*/, '')];
|
||||
return r.map[ext] || r.nmap[fn] || prog;
|
||||
};
|
||||
|
||||
r.addlinks = function () {
|
||||
r.files = [];
|
||||
@@ -5052,7 +5157,7 @@ var showfile = (function () {
|
||||
if (!lang)
|
||||
continue;
|
||||
|
||||
r.files.push({ 'id': link.id, 'name': uricom_dec(fn) });
|
||||
r.files.push({ 'id': link.id, 'name': link.fn });
|
||||
|
||||
var ah = ebi(link.id),
|
||||
td = ah.closest('tr').getElementsByTagName('td')[0];
|
||||
@@ -5188,7 +5293,7 @@ var showfile = (function () {
|
||||
txt = doc[2],
|
||||
name = url.split('?')[0].split('/').pop(),
|
||||
tname = uricom_dec(name),
|
||||
lang = r.getlang(name),
|
||||
lang = r.getlang(name, txt),
|
||||
is_md = lang == 'md';
|
||||
|
||||
ebi('files').style.display = ebi('gfiles').style.display = ebi('lazy').style.display = ebi('pro').style.display = ebi('epi').style.display = 'none';
|
||||
@@ -5348,8 +5453,8 @@ var showfile = (function () {
|
||||
Prism.highlightElement(el);
|
||||
}
|
||||
catch (ex) { }
|
||||
btn.setAttribute('download', ebi('docname').innerHTML);
|
||||
btn.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(jt));
|
||||
btn.setAttribute('download', ebi('docname').innerHTML);
|
||||
btn.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(jt));
|
||||
};
|
||||
|
||||
r.mktree = function () {
|
||||
@@ -5377,7 +5482,7 @@ var showfile = (function () {
|
||||
sel = false;
|
||||
|
||||
for (var b = 0; b < sels.length; b++)
|
||||
if (vsplit(sels[b].vp)[1] == lin)
|
||||
if (sels[b].fn == lin)
|
||||
sel = true;
|
||||
|
||||
clmod(lis[a], 'hl', lin == fn);
|
||||
@@ -5400,7 +5505,7 @@ var showfile = (function () {
|
||||
r.tgltail = function () {
|
||||
if (!window.TextDecoderStream) {
|
||||
bcfg_set('taildoc', r.taildoc = false);
|
||||
return toast.err(10, L.tail_2old);
|
||||
return toast.err(10, L.b2old + ';\n' + L.tail_2old);
|
||||
}
|
||||
r.show(r.url, true);
|
||||
};
|
||||
@@ -5557,6 +5662,16 @@ var thegrid = (function () {
|
||||
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) {
|
||||
if (v) {
|
||||
r.ln += v;
|
||||
@@ -5601,8 +5716,8 @@ var thegrid = (function () {
|
||||
fid = oth.getAttribute('id'),
|
||||
aplay = ebi('a' + fid),
|
||||
atext = ebi('t' + fid),
|
||||
is_txt = atext && !/\.ts$/.test(href) && showfile.getlang(href),
|
||||
is_img = img_re.test(href),
|
||||
is_txt = atext && !is_img && !/\.ts$/.test(href) && showfile.getlang(href),
|
||||
is_dir = href.endsWith('/'),
|
||||
is_srch = !!ebi('unsearch'),
|
||||
in_tree = is_dir && treectl.find(oth.textContent.slice(0, -1)),
|
||||
@@ -5669,8 +5784,10 @@ var thegrid = (function () {
|
||||
var ths = QSA('#ggrid>a');
|
||||
|
||||
for (var a = 0, aa = ths.length; a < aa; a++) {
|
||||
var tr = ebi(ths[a].getAttribute('ref')).closest('tr'),
|
||||
cl = tr.className || '';
|
||||
var ref = ths[a].getAttribute('ref');
|
||||
if (!ref)
|
||||
continue;
|
||||
var cl = ebi(ref).closest('tr').className || '';
|
||||
|
||||
if (noq_href(ths[a]).endsWith('/'))
|
||||
cl += ' dir';
|
||||
@@ -5779,7 +5896,7 @@ var thegrid = (function () {
|
||||
ihref = addq(ihref, 'th=' + (
|
||||
have_jxl ? 'x' :
|
||||
have_webp ? 'w' :
|
||||
'j'
|
||||
'j'
|
||||
));
|
||||
if (!r.crop)
|
||||
ihref += 'f';
|
||||
@@ -5806,10 +5923,15 @@ var thegrid = (function () {
|
||||
|
||||
html.push('<a href="' + ohref + '" ref="' + ref +
|
||||
'"' + ac + ' ttt="' + esc(name) + '"><img style="height:' +
|
||||
(r.sz / 1.25) + 'em" loading="lazy" onload="th_onload(this)" src="' +
|
||||
(r.sz / 1.25) + 'em" loading="lazy" fetchPriority="low" src="' +
|
||||
ihref + '" /><span' + ac + '>' + ao.innerHTML + '</span></a>');
|
||||
}
|
||||
ggrid.innerHTML = html.join('\n');
|
||||
|
||||
var ths = QSA('#ggrid>a>img');
|
||||
for (var a = 0, aa = ths.length; a < aa; a++)
|
||||
ths[a].onload = th_onload;
|
||||
|
||||
clmod(ggrid, 'crop', r.crop);
|
||||
clmod(ggrid, 'nocrop', !r.crop);
|
||||
|
||||
@@ -5820,7 +5942,7 @@ var thegrid = (function () {
|
||||
if (srch && r.sel)
|
||||
gsel.click();
|
||||
|
||||
var ths = QSA('#ggrid>a');
|
||||
ths = QSA('#ggrid>a');
|
||||
for (var a = 0, aa = ths.length; a < aa; a++) {
|
||||
ths[a].ondblclick = gclick2;
|
||||
ths[a].onclick = gclick1;
|
||||
@@ -5855,7 +5977,11 @@ var thegrid = (function () {
|
||||
},
|
||||
onChange: function (i, maxIdx) {
|
||||
if (this[i].imageElement) {
|
||||
sethash('g' + this[i].imageElement.getAttribute('ref') + getsort());
|
||||
var h = 'g' + this[i].imageElement.getAttribute('ref');
|
||||
if (this[i].page)
|
||||
h += ',p' + this[i].page;
|
||||
h += getsort();
|
||||
sethash(h);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -5944,14 +6070,25 @@ var thegrid = (function () {
|
||||
pbar.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;
|
||||
|
||||
return r;
|
||||
})();
|
||||
|
||||
|
||||
function th_onload(el) {
|
||||
el.style.height = '';
|
||||
function th_onload() {
|
||||
this.style.height = '';
|
||||
}
|
||||
|
||||
|
||||
@@ -6188,6 +6325,9 @@ var ahotkeys = function (e) {
|
||||
return ebi('griden').click();
|
||||
}
|
||||
|
||||
if (aet == 'input')
|
||||
return;
|
||||
|
||||
var in_ftab = (aet == 'tr' || aet == 'td') && ae.closest('#files');
|
||||
if (in_ftab) {
|
||||
var d = '', rem = 0;
|
||||
@@ -6321,6 +6461,11 @@ var ahotkeys = function (e) {
|
||||
if (k == 'F2')
|
||||
return fileman.rename();
|
||||
|
||||
if (k == 'F4') {
|
||||
enspin('t', 1);
|
||||
return treectl.goto();
|
||||
}
|
||||
|
||||
if (!treectl.hidden && (!sh || !thegrid.en)) {
|
||||
if (kl == 'a')
|
||||
return QS('#twig').click();
|
||||
@@ -7572,6 +7717,11 @@ var treectl = (function () {
|
||||
tn.href = addq(tn.href, 'v');
|
||||
}
|
||||
|
||||
|
||||
if (wopi_set && wopi_set.has(tn.ext))
|
||||
tn.lead = '<a href="?wopi=' + bhref +
|
||||
'" rel="nofollow" target="_blank" name="' + hname + '">📄</a>';
|
||||
|
||||
if (tn.lead == '-')
|
||||
tn.lead = '<a href="?doc=' + bhref + '" id="t' + id +
|
||||
'" rel="nofollow" class="doc' + (lang ? ' bri' : '') +
|
||||
@@ -7656,11 +7806,15 @@ var treectl = (function () {
|
||||
}
|
||||
}
|
||||
|
||||
if (thegrid.gaen && sread('griden') != 1)
|
||||
thegrid.autogrid(res);
|
||||
|
||||
if (url) setTimeout(asdf, 1); else asdf();
|
||||
}
|
||||
|
||||
r.hydrate = function () {
|
||||
qsr('#bbsw');
|
||||
qsr('#js_bork');
|
||||
srvinf = ebi('srv_info').innerHTML.slice(6, -7);
|
||||
if (ls0 === null) {
|
||||
r.ls_cb = showfile.addlinks;
|
||||
@@ -7777,7 +7931,7 @@ var treectl = (function () {
|
||||
delete res['a'];
|
||||
var keys = Object.keys(res);
|
||||
for (var a = 0; a < keys.length; a++)
|
||||
keys[a] = [uricom_dec(keys[a]), keys[a]];
|
||||
keys[a] = [uri2txt(keys[a]), keys[a]];
|
||||
|
||||
if (ENATSORT)
|
||||
keys.sort(function (a, b) { return NATSORT.compare(a[0], b[0]); });
|
||||
@@ -7865,12 +8019,12 @@ var treectl = (function () {
|
||||
})();
|
||||
|
||||
|
||||
function enspin(i) {
|
||||
function enspin(i, ns) {
|
||||
i = 'dlt_' + i;
|
||||
if (ebi(i))
|
||||
return;
|
||||
var d = mknod('div', i, SPINNER);
|
||||
d.className = 'dumb_loader_thing';
|
||||
d.className = 'dumb_loader_thing' + ( ns? ' ns' : '');
|
||||
if (SPINNER_CSS)
|
||||
d.style.cssText = SPINNER_CSS;
|
||||
document.body.appendChild(d);
|
||||
@@ -7939,12 +8093,17 @@ function apply_perms(res) {
|
||||
a.style.display = '';
|
||||
tt.att(QS('#ops'));
|
||||
|
||||
var v_perms = perms.slice(0);
|
||||
var have_read = has(perms, 'read');
|
||||
if (have_read)
|
||||
apop(v_perms, 'get');
|
||||
|
||||
for (var a = 0; a < chk.length; a++)
|
||||
if (has(perms, chk[a]))
|
||||
if (has(v_perms, chk[a]))
|
||||
axs.push(chk[a].slice(0, 1).toUpperCase() + chk[a].slice(1));
|
||||
|
||||
axs = axs.join('-');
|
||||
if (perms.length == 1) {
|
||||
if (v_perms.length == 1) {
|
||||
aclass = ' class="warn">';
|
||||
axs += '-Only';
|
||||
}
|
||||
@@ -7986,7 +8145,6 @@ function apply_perms(res) {
|
||||
document.body.setAttribute('perms', perms.join(' '));
|
||||
|
||||
var have_write = has(perms, "write"),
|
||||
have_read = has(perms, "read"),
|
||||
de = document.documentElement,
|
||||
tds = QSA('#u2conf td');
|
||||
|
||||
@@ -8492,7 +8650,7 @@ var setfszf = (function () {
|
||||
setck('cplng=' + lang);
|
||||
freshen();
|
||||
var t = L.tt == 'English' ? '' : Ls.eng.lang_set;
|
||||
modal.confirm(L.lang_set + "\n\n" + t, location.reload.bind(location), null);
|
||||
modal.confirm(L.lang_set + "\n\n" + t, relod, null);
|
||||
}
|
||||
|
||||
freshen();
|
||||
@@ -8629,6 +8787,7 @@ var msel = (function () {
|
||||
item.id = links[a].getAttribute('id');
|
||||
item.sel = clgot(links[a].closest('tr'), 'sel');
|
||||
item.vp = href.indexOf('/') !== -1 ? href : vbase + href;
|
||||
item.fn = uricom_dec(href.split('/').pop());
|
||||
|
||||
if (dk) {
|
||||
var m = /[?&](k=[^&#]+)/.exec(qhref);
|
||||
@@ -8743,7 +8902,7 @@ var msel = (function () {
|
||||
r.evsel = function (e, fun) {
|
||||
ev(e);
|
||||
r.so = r.pr = null;
|
||||
var trs = QSA('#files tbody tr');
|
||||
var trs = QSA('#files tbody tr' + (ebi('unsearch') ? ':not(.srch_hdr)' : ''));
|
||||
for (var a = 0, aa = trs.length; a < aa; a++)
|
||||
clmod(trs[a], 'sel', fun);
|
||||
r.selui();
|
||||
@@ -8885,6 +9044,13 @@ var msel = (function () {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.status == 405) {
|
||||
tb.value = '';
|
||||
sf.textContent = 'already existed';
|
||||
treectl.goto(this.vp + uricom_enc(this.dn) + '/', true);
|
||||
return tree_scrollto();
|
||||
}
|
||||
|
||||
xhrchk(this, L.fd_xe1, L.fd_xe2);
|
||||
|
||||
if (this.status !== 201) {
|
||||
@@ -9011,7 +9177,7 @@ var sandboxjs = (function () {
|
||||
var ret = '',
|
||||
busy = false,
|
||||
url = SR + '/.cpr/w/util.js?_=' + TS,
|
||||
tag = '<script src="' + url + '"></script>';
|
||||
tag = '<script nonce="' + JS_NONCE + '" src="' + url + '"></script>';
|
||||
|
||||
return function () {
|
||||
if (ret || busy)
|
||||
@@ -9021,7 +9187,7 @@ var sandboxjs = (function () {
|
||||
xhr.open('GET', url, true);
|
||||
xhr.onload = function () {
|
||||
if (this.status == 200)
|
||||
ret = '<script>' + this.responseText + '</script>';
|
||||
ret = '<script nonce="' + JS_NONCE + '">' + this.responseText + '</script>';
|
||||
};
|
||||
xhr.send();
|
||||
busy = true;
|
||||
@@ -9043,7 +9209,7 @@ function show_md(md, name, div, url, depth) {
|
||||
if (depth) {
|
||||
clmod(div, 'raw', 1);
|
||||
div.textContent = "--[ " + name + " ]---------\r\n" + md;
|
||||
return toast.warn(10, errmsg + (WebAssembly ? 'failed to load marked.js' : 'your browser is too old'));
|
||||
return toast.warn(10, errmsg + (WebAssembly ? 'failed to load marked.js' : L.b2old));
|
||||
}
|
||||
|
||||
wfp_debounce.n--;
|
||||
@@ -9169,8 +9335,9 @@ function sandbox(tgt, rules, allow, cls, html) {
|
||||
html = '<html class="iframe ' + document.documentElement.className +
|
||||
'"><head><style>html{background:#eee;color:#000}</style><style>' + globalcss() +
|
||||
'</style><base target="_parent"></head><body id="b" class="logue ' + cls + '">' + html +
|
||||
'<script>' + env + '</script>' + sandboxjs() +
|
||||
'<script>var d=document.documentElement,TS="' + TS + '",' +
|
||||
'<script nonce="' + JS_NONCE + '">' + env + '</script>' + sandboxjs() +
|
||||
'<script nonce="' + JS_NONCE + '">' +
|
||||
'var d=document.documentElement,TS="' + TS + '",' +
|
||||
'loc=new URL("' + location.href.split('?')[0] + '");' +
|
||||
'function say(m){window.parent.postMessage(m,"*")};' +
|
||||
'setTimeout(function(){var its=0,pih=-1,f=function(){' +
|
||||
@@ -9195,6 +9362,7 @@ function sandbox(tgt, rules, allow, cls, html) {
|
||||
fr.setAttribute('sandbox', rules ? 'allow-' + rules.replace(/ /g, ' allow-') : '');
|
||||
fr.setAttribute('allow', allow);
|
||||
fr.setAttribute('srcdoc', html);
|
||||
tgt.innerHTML = '';
|
||||
tgt.appendChild(fr);
|
||||
treectl.sb_msg = true;
|
||||
return true;
|
||||
@@ -9626,7 +9794,7 @@ var rcm = (function () {
|
||||
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>');
|
||||
QS("#files tbody").appendChild(row);
|
||||
}
|
||||
}
|
||||
else {
|
||||
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>');
|
||||
@@ -9690,6 +9858,7 @@ var rcm = (function () {
|
||||
case 'pla': play('f-' + selFile.id); break;
|
||||
case 'txt': showfile.show(selFile.name); break;
|
||||
case 'md': location = selFile.path + (has(selFile.path, '?') ? '&v' : '?v'); break;
|
||||
case 'wopi': window.open('?wopi=' + selFile.name, '_blank').focus(); break;
|
||||
case 'cpl': cliptxt(selFile.url, function() {toast.ok(2, L.clipped)}); break;
|
||||
case 'dl': ebi('seldl').click(); break;
|
||||
case 'zip': ebi('selzip').click(); break;
|
||||
@@ -9750,13 +9919,20 @@ var rcm = (function () {
|
||||
|
||||
var has_sel = msel.getsel().length;
|
||||
var has_clip = fileman.clip.length;
|
||||
var ext = (function() {
|
||||
if (!selFile.name) return null;
|
||||
var di = selFile.name.lastIndexOf('.');
|
||||
if (di < 1) return null;
|
||||
return selFile.name.slice(di + 1).toLowerCase();
|
||||
})();
|
||||
|
||||
clmod(ebi('ropn'), 'hide', !selFile.path);
|
||||
clmod(ebi('rply'), 'hide', selFile.type != 'gf' && selFile.type != 'af');
|
||||
clmod(ebi('rpla'), 'hide', selFile.type != 'gf');
|
||||
clmod(ebi('rtxt'), 'hide', !selFile.id);
|
||||
clmod(ebi('rs1'), 'hide', !selFile.path);
|
||||
clmod(ebi('rmd'), 'hide', !selFile.name || selFile.name.slice(-3) != ".md");
|
||||
clmod(ebi('rmd'), 'hide', !selFile.name || ext != "md");
|
||||
clmod(ebi('rwopi'), 'hide', !selFile.name || !wopi_set || !wopi_set.has(ext));
|
||||
clmod(ebi('rcpl'), 'hide', !selFile.path);
|
||||
clmod(ebi('rdl'), 'hide', !has_sel);
|
||||
clmod(ebi('rzip'), 'hide', !has_sel);
|
||||
@@ -9888,19 +10064,19 @@ function reload_browser() {
|
||||
var selbox = null;
|
||||
var ttimer = null;
|
||||
|
||||
var lpdelay = 250;
|
||||
var lpdelay = 250;
|
||||
var mvthresh = 44;
|
||||
|
||||
function unbox() {
|
||||
qsr('.selbox');
|
||||
ebi('gfiles').style.removeProperty('pointer-events')
|
||||
ebi('wrap').style.removeProperty('user-select')
|
||||
|
||||
|
||||
if (selbox) {
|
||||
console.log(selbox)
|
||||
window.getSelection().removeAllRanges();
|
||||
}
|
||||
|
||||
|
||||
is_selma = false;
|
||||
dragging = false;
|
||||
fwrap = null;
|
||||
@@ -9933,17 +10109,17 @@ function reload_browser() {
|
||||
function sel_start(e) {
|
||||
if (e.button !== 0 && e.type !== 'touchstart') return;
|
||||
if (!thegrid.en || !treectl.dsel) return;
|
||||
if (e.target.closest('#widget,#ops,.opview,.doc')) return;
|
||||
if (e.target.closest('#widget,#ops,.opview,.doc,.bbox-open')) return;
|
||||
|
||||
if (e.target.closest('#gfiles'))
|
||||
ebi('gfiles').style.userSelect = "none"
|
||||
ebi('gfiles').style.userSelect = "none";
|
||||
|
||||
var pos = getpp(e);
|
||||
startx = pos.x;
|
||||
starty = pos.y;
|
||||
is_selma = true;
|
||||
ttimer = null;
|
||||
|
||||
|
||||
if (e.type === 'touchstart') {
|
||||
ttimer = setTimeout(function() {
|
||||
ttimer = null;
|
||||
@@ -9951,7 +10127,7 @@ function reload_browser() {
|
||||
}, lpdelay);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function start_drag() {
|
||||
if (dragging) return;
|
||||
|
||||
@@ -9962,7 +10138,7 @@ function reload_browser() {
|
||||
|
||||
ebi('gfiles').style.pointerEvents = 'none';
|
||||
}
|
||||
|
||||
|
||||
function sel_move(e) {
|
||||
if (!is_selma) return;
|
||||
var pos = getpp(e);
|
||||
@@ -9977,7 +10153,9 @@ function reload_browser() {
|
||||
return;
|
||||
}
|
||||
if (!dragging && dist > mvthresh && !window.getSelection().toString()) {
|
||||
if (fwrap = e.target.closest('#wrap'))
|
||||
if (e.target instanceof Element)
|
||||
fwrap = e.target.closest('#wrap');
|
||||
if (fwrap)
|
||||
fwrap.style.userSelect = 'none';
|
||||
else return;
|
||||
start_drag();
|
||||
@@ -10021,11 +10199,12 @@ function reload_browser() {
|
||||
|
||||
window.addEventListener('dragstart', function(e) {
|
||||
if (treectl.dsel && (is_selma || dragging)) {
|
||||
e.preventDefault();
|
||||
if (!QS('body.bbox-open'))
|
||||
ev(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
dsel_init();
|
||||
})();
|
||||
|
||||
@@ -10068,7 +10247,7 @@ var mpss = (function() {
|
||||
|
||||
var gain = afilt.ssg.gain;
|
||||
var duration = ae.duration || 0;
|
||||
|
||||
|
||||
var slimit = duration * (config.sthresh / 100);
|
||||
var elimit = duration * (1 - (config.etresh / 100));
|
||||
var in_limits = ae.currentTime < slimit || ae.currentTime > elimit;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<p>sorry for the inconvenience</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
<script nonce="{{ js_nonce }}">
|
||||
setTimeout(function() {
|
||||
document.getElementById('box').style.opacity = 1;
|
||||
}, 500);
|
||||
|
||||
@@ -36,9 +36,10 @@
|
||||
{%- endif %}
|
||||
</div>
|
||||
<a href="#" id="repl">π</a>
|
||||
<script>
|
||||
<script nonce="{{ js_nonce }}">
|
||||
|
||||
var SR="{{ r }}",
|
||||
JS_NONCE = "{{ js_nonce }}",
|
||||
lang="{{ lang }}",
|
||||
dfavico="{{ favico }}";
|
||||
|
||||
@@ -46,11 +47,11 @@ var STG = window.localStorage;
|
||||
document.documentElement.className = (STG && STG.cpp_thm) || "{{ this.args.theme }}";
|
||||
|
||||
</script>
|
||||
<script src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
|
||||
{%- if js %}
|
||||
<script src="{{ js }}_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ js }}_={{ ts }}"></script>
|
||||
{%- endif %}
|
||||
<script>
|
||||
<script nonce="{{ js_nonce }}">
|
||||
Date.now();function jsldp(a,b){2!=window[a]&&alert("FATAL ERROR: cannot load "+b+".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R")}
|
||||
jsldp("J_UTL","util");
|
||||
</script>
|
||||
|
||||
+9
-14
@@ -131,19 +131,14 @@ write markdown (most html is 🙆 too)
|
||||
</div>
|
||||
{%- endif %}
|
||||
|
||||
<script>
|
||||
<script nonce="{{ js_nonce }}">
|
||||
|
||||
var SR = "{{ r }}",
|
||||
JS_NONCE = "{{ js_nonce }}",
|
||||
CGV1 = {{ cgv1 }},
|
||||
last_modified = {{ lastmod }},
|
||||
have_emp = {{ have_emp }},
|
||||
md_no_br = {{ md_no_br }},
|
||||
dfavico = "{{ favico }}";
|
||||
|
||||
var md_opt = {
|
||||
link_md_as_html: false,
|
||||
modpoll_freq: {{ md_chk_rate }}
|
||||
};
|
||||
|
||||
(function () {
|
||||
var l = window.localStorage,
|
||||
drk = (l && l.light) != 1,
|
||||
@@ -159,13 +154,13 @@ try { l.light = drk? 0:1; } catch (ex) { }
|
||||
})();
|
||||
|
||||
</script>
|
||||
<script src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
|
||||
<script src="{{ r }}/.cpr/w/deps/marked.js?_={{ ts }}"></script>
|
||||
<script src="{{ r }}/.cpr/w/md.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/deps/marked.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/md.js?_={{ ts }}"></script>
|
||||
{%- if edit %}
|
||||
<script src="{{ r }}/.cpr/w/md2.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/md2.js?_={{ ts }}"></script>
|
||||
{%- endif %}
|
||||
<script>
|
||||
<script nonce="{{ js_nonce }}">
|
||||
Date.now();function jsldp(a,b){2!=window[a]&&alert("FATAL ERROR: cannot load "+b+".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R")}
|
||||
jsldp("J_UTL","util");
|
||||
jsldp("J_MD","md");
|
||||
@@ -174,7 +169,7 @@ try { l.light = drk? 0:1; } catch (ex) { }
|
||||
{%- endif %}
|
||||
</script>
|
||||
{%- if js %}
|
||||
<script src="{{ js }}_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ js }}_={{ ts }}"></script>
|
||||
{%- endif %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -316,8 +316,8 @@ function Modpoll() {
|
||||
};
|
||||
|
||||
setTimeout(r.periodic, 300);
|
||||
if (md_opt.modpoll_freq > 0)
|
||||
setInterval(r.periodic, 1000 * md_opt.modpoll_freq);
|
||||
if (modpoll_freq > 0)
|
||||
setInterval(r.periodic, 1000 * modpoll_freq);
|
||||
|
||||
return r;
|
||||
}
|
||||
@@ -325,6 +325,9 @@ var modpoll = new Modpoll();
|
||||
|
||||
|
||||
window.onbeforeunload = function (e) {
|
||||
if (window.gtfo)
|
||||
return;
|
||||
|
||||
if ((ebi("save").className + '').indexOf('disabled') >= 0)
|
||||
return; //nice (todo)
|
||||
|
||||
|
||||
+9
-14
@@ -28,19 +28,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" id="repl">π</a>
|
||||
<script>
|
||||
<script nonce="{{ js_nonce }}">
|
||||
|
||||
var SR = "{{ r }}",
|
||||
JS_NONCE = "{{ js_nonce }}",
|
||||
CGV1 = {{ cgv1 }},
|
||||
last_modified = {{ lastmod }},
|
||||
have_emp = {{ have_emp }},
|
||||
md_no_br = {{ md_no_br }},
|
||||
dfavico = "{{ favico }}";
|
||||
|
||||
var md_opt = {
|
||||
link_md_as_html: false,
|
||||
modpoll_freq: {{ md_chk_rate }}
|
||||
};
|
||||
|
||||
var lightswitch = (function () {
|
||||
var l = window.localStorage,
|
||||
drk = (l && l.light) != 1,
|
||||
@@ -54,14 +49,14 @@ try { l.light = drk? 0:1; } catch (ex) { }
|
||||
})();
|
||||
|
||||
</script>
|
||||
<script src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
|
||||
<script src="{{ r }}/.cpr/w/deps/marked.js?_={{ ts }}"></script>
|
||||
<script src="{{ r }}/.cpr/w/deps/easymde.js?_={{ ts }}"></script>
|
||||
<script src="{{ r }}/.cpr/w/mde.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/deps/marked.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/deps/easymde.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/mde.js?_={{ ts }}"></script>
|
||||
{%- if js %}
|
||||
<script src="{{ js }}_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ js }}_={{ ts }}"></script>
|
||||
{%- endif %}
|
||||
<script>
|
||||
<script nonce="{{ js_nonce }}">
|
||||
Date.now();function jsldp(a,b){2!=window[a]&&alert("FATAL ERROR: cannot load "+b+".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R")}
|
||||
jsldp("J_UTL","util");
|
||||
jsldp("J_MDE","mde");
|
||||
|
||||
@@ -40,19 +40,19 @@ a{color:#fc5}</style>
|
||||
{%- endif %}
|
||||
|
||||
{%- if click %}
|
||||
<script>document.getElementsByTagName("a")[0].click()</script>
|
||||
<script nonce="{{ js_nonce }}">document.getElementsByTagName("a")[0].click()</script>
|
||||
{%- endif %}
|
||||
</div>
|
||||
|
||||
{%- if redir %}
|
||||
<script>
|
||||
<script nonce="{{ js_nonce }}">
|
||||
setTimeout(function() {
|
||||
location.replace("{{ redir }}");
|
||||
}, 800);
|
||||
</script>
|
||||
{%- endif %}
|
||||
{%- if js %}
|
||||
<script src="{{ js }}_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ js }}_={{ ts }}"></script>
|
||||
{%- endif %}
|
||||
</body>
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<id>{{ opds_id }}</id>
|
||||
<title>{{ opds_title | e }}</title>
|
||||
<link rel="search"
|
||||
href="{{ opds_osd | e }}"
|
||||
type="application/opensearchdescription+xml"/>
|
||||
{%- for d in dirs %}
|
||||
<entry>
|
||||
<id>{{ d.opds_id }}</id>
|
||||
<title>{{ d.name | e }}</title>
|
||||
<link rel="subsection"
|
||||
href="{{ d.href | e }}"
|
||||
@@ -14,6 +17,7 @@
|
||||
{%- endfor %}
|
||||
{%- for f in files %}
|
||||
<entry>
|
||||
<id>{{ f.opds_id }}</id>
|
||||
<title>{{ f.name | e }}</title>
|
||||
<updated>{{ f.iso8601 }}</updated>
|
||||
<link rel="http://opds-spec.org/acquisition"
|
||||
|
||||
@@ -22,9 +22,10 @@
|
||||
<div id="tw"></div>
|
||||
</div>
|
||||
<a href="#" id="repl">π</a>
|
||||
<script>
|
||||
<script nonce="{{ js_nonce }}">
|
||||
|
||||
var SR="{{ r }}",
|
||||
JS_NONCE = "{{ js_nonce }}",
|
||||
lang="{{ lang }}",
|
||||
dutc={{ this.args.js_utc }},
|
||||
dfavico="{{ favico }}";
|
||||
@@ -33,13 +34,13 @@ var STG = window.localStorage;
|
||||
document.documentElement.className = (STG && STG.cpp_thm) || "{{ this.args.theme }}";
|
||||
|
||||
</script>
|
||||
<script src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
|
||||
<script>var V={{ v }};</script>
|
||||
<script src="{{ r }}/.cpr/w/rups.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}">var V={{ v }};</script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/rups.js?_={{ ts }}"></script>
|
||||
{%- if js %}
|
||||
<script src="{{ js }}_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ js }}_={{ ts }}"></script>
|
||||
{%- endif %}
|
||||
<script>
|
||||
<script nonce="{{ js_nonce }}">
|
||||
Date.now();function jsldp(a,b){2!=window[a]&&alert("FATAL ERROR: cannot load "+b+".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R")}
|
||||
jsldp("J_UTL","util");
|
||||
jsldp("J_RUP","rups");
|
||||
|
||||
@@ -62,9 +62,10 @@
|
||||
{%- endif %}
|
||||
</div>
|
||||
<a href="#" id="repl">π</a>
|
||||
<script>
|
||||
<script nonce="{{ js_nonce }}">
|
||||
|
||||
var SR="{{ r }}",
|
||||
JS_NONCE = "{{ js_nonce }}",
|
||||
shr="{{ shr }}",
|
||||
lang="{{ lang }}",
|
||||
dutc={{ this.args.js_utc }},
|
||||
@@ -74,12 +75,12 @@ var STG = window.localStorage;
|
||||
document.documentElement.className = (STG && STG.cpp_thm) || "{{ this.args.theme }}";
|
||||
|
||||
</script>
|
||||
<script src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
|
||||
<script src="{{ r }}/.cpr/w/shares.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/shares.js?_={{ ts }}"></script>
|
||||
{%- if js %}
|
||||
<script src="{{ js }}_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ js }}_={{ ts }}"></script>
|
||||
{%- endif %}
|
||||
<script>
|
||||
<script nonce="{{ js_nonce }}">
|
||||
Date.now();function jsldp(a,b){2!=window[a]&&alert("FATAL ERROR: cannot load "+b+".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R")}
|
||||
jsldp("J_UTL","util");
|
||||
jsldp("J_SHR","shares");
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
<a id="ag" href="{{ r }}/?idp">view idp cache</a><br />
|
||||
{%- endif %}
|
||||
|
||||
<a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a><br />
|
||||
<a id="k" href="{{ r }}/?reset" class="r">reset client settings</a><br />
|
||||
|
||||
{%- if this.uname != '*' and not in_shr %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
@@ -206,25 +206,31 @@
|
||||
{%- if not this.args.nb %}
|
||||
<span id="pb"><span>powered by</span> <a href="{{ this.args.pb_url }}">copyparty {{ver}}</a></span>
|
||||
{%- endif %}
|
||||
<script>
|
||||
<script nonce="{{ js_nonce }}">
|
||||
|
||||
var SR="{{ r }}",
|
||||
JS_NONCE = "{{ js_nonce }}",
|
||||
lang="{{ lang }}",
|
||||
dfavico="{{ favico }}";
|
||||
|
||||
var STG = window.localStorage;
|
||||
document.documentElement.className = (STG && STG.cpp_thm) || "{{ this.args.theme }}";
|
||||
|
||||
document.getElementById('k').onclick = function () {
|
||||
localStorage.clear();
|
||||
return true;
|
||||
};
|
||||
|
||||
</script>
|
||||
<script src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
|
||||
{%- if lang != "eng" %}
|
||||
<script src="{{ r }}/.cpr/w/tl/{{ lang }}.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/tl/{{ lang }}.js?_={{ ts }}"></script>
|
||||
{%- endif %}
|
||||
<script src="{{ r }}/.cpr/w/splash.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/splash.js?_={{ ts }}"></script>
|
||||
{%- if js %}
|
||||
<script src="{{ js }}_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ js }}_={{ ts }}"></script>
|
||||
{%- endif %}
|
||||
<script>
|
||||
<script nonce="{{ js_nonce }}">
|
||||
Date.now();function jsldp(a,b){2!=window[a]&&alert("FATAL ERROR: cannot load "+b+".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R")}
|
||||
jsldp("J_UTL","util");
|
||||
jsldp("J_SPL","splash");
|
||||
|
||||
@@ -65,13 +65,21 @@ o = ebi('u');
|
||||
if (o && /[0-9]+$/.exec(o.innerHTML))
|
||||
o.innerHTML = shumantime(o.innerHTML);
|
||||
|
||||
o = ebi('uhash')
|
||||
o = ebi('uhash');
|
||||
if (o)
|
||||
o.value = '' + location.hash;
|
||||
|
||||
if (/\&re=/.test('' + location))
|
||||
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() {
|
||||
if (!ebi('x'))
|
||||
return;
|
||||
|
||||
+17
-16
@@ -54,7 +54,7 @@
|
||||
<div class="os win">
|
||||
<p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p>
|
||||
<pre>
|
||||
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
|
||||
rclone config create --obscure {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s --network-mode {{ aname }}-dav:{{ rvp }} <b>W:</b>
|
||||
</pre>
|
||||
<ul>
|
||||
@@ -77,7 +77,7 @@
|
||||
<div class="os lin">
|
||||
<p>rclone (v1.63 or later) is recommended:</p>
|
||||
<pre>
|
||||
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
|
||||
rclone config create --obscure {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>mp</b>
|
||||
</pre>
|
||||
<ul>
|
||||
@@ -145,9 +145,9 @@
|
||||
<p>connect with plaintext FTP:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
|
||||
rclone config create --obscure {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
|
||||
{%- else %}
|
||||
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
|
||||
rclone config create --obscure {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
|
||||
{%- endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s --network-mode {{ aname }}-ftp:{{ rvp }} <b>W:</b>
|
||||
</pre>
|
||||
@@ -156,9 +156,9 @@
|
||||
<p>connect with TLS-encrypted FTPS:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
|
||||
rclone config create --obscure {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
|
||||
{%- else %}
|
||||
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
|
||||
rclone config create --obscure {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
|
||||
{%- endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s --network-mode {{ aname }}-ftps:{{ rvp }} <b>W:</b>
|
||||
</pre>
|
||||
@@ -184,9 +184,9 @@
|
||||
<p>connect with plaintext FTP:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
|
||||
rclone config create --obscure {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
|
||||
{%- else %}
|
||||
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
|
||||
rclone config create --obscure {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
|
||||
{%- endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>mp</b>
|
||||
</pre>
|
||||
@@ -195,9 +195,9 @@
|
||||
<p>connect with TLS-encrypted FTPS:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
|
||||
rclone config create --obscure {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
|
||||
{%- else %}
|
||||
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
|
||||
rclone config create --obscure {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
|
||||
{%- endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>mp</b>
|
||||
</pre>
|
||||
@@ -233,7 +233,7 @@
|
||||
<h1>partyfuse</h1>
|
||||
<p>
|
||||
<a href="{{ r }}/.cpr/a/partyfuse.py">partyfuse.py</a> -- fast, read-only,
|
||||
needs <a href="{{ r }}/.cpr/w/deps/fuse.py">fuse.py</a> in the same folder,
|
||||
needs <a href="{{ r }}/.cpr/w/deps/mfusepy.py">mfusepy.py</a> in the same folder,
|
||||
<span class="os win">needs <a href="https://winfsp.dev/rel/">winfsp</a></span>
|
||||
<span class="os lin">doesn't need root</span>
|
||||
</p>
|
||||
@@ -347,9 +347,10 @@
|
||||
|
||||
</div>
|
||||
<a href="#" id="repl">π</a>
|
||||
<script>
|
||||
<script nonce="{{ js_nonce }}">
|
||||
|
||||
var SR="{{ r }}",
|
||||
JS_NONCE = "{{ js_nonce }}",
|
||||
lang="{{ lang }}",
|
||||
dfavico="{{ favico }}";
|
||||
|
||||
@@ -357,12 +358,12 @@ var STG = window.localStorage;
|
||||
document.documentElement.className = (STG && STG.cpp_thm) || "{{ args.theme }}";
|
||||
|
||||
</script>
|
||||
<script src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
|
||||
<script src="{{ r }}/.cpr/w/svcs.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/svcs.js?_={{ ts }}"></script>
|
||||
{%- if js %}
|
||||
<script src="{{ js }}_={{ ts }}"></script>
|
||||
<script nonce="{{ js_nonce }}" src="{{ js }}_={{ ts }}"></script>
|
||||
{%- endif %}
|
||||
<script>
|
||||
<script nonce="{{ js_nonce }}">
|
||||
Date.now();function jsldp(a,b){2!=window[a]&&alert("FATAL ERROR: cannot load "+b+".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R")}
|
||||
jsldp("J_UTL","util");
|
||||
jsldp("J_SVC","svcs");
|
||||
|
||||
@@ -39,6 +39,7 @@ Ls.chi = {
|
||||
["ctrl-V", "粘贴(移动/复制)到此处"],
|
||||
["Y", "下载选中项"],
|
||||
["F2", "重命名选中项"],
|
||||
["F4", "刷新"], //m
|
||||
|
||||
"文件列表选择",
|
||||
["space", "切换文件选择"],
|
||||
@@ -221,6 +222,7 @@ Ls.chi = {
|
||||
"cl_hpick": "在下方文件列表中点击某列表头即可从表中隐去该列",
|
||||
"cl_hcancel": "列隐藏操作已中止",
|
||||
"cl_rcm": "右键菜单",
|
||||
"cl_gauto": "自动网格", //m
|
||||
|
||||
"ct_grid": '田 网格',
|
||||
"ct_ttips": '◔ ◡ ◔">ℹ️ 提示',
|
||||
@@ -275,6 +277,8 @@ Ls.chi = {
|
||||
"tt_dynt": "自动随着目录树展开而变宽",
|
||||
"tt_wrap": "自动换行",
|
||||
"tt_hover": "悬停时完整显示出写不下的文字$N(启用后,鼠标光标只有$N 位于左边线上才滚得动)",
|
||||
"tt_gauto": "根据文件夹内容以网格或列表显示", //m
|
||||
"tt_gathr": "当此比例的文件为图片/视频时使用网格", //m
|
||||
|
||||
"ml_pmode": "文件夹播完后",
|
||||
"ml_btns": "命令",
|
||||
@@ -570,9 +574,10 @@ Ls.chi = {
|
||||
"u_https1": "你应该",
|
||||
"u_https2": "切换到 https",
|
||||
"u_https3": "以获得更好的性能",
|
||||
"u_ancient": '你还在用远古浏览器,也许你应该 <a href="#" onclick="goto(\'bup\')">改用 bup</a>',
|
||||
"u_ancient": '你还在用远古浏览器,也许你应该 <a href="#" id="u2nah">改用 bup</a>',
|
||||
"u_nowork": "需要 Firefox 53+ 或 Chrome 57+ 或 iOS 11+",
|
||||
"tail_2old": "需要 Firefox 105+ 或 Chrome 71+ 或 iOS 14.5+",
|
||||
"b2old": '浏览器版本太低', //m
|
||||
"u_nodrop": '浏览器版本低,不支持通过拖动文件到窗口来上传文件',
|
||||
"u_notdir": "这不是文件夹!\n\n你的浏览器太老了,\n请尝试将文件夹拖入窗口",
|
||||
"u_uri": "要从其他浏览器窗口拖放图片,\n请将其拖放到大上传按钮上",
|
||||
@@ -656,6 +661,7 @@ Ls.chi = {
|
||||
"rc_pla": "作为音频播放",
|
||||
"rc_txt": "在文本文件查看器中打开",
|
||||
"rc_md": "在 markdown 查看器中打开",
|
||||
"rc_wopi": "在 office 编辑器中打开", //m
|
||||
"rc_dl": "下载",
|
||||
"rc_zip": "下载为压缩包",
|
||||
"rc_cpl": "复制链接",
|
||||
|
||||
@@ -39,6 +39,7 @@ Ls.cze = {
|
||||
["ctrl-V", "vložit (přesunout/kopírovat) zde"],
|
||||
["Y", "stáhnout vybrané"],
|
||||
["F2", "přejmenovat vybrané"],
|
||||
["F4", "obnovit"], //m
|
||||
|
||||
"výběr souborů",
|
||||
["space", "přepnout výběr souboru"],
|
||||
@@ -225,6 +226,7 @@ Ls.cze = {
|
||||
"cl_hpick": "klepněte na záhlaví sloupců pro skrytí v tabulce níže",
|
||||
"cl_hcancel": "skrývání sloupců zrušeno",
|
||||
"cl_rcm": "kontextová nabídka", //m
|
||||
"cl_gauto": "auto mřížka", //m
|
||||
|
||||
"ct_grid": '田 mřížka',
|
||||
"ct_ttips": '◔ ◡ ◔">ℹ️ nápovědy',
|
||||
@@ -279,6 +281,8 @@ Ls.cze = {
|
||||
"tt_dynt": "automaticky rozrůstat jak se strom rozšiřuje",
|
||||
"tt_wrap": "zalomení řádků",
|
||||
"tt_hover": "odhalit přetékající řádky při najetí$N( ruší posun pokud kurzor myši $N 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_btns": "příkazy",
|
||||
@@ -573,9 +577,10 @@ Ls.cze = {
|
||||
"u_https1": "měli byste",
|
||||
"u_https2": "přejít na https",
|
||||
"u_https3": "pro lepší výkon",
|
||||
"u_ancient": "váš prohlížeč je úctyhodně starý -- možná byste měli <a href=\"#\" onclick=\"goto('bup')\">použít bup</a>",
|
||||
"u_ancient": "váš prohlížeč je úctyhodně starý -- možná byste měli <a href=\"#\" id=\"u2nah\">použít bup</a>",
|
||||
"u_nowork": "vyžadován firefox 53+ nebo chrome 57+ nebo iOS 11+",
|
||||
"tail_2old": "vyžadován firefox 105+ nebo chrome 71+ nebo iOS 14.5+",
|
||||
"b2old": "váš prohlížeč je příliš starý", //m
|
||||
"u_nodrop": "váš prohlížeč je příliš starý pro nahrávání přetažením (drag-and-drop)",
|
||||
"u_notdir": "toto není složka!\n\nváš prohlížeč je příliš starý,\nzkuste prosím soubory přetáhnout",
|
||||
"u_uri": "pro přetažení obrázků z jiných oken prohlížeče,\nje prosím přetáhněte na velké tlačítko pro nahrávání",
|
||||
@@ -659,6 +664,7 @@ Ls.cze = {
|
||||
"rc_pla": "přehrát jako zvuk", //m
|
||||
"rc_txt": "otevřít v prohlížeči souborů", //m
|
||||
"rc_md": "otevřít v textovém editoru", //m
|
||||
"rc_wopi": "otevřít v office editoru", //m
|
||||
"rc_dl": "stáhnout", //m
|
||||
"rc_zip": "stáhnout jako archiv", //m
|
||||
"rc_cpl": "kopírovat odkaz", //m
|
||||
|
||||
Loaded 100 of 191 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user