Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42d00050c1 | ||
|
|
4bb0e6e75a | ||
|
|
2f7f9de3f5 | ||
|
|
f31ac90932 | ||
|
|
439cb7f85b | ||
|
|
af193ee834 | ||
|
|
c06126cc9d | ||
|
|
897ffbbbd0 | ||
|
|
8244d3b4fc | ||
|
|
74266af6d1 | ||
|
|
8c552f1ad1 | ||
|
|
bf5850785f | ||
|
|
feecb3e0b8 | ||
|
|
08d8c82167 | ||
|
|
5239e7ac0c | ||
|
|
9937c2e755 | ||
|
|
f1e947f37d | ||
|
|
a70a49b9c9 | ||
|
|
fe700dcf1a | ||
|
|
c8e3ed3aae | ||
|
|
b8733653a3 | ||
|
|
b772a4f8bb | ||
|
|
9e5253ef87 | ||
|
|
7b94e4edf3 | ||
|
|
da26ec36ca | ||
|
|
443acf2f8b | ||
|
|
6c90e3893d | ||
|
|
ea002ee71d | ||
|
|
ab18893cd2 | ||
|
|
844d16b9e5 | ||
|
|
989cc613ef | ||
|
|
4f0cad5468 | ||
|
|
f89de6b35d | ||
|
|
e0bcb88ee7 | ||
|
|
a0022805d1 | ||
|
|
853adb5d04 | ||
|
|
7744226b5c | ||
|
|
d94b5b3fc9 | ||
|
|
e6ba065bc2 | ||
|
|
59a53ba9ac | ||
|
|
b88cc7b5ce | ||
|
|
5ab54763c6 | ||
|
|
59f815ff8c | ||
|
|
9c42cbec6f | ||
|
|
f471b05aa4 | ||
|
|
34c32e3e89 | ||
|
|
a080759a03 | ||
|
|
0ae12868e5 | ||
|
|
ef52e2c06c | ||
|
|
32c912bb16 | ||
|
|
20870fda79 | ||
|
|
bdfe2c1a5f | ||
|
|
cb99fbf442 | ||
|
|
bccc44dc21 | ||
|
|
2f20d29edd | ||
|
|
c6acd3a904 | ||
|
|
2b24c50eb7 | ||
|
|
d30ae8453d | ||
|
|
8e5c436bef | ||
|
|
f500e55e68 | ||
|
|
9700a12366 | ||
|
|
2b6a34dc5c | ||
|
|
ee80cdb9cf | ||
|
|
2def4cd248 | ||
|
|
0287c7baa5 | ||
|
|
51d31588e6 | ||
|
|
32553e4520 | ||
|
|
211a30da38 | ||
|
|
bdbcbbb002 | ||
|
|
e78af02241 | ||
|
|
115020ba60 | ||
|
|
66abf17bae | ||
|
|
b377791be7 | ||
|
|
78919e65d6 | ||
|
|
84b52ea8c5 | ||
|
|
fd89f7ecb9 | ||
|
|
2ebfdc2562 | ||
|
|
dbf1cbc8af | ||
|
|
a259704596 | ||
|
|
04b55f1a1d | ||
|
|
206af8f151 | ||
|
|
645bb5c990 | ||
|
|
f8966222e4 | ||
|
|
d71f844b43 | ||
|
|
e8b7f65f82 | ||
|
|
f193f398c1 | ||
|
|
b6554a7f8c | ||
|
|
3f05b6655c | ||
|
|
51a83b04a0 | ||
|
|
0c03921965 | ||
|
|
2527e90325 | ||
|
|
7f08f10c37 | ||
|
|
1c011ff0bb | ||
|
|
a1ad608267 | ||
|
|
547a486387 | ||
|
|
7741870dc7 | ||
|
|
8785d2f9fe | ||
|
|
d744f3ff8f | ||
|
|
8ca996e2f7 | ||
|
|
096de50889 | ||
|
|
bec3fee9ee | ||
|
|
8413ed6d1f | ||
|
|
055302b5be | ||
|
|
8016e6711b | ||
|
|
c8ea4066b1 | ||
|
|
6cc7101d31 | ||
|
|
263adec70a | ||
|
|
ac96fd9c96 | ||
|
|
e5582605cd | ||
|
|
1b52ef1f8a | ||
|
|
503face974 | ||
|
|
13e77777d7 | ||
|
|
89c6c2e0d9 | ||
|
|
14af136fcd | ||
|
|
d39a99c929 | ||
|
|
43ee6b9f5b | ||
|
|
8a38101e48 | ||
|
|
5026b21226 | ||
|
|
d07859e8e6 | ||
|
|
df7219d3b6 | ||
|
|
ad9be54f55 | ||
|
|
eeecc50757 | ||
|
|
8ff7094e4d | ||
|
|
58ae38c613 | ||
|
|
7f1c992601 | ||
|
|
fbfdd8338b | ||
|
|
bbc379906a | ||
|
|
33f41f3e61 | ||
|
|
655f6d00f8 | ||
|
|
fd552842d4 | ||
|
|
6bd087ddc5 | ||
|
|
0504b010a1 | ||
|
|
39cc92d4bc | ||
|
|
a0da0122b9 | ||
|
|
879e83e24f | ||
|
|
64ad585318 | ||
|
|
f262aee800 | ||
|
|
d4da386172 | ||
|
|
5d92f4df49 | ||
|
|
6f8a588c4d | ||
|
|
7c8e368721 | ||
|
|
f7a43a8e46 | ||
|
|
02879713a2 | ||
|
|
acbb8267e1 | ||
|
|
8796c09f56 | ||
|
|
d636316a19 | ||
|
|
a96d9ac6cb | ||
|
|
643e222986 | ||
|
|
ed524d84bb | ||
|
|
f0cdd9f25d | ||
|
|
4e797a7156 | ||
|
|
136c0fdc2b | ||
|
|
35165f8472 | ||
|
|
cab999978e | ||
|
|
caf7e93f5e | ||
|
|
10bc2d9205 |
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@@ -19,8 +19,7 @@
|
||||
"-emp",
|
||||
"-e2dsa",
|
||||
"-e2ts",
|
||||
"-mtp",
|
||||
".bpm=f,bin/mtag/audio-bpm.py",
|
||||
"-mtp=.bpm=f,bin/mtag/audio-bpm.py",
|
||||
"-aed:wark",
|
||||
"-vsrv::r:rw,ed:c,dupe",
|
||||
"-vdist:dist:r"
|
||||
|
||||
190
README.md
190
README.md
@@ -3,20 +3,23 @@
|
||||
turn almost any device into a file server with resumable uploads/downloads using [*any*](#browser-support) web browser
|
||||
|
||||
* server only needs Python (2 or 3), all dependencies optional
|
||||
* 🔌 protocols: [http](#the-browser) // [ftp](#ftp-server) // [webdav](#webdav-server) // [smb/cifs](#smb-server)
|
||||
* 🔌 protocols: [http](#the-browser) // [webdav](#webdav-server) // [ftp](#ftp-server) // [tftp](#tftp-server) // [smb/cifs](#smb-server)
|
||||
* 📱 [android app](#android-app) // [iPhone shortcuts](#ios-shortcuts)
|
||||
|
||||
👉 **[Get started](#quickstart)!** or visit the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running from a basement in finland
|
||||
|
||||
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer)
|
||||
|
||||
🎬 **videos:** [upload](https://a.ocv.me/pub/demo/pics-vids/up2k.webm) // [cli-upload](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm) // [race-the-beam](https://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm)
|
||||
|
||||
|
||||
## readme toc
|
||||
|
||||
* top
|
||||
* [quickstart](#quickstart) - just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** -- that's it! 🎉
|
||||
* [at home](#at-home) - make it accessible over the internet
|
||||
* [on servers](#on-servers) - you may also want these, especially on servers
|
||||
* [features](#features)
|
||||
* [features](#features) - also see [comparison to similar software](./docs/versus.md)
|
||||
* [testimonials](#testimonials) - small collection of user feedback
|
||||
* [motivations](#motivations) - project goals / philosophy
|
||||
* [notes](#notes) - general notes
|
||||
@@ -37,6 +40,7 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
* [file-search](#file-search) - dropping files into the browser also lets you see if they exist on the server
|
||||
* [unpost](#unpost) - undo/delete accidental uploads
|
||||
* [self-destruct](#self-destruct) - uploads can be given a lifetime
|
||||
* [race the beam](#race-the-beam) - download files while they're still uploading ([demo video](http://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm))
|
||||
* [file manager](#file-manager) - cut/paste, rename, and delete files/folders (if you have permission)
|
||||
* [batch rename](#batch-rename) - select some files and press `F2` to bring up the rename UI
|
||||
* [media player](#media-player) - plays almost every audio format there is
|
||||
@@ -53,6 +57,7 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
* [ftp server](#ftp-server) - an FTP server can be started using `--ftp 3921`
|
||||
* [webdav server](#webdav-server) - with read-write support
|
||||
* [connecting to webdav from windows](#connecting-to-webdav-from-windows) - using the GUI
|
||||
* [tftp server](#tftp-server) - a TFTP server (read/write) can be started using `--tftp 3969`
|
||||
* [smb server](#smb-server) - unsafe, slow, not recommended for wan
|
||||
* [browser ux](#browser-ux) - tweaking the ui
|
||||
* [file indexing](#file-indexing) - enables dedup and music search ++
|
||||
@@ -69,14 +74,16 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
* [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/))
|
||||
* [handlers](#handlers) - redefine behavior with plugins ([examples](./bin/handlers/))
|
||||
* [identity providers](#identity-providers) - replace copyparty passwords with oauth and such
|
||||
* [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 dont wanna be indexed
|
||||
* [themes](#themes)
|
||||
* [complete examples](#complete-examples)
|
||||
* [reverse-proxy](#reverse-proxy) - running copyparty next to other websites
|
||||
* [real-ip](#real-ip) - teaching copyparty how to see client IPs
|
||||
* [prometheus](#prometheus) - metrics/stats can be enabled
|
||||
* [packages](#packages) - the party might be closer than you think
|
||||
* [arch package](#arch-package) - now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
|
||||
* [fedora package](#fedora-package) - currently **NOT** available on [copr-pypi](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/)
|
||||
* [fedora package](#fedora-package) - does not exist yet
|
||||
* [nix package](#nix-package) - `nix profile install github:9001/copyparty`
|
||||
* [nixos module](#nixos-module)
|
||||
* [browser support](#browser-support) - TLDR: yes
|
||||
@@ -91,6 +98,7 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
* [gotchas](#gotchas) - behavior that might be unexpected
|
||||
* [cors](#cors) - cross-site request config
|
||||
* [filekeys](#filekeys) - prevent filename bruteforcing
|
||||
* [dirkeys](#dirkeys) - share specific folders in a volume
|
||||
* [password hashing](#password-hashing) - you can hash passwords
|
||||
* [https](#https) - both HTTP and HTTPS are accepted
|
||||
* [recovering from crashes](#recovering-from-crashes)
|
||||
@@ -103,7 +111,7 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
* [sfx](#sfx) - the self-contained "binary"
|
||||
* [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+)
|
||||
* [install on android](#install-on-android)
|
||||
* [reporting bugs](#reporting-bugs) - ideas for context to include in bug reports
|
||||
* [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)
|
||||
|
||||
|
||||
@@ -122,7 +130,7 @@ enable thumbnails (images/audio/video), media indexing, and audio transcoding by
|
||||
|
||||
* **Alpine:** `apk add py3-pillow ffmpeg`
|
||||
* **Debian:** `apt install --no-install-recommends python3-pil ffmpeg`
|
||||
* **Fedora:** rpmfusion + `dnf install python3-pillow ffmpeg`
|
||||
* **Fedora:** rpmfusion + `dnf install python3-pillow ffmpeg --allowerasing`
|
||||
* **FreeBSD:** `pkg install py39-sqlite3 py39-pillow ffmpeg`
|
||||
* **MacOS:** `port install py-Pillow ffmpeg`
|
||||
* **MacOS** (alternative): `brew install pillow ffmpeg`
|
||||
@@ -143,6 +151,17 @@ some recommended options:
|
||||
* see [accounts and volumes](#accounts-and-volumes) (or `--help-accounts`) for the syntax and other permissions
|
||||
|
||||
|
||||
### at home
|
||||
|
||||
make it accessible over the internet by starting a [cloudflare quicktunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/) like so:
|
||||
|
||||
first download [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) and then start the tunnel with `cloudflared tunnel --url http://127.0.0.1:3923`
|
||||
|
||||
as the tunnel starts, it will show a URL which you can share to let anyone browse your stash or upload files to you
|
||||
|
||||
since people will be connecting through cloudflare, run copyparty with `--xff-hdr cf-connecting-ip` to detect client IPs correctly
|
||||
|
||||
|
||||
### on servers
|
||||
|
||||
you may also want these, especially on servers:
|
||||
@@ -157,21 +176,24 @@ you may also want these, especially on servers:
|
||||
and remember to open the ports you want; here's a complete example including every feature copyparty has to offer:
|
||||
```
|
||||
firewall-cmd --permanent --add-port={80,443,3921,3923,3945,3990}/tcp # --zone=libvirt
|
||||
firewall-cmd --permanent --add-port=12000-12099/tcp --permanent # --zone=libvirt
|
||||
firewall-cmd --permanent --add-port={1900,5353}/udp # --zone=libvirt
|
||||
firewall-cmd --permanent --add-port=12000-12099/tcp # --zone=libvirt
|
||||
firewall-cmd --permanent --add-port={69,1900,3969,5353}/udp # --zone=libvirt
|
||||
firewall-cmd --reload
|
||||
```
|
||||
(1900:ssdp, 3921:ftp, 3923:http/https, 3945:smb, 3990:ftps, 5353:mdns, 12000:passive-ftp)
|
||||
(69:tftp, 1900:ssdp, 3921:ftp, 3923:http/https, 3945:smb, 3969:tftp, 3990:ftps, 5353:mdns, 12000:passive-ftp)
|
||||
|
||||
|
||||
## features
|
||||
|
||||
also see [comparison to similar software](./docs/versus.md)
|
||||
|
||||
* backend stuff
|
||||
* ☑ IPv6
|
||||
* ☑ [multiprocessing](#performance) (actual multithreading)
|
||||
* ☑ volumes (mountpoints)
|
||||
* ☑ [accounts](#accounts-and-volumes)
|
||||
* ☑ [ftp server](#ftp-server)
|
||||
* ☑ [tftp server](#tftp-server)
|
||||
* ☑ [webdav server](#webdav-server)
|
||||
* ☑ [smb/cifs server](#smb-server)
|
||||
* ☑ [qr-code](#qr-code) for quick access
|
||||
@@ -187,6 +209,7 @@ firewall-cmd --reload
|
||||
* ☑ write-only folders
|
||||
* ☑ [unpost](#unpost): undo/delete accidental uploads
|
||||
* ☑ [self-destruct](#self-destruct) (specified server-side or client-side)
|
||||
* ☑ [race the beam](#race-the-beam) (almost like peer-to-peer)
|
||||
* ☑ symlink/discard duplicates (content-matching)
|
||||
* download
|
||||
* ☑ single files in browser
|
||||
@@ -195,7 +218,7 @@ firewall-cmd --reload
|
||||
* browser
|
||||
* ☑ [navpane](#navpane) (directory tree sidebar)
|
||||
* ☑ file manager (cut/paste, delete, [batch-rename](#batch-rename))
|
||||
* ☑ audio player (with [OS media controls](https://user-images.githubusercontent.com/241032/215347492-b4250797-6c90-4e09-9a4c-721edf2fb15c.png) and opus transcoding)
|
||||
* ☑ audio player (with [OS media controls](https://user-images.githubusercontent.com/241032/215347492-b4250797-6c90-4e09-9a4c-721edf2fb15c.png) and opus/mp3 transcoding)
|
||||
* ☑ image gallery with webm player
|
||||
* ☑ textfile browser with syntax hilighting
|
||||
* ☑ [thumbnails](#thumbnails)
|
||||
@@ -284,6 +307,9 @@ roughly sorted by chance of encounter
|
||||
* cannot index non-ascii filenames with `-e2d`
|
||||
* cannot handle filenames with mojibake
|
||||
|
||||
if you have a new exciting bug to share, see [reporting bugs](#reporting-bugs)
|
||||
|
||||
|
||||
## not my bugs
|
||||
|
||||
same order here too
|
||||
@@ -339,9 +365,24 @@ upgrade notes
|
||||
* yes, using the [`g` permission](#accounts-and-volumes), see the examples there
|
||||
* you can also do this with linux filesystem permissions; `chmod 111 music` will make it possible to access files and folders inside the `music` folder but not list the immediate contents -- also works with other software, not just copyparty
|
||||
|
||||
* 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
|
||||
|
||||
* 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)
|
||||
|
||||
* can I make copyparty download a file to my server if I give it a URL?
|
||||
* yes, using [hooks](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/wget.py)
|
||||
|
||||
* firefox refuses to connect over https, saying "Secure Connection Failed" or "SEC_ERROR_BAD_SIGNATURE", but the usual button to "Accept the Risk and Continue" is not shown
|
||||
* firefox has corrupted its certstore; fix this by exiting firefox, then find and delete the file named `cert9.db` somewhere in your firefox profile folder
|
||||
|
||||
* the server keeps saying `thank you for playing` when I try to access the website
|
||||
* you've gotten banned for malicious traffic! if this happens by mistake, and you're running a reverse-proxy and/or something like cloudflare, see [real-ip](#real-ip) on how to fix this
|
||||
|
||||
* copyparty seems to think I am using http, even though the URL is https
|
||||
* your reverse-proxy is not sending the `X-Forwarded-Proto: https` header; this could be because your reverse-proxy itself is confused. Ensure that none of the intermediates (such as cloudflare) are terminating https before the traffic hits your entrypoint
|
||||
|
||||
* i want to learn python and/or programming and am considering looking at the copyparty source code in that occasion
|
||||
* ```bash
|
||||
_| _ __ _ _|_
|
||||
@@ -565,7 +606,7 @@ you can also zip a selection of files or folders by clicking them in the browser
|
||||
|
||||

|
||||
|
||||
cool trick: download a folder by appending url-params `?tar&opus` to transcode all audio files (except aac|m4a|mp3|ogg|opus|wma) to opus before they're added to the archive
|
||||
cool trick: download a folder by appending url-params `?tar&opus` or `?tar&mp3` to transcode all audio files (except aac|m4a|mp3|ogg|opus|wma) to opus/mp3 before they're added to the archive
|
||||
* super useful if you're 5 minutes away from takeoff and realize you don't have any music on your phone but your server only has flac files and downloading those will burn through all your data + there wouldn't be enough time anyways
|
||||
* and url-params `&j` / `&w` produce jpeg/webm thumbnails/spectrograms instead of the original audio/video/images
|
||||
* can also be used to pregenerate thumbnails; combine with `--th-maxage=9999999` or `--th-clean=0`
|
||||
@@ -579,7 +620,7 @@ this initiates an upload using `up2k`; there are two uploaders available:
|
||||
* `[🎈] bup`, the basic uploader, supports almost every browser since netscape 4.0
|
||||
* `[🚀] up2k`, the good / fancy one
|
||||
|
||||
NB: you can undo/delete your own uploads with `[🧯]` [unpost](#unpost)
|
||||
NB: you can undo/delete your own uploads with `[🧯]` [unpost](#unpost) (and this is also where you abort unfinished uploads, but you have to refresh the page first)
|
||||
|
||||
up2k has several advantages:
|
||||
* you can drop folders into the browser (files are added recursively)
|
||||
@@ -594,7 +635,7 @@ up2k has several advantages:
|
||||
> it is perfectly safe to restart / upgrade copyparty while someone is uploading to it!
|
||||
> all known up2k clients will resume just fine 💪
|
||||
|
||||
see [up2k](#up2k) for details on how it works, or watch a [demo video](https://a.ocv.me/pub/demo/pics-vids/#gf-0f6f5c0d)
|
||||
see [up2k](./docs/devnotes.md#up2k) for details on how it works, or watch a [demo video](https://a.ocv.me/pub/demo/pics-vids/#gf-0f6f5c0d)
|
||||
|
||||

|
||||
|
||||
@@ -660,6 +701,13 @@ clients can specify a shorter expiration time using the [up2k ui](#uploading) --
|
||||
specifying a custom expiration time client-side will affect the timespan in which unposts are permitted, so keep an eye on the estimates in the up2k ui
|
||||
|
||||
|
||||
### race the beam
|
||||
|
||||
download files while they're still uploading ([demo video](http://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm)) -- it's almost like peer-to-peer
|
||||
|
||||
requires the file to be uploaded using up2k (which is the default drag-and-drop uploader), alternatively the command-line program
|
||||
|
||||
|
||||
## file manager
|
||||
|
||||
cut/paste, rename, and delete files/folders (if you have permission)
|
||||
@@ -756,9 +804,9 @@ open the `[🎺]` media-player-settings tab to configure it,
|
||||
* `[loop]` keeps looping the folder
|
||||
* `[next]` plays into the next folder
|
||||
* "transcode":
|
||||
* `[flac]` converts `flac` and `wav` files into opus
|
||||
* `[aac]` converts `aac` and `m4a` files into opus
|
||||
* `[oth]` converts all other known formats into opus
|
||||
* `[flac]` converts `flac` and `wav` files into opus (if supported by browser) or mp3
|
||||
* `[aac]` converts `aac` and `m4a` files into opus (if supported by browser) or mp3
|
||||
* `[oth]` converts all other known formats into opus (if supported by browser) or mp3
|
||||
* `aac|ac3|aif|aiff|alac|alaw|amr|ape|au|dfpwm|dts|flac|gsm|it|m4a|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|ogg|okt|opus|ra|s3m|tak|tta|ulaw|wav|wma|wv|xm|xpk`
|
||||
* "tint" reduces the contrast of the playback bar
|
||||
|
||||
@@ -943,6 +991,35 @@ known client bugs:
|
||||
* latin-1 is fine, hiragana is not (not even as shift-jis on japanese xp)
|
||||
|
||||
|
||||
## tftp server
|
||||
|
||||
a TFTP server (read/write) can be started using `--tftp 3969` (you probably want [ftp](#ftp-server) instead unless you are *actually* communicating with hardware from the 90s (in which case we should definitely hang some time))
|
||||
|
||||
> that makes this the first RTX DECT Base that has been updated using copyparty 🎉
|
||||
|
||||
* based on [partftpy](https://github.com/9001/partftpy)
|
||||
* no accounts; read from world-readable folders, write to world-writable, overwrite in world-deletable
|
||||
* needs a dedicated port (cannot share with the HTTP/HTTPS API)
|
||||
* run as root (or see below) to use the spec-recommended port `69` (nice)
|
||||
* can reply from a predefined portrange (good for firewalls)
|
||||
* only supports the binary/octet/image transfer mode (no netascii)
|
||||
* [RFC 7440](https://datatracker.ietf.org/doc/html/rfc7440) is **not** supported, so will be extremely slow over WAN
|
||||
* assuming default blksize (512), expect 1100 KiB/s over 100BASE-T, 400-500 KiB/s over wifi, 200 on bad wifi
|
||||
|
||||
most clients expect to find TFTP on port 69, but on linux and macos you need to be root to listen on that. Alternatively, listen on 3969 and use NAT on the server to forward 69 to that port;
|
||||
* on linux: `iptables -t nat -A PREROUTING -i eth0 -p udp --dport 69 -j REDIRECT --to-port 3969`
|
||||
|
||||
some recommended TFTP clients:
|
||||
* curl (cross-platform, read/write)
|
||||
* get: `curl --tftp-blksize 1428 tftp://127.0.0.1:3969/firmware.bin`
|
||||
* put: `curl --tftp-blksize 1428 -T firmware.bin tftp://127.0.0.1:3969/`
|
||||
* windows: `tftp.exe` (you probably already have it)
|
||||
* `tftp -i 127.0.0.1 put firmware.bin`
|
||||
* linux: `tftp-hpa`, `atftp`
|
||||
* `atftp --option "blksize 1428" 127.0.0.1 3969 -p -l firmware.bin -r firmware.bin`
|
||||
* `tftp -v -m binary 127.0.0.1 3969 -c put firmware.bin`
|
||||
|
||||
|
||||
## smb server
|
||||
|
||||
unsafe, slow, not recommended for wan, enable with `--smb` for read-only or `--smbw` for read-write
|
||||
@@ -973,7 +1050,7 @@ known client bugs:
|
||||
* however smb1 is buggy and is not enabled by default on win10 onwards
|
||||
* windows cannot access folders which contain filenames with invalid unicode or forbidden characters (`<>:"/\|?*`), or names ending with `.`
|
||||
|
||||
the smb protocol listens on TCP port 445, which is a privileged port on linux and macos, which would require running copyparty as root. However, this can be avoided by listening on another port using `--smb-port 3945` and then using NAT to forward the traffic from 445 to there;
|
||||
the smb protocol listens on TCP port 445, which is a privileged port on linux and macos, which would require running copyparty as root. However, this can be avoided by listening on another port using `--smb-port 3945` and then using NAT on the server to forward the traffic from 445 to there;
|
||||
* on linux: `iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 445 -j REDIRECT --to-port 3945`
|
||||
|
||||
authenticate with one of the following:
|
||||
@@ -990,6 +1067,8 @@ tweaking the ui
|
||||
* to sort in music order (album, track, artist, title) with filename as fallback, you could `--sort tags/Cirle,tags/.tn,tags/Artist,tags/Title,href`
|
||||
* to sort by upload date, first enable showing the upload date in the listing with `-e2d -mte +.up_at` and then `--sort tags/.up_at`
|
||||
|
||||
see [./docs/rice](./docs/rice) for more
|
||||
|
||||
|
||||
## file indexing
|
||||
|
||||
@@ -1221,11 +1300,26 @@ replace 404 and 403 errors with something completely different (that's it for no
|
||||
|
||||
replace copyparty passwords with oauth and such
|
||||
|
||||
work is [ongoing](https://github.com/9001/copyparty/issues/62) to support authenticating / authorizing users based on a separate authentication proxy, which makes it possible to support oauth, single-sign-on, etc.
|
||||
you can disable the built-in password-based login sysem, and instead replace it with a separate piece of software (an identity provider) which will then handle authenticating / authorizing of users; this makes it possible to login with passkeys / fido2 / webauthn / yubikey / ldap / active directory / oauth / many other single-sign-on contraptions
|
||||
|
||||
it is currently possible to specify `--idp-h-usr x-username`; copyparty will then skip password validation and blindly trust the username specified in the `X-Username` request header
|
||||
a popular choice is [Authelia](https://www.authelia.com/) (config-file based), another one is [authentik](https://goauthentik.io/) (GUI-based, more complex)
|
||||
|
||||
the remaining stuff (accepting user groups through another header, creating volumes on the fly) are still to-do; configuration will probably [look like this](./docs/examples/docker/idp/copyparty.conf)
|
||||
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)
|
||||
|
||||
a more complete example of the copyparty configuration options [look like this](./docs/examples/docker/idp/copyparty.conf)
|
||||
|
||||
|
||||
## using the cloud as storage
|
||||
|
||||
connecting to an aws s3 bucket and similar
|
||||
|
||||
there is no built-in support for this, but you can use FUSE-software such as [rclone](https://rclone.org/) / [geesefs](https://github.com/yandex-cloud/geesefs) / [JuiceFS](https://juicefs.com/en/) to first mount your cloud storage as a local disk, and then let copyparty use (a folder in) that disk as a volume
|
||||
|
||||
you may experience poor upload performance this way, but that can sometimes be fixed by specifying the volflag `sparse` to force the use of sparse files; this has improved the upload speeds from `1.5 MiB/s` to over `80 MiB/s` in one case, but note that you are also more likely to discover funny bugs in your FUSE software this way, so buckle up
|
||||
|
||||
someone has also tested geesefs in combination with [gocryptfs](https://nuetzlich.net/gocryptfs/) with surprisingly good results, getting 60 MiB/s upload speeds on a gbit line, but JuiceFS won with 80 MiB/s using its built-in encryption
|
||||
|
||||
you may improve performance by specifying larger values for `--iobuf` / `--s-rd-sz` / `--s-wr-sz`
|
||||
|
||||
|
||||
## hiding from google
|
||||
@@ -1261,6 +1355,8 @@ the classname of the HTML tag is set according to the selected theme, which is u
|
||||
|
||||
see the top of [./copyparty/web/browser.css](./copyparty/web/browser.css) where the color variables are set, and there's layout-specific stuff near the bottom
|
||||
|
||||
if you want to change the fonts, see [./docs/rice/](./docs/rice/)
|
||||
|
||||
|
||||
## complete examples
|
||||
|
||||
@@ -1321,6 +1417,15 @@ example webserver configs:
|
||||
* [apache2 config](contrib/apache/copyparty.conf) -- location-based
|
||||
|
||||
|
||||
### real-ip
|
||||
|
||||
teaching copyparty how to see client IPs when running behind a reverse-proxy, or a WAF, or another protection service such as cloudflare
|
||||
|
||||
if you (and maybe everybody else) keep getting a message that says `thank you for playing`, then you've gotten banned for malicious traffic. This ban applies to the IP address that copyparty *thinks* identifies the shady client -- so, depending on your setup, you might have to tell copyparty where to find the correct IP
|
||||
|
||||
for most common setups, there should be a helpful message in the server-log explaining what to do, but see [docs/xff.md](docs/xff.md) if you want to learn more, including a quick hack to **just make it work** (which is **not** recommended, but hey...)
|
||||
|
||||
|
||||
## prometheus
|
||||
|
||||
metrics/stats can be enabled at URL `/.cpr/metrics` for grafana / prometheus / etc (openmetrics 1.0.0)
|
||||
@@ -1400,17 +1505,7 @@ it comes with a [systemd service](./contrib/package/arch/copyparty.service) and
|
||||
|
||||
## fedora package
|
||||
|
||||
currently **NOT** available on [copr-pypi](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/) , fedora is having issues with their build servers and won't be fixed for several months
|
||||
|
||||
if you previously installed copyparty from copr, you may run one of the following commands to upgrade to a more recent version:
|
||||
|
||||
```bash
|
||||
dnf install https://ocv.me/copyparty/fedora/37/python3-copyparty.fc37.noarch.rpm
|
||||
dnf install https://ocv.me/copyparty/fedora/38/python3-copyparty.fc38.noarch.rpm
|
||||
dnf install https://ocv.me/copyparty/fedora/39/python3-copyparty.fc39.noarch.rpm
|
||||
```
|
||||
|
||||
to run copyparty as a service, use the [systemd service scripts](https://github.com/9001/copyparty/tree/hovudstraum/contrib/systemd), just replace `/usr/bin/python3 /usr/local/bin/copyparty-sfx.py` with `/usr/bin/copyparty`
|
||||
does not exist yet; using the [copr-pypi](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/) builds is **NOT recommended** because updates can be delayed by [several months](https://github.com/fedora-copr/copr/issues/3056)
|
||||
|
||||
|
||||
## nix package
|
||||
@@ -1673,14 +1768,16 @@ below are some tweaks roughly ordered by usefulness:
|
||||
|
||||
* `-q` disables logging and can help a bunch, even when combined with `-lo` to redirect logs to file
|
||||
* `--hist` pointing to a fast location (ssd) will make directory listings and searches faster when `-e2d` or `-e2t` is set
|
||||
* and also makes thumbnails load faster, regardless of e2d/e2t
|
||||
* `--no-hash .` when indexing a network-disk if you don't care about the actual filehashes and only want the names/tags searchable
|
||||
* if your volumes are on a network-disk such as NFS / SMB / s3, specifying larger values for `--iobuf` and/or `--s-rd-sz` and/or `--s-wr-sz` may help; try setting all of them to `524288` or `1048576` or `4194304`
|
||||
* `--no-htp --hash-mt=0 --mtag-mt=1 --th-mt=1` minimizes the number of threads; can help in some eccentric environments (like the vscode debugger)
|
||||
* `-j0` enables multiprocessing (actual multithreading), can reduce latency to `20+80/numCores` percent and generally improve performance in cpu-intensive workloads, for example:
|
||||
* lots of connections (many users or heavy clients)
|
||||
* simultaneous downloads and uploads saturating a 20gbps connection
|
||||
* if `-e2d` is enabled, `-j2` gives 4x performance for directory listings; `-j4` gives 16x
|
||||
|
||||
...however it adds an overhead to internal communication so it might be a net loss, see if it works 4 u
|
||||
...however it also increases the server/filesystem/HDD load during uploads, and adds an overhead to internal communication, so it is usually a better idea to don't
|
||||
* 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)
|
||||
|
||||
@@ -1689,7 +1786,7 @@ below are some tweaks roughly ordered by usefulness:
|
||||
|
||||
when uploading files,
|
||||
|
||||
* chrome is recommended, at least compared to firefox:
|
||||
* chrome is recommended (unfortunately), at least compared to firefox:
|
||||
* up to 90% faster when hashing, especially on SSDs
|
||||
* up to 40% faster when uploading over extremely fast internets
|
||||
* but [u2c.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) can be 40% faster than chrome again
|
||||
@@ -1770,12 +1867,29 @@ cors can be configured with `--acao` and `--acam`, or the protections entirely d
|
||||
|
||||
prevent filename bruteforcing
|
||||
|
||||
volflag `c,fk` generates filekeys (per-file accesskeys) for all files; users which have full read-access (permission `r`) will then see URLs with the correct filekey `?k=...` appended to the end, and `g` users must provide that URL including the correct key to avoid a 404
|
||||
volflag `fk` generates filekeys (per-file accesskeys) for all files; users which have full read-access (permission `r`) will then see URLs with the correct filekey `?k=...` appended to the end, and `g` users must provide that URL including the correct key to avoid a 404
|
||||
|
||||
by default, filekeys are generated based on salt (`--fk-salt`) + filesystem-path + file-size + inode (if not windows); add volflag `fka` to generate slightly weaker filekeys which will not be invalidated if the file is edited (only salt + path)
|
||||
|
||||
permissions `wG` (write + upget) lets users upload files and receive their own filekeys, still without being able to see other uploads
|
||||
|
||||
### dirkeys
|
||||
|
||||
share specific folders in a volume without giving away full read-access to the rest -- the visitor only needs the `g` (get) permission to view the link
|
||||
|
||||
volflag `dk` generates dirkeys (per-directory accesskeys) for all folders, granting read-access to that folder; by default only that folder itself, no subfolders
|
||||
|
||||
volflag `dky` disables the actual key-check, meaning anyone can see the contents of a folder where they have `g` access, but not its subdirectories
|
||||
|
||||
* `dk` + `dky` gives the same behavior as if all users with `g` access have full read-access, but subfolders are hidden files (their names start with a dot), so `dky` is an alternative to renaming all the folders for that purpose, maybe just for some users
|
||||
|
||||
volflag `dks` lets people enter subfolders as well, and also enables download-as-zip/tar
|
||||
|
||||
dirkeys are generated based on another salt (`--dk-salt`) + filesystem-path and have a few limitations:
|
||||
* the key does not change if the contents of the folder is modified
|
||||
* if you need a new dirkey, either change the salt or rename the folder
|
||||
* linking to a textfile (so it opens in the textfile viewer) is not possible if recipient doesn't have read-access
|
||||
|
||||
|
||||
## password hashing
|
||||
|
||||
@@ -1911,7 +2025,12 @@ if you want thumbnails (photos+videos) and you're okay with spending another 132
|
||||
|
||||
# reporting bugs
|
||||
|
||||
ideas for context to include in bug reports
|
||||
ideas for context to include, and where to submit them
|
||||
|
||||
please get in touch using any of the following URLs:
|
||||
* https://github.com/9001/copyparty/ **(primary)**
|
||||
* https://gitlab.com/9001/copyparty/ *(mirror)*
|
||||
* https://codeberg.org/9001/copyparty *(mirror)*
|
||||
|
||||
in general, commandline arguments (and config file if any)
|
||||
|
||||
@@ -1926,3 +2045,6 @@ if there's a wall of base64 in the log (thread stacks) then please include that,
|
||||
# devnotes
|
||||
|
||||
for build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md)
|
||||
|
||||
see [./docs/TODO.md](./docs/TODO.md) for planned features / fixes / changes
|
||||
|
||||
|
||||
@@ -223,12 +223,15 @@ install_vamp() {
|
||||
# use msys2 in mingw-w64 mode
|
||||
# pacman -S --needed mingw-w64-x86_64-{ffmpeg,python,python-pip,vamp-plugin-sdk}
|
||||
|
||||
$pybin -m pip install --user vamp
|
||||
$pybin -m pip install --user vamp || {
|
||||
printf '\n\033[7malright, trying something else...\033[0m\n'
|
||||
$pybin -m pip install --user --no-build-isolation vamp
|
||||
}
|
||||
|
||||
cd "$td"
|
||||
echo '#include <vamp-sdk/Plugin.h>' | g++ -x c++ -c -o /dev/null - || [ -e ~/pe/vamp-sdk ] || {
|
||||
printf '\033[33mcould not find the vamp-sdk, building from source\033[0m\n'
|
||||
(dl_files yolo https://code.soundsoftware.ac.uk/attachments/download/2691/vamp-plugin-sdk-2.10.0.tar.gz)
|
||||
(dl_files yolo https://ocv.me/mirror/vamp-plugin-sdk-2.10.0.tar.gz)
|
||||
sha512sum -c <(
|
||||
echo "153b7f2fa01b77c65ad393ca0689742d66421017fd5931d216caa0fcf6909355fff74706fabbc062a3a04588a619c9b515a1dae00f21a57afd97902a355c48ed -"
|
||||
) <vamp-plugin-sdk-2.10.0.tar.gz
|
||||
@@ -244,7 +247,7 @@ install_vamp() {
|
||||
cd "$td"
|
||||
have_beatroot || {
|
||||
printf '\033[33mcould not find the vamp beatroot plugin, building from source\033[0m\n'
|
||||
(dl_files yolo https://code.soundsoftware.ac.uk/attachments/download/885/beatroot-vamp-v1.0.tar.gz)
|
||||
(dl_files yolo https://ocv.me/mirror/beatroot-vamp-v1.0.tar.gz)
|
||||
sha512sum -c <(
|
||||
echo "1f444d1d58ccf565c0adfe99f1a1aa62789e19f5071e46857e2adfbc9d453037bc1c4dcb039b02c16240e9b97f444aaff3afb625c86aa2470233e711f55b6874 -"
|
||||
) <beatroot-vamp-v1.0.tar.gz
|
||||
|
||||
22
bin/u2c.py
22
bin/u2c.py
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
S_VERSION = "1.14"
|
||||
S_BUILD_DT = "2024-01-27"
|
||||
S_VERSION = "1.16"
|
||||
S_BUILD_DT = "2024-04-20"
|
||||
|
||||
"""
|
||||
u2c.py: upload to copyparty
|
||||
@@ -29,7 +29,7 @@ import platform
|
||||
import threading
|
||||
import datetime
|
||||
|
||||
EXE = sys.executable.endswith("exe")
|
||||
EXE = bool(getattr(sys, "frozen", False))
|
||||
|
||||
try:
|
||||
import argparse
|
||||
@@ -563,7 +563,7 @@ def handshake(ar, file, search):
|
||||
else:
|
||||
if ar.touch:
|
||||
req["umod"] = True
|
||||
if ar.dr:
|
||||
if ar.ow:
|
||||
req["replace"] = True
|
||||
|
||||
headers = {"Content-Type": "text/plain"} # <=1.5.1 compat
|
||||
@@ -846,12 +846,12 @@ class Ctl(object):
|
||||
txt = " "
|
||||
|
||||
if not self.up_br:
|
||||
spd = self.hash_b / (time.time() - self.t0)
|
||||
eta = (self.nbytes - self.hash_b) / (spd + 1)
|
||||
spd = self.hash_b / ((time.time() - self.t0) or 1)
|
||||
eta = (self.nbytes - self.hash_b) / (spd or 1)
|
||||
else:
|
||||
spd = self.up_br / (time.time() - self.t0_up)
|
||||
spd = self.up_br / ((time.time() - self.t0_up) or 1)
|
||||
spd = self.spd = (self.spd or spd) * 0.9 + spd * 0.1
|
||||
eta = (self.nbytes - self.up_b) / (spd + 1)
|
||||
eta = (self.nbytes - self.up_b) / (spd or 1)
|
||||
|
||||
spd = humansize(spd)
|
||||
self.eta = str(datetime.timedelta(seconds=int(eta)))
|
||||
@@ -1140,6 +1140,7 @@ source file/folder selection uses rsync syntax, meaning that:
|
||||
ap.add_argument("-x", type=unicode, metavar="REGEX", default="", help="skip file if filesystem-abspath matches REGEX, example: '.*/\\.hist/.*'")
|
||||
ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible")
|
||||
ap.add_argument("--touch", action="store_true", help="if last-modified timestamps differ, push local to server (need write+delete perms)")
|
||||
ap.add_argument("--ow", action="store_true", help="overwrite existing files instead of autorenaming")
|
||||
ap.add_argument("--version", action="store_true", help="show version and exit")
|
||||
|
||||
ap = app.add_argument_group("compatibility")
|
||||
@@ -1148,7 +1149,7 @@ source file/folder selection uses rsync syntax, meaning that:
|
||||
|
||||
ap = app.add_argument_group("folder sync")
|
||||
ap.add_argument("--dl", action="store_true", help="delete local files after uploading")
|
||||
ap.add_argument("--dr", action="store_true", help="delete remote files which don't exist locally")
|
||||
ap.add_argument("--dr", action="store_true", help="delete remote files which don't exist locally (implies --ow)")
|
||||
ap.add_argument("--drd", action="store_true", help="delete remote files during upload instead of afterwards; reduces peak disk space usage, but will reupload instead of detecting renames")
|
||||
|
||||
ap = app.add_argument_group("performance tweaks")
|
||||
@@ -1178,6 +1179,9 @@ source file/folder selection uses rsync syntax, meaning that:
|
||||
if ar.drd:
|
||||
ar.dr = True
|
||||
|
||||
if ar.dr:
|
||||
ar.ow = True
|
||||
|
||||
for k in "dl dr drd".split():
|
||||
errs = []
|
||||
if ar.safe and getattr(ar, k):
|
||||
|
||||
@@ -16,11 +16,8 @@
|
||||
* sharex config file to upload screenshots and grab the URL
|
||||
* `RequestURL`: full URL to the target folder
|
||||
* `pw`: password (remove the `pw` line if anon-write)
|
||||
|
||||
however if your copyparty is behind a reverse-proxy, you may want to use [`sharex-html.sxcu`](sharex-html.sxcu) instead:
|
||||
* `RequestURL`: full URL to the target folder
|
||||
* `URL`: full URL to the root folder (with trailing slash) followed by `$regex:1|1$`
|
||||
* `pw`: password (remove `Parameters` if anon-write)
|
||||
* the `act:bput` thing is optional since copyparty v1.9.29
|
||||
* using an older sharex version, maybe sharex v12.1.1 for example? dw fam i got your back 👉😎👉 [`sharex12.sxcu`](sharex12.sxcu)
|
||||
|
||||
### [`send-to-cpp.contextlet.json`](send-to-cpp.contextlet.json)
|
||||
* browser integration, kind of? custom rightclick actions and stuff
|
||||
|
||||
@@ -11,6 +11,14 @@
|
||||
# (5'000 requests per second, or 20gbps upload/download in parallel)
|
||||
#
|
||||
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
|
||||
#
|
||||
# if you are behind cloudflare (or another protection service),
|
||||
# remember to reject all connections which are not coming from your
|
||||
# protection service -- for cloudflare in particular, you can
|
||||
# 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
|
||||
|
||||
upstream cpp {
|
||||
server 127.0.0.1:3923 fail_timeout=1s;
|
||||
@@ -21,7 +29,10 @@ server {
|
||||
listen [::]:443 ssl;
|
||||
|
||||
server_name fs.example.com;
|
||||
|
||||
|
||||
# uncomment the following line to reject non-cloudflare connections, ensuring client IPs cannot be spoofed:
|
||||
#include /etc/nginx/cloudflare-only.conf;
|
||||
|
||||
location / {
|
||||
proxy_pass http://cpp;
|
||||
proxy_redirect off;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Maintainer: icxes <dev.null@need.moe>
|
||||
pkgname=copyparty
|
||||
pkgver="1.9.30"
|
||||
pkgver="1.12.2"
|
||||
pkgrel=1
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, zeroconf, media indexer, thumbnails++"
|
||||
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')
|
||||
@@ -21,7 +21,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
|
||||
)
|
||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||
backup=("etc/${pkgname}.d/init" )
|
||||
sha256sums=("dd93fcc0f8c4885724f65d4c60d93af17bc8d6f55e5b4a00e8f75f40cc7d1552")
|
||||
sha256sums=("e4fd6733e5361f5ceb2ae950f71f65f2609c2b69d45f47e8b2a2f128fb67de0a")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.9.30/copyparty-sfx.py",
|
||||
"version": "1.9.30",
|
||||
"hash": "sha256-MUHlRrNF5DwEpnGIKjfry0pM26OlnrPgWiwPxfRXUT4="
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.12.2/copyparty-sfx.py",
|
||||
"version": "1.12.2",
|
||||
"hash": "sha256-GJts5N0leK/WHqpqb+eB1JjBvf6TRpzCc9R7AIHkujo="
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"Version": "13.5.0",
|
||||
"Name": "copyparty-html",
|
||||
"DestinationType": "ImageUploader",
|
||||
"RequestMethod": "POST",
|
||||
"RequestURL": "http://127.0.0.1:3923/sharex",
|
||||
"Parameters": {
|
||||
"pw": "wark"
|
||||
},
|
||||
"Body": "MultipartFormData",
|
||||
"Arguments": {
|
||||
"act": "bput"
|
||||
},
|
||||
"FileFormName": "f",
|
||||
"RegexList": [
|
||||
"bytes // <a href=\"/([^\"]+)\""
|
||||
],
|
||||
"URL": "http://127.0.0.1:3923/$regex:1|1$"
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
{
|
||||
"Version": "13.5.0",
|
||||
"Version": "15.0.0",
|
||||
"Name": "copyparty",
|
||||
"DestinationType": "ImageUploader",
|
||||
"RequestMethod": "POST",
|
||||
"RequestURL": "http://127.0.0.1:3923/sharex",
|
||||
"Parameters": {
|
||||
"pw": "wark",
|
||||
"j": null
|
||||
},
|
||||
"Headers": {
|
||||
"pw": "PUT_YOUR_PASSWORD_HERE_MY_DUDE"
|
||||
},
|
||||
"Body": "MultipartFormData",
|
||||
"Arguments": {
|
||||
"act": "bput"
|
||||
},
|
||||
"FileFormName": "f",
|
||||
"URL": "$json:files[0].url$"
|
||||
"URL": "{json:files[0].url}"
|
||||
}
|
||||
|
||||
13
contrib/sharex12.sxcu
Normal file
13
contrib/sharex12.sxcu
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"Name": "copyparty",
|
||||
"DestinationType": "ImageUploader, TextUploader, FileUploader",
|
||||
"RequestURL": "http://127.0.0.1:3923/sharex",
|
||||
"FileFormName": "f",
|
||||
"Arguments": {
|
||||
"act": "bput"
|
||||
},
|
||||
"Headers": {
|
||||
"accept": "url",
|
||||
"pw": "PUT_YOUR_PASSWORD_HERE_MY_DUDE"
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,6 @@ class EnvParams(object):
|
||||
self.t0 = time.time()
|
||||
self.mod = ""
|
||||
self.cfg = ""
|
||||
self.ox = getattr(sys, "oxidized", None)
|
||||
|
||||
|
||||
E = EnvParams()
|
||||
|
||||
@@ -43,6 +43,7 @@ from .util import (
|
||||
DEF_MTH,
|
||||
IMPLICATIONS,
|
||||
JINJA_VER,
|
||||
PARTFTPY_VER,
|
||||
PY_DESC,
|
||||
PYFTPD_VER,
|
||||
SQLITE_VER,
|
||||
@@ -156,7 +157,8 @@ def warn(msg: str) -> None:
|
||||
|
||||
|
||||
def init_E(EE: EnvParams) -> None:
|
||||
# __init__ runs 18 times when oxidized; do expensive stuff here
|
||||
# some cpython alternatives (such as pyoxidizer) can
|
||||
# __init__ several times, so do expensive stuff here
|
||||
|
||||
E = EE # pylint: disable=redefined-outer-name
|
||||
|
||||
@@ -189,34 +191,9 @@ def init_E(EE: EnvParams) -> None:
|
||||
|
||||
raise Exception("could not find a writable path for config")
|
||||
|
||||
def _unpack() -> str:
|
||||
import atexit
|
||||
import tarfile
|
||||
import tempfile
|
||||
from importlib.resources import open_binary
|
||||
|
||||
td = tempfile.TemporaryDirectory(prefix="")
|
||||
atexit.register(td.cleanup)
|
||||
tdn = td.name
|
||||
|
||||
with open_binary("copyparty", "z.tar") as tgz:
|
||||
with tarfile.open(fileobj=tgz) as tf:
|
||||
try:
|
||||
tf.extractall(tdn, filter="tar")
|
||||
except TypeError:
|
||||
tf.extractall(tdn) # nosec (archive is safe)
|
||||
|
||||
return tdn
|
||||
|
||||
try:
|
||||
E.mod = os.path.dirname(os.path.realpath(__file__))
|
||||
if E.mod.endswith("__init__"):
|
||||
E.mod = os.path.dirname(E.mod)
|
||||
except:
|
||||
if not E.ox:
|
||||
raise
|
||||
|
||||
E.mod = _unpack()
|
||||
E.mod = os.path.dirname(os.path.realpath(__file__))
|
||||
if E.mod.endswith("__init__"):
|
||||
E.mod = os.path.dirname(E.mod)
|
||||
|
||||
if sys.platform == "win32":
|
||||
bdir = os.environ.get("APPDATA") or os.environ.get("TEMP") or "."
|
||||
@@ -273,6 +250,19 @@ def get_fk_salt() -> str:
|
||||
return ret.decode("utf-8")
|
||||
|
||||
|
||||
def get_dk_salt() -> str:
|
||||
fp = os.path.join(E.cfg, "dk-salt.txt")
|
||||
try:
|
||||
with open(fp, "rb") as f:
|
||||
ret = f.read().strip()
|
||||
except:
|
||||
ret = base64.b64encode(os.urandom(30))
|
||||
with open(fp, "wb") as f:
|
||||
f.write(ret + b"\n")
|
||||
|
||||
return ret.decode("utf-8")
|
||||
|
||||
|
||||
def get_ah_salt() -> str:
|
||||
fp = os.path.join(E.cfg, "ah-salt.txt")
|
||||
try:
|
||||
@@ -394,7 +384,7 @@ def configure_ssl_ciphers(al: argparse.Namespace) -> None:
|
||||
|
||||
def args_from_cfg(cfg_path: str) -> list[str]:
|
||||
lines: list[str] = []
|
||||
expand_config_file(lines, cfg_path, "")
|
||||
expand_config_file(None, lines, cfg_path, "")
|
||||
lines = upgrade_cfg_fmt(None, argparse.Namespace(vc=False), lines, "")
|
||||
|
||||
ret: list[str] = []
|
||||
@@ -502,6 +492,10 @@ def get_sects():
|
||||
* "\033[33mperm\033[0m" is "permissions,username1,username2,..."
|
||||
* "\033[32mvolflag\033[0m" is config flags to set on this volume
|
||||
|
||||
--grp takes groupname:username1,username2,...
|
||||
and groupnames can be used instead of usernames in -v
|
||||
by prefixing the groupname with %
|
||||
|
||||
list of permissions:
|
||||
"r" (read): list folder contents, download files
|
||||
"w" (write): upload files; need "r" to see the uploads
|
||||
@@ -839,6 +833,7 @@ def add_general(ap, nc, srvname):
|
||||
ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores, 0=all")
|
||||
ap2.add_argument("-a", metavar="ACCT", type=u, action="append", help="add account, \033[33mUSER\033[0m:\033[33mPASS\033[0m; example [\033[32med:wark\033[0m]")
|
||||
ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="add volume, \033[33mSRC\033[0m:\033[33mDST\033[0m:\033[33mFLAG\033[0m; examples [\033[32m.::r\033[0m], [\033[32m/mnt/nas/music:/music:r:aed\033[0m], see --help-accounts")
|
||||
ap2.add_argument("--grp", metavar="G:N,N", type=u, action="append", help="add group, \033[33mNAME\033[0m:\033[33mUSER1\033[0m,\033[33mUSER2\033[0m,\033[33m...\033[0m; example [\033[32madmins:ed,foo,bar\033[0m]")
|
||||
ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files (volflag=dots)")
|
||||
ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,get", help="how to handle url-form POSTs; see \033[33m--help-urlform\033[0m")
|
||||
ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="server terminal title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]")
|
||||
@@ -861,8 +856,10 @@ def add_qr(ap, tty):
|
||||
|
||||
def add_fs(ap):
|
||||
ap2 = ap.add_argument_group("filesystem options")
|
||||
rm_re_def = "5/0.1" if ANYWIN else "0/0"
|
||||
rm_re_def = "15/0.1" if ANYWIN else "0/0"
|
||||
ap2.add_argument("--rm-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be deleted because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=rm_retry)")
|
||||
ap2.add_argument("--mv-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be renamed because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=mv_retry)")
|
||||
ap2.add_argument("--iobuf", metavar="BYTES", type=int, default=256*1024, help="file I/O buffer-size; if your volumes are on a network drive, try increasing to \033[32m524288\033[0m or even \033[32m4194304\033[0m (and let me know if that improves your performance)")
|
||||
|
||||
|
||||
def add_upload(ap):
|
||||
@@ -870,6 +867,7 @@ def add_upload(ap):
|
||||
ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless \033[33m-ed\033[0m")
|
||||
ap2.add_argument("--plain-ip", action="store_true", help="when avoiding filename collisions by appending the uploader's ip to the filename: append the plaintext ip instead of salting and hashing the ip")
|
||||
ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled, default=12h")
|
||||
ap2.add_argument("--u2abort", metavar="NUM", type=int, default=1, help="clients can abort incomplete uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=u2abort)")
|
||||
ap2.add_argument("--blank-wt", metavar="SEC", type=int, default=300, help="file write grace period (any client can write to a blank file last-modified more recently than \033[33mSEC\033[0m seconds ago)")
|
||||
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600")
|
||||
ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload (bad idea to enable this on windows and/or cow filesystems)")
|
||||
@@ -900,8 +898,8 @@ def add_network(ap):
|
||||
ap2.add_argument("--ll", action="store_true", help="include link-local IPv4/IPv6 in mDNS replies, even if the NIC has routable IPs (breaks some mDNS clients)")
|
||||
ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to associate clients with; [\033[32m0\033[0m]=tcp, [\033[32m1\033[0m]=origin (first x-fwd, unsafe), [\033[32m2\033[0m]=outermost-proxy, [\033[32m3\033[0m]=second-proxy, [\033[32m-1\033[0m]=closest-proxy")
|
||||
ap2.add_argument("--xff-hdr", metavar="NAME", type=u, default="x-forwarded-for", help="if reverse-proxied, which http header to read the client's real ip from")
|
||||
ap2.add_argument("--xff-src", metavar="IP", type=u, default="127., ::1", help="comma-separated list of trusted reverse-proxy IPs; only accept the real-ip header (\033[33m--xff-hdr\033[0m) if the incoming connection is from an IP starting with either of these. Can be disabled with [\033[32many\033[0m] if you are behind cloudflare (or similar) and are using \033[32m--xff-hdr=cf-connecting-ip\033[0m (or similar)")
|
||||
ap2.add_argument("--ipa", metavar="PREFIX", type=u, default="", help="only accept connections from IP-addresses starting with \033[33mPREFIX\033[0m; example: [\033[32m127., 10.89., 192.168.\033[0m]")
|
||||
ap2.add_argument("--xff-src", metavar="CIDR", type=u, default="127.0.0.0/8, ::1/128", help="comma-separated list of trusted reverse-proxy CIDRs; only accept the real-ip header (\033[33m--xff-hdr\033[0m) and IdP headers if the incoming connection is from an IP within either of these subnets. Specify [\033[32mlan\033[0m] to allow all LAN / private / non-internet IPs. Can be disabled with [\033[32many\033[0m] if you are behind cloudflare (or similar) and are using \033[32m--xff-hdr=cf-connecting-ip\033[0m (or similar)")
|
||||
ap2.add_argument("--ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m; examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]")
|
||||
ap2.add_argument("--rp-loc", metavar="PATH", type=u, default="", help="if reverse-proxying on a location instead of a dedicated domain/subdomain, provide the base location here; example: [\033[32m/foo/bar\033[0m]")
|
||||
if ANYWIN:
|
||||
ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances")
|
||||
@@ -909,6 +907,7 @@ def add_network(ap):
|
||||
ap2.add_argument("--freebind", action="store_true", help="allow listening on IPs which do not yet exist, for example if the network interfaces haven't finished going up. Only makes sense for IPs other than '0.0.0.0', '127.0.0.1', '::', and '::1'. May require running as root (unless net.ipv6.ip_nonlocal_bind)")
|
||||
ap2.add_argument("--s-thead", metavar="SEC", type=int, default=120, help="socket timeout (read request header)")
|
||||
ap2.add_argument("--s-tbody", metavar="SEC", type=float, default=186, help="socket timeout (read/write request/response bodies). Use 60 on fast servers (default is extremely safe). Disable with 0 if reverse-proxied for a 2%% speed boost")
|
||||
ap2.add_argument("--s-rd-sz", metavar="B", type=int, default=256*1024, help="socket read size in bytes (indirectly affects filesystem writes; recommendation: keep equal-to or lower-than \033[33m--iobuf\033[0m)")
|
||||
ap2.add_argument("--s-wr-sz", metavar="B", type=int, default=256*1024, help="socket write size in bytes")
|
||||
ap2.add_argument("--s-wr-slp", metavar="SEC", type=float, default=0, help="debug: socket write delay in seconds")
|
||||
ap2.add_argument("--rsp-slp", metavar="SEC", type=float, default=0, help="debug: response delay in seconds")
|
||||
@@ -948,8 +947,11 @@ def add_cert(ap, cert_path):
|
||||
def add_auth(ap):
|
||||
ap2 = ap.add_argument_group('IdP / identity provider / user authentication options')
|
||||
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, default="", help="bypass the copyparty authentication checks and assume the request-header \033[33mHN\033[0m contains the username of the requesting user (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
|
||||
return
|
||||
ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control")
|
||||
ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present")
|
||||
ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m")
|
||||
ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app")
|
||||
ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins")
|
||||
|
||||
|
||||
def add_zeroconf(ap):
|
||||
@@ -993,12 +995,12 @@ def add_zc_ssdp(ap):
|
||||
|
||||
|
||||
def add_ftp(ap):
|
||||
ap2 = ap.add_argument_group('FTP options')
|
||||
ap2 = ap.add_argument_group('FTP options (TCP only)')
|
||||
ap2.add_argument("--ftp", metavar="PORT", type=int, help="enable FTP server on \033[33mPORT\033[0m, for example \033[32m3921")
|
||||
ap2.add_argument("--ftps", metavar="PORT", type=int, help="enable FTPS server on \033[33mPORT\033[0m, for example \033[32m3990")
|
||||
ap2.add_argument("--ftpv", action="store_true", help="verbose")
|
||||
ap2.add_argument("--ftp4", action="store_true", help="only listen on IPv4")
|
||||
ap2.add_argument("--ftp-ipa", metavar="PFX", type=u, default="", help="only accept connections from IP-addresses starting with \033[33mPFX\033[0m; specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m. Example: [\033[32m127., 10.89., 192.168.\033[0m]")
|
||||
ap2.add_argument("--ftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m; specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]")
|
||||
ap2.add_argument("--ftp-wt", metavar="SEC", type=int, default=7, help="grace period for resuming interrupted uploads (any client can write to any file last-modified more recently than \033[33mSEC\033[0m seconds ago)")
|
||||
ap2.add_argument("--ftp-nat", metavar="ADDR", type=u, help="the NAT address to use for passive connections")
|
||||
ap2.add_argument("--ftp-pr", metavar="P-P", type=u, help="the range of TCP ports to use for passive connections, for example \033[32m12000-13000")
|
||||
@@ -1013,6 +1015,18 @@ def add_webdav(ap):
|
||||
ap2.add_argument("--dav-auth", action="store_true", help="force auth for all folders (required by davfs2 when only some folders are world-readable) (volflag=davauth)")
|
||||
|
||||
|
||||
def add_tftp(ap):
|
||||
ap2 = ap.add_argument_group('TFTP options (UDP only)')
|
||||
ap2.add_argument("--tftp", metavar="PORT", type=int, help="enable TFTP server on \033[33mPORT\033[0m, for example \033[32m69 \033[0mor \033[32m3969")
|
||||
ap2.add_argument("--tftpv", action="store_true", help="verbose")
|
||||
ap2.add_argument("--tftpvv", action="store_true", help="verboser")
|
||||
ap2.add_argument("--tftp-no-fast", action="store_true", help="debug: disable optimizations")
|
||||
ap2.add_argument("--tftp-lsf", metavar="PTN", type=u, default="\\.?(dir|ls)(\\.txt)?", help="return a directory listing if a file with this name is requested and it does not exist; defaults matches .ls, dir, .dir.txt, ls.txt, ...")
|
||||
ap2.add_argument("--tftp-nols", action="store_true", help="if someone tries to download a directory, return an error instead of showing its directory listing")
|
||||
ap2.add_argument("--tftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m; specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]")
|
||||
ap2.add_argument("--tftp-pr", metavar="P-P", type=u, help="the range of UDP ports to use for data transfer, for example \033[32m12000-13000")
|
||||
|
||||
|
||||
def add_smb(ap):
|
||||
ap2 = ap.add_argument_group('SMB/CIFS options')
|
||||
ap2.add_argument("--smb", action="store_true", help="enable smb (read-only) -- this requires running copyparty as root on linux and macos unless \033[33m--smb-port\033[0m is set above 1024 and your OS does port-forwarding from 445 to that.\n\033[1;31mWARNING:\033[0m this protocol is DANGEROUS and buggy! Never expose to the internet!")
|
||||
@@ -1077,6 +1091,8 @@ def add_optouts(ap):
|
||||
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
|
||||
ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)")
|
||||
ap2.add_argument("--no-lifetime", action="store_true", help="do not allow clients (or server config) to schedule an upload to be deleted after a given time")
|
||||
ap2.add_argument("--no-pipe", action="store_true", help="disable race-the-beam (lockstep download of files which are currently being uploaded) (volflag=nopipe)")
|
||||
ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader IPs into the database")
|
||||
|
||||
|
||||
def add_safety(ap):
|
||||
@@ -1102,19 +1118,21 @@ def add_safety(ap):
|
||||
ap2.add_argument("--ban-url", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m sus URL's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; applies only to permissions g/G/h (decent replacement for \033[33m--ban-404\033[0m if that can't be used)")
|
||||
ap2.add_argument("--sus-urls", metavar="R", type=u, default=r"\.php$|(^|/)wp-(admin|content|includes)/", help="URLs which are considered sus / eligible for banning; disable with blank or [\033[32mno\033[0m]")
|
||||
ap2.add_argument("--nonsus-urls", metavar="R", type=u, default=r"^(favicon\.ico|robots\.txt)$|^apple-touch-icon|^\.well-known", help="harmless URLs ignored from 404-bans; disable with blank or [\033[32mno\033[0m]")
|
||||
ap2.add_argument("--early-ban", action="store_true", help="if a client is banned, reject its connection as soon as possible; not a good idea to enable when proxied behind cloudflare since it could ban your reverse-proxy")
|
||||
ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for \033[33mMIN\033[0m minutes (and also kill its active connections) -- disable with 0")
|
||||
ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]")
|
||||
ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives")
|
||||
ap2.add_argument("--acam", metavar="V[,V]", type=u, default="GET,HEAD", help="Access-Control-Allow-Methods; list of methods to accept from offsite ('*' behaves like \033[33m--acao\033[0m's description)")
|
||||
|
||||
|
||||
def add_salt(ap, fk_salt, ah_salt):
|
||||
def add_salt(ap, fk_salt, dk_salt, ah_salt):
|
||||
ap2 = ap.add_argument_group('salting options')
|
||||
ap2.add_argument("--ah-alg", metavar="ALG", type=u, default="none", help="account-pw hashing algorithm; one of these, best to worst: \033[32margon2 scrypt sha2 none\033[0m (each optionally followed by alg-specific comma-sep. config)")
|
||||
ap2.add_argument("--ah-salt", metavar="SALT", type=u, default=ah_salt, help="account-pw salt; ignored if \033[33m--ah-alg\033[0m is none (default)")
|
||||
ap2.add_argument("--ah-gen", metavar="PW", type=u, default="", help="generate hashed password for \033[33mPW\033[0m, or read passwords from STDIN if \033[33mPW\033[0m is [\033[32m-\033[0m]")
|
||||
ap2.add_argument("--ah-cli", action="store_true", help="launch an interactive shell which hashes passwords without ever storing or displaying the original passwords")
|
||||
ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files")
|
||||
ap2.add_argument("--dk-salt", metavar="SALT", type=u, default=dk_salt, help="per-directory accesskey salt; used to generate unpredictable URLs to share folders with users who only have the 'get' permission")
|
||||
ap2.add_argument("--warksalt", metavar="SALT", type=u, default="hunter2", help="up2k file-hash salt; serves no purpose, no reason to change this (but delete all databases if you do)")
|
||||
|
||||
|
||||
@@ -1157,7 +1175,8 @@ def add_thumbnail(ap):
|
||||
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails")
|
||||
ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60, help="conversion timeout in seconds (volflag=convt)")
|
||||
ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=6, help="max memory usage (GiB) permitted by thumbnailer; not very accurate")
|
||||
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image by default (client can override in UI) (volflag=nocrop)")
|
||||
ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32mfy\033[0m]=crop, [\033[32mfn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)")
|
||||
ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32mfy\033[0m]=yes, [\033[32mfn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)")
|
||||
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference")
|
||||
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
|
||||
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
|
||||
@@ -1179,6 +1198,8 @@ def add_thumbnail(ap):
|
||||
|
||||
def add_transcoding(ap):
|
||||
ap2 = ap.add_argument_group('transcoding options')
|
||||
ap2.add_argument("--q-opus", metavar="KBPS", type=int, default=128, help="target bitrate for transcoding to opus; set 0 to disable")
|
||||
ap2.add_argument("--q-mp3", metavar="QUALITY", type=u, default="q2", help="target quality for transcoding to mp3, for example [\033[32m192k\033[0m] (CBR) or [\033[32mq0\033[0m] (CQ/CRF, q0=maxquality, q9=smallest); set 0 to disable")
|
||||
ap2.add_argument("--no-acode", action="store_true", help="disable audio transcoding")
|
||||
ap2.add_argument("--no-bacode", action="store_true", help="disable batch audio transcoding by folder download (zip/tar)")
|
||||
ap2.add_argument("--ac-maxage", metavar="SEC", type=int, default=86400, help="delete cached transcode output after \033[33mSEC\033[0m seconds")
|
||||
@@ -1197,7 +1218,7 @@ def add_db_general(ap, hcores):
|
||||
ap2.add_argument("--no-hash", metavar="PTN", type=u, help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (volflag=nohash)")
|
||||
ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scans (volflag=noidx)")
|
||||
ap2.add_argument("--no-dhash", action="store_true", help="disable rescan acceleration; do full database integrity check -- makes the db ~5%% smaller and bootup/rescans 3~10x slower")
|
||||
ap2.add_argument("--re-dhash", action="store_true", help="rebuild the cache if it gets out of sync (for example crash on startup during metadata scanning)")
|
||||
ap2.add_argument("--re-dhash", action="store_true", help="force a cache rebuild on startup; enable this once if it gets out of sync (should never be necessary)")
|
||||
ap2.add_argument("--no-forget", action="store_true", help="never forget indexed files, even when deleted from disk -- makes it impossible to ever upload the same file twice -- only useful for offloading uploads to a cloud service or something (volflag=noforget)")
|
||||
ap2.add_argument("--dbd", metavar="PROFILE", default="wal", help="database durability profile; sets the tradeoff between robustness and speed, see \033[33m--help-dbd\033[0m (volflag=dbd)")
|
||||
ap2.add_argument("--xlink", action="store_true", help="on upload: check all volumes for dupes, not just the target volume (volflag=xlink)")
|
||||
@@ -1255,6 +1276,7 @@ def add_ui(ap, retry):
|
||||
ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
|
||||
ap2.add_argument("--pb-url", metavar="URL", type=u, default="https://github.com/9001/copyparty", help="powered-by link; disable with \033[33m-np\033[0m")
|
||||
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)")
|
||||
ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
|
||||
ap2.add_argument("--md-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to ALLOW for README.md docs (volflag=md_sbf); see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox")
|
||||
ap2.add_argument("--lg-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to ALLOW for prologue/epilogue docs (volflag=lg_sbf)")
|
||||
ap2.add_argument("--no-sb-md", action="store_true", help="don't sandbox README.md documents (volflags: no_sb_md | sb_md)")
|
||||
@@ -1294,6 +1316,7 @@ def run_argparse(
|
||||
cert_path = os.path.join(E.cfg, "cert.pem")
|
||||
|
||||
fk_salt = get_fk_salt()
|
||||
dk_salt = get_dk_salt()
|
||||
ah_salt = get_ah_salt()
|
||||
|
||||
# alpine peaks at 5 threads for some reason,
|
||||
@@ -1322,9 +1345,10 @@ def run_argparse(
|
||||
add_transcoding(ap)
|
||||
add_ftp(ap)
|
||||
add_webdav(ap)
|
||||
add_tftp(ap)
|
||||
add_smb(ap)
|
||||
add_safety(ap)
|
||||
add_salt(ap, fk_salt, ah_salt)
|
||||
add_salt(ap, fk_salt, dk_salt, ah_salt)
|
||||
add_optouts(ap)
|
||||
add_shutdown(ap)
|
||||
add_yolo(ap)
|
||||
@@ -1375,7 +1399,7 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
if argv is None:
|
||||
argv = sys.argv
|
||||
|
||||
f = '\033[36mcopyparty v{} "\033[35m{}\033[36m" ({})\n{}\033[0;36m\n sqlite v{} | jinja2 v{} | pyftpd v{}\n\033[0m'
|
||||
f = '\033[36mcopyparty v{} "\033[35m{}\033[36m" ({})\n{}\033[0;36m\n sqlite {} | jinja {} | pyftpd {} | tftp {}\n\033[0m'
|
||||
f = f.format(
|
||||
S_VERSION,
|
||||
CODENAME,
|
||||
@@ -1384,6 +1408,7 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
SQLITE_VER,
|
||||
JINJA_VER,
|
||||
PYFTPD_VER,
|
||||
PARTFTPY_VER,
|
||||
)
|
||||
lprint(f)
|
||||
|
||||
@@ -1415,6 +1440,8 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
deprecated: list[tuple[str, str]] = [
|
||||
("--salt", "--warksalt"),
|
||||
("--hdr-au-usr", "--idp-h-usr"),
|
||||
("--idp-h-sep", "--idp-gsep"),
|
||||
("--th-no-crop", "--th-crop=n"),
|
||||
]
|
||||
for dk, nk in deprecated:
|
||||
idx = -1
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (1, 9, 31)
|
||||
CODENAME = "prometheable"
|
||||
BUILD_DT = (2024, 2, 3)
|
||||
VERSION = (1, 13, 0)
|
||||
CODENAME = "race the beam"
|
||||
BUILD_DT = (2024, 4, 20)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
@@ -18,7 +18,6 @@ from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap
|
||||
from .pwhash import PWHash
|
||||
from .util import (
|
||||
IMPLICATIONS,
|
||||
META_NOBOTS,
|
||||
SQLITE_VER,
|
||||
UNPLICATIONS,
|
||||
UTC,
|
||||
@@ -34,6 +33,7 @@ from .util import (
|
||||
uncyg,
|
||||
undot,
|
||||
unhumanize,
|
||||
vsplit,
|
||||
)
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
@@ -61,6 +61,10 @@ BAD_CFG = "invalid config; {}".format(SEE_LOG)
|
||||
SBADCFG = " ({})".format(BAD_CFG)
|
||||
|
||||
|
||||
class CfgEx(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AXS(object):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -193,7 +197,7 @@ class Lim(object):
|
||||
self.dft = int(time.time()) + 300
|
||||
self.dfv = get_df(abspath)[0] or 0
|
||||
for j in list(self.reg.values()) if self.reg else []:
|
||||
self.dfv -= int(j["size"] / len(j["hash"]) * len(j["need"]))
|
||||
self.dfv -= int(j["size"] / (len(j["hash"]) or 999) * len(j["need"]))
|
||||
|
||||
if already_written:
|
||||
sz = 0
|
||||
@@ -551,7 +555,12 @@ class VFS(object):
|
||||
# no vfs nodes in the list of real inodes
|
||||
real = [x for x in real if x[0] not in self.nodes]
|
||||
|
||||
dbv = self.dbv or self
|
||||
for name, vn2 in sorted(self.nodes.items()):
|
||||
if vn2.dbv == dbv and self.flags.get("dk"):
|
||||
virt_vis[name] = vn2
|
||||
continue
|
||||
|
||||
ok = False
|
||||
zx = vn2.axs
|
||||
axs = [zx.uread, zx.uwrite, zx.umove, zx.udel, zx.uget]
|
||||
@@ -780,6 +789,20 @@ class AuthSrv(object):
|
||||
self.line_ctr = 0
|
||||
self.indent = ""
|
||||
|
||||
# fwd-decl
|
||||
self.vfs = VFS(log_func, "", "", AXS(), {})
|
||||
self.acct: dict[str, str] = {}
|
||||
self.iacct: dict[str, str] = {}
|
||||
self.grps: dict[str, list[str]] = {}
|
||||
self.re_pwd: Optional[re.Pattern] = None
|
||||
|
||||
# all volumes observed since last restart
|
||||
self.idp_vols: dict[str, str] = {} # vpath->abspath
|
||||
|
||||
# all users/groups observed since last restart
|
||||
self.idp_accs: dict[str, list[str]] = {} # username->groupnames
|
||||
self.idp_usr_gh: dict[str, str] = {} # username->group-header-value (cache)
|
||||
|
||||
self.mutex = threading.Lock()
|
||||
self.reload()
|
||||
|
||||
@@ -797,6 +820,86 @@ class AuthSrv(object):
|
||||
|
||||
yield prev, True
|
||||
|
||||
def idp_checkin(
|
||||
self, broker: Optional["BrokerCli"], uname: str, gname: str
|
||||
) -> bool:
|
||||
if uname in self.acct:
|
||||
return False
|
||||
|
||||
if self.idp_usr_gh.get(uname) == gname:
|
||||
return False
|
||||
|
||||
gnames = [x.strip() for x in self.args.idp_gsep.split(gname)]
|
||||
gnames.sort()
|
||||
|
||||
with self.mutex:
|
||||
self.idp_usr_gh[uname] = gname
|
||||
if self.idp_accs.get(uname) == gnames:
|
||||
return False
|
||||
|
||||
self.idp_accs[uname] = gnames
|
||||
|
||||
t = "reinitializing due to new user from IdP: [%s:%s]"
|
||||
self.log(t % (uname, gnames), 3)
|
||||
|
||||
if not broker:
|
||||
# only true for tests
|
||||
self._reload()
|
||||
return True
|
||||
|
||||
broker.ask("_reload_blocking", False).get()
|
||||
return True
|
||||
|
||||
def _map_volume_idp(
|
||||
self,
|
||||
src: str,
|
||||
dst: str,
|
||||
mount: dict[str, str],
|
||||
daxs: dict[str, AXS],
|
||||
mflags: dict[str, dict[str, Any]],
|
||||
un_gns: dict[str, list[str]],
|
||||
) -> list[tuple[str, str, str, str]]:
|
||||
ret: list[tuple[str, str, str, str]] = []
|
||||
visited = set()
|
||||
src0 = src # abspath
|
||||
dst0 = dst # vpath
|
||||
|
||||
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
|
||||
un_gn = [("", "")]
|
||||
|
||||
for un, gn in un_gn:
|
||||
# if ap/vp has a user/group placeholder, make sure to keep
|
||||
# track so the same user/gruop is mapped when setting perms;
|
||||
# otherwise clear un/gn to indicate it's a regular volume
|
||||
|
||||
src1 = src0.replace("${u}", un or "\n")
|
||||
dst1 = dst0.replace("${u}", un or "\n")
|
||||
if src0 == src1 and dst0 == dst1:
|
||||
un = ""
|
||||
|
||||
src = src1.replace("${g}", gn or "\n")
|
||||
dst = dst1.replace("${g}", gn or "\n")
|
||||
if src == src1 and dst == dst1:
|
||||
gn = ""
|
||||
|
||||
if "\n" in (src + dst):
|
||||
continue
|
||||
|
||||
label = "%s\n%s" % (src, dst)
|
||||
if label in visited:
|
||||
continue
|
||||
visited.add(label)
|
||||
|
||||
src, dst = self._map_volume(src, dst, mount, daxs, mflags)
|
||||
if src:
|
||||
ret.append((src, dst, un, gn))
|
||||
if un or gn:
|
||||
self.idp_vols[dst] = src
|
||||
|
||||
return ret
|
||||
|
||||
def _map_volume(
|
||||
self,
|
||||
src: str,
|
||||
@@ -804,7 +907,11 @@ class AuthSrv(object):
|
||||
mount: dict[str, str],
|
||||
daxs: dict[str, AXS],
|
||||
mflags: dict[str, dict[str, Any]],
|
||||
) -> None:
|
||||
) -> tuple[str, str]:
|
||||
src = os.path.expandvars(os.path.expanduser(src))
|
||||
src = absreal(src)
|
||||
dst = dst.strip("/")
|
||||
|
||||
if dst in mount:
|
||||
t = "multiple filesystem-paths mounted at [/{}]:\n [{}]\n [{}]"
|
||||
self.log(t.format(dst, mount[dst], src), c=1)
|
||||
@@ -825,6 +932,7 @@ class AuthSrv(object):
|
||||
mount[dst] = src
|
||||
daxs[dst] = AXS()
|
||||
mflags[dst] = {}
|
||||
return (src, dst)
|
||||
|
||||
def _e(self, desc: Optional[str] = None) -> None:
|
||||
if not self.args.vc or not self.line_ctr:
|
||||
@@ -852,31 +960,76 @@ class AuthSrv(object):
|
||||
|
||||
self.log(t.format(self.line_ctr, c, self.indent, ln, desc))
|
||||
|
||||
def _all_un_gn(
|
||||
self,
|
||||
acct: dict[str, str],
|
||||
grps: dict[str, list[str]],
|
||||
) -> dict[str, list[str]]:
|
||||
"""
|
||||
generate list of all confirmed pairs of username/groupname seen since last restart;
|
||||
in case of conflicting group memberships then it is selected as follows:
|
||||
* any non-zero value from IdP group header
|
||||
* otherwise take --grps / [groups]
|
||||
"""
|
||||
ret = {un: gns[:] for un, gns in self.idp_accs.items()}
|
||||
ret.update({zs: [""] for zs in acct if zs not in ret})
|
||||
for gn, uns in grps.items():
|
||||
for un in uns:
|
||||
try:
|
||||
ret[un].append(gn)
|
||||
except:
|
||||
ret[un] = [gn]
|
||||
|
||||
return ret
|
||||
|
||||
def _parse_config_file(
|
||||
self,
|
||||
fp: str,
|
||||
cfg_lines: list[str],
|
||||
acct: dict[str, str],
|
||||
grps: dict[str, list[str]],
|
||||
daxs: dict[str, AXS],
|
||||
mflags: dict[str, dict[str, Any]],
|
||||
mount: dict[str, str],
|
||||
) -> None:
|
||||
self.line_ctr = 0
|
||||
|
||||
expand_config_file(cfg_lines, fp, "")
|
||||
expand_config_file(self.log, cfg_lines, fp, "")
|
||||
if self.args.vc:
|
||||
lns = ["{:4}: {}".format(n, s) for n, s in enumerate(cfg_lines, 1)]
|
||||
self.log("expanded config file (unprocessed):\n" + "\n".join(lns))
|
||||
|
||||
cfg_lines = upgrade_cfg_fmt(self.log, self.args, cfg_lines, fp)
|
||||
|
||||
# due to IdP, volumes must be parsed after users and groups;
|
||||
# do volumes in a 2nd pass to allow arbitrary order in config files
|
||||
for npass in range(1, 3):
|
||||
if self.args.vc:
|
||||
self.log("parsing config files; pass %d/%d" % (npass, 2))
|
||||
self._parse_config_file_2(cfg_lines, acct, grps, daxs, mflags, mount, npass)
|
||||
|
||||
def _parse_config_file_2(
|
||||
self,
|
||||
cfg_lines: list[str],
|
||||
acct: dict[str, str],
|
||||
grps: dict[str, list[str]],
|
||||
daxs: dict[str, AXS],
|
||||
mflags: dict[str, dict[str, Any]],
|
||||
mount: dict[str, str],
|
||||
npass: int,
|
||||
) -> None:
|
||||
self.line_ctr = 0
|
||||
all_un_gn = self._all_un_gn(acct, grps)
|
||||
|
||||
cat = ""
|
||||
catg = "[global]"
|
||||
cata = "[accounts]"
|
||||
catgrp = "[groups]"
|
||||
catx = "accs:"
|
||||
catf = "flags:"
|
||||
ap: Optional[str] = None
|
||||
vp: Optional[str] = None
|
||||
vols: list[tuple[str, str, str, str]] = []
|
||||
for ln in cfg_lines:
|
||||
self.line_ctr += 1
|
||||
ln = ln.split(" #")[0].strip()
|
||||
@@ -889,7 +1042,7 @@ class AuthSrv(object):
|
||||
subsection = ln in (catx, catf)
|
||||
if ln.startswith("[") or subsection:
|
||||
self._e()
|
||||
if ap is None and vp is not None:
|
||||
if npass > 1 and ap is None and vp is not None:
|
||||
t = "the first line after [/{}] must be a filesystem path to share on that volume"
|
||||
raise Exception(t.format(vp))
|
||||
|
||||
@@ -905,6 +1058,8 @@ class AuthSrv(object):
|
||||
self._l(ln, 6, t)
|
||||
elif ln == cata:
|
||||
self._l(ln, 5, "begin user-accounts section")
|
||||
elif ln == catgrp:
|
||||
self._l(ln, 5, "begin user-groups section")
|
||||
elif ln.startswith("[/"):
|
||||
vp = ln[1:-1].strip("/")
|
||||
self._l(ln, 2, "define volume at URL [/{}]".format(vp))
|
||||
@@ -941,15 +1096,39 @@ class AuthSrv(object):
|
||||
raise Exception(t + SBADCFG)
|
||||
continue
|
||||
|
||||
if cat == catgrp:
|
||||
try:
|
||||
gn, zs1 = [zs.strip() for zs in ln.split(":", 1)]
|
||||
uns = [zs.strip() for zs in zs1.split(",")]
|
||||
t = "group [%s] = " % (gn,)
|
||||
t += ", ".join("user [%s]" % (x,) for x in uns)
|
||||
self._l(ln, 5, t)
|
||||
grps[gn] = uns
|
||||
except:
|
||||
t = 'lines inside the [groups] section must be "groupname: user1, user2, user..."'
|
||||
raise Exception(t + SBADCFG)
|
||||
continue
|
||||
|
||||
if vp is not None and ap is None:
|
||||
if npass != 2:
|
||||
continue
|
||||
|
||||
ap = ln
|
||||
ap = os.path.expandvars(os.path.expanduser(ap))
|
||||
ap = absreal(ap)
|
||||
self._l(ln, 2, "bound to filesystem-path [{}]".format(ap))
|
||||
self._map_volume(ap, vp, mount, daxs, mflags)
|
||||
vols = self._map_volume_idp(ap, vp, mount, daxs, mflags, all_un_gn)
|
||||
if not vols:
|
||||
ap = vp = None
|
||||
self._l(ln, 2, "└─no users/groups known; was not mapped")
|
||||
elif len(vols) > 1:
|
||||
for vol in vols:
|
||||
self._l(ln, 2, "└─mapping: [%s] => [%s]" % (vol[1], vol[0]))
|
||||
continue
|
||||
|
||||
if cat == catx:
|
||||
if npass != 2 or not ap:
|
||||
# not stage2, or unmapped ${u}/${g}
|
||||
continue
|
||||
|
||||
err = ""
|
||||
try:
|
||||
self._l(ln, 5, "volume access config:")
|
||||
@@ -960,14 +1139,20 @@ class AuthSrv(object):
|
||||
if " " in re.sub(", *", "", sv).strip():
|
||||
err = "list of users is not comma-separated; "
|
||||
raise Exception(err)
|
||||
assert vp is not None
|
||||
self._read_vol_str(sk, sv.replace(" ", ""), daxs[vp], mflags[vp])
|
||||
sv = sv.replace(" ", "")
|
||||
self._read_vol_str_idp(sk, sv, vols, all_un_gn, daxs, mflags)
|
||||
continue
|
||||
except CfgEx:
|
||||
raise
|
||||
except:
|
||||
err += "accs entries must be 'rwmdgGhaA.: user1, user2, ...'"
|
||||
raise Exception(err + SBADCFG)
|
||||
raise CfgEx(err + SBADCFG)
|
||||
|
||||
if cat == catf:
|
||||
if npass != 2 or not ap:
|
||||
# not stage2, or unmapped ${u}/${g}
|
||||
continue
|
||||
|
||||
err = ""
|
||||
try:
|
||||
self._l(ln, 6, "volume-specific config:")
|
||||
@@ -984,11 +1169,14 @@ class AuthSrv(object):
|
||||
else:
|
||||
fstr += ",{}={}".format(sk, sv)
|
||||
assert vp is not None
|
||||
self._read_vol_str("c", fstr[1:], daxs[vp], mflags[vp])
|
||||
self._read_vol_str_idp(
|
||||
"c", fstr[1:], vols, all_un_gn, daxs, mflags
|
||||
)
|
||||
fstr = ""
|
||||
if fstr:
|
||||
assert vp is not None
|
||||
self._read_vol_str("c", fstr[1:], daxs[vp], mflags[vp])
|
||||
self._read_vol_str_idp(
|
||||
"c", fstr[1:], vols, all_un_gn, daxs, mflags
|
||||
)
|
||||
continue
|
||||
except:
|
||||
err += "flags entries (volflags) must be one of the following:\n 'flag1, flag2, ...'\n 'key: value'\n 'flag1, flag2, key: value'"
|
||||
@@ -999,12 +1187,18 @@ class AuthSrv(object):
|
||||
self._e()
|
||||
self.line_ctr = 0
|
||||
|
||||
def _read_vol_str(
|
||||
self, lvl: str, uname: str, axs: AXS, flags: dict[str, Any]
|
||||
def _read_vol_str_idp(
|
||||
self,
|
||||
lvl: str,
|
||||
uname: str,
|
||||
vols: list[tuple[str, str, str, str]],
|
||||
un_gns: dict[str, list[str]],
|
||||
axs: dict[str, AXS],
|
||||
flags: dict[str, dict[str, Any]],
|
||||
) -> None:
|
||||
if lvl.strip("crwmdgGhaA."):
|
||||
t = "%s,%s" % (lvl, uname) if uname else lvl
|
||||
raise Exception("invalid config value (volume or volflag): %s" % (t,))
|
||||
raise CfgEx("invalid config value (volume or volflag): %s" % (t,))
|
||||
|
||||
if lvl == "c":
|
||||
# here, 'uname' is not a username; it is a volflag name... sorry
|
||||
@@ -1019,16 +1213,62 @@ class AuthSrv(object):
|
||||
while "," in uname:
|
||||
# one or more bools before the final flag; eat them
|
||||
n1, uname = uname.split(",", 1)
|
||||
self._read_volflag(flags, n1, True, False)
|
||||
for _, vp, _, _ in vols:
|
||||
self._read_volflag(flags[vp], n1, True, False)
|
||||
|
||||
for _, vp, _, _ in vols:
|
||||
self._read_volflag(flags[vp], uname, cval, False)
|
||||
|
||||
self._read_volflag(flags, uname, cval, False)
|
||||
return
|
||||
|
||||
if uname == "":
|
||||
uname = "*"
|
||||
|
||||
junkset = set()
|
||||
unames = []
|
||||
for un in uname.replace(",", " ").strip().split():
|
||||
if un.startswith("@"):
|
||||
grp = un[1:]
|
||||
uns = [x[0] for x in un_gns.items() if grp in x[1]]
|
||||
if grp == "${g}":
|
||||
unames.append(un)
|
||||
elif not uns and not self.args.idp_h_grp:
|
||||
t = "group [%s] must be defined with --grp argument (or in a [groups] config section)"
|
||||
raise CfgEx(t % (grp,))
|
||||
|
||||
unames.extend(uns)
|
||||
else:
|
||||
unames.append(un)
|
||||
|
||||
# unames may still contain ${u} and ${g} so now expand those;
|
||||
un_gn = [(un, gn) for un, gns in un_gns.items() for gn in gns]
|
||||
|
||||
for src, dst, vu, vg in vols:
|
||||
unames2 = set(unames)
|
||||
|
||||
if "${u}" in unames:
|
||||
if not vu:
|
||||
t = "cannot use ${u} in accs of volume [%s] because the volume url does not contain ${u}"
|
||||
raise CfgEx(t % (src,))
|
||||
unames2.add(vu)
|
||||
|
||||
if "@${g}" in unames:
|
||||
if not vg:
|
||||
t = "cannot use @${g} in accs of volume [%s] because the volume url does not contain @${g}"
|
||||
raise CfgEx(t % (src,))
|
||||
unames2.update([un for un, gn in un_gn if gn == vg])
|
||||
|
||||
if "${g}" in unames:
|
||||
t = 'the accs of volume [%s] contains "${g}" but the only supported way of specifying that is "@${g}"'
|
||||
raise CfgEx(t % (src,))
|
||||
|
||||
unames2.discard("${u}")
|
||||
unames2.discard("@${g}")
|
||||
|
||||
self._read_vol_str(lvl, list(unames2), axs[dst])
|
||||
|
||||
def _read_vol_str(self, lvl: str, unames: list[str], axs: AXS) -> None:
|
||||
junkset = set()
|
||||
for un in unames:
|
||||
for alias, mapping in [
|
||||
("h", "gh"),
|
||||
("G", "gG"),
|
||||
@@ -1105,12 +1345,18 @@ class AuthSrv(object):
|
||||
then supplementing with config files
|
||||
before finally building the VFS
|
||||
"""
|
||||
with self.mutex:
|
||||
self._reload()
|
||||
|
||||
def _reload(self) -> None:
|
||||
acct: dict[str, str] = {} # username:password
|
||||
grps: dict[str, list[str]] = {} # groupname:usernames
|
||||
daxs: dict[str, AXS] = {}
|
||||
mflags: dict[str, dict[str, Any]] = {} # moutpoint:flags
|
||||
mount: dict[str, str] = {} # dst:src (mountpoint:realpath)
|
||||
|
||||
self.idp_vols = {} # yolo
|
||||
|
||||
if self.args.a:
|
||||
# list of username:password
|
||||
for x in self.args.a:
|
||||
@@ -1121,9 +1367,22 @@ class AuthSrv(object):
|
||||
t = '\n invalid value "{}" for argument -a, must be username:password'
|
||||
raise Exception(t.format(x))
|
||||
|
||||
if self.args.grp:
|
||||
# list of groupname:username,username,...
|
||||
for x in self.args.grp:
|
||||
try:
|
||||
# accept both = and : as separator between groupname and usernames,
|
||||
# accept both , and : as separators between usernames
|
||||
zs1, zs2 = x.replace("=", ":").split(":", 1)
|
||||
grps[zs1] = zs2.replace(":", ",").split(",")
|
||||
except:
|
||||
t = '\n invalid value "{}" for argument --grp, must be groupname:username1,username2,...'
|
||||
raise Exception(t.format(x))
|
||||
|
||||
if self.args.v:
|
||||
# list of src:dst:permset:permset:...
|
||||
# permset is <rwmdgGhaA.>[,username][,username] or <c>,<flag>[=args]
|
||||
all_un_gn = self._all_un_gn(acct, grps)
|
||||
for v_str in self.args.v:
|
||||
m = re_vol.match(v_str)
|
||||
if not m:
|
||||
@@ -1133,20 +1392,19 @@ class AuthSrv(object):
|
||||
if WINDOWS:
|
||||
src = uncyg(src)
|
||||
|
||||
# print("\n".join([src, dst, perms]))
|
||||
src = absreal(src)
|
||||
dst = dst.strip("/")
|
||||
self._map_volume(src, dst, mount, daxs, mflags)
|
||||
vols = self._map_volume_idp(src, dst, mount, daxs, mflags, all_un_gn)
|
||||
|
||||
for x in perms.split(":"):
|
||||
lvl, uname = x.split(",", 1) if "," in x else [x, ""]
|
||||
self._read_vol_str(lvl, uname, daxs[dst], mflags[dst])
|
||||
self._read_vol_str_idp(lvl, uname, vols, all_un_gn, daxs, mflags)
|
||||
|
||||
if self.args.c:
|
||||
for cfg_fn in self.args.c:
|
||||
lns: list[str] = []
|
||||
try:
|
||||
self._parse_config_file(cfg_fn, lns, acct, daxs, mflags, mount)
|
||||
self._parse_config_file(
|
||||
cfg_fn, lns, acct, grps, daxs, mflags, mount
|
||||
)
|
||||
|
||||
zs = "#\033[36m cfg files in "
|
||||
zst = [x[len(zs) :] for x in lns if x.startswith(zs)]
|
||||
@@ -1177,7 +1435,7 @@ class AuthSrv(object):
|
||||
|
||||
mount = cased
|
||||
|
||||
if not mount:
|
||||
if not mount and not self.args.idp_h_usr:
|
||||
# -h says our defaults are CWD at root and read/write for everyone
|
||||
axs = AXS(["*"], ["*"], None, None)
|
||||
vfs = VFS(self.log_func, absreal("."), "", axs, {})
|
||||
@@ -1213,9 +1471,13 @@ class AuthSrv(object):
|
||||
vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
|
||||
vol.root = vfs
|
||||
|
||||
zss = set(acct)
|
||||
zss.update(self.idp_accs)
|
||||
zss.discard("*")
|
||||
unames = ["*"] + list(sorted(zss))
|
||||
|
||||
for perm in "read write move del get pget html admin dot".split():
|
||||
axs_key = "u" + perm
|
||||
unames = ["*"] + list(acct.keys())
|
||||
for vp, vol in vfs.all_vols.items():
|
||||
zx = getattr(vol.axs, axs_key)
|
||||
if "*" in zx:
|
||||
@@ -1249,18 +1511,20 @@ class AuthSrv(object):
|
||||
]:
|
||||
for usr in d:
|
||||
all_users[usr] = 1
|
||||
if usr != "*" and usr not in acct:
|
||||
if usr != "*" and usr not in acct and usr not in self.idp_accs:
|
||||
missing_users[usr] = 1
|
||||
if "*" not in d:
|
||||
associated_users[usr] = 1
|
||||
|
||||
if missing_users:
|
||||
self.log(
|
||||
"you must -a the following users: "
|
||||
+ ", ".join(k for k in sorted(missing_users)),
|
||||
c=1,
|
||||
)
|
||||
raise Exception(BAD_CFG)
|
||||
zs = ", ".join(k for k in sorted(missing_users))
|
||||
if self.args.idp_h_usr:
|
||||
t = "the following users are unknown, and assumed to come from IdP: "
|
||||
self.log(t + zs, c=6)
|
||||
else:
|
||||
t = "you must -a the following users: "
|
||||
self.log(t + zs, c=1)
|
||||
raise Exception(BAD_CFG)
|
||||
|
||||
if LEELOO_DALLAS in all_users:
|
||||
raise Exception("sorry, reserved username: " + LEELOO_DALLAS)
|
||||
@@ -1401,13 +1665,6 @@ class AuthSrv(object):
|
||||
if not vol.flags.get("robots"):
|
||||
vol.flags["norobots"] = True
|
||||
|
||||
for vol in vfs.all_vols.values():
|
||||
h = [vol.flags.get("html_head", self.args.html_head)]
|
||||
if vol.flags.get("norobots"):
|
||||
h.insert(0, META_NOBOTS)
|
||||
|
||||
vol.flags["html_head"] = "\n".join([x for x in h if x])
|
||||
|
||||
for vol in vfs.all_vols.values():
|
||||
if self.args.no_vthumb:
|
||||
vol.flags["dvthumb"] = True
|
||||
@@ -1429,6 +1686,20 @@ class AuthSrv(object):
|
||||
vol.flags["fk"] = int(fk) if fk is not True else 8
|
||||
have_fk = True
|
||||
|
||||
dk = vol.flags.get("dk")
|
||||
dks = vol.flags.get("dks")
|
||||
dky = vol.flags.get("dky")
|
||||
if dks is not None and dky is not None:
|
||||
t = "WARNING: volume /%s has both dks and dky enabled; this is too yolo and not permitted"
|
||||
raise Exception(t % (vol.vpath,))
|
||||
|
||||
if dks and not dk:
|
||||
dk = dks
|
||||
if dky and not dk:
|
||||
dk = dky
|
||||
if dk:
|
||||
vol.flags["dk"] = int(dk) if dk is not True else 8
|
||||
|
||||
if have_fk and re.match(r"^[0-9\.]+$", self.args.fk_salt):
|
||||
self.log("filekey salt: {}".format(self.args.fk_salt))
|
||||
|
||||
@@ -1485,7 +1756,7 @@ class AuthSrv(object):
|
||||
if k not in vol.flags:
|
||||
vol.flags[k] = getattr(self.args, k)
|
||||
|
||||
for k in ("nrand",):
|
||||
for k in ("nrand", "u2abort"):
|
||||
if k in vol.flags:
|
||||
vol.flags[k] = int(vol.flags[k])
|
||||
|
||||
@@ -1493,13 +1764,14 @@ class AuthSrv(object):
|
||||
if k in vol.flags:
|
||||
vol.flags[k] = float(vol.flags[k])
|
||||
|
||||
try:
|
||||
zs1, zs2 = vol.flags["rm_retry"].split("/")
|
||||
vol.flags["rm_re_t"] = float(zs1)
|
||||
vol.flags["rm_re_r"] = float(zs2)
|
||||
except:
|
||||
t = 'volume "/%s" has invalid rm_retry [%s]'
|
||||
raise Exception(t % (vol.vpath, vol.flags.get("rm_retry")))
|
||||
for k in ("mv_re", "rm_re"):
|
||||
try:
|
||||
zs1, zs2 = vol.flags[k + "try"].split("/")
|
||||
vol.flags[k + "_t"] = float(zs1)
|
||||
vol.flags[k + "_r"] = float(zs2)
|
||||
except:
|
||||
t = 'volume "/%s" has invalid %stry [%s]'
|
||||
raise Exception(t % (vol.vpath, k, vol.flags.get(k + "try")))
|
||||
|
||||
for k1, k2 in IMPLICATIONS:
|
||||
if k1 in vol.flags:
|
||||
@@ -1749,20 +2021,37 @@ class AuthSrv(object):
|
||||
except Pebkac:
|
||||
self.warn_anonwrite = True
|
||||
|
||||
with self.mutex:
|
||||
self.vfs = vfs
|
||||
self.acct = acct
|
||||
self.iacct = {v: k for k, v in acct.items()}
|
||||
idp_err = "WARNING! The following IdP volumes are mounted directly below another volume where anonymous users can read and/or write files. This is a SECURITY HAZARD!! When copyparty is restarted, it will not know about these IdP volumes yet. These volumes will then be accessible by anonymous users UNTIL one of the users associated with their volume sends a request to the server. RECOMMENDATION: You should create a restricted volume where nobody can read/write files, and make sure that all IdP volumes are configured to appear somewhere below that volume."
|
||||
for idp_vp in self.idp_vols:
|
||||
parent_vp = vsplit(idp_vp)[0]
|
||||
vn, _ = vfs.get(parent_vp, "*", False, False)
|
||||
zs = (
|
||||
"READABLE"
|
||||
if "*" in vn.axs.uread
|
||||
else "WRITABLE"
|
||||
if "*" in vn.axs.uwrite
|
||||
else ""
|
||||
)
|
||||
if zs:
|
||||
t = '\nWARNING: Volume "/%s" appears below "/%s" and would be WORLD-%s'
|
||||
idp_err += t % (idp_vp, vn.vpath, zs)
|
||||
if "\n" in idp_err:
|
||||
self.log(idp_err, 1)
|
||||
|
||||
self.re_pwd = None
|
||||
pwds = [re.escape(x) for x in self.iacct.keys()]
|
||||
if pwds:
|
||||
if self.ah.on:
|
||||
zs = r"(\[H\] pw:.*|[?&]pw=)([^&]+)"
|
||||
else:
|
||||
zs = r"(\[H\] pw:.*|=)(" + "|".join(pwds) + r")([]&; ]|$)"
|
||||
self.vfs = vfs
|
||||
self.acct = acct
|
||||
self.grps = grps
|
||||
self.iacct = {v: k for k, v in acct.items()}
|
||||
|
||||
self.re_pwd = re.compile(zs)
|
||||
self.re_pwd = None
|
||||
pwds = [re.escape(x) for x in self.iacct.keys()]
|
||||
if pwds:
|
||||
if self.ah.on:
|
||||
zs = r"(\[H\] pw:.*|[?&]pw=)([^&]+)"
|
||||
else:
|
||||
zs = r"(\[H\] pw:.*|=)(" + "|".join(pwds) + r")([]&; ]|$)"
|
||||
|
||||
self.re_pwd = re.compile(zs)
|
||||
|
||||
def setup_pwhash(self, acct: dict[str, str]) -> None:
|
||||
self.ah = PWHash(self.args)
|
||||
@@ -2004,6 +2293,12 @@ class AuthSrv(object):
|
||||
ret.append(" {}: {}".format(u, p))
|
||||
ret.append("")
|
||||
|
||||
if self.grps:
|
||||
ret.append("[groups]")
|
||||
for gn, uns in self.grps.items():
|
||||
ret.append(" %s: %s" % (gn, ", ".join(uns)))
|
||||
ret.append("")
|
||||
|
||||
for vol in self.vfs.all_vols.values():
|
||||
ret.append("[/{}]".format(vol.vpath))
|
||||
ret.append(" " + vol.realpath)
|
||||
@@ -2101,27 +2396,50 @@ def split_cfg_ln(ln: str) -> dict[str, Any]:
|
||||
return ret
|
||||
|
||||
|
||||
def expand_config_file(ret: list[str], fp: str, ipath: str) -> None:
|
||||
def expand_config_file(
|
||||
log: Optional["NamedLogger"], ret: list[str], fp: str, ipath: str
|
||||
) -> None:
|
||||
"""expand all % file includes"""
|
||||
fp = absreal(fp)
|
||||
if len(ipath.split(" -> ")) > 64:
|
||||
raise Exception("hit max depth of 64 includes")
|
||||
|
||||
if os.path.isdir(fp):
|
||||
names = os.listdir(fp)
|
||||
crumb = "#\033[36m cfg files in {} => {}\033[0m".format(fp, names)
|
||||
ret.append(crumb)
|
||||
for fn in sorted(names):
|
||||
names = list(sorted(os.listdir(fp)))
|
||||
cnames = [x for x in names if x.lower().endswith(".conf")]
|
||||
if not cnames:
|
||||
t = "warning: tried to read config-files from folder '%s' but it does not contain any "
|
||||
if names:
|
||||
t += ".conf files; the following files were ignored: %s"
|
||||
t = t % (fp, ", ".join(names[:8]))
|
||||
else:
|
||||
t += "files at all"
|
||||
t = t % (fp,)
|
||||
|
||||
if log:
|
||||
log(t, 3)
|
||||
|
||||
ret.append("#\033[33m %s\033[0m" % (t,))
|
||||
else:
|
||||
zs = "#\033[36m cfg files in %s => %s\033[0m" % (fp, cnames)
|
||||
ret.append(zs)
|
||||
|
||||
for fn in cnames:
|
||||
fp2 = os.path.join(fp, fn)
|
||||
if not fp2.endswith(".conf") or fp2 in ipath:
|
||||
if fp2 in ipath:
|
||||
continue
|
||||
|
||||
expand_config_file(ret, fp2, ipath)
|
||||
expand_config_file(log, ret, fp2, ipath)
|
||||
|
||||
if ret[-1] == crumb:
|
||||
# no config files below; remove breadcrumb
|
||||
ret.pop()
|
||||
return
|
||||
|
||||
if not os.path.exists(fp):
|
||||
t = "warning: tried to read config from '%s' but the file/folder does not exist"
|
||||
t = t % (fp,)
|
||||
if log:
|
||||
log(t, 3)
|
||||
|
||||
ret.append("#\033[31m %s\033[0m" % (t,))
|
||||
return
|
||||
|
||||
ipath += " -> " + fp
|
||||
@@ -2135,7 +2453,7 @@ def expand_config_file(ret: list[str], fp: str, ipath: str) -> None:
|
||||
fp2 = ln[1:].strip()
|
||||
fp2 = os.path.join(os.path.dirname(fp), fp2)
|
||||
ofs = len(ret)
|
||||
expand_config_file(ret, fp2, ipath)
|
||||
expand_config_file(log, ret, fp2, ipath)
|
||||
for n in range(ofs, len(ret)):
|
||||
ret[n] = pad + ret[n]
|
||||
continue
|
||||
|
||||
@@ -6,7 +6,8 @@ import os
|
||||
import shutil
|
||||
import time
|
||||
|
||||
from .util import Netdev, runcmd
|
||||
from .__init__ import ANYWIN
|
||||
from .util import Netdev, runcmd, wrename, wunlink
|
||||
|
||||
HAVE_CFSSL = True
|
||||
|
||||
@@ -14,6 +15,12 @@ if True: # pylint: disable=using-constant-test
|
||||
from .util import RootLogger
|
||||
|
||||
|
||||
if ANYWIN:
|
||||
VF = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1}
|
||||
else:
|
||||
VF = {"mv_re_t": 0, "rm_re_t": 0}
|
||||
|
||||
|
||||
def ensure_cert(log: "RootLogger", args) -> None:
|
||||
"""
|
||||
the default cert (and the entire TLS support) is only here to enable the
|
||||
@@ -105,8 +112,12 @@ def _gen_ca(log: "RootLogger", args):
|
||||
raise Exception("failed to translate ca-cert: {}, {}".format(rc, se), 3)
|
||||
|
||||
bname = os.path.join(args.crt_dir, "ca")
|
||||
os.rename(bname + "-key.pem", bname + ".key")
|
||||
os.unlink(bname + ".csr")
|
||||
try:
|
||||
wunlink(log, bname + ".key", VF)
|
||||
except:
|
||||
pass
|
||||
wrename(log, bname + "-key.pem", bname + ".key", VF)
|
||||
wunlink(log, bname + ".csr", VF)
|
||||
|
||||
log("cert", "new ca OK", 2)
|
||||
|
||||
@@ -185,11 +196,11 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
||||
|
||||
bname = os.path.join(args.crt_dir, "srv")
|
||||
try:
|
||||
os.unlink(bname + ".key")
|
||||
wunlink(log, bname + ".key", VF)
|
||||
except:
|
||||
pass
|
||||
os.rename(bname + "-key.pem", bname + ".key")
|
||||
os.unlink(bname + ".csr")
|
||||
wrename(log, bname + "-key.pem", bname + ".key", VF)
|
||||
wunlink(log, bname + ".csr", VF)
|
||||
|
||||
with open(os.path.join(args.crt_dir, "ca.pem"), "rb") as f:
|
||||
ca = f.read()
|
||||
|
||||
@@ -16,11 +16,11 @@ def vf_bmap() -> dict[str, str]:
|
||||
"no_dedup": "copydupes",
|
||||
"no_dupe": "nodupe",
|
||||
"no_forget": "noforget",
|
||||
"no_pipe": "nopipe",
|
||||
"no_robots": "norobots",
|
||||
"no_thumb": "dthumb",
|
||||
"no_vthumb": "dvthumb",
|
||||
"no_athumb": "dathumb",
|
||||
"th_no_crop": "nocrop",
|
||||
}
|
||||
for k in (
|
||||
"dotsrch",
|
||||
@@ -56,15 +56,19 @@ def vf_vmap() -> dict[str, str]:
|
||||
"re_maxage": "scan",
|
||||
"th_convt": "convt",
|
||||
"th_size": "thsize",
|
||||
"th_crop": "crop",
|
||||
"th_x3": "th3x",
|
||||
}
|
||||
for k in (
|
||||
"dbd",
|
||||
"lg_sbf",
|
||||
"md_sbf",
|
||||
"nrand",
|
||||
"mv_retry",
|
||||
"rm_retry",
|
||||
"sort",
|
||||
"unlist",
|
||||
"u2abort",
|
||||
"u2ts",
|
||||
):
|
||||
ret[k] = k
|
||||
@@ -115,6 +119,7 @@ flagcats = {
|
||||
"hardlink": "does dedup with hardlinks instead of symlinks",
|
||||
"neversymlink": "disables symlink fallback; full copy instead",
|
||||
"copydupes": "disables dedup, always saves full copies of dupes",
|
||||
"sparse": "force use of sparse files, mainly for s3-backed storage",
|
||||
"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",
|
||||
@@ -129,6 +134,7 @@ flagcats = {
|
||||
"rand": "force randomized filenames, 9 chars long by default",
|
||||
"nrand=N": "randomized filenames are N chars long",
|
||||
"u2ts=fc": "[f]orce [c]lient-last-modified or [u]pload-time",
|
||||
"u2abort=1": "allow aborting unfinished uploads? 0=no 1=strict 2=ip-chk 3=acct-chk",
|
||||
"sz=1k-3m": "allow filesizes between 1 KiB and 3MiB",
|
||||
"df=1g": "ensure 1 GiB free disk space",
|
||||
},
|
||||
@@ -172,7 +178,8 @@ flagcats = {
|
||||
"dathumb": "disables audio thumbnails (spectrograms)",
|
||||
"dithumb": "disables image thumbnails",
|
||||
"thsize": "thumbnail res; WxH",
|
||||
"nocrop": "disable center-cropping by default",
|
||||
"crop": "center-cropping (y/n/fy/fn)",
|
||||
"th3x": "3x resolution (y/n/fy/fn)",
|
||||
"convt": "conversion timeout in seconds",
|
||||
},
|
||||
"handlers\n(better explained in --help-handlers)": {
|
||||
@@ -209,6 +216,7 @@ flagcats = {
|
||||
"dots": "allow all users with read-access to\nenable the option to show dotfiles in listings",
|
||||
"fk=8": 'generates per-file accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes',
|
||||
"fka=8": 'generates slightly weaker per-file accesskeys,\nwhich are then required at the "g" permission;\nnot affected by filesize or inode numbers',
|
||||
"mv_retry": "ms-windows: timeout for renaming busy files",
|
||||
"rm_retry": "ms-windows: timeout for deleting busy files",
|
||||
"davauth": "ask webdav clients to login for all folders",
|
||||
"davrt": "show lastmod time of symlink destination, not the link itself\n(note: this option is always enabled for recursive listings)",
|
||||
|
||||
@@ -20,6 +20,7 @@ from .authsrv import VFS
|
||||
from .bos import bos
|
||||
from .util import (
|
||||
Daemon,
|
||||
ODict,
|
||||
Pebkac,
|
||||
exclude_dotfiles,
|
||||
fsenc,
|
||||
@@ -217,7 +218,7 @@ class FtpFs(AbstractedFS):
|
||||
raise FSE("Cannot open existing file for writing")
|
||||
|
||||
self.validpath(ap)
|
||||
return open(fsenc(ap), mode)
|
||||
return open(fsenc(ap), mode, self.args.iobuf)
|
||||
|
||||
def chdir(self, path: str) -> None:
|
||||
nwd = join(self.cwd, path)
|
||||
@@ -299,7 +300,7 @@ class FtpFs(AbstractedFS):
|
||||
|
||||
vp = join(self.cwd, path).lstrip("/")
|
||||
try:
|
||||
self.hub.up2k.handle_rm(self.uname, self.h.cli_ip, [vp], [], False)
|
||||
self.hub.up2k.handle_rm(self.uname, self.h.cli_ip, [vp], [], False, False)
|
||||
except Exception as ex:
|
||||
raise FSE(str(ex))
|
||||
|
||||
@@ -409,7 +410,7 @@ class FtpHandler(FTPHandler):
|
||||
if cip.startswith("::ffff:"):
|
||||
cip = cip[7:]
|
||||
|
||||
if self.args.ftp_ipa_re and not self.args.ftp_ipa_re.match(cip):
|
||||
if self.args.ftp_ipa_nm and not self.args.ftp_ipa_nm.map(cip):
|
||||
logging.warning("client rejected (--ftp-ipa): %s", cip)
|
||||
self.connected = False
|
||||
conn.close()
|
||||
@@ -545,6 +546,8 @@ class Ftpd(object):
|
||||
if self.args.ftp4:
|
||||
ips = [x for x in ips if ":" not in x]
|
||||
|
||||
ips = list(ODict.fromkeys(ips)) # dedup
|
||||
|
||||
ioloop = IOLoop()
|
||||
for ip in ips:
|
||||
for h, lp in hs:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@ from .mtag import HAVE_FFMPEG
|
||||
from .th_cli import ThumbCli
|
||||
from .th_srv import HAVE_PIL, HAVE_VIPS
|
||||
from .u2idx import U2idx
|
||||
from .util import HMaccas, shut_socket
|
||||
from .util import HMaccas, NetMap, shut_socket
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Optional, Pattern, Union
|
||||
@@ -50,11 +50,15 @@ class HttpConn(object):
|
||||
self.addr = addr
|
||||
self.hsrv = hsrv
|
||||
|
||||
self.mutex: threading.Lock = hsrv.mutex # mypy404
|
||||
self.u2mutex: threading.Lock = hsrv.u2mutex # mypy404
|
||||
self.args: argparse.Namespace = hsrv.args # mypy404
|
||||
self.E: EnvParams = self.args.E
|
||||
self.asrv: AuthSrv = hsrv.asrv # mypy404
|
||||
self.u2fh: Util.FHC = hsrv.u2fh # mypy404
|
||||
self.pipes: Util.CachedDict = hsrv.pipes # mypy404
|
||||
self.ipa_nm: Optional[NetMap] = hsrv.ipa_nm
|
||||
self.xff_nm: Optional[NetMap] = hsrv.xff_nm
|
||||
self.xff_lan: NetMap = hsrv.xff_lan # type: ignore
|
||||
self.iphash: HMaccas = hsrv.broker.iphash
|
||||
self.bans: dict[str, int] = hsrv.bans
|
||||
self.aclose: dict[str, int] = hsrv.aclose
|
||||
|
||||
@@ -61,12 +61,14 @@ from .u2idx import U2idx
|
||||
from .util import (
|
||||
E_SCK,
|
||||
FHC,
|
||||
CachedDict,
|
||||
Daemon,
|
||||
Garda,
|
||||
Magician,
|
||||
Netdev,
|
||||
NetMap,
|
||||
absreal,
|
||||
build_netmap,
|
||||
ipnorm,
|
||||
min_ex,
|
||||
shut_socket,
|
||||
@@ -103,7 +105,7 @@ class HttpSrv(object):
|
||||
self.t0 = time.time()
|
||||
nsuf = "-n{}-i{:x}".format(nid, os.getpid()) if nid else ""
|
||||
self.magician = Magician()
|
||||
self.nm = NetMap([], {})
|
||||
self.nm = NetMap([], [])
|
||||
self.ssdp: Optional["SSDPr"] = None
|
||||
self.gpwd = Garda(self.args.ban_pw)
|
||||
self.g404 = Garda(self.args.ban_404)
|
||||
@@ -117,6 +119,7 @@ class HttpSrv(object):
|
||||
self.bound: set[tuple[str, int]] = set()
|
||||
self.name = "hsrv" + nsuf
|
||||
self.mutex = threading.Lock()
|
||||
self.u2mutex = threading.Lock()
|
||||
self.stopping = False
|
||||
|
||||
self.tp_nthr = 0 # actual
|
||||
@@ -128,6 +131,7 @@ class HttpSrv(object):
|
||||
self.t_periodic: Optional[threading.Thread] = None
|
||||
|
||||
self.u2fh = FHC()
|
||||
self.pipes = CachedDict(0.2)
|
||||
self.metrics = Metrics(self)
|
||||
self.nreq = 0
|
||||
self.nsus = 0
|
||||
@@ -149,6 +153,10 @@ class HttpSrv(object):
|
||||
zs = os.path.join(self.E.mod, "web", "deps", "prism.js.gz")
|
||||
self.prism = os.path.exists(zs)
|
||||
|
||||
self.ipa_nm = build_netmap(self.args.ipa)
|
||||
self.xff_nm = build_netmap(self.args.xff_src)
|
||||
self.xff_lan = build_netmap("lan")
|
||||
|
||||
self.statics: set[str] = set()
|
||||
self._build_statics()
|
||||
|
||||
@@ -190,7 +198,7 @@ class HttpSrv(object):
|
||||
for fn in df:
|
||||
ap = absreal(os.path.join(dp, fn))
|
||||
self.statics.add(ap)
|
||||
if ap.endswith(".gz") or ap.endswith(".br"):
|
||||
if ap.endswith(".gz"):
|
||||
self.statics.add(ap[:-3])
|
||||
|
||||
def set_netdevs(self, netdevs: dict[str, Netdev]) -> None:
|
||||
@@ -198,7 +206,7 @@ class HttpSrv(object):
|
||||
for ip, _ in self.bound:
|
||||
ips.add(ip)
|
||||
|
||||
self.nm = NetMap(list(ips), netdevs)
|
||||
self.nm = NetMap(list(ips), list(netdevs))
|
||||
|
||||
def start_threads(self, n: int) -> None:
|
||||
self.tp_nthr += n
|
||||
@@ -220,7 +228,7 @@ class HttpSrv(object):
|
||||
def periodic(self) -> None:
|
||||
while True:
|
||||
time.sleep(2 if self.tp_ncli or self.ncli else 10)
|
||||
with self.mutex:
|
||||
with self.u2mutex, self.mutex:
|
||||
self.u2fh.clean()
|
||||
if self.tp_q:
|
||||
self.tp_ncli = max(self.ncli, self.tp_ncli - 2)
|
||||
|
||||
@@ -8,7 +8,7 @@ import re
|
||||
|
||||
from .__init__ import PY2
|
||||
from .th_srv import HAVE_PIL, HAVE_PILF
|
||||
from .util import BytesIO # type: ignore
|
||||
from .util import BytesIO, html_escape # type: ignore
|
||||
|
||||
|
||||
class Ico(object):
|
||||
@@ -31,10 +31,9 @@ class Ico(object):
|
||||
|
||||
w = 100
|
||||
h = 30
|
||||
if not self.args.th_no_crop and as_thumb:
|
||||
if as_thumb:
|
||||
sw, sh = self.args.th_size.split("x")
|
||||
h = int(100.0 / (float(sw) / float(sh)))
|
||||
w = 100
|
||||
|
||||
if chrome:
|
||||
# cannot handle more than ~2000 unique SVGs
|
||||
@@ -99,6 +98,6 @@ class Ico(object):
|
||||
fill="#{}" font-family="monospace" font-size="14px" style="letter-spacing:.5px">{}</text>
|
||||
</g></svg>
|
||||
"""
|
||||
svg = svg.format(h, c[:6], c[6:], ext)
|
||||
svg = svg.format(h, c[:6], c[6:], html_escape(ext, True))
|
||||
|
||||
return "image/svg+xml", svg.encode("utf-8")
|
||||
|
||||
@@ -206,6 +206,9 @@ class Metrics(object):
|
||||
try:
|
||||
x = self.hsrv.broker.ask("up2k.get_unfinished")
|
||||
xs = x.get()
|
||||
if not xs:
|
||||
raise Exception("up2k mutex acquisition timed out")
|
||||
|
||||
xj = json.loads(xs)
|
||||
for ptop, (nbytes, nfiles) in xj.items():
|
||||
tnbytes += nbytes
|
||||
|
||||
@@ -551,8 +551,7 @@ class MTag(object):
|
||||
pypath = str(os.pathsep.join(zsl))
|
||||
env["PYTHONPATH"] = pypath
|
||||
except:
|
||||
if not E.ox and not EXE:
|
||||
raise
|
||||
raise # might be expected outside cpython
|
||||
|
||||
ret: dict[str, Any] = {}
|
||||
for tagname, parser in sorted(parsers.items(), key=lambda x: (x[1].pri, x[0])):
|
||||
|
||||
@@ -110,7 +110,7 @@ class MCast(object):
|
||||
)
|
||||
|
||||
ips = [x for x in ips if x not in ("::1", "127.0.0.1")]
|
||||
ips = find_prefix(ips, netdevs)
|
||||
ips = find_prefix(ips, list(netdevs))
|
||||
|
||||
on = self.on[:]
|
||||
off = self.off[:]
|
||||
@@ -206,6 +206,7 @@ class MCast(object):
|
||||
except:
|
||||
t = "announce failed on {} [{}]:\n{}"
|
||||
self.log(t.format(netdev, ip, min_ex()), 3)
|
||||
sck.close()
|
||||
|
||||
if self.args.zm_msub:
|
||||
for s1 in self.srv.values():
|
||||
|
||||
@@ -340,7 +340,7 @@ class SMB(object):
|
||||
yeet("blocked delete (no-del-acc): " + vpath)
|
||||
|
||||
vpath = vpath.replace("\\", "/").lstrip("/")
|
||||
self.hub.up2k.handle_rm(uname, "1.7.6.2", [vpath], [], False)
|
||||
self.hub.up2k.handle_rm(uname, "1.7.6.2", [vpath], [], False, False)
|
||||
|
||||
def _utime(self, vpath: str, times: tuple[float, float]) -> None:
|
||||
if not self.args.smbw:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import stat
|
||||
import tarfile
|
||||
@@ -44,11 +45,12 @@ class StreamTar(StreamArc):
|
||||
def __init__(
|
||||
self,
|
||||
log: "NamedLogger",
|
||||
args: argparse.Namespace,
|
||||
fgen: Generator[dict[str, Any], None, None],
|
||||
cmp: str = "",
|
||||
**kwargs: Any
|
||||
):
|
||||
super(StreamTar, self).__init__(log, fgen)
|
||||
super(StreamTar, self).__init__(log, args, fgen)
|
||||
|
||||
self.ci = 0
|
||||
self.co = 0
|
||||
@@ -126,7 +128,7 @@ class StreamTar(StreamArc):
|
||||
inf.gid = 0
|
||||
|
||||
self.ci += inf.size
|
||||
with open(fsenc(src), "rb", 512 * 1024) as fo:
|
||||
with open(fsenc(src), "rb", self.args.iobuf) as fo:
|
||||
self.tar.addfile(inf, fo)
|
||||
|
||||
def _gen(self) -> None:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
@@ -20,10 +21,12 @@ class StreamArc(object):
|
||||
def __init__(
|
||||
self,
|
||||
log: "NamedLogger",
|
||||
args: argparse.Namespace,
|
||||
fgen: Generator[dict[str, Any], None, None],
|
||||
**kwargs: Any
|
||||
):
|
||||
self.log = log
|
||||
self.args = args
|
||||
self.fgen = fgen
|
||||
self.stopped = False
|
||||
|
||||
@@ -78,7 +81,9 @@ def enthumb(
|
||||
) -> dict[str, Any]:
|
||||
rem = f["vp"]
|
||||
ext = rem.rsplit(".", 1)[-1].lower()
|
||||
if fmt == "opus" and ext in "aac|m4a|mp3|ogg|opus|wma".split("|"):
|
||||
if (fmt == "mp3" and ext == "mp3") or (
|
||||
fmt == "opus" and ext in "aac|m4a|mp3|ogg|opus|wma".split("|")
|
||||
):
|
||||
raise Exception()
|
||||
|
||||
vp = vjoin(vtop, rem.split("/", 1)[1])
|
||||
|
||||
@@ -28,7 +28,7 @@ if True: # pylint: disable=using-constant-test
|
||||
import typing
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from .__init__ import ANYWIN, EXE, MACOS, TYPE_CHECKING, EnvParams, unicode
|
||||
from .__init__ import ANYWIN, EXE, MACOS, TYPE_CHECKING, E, EnvParams, unicode
|
||||
from .authsrv import BAD_CFG, AuthSrv
|
||||
from .cert import ensure_cert
|
||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE
|
||||
@@ -49,6 +49,7 @@ from .util import (
|
||||
ODict,
|
||||
alltrace,
|
||||
ansi_re,
|
||||
build_netmap,
|
||||
min_ex,
|
||||
mp,
|
||||
odfusion,
|
||||
@@ -94,7 +95,7 @@ class SvcHub(object):
|
||||
self.stopping = False
|
||||
self.stopped = False
|
||||
self.reload_req = False
|
||||
self.reloading = False
|
||||
self.reloading = 0
|
||||
self.stop_cond = threading.Condition()
|
||||
self.nsigs = 3
|
||||
self.retcode = 0
|
||||
@@ -133,7 +134,7 @@ class SvcHub(object):
|
||||
if not self._process_config():
|
||||
raise Exception(BAD_CFG)
|
||||
|
||||
# for non-http clients (ftp)
|
||||
# for non-http clients (ftp, tftp)
|
||||
self.bans: dict[str, int] = {}
|
||||
self.gpwd = Garda(self.args.ban_pw)
|
||||
self.g404 = Garda(self.args.ban_404)
|
||||
@@ -154,6 +155,8 @@ class SvcHub(object):
|
||||
lg.handlers = [lh]
|
||||
lg.setLevel(logging.DEBUG)
|
||||
|
||||
self._check_env()
|
||||
|
||||
if args.stackmon:
|
||||
start_stackmon(args.stackmon, 0)
|
||||
|
||||
@@ -170,6 +173,26 @@ class SvcHub(object):
|
||||
self.log("root", t.format(args.j), c=3)
|
||||
args.no_fpool = True
|
||||
|
||||
for name, arg in (
|
||||
("iobuf", "iobuf"),
|
||||
("s-rd-sz", "s_rd_sz"),
|
||||
("s-wr-sz", "s_wr_sz"),
|
||||
):
|
||||
zi = getattr(args, arg)
|
||||
if zi < 32768:
|
||||
t = "WARNING: expect very poor performance because you specified a very low value (%d) for --%s"
|
||||
self.log("root", t % (zi, name), 3)
|
||||
zi = 2
|
||||
zi2 = 2 ** (zi - 1).bit_length()
|
||||
if zi != zi2:
|
||||
zi3 = 2 ** ((zi - 1).bit_length() - 1)
|
||||
t = "WARNING: expect poor performance because --%s is not a power-of-two; consider using %d or %d instead of %d"
|
||||
self.log("root", t % (name, zi2, zi3, zi), 3)
|
||||
|
||||
if args.s_rd_sz > args.iobuf:
|
||||
t = "WARNING: --s-rd-sz (%d) is larger than --iobuf (%d); this may lead to reduced performance"
|
||||
self.log("root", t % (args.s_rd_sz, args.iobuf), 3)
|
||||
|
||||
bri = "zy"[args.theme % 2 :][:1]
|
||||
ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)]
|
||||
args.theme = "{0}{1} {0} {1}".format(ch, bri)
|
||||
@@ -253,6 +276,11 @@ class SvcHub(object):
|
||||
if want_ff and ANYWIN:
|
||||
self.log("thumb", "download FFmpeg to fix it:\033[0m " + FFMPEG_URL, 3)
|
||||
|
||||
if not args.no_acode:
|
||||
if not re.match("^(0|[qv][0-9]|[0-9]{2,3}k)$", args.q_mp3.lower()):
|
||||
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,))
|
||||
|
||||
args.th_poke = min(args.th_poke, args.th_maxage, args.ac_maxage)
|
||||
|
||||
zms = ""
|
||||
@@ -268,6 +296,12 @@ class SvcHub(object):
|
||||
Daemon(self.start_ftpd, "start_ftpd")
|
||||
zms += "f" if args.ftp else "F"
|
||||
|
||||
if args.tftp:
|
||||
from .tftpd import Tftpd
|
||||
|
||||
self.tftpd: Optional[Tftpd] = None
|
||||
Daemon(self.start_ftpd, "start_tftpd")
|
||||
|
||||
if args.smb:
|
||||
# impacket.dcerpc is noisy about listen timeouts
|
||||
sto = socket.getdefaulttimeout()
|
||||
@@ -297,10 +331,12 @@ class SvcHub(object):
|
||||
|
||||
def start_ftpd(self) -> None:
|
||||
time.sleep(30)
|
||||
if self.ftpd:
|
||||
return
|
||||
|
||||
self.restart_ftpd()
|
||||
if hasattr(self, "ftpd") and not self.ftpd:
|
||||
self.restart_ftpd()
|
||||
|
||||
if hasattr(self, "tftpd") and not self.tftpd:
|
||||
self.restart_tftpd()
|
||||
|
||||
def restart_ftpd(self) -> None:
|
||||
if not hasattr(self, "ftpd"):
|
||||
@@ -317,6 +353,17 @@ class SvcHub(object):
|
||||
self.ftpd = Ftpd(self)
|
||||
self.log("root", "started FTPd")
|
||||
|
||||
def restart_tftpd(self) -> None:
|
||||
if not hasattr(self, "tftpd"):
|
||||
return
|
||||
|
||||
from .tftpd import Tftpd
|
||||
|
||||
if self.tftpd:
|
||||
return # todo
|
||||
|
||||
self.tftpd = Tftpd(self)
|
||||
|
||||
def thr_httpsrv_up(self) -> None:
|
||||
time.sleep(1 if self.args.ign_ebind_all else 5)
|
||||
expected = self.broker.num_workers * self.tcpsrv.nsrv
|
||||
@@ -366,6 +413,23 @@ class SvcHub(object):
|
||||
|
||||
Daemon(self.sd_notify, "sd-notify")
|
||||
|
||||
def _check_env(self) -> None:
|
||||
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"
|
||||
self.log("root", t, 3)
|
||||
if self.args.bauth_last:
|
||||
self.log("root", "WARNING: ignoring --bauth-last due to --no-bauth", 3)
|
||||
|
||||
def _process_config(self) -> bool:
|
||||
al = self.args
|
||||
|
||||
@@ -432,6 +496,13 @@ class SvcHub(object):
|
||||
else:
|
||||
setattr(al, k, re.compile(vs))
|
||||
|
||||
for k in "tftp_lsf".split(" "):
|
||||
vs = getattr(al, k)
|
||||
if not vs or vs == "no":
|
||||
setattr(al, k, None)
|
||||
else:
|
||||
setattr(al, k, re.compile("^" + vs + "$"))
|
||||
|
||||
if not al.sus_urls:
|
||||
al.ban_url = "no"
|
||||
elif al.ban_url == "no":
|
||||
@@ -439,11 +510,11 @@ class SvcHub(object):
|
||||
|
||||
al.xff_hdr = al.xff_hdr.lower()
|
||||
al.idp_h_usr = al.idp_h_usr.lower()
|
||||
# al.idp_h_grp = al.idp_h_grp.lower()
|
||||
al.idp_h_grp = al.idp_h_grp.lower()
|
||||
al.idp_h_key = al.idp_h_key.lower()
|
||||
|
||||
al.xff_re = self._ipa2re(al.xff_src)
|
||||
al.ipa_re = self._ipa2re(al.ipa)
|
||||
al.ftp_ipa_re = self._ipa2re(al.ftp_ipa or al.ipa)
|
||||
al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa)
|
||||
al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa)
|
||||
|
||||
mte = ODict.fromkeys(DEF_MTE.split(","), True)
|
||||
al.mte = odfusion(mte, al.mte)
|
||||
@@ -460,6 +531,18 @@ class SvcHub(object):
|
||||
if ptn:
|
||||
setattr(self.args, k, re.compile(ptn))
|
||||
|
||||
for k in ["idp_gsep"]:
|
||||
ptn = getattr(self.args, k)
|
||||
if "]" in ptn:
|
||||
ptn = "]" + ptn.replace("]", "")
|
||||
if "[" in ptn:
|
||||
ptn = ptn.replace("[", "") + "["
|
||||
if "-" in ptn:
|
||||
ptn = ptn.replace("-", "") + "-"
|
||||
|
||||
ptn = ptn.replace("\\", "\\\\").replace("^", "\\^")
|
||||
setattr(self.args, k, re.compile("[%s]" % (ptn,)))
|
||||
|
||||
try:
|
||||
zf1, zf2 = self.args.rm_retry.split("/")
|
||||
self.args.rm_re_t = float(zf1)
|
||||
@@ -467,6 +550,13 @@ class SvcHub(object):
|
||||
except:
|
||||
raise Exception("invalid --rm-retry [%s]" % (self.args.rm_retry,))
|
||||
|
||||
try:
|
||||
zf1, zf2 = self.args.mv_retry.split("/")
|
||||
self.args.mv_re_t = float(zf1)
|
||||
self.args.mv_re_r = float(zf2)
|
||||
except:
|
||||
raise Exception("invalid --mv-retry [%s]" % (self.args.mv_retry,))
|
||||
|
||||
return True
|
||||
|
||||
def _ipa2re(self, txt) -> Optional[re.Pattern]:
|
||||
@@ -635,21 +725,37 @@ class SvcHub(object):
|
||||
self.log("root", "ssdp startup failed;\n" + min_ex(), 3)
|
||||
|
||||
def reload(self) -> str:
|
||||
if self.reloading:
|
||||
return "cannot reload; already in progress"
|
||||
with self.up2k.mutex:
|
||||
if self.reloading:
|
||||
return "cannot reload; already in progress"
|
||||
self.reloading = 1
|
||||
|
||||
self.reloading = True
|
||||
Daemon(self._reload, "reloading")
|
||||
return "reload initiated"
|
||||
|
||||
def _reload(self) -> None:
|
||||
self.log("root", "reload scheduled")
|
||||
def _reload(self, rescan_all_vols: bool = True) -> None:
|
||||
with self.up2k.mutex:
|
||||
if self.reloading != 1:
|
||||
return
|
||||
self.reloading = 2
|
||||
self.log("root", "reloading config")
|
||||
self.asrv.reload()
|
||||
self.up2k.reload()
|
||||
self.up2k.reload(rescan_all_vols)
|
||||
self.broker.reload()
|
||||
self.reloading = 0
|
||||
|
||||
self.reloading = False
|
||||
def _reload_blocking(self, rescan_all_vols: bool = True) -> None:
|
||||
while True:
|
||||
with self.up2k.mutex:
|
||||
if self.reloading < 2:
|
||||
self.reloading = 1
|
||||
break
|
||||
time.sleep(0.05)
|
||||
|
||||
# try to handle multiple pending IdP reloads at once:
|
||||
time.sleep(0.2)
|
||||
|
||||
self._reload(rescan_all_vols=rescan_all_vols)
|
||||
|
||||
def stop_thr(self) -> None:
|
||||
while not self.stop_req:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import calendar
|
||||
import stat
|
||||
import time
|
||||
@@ -218,12 +219,13 @@ class StreamZip(StreamArc):
|
||||
def __init__(
|
||||
self,
|
||||
log: "NamedLogger",
|
||||
args: argparse.Namespace,
|
||||
fgen: Generator[dict[str, Any], None, None],
|
||||
utf8: bool = False,
|
||||
pre_crc: bool = False,
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
super(StreamZip, self).__init__(log, fgen)
|
||||
super(StreamZip, self).__init__(log, args, fgen)
|
||||
|
||||
self.utf8 = utf8
|
||||
self.pre_crc = pre_crc
|
||||
@@ -248,7 +250,7 @@ class StreamZip(StreamArc):
|
||||
|
||||
crc = 0
|
||||
if self.pre_crc:
|
||||
for buf in yieldfile(src):
|
||||
for buf in yieldfile(src, self.args.iobuf):
|
||||
crc = zlib.crc32(buf, crc)
|
||||
|
||||
crc &= 0xFFFFFFFF
|
||||
@@ -257,7 +259,7 @@ class StreamZip(StreamArc):
|
||||
buf = gen_hdr(None, name, sz, ts, self.utf8, crc, self.pre_crc)
|
||||
yield self._ct(buf)
|
||||
|
||||
for buf in yieldfile(src):
|
||||
for buf in yieldfile(src, self.args.iobuf):
|
||||
if not self.pre_crc:
|
||||
crc = zlib.crc32(buf, crc)
|
||||
|
||||
|
||||
@@ -309,6 +309,7 @@ class TcpSrv(object):
|
||||
self.hub.start_zeroconf()
|
||||
gencert(self.log, self.args, self.netdevs)
|
||||
self.hub.restart_ftpd()
|
||||
self.hub.restart_tftpd()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
self.stopping = True
|
||||
|
||||
434
copyparty/tftpd.py
Normal file
434
copyparty/tftpd.py
Normal file
@@ -0,0 +1,434 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
try:
|
||||
from types import SimpleNamespace
|
||||
except:
|
||||
|
||||
class SimpleNamespace(object):
|
||||
def __init__(self, **attr):
|
||||
self.__dict__.update(attr)
|
||||
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import stat
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
import inspect
|
||||
except:
|
||||
pass
|
||||
|
||||
from partftpy import (
|
||||
TftpContexts,
|
||||
TftpPacketFactory,
|
||||
TftpPacketTypes,
|
||||
TftpServer,
|
||||
TftpStates,
|
||||
)
|
||||
from partftpy.TftpShared import TftpException
|
||||
|
||||
from .__init__ import EXE, TYPE_CHECKING
|
||||
from .authsrv import VFS
|
||||
from .bos import bos
|
||||
from .util import BytesIO, Daemon, ODict, exclude_dotfiles, min_ex, runhook, undot
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .svchub import SvcHub
|
||||
|
||||
|
||||
lg = logging.getLogger("tftp")
|
||||
debug, info, warning, error = (lg.debug, lg.info, lg.warning, lg.error)
|
||||
|
||||
|
||||
def noop(*a, **ka) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _serverInitial(self, pkt: Any, raddress: str, rport: int) -> bool:
|
||||
info("connection from %s:%s", raddress, rport)
|
||||
ret = _sinitial[0](self, pkt, raddress, rport)
|
||||
nm = _hub[0].args.tftp_ipa_nm
|
||||
if nm and not nm.map(raddress):
|
||||
yeet("client rejected (--tftp-ipa): %s" % (raddress,))
|
||||
return ret
|
||||
|
||||
|
||||
# patch ipa-check into partftpd (part 1/2)
|
||||
_hub: list["SvcHub"] = []
|
||||
_sinitial: list[Any] = []
|
||||
|
||||
|
||||
class Tftpd(object):
|
||||
def __init__(self, hub: "SvcHub") -> None:
|
||||
self.hub = hub
|
||||
self.args = hub.args
|
||||
self.asrv = hub.asrv
|
||||
self.log = hub.log
|
||||
self.mutex = threading.Lock()
|
||||
|
||||
_hub[:] = []
|
||||
_hub.append(hub)
|
||||
|
||||
lg.setLevel(logging.DEBUG if self.args.tftpv else logging.INFO)
|
||||
for x in ["partftpy", "partftpy.TftpStates", "partftpy.TftpServer"]:
|
||||
lgr = logging.getLogger(x)
|
||||
lgr.setLevel(logging.DEBUG if self.args.tftpv else logging.INFO)
|
||||
|
||||
if not self.args.tftpv and not self.args.tftpvv:
|
||||
# contexts -> states -> packettypes -> shared
|
||||
# contexts -> packetfactory
|
||||
# packetfactory -> packettypes
|
||||
Cs = [
|
||||
TftpPacketTypes,
|
||||
TftpPacketFactory,
|
||||
TftpStates,
|
||||
TftpContexts,
|
||||
TftpServer,
|
||||
]
|
||||
cbak = []
|
||||
if not self.args.tftp_no_fast and not EXE:
|
||||
try:
|
||||
ptn = re.compile(r"(^\s*)log\.debug\(.*\)$")
|
||||
for C in Cs:
|
||||
cbak.append(C.__dict__)
|
||||
src1 = inspect.getsource(C).split("\n")
|
||||
src2 = "\n".join([ptn.sub("\\1pass", ln) for ln in src1])
|
||||
cfn = C.__spec__.origin
|
||||
exec (compile(src2, filename=cfn, mode="exec"), C.__dict__)
|
||||
except Exception:
|
||||
t = "failed to optimize tftp code; run with --tftp-noopt if there are issues:\n"
|
||||
self.log("tftp", t + min_ex(), 3)
|
||||
for n, zd in enumerate(cbak):
|
||||
Cs[n].__dict__ = zd
|
||||
|
||||
for C in Cs:
|
||||
C.log.debug = noop
|
||||
|
||||
# patch ipa-check into partftpd (part 2/2)
|
||||
_sinitial[:] = []
|
||||
_sinitial.append(TftpStates.TftpServerState.serverInitial)
|
||||
TftpStates.TftpServerState.serverInitial = _serverInitial
|
||||
|
||||
# patch vfs into partftpy
|
||||
TftpContexts.open = self._open
|
||||
TftpStates.open = self._open
|
||||
|
||||
fos = SimpleNamespace()
|
||||
for k in os.__dict__:
|
||||
try:
|
||||
setattr(fos, k, getattr(os, k))
|
||||
except:
|
||||
pass
|
||||
fos.access = self._access
|
||||
fos.mkdir = self._mkdir
|
||||
fos.unlink = self._unlink
|
||||
fos.sep = "/"
|
||||
TftpContexts.os = fos
|
||||
TftpServer.os = fos
|
||||
TftpStates.os = fos
|
||||
|
||||
fop = SimpleNamespace()
|
||||
for k in os.path.__dict__:
|
||||
try:
|
||||
setattr(fop, k, getattr(os.path, k))
|
||||
except:
|
||||
pass
|
||||
fop.abspath = self._p_abspath
|
||||
fop.exists = self._p_exists
|
||||
fop.isdir = self._p_isdir
|
||||
fop.normpath = self._p_normpath
|
||||
fos.path = fop
|
||||
|
||||
self._disarm(fos)
|
||||
|
||||
ip = next((x for x in self.args.i if ":" not in x), None)
|
||||
if not ip:
|
||||
self.log("tftp", "IPv6 not supported for tftp; listening on 0.0.0.0", 3)
|
||||
ip = "0.0.0.0"
|
||||
|
||||
self.port = int(self.args.tftp)
|
||||
self.srv = []
|
||||
self.ips = []
|
||||
|
||||
ports = []
|
||||
if self.args.tftp_pr:
|
||||
p1, p2 = [int(x) for x in self.args.tftp_pr.split("-")]
|
||||
ports = list(range(p1, p2 + 1))
|
||||
|
||||
ips = self.args.i
|
||||
if "::" in ips:
|
||||
ips.append("0.0.0.0")
|
||||
|
||||
if self.args.ftp4:
|
||||
ips = [x for x in ips if ":" not in x]
|
||||
|
||||
ips = list(ODict.fromkeys(ips)) # dedup
|
||||
|
||||
for ip in ips:
|
||||
name = "tftp_%s" % (ip,)
|
||||
Daemon(self._start, name, [ip, ports])
|
||||
time.sleep(0.2) # give dualstack a chance
|
||||
|
||||
def nlog(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.log("tftp", msg, c)
|
||||
|
||||
def _start(self, ip, ports):
|
||||
fam = socket.AF_INET6 if ":" in ip else socket.AF_INET
|
||||
have_been_alive = False
|
||||
while True:
|
||||
srv = TftpServer.TftpServer("/", self._ls)
|
||||
with self.mutex:
|
||||
self.srv.append(srv)
|
||||
self.ips.append(ip)
|
||||
|
||||
try:
|
||||
# this is the listen loop; it should block forever
|
||||
srv.listen(ip, self.port, af_family=fam, ports=ports)
|
||||
except:
|
||||
with self.mutex:
|
||||
self.srv.remove(srv)
|
||||
self.ips.remove(ip)
|
||||
|
||||
try:
|
||||
srv.sock.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
bound = bool(srv.listenport)
|
||||
except:
|
||||
bound = False
|
||||
|
||||
if bound:
|
||||
# this instance has managed to bind at least once
|
||||
have_been_alive = True
|
||||
|
||||
if have_been_alive:
|
||||
t = "tftp server [%s]:%d crashed; restarting in 3 sec:\n%s"
|
||||
error(t, ip, self.port, min_ex())
|
||||
time.sleep(3)
|
||||
continue
|
||||
|
||||
# server failed to start; could be due to dualstack (ipv6 managed to bind and this is ipv4)
|
||||
if ip != "0.0.0.0" or "::" not in self.ips:
|
||||
# nope, it's fatal
|
||||
t = "tftp server [%s]:%d failed to start:\n%s"
|
||||
error(t, ip, self.port, min_ex())
|
||||
|
||||
# yep; ignore
|
||||
# (TODO: move the "listening @ ..." infolog in partftpy to
|
||||
# after the bind attempt so it doesn't print twice)
|
||||
return
|
||||
|
||||
info("tftp server [%s]:%d terminated", ip, self.port)
|
||||
break
|
||||
|
||||
def stop(self):
|
||||
with self.mutex:
|
||||
srvs = self.srv[:]
|
||||
|
||||
for srv in srvs:
|
||||
srv.stop()
|
||||
|
||||
def _v2a(self, caller: str, vpath: str, perms: list, *a: Any) -> tuple[VFS, str]:
|
||||
vpath = vpath.replace("\\", "/").lstrip("/")
|
||||
if not perms:
|
||||
perms = [True, True]
|
||||
|
||||
debug('%s("%s", %s) %s\033[K\033[0m', caller, vpath, str(a), perms)
|
||||
vfs, rem = self.asrv.vfs.get(vpath, "*", *perms)
|
||||
return vfs, vfs.canonical(rem)
|
||||
|
||||
def _ls(self, vpath: str, raddress: str, rport: int, force=False) -> Any:
|
||||
# generate file listing if vpath is dir.txt and return as file object
|
||||
if not force:
|
||||
vpath, fn = os.path.split(vpath.replace("\\", "/"))
|
||||
ptn = self.args.tftp_lsf
|
||||
if not ptn or not ptn.match(fn.lower()):
|
||||
return None
|
||||
|
||||
vn, rem = self.asrv.vfs.get(vpath, "*", True, False)
|
||||
fsroot, vfs_ls, vfs_virt = vn.ls(
|
||||
rem,
|
||||
"*",
|
||||
not self.args.no_scandir,
|
||||
[[True, False]],
|
||||
)
|
||||
dnames = set([x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)])
|
||||
dirs1 = [(v.st_mtime, v.st_size, k + "/") for k, v in vfs_ls if k in dnames]
|
||||
fils1 = [(v.st_mtime, v.st_size, k) for k, v in vfs_ls if k not in dnames]
|
||||
real1 = dirs1 + fils1
|
||||
realt = [(datetime.fromtimestamp(mt), sz, fn) for mt, sz, fn in real1]
|
||||
reals = [
|
||||
(
|
||||
"%04d-%02d-%02d %02d:%02d:%02d"
|
||||
% (
|
||||
zd.year,
|
||||
zd.month,
|
||||
zd.day,
|
||||
zd.hour,
|
||||
zd.minute,
|
||||
zd.second,
|
||||
),
|
||||
sz,
|
||||
fn,
|
||||
)
|
||||
for zd, sz, fn in realt
|
||||
]
|
||||
virs = [("????-??-?? ??:??:??", 0, k + "/") for k in vfs_virt.keys()]
|
||||
ls = virs + reals
|
||||
|
||||
if "*" not in vn.axs.udot:
|
||||
names = set(exclude_dotfiles([x[2] for x in ls]))
|
||||
ls = [x for x in ls if x[2] in names]
|
||||
|
||||
try:
|
||||
biggest = max([x[1] for x in ls])
|
||||
except:
|
||||
biggest = 0
|
||||
|
||||
perms = []
|
||||
if "*" in vn.axs.uread:
|
||||
perms.append("read")
|
||||
if "*" in vn.axs.udot:
|
||||
perms.append("hidden")
|
||||
if "*" in vn.axs.uwrite:
|
||||
if "*" in vn.axs.udel:
|
||||
perms.append("overwrite")
|
||||
else:
|
||||
perms.append("write")
|
||||
|
||||
fmt = "{{}} {{:{},}} {{}}"
|
||||
fmt = fmt.format(len("{:,}".format(biggest)))
|
||||
retl = ["# permissions: %s" % (", ".join(perms),)]
|
||||
retl += [fmt.format(*x) for x in ls]
|
||||
ret = "\n".join(retl).encode("utf-8", "replace")
|
||||
return BytesIO(ret + b"\n")
|
||||
|
||||
def _open(self, vpath: str, mode: str, *a: Any, **ka: Any) -> Any:
|
||||
rd = wr = False
|
||||
if mode == "rb":
|
||||
rd = True
|
||||
elif mode == "wb":
|
||||
wr = True
|
||||
else:
|
||||
raise Exception("bad mode %s" % (mode,))
|
||||
|
||||
vfs, ap = self._v2a("open", vpath, [rd, wr])
|
||||
if wr:
|
||||
if "*" not in vfs.axs.uwrite:
|
||||
yeet("blocked write; folder not world-writable: /%s" % (vpath,))
|
||||
|
||||
if bos.path.exists(ap) and "*" not in vfs.axs.udel:
|
||||
yeet("blocked write; folder not world-deletable: /%s" % (vpath,))
|
||||
|
||||
xbu = vfs.flags.get("xbu")
|
||||
if xbu and not runhook(
|
||||
self.nlog, xbu, ap, vpath, "", "", 0, 0, "8.3.8.7", 0, ""
|
||||
):
|
||||
yeet("blocked by xbu server config: " + vpath)
|
||||
|
||||
if not self.args.tftp_nols and bos.path.isdir(ap):
|
||||
return self._ls(vpath, "", 0, True)
|
||||
|
||||
if not a:
|
||||
a = [self.args.iobuf]
|
||||
|
||||
return open(ap, mode, *a, **ka)
|
||||
|
||||
def _mkdir(self, vpath: str, *a) -> None:
|
||||
vfs, ap = self._v2a("mkdir", vpath, [])
|
||||
if "*" not in vfs.axs.uwrite:
|
||||
yeet("blocked mkdir; folder not world-writable: /%s" % (vpath,))
|
||||
|
||||
return bos.mkdir(ap)
|
||||
|
||||
def _unlink(self, vpath: str) -> None:
|
||||
# return bos.unlink(self._v2a("stat", vpath, *a)[1])
|
||||
vfs, ap = self._v2a("delete", vpath, [True, False, False, True])
|
||||
|
||||
try:
|
||||
inf = bos.stat(ap)
|
||||
except:
|
||||
return
|
||||
|
||||
if not stat.S_ISREG(inf.st_mode) or inf.st_size:
|
||||
yeet("attempted delete of non-empty file")
|
||||
|
||||
vpath = vpath.replace("\\", "/").lstrip("/")
|
||||
self.hub.up2k.handle_rm("*", "8.3.8.7", [vpath], [], False, False)
|
||||
|
||||
def _access(self, *a: Any) -> bool:
|
||||
return True
|
||||
|
||||
def _p_abspath(self, vpath: str) -> str:
|
||||
return "/" + undot(vpath)
|
||||
|
||||
def _p_normpath(self, *a: Any) -> str:
|
||||
return ""
|
||||
|
||||
def _p_exists(self, vpath: str) -> bool:
|
||||
try:
|
||||
ap = self._v2a("p.exists", vpath, [False, False])[1]
|
||||
bos.stat(ap)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def _p_isdir(self, vpath: str) -> bool:
|
||||
try:
|
||||
st = bos.stat(self._v2a("p.isdir", vpath, [False, False])[1])
|
||||
ret = stat.S_ISDIR(st.st_mode)
|
||||
return ret
|
||||
except:
|
||||
return False
|
||||
|
||||
def _hook(self, *a: Any, **ka: Any) -> None:
|
||||
src = inspect.currentframe().f_back.f_code.co_name
|
||||
error("\033[31m%s:hook(%s)\033[0m", src, a)
|
||||
raise Exception("nope")
|
||||
|
||||
def _disarm(self, fos: SimpleNamespace) -> None:
|
||||
fos.chmod = self._hook
|
||||
fos.chown = self._hook
|
||||
fos.close = self._hook
|
||||
fos.ftruncate = self._hook
|
||||
fos.lchown = self._hook
|
||||
fos.link = self._hook
|
||||
fos.listdir = self._hook
|
||||
fos.lstat = self._hook
|
||||
fos.open = self._hook
|
||||
fos.remove = self._hook
|
||||
fos.rename = self._hook
|
||||
fos.replace = self._hook
|
||||
fos.scandir = self._hook
|
||||
fos.stat = self._hook
|
||||
fos.symlink = self._hook
|
||||
fos.truncate = self._hook
|
||||
fos.utime = self._hook
|
||||
fos.walk = self._hook
|
||||
|
||||
fos.path.expanduser = self._hook
|
||||
fos.path.expandvars = self._hook
|
||||
fos.path.getatime = self._hook
|
||||
fos.path.getctime = self._hook
|
||||
fos.path.getmtime = self._hook
|
||||
fos.path.getsize = self._hook
|
||||
fos.path.isabs = self._hook
|
||||
fos.path.isfile = self._hook
|
||||
fos.path.islink = self._hook
|
||||
fos.path.realpath = self._hook
|
||||
|
||||
|
||||
def yeet(msg: str) -> None:
|
||||
warning(msg)
|
||||
raise TftpException(msg)
|
||||
@@ -57,7 +57,7 @@ class ThumbCli(object):
|
||||
if is_vid and "dvthumb" in dbv.flags:
|
||||
return None
|
||||
|
||||
want_opus = fmt in ("opus", "caf")
|
||||
want_opus = fmt in ("opus", "caf", "mp3")
|
||||
is_au = ext in self.fmt_ffa
|
||||
if is_au:
|
||||
if want_opus:
|
||||
@@ -78,16 +78,34 @@ class ThumbCli(object):
|
||||
if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg", "png"]:
|
||||
return os.path.join(ptop, rem)
|
||||
|
||||
if fmt == "j" and self.args.th_no_jpg:
|
||||
fmt = "w"
|
||||
if fmt[:1] in "jw":
|
||||
sfmt = fmt[:1]
|
||||
|
||||
if fmt == "w":
|
||||
if (
|
||||
self.args.th_no_webp
|
||||
or (is_img and not self.can_webp)
|
||||
or (self.args.th_ff_jpg and (not is_img or preferred == "ff"))
|
||||
):
|
||||
fmt = "j"
|
||||
if sfmt == "j" and self.args.th_no_jpg:
|
||||
sfmt = "w"
|
||||
|
||||
if sfmt == "w":
|
||||
if (
|
||||
self.args.th_no_webp
|
||||
or (is_img and not self.can_webp)
|
||||
or (self.args.th_ff_jpg and (not is_img or preferred == "ff"))
|
||||
):
|
||||
sfmt = "j"
|
||||
|
||||
vf_crop = dbv.flags["crop"]
|
||||
vf_th3x = dbv.flags["th3x"]
|
||||
|
||||
if "f" in vf_crop:
|
||||
sfmt += "f" if "n" in vf_crop else ""
|
||||
else:
|
||||
sfmt += "f" if "f" in fmt else ""
|
||||
|
||||
if "f" in vf_th3x:
|
||||
sfmt += "3" if "y" in vf_th3x else ""
|
||||
else:
|
||||
sfmt += "3" if "3" in fmt else ""
|
||||
|
||||
fmt = sfmt
|
||||
|
||||
histpath = self.asrv.vfs.histtab.get(ptop)
|
||||
if not histpath:
|
||||
|
||||
@@ -16,9 +16,9 @@ from .__init__ import ANYWIN, TYPE_CHECKING
|
||||
from .authsrv import VFS
|
||||
from .bos import bos
|
||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
|
||||
from .util import BytesIO # type: ignore
|
||||
from .util import (
|
||||
FFMPEG_URL,
|
||||
BytesIO, # type: ignore
|
||||
Cooldown,
|
||||
Daemon,
|
||||
Pebkac,
|
||||
@@ -28,6 +28,7 @@ from .util import (
|
||||
runcmd,
|
||||
statdir,
|
||||
vsplit,
|
||||
wrename,
|
||||
wunlink,
|
||||
)
|
||||
|
||||
@@ -97,8 +98,8 @@ def thumb_path(histpath: str, rem: str, mtime: float, fmt: str, ffa: set[str]) -
|
||||
|
||||
# spectrograms are never cropped; strip fullsize flag
|
||||
ext = rem.split(".")[-1].lower()
|
||||
if ext in ffa and fmt in ("wf", "jf"):
|
||||
fmt = fmt[:1]
|
||||
if ext in ffa and fmt[:2] in ("wf", "jf"):
|
||||
fmt = fmt.replace("f", "")
|
||||
|
||||
rd += "\n" + fmt
|
||||
h = hashlib.sha512(afsenc(rd)).digest()
|
||||
@@ -109,7 +110,7 @@ def thumb_path(histpath: str, rem: str, mtime: float, fmt: str, ffa: set[str]) -
|
||||
h = hashlib.sha512(afsenc(fn)).digest()
|
||||
fn = base64.urlsafe_b64encode(h).decode("ascii")[:24]
|
||||
|
||||
if fmt in ("opus", "caf"):
|
||||
if fmt in ("opus", "caf", "mp3"):
|
||||
cat = "ac"
|
||||
else:
|
||||
fc = fmt[:1]
|
||||
@@ -200,9 +201,10 @@ class ThumbSrv(object):
|
||||
with self.mutex:
|
||||
return not self.nthr
|
||||
|
||||
def getres(self, vn: VFS) -> tuple[int, int]:
|
||||
def getres(self, vn: VFS, fmt: str) -> tuple[int, int]:
|
||||
mul = 3 if "3" in fmt else 1
|
||||
w, h = vn.flags["thsize"].split("x")
|
||||
return int(w), int(h)
|
||||
return int(w) * mul, int(h) * mul
|
||||
|
||||
def get(self, ptop: str, rem: str, mtime: float, fmt: str) -> Optional[str]:
|
||||
histpath = self.asrv.vfs.histtab.get(ptop)
|
||||
@@ -306,6 +308,8 @@ class ThumbSrv(object):
|
||||
elif lib == "ff" and ext in self.fmt_ffa:
|
||||
if tpath.endswith(".opus") or tpath.endswith(".caf"):
|
||||
funs.append(self.conv_opus)
|
||||
elif tpath.endswith(".mp3"):
|
||||
funs.append(self.conv_mp3)
|
||||
elif tpath.endswith(".png"):
|
||||
funs.append(self.conv_waves)
|
||||
png_ok = True
|
||||
@@ -343,7 +347,7 @@ class ThumbSrv(object):
|
||||
pass
|
||||
|
||||
try:
|
||||
bos.rename(ttpath, tpath)
|
||||
wrename(self.log, ttpath, tpath, vn.flags)
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -364,7 +368,7 @@ class ThumbSrv(object):
|
||||
|
||||
def fancy_pillow(self, im: "Image.Image", fmt: str, vn: VFS) -> "Image.Image":
|
||||
# exif_transpose is expensive (loads full image + unconditional copy)
|
||||
res = self.getres(vn)
|
||||
res = self.getres(vn, fmt)
|
||||
r = max(*res) * 2
|
||||
im.thumbnail((r, r), resample=Image.LANCZOS)
|
||||
try:
|
||||
@@ -379,7 +383,7 @@ class ThumbSrv(object):
|
||||
if rot in rots:
|
||||
im = im.transpose(rots[rot])
|
||||
|
||||
if fmt.endswith("f"):
|
||||
if "f" in fmt:
|
||||
im.thumbnail(res, resample=Image.LANCZOS)
|
||||
else:
|
||||
iw, ih = im.size
|
||||
@@ -396,7 +400,7 @@ class ThumbSrv(object):
|
||||
im = self.fancy_pillow(im, fmt, vn)
|
||||
except Exception as ex:
|
||||
self.log("fancy_pillow {}".format(ex), "90")
|
||||
im.thumbnail(self.getres(vn))
|
||||
im.thumbnail(self.getres(vn, fmt))
|
||||
|
||||
fmts = ["RGB", "L"]
|
||||
args = {"quality": 40}
|
||||
@@ -422,10 +426,10 @@ class ThumbSrv(object):
|
||||
def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
self.wait4ram(0.2, tpath)
|
||||
crops = ["centre", "none"]
|
||||
if fmt.endswith("f"):
|
||||
if "f" in fmt:
|
||||
crops = ["none"]
|
||||
|
||||
w, h = self.getres(vn)
|
||||
w, h = self.getres(vn, fmt)
|
||||
kw = {"height": h, "size": "down", "intent": "relative"}
|
||||
|
||||
for c in crops:
|
||||
@@ -454,12 +458,12 @@ class ThumbSrv(object):
|
||||
seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")]
|
||||
|
||||
scale = "scale={0}:{1}:force_original_aspect_ratio="
|
||||
if fmt.endswith("f"):
|
||||
if "f" in fmt:
|
||||
scale += "decrease,setsar=1:1"
|
||||
else:
|
||||
scale += "increase,crop={0}:{1},setsar=1:1"
|
||||
|
||||
res = self.getres(vn)
|
||||
res = self.getres(vn, fmt)
|
||||
bscale = scale.format(*list(res)).encode("utf-8")
|
||||
# fmt: off
|
||||
cmd = [
|
||||
@@ -594,7 +598,11 @@ class ThumbSrv(object):
|
||||
need = 0.2 + dur / coeff
|
||||
self.wait4ram(need, tpath)
|
||||
|
||||
fc = "[0:a:0]aresample=48000{},showspectrumpic=s=640x512,crop=780:544:70:50[o]"
|
||||
fc = "[0:a:0]aresample=48000{},showspectrumpic=s="
|
||||
if "3" in fmt:
|
||||
fc += "1280x1024,crop=1420:1056:70:48[o]"
|
||||
else:
|
||||
fc += "640x512,crop=780:544:70:48[o]"
|
||||
|
||||
if self.args.th_ff_swr:
|
||||
fco = ":filter_size=128:cutoff=0.877"
|
||||
@@ -632,8 +640,47 @@ class ThumbSrv(object):
|
||||
cmd += [fsenc(tpath)]
|
||||
self._run_ff(cmd, vn)
|
||||
|
||||
def conv_mp3(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
quality = self.args.q_mp3.lower()
|
||||
if self.args.no_acode or not quality:
|
||||
raise Exception("disabled in server config")
|
||||
|
||||
self.wait4ram(0.2, tpath)
|
||||
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
if "ac" not in ret:
|
||||
raise Exception("not audio")
|
||||
|
||||
if quality.endswith("k"):
|
||||
qk = b"-b:a"
|
||||
qv = quality.encode("ascii")
|
||||
else:
|
||||
qk = b"-q:a"
|
||||
qv = quality[1:].encode("ascii")
|
||||
|
||||
# extremely conservative choices for output format
|
||||
# (always 2ch 44k1) because if a device is old enough
|
||||
# to not support opus then it's probably also super picky
|
||||
|
||||
# fmt: off
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
b"-i", fsenc(abspath),
|
||||
b"-map_metadata", b"-1",
|
||||
b"-map", b"0:a:0",
|
||||
b"-ar", b"44100",
|
||||
b"-ac", b"2",
|
||||
b"-c:a", b"libmp3lame",
|
||||
qk, qv,
|
||||
fsenc(tpath)
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, oom=300)
|
||||
|
||||
def conv_opus(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
if self.args.no_acode:
|
||||
if self.args.no_acode or not self.args.q_opus:
|
||||
raise Exception("disabled in server config")
|
||||
|
||||
self.wait4ram(0.2, tpath)
|
||||
@@ -657,6 +704,7 @@ class ThumbSrv(object):
|
||||
pass
|
||||
|
||||
caf_src = abspath if src_opus else tmp_opus
|
||||
bq = ("%dk" % (self.args.q_opus,)).encode("ascii")
|
||||
|
||||
if not want_caf or not src_opus:
|
||||
# fmt: off
|
||||
@@ -669,7 +717,7 @@ class ThumbSrv(object):
|
||||
b"-map_metadata", b"-1",
|
||||
b"-map", b"0:a:0",
|
||||
b"-c:a", b"libopus",
|
||||
b"-b:a", b"128k",
|
||||
b"-b:a", bq,
|
||||
fsenc(tmp_opus)
|
||||
]
|
||||
# fmt: on
|
||||
@@ -692,7 +740,7 @@ class ThumbSrv(object):
|
||||
b"-map_metadata", b"-1",
|
||||
b"-ac", b"2",
|
||||
b"-c:a", b"libopus",
|
||||
b"-b:a", b"128k",
|
||||
b"-b:a", bq,
|
||||
b"-f", b"caf",
|
||||
fsenc(tpath)
|
||||
]
|
||||
@@ -766,7 +814,7 @@ class ThumbSrv(object):
|
||||
|
||||
def _clean(self, cat: str, thumbpath: str) -> int:
|
||||
# self.log("cln {}".format(thumbpath))
|
||||
exts = ["jpg", "webp", "png"] if cat == "th" else ["opus", "caf"]
|
||||
exts = ["jpg", "webp", "png"] if cat == "th" else ["opus", "caf", "mp3"]
|
||||
maxage = getattr(self.args, cat + "_maxage")
|
||||
now = time.time()
|
||||
prev_b64 = None
|
||||
|
||||
@@ -21,7 +21,7 @@ from copy import deepcopy
|
||||
|
||||
from queue import Queue
|
||||
|
||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, WINDOWS
|
||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, WINDOWS, E
|
||||
from .authsrv import LEELOO_DALLAS, SSEELOG, VFS, AuthSrv
|
||||
from .bos import bos
|
||||
from .cfg import vf_bmap, vf_cmap, vf_vmap
|
||||
@@ -35,6 +35,7 @@ from .util import (
|
||||
Pebkac,
|
||||
ProgressPrinter,
|
||||
absreal,
|
||||
alltrace,
|
||||
atomic_move,
|
||||
db_ex_chk,
|
||||
dir_is_empty,
|
||||
@@ -87,6 +88,12 @@ zsg = "avif,avifs,bmp,gif,heic,heics,heif,heifs,ico,j2p,j2k,jp2,jpeg,jpg,jpx,png
|
||||
CV_EXTS = set(zsg.split(","))
|
||||
|
||||
|
||||
HINT_HISTPATH = "you could try moving the database to another location (preferably an SSD or NVME drive) using either the --hist argument (global option for all volumes), or the hist volflag (just for this volume)"
|
||||
|
||||
|
||||
VF_CAREFUL = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1}
|
||||
|
||||
|
||||
class Dbw(object):
|
||||
def __init__(self, c: "sqlite3.Cursor", n: int, t: float) -> None:
|
||||
self.c = c
|
||||
@@ -132,6 +139,7 @@ class Up2k(object):
|
||||
self.need_rescan: set[str] = set()
|
||||
self.db_act = 0.0
|
||||
|
||||
self.reg_mutex = threading.Lock()
|
||||
self.registry: dict[str, dict[str, dict[str, Any]]] = {}
|
||||
self.flags: dict[str, dict[str, Any]] = {}
|
||||
self.droppable: dict[str, list[str]] = {}
|
||||
@@ -139,7 +147,7 @@ class Up2k(object):
|
||||
self.volsize: dict["sqlite3.Cursor", int] = {}
|
||||
self.volstate: dict[str, str] = {}
|
||||
self.vol_act: dict[str, float] = {}
|
||||
self.busy_aps: set[str] = set()
|
||||
self.busy_aps: dict[str, int] = {}
|
||||
self.dupesched: dict[str, list[tuple[str, str, float]]] = {}
|
||||
self.snap_prev: dict[str, Optional[tuple[int, float]]] = {}
|
||||
|
||||
@@ -150,7 +158,7 @@ class Up2k(object):
|
||||
self.hashq: Queue[
|
||||
tuple[str, str, dict[str, Any], str, str, str, float, str, bool]
|
||||
] = Queue()
|
||||
self.tagq: Queue[tuple[str, str, str, str, str, float]] = Queue()
|
||||
self.tagq: Queue[tuple[str, str, str, str, int, str, float]] = Queue()
|
||||
self.tag_event = threading.Condition()
|
||||
self.hashq_mutex = threading.Lock()
|
||||
self.n_hashq = 0
|
||||
@@ -195,11 +203,20 @@ class Up2k(object):
|
||||
|
||||
Daemon(self.deferred_init, "up2k-deferred-init")
|
||||
|
||||
def reload(self) -> None:
|
||||
self.gid += 1
|
||||
self.log("reload #{} initiated".format(self.gid))
|
||||
def reload(self, rescan_all_vols: bool) -> None:
|
||||
"""mutex(main) me"""
|
||||
self.log("reload #{} scheduled".format(self.gid + 1))
|
||||
all_vols = self.asrv.vfs.all_vols
|
||||
self.rescan(all_vols, list(all_vols.keys()), True, False)
|
||||
|
||||
with self.reg_mutex:
|
||||
scan_vols = [
|
||||
k for k, v in all_vols.items() if v.realpath not in self.registry
|
||||
]
|
||||
|
||||
if rescan_all_vols:
|
||||
scan_vols = list(all_vols.keys())
|
||||
|
||||
self._rescan(all_vols, scan_vols, True, False)
|
||||
|
||||
def deferred_init(self) -> None:
|
||||
all_vols = self.asrv.vfs.all_vols
|
||||
@@ -208,7 +225,7 @@ class Up2k(object):
|
||||
if self.stop:
|
||||
# up-mt consistency not guaranteed if init is interrupted;
|
||||
# drop caches for a full scan on next boot
|
||||
with self.mutex:
|
||||
with self.mutex, self.reg_mutex:
|
||||
self._drop_caches()
|
||||
|
||||
if self.pp:
|
||||
@@ -228,8 +245,6 @@ class Up2k(object):
|
||||
for n in range(max(1, self.args.mtag_mt)):
|
||||
Daemon(self._tagger, "tagger-{}".format(n))
|
||||
|
||||
Daemon(self._run_all_mtp, "up2k-mtp-init")
|
||||
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
if self.pp:
|
||||
msg += "\033[K"
|
||||
@@ -276,11 +291,75 @@ class Up2k(object):
|
||||
min(1000 * 24 * 60 * 60 - 1, time.time() - self.db_act)
|
||||
),
|
||||
}
|
||||
return json.dumps(ret, indent=4)
|
||||
return json.dumps(ret, separators=(",\n", ": "))
|
||||
|
||||
def find_job_by_ap(self, ptop: str, ap: str) -> str:
|
||||
try:
|
||||
if ANYWIN:
|
||||
ap = ap.replace("\\", "/")
|
||||
|
||||
vp = ap[len(ptop) :].strip("/")
|
||||
dn, fn = vsplit(vp)
|
||||
with self.reg_mutex:
|
||||
tab2 = self.registry[ptop]
|
||||
for job in tab2.values():
|
||||
if job["prel"] == dn and job["name"] == fn:
|
||||
return json.dumps(job, separators=(",\n", ": "))
|
||||
except:
|
||||
pass
|
||||
|
||||
return "{}"
|
||||
|
||||
def get_unfinished_by_user(self, uname, ip) -> str:
|
||||
if PY2 or not self.reg_mutex.acquire(timeout=2):
|
||||
return '[{"timeout":1}]'
|
||||
|
||||
ret: list[tuple[int, str, int, int, int]] = []
|
||||
try:
|
||||
for ptop, tab2 in self.registry.items():
|
||||
cfg = self.flags.get(ptop, {}).get("u2abort", 1)
|
||||
if not cfg:
|
||||
continue
|
||||
addr = (ip or "\n") if cfg in (1, 2) else ""
|
||||
user = (uname or "\n") if cfg in (1, 3) else ""
|
||||
drp = self.droppable.get(ptop, {})
|
||||
for wark, job in tab2.items():
|
||||
if (
|
||||
wark in drp
|
||||
or (user and user != job["user"])
|
||||
or (addr and addr != job["addr"])
|
||||
):
|
||||
continue
|
||||
|
||||
zt5 = (
|
||||
int(job["t0"]),
|
||||
djoin(job["vtop"], job["prel"], job["name"]),
|
||||
job["size"],
|
||||
len(job["need"]),
|
||||
len(job["hash"]),
|
||||
)
|
||||
ret.append(zt5)
|
||||
finally:
|
||||
self.reg_mutex.release()
|
||||
|
||||
if ANYWIN:
|
||||
ret = [(x[0], x[1].replace("\\", "/"), x[2], x[3], x[4]) for x in ret]
|
||||
|
||||
ret.sort(reverse=True)
|
||||
ret2 = [
|
||||
{
|
||||
"at": at,
|
||||
"vp": "/" + quotep(vp),
|
||||
"pd": 100 - ((nn * 100) // (nh or 1)),
|
||||
"sz": sz,
|
||||
}
|
||||
for (at, vp, sz, nn, nh) in ret
|
||||
]
|
||||
return json.dumps(ret2, separators=(",\n", ": "))
|
||||
|
||||
def get_unfinished(self) -> str:
|
||||
if PY2 or not self.mutex.acquire(timeout=0.5):
|
||||
return "{}"
|
||||
if PY2 or not self.reg_mutex.acquire(timeout=0.5):
|
||||
return ""
|
||||
|
||||
ret: dict[str, tuple[int, int]] = {}
|
||||
try:
|
||||
@@ -301,17 +380,17 @@ class Up2k(object):
|
||||
|
||||
ret[ptop] = (nbytes, nfiles)
|
||||
finally:
|
||||
self.mutex.release()
|
||||
self.reg_mutex.release()
|
||||
|
||||
return json.dumps(ret, indent=4)
|
||||
return json.dumps(ret, separators=(",\n", ": "))
|
||||
|
||||
def get_volsize(self, ptop: str) -> tuple[int, int]:
|
||||
with self.mutex:
|
||||
with self.reg_mutex:
|
||||
return self._get_volsize(ptop)
|
||||
|
||||
def get_volsizes(self, ptops: list[str]) -> list[tuple[int, int]]:
|
||||
ret = []
|
||||
with self.mutex:
|
||||
with self.reg_mutex:
|
||||
for ptop in ptops:
|
||||
ret.append(self._get_volsize(ptop))
|
||||
|
||||
@@ -333,14 +412,21 @@ class Up2k(object):
|
||||
def rescan(
|
||||
self, all_vols: dict[str, VFS], scan_vols: list[str], wait: bool, fscan: bool
|
||||
) -> str:
|
||||
with self.mutex:
|
||||
return self._rescan(all_vols, scan_vols, wait, fscan)
|
||||
|
||||
def _rescan(
|
||||
self, all_vols: dict[str, VFS], scan_vols: list[str], wait: bool, fscan: bool
|
||||
) -> str:
|
||||
"""mutex(main) me"""
|
||||
if not wait and self.pp:
|
||||
return "cannot initiate; scan is already in progress"
|
||||
|
||||
args = (all_vols, scan_vols, fscan)
|
||||
self.gid += 1
|
||||
Daemon(
|
||||
self.init_indexes,
|
||||
"up2k-rescan-{}".format(scan_vols[0] if scan_vols else "all"),
|
||||
args,
|
||||
(all_vols, scan_vols, fscan, self.gid),
|
||||
)
|
||||
return ""
|
||||
|
||||
@@ -452,7 +538,7 @@ class Up2k(object):
|
||||
if vp:
|
||||
fvp = "%s/%s" % (vp, fvp)
|
||||
|
||||
self._handle_rm(LEELOO_DALLAS, "", fvp, [], True)
|
||||
self._handle_rm(LEELOO_DALLAS, "", fvp, [], True, False)
|
||||
nrm += 1
|
||||
|
||||
if nrm:
|
||||
@@ -548,7 +634,7 @@ class Up2k(object):
|
||||
runihook(self.log, cmd, vol, ups)
|
||||
|
||||
def _vis_job_progress(self, job: dict[str, Any]) -> str:
|
||||
perc = 100 - (len(job["need"]) * 100.0 / len(job["hash"]))
|
||||
perc = 100 - (len(job["need"]) * 100.0 / (len(job["hash"]) or 1))
|
||||
path = djoin(job["ptop"], job["prel"], job["name"])
|
||||
return "{:5.1f}% {}".format(perc, path)
|
||||
|
||||
@@ -571,19 +657,32 @@ class Up2k(object):
|
||||
return True, ret
|
||||
|
||||
def init_indexes(
|
||||
self, all_vols: dict[str, VFS], scan_vols: list[str], fscan: bool
|
||||
self, all_vols: dict[str, VFS], scan_vols: list[str], fscan: bool, gid: int = 0
|
||||
) -> bool:
|
||||
gid = self.gid
|
||||
while self.pp and gid == self.gid:
|
||||
time.sleep(0.1)
|
||||
if not gid:
|
||||
with self.mutex:
|
||||
gid = self.gid
|
||||
|
||||
if gid != self.gid:
|
||||
return False
|
||||
nspin = 0
|
||||
while True:
|
||||
nspin += 1
|
||||
if nspin > 1:
|
||||
time.sleep(0.1)
|
||||
|
||||
with self.mutex:
|
||||
if gid != self.gid:
|
||||
return False
|
||||
|
||||
if self.pp:
|
||||
continue
|
||||
|
||||
self.pp = ProgressPrinter(self.log, self.args)
|
||||
|
||||
break
|
||||
|
||||
if gid:
|
||||
self.log("reload #{} running".format(self.gid))
|
||||
self.log("reload #%d running" % (gid,))
|
||||
|
||||
self.pp = ProgressPrinter(self.log, self.args)
|
||||
vols = list(all_vols.values())
|
||||
t0 = time.time()
|
||||
have_e2d = False
|
||||
@@ -601,7 +700,7 @@ class Up2k(object):
|
||||
self.log(msg, c=3)
|
||||
|
||||
live_vols = []
|
||||
with self.mutex:
|
||||
with self.mutex, self.reg_mutex:
|
||||
# only need to protect register_vpath but all in one go feels right
|
||||
for vol in vols:
|
||||
try:
|
||||
@@ -643,7 +742,7 @@ class Up2k(object):
|
||||
|
||||
if self.args.re_dhash or [zv for zv in vols if "e2tsr" in zv.flags]:
|
||||
self.args.re_dhash = False
|
||||
with self.mutex:
|
||||
with self.mutex, self.reg_mutex:
|
||||
self._drop_caches()
|
||||
|
||||
for vol in vols:
|
||||
@@ -720,7 +819,9 @@ class Up2k(object):
|
||||
self.volstate[vol.vpath] = "online (mtp soon)"
|
||||
|
||||
for vol in need_vac:
|
||||
reg = self.register_vpath(vol.realpath, vol.flags)
|
||||
with self.mutex, self.reg_mutex:
|
||||
reg = self.register_vpath(vol.realpath, vol.flags)
|
||||
|
||||
assert reg
|
||||
cur, _ = reg
|
||||
with self.mutex:
|
||||
@@ -734,7 +835,9 @@ class Up2k(object):
|
||||
if vol.flags["dbd"] == "acid":
|
||||
continue
|
||||
|
||||
reg = self.register_vpath(vol.realpath, vol.flags)
|
||||
with self.mutex, self.reg_mutex:
|
||||
reg = self.register_vpath(vol.realpath, vol.flags)
|
||||
|
||||
try:
|
||||
assert reg
|
||||
cur, db_path = reg
|
||||
@@ -767,26 +870,21 @@ class Up2k(object):
|
||||
msg = "could not read tags because no backends are available (Mutagen or FFprobe)"
|
||||
self.log(msg, c=1)
|
||||
|
||||
thr = None
|
||||
if self.mtag:
|
||||
t = "online (running mtp)"
|
||||
if scan_vols:
|
||||
thr = Daemon(self._run_all_mtp, "up2k-mtp-scan", r=False)
|
||||
else:
|
||||
self.pp = None
|
||||
t = "online, idle"
|
||||
|
||||
t = "online (running mtp)" if self.mtag else "online, idle"
|
||||
for vol in vols:
|
||||
self.volstate[vol.vpath] = t
|
||||
|
||||
if thr:
|
||||
thr.start()
|
||||
if self.mtag:
|
||||
Daemon(self._run_all_mtp, "up2k-mtp-scan", (gid,))
|
||||
else:
|
||||
self.pp = None
|
||||
|
||||
return have_e2d
|
||||
|
||||
def register_vpath(
|
||||
self, ptop: str, flags: dict[str, Any]
|
||||
) -> Optional[tuple["sqlite3.Cursor", str]]:
|
||||
"""mutex(main,reg) me"""
|
||||
histpath = self.asrv.vfs.histtab.get(ptop)
|
||||
if not histpath:
|
||||
self.log("no histpath for [{}]".format(ptop))
|
||||
@@ -809,7 +907,7 @@ class Up2k(object):
|
||||
ft = "\033[0;32m{}{:.0}"
|
||||
ff = "\033[0;35m{}{:.0}"
|
||||
fv = "\033[0;36m{}:\033[90m{}"
|
||||
fx = set(("html_head", "rm_re_t", "rm_re_r"))
|
||||
fx = set(("html_head", "rm_re_t", "rm_re_r", "mv_re_t", "mv_re_r"))
|
||||
fd = vf_bmap()
|
||||
fd.update(vf_cmap())
|
||||
fd.update(vf_vmap())
|
||||
@@ -892,7 +990,7 @@ class Up2k(object):
|
||||
return None
|
||||
|
||||
try:
|
||||
cur = self._open_db(db_path)
|
||||
cur = self._open_db_wd(db_path)
|
||||
|
||||
# speeds measured uploading 520 small files on a WD20SPZX (SMR 2.5" 5400rpm 4kb)
|
||||
dbd = flags["dbd"]
|
||||
@@ -935,8 +1033,8 @@ class Up2k(object):
|
||||
|
||||
return cur, db_path
|
||||
except:
|
||||
msg = "cannot use database at [{}]:\n{}"
|
||||
self.log(msg.format(ptop, traceback.format_exc()))
|
||||
msg = "ERROR: cannot use database at [%s]:\n%s\n\033[33mhint: %s\n"
|
||||
self.log(msg % (db_path, traceback.format_exc(), HINT_HISTPATH), 1)
|
||||
|
||||
return None
|
||||
|
||||
@@ -970,7 +1068,9 @@ class Up2k(object):
|
||||
dev = cst.st_dev if vol.flags.get("xdev") else 0
|
||||
|
||||
with self.mutex:
|
||||
reg = self.register_vpath(top, vol.flags)
|
||||
with self.reg_mutex:
|
||||
reg = self.register_vpath(top, vol.flags)
|
||||
|
||||
assert reg and self.pp
|
||||
cur, db_path = reg
|
||||
|
||||
@@ -1567,7 +1667,7 @@ class Up2k(object):
|
||||
|
||||
def _build_tags_index(self, vol: VFS) -> tuple[int, int, bool]:
|
||||
ptop = vol.realpath
|
||||
with self.mutex:
|
||||
with self.mutex, self.reg_mutex:
|
||||
reg = self.register_vpath(ptop, vol.flags)
|
||||
|
||||
assert reg and self.pp
|
||||
@@ -1588,6 +1688,7 @@ class Up2k(object):
|
||||
return ret
|
||||
|
||||
def _drop_caches(self) -> None:
|
||||
"""mutex(main,reg) me"""
|
||||
self.log("dropping caches for a full filesystem scan")
|
||||
for vol in self.asrv.vfs.all_vols.values():
|
||||
reg = self.register_vpath(vol.realpath, vol.flags)
|
||||
@@ -1763,7 +1864,7 @@ class Up2k(object):
|
||||
params: tuple[Any, ...],
|
||||
flt: int,
|
||||
) -> tuple[tempfile.SpooledTemporaryFile[bytes], int]:
|
||||
"""mutex me"""
|
||||
"""mutex(main) me"""
|
||||
n = 0
|
||||
c2 = cur.connection.cursor()
|
||||
tf = tempfile.SpooledTemporaryFile(1024 * 1024 * 8, "w+b", prefix="cpp-tq-")
|
||||
@@ -1805,26 +1906,28 @@ class Up2k(object):
|
||||
self.pending_tags = []
|
||||
return ret
|
||||
|
||||
def _run_all_mtp(self) -> None:
|
||||
gid = self.gid
|
||||
def _run_all_mtp(self, gid: int) -> None:
|
||||
t0 = time.time()
|
||||
for ptop, flags in self.flags.items():
|
||||
if "mtp" in flags:
|
||||
if ptop not in self.entags:
|
||||
t = "skipping mtp for unavailable volume {}"
|
||||
self.log(t.format(ptop), 1)
|
||||
continue
|
||||
self._run_one_mtp(ptop, gid)
|
||||
else:
|
||||
self._run_one_mtp(ptop, gid)
|
||||
|
||||
vtop = "\n"
|
||||
for vol in self.asrv.vfs.all_vols.values():
|
||||
if vol.realpath == ptop:
|
||||
vtop = vol.vpath
|
||||
if "running mtp" in self.volstate.get(vtop, ""):
|
||||
self.volstate[vtop] = "online, idle"
|
||||
|
||||
td = time.time() - t0
|
||||
msg = "mtp finished in {:.2f} sec ({})"
|
||||
self.log(msg.format(td, s2hms(td, True)))
|
||||
|
||||
self.pp = None
|
||||
for k in list(self.volstate.keys()):
|
||||
if "OFFLINE" not in self.volstate[k]:
|
||||
self.volstate[k] = "online, idle"
|
||||
|
||||
if self.args.exit == "idx":
|
||||
self.hub.sigterm()
|
||||
|
||||
@@ -2051,12 +2154,13 @@ class Up2k(object):
|
||||
return
|
||||
|
||||
try:
|
||||
st = bos.stat(qe.abspath)
|
||||
if not qe.mtp:
|
||||
if self.args.mtag_vv:
|
||||
t = "tag-thr: {}({})"
|
||||
self.log(t.format(self.mtag.backend, qe.abspath), "90")
|
||||
|
||||
tags = self.mtag.get(qe.abspath)
|
||||
tags = self.mtag.get(qe.abspath) if st.st_size else {}
|
||||
else:
|
||||
if self.args.mtag_vv:
|
||||
t = "tag-thr: {}({})"
|
||||
@@ -2094,14 +2198,19 @@ class Up2k(object):
|
||||
ip: str,
|
||||
at: float,
|
||||
) -> int:
|
||||
"""will mutex"""
|
||||
"""will mutex(main)"""
|
||||
assert self.mtag
|
||||
|
||||
if not bos.path.isfile(abspath):
|
||||
try:
|
||||
st = bos.stat(abspath)
|
||||
except:
|
||||
return 0
|
||||
|
||||
if not stat.S_ISREG(st.st_mode):
|
||||
return 0
|
||||
|
||||
try:
|
||||
tags = self.mtag.get(abspath)
|
||||
tags = self.mtag.get(abspath) if st.st_size else {}
|
||||
except Exception as ex:
|
||||
self._log_tag_err("", abspath, ex)
|
||||
return 0
|
||||
@@ -2121,7 +2230,7 @@ class Up2k(object):
|
||||
abspath: str,
|
||||
tags: dict[str, Union[str, float]],
|
||||
) -> int:
|
||||
"""mutex me"""
|
||||
"""mutex(main) me"""
|
||||
assert self.mtag
|
||||
|
||||
if not bos.path.isfile(abspath):
|
||||
@@ -2155,6 +2264,46 @@ class Up2k(object):
|
||||
def _trace(self, msg: str) -> None:
|
||||
self.log("ST: {}".format(msg))
|
||||
|
||||
def _open_db_wd(self, db_path: str) -> "sqlite3.Cursor":
|
||||
ok: list[int] = []
|
||||
Daemon(self._open_db_timeout, "opendb_watchdog", [db_path, ok])
|
||||
try:
|
||||
return self._open_db(db_path)
|
||||
finally:
|
||||
ok.append(1)
|
||||
|
||||
def _open_db_timeout(self, db_path, ok: list[int]) -> None:
|
||||
# give it plenty of time due to the count statement (and wisdom from byte's box)
|
||||
for _ in range(60):
|
||||
time.sleep(1)
|
||||
if ok:
|
||||
return
|
||||
|
||||
t = "WARNING:\n\n initializing an up2k database is taking longer than one minute; something has probably gone wrong:\n\n"
|
||||
self._log_sqlite_incompat(db_path, t)
|
||||
|
||||
def _log_sqlite_incompat(self, db_path, t0) -> None:
|
||||
txt = t0 or ""
|
||||
digest = hashlib.sha512(db_path.encode("utf-8", "replace")).digest()
|
||||
stackname = base64.urlsafe_b64encode(digest[:9]).decode("utf-8")
|
||||
stackpath = os.path.join(E.cfg, "stack-%s.txt" % (stackname,))
|
||||
|
||||
t = " the filesystem at %s may not support locking, or is otherwise incompatible with sqlite\n\n %s\n\n"
|
||||
t += " PS: if you think this is a bug and wish to report it, please include your configuration + the following file: %s\n"
|
||||
txt += t % (db_path, HINT_HISTPATH, stackpath)
|
||||
self.log(txt, 3)
|
||||
|
||||
try:
|
||||
stk = alltrace()
|
||||
with open(stackpath, "wb") as f:
|
||||
f.write(stk.encode("utf-8", "replace"))
|
||||
except Exception as ex:
|
||||
self.log("warning: failed to write %s: %s" % (stackpath, ex), 3)
|
||||
|
||||
if self.args.q:
|
||||
t = "-" * 72
|
||||
raise Exception("%s\n%s\n%s" % (t, txt, t))
|
||||
|
||||
def _orz(self, db_path: str) -> "sqlite3.Cursor":
|
||||
c = sqlite3.connect(
|
||||
db_path, timeout=self.timeout, check_same_thread=False
|
||||
@@ -2167,7 +2316,7 @@ class Up2k(object):
|
||||
cur = self._orz(db_path)
|
||||
ver = self._read_ver(cur)
|
||||
if not existed and ver is None:
|
||||
return self._create_db(db_path, cur)
|
||||
return self._try_create_db(db_path, cur)
|
||||
|
||||
if ver == 4:
|
||||
try:
|
||||
@@ -2205,8 +2354,16 @@ class Up2k(object):
|
||||
db = cur.connection
|
||||
cur.close()
|
||||
db.close()
|
||||
bos.unlink(db_path)
|
||||
return self._create_db(db_path, None)
|
||||
self._delete_db(db_path)
|
||||
return self._try_create_db(db_path, None)
|
||||
|
||||
def _delete_db(self, db_path: str):
|
||||
for suf in ("", "-shm", "-wal", "-journal"):
|
||||
try:
|
||||
bos.unlink(db_path + suf)
|
||||
except:
|
||||
if not suf:
|
||||
raise
|
||||
|
||||
def _backup_db(
|
||||
self, db_path: str, cur: "sqlite3.Cursor", ver: Optional[int], msg: str
|
||||
@@ -2243,6 +2400,18 @@ class Up2k(object):
|
||||
return int(rows[0][0])
|
||||
return None
|
||||
|
||||
def _try_create_db(
|
||||
self, db_path: str, cur: Optional["sqlite3.Cursor"]
|
||||
) -> "sqlite3.Cursor":
|
||||
try:
|
||||
return self._create_db(db_path, cur)
|
||||
except:
|
||||
try:
|
||||
self._delete_db(db_path)
|
||||
except:
|
||||
pass
|
||||
raise
|
||||
|
||||
def _create_db(
|
||||
self, db_path: str, cur: Optional["sqlite3.Cursor"]
|
||||
) -> "sqlite3.Cursor":
|
||||
@@ -2346,28 +2515,36 @@ class Up2k(object):
|
||||
|
||||
cur.connection.commit()
|
||||
|
||||
def _job_volchk(self, cj: dict[str, Any]) -> None:
|
||||
if not self.register_vpath(cj["ptop"], cj["vcfg"]):
|
||||
if cj["ptop"] not in self.registry:
|
||||
raise Pebkac(410, "location unavailable")
|
||||
|
||||
def handle_json(self, cj: dict[str, Any], busy_aps: set[str]) -> dict[str, Any]:
|
||||
def handle_json(
|
||||
self, cj: dict[str, Any], busy_aps: dict[str, int]
|
||||
) -> dict[str, Any]:
|
||||
# busy_aps is u2fh (always undefined if -j0) so this is safe
|
||||
self.busy_aps = busy_aps
|
||||
got_lock = False
|
||||
try:
|
||||
# bit expensive; 3.9=10x 3.11=2x
|
||||
if self.mutex.acquire(timeout=10):
|
||||
self._job_volchk(cj)
|
||||
self.mutex.release()
|
||||
got_lock = True
|
||||
with self.reg_mutex:
|
||||
return self._handle_json(cj)
|
||||
else:
|
||||
t = "cannot receive uploads right now;\nserver busy with {}.\nPlease wait; the client will retry..."
|
||||
raise Pebkac(503, t.format(self.blocked or "[unknown]"))
|
||||
except TypeError:
|
||||
if not PY2:
|
||||
raise
|
||||
with self.mutex:
|
||||
self._job_volchk(cj)
|
||||
with self.mutex, self.reg_mutex:
|
||||
return self._handle_json(cj)
|
||||
finally:
|
||||
if got_lock:
|
||||
self.mutex.release()
|
||||
|
||||
def _handle_json(self, cj: dict[str, Any]) -> dict[str, Any]:
|
||||
ptop = cj["ptop"]
|
||||
if not self.register_vpath(ptop, cj["vcfg"]):
|
||||
if ptop not in self.registry:
|
||||
raise Pebkac(410, "location unavailable")
|
||||
|
||||
cj["name"] = sanitize_fn(cj["name"], "", [".prologue.html", ".epilogue.html"])
|
||||
cj["poke"] = now = self.db_act = self.vol_act[ptop] = time.time()
|
||||
wark = self._get_wark(cj)
|
||||
@@ -2382,7 +2559,7 @@ class Up2k(object):
|
||||
# refuse out-of-order / multithreaded uploading if sprs False
|
||||
sprs = self.fstab.get(pdir) != "ng"
|
||||
|
||||
with self.mutex:
|
||||
if True:
|
||||
jcur = self.cur.get(ptop)
|
||||
reg = self.registry[ptop]
|
||||
vfs = self.asrv.vfs.all_vols[cj["vtop"]]
|
||||
@@ -2601,6 +2778,9 @@ class Up2k(object):
|
||||
a = [job[x] for x in zs.split()]
|
||||
self.db_add(cur, vfs.flags, *a)
|
||||
cur.connection.commit()
|
||||
elif wark in reg:
|
||||
# checks out, but client may have hopped IPs
|
||||
job["addr"] = cj["addr"]
|
||||
|
||||
if not job:
|
||||
ap1 = djoin(cj["ptop"], cj["prel"])
|
||||
@@ -2817,7 +2997,7 @@ class Up2k(object):
|
||||
def handle_chunk(
|
||||
self, ptop: str, wark: str, chash: str
|
||||
) -> tuple[int, list[int], str, float, bool]:
|
||||
with self.mutex:
|
||||
with self.mutex, self.reg_mutex:
|
||||
self.db_act = self.vol_act[ptop] = time.time()
|
||||
job = self.registry[ptop].get(wark)
|
||||
if not job:
|
||||
@@ -2860,7 +3040,7 @@ class Up2k(object):
|
||||
return chunksize, ofs, path, job["lmod"], job["sprs"]
|
||||
|
||||
def release_chunk(self, ptop: str, wark: str, chash: str) -> bool:
|
||||
with self.mutex:
|
||||
with self.reg_mutex:
|
||||
job = self.registry[ptop].get(wark)
|
||||
if job:
|
||||
job["busy"].pop(chash, None)
|
||||
@@ -2868,7 +3048,7 @@ class Up2k(object):
|
||||
return True
|
||||
|
||||
def confirm_chunk(self, ptop: str, wark: str, chash: str) -> tuple[int, str]:
|
||||
with self.mutex:
|
||||
with self.mutex, self.reg_mutex:
|
||||
self.db_act = self.vol_act[ptop] = time.time()
|
||||
try:
|
||||
job = self.registry[ptop][wark]
|
||||
@@ -2891,16 +3071,16 @@ class Up2k(object):
|
||||
|
||||
if self.args.nw:
|
||||
self.regdrop(ptop, wark)
|
||||
return ret, dst
|
||||
|
||||
return ret, dst
|
||||
|
||||
def finish_upload(self, ptop: str, wark: str, busy_aps: set[str]) -> None:
|
||||
self.busy_aps = busy_aps
|
||||
with self.mutex:
|
||||
with self.mutex, self.reg_mutex:
|
||||
self._finish_upload(ptop, wark)
|
||||
|
||||
def _finish_upload(self, ptop: str, wark: str) -> None:
|
||||
"""mutex(main,reg) me"""
|
||||
try:
|
||||
job = self.registry[ptop][wark]
|
||||
pdir = djoin(job["ptop"], job["prel"])
|
||||
@@ -2913,12 +3093,11 @@ class Up2k(object):
|
||||
t = "finish_upload {} with remaining chunks {}"
|
||||
raise Pebkac(500, t.format(wark, job["need"]))
|
||||
|
||||
# self.log("--- " + wark + " " + dst + " finish_upload atomic " + dst, 4)
|
||||
atomic_move(src, dst)
|
||||
|
||||
upt = job.get("at") or time.time()
|
||||
vflags = self.flags[ptop]
|
||||
|
||||
atomic_move(self.log, src, dst, vflags)
|
||||
|
||||
times = (int(time.time()), int(job["lmod"]))
|
||||
self.log(
|
||||
"no more chunks, setting times {} ({}) on {}".format(
|
||||
@@ -2974,6 +3153,7 @@ class Up2k(object):
|
||||
cur.connection.commit()
|
||||
|
||||
def regdrop(self, ptop: str, wark: str) -> None:
|
||||
"""mutex(main,reg) me"""
|
||||
olds = self.droppable[ptop]
|
||||
if wark:
|
||||
olds.append(wark)
|
||||
@@ -3034,7 +3214,7 @@ class Up2k(object):
|
||||
raise
|
||||
|
||||
if "e2t" in self.flags[ptop]:
|
||||
self.tagq.put((ptop, wark, rd, fn, ip, at))
|
||||
self.tagq.put((ptop, wark, rd, fn, sz, ip, at))
|
||||
self.n_tagq += 1
|
||||
|
||||
return True
|
||||
@@ -3068,16 +3248,23 @@ class Up2k(object):
|
||||
at: float,
|
||||
skip_xau: bool = False,
|
||||
) -> None:
|
||||
"""mutex(main) me"""
|
||||
self.db_rm(db, rd, fn, sz)
|
||||
|
||||
if not ip:
|
||||
db_ip = ""
|
||||
else:
|
||||
# plugins may expect this to look like an actual IP
|
||||
db_ip = "1.1.1.1" if self.args.no_db_ip else ip
|
||||
|
||||
sql = "insert into up values (?,?,?,?,?,?,?)"
|
||||
v = (wark, int(ts), sz, rd, fn, ip or "", int(at or 0))
|
||||
v = (wark, int(ts), sz, rd, fn, db_ip, int(at or 0))
|
||||
try:
|
||||
db.execute(sql, v)
|
||||
except:
|
||||
assert self.mem_cur
|
||||
rd, fn = s3enc(self.mem_cur, rd, fn)
|
||||
v = (wark, int(ts), sz, rd, fn, ip or "", int(at or 0))
|
||||
v = (wark, int(ts), sz, rd, fn, db_ip, int(at or 0))
|
||||
db.execute(sql, v)
|
||||
|
||||
self.volsize[db] += sz
|
||||
@@ -3137,7 +3324,13 @@ class Up2k(object):
|
||||
pass
|
||||
|
||||
def handle_rm(
|
||||
self, uname: str, ip: str, vpaths: list[str], lim: list[int], rm_up: bool
|
||||
self,
|
||||
uname: str,
|
||||
ip: str,
|
||||
vpaths: list[str],
|
||||
lim: list[int],
|
||||
rm_up: bool,
|
||||
unpost: bool,
|
||||
) -> str:
|
||||
n_files = 0
|
||||
ok = {}
|
||||
@@ -3147,7 +3340,7 @@ class Up2k(object):
|
||||
self.log("hit delete limit of {} files".format(lim[1]), 3)
|
||||
break
|
||||
|
||||
a, b, c = self._handle_rm(uname, ip, vp, lim, rm_up)
|
||||
a, b, c = self._handle_rm(uname, ip, vp, lim, rm_up, unpost)
|
||||
n_files += a
|
||||
for k in b:
|
||||
ok[k] = 1
|
||||
@@ -3161,25 +3354,46 @@ class Up2k(object):
|
||||
return "deleted {} files (and {}/{} folders)".format(n_files, iok, iok + ing)
|
||||
|
||||
def _handle_rm(
|
||||
self, uname: str, ip: str, vpath: str, lim: list[int], rm_up: bool
|
||||
self, uname: str, ip: str, vpath: str, lim: list[int], rm_up: bool, unpost: bool
|
||||
) -> tuple[int, list[str], list[str]]:
|
||||
self.db_act = time.time()
|
||||
try:
|
||||
partial = ""
|
||||
if not unpost:
|
||||
permsets = [[True, False, False, True]]
|
||||
vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0])
|
||||
vn, rem = vn.get_dbv(rem)
|
||||
unpost = False
|
||||
except:
|
||||
else:
|
||||
# unpost with missing permissions? verify with db
|
||||
if not self.args.unpost:
|
||||
raise Pebkac(400, "the unpost feature is disabled in server config")
|
||||
|
||||
unpost = True
|
||||
permsets = [[False, True]]
|
||||
vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0])
|
||||
vn, rem = vn.get_dbv(rem)
|
||||
with self.mutex:
|
||||
_, _, _, _, dip, dat = self._find_from_vpath(vn.realpath, rem)
|
||||
ptop = vn.realpath
|
||||
with self.mutex, self.reg_mutex:
|
||||
abrt_cfg = self.flags.get(ptop, {}).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 ""
|
||||
reg = self.registry.get(ptop, {}) if abrt_cfg else {}
|
||||
for wark, job in reg.items():
|
||||
if (user and user != job["user"]) or (addr and addr != job["addr"]):
|
||||
continue
|
||||
jrem = djoin(job["prel"], job["name"])
|
||||
if ANYWIN:
|
||||
jrem = jrem.replace("\\", "/")
|
||||
if jrem == rem:
|
||||
if job["ptop"] != ptop:
|
||||
t = "job.ptop [%s] != vol.ptop [%s] ??"
|
||||
raise Exception(t % (job["ptop"] != ptop))
|
||||
partial = vn.canonical(vjoin(job["prel"], job["tnam"]))
|
||||
break
|
||||
if partial:
|
||||
dip = ip
|
||||
dat = time.time()
|
||||
else:
|
||||
if not self.args.unpost:
|
||||
t = "the unpost feature is disabled in server config"
|
||||
raise Pebkac(400, t)
|
||||
|
||||
_, _, _, _, dip, dat = self._find_from_vpath(ptop, rem)
|
||||
|
||||
t = "you cannot delete this: "
|
||||
if not dip:
|
||||
@@ -3261,7 +3475,7 @@ class Up2k(object):
|
||||
continue
|
||||
|
||||
n_files += 1
|
||||
with self.mutex:
|
||||
with self.mutex, self.reg_mutex:
|
||||
cur = None
|
||||
try:
|
||||
ptop = dbv.realpath
|
||||
@@ -3272,6 +3486,9 @@ class Up2k(object):
|
||||
cur.connection.commit()
|
||||
|
||||
wunlink(self.log, abspath, dbv.flags)
|
||||
if partial:
|
||||
wunlink(self.log, partial, dbv.flags)
|
||||
partial = ""
|
||||
if xad:
|
||||
runhook(
|
||||
self.log,
|
||||
@@ -3376,6 +3593,7 @@ class Up2k(object):
|
||||
def _mv_file(
|
||||
self, uname: str, svp: str, dvp: str, curs: set["sqlite3.Cursor"]
|
||||
) -> str:
|
||||
"""mutex(main) me; will mutex(reg)"""
|
||||
svn, srem = self.asrv.vfs.get(svp, uname, True, False, True)
|
||||
svn, srem = svn.get_dbv(srem)
|
||||
|
||||
@@ -3456,7 +3674,9 @@ class Up2k(object):
|
||||
if c2 and c2 != c1:
|
||||
self._copy_tags(c1, c2, w)
|
||||
|
||||
has_dupes = self._forget_file(svn.realpath, srem, c1, w, is_xvol, fsize)
|
||||
with self.reg_mutex:
|
||||
has_dupes = self._forget_file(svn.realpath, srem, c1, w, is_xvol, fsize)
|
||||
|
||||
if not is_xvol:
|
||||
has_dupes = self._relink(w, svn.realpath, srem, dabs)
|
||||
|
||||
@@ -3495,7 +3715,7 @@ class Up2k(object):
|
||||
self._symlink(dlink, dabs, dvn.flags, lmod=ftime)
|
||||
wunlink(self.log, sabs, svn.flags)
|
||||
else:
|
||||
atomic_move(sabs, dabs)
|
||||
atomic_move(self.log, sabs, dabs, svn.flags)
|
||||
|
||||
except OSError as ex:
|
||||
if ex.errno != errno.EXDEV:
|
||||
@@ -3586,7 +3806,10 @@ class Up2k(object):
|
||||
drop_tags: bool,
|
||||
sz: int,
|
||||
) -> bool:
|
||||
"""forgets file in db, fixes symlinks, does not delete"""
|
||||
"""
|
||||
mutex(main,reg) me
|
||||
forgets file in db, fixes symlinks, does not delete
|
||||
"""
|
||||
srd, sfn = vsplit(vrem)
|
||||
has_dupes = False
|
||||
self.log("forgetting {}".format(vrem))
|
||||
@@ -3616,9 +3839,10 @@ class Up2k(object):
|
||||
)
|
||||
job = reg.get(wark) if wark else None
|
||||
if job:
|
||||
t = "forgetting partial upload {} ({})"
|
||||
p = self._vis_job_progress(job)
|
||||
self.log(t.format(wark, p))
|
||||
if job["need"]:
|
||||
t = "forgetting partial upload {} ({})"
|
||||
p = self._vis_job_progress(job)
|
||||
self.log(t.format(wark, p))
|
||||
assert wark
|
||||
del reg[wark]
|
||||
|
||||
@@ -3671,8 +3895,7 @@ class Up2k(object):
|
||||
self.log("linkswap [{}] and [{}]".format(sabs, slabs))
|
||||
mt = bos.path.getmtime(slabs, False)
|
||||
flags = self.flags.get(ptop) or {}
|
||||
wunlink(self.log, slabs, flags)
|
||||
bos.rename(sabs, slabs)
|
||||
atomic_move(self.log, sabs, slabs, flags)
|
||||
bos.utime(slabs, (int(time.time()), int(mt)), False)
|
||||
self._symlink(slabs, sabs, flags, False)
|
||||
full[slabs] = (ptop, rem)
|
||||
@@ -3761,7 +3984,7 @@ class Up2k(object):
|
||||
csz = up2k_chunksize(fsz)
|
||||
ret = []
|
||||
suffix = " MB, {}".format(path)
|
||||
with open(fsenc(path), "rb", 512 * 1024) as f:
|
||||
with open(fsenc(path), "rb", self.args.iobuf) as f:
|
||||
if self.mth and fsz >= 1024 * 512:
|
||||
tlt = self.mth.hash(f, fsz, csz, self.pp, prefix, suffix)
|
||||
ret = [x[0] for x in tlt]
|
||||
@@ -3866,7 +4089,13 @@ class Up2k(object):
|
||||
|
||||
if not ANYWIN and sprs and sz > 1024 * 1024:
|
||||
fs = self.fstab.get(pdir)
|
||||
if fs != "ok":
|
||||
if fs == "ok":
|
||||
pass
|
||||
elif "sparse" in self.flags[job["ptop"]]:
|
||||
t = "volflag 'sparse' is forcing use of sparse files for uploads to [%s]"
|
||||
self.log(t % (job["ptop"],))
|
||||
relabel = True
|
||||
else:
|
||||
relabel = True
|
||||
f.seek(1024 * 1024 - 1)
|
||||
f.write(b"e")
|
||||
@@ -3905,7 +4134,7 @@ class Up2k(object):
|
||||
self.do_snapshot()
|
||||
|
||||
def do_snapshot(self) -> None:
|
||||
with self.mutex:
|
||||
with self.mutex, self.reg_mutex:
|
||||
for k, reg in self.registry.items():
|
||||
self._snap_reg(k, reg)
|
||||
|
||||
@@ -3973,11 +4202,11 @@ class Up2k(object):
|
||||
|
||||
path2 = "{}.{}".format(path, os.getpid())
|
||||
body = {"droppable": self.droppable[ptop], "registry": reg}
|
||||
j = json.dumps(body, indent=2, sort_keys=True).encode("utf-8")
|
||||
j = json.dumps(body, sort_keys=True, separators=(",\n", ": ")).encode("utf-8")
|
||||
with gzip.GzipFile(path2, "wb") as f:
|
||||
f.write(j)
|
||||
|
||||
atomic_move(path2, path)
|
||||
atomic_move(self.log, path2, path, VF_CAREFUL)
|
||||
|
||||
self.log("snap: {} |{}|".format(path, len(reg.keys())))
|
||||
self.snap_prev[ptop] = etag
|
||||
@@ -3991,14 +4220,14 @@ class Up2k(object):
|
||||
with self.mutex:
|
||||
self.n_tagq -= 1
|
||||
|
||||
ptop, wark, rd, fn, ip, at = self.tagq.get()
|
||||
ptop, wark, rd, fn, sz, ip, at = self.tagq.get()
|
||||
if "e2t" not in self.flags[ptop]:
|
||||
continue
|
||||
|
||||
# self.log("\n " + repr([ptop, rd, fn]))
|
||||
abspath = djoin(ptop, rd, fn)
|
||||
try:
|
||||
tags = self.mtag.get(abspath)
|
||||
tags = self.mtag.get(abspath) if sz else {}
|
||||
ntags1 = len(tags)
|
||||
parsers = self._get_parsers(ptop, tags, abspath)
|
||||
if self.args.mtag_vv:
|
||||
@@ -4046,7 +4275,7 @@ class Up2k(object):
|
||||
raise Exception("invalid hash task")
|
||||
|
||||
try:
|
||||
if not self._hash_t(task):
|
||||
if not self._hash_t(task) and self.stop:
|
||||
return
|
||||
except Exception as ex:
|
||||
self.log("failed to hash %s: %s" % (task, ex), 1)
|
||||
@@ -4056,7 +4285,7 @@ class Up2k(object):
|
||||
) -> bool:
|
||||
ptop, vtop, flags, rd, fn, ip, at, usr, skip_xau = task
|
||||
# self.log("hashq {} pop {}/{}/{}".format(self.n_hashq, ptop, rd, fn))
|
||||
with self.mutex:
|
||||
with self.mutex, self.reg_mutex:
|
||||
if not self.register_vpath(ptop, flags):
|
||||
return True
|
||||
|
||||
@@ -4074,7 +4303,7 @@ class Up2k(object):
|
||||
|
||||
wark = up2k_wark_from_hashlist(self.salt, inf.st_size, hashes)
|
||||
|
||||
with self.mutex:
|
||||
with self.mutex, self.reg_mutex:
|
||||
self.idx_wark(
|
||||
self.flags[ptop],
|
||||
rd,
|
||||
|
||||
@@ -186,7 +186,7 @@ else:
|
||||
|
||||
SYMTIME = sys.version_info > (3, 6) and os.utime in os.supports_follow_symlinks
|
||||
|
||||
META_NOBOTS = '<meta name="robots" content="noindex, nofollow">'
|
||||
META_NOBOTS = '<meta name="robots" content="noindex, nofollow">\n'
|
||||
|
||||
FFMPEG_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z"
|
||||
|
||||
@@ -423,16 +423,32 @@ try:
|
||||
except:
|
||||
PYFTPD_VER = "(None)"
|
||||
|
||||
try:
|
||||
from partftpy.__init__ import __version__ as PARTFTPY_VER
|
||||
except:
|
||||
PARTFTPY_VER = "(None)"
|
||||
|
||||
|
||||
PY_DESC = py_desc()
|
||||
|
||||
VERSIONS = "copyparty v{} ({})\n{}\n sqlite v{} | jinja v{} | pyftpd v{}".format(
|
||||
S_VERSION, S_BUILD_DT, PY_DESC, SQLITE_VER, JINJA_VER, PYFTPD_VER
|
||||
VERSIONS = (
|
||||
"copyparty v{} ({})\n{}\n sqlite {} | jinja {} | pyftpd {} | tftp {}".format(
|
||||
S_VERSION, S_BUILD_DT, PY_DESC, SQLITE_VER, JINJA_VER, PYFTPD_VER, PARTFTPY_VER
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
_: Any = (mp, BytesIO, quote, unquote, SQLITE_VER, JINJA_VER, PYFTPD_VER)
|
||||
__all__ = ["mp", "BytesIO", "quote", "unquote", "SQLITE_VER", "JINJA_VER", "PYFTPD_VER"]
|
||||
_: Any = (mp, BytesIO, quote, unquote, SQLITE_VER, JINJA_VER, PYFTPD_VER, PARTFTPY_VER)
|
||||
__all__ = [
|
||||
"mp",
|
||||
"BytesIO",
|
||||
"quote",
|
||||
"unquote",
|
||||
"SQLITE_VER",
|
||||
"JINJA_VER",
|
||||
"PYFTPD_VER",
|
||||
"PARTFTPY_VER",
|
||||
]
|
||||
|
||||
|
||||
class Daemon(threading.Thread):
|
||||
@@ -536,25 +552,33 @@ class HLog(logging.Handler):
|
||||
elif record.name.startswith("impacket"):
|
||||
if self.ptn_smb_ign.match(msg):
|
||||
return
|
||||
elif record.name.startswith("partftpy."):
|
||||
record.name = record.name[9:]
|
||||
|
||||
self.log_func(record.name[-21:], msg, c)
|
||||
|
||||
|
||||
class NetMap(object):
|
||||
def __init__(self, ips: list[str], netdevs: dict[str, Netdev]) -> None:
|
||||
def __init__(self, ips: list[str], cidrs: list[str], keep_lo=False) -> None:
|
||||
"""
|
||||
ips: list of plain ipv4/ipv6 IPs, not cidr
|
||||
cidrs: list of cidr-notation IPs (ip/prefix)
|
||||
"""
|
||||
if "::" in ips:
|
||||
ips = [x for x in ips if x != "::"] + list(
|
||||
[x.split("/")[0] for x in netdevs if ":" in x]
|
||||
[x.split("/")[0] for x in cidrs if ":" in x]
|
||||
)
|
||||
ips.append("0.0.0.0")
|
||||
|
||||
if "0.0.0.0" in ips:
|
||||
ips = [x for x in ips if x != "0.0.0.0"] + list(
|
||||
[x.split("/")[0] for x in netdevs if ":" not in x]
|
||||
[x.split("/")[0] for x in cidrs if ":" not in x]
|
||||
)
|
||||
|
||||
ips = [x for x in ips if x not in ("::1", "127.0.0.1")]
|
||||
ips = find_prefix(ips, netdevs)
|
||||
if not keep_lo:
|
||||
ips = [x for x in ips if x not in ("::1", "127.0.0.1")]
|
||||
|
||||
ips = find_prefix(ips, cidrs)
|
||||
|
||||
self.cache: dict[str, str] = {}
|
||||
self.b2sip: dict[bytes, str] = {}
|
||||
@@ -571,6 +595,9 @@ class NetMap(object):
|
||||
self.bip.sort(reverse=True)
|
||||
|
||||
def map(self, ip: str) -> str:
|
||||
if ip.startswith("::ffff:"):
|
||||
ip = ip[7:]
|
||||
|
||||
try:
|
||||
return self.cache[ip]
|
||||
except:
|
||||
@@ -732,15 +759,46 @@ class CachedSet(object):
|
||||
self.oldest = now
|
||||
|
||||
|
||||
class CachedDict(object):
|
||||
def __init__(self, maxage: float) -> None:
|
||||
self.c: dict[str, tuple[float, Any]] = {}
|
||||
self.maxage = maxage
|
||||
self.oldest = 0.0
|
||||
|
||||
def set(self, k: str, v: Any) -> None:
|
||||
now = time.time()
|
||||
self.c[k] = (now, v)
|
||||
if now - self.oldest < self.maxage:
|
||||
return
|
||||
|
||||
c = self.c = {k: v for k, v in self.c.items() if now - v[0] < self.maxage}
|
||||
try:
|
||||
self.oldest = min([x[0] for x in c.values()])
|
||||
except:
|
||||
self.oldest = now
|
||||
|
||||
def get(self, k: str) -> Optional[tuple[str, Any]]:
|
||||
try:
|
||||
ts, ret = self.c[k]
|
||||
now = time.time()
|
||||
if now - ts > self.maxage:
|
||||
del self.c[k]
|
||||
return None
|
||||
return ret
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
class FHC(object):
|
||||
class CE(object):
|
||||
def __init__(self, fh: typing.BinaryIO) -> None:
|
||||
self.ts: float = 0
|
||||
self.fhs = [fh]
|
||||
self.all_fhs = set([fh])
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.cache: dict[str, FHC.CE] = {}
|
||||
self.aps: set[str] = set()
|
||||
self.aps: dict[str, int] = {}
|
||||
|
||||
def close(self, path: str) -> None:
|
||||
try:
|
||||
@@ -752,7 +810,7 @@ class FHC(object):
|
||||
fh.close()
|
||||
|
||||
del self.cache[path]
|
||||
self.aps.remove(path)
|
||||
del self.aps[path]
|
||||
|
||||
def clean(self) -> None:
|
||||
if not self.cache:
|
||||
@@ -773,9 +831,12 @@ class FHC(object):
|
||||
return self.cache[path].fhs.pop()
|
||||
|
||||
def put(self, path: str, fh: typing.BinaryIO) -> None:
|
||||
self.aps.add(path)
|
||||
if path not in self.aps:
|
||||
self.aps[path] = 0
|
||||
|
||||
try:
|
||||
ce = self.cache[path]
|
||||
ce.all_fhs.add(fh)
|
||||
ce.fhs.append(fh)
|
||||
except:
|
||||
ce = self.CE(fh)
|
||||
@@ -1373,10 +1434,15 @@ def ren_open(
|
||||
|
||||
class MultipartParser(object):
|
||||
def __init__(
|
||||
self, log_func: "NamedLogger", sr: Unrecv, http_headers: dict[str, str]
|
||||
self,
|
||||
log_func: "NamedLogger",
|
||||
args: argparse.Namespace,
|
||||
sr: Unrecv,
|
||||
http_headers: dict[str, str],
|
||||
):
|
||||
self.sr = sr
|
||||
self.log = log_func
|
||||
self.args = args
|
||||
self.headers = http_headers
|
||||
|
||||
self.re_ctype = re.compile(r"^content-type: *([^; ]+)", re.IGNORECASE)
|
||||
@@ -1475,7 +1541,7 @@ class MultipartParser(object):
|
||||
|
||||
def _read_data(self) -> Generator[bytes, None, None]:
|
||||
blen = len(self.boundary)
|
||||
bufsz = 32 * 1024
|
||||
bufsz = self.args.s_rd_sz
|
||||
while True:
|
||||
try:
|
||||
buf = self.sr.recv(bufsz)
|
||||
@@ -1750,7 +1816,7 @@ def get_spd(nbyte: int, t0: float, t: Optional[float] = None) -> str:
|
||||
if t is None:
|
||||
t = time.time()
|
||||
|
||||
bps = nbyte / ((t - t0) + 0.001)
|
||||
bps = nbyte / ((t - t0) or 0.001)
|
||||
s1 = humansize(nbyte).replace(" ", "\033[33m").replace("iB", "")
|
||||
s2 = humansize(bps).replace(" ", "\033[35m").replace("iB", "")
|
||||
return "%s \033[0m%s/s\033[0m" % (s1, s2)
|
||||
@@ -1902,10 +1968,10 @@ def ipnorm(ip: str) -> str:
|
||||
return ip
|
||||
|
||||
|
||||
def find_prefix(ips: list[str], netdevs: dict[str, Netdev]) -> list[str]:
|
||||
def find_prefix(ips: list[str], cidrs: list[str]) -> list[str]:
|
||||
ret = []
|
||||
for ip in ips:
|
||||
hit = next((x for x in netdevs if x.startswith(ip + "/")), None)
|
||||
hit = next((x for x in cidrs if x.startswith(ip + "/") or ip == x), None)
|
||||
if hit:
|
||||
ret.append(hit)
|
||||
return ret
|
||||
@@ -2093,26 +2159,29 @@ def lsof(log: "NamedLogger", abspath: str) -> None:
|
||||
log("lsof failed; " + min_ex(), 3)
|
||||
|
||||
|
||||
def atomic_move(usrc: str, udst: str) -> None:
|
||||
src = fsenc(usrc)
|
||||
dst = fsenc(udst)
|
||||
if not PY2:
|
||||
os.replace(src, dst)
|
||||
def _fs_mvrm(
|
||||
log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any]
|
||||
) -> bool:
|
||||
bsrc = fsenc(src)
|
||||
bdst = fsenc(dst)
|
||||
if atomic:
|
||||
k = "mv_re_"
|
||||
act = "atomic-rename"
|
||||
osfun = os.replace
|
||||
args = [bsrc, bdst]
|
||||
elif dst:
|
||||
k = "mv_re_"
|
||||
act = "rename"
|
||||
osfun = os.rename
|
||||
args = [bsrc, bdst]
|
||||
else:
|
||||
if os.path.exists(dst):
|
||||
os.unlink(dst)
|
||||
k = "rm_re_"
|
||||
act = "delete"
|
||||
osfun = os.unlink
|
||||
args = [bsrc]
|
||||
|
||||
os.rename(src, dst)
|
||||
|
||||
|
||||
def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
|
||||
maxtime = flags.get("rm_re_t", 0.0)
|
||||
bpath = fsenc(abspath)
|
||||
if not maxtime:
|
||||
os.unlink(bpath)
|
||||
return True
|
||||
|
||||
chill = flags.get("rm_re_r", 0.0)
|
||||
maxtime = flags.get(k + "t", 0.0)
|
||||
chill = flags.get(k + "r", 0.0)
|
||||
if chill < 0.001:
|
||||
chill = 0.1
|
||||
|
||||
@@ -2120,14 +2189,19 @@ def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
|
||||
t0 = now = time.time()
|
||||
for attempt in range(90210):
|
||||
try:
|
||||
if ino and os.stat(bpath).st_ino != ino:
|
||||
log("inode changed; aborting delete")
|
||||
if ino and os.stat(bsrc).st_ino != ino:
|
||||
t = "src inode changed; aborting %s %s"
|
||||
log(t % (act, src), 1)
|
||||
return False
|
||||
os.unlink(bpath)
|
||||
if (dst and not atomic) and os.path.exists(bdst):
|
||||
t = "something appeared at dst; aborting rename [%s] ==> [%s]"
|
||||
log(t % (src, dst), 1)
|
||||
return False
|
||||
osfun(*args)
|
||||
if attempt:
|
||||
now = time.time()
|
||||
t = "deleted in %.2f sec, attempt %d"
|
||||
log(t % (now - t0, attempt + 1))
|
||||
t = "%sd in %.2f sec, attempt %d: %s"
|
||||
log(t % (act, now - t0, attempt + 1, src))
|
||||
return True
|
||||
except OSError as ex:
|
||||
now = time.time()
|
||||
@@ -2137,15 +2211,45 @@ def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
|
||||
raise
|
||||
if not attempt:
|
||||
if not PY2:
|
||||
ino = os.stat(bpath).st_ino
|
||||
t = "delete failed (err.%d); retrying for %d sec: %s"
|
||||
log(t % (ex.errno, maxtime + 0.99, abspath))
|
||||
ino = os.stat(bsrc).st_ino
|
||||
t = "%s failed (err.%d); retrying for %d sec: [%s]"
|
||||
log(t % (act, ex.errno, maxtime + 0.99, src))
|
||||
|
||||
time.sleep(chill)
|
||||
|
||||
return False # makes pylance happy
|
||||
|
||||
|
||||
def atomic_move(log: "NamedLogger", src: str, dst: str, flags: dict[str, Any]) -> None:
|
||||
bsrc = fsenc(src)
|
||||
bdst = fsenc(dst)
|
||||
if PY2:
|
||||
if os.path.exists(bdst):
|
||||
_fs_mvrm(log, dst, "", False, flags) # unlink
|
||||
|
||||
_fs_mvrm(log, src, dst, False, flags) # rename
|
||||
elif flags.get("mv_re_t"):
|
||||
_fs_mvrm(log, src, dst, True, flags)
|
||||
else:
|
||||
os.replace(bsrc, bdst)
|
||||
|
||||
|
||||
def wrename(log: "NamedLogger", src: str, dst: str, flags: dict[str, Any]) -> bool:
|
||||
if not flags.get("mv_re_t"):
|
||||
os.rename(fsenc(src), fsenc(dst))
|
||||
return True
|
||||
|
||||
return _fs_mvrm(log, src, dst, False, flags)
|
||||
|
||||
|
||||
def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
|
||||
if not flags.get("rm_re_t"):
|
||||
os.unlink(fsenc(abspath))
|
||||
return True
|
||||
|
||||
return _fs_mvrm(log, abspath, "", False, flags)
|
||||
|
||||
|
||||
def get_df(abspath: str) -> tuple[Optional[int], Optional[int]]:
|
||||
try:
|
||||
# some fuses misbehave
|
||||
@@ -2216,10 +2320,11 @@ def shut_socket(log: "NamedLogger", sck: socket.socket, timeout: int = 3) -> Non
|
||||
sck.close()
|
||||
|
||||
|
||||
def read_socket(sr: Unrecv, total_size: int) -> Generator[bytes, None, None]:
|
||||
def read_socket(
|
||||
sr: Unrecv, bufsz: int, total_size: int
|
||||
) -> Generator[bytes, None, None]:
|
||||
remains = total_size
|
||||
while remains > 0:
|
||||
bufsz = 32 * 1024
|
||||
if bufsz > remains:
|
||||
bufsz = remains
|
||||
|
||||
@@ -2233,16 +2338,16 @@ def read_socket(sr: Unrecv, total_size: int) -> Generator[bytes, None, None]:
|
||||
yield buf
|
||||
|
||||
|
||||
def read_socket_unbounded(sr: Unrecv) -> Generator[bytes, None, None]:
|
||||
def read_socket_unbounded(sr: Unrecv, bufsz: int) -> Generator[bytes, None, None]:
|
||||
try:
|
||||
while True:
|
||||
yield sr.recv(32 * 1024)
|
||||
yield sr.recv(bufsz)
|
||||
except:
|
||||
return
|
||||
|
||||
|
||||
def read_socket_chunked(
|
||||
sr: Unrecv, log: Optional["NamedLogger"] = None
|
||||
sr: Unrecv, bufsz: int, log: Optional["NamedLogger"] = None
|
||||
) -> Generator[bytes, None, None]:
|
||||
err = "upload aborted: expected chunk length, got [{}] |{}| instead"
|
||||
while True:
|
||||
@@ -2276,7 +2381,7 @@ def read_socket_chunked(
|
||||
if log:
|
||||
log("receiving %d byte chunk" % (chunklen,))
|
||||
|
||||
for chunk in read_socket(sr, chunklen):
|
||||
for chunk in read_socket(sr, bufsz, chunklen):
|
||||
yield chunk
|
||||
|
||||
x = sr.recv_ex(2, False)
|
||||
@@ -2299,10 +2404,46 @@ def list_ips() -> list[str]:
|
||||
return list(ret)
|
||||
|
||||
|
||||
def yieldfile(fn: str) -> Generator[bytes, None, None]:
|
||||
with open(fsenc(fn), "rb", 512 * 1024) as f:
|
||||
def build_netmap(csv: str):
|
||||
csv = csv.lower().strip()
|
||||
|
||||
if csv in ("any", "all", "no", ",", ""):
|
||||
return None
|
||||
|
||||
if csv in ("lan", "local", "private", "prvt"):
|
||||
csv = "10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fd00::/8" # lan
|
||||
csv += ", 169.254.0.0/16, fe80::/10" # link-local
|
||||
csv += ", 127.0.0.0/8, ::1/128" # loopback
|
||||
|
||||
srcs = [x.strip() for x in csv.split(",") if x.strip()]
|
||||
cidrs = []
|
||||
for zs in srcs:
|
||||
if not zs.endswith("."):
|
||||
cidrs.append(zs)
|
||||
continue
|
||||
|
||||
# translate old syntax "172.19." => "172.19.0.0/16"
|
||||
words = len(zs.rstrip(".").split("."))
|
||||
if words == 1:
|
||||
zs += "0.0.0/8"
|
||||
elif words == 2:
|
||||
zs += "0.0/16"
|
||||
elif words == 3:
|
||||
zs += "0/24"
|
||||
else:
|
||||
raise Exception("invalid config value [%s]" % (zs,))
|
||||
|
||||
cidrs.append(zs)
|
||||
|
||||
ips = [x.split("/")[0] for x in cidrs]
|
||||
return NetMap(ips, cidrs, True)
|
||||
|
||||
|
||||
def yieldfile(fn: str, bufsz: int) -> Generator[bytes, None, None]:
|
||||
readsz = min(bufsz, 128 * 1024)
|
||||
with open(fsenc(fn), "rb", bufsz) as f:
|
||||
while True:
|
||||
buf = f.read(128 * 1024)
|
||||
buf = f.read(readsz)
|
||||
if not buf:
|
||||
break
|
||||
|
||||
|
||||
@@ -17,8 +17,10 @@ window.baguetteBox = (function () {
|
||||
titleTag: false,
|
||||
async: false,
|
||||
preload: 2,
|
||||
refocus: true,
|
||||
afterShow: null,
|
||||
afterHide: null,
|
||||
duringHide: null,
|
||||
onChange: null,
|
||||
},
|
||||
overlay, slider, btnPrev, btnNext, btnHelp, btnAnim, btnRotL, btnRotR, btnSel, btnFull, btnVmode, btnClose,
|
||||
@@ -27,6 +29,7 @@ window.baguetteBox = (function () {
|
||||
isOverlayVisible = false,
|
||||
touch = {}, // start-pos
|
||||
touchFlag = false, // busy
|
||||
scrollTimer = 0,
|
||||
re_i = /^[^?]+\.(a?png|avif|bmp|gif|heif|jpe?g|jfif|svg|webp)(\?|$)/i,
|
||||
re_v = /^[^?]+\.(webm|mkv|mp4)(\?|$)/i,
|
||||
anims = ['slideIn', 'fadeIn', 'none'],
|
||||
@@ -89,6 +92,30 @@ window.baguetteBox = (function () {
|
||||
touchendHandler();
|
||||
};
|
||||
|
||||
var overlayWheelHandler = function (e) {
|
||||
if (!options.noScrollbars || anymod(e))
|
||||
return;
|
||||
|
||||
ev(e);
|
||||
|
||||
var x = e.deltaX,
|
||||
y = e.deltaY,
|
||||
d = Math.abs(x) > Math.abs(y) ? x : y;
|
||||
|
||||
if (e.deltaMode)
|
||||
d *= 10;
|
||||
|
||||
if (Date.now() - scrollTimer < (Math.abs(d) > 20 ? 100 : 300))
|
||||
return;
|
||||
|
||||
scrollTimer = Date.now();
|
||||
|
||||
if (d > 0)
|
||||
showNextImage();
|
||||
else
|
||||
showPreviousImage();
|
||||
};
|
||||
|
||||
var trapFocusInsideOverlay = function (e) {
|
||||
if (overlay.style.display === 'block' && (overlay.contains && !overlay.contains(e.target))) {
|
||||
e.stopPropagation();
|
||||
@@ -144,7 +171,7 @@ window.baguetteBox = (function () {
|
||||
selectorData.galleries.push(gallery);
|
||||
});
|
||||
|
||||
return selectorData.galleries;
|
||||
return [selectorData.galleries, options];
|
||||
}
|
||||
|
||||
function clearCachedData() {
|
||||
@@ -392,8 +419,7 @@ window.baguetteBox = (function () {
|
||||
}
|
||||
|
||||
function dlpic() {
|
||||
var url = findfile()[3].href;
|
||||
url += (url.indexOf('?') < 0 ? '?' : '&') + 'cache';
|
||||
var url = addq(findfile()[3].href, 'cache');
|
||||
dl_file(url);
|
||||
}
|
||||
|
||||
@@ -450,6 +476,7 @@ window.baguetteBox = (function () {
|
||||
bind(document, 'keyup', keyUpHandler);
|
||||
bind(document, 'fullscreenchange', onFSC);
|
||||
bind(overlay, 'click', overlayClickHandler);
|
||||
bind(overlay, 'wheel', overlayWheelHandler);
|
||||
bind(btnPrev, 'click', showPreviousImage);
|
||||
bind(btnNext, 'click', showNextImage);
|
||||
bind(btnClose, 'click', hideOverlay);
|
||||
@@ -472,6 +499,7 @@ window.baguetteBox = (function () {
|
||||
unbind(document, 'keyup', keyUpHandler);
|
||||
unbind(document, 'fullscreenchange', onFSC);
|
||||
unbind(overlay, 'click', overlayClickHandler);
|
||||
unbind(overlay, 'wheel', overlayWheelHandler);
|
||||
unbind(btnPrev, 'click', showPreviousImage);
|
||||
unbind(btnNext, 'click', showNextImage);
|
||||
unbind(btnClose, 'click', hideOverlay);
|
||||
@@ -582,7 +610,7 @@ window.baguetteBox = (function () {
|
||||
isOverlayVisible = true;
|
||||
}
|
||||
|
||||
function hideOverlay(e) {
|
||||
function hideOverlay(e, dtor) {
|
||||
ev(e);
|
||||
playvid(false);
|
||||
removeFromCache('#files');
|
||||
@@ -590,16 +618,22 @@ window.baguetteBox = (function () {
|
||||
document.documentElement.style.overflowY = 'auto';
|
||||
document.body.style.overflowY = 'auto';
|
||||
}
|
||||
if (overlay.style.display === 'none')
|
||||
|
||||
try {
|
||||
if (document.fullscreenElement)
|
||||
document.exitFullscreen();
|
||||
}
|
||||
catch (ex) { }
|
||||
isFullscreen = false;
|
||||
|
||||
if (dtor || overlay.style.display === 'none')
|
||||
return;
|
||||
|
||||
if (options.duringHide)
|
||||
options.duringHide();
|
||||
|
||||
sethash('');
|
||||
unbindEvents();
|
||||
try {
|
||||
document.exitFullscreen();
|
||||
isFullscreen = false;
|
||||
}
|
||||
catch (ex) { }
|
||||
|
||||
// Fade out and hide the overlay
|
||||
overlay.className = '';
|
||||
@@ -613,7 +647,7 @@ window.baguetteBox = (function () {
|
||||
if (options.afterHide)
|
||||
options.afterHide();
|
||||
|
||||
documentLastFocus && documentLastFocus.focus();
|
||||
options.refocus && documentLastFocus && documentLastFocus.focus();
|
||||
isOverlayVisible = false;
|
||||
unvid();
|
||||
unfig();
|
||||
@@ -677,7 +711,7 @@ window.baguetteBox = (function () {
|
||||
options.captions.call(currentGallery, imageElement) :
|
||||
imageElement.getAttribute('data-caption') || imageElement.title;
|
||||
|
||||
imageSrc += imageSrc.indexOf('?') < 0 ? '?cache' : '&cache';
|
||||
imageSrc = addq(imageSrc, 'cache');
|
||||
|
||||
if (is_vid && index != currentIndex)
|
||||
return; // no preload
|
||||
@@ -715,6 +749,9 @@ window.baguetteBox = (function () {
|
||||
|
||||
figure.appendChild(image);
|
||||
|
||||
if (is_vid && window.afilt)
|
||||
afilt.apply(undefined, image);
|
||||
|
||||
if (options.async && callback)
|
||||
callback();
|
||||
}
|
||||
@@ -1057,6 +1094,7 @@ window.baguetteBox = (function () {
|
||||
}
|
||||
|
||||
function destroyPlugin() {
|
||||
hideOverlay(undefined, true);
|
||||
unbindEvents();
|
||||
clearCachedData();
|
||||
document.getElementsByTagName('body')[0].removeChild(ebi('bbox-overlay'));
|
||||
|
||||
@@ -494,6 +494,7 @@ html.dz {
|
||||
|
||||
text-shadow: none;
|
||||
font-family: 'scp', monospace, monospace;
|
||||
font-family: var(--font-mono), 'scp', monospace, monospace;
|
||||
}
|
||||
html.dy {
|
||||
--fg: #000;
|
||||
@@ -603,6 +604,7 @@ html {
|
||||
color: var(--fg);
|
||||
background: var(--bgg);
|
||||
font-family: sans-serif;
|
||||
font-family: var(--font-main), sans-serif;
|
||||
text-shadow: 1px 1px 0px var(--bg-max);
|
||||
}
|
||||
html, body {
|
||||
@@ -611,6 +613,7 @@ html, body {
|
||||
}
|
||||
pre, code, tt, #doc, #doc>code {
|
||||
font-family: 'scp', monospace, monospace;
|
||||
font-family: var(--font-mono), 'scp', monospace, monospace;
|
||||
}
|
||||
.ayjump {
|
||||
position: fixed;
|
||||
@@ -696,12 +699,12 @@ a:hover {
|
||||
.s0:after,
|
||||
.s1:after {
|
||||
content: '⌄';
|
||||
margin-left: -.1em;
|
||||
margin-left: -.15em;
|
||||
}
|
||||
.s0r:after,
|
||||
.s1r:after {
|
||||
content: '⌃';
|
||||
margin-left: -.1em;
|
||||
margin-left: -.15em;
|
||||
}
|
||||
.s0:after,
|
||||
.s0r:after {
|
||||
@@ -712,7 +715,7 @@ a:hover {
|
||||
color: var(--sort-2);
|
||||
}
|
||||
#files thead th:after {
|
||||
margin-right: -.7em;
|
||||
margin-right: -.5em;
|
||||
}
|
||||
#files tbody tr:hover td,
|
||||
#files tbody tr:hover td+td {
|
||||
@@ -741,6 +744,15 @@ html #files.hhpick thead th {
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
}
|
||||
#files tr.fade a {
|
||||
color: #999;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-style: italic;
|
||||
}
|
||||
html.y #files tr.fade a {
|
||||
color: #999;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
#files tr:nth-child(2n) td {
|
||||
background: var(--row-alt);
|
||||
}
|
||||
@@ -759,6 +771,7 @@ html #files.hhpick thead th {
|
||||
}
|
||||
#files tbody td:nth-child(3) {
|
||||
font-family: 'scp', monospace, monospace;
|
||||
font-family: var(--font-mono), 'scp', monospace, monospace;
|
||||
text-align: right;
|
||||
padding-right: 1em;
|
||||
white-space: nowrap;
|
||||
@@ -821,6 +834,7 @@ html.y #path a:hover {
|
||||
.logue.raw {
|
||||
white-space: pre;
|
||||
font-family: 'scp', 'consolas', monospace;
|
||||
font-family: var(--font-mono), 'scp', 'consolas', monospace;
|
||||
}
|
||||
#doc>iframe,
|
||||
.logue>iframe {
|
||||
@@ -985,6 +999,10 @@ html.y #path a:hover {
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
#ggrid.nocrop>a img {
|
||||
max-height: 20em;
|
||||
max-height: calc(var(--grid-sz)*2);
|
||||
}
|
||||
#ggrid>a.dir:before {
|
||||
content: '📂';
|
||||
}
|
||||
@@ -1151,9 +1169,6 @@ html.y #widget.open {
|
||||
@keyframes spin {
|
||||
100% {transform: rotate(360deg)}
|
||||
}
|
||||
@media (prefers-reduced-motion) {
|
||||
@keyframes spin { }
|
||||
}
|
||||
@keyframes fadein {
|
||||
0% {opacity: 0}
|
||||
100% {opacity: 1}
|
||||
@@ -1247,6 +1262,13 @@ html.y #widget.open {
|
||||
0% {opacity:0}
|
||||
100% {opacity:1}
|
||||
}
|
||||
#ggrid>a.glow {
|
||||
animation: gexit .6s ease-out;
|
||||
}
|
||||
@keyframes gexit {
|
||||
0% {box-shadow: 0 0 0 2em var(--a)}
|
||||
100% {box-shadow: 0 0 0em 0em var(--a)}
|
||||
}
|
||||
#wzip a {
|
||||
font-size: .4em;
|
||||
margin: -.3em .1em;
|
||||
@@ -1409,6 +1431,7 @@ input[type="checkbox"]:checked+label {
|
||||
}
|
||||
html.dz input {
|
||||
font-family: 'scp', monospace, monospace;
|
||||
font-family: var(--font-mono), 'scp', monospace, monospace;
|
||||
}
|
||||
.opwide div>span>input+label {
|
||||
padding: .3em 0 .3em .3em;
|
||||
@@ -1586,6 +1609,7 @@ html {
|
||||
padding: .2em .4em;
|
||||
font-size: 1.2em;
|
||||
margin: .2em;
|
||||
display: inline-block;
|
||||
white-space: pre;
|
||||
position: relative;
|
||||
top: -.12em;
|
||||
@@ -1694,6 +1718,7 @@ html.y #tree.nowrap .ntree a+a:hover {
|
||||
}
|
||||
.ntree a:first-child {
|
||||
font-family: 'scp', monospace, monospace;
|
||||
font-family: var(--font-mono), 'scp', monospace, monospace;
|
||||
font-size: 1.2em;
|
||||
line-height: 0;
|
||||
}
|
||||
@@ -1721,6 +1746,7 @@ html.y #tree.nowrap .ntree a+a:hover {
|
||||
}
|
||||
#files th span {
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#files>thead>tr>th.min,
|
||||
#files td.min {
|
||||
@@ -1758,9 +1784,6 @@ html.y #tree.nowrap .ntree a+a:hover {
|
||||
margin: .7em 0 .7em .5em;
|
||||
padding-left: .5em;
|
||||
}
|
||||
.opwide>div>div>a {
|
||||
line-height: 2em;
|
||||
}
|
||||
.opwide>div>h3 {
|
||||
color: var(--fg-weak);
|
||||
margin: 0 .4em;
|
||||
@@ -1775,6 +1798,7 @@ html.y #tree.nowrap .ntree a+a:hover {
|
||||
padding: 0;
|
||||
}
|
||||
#thumbs,
|
||||
#au_prescan,
|
||||
#au_fullpre,
|
||||
#au_os_seek,
|
||||
#au_osd_cv,
|
||||
@@ -1782,7 +1806,8 @@ html.y #tree.nowrap .ntree a+a:hover {
|
||||
opacity: .3;
|
||||
}
|
||||
#griden.on+#thumbs,
|
||||
#au_preload.on+#au_fullpre,
|
||||
#au_preload.on+#au_prescan,
|
||||
#au_preload.on+#au_prescan+#au_fullpre,
|
||||
#au_os_ctl.on+#au_os_seek,
|
||||
#au_os_ctl.on+#au_os_seek+#au_osd_cv,
|
||||
#u2turbo.on+#u2tdate {
|
||||
@@ -1822,6 +1847,10 @@ html.y #tree.nowrap .ntree a+a:hover {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#unpost td:nth-child(3),
|
||||
#unpost td:nth-child(4) {
|
||||
text-align: right;
|
||||
}
|
||||
#rui {
|
||||
background: #fff;
|
||||
background: var(--bg);
|
||||
@@ -1849,6 +1878,7 @@ html.y #tree.nowrap .ntree a+a:hover {
|
||||
}
|
||||
#rn_vadv input {
|
||||
font-family: 'scp', monospace, monospace;
|
||||
font-family: var(--font-mono), 'scp', monospace, monospace;
|
||||
}
|
||||
#rui td+td,
|
||||
#rui td input[type="text"] {
|
||||
@@ -1912,6 +1942,7 @@ html.y #doc {
|
||||
#doc.mdo {
|
||||
white-space: normal;
|
||||
font-family: sans-serif;
|
||||
font-family: var(--font-main), sans-serif;
|
||||
}
|
||||
#doc.prism * {
|
||||
line-height: 1.5em;
|
||||
@@ -1971,6 +2002,7 @@ a.btn,
|
||||
}
|
||||
#hkhelp td:first-child {
|
||||
font-family: 'scp', monospace, monospace;
|
||||
font-family: var(--font-mono), 'scp', monospace, monospace;
|
||||
}
|
||||
html.noscroll,
|
||||
html.noscroll .sbar {
|
||||
@@ -2480,6 +2512,7 @@ html.y #bbox-overlay figcaption a {
|
||||
}
|
||||
#op_up2k.srch td.prog {
|
||||
font-family: sans-serif;
|
||||
font-family: var(--font-main), sans-serif;
|
||||
font-size: 1em;
|
||||
width: auto;
|
||||
}
|
||||
@@ -2494,6 +2527,7 @@ html.y #bbox-overlay figcaption a {
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
font-family: 'scp', monospace, monospace;
|
||||
font-family: var(--font-mono), 'scp', monospace, monospace;
|
||||
}
|
||||
#u2etas.o {
|
||||
width: 20em;
|
||||
@@ -2563,6 +2597,7 @@ html.y #bbox-overlay figcaption a {
|
||||
#u2cards span {
|
||||
color: var(--fg-max);
|
||||
font-family: 'scp', monospace;
|
||||
font-family: var(--font-mono), 'scp', monospace;
|
||||
}
|
||||
#u2cards > a:nth-child(4) > span {
|
||||
display: inline-block;
|
||||
@@ -2728,6 +2763,7 @@ html.b #u2conf a.b:hover {
|
||||
}
|
||||
.prog {
|
||||
font-family: 'scp', monospace, monospace;
|
||||
font-family: var(--font-mono), 'scp', monospace, monospace;
|
||||
}
|
||||
#u2tab span.inf,
|
||||
#u2tab span.ok,
|
||||
@@ -3136,7 +3172,7 @@ html.d #treepar {
|
||||
margin-top: 1.7em;
|
||||
}
|
||||
}
|
||||
@supports (display: grid) {
|
||||
@supports (display: grid) and (gap: 1em) {
|
||||
#ggrid {
|
||||
display: grid;
|
||||
margin: 0em 0.25em;
|
||||
@@ -3161,3 +3197,24 @@ html.d #treepar {
|
||||
padding: 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
@keyframes spin { }
|
||||
@keyframes gexit { }
|
||||
@keyframes bounce { }
|
||||
@keyframes bounceFromLeft { }
|
||||
@keyframes bounceFromRight { }
|
||||
|
||||
#ggrid>a:before,
|
||||
#widget.anim,
|
||||
#u2tabw,
|
||||
.dropdesc,
|
||||
.dropdesc b,
|
||||
.dropdesc>div>div {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.8, minimum-scale=0.6">
|
||||
<meta name="theme-color" content="#333">
|
||||
{{ html_head }}
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/browser.css?_={{ ts }}">
|
||||
{{ html_head }}
|
||||
{%- if css %}
|
||||
<link rel="stylesheet" media="screen" href="{{ css }}_={{ ts }}">
|
||||
{%- endif %}
|
||||
@@ -161,3 +161,4 @@
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,12 +6,12 @@
|
||||
<title>{{ title }}</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||
{{ html_head }}
|
||||
<style>
|
||||
html{font-family:sans-serif}
|
||||
td{border:1px solid #999;border-width:1px 1px 0 0;padding:0 5px}
|
||||
a{display:block}
|
||||
</style>
|
||||
{{ html_head }}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -61,3 +61,4 @@
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -25,3 +25,4 @@
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ html, body {
|
||||
color: #333;
|
||||
background: #eee;
|
||||
font-family: sans-serif;
|
||||
font-family: var(--font-main), sans-serif;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
html.y #helpbox a {
|
||||
@@ -67,6 +68,7 @@ a {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-family: 'scp', monospace, monospace;
|
||||
font-family: var(--font-mono), 'scp', monospace, monospace;
|
||||
font-weight: bold;
|
||||
font-size: 1.3em;
|
||||
line-height: .1em;
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
||||
<meta name="theme-color" content="#333">
|
||||
{{ html_head }}
|
||||
<link rel="stylesheet" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
<link rel="stylesheet" href="{{ r }}/.cpr/md.css?_={{ ts }}">
|
||||
{%- if edit %}
|
||||
<link rel="stylesheet" href="{{ r }}/.cpr/md2.css?_={{ ts }}">
|
||||
{%- endif %}
|
||||
{{ html_head }}
|
||||
</head>
|
||||
<body>
|
||||
<div id="mn"></div>
|
||||
@@ -160,3 +160,4 @@ try { l.light = drk? 0:1; } catch (ex) { }
|
||||
<script src="{{ r }}/.cpr/md2.js?_={{ ts }}"></script>
|
||||
{%- endif %}
|
||||
</body></html>
|
||||
|
||||
|
||||
@@ -512,13 +512,6 @@ dom_navtgl.onclick = function () {
|
||||
redraw();
|
||||
};
|
||||
|
||||
if (!HTTPS && location.hostname != '127.0.0.1') try {
|
||||
ebi('edit2').onclick = function (e) {
|
||||
toast.err(0, "the fancy editor is only available over https");
|
||||
return ev(e);
|
||||
}
|
||||
} catch (ex) { }
|
||||
|
||||
if (sread('hidenav') == 1)
|
||||
dom_navtgl.onclick();
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
width: calc(100% - 56em);
|
||||
}
|
||||
#mw {
|
||||
left: calc(100% - 55em);
|
||||
left: max(0em, calc(100% - 55em));
|
||||
overflow-y: auto;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
@@ -56,6 +56,7 @@
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: 'scp', monospace, monospace;
|
||||
font-family: var(--font-mono), 'scp', monospace, monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
@@ -368,14 +368,14 @@ function save(e) {
|
||||
|
||||
function save_cb() {
|
||||
if (this.status !== 200)
|
||||
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||
return toast.err(0, 'Error! The file was NOT saved.\n\nError ' + this.status + ":\n" + unpre(this.responseText));
|
||||
|
||||
var r;
|
||||
try {
|
||||
r = JSON.parse(this.responseText);
|
||||
}
|
||||
catch (ex) {
|
||||
return toast.err(0, 'Failed to parse reply from server:\n\n' + this.responseText);
|
||||
return toast.err(0, 'Error! The file was likely NOT saved.\n\nFailed to parse reply from server:\n\n' + unpre(this.responseText));
|
||||
}
|
||||
|
||||
if (!r.ok) {
|
||||
@@ -418,7 +418,7 @@ function run_savechk(lastmod, txt, btn, ntry) {
|
||||
|
||||
function savechk_cb() {
|
||||
if (this.status !== 200)
|
||||
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||
return toast.err(0, 'Error! The file was NOT saved.\n\nError ' + this.status + ":\n" + unpre(this.responseText));
|
||||
|
||||
var doc1 = this.txt.replace(/\r\n/g, "\n");
|
||||
var doc2 = this.responseText.replace(/\r\n/g, "\n");
|
||||
|
||||
@@ -17,6 +17,7 @@ html, body {
|
||||
padding: 0;
|
||||
min-height: 100%;
|
||||
font-family: sans-serif;
|
||||
font-family: var(--font-main), sans-serif;
|
||||
background: #f7f7f7;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
||||
<meta name="theme-color" content="#333">
|
||||
{{ html_head }}
|
||||
<link rel="stylesheet" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
<link rel="stylesheet" href="{{ r }}/.cpr/mde.css?_={{ ts }}">
|
||||
<link rel="stylesheet" href="{{ r }}/.cpr/deps/mini-fa.css?_={{ ts }}">
|
||||
<link rel="stylesheet" href="{{ r }}/.cpr/deps/easymde.css?_={{ ts }}">
|
||||
{{ html_head }}
|
||||
</head>
|
||||
<body>
|
||||
<div id="mw">
|
||||
@@ -54,3 +54,4 @@ try { l.light = drk? 0:1; } catch (ex) { }
|
||||
<script src="{{ r }}/.cpr/deps/easymde.js?_={{ ts }}"></script>
|
||||
<script src="{{ r }}/.cpr/mde.js?_={{ ts }}"></script>
|
||||
</body></html>
|
||||
|
||||
|
||||
@@ -134,14 +134,14 @@ function save(mde) {
|
||||
|
||||
function save_cb() {
|
||||
if (this.status !== 200)
|
||||
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||
return toast.err(0, 'Error! The file was NOT saved.\n\nError ' + this.status + ":\n" + unpre(this.responseText));
|
||||
|
||||
var r;
|
||||
try {
|
||||
r = JSON.parse(this.responseText);
|
||||
}
|
||||
catch (ex) {
|
||||
return toast.err(0, 'Failed to parse reply from server:\n\n' + this.responseText);
|
||||
return toast.err(0, 'Error! The file was likely NOT saved.\n\nFailed to parse reply from server:\n\n' + unpre(this.responseText));
|
||||
}
|
||||
|
||||
if (!r.ok) {
|
||||
@@ -180,7 +180,7 @@ function save_cb() {
|
||||
|
||||
function save_chk() {
|
||||
if (this.status !== 200)
|
||||
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||
return toast.err(0, 'Error! The file was NOT saved.\n\nError ' + this.status + ":\n" + unpre(this.responseText));
|
||||
|
||||
var doc1 = this.txt.replace(/\r\n/g, "\n");
|
||||
var doc2 = this.responseText.replace(/\r\n/g, "\n");
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
:root {
|
||||
--font-main: sans-serif;
|
||||
--font-serif: serif;
|
||||
--font-mono: 'scp';
|
||||
}
|
||||
html,body,tr,th,td,#files,a {
|
||||
color: inherit;
|
||||
background: none;
|
||||
@@ -10,6 +15,7 @@ html {
|
||||
color: #ccc;
|
||||
background: #333;
|
||||
font-family: sans-serif;
|
||||
font-family: var(--font-main), sans-serif;
|
||||
text-shadow: 1px 1px 0px #000;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
@@ -23,6 +29,7 @@ html, body {
|
||||
}
|
||||
pre {
|
||||
font-family: monospace, monospace;
|
||||
font-family: var(--font-mono), monospace, monospace;
|
||||
}
|
||||
a {
|
||||
color: #fc5;
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||
<meta name="theme-color" content="#333">
|
||||
{{ html_head }}
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/msg.css?_={{ ts }}">
|
||||
{{ html_head }}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -48,4 +48,5 @@
|
||||
{%- endif %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ html {
|
||||
color: #333;
|
||||
background: #f7f7f7;
|
||||
font-family: sans-serif;
|
||||
font-family: var(--font-main), sans-serif;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
#wrap {
|
||||
@@ -127,6 +128,7 @@ pre, code {
|
||||
color: #480;
|
||||
background: #fff;
|
||||
font-family: 'scp', monospace, monospace;
|
||||
font-family: var(--font-mono), 'scp', monospace, monospace;
|
||||
border: 1px solid rgba(128,128,128,0.3);
|
||||
border-radius: .2em;
|
||||
padding: .15em .2em;
|
||||
@@ -188,11 +190,21 @@ input {
|
||||
padding: .5em .7em;
|
||||
margin: 0 .5em 0 0;
|
||||
}
|
||||
input::placeholder {
|
||||
font-size: 1.2em;
|
||||
font-style: italic;
|
||||
letter-spacing: .04em;
|
||||
opacity: 0.64;
|
||||
color: #930;
|
||||
}
|
||||
html.z input {
|
||||
color: #fff;
|
||||
background: #626;
|
||||
border-color: #c2c;
|
||||
}
|
||||
html.z input::placeholder {
|
||||
color: #fff;
|
||||
}
|
||||
html.z .num {
|
||||
border-color: #777;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||
<meta name="theme-color" content="#333">
|
||||
{{ html_head }}
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/splash.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
{{ html_head }}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -78,13 +78,15 @@
|
||||
|
||||
<h1 id="cc">client config:</h1>
|
||||
<ul>
|
||||
{% if k304 or k304vis %}
|
||||
{% if k304 %}
|
||||
<li><a id="h" href="{{ r }}/?k304=n">disable k304</a> (currently enabled)
|
||||
{%- else %}
|
||||
<li><a id="i" href="{{ r }}/?k304=y" class="r">enable k304</a> (currently disabled)
|
||||
{% endif %}
|
||||
<blockquote id="j">enabling this 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 %}
|
||||
|
||||
<li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li>
|
||||
</ul>
|
||||
|
||||
@@ -92,7 +94,7 @@
|
||||
<div>
|
||||
<form method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}">
|
||||
<input type="hidden" name="act" value="login" />
|
||||
<input type="password" name="cppwd" />
|
||||
<input type="password" name="cppwd" placeholder=" password" />
|
||||
<input type="submit" value="Login" />
|
||||
{% if ahttps %}
|
||||
<a id="w" href="{{ ahttps }}">switch to https</a>
|
||||
@@ -118,3 +120,4 @@ document.documentElement.className = (STG && STG.cpp_thm) || "{{ this.args.theme
|
||||
<script src="{{ r }}/.cpr/splash.js?_={{ ts }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ var Ls = {
|
||||
"d1": "tilstand",
|
||||
"d2": "vis tilstanden til alle tråder",
|
||||
"e1": "last innst.",
|
||||
"e2": "leser inn konfigurasjonsfiler på nytt$N(kontoer, volumer, volumbrytere)$Nog kartlegger alle e2ds-volumer",
|
||||
"e2": "leser inn konfigurasjonsfiler på nytt$N(kontoer, volumer, volumbrytere)$Nog kartlegger alle e2ds-volumer$N$Nmerk: endringer i globale parametere$Nkrever en full restart for å ta gjenge",
|
||||
"f1": "du kan betrakte:",
|
||||
"g1": "du kan laste opp til:",
|
||||
"cc1": "klient-konfigurasjon",
|
||||
@@ -30,7 +30,7 @@ var Ls = {
|
||||
},
|
||||
"eng": {
|
||||
"d2": "shows the state of all active threads",
|
||||
"e2": "reload config files (accounts/volumes/volflags),$Nand rescan all e2ds volumes",
|
||||
"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",
|
||||
"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$N$NWARNING: this will show your password!",
|
||||
}
|
||||
@@ -49,6 +49,15 @@ for (var k in (d || {})) {
|
||||
o[a].setAttribute("tt", d[k]);
|
||||
}
|
||||
|
||||
try {
|
||||
if (is_idp) {
|
||||
var z = ['#l+div', '#l', '#c'];
|
||||
for (var a = 0; a < z.length; a++)
|
||||
QS(z[a]).style.display = 'none';
|
||||
}
|
||||
}
|
||||
catch (ex) { }
|
||||
|
||||
tt.init();
|
||||
var o = QS('input[name="cppwd"]');
|
||||
if (!ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight)
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||
<meta name="theme-color" content="#333">
|
||||
{{ html_head }}
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/splash.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
<style>ul{padding-left:1.3em}li{margin:.4em 0}</style>
|
||||
{{ html_head }}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -246,3 +246,4 @@ document.documentElement.className = (STG && STG.cpp_thm) || "{{ args.theme }}";
|
||||
<script src="{{ r }}/.cpr/svcs.js?_={{ ts }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
:root {
|
||||
--font-main: sans-serif;
|
||||
--font-serif: serif;
|
||||
--font-mono: 'scp';
|
||||
|
||||
--fg: #ccc;
|
||||
--fg-max: #fff;
|
||||
--bg-u2: #2b2b2b;
|
||||
@@ -378,6 +382,7 @@ html.y textarea:focus {
|
||||
.mdo code,
|
||||
.mdo tt {
|
||||
font-family: 'scp', monospace, monospace;
|
||||
font-family: var(--font-mono), 'scp', monospace, monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -447,6 +452,7 @@ html.y textarea:focus {
|
||||
}
|
||||
.mdo blockquote {
|
||||
font-family: serif;
|
||||
font-family: var(--font-serif), serif;
|
||||
background: #f7f7f7;
|
||||
border: .07em dashed #ccc;
|
||||
padding: 0 2em;
|
||||
@@ -580,3 +586,11 @@ hr {
|
||||
border: .07em dashed #444;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
#toast,
|
||||
#toast a#toastc,
|
||||
#tt {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ function goto_up2k() {
|
||||
var up2k = null,
|
||||
up2k_hooks = [],
|
||||
hws = [],
|
||||
sha_js = window.WebAssembly ? 'hw' : 'ac', // ff53,c57,sa11
|
||||
sha_js = WebAssembly ? 'hw' : 'ac', // ff53,c57,sa11
|
||||
m = 'will use ' + sha_js + ' instead of native sha512 due to';
|
||||
|
||||
try {
|
||||
@@ -717,7 +717,7 @@ function Donut(uc, st) {
|
||||
sfx();
|
||||
|
||||
// firefox may forget that filedrops are user-gestures so it can skip this:
|
||||
if (uc.upnag && window.Notification && Notification.permission == 'granted')
|
||||
if (uc.upnag && Notification && Notification.permission == 'granted')
|
||||
new Notification(uc.nagtxt);
|
||||
}
|
||||
|
||||
@@ -779,8 +779,8 @@ function up2k_init(subtle) {
|
||||
};
|
||||
|
||||
setTimeout(function () {
|
||||
if (window.WebAssembly && !hws.length)
|
||||
fetch(SR + '/.cpr/w.hash.js' + CB);
|
||||
if (WebAssembly && !hws.length)
|
||||
fetch(SR + '/.cpr/w.hash.js?_=' + TS);
|
||||
}, 1000);
|
||||
|
||||
function showmodal(msg) {
|
||||
@@ -869,7 +869,7 @@ function up2k_init(subtle) {
|
||||
bcfg_bind(uc, 'turbo', 'u2turbo', turbolvl > 1, draw_turbo);
|
||||
bcfg_bind(uc, 'datechk', 'u2tdate', turbolvl < 3, null);
|
||||
bcfg_bind(uc, 'az', 'u2sort', u2sort.indexOf('n') + 1, set_u2sort);
|
||||
bcfg_bind(uc, 'hashw', 'hashw', !!window.WebAssembly && (!subtle || !CHROME || MOBILE || VCHROME >= 107), set_hashw);
|
||||
bcfg_bind(uc, 'hashw', 'hashw', !!WebAssembly && (!subtle || !CHROME || MOBILE || VCHROME >= 107), set_hashw);
|
||||
bcfg_bind(uc, 'upnag', 'upnag', false, set_upnag);
|
||||
bcfg_bind(uc, 'upsfx', 'upsfx', false, set_upsfx);
|
||||
|
||||
@@ -1347,9 +1347,9 @@ function up2k_init(subtle) {
|
||||
var evpath = get_evpath(),
|
||||
draw_each = good_files.length < 50;
|
||||
|
||||
if (window.WebAssembly && !hws.length) {
|
||||
if (WebAssembly && !hws.length) {
|
||||
for (var a = 0; a < Math.min(navigator.hardwareConcurrency || 4, 16); a++)
|
||||
hws.push(new Worker(SR + '/.cpr/w.hash.js' + CB));
|
||||
hws.push(new Worker(SR + '/.cpr/w.hash.js?_=' + TS));
|
||||
|
||||
console.log(hws.length + " hashers");
|
||||
}
|
||||
@@ -1722,8 +1722,6 @@ function up2k_init(subtle) {
|
||||
ebi('u2etas').style.textAlign = 'left';
|
||||
}
|
||||
etafun();
|
||||
if (pvis.act == 'bz')
|
||||
pvis.changecard('bz');
|
||||
}
|
||||
|
||||
if (flag) {
|
||||
@@ -1859,6 +1857,9 @@ function up2k_init(subtle) {
|
||||
timer.rm(donut.do);
|
||||
ebi('u2tabw').style.minHeight = '0px';
|
||||
utw_minh = 0;
|
||||
|
||||
if (pvis.act == 'bz')
|
||||
pvis.changecard('bz');
|
||||
}
|
||||
|
||||
function chill(t) {
|
||||
@@ -2256,6 +2257,7 @@ function up2k_init(subtle) {
|
||||
console.log('handshake onerror, retrying', t.name, t);
|
||||
apop(st.busy.handshake, t);
|
||||
st.todo.handshake.unshift(t);
|
||||
t.cooldown = Date.now() + 5000 + Math.floor(Math.random() * 3000);
|
||||
t.keepalive = keepalive;
|
||||
};
|
||||
var orz = function (e) {
|
||||
@@ -2263,16 +2265,26 @@ function up2k_init(subtle) {
|
||||
return console.log('zombie handshake onload', t.name, t);
|
||||
|
||||
if (xhr.status == 200) {
|
||||
try {
|
||||
var response = JSON.parse(xhr.responseText);
|
||||
}
|
||||
catch (ex) {
|
||||
apop(st.busy.handshake, t);
|
||||
st.todo.handshake.unshift(t);
|
||||
t.cooldown = Date.now() + 5000 + Math.floor(Math.random() * 3000);
|
||||
return toast.err(0, 'Handshake error; will retry...\n\n' + L.badreply + ':\n\n' + unpre(xhr.responseText));
|
||||
}
|
||||
|
||||
t.t_handshake = Date.now();
|
||||
if (keepalive) {
|
||||
apop(st.busy.handshake, t);
|
||||
tasker();
|
||||
return;
|
||||
}
|
||||
|
||||
if (toast.tag === t)
|
||||
toast.ok(5, L.u_fixed);
|
||||
|
||||
var response = JSON.parse(xhr.responseText);
|
||||
if (!response.name) {
|
||||
var msg = '',
|
||||
smsg = '';
|
||||
@@ -2856,6 +2868,8 @@ function up2k_init(subtle) {
|
||||
new_state = false;
|
||||
fixed = true;
|
||||
}
|
||||
if (new_state === undefined)
|
||||
new_state = can_write ? false : have_up2k_idx ? true : undefined;
|
||||
}
|
||||
|
||||
if (new_state === undefined)
|
||||
@@ -2936,7 +2950,7 @@ function up2k_init(subtle) {
|
||||
}
|
||||
|
||||
function set_hashw() {
|
||||
if (!window.WebAssembly) {
|
||||
if (!WebAssembly) {
|
||||
bcfg_set('hashw', uc.hashw = false);
|
||||
toast.err(10, L.u_nowork);
|
||||
}
|
||||
@@ -2953,7 +2967,7 @@ function up2k_init(subtle) {
|
||||
nopenag();
|
||||
}
|
||||
|
||||
if (!window.Notification || !HTTPS)
|
||||
if (!Notification || !HTTPS)
|
||||
return nopenag();
|
||||
|
||||
if (en && Notification.permission == 'default')
|
||||
@@ -2975,7 +2989,7 @@ function up2k_init(subtle) {
|
||||
};
|
||||
}
|
||||
|
||||
if (uc.upnag && (!window.Notification || Notification.permission != 'granted'))
|
||||
if (uc.upnag && (!Notification || Notification.permission != 'granted'))
|
||||
bcfg_set('upnag', uc.upnag = false);
|
||||
|
||||
ebi('nthread_add').onclick = function (e) {
|
||||
|
||||
@@ -16,7 +16,6 @@ var wah = '',
|
||||
NOAC = 'autocorrect="off" autocapitalize="off"',
|
||||
L, tt, treectl, thegrid, up2k, asmCrypto, hashwasm, vbar, marked,
|
||||
T0 = Date.now(),
|
||||
CB = '?_=' + Math.floor(T0 / 1000).toString(36),
|
||||
R = SR.slice(1),
|
||||
RS = R ? "/" + R : "",
|
||||
HALFMAX = 8192 * 8192 * 8192 * 8192,
|
||||
@@ -52,8 +51,6 @@ catch (ex) {
|
||||
}
|
||||
|
||||
try {
|
||||
CB = '?' + document.currentScript.src.split('?').pop();
|
||||
|
||||
if (navigator.userAgentData.mobile)
|
||||
MOBILE = true;
|
||||
|
||||
@@ -130,7 +127,7 @@ if ((document.location + '').indexOf(',rej,') + 1)
|
||||
|
||||
try {
|
||||
console.hist = [];
|
||||
var CMAXHIST = 100;
|
||||
var CMAXHIST = 1000;
|
||||
var hook = function (t) {
|
||||
var orig = console[t].bind(console),
|
||||
cfun = function () {
|
||||
@@ -157,6 +154,10 @@ catch (ex) {
|
||||
}
|
||||
var crashed = false, ignexd = {}, evalex_fatal = false;
|
||||
function vis_exh(msg, url, lineNo, columnNo, error) {
|
||||
var ekey = url + '\n' + lineNo + '\n' + msg;
|
||||
if (ignexd[ekey] || crashed)
|
||||
return;
|
||||
|
||||
msg = String(msg);
|
||||
url = String(url);
|
||||
|
||||
@@ -175,11 +176,10 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
|
||||
if (IE && url.indexOf('prism.js') + 1)
|
||||
return;
|
||||
|
||||
var ekey = url + '\n' + lineNo + '\n' + msg;
|
||||
if (ignexd[ekey] || crashed)
|
||||
return;
|
||||
if (url.indexOf('easymde.js') + 1)
|
||||
return; // clicking the preview pane
|
||||
|
||||
if (url.indexOf('deps/marked.js') + 1 && !window.WebAssembly)
|
||||
if (url.indexOf('deps/marked.js') + 1 && !WebAssembly)
|
||||
return; // ff<52
|
||||
|
||||
crashed = true;
|
||||
@@ -737,6 +737,15 @@ function vjoin(p1, p2) {
|
||||
}
|
||||
|
||||
|
||||
function addq(url, q) {
|
||||
var uh = url.split('#', 1),
|
||||
u = uh[0],
|
||||
h = uh.length == 1 ? '' : '#' + uh[1];
|
||||
|
||||
return u + (u.indexOf('?') < 0 ? '?' : '&') + (q === undefined ? '' : q) + h;
|
||||
}
|
||||
|
||||
|
||||
function uricom_enc(txt, do_fb_enc) {
|
||||
try {
|
||||
return encodeURIComponent(txt);
|
||||
@@ -1414,9 +1423,12 @@ function lf2br(txt) {
|
||||
}
|
||||
|
||||
|
||||
function unpre(txt) {
|
||||
function hunpre(txt) {
|
||||
return ('' + txt).replace(/^<pre>/, '');
|
||||
}
|
||||
function unpre(txt) {
|
||||
return esc(hunpre(txt));
|
||||
}
|
||||
|
||||
|
||||
var toast = (function () {
|
||||
@@ -1463,13 +1475,15 @@ var toast = (function () {
|
||||
clmod(obj, 'vis');
|
||||
r.visible = false;
|
||||
r.tag = obj;
|
||||
if (!window.WebAssembly)
|
||||
if (!WebAssembly)
|
||||
te = setTimeout(function () {
|
||||
obj.className = 'hide';
|
||||
}, 500);
|
||||
};
|
||||
|
||||
r.show = function (cl, sec, txt, tag) {
|
||||
txt = (txt + '').slice(0, 16384);
|
||||
|
||||
var same = r.visible && txt == r.p_txt && r.p_sec == sec,
|
||||
delta = Date.now() - r.p_t;
|
||||
|
||||
@@ -1877,7 +1891,7 @@ function md_thumbs(md) {
|
||||
float = has(flags, 'l') ? 'left' : has(flags, 'r') ? 'right' : '';
|
||||
|
||||
if (!/[?&]cache/.exec(url))
|
||||
url += (url.indexOf('?') < 0 ? '?' : '&') + 'cache=i';
|
||||
url = addq(url, 'cache=i');
|
||||
|
||||
md[a] = '<a href="' + url + '" class="mdth mdth' + float.slice(0, 1) + '"><img src="' + url + '&th=w" alt="' + alt + '" /></a>' + md[a].slice(o2 + 1);
|
||||
}
|
||||
@@ -1990,15 +2004,21 @@ function xhrchk(xhr, prefix, e404, lvl, tag) {
|
||||
if (tag === undefined)
|
||||
tag = prefix;
|
||||
|
||||
var errtxt = (xhr.response && xhr.response.err) || xhr.responseText,
|
||||
var errtxt = ((xhr.response && xhr.response.err) || xhr.responseText) || '',
|
||||
suf = '',
|
||||
fun = toast[lvl || 'err'],
|
||||
is_cf = /[Cc]loud[f]lare|>Just a mo[m]ent|#cf-b[u]bbles|Chec[k]ing your br[o]wser|\/chall[e]nge-platform|"chall[e]nge-error|nable Ja[v]aScript and cook/.test(errtxt);
|
||||
|
||||
if (errtxt.startsWith('<pre>'))
|
||||
suf = '\n\nerror-details: «' + unpre(errtxt).split('\n')[0].trim() + '»';
|
||||
else
|
||||
errtxt = esc(errtxt).slice(0, 32768);
|
||||
|
||||
if (xhr.status == 403 && !is_cf)
|
||||
return toast.err(0, prefix + (L && L.xhr403 || "403: access denied\n\ntry pressing F5, maybe you got logged out"), tag);
|
||||
return toast.err(0, prefix + (L && L.xhr403 || "403: access denied\n\ntry pressing F5, maybe you got logged out") + suf, tag);
|
||||
|
||||
if (xhr.status == 404)
|
||||
return toast.err(0, prefix + e404, tag);
|
||||
return toast.err(0, prefix + e404 + suf, tag);
|
||||
|
||||
if (is_cf && (xhr.status == 403 || xhr.status == 503)) {
|
||||
var now = Date.now(), td = now - cf_cha_t;
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
|
||||
# other stuff
|
||||
|
||||
## [`TODO.md`](TODO.md)
|
||||
* planned features / fixes / changes
|
||||
|
||||
## [`example.conf`](example.conf)
|
||||
* example config file for `-c`
|
||||
|
||||
|
||||
15
docs/TODO.md
Normal file
15
docs/TODO.md
Normal file
@@ -0,0 +1,15 @@
|
||||
a living list of upcoming features / fixes / changes, very roughly in order of priority
|
||||
|
||||
* download accelerator
|
||||
* definitely download chunks in parallel
|
||||
* maybe resumable downloads (chrome-only, jank api)
|
||||
* maybe checksum validation (return sha512 of requested range in responses, and probably also warks)
|
||||
|
||||
* [github issue #37](https://github.com/9001/copyparty/issues/37) - upload PWA
|
||||
* or [maybe not](https://arstechnica.com/tech-policy/2024/02/apple-under-fire-for-disabling-iphone-web-apps-eu-asks-developers-to-weigh-in/), or [maybe](https://arstechnica.com/gadgets/2024/03/apple-changes-course-will-keep-iphone-eu-web-apps-how-they-are-in-ios-17-4/)
|
||||
|
||||
* [github issue #57](https://github.com/9001/copyparty/issues/57) - config GUI
|
||||
* configs given to -c can be ordered with numerical prefix
|
||||
* autorevert settings if it fails to apply
|
||||
* countdown until session invalidates in settings gui, with refresh-button
|
||||
|
||||
96
docs/bufsize.txt
Normal file
96
docs/bufsize.txt
Normal file
@@ -0,0 +1,96 @@
|
||||
notes from testing various buffer sizes of files and sockets
|
||||
|
||||
summary:
|
||||
|
||||
download-folder-as-tar: would be 7% faster with --iobuf 65536 (but got 20% faster in v1.11.2)
|
||||
|
||||
download-folder-as-zip: optimal with default --iobuf 262144
|
||||
|
||||
download-file-over-https: optimal with default --iobuf 262144
|
||||
|
||||
put-large-file: optimal with default --iobuf 262144, --s-rd-sz 262144 (and got 14% faster in v1.11.2)
|
||||
|
||||
post-large-file: optimal with default --iobuf 262144, --s-rd-sz 262144 (and got 18% faster in v1.11.2)
|
||||
|
||||
----
|
||||
|
||||
oha -z10s -c1 --ipv4 --insecure http://127.0.0.1:3923/bigs/?tar
|
||||
3.3 req/s 1.11.1
|
||||
4.3 4.0 3.3 req/s 1.12.2
|
||||
64 256 512 --iobuf 256 (prefer smaller)
|
||||
32 32 32 --s-rd-sz
|
||||
|
||||
oha -z10s -c1 --ipv4 --insecure http://127.0.0.1:3923/bigs/?zip
|
||||
2.9 req/s 1.11.1
|
||||
2.5 2.9 2.9 req/s 1.12.2
|
||||
64 256 512 --iobuf 256 (prefer bigger)
|
||||
32 32 32 --s-rd-sz
|
||||
|
||||
oha -z10s -c1 --ipv4 --insecure http://127.0.0.1:3923/pairdupes/?tar
|
||||
8.3 req/s 1.11.1
|
||||
8.4 8.4 8.5 req/s 1.12.2
|
||||
64 256 512 --iobuf 256 (prefer bigger)
|
||||
32 32 32 --s-rd-sz
|
||||
|
||||
oha -z10s -c1 --ipv4 --insecure http://127.0.0.1:3923/pairdupes/?zip
|
||||
13.9 req/s 1.11.1
|
||||
14.1 14.0 13.8 req/s 1.12.2
|
||||
64 256 512 --iobuf 256 (prefer smaller)
|
||||
32 32 32 --s-rd-sz
|
||||
|
||||
oha -z10s -c1 --ipv4 --insecure http://127.0.0.1:3923/pairdupes/987a
|
||||
5260 req/s 1.11.1
|
||||
5246 5246 5280 5268 req/s 1.12.2
|
||||
64 256 512 256 --iobuf dontcare
|
||||
32 32 32 512 --s-rd-sz dontcare
|
||||
|
||||
oha -z10s -c1 --ipv4 --insecure https://127.0.0.1:3923/pairdupes/987a
|
||||
4445 req/s 1.11.1
|
||||
4462 4494 4444 req/s 1.12.2
|
||||
64 256 512 --iobuf dontcare
|
||||
32 32 32 --s-rd-sz
|
||||
|
||||
oha -z10s -c1 --ipv4 --insecure http://127.0.0.1:3923/bigs/gssc-02-cannonball-skydrift/track10.cdda.flac
|
||||
95 req/s 1.11.1
|
||||
95 97 req/s 1.12.2
|
||||
64 512 --iobuf dontcare
|
||||
32 32 --s-rd-sz
|
||||
|
||||
oha -z10s -c1 --ipv4 --insecure https://127.0.0.1:3923/bigs/gssc-02-cannonball-skydrift/track10.cdda.flac
|
||||
15.4 req/s 1.11.1
|
||||
15.4 15.3 14.9 15.4 req/s 1.12.2
|
||||
64 256 512 512 --iobuf 256 (prefer smaller, and smaller than s-wr-sz)
|
||||
32 32 32 32 --s-rd-sz
|
||||
256 256 256 512 --s-wr-sz
|
||||
|
||||
----
|
||||
|
||||
python3 ~/dev/old/copyparty\ v1.11.1\ dont\ ban\ the\ pipes.py -q -i 127.0.0.1 -v .::A --daw
|
||||
python3 ~/dev/copyparty/dist/copyparty-sfx.py -q -i 127.0.0.1 -v .::A --daw --iobuf $((1024*512))
|
||||
|
||||
oha -z10s -c1 --ipv4 --insecure -mPUT -r0 -D ~/Music/gssc-02-cannonball-skydrift/track10.cdda.flac http://127.0.0.1:3923/a.bin
|
||||
10.8 req/s 1.11.1
|
||||
10.8 11.5 11.8 12.1 12.2 12.3 req/s new
|
||||
512 512 512 512 512 256 --iobuf 256
|
||||
32 64 128 256 512 256 --s-rd-sz 256 (prefer bigger)
|
||||
|
||||
----
|
||||
|
||||
buildpost() {
|
||||
b=--jeg-er-grensestaven;
|
||||
printf -- "$b\r\nContent-Disposition: form-data; name=\"act\"\r\n\r\nbput\r\n$b\r\nContent-Disposition: form-data; name=\"f\"; filename=\"a.bin\"\r\nContent-Type: audio/mpeg\r\n\r\n"
|
||||
cat "$1"
|
||||
printf -- "\r\n${b}--\r\n"
|
||||
}
|
||||
buildpost ~/Music/gssc-02-cannonball-skydrift/track10.cdda.flac >big.post
|
||||
buildpost ~/Music/bottomtext.txt >smol.post
|
||||
|
||||
oha -z10s -c1 --ipv4 --insecure -mPOST -r0 -T 'multipart/form-data; boundary=jeg-er-grensestaven' -D big.post http://127.0.0.1:3923/?replace
|
||||
9.6 11.2 11.3 11.1 10.9 req/s v1.11.2
|
||||
512 512 256 128 256 --iobuf 256
|
||||
32 512 256 128 128 --s-rd-sz 256
|
||||
|
||||
oha -z10s -c1 --ipv4 --insecure -mPOST -r0 -T 'multipart/form-data; boundary=jeg-er-grensestaven' -D smol.post http://127.0.0.1:3923/?replace
|
||||
2445 2414 2401 2437
|
||||
256 128 256 256 --iobuf 256
|
||||
128 128 256 64 --s-rd-sz 128 (but use 256 since big posts are more important)
|
||||
@@ -1,3 +1,300 @@
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0412-2110 `v1.12.2` ie11 fix
|
||||
|
||||
## new features
|
||||
|
||||
* new option `--bauth-last` for when you're hosting other [basic-auth](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) services on the same domain 7b94e4ed
|
||||
* makes it possible to log into copyparty as intended, but it still sees the passwords from the other service until you do
|
||||
* alternatively, the other new option `--no-bauth` entirely disables basic-auth support, but that also kills [the android app](https://github.com/9001/party-up)
|
||||
|
||||
## bugfixes
|
||||
|
||||
* internet explorer isn't working?! FIX IT!!! 9e5253ef
|
||||
* audio transcoding was buggy with filekeys enabled b8733653
|
||||
* on windows, theoretical chance that antivirus could interrupt renaming files, so preemptively guard against that c8e3ed3a
|
||||
|
||||
## other changes
|
||||
|
||||
* add a "password" placeholder on the login page since you might think it's asking for a username da26ec36
|
||||
* config buttons were jank on iOS b772a4f8
|
||||
* readme: [making your homeserver accessible from the internet](https://github.com/9001/copyparty#at-home)
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0409-2334 `v1.12.1` scrolling stuff
|
||||
|
||||
## new features
|
||||
|
||||
* while viewing pictures/videos, the scrollwheel can be used to view the prev/next file 844d16b9
|
||||
|
||||
## bugfixes
|
||||
|
||||
* #81 (scrolling suddenly getting disabled) properly fixed after @icxes found another way to reproduce it (thx) 4f0cad54
|
||||
* and fixed at least one javascript glitch introduced in v1.12.0 while adding dirkeys 989cc613
|
||||
* directory tree sidebar could fail to render when popping browser history into the lightbox
|
||||
|
||||
## other changes
|
||||
|
||||
* music preloader is slightly less hyper f89de6b3
|
||||
* u2c.exe: updated TLS-certs and deps ab18893c
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0406-2011 `v1.12.0` locksmith
|
||||
|
||||
## new features
|
||||
|
||||
* #64 dirkeys; option to auto-generate passwords for folders, so you can give someone a link to a specific folder inside a volume without sharing the rest of the volume 10bc2d92 32c912bb ef52e2c0 0ae12868
|
||||
* enabled by volflag `dk` (exact folder only) and/or volflag `dks` (also subfolders); see [readme](https://github.com/9001/copyparty#dirkeys)
|
||||
* audio transcoding to mp3 if browser doesn't support opus a080759a
|
||||
* recursively transcode and download a folder using `?tar&mp3`
|
||||
* accidentally adds support for playing just about any audio format in ie11
|
||||
* audio equalizer also applies to videos 7744226b
|
||||
|
||||
## bugfixes
|
||||
|
||||
* #81 scrolling could break after viewing an image in the lightbox 9c42cbec
|
||||
* on phones, audio playback could stop if network is slow/unreliable 59f815ff b88cc7b5 59a53ba9
|
||||
* fixes the issue on android, but ios/safari appears to be [impossible](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#music-playback-halting-on-phones) d94b5b3f
|
||||
|
||||
## other changes
|
||||
|
||||
* updated dompurify to 3.0.11
|
||||
* copyparty.exe: updated to python 3.11.9
|
||||
* support for building with pyoxidizer was removed 5ab54763
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0323-1724 `v1.11.2` public idp volumes
|
||||
|
||||
* read-only demo server at https://a.ocv.me/pub/demo/
|
||||
* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) ╱ [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) ╱ [client testbed](https://cd.ocv.me/b/)
|
||||
|
||||
there is a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone` in case of future important updates, such as [vulnerabilities](https://github.com/9001/copyparty/security) (most recently 2023-07-23)
|
||||
|
||||
## new features
|
||||
|
||||
* global-option `--iobuf` to set a custom I/O buffersize 2b24c50e
|
||||
* changes the default buffersize to 256 KiB everywhere (was a mix of 64 and 512)
|
||||
* may improve performance of networked volumes (s3 etc.) if increased
|
||||
* on gbit networks: download-as-tar is now up to 20% faster
|
||||
* slightly faster FTP and TFTP too
|
||||
|
||||
* global-option `--s-rd-sz` to set a custom read-size for sockets c6acd3a9
|
||||
* changes the default from 32 to 256 KiB
|
||||
* may improve performance of networked volumes (s3 etc.) if increased
|
||||
* on 10gbit networks: uploading large files is now up to 17% faster
|
||||
|
||||
* add url parameter `?replace` to overwrite any existing files with a multipart-post c6acd3a9
|
||||
|
||||
## bugfixes
|
||||
|
||||
* #79 idp volumes (introduced in [v1.11.0](https://github.com/9001/copyparty/releases/tag/v1.11.0)) would only accept permissions for the user that owned the volume; was impossible to grant read/write-access to other users d30ae845
|
||||
|
||||
## other changes
|
||||
|
||||
* mention the [lack of persistence for idp volumes](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#important-notes) in the IdP docs 2f20d29e
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0318-1709 `v1.11.1` dont ban the pipes
|
||||
|
||||
the [previous release](https://github.com/9001/copyparty/releases/tag/v1.11.0) had all the fun new features... this one's just bugfixes
|
||||
|
||||
* read-only demo server at https://a.ocv.me/pub/demo/
|
||||
* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) ╱ [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) ╱ [client testbed](https://cd.ocv.me/b/)
|
||||
|
||||
### no vulnerabilities since 2023-07-23
|
||||
* there is a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone` in case of future important updates
|
||||
* [v1.8.7](https://github.com/9001/copyparty/releases/tag/v1.8.7) (2023-07-23) - [CVE-2023-38501](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-38501) - reflected XSS
|
||||
* [v1.8.2](https://github.com/9001/copyparty/releases/tag/v1.8.2) (2023-07-14) - [CVE-2023-37474](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-37474) - path traversal (first CVE)
|
||||
|
||||
## bugfixes
|
||||
|
||||
* less aggressive rejection of requests from banned IPs 51d31588
|
||||
* clients would get kicked before the header was parsed (which contains the xff header), meaning the server could become inaccessible to everyone if the reverse-proxy itself were to "somehow" get banned
|
||||
* ...which can happen if a server behind cloudflare also accepts non-cloudflare connections, meaning the client IP would not be resolved, and it'll ban the LAN IP instead heh
|
||||
* that part still happens, but now it won't affect legit clients through the intended route
|
||||
* the old behavior can be restored with `--early-ban` to save some cycles, and/or avoid slowloris somewhat
|
||||
* the unpost feature could appear to be disabled on servers where no volume was mapped to `/` 0287c7ba
|
||||
* python 3.12 support for [compiling the dependencies](https://github.com/9001/copyparty/tree/hovudstraum/bin/mtag#dependencies) necessary to detect bpm/key in audio files 32553e45
|
||||
|
||||
## other changes
|
||||
|
||||
* mention [real-ip configuration](https://github.com/9001/copyparty?tab=readme-ov-file#real-ip) in the readme ee80cdb9
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0315-2047 `v1.11.0` You Can (Not) Proceed
|
||||
|
||||
this release was made possible by [stoltzekleiven, kvikklunsj, and tako](https://a.ocv.me/pub/g/nerd-stuff/2024-0310-stoltzekleiven.jpg)
|
||||
|
||||
## new features
|
||||
|
||||
* #62 support for [identity providers](https://github.com/9001/copyparty#identity-providers) and automatically creating volumes for each user/group ("home folders")
|
||||
* login with passkeys / fido2 / webauthn / yubikey / ldap / active directory / oauth / many other single-sign-on contraptions
|
||||
* [documentation](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md) and [examples](https://github.com/9001/copyparty/tree/hovudstraum/docs/examples/docker/idp-authelia-traefik) could still use some help (I did my best)
|
||||
* #77 UI to cancel unfinished uploads (available in the 🧯 unpost tab) 3f05b665
|
||||
* the user's IP and username must match the upload by default; can be changed with global-option / volflag `u2abort`
|
||||
* new volflag `sparse` to pretend sparse files are supported even if the filesystem doesn't 8785d2f9
|
||||
* gives drastically better performance when writing to s3 buckets through juicefs/geesefs
|
||||
* only for when you know the filesystem can deal with it (so juicefs/geesefs is OK, but **definitely not** fat32)
|
||||
* `--xff-src` and `--ipa` now support CIDR notation (but the old syntax still works) b377791b
|
||||
* ux:
|
||||
* #74 option to use [custom fonts](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice) 263adec7 6cc7101d 8016e671
|
||||
* option to disable autoplay when page url contains a song hash 8413ed6d
|
||||
* good if you're using copyparty to listen to music at the office and the office policy is to have the webbrowser automatically restart to install updates, meaning your coworkers are suddenly and involuntarily enjoying some loud af jcore while you're asleep at home
|
||||
|
||||
## bugfixes
|
||||
|
||||
* don't panic if cloudflare (or another reverse-proxy) decides to hijack json responses and replace them with html 7741870d
|
||||
* #73 the fancy markdown editor was incompatible with caddy (a reverse-proxy) ac96fd9c
|
||||
* media player could get confused if neighboring folders had songs with the same filenames 206af8f1
|
||||
* benign race condition in the config reloader (could only be triggered by admins and/or SIGUSR1) 096de508
|
||||
* running tftp with optimizations enabled would cause issues for `--ipa` b377791b
|
||||
* cosmetic tftp bugs 115020ba
|
||||
* ux:
|
||||
* up2k rendering glitch if the last couple uploads were dupes 547a4863
|
||||
* up2k rendering glitch when switching between readonly/writeonly folders 51a83b04
|
||||
* markdown editor preview was glitchy on tiny screens e5582605
|
||||
|
||||
## other changes
|
||||
|
||||
* add a [sharex v12.1](https://github.com/9001/copyparty/tree/hovudstraum/contrib#sharexsxcu) config example 2527e903
|
||||
* make it easier to discover/diagnose issues with docker and/or reverse-proxy config d744f3ff
|
||||
* stop recommending the use of `--xff-src=any` in the log messages 7f08f10c
|
||||
* ux:
|
||||
* remove the `k304` togglebutton in the controlpanel by default 1c011ff0
|
||||
* mention that a full restart is required for `[global]` config changes to take effect 0c039219
|
||||
* docs e78af022
|
||||
* [how to use copyparty with amazon aws s3](https://github.com/9001/copyparty#using-the-cloud-as-storage)
|
||||
* faq: http/https confusion caused by incorrectly configured cloudflare
|
||||
* #76 docker: ftp-server howto
|
||||
* copyparty.exe: updated pyinstaller to 6.5.0 bdbcbbb0
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0221-2132 `v1.10.2` tall thumbs
|
||||
|
||||
## new features
|
||||
|
||||
* thumbnails can be way taller when centercrop is disabled in the browser UI 5026b212
|
||||
* good for folders with lots of portrait pics (no more letterboxing)
|
||||
* more thumbnail stuff:
|
||||
* zoom levels are twice as granular 5026b212
|
||||
* write-only folders get an "upload-only" icon 89c6c2e0
|
||||
* inaccessible files/folders get a 403/404 icon 8a38101e
|
||||
|
||||
## bugfixes
|
||||
|
||||
* tftp fixes d07859e8
|
||||
* server could crash if a nic disappeared / got restarted mid-transfer
|
||||
* tiny resource leak if dualstack causes ipv4 bind to fail
|
||||
* thumbnails:
|
||||
* when behind a caching proxy (cloudflare), icons in folders would be a random mix of png and svg 43ee6b9f
|
||||
* produce valid folder icons when thumbnails are disabled 14af136f
|
||||
* trailing newline in html responses d39a99c9
|
||||
|
||||
## other changes
|
||||
|
||||
* webdeps: update dompurify 13e77777
|
||||
* copyparty.exe: update jinja2, markupsafe, pyinstaller, upx 13e77777
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0218-1554 `v1.10.1` big thumbs
|
||||
|
||||
## new features
|
||||
|
||||
* button to enable hi-res thumbnails 33f41f3e 58ae38c6
|
||||
* enable with the `3x` button in the gridview
|
||||
* can be force-enabled/disabled serverside with `--th-x3` or volflag `th3x`
|
||||
* tftp: IPv6 support and UTF-8 filenames + optimizations 0504b010
|
||||
* ux:
|
||||
* when closing the image viewer, scroll to the last viewed pic bbc37990
|
||||
* respect `prefers-reduced-motion` some more places fbfdd833
|
||||
|
||||
## bugfixes
|
||||
|
||||
* #72 impossible to delete recently uploaded zerobyte files if database was disabled 6bd087dd
|
||||
* tftp now works in `copyparty.exe`, `copyparty32.exe`, `copyparty-winpe64.exe`
|
||||
* the [sharex config example](https://github.com/9001/copyparty/tree/hovudstraum/contrib#sharexsxcu) was still using cookie-auth 8ff7094e
|
||||
* ux:
|
||||
* prevent scrolling while a pic is open 7f1c9926
|
||||
* fix gridview in older firefox versions 7f1c9926
|
||||
|
||||
## other changes
|
||||
|
||||
* thumbnail center-cropping can be force-enabled/disabled serverside with `--th-crop` or volflag `crop`
|
||||
* replaces `--th-no-crop` which is now deprecated (but will continue to work)
|
||||
|
||||
----
|
||||
|
||||
this release contains a build of `copyparty-winpe64.exe` which is almost **entirely useless,** except for in *extremely specific scenarios*, namely the kind where a TFTP server could also be useful -- the [previous build](https://github.com/9001/copyparty/releases/download/v1.8.7/copyparty-winpe64.exe) was from [version 1.8.7](https://github.com/9001/copyparty/releases/tag/v1.8.7) (2023-07-23)
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0215-0000 `v1.10.0` tftp
|
||||
|
||||
## new features
|
||||
|
||||
* TFTP server d636316a 8796c09f acbb8267 02879713
|
||||
* based on [partftpy](https://github.com/9001/partftpy), has most essential features EXCEPT for [rfc7440](https://datatracker.ietf.org/doc/html/rfc7440) so WAN will be slow
|
||||
* is already doing real work out in the wild! see the fantastic quote in the [readme](https://github.com/9001/copyparty?tab=readme-ov-file#tftp-server)
|
||||
* detect some (un)common configuration mistakes
|
||||
* buggy reverse-proxy which strips away all URL parameters 136c0fdc
|
||||
* could cause the browser to get stuck in a refresh-loop
|
||||
* a volume on an sqlite-incompatible filesystem (a remote cifs server or such) and an up2k volume inside d4da3861
|
||||
* sqlite could deadlock or randomly throw exceptions; serverlog will now explain how to fix it
|
||||
* ie11: file selection with shift-up/down 64ad5853
|
||||
|
||||
## bugfixes
|
||||
|
||||
* prevent music playback from stopping at the end of a folder f262aee8
|
||||
* preloader will now proactively hunt for the next file to play as the last song is ending
|
||||
* in very specific scenarios, clients could be told their upload had finished processing a tiny bit too early, while the HDD was still busy taking in the last couple bytes 6f8a588c
|
||||
* so if you expected to find the complete file on the server HDD immediately as the final chunk got confirmed, that was not necessarily the case if your server HDD was severely overloaded to the point where closing a file takes half a minute
|
||||
* huge thx to friend with said overloaded server for finding all the crazy edge cases
|
||||
* ignore harmless javascript errors from easymde 879e83e2
|
||||
|
||||
## other changes
|
||||
|
||||
* the "copy currently playing song info to clipboard" button now excludes the uploader IP ed524d84
|
||||
* mention that enabling `-j0` can improve HDD load during uploads 5d92f4df
|
||||
* mention a debian-specific docker bug which prevents starting most containers (not just copyparty) 4e797a71
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0203-1533 `v1.9.31` eject
|
||||
|
||||
## new features
|
||||
|
||||
* disable mkdir / new-doc buttons until a name is provided d3db6d29
|
||||
* warning about browsers limiting the number of connections c354a38b
|
||||
|
||||
## bugfixes
|
||||
|
||||
* #71 stop videos from buffering in the background a17c267d
|
||||
* improve up2k ETA on slow networks / many connections c1180d6f
|
||||
* u2c: exclude-filter didn't apply to file deletions b2e23340
|
||||
* `--touch` / `re📅` didn't apply to zerobyte files 945170e2
|
||||
|
||||
## other changes
|
||||
|
||||
* notes on [hardlink/symlink conversion](https://github.com/9001/copyparty/blob/6c2c6090/docs/notes.sh#L35-L46) 6c2c6090
|
||||
* [lore](https://github.com/9001/copyparty/blob/hovudstraum/docs/notes.md#trivia--lore) b1cf5884
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0125-2252 `v1.9.30` retime
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
* [just the sfx](#just-the-sfx)
|
||||
* [build from release tarball](#build-from-release-tarball) - uses the included prebuilt webdeps
|
||||
* [complete release](#complete-release)
|
||||
* [todo](#todo) - roughly sorted by priority
|
||||
* [debugging](#debugging)
|
||||
* [music playback halting on phones](#music-playback-halting-on-phones) - mostly fine on android
|
||||
* [discarded ideas](#discarded-ideas)
|
||||
|
||||
|
||||
@@ -164,6 +165,7 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
|
||||
| PUT | `?xz` | (binary data) | compress with xz and write into file at URL |
|
||||
| mPOST | | `f=FILE` | upload `FILE` into the folder at URL |
|
||||
| mPOST | `?j` | `f=FILE` | ...and reply with json |
|
||||
| mPOST | `?replace` | `f=FILE` | ...and overwrite existing files |
|
||||
| mPOST | | `act=mkdir`, `name=foo` | create directory `foo` at URL |
|
||||
| POST | `?delete` | | delete URL recursively |
|
||||
| jPOST | `?delete` | `["/foo","/bar"]` | delete `/foo` and `/bar` recursively |
|
||||
@@ -218,7 +220,7 @@ if you don't need all the features, you can repack the sfx and save a bunch of s
|
||||
* `269k` after `./scripts/make-sfx.sh re no-cm no-hl`
|
||||
|
||||
the features you can opt to drop are
|
||||
* `cm`/easymde, the "fancy" markdown editor, saves ~82k
|
||||
* `cm`/easymde, the "fancy" markdown editor, saves ~89k
|
||||
* `hl`, prism, the syntax hilighter, saves ~41k
|
||||
* `fnt`, source-code-pro, the monospace font, saves ~9k
|
||||
* `dd`, the custom mouse cursor for the media player tray tab, saves ~2k
|
||||
@@ -242,6 +244,7 @@ python3 -m venv .venv
|
||||
pip install jinja2 strip_hints # MANDATORY
|
||||
pip install mutagen # audio metadata
|
||||
pip install pyftpdlib # ftp server
|
||||
pip install partftpy # tftp server
|
||||
pip install impacket # smb server -- disable Windows Defender if you REALLY need this on windows
|
||||
pip install Pillow pyheif-pillow-opener pillow-avif-plugin # thumbnails
|
||||
pip install pyvips # faster thumbnails
|
||||
@@ -299,15 +302,26 @@ in the `scripts` folder:
|
||||
* run `./rls.sh 1.2.3` which uploads to pypi + creates github release + sfx
|
||||
|
||||
|
||||
# todo
|
||||
# debugging
|
||||
|
||||
roughly sorted by priority
|
||||
## music playback halting on phones
|
||||
|
||||
* nothing! currently
|
||||
mostly fine on android, but still haven't find a way to massage iphones into behaving well
|
||||
|
||||
* conditionally starting/stopping mp.fau according to mp.au.readyState <3 or <4 doesn't help
|
||||
* loop=true doesn't work, and manually looping mp.fau from an onended also doesn't work (it does nothing)
|
||||
* assigning fau.currentTime in a timer doesn't work, as safari merely pretends to assign it
|
||||
* on ios 16.7.7, mp.fau can sometimes make everything visibly work correctly, but no audio is actually hitting the speakers
|
||||
|
||||
can be reproduced with `--no-sendfile --s-wr-sz 8192 --s-wr-slp 0.3 --rsp-slp 6` and then play a collection of small audio files with the screen off, `ffmpeg -i track01.cdda.flac -c:a libopus -b:a 128k -segment_time 12 -f segment smol-%02d.opus`
|
||||
|
||||
|
||||
## discarded ideas
|
||||
|
||||
* optimization attempts which didn't improve performance
|
||||
* remove brokers / multiprocessing stuff; https://github.com/9001/copyparty/tree/no-broker
|
||||
* reduce the nesting / indirections in `HttpCli` / `httpcli.py`
|
||||
* nearly zero benefit from stuff like replacing all the `self.conn.hsrv` with a local `hsrv` variable
|
||||
* reduce up2k roundtrips
|
||||
* start from a chunk index and just go
|
||||
* terminate client on bad data
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
# q, lo: /cfg/log/%Y-%m%d.log # log to file instead of docker
|
||||
|
||||
# ftp: 3921 # enable ftp server on port 3921
|
||||
# p: 3939 # listen on another port
|
||||
# ipa: 10.89. # only allow connections from 10.89.*
|
||||
# df: 16 # stop accepting uploads if less than 16 GB free disk space
|
||||
|
||||
50
docs/examples/docker/idp-authelia-traefik/README.md
Normal file
50
docs/examples/docker/idp-authelia-traefik/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
> [!WARNING]
|
||||
> I am unable to guarantee the quality, safety, and security of anything in this folder; it is a combination of examples I found online. Please submit corrections or improvements 🙏
|
||||
|
||||
to try this out with minimal adjustments:
|
||||
* specify what filesystem-path to share with copyparty, replacing the default/example value `/srv/pub` in `docker-compose.yml`
|
||||
* add `127.0.0.1 fs.example.com traefik.example.com authelia.example.com` to your `/etc/hosts`
|
||||
* `sudo docker-compose up`
|
||||
* login to https://fs.example.com/ with username `authelia` password `authelia`
|
||||
|
||||
to use this in a safe and secure manner:
|
||||
* follow a guide on setting up authelia properly (TODO:link) and use the copyparty-specific parts of this folder as inspiration for your own config; namely the `cpp` subfolder and the `copyparty` service in `docker-compose.yml`
|
||||
|
||||
this folder is based on:
|
||||
* https://github.com/authelia/authelia/tree/39763aaed24c4abdecd884b47357a052b235942d/examples/compose/lite
|
||||
|
||||
incomplete list of modifications made:
|
||||
* support for running with podman as root on fedora (`:z` volumes, `label:disable`)
|
||||
* explicitly using authelia `v4.38.0-beta3` because config syntax changed since last stable release
|
||||
* disabled automatic letsencrypt certificate signing
|
||||
* reduced logging from debug to info
|
||||
* added a warning that traefik is given access to the docker socket (as recommended by traefik docs) which means traefik is able to break out of the container and has full root access on the host machine
|
||||
|
||||
|
||||
# security
|
||||
|
||||
there is probably/definitely room for improvement in this example setup. Some ideas taken from [github issue #62](https://github.com/9001/copyparty/issues/62):
|
||||
|
||||
* Add in a redis password to limit attacker lateral movement in the system
|
||||
* Move redis to a private network shared with just authelia
|
||||
* Pin to image hashes (or go all in on updates and add `watchtower`)
|
||||
* Drop bridge networking for just exposing traefik's public ports
|
||||
* Configure docker for non-root access to docker socket and then move traefik to use [non-root perms](https://docs.docker.com/engine/security/rootless/)
|
||||
|
||||
if you manage to improve on any of this, especially in a way that might be useful for other people, consider sending a PR :>
|
||||
|
||||
|
||||
# performance
|
||||
|
||||
currently **not optimal,** at least when compared to running the python sfx outside of docker... some numbers from my laptop (ryzen4500u/fedora39):
|
||||
|
||||
| req/s | https D/L | http D/L | approach |
|
||||
| -----:| ----------:|:--------:| -------- |
|
||||
| 5200 | 1294 MiB/s | 5+ GiB/s | [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) running on host |
|
||||
| 4370 | 725 MiB/s | 4+ GiB/s | `docker run copyparty/ac` |
|
||||
| 2420 | 694 MiB/s | n/a | `copyparty/ac` behind traefik |
|
||||
| 75 | 694 MiB/s | n/a | traefik and authelia **(you are here)** |
|
||||
|
||||
authelia is behaving strangely, handling 340 requests per second for a while, but then it suddenly drops to 75 and stays there...
|
||||
|
||||
I'm assuming all of the performance issues is due to a misconfiguration of authelia/traefik/docker on my end, but I don't relly know where to start
|
||||
@@ -0,0 +1,66 @@
|
||||
# based on https://github.com/authelia/authelia/blob/39763aaed24c4abdecd884b47357a052b235942d/examples/compose/lite/authelia/configuration.yml
|
||||
|
||||
# Authelia configuration
|
||||
|
||||
# This secret can also be set using the env variables AUTHELIA_JWT_SECRET_FILE
|
||||
jwt_secret: a_very_important_secret
|
||||
|
||||
server:
|
||||
address: 'tcp://:9091'
|
||||
|
||||
log:
|
||||
level: info # debug
|
||||
|
||||
totp:
|
||||
issuer: authelia.com
|
||||
|
||||
authentication_backend:
|
||||
file:
|
||||
path: /config/users_database.yml
|
||||
|
||||
access_control:
|
||||
default_policy: deny
|
||||
rules:
|
||||
# Rules applied to everyone
|
||||
- domain: traefik.example.com
|
||||
policy: one_factor
|
||||
- domain: fs.example.com
|
||||
policy: one_factor
|
||||
|
||||
session:
|
||||
# This secret can also be set using the env variables AUTHELIA_SESSION_SECRET_FILE
|
||||
secret: unsecure_session_secret
|
||||
|
||||
cookies:
|
||||
- name: authelia_session
|
||||
domain: example.com # Should match whatever your root protected domain is
|
||||
default_redirection_url: https://fs.example.com
|
||||
authelia_url: https://authelia.example.com/
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
|
||||
redis:
|
||||
host: redis
|
||||
port: 6379
|
||||
# This secret can also be set using the env variables AUTHELIA_SESSION_REDIS_PASSWORD_FILE
|
||||
# password: authelia
|
||||
|
||||
regulation:
|
||||
max_retries: 3
|
||||
find_time: 120
|
||||
ban_time: 300
|
||||
|
||||
storage:
|
||||
encryption_key: you_must_generate_a_random_string_of_more_than_twenty_chars_and_configure_this
|
||||
local:
|
||||
path: /config/db.sqlite3
|
||||
|
||||
notifier:
|
||||
disable_startup_check: true
|
||||
smtp:
|
||||
username: test
|
||||
# This secret can also be set using the env variables AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE
|
||||
password: password
|
||||
host: mail.example.com
|
||||
port: 25
|
||||
sender: admin@example.com
|
||||
@@ -0,0 +1,18 @@
|
||||
# based on https://github.com/authelia/authelia/blob/39763aaed24c4abdecd884b47357a052b235942d/examples/compose/lite/authelia/users_database.yml
|
||||
|
||||
# Users Database
|
||||
|
||||
# This file can be used if you do not have an LDAP set up.
|
||||
|
||||
# List of users
|
||||
users:
|
||||
authelia:
|
||||
disabled: false
|
||||
displayname: "Authelia User"
|
||||
# Password is authelia
|
||||
password: "$6$rounds=50000$BpLnfgDsc2WD8F2q$Zis.ixdg9s/UOJYrs56b5QEZFiZECu0qZVNsIYxBaNJ7ucIL.nlxVCT5tqh8KHG8X4tlwCFm5r6NTOZZ5qRFN/"
|
||||
email: authelia@authelia.com
|
||||
groups:
|
||||
- admins
|
||||
- dev
|
||||
- su
|
||||
82
docs/examples/docker/idp-authelia-traefik/cpp/copyparty.conf
Normal file
82
docs/examples/docker/idp-authelia-traefik/cpp/copyparty.conf
Normal file
@@ -0,0 +1,82 @@
|
||||
# not actually YAML but lets pretend:
|
||||
# -*- mode: yaml -*-
|
||||
# vim: ft=yaml:
|
||||
|
||||
|
||||
# example config for how authelia can be used to replace
|
||||
# copyparty's built-in authentication/authorization mechanism,
|
||||
# providing copyparty with HTTP headers through traefik to
|
||||
# signify who the user is, and what groups they belong to
|
||||
#
|
||||
# the filesystem-path that will be shared with copyparty is
|
||||
# specified in the docker-compose in the parent folder, where
|
||||
# a real filesystem-path is mapped onto this container's path `/w`,
|
||||
# meaning `/w` in this config-file is actually `/srv/pub` in the
|
||||
# outside world (assuming you didn't modify that value)
|
||||
|
||||
|
||||
[global]
|
||||
e2dsa # enable file indexing and filesystem scanning
|
||||
e2ts # enable multimedia indexing
|
||||
ansi # enable colors in log messages
|
||||
#q # disable logging for more performance
|
||||
|
||||
# if we are confident that we got the docker-network config correct
|
||||
# (meaning copyparty is only accessible through traefik, and
|
||||
# traefik makes sure that all requests go through authelia),
|
||||
# then accept X-Forwarded-For and IdP headers from any private IP:
|
||||
xff-src: lan
|
||||
|
||||
# enable IdP support by expecting username/groupname in
|
||||
# http-headers provided by the reverse-proxy; header "X-IdP-User"
|
||||
# will contain the username, "X-IdP-Group" the groupname
|
||||
idp-h-usr: remote-user
|
||||
idp-h-grp: remote-groups
|
||||
|
||||
# DEBUG: show all incoming request headers from traefik/authelia
|
||||
#ihead: *
|
||||
|
||||
|
||||
[/] # create a volume at "/" (the webroot), which will
|
||||
/w # share /w (the docker data volume, which is mapped to /srv/pub on the host in docker-compose.yml)
|
||||
accs:
|
||||
rw: * # everyone gets read-access, but
|
||||
rwmda: @su # the group "su" gets read-write-move-delete-admin
|
||||
|
||||
|
||||
[/u/${u}] # each user gets their own home-folder at /u/username
|
||||
/w/u/${u} # which will be "u/username" in the docker data volume
|
||||
accs:
|
||||
r: * # read-access for anyone, and
|
||||
rwmda: ${u}, @su # read-write-move-delete-admin for that username + the "su" group
|
||||
|
||||
|
||||
[/u/${u}/priv] # each user also gets a private area at /u/username/priv
|
||||
/w/u/${u}/priv # stored at DATAVOLUME/u/username/priv
|
||||
accs:
|
||||
rwmda: ${u}, @su # read-write-move-delete-admin for that username + the "su" group
|
||||
|
||||
|
||||
[/lounge/${g}] # each group gets their own shared volume
|
||||
/w/lounge/${g} # stored at DATAVOLUME/lounge/groupname
|
||||
accs:
|
||||
r: * # read-access for anyone, and
|
||||
rwmda: @${g}, @su # read-write-move-delete-admin for that group + the "su" group
|
||||
|
||||
|
||||
[/lounge/${g}/priv] # and a private area for each group too
|
||||
/w/lounge/${g}/priv # stored at DATAVOLUME/lounge/groupname/priv
|
||||
accs:
|
||||
rwmda: @${g}, @su # read-write-move-delete-admin for that group + the "su" group
|
||||
|
||||
|
||||
# and create some strategic volumes to prevent anyone from gaining
|
||||
# unintended access to priv folders if the users/groups db is lost
|
||||
[/u]
|
||||
/w/u
|
||||
accs:
|
||||
rwmda: @su
|
||||
[/lounge]
|
||||
/w/lounge
|
||||
accs:
|
||||
rwmda: @su
|
||||
99
docs/examples/docker/idp-authelia-traefik/docker-compose.yml
Normal file
99
docs/examples/docker/idp-authelia-traefik/docker-compose.yml
Normal file
@@ -0,0 +1,99 @@
|
||||
version: '3.3'
|
||||
|
||||
networks:
|
||||
net:
|
||||
driver: bridge
|
||||
|
||||
services:
|
||||
copyparty:
|
||||
image: copyparty/ac
|
||||
container_name: idp_copyparty
|
||||
user: "1000:1000" # should match the user/group of your fileshare volumes
|
||||
volumes:
|
||||
- ./cpp/:/cfg:z # the copyparty config folder
|
||||
- /srv/pub:/w:z # this is where we declare that "/srv/pub" is the filesystem-path on the server that shall be shared online
|
||||
networks:
|
||||
- net
|
||||
expose:
|
||||
- 3923
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.copyparty.rule=Host(`fs.example.com`)'
|
||||
- 'traefik.http.routers.copyparty.entrypoints=https'
|
||||
- 'traefik.http.routers.copyparty.tls=true'
|
||||
- 'traefik.http.routers.copyparty.middlewares=authelia@docker'
|
||||
stop_grace_period: 15s # thumbnailer is allowed to continue finishing up for 10s after the shutdown signal
|
||||
|
||||
authelia:
|
||||
image: authelia/authelia:v4.38.0-beta3 # the config files in the authelia folder use the new syntax
|
||||
container_name: idp_authelia
|
||||
volumes:
|
||||
- ./authelia:/config:z
|
||||
networks:
|
||||
- net
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.authelia.rule=Host(`authelia.example.com`)'
|
||||
- 'traefik.http.routers.authelia.entrypoints=https'
|
||||
- 'traefik.http.routers.authelia.tls=true'
|
||||
#- 'traefik.http.routers.authelia.tls.certresolver=letsencrypt' # uncomment this to enable automatic certificate signing (1/2)
|
||||
- 'traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/authz/forward-auth?authelia_url=https://authelia.example.com'
|
||||
- 'traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true'
|
||||
- 'traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email'
|
||||
expose:
|
||||
- 9091
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
disable: true
|
||||
environment:
|
||||
- TZ=Etc/UTC
|
||||
|
||||
redis:
|
||||
image: redis:7.2.4-alpine3.19
|
||||
container_name: idp_redis
|
||||
volumes:
|
||||
- ./redis:/data:z
|
||||
networks:
|
||||
- net
|
||||
expose:
|
||||
- 6379
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Etc/UTC
|
||||
|
||||
traefik:
|
||||
image: traefik:2.11.0
|
||||
container_name: idp_traefik
|
||||
volumes:
|
||||
- ./traefik:/etc/traefik:z
|
||||
- /var/run/docker.sock:/var/run/docker.sock # WARNING: this gives traefik full root-access to the host OS, but is recommended/required(?) by traefik
|
||||
security_opt:
|
||||
- label:disable # disable selinux because it (rightly) blocks access to docker.sock
|
||||
networks:
|
||||
- net
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.api.rule=Host(`traefik.example.com`)'
|
||||
- 'traefik.http.routers.api.entrypoints=https'
|
||||
- 'traefik.http.routers.api.service=api@internal'
|
||||
- 'traefik.http.routers.api.tls=true'
|
||||
#- 'traefik.http.routers.api.tls.certresolver=letsencrypt' # uncomment this to enable automatic certificate signing (2/2)
|
||||
- 'traefik.http.routers.api.middlewares=authelia@docker'
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
command:
|
||||
- '--api'
|
||||
- '--providers.docker=true'
|
||||
- '--providers.docker.exposedByDefault=false'
|
||||
- '--entrypoints.http=true'
|
||||
- '--entrypoints.http.address=:80'
|
||||
- '--entrypoints.http.http.redirections.entrypoint.to=https'
|
||||
- '--entrypoints.http.http.redirections.entrypoint.scheme=https'
|
||||
- '--entrypoints.https=true'
|
||||
- '--entrypoints.https.address=:443'
|
||||
- '--certificatesResolvers.letsencrypt.acme.email=your-email@your-domain.com'
|
||||
- '--certificatesResolvers.letsencrypt.acme.storage=/etc/traefik/acme.json'
|
||||
- '--certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=http'
|
||||
- '--log=true'
|
||||
- '--log.level=WARNING' # DEBUG
|
||||
12
docs/examples/docker/idp-authentik-traefik/README.md
Normal file
12
docs/examples/docker/idp-authentik-traefik/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
> [!WARNING]
|
||||
> I am unable to guarantee the quality, safety, and security of anything in this folder; it is a combination of examples I found online. Please submit corrections or improvements 🙏
|
||||
|
||||
> [!WARNING]
|
||||
> does not work yet... if you are able to fix this, please do!
|
||||
|
||||
this is based on:
|
||||
* https://goauthentik.io/docker-compose.yml
|
||||
* https://goauthentik.io/docs/providers/proxy/server_traefik
|
||||
|
||||
incomplete list of modifications made:
|
||||
* support for running with podman as root on fedora (`:z` volumes, `label:disable`)
|
||||
@@ -0,0 +1,88 @@
|
||||
# https://goauthentik.io/docker-compose.yml
|
||||
---
|
||||
version: "3.4"
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
image: docker.io/library/postgres:12-alpine
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||
start_period: 20s
|
||||
interval: 30s
|
||||
retries: 5
|
||||
timeout: 5s
|
||||
volumes:
|
||||
- database:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${PG_PASS:?database password required}
|
||||
POSTGRES_USER: ${PG_USER:-authentik}
|
||||
POSTGRES_DB: ${PG_DB:-authentik}
|
||||
env_file:
|
||||
- .env
|
||||
redis:
|
||||
image: docker.io/library/redis:alpine
|
||||
command: --save 60 1 --loglevel warning
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
||||
start_period: 20s
|
||||
interval: 30s
|
||||
retries: 5
|
||||
timeout: 3s
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.2.1}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
AUTHENTIK_REDIS__HOST: redis
|
||||
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
volumes:
|
||||
- ./media:/media
|
||||
- ./custom-templates:/templates
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "${COMPOSE_PORT_HTTP:-9000}:9000"
|
||||
- "${COMPOSE_PORT_HTTPS:-9443}:9443"
|
||||
depends_on:
|
||||
- postgresql
|
||||
- redis
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.2.1}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
AUTHENTIK_REDIS__HOST: redis
|
||||
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
# `user: root` and the docker socket volume are optional.
|
||||
# See more for the docker socket integration here:
|
||||
# https://goauthentik.io/docs/outposts/integrations/docker
|
||||
# Removing `user: root` also prevents the worker from fixing the permissions
|
||||
# on the mounted folders, so when removing this make sure the folders have the correct UID/GID
|
||||
# (1000:1000 by default)
|
||||
user: root
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./media:/media
|
||||
- ./certs:/certs
|
||||
- ./custom-templates:/templates
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- postgresql
|
||||
- redis
|
||||
|
||||
volumes:
|
||||
database:
|
||||
driver: local
|
||||
redis:
|
||||
driver: local
|
||||
@@ -0,0 +1,46 @@
|
||||
# https://goauthentik.io/docs/providers/proxy/server_traefik
|
||||
---
|
||||
version: "3.7"
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v2.2
|
||||
container_name: traefik
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- 80:80
|
||||
command:
|
||||
- "--api"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedByDefault=false"
|
||||
- "--entrypoints.web.address=:80"
|
||||
|
||||
authentik-proxy:
|
||||
image: ghcr.io/goauthentik/proxy
|
||||
ports:
|
||||
- 9000:9000
|
||||
- 9443:9443
|
||||
environment:
|
||||
AUTHENTIK_HOST: https://your-authentik.tld
|
||||
AUTHENTIK_INSECURE: "false"
|
||||
AUTHENTIK_TOKEN: token-generated-by-authentik
|
||||
# Starting with 2021.9, you can optionally set this too
|
||||
# when authentik_host for internal communication doesn't match the public URL
|
||||
# AUTHENTIK_HOST_BROWSER: https://external-domain.tld
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.port: 9000
|
||||
traefik.http.routers.authentik.rule: Host(`app.company`) && PathPrefix(`/outpost.goauthentik.io/`)
|
||||
# `authentik-proxy` refers to the service name in the compose file.
|
||||
traefik.http.middlewares.authentik.forwardauth.address: http://authentik-proxy:9000/outpost.goauthentik.io/auth/traefik
|
||||
traefik.http.middlewares.authentik.forwardauth.trustForwardHeader: true
|
||||
traefik.http.middlewares.authentik.forwardauth.authResponseHeaders: X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid,X-authentik-jwt,X-authentik-meta-jwks,X-authentik-meta-outpost,X-authentik-meta-provider,X-authentik-meta-app,X-authentik-meta-version
|
||||
restart: unless-stopped
|
||||
|
||||
whoami:
|
||||
image: containous/whoami
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.whoami.rule: Host(`app.company`)
|
||||
traefik.http.routers.whoami.middlewares: authentik@docker
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,72 @@
|
||||
# not actually YAML but lets pretend:
|
||||
# -*- mode: yaml -*-
|
||||
# vim: ft=yaml:
|
||||
|
||||
|
||||
# example config for how copyparty can be used with an identity
|
||||
# provider, replacing the built-in authentication/authorization
|
||||
# mechanism, and instead expecting the reverse-proxy to provide
|
||||
# the requester's username (and possibly a group-name, for
|
||||
# optional group-based access control)
|
||||
#
|
||||
# the filesystem-path `/w` is used as the storage location
|
||||
# because that is the data-volume in the docker containers,
|
||||
# because a deployment like this (with an IdP) is more commonly
|
||||
# seen in containerized environments -- but this is not required
|
||||
|
||||
|
||||
[global]
|
||||
e2dsa # enable file indexing and filesystem scanning
|
||||
e2ts # enable multimedia indexing
|
||||
ansi # enable colors in log messages
|
||||
|
||||
# enable IdP support by expecting username/groupname in
|
||||
# http-headers provided by the reverse-proxy; header "X-IdP-User"
|
||||
# will contain the username, "X-IdP-Group" the groupname
|
||||
idp-h-usr: x-idp-user
|
||||
idp-h-grp: x-idp-group
|
||||
|
||||
|
||||
[/] # create a volume at "/" (the webroot), which will
|
||||
/w # share /w (the docker data volume, which is mapped to /srv/pub on the host in docker-compose.yml)
|
||||
accs:
|
||||
rw: * # everyone gets read-access, but
|
||||
rwmda: @su # the group "su" gets read-write-move-delete-admin
|
||||
|
||||
|
||||
[/u/${u}] # each user gets their own home-folder at /u/username
|
||||
/w/u/${u} # which will be "u/username" in the docker data volume
|
||||
accs:
|
||||
r: * # read-access for anyone, and
|
||||
rwmda: ${u}, @su # read-write-move-delete-admin for that username + the "su" group
|
||||
|
||||
|
||||
[/u/${u}/priv] # each user also gets a private area at /u/username/priv
|
||||
/w/u/${u}/priv # stored at DATAVOLUME/u/username/priv
|
||||
accs:
|
||||
rwmda: ${u}, @su # read-write-move-delete-admin for that username + the "su" group
|
||||
|
||||
|
||||
[/lounge/${g}] # each group gets their own shared volume
|
||||
/w/lounge/${g} # stored at DATAVOLUME/lounge/groupname
|
||||
accs:
|
||||
r: * # read-access for anyone, and
|
||||
rwmda: @${g}, @su # read-write-move-delete-admin for that group + the "su" group
|
||||
|
||||
|
||||
[/lounge/${g}/priv] # and a private area for each group too
|
||||
/w/lounge/${g}/priv # stored at DATAVOLUME/lounge/groupname/priv
|
||||
accs:
|
||||
rwmda: @${g}, @su # read-write-move-delete-admin for that group + the "su" group
|
||||
|
||||
|
||||
# and create some strategic volumes to prevent anyone from gaining
|
||||
# unintended access to priv folders if the users/groups db is lost
|
||||
[/u]
|
||||
/w/u
|
||||
accs:
|
||||
rwmda: @su
|
||||
[/lounge]
|
||||
/w/lounge
|
||||
accs:
|
||||
rwmda: @su
|
||||
131
docs/examples/docker/idp-authentik-traefik/docker-compose.yml
Normal file
131
docs/examples/docker/idp-authentik-traefik/docker-compose.yml
Normal file
@@ -0,0 +1,131 @@
|
||||
version: "3.4"
|
||||
|
||||
volumes:
|
||||
database:
|
||||
driver: local
|
||||
redis:
|
||||
driver: local
|
||||
|
||||
services:
|
||||
copyparty:
|
||||
image: copyparty/ac
|
||||
container_name: idp_copyparty
|
||||
restart: unless-stopped
|
||||
user: "1000:1000" # should match the user/group of your fileshare volumes
|
||||
volumes:
|
||||
- ./cpp/:/cfg:z # the copyparty config folder
|
||||
- /srv/pub:/w:z # this is where we declare that "/srv/pub" is the filesystem-path on the server that shall be shared online
|
||||
ports:
|
||||
- 3923
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.fs.rule=Host(`fs.example.com`)'
|
||||
- 'traefik.http.routers.fs.entrypoints=http'
|
||||
#- 'traefik.http.routers.fs.middlewares=authelia@docker' # TODO: ???
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget --spider -q 127.0.0.1:3923/?reset"]
|
||||
interval: 1m
|
||||
timeout: 2s
|
||||
retries: 5
|
||||
start_period: 15s
|
||||
stop_grace_period: 15s # thumbnailer is allowed to continue finishing up for 10s after the shutdown signal
|
||||
|
||||
traefik:
|
||||
image: traefik:v2.11
|
||||
container_name: traefik
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock # WARNING: this gives traefik full root-access to the host OS, but is recommended/required(?) by traefik
|
||||
security_opt:
|
||||
- label:disable # disable selinux because it (rightly) blocks access to docker.sock
|
||||
ports:
|
||||
- 80:80
|
||||
command:
|
||||
- '--api'
|
||||
- '--providers.docker=true'
|
||||
- '--providers.docker.exposedByDefault=false'
|
||||
- '--entrypoints.web.address=:80'
|
||||
|
||||
postgresql:
|
||||
image: docker.io/library/postgres:12-alpine
|
||||
container_name: idp_postgresql
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||
start_period: 20s
|
||||
interval: 30s
|
||||
retries: 5
|
||||
timeout: 5s
|
||||
volumes:
|
||||
- database:/var/lib/postgresql/data:z
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgrass
|
||||
POSTGRES_USER: authentik
|
||||
POSTGRES_DB: authentik
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
redis:
|
||||
image: docker.io/library/redis:alpine
|
||||
command: --save 60 1 --loglevel warning
|
||||
container_name: idp_redis
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
||||
start_period: 20s
|
||||
interval: 30s
|
||||
retries: 5
|
||||
timeout: 3s
|
||||
volumes:
|
||||
- redis:/data:z
|
||||
|
||||
authentik_server:
|
||||
image: ghcr.io/goauthentik/server:2024.2.1
|
||||
container_name: idp_authentik_server
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
AUTHENTIK_REDIS__HOST: redis
|
||||
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||
AUTHENTIK_POSTGRESQL__USER: authentik
|
||||
AUTHENTIK_POSTGRESQL__NAME: authentik
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: postgrass
|
||||
volumes:
|
||||
- ./media:/media:z
|
||||
- ./custom-templates:/templates:z
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- 9000
|
||||
- 9443
|
||||
depends_on:
|
||||
- postgresql
|
||||
- redis
|
||||
|
||||
authentik_worker:
|
||||
image: ghcr.io/goauthentik/server:2024.2.1
|
||||
container_name: idp_authentik_worker
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
AUTHENTIK_REDIS__HOST: redis
|
||||
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||
AUTHENTIK_POSTGRESQL__USER: authentik
|
||||
AUTHENTIK_POSTGRESQL__NAME: authentik
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: postgrass
|
||||
# `user: root` and the docker socket volume are optional.
|
||||
# See more for the docker socket integration here:
|
||||
# https://goauthentik.io/docs/outposts/integrations/docker
|
||||
# Removing `user: root` also prevents the worker from fixing the permissions
|
||||
# on the mounted folders, so when removing this make sure the folders have the correct UID/GID
|
||||
# (1000:1000 by default)
|
||||
user: root
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./media:/media:z
|
||||
- ./certs:/certs:z
|
||||
- ./custom-templates:/templates:z
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- postgresql
|
||||
- redis
|
||||
@@ -26,6 +26,24 @@
|
||||
idp-h-usr: x-idp-user
|
||||
idp-h-grp: x-idp-group
|
||||
|
||||
# but copyparty will refuse to accept those headers unless you
|
||||
# tell it the LAN IP of the reverse-proxy to expect them from,
|
||||
# preventing malicious users from pretending to be the proxy;
|
||||
# pay attention to the warning message in the logs and then
|
||||
# adjust the following config option accordingly:
|
||||
xff-src: 192.168.0.0/16
|
||||
|
||||
# or just allow all LAN / private IPs (probably good enough):
|
||||
xff-src: lan
|
||||
|
||||
# an additional, optional security measure is to expect a
|
||||
# secret header name from the reverse-proxy; you can enable
|
||||
# this feature by setting the header-name to expect here:
|
||||
#idp-h-key: shangala-bangala
|
||||
|
||||
# convenient debug option:
|
||||
# log all incoming request headers from the proxy
|
||||
#ihead: *
|
||||
|
||||
[/] # create a volume at "/" (the webroot), which will
|
||||
/w # share /w (the docker data volume)
|
||||
|
||||
22
docs/idp.md
Normal file
22
docs/idp.md
Normal file
@@ -0,0 +1,22 @@
|
||||
there is a [docker-compose example](./examples/docker/idp-authelia-traefik) which is hopefully a good starting point (meaning you can skip the steps below) -- but if you want to set this up from scratch yourself (or learn about how it works), keep reading:
|
||||
|
||||
to configure IdP from scratch, you must place copyparty behind a reverse-proxy which sends all requests through a middleware (the IdP / identity-provider service) which will inject a set of headers into the requests, telling copyparty who the user is
|
||||
|
||||
in the copyparty `[global]` config, specify which headers to read client info from; username is required (`idp-h-usr: X-Authooley-User`), group(s) are optional (`idp-h-grp: X-Authooley-Groups`)
|
||||
|
||||
* it is also required to specify the subnet that legit requests will be coming from, for example `--xff-src=10.88.0.0/24` to allow 10.88.x.x (or `--xff-src=lan` for all private IPs), and it is recommended to configure the reverseproxy to include a secret header as proof that the other headers are also legit (and not smuggled in by a malicious client), telling copyparty the headername to expect with `idp-h-key: shangala-bangala`
|
||||
|
||||
|
||||
# important notes
|
||||
|
||||
## IdP volumes are forgotten on shutdown
|
||||
|
||||
IdP volumes, meaning dynamically-created volumes, meaning volumes that contain `${u}` or `${g}` in their URL, will be forgotten during a server restart and then "revived" when the volume's owner sends their first request after the restart
|
||||
|
||||
until each IdP volume is revived, it will inherit the permissions of its parent volume (if any)
|
||||
|
||||
this means that, if an IdP volume is located inside a folder that is readable by anyone, then each of those IdP volumes will **also become readable by anyone** until the volume is revived
|
||||
|
||||
and likewise -- if the IdP volume is inside a folder that is only accessible by certain users, but the IdP volume is configured to allow access from unauthenticated users, then the contents of the volume will NOT be accessible until it is revived
|
||||
|
||||
until this limitation is fixed (if ever), it is recommended to place IdP volumes inside an appropriate parent volume, so they can inherit acceptable permissions until their revival; see the "strategic volumes" at the bottom of [./examples/docker/idp/copyparty.conf](./examples/docker/idp/copyparty.conf)
|
||||
@@ -24,6 +24,10 @@ https://github.com/giampaolo/pyftpdlib/
|
||||
C: 2007 Giampaolo Rodola
|
||||
L: MIT
|
||||
|
||||
https://github.com/9001/partftpy
|
||||
C: 2010-2021 Michael P. Soulier
|
||||
L: MIT
|
||||
|
||||
https://github.com/nayuki/QR-Code-generator/
|
||||
C: Project Nayuki
|
||||
L: MIT
|
||||
|
||||
129
docs/nuitka.txt
129
docs/nuitka.txt
@@ -1,82 +1,71 @@
|
||||
# recipe for building an exe with nuitka (extreme jank edition)
|
||||
#
|
||||
# NOTE: win7 and win10 builds both work on win10 but
|
||||
# on win7 they immediately c0000005 in kernelbase.dll
|
||||
#
|
||||
# first install python-3.6.8-amd64.exe
|
||||
# [x] add to path
|
||||
#
|
||||
|
||||
NOTE: copyparty runs SLOWER when compiled with nuitka;
|
||||
just use copyparty-sfx.py and/or pyinstaller instead
|
||||
|
||||
( the sfx and the pyinstaller EXEs are equally fast if you
|
||||
have the latest jinja2 installed, but the older jinja that
|
||||
comes bundled with the sfx is slightly faster yet )
|
||||
|
||||
roughly, copyparty-sfx.py is 6% faster than copyparty.exe
|
||||
(win10-pyinstaller), and copyparty.exe is 10% faster than
|
||||
nuitka, making copyparty-sfx.py 17% faster than nuitka
|
||||
|
||||
NOTE: every time a nuitka-compiled copyparty.exe is launched,
|
||||
it will show the windows firewall prompt since nuitka will
|
||||
pick a new unique location in %TEMP% to unpack an exe into,
|
||||
unlike pyinstaller which doesn't fork itself on startup...
|
||||
might be fixable by configuring nuitka differently, idk
|
||||
|
||||
NOTE: nuitka EXEs are larger than pyinstaller ones;
|
||||
a minimal nuitka build of just the sfx (with its bundled
|
||||
dependencies) was already the same size as the pyinstaller
|
||||
copyparty.exe which also includes Mutagen and Pillow
|
||||
|
||||
NOTE: nuitka takes a lot longer to build than pyinstaller
|
||||
(due to actual compilation of course, but still)
|
||||
|
||||
NOTE: binaries built with nuitka cannot run on windows7,
|
||||
even when compiled with python 3.6 on windows 7 itself
|
||||
|
||||
NOTE: `--python-flags=-m` is the magic sauce to
|
||||
correctly compile `from .util import Daemon`
|
||||
(which otherwise only explodes at runtime)
|
||||
|
||||
NOTE: `--deployment` doesn't seem to affect performance
|
||||
|
||||
########################################################################
|
||||
# copypaste the rest of this file into cmd
|
||||
|
||||
rem from pypi
|
||||
cd \users\ed\downloads
|
||||
python -m pip install --user Nuitka-0.6.14.7.tar.gz
|
||||
|
||||
rem https://github.com/brechtsanders/winlibs_mingw/releases/download/10.2.0-11.0.0-8.0.0-r5/winlibs-x86_64-posix-seh-gcc-10.2.0-llvm-11.0.0-mingw-w64-8.0.0-r5.zip
|
||||
mkdir C:\Users\ed\AppData\Local\Nuitka\
|
||||
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\
|
||||
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\gcc\
|
||||
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\gcc\x86_64\
|
||||
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\gcc\x86_64\10.2.0-11.0.0-8.0.0-r5\
|
||||
copy c:\users\ed\downloads\winlibs-x86_64-posix-seh-gcc-10.2.0-llvm-11.0.0-mingw-w64-8.0.0-r5.zip C:\Users\ed\AppData\Local\Nuitka\Nuitka\gcc\x86_64\10.2.0-11.0.0-8.0.0-r5\winlibs-x86_64-posix-seh-gcc-10.2.0-llvm-11.0.0-mingw-w64-8.0.0-r5.zip
|
||||
|
||||
rem https://github.com/ccache/ccache/releases/download/v3.7.12/ccache-3.7.12-windows-32.zip
|
||||
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\ccache\
|
||||
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\ccache\v3.7.12\
|
||||
copy c:\users\ed\downloads\ccache-3.7.12-windows-32.zip C:\Users\ed\AppData\Local\Nuitka\Nuitka\ccache\v3.7.12\ccache-3.7.12-windows-32.zip
|
||||
python -m pip install --user -U nuitka
|
||||
|
||||
rem https://dependencywalker.com/depends22_x64.zip
|
||||
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\depends\
|
||||
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\depends\x86_64\
|
||||
copy c:\users\ed\downloads\depends22_x64.zip C:\Users\ed\AppData\Local\Nuitka\Nuitka\depends\x86_64\depends22_x64.zip
|
||||
cd %homedrive%
|
||||
cd %homepath%\downloads
|
||||
|
||||
cd \
|
||||
rd /s /q %appdata%\..\local\temp\pe-copyparty
|
||||
cd \users\ed\downloads
|
||||
python copyparty-sfx.py -h
|
||||
cd %appdata%\..\local\temp\pe-copyparty\copyparty
|
||||
rd /s /q copypuitka
|
||||
mkdir copypuitka
|
||||
cd copypuitka
|
||||
|
||||
python
|
||||
import os, re
|
||||
os.rename('../dep-j2/jinja2', '../jinja2')
|
||||
os.rename('../dep-j2/markupsafe', '../markupsafe')
|
||||
rd /s /q %temp%\pe-copyparty
|
||||
python ..\copyparty-sfx.py --version
|
||||
|
||||
print("# nuitka dies if .__init__.stuff is imported")
|
||||
with open('__init__.py','r',encoding='utf-8') as f:
|
||||
t1 = f.read()
|
||||
move %temp%\pe-copyparty\copyparty .\
|
||||
move %temp%\pe-copyparty\partftpy .\
|
||||
move %temp%\pe-copyparty\ftp\pyftpdlib .\
|
||||
move %temp%\pe-copyparty\j2\jinja2 .\
|
||||
move %temp%\pe-copyparty\j2\markupsafe .\
|
||||
|
||||
with open('util.py','r',encoding='utf-8') as f:
|
||||
t2 = f.read().split('\n')[3:]
|
||||
rd /s /q %temp%\pe-copyparty
|
||||
|
||||
t2 = [x for x in t2 if 'from .__init__' not in x]
|
||||
t = t1 + '\n'.join(t2)
|
||||
with open('__init__.py','w',encoding='utf-8') as f:
|
||||
f.write('\n')
|
||||
python -m nuitka ^
|
||||
--onefile --deployment --python-flag=-m ^
|
||||
--include-package=markupsafe ^
|
||||
--include-package=jinja2 ^
|
||||
--include-package=partftpy ^
|
||||
--include-package=pyftpdlib ^
|
||||
--include-data-dir=copyparty\web=copyparty\web ^
|
||||
--include-data-dir=copyparty\res=copyparty\res ^
|
||||
--run copyparty
|
||||
|
||||
with open('util.py','w',encoding='utf-8') as f:
|
||||
f.write(t)
|
||||
|
||||
print("# local-imports fail, prefix module names")
|
||||
ptn = re.compile(r'^( *from )(\.[^ ]+ import .*)')
|
||||
for d, _, fs in os.walk('.'):
|
||||
for f in fs:
|
||||
fp = os.path.join(d, f)
|
||||
if not fp.endswith('.py'):
|
||||
continue
|
||||
t = ''
|
||||
with open(fp,'r',encoding='utf-8') as f:
|
||||
for ln in [x.rstrip('\r\n') for x in f]:
|
||||
m = ptn.match(ln)
|
||||
if not m:
|
||||
t += ln + '\n'
|
||||
continue
|
||||
p1, p2 = m.groups()
|
||||
t += "{}copyparty{}\n".format(p1, p2).replace("__init__", "util")
|
||||
with open(fp,'w',encoding='utf-8') as f:
|
||||
f.write(t)
|
||||
|
||||
exit()
|
||||
|
||||
cd ..
|
||||
|
||||
rd /s /q bout & python -m nuitka --standalone --onefile --windows-onefile-tempdir --python-flag=no_site --assume-yes-for-downloads --include-data-dir=copyparty\web=copyparty\web --include-data-dir=copyparty\res=copyparty\res --run --output-dir=bout --mingw64 --include-package=markupsafe --include-package=jinja2 copyparty
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
pyoxidizer doesn't crosscompile yet so need to build in a windows vm,
|
||||
luckily possible to do mostly airgapped (https-proxy for crates)
|
||||
|
||||
none of this is version-specific but doing absolute links just in case
|
||||
(only exception is py3.8 which is the final win7 ver)
|
||||
|
||||
# deps (download on linux host):
|
||||
https://www.python.org/ftp/python/3.10.7/python-3.10.7-amd64.exe
|
||||
https://github.com/indygreg/PyOxidizer/releases/download/pyoxidizer%2F0.22.0/pyoxidizer-0.22.0-x86_64-pc-windows-msvc.zip
|
||||
https://github.com/upx/upx/releases/download/v3.96/upx-3.96-win64.zip
|
||||
https://static.rust-lang.org/dist/rust-1.61.0-x86_64-pc-windows-msvc.msi
|
||||
https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.8.13%2B20220528-i686-pc-windows-msvc-static-noopt-full.tar.zst
|
||||
|
||||
# need cl.exe, prefer 2017 -- download on linux host:
|
||||
https://visualstudio.microsoft.com/downloads/?q=build+tools
|
||||
https://docs.microsoft.com/en-us/visualstudio/releases/2022/release-history#release-dates-and-build-numbers
|
||||
https://aka.ms/vs/15/release/vs_buildtools.exe # 2017
|
||||
https://aka.ms/vs/16/release/vs_buildtools.exe # 2019
|
||||
https://aka.ms/vs/17/release/vs_buildtools.exe # 2022
|
||||
https://docs.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-build-tools?view=vs-2017
|
||||
|
||||
# use disposable w10 vm to prep offline installer; xfer to linux host with firefox to copyparty
|
||||
vs_buildtools-2017.exe --add Microsoft.VisualStudio.Workload.MSBuildTools --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Component.Windows10SDK.17763 --layout c:\msbt2017 --lang en-us
|
||||
|
||||
# need two proxies on host; s5s or ssh for msys2(socks5), and tinyproxy for rust(http)
|
||||
UP=- python3 socks5server.py 192.168.123.1 4321
|
||||
ssh -vND 192.168.123.1:4321 localhost
|
||||
git clone https://github.com/tinyproxy/tinyproxy.git
|
||||
./autogen.sh
|
||||
./configure --prefix=/home/ed/pe/tinyproxy
|
||||
make -j24 install
|
||||
printf '%s\n' >cfg "Port 4380" "Listen 192.168.123.1"
|
||||
./tinyproxy -dccfg
|
||||
|
||||
https://github.com/msys2/msys2-installer/releases/download/2022-09-04/msys2-x86_64-20220904.exe
|
||||
export all_proxy=socks5h://192.168.123.1:4321
|
||||
# if chat dies after auth (2 messages) it probably failed dns, note the h in socks5h to tunnel dns
|
||||
pacman -Syuu
|
||||
pacman -S git patch mingw64/mingw-w64-x86_64-zopfli
|
||||
cd /c && curl -k https://192.168.123.1:3923/ro/ox/msbt2017/?tar | tar -xv
|
||||
|
||||
first install certs from msbt/certificates then admin-cmd `vs_buildtools.exe --noweb`,
|
||||
default selection (vc++2017-v15.9-v14.16, vc++redist, vc++bt-core) += win10sdk (for io.h)
|
||||
|
||||
install rust without documentation, python 3.10, put upx and pyoxidizer into ~/bin,
|
||||
[cmd.exe] python -m pip install --user -U wheel-0.37.1.tar.gz strip-hints-0.1.10.tar.gz
|
||||
p=192.168.123.1:4380; export https_proxy=$p; export http_proxy=$p
|
||||
|
||||
# and with all of the one-time-setup out of the way,
|
||||
mkdir /c/d; cd /c/d && curl -k https://192.168.123.1:3923/cpp/gb?pw=wark > gb && git clone gb copyparty
|
||||
cd /c/d/copyparty/ && curl -k https://192.168.123.1:3923/cpp/patch?pw=wark | patch -p1
|
||||
cd /c/d/copyparty/scripts && CARGO_HTTP_CHECK_REVOKE=false PATH=/c/Users/$USER/AppData/Local/Programs/Python/Python310:/c/Users/$USER/bin:"$(cygpath "C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\VC\Tools\MSVC\14.16.27023\bin\Hostx86\x86"):$PATH" ./make-sfx.sh ox ultra
|
||||
49
docs/rice/README.md
Normal file
49
docs/rice/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# custom fonts
|
||||
|
||||
to change the fonts in the web-UI, first save the following text (the default font-config) to a new css file, for example named `customfonts.css` in your webroot:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--font-main: sans-serif;
|
||||
--font-serif: serif;
|
||||
--font-mono: 'scp';
|
||||
}
|
||||
```
|
||||
|
||||
add this to your copyparty config so the css file gets loaded: `--html-head='<link rel="stylesheet" href="/customfonts.css">'`
|
||||
|
||||
alternatively, if you are using a config file instead of commandline args:
|
||||
|
||||
```yaml
|
||||
[global]
|
||||
html-head: <link rel="stylesheet" href="/customfonts.css">
|
||||
```
|
||||
|
||||
restart copyparty for the config change to take effect
|
||||
|
||||
edit the css file you made and press `ctrl`-`shift`-`R` in the browser to see the changes as you go (no need to restart copyparty for each change)
|
||||
|
||||
if you are introducing a new ttf/woff font, don't forget to declare the font itself in the css file; here's one of the default fonts from `ui.css`:
|
||||
|
||||
```css
|
||||
@font-face {
|
||||
font-family: 'scp';
|
||||
font-display: swap;
|
||||
src: local('Source Code Pro Regular'), local('SourceCodePro-Regular'), url(deps/scp.woff2) format('woff2');
|
||||
}
|
||||
```
|
||||
|
||||
and because textboxes don't inherit fonts by default, you can force it like this:
|
||||
|
||||
```css
|
||||
input[type=text], input[type=submit], input[type=button] { font-family: var(--font-main) }
|
||||
```
|
||||
|
||||
and if you want to have a monospace font in the fancy markdown editor, do this:
|
||||
|
||||
```css
|
||||
.EasyMDEContainer .CodeMirror { font-family: var(--font-mono) }
|
||||
```
|
||||
|
||||
NB: `<textarea id="mt">` and `<div id="mtr">` in the regular markdown editor must have the same font; none of the suggestions above will cause any issues but keep it in mind if you're getting creative
|
||||
|
||||
331
docs/versus.md
331
docs/versus.md
@@ -48,6 +48,7 @@ currently up to date with [awesome-selfhosted](https://github.com/awesome-selfho
|
||||
* [filebrowser](#filebrowser)
|
||||
* [filegator](#filegator)
|
||||
* [sftpgo](#sftpgo)
|
||||
* [arozos](#arozos)
|
||||
* [updog](#updog)
|
||||
* [goshs](#goshs)
|
||||
* [gimme-that](#gimme-that)
|
||||
@@ -93,6 +94,7 @@ the softwares,
|
||||
* `j` = [filebrowser](https://github.com/filebrowser/filebrowser)
|
||||
* `k` = [filegator](https://github.com/filegator/filegator)
|
||||
* `l` = [sftpgo](https://github.com/drakkan/sftpgo)
|
||||
* `m` = [arozos](https://github.com/tobychui/arozos)
|
||||
|
||||
some softwares not in the matrixes,
|
||||
* [updog](#updog)
|
||||
@@ -113,22 +115,22 @@ symbol legend,
|
||||
|
||||
## general
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| intuitive UX | | ╱ | █ | █ | █ | | █ | █ | █ | █ | █ | █ |
|
||||
| config GUI | | █ | █ | █ | █ | | | █ | █ | █ | | █ |
|
||||
| good documentation | | | | █ | █ | █ | █ | | | █ | █ | ╱ |
|
||||
| runs on iOS | ╱ | | | | | ╱ | | | | | | |
|
||||
| runs on Android | █ | | | | | █ | | | | | | |
|
||||
| runs on WinXP | █ | █ | | | | █ | | | | | | |
|
||||
| runs on Windows | █ | █ | █ | █ | █ | █ | █ | ╱ | █ | █ | █ | █ |
|
||||
| runs on Linux | █ | ╱ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| runs on Macos | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| runs on FreeBSD | █ | | | • | █ | █ | █ | • | █ | █ | | █ |
|
||||
| portable binary | █ | █ | █ | | | █ | █ | | | █ | | █ |
|
||||
| zero setup, just go | █ | █ | █ | | | ╱ | █ | | | █ | | ╱ |
|
||||
| android app | ╱ | | | █ | █ | | | | | | | |
|
||||
| iOS app | ╱ | | | █ | █ | | | | | | | |
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| intuitive UX | | ╱ | █ | █ | █ | | █ | █ | █ | █ | █ | █ | █ |
|
||||
| config GUI | | █ | █ | █ | █ | | | █ | █ | █ | | █ | █ |
|
||||
| good documentation | | | | █ | █ | █ | █ | | | █ | █ | ╱ | ╱ |
|
||||
| runs on iOS | ╱ | | | | | ╱ | | | | | | | |
|
||||
| runs on Android | █ | | | | | █ | | | | | | | |
|
||||
| runs on WinXP | █ | █ | | | | █ | | | | | | | |
|
||||
| runs on Windows | █ | █ | █ | █ | █ | █ | █ | ╱ | █ | █ | █ | █ | ╱ |
|
||||
| runs on Linux | █ | ╱ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| runs on Macos | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | |
|
||||
| runs on FreeBSD | █ | | | • | █ | █ | █ | • | █ | █ | | █ | |
|
||||
| portable binary | █ | █ | █ | | | █ | █ | | | █ | | █ | █ |
|
||||
| zero setup, just go | █ | █ | █ | | | ╱ | █ | | | █ | | ╱ | █ |
|
||||
| android app | ╱ | | | █ | █ | | | | | | | | |
|
||||
| iOS app | ╱ | | | █ | █ | | | | | | | | |
|
||||
|
||||
* `zero setup` = you can get a mostly working setup by just launching the app, without having to install any software or configure whatever
|
||||
* `a`/copyparty remarks:
|
||||
@@ -140,37 +142,39 @@ symbol legend,
|
||||
* `f`/rclone must be started with the command `rclone serve webdav .` or similar
|
||||
* `h`/chibisafe has undocumented windows support
|
||||
* `i`/sftpgo must be launched with a command
|
||||
* `m`/arozos has partial windows support
|
||||
|
||||
|
||||
## file transfer
|
||||
|
||||
*the thing that copyparty is actually kinda good at*
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| download folder as zip | █ | █ | █ | █ | ╱ | | █ | | █ | █ | ╱ | █ |
|
||||
| download folder as tar | █ | | | | | | | | | █ | | |
|
||||
| upload | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| parallel uploads | █ | | | █ | █ | | • | | █ | | █ | |
|
||||
| resumable uploads | █ | | | | | | | | █ | | █ | ╱ |
|
||||
| upload segmenting | █ | | | | | | | █ | █ | | █ | ╱ |
|
||||
| upload acceleration | █ | | | | | | | | █ | | █ | |
|
||||
| upload verification | █ | | | █ | █ | | | | █ | | | |
|
||||
| upload deduplication | █ | | | | █ | | | | █ | | | |
|
||||
| upload a 999 TiB file | █ | | | | █ | █ | • | | █ | | █ | ╱ |
|
||||
| keep last-modified time | █ | | | █ | █ | █ | | | | | | █ |
|
||||
| upload rules | ╱ | ╱ | ╱ | ╱ | ╱ | | | ╱ | ╱ | | ╱ | ╱ |
|
||||
| ┗ max disk usage | █ | █ | | | █ | | | | █ | | | █ |
|
||||
| ┗ max filesize | █ | | | | | | | █ | | | █ | █ |
|
||||
| ┗ max items in folder | █ | | | | | | | | | | | ╱ |
|
||||
| ┗ max file age | █ | | | | | | | | █ | | | |
|
||||
| ┗ max uploads over time | █ | | | | | | | | | | | ╱ |
|
||||
| ┗ compress before write | █ | | | | | | | | | | | |
|
||||
| ┗ randomize filename | █ | | | | | | | █ | █ | | | |
|
||||
| ┗ mimetype reject-list | ╱ | | | | | | | | • | ╱ | | ╱ |
|
||||
| ┗ extension reject-list | ╱ | | | | | | | █ | • | ╱ | | ╱ |
|
||||
| checksums provided | | | | █ | █ | | | | █ | ╱ | | |
|
||||
| cloud storage backend | ╱ | ╱ | ╱ | █ | █ | █ | ╱ | | | ╱ | █ | █ |
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| download folder as zip | █ | █ | █ | █ | ╱ | | █ | | █ | █ | ╱ | █ | ╱ |
|
||||
| download folder as tar | █ | | | | | | | | | █ | | | |
|
||||
| upload | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| parallel uploads | █ | | | █ | █ | | • | | █ | | █ | | █ |
|
||||
| resumable uploads | █ | | | | | | | | █ | | █ | ╱ | |
|
||||
| upload segmenting | █ | | | | | | | █ | █ | | █ | ╱ | █ |
|
||||
| upload acceleration | █ | | | | | | | | █ | | █ | | |
|
||||
| upload verification | █ | | | █ | █ | | | | █ | | | | |
|
||||
| upload deduplication | █ | | | | █ | | | | █ | | | | |
|
||||
| upload a 999 TiB file | █ | | | | █ | █ | • | | █ | | █ | ╱ | ╱ |
|
||||
| race the beam ("p2p") | █ | | | | | | | | | • | | | |
|
||||
| keep last-modified time | █ | | | █ | █ | █ | | | | | | █ | |
|
||||
| upload rules | ╱ | ╱ | ╱ | ╱ | ╱ | | | ╱ | ╱ | | ╱ | ╱ | ╱ |
|
||||
| ┗ max disk usage | █ | █ | | | █ | | | | █ | | | █ | █ |
|
||||
| ┗ max filesize | █ | | | | | | | █ | | | █ | █ | █ |
|
||||
| ┗ max items in folder | █ | | | | | | | | | | | ╱ | |
|
||||
| ┗ max file age | █ | | | | | | | | █ | | | | |
|
||||
| ┗ max uploads over time | █ | | | | | | | | | | | ╱ | |
|
||||
| ┗ compress before write | █ | | | | | | | | | | | | |
|
||||
| ┗ randomize filename | █ | | | | | | | █ | █ | | | | |
|
||||
| ┗ mimetype reject-list | ╱ | | | | | | | | • | ╱ | | ╱ | • |
|
||||
| ┗ extension reject-list | ╱ | | | | | | | █ | • | ╱ | | ╱ | • |
|
||||
| checksums provided | | | | █ | █ | | | | █ | ╱ | | | |
|
||||
| cloud storage backend | ╱ | ╱ | ╱ | █ | █ | █ | ╱ | | | ╱ | █ | █ | ╱ |
|
||||
|
||||
* `upload segmenting` = files are sliced into chunks, making it possible to upload files larger than 100 MiB on cloudflare for example
|
||||
|
||||
@@ -178,6 +182,8 @@ symbol legend,
|
||||
|
||||
* `upload verification` = uploads are checksummed or otherwise confirmed to have been transferred correctly
|
||||
|
||||
* `race the beam` = files can be downloaded while they're still uploading; downloaders are slowed down such that the uploader is always ahead
|
||||
|
||||
* `checksums provided` = when downloading a file from the server, the file's checksum is provided for verification client-side
|
||||
|
||||
* `cloud storage backend` = able to serve files from (and write to) s3 or similar cloud services; `╱` means the software can do this with some help from `rclone mount` as a bridge
|
||||
@@ -192,25 +198,27 @@ symbol legend,
|
||||
* resumable/segmented uploads only over SFTP, not over HTTP
|
||||
* upload rules are totals only, not over time
|
||||
* can probably do extension/mimetype rejection similar to copyparty
|
||||
* `m`/arozos download-as-zip is not streaming; it creates the full zipfile before download can start, and fails on big folders
|
||||
|
||||
|
||||
## protocols and client support
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| serve https | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| serve webdav | █ | | | █ | █ | █ | █ | | █ | | | █ |
|
||||
| serve ftp | █ | | | | | █ | | | | | | █ |
|
||||
| serve ftps | █ | | | | | █ | | | | | | █ |
|
||||
| serve sftp | | | | | | █ | | | | | | █ |
|
||||
| serve smb/cifs | ╱ | | | | | █ | | | | | | |
|
||||
| serve dlna | | | | | | █ | | | | | | |
|
||||
| listen on unix-socket | | | | █ | █ | | █ | █ | █ | | █ | █ |
|
||||
| zeroconf | █ | | | | | | | | | | | |
|
||||
| supports netscape 4 | ╱ | | | | | █ | | | | | • | |
|
||||
| ...internet explorer 6 | ╱ | █ | | █ | | █ | | | | | • | |
|
||||
| mojibake filenames | █ | | | • | • | █ | █ | • | • | • | | ╱ |
|
||||
| undecodable filenames | █ | | | • | • | █ | | • | • | | | ╱ |
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| serve https | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| serve webdav | █ | | | █ | █ | █ | █ | | █ | | | █ | █ |
|
||||
| serve ftp (tcp) | █ | | | | | █ | | | | | | █ | █ |
|
||||
| serve ftps (tls) | █ | | | | | █ | | | | | | █ | |
|
||||
| serve tftp (udp) | █ | | | | | | | | | | | | |
|
||||
| serve sftp (ssh) | | | | | | █ | | | | | | █ | █ |
|
||||
| serve smb/cifs | ╱ | | | | | █ | | | | | | | |
|
||||
| serve dlna | | | | | | █ | | | | | | | |
|
||||
| listen on unix-socket | | | | █ | █ | | █ | █ | █ | | █ | █ | |
|
||||
| zeroconf | █ | | | | | | | | | | | | █ |
|
||||
| supports netscape 4 | ╱ | | | | | █ | | | | | • | | ╱ |
|
||||
| ...internet explorer 6 | ╱ | █ | | █ | | █ | | | | | • | | ╱ |
|
||||
| mojibake filenames | █ | | | • | • | █ | █ | • | █ | • | | ╱ | |
|
||||
| undecodable filenames | █ | | | • | • | █ | | • | | | | ╱ | |
|
||||
|
||||
* `webdav` = protocol convenient for mounting a remote server as a local filesystem; see zeroconf:
|
||||
* `zeroconf` = the server announces itself on the LAN, [automatically appearing](https://user-images.githubusercontent.com/241032/215344737-0eae8d98-9496-4256-9aa8-cd2f6971810d.png) on other zeroconf-capable devices
|
||||
@@ -221,61 +229,66 @@ symbol legend,
|
||||
* extremely minimal samba/cifs server
|
||||
* netscape 4 / ie6 support is mostly listed as a joke altho some people have actually found it useful ([ie4 tho](https://user-images.githubusercontent.com/241032/118192791-fb31fe00-b446-11eb-9647-898ea8efc1f7.png))
|
||||
* `l`/sftpgo translates mojibake filenames into valid utf-8 (information loss)
|
||||
* `m`/arozos has readonly-support for older browsers; no uploading
|
||||
|
||||
|
||||
## server configuration
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| config from cmd args | █ | | | | | █ | █ | | | █ | | ╱ |
|
||||
| config files | █ | █ | █ | ╱ | ╱ | █ | | █ | | █ | • | ╱ |
|
||||
| runtime config reload | █ | █ | █ | | | | | █ | █ | █ | █ | |
|
||||
| same-port http / https | █ | | | | | | | | | | | |
|
||||
| listen multiple ports | █ | | | | | | | | | | | █ |
|
||||
| virtual file system | █ | █ | █ | | | | █ | | | | | █ |
|
||||
| reverse-proxy ok | █ | | █ | █ | █ | █ | █ | █ | • | • | • | █ |
|
||||
| folder-rproxy ok | █ | | | | █ | █ | | • | • | • | • | |
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| config from cmd args | █ | | | | | █ | █ | | | █ | | ╱ | ╱ |
|
||||
| config files | █ | █ | █ | ╱ | ╱ | █ | | █ | | █ | • | ╱ | ╱ |
|
||||
| runtime config reload | █ | █ | █ | | | | | █ | █ | █ | █ | | █ |
|
||||
| same-port http / https | █ | | | | | | | | | | | | |
|
||||
| listen multiple ports | █ | | | | | | | | | | | █ | |
|
||||
| virtual file system | █ | █ | █ | | | | █ | | | | | █ | |
|
||||
| reverse-proxy ok | █ | | █ | █ | █ | █ | █ | █ | • | • | • | █ | ╱ |
|
||||
| folder-rproxy ok | █ | | | | █ | █ | | • | • | • | • | | • |
|
||||
|
||||
* `folder-rproxy` = reverse-proxying without dedicating an entire (sub)domain, using a subfolder instead
|
||||
* `l`/sftpgo:
|
||||
* config: users must be added through gui / api calls
|
||||
* `m`/arozos:
|
||||
* configuration is primarily through GUI
|
||||
* reverse-proxy is not guaranteed to see the correct client IP
|
||||
|
||||
|
||||
## server capabilities
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| accounts | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| per-account chroot | | | | | | | | | | | | █ |
|
||||
| single-sign-on | | | | █ | █ | | | | • | | | |
|
||||
| token auth | | | | █ | █ | | | █ | | | | |
|
||||
| 2fa | | | | █ | █ | | | | | | | █ |
|
||||
| per-volume permissions | █ | █ | █ | █ | █ | █ | █ | | █ | █ | ╱ | █ |
|
||||
| per-folder permissions | ╱ | | | █ | █ | | █ | | █ | █ | ╱ | █ |
|
||||
| per-file permissions | | | | █ | █ | | █ | | █ | | | |
|
||||
| per-file passwords | █ | | | █ | █ | | █ | | █ | | | |
|
||||
| unmap subfolders | █ | | | | | | █ | | | █ | ╱ | • |
|
||||
| index.html blocks list | ╱ | | | | | | █ | | | • | | |
|
||||
| write-only folders | █ | | | | | | | | | | █ | █ |
|
||||
| files stored as-is | █ | █ | █ | █ | | █ | █ | | | █ | █ | █ |
|
||||
| file versioning | | | | █ | █ | | | | | | | |
|
||||
| file encryption | | | | █ | █ | █ | | | | | | █ |
|
||||
| file indexing | █ | | █ | █ | █ | | | █ | █ | █ | | |
|
||||
| ┗ per-volume db | █ | | • | • | • | | | • | • | | | |
|
||||
| ┗ db stored in folder | █ | | | | | | | • | • | █ | | |
|
||||
| ┗ db stored out-of-tree | █ | | █ | █ | █ | | | • | • | █ | | |
|
||||
| ┗ existing file tree | █ | | █ | | | | | | | █ | | |
|
||||
| file action event hooks | █ | | | | | | | | | █ | | █ |
|
||||
| one-way folder sync | █ | | | █ | █ | █ | | | | | | |
|
||||
| full sync | | | | █ | █ | | | | | | | |
|
||||
| speed throttle | | █ | █ | | | █ | | | █ | | | █ |
|
||||
| anti-bruteforce | █ | █ | █ | █ | █ | | | | • | | | █ |
|
||||
| dyndns updater | | █ | | | | | | | | | | |
|
||||
| self-updater | | | █ | | | | | | | | | |
|
||||
| log rotation | █ | | █ | █ | █ | | | • | █ | | | █ |
|
||||
| upload tracking / log | █ | █ | • | █ | █ | | | █ | █ | | | ╱ |
|
||||
| curl-friendly ls | █ | | | | | | | | | | | |
|
||||
| curl-friendly upload | █ | | | | | █ | █ | • | | | | |
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| accounts | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| per-account chroot | | | | | | | | | | | | █ | |
|
||||
| single-sign-on | ╱ | | | █ | █ | | | | • | | | | |
|
||||
| token auth | ╱ | | | █ | █ | | | █ | | | | | █ |
|
||||
| 2fa | ╱ | | | █ | █ | | | | | | | █ | ╱ |
|
||||
| per-volume permissions | █ | █ | █ | █ | █ | █ | █ | | █ | █ | ╱ | █ | █ |
|
||||
| per-folder permissions | ╱ | | | █ | █ | | █ | | █ | █ | ╱ | █ | █ |
|
||||
| per-file permissions | | | | █ | █ | | █ | | █ | | | | █ |
|
||||
| per-file passwords | █ | | | █ | █ | | █ | | █ | | | | █ |
|
||||
| unmap subfolders | █ | | | | | | █ | | | █ | ╱ | • | |
|
||||
| index.html blocks list | ╱ | | | | | | █ | | | • | | | |
|
||||
| write-only folders | █ | | | | | | | | | | █ | █ | |
|
||||
| files stored as-is | █ | █ | █ | █ | | █ | █ | | | █ | █ | █ | █ |
|
||||
| file versioning | | | | █ | █ | | | | | | | | |
|
||||
| file encryption | | | | █ | █ | █ | | | | | | █ | |
|
||||
| file indexing | █ | | █ | █ | █ | | | █ | █ | █ | | | |
|
||||
| ┗ per-volume db | █ | | • | • | • | | | • | • | | | | |
|
||||
| ┗ db stored in folder | █ | | | | | | | • | • | █ | | | |
|
||||
| ┗ db stored out-of-tree | █ | | █ | █ | █ | | | • | • | █ | | | |
|
||||
| ┗ existing file tree | █ | | █ | | | | | | | █ | | | |
|
||||
| file action event hooks | █ | | | | | | | | | █ | | █ | • |
|
||||
| one-way folder sync | █ | | | █ | █ | █ | | | | | | | |
|
||||
| full sync | | | | █ | █ | | | | | | | | |
|
||||
| speed throttle | | █ | █ | | | █ | | | █ | | | █ | |
|
||||
| anti-bruteforce | █ | █ | █ | █ | █ | | | | • | | | █ | • |
|
||||
| dyndns updater | | █ | | | | | | | | | | | |
|
||||
| self-updater | | | █ | | | | | | | | | | █ |
|
||||
| log rotation | █ | | █ | █ | █ | | | • | █ | | | █ | • |
|
||||
| upload tracking / log | █ | █ | • | █ | █ | | | █ | █ | | | ╱ | █ |
|
||||
| prometheus metrics | █ | | | █ | | | | | | | | █ | |
|
||||
| curl-friendly ls | █ | | | | | | | | | | | | |
|
||||
| curl-friendly upload | █ | | | | | █ | █ | • | | | | | |
|
||||
|
||||
* `unmap subfolders` = "shadowing"; mounting a local folder in the middle of an existing filesystem tree in order to disable access below that path
|
||||
* `files stored as-is` = uploaded files are trivially readable from the server HDD, not sliced into chunks or in weird folder structures or anything like that
|
||||
@@ -288,6 +301,7 @@ symbol legend,
|
||||
* `curl-friendly ls` = returns a [sortable plaintext folder listing](https://user-images.githubusercontent.com/241032/215322619-ea5fd606-3654-40ad-94ee-2bc058647bb2.png) when curled
|
||||
* `curl-friendly upload` = uploading with curl is just `curl -T some.bin http://.../`
|
||||
* `a`/copyparty remarks:
|
||||
* single-sign-on, token-auth, and 2fa is *possible* through authelia/authentik or similar, but nobody's made an example yet
|
||||
* one-way folder sync from local to server can be done efficiently with [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy), or with webdav and conventional rsync
|
||||
* can hot-reload config files (with just a few exceptions)
|
||||
* can set per-folder permissions if that folder is made into a separate volume, so there is configuration overhead
|
||||
@@ -300,49 +314,51 @@ symbol legend,
|
||||
* `l`/sftpgo:
|
||||
* `file action event hooks` also include on-download triggers
|
||||
* `upload tracking / log` in main logfile
|
||||
* `m`/arozos:
|
||||
* `2fa` maybe possible through LDAP/Oauth
|
||||
|
||||
|
||||
## client features
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l |
|
||||
| ---------------------- | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| single-page app | █ | | █ | █ | █ | | | █ | █ | █ | █ | |
|
||||
| themes | █ | █ | | █ | | | | | █ | | | |
|
||||
| directory tree nav | █ | ╱ | | | █ | | | | █ | | ╱ | |
|
||||
| multi-column sorting | █ | | | | | | | | | | | |
|
||||
| thumbnails | █ | | | ╱ | ╱ | | | █ | █ | ╱ | | |
|
||||
| ┗ image thumbnails | █ | | | █ | █ | | | █ | █ | █ | | |
|
||||
| ┗ video thumbnails | █ | | | █ | █ | | | | █ | | | |
|
||||
| ┗ audio spectrograms | █ | | | | | | | | | | | |
|
||||
| audio player | █ | | | █ | █ | | | | █ | ╱ | | |
|
||||
| ┗ gapless playback | █ | | | | | | | | • | | | |
|
||||
| ┗ audio equalizer | █ | | | | | | | | | | | |
|
||||
| ┗ waveform seekbar | █ | | | | | | | | | | | |
|
||||
| ┗ OS integration | █ | | | | | | | | | | | |
|
||||
| ┗ transcode to lossy | █ | | | | | | | | | | | |
|
||||
| video player | █ | | | █ | █ | | | | █ | █ | | |
|
||||
| ┗ video transcoding | | | | | | | | | █ | | | |
|
||||
| audio BPM detector | █ | | | | | | | | | | | |
|
||||
| audio key detector | █ | | | | | | | | | | | |
|
||||
| search by path / name | █ | █ | █ | █ | █ | | █ | | █ | █ | ╱ | |
|
||||
| search by date / size | █ | | | | █ | | | █ | █ | | | |
|
||||
| search by bpm / key | █ | | | | | | | | | | | |
|
||||
| search by custom tags | | | | | | | | █ | █ | | | |
|
||||
| search in file contents | | | | █ | █ | | | | █ | | | |
|
||||
| search by custom parser | █ | | | | | | | | | | | |
|
||||
| find local file | █ | | | | | | | | | | | |
|
||||
| undo recent uploads | █ | | | | | | | | | | | |
|
||||
| create directories | █ | | | █ | █ | ╱ | █ | █ | █ | █ | █ | █ |
|
||||
| image viewer | █ | | | █ | █ | | | | █ | █ | █ | |
|
||||
| markdown viewer | █ | | | | █ | | | | █ | ╱ | ╱ | |
|
||||
| markdown editor | █ | | | | █ | | | | █ | ╱ | ╱ | |
|
||||
| readme.md in listing | █ | | | █ | | | | | | | | |
|
||||
| rename files | █ | █ | █ | █ | █ | ╱ | █ | | █ | █ | █ | █ |
|
||||
| batch rename | █ | | | | | | | | █ | | | |
|
||||
| cut / paste files | █ | █ | | █ | █ | | | | █ | | | |
|
||||
| move files | █ | █ | | █ | █ | | █ | | █ | █ | █ | |
|
||||
| delete files | █ | █ | | █ | █ | ╱ | █ | █ | █ | █ | █ | █ |
|
||||
| copy files | | | | | █ | | | | █ | █ | █ | |
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
| ---------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| single-page app | █ | | █ | █ | █ | | | █ | █ | █ | █ | | █ |
|
||||
| themes | █ | █ | | █ | | | | | █ | | | | |
|
||||
| directory tree nav | █ | ╱ | | | █ | | | | █ | | ╱ | | |
|
||||
| multi-column sorting | █ | | | | | | | | | | | | |
|
||||
| thumbnails | █ | | | ╱ | ╱ | | | █ | █ | ╱ | | | █ |
|
||||
| ┗ image thumbnails | █ | | | █ | █ | | | █ | █ | █ | | | █ |
|
||||
| ┗ video thumbnails | █ | | | █ | █ | | | | █ | | | | █ |
|
||||
| ┗ audio spectrograms | █ | | | | | | | | | | | | |
|
||||
| audio player | █ | | | █ | █ | | | | █ | ╱ | | | █ |
|
||||
| ┗ gapless playback | █ | | | | | | | | • | | | | |
|
||||
| ┗ audio equalizer | █ | | | | | | | | | | | | |
|
||||
| ┗ waveform seekbar | █ | | | | | | | | | | | | |
|
||||
| ┗ OS integration | █ | | | | | | | | | | | | |
|
||||
| ┗ transcode to lossy | █ | | | | | | | | | | | | |
|
||||
| video player | █ | | | █ | █ | | | | █ | █ | | | █ |
|
||||
| ┗ video transcoding | | | | | | | | | █ | | | | |
|
||||
| audio BPM detector | █ | | | | | | | | | | | | |
|
||||
| audio key detector | █ | | | | | | | | | | | | |
|
||||
| search by path / name | █ | █ | █ | █ | █ | | █ | | █ | █ | ╱ | | |
|
||||
| search by date / size | █ | | | | █ | | | █ | █ | | | | |
|
||||
| search by bpm / key | █ | | | | | | | | | | | | |
|
||||
| search by custom tags | | | | | | | | █ | █ | | | | |
|
||||
| search in file contents | | | | █ | █ | | | | █ | | | | |
|
||||
| search by custom parser | █ | | | | | | | | | | | | |
|
||||
| find local file | █ | | | | | | | | | | | | |
|
||||
| undo recent uploads | █ | | | | | | | | | | | | |
|
||||
| create directories | █ | | | █ | █ | ╱ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| image viewer | █ | | | █ | █ | | | | █ | █ | █ | | █ |
|
||||
| markdown viewer | █ | | | | █ | | | | █ | ╱ | ╱ | | █ |
|
||||
| markdown editor | █ | | | | █ | | | | █ | ╱ | ╱ | | █ |
|
||||
| readme.md in listing | █ | | | █ | | | | | | | | | |
|
||||
| rename files | █ | █ | █ | █ | █ | ╱ | █ | | █ | █ | █ | █ | █ |
|
||||
| batch rename | █ | | | | | | | | █ | | | | |
|
||||
| cut / paste files | █ | █ | | █ | █ | | | | █ | | | | █ |
|
||||
| move files | █ | █ | | █ | █ | | █ | | █ | █ | █ | | █ |
|
||||
| delete files | █ | █ | | █ | █ | ╱ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| copy files | | | | | █ | | | | █ | █ | █ | | █ |
|
||||
|
||||
* `single-page app` = multitasking; possible to continue navigating while uploading
|
||||
* `audio player » os-integration` = use the [lockscreen](https://user-images.githubusercontent.com/241032/142711926-0700be6c-3e31-47b3-9928-53722221f722.png) or [media hotkeys](https://user-images.githubusercontent.com/241032/215347492-b4250797-6c90-4e09-9a4c-721edf2fb15c.png) to play/pause, prev/next song
|
||||
@@ -358,14 +374,14 @@ symbol legend,
|
||||
|
||||
## integration
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| OS alert on upload | █ | | | | | | | | | ╱ | | ╱ |
|
||||
| discord | █ | | | | | | | | | ╱ | | ╱ |
|
||||
| ┗ announce uploads | █ | | | | | | | | | | | ╱ |
|
||||
| ┗ custom embeds | | | | | | | | | | | | ╱ |
|
||||
| sharex | █ | | | █ | | █ | ╱ | █ | | | | |
|
||||
| flameshot | | | | | | █ | | | | | | |
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| OS alert on upload | █ | | | | | | | | | ╱ | | ╱ | |
|
||||
| discord | █ | | | | | | | | | ╱ | | ╱ | |
|
||||
| ┗ announce uploads | █ | | | | | | | | | | | ╱ | |
|
||||
| ┗ custom embeds | | | | | | | | | | | | ╱ | |
|
||||
| sharex | █ | | | █ | | █ | ╱ | █ | | | | | |
|
||||
| flameshot | | | | | | █ | | | | | | | |
|
||||
|
||||
* sharex `╱` = yes, but does not provide example sharex config
|
||||
* `a`/copyparty remarks:
|
||||
@@ -391,6 +407,7 @@ symbol legend,
|
||||
| filebrowser | go | █ apl2 | 20 MB |
|
||||
| filegator | php | █ mit | • |
|
||||
| sftpgo | go | ‼ agpl | 44 MB |
|
||||
| arozos | go | ░ gpl3 | 531 MB |
|
||||
| updog | python | █ mit | 17 MB |
|
||||
| goshs | go | █ mit | 11 MB |
|
||||
| gimme-that | python | █ mit | 4.8 MB |
|
||||
@@ -502,12 +519,14 @@ symbol legend,
|
||||
* ✅ token auth (api keys)
|
||||
|
||||
## [kodbox](https://github.com/kalcaddle/kodbox)
|
||||
* this thing is insane
|
||||
* this thing is insane (but is getting competition from [arozos](#arozos))
|
||||
* php; [docker](https://hub.docker.com/r/kodcloud/kodbox)
|
||||
* 🔵 *upload segmenting, acceleration, and integrity checking!*
|
||||
* ⚠️ but uploads are not resumable(?)
|
||||
* ⚠️ not portable
|
||||
* ⚠️ isolated on-disk file hierarchy, incompatible with other software
|
||||
* ⚠️ uploading small files to copyparty is 16x faster
|
||||
* ⚠️ uploading large files to copyparty is 3x faster
|
||||
* ⚠️ http/webdav only; no ftp or zeroconf
|
||||
* ⚠️ some parts of the GUI are in chinese
|
||||
* ✅ fantastic ui/ux
|
||||
@@ -567,6 +586,24 @@ symbol legend,
|
||||
* ✅ on-download event hook (otherwise same as copyparty)
|
||||
* ✅ more extensive permissions control
|
||||
|
||||
## [arozos](https://github.com/tobychui/arozos)
|
||||
* big suite of applications similar to [kodbox](#kodbox), copyparty is better at downloading/uploading/music/indexing but arozos has other advantages
|
||||
* go; primarily linux (limited support for windows)
|
||||
* ⚠️ uploads not resumable / integrity-checked
|
||||
* ⚠️ uploading small files to copyparty is 2.7x faster
|
||||
* ⚠️ uploading large files to copyparty is at least 10% faster
|
||||
* arozos is websocket-based, 512 KiB chunks; writes each chunk to separate files and then merges
|
||||
* copyparty splices directly into the final file; faster and better for the HDD and filesystem
|
||||
* ⚠️ no directory tree navpane; not as easy to navigate
|
||||
* ⚠️ download-as-zip is not streaming; creates a temp.file on the server
|
||||
* ⚠️ not self-contained (pulls from jsdelivr)
|
||||
* ⚠️ has an audio player, but supports less filetypes
|
||||
* ⚠️ limited support for configuring real-ip detection
|
||||
* ✅ sftp server
|
||||
* ✅ settings gui
|
||||
* ✅ good-looking gui
|
||||
* ✅ an IDE, msoffice viewer, rich host integration, much more
|
||||
|
||||
## [updog](https://github.com/sc0tfree/updog)
|
||||
* python; cross-platform
|
||||
* basic directory listing with upload feature
|
||||
|
||||
45
docs/xff.md
Normal file
45
docs/xff.md
Normal file
@@ -0,0 +1,45 @@
|
||||
when running behind a reverse-proxy, or a WAF, or another protection service such as cloudflare:
|
||||
|
||||
if you (and maybe everybody else) keep getting a message that says `thank you for playing`, then you've gotten banned for malicious traffic. This ban applies to the IP-address that copyparty *thinks* identifies the shady client -- so, depending on your setup, you might have to tell copyparty where to find the correct IP
|
||||
|
||||
knowing the correct IP is also crucial for some other features, such as the unpost feature which lets you delete your own recent uploads -- but if everybody has the same IP, well...
|
||||
|
||||
----
|
||||
|
||||
for most common setups, there should be a helpful message in the server-log explaining what to do, something like `--xff-src=10.88.0.0/16` or `--xff-src=lan` to accept the `X-Forwarded-For` header from your reverse-proxy with a LAN IP of `10.88.x.y`
|
||||
|
||||
if you are behind cloudflare, it is recommended to also set `--xff-hdr=cf-connecting-ip` to use a more trustworthy source of info, but then it's also very important to ensure your reverse-proxy does not accept connections from anything BUT cloudflare; you can do this by generating an ip-address allowlist and reject all other connections
|
||||
|
||||
* if you are using nginx as your reverse-proxy, see the [example nginx config](https://github.com/9001/copyparty/blob/hovudstraum/contrib/nginx/copyparty.conf) on how the cloudflare allowlist can be done
|
||||
|
||||
----
|
||||
|
||||
the server-log will give recommendations in the form of commandline arguments;
|
||||
|
||||
to do the same thing using config files, take the options that are suggested in the serverlog and put them into the `[global]` section in your `copyparty.conf` like so:
|
||||
|
||||
```yaml
|
||||
[global]
|
||||
xff-src: lan
|
||||
xff-hdr: cf-connecting-ip
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
# but if you just want to get it working:
|
||||
|
||||
...and don't care about security, you can optionally disable the bot-detectors, either by specifying commandline-args `--ban-404=no --ban-403=no --ban-422=no --ban-url=no --ban-pw=no`
|
||||
|
||||
or by adding these lines inside the `[global]` section in your `copyparty.conf`:
|
||||
|
||||
```yaml
|
||||
[global]
|
||||
ban-404: no
|
||||
ban-403: no
|
||||
ban-422: no
|
||||
ban-url: no
|
||||
ban-pw: no
|
||||
```
|
||||
|
||||
but remember that this will make other features insecure as well, such as unpost
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# builds win7-i386 exe on win10-ltsc-1809(17763.316)
|
||||
# see docs/pyoxidizer.txt
|
||||
|
||||
def make_exe():
|
||||
dist = default_python_distribution(flavor="standalone_static", python_version="3.8")
|
||||
policy = dist.make_python_packaging_policy()
|
||||
policy.allow_files = True
|
||||
policy.allow_in_memory_shared_library_loading = True
|
||||
#policy.bytecode_optimize_level_zero = True
|
||||
#policy.include_distribution_sources = False # error instantiating embedded Python interpreter: during initializing Python main: init_fs_encoding: failed to get the Python codec of the filesystem encoding
|
||||
policy.include_distribution_resources = False
|
||||
policy.include_non_distribution_sources = False
|
||||
policy.include_test = False
|
||||
python_config = dist.make_python_interpreter_config()
|
||||
#python_config.module_search_paths = ["$ORIGIN/lib"]
|
||||
|
||||
python_config.run_module = "copyparty"
|
||||
exe = dist.to_python_executable(
|
||||
name="copyparty",
|
||||
config=python_config,
|
||||
packaging_policy=policy,
|
||||
)
|
||||
exe.windows_runtime_dlls_mode = "never"
|
||||
exe.windows_subsystem = "console"
|
||||
exe.add_python_resources(exe.read_package_root(
|
||||
path="sfx",
|
||||
packages=[
|
||||
"copyparty",
|
||||
"jinja2",
|
||||
"markupsafe",
|
||||
"pyftpdlib",
|
||||
"python-magic",
|
||||
]
|
||||
))
|
||||
return exe
|
||||
|
||||
def make_embedded_resources(exe):
|
||||
return exe.to_embedded_resources()
|
||||
|
||||
def make_install(exe):
|
||||
files = FileManifest()
|
||||
files.add_python_resource("copyparty", exe)
|
||||
return files
|
||||
|
||||
register_target("exe", make_exe)
|
||||
register_target("resources", make_embedded_resources, depends=["exe"], default_build_script=True)
|
||||
register_target("install", make_install, depends=["exe"], default=True)
|
||||
resolve_targets()
|
||||
@@ -28,6 +28,7 @@ classifiers = [
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: Jython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Operating System :: OS Independent",
|
||||
"Environment :: Console",
|
||||
"Environment :: No Input/Output (Daemon)",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
@@ -48,6 +49,7 @@ thumbnails2 = ["pyvips"]
|
||||
audiotags = ["mutagen"]
|
||||
ftpd = ["pyftpdlib"]
|
||||
ftps = ["pyftpdlib", "pyopenssl"]
|
||||
tftpd = ["partftpy>=0.3.1"]
|
||||
pwhash = ["argon2-cffi"]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -3,7 +3,7 @@ WORKDIR /z
|
||||
ENV ver_asmcrypto=c72492f4a66e17a0e5dd8ad7874de354f3ccdaa5 \
|
||||
ver_hashwasm=4.10.0 \
|
||||
ver_marked=4.3.0 \
|
||||
ver_dompf=3.0.8 \
|
||||
ver_dompf=3.0.11 \
|
||||
ver_mde=2.18.0 \
|
||||
ver_codemirror=5.65.16 \
|
||||
ver_fontawesome=5.13.0 \
|
||||
@@ -24,7 +24,9 @@ ENV ver_asmcrypto=c72492f4a66e17a0e5dd8ad7874de354f3ccdaa5 \
|
||||
# the scp url is regular latin from https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap
|
||||
RUN mkdir -p /z/dist/no-pk \
|
||||
&& wget https://fonts.gstatic.com/s/sourcecodepro/v11/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2 -O scp.woff2 \
|
||||
&& apk add cmake make g++ git bash npm patch wget tar pigz brotli gzip unzip python3 python3-dev brotli py3-brotli \
|
||||
&& apk add \
|
||||
bash brotli cmake make g++ git gzip lame npm patch pigz \
|
||||
python3 python3-dev py3-brotli sox tar unzip wget \
|
||||
&& rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \
|
||||
&& wget https://github.com/openpgpjs/asmcrypto.js/archive/$ver_asmcrypto.tar.gz -O asmcrypto.tgz \
|
||||
&& wget https://github.com/markedjs/marked/archive/v$ver_marked.tar.gz -O marked.tgz \
|
||||
@@ -58,6 +60,11 @@ RUN mkdir -p /z/dist/no-pk \
|
||||
&& tar -xf zopfli.tgz
|
||||
|
||||
|
||||
COPY busy-mp3.sh /z/
|
||||
RUN /z/busy-mp3.sh \
|
||||
&& mv -v /dev/shm/busy.mp3.gz /z/dist
|
||||
|
||||
|
||||
# build fonttools (which needs zopfli)
|
||||
RUN tar -xf zopfli.tgz \
|
||||
&& cd zopfli* \
|
||||
@@ -143,9 +150,8 @@ RUN ./genprism.sh $ver_prism
|
||||
|
||||
|
||||
# compress
|
||||
COPY brotli.makefile zopfli.makefile /z/dist/
|
||||
COPY zopfli.makefile /z/dist/
|
||||
RUN cd /z/dist \
|
||||
&& make -j$(nproc) -f brotli.makefile \
|
||||
&& make -j$(nproc) -f zopfli.makefile \
|
||||
&& rm *.makefile \
|
||||
&& mv no-pk/* . \
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
all: $(addsuffix .br, $(wildcard easymde*))
|
||||
|
||||
%.br: %
|
||||
brotli -jZ $<
|
||||
61
scripts/deps-docker/busy-mp3.sh
Executable file
61
scripts/deps-docker/busy-mp3.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cat >/dev/null <<EOF
|
||||
a frame is 1152 samples
|
||||
1sec @ 48000 = 41.66 frames
|
||||
11 frames = 12672 samples = 0.264 sec
|
||||
22 frames = 25344 samples = 0.528 sec
|
||||
EOF
|
||||
|
||||
fast=1
|
||||
fast=
|
||||
|
||||
echo
|
||||
mkdir -p /dev/shm/$1
|
||||
cd /dev/shm/$1
|
||||
find -maxdepth 1 -type f -iname 'a.*.mp3*' -delete
|
||||
min=99999999
|
||||
|
||||
for freq in 425; do # {400..500}
|
||||
for vol in 0; do # {10..30}
|
||||
for kbps in 32; do
|
||||
for fdur in 1124; do # {800..1200}
|
||||
for fdu2 in 1042; do # {800..1200}
|
||||
for ftyp in h; do # q h t l p
|
||||
for ofs1 in 9214; do # {0..32768}
|
||||
for ofs2 in 0; do # {0..4096}
|
||||
for ofs3 in 0; do # {0..4096}
|
||||
for nores in --nores; do # '' --nores
|
||||
|
||||
f=a.b$kbps$nores-f$freq-v$vol-$ftyp$fdur-$fdu2-o$ofs1-$ofs2-$ofs3
|
||||
sox -r48000 -Dn -r48000 -b16 -c2 -t raw s1.pcm synth 25344s sin $freq vol 0.$vol fade $ftyp ${fdur}s 25344s ${fdu2}s
|
||||
sox -r48000 -Dn -r48000 -b16 -c2 -t raw s0.pcm synth 12672s sin $freq vol 0
|
||||
tail -c +$ofs1 s0.pcm >s0a.pcm
|
||||
tail -c +$ofs2 s0.pcm >s0b.pcm
|
||||
tail -c +$ofs3 s0.pcm >s0c.pcm
|
||||
cat s{0a,1,0,0b,1,0c}.pcm > a.pcm
|
||||
lame --silent -r -s 48 --bitwidth 16 --signed a.pcm -m j --resample 48 -b $kbps -q 0 $nores $f.mp3
|
||||
if [ $fast ]
|
||||
then gzip -c9 <$f.mp3 >$f.mp3.gz
|
||||
else pigz -c11 -I1 <$f.mp3 >$f.mp3.gz
|
||||
fi
|
||||
sz=$(wc -c <$f.mp3.gz)
|
||||
printf '\033[A%d %s\033[K\n' $sz $f
|
||||
[ $sz -le $((min+10)) ] && echo
|
||||
[ $sz -le $min ] && echo && min=$sz
|
||||
|
||||
done;done;done;done;done;done;done;done;done;done
|
||||
true
|
||||
|
||||
f=a.b32--nores-f425-v0-h1124-1042-o9214-0-0.mp3
|
||||
[ $fast ] &&
|
||||
pigz -c11 -I1 <$f >busy.mp3.gz ||
|
||||
mv $f.gz busy.mp3.gz
|
||||
|
||||
sz=$(wc -c <busy.mp3.gz)
|
||||
[ "$sz" -eq 106 ] &&
|
||||
echo busy.mp3 built successfully ||
|
||||
echo WARNING: unexpected size of busy.mp3
|
||||
|
||||
find -maxdepth 1 -type f -iname 'a.*.mp3*' -delete
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM fedora:38
|
||||
FROM fedora:39
|
||||
WORKDIR /z
|
||||
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
|
||||
@@ -21,7 +21,7 @@ RUN dnf install -y \
|
||||
vips vips-jxl vips-poppler vips-magick \
|
||||
python3-numpy fftw libsndfile \
|
||||
gcc gcc-c++ make cmake patchelf jq \
|
||||
python3-devel ffmpeg-devel fftw-devel libsndfile-devel python3-setuptools \
|
||||
python3-devel ffmpeg-devel fftw-devel libsndfile-devel python3-setuptools python3-wheel \
|
||||
vamp-plugin-sdk qm-vamp-plugins \
|
||||
vamp-plugin-sdk-devel vamp-plugin-sdk-static \
|
||||
&& rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \
|
||||
@@ -29,7 +29,7 @@ RUN dnf install -y \
|
||||
&& bash install-deps.sh \
|
||||
&& dnf erase -y \
|
||||
gcc gcc-c++ make cmake patchelf jq \
|
||||
python3-devel ffmpeg-devel fftw-devel libsndfile-devel python3-setuptools \
|
||||
python3-devel ffmpeg-devel fftw-devel libsndfile-devel python3-setuptools python3-wheel \
|
||||
vamp-plugin-sdk-devel vamp-plugin-sdk-static \
|
||||
&& dnf clean all \
|
||||
&& find /usr/ -name __pycache__ | xargs rm -rf \
|
||||
|
||||
@@ -33,6 +33,8 @@ the recommended way to configure copyparty inside a container is to mount a fold
|
||||
* but you can also provide arguments to the docker command if you prefer that
|
||||
* config files must be named `something.conf` to get picked up
|
||||
|
||||
also see [docker-specific recommendations](#docker-specific-recommendations)
|
||||
|
||||
|
||||
## editions
|
||||
|
||||
@@ -79,6 +81,35 @@ or using commandline arguments,
|
||||
```
|
||||
|
||||
|
||||
# faq
|
||||
|
||||
the following advice is best-effort and not guaranteed to be entirely correct
|
||||
|
||||
* q: starting a rootless container on debian 12 fails with `failed to register layer: lsetxattr user.overlay.impure /etc: operation not supported`
|
||||
* a: docker's default rootless configuration on debian is to use the overlay2 storage driver; this does not work. Your options are to replace docker with podman (good choice), or to configure docker to use the `fuse-overlayfs` storage driver
|
||||
|
||||
|
||||
|
||||
# docker-specific recommendations
|
||||
|
||||
* copyparty will generally create a `.hist` folder at the top of each volume, which contains the filesystem index, thumbnails and such. For performance reasons, but also just to keep things tidy, it might be convenient to store these inside the config folder instead. Add the line `hist: /cfg/hists/` inside the `[global]` section of your `copyparty.conf` to do this
|
||||
|
||||
|
||||
## enabling the ftp server
|
||||
|
||||
...is tricky because ftp is a weird protocol and docker is making it worse 🎉
|
||||
|
||||
add the following three config entries into the `[global]` section of your `copyparty.conf`:
|
||||
|
||||
* `ftp: 3921` to enable the service, listening for connections on port 3921
|
||||
|
||||
* `ftp-nat: 127.0.0.1` but replace `127.0.0.1` with the actual external IP of your server; the clients will only be able to connect to this IP, even if the server has multiple IPs
|
||||
|
||||
* `ftp-pr: 12000-12099` to restrict the [passive-mode](http://slacksite.com/other/ftp.html#passive) port selection range; this allows up to 100 simultaneous file transfers
|
||||
|
||||
then finally update your docker config so that the port-range you specified (12000-12099) is exposed to the internet
|
||||
|
||||
|
||||
# build the images yourself
|
||||
|
||||
basically `./make.sh hclean pull img push` but see [devnotes.md](./devnotes.md)
|
||||
|
||||
@@ -77,13 +77,14 @@ function have() {
|
||||
}
|
||||
|
||||
function load_env() {
|
||||
. buildenv/bin/activate
|
||||
have setuptools
|
||||
have wheel
|
||||
have build
|
||||
have twine
|
||||
have jinja2
|
||||
have strip_hints
|
||||
. buildenv/bin/activate || return 1
|
||||
have setuptools &&
|
||||
have wheel &&
|
||||
have build &&
|
||||
have twine &&
|
||||
have jinja2 &&
|
||||
have strip_hints &&
|
||||
return 0 || return 1
|
||||
}
|
||||
|
||||
load_env || {
|
||||
|
||||
@@ -16,8 +16,6 @@ help() { exec cat <<'EOF'
|
||||
# `re` does a repack of an sfx which you already executed once
|
||||
# (grabs files from the sfx-created tempdir), overrides `clean`
|
||||
#
|
||||
# `ox` builds a pyoxidizer exe instead of py
|
||||
#
|
||||
# `gz` creates a gzip-compressed python sfx instead of bzip2
|
||||
#
|
||||
# `lang` limits which languages/translations to include,
|
||||
@@ -26,8 +24,9 @@ help() { exec cat <<'EOF'
|
||||
# _____________________________________________________________________
|
||||
# core features:
|
||||
#
|
||||
# `no-ftp` saves ~33k by removing the ftp server and filetype detector,
|
||||
# disabling --ftpd and --magic
|
||||
# `no-ftp` saves ~30k by removing the ftp server, disabling --ftp
|
||||
#
|
||||
# `no-tfp` saves ~10k by removing the tftp server, disabling --tftp
|
||||
#
|
||||
# `no-smb` saves ~3.5k by removing the smb / cifs server
|
||||
#
|
||||
@@ -36,7 +35,7 @@ help() { exec cat <<'EOF'
|
||||
# _____________________________________________________________________
|
||||
# web features:
|
||||
#
|
||||
# `no-cm` saves ~82k by removing easymde/codemirror
|
||||
# `no-cm` saves ~89k by removing easymde/codemirror
|
||||
# (the fancy markdown editor)
|
||||
#
|
||||
# `no-hl` saves ~41k by removing syntax hilighting in the text viewer
|
||||
@@ -110,10 +109,10 @@ while [ ! -z "$1" ]; do
|
||||
case $1 in
|
||||
clean) clean=1 ; ;;
|
||||
re) repack=1 ; ;;
|
||||
ox) use_ox=1 ; ;;
|
||||
gz) use_gz=1 ; ;;
|
||||
gzz) shift;use_gzz=$1;use_gz=1; ;;
|
||||
no-ftp) no_ftp=1 ; ;;
|
||||
no-tfp) no_tfp=1 ; ;;
|
||||
no-smb) no_smb=1 ; ;;
|
||||
no-zm) no_zm=1 ; ;;
|
||||
no-fnt) no_fnt=1 ; ;;
|
||||
@@ -165,7 +164,8 @@ necho() {
|
||||
[ $repack ] && {
|
||||
old="$tmpdir/pe-copyparty.$(id -u)"
|
||||
echo "repack of files in $old"
|
||||
cp -pR "$old/"*{py2,py37,j2,copyparty} .
|
||||
cp -pR "$old/"*{py2,py37,magic,j2,copyparty} .
|
||||
cp -pR "$old/"*partftpy . || true
|
||||
cp -pR "$old/"*ftp . || true
|
||||
}
|
||||
|
||||
@@ -221,6 +221,16 @@ necho() {
|
||||
mkdir ftp/
|
||||
mv pyftpdlib ftp/
|
||||
|
||||
necho collecting partftpy
|
||||
f="../build/partftpy-0.3.1.tar.gz"
|
||||
[ -e "$f" ] ||
|
||||
(url=https://files.pythonhosted.org/packages/37/79/1a1de1d3fdf27ddc9c2d55fec6552e7b8ed115258fedac6120679898b83d/partftpy-0.3.1.tar.gz;
|
||||
wget -O$f "$url" || curl -L "$url" >$f)
|
||||
|
||||
tar -zxf $f
|
||||
mv partftpy-*/partftpy .
|
||||
rm -rf partftpy-* partftpy/bin
|
||||
|
||||
necho collecting python-magic
|
||||
v=0.4.27
|
||||
f="../build/python-magic-$v.tar.gz"
|
||||
@@ -234,7 +244,6 @@ necho() {
|
||||
rm -rf python-magic-*
|
||||
rm magic/compat.py
|
||||
iawk '/^def _add_compat/{o=1} !o; /^_add_compat/{o=0}' magic/__init__.py
|
||||
mv magic ftp/ # doesn't provide a version label anyways
|
||||
|
||||
# enable this to dynamically remove type hints at startup,
|
||||
# in case a future python version can use them for performance
|
||||
@@ -356,7 +365,7 @@ git describe --tags >/dev/null 2>/dev/null && {
|
||||
|
||||
printf '%s\n' "$git_ver" | grep -qE '^v[0-9\.]+-[0-9]+-g[0-9a-f]+$' && {
|
||||
# long format (unreleased commit)
|
||||
t_ver="$(printf '%s\n' "$ver" | sed -r 's/\./, /g; s/(.*) (.*)/\1 "\2"/')"
|
||||
t_ver="$(printf '%s\n' "$ver" | sed -r 's/[-.]/, /g; s/(.*) (.*)/\1 "\2"/')"
|
||||
}
|
||||
|
||||
[ -z "$t_ver" ] && {
|
||||
@@ -394,7 +403,7 @@ find -type f -name ._\* | while IFS= read -r f; do cmp <(printf '\x00\x05\x16')
|
||||
|
||||
rm -f copyparty/web/deps/*.full.* copyparty/web/dbg-* copyparty/web/Makefile
|
||||
|
||||
find copyparty | LC_ALL=C sort | sed -r 's/\.(gz|br)$//;s/$/,/' > have
|
||||
find copyparty | LC_ALL=C sort | sed -r 's/\.gz$//;s/$/,/' > have
|
||||
cat have | while IFS= read -r x; do
|
||||
grep -qF -- "$x" ../scripts/sfx.ls || {
|
||||
echo "unexpected file: $x"
|
||||
@@ -409,8 +418,10 @@ iawk '/^ {0,4}[^ ]/{s=0}/^ {4}def (serve_forever|_loop)/{s=1}!s' ftp/pyftpdlib/s
|
||||
rm -f ftp/pyftpdlib/{__main__,prefork}.py
|
||||
|
||||
[ $no_ftp ] &&
|
||||
rm -rf copyparty/ftpd.py ftp &&
|
||||
sed -ri '/\.ftp/d' copyparty/svchub.py
|
||||
rm -rf copyparty/ftpd.py ftp
|
||||
|
||||
[ $no_tfp ] &&
|
||||
rm -rf copyparty/tftpd.py partftpy
|
||||
|
||||
[ $no_smb ] &&
|
||||
rm -f copyparty/smbd.py
|
||||
@@ -447,8 +458,8 @@ rm -f ftp/pyftpdlib/{__main__,prefork}.py
|
||||
iawk '/^\}/{l=0} !l; /^var Ls =/{l=1;next} o; /^\t["}]/{o=0} /^\t"'"$langs"'"/{o=1;print}' $f
|
||||
done
|
||||
|
||||
[ ! $repack ] && [ ! $use_ox ] && {
|
||||
# uncomment; oxidized drops 45 KiB but becomes undebuggable
|
||||
[ ! $repack ] && {
|
||||
# uncomment
|
||||
find | grep -E '\.py$' |
|
||||
grep -vE '__version__' |
|
||||
tr '\n' '\0' |
|
||||
@@ -556,40 +567,13 @@ nf=$(ls -1 "$zdir"/arc.* 2>/dev/null | wc -l)
|
||||
}
|
||||
|
||||
|
||||
[ $use_ox ] && {
|
||||
tgt=x86_64-pc-windows-msvc
|
||||
tgt=i686-pc-windows-msvc # 2M smaller (770k after upx)
|
||||
bdir=build/$tgt/release/install/copyparty
|
||||
|
||||
t="res web"
|
||||
(printf "\n\n\nBUT WAIT! THERE'S MORE!!\n\n";
|
||||
cat ../$bdir/COPYING.txt) >> copyparty/res/COPYING.txt ||
|
||||
echo "copying.txt 404 pls rebuild"
|
||||
|
||||
mv ftp/* j2/* .
|
||||
rm -rf ftp j2 py2 py37
|
||||
(cd copyparty; tar -cvf z.tar $t; rm -rf $t)
|
||||
cd ..
|
||||
pyoxidizer build --release --target-triple $tgt
|
||||
mv $bdir/copyparty.exe dist/
|
||||
cp -pv "$(for d in '/c/Program Files (x86)/Microsoft Visual Studio/'*'/BuildTools/VC/Redist/MSVC'; do
|
||||
find "$d" -name vcruntime140.dll; done | sort | grep -vE '/x64/|/onecore/' | head -n 1)" dist/
|
||||
dist/copyparty.exe --version
|
||||
cp -pv dist/copyparty{,.orig}.exe
|
||||
[ $ultra ] && a="--best --lzma" || a=-1
|
||||
/bin/time -f %es upx $a dist/copyparty.exe >/dev/null
|
||||
ls -al dist/copyparty{,.orig}.exe
|
||||
exit 0
|
||||
}
|
||||
|
||||
|
||||
echo gen tarlist
|
||||
for d in copyparty j2 py2 py37 ftp; do find $d -type f; done | # strip_hints
|
||||
for d in copyparty partftpy magic j2 py2 py37 ftp; do find $d -type f || true; done | # strip_hints
|
||||
sed -r 's/(.*)\.(.*)/\2 \1/' | LC_ALL=C sort |
|
||||
sed -r 's/([^ ]*) (.*)/\2.\1/' | grep -vE '/list1?$' > list1
|
||||
|
||||
for n in {1..50}; do
|
||||
(grep -vE '\.(gz|br)$' list1; grep -E '\.(gz|br)$' list1 | (shuf||gshuf) ) >list || true
|
||||
(grep -vE '\.gz$' list1; grep -E '\.gz$' list1 | (shuf||gshuf) ) >list || true
|
||||
s=$( (sha1sum||shasum) < list | cut -c-16)
|
||||
grep -q $s "$zdir/h" 2>/dev/null && continue
|
||||
echo $s >> "$zdir/h"
|
||||
|
||||
@@ -37,7 +37,7 @@ rm -rf $TEMP/pe-copyparty*
|
||||
python copyparty-sfx.py --version
|
||||
|
||||
rm -rf mods; mkdir mods
|
||||
cp -pR $TEMP/pe-copyparty/copyparty/ $TEMP/pe-copyparty/{ftp,j2}/* mods/
|
||||
cp -pR $TEMP/pe-copyparty/{copyparty,partftpy}/ $TEMP/pe-copyparty/{ftp,j2}/* mods/
|
||||
[ $w10 ] && rm -rf mods/{jinja2,markupsafe}
|
||||
|
||||
af() { awk "$1" <$2 >tf; mv tf "$2"; }
|
||||
@@ -118,4 +118,12 @@ base64 | head -c12 >> dist/copyparty.exe
|
||||
|
||||
dist/copyparty.exe --version
|
||||
|
||||
curl -fkT dist/copyparty.exe -b cppwd=wark https://192.168.123.1:3923/copyparty$esuf.exe
|
||||
csum=$(sha512sum <dist/copyparty.exe | cut -c-56)
|
||||
|
||||
curl -fkT dist/copyparty.exe -b cppwd=wark https://192.168.123.1:3923/copyparty$esuf.exe >uplod.log
|
||||
cat uplod.log
|
||||
|
||||
grep -q $csum uplod.log && echo upload OK || {
|
||||
echo UPLOAD FAILED
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
f117016b1e6a7d7e745db30d3e67f1acf7957c443a0dd301b6c5e10b8368f2aa4db6be9782d2d3f84beadd139bfeef4982e40f21ca5d9065cb794eeb0e473e82 altgraph-0.17.4-py2.py3-none-any.whl
|
||||
eda6c38fc4d813fee897e969ff9ecc5acc613df755ae63df0392217bbd67408b5c1f6c676f2bf5497b772a3eb4e1a360e1245e1c16ee83f0af555f1ab82c3977 Git-2.39.1-32-bit.exe
|
||||
e0d2e6183437af321a36944f04a501e85181243e5fa2da3254254305dd8119161f62048bc56bff8849b49f546ff175b02b4c999401f1c404f6b88e6f46a9c96e Git-2.44.0-32-bit.exe
|
||||
9d2c31701a4d3fef553928c00528a48f9e1854ab5333528b50e358a214eba90029d687f039bcda5760b6fdf9f2de3bcf3784ae21a6374cf2a97a845d33b636c6 packaging-24.0-py3-none-any.whl
|
||||
17ce52ba50692a9d964f57a23ac163fb74c77fdeb2ca988a6d439ae1fe91955ff43730c073af97a7b3223093ffea3479a996b9b50ee7fba0869247a56f74baa6 pefile-2023.2.7-py3-none-any.whl
|
||||
f298e34356b5590dde7477d7b3a88ad39c622a2bcf3fcd7c53870ce8384dd510f690af81b8f42e121a22d3968a767d2e07595036b2ed7049c8ef4d112bcf3a61 pyinstaller-5.13.2-py3-none-win32.whl
|
||||
f23615c522ed58b9a05978ba4c69c06224590f3a6adbd8e89b31838b181a57160739ceff1fc2ba6f4239b8fee46f92ce02910b2debda2710558ed42cff1ce3f1 pyinstaller-6.1.0-py3-none-win_amd64.whl
|
||||
5747b3b119629c4cf956f0eaa85f29218bb3680d3a4a262fa6e976e56b35067302e153d2c0a001505f2cb642b1f78752567889b3b82e342d6cd29aac8b70e92e pyinstaller_hooks_contrib-2023.10-py2.py3-none-any.whl
|
||||
126ca016c00256f4ff13c88707ead21b3b98f3c665ae57a5bcbb80c8be3004bff36d9c7f9a1cc9d20551019708f2b195154f302d80a1e5a2026d6d0fe9f3d5f4 pyinstaller_hooks_contrib-2024.3-py2.py3-none-any.whl
|
||||
749a473646c6d4c7939989649733d4c7699fd1c359c27046bf5bc9c070d1a4b8b986bbc65f60d7da725baf16dbfdd75a4c2f5bb8335f2cb5685073f5fee5c2d1 pywin32_ctypes-0.2.2-py3-none-any.whl
|
||||
6e0d854040baff861e1647d2bece7d090bc793b2bd9819c56105b94090df54881a6a9b43ebd82578cd7c76d47181571b671e60672afd9def389d03c9dae84fcf setuptools-68.2.2-py3-none-any.whl
|
||||
3c5adf0a36516d284a2ede363051edc1bcc9df925c5a8a9fa2e03cab579dd8d847fdad42f7fd5ba35992e08234c97d2dbfec40a9d12eec61c8dc03758f2bd88e typing_extensions-4.4.0-py3-none-any.whl
|
||||
8d16a967a0a7872a7575b1005cf66915deacda6ee8611fbb52f42fc3e3beb2f901a5140c942a5d146bd412b92bfa9cbadd82beeba83df6d70930c6dc26608a5b upx-4.1.0-win32.zip
|
||||
# u2c (win7)
|
||||
f3390290b896019b2fa169932390e4930d1c03c014e1f6db2405ca2eb1f51f5f5213f725885853805b742997b0edb369787e5c0069d217bc4e8b957f847f58b6 certifi-2023.11.17-py3-none-any.whl
|
||||
904eb57b13bea80aea861de86987e618665d37fa9ea0856e0125a9ba767a53e5064de0b9c4735435a2ddf4f16f7f7d2c75a682e1de83d9f57922bdca8e29988c charset_normalizer-3.3.0-cp37-cp37m-win32.whl
|
||||
ffdd45326f4e91c02714f7a944cbcc2fdd09299f709cfa8aec0892053eef0134fb80d9ba3790afd319538a86feb619037cbf533e2f5939cb56b35bb17f56c858 idna-3.4-py3-none-any.whl
|
||||
7a3bd4849f95e1715fe2e99613df70a0fedd944a9bfde71a0fadb837fe62c3431c30da4f0b75c74de6f1a459f1fdf7cb62eaf404fdbe45e2d121e0b1021f1580 certifi-2024.2.2-py3-none-any.whl
|
||||
9cc8acc5e269e6421bc32bb89261101da29d6ca337d39d60b9106de9ed7904e188716e4a48d78a2c4329026443fcab7acab013d2fe43778e30d6c4e4506a1b91 charset_normalizer-3.3.2-cp37-cp37m-win32.whl
|
||||
0ec1ae5c928b4a0001a254c8598b746049406e1eed720bfafa94d4474078eff76bf6e032124e2d4df4619052836523af36162443c6d746487b387d2e3476e691 idna-3.6-py3-none-any.whl
|
||||
b795abb26ba2f04f1afcfb196f21f638014b26c8186f8f488f1c2d91e8e0220962fbd259dbc9c3875222eb47fc95c73fc0606aaa6602b9ebc524809c9ba3501f requests-2.31.0-py3-none-any.whl
|
||||
5a25cb9b79bb6107f9055dc3e9f62ebc6d4d9ca2c730d824985c93cd82406b723c200d6300c5064e42ee9fc7a2853d6ec6661394f3ed7bac03750e1f2a6840d1 urllib3-1.26.17-py2.py3-none-any.whl
|
||||
61ed4500b6361632030f05229705c5c5a52cb47e31c0e6b55151c8f3beed631cd752ca6c3d6393d56a2acf6a453cfcf801e877116123c550922249c3a976e0f4 urllib3-1.26.18-py2.py3-none-any.whl
|
||||
# win7
|
||||
91c025f7d94bcdf93df838fab67053165a414fc84e8496f92ecbb910dd55f6b6af5e360bbd051444066880c5a6877e75157bd95e150ead46e5c605930dfc50f2 future-0.18.2.tar.gz
|
||||
c06b3295d1d0b0f0a6f9a6cd0be861b9b643b4a5ea37857f0bd41c45deaf27bb927b71922dab74e633e43d75d04a9bd0d1c4ad875569740b0f2a98dd2bfa5113 importlib_metadata-5.0.0-py3-none-any.whl
|
||||
016a8cbd09384f1a9a44cb0e8274df75a8bcb2f3966bb5d708c62145289efaa5db98f75256c97e4f8046735ce2e529fbb076f284a46cdb716e89a75660200ad9 pip-23.2.1-py3-none-any.whl
|
||||
d130bfa136bd171b9972b5c281c578545f2a84a909fdf18a6d2d71dd12fb3d512a7a1fa5cf7300433adece1d306eb2f22d7278f4c90e744e04dc67ba627a82c0 future-1.0.0-py3-none-any.whl
|
||||
0b4d07434bf8d314f42893d90bce005545b44a509e7353a73cad26dc9360b44e2824218a1a74f8174d02eba87fba91baffa82c8901279a32ebc6b8386b1b4275 importlib_metadata-6.7.0-py3-none-any.whl
|
||||
5d7462a584105bccaa9cf376f5a8c5827ead099c813c8af7392d478a4398f373d9e8cac7bbad2db51b335411ab966b21e119b1b1234c9a7ab70c6ddfc9306da6 pip-24.0-py3-none-any.whl
|
||||
f298e34356b5590dde7477d7b3a88ad39c622a2bcf3fcd7c53870ce8384dd510f690af81b8f42e121a22d3968a767d2e07595036b2ed7049c8ef4d112bcf3a61 pyinstaller-5.13.2-py3-none-win32.whl
|
||||
6bb73cc2db795c59c92f2115727f5c173cacc9465af7710db9ff2f2aec2d73130d0992d0f16dcb3fac222dc15c0916562d0813b2337401022020673a4461df3d python-3.7.9-amd64.exe
|
||||
500747651c87f59f2436c5ab91207b5b657856e43d10083f3ce27efb196a2580fadd199a4209519b409920c562aaaa7dcbdfb83ed2072a43eaccae6e2d056f31 python-3.7.9.exe
|
||||
03e50aecc85914567c114e38a1777e32628ee098756f37177bc23220eab33ac7d3ff591fd162db3b4d4e34d55cee93ef0dc67af68a69c38bb1435e0768dee57e typing_extensions-4.7.1-py3-none-any.whl
|
||||
2e04acff170ca3bbceeeb18489c687126c951ec0bfd53cccfb389ba8d29a4576c1a9e8f2e5ea26c84dd21bfa2912f4e71fa72c1e2653b71e34afc0e65f1722d4 upx-4.2.2-win32.zip
|
||||
68e1b618d988be56aaae4e2eb92bc0093627a00441c1074ebe680c41aa98a6161e52733ad0c59888c643a33fe56884e4f935178b2557fbbdd105e92e0d993df6 windows6.1-kb2533623-x64.msu
|
||||
479a63e14586ab2f2228208116fc149ed8ee7b1e4ff360754f5bda4bf765c61af2e04b5ef123976623d04df4976b7886e0445647269da81436bd0a7b5671d361 windows6.1-kb2533623-x86.msu
|
||||
ba91ab0518c61eff13e5612d9e6b532940813f6b56e6ed81ea6c7c4d45acee4d98136a383a25067512b8f75538c67c987cf3944bfa0229e3cb677e2fb81e763e zipp-3.10.0-py3-none-any.whl
|
||||
ac96786e5d35882e0c5b724794329c9125c2b86ae7847f17acfc49f0d294312c6afc1c3f248655de3f0ccb4ca426d7957d02ba702f4a15e9fcd7e2c314e72c19 zipp-3.15.0-py3-none-any.whl
|
||||
# win10
|
||||
00558cca2e0ac813d404252f6e5aeacb50546822ecb5d0570228b8ddd29d94e059fbeb6b90393dee5abcddaca1370aca784dc9b095cbb74e980b3c024767fb24 Jinja2-3.1.2-py3-none-any.whl
|
||||
7f8f4daa4f4f2dbf24cdd534b2952ee3fba6334eb42b37465ccda3aa1cccc3d6204aa6bfffb8a83bf42ec59c702b5b5247d4c8ee0d4df906334ae53072ef8c4c MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl
|
||||
e3e2e6bd511dec484dd0292f4c46c55c88a885eabf15413d53edea2dd4a4dbae1571735b9424f78c0cd7f1082476a8259f31fd3f63990f726175470f636df2b3 Jinja2-3.1.3-py3-none-any.whl
|
||||
e21495f1d473d855103fb4a243095b498ec90eb68776b0f9b48e994990534f7286c0292448e129c507e5d70409f8a05cca58b98d59ce2a815993d0a873dfc480 MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl
|
||||
8a6e2b13a2ec4ef914a5d62aad3db6464d45e525a82e07f6051ed10474eae959069e165dba011aefb8207cdfd55391d73d6f06362c7eb247b08763106709526e mutagen-1.47.0-py3-none-any.whl
|
||||
656015f5cc2c04aa0653ee5609c39a7e5f0b6a58c84fe26b20bd070c52d20b4effb810132f7fb771168483e9fd975cc3302837dd7a1a687ee058b0460c857cc4 packaging-23.2-py3-none-any.whl
|
||||
424e20dc7263a31d524307bc39ed755a9dd82f538086fff68d98dd97e236c9b00777a8ac2e3853081b532b0e93cef44983e74d0ab274877440e8b7341b19358a pillow-10.2.0-cp311-cp311-win_amd64.whl
|
||||
2e6a57bab45b5a825a2073780c73980cbf5aafd99dc3b28660ea3f5f658f04668cd0f01c7de0bb79e362ff4e3b8f01dd4f671d3a2e054d3071baefdcf0b0e4ba python-3.11.7-amd64.exe
|
||||
1dfe6f66bef5c9d62c9028a964196b902772ec9e19db215f3f41acb8d2d563586988d81b455fa6b895b434e9e1e9d57e4d271d1b1214483bdb3eadffcbba6a33 pillow-10.3.0-cp311-cp311-win_amd64.whl
|
||||
8760eab271e79256ae3bfb4af8ccc59010cb5d2eccdd74b325d1a533ae25eb127d51c2ec28ff90d449afed32dd7d6af62934fe9caaf1ae1f4d4831e948e912da pyinstaller-6.5.0-py3-none-win_amd64.whl
|
||||
897a14d5ee5cbc6781a0f48beffc27807a4f789d58c4329d899233f615d168a5dcceddf7f8f2d5bb52212ddcf3eba4664590d9f1fdb25bb5201f44899e03b2f7 python-3.11.9-amd64.exe
|
||||
729dc52f1a02bc6274d012ce33f534102975a828cba11f6029600ea40e2d23aefeb07bf4ae19f9621d0565dd03eb2635bbb97d45fb692c1f756315e8c86c5255 upx-4.2.2-win64.zip
|
||||
|
||||
@@ -8,7 +8,7 @@ run ./build.sh in git-bash to build + upload the exe
|
||||
to obtain the files referenced below, see ./deps.txt
|
||||
|
||||
download + install git (32bit OK on 64):
|
||||
http://192.168.123.1:3923/ro/pyi/Git-2.39.1-32-bit.exe
|
||||
http://192.168.123.1:3923/ro/pyi/Git-2.44.0-32-bit.exe
|
||||
|
||||
===[ copy-paste into git-bash ]================================
|
||||
uname -s | grep NT-10 && w10=1 || {
|
||||
@@ -16,35 +16,36 @@ uname -s | grep NT-10 && w10=1 || {
|
||||
}
|
||||
fns=(
|
||||
altgraph-0.17.4-py2.py3-none-any.whl
|
||||
packaging-24.0-py3-none-any.whl
|
||||
pefile-2023.2.7-py3-none-any.whl
|
||||
pyinstaller_hooks_contrib-2023.10-py2.py3-none-any.whl
|
||||
pyinstaller_hooks_contrib-2024.3-py2.py3-none-any.whl
|
||||
pywin32_ctypes-0.2.2-py3-none-any.whl
|
||||
setuptools-68.2.2-py3-none-any.whl
|
||||
upx-4.1.0-win32.zip
|
||||
)
|
||||
[ $w10 ] && fns+=(
|
||||
pyinstaller-6.1.0-py3-none-win_amd64.whl
|
||||
Jinja2-3.1.2-py3-none-any.whl
|
||||
MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl
|
||||
pyinstaller-6.5.0-py3-none-win_amd64.whl
|
||||
Jinja2-3.1.3-py3-none-any.whl
|
||||
MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl
|
||||
mutagen-1.47.0-py3-none-any.whl
|
||||
packaging-23.2-py3-none-any.whl
|
||||
pillow-10.2.0-cp311-cp311-win_amd64.whl
|
||||
python-3.11.7-amd64.exe
|
||||
pillow-10.3.0-cp311-cp311-win_amd64.whl
|
||||
python-3.11.9-amd64.exe
|
||||
upx-4.2.2-win64.zip
|
||||
)
|
||||
[ $w7 ] && fns+=(
|
||||
pyinstaller-5.13.2-py3-none-win32.whl
|
||||
certifi-2023.11.17-py3-none-any.whl
|
||||
chardet-5.1.0-py3-none-any.whl
|
||||
idna-3.4-py3-none-any.whl
|
||||
requests-2.28.2-py3-none-any.whl
|
||||
urllib3-1.26.14-py2.py3-none-any.whl
|
||||
certifi-2024.2.2-py3-none-any.whl
|
||||
charset_normalizer-3.3.2-cp37-cp37m-win32.whl
|
||||
idna-3.6-py3-none-any.whl
|
||||
requests-2.31.0-py3-none-any.whl
|
||||
urllib3-1.26.18-py2.py3-none-any.whl
|
||||
upx-4.2.2-win32.zip
|
||||
)
|
||||
[ $w7 ] && fns+=(
|
||||
future-0.18.2.tar.gz
|
||||
importlib_metadata-5.0.0-py3-none-any.whl
|
||||
pip-23.2.1-py3-none-any.whl
|
||||
typing_extensions-4.4.0-py3-none-any.whl
|
||||
zipp-3.10.0-py3-none-any.whl
|
||||
future-1.0.0-py3-none-any.whl
|
||||
importlib_metadata-6.7.0-py3-none-any.whl
|
||||
pip-24.0-py3-none-any.whl
|
||||
typing_extensions-4.7.1-py3-none-any.whl
|
||||
zipp-3.15.0-py3-none-any.whl
|
||||
)
|
||||
[ $w7x64 ] && fns+=(
|
||||
windows6.1-kb2533623-x64.msu
|
||||
@@ -76,9 +77,10 @@ yes | unzip upx-*-win32.zip &&
|
||||
mv upx-*/upx.exe . &&
|
||||
python -m ensurepip &&
|
||||
{ [ $w10 ] || python -m pip install --user -U pip-*.whl; } &&
|
||||
{ [ $w7 ] || python -m pip install --user -U {packaging,setuptools,mutagen,Pillow,Jinja2,MarkupSafe}-*.whl; } &&
|
||||
python -m pip install --user -U packaging-*.whl &&
|
||||
{ [ $w7 ] || python -m pip install --user -U {setuptools,mutagen,Pillow,Jinja2,MarkupSafe}-*.whl; } &&
|
||||
{ [ $w10 ] || python -m pip install --user -U {requests,urllib3,charset_normalizer,certifi,idna}-*.whl; } &&
|
||||
{ [ $w10 ] || python -m pip install --user -U future-*.tar.gz importlib_metadata-*.whl typing_extensions-*.whl zipp-*.whl; } &&
|
||||
{ [ $w10 ] || python -m pip install --user -U future-*.whl importlib_metadata-*.whl typing_extensions-*.whl zipp-*.whl; } &&
|
||||
python -m pip install --user -U pyinstaller-*.whl pefile-*.whl pywin32_ctypes-*.whl pyinstaller_hooks_contrib-*.whl altgraph-*.whl &&
|
||||
sed -ri 's/--lzma/--best/' $appd/Python/Python$pyv/site-packages/pyinstaller/building/utils.py &&
|
||||
curl -fkLO https://192.168.123.1:3923/cpp/scripts/uncomment.py &&
|
||||
|
||||
@@ -45,4 +45,12 @@ $APPDATA/python/python37/scripts/pyinstaller -y --clean --upx-dir=. up2k.spec
|
||||
|
||||
./dist/u2c.exe --version
|
||||
|
||||
curl -fkT dist/u2c.exe -HPW:wark https://192.168.123.1:3923/
|
||||
csum=$(sha512sum <dist/u2c.exe | cut -c-56)
|
||||
|
||||
curl -fkT dist/u2c.exe -HPW:wark https://192.168.123.1:3923/ >uplod.log
|
||||
cat uplod.log
|
||||
|
||||
grep -q $csum uplod.log && echo upload OK || {
|
||||
echo UPLOAD FAILED
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ copyparty/sutil.py,
|
||||
copyparty/svchub.py,
|
||||
copyparty/szip.py,
|
||||
copyparty/tcpsrv.py,
|
||||
copyparty/tftpd.py,
|
||||
copyparty/th_cli.py,
|
||||
copyparty/th_srv.py,
|
||||
copyparty/u2idx.py,
|
||||
@@ -80,6 +81,7 @@ copyparty/web/dd/5.png,
|
||||
copyparty/web/dd/__init__.py,
|
||||
copyparty/web/deps,
|
||||
copyparty/web/deps/__init__.py,
|
||||
copyparty/web/deps/busy.mp3,
|
||||
copyparty/web/deps/easymde.css,
|
||||
copyparty/web/deps/easymde.js,
|
||||
copyparty/web/deps/marked.js,
|
||||
|
||||
@@ -234,8 +234,9 @@ def u8(gen):
|
||||
|
||||
|
||||
def yieldfile(fn):
|
||||
with open(fn, "rb") as f:
|
||||
for block in iter(lambda: f.read(64 * 1024), b""):
|
||||
s = 64 * 1024
|
||||
with open(fn, "rb", s * 4) as f:
|
||||
for block in iter(lambda: f.read(s), b""):
|
||||
yield block
|
||||
|
||||
|
||||
|
||||
36
scripts/test/tftp.sh
Executable file
36
scripts/test/tftp.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
|
||||
# PYTHONPATH=.:~/dev/partftpy/ taskset -c 0 python3 -m copyparty -v srv::r -v srv/junk:junk:A --tftp 3969
|
||||
|
||||
get_src=~/dev/copyparty/srv/palette.flac
|
||||
get_fn=${get_src##*/}
|
||||
|
||||
put_src=~/Downloads/102.zip
|
||||
put_dst=~/dev/copyparty/srv/junk/102.zip
|
||||
|
||||
cd /dev/shm
|
||||
|
||||
echo curl get 1428 v4; curl --tftp-blksize 1428 tftp://127.0.0.1:3969/$get_fn | cmp $get_src || exit 1
|
||||
echo curl get 1428 v6; curl --tftp-blksize 1428 tftp://[::1]:3969/$get_fn | cmp $get_src || exit 1
|
||||
|
||||
echo curl put 1428 v4; rm -f $put_dst && curl --tftp-blksize 1428 -T $put_src tftp://127.0.0.1:3969/junk/ && cmp $put_src $put_dst || exit 1
|
||||
echo curl put 1428 v6; rm -f $put_dst && curl --tftp-blksize 1428 -T $put_src tftp://[::1]:3969/junk/ && cmp $put_src $put_dst || exit 1
|
||||
|
||||
echo atftp get 1428; rm -f $get_fn && ~/src/atftp/atftp --option "blksize 1428" -g -r $get_fn 127.0.0.1 3969 && cmp $get_fn $get_src || exit 1
|
||||
|
||||
echo atftp put 1428; rm -f $put_dst && ~/src/atftp/atftp --option "blksize 1428" 127.0.0.1 3969 -p -l $put_src -r junk/102.zip && cmp $put_src $put_dst || exit 1
|
||||
|
||||
echo tftp-hpa get; rm -f $put_dst && tftp -v -m binary 127.0.0.1 3969 -c get $get_fn && cmp $get_src $get_fn || exit 1
|
||||
|
||||
echo tftp-hpa put; rm -f $put_dst && tftp -v -m binary 127.0.0.1 3969 -c put $put_src junk/102.zip && cmp $put_src $put_dst || exit 1
|
||||
|
||||
echo curl get 512; curl tftp://127.0.0.1:3969/$get_fn | cmp $get_src || exit 1
|
||||
|
||||
echo curl put 512; rm -f $put_dst && curl -T $put_src tftp://127.0.0.1:3969/junk/ && cmp $put_src $put_dst || exit 1
|
||||
|
||||
echo atftp get 512; rm -f $get_fn && ~/src/atftp/atftp -g -r $get_fn 127.0.0.1 3969 && cmp $get_fn $get_src || exit 1
|
||||
|
||||
echo atftp put 512; rm -f $put_dst && ~/src/atftp/atftp 127.0.0.1 3969 -p -l $put_src -r junk/102.zip && cmp $put_src $put_dst || exit 1
|
||||
|
||||
echo nice
|
||||
4
setup.py
4
setup.py
@@ -84,7 +84,7 @@ args = {
|
||||
"version": about["__version__"],
|
||||
"description": (
|
||||
"Portable file server with accelerated resumable uploads, "
|
||||
+ "deduplication, WebDAV, FTP, zeroconf, media indexer, "
|
||||
+ "deduplication, WebDAV, FTP, TFTP, zeroconf, media indexer, "
|
||||
+ "video thumbnails, audio transcoding, and write-only folders"
|
||||
),
|
||||
"long_description": long_description,
|
||||
@@ -111,6 +111,7 @@ args = {
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: Jython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Operating System :: OS Independent",
|
||||
"Environment :: Console",
|
||||
"Environment :: No Input/Output (Daemon)",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
@@ -140,6 +141,7 @@ args = {
|
||||
"audiotags": ["mutagen"],
|
||||
"ftpd": ["pyftpdlib"],
|
||||
"ftps": ["pyftpdlib", "pyopenssl"],
|
||||
"tftpd": ["partftpy>=0.3.1"],
|
||||
"pwhash": ["argon2-cffi"],
|
||||
},
|
||||
"entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user