mirror of
https://github.com/9001/copyparty.git
synced 2025-10-02 14:42:28 -06:00
Merge branch '9001:hovudstraum' into podman-systemd-service
This commit is contained in:
commit
f67e193cb0
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -43,3 +43,6 @@ scripts/docker/*.err
|
|||
|
||||
# nix build output link
|
||||
result
|
||||
|
||||
# IDEA config
|
||||
.idea/
|
||||
|
|
50
.vscode/launch.json
vendored
50
.vscode/launch.json
vendored
|
@ -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
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
230
README.md
|
@ -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
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# [`u2c.py`](u2c.py)
|
||||
* command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm)
|
||||
* file uploads, file-search, autoresume of aborted/broken uploads
|
||||
* sync local folder to server
|
||||
* [sync local folder to server](https://github.com/9001/copyparty/#folder-sync)
|
||||
* generally faster than browsers
|
||||
* if something breaks just restart it
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
# usage: ./bubbleparty.sh ./copyparty-sfx.py ....
|
||||
bwrap \
|
||||
--unshare-all \
|
||||
|
@ -9,7 +9,7 @@ bwrap \
|
|||
--dev-bind /dev /dev \
|
||||
--dir /tmp \
|
||||
--dir /var \
|
||||
--bind $(pwd) $(pwd) \
|
||||
--bind "$(pwd)" "$(pwd)" \
|
||||
--share-net \
|
||||
--die-with-parent \
|
||||
--file 11 /etc/passwd \
|
||||
|
|
|
@ -8,7 +8,7 @@ import sqlite3
|
|||
import argparse
|
||||
|
||||
DB_VER1 = 3
|
||||
DB_VER2 = 5
|
||||
DB_VER2 = 6
|
||||
|
||||
BY_PATH = None
|
||||
NC = None
|
||||
|
@ -39,7 +39,7 @@ def ls(db):
|
|||
print(f"{nfiles} files")
|
||||
print(f"{ntags} tags\n")
|
||||
|
||||
print("number of occurences for each tag,")
|
||||
print("number of occurrences for each tag,")
|
||||
print(" 'x' = file has no tags")
|
||||
print(" 't:mtp' = the mtp flag (file not mtp processed yet)")
|
||||
print()
|
||||
|
|
|
@ -46,7 +46,7 @@ def main(cli, vn, rem):
|
|||
|
||||
# uncomment one of these:
|
||||
send_http_302_temporary_redirect(cli, new_path)
|
||||
#send_http_301_permanent_redirect(cli, new_path)
|
||||
#send_errorpage_with_redirect_link(cli, new_path)
|
||||
# send_http_301_permanent_redirect(cli, new_path)
|
||||
# send_errorpage_with_redirect_link(cli, new_path)
|
||||
|
||||
return "true"
|
||||
|
|
|
@ -9,7 +9,7 @@ from plyer import notification
|
|||
_ = r"""
|
||||
show os notification on upload; works on windows, linux, macos, android
|
||||
|
||||
depdencies:
|
||||
dependencies:
|
||||
windows: python3 -m pip install --user -U plyer
|
||||
linux: python3 -m pip install --user -U plyer
|
||||
macos: python3 -m pip install --user -U plyer pyobjus
|
||||
|
|
60
bin/hooks/reject-and-explain.py
Normal file
60
bin/hooks/reject-and-explain.py
Normal 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()
|
|
@ -66,7 +66,7 @@ def main():
|
|||
try:
|
||||
sp.check_call(cmd)
|
||||
except:
|
||||
t = "-- FAILED TO DONWLOAD " + name
|
||||
t = "-- FAILED TO DOWNLOAD " + name
|
||||
print(f"{t}\n", end="")
|
||||
open(t, "wb").close()
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ example copyparty config to use this:
|
|||
--urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=guestbook=t10,ad,p,bin/mtag/guestbook-read.py:mte=+guestbook
|
||||
|
||||
explained:
|
||||
for realpath srv/hello (served at /hello), write-only for eveyrone,
|
||||
for realpath srv/hello (served at /hello), write-only for everyone,
|
||||
enable file analysis on upload (e2ts),
|
||||
use mtp plugin "bin/mtag/guestbook-read.py" to provide metadata tag "guestbook",
|
||||
do this on all uploads regardless of extension,
|
||||
|
|
|
@ -11,7 +11,7 @@ example copyparty config to use this:
|
|||
--urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=xgb=ebin,t10,ad,p,bin/mtag/guestbook.py:mte=+xgb
|
||||
|
||||
explained:
|
||||
for realpath srv/hello (served at /hello),write-only for eveyrone,
|
||||
for realpath srv/hello (served at /hello),write-only for everyone,
|
||||
enable file analysis on upload (e2ts),
|
||||
use mtp plugin "bin/mtag/guestbook.py" to provide metadata tag "xgb",
|
||||
do this on all uploads with the file extension "bin",
|
||||
|
|
|
@ -84,7 +84,7 @@ def main():
|
|||
# on success, delete the .bin file which contains the URL
|
||||
os.unlink(fp)
|
||||
except:
|
||||
open("-- FAILED TO DONWLOAD " + name, "wb").close()
|
||||
open("-- FAILED TO DOWNLOAD " + name, "wb").close()
|
||||
|
||||
os.unlink(tfn)
|
||||
print(url)
|
||||
|
|
|
@ -6,8 +6,8 @@ __copyright__ = 2019
|
|||
__license__ = "MIT"
|
||||
__url__ = "https://github.com/9001/copyparty/"
|
||||
|
||||
S_VERSION = "2.0"
|
||||
S_BUILD_DT = "2024-10-01"
|
||||
S_VERSION = "2.1"
|
||||
S_BUILD_DT = "2025-09-06"
|
||||
|
||||
"""
|
||||
mount a copyparty server (local or remote) as a filesystem
|
||||
|
@ -99,7 +99,7 @@ except:
|
|||
elif MACOS:
|
||||
libfuse = "install https://osxfuse.github.io/"
|
||||
else:
|
||||
libfuse = "apt install libfuse3-3\n modprobe fuse"
|
||||
libfuse = "apt install libfuse2\n modprobe fuse"
|
||||
|
||||
m = """\033[33m
|
||||
could not import fuse; these may help:
|
||||
|
@ -359,7 +359,7 @@ class Gateway(object):
|
|||
def sendreq(self, meth, path, headers, **kwargs):
|
||||
tid = get_tid()
|
||||
if self.password:
|
||||
headers["Cookie"] = "=".join(["cppwd", self.password])
|
||||
headers["PW"] = self.password
|
||||
|
||||
try:
|
||||
c = self.getconn(tid)
|
||||
|
@ -902,9 +902,7 @@ class CPPF(Operations):
|
|||
return ret
|
||||
|
||||
def _readdir(self, path, fh=None):
|
||||
path = path.strip("/")
|
||||
dbg("readdir %r [%s]", path, fh)
|
||||
|
||||
dbg("dircache miss")
|
||||
ret = self.gw.listdir(path)
|
||||
if not self.n_dircache:
|
||||
return ret
|
||||
|
@ -914,11 +912,17 @@ class CPPF(Operations):
|
|||
self.dircache.append(cn)
|
||||
self.clean_dircache()
|
||||
|
||||
# import pprint; pprint.pprint(ret)
|
||||
return ret
|
||||
|
||||
def readdir(self, path, fh=None):
|
||||
return [".", ".."] + list(self._readdir(path, fh))
|
||||
dbg("readdir %r [%s]", path, fh)
|
||||
path = path.strip("/")
|
||||
cn = self.get_cached_dir(path)
|
||||
if cn:
|
||||
ret = cn.data
|
||||
else:
|
||||
ret = self._readdir(path, fh)
|
||||
return [".", ".."] + list(ret)
|
||||
|
||||
def read(self, path, length, offset, fh=None):
|
||||
req_max = 1024 * 1024 * 8
|
||||
|
@ -993,7 +997,6 @@ class CPPF(Operations):
|
|||
if cn:
|
||||
dents = cn.data
|
||||
else:
|
||||
dbg("cache miss")
|
||||
dents = self._readdir(dirpath)
|
||||
|
||||
try:
|
||||
|
@ -1141,10 +1144,15 @@ def main():
|
|||
if WINDOWS:
|
||||
examples.append("http://192.168.1.69:3923/music/ M:")
|
||||
|
||||
epi = "example:" + ex_pre + ex_pre.join(examples)
|
||||
epi += """\n
|
||||
NOTE: if server has --usernames enabled, then password is "username:password"
|
||||
"""
|
||||
|
||||
ap = argparse.ArgumentParser(
|
||||
formatter_class=TheArgparseFormatter,
|
||||
description="mount a copyparty server as a local filesystem -- " + ver,
|
||||
epilog="example:" + ex_pre + ex_pre.join(examples),
|
||||
epilog=epi,
|
||||
)
|
||||
# fmt: off
|
||||
ap.add_argument("base_url", type=str, help="remote copyparty URL to mount")
|
||||
|
|
|
@ -141,7 +141,7 @@ chmod 777 "$jail/tmp"
|
|||
|
||||
|
||||
# create a dev
|
||||
(cd $jail; mkdir -p dev; cd dev
|
||||
(cd "$jail"; mkdir -p dev; cd dev
|
||||
[ -e null ] || mknod -m 666 null c 1 3
|
||||
[ -e zero ] || mknod -m 666 zero c 1 5
|
||||
[ -e random ] || mknod -m 444 random c 1 8
|
||||
|
|
20
bin/u2c.py
20
bin/u2c.py
|
@ -1,8 +1,8 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
S_VERSION = "2.11"
|
||||
S_BUILD_DT = "2025-05-18"
|
||||
S_VERSION = "2.13"
|
||||
S_BUILD_DT = "2025-09-05"
|
||||
|
||||
"""
|
||||
u2c.py: upload to copyparty
|
||||
|
@ -10,7 +10,7 @@ u2c.py: upload to copyparty
|
|||
https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py
|
||||
|
||||
- dependencies: no
|
||||
- supports python 2.6, 2.7, and 3.3 through 3.12
|
||||
- supports python 2.6, 2.7, and 3.3 through 3.14
|
||||
- if something breaks just try again and it'll autoresume
|
||||
"""
|
||||
|
||||
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
# generate the list of permitted IP ranges like so:
|
||||
# (curl -s https://www.cloudflare.com/ips-v{4,6} | sed 's/^/allow /; s/$/;/'; echo; echo "deny all;") > /etc/nginx/cloudflare-only.conf
|
||||
#
|
||||
# and then enable it below by uncomenting the cloudflare-only.conf line
|
||||
# and then enable it below by uncommenting the cloudflare-only.conf line
|
||||
#
|
||||
# ======================================================================
|
||||
|
||||
|
|
|
@ -50,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
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 "┗━━━━━━━━━━━━━━━──-"
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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/`
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
lib,
|
||||
stdenv,
|
||||
makeWrapper,
|
||||
buildPythonApplication,
|
||||
fetchurl,
|
||||
util-linux,
|
||||
python,
|
||||
setuptools,
|
||||
jinja2,
|
||||
impacket,
|
||||
pyopenssl,
|
||||
|
@ -15,6 +15,10 @@
|
|||
pyzmq,
|
||||
ffmpeg,
|
||||
mutagen,
|
||||
pyftpdlib,
|
||||
magic,
|
||||
partftpy,
|
||||
fusepy, # for partyfuse
|
||||
|
||||
# use argon2id-hashed passwords in config files (sha2 is always available)
|
||||
withHashedPasswords ? true,
|
||||
|
@ -40,12 +44,21 @@
|
|||
# send ZeroMQ messages from event-hooks
|
||||
withZeroMQ ? true,
|
||||
|
||||
# enable FTP server
|
||||
withFTP ? true,
|
||||
|
||||
# enable FTPS support in the FTP server
|
||||
withFTPS ? false,
|
||||
|
||||
# enable TFTP server
|
||||
withTFTP ? false,
|
||||
|
||||
# samba/cifs server; dangerous and buggy, enable if you really need it
|
||||
withSMB ? false,
|
||||
|
||||
# enables filetype detection for nameless uploads
|
||||
withMagic ? false,
|
||||
|
||||
# extra packages to add to the PATH
|
||||
extraPackages ? [ ],
|
||||
|
||||
|
@ -58,14 +71,23 @@
|
|||
|
||||
let
|
||||
pinData = lib.importJSON ./pin.json;
|
||||
pyEnv = python.withPackages (
|
||||
ps:
|
||||
with ps;
|
||||
runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg);
|
||||
in
|
||||
buildPythonApplication {
|
||||
pname = "copyparty";
|
||||
inherit (pinData) version;
|
||||
src = fetchurl {
|
||||
inherit (pinData) url hash;
|
||||
};
|
||||
dependencies =
|
||||
[
|
||||
jinja2
|
||||
fusepy
|
||||
]
|
||||
++ lib.optional withSMB impacket
|
||||
++ lib.optional withFTP pyftpdlib
|
||||
++ lib.optional withFTPS pyopenssl
|
||||
++ lib.optional withTFTP partftpy
|
||||
++ lib.optional withCertgen cfssl
|
||||
++ lib.optional withThumbnails pillow
|
||||
++ lib.optional withFastThumbnails pyvips
|
||||
|
@ -73,25 +95,14 @@ let
|
|||
++ lib.optional withBasicAudioMetadata mutagen
|
||||
++ lib.optional withHashedPasswords argon2-cffi
|
||||
++ lib.optional withZeroMQ pyzmq
|
||||
++ (extraPythonPackages ps)
|
||||
);
|
||||
++ lib.optional withMagic magic
|
||||
++ (extraPythonPackages python.pkgs);
|
||||
makeWrapperArgs = [ "--prefix PATH : ${lib.makeBinPath runtimeDeps}" ];
|
||||
|
||||
runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg);
|
||||
in
|
||||
stdenv.mkDerivation {
|
||||
pname = "copyparty";
|
||||
inherit (pinData) version;
|
||||
src = fetchurl {
|
||||
inherit (pinData) url hash;
|
||||
};
|
||||
nativeBuildInputs = [ makeWrapper ];
|
||||
dontUnpack = true;
|
||||
installPhase = ''
|
||||
install -Dm755 $src $out/share/copyparty-sfx.py
|
||||
makeWrapper ${pyEnv.interpreter} $out/bin/copyparty \
|
||||
--prefix PATH : ${lib.makeBinPath runtimeDeps} \
|
||||
--add-flag $out/share/copyparty-sfx.py
|
||||
'';
|
||||
pyproject = true;
|
||||
build-system = [
|
||||
setuptools
|
||||
];
|
||||
meta = {
|
||||
description = "Turn almost any device into a file server";
|
||||
longDescription = ''
|
||||
|
@ -101,8 +112,7 @@ stdenv.mkDerivation {
|
|||
homepage = "https://github.com/9001/copyparty";
|
||||
changelog = "https://github.com/9001/copyparty/releases/tag/v${pinData.version}";
|
||||
license = lib.licenses.mit;
|
||||
inherit (python.meta) platforms;
|
||||
mainProgram = "copyparty";
|
||||
sourceProvenance = [ lib.sourceTypes.binaryBytecode ];
|
||||
sourceProvenance = [ lib.sourceTypes.fromSource ];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.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="
|
||||
}
|
|
@ -11,14 +11,14 @@ import base64
|
|||
import json
|
||||
import hashlib
|
||||
import sys
|
||||
import re
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
OUTPUT_FILE = Path("pin.json")
|
||||
TARGET_ASSET = "copyparty-sfx.py"
|
||||
TARGET_ASSET = lambda version: f"copyparty-{version}.tar.gz"
|
||||
HASH_TYPE = "sha256"
|
||||
LATEST_RELEASE_URL = "https://api.github.com/repos/9001/copyparty/releases/latest"
|
||||
DOWNLOAD_URL = lambda version: f"https://github.com/9001/copyparty/releases/download/v{version}/{TARGET_ASSET}"
|
||||
DOWNLOAD_URL = lambda version: f"https://github.com/9001/copyparty/releases/download/v{version}/{TARGET_ASSET(version)}"
|
||||
|
||||
|
||||
def get_formatted_hash(binary):
|
||||
|
@ -29,11 +29,13 @@ def get_formatted_hash(binary):
|
|||
return f"{HASH_TYPE}-{encoded_hash}"
|
||||
|
||||
|
||||
def version_from_sfx(binary):
|
||||
result = re.search(b'^VER = "(.*)"$', binary, re.MULTILINE)
|
||||
if result:
|
||||
return result.groups(1)[0].decode("ascii")
|
||||
def version_from_tar_gz(path):
|
||||
with tarfile.open(path) as tarball:
|
||||
release_name = tarball.getmembers()[0].name
|
||||
prefix = "copyparty-"
|
||||
|
||||
if release_name.startswith(prefix):
|
||||
return release_name.replace(prefix, "")
|
||||
raise ValueError("version not found in provided file")
|
||||
|
||||
|
||||
|
@ -42,7 +44,7 @@ def remote_release_pin():
|
|||
|
||||
response = requests.get(LATEST_RELEASE_URL).json()
|
||||
version = response["tag_name"].lstrip("v")
|
||||
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET][0]
|
||||
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET(version)][0]
|
||||
download_url = asset_info["browser_download_url"]
|
||||
asset = requests.get(download_url)
|
||||
formatted_hash = get_formatted_hash(asset.content)
|
||||
|
@ -52,10 +54,9 @@ def remote_release_pin():
|
|||
|
||||
|
||||
def local_release_pin(path):
|
||||
asset = path.read_bytes()
|
||||
version = version_from_sfx(asset)
|
||||
version = version_from_tar_gz(path)
|
||||
download_url = DOWNLOAD_URL(version)
|
||||
formatted_hash = get_formatted_hash(asset)
|
||||
formatted_hash = get_formatted_hash(path.read_bytes())
|
||||
|
||||
result = {"url": download_url, "version": version, "hash": formatted_hash}
|
||||
return result
|
||||
|
|
11
contrib/package/nix/overlay.nix
Normal file
11
contrib/package/nix/overlay.nix
Normal file
|
@ -0,0 +1,11 @@
|
|||
final: prev: {
|
||||
copyparty = final.python3.pkgs.callPackage ./copyparty {
|
||||
ffmpeg = final.ffmpeg-full;
|
||||
};
|
||||
|
||||
python3 = prev.python3.override {
|
||||
packageOverrides = pyFinal: pyPrev: {
|
||||
partftpy = pyFinal.callPackage ./partftpy { };
|
||||
};
|
||||
};
|
||||
}
|
30
contrib/package/nix/partftpy/default.nix
Normal file
30
contrib/package/nix/partftpy/default.nix
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchurl,
|
||||
setuptools,
|
||||
}:
|
||||
let
|
||||
pinData = lib.importJSON ./pin.json;
|
||||
in
|
||||
|
||||
buildPythonPackage rec {
|
||||
pname = "partftpy";
|
||||
inherit (pinData) version;
|
||||
pyproject = true;
|
||||
|
||||
src = fetchurl {
|
||||
inherit (pinData) url hash;
|
||||
};
|
||||
|
||||
build-system = [ setuptools ];
|
||||
|
||||
pythonImportsCheck = [ "partftpy.TftpServer" ];
|
||||
|
||||
meta = {
|
||||
description = "Pure Python TFTP library (copyparty edition)";
|
||||
homepage = "https://github.com/9001/partftpy";
|
||||
changelog = "https://github.com/9001/partftpy/releases/tag/${version}";
|
||||
license = lib.licenses.mit;
|
||||
};
|
||||
}
|
5
contrib/package/nix/partftpy/pin.json
Normal file
5
contrib/package/nix/partftpy/pin.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"url": "https://github.com/9001/partftpy/releases/download/v0.4.0/partftpy-0.4.0.tar.gz",
|
||||
"version": "0.4.0",
|
||||
"hash": "sha256-5Q2zyuJ892PGZmb+YXg0ZPW/DK8RDL1uE0j5HPd4We0="
|
||||
}
|
50
contrib/package/nix/partftpy/update.py
Executable file
50
contrib/package/nix/partftpy/update.py
Executable file
|
@ -0,0 +1,50 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Update the Nix package pin
|
||||
#
|
||||
# Usage: ./update.sh
|
||||
|
||||
import base64
|
||||
import json
|
||||
import hashlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
OUTPUT_FILE = Path("pin.json")
|
||||
TARGET_ASSET = lambda version: f"partftpy-{version}.tar.gz"
|
||||
HASH_TYPE = "sha256"
|
||||
LATEST_RELEASE_URL = "https://api.github.com/repos/9001/partftpy/releases/latest"
|
||||
|
||||
|
||||
def get_formatted_hash(binary):
|
||||
hasher = hashlib.new("sha256")
|
||||
hasher.update(binary)
|
||||
asset_hash = hasher.digest()
|
||||
encoded_hash = base64.b64encode(asset_hash).decode("ascii")
|
||||
return f"{HASH_TYPE}-{encoded_hash}"
|
||||
|
||||
|
||||
def remote_release_pin():
|
||||
import requests
|
||||
|
||||
response = requests.get(LATEST_RELEASE_URL).json()
|
||||
version = response["tag_name"].lstrip("v")
|
||||
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET(version)][0]
|
||||
download_url = asset_info["browser_download_url"]
|
||||
asset = requests.get(download_url)
|
||||
formatted_hash = get_formatted_hash(asset.content)
|
||||
|
||||
result = {"url": download_url, "version": version, "hash": formatted_hash}
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
result = remote_release_pin()
|
||||
|
||||
print(result)
|
||||
json_result = json.dumps(result, indent=4)
|
||||
OUTPUT_FILE.write_text(json_result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
stdenvNoCC,
|
||||
copyparty,
|
||||
python3,
|
||||
makeBinaryWrapper,
|
||||
}:
|
||||
let
|
||||
python = python3.withPackages (p: [ p.fusepy ]);
|
||||
in
|
||||
stdenvNoCC.mkDerivation {
|
||||
pname = "partyfuse";
|
||||
inherit (copyparty) version meta;
|
||||
src = ../../../..;
|
||||
|
||||
nativeBuildInputs = [ makeBinaryWrapper ];
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
install -Dm444 bin/partyfuse.py -t $out/share/copyparty
|
||||
makeWrapper ${python.interpreter} $out/bin/partyfuse \
|
||||
--add-flag $out/share/copyparty/partyfuse.py
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
{
|
||||
stdenvNoCC,
|
||||
copyparty,
|
||||
python312,
|
||||
makeBinaryWrapper,
|
||||
}:
|
||||
stdenvNoCC.mkDerivation {
|
||||
pname = "u2c";
|
||||
inherit (copyparty) version meta;
|
||||
src = ../../../..;
|
||||
|
||||
nativeBuildInputs = [ makeBinaryWrapper ];
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
install -Dm444 bin/u2c.py -t $out/share/copyparty
|
||||
mkdir $out/bin
|
||||
makeWrapper ${python312.interpreter} $out/bin/u2c \
|
||||
--add-flag $out/share/copyparty/u2c.py
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
}
|
62
contrib/package/rpm/copyparty.spec
Normal file
62
contrib/package/rpm/copyparty.spec
Normal file
|
@ -0,0 +1,62 @@
|
|||
Name: copyparty
|
||||
Version: $pkgver
|
||||
Release: $pkgrel
|
||||
License: MIT
|
||||
Group: Utilities
|
||||
URL: https://github.com/9001/copyparty
|
||||
Source0: copyparty-$pkgver.tar.gz
|
||||
Summary: File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++
|
||||
BuildArch: noarch
|
||||
BuildRequires: python3, python3-devel, pyproject-rpm-macros, python-setuptools, python-wheel, make
|
||||
Requires: python3, (python3-jinja2 or python-jinja2), lsof
|
||||
Recommends: ffmpeg, (golang-github-cloudflare-cfssl or cfssl), python-mutagen, python-pillow, python-pyvips
|
||||
Recommends: qm-vamp-plugins, python-argon2-cffi, (python-pyopenssl or pyopenssl), python-impacket
|
||||
|
||||
%description
|
||||
Portable file server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps
|
||||
|
||||
See release at https://github.com/9001/copyparty/releases
|
||||
|
||||
%global debug_package %{nil}
|
||||
|
||||
%generate_buildrequires
|
||||
%pyproject_buildrequires
|
||||
|
||||
%prep
|
||||
%setup -q
|
||||
|
||||
%build
|
||||
cd "copyparty/web"
|
||||
make
|
||||
cd -
|
||||
%pyproject_wheel
|
||||
|
||||
%install
|
||||
mkdir -p %{buildroot}%{_bindir}
|
||||
mkdir -p %{buildroot}%{_libdir}/systemd/{system,user}
|
||||
mkdir -p %{buildroot}/etc/%{name}
|
||||
mkdir -p %{buildroot}/var/lib/%{name}-jail
|
||||
mkdir -p %{buildroot}%{_datadir}/licenses/%{name}
|
||||
|
||||
%pyproject_install
|
||||
%pyproject_save_files copyparty
|
||||
|
||||
install -m 0755 bin/prisonparty.sh %{buildroot}%{_bindir}/prisonpary.sh
|
||||
install -m 0644 contrib/systemd/%{name}.conf %{buildroot}/etc/%{name}/%{name}.conf
|
||||
install -m 0644 contrib/systemd/%{name}@.service %{buildroot}%{_libdir}/systemd/system/%{name}@.service
|
||||
install -m 0644 contrib/systemd/%{name}-user.service %{buildroot}%{_libdir}/systemd/user/%{name}.service
|
||||
install -m 0644 contrib/systemd/prisonparty@.service %{buildroot}%{_libdir}/systemd/system/prisonparty@.service
|
||||
install -m 0644 contrib/systemd/index.md %{buildroot}/var/lib/%{name}-jail/README.md
|
||||
install -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE
|
||||
|
||||
%files -n copyparty -f %{pyproject_files}
|
||||
%license LICENSE
|
||||
%{_bindir}/copyparty
|
||||
%{_bindir}/partyfuse
|
||||
%{_bindir}/u2c
|
||||
%{_bindir}/prisonpary.sh
|
||||
/etc/%{name}/%{name}.conf
|
||||
%{_libdir}/systemd/system/%{name}@.service
|
||||
%{_libdir}/systemd/user/%{name}.service
|
||||
%{_libdir}/systemd/system/prisonparty@.service
|
||||
/var/lib/%{name}-jail/README.md
|
|
@ -101,10 +101,10 @@
|
|||
|
||||
function our_hotkey_handler(e) {
|
||||
// bail if either ALT, CTRL, or SHIFT is pressed
|
||||
if (e.altKey || e.shiftKey || e.isComposing || ctrl(e))
|
||||
if (anymod(e))
|
||||
return main_hotkey_handler(e); // let copyparty handle this keystroke
|
||||
|
||||
var key_name = (e.code || e.key) + '',
|
||||
var keycode = (e.key || e.code) + '',
|
||||
ae = document.activeElement,
|
||||
aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : '';
|
||||
|
||||
|
@ -114,7 +114,7 @@
|
|||
if (aet && !/^(a|button|tr|td|div|pre)$/.test(aet))
|
||||
return main_hotkey_handler(e); // let copyparty handle this keystroke
|
||||
|
||||
if (key_name == 'KeyW') {
|
||||
if (keycode == 'w' || keycode == 'KeyW') {
|
||||
// okay, this one's for us... do the thing
|
||||
action_to_perform();
|
||||
return ev(e);
|
||||
|
|
71
contrib/setup-ashell.sh
Normal file
71
contrib/setup-ashell.sh
Normal file
|
@ -0,0 +1,71 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# this script will install copyparty onto an iOS device (iPhone/iPad)
|
||||
#
|
||||
# step 1: install a-Shell:
|
||||
# https://apps.apple.com/us/app/a-shell/id1473805438
|
||||
#
|
||||
# step 2: copypaste the following command into a-Shell:
|
||||
# curl -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
|
26
contrib/systemd/copyparty-user.service
Normal file
26
contrib/systemd/copyparty-user.service
Normal 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
|
|
@ -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
|
42
contrib/systemd/copyparty.example.conf
Normal file
42
contrib/systemd/copyparty.example.conf
Normal 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
|
30
contrib/systemd/copyparty@.service
Normal file
30
contrib/systemd/copyparty@.service
Normal 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
10
contrib/systemd/index.md
Normal 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`
|
|
@ -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
|
|
@ -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
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
real = list(statdir(self.log, scandir, lstat, abspath, throw))
|
||||
real.sort()
|
||||
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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import traceback
|
||||
|
||||
from queue import Queue
|
||||
|
||||
|
|
|
@ -130,6 +130,7 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
|||
nlog: "NamedLogger" = lambda msg, c=0: log("cert-gen-srv", msg, c)
|
||||
|
||||
names = args.crt_ns.split(",") if args.crt_ns else []
|
||||
names = [x.strip() for x in names]
|
||||
if not args.crt_exact:
|
||||
for n in names[:]:
|
||||
names.append("*.{}".format(n))
|
||||
|
|
|
@ -44,6 +44,7 @@ def vf_bmap() -> dict[str, str]:
|
|||
"gsel",
|
||||
"hardlink",
|
||||
"magic",
|
||||
"md_no_br",
|
||||
"no_db_ip",
|
||||
"no_sb_md",
|
||||
"no_sb_lg",
|
||||
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,27 +486,37 @@ class FtpHandler(FTPHandler):
|
|||
def ftp_STOR(self, file: str, mode: str = "w") -> Any:
|
||||
# Optional[str]
|
||||
vp = join(self.fs.cwd, file).lstrip("/")
|
||||
ap, vfs, rem = self.fs.v2a(vp, w=True)
|
||||
try:
|
||||
ap, vfs, rem = self.fs.v2a(vp, w=True)
|
||||
except Exception as ex:
|
||||
self.respond("550 %s" % (ex,), logging.info)
|
||||
return
|
||||
self.vfs_map[ap] = vp
|
||||
xbu = vfs.flags.get("xbu")
|
||||
if xbu and not runhook(
|
||||
None,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.ftpd",
|
||||
xbu,
|
||||
ap,
|
||||
vp,
|
||||
"",
|
||||
self.uname,
|
||||
self.hub.asrv.vfs.get_perms(vp, self.uname),
|
||||
0,
|
||||
0,
|
||||
self.cli_ip,
|
||||
time.time(),
|
||||
"",
|
||||
):
|
||||
raise FSE("Upload blocked by xbu server config")
|
||||
if xbu:
|
||||
hr = runhook(
|
||||
None,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.ftpd",
|
||||
xbu,
|
||||
ap,
|
||||
vp,
|
||||
"",
|
||||
self.uname,
|
||||
self.hub.asrv.vfs.get_perms(vp, self.uname),
|
||||
0,
|
||||
0,
|
||||
self.cli_ip,
|
||||
time.time(),
|
||||
"",
|
||||
)
|
||||
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
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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> ")
|
||||
|
|
|
@ -246,24 +246,29 @@ class SMB(object):
|
|||
|
||||
ap = absreal(ap)
|
||||
xbu = vfs.flags.get("xbu")
|
||||
if xbu and not runhook(
|
||||
self.nlog,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.smb",
|
||||
xbu,
|
||||
ap,
|
||||
vpath,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
"1.7.6.2",
|
||||
time.time(),
|
||||
"",
|
||||
):
|
||||
yeet("blocked by xbu server config: %r" % (vpath,))
|
||||
if xbu:
|
||||
hr = runhook(
|
||||
self.nlog,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.smb",
|
||||
xbu,
|
||||
ap,
|
||||
vpath,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
"1.7.6.2",
|
||||
time.time(),
|
||||
"",
|
||||
)
|
||||
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 = "?"
|
||||
|
|
|
@ -200,6 +200,25 @@ class QrCode(object):
|
|||
|
||||
return "\n".join(rows)
|
||||
|
||||
def to_png(self, zoom, pad, bg, fg, ap) -> None:
|
||||
from PIL import Image
|
||||
|
||||
tab = self.modules
|
||||
sz = self.size
|
||||
psz = sz + pad * 2
|
||||
if bg:
|
||||
img = Image.new("RGB", (psz, psz), bg)
|
||||
else:
|
||||
img = Image.new("RGBA", (psz, psz), (0, 0, 0, 0))
|
||||
fg = (fg[0], fg[1], fg[2], 255)
|
||||
for y in range(sz):
|
||||
for x in range(sz):
|
||||
if tab[y][x]:
|
||||
img.putpixel((x + pad, y + pad), fg)
|
||||
if zoom != 1:
|
||||
img = img.resize((sz * zoom, sz * zoom), Image.Resampling.NEAREST)
|
||||
img.save(ap)
|
||||
|
||||
def _draw_function_patterns(self) -> None:
|
||||
# Draw horizontal and vertical timing patterns
|
||||
for i in range(self.size):
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
|
@ -26,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:
|
||||
self.log("qr-code", self.tcpsrv.qr)
|
||||
if self.args.qr_pin:
|
||||
self.sticky_qr()
|
||||
if self.args.qr_wait or self.args.qr_every or self.args.qr_winch:
|
||||
Daemon(self._qr_thr, "qr")
|
||||
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:
|
||||
|
|
|
@ -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
|
||||
bound: Optional[socket.socket] = None
|
||||
tcp = False
|
||||
|
||||
if "unix:" in ip:
|
||||
tcp = False
|
||||
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
|
||||
|
|
|
@ -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,24 +363,29 @@ class Tftpd(object):
|
|||
yeet("blocked write; folder not world-deletable: /%s" % (vpath,))
|
||||
|
||||
xbu = vfs.flags.get("xbu")
|
||||
if xbu and not runhook(
|
||||
self.nlog,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.tftpd",
|
||||
xbu,
|
||||
ap,
|
||||
vpath,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
"8.3.8.7",
|
||||
time.time(),
|
||||
"",
|
||||
):
|
||||
yeet("blocked by xbu server config: %r" % (vpath,))
|
||||
if xbu:
|
||||
hr = runhook(
|
||||
self.nlog,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.tftpd",
|
||||
xbu,
|
||||
ap,
|
||||
vpath,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
"8.3.8.7",
|
||||
time.time(),
|
||||
"",
|
||||
)
|
||||
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)
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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,7 +86,10 @@ try:
|
|||
if os.environ.get("PRTY_NO_PIL_HEIF"):
|
||||
raise Exception()
|
||||
|
||||
from pyheif_pillow_opener import register_heif_opener
|
||||
try:
|
||||
from pillow_heif import register_heif_opener
|
||||
except ImportError:
|
||||
from pyheif_pillow_opener import register_heif_opener
|
||||
|
||||
register_heif_opener()
|
||||
HAVE_HEIF = True
|
||||
|
@ -112,14 +116,28 @@ except:
|
|||
|
||||
try:
|
||||
if os.environ.get("PRTY_NO_VIPS"):
|
||||
raise Exception()
|
||||
raise ImportError()
|
||||
|
||||
HAVE_VIPS = True
|
||||
import pyvips
|
||||
|
||||
logging.getLogger("pyvips").setLevel(logging.WARNING)
|
||||
except:
|
||||
except Exception as e:
|
||||
HAVE_VIPS = False
|
||||
if not isinstance(e, ImportError):
|
||||
logging.warning("libvips found, but failed to load: " + str(e))
|
||||
|
||||
|
||||
try:
|
||||
if os.environ.get("PRTY_NO_RAW"):
|
||||
raise Exception()
|
||||
|
||||
HAVE_RAW = True
|
||||
import rawpy
|
||||
|
||||
logging.getLogger("rawpy").setLevel(logging.WARNING)
|
||||
except:
|
||||
HAVE_RAW = False
|
||||
|
||||
|
||||
th_dir_cache = {}
|
||||
|
@ -205,11 +223,19 @@ class ThumbSrv(object):
|
|||
if self.args.th_clean:
|
||||
Daemon(self.cleaner, "thumb.cln")
|
||||
|
||||
self.fmt_pil, self.fmt_vips, self.fmt_ffi, self.fmt_ffv, self.fmt_ffa = [
|
||||
(
|
||||
self.fmt_pil,
|
||||
self.fmt_vips,
|
||||
self.fmt_raw,
|
||||
self.fmt_ffi,
|
||||
self.fmt_ffv,
|
||||
self.fmt_ffa,
|
||||
) = [
|
||||
set(y.split(","))
|
||||
for y in [
|
||||
self.args.th_r_pil,
|
||||
self.args.th_r_vips,
|
||||
self.args.th_r_raw,
|
||||
self.args.th_r_ffi,
|
||||
self.args.th_r_ffv,
|
||||
self.args.th_r_ffa,
|
||||
|
@ -232,6 +258,9 @@ class ThumbSrv(object):
|
|||
if "vips" in self.args.th_dec:
|
||||
self.thumbable |= self.fmt_vips
|
||||
|
||||
if "raw" in self.args.th_dec:
|
||||
self.thumbable |= self.fmt_raw
|
||||
|
||||
if "ff" in self.args.th_dec:
|
||||
for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]:
|
||||
self.thumbable |= zss
|
||||
|
@ -313,6 +342,7 @@ class ThumbSrv(object):
|
|||
"thumbable": self.thumbable,
|
||||
"pil": self.fmt_pil,
|
||||
"vips": self.fmt_vips,
|
||||
"raw": self.fmt_raw,
|
||||
"ffi": self.fmt_ffi,
|
||||
"ffv": self.fmt_ffv,
|
||||
"ffa": self.fmt_ffa,
|
||||
|
@ -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,35 +512,38 @@ class ThumbSrv(object):
|
|||
|
||||
return im
|
||||
|
||||
def conv_image_pil(self, im: "Image.Image", tpath: str, fmt: str, vn: VFS) -> None:
|
||||
try:
|
||||
im = self.fancy_pillow(im, fmt, vn)
|
||||
except Exception as ex:
|
||||
self.log("fancy_pillow {}".format(ex), "90")
|
||||
im.thumbnail(self.getres(vn, fmt))
|
||||
|
||||
fmts = ["RGB", "L"]
|
||||
args = {"quality": 40}
|
||||
|
||||
if tpath.endswith(".webp"):
|
||||
# quality 80 = pillow-default
|
||||
# quality 75 = ffmpeg-default
|
||||
# method 0 = pillow-default, fast
|
||||
# method 4 = ffmpeg-default
|
||||
# method 6 = max, slow
|
||||
fmts.extend(("RGBA", "LA"))
|
||||
args["method"] = 6
|
||||
else:
|
||||
# default q = 75
|
||||
args["progressive"] = True
|
||||
|
||||
if im.mode not in fmts:
|
||||
# print("conv {}".format(im.mode))
|
||||
im = im.convert("RGB")
|
||||
|
||||
im.save(tpath, **args)
|
||||
|
||||
def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
self.wait4ram(0.2, tpath)
|
||||
with Image.open(fsenc(abspath)) as im:
|
||||
try:
|
||||
im = self.fancy_pillow(im, fmt, vn)
|
||||
except Exception as ex:
|
||||
self.log("fancy_pillow {}".format(ex), "90")
|
||||
im.thumbnail(self.getres(vn, fmt))
|
||||
|
||||
fmts = ["RGB", "L"]
|
||||
args = {"quality": 40}
|
||||
|
||||
if tpath.endswith(".webp"):
|
||||
# quality 80 = pillow-default
|
||||
# quality 75 = ffmpeg-default
|
||||
# method 0 = pillow-default, fast
|
||||
# method 4 = ffmpeg-default
|
||||
# method 6 = max, slow
|
||||
fmts.extend(("RGBA", "LA"))
|
||||
args["method"] = 6
|
||||
else:
|
||||
# default q = 75
|
||||
args["progressive"] = True
|
||||
|
||||
if im.mode not in fmts:
|
||||
# print("conv {}".format(im.mode))
|
||||
im = im.convert("RGB")
|
||||
|
||||
im.save(tpath, **args)
|
||||
self.conv_image_pil(im, tpath, fmt, vn)
|
||||
|
||||
def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
self.wait4ram(0.2, tpath)
|
||||
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
liname = iname.lower()
|
||||
if (
|
||||
sz
|
||||
and (
|
||||
if sz:
|
||||
liname = iname.lower()
|
||||
ext = liname.rsplit(".", 1)[-1]
|
||||
if (
|
||||
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
|
||||
):
|
||||
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)
|
||||
n_tags = self._tagscan_file(cur, entags, w, abspath, ip, at, un)
|
||||
else:
|
||||
oth_tags = {}
|
||||
if ip:
|
||||
oth_tags = {"up_ip": ip, "up_at": at}
|
||||
else:
|
||||
oth_tags = {}
|
||||
oth_tags["up_ip"] = ip
|
||||
if at:
|
||||
oth_tags["up_at"] = at
|
||||
if un:
|
||||
oth_tags["up_by"] = un
|
||||
|
||||
mpool.put(Mpqe({}, entags, w, abspath, oth_tags))
|
||||
with self.mutex:
|
||||
|
@ -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,14 +3684,15 @@ class Up2k(object):
|
|||
t = t.format(job["name"], nchunks[0][0], coffsets[0][0], cur_sz)
|
||||
raise Pebkac(400, t)
|
||||
|
||||
job["busy"][chash] = 1
|
||||
for chash in chashes:
|
||||
job["busy"][chash] = 1
|
||||
|
||||
job["poke"] = time.time()
|
||||
|
||||
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,9 +3907,22 @@ class Up2k(object):
|
|||
assert self.mem_cur # !rm
|
||||
r = db.execute(sql, s3enc(self.mem_cur, rd, fn))
|
||||
|
||||
if r.rowcount:
|
||||
self.volsize[db] -= sz
|
||||
self.volnfiles[db] -= 1
|
||||
if not r.rowcount:
|
||||
return
|
||||
|
||||
self.volsize[db] -= sz
|
||||
self.volnfiles[db] -= 1
|
||||
|
||||
if "nodirsz" not in vflags:
|
||||
try:
|
||||
q = "update ds set nf=nf-1, sz=sz-? where rd=?"
|
||||
while True:
|
||||
db.execute(q, (sz, rd))
|
||||
if not rd:
|
||||
break
|
||||
rd = rd.rsplit("/", 1)[0] if "/" in rd else ""
|
||||
except:
|
||||
pass
|
||||
|
||||
def db_add(
|
||||
self,
|
||||
|
@ -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)
|
||||
bos.utime(slabs, (int(time.time()), int(mt)), False)
|
||||
try:
|
||||
bos.utime(slabs, (int(time.time()), int(mt)), False)
|
||||
except:
|
||||
self.log("relink: failed to utime(%r, %s)" % (slabs, mt), 3)
|
||||
self._symlink(slabs, sabs, flags, False, is_mv=True)
|
||||
full[slabs] = (ptop, rem)
|
||||
sabs = slabs
|
||||
|
@ -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,8 +5138,10 @@ class Up2k(object):
|
|||
job["t0"],
|
||||
"",
|
||||
)
|
||||
if not hr:
|
||||
t = "upload blocked by xbu server config: %r" % (vp_chk,)
|
||||
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)
|
||||
if hr.get("reloc"):
|
||||
|
@ -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:
|
||||
|
|
|
@ -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,10 +1593,12 @@ def vol_san(vols: list["VFS"], txt: bytes) -> bytes:
|
|||
bvp = vol.vpath.encode("utf-8")
|
||||
bvph = b"$hist(/" + bvp + b")"
|
||||
|
||||
txt = txt.replace(bap, bvp)
|
||||
txt = txt.replace(bhp, bvph)
|
||||
txt = txt.replace(bap.replace(b"\\", b"\\\\"), bvp)
|
||||
txt = txt.replace(bhp.replace(b"\\", b"\\\\"), bvph)
|
||||
if bap:
|
||||
txt = txt.replace(bap, bvp)
|
||||
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:
|
||||
bdp = vol.dbpath.encode("utf-8")
|
||||
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 |
|
@ -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>
|
||||
|
|
|
@ -130,7 +130,8 @@ write markdown (most html is 🙆 too)
|
|||
|
||||
var SR = "{{ r }}",
|
||||
last_modified = {{ lastmod }},
|
||||
have_emp = {{ "true" if have_emp else "false" }},
|
||||
have_emp = {{ have_emp }},
|
||||
md_no_br = {{ md_no_br }},
|
||||
dfavico = "{{ favico }}";
|
||||
|
||||
var md_opt = {
|
||||
|
|
|
@ -201,7 +201,7 @@ function convert_markdown(md_text, dest_dom) {
|
|||
|
||||
var marked_opts = {
|
||||
//headerPrefix: 'h-',
|
||||
breaks: true,
|
||||
breaks: !md_no_br,
|
||||
gfm: true
|
||||
};
|
||||
|
||||
|
@ -217,7 +217,7 @@ function convert_markdown(md_text, dest_dom) {
|
|||
catch (ex) {
|
||||
if (IE) {
|
||||
dest_dom.innerHTML = 'IE cannot into markdown ;_;';
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ext)
|
||||
|
@ -344,6 +344,8 @@ function convert_markdown(md_text, dest_dom) {
|
|||
}
|
||||
catch (ex) { }
|
||||
}, 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
@ -422,7 +424,7 @@ function init_toc() {
|
|||
}
|
||||
}
|
||||
|
||||
// hilight the correct toc items + scroll into view
|
||||
// highlight the correct toc items + scroll into view
|
||||
function freshen_toclist() {
|
||||
if (anchors.length == 0)
|
||||
return;
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
"use strict";
|
||||
|
||||
|
||||
var sloc0 = '' + location,
|
||||
dbg_kbd = /[?&]dbgkbd\b/.exec(sloc0);
|
||||
|
||||
|
||||
// server state
|
||||
var server_md = dom_src.value;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
31
copyparty/web/opds.xml
Normal 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>
|
|
@ -19,14 +19,7 @@
|
|||
<a href="{{ r }}/?h">control-panel</a>
|
||||
Filter: <input type="text" id="filter" size="20" placeholder="documents/passwords" />
|
||||
<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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -15,15 +15,19 @@
|
|||
<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 <small>(you're not logged in)</small></p>
|
||||
{%- else %}
|
||||
<a id="c" href="{{ r }}/?pw=x" class="logout">logout</a>
|
||||
<p><span id="m">welcome back,</span> <strong>{{ this.uname|e }}</strong></p>
|
||||
{%- endif %}
|
||||
{%- if this.uname == '*' %}
|
||||
<p id="b">howdy stranger <small>(you're not logged in)</small></p>
|
||||
{%- else %}
|
||||
{%- if this.args.idp_logout %}
|
||||
<a id="c" href="{{ this.args.idp_logout }}" class="logout">logout</a>
|
||||
{%- else %}
|
||||
<a id="c" href="{{ r }}/?pw=x" class="logout">logout</a>
|
||||
{%- endif %}
|
||||
<p><span id="m">welcome back,</span> <strong id="un">{{ this.uname|e }}</strong></p>
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
|
||||
{%- if msg %}
|
||||
|
@ -37,9 +41,9 @@
|
|||
<table class="vols">
|
||||
<thead><tr><th>%</th><th>speed</th><th>eta</th><th>idle</th><th>dir</th><th>file</th></tr></thead>
|
||||
<tbody>
|
||||
{% for u in ups %}
|
||||
{%- for u in ups %}
|
||||
<tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td><a href="{{ u[4] }}">{{ u[5]|e }}</a></td><td>{{ u[6]|e }}</td></tr>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{%- endif %}
|
||||
|
@ -49,9 +53,9 @@
|
|||
<table class="vols">
|
||||
<thead><tr><th>%</th><th>sent</th><th>speed</th><th>eta</th><th>idle</th><th></th><th>dir</th><th>file</th></tr></thead>
|
||||
<tbody>
|
||||
{% for u in dls %}
|
||||
{%- for u in dls %}
|
||||
<tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td>{{ u[4] }}</td><td>{{ u[5] }}</td><td><a href="{{ u[6] }}">{{ u[7]|e }}</a></td><td>{{ u[8] }}</td></tr>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{%- endif %}
|
||||
|
@ -70,11 +74,11 @@
|
|||
<table class="vols">
|
||||
<thead><tr><th>vol</th><th id="t">action</th><th>status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for mp in avol %}
|
||||
{%- for mp in avol %}
|
||||
{%- if mp in vstate and vstate[mp] %}
|
||||
<tr><td><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></td><td><a class="s" href="{{ r }}{{ mp }}?scan">rescan</a></td><td>{{ vstate[mp] }}</td></tr>
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</td></tr></table>
|
||||
|
@ -87,18 +91,18 @@
|
|||
{%- if rvol %}
|
||||
<h1 id="f">you can browse:</h1>
|
||||
<ul>
|
||||
{% for mp in rvol %}
|
||||
{%- for mp in rvol %}
|
||||
<li><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></li>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
{%- endif %}
|
||||
|
||||
{%- if wvol %}
|
||||
<h1 id="g">you can upload to:</h1>
|
||||
<ul>
|
||||
{% for mp in wvol %}
|
||||
{%- for mp in wvol %}
|
||||
<li><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></li>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
{%- endif %}
|
||||
|
||||
|
@ -110,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>
|
||||
|
|
|
@ -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 ┐( ´ -`)┌",
|
||||
"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: 文件不存在 ┐( ´ -`)┌",
|
||||
"o1": '或者你可能没有权限?尝试输入密码或 <a href="' + SR + '/?h">回家</a>',
|
||||
|
@ -93,6 +105,52 @@ var Ls = {
|
|||
"af1": "显示最近上传的文件", //m
|
||||
"ag1": "查看已知 IdP 用户", //m
|
||||
},
|
||||
"cze": {
|
||||
"a1": "obnovit",
|
||||
"b1": "ahoj cizinče <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 ┐( ´ -`)┌",
|
||||
"o1": 'nebo možná nemáš přístup -- zkus heslo nebo <a href="' + SR + '/?h">jdi domů</a>',
|
||||
"p1": "403 zakázáno ~┻━┻",
|
||||
"q1": 'použij heslo nebo <a href="' + SR + '/?h">jdi domů</a>',
|
||||
"r1": "jdi domů",
|
||||
".s1": "znovu prohledat",
|
||||
"t1": "akce",
|
||||
"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? <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 ┐( ´ -`)┌",
|
||||
"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 <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 ┐( ´ -`)┌",
|
||||
"o1": 'aŭ eble vi ne havas rajton -- provu uzi pasvorton aŭ <a href="' + SR + '/?h">iri hejmen</a>',
|
||||
"p1": "403 ne permesita ~┻━┻",
|
||||
"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 <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 ┐( ´ -`)┌",
|
||||
"o1": 'tai ehkä sinulla ei vain ole käyttöoikeuksia? kokeile salasanaa tai <a href="' + SR + '/?h">mene kotiin</a>',
|
||||
|
@ -173,9 +288,383 @@ var Ls = {
|
|||
"ac1": "ota no304 käyttöön",
|
||||
"ad1": "no304:n lopettaa välimuistin käytön kokonaan; kokeile tätä jos k304 ei riittänyt. Tuhlaa valtavan määrän verkkoliikennettä!",
|
||||
"ae1": "lähtevät:",
|
||||
"af1": "näytä viimeaikaiset lataukset",
|
||||
"af1": "näytä viimeaikaiset lataukset",
|
||||
"ag1": "näytä tunnetut IdP-käyttäjät",
|
||||
},
|
||||
"fra": {
|
||||
"a1": "rafraîchir",
|
||||
"b1": "salut étranger <small>(vous n'êtes pas connecté.)</small>",
|
||||
"c1": "déconnexion",
|
||||
"d1": "vidange de la pile",
|
||||
"d2": "affiche l'état de tous les threads actifs",
|
||||
"e1": "recharger la configuration",
|
||||
"e2": "recharger le fichier de configuration (comptes/volumes/indicateurs de volume),$Net rescanner tous les volumes e2ds$N$Nnote : n'importe quel changement aux paramètres globaux$Nnécessite un redémarrage complet pour prendre effet",
|
||||
"f1": "vous pouvez naviguer :",
|
||||
"g1": "vous pouvez télécharger sur :",
|
||||
"cc1": "autres choses :",
|
||||
"h1": "désactiver k304",
|
||||
"i1": "activer k304",
|
||||
"j1": "activer k304 va déconnecter votre client sur chaque HTTP 304, ce qui peut éviter à certains proxies défectueux de rester bloqués (les pages ne se chargent soudainement plus), <em>mais</em> cela ralentira également les choses en général",
|
||||
"k1": "réinitialiser les paramètres du client",
|
||||
"l1": "connectez-vous pour en savoir plus :",
|
||||
"ls3": "se connecter", //m
|
||||
"lu4": "nom d'utilisateur", //m
|
||||
"lp4": "mot de passe", //m
|
||||
"lo3": "déconnecter “{0}” partout", //m
|
||||
"lo2": "cela mettra fin à la session sur tous les navigateurs", //m
|
||||
"m1": "heureux de vous revoir,",
|
||||
"n1": "404 introuvable ┐( ´ -`)┌",
|
||||
"o1": 'ou peut-être que vous n\'y avez pas accès -- essayer un mot de passe ou <a href="' + SR + '/?h">aller à la page d\'accueil</a>',
|
||||
"p1": "403 interdit ~┻━┻",
|
||||
"q1": 'utiliser un mot de passe ou <a href="' + SR + '/?h">aller à la page d\'accueil</a>',
|
||||
"r1": "aller à la page d\'accueil",
|
||||
".s1": "rescanner",
|
||||
"t1": "action",
|
||||
"u2": "temps écoulé depuis la dernière écriture sur le serveur$N(téléchargement/renommage/...)$N$N17j = 17 jours$N1h23 = 1 heure 23 minutes$N4m56 = 4 minutes 56 secondes",
|
||||
"v1": "connecter",
|
||||
"v2": "utilisez ce serveur en tant que disque dur local",
|
||||
"w1": "passer à https",
|
||||
"x1": "changer mot de passe",
|
||||
"y1": "modifier les partages",
|
||||
"z1": "déverrouiller ce partage :",
|
||||
"ta1": "entrez d'abord votre nouveau mot de passe",
|
||||
"ta2": "répétez pour confirmer le nouveau mot de passe :",
|
||||
"ta3": "une faute de frappe a été détectée ; veuillez réessayer.",
|
||||
"aa1": "fichiers entrants :",
|
||||
"ab1": "désactiver no304",
|
||||
"ac1": "activer no304",
|
||||
"ad1": "l'activation de no304 désactivera toute mise en cache ; essayez ceci si k304 n'était pas suffisant. Cela va générer un trafic réseau considérable !",
|
||||
"ae1": "téléchargements actifs :",
|
||||
"af1": "afficher les derniers téléchargements",
|
||||
},
|
||||
"grc": {
|
||||
"a1": "ανανέωση",
|
||||
"b1": "γεια σου ξένε! <small>(δεν είσαι συνδεδεμένος)</small>",
|
||||
"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 δεν βρέθηκε ┐( ´ -`)┌",
|
||||
"o1": '´η μήπως δεν έχεις πρόσβαση -- δοκίμασε έναν κωδικό <a href="' + SR + '/?h">πήγαινε στην αρχική</a>',
|
||||
"p1": "403 απαγορευμένο ~┻━┻",
|
||||
"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 <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 ┐( ´ -`)┌",
|
||||
"o1": "oppure forse non hai accesso? prova una password o <a href=\"SR/?h\">torna alla home</a>",
|
||||
"p1": "403: accesso negato ~┻━┻",
|
||||
"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": "어이 친구! 처음 보는 얼굴인데? <small>(로그인되어 있지 않습니다)</small>",
|
||||
"c1": "로그아웃",
|
||||
"d1": "스택 덤프하기",
|
||||
"d2": "모든 활성 스레드의 상태를 표시합니다",
|
||||
"e1": "설정 다시 불러오기",
|
||||
"e2": "설정 파일(계정/볼륨/볼륨 플래그)을 다시 불러오고,$N모든 e2ds 볼륨을 다시 스캔합니다$N$N참고: 전역 설정에 대한 변경 사항은$N적용하려면 전체 재시작이 필요합니다",
|
||||
"f1": "탐색 가능한 곳:",
|
||||
"g1": "업로드 가능한 곳:",
|
||||
"cc1": "기타 항목:",
|
||||
"h1": "k304 비활성화",
|
||||
"i1": "k304 활성화",
|
||||
"j1": "k304를 활성화하면 모든 HTTP 304 응답 시 클라이언트 연결이 끊어집니다. 이는 일부 프록시가 멈추는 현상(갑자기 페이지가 로드되지 않음)을 방지할 수 있지만, <em>대신 전반적인 속도는 느려집니다.</em>",
|
||||
"k1": "클라이언트 설정 초기화",
|
||||
"l1": "로그인하기:",
|
||||
"ls3": "로그인", //m
|
||||
"lu4": "사용자 이름", //m
|
||||
"lp4": "비밀번호", //m
|
||||
"lo3": "{0}을(를) 모든 곳에서 로그아웃", //m
|
||||
"lo2": "이 작업은 모든 브라우저에서 세션을 종료합니다", //m
|
||||
"m1": "또 오셨네요,",
|
||||
"n1": "404 찾을 수 없음 ┐( ´ -`)┌",
|
||||
"o1": "또는 접근 권한이 없을 수 있습니다. 비밀번호를 입력하거나 <a href=\"' + SR + '/?h\">홈으로 이동</a>하세요",
|
||||
"p1": "403 접근 금지 ~┻━┻",
|
||||
"q1": "비밀번호를 입력하거나 <a href=\"' + SR + '/?h\">홈으로 이동</a>하세요",
|
||||
"r1": "홈으로 이동",
|
||||
".s1": "다시 스캔",
|
||||
"t1": "작업",
|
||||
"u2": "서버에 마지막으로 쓰기 작업을 한 후 경과된 시간$N(업로드 / 이름 변경 / 등등...)$N$N17d = 17일$N1h23 = 1시간 23분$N4m56 = 4분 56초",
|
||||
"v1": "연결",
|
||||
"v2": "이 서버를 로컬 하드디스크처럼 사용하기",
|
||||
"w1": "HTTPS로 전환",
|
||||
"x1": "비밀번호 변경",
|
||||
"y1": "공유 설정",
|
||||
"z1": "이 공유 잠금해제:",
|
||||
"ta1": "새 비밀번호를 먼저 입력하세요",
|
||||
"ta2": "새 비밀번호 확인을 위해 다시 입력하세요:",
|
||||
"ta3": "오타가 있습니다. 다시 시도해주세요",
|
||||
"aa1": "수신 중인 파일:",
|
||||
"ab1": "no304 비활성화",
|
||||
"ac1": "no304 활성화",
|
||||
"ad1": "no304를 활성화하면 모든 캐싱이 비활성화됩니다. k304로 충분하지 않은 경우 시도해보세요. 네트워크 트래픽이 대량으로 낭비됩니다!",
|
||||
"ae1": "활성 다운로드:",
|
||||
"af1": "최근 업로드 보기",
|
||||
"ag1": "IdP 캐시 보기"
|
||||
},
|
||||
"nld": {
|
||||
"a1": "Update",
|
||||
"b1": "Hallo, hoe gaat het met jou? <small>(Je bent niet ingelogd)</small>",
|
||||
"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 ┐( ´ -`)┌",
|
||||
"o1": 'of misschien heb je geen toegang? probeer een wachtwoord of <a href="' + SR + '/?h">ga naar startscherm</a>',
|
||||
"p1": "403: toegang geweigerd ~┻━┻",
|
||||
"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 <small>(du er ikkje logga inn)</small>",
|
||||
"c1": "logg ut",
|
||||
"d1": "tilstand",
|
||||
"d2": "vis tilstanden åt alle trådar",
|
||||
"e1": "last innst.",
|
||||
"e2": "les inn konfigurasjonsfiler på nytt$N(kontoer, volum, volumbrytarar)$Nog kartlegg alle e2ds-volum$N$Nmerk: endringer i globale parametrar$Nkrev ein full restart for å gjelde",
|
||||
"f1": "du kan sjå på:",
|
||||
"g1": "du kan laste opp åt:",
|
||||
"cc1": "brytarar og slikt:",
|
||||
"h1": "skru av k304",
|
||||
"i1": "skru på k304",
|
||||
"j1": "k304 bryt tilkoplinga for kvar HTTP 304. Dette hjelp mot visse mellomtjenarar som kan sette seg fast / plutselig sluttar å laste sider, men det sett óg ytinga ned betydelig",
|
||||
"k1": "nullstill innstillinger",
|
||||
"l1": "logg inn:",
|
||||
"ls3": "logg inn",
|
||||
"lu4": "brukarnamn",
|
||||
"lp4": "passord",
|
||||
"lo3": "logg ut “{0}” overalt",
|
||||
"lo2": "avslutt økta på alle nettlesarar",
|
||||
"m1": "velkomen attende,",
|
||||
"n1": "404: filen finnast ikkje ┐( ´ -`)┌",
|
||||
"o1": 'eller kanskje du ikkje har høve? prøv eit passord eller <a href="' + SR + '/?h">gå heim</a>',
|
||||
"p1": "403: tilgang nektet ~┻━┻",
|
||||
"q1": 'prøv eit passord eller <a href="' + SR + '/?h">gå heim</a>',
|
||||
"r1": "gå heim",
|
||||
".s1": "kartlegg",
|
||||
"t1": "handling",
|
||||
"u2": "tid sidan nokon sist skreiv åt serveren$N( opplastning / namnendring / ... )$N$N17d = 17 dagar$N1h23 = 1 time 23 minutt$N4m56 = 4 minutt 56 sekund",
|
||||
"v1": "kople åt",
|
||||
"v2": "bruk denne serveren som ein lokal harddisk",
|
||||
"w1": "bytt åt https",
|
||||
"x1": "bytt passord",
|
||||
"y1": "dine delinger",
|
||||
"z1": "lås opp område:",
|
||||
"ta1": "du må skrive eit nytt passord først",
|
||||
"ta2": "gjenta for å stadfeste nytt passord:",
|
||||
"ta3": "fant ein skrivefeil; vennligst prøv igjen",
|
||||
"aa1": "innkommande:",
|
||||
"ab1": "skru av no304",
|
||||
"ac1": "skru på no304",
|
||||
"ad1": "no304 stoppar all bruk av cache. Hvis ikkje k304 var nok, prøv denne. Vil mangedoble dataforbruk!",
|
||||
"ae1": "utgående:",
|
||||
"af1": "vis nylig opplasta filer",
|
||||
"ag1": "vis kjente IdP-brukarar",
|
||||
},
|
||||
"pol": {
|
||||
"a1": "odśwież",
|
||||
"b1": "witaj, nieznajomy <small>(nie jesteś zalogowany)</small>",
|
||||
"c1": "wyloguj się",
|
||||
"d1": "zrzut stosu",
|
||||
"d2": "pokazuje status wszystkich aktywnych wątków",
|
||||
"e1": "przeładuj konfigurację",
|
||||
"e2": "przeładuj pliki konfiguracyjne (konta/wolumeny/flagi wolumenów),$Ni przeskanuje wszystkie wolumeny e2ds$N$Nnotka: zmiany konfiguracji globalnej$Nwymagają pełnego uruchomienia ponownie serwera, aby zaczęły obowiązywać",
|
||||
"f1": "możesz przeglądać:",
|
||||
"g1": "możesz przesyłać do:",
|
||||
"cc1": "inne:",
|
||||
"h1": "wyłącz k304",
|
||||
"i1": "włącz k304",
|
||||
"j1": "włączenie k304 będzie odłączało klienta przy każdorazowym otrzymaniu kodu HTTP 304, co może zapobiec wieszaniu się wadliwych proxy, <em>ale</em> spowolni ogólne działanie",
|
||||
"k1": "zresetuj ustawienia klienta",
|
||||
"l1": "zaloguj się po więcej:",
|
||||
"ls3": "zaloguj się", //m
|
||||
"lu4": "nazwa użytkownika", //m
|
||||
"lp4": "hasło", //m
|
||||
"lo3": "wyloguj “{0}” wszędzie", //m
|
||||
"lo2": "spowoduje to zakończenie sesji we wszystkich przeglądarkach", //m
|
||||
"m1": "Witaj,",
|
||||
"n1": "404 nie znaleziono ┐( ´ -`)┌",
|
||||
"o1": 'lub możesz nie mieć dostępu -- spróbuj wprowadzić hasło lub <a href="' + SR + '/?h">przejdź do strony głównej</a>',
|
||||
"p1": "403 odmowa dostępu ~┻━┻",
|
||||
"q1": 'użyj hasła lub <a href="' + SR + '/?h">przejdź do strony głównej</a>',
|
||||
"r1": "idź do strony głównej",
|
||||
".s1": "przeskanuj ponownie",
|
||||
"t1": "akcje",
|
||||
"u2": "czas od ostatniej interakcji z serwerem$N( przesyłania / zmiany nazwy / ... )$N$N17d = 17 dni$N1h23 = 1 godzina 23 minuty$N4m56 = 4 minuty 56 sekund",
|
||||
"v1": "połącz",
|
||||
"v2": "używaj tego serwera jako dysku lokalnego",
|
||||
"w1": "przejdź na HTTPS",
|
||||
"x1": "zmień hasło",
|
||||
"y1": "edytuj udostępnione",
|
||||
"z1": "odblokuj udostępnienie:",
|
||||
"ta1": "najpierw wprowadź nowe hasło",
|
||||
"ta2": "powtórz hasło dla potwierdzenia:",
|
||||
"ta3": "znaleziono literówkę, spróbuj ponownie",
|
||||
"aa1": "pliki przychodzące:",
|
||||
"ab1": "wyłącz no304",
|
||||
"ac1": "włącz no304",
|
||||
"ad1": "włączenie no304 wyłączy przechowywanie jakiejkolwiek pamięci podręcznej. Zmarnuje to olbrzymią ilość ruchu sieciowego!",
|
||||
"ae1": "trwające pobierania:",
|
||||
"af1": "pokaż ostatnio przesłane pliki",
|
||||
"ag1": "pokaż znanych użytkowników IdP",
|
||||
},
|
||||
"por": {
|
||||
"a1": "atualizar",
|
||||
"b1": "olá <small>(você não está logado)</small>",
|
||||
"c1": "encerrar sessão",
|
||||
"d1": "despejar o estado da pilha",
|
||||
"d2": "mostra o estado de todos os threads ativos",
|
||||
"e1": "recarregar configuração",
|
||||
"e2": "recarregar arquivos de configuração (contas/volumes/indicadores de volume),$N e reescanear todos os volumes e2ds$N$Nnota: qualquer alteração na configuração global$N requer uma reinicialização completa para ter efeito",
|
||||
"f1": "você pode navegar:",
|
||||
"g1": "você pode fazer upload para:",
|
||||
"cc1": "outras coisas:",
|
||||
"h1": "desativar k304",
|
||||
"i1": "ativar k304",
|
||||
"j1": "ativar k304 irá desconectar seu cliente em cada HTTP 304, o que pode evitar que alguns proxies com erros fiquem presos (parando de carregar páginas de repente), <em>mas</em> também irá desacelerar as coisas em geral",
|
||||
"k1": "redefinir config. de cliente",
|
||||
"l1": "faça login para mais:",
|
||||
"ls3": "fazer login",
|
||||
"lu4": "nome de usuário",
|
||||
"lp4": "senha",
|
||||
"lo3": "encerrar sessão de \"{0}\" em todos os lugares",
|
||||
"lo2": "isso irá encerrar a sessão em todos os navegadores",
|
||||
"m1": "bem-vindo de volta,",
|
||||
"n1": "404 não encontrado ┐( ´ -`)┌",
|
||||
"o1": "ou talvez você não tenha acesso? -- tente com uma senha ou volte para o início",
|
||||
"p1": "403 proibido ~┻━┻",
|
||||
"q1": "use uma senha ou volte para o início",
|
||||
"r1": "ir para o início",
|
||||
".s1": "reescanear",
|
||||
"t1": "ação",
|
||||
"u2": "tempo desde a última gravação no servidor$N( upload / renomear / ... )$N$N17d = 17 dias$N1h23 = 1 hora 23 minutos$N4m56 = 4 minutos 56 segundos",
|
||||
"v1": "conectar",
|
||||
"v2": "usar este servidor como um disco rígido local",
|
||||
"w1": "mudar para https",
|
||||
"x1": "mudar senha",
|
||||
"y1": "editar recursos compartilhados",
|
||||
"z1": "desbloquear este recurso compartilhado:",
|
||||
"ta1": "primeiro digite sua nova senha",
|
||||
"ta2": "repita para confirmar a nova senha:",
|
||||
"ta3": "há um erro; por favor, tente novamente",
|
||||
"aa1": "arquivos de entrada:",
|
||||
"ab1": "desativar no304",
|
||||
"ac1": "ativar no304",
|
||||
"ad1": "ativar no304 irá desabilitar todo o armazenamento em cache; tente isso se k304 não for suficiente. Isso irá desperdiçar uma grande quantidade de tráfego de rede!",
|
||||
"ae1": "downloads ativos:",
|
||||
"af1": "mostrar uploads recentes",
|
||||
"ag1": "mostrar usuários IdP conhecidos"
|
||||
},
|
||||
"spa": {
|
||||
"a1": "actualizar",
|
||||
"b1": "hola <small>(no has iniciado sesión)</small>",
|
||||
|
@ -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 ┐( ´ -`)┌",
|
||||
"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 <small>(du är inte inloggad)</small>",
|
||||
"c1": "logga ut",
|
||||
"d1": "dumpa stacken",
|
||||
"d2": "visar tillståndet på alla aktiva trådar",
|
||||
"e1": "ladda om konfig.",
|
||||
"e2": "ladda om konfigurationsfiler (konton/volymer/volflaggor),$Noch skanna om alla e2ds-volymer$N$Nobs.: ändrade globala inställningar$Nkräver en fullständig omstart",
|
||||
"f1": "du kan bläddra:",
|
||||
"g1": "du kan ladda upp till:",
|
||||
"cc1": "annat:",
|
||||
"h1": "avaktivera k304",
|
||||
"i1": "aktivera k304",
|
||||
"j1": "med k304 aktiverad kommer klienten att koppla bort sig vid varje HTTP 304-fel, vilket kan hindra vissa buggiga proxyservrar från att fastna (sidor slutar ladda), <em>men</em> saker kommer också att bli långsammare i allmänhet",
|
||||
"k1": "återställ klientinställningar",
|
||||
"l1": "logga in för att se mer:",
|
||||
"ls3": "logga in", //m
|
||||
"lu4": "användarnamn", //m
|
||||
"lp4": "lösenord", //m
|
||||
"lo3": "logga ut “{0}” överallt", //m
|
||||
"lo2": "avsluta sessionen i alla webbläsare", //m
|
||||
"m1": "välkommen tillbaka,",
|
||||
"n1": "404 hittades inte ┐( ´ -`)┌",
|
||||
"o1": 'eller så har du kanske inte tillgång -- prova ett lösenord eller <a href="' + SR + '/?h">åk hem</a>',
|
||||
"p1": "403 nekat ~┻━┻",
|
||||
"q1": 'använd ett lösenord eller <a href="' + SR + '/?h">åk hem</a>',
|
||||
"r1": "åk hem",
|
||||
".s1": "skanna om",
|
||||
"t1": "åtgärd",
|
||||
"u2": "tid sedan senaste serverskrivning$N( uppladdning / namnbyte / ... )$N$N17d = 17 dagar$N1h23 = 1 timme 23 minuter$N4m56 = 4 minuter 56 sekunder",
|
||||
"v1": "koppla upp",
|
||||
"v2": "använd denna server som en lokal disk",
|
||||
"w1": "byt till https",
|
||||
"x1": "byt lösenord",
|
||||
"y1": "redigera utdelningar",
|
||||
"z1": "lås upp denna utdelning:",
|
||||
"ta1": "fyll i ditt nya lösenord",
|
||||
"ta2": "upprepa det nya lösenordet:",
|
||||
"ta3": "det blev fel; vänligen försök igen",
|
||||
"aa1": "inkommande filer:",
|
||||
"ab1": "avaktivera no304",
|
||||
"ac1": "aktivera no304",
|
||||
"ad1": "detta stänger av all cachning; prova detta om k304 inte räckte till. Detta kommer att slösa enorma mängder nätverkstrafik!",
|
||||
"ae1": "aktiva nedladdningar:",
|
||||
"af1": "visa senaste uppladdningar",
|
||||
"ag1": "visa idp-cache"
|
||||
},
|
||||
"ukr": {
|
||||
"a1": "оновити",
|
||||
"b1": "привітик, незнайомцю <small>(ви не авторизовані)</small>",
|
||||
"c1": "вийти",
|
||||
"d1": "трасування стека",
|
||||
"d2": "показує стан усіх активних потоків",
|
||||
"e1": "перезавантажити конфіг",
|
||||
"e2": "перезавантажити файли конфігурації (облікові записи/томи/прапорці),$Nта пересканувати всі томи e2ds$N$Nувага: будь-які зміни глобальних налаштувань$Nвимагають повного перезапуску",
|
||||
"f1": "ви можете бачити:",
|
||||
"g1": "ви можете завантажувати файли в:",
|
||||
"cc1": "всяка всячина:",
|
||||
"h1": "вимкнути k304",
|
||||
"i1": "увімкнути k304",
|
||||
"j1": "увімкнення k304 буде відключати ваш клієнт при кожному HTTP 304, що може запобігти зависанню деяких глючних проксі (раптово перестають завантажувати сторінки), <em>але</em> це також зробить усе повільнішим загалом",
|
||||
"k1": "скинути налаштування клієнта",
|
||||
"l1": "авторизуйтесь для інших опцій:",
|
||||
"ls3": "увійти", //m
|
||||
"lu4": "ім'я користувача", //m
|
||||
"lp4": "пароль", //m
|
||||
"lo3": "вийти з облікового запису “{0}” всюди", //m
|
||||
"lo2": "це завершить сеанс у всіх браузерах", //m
|
||||
"m1": "з поверненням,",
|
||||
"n1": "404 не знайдено ┐( ´ -`)┌",
|
||||
"o1": 'або у вас немає доступу -- спробуйте авторизуватися або <a href="' + SR + '/?h">повернутися на головну</a>',
|
||||
"p1": "403 доступ заборонений ~┻━┻",
|
||||
"q1": 'авторизуйтесь або <a href="' + SR + '/?h">поверніться на головну</a>',
|
||||
"r1": "повернутися на головну",
|
||||
".s1": "пересканувати",
|
||||
"t1": "дія",
|
||||
"u2": "час з останнього запису сервера$N( завантаження / перейменування / ... )$N$N17d = 17 днів$N1h23 = 1 година 23 хвилини$N4m56 = 4 хвилини 56 секунд",
|
||||
"v1": "підключити",
|
||||
"v2": "використовувати цей сервер як локальний HDD",
|
||||
"w1": "перейти на https",
|
||||
"x1": "змінити пароль",
|
||||
"y1": "керування доступом",
|
||||
"z1": "розблокувати:",
|
||||
"ta1": "спочатку заповніть ваш новий пароль",
|
||||
"ta2": "повторіть для підтвердження нового пароля:",
|
||||
"ta3": "описка; спробуйте знову",
|
||||
"aa1": "вхідні файли:",
|
||||
"ab1": "вимкнути no304",
|
||||
"ac1": "увімкнути no304",
|
||||
"ad1": "увімкнення no304 вимкне все кешування; спробуйте це, якщо k304 було недостатньо. Це витратить величезну кількість мережевого трафіку!",
|
||||
"ae1": "активні завантаження:",
|
||||
"af1": "показати нещодавні завантаження",
|
||||
"ag1": "показати відомих IdP-користувачів"
|
||||
},
|
||||
"rus": {
|
||||
"a1": "обновить",
|
||||
"b1": "приветик, незнакомец <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 не найдено ┐( ´ -`)┌",
|
||||
"o1": 'или у вас нет доступа -- попробуйте авторизоваться или <a href="' + SR + '/?h">вернуться на главную</a>',
|
||||
|
@ -260,6 +853,47 @@ var Ls = {
|
|||
"af1": "показать недавние загрузки",
|
||||
"ag1": "показать известных IdP-пользователей",
|
||||
},
|
||||
"tur": {
|
||||
"a1": "yenile",
|
||||
"b1": "N'aber aga <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ı ┐( ´ -`)┌",
|
||||
"o1": 'ya da erişim iznin yok -- bir şifre dene veya <a href="' + SR + '/?h">ana sayfaya dön</a>',
|
||||
"p1": "403 yasaklandı ~┻━┻",
|
||||
"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'))
|
||||
|
|
|
@ -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> </code> (space)</li>
|
||||
</ul>
|
||||
|
||||
<p>if you want to use the native WebDAV client in windows instead (slow and buggy), first run <a href="{{ r }}/.cpr/a/webdav-cfg.bat">webdav-cfg.bat</a> to remove the 47 MiB filesize limit (also fixes latency and password login), then connect:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
net use <b>w:</b> http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} <b>{{ pw }}</b> /user:{{ b_un }}{% endif %}
|
||||
{%- else %}
|
||||
net use <b>w:</b> http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} k /user:<b>{{ pw }}</b>{% endif %}
|
||||
{%- endif %}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div class="os lin">
|
||||
<p>rclone (v1.63 or later) is recommended:</p>
|
||||
<pre>
|
||||
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
|
||||
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>mp</b>
|
||||
</pre>
|
||||
<ul>
|
||||
{% if s %}
|
||||
{%- if s %}
|
||||
<li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
|
||||
<li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li>
|
||||
</ul>
|
||||
<p>alternatively use davfs2 (requires root, is slower, forgets lastmodified-timestamp on upload):</p>
|
||||
<pre>
|
||||
yum install davfs2
|
||||
{%- if un %}
|
||||
{% if accs %}printf '%s\n' {{ b_un }} <b>{{ pw }}</b> | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b>
|
||||
{%- else %}
|
||||
{% if accs %}printf '%s\n' <b>{{ pw }}</b> k | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b>
|
||||
{%- endif %}
|
||||
</pre>
|
||||
{%- if accs %}
|
||||
<p>make davfs2 automount on boot:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} {{ b_un }} <b>{{ pw }}</b>" >> /etc/davfs2/secrets
|
||||
{%- else %}
|
||||
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>{{ pw }}</b> k" >> /etc/davfs2/secrets
|
||||
{%- endif %}
|
||||
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b> davfs rw,user,uid=1000,noauto 0 0" >> /etc/fstab
|
||||
</pre>
|
||||
{%- endif %}
|
||||
<p>or the emergency alternative (gnome/gui-only):</p>
|
||||
<!-- gnome-bug: ignores vp -->
|
||||
<pre>
|
||||
{%- if accs %}
|
||||
echo <b>{{ pw }}</b> | gio mount dav{{ s }}://k@{{ ep }}/{{ rvp }}
|
||||
echo <b>{{ pw }}</b> | gio mount dav{{ s }}://{{ b_un }}@{{ ep }}/{{ rvp }}
|
||||
{%- else %}
|
||||
gio mount -a dav{{ s }}://{{ ep }}/{{ rvp }}
|
||||
{%- endif %}
|
||||
|
@ -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> </code> (space)</li>
|
||||
</ul>
|
||||
<p>if you want to use the native FTP client in windows instead (please dont), press <code>win+R</code> and run this command:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}{{ b_un }}:<b>{{ pw }}</b>@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
|
||||
{%- else %}
|
||||
explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}<b>{{ pw }}</b>:k@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
|
||||
{%- endif %}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div class="os lin">
|
||||
{% if args.ftp %}
|
||||
{%- if args.ftp %}
|
||||
<p>connect with plaintext FTP:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
|
||||
{%- else %}
|
||||
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
|
||||
{%- endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>mp</b>
|
||||
</pre>
|
||||
{% endif %}
|
||||
{% if args.ftps %}
|
||||
{%- endif %}
|
||||
{%- if args.ftps %}
|
||||
<p>connect with TLS-encrypted FTPS:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
|
||||
{%- else %}
|
||||
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
|
||||
{%- endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>mp</b>
|
||||
</pre>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<ul>
|
||||
{% if args.ftps %}
|
||||
{%- if args.ftps %}
|
||||
<li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
|
||||
<li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li>
|
||||
</ul>
|
||||
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -430,6 +430,15 @@ html.y textarea:focus {
|
|||
.mdo code {
|
||||
font-size: .96em;
|
||||
}
|
||||
html.z .mdo a>code,
|
||||
html.y .mdo a>code {
|
||||
color: inherit;
|
||||
background: inherit;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
}
|
||||
.mdo h1,
|
||||
.mdo h2 {
|
||||
line-height: 1.5em;
|
||||
|
|
|
@ -50,7 +50,7 @@ catch (ex) {
|
|||
}
|
||||
catch (ex) {
|
||||
console.log('up2k init failed:', ex);
|
||||
toast.err(10, 'could not initialze up2k\n\n' + basenames(ex));
|
||||
toast.err(10, 'could not initialize up2k\n\n' + basenames(ex));
|
||||
}
|
||||
}
|
||||
treectl.onscroll();
|
||||
|
@ -732,7 +732,7 @@ function Donut(uc, st) {
|
|||
tstrober = setInterval(strobe, 300);
|
||||
|
||||
if (uc.upsfx && actx && actx.state != 'suspended')
|
||||
sfx();
|
||||
sfx_nice();
|
||||
|
||||
// firefox may forget that filedrops are user-gestures so it can skip this:
|
||||
if (uc.upnag && Notification && Notification.permission == 'granted')
|
||||
|
@ -745,8 +745,10 @@ function Donut(uc, st) {
|
|||
if (!txt)
|
||||
clearInterval(tstrober);
|
||||
}
|
||||
}
|
||||
|
||||
function sfx() {
|
||||
function sfx_nice() {
|
||||
if (true) {
|
||||
var osc = actx.createOscillator(),
|
||||
gain = actx.createGain(),
|
||||
gg = gain.gain,
|
||||
|
@ -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 () {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue