mirror of
https://github.com/9001/copyparty.git
synced 2025-10-02 14:42:28 -06:00
Merge branch 'tr_localization' of github.com:NandeMD/copyparty into tr_localization
This commit is contained in:
commit
24f5c39f59
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -43,3 +43,6 @@ scripts/docker/*.err
|
|||
|
||||
# nix build output link
|
||||
result
|
||||
|
||||
# IDEA config
|
||||
.idea/
|
||||
|
|
|
@ -6,13 +6,13 @@ but please:
|
|||
|
||||
|
||||
|
||||
# do not use AI / LMM when writing code
|
||||
# do not use AI / LLM when writing code
|
||||
|
||||
copyparty is 100% organic, free-range, human-written software!
|
||||
|
||||
> ⚠ you are now entering a no-copilot zone
|
||||
|
||||
the *only* place where LMM/AI *may* be accepted is for [localization](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) if you are fluent and have confirmed that the translation is accurate.
|
||||
the *only* place where LLM/AI *may* be accepted is for [localization](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) if you are fluent and have confirmed that the translation is accurate.
|
||||
|
||||
sorry for the harsh tone, but this is important to me 🙏
|
||||
|
||||
|
|
147
README.md
147
README.md
|
@ -90,7 +90,9 @@ made in Norway 🇳🇴
|
|||
* [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/))
|
||||
* [handlers](#handlers) - redefine behavior with plugins ([examples](./bin/handlers/))
|
||||
* [ip auth](#ip-auth) - autologin based on IP range (CIDR)
|
||||
* [restrict to ip](#restrict-to-ip) - limit a user to certain IP ranges (CIDR)
|
||||
* [identity providers](#identity-providers) - replace copyparty passwords with oauth and such
|
||||
* [generic header auth](#generic-header-auth) - other ways to auth by header
|
||||
* [user-changeable passwords](#user-changeable-passwords) - if permitted, users can change their own passwords
|
||||
* [using the cloud as storage](#using-the-cloud-as-storage) - connecting to an aws s3 bucket and similar
|
||||
* [hiding from google](#hiding-from-google) - tell search engines you don't wanna be indexed
|
||||
|
@ -110,6 +112,7 @@ made in Norway 🇳🇴
|
|||
* [packages](#packages) - the party might be closer than you think
|
||||
* [arch package](#arch-package) - `pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/))
|
||||
* [fedora package](#fedora-package) - does not exist yet
|
||||
* [homebrew formulae](#homebrew-formulae) - `brew install copyparty ffmpeg`
|
||||
* [nix package](#nix-package) - `nix profile install github:9001/copyparty`
|
||||
* [nixos module](#nixos-module)
|
||||
* [browser support](#browser-support) - TLDR: yes
|
||||
|
@ -139,6 +142,7 @@ made in Norway 🇳🇴
|
|||
* [copyparty.exe](#copypartyexe) - download [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) (win8+) or [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) (win7+)
|
||||
* [zipapp](#zipapp) - another emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz)
|
||||
* [install on android](#install-on-android)
|
||||
* [install on iOS](#install-on-iOS)
|
||||
* [reporting bugs](#reporting-bugs) - ideas for context to include, and where to submit them
|
||||
* [devnotes](#devnotes) - for build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md)
|
||||
|
||||
|
@ -153,6 +157,7 @@ just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/
|
|||
* or if you cannot install python, you can use [copyparty.exe](#copypartyexe) instead
|
||||
* or install [on arch](#arch-package) ╱ [on NixOS](#nixos-module) ╱ [through nix](#nix-package)
|
||||
* or if you are on android, [install copyparty in termux](#install-on-android)
|
||||
* or maybe an iPhone or iPad? [install in a-Shell on iOS](#install-on-iOS)
|
||||
* or maybe you have a [synology nas / dsm](./docs/synology-dsm.md)
|
||||
* or if you have [uv](https://docs.astral.sh/uv/) installed, run `uv tool run copyparty`
|
||||
* or if your computer is messed up and nothing else works, [try the pyz](#zipapp)
|
||||
|
@ -239,7 +244,7 @@ also see [comparison to similar software](./docs/versus.md)
|
|||
* ☑ [upnp / zeroconf / mdns / ssdp](#zeroconf)
|
||||
* ☑ [event hooks](#event-hooks) / script runner
|
||||
* ☑ [reverse-proxy support](https://github.com/9001/copyparty#reverse-proxy)
|
||||
* ☑ cross-platform (Windows, Linux, Macos, Android, FreeBSD, arm32/arm64, ppc64le, s390x, risc-v/riscv64)
|
||||
* ☑ cross-platform (Windows, Linux, Macos, Android, iOS, FreeBSD, arm32/arm64, ppc64le, s390x, risc-v/riscv64)
|
||||
* upload
|
||||
* ☑ basic: plain multipart, ie6 support
|
||||
* ☑ [up2k](#uploading): js, resumable, multithreaded
|
||||
|
@ -262,10 +267,11 @@ also see [comparison to similar software](./docs/versus.md)
|
|||
* ☑ play video files as audio (converted on server)
|
||||
* ☑ create and play [m3u8 playlists](#playlists)
|
||||
* ☑ image gallery with webm player
|
||||
* ☑ [textfile browser](#textfile-viewer) with syntax hilighting
|
||||
* ☑ [textfile browser](#textfile-viewer) with syntax highlighting
|
||||
* ☑ realtime streaming of growing files (logfiles and such)
|
||||
* ☑ [thumbnails](#thumbnails)
|
||||
* ☑ ...of images using Pillow, pyvips, or FFmpeg
|
||||
* ☑ ...of RAW images using rawpy
|
||||
* ☑ ...of videos using FFmpeg
|
||||
* ☑ ...of audio (spectrograms) using FFmpeg
|
||||
* ☑ cache eviction (max-age; maybe max-size eventually)
|
||||
|
@ -512,6 +518,8 @@ examples:
|
|||
* replacing the `g` permission with `wg` would let anonymous users upload files, but not see the required filekey to access it
|
||||
* replacing the `g` permission with `wG` would let anonymous users upload files, receiving a working direct link in return
|
||||
|
||||
if you want to grant access to all users who are logged in, the group `acct` will always contain all known users, so for example `-v /mnt/music:music:r,@acct`
|
||||
|
||||
anyone trying to bruteforce a password gets banned according to `--ban-pw`; default is 24h ban for 9 failed attempts in 1 hour
|
||||
|
||||
and if you want to use config files instead of commandline args (good!) then here's the same examples as a configfile; save it as `foobar.conf` and use it like this: `python copyparty-sfx.py -c foobar.conf`
|
||||
|
@ -537,6 +545,7 @@ and if you want to use config files instead of commandline args (good!) then her
|
|||
accs:
|
||||
r: u1, u2 # only these accounts can read,
|
||||
r: @g1 # (exactly the same, just with a group instead)
|
||||
r: @acct # (alternatively, ALL users who are logged in)
|
||||
rw: u3 # and only u3 can read-write
|
||||
|
||||
[/inc]
|
||||
|
@ -563,6 +572,8 @@ for example `-v /mnt::r -v /var/empty:web/certs:r` mounts the server folder `/mn
|
|||
|
||||
the example config file right above this section may explain this better; the first volume `/` is mapped to `/srv` which means http://127.0.0.1:3923/music would try to read `/srv/music` on the server filesystem, but since there's another volume at `/music` mapped to `/mnt/music` then it'll go to `/mnt/music` instead
|
||||
|
||||
> ℹ️ this also works for single files, because files can also be volumes
|
||||
|
||||
|
||||
## dotfiles
|
||||
|
||||
|
@ -825,7 +836,7 @@ the up2k UI is the epitome of polished intuitive experiences:
|
|||
* `[🔎]` switch between upload and [file-search](#file-search) mode
|
||||
* ignore `[🔎]` if you add files by dragging them into the browser
|
||||
|
||||
and then theres the tabs below it,
|
||||
and then there's the tabs below it,
|
||||
* `[ok]` is the files which completed successfully
|
||||
* `[ng]` is the ones that failed / got rejected (already exists, ...)
|
||||
* `[done]` shows a combined list of `[ok]` and `[ng]`, chronological order
|
||||
|
@ -1057,7 +1068,7 @@ plays almost every audio format there is (if the server has FFmpeg installed fo
|
|||
|
||||
the following audio formats are usually always playable, even without FFmpeg: `aac|flac|m4a|mp3|ogg|opus|wav`
|
||||
|
||||
some hilights:
|
||||
some highlights:
|
||||
* OS integration; control playback from your phone's lockscreen ([windows](https://user-images.githubusercontent.com/241032/233213022-298a98ba-721a-4cf1-a3d4-f62634bc53d5.png) // [iOS](https://user-images.githubusercontent.com/241032/142711926-0700be6c-3e31-47b3-9928-53722221f722.png) // [android](https://user-images.githubusercontent.com/241032/233212311-a7368590-08c7-4f9f-a1af-48ccf3f36fad.png))
|
||||
* shows the audio waveform in the seekbar
|
||||
* not perfectly gapless but can get really close (see settings + eq below); good enough to enjoy gapless albums as intended
|
||||
|
@ -1288,6 +1299,12 @@ print a qr-code [(screenshot)](https://user-images.githubusercontent.com/241032/
|
|||
* `--qrl lootbox/?pw=hunter2` appends to the url, linking to the `lootbox` folder with password `hunter2`
|
||||
* `--qrz 1` forces 1x zoom instead of autoscaling to fit the terminal size
|
||||
* 1x may render incorrectly on some terminals/fonts, but 2x should always work
|
||||
* `--qr-pin 1` makes the qr-code stick to the bottom of the console (never scrolls away)
|
||||
* `--qr-file qr.txt:1:2` writes a small qr-code to `qr.txt`
|
||||
* `--qr-file qr.txt:2:2` writes a big qr-code to `qr.txt`
|
||||
* `--qr-file qr.svg:1:2` writes a vector-graphics qr-code to `qr.svg`
|
||||
* `--qr-file qr.png:8:4:333333:ffcc55` writes an 8x-magnified yellow-on-gray `qr.png`
|
||||
* `--qr-file qr.png:8:4::ffffff` writes an 8x-magnified white-on-transparent `qr.png`
|
||||
|
||||
it uses the server hostname if [mdns](#mdns) is enabled, otherwise it'll use your external ip (default route) unless `--qri` specifies a specific ip-prefix or domain
|
||||
|
||||
|
@ -1311,6 +1328,16 @@ some recommended FTP / FTPS clients; `wark` = example password:
|
|||
* https://rclone.org/ does FTPS with `tls=false explicit_tls=true`
|
||||
* `lftp -u k,wark -p 3921 127.0.0.1 -e ls`
|
||||
* `lftp -u k,wark -p 3990 127.0.0.1 -e 'set ssl:verify-certificate no; ls'`
|
||||
* `curl ftp://127.0.0.1:3921/` (plaintext ftp)
|
||||
* `curl --ssl-reqd ftp://127.0.0.1:3990/` (encrypted ftps)
|
||||
|
||||
config file example, which restricts FTP to only use ports 3921 and 12000-12099 so all of those ports must be opened in your firewall:
|
||||
|
||||
```yaml
|
||||
[global]
|
||||
ftp: 3921
|
||||
ftp-pr: 12000-12099
|
||||
```
|
||||
|
||||
|
||||
## webdav server
|
||||
|
@ -1406,6 +1433,7 @@ and some minor issues,
|
|||
* win10 onwards does not allow connecting anonymously / without accounts
|
||||
* python3 only
|
||||
* slow (the builtin webdav support in windows is 5x faster, and rclone-webdav is 30x faster)
|
||||
* those numbers are specifically for copyparty's smb-server (because it sucks); other smb-servers should be similar to webdav
|
||||
|
||||
known client bugs:
|
||||
* on win7 only, `--smb1` is much faster than smb2 (default) because it keeps rescanning folders on smb2
|
||||
|
@ -1892,6 +1920,20 @@ repeat the option to map additional subnets
|
|||
**be careful with this one!** if you have a reverseproxy, then you definitely want to make sure you have [real-ip](#real-ip) configured correctly, and it's probably a good idea to nullmap the reverseproxy's IP just in case; so if your reverseproxy is sending requests from `172.24.27.9` then that would be `--ipu=172.24.27.9/32=`
|
||||
|
||||
|
||||
### restrict to ip
|
||||
|
||||
limit a user to certain IP ranges (CIDR) , using the global-option `--ipr`
|
||||
|
||||
for example, if the user `spartacus` should get rejected if they're not connecting from an IP that starts with `192.168.123` or `172.16`, then you can either specify `--ipr=192.168.123.0/24,172.16.0.0/16=spartacus` as a commandline option, or put this in a config file:
|
||||
|
||||
```yaml
|
||||
[global]
|
||||
ipr: 192.168.123.0/24,172.16.0.0/16=spartacus
|
||||
```
|
||||
|
||||
repeat the option to map additional users
|
||||
|
||||
|
||||
## identity providers
|
||||
|
||||
replace copyparty passwords with oauth and such
|
||||
|
@ -1900,6 +1942,10 @@ you can disable the built-in password-based login system, and instead replace it
|
|||
|
||||
* the regular config-defined users will be used as a fallback for requests which don't include a valid (trusted) IdP username header
|
||||
|
||||
* `--auth-ord` configured auth precedence, for example to allow overriding the IdP with a copyparty password
|
||||
|
||||
* the login/logout links/buttons can be replaced with links to your IdP with `--idp-login` and `--idp-logout` , for example `--idp-login /idp/login/?redir={dst}` will expand `{dst}` to the page the user was on when clicking Login
|
||||
|
||||
* if your IdP-server is slow, consider `--idp-cookie` and let requests with the cookie `cppws` bypass the IdP; experimental sessions-based feature added for a party
|
||||
|
||||
some popular identity providers are [Authelia](https://www.authelia.com/) (config-file based) and [authentik](https://goauthentik.io/) (GUI-based, more complex)
|
||||
|
@ -1911,6 +1957,20 @@ a more complete example of the copyparty configuration options [look like this](
|
|||
but if you just want to let users change their own passwords, then you probably want [user-changeable passwords](#user-changeable-passwords) instead
|
||||
|
||||
|
||||
### generic header auth
|
||||
|
||||
other ways to auth by header
|
||||
|
||||
if you have a middleware which adds a header with a user identifier, for example tailscale's `Tailscale-User-Login: alice.m@forest.net` then you can automatically auth as `alice` by defining that mapping with `--idp-hm-usr '^Tailscale-User-Login^alice.m@forest.net^alice'` or the following config file:
|
||||
|
||||
```yaml
|
||||
[global]
|
||||
idp-hm-usr: ^Tailscale-User-Login^alice.m@forest.net^alice
|
||||
```
|
||||
|
||||
repeat the whole `idp-hm-usr` option to add more mappings
|
||||
|
||||
|
||||
## user-changeable passwords
|
||||
|
||||
if permitted, users can change their own passwords in the control-panel
|
||||
|
@ -2126,7 +2186,7 @@ when connecting the reverse-proxy to `127.0.0.1` instead (the basic and/or old-f
|
|||
|
||||
in summary, `haproxy > caddy > traefik > nginx > apache > lighttpd`, and use uds when possible (traefik does not support it yet)
|
||||
|
||||
* if these results are bullshit because my config exampels are bad, please submit corrections!
|
||||
* if these results are bullshit because my config examples are bad, please submit corrections!
|
||||
|
||||
|
||||
## permanent cloudflare tunnel
|
||||
|
@ -2262,6 +2322,7 @@ buggy feature? rip it out by setting any of the following environment variables
|
|||
| `PRTY_NO_SQLITE` | disable all database-related functionality (file indexing, metadata indexing, most file deduplication logic) |
|
||||
| `PRTY_NO_TLS` | disable native HTTPS support; if you still want to accept HTTPS connections then TLS must now be terminated by a reverse-proxy |
|
||||
| `PRTY_NO_TPOKE` | disable systemd-tmpfilesd avoider |
|
||||
| `PRTY_UNSAFE_STATE` | allow storing secrets into emergency-fallback locations |
|
||||
|
||||
example: `PRTY_NO_IFADDR=1 python3 copyparty-sfx.py`
|
||||
|
||||
|
@ -2297,6 +2358,15 @@ after installing, start either the system service or the user service and naviga
|
|||
does not exist yet; there are rumours that it is being packaged! keep an eye on this space...
|
||||
|
||||
|
||||
## homebrew formulae
|
||||
|
||||
`brew install copyparty ffmpeg` -- https://formulae.brew.sh/formula/copyparty
|
||||
|
||||
should work on all macs (both intel and apple silicon) and all relevant macos versions
|
||||
|
||||
the homebrew package is maintained by the homebrew team (thanks!)
|
||||
|
||||
|
||||
## nix package
|
||||
|
||||
`nix profile install github:9001/copyparty`
|
||||
|
@ -2310,7 +2380,7 @@ some recommended dependencies are enabled by default; [override the package](htt
|
|||
|
||||
## nixos module
|
||||
|
||||
for this setup, you will need a [flake-enabled](https://nixos.wiki/wiki/Flakes) installation of NixOS.
|
||||
for [flake-enabled](https://nixos.wiki/wiki/Flakes) installations of NixOS:
|
||||
|
||||
```nix
|
||||
{
|
||||
|
@ -2337,6 +2407,33 @@ for this setup, you will need a [flake-enabled](https://nixos.wiki/wiki/Flakes)
|
|||
}
|
||||
```
|
||||
|
||||
if you don't use a flake in your configuration, you can use other dependency management tools like [npins](https://github.com/andir/npins), [niv](https://github.com/nmattia/niv), or even plain [`fetchTarball`](https://nix.dev/manual/nix/stable/language/builtins#builtins-fetchTarball), like so:
|
||||
|
||||
```nix
|
||||
{ pkgs, ... }:
|
||||
|
||||
let
|
||||
# npins example, adjust for your setup. copyparty should be a path to the downloaded repo
|
||||
# for niv, just replace the npins folder import with the sources.nix file
|
||||
copyparty = (import ./npins).copyparty;
|
||||
|
||||
# or with fetchTarball:
|
||||
copyparty = fetchTarball "https://github.com/9001/copyparty/archive/hovudstraum.tar.gz";
|
||||
in
|
||||
|
||||
{
|
||||
# load the copyparty NixOS module
|
||||
imports = [ "${copyparty}/contrib/nixos/modules/copyparty.nix" ];
|
||||
|
||||
# add the copyparty overlay to expose the package to the module
|
||||
nixpkgs.overlays = [ (import "${copyparty}/contrib/package/nix/overlay.nix") ];
|
||||
# (optional) install the package globally
|
||||
environment.systemPackages = [ pkgs.copyparty ];
|
||||
# configure the copyparty module
|
||||
services.copyparty.enable = true;
|
||||
}
|
||||
```
|
||||
|
||||
copyparty on NixOS is configured via `services.copyparty` options, for example:
|
||||
```nix
|
||||
services.copyparty = {
|
||||
|
@ -2523,11 +2620,20 @@ sync folders to/from copyparty
|
|||
|
||||
NOTE: full bidirectional sync, like what [nextcloud](https://docs.nextcloud.com/server/latest/user_manual/sv/files/desktop_mobile_sync.html) and [syncthing](https://syncthing.net/) does, will never be supported! Only single-direction sync (server-to-client, or client-to-server) is possible with copyparty
|
||||
|
||||
* if you want bidirectional sync, then copyparty and syncthing *should* be entirely safe to combine; they should be able to collaborate on the same folders without causing any trouble for eachother. Many people do this, and there have been no issues so far. But, if you *do* encounter any problems, please [file a copyparty bug](https://github.com/9001/copyparty/issues/new/choose) and I'll try to help -- just keep in mind I've never used syncthing before :-)
|
||||
|
||||
the commandline uploader [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy) with `--dr` is the best way to sync a folder to copyparty; verifies checksums and does files in parallel, and deletes unexpected files on the server after upload has finished which makes file-renames really cheap (it'll rename serverside and skip uploading)
|
||||
|
||||
if you want to sync with `u2c.py` then:
|
||||
* the `e2dsa` option (either globally or volflag) must be enabled on the server for the volumes you're syncing into
|
||||
* ...but DON'T enable global-options `no-hash` or `no-idx` (or volflags `nohash` / `noidx`), or at least make sure they are configured so they do not affect anything you are syncing into
|
||||
* ...and u2c needs the delete-permission, so either `rwd` at minimum, or just `A` which is the same as `rwmd.a`
|
||||
* quick reminder that `a` and `A` are different permissions, and `.` is very useful for sync
|
||||
|
||||
alternatively there is [rclone](./docs/rclone.md) which allows for bidirectional sync and is *way* more flexible (stream files straight from sftp/s3/gcs to copyparty, ...), although there is no integrity check and it won't work with files over 100 MiB if copyparty is behind cloudflare
|
||||
|
||||
* starting from rclone v1.63, rclone is faster than u2c.py on low-latency connections
|
||||
* but this is only true for the initial upload; u2c will be faster for periodic syncing
|
||||
|
||||
|
||||
## mount as drive
|
||||
|
@ -2569,6 +2675,8 @@ there is no iPhone app, but the following shortcuts are almost as good:
|
|||
* can download links and rehost the target file on copyparty (see first comment inside the shortcut)
|
||||
* pics become lowres if you share from gallery to shortcut, so better to launch the shortcut and pick stuff from there
|
||||
|
||||
if you want to run the copyparty server on your iPhone or iPad, see [install on iOS](#install-on-iOS)
|
||||
|
||||
|
||||
# performance
|
||||
|
||||
|
@ -2792,9 +2900,10 @@ enable [music tags](#metadata-from-audio-files):
|
|||
enable [thumbnails](#thumbnails) of...
|
||||
* **images:** `Pillow` and/or `pyvips` and/or `ffmpeg` (requires py2.7 or py3.5+)
|
||||
* **videos/audio:** `ffmpeg` and `ffprobe` somewhere in `$PATH`
|
||||
* **HEIF pictures:** `pyvips` or `ffmpeg` or `pyheif-pillow-opener` (requires Linux or a C compiler)
|
||||
* **HEIF pictures:** `pyvips` or `ffmpeg` or `pillow-heif`
|
||||
* **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin` or pillow v11.3+
|
||||
* **JPEG XL pictures:** `pyvips` or `ffmpeg`
|
||||
* **RAW images:** `rawpy`, plus one of `pyvips` or `Pillow` (for some formats)
|
||||
|
||||
enable sending [zeromq messages](#zeromq) from event-hooks: `pyzmq`
|
||||
|
||||
|
@ -2825,9 +2934,10 @@ set any of the following environment variables to disable its associated optiona
|
|||
| `PRTY_NO_PIL` | disable all [Pillow](https://pypi.org/project/pillow/)-based thumbnail support; will fallback to libvips or ffmpeg |
|
||||
| `PRTY_NO_PILF` | disable Pillow `ImageFont` text rendering, used for folder thumbnails |
|
||||
| `PRTY_NO_PIL_AVIF` | disable Pillow avif support (internal and/or [plugin](https://pypi.org/project/pillow-avif-plugin/)) |
|
||||
| `PRTY_NO_PIL_HEIF` | disable 3rd-party Pillow plugin for [HEIF support](https://pypi.org/project/pyheif-pillow-opener/) |
|
||||
| `PRTY_NO_PIL_HEIF` | disable 3rd-party Pillow plugin for [HEIF support](https://pypi.org/project/pillow-heif/) |
|
||||
| `PRTY_NO_PIL_WEBP` | disable use of native webp support in Pillow |
|
||||
| `PRTY_NO_PSUTIL` | do not use [psutil](https://pypi.org/project/psutil/) for reaping stuck hooks and plugins on Windows |
|
||||
| `PRTY_NO_RAW` | disable all [rawpy](https://pypi.org/project/rawpy/)-based thumbnail support for RAW images |
|
||||
| `PRTY_NO_VIPS` | disable all [libvips](https://pypi.org/project/pyvips/)-based thumbnail support; will fallback to Pillow or ffmpeg |
|
||||
|
||||
example: `PRTY_NO_PIL=1 python3 copyparty-sfx.py`
|
||||
|
@ -2900,6 +3010,27 @@ if you want thumbnails (photos+videos) and you're okay with spending another 132
|
|||
* or if you want to use `vips` for photo-thumbs instead, `pkg install libvips && python -m pip install --user -U wheel && python -m pip install --user -U pyvips && (cd /data/data/com.termux/files/usr/lib/; ln -s libgobject-2.0.so{,.0}; ln -s libvips.so{,.42})`
|
||||
|
||||
|
||||
# install on iOS
|
||||
|
||||
first install one of the following:
|
||||
* [a-Shell mini](https://apps.apple.com/us/app/a-shell-mini/id1543537943) gives you the essential features
|
||||
* [a-Shell](https://apps.apple.com/us/app/a-shell/id1473805438) also enables audio transcoding and better thubmnails
|
||||
|
||||
and then copypaste the following command into `a-Shell`:
|
||||
|
||||
```sh
|
||||
curl https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh | sh
|
||||
```
|
||||
|
||||
what this does:
|
||||
* creates a basic [config file](#accounts-and-volumes) named `cpc` which you can edit with `vim cpc`
|
||||
* adds the command `cpp` to launch copyparty with that config file
|
||||
|
||||
known issues:
|
||||
* cannot run in the background; it needs to be on-screen to accept connections / uploads / downloads
|
||||
* the best way to exit copyparty is to swipe away the app
|
||||
|
||||
|
||||
# reporting bugs
|
||||
|
||||
ideas for context to include, and where to submit them
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# [`u2c.py`](u2c.py)
|
||||
* command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm)
|
||||
* file uploads, file-search, autoresume of aborted/broken uploads
|
||||
* sync local folder to server
|
||||
* [sync local folder to server](https://github.com/9001/copyparty/#folder-sync)
|
||||
* generally faster than browsers
|
||||
* if something breaks just restart it
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
# usage: ./bubbleparty.sh ./copyparty-sfx.py ....
|
||||
bwrap \
|
||||
--unshare-all \
|
||||
|
@ -9,7 +9,7 @@ bwrap \
|
|||
--dev-bind /dev /dev \
|
||||
--dir /tmp \
|
||||
--dir /var \
|
||||
--bind $(pwd) $(pwd) \
|
||||
--bind "$(pwd)" "$(pwd)" \
|
||||
--share-net \
|
||||
--die-with-parent \
|
||||
--file 11 /etc/passwd \
|
||||
|
|
|
@ -8,7 +8,7 @@ import sqlite3
|
|||
import argparse
|
||||
|
||||
DB_VER1 = 3
|
||||
DB_VER2 = 5
|
||||
DB_VER2 = 6
|
||||
|
||||
BY_PATH = None
|
||||
NC = None
|
||||
|
@ -39,7 +39,7 @@ def ls(db):
|
|||
print(f"{nfiles} files")
|
||||
print(f"{ntags} tags\n")
|
||||
|
||||
print("number of occurences for each tag,")
|
||||
print("number of occurrences for each tag,")
|
||||
print(" 'x' = file has no tags")
|
||||
print(" 't:mtp' = the mtp flag (file not mtp processed yet)")
|
||||
print()
|
||||
|
|
|
@ -46,7 +46,7 @@ def main(cli, vn, rem):
|
|||
|
||||
# uncomment one of these:
|
||||
send_http_302_temporary_redirect(cli, new_path)
|
||||
#send_http_301_permanent_redirect(cli, new_path)
|
||||
#send_errorpage_with_redirect_link(cli, new_path)
|
||||
# send_http_301_permanent_redirect(cli, new_path)
|
||||
# send_errorpage_with_redirect_link(cli, new_path)
|
||||
|
||||
return "true"
|
||||
|
|
|
@ -9,7 +9,7 @@ from plyer import notification
|
|||
_ = r"""
|
||||
show os notification on upload; works on windows, linux, macos, android
|
||||
|
||||
depdencies:
|
||||
dependencies:
|
||||
windows: python3 -m pip install --user -U plyer
|
||||
linux: python3 -m pip install --user -U plyer
|
||||
macos: python3 -m pip install --user -U plyer pyobjus
|
||||
|
|
|
@ -66,7 +66,7 @@ def main():
|
|||
try:
|
||||
sp.check_call(cmd)
|
||||
except:
|
||||
t = "-- FAILED TO DONWLOAD " + name
|
||||
t = "-- FAILED TO DOWNLOAD " + name
|
||||
print(f"{t}\n", end="")
|
||||
open(t, "wb").close()
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ example copyparty config to use this:
|
|||
--urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=guestbook=t10,ad,p,bin/mtag/guestbook-read.py:mte=+guestbook
|
||||
|
||||
explained:
|
||||
for realpath srv/hello (served at /hello), write-only for eveyrone,
|
||||
for realpath srv/hello (served at /hello), write-only for everyone,
|
||||
enable file analysis on upload (e2ts),
|
||||
use mtp plugin "bin/mtag/guestbook-read.py" to provide metadata tag "guestbook",
|
||||
do this on all uploads regardless of extension,
|
||||
|
|
|
@ -11,7 +11,7 @@ example copyparty config to use this:
|
|||
--urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=xgb=ebin,t10,ad,p,bin/mtag/guestbook.py:mte=+xgb
|
||||
|
||||
explained:
|
||||
for realpath srv/hello (served at /hello),write-only for eveyrone,
|
||||
for realpath srv/hello (served at /hello),write-only for everyone,
|
||||
enable file analysis on upload (e2ts),
|
||||
use mtp plugin "bin/mtag/guestbook.py" to provide metadata tag "xgb",
|
||||
do this on all uploads with the file extension "bin",
|
||||
|
|
|
@ -84,7 +84,7 @@ def main():
|
|||
# on success, delete the .bin file which contains the URL
|
||||
os.unlink(fp)
|
||||
except:
|
||||
open("-- FAILED TO DONWLOAD " + name, "wb").close()
|
||||
open("-- FAILED TO DOWNLOAD " + name, "wb").close()
|
||||
|
||||
os.unlink(tfn)
|
||||
print(url)
|
||||
|
|
|
@ -6,8 +6,8 @@ __copyright__ = 2019
|
|||
__license__ = "MIT"
|
||||
__url__ = "https://github.com/9001/copyparty/"
|
||||
|
||||
S_VERSION = "2.0"
|
||||
S_BUILD_DT = "2024-10-01"
|
||||
S_VERSION = "2.1"
|
||||
S_BUILD_DT = "2025-09-06"
|
||||
|
||||
"""
|
||||
mount a copyparty server (local or remote) as a filesystem
|
||||
|
@ -99,7 +99,7 @@ except:
|
|||
elif MACOS:
|
||||
libfuse = "install https://osxfuse.github.io/"
|
||||
else:
|
||||
libfuse = "apt install libfuse3-3\n modprobe fuse"
|
||||
libfuse = "apt install libfuse2\n modprobe fuse"
|
||||
|
||||
m = """\033[33m
|
||||
could not import fuse; these may help:
|
||||
|
@ -359,7 +359,7 @@ class Gateway(object):
|
|||
def sendreq(self, meth, path, headers, **kwargs):
|
||||
tid = get_tid()
|
||||
if self.password:
|
||||
headers["Cookie"] = "=".join(["cppwd", self.password])
|
||||
headers["PW"] = self.password
|
||||
|
||||
try:
|
||||
c = self.getconn(tid)
|
||||
|
@ -902,9 +902,7 @@ class CPPF(Operations):
|
|||
return ret
|
||||
|
||||
def _readdir(self, path, fh=None):
|
||||
path = path.strip("/")
|
||||
dbg("readdir %r [%s]", path, fh)
|
||||
|
||||
dbg("dircache miss")
|
||||
ret = self.gw.listdir(path)
|
||||
if not self.n_dircache:
|
||||
return ret
|
||||
|
@ -914,11 +912,17 @@ class CPPF(Operations):
|
|||
self.dircache.append(cn)
|
||||
self.clean_dircache()
|
||||
|
||||
# import pprint; pprint.pprint(ret)
|
||||
return ret
|
||||
|
||||
def readdir(self, path, fh=None):
|
||||
return [".", ".."] + list(self._readdir(path, fh))
|
||||
dbg("readdir %r [%s]", path, fh)
|
||||
path = path.strip("/")
|
||||
cn = self.get_cached_dir(path)
|
||||
if cn:
|
||||
ret = cn.data
|
||||
else:
|
||||
ret = self._readdir(path, fh)
|
||||
return [".", ".."] + list(ret)
|
||||
|
||||
def read(self, path, length, offset, fh=None):
|
||||
req_max = 1024 * 1024 * 8
|
||||
|
@ -993,7 +997,6 @@ class CPPF(Operations):
|
|||
if cn:
|
||||
dents = cn.data
|
||||
else:
|
||||
dbg("cache miss")
|
||||
dents = self._readdir(dirpath)
|
||||
|
||||
try:
|
||||
|
@ -1141,10 +1144,15 @@ def main():
|
|||
if WINDOWS:
|
||||
examples.append("http://192.168.1.69:3923/music/ M:")
|
||||
|
||||
epi = "example:" + ex_pre + ex_pre.join(examples)
|
||||
epi += """\n
|
||||
NOTE: if server has --usernames enabled, then password is "username:password"
|
||||
"""
|
||||
|
||||
ap = argparse.ArgumentParser(
|
||||
formatter_class=TheArgparseFormatter,
|
||||
description="mount a copyparty server as a local filesystem -- " + ver,
|
||||
epilog="example:" + ex_pre + ex_pre.join(examples),
|
||||
epilog=epi,
|
||||
)
|
||||
# fmt: off
|
||||
ap.add_argument("base_url", type=str, help="remote copyparty URL to mount")
|
||||
|
|
|
@ -141,7 +141,7 @@ chmod 777 "$jail/tmp"
|
|||
|
||||
|
||||
# create a dev
|
||||
(cd $jail; mkdir -p dev; cd dev
|
||||
(cd "$jail"; mkdir -p dev; cd dev
|
||||
[ -e null ] || mknod -m 666 null c 1 3
|
||||
[ -e zero ] || mknod -m 666 zero c 1 5
|
||||
[ -e random ] || mknod -m 444 random c 1 8
|
||||
|
|
18
bin/u2c.py
18
bin/u2c.py
|
@ -1,8 +1,8 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
S_VERSION = "2.11"
|
||||
S_BUILD_DT = "2025-05-18"
|
||||
S_VERSION = "2.13"
|
||||
S_BUILD_DT = "2025-09-05"
|
||||
|
||||
"""
|
||||
u2c.py: upload to copyparty
|
||||
|
@ -10,7 +10,7 @@ u2c.py: upload to copyparty
|
|||
https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py
|
||||
|
||||
- dependencies: no
|
||||
- supports python 2.6, 2.7, and 3.3 through 3.12
|
||||
- supports python 2.6, 2.7, and 3.3 through 3.14
|
||||
- if something breaks just try again and it'll autoresume
|
||||
"""
|
||||
|
||||
|
@ -590,9 +590,10 @@ def undns(url):
|
|||
|
||||
def _scd(err, top):
|
||||
"""non-recursive listing of directory contents, along with stat() info"""
|
||||
top_ = os.path.join(top, b"")
|
||||
with os.scandir(top) as dh:
|
||||
for fh in dh:
|
||||
abspath = os.path.join(top, fh.name)
|
||||
abspath = top_ + fh.name
|
||||
try:
|
||||
yield [abspath, fh.stat()]
|
||||
except Exception as ex:
|
||||
|
@ -601,8 +602,9 @@ def _scd(err, top):
|
|||
|
||||
def _lsd(err, top):
|
||||
"""non-recursive listing of directory contents, along with stat() info"""
|
||||
top_ = os.path.join(top, b"")
|
||||
for name in os.listdir(top):
|
||||
abspath = os.path.join(top, name)
|
||||
abspath = top_ + name
|
||||
try:
|
||||
yield [abspath, os.stat(abspath)]
|
||||
except Exception as ex:
|
||||
|
@ -677,7 +679,7 @@ def walkdirs(err, tops, excl):
|
|||
yield stop, ap[len(stop) :].lstrip(sep), inf
|
||||
else:
|
||||
d, n = top.rsplit(sep, 1)
|
||||
yield d, n, os.stat(top)
|
||||
yield d or b"/", n, os.stat(top)
|
||||
|
||||
|
||||
# mostly from copyparty/util.py
|
||||
|
@ -1527,10 +1529,10 @@ def main():
|
|||
|
||||
# fmt: off
|
||||
ap = app = argparse.ArgumentParser(formatter_class=APF, description="copyparty up2k uploader / filesearch tool " + ver, epilog="""
|
||||
NOTE:
|
||||
source file/folder selection uses rsync syntax, meaning that:
|
||||
NOTE: source file/folder selection uses rsync syntax, meaning that:
|
||||
"foo" uploads the entire folder to URL/foo/
|
||||
"foo/" uploads the CONTENTS of the folder into URL/
|
||||
NOTE: if server has --usernames enabled, then password is "username:password"
|
||||
""")
|
||||
|
||||
ap.add_argument("url", type=unicode, help="server url, including destination folder")
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
# generate the list of permitted IP ranges like so:
|
||||
# (curl -s https://www.cloudflare.com/ips-v{4,6} | sed 's/^/allow /; s/$/;/'; echo; echo "deny all;") > /etc/nginx/cloudflare-only.conf
|
||||
#
|
||||
# and then enable it below by uncomenting the cloudflare-only.conf line
|
||||
# and then enable it below by uncommenting the cloudflare-only.conf line
|
||||
#
|
||||
# ======================================================================
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ let
|
|||
|
||||
configStr = ''
|
||||
${mkSection "global" cfg.settings}
|
||||
${cfg.globalExtraConfig}
|
||||
${mkSection "accounts" (accountsWithPlaceholders cfg.accounts)}
|
||||
${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)}
|
||||
'';
|
||||
|
@ -131,6 +132,12 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
globalExtraConfig = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "Appended to the end of the [global] section verbatim. This is useful for flags which are used in a repeating manner (e.g. ipu: 255.255.255.1=user) which can't be repeated in the settings = {} attribute set.";
|
||||
};
|
||||
|
||||
accounts = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
|
@ -373,3 +380,4 @@ in
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead.
|
||||
|
||||
pkgname=copyparty
|
||||
pkgver="1.19.0"
|
||||
pkgver="1.19.8"
|
||||
pkgrel=1
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||
arch=("any")
|
||||
|
@ -23,7 +23,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
|
|||
)
|
||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||
backup=("etc/${pkgname}/copyparty.conf" )
|
||||
sha256sums=("179b027d51e4fe7ebdab2b18c07475d52c57e2ce69256292b157a8efacd82118")
|
||||
sha256sums=("3143ba5216c8d4cf1fbc58fa08c6ecef955de04b5e34b3910ab0b71cffec88ef")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
|
||||
pkgname=copyparty
|
||||
pkgver=1.19.0
|
||||
pkgver=1.19.8
|
||||
pkgrel=1
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||
arch=("any")
|
||||
|
@ -20,7 +20,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
|
|||
)
|
||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||
backup=("/etc/${pkgname}.d/init" )
|
||||
sha256sums=("179b027d51e4fe7ebdab2b18c07475d52c57e2ce69256292b157a8efacd82118")
|
||||
sha256sums=("3143ba5216c8d4cf1fbc58fa08c6ecef955de04b5e34b3910ab0b71cffec88ef")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
lib,
|
||||
stdenv,
|
||||
makeWrapper,
|
||||
buildPythonApplication,
|
||||
fetchurl,
|
||||
util-linux,
|
||||
python,
|
||||
setuptools,
|
||||
jinja2,
|
||||
impacket,
|
||||
pyopenssl,
|
||||
|
@ -15,6 +15,10 @@
|
|||
pyzmq,
|
||||
ffmpeg,
|
||||
mutagen,
|
||||
pyftpdlib,
|
||||
magic,
|
||||
partftpy,
|
||||
fusepy, # for partyfuse
|
||||
|
||||
# use argon2id-hashed passwords in config files (sha2 is always available)
|
||||
withHashedPasswords ? true,
|
||||
|
@ -40,12 +44,21 @@
|
|||
# send ZeroMQ messages from event-hooks
|
||||
withZeroMQ ? true,
|
||||
|
||||
# enable FTP server
|
||||
withFTP ? true,
|
||||
|
||||
# enable FTPS support in the FTP server
|
||||
withFTPS ? false,
|
||||
|
||||
# enable TFTP server
|
||||
withTFTP ? false,
|
||||
|
||||
# samba/cifs server; dangerous and buggy, enable if you really need it
|
||||
withSMB ? false,
|
||||
|
||||
# enables filetype detection for nameless uploads
|
||||
withMagic ? false,
|
||||
|
||||
# extra packages to add to the PATH
|
||||
extraPackages ? [ ],
|
||||
|
||||
|
@ -58,14 +71,23 @@
|
|||
|
||||
let
|
||||
pinData = lib.importJSON ./pin.json;
|
||||
pyEnv = python.withPackages (
|
||||
ps:
|
||||
with ps;
|
||||
runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg);
|
||||
in
|
||||
buildPythonApplication {
|
||||
pname = "copyparty";
|
||||
inherit (pinData) version;
|
||||
src = fetchurl {
|
||||
inherit (pinData) url hash;
|
||||
};
|
||||
dependencies =
|
||||
[
|
||||
jinja2
|
||||
fusepy
|
||||
]
|
||||
++ lib.optional withSMB impacket
|
||||
++ lib.optional withFTP pyftpdlib
|
||||
++ lib.optional withFTPS pyopenssl
|
||||
++ lib.optional withTFTP partftpy
|
||||
++ lib.optional withCertgen cfssl
|
||||
++ lib.optional withThumbnails pillow
|
||||
++ lib.optional withFastThumbnails pyvips
|
||||
|
@ -73,25 +95,14 @@ let
|
|||
++ lib.optional withBasicAudioMetadata mutagen
|
||||
++ lib.optional withHashedPasswords argon2-cffi
|
||||
++ lib.optional withZeroMQ pyzmq
|
||||
++ (extraPythonPackages ps)
|
||||
);
|
||||
++ lib.optional withMagic magic
|
||||
++ (extraPythonPackages python.pkgs);
|
||||
makeWrapperArgs = [ "--prefix PATH : ${lib.makeBinPath runtimeDeps}" ];
|
||||
|
||||
runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg);
|
||||
in
|
||||
stdenv.mkDerivation {
|
||||
pname = "copyparty";
|
||||
inherit (pinData) version;
|
||||
src = fetchurl {
|
||||
inherit (pinData) url hash;
|
||||
};
|
||||
nativeBuildInputs = [ makeWrapper ];
|
||||
dontUnpack = true;
|
||||
installPhase = ''
|
||||
install -Dm755 $src $out/share/copyparty-sfx.py
|
||||
makeWrapper ${pyEnv.interpreter} $out/bin/copyparty \
|
||||
--prefix PATH : ${lib.makeBinPath runtimeDeps} \
|
||||
--add-flag $out/share/copyparty-sfx.py
|
||||
'';
|
||||
pyproject = true;
|
||||
build-system = [
|
||||
setuptools
|
||||
];
|
||||
meta = {
|
||||
description = "Turn almost any device into a file server";
|
||||
longDescription = ''
|
||||
|
@ -101,8 +112,7 @@ stdenv.mkDerivation {
|
|||
homepage = "https://github.com/9001/copyparty";
|
||||
changelog = "https://github.com/9001/copyparty/releases/tag/v${pinData.version}";
|
||||
license = lib.licenses.mit;
|
||||
inherit (python.meta) platforms;
|
||||
mainProgram = "copyparty";
|
||||
sourceProvenance = [ lib.sourceTypes.binaryBytecode ];
|
||||
sourceProvenance = [ lib.sourceTypes.fromSource ];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.19.0/copyparty-sfx.py",
|
||||
"version": "1.19.0",
|
||||
"hash": "sha256-9A+zPtkVtUuGHB/JJV3fhVtJderLUGxHqvuJQz0/1+Q="
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.19.8/copyparty-1.19.8.tar.gz",
|
||||
"version": "1.19.8",
|
||||
"hash": "sha256-MUO6UhbI1M8fvFj6CMbs75Vd4EteNLORCrC3HP/siO8="
|
||||
}
|
|
@ -11,14 +11,14 @@ import base64
|
|||
import json
|
||||
import hashlib
|
||||
import sys
|
||||
import re
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
OUTPUT_FILE = Path("pin.json")
|
||||
TARGET_ASSET = "copyparty-sfx.py"
|
||||
TARGET_ASSET = lambda version: f"copyparty-{version}.tar.gz"
|
||||
HASH_TYPE = "sha256"
|
||||
LATEST_RELEASE_URL = "https://api.github.com/repos/9001/copyparty/releases/latest"
|
||||
DOWNLOAD_URL = lambda version: f"https://github.com/9001/copyparty/releases/download/v{version}/{TARGET_ASSET}"
|
||||
DOWNLOAD_URL = lambda version: f"https://github.com/9001/copyparty/releases/download/v{version}/{TARGET_ASSET(version)}"
|
||||
|
||||
|
||||
def get_formatted_hash(binary):
|
||||
|
@ -29,11 +29,13 @@ def get_formatted_hash(binary):
|
|||
return f"{HASH_TYPE}-{encoded_hash}"
|
||||
|
||||
|
||||
def version_from_sfx(binary):
|
||||
result = re.search(b'^VER = "(.*)"$', binary, re.MULTILINE)
|
||||
if result:
|
||||
return result.groups(1)[0].decode("ascii")
|
||||
def version_from_tar_gz(path):
|
||||
with tarfile.open(path) as tarball:
|
||||
release_name = tarball.getmembers()[0].name
|
||||
prefix = "copyparty-"
|
||||
|
||||
if release_name.startswith(prefix):
|
||||
return release_name.replace(prefix, "")
|
||||
raise ValueError("version not found in provided file")
|
||||
|
||||
|
||||
|
@ -42,7 +44,7 @@ def remote_release_pin():
|
|||
|
||||
response = requests.get(LATEST_RELEASE_URL).json()
|
||||
version = response["tag_name"].lstrip("v")
|
||||
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET][0]
|
||||
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET(version)][0]
|
||||
download_url = asset_info["browser_download_url"]
|
||||
asset = requests.get(download_url)
|
||||
formatted_hash = get_formatted_hash(asset.content)
|
||||
|
@ -52,10 +54,9 @@ def remote_release_pin():
|
|||
|
||||
|
||||
def local_release_pin(path):
|
||||
asset = path.read_bytes()
|
||||
version = version_from_sfx(asset)
|
||||
version = version_from_tar_gz(path)
|
||||
download_url = DOWNLOAD_URL(version)
|
||||
formatted_hash = get_formatted_hash(asset)
|
||||
formatted_hash = get_formatted_hash(path.read_bytes())
|
||||
|
||||
result = {"url": download_url, "version": version, "hash": formatted_hash}
|
||||
return result
|
||||
|
|
11
contrib/package/nix/overlay.nix
Normal file
11
contrib/package/nix/overlay.nix
Normal file
|
@ -0,0 +1,11 @@
|
|||
final: prev: {
|
||||
copyparty = final.python3.pkgs.callPackage ./copyparty {
|
||||
ffmpeg = final.ffmpeg-full;
|
||||
};
|
||||
|
||||
python3 = prev.python3.override {
|
||||
packageOverrides = pyFinal: pyPrev: {
|
||||
partftpy = pyFinal.callPackage ./partftpy { };
|
||||
};
|
||||
};
|
||||
}
|
30
contrib/package/nix/partftpy/default.nix
Normal file
30
contrib/package/nix/partftpy/default.nix
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchurl,
|
||||
setuptools,
|
||||
}:
|
||||
let
|
||||
pinData = lib.importJSON ./pin.json;
|
||||
in
|
||||
|
||||
buildPythonPackage rec {
|
||||
pname = "partftpy";
|
||||
inherit (pinData) version;
|
||||
pyproject = true;
|
||||
|
||||
src = fetchurl {
|
||||
inherit (pinData) url hash;
|
||||
};
|
||||
|
||||
build-system = [ setuptools ];
|
||||
|
||||
pythonImportsCheck = [ "partftpy.TftpServer" ];
|
||||
|
||||
meta = {
|
||||
description = "Pure Python TFTP library (copyparty edition)";
|
||||
homepage = "https://github.com/9001/partftpy";
|
||||
changelog = "https://github.com/9001/partftpy/releases/tag/${version}";
|
||||
license = lib.licenses.mit;
|
||||
};
|
||||
}
|
5
contrib/package/nix/partftpy/pin.json
Normal file
5
contrib/package/nix/partftpy/pin.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"url": "https://github.com/9001/partftpy/releases/download/v0.4.0/partftpy-0.4.0.tar.gz",
|
||||
"version": "0.4.0",
|
||||
"hash": "sha256-5Q2zyuJ892PGZmb+YXg0ZPW/DK8RDL1uE0j5HPd4We0="
|
||||
}
|
50
contrib/package/nix/partftpy/update.py
Executable file
50
contrib/package/nix/partftpy/update.py
Executable file
|
@ -0,0 +1,50 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Update the Nix package pin
|
||||
#
|
||||
# Usage: ./update.sh
|
||||
|
||||
import base64
|
||||
import json
|
||||
import hashlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
OUTPUT_FILE = Path("pin.json")
|
||||
TARGET_ASSET = lambda version: f"partftpy-{version}.tar.gz"
|
||||
HASH_TYPE = "sha256"
|
||||
LATEST_RELEASE_URL = "https://api.github.com/repos/9001/partftpy/releases/latest"
|
||||
|
||||
|
||||
def get_formatted_hash(binary):
|
||||
hasher = hashlib.new("sha256")
|
||||
hasher.update(binary)
|
||||
asset_hash = hasher.digest()
|
||||
encoded_hash = base64.b64encode(asset_hash).decode("ascii")
|
||||
return f"{HASH_TYPE}-{encoded_hash}"
|
||||
|
||||
|
||||
def remote_release_pin():
|
||||
import requests
|
||||
|
||||
response = requests.get(LATEST_RELEASE_URL).json()
|
||||
version = response["tag_name"].lstrip("v")
|
||||
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET(version)][0]
|
||||
download_url = asset_info["browser_download_url"]
|
||||
asset = requests.get(download_url)
|
||||
formatted_hash = get_formatted_hash(asset.content)
|
||||
|
||||
result = {"url": download_url, "version": version, "hash": formatted_hash}
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
result = remote_release_pin()
|
||||
|
||||
print(result)
|
||||
json_result = json.dumps(result, indent=4)
|
||||
OUTPUT_FILE.write_text(json_result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
stdenvNoCC,
|
||||
copyparty,
|
||||
python3,
|
||||
makeBinaryWrapper,
|
||||
}:
|
||||
let
|
||||
python = python3.withPackages (p: [ p.fusepy ]);
|
||||
in
|
||||
stdenvNoCC.mkDerivation {
|
||||
pname = "partyfuse";
|
||||
inherit (copyparty) version meta;
|
||||
src = ../../../..;
|
||||
|
||||
nativeBuildInputs = [ makeBinaryWrapper ];
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
install -Dm444 bin/partyfuse.py -t $out/share/copyparty
|
||||
makeWrapper ${python.interpreter} $out/bin/partyfuse \
|
||||
--add-flag $out/share/copyparty/partyfuse.py
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
{
|
||||
stdenvNoCC,
|
||||
copyparty,
|
||||
python312,
|
||||
makeBinaryWrapper,
|
||||
}:
|
||||
stdenvNoCC.mkDerivation {
|
||||
pname = "u2c";
|
||||
inherit (copyparty) version meta;
|
||||
src = ../../../..;
|
||||
|
||||
nativeBuildInputs = [ makeBinaryWrapper ];
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
install -Dm444 bin/u2c.py -t $out/share/copyparty
|
||||
mkdir $out/bin
|
||||
makeWrapper ${python312.interpreter} $out/bin/u2c \
|
||||
--add-flag $out/share/copyparty/u2c.py
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
}
|
62
contrib/package/rpm/copyparty.spec
Normal file
62
contrib/package/rpm/copyparty.spec
Normal file
|
@ -0,0 +1,62 @@
|
|||
Name: copyparty
|
||||
Version: $pkgver
|
||||
Release: $pkgrel
|
||||
License: MIT
|
||||
Group: Utilities
|
||||
URL: https://github.com/9001/copyparty
|
||||
Source0: copyparty-$pkgver.tar.gz
|
||||
Summary: File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++
|
||||
BuildArch: noarch
|
||||
BuildRequires: python3, python3-devel, pyproject-rpm-macros, python-setuptools, python-wheel, make
|
||||
Requires: python3, (python3-jinja2 or python-jinja2), lsof
|
||||
Recommends: ffmpeg, (golang-github-cloudflare-cfssl or cfssl), python-mutagen, python-pillow, python-pyvips
|
||||
Recommends: qm-vamp-plugins, python-argon2-cffi, (python-pyopenssl or pyopenssl), python-impacket
|
||||
|
||||
%description
|
||||
Portable file server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps
|
||||
|
||||
See release at https://github.com/9001/copyparty/releases
|
||||
|
||||
%global debug_package %{nil}
|
||||
|
||||
%generate_buildrequires
|
||||
%pyproject_buildrequires
|
||||
|
||||
%prep
|
||||
%setup -q
|
||||
|
||||
%build
|
||||
cd "copyparty/web"
|
||||
make
|
||||
cd -
|
||||
%pyproject_wheel
|
||||
|
||||
%install
|
||||
mkdir -p %{buildroot}%{_bindir}
|
||||
mkdir -p %{buildroot}%{_libdir}/systemd/{system,user}
|
||||
mkdir -p %{buildroot}/etc/%{name}
|
||||
mkdir -p %{buildroot}/var/lib/%{name}-jail
|
||||
mkdir -p %{buildroot}%{_datadir}/licenses/%{name}
|
||||
|
||||
%pyproject_install
|
||||
%pyproject_save_files copyparty
|
||||
|
||||
install -m 0755 bin/prisonparty.sh %{buildroot}%{_bindir}/prisonpary.sh
|
||||
install -m 0644 contrib/systemd/%{name}.conf %{buildroot}/etc/%{name}/%{name}.conf
|
||||
install -m 0644 contrib/systemd/%{name}@.service %{buildroot}%{_libdir}/systemd/system/%{name}@.service
|
||||
install -m 0644 contrib/systemd/%{name}-user.service %{buildroot}%{_libdir}/systemd/user/%{name}.service
|
||||
install -m 0644 contrib/systemd/prisonparty@.service %{buildroot}%{_libdir}/systemd/system/prisonparty@.service
|
||||
install -m 0644 contrib/systemd/index.md %{buildroot}/var/lib/%{name}-jail/README.md
|
||||
install -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE
|
||||
|
||||
%files -n copyparty -f %{pyproject_files}
|
||||
%license LICENSE
|
||||
%{_bindir}/copyparty
|
||||
%{_bindir}/partyfuse
|
||||
%{_bindir}/u2c
|
||||
%{_bindir}/prisonpary.sh
|
||||
/etc/%{name}/%{name}.conf
|
||||
%{_libdir}/systemd/system/%{name}@.service
|
||||
%{_libdir}/systemd/user/%{name}.service
|
||||
%{_libdir}/systemd/system/prisonparty@.service
|
||||
/var/lib/%{name}-jail/README.md
|
|
@ -101,10 +101,10 @@
|
|||
|
||||
function our_hotkey_handler(e) {
|
||||
// bail if either ALT, CTRL, or SHIFT is pressed
|
||||
if (e.altKey || e.shiftKey || e.isComposing || ctrl(e))
|
||||
if (anymod(e))
|
||||
return main_hotkey_handler(e); // let copyparty handle this keystroke
|
||||
|
||||
var key_name = (e.code || e.key) + '',
|
||||
var keycode = (e.key || e.code) + '',
|
||||
ae = document.activeElement,
|
||||
aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : '';
|
||||
|
||||
|
@ -114,7 +114,7 @@
|
|||
if (aet && !/^(a|button|tr|td|div|pre)$/.test(aet))
|
||||
return main_hotkey_handler(e); // let copyparty handle this keystroke
|
||||
|
||||
if (key_name == 'KeyW') {
|
||||
if (keycode == 'w' || keycode == 'KeyW') {
|
||||
// okay, this one's for us... do the thing
|
||||
action_to_perform();
|
||||
return ev(e);
|
||||
|
|
71
contrib/setup-ashell.sh
Normal file
71
contrib/setup-ashell.sh
Normal file
|
@ -0,0 +1,71 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# this script will install copyparty onto an iOS device (iPhone/iPad)
|
||||
#
|
||||
# step 1: install a-Shell:
|
||||
# https://apps.apple.com/us/app/a-shell/id1473805438
|
||||
#
|
||||
# step 2: copypaste the following command into a-Shell:
|
||||
# curl https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh
|
||||
#
|
||||
# step 3: launch copyparty with this command: cpp
|
||||
#
|
||||
# if you ever want to upgrade copyparty, just repeat step 2
|
||||
|
||||
|
||||
|
||||
cd "$HOME/Documents"
|
||||
curl -Locopyparty https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py
|
||||
|
||||
|
||||
|
||||
# create the config file? (cannot use heredoc because body too large)
|
||||
[ -e cpc ] || {
|
||||
echo '[global]' >cpc
|
||||
echo ' p: 80, 443, 3923 # enable http and https on these ports' >>cpc
|
||||
echo ' e2dsa # enable file indexing and filesystem scanning' >>cpc
|
||||
echo ' e2ts # and enable multimedia indexing' >>cpc
|
||||
echo ' ver # show copyparty version in the controlpanel' >>cpc
|
||||
echo ' qrz: 2 # enable qr-code and make it big' >>cpc
|
||||
echo ' qrp: 1 # reduce qr-code padding' >>cpc
|
||||
echo ' qr-fg: -1 # optimize for basic/simple terminals' >>cpc
|
||||
echo ' qr-wait: 0.3 # less chance of getting scrolled away' >>cpc
|
||||
echo '' >>cpc
|
||||
echo ' # enable these by uncommenting them:' >>cpc
|
||||
echo ' # ftp: 21 # enable ftp server on port 21' >>cpc
|
||||
echo ' # tftp: 69 # enable tftp server on port 69' >>cpc
|
||||
echo '' >>cpc
|
||||
echo '[/]' >>cpc
|
||||
echo ' ~/Documents' >>cpc
|
||||
echo ' accs:' >>cpc
|
||||
echo ' A: *' >>cpc
|
||||
}
|
||||
|
||||
|
||||
|
||||
# create the launcher?
|
||||
[ -e cpp ] || {
|
||||
echo '#!/bin/sh' >cpp
|
||||
echo '' >>cpp
|
||||
echo '# change the font so the qr-code draws correctly:' >>cpp
|
||||
echo 'config -n "Menlo" # name' >>cpp
|
||||
echo 'config -s 8 # size' >>cpp
|
||||
echo '' >>cpp
|
||||
echo '# launch copyparty' >>cpp
|
||||
echo 'exec copyparty -c cpc "$@"' >>cpp
|
||||
}
|
||||
|
||||
|
||||
|
||||
chmod 755 copyparty cpp
|
||||
echo
|
||||
echo =================================
|
||||
echo
|
||||
echo 'okay, all done!'
|
||||
echo
|
||||
echo 'you can edit your config'
|
||||
echo 'with this command: vim cpc'
|
||||
echo
|
||||
echo 'you can run copyparty'
|
||||
echo 'with this command: cpp'
|
||||
echo
|
|
@ -111,7 +111,9 @@ class EnvParams(object):
|
|||
def __init__(self) -> None:
|
||||
self.t0 = time.time()
|
||||
self.mod = ""
|
||||
self.mod_ = ""
|
||||
self.cfg = ""
|
||||
self.scfg = True
|
||||
|
||||
|
||||
E = EnvParams()
|
||||
|
|
|
@ -36,6 +36,7 @@ from .__init__ import (
|
|||
)
|
||||
from .__version__ import CODENAME, S_BUILD_DT, S_VERSION
|
||||
from .authsrv import expand_config_file, split_cfg_ln, upgrade_cfg_fmt
|
||||
from .bos import bos
|
||||
from .cfg import flagcats, onedash
|
||||
from .svchub import SvcHub
|
||||
from .util import (
|
||||
|
@ -186,7 +187,7 @@ def init_E(EE: EnvParams) -> None:
|
|||
|
||||
E = EE # pylint: disable=redefined-outer-name
|
||||
|
||||
def get_unixdir() -> str:
|
||||
def get_unixdir() -> tuple[str, bool]:
|
||||
paths: list[tuple[Callable[..., Any], str]] = [
|
||||
(os.environ.get, "XDG_CONFIG_HOME"),
|
||||
(os.path.expanduser, "~/.config"),
|
||||
|
@ -196,41 +197,57 @@ def init_E(EE: EnvParams) -> None:
|
|||
(unicode, "/tmp"),
|
||||
]
|
||||
errs = []
|
||||
for chk in [os.listdir, os.mkdir]:
|
||||
for npath, (pf, pa) in enumerate(paths):
|
||||
p = ""
|
||||
for npath, (pf, pa) in enumerate(paths):
|
||||
priv = npath < 2 # private/trusted location
|
||||
ram = npath > 1 # "nonvolatile"; not semantically same as `not priv`
|
||||
p = ""
|
||||
try:
|
||||
p = pf(pa)
|
||||
if not p or p.startswith("~"):
|
||||
continue
|
||||
|
||||
p = os.path.normpath(p)
|
||||
mkdir = not os.path.isdir(p)
|
||||
if mkdir:
|
||||
os.mkdir(p, 0o700)
|
||||
|
||||
p = os.path.join(p, "copyparty")
|
||||
if not priv and os.path.isdir(p):
|
||||
uid = os.geteuid()
|
||||
if os.stat(p).st_uid != uid:
|
||||
p += ".%s" % (uid,)
|
||||
if os.path.isdir(p) and os.stat(p).st_uid != uid:
|
||||
raise Exception("filesystem has broken unix permissions")
|
||||
try:
|
||||
p = pf(pa)
|
||||
# print(chk.__name__, p, pa)
|
||||
if not p or p.startswith("~"):
|
||||
continue
|
||||
os.listdir(p)
|
||||
except:
|
||||
os.mkdir(p, 0o700)
|
||||
|
||||
p = os.path.normpath(p)
|
||||
chk(p) # type: ignore
|
||||
p = os.path.join(p, "copyparty")
|
||||
if not os.path.isdir(p):
|
||||
os.mkdir(p)
|
||||
if ram:
|
||||
t = "Using %s/copyparty [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/"
|
||||
errs.append(t % (pa, p))
|
||||
elif mkdir:
|
||||
t = "Using %s/copyparty [%s] for config%s (Warning: %s did not exist and was created just now)"
|
||||
errs.append(t % (pa, p, " instead" if npath else "", pa))
|
||||
elif errs:
|
||||
errs.append("Using %s/copyparty [%s] instead" % (pa, p))
|
||||
|
||||
if npath > 1:
|
||||
t = "Using [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/"
|
||||
errs.append(t % (p,))
|
||||
elif errs:
|
||||
errs.append("Using [%s] instead" % (p,))
|
||||
if errs:
|
||||
warn(". ".join(errs))
|
||||
|
||||
if errs:
|
||||
warn(". ".join(errs))
|
||||
return p, priv
|
||||
except Exception as ex:
|
||||
if p:
|
||||
t = "Unable to store config in %s [%s] due to %r"
|
||||
errs.append(t % (pa, p, ex))
|
||||
|
||||
return p # type: ignore
|
||||
except Exception as ex:
|
||||
if p and npath < 2:
|
||||
t = "Unable to store config in [%s] due to %r"
|
||||
errs.append(t % (p, ex))
|
||||
|
||||
raise Exception("could not find a writable path for config")
|
||||
t = "could not find a writable path for runtime state:\n> %s"
|
||||
raise Exception(t % ("\n> ".join(errs)))
|
||||
|
||||
E.mod = os.path.dirname(os.path.realpath(__file__))
|
||||
if E.mod.endswith("__init__"):
|
||||
E.mod = os.path.dirname(E.mod)
|
||||
E.mod_ = os.path.join(E.mod, "")
|
||||
|
||||
try:
|
||||
p = os.environ.get("XDG_CONFIG_HOME")
|
||||
|
@ -241,7 +258,7 @@ def init_E(EE: EnvParams) -> None:
|
|||
p = os.path.abspath(os.path.realpath(p))
|
||||
p = os.path.join(p, "copyparty")
|
||||
if not os.path.isdir(p):
|
||||
os.mkdir(p)
|
||||
os.mkdir(p, 0o700)
|
||||
os.listdir(p)
|
||||
except:
|
||||
p = ""
|
||||
|
@ -254,11 +271,11 @@ def init_E(EE: EnvParams) -> None:
|
|||
elif sys.platform == "darwin":
|
||||
E.cfg = os.path.expanduser("~/Library/Preferences/copyparty")
|
||||
else:
|
||||
E.cfg = get_unixdir()
|
||||
E.cfg, E.scfg = get_unixdir()
|
||||
|
||||
E.cfg = E.cfg.replace("\\", "/")
|
||||
try:
|
||||
os.makedirs(E.cfg)
|
||||
bos.makedirs(E.cfg, bos.MKD_700)
|
||||
except:
|
||||
if not os.path.isdir(E.cfg):
|
||||
raise
|
||||
|
@ -436,6 +453,29 @@ def args_from_cfg(cfg_path: str) -> list[str]:
|
|||
return ret
|
||||
|
||||
|
||||
def expand_cfg(argv) -> list[str]:
|
||||
if CFG_DEF:
|
||||
supp = args_from_cfg(CFG_DEF[0])
|
||||
argv = argv[:1] + supp + argv[1:]
|
||||
|
||||
n = 0
|
||||
while n < len(argv):
|
||||
v1 = argv[n]
|
||||
v1v = v1[2:].lstrip("=")
|
||||
try:
|
||||
v2 = argv[n + 1]
|
||||
except:
|
||||
v2 = ""
|
||||
|
||||
n += 1
|
||||
if v1 == "-c" and v2 and os.path.isfile(v2):
|
||||
n += 1
|
||||
argv = argv[:n] + args_from_cfg(v2) + argv[n:]
|
||||
elif v1.startswith("-c") and v1v and os.path.isfile(v1v):
|
||||
argv = argv[:n] + args_from_cfg(v1v) + argv[n:]
|
||||
return argv
|
||||
|
||||
|
||||
def sighandler(sig: Optional[int] = None, frame: Optional[FrameType] = None) -> None:
|
||||
msg = [""] * 5
|
||||
for th in threading.enumerate():
|
||||
|
@ -609,8 +649,77 @@ def get_sects():
|
|||
if no accounts or volumes are configured,
|
||||
current folder will be read/write for everyone
|
||||
|
||||
the group @acct will always have every user with an account
|
||||
(the name of that group can be changed with --grp-all)
|
||||
|
||||
consider the config file for more flexible account/volume management,
|
||||
including dynamic reload at runtime (and being more readable w)
|
||||
|
||||
see \033[32m--help-auth\033[0m for ways to provide the password in requests;
|
||||
see \033[32m--help-idp\033[0m for replacing it with SSO and auth-middlewares
|
||||
"""
|
||||
),
|
||||
],
|
||||
[
|
||||
"auth",
|
||||
"how to login from a client",
|
||||
dedent(
|
||||
"""
|
||||
different ways to provide the password so you become authenticated:
|
||||
|
||||
login with the ui:
|
||||
go to \033[36mhttp://127.0.0.1:3923/?h\033[0m and login there
|
||||
|
||||
send the password in the '\033[36mPW\033[0m' http-header:
|
||||
\033[36mPW: \033[35mhunter2\033[0m
|
||||
or if you have \033[33m--accounts\033[0m enabled,
|
||||
\033[36mPW: \033[35med:hunter2\033[0m
|
||||
|
||||
send the password in the URL itself:
|
||||
\033[36mhttp://127.0.0.1:3923/\033[35m?pw=hunter2\033[0m
|
||||
or if you have \033[33m--accounts\033[0m enabled,
|
||||
\033[36mhttp://127.0.0.1:3923/\033[35m?pw=ed:hunter2\033[0m
|
||||
|
||||
use basic-authentication:
|
||||
\033[36mhttp://\033[35med:hunter2\033[36m@127.0.0.1:3923/\033[0m
|
||||
which should be the same as this header:
|
||||
\033[36mAuthorization: Basic \033[35mZWQ6aHVudGVyMg==\033[0m
|
||||
"""
|
||||
),
|
||||
],
|
||||
[
|
||||
"auth-ord",
|
||||
"authentication precedence",
|
||||
dedent(
|
||||
"""
|
||||
\033[33m--auth-ord\033[0m is a comma-separated list of auth options
|
||||
(one or more of the [\033[35moptions\033[0m] below); first one wins
|
||||
|
||||
[\033[35mpw\033[0m] is conventional login, for example the "\033[36mPW\033[0m" header,
|
||||
or the \033[36m?pw=\033[0m[...] URL-suffix, or a valid session cookie
|
||||
(see \033[33m--help-auth\033[0m)
|
||||
|
||||
[\033[35midp\033[0m] is a username provided in the http-request-header
|
||||
defined by \033[33m--idp-h-usr\033[0m and/or \033[33m--idp-hm-usr\033[0m, which is
|
||||
provided by an authentication middleware such as
|
||||
authentik, authelia, tailscale, ... (see \033[33m--help-idp\033[0m)
|
||||
|
||||
[\033[35midp-h\033[0m] is specifically an \033[33m--idp-h-usr\033[0m header,
|
||||
[\033[35midp-hm\033[0m] is specifically an \033[33m--idp-hm-usr\033[0m header;
|
||||
[\033[35midp\033[0m] is the same as [\033[35midp-hm,idp-h\033[0m]
|
||||
|
||||
[\033[35mipu\033[0m] is a mapping from an IP-address to a username,
|
||||
auto-authing that client-IP to that account
|
||||
(see the description of \033[36m--ipu\033[0m in \033[33m--help\033[0m)
|
||||
|
||||
NOTE: even if an option (\033[35mpw\033[0m/\033[35mipu\033[0m/...) is not in the list,
|
||||
it may still be enabled and can still take effect if
|
||||
none of the other alternatives identify the user
|
||||
|
||||
NOTE: if [\033[35mipu\033[0m] is in the list, it must be FIRST or LAST
|
||||
|
||||
NOTE: if [\033[35mpw\033[0m] is not in the list, the logout-button
|
||||
will be hidden when any idp feature is enabled
|
||||
"""
|
||||
),
|
||||
],
|
||||
|
@ -719,7 +828,7 @@ def get_sects():
|
|||
\033[36mc0\033[35m show all process output (default)
|
||||
\033[36mc1\033[35m show only stderr
|
||||
\033[36mc2\033[35m show only stdout
|
||||
\033[36mc3\033[35m mute all process otput
|
||||
\033[36mc3\033[35m mute all process output
|
||||
\033[0m
|
||||
examples:
|
||||
|
||||
|
@ -762,6 +871,41 @@ def get_sects():
|
|||
the upload speed can easily drop to 10% for small files)"""
|
||||
),
|
||||
],
|
||||
[
|
||||
"idp",
|
||||
"replacing the login system with fancy middleware",
|
||||
dedent(
|
||||
"""
|
||||
if you already have a centralized service which handles
|
||||
user-authentication for other services already, you can
|
||||
integrate copyparty with that for automatic login
|
||||
|
||||
if the middleware is providing the username in an http-header
|
||||
named '\033[35mtheUsername\033[0m' then do this: \033[36m--idp-h-usr theUsername\033[0m
|
||||
|
||||
if the middleware is providing a list of groups in the header
|
||||
named '\033[35mtheGroups\033[0m' then do this: \033[36m--idp-h-grp theGroup\033[0m
|
||||
|
||||
if the list of groups is separated by '\033[35m%\033[0m' then \033[36m--idp-gsep %\033[0m
|
||||
|
||||
if the middleware is providing a header named '\033[35mAccount\033[0m'
|
||||
and the value is '\033[35malice@forest.net\033[0m' but the username is
|
||||
actually '\033[35mmarisa\033[0m' then do this for each user:
|
||||
\033[36m--idp-hm-usr ^Account^alice@forest.net^marisa\033[0m
|
||||
(the separator '\033[35m^\033[0m' can be any character)
|
||||
|
||||
make ABSOLUTELY SURE that the header can only be set by your
|
||||
middleware and not by clients! and, as an extra precaution,
|
||||
send a header named '\033[36mfinalmasterspark\033[0m' (a secret keyword)
|
||||
and then \033[36m--idp-h-key finalmasterspark\033[0m to require that
|
||||
|
||||
the login/logout links/buttons can be replaced with links
|
||||
going to your IdP's UI; \033[36m--idp-login /login/?redir={dst}\033[0m
|
||||
will expand \033[36m{dst}\033[0m to the URL of the current page, so
|
||||
the IdP can redirect the user back to where they were
|
||||
"""
|
||||
),
|
||||
],
|
||||
[
|
||||
"urlform",
|
||||
"how to handle url-form POSTs",
|
||||
|
@ -1006,6 +1150,7 @@ def add_general(ap, nc, srvname):
|
|||
ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add volume, \033[33mSRC\033[0m:\033[33mDST\033[0m:\033[33mFLAG\033[0m; examples [\033[32m.::r\033[0m], [\033[32m/mnt/nas/music:/music:r:aed\033[0m], see --help-accounts")
|
||||
ap2.add_argument("--grp", metavar="G:N,N", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add group, \033[33mNAME\033[0m:\033[33mUSER1\033[0m,\033[33mUSER2\033[0m,\033[33m...\033[0m; example [\033[32madmins:ed,foo,bar\033[0m]")
|
||||
ap2.add_argument("--usernames", action="store_true", help="require username and password for login; default is just password")
|
||||
ap2.add_argument("--chdir", metavar="PATH", type=u, help="change working-directory to \033[33mPATH\033[0m before mapping volumes")
|
||||
ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files (volflag=dots)")
|
||||
ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,xm", help="how to handle url-form POSTs; see \033[33m--help-urlform\033[0m")
|
||||
ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="server terminal title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]")
|
||||
|
@ -1019,14 +1164,19 @@ def add_general(ap, nc, srvname):
|
|||
|
||||
def add_qr(ap, tty):
|
||||
ap2 = ap.add_argument_group("qr options")
|
||||
ap2.add_argument("--qr", action="store_true", help="show http:// QR-code on startup")
|
||||
ap2.add_argument("--qrs", action="store_true", help="show https:// QR-code on startup")
|
||||
ap2.add_argument("--qr", action="store_true", help="show QR-code on startup")
|
||||
ap2.add_argument("--qrs", action="store_true", help="change the QR-code URL to https://")
|
||||
ap2.add_argument("--qrl", metavar="PATH", type=u, default="", help="location to include in the url, for example [\033[32mpriv/?pw=hunter2\033[0m]")
|
||||
ap2.add_argument("--qri", metavar="PREFIX", type=u, default="", help="select IP which starts with \033[33mPREFIX\033[0m; [\033[32m.\033[0m] to force default IP when mDNS URL would have been used instead")
|
||||
ap2.add_argument("--qr-fg", metavar="COLOR", type=int, default=0 if tty else 16, help="foreground; try [\033[32m0\033[0m] if the qr-code is unreadable")
|
||||
ap2.add_argument("--qr-fg", metavar="COLOR", type=int, default=0 if tty else 16, help="foreground; try [\033[32m0\033[0m] or [\033[32m-1\033[0m] if the qr-code is unreadable")
|
||||
ap2.add_argument("--qr-bg", metavar="COLOR", type=int, default=229, help="background (white=255)")
|
||||
ap2.add_argument("--qrp", metavar="CELLS", type=int, default=4, help="padding (spec says 4 or more, but 1 is usually fine)")
|
||||
ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)")
|
||||
ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr")
|
||||
ap2.add_argument("--qr-wait", metavar="SEC", type=float, default=0, help="wait \033[33mSEC\033[0m before printing the qr-code to the log")
|
||||
ap2.add_argument("--qr-every", metavar="SEC", type=float, default=0, help="print the qr-code every \033[33mSEC\033[0m (try this with/without --qr-pin in case of issues)")
|
||||
ap2.add_argument("--qr-winch", metavar="SEC", type=float, default=0, help="when --qr-pin is enabled, check for terminal size change every \033[33mSEC\033[0m")
|
||||
ap2.add_argument("--qr-file", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m write qr-code to file.\n └─To create txt or svg, \033[33mTXT\033[0m is Filepath:Zoom:Pad, for example [\033[32mqr.txt:1:2\033[0m]\n └─To create png or gif, \033[33mTXT\033[0m is Filepath:Zoom:Pad:Foreground:Background, for example [\033[32mqr.png:8:2:333333:ffcc55\033[0m], or [\033[32mqr.png:8:2::ffcc55\033[0m] for transparent")
|
||||
|
||||
|
||||
def add_fs(ap):
|
||||
|
@ -1043,6 +1193,7 @@ def add_share(ap):
|
|||
ap2 = ap.add_argument_group("share-url options")
|
||||
ap2.add_argument("--shr", metavar="DIR", type=u, default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]")
|
||||
ap2.add_argument("--shr-db", metavar="FILE", type=u, default=db_path, help="database to store shares in")
|
||||
ap2.add_argument("--shr-who", metavar="TXT", type=u, default="auth", help="who can create a share? [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mauth\033[0m]=authenticated (volflag=shr_who)")
|
||||
ap2.add_argument("--shr-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to view/delete any share")
|
||||
ap2.add_argument("--shr-rt", metavar="MIN", type=int, default=1440, help="shares can be revived by their owner if they expired less than MIN minutes ago; [\033[32m60\033[0m]=hour, [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week")
|
||||
ap2.add_argument("--shr-v", action="store_true", help="debug")
|
||||
|
@ -1056,6 +1207,7 @@ def add_upload(ap):
|
|||
ap2.add_argument("--put-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for PUT/WebDAV uploads: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=put_ck)")
|
||||
ap2.add_argument("--bup-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for bup/basic-uploader: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=bup_ck)")
|
||||
ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled, default=12h")
|
||||
ap2.add_argument("--unp-who", metavar="NUM", type=int, default=1, help="clients can undo recent uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=unp_who)")
|
||||
ap2.add_argument("--u2abort", metavar="NUM", type=int, default=1, help="clients can abort incomplete uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=u2abort)")
|
||||
ap2.add_argument("--blank-wt", metavar="SEC", type=int, default=300, help="file write grace period (any client can write to a blank file last-modified more recently than \033[33mSEC\033[0m seconds ago)")
|
||||
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600")
|
||||
|
@ -1149,7 +1301,8 @@ def add_auth(ap):
|
|||
idp_db = os.path.join(E.cfg, "idp.db")
|
||||
ses_db = os.path.join(E.cfg, "sessions.db")
|
||||
ap2 = ap.add_argument_group("IdP / identity provider / user authentication options")
|
||||
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, default="", help="bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
|
||||
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
|
||||
ap2.add_argument("--idp-hm-usr", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mT\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp")
|
||||
ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control")
|
||||
ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present")
|
||||
ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m")
|
||||
|
@ -1157,12 +1310,24 @@ def add_auth(ap):
|
|||
ap2.add_argument("--idp-store", metavar="N", type=int, default=1, help="how to use \033[33m--idp-db\033[0m; [\033[32m0\033[0m] = entirely disable, [\033[32m1\033[0m] = write-only (effectively disabled), [\033[32m2\033[0m] = remember users, [\033[32m3\033[0m] = remember users and groups.\nNOTE: Will remember and restore the IdP-volumes of all users for all eternity if set to 2 or 3, even when user is deleted from your IdP")
|
||||
ap2.add_argument("--idp-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to use /?idp (the cache management UI)")
|
||||
ap2.add_argument("--idp-cookie", metavar="S", type=int, default=0, help="generate a session-token for IdP users which is written to cookie \033[33mcppws\033[0m (or \033[33mcppwd\033[0m if plaintext), to reduce the load on the IdP server, lifetime \033[33mS\033[0m seconds.\n └─note: The expiration time is a client hint only; the actual lifetime of the session-token is infinite (until next restart with \033[33m--ses-db\033[0m wiped)")
|
||||
ap2.add_argument("--idp-login", metavar="L", type=u, default="", help="replace all login-buttons with a link to URL \033[33mL\033[0m (unless \033[32mpw\033[0m is in \033[33m--auth-ord\033[0m then both will be shown); [\033[32m{dst}\033[0m] expands to url of current page")
|
||||
ap2.add_argument("--idp-login-t", metavar="T", type=u, default="Login with SSO", help="the label/text for the idp-login button")
|
||||
ap2.add_argument("--idp-logout", metavar="L", type=u, default="", help="replace all logout-buttons with a link to URL \033[33mL\033[0m")
|
||||
ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see --help-auth-ord")
|
||||
ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app")
|
||||
ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins")
|
||||
ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)")
|
||||
ap2.add_argument("--ses-len", metavar="CHARS", type=int, default=20, help="session key length; default is 120 bits ((20//4)*4*6)")
|
||||
ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies")
|
||||
ap2.add_argument("--grp-all", metavar="NAME", type=u, default="acct", help="the name of the auto-generated group which contains every username which is known")
|
||||
ap2.add_argument("--ipu", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m users with IP matching \033[33mCIDR\033[0m are auto-authenticated as username \033[33mUSR\033[0m; example: [\033[32m172.16.24.0/24=dave]")
|
||||
ap2.add_argument("--ipr", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m username \033[33mUSR\033[0m can only connect from an IP matching one or more \033[33mCIDR\033[0m (comma-sep.); example: [\033[32m192.168.123.0/24,172.16.0.0/16=dave]")
|
||||
ap2.add_argument("--have-idp-hdrs", type=u, default="", help=argparse.SUPPRESS)
|
||||
ap2.add_argument("--have-ipu-or-ipr", type=u, default="", help=argparse.SUPPRESS)
|
||||
ap2.add_argument("--ao-idp-before-pw", type=u, default="", help=argparse.SUPPRESS)
|
||||
ap2.add_argument("--ao-h-before-hm", type=u, default="", help=argparse.SUPPRESS)
|
||||
ap2.add_argument("--ao-ipu-wins", type=u, default="", help=argparse.SUPPRESS)
|
||||
ap2.add_argument("--ao-have-pw", type=u, default="", help=argparse.SUPPRESS)
|
||||
|
||||
|
||||
def add_chpw(ap):
|
||||
|
@ -1201,6 +1366,7 @@ def add_zc_mdns(ap):
|
|||
ap2.add_argument("--zm-lh", metavar="PATH", type=u, default="", help="link a specific folder for http shares")
|
||||
ap2.add_argument("--zm-lf", metavar="PATH", type=u, default="", help="link a specific folder for ftp shares")
|
||||
ap2.add_argument("--zm-ls", metavar="PATH", type=u, default="", help="link a specific folder for smb shares")
|
||||
ap2.add_argument("--zm-fqdn", metavar="FQDN", type=u, default="--name.local", help="the domain to announce; NOTE: using anything other than .local is nonstandard and could cause problems")
|
||||
ap2.add_argument("--zm-mnic", action="store_true", help="merge NICs which share subnets; assume that same subnet means same network")
|
||||
ap2.add_argument("--zm-msub", action="store_true", help="merge subnets on each NIC -- always enabled for ipv6 -- reduces network load, but gnome-gvfs clients may stop working, and clients cannot be in subnets that the server is not")
|
||||
ap2.add_argument("--zm-noneg", action="store_true", help="disable NSEC replies -- try this if some clients don't see copyparty")
|
||||
|
@ -1307,6 +1473,7 @@ def add_yolo(ap):
|
|||
ap2.add_argument("--no-fnugg", action="store_true", help="disable the smoketest for caching-related issues in the web-UI")
|
||||
ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET")
|
||||
ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)")
|
||||
ap2.add_argument("--unsafe-state", action="store_true", help="when one of the emergency fallback locations are used for runtime state ($TMPDIR, /tmp), certain features will be force-disabled for security reasons by default. This option overrides that safeguard and allows unsafe storage of secrets")
|
||||
|
||||
|
||||
def add_optouts(ap):
|
||||
|
@ -1317,9 +1484,10 @@ def add_optouts(ap):
|
|||
ap2.add_argument("--no-del", action="store_true", help="disable delete operations")
|
||||
ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations")
|
||||
ap2.add_argument("--no-cp", action="store_true", help="disable copy operations")
|
||||
ap2.add_argument("--no-fs-abrt", action="store_true", help="disable ability to abort ongoing copy/move")
|
||||
ap2.add_argument("-nth", action="store_true", help="no title hostname; don't show \033[33m--name\033[0m in <title>")
|
||||
ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI")
|
||||
ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI")
|
||||
ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI. This is the same as --du-who no")
|
||||
ap2.add_argument("-nb", action="store_true", help="no powered-by-copyparty branding in UI")
|
||||
ap2.add_argument("--zipmaxn", metavar="N", type=u, default="0", help="reject download-as-zip if more than \033[33mN\033[0m files in total; optionally takes a unit suffix: [\033[32m256\033[0m], [\033[32m9K\033[0m], [\033[32m4G\033[0m] (volflag=zipmaxn)")
|
||||
ap2.add_argument("--zipmaxs", metavar="SZ", type=u, default="0", help="reject download-as-zip if total download size exceeds \033[33mSZ\033[0m bytes; optionally takes a unit suffix: [\033[32m256M\033[0m], [\033[32m4G\033[0m], [\033[32m2T\033[0m] (volflag=zipmaxs)")
|
||||
|
@ -1338,7 +1506,7 @@ def add_optouts(ap):
|
|||
def add_safety(ap):
|
||||
ap2 = ap.add_argument_group("safety options")
|
||||
ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js")
|
||||
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --vague-403 -nih")
|
||||
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav requires login, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --dav-auth --vague-403 -nih")
|
||||
ap2.add_argument("-sss", action="store_true", help="further increase safety: Enable logging to disk, scan for dangerous symlinks.\n └─Alias of\033[32m -ss --no-dav --no-logues --no-readme -lo=cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz --ls=**,*,ln,p,r")
|
||||
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, default="", help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m (see \033[33m--help-ls\033[0m); example [\033[32m**,*,ln,p,r\033[0m]")
|
||||
ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)")
|
||||
|
@ -1360,6 +1528,8 @@ def add_safety(ap):
|
|||
ap2.add_argument("--sus-urls", metavar="R", type=u, default=r"\.php$|(^|/)wp-(admin|content|includes)/", help="URLs which are considered sus / eligible for banning; disable with blank or [\033[32mno\033[0m]")
|
||||
ap2.add_argument("--nonsus-urls", metavar="R", type=u, default=r"^(favicon\.ico|robots\.txt)$|^apple-touch-icon|^\.well-known", help="harmless URLs ignored from 404-bans; disable with blank or [\033[32mno\033[0m]")
|
||||
ap2.add_argument("--early-ban", action="store_true", help="if a client is banned, reject its connection as soon as possible; not a good idea to enable when proxied behind cloudflare since it could ban your reverse-proxy")
|
||||
ap2.add_argument("--cookie-nmax", metavar="N", type=int, default=50, help="reject HTTP-request from client if they send more than N cookies")
|
||||
ap2.add_argument("--cookie-cmax", metavar="N", type=int, default=8192, help="reject HTTP-request from client if more than N characters in Cookie header")
|
||||
ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for \033[33mMIN\033[0m minutes (and also kill its active connections) -- disable with 0")
|
||||
ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]")
|
||||
ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives")
|
||||
|
@ -1390,7 +1560,7 @@ def add_shutdown(ap):
|
|||
def add_logging(ap):
|
||||
ap2 = ap.add_argument_group("logging options")
|
||||
ap2.add_argument("-q", action="store_true", help="quiet; disable most STDOUT messages")
|
||||
ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile, example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)")
|
||||
ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile; use .txt for plaintext or .xz for compressed. Example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)")
|
||||
ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR")
|
||||
ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR")
|
||||
ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster")
|
||||
|
@ -1398,6 +1568,7 @@ def add_logging(ap):
|
|||
ap2.add_argument("--log-utc", action="store_true", help="do not use local timezone; assume the TZ env-var is UTC (tiny bit faster)")
|
||||
ap2.add_argument("--log-tdec", metavar="N", type=int, default=3, help="timestamp resolution / number of timestamp decimals")
|
||||
ap2.add_argument("--log-badpwd", metavar="N", type=int, default=2, help="log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed")
|
||||
ap2.add_argument("--log-badxml", action="store_true", help="log any invalid XML received from a client")
|
||||
ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs")
|
||||
ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling")
|
||||
ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
|
||||
|
@ -1426,11 +1597,12 @@ def add_thumbnail(ap):
|
|||
ap2.add_argument("--no-athumb", action="store_true", help="disable audio thumbnails (spectrograms) (volflag=dathumb)")
|
||||
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)")
|
||||
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails")
|
||||
ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="conversion timeout in seconds (volflag=convt)")
|
||||
ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="convert-to-image timeout in seconds (volflag=convt)")
|
||||
ap2.add_argument("--ac-convt", metavar="SEC", type=float, default=150.0, help="convert-to-audio timeout in seconds (volflag=aconvt)")
|
||||
ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=th_ram, help="max memory usage (GiB) permitted by thumbnailer; not very accurate")
|
||||
ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32my\033[0m]=crop, [\033[32mn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)")
|
||||
ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)")
|
||||
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference")
|
||||
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,raw,ff", help="image decoders, in order of preference")
|
||||
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
|
||||
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
|
||||
ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg output for video thumbs (avoids issues on some FFmpeg builds)")
|
||||
|
@ -1439,16 +1611,19 @@ def add_thumbnail(ap):
|
|||
ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled")
|
||||
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than \033[33m--th-poke\033[0m seconds will get deleted every \033[33m--th-clean\033[0m seconds")
|
||||
ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern")
|
||||
ap2.add_argument("--th-spec-p", metavar="N", type=u, default=1, help="for music, do spectrograms or embedded coverart? [\033[32m0\033[0m]=only-art, [\033[32m1\033[0m]=prefer-art, [\033[32m2\033[0m]=only-spec")
|
||||
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
|
||||
# https://github.com/libvips/libvips
|
||||
# https://stackoverflow.com/a/47612661
|
||||
# ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:'
|
||||
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow")
|
||||
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips")
|
||||
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow")
|
||||
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips")
|
||||
ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mrw,nef,orf,pef,raf,raw,sr2,srf,x3f", help="image formats to decode using rawpy")
|
||||
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)")
|
||||
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz", help="audio/image formats to decompress before passing to ffmpeg")
|
||||
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz, epub=jpg.epub", help="audio/image formats to decompress before passing to ffmpeg")
|
||||
|
||||
|
||||
def add_transcoding(ap):
|
||||
|
@ -1493,8 +1668,8 @@ def add_db_general(ap, hcores):
|
|||
ap2.add_argument("-e2vp", action="store_true", help="on hash mismatch: panic and quit copyparty")
|
||||
ap2.add_argument("--hist", metavar="PATH", type=u, default="", help="where to store volume data (db, thumbs); default is a folder named \".hist\" inside each volume (volflag=hist)")
|
||||
ap2.add_argument("--dbpath", metavar="PATH", type=u, default="", help="override where the volume databases are to be placed; default is the same as \033[33m--hist\033[0m (volflag=dbpath)")
|
||||
ap2.add_argument("--no-hash", metavar="PTN", type=u, default="", help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (volflag=nohash)")
|
||||
ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scans (volflag=noidx)")
|
||||
ap2.add_argument("--no-hash", metavar="PTN", type=u, default="", help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (must be specified as one big regex, not multiple times) (volflag=nohash)")
|
||||
ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scan (must be specified as one big regex, not multiple times) (volflag=noidx)")
|
||||
ap2.add_argument("--no-dirsz", action="store_true", help="do not show total recursive size of folders in listings, show inode size instead; slightly faster (volflag=nodirsz)")
|
||||
ap2.add_argument("--re-dirsz", action="store_true", help="if the directory-sizes in the UI are bonkers, use this along with \033[33m-e2dsa\033[0m to rebuild the index from scratch")
|
||||
ap2.add_argument("--no-dhash", action="store_true", help="disable rescan acceleration; do full database integrity check -- makes the db ~5%% smaller and bootup/rescans 3~10x slower")
|
||||
|
@ -1531,6 +1706,7 @@ def add_db_metadata(ap):
|
|||
|
||||
def add_txt(ap):
|
||||
ap2 = ap.add_argument_group("textfile options")
|
||||
ap2.add_argument("--md-no-br", action="store_true", help="markdown: disable newline-is-newline; will only render a newline into the html given two trailing spaces or a double-newline (volflag=md_no_br)")
|
||||
ap2.add_argument("--md-hist", metavar="TXT", type=u, default="s", help="where to store old version of markdown files; [\033[32ms\033[0m]=subfolder, [\033[32mv\033[0m]=volume-histpath, [\033[32mn\033[0m]=nope/disabled (volflag=md_hist)")
|
||||
ap2.add_argument("--txt-eol", metavar="TYPE", type=u, default="", help="enable EOL conversion when writing documents; supported: CRLF, LF (volflag=txt_eol)")
|
||||
ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="the textfile editor will check for serverside changes every \033[33mSEC\033[0m seconds")
|
||||
|
@ -1560,13 +1736,14 @@ def add_og(ap):
|
|||
|
||||
|
||||
def add_ui(ap, retry):
|
||||
THEMES = 10
|
||||
ap2 = ap.add_argument_group("ui options")
|
||||
ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
|
||||
ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)")
|
||||
ap2.add_argument("--localtime", action="store_true", help="default to local timezone instead of UTC")
|
||||
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language; one of the following: \033[32meng nor chi\033[0m")
|
||||
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..7)")
|
||||
ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed")
|
||||
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language, for example \033[32meng\033[0m / \033[32mnor\033[0m / ...")
|
||||
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..%d)" % (THEMES - 1,))
|
||||
ap2.add_argument("--themes", metavar="NUM", type=int, default=THEMES, help="number of themes installed")
|
||||
ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent")
|
||||
ap2.add_argument("--sort", metavar="C,C,C", type=u, default="href", help="default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: \033[32mhref -href ext sz ts tags/Album tags/.tn\033[0m (volflag=sort)")
|
||||
ap2.add_argument("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)")
|
||||
|
@ -1588,7 +1765,11 @@ def add_ui(ap, retry):
|
|||
ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
|
||||
ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
|
||||
ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-nb\033[0m")
|
||||
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)")
|
||||
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m). This is the same as --ver-who all")
|
||||
ap2.add_argument("--ver-who", metavar="TXT", type=u, default="no", help="only show version for: [\033[32ma\033[0m]=admin-permission-anywhere, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone")
|
||||
ap2.add_argument("--du-who", metavar="TXT", type=u, default="all", help="only show disk usage for: [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mrw\033[0m]=read-write, [\033[32mw\033[0m]=write, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone (volflag=du_who)")
|
||||
ap2.add_argument("--ver-iwho", type=int, default=0, help=argparse.SUPPRESS)
|
||||
ap2.add_argument("--du-iwho", type=int, default=0, help=argparse.SUPPRESS)
|
||||
ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
|
||||
ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
|
||||
ap2.add_argument("--ctl-re", metavar="SEC", type=int, default=1, help="the controlpanel Refresh-button will autorefresh every SEC; [\033[32m0\033[0m] = just once")
|
||||
|
@ -1767,16 +1948,7 @@ def main(argv: Optional[list[str]] = None) -> None:
|
|||
|
||||
ensure_webdeps()
|
||||
|
||||
for k, v in zip(argv[1:], argv[2:]):
|
||||
if k == "-c" and os.path.isfile(v):
|
||||
supp = args_from_cfg(v)
|
||||
argv.extend(supp)
|
||||
|
||||
for k in argv[1:]:
|
||||
v = k[2:]
|
||||
if k.startswith("-c") and v and os.path.isfile(v):
|
||||
supp = args_from_cfg(v)
|
||||
argv.extend(supp)
|
||||
argv = expand_cfg(argv)
|
||||
|
||||
deprecated: list[tuple[str, str]] = [
|
||||
("--salt", "--warksalt"),
|
||||
|
@ -1845,6 +2017,9 @@ def main(argv: Optional[list[str]] = None) -> None:
|
|||
except:
|
||||
sys.exit(1)
|
||||
|
||||
if al.chdir:
|
||||
os.chdir(al.chdir)
|
||||
|
||||
if al.ansi:
|
||||
al.no_ansi = False
|
||||
elif not al.no_ansi:
|
||||
|
@ -1872,7 +2047,7 @@ def main(argv: Optional[list[str]] = None) -> None:
|
|||
if not HAVE_IPV6 and al.i == "::":
|
||||
al.i = "0.0.0.0"
|
||||
|
||||
al.i = al.i.split(",")
|
||||
al.i = [x.strip() for x in al.i.split(",")]
|
||||
try:
|
||||
if "-" in al.p:
|
||||
lo, hi = [int(x) for x in al.p.split("-")]
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# coding: utf-8
|
||||
|
||||
VERSION = (1, 19, 0)
|
||||
VERSION = (1, 19, 8)
|
||||
CODENAME = "usernames"
|
||||
BUILD_DT = (2025, 8, 7)
|
||||
BUILD_DT = (2025, 9, 7)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
|
|
@ -431,6 +431,8 @@ class VFS(object):
|
|||
|
||||
self.get_dbv = self._get_dbv
|
||||
self.ls = self._ls
|
||||
self.canonical = self._canonical
|
||||
self.dcanonical = self._dcanonical
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "VFS(%s)" % (
|
||||
|
@ -624,7 +626,7 @@ class VFS(object):
|
|||
vrem = vjoin(self.vpath[len(dbv.vpath) :].lstrip("/"), vrem)
|
||||
return dbv, vrem
|
||||
|
||||
def canonical(self, rem: str, resolve: bool = True) -> str:
|
||||
def _canonical(self, rem: str, resolve: bool = True) -> str:
|
||||
"""returns the canonical path (fully-resolved absolute fs path)"""
|
||||
ap = self.realpath
|
||||
if rem:
|
||||
|
@ -632,7 +634,7 @@ class VFS(object):
|
|||
|
||||
return absreal(ap) if resolve else ap
|
||||
|
||||
def dcanonical(self, rem: str) -> str:
|
||||
def _dcanonical(self, rem: str) -> str:
|
||||
"""resolves until the final component (filename)"""
|
||||
ap = self.realpath
|
||||
if rem:
|
||||
|
@ -641,6 +643,44 @@ class VFS(object):
|
|||
ad, fn = os.path.split(ap)
|
||||
return os.path.join(absreal(ad), fn)
|
||||
|
||||
def _canonical_shr(self, rem: str, resolve: bool = True) -> str:
|
||||
"""returns the canonical path (fully-resolved absolute fs path)"""
|
||||
ap = self.realpath
|
||||
if rem:
|
||||
ap += "/" + rem
|
||||
|
||||
rap = absreal(ap)
|
||||
if self.shr_files:
|
||||
assert self.shr_src # !rm
|
||||
vn, rem = self.shr_src
|
||||
chk = absreal(os.path.join(vn.realpath, rem))
|
||||
if chk != rap:
|
||||
# not the dir itself; assert file allowed
|
||||
ad, fn = os.path.split(rap)
|
||||
if chk != ad or fn not in self.shr_files:
|
||||
return "\n\n"
|
||||
|
||||
return rap if resolve else ap
|
||||
|
||||
def _dcanonical_shr(self, rem: str) -> str:
|
||||
"""resolves until the final component (filename)"""
|
||||
ap = self.realpath
|
||||
if rem:
|
||||
ap += "/" + rem
|
||||
|
||||
ad, fn = os.path.split(ap)
|
||||
ad = absreal(ad)
|
||||
if self.shr_files:
|
||||
assert self.shr_src # !rm
|
||||
vn, rem = self.shr_src
|
||||
chk = absreal(os.path.join(vn.realpath, rem))
|
||||
if chk != absreal(ap):
|
||||
# not the dir itself; assert file allowed
|
||||
if ad != chk or fn not in self.shr_files:
|
||||
return "\n\n"
|
||||
|
||||
return os.path.join(ad, fn)
|
||||
|
||||
def _ls_nope(
|
||||
self, *a, **ka
|
||||
) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]:
|
||||
|
@ -881,6 +921,15 @@ class VFS(object):
|
|||
return None
|
||||
|
||||
if "xvol" in self.flags:
|
||||
self_ap = self.realpath + os.sep
|
||||
if aps.startswith(self_ap):
|
||||
vp = aps[len(self_ap) :]
|
||||
if ANYWIN:
|
||||
vp = vp.replace(os.sep, "/")
|
||||
vn2, _ = self._find(vp)
|
||||
if self == vn2:
|
||||
return self
|
||||
|
||||
all_aps = self.shr_all_aps or self.root.all_aps
|
||||
|
||||
for vap, vns in all_aps:
|
||||
|
@ -967,6 +1016,14 @@ class AuthSrv(object):
|
|||
self.indent = ""
|
||||
self.is_lxc = args.c == ["/z/initcfg"]
|
||||
|
||||
self._vf0b = {
|
||||
"tcolor": self.args.tcolor,
|
||||
"du_iwho": self.args.du_iwho,
|
||||
"shr_who": self.args.shr_who if self.args.shr else "no",
|
||||
}
|
||||
self._vf0 = self._vf0b.copy()
|
||||
self._vf0["d2d"] = True
|
||||
|
||||
# fwd-decl
|
||||
self.vfs = VFS(log_func, "", "", "", AXS(), {})
|
||||
self.acct: dict[str, str] = {} # uname->pw
|
||||
|
@ -1005,7 +1062,10 @@ class AuthSrv(object):
|
|||
yield prev, True
|
||||
|
||||
def vf0(self):
|
||||
return {"d2d": True, "tcolor": self.args.tcolor}
|
||||
return self._vf0.copy()
|
||||
|
||||
def vf0b(self):
|
||||
return self._vf0b.copy()
|
||||
|
||||
def idp_checkin(
|
||||
self, broker: Optional["BrokerCli"], uname: str, gname: str
|
||||
|
@ -1099,6 +1159,9 @@ class AuthSrv(object):
|
|||
if rejected:
|
||||
continue
|
||||
|
||||
if gn == self.args.grp_all:
|
||||
gn = ""
|
||||
|
||||
# if ap/vp has a user/group placeholder, make sure to keep
|
||||
# track so the same user/group is mapped when setting perms;
|
||||
# otherwise clear un/gn to indicate it's a regular volume
|
||||
|
@ -1208,6 +1271,7 @@ class AuthSrv(object):
|
|||
self.load_idp_db(bool(self.idp_accs))
|
||||
ret = {un: gns[:] for un, gns in self.idp_accs.items()}
|
||||
ret.update({zs: [""] for zs in acct if zs not in ret})
|
||||
grps[self.args.grp_all] = list(ret.keys())
|
||||
for gn, uns in grps.items():
|
||||
for un in uns:
|
||||
try:
|
||||
|
@ -1315,6 +1379,10 @@ class AuthSrv(object):
|
|||
zt = split_cfg_ln(ln)
|
||||
for zs, za in zt.items():
|
||||
zs = zs.lstrip("-")
|
||||
if "=" in zs:
|
||||
t = "WARNING: found an option named [%s] in your [global] config; did you mean to say [%s: %s] instead?"
|
||||
zs1, zs2 = zs.split("=", 1)
|
||||
self.log(t % (zs, zs1, zs2), 3)
|
||||
if za is True:
|
||||
self._e("└─argument [{}]".format(zs))
|
||||
else:
|
||||
|
@ -1324,6 +1392,10 @@ class AuthSrv(object):
|
|||
if cat == cata:
|
||||
try:
|
||||
u, p = [zs.strip() for zs in ln.split(":", 1)]
|
||||
if "=" in u and not p:
|
||||
t = "WARNING: found username [%s] in your [accounts] config; did you mean to say [%s: %s] instead?"
|
||||
zs1, zs2 = u.split("=", 1)
|
||||
self.log(t % (u, zs1, zs2), 3)
|
||||
self._l(ln, 5, "account [{}], password [{}]".format(u, p))
|
||||
acct[u] = p
|
||||
except:
|
||||
|
@ -1394,6 +1466,10 @@ class AuthSrv(object):
|
|||
zd = split_cfg_ln(ln)
|
||||
fstr = ""
|
||||
for sk, sv in zd.items():
|
||||
if "=" in sk:
|
||||
t = "WARNING: found a volflag named [%s] in your config; did you mean to say [%s: %s] instead?"
|
||||
zs1, zs2 = sk.split("=", 1)
|
||||
self.log(t % (sk, zs1, zs2), 3)
|
||||
bad = re.sub(r"[a-z0-9_-]", "", sk).lstrip("-")
|
||||
if bad:
|
||||
err = "bad characters [{}] in volflag name [{}]; "
|
||||
|
@ -1634,6 +1710,7 @@ class AuthSrv(object):
|
|||
# accept both , and : as separators between usernames
|
||||
zs1, zs2 = x.replace("=", ":").split(":", 1)
|
||||
grps[zs1] = zs2.replace(":", ",").split(",")
|
||||
grps[zs1] = [x.strip() for x in grps[zs1]]
|
||||
except:
|
||||
t = '\n invalid value "{}" for argument --grp, must be groupname:username1,username2,...'
|
||||
raise Exception(t.format(x))
|
||||
|
@ -1685,6 +1762,10 @@ class AuthSrv(object):
|
|||
self.log("\n{0}\n{1}{0}".format(t, "\n".join(slns)))
|
||||
raise
|
||||
|
||||
self.args.have_idp_hdrs = bool(self.args.idp_h_usr or self.args.idp_hm_usr)
|
||||
self.args.have_ipu_or_ipr = bool(self.args.ipu or self.args.ipr)
|
||||
self.setup_auth_ord()
|
||||
|
||||
self.setup_pwhash(acct)
|
||||
defpw = acct.copy()
|
||||
self.setup_chpw(acct)
|
||||
|
@ -1697,7 +1778,7 @@ class AuthSrv(object):
|
|||
|
||||
mount = cased
|
||||
|
||||
if not mount and not self.args.idp_h_usr:
|
||||
if not mount and not self.args.have_idp_hdrs:
|
||||
# -h says our defaults are CWD at root and read/write for everyone
|
||||
axs = AXS(["*"], ["*"], None, None)
|
||||
ehint = ""
|
||||
|
@ -1721,12 +1802,15 @@ class AuthSrv(object):
|
|||
files = os.listdir(E.cfg)
|
||||
except:
|
||||
files = []
|
||||
hits = [x for x in files if x.lower().endswith(".conf")]
|
||||
hits = [
|
||||
x
|
||||
for x in files
|
||||
if x.lower().endswith(".conf") and not x.startswith(".")
|
||||
]
|
||||
if hits:
|
||||
t = "Hint: Found some config files in [%s], but these were not automatically loaded because they are in the wrong place%s %s\n"
|
||||
self.log(t % (E.cfg, ehint, ", ".join(hits)), 3)
|
||||
zvf = {"tcolor": self.args.tcolor}
|
||||
vfs = VFS(self.log_func, absreal("."), "", "", axs, zvf)
|
||||
vfs = VFS(self.log_func, absreal("."), "", "", axs, self.vf0b())
|
||||
if not axs.uread:
|
||||
self.badcfg1 = True
|
||||
elif "" not in mount:
|
||||
|
@ -1870,7 +1954,7 @@ class AuthSrv(object):
|
|||
|
||||
if missing_users:
|
||||
zs = ", ".join(k for k in sorted(missing_users))
|
||||
if self.args.idp_h_usr:
|
||||
if self.args.have_idp_hdrs:
|
||||
t = "the following users are unknown, and assumed to come from IdP: "
|
||||
self.log(t + zs, c=6)
|
||||
else:
|
||||
|
@ -1881,6 +1965,16 @@ class AuthSrv(object):
|
|||
if LEELOO_DALLAS in all_users:
|
||||
raise Exception("sorry, reserved username: " + LEELOO_DALLAS)
|
||||
|
||||
zsl = []
|
||||
for usr in list(acct)[:]:
|
||||
zs = acct[usr].strip()
|
||||
if not zs:
|
||||
zs = ub64enc(os.urandom(48)).decode("ascii")
|
||||
zsl.append(usr)
|
||||
acct[usr] = zs
|
||||
if zsl:
|
||||
self.log("generated random passwords for users %r" % (zsl,), 6)
|
||||
|
||||
seenpwds = {}
|
||||
for usr, pwd in acct.items():
|
||||
if pwd in seenpwds:
|
||||
|
@ -2201,12 +2295,12 @@ class AuthSrv(object):
|
|||
if vf not in vol.flags:
|
||||
vol.flags[vf] = getattr(self.args, ga)
|
||||
|
||||
zs = "forget_ip gid nrand tail_who u2abort u2ow uid ups_who zip_who"
|
||||
zs = "forget_ip gid nrand tail_who th_spec_p u2abort u2ow uid unp_who ups_who zip_who"
|
||||
for k in zs.split():
|
||||
if k in vol.flags:
|
||||
vol.flags[k] = int(vol.flags[k])
|
||||
|
||||
zs = "convt tail_fd tail_rate tail_tmax"
|
||||
zs = "aconvt convt tail_fd tail_rate tail_tmax"
|
||||
for k in zs.split():
|
||||
if k in vol.flags:
|
||||
vol.flags[k] = float(vol.flags[k])
|
||||
|
@ -2250,6 +2344,11 @@ class AuthSrv(object):
|
|||
vol.lim.uid = vol.flags["uid"]
|
||||
vol.lim.gid = vol.flags["gid"]
|
||||
|
||||
vol.flags["du_iwho"] = n_du_who(vol.flags["du_who"])
|
||||
|
||||
if not enshare:
|
||||
vol.flags["shr_who"] = "no"
|
||||
|
||||
if vol.flags.get("og"):
|
||||
self.args.uqe = True
|
||||
|
||||
|
@ -2537,7 +2636,7 @@ class AuthSrv(object):
|
|||
if not self.args.no_voldump:
|
||||
self.log(t)
|
||||
|
||||
if have_e2d or self.args.idp_h_usr:
|
||||
if have_e2d or self.args.have_idp_hdrs:
|
||||
t = self.chk_sqlite_threadsafe()
|
||||
if t:
|
||||
self.log("\n\033[{}\033[0m\n".format(t))
|
||||
|
@ -2692,6 +2791,8 @@ class AuthSrv(object):
|
|||
|
||||
shn.shr_files = set(fns)
|
||||
shn.ls = shn._ls_shr
|
||||
shn.canonical = shn._canonical_shr
|
||||
shn.dcanonical = shn._dcanonical_shr
|
||||
else:
|
||||
shn.ls = shn._ls
|
||||
|
||||
|
@ -2756,6 +2857,7 @@ class AuthSrv(object):
|
|||
"dcrop": vf["crop"],
|
||||
"dth3x": vf["th3x"],
|
||||
"u2ts": vf["u2ts"],
|
||||
"shr_who": vf["shr_who"],
|
||||
"frand": bool(vf.get("rand")),
|
||||
"lifetime": vf.get("lifetime") or 0,
|
||||
"unlist": vf.get("unlist") or "",
|
||||
|
@ -2764,16 +2866,19 @@ class AuthSrv(object):
|
|||
js_htm = {
|
||||
"SPINNER": self.args.spinner,
|
||||
"s_name": self.args.bname,
|
||||
"idp_login": self.args.idp_login,
|
||||
"have_up2k_idx": "e2d" in vf,
|
||||
"have_acode": not self.args.no_acode,
|
||||
"have_c2flac": self.args.allow_flac,
|
||||
"have_c2wav": self.args.allow_wav,
|
||||
"have_shr": self.args.shr,
|
||||
"shr_who": vf["shr_who"],
|
||||
"have_zip": not self.args.no_zip,
|
||||
"have_mv": not self.args.no_mv,
|
||||
"have_del": not self.args.no_del,
|
||||
"have_unpost": int(self.args.unpost),
|
||||
"have_emp": self.args.emp,
|
||||
"have_emp": int(self.args.emp),
|
||||
"md_no_br": int(vf.get("md_no_br") or 0),
|
||||
"ext_th": vf.get("ext_th_d") or {},
|
||||
"sb_md": "" if "no_sb_md" in vf else (vf.get("md_sbf") or "y"),
|
||||
"sba_md": vf.get("md_sba") or "",
|
||||
|
@ -2824,10 +2929,22 @@ class AuthSrv(object):
|
|||
zs = str(vol.flags.get("tcolor") or self.args.tcolor)
|
||||
vol.flags["tcolor"] = zs.lstrip("#")
|
||||
|
||||
def setup_auth_ord(self) -> None:
|
||||
ao = [x.strip() for x in self.args.auth_ord.split(",")]
|
||||
if "idp" in ao:
|
||||
zi = ao.index("idp")
|
||||
ao = ao[:zi] + ["idp-hm", "idp-h"] + ao[zi:]
|
||||
zsl = "pw idp-h idp-hm ipu".split()
|
||||
pw, h, hm, ipu = [ao.index(x) if x in ao else 99 for x in zsl]
|
||||
self.args.ao_idp_before_pw = min(h, hm) < pw
|
||||
self.args.ao_h_before_hm = h < hm
|
||||
self.args.ao_ipu_wins = ipu == 0
|
||||
self.args.ao_have_pw = pw < 99 or not self.args.have_idp_hdrs
|
||||
|
||||
def load_idp_db(self, quiet=False) -> None:
|
||||
# mutex me
|
||||
level = self.args.idp_store
|
||||
if level < 2 or not self.args.idp_h_usr:
|
||||
if level < 2 or not self.args.have_idp_hdrs:
|
||||
return
|
||||
|
||||
assert sqlite3 # type: ignore # !rm
|
||||
|
@ -2884,7 +3001,7 @@ class AuthSrv(object):
|
|||
n = []
|
||||
q = "insert into us values (?,?,?)"
|
||||
accs = list(self.acct)
|
||||
if self.args.idp_h_usr and self.args.idp_cookie:
|
||||
if self.args.have_idp_hdrs and self.args.idp_cookie:
|
||||
accs.extend(self.idp_accs.keys())
|
||||
for uname in accs:
|
||||
if uname not in ases:
|
||||
|
@ -3416,6 +3533,30 @@ class AuthSrv(object):
|
|||
self.log("generated config:\n\n" + "\n".join(ret))
|
||||
|
||||
|
||||
def n_du_who(s: str) -> int:
|
||||
if s == "all":
|
||||
return 9
|
||||
if s == "auth":
|
||||
return 7
|
||||
if s == "w":
|
||||
return 5
|
||||
if s == "rw":
|
||||
return 4
|
||||
if s == "a":
|
||||
return 3
|
||||
return 0
|
||||
|
||||
|
||||
def n_ver_who(s: str) -> int:
|
||||
if s == "all":
|
||||
return 9
|
||||
if s == "auth":
|
||||
return 6
|
||||
if s == "a":
|
||||
return 3
|
||||
return 0
|
||||
|
||||
|
||||
def split_cfg_ln(ln: str) -> dict[str, Any]:
|
||||
# "a, b, c: 3" => {a:true, b:true, c:3}
|
||||
ret = {}
|
||||
|
@ -3448,7 +3589,9 @@ def expand_config_file(
|
|||
|
||||
if os.path.isdir(fp):
|
||||
names = list(sorted(os.listdir(fp)))
|
||||
cnames = [x for x in names if x.lower().endswith(".conf")]
|
||||
cnames = [
|
||||
x for x in names if x.lower().endswith(".conf") and not x.startswith(".")
|
||||
]
|
||||
if not cnames:
|
||||
t = "warning: tried to read config-files from folder '%s' but it does not contain any "
|
||||
if names:
|
||||
|
|
|
@ -2,18 +2,22 @@
|
|||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
from ..util import SYMTIME, fsdec, fsenc
|
||||
from . import path as path
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from ..util import NamedLogger
|
||||
|
||||
MKD_755 = {"chmod_d": 0o755}
|
||||
MKD_700 = {"chmod_d": 0o700}
|
||||
UTIME_CLAMPS = ((max, -2147483647), (max, 1), (min, 4294967294), (min, 2147483646))
|
||||
|
||||
_ = (path, MKD_755, MKD_700)
|
||||
__all__ = ["path", "MKD_755", "MKD_700"]
|
||||
_ = (path, MKD_755, MKD_700, UTIME_CLAMPS)
|
||||
__all__ = ["path", "MKD_755", "MKD_700", "UTIME_CLAMPS"]
|
||||
|
||||
# grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c
|
||||
# printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')"
|
||||
|
@ -99,6 +103,40 @@ def utime(
|
|||
return os.utime(fsenc(p), times)
|
||||
|
||||
|
||||
def utime_c(
|
||||
log: Union["NamedLogger", Any], p: str, ts: int, follow_symlinks: bool = True, throw: bool = False
|
||||
) -> Optional[int]:
|
||||
clamp = 0
|
||||
ov = ts
|
||||
bp = fsenc(p)
|
||||
now = int(time.time())
|
||||
while True:
|
||||
try:
|
||||
if SYMTIME:
|
||||
os.utime(bp, (now, ts), follow_symlinks=follow_symlinks)
|
||||
else:
|
||||
os.utime(bp, (now, ts))
|
||||
if clamp:
|
||||
t = "filesystem rejected utime(%r); clamped %s to %s"
|
||||
log(t % (p, ov, ts))
|
||||
return ts
|
||||
except Exception as ex:
|
||||
pv = ts
|
||||
while clamp < len(UTIME_CLAMPS):
|
||||
fun, cv = UTIME_CLAMPS[clamp]
|
||||
ts = fun(ts, cv)
|
||||
clamp += 1
|
||||
if ts != pv:
|
||||
break
|
||||
if clamp >= len(UTIME_CLAMPS):
|
||||
if throw:
|
||||
raise
|
||||
else:
|
||||
t = "could not utime(%r) to %s; %s, %r"
|
||||
log(t % (p, ov, ex, ex))
|
||||
return None
|
||||
|
||||
|
||||
if hasattr(os, "lstat"):
|
||||
|
||||
def lstat(p: str) -> os.stat_result:
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import traceback
|
||||
|
||||
from queue import Queue
|
||||
|
||||
|
|
|
@ -130,6 +130,7 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
|||
nlog: "NamedLogger" = lambda msg, c=0: log("cert-gen-srv", msg, c)
|
||||
|
||||
names = args.crt_ns.split(",") if args.crt_ns else []
|
||||
names = [x.strip() for x in names]
|
||||
if not args.crt_exact:
|
||||
for n in names[:]:
|
||||
names.append("*.{}".format(n))
|
||||
|
|
|
@ -44,6 +44,7 @@ def vf_bmap() -> dict[str, str]:
|
|||
"gsel",
|
||||
"hardlink",
|
||||
"magic",
|
||||
"md_no_br",
|
||||
"no_db_ip",
|
||||
"no_sb_md",
|
||||
"no_sb_lg",
|
||||
|
@ -68,6 +69,7 @@ def vf_bmap() -> dict[str, str]:
|
|||
def vf_vmap() -> dict[str, str]:
|
||||
"""argv-to-volflag: simple values"""
|
||||
ret = {
|
||||
"ac_convt": "aconvt",
|
||||
"no_hash": "nohash",
|
||||
"no_idx": "noidx",
|
||||
"re_maxage": "scan",
|
||||
|
@ -82,6 +84,7 @@ def vf_vmap() -> dict[str, str]:
|
|||
"chmod_d",
|
||||
"chmod_f",
|
||||
"dbd",
|
||||
"du_who",
|
||||
"forget_ip",
|
||||
"hsortn",
|
||||
"html_head",
|
||||
|
@ -105,18 +108,21 @@ def vf_vmap() -> dict[str, str]:
|
|||
"put_name",
|
||||
"mv_retry",
|
||||
"rm_retry",
|
||||
"shr_who",
|
||||
"sort",
|
||||
"tail_fd",
|
||||
"tail_rate",
|
||||
"tail_tmax",
|
||||
"tail_who",
|
||||
"tcolor",
|
||||
"th_spec_p",
|
||||
"txt_eol",
|
||||
"unlist",
|
||||
"u2abort",
|
||||
"u2ts",
|
||||
"uid",
|
||||
"gid",
|
||||
"unp_who",
|
||||
"ups_who",
|
||||
"zip_who",
|
||||
"zipmaxn",
|
||||
|
@ -260,7 +266,9 @@ flagcats = {
|
|||
"thsize": "thumbnail res; WxH",
|
||||
"crop": "center-cropping (y/n/fy/fn)",
|
||||
"th3x": "3x resolution (y/n/fy/fn)",
|
||||
"convt": "conversion timeout in seconds",
|
||||
"convt": "convert-to-image timeout in seconds",
|
||||
"aconvt": "convert-to-audio timeout in seconds",
|
||||
"th_spec_p=1": "make spectrograms? 0=never 1=fallback 2=always",
|
||||
"ext_th=s=/b.png": "use /b.png as thumbnail for file-extension s",
|
||||
},
|
||||
"handlers\n(better explained in --help-handlers)": {
|
||||
|
@ -290,6 +298,7 @@ flagcats = {
|
|||
"html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH",
|
||||
"tcolor=#fc0": "theme color (a hint for webbrowsers, discord, etc.)",
|
||||
"nodirsz": "don't show total folder size",
|
||||
"du_who=all": "show disk-usage info to everyone",
|
||||
"robots": "allows indexing by search engines (default)",
|
||||
"norobots": "kindly asks search engines to leave",
|
||||
"unlistcr": "don't list read-access in controlpanel",
|
||||
|
@ -319,6 +328,7 @@ flagcats = {
|
|||
"og_ua": "if defined: only send OG html if useragent matches this regex",
|
||||
},
|
||||
"textfiles": {
|
||||
"md_no_br": "newline only on double-newline or two tailing spaces",
|
||||
"md_hist": "where to put markdown backups; s=subfolder, v=volHist, n=nope",
|
||||
"exp": "enable textfile expansion; see --help-exp",
|
||||
"exp_md": "placeholders to expand in markdown files; see --help",
|
||||
|
@ -341,6 +351,8 @@ flagcats = {
|
|||
"dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so',
|
||||
"rss": "allow '?rss' URL suffix (experimental)",
|
||||
"rmagic": "expensive analysis for mimetype accuracy",
|
||||
"shr_who=auth": "who can create shares? no/auth/a",
|
||||
"unp_who=2": "unpost only if same... 1=ip+name, 2=ip, 3=name",
|
||||
"ups_who=2": "restrict viewing the list of recent uploads",
|
||||
"zip_who=2": "restrict access to download-as-zip/tar",
|
||||
"zipmaxn=9k": "reject download-as-zip if more than 9000 files",
|
||||
|
|
|
@ -65,6 +65,9 @@ DXMLParser = _DXMLParser
|
|||
|
||||
|
||||
def parse_xml(txt: str) -> ET.Element:
|
||||
"""
|
||||
Parse XML into an xml.etree.ElementTree.Element while defusing some unsafe parts.
|
||||
"""
|
||||
parser = DXMLParser()
|
||||
parser.feed(txt)
|
||||
return parser.close() # type: ignore
|
||||
|
|
|
@ -68,13 +68,13 @@ class FtpAuth(DummyAuthorizer):
|
|||
if ip.startswith("::ffff:"):
|
||||
ip = ip[7:]
|
||||
|
||||
ip = ipnorm(ip)
|
||||
ipn = ipnorm(ip)
|
||||
bans = self.hub.bans
|
||||
if ip in bans:
|
||||
rt = bans[ip] - time.time()
|
||||
if ipn in bans:
|
||||
rt = bans[ipn] - time.time()
|
||||
if rt < 0:
|
||||
logging.info("client unbanned")
|
||||
del bans[ip]
|
||||
del bans[ipn]
|
||||
else:
|
||||
raise AuthenticationFailed("banned")
|
||||
|
||||
|
@ -96,6 +96,10 @@ class FtpAuth(DummyAuthorizer):
|
|||
|
||||
if args.ipu and uname == "*":
|
||||
uname = args.ipu_iu[args.ipu_nm.map(ip)]
|
||||
if args.ipr and uname in args.ipr_u:
|
||||
if not args.ipr_u[uname].map(ip):
|
||||
logging.warning("username [%s] rejected by --ipr", uname)
|
||||
uname = "*"
|
||||
|
||||
if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):
|
||||
g = self.hub.gpwd
|
||||
|
@ -148,10 +152,6 @@ class FtpFs(AbstractedFS):
|
|||
self.cwd = "/" # pyftpdlib convention of leading slash
|
||||
self.root = "/var/lib/empty"
|
||||
|
||||
self.can_read = self.can_write = self.can_move = False
|
||||
self.can_delete = self.can_get = self.can_upget = False
|
||||
self.can_admin = self.can_dot = False
|
||||
|
||||
self.listdirinfo = self.listdir
|
||||
self.chdir(".")
|
||||
|
||||
|
@ -214,7 +214,7 @@ class FtpFs(AbstractedFS):
|
|||
m: bool = False,
|
||||
d: bool = False,
|
||||
) -> tuple[str, VFS, str]:
|
||||
return self.v2a(os.path.join(self.cwd, vpath), r, w, m, d)
|
||||
return self.v2a(join(self.cwd, vpath), r, w, m, d)
|
||||
|
||||
def ftp2fs(self, ftppath: str) -> str:
|
||||
# return self.v2a(ftppath)
|
||||
|
@ -285,21 +285,14 @@ class FtpFs(AbstractedFS):
|
|||
# returning 550 is library-default and suitable
|
||||
raise FSE("No such file or directory")
|
||||
|
||||
avfs = vfs.chk_ap(ap, st)
|
||||
if not avfs:
|
||||
raise FSE("Permission denied", 1)
|
||||
if vfs.realpath:
|
||||
avfs = vfs.chk_ap(ap, st)
|
||||
if not avfs:
|
||||
raise FSE("Permission denied", 1)
|
||||
else:
|
||||
avfs = vfs
|
||||
|
||||
self.cwd = nwd
|
||||
(
|
||||
self.can_read,
|
||||
self.can_write,
|
||||
self.can_move,
|
||||
self.can_delete,
|
||||
self.can_get,
|
||||
self.can_upget,
|
||||
self.can_admin,
|
||||
self.can_dot,
|
||||
) = avfs.can_access("", self.h.uname)
|
||||
|
||||
def mkdir(self, path: str) -> None:
|
||||
ap, vfs, _ = self.rv2a(path, w=True)
|
||||
|
@ -322,7 +315,7 @@ class FtpFs(AbstractedFS):
|
|||
vfs_ls = [x[0] for x in vfs_ls1]
|
||||
vfs_ls.extend(vfs_virt.keys())
|
||||
|
||||
if not self.can_dot:
|
||||
if self.uname not in vfs.axs.udot:
|
||||
vfs_ls = exclude_dotfiles(vfs_ls)
|
||||
|
||||
vfs_ls.sort()
|
||||
|
@ -370,16 +363,13 @@ class FtpFs(AbstractedFS):
|
|||
raise FSE(str(ex))
|
||||
|
||||
def rename(self, src: str, dst: str) -> None:
|
||||
if not self.can_move:
|
||||
raise FSE("Not allowed for user " + self.h.uname)
|
||||
|
||||
if self.args.no_mv:
|
||||
raise FSE("The rename/move feature is disabled in server config")
|
||||
|
||||
svp = join(self.cwd, src).lstrip("/")
|
||||
dvp = join(self.cwd, dst).lstrip("/")
|
||||
try:
|
||||
self.hub.up2k.handle_mv(self.uname, self.h.cli_ip, svp, dvp)
|
||||
self.hub.up2k.handle_mv("", self.uname, self.h.cli_ip, svp, dvp)
|
||||
except Exception as ex:
|
||||
raise FSE(str(ex))
|
||||
|
||||
|
@ -403,7 +393,7 @@ class FtpFs(AbstractedFS):
|
|||
|
||||
def utime(self, path: str, timeval: float) -> None:
|
||||
ap = self.rv2a(path, w=True)[0]
|
||||
return bos.utime(ap, (timeval, timeval))
|
||||
bos.utime_c(logging.warning, ap, int(timeval), False)
|
||||
|
||||
def lstat(self, path: str) -> os.stat_result:
|
||||
ap = self.rv2a(path)[0]
|
||||
|
@ -492,7 +482,11 @@ class FtpHandler(FTPHandler):
|
|||
def ftp_STOR(self, file: str, mode: str = "w") -> Any:
|
||||
# Optional[str]
|
||||
vp = join(self.fs.cwd, file).lstrip("/")
|
||||
ap, vfs, rem = self.fs.v2a(vp, w=True)
|
||||
try:
|
||||
ap, vfs, rem = self.fs.v2a(vp, w=True)
|
||||
except Exception as ex:
|
||||
self.respond("550 %s" % (ex,), logging.info)
|
||||
return
|
||||
self.vfs_map[ap] = vp
|
||||
xbu = vfs.flags.get("xbu")
|
||||
if xbu and not runhook(
|
||||
|
|
|
@ -12,7 +12,6 @@ import random
|
|||
import re
|
||||
import socket
|
||||
import stat
|
||||
import string
|
||||
import sys
|
||||
import threading # typechk
|
||||
import time
|
||||
|
@ -31,7 +30,7 @@ try:
|
|||
except:
|
||||
pass
|
||||
|
||||
from .__init__ import ANYWIN, PY2, RES, TYPE_CHECKING, EnvParams, unicode
|
||||
from .__init__ import ANYWIN, RES, TYPE_CHECKING, EnvParams, unicode
|
||||
from .__version__ import S_VERSION
|
||||
from .authsrv import LEELOO_DALLAS, VFS # typechk
|
||||
from .bos import bos
|
||||
|
@ -66,6 +65,7 @@ from .util import (
|
|||
exclude_dotfiles,
|
||||
formatdate,
|
||||
fsenc,
|
||||
gen_content_disposition,
|
||||
gen_filekey,
|
||||
gen_filekey_dbg,
|
||||
gencookie,
|
||||
|
@ -394,10 +394,10 @@ class HttpCli(object):
|
|||
zsl = [
|
||||
" rproxy: %d if this client's IP-address is [%s]"
|
||||
% (-1 - zd, zs.strip())
|
||||
for zd, zs in enumerate(zsl)
|
||||
for zd, zs in enumerate(zsl[::-1])
|
||||
]
|
||||
t = 'could not determine the client\'s IP-address because the global-option --rproxy has not been configured, so the request-header [%s] specified by global-option --xff-hdr cannot be used safely! Please see the "reverse-proxy" section in the readme. The best approach is to configure your reverse-proxy to give copyparty the exact IP-address to assume (perhaps in another header), but you may also try the following:'
|
||||
t = t % (self.args.xff_hdr,)
|
||||
t = 'could not determine the client\'s IP-address because the global-option --rproxy has not been configured, so the request-header [%s] specified by global-option --xff-hdr cannot be used safely! The raw header value was [%s]. Please see the "reverse-proxy" section in the readme. The best approach is to configure your reverse-proxy to give copyparty the exact IP-address to assume (perhaps in another header), but you may also try the following:'
|
||||
t = t % (self.args.xff_hdr, zso)
|
||||
self.log("%s\n\n%s\n" % (t, "\n".join(zsl)), 3)
|
||||
|
||||
pip = self.conn.addr[0]
|
||||
|
@ -562,19 +562,23 @@ class HttpCli(object):
|
|||
|
||||
zso = self.headers.get("cookie")
|
||||
if zso:
|
||||
if len(zso) > 8192:
|
||||
if len(zso) > self.args.cookie_cmax:
|
||||
self.loud_reply("cookie header too big", status=400)
|
||||
return False
|
||||
zsll = [x.split("=", 1) for x in zso.split(";") if "=" in x]
|
||||
cookies = {k.strip(): unescape_cookie(zs) for k, zs in zsll}
|
||||
cookie_pw = cookies.get("cppws") or cookies.get("cppwd") or ""
|
||||
cookie_pw = cookies.get("cppws" if self.is_https else "cppwd") or ""
|
||||
if "b" in cookies and "b" not in uparam:
|
||||
uparam["b"] = cookies["b"]
|
||||
if len(cookies) > self.args.cookie_nmax:
|
||||
self.loud_reply("too many cookies", status=400)
|
||||
else:
|
||||
cookies = {}
|
||||
cookie_pw = ""
|
||||
|
||||
if len(uparam) > 10 or len(cookies) > 50:
|
||||
if len(uparam) > 12:
|
||||
t = "http-request rejected; num.params: %d %r"
|
||||
self.log(t % (len(uparam), self.req), 3)
|
||||
self.loud_reply("u wot m8", status=400)
|
||||
return False
|
||||
|
||||
|
@ -620,8 +624,24 @@ class HttpCli(object):
|
|||
or "*"
|
||||
)
|
||||
|
||||
if self.args.idp_h_usr:
|
||||
idp_usr = self.headers.get(self.args.idp_h_usr) or ""
|
||||
if self.args.have_idp_hdrs and (
|
||||
self.uname == "*" or self.args.ao_idp_before_pw
|
||||
):
|
||||
idp_usr = ""
|
||||
if self.args.idp_hm_usr:
|
||||
for hn, hmv in self.args.idp_hm_usr_p.items():
|
||||
zs = self.headers.get(hn)
|
||||
if zs:
|
||||
for zs1, zs2 in hmv.items():
|
||||
if zs == zs1:
|
||||
idp_usr = zs2
|
||||
break
|
||||
if idp_usr:
|
||||
break
|
||||
for hn in self.args.idp_h_usr:
|
||||
if idp_usr and not self.args.ao_h_before_hm:
|
||||
break
|
||||
idp_usr = self.headers.get(hn) or idp_usr
|
||||
if idp_usr:
|
||||
idp_grp = (
|
||||
self.headers.get(self.args.idp_h_grp) or ""
|
||||
|
@ -670,15 +690,24 @@ class HttpCli(object):
|
|||
if idp_usr in self.asrv.vfs.aread:
|
||||
self.pw = ""
|
||||
self.uname = idp_usr
|
||||
self.html_head += "<script>var is_idp=1</script>\n"
|
||||
if self.args.ao_have_pw or self.args.idp_logout:
|
||||
self.html_head += "<script>var is_idp=1</script>\n"
|
||||
else:
|
||||
self.html_head += "<script>var is_idp=2</script>\n"
|
||||
zs = self.asrv.ases.get(idp_usr)
|
||||
if zs:
|
||||
self.set_idp_cookie(zs)
|
||||
else:
|
||||
self.log("unknown username: %r" % (idp_usr,), 1)
|
||||
|
||||
if self.args.ipu and self.uname == "*":
|
||||
self.uname = self.conn.ipu_iu[self.conn.ipu_nm.map(self.ip)]
|
||||
if self.args.have_ipu_or_ipr:
|
||||
if self.args.ipu and (self.uname == "*" or self.args.ao_ipu_wins):
|
||||
self.uname = self.conn.ipu_iu[self.conn.ipu_nm.map(self.ip)]
|
||||
ipr = self.conn.hsrv.ipr
|
||||
if ipr and self.uname in ipr:
|
||||
if not ipr[self.uname].map(self.ip):
|
||||
self.log("username [%s] rejected by --ipr" % (self.uname,), 3)
|
||||
self.uname = "*"
|
||||
|
||||
self.rvol = self.asrv.vfs.aread[self.uname]
|
||||
self.wvol = self.asrv.vfs.awrite[self.uname]
|
||||
|
@ -700,7 +729,7 @@ class HttpCli(object):
|
|||
cookies["b"] = ""
|
||||
|
||||
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
|
||||
if "xdev" in vn.flags or "xvol" in vn.flags:
|
||||
if vn.realpath and ("xdev" in vn.flags or "xvol" in vn.flags):
|
||||
ap = vn.canonical(rem)
|
||||
avn = vn.chk_ap(ap)
|
||||
else:
|
||||
|
@ -790,6 +819,15 @@ class HttpCli(object):
|
|||
6 if em.startswith("client d/c ") else 3,
|
||||
)
|
||||
|
||||
if self.hint and self.hint.startswith("<xml> "):
|
||||
if self.args.log_badxml:
|
||||
t = "invalid XML received from client: %r"
|
||||
self.log(t % (self.hint[6:],), 6)
|
||||
else:
|
||||
t = "received invalid XML from client; enable --log-badxml to see the whole XML in the log"
|
||||
self.log(t, 6)
|
||||
self.hint = ""
|
||||
|
||||
msg = "%s\r\nURL: %s\r\n" % (em, self.vpath)
|
||||
if self.hint:
|
||||
msg += "hint: %s\r\n" % (self.hint,)
|
||||
|
@ -1217,7 +1255,7 @@ class HttpCli(object):
|
|||
|
||||
res_path = "web/" + self.vpath[5:]
|
||||
if res_path in RES:
|
||||
ap = os.path.join(self.E.mod, res_path)
|
||||
ap = self.E.mod_ + res_path
|
||||
if bos.path.exists(ap) or bos.path.exists(ap + ".gz"):
|
||||
return self.tx_file(ap)
|
||||
else:
|
||||
|
@ -1506,7 +1544,9 @@ class HttpCli(object):
|
|||
if not rbuf or len(buf) >= 32768:
|
||||
break
|
||||
|
||||
xroot = parse_xml(buf.decode(enc, "replace"))
|
||||
sbuf = buf.decode(enc, "replace")
|
||||
self.hint = "<xml> " + sbuf
|
||||
xroot = parse_xml(sbuf)
|
||||
xtag = next((x for x in xroot if x.tag.split("}")[-1] == "prop"), None)
|
||||
if xtag is not None:
|
||||
props = set([y.tag.split("}")[-1] for y in xtag])
|
||||
|
@ -1592,13 +1632,24 @@ class HttpCli(object):
|
|||
self.log("inaccessible: %r" % ("/" + self.vpath,))
|
||||
raise Pebkac(401, "authenticate")
|
||||
|
||||
if "quota-available-bytes" in props and not self.args.nid:
|
||||
zi = vn.flags["du_iwho"] if "quota-available-bytes" in props else 0
|
||||
if zi and (
|
||||
zi == 9
|
||||
or (zi == 7 and self.uname != "*")
|
||||
or (zi == 5 and self.can_write)
|
||||
or (zi == 4 and self.can_write and self.can_read)
|
||||
or (zi == 3 and self.can_admin)
|
||||
):
|
||||
bfree, btot, _ = get_df(vn.realpath, False)
|
||||
if btot:
|
||||
df = {
|
||||
"quota-available-bytes": str(bfree),
|
||||
"quota-used-bytes": str(btot - bfree),
|
||||
}
|
||||
if "quotaused" in props: # macos finder crazytalk
|
||||
df["quotaused"] = df["quota-used-bytes"]
|
||||
if "quota" in props:
|
||||
df["quota"] = df["quota-available-bytes"] # idk, makes it happy
|
||||
else:
|
||||
df = {}
|
||||
else:
|
||||
|
@ -1708,6 +1759,7 @@ class HttpCli(object):
|
|||
uenc = enc.upper()
|
||||
|
||||
txt = buf.decode(enc, "replace")
|
||||
self.hint = "<xml> " + txt
|
||||
ET.register_namespace("D", "DAV:")
|
||||
xroot = mkenod("D:orz")
|
||||
xroot.insert(0, parse_xml(txt))
|
||||
|
@ -1768,6 +1820,7 @@ class HttpCli(object):
|
|||
uenc = enc.upper()
|
||||
|
||||
txt = buf.decode(enc, "replace")
|
||||
self.hint = "<xml> " + txt
|
||||
ET.register_namespace("D", "DAV:")
|
||||
lk = parse_xml(txt)
|
||||
assert lk.tag == "{DAV:}lockinfo"
|
||||
|
@ -1983,6 +2036,9 @@ class HttpCli(object):
|
|||
if "eshare" in self.uparam:
|
||||
return self.handle_eshare()
|
||||
|
||||
if "fs_abrt" in self.uparam:
|
||||
return self.handle_fs_abrt()
|
||||
|
||||
if "application/octet-stream" in ctype:
|
||||
return self.handle_post_binary()
|
||||
|
||||
|
@ -2280,12 +2336,7 @@ class HttpCli(object):
|
|||
at = mt = time.time() - lifetime
|
||||
cli_mt = self.headers.get("x-oc-mtime")
|
||||
if cli_mt:
|
||||
try:
|
||||
mt = int(cli_mt)
|
||||
times = (int(time.time()), mt)
|
||||
bos.utime(path, times, False)
|
||||
except:
|
||||
pass
|
||||
bos.utime_c(self.log, path, int(cli_mt), False)
|
||||
|
||||
if nameless and "magic" in vfs.flags:
|
||||
try:
|
||||
|
@ -3007,7 +3058,7 @@ class HttpCli(object):
|
|||
self.asrv.forget_session(self.conn.hsrv.broker, self.uname)
|
||||
self.get_pwd_cookie("x")
|
||||
|
||||
dst = self.args.SRS + "?h"
|
||||
dst = self.args.idp_logout or (self.args.SRS + "?h")
|
||||
h2 = '<a href="' + dst + '">continue</a>'
|
||||
html = self.j2s("msg", h1="ok bye", h2=h2, redir=dst)
|
||||
self.reply(html.encode("utf-8"))
|
||||
|
@ -3020,6 +3071,11 @@ class HttpCli(object):
|
|||
uname = self.asrv.iacct.get(hpwd)
|
||||
if uname:
|
||||
pwd = self.asrv.ases.get(uname) or pwd
|
||||
if uname and self.conn.hsrv.ipr:
|
||||
znm = self.conn.hsrv.ipr.get(uname)
|
||||
if znm and not znm.map(self.ip):
|
||||
self.log("username [%s] rejected by --ipr" % (self.uname,), 3)
|
||||
uname = ""
|
||||
if uname:
|
||||
msg = "hi " + uname
|
||||
dur = int(60 * 60 * self.args.logout)
|
||||
|
@ -3363,8 +3419,6 @@ class HttpCli(object):
|
|||
sz, sha_hex, sha_b64 = copier(
|
||||
p_data, f, hasher, max_sz, self.args.s_wr_slp
|
||||
)
|
||||
if sz == 0:
|
||||
raise Pebkac(400, "empty files in post")
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
|
@ -3982,6 +4036,13 @@ class HttpCli(object):
|
|||
if not editions:
|
||||
return self.tx_404()
|
||||
|
||||
#
|
||||
# force download
|
||||
|
||||
if "dl" in self.ouparam:
|
||||
cdis = gen_content_disposition(os.path.basename(req_path))
|
||||
self.out_headers["Content-Disposition"] = cdis
|
||||
|
||||
#
|
||||
# if-modified
|
||||
|
||||
|
@ -4150,6 +4211,13 @@ class HttpCli(object):
|
|||
if not editions:
|
||||
return self.tx_404()
|
||||
|
||||
#
|
||||
# force download
|
||||
|
||||
if "dl" in self.ouparam:
|
||||
cdis = gen_content_disposition(os.path.basename(req_path))
|
||||
self.out_headers["Content-Disposition"] = cdis
|
||||
|
||||
#
|
||||
# if-modified
|
||||
|
||||
|
@ -4698,24 +4766,7 @@ class HttpCli(object):
|
|||
if maxn < nf:
|
||||
raise Pebkac(400, t)
|
||||
|
||||
safe = (string.ascii_letters + string.digits).replace("%", "")
|
||||
afn = "".join([x if x in safe.replace('"', "") else "_" for x in fn])
|
||||
bascii = unicode(safe).encode("utf-8")
|
||||
zb = fn.encode("utf-8", "xmlcharrefreplace")
|
||||
if not PY2:
|
||||
zbl = [
|
||||
chr(x).encode("utf-8")
|
||||
if x in bascii
|
||||
else "%{:02x}".format(x).encode("ascii")
|
||||
for x in zb
|
||||
]
|
||||
else:
|
||||
zbl = [unicode(x) if x in bascii else "%{:02x}".format(ord(x)) for x in zb]
|
||||
|
||||
ufn = b"".join(zbl).decode("ascii")
|
||||
|
||||
cdis = "attachment; filename=\"{}.{}\"; filename*=UTF-8''{}.{}"
|
||||
cdis = cdis.format(afn, ext, ufn, ext)
|
||||
cdis = gen_content_disposition("%s.%s" % (fn, ext))
|
||||
self.log(repr(cdis))
|
||||
self.send_headers(None, mime=mime, headers={"Content-Disposition": cdis})
|
||||
|
||||
|
@ -4902,7 +4953,8 @@ class HttpCli(object):
|
|||
"lastmod": int(ts_md * 1000),
|
||||
"lang": self.args.lang,
|
||||
"favico": self.args.favico,
|
||||
"have_emp": self.args.emp,
|
||||
"have_emp": int(self.args.emp),
|
||||
"md_no_br": int(vn.flags.get("md_no_br") or 0),
|
||||
"md_chk_rate": self.args.mcr,
|
||||
"md": boundary,
|
||||
"arg_base": arg_base,
|
||||
|
@ -4970,10 +5022,20 @@ class HttpCli(object):
|
|||
else:
|
||||
rip = host
|
||||
|
||||
defpw = "dave:hunter2" if self.args.usernames else "hunter2"
|
||||
|
||||
vp = (self.uparam["hc"] or "").lstrip("/")
|
||||
pw = self.pw or "hunter2"
|
||||
pw = self.ouparam.get("pw") or defpw
|
||||
if pw in self.asrv.sesa:
|
||||
pw = "hunter2"
|
||||
pw = defpw
|
||||
|
||||
unpw = pw
|
||||
try:
|
||||
un, pw = unpw.split(":")
|
||||
except:
|
||||
un = ""
|
||||
if self.args.usernames:
|
||||
un = "dave"
|
||||
|
||||
html = self.j2s(
|
||||
"svcs",
|
||||
|
@ -4987,7 +5049,10 @@ class HttpCli(object):
|
|||
host=html_sh_esc(host),
|
||||
hport=html_sh_esc(hport),
|
||||
aname=aname,
|
||||
b_un=("<b>%s</b>" % (html_sh_esc(un),)) if un else "k",
|
||||
un=html_sh_esc(un),
|
||||
pw=html_sh_esc(pw),
|
||||
unpw=html_sh_esc(unpw),
|
||||
)
|
||||
self.reply(html.encode("utf-8"))
|
||||
return True
|
||||
|
@ -5112,6 +5177,11 @@ class HttpCli(object):
|
|||
elif nre:
|
||||
re_btn = "&re=%s" % (nre,)
|
||||
|
||||
zi = self.args.ver_iwho
|
||||
show_ver = zi and (
|
||||
zi == 9 or (zi == 6 and self.uname != "*") or (zi == 3 and avol)
|
||||
)
|
||||
|
||||
html = self.j2s(
|
||||
"splash",
|
||||
this=self,
|
||||
|
@ -5134,7 +5204,7 @@ class HttpCli(object):
|
|||
no304=self.no304(),
|
||||
k304vis=self.args.k304 > 0,
|
||||
no304vis=self.args.no304 > 0,
|
||||
ver=S_VERSION if self.args.ver else "",
|
||||
ver=S_VERSION if show_ver else "",
|
||||
chpw=self.args.chpw and self.uname != "*",
|
||||
ahttps="" if self.is_https else "https://" + self.host + self.req,
|
||||
)
|
||||
|
@ -5349,8 +5419,9 @@ class HttpCli(object):
|
|||
|
||||
if dk_sz and fsroot:
|
||||
kdirs = []
|
||||
fsroot_ = os.path.join(fsroot, "")
|
||||
for dn in dirs:
|
||||
ap = os.path.join(fsroot, dn)
|
||||
ap = fsroot_ + dn
|
||||
zs = self.gen_fk(2, self.args.dk_salt, ap, 0, 0)[:dk_sz]
|
||||
kdirs.append(dn + "?k=" + zs)
|
||||
dirs = kdirs
|
||||
|
@ -5470,6 +5541,10 @@ class HttpCli(object):
|
|||
and ("*" in x.axs.uwrite or self.uname in x.axs.uwrite or x == shr_dbv)
|
||||
]
|
||||
|
||||
q = ""
|
||||
qp = (0,)
|
||||
q_c = -1
|
||||
|
||||
for vol in allvols:
|
||||
cur = idx.get_cur(vol)
|
||||
if not cur:
|
||||
|
@ -5477,17 +5552,31 @@ class HttpCli(object):
|
|||
|
||||
nfk, fk_alg = fk_vols.get(vol) or (0, 0)
|
||||
|
||||
zi = vol.flags["unp_who"]
|
||||
if q_c != zi:
|
||||
q_c = zi
|
||||
q = "select sz, rd, fn, at from up where "
|
||||
if zi == 1:
|
||||
q += "ip=? and un=?"
|
||||
qp = (self.ip, self.uname, lim)
|
||||
elif zi == 2:
|
||||
q += "ip=?"
|
||||
qp = (self.ip, lim)
|
||||
if zi == 3:
|
||||
q += "un=?"
|
||||
qp = (self.uname, lim)
|
||||
q += " and at>? order by at desc"
|
||||
|
||||
n = 2000
|
||||
q = "select sz, rd, fn, at from up where ip=? and at>? order by at desc"
|
||||
for sz, rd, fn, at in cur.execute(q, (self.ip, lim)):
|
||||
for sz, rd, fn, at in cur.execute(q, qp):
|
||||
vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x)
|
||||
if nfi == 0 or (nfi == 1 and vfi in vp):
|
||||
if nfi == 0 or (nfi == 1 and vfi in vp.lower()):
|
||||
pass
|
||||
elif nfi == 2:
|
||||
if not vp.startswith(vfi):
|
||||
if not vp.lower().startswith(vfi):
|
||||
continue
|
||||
elif nfi == 3:
|
||||
if not vp.endswith(vfi):
|
||||
if not vp.lower().endswith(vfi):
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
@ -5604,16 +5693,16 @@ class HttpCli(object):
|
|||
continue
|
||||
|
||||
n = 1000
|
||||
q = "select sz, rd, fn, ip, at from up where at>0 order by at desc"
|
||||
for sz, rd, fn, ip, at in cur.execute(q):
|
||||
q = "select sz, rd, fn, ip, at, un from up where at>0 order by at desc"
|
||||
for sz, rd, fn, ip, at, un in cur.execute(q):
|
||||
vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x)
|
||||
if nfi == 0 or (nfi == 1 and vfi in vp):
|
||||
if nfi == 0 or (nfi == 1 and vfi in vp.lower()):
|
||||
pass
|
||||
elif nfi == 2:
|
||||
if not vp.startswith(vfi):
|
||||
if not vp.lower().startswith(vfi):
|
||||
continue
|
||||
elif nfi == 3:
|
||||
if not vp.endswith(vfi):
|
||||
if not vp.lower().endswith(vfi):
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
@ -5626,6 +5715,7 @@ class HttpCli(object):
|
|||
"sz": sz,
|
||||
"ip": ip,
|
||||
"at": at,
|
||||
"un": un,
|
||||
"nfk": nfk,
|
||||
"adm": adm,
|
||||
}
|
||||
|
@ -5670,12 +5760,16 @@ class HttpCli(object):
|
|||
adm = rv.pop("adm")
|
||||
if not adm:
|
||||
rv["ip"] = "(You)" if rv["ip"] == self.ip else "(?)"
|
||||
if rv["un"] not in ("*", self.uname):
|
||||
rv["un"] = "(?)"
|
||||
else:
|
||||
for rv in ret:
|
||||
adm = rv.pop("adm")
|
||||
if not adm:
|
||||
rv["ip"] = "(You)" if rv["ip"] == self.ip else "(?)"
|
||||
rv["at"] = 0
|
||||
if rv["un"] not in ("*", self.uname):
|
||||
rv["un"] = "(?)"
|
||||
|
||||
if self.is_vproxied:
|
||||
for v in ret:
|
||||
|
@ -5859,6 +5953,14 @@ class HttpCli(object):
|
|||
except:
|
||||
raise Pebkac(400, "you dont have all the perms you tried to grant")
|
||||
|
||||
zs = vfs.flags["shr_who"]
|
||||
if zs == "auth" and self.uname != "*":
|
||||
pass
|
||||
elif zs == "a" and self.uname in vfs.axs.uadmin:
|
||||
pass
|
||||
else:
|
||||
raise Pebkac(400, "you dont have perms to create shares from this volume")
|
||||
|
||||
ap, reals, _ = vfs.ls(
|
||||
rem, self.uname, not self.args.no_scandir, [[s_rd, s_wr, s_mv, s_del]]
|
||||
)
|
||||
|
@ -5954,7 +6056,9 @@ class HttpCli(object):
|
|||
self.asrv.vfs.get(vdst, self.uname, False, True, False, True)
|
||||
wunlink(self.log, dabs, dvn.flags)
|
||||
|
||||
x = self.conn.hsrv.broker.ask("up2k.handle_mv", self.uname, self.ip, vsrc, vdst)
|
||||
x = self.conn.hsrv.broker.ask(
|
||||
"up2k.handle_mv", self.ouparam.get("akey"), self.uname, self.ip, vsrc, vdst
|
||||
)
|
||||
self.loud_reply(x.get(), status=201)
|
||||
return True
|
||||
|
||||
|
@ -5984,10 +6088,21 @@ class HttpCli(object):
|
|||
self.asrv.vfs.get(vdst, self.uname, False, True, False, True)
|
||||
wunlink(self.log, dabs, dvn.flags)
|
||||
|
||||
x = self.conn.hsrv.broker.ask("up2k.handle_cp", self.uname, self.ip, vsrc, vdst)
|
||||
x = self.conn.hsrv.broker.ask(
|
||||
"up2k.handle_cp", self.ouparam.get("akey"), self.uname, self.ip, vsrc, vdst
|
||||
)
|
||||
self.loud_reply(x.get(), status=201)
|
||||
return True
|
||||
|
||||
def handle_fs_abrt(self):
|
||||
if self.args.no_fs_abrt:
|
||||
t = "aborting an ongoing copy/move is disabled in server config"
|
||||
raise Pebkac(403, t)
|
||||
|
||||
self.conn.hsrv.broker.say("up2k.handle_fs_abrt", self.uparam["fs_abrt"])
|
||||
self.loud_reply("aborting", status=200)
|
||||
return True
|
||||
|
||||
def tx_ls(self, ls: dict[str, Any]) -> bool:
|
||||
dirs = ls["dirs"]
|
||||
files = ls["files"]
|
||||
|
@ -6107,16 +6222,13 @@ class HttpCli(object):
|
|||
add_og = "og" in vn.flags
|
||||
if add_og:
|
||||
if "th" in self.uparam or "raw" in self.uparam:
|
||||
og_ua = add_og = False
|
||||
elif self.args.og_ua:
|
||||
og_ua = add_og = self.args.og_ua.search(self.ua)
|
||||
else:
|
||||
og_ua = False
|
||||
add_og = True
|
||||
add_og = False
|
||||
elif vn.flags["og_ua"]:
|
||||
add_og = vn.flags["og_ua"].search(self.ua)
|
||||
og_fn = ""
|
||||
|
||||
if "v" in self.uparam:
|
||||
add_og = og_ua = True
|
||||
add_og = True
|
||||
|
||||
if "b" in self.uparam:
|
||||
self.out_headers["X-Robots-Tag"] = "noindex, nofollow"
|
||||
|
@ -6237,7 +6349,7 @@ class HttpCli(object):
|
|||
|
||||
is_md = abspath.lower().endswith(".md")
|
||||
if add_og and not is_md:
|
||||
if og_ua or self.host not in self.headers.get("referer", ""):
|
||||
if self.host not in self.headers.get("referer", ""):
|
||||
self.vpath, og_fn = vsplit(self.vpath)
|
||||
vpath = self.vpath
|
||||
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
|
||||
|
@ -6277,7 +6389,14 @@ class HttpCli(object):
|
|||
except:
|
||||
self.log("#wow #whoa")
|
||||
|
||||
if not self.args.nid:
|
||||
zi = vn.flags["du_iwho"]
|
||||
if zi and (
|
||||
zi == 9
|
||||
or (zi == 7 and self.uname != "*")
|
||||
or (zi == 5 and self.can_write)
|
||||
or (zi == 4 and self.can_write and self.can_read)
|
||||
or (zi == 3 and self.can_admin)
|
||||
):
|
||||
free, total, zs = get_df(abspath, False)
|
||||
if total:
|
||||
h1 = humansize(free or 0)
|
||||
|
@ -6584,13 +6703,15 @@ class HttpCli(object):
|
|||
tags = {k: v for k, v in r}
|
||||
|
||||
if is_admin:
|
||||
q = "select ip, at from up where rd=? and fn=?"
|
||||
q = "select ip, at, un from up where rd=? and fn=?"
|
||||
try:
|
||||
zs1, zs2 = icur.execute(q, erd_efn).fetchone()
|
||||
zs1, zs2, zs3 = icur.execute(q, erd_efn).fetchone()
|
||||
if zs1:
|
||||
tags["up_ip"] = zs1
|
||||
if zs2:
|
||||
tags[".up_at"] = zs2
|
||||
if zs3:
|
||||
tags["up_by"] = zs3
|
||||
except:
|
||||
pass
|
||||
elif add_up_at:
|
||||
|
@ -6611,7 +6732,7 @@ class HttpCli(object):
|
|||
|
||||
lmte = list(mte)
|
||||
if self.can_admin:
|
||||
lmte.extend(("up_ip", ".up_at"))
|
||||
lmte.extend(("up_by", "up_ip", ".up_at"))
|
||||
|
||||
if "nodirsz" not in vf:
|
||||
tagset.add(".files")
|
||||
|
|
|
@ -70,6 +70,7 @@ from .util import (
|
|||
build_netmap,
|
||||
has_resource,
|
||||
ipnorm,
|
||||
load_ipr,
|
||||
load_ipu,
|
||||
load_resource,
|
||||
min_ex,
|
||||
|
@ -193,6 +194,11 @@ class HttpSrv(object):
|
|||
else:
|
||||
self.ipu_iu = self.ipu_nm = None
|
||||
|
||||
if self.args.ipr:
|
||||
self.ipr = load_ipr(self.log, self.args.ipr)
|
||||
else:
|
||||
self.ipr = None
|
||||
|
||||
self.ipa_nm = build_netmap(self.args.ipa)
|
||||
self.xff_nm = build_netmap(self.args.xff_src)
|
||||
self.xff_lan = build_netmap("lan")
|
||||
|
@ -565,7 +571,7 @@ class HttpSrv(object):
|
|||
|
||||
v = self.E.t0
|
||||
try:
|
||||
with os.scandir(os.path.join(self.E.mod, "web")) as dh:
|
||||
with os.scandir(self.E.mod_ + "web") as dh:
|
||||
for fh in dh:
|
||||
inf = fh.stat()
|
||||
v = max(v, inf.st_mtime)
|
||||
|
|
|
@ -27,7 +27,7 @@ from .stolen.dnslib import (
|
|||
DNSRecord,
|
||||
set_avahi_379,
|
||||
)
|
||||
from .util import CachedSet, Daemon, Netdev, list_ips, min_ex
|
||||
from .util import IP6_LL, CachedSet, Daemon, Netdev, list_ips, min_ex
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .svchub import SvcHub
|
||||
|
@ -76,7 +76,8 @@ class MDNS(MCast):
|
|||
if not self.args.zm_nwa_1:
|
||||
set_avahi_379()
|
||||
|
||||
zs = self.args.name + ".local."
|
||||
zs = self.args.zm_fqdn or (self.args.name + ".local")
|
||||
zs = zs.replace("--name", self.args.name).rstrip(".") + "."
|
||||
zs = zs.encode("ascii", "replace").decode("ascii", "replace")
|
||||
self.hn = "-".join(x for x in zs.split("?") if x) or (
|
||||
"vault-{}".format(random.randint(1, 255))
|
||||
|
@ -374,7 +375,7 @@ class MDNS(MCast):
|
|||
cip = addr[0]
|
||||
v6 = ":" in cip
|
||||
if (cip.startswith("169.254") and not self.ll_ok) or (
|
||||
v6 and not cip.startswith("fe80")
|
||||
v6 and not cip.startswith(IP6_LL)
|
||||
):
|
||||
return
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ from .util import (
|
|||
)
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Optional, Union
|
||||
from typing import IO, Any, Optional, Union
|
||||
|
||||
from .util import NamedLogger, RootLogger
|
||||
|
||||
|
@ -176,6 +176,9 @@ def au_unpk(
|
|||
raise Exception("no images inside cbz")
|
||||
fi = zf.open(using)
|
||||
|
||||
elif pk == "epub":
|
||||
fi = get_cover_from_epub(log, abspath)
|
||||
|
||||
else:
|
||||
raise Exception("unknown compression %s" % (pk,))
|
||||
|
||||
|
@ -205,7 +208,7 @@ def au_unpk(
|
|||
|
||||
def ffprobe(
|
||||
abspath: str, timeout: int = 60
|
||||
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]:
|
||||
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:
|
||||
cmd = [
|
||||
b"ffprobe",
|
||||
b"-hide_banner",
|
||||
|
@ -219,8 +222,17 @@ def ffprobe(
|
|||
return parse_ffprobe(so)
|
||||
|
||||
|
||||
def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]:
|
||||
"""ffprobe -show_format -show_streams"""
|
||||
def parse_ffprobe(
|
||||
txt: str,
|
||||
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:
|
||||
"""
|
||||
txt: output from ffprobe -show_format -show_streams
|
||||
returns:
|
||||
* normalized tags
|
||||
* original/raw tags
|
||||
* list of streams
|
||||
* format props
|
||||
"""
|
||||
streams = []
|
||||
fmt = {}
|
||||
g = {}
|
||||
|
@ -313,7 +325,7 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
|
|||
ret[rk] = v1
|
||||
|
||||
if ret.get("vc") == "ansi": # shellscript
|
||||
return {}, {}
|
||||
return {}, {}, [], {}
|
||||
|
||||
for strm in streams:
|
||||
for sk, sv in strm.items():
|
||||
|
@ -362,7 +374,77 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
|
|||
zero = int("0")
|
||||
zd = {k: (zero, v) for k, v in ret.items()}
|
||||
|
||||
return zd, md
|
||||
return zd, md, streams, fmt
|
||||
|
||||
|
||||
def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]:
|
||||
import zipfile
|
||||
|
||||
from .dxml import parse_xml
|
||||
|
||||
try:
|
||||
from urlparse import urljoin # Python2
|
||||
except ImportError:
|
||||
from urllib.parse import urljoin # Python3
|
||||
|
||||
with zipfile.ZipFile(abspath, "r") as z:
|
||||
# First open the container file to find the package document (.opf file)
|
||||
try:
|
||||
container_root = parse_xml(z.read("META-INF/container.xml").decode())
|
||||
except KeyError:
|
||||
log("epub: no container file found in %s" % (abspath,))
|
||||
return None
|
||||
|
||||
# https://www.w3.org/TR/epub-33/#sec-container.xml-rootfile-elem
|
||||
container_ns = {"": "urn:oasis:names:tc:opendocument:xmlns:container"}
|
||||
# One file could contain multiple package documents, default to the first one
|
||||
rootfile_path = container_root.find("./rootfiles/rootfile", container_ns).get(
|
||||
"full-path"
|
||||
)
|
||||
|
||||
# Then open the first package document to find the path of the cover image
|
||||
try:
|
||||
package_root = parse_xml(z.read(rootfile_path).decode())
|
||||
except KeyError:
|
||||
log("epub: no package document found in %s" % (abspath,))
|
||||
return None
|
||||
|
||||
# https://www.w3.org/TR/epub-33/#sec-package-doc
|
||||
package_ns = {"": "http://www.idpf.org/2007/opf"}
|
||||
# https://www.w3.org/TR/epub-33/#sec-cover-image
|
||||
coverimage_path_node = package_root.find(
|
||||
"./manifest/item[@properties='cover-image']", package_ns
|
||||
)
|
||||
if coverimage_path_node is not None:
|
||||
coverimage_path = coverimage_path_node.get("href")
|
||||
else:
|
||||
# This might be an EPUB2 file, try the legacy way of specifying covers
|
||||
coverimage_path = _get_cover_from_epub2(log, package_root, package_ns)
|
||||
|
||||
# This url is either absolute (in the .epub) or relative to the package document
|
||||
adjusted_cover_path = urljoin(rootfile_path, coverimage_path)
|
||||
|
||||
return z.open(adjusted_cover_path)
|
||||
|
||||
|
||||
def _get_cover_from_epub2(
|
||||
log: "NamedLogger", package_root, package_ns
|
||||
) -> Optional[str]:
|
||||
# <meta name="cover" content="id-to-cover-image"> in <metadata>, then
|
||||
# <item> in <manifest>
|
||||
cover_id = package_root.find("./metadata/meta[@name='cover']", package_ns).get(
|
||||
"content"
|
||||
)
|
||||
|
||||
if not cover_id:
|
||||
return None
|
||||
|
||||
for node in package_root.iterfind("./manifest/item", package_ns):
|
||||
if node.get("id") == cover_id:
|
||||
cover_path = node.get("href")
|
||||
return cover_path
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class MTag(object):
|
||||
|
@ -633,7 +715,7 @@ class MTag(object):
|
|||
if not bos.path.isfile(abspath):
|
||||
return {}
|
||||
|
||||
ret, md = ffprobe(abspath, self.args.mtag_to)
|
||||
ret, md, _, _ = ffprobe(abspath, self.args.mtag_to)
|
||||
|
||||
if self.args.mtag_vv:
|
||||
for zd in (ret, dict(md)):
|
||||
|
|
|
@ -15,7 +15,7 @@ from ipaddress import (
|
|||
)
|
||||
|
||||
from .__init__ import MACOS, TYPE_CHECKING
|
||||
from .util import Daemon, Netdev, find_prefix, min_ex, spack
|
||||
from .util import IP6_LL, IP64_LL, Daemon, Netdev, find_prefix, min_ex, spack
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .svchub import SvcHub
|
||||
|
@ -145,7 +145,7 @@ class MCast(object):
|
|||
all_selected = ips[:]
|
||||
|
||||
# discard non-linklocal ipv6
|
||||
ips = [x for x in ips if ":" not in x or x.startswith("fe80")]
|
||||
ips = [x for x in ips if ":" not in x or x.startswith(IP6_LL)]
|
||||
|
||||
if not ips:
|
||||
raise NoIPs()
|
||||
|
@ -183,7 +183,7 @@ class MCast(object):
|
|||
srv.ips[oth_ip.split("/")[0]] = ipaddress.ip_network(oth_ip, False)
|
||||
|
||||
# gvfs breaks if a linklocal ip appears in a dns reply
|
||||
ll = {k: v for k, v in srv.ips.items() if k.startswith(("169.254", "fe80"))}
|
||||
ll = {k: v for k, v in srv.ips.items() if k.startswith(IP64_LL)}
|
||||
rt = {k: v for k, v in srv.ips.items() if k not in ll}
|
||||
|
||||
if self.args.ll or not rt:
|
||||
|
|
|
@ -25,6 +25,7 @@ class PWHash(object):
|
|||
self.args = args
|
||||
|
||||
zsl = args.ah_alg.split(",")
|
||||
zsl = [x.strip() for x in zsl]
|
||||
alg = zsl[0]
|
||||
if alg == "none":
|
||||
alg = ""
|
||||
|
|
|
@ -318,7 +318,7 @@ class SMB(object):
|
|||
t = "blocked rename (no-move-acc %s): /%s @%s"
|
||||
yeet(t % (vfs1.axs.umove, vp1, uname))
|
||||
|
||||
self.hub.up2k.handle_mv(uname, "1.7.6.2", vp1, vp2)
|
||||
self.hub.up2k.handle_mv("", uname, "1.7.6.2", vp1, vp2)
|
||||
try:
|
||||
bos.makedirs(ap2, vf=vfs2.flags)
|
||||
except:
|
||||
|
@ -373,7 +373,7 @@ class SMB(object):
|
|||
t = "blocked utime (no-write-acc %s): /%s @%s"
|
||||
yeet(t % (vfs.axs.uwrite, vpath, uname))
|
||||
|
||||
return bos.utime(ap, times)
|
||||
bos.utime_c(info, ap, int(times[1]), False)
|
||||
|
||||
def _p_exists(self, vpath: str) -> bool:
|
||||
# ap = "?"
|
||||
|
|
|
@ -200,6 +200,25 @@ class QrCode(object):
|
|||
|
||||
return "\n".join(rows)
|
||||
|
||||
def to_png(self, zoom, pad, bg, fg, ap) -> None:
|
||||
from PIL import Image
|
||||
|
||||
tab = self.modules
|
||||
sz = self.size
|
||||
psz = sz + pad * 2
|
||||
if bg:
|
||||
img = Image.new("RGB", (psz, psz), bg)
|
||||
else:
|
||||
img = Image.new("RGBA", (psz, psz), (0, 0, 0, 0))
|
||||
fg = (fg[0], fg[1], fg[2], 255)
|
||||
for y in range(sz):
|
||||
for x in range(sz):
|
||||
if tab[y][x]:
|
||||
img.putpixel((x + pad, y + pad), fg)
|
||||
if zoom != 1:
|
||||
img = img.resize((sz * zoom, sz * zoom), Image.Resampling.NEAREST)
|
||||
img.save(ap)
|
||||
|
||||
def _draw_function_patterns(self) -> None:
|
||||
# Draw horizontal and vertical timing patterns
|
||||
for i in range(self.size):
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
|
@ -26,7 +27,7 @@ if True: # pylint: disable=using-constant-test
|
|||
from typing import Any, Optional, Union
|
||||
|
||||
from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode
|
||||
from .authsrv import BAD_CFG, AuthSrv
|
||||
from .authsrv import BAD_CFG, AuthSrv, n_du_who, n_ver_who
|
||||
from .bos import bos
|
||||
from .cert import ensure_cert
|
||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN
|
||||
|
@ -38,6 +39,7 @@ from .th_srv import (
|
|||
HAVE_FFPROBE,
|
||||
HAVE_HEIF,
|
||||
HAVE_PIL,
|
||||
HAVE_RAW,
|
||||
HAVE_VIPS,
|
||||
HAVE_WEBP,
|
||||
ThumbSrv,
|
||||
|
@ -64,6 +66,7 @@ from .util import (
|
|||
build_netmap,
|
||||
expat_ver,
|
||||
gzip,
|
||||
load_ipr,
|
||||
load_ipu,
|
||||
lock_file,
|
||||
min_ex,
|
||||
|
@ -72,6 +75,7 @@ from .util import (
|
|||
pybin,
|
||||
start_log_thrs,
|
||||
start_stackmon,
|
||||
termsize,
|
||||
ub64enc,
|
||||
)
|
||||
|
||||
|
@ -130,6 +134,7 @@ class SvcHub(object):
|
|||
self.nsigs = 3
|
||||
self.retcode = 0
|
||||
self.httpsrv_up = 0
|
||||
self.qr_tsz = None
|
||||
|
||||
self.log_mutex = threading.Lock()
|
||||
self.cday = 0
|
||||
|
@ -152,6 +157,7 @@ class SvcHub(object):
|
|||
args.no_del = True
|
||||
args.no_mv = True
|
||||
args.hardlink = True
|
||||
args.dav_auth = True
|
||||
args.vague_403 = True
|
||||
args.nih = True
|
||||
|
||||
|
@ -240,7 +246,7 @@ class SvcHub(object):
|
|||
t = "WARNING: --th-ram-max is very small (%.2f GiB); will not be able to %s"
|
||||
self.log("root", t % (args.th_ram_max, zs), 3)
|
||||
|
||||
if args.chpw and args.idp_h_usr:
|
||||
if args.chpw and args.have_idp_hdrs:
|
||||
t = "ERROR: user-changeable passwords is incompatible with IdP/identity-providers; you must disable either --chpw or --idp-h-usr"
|
||||
self.log("root", t, 1)
|
||||
raise Exception(t)
|
||||
|
@ -256,6 +262,10 @@ class SvcHub(object):
|
|||
setattr(args, "ipu_iu", iu)
|
||||
setattr(args, "ipu_nm", nm)
|
||||
|
||||
if args.ipr:
|
||||
ipr = load_ipr(self.log, args.ipr, True)
|
||||
setattr(args, "ipr_u", ipr)
|
||||
|
||||
for zs in "ah_salt fk_salt dk_salt".split():
|
||||
if getattr(args, "show_%s" % (zs,)):
|
||||
self.log("root", "effective %s is %s" % (zs, getattr(args, zs)))
|
||||
|
@ -265,7 +275,7 @@ class SvcHub(object):
|
|||
args.no_ses = True
|
||||
args.shr = ""
|
||||
|
||||
if args.idp_store and args.idp_h_usr:
|
||||
if args.idp_store and args.have_idp_hdrs:
|
||||
self.setup_db("idp")
|
||||
|
||||
if not self.args.no_ses:
|
||||
|
@ -279,6 +289,14 @@ class SvcHub(object):
|
|||
ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)]
|
||||
args.theme = "{0}{1} {0} {1}".format(ch, bri)
|
||||
|
||||
if args.nid:
|
||||
args.du_who = "no"
|
||||
args.du_iwho = n_du_who(args.du_who)
|
||||
|
||||
if args.ver and args.ver_who == "no":
|
||||
args.ver_who = "all"
|
||||
args.ver_iwho = n_ver_who(args.ver_who)
|
||||
|
||||
if args.nih:
|
||||
args.vname = ""
|
||||
args.doctitle = args.doctitle.replace(" @ --name", "")
|
||||
|
@ -316,11 +334,13 @@ class SvcHub(object):
|
|||
|
||||
self._feature_test()
|
||||
|
||||
decs = {k: 1 for k in self.args.th_dec.split(",")}
|
||||
decs = {k.strip(): 1 for k in self.args.th_dec.split(",")}
|
||||
if not HAVE_VIPS:
|
||||
decs.pop("vips", None)
|
||||
if not HAVE_PIL:
|
||||
decs.pop("pil", None)
|
||||
if not HAVE_RAW:
|
||||
decs.pop("raw", None)
|
||||
if not HAVE_FFMPEG or not HAVE_FFPROBE:
|
||||
decs.pop("ff", None)
|
||||
|
||||
|
@ -422,11 +442,14 @@ class SvcHub(object):
|
|||
|
||||
# create netmaps early to avoid firewall gaps,
|
||||
# but the mutex blocks multiprocessing startup
|
||||
for zs in "ipu_iu ftp_ipa_nm tftp_ipa_nm".split():
|
||||
for zs in "ipu_nm ftp_ipa_nm tftp_ipa_nm".split():
|
||||
try:
|
||||
getattr(args, zs).mutex = threading.Lock()
|
||||
except:
|
||||
pass
|
||||
if args.ipr:
|
||||
for nm in args.ipr_u.values():
|
||||
nm.mutex = threading.Lock()
|
||||
|
||||
def _db_onfail_ses(self) -> None:
|
||||
self.args.no_ses = True
|
||||
|
@ -772,6 +795,80 @@ class SvcHub(object):
|
|||
def sigterm(self) -> None:
|
||||
self.signal_handler(signal.SIGTERM, None)
|
||||
|
||||
def sticky_qr(self) -> None:
|
||||
self._sticky_qr()
|
||||
|
||||
def _unsticky_qr(self, flush=True) -> None:
|
||||
print("\033[s\033[J\033[r\033[u", file=sys.stderr, end="")
|
||||
if flush:
|
||||
sys.stderr.flush()
|
||||
|
||||
def _sticky_qr(self, force: bool = False) -> None:
|
||||
sz = termsize()
|
||||
if self.qr_tsz == sz:
|
||||
if not force:
|
||||
return
|
||||
else:
|
||||
force = False
|
||||
|
||||
if self.qr_tsz:
|
||||
self._unsticky_qr(False)
|
||||
else:
|
||||
atexit.register(self._unsticky_qr)
|
||||
|
||||
tw, th = self.qr_tsz = sz
|
||||
zs1, qr = self.tcpsrv.qr.split("\n", 1)
|
||||
url, colr = zs1.split(" ", 1)
|
||||
nl = len(qr.split("\n")) # numlines
|
||||
lp = 3 if nl * 2 + 4 < tw else 0 # leftpad
|
||||
lp0 = lp
|
||||
if self.args.qr_pin == 2:
|
||||
url = ""
|
||||
else:
|
||||
while lp and (nl + lp) * 2 + len(url) + 1 > tw:
|
||||
lp -= 1
|
||||
if (nl + lp) * 2 + len(url) + 1 > tw:
|
||||
qr = url + "\n" + qr
|
||||
url = ""
|
||||
nl += 1
|
||||
lp = lp0
|
||||
sh = 1 + th - nl
|
||||
if lp:
|
||||
zs = " " * lp
|
||||
qr = zs + qr.replace("\n", "\n" + zs)
|
||||
if url:
|
||||
url = "%s\033[%d;%dH%s\033[0m" % (colr, sh + 1, (nl + lp) * 2, url)
|
||||
qr = colr + qr
|
||||
|
||||
t = "%s\033[%dA" % ("\n" * nl, nl)
|
||||
t = "%s\033[s\033[1;%dr\033[%dH%s%s\033[u" % (t, sh - 1, sh, qr, url)
|
||||
if not force:
|
||||
self.log("qr", "sticky-qrcode %sx%s,%s" % (tw, th, sh), 6)
|
||||
self.pr(t, file=sys.stderr, end="")
|
||||
|
||||
def _qr_thr(self):
|
||||
qr = self.tcpsrv.qr
|
||||
w8 = self.args.qr_wait
|
||||
if w8:
|
||||
time.sleep(w8)
|
||||
self.log("qr-code", qr)
|
||||
w8 = self.args.qr_every
|
||||
msg = "%s\033[%dA" % (qr, len(qr.split("\n")))
|
||||
while w8:
|
||||
time.sleep(w8)
|
||||
if self.stopping:
|
||||
break
|
||||
if self.args.qr_pin:
|
||||
self._sticky_qr(True)
|
||||
else:
|
||||
self.log("qr-code", msg)
|
||||
w8 = self.args.qr_winch
|
||||
while w8:
|
||||
time.sleep(w8)
|
||||
if self.stopping:
|
||||
break
|
||||
self._sticky_qr()
|
||||
|
||||
def cb_httpsrv_up(self) -> None:
|
||||
self.httpsrv_up += 1
|
||||
if self.httpsrv_up != self.broker.num_workers:
|
||||
|
@ -784,7 +881,12 @@ class SvcHub(object):
|
|||
break
|
||||
|
||||
if self.tcpsrv.qr:
|
||||
self.log("qr-code", self.tcpsrv.qr)
|
||||
if self.args.qr_pin:
|
||||
self.sticky_qr()
|
||||
if self.args.qr_wait or self.args.qr_every or self.args.qr_winch:
|
||||
Daemon(self._qr_thr, "qr")
|
||||
elif not self.args.qr_pin:
|
||||
self.log("qr-code", self.tcpsrv.qr)
|
||||
else:
|
||||
self.log("root", "workers OK\n")
|
||||
|
||||
|
@ -811,6 +913,7 @@ class SvcHub(object):
|
|||
(HAVE_ZMQ, "pyzmq", "send zeromq messages from event-hooks"),
|
||||
(HAVE_HEIF, "pillow-heif", "read .heif images with pillow (rarely useful)"),
|
||||
(HAVE_AVIF, "pillow-avif", "read .avif images with pillow (rarely useful)"),
|
||||
(HAVE_RAW, "rawpy", "read RAW images"),
|
||||
]
|
||||
if ANYWIN:
|
||||
to_check += [
|
||||
|
@ -881,6 +984,24 @@ class SvcHub(object):
|
|||
t = "WARNING:\nDisabling WebDAV support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n"
|
||||
self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1)
|
||||
|
||||
if not E.scfg and not al.unsafe_state and not os.getenv("PRTY_UNSAFE_STATE"):
|
||||
t = "because runtime config is currently being stored in an untrusted emergency-fallback location. Please fix your environment so either XDG_CONFIG_HOME or ~/.config can be used instead, or disable this safeguard with --unsafe-state or env-var PRTY_UNSAFE_STATE=1."
|
||||
if not al.no_ses:
|
||||
al.no_ses = True
|
||||
t2 = "A consequence of this misconfiguration is that passwords will now be sent in the HTTP-header of every request!"
|
||||
self.log("root", "WARNING:\nWill disable sessions %s %s" % (t, t2), 1)
|
||||
if al.idp_store == 1:
|
||||
al.idp_store = 0
|
||||
self.log("root", "WARNING:\nDisabling --idp-store %s" % (t,), 3)
|
||||
if al.idp_store:
|
||||
t2 = "ERROR: Cannot enable --idp-store %s" % (t,)
|
||||
self.log("root", t2, 1)
|
||||
raise Exception(t2)
|
||||
if al.shr:
|
||||
t2 = "ERROR: Cannot enable shares %s" % (t,)
|
||||
self.log("root", t2, 1)
|
||||
raise Exception(t2)
|
||||
|
||||
def _process_config(self) -> bool:
|
||||
al = self.args
|
||||
|
||||
|
@ -969,10 +1090,23 @@ class SvcHub(object):
|
|||
al.sus_urls = None
|
||||
|
||||
al.xff_hdr = al.xff_hdr.lower()
|
||||
al.idp_h_usr = al.idp_h_usr.lower()
|
||||
al.idp_h_usr = [x.lower() for x in al.idp_h_usr or []]
|
||||
al.idp_h_grp = al.idp_h_grp.lower()
|
||||
al.idp_h_key = al.idp_h_key.lower()
|
||||
|
||||
al.idp_hm_usr_p = {}
|
||||
for zs0 in al.idp_hm_usr or []:
|
||||
try:
|
||||
sep = zs0[:1]
|
||||
hn, zs1, zs2 = zs0[1:].split(sep)
|
||||
hn = hn.lower()
|
||||
if hn in al.idp_hm_usr_p:
|
||||
al.idp_hm_usr_p[hn][zs1] = zs2
|
||||
else:
|
||||
al.idp_hm_usr_p[hn] = {zs1: zs2}
|
||||
except:
|
||||
raise Exception("invalid --idp-hm-usr [%s]" % (zs0,))
|
||||
|
||||
al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa, True)
|
||||
al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa, True)
|
||||
|
||||
|
@ -1024,7 +1158,7 @@ class SvcHub(object):
|
|||
al.tcolor = "".join([x * 2 for x in al.tcolor])
|
||||
|
||||
zs = al.u2sz
|
||||
zsl = zs.split(",")
|
||||
zsl = [x.strip() for x in zs.split(",")]
|
||||
if len(zsl) not in (1, 3):
|
||||
t = "invalid --u2sz; must be either one number, or a comma-separated list of three numbers (min,default,max)"
|
||||
raise Exception(t)
|
||||
|
@ -1400,7 +1534,14 @@ class SvcHub(object):
|
|||
|
||||
fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n"
|
||||
if self.no_ansi:
|
||||
fmt = "%s %-21s %s\n"
|
||||
if c == 1:
|
||||
fmt = "%s %-21s CRIT: %s\n"
|
||||
elif c == 3:
|
||||
fmt = "%s %-21s WARN: %s\n"
|
||||
elif c == 6:
|
||||
fmt = "%s %-21s BTW: %s\n"
|
||||
else:
|
||||
fmt = "%s %-21s LOG: %s\n"
|
||||
if "\033" in msg:
|
||||
msg = RE_ANSI.sub("", msg)
|
||||
if "\033" in src:
|
||||
|
|
|
@ -9,13 +9,14 @@ import time
|
|||
|
||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
|
||||
from .cert import gencert
|
||||
from .stolen.qrcodegen import QrCode
|
||||
from .stolen.qrcodegen import QrCode, qr2svg
|
||||
from .util import (
|
||||
E_ACCESS,
|
||||
E_ADDR_IN_USE,
|
||||
E_ADDR_NOT_AVAIL,
|
||||
E_UNREACH,
|
||||
HAVE_IPV6,
|
||||
IP6_LL,
|
||||
IP6ALL,
|
||||
VF_CAREFUL,
|
||||
Netdev,
|
||||
|
@ -140,12 +141,12 @@ class TcpSrv(object):
|
|||
# keep IPv6 LL-only nics
|
||||
ll_ok: set[str] = set()
|
||||
for ip, nd in self.netdevs.items():
|
||||
if not ip.startswith("fe80"):
|
||||
if not ip.startswith(IP6_LL):
|
||||
continue
|
||||
|
||||
just_ll = True
|
||||
for ip2, nd2 in self.netdevs.items():
|
||||
if nd == nd2 and ":" in ip2 and not ip2.startswith("fe80"):
|
||||
if nd == nd2 and ":" in ip2 and not ip2.startswith(IP6_LL):
|
||||
just_ll = False
|
||||
|
||||
if just_ll or self.args.ll:
|
||||
|
@ -164,7 +165,7 @@ class TcpSrv(object):
|
|||
title_vars = [x[1:] for x in self.args.wintitle.split(" ") if x.startswith("$")]
|
||||
t = "available @ {}://{}:{}/ (\033[33m{}\033[0m)"
|
||||
for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
|
||||
if ip.startswith("fe80") and ip not in ll_ok:
|
||||
if ip.startswith(IP6_LL) and ip not in ll_ok:
|
||||
continue
|
||||
|
||||
for port in sorted(self.args.p):
|
||||
|
@ -304,6 +305,10 @@ class TcpSrv(object):
|
|||
if os.path.exists(ip):
|
||||
os.unlink(ip)
|
||||
srv.bind(ip)
|
||||
if uds_gid != -1:
|
||||
os.chown(ip, -1, uds_gid)
|
||||
if uds_perm != -1:
|
||||
os.chmod(ip, uds_perm)
|
||||
else:
|
||||
tf = "%s.%d" % (ip, os.getpid())
|
||||
if os.path.exists(tf):
|
||||
|
@ -614,9 +619,17 @@ class TcpSrv(object):
|
|||
|
||||
fg = self.args.qr_fg
|
||||
bg = self.args.qr_bg
|
||||
nocolor = fg == -1
|
||||
if nocolor:
|
||||
fg = 0
|
||||
|
||||
pad = self.args.qrp
|
||||
zoom = self.args.qrz
|
||||
qrc = QrCode.encode_binary(btxt)
|
||||
|
||||
for zs in self.args.qr_file or []:
|
||||
self._qr2file(qrc, zs)
|
||||
|
||||
if zoom == 0:
|
||||
try:
|
||||
tw, th = termsize()
|
||||
|
@ -632,6 +645,8 @@ class TcpSrv(object):
|
|||
halfc = "\033[40;48;5;{0}m{1}\033[47;48;5;{2}m"
|
||||
if not fg:
|
||||
halfc = "\033[0;40m{1}\033[0;47m"
|
||||
if nocolor:
|
||||
halfc = "\033[0;7m{1}\033[0m"
|
||||
|
||||
def ansify(m: re.Match) -> str:
|
||||
return halfc.format(fg, " " * len(m.group(1)), bg)
|
||||
|
@ -641,6 +656,8 @@ class TcpSrv(object):
|
|||
|
||||
qr = qr.replace("\n", "\033[K\n") + "\033[K" # win10do
|
||||
cc = " \033[0;38;5;{0};47;48;5;{1}m" if fg else " \033[0;30;47m"
|
||||
if nocolor:
|
||||
cc = " \033[0m"
|
||||
t = cc + "\n{2}\033[999G\033[0m\033[J"
|
||||
t = t.format(fg, bg, qr)
|
||||
if ANYWIN:
|
||||
|
@ -648,3 +665,29 @@ class TcpSrv(object):
|
|||
t = t.replace("\n", "`\n`")
|
||||
|
||||
return txt + t
|
||||
|
||||
def _qr2file(self, qrc: QrCode, txt: str):
|
||||
if ".txt:" in txt or ".svg:" in txt:
|
||||
ap, zs1, zs2 = txt.rsplit(":", 2)
|
||||
bg = fg = ""
|
||||
else:
|
||||
ap, zs1, zs2, bg, fg = txt.rsplit(":", 4)
|
||||
zoom = int(zs1)
|
||||
pad = int(zs2)
|
||||
|
||||
if ap.endswith(".txt"):
|
||||
if zoom not in (1, 2):
|
||||
raise Exception("invalid zoom for qr.txt; must be 1 or 2")
|
||||
with open(ap, "wb") as f:
|
||||
f.write(qrc.render(zoom, pad).encode("utf-8"))
|
||||
elif ap.endswith(".svg"):
|
||||
with open(ap, "wb") as f:
|
||||
f.write(qr2svg(qrc, pad).encode("utf-8"))
|
||||
else:
|
||||
qrc.to_png(zoom, pad, self._h2i(bg), self._h2i(fg), ap)
|
||||
|
||||
def _h2i(self, hs):
|
||||
try:
|
||||
return tuple(int(hs[i : i + 2], 16) for i in (0, 2, 4))
|
||||
except:
|
||||
return None
|
||||
|
|
|
@ -36,11 +36,15 @@ class ThumbCli(object):
|
|||
if not c:
|
||||
raise Exception()
|
||||
except:
|
||||
c = {k: set() for k in ["thumbable", "pil", "vips", "ffi", "ffv", "ffa"]}
|
||||
c = {
|
||||
k: set()
|
||||
for k in ["thumbable", "pil", "vips", "raw", "ffi", "ffv", "ffa"]
|
||||
}
|
||||
|
||||
self.thumbable = c["thumbable"]
|
||||
self.fmt_pil = c["pil"]
|
||||
self.fmt_vips = c["vips"]
|
||||
self.fmt_raw = c["raw"]
|
||||
self.fmt_ffi = c["ffi"]
|
||||
self.fmt_ffv = c["ffv"]
|
||||
self.fmt_ffa = c["ffa"]
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
@ -85,7 +86,10 @@ try:
|
|||
if os.environ.get("PRTY_NO_PIL_HEIF"):
|
||||
raise Exception()
|
||||
|
||||
from pyheif_pillow_opener import register_heif_opener
|
||||
try:
|
||||
from pillow_heif import register_heif_opener
|
||||
except ImportError:
|
||||
from pyheif_pillow_opener import register_heif_opener
|
||||
|
||||
register_heif_opener()
|
||||
HAVE_HEIF = True
|
||||
|
@ -112,14 +116,28 @@ except:
|
|||
|
||||
try:
|
||||
if os.environ.get("PRTY_NO_VIPS"):
|
||||
raise Exception()
|
||||
raise ImportError()
|
||||
|
||||
HAVE_VIPS = True
|
||||
import pyvips
|
||||
|
||||
logging.getLogger("pyvips").setLevel(logging.WARNING)
|
||||
except:
|
||||
except Exception as e:
|
||||
HAVE_VIPS = False
|
||||
if not isinstance(e, ImportError):
|
||||
logging.warning("libvips found, but failed to load: " + str(e))
|
||||
|
||||
|
||||
try:
|
||||
if os.environ.get("PRTY_NO_RAW"):
|
||||
raise Exception()
|
||||
|
||||
HAVE_RAW = True
|
||||
import rawpy
|
||||
|
||||
logging.getLogger("rawpy").setLevel(logging.WARNING)
|
||||
except:
|
||||
HAVE_RAW = False
|
||||
|
||||
|
||||
th_dir_cache = {}
|
||||
|
@ -205,11 +223,19 @@ class ThumbSrv(object):
|
|||
if self.args.th_clean:
|
||||
Daemon(self.cleaner, "thumb.cln")
|
||||
|
||||
self.fmt_pil, self.fmt_vips, self.fmt_ffi, self.fmt_ffv, self.fmt_ffa = [
|
||||
(
|
||||
self.fmt_pil,
|
||||
self.fmt_vips,
|
||||
self.fmt_raw,
|
||||
self.fmt_ffi,
|
||||
self.fmt_ffv,
|
||||
self.fmt_ffa,
|
||||
) = [
|
||||
set(y.split(","))
|
||||
for y in [
|
||||
self.args.th_r_pil,
|
||||
self.args.th_r_vips,
|
||||
self.args.th_r_raw,
|
||||
self.args.th_r_ffi,
|
||||
self.args.th_r_ffv,
|
||||
self.args.th_r_ffa,
|
||||
|
@ -232,6 +258,9 @@ class ThumbSrv(object):
|
|||
if "vips" in self.args.th_dec:
|
||||
self.thumbable |= self.fmt_vips
|
||||
|
||||
if "raw" in self.args.th_dec:
|
||||
self.thumbable |= self.fmt_raw
|
||||
|
||||
if "ff" in self.args.th_dec:
|
||||
for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]:
|
||||
self.thumbable |= zss
|
||||
|
@ -313,6 +342,7 @@ class ThumbSrv(object):
|
|||
"thumbable": self.thumbable,
|
||||
"pil": self.fmt_pil,
|
||||
"vips": self.fmt_vips,
|
||||
"raw": self.fmt_raw,
|
||||
"ffi": self.fmt_ffi,
|
||||
"ffv": self.fmt_ffv,
|
||||
"ffa": self.fmt_ffa,
|
||||
|
@ -368,6 +398,8 @@ class ThumbSrv(object):
|
|||
funs.append(self.conv_pil)
|
||||
elif lib == "vips" and ext in self.fmt_vips:
|
||||
funs.append(self.conv_vips)
|
||||
elif lib == "raw" and ext in self.fmt_raw:
|
||||
funs.append(self.conv_raw)
|
||||
elif can_au and (want_png or want_au):
|
||||
if want_opus:
|
||||
funs.append(self.conv_opus)
|
||||
|
@ -480,35 +512,38 @@ class ThumbSrv(object):
|
|||
|
||||
return im
|
||||
|
||||
def conv_image_pil(self, im: "Image.Image", tpath: str, fmt: str, vn: VFS) -> None:
|
||||
try:
|
||||
im = self.fancy_pillow(im, fmt, vn)
|
||||
except Exception as ex:
|
||||
self.log("fancy_pillow {}".format(ex), "90")
|
||||
im.thumbnail(self.getres(vn, fmt))
|
||||
|
||||
fmts = ["RGB", "L"]
|
||||
args = {"quality": 40}
|
||||
|
||||
if tpath.endswith(".webp"):
|
||||
# quality 80 = pillow-default
|
||||
# quality 75 = ffmpeg-default
|
||||
# method 0 = pillow-default, fast
|
||||
# method 4 = ffmpeg-default
|
||||
# method 6 = max, slow
|
||||
fmts.extend(("RGBA", "LA"))
|
||||
args["method"] = 6
|
||||
else:
|
||||
# default q = 75
|
||||
args["progressive"] = True
|
||||
|
||||
if im.mode not in fmts:
|
||||
# print("conv {}".format(im.mode))
|
||||
im = im.convert("RGB")
|
||||
|
||||
im.save(tpath, **args)
|
||||
|
||||
def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
self.wait4ram(0.2, tpath)
|
||||
with Image.open(fsenc(abspath)) as im:
|
||||
try:
|
||||
im = self.fancy_pillow(im, fmt, vn)
|
||||
except Exception as ex:
|
||||
self.log("fancy_pillow {}".format(ex), "90")
|
||||
im.thumbnail(self.getres(vn, fmt))
|
||||
|
||||
fmts = ["RGB", "L"]
|
||||
args = {"quality": 40}
|
||||
|
||||
if tpath.endswith(".webp"):
|
||||
# quality 80 = pillow-default
|
||||
# quality 75 = ffmpeg-default
|
||||
# method 0 = pillow-default, fast
|
||||
# method 4 = ffmpeg-default
|
||||
# method 6 = max, slow
|
||||
fmts.extend(("RGBA", "LA"))
|
||||
args["method"] = 6
|
||||
else:
|
||||
# default q = 75
|
||||
args["progressive"] = True
|
||||
|
||||
if im.mode not in fmts:
|
||||
# print("conv {}".format(im.mode))
|
||||
im = im.convert("RGB")
|
||||
|
||||
im.save(tpath, **args)
|
||||
self.conv_image_pil(im, tpath, fmt, vn)
|
||||
|
||||
def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
self.wait4ram(0.2, tpath)
|
||||
|
@ -531,9 +566,53 @@ class ThumbSrv(object):
|
|||
assert img # type: ignore # !rm
|
||||
img.write_to_file(tpath, Q=40)
|
||||
|
||||
def conv_raw(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
self.wait4ram(0.2, tpath)
|
||||
with rawpy.imread(abspath) as raw:
|
||||
thumb = raw.extract_thumb()
|
||||
if thumb.format == rawpy.ThumbFormat.JPEG and tpath.endswith(".jpg"):
|
||||
# if we have a jpg thumbnail and no webp output is available,
|
||||
# just write the jpg directly (it'll be the wrong size, but it's fast)
|
||||
with open(tpath, "wb") as f:
|
||||
f.write(thumb.data)
|
||||
if HAVE_VIPS:
|
||||
crops = ["centre", "none"]
|
||||
if "f" in fmt:
|
||||
crops = ["none"]
|
||||
w, h = self.getres(vn, fmt)
|
||||
kw = {"height": h, "size": "down", "intent": "relative"}
|
||||
|
||||
for c in crops:
|
||||
try:
|
||||
kw["crop"] = c
|
||||
if thumb.format == rawpy.ThumbFormat.BITMAP:
|
||||
img = pyvips.Image.new_from_array(
|
||||
thumb.data, interpretation="rgb"
|
||||
)
|
||||
img = img.thumbnail_image(w, **kw)
|
||||
else:
|
||||
img = pyvips.Image.thumbnail_buffer(thumb.data, w, **kw)
|
||||
break
|
||||
except:
|
||||
if c == crops[-1]:
|
||||
raise
|
||||
|
||||
assert img # type: ignore # !rm
|
||||
img.write_to_file(tpath, Q=40)
|
||||
elif HAVE_PIL:
|
||||
if thumb.format == rawpy.ThumbFormat.BITMAP:
|
||||
im = Image.fromarray(thumb.data, "RGB")
|
||||
else:
|
||||
im = Image.open(io.BytesIO(thumb.data))
|
||||
self.conv_image_pil(im, tpath, fmt, vn)
|
||||
else:
|
||||
raise Exception(
|
||||
"either pil or vips is needed to process embedded bitmap thumbnails in raw files"
|
||||
)
|
||||
|
||||
def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
self.wait4ram(0.2, tpath)
|
||||
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
if not ret:
|
||||
return
|
||||
|
||||
|
@ -544,6 +623,17 @@ class ThumbSrv(object):
|
|||
dur = ret[".dur"][1] if ".dur" in ret else 4
|
||||
seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")]
|
||||
|
||||
self._ffmpeg_im(abspath, tpath, fmt, vn, seek, b"0:v:0")
|
||||
|
||||
def _ffmpeg_im(
|
||||
self,
|
||||
abspath: str,
|
||||
tpath: str,
|
||||
fmt: str,
|
||||
vn: VFS,
|
||||
seek: list[bytes],
|
||||
imap: bytes,
|
||||
) -> None:
|
||||
scale = "scale={0}:{1}:force_original_aspect_ratio="
|
||||
if "f" in fmt:
|
||||
scale += "decrease,setsar=1:1"
|
||||
|
@ -562,7 +652,7 @@ class ThumbSrv(object):
|
|||
cmd += seek
|
||||
cmd += [
|
||||
b"-i", fsenc(abspath),
|
||||
b"-map", b"0:v:0",
|
||||
b"-map", imap,
|
||||
b"-vf", bscale,
|
||||
b"-frames:v", b"1",
|
||||
b"-metadata:s:v:0", b"rotate=0",
|
||||
|
@ -583,11 +673,11 @@ class ThumbSrv(object):
|
|||
]
|
||||
|
||||
cmd += [fsenc(tpath)]
|
||||
self._run_ff(cmd, vn)
|
||||
self._run_ff(cmd, vn, "convt")
|
||||
|
||||
def _run_ff(self, cmd: list[bytes], vn: VFS, oom: int = 400) -> None:
|
||||
def _run_ff(self, cmd: list[bytes], vn: VFS, kto: str, oom: int = 400) -> None:
|
||||
# self.log((b" ".join(cmd)).decode("utf-8"))
|
||||
ret, _, serr = runcmd(cmd, timeout=vn.flags["convt"], nice=True, oom=oom)
|
||||
ret, _, serr = runcmd(cmd, timeout=vn.flags[kto], nice=True, oom=oom)
|
||||
if not ret:
|
||||
return
|
||||
|
||||
|
@ -631,7 +721,7 @@ class ThumbSrv(object):
|
|||
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
|
||||
|
||||
def conv_waves(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
if "ac" not in ret:
|
||||
raise Exception("not audio")
|
||||
|
||||
|
@ -669,7 +759,7 @@ class ThumbSrv(object):
|
|||
# fmt: on
|
||||
|
||||
cmd += [fsenc(tpath)]
|
||||
self._run_ff(cmd, vn)
|
||||
self._run_ff(cmd, vn, "convt")
|
||||
|
||||
if "pngquant" in vn.flags:
|
||||
wtpath = tpath + ".png"
|
||||
|
@ -690,11 +780,31 @@ class ThumbSrv(object):
|
|||
else:
|
||||
atomic_move(self.log, wtpath, tpath, vn.flags)
|
||||
|
||||
def conv_emb_cv(
|
||||
self, abspath: str, tpath: str, fmt: str, vn: VFS, strm: dict[str, Any]
|
||||
) -> None:
|
||||
self.wait4ram(0.2, tpath)
|
||||
self._ffmpeg_im(
|
||||
abspath, tpath, fmt, vn, [], b"0:" + strm["index"].encode("ascii")
|
||||
)
|
||||
|
||||
def conv_spec(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
ret, raw, strms, ctnr = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
if "ac" not in ret:
|
||||
raise Exception("not audio")
|
||||
|
||||
want_spec = vn.flags.get("th_spec_p", 1)
|
||||
if want_spec < 2:
|
||||
for strm in strms:
|
||||
if (
|
||||
strm.get("codec_type") == "video"
|
||||
and strm.get("DISPOSITION:attached_pic") == "1"
|
||||
):
|
||||
return self.conv_emb_cv(abspath, tpath, fmt, vn, strm)
|
||||
|
||||
if not want_spec:
|
||||
raise Exception("spectrograms forbidden by volflag")
|
||||
|
||||
fext = abspath.split(".")[-1].lower()
|
||||
|
||||
# https://trac.ffmpeg.org/ticket/10797
|
||||
|
@ -730,7 +840,7 @@ class ThumbSrv(object):
|
|||
b"-y", fsenc(infile),
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn)
|
||||
self._run_ff(cmd, vn, "convt")
|
||||
|
||||
fc = "[0:a:0]aresample=48000{},showspectrumpic=s="
|
||||
if "3" in fmt:
|
||||
|
@ -772,7 +882,7 @@ class ThumbSrv(object):
|
|||
]
|
||||
|
||||
cmd += [fsenc(tpath)]
|
||||
self._run_ff(cmd, vn)
|
||||
self._run_ff(cmd, vn, "convt")
|
||||
|
||||
def conv_mp3(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
quality = self.args.q_mp3.lower()
|
||||
|
@ -780,7 +890,7 @@ class ThumbSrv(object):
|
|||
raise Exception("disabled in server config")
|
||||
|
||||
self.wait4ram(0.2, tpath)
|
||||
tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
tags, rawtags, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
if "ac" not in tags:
|
||||
raise Exception("not audio")
|
||||
|
||||
|
@ -811,14 +921,14 @@ class ThumbSrv(object):
|
|||
fsenc(tpath)
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, oom=300)
|
||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
||||
|
||||
def conv_flac(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
if self.args.no_acode or not self.args.allow_flac:
|
||||
raise Exception("flac not permitted in server config")
|
||||
|
||||
self.wait4ram(0.2, tpath)
|
||||
tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
tags, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
if "ac" not in tags:
|
||||
raise Exception("not audio")
|
||||
|
||||
|
@ -836,14 +946,14 @@ class ThumbSrv(object):
|
|||
fsenc(tpath)
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, oom=300)
|
||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
||||
|
||||
def conv_wav(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
if self.args.no_acode or not self.args.allow_wav:
|
||||
raise Exception("wav not permitted in server config")
|
||||
|
||||
self.wait4ram(0.2, tpath)
|
||||
tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
tags, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
if "ac" not in tags:
|
||||
raise Exception("not audio")
|
||||
|
||||
|
@ -871,14 +981,14 @@ class ThumbSrv(object):
|
|||
fsenc(tpath)
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, oom=300)
|
||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
||||
|
||||
def conv_opus(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
if self.args.no_acode or not self.args.q_opus:
|
||||
raise Exception("disabled in server config")
|
||||
|
||||
self.wait4ram(0.2, tpath)
|
||||
tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
tags, rawtags, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
if "ac" not in tags:
|
||||
raise Exception("not audio")
|
||||
|
||||
|
@ -927,7 +1037,7 @@ class ThumbSrv(object):
|
|||
fsenc(tpath)
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, oom=300)
|
||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
||||
|
||||
def _conv_caf(
|
||||
self,
|
||||
|
@ -967,7 +1077,7 @@ class ThumbSrv(object):
|
|||
fsenc(tmp_opus)
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, oom=300)
|
||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
||||
|
||||
# iOS fails to play some "insufficiently complex" files
|
||||
# (average file shorter than 8 seconds), so of course we
|
||||
|
@ -994,7 +1104,7 @@ class ThumbSrv(object):
|
|||
fsenc(tpath)
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, oom=300)
|
||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
||||
|
||||
else:
|
||||
# simple remux should be safe
|
||||
|
@ -1013,7 +1123,7 @@ class ThumbSrv(object):
|
|||
fsenc(tpath)
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, oom=300)
|
||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
||||
|
||||
try:
|
||||
wunlink(self.log, tmp_opus, vn.flags)
|
||||
|
|
|
@ -391,7 +391,7 @@ class U2idx(object):
|
|||
fk_alg = 2 if "fka" in flags else 1
|
||||
c = cur.execute(uq, tuple(vuv))
|
||||
for hit in c:
|
||||
w, ts, sz, rd, fn, ip, at = hit[:7]
|
||||
w, ts, sz, rd, fn = hit[:5]
|
||||
|
||||
if rd.startswith("//") or fn.startswith("//"):
|
||||
rd, fn = s3dec(rd, fn)
|
||||
|
|
|
@ -60,6 +60,7 @@ from .util import (
|
|||
sfsenc,
|
||||
spack,
|
||||
statdir,
|
||||
trystat_shutil_copy2,
|
||||
ub64enc,
|
||||
unhumanize,
|
||||
vjoin,
|
||||
|
@ -77,7 +78,7 @@ except:
|
|||
if HAVE_SQLITE3:
|
||||
import sqlite3
|
||||
|
||||
DB_VER = 5
|
||||
DB_VER = 6
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Optional, Pattern, Union
|
||||
|
@ -91,6 +92,9 @@ ICV_EXTS = set(zsg.split(","))
|
|||
zsg = "3gp,asf,av1,avc,avi,flv,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,vob,webm,wmv"
|
||||
VCV_EXTS = set(zsg.split(","))
|
||||
|
||||
zsg = "aif,aiff,alac,ape,flac,m4a,mp3,oga,ogg,opus,tak,tta,wav,wma,wv"
|
||||
ACV_EXTS = set(zsg.split(","))
|
||||
|
||||
zsg = "nohash noidx xdev xvol"
|
||||
VF_AFFECTS_INDEXING = set(zsg.split(" "))
|
||||
|
||||
|
@ -144,6 +148,7 @@ class Up2k(object):
|
|||
|
||||
self.salt = self.args.warksalt
|
||||
self.r_hash = re.compile("^[0-9a-zA-Z_-]{44}$")
|
||||
self.abrt_key = ""
|
||||
|
||||
self.gid = 0
|
||||
self.gt0 = 0
|
||||
|
@ -410,10 +415,11 @@ class Up2k(object):
|
|||
|
||||
ret: list[tuple[int, str, int, int, int]] = []
|
||||
userset = set([(uname or "\n"), "*"])
|
||||
e_d = {}
|
||||
n = 1000
|
||||
try:
|
||||
for ptop, tab2 in self.registry.items():
|
||||
cfg = self.flags.get(ptop, {}).get("u2abort", 1)
|
||||
cfg = self.flags.get(ptop, e_d).get("u2abort", 1)
|
||||
if not cfg:
|
||||
continue
|
||||
addr = (ip or "\n") if cfg in (1, 2) else ""
|
||||
|
@ -902,7 +908,7 @@ class Up2k(object):
|
|||
self.iacct = self.asrv.iacct
|
||||
self.grps = self.asrv.grps
|
||||
|
||||
have_e2d = self.args.idp_h_usr or self.args.chpw or self.args.shr
|
||||
have_e2d = self.args.have_idp_hdrs or self.args.chpw or self.args.shr
|
||||
vols = list(all_vols.values())
|
||||
t0 = time.time()
|
||||
|
||||
|
@ -922,6 +928,12 @@ class Up2k(object):
|
|||
with self.mutex, self.reg_mutex:
|
||||
# only need to protect register_vpath but all in one go feels right
|
||||
for vol in vols:
|
||||
if bos.path.isfile(vol.realpath):
|
||||
self.volstate[vol.vpath] = "online (just-a-file)"
|
||||
t = "NOTE: volume [/%s] is a file, not a folder"
|
||||
self.log(t % (vol.vpath,))
|
||||
continue
|
||||
|
||||
try:
|
||||
# mkdir gonna happen at snap anyways;
|
||||
bos.makedirs(vol.realpath, vf=vol.flags)
|
||||
|
@ -1128,7 +1140,7 @@ class Up2k(object):
|
|||
ft = "\033[0;32m{}{:.0}"
|
||||
ff = "\033[0;35m{}{:.0}"
|
||||
fv = "\033[0;36m{}:\033[90m{}"
|
||||
zs = "ext_th_d html_head put_name2 mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
|
||||
zs = "du_iwho ext_th_d html_head put_name2 mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
|
||||
fx = set(zs.split())
|
||||
fd = vf_bmap()
|
||||
fd.update(vf_cmap())
|
||||
|
@ -1482,7 +1494,8 @@ class Up2k(object):
|
|||
unreg: list[str] = []
|
||||
files: list[tuple[int, int, str]] = []
|
||||
fat32 = True
|
||||
cv = vcv = ""
|
||||
cv = vcv = acv = ""
|
||||
e_d = {}
|
||||
|
||||
th_cvd = self.args.th_coversd
|
||||
th_cvds = self.args.th_coversd_set
|
||||
|
@ -1592,9 +1605,11 @@ class Up2k(object):
|
|||
cv = iname
|
||||
elif not vcv and ext in VCV_EXTS and not iname.startswith("."):
|
||||
vcv = iname
|
||||
elif not acv and ext in ACV_EXTS and not iname.startswith("."):
|
||||
acv = iname
|
||||
|
||||
if not cv:
|
||||
cv = vcv
|
||||
cv = vcv or acv
|
||||
|
||||
if not self.args.no_dirsz:
|
||||
tnf += len(files)
|
||||
|
@ -1654,7 +1669,7 @@ class Up2k(object):
|
|||
abspath = cdirs + fn
|
||||
nohash = reh.search(abspath) if reh else False
|
||||
|
||||
sql = "select w, mt, sz, ip, at from up where rd = ? and fn = ?"
|
||||
sql = "select w, mt, sz, ip, at, un from up where rd = ? and fn = ?"
|
||||
try:
|
||||
c = db.c.execute(sql, (rd, fn))
|
||||
except:
|
||||
|
@ -1663,7 +1678,7 @@ class Up2k(object):
|
|||
in_db = list(c.fetchall())
|
||||
if in_db:
|
||||
self.pp.n -= 1
|
||||
dw, dts, dsz, ip, at = in_db[0]
|
||||
dw, dts, dsz, ip, at, un = in_db[0]
|
||||
if len(in_db) > 1:
|
||||
t = "WARN: multiple entries: %r => %r |%d|\n%r"
|
||||
rep_db = "\n".join([repr(x) for x in in_db])
|
||||
|
@ -1676,9 +1691,12 @@ class Up2k(object):
|
|||
if dts == lmod and dsz == sz and (nohash or dw[0] != "#" or not sz):
|
||||
continue
|
||||
|
||||
if un is None:
|
||||
un = ""
|
||||
|
||||
t = "reindex %r => %r mtime(%s/%s) size(%s/%s)"
|
||||
self.log(t % (top, rp, dts, lmod, dsz, sz))
|
||||
self.db_rm(db.c, rd, fn, 0)
|
||||
self.db_rm(db.c, e_d, rd, fn, 0)
|
||||
tfa += 1
|
||||
db.n += 1
|
||||
in_db = []
|
||||
|
@ -1686,6 +1704,7 @@ class Up2k(object):
|
|||
dw = ""
|
||||
ip = ""
|
||||
at = 0
|
||||
un = ""
|
||||
|
||||
self.pp.msg = "a%d %s" % (self.pp.n, abspath)
|
||||
|
||||
|
@ -1711,9 +1730,10 @@ class Up2k(object):
|
|||
if dw and dw != wark:
|
||||
ip = ""
|
||||
at = 0
|
||||
un = ""
|
||||
|
||||
# skip upload hooks by not providing vflags
|
||||
self.db_add(db.c, {}, rd, fn, lmod, sz, "", "", wark, wark, "", "", ip, at)
|
||||
self.db_add(db.c, e_d, rd, fn, lmod, sz, "", "", wark, wark, "", un, ip, at)
|
||||
db.n += 1
|
||||
db.nf += 1
|
||||
tfa += 1
|
||||
|
@ -1773,7 +1793,7 @@ class Up2k(object):
|
|||
rm_files = [x for x in hits if x not in seen_files]
|
||||
n_rm = len(rm_files)
|
||||
for fn in rm_files:
|
||||
self.db_rm(db.c, rd, fn, 0)
|
||||
self.db_rm(db.c, e_d, rd, fn, 0)
|
||||
|
||||
if n_rm:
|
||||
self.log("forgot {} deleted files".format(n_rm))
|
||||
|
@ -2150,8 +2170,8 @@ class Up2k(object):
|
|||
|
||||
with self.mutex:
|
||||
try:
|
||||
q = "select rd, fn, ip, at from up where substr(w,1,16)=? and +w=?"
|
||||
rd, fn, ip, at = cur.execute(q, (w16, w)).fetchone()
|
||||
q = "select rd, fn, ip, at, un from up where substr(w,1,16)=? and +w=?"
|
||||
rd, fn, ip, at, un = cur.execute(q, (w16, w)).fetchone()
|
||||
except:
|
||||
# file modified/deleted since spooling
|
||||
continue
|
||||
|
@ -2170,12 +2190,15 @@ class Up2k(object):
|
|||
abspath = djoin(ptop, rd, fn)
|
||||
self.pp.msg = "c%d %s" % (nq, abspath)
|
||||
if not mpool:
|
||||
n_tags = self._tagscan_file(cur, entags, w, abspath, ip, at)
|
||||
n_tags = self._tagscan_file(cur, entags, w, abspath, ip, at, un)
|
||||
else:
|
||||
oth_tags = {}
|
||||
if ip:
|
||||
oth_tags = {"up_ip": ip, "up_at": at}
|
||||
else:
|
||||
oth_tags = {}
|
||||
oth_tags["up_ip"] = ip
|
||||
if at:
|
||||
oth_tags["up_at"] = at
|
||||
if un:
|
||||
oth_tags["up_by"] = un
|
||||
|
||||
mpool.put(Mpqe({}, entags, w, abspath, oth_tags))
|
||||
with self.mutex:
|
||||
|
@ -2331,8 +2354,8 @@ class Up2k(object):
|
|||
if w in in_progress:
|
||||
continue
|
||||
|
||||
q = "select rd, fn, ip, at from up where substr(w,1,16)=? limit 1"
|
||||
rd, fn, ip, at = cur.execute(q, (w,)).fetchone()
|
||||
q = "select rd, fn, ip, at, un from up where substr(w,1,16)=? limit 1"
|
||||
rd, fn, ip, at, un = cur.execute(q, (w,)).fetchone()
|
||||
rd, fn = s3dec(rd, fn)
|
||||
abspath = djoin(ptop, rd, fn)
|
||||
|
||||
|
@ -2356,7 +2379,10 @@ class Up2k(object):
|
|||
|
||||
if ip:
|
||||
oth_tags["up_ip"] = ip
|
||||
if at:
|
||||
oth_tags["up_at"] = at
|
||||
if un:
|
||||
oth_tags["up_by"] = un
|
||||
|
||||
jobs.append(Mpqe(parsers, set(), w, abspath, oth_tags))
|
||||
in_progress[w] = True
|
||||
|
@ -2545,6 +2571,7 @@ class Up2k(object):
|
|||
abspath: str,
|
||||
ip: str,
|
||||
at: float,
|
||||
un: Optional[str],
|
||||
) -> int:
|
||||
"""will mutex(main)"""
|
||||
assert self.mtag # !rm
|
||||
|
@ -2565,7 +2592,10 @@ class Up2k(object):
|
|||
|
||||
if ip:
|
||||
tags["up_ip"] = ip
|
||||
if at:
|
||||
tags["up_at"] = at
|
||||
if un:
|
||||
tags["up_by"] = un
|
||||
|
||||
with self.mutex:
|
||||
return self._tag_file(write_cur, entags, wark, abspath, tags)
|
||||
|
@ -2669,16 +2699,19 @@ class Up2k(object):
|
|||
if not existed and ver is None:
|
||||
return self._try_create_db(db_path, cur)
|
||||
|
||||
if ver == 4:
|
||||
for upver in (4, 5):
|
||||
if ver != upver:
|
||||
continue
|
||||
try:
|
||||
t = "creating backup before upgrade: "
|
||||
cur = self._backup_db(db_path, cur, ver, t)
|
||||
self._upgrade_v4(cur)
|
||||
ver = 5
|
||||
getattr(self, "_upgrade_v%d" % (upver,))(cur)
|
||||
ver += 1 # type: ignore
|
||||
except:
|
||||
self.log("WARN: failed to upgrade from v4", 3)
|
||||
self.log("WARN: failed to upgrade from v%d" % (ver,), 3)
|
||||
|
||||
if ver == DB_VER:
|
||||
# these no longer serve their intended purpose but they're great as additional sanchks
|
||||
self._add_dhash_tab(cur)
|
||||
self._add_xiu_tab(cur)
|
||||
self._add_cv_tab(cur)
|
||||
|
@ -2736,7 +2769,7 @@ class Up2k(object):
|
|||
cur.close()
|
||||
db.close()
|
||||
|
||||
shutil.copy2(fsenc(db_path), fsenc(bak))
|
||||
trystat_shutil_copy2(self.log, fsenc(db_path), fsenc(bak))
|
||||
return self._orz(db_path)
|
||||
|
||||
def _read_ver(self, cur: "sqlite3.Cursor") -> Optional[int]:
|
||||
|
@ -2780,7 +2813,7 @@ class Up2k(object):
|
|||
idx = r"create index up_w on up(w)"
|
||||
|
||||
for cmd in [
|
||||
r"create table up (w text, mt int, sz int, rd text, fn text, ip text, at int)",
|
||||
r"create table up (w text, mt int, sz int, rd text, fn text, ip text, at int, un text)",
|
||||
r"create index up_vp on up(rd, fn)",
|
||||
r"create index up_fn on up(fn)",
|
||||
r"create index up_ip on up(ip)",
|
||||
|
@ -2813,6 +2846,15 @@ class Up2k(object):
|
|||
|
||||
cur.connection.commit()
|
||||
|
||||
def _upgrade_v5(self, cur: "sqlite3.Cursor") -> None:
|
||||
for cmd in [
|
||||
r"alter table up add column un text",
|
||||
r"update kv set v=6 where k='sver'",
|
||||
]:
|
||||
cur.execute(cmd)
|
||||
|
||||
cur.connection.commit()
|
||||
|
||||
def _add_dhash_tab(self, cur: "sqlite3.Cursor") -> None:
|
||||
# v5 -> v5a
|
||||
try:
|
||||
|
@ -3010,7 +3052,7 @@ class Up2k(object):
|
|||
argv = [dwark[:16], dwark]
|
||||
|
||||
c2 = cur.execute(q, tuple(argv))
|
||||
for _, dtime, dsize, dp_dir, dp_fn, ip, at in c2:
|
||||
for _, dtime, dsize, dp_dir, dp_fn, ip, at, _ in c2:
|
||||
if dp_dir.startswith("//") or dp_fn.startswith("//"):
|
||||
dp_dir, dp_fn = s3dec(dp_dir, dp_fn)
|
||||
|
||||
|
@ -3102,7 +3144,7 @@ class Up2k(object):
|
|||
for cur, dp_dir, dp_fn in lost:
|
||||
t = "forgetting desynced db entry: %r"
|
||||
self.log(t % ("/" + vjoin(vjoin(vfs.vpath, dp_dir), dp_fn)))
|
||||
self.db_rm(cur, dp_dir, dp_fn, cj["size"])
|
||||
self.db_rm(cur, vfs.flags, dp_dir, dp_fn, cj["size"])
|
||||
if c2 and c2 != cur:
|
||||
c2.connection.commit()
|
||||
|
||||
|
@ -3396,10 +3438,9 @@ class Up2k(object):
|
|||
cur.connection.commit()
|
||||
|
||||
ap = djoin(job["ptop"], job["prel"], job["name"])
|
||||
times = (int(time.time()), int(cj["lmod"]))
|
||||
bos.utime(ap, times, False)
|
||||
mt = bos.utime_c(self.log, ap, int(cj["lmod"]), False, True)
|
||||
|
||||
self.log("touched %r from %d to %d" % (ap, job["lmod"], cj["lmod"]))
|
||||
self.log("touched %r from %d to %d" % (ap, job["lmod"], mt))
|
||||
except Exception as ex:
|
||||
self.log("umod failed, %r" % (ex,), 3)
|
||||
|
||||
|
@ -3432,8 +3473,8 @@ class Up2k(object):
|
|||
try:
|
||||
vrel = vjoin(job["prel"], fname)
|
||||
xlink = bool(vf.get("xlink"))
|
||||
cur, wark, _, _, _, _ = self._find_from_vpath(ptop, vrel)
|
||||
self._forget_file(ptop, vrel, cur, wark, True, st.st_size, xlink)
|
||||
cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, vrel)
|
||||
self._forget_file(ptop, vrel, vf, cur, wark, True, st.st_size, xlink)
|
||||
except Exception as ex:
|
||||
self.log("skipping replace-relink: %r" % (ex,))
|
||||
finally:
|
||||
|
@ -3551,11 +3592,10 @@ class Up2k(object):
|
|||
t = "BUG: no valid sources to link from! orig(%r) fsrc(%r) link(%r)"
|
||||
self.log(t, 1)
|
||||
raise Exception(t % (src, fsrc, dst))
|
||||
shutil.copy2(fsenc(csrc), fsenc(dst))
|
||||
trystat_shutil_copy2(self.log, fsenc(csrc), fsenc(dst))
|
||||
|
||||
if lmod and (not linked or SYMTIME):
|
||||
times = (int(time.time()), int(lmod))
|
||||
bos.utime(dst, times, False)
|
||||
bos.utime_c(self.log, dst, int(lmod), False)
|
||||
|
||||
def handle_chunks(
|
||||
self, ptop: str, wark: str, chashes: list[str]
|
||||
|
@ -3728,10 +3768,8 @@ class Up2k(object):
|
|||
times = (int(time.time()), int(job["lmod"]))
|
||||
t = "no more chunks, setting times %s (%d) on %r"
|
||||
self.log(t % (times, bos.path.getsize(dst), dst))
|
||||
try:
|
||||
bos.utime(dst, times)
|
||||
except:
|
||||
self.log("failed to utime (%r, %s)" % (dst, times))
|
||||
bos.utime_c(self.log, dst, times[1], False)
|
||||
# the above logmsg (and associated logic) is retained due to unforget.py
|
||||
|
||||
zs = "prel name lmod size ptop vtop wark dwrk host user addr"
|
||||
z2 = [job[x] for x in zs.split()]
|
||||
|
@ -3850,7 +3888,9 @@ class Up2k(object):
|
|||
|
||||
return True
|
||||
|
||||
def db_rm(self, db: "sqlite3.Cursor", rd: str, fn: str, sz: int) -> None:
|
||||
def db_rm(
|
||||
self, db: "sqlite3.Cursor", vflags: dict[str, Any], rd: str, fn: str, sz: int
|
||||
) -> None:
|
||||
sql = "delete from up where rd = ? and fn = ?"
|
||||
try:
|
||||
r = db.execute(sql, (rd, fn))
|
||||
|
@ -3858,9 +3898,22 @@ class Up2k(object):
|
|||
assert self.mem_cur # !rm
|
||||
r = db.execute(sql, s3enc(self.mem_cur, rd, fn))
|
||||
|
||||
if r.rowcount:
|
||||
self.volsize[db] -= sz
|
||||
self.volnfiles[db] -= 1
|
||||
if not r.rowcount:
|
||||
return
|
||||
|
||||
self.volsize[db] -= sz
|
||||
self.volnfiles[db] -= 1
|
||||
|
||||
if "nodirsz" not in vflags:
|
||||
try:
|
||||
q = "update ds set nf=nf-1, sz=sz-? where rd=?"
|
||||
while True:
|
||||
db.execute(q, (sz, rd))
|
||||
if not rd:
|
||||
break
|
||||
rd = rd.rsplit("/", 1)[0] if "/" in rd else ""
|
||||
except:
|
||||
pass
|
||||
|
||||
def db_add(
|
||||
self,
|
||||
|
@ -3881,7 +3934,7 @@ class Up2k(object):
|
|||
skip_xau: bool = False,
|
||||
) -> None:
|
||||
"""mutex(main) me"""
|
||||
self.db_rm(db, rd, fn, sz)
|
||||
self.db_rm(db, vflags, rd, fn, sz)
|
||||
|
||||
if not ip:
|
||||
db_ip = ""
|
||||
|
@ -3889,14 +3942,14 @@ class Up2k(object):
|
|||
# plugins may expect this to look like an actual IP
|
||||
db_ip = "1.1.1.1" if "no_db_ip" in vflags else ip
|
||||
|
||||
sql = "insert into up values (?,?,?,?,?,?,?)"
|
||||
v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0))
|
||||
sql = "insert into up values (?,?,?,?,?,?,?,?)"
|
||||
v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0), usr)
|
||||
try:
|
||||
db.execute(sql, v)
|
||||
except:
|
||||
assert self.mem_cur # !rm
|
||||
rd, fn = s3enc(self.mem_cur, rd, fn)
|
||||
v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0))
|
||||
v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0), usr)
|
||||
db.execute(sql, v)
|
||||
|
||||
self.volsize[db] += sz
|
||||
|
@ -3988,6 +4041,9 @@ class Up2k(object):
|
|||
except:
|
||||
pass
|
||||
|
||||
def handle_fs_abrt(self, akey: str) -> None:
|
||||
self.abrt_key = akey
|
||||
|
||||
def handle_rm(
|
||||
self,
|
||||
uname: str,
|
||||
|
@ -4034,7 +4090,7 @@ class Up2k(object):
|
|||
vn, rem = vn0.get_dbv(rem0)
|
||||
ptop = vn.realpath
|
||||
with self.mutex, self.reg_mutex:
|
||||
abrt_cfg = self.flags.get(ptop, {}).get("u2abort", 1)
|
||||
abrt_cfg = vn.flags.get("u2abort", 1)
|
||||
addr = (ip or "\n") if abrt_cfg in (1, 2) else ""
|
||||
user = ((uname or "\n"), "*") if abrt_cfg in (1, 3) else None
|
||||
reg = self.registry.get(ptop, {}) if abrt_cfg else {}
|
||||
|
@ -4055,17 +4111,22 @@ class Up2k(object):
|
|||
if partial:
|
||||
dip = ip
|
||||
dat = time.time()
|
||||
dun = uname
|
||||
un_cfg = 1
|
||||
else:
|
||||
if not self.args.unpost:
|
||||
un_cfg = vn.flags["unp_who"]
|
||||
if not self.args.unpost or not un_cfg:
|
||||
t = "the unpost feature is disabled in server config"
|
||||
raise Pebkac(400, t)
|
||||
|
||||
_, _, _, _, dip, dat = self._find_from_vpath(ptop, rem)
|
||||
_, _, _, _, dip, dat, dun = self._find_from_vpath(ptop, rem)
|
||||
|
||||
t = "you cannot delete this: "
|
||||
if not dip:
|
||||
t += "file not found"
|
||||
elif dip != ip:
|
||||
elif dip != ip and un_cfg in (1, 2):
|
||||
t += "not uploaded by (You)"
|
||||
elif dun != uname and un_cfg in (1, 3):
|
||||
t += "not uploaded by (You)"
|
||||
elif dat < time.time() - self.args.unpost:
|
||||
t += "uploaded too long ago"
|
||||
|
@ -4154,9 +4215,9 @@ class Up2k(object):
|
|||
try:
|
||||
ptop = dbv.realpath
|
||||
xlink = bool(dbv.flags.get("xlink"))
|
||||
cur, wark, _, _, _, _ = self._find_from_vpath(ptop, volpath)
|
||||
cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, volpath)
|
||||
self._forget_file(
|
||||
ptop, volpath, cur, wark, True, st.st_size, xlink
|
||||
ptop, volpath, dbv.flags, cur, wark, True, st.st_size, xlink
|
||||
)
|
||||
finally:
|
||||
if cur:
|
||||
|
@ -4197,7 +4258,7 @@ class Up2k(object):
|
|||
|
||||
return n_files, ok + ok2, ng + ng2
|
||||
|
||||
def handle_cp(self, uname: str, ip: str, svp: str, dvp: str) -> str:
|
||||
def handle_cp(self, abrt: str, uname: str, ip: str, svp: str, dvp: str) -> str:
|
||||
if svp == dvp or dvp.startswith(svp + "/"):
|
||||
raise Pebkac(400, "cp: cannot copy parent into subfolder")
|
||||
|
||||
|
@ -4244,6 +4305,8 @@ class Up2k(object):
|
|||
|
||||
dvpf = dvp + svpf[len(svp) :]
|
||||
self._cp_file(uname, ip, svpf, dvpf, curs)
|
||||
if abrt and abrt == self.abrt_key:
|
||||
raise Pebkac(400, "filecopy aborted by http-api")
|
||||
|
||||
for v in curs:
|
||||
v.connection.commit()
|
||||
|
@ -4313,7 +4376,7 @@ class Up2k(object):
|
|||
|
||||
bos.makedirs(os.path.dirname(dabs), vf=dvn.flags)
|
||||
|
||||
c1, w, ftime_, fsize_, ip, at = self._find_from_vpath(
|
||||
c1, w, ftime_, fsize_, ip, at, un = self._find_from_vpath(
|
||||
svn_dbv.realpath, srem_dbv
|
||||
)
|
||||
c2 = self.cur.get(dvn.realpath)
|
||||
|
@ -4338,7 +4401,7 @@ class Up2k(object):
|
|||
w,
|
||||
w,
|
||||
"",
|
||||
"",
|
||||
un or "",
|
||||
ip or "",
|
||||
at or 0,
|
||||
)
|
||||
|
@ -4365,7 +4428,7 @@ class Up2k(object):
|
|||
b1, b2 = fsenc(sabs), fsenc(dabs)
|
||||
is_link = os.path.islink(b1) # due to _relink
|
||||
try:
|
||||
shutil.copy2(b1, b2)
|
||||
trystat_shutil_copy2(self.log, b1, b2)
|
||||
except:
|
||||
try:
|
||||
wunlink(self.log, dabs, dvn.flags)
|
||||
|
@ -4411,7 +4474,7 @@ class Up2k(object):
|
|||
|
||||
return "k"
|
||||
|
||||
def handle_mv(self, uname: str, ip: str, svp: str, dvp: str) -> str:
|
||||
def handle_mv(self, abrt: str, uname: str, ip: str, svp: str, dvp: str) -> str:
|
||||
if svp == dvp or dvp.startswith(svp + "/"):
|
||||
raise Pebkac(400, "mv: cannot move parent into subfolder")
|
||||
|
||||
|
@ -4466,6 +4529,8 @@ class Up2k(object):
|
|||
|
||||
dvpf = dvp + svpf[len(svp) :]
|
||||
self._mv_file(uname, ip, svpf, dvpf, curs)
|
||||
if abrt and abrt == self.abrt_key:
|
||||
raise Pebkac(400, "filemove aborted by http-api")
|
||||
|
||||
for v in curs:
|
||||
v.connection.commit()
|
||||
|
@ -4597,7 +4662,7 @@ class Up2k(object):
|
|||
|
||||
return "k"
|
||||
|
||||
c1, w, ftime_, fsize_, ip, at = self._find_from_vpath(svn.realpath, srem)
|
||||
c1, w, ftime_, fsize_, ip, at, un = self._find_from_vpath(svn.realpath, srem)
|
||||
c2 = self.cur.get(dvn.realpath)
|
||||
|
||||
has_dupes = False
|
||||
|
@ -4610,7 +4675,14 @@ class Up2k(object):
|
|||
|
||||
with self.reg_mutex:
|
||||
has_dupes = self._forget_file(
|
||||
svn.realpath, srem, c1, w, is_xvol, fsize_ or fsize, xlink
|
||||
svn.realpath,
|
||||
srem,
|
||||
svn.flags,
|
||||
c1,
|
||||
w,
|
||||
is_xvol,
|
||||
fsize_ or fsize,
|
||||
xlink,
|
||||
)
|
||||
|
||||
if not is_xvol:
|
||||
|
@ -4631,7 +4703,7 @@ class Up2k(object):
|
|||
w,
|
||||
w,
|
||||
"",
|
||||
"",
|
||||
un or "",
|
||||
ip or "",
|
||||
at or 0,
|
||||
)
|
||||
|
@ -4662,7 +4734,7 @@ class Up2k(object):
|
|||
b1, b2 = fsenc(sabs), fsenc(dabs)
|
||||
is_link = os.path.islink(b1) # due to _relink
|
||||
try:
|
||||
shutil.copy2(b1, b2)
|
||||
trystat_shutil_copy2(self.log, b1, b2)
|
||||
except:
|
||||
try:
|
||||
wunlink(self.log, dabs, dvn.flags)
|
||||
|
@ -4731,13 +4803,14 @@ class Up2k(object):
|
|||
Optional[int],
|
||||
str,
|
||||
Optional[int],
|
||||
str,
|
||||
]:
|
||||
cur = self.cur.get(ptop)
|
||||
if not cur:
|
||||
return None, None, None, None, "", None
|
||||
return None, None, None, None, "", None, ""
|
||||
|
||||
rd, fn = vsplit(vrem)
|
||||
q = "select w, mt, sz, ip, at from up where rd=? and fn=? limit 1"
|
||||
q = "select w, mt, sz, ip, at, un from up where rd=? and fn=? limit 1"
|
||||
try:
|
||||
c = cur.execute(q, (rd, fn))
|
||||
except:
|
||||
|
@ -4746,14 +4819,15 @@ class Up2k(object):
|
|||
|
||||
hit = c.fetchone()
|
||||
if hit:
|
||||
wark, ftime, fsize, ip, at = hit
|
||||
return cur, wark, ftime, fsize, ip, at
|
||||
return cur, None, None, None, "", None
|
||||
wark, ftime, fsize, ip, at, un = hit
|
||||
return cur, wark, ftime, fsize, ip, at, un
|
||||
return cur, None, None, None, "", None, ""
|
||||
|
||||
def _forget_file(
|
||||
self,
|
||||
ptop: str,
|
||||
vrem: str,
|
||||
vflags: dict[str, Any],
|
||||
cur: Optional["sqlite3.Cursor"],
|
||||
wark: Optional[str],
|
||||
drop_tags: bool,
|
||||
|
@ -4778,7 +4852,7 @@ class Up2k(object):
|
|||
q = "delete from mt where w=?"
|
||||
cur.execute(q, (wark[:16],))
|
||||
|
||||
self.db_rm(cur, srd, sfn, sz)
|
||||
self.db_rm(cur, vflags, srd, sfn, sz)
|
||||
|
||||
reg = self.registry.get(ptop)
|
||||
if reg:
|
||||
|
@ -4867,7 +4941,10 @@ class Up2k(object):
|
|||
mt = bos.path.getmtime(slabs, False)
|
||||
flags = self.flags.get(ptop) or {}
|
||||
atomic_move(self.log, sabs, slabs, flags)
|
||||
bos.utime(slabs, (int(time.time()), int(mt)), False)
|
||||
try:
|
||||
bos.utime(slabs, (int(time.time()), int(mt)), False)
|
||||
except:
|
||||
self.log("relink: failed to utime(%r, %s)" % (slabs, mt), 3)
|
||||
self._symlink(slabs, sabs, flags, False, is_mv=True)
|
||||
full[slabs] = (ptop, rem)
|
||||
sabs = slabs
|
||||
|
|
|
@ -52,6 +52,7 @@ from .__init__ import (
|
|||
VT100,
|
||||
WINDOWS,
|
||||
EnvParams,
|
||||
unicode,
|
||||
)
|
||||
from .__version__ import S_BUILD_DT, S_VERSION
|
||||
from .stolen import surrogateescape
|
||||
|
@ -112,7 +113,13 @@ E_ACCESS = _ens("EACCES WSAEACCES")
|
|||
E_UNREACH = _ens("EHOSTUNREACH WSAEHOSTUNREACH ENETUNREACH WSAENETUNREACH")
|
||||
|
||||
IP6ALL = "0:0:0:0:0:0:0:0"
|
||||
IP6_LL = ("fe8", "fe9", "fea", "feb")
|
||||
IP64_LL = ("fe8", "fe9", "fea", "feb", "169.254")
|
||||
|
||||
UC_CDISP = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._"
|
||||
BC_CDISP = UC_CDISP.encode("ascii")
|
||||
UC_CDISP_SET = set(UC_CDISP)
|
||||
BC_CDISP_SET = set(BC_CDISP)
|
||||
|
||||
try:
|
||||
import fcntl
|
||||
|
@ -399,6 +406,9 @@ application swf=x-shockwave-flash m3u=vnd.apple.mpegurl db3=vnd.sqlite3 sqlite=v
|
|||
text ass=plain ssa=plain
|
||||
image jpg=jpeg xpm=x-xpixmap psd=vnd.adobe.photoshop jpf=jpx tif=tiff ico=x-icon djvu=vnd.djvu
|
||||
image heic=heic-sequence heif=heif-sequence hdr=vnd.radiance svg=svg+xml
|
||||
image arw=x-sony-arw cr2=x-canon-cr2 crw=x-canon-crw dcr=x-kodak-dcr dng=x-adobe-dng erf=x-epson-erf
|
||||
image k25=x-kodak-k25 kdc=x-kodak-kdc mrw=x-minolta-mrw nef=x-nikon-nef orf=x-olympus-orf
|
||||
image pef=x-pentax-pef raf=x-fuji-raf raw=x-panasonic-raw sr2=x-sony-sr2 srf=x-sony-srf x3f=x-sigma-x3f
|
||||
audio caf=x-caf mp3=mpeg m4a=mp4 mid=midi mpc=musepack aif=aiff au=basic qcp=qcelp
|
||||
video mkv=x-matroska mov=quicktime avi=x-msvideo m4v=x-m4v ts=mp2t
|
||||
video asf=x-ms-asf flv=x-flv 3gp=3gpp 3g2=3gpp2 rmvb=vnd.rn-realmedia-vbr
|
||||
|
@ -2068,6 +2078,29 @@ def gencookie(
|
|||
)
|
||||
|
||||
|
||||
def gen_content_disposition(fn: str) -> str:
|
||||
safe = UC_CDISP_SET
|
||||
bsafe = BC_CDISP_SET
|
||||
fn = fn.replace("/", "_").replace("\\", "_")
|
||||
zb = fn.encode("utf-8", "xmlcharrefreplace")
|
||||
if not PY2:
|
||||
zbl = [
|
||||
chr(x).encode("utf-8")
|
||||
if x in bsafe
|
||||
else "%{:02X}".format(x).encode("ascii")
|
||||
for x in zb
|
||||
]
|
||||
else:
|
||||
zbl = [unicode(x) if x in bsafe else "%{:02X}".format(ord(x)) for x in zb]
|
||||
|
||||
ufn = b"".join(zbl).decode("ascii")
|
||||
afn = "".join([x if x in safe else "_" for x in fn]).lstrip(".")
|
||||
while ".." in afn:
|
||||
afn = afn.replace("..", ".")
|
||||
|
||||
return "attachment; filename=\"%s\"; filename*=UTF-8''%s" % (afn, ufn)
|
||||
|
||||
|
||||
def humansize(sz: float, terse: bool = False) -> str:
|
||||
for unit in HUMANSIZE_UNITS:
|
||||
if sz < 1024:
|
||||
|
@ -2606,6 +2639,24 @@ def set_fperms(f: Union[typing.BinaryIO, typing.IO[Any]], vf: dict[str, Any]) ->
|
|||
os.fchown(fno, vf["uid"], vf["gid"])
|
||||
|
||||
|
||||
def trystat_shutil_copy2(log: "NamedLogger", src: bytes, dst: bytes) -> bytes:
|
||||
try:
|
||||
return shutil.copy2(src, dst)
|
||||
except:
|
||||
# ignore failed mtime on linux+ntfs; for example:
|
||||
# shutil.py:437 <copy2>: copystat(src, dst, follow_symlinks=follow_symlinks)
|
||||
# shutil.py:376 <copystat>: lookup("utime")(dst, ns=(st.st_atime_ns, st.st_mtime_ns),
|
||||
# [PermissionError] [Errno 1] Operation not permitted, '/windows/_videos'
|
||||
_, _, tb = sys.exc_info()
|
||||
for _, _, fun, _ in traceback.extract_tb(tb):
|
||||
if fun == "copystat":
|
||||
if log:
|
||||
t = "warning: failed to retain some file attributes (timestamp and/or permissions) during copy from %r to %r:\n%s"
|
||||
log(t % (src, dst, min_ex()), 3)
|
||||
return dst # close enough
|
||||
raise
|
||||
|
||||
|
||||
def _fs_mvrm(
|
||||
log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any]
|
||||
) -> bool:
|
||||
|
@ -2951,6 +3002,27 @@ def load_ipu(
|
|||
return ip_u, nm
|
||||
|
||||
|
||||
def load_ipr(
|
||||
log: "RootLogger", iprs: list[str], defer_mutex: bool = False
|
||||
) -> dict[str, NetMap]:
|
||||
ret = {}
|
||||
for ipr in iprs:
|
||||
try:
|
||||
zs, uname = ipr.split("=")
|
||||
cidrs = zs.split(",")
|
||||
except:
|
||||
t = "\n invalid value %r for argument --ipr; must be CIDR[,CIDR[,...]]=UNAME (192.168.0.0/16=amelia)"
|
||||
raise Exception(t % (ipr,))
|
||||
try:
|
||||
nm = NetMap(["::"], cidrs, True, True, defer_mutex)
|
||||
except Exception as ex:
|
||||
t = "failed to translate --ipr into netmap, probably due to invalid config: %r"
|
||||
log("root", t % (ex,), 1)
|
||||
raise
|
||||
ret[uname] = nm
|
||||
return ret
|
||||
|
||||
|
||||
def yieldfile(fn: str, bufsz: int) -> Generator[bytes, None, None]:
|
||||
readsz = min(bufsz, 128 * 1024)
|
||||
with open(fsenc(fn), "rb", bufsz) as f:
|
||||
|
@ -3135,8 +3207,9 @@ def statdir(
|
|||
else:
|
||||
src = "listdir"
|
||||
fun: Any = os.lstat if lstat else os.stat
|
||||
btop_ = os.path.join(btop, b"")
|
||||
for name in os.listdir(btop):
|
||||
abspath = os.path.join(btop, name)
|
||||
abspath = btop_ + name
|
||||
try:
|
||||
yield (fsdec(name), fun(abspath))
|
||||
except Exception as ex:
|
||||
|
@ -3175,7 +3248,9 @@ def rmdirs(
|
|||
|
||||
stats = statdir(logger, scandir, lstat, top, False)
|
||||
dirs = [x[0] for x in stats if stat.S_ISDIR(x[1].st_mode)]
|
||||
dirs = [os.path.join(top, x) for x in dirs]
|
||||
if dirs:
|
||||
top_ = os.path.join(top, "")
|
||||
dirs = [top_ + x for x in dirs]
|
||||
ok = []
|
||||
ng = []
|
||||
for d in reversed(dirs):
|
||||
|
@ -3578,7 +3653,7 @@ def runihook(
|
|||
verbose: bool,
|
||||
cmd: str,
|
||||
vol: "VFS",
|
||||
ups: list[tuple[str, int, int, str, str, str, int]],
|
||||
ups: list[tuple[str, int, int, str, str, str, int, str]],
|
||||
) -> bool:
|
||||
_, chk, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd)
|
||||
bcmd = [sfsenc(x) for x in acmd]
|
||||
|
@ -4151,7 +4226,7 @@ def _pkg_resource_exists(pkg: str, name: str) -> bool:
|
|||
|
||||
|
||||
def stat_resource(E: EnvParams, name: str):
|
||||
path = os.path.join(E.mod, name)
|
||||
path = E.mod_ + name
|
||||
if os.path.exists(path):
|
||||
return os.stat(fsenc(path))
|
||||
return None
|
||||
|
@ -4198,7 +4273,7 @@ def _has_resource(name: str):
|
|||
|
||||
|
||||
def has_resource(E: EnvParams, name: str):
|
||||
return _has_resource(name) or os.path.exists(os.path.join(E.mod, name))
|
||||
return _has_resource(name) or os.path.exists(E.mod_ + name)
|
||||
|
||||
|
||||
def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]:
|
||||
|
@ -4223,7 +4298,7 @@ def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]:
|
|||
stream = codecs.getreader(enc)(stream)
|
||||
return stream
|
||||
|
||||
ap = os.path.join(E.mod, name)
|
||||
ap = E.mod_ + name
|
||||
|
||||
if PY2:
|
||||
return codecs.open(ap, "r", encoding=enc) # type: ignore
|
||||
|
|
|
@ -44,10 +44,12 @@ window.baguetteBox = (function () {
|
|||
loopA = null,
|
||||
loopB = null,
|
||||
url_ts = null,
|
||||
un_pp = 0,
|
||||
resume_mp = false;
|
||||
|
||||
var onFSC = function (e) {
|
||||
isFullscreen = !!document.fullscreenElement;
|
||||
clmod(document.documentElement, 'bb_fsc', isFullscreen);
|
||||
};
|
||||
|
||||
var overlayClickHandler = function (e) {
|
||||
|
@ -277,23 +279,30 @@ window.baguetteBox = (function () {
|
|||
if (modal.busy)
|
||||
return;
|
||||
|
||||
if (e.key == '?')
|
||||
return halp();
|
||||
|
||||
if (anymod(e, true))
|
||||
return;
|
||||
|
||||
var k = (e.code || e.key) + '', v = vid(), pos = -1;
|
||||
var k = (e.key || e.code) + '', v = vid();
|
||||
|
||||
if (k == "BracketLeft")
|
||||
if (k.startsWith('Key'))
|
||||
k = k.slice(3);
|
||||
else if (k.startsWith('Digit'))
|
||||
k = k.slice(5);
|
||||
|
||||
var kl = k.toLowerCase();
|
||||
|
||||
if (k == '?')
|
||||
return halp();
|
||||
|
||||
if (k == "[" || k == "BracketLeft")
|
||||
setloop(1);
|
||||
else if (k == "BracketRight")
|
||||
else if (k == "]" || k == "BracketRight")
|
||||
setloop(2);
|
||||
else if (e.shiftKey && k != "KeyR" && k != "R")
|
||||
else if (e.shiftKey && kl != "r")
|
||||
return;
|
||||
else if (k == "ArrowLeft" || k == "KeyJ" || k == "Left" || k == "j")
|
||||
else if (k == "ArrowLeft" || k == "Left" || kl == "j")
|
||||
showPreviousImage();
|
||||
else if (k == "ArrowRight" || k == "KeyL" || k == "Right" || k == "l")
|
||||
else if (k == "ArrowRight" || k == "Right" || kl == "l")
|
||||
showNextImage();
|
||||
else if (k == "Escape" || k == "Esc")
|
||||
hideOverlay();
|
||||
|
@ -301,34 +310,37 @@ window.baguetteBox = (function () {
|
|||
showFirstImage(e);
|
||||
else if (k == "End")
|
||||
showLastImage(e);
|
||||
else if (k == "Space" || k == "KeyP" || k == "KeyK")
|
||||
else if (k == "Space" || k == "Spacebar" || kl == " " || kl == "p" || kl == "k")
|
||||
playpause();
|
||||
else if (k == "KeyU" || k == "KeyO")
|
||||
relseek(k == "KeyU" ? -10 : 10);
|
||||
else if (k.indexOf('Digit') === 0 && v)
|
||||
v.currentTime = v.duration * parseInt(k.slice(-1)) * 0.1;
|
||||
else if (k == "KeyM" && v) {
|
||||
else if (kl == "u" || kl == "o")
|
||||
relseek(kl == "u" ? -10 : 10);
|
||||
else if (v && /^[0-9]$/.test(k))
|
||||
v.currentTime = v.duration * parseInt(k) * 0.1;
|
||||
else if (kl == "m" && v) {
|
||||
v.muted = vmute = !vmute;
|
||||
mp_ctl();
|
||||
}
|
||||
else if (k == "KeyV" && v) {
|
||||
else if (kl == "v" && v) {
|
||||
vloop = !vloop;
|
||||
vnext = vnext && !vloop;
|
||||
setVmode();
|
||||
}
|
||||
else if (k == "KeyC" && v) {
|
||||
else if (kl == "c" && v) {
|
||||
vnext = !vnext;
|
||||
vloop = vloop && !vnext;
|
||||
setVmode();
|
||||
}
|
||||
else if (k == "KeyF")
|
||||
else if (kl == "f")
|
||||
tglfull();
|
||||
else if (k == "KeyS" || k == "s")
|
||||
else if (kl == "s")
|
||||
tglsel();
|
||||
else if (k == "KeyR" || k == "r" || k == "R")
|
||||
else if (kl == "r")
|
||||
rotn(e.shiftKey ? -1 : 1);
|
||||
else if (k == "KeyY")
|
||||
else if (kl == "y")
|
||||
dlpic();
|
||||
else
|
||||
return;
|
||||
return ev(e);
|
||||
}
|
||||
|
||||
function anim() {
|
||||
|
@ -402,7 +414,7 @@ window.baguetteBox = (function () {
|
|||
if (isFullscreen)
|
||||
document.exitFullscreen();
|
||||
else
|
||||
(vid() || ebi('bbox-overlay')).requestFullscreen();
|
||||
ebi('bbox-overlay').requestFullscreen();
|
||||
}
|
||||
catch (ex) {
|
||||
if (IPHONE)
|
||||
|
@ -449,10 +461,12 @@ window.baguetteBox = (function () {
|
|||
if (anymod(e))
|
||||
return;
|
||||
|
||||
var k = e.code + '';
|
||||
var k = (e.key || e.code) + '';
|
||||
|
||||
if (k == "Space")
|
||||
ev(e);
|
||||
if (k == "Space" || k == "Spacebar" || k == " ") {
|
||||
un_pp = Date.now();
|
||||
return ev(e);
|
||||
}
|
||||
}
|
||||
|
||||
var passiveSupp = false;
|
||||
|
@ -777,8 +791,7 @@ window.baguetteBox = (function () {
|
|||
image.setAttribute('playsinline', '1');
|
||||
// ios ignores poster
|
||||
image.onended = vidEnd;
|
||||
image.onplay = function () { show_buttons(1); };
|
||||
image.onpause = function () { show_buttons(); };
|
||||
image.onplay = image.onpause = ppHandler;
|
||||
}
|
||||
image.alt = thumbnailElement ? thumbnailElement.alt || '' : '';
|
||||
if (options.titleTag && imageCaption)
|
||||
|
@ -793,6 +806,15 @@ window.baguetteBox = (function () {
|
|||
callback();
|
||||
}
|
||||
|
||||
function ppHandler() {
|
||||
var now = Date.now();
|
||||
if (now - un_pp < 50) {
|
||||
un_pp = 0;
|
||||
return playpause(); // browser undid space hotkey
|
||||
}
|
||||
show_buttons(this.paused ? 1 : 0);
|
||||
}
|
||||
|
||||
function showNextImage(e) {
|
||||
ev(e);
|
||||
return show(currentIndex + 1);
|
||||
|
|
|
@ -571,6 +571,7 @@ pre, code, tt, #doc, #doc>code {
|
|||
overflow: hidden;
|
||||
width: 0;
|
||||
height: 0;
|
||||
left: -10em;
|
||||
color: var(--bg);
|
||||
}
|
||||
html .ayjump:focus {
|
||||
|
@ -881,6 +882,9 @@ html.y #path a:hover {
|
|||
#flogout {
|
||||
display: inline;
|
||||
}
|
||||
html.dz #flogout {
|
||||
margin-left: 1em;
|
||||
}
|
||||
#goh+span {
|
||||
color: var(--bg-u5);
|
||||
padding-left: .5em;
|
||||
|
@ -1388,6 +1392,7 @@ html.y #ops svg circle {
|
|||
.opview select {
|
||||
padding: .3em;
|
||||
margin: .2em .4em;
|
||||
background: var(--bg-u3);
|
||||
}
|
||||
.opview input.err {
|
||||
color: var(--err-fg);
|
||||
|
@ -1553,11 +1558,13 @@ html {
|
|||
#treepar {
|
||||
z-index: 1;
|
||||
position: fixed;
|
||||
background: #fff;
|
||||
background: var(--tree-bg);
|
||||
left: -.96em;
|
||||
width: calc(.3em + var(--nav-sz) - var(--sbw));
|
||||
border-bottom: 1px solid var(--bg-u5);
|
||||
overflow: hidden;
|
||||
border-right: .5em solid #999\9;
|
||||
}
|
||||
#treepar.off {
|
||||
display: none;
|
||||
|
@ -1930,6 +1937,11 @@ html.y #tree.nowrap .ntree a+a:hover {
|
|||
padding: 0;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
#fs_abrt {
|
||||
margin-top: 1em;
|
||||
text-shadow: 0;
|
||||
box-shadow: 1px 1px 0 var(--bg-d3);
|
||||
}
|
||||
#doc {
|
||||
overflow: visible;
|
||||
background: #fff;
|
||||
|
@ -1988,6 +2000,7 @@ html.y #doc .line-highlight {
|
|||
}
|
||||
#seldoc.sel {
|
||||
color: var(--fg2-max);
|
||||
background: #f0f;
|
||||
background: var(--g-sel-b1);
|
||||
}
|
||||
#pvol,
|
||||
|
@ -2007,6 +2020,7 @@ a.btn,
|
|||
user-select: none;
|
||||
}
|
||||
#hkhelp {
|
||||
background: #fff;
|
||||
background: var(--bg);
|
||||
}
|
||||
#hkhelp table {
|
||||
|
@ -2120,6 +2134,13 @@ html.noscroll .sbar::-webkit-scrollbar {
|
|||
vertical-align: middle;
|
||||
transition: transform .23s, left .23s, top .23s, width .23s, height .23s;
|
||||
}
|
||||
html.bb_fsc .full-image img,
|
||||
html.bb_fsc .full-image video {
|
||||
max-height: 100%;
|
||||
}
|
||||
html.bb_fsc figcaption {
|
||||
display: none;
|
||||
}
|
||||
.full-image img.nt,
|
||||
.full-image video.nt {
|
||||
transition: none;
|
||||
|
@ -3272,3 +3293,793 @@ html.d #treepar {
|
|||
transition: background-color .3s ease, color .3s ease;
|
||||
}
|
||||
}
|
||||
html.ey {
|
||||
--negative-space: 0em; /* Use this to change the global spacing of the 95 theme */
|
||||
--font-main: consolas;
|
||||
--font-serif: consolas;
|
||||
--font-mono: consolas;
|
||||
--w: #fff;
|
||||
--w2: #dfdfdf;
|
||||
--w3: grey;
|
||||
--fg: #000;
|
||||
--fg-max: #0000ff;
|
||||
--fg-weak: #0000ff;
|
||||
--bg: #c6c3c6;
|
||||
--bg-d3: #ff0;
|
||||
--bg-d2: var(--w3);
|
||||
--bg-d1: var(--bg);
|
||||
--bg-u2: var(--bg);
|
||||
--bg-u3: var(--bg);
|
||||
--bg-u5: var(--shadow-color-2);
|
||||
--tab-alt: #00f;
|
||||
--g-fsel-bg: #00f;
|
||||
--g-sel-bg: #00f;
|
||||
--g-fsel-b1: #fff;
|
||||
--row-alt: var(--w);
|
||||
--scroll: var(--silver);
|
||||
--f-sel-sh: transparent;
|
||||
--a: #000;
|
||||
--a-b: #fff;
|
||||
--a-hil: #fff;
|
||||
--a-h-bg: var(--bg);
|
||||
--a-dark: var(--a);
|
||||
--a-gray: var(--fg-weak);
|
||||
--btn-fg: var(--fg);
|
||||
--btn-bg: var(--bg);
|
||||
--btn-h-fg: var(--fg);
|
||||
--btn-h-bg: var(--bg);
|
||||
--btn-1-fg: var(--fg);
|
||||
--btn-1-bg: var(--bg);
|
||||
--btn-1h-bg: var(--bg-d3);
|
||||
--txt-sh: a;
|
||||
--txt-bg: var(--white);
|
||||
--u2-b1-bg: var(--w2);
|
||||
--u2-b2-bg: var(--w2);
|
||||
--u2-txt-bg: var(--w2);
|
||||
--u2-tab-bg: a;
|
||||
--u2-tab-1-bg: var(--w2);
|
||||
--sort-1: var(--fg-weak);
|
||||
--tree-bg: var(--w);
|
||||
--g-b1: a;
|
||||
--g-b2: a;
|
||||
--g-f-bg: var(--w2);
|
||||
--f-sh1: 0.1;
|
||||
--f-sh2: 0.02;
|
||||
--f-sh3: 0.1;
|
||||
--f-h-b1: a;
|
||||
--srv-1: var(--w);
|
||||
--srv-3: var(--a);
|
||||
--mp-sh: a;
|
||||
--black: #000;
|
||||
--white: #fff;
|
||||
--grey: grey;
|
||||
--silver: silver;
|
||||
--transparent: transparent;
|
||||
--shadow-color-1: #0a0a0a;
|
||||
--shadow-color-2: #808080;
|
||||
--border-dashed-black: 1px dashed var(--black);
|
||||
--radius: 0;
|
||||
--focus-outline: 1px dashed var(--black);
|
||||
--hover-outline: 1px dotted var(--black);
|
||||
--fm-off: var(--w3);
|
||||
--ttlbar: linear-gradient(90deg, navy, #1084d0);
|
||||
--inset-bg: var(--white);
|
||||
--scroll-bkg: var(--white);
|
||||
|
||||
/*All sides*/
|
||||
--shadow-outset: inset -1px -1px var(--shadow-color-1),
|
||||
inset 1px 1px var(--white), inset -2px -2px var(--grey),
|
||||
inset 2px 2px var(--w2);
|
||||
|
||||
--shadow-inset: inset -1px -1px var(--white),
|
||||
inset 1px 1px var(--shadow-color-1), inset -2px -2px var(--w2),
|
||||
inset 2px 2px var(--shadow-color-2);
|
||||
|
||||
--shadow-input: inset -1px -1px var(--white), inset 1px 1px var(--grey),
|
||||
inset -2px -2px var(--w2), inset 2px 2px var(--shadow-color-1);
|
||||
|
||||
/*Indiv sides*/
|
||||
--shadow-outset-bottom: inset 0 -1px var(--shadow-color-1),
|
||||
inset 0 -2px var(--grey);
|
||||
--shadow-outset-right: inset -1px 0 var(--shadow-color-1),
|
||||
inset -2px 0 var(--grey);
|
||||
--shadow-outset-left: inset 1px 0 var(--white), inset 2px 0 var(--w2);
|
||||
--shadow-outset-top: inset 0 1px var(--white), inset 0 2px var(--w2);
|
||||
|
||||
--shadow-inset-bottom: inset 0 -1px var(--white), inset 0 -2px var(--w2);
|
||||
--shadow-inset-right: inset -1px 0 var(--white), inset -2px 0 var(--w2);
|
||||
--shadow-inset-left: inset 1px 0 var(--shadow-color-1),
|
||||
inset 2px 0 var(--shadow-color-2);
|
||||
--shadow-inset-top: inset 0 1px var(--shadow-color-1),
|
||||
inset 0 2px var(--shadow-color-2);
|
||||
}
|
||||
html.ez {
|
||||
--negative-space: 0em; /* Use this to change the global spacing of your theme :) */
|
||||
--font-main: consolas;
|
||||
--font-serif: consolas;
|
||||
--font-mono: consolas;
|
||||
--w: #fff;
|
||||
--w2: var(--inset-bg);
|
||||
--w3: grey;
|
||||
--fg: #cfcfcf;
|
||||
--fg-max: #47b8ff;
|
||||
--fg-weak: #47b8ff;
|
||||
--bg: #383838;
|
||||
--bg-d3: #600000;
|
||||
--bg-d2: var(--shadow-color-1);
|
||||
--bg-d1: var(--bg);
|
||||
--u2-tab-1-fg: #ff0;
|
||||
--bg-u2: var(--bg);
|
||||
--bg-u3: var(--bg);
|
||||
--bg-u5: var(--shadow-color-2);
|
||||
--tab-alt: #47b8ff;
|
||||
--g-fsel-bg: #0000b7;
|
||||
--g-sel-bg: #00f;
|
||||
--g-fsel-b1: #fff;
|
||||
--row-alt: #555555;
|
||||
--scroll: #555555;
|
||||
--f-sel-sh: transparent;
|
||||
--a: var(--fg);
|
||||
--a-b: var(--fg);
|
||||
--a-hil: var(--fg);
|
||||
--btn-1h-bg: var(--bg-d3);
|
||||
--a-h-bg: var(--bg);
|
||||
--a-dark: var(--a);
|
||||
--a-gray: var(--fg-weak);
|
||||
--btn-fg: var(--white);
|
||||
--btn-bg: var(--bg);
|
||||
--btn-h-fg: var(--white);
|
||||
--btn-h-bg: var(--bg);
|
||||
--btn-1-fg: var(--white);
|
||||
--btn-1-bg: var(--bg);
|
||||
--txt-sh: a;
|
||||
--u2-b1-bg: var(--w2);
|
||||
--u2-b2-bg: var(--w2);
|
||||
--u2-txt-bg: var(--w2);
|
||||
--u2-tab-bg: a;
|
||||
--u2-tab-1-bg: var(--w2);
|
||||
--sort-1: var(--fg-weak);
|
||||
--g-b1: a;
|
||||
--g-b2: a;
|
||||
--g-f-bg: var(--w2);
|
||||
--f-sh1: 0.1;
|
||||
--f-sh2: 0.02;
|
||||
--f-sh3: 0.1;
|
||||
--f-h-b1: a;
|
||||
--srv-1: var(--w);
|
||||
--srv-3: var(--a);
|
||||
--mp-sh: a;
|
||||
--black: #000;
|
||||
--white: #fff;
|
||||
--grey: grey;
|
||||
--silver: #858585;
|
||||
--transparent: transparent;
|
||||
--shadow-color-1: #101010;
|
||||
--shadow-color-2: #1f1f1f;
|
||||
--border-dashed-black: 1px dashed var(--shadow-color-1);
|
||||
--radius: 0;
|
||||
--focus-outline: 1px dashed var(--white);
|
||||
--hover-outline: 1px dotted var(--white);
|
||||
--fm-off: var(--w3);
|
||||
--ttlbar: linear-gradient(90deg, var(--shadow-color-1) 20%, #888888);
|
||||
--inset-bg: #3f3f3f;
|
||||
--tree-bg: var(--inset-bg);
|
||||
--txt-bg: var(--inset-bg);
|
||||
--scroll-bkg: var(--black);
|
||||
|
||||
/*All sides*/
|
||||
--shadow-outset: inset -1px -1px var(--shadow-color-1), inset 1px 1px #878787,
|
||||
inset -2px -2px var(--shadow-color-2), inset 2px 2px #575757;
|
||||
|
||||
--shadow-inset: inset -1px -1px #878787, inset 1px 1px var(--shadow-color-1),
|
||||
inset -2px -2px #575757, inset 2px 2px var(--shadow-color-2);
|
||||
|
||||
--shadow-input: inset -1px -1px var(--white),
|
||||
inset 1px 1px var(--shadow-color-2), inset -2px -2px #575757,
|
||||
inset 2px 2px var(--shadow-color-1);
|
||||
|
||||
--shadow-outset-bottom: inset 0 -1px var(--shadow-color-1),
|
||||
inset 0 -2px var(--shadow-color-2);
|
||||
--shadow-outset-right: inset -1px 0 var(--shadow-color-1),
|
||||
inset -2px 0 var(--shadow-color-2);
|
||||
--shadow-outset-left: inset 1px 0 #878787, inset 2px 0 #575757;
|
||||
--shadow-outset-top: inset 0 1px #878787, inset 0 2px #575757;
|
||||
|
||||
--shadow-inset-bottom: inset 0 -1px #878787, inset 0 -2px #575757;
|
||||
--shadow-inset-right: inset -1px 0 #878787, inset -2px 0 #575757;
|
||||
--shadow-inset-left: inset 1px 0 var(--shadow-color-1),
|
||||
inset 2px 0 var(--shadow-color-2);
|
||||
--shadow-inset-top: inset 0 1px var(--shadow-color-1),
|
||||
inset 0 2px var(--shadow-color-2);
|
||||
}
|
||||
|
||||
html.e {
|
||||
text-shadow: none;
|
||||
}
|
||||
html.e #files,
|
||||
html.e #u2conf input[type="checkbox"]:hover + label,
|
||||
html.e .tgl.btn.on:hover,
|
||||
html.e body {
|
||||
background: var(--bg);
|
||||
}
|
||||
html.e #pctl a,
|
||||
html.e #repl,
|
||||
html.e #u2conf a,
|
||||
html.e #u2conf input[type="checkbox"] + label,
|
||||
html.e #wfp a,
|
||||
html.e .btn,
|
||||
html.e .eq_step,
|
||||
html.e input[type="submit"] {
|
||||
box-shadow: var(--shadow-outset);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg);
|
||||
border: 0;
|
||||
}
|
||||
a.s0r,
|
||||
html.e #ghead a.s0,
|
||||
html.e #u2conf input[type="checkbox"]:checked + label,
|
||||
html.e .tgl.btn.on,
|
||||
html.e input[type="submit"]:active {
|
||||
box-shadow: var(--shadow-inset) !important;
|
||||
}
|
||||
html.e #ops a:hover,
|
||||
html.e #pctl a:hover,
|
||||
html.e #repl:hover,
|
||||
html.e #u2conf a:hover,
|
||||
html.e #u2conf input[type="checkbox"]:hover + label,
|
||||
html.e #wfp a:hover,
|
||||
html.e .btn:hover,
|
||||
html.e .eq_step:hover,
|
||||
html.e input[type="submit"]:hover {
|
||||
outline: var(--hover-outline);
|
||||
outline-offset: -4px;
|
||||
}
|
||||
html.e .ntree a:hover,
|
||||
html.e :focus,
|
||||
html.e :focus + label,
|
||||
html.e a:active,
|
||||
html.e tr:focus,
|
||||
input[type="text"]:focus {
|
||||
outline: var(--focus-outline) !important;
|
||||
}
|
||||
html.e tr:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
html.e #pctl a:focus,
|
||||
html.e #repl:hover,
|
||||
html.e #u2conf input[type="checkbox"]:focus + label,
|
||||
html.e #wfp a:focus,
|
||||
html.e .btn:focus,
|
||||
html.e .eq_step:focus {
|
||||
border: 0 !important;
|
||||
outline: var(--focus-outline) !important;
|
||||
outline-offset: 2px;
|
||||
box-shadow: var(--shadow-outset) !important;
|
||||
}
|
||||
html.e #files tbody,
|
||||
html.e #u2cards a.act {
|
||||
box-shadow: var(--shadow-inset);
|
||||
}
|
||||
html.e #files {
|
||||
border: 2px groove var(--transparent);
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 0.3em;
|
||||
top: 0;
|
||||
border: 0;
|
||||
}
|
||||
html.e #files tbody tr td,
|
||||
html.e #files thead th {
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
#files td {
|
||||
background: var(--w2);
|
||||
}
|
||||
html.e #files tr {
|
||||
background-color: var(--black);
|
||||
}
|
||||
html.e #srv_info span,
|
||||
html.e label {
|
||||
color: var(--btn-fg) !important;
|
||||
}
|
||||
html.e #acc_info {
|
||||
background: var(--transparent);
|
||||
color: var(--white);
|
||||
height: 2em;
|
||||
left: 1em;
|
||||
width: fit-content;
|
||||
}
|
||||
html.e #acc_info,
|
||||
html.e #ops,
|
||||
html.e #srv_info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
html.e #acc_info span.warn,
|
||||
html.e #acc_info a {
|
||||
color: var(--white);
|
||||
}
|
||||
html.e #flogout:before {
|
||||
padding-left: 0.2em;
|
||||
padding-right: 0.4em;
|
||||
content: " | ";
|
||||
}
|
||||
html.e #blogout {
|
||||
color: var(--w);
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
html.e .opwide > div {
|
||||
border-left: 1px solid var(--fg);
|
||||
}
|
||||
html.e #srv_info {
|
||||
background: var(--transparent);
|
||||
color: var(--white);
|
||||
height: fit-content;
|
||||
top: 3.2em;
|
||||
left: 1em;
|
||||
gap: 0.2em;
|
||||
}
|
||||
html.e #u2cards a.act {
|
||||
padding: 0.2em 1em;
|
||||
}
|
||||
html.e #u2btn {
|
||||
border: var(--border-dashed-black);
|
||||
border-radius: var(--border-radius);
|
||||
transform: translateY(30%);
|
||||
}
|
||||
html.e #ops,
|
||||
html.e #ops a {
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
html.e #acc_info {
|
||||
background: var(--transparent);
|
||||
color: var(--white);
|
||||
height: fit-content;
|
||||
align-items: center;
|
||||
top: 3.2em;
|
||||
right: 1em;
|
||||
left: auto;
|
||||
display: flex;
|
||||
gap: 0.2em;
|
||||
}
|
||||
html.e #u2btn {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
html.e #ops {
|
||||
background: var(--ttlbar);
|
||||
/*HC*/
|
||||
box-shadow: inset 0-1px grey, inset 0-2px var(--shadow-color-1);
|
||||
height: 2em;
|
||||
gap: 0.6em;
|
||||
padding: 0.2em;
|
||||
flex-direction: row-reverse;
|
||||
margin-bottom: 1.2em;
|
||||
}
|
||||
html.e #srch_form,
|
||||
html.e .opbox {
|
||||
padding-bottom: 1em;
|
||||
padding-top: 1em;
|
||||
max-width: 100vw;
|
||||
}
|
||||
html.e #ghead,
|
||||
html.e #ops a {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
html.e #ops a {
|
||||
text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5);
|
||||
height: 1.4em;
|
||||
padding: 0;
|
||||
box-shadow: var(--shadow-outset);
|
||||
background: var(--bg);
|
||||
aspect-ratio: 1/1;
|
||||
justify-content: center;
|
||||
font-size: 1.25em;
|
||||
z-index: 4;
|
||||
}
|
||||
html.e #blogout:focus,
|
||||
html.e #ops a:focus {
|
||||
outline: 1px dashed var(--w) !important;
|
||||
}
|
||||
|
||||
html.e #blogout:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
html.e #ops > a:not(:first-child).act {
|
||||
height: 1.4em;
|
||||
width: 1.4em;
|
||||
padding-bottom: 0.3em;
|
||||
margin-top: 0.3em;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
box-shadow: var(--shadow-inset-left), var(--shadow-inset-top),
|
||||
var(--shadow-inset-right);
|
||||
z-index: 6;
|
||||
}
|
||||
html.e #ops a.act {
|
||||
box-shadow: var(--shadow-inset);
|
||||
border-bottom: 0;
|
||||
}
|
||||
html.e a:active {
|
||||
border: 0;
|
||||
}
|
||||
html.e :focus,
|
||||
html.e :focus + label {
|
||||
border: 0 !important;
|
||||
outline-offset: 1px;
|
||||
border-radius: var(--radius) !important;
|
||||
box-shadow: inherit;
|
||||
}
|
||||
html.e #opa_x {
|
||||
text-shadow: 0 0 0 var(--transparent) !important;
|
||||
color: var(--bg) !important;
|
||||
display: flex;
|
||||
}
|
||||
html.e #opa_x:before {
|
||||
content: "⨯";
|
||||
color: var(--fg) !important;
|
||||
margin-top: -0.1em;
|
||||
font-size: 1.75em;
|
||||
position: absolute;
|
||||
}
|
||||
html.e .opbox {
|
||||
margin: -1.2em 0 0;
|
||||
box-shadow: var(--shadow-inset-bottom), var(--shadow-inset-left),
|
||||
var(--shadow-inset-right);
|
||||
border-radius: var(--radius);
|
||||
z-index: 5;
|
||||
background: var(--bg);
|
||||
}
|
||||
html.e #srch_form {
|
||||
margin: 0;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
html.e #op_unpost {
|
||||
max-width: 100vw;
|
||||
margin: 0;
|
||||
}
|
||||
html.e label:focus {
|
||||
box-shadow: 0 0;
|
||||
}
|
||||
html.e #tree {
|
||||
box-shadow: none;
|
||||
padding-right: 5px;
|
||||
}
|
||||
html.e #tt {
|
||||
background: var(--w2);
|
||||
}
|
||||
html.e .mdo a {
|
||||
background: 0 0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
html.e .mdo code,
|
||||
html.e .mdo pre {
|
||||
color: var(--white);
|
||||
background: var(--w2);
|
||||
border: 0;
|
||||
}
|
||||
html.e .mdo h1,
|
||||
html.e .mdo h2 {
|
||||
background: 0 0;
|
||||
border-color: var(--w2);
|
||||
}
|
||||
html.e #tt,
|
||||
html.e .mdo ol ol,
|
||||
html.e .mdo ol ul,
|
||||
html.e .mdo ul ol,
|
||||
html.e .mdo ul ul {
|
||||
border-color: var(--w2);
|
||||
}
|
||||
html.e .mdo li > em,
|
||||
html.e .mdo p > em,
|
||||
html.e .mdo td > em {
|
||||
color: #fd0;
|
||||
}
|
||||
html.e input.txtbox,
|
||||
html.e input[type="text"],
|
||||
html.e select {
|
||||
background-color: var(--txt-bg);
|
||||
box-shadow: var(--shadow-input) !important;
|
||||
box-sizing: border-box;
|
||||
padding: 3px 4px;
|
||||
border-radius: var(--radius);
|
||||
border: 0;
|
||||
}
|
||||
html.e #gfiles {
|
||||
box-shadow: var(--shadow-outset);
|
||||
background: var(--bg);
|
||||
padding: 0.4em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3em;
|
||||
}
|
||||
html.e #ggrid {
|
||||
background-color: var(--inset-bg);
|
||||
box-shadow: var(--shadow-input);
|
||||
padding: 1.5em;
|
||||
margin: 0;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
html.e #ghead {
|
||||
margin: 0;
|
||||
justify-content: flex-end;
|
||||
gap: 0.4em;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
top: 0px;
|
||||
border-radius: 0px;
|
||||
}
|
||||
html.e #ghead a {
|
||||
margin: 0;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
html.e ::-webkit-scrollbar,
|
||||
html.e::-webkit-scrollbar {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
background: var(--transparent) !important;
|
||||
}
|
||||
html.e ::-webkit-scrollbar-button,
|
||||
html.e ::-webkit-scrollbar-thumb,
|
||||
html.e::-webkit-scrollbar-button,
|
||||
html.e::-webkit-scrollbar-thumb {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
background: var(--scroll) !important;
|
||||
/*HC*/
|
||||
box-shadow: var(--shadow-outset);
|
||||
border: 1px solid !important;
|
||||
border-color: var(--silver) var(--black) var(--black) var(--silver) !important;
|
||||
}
|
||||
html.e ::-webkit-scrollbar-track,
|
||||
html.e::-webkit-scrollbar-track {
|
||||
image-rendering: optimize-contrast !important;
|
||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAyIDIiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyI+CjxtZXRhZGF0YT5NYWRlIHdpdGggUGl4ZWxzIHRvIFN2ZyBodHRwczovL2NvZGVwZW4uaW8vc2hzaGF3L3Blbi9YYnh2Tmo8L21ldGFkYXRhPgo8cGF0aCBzdHJva2U9IiNjMGMwYzAiIGQ9Ik0wIDBoMU0xIDFoMSIgLz4KPC9zdmc+) !important;
|
||||
background-position: 0 0 !important;
|
||||
background-repeat: repeat !important;
|
||||
background-size: 2px !important;
|
||||
background: var(--scroll-bkg);
|
||||
}
|
||||
#tree::-webkit-scrollbar,
|
||||
#tree::-webkit-scrollbar-track {
|
||||
background: var(--scroll-bkg);
|
||||
}
|
||||
html.e ::-webkit-scrollbar-button,
|
||||
html.e::-webkit-scrollbar-button {
|
||||
background-repeat: no-repeat !important;
|
||||
background-size: 16px !important;
|
||||
}
|
||||
html.e ::-webkit-scrollbar-button:single-button:vertical:decrement,
|
||||
html.e::-webkit-scrollbar-button:single-button:vertical:decrement {
|
||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTcgNWgxTTYgNmgzTTUgN2g1TTQgOGg3IiAvPgo8L3N2Zz4=) !important;
|
||||
}
|
||||
html.e ::-webkit-scrollbar-button:single-button:vertical:increment,
|
||||
html.e::-webkit-scrollbar-button:single-button:vertical:increment {
|
||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTQgNWg3TTUgNmg1TTYgN2gzTTcgOGgxIiAvPgo8L3N2Zz4=) !important;
|
||||
}
|
||||
html.e ::-webkit-scrollbar-button:single-button:horizontal:decrement,
|
||||
html.e::-webkit-scrollbar-button:single-button:horizontal:decrement {
|
||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTggM2gxTTcgNGgyTTYgNWgzTTUgNmg0TTYgN2gzTTcgOGgyTTggOWgxIiAvPgo8L3N2Zz4=) !important;
|
||||
}
|
||||
html.e ::-webkit-scrollbar-button:single-button:horizontal:increment,
|
||||
html.e::-webkit-scrollbar-button:single-button:horizontal:increment {
|
||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTYgM2gxTTYgNGgyTTYgNWgzTTYgNmg0TTYgN2gzTTYgOGgyTTYgOWgxIiAvPgo8L3N2Zz4=) !important;
|
||||
}
|
||||
html.e ::-webkit-scrollbar-corner,
|
||||
html.e::-webkit-scrollbar-corner {
|
||||
background: var(--silver) !important;
|
||||
}
|
||||
html,
|
||||
html.e #tree {
|
||||
scrollbar-color: inherit !important;
|
||||
}
|
||||
html.e #tree {
|
||||
background: var(--bg);
|
||||
padding-left: 0.4em;
|
||||
padding-top: 0;
|
||||
margin-left: var(--negative-space);
|
||||
}
|
||||
html.e.noscroll #tree {
|
||||
/*HC*/
|
||||
box-shadow: 1px 1px var(--grey), 2px 2px var(--shadow-color-1),
|
||||
var(--shadow-outset-bottom);
|
||||
}
|
||||
html.e #treeh {
|
||||
background: var(--bg);
|
||||
box-shadow: var(--shadow-outset-top), var(--shadow-outset-bottom);
|
||||
width: calc(1.5em + var(--nav-sz) - var(--sbw));
|
||||
height: 2.4em;
|
||||
border: none;
|
||||
top: -2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6em;
|
||||
}
|
||||
html.e #treeh .btn {
|
||||
margin: 0px;
|
||||
top: auto;
|
||||
}
|
||||
html.e #tree ul {
|
||||
border-left: var(--border-dashed-black);
|
||||
margin-left: 2.15em;
|
||||
}
|
||||
html.e .ntree a:first-child {
|
||||
font-family: scp, monospace, monospace;
|
||||
font-size: 1.2em;
|
||||
line-height: 0;
|
||||
background: var(--inset-bg);
|
||||
aspect-ratio: 1/1;
|
||||
text-align: center;
|
||||
align-content: center;
|
||||
border-radius: var(--radius) !important;
|
||||
padding: 0.057em;
|
||||
border: 1px solid var(--black);
|
||||
}
|
||||
html.e .ntree a:first-child:after {
|
||||
content: ".";
|
||||
position: absolute;
|
||||
border-top: var(--border-dashed-black);
|
||||
color: var(--transparent);
|
||||
font-size: 0.9em;
|
||||
margin-left: 0.13em;
|
||||
}
|
||||
html.e #treeul {
|
||||
border: 0 !important;
|
||||
position: static;
|
||||
margin: 0 !important;
|
||||
min-height: 100%;
|
||||
height: max-content;
|
||||
}
|
||||
html.e .ntree a:last-of-type:before {
|
||||
content: "📁";
|
||||
margin-left: 0.3em;
|
||||
}
|
||||
html.e .ntree {
|
||||
padding-left: 1em !important;
|
||||
padding-top: 0.3em !important;
|
||||
background: var(--inset-bg);
|
||||
box-shadow: var(--shadow-inset-left), var(--shadow-inset-bottom);
|
||||
}
|
||||
html.e #tree li {
|
||||
margin-left: -0.5em;
|
||||
border-top: 0;
|
||||
}
|
||||
html.e .ntree a:hover {
|
||||
outline-offset: -2px;
|
||||
color: var(--fg);
|
||||
border-radius: var(--radius) !important;
|
||||
}
|
||||
html.e #treepar {
|
||||
width: calc(-1em + var(--nav-sz) - var(--sbw));
|
||||
overflow: hidden;
|
||||
left: -0.7em;
|
||||
box-shadow: var(--shadow-inset-left), var(--shadow-inset-top);
|
||||
border-left: 0 !important;
|
||||
border-bottom: var(--border-dashed-black);
|
||||
margin-left: calc(2.1em - (1em - var(--negative-space))) !important;
|
||||
}
|
||||
html.e #path,
|
||||
html.e #widgeti,
|
||||
html.e #wtoggle,
|
||||
html.e #wtoggle a,
|
||||
html.e #files,
|
||||
html.e #files thead th,
|
||||
html.e #ghead a,
|
||||
html.e #tree {
|
||||
box-shadow: var(--shadow-outset);
|
||||
}
|
||||
html.e.noscroll #treepar {
|
||||
width: calc(var(--nav-sz) - 1em);
|
||||
}
|
||||
html.e #docul {
|
||||
border-left: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
html.e #wrap {
|
||||
transform: translateX(calc((var(--negative-space) * 2) - 1.2em));
|
||||
padding-right: var(--negative-space);
|
||||
position: relative;
|
||||
margin-right: calc((var(--negative-space) * 2) - 1.2em);
|
||||
margin-top: var(--negative-space);
|
||||
margin-left: 1.2em;
|
||||
/*overflow-x: auto; fix for OOB table when screen space is limited (mobile), but removes sticky header*/
|
||||
}
|
||||
html.e input[type="radio"] {
|
||||
accent-color: #232323;
|
||||
}
|
||||
html.e #path {
|
||||
width: calc(100% - 0.4em);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 0.2em;
|
||||
overflow-x: auto;
|
||||
}
|
||||
html.e #path i {
|
||||
border: 1px solid var(--w);
|
||||
border-color: var(--w);
|
||||
margin: 0;
|
||||
border-width: 0.1em 0.1em 0 0;
|
||||
height: 0.5em;
|
||||
width: 0.5em;
|
||||
}/*
|
||||
html.e #hovertree:after {
|
||||
color: red;
|
||||
content: "BUGGY";
|
||||
html.ez #hovertree:after {
|
||||
color: rgb(255 98 98);
|
||||
content: "BUGGY";
|
||||
}
|
||||
}*/
|
||||
html.e #widget {
|
||||
box-shadow: 0 0;
|
||||
border: 0 !important;
|
||||
}
|
||||
html.e #wtico,
|
||||
html.e #zip1 {
|
||||
box-shadow: 0 0 !important;
|
||||
}
|
||||
html.e #wtgrid {
|
||||
top: -0.09em;
|
||||
}
|
||||
html.e #wfs,
|
||||
html.e #wm3u,
|
||||
html.e #wnp,
|
||||
html.e #wzip {
|
||||
border-width: 0 1px 0 0;
|
||||
}
|
||||
html.e #wfm.act + #wzip1 + #wzip,
|
||||
html.e #wfm.act + #wzip1 + #wzip + #wnp {
|
||||
border-left-width: 1px;
|
||||
}
|
||||
html.e #barpos {
|
||||
/* border-radius: var(--radius); */
|
||||
box-shadow: var(--shadow-inset);
|
||||
}
|
||||
html.e #goh + span {
|
||||
border-left: 0.1em solid var(--bg-u5);
|
||||
}
|
||||
html.e #wfp {
|
||||
margin: var(--negative-space);
|
||||
font-size: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
html.e #wfp a {
|
||||
font-size: large;
|
||||
display: inline-block;
|
||||
}
|
||||
html.e #repl {
|
||||
font-size: large;
|
||||
padding: 0.33em;
|
||||
right: calc(var(--negative-space) * 0.89);
|
||||
position: absolute;
|
||||
}
|
||||
html.e #epi {
|
||||
text-align: center;
|
||||
text-wrap-mode: nowrap;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
html.e #epi.logue:not(.mdo) {
|
||||
padding: 0.8em;
|
||||
box-shadow: var(--shadow-outset);
|
||||
}
|
||||
|
||||
html.e #epi.logue.mdo {
|
||||
padding-left: 3px;
|
||||
}
|
||||
|
||||
html.e #doc {
|
||||
box-shadow: var(--shadow-inset);
|
||||
background: var(--inset-bg);
|
||||
margin: 0.2em;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
html.e #detree {
|
||||
padding: 0px;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/browser.css?_={{ ts }}">
|
||||
{{ html_head }}
|
||||
{{- html_head }}
|
||||
{%- if css %}
|
||||
<link rel="stylesheet" media="screen" href="{{ css }}_={{ ts }}">
|
||||
{%- endif %}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -10,7 +10,7 @@
|
|||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/shares.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
{{ html_head }}
|
||||
{{- html_head }}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -23,17 +23,17 @@
|
|||
<th>user</th>
|
||||
<th>groups</th>
|
||||
</tr></thead><tbody>
|
||||
{% for un, gn in rows %}
|
||||
{%- for un, gn in rows %}
|
||||
<tr>
|
||||
<td><a href="{{ r }}/?idp=rm={{ un|e }}">forget</a></td>
|
||||
<td>{{ un|e }}</td>
|
||||
<td>{{ gn|e }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody></table>
|
||||
{% if not rows %}
|
||||
{%- if not rows %}
|
||||
(there are no IdP users in the cache)
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
</div>
|
||||
<a href="#" id="repl">π</a>
|
||||
<script>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
{%- if edit %}
|
||||
<link rel="stylesheet" href="{{ r }}/.cpr/md2.css?_={{ ts }}">
|
||||
{%- endif %}
|
||||
{{ html_head }}
|
||||
{{- html_head }}
|
||||
</head>
|
||||
<body>
|
||||
<div id="mn"></div>
|
||||
|
@ -130,7 +130,8 @@ write markdown (most html is 🙆 too)
|
|||
|
||||
var SR = "{{ r }}",
|
||||
last_modified = {{ lastmod }},
|
||||
have_emp = {{ "true" if have_emp else "false" }},
|
||||
have_emp = {{ have_emp }},
|
||||
md_no_br = {{ md_no_br }},
|
||||
dfavico = "{{ favico }}";
|
||||
|
||||
var md_opt = {
|
||||
|
|
|
@ -201,7 +201,7 @@ function convert_markdown(md_text, dest_dom) {
|
|||
|
||||
var marked_opts = {
|
||||
//headerPrefix: 'h-',
|
||||
breaks: true,
|
||||
breaks: !md_no_br,
|
||||
gfm: true
|
||||
};
|
||||
|
||||
|
@ -217,7 +217,7 @@ function convert_markdown(md_text, dest_dom) {
|
|||
catch (ex) {
|
||||
if (IE) {
|
||||
dest_dom.innerHTML = 'IE cannot into markdown ;_;';
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ext)
|
||||
|
@ -344,6 +344,8 @@ function convert_markdown(md_text, dest_dom) {
|
|||
}
|
||||
catch (ex) { }
|
||||
}, 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
@ -422,7 +424,7 @@ function init_toc() {
|
|||
}
|
||||
}
|
||||
|
||||
// hilight the correct toc items + scroll into view
|
||||
// highlight the correct toc items + scroll into view
|
||||
function freshen_toclist() {
|
||||
if (anchors.length == 0)
|
||||
return;
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
"use strict";
|
||||
|
||||
|
||||
var sloc0 = '' + location,
|
||||
dbg_kbd = /[?&]dbgkbd\b/.exec(sloc0);
|
||||
|
||||
|
||||
// server state
|
||||
var server_md = dom_src.value;
|
||||
|
||||
|
@ -697,8 +701,14 @@ function reLastIndexOf(txt, ptn, end) {
|
|||
// table formatter
|
||||
function fmt_table(e) {
|
||||
if (e) e.preventDefault();
|
||||
//dom_tbox.className = '';
|
||||
|
||||
try {
|
||||
fmt_table2();
|
||||
}
|
||||
catch (ex) {
|
||||
return toast.err(7, 'table-format (CTRL-K) failed:\n' + ex);
|
||||
}
|
||||
}
|
||||
function fmt_table2() {
|
||||
var txt = dom_src.value,
|
||||
ofs = dom_src.selectionStart,
|
||||
//o0 = txt.lastIndexOf('\n\n', ofs),
|
||||
|
@ -930,23 +940,30 @@ var set_lno = (function () {
|
|||
|
||||
// hotkeys / toolbar
|
||||
(function () {
|
||||
var keydown = function (ev) {
|
||||
if (!ev && window.event) {
|
||||
ev = window.event;
|
||||
var keydown = function (e) {
|
||||
if (!e && window.event) {
|
||||
e = window.event;
|
||||
if (dev_fbw == 1) {
|
||||
toast.warn(10, 'hello from fallback code ;_;\ncheck console trace');
|
||||
console.error('using window.event');
|
||||
}
|
||||
}
|
||||
var kc = ev.code || ev.keyCode || ev.which,
|
||||
var k = (e.key || e.code) + '',
|
||||
editing = document.activeElement == dom_src;
|
||||
|
||||
//console.log(ev.key, ev.code, ev.keyCode, ev.which);
|
||||
if (ctrl(ev) && (ev.code == "KeyS" || kc == 83)) {
|
||||
if (k.startsWith('Key'))
|
||||
k = k.slice(3);
|
||||
|
||||
var kl = k.toLowerCase();
|
||||
|
||||
if (dbg_kbd)
|
||||
console.log('KBD', k, kl, e.key, e.code, e.keyCode, e.which);
|
||||
|
||||
if (ctrl(e) && kl == "s") {
|
||||
save();
|
||||
return false;
|
||||
}
|
||||
if (ev.code == "Escape" || kc == 27) {
|
||||
if (k == "Escape" || k == "Esc") {
|
||||
var d = ebi('helpclose');
|
||||
if (d)
|
||||
d.click();
|
||||
|
@ -954,46 +971,44 @@ var set_lno = (function () {
|
|||
if (editing)
|
||||
set_lno();
|
||||
|
||||
if (ctrl(ev)) {
|
||||
if (ev.code == "KeyE") {
|
||||
if (ctrl(e)) {
|
||||
if (kl == "e") {
|
||||
dom_nsbs.click();
|
||||
return false;
|
||||
}
|
||||
if (!editing)
|
||||
return true;
|
||||
|
||||
if (ev.code == "KeyH" || kc == 72) {
|
||||
md_header(ev.shiftKey);
|
||||
if (kl == "h") {
|
||||
md_header(e.shiftKey);
|
||||
return false;
|
||||
}
|
||||
if (ev.code == "KeyZ" || kc == 90) {
|
||||
if (ev.shiftKey)
|
||||
if (kl == "z") {
|
||||
if (e.shiftKey)
|
||||
action_stack.redo();
|
||||
else
|
||||
action_stack.undo();
|
||||
|
||||
return false;
|
||||
}
|
||||
if (ev.code == "KeyY" || kc == 89) {
|
||||
if (kl == "y") {
|
||||
action_stack.redo();
|
||||
return false;
|
||||
}
|
||||
if (ev.code == "KeyK") {
|
||||
if (kl == "k") {
|
||||
fmt_table();
|
||||
return false;
|
||||
}
|
||||
if (ev.code == "KeyU") {
|
||||
if (kl == "u") {
|
||||
iter_uni();
|
||||
return false;
|
||||
}
|
||||
var up = ev.code == "ArrowUp" || kc == 38;
|
||||
var dn = ev.code == "ArrowDown" || kc == 40;
|
||||
if (up || dn) {
|
||||
md_p_jump(dn);
|
||||
if (k == "ArrowUp" || k == "ArrowDown") {
|
||||
md_p_jump(k == "ArrowDown");
|
||||
return false;
|
||||
}
|
||||
if (ev.code == "KeyX" || ev.code == "KeyC") {
|
||||
md_cut(ev.code == "KeyX");
|
||||
if (kl == "x" || kl == "c") {
|
||||
md_cut(kl == "x");
|
||||
return true; //sic
|
||||
}
|
||||
}
|
||||
|
@ -1001,18 +1016,18 @@ var set_lno = (function () {
|
|||
if (!editing)
|
||||
return true;
|
||||
|
||||
if (ev.code == "Tab" || kc == 9) {
|
||||
md_indent(ev.shiftKey);
|
||||
if (k == "Tab") {
|
||||
md_indent(e.shiftKey);
|
||||
return false;
|
||||
}
|
||||
if (ev.code == "Home" || kc == 36) {
|
||||
md_home(ev.shiftKey);
|
||||
if (k == "Home") {
|
||||
md_home(e.shiftKey);
|
||||
return false;
|
||||
}
|
||||
if (!ev.shiftKey && ((ev.code + '').endsWith("Enter") || kc == 13)) {
|
||||
if (!e.shiftKey && k.endsWith("Enter")) {
|
||||
return md_newline();
|
||||
}
|
||||
if (!ev.shiftKey && kc == 8) {
|
||||
if (!e.shiftKey && k == "Backspace") {
|
||||
return md_backspace();
|
||||
}
|
||||
}
|
||||
|
@ -1036,7 +1051,9 @@ ebi('help').onclick = function (e) {
|
|||
var dom = ebi('helpbox');
|
||||
var dtxt = dom.getElementsByTagName('textarea');
|
||||
if (dtxt.length > 0) {
|
||||
convert_markdown(dtxt[0].value, dom);
|
||||
var txt = dtxt[0].value;
|
||||
if (!convert_markdown(txt, dom))
|
||||
dom.innerText = txt.split('## markdown')[0];
|
||||
dom.innerHTML = '<a href="#" id="helpclose">close</a>' + dom.innerHTML;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<link rel="stylesheet" href="{{ r }}/.cpr/mde.css?_={{ ts }}">
|
||||
<link rel="stylesheet" href="{{ r }}/.cpr/deps/mini-fa.css?_={{ ts }}">
|
||||
<link rel="stylesheet" href="{{ r }}/.cpr/deps/easymde.css?_={{ ts }}">
|
||||
{{ html_head }}
|
||||
{{- html_head }}
|
||||
</head>
|
||||
<body>
|
||||
<div id="mw">
|
||||
|
@ -28,7 +28,8 @@
|
|||
|
||||
var SR = "{{ r }}",
|
||||
last_modified = {{ lastmod }},
|
||||
have_emp = {{ "true" if have_emp else "false" }},
|
||||
have_emp = {{ have_emp }},
|
||||
md_no_br = {{ md_no_br }},
|
||||
dfavico = "{{ favico }}";
|
||||
|
||||
var md_opt = {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/msg.css?_={{ ts }}">
|
||||
{{ html_head }}
|
||||
{{- html_head }}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/rups.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
{{ html_head }}
|
||||
{{- html_head }}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
function render() {
|
||||
var html = ['<table id="tab"><thead><tr><th>size</th><th>who</th><th>when</th><th>age</th><th>dir</th><th>file</th></tr></thead><tbody>'];
|
||||
var html = ['<table id="tab"><thead><tr><th>size</th><th>who</th><th>ip</th><th>when</th><th>age</th><th>dir</th><th>file</th></tr></thead><tbody>'];
|
||||
var ups = V.ups, now = V.now;
|
||||
ebi('filter').value = V.filter;
|
||||
ebi('hits').innerHTML = 'showing ' + ups.length + ' files';
|
||||
|
@ -16,6 +16,7 @@ function render() {
|
|||
sz = ('' + f.sz).replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
||||
|
||||
html.push('<tr><td>' + sz +
|
||||
'</td><td>' + (f.un || '') +
|
||||
'</td><td>' + f.ip +
|
||||
'</td><td>' + ts +
|
||||
'</td><td>' + sa +
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/shares.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
{{ html_head }}
|
||||
{{- html_head }}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -36,7 +36,7 @@
|
|||
<th>hrs</th>
|
||||
<th>add time</th>
|
||||
</tr></thead><tbody>
|
||||
{% for k, pw, vp, pr, st, un, t0, t1 in rows %}
|
||||
{%- for k, pw, vp, pr, st, un, t0, t1 in rows %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ r }}{{ shr }}{{ k }}?qr">qr</a>
|
||||
|
@ -54,11 +54,11 @@
|
|||
<td>{{ "inf" if not t1 else "dead" if t1 < now else ((t1 - now) / 3600) | round(1) }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody></table>
|
||||
{% if not rows %}
|
||||
{%- if not rows %}
|
||||
(you don't have any active shares btw)
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
</div>
|
||||
<a href="#" id="repl">π</a>
|
||||
<script>
|
||||
|
|
|
@ -24,6 +24,7 @@ h1 {
|
|||
li {
|
||||
margin: 1em 0;
|
||||
}
|
||||
#lo,
|
||||
a {
|
||||
color: #047;
|
||||
background: #fff;
|
||||
|
@ -37,6 +38,7 @@ a {
|
|||
td a {
|
||||
margin: 0;
|
||||
}
|
||||
#wb,
|
||||
#w {
|
||||
color: #fff;
|
||||
background: #940;
|
||||
|
@ -47,6 +49,7 @@ td a {
|
|||
float: right;
|
||||
margin: -.2em 0 0 .8em;
|
||||
}
|
||||
#lo,
|
||||
.logout,
|
||||
a.r {
|
||||
color: #c04;
|
||||
|
@ -176,12 +179,14 @@ html.z {
|
|||
html.z h1 {
|
||||
border-color: #777;
|
||||
}
|
||||
html.z #lo,
|
||||
html.z a {
|
||||
color: #fff;
|
||||
background: #057;
|
||||
border-color: #37a;
|
||||
}
|
||||
html.z .logout,
|
||||
html.z #lo,
|
||||
html.z a.r {
|
||||
background: #804;
|
||||
border-color: #c28;
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/splash.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
{{ html_head }}
|
||||
{{- html_head }}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -18,12 +18,16 @@
|
|||
<a id="a" href="{{ r }}/?h{{ re }}" class="af">refresh</a>
|
||||
<a id="v" href="{{ r }}/?hc" class="af">connect</a>
|
||||
|
||||
{%- if this.uname == '*' %}
|
||||
<p id="b">howdy stranger <small>(you're not logged in)</small></p>
|
||||
{%- else %}
|
||||
<a id="c" href="{{ r }}/?pw=x" class="logout">logout</a>
|
||||
<p><span id="m">welcome back,</span> <strong>{{ this.uname|e }}</strong></p>
|
||||
{%- endif %}
|
||||
{%- if this.uname == '*' %}
|
||||
<p id="b">howdy stranger <small>(you're not logged in)</small></p>
|
||||
{%- else %}
|
||||
{%- if this.args.idp_logout %}
|
||||
<a id="c" href="{{ this.args.idp_logout }}" class="logout">logout</a>
|
||||
{%- else %}
|
||||
<a id="c" href="{{ r }}/?pw=x" class="logout">logout</a>
|
||||
{%- endif %}
|
||||
<p><span id="m">welcome back,</span> <strong id="un">{{ this.uname|e }}</strong></p>
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
|
||||
{%- if msg %}
|
||||
|
@ -37,9 +41,9 @@
|
|||
<table class="vols">
|
||||
<thead><tr><th>%</th><th>speed</th><th>eta</th><th>idle</th><th>dir</th><th>file</th></tr></thead>
|
||||
<tbody>
|
||||
{% for u in ups %}
|
||||
{%- for u in ups %}
|
||||
<tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td><a href="{{ u[4] }}">{{ u[5]|e }}</a></td><td>{{ u[6]|e }}</td></tr>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{%- endif %}
|
||||
|
@ -49,9 +53,9 @@
|
|||
<table class="vols">
|
||||
<thead><tr><th>%</th><th>sent</th><th>speed</th><th>eta</th><th>idle</th><th></th><th>dir</th><th>file</th></tr></thead>
|
||||
<tbody>
|
||||
{% for u in dls %}
|
||||
{%- for u in dls %}
|
||||
<tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td>{{ u[4] }}</td><td>{{ u[5] }}</td><td><a href="{{ u[6] }}">{{ u[7]|e }}</a></td><td>{{ u[8] }}</td></tr>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{%- endif %}
|
||||
|
@ -70,11 +74,11 @@
|
|||
<table class="vols">
|
||||
<thead><tr><th>vol</th><th id="t">action</th><th>status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for mp in avol %}
|
||||
{%- for mp in avol %}
|
||||
{%- if mp in vstate and vstate[mp] %}
|
||||
<tr><td><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></td><td><a class="s" href="{{ r }}{{ mp }}?scan">rescan</a></td><td>{{ vstate[mp] }}</td></tr>
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</td></tr></table>
|
||||
|
@ -87,18 +91,18 @@
|
|||
{%- if rvol %}
|
||||
<h1 id="f">you can browse:</h1>
|
||||
<ul>
|
||||
{% for mp in rvol %}
|
||||
{%- for mp in rvol %}
|
||||
<li><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></li>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
{%- endif %}
|
||||
|
||||
{%- if wvol %}
|
||||
<h1 id="g">you can upload to:</h1>
|
||||
<ul>
|
||||
{% for mp in wvol %}
|
||||
{%- for mp in wvol %}
|
||||
<li><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></li>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
{%- endif %}
|
||||
|
||||
|
@ -110,64 +114,83 @@
|
|||
<input type="password" id="lp" name="cppwd" placeholder=" password" />
|
||||
<input type="hidden" name="uhash" id="uhash" value="x" />
|
||||
<input type="submit" id="ls" value="Unlock" />
|
||||
{% if ahttps %}
|
||||
{%- if ahttps %}
|
||||
<a id="w" href="{{ ahttps }}">switch to https</a>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
</form>
|
||||
</div>
|
||||
{%- else %}
|
||||
<h1 id="l">login for more:</h1>
|
||||
<div>
|
||||
{%- if this.args.idp_login %}
|
||||
<ul><li>
|
||||
<a href="{{ this.args.idp_login | replace("{dst}",r+"/"+qvpath) }}">{{ this.args.idp_login_t }}</a>
|
||||
{%- if this.args.ao_have_pw %}or alternatively:{%- endif %}
|
||||
</li></ul>
|
||||
{%- endif %}
|
||||
{%- if this.args.ao_have_pw %}
|
||||
<form id="lf" method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}">
|
||||
<input type="hidden" id="la" name="act" value="login" />
|
||||
{% if this.args.usernames %}
|
||||
{%- if this.args.usernames %}
|
||||
<input type="text" id="lu" name="uname" placeholder=" username" size="12" />
|
||||
<input type="password" id="lp" name="cppwd" placeholder=" password" size="12" />
|
||||
{% else %}
|
||||
{%- else %}
|
||||
<input type="password" id="lp" name="cppwd" placeholder=" password" />
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<input type="hidden" name="uhash" id="uhash" value="x" />
|
||||
<input type="submit" id="ls" value="login" />
|
||||
{% if chpw %}
|
||||
{%- if chpw %}
|
||||
<a id="x" href="#">change password</a>
|
||||
{% endif %}
|
||||
{% if ahttps %}
|
||||
{%- endif %}
|
||||
{%- if ahttps %}
|
||||
<a id="w" href="{{ ahttps }}">switch to https</a>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
</form>
|
||||
{%- endif %}
|
||||
</div>
|
||||
{%- endif %}
|
||||
|
||||
<h1 id="cc">other stuff:</h1>
|
||||
<ul>
|
||||
{%- if ahttps %}
|
||||
<li><a id="wb" href="{{ ahttps }}">switch to https</a></li>
|
||||
{%- endif %}
|
||||
|
||||
{%- if this.uname in this.args.idp_adm_set %}
|
||||
<li><a id="ag" href="{{ r }}/?idp">view idp cache</a></li>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
|
||||
{%- if this.uname != '*' and this.args.shr %}
|
||||
<li><a id="y" href="{{ r }}/?shares">edit shares</a></li>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
|
||||
{% if k304 or k304vis %}
|
||||
{% if k304 %}
|
||||
{%- if k304 or k304vis %}
|
||||
{%- if k304 %}
|
||||
<li><a id="h" href="{{ r }}/?cc&setck=k304=n">disable k304</a> (currently enabled)
|
||||
{%- else %}
|
||||
<li><a id="i" href="{{ r }}/?cc&setck=k304=y" class="r">enable k304</a> (currently disabled)
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<blockquote id="j">enabling k304 will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general</blockquote></li>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
|
||||
{% if no304 or no304vis %}
|
||||
{% if no304 %}
|
||||
{%- if no304 or no304vis %}
|
||||
{%- if no304 %}
|
||||
<li><a id="ab" href="{{ r }}/?cc&setck=no304=n">disable no304</a> (currently enabled)
|
||||
{%- else %}
|
||||
<li><a id="ac" href="{{ r }}/?cc&setck=no304=y" class="r">enable no304</a> (currently disabled)
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<blockquote id="ad">enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!</blockquote></li>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
|
||||
<li><a id="af" href="{{ r }}/?ru">show recent uploads</a></li>
|
||||
<li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li>
|
||||
|
||||
{%- if this.uname != '*' and not in_shr %}
|
||||
<li><form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="act" value="logout" />
|
||||
<input type="submit" id="lo" value="logout “{{ this.uname|e }}” everywhere" />
|
||||
</form></li>
|
||||
{%- endif %}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -17,6 +17,11 @@ var Ls = {
|
|||
"j1": "k304 bryter tilkoplingen for hver HTTP 304. Dette hjelper mot visse mellomtjenere som kan sette seg fast / plutselig slutter å laste sider, men det reduserer også ytelsen betydelig",
|
||||
"k1": "nullstill innstillinger",
|
||||
"l1": "logg inn:",
|
||||
"ls3": "logg inn",
|
||||
"lu4": "brukernavn",
|
||||
"lp4": "passord",
|
||||
"lo3": "logg ut “{0}” overalt",
|
||||
"lo2": "avslutter økten på alle nettlesere",
|
||||
"m1": "velkommen tilbake,",
|
||||
"n1": "404: filen finnes ikke ┐( ´ -`)┌",
|
||||
"o1": 'eller kanskje du ikke har tilgang? prøv et passord eller <a href="' + SR + '/?h">gå hjem</a>',
|
||||
|
@ -46,6 +51,7 @@ var Ls = {
|
|||
"eng": {
|
||||
"d2": "shows the state of all active threads",
|
||||
"e2": "reload config files (accounts/volumes/volflags),$Nand rescan all e2ds volumes$N$Nnote: any changes to global settings$Nrequire a full restart to take effect",
|
||||
"lo2": "ends the session on all browsers",
|
||||
"u2": "time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds",
|
||||
"v2": "use this server as a local HDD",
|
||||
"ta1": "fill in your new password first",
|
||||
|
@ -68,6 +74,11 @@ var Ls = {
|
|||
"j1": "k304 会在每个 HTTP 304 时断开连接。这有助于避免某些代理服务器卡住或突然停止加载页面,但也会显著降低性能。",
|
||||
"k1": "重置设置",
|
||||
"l1": "登录:",
|
||||
"ls3": "登录", //m
|
||||
"lu4": "用户名", //m
|
||||
"lp4": "密码", //m
|
||||
"lo3": "在所有地方注销 {0}", //m
|
||||
"lo2": "这将结束在所有浏览器中的会话", //m
|
||||
"m1": "欢迎回来,",
|
||||
"n1": "404: 文件不存在 ┐( ´ -`)┌",
|
||||
"o1": '或者你可能没有权限?尝试输入密码或 <a href="' + SR + '/?h">回家</a>',
|
||||
|
@ -98,33 +109,38 @@ var Ls = {
|
|||
"a1": "obnovit",
|
||||
"b1": "ahoj cizinče <small>(nejsi přihlášen)</small>",
|
||||
"c1": "odhlásit se",
|
||||
"d1": "vypsat zásobníku", // TLNote: "d2" is the tooltip for this button
|
||||
"d1": "vypsat zásobníku",
|
||||
"d2": "zobrazit stav všech aktivních vláken",
|
||||
"e1": "znovu načíst konfiguraci",
|
||||
"e2": "znovu načíst konfigurační soubory (accounts/volumes/volflags),$Na prohledat všechny e2ds úložiště$N$Npoznámka: všechny změny globálních nastavení$Nvyžadují úplné restartování, aby se projevily",
|
||||
"f1": "můžeš procházet:",
|
||||
"g1": "můžeš nahrávat do:",
|
||||
"cc1": "další věci:",
|
||||
"h1": "zakázat k304", // TLNote: "j1" explains what k304 is
|
||||
"h1": "zakázat k304",
|
||||
"i1": "povolit k304",
|
||||
"j1": "povolení k304 odpojí vašeho klienta při každém HTTP 304, což může zabránit některým chybovým proxy serverům, aby se zasekly (náhle nenačítaly stránky), <em>ale</em> také to obecně zpomalí věci",
|
||||
"k1": "resetovat nastavení klienta",
|
||||
"l1": "přihlaste se pro více:",
|
||||
"m1": "vítej zpět,", // TLNote: "welcome back, USERNAME"
|
||||
"ls3": "přihlásit se", //m
|
||||
"lu4": "uživatelské jméno", //m
|
||||
"lp4": "heslo", //m
|
||||
"lo3": "odhlásit “{0}” všude", //m
|
||||
"lo2": "tímto ukončíte relaci ve všech prohlížečích", //m
|
||||
"m1": "vítej zpět,",
|
||||
"n1": "404 nenalezeno ┐( ´ -`)┌",
|
||||
"o1": 'nebo možná nemáš přístup -- zkus heslo nebo <a href="' + SR + '/?h">jdi domů</a>',
|
||||
"p1": "403 zakázáno ~┻━┻",
|
||||
"q1": 'použij heslo nebo <a href="' + SR + '/?h">jdi domů</a>',
|
||||
"r1": "jdi domů",
|
||||
".s1": "znovu prohledat",
|
||||
"t1": "akce", // TLNote: this is the header above the "rescan" buttons
|
||||
"t1": "akce",
|
||||
"u2": "čas od posledního zápisu na server$N( upload / rename / ... )$N$N17d = 17 dní$N1h23 = 1 hodina 23 minut$N4m56 = 4 minuty 56 sekund",
|
||||
"v1": "připojit",
|
||||
"v2": "použít tento server jako místní HDD",
|
||||
"w1": "přepnout na https",
|
||||
"x1": "změnit heslo",
|
||||
"y1": "upravit sdílení", // TLNote: shows the list of folders that the user has decided to share
|
||||
"z1": "odblokovat toto sdílení:", // TLNote: the password prompt to see a hidden share
|
||||
"y1": "upravit sdílení",
|
||||
"z1": "odblokovat toto sdílení:",
|
||||
"ta1": "nejprve vyplňte své nové heslo",
|
||||
"ta2": "zopakujte pro potvrzení nového hesla:",
|
||||
"ta3": "nalezen překlep; zkuste to prosím znovu",
|
||||
|
@ -151,6 +167,11 @@ var Ls = {
|
|||
"j1": "k304 trennt die Clientverbindung bei jedem HTTP 304, was Bugs mit problematischen Proxies vorbeugen kann (z.B. nicht ladenden Seiten), macht Dinge aber generell langsamer",
|
||||
"k1": "Client-Einstellungen zurücksetzen",
|
||||
"l1": "Melde dich an für mehr:",
|
||||
"ls3": "Anmelden", //m
|
||||
"lu4": "Benutzername", //m
|
||||
"lp4": "Passwort", //m
|
||||
"lo3": "“{0}” überall abmelden", //m
|
||||
"lo2": "Dies beendet die Sitzung in allen Browsern", //m
|
||||
"m1": "Willkommen zurück,",
|
||||
"n1": "404 Nicht gefunden ┐( ´ -`)┌",
|
||||
"o1": 'or maybe you don\'t have access -- try a password or <a href="' + SR + '/?h">go home</a>',
|
||||
|
@ -192,6 +213,11 @@ var Ls = {
|
|||
"j1": "k304 katkaisee yhteytesi jokaisella HTTP 304:llä, mikä voi estää joitain bugisia välityspalvelimia jumittumasta/lopettamasta sivujen lataamista, <em>mutta</em> se myös vähentää suorituskykyä",
|
||||
"k1": "nollaa asetukset",
|
||||
"l1": "kirjaudu sisään:",
|
||||
"ls3": "kirjaudu sisään", //m
|
||||
"lu4": "käyttäjätunnus", //m
|
||||
"lp4": "salasana", //m
|
||||
"lo3": "kirjaa “{0}” ulos kaikkialta", //m
|
||||
"lo2": "tämä lopettaa istunnon kaikissa selaimissa", //m
|
||||
"m1": "tervetuloa takaisin,",
|
||||
"n1": "404: ei löytynyt mitään ┐( ´ -`)┌",
|
||||
"o1": 'tai ehkä sinulla ei vain ole käyttöoikeuksia? kokeile salasanaa tai <a href="' + SR + '/?h">mene kotiin</a>',
|
||||
|
@ -218,6 +244,52 @@ var Ls = {
|
|||
"af1": "näytä viimeaikaiset lataukset",
|
||||
"ag1": "näytä tunnetut IdP-käyttäjät",
|
||||
},
|
||||
"fra": {
|
||||
"a1": "rafraîchir",
|
||||
"b1": "salut étranger <small>(vous n'êtes pas connecté.)</small>",
|
||||
"c1": "déconnexion",
|
||||
"d1": "vidange de la pile",
|
||||
"d2": "affiche l'état de tous les threads actifs",
|
||||
"e1": "recharger la configuration",
|
||||
"e2": "recharger le fichier de configuration (comptes/volumes/indicateurs de volume),$Net rescanner tous les volumes e2ds$N$Nnote : n'importe quel changement aux paramètres globaux$Nnécessite un redémarrage complet pour prendre effet",
|
||||
"f1": "vous pouvez naviguer :",
|
||||
"g1": "vous pouvez télécharger sur :",
|
||||
"cc1": "autres choses :",
|
||||
"h1": "désactiver k304",
|
||||
"i1": "activer k304",
|
||||
"j1": "activer k304 va déconnecter votre client sur chaque HTTP 304, ce qui peut éviter à certains proxies défectueux de rester bloqués (les pages ne se chargent soudainement plus), <em>mais</em> cela ralentira également les choses en général",
|
||||
"k1": "réinitialiser les paramètres du client",
|
||||
"l1": "connectez-vous pour en savoir plus :",
|
||||
"ls3": "se connecter", //m
|
||||
"lu4": "nom d'utilisateur", //m
|
||||
"lp4": "mot de passe", //m
|
||||
"lo3": "déconnecter “{0}” partout", //m
|
||||
"lo2": "cela mettra fin à la session sur tous les navigateurs", //m
|
||||
"m1": "heureux de vous revoir,",
|
||||
"n1": "404 introuvable ┐( ´ -`)┌",
|
||||
"o1": 'ou peut-être que vous n\'y avez pas accès -- essayer un mot de passe ou <a href="' + SR + '/?h">aller à la page d\'accueil</a>',
|
||||
"p1": "403 interdit ~┻━┻",
|
||||
"q1": 'utiliser un mot de passe ou <a href="' + SR + '/?h">aller à la page d\'accueil</a>',
|
||||
"r1": "aller à la page d\'accueil",
|
||||
".s1": "rescanner",
|
||||
"t1": "action",
|
||||
"u2": "temps écoulé depuis la dernière écriture sur le serveur$N(téléchargement/renommage/...)$N$N17j = 17 jours$N1h23 = 1 heure 23 minutes$N4m56 = 4 minutes 56 secondes",
|
||||
"v1": "connecter",
|
||||
"v2": "utilisez ce serveur en tant que disque dur local",
|
||||
"w1": "passer à https",
|
||||
"x1": "changer mot de passe",
|
||||
"y1": "modifier les partages",
|
||||
"z1": "déverrouiller ce partage :",
|
||||
"ta1": "entrez d'abord votre nouveau mot de passe",
|
||||
"ta2": "répétez pour confirmer le nouveau mot de passe :",
|
||||
"ta3": "une faute de frappe a été détectée ; veuillez réessayer.",
|
||||
"aa1": "fichiers entrants :",
|
||||
"ab1": "désactiver no304",
|
||||
"ac1": "activer no304",
|
||||
"ad1": "l'activation de no304 désactivera toute mise en cache ; essayez ceci si k304 n'était pas suffisant. Cela va générer un trafic réseau considérable !",
|
||||
"ae1": "téléchargements actifs :",
|
||||
"af1": "afficher les derniers téléchargements",
|
||||
},
|
||||
"grc": {
|
||||
"a1": "ανανέωση",
|
||||
"b1": "γεια σου ξένε! <small>(δεν είσαι συνδεδεμένος)</small>",
|
||||
|
@ -234,6 +306,11 @@ var Ls = {
|
|||
"j1": "η ενεργοποίηση του k304 θα αποσυνδέσει το πρόγραμμα πελάτη σου σε κάθε HTTP 304, κάτι που μπορεί να αποτρέψει κάποια προβληματικά proxies από το να κολλάνε (να μην φορτώνουν ξαφνικά σελίδες), <em>αλλά</em> θα κάνει τα πράγματα, γενικά πιο αργά",
|
||||
"k1": "επαναφορά ρυθμίσεων στο πρόγραμμα πελάτη",
|
||||
"l1": "συνδέσου για περισσότερα:",
|
||||
"ls3": "σύνδεση", //m
|
||||
"lu4": "όνομα χρήστη", //m
|
||||
"lp4": "κωδικός πρόσβασης", //m
|
||||
"lo3": "αποσύνδεση του “{0}” από παντού", //m
|
||||
"lo2": "αυτό θα τερματίσει τη συνεδρία σε όλους τους περιηγητές", //m
|
||||
"m1": "καλώς ήρθες,",
|
||||
"n1": "404 δεν βρέθηκε ┐( ´ -`)┌",
|
||||
"o1": '´η μήπως δεν έχεις πρόσβαση -- δοκίμασε έναν κωδικό <a href="' + SR + '/?h">πήγαινε στην αρχική</a>',
|
||||
|
@ -275,6 +352,11 @@ var Ls = {
|
|||
"j1": "k304 interrompe la connessione per ogni HTTP 304. Questo aiuta contro alcuni proxy difettosi che possono bloccarsi o smettere improvvisamente di caricare pagine, ma riduce notevolmente le prestazioni",
|
||||
"k1": "resetta impostazioni",
|
||||
"l1": "accedi:",
|
||||
"ls3": "accedi", //m
|
||||
"lu4": "nome utente", //m
|
||||
"lp4": "password", //m
|
||||
"lo3": "disconnetti “{0}” ovunque", //m
|
||||
"lo2": "questo terminerà la sessione su tutti i browser", //m
|
||||
"m1": "bentornato,",
|
||||
"n1": "404: file non trovato ┐( ´ -`)┌",
|
||||
"o1": "oppure forse non hai accesso? prova una password o <a href=\"SR/?h\">torna alla home</a>",
|
||||
|
@ -301,6 +383,53 @@ var Ls = {
|
|||
"af1": "mostra i file caricati di recente",
|
||||
"ag1": "mostra utenti IdP conosciuti"
|
||||
},
|
||||
"kor": {
|
||||
"a1": "새로고침",
|
||||
"b1": "어이 친구! 처음 보는 얼굴인데? <small>(로그인되어 있지 않습니다)</small>",
|
||||
"c1": "로그아웃",
|
||||
"d1": "스택 덤프하기",
|
||||
"d2": "모든 활성 스레드의 상태를 표시합니다",
|
||||
"e1": "설정 다시 불러오기",
|
||||
"e2": "설정 파일(계정/볼륨/볼륨 플래그)을 다시 불러오고,$N모든 e2ds 볼륨을 다시 스캔합니다$N$N참고: 전역 설정에 대한 변경 사항은$N적용하려면 전체 재시작이 필요합니다",
|
||||
"f1": "탐색 가능한 곳:",
|
||||
"g1": "업로드 가능한 곳:",
|
||||
"cc1": "기타 항목:",
|
||||
"h1": "k304 비활성화",
|
||||
"i1": "k304 활성화",
|
||||
"j1": "k304를 활성화하면 모든 HTTP 304 응답 시 클라이언트 연결이 끊어집니다. 이는 일부 프록시가 멈추는 현상(갑자기 페이지가 로드되지 않음)을 방지할 수 있지만, <em>대신 전반적인 속도는 느려집니다.</em>",
|
||||
"k1": "클라이언트 설정 초기화",
|
||||
"l1": "로그인하기:",
|
||||
"ls3": "로그인", //m
|
||||
"lu4": "사용자 이름", //m
|
||||
"lp4": "비밀번호", //m
|
||||
"lo3": "{0}을(를) 모든 곳에서 로그아웃", //m
|
||||
"lo2": "이 작업은 모든 브라우저에서 세션을 종료합니다", //m
|
||||
"m1": "또 오셨네요,",
|
||||
"n1": "404 찾을 수 없음 ┐( ´ -`)┌",
|
||||
"o1": "또는 접근 권한이 없을 수 있습니다. 비밀번호를 입력하거나 <a href=\"' + SR + '/?h\">홈으로 이동</a>하세요",
|
||||
"p1": "403 접근 금지 ~┻━┻",
|
||||
"q1": "비밀번호를 입력하거나 <a href=\"' + SR + '/?h\">홈으로 이동</a>하세요",
|
||||
"r1": "홈으로 이동",
|
||||
".s1": "다시 스캔",
|
||||
"t1": "작업",
|
||||
"u2": "서버에 마지막으로 쓰기 작업을 한 후 경과된 시간$N(업로드 / 이름 변경 / 등등...)$N$N17d = 17일$N1h23 = 1시간 23분$N4m56 = 4분 56초",
|
||||
"v1": "연결",
|
||||
"v2": "이 서버를 로컬 하드디스크처럼 사용하기",
|
||||
"w1": "HTTPS로 전환",
|
||||
"x1": "비밀번호 변경",
|
||||
"y1": "공유 설정",
|
||||
"z1": "이 공유 잠금해제:",
|
||||
"ta1": "새 비밀번호를 먼저 입력하세요",
|
||||
"ta2": "새 비밀번호 확인을 위해 다시 입력하세요:",
|
||||
"ta3": "오타가 있습니다. 다시 시도해주세요",
|
||||
"aa1": "수신 중인 파일:",
|
||||
"ab1": "no304 비활성화",
|
||||
"ac1": "no304 활성화",
|
||||
"ad1": "no304를 활성화하면 모든 캐싱이 비활성화됩니다. k304로 충분하지 않은 경우 시도해보세요. 네트워크 트래픽이 대량으로 낭비됩니다!",
|
||||
"ae1": "활성 다운로드:",
|
||||
"af1": "최근 업로드 보기",
|
||||
"ag1": "IdP 캐시 보기"
|
||||
},
|
||||
"nld": {
|
||||
"a1": "Update",
|
||||
"b1": "Hallo, hoe gaat het met jou? <small>(Je bent niet ingelogd)</small>",
|
||||
|
@ -317,6 +446,11 @@ var Ls = {
|
|||
"j1": "k304 verbreekt de verbinding voor elke HTTP 304. Dit helpt tegen bepaalde proxy servers die kunnen vastlopen/plotseling stoppen met het laden van pagina's, maar het vermindert ook de prestaties aanzienlijk",
|
||||
"k1": "Instellingen resetten",
|
||||
"l1": "Inloggen:",
|
||||
"ls3": "inloggen", //m
|
||||
"lu4": "gebruikersnaam", //m
|
||||
"lp4": "wachtwoord", //m
|
||||
"lo3": "“{0}” overal afmelden", //m
|
||||
"lo2": "dit zal de sessie in alle browsers beëindigen", //m
|
||||
"m1": "Welkom terug,",
|
||||
"n1": "404: bestand bestaat niet ┐( ´ -`)┌",
|
||||
"o1": 'of misschien heb je geen toegang? probeer een wachtwoord of <a href="' + SR + '/?h">ga naar startscherm</a>',
|
||||
|
@ -343,6 +477,147 @@ var Ls = {
|
|||
"af1": "Recent geüploade bestanden weergeven",
|
||||
"ag1": "Bekende IdP-gebruikers weergeven",
|
||||
},
|
||||
"nno": {
|
||||
"a1": "oppdatér",
|
||||
"b1": "heisann <small>(du er ikkje logga inn)</small>",
|
||||
"c1": "logg ut",
|
||||
"d1": "tilstand",
|
||||
"d2": "vis tilstanden åt alle trådar",
|
||||
"e1": "last innst.",
|
||||
"e2": "les inn konfigurasjonsfiler på nytt$N(kontoer, volum, volumbrytarar)$Nog kartlegg alle e2ds-volum$N$Nmerk: endringer i globale parametrar$Nkrev ein full restart for å gjelde",
|
||||
"f1": "du kan sjå på:",
|
||||
"g1": "du kan laste opp åt:",
|
||||
"cc1": "brytarar og slikt:",
|
||||
"h1": "skru av k304",
|
||||
"i1": "skru på k304",
|
||||
"j1": "k304 bryt tilkoplinga for kvar HTTP 304. Dette hjelp mot visse mellomtjenarar som kan sette seg fast / plutselig sluttar å laste sider, men det sett óg ytinga ned betydelig",
|
||||
"k1": "nullstill innstillinger",
|
||||
"l1": "logg inn:",
|
||||
"ls3": "logg inn",
|
||||
"lu4": "brukarnamn",
|
||||
"lp4": "passord",
|
||||
"lo3": "logg ut “{0}” overalt",
|
||||
"lo2": "avslutt økta på alle nettlesarar",
|
||||
"m1": "velkomen attende,",
|
||||
"n1": "404: filen finnast ikkje ┐( ´ -`)┌",
|
||||
"o1": 'eller kanskje du ikkje har høve? prøv eit passord eller <a href="' + SR + '/?h">gå heim</a>',
|
||||
"p1": "403: tilgang nektet ~┻━┻",
|
||||
"q1": 'prøv eit passord eller <a href="' + SR + '/?h">gå heim</a>',
|
||||
"r1": "gå heim",
|
||||
".s1": "kartlegg",
|
||||
"t1": "handling",
|
||||
"u2": "tid sidan nokon sist skreiv åt serveren$N( opplastning / namnendring / ... )$N$N17d = 17 dagar$N1h23 = 1 time 23 minutt$N4m56 = 4 minutt 56 sekund",
|
||||
"v1": "kople åt",
|
||||
"v2": "bruk denne serveren som ein lokal harddisk",
|
||||
"w1": "bytt åt https",
|
||||
"x1": "bytt passord",
|
||||
"y1": "dine delinger",
|
||||
"z1": "lås opp område:",
|
||||
"ta1": "du må skrive eit nytt passord først",
|
||||
"ta2": "gjenta for å stadfeste nytt passord:",
|
||||
"ta3": "fant ein skrivefeil; vennligst prøv igjen",
|
||||
"aa1": "innkommande:",
|
||||
"ab1": "skru av no304",
|
||||
"ac1": "skru på no304",
|
||||
"ad1": "no304 stoppar all bruk av cache. Hvis ikkje k304 var nok, prøv denne. Vil mangedoble dataforbruk!",
|
||||
"ae1": "utgående:",
|
||||
"af1": "vis nylig opplasta filer",
|
||||
"ag1": "vis kjente IdP-brukarar",
|
||||
},
|
||||
"pol": {
|
||||
"a1": "odśwież",
|
||||
"b1": "witaj, nieznajomy <small>(nie jesteś zalogowany)</small>",
|
||||
"c1": "wyloguj się",
|
||||
"d1": "zrzut stosu",
|
||||
"d2": "pokazuje status wszystkich aktywnych wątków",
|
||||
"e1": "przeładuj konfigurację",
|
||||
"e2": "przeładuj pliki konfiguracyjne (konta/wolumeny/flagi wolumenów),$Ni przeskanuje wszystkie wolumeny e2ds$N$Nnotka: zmiany konfiguracji globalnej$Nwymagają pełnego uruchomienia ponownie serwera, aby zaczęły obowiązywać",
|
||||
"f1": "możesz przeglądać:",
|
||||
"g1": "możesz przesyłać do:",
|
||||
"cc1": "inne:",
|
||||
"h1": "wyłącz k304",
|
||||
"i1": "włącz k304",
|
||||
"j1": "włączenie k304 będzie odłączało klienta przy każdorazowym otrzymaniu kodu HTTP 304, co może zapobiec wieszaniu się wadliwych proxy, <em>ale</em> spowolni ogólne działanie",
|
||||
"k1": "zresetuj ustawienia klienta",
|
||||
"l1": "zaloguj się po więcej:",
|
||||
"ls3": "zaloguj się", //m
|
||||
"lu4": "nazwa użytkownika", //m
|
||||
"lp4": "hasło", //m
|
||||
"lo3": "wyloguj “{0}” wszędzie", //m
|
||||
"lo2": "spowoduje to zakończenie sesji we wszystkich przeglądarkach", //m
|
||||
"m1": "Witaj,",
|
||||
"n1": "404 nie znaleziono ┐( ´ -`)┌",
|
||||
"o1": 'lub możesz nie mieć dostępu -- spróbuj wprowadzić hasło lub <a href="' + SR + '/?h">przejdź do strony głównej</a>',
|
||||
"p1": "403 odmowa dostępu ~┻━┻",
|
||||
"q1": 'użyj hasła lub <a href="' + SR + '/?h">przejdź do strony głównej</a>',
|
||||
"r1": "idź do strony głównej",
|
||||
".s1": "przeskanuj ponownie",
|
||||
"t1": "akcje",
|
||||
"u2": "czas od ostatniej interakcji z serwerem$N( przesyłania / zmiany nazwy / ... )$N$N17d = 17 dni$N1h23 = 1 godzina 23 minuty$N4m56 = 4 minuty 56 sekund",
|
||||
"v1": "połącz",
|
||||
"v2": "używaj tego serwera jako dysku lokalnego",
|
||||
"w1": "przejdź na HTTPS",
|
||||
"x1": "zmień hasło",
|
||||
"y1": "edytuj udostępnione",
|
||||
"z1": "odblokuj udostępnienie:",
|
||||
"ta1": "najpierw wprowadź nowe hasło",
|
||||
"ta2": "powtórz hasło dla potwierdzenia:",
|
||||
"ta3": "znaleziono literówkę, spróbuj ponownie",
|
||||
"aa1": "pliki przychodzące:",
|
||||
"ab1": "wyłącz no304",
|
||||
"ac1": "włącz no304",
|
||||
"ad1": "włączenie no304 wyłączy przechowywanie jakiejkolwiek pamięci podręcznej. Zmarnuje to olbrzymią ilość ruchu sieciowego!",
|
||||
"ae1": "trwające pobierania:",
|
||||
"af1": "pokaż ostatnio przesłane pliki",
|
||||
"ag1": "pokaż znanych użytkowników IdP",
|
||||
},
|
||||
"por": {
|
||||
"a1": "atualizar",
|
||||
"b1": "olá <small>(você não está logado)</small>",
|
||||
"c1": "encerrar sessão",
|
||||
"d1": "despejar o estado da pilha",
|
||||
"d2": "mostra o estado de todos os threads ativos",
|
||||
"e1": "recarregar configuração",
|
||||
"e2": "recarregar arquivos de configuração (contas/volumes/indicadores de volume),$N e reescanear todos os volumes e2ds$N$Nnota: qualquer alteração na configuração global$N requer uma reinicialização completa para ter efeito",
|
||||
"f1": "você pode navegar:",
|
||||
"g1": "você pode fazer upload para:",
|
||||
"cc1": "outras coisas:",
|
||||
"h1": "desativar k304",
|
||||
"i1": "ativar k304",
|
||||
"j1": "ativar k304 irá desconectar seu cliente em cada HTTP 304, o que pode evitar que alguns proxies com erros fiquem presos (parando de carregar páginas de repente), <em>mas</em> também irá desacelerar as coisas em geral",
|
||||
"k1": "redefinir config. de cliente",
|
||||
"l1": "faça login para mais:",
|
||||
"ls3": "fazer login",
|
||||
"lu4": "nome de usuário",
|
||||
"lp4": "senha",
|
||||
"lo3": "encerrar sessão de \"{0}\" em todos os lugares",
|
||||
"lo2": "isso irá encerrar a sessão em todos os navegadores",
|
||||
"m1": "bem-vindo de volta,",
|
||||
"n1": "404 não encontrado ┐( ´ -`)┌",
|
||||
"o1": "ou talvez você não tenha acesso? -- tente com uma senha ou volte para o início",
|
||||
"p1": "403 proibido ~┻━┻",
|
||||
"q1": "use uma senha ou volte para o início",
|
||||
"r1": "ir para o início",
|
||||
".s1": "reescanear",
|
||||
"t1": "ação",
|
||||
"u2": "tempo desde a última gravação no servidor$N( upload / renomear / ... )$N$N17d = 17 dias$N1h23 = 1 hora 23 minutos$N4m56 = 4 minutos 56 segundos",
|
||||
"v1": "conectar",
|
||||
"v2": "usar este servidor como um disco rígido local",
|
||||
"w1": "mudar para https",
|
||||
"x1": "mudar senha",
|
||||
"y1": "editar recursos compartilhados",
|
||||
"z1": "desbloquear este recurso compartilhado:",
|
||||
"ta1": "primeiro digite sua nova senha",
|
||||
"ta2": "repita para confirmar a nova senha:",
|
||||
"ta3": "há um erro; por favor, tente novamente",
|
||||
"aa1": "arquivos de entrada:",
|
||||
"ab1": "desativar no304",
|
||||
"ac1": "ativar no304",
|
||||
"ad1": "ativar no304 irá desabilitar todo o armazenamento em cache; tente isso se k304 não for suficiente. Isso irá desperdiçar uma grande quantidade de tráfego de rede!",
|
||||
"ae1": "downloads ativos:",
|
||||
"af1": "mostrar uploads recentes",
|
||||
"ag1": "mostrar usuários IdP conhecidos"
|
||||
},
|
||||
"spa": {
|
||||
"a1": "actualizar",
|
||||
"b1": "hola <small>(no has iniciado sesión)</small>",
|
||||
|
@ -359,6 +634,11 @@ var Ls = {
|
|||
"j1": "activar k304 desconectará tu cliente en cada HTTP 304, lo que puede evitar que algunos proxies con errores se atasquen (dejando de cargar páginas de repente), <em>pero</em> también ralentizará las cosas en general",
|
||||
"k1": "restablecer config. de cliente",
|
||||
"l1": "inicia sesión para más:",
|
||||
"ls3": "iniciar sesión", //m
|
||||
"lu4": "nombre de usuario", //m
|
||||
"lp4": "contraseña", //m
|
||||
"lo3": "cerrar sesión de “{0}” en todas partes", //m
|
||||
"lo2": "esto finalizará la sesión en todos los navegadores", //m
|
||||
"m1": "bienvenido de nuevo,",
|
||||
"n1": "404 no encontrado ┐( ´ -`)┌",
|
||||
"o1": '¿o quizás no tienes acceso? -- prueba con una contraseña o <a href=\"' + SR + '/?h\">vuelve al inicio</a>',
|
||||
|
@ -385,6 +665,53 @@ var Ls = {
|
|||
"af1": "mostrar subidas recientes",
|
||||
"ag1": "mostrar usuarios IdP conocidos"
|
||||
},
|
||||
"swe": {
|
||||
"a1": "uppdatera",
|
||||
"b1": "tjena främling <small>(du är inte inloggad)</small>",
|
||||
"c1": "logga ut",
|
||||
"d1": "dumpa stacken",
|
||||
"d2": "visar tillståndet på alla aktiva trådar",
|
||||
"e1": "ladda om konfig.",
|
||||
"e2": "ladda om konfigurationsfiler (konton/volymer/volflaggor),$Noch skanna om alla e2ds-volymer$N$Nobs.: ändrade globala inställningar$Nkräver en fullständig omstart",
|
||||
"f1": "du kan bläddra:",
|
||||
"g1": "du kan ladda upp till:",
|
||||
"cc1": "annat:",
|
||||
"h1": "avaktivera k304",
|
||||
"i1": "aktivera k304",
|
||||
"j1": "med k304 aktiverad kommer klienten att koppla bort sig vid varje HTTP 304-fel, vilket kan hindra vissa buggiga proxyservrar från att fastna (sidor slutar ladda), <em>men</em> saker kommer också att bli långsammare i allmänhet",
|
||||
"k1": "återställ klientinställningar",
|
||||
"l1": "logga in för att se mer:",
|
||||
"ls3": "logga in", //m
|
||||
"lu4": "användarnamn", //m
|
||||
"lp4": "lösenord", //m
|
||||
"lo3": "logga ut “{0}” överallt", //m
|
||||
"lo2": "avsluta sessionen i alla webbläsare", //m
|
||||
"m1": "välkommen tillbaka,",
|
||||
"n1": "404 hittades inte ┐( ´ -`)┌",
|
||||
"o1": 'eller så har du kanske inte tillgång -- prova ett lösenord eller <a href="' + SR + '/?h">åk hem</a>',
|
||||
"p1": "403 nekat ~┻━┻",
|
||||
"q1": 'använd ett lösenord eller <a href="' + SR + '/?h">åk hem</a>',
|
||||
"r1": "åk hem",
|
||||
".s1": "skanna om",
|
||||
"t1": "åtgärd",
|
||||
"u2": "tid sedan senaste serverskrivning$N( uppladdning / namnbyte / ... )$N$N17d = 17 dagar$N1h23 = 1 timme 23 minuter$N4m56 = 4 minuter 56 sekunder",
|
||||
"v1": "koppla upp",
|
||||
"v2": "använd denna server som en lokal disk",
|
||||
"w1": "byt till https",
|
||||
"x1": "byt lösenord",
|
||||
"y1": "redigera utdelningar",
|
||||
"z1": "lås upp denna utdelning:",
|
||||
"ta1": "fyll i ditt nya lösenord",
|
||||
"ta2": "upprepa det nya lösenordet:",
|
||||
"ta3": "det blev fel; vänligen försök igen",
|
||||
"aa1": "inkommande filer:",
|
||||
"ab1": "avaktivera no304",
|
||||
"ac1": "aktivera no304",
|
||||
"ad1": "detta stänger av all cachning; prova detta om k304 inte räckte till. Detta kommer att slösa enorma mängder nätverkstrafik!",
|
||||
"ae1": "aktiva nedladdningar:",
|
||||
"af1": "visa senaste uppladdningar",
|
||||
"ag1": "visa idp-cache"
|
||||
},
|
||||
"ukr": {
|
||||
"a1": "оновити",
|
||||
"b1": "привітик, незнайомцю <small>(ви не авторизовані)</small>",
|
||||
|
@ -401,6 +728,11 @@ var Ls = {
|
|||
"j1": "увімкнення k304 буде відключати ваш клієнт при кожному HTTP 304, що може запобігти зависанню деяких глючних проксі (раптово перестають завантажувати сторінки), <em>але</em> це також зробить усе повільнішим загалом",
|
||||
"k1": "скинути налаштування клієнта",
|
||||
"l1": "авторизуйтесь для інших опцій:",
|
||||
"ls3": "увійти", //m
|
||||
"lu4": "ім'я користувача", //m
|
||||
"lp4": "пароль", //m
|
||||
"lo3": "вийти з облікового запису “{0}” всюди", //m
|
||||
"lo2": "це завершить сеанс у всіх браузерах", //m
|
||||
"m1": "з поверненням,",
|
||||
"n1": "404 не знайдено ┐( ´ -`)┌",
|
||||
"o1": 'або у вас немає доступу -- спробуйте авторизуватися або <a href="' + SR + '/?h">повернутися на головну</a>',
|
||||
|
@ -443,6 +775,11 @@ var Ls = {
|
|||
"j1": "включённый k304 будет отключать вас при получении HTTP 304, что может помочь при работе с некоторыми глючными прокси (перестают загружаться страницы), <em>но</em> это также сделает работу клиента медленнее",
|
||||
"k1": "сбросить локальные настройки",
|
||||
"l1": "авторизуйтесь для других опций:",
|
||||
"ls3": "войти", //m
|
||||
"lu4": "имя пользователя", //m
|
||||
"lp4": "пароль", //m
|
||||
"lo3": "выйти из “{0}” везде", //m
|
||||
"lo2": "это завершит сеанс во всех браузерах", //m
|
||||
"m1": "с возвращением,",
|
||||
"n1": "404 не найдено ┐( ´ -`)┌",
|
||||
"o1": 'или у вас нет доступа -- попробуйте авторизоваться или <a href="' + SR + '/?h">вернуться на главную</a>',
|
||||
|
@ -518,6 +855,8 @@ if (window.langmod)
|
|||
var d = Ls[sread("cpp_lang", Object.keys(Ls)) || lang] ||
|
||||
Ls.eng || Ls.nor || Ls.chi;
|
||||
|
||||
d.wb = d.w;
|
||||
|
||||
for (var k in (d || {})) {
|
||||
var f = k.slice(-1),
|
||||
i = k.slice(0, -1),
|
||||
|
@ -528,10 +867,17 @@ for (var k in (d || {})) {
|
|||
o[a].innerHTML = d[k];
|
||||
else if (f == 2)
|
||||
o[a].setAttribute("tt", d[k]);
|
||||
else if (f == 3)
|
||||
o[a].setAttribute("value", d[k]);
|
||||
else if (f == 4)
|
||||
o[a].setAttribute("placeholder", " " + d[k]);
|
||||
}
|
||||
var o1 = ebi('lo'), o2 = ebi('un');
|
||||
if (o1 && o2 && d.lo3)
|
||||
o1.setAttribute("value", d.lo3.format(o2.textContent));
|
||||
|
||||
try {
|
||||
if (is_idp) {
|
||||
if (is_idp > 1) {
|
||||
var z = ['#l+div', '#l', '#c'];
|
||||
for (var a = 0; a < z.length; a++)
|
||||
QS(z[a]).style.display = 'none';
|
||||
|
@ -541,14 +887,16 @@ catch (ex) { }
|
|||
|
||||
tt.init();
|
||||
var o = QS('input[name="uname"]') || QS('input[name="cppwd"]');
|
||||
if (!ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight)
|
||||
if (o && !MOBILE && !ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight)
|
||||
o.focus();
|
||||
|
||||
o = ebi('u');
|
||||
if (o && /[0-9]+$/.exec(o.innerHTML))
|
||||
o.innerHTML = shumantime(o.innerHTML);
|
||||
|
||||
ebi('uhash').value = '' + location.hash;
|
||||
o = ebi('uhash')
|
||||
if (o)
|
||||
o.value = '' + location.hash;
|
||||
|
||||
if (/\&re=/.test('' + location))
|
||||
ebi('a').className = 'af g';
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/splash.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
<style>ul{padding-left:1.3em}li{margin:.4em 0}.txa{float:right;margin:0 0 0 1em}</style>
|
||||
{{ html_head }}
|
||||
{{- html_head }}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -31,10 +31,10 @@
|
|||
<br />
|
||||
<span class="os win lin mac">placeholders:</span>
|
||||
<span class="os win">
|
||||
{% if accs %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>W:</b></code>=mountpoint
|
||||
{% if accs %}{% if un %}<code><b id="un0">{{ un }}</b></code>=username, <code><b id="up0">{{ unpw }}</b></code>=username:password, {% endif %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>W:</b></code>=mountpoint
|
||||
</span>
|
||||
<span class="os lin mac">
|
||||
{% if accs %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint
|
||||
{% if accs %}{% if un %}<code><b id="un0">{{ un }}</b></code>=username, <code><b id="up0">{{ unpw }}</b></code>=username:password, {% endif %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint
|
||||
</span>
|
||||
{% if accs %}<a href="#" id="setpw">use real password</a>{% endif %}
|
||||
<a href="#" id="qr">show qr</a>
|
||||
|
@ -42,9 +42,9 @@
|
|||
|
||||
|
||||
|
||||
{% if args.idp_h_usr %}
|
||||
{% if args.have_idp_hdrs %}
|
||||
<p style="line-height:2em"><b>WARNING:</b> this server is using IdP-based authentication, so this stuff may not work as advertised. Depending on server config, these commands can probably only be used to access areas which don't require authentication, unless you auth using any non-IdP accounts defined in the copyparty config. Please see <a href="https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients">the IdP docs</a></p>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
|
||||
|
||||
|
||||
|
@ -54,50 +54,64 @@
|
|||
<div class="os win">
|
||||
<p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p>
|
||||
<pre>
|
||||
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
|
||||
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>W:</b>
|
||||
</pre>
|
||||
<ul>
|
||||
{% if s %}
|
||||
{%- if s %}
|
||||
<li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li>
|
||||
</ul>
|
||||
|
||||
<p>if you want to use the native WebDAV client in windows instead (slow and buggy), first run <a href="{{ r }}/.cpr/a/webdav-cfg.bat">webdav-cfg.bat</a> to remove the 47 MiB filesize limit (also fixes latency and password login), then connect:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
net use <b>w:</b> http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} <b>{{ pw }}</b> /user:{{ b_un }}{% endif %}
|
||||
{%- else %}
|
||||
net use <b>w:</b> http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} k /user:<b>{{ pw }}</b>{% endif %}
|
||||
{%- endif %}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div class="os lin">
|
||||
<p>rclone (v1.63 or later) is recommended:</p>
|
||||
<pre>
|
||||
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
|
||||
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>mp</b>
|
||||
</pre>
|
||||
<ul>
|
||||
{% if s %}
|
||||
{%- if s %}
|
||||
<li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
|
||||
<li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li>
|
||||
</ul>
|
||||
<p>alternatively use davfs2 (requires root, is slower, forgets lastmodified-timestamp on upload):</p>
|
||||
<pre>
|
||||
yum install davfs2
|
||||
{%- if un %}
|
||||
{% if accs %}printf '%s\n' {{ b_un }} <b>{{ pw }}</b> | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b>
|
||||
{%- else %}
|
||||
{% if accs %}printf '%s\n' <b>{{ pw }}</b> k | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b>
|
||||
{%- endif %}
|
||||
</pre>
|
||||
{%- if accs %}
|
||||
<p>make davfs2 automount on boot:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} {{ b_un }} <b>{{ pw }}</b>" >> /etc/davfs2/secrets
|
||||
{%- else %}
|
||||
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>{{ pw }}</b> k" >> /etc/davfs2/secrets
|
||||
{%- endif %}
|
||||
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b> davfs rw,user,uid=1000,noauto 0 0" >> /etc/fstab
|
||||
</pre>
|
||||
{%- endif %}
|
||||
<p>or the emergency alternative (gnome/gui-only):</p>
|
||||
<!-- gnome-bug: ignores vp -->
|
||||
<pre>
|
||||
{%- if accs %}
|
||||
echo <b>{{ pw }}</b> | gio mount dav{{ s }}://k@{{ ep }}/{{ rvp }}
|
||||
echo <b>{{ pw }}</b> | gio mount dav{{ s }}://{{ b_un }}@{{ ep }}/{{ rvp }}
|
||||
{%- else %}
|
||||
gio mount -a dav{{ s }}://{{ ep }}/{{ rvp }}
|
||||
{%- endif %}
|
||||
|
@ -107,18 +121,18 @@
|
|||
|
||||
<div class="os mac">
|
||||
<pre>
|
||||
osascript -e ' mount volume "http{{ s }}://k:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}" '
|
||||
osascript -e ' mount volume "http{{ s }}://{{ b_un }}:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}" '
|
||||
</pre>
|
||||
<p>or you can open up a Finder, press command-K and paste this instead:</p>
|
||||
<pre>
|
||||
http{{ s }}://k:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}
|
||||
http{{ s }}://{{ b_un }}:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}
|
||||
</pre>
|
||||
|
||||
{% if s %}
|
||||
{%- if s %}
|
||||
<p><em>replace <code>https</code> with <code>http</code> if it doesn't work</em></p>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
|
||||
|
||||
|
||||
|
@ -127,51 +141,71 @@
|
|||
|
||||
<div class="os win">
|
||||
<p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p>
|
||||
{% if args.ftp %}
|
||||
{%- if args.ftp %}
|
||||
<p>connect with plaintext FTP:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
|
||||
{%- else %}
|
||||
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
|
||||
{%- endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>W:</b>
|
||||
</pre>
|
||||
{% endif %}
|
||||
{% if args.ftps %}
|
||||
{%- endif %}
|
||||
{%- if args.ftps %}
|
||||
<p>connect with TLS-encrypted FTPS:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
|
||||
{%- else %}
|
||||
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
|
||||
{%- endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>W:</b>
|
||||
</pre>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<ul>
|
||||
{% if args.ftps %}
|
||||
{%- if args.ftps %}
|
||||
<li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li>
|
||||
</ul>
|
||||
<p>if you want to use the native FTP client in windows instead (please dont), press <code>win+R</code> and run this command:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}{{ b_un }}:<b>{{ pw }}</b>@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
|
||||
{%- else %}
|
||||
explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}<b>{{ pw }}</b>:k@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
|
||||
{%- endif %}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div class="os lin">
|
||||
{% if args.ftp %}
|
||||
{%- if args.ftp %}
|
||||
<p>connect with plaintext FTP:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
|
||||
{%- else %}
|
||||
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
|
||||
{%- endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>mp</b>
|
||||
</pre>
|
||||
{% endif %}
|
||||
{% if args.ftps %}
|
||||
{%- endif %}
|
||||
{%- if args.ftps %}
|
||||
<p>connect with TLS-encrypted FTPS:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
|
||||
{%- else %}
|
||||
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
|
||||
{%- endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>mp</b>
|
||||
</pre>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<ul>
|
||||
{% if args.ftps %}
|
||||
{%- if args.ftps %}
|
||||
<li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
|
||||
<li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li>
|
||||
</ul>
|
||||
|
@ -179,7 +213,7 @@
|
|||
<!-- gnome-bug: ignores vp -->
|
||||
<pre>
|
||||
{%- if accs %}
|
||||
echo <b>{{ pw }}</b> | gio mount ftp{{ "" if args.ftp else "s" }}://k@{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
|
||||
echo <b>{{ pw }}</b> | gio mount ftp{{ "" if args.ftp else "s" }}://{{ b_un }}@{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
|
||||
{%- else %}
|
||||
gio mount -a ftp{{ "" if args.ftp else "s" }}://{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
|
||||
{%- endif %}
|
||||
|
@ -189,10 +223,10 @@
|
|||
<div class="os mac">
|
||||
<p>note: FTP is read-only on macos; please use WebDAV instead</p>
|
||||
<pre>
|
||||
open {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}k:<b>{{ pw }}</b>@{% else %}anonymous:@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
|
||||
open {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}{{ b_un }}:<b>{{ pw }}</b>@{% else %}anonymous:@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
|
||||
</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
|
||||
|
||||
|
||||
|
@ -204,11 +238,11 @@
|
|||
<span class="os lin">doesn't need root</span>
|
||||
</p>
|
||||
<pre>
|
||||
partyfuse.py{% if accs %} -a <b>{{ pw }}</b>{% endif %} http{{ s }}://{{ ep }}/{{ rvp }} <b><span class="os win">W:</span><span class="os lin mac">mp</span></b>
|
||||
partyfuse.py{% if accs %} -a <b>{{ unpw }}</b>{% endif %} http{{ s }}://{{ ep }}/{{ rvp }} <b><span class="os win">W:</span><span class="os lin mac">mp</span></b>
|
||||
</pre>
|
||||
{% if s %}
|
||||
{%- if s %}
|
||||
<ul><li>if you are on LAN (or just dont have valid certificates), add <code>-td</code></li></ul>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<p>
|
||||
you can use <a href="{{ r }}/.cpr/a/u2c.py">u2c.py</a> to upload (sometimes faster than web-browsers)
|
||||
</p>
|
||||
|
@ -217,6 +251,10 @@
|
|||
{% if args.smb %}
|
||||
<h1>SMB / CIFS</h1>
|
||||
|
||||
{%- if un %}
|
||||
<h2>not available on this server because <code>--usernames</code> is enabled in the server config</h2>
|
||||
{%- else %}
|
||||
|
||||
<div class="os win">
|
||||
<pre>
|
||||
net use <b>w:</b> \\{{ host }}\a{% if accs %} k /user:<b>{{ pw }}</b>{% endif %}
|
||||
|
@ -234,7 +272,8 @@
|
|||
<pre class="os mac">
|
||||
open 'smb://<b>{{ pw }}</b>:k@{{ host }}/a'
|
||||
</pre>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
|
||||
|
||||
|
||||
|
@ -247,7 +286,7 @@
|
|||
{ "Version": "15.0.0", "Name": "copyparty",
|
||||
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
|
||||
"Headers": {
|
||||
{% if accs %}"pw": "<b>{{ pw }}</b>", {% endif %}"accept": "url"
|
||||
{% if accs %}"pw": "<b>{{ unpw }}</b>", {% endif %}"accept": "url"
|
||||
},
|
||||
"DestinationType": "ImageUploader, TextUploader, FileUploader",
|
||||
"Body": "MultipartFormData", "URL": "{response}",
|
||||
|
@ -260,7 +299,7 @@
|
|||
{ "Name": "copyparty",
|
||||
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
|
||||
"Headers": {
|
||||
{% if accs %}"pw": "<b>{{ pw }}</b>", {% endif %}"accept": "url"
|
||||
{% if accs %}"pw": "<b>{{ unpw }}</b>", {% endif %}"accept": "url"
|
||||
},
|
||||
"DestinationType": "ImageUploader, TextUploader, FileUploader",
|
||||
"FileFormName": "f" }
|
||||
|
@ -278,7 +317,9 @@
|
|||
{ "Name": "copyparty",
|
||||
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
|
||||
"Headers": {
|
||||
{% if accs %}"pw": "<b>{{ pw }}</b>",{% endif %}
|
||||
{%- if accs %}
|
||||
"pw": "<b>{{ unpw }}</b>",
|
||||
{%- endif %}
|
||||
"accept": "json"
|
||||
},
|
||||
"ResponseURL": "{{ '{{fileurl}}' }}",
|
||||
|
@ -295,7 +336,7 @@
|
|||
|
||||
<pre class="dl" name="flameshot.sh">
|
||||
#!/bin/bash
|
||||
pw="<b>{{ pw }}</b>"
|
||||
pw="<b>{{ unpw }}</b>"
|
||||
url="http{{ s }}://{{ ep }}/{{ rvp }}"
|
||||
filename="$(date +%Y-%m%d-%H%M%S).png"
|
||||
flameshot gui -s -r | curl -sT- "$url$filename?want=url&pw=$pw" | xsel -ib
|
||||
|
|
|
@ -49,21 +49,47 @@ function setos(os) {
|
|||
setos(WINDOWS ? 'win' : LINUX ? 'lin' : MACOS ? 'mac' : 'idk');
|
||||
|
||||
|
||||
var pw = '';
|
||||
var un, un0, pw, pw0, unpw, up0;
|
||||
function setpw(e) {
|
||||
ev(e);
|
||||
if (!ebi('un0'))
|
||||
return askpw();
|
||||
|
||||
modal.prompt('username:', '', function (v) {
|
||||
if (!v)
|
||||
return;
|
||||
|
||||
un = v;
|
||||
un0 = ebi('un0').innerHTML;
|
||||
var oa = QSA('b');
|
||||
|
||||
for (var a = 0; a < oa.length; a++)
|
||||
if (oa[a].innerHTML == un0)
|
||||
oa[a].textContent = un;
|
||||
|
||||
askpw();
|
||||
});
|
||||
}
|
||||
function askpw() {
|
||||
modal.prompt('password:', '', function (v) {
|
||||
if (!v)
|
||||
return;
|
||||
|
||||
pw = v;
|
||||
var pw0 = ebi('pw0').innerHTML,
|
||||
oa = QSA('b');
|
||||
pw0 = ebi('pw0').innerHTML;
|
||||
var oa = QSA('b');
|
||||
|
||||
for (var a = 0; a < oa.length; a++)
|
||||
if (oa[a].innerHTML == pw0)
|
||||
oa[a].textContent = v;
|
||||
oa[a].textContent = pw;
|
||||
|
||||
if (un) {
|
||||
unpw = un ? (un+':'+pw) : pw;
|
||||
up0 = ebi('up0').innerHTML;
|
||||
for (var a = 0; a < oa.length; a++)
|
||||
if (oa[a].innerHTML == up0)
|
||||
oa[a].textContent = unpw;
|
||||
}
|
||||
add_dls();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -430,6 +430,15 @@ html.y textarea:focus {
|
|||
.mdo code {
|
||||
font-size: .96em;
|
||||
}
|
||||
html.z .mdo a>code,
|
||||
html.y .mdo a>code {
|
||||
color: inherit;
|
||||
background: inherit;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
}
|
||||
.mdo h1,
|
||||
.mdo h2 {
|
||||
line-height: 1.5em;
|
||||
|
|
|
@ -50,7 +50,7 @@ catch (ex) {
|
|||
}
|
||||
catch (ex) {
|
||||
console.log('up2k init failed:', ex);
|
||||
toast.err(10, 'could not initialze up2k\n\n' + basenames(ex));
|
||||
toast.err(10, 'could not initialize up2k\n\n' + basenames(ex));
|
||||
}
|
||||
}
|
||||
treectl.onscroll();
|
||||
|
@ -732,7 +732,7 @@ function Donut(uc, st) {
|
|||
tstrober = setInterval(strobe, 300);
|
||||
|
||||
if (uc.upsfx && actx && actx.state != 'suspended')
|
||||
sfx();
|
||||
sfx_nice();
|
||||
|
||||
// firefox may forget that filedrops are user-gestures so it can skip this:
|
||||
if (uc.upnag && Notification && Notification.permission == 'granted')
|
||||
|
@ -745,8 +745,10 @@ function Donut(uc, st) {
|
|||
if (!txt)
|
||||
clearInterval(tstrober);
|
||||
}
|
||||
}
|
||||
|
||||
function sfx() {
|
||||
function sfx_nice() {
|
||||
if (true) {
|
||||
var osc = actx.createOscillator(),
|
||||
gain = actx.createGain(),
|
||||
gg = gain.gain,
|
||||
|
@ -2831,7 +2833,7 @@ function up2k_init(subtle) {
|
|||
if (!t.t_uploading)
|
||||
t.t_uploading = Date.now();
|
||||
|
||||
pvis.seth(t.n, 1, "🚀 send");
|
||||
pvis.seth(t.n, 1, "🚀 " + L.ul_send);
|
||||
|
||||
var chunksize = get_chunksize(t.size),
|
||||
car = pcar * chunksize,
|
||||
|
@ -3037,10 +3039,12 @@ function up2k_init(subtle) {
|
|||
if (anymod(e))
|
||||
return;
|
||||
|
||||
if (e.code == 'ArrowUp')
|
||||
var k = e.key || e.code;
|
||||
|
||||
if (k == 'ArrowUp')
|
||||
bumpthread(1);
|
||||
|
||||
if (e.code == 'ArrowDown')
|
||||
if (k == 'ArrowDown')
|
||||
bumpthread(-1);
|
||||
}
|
||||
|
||||
|
@ -3103,7 +3107,8 @@ function up2k_init(subtle) {
|
|||
ebi('u2szg').addEventListener('blur', read_u2sz);
|
||||
ebi('u2szg').onkeydown = function (e) {
|
||||
if (anymod(e)) return;
|
||||
var n = e.code == 'ArrowUp' ? 1 : e.code == 'ArrowDown' ? -1 : 0;
|
||||
var k = e.key || e.code,
|
||||
n = k == 'ArrowUp' ? 1 : k == 'ArrowDown' ? -1 : 0;
|
||||
if (!n) return;
|
||||
this.value = parseInt(this.value) + n;
|
||||
read_u2sz();
|
||||
|
@ -3180,7 +3185,8 @@ function up2k_init(subtle) {
|
|||
|
||||
function kd_life(e) {
|
||||
var el = e.target,
|
||||
d = e.code == 'ArrowUp' ? 1 : e.code == 'ArrowDown' ? -1 : 0;
|
||||
k = e.key || e.code,
|
||||
d = k == 'ArrowUp' ? 1 : k == 'ArrowDown' ? -1 : 0;
|
||||
|
||||
if (anymod(e) || !d)
|
||||
return;
|
||||
|
@ -3421,6 +3427,7 @@ if (QS('#op_up2k.act'))
|
|||
goto_up2k();
|
||||
|
||||
apply_perms({ "perms": perms, "frand": frand, "u2ts": u2ts });
|
||||
fileman.render();
|
||||
|
||||
|
||||
(function () {
|
||||
|
|
|
@ -1263,10 +1263,13 @@ function sethash(hv) {
|
|||
|
||||
function dl_file(url) {
|
||||
console.log('DL [%s]', url);
|
||||
var o = mknod('a');
|
||||
qsr('#dlfth');
|
||||
var o = mknod('a', 'dlfth');
|
||||
o.setAttribute('href', url);
|
||||
o.setAttribute('download', '');
|
||||
o.click();
|
||||
document.body.appendChild(o);
|
||||
ebi('dlfth').click();
|
||||
qsr('#dlfth');
|
||||
}
|
||||
|
||||
|
||||
|
@ -1821,12 +1824,12 @@ var modal = (function () {
|
|||
};
|
||||
|
||||
var onkey = function (e) {
|
||||
var k = (e.code || e.key) + '',
|
||||
var k = (e.key || e.code) + '',
|
||||
eok = ebi('modal-ok'),
|
||||
eng = ebi('modal-ng'),
|
||||
ae = document.activeElement;
|
||||
|
||||
if (k == 'Space' && ae && (ae === eok || ae === eng))
|
||||
if ((k == 'Space' || k == 'Spacebar' || k == ' ') && ae && (ae === eok || ae === eng))
|
||||
k = 'Enter';
|
||||
|
||||
if (k.endsWith('Enter')) {
|
||||
|
|
|
@ -1,3 +1,209 @@
|
|||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0828-2014 `v1.19.7` chdir
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* new option `chdir` to change the PWD (process working-directory) before volumes are mapped 14555d58
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* fix using empty folders as statefile storage ([v1.19.6](https://github.com/9001/copyparty/releases/tag/v1.19.6) made this a bit too strict) 0d96786e
|
||||
* holding I/K to scroll through folders quickly now works better 914686ec
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* #717 docker: fix the image repo metadata (thx @EmilyxFox!) 6f087117
|
||||
* docker: change `$HOME` to `/state` 01cf20a0 d1f75229
|
||||
* and use the new `chdir` option to preserve old config-file semantics 14555d58
|
||||
* helps avoid statefiles accidentally landing in `/w` as a consequence of misconfiguration
|
||||
|
||||
## 🌠 fun facts
|
||||
|
||||
* this release was made at [RevSpace NL](https://a.ocv.me/pub/g/nerd-stuff/PXL_20250828_202820075.jpg?cache)
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0827-2038 `v1.19.6` auth-precedence
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #673 add Portuguese translation (thx anonymous!) 4b8c2215
|
||||
* ...and enable the Polish translation (whoops) 8f235be6
|
||||
* #689 add option to control authentication priority/precedence 543b7ea9
|
||||
* url-parameter `?dl` forces file download instead of displaying in-browser 48d6224e
|
||||
* #533 more ways to make the QR-code always-visible in the console 2848941e
|
||||
* #695 option to log invalid xml from clients 28b93d79
|
||||
* #552 configurable markdown newline behavior 0491123b
|
||||
* and tweak the styling of monospace in links 68503444
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* #628 FTP-server now accepts connections from IPv6 link-local addresses 978801d0
|
||||
* incorrect assumption that all IPv6 link-local addresses start with `fe80` d39c74c1
|
||||
* ftp: fix file rename d40f061a
|
||||
* u2c: couldn't upload files located at the very top of the unix file hierarchy 599e82f2
|
||||
* #699 markdown-editor: fix panic if the table-formatter is executed on something that isn't a table 4c042b3c
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* #696 a volume can be one single file, not just folders aa1c9213
|
||||
* #442 strongly prefer XDG_CONFIG_HOME as config location 35472557
|
||||
* #691 album-art collected from audio-files can now become folder thumbnails 0b50fde3
|
||||
* allow spaces in more of the comma-separated options d30240b4
|
||||
* docs:
|
||||
* mention config requirements for [syncing folders](https://github.com/9001/copyparty/#folder-sync) with u2c 6cd0a396 59f142cd
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0821-2319 `v1.19.5` it runs on iOS
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #328 run copyparty on iPhones; see [install on iOS](https://github.com/9001/copyparty#install-on-iOS) in the readme ca98d54f
|
||||
* cannot run in the background, doesn't have full access to your files, and is slightly buggy, but it *works*
|
||||
* [running on android](https://github.com/9001/copyparty#install-on-android) gives you a much better experience
|
||||
* save the qr-code to a file (txt/svg/png) 202ddeac
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* #661 fix incorrect `rproxy` hint in the logs 6c76614e
|
||||
* #649 fix js-crash when tapping in the exactly correct place (thx @hahaslav for debugging!) 0de07d8e
|
||||
* #628 ftpd: fix banning IPv6 clients 6d76254c
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* #296 nixos: support non-flake setups (thx @Sorixelle!) 20ef74cd 32593670
|
||||
* config-parser catches and explains a few more common mistakes cc65b1b5
|
||||
* docs:
|
||||
* #490, #199: readme: confirm that combining copyparty and syncthing is safe c51371c7
|
||||
* #377 improved authelia docker example (thx @xFuture603!) cd8771fa
|
||||
* mention the homebrew formulae f9cb2c15
|
||||
* #651 versus.md: fix hfs3 comparison (thx @rejetto!) 7a4973fa
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0817-1556 `v1.19.4` take two (fix cfg vols)
|
||||
|
||||
## ℹ️ this upgrade is a one-way ticket
|
||||
|
||||
* your up2k database (`.hist/up2k.db`), used by the `e2d` filesystem indexing feature, **will be upgraded to a new format** which older copyparty versions cannot read. A backup of each database will be created automatically, named `up2k.db.bak.SOMETHING.v5`. If you need to downgrade to a previous version: Shutdown copyparty, delete these files: `up2k.db up2k.db-shm up2k.db-wal` and then copy `up2k.db.bak.*.v5` to `up2k.db`
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* new translations:
|
||||
* #551 Swedish (thx @Bevinsky!) d676a86f
|
||||
* #551 Korean (thx @nyqui!) 4e878d2f
|
||||
* #581 new theme: phi95 (thx @varphi-online!) d8662aeb
|
||||
* #567 .raw image thumbnails (thx @ar-nelson!) 0177a9b4
|
||||
* available in docker-images `iv` and `dj`
|
||||
* #561 epub thumbnails (thx @Scotsguy!) 9435e6b2
|
||||
* #252 music thumbnails use embdded coverart if available 98d117b8
|
||||
* thumbnails folder `.hist/th` must be deleted to take effect
|
||||
* #530 show username of uploaders in file listings; requires `a` (admin) permission 4df033ec
|
||||
* #604 a new group `@acct` which automatically contains all known usernames 68907eaf
|
||||
* controlpanel has a dedicated "logout all sessions" button, similar to the logout-link in the browser f4a3fba2
|
||||
* #397 accounts can be restricted to certian IPs 62e072a2
|
||||
* #504 automatic login through tailscale auth a4649d1e
|
||||
* #533 sticky qr-code with `--qr-pin 1` 1ebe06f5
|
||||
* #572 button to abort copy/move 715d374e
|
||||
* #618 "download selected files" didn't work on firefox 52 (winxp) dcc6b1b4
|
||||
* max number of cookies to allow can be configured 6303effe
|
||||
* good if you have too many selfhosted services on one domain (but will beware of the spec-mandataed max length of the cookie field!)
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* fix xvol/xdev edgecases:
|
||||
* #603 rootless vfs 554cc2f3
|
||||
* false-positive with overlapping volumes d9046f7e
|
||||
* #573 ftp: attempting an upload into read-only folder no longer kills the connection 3aa8b7aa
|
||||
* #306 adjust navpane for `--rp-loc` (location-based proxying)
|
||||
* #556 more sensible config expansion order f4727f8e
|
||||
* #624 ...which broke things bf1fdcab
|
||||
* the video player now stays fullscreen between videos 782e2f1d
|
||||
* heif thumbnailing with libvips
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* #253 build nix-packages from source (thx @toast003, @chinponya!) 187cae25
|
||||
* #616 logfiles will have a plaintext severity column if `--no-ansi` d4cf42e7
|
||||
* #598 separate option `--ac-convt` for audio transcoding timeout d5623057
|
||||
* #596 users with a blank password gets a strong random-generated one 7f448750
|
||||
* copyparty.exe: upgrade to python 3.13.7
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0810-1226 `v1.19.1` archlinux fix
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* new translations:
|
||||
* #486 French (thx @Tr3yWay996, @Packingdustry, @Alee14, @jakubiakfr, @Equinoxs!) e9ddfccf 7aa21483 b87f8f1b
|
||||
* #463 Polish (thx @pufereq and @daimond113!) 392a4db5
|
||||
* #537 Nynorsk (thx @chinatsu!) 3931bc27
|
||||
* #549 custom mdns domain 3c78c6a8
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* #539 FTP glitches when running on windows 8ba98877
|
||||
* #555 global-config didn't load through PRTY_CONFIG (thx @icxes!) 074e106e
|
||||
* macos: could take a while to establish webdav connection from finder a01870b7
|
||||
* ux:
|
||||
* dropdown colors 347cf6a5
|
||||
* case-sensitivity in filters e5e82295
|
||||
* iOS being too enthusiastic about using saved passwords 03acd65e
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0807-2213 `v1.19.0` usernames
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #511 login with username and password (not just password) can now optionally be enabled with `--usernames` 346515cc
|
||||
* if you have enabled password hashing (`ah-alg: argon2` or similar) then you will need to hash your passwords again after enabling usernames, hashing them as `username:password:`
|
||||
* #468 add Greek translation (thx @chamdim!) 50f46187 392abd06
|
||||
* #471 add Czech translation (thx @kubakubakuba!) c9556583
|
||||
* #515 support systemd socket acivation (thx @mati1210!) 9b9d2a92
|
||||
* #523 add QR-code to the connectpage bcc3b156
|
||||
* #513 optional EOL-conversion for texteditor 8b31ed88
|
||||
* controlpanel refresh-button now toggles automatic refresh 7ae84dea
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* fix stuck uploads when the up2k database (`e2d`) is not enabled 4a043568
|
||||
* if more than 60'000 files were uploaded and there were several dupes of some files, they could get stuck and never upload
|
||||
* upload performance is improved remarkably by enabling `e2d` so such huge uploads non-e2d had not been tested in a long time
|
||||
* #467 #470 fix ui-crash when exporting links of all uploaded files to clipboard (thx @geekalaa!) 0df1901f
|
||||
* #487 fix ui-crash when the location url-part is `//` 0f55a1ae
|
||||
* fix viewing `.MD` files (8a0746c6)
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* when a reverse-proxy is detected, force explicit configuration of `--rproxy` to obtain correct client IP 3f8cb7e8
|
||||
* a bit inconvenient, but helps prevent potentially-dangerous misconfiguration
|
||||
* the necessary configuration changes are explained in the serverlog (you can't miss it)
|
||||
* thanks to @person4268 for pointing out that there was room for improvements!
|
||||
* failed login attempts now only log a sha512 hash of the provided password
|
||||
* to see login-attempts with incorrect passwords as plaintext like before, `log-badpwd: 1`
|
||||
* #502 add systemd user services and templated services (thx @icxes!) 34d98e99
|
||||
* #475 improve helptext for multivalue global-options c2ac57a2
|
||||
* #475 add [chungus.conf](https://github.com/9001/copyparty/blob/hovudstraum/docs/chungus.conf), massive extensive nonsensical demo config b664ebb0
|
||||
* try to detect proxies with incorrect caching behavior 9e980bb5
|
||||
* recent-uploads now support ie9 a57f7cc2
|
||||
* languages and themes are now dropdowns a9ee4f24
|
||||
* copyparty.exe: upgrade python to 3.13.6 a98360f2
|
||||
* introduce [copyparty-en.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-en.py), english-only edition of copyparty-sfx.py to save space 33497e6b
|
||||
|
||||
## 🗿 known issues
|
||||
|
||||
* the `copyparty.pyz` in this release is english-only, and does not include the translations -- they got lost in transit while adjusting the buildscripts to make `copyparty-en.py`
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0804-0013 `v1.18.10` idp speedboost
|
||||
|
||||
|
|
|
@ -1083,7 +1083,7 @@
|
|||
th-x3: n # default
|
||||
|
||||
# image decoders, in order of preference
|
||||
th-dec: vips,pil,ff # default
|
||||
th-dec: vips,pil,raw,ff # default
|
||||
|
||||
# disable jpg output
|
||||
th-no-jpg
|
||||
|
@ -1115,6 +1115,9 @@
|
|||
# image formats to decode using pyvips
|
||||
th-r-vips: a,very,long,list,of,file,extensions # hint
|
||||
|
||||
# image formats to decode using rawpy
|
||||
th-r-raw: a,very,long,list,of,file,extensions # hint
|
||||
|
||||
# image formats to decode using ffmpeg
|
||||
th-r-ffi: a,very,long,list,of,file,extensions # hint
|
||||
|
||||
|
|
|
@ -160,6 +160,7 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
|
|||
|
||||
| method | params | result |
|
||||
|--|--|--|
|
||||
| GET | `?dl` | download file (don't show in-browser) |
|
||||
| GET | `?ls` | list files/folders at URL as JSON |
|
||||
| GET | `?ls&dots` | list files/folders at URL as JSON, including dotfiles |
|
||||
| GET | `?ls=t` | list files/folders at URL as plaintext |
|
||||
|
@ -328,7 +329,7 @@ if you don't need all the features, you can repack the sfx and save a bunch of s
|
|||
|
||||
the features you can opt to drop are
|
||||
* `cm`/easymde, the "fancy" markdown editor, saves ~89k
|
||||
* `hl`, prism, the syntax hilighter, saves ~41k
|
||||
* `hl`, prism, the syntax highlighter, saves ~41k
|
||||
* `fnt`, source-code-pro, the monospace font, saves ~9k
|
||||
|
||||
for the `re`pack to work, first run one of the sfx'es once to unpack it
|
||||
|
@ -354,7 +355,7 @@ pip install mutagen # audio metadata
|
|||
pip install pyftpdlib # ftp server
|
||||
pip install partftpy # tftp server
|
||||
pip install impacket # smb server -- disable Windows Defender if you REALLY need this on windows
|
||||
pip install Pillow pyheif-pillow-opener # thumbnails
|
||||
pip install Pillow pillow-heif # thumbnails
|
||||
pip install pyvips # faster thumbnails
|
||||
pip install psutil # better cleanup of stuck metadata parsers on windows
|
||||
pip install black==21.12b0 click==8.0.2 bandit pylint flake8 isort mypy # vscode tooling
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
[global]
|
||||
e2dsa # enable file indexing and filesystem scanning
|
||||
e2ts # enable multimedia indexing
|
||||
ansi # enable colors in log messages
|
||||
ansi # enable colors in log messages (both in logfiles and stdout)
|
||||
|
||||
# q, lo: /cfg/log/%Y-%m%d.log # log to file instead of docker
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
services:
|
||||
---
|
||||
|
||||
services:
|
||||
copyparty:
|
||||
image: copyparty/ac:latest
|
||||
container_name: copyparty
|
||||
|
|
|
@ -8,7 +8,7 @@ to try this out with minimal adjustments:
|
|||
* login to https://fs.example.com/ with username `authelia` password `authelia`
|
||||
|
||||
to use this in a safe and secure manner:
|
||||
* follow a guide on setting up authelia properly (TODO:link) and use the copyparty-specific parts of this folder as inspiration for your own config; namely the `cpp` subfolder and the `copyparty` service in `docker-compose.yml`
|
||||
* follow a guide on setting up [authelia](https://www.authelia.com/integration/proxies/traefik/#docker-compose) properly and use the copyparty-specific parts of this folder as inspiration for your own config; namely the `cpp` subfolder and the `copyparty` service in `docker-compose.yml`
|
||||
|
||||
this folder is based on:
|
||||
* https://github.com/authelia/authelia/tree/39763aaed24c4abdecd884b47357a052b235942d/examples/compose/lite
|
||||
|
@ -16,20 +16,18 @@ this folder is based on:
|
|||
incomplete list of modifications made:
|
||||
* support for running with podman as root on fedora (`:z` volumes, `label:disable`)
|
||||
* explicitly using authelia `v4.38.0-beta3` because config syntax changed since last stable release
|
||||
* disabled automatic letsencrypt certificate signing
|
||||
* reduced logging from debug to info
|
||||
* added a warning that traefik is given access to the docker socket (as recommended by traefik docs) which means traefik is able to break out of the container and has full root access on the host machine
|
||||
* implemented a docker socket-proxy to not bind the docker.socket directly to traefik
|
||||
* using valkey instead of redis for caching
|
||||
|
||||
|
||||
# security
|
||||
|
||||
there is probably/definitely room for improvement in this example setup. Some ideas taken from [github issue #62](https://github.com/9001/copyparty/issues/62):
|
||||
|
||||
* Add in a redis password to limit attacker lateral movement in the system
|
||||
* Move redis to a private network shared with just authelia
|
||||
* Pin to image hashes (or go all in on updates and add `watchtower`)
|
||||
* Move valkey to a private network shared with just authelia
|
||||
* Add `watchtower` to manage your image version updates
|
||||
* Drop bridge networking for just exposing traefik's public ports
|
||||
* Configure docker for non-root access to docker socket and then move traefik to use [non-root perms](https://docs.docker.com/engine/security/rootless/)
|
||||
|
||||
if you manage to improve on any of this, especially in a way that might be useful for other people, consider sending a PR :>
|
||||
|
||||
|
@ -47,4 +45,4 @@ currently **not optimal,** at least when compared to running the python sfx outs
|
|||
|
||||
authelia is behaving strangely, handling 340 requests per second for a while, but then it suddenly drops to 75 and stays there...
|
||||
|
||||
I'm assuming all of the performance issues is due to a misconfiguration of authelia/traefik/docker on my end, but I don't relly know where to start
|
||||
I'm assuming all of the performance issues is due to a misconfiguration of authelia/traefik/docker on my end, but I don't really know where to start
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
# based on https://github.com/authelia/authelia/blob/39763aaed24c4abdecd884b47357a052b235942d/examples/compose/lite/authelia/configuration.yml
|
||||
|
||||
# Authelia configuration
|
||||
|
||||
# This secret can also be set using the env variables AUTHELIA_JWT_SECRET_FILE
|
||||
jwt_secret: a_very_important_secret
|
||||
identity_validation:
|
||||
reset_password:
|
||||
jwt_secret: 'a_very_important_secret_so_please_change_this'
|
||||
|
||||
server:
|
||||
address: 'tcp://:9091'
|
||||
|
||||
log:
|
||||
level: info # debug
|
||||
level: info
|
||||
|
||||
totp:
|
||||
issuer: authelia.com
|
||||
|
@ -21,29 +20,26 @@ authentication_backend:
|
|||
access_control:
|
||||
default_policy: deny
|
||||
rules:
|
||||
# Rules applied to everyone
|
||||
- domain: traefik.example.com
|
||||
policy: one_factor
|
||||
- domain: auth.example.com
|
||||
policy: bypass # Allow access to the login UI
|
||||
- domain: fs.example.com
|
||||
policy: one_factor
|
||||
|
||||
session:
|
||||
# This secret can also be set using the env variables AUTHELIA_SESSION_SECRET_FILE
|
||||
secret: unsecure_session_secret
|
||||
|
||||
cookies:
|
||||
- name: authelia_session
|
||||
domain: example.com # Should match whatever your root protected domain is
|
||||
domain: example.com # this should match whatever your root protected domain is
|
||||
default_redirection_url: https://fs.example.com
|
||||
authelia_url: https://authelia.example.com/
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
|
||||
redis:
|
||||
host: redis
|
||||
host: valkey
|
||||
port: 6379
|
||||
# This secret can also be set using the env variables AUTHELIA_SESSION_REDIS_PASSWORD_FILE
|
||||
# password: authelia
|
||||
password: your_secure_password_here
|
||||
|
||||
|
||||
regulation:
|
||||
max_retries: 3
|
||||
|
@ -58,9 +54,7 @@ storage:
|
|||
notifier:
|
||||
disable_startup_check: true
|
||||
smtp:
|
||||
username: test
|
||||
# This secret can also be set using the env variables AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE
|
||||
password: password
|
||||
host: mail.example.com
|
||||
port: 25
|
||||
sender: admin@example.com
|
||||
address: 'smtp://127.0.0.1:25'
|
||||
username: 'test'
|
||||
password: 'password'
|
||||
sender: "Authelia <admin@example.com>"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
version: '3.3'
|
||||
---
|
||||
|
||||
networks:
|
||||
net:
|
||||
|
@ -6,7 +6,7 @@ networks:
|
|||
|
||||
services:
|
||||
copyparty:
|
||||
image: copyparty/ac
|
||||
image: copyparty/ac:latest
|
||||
container_name: idp_copyparty
|
||||
user: "1000:1000" # should match the user/group of your fileshare volumes
|
||||
volumes:
|
||||
|
@ -19,19 +19,19 @@ services:
|
|||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.copyparty.rule=Host(`fs.example.com`)'
|
||||
- 'traefik.http.routers.copyparty.entrypoints=https'
|
||||
- 'traefik.http.routers.copyparty.entrypoints=websecure'
|
||||
- 'traefik.http.routers.copyparty.tls=true'
|
||||
- 'traefik.http.routers.copyparty.tls.certresolver=letsencrypt' # ← THIS IS CRUCIAL
|
||||
- 'traefik.http.routers.copyparty.middlewares=authelia@docker'
|
||||
stop_grace_period: 15s # thumbnailer is allowed to continue finishing up for 10s after the shutdown signal
|
||||
environment:
|
||||
LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE
|
||||
# enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram)
|
||||
|
||||
PYTHONUNBUFFERED: 1
|
||||
# ensures log-messages are not delayed (but can reduce speed a tiny bit)
|
||||
|
||||
authelia:
|
||||
image: authelia/authelia:v4.38.0-beta3 # the config files in the authelia folder use the new syntax
|
||||
image: authelia/authelia:4.39.5@sha256:023e02e5203dfa0ebaee7a48b5bae34f393d1f9cada4a9df7fbf87eb1759c671
|
||||
container_name: idp_authelia
|
||||
volumes:
|
||||
- ./authelia:/config:z
|
||||
|
@ -40,25 +40,23 @@ services:
|
|||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.authelia.rule=Host(`authelia.example.com`)'
|
||||
- 'traefik.http.routers.authelia.entrypoints=https'
|
||||
- 'traefik.http.routers.authelia.entrypoints=websecure'
|
||||
- 'traefik.http.routers.authelia.tls=true'
|
||||
#- 'traefik.http.routers.authelia.tls.certresolver=letsencrypt' # uncomment this to enable automatic certificate signing (1/2)
|
||||
- 'traefik.http.routers.authelia.tls.certresolver=letsencrypt'
|
||||
- 'traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/authz/forward-auth?authelia_url=https://authelia.example.com'
|
||||
- 'traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true'
|
||||
- 'traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email'
|
||||
expose:
|
||||
- 9091
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
disable: true
|
||||
environment:
|
||||
- TZ=Etc/UTC
|
||||
|
||||
redis:
|
||||
image: redis:7.2.4-alpine3.19
|
||||
container_name: idp_redis
|
||||
valkey:
|
||||
image: valkey/valkey:8.1.3-alpine3.22@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4
|
||||
container_name: idp_valkey
|
||||
volumes:
|
||||
- ./redis:/data:z
|
||||
- ./valkey:/data:z
|
||||
networks:
|
||||
- net
|
||||
expose:
|
||||
|
@ -66,40 +64,55 @@ services:
|
|||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Etc/UTC
|
||||
- VALKEY_EXTRA_FLAGS=--requirepass your_secure_password_here
|
||||
|
||||
socket-proxy:
|
||||
image: lscr.io/linuxserver/socket-proxy:3.2.3@sha256:63d2e0ce6bb0d12dfdbde5c3af31d08fee343ec3801a050c8197a3f5ffae8bed
|
||||
container_name: idp_socket_proxy
|
||||
environment:
|
||||
- CONTAINERS=1
|
||||
- NETWORKS=1
|
||||
- EVENTS=1
|
||||
- PING=1
|
||||
- VERSION=1
|
||||
- LOG_LEVEL=warning
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /run
|
||||
networks:
|
||||
- net
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 2375
|
||||
|
||||
traefik:
|
||||
image: traefik:2.11.0
|
||||
image: traefik:3.5.0@sha256:4e7175cfe19be83c6b928cae49dde2f2788fb307189a4dc9550b67acf30c11a5
|
||||
container_name: idp_traefik
|
||||
volumes:
|
||||
- ./traefik:/etc/traefik:z
|
||||
- /var/run/docker.sock:/var/run/docker.sock # WARNING: this gives traefik full root-access to the host OS, but is recommended/required(?) by traefik
|
||||
security_opt:
|
||||
- label:disable # disable selinux because it (rightly) blocks access to docker.sock
|
||||
networks:
|
||||
- net
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.api.rule=Host(`traefik.example.com`)'
|
||||
- 'traefik.http.routers.api.entrypoints=https'
|
||||
- 'traefik.http.routers.api.service=api@internal'
|
||||
- 'traefik.http.routers.api.tls=true'
|
||||
#- 'traefik.http.routers.api.tls.certresolver=letsencrypt' # uncomment this to enable automatic certificate signing (2/2)
|
||||
- 'traefik.http.routers.api.middlewares=authelia@docker'
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
command:
|
||||
- '--api'
|
||||
- '--providers.docker=true'
|
||||
- '--global.sendAnonymousUsage=false'
|
||||
- '--providers.docker.endpoint=tcp://socket-proxy:2375'
|
||||
- '--providers.docker.exposedByDefault=false'
|
||||
- '--entrypoints.http=true'
|
||||
- '--entrypoints.http.address=:80'
|
||||
- '--entrypoints.http.http.redirections.entrypoint.to=https'
|
||||
- '--entrypoints.http.http.redirections.entrypoint.scheme=https'
|
||||
- '--entrypoints.https=true'
|
||||
- '--entrypoints.https.address=:443'
|
||||
- '--entrypoints.web.address=:80'
|
||||
- '--entrypoints.web.http.redirections.entrypoint.to=websecure'
|
||||
- '--entrypoints.web.http.redirections.entrypoint.scheme=https'
|
||||
- '--entrypoints.websecure.address=:443'
|
||||
- '--certificatesResolvers.letsencrypt.acme.email=your-email@your-domain.com'
|
||||
- '--certificatesResolvers.letsencrypt.acme.storage=/etc/traefik/acme.json'
|
||||
- '--certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=http'
|
||||
- '--log=true'
|
||||
- '--log.level=WARNING' # DEBUG
|
||||
- '--certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=web'
|
||||
- '--log.level=INFO'
|
||||
depends_on:
|
||||
- socket-proxy
|
||||
|
|
|
@ -71,7 +71,7 @@ avg() { awk 'function pr(ncsz) {if (nsmp>0) {printf "%3s %s\n", csz, sum/nsmp} c
|
|||
python3 -um copyparty -nw -v srv::rw -i 127.0.0.1 2>&1 | tee log
|
||||
cat log | awk '!/"purl"/{next} {s=$1;sub(/[^m]+m/,"");gsub(/:/," ");t=60*(60*$1+$2)+$3} t<p{t+=86400} !a{a=t;sa=s} {b=t;sb=s} END {print b-a,sa,sb}'
|
||||
|
||||
# or if the client youre measuring dies for ~15sec every once ina while and you wanna filter those out,
|
||||
# or if the client you're measuring dies for ~15sec every once ina while and you wanna filter those out,
|
||||
cat log | awk '!/"purl"/{next} {s=$1;sub(/[^m]+m/,"");gsub(/:/," ");t=60*(60*$1+$2)+$3} t<p{t+=86400} !p{a=t;p=t;r=0;next} t-p>1{printf "%.3f += %.3f - %.3f (%.3f) # %.3f -> %.3f\n",r,p,a,p-a,p,t;r+=p-a;a=t} {p=t} END {print r+p-a}'
|
||||
|
||||
|
||||
|
@ -337,3 +337,5 @@ mk && t0="$(date)" && while true; do date -s "$(date '+ 1 hour')"; systemd-tmpfi
|
|||
mk && sudo -u ed flock /tmp/foo sleep 40 & sleep 1; ps aux | grep -E 'sleep 40$' && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; done; echo "$t0"
|
||||
mk && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; tar -cf/dev/null /tmp/foo; done; echo "$t0"
|
||||
|
||||
# number of megabytes downloaded since some date
|
||||
awk </var/log/wjaycore.out '/^..36m2025-05-20/{o=1} !o{next} !/ plain 20[06](,| \[[^,]+\],) +[0-9.]+.\[33m[KM] .* n[0-9]+$/{next} {v=$0;sub(/.* plain 20[06](,| \[[^,]+\],) +/,"",v);sub(/ .*/,"",v);u=v;sub(/.\[.*/,"",v);sub(/.*m/,"",u);$0=u} /[KMG]/{v*=1024} /[MG]/{v*=1024} /G/{v*=1024} {t+=v} END{printf "%d\n",t/(1024*1024)}'
|
||||
|
|
|
@ -68,7 +68,7 @@ currently up to date with [awesome-selfhosted](https://github.com/awesome-selfho
|
|||
* [kodbox](https://github.com/kalcaddle/kodbox) ([review](#kodbox)) appears to be a fantastic alternative if you're not worried about running chinese software, with several advantages over copyparty
|
||||
* but anything you want to share must be moved into the kodbox filesystem
|
||||
* [seafile](https://github.com/haiwen/seafile) ([review](#seafile)) and [nextcloud](https://github.com/nextcloud/server) ([review](#nextcloud)) could be decent alternatives if you need something heavier than copyparty
|
||||
* but their [license](https://snyk.io/learn/agpl-license/) is [problematic](https://opensource.google/documentation/reference/using/agpl-policy)
|
||||
* but their [license (AGPL)](https://snyk.io/learn/agpl-license/) is [thorny](https://opensource.google/documentation/reference/using/agpl-policy)
|
||||
* and copyparty is way better at uploads in particular (resumable, accelerated)
|
||||
* and anything you want to share must be moved into the respective filesystems
|
||||
* [filebrowser](https://github.com/filebrowser/filebrowser) ([review](#filebrowser)) and [dufs](https://github.com/sigoden/dufs) ([review](#dufs)) are simpler copyparties but with a settings gui
|
||||
|
@ -123,14 +123,14 @@ symbol legend,
|
|||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| intuitive UX | | ╱ | █ | █ | █ | | █ | █ | █ | █ | █ | █ | █ |
|
||||
| config GUI | | █ | █ | █ | █ | | | █ | █ | █ | | █ | █ |
|
||||
| good documentation | | | | █ | █ | █ | █ | | | █ | █ | ╱ | ╱ |
|
||||
| good documentation | | | █ | █ | █ | █ | █ | | | █ | █ | ╱ | ╱ |
|
||||
| runs on iOS | ╱ | | | | | ╱ | | | | | | | |
|
||||
| runs on Android | █ | | | | | █ | | | | | | | |
|
||||
| runs on Android | █ | | █ | | | █ | | | | | | | |
|
||||
| runs on WinXP | █ | █ | | | | █ | | | | | | | |
|
||||
| runs on Windows | █ | █ | █ | █ | █ | █ | █ | ╱ | █ | █ | █ | █ | ╱ |
|
||||
| runs on Linux | █ | ╱ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| runs on Macos | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | |
|
||||
| runs on FreeBSD | █ | | | • | █ | █ | █ | • | █ | █ | | █ | |
|
||||
| runs on FreeBSD | █ | | █ | • | █ | █ | █ | • | █ | █ | | █ | |
|
||||
| runs on Risc-V | █ | | | █ | █ | █ | | • | | █ | | | |
|
||||
| portable binary | █ | █ | █ | | | █ | █ | | | █ | | █ | █ |
|
||||
| zero setup, just go | █ | █ | █ | | | ╱ | █ | | | █ | | ╱ | █ |
|
||||
|
@ -140,7 +140,7 @@ symbol legend,
|
|||
* `zero setup` = you can get a mostly working setup by just launching the app, without having to install any software or configure whatever
|
||||
* `a`/copyparty remarks:
|
||||
* no gui for server settings; only for client-side stuff
|
||||
* can theoretically run on iOS / iPads using [iSH](https://ish.app/), but only the iPad will offer sufficient multitasking i think
|
||||
* runs on iOS / iPads using [a-Shell](https://holzschu.github.io/a-Shell_iOS/) (pretty good) or [iSH](https://ish.app/) (very slow) but cannot run in the background and is not able to share all of your phone storage (just a separate dedicated folder)
|
||||
* [android app](https://f-droid.org/en/packages/me.ocv.partyup/) is for uploading only
|
||||
* no iOS app but has [shortcuts](https://github.com/9001/copyparty#ios-shortcuts) for easy uploading
|
||||
* `b`/hfs2 runs on linux through wine
|
||||
|
@ -161,7 +161,7 @@ symbol legend,
|
|||
| upload | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | ╱ | █ | █ |
|
||||
| parallel uploads | █ | | | █ | █ | | • | | █ | █ | █ | | █ |
|
||||
| resumable uploads | █ | | █ | | | | | | █ | █ | █ | ╱ | |
|
||||
| upload segmenting | █ | | | █ | | | | █ | █ | █ | █ | ╱ | █ |
|
||||
| upload segmenting | █ | | █ | █ | | | | █ | █ | █ | █ | ╱ | █ |
|
||||
| upload acceleration | █ | | | | | | | | █ | | █ | | |
|
||||
| upload verification | █ | | | █ | █ | | | | █ | | | | |
|
||||
| upload deduplication | █ | | | | █ | | | | █ | | | | |
|
||||
|
@ -169,7 +169,7 @@ symbol legend,
|
|||
| CTRL-V from device | █ | | | █ | | | | | | | | | |
|
||||
| race the beam ("p2p") | █ | | | | | | | | | | | | |
|
||||
| "tail -f" streaming | █ | | | | | | | | | | | | |
|
||||
| keep last-modified time | █ | | | █ | █ | █ | | | | | | █ | |
|
||||
| keep last-modified time | █ | | █ | █ | █ | █ | | | | | | █ | |
|
||||
| upload rules | ╱ | ╱ | ╱ | ╱ | ╱ | | | ╱ | ╱ | | ╱ | ╱ | ╱ |
|
||||
| ┗ max disk usage | █ | █ | █ | | █ | | | | █ | | | █ | █ |
|
||||
| ┗ max filesize | █ | | | | | | | █ | | | █ | █ | █ |
|
||||
|
@ -251,7 +251,7 @@ symbol legend,
|
|||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| config from cmd args | █ | | | | | █ | █ | | | █ | | ╱ | ╱ |
|
||||
| config from cmd args | █ | | █ | | | █ | █ | | | █ | | ╱ | ╱ |
|
||||
| config files | █ | █ | █ | ╱ | ╱ | █ | | █ | | █ | • | ╱ | ╱ |
|
||||
| runtime config reload | █ | █ | █ | | | | | █ | █ | █ | █ | | █ |
|
||||
| same-port http / https | █ | | | | | | | | | | | | |
|
||||
|
@ -276,10 +276,10 @@ symbol legend,
|
|||
| per-account chroot | | | | | | | | | | | | █ | |
|
||||
| single-sign-on | ╱ | | | █ | █ | | | | • | | | | |
|
||||
| token auth | ╱ | | | █ | █ | | | █ | | | | | █ |
|
||||
| 2fa | ╱ | | | █ | █ | | | | | | | █ | ╱ |
|
||||
| 2fa | ╱ | | / | █ | █ | | | | | | | █ | ╱ |
|
||||
| per-volume permissions | █ | █ | █ | █ | █ | █ | █ | | █ | █ | ╱ | █ | █ |
|
||||
| per-folder permissions | ╱ | | | █ | █ | | █ | | █ | █ | ╱ | █ | █ |
|
||||
| per-file permissions | | | | █ | █ | | █ | | █ | | | | █ |
|
||||
| per-folder permissions | ╱ | | █ | █ | █ | | █ | | █ | █ | ╱ | █ | █ |
|
||||
| per-file permissions | | | █ | █ | █ | | █ | | █ | | | | █ |
|
||||
| per-file passwords | █ | | | █ | █ | | █ | | █ | | | | █ |
|
||||
| unmap subfolders | █ | | █ | | | | █ | | | █ | ╱ | • | |
|
||||
| index.html blocks list | ╱ | | | | | | █ | | | • | | | |
|
||||
|
@ -297,13 +297,13 @@ symbol legend,
|
|||
| full sync | | | | █ | █ | | | | | | | | |
|
||||
| speed throttle | | █ | █ | | | █ | | | █ | | | █ | |
|
||||
| anti-bruteforce | █ | █ | █ | █ | █ | | | | • | | | █ | • |
|
||||
| dyndns updater | | █ | | | | | | | | | | | |
|
||||
| dyndns updater | | █ | █ | | | | | | | | | | |
|
||||
| self-updater | | | █ | | | | | | | | | | █ |
|
||||
| log rotation | █ | | █ | █ | █ | | | • | █ | | | █ | • |
|
||||
| upload tracking / log | █ | █ | • | █ | █ | | | █ | █ | | | ╱ | █ |
|
||||
| prometheus metrics | █ | | | █ | | | | | | | | █ | |
|
||||
| curl-friendly ls | █ | | | | | | | | | | | | |
|
||||
| curl-friendly upload | █ | | | | | █ | █ | • | | | | | |
|
||||
| curl-friendly upload | █ | | █ | | | █ | █ | • | | | | | |
|
||||
|
||||
* `unmap subfolders` = "shadowing"; mounting a local folder in the middle of an existing filesystem tree in order to disable access below that path
|
||||
* `files stored as-is` = uploaded files are trivially readable from the server HDD, not sliced into chunks or in weird folder structures or anything like that
|
||||
|
@ -332,7 +332,8 @@ symbol legend,
|
|||
* `upload tracking / log` in main logfile
|
||||
* `m`/arozos:
|
||||
* `2fa` maybe possible through LDAP/Oauth
|
||||
|
||||
* `c`/hfs3
|
||||
* `2fa` available by installing a plugin
|
||||
|
||||
## client features
|
||||
|
||||
|
@ -342,18 +343,18 @@ symbol legend,
|
|||
| themes | █ | █ | █ | █ | | | | | █ | | | | |
|
||||
| directory tree nav | █ | ╱ | | | █ | | | | █ | | ╱ | | |
|
||||
| multi-column sorting | █ | | | | | | | | | | | | |
|
||||
| thumbnails | █ | | | ╱ | ╱ | | | █ | █ | ╱ | | | █ |
|
||||
| ┗ image thumbnails | █ | | | █ | █ | | | █ | █ | █ | | | █ |
|
||||
| thumbnails | █ | | / | ╱ | ╱ | | | █ | █ | ╱ | | | █ |
|
||||
| ┗ image thumbnails | █ | | / | █ | █ | | | █ | █ | █ | | | █ |
|
||||
| ┗ video thumbnails | █ | | | █ | █ | | | | █ | | | | █ |
|
||||
| ┗ audio spectrograms | █ | | | | | | | | | | | | |
|
||||
| audio player | █ | | ╱ | █ | █ | | | | █ | ╱ | | | █ |
|
||||
| ┗ gapless playback | █ | | | | | | | | • | | | | |
|
||||
| ┗ audio equalizer | █ | | | | | | | | | | | | |
|
||||
| ┗ waveform seekbar | █ | | | | | | | | | | | | |
|
||||
| ┗ OS integration | █ | | | | | | | | | | | | |
|
||||
| ┗ OS integration | █ | | █ | | | | | | | | | | |
|
||||
| ┗ transcode to lossy | █ | | | | | | | | | | | | |
|
||||
| video player | █ | | | █ | █ | | | | █ | █ | | | █ |
|
||||
| ┗ video transcoding | | | | | | | | | █ | | | | |
|
||||
| video player | █ | | █ | █ | █ | | | | █ | █ | | | █ |
|
||||
| ┗ video transcoding | | | / | | | | | | █ | | | | |
|
||||
| audio BPM detector | █ | | | | | | | | | | | | |
|
||||
| audio key detector | █ | | | | | | | | | | | | |
|
||||
| search by path / name | █ | █ | █ | █ | █ | | █ | | █ | █ | ╱ | | |
|
||||
|
@ -366,15 +367,15 @@ symbol legend,
|
|||
| undo recent uploads | █ | | | | | | | | | | | | |
|
||||
| create directories | █ | | █ | █ | █ | ╱ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| image viewer | █ | | █ | █ | █ | | | | █ | █ | █ | | █ |
|
||||
| markdown viewer | █ | | | | █ | | | | █ | ╱ | ╱ | | █ |
|
||||
| markdown viewer | █ | | / | | █ | | | | █ | ╱ | ╱ | | █ |
|
||||
| markdown editor | █ | | | | █ | | | | █ | ╱ | ╱ | | █ |
|
||||
| readme.md in listing | █ | | | █ | | | | | | | | | |
|
||||
| readme.md in listing | █ | | / | █ | | | | | | | | | |
|
||||
| rename files | █ | █ | █ | █ | █ | ╱ | █ | | █ | █ | █ | █ | █ |
|
||||
| batch rename | █ | | | | | | | | █ | | | | |
|
||||
| cut / paste files | █ | █ | | █ | █ | | | | █ | | | | █ |
|
||||
| cut / paste files | █ | █ | █ | █ | █ | | | | █ | | | | █ |
|
||||
| move files | █ | █ | █ | █ | █ | | █ | | █ | █ | █ | | █ |
|
||||
| delete files | █ | █ | █ | █ | █ | ╱ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| copy files | | | | | █ | | | | █ | █ | █ | | █ |
|
||||
| copy files | | | / | | █ | | | | █ | █ | █ | | █ |
|
||||
|
||||
* `single-page app` = multitasking; possible to continue navigating while uploading
|
||||
* `audio player » os-integration` = use the [lockscreen](https://user-images.githubusercontent.com/241032/142711926-0700be6c-3e31-47b3-9928-53722221f722.png) or [media hotkeys](https://user-images.githubusercontent.com/241032/215347492-b4250797-6c90-4e09-9a4c-721edf2fb15c.png) to play/pause, prev/next song
|
||||
|
@ -383,8 +384,6 @@ symbol legend,
|
|||
* `undo recent uploads` = accounts without delete permissions have a time window where they can undo their own uploads
|
||||
* `a`/copyparty has teeny-tiny skips playing gapless albums depending on audio codec (opus best)
|
||||
* `b`/hfs2 has a very basic directory tree view, not showing sibling folders
|
||||
* `c`/hfs3 remarks:
|
||||
* audio playback does not continue into next song
|
||||
* `f`/rclone can do some file management (mkdir, rename, delete) when hosting througn webdav
|
||||
* `j`/filebrowser remarks:
|
||||
* audio playback does not continue into next song
|
||||
|
@ -396,9 +395,9 @@ symbol legend,
|
|||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| OS alert on upload | █ | | | | | | | | | ╱ | | ╱ | |
|
||||
| discord | █ | | | | | | | | | ╱ | | ╱ | |
|
||||
| ┗ announce uploads | █ | | | | | | | | | | | ╱ | |
|
||||
| OS alert on upload | ╱ | | | | | | | | | ╱ | | ╱ | |
|
||||
| discord | ╱ | | | | | | | | | ╱ | | ╱ | |
|
||||
| ┗ announce uploads | ╱ | | | | | | | | | | | ╱ | |
|
||||
| ┗ custom embeds | | | | | | | | | | | | ╱ | |
|
||||
| sharex | █ | | | █ | | █ | ╱ | █ | | | | | |
|
||||
| flameshot | | | | | | █ | | | | | | | |
|
||||
|
@ -471,10 +470,8 @@ symbol legend,
|
|||
* vfs with gui config, per-volume permissions
|
||||
* tested locally, v0.53.2 on archlinux
|
||||
* 🔵 uploads are resumable
|
||||
* ⚠️ uploads are not segmented; max upload size 100 MiB on cloudflare
|
||||
* ⚠️ uploads are not accelerated (copyparty is 3x faster across the atlantic)
|
||||
* ⚠️ uploads are not integrity-checked
|
||||
* ⚠️ copies the file after upload; need twice filesize free disk space
|
||||
* ⚠️ uploading small files is decent; `107` files per sec (copyparty does `670`/sec, 6x faster)
|
||||
* ⚠️ doesn't support crazy filenames
|
||||
* ✅ config GUI
|
||||
|
@ -575,7 +572,7 @@ symbol legend,
|
|||
* ✅ file tags; file discussions!?
|
||||
* ✅ video transcoding
|
||||
* ✅ unzip uploaded archives
|
||||
* ✅ IDE with syntax hilighting
|
||||
* ✅ IDE with syntax highlighting
|
||||
* ✅ wysiwyg editor for openoffice files
|
||||
|
||||
## [filebrowser](https://github.com/filebrowser/filebrowser)
|
||||
|
|
16
flake.nix
16
flake.nix
|
@ -12,19 +12,7 @@
|
|||
}:
|
||||
{
|
||||
nixosModules.default = ./contrib/nixos/modules/copyparty.nix;
|
||||
overlays.default = final: prev: rec {
|
||||
copyparty = final.python3.pkgs.callPackage ./contrib/package/nix/copyparty {
|
||||
ffmpeg = final.ffmpeg-full;
|
||||
};
|
||||
|
||||
partyfuse = prev.callPackage ./contrib/package/nix/partyfuse {
|
||||
inherit copyparty;
|
||||
};
|
||||
|
||||
u2c = prev.callPackage ./contrib/package/nix/u2c {
|
||||
inherit copyparty;
|
||||
};
|
||||
};
|
||||
overlays.default = import ./contrib/package/nix/overlay.nix;
|
||||
}
|
||||
// flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
|
@ -54,8 +42,6 @@
|
|||
packages = {
|
||||
inherit (pkgs)
|
||||
copyparty
|
||||
partyfuse
|
||||
u2c
|
||||
;
|
||||
default = self.packages.${system}.copyparty;
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
FROM alpine:latest
|
||||
WORKDIR /z
|
||||
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.title="copyparty-ac" \
|
||||
org.opencontainers.image.description="copyparty with Pillow and FFmpeg (image/audio/video thumbnails, audio transcoding, media tags)"
|
||||
|
@ -16,6 +16,6 @@ COPY i/dist/copyparty-sfx.py innvikler.sh ./
|
|||
ADD base ./base
|
||||
RUN ash innvikler.sh ac
|
||||
|
||||
WORKDIR /w
|
||||
WORKDIR /state
|
||||
EXPOSE 3923
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"]
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
FROM alpine:latest
|
||||
WORKDIR /z
|
||||
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.title="copyparty-dj" \
|
||||
org.opencontainers.image.description="copyparty with all optional dependencies, including musical key / bpm detection"
|
||||
|
@ -19,13 +19,16 @@ RUN apk add -U !pyc \
|
|||
vips-jxl vips-heif vips-poppler vips-magick \
|
||||
py3-numpy fftw libsndfile \
|
||||
vamp-sdk vamp-sdk-libs \
|
||||
libraw py3-numpy cython \
|
||||
&& apk add -t .bd \
|
||||
bash wget gcc g++ make cmake patchelf \
|
||||
python3-dev ffmpeg-dev fftw-dev libsndfile-dev \
|
||||
py3-wheel py3-numpy-dev libffi-dev \
|
||||
vamp-sdk-dev \
|
||||
libraw-dev py3-numpy-dev \
|
||||
&& rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \
|
||||
&& python3 -m pip install pyvips \
|
||||
&& python3 -m pip install "$(wget -O- https://api.github.com/repos/letmaik/rawpy/releases/latest | awk -F\" '$2=="tarball_url"{print$4}')" \
|
||||
&& bash install-deps.sh \
|
||||
&& apk del py3-pip .bd \
|
||||
&& chmod 777 /root \
|
||||
|
@ -35,6 +38,6 @@ COPY i/dist/copyparty-sfx.py innvikler.sh ./
|
|||
ADD base ./base
|
||||
RUN ash innvikler.sh dj
|
||||
|
||||
WORKDIR /w
|
||||
WORKDIR /state
|
||||
EXPOSE 3923
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"]
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
FROM alpine:latest
|
||||
WORKDIR /z
|
||||
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.title="copyparty-im" \
|
||||
org.opencontainers.image.description="copyparty with Pillow and Mutagen (image thumbnails, media tags)"
|
||||
|
@ -15,6 +15,6 @@ COPY i/dist/copyparty-sfx.py innvikler.sh ./
|
|||
ADD base ./base
|
||||
RUN ash innvikler.sh im
|
||||
|
||||
WORKDIR /w
|
||||
WORKDIR /state
|
||||
EXPOSE 3923
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"]
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
FROM alpine:latest
|
||||
WORKDIR /z
|
||||
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.title="copyparty-iv" \
|
||||
org.opencontainers.image.description="copyparty with Pillow, FFmpeg, libvips (image/audio/video thumbnails, audio transcoding, media tags)"
|
||||
|
@ -14,17 +14,20 @@ RUN apk add -U !pyc \
|
|||
ffmpeg \
|
||||
py3-magic \
|
||||
vips-jxl vips-heif vips-poppler vips-magick \
|
||||
libraw py3-numpy cython \
|
||||
&& apk add -t .bd \
|
||||
bash wget gcc g++ make cmake patchelf \
|
||||
python3-dev py3-wheel libffi-dev \
|
||||
libraw-dev py3-numpy-dev \
|
||||
&& rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \
|
||||
&& python3 -m pip install pyvips \
|
||||
&& python3 -m pip install "$(wget -O- https://api.github.com/repos/letmaik/rawpy/releases/latest | awk -F\" '$2=="tarball_url"{print$4}')" \
|
||||
&& apk del py3-pip .bd
|
||||
|
||||
COPY i/dist/copyparty-sfx.py innvikler.sh ./
|
||||
ADD base ./base
|
||||
RUN ash innvikler.sh iv
|
||||
|
||||
WORKDIR /w
|
||||
WORKDIR /state
|
||||
EXPOSE 3923
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"]
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
FROM alpine:latest
|
||||
WORKDIR /z
|
||||
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.title="copyparty-min" \
|
||||
org.opencontainers.image.description="just copyparty, no thumbnails / media tags / audio transcoding"
|
||||
|
@ -13,6 +13,6 @@ RUN apk --no-cache add !pyc \
|
|||
COPY i/dist/copyparty-sfx.py innvikler.sh ./
|
||||
RUN ash innvikler.sh min
|
||||
|
||||
WORKDIR /w
|
||||
WORKDIR /state
|
||||
EXPOSE 3923
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "--no-thumb", "-c", "/z/initcfg"]
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "--no-thumb", "-c", "/z/initcfg"]
|
||||
|
|
|
@ -15,9 +15,15 @@ rm -rf /z/base
|
|||
rm -rf /var/cache/apk/* /root/.cache
|
||||
|
||||
# initial config; common for all flavors
|
||||
mkdir /cfg /w
|
||||
chmod 777 /cfg /w
|
||||
echo % /cfg > initcfg
|
||||
mkdir /state /cfg /w
|
||||
chmod 777 /state /cfg /w
|
||||
cat >initcfg <<'EOF'
|
||||
[global]
|
||||
chdir: /w
|
||||
no-crt
|
||||
|
||||
% /cfg
|
||||
EOF
|
||||
|
||||
# unpack sfx and dive in
|
||||
python3 copyparty-sfx.py --version
|
||||
|
|
|
@ -102,12 +102,18 @@ filt=
|
|||
# arm takes forever so make it top priority
|
||||
[ ${a::3} == arm ] && nice= || nice=-n20
|
||||
|
||||
# not sure if this is necessary or if inherit-annotations=false was enough, but won't hurt
|
||||
readarray -t annot < <(awk <Dockerfile.$i '/org.opencontainers.image/{sub(/[^\.]+/,"");sub(/[" \\]+$/,"");sub(/"/,"");print"--annotation";print"org"$0}')
|
||||
annot+=( --annotation "org.opencontainers.image.created=$( date -u +%Y-%m-%dT%H:%M:%SZ )" )
|
||||
|
||||
# --pull=never does nothing at all btw
|
||||
(set -x
|
||||
nice $nice podman build \
|
||||
--squash \
|
||||
--pull=never \
|
||||
--from localhost/alpine-$a \
|
||||
--inherit-annotations=false \
|
||||
"${annot[@]}" \
|
||||
-t copyparty-$i-$a$suf \
|
||||
-f Dockerfile.$i . ||
|
||||
(echo $? $i-$a >> err; printf '%096d\n' $(seq 1 42))
|
||||
|
|
|
@ -7,7 +7,7 @@ import subprocess as sp
|
|||
|
||||
# to convert the copyparty --help to html, run this in xfce4-terminal @ 140x43:
|
||||
_ = r""""
|
||||
echo; for a in '' -bind -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -chmod -pwhash -zm; do
|
||||
echo; for a in '' -bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -urlform -exp -ls -dbd -chmod -pwhash -zm; do
|
||||
./copyparty-sfx.py --help$a 2>/dev/null; printf '\n\n\n%0139d\n\n\n'; done # xfce4-terminal @ 140x43
|
||||
"""
|
||||
# click [edit] => [select all]
|
||||
|
|
|
@ -23,7 +23,7 @@ exit 0
|
|||
|
||||
|
||||
# first open an infinitely wide console (this is why you own an ultrawide) and copypaste this into it:
|
||||
for a in '' -bind -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -chmod -pwhash -zm; do
|
||||
for a in '' -bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -urlform -exp -ls -dbd -chmod -pwhash -zm; do
|
||||
./copyparty-sfx.py --help$a 2>/dev/null; printf '\n\n\n%0255d\n\n\n'; done
|
||||
|
||||
# then copypaste all of the output by pressing ctrl-shift-a, ctrl-shift-c
|
||||
|
|
66
scripts/make-rpm.sh
Executable file
66
scripts/make-rpm.sh
Executable file
|
@ -0,0 +1,66 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
#--localbuild to build webdeps and tar locally; otherwise just download prebuilt
|
||||
#--pm change packagemanager; otherwise default to dnf
|
||||
|
||||
while [ ! -z "$1" ]; do
|
||||
case $1 in
|
||||
local-build) local_build=1 ; ;;
|
||||
pm) shift;packagemanager="$1"; ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
[ -e copyparty/__main__.py ] || cd ..
|
||||
[ -e copyparty/__main__.py ] ||
|
||||
{
|
||||
echo "run me from within the project root folder"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
packagemanager=${packagemanager:-dnf}
|
||||
ver=$(awk '/^VERSION/{gsub(/[^0-9]/," ");printf "%d.%d.%d\n",$1,$2,$3}' copyparty/__version__.py)
|
||||
releasedir="dist/temp_copyparty_$ver"
|
||||
sourcepkg="copyparty-$ver.tar.gz"
|
||||
|
||||
#make temporary directory to build rpm in
|
||||
mkdir -p $releasedir/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||
trap "rm -rf $releasedir" EXIT
|
||||
|
||||
# make/get tarball
|
||||
if [ $local_build ]; then
|
||||
if [ ! -f "copyparty/web/deps/mini-fa.woff" ]; then
|
||||
sudo $packagemanager update
|
||||
sudo $packagemanager install podman-docker docker
|
||||
make -C deps-docker
|
||||
fi
|
||||
if [ ! -f "dist/$sourcepkg" ]; then
|
||||
./$cppdir/scripts/make-sfx.sh gz fast # pulls some build-deps + good smoketest
|
||||
./$cppdir/scripts/make-tgz-release.sh "$ver"
|
||||
fi
|
||||
else
|
||||
if [ ! -f "dist/$sourcepkg" ]; then
|
||||
curl -OL https://github.com/9001/copyparty/releases/download/v$ver/$sourcepkg --output-dir dist
|
||||
fi
|
||||
fi
|
||||
|
||||
cp dist/$sourcepkg "$releasedir/SOURCES/$sourcepkg"
|
||||
|
||||
cp "contrib/package/rpm/copyparty.spec" "$releasedir/SPECS/"
|
||||
sed -i "s/\$pkgver/$ver/g" "$releasedir/SPECS/copyparty.spec"
|
||||
sed -i "s/\$pkgrel/1/g" "$releasedir/SPECS/copyparty.spec"
|
||||
|
||||
sudo $packagemanager update
|
||||
sudo $packagemanager install \
|
||||
rpmdevtools python-devel pyproject-rpm-macros \
|
||||
python-wheel python-setuptools python-jinja2 \
|
||||
make pigz
|
||||
cd "$releasedir/"
|
||||
rpmbuild --define "_topdir `pwd`" -bb SPECS/copyparty.spec
|
||||
cd -
|
||||
|
||||
rpm="copyparty-$ver-1.noarch.rpm"
|
||||
mv "$releasedir/RPMS/noarch/$rpm" dist/$rpm
|
|
@ -41,7 +41,7 @@ help() { exec cat <<'EOF'
|
|||
# `no-cm` saves ~89k by removing easymde/codemirror
|
||||
# (the fancy markdown editor)
|
||||
#
|
||||
# `no-hl` saves ~41k by removing syntax hilighting in the text viewer
|
||||
# `no-hl` saves ~41k by removing syntax highlighting in the text viewer
|
||||
#
|
||||
# `no-fnt` saves ~9k by removing the source-code-pro font
|
||||
# (browsers will try to use 'Consolas' instead)
|
||||
|
@ -217,6 +217,8 @@ necho() {
|
|||
tar -zxf $f
|
||||
mv pyftpdlib-*/pyftpdlib .
|
||||
rm -rf pyftpdlib-* pyftpdlib/test
|
||||
patch -s -p1 <../scripts/patches/pyftpdlib-win313.patch
|
||||
patch -s -p1 <../scripts/patches/pyftpdlib-fe80.patch
|
||||
for f in pyftpdlib/_async{hat,ore}.py; do
|
||||
[ -e "$f" ] || continue;
|
||||
iawk 'NR<4||NR>27||!/^#/;NR==4{print"# license: https://opensource.org/licenses/ISC\n"}' $f
|
||||
|
|
37
scripts/patches/pyftpdlib-fe80.patch
Normal file
37
scripts/patches/pyftpdlib-fe80.patch
Normal file
|
@ -0,0 +1,37 @@
|
|||
accept connections from IPv6 link-local addresses
|
||||
|
||||
diff -NarU1 a/pyftpdlib/handlers.py b/pyftpdlib2/handlers.py
|
||||
--- a/pyftpdlib/handlers.py 2024-06-23 14:03:38
|
||||
+++ b/pyftpdlib/handlers.py 2025-08-22 21:59:40
|
||||
@@ -451,3 +451,4 @@
|
||||
|
||||
- local_ip = self.cmd_channel.socket.getsockname()[0]
|
||||
+ sockname = list(self.cmd_channel.socket.getsockname())
|
||||
+ local_ip = sockname[0]
|
||||
if local_ip in self.cmd_channel.masquerade_address_map:
|
||||
@@ -459,3 +460,5 @@
|
||||
|
||||
- if self.cmd_channel.server.socket.family != socket.AF_INET:
|
||||
+ if local_ip.startswith('fe') and local_ip[2:3] in "89ab":
|
||||
+ af = socket.AF_INET6 # link-local
|
||||
+ elif self.cmd_channel.server.socket.family != socket.AF_INET:
|
||||
# dual stack IPv4/IPv6 support
|
||||
@@ -472,3 +475,4 @@
|
||||
# free unprivileged random port.
|
||||
- self.bind((local_ip, 0))
|
||||
+ sockname[1] = 0
|
||||
+ self.bind(tuple(sockname))
|
||||
else:
|
||||
@@ -478,4 +482,5 @@
|
||||
self.set_reuse_addr()
|
||||
+ sockname[1] = port
|
||||
try:
|
||||
- self.bind((local_ip, port))
|
||||
+ self.bind(tuple(sockname))
|
||||
except PermissionError:
|
||||
@@ -495,3 +500,4 @@
|
||||
else:
|
||||
- self.bind((local_ip, 0))
|
||||
+ sockname[1] = 0
|
||||
+ self.bind(tuple(sockname))
|
||||
self.cmd_channel.log(
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue