Merge branch '9001:hovudstraum' into podman-systemd-service

This commit is contained in:
Daniel Lovegrove 2025-10-01 11:48:57 -05:00 committed by GitHub
commit f67e193cb0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
146 changed files with 17144 additions and 1871 deletions

3
.gitignore vendored
View file

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

50
.vscode/launch.json vendored
View file

@ -3,7 +3,7 @@
"configurations": [
{
"name": "Run copyparty",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "copyparty",
"console": "integratedTerminal",
@ -11,30 +11,46 @@
"justMyCode": false,
"env": {
"PYDEVD_DISABLE_FILE_VALIDATION": "1",
"PYTHONWARNINGS": "always", //error
"PYTHONWARNINGS": "always" //error
},
"args": [
//"-nw",
"-ed",
"-emp",
//"-nw", // no-write; for testing uploads without writing to disk
//"-q", // quiet; speedboost when console output is not needed
// # increase debugger performance:
//"no-htp",
//"hash-mt=0",
//"mtag-mt=1",
//"th-mt=1",
// # listen for FTP and TFTP
"--ftp=3921",
"--ftp-pr=12000-12099",
"--tftp=3969",
// # listen on all IPv6, all IPv4, and unix-socket
"-i::,unix:777:a.sock",
// # misc
"--dedup",
"-e2dsa",
"-e2ts",
"-mtp=.bpm=f,bin/mtag/audio-bpm.py",
"--rss",
"--shr=/shr",
"--stats",
"-z",
// # users + volumes
"-aed:wark",
"-vsrv::r:rw,ed:c,dupe",
"-vdist:dist:r"
"-vdist:dist:r",
"-vsrv::r:rw,ed",
"-vsrv/junk:junk:r:A,ed",
"--ver"
]
},
{
"name": "No debug",
"preLaunchTask": "no_dbg",
"type": "python",
//"request": "attach", "port": 42069
// fork: nc -l 42069 </dev/null
},
{
"name": "Run active unit test",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "unittest",
"console": "integratedTerminal",
@ -51,6 +67,6 @@
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": false
},
}
]
}

View file

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

230
README.md
View file

@ -50,6 +50,7 @@ made in Norway 🇳🇴
* [shares](#shares) - share a file or folder by creating a temporary link
* [batch rename](#batch-rename) - select some files and press `F2` to bring up the rename UI
* [rss feeds](#rss-feeds) - monitor a folder with your RSS reader
* [opds feeds](#opds-feeds) - browse and download files from your e-book reader
* [recent uploads](#recent-uploads) - list all recent uploads
* [media player](#media-player) - plays almost every audio format there is
* [playlists](#playlists) - create and play [m3u8](https://en.wikipedia.org/wiki/M3U) playlists
@ -90,7 +91,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 +113,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 +143,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)
@ -147,10 +152,13 @@ made in Norway 🇳🇴
just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** -- that's it! 🎉
> the sfx is a [self-extractor](https://github.com/9001/copyparty/issues/270) which unpacks an embedded `tar.gz` into `$TEMP` -- if this looks too scary, you can use the [zipapp](#zipapp) which has slightly worse performance
* or install through [pypi](https://pypi.org/project/copyparty/): `python3 -m pip install --user -U copyparty`
* 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)
@ -238,7 +246,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
@ -261,10 +269,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)
@ -436,6 +445,7 @@ upgrade notes
* can I link someone to a password-protected volume/file by including the password in the URL?
* yes, by adding `?pw=hunter2` to the end; replace `?` with `&` if there are parameters in the URL already, meaning it contains a `?` near the end
* if you have enabled `--usernames` then do `?pw=username:password` instead
* how do I stop `.hist` folders from appearing everywhere on my HDD?
* by default, a `.hist` folder is created inside each volume for the filesystem index, thumbnails, audio transcodes, and markdown document history. Use the `--hist` global-option or the `hist` volflag to move it somewhere else; see [database location](#database-location)
@ -510,16 +520,23 @@ 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`
* you can also `PRTY_CONFIG=foobar.conf python copyparty-sfx.py` (convenient in docker etc)
```yaml
[accounts]
u1: p1 # create account "u1" with password "p1"
u2: p2 # (note that comments must have
u3: p3 # two spaces before the # sign)
[groups]
g1: u1, u2 # create a group
[/] # this URL will be mapped to...
/srv # ...this folder on the server filesystem
accs:
@ -529,6 +546,8 @@ and if you want to use config files instead of commandline args (good!) then her
/mnt/music # which is mapped to this folder
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]
@ -555,6 +574,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
@ -817,7 +838,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
@ -849,6 +870,8 @@ the files will be hashed on the client-side, and each hash is sent to the server
files go into `[ok]` if they exist (and you get a link to where it is), otherwise they land in `[ng]`
* the main reason filesearch is combined with the uploader is cause the code was too spaghetti to separate it out somewhere else, this is no longer the case but now i've warmed up to the idea too much
if you have a "wark" (file-identifier/checksum) then you can also search for that in the [🔎] tab by putting `w = kFpDiztbZc8Z1Lzi` in the `raw` field
### unpost
@ -1009,6 +1032,7 @@ a feed example: https://cd.ocv.me/a/d2/d22/?rss&fext=mp3
url parameters:
* `pw=hunter2` for password auth
* if you enabled `--usernames` then do `pw=username:password` instead
* `recursive` to also include subfolders
* `title=foo` changes the feed title (default: folder name)
* `fext=mp3,opus` only include mp3 and opus files (default: all)
@ -1020,6 +1044,28 @@ url parameters:
* uppercase = reverse-sort; `M` = oldest file first
## opds feeds
browse and download files from your e-book reader
enabled with the `opds` volflag or `--opds` global option
add `?opds` to the end of the url you would like to browse, then input that in your opds client.
for example: `https://copyparty.example/books/?opds`.
to log in with a password, enter it into either of the username or password fields in your client.
- if you've enabled `--usernames`, then you need to enter both username and password .
note: some clients (e.g. Moon+ Reader) will not send the password when downloading cover images, which will
cause your ip to be banned by copyparty. to work around this, you can grant the [`g` permission](#accounts-and-volumes)
to unauthenticated requests and enable [filekeys](#filekeys) to prevent guessing filenames. for example:
`-vbooks:books:r,ed:g:c,fk,opds`
by default, not all file types will be listed in opds feeds. to change this, add the extension to
`--opds-exts` (volflag: `opds_exts`), or empty the list to list everything
## recent uploads
list all recent uploads by clicking "show recent uploads" in the controlpanel
@ -1048,7 +1094,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
@ -1085,6 +1131,9 @@ open the `[🎺]` media-player-settings tab to configure it,
* `[awo]` is `opus` in a `weba` file, good for iPhones (iOS 17.5 and newer) but Apple is still fixing some state-confusion bugs as of iOS 18.2.1
* `[caf]` is `opus` in a `caf` file, good for iPhones (iOS 11 through 17), technically unsupported by Apple but works for the most part
* `[mp3]` -- the myth, the legend, the undying master of mediocre sound quality that definitely works everywhere
* `[flac]` -- lossless but compressed, for LAN and/or fiber playback on electrostatic headphones
* `[wav]` -- lossless and uncompressed, for LAN and/or fiber playback on electrostatic headphones connected to very old equipment
* `flac` and `wav` must be enabled with `--allow-flac` / `--allow-wav` to allow spending the disk space
* "tint" reduces the contrast of the playback bar
@ -1219,7 +1268,7 @@ using arguments or config files, or a mix of both:
**NB:** as humongous as this readme is, there is also a lot of undocumented features. Run copyparty with `--help` to see all available global options; all of those can be used in the `[global]` section of config files, and everything listed in `--help-flags` can be used in volumes as volflags.
* if running in docker/podman, try this: `docker run --rm -it copyparty/ac --help`
* or see this (probably outdated): https://ocv.me/copyparty/helptext.html
* or see this: https://ocv.me/copyparty/helptext.html
* or if you prefer plaintext, https://ocv.me/copyparty/helptext.txt
@ -1276,6 +1325,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
@ -1291,6 +1346,7 @@ an FTP server can be started using `--ftp 3921`, and/or `--ftps` for explicit T
* if you enable both `ftp` and `ftps`, the port-range will be divided in half
* some older software (filezilla on debian-stable) cannot passive-mode with TLS
* login with any username + your password, or put your password in the username field
* unless you enabled `--usernames`
some recommended FTP / FTPS clients; `wark` = example password:
* https://winscp.net/eng/download.php
@ -1298,6 +1354,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
@ -1308,11 +1374,12 @@ click the [connect](http://127.0.0.1:3923/?hc) button in the control-panel to se
general usage:
* login with any username + your password, or put your password in the username field (password field can be empty/whatever)
* unless you enabled `--usernames`
on macos, connect from finder:
* [Go] -> [Connect to Server...] -> http://192.168.123.1:3923/
in order to grant full write-access to webdav clients, the volflag `daw` must be set and the account must also have delete-access (otherwise the client won't be allowed to replace the contents of existing files, which is how webdav works)
to upload or edit files with WebDAV clients, enable the `daw` volflag (because most WebDAV clients expect this) and give your account the delete-permission. This avoids getting several copies of the same file on the server. HOWEVER: This will also make all PUT-uploads overwrite existing files if the user has delete-access, so use with caution.
> note: if you have enabled [IdP authentication](#identity-providers) then that may cause issues for some/most webdav clients; see [the webdav section in the IdP docs](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients)
@ -1323,6 +1390,7 @@ using the GUI (winXP or later):
* rightclick [my computer] -> [map network drive] -> Folder: `http://192.168.123.1:3923/`
* on winXP only, click the `Sign up for online storage` hyperlink instead and put the URL there
* providing your password as the username is recommended; the password field can be anything or empty
* unless you enabled `--usernames`
the webdav client that's built into windows has the following list of bugs; you can avoid all of these by connecting with rclone instead:
* win7+ doesn't actually send the password to the server when reauthenticating after a reboot unless you first try to login with an incorrect password and then switch to the correct password
@ -1380,6 +1448,7 @@ some **BIG WARNINGS** specific to SMB/CIFS, in decreasing importance:
* the smb backend is not fully integrated with vfs, meaning there could be security issues (path traversal). Please use `--smb-port` (see below) and [prisonparty](./bin/prisonparty.sh) or [bubbleparty](./bin/bubbleparty.sh)
* account passwords work per-volume as expected, and so does account permissions (read/write/move/delete), but `--smbw` must be given to allow write-access from smb
* [shadowing](#shadowing) probably works as expected but no guarantees
* not compatible with pw-hashing or `--usernames`
and some minor issues,
* clients only see the first ~400 files in big folders;
@ -1390,6 +1459,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
@ -1426,6 +1496,8 @@ note that this disables hotlinking because the opengraph spec demands it; to sne
you can also hotlink files regardless by appending `?raw` to the url
> WARNING: if you plan to use WebDAV, then `--og-ua` / `og_ua` must be configured
if you want to entirely replace the copyparty response with your own jinja2 template, give the template filepath to `--og-tpl` or volflag `og_tpl` (all members of `HttpCli` are available through the `this` object)
@ -1508,6 +1580,7 @@ the same arguments can be set as volflags, in addition to `d2d`, `d2ds`, `d2t`,
note:
* upload-times can be displayed in the file listing by enabling the `.up_at` metadata key, either globally with `-e2d -mte +.up_at` or per-volume with volflags `e2d,mte=+.up_at` (will have a ~17% performance impact on directory listings)
* and file checksums can be shown with global-option `-e2d -mte +w` or volflag `e2d,mte=+w` (always active for users with permission `a`)
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise
config file example (these options are recommended btw):
@ -1591,6 +1664,7 @@ set upload rules using volflags, some examples:
* `:c,nosub` disallow uploading into subdirectories; goes well with `rotn` and `rotf`:
* `:c,rotn=1000,2` moves uploads into subfolders, up to 1000 files in each folder before making a new one, two levels deep (must be at least 1)
* `:c,rotf=%Y/%m/%d/%H` enforces files to be uploaded into a structure of subfolders according to that date format
* `:c,rotf_tz=Europe/Oslo` sets the timezone (default is UTC unless global-option `rotf-tz` is changed)
* if someone uploads to `/foo/bar` the path would be rewritten to `/foo/bar/2021/08/06/23` for example
* but the actual value is not verified, just the structure, so the uploader can choose any values which conform to the format string
* just to avoid additional complexity in up2k which is enough of a mess already
@ -1874,6 +1948,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
@ -1882,6 +1970,12 @@ 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)
there is a [docker-compose example](./docs/examples/docker/idp-authelia-traefik) which is hopefully a good starting point (alternatively see [./docs/idp.md](./docs/idp.md) if you're the DIY type)
@ -1891,6 +1985,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
@ -2044,7 +2152,11 @@ you can either:
* or do location-based proxying, using `--rp-loc=/stuff` to tell copyparty where it is mounted -- has a slight performance cost and higher chance of bugs
* if copyparty says `incorrect --rp-loc or webserver config; expected vpath starting with [...]` it's likely because the webserver is stripping away the proxy location from the request URLs -- see the `ProxyPass` in the apache example below
when running behind a reverse-proxy (this includes services like cloudflare), it is important to configure real-ip correctly, as many features rely on knowing the client's IP. Look out for red and yellow log messages which explain how to do this. But basically, set `--xff-hdr` to the name of the http header to read the IP from (usually `x-forwarded-for`, but cloudflare uses `cf-connecting-ip`), and then `--xff-src` to the IP of the reverse-proxy so copyparty will trust the xff-hdr. Note that `--rp-loc` in particular will not work at all unless you do this
when running behind a reverse-proxy (this includes services like cloudflare), it is important to configure real-ip correctly, as many features rely on knowing the client's IP. The best/safest approach is to configure your reverse-proxy so it gives copyparty a header which only contains the client's true/real IP-address, and then setting `--xff-hdr theHeaderName --rproxy 1` but alternatively, if you want/need to let copyparty handle this, look out for red and yellow log messages which explain how to do that. Basically, the log will say this:
> set `--xff-hdr` to the name of the http-header to read the IP from (usually `x-forwarded-for`, but cloudflare uses `cf-connecting-ip`), and then `--xff-src` to the IP of the reverse-proxy so copyparty will trust the xff-hdr. You will also need to configure `--rproxy` to `1` if the header only contains one IP (the correct one) or to a *negative value* if it contains multiple; `-1` being the rightmost and most trusted IP (the nearest proxy, so usually not the correct one), `-2` being the second-closest hop, and so on
Note that `--rp-loc` in particular will not work at all unless you configure the above correctly
some reverse proxies (such as [Caddy](https://caddyserver.com/)) can automatically obtain a valid https/tls certificate for you, and some support HTTP/2 and QUIC which *could* be a nice speed boost, depending on a lot of factors
* **warning:** nginx-QUIC (HTTP/3) is still experimental and can make uploads much slower, so HTTP/1.1 is recommended for now
@ -2102,7 +2214,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
@ -2238,6 +2350,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`
@ -2263,11 +2376,9 @@ if your distro/OS is not mentioned below, there might be some hints in the [«on
`pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/))
it comes with a [systemd service](./contrib/package/arch/copyparty.service) and expects to find one or more [config files](./docs/example.conf) in `/etc/copyparty.d/`
it comes with a [systemd service](./contrib/systemd/copyparty@.service) as well as a [user service](./contrib/systemd/copyparty-user.service), and expects to find a [config file](./contrib/systemd/copyparty.example.conf) in `/etc/copyparty/copyparty.conf` or `~/.config/copyparty/copyparty.conf`
after installing it, you may want to `cp /usr/lib/systemd/system/copyparty.service /etc/systemd/system/` and then `vim /etc/systemd/system/copyparty.service` to change what user/group it is running as (you only need to do this once)
NOTE: there used to be an aur package; this evaporated when copyparty was adopted by the official archlinux repos. If you're still using the aur package, please move
after installing, start either the system service or the user service and navigate to http://127.0.0.1:3923 for further instructions (unless you already edited the config files, in which case you are good to go, probably)
## fedora package
@ -2275,6 +2386,15 @@ NOTE: there used to be an aur package; this evaporated when copyparty was adopte
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`
@ -2288,7 +2408,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
{
@ -2315,6 +2435,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 = {
@ -2430,6 +2577,7 @@ quick summary of more eccentric web-browsers trying to view a directory index:
| **SerenityOS** (7e98457) | hits a page fault, works with `?b=u`, file upload not-impl |
| **sony psp** 5.50 | can browse, upload/mkdir/msg (thx dwarf) [screenshot](https://github.com/user-attachments/assets/9d21f020-1110-4652-abeb-6fc09c533d4f) |
| **nintendo 3ds** | can browse, upload, view thumbnails (thx bnjmn) |
| **Nintendo Wii (Opera 9.0 "Internet Channel")** | can browse, can't upload or download (no local storage), can view images - works best with `?b=u`, default view broken |
<p align="center"><img src="https://github.com/user-attachments/assets/88deab3d-6cad-4017-8841-2f041472b853" /></p>
@ -2489,6 +2637,8 @@ you can provide passwords using header `PW: hunter2`, cookie `cppwd=hunter2`, ur
> for basic-authentication, all of the following are accepted: `password` / `whatever:password` / `password:whatever` (the username is ignored)
* unless you've enabled `--usernames`, then it's `PW: usr:pwd`, cookie `cppwd=usr:pwd`, url-param `?pw=usr:pwd`
NOTE: curl will not send the original filename if you use `-T` combined with url-params! Also, make sure to always leave a trailing slash in URLs unless you want to override the filename
@ -2498,11 +2648,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
@ -2544,6 +2703,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
@ -2572,6 +2733,10 @@ below are some tweaks roughly ordered by usefulness:
* using [pypy](https://www.pypy.org/) instead of [cpython](https://www.python.org/) *can* be 70% faster for some workloads, but slower for many others
* and pypy can sometimes crash on startup with `-j0` (TODO make issue)
* if you are running the copyparty server **on Windows or Macos:**
* `--casechk=n` makes it much faster, but also awakens [the usual surprises](https://github.com/9001/copyparty/issues/781) you expect from a case-insensitive filesystem
* this is the same as `casechk: n` in a config-file
## client-side
@ -2600,7 +2765,7 @@ there is a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone`
some notes on hardening
* set `--rproxy 0` if your copyparty is directly facing the internet (not through a reverse-proxy)
* set `--rproxy 0` *if and only if* your copyparty is directly facing the internet (not through a reverse-proxy)
* cors doesn't work right otherwise
* if you allow anonymous uploads or otherwise don't trust the contents of a volume, you can prevent XSS with volflag `nohtml`
* this returns html documents as plaintext, and also disables markdown rendering
@ -2700,6 +2865,12 @@ optionally also specify `--ah-cli` to enter an interactive mode where it will ha
the default configs take about 0.4 sec and 256 MiB RAM to process a new password on a decent laptop
when generating hashes using `--ah-cli` for docker or systemd services, make sure it is using the same `--ah-salt` by:
* inspecting the generated salt using `--show-ah-salt` in copyparty service configuration
* setting the same `--ah-salt` in both environments
> ⚠️ if you have enabled `--usernames` then provide the password as `username:password` when hashing it, for example `ed:hunter2`
## https
@ -2761,9 +2932,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`
@ -2794,9 +2966,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`
@ -2817,6 +2990,8 @@ these are standalone programs and will never be imported / evaluated by copypart
the self-contained "binary" (recommended!) [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) will unpack itself and run copyparty, assuming you have python installed of course
if you only need english, [copyparty-en.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-en.py) is the same thing but smaller
you can reduce the sfx size by repacking it; see [./docs/devnotes.md#sfx-repack](./docs/devnotes.md#sfx-repack)
@ -2844,7 +3019,7 @@ then again, if you are already into downloading shady binaries from the internet
## zipapp
another emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz) has less features, is slow, requires python 3.7 or newer, worse compression, and more importantly is unable to benefit from more recent versions of jinja2 and such (which makes it less secure)... lots of drawbacks with this one really -- but it does not unpack any temporary files to disk, so it *may* just work if the regular sfx fails to start because the computer is messed up in certain funky ways, so it's worth a shot if all else fails
another emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz) has less features, is slow, requires python 3.7 or newer, worse compression, and more importantly is unable to benefit from more recent versions of jinja2 and such (which makes it less secure)... lots of drawbacks with this one really -- but, unlike the sfx, it is a completely normal zipfile which does not unpack any temporary files to disk, so it *may* just work if the regular sfx fails to start because the computer is messed up in certain funky ways, so it's worth a shot if all else fails
run it by doubleclicking it, or try typing `python copyparty.pyz` in your terminal/console/commandline/telex if that fails
@ -2867,6 +3042,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 -L https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh | sh
```
what this does:
* creates a basic [config file](#accounts-and-volumes) named `cpc` which you can edit with `vim cpc`
* adds the command `cpp` to launch copyparty with that config file
known issues:
* cannot run in the background; it needs to be on-screen to accept connections / uploads / downloads
* the best way to exit copyparty is to swipe away the app
# reporting bugs
ideas for context to include, and where to submit them

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,60 @@
#!/usr/bin/env python3
import json
import os
import re
import sys
_ = r"""
reject file upload (with a nice explanation why)
example usage as global config:
--xbu j,c1,bin/hooks/reject-and-explain.py
example usage as a volflag (per-volume config):
-v srv/inc:inc:r:rw,ed:c,xbu=j,c1,bin/hooks/reject-and-explain.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(share filesystem-path srv/inc as volume /inc,
readable by everyone, read-write for user 'ed',
running this plugin on all uploads with the params listed below)
example usage as a volflag in a copyparty config file:
[/inc]
srv/inc
accs:
r: *
rw: ed
flags:
xbu: j,c1,bin/hooks/reject-and-explain.py
parameters explained,
xbu = execute-before-upload (can also be xau, execute-after-upload)
j = this hook needs upload information as json (not just the filename)
c1 = this hook returns json on stdout, so tell copyparty to read that
"""
def main():
inf = json.loads(sys.argv[1])
vdir, fn = os.path.split(inf["vp"])
print("inf[vp] = %r" % (inf["vp"],), file=sys.stderr)
# the following is what decides if we'll accept the upload or reject it:
# we check if the upload-folder url matches the following regex-pattern:
ok = re.search(r"(^|/)day[0-9]+$", vdir, re.IGNORECASE)
if ok:
# allow the upload
print("{}")
return
# the upload was rejected; display the following errortext:
errmsg = "Files can only be uploaded into a folder named 'DayN' where N is a number, for example 'Day573'. This file was REJECTED: "
errmsg += inf["vp"] # if you want to mention the file's url
print(json.dumps({"rejectmsg": errmsg}))
if __name__ == "__main__":
main()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,12 @@
* works on windows, linux and macos
* assumes `copyparty-sfx.py` was renamed to `copyparty.py` in the same folder as `copyparty.bat`
### [`setup-ashell.sh`](setup-ashell.sh)
* run copyparty on an iPhone/iPad using [a-Shell](https://holzschu.github.io/a-Shell_iOS/)
* not very useful due to limitations in iOS:
* not able to share all of your phone's storage
* cannot run in the background
### [`index.html`](index.html)
* drop-in redirect from an httpd to copyparty
* assumes the webserver and copyparty is running on the same server/IP

View file

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

View file

@ -50,7 +50,9 @@ let
configStr = ''
${mkSection "global" cfg.settings}
${cfg.globalExtraConfig}
${mkSection "accounts" (accountsWithPlaceholders cfg.accounts)}
${mkSection "groups" cfg.groups}
${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)}
'';
@ -131,6 +133,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 (
@ -160,6 +168,19 @@ in
'';
};
groups = mkOption {
type = types.attrsOf (types.listOf types.str);
description = ''
A set of copyparty groups to create and the users that should be part of each group.
'';
default = { };
example = literalExpression ''
{
group_name = [ "user1" "user2" ];
};
'';
};
volumes = mkOption {
type = types.attrsOf (
types.submodule (
@ -373,3 +394,4 @@ in
}
);
}

View file

@ -1,57 +1,48 @@
# Maintainer: icxes <dev.null@need.moe>
# Contributor: Morgan Adamiec <morganamilo@archlinux.org>
# 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.18.9"
pkgver="1.19.15"
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any")
url="https://github.com/9001/${pkgname}"
license=('MIT')
depends=("python" "lsof" "python-jinja")
depends=("bash" "python" "lsof" "python-jinja")
makedepends=("python-wheel" "python-setuptools" "python-build" "python-installer" "make" "pigz")
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
"cfssl: generate TLS certificates on startup (pointless when reverse-proxied)"
"cfssl: generate TLS certificates on startup"
"python-mutagen: music tags (alternative)"
"python-pillow: thumbnails for images"
"python-pyvips: thumbnails for images (higher quality, faster, uses more ram)"
"libkeyfinder-git: detection of musical keys"
"qm-vamp-plugins: BPM detection"
"libkeyfinder: detection of musical keys"
"python-pyopenssl: ftps functionality"
"python-pyzmq: send zeromq messages from event-hooks"
"python-argon2-cffi: hashed passwords in config"
"python-impacket-git: smb support (bad idea)"
)
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("etc/${pkgname}.d/init" )
sha256sums=("d5d33b50d6717e52427956beb1687061a6f28b467997506505151e3ae18c58e5")
backup=("etc/${pkgname}/copyparty.conf" )
sha256sums=("95f4c1e517acaadd5fdc38b47047f16a7cd51495bd94ae9182274f256db59834")
build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
make
cd "${srcdir}/${pkgname}-${pkgver}"
pushd copyparty/web
make -j$(nproc)
rm Makefile
popd
python3 -m build -wn
python -m build --wheel --no-isolation
}
package() {
cd "${srcdir}/${pkgname}-${pkgver}"
python3 -m installer -d "$pkgdir" dist/*.whl
python -m installer --destdir="$pkgdir" dist/*.whl
install -dm755 "${pkgdir}/etc/${pkgname}.d"
install -dm755 "${pkgdir}/etc/${pkgname}"
install -Dm755 "bin/prisonparty.sh" "${pkgdir}/usr/bin/prisonparty"
install -Dm644 "contrib/package/arch/${pkgname}.conf" "${pkgdir}/etc/${pkgname}.d/init"
install -Dm644 "contrib/package/arch/${pkgname}.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}.service"
install -Dm644 "contrib/package/arch/prisonparty.service" "${pkgdir}/usr/lib/systemd/system/prisonparty.service"
install -Dm644 "contrib/package/arch/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md"
install -Dm644 "contrib/systemd/${pkgname}.conf" "${pkgdir}/etc/${pkgname}/copyparty.conf"
install -Dm644 "contrib/systemd/${pkgname}@.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}@.service"
install -Dm644 "contrib/systemd/${pkgname}-user.service" "${pkgdir}/usr/lib/systemd/user/${pkgname}.service"
install -Dm644 "contrib/systemd/prisonparty@.service" "${pkgdir}/usr/lib/systemd/system/prisonparty@.service"
install -Dm644 "contrib/systemd/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md"
install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
find /etc/${pkgname}.d -iname '*.conf' 2>/dev/null | grep -qE . && return
echo "┏━━━━━━━━━━━━━━━──-"
echo "┃ Configure ${pkgname} by adding .conf files into /etc/${pkgname}.d/"
echo "┃ and maybe copy+edit one of the following to /etc/systemd/system/:"
echo "┣━♦ /usr/lib/systemd/system/${pkgname}.service (standard)"
echo "┣━♦ /usr/lib/systemd/system/prisonparty.service (chroot)"
echo "┗━━━━━━━━━━━━━━━──-"
}

View file

@ -1,7 +0,0 @@
## import all *.conf files from the current folder (/etc/copyparty.d)
% ./
# add additional .conf files to this folder;
# see example config files for reference:
# https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf
# https://github.com/9001/copyparty/tree/hovudstraum/docs/copyparty.d

View file

@ -1,32 +0,0 @@
# this will start `/usr/bin/copyparty-sfx.py`
# and read config from `/etc/copyparty.d/*.conf`
#
# you probably want to:
# change "User=cpp" and "/home/cpp/" to another user
#
# unless you add -q to disable logging, you may want to remove the
# following line to allow buffering (slightly better performance):
# Environment=PYTHONUNBUFFERED=x
[Unit]
Description=copyparty file server
[Service]
Type=notify
SyslogIdentifier=copyparty
Environment=PYTHONUNBUFFERED=x
WorkingDirectory=/var/lib/copyparty-jail
ExecReload=/bin/kill -s USR1 $MAINPID
# user to run as + where the TLS certificate is (if any)
User=cpp
Environment=XDG_CONFIG_HOME=/home/cpp/.config
# stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
# run copyparty
ExecStart=/usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init
[Install]
WantedBy=multi-user.target

View file

@ -1,3 +0,0 @@
this is `/var/lib/copyparty-jail`, the fallback webroot when copyparty has not yet been configured
please add some `*.conf` files to `/etc/copyparty.d/`

View file

@ -2,7 +2,7 @@
pkgname=copyparty
pkgver=1.18.9
pkgver=1.19.15
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=("d5d33b50d6717e52427956beb1687061a6f28b467997506505151e3ae18c58e5")
sha256sums=("95f4c1e517acaadd5fdc38b47047f16a7cd51495bd94ae9182274f256db59834")
build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"

View file

@ -26,7 +26,7 @@ Environment=XDG_CONFIG_HOME=/home/cpp/.config
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
# run copyparty
ExecStart=/usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty -c /etc/copyparty.d/init
[Install]
WantedBy=multi-user.target

View file

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

View file

@ -1,5 +1,5 @@
{
"url": "https://github.com/9001/copyparty/releases/download/v1.18.9/copyparty-sfx.py",
"version": "1.18.9",
"hash": "sha256-R1OVx4f8GERAG80ZcHAIP6HK2TlBbKJZpvnJmJbGPRY="
"url": "https://github.com/9001/copyparty/releases/download/v1.19.15/copyparty-1.19.15.tar.gz",
"version": "1.19.15",
"hash": "sha256-lfTB5Resqt1f3Di0cEfxanzVFJW9lK6RgidPJW21mDQ="
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

@ -0,0 +1,26 @@
# this will start `/usr/bin/copyparty`
# and read config from `$HOME/.config/copyparty.conf`
#
# unless you add -q to disable logging, you may want to remove the
# following line to allow buffering (slightly better performance):
# Environment=PYTHONUNBUFFERED=x
[Unit]
Description=copyparty file server
[Service]
Type=notify
SyslogIdentifier=copyparty
WorkingDirectory=/var/lib/copyparty-jail
Environment=PYTHONUNBUFFERED=x
Environment=PRTY_CONFIG=%h/.config/copyparty/copyparty.conf
ExecReload=/bin/kill -s USR1 $MAINPID
# ensure there is a config
ExecStartPre=/bin/bash -c 'if [[ ! -f %h/.config/copyparty/copyparty.conf ]]; then mkdir -p %h/.config/copyparty; cp /etc/copyparty/copyparty.conf %h/.config/copyparty/copyparty.conf; fi'
# run copyparty
ExecStart=/usr/bin/python3 /usr/bin/copyparty
[Install]
WantedBy=default.target

View file

@ -1,42 +1,13 @@
# not actually YAML but lets pretend:
# -*- mode: yaml -*-
# vim: ft=yaml:
# put this file in /etc/
[global]
e2dsa # enable file indexing and filesystem scanning
e2ts # and enable multimedia indexing
ansi # and colors in log messages
# disable logging to stdout/journalctl and log to a file instead;
# $LOGS_DIRECTORY is usually /var/log/copyparty (comes from systemd)
# and copyparty replaces %Y-%m%d with Year-MonthDay, so the
# full path will be something like /var/log/copyparty/2023-1130.txt
# (note: enable compression by adding .xz at the end)
q, lo: $LOGS_DIRECTORY/%Y-%m%d.log
# p: 80,443,3923 # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE)
# i: 127.0.0.1 # only allow connections from localhost (reverse-proxies)
# ftp: 3921 # enable ftp server on port 3921
# p: 3939 # listen on another port
# df: 16 # stop accepting uploads if less than 16 GB free disk space
# ver # show copyparty version in the controlpanel
# grid # show thumbnails/grid-view by default
# theme: 2 # monokai
# name: datasaver # change the server-name that's displayed in the browser
# stats, nos-dup # enable the prometheus endpoint, but disable the dupes counter (too slow)
# no-robots, force-js # make it harder for search engines to read your server
i: 127.0.0.1
[accounts]
ed: wark # username: password
user: password
[/] # create a volume at "/" (the webroot), which will
/mnt # share the contents of the "/mnt" folder
[/]
/var/lib/copyparty-jail
accs:
rw: * # everyone gets read-write access, but
rwmda: ed # the user "ed" gets read-write-move-delete-admin
r: *
rwdma: user
flags:
grid

View file

@ -0,0 +1,42 @@
# not actually YAML but lets pretend:
# -*- mode: yaml -*-
# vim: ft=yaml:
# put this file in /etc/
[global]
e2dsa # enable file indexing and filesystem scanning
e2ts # and enable multimedia indexing
ansi # and colors in log messages
# disable logging to stdout/journalctl and log to a file instead;
# $LOGS_DIRECTORY is usually /var/log/copyparty (comes from systemd)
# and copyparty replaces %Y-%m%d with Year-MonthDay, so the
# full path will be something like /var/log/copyparty/2023-1130.txt
# (note: enable compression by adding .xz at the end)
q, lo: $LOGS_DIRECTORY/%Y-%m%d.log
# p: 80,443,3923 # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE)
# i: 127.0.0.1 # only allow connections from localhost (reverse-proxies)
# ftp: 3921 # enable ftp server on port 3921
# p: 3939 # listen on another port
# df: 16 # stop accepting uploads if less than 16 GB free disk space
# ver # show copyparty version in the controlpanel
# grid # show thumbnails/grid-view by default
# theme: 2 # monokai
# name: datasaver # change the server-name that's displayed in the browser
# stats, nos-dup # enable the prometheus endpoint, but disable the dupes counter (too slow)
# no-robots, force-js # make it harder for search engines to read your server
[accounts]
ed: wark # username: password
[/] # create a volume at "/" (the webroot), which will
/mnt # share the contents of the "/mnt" folder
accs:
rw: * # everyone gets read-write access, but
rwmda: ed # the user "ed" gets read-write-move-delete-admin

View file

@ -0,0 +1,30 @@
# this will start `/usr/bin/copyparty`
# and read config from `/etc/copyparty/copyparty.conf`
#
# the %i refers to whatever you put after the copyparty@
# so with copyparty@foo.service, %i == foo
#
# unless you add -q to disable logging, you may want to remove the
# following line to allow buffering (slightly better performance):
# Environment=PYTHONUNBUFFERED=x
[Unit]
Description=copyparty file server
[Service]
Type=notify
SyslogIdentifier=copyparty
WorkingDirectory=/var/lib/copyparty-jail
Environment=PYTHONUNBUFFERED=x
Environment=PRTY_CONFIG=/etc/copyparty/copyparty.conf
ExecReload=/bin/kill -s USR1 $MAINPID
# user to run as + where the TLS certificate is (if any)
User=%i
Environment=XDG_CONFIG_HOME=/home/%i/.config
# run copyparty
ExecStart=/usr/bin/python3 /usr/bin/copyparty
[Install]
WantedBy=multi-user.target

10
contrib/systemd/index.md Normal file
View file

@ -0,0 +1,10 @@
this is `/var/lib/copyparty-jail`, the fallback webroot when copyparty has not yet been configured
please edit `/etc/copyparty/copyparty.conf` (if running as a system service)
or `$HOME/.config/copyparty/copyparty.conf` if running as a user service
a basic configuration example is available at https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf
a configuration example that explains most flags is available at https://github.com/9001/copyparty/blob/hovudstraum/docs/chungus.conf
the full list of configuration options can be seen at https://ocv.me/copyparty/helptext.html
or by running `copyparty --help`

View file

@ -1,11 +1,13 @@
# this will start `/usr/bin/copyparty-sfx.py`
# this will start `/usr/bin/copyparty`
# in a chroot, preventing accidental access elsewhere,
# and read copyparty config from `/etc/copyparty.d/*.conf`
# and read copyparty config from `/etc/copyparty/copyparty.conf`
#
# expose additional filesystem locations to copyparty
# by listing them between the last `cpp` and `--`
# by listing them between the last `%i` and `--`
#
# `cpp cpp` = user/group to run copyparty as; can be IDs (1000 1000)
# `%i %i` = user/group to run copyparty as; can be IDs (1000 1000)
# the %i refers to whatever you put after the prisonparty@
# so with prisonparty@foo.service, %i == foo
#
# unless you add -q to disable logging, you may want to remove the
# following line to allow buffering (slightly better performance):
@ -15,19 +17,22 @@
Description=copyparty file server
[Service]
Type=notify
SyslogIdentifier=prisonparty
Environment=PYTHONUNBUFFERED=x
WorkingDirectory=/var/lib/copyparty-jail
Environment=PYTHONUNBUFFERED=x
Environment=PRTY_CONFIG=/etc/copyparty/copyparty.conf
ExecReload=/bin/kill -s USR1 $MAINPID
# stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
# user to run as + where the TLS certificate is (if any)
User=%i
Environment=XDG_CONFIG_HOME=/home/%i/.config
# run copyparty
ExecStart=/bin/bash /usr/bin/prisonparty /var/lib/copyparty-jail cpp cpp \
/etc/copyparty.d \
ExecStart=/bin/bash /usr/bin/prisonparty /var/lib/copyparty-jail %i %i \
/etc/copyparty \
-- \
/usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init
/usr/bin/python3 /usr/bin/copyparty
[Install]
WantedBy=multi-user.target

View file

@ -63,10 +63,6 @@ web/browser.js
web/browser2.html
web/cf.html
web/copyparty.gif
web/dd/2.png
web/dd/3.png
web/dd/4.png
web/dd/5.png
web/deps/busy.mp3
web/deps/easymde.css
web/deps/easymde.js
@ -92,6 +88,7 @@ web/mde.html
web/mde.js
web/msg.css
web/msg.html
web/opds.xml
web/rups.css
web/rups.html
web/rups.js
@ -115,7 +112,9 @@ class EnvParams(object):
def __init__(self) -> None:
self.t0 = time.time()
self.mod = ""
self.mod_ = ""
self.cfg = ""
self.scfg = True
E = EnvParams()

File diff suppressed because it is too large Load diff

View file

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

View file

@ -13,7 +13,7 @@ import threading
import time
from datetime import datetime
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, WINDOWS, E
from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, WINDOWS, E
from .bos import bos
from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap
from .pwhash import PWHash
@ -21,8 +21,10 @@ from .util import (
DEF_MTE,
DEF_MTH,
EXTS,
FAVICON_MIMES,
HAVE_SQLITE3,
IMPLICATIONS,
META_NOBOTS,
MIMES,
SQLITE_VER,
UNPLICATIONS,
@ -99,6 +101,8 @@ SBADCFG = " ({})".format(BAD_CFG)
PTN_U_GRP = re.compile(r"\$\{u(%[+-][^}]+)\}")
PTN_G_GRP = re.compile(r"\$\{g(%[+-][^}]+)\}")
PTN_U_ANY = re.compile(r"(\${[u][}%])")
PTN_G_ANY = re.compile(r"(\${[g][}%])")
PTN_SIGIL = re.compile(r"(\${[ug][}%])")
@ -167,14 +171,19 @@ class Lim(object):
self.rotn = 0 # rot num files
self.rotl = 0 # rot depth
self.rotf = "" # rot datefmt
self.rotf_tz = UTC # rot timezone
self.rot_re = re.compile("") # rotf check
def log(self, msg: str, c: Union[int, str] = 0) -> None:
if self.log_func:
self.log_func("up-lim", msg, c)
def set_rotf(self, fmt: str) -> None:
def set_rotf(self, fmt: str, tz: str) -> None:
self.rotf = fmt
if tz != "UTC":
from zoneinfo import ZoneInfo
self.rotf_tz = ZoneInfo(tz)
r = re.escape(fmt).replace("%Y", "[0-9]{4}").replace("%j", "[0-9]{3}")
r = re.sub("%[mdHMSWU]", "[0-9]{2}", r)
self.rot_re = re.compile("(^|/)" + r + "$")
@ -278,7 +287,7 @@ class Lim(object):
if self.rot_re.search(path.replace("\\", "/")):
return path, ""
suf = datetime.now(UTC).strftime(self.rotf)
suf = datetime.now(self.rotf_tz).strftime(self.rotf)
if path:
path += "/"
@ -424,10 +433,14 @@ class VFS(object):
self.all_nodes[vpath] = self
self.all_aps = [(rp, [self])]
self.all_vps = [(vp, self)]
self.canonical = self._canonical
self.dcanonical = self._dcanonical
else:
self.histpath = self.dbpath = ""
self.all_aps = []
self.all_vps = []
self.canonical = self._canonical_null
self.dcanonical = self._dcanonical_null
self.get_dbv = self._get_dbv
self.ls = self._ls
@ -624,7 +637,39 @@ 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 casechk(self, rem: str, do_stat: bool) -> bool:
ap = self.canonical(rem, False)
if do_stat and not bos.path.exists(ap):
return True # doesn't exist at all; good to go
dp, fn = os.path.split(ap)
if not fn:
return True # filesystem root
try:
fns = os.listdir(dp)
except:
return True # maybe chmod 111; assume ok
if fn in fns:
return True
hit = "<?>"
lfn = fn.lower()
for zs in fns:
if lfn == zs.lower():
hit = zs
break
if not hit:
return True # NFC/NFD or something, can't be helped either way
if self.log:
t = "returning 404 due to underlying case-insensitive filesystem:\n http-req: %r\n local-fs: %r"
self.log("vfs", t % (fn, hit))
return False
def _canonical_null(self, rem: str, resolve: bool = True) -> str:
return ""
def _dcanonical_null(self, rem: str) -> str:
return ""
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 +677,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 +686,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"]]:
@ -673,8 +756,12 @@ class VFS(object):
"""return user-readable [fsdir,real,virt] items at vpath"""
virt_vis = {} # nodes readable by user
abspath = self.canonical(rem)
if abspath:
real = list(statdir(self.log, scandir, lstat, abspath, throw))
real.sort()
else:
real = []
if not rem:
# no vfs nodes in the list of real inodes
real = [x for x in real if x[0] not in self.nodes]
@ -881,6 +968,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 +1063,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 +1109,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
@ -1071,6 +1178,16 @@ class AuthSrv(object):
src0 = src # abspath
dst0 = dst # vpath
zsl = []
for ptn, sigil in ((PTN_U_ANY, "${u}"), (PTN_G_ANY, "${g}")):
if bool(ptn.search(src)) != bool(ptn.search(dst)):
zsl.append(sigil)
if zsl:
t = "ERROR: if %s is mentioned in a volume definition, it must be included in both the filesystem-path [%s] and the volume-url [/%s]"
t = "\n".join([t % (x, src, dst) for x in zsl])
self.log(t, 1)
raise Exception(t)
un_gn = [(un, gn) for un, gns in un_gns.items() for gn in gns]
if not un_gn:
# ensure volume creation if there's no users
@ -1099,6 +1216,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
@ -1160,8 +1280,8 @@ class AuthSrv(object):
self.log(t, c=3)
raise Exception(BAD_CFG)
if not bos.path.isdir(src):
self.log("warning: filesystem-path does not exist: {}".format(src), 3)
if not bos.path.exists(src):
self.log("warning: filesystem-path did not exist: %r" % (src,), 3)
mount[dst] = (src, dst0)
daxs[dst] = AXS()
@ -1208,6 +1328,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 +1436,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 +1449,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 +1523,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 +1767,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 +1819,9 @@ class AuthSrv(object):
self.log("\n{0}\n{1}{0}".format(t, "\n".join(slns)))
raise
derive_args(self.args)
self.setup_auth_ord()
self.setup_pwhash(acct)
defpw = acct.copy()
self.setup_chpw(acct)
@ -1697,9 +1834,10 @@ 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 = ""
if self.is_lxc:
t = "Read-access has been disabled due to failsafe: Docker detected, but %s. This failsafe is to prevent unintended access if this is due to accidental loss of config. You can override this safeguard and allow read/write to all of /w/ by adding the following arguments to the docker container: -v .::rw"
if len(cfg_files_loaded) == 1:
@ -1709,11 +1847,26 @@ class AuthSrv(object):
else:
self.log(t % ("the config does not define any volumes",), 1)
axs = AXS()
ehint = "; please try moving them up one level, into the parent folder:"
elif self.args.c:
t = "Read-access has been disabled due to failsafe: No volumes were defined by the config-file. This failsafe is to prevent unintended access if this is due to accidental loss of config. You can override this safeguard and allow read/write to the working-directory by adding the following arguments: -v .::rw"
self.log(t, 1)
axs = AXS()
vfs = VFS(self.log_func, absreal("."), "", "", axs, self.vf0())
ehint = ":"
if ehint:
try:
files = os.listdir(E.cfg)
except:
files = []
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)
vfs = VFS(self.log_func, absreal("."), "", "", axs, self.vf0b())
if not axs.uread:
self.badcfg1 = True
elif "" not in mount:
@ -1749,7 +1902,7 @@ class AuthSrv(object):
vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
vol.root = vfs
zs = "neversymlink"
zs = "neversymlink du_iwho"
k_ign = set(zs.split())
for vol in vfs.all_vols.values():
unknown_flags = set()
@ -1857,7 +2010,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:
@ -1868,6 +2021,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:
@ -1891,6 +2054,8 @@ class AuthSrv(object):
promote = []
demote = []
for vol in vfs.all_vols.values():
if not vol.realpath:
continue
hid = self.hid_cache.get(vol.realpath)
if not hid:
zb = hashlib.sha512(afsenc(vol.realpath)).digest()
@ -1929,6 +2094,8 @@ class AuthSrv(object):
vol.histpath = absreal(vol.histpath)
for vol in vfs.all_vols.values():
if not vol.realpath:
continue
hid = self.hid_cache[vol.realpath]
vflag = vol.flags.get("dbpath")
if vflag == "-":
@ -2057,7 +2224,7 @@ class AuthSrv(object):
zs = vol.flags.get("rotf")
if zs:
use = True
lim.set_rotf(zs)
lim.set_rotf(zs, vol.flags.get("rotf_tz") or "UTC")
zs = vol.flags.get("maxn")
if zs:
@ -2188,12 +2355,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])
@ -2237,6 +2404,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"] = self.args.shr_who = "no"
if vol.flags.get("og"):
self.args.uqe = True
@ -2323,6 +2495,41 @@ class AuthSrv(object):
t = "WARNING: volume [/%s]: invalid value specified for ext-th: %s"
self.log(t % (vol.vpath, etv), 3)
zs = str(vol.flags.get("html_head") or "")
if zs and zs[:1] in "%@":
vol.flags["html_head_d"] = zs
head_s = str(vol.flags.get("html_head_s") or "")
else:
zs2 = str(vol.flags.get("html_head_s") or "")
if zs2 and zs:
head_s = "%s\n%s\n" % (zs2.strip(), zs.strip())
else:
head_s = zs2 or zs
if head_s and not head_s.endswith("\n"):
head_s += "\n"
if "norobots" in vol.flags:
head_s += META_NOBOTS
ico_url = vol.flags.get("ufavico")
if ico_url:
ico_ext = ico_url.split("?")[0].split(".")[-1].lower()
if ico_ext in FAVICON_MIMES:
zs = '<link rel="icon" type="%s" href="%s">\n'
head_s += zs % (FAVICON_MIMES[ico_ext], ico_url)
elif ico_ext == "ico":
zs = '<link rel="shortcut icon" href="%s">\n'
head_s += zs % (ico_url,)
if head_s:
vol.flags["html_head_s"] = head_s
else:
vol.flags.pop("html_head_s", None)
if not vol.flags.get("html_head_d"):
vol.flags.pop("html_head_d", None)
vol.check_landmarks()
# d2d drops all database features for a volume
@ -2412,6 +2619,47 @@ class AuthSrv(object):
self.log(t.format(vol.vpath, mtp), 1)
errors = True
for vol in vfs.all_nodes.values():
if not vol.realpath or os.path.isfile(vol.realpath):
continue
ccs = vol.flags["casechk"][:1].lower()
if ccs in ("y", "n"):
if ccs == "y":
vol.flags["bcasechk"] = True
continue
try:
bos.makedirs(vol.realpath, vf=vol.flags)
files = os.listdir(vol.realpath)
for fn in files:
fn2 = fn.lower()
if fn == fn2:
fn2 = fn.upper()
if fn == fn2 or fn2 in files:
continue
is_ci = os.path.exists(os.path.join(vol.realpath, fn2))
ccs = "y" if is_ci else "n"
break
if ccs not in ("y", "n"):
ap = os.path.join(vol.realpath, "casechk")
open(ap, "wb").close()
ccs = "y" if os.path.exists(ap[:-1] + "K") else "n"
os.unlink(ap)
except Exception as ex:
if ANYWIN:
zs = "Windows"
ccs = "y"
elif MACOS:
zs = "Macos"
ccs = "y"
else:
zs = "Linux"
ccs = "n"
t = "unable to determine if filesystem at %r is case-insensitive due to %r; assuming casechk=%s due to %s"
self.log(t % (vol.realpath, ex, ccs, zs), 3)
vol.flags["casechk"] = ccs
if ccs == "y":
vol.flags["bcasechk"] = True
tags = self.args.mtp or []
tags = [x.split("=")[0] for x in tags]
tags = [y for x in tags for y in x.split(",")]
@ -2515,7 +2763,11 @@ class AuthSrv(object):
if "dedup" in zv.flags:
have_dedup = True
if "e2d" not in zv.flags and "hardlink" not in zv.flags:
if (
"e2d" not in zv.flags
and "hardlink" not in zv.flags
and "reflink" not in zv.flags
):
unsafe_dedup.append("/" + zv.vpath)
t += "\n"
@ -2524,7 +2776,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))
@ -2629,6 +2881,8 @@ class AuthSrv(object):
self.re_pwd = None
pwds = [re.escape(x) for x in self.iacct.keys()]
pwds.extend(list(self.sesa))
if self.args.usernames:
pwds.extend([x.split(":", 1)[1] for x in pwds if ":" in x])
if pwds:
if self.ah.on:
zs = r"(\[H\] pw:.*|[?&]pw=)([^&]+)"
@ -2677,8 +2931,12 @@ 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
shn.canonical = shn._canonical
shn.dcanonical = shn._dcanonical
shn.shr_owner = s_un
shn.shr_src = (s_vfs, s_rem)
@ -2741,6 +2999,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 "",
@ -2749,14 +3008,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 "",
@ -2776,6 +3040,7 @@ class AuthSrv(object):
"dvol": self.args.au_vol,
"idxh": int(self.args.ih),
"dutc": not self.args.localtime,
"dfszf": self.args.ui_filesz,
"themes": self.args.themes,
"turbolvl": self.args.turbo,
"nosubtle": self.args.nosubtle,
@ -2807,10 +3072,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
@ -2866,7 +3143,10 @@ class AuthSrv(object):
n = []
q = "insert into us values (?,?,?)"
for uname in self.acct:
accs = list(self.acct)
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:
sid = ub64enc(os.urandom(blen)).decode("ascii")
cur.execute(q, (uname, sid, int(time.time())))
@ -2924,6 +3204,9 @@ class AuthSrv(object):
t = "minimum password length: %d characters"
return False, t % (self.args.chpw_len,)
if self.args.usernames:
pw = "%s:%s" % (uname, pw)
hpw = self.ah.hash(pw) if self.ah.on else pw
if hpw == self.acct[uname]:
@ -3015,6 +3298,12 @@ class AuthSrv(object):
self.log("chpw: " + msg, 6)
def setup_pwhash(self, acct: dict[str, str]) -> None:
if self.args.usernames:
for uname, pw in list(acct.items())[:]:
if pw.startswith("+") and len(pw) == 33:
continue
acct[uname] = "%s:%s" % (uname, pw)
self.ah = PWHash(self.args)
if not self.ah.on:
if self.args.ah_cli or self.args.ah_gen:
@ -3387,6 +3676,35 @@ class AuthSrv(object):
self.log("generated config:\n\n" + "\n".join(ret))
def derive_args(args: argparse.Namespace) -> None:
args.have_idp_hdrs = bool(args.idp_h_usr or args.idp_hm_usr)
args.have_ipu_or_ipr = bool(args.ipu or args.ipr)
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 = {}
@ -3419,7 +3737,9 @@ def expand_config_file(
if os.path.isdir(fp):
names = list(sorted(os.listdir(fp)))
cnames = [x for x in names if x.lower().endswith(".conf")]
cnames = [
x for x in names if x.lower().endswith(".conf") and not x.startswith(".")
]
if not cnames:
t = "warning: tried to read config-files from folder '%s' but it does not contain any "
if names:

View file

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

View file

@ -12,6 +12,7 @@ import queue
from .__init__ import ANYWIN
from .authsrv import AuthSrv
from .broker_util import BrokerCli, ExceptionalQueue, NotExQueue
from .fsutil import ramdisk_chk
from .httpsrv import HttpSrv
from .util import FAKE_MP, Daemon, HMaccas
@ -56,6 +57,7 @@ class MpWorker(BrokerCli):
# starting to look like a good idea
self.asrv = AuthSrv(args, None, False)
ramdisk_chk(self.asrv)
# instantiate all services here (TODO: inheritance?)
self.iphash = HMaccas(os.path.join(self.args.E.cfg, "iphash"), 8)
@ -99,6 +101,7 @@ class MpWorker(BrokerCli):
if dest == "reload":
self.logw("mpw.asrv reloading")
self.asrv.reload()
ramdisk_chk(self.asrv)
self.logw("mpw.asrv reloaded")
continue

View file

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

View file

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

View file

@ -44,6 +44,7 @@ def vf_bmap() -> dict[str, str]:
"gsel",
"hardlink",
"magic",
"md_no_br",
"no_db_ip",
"no_sb_md",
"no_sb_lg",
@ -51,11 +52,14 @@ def vf_bmap() -> dict[str, str]:
"og",
"og_no_head",
"og_s_title",
"opds",
"rand",
"reflink",
"rm_partial",
"rmagic",
"rss",
"wo_up_readme",
"wram",
"xdev",
"xlink",
"xvol",
@ -68,6 +72,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",
@ -79,12 +84,16 @@ def vf_vmap() -> dict[str, str]:
}
for k in (
"bup_ck",
"casechk",
"chmod_d",
"chmod_f",
"dbd",
"du_who",
"ufavico",
"forget_ip",
"hsortn",
"html_head",
"html_head_s",
"lg_sbf",
"md_sbf",
"lg_sba",
@ -101,21 +110,26 @@ def vf_vmap() -> dict[str, str]:
"og_title_i",
"og_tpl",
"og_ua",
"opds_exts",
"put_ck",
"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",
@ -179,8 +193,10 @@ flagcats = {
"chmod_f=644": "unix-permission for new files",
"uid=573": "change owner of new files/folders to unix-user 573",
"gid=999": "change owner of new files/folders to unix-group 999",
"wram": "allow uploading into ramdisks",
"sparse": "force use of sparse files, mainly for s3-backed storage",
"nosparse": "deny use of sparse files, mainly for slow storage",
"rm_partial": "delete unfinished uploads from HDD when they timeout",
"daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
"nosub": "forces all uploads into the top folder of the vfs",
"magic": "enables filetype detection for nameless uploads",
@ -209,6 +225,7 @@ flagcats = {
"upload rotation\n(moves all uploads into the specified folder structure)": {
"rotn=100,3": "3 levels of subfolders with 100 entries in each",
"rotf=%Y-%m/%d-%H": "date-formatted organizing",
"rotf_tz=Europe/Oslo": "timezone (default=UTC)",
"lifetime=3600": "uploads are deleted after 1 hour",
},
"database, general": {
@ -237,6 +254,7 @@ flagcats = {
"no_db_ip": "never store uploader-IP in the db; disables unpost",
"fat32": "avoid excessive reindexing on android sdcardfs",
"dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
"casechk=auto": "actively prevent case-insensitive filesystem? y/n",
"xlink": "cross-volume dupe detection / linking (dangerous)",
"xdev": "do not descend into other filesystems",
"xvol": "do not follow symlinks leaving the volume root",
@ -259,7 +277,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)": {
@ -285,10 +305,13 @@ flagcats = {
"sort": "default sort order",
"nsort": "natural-sort of leading digits in filenames",
"hsortn": "number of sort-rules to add to media URLs",
"ufavico=URL": "per-volume favicon (.ico/png/gif/svg)",
"unlist": "dont list files matching REGEX",
"html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH",
"html_head_s=TXT": "additional static text in the html <head>",
"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",
@ -317,11 +340,17 @@ flagcats = {
"og_no_head": "you want to add tags manually with og_tpl",
"og_ua": "if defined: only send OG html if useragent matches this regex",
},
"opds": {
"opds": "enable OPDS",
"opds_exts": "file formats to list in OPDS feeds; leave empty to show everything",
},
"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",
"exp_lg": "placeholders to expand in prologue/epilogue; see --help",
"txt_eol=lf": "enable EOL conversion when writing docs (LF or CRLF)",
},
"tailing": {
"notail": "disable ?tail (download a growing file continuously)",
@ -339,6 +368,8 @@ flagcats = {
"dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so',
"rss": "allow '?rss' URL suffix (experimental)",
"rmagic": "expensive analysis for mimetype accuracy",
"shr_who=auth": "who can create shares? no/auth/a",
"unp_who=2": "unpost only if same... 1=ip+name, 2=ip, 3=name",
"ups_who=2": "restrict viewing the list of recent uploads",
"zip_who=2": "restrict access to download-as-zip/tar",
"zipmaxn=9k": "reject download-as-zip if more than 9000 files",

View file

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

View file

@ -7,7 +7,7 @@ import re
import time
from .__init__ import ANYWIN, MACOS
from .authsrv import AXS, VFS
from .authsrv import AXS, VFS, AuthSrv
from .bos import bos
from .util import chkcmd, min_ex, undot
@ -18,22 +18,25 @@ if True: # pylint: disable=using-constant-test
class Fstab(object):
def __init__(self, log: "RootLogger", args: argparse.Namespace):
def __init__(self, log: "RootLogger", args: argparse.Namespace, verbose: bool):
self.log_func = log
self.verbose = verbose
self.warned = False
self.trusted = False
self.tab: Optional[VFS] = None
self.oldtab: Optional[VFS] = None
self.srctab = "a"
self.cache: dict[str, str] = {}
self.cache: dict[str, tuple[str, str]] = {}
self.age = 0.0
self.maxage = args.mtab_age
def log(self, msg: str, c: Union[int, str] = 0) -> None:
if not c or self.verbose:
return
self.log_func("fstab", msg, c)
def get(self, path: str) -> str:
def get(self, path: str) -> tuple[str, str]:
now = time.time()
if now - self.age > self.maxage or len(self.cache) > 9000:
self.age = now
@ -41,6 +44,7 @@ class Fstab(object):
self.tab = None
self.cache = {}
mp = ""
fs = "ext4"
msg = "failed to determine filesystem at %r; assuming %s\n%s"
@ -50,7 +54,7 @@ class Fstab(object):
path = self._winpath(path)
except:
self.log(msg % (path, fs, min_ex()), 3)
return fs
return fs, ""
path = undot(path)
try:
@ -59,14 +63,14 @@ class Fstab(object):
pass
try:
fs = self.get_w32(path) if ANYWIN else self.get_unix(path)
fs, mp = self.get_w32(path) if ANYWIN else self.get_unix(path)
except:
self.log(msg % (path, fs, min_ex()), 3)
fs = fs.lower()
self.cache[path] = fs
self.log("found %s at %r" % (fs, path))
return fs
self.cache[path] = (fs, mp)
self.log("found %s at %r, %r" % (fs, mp, path))
return fs, mp
def _winpath(self, path: str) -> str:
# try to combine volume-label + st_dev (vsn)
@ -81,34 +85,49 @@ class Fstab(object):
self.tab = VFS(self.log_func, "idk", "/", "/", AXS(), {})
self.trusted = False
def build_tab(self) -> None:
self.log("inspecting mtab for changes")
def _from_sp_mount(self) -> dict[str, str]:
sptn = r"^.*? on (.*) type ([^ ]+) \(.*"
if MACOS:
sptn = r"^.*? on (.*) \(([^ ]+), .*"
ptn = re.compile(sptn)
so, _ = chkcmd(["mount"])
tab1: list[tuple[str, str]] = []
atab = []
dtab: dict[str, str] = {}
for ln in so.split("\n"):
m = ptn.match(ln)
if not m:
continue
zs1, zs2 = m.groups()
tab1.append((str(zs1), str(zs2)))
atab.append(ln)
dtab[str(zs1)] = str(zs2)
return dtab
def _from_proc(self) -> dict[str, str]:
ret: dict[str, str] = {}
with open("/proc/self/mounts", "rb", 262144) as f:
src = f.read(262144).decode("utf-8", "replace").split("\n")
for zsl in [x.split(" ") for x in src]:
if len(zsl) < 3:
continue
zs = zsl[1]
zs = zs.replace("\\011", "\t").replace("\\040", " ").replace("\\134", "\\")
ret[zs] = zsl[2]
return ret
def build_tab(self) -> None:
self.log("inspecting mtab for changes")
dtab = self._from_sp_mount() if MACOS else self._from_proc()
# keep empirically-correct values if mounttab unchanged
srctab = "\n".join(sorted(atab))
srctab = str(sorted(dtab.items()))
if srctab == self.srctab:
self.tab = self.oldtab
return
self.log("mtab has changed; reevaluating support for sparse files")
tab1 = list(dtab.items())
tab1.sort(key=lambda x: (len(x[0]), x[0]))
path1, fs1 = tab1[0]
tab = VFS(self.log_func, fs1, path1, path1, AXS(), {})
@ -146,7 +165,7 @@ class Fstab(object):
vn.realpath = ptn.sub(nval, vn.realpath)
visit.extend(list(vn.nodes.values()))
def get_unix(self, path: str) -> str:
def get_unix(self, path: str) -> tuple[str, str]:
if not self.tab:
try:
self.build_tab()
@ -155,20 +174,44 @@ class Fstab(object):
# prisonparty or other restrictive environment
if not self.warned:
self.warned = True
self.log("failed to build tab:\n{}".format(min_ex()), 3)
t = "failed to associate fs-mounts with the VFS (this is fine):\n%s"
self.log(t % (min_ex(),), 6)
self.build_fallback()
assert self.tab # !rm
ret = self.tab._find(path)[0]
if self.trusted or path == ret.vpath:
return ret.realpath.split("/")[0]
return ret.realpath.split("/")[0], ret.vpath
else:
return "idk"
return "idk", ""
def get_w32(self, path: str) -> str:
def get_w32(self, path: str) -> tuple[str, str]:
if not self.tab:
self.build_fallback()
assert self.tab # !rm
ret = self.tab._find(path)[0]
return ret.realpath
return ret.realpath, ""
def ramdisk_chk(asrv: AuthSrv) -> None:
# should have been in authsrv but that's a circular import
mods = []
ramfs = ("tmpfs", "overlay")
log = asrv.log_func or print
fstab = Fstab(log, asrv.args, False)
for vn in asrv.vfs.all_nodes.values():
if not vn.axs.uwrite or "wram" in vn.flags:
continue
ap = vn.realpath
if not ap or os.path.isfile(ap):
continue
fs, mp = fstab.get(ap)
mp = "/" + mp.strip("/")
if fs == "tmpfs" or (mp == "/" and fs in ramfs):
mods.append((vn.vpath, ap, fs, mp))
vn.axs.uwrite.clear()
if mods:
t = "WARNING: write-access was removed from the following volumes because they are not mapped to an actual HDD for storage! All uploaded data would live in RAM only, and all uploaded files would be LOST on next reboot. To allow uploading and ignore this hazard, enable the 'wram' option (global/volflag). List of affected volumes:"
t2 = ["\n volume=[/%s], abspath=%r, type=%s, root=%r" % x for x in mods]
log("vfs", t + "".join(t2) + "\n", 1)

View file

@ -68,13 +68,13 @@ class FtpAuth(DummyAuthorizer):
if ip.startswith("::ffff:"):
ip = ip[7:]
ip = ipnorm(ip)
ipn = ipnorm(ip)
bans = self.hub.bans
if ip in bans:
rt = bans[ip] - time.time()
if ipn in bans:
rt = bans[ipn] - time.time()
if rt < 0:
logging.info("client unbanned")
del bans[ip]
del bans[ipn]
else:
raise AuthenticationFailed("banned")
@ -83,7 +83,12 @@ class FtpAuth(DummyAuthorizer):
uname = "*"
if username != "anonymous":
uname = ""
for zs in (password, username):
if args.usernames:
alts = ["%s:%s" % (username, password)]
else:
alts = password, username
for zs in alts:
zs = asrv.iacct.get(asrv.ah.hash(zs), "")
if zs:
uname = zs
@ -91,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
@ -143,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(".")
@ -197,6 +202,9 @@ class FtpFs(AbstractedFS):
if r and not cr or w and not cw or m and not cm or d and not cd:
raise FSE(t.format(vpath), 1)
if "bcasechk" in vfs.flags and not vfs.casechk(rem, True):
raise FSE("No such file or directory", 1)
return os.path.join(vfs.realpath, rem), vfs, rem
except Pebkac as ex:
raise FSE(str(ex))
@ -209,7 +217,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)
@ -271,6 +279,10 @@ class FtpFs(AbstractedFS):
def chdir(self, path: str) -> None:
nwd = join(self.cwd, path)
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
if not vfs.realpath:
self.cwd = nwd
return
ap = vfs.canonical(rem)
try:
st = bos.stat(ap)
@ -285,16 +297,6 @@ class FtpFs(AbstractedFS):
raise FSE("Permission denied", 1)
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)
@ -317,7 +319,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()
@ -365,16 +367,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))
@ -398,7 +397,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]
@ -487,10 +486,15 @@ class FtpHandler(FTPHandler):
def ftp_STOR(self, file: str, mode: str = "w") -> Any:
# Optional[str]
vp = join(self.fs.cwd, file).lstrip("/")
try:
ap, vfs, rem = self.fs.v2a(vp, w=True)
except Exception as ex:
self.respond("550 %s" % (ex,), logging.info)
return
self.vfs_map[ap] = vp
xbu = vfs.flags.get("xbu")
if xbu and not runhook(
if xbu:
hr = runhook(
None,
None,
self.hub.up2k,
@ -506,8 +510,13 @@ class FtpHandler(FTPHandler):
self.cli_ip,
time.time(),
"",
):
raise FSE("Upload blocked by xbu server config")
)
t = hr.get("rejectmsg") or ""
if t or not hr:
if not t:
t = "Upload blocked by xbu server config: %r" % (vp,)
self.respond("550 %s" % (t,), logging.info)
return
# print("ftp_STOR: {} {} => {}".format(vp, mode, ap))
ret = FTPHandler.ftp_STOR(self, file, mode)
@ -607,7 +616,7 @@ class Ftpd(object):
if "::" in ips:
ips.append("0.0.0.0")
ips = [x for x in ips if "unix:" not in x]
ips = [x for x in ips if not x.startswith(("unix:", "fd:"))]
if self.args.ftp4:
ips = [x for x in ips if ":" not in x]

File diff suppressed because it is too large Load diff

View file

@ -70,6 +70,7 @@ from .util import (
build_netmap,
has_resource,
ipnorm,
load_ipr,
load_ipu,
load_resource,
min_ex,
@ -186,6 +187,7 @@ class HttpSrv(object):
"svcs",
]
self.j2 = {x: env.get_template(x + ".html") for x in jn}
self.j2["opds"] = env.get_template("opds.xml")
self.prism = has_resource(self.E, "web/deps/prism.js.gz")
if self.args.ipu:
@ -193,6 +195,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")
@ -324,7 +331,8 @@ class HttpSrv(object):
spins = 0
while self.ncli >= self.nclimax:
if not spins:
self.log(self.name, "at connection limit; waiting", 3)
t = "at connection limit (global-option 'nc'); waiting"
self.log(self.name, t, 3)
spins += 1
time.sleep(0.1)
@ -564,7 +572,7 @@ class HttpSrv(object):
v = self.E.t0
try:
with os.scandir(os.path.join(self.E.mod, "web")) as dh:
with os.scandir(self.E.mod_ + "web") as dh:
for fh in dh:
inf = fh.stat()
v = max(v, inf.st_mtime)

View file

@ -12,7 +12,9 @@ from ipaddress import IPv4Network, IPv6Network
from .__init__ import TYPE_CHECKING
from .__init__ import unicode as U
from .multicast import MC_Sck, MCast
from .stolen.dnslib import AAAA
from .stolen.dnslib import (
AAAA,
)
from .stolen.dnslib import CLASS as DC
from .stolen.dnslib import (
NSEC,
@ -27,7 +29,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 +78,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))
@ -99,9 +102,14 @@ class MDNS(MCast):
self.log_func(self.logsrc, msg, c)
def build_svcs(self) -> tuple[dict[str, dict[str, Any]], set[str]]:
ar = self.args
zms = self.args.zms
http = {"port": 80 if 80 in self.args.p else self.args.p[0]}
https = {"port": 443 if 443 in self.args.p else self.args.p[0]}
zi = ar.zm_http
http = {"port": zi if zi != -1 else 80 if 80 in ar.p else ar.p[0]}
zi = ar.zm_https
https = {"port": zi if zi != -1 else 443 if 443 in ar.p else ar.p[0]}
webdav = http.copy()
webdavs = https.copy()
webdav["u"] = webdavs["u"] = "u" # KDE requires username
@ -126,16 +134,16 @@ class MDNS(MCast):
svcs: dict[str, dict[str, Any]] = {}
if "d" in zms:
if "d" in zms and http["port"]:
svcs["_webdav._tcp.local."] = webdav
if "D" in zms:
if "D" in zms and https["port"]:
svcs["_webdavs._tcp.local."] = webdavs
if "h" in zms:
if "h" in zms and http["port"]:
svcs["_http._tcp.local."] = http
if "H" in zms:
if "H" in zms and https["port"]:
svcs["_https._tcp.local."] = https
if "f" in zms.lower():
@ -374,7 +382,7 @@ class MDNS(MCast):
cip = addr[0]
v6 = ":" in cip
if (cip.startswith("169.254") and not self.ll_ok) or (
v6 and not cip.startswith("fe80")
v6 and not cip.startswith(IP6_LL)
):
return

View file

@ -29,7 +29,7 @@ from .util import (
)
if True: # pylint: disable=using-constant-test
from typing import Any, Optional, Union
from typing import IO, Any, Optional, Union
from .util import NamedLogger, RootLogger
@ -67,6 +67,8 @@ HAVE_FFPROBE = not os.environ.get("PRTY_NO_FFPROBE") and have_ff("ffprobe")
CBZ_PICS = set("png jpg jpeg gif bmp tga tif tiff webp avif".split())
CBZ_01 = re.compile(r"(^|[^0-9v])0+[01]\b")
FMT_AU = set("mp3 ogg flac wav".split())
class MParser(object):
def __init__(self, cmdline: str) -> None:
@ -174,6 +176,10 @@ def au_unpk(
raise Exception("no images inside cbz")
fi = zf.open(using)
elif pk == "epub":
fi = get_cover_from_epub(log, abspath)
assert fi # !rm
else:
raise Exception("unknown compression %s" % (pk,))
@ -203,7 +209,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",
@ -217,8 +223,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 = {}
@ -242,7 +257,7 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
ret: dict[str, Any] = {} # processed
md: dict[str, list[Any]] = {} # raw tags
is_audio = fmt.get("format_name") in ["mp3", "ogg", "flac", "wav"]
is_audio = fmt.get("format_name") in FMT_AU
if fmt.get("filename", "").split(".")[-1].lower() in ["m4a", "aac"]:
is_audio = True
@ -270,6 +285,8 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
["channel_layout", "chs"],
["sample_rate", ".hz"],
["bit_rate", ".aq"],
["bits_per_sample", ".bps"],
["bits_per_raw_sample", ".bprs"],
["duration", ".dur"],
]
@ -309,7 +326,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():
@ -358,7 +375,76 @@ 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>
xn = package_root.find("./metadata/meta[@name='cover']", package_ns)
cover_id = xn.get("content") if xn is not None else None
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):
@ -424,7 +510,6 @@ class MTag(object):
"album-artist",
"tpe2",
"aart",
"conductor",
"organization",
"band",
],
@ -629,7 +714,7 @@ class MTag(object):
if not bos.path.isfile(abspath):
return {}
ret, md = ffprobe(abspath, self.args.mtag_to)
ret, md, _, _ = ffprobe(abspath, self.args.mtag_to)
if self.args.mtag_vv:
for zd in (ret, dict(md)):

View file

@ -15,7 +15,7 @@ from ipaddress import (
)
from .__init__ import MACOS, TYPE_CHECKING
from .util import Daemon, Netdev, find_prefix, min_ex, spack
from .util import IP6_LL, IP64_LL, Daemon, Netdev, find_prefix, min_ex, spack
if TYPE_CHECKING:
from .svchub import SvcHub
@ -96,7 +96,10 @@ class MCast(object):
def create_servers(self) -> list[str]:
bound: list[str] = []
netdevs = self.hub.tcpsrv.netdevs
ips = [x[0] for x in self.hub.tcpsrv.bound]
blist = self.hub.tcpsrv.bound
if self.args.http_no_tcp:
blist = self.hub.tcpsrv.seen_eps
ips = [x[0] for x in blist]
if "::" in ips:
ips = [x for x in ips if x != "::"] + list(
@ -145,7 +148,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,11 +186,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") or k.startswith("fe80")
}
ll = {k: v for k, v in srv.ips.items() if k.startswith(IP64_LL)}
rt = {k: v for k, v in srv.ips.items() if k not in ll}
if self.args.ll or not rt:

View file

@ -25,6 +25,7 @@ class PWHash(object):
self.args = args
zsl = args.ah_alg.split(",")
zsl = [x.strip() for x in zsl]
alg = zsl[0]
if alg == "none":
alg = ""
@ -147,6 +148,10 @@ class PWHash(object):
def cli(self) -> None:
import getpass
if self.args.usernames:
t = "since you have enabled --usernames, please provide username:password"
print(t)
while True:
try:
p1 = getpass.getpass("password> ")

View file

@ -246,7 +246,8 @@ class SMB(object):
ap = absreal(ap)
xbu = vfs.flags.get("xbu")
if xbu and not runhook(
if xbu:
hr = runhook(
self.nlog,
None,
self.hub.up2k,
@ -262,8 +263,12 @@ class SMB(object):
"1.7.6.2",
time.time(),
"",
):
yeet("blocked by xbu server config: %r" % (vpath,))
)
t = hr.get("rejectmsg") or ""
if t or not hr:
if not t:
t = "blocked by xbu server config: %r" % (vpath,)
yeet(t)
ret = bos.open(ap, flags, *a, mode=chmod, **ka)
if wr:
@ -318,7 +323,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 +378,7 @@ class SMB(object):
t = "blocked utime (no-write-acc %s): /%s @%s"
yeet(t % (vfs.axs.uwrite, vpath, uname))
return bos.utime(ap, times)
bos.utime_c(info, ap, int(times[1]), False)
def _p_exists(self, vpath: str) -> bool:
# ap = "?"

View file

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

View file

@ -2,6 +2,7 @@
from __future__ import print_function, unicode_literals
import argparse
import atexit
import errno
import logging
import os
@ -26,9 +27,10 @@ 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, derive_args, n_du_who, n_ver_who
from .bos import bos
from .cert import ensure_cert
from .fsutil import ramdisk_chk
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN
from .pwhash import HAVE_ARGON2
from .tcpsrv import TcpSrv
@ -38,6 +40,7 @@ from .th_srv import (
HAVE_FFPROBE,
HAVE_HEIF,
HAVE_PIL,
HAVE_RAW,
HAVE_VIPS,
HAVE_WEBP,
ThumbSrv,
@ -64,6 +67,7 @@ from .util import (
build_netmap,
expat_ver,
gzip,
load_ipr,
load_ipu,
lock_file,
min_ex,
@ -72,6 +76,7 @@ from .util import (
pybin,
start_log_thrs,
start_stackmon,
termsize,
ub64enc,
)
@ -130,6 +135,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
@ -151,7 +157,8 @@ class SvcHub(object):
args.unpost = 0
args.no_del = True
args.no_mv = True
args.hardlink = True
args.reflink = True
args.dav_auth = True
args.vague_403 = True
args.nih = True
@ -240,8 +247,8 @@ 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:
t = "ERROR: user-changeable passwords is incompatible with IdP/identity-providers; you must disable either --chpw or --idp-h-usr"
if args.chpw and args.have_idp_hdrs and "pw" not in args.auth_ord.split(","):
t = "ERROR: user-changeable passwords is not compatible with your current configuration. Choose one of these options to fix it:\n option1: disable --chpw\n option2: remove all use of IdP features; --idp-*\n option3: change --auth-ord to something like pw,idp,ipu"
self.log("root", t, 1)
raise Exception(t)
@ -256,6 +263,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 +276,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 +290,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", "")
@ -292,6 +311,7 @@ class SvcHub(object):
# initiate all services to manage
self.asrv = AuthSrv(self.args, self.log, dargs=self.dargs)
ramdisk_chk(self.asrv)
if args.cgen:
self.asrv.cgen()
@ -316,11 +336,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)
@ -369,7 +391,10 @@ class SvcHub(object):
t = "invalid mp3 transcoding quality [%s] specified; only supports [0] to disable, a CBR value such as [192k], or a CQ/CRF value such as [v2]"
raise Exception(t % (args.q_mp3,))
else:
args.au_unpk = {}
zss = set(args.th_r_ffa.split(",") + args.th_r_ffv.split(","))
args.au_unpk = {
k: v for k, v in args.au_unpk.items() if v.split(".")[0] not in zss
}
args.th_poke = min(args.th_poke, args.th_maxage, args.ac_maxage)
@ -422,11 +447,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 +800,84 @@ 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)
if self.args.qr_stdout:
self.pr(self.tcpsrv.qr)
if self.args.qr_stderr:
self.pr(self.tcpsrv.qr, file=sys.stderr)
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 +890,17 @@ class SvcHub(object):
break
if self.tcpsrv.qr:
if self.args.qr_pin:
self.sticky_qr()
if self.args.qr_wait or self.args.qr_every or self.args.qr_winch:
Daemon(self._qr_thr, "qr")
else:
if not self.args.qr_pin:
self.log("qr-code", self.tcpsrv.qr)
if self.args.qr_stdout:
self.pr(self.tcpsrv.qr)
if self.args.qr_stderr:
self.pr(self.tcpsrv.qr, file=sys.stderr)
else:
self.log("root", "workers OK\n")
@ -811,6 +927,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 += [
@ -850,15 +967,6 @@ class SvcHub(object):
def _check_env(self) -> None:
al = self.args
try:
files = os.listdir(E.cfg)
except:
files = []
hits = [x for x in files if x.lower().endswith(".conf")]
if hits:
t = "WARNING: found config files in [%s]: %s\n config files are not expected here, and will NOT be loaded (unless your setup is intentionally hella funky)"
self.log("root", t % (E.cfg, ", ".join(hits)), 3)
if self.args.no_bauth:
t = "WARNING: --no-bauth disables support for the Android app; you may want to use --bauth-last instead"
@ -868,7 +976,7 @@ class SvcHub(object):
have_tcp = False
for zs in al.i:
if not zs.startswith("unix:"):
if not zs.startswith(("unix:", "fd:")):
have_tcp = True
if not have_tcp:
zb = False
@ -878,7 +986,7 @@ class SvcHub(object):
setattr(al, zs, False)
zb = True
if zb:
t = "only listening on unix-sockets; cannot enable zeroconf/mdns/ssdp as requested"
t = "not listening on any ip-addresses (only unix-sockets and/or FDs); cannot enable zeroconf/mdns/ssdp as requested"
self.log("root", t, 3)
if not self.args.no_dav:
@ -890,6 +998,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
@ -978,10 +1104,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)
@ -1033,7 +1172,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)
@ -1050,6 +1189,7 @@ class SvcHub(object):
zi2 = zi
al.u2sz = ",".join(zsl)
derive_args(al)
return True
def _ipa2re(self, txt) -> Optional[re.Pattern]:
@ -1225,6 +1365,7 @@ class SvcHub(object):
with self.reload_mutex:
self.log("root", "reloading config")
self.asrv.reload(9 if up2k else 4)
ramdisk_chk(self.asrv)
if up2k:
self.up2k.reload(rescan_all_vols)
t += "; volumes are now reinitializing"
@ -1409,7 +1550,14 @@ class SvcHub(object):
fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n"
if self.no_ansi:
fmt = "%s %-21s %s\n"
if c == 1:
fmt = "%s %-21s CRIT: %s\n"
elif c == 3:
fmt = "%s %-21s WARN: %s\n"
elif c == 6:
fmt = "%s %-21s BTW: %s\n"
else:
fmt = "%s %-21s LOG: %s\n"
if "\033" in msg:
msg = RE_ANSI.sub("", msg)
if "\033" in src:

View file

@ -9,13 +9,14 @@ import time
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
from .cert import gencert
from .stolen.qrcodegen import QrCode
from .stolen.qrcodegen import QrCode, qr2svg
from .util import (
E_ACCESS,
E_ADDR_IN_USE,
E_ADDR_NOT_AVAIL,
E_UNREACH,
HAVE_IPV6,
IP6_LL,
IP6ALL,
VF_CAREFUL,
Netdev,
@ -25,8 +26,8 @@ from .util import (
termsize,
)
if True:
from typing import Generator, Union
if True: # pylint: disable=using-constant-test
from typing import Generator, Optional, Union
if TYPE_CHECKING:
from .svchub import SvcHub
@ -58,6 +59,7 @@ class TcpSrv(object):
self.stopping = False
self.srv: list[socket.socket] = []
self.bound: list[tuple[str, int]] = []
self.seen_eps: list[tuple[str, int]] = [] # also skipped by uds-only
self.netdevs: dict[str, Netdev] = {}
self.netlist = ""
self.nsrv = 0
@ -140,12 +142,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 +166,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):
@ -245,8 +247,10 @@ class TcpSrv(object):
def _listen(self, ip: str, port: int) -> None:
uds_perm = uds_gid = -1
if "unix:" in ip:
bound: Optional[socket.socket] = None
tcp = False
if "unix:" in ip:
ipv = socket.AF_UNIX
uds = ip.split(":")
ip = uds[-1]
@ -259,7 +263,12 @@ class TcpSrv(object):
import grp
uds_gid = grp.getgrnam(uds[2]).gr_gid
elif "fd:" in ip:
fd = ip[3:]
bound = socket.socket(fileno=int(fd))
tcp = bound.proto == socket.IPPROTO_TCP
ipv = bound.family
elif ":" in ip:
tcp = True
ipv = socket.AF_INET6
@ -267,7 +276,7 @@ class TcpSrv(object):
tcp = True
ipv = socket.AF_INET
srv = socket.socket(ipv, socket.SOCK_STREAM)
srv = bound or socket.socket(ipv, socket.SOCK_STREAM)
if not ANYWIN or self.args.reuseaddr:
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@ -285,14 +294,25 @@ class TcpSrv(object):
if getattr(self.args, "freebind", False):
srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1)
if bound:
self.srv.append(srv)
return
try:
if tcp:
if self.args.http_no_tcp:
self.seen_eps.append((ip, port))
return
srv.bind((ip, port))
else:
if ANYWIN or self.args.rm_sck:
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):
@ -393,6 +413,7 @@ class TcpSrv(object):
self.srv = srvs
self.bound = bound
self.seen_eps = list(set(self.seen_eps + bound))
self.nsrv = len(srvs)
self._distribute_netdevs()
@ -437,7 +458,7 @@ class TcpSrv(object):
def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]:
from .stolen.ifaddr import get_adapters
listen_ips = [x for x in listen_ips if "unix:" not in x]
listen_ips = [x for x in listen_ips if not x.startswith(("unix:", "fd:"))]
nics = get_adapters(True)
eps: dict[str, Netdev] = {}
@ -583,8 +604,7 @@ class TcpSrv(object):
if not ip:
return ""
if ":" in ip:
ip = "[{}]".format(ip)
hip = "[%s]" % (ip,) if ":" in ip else ip
if self.args.http_only:
https = ""
@ -596,7 +616,7 @@ class TcpSrv(object):
ports = t1.get(ip, t2.get(ip, []))
dport = 443 if https else 80
port = "" if dport in ports or not ports else ":{}".format(ports[0])
txt = "http{}://{}{}/{}".format(https, ip, port, self.args.qrl)
txt = "http{}://{}{}/{}".format(https, hip, port, self.args.qrl)
btxt = txt.encode("utf-8")
if PY2:
@ -604,9 +624,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()
@ -622,6 +650,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)
@ -631,6 +661,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:
@ -638,3 +670,29 @@ class TcpSrv(object):
t = t.replace("\n", "`\n`")
return txt + t
def _qr2file(self, qrc: QrCode, txt: str):
if ".txt:" in txt or ".svg:" in txt:
ap, zs1, zs2 = txt.rsplit(":", 2)
bg = fg = ""
else:
ap, zs1, zs2, bg, fg = txt.rsplit(":", 4)
zoom = int(zs1)
pad = int(zs2)
if ap.endswith(".txt"):
if zoom not in (1, 2):
raise Exception("invalid zoom for qr.txt; must be 1 or 2")
with open(ap, "wb") as f:
f.write(qrc.render(zoom, pad).encode("utf-8"))
elif ap.endswith(".svg"):
with open(ap, "wb") as f:
f.write(qr2svg(qrc, pad).encode("utf-8"))
else:
qrc.to_png(zoom, pad, self._h2i(bg), self._h2i(fg), ap)
def _h2i(self, hs):
try:
return tuple(int(hs[i : i + 2], 16) for i in (0, 2, 4))
except:
return None

View file

@ -179,7 +179,7 @@ class Tftpd(object):
if "::" in ips:
ips.append("0.0.0.0")
ips = [x for x in ips if "unix:" not in x]
ips = [x for x in ips if not x.startswith(("unix:", "fd:"))]
if self.args.tftp4:
ips = [x for x in ips if ":" not in x]
@ -363,7 +363,8 @@ class Tftpd(object):
yeet("blocked write; folder not world-deletable: /%s" % (vpath,))
xbu = vfs.flags.get("xbu")
if xbu and not runhook(
if xbu:
hr = runhook(
self.nlog,
None,
self.hub.up2k,
@ -379,8 +380,12 @@ class Tftpd(object):
"8.3.8.7",
time.time(),
"",
):
yeet("blocked by xbu server config: %r" % (vpath,))
)
t = hr.get("rejectmsg") or ""
if t or not hr:
if not t:
t = "upload blocked by xbu server config: %r" % (vpath,)
yeet(t)
if not self.args.tftp_nols and bos.path.isdir(ap):
return self._ls(vpath, "", 0, True)

View file

@ -36,11 +36,15 @@ class ThumbCli(object):
if not c:
raise Exception()
except:
c = {k: set() for k in ["thumbable", "pil", "vips", "ffi", "ffv", "ffa"]}
c = {
k: set()
for k in ["thumbable", "pil", "vips", "raw", "ffi", "ffv", "ffa"]
}
self.thumbable = c["thumbable"]
self.fmt_pil = c["pil"]
self.fmt_vips = c["vips"]
self.fmt_raw = c["raw"]
self.fmt_ffi = c["ffi"]
self.fmt_ffv = c["ffv"]
self.fmt_ffa = c["ffa"]
@ -88,7 +92,7 @@ class ThumbCli(object):
if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg", "png"]:
return os.path.join(ptop, rem)
if fmt[:1] in "jw":
if fmt[:1] in "jw" and fmt != "wav":
sfmt = fmt[:1]
if sfmt == "j" and self.args.th_no_jpg:
@ -129,7 +133,7 @@ class ThumbCli(object):
tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa)
tpaths = [tpath]
if fmt[:1] == "w":
if fmt[:1] == "w" and fmt != "wav":
# also check for jpg (maybe webp is unavailable)
tpaths.append(tpath.rsplit(".", 1)[0] + ".jpg")

View file

@ -2,6 +2,7 @@
from __future__ import print_function, unicode_literals
import hashlib
import io
import logging
import os
import re
@ -50,7 +51,7 @@ HAVE_AVIF = False
HAVE_WEBP = False
EXTS_TH = set(["jpg", "webp", "png"])
EXTS_AC = set(["opus", "owa", "caf", "mp3"])
EXTS_AC = set(["opus", "owa", "caf", "mp3", "flac", "wav"])
EXTS_SPEC_SAFE = set("aif aiff flac mp3 opus wav".split())
PTN_TS = re.compile("^-?[0-9a-f]{8,10}$")
@ -85,6 +86,9 @@ try:
if os.environ.get("PRTY_NO_PIL_HEIF"):
raise Exception()
try:
from pillow_heif import register_heif_opener
except ImportError:
from pyheif_pillow_opener import register_heif_opener
register_heif_opener()
@ -112,14 +116,28 @@ except:
try:
if os.environ.get("PRTY_NO_VIPS"):
raise Exception()
raise ImportError()
HAVE_VIPS = True
import pyvips
logging.getLogger("pyvips").setLevel(logging.WARNING)
except:
except Exception as e:
HAVE_VIPS = False
if not isinstance(e, ImportError):
logging.warning("libvips found, but failed to load: " + str(e))
try:
if os.environ.get("PRTY_NO_RAW"):
raise Exception()
HAVE_RAW = True
import rawpy
logging.getLogger("rawpy").setLevel(logging.WARNING)
except:
HAVE_RAW = False
th_dir_cache = {}
@ -205,11 +223,19 @@ class ThumbSrv(object):
if self.args.th_clean:
Daemon(self.cleaner, "thumb.cln")
self.fmt_pil, self.fmt_vips, self.fmt_ffi, self.fmt_ffv, self.fmt_ffa = [
(
self.fmt_pil,
self.fmt_vips,
self.fmt_raw,
self.fmt_ffi,
self.fmt_ffv,
self.fmt_ffa,
) = [
set(y.split(","))
for y in [
self.args.th_r_pil,
self.args.th_r_vips,
self.args.th_r_raw,
self.args.th_r_ffi,
self.args.th_r_ffv,
self.args.th_r_ffa,
@ -232,6 +258,9 @@ class ThumbSrv(object):
if "vips" in self.args.th_dec:
self.thumbable |= self.fmt_vips
if "raw" in self.args.th_dec:
self.thumbable |= self.fmt_raw
if "ff" in self.args.th_dec:
for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]:
self.thumbable |= zss
@ -313,6 +342,7 @@ class ThumbSrv(object):
"thumbable": self.thumbable,
"pil": self.fmt_pil,
"vips": self.fmt_vips,
"raw": self.fmt_raw,
"ffi": self.fmt_ffi,
"ffv": self.fmt_ffv,
"ffa": self.fmt_ffa,
@ -355,8 +385,10 @@ class ThumbSrv(object):
tex = tpath.rsplit(".", 1)[-1]
want_mp3 = tex == "mp3"
want_opus = tex in ("opus", "owa", "caf")
want_flac = tex == "flac"
want_wav = tex == "wav"
want_png = tex == "png"
want_au = want_mp3 or want_opus
want_au = want_mp3 or want_opus or want_flac or want_wav
for lib in self.args.th_dec:
can_au = lib == "ff" and (
ext in self.fmt_ffa or ext in self.fmt_ffv
@ -366,11 +398,17 @@ 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)
elif want_mp3:
funs.append(self.conv_mp3)
elif want_flac:
funs.append(self.conv_flac)
elif want_wav:
funs.append(self.conv_wav)
elif want_png:
funs.append(self.conv_waves)
png_ok = True
@ -474,9 +512,7 @@ class ThumbSrv(object):
return im
def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath)
with Image.open(fsenc(abspath)) as im:
def conv_image_pil(self, im: "Image.Image", tpath: str, fmt: str, vn: VFS) -> None:
try:
im = self.fancy_pillow(im, fmt, vn)
except Exception as ex:
@ -504,6 +540,11 @@ class ThumbSrv(object):
im.save(tpath, **args)
def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath)
with Image.open(fsenc(abspath)) as im:
self.conv_image_pil(im, tpath, fmt, vn)
def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath)
crops = ["centre", "none"]
@ -525,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
@ -538,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"
@ -556,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",
@ -577,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
@ -625,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")
@ -663,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"
@ -684,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
@ -724,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:
@ -766,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()
@ -774,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")
@ -805,14 +921,74 @@ 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, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in tags:
raise Exception("not audio")
self.log("conv2 flac", 6)
# fmt: off
cmd = [
b"ffmpeg",
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
b"-i", fsenc(abspath),
b"-map", b"0:a:0",
b"-c:a", b"flac",
fsenc(tpath)
]
# fmt: on
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, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in tags:
raise Exception("not audio")
bits = tags[".bps"][1]
if bits == 0.0:
bits = tags[".bprs"][1]
codec = b"pcm_s32le"
if bits <= 16.0:
codec = b"pcm_s16le"
elif bits <= 24.0:
codec = b"pcm_s24le"
self.log("conv2 wav", 6)
# fmt: off
cmd = [
b"ffmpeg",
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
b"-i", fsenc(abspath),
b"-map", b"0:a:0",
b"-c:a", codec,
fsenc(tpath)
]
# fmt: on
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")
@ -861,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,
@ -901,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
@ -928,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
@ -947,7 +1123,7 @@ class ThumbSrv(object):
fsenc(tpath)
]
# fmt: on
self._run_ff(cmd, vn, oom=300)
self._run_ff(cmd, vn, "aconvt", oom=300)
try:
wunlink(self.log, tmp_opus, vn.flags)

View file

@ -53,6 +53,11 @@ class U2idx(object):
self.log("your python does not have sqlite3; searching will be disabled")
return
if self.args.srch_icase:
self._open_db = self._open_db_icase
else:
self._open_db = self._open_db_std
assert sqlite3 # type: ignore # !rm
self.active_id = ""
@ -69,6 +74,16 @@ class U2idx(object):
def log(self, msg: str, c: Union[int, str] = 0) -> None:
self.log_func("u2idx", msg, c)
def _open_db_std(self, *args, **kwargs):
assert sqlite3 # type: ignore # !rm
kwargs["check_same_thread"] = False
return sqlite3.connect(*args, **kwargs)
def _open_db_icase(self, *args, **kwargs):
db = self._open_db_std(*args, **kwargs)
db.create_function("casefold", 1, lambda x: x.casefold() if x else x)
return db
def shutdown(self) -> None:
if not HAVE_SQLITE3:
return
@ -148,8 +163,7 @@ class U2idx(object):
uri = ""
try:
uri = "{}?mode=ro&nolock=1".format(Path(db_path).as_uri())
db = sqlite3.connect(uri, timeout=2, uri=True, check_same_thread=False)
cur = db.cursor()
cur = self._open_db(uri, timeout=2, uri=True).cursor()
cur.execute('pragma table_info("up")').fetchone()
self.log("ro: %r" % (db_path,))
except:
@ -160,7 +174,7 @@ class U2idx(object):
if not cur:
# on windows, this steals the write-lock from up2k.deferred_init --
# seen on win 10.0.17763.2686, py 3.10.4, sqlite 3.37.2
cur = sqlite3.connect(db_path, timeout=2, check_same_thread=False).cursor()
cur = self._open_db(db_path, timeout=2).cursor()
self.log("opened %r" % (db_path,))
self.cur[ptop] = cur
@ -173,6 +187,8 @@ class U2idx(object):
if not HAVE_SQLITE3:
return [], [], False
icase = self.args.srch_icase
q = ""
v: Union[str, int] = ""
va: list[Union[str, int]] = []
@ -180,6 +196,7 @@ class U2idx(object):
is_key = True
is_size = False
is_date = False
is_wark = False
field_end = "" # closing parenthesis or whatever
kw_key = ["(", ")", "and ", "or ", "not "]
kw_val = ["==", "=", "!=", ">", ">=", "<", "<=", "like "]
@ -198,6 +215,8 @@ class U2idx(object):
is_key = kw in kw_key
uq = uq[len(kw) :]
ok = True
if is_wark:
kw = "= "
q += kw
break
@ -232,9 +251,17 @@ class U2idx(object):
elif v == "path":
v = "trim(?||up.rd,'/')"
va.append("\nrd")
if icase:
v = "casefold(%s)" % (v,)
elif v == "name":
v = "up.fn"
if icase:
v = "casefold(%s)" % (v,)
elif v == "w":
v = "substr(up.w,1,16)"
is_wark = True
elif v == "tags" or ptn_mt.match(v):
have_mt = True
@ -247,7 +274,7 @@ class U2idx(object):
v = "exists(select 1 from mt where mt.w = mtw and " + vq
else:
raise Pebkac(400, "invalid key [{}]".format(v))
raise Pebkac(400, "invalid key %r" % (v,))
q += v + " "
continue
@ -276,6 +303,14 @@ class U2idx(object):
is_size = False
v = int(float(v) * 1024 * 1024)
elif is_wark:
is_wark = False
v = v.strip("*")
if len(v) > 16:
v = v[:16]
if len(v) < 16:
raise Pebkac(400, "w/filehash must be 16+ chars")
else:
if v.startswith("*"):
head = "'%'||"
@ -285,6 +320,12 @@ class U2idx(object):
tail = "||'%'"
v = v[:-1]
if icase and "casefold(" in q:
try:
v = unicode(v).casefold()
except:
v = unicode(v).lower()
q += " {}?{} ".format(head, tail)
va.append(v)
is_key = True
@ -319,7 +360,7 @@ class U2idx(object):
uname: str,
vols: list[VFS],
uq: str,
uv: list[Union[str, int]],
uv: Union[list[str], list[Union[str, int]]],
have_mt: bool,
sort: bool,
lim: int,
@ -391,7 +432,7 @@ class U2idx(object):
fk_alg = 2 if "fka" in flags else 1
c = cur.execute(uq, tuple(vuv))
for hit in c:
w, ts, sz, rd, fn, ip, at = hit[:7]
w, ts, sz, rd, fn = hit[:5]
if rd.startswith("//") or fn.startswith("//"):
rd, fn = s3dec(rd, fn)

View file

@ -18,13 +18,15 @@ from copy import deepcopy
from queue import Queue
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, WINDOWS, E
from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, WINDOWS, E
from .authsrv import LEELOO_DALLAS, SEESLOG, VFS, AuthSrv
from .bos import bos
from .cfg import vf_bmap, vf_cmap, vf_vmap
from .fsutil import Fstab
from .mtag import MParser, MTag
from .util import (
E_FS_CRIT,
E_FS_MEH,
HAVE_SQLITE3,
SYMTIME,
VF_CAREFUL,
@ -60,6 +62,7 @@ from .util import (
sfsenc,
spack,
statdir,
trystat_shutil_copy2,
ub64enc,
unhumanize,
vjoin,
@ -77,7 +80,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
@ -86,7 +89,13 @@ if TYPE_CHECKING:
from .svchub import SvcHub
zsg = "avif,avifs,bmp,gif,heic,heics,heif,heifs,ico,j2p,j2k,jp2,jpeg,jpg,jpx,png,tga,tif,tiff,webp"
CV_EXTS = set(zsg.split(","))
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(" "))
@ -141,6 +150,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
@ -205,7 +215,7 @@ class Up2k(object):
t = "could not initialize sqlite3, will use in-memory registry only"
self.log(t, 3)
self.fstab = Fstab(self.log_func, self.args)
self.fstab = Fstab(self.log_func, self.args, True)
self.gen_fk = self._gen_fk if self.args.log_fk else gen_filekey
if self.args.hash_mt < 2:
@ -372,11 +382,12 @@ class Up2k(object):
if ineed == ihash or not ineed:
continue
poke = job["poke"]
zt = (
ineed / ihash,
job["size"],
int(job["t0c"]),
int(job["poke"]),
int(job.get("t0c", poke)),
int(poke),
djoin(vtop, job["prel"], job["name"]),
)
ret.append(zt)
@ -406,10 +417,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 ""
@ -898,7 +910,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()
@ -918,6 +930,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)
@ -1124,7 +1142,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 = "bcasechk du_iwho ext_th_d html_head html_head_d html_head_s 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())
@ -1478,10 +1496,12 @@ class Up2k(object):
unreg: list[str] = []
files: list[tuple[int, int, str]] = []
fat32 = True
cv = ""
cv = vcv = acv = ""
e_d = {}
th_cvd = self.args.th_coversd
th_cvds = self.args.th_coversd_set
scan_pr_s = self.args.scan_pr_s
assert self.pp and self.mem_cur # !rm
self.pp.msg = "a%d %s" % (self.pp.n, cdir)
@ -1573,25 +1593,26 @@ class Up2k(object):
rsz += sz
files.append((sz, lmod, iname))
if sz:
liname = iname.lower()
ext = liname.rsplit(".", 1)[-1]
if (
sz
and (
liname in th_cvds
or (
not cv
and liname.rsplit(".", 1)[-1] in CV_EXTS
and not iname.startswith(".")
)
)
and (
or (not cv and ext in ICV_EXTS and not iname.startswith("."))
) and (
not cv
or liname not in th_cvds
or cv.lower() not in th_cvds
or th_cvd.index(liname) < th_cvd.index(cv.lower())
)
):
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 or acv
if not self.args.no_dirsz:
tnf += len(files)
@ -1651,7 +1672,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:
@ -1660,7 +1681,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])
@ -1673,9 +1694,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 = []
@ -1683,13 +1707,14 @@ class Up2k(object):
dw = ""
ip = ""
at = 0
un = ""
self.pp.msg = "a%d %s" % (self.pp.n, abspath)
if nohash or not sz:
wark = up2k_wark_from_metadata(self.salt, sz, lmod, rd, fn)
else:
if sz > 1024 * 1024:
if sz > 1024 * 1024 * scan_pr_s:
self.log("file: %r" % (abspath,))
try:
@ -1697,7 +1722,7 @@ class Up2k(object):
abspath, "a{}, ".format(self.pp.n)
)
except Exception as ex:
self.log("hash: %r @ %r" % (ex, abspath))
self._ex_hash(ex, abspath)
continue
if not hashes:
@ -1708,9 +1733,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
@ -1770,7 +1796,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))
@ -1957,7 +1983,7 @@ class Up2k(object):
try:
hashes, _ = self._hashlist_from_file(abspath, pf)
except Exception as ex:
self.log("hash: %r @ %r" % (ex, abspath))
self._ex_hash(ex, abspath)
continue
if not hashes:
@ -2147,8 +2173,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
@ -2167,12 +2193,15 @@ class Up2k(object):
abspath = djoin(ptop, rd, fn)
self.pp.msg = "c%d %s" % (nq, abspath)
if not mpool:
n_tags = self._tagscan_file(cur, entags, w, abspath, ip, at)
else:
if ip:
oth_tags = {"up_ip": ip, "up_at": at}
n_tags = self._tagscan_file(cur, entags, w, abspath, ip, at, un)
else:
oth_tags = {}
if ip:
oth_tags["up_ip"] = ip
if at:
oth_tags["up_at"] = at
if un:
oth_tags["up_by"] = un
mpool.put(Mpqe({}, entags, w, abspath, oth_tags))
with self.mutex:
@ -2328,8 +2357,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)
@ -2353,7 +2382,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
@ -2542,6 +2574,7 @@ class Up2k(object):
abspath: str,
ip: str,
at: float,
un: Optional[str],
) -> int:
"""will mutex(main)"""
assert self.mtag # !rm
@ -2562,7 +2595,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)
@ -2666,16 +2702,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)
@ -2733,7 +2772,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]:
@ -2777,7 +2816,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)",
@ -2810,6 +2849,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:
@ -2961,7 +3009,7 @@ class Up2k(object):
# check if filesystem supports sparse files;
# refuse out-of-order / multithreaded uploading if sprs False
sprs = self.fstab.get(pdir) != "ng"
sprs = self.fstab.get(pdir)[0] != "ng"
if True:
jcur = self.cur.get(ptop)
@ -3007,7 +3055,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)
@ -3099,7 +3147,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()
@ -3252,8 +3300,11 @@ class Up2k(object):
job["at"],
"",
)
if not hr:
t = "upload blocked by xbu server config: %r" % (dst,)
t = hr.get("rejectmsg") or ""
if t or not hr:
if not t:
t = "upload blocked by xbu server config: %r"
t = t % (vp,)
self.log(t, 1)
raise Pebkac(403, t)
if hr.get("reloc"):
@ -3393,10 +3444,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)
@ -3429,8 +3479,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:
@ -3548,11 +3598,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]
@ -3635,6 +3684,7 @@ class Up2k(object):
t = t.format(job["name"], nchunks[0][0], coffsets[0][0], cur_sz)
raise Pebkac(400, t)
for chash in chashes:
job["busy"][chash] = 1
job["poke"] = time.time()
@ -3642,7 +3692,7 @@ class Up2k(object):
return chashes, chunksize, coffsets, path, job["lmod"], job["size"], job["sprs"]
def fast_confirm_chunks(
self, ptop: str, wark: str, chashes: list[str]
self, ptop: str, wark: str, chashes: list[str], locked: list[str]
) -> tuple[int, str]:
if not self.mutex.acquire(False):
return -1, ""
@ -3650,7 +3700,7 @@ class Up2k(object):
self.mutex.release()
return -1, ""
try:
return self._confirm_chunks(ptop, wark, chashes, chashes)
return self._confirm_chunks(ptop, wark, chashes, locked, False)
finally:
self.reg_mutex.release()
self.mutex.release()
@ -3659,10 +3709,10 @@ class Up2k(object):
self, ptop: str, wark: str, written: list[str], locked: list[str]
) -> tuple[int, str]:
with self.mutex, self.reg_mutex:
return self._confirm_chunks(ptop, wark, written, locked)
return self._confirm_chunks(ptop, wark, written, locked, True)
def _confirm_chunks(
self, ptop: str, wark: str, written: list[str], locked: list[str]
self, ptop: str, wark: str, written: list[str], locked: list[str], final: bool
) -> tuple[int, str]:
if True:
self.db_act = self.vol_act[ptop] = time.time()
@ -3674,14 +3724,16 @@ class Up2k(object):
except Exception as ex:
return -2, "confirm_chunk, wark(%r)" % (ex,) # type: ignore
for chash in locked:
for chash in locked if final else written:
job["busy"].pop(chash, None)
try:
for chash in written:
job["need"].remove(chash)
except Exception as ex:
# dead tcp connections can get here by timeout (OK)
for zs in locked:
if job["busy"].pop(zs, None):
self.log("panic-unlock wark(%s) chunk(%s)" % (wark, zs), 1)
return -2, "confirm_chunk, chash(%s) %r" % (chash, ex) # type: ignore
ret = len(job["need"])
@ -3725,10 +3777,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()]
@ -3847,7 +3897,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))
@ -3855,10 +3907,23 @@ class Up2k(object):
assert self.mem_cur # !rm
r = db.execute(sql, s3enc(self.mem_cur, rd, fn))
if r.rowcount:
if not r.rowcount:
return
self.volsize[db] -= sz
self.volnfiles[db] -= 1
if "nodirsz" not in vflags:
try:
q = "update ds set nf=nf-1, sz=sz-? where rd=?"
while True:
db.execute(q, (sz, rd))
if not rd:
break
rd = rd.rsplit("/", 1)[0] if "/" in rd else ""
except:
pass
def db_add(
self,
db: "sqlite3.Cursor",
@ -3878,7 +3943,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 = ""
@ -3886,14 +3951,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
@ -3919,8 +3984,11 @@ class Up2k(object):
at or time.time(),
"",
)
if not hr:
t = "upload blocked by xau server config"
t = hr.get("rejectmsg") or ""
if t or not hr:
if not t:
t = "upload blocked by xau server config: %r"
t = t % (djoin(vtop, rd, fn),)
self.log(t, 1)
wunlink(self.log, dst, vflags)
self.registry[ptop].pop(wark, None)
@ -3985,6 +4053,9 @@ class Up2k(object):
except:
pass
def handle_fs_abrt(self, akey: str) -> None:
self.abrt_key = akey
def handle_rm(
self,
uname: str,
@ -4031,7 +4102,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 {}
@ -4052,17 +4123,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"
@ -4082,6 +4158,9 @@ class Up2k(object):
except:
raise Pebkac(400, "file not found on disk (already deleted?)")
if "bcasechk" in vn.flags and not vn.casechk(rem, False):
raise Pebkac(400, "file does not exist case-sensitively")
scandir = not self.args.no_scandir
if is_dir:
# note: deletion inside shares would require a rewrite here;
@ -4151,9 +4230,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:
@ -4194,7 +4273,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")
@ -4206,6 +4285,9 @@ class Up2k(object):
self.db_act = self.vol_act[svn_dbv.realpath] = time.time()
st = bos.stat(sabs)
if "bcasechk" in svn.flags and not svn.casechk(srem, False):
raise Pebkac(400, "file does not exist case-sensitively")
if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode):
with self.mutex:
try:
@ -4241,6 +4323,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()
@ -4310,7 +4394,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)
@ -4335,7 +4419,7 @@ class Up2k(object):
w,
w,
"",
"",
un or "",
ip or "",
at or 0,
)
@ -4362,7 +4446,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)
@ -4408,7 +4492,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")
@ -4422,6 +4506,9 @@ class Up2k(object):
raise Pebkac(400, "mv: cannot move a mountpoint")
st = bos.lstat(sabs)
if "bcasechk" in svn.flags and not svn.casechk(srem, False):
raise Pebkac(400, "file does not exist case-sensitively")
if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode):
with self.mutex:
try:
@ -4463,6 +4550,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()
@ -4594,7 +4683,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
@ -4607,7 +4696,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:
@ -4628,7 +4724,7 @@ class Up2k(object):
w,
w,
"",
"",
un or "",
ip or "",
at or 0,
)
@ -4659,7 +4755,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)
@ -4728,13 +4824,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:
@ -4743,14 +4840,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,
@ -4775,7 +4873,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:
@ -4864,7 +4962,10 @@ class Up2k(object):
mt = bos.path.getmtime(slabs, False)
flags = self.flags.get(ptop) or {}
atomic_move(self.log, sabs, slabs, flags)
try:
bos.utime(slabs, (int(time.time()), int(mt)), False)
except:
self.log("relink: failed to utime(%r, %s)" % (slabs, mt), 3)
self._symlink(slabs, sabs, flags, False, is_mv=True)
full[slabs] = (ptop, rem)
sabs = slabs
@ -4937,7 +5038,7 @@ class Up2k(object):
for k in cj["hash"]:
if not self.r_hash.match(k):
raise Pebkac(
400, "at least one hash is not according to spec: {}".format(k)
400, "at least one hash is not according to spec: %r" % (k,)
)
# try to use client-provided timestamp, don't care if it fails somehow
@ -4994,6 +5095,16 @@ class Up2k(object):
return ret, st
def _ex_hash(self, ex: Exception, ap: str) -> None:
eno = getattr(ex, "errno", 0)
if eno in E_FS_MEH:
return self.log("hashing failed; %r @ %r" % (ex, ap))
if eno not in E_FS_CRIT:
return self.log("hashing failed; %r @ %r\n%s" % (ex, ap, min_ex()), 3)
t = "hashing failed; %r @ %r\n%s\nWARNING: This MAY indicate a serious issue with your harddisk or filesystem! Please investigate %sOS-logs\n"
t2 = "" if ANYWIN or MACOS else "dmesg and "
return self.log(t % (ex, ap, min_ex(), t2), 1)
def _new_upload(self, job: dict[str, Any], vfs: VFS, depth: int) -> dict[str, str]:
pdir = djoin(job["ptop"], job["prel"])
if not job["size"]:
@ -5027,7 +5138,9 @@ class Up2k(object):
job["t0"],
"",
)
if not hr:
t = hr.get("rejectmsg") or ""
if t or not hr:
if not t:
t = "upload blocked by xbu server config: %r" % (vp_chk,)
self.log(t, 1)
raise Pebkac(403, t)
@ -5090,7 +5203,7 @@ class Up2k(object):
sprs = False
if not ANYWIN and sprs and sz > 1024 * 1024:
fs = self.fstab.get(pdir)
fs, mnt = self.fstab.get(pdir)
if fs == "ok":
pass
elif "nosparse" in vf:
@ -5176,17 +5289,21 @@ class Up2k(object):
self.log("\n".join([t] + vis))
for job in rm:
del reg[job["wark"]]
rsv_cleared = False
try:
# remove the filename reservation
path = djoin(job["ptop"], job["prel"], job["name"])
if bos.path.getsize(path) == 0:
bos.unlink(path)
rsv_cleared = True
except:
pass
try:
if len(job["hash"]) == len(job["need"]):
# PARTIAL is empty, delete that too
if len(job["hash"]) == len(job["need"]) or (
rsv_cleared and "rm_partial" in self.flags[job["ptop"]]
):
# PARTIAL is empty (hash==need) or --rm-partial, so delete that too
path = djoin(job["ptop"], job["prel"], job["tnam"])
bos.unlink(path)
except:

View file

@ -52,6 +52,7 @@ from .__init__ import (
VT100,
WINDOWS,
EnvParams,
unicode,
)
from .__version__ import S_BUILD_DT, S_VERSION
from .stolen import surrogateescape
@ -110,9 +111,17 @@ E_ADDR_NOT_AVAIL = _ens("EADDRNOTAVAIL WSAEADDRNOTAVAIL")
E_ADDR_IN_USE = _ens("EADDRINUSE WSAEADDRINUSE")
E_ACCESS = _ens("EACCES WSAEACCES")
E_UNREACH = _ens("EHOSTUNREACH WSAEHOSTUNREACH ENETUNREACH WSAENETUNREACH")
E_FS_MEH = _ens("EPERM EACCES ENOENT ENOTCAPABLE")
E_FS_CRIT = _ens("EIO EFAULT EUCLEAN ENOTBLK")
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
@ -369,6 +378,13 @@ DAV_ALLPROP_L = [
DAV_ALLPROPS = set(DAV_ALLPROP_L)
FAVICON_MIMES = {
"gif": "image/gif",
"png": "image/png",
"svg": "image/svg+xml",
}
MIMES = {
"opus": "audio/ogg; codecs=opus",
"owa": "audio/webm; codecs=opus",
@ -399,6 +415,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
@ -420,7 +439,7 @@ EXTS["vnd.mozilla.apng"] = "png"
MAGIC_MAP = {"jpeg": "jpg"}
DEF_EXP = "self.ip self.ua self.uname self.host cfg.name cfg.logout vf.scan vf.thsize hdr.cf_ipcountry srv.itime srv.htime"
DEF_EXP = "self.ip self.ua self.uname self.host cfg.name cfg.logout vf.scan vf.thsize hdr.cf-ipcountry srv.itime srv.htime"
DEF_MTE = ".files,circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash"
@ -536,6 +555,8 @@ def py_desc() -> str:
ofs = py_ver.find(".final.")
if ofs > 0:
py_ver = py_ver[:ofs]
if "free-threading" in sys.version:
py_ver += "t"
host_os = platform.system()
compiler = platform.python_compiler().split("http")[0]
@ -646,6 +667,9 @@ def read_utf8(log: Optional["NamedLogger"], ap: Union[str, bytes], strict: bool)
with open(ap, "rb") as f:
buf = f.read()
if buf.startswith(b"\xef\xbb\xbf"):
buf = buf[3:]
try:
return buf.decode("utf-8", "strict")
except UnicodeDecodeError as ex:
@ -1111,16 +1135,18 @@ class ProgressPrinter(threading.Thread):
sigblock()
tp = 0
msg = None
no_stdout = self.args.q
slp_pr = self.args.scan_pr_r
slp_ps = min(slp_pr, self.args.scan_st_r)
no_stdout = self.args.q or slp_pr == slp_ps
fmt = " {}\033[K\r" if VT100 else " {} $\r"
while not self.end:
time.sleep(0.1)
time.sleep(slp_ps)
if msg == self.msg or self.end:
continue
msg = self.msg
now = time.time()
if msg and now - tp > 10:
if msg and now - tp >= slp_pr:
tp = now
self.log("progress: %r" % (msg,), 6)
@ -1178,21 +1204,21 @@ class MTHash(object):
for nch in range(nchunks):
self.work_q.put(nch)
ex = ""
ex: Optional[Exception] = None
for nch in range(nchunks):
qe = self.done_q.get()
try:
nch, dig, ofs, csz = qe
chunks[nch] = (dig, ofs, csz)
except:
ex = ex or str(qe)
ex = ex or qe # type: ignore
if pp:
mb = (fsz - nch * chunksz) // (1024 * 1024)
pp.msg = prefix + str(mb) + suffix
if ex:
raise Exception(ex)
raise ex
ret = []
for n in range(nchunks):
@ -1209,7 +1235,7 @@ class MTHash(object):
try:
v = self.hash_at(ofs)
except Exception as ex:
v = str(ex) # type: ignore
v = ex # type: ignore
self.done_q.put(v)
@ -1567,9 +1593,11 @@ def vol_san(vols: list["VFS"], txt: bytes) -> bytes:
bvp = vol.vpath.encode("utf-8")
bvph = b"$hist(/" + bvp + b")"
if bap:
txt = txt.replace(bap, bvp)
txt = txt.replace(bhp, bvph)
txt = txt.replace(bap.replace(b"\\", b"\\\\"), bvp)
if bhp:
txt = txt.replace(bhp, bvph)
txt = txt.replace(bhp.replace(b"\\", b"\\\\"), bvph)
if vol.histpath != vol.dbpath:
@ -1749,12 +1777,12 @@ class MultipartParser(object):
continue
if m.group(1).lower() != "form-data":
raise Pebkac(400, "not form-data: {}".format(ln))
raise Pebkac(400, "not form-data: %r" % (ln,))
try:
field = self.re_cdisp_field.match(ln).group(1) # type: ignore
except:
raise Pebkac(400, "missing field name: {}".format(ln))
raise Pebkac(400, "missing field name: %r" % (ln,))
try:
fn = self.re_cdisp_file.match(ln).group(1) # type: ignore
@ -1926,7 +1954,7 @@ def get_boundary(headers: dict[str, str]) -> str:
ct = headers["content-type"]
m = re.match(ptn, ct, re.IGNORECASE)
if not m:
raise Pebkac(400, "invalid content-type for a multipart post: {}".format(ct))
raise Pebkac(400, "invalid content-type for a multipart post: %r" % (ct,))
return m.group(2)
@ -2068,6 +2096,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:
@ -2075,6 +2126,7 @@ def humansize(sz: float, terse: bool = False) -> str:
sz /= 1024.0
assert unit # type: ignore # !rm
if terse:
return "%s%s" % (str(sz)[:4].rstrip("."), unit[:1])
else:
@ -2227,14 +2279,14 @@ def odfusion(
ret = base.copy()
if oth.startswith("+"):
for k in words1:
ret[k] = True
ret[k] = True # type: ignore
elif oth[:1] in ("-", "/"):
for k in words1:
ret.pop(k, None)
ret.pop(k, None) # type: ignore
else:
ret = ODict.fromkeys(words0, True)
return ret
return ret # type: ignore
def ipnorm(ip: str) -> str:
@ -2606,6 +2658,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:
@ -2644,7 +2714,7 @@ def _fs_mvrm(
t = "something appeared at dst; aborting rename %r ==> %r"
log(t % (src, dst), 1)
return False
osfun(*args)
osfun(*args) # type: ignore
if attempt:
now = time.time()
t = "%sd in %.2f sec, attempt %d: %r"
@ -2694,7 +2764,7 @@ def atomic_move(log: "NamedLogger", src: str, dst: str, flags: dict[str, Any]) -
os.unlink(bdst)
except:
pass
shutil.move(bsrc, bdst)
shutil.move(bsrc, bdst) # type: ignore
def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
@ -2737,6 +2807,8 @@ def get_df(abspath: str, prune: bool) -> tuple[int, int, str]:
if not ANYWIN and not MACOS:
def siocoutq(sck: socket.socket) -> int:
assert fcntl # type: ignore # !rm
assert termios # type: ignore # !rm
# SIOCOUTQ^sockios.h == TIOCOUTQ^ioctl.h
try:
zb = fcntl.ioctl(sck.fileno(), termios.TIOCOUTQ, b"AAAA")
@ -2951,6 +3023,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:
@ -2982,6 +3075,17 @@ def justcopy(
return tlen, "checksum-disabled", "checksum-disabled"
def eol_conv(
fin: Generator[bytes, None, None], conv: str
) -> Generator[bytes, None, None]:
crlf = conv.lower() == "crlf"
for buf in fin:
buf = buf.replace(b"\r", b"")
if crlf:
buf = buf.replace(b"\n", b"\r\n")
yield buf
def hashcopy(
fin: Generator[bytes, None, None],
fout: Union[typing.BinaryIO, typing.IO[Any]],
@ -3068,7 +3172,7 @@ def sendfile_kern(
try:
req = min(0x2000000, upper - ofs) # 32 MiB
if use_poll:
poll.poll(10000)
poll.poll(10000) # type: ignore
else:
select.select([], [out_fd], [], 10)
n = os.sendfile(out_fd, in_fd, ofs, req)
@ -3124,8 +3228,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:
@ -3164,7 +3269,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):
@ -3355,7 +3462,9 @@ NICEB = NICES.encode("utf-8")
def runcmd(
argv: Union[list[bytes], list[str]], timeout: Optional[float] = None, **ka: Any
argv: Union[list[bytes], list[str], list["LiteralString"]],
timeout: Optional[float] = None,
**ka: Any
) -> tuple[int, str, str]:
isbytes = isinstance(argv[0], (bytes, bytearray))
oom = ka.pop("oom", 0) # 0..1000
@ -3374,19 +3483,19 @@ def runcmd(
if ANYWIN:
if isbytes:
if argv[0] in CMD_EXEB:
argv[0] += b".exe"
argv[0] += b".exe" # type: ignore
else:
if argv[0] in CMD_EXES:
argv[0] += ".exe"
argv[0] += ".exe" # type: ignore
if ka.pop("nice", None):
if WINDOWS:
ka["creationflags"] = 0x4000
elif NICEB:
if isbytes:
argv = [NICEB] + argv
argv = [NICEB] + argv # type: ignore
else:
argv = [NICES] + argv
argv = [NICES] + argv # type: ignore
p = sp.Popen(argv, stdout=cout, stderr=cerr, **ka)
@ -3398,10 +3507,10 @@ def runcmd(
pass
if not timeout or PY2:
bout, berr = p.communicate(sin)
bout, berr = p.communicate(sin) # type: ignore
else:
try:
bout, berr = p.communicate(sin, timeout=timeout)
bout, berr = p.communicate(sin, timeout=timeout) # type: ignore
except sp.TimeoutExpired:
if kill == "n":
return -18, "", "" # SIGCONT; leave it be
@ -3411,7 +3520,7 @@ def runcmd(
killtree(p.pid)
try:
bout, berr = p.communicate(timeout=1)
bout, berr = p.communicate(timeout=1) # type: ignore
except:
bout = b""
berr = b""
@ -3567,7 +3676,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]
@ -3834,7 +3943,7 @@ def runhook(
txt: str,
) -> dict[str, Any]:
assert broker or up2k # !rm
args = (broker or up2k).args
args = (broker or up2k).args # type: ignore
verbose = args.hook_v
vp = vp.replace("\\", "/")
ret = {"rc": 0}
@ -3852,6 +3961,7 @@ def runhook(
if broker:
broker.say("up2k.hook_fx", k, v, vp)
else:
assert up2k # !rm
up2k.fx_backlog.append((k, v, vp))
elif k == "reloc" and v:
# idk, just take the last one ig
@ -4011,6 +4121,8 @@ def termsize() -> tuple[int, int]:
env = os.environ
def ioctl_GWINSZ(fd: int) -> Optional[tuple[int, int]]:
assert fcntl # type: ignore # !rm
assert termios # type: ignore # !rm
try:
cr = sunpack(b"hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, b"AAAA"))
return cr[::-1]
@ -4140,7 +4252,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
@ -4187,7 +4299,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]:
@ -4212,7 +4324,7 @@ def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]:
stream = codecs.getreader(enc)(stream)
return stream
ap = os.path.join(E.mod, name)
ap = E.mod_ + name
if PY2:
return codecs.open(ap, "r", encoding=enc) # type: ignore

View file

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

View file

@ -84,6 +84,13 @@
--sort-1: #fb0;
--sort-2: #d09;
--sz-b: #aaa;
--sz-k: #4ff;
--sz-m: var(--tab-alt);
--sz-g: var(--a);
--sz-t: var(--sz-g);
--sz-p: var(--sz-t);
--srv-1: #aaa;
--srv-2: #a73;
--srv-3: #f4c;
@ -187,6 +194,9 @@ html.y {
--sort-1: #059;
--sort-2: #f5d;
--sz-b: #777;
--sz-k: #380;
--srv-1: #555;
--srv-2: #c83;
--srv-3: #c0a;
@ -344,6 +354,9 @@ html.cz {
--btn-1-bb: .2em solid #e90;
--btn-1-bs: 0 .1em .8em rgba(255,205,0,0.9);
--sz-b: #ddd;
--sz-k: #c9f;
--srv-3: #fff;
--u2-tab-b1: var(--bg-d3);
@ -571,6 +584,7 @@ pre, code, tt, #doc, #doc>code {
overflow: hidden;
width: 0;
height: 0;
left: -10em;
color: var(--bg);
}
html .ayjump:focus {
@ -739,6 +753,15 @@ html.y #files tr.fade a {
#files tbody tr td:last-child {
white-space: nowrap;
}
#files span.fsz_B { color: var(--sz-b); }
#files span.fsz_K { color: var(--sz-k); }
#files span.fsz_M { color: var(--sz-m); }
#files span.fsz_G { color: var(--sz-g); }
#files span.fsz_T { color: var(--sz-t); }
#files span.fsz_P { color: var(--sz-p); }
html.y #files span.fsz_G,
html.y #files span.fsz_T,
html.y #files span.fsz_P { font-weight: bold }
#files thead th[style] {
width: auto !important;
}
@ -881,6 +904,9 @@ html.y #path a:hover {
#flogout {
display: inline;
}
html.dz #flogout {
margin-left: 1em;
}
#goh+span {
color: var(--bg-u5);
padding-left: .5em;
@ -1113,18 +1139,7 @@ html.y #widget.open {
top: -.12em;
}
#wtico {
cursor: url(dd/4.png), pointer;
animation: cursor 500ms;
}
#wtico:hover {
animation: cursor 500ms infinite;
}
@keyframes cursor {
0% {cursor: url(dd/2.png), pointer}
30% {cursor: url(dd/3.png), pointer}
50% {cursor: url(dd/4.png), pointer}
75% {cursor: url(dd/5.png), pointer}
85% {cursor: url(dd/4.png), pointer}
cursor: pointer;
}
@keyframes spin {
100% {transform: rotate(360deg)}
@ -1162,6 +1177,9 @@ html.y #widget.open {
border: 1px solid var(--bg-u5);
border-width: 0 .1em 0 0;
}
#wzip1 {
margin-right: .2em;
}
#wfm.act+#wzip1+#wzip,
#wfm.act+#wzip1+#wzip+#wnp {
margin-left: .2em;
@ -1385,6 +1403,7 @@ html.y #ops svg circle {
#op_cfg input[type=text] {
top: -.3em;
}
.opview select,
.opview input[type=text] {
color: var(--fg);
background: var(--txt-bg);
@ -1395,6 +1414,11 @@ html.y #ops svg circle {
border-radius: .2em;
padding: .2em .3em;
}
.opview select {
padding: .3em;
margin: .2em .4em;
background: var(--bg-u3);
}
.opview input.err {
color: var(--err-fg);
background: var(--err-bg);
@ -1559,11 +1583,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;
@ -1936,6 +1962,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;
@ -1994,6 +2025,7 @@ html.y #doc .line-highlight {
}
#seldoc.sel {
color: var(--fg2-max);
background: #f0f;
background: var(--g-sel-b1);
}
#pvol,
@ -2013,6 +2045,7 @@ a.btn,
user-select: none;
}
#hkhelp {
background: #fff;
background: var(--bg);
}
#hkhelp table {
@ -2126,6 +2159,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;
@ -3278,3 +3318,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;
}

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 B

View file

@ -23,17 +23,17 @@
<th>user</th>
<th>groups</th>
</tr></thead><tbody>
{% for un, gn in rows %}
{%- for un, gn in rows %}
<tr>
<td><a href="{{ r }}/?idp=rm={{ un|e }}">forget</a></td>
<td>{{ un|e }}</td>
<td>{{ gn|e }}</td>
</tr>
{% endfor %}
{%- endfor %}
</tbody></table>
{% if not rows %}
{%- if not rows %}
(there are no IdP users in the cache)
{% endif %}
{%- endif %}
</div>
<a href="#" id="repl">π</a>
<script>

View file

@ -130,7 +130,8 @@ write markdown (most html is 🙆 too)
var SR = "{{ r }}",
last_modified = {{ lastmod }},
have_emp = {{ "true" if have_emp else "false" }},
have_emp = {{ have_emp }},
md_no_br = {{ md_no_br }},
dfavico = "{{ favico }}";
var md_opt = {

View file

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

View file

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

View file

@ -28,7 +28,8 @@
var SR = "{{ r }}",
last_modified = {{ lastmod }},
have_emp = {{ "true" if have_emp else "false" }},
have_emp = {{ have_emp }},
md_no_br = {{ md_no_br }},
dfavico = "{{ favico }}";
var md_opt = {

View file

@ -6,7 +6,7 @@ var dom_doc = ebi('m');
var dom_md = ebi('mt');
(function () {
var n = document.location + '';
var n = location + '';
n = (n.slice(n.indexOf('//') + 2).split('?')[0] + '?v').split('/');
n[0] = 'top';
var loc = [];
@ -113,7 +113,7 @@ function save(mde) {
fd.append("lastmod", (force ? -1 : last_modified));
fd.append("body", txt);
var url = (document.location + '').split('?')[0];
var url = (location + '').split('?')[0];
var xhr = new XHR();
xhr.open('POST', url, true);
xhr.responseType = 'text';
@ -166,7 +166,7 @@ function save_cb() {
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
// download the saved doc from the server and compare
var url = (document.location + '').split('?')[0] + '?_=' + Date.now();
var url = (location + '').split('?')[0] + '?_=' + Date.now();
var xhr = new XHR();
xhr.open('GET', url, true);
xhr.responseType = 'text';

31
copyparty/web/opds.xml Normal file
View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
{%- for d in dirs %}
<entry>
<title>{{ d.name }}</title>
<link rel="subsection"
href="{{ d.href | e }}"
type="application/atom+xml;profile=opds-catalog"/>
<updated>{{ d.iso8601 }}</updated>
</entry>
{%- endfor %}
{%- for f in files %}
<entry>
<title>{{ f.name }}</title>
<updated>{{ f.iso8601 }}</updated>
<link rel="http://opds-spec.org/acquisition"
href="{{ f.href | e }}"
type="{{ f.mime }}"/>
{%- if f.jpeg_thumb_href != None %}
<link rel="http://opds-spec.org/image/thumbnail"
href="{{ f.jpeg_thumb_href | e }}"
type="image/jpeg"/>
{%- endif %}
{%- if f.jpeg_thumb_href_hires != None %}
<link rel="http://opds-spec.org/image"
href="{{ f.jpeg_thumb_href_hires | e }}"
type="image/jpeg"/>
{%- endif %}
</entry>
{%- endfor %}
</feed>

View file

@ -19,14 +19,7 @@
<a href="{{ r }}/?h">control-panel</a>
&nbsp; Filter: <input type="text" id="filter" size="20" placeholder="documents/passwords" />
&nbsp; <span id="hits"></span>
<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 id="tb"></tbody></table>
<div id="tw"></div>
</div>
<a href="#" id="repl">π</a>
<script>

View file

@ -1,5 +1,6 @@
function render() {
var ups = V.ups, now = V.now, html = [];
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';
@ -15,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 +
@ -26,7 +28,8 @@ function render() {
var t = V.filter ? ' matching the filter' : '';
html = ['<tr><td colspan="6">there are no uploads' + t + '</td></tr>'];
}
ebi('tb').innerHTML = html.join('');
html.push('</tbody></table>');
ebi('tw').innerHTML = html.join('\n');
}
render();
@ -46,7 +49,7 @@ function ask(e) {
V = JSON.parse(this.responseText)
}
catch (ex) {
ebi('tb').innerHTML = '<tr><td colspan="6">failed to decode server response as json: <pre>' + esc(this.responseText) + '</pre></td></tr>';
ebi('tw').innerHTML = 'failed to decode server response as json: <pre>' + esc(this.responseText) + '</pre>';
return;
}
render();

View file

@ -36,7 +36,7 @@
<th>hrs</th>
<th>add time</th>
</tr></thead><tbody>
{% for k, pw, vp, pr, st, un, t0, t1 in rows %}
{%- for k, pw, vp, pr, st, un, t0, t1 in rows %}
<tr>
<td>
<a href="{{ r }}{{ shr }}{{ k }}?qr">qr</a>
@ -54,11 +54,11 @@
<td>{{ "inf" if not t1 else "dead" if t1 < now else ((t1 - now) / 3600) | round(1) }}</td>
<td></td>
</tr>
{% endfor %}
{%- endfor %}
</tbody></table>
{% if not rows %}
{%- if not rows %}
(you don't have any active shares btw)
{% endif %}
{%- endif %}
</div>
<a href="#" id="repl">π</a>
<script>

View file

@ -1,11 +1,9 @@
var SRS = SR.trimEnd('/') + '/';
var t = QSA('a[k]');
for (var a = 0; a < t.length; a++)
t[a].onclick = rm;
function rm() {
var u = SRS + '?eshare=rm&skey=' + uricom_enc(this.getAttribute('k')),
var u = SR + '/?eshare=rm&skey=' + uricom_enc(this.getAttribute('k')),
xhr = new XHR();
xhr.open('POST', u, true);
@ -15,7 +13,7 @@ function rm() {
function bump() {
var k = this.closest('tr').getElementsByTagName('a')[2].getAttribute('k'),
u = SRS + '?skey=' + uricom_enc(k) + '&eshare=' + this.value,
u = SR + '/?skey=' + uricom_enc(k) + '&eshare=' + this.value,
xhr = new XHR();
xhr.open('POST', u, true);
@ -27,7 +25,7 @@ function cb() {
if (this.status !== 200)
return modal.alert('<h6>server error</h6>' + esc(unpre(this.responseText)));
document.location = '?shares';
location = '?shares';
}
function qr(e) {

View file

@ -24,6 +24,7 @@ h1 {
li {
margin: 1em 0;
}
#lo,
a {
color: #047;
background: #fff;
@ -37,6 +38,7 @@ a {
td a {
margin: 0;
}
#wb,
#w {
color: #fff;
background: #940;
@ -47,6 +49,7 @@ td a {
float: right;
margin: -.2em 0 0 .8em;
}
#lo,
.logout,
a.r {
color: #c04;
@ -115,6 +118,26 @@ table {
.btns>a:first-child {
margin-left: 0;
}
.agr br {
display: none;
}
#lo,
.agr a,
.agr form {
margin: 0 .5em 0 0;
line-height: 4em;
}
.agr form,
.agr input {
display: inline;
padding: 0;
margin: 0;
}
#lo,
.agr input {
line-height: 1em;
font-weight: normal;
}
#msg {
margin: 3em 0;
}
@ -176,12 +199,14 @@ html.z {
html.z h1 {
border-color: #777;
}
html.z #lo,
html.z a {
color: #fff;
background: #057;
border-color: #37a;
}
html.z .logout,
html.z #lo,
html.z a.r {
background: #804;
border-color: #c28;

View file

@ -15,14 +15,18 @@
<body>
<div id="wrap">
{%- if not in_shr %}
<a id="a" href="{{ r }}/?h" class="af">refresh</a>
<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 &nbsp; <small>(you're not logged in)</small></p>
{%- else %}
{%- if this.args.idp_logout %}
<a id="c" href="{{ this.args.idp_logout }}" class="logout">logout</a>
{%- else %}
<a id="c" href="{{ r }}/?pw=x" class="logout">logout</a>
<p><span id="m">welcome back,</span> <strong>{{ this.uname|e }}</strong></p>
{%- endif %}
<p><span id="m">welcome back,</span> <strong id="un">{{ this.uname|e }}</strong></p>
{%- endif %}
{%- endif %}
@ -37,9 +41,9 @@
<table class="vols">
<thead><tr><th>%</th><th>speed</th><th>eta</th><th>idle</th><th>dir</th><th>file</th></tr></thead>
<tbody>
{% for u in ups %}
{%- for u in ups %}
<tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td><a href="{{ u[4] }}">{{ u[5]|e }}</a></td><td>{{ u[6]|e }}</td></tr>
{% endfor %}
{%- endfor %}
</tbody>
</table>
{%- endif %}
@ -49,9 +53,9 @@
<table class="vols">
<thead><tr><th>%</th><th>sent</th><th>speed</th><th>eta</th><th>idle</th><th></th><th>dir</th><th>file</th></tr></thead>
<tbody>
{% for u in dls %}
{%- for u in dls %}
<tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td>{{ u[4] }}</td><td>{{ u[5] }}</td><td><a href="{{ u[6] }}">{{ u[7]|e }}</a></td><td>{{ u[8] }}</td></tr>
{% endfor %}
{%- endfor %}
</tbody>
</table>
{%- endif %}
@ -70,11 +74,11 @@
<table class="vols">
<thead><tr><th>vol</th><th id="t">action</th><th>status</th></tr></thead>
<tbody>
{% for mp in avol %}
{%- for mp in avol %}
{%- if mp in vstate and vstate[mp] %}
<tr><td><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></td><td><a class="s" href="{{ r }}{{ mp }}?scan">rescan</a></td><td>{{ vstate[mp] }}</td></tr>
{%- endif %}
{% endfor %}
{%- endfor %}
</tbody>
</table>
</td></tr></table>
@ -87,18 +91,18 @@
{%- if rvol %}
<h1 id="f">you can browse:</h1>
<ul>
{% for mp in rvol %}
{%- for mp in rvol %}
<li><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></li>
{% endfor %}
{%- endfor %}
</ul>
{%- endif %}
{%- if wvol %}
<h1 id="g">you can upload to:</h1>
<ul>
{% for mp in wvol %}
{%- for mp in wvol %}
<li><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></li>
{% endfor %}
{%- endfor %}
</ul>
{%- endif %}
@ -110,59 +114,85 @@
<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 %}
<input type="text" id="lu" name="uname" placeholder=" username" size="12" />
<input type="password" id="lp" name="cppwd" placeholder=" password" size="12" />
{%- else %}
<input type="password" id="lp" name="cppwd" placeholder=" password" />
{%- 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 this.uname in this.args.idp_adm_set %}
<li><a id="ag" href="{{ r }}/?idp">view idp cache</a></li>
{% endif %}
<div class="agr">
{%- if ahttps %}
<a id="wb" href="{{ ahttps }}">switch to https</a><br />
{%- endif %}
<a id="af" href="{{ r }}/?ru">show recent uploads</a><br />
{%- if this.uname != '*' and this.args.shr %}
<li><a id="y" href="{{ r }}/?shares">edit shares</a></li>
{% endif %}
<a id="y" href="{{ r }}/?shares">edit shares</a><br />
{%- endif %}
{% if k304 or k304vis %}
{% if k304 %}
{%- if this.uname in this.args.idp_adm_set %}
<a id="ag" href="{{ r }}/?idp">view idp cache</a><br />
{%- endif %}
<a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a><br />
{%- if this.uname != '*' and not in_shr %}
<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>
{%- endif %}
</div>
<ul>
{%- 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 %}
<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>
{%- endif %}
</ul>
</div>

View file

@ -1,4 +1,5 @@
// please add translations in alphabetic order, but keep "nor" and "eng" first
// (lines ending with //m are machine translations)
var Ls = {
"nor": {
"a1": "oppdater",
@ -16,6 +17,11 @@ var Ls = {
"j1": "k304 bryter tilkoplingen for hver HTTP 304. Dette hjelper mot visse mellomtjenere som kan sette seg fast / plutselig slutter å laste sider, men det reduserer også ytelsen betydelig",
"k1": "nullstill innstillinger",
"l1": "logg inn:",
"ls3": "logg inn",
"lu4": "brukernavn",
"lp4": "passord",
"lo3": "logg ut “{0}” overalt",
"lo2": "avslutter økten på alle nettlesere",
"m1": "velkommen tilbake,",
"n1": "404: filen finnes ikke &nbsp;┐( ´ -`)┌",
"o1": 'eller kanskje du ikke har tilgang? prøv et passord eller <a href="' + SR + '/?h">gå hjem</a>',
@ -45,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",
@ -67,6 +74,11 @@ var Ls = {
"j1": "k304 会在每个 HTTP 304 时断开连接。这有助于避免某些代理服务器卡住或突然停止加载页面,但也会显著降低性能。",
"k1": "重置设置",
"l1": "登录:",
"ls3": "登录", //m
"lu4": "用户名", //m
"lp4": "密码", //m
"lo3": "在所有地方注销 {0}", //m
"lo2": "这将结束在所有浏览器中的会话", //m
"m1": "欢迎回来,",
"n1": "404: 文件不存在 &nbsp;┐( ´ -`)┌",
"o1": '或者你可能没有权限?尝试输入密码或 <a href="' + SR + '/?h">回家</a>',
@ -93,6 +105,52 @@ var Ls = {
"af1": "显示最近上传的文件", //m
"ag1": "查看已知 IdP 用户", //m
},
"cze": {
"a1": "obnovit",
"b1": "ahoj cizinče &nbsp; <small>(nejsi přihlášen)</small>",
"c1": "odhlásit se",
"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",
"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:",
"ls3": "přihlásit se", //m
"lu4": "uživatelské jméno", //m
"lp4": "heslo", //m
"lo3": "odhlásit “{0}” všude", //m
"lo2": "tímto ukončíte relaci ve všech prohlížečích", //m
"m1": "vítej zpět,",
"n1": "404 nenalezeno &nbsp;┐( ´ -`)┌",
"o1": 'nebo možná nemáš přístup -- zkus heslo nebo <a href="' + SR + '/?h">jdi domů</a>',
"p1": "403 zakázáno &nbsp;~┻━┻",
"q1": 'použij heslo nebo <a href="' + SR + '/?h">jdi domů</a>',
"r1": "jdi domů",
".s1": "znovu prohledat",
"t1": "akce",
"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í",
"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",
"aa1": "příchozí soubory:",
"ab1": "deaktivovat no304",
"ac1": "povolit no304",
"ad1": "povolení no304 deaktivuje veškeré mezipaměti; zkuste to, pokud k304 nestačilo. To ovšem zapříčíní obrovské množství síťového provozu!",
"ae1": "aktivní stahování:",
"af1": "zobrazit nedávné nahrávání",
},
"deu": {
"a1": "Neu laden",
"b1": "Tach, wie geht's? &nbsp; <small>(Du bist nicht angemeldet)</small>",
@ -109,6 +167,11 @@ var Ls = {
"j1": "k304 trennt die Clientverbindung bei jedem HTTP 304, was Bugs mit problematischen Proxies vorbeugen kann (z.B. nicht ladenden Seiten), macht Dinge aber generell langsamer",
"k1": "Client-Einstellungen zurücksetzen",
"l1": "Melde dich an für mehr:",
"ls3": "Anmelden", //m
"lu4": "Benutzername", //m
"lp4": "Passwort", //m
"lo3": "“{0}” überall abmelden", //m
"lo2": "Dies beendet die Sitzung in allen Browsern", //m
"m1": "Willkommen zurück,",
"n1": "404 Nicht gefunden &nbsp;┐( ´ -`)┌",
"o1": 'or maybe you don\'t have access -- try a password or <a href="' + SR + '/?h">go home</a>',
@ -134,6 +197,53 @@ var Ls = {
"ae1": "Aktive Downloads:",
"af1": "Zeige neue Uploads",
},
"epo": {
"a1": "reŝargi",
"b1": "sal, nekonatulo &nbsp; <small>(vi ne estas ensalutita)</small>",
"c1": "elsaluti",
"d1": "montru stakon", // TLNote: "d2" is the tooltip for this button
"d2": "montras la staton de ĉiuj aktivaj fadenoj",
"e1": "reŝargi CFGon",
"e2": "reŝargas la agordo-dosierojn (kontoj/portiloj/portilo-flagoj),$Nkaj reskanas ĉiuj portiloj de ed2s$N$Nnoto: ĉiuj ŝanĝoj de ĝeneralaj agordoj postulas$Npostulas tutan restartigon por efektiviĝi",
"f1": "vi povas vidi:",
"g1": "vi povas alŝuti al:",
"cc1": "aliaĵoj:",
"h1": "malŝalti k304-on", // TLNote: "j1" explains what k304 is
"i1": "ŝalti k304-on",
"j1": "k304 malkonektas vian klienton je ĉiu HTTP-eraro 304; tio povas eviti paraliziĝon dum uzado de difektitaj retperantoj (paĝoj subite ne ŝargiĝas), <em>sed</em> ĝi ankaŭ plimalrapidigas ĉion",
"k1": "rekomenci agordojn de kliento",
"l1": "ensaluti por pli da opcioj:",
"ls3": "ensaluti", //m
"lu4": "uzantnomo", //m
"lp4": "pasvorto", //m
"lo3": "ensaluti kiel “{0}” ĉie", //m
"lo2": "ĉi tiu finigos seancon en ĉiuj retumiloj", //m
"m1": "bonvenon denove,", // TLNote: "welcome back, USERNAME"
"n1": "404 ne trovita &nbsp;┐( ´ -`)┌",
"o1": 'aŭ eble vi ne havas rajton -- provu uzi pasvorton aŭ <a href="' + SR + '/?h">iri hejmen</a>',
"p1": "403 ne permesita &nbsp;~┻━┻",
"q1": 'uzu pasvorton aŭ <a href="' + SR + '/?h">iru hejmen</a>',
"r1": "hejmen",
".s1": "reskani",
"t1": "ago", // TLNote: this is the header above the "rescan" buttons
"u2": "tempo post lasta skribo (alŝuto / alinomado / ...) je servilo$N( upload / rename / ... )$N$N17d = 17 tagoj$N1h23 = 1 horo 23 minutoj$N4m56 = 4 minutoj 56 sekundoj",
"v1": "konekti",
"v2": "uzi ĉi tiun servilon kiel loka disko",
"w1": "uzi HTTPS-protokolon",
"x1": "ŝanĝi pasvorton",
"y1": "redakti komunaĵojn", // TLNote: shows the list of folders that the user has decided to share
"z1": "malŝlosi ĉi tiun komunaĵon:", // TLNote: the password prompt to see a hidden share
"ta1": "entajpu novan pasvorton unue",
"ta2": "retajpu por konfirmi:",
"ta3": "tajpo-eraro; bonvolu provu denove",
"aa1": "aktivaj alŝutoj:",
"ab1": "malŝalti no304-on",
"ac1": "ŝalti no304-on",
"ad1": "no304 malŝaltas ĉiun kaŝmemoradon; provu ĉi tion, se k304 ne riparis la difektojn. Ĉi tiu agordo malŝparas multon da datumtrafiko!",
"ae1": "aktivaj elŝutoj:",
"af1": "montri lastajn alŝutojn",
"ag1": "montri kaŝmemoron de idp",
},
"fin": {
"a1": "päivitä",
"b1": "hei sie muukalainen &nbsp; <small>(et ole kirjautunut sisään)</small>",
@ -142,14 +252,19 @@ var Ls = {
"d2": "näytä kaikkien aktiivisten säikeiden tila",
"e1": "päivitä konffit",
"e2": "lataa konfiguraatiotiedostot uudelleen (käyttäjätilit/asemat/asemaflagit),$Nja skannaa kaikki e2ds asemat uudelleen$N$Nhuom: kaikki global-asetuksiin$Ntehdyt muutokset vaativat täyden$Nuudelleenkäynnistyksen",
"f1": "voit selata:",
"g1": "voit ladata:",
"f1": "voit selata näitä:",
"g1": "voit ladata näihin:",
"cc1": "muuta:",
"h1": "poista k304 käytöstä",
"i1": "ota k304 käyttöön",
"j1": "k304 katkaisee yhteytesi jokaisella HTTP 304:llä, mikä voi estää joitain bugisia välityspalvelimia jumittumasta/lopettamasta sivujen lataamista, <em>mutta</em> se myös vähentää suorituskykyä",
"k1": "nollaa asetukset",
"l1": "kirjaudu sisään:",
"ls3": "kirjaudu sisään", //m
"lu4": "käyttäjätunnus", //m
"lp4": "salasana", //m
"lo3": "kirjaa “{0}” ulos kaikkialta", //m
"lo2": "tämä lopettaa istunnon kaikissa selaimissa", //m
"m1": "tervetuloa takaisin,",
"n1": "404: ei löytynyt mitään &nbsp;┐( ´ -`)┌",
"o1": 'tai ehkä sinulla ei vain ole käyttöoikeuksia? kokeile salasanaa tai <a href="' + SR + '/?h">mene kotiin</a>',
@ -176,6 +291,380 @@ var Ls = {
"af1": "näytä viimeaikaiset lataukset",
"ag1": "näytä tunnetut IdP-käyttäjät",
},
"fra": {
"a1": "rafraîchir",
"b1": "salut étranger &nbsp; <small>(vous n'êtes pas connecté.)</small>",
"c1": "déconnexion",
"d1": "vidange de la pile",
"d2": "affiche l'état de tous les threads actifs",
"e1": "recharger la configuration",
"e2": "recharger le fichier de configuration (comptes/volumes/indicateurs de volume),$Net rescanner tous les volumes e2ds$N$Nnote : n'importe quel changement aux paramètres globaux$Nnécessite un redémarrage complet pour prendre effet",
"f1": "vous pouvez naviguer :",
"g1": "vous pouvez télécharger sur :",
"cc1": "autres choses :",
"h1": "désactiver k304",
"i1": "activer k304",
"j1": "activer k304 va déconnecter votre client sur chaque HTTP 304, ce qui peut éviter à certains proxies défectueux de rester bloqués (les pages ne se chargent soudainement plus), <em>mais</em> cela ralentira également les choses en général",
"k1": "réinitialiser les paramètres du client",
"l1": "connectez-vous pour en savoir plus :",
"ls3": "se connecter", //m
"lu4": "nom d'utilisateur", //m
"lp4": "mot de passe", //m
"lo3": "déconnecter “{0}” partout", //m
"lo2": "cela mettra fin à la session sur tous les navigateurs", //m
"m1": "heureux de vous revoir,",
"n1": "404 introuvable &nbsp;┐( ´ -`)┌",
"o1": 'ou peut-être que vous n\'y avez pas accès -- essayer un mot de passe ou <a href="' + SR + '/?h">aller à la page d\'accueil</a>',
"p1": "403 interdit &nbsp;~┻━┻",
"q1": 'utiliser un mot de passe ou <a href="' + SR + '/?h">aller à la page d\'accueil</a>',
"r1": "aller à la page d\'accueil",
".s1": "rescanner",
"t1": "action",
"u2": "temps écoulé depuis la dernière écriture sur le serveur$N(téléchargement/renommage/...)$N$N17j = 17 jours$N1h23 = 1 heure 23 minutes$N4m56 = 4 minutes 56 secondes",
"v1": "connecter",
"v2": "utilisez ce serveur en tant que disque dur local",
"w1": "passer à https",
"x1": "changer mot de passe",
"y1": "modifier les partages",
"z1": "déverrouiller ce partage :",
"ta1": "entrez d'abord votre nouveau mot de passe",
"ta2": "répétez pour confirmer le nouveau mot de passe :",
"ta3": "une faute de frappe a été détectée ; veuillez réessayer.",
"aa1": "fichiers entrants :",
"ab1": "désactiver no304",
"ac1": "activer no304",
"ad1": "l'activation de no304 désactivera toute mise en cache ; essayez ceci si k304 n'était pas suffisant. Cela va générer un trafic réseau considérable !",
"ae1": "téléchargements actifs :",
"af1": "afficher les derniers téléchargements",
},
"grc": {
"a1": "ανανέωση",
"b1": "γεια σου ξένε! &nbsp; <small>(δεν είσαι συνδεδεμένος)</small>",
"c1": "αποσύνδεση",
"d1": "λίστα διεργασιών",
"d2": "εμφανίζει την κατάσταση όλων των ενεργών διεργασιών",
"e1": "επαναφόρτωση του cfg",
"e2": "φορτώνει ξανά τα αρχεία ρυθμίσεων (λογαριασμοί/τόμοι/volflags),$Nκαι κάνει επανεξέταση όλων των τόμων e2ds$N$Nσημείωση: οποιαδήποτε αλλαγή στις καθολικές ρυθμίσεις$Nαπαιτεί πλήρη επανεκκίνηση για να εφαρμοστεί",
"f1": "μπορείς να περιηγηθείς:",
"g1": "μπορείς να εκτελέσεις μεταφόρτωση σε:",
"cc1": "άλλα πράγματα:",
"h1": "απενεργοποίση k304",
"i1": "ενεργοποίηση k304",
"j1": "η ενεργοποίηση του k304 θα αποσυνδέσει το πρόγραμμα πελάτη σου σε κάθε HTTP 304, κάτι που μπορεί να αποτρέψει κάποια προβληματικά proxies από το να κολλάνε (να μην φορτώνουν ξαφνικά σελίδες), <em>αλλά</em> θα κάνει τα πράγματα, γενικά πιο αργά",
"k1": "επαναφορά ρυθμίσεων στο πρόγραμμα πελάτη",
"l1": "συνδέσου για περισσότερα:",
"ls3": "σύνδεση", //m
"lu4": "όνομα χρήστη", //m
"lp4": "κωδικός πρόσβασης", //m
"lo3": "αποσύνδεση του “{0}” από παντού", //m
"lo2": "αυτό θα τερματίσει τη συνεδρία σε όλους τους περιηγητές", //m
"m1": "καλώς ήρθες,",
"n1": "404 δεν βρέθηκε &nbsp;┐( ´ -`)┌",
"o1": '´η μήπως δεν έχεις πρόσβαση -- δοκίμασε έναν κωδικό <a href="' + SR + '/?h">πήγαινε στην αρχική</a>',
"p1": "403 απαγορευμένο &nbsp;~┻━┻",
"q1": 'δοκίμασε έναν κωδικό <a href="' + SR + '/?h">πήγαινε στην αρχική</a>',
"r1": "πίσω στην αρχική",
".s1": "επανάληψη σάρωσης",
"t1": "ενέργεια",
"u2": "χρόνος από την τελευταία εγγραφή του διακομιστή$N( μεταφόρτωση / μετονομασία / ... )$N$N17d = 17 days$N1ω23 = 1 ώρα 23 λεπτά$N4λ56 = 4 λεπτά 56 δευτερόλεπτα",
"v1": "σύνδεση",
"v2": "χρησιμοποίησε αυτόν το διακομιστή σαν τοπικό δίσκο",
"w1": "εναλλαγή σε https",
"x1": "αλλαγή κωδικού",
"y1": "επεξεργασία κοινόχρηστων φακέλων",
"z1": "ξεκλείδωμα αυτού του κοινόχρηστου φακέλου:",
"ta1": "συμπλήρωσε πρώτα το νέο σου κωδικό",
"ta2": "επανέλαβε για να επιβεβαιώσεις το νέο κωδικό:",
"ta3": "βρέθηκε τυπογραφικό λάθος· δοκίμασε ξανά",
"aa1": "εισερχόμενα αρχεία:",
"ab1": "απενεργοποίηση no304",
"ac1": "ενεργοποίηση no304",
"ad1": "η ενεργοποίηση του no304 θα απενεργοποιήσει όλη την προσωρινή αποθήκευση· δοκίμασέ το αν το k304 δεν ήταν αρκετό. Προσοχή, θα σπαταλήσει τεράστιο όγκο δικτυακής κίνησης!",
"ae1": "ενεργές μεταφορτώσεις:",
"af1": "προβολή πρόσφατων μεταφορτώσεων",
},
"ita": {
"a1": "aggiorna",
"b1": "ciao &nbsp; <small>(non sei connesso)</small>",
"c1": "disconnetti",
"d1": "stato",
"d2": "mostra lo stato di tutti i thread attivi",
"e1": "ricarica configurazione",
"e2": "ricarica i file di configurazione (account/volumi/flag dei volumi),\n e riesegue la scansione di tutti i volumi e2ds.\n\nNota: qualsiasi modifica alle impostazioni globali richiede un riavvio completo per avere effetto",
"f1": "puoi visualizzare:",
"g1": "puoi caricare su:",
"cc1": "altro:",
"h1": "disattiva k304",
"i1": "attiva k304",
"j1": "k304 interrompe la connessione per ogni HTTP 304. Questo aiuta contro alcuni proxy difettosi che possono bloccarsi o smettere improvvisamente di caricare pagine, ma riduce notevolmente le prestazioni",
"k1": "resetta impostazioni",
"l1": "accedi:",
"ls3": "accedi", //m
"lu4": "nome utente", //m
"lp4": "password", //m
"lo3": "disconnetti “{0}” ovunque", //m
"lo2": "questo terminerà la sessione su tutti i browser", //m
"m1": "bentornato,",
"n1": "404: file non trovato &nbsp;┐( ´ -`)┌",
"o1": "oppure forse non hai accesso? prova una password o <a href=\"SR/?h\">torna alla home</a>",
"p1": "403: accesso negato &nbsp;~┻━┻",
"q1": "prova una password o <a href=\"SR/?h\">torna alla home</a>",
"r1": "torna alla home",
".s1": "mappa",
"t1": "azione",
"u2": "tempo dall'ultima scrittura sul server\n (caricamento / rinomina / ...)\n\n17d = 17 giorni\n1h23 = 1 ora 23 minuti\n4m56 = 4 minuti 56 secondi",
"v1": "connetti",
"v2": "usa questo server come un disco locale",
"w1": "passa a https",
"x1": "cambia password",
"y1": "le tue condivisioni",
"z1": "sblocca area:",
"ta1": "devi prima inserire una nuova password",
"ta2": "ripeti per confermare la nuova password:",
"ta3": "errore di digitazione; riprova",
"aa1": "in arrivo:",
"ab1": "disattiva no304",
"ac1": "attiva no304",
"ad1": "no304 disabilita completamente la cache. Se k304 non è sufficiente, prova questa opzione. Aumenterà notevolmente il consumo di dati!",
"ae1": "in uscita:",
"af1": "mostra i file caricati di recente",
"ag1": "mostra utenti IdP conosciuti"
},
"kor": {
"a1": "새로고침",
"b1": "어이 친구! 처음 보는 얼굴인데? &nbsp; <small>(로그인되어 있지 않습니다)</small>",
"c1": "로그아웃",
"d1": "스택 덤프하기",
"d2": "모든 활성 스레드의 상태를 표시합니다",
"e1": "설정 다시 불러오기",
"e2": "설정 파일(계정/볼륨/볼륨 플래그)을 다시 불러오고,$N모든 e2ds 볼륨을 다시 스캔합니다$N$N참고: 전역 설정에 대한 변경 사항은$N적용하려면 전체 재시작이 필요합니다",
"f1": "탐색 가능한 곳:",
"g1": "업로드 가능한 곳:",
"cc1": "기타 항목:",
"h1": "k304 비활성화",
"i1": "k304 활성화",
"j1": "k304를 활성화하면 모든 HTTP 304 응답 시 클라이언트 연결이 끊어집니다. 이는 일부 프록시가 멈추는 현상(갑자기 페이지가 로드되지 않음)을 방지할 수 있지만, <em>대신 전반적인 속도는 느려집니다.</em>",
"k1": "클라이언트 설정 초기화",
"l1": "로그인하기:",
"ls3": "로그인", //m
"lu4": "사용자 이름", //m
"lp4": "비밀번호", //m
"lo3": "{0}을(를) 모든 곳에서 로그아웃", //m
"lo2": "이 작업은 모든 브라우저에서 세션을 종료합니다", //m
"m1": "또 오셨네요,",
"n1": "404 찾을 수 없음 &nbsp;┐( ´ -`)┌",
"o1": "또는 접근 권한이 없을 수 있습니다. 비밀번호를 입력하거나 <a href=\"' + SR + '/?h\">홈으로 이동</a>하세요",
"p1": "403 접근 금지 &nbsp;~┻━┻",
"q1": "비밀번호를 입력하거나 <a href=\"' + SR + '/?h\">홈으로 이동</a>하세요",
"r1": "홈으로 이동",
".s1": "다시 스캔",
"t1": "작업",
"u2": "서버에 마지막으로 쓰기 작업을 한 후 경과된 시간$N(업로드 / 이름 변경 / 등등...)$N$N17d = 17일$N1h23 = 1시간 23분$N4m56 = 4분 56초",
"v1": "연결",
"v2": "이 서버를 로컬 하드디스크처럼 사용하기",
"w1": "HTTPS로 전환",
"x1": "비밀번호 변경",
"y1": "공유 설정",
"z1": "이 공유 잠금해제:",
"ta1": "새 비밀번호를 먼저 입력하세요",
"ta2": "새 비밀번호 확인을 위해 다시 입력하세요:",
"ta3": "오타가 있습니다. 다시 시도해주세요",
"aa1": "수신 중인 파일:",
"ab1": "no304 비활성화",
"ac1": "no304 활성화",
"ad1": "no304를 활성화하면 모든 캐싱이 비활성화됩니다. k304로 충분하지 않은 경우 시도해보세요. 네트워크 트래픽이 대량으로 낭비됩니다!",
"ae1": "활성 다운로드:",
"af1": "최근 업로드 보기",
"ag1": "IdP 캐시 보기"
},
"nld": {
"a1": "Update",
"b1": "Hallo, hoe gaat het met jou? &nbsp; <small>(Je bent niet ingelogd)</small>",
"c1": "Uitloggen",
"d1": "Voorwaarde",
"d2": "Toont de status van alle actieve threads",
"e1": "Configuratie opnieuw laden.",
"e2": "Leest configuratiebestanden opnieuw in$N(accounts, volumes, volumeschakelaars)$Nen brengt alle e2ds-volumes in kaart$N$Nopmerking: veranderingen in globale parameters$Nvereist een volledige herstart van de server",
"f1": "Je kan het volgende lezen:",
"g1": "Je kan naar het volgende uploaden:",
"cc1": "Schakelaars en dergelijke:",
"h1": "k304 uitschakelen",
"i1": "k304 inschakelen",
"j1": "k304 verbreekt de verbinding voor elke HTTP 304. Dit helpt tegen bepaalde proxy servers die kunnen vastlopen/plotseling stoppen met het laden van pagina's, maar het vermindert ook de prestaties aanzienlijk",
"k1": "Instellingen resetten",
"l1": "Inloggen:",
"ls3": "inloggen", //m
"lu4": "gebruikersnaam", //m
"lp4": "wachtwoord", //m
"lo3": "“{0}” overal afmelden", //m
"lo2": "dit zal de sessie in alle browsers beëindigen", //m
"m1": "Welkom terug,",
"n1": "404: bestand bestaat niet &nbsp;┐( ´ -`)┌",
"o1": 'of misschien heb je geen toegang? probeer een wachtwoord of <a href="' + SR + '/?h">ga naar startscherm</a>',
"p1": "403: toegang geweigerd &nbsp;~┻━┻",
"q1": 'Probeer een wachtwoord of <a href="' + SR + '/?h">ga naar startscherm</a>',
"r1": "Ga naar startscherm",
".s1": "Kaart",
"t1": "Actie",
"u2": "Tijd sinds iemand voor het laatst naar de server schreef$N( upload / naamswijziging / ... )$N$N17d = 17 dagen$N1h23 = 1 uur 23 minuten$N4m56 = 4 minuten 56 secondes",
"v1": "Verbinden",
"v2": "Gebruik deze server als een lokale harde schijf",
"w1": "Overschakelen naar https",
"x1": "Wachtwoord wijzigen",
"y1": "Jou gedeelde items",
"z1": "Ontgrendel gebied:",
"ta1": "Je moet eerst een nieuw wachtwoord invoeren",
"ta2": "Herhaal om nieuw wachtwoord te bevestigen:",
"ta3": "Typefout gevonden; probeer het opnieuw",
"aa1": "Inkomend:",
"ab1": "Schakel nr. 304 uit",
"ac1": "Schakel nr. 304 in",
"ad1": "Nr. 304 stopt al het cachegebruik. Als k304 niet voldoende was, probeer dan deze. Vermenigvuldigt het dataverbruik.!",
"ae1": "Uitgaand:",
"af1": "Recent geüploade bestanden weergeven",
"ag1": "Bekende IdP-gebruikers weergeven",
},
"nno": {
"a1": "oppdatér",
"b1": "heisann &nbsp; <small>(du er ikkje logga inn)</small>",
"c1": "logg ut",
"d1": "tilstand",
"d2": "vis tilstanden åt alle trådar",
"e1": "last innst.",
"e2": "les inn konfigurasjonsfiler på nytt$N(kontoer, volum, volumbrytarar)$Nog kartlegg alle e2ds-volum$N$Nmerk: endringer i globale parametrar$Nkrev ein full restart for å gjelde",
"f1": "du kan sjå på:",
"g1": "du kan laste opp åt:",
"cc1": "brytarar og slikt:",
"h1": "skru av k304",
"i1": "skru på k304",
"j1": "k304 bryt tilkoplinga for kvar HTTP 304. Dette hjelp mot visse mellomtjenarar som kan sette seg fast / plutselig sluttar å laste sider, men det sett óg ytinga ned betydelig",
"k1": "nullstill innstillinger",
"l1": "logg inn:",
"ls3": "logg inn",
"lu4": "brukarnamn",
"lp4": "passord",
"lo3": "logg ut “{0}” overalt",
"lo2": "avslutt økta på alle nettlesarar",
"m1": "velkomen attende,",
"n1": "404: filen finnast ikkje &nbsp;┐( ´ -`)┌",
"o1": 'eller kanskje du ikkje har høve? prøv eit passord eller <a href="' + SR + '/?h">gå heim</a>',
"p1": "403: tilgang nektet &nbsp;~┻━┻",
"q1": 'prøv eit passord eller <a href="' + SR + '/?h">gå heim</a>',
"r1": "gå heim",
".s1": "kartlegg",
"t1": "handling",
"u2": "tid sidan nokon sist skreiv åt serveren$N( opplastning / namnendring / ... )$N$N17d = 17 dagar$N1h23 = 1 time 23 minutt$N4m56 = 4 minutt 56 sekund",
"v1": "kople åt",
"v2": "bruk denne serveren som ein lokal harddisk",
"w1": "bytt åt https",
"x1": "bytt passord",
"y1": "dine delinger",
"z1": "lås opp område:",
"ta1": "du må skrive eit nytt passord først",
"ta2": "gjenta for å stadfeste nytt passord:",
"ta3": "fant ein skrivefeil; vennligst prøv igjen",
"aa1": "innkommande:",
"ab1": "skru av no304",
"ac1": "skru på no304",
"ad1": "no304 stoppar all bruk av cache. Hvis ikkje k304 var nok, prøv denne. Vil mangedoble dataforbruk!",
"ae1": "utgående:",
"af1": "vis nylig opplasta filer",
"ag1": "vis kjente IdP-brukarar",
},
"pol": {
"a1": "odśwież",
"b1": "witaj, nieznajomy &nbsp; <small>(nie jesteś zalogowany)</small>",
"c1": "wyloguj się",
"d1": "zrzut stosu",
"d2": "pokazuje status wszystkich aktywnych wątków",
"e1": "przeładuj konfigurację",
"e2": "przeładuj pliki konfiguracyjne (konta/wolumeny/flagi wolumenów),$Ni przeskanuje wszystkie wolumeny e2ds$N$Nnotka: zmiany konfiguracji globalnej$Nwymagają pełnego uruchomienia ponownie serwera, aby zaczęły obowiązywać",
"f1": "możesz przeglądać:",
"g1": "możesz przesyłać do:",
"cc1": "inne:",
"h1": "wyłącz k304",
"i1": "włącz k304",
"j1": "włączenie k304 będzie odłączało klienta przy każdorazowym otrzymaniu kodu HTTP 304, co może zapobiec wieszaniu się wadliwych proxy, <em>ale</em> spowolni ogólne działanie",
"k1": "zresetuj ustawienia klienta",
"l1": "zaloguj się po więcej:",
"ls3": "zaloguj się", //m
"lu4": "nazwa użytkownika", //m
"lp4": "hasło", //m
"lo3": "wyloguj “{0}” wszędzie", //m
"lo2": "spowoduje to zakończenie sesji we wszystkich przeglądarkach", //m
"m1": "Witaj,",
"n1": "404 nie znaleziono &nbsp;┐( ´ -`)┌",
"o1": 'lub możesz nie mieć dostępu -- spróbuj wprowadzić hasło lub <a href="' + SR + '/?h">przejdź do strony głównej</a>',
"p1": "403 odmowa dostępu &nbsp;~┻━┻",
"q1": 'użyj hasła lub <a href="' + SR + '/?h">przejdź do strony głównej</a>',
"r1": "idź do strony głównej",
".s1": "przeskanuj ponownie",
"t1": "akcje",
"u2": "czas od ostatniej interakcji z serwerem$N( przesyłania / zmiany nazwy / ... )$N$N17d = 17 dni$N1h23 = 1 godzina 23 minuty$N4m56 = 4 minuty 56 sekund",
"v1": "połącz",
"v2": "używaj tego serwera jako dysku lokalnego",
"w1": "przejdź na HTTPS",
"x1": "zmień hasło",
"y1": "edytuj udostępnione",
"z1": "odblokuj udostępnienie:",
"ta1": "najpierw wprowadź nowe hasło",
"ta2": "powtórz hasło dla potwierdzenia:",
"ta3": "znaleziono literówkę, spróbuj ponownie",
"aa1": "pliki przychodzące:",
"ab1": "wyłącz no304",
"ac1": "włącz no304",
"ad1": "włączenie no304 wyłączy przechowywanie jakiejkolwiek pamięci podręcznej. Zmarnuje to olbrzymią ilość ruchu sieciowego!",
"ae1": "trwające pobierania:",
"af1": "pokaż ostatnio przesłane pliki",
"ag1": "pokaż znanych użytkowników IdP",
},
"por": {
"a1": "atualizar",
"b1": "olá &nbsp; <small>(você não está logado)</small>",
"c1": "encerrar sessão",
"d1": "despejar o estado da pilha",
"d2": "mostra o estado de todos os threads ativos",
"e1": "recarregar configuração",
"e2": "recarregar arquivos de configuração (contas/volumes/indicadores de volume),$N e reescanear todos os volumes e2ds$N$Nnota: qualquer alteração na configuração global$N requer uma reinicialização completa para ter efeito",
"f1": "você pode navegar:",
"g1": "você pode fazer upload para:",
"cc1": "outras coisas:",
"h1": "desativar k304",
"i1": "ativar k304",
"j1": "ativar k304 irá desconectar seu cliente em cada HTTP 304, o que pode evitar que alguns proxies com erros fiquem presos (parando de carregar páginas de repente), <em>mas</em> também irá desacelerar as coisas em geral",
"k1": "redefinir config. de cliente",
"l1": "faça login para mais:",
"ls3": "fazer login",
"lu4": "nome de usuário",
"lp4": "senha",
"lo3": "encerrar sessão de \"{0}\" em todos os lugares",
"lo2": "isso irá encerrar a sessão em todos os navegadores",
"m1": "bem-vindo de volta,",
"n1": "404 não encontrado &nbsp;┐( ´ -`)┌",
"o1": "ou talvez você não tenha acesso? -- tente com uma senha ou volte para o início",
"p1": "403 proibido &nbsp;~┻━┻",
"q1": "use uma senha ou volte para o início",
"r1": "ir para o início",
".s1": "reescanear",
"t1": "ação",
"u2": "tempo desde a última gravação no servidor$N( upload / renomear / ... )$N$N17d = 17 dias$N1h23 = 1 hora 23 minutos$N4m56 = 4 minutos 56 segundos",
"v1": "conectar",
"v2": "usar este servidor como um disco rígido local",
"w1": "mudar para https",
"x1": "mudar senha",
"y1": "editar recursos compartilhados",
"z1": "desbloquear este recurso compartilhado:",
"ta1": "primeiro digite sua nova senha",
"ta2": "repita para confirmar a nova senha:",
"ta3": "há um erro; por favor, tente novamente",
"aa1": "arquivos de entrada:",
"ab1": "desativar no304",
"ac1": "ativar no304",
"ad1": "ativar no304 irá desabilitar todo o armazenamento em cache; tente isso se k304 não for suficiente. Isso irá desperdiçar uma grande quantidade de tráfego de rede!",
"ae1": "downloads ativos:",
"af1": "mostrar uploads recentes",
"ag1": "mostrar usuários IdP conhecidos"
},
"spa": {
"a1": "actualizar",
"b1": "hola &nbsp; <small>(no has iniciado sesión)</small>",
@ -192,6 +681,11 @@ var Ls = {
"j1": "activar k304 desconectará tu cliente en cada HTTP 304, lo que puede evitar que algunos proxies con errores se atasquen (dejando de cargar páginas de repente), <em>pero</em> también ralentizará las cosas en general",
"k1": "restablecer config. de cliente",
"l1": "inicia sesión para más:",
"ls3": "iniciar sesión", //m
"lu4": "nombre de usuario", //m
"lp4": "contraseña", //m
"lo3": "cerrar sesión de “{0}” en todas partes", //m
"lo2": "esto finalizará la sesión en todos los navegadores", //m
"m1": "bienvenido de nuevo,",
"n1": "404 no encontrado &nbsp;┐( ´ -`)┌",
"o1": '¿o quizás no tienes acceso? -- prueba con una contraseña o <a href=\"' + SR + '/?h\">vuelve al inicio</a>',
@ -218,6 +712,100 @@ var Ls = {
"af1": "mostrar subidas recientes",
"ag1": "mostrar usuarios IdP conocidos"
},
"swe": {
"a1": "uppdatera",
"b1": "tjena främling &nbsp; <small>(du är inte inloggad)</small>",
"c1": "logga ut",
"d1": "dumpa stacken",
"d2": "visar tillståndet på alla aktiva trådar",
"e1": "ladda om konfig.",
"e2": "ladda om konfigurationsfiler (konton/volymer/volflaggor),$Noch skanna om alla e2ds-volymer$N$Nobs.: ändrade globala inställningar$Nkräver en fullständig omstart",
"f1": "du kan bläddra:",
"g1": "du kan ladda upp till:",
"cc1": "annat:",
"h1": "avaktivera k304",
"i1": "aktivera k304",
"j1": "med k304 aktiverad kommer klienten att koppla bort sig vid varje HTTP 304-fel, vilket kan hindra vissa buggiga proxyservrar från att fastna (sidor slutar ladda), <em>men</em> saker kommer också att bli långsammare i allmänhet",
"k1": "återställ klientinställningar",
"l1": "logga in för att se mer:",
"ls3": "logga in", //m
"lu4": "användarnamn", //m
"lp4": "lösenord", //m
"lo3": "logga ut “{0}” överallt", //m
"lo2": "avsluta sessionen i alla webbläsare", //m
"m1": "välkommen tillbaka,",
"n1": "404 hittades inte &nbsp;┐( ´ -`)┌",
"o1": 'eller så har du kanske inte tillgång -- prova ett lösenord eller <a href="' + SR + '/?h">åk hem</a>',
"p1": "403 nekat &nbsp;~┻━┻",
"q1": 'använd ett lösenord eller <a href="' + SR + '/?h">åk hem</a>',
"r1": "åk hem",
".s1": "skanna om",
"t1": "åtgärd",
"u2": "tid sedan senaste serverskrivning$N( uppladdning / namnbyte / ... )$N$N17d = 17 dagar$N1h23 = 1 timme 23 minuter$N4m56 = 4 minuter 56 sekunder",
"v1": "koppla upp",
"v2": "använd denna server som en lokal disk",
"w1": "byt till https",
"x1": "byt lösenord",
"y1": "redigera utdelningar",
"z1": "lås upp denna utdelning:",
"ta1": "fyll i ditt nya lösenord",
"ta2": "upprepa det nya lösenordet:",
"ta3": "det blev fel; vänligen försök igen",
"aa1": "inkommande filer:",
"ab1": "avaktivera no304",
"ac1": "aktivera no304",
"ad1": "detta stänger av all cachning; prova detta om k304 inte räckte till. Detta kommer att slösa enorma mängder nätverkstrafik!",
"ae1": "aktiva nedladdningar:",
"af1": "visa senaste uppladdningar",
"ag1": "visa idp-cache"
},
"ukr": {
"a1": "оновити",
"b1": "привітик, незнайомцю &nbsp; <small>(ви не авторизовані)</small>",
"c1": "вийти",
"d1": "трасування стека",
"d2": "показує стан усіх активних потоків",
"e1": "перезавантажити конфіг",
"e2": "перезавантажити файли конфігурації (облікові записи/томи/прапорці),$Nта пересканувати всі томи e2ds$N$Nувага: будь-які зміни глобальних налаштувань$Nвимагають повного перезапуску",
"f1": "ви можете бачити:",
"g1": "ви можете завантажувати файли в:",
"cc1": "всяка всячина:",
"h1": "вимкнути k304",
"i1": "увімкнути k304",
"j1": "увімкнення k304 буде відключати ваш клієнт при кожному HTTP 304, що може запобігти зависанню деяких глючних проксі (раптово перестають завантажувати сторінки), <em>але</em> це також зробить усе повільнішим загалом",
"k1": "скинути налаштування клієнта",
"l1": "авторизуйтесь для інших опцій:",
"ls3": "увійти", //m
"lu4": "ім'я користувача", //m
"lp4": "пароль", //m
"lo3": "вийти з облікового запису “{0}” всюди", //m
"lo2": "це завершить сеанс у всіх браузерах", //m
"m1": "з поверненням,",
"n1": "404 не знайдено &nbsp;┐( ´ -`)┌",
"o1": 'або у вас немає доступу -- спробуйте авторизуватися або <a href="' + SR + '/?h">повернутися на головну</a>',
"p1": "403 доступ заборонений &nbsp;~┻━┻",
"q1": 'авторизуйтесь або <a href="' + SR + '/?h">поверніться на головну</a>',
"r1": "повернутися на головну",
".s1": "пересканувати",
"t1": "дія",
"u2": "час з останнього запису сервера$N( завантаження / перейменування / ... )$N$N17d = 17 днів$N1h23 = 1 година 23 хвилини$N4m56 = 4 хвилини 56 секунд",
"v1": "підключити",
"v2": "використовувати цей сервер як локальний HDD",
"w1": "перейти на https",
"x1": "змінити пароль",
"y1": "керування доступом",
"z1": "розблокувати:",
"ta1": "спочатку заповніть ваш новий пароль",
"ta2": "повторіть для підтвердження нового пароля:",
"ta3": "описка; спробуйте знову",
"aa1": "вхідні файли:",
"ab1": "вимкнути no304",
"ac1": "увімкнути no304",
"ad1": "увімкнення no304 вимкне все кешування; спробуйте це, якщо k304 було недостатньо. Це витратить величезну кількість мережевого трафіку!",
"ae1": "активні завантаження:",
"af1": "показати нещодавні завантаження",
"ag1": "показати відомих IdP-користувачів"
},
"rus": {
"a1": "обновить",
"b1": "приветик, незнакомец &nbsp; <small>(вы не авторизованы)</small>",
@ -234,6 +822,11 @@ var Ls = {
"j1": "включённый k304 будет отключать вас при получении HTTP 304, что может помочь при работе с некоторыми глючными прокси (перестают загружаться страницы), <em>но</em> это также сделает работу клиента медленнее",
"k1": "сбросить локальные настройки",
"l1": "авторизуйтесь для других опций:",
"ls3": "войти", //m
"lu4": "имя пользователя", //m
"lp4": "пароль", //m
"lo3": "выйти из “{0}” везде", //m
"lo2": "это завершит сеанс во всех браузерах", //m
"m1": "с возвращением,",
"n1": "404 не найдено &nbsp;┐( ´ -`)┌",
"o1": 'или у вас нет доступа -- попробуйте авторизоваться или <a href="' + SR + '/?h">вернуться на главную</a>',
@ -260,6 +853,47 @@ var Ls = {
"af1": "показать недавние загрузки",
"ag1": "показать известных IdP-пользователей",
},
"tur": {
"a1": "yenile",
"b1": "N'aber aga &nbsp; <small>(giriş yapmamışsın)</small>",
"c1": ıkış yap",
"d1": "yığını yolla",
"d2": "tüm aktif iş parçacıklarının durumunu gösterir",
"e1": "cfg'yi yenile",
"e2": "yapılandırma dosyalarını yenile (hesaplar/hacimler/hacim bayrakları),$Nve tüm e2ds hacimlerini yeniden tarayın$N$Nnot: global ayarlardaki herhangi bir değişiklik$Netkili hale gelmesi için tam bir yeniden başlatma gerektirir",
"f1": "göz atabilirsiniz:",
"g1": "yükleyebilirsiniz:",
"cc1": "diğer şeyler:",
"h1": "k304'ü devre dışı bırak",
"i1": "k304'ü etkinleştir",
"j1": "k304'ü etkinleştirmek, her HTTP 304'te istemcinizin bağlantısını keser, bu da bazı hatalı proxy'lerin takılmasını önleyebilir (sayfaların birdenbire yüklenmesinin durması gibi); <em>ama</em> bu, aynı zamanda genel olarak işleyişi yavaşlatır",
"k1": "istemci ayarlarını sıfırla",
"l1": "daha fazlası için giriş yap:",
"m1": "hoş geldin,",
"n1": "404 bulunamadı &nbsp;┐( ´ -`)┌",
"o1": 'ya da erişim iznin yok -- bir şifre dene veya <a href="' + SR + '/?h">ana sayfaya dön</a>',
"p1": "403 yasaklandı &nbsp;~┻━┻",
"q1": 'bir şifre kullan veya <a href="' + SR + '/?h">ana sayfaya dön</a>',
"r1": "ana sayfaya dön",
".s1": "yeniden tara",
"t1": "işlem",
"u2": "son sunucu yazma zamanı$N( yükleme / yeniden adlandırma / ... )$N$N17d = 17 gün$N1h23 = 1 saat 23 dakika$N4m56 = 4 dakika 56 saniye",
"v1": "bağlan",
"v2": "bu sunucuyu yerel HDD olarak kullan",
"w1": "https'ye geç",
"x1": "şifreyi değiştir",
"y1": "paylaşılanları düzenle",
"z1": "gizli paylaşımın kilidini aç:",
"ta1": "ilk önce yeni şifreyi doldur",
"ta2": "yeni şifreyi onaylamak için tekrar girin:",
"ta3": "bir yazım hatası bulundu; lütfen tekrar deneyin",
"aa1": "gelen dosyalar:",
"ab1": "no304'ü devre dışı bırak",
"ac1": "no304'ü etkinleştir",
"ad1": "no304'ü etkinleştirmek, tüm önbelleği devre dışı bırakır; bunu k304 yeterli olmadıysa deneyin. Bu, büyük miktarda ağ trafiği israf edecektir!",
"ae1": "aktif indirmeler:",
"af1": "son yüklemeleri göster",
},
};
if (window.langmod)
@ -268,6 +902,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),
@ -278,10 +914,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';
@ -290,15 +933,20 @@ try {
catch (ex) { }
tt.init();
var o = QS('input[name="cppwd"]');
if (!ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight)
var o = QS('input[name="uname"]') || QS('input[name="cppwd"]');
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';
(function() {
if (!ebi('x'))

View file

@ -31,19 +31,20 @@
<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>
</p>
{% 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 %}
@ -53,50 +54,64 @@
<div class="os win">
<p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p>
<pre>
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>W:</b>
</pre>
<ul>
{% if s %}
{%- if s %}
<li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li>
{% endif %}
{%- endif %}
<li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>
</ul>
<p>if you want to use the native WebDAV client in windows instead (slow and buggy), first run <a href="{{ r }}/.cpr/a/webdav-cfg.bat">webdav-cfg.bat</a> to remove the 47 MiB filesize limit (also fixes latency and password login), then connect:</p>
<pre>
{%- if un %}
net use <b>w:</b> http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} <b>{{ pw }}</b> /user:{{ b_un }}{% endif %}
{%- else %}
net use <b>w:</b> http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} k /user:<b>{{ pw }}</b>{% endif %}
{%- endif %}
</pre>
</div>
<div class="os lin">
<p>rclone (v1.63 or later) is recommended:</p>
<pre>
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>mp</b>
</pre>
<ul>
{% if s %}
{%- if s %}
<li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li>
{% endif %}
{%- endif %}
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
<li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>
</ul>
<p>alternatively use davfs2 (requires root, is slower, forgets lastmodified-timestamp on upload):</p>
<pre>
yum install davfs2
{%- if un %}
{% if accs %}printf '%s\n' {{ b_un }} <b>{{ pw }}</b> | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b>
{%- else %}
{% if accs %}printf '%s\n' <b>{{ pw }}</b> k | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b>
{%- endif %}
</pre>
{%- if accs %}
<p>make davfs2 automount on boot:</p>
<pre>
{%- if un %}
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} {{ b_un }} <b>{{ pw }}</b>" >> /etc/davfs2/secrets
{%- else %}
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>{{ pw }}</b> k" >> /etc/davfs2/secrets
{%- endif %}
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b> davfs rw,user,uid=1000,noauto 0 0" >> /etc/fstab
</pre>
{%- endif %}
<p>or the emergency alternative (gnome/gui-only):</p>
<!-- gnome-bug: ignores vp -->
<pre>
{%- if accs %}
echo <b>{{ pw }}</b> | gio mount dav{{ s }}://k@{{ ep }}/{{ rvp }}
echo <b>{{ pw }}</b> | gio mount dav{{ s }}://{{ b_un }}@{{ ep }}/{{ rvp }}
{%- else %}
gio mount -a dav{{ s }}://{{ ep }}/{{ rvp }}
{%- endif %}
@ -106,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 %}
@ -126,51 +141,71 @@
<div class="os win">
<p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p>
{% if args.ftp %}
{%- if args.ftp %}
<p>connect with plaintext FTP:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
{%- else %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>W:</b>
</pre>
{% endif %}
{% if args.ftps %}
{%- endif %}
{%- if args.ftps %}
<p>connect with TLS-encrypted FTPS:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
{%- else %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>W:</b>
</pre>
{% endif %}
{%- endif %}
<ul>
{% if args.ftps %}
{%- if args.ftps %}
<li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li>
{% endif %}
{%- endif %}
<li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>
</ul>
<p>if you want to use the native FTP client in windows instead (please dont), press <code>win+R</code> and run this command:</p>
<pre>
{%- if un %}
explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}{{ b_un }}:<b>{{ pw }}</b>@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
{%- else %}
explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}<b>{{ pw }}</b>:k@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
{%- endif %}
</pre>
</div>
<div class="os lin">
{% if args.ftp %}
{%- if args.ftp %}
<p>connect with plaintext FTP:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
{%- else %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>mp</b>
</pre>
{% endif %}
{% if args.ftps %}
{%- endif %}
{%- if args.ftps %}
<p>connect with TLS-encrypted FTPS:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
{%- else %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>mp</b>
</pre>
{% endif %}
{%- endif %}
<ul>
{% if args.ftps %}
{%- if args.ftps %}
<li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li>
{% endif %}
{%- endif %}
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
<li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>
</ul>
@ -178,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 %}
@ -188,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 %}
@ -203,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>
@ -216,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 %}
@ -233,7 +272,8 @@
<pre class="os mac">
open 'smb://<b>{{ pw }}</b>:k@{{ host }}/a'
</pre>
{% endif %}
{%- endif %}
{%- endif %}
@ -246,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}",
@ -259,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" }
@ -277,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}}' }}",
@ -294,7 +336,7 @@
<pre class="dl" name="flameshot.sh">
#!/bin/bash
pw="<b>{{ pw }}</b>"
pw="<b>{{ unpw }}</b>"
url="http{{ s }}://{{ ep }}/{{ rvp }}"
filename="$(date +%Y-%m%d-%H%M%S).png"
flameshot gui -s -r | curl -sT- "$url$filename?want=url&pw=$pw" | xsel -ib

View file

@ -49,21 +49,58 @@ function setos(os) {
setos(WINDOWS ? 'win' : LINUX ? 'lin' : MACOS ? 'mac' : 'idk');
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;
var pw0 = ebi('pw0').innerHTML,
oa = QSA('b');
pw = v;
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();
});
}
if (ebi('setpw'))
ebi('setpw').onclick = setpw;
ebi('qr').onclick = function () {
var url = ('' + location).split('?')[0];
if (pw)
url += '?pw=' + pw;
var txt = esc(url) + '<img class="b64" width="100" height="100" src="' + addq(url, 'qr') + '" />';
modal.alert(txt);
};

View file

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

View file

@ -50,7 +50,7 @@ catch (ex) {
}
catch (ex) {
console.log('up2k init failed:', ex);
toast.err(10, 'could not initialze up2k\n\n' + basenames(ex));
toast.err(10, 'could not initialize up2k\n\n' + basenames(ex));
}
}
treectl.onscroll();
@ -732,7 +732,7 @@ function Donut(uc, st) {
tstrober = setInterval(strobe, 300);
if (uc.upsfx && actx && actx.state != 'suspended')
sfx();
sfx_nice();
// firefox may forget that filedrops are user-gestures so it can skip this:
if (uc.upnag && Notification && Notification.permission == 'granted')
@ -745,8 +745,10 @@ function Donut(uc, st) {
if (!txt)
clearInterval(tstrober);
}
}
function sfx() {
function sfx_nice() {
if (true) {
var osc = actx.createOscillator(),
gain = actx.createGain(),
gg = gain.gain,
@ -964,6 +966,7 @@ function up2k_init(subtle) {
"t": 0
},
"car": 0,
"nre": 0,
"slow_io": null,
"oserr": false,
"modn": 0,
@ -1572,7 +1575,7 @@ function up2k_init(subtle) {
function linklist() {
var ret = [],
base = document.location.origin.replace(/\/$/, '');
base = location.origin.replace(/\/$/, '');
for (var a = 0; a < st.files.length; a++) {
var t = st.files[a],
@ -1595,7 +1598,7 @@ function up2k_init(subtle) {
ev(e);
var txt = linklist();
cliptxt(txt + '\n', function () {
toast.inf(5, un_clip.format(txt.split('\n').length));
toast.inf(5, L.un_clip.format(txt.split('\n').length));
});
};
@ -1783,8 +1786,7 @@ function up2k_init(subtle) {
}
var tasker = (function () {
var running = false,
was_busy = false;
var running = false;
var defer = function () {
running = false;
@ -1801,7 +1803,17 @@ function up2k_init(subtle) {
while (true) {
var now = Date.now(),
blocktime = now - r.tact,
is_busy = st.car < st.files.length;
was_busy = !!st.is_busy,
is_busy = !!( // gzip take the wheel
st.car < st.files.length ||
st.busy.hash.length ||
st.todo.hash.length ||
st.busy.handshake.length ||
st.todo.handshake.length ||
st.busy.upload.length ||
st.todo.upload.length ||
st.busy.head.length ||
st.todo.head.length);
if (blocktime > 2500)
console.log('main thread blocked for ' + blocktime);
@ -1809,7 +1821,16 @@ function up2k_init(subtle) {
r.tact = now;
if (was_busy && !is_busy) {
for (var a = 0; a < st.files.length; a++) {
var nre = 0, nf = 0;
for (var a = 0; a < st.files.length; a++)
if (st.files[a].want_recheck)
nre++;
console.log('nre', nre, 'st', st.nre);
if (st.nre != nre) {
st.nre = nre;
nf = st.files.length;
}
for (var a = 0; a < nf; a++) {
var t = st.files[a];
if (t.want_recheck) {
t.rechecks++;
@ -1817,7 +1838,7 @@ function up2k_init(subtle) {
push_t(st.todo.handshake, t);
}
}
is_busy = st.todo.handshake.length;
is_busy = !!st.todo.handshake.length;
try {
if (!is_busy && !uc.fsearch && !msel.getsel().length && (!mp.au || mp.au.paused))
treectl.goto();
@ -1826,7 +1847,7 @@ function up2k_init(subtle) {
}
if (was_busy != is_busy) {
st.is_busy = was_busy = is_busy;
st.is_busy = is_busy;
window[(is_busy ? "add" : "remove") +
"EventListener"]("beforeunload", warn_uploader_busy);
@ -1947,7 +1968,7 @@ function up2k_init(subtle) {
for (var a = 0; a < st.files.length; a++) {
var t = st.files[a];
if (t.want_recheck && !t.rechecks)
if (t.want_recheck && t.rechecks < 999)
return;
}
@ -2693,8 +2714,9 @@ function up2k_init(subtle) {
if (ofs !== -1) {
err = err.slice(0, ofs + 1) + linksplit(err.slice(ofs + 2).trimEnd()).join(' / ');
}
if (!t.rechecks && (err_pend || err_srcb)) {
if (!t.rechecks)
t.rechecks = 0;
if (t.rechecks < 999 && (err_pend || err_srcb)) {
t.want_recheck = true;
if (st.busy.upload.length || st.busy.handshake.length || st.bytes.uploaded) {
err = L.u_dupdefer;
@ -2811,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,
@ -3017,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);
}
@ -3083,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();
@ -3160,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;
@ -3401,6 +3427,8 @@ if (QS('#op_up2k.act'))
goto_up2k();
apply_perms({ "perms": perms, "frand": frand, "u2ts": u2ts });
if (ls0)
fileman.render();
(function () {

View file

@ -120,7 +120,7 @@ function esc(txt) {
function basenames(txt) {
return (txt + '').replace(/https?:\/\/[^ \/]+\//g, '/').replace(/js\?_=[a-zA-Z]{4}/g, 'js');
}
if ((document.location + '').indexOf(',rej,') + 1)
if ((location + '').indexOf(',rej,') + 1)
window.onunhandledrejection = function (e) {
var err = e.reason;
try {
@ -180,6 +180,9 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
if (!/\.js($|\?)/.exec(url))
return; // chrome debugger
if (url.indexOf('extension://') + 1)
return;
if (url.indexOf(' > eval') + 1 && !evalex_fatal)
return; // md timer
@ -738,7 +741,7 @@ function assert_vp(path) {
if (path.indexOf('//') + 1)
throw 'nonlocal1: ' + path;
var o = window.location.origin;
var o = location.origin;
if (have_URL && (new URL(path, o)).origin != o)
throw 'nonlocal2: ' + path;
}
@ -890,7 +893,7 @@ function uricom_adec(arr, li) {
function get_evpath() {
var ret = document.location.pathname;
var ret = location.pathname;
if (ret.indexOf('/') !== 0)
ret = '/' + ret;
@ -957,15 +960,93 @@ function f2f(val, nd) {
}
var HSZ_U = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
function humansize(b, terse) {
var i = 0, u = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
while (b >= 1000 && i < u.length - 1) {
b /= 1024;
i += 1;
}
var i = 0;
while (b >= 1000 && i < 5) { b /= 1024; i += 1; }
return (f2f(b, b >= 100 ? 0 : b >= 10 ? 1 : 2) +
' ' + (terse ? u[i].charAt(0) : u[i]));
' ' + (terse ? HSZ_U[i].charAt(0) : HSZ_U[i]));
}
function humansize_su(b) {
var i = 0;
while (b >= 1000 && i < 5) { b /= 1024; i += 1; }
return [b, HSZ_U[i]];
}
function humansize_0(b) {
return '' + b;
}
function humansize_1(b) {
return ('' + b).replace(/\B(?=(\d{3})+(?!\d))/g, " ");
}
function humansize_2g(b) {
var z = humansize_su(b), u = z[1].charAt(0); b = z[0];
return [f2f(b, b >= 100 ? 0 : b >= 10 ? 1 : 2) + ' ' + u, u];
}
function humansize_3g(b) {
var z = humansize_su(b), u = z[1].charAt(0); b = z[0];
return [f2f(b, b >= 10 ? 0 : 1) + ' ' + u, u];
}
function humansize_4g(b) {
var z = humansize_su(b), u = z[1]; b = z[0];
return [parseFloat(b.toFixed(b >= 100 ? 0 : b >= 10 ? 1 : 2)) + ' ' + u, u.charAt(0)];
}
function humansize_5g(b) {
var z = humansize_su(b), u = z[1]; b = z[0];
return [parseFloat(b.toFixed(b >= 10 ? 0 : 1)) + ' ' + u, u.charAt(0)];
}
function humansize_2(b) {
return humansize_2g(b)[0];
}
function humansize_3(b) {
return humansize_3g(b)[0];
}
function humansize_4(b) {
return humansize_4g(b)[0];
}
function humansize_5(b) {
return humansize_5g(b)[0];
}
function humansize_2c(b) {
var v = humansize_2g(b);
return '<span class="fsz_' + v[1].charAt(0) + '">' + v[0] + '</span>';
}
function humansize_3c(b) {
var v = humansize_3g(b);
return '<span class="fsz_' + v[1].charAt(0) + '">' + v[0] + '</span>';
}
function humansize_4c(b) {
var v = humansize_4g(b);
return '<span class="fsz_' + v[1].charAt(0) + '">' + v[0] + '</span>';
}
function humansize_5c(b) {
var v = humansize_5g(b);
return '<span class="fsz_' + v[1].charAt(0) + '">' + v[0] + '</span>';
}
function humansize_fuzzy(b) {
if (b <= 0) return "yes";
if (b <= 80) return "hullkort";
if (b <= 368640) return "5¼ DD";
if (b <= 1474560) return "save icon";
if (b <= 2880000) return "3½ Extended";
if (b <= 13107200) return "C90 Tape";
if (b <= 21000000) return "Floptical";
if (b <= 33554432) return "MPMan F10";
if (b <= 50000000) return "creditcardCD";
if (b <= 100663296) return "Zipdisk";
if (b <= 170000000) return "MD";
if (b <= 220200960) return "8cm CD";
if (b <= 737280000) return "CD-R";
if (b <= 900000000) return "UMD";
if (b <= 1300000000) return "GD-ROM";
if (b <= 4700000000) return "DVD";
if (b <= 9400000000) return "DVD-DL";
return "LTO";
}
var humansize_fmts = ['0', '1', '2', '2c', '3', '3c', '4', '4c', '5', '5c', 'fuzzy'];
window.filesizefun = (function () {
var v = sread('fszfmt', humansize_fmts);
return window['humansize_' + (v || window.dfszf)] || humansize_1;
})();
function humantime(v) {
@ -1017,9 +1098,13 @@ function lhumantime(v) {
if (!L || tp.length < 2 || tp[1].indexOf('$') + 1)
return t;
var ret = '';
for (var a = 0; a < tp.length; a += 2)
ret += tp[a] + ' ' + L['ht_' + tp[a + 1] + (tp[a]==1?1:2)] + L.ht_and;
var u, n, ret = '';
for (var a = 0; a < tp.length; a += 2) {
n = tp[a];
u = L.ht_h5 ? (n==1 ? 1 : (n>1&&n<5) ? 2 : 5) :
(n==1 ? 1 : 2);
ret += tp[a] + ' ' + L['ht_' + tp[a + 1] + u] + L.ht_and;
}
return ret.slice(0, -L.ht_and.length);
}
@ -1246,20 +1331,23 @@ function hist_replace(url) {
function sethash(hv) {
if (window.history && history.replaceState) {
hist_replace(document.location.pathname + document.location.search + '#' + hv);
hist_replace(location.pathname + location.search + '#' + hv);
}
else {
document.location.hash = hv;
location.hash = 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');
}
@ -1814,12 +1902,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')) {
@ -2171,6 +2259,19 @@ function bchrome() {
}
bchrome();
var XC_CMSG = {
502: "bad gateway (server offline)",
503: "server offline",
504: "gateway timeout (server busy)",
529: "gateway timeout (server busy)",
520: "unknown error from server",
521: "server offline",
523: "server offline",
522: "proxy timeout (server busy)",
524: "proxy timeout (server busy)",
598: "proxy timeout (server busy)",
599: "proxy timeout (server busy)",
};
var cf_cha_t = 0;
function xhrchk(xhr, prefix, e404, lvl, tag) {
if (xhr.status < 400 && xhr.status >= 200)
@ -2195,6 +2296,9 @@ function xhrchk(xhr, prefix, e404, lvl, tag) {
if (xhr.status == 404)
return toast.err(0, prefix + e404 + suf, tag);
if (XC_CMSG[xhr.status])
errtxt = XC_CMSG[xhr.status];
if (!xhr.status && !errtxt)
return toast.err(0, prefix + L.xhr0);

View file

@ -1,3 +1,475 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0923-2247 `v1.19.14` Voile, the Magic Library
## 🧪 new features
* #779 add [OPDS](https://opds.io/) support (thx @Scotsguy!) 6dbd9901
* copyparty can now serve books for [KOReader](https://koreader.rocks/)
* [the mandatory soundtrack](https://www.youtube.com/watch?v=F8Aex6tzH-s)
* #786 add Turkish translation (thx @NandeMD!) 549fe33f
* #808 support reading config-files in UTF8-BOM 5e4ff90b
* make more http-errors return a friendly errortext rather than the scary wall of html 9d066414
## 🩹 bugfixes
* #842 could not navpane into webroot if webroot is unmapped 0941fd4e
* upload-resume becomes funky when the OS/network is overloaded to the point where it starts dropping connections left and right -- the issue was reported on discord and I don't have a good way to reproduce it, but these changes may help and/or fix it:
* b136a5b0 panic and drop chunk reservations if client or connection glitches out
* 38df223b also drop reservations if subchunk logic hits an edgecase
## 🔧 other changes
* [versus.md](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) tweaks:
* #840 tooltips in the table headers (thx @guano!) e9ca36fa
* #839 sftpgo updates (thx @augustanational!) a053a663
## 🌠 fun facts
* this release is identical to v1.19.13 except [the pypi package isn't messed up](https://github.com/9001/copyparty/issues/847) 👉😎👉
* as if the 13 wasn't foreshadowing enough
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0921-2211 `v1.19.12` conlangparty
## 🧪 new features
* #787 add Esperanto translation (thx @slashdevslashurandom!) 15d3c2fb
* #802 timezone can be specified for the rotf upload rule (thx @Lehmustus!) 1460fe97
## 🩹 bugfixes
* #837 sharing an entire HDD on Windows ([v1.19.9](https://github.com/9001/copyparty/releases/tag/v1.19.9) regression) 6a244320
* sharing your whole [【Dドライブ】](https://www.youtube.com/watch?v=BFfYrxm2t58) is once again possible
* TLNote: `Dドライブ` means "D:\ drive"
* if you can't upgrade, a workaround is global-option `casechk: n`
* `/?ls` on an unmapped root didn't give a sensible response; now it should be okay except it won't have a `cfg` field 8f6194fe
## 🔧 other changes
* #836 hide the unpost tab in folders where user has no write-access ca872c40
* #835 fix webdep buildscript to avoid an edgecase on some platforms (thx @25huizengek1!) 260da2f4
## 🌠 fun facts
* the esperanto translation was the final straw; `copyparty-sfx.py` is now 1 MiB large
* `copyparty-en.py` is still a comfy 759 KiB
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0920-1011 `v1.19.11` ftp fix
## 🩹 bugfixes
* #827 ftp on servers with unmapped root broke in v1.19.9 280815f1
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0919-2244 `v1.19.10` ramdisk kinshi
## 🧪 new features
* prevent uploading into ramdisks by default 59a01221 538a205c
* safeguard against misconfigured docker containers, where certain parts of the vfs has not been mapped to actual storage, for example `/w/music` is but `/w/` itself isn't
* can be disabled with `wram` (global-option and/or volflag), mainly for ephemeral servers
* #799 nixos: groups can be specified (thx @AnyTimeTraveler!) ee5f3190
* the logspam from the filesystem indexer can be reduced/disabled 478f1c76
* new options `scan-st-r`, `scan-pr-r`, `scan-pr-s`
## 🩹 bugfixes
* #809 medialinks (`#af-badf00d`) would fail on the very first pageload from a new browser 5996a58b
* #806 instructions for running on iOS was bad (thx @GhelloZ!) 35326a6f
## 🔧 other changes
* copyparty32.exe is now english-only, to save space 669b1075
* version info on startup indicates free-threading or not 65591528
* docs: explain the `daw` option better a043d7cf
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0915-0019 `v1.19.9` case-sensitivity, give or take
## 🧪 new features
* #781 case-sensitive behavior is now simulated on Windows/Macos/Fat32/NTFS 8b66874b
* avoids some of the scary issues associated with case-insensitive filesystems
* unfortunately this is expensive and may be **noticeably slower in large folders;** disable the safeguard with `casechk: n` if you know you don't need it
* #789 case-insensitive search for unicode filenames/paths (thx @km-clay!) e2aa8fc1 ecd18adc
* default-disabled because it is somewhat expensive; enable with global-option `srch-icase`
* [CB-1](https://codeberg.org/9001/copyparty/issues/1) add `--qr-stdout` and `--qr-stderr` to show qr-code even with `-q` d7887f3d
## 🩹 bugfixes
* #775 the basic-uploader didn't accept empty files 25749b4b
* opt-out from index.html with `?v` did not work as documented 3d09bec1
* Windows: dedup could get rejected by the filesystem if the origin file had a timestamp from the cambrian era e09f3c9e
* webdav would incorrectly return an error for Depth:0 on an unmapped root 3a2381ff
* markdown-editor would waste another http roundtrip on certain documents 14b7e514
* `--help` didn't render if terminal was non-UTF8 3f454927
## 🔧 other changes
* #788 fixed a hotkey typo in the imageviewer (thx @tkroo!) 5c1a43c7
* #778 improved polish translation (thx @daimond113!) 52438bcc
* #798 debian: fixed an issue in the systemd script (thx @Beethoven-n, and congrats on commit number 4000!) dfd9e007
* media-tag `conductor` is no longer mapped to `circle` (album-artist) 9c9e4057
* "download-selection-as-zip" now produces a better filename, `sel-FOLDERNAME.zip` instead of `FIRSTFILE.zip` 8f587627
* detect and warn if IdP volumes are misconfigured in a particular way 83bd1974
## 🌠 fun facts
* the themesong of this release is [KO3 - Give it up?](https://www.youtube.com/watch?v=8w_na7HAppU) because that's what the car mechanic got to enjoy when i forgot to unplug the flashdrive before handing in the shitbox for service
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0907-2300 `v1.19.8` SECURITY: fix single-file shares
## ⚠️ ATTN: this release fixes [CVE-2025-58753](https://github.com/9001/copyparty/security/advisories/GHSA-pxvw-4w88-6x95), an issue with shares
* when a share is created for just one or more files inside a folder, it was possible to access the other files inside that folder by guessing the filenames
* it was not possible to descend into subdirectories in this manner; only the sibling files were accessible
* NOTE: this does NOT affect filekeys; this is specifically regarding the `shr` global-option
## recent important news
* [v1.19.8 (2025-09-07)](https://github.com/9001/copyparty/releases/tag/v1.19.8) fixed [CVE-2025-58753](https://github.com/9001/copyparty/security/advisories/GHSA-pxvw-4w88-6x95) (a missing permission-check inside single-file shares)
* [v1.15.0 (2024-09-08)](https://github.com/9001/copyparty/releases/tag/v1.15.0) changed upload deduplication to be default-disabled
* [v1.14.3 (2024-08-30)](https://github.com/9001/copyparty/releases/tag/v1.14.3) fixed a bug that was introduced in v1.13.8 (2024-08-13); this bug could lead to **data loss** -- see the v1.14.3 release-notes for details
## 🧪 new features
* #761 IdP: option to replace the login/logout links and buttons with redirects into an IdP UI 09f22993
* #726 disk-usage and server-version can be selectively hidden according to user permissions 19a4c453
* option `--shr-who` / volflag `shr_who` decides who is able to create a share of that volume edafa158
* #751 nixos: add globalExtraConfig to specify repeatable config parameters (thx @xvrqt!) 09e3018b
* some very small speedups (mainly u2c and ancient python versions) 74821a38
* #759 #393 total folder size now decreases when files inside are deleted 96b109b0
* would previously require a reindex to get back on track
## 🩹 bugfixes
* fix [GHSA-pxvw-4w88-6x95](https://github.com/9001/copyparty/security/advisories/GHSA-pxvw-4w88-6x95) by fencing fileshares to just the shared files e0a92ba7
* #397 prevent hinting at valid passwords, even if they cannot be used to authenticate with 7a4ee4db
* #747 disable some features if `/tmp` must be used for runtime config e6755aa8
* the config-folder will now also be created with chmod 700 (accessible by owner only)
* #733 #298 fix hotkeys on non-qwerty keyboard layouts (dvorak etc.) e798a9a5
* #539 ftp-server: support clients which never does a CWD b0496311
* ignore the plaintext session-cookie on https; fixes some confusing behavior when switching from https to http c71128fd
* `og-ua` would prevent clients matching the pattern from accessing fullsize files
* `og-ua` was only possible to set globally; the `og_ua` volflag was ignored 422f8f62
* uds / unix-domain-sockets got wrong permissions when `rm-sck` was used e270fe60
* #727 macos: support running from config-files 230a1462
* #539 avoid issues if someone uploads a file with a last-modified timestamp from year -9999999999999 eeb7738b
* using the spacebar to pause a video was jank on chrome bfcb6eac
* block the next-song hotkey while a folder is loading f7e08ed0
* #748 fix rare js-panic when an action is aborted aaeec11f
* #738 bubbleparty: use /bin/bash (thx @ckastner!) 0469b5a2
## 🔧 other changes
* partyfuse: nice speedup by caching `readdir` too 06d2654b
* partyfuse: explain usage with usernames 1cdb3880
* connect-page: better examples when usernames enabled 3bdef75e
* docker: fix image annotations ab562382
## 🌠 fun facts
* konami's biggest legacy lives on f0caf881 bd6d1f96
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 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
## 🧪 new features
* #426 add Dutch translation (thx @DeStilleGast!) 3798e19a
* #458 add Italian translation (thx @AOTREVAI!) a38e6e65
* #456 transcode to flac/wav (thx @missaustraliana!) b469db3c b2d48c64 0d09fb68
* #439 config-file can be provided through `PRTY_CONFIG` (thx @icxes!) 971360e9
* #459 videos can become folder thumbnails 16bbcce5
* add `--idp-cookie`, session-tickets for IdP auth (performance boost) f9502c3d
* useful when the IdP-server becomes a bottleneck
## 🩹 bugfixes
* #412 fix PUT-uploads into volumes with `nosub` volflag 47fa4a92
* #435 ignore spurious exceptions from browser extensions 39e55824
* #449 IPv6 QR-Code didn't include port 66a5bf36
* #295 do not force `d2d` in blank vfs (introduced in v1.18.3) 848315c0
## 🔧 other changes
* #440 improved finnish translation (thx @icxes!) a68d5b03
* point to the `-nc` option in the "at max connections" warning 153d240d
* the play-button now indicates "play-as-audio" for video-files 40d56bb3
* docs:
* #411 improve password-hashing instructions (thx @chinponya!) c69c7c8a
* #429 improve `--cert` helptext (thx @kzshantonu!) 7e3825f8
* #413 copyparty is Wii Internet Channel compatible! (thx @techflashYT!) 50f16293
* #461 how to use groups without IdP e85a7107
* mention that WebDAV and OpenGraph are incompatible by default (and how to fix that) 0bc1b8f7
* #345 short explanation about the sfx in quickstart ae5eefc5
* #398 pypi-package now has extra-group `all` 6eaf8af1
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0801-2056 `v1.18.9` fix Denial-of-Service
## ⚠️ ATTN: this release fixes a Denial-of-Service vuln
[CVE-2025-54796](https://github.com/9001/copyparty/security/advisories/GHSA-5662-2rj7-f2v6): an unauthenticated user could make the server grind to a halt by accessing a particular URL
## recent important news
* [v1.18.9 (2025-08-01)](https://github.com/9001/copyparty/releases/tag/v1.18.9) fixed [CVE-2025-54796](https://github.com/9001/copyparty/security/advisories/GHSA-5662-2rj7-f2v6) (Denial-of-Service)
* [v1.15.0 (2024-09-08)](https://github.com/9001/copyparty/releases/tag/v1.15.0) changed upload deduplication to be default-disabled
* [v1.14.3 (2024-08-30)](https://github.com/9001/copyparty/releases/tag/v1.14.3) fixed a bug that was introduced in v1.13.8 (2024-08-13); this bug could lead to **data loss** -- see the v1.14.3 release-notes for details
## 🧪 new features
* #310 translated to Spanish (thx @herruzo99!) a1dfd0be
* #350 translated to Ukrainian (thx @MrMebelMan!) fea45e45
* #321 translated to Russian (thx @A1Asriel!) 0b05c726
* #381 translated to Finnish (thx @icxes and @Permik!) 7ecedb2c
* haha it says surf
* #312 add option to use localtime in the UI ad23b253
* #386 initial packaging for debian (thx @Beethoven-n!) 3c6f0b17
## 🩹 bugfixes
* CVE-2025-54796 / GHSA-5662-2rj7-f2v6 09910ba8
* #347 fix upload-abort when uploading to a share 6d6d79fc
* fix xiu backlog dropping on restart 3222ba3a
* #375 fix crash on really old versions of python2.7 (thx @bb!) b69d5901
* #388 another python2.7 fix: improve unicode support in u2c (thx @KevinXuxuxu!) 9c197535
* log creator of new/blank markdown docs d0d2f206
* #400 config didn't support indenting with tabs c1604288
## 🔧 other changes
* `ack` was changed to `continue` 4fa7be2a
## 🌠 fun facts
* the translations have made the sfx size balloon from 766 to 845 KiB in under a week... nice! keep em coming :tada:
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0731-0833 `v1.18.8` sfx hotfix

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