Compare commits
161 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3cecabca3 | ||
|
|
662541c64c | ||
|
|
225bd80ea8 | ||
|
|
85e54980cc | ||
|
|
a19a0fa9f3 | ||
|
|
9bb6e0dc62 | ||
|
|
15ddcf53e7 | ||
|
|
6b54972ec0 | ||
|
|
0219eada23 | ||
|
|
8916bce306 | ||
|
|
99edba4fd9 | ||
|
|
64de3e01e8 | ||
|
|
8222ccc40b | ||
|
|
dc449bf8b0 | ||
|
|
ef0ecf878b | ||
|
|
53f1e3c91d | ||
|
|
eeef80919f | ||
|
|
987bce2182 | ||
|
|
b511d686f0 | ||
|
|
132a83501e | ||
|
|
e565ad5f55 | ||
|
|
f955d2bd58 | ||
|
|
5953399090 | ||
|
|
d26a944d95 | ||
|
|
50dac15568 | ||
|
|
ac1e11e4ce | ||
|
|
d749683d48 | ||
|
|
84e8e1ddfb | ||
|
|
6e58514b84 | ||
|
|
803e156509 | ||
|
|
c06aa683eb | ||
|
|
6644ceef49 | ||
|
|
bd3b3863ae | ||
|
|
ffd4f9c8b9 | ||
|
|
760ff2db72 | ||
|
|
f37187a041 | ||
|
|
1cdb170290 | ||
|
|
d5de3f2fe0 | ||
|
|
d76673e62d | ||
|
|
c549f367c1 | ||
|
|
927c3bce96 | ||
|
|
d75a2c77da | ||
|
|
e6c55d7ff9 | ||
|
|
4c2cb26991 | ||
|
|
dfe7f1d9af | ||
|
|
666297f6fb | ||
|
|
55a011b9c1 | ||
|
|
27aff12a1e | ||
|
|
9a87ee2fe4 | ||
|
|
0a9f4c6074 | ||
|
|
7219331057 | ||
|
|
2fd12a839c | ||
|
|
8c73e0cbc2 | ||
|
|
52e06226a2 | ||
|
|
452592519d | ||
|
|
c9281f8912 | ||
|
|
36d6d29a0c | ||
|
|
db6059e100 | ||
|
|
aab57cb24b | ||
|
|
f00b939402 | ||
|
|
bef9617638 | ||
|
|
692175f5b0 | ||
|
|
5ad65450c4 | ||
|
|
60c96f990a | ||
|
|
07b2bf1104 | ||
|
|
ac1bc232a9 | ||
|
|
5919607ad0 | ||
|
|
07ea629ca5 | ||
|
|
b629d18df6 | ||
|
|
566cbb6507 | ||
|
|
400d700845 | ||
|
|
82ce6862ee | ||
|
|
38e4fdfe03 | ||
|
|
c04662798d | ||
|
|
19d156ff4e | ||
|
|
87c60a1ec9 | ||
|
|
2c92dab165 | ||
|
|
5c1e23907d | ||
|
|
925c7f0a57 | ||
|
|
feed08deb2 | ||
|
|
560d7b6672 | ||
|
|
565daee98b | ||
|
|
e396c5c2b5 | ||
|
|
1ee2cdd089 | ||
|
|
beacedab50 | ||
|
|
25139a4358 | ||
|
|
f8491970fd | ||
|
|
da091aec85 | ||
|
|
e9eb5affcd | ||
|
|
c1918bc36c | ||
|
|
fdda567f50 | ||
|
|
603d0ed72b | ||
|
|
b15a4ef79f | ||
|
|
48a6789d36 | ||
|
|
36f2c446af | ||
|
|
69517e4624 | ||
|
|
ea270ab9f2 | ||
|
|
b6cf2d3089 | ||
|
|
e8db3dd37f | ||
|
|
27485a4cb1 | ||
|
|
253a414443 | ||
|
|
f6e693f0f5 | ||
|
|
c5f7cfc355 | ||
|
|
bc2c1e427a | ||
|
|
95d9e693c6 | ||
|
|
70a3cf36d1 | ||
|
|
aa45fccf11 | ||
|
|
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 | ||
|
|
10bc2d9205 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,6 +12,7 @@ copyparty.egg-info/
|
||||
/dist/
|
||||
/py2/
|
||||
/sfx*
|
||||
/pyz/
|
||||
/unt/
|
||||
/log/
|
||||
|
||||
|
||||
171
README.md
171
README.md
@@ -10,13 +10,16 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
|
||||
📷 **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,12 +40,14 @@ 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
|
||||
* [audio equalizer](#audio-equalizer) - and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression)
|
||||
* [fix unreliable playback on android](#fix-unreliable-playback-on-android) - due to phone / app settings
|
||||
* [markdown viewer](#markdown-viewer) - and there are *two* editors
|
||||
* [markdown vars](#markdown-vars) - dynamic docs with serverside variable expansion
|
||||
* [other tricks](#other-tricks)
|
||||
* [searching](#searching) - search by size, date, path/name, mp3-tags, ...
|
||||
* [server config](#server-config) - using arguments or config files, or a mix of both
|
||||
@@ -56,6 +61,7 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
* [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
|
||||
* [opengraph](#opengraph) - discord and social-media embeds
|
||||
* [file indexing](#file-indexing) - enables dedup and music search ++
|
||||
* [exclude-patterns](#exclude-patterns) - to save some time
|
||||
* [filesystem guards](#filesystem-guards) - avoid traversing into other filesystems
|
||||
@@ -77,6 +83,8 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
* [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
|
||||
* [other extremely specific features](#other-extremely-specific-features) - you'll never find a use for these
|
||||
* [custom mimetypes](#custom-mimetypes) - change the association of a file extension
|
||||
* [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) - does not exist yet
|
||||
@@ -94,6 +102,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,8 +112,9 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
* [dependencies](#dependencies) - mandatory deps
|
||||
* [optional dependencies](#optional-dependencies) - install these to enable bonus features
|
||||
* [optional gpl stuff](#optional-gpl-stuff)
|
||||
* [sfx](#sfx) - the self-contained "binary"
|
||||
* [sfx](#sfx) - the self-contained "binary" (recommended!)
|
||||
* [copyparty.exe](#copypartyexe) - download [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) (win8+) or [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) (win7+)
|
||||
* [zipapp](#zipapp) - another emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz)
|
||||
* [install on android](#install-on-android)
|
||||
* [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)
|
||||
@@ -118,6 +128,7 @@ just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/
|
||||
* or if you cannot install python, you can use [copyparty.exe](#copypartyexe) instead
|
||||
* or install [on arch](#arch-package) ╱ [on NixOS](#nixos-module) ╱ [through nix](#nix-package)
|
||||
* or if you are on android, [install copyparty in termux](#install-on-android)
|
||||
* or if your computer is messed up and nothing else works, [try the pyz](#zipapp)
|
||||
* or if you prefer to [use docker](./scripts/docker/) 🐋 you can do that too
|
||||
* docker has all deps built-in, so skip this step:
|
||||
|
||||
@@ -125,7 +136,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`
|
||||
@@ -146,6 +157,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:
|
||||
@@ -169,6 +191,8 @@ firewall-cmd --reload
|
||||
|
||||
## features
|
||||
|
||||
also see [comparison to similar software](./docs/versus.md)
|
||||
|
||||
* backend stuff
|
||||
* ☑ IPv6
|
||||
* ☑ [multiprocessing](#performance) (actual multithreading)
|
||||
@@ -185,12 +209,13 @@ firewall-cmd --reload
|
||||
* upload
|
||||
* ☑ basic: plain multipart, ie6 support
|
||||
* ☑ [up2k](#uploading): js, resumable, multithreaded
|
||||
* unaffected by cloudflare's max-upload-size (100 MiB)
|
||||
* **no filesize limit!** ...unless you use Cloudflare, then it's 383.9 GiB
|
||||
* ☑ stash: simple PUT filedropper
|
||||
* ☑ filename randomizer
|
||||
* ☑ 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
|
||||
@@ -199,7 +224,8 @@ 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)
|
||||
* ☑ play video files as audio (converted on server)
|
||||
* ☑ image gallery with webm player
|
||||
* ☑ textfile browser with syntax hilighting
|
||||
* ☑ [thumbnails](#thumbnails)
|
||||
@@ -207,6 +233,7 @@ firewall-cmd --reload
|
||||
* ☑ ...of videos using FFmpeg
|
||||
* ☑ ...of audio (spectrograms) using FFmpeg
|
||||
* ☑ cache eviction (max-age; maybe max-size eventually)
|
||||
* ☑ multilingual UI (english, norwegian, [add your own](./docs/rice/#translations)))
|
||||
* ☑ SPA (browse while uploading)
|
||||
* server indexing
|
||||
* ☑ [locate files by contents](#file-search)
|
||||
@@ -215,9 +242,11 @@ firewall-cmd --reload
|
||||
* client support
|
||||
* ☑ [folder sync](#folder-sync)
|
||||
* ☑ [curl-friendly](https://user-images.githubusercontent.com/241032/215322619-ea5fd606-3654-40ad-94ee-2bc058647bb2.png)
|
||||
* ☑ [opengraph](#opengraph) (discord embeds)
|
||||
* markdown
|
||||
* ☑ [viewer](#markdown-viewer)
|
||||
* ☑ editor (sure why not)
|
||||
* ☑ [variables](#markdown-vars)
|
||||
|
||||
PS: something missing? post any crazy ideas you've got as a [feature request](https://github.com/9001/copyparty/issues/new?assignees=9001&labels=enhancement&template=feature_request.md) or [discussion](https://github.com/9001/copyparty/discussions/new?category=ideas) 🤙
|
||||
|
||||
@@ -387,7 +416,7 @@ configuring accounts/volumes with arguments:
|
||||
`-v .::r,usr1,usr2:rw,usr3,usr4` = usr1/2 read-only, 3/4 read-write
|
||||
|
||||
permissions:
|
||||
* `r` (read): browse folder contents, download files, download as zip/tar
|
||||
* `r` (read): browse folder contents, download files, download as zip/tar, see filekeys/dirkeys
|
||||
* `w` (write): upload files, move files *into* this folder
|
||||
* `m` (move): move files/folders *from* this folder
|
||||
* `d` (delete): delete files/folders
|
||||
@@ -547,6 +576,7 @@ it does static images with Pillow / pyvips / FFmpeg, and uses FFmpeg for video f
|
||||
audio files are covnerted into spectrograms using FFmpeg unless you `--no-athumb` (and some FFmpeg builds may need `--th-ff-swr`)
|
||||
|
||||
images with the following names (see `--th-covers`) become the thumbnail of the folder they're in: `folder.png`, `folder.jpg`, `cover.png`, `cover.jpg`
|
||||
* the order is significant, so if both `cover.png` and `folder.jpg` exist in a folder, it will pick the first matching `--th-covers` entry (`folder.jpg`)
|
||||
* and, if you enable [file indexing](#file-indexing), it will also try those names as dotfiles (`.folder.jpg` and so), and then fallback on the first picture in the folder (if it has any pictures at all)
|
||||
|
||||
in the grid/thumbnail view, if the audio player panel is open, songs will start playing when clicked
|
||||
@@ -554,6 +584,7 @@ in the grid/thumbnail view, if the audio player panel is open, songs will start
|
||||
|
||||
enabling `multiselect` lets you click files to select them, and then shift-click another file for range-select
|
||||
* `multiselect` is mostly intended for phones/tablets, but the `sel` option in the `[⚙️] settings` tab is better suited for desktop use, allowing selection by CTRL-clicking and range-selection with SHIFT-click, all without affecting regular clicking
|
||||
* the `sel` option can be made default globally with `--gsel` or per-volume with volflag `gsel`
|
||||
|
||||
|
||||
## zip downloads
|
||||
@@ -587,17 +618,23 @@ 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
|
||||
* and url-params `&j` / `&w` produce jpeg/webm thumbnails/spectrograms instead of the original audio/video/images (`&p` for audio waveforms)
|
||||
* can also be used to pregenerate thumbnails; combine with `--th-maxage=9999999` or `--th-clean=0`
|
||||
|
||||
|
||||
## uploading
|
||||
|
||||
drag files/folders into the web-browser to upload (or use the [command-line uploader](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy))
|
||||
drag files/folders into the web-browser to upload
|
||||
|
||||
this initiates an upload using `up2k`; there are two uploaders available:
|
||||
dragdrop is the recommended way, but you may also:
|
||||
|
||||
* select some files (not folders) in your file explorer and press CTRL-V inside the browser window
|
||||
* use the [command-line uploader](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy)
|
||||
* upload using [curl or sharex](#client-examples)
|
||||
|
||||
when uploading files through dragdrop or CTRL-V, this initiates an upload using `up2k`; there are two browser-based uploaders available:
|
||||
* `[🎈] bup`, the basic uploader, supports almost every browser since netscape 4.0
|
||||
* `[🚀] up2k`, the good / fancy one
|
||||
|
||||
@@ -610,13 +647,14 @@ up2k has several advantages:
|
||||
* uploads resume if you reboot your browser or pc, just upload the same files again
|
||||
* server detects any corruption; the client reuploads affected chunks
|
||||
* the client doesn't upload anything that already exists on the server
|
||||
* no filesize limit unless imposed by a proxy, for example Cloudflare, which blocks uploads over 383.9 GiB
|
||||
* much higher speeds than ftp/scp/tarpipe on some internet connections (mainly american ones) thanks to parallel connections
|
||||
* the last-modified timestamp of the file is preserved
|
||||
|
||||
> 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)
|
||||
|
||||

|
||||
|
||||
@@ -677,11 +715,18 @@ uploads can be given a lifetime, afer which they expire / self-destruct
|
||||
|
||||
the feature must be enabled per-volume with the `lifetime` [upload rule](#upload-rules) which sets the upper limit for how long a file gets to stay on the server
|
||||
|
||||
clients can specify a shorter expiration time using the [up2k ui](#uploading) -- the relevant options become visible upon navigating into a folder with `lifetimes` enabled -- or by using the `life` [upload modifier](#write)
|
||||
clients can specify a shorter expiration time using the [up2k ui](#uploading) -- the relevant options become visible upon navigating into a folder with `lifetimes` enabled -- or by using the `life` [upload modifier](./docs/devnotes.md#write)
|
||||
|
||||
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)
|
||||
@@ -757,6 +802,7 @@ some hilights:
|
||||
* OS integration; control playback from your phone's lockscreen ([windows](https://user-images.githubusercontent.com/241032/233213022-298a98ba-721a-4cf1-a3d4-f62634bc53d5.png) // [iOS](https://user-images.githubusercontent.com/241032/142711926-0700be6c-3e31-47b3-9928-53722221f722.png) // [android](https://user-images.githubusercontent.com/241032/233212311-a7368590-08c7-4f9f-a1af-48ccf3f36fad.png))
|
||||
* shows the audio waveform in the seekbar
|
||||
* not perfectly gapless but can get really close (see settings + eq below); good enough to enjoy gapless albums as intended
|
||||
* videos can be played as audio, without wasting bandwidth on the video
|
||||
|
||||
click the `play` link next to an audio file, or copy the link target to [share it](https://a.ocv.me/pub/demo/music/Ubiktune%20-%20SOUNDSHOCK%202%20-%20FM%20FUNK%20TERRROR!!/#af-1fbfba61&t=18) (optionally with a timestamp to start playing from, like that example does)
|
||||
|
||||
@@ -778,9 +824,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
|
||||
|
||||
@@ -817,6 +863,13 @@ other notes,
|
||||
* the document preview has a max-width which is the same as an A4 paper when printed
|
||||
|
||||
|
||||
### markdown vars
|
||||
|
||||
dynamic docs with serverside variable expansion to replace stuff like `{{self.ip}}` with the client's IP, or `{{srv.htime}}` with the current time on the server
|
||||
|
||||
see [./srv/expand/](./srv/expand/) for usage and examples
|
||||
|
||||
|
||||
## other tricks
|
||||
|
||||
* you can link a particular timestamp in an audio file by adding it to the URL, such as `&20` / `&20s` / `&1m20` / `&t=1:20` after the `.../#af-c8960dab`
|
||||
@@ -831,6 +884,8 @@ other notes,
|
||||
|
||||
* files named `.prologue.html` / `.epilogue.html` will be rendered before/after directory listings unless `--no-logues`
|
||||
|
||||
* files named `descript.ion` / `DESCRIPT.ION` are parsed and displayed in the file listing, or as the epilogue if nonstandard
|
||||
|
||||
* files named `README.md` / `readme.md` will be rendered after directory listings unless `--no-readme` (but `.epilogue.html` takes precedence)
|
||||
|
||||
* `README.md` and `*logue.html` can contain placeholder values which are replaced server-side before embedding into directory listings; see `--help-exp`
|
||||
@@ -865,6 +920,8 @@ using arguments or config files, or a mix of both:
|
||||
|
||||
**NB:** as humongous as this readme is, there is also a lot of undocumented features. Run copyparty with `--help` to see all available global options; all of those can be used in the `[global]` section of config files, and everything listed in `--help-flags` can be used in volumes as volflags.
|
||||
* if running in docker/podman, try this: `docker run --rm -it copyparty/ac --help`
|
||||
* or see this (probably outdated): https://ocv.me/copyparty/helptext.html
|
||||
* or if you prefer plaintext, https://ocv.me/copyparty/helptext.txt
|
||||
|
||||
|
||||
## zeroconf
|
||||
@@ -933,7 +990,7 @@ some recommended FTP / FTPS clients; `wark` = example password:
|
||||
|
||||
## webdav server
|
||||
|
||||
with read-write support, supports winXP and later, macos, nautilus/gvfs
|
||||
with read-write support, supports winXP and later, macos, nautilus/gvfs ... a greay way to [access copyparty straight from the file explorer in your OS](#mount-as-drive)
|
||||
|
||||
click the [connect](http://127.0.0.1:3923/?hc) button in the control-panel to see connection instructions for windows, linux, macos
|
||||
|
||||
@@ -1041,6 +1098,23 @@ 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, including how to add stuff (css/`<meta>`/...) to the html `<head>` tag, or to add your own translation
|
||||
|
||||
|
||||
## opengraph
|
||||
|
||||
discord and social-media embeds
|
||||
|
||||
can be enabled globally with `--og` or per-volume with volflag `og`
|
||||
|
||||
note that this disables hotlinking because the opengraph spec demands it; to sneak past this intentional limitation, you can enable opengraph selectively by user-agent, for example `--og-ua '(Discord|Twitter|Slack)bot'` (or volflag `og_ua`)
|
||||
|
||||
you can also hotlink files regardless by appending `?raw` to the url
|
||||
|
||||
NOTE: because discord (and maybe others) strip query args such as `?raw` in opengraph tags, any links which require a filekey or dirkey will not work
|
||||
|
||||
if you want to entirely replace the copyparty response with your own jinja2 template, give the template filepath to `--og-tpl` or volflag `og_tpl` (all members of `HttpCli` are available through the `this` object)
|
||||
|
||||
|
||||
## file indexing
|
||||
|
||||
@@ -1380,8 +1454,9 @@ you can either:
|
||||
* or do location-based proxying, using `--rp-loc=/stuff` to tell copyparty where it is mounted -- has a slight performance cost and higher chance of bugs
|
||||
* if copyparty says `incorrect --rp-loc or webserver config; expected vpath starting with [...]` it's likely because the webserver is stripping away the proxy location from the request URLs -- see the `ProxyPass` in the apache example below
|
||||
|
||||
some reverse proxies (such as [Caddy](https://caddyserver.com/)) can automatically obtain a valid https/tls certificate for you, and some support HTTP/2 and QUIC which could be a nice speed boost
|
||||
* **warning:** nginx-QUIC is still experimental and can make uploads much slower, so HTTP/2 is recommended for now
|
||||
some reverse proxies (such as [Caddy](https://caddyserver.com/)) can automatically obtain a valid https/tls certificate for you, and some support HTTP/2 and QUIC which *could* be a nice speed boost, depending on a lot of factors
|
||||
* **warning:** nginx-QUIC (HTTP/3) is still experimental and can make uploads much slower, so HTTP/1.1 is recommended for now
|
||||
* depending on server/client, HTTP/1.1 can also be 5x faster than HTTP/2
|
||||
|
||||
example webserver configs:
|
||||
|
||||
@@ -1461,6 +1536,28 @@ the following options are available to disable some of the metrics:
|
||||
note: the following metrics are counted incorrectly if multiprocessing is enabled with `-j`: `cpp_http_conns`, `cpp_http_reqs`, `cpp_sus_reqs`, `cpp_active_bans`, `cpp_total_bans`
|
||||
|
||||
|
||||
## other extremely specific features
|
||||
|
||||
you'll never find a use for these:
|
||||
|
||||
|
||||
### custom mimetypes
|
||||
|
||||
change the association of a file extension
|
||||
|
||||
using commandline args, you can do something like `--mime gif=image/jif` and `--mime ts=text/x.typescript` (can be specified multiple times)
|
||||
|
||||
in a config-file, this is the same as:
|
||||
|
||||
```yaml
|
||||
[global]
|
||||
mime: gif=image/jif
|
||||
mime: ts=text/x.typescript
|
||||
```
|
||||
|
||||
run copyparty with `--mimes` to list all the default mappings
|
||||
|
||||
|
||||
# packages
|
||||
|
||||
the party might be closer than you think
|
||||
@@ -1704,12 +1801,14 @@ alternatively, some alternatives roughly sorted by speed (unreproducible benchma
|
||||
* [rclone-http](./docs/rclone.md) (26s), read-only
|
||||
* [partyfuse.py](./bin/#partyfusepy) (35s), read-only
|
||||
* [rclone-ftp](./docs/rclone.md) (47s), read/WRITE
|
||||
* davfs2 (103s), read/WRITE, *very fast* on small files
|
||||
* davfs2 (103s), read/WRITE
|
||||
* [win10-webdav](#webdav-server) (138s), read/WRITE
|
||||
* [win10-smb2](#smb-server) (387s), read/WRITE
|
||||
|
||||
most clients will fail to mount the root of a copyparty server unless there is a root volume (so you get the admin-panel instead of a browser when accessing it) -- in that case, mount a specific volume instead
|
||||
|
||||
if you have volumes that are accessible without a password, then some webdav clients (such as davfs2) require the global-option `--dav-auth` to access any password-protected areas
|
||||
|
||||
|
||||
# android app
|
||||
|
||||
@@ -1738,6 +1837,7 @@ defaults are usually fine - expect `8 GiB/s` download, `1 GiB/s` upload
|
||||
|
||||
below are some tweaks roughly ordered by usefulness:
|
||||
|
||||
* disabling HTTP/2 and HTTP/3 can make uploads 5x faster, depending on server/client software
|
||||
* `-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
|
||||
@@ -1839,12 +1939,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 (as if 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
|
||||
|
||||
@@ -1861,7 +1978,7 @@ the default configs take about 0.4 sec and 256 MiB RAM to process a new password
|
||||
|
||||
both HTTP and HTTPS are accepted by default, but letting a [reverse proxy](#reverse-proxy) handle the https/tls/ssl would be better (probably more secure by default)
|
||||
|
||||
copyparty doesn't speak HTTP/2 or QUIC, so using a reverse proxy would solve that as well
|
||||
copyparty doesn't speak HTTP/2 or QUIC, so using a reverse proxy would solve that as well -- but note that HTTP/1 is usually faster than both HTTP/2 and HTTP/3
|
||||
|
||||
if [cfssl](https://github.com/cloudflare/cfssl/releases/latest) is installed, copyparty will automatically create a CA and server-cert on startup
|
||||
* the certs are written to `--crt-dir` for distribution, see `--help` for the other `--crt` options
|
||||
@@ -1936,7 +2053,7 @@ these are standalone programs and will never be imported / evaluated by copypart
|
||||
|
||||
# sfx
|
||||
|
||||
the self-contained "binary" [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) will unpack itself and run copyparty, assuming you have python installed of course
|
||||
the self-contained "binary" (recommended!) [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) will unpack itself and run copyparty, assuming you have python installed of course
|
||||
|
||||
you can reduce the sfx size by repacking it; see [./docs/devnotes.md#sfx-repack](./docs/devnotes.md#sfx-repack)
|
||||
|
||||
@@ -1963,6 +2080,16 @@ meanwhile [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/d
|
||||
then again, if you are already into downloading shady binaries from the internet, you may also want my [minimal builds](./scripts/pyinstaller#ffmpeg) of [ffmpeg](https://ocv.me/stuff/bin/ffmpeg.exe) and [ffprobe](https://ocv.me/stuff/bin/ffprobe.exe) which enables copyparty to extract multimedia-info, do audio-transcoding, and thumbnails/spectrograms/waveforms, however it's much better to instead grab a [recent official build](https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z) every once ina while if you can afford the size
|
||||
|
||||
|
||||
## zipapp
|
||||
|
||||
another emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz) has less features, requires python 3.7 or newer, worse compression, and more importantly is unable to benefit from more recent versions of jinja2 and such (which makes it less secure)... lots of drawbacks with this one really -- but it *may* just work if the regular sfx fails to start because the computer is messed up in certain funky ways, so it's worth a shot if all else fails
|
||||
|
||||
run it by doubleclicking it, or try typing `python copyparty.pyz` in your terminal/console/commandline/telex if that fails
|
||||
|
||||
it is a python [zipapp](https://docs.python.org/3/library/zipapp.html) meaning it doesn't have to unpack its own python code anywhere to run, so if the filesystem is busted it has a better chance of getting somewhere
|
||||
* but note that it currently still needs to extract the web-resources somewhere (they'll land in the default TEMP-folder of your OS)
|
||||
|
||||
|
||||
# install on android
|
||||
|
||||
install [Termux](https://termux.com/) + its companion app `Termux:API` (see [ocv.me/termux](https://ocv.me/termux/)) and then copy-paste this into Termux (long-tap) all at once:
|
||||
|
||||
@@ -2,7 +2,7 @@ standalone programs which are executed by copyparty when an event happens (uploa
|
||||
|
||||
these programs either take zero arguments, or a filepath (the affected file), or a json message with filepath + additional info
|
||||
|
||||
run copyparty with `--help-hooks` for usage details / hook type explanations (xbu/xau/xiu/xbr/xar/xbd/xad)
|
||||
run copyparty with `--help-hooks` for usage details / hook type explanations (xm/xbu/xau/xiu/xbr/xar/xbd/xad/xban)
|
||||
|
||||
> **note:** in addition to event hooks (the stuff described here), copyparty has another api to run your programs/scripts while providing way more information such as audio tags / video codecs / etc and optionally daisychaining data between scripts in a processing pipeline; if that's what you want then see [mtp plugins](../mtag/) instead
|
||||
|
||||
@@ -13,6 +13,7 @@ run copyparty with `--help-hooks` for usage details / hook type explanations (xb
|
||||
* [image-noexif.py](image-noexif.py) removes image exif by overwriting / directly editing the uploaded file
|
||||
* [discord-announce.py](discord-announce.py) announces new uploads on discord using webhooks ([example](https://user-images.githubusercontent.com/241032/215304439-1c1cb3c8-ec6f-4c17-9f27-81f969b1811a.png))
|
||||
* [reject-mimetype.py](reject-mimetype.py) rejects uploads unless the mimetype is acceptable
|
||||
* [into-the-cache-it-goes.py](into-the-cache-it-goes.py) avoids bugs in caching proxies by immediately downloading each file that is uploaded
|
||||
|
||||
|
||||
# upload batches
|
||||
@@ -27,3 +28,5 @@ these are `--xiu` hooks; unlike `xbu` and `xau` (which get executed on every sin
|
||||
|
||||
# on message
|
||||
* [wget.py](wget.py) lets you download files by POSTing URLs to copyparty
|
||||
* [qbittorrent-magnet.py](qbittorrent-magnet.py) starts downloading a torrent if you post a magnet url
|
||||
* [msg-log.py](msg-log.py) is a guestbook; logs messages to a doc in the same folder
|
||||
|
||||
@@ -12,19 +12,28 @@ announces a new upload on discord
|
||||
example usage as global config:
|
||||
--xau f,t5,j,bin/hooks/discord-announce.py
|
||||
|
||||
parameters explained,
|
||||
xau = execute after upload
|
||||
f = fork; don't delay other hooks while this is running
|
||||
t5 = timeout if it's still running after 5 sec
|
||||
j = this hook needs upload information as json (not just the filename)
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:r:rw,ed:c,xau=f,t5,j,bin/hooks/discord-announce.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all uploads with the params listed below)
|
||||
running this plugin on all uploads with the params explained above)
|
||||
|
||||
parameters explained,
|
||||
xbu = execute after upload
|
||||
f = fork; don't wait for it to finish
|
||||
t5 = timeout if it's still running after 5 sec
|
||||
j = provide upload information as json; not just the filename
|
||||
example usage as a volflag in a copyparty config file:
|
||||
[/inc]
|
||||
srv/inc
|
||||
accs:
|
||||
r: *
|
||||
rw: ed
|
||||
flags:
|
||||
xau: f,t5,j,bin/hooks/discord-announce.py
|
||||
|
||||
replace "xau" with "xbu" to announce Before upload starts instead of After completion
|
||||
|
||||
|
||||
140
bin/hooks/into-the-cache-it-goes.py
Normal file
140
bin/hooks/into-the-cache-it-goes.py
Normal file
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import platform
|
||||
import subprocess as sp
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
_ = r"""
|
||||
try to avoid race conditions in caching proxies
|
||||
(primarily cloudflare, but probably others too)
|
||||
by means of the most obvious solution possible:
|
||||
|
||||
just as each file has finished uploading, use
|
||||
the server's external URL to download the file
|
||||
so that it ends up in the cache, warm and snug
|
||||
|
||||
this intentionally delays the upload response
|
||||
as it waits for the file to finish downloading
|
||||
before copyparty is allowed to return the URL
|
||||
|
||||
NOTE: you must edit this script before use,
|
||||
replacing https://example.com with your URL
|
||||
|
||||
NOTE: if the files are only accessible with a
|
||||
password and/or filekey, you must also add
|
||||
a cromulent password in the PASSWORD field
|
||||
|
||||
NOTE: needs either wget, curl, or "requests":
|
||||
python3 -m pip install --user -U requests
|
||||
|
||||
|
||||
example usage as global config:
|
||||
--xau j,t10,bin/hooks/into-the-cache-it-goes.py
|
||||
|
||||
parameters explained,
|
||||
xau = execute after upload
|
||||
j = this hook needs upload information as json (not just the filename)
|
||||
t10 = abort download and continue if it takes longer than 10sec
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:r:rw,ed:xau=j,t10,bin/hooks/into-the-cache-it-goes.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all uploads with params explained above)
|
||||
|
||||
example usage as a volflag in a copyparty config file:
|
||||
[/inc]
|
||||
srv/inc
|
||||
accs:
|
||||
r: *
|
||||
rw: ed
|
||||
flags:
|
||||
xau: j,t10,bin/hooks/into-the-cache-it-goes.py
|
||||
"""
|
||||
|
||||
|
||||
# replace this with your site's external URL
|
||||
# (including the :portnumber if necessary)
|
||||
SITE_URL = "https://example.com"
|
||||
|
||||
# if downloading is protected by passwords or filekeys,
|
||||
# specify a valid password between the quotes below:
|
||||
PASSWORD = ""
|
||||
|
||||
# if file is larger than this, skip download
|
||||
MAX_MEGABYTES = 8
|
||||
|
||||
# =============== END OF CONFIG ===============
|
||||
|
||||
|
||||
WINDOWS = platform.system() == "Windows"
|
||||
|
||||
|
||||
def main():
|
||||
fun = download_with_python
|
||||
if shutil.which("curl"):
|
||||
fun = download_with_curl
|
||||
elif shutil.which("wget"):
|
||||
fun = download_with_wget
|
||||
|
||||
inf = json.loads(sys.argv[1])
|
||||
|
||||
if inf["sz"] > 1024 * 1024 * MAX_MEGABYTES:
|
||||
print("[into-the-cache] file is too large; will not download")
|
||||
return
|
||||
|
||||
file_url = "/"
|
||||
if inf["vp"]:
|
||||
file_url += inf["vp"] + "/"
|
||||
file_url += inf["ap"].replace("\\", "/").split("/")[-1]
|
||||
file_url = SITE_URL.rstrip("/") + quote(file_url, safe=b"/")
|
||||
|
||||
print("[into-the-cache] %s(%s)" % (fun.__name__, file_url))
|
||||
fun(file_url, PASSWORD.strip())
|
||||
|
||||
print("[into-the-cache] Download OK")
|
||||
|
||||
|
||||
def download_with_curl(url, pw):
|
||||
cmd = ["curl"]
|
||||
|
||||
if pw:
|
||||
cmd += ["-HPW:%s" % (pw,)]
|
||||
|
||||
nah = sp.DEVNULL
|
||||
sp.check_call(cmd + [url], stdout=nah, stderr=nah)
|
||||
|
||||
|
||||
def download_with_wget(url, pw):
|
||||
cmd = ["wget", "-O"]
|
||||
|
||||
cmd += ["nul" if WINDOWS else "/dev/null"]
|
||||
|
||||
if pw:
|
||||
cmd += ["--header=PW:%s" % (pw,)]
|
||||
|
||||
nah = sp.DEVNULL
|
||||
sp.check_call(cmd + [url], stdout=nah, stderr=nah)
|
||||
|
||||
|
||||
def download_with_python(url, pw):
|
||||
import requests
|
||||
|
||||
headers = {}
|
||||
if pw:
|
||||
headers["PW"] = pw
|
||||
|
||||
with requests.get(url, headers=headers, stream=True) as r:
|
||||
r.raise_for_status()
|
||||
for _ in r.iter_content(chunk_size=1024 * 256):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -14,19 +14,32 @@ except:
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
"""
|
||||
_ = r"""
|
||||
use copyparty as a dumb messaging server / guestbook thing;
|
||||
accepts guestbook entries from 📟 (message-to-server-log) in the web-ui
|
||||
initially contributed by @clach04 in https://github.com/9001/copyparty/issues/35 (thanks!)
|
||||
|
||||
Sample usage:
|
||||
|
||||
example usage as global config:
|
||||
python copyparty-sfx.py --xm j,bin/hooks/msg-log.py
|
||||
|
||||
Where:
|
||||
parameters explained,
|
||||
xm = execute on message (📟)
|
||||
j = this hook needs message information as json (not just the message-text)
|
||||
|
||||
xm = execute on message-to-server-log
|
||||
j = provide message information as json; not just the text - this script REQUIRES json
|
||||
t10 = timeout and kill download after 10 secs
|
||||
example usage as a volflag (per-volume config):
|
||||
python copyparty-sfx.py -v srv/log:log:r:c,xm=j,bin/hooks/msg-log.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/log as volume /log, readable by everyone,
|
||||
running this plugin on all messages with the params explained above)
|
||||
|
||||
example usage as a volflag in a copyparty config file:
|
||||
[/log]
|
||||
srv/log
|
||||
accs:
|
||||
r: *
|
||||
flags:
|
||||
xm: j,bin/hooks/msg-log.py
|
||||
"""
|
||||
|
||||
|
||||
|
||||
128
bin/hooks/qbittorrent-magnet.py
Executable file
128
bin/hooks/qbittorrent-magnet.py
Executable file
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import subprocess as sp
|
||||
|
||||
|
||||
_ = r"""
|
||||
start downloading a torrent by POSTing a magnet URL to copyparty,
|
||||
for example using 📟 (message-to-server-log) in the web-ui
|
||||
|
||||
by default it will download the torrent to the folder you were in
|
||||
when you pasted the magnet into the message-to-server-log field
|
||||
|
||||
you can optionally specify another location by adding a whitespace
|
||||
after the magnet URL followed by the name of the subfolder to DL into,
|
||||
or for example "anime/airing" would download to /srv/media/anime/airing
|
||||
because the keyword "anime" is in the DESTS config below
|
||||
|
||||
needs python3
|
||||
|
||||
example usage as global config (not a good idea):
|
||||
python copyparty-sfx.py --xm aw,f,j,t60,bin/hooks/qbittorrent-magnet.py
|
||||
|
||||
parameters explained,
|
||||
xm = execute on message (📟)
|
||||
aw = only users with write-access can use this
|
||||
f = fork; don't delay other hooks while this is running
|
||||
j = provide message information as json (not just the text)
|
||||
t60 = abort if qbittorrent has to think about it for more than 1 min
|
||||
|
||||
example usage as a volflag (per-volume config, much better):
|
||||
-v srv/qb:qb:A,ed:c,xm=aw,f,j,t60,bin/hooks/qbittorrent-magnet.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/qb as volume /qb with Admin for user 'ed',
|
||||
running this plugin on all messages with the params explained above)
|
||||
|
||||
example usage as a volflag in a copyparty config file:
|
||||
[/qb]
|
||||
srv/qb
|
||||
accs:
|
||||
A: ed
|
||||
flags:
|
||||
xm: aw,f,j,t60,bin/hooks/qbittorrent-magnet.py
|
||||
|
||||
the volflag examples only kicks in if you send the torrent magnet
|
||||
while you're in the /qb folder (or any folder below there)
|
||||
"""
|
||||
|
||||
|
||||
# list of usernames to allow
|
||||
ALLOWLIST = [ "ed", "morpheus" ]
|
||||
|
||||
|
||||
# list of destination aliases to translate into full filesystem
|
||||
# paths; takes effect if the first folder component in the
|
||||
# custom download location matches anything in this dict
|
||||
DESTS = {
|
||||
"iso": "/srv/pub/linux-isos",
|
||||
"anime": "/srv/media/anime",
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
inf = json.loads(sys.argv[1])
|
||||
url = inf["txt"]
|
||||
if not url.lower().startswith("magnet:?"):
|
||||
# not a magnet, abort
|
||||
return
|
||||
|
||||
if inf["user"] not in ALLOWLIST:
|
||||
print("🧲 denied for user", inf["user"])
|
||||
return
|
||||
|
||||
# might as well run the command inside the filesystem folder
|
||||
# which matches the URL that the magnet message was sent to
|
||||
os.chdir(inf["ap"])
|
||||
|
||||
# is there is a custom download location in the url?
|
||||
dst = ""
|
||||
if " " in url:
|
||||
url, dst = url.split(" ", 1)
|
||||
|
||||
# is the location in the predefined list of locations?
|
||||
parts = dst.replace("\\", "/").split("/")
|
||||
if parts[0] in DESTS:
|
||||
dst = os.path.join(DESTS[parts[0]], *(parts[1:]))
|
||||
|
||||
else:
|
||||
# nope, so download to the current folder instead;
|
||||
# comment the dst line below to instead use the default
|
||||
# download location from your qbittorrent settings
|
||||
dst = inf["ap"]
|
||||
pass
|
||||
|
||||
# archlinux has a -nox suffix for qbittorrent if headless
|
||||
# so check if we should be using that
|
||||
if shutil.which("qbittorrent-nox"):
|
||||
torrent_bin = "qbittorrent-nox"
|
||||
else:
|
||||
torrent_bin = "qbittorrent"
|
||||
|
||||
# the command to add a new torrent, adjust if necessary
|
||||
cmd = [torrent_bin, url]
|
||||
if dst:
|
||||
cmd += ["--save-path=%s" % (dst,)]
|
||||
|
||||
# if copyparty and qbittorrent are running as different users
|
||||
# you may have to do something like the following
|
||||
# (assuming qbittorrent* is nopasswd-allowed in sudoers):
|
||||
#
|
||||
# cmd = ["sudo", "-u", "qbitter"] + cmd
|
||||
|
||||
print("🧲", cmd)
|
||||
|
||||
try:
|
||||
sp.check_call(cmd)
|
||||
except:
|
||||
print("🧲 FAILED TO ADD", url)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -9,25 +9,38 @@ import subprocess as sp
|
||||
_ = r"""
|
||||
use copyparty as a file downloader by POSTing URLs as
|
||||
application/x-www-form-urlencoded (for example using the
|
||||
message/pager function on the website)
|
||||
📟 message-to-server-log in the web-ui)
|
||||
|
||||
example usage as global config:
|
||||
--xm f,j,t3600,bin/hooks/wget.py
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:r:rw,ed:c,xm=f,j,t3600,bin/hooks/wget.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all messages with the params listed below)
|
||||
--xm aw,f,j,t3600,bin/hooks/wget.py
|
||||
|
||||
parameters explained,
|
||||
xm = execute on message-to-server-log
|
||||
f = fork so it doesn't block uploads
|
||||
j = provide message information as json; not just the text
|
||||
aw = only users with write-access can use this
|
||||
f = fork; don't delay other hooks while this is running
|
||||
j = provide message information as json (not just the text)
|
||||
c3 = mute all output
|
||||
t3600 = timeout and kill download after 1 hour
|
||||
t3600 = timeout and abort download after 1 hour
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:r:rw,ed:c,xm=aw,f,j,t3600,bin/hooks/wget.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all messages with the params explained above)
|
||||
|
||||
example usage as a volflag in a copyparty config file:
|
||||
[/inc]
|
||||
srv/inc
|
||||
accs:
|
||||
r: *
|
||||
rw: ed
|
||||
flags:
|
||||
xm: aw,f,j,t3600,bin/hooks/wget.py
|
||||
|
||||
the volflag examples only kicks in if you send the message
|
||||
while you're in the /inc folder (or any folder below there)
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ install_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
|
||||
@@ -247,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
|
||||
|
||||
194
bin/u2c.py
194
bin/u2c.py
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
S_VERSION = "1.15"
|
||||
S_BUILD_DT = "2024-02-18"
|
||||
S_VERSION = "1.21"
|
||||
S_BUILD_DT = "2024-07-26"
|
||||
|
||||
"""
|
||||
u2c.py: upload to copyparty
|
||||
@@ -20,6 +20,7 @@ import sys
|
||||
import stat
|
||||
import math
|
||||
import time
|
||||
import json
|
||||
import atexit
|
||||
import signal
|
||||
import socket
|
||||
@@ -80,11 +81,20 @@ req_ses = requests.Session()
|
||||
|
||||
class Daemon(threading.Thread):
|
||||
def __init__(self, target, name=None, a=None):
|
||||
# type: (Any, Any, Any) -> None
|
||||
threading.Thread.__init__(self, target=target, args=a or (), name=name)
|
||||
threading.Thread.__init__(self, name=name)
|
||||
self.a = a or ()
|
||||
self.fun = target
|
||||
self.daemon = True
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
signal.pthread_sigmask(signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM])
|
||||
except:
|
||||
pass
|
||||
|
||||
self.fun(*self.a)
|
||||
|
||||
|
||||
class File(object):
|
||||
"""an up2k upload task; represents a single file"""
|
||||
@@ -101,18 +111,22 @@ class File(object):
|
||||
# set by get_hashlist
|
||||
self.cids = [] # type: list[tuple[str, int, int]] # [ hash, ofs, sz ]
|
||||
self.kchunks = {} # type: dict[str, tuple[int, int]] # hash: [ ofs, sz ]
|
||||
self.t_hash = 0.0 # type: float
|
||||
|
||||
# set by handshake
|
||||
self.recheck = False # duplicate; redo handshake after all files done
|
||||
self.ucids = [] # type: list[str] # chunks which need to be uploaded
|
||||
self.wark = "" # type: str
|
||||
self.url = "" # type: str
|
||||
self.nhs = 0
|
||||
self.nhs = 0 # type: int
|
||||
|
||||
# set by upload
|
||||
self.t0_up = 0.0 # type: float
|
||||
self.t1_up = 0.0 # type: float
|
||||
self.nojoin = 0 # type: int
|
||||
self.up_b = 0 # type: int
|
||||
self.up_c = 0 # type: int
|
||||
self.cd = 0
|
||||
self.cd = 0 # type: int
|
||||
|
||||
# t = "size({}) lmod({}) top({}) rel({}) abs({}) name({})\n"
|
||||
# eprint(t.format(self.size, self.lmod, self.top, self.rel, self.abs, self.name))
|
||||
@@ -121,10 +135,20 @@ class File(object):
|
||||
class FileSlice(object):
|
||||
"""file-like object providing a fixed window into a file"""
|
||||
|
||||
def __init__(self, file, cid):
|
||||
def __init__(self, file, cids):
|
||||
# type: (File, str) -> None
|
||||
|
||||
self.car, self.len = file.kchunks[cid]
|
||||
self.file = file
|
||||
self.cids = cids
|
||||
|
||||
self.car, tlen = file.kchunks[cids[0]]
|
||||
for cid in cids[1:]:
|
||||
ofs, clen = file.kchunks[cid]
|
||||
if ofs != self.car + tlen:
|
||||
raise Exception(9)
|
||||
tlen += clen
|
||||
|
||||
self.len = tlen
|
||||
self.cdr = self.car + self.len
|
||||
self.ofs = 0 # type: int
|
||||
self.f = open(file.abs, "rb", 512 * 1024)
|
||||
@@ -348,7 +372,7 @@ def undns(url):
|
||||
usp = urlsplit(url)
|
||||
hn = usp.hostname
|
||||
gai = None
|
||||
eprint("resolving host [{0}] ...".format(hn), end="")
|
||||
eprint("resolving host [%s] ..." % (hn,))
|
||||
try:
|
||||
gai = socket.getaddrinfo(hn, None)
|
||||
hn = gai[0][4][0]
|
||||
@@ -366,7 +390,7 @@ def undns(url):
|
||||
|
||||
usp = usp._replace(netloc=hn)
|
||||
url = urlunsplit(usp)
|
||||
eprint(" {0}".format(url))
|
||||
eprint(" %s\n" % (url,))
|
||||
return url
|
||||
|
||||
|
||||
@@ -509,6 +533,8 @@ def get_hashlist(file, pcb, mth):
|
||||
file_ofs = 0
|
||||
ret = []
|
||||
with open(file.abs, "rb", 512 * 1024) as f:
|
||||
t0 = time.time()
|
||||
|
||||
if mth and file.size >= 1024 * 512:
|
||||
ret = mth.hash(f, file.size, chunk_sz, pcb, file)
|
||||
file_rem = 0
|
||||
@@ -535,10 +561,12 @@ def get_hashlist(file, pcb, mth):
|
||||
if pcb:
|
||||
pcb(file, file_ofs)
|
||||
|
||||
file.t_hash = time.time() - t0
|
||||
file.cids = ret
|
||||
file.kchunks = {}
|
||||
for k, v1, v2 in ret:
|
||||
file.kchunks[k] = [v1, v2]
|
||||
if k not in file.kchunks:
|
||||
file.kchunks[k] = [v1, v2]
|
||||
|
||||
|
||||
def handshake(ar, file, search):
|
||||
@@ -563,7 +591,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
|
||||
@@ -580,7 +608,8 @@ def handshake(ar, file, search):
|
||||
sc = 600
|
||||
txt = ""
|
||||
try:
|
||||
r = req_ses.post(url, headers=headers, json=req)
|
||||
zs = json.dumps(req, separators=(",\n", ": "))
|
||||
r = req_ses.post(url, headers=headers, data=zs)
|
||||
sc = r.status_code
|
||||
txt = r.text
|
||||
if sc < 400:
|
||||
@@ -627,13 +656,13 @@ def handshake(ar, file, search):
|
||||
return r["hash"], r["sprs"]
|
||||
|
||||
|
||||
def upload(file, cid, pw, stats):
|
||||
# type: (File, str, str, str) -> None
|
||||
"""upload one specific chunk, `cid` (a chunk-hash)"""
|
||||
def upload(fsl, pw, stats):
|
||||
# type: (FileSlice, str, str) -> None
|
||||
"""upload a range of file data, defined by one or more `cid` (chunk-hash)"""
|
||||
|
||||
headers = {
|
||||
"X-Up2k-Hash": cid,
|
||||
"X-Up2k-Wark": file.wark,
|
||||
"X-Up2k-Hash": ",".join(fsl.cids),
|
||||
"X-Up2k-Wark": fsl.file.wark,
|
||||
"Content-Type": "application/octet-stream",
|
||||
}
|
||||
|
||||
@@ -643,15 +672,24 @@ def upload(file, cid, pw, stats):
|
||||
if pw:
|
||||
headers["Cookie"] = "=".join(["cppwd", pw])
|
||||
|
||||
f = FileSlice(file, cid)
|
||||
try:
|
||||
r = req_ses.post(file.url, headers=headers, data=f)
|
||||
r = req_ses.post(fsl.file.url, headers=headers, data=fsl)
|
||||
|
||||
if r.status_code == 400:
|
||||
txt = r.text
|
||||
if (
|
||||
"already being written" in txt
|
||||
or "already got that" in txt
|
||||
or "only sibling chunks" in txt
|
||||
):
|
||||
fsl.file.nojoin = 1
|
||||
|
||||
if not r:
|
||||
raise Exception(repr(r))
|
||||
|
||||
_ = r.content
|
||||
finally:
|
||||
f.f.close()
|
||||
fsl.f.close()
|
||||
|
||||
|
||||
class Ctl(object):
|
||||
@@ -715,6 +753,9 @@ class Ctl(object):
|
||||
if ar.safe:
|
||||
self._safe()
|
||||
else:
|
||||
self.at_hash = 0.0
|
||||
self.at_up = 0.0
|
||||
self.at_upr = 0.0
|
||||
self.hash_f = 0
|
||||
self.hash_c = 0
|
||||
self.hash_b = 0
|
||||
@@ -734,7 +775,7 @@ class Ctl(object):
|
||||
|
||||
self.mutex = threading.Lock()
|
||||
self.q_handshake = Queue() # type: Queue[File]
|
||||
self.q_upload = Queue() # type: Queue[tuple[File, str]]
|
||||
self.q_upload = Queue() # type: Queue[FileSlice]
|
||||
|
||||
self.st_hash = [None, "(idle, starting...)"] # type: tuple[File, int]
|
||||
self.st_up = [None, "(idle, starting...)"] # type: tuple[File, int]
|
||||
@@ -779,7 +820,8 @@ class Ctl(object):
|
||||
for nc, cid in enumerate(hs):
|
||||
print(" {0} up {1}".format(ncs - nc, cid))
|
||||
stats = "{0}/0/0/{1}".format(nf, self.nfiles - nf)
|
||||
upload(file, cid, self.ar.a, stats)
|
||||
fslice = FileSlice(file, [cid])
|
||||
upload(fslice, self.ar.a, stats)
|
||||
|
||||
print(" ok!")
|
||||
if file.recheck:
|
||||
@@ -788,7 +830,7 @@ class Ctl(object):
|
||||
if not self.recheck:
|
||||
return
|
||||
|
||||
eprint("finalizing {0} duplicate files".format(len(self.recheck)))
|
||||
eprint("finalizing %d duplicate files\n" % (len(self.recheck),))
|
||||
for file in self.recheck:
|
||||
handshake(self.ar, file, search)
|
||||
|
||||
@@ -862,10 +904,17 @@ class Ctl(object):
|
||||
t = "{0} eta @ {1}/s, {2}, {3}# left".format(self.eta, spd, sleft, nleft)
|
||||
eprint(txt + "\033]0;{0}\033\\\r{0}{1}".format(t, tail))
|
||||
|
||||
if self.hash_b and self.at_hash:
|
||||
spd = humansize(self.hash_b / self.at_hash)
|
||||
eprint("\nhasher: %.2f sec, %s/s\n" % (self.at_hash, spd))
|
||||
if self.up_b and self.at_up:
|
||||
spd = humansize(self.up_b / self.at_up)
|
||||
eprint("upload: %.2f sec, %s/s\n" % (self.at_up, spd))
|
||||
|
||||
if not self.recheck:
|
||||
return
|
||||
|
||||
eprint("finalizing {0} duplicate files".format(len(self.recheck)))
|
||||
eprint("finalizing %d duplicate files\n" % (len(self.recheck),))
|
||||
for file in self.recheck:
|
||||
handshake(self.ar, file, False)
|
||||
|
||||
@@ -1051,21 +1100,62 @@ class Ctl(object):
|
||||
self.handshaker_busy -= 1
|
||||
|
||||
if not hs:
|
||||
kw = "uploaded" if file.up_b else " found"
|
||||
print("{0} {1}".format(kw, upath))
|
||||
for cid in hs:
|
||||
self.q_upload.put([file, cid])
|
||||
self.at_hash += file.t_hash
|
||||
|
||||
if self.ar.spd:
|
||||
if VT100:
|
||||
c1 = "\033[36m"
|
||||
c2 = "\033[0m"
|
||||
else:
|
||||
c1 = c2 = ""
|
||||
|
||||
spd_h = humansize(file.size / file.t_hash, True)
|
||||
if file.up_b:
|
||||
t_up = file.t1_up - file.t0_up
|
||||
spd_u = humansize(file.size / t_up, True)
|
||||
|
||||
t = "uploaded %s %s(h:%.2fs,%s/s,up:%.2fs,%s/s)%s"
|
||||
print(t % (upath, c1, file.t_hash, spd_h, t_up, spd_u, c2))
|
||||
else:
|
||||
t = " found %s %s(%.2fs,%s/s)%s"
|
||||
print(t % (upath, c1, file.t_hash, spd_h, c2))
|
||||
else:
|
||||
kw = "uploaded" if file.up_b else " found"
|
||||
print("{0} {1}".format(kw, upath))
|
||||
|
||||
chunksz = up2k_chunksize(file.size)
|
||||
njoin = (self.ar.sz * 1024 * 1024) // chunksz
|
||||
cs = hs[:]
|
||||
while cs:
|
||||
fsl = FileSlice(file, cs[:1])
|
||||
try:
|
||||
if file.nojoin:
|
||||
raise Exception()
|
||||
for n in range(2, min(len(cs), njoin + 1)):
|
||||
fsl = FileSlice(file, cs[:n])
|
||||
except:
|
||||
pass
|
||||
cs = cs[len(fsl.cids) :]
|
||||
self.q_upload.put(fsl)
|
||||
|
||||
def uploader(self):
|
||||
while True:
|
||||
task = self.q_upload.get()
|
||||
if not task:
|
||||
fsl = self.q_upload.get()
|
||||
if not fsl:
|
||||
self.st_up = [None, "(finished)"]
|
||||
break
|
||||
|
||||
file = fsl.file
|
||||
cids = fsl.cids
|
||||
|
||||
with self.mutex:
|
||||
if not self.uploader_busy:
|
||||
self.at_upr = time.time()
|
||||
self.uploader_busy += 1
|
||||
self.t0_up = self.t0_up or time.time()
|
||||
if not file.t0_up:
|
||||
file.t0_up = time.time()
|
||||
if not self.t0_up:
|
||||
self.t0_up = file.t0_up
|
||||
|
||||
stats = "%d/%d/%d/%d %d/%d %s" % (
|
||||
self.up_f,
|
||||
@@ -1077,28 +1167,30 @@ class Ctl(object):
|
||||
self.eta,
|
||||
)
|
||||
|
||||
file, cid = task
|
||||
try:
|
||||
upload(file, cid, self.ar.a, stats)
|
||||
upload(fsl, self.ar.a, stats)
|
||||
except Exception as ex:
|
||||
t = "upload failed, retrying: {0} #{1} ({2})\n"
|
||||
eprint(t.format(file.name, cid[:8], ex))
|
||||
t = "upload failed, retrying: %s #%s+%d (%s)\n"
|
||||
eprint(t % (file.name, cids[0][:8], len(cids) - 1, ex))
|
||||
file.cd = time.time() + self.ar.cd
|
||||
# handshake will fix it
|
||||
|
||||
with self.mutex:
|
||||
sz = file.kchunks[cid][1]
|
||||
file.ucids = [x for x in file.ucids if x != cid]
|
||||
sz = fsl.len
|
||||
file.ucids = [x for x in file.ucids if x not in cids]
|
||||
if not file.ucids:
|
||||
file.t1_up = time.time()
|
||||
self.q_handshake.put(file)
|
||||
|
||||
self.st_up = [file, cid]
|
||||
self.st_up = [file, cids[0]]
|
||||
file.up_b += sz
|
||||
self.up_b += sz
|
||||
self.up_br += sz
|
||||
file.up_c += 1
|
||||
self.up_c += 1
|
||||
self.uploader_busy -= 1
|
||||
if not self.uploader_busy:
|
||||
self.at_up += time.time() - self.at_upr
|
||||
|
||||
def up_done(self, file):
|
||||
if self.ar.dl:
|
||||
@@ -1135,11 +1227,13 @@ source file/folder selection uses rsync syntax, meaning that:
|
||||
ap.add_argument("url", type=unicode, help="server url, including destination folder")
|
||||
ap.add_argument("files", type=unicode, nargs="+", help="files and/or folders to process")
|
||||
ap.add_argument("-v", action="store_true", help="verbose")
|
||||
ap.add_argument("-a", metavar="PASSWORD", help="password or $filepath")
|
||||
ap.add_argument("-a", metavar="PASSWD", help="password or $filepath")
|
||||
ap.add_argument("-s", action="store_true", help="file-search (disables upload)")
|
||||
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("--spd", action="store_true", help="print speeds for each file")
|
||||
ap.add_argument("--version", action="store_true", help="show version and exit")
|
||||
|
||||
ap = app.add_argument_group("compatibility")
|
||||
@@ -1148,12 +1242,13 @@ 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")
|
||||
ap.add_argument("-j", type=int, metavar="THREADS", default=4, help="parallel connections")
|
||||
ap.add_argument("-J", type=int, metavar="THREADS", default=hcores, help="num cpu-cores to use for hashing; set 0 or 1 for single-core hashing")
|
||||
ap.add_argument("-j", type=int, metavar="CONNS", default=2, help="parallel connections")
|
||||
ap.add_argument("-J", type=int, metavar="CORES", default=hcores, help="num cpu-cores to use for hashing; set 0 or 1 for single-core hashing")
|
||||
ap.add_argument("--sz", type=int, metavar="MiB", default=64, help="try to make each POST this big")
|
||||
ap.add_argument("-nh", action="store_true", help="disable hashing while uploading")
|
||||
ap.add_argument("-ns", action="store_true", help="no status panel (for slow consoles and macos)")
|
||||
ap.add_argument("--cd", type=float, metavar="SEC", default=5, help="delay before reattempting a failed handshake/upload")
|
||||
@@ -1161,7 +1256,7 @@ source file/folder selection uses rsync syntax, meaning that:
|
||||
ap.add_argument("-z", action="store_true", help="ZOOMIN' (skip uploading files if they exist at the destination with the ~same last-modified timestamp, so same as yolo / turbo with date-chk but even faster)")
|
||||
|
||||
ap = app.add_argument_group("tls")
|
||||
ap.add_argument("-te", metavar="PEM_FILE", help="certificate to expect/verify")
|
||||
ap.add_argument("-te", metavar="PATH", help="path to ca.pem or cert.pem to expect/verify")
|
||||
ap.add_argument("-td", action="store_true", help="disable certificate check")
|
||||
# fmt: on
|
||||
|
||||
@@ -1178,6 +1273,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):
|
||||
@@ -1196,6 +1294,14 @@ source file/folder selection uses rsync syntax, meaning that:
|
||||
if "://" not in ar.url:
|
||||
ar.url = "http://" + ar.url
|
||||
|
||||
if "https://" in ar.url.lower():
|
||||
try:
|
||||
import ssl, zipfile
|
||||
except:
|
||||
t = "ERROR: https is not available for some reason; please use http"
|
||||
print("\n\n %s\n\n" % (t,))
|
||||
raise
|
||||
|
||||
if ar.a and ar.a.startswith("$"):
|
||||
fn = ar.a[1:]
|
||||
print("reading password from file [{0}]".format(fn))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: icxes <dev.null@need.moe>
|
||||
pkgname=copyparty
|
||||
pkgver="1.11.1"
|
||||
pkgver="1.13.5"
|
||||
pkgrel=1
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||
arch=("any")
|
||||
@@ -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=("13e4a65d1854f4f95308fa91c00bd8a5f5977b3ea4fa844ed08c7e1cb1c4bf29")
|
||||
sha256sums=("83bf52ac03256ee6fe405a912e2767578692760f9554f821dfcab0700dd58082")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.11.1/copyparty-sfx.py",
|
||||
"version": "1.11.1",
|
||||
"hash": "sha256-q7RiaB5yo1EDTwdPeMCNFnmcNj0TsKzBsbsddMSqTH4="
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.13.5/copyparty-sfx.py",
|
||||
"version": "1.13.5",
|
||||
"hash": "sha256-I+dqsiScYPcX6JpLgwVoLs7l0FlbXabc/Ofqye9RQI0="
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
#
|
||||
# installation:
|
||||
# wget https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py -O /usr/local/bin/copyparty-sfx.py
|
||||
# useradd -r -s /sbin/nologin -d /var/lib/copyparty copyparty
|
||||
# useradd -r -s /sbin/nologin -m -d /var/lib/copyparty copyparty
|
||||
# firewall-cmd --permanent --add-port=3923/tcp # --zone=libvirt
|
||||
# firewall-cmd --reload
|
||||
# cp -pv copyparty.service /etc/systemd/system/
|
||||
@@ -12,11 +12,18 @@
|
||||
# restorecon -vr /etc/systemd/system/copyparty.service # on fedora/rhel
|
||||
# systemctl daemon-reload && systemctl enable --now copyparty
|
||||
#
|
||||
# every time you edit this file, you must "systemctl daemon-reload"
|
||||
# for the changes to take effect and then "systemctl restart copyparty"
|
||||
#
|
||||
# if it fails to start, first check this: systemctl status copyparty
|
||||
# then try starting it while viewing logs:
|
||||
# journalctl -fan 100
|
||||
# tail -Fn 100 /var/log/copyparty/$(date +%Y-%m%d.log)
|
||||
#
|
||||
# if you run into any issues, for example thumbnails not working,
|
||||
# try removing the "some quick hardening" section and then please
|
||||
# let me know if that actually helped so we can look into it
|
||||
#
|
||||
# you may want to:
|
||||
# - change "User=copyparty" and "/var/lib/copyparty/" to another user
|
||||
# - edit /etc/copyparty.conf to configure copyparty
|
||||
|
||||
118
contrib/themes/bsod.css
Normal file
118
contrib/themes/bsod.css
Normal file
@@ -0,0 +1,118 @@
|
||||
/* copy bsod.* into a folder named ".themes" in your webroot and then
|
||||
--themes=10 --theme=9 --css-browser=/.themes/bsod.css
|
||||
*/
|
||||
|
||||
html.ey {
|
||||
--w2: #3d7bbc;
|
||||
--w3: #5fcbec;
|
||||
|
||||
--fg: #fff;
|
||||
--fg-max: #fff;
|
||||
--fg-weak: var(--w3);
|
||||
|
||||
--bg: #2067b2;
|
||||
--bg-d3: var(--bg);
|
||||
--bg-d2: var(--w2);
|
||||
--bg-d1: var(--fg-weak);
|
||||
--bg-u2: var(--bg);
|
||||
--bg-u3: var(--bg);
|
||||
--bg-u5: var(--w2);
|
||||
|
||||
--tab-alt: var(--fg-weak);
|
||||
--row-alt: var(--w2);
|
||||
|
||||
--scroll: var(--w3);
|
||||
|
||||
--a: #fff;
|
||||
--a-b: #fff;
|
||||
--a-hil: #fff;
|
||||
--a-h-bg: var(--fg-weak);
|
||||
--a-dark: var(--a);
|
||||
--a-gray: var(--fg-weak);
|
||||
|
||||
--btn-fg: var(--a);
|
||||
--btn-bg: var(--w2);
|
||||
--btn-h-fg: var(--w2);
|
||||
--btn-1-fg: var(--bg);
|
||||
--btn-1-bg: var(--a);
|
||||
--txt-sh: a;
|
||||
--txt-bg: var(--w2);
|
||||
|
||||
--u2-b1-bg: var(--w2);
|
||||
--u2-b2-bg: var(--w2);
|
||||
--u2-o-bg: var(--w2);
|
||||
--u2-o-1-bg: var(--a);
|
||||
--u2-txt-bg: var(--w2);
|
||||
--u2-tab-bg: a;
|
||||
--u2-tab-1-bg: var(--w2);
|
||||
|
||||
--sort-1: var(--a);
|
||||
--sort-1: var(--fg-weak);
|
||||
|
||||
--tree-bg: var(--bg);
|
||||
|
||||
--g-b1: a;
|
||||
--g-b2: a;
|
||||
--g-f-bg: var(--w2);
|
||||
|
||||
--f-sh1: 0.1;
|
||||
--f-sh2: 0.02;
|
||||
--f-sh3: 0.1;
|
||||
--f-h-b1: a;
|
||||
|
||||
--srv-1: var(--a);
|
||||
--srv-3: var(--a);
|
||||
|
||||
--mp-sh: a;
|
||||
}
|
||||
|
||||
html.ey {
|
||||
background: url('bsod.png') top 5em right 4.5em no-repeat fixed var(--bg);
|
||||
}
|
||||
html.ey body#b {
|
||||
background: var(--bg); /*sandbox*/
|
||||
}
|
||||
html.ey #ops {
|
||||
margin: 1.7em 1.5em 0 1.5em;
|
||||
border-radius: .3em;
|
||||
border-width: 1px 0;
|
||||
}
|
||||
html.ey #ops a {
|
||||
text-shadow: 1px 1px 0 rgba(0,0,0,0.5);
|
||||
}
|
||||
html.ey .opbox {
|
||||
margin: 1.5em 0 0 0;
|
||||
}
|
||||
html.ey #tree {
|
||||
box-shadow: none;
|
||||
}
|
||||
html.ey #tt {
|
||||
border-color: var(--w2);
|
||||
background: var(--w2);
|
||||
}
|
||||
html.ey .mdo a {
|
||||
background: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
html.ey .mdo pre,
|
||||
html.ey .mdo code {
|
||||
color: #fff;
|
||||
background: var(--w2);
|
||||
border: none;
|
||||
}
|
||||
html.ey .mdo h1,
|
||||
html.ey .mdo h2 {
|
||||
background: none;
|
||||
border-color: var(--w2);
|
||||
}
|
||||
html.ey .mdo ul ul,
|
||||
html.ey .mdo ul ol,
|
||||
html.ey .mdo ol ul,
|
||||
html.ey .mdo ol ol {
|
||||
border-color: var(--w2);
|
||||
}
|
||||
html.ey .mdo p>em,
|
||||
html.ey .mdo li>em,
|
||||
html.ey .mdo td>em {
|
||||
color: #fd0;
|
||||
}
|
||||
BIN
contrib/themes/bsod.png
Normal file
BIN
contrib/themes/bsod.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -56,7 +56,6 @@ class EnvParams(object):
|
||||
self.t0 = time.time()
|
||||
self.mod = ""
|
||||
self.cfg = ""
|
||||
self.ox = getattr(sys, "oxidized", None)
|
||||
|
||||
|
||||
E = EnvParams()
|
||||
|
||||
268
copyparty/__main__.py
Executable file → Normal file
268
copyparty/__main__.py
Executable file → Normal file
@@ -13,6 +13,7 @@ import base64
|
||||
import locale
|
||||
import os
|
||||
import re
|
||||
import select
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
@@ -41,13 +42,16 @@ from .util import (
|
||||
DEF_EXP,
|
||||
DEF_MTE,
|
||||
DEF_MTH,
|
||||
HAVE_IPV6,
|
||||
IMPLICATIONS,
|
||||
JINJA_VER,
|
||||
MIMES,
|
||||
PARTFTPY_VER,
|
||||
PY_DESC,
|
||||
PYFTPD_VER,
|
||||
SQLITE_VER,
|
||||
UNPLICATIONS,
|
||||
Daemon,
|
||||
align_tab,
|
||||
ansi_re,
|
||||
dedent,
|
||||
@@ -157,7 +161,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
|
||||
|
||||
@@ -170,8 +175,10 @@ def init_E(EE: EnvParams) -> None:
|
||||
(os.environ.get, "TMP"),
|
||||
(unicode, "/tmp"),
|
||||
]
|
||||
errs = []
|
||||
for chk in [os.listdir, os.mkdir]:
|
||||
for pf, pa in paths:
|
||||
for npath, (pf, pa) in enumerate(paths):
|
||||
p = ""
|
||||
try:
|
||||
p = pf(pa)
|
||||
# print(chk.__name__, p, pa)
|
||||
@@ -184,40 +191,26 @@ def init_E(EE: EnvParams) -> None:
|
||||
if not os.path.isdir(p):
|
||||
os.mkdir(p)
|
||||
|
||||
if npath > 1:
|
||||
t = "Using [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/"
|
||||
errs.append(t % (p,))
|
||||
elif errs:
|
||||
errs.append("Using [%s] instead" % (p,))
|
||||
|
||||
if errs:
|
||||
print("WARNING: " + ". ".join(errs))
|
||||
|
||||
return p # type: ignore
|
||||
except:
|
||||
pass
|
||||
except Exception as ex:
|
||||
if p and npath < 2:
|
||||
t = "Unable to store config in [%s] due to %r"
|
||||
errs.append(t % (p, ex))
|
||||
|
||||
raise Exception("could not find a writable path for config")
|
||||
|
||||
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 "."
|
||||
@@ -274,6 +267,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:
|
||||
@@ -288,6 +294,9 @@ def get_ah_salt() -> str:
|
||||
|
||||
|
||||
def ensure_locale() -> None:
|
||||
if ANYWIN and PY2:
|
||||
return # maybe XP, so busted 65001
|
||||
|
||||
safe = "en_US.UTF-8"
|
||||
for x in [
|
||||
safe,
|
||||
@@ -481,6 +490,22 @@ def disable_quickedit() -> None:
|
||||
cmode(True, mode | 4)
|
||||
|
||||
|
||||
def sfx_tpoke(top: str):
|
||||
files = [top] + [
|
||||
os.path.join(dp, p) for dp, dd, df in os.walk(top) for p in dd + df
|
||||
]
|
||||
while True:
|
||||
t = int(time.time())
|
||||
for f in list(files):
|
||||
try:
|
||||
os.utime(f, (t, t))
|
||||
except Exception as ex:
|
||||
lprint("<TPOKE> [%s] %r" % (f, ex))
|
||||
files.remove(f)
|
||||
|
||||
time.sleep(78123)
|
||||
|
||||
|
||||
def showlic() -> None:
|
||||
p = os.path.join(E.mod, "res", "COPYING.txt")
|
||||
if not os.path.exists(p):
|
||||
@@ -615,12 +640,12 @@ def get_sects():
|
||||
\033[36mxban\033[35m executes CMD if someone gets banned
|
||||
\033[0m
|
||||
can be defined as --args or volflags; for example \033[36m
|
||||
--xau notify-send
|
||||
-v .::r:c,xau=notify-send
|
||||
--xau foo.py
|
||||
-v .::r:c,xau=bar.py
|
||||
\033[0m
|
||||
commands specified as --args are appended to volflags;
|
||||
each --arg and volflag can be specified multiple times,
|
||||
each command will execute in order unless one returns non-zero
|
||||
hooks specified as commandline --args are appended to volflags;
|
||||
each commandline --arg and volflag can be specified multiple times,
|
||||
each hook will execute in order unless one returns non-zero
|
||||
|
||||
optionally prefix the command with comma-sep. flags similar to -mtp:
|
||||
|
||||
@@ -631,6 +656,10 @@ def get_sects():
|
||||
\033[36mtN\033[35m sets an N sec timeout before the command is abandoned
|
||||
\033[36miN\033[35m xiu only: volume must be idle for N sec (default = 5)
|
||||
|
||||
\033[36mar\033[35m only run hook if user has read-access
|
||||
\033[36marw\033[35m only run hook if user has read-write-access
|
||||
\033[36marwmd\033[35m ...and so on... (doesn't work for xiu or xban)
|
||||
|
||||
\033[36mkt\033[35m kills the entire process tree on timeout (default),
|
||||
\033[36mkm\033[35m kills just the main process
|
||||
\033[36mkn\033[35m lets it continue running until copyparty is terminated
|
||||
@@ -640,6 +669,21 @@ def get_sects():
|
||||
\033[36mc2\033[35m show only stdout
|
||||
\033[36mc3\033[35m mute all process otput
|
||||
\033[0m
|
||||
examples:
|
||||
|
||||
\033[36m--xm some.py\033[35m runs \033[33msome.py msgtxt\033[35m on each 📟 message;
|
||||
\033[33mmsgtxt\033[35m is the message that was written into the web-ui
|
||||
|
||||
\033[36m--xm j,some.py\033[35m runs \033[33msome.py jsontext\033[35m on each 📟 message;
|
||||
\033[33mjsontext\033[35m is the message info (ip, user, ..., msg-text)
|
||||
|
||||
\033[36m--xm aw,j,some.py\033[35m requires user to have write-access
|
||||
|
||||
\033[36m--xm aw,,notify-send,hey,--\033[35m shows an OS alert on linux;
|
||||
the \033[33m,,\033[35m stops copyparty from reading the rest as flags and
|
||||
the \033[33m--\033[35m stops notify-send from reading the message as args
|
||||
and the alert will be "hey" followed by the messagetext
|
||||
\033[0m
|
||||
each hook is executed once for each event, except for \033[36mxiu\033[0m
|
||||
which builds up a backlog of uploads, running the hook just once
|
||||
as soon as the volume has been idle for iN seconds (5 by default)
|
||||
@@ -666,7 +710,10 @@ def get_sects():
|
||||
\033[36mstash\033[35m dumps the data to file and returns length + checksum
|
||||
\033[36msave,get\033[35m dumps to file and returns the page like a GET
|
||||
\033[36mprint,get\033[35m prints the data in the log and returns GET
|
||||
(leave out the ",get" to return an error instead)
|
||||
(leave out the ",get" to return an error instead)\033[0m
|
||||
|
||||
note that the \033[35m--xm\033[0m hook will only run if \033[35m--urlform\033[0m
|
||||
is either \033[36mprint\033[0m or the default \033[36mprint,get\033[0m
|
||||
"""
|
||||
),
|
||||
],
|
||||
@@ -831,7 +878,7 @@ def build_flags_desc():
|
||||
v = v.replace("\n", "\n ")
|
||||
ret += "\n \033[36m{}\033[35m {}".format(k, v)
|
||||
|
||||
return ret + "\033[0m"
|
||||
return ret
|
||||
|
||||
|
||||
# fmt: off
|
||||
@@ -849,6 +896,8 @@ def add_general(ap, nc, srvname):
|
||||
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-]")
|
||||
ap2.add_argument("--name", metavar="TXT", type=u, default=srvname, help="server name (displayed topleft in browser and in mDNS)")
|
||||
ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]")
|
||||
ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit")
|
||||
ap2.add_argument("--license", action="store_true", help="show licenses and exit")
|
||||
ap2.add_argument("--version", action="store_true", help="show versions and exit")
|
||||
|
||||
@@ -867,9 +916,11 @@ 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)")
|
||||
ap2.add_argument("--mtab-age", metavar="SEC", type=int, default=60, help="rebuild mountpoint cache every \033[33mSEC\033[0m to keep track of sparse-files support; keep low on servers with removable media")
|
||||
|
||||
|
||||
def add_upload(ap):
|
||||
@@ -888,15 +939,16 @@ def add_upload(ap):
|
||||
ap2.add_argument("--no-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)")
|
||||
ap2.add_argument("--no-snap", action="store_true", help="disable snapshots -- forget unfinished uploads on shutdown; don't create .hist/up2k.snap files -- abandoned/interrupted uploads must be cleaned up manually")
|
||||
ap2.add_argument("--snap-wri", metavar="SEC", type=int, default=300, help="write upload state to ./hist/up2k.snap every \033[33mSEC\033[0m seconds; allows resuming incomplete uploads after a server crash")
|
||||
ap2.add_argument("--snap-drop", metavar="MIN", type=float, default=1440, help="forget unfinished uploads after \033[33mMIN\033[0m minutes; impossible to resume them after that (360=6h, 1440=24h)")
|
||||
ap2.add_argument("--snap-drop", metavar="MIN", type=float, default=1440.0, help="forget unfinished uploads after \033[33mMIN\033[0m minutes; impossible to resume them after that (360=6h, 1440=24h)")
|
||||
ap2.add_argument("--u2ts", metavar="TXT", type=u, default="c", help="how to timestamp uploaded files; [\033[32mc\033[0m]=client-last-modified, [\033[32mu\033[0m]=upload-time, [\033[32mfc\033[0m]=force-c, [\033[32mfu\033[0m]=force-u (volflag=u2ts)")
|
||||
ap2.add_argument("--rand", action="store_true", help="force randomized filenames, \033[33m--nrand\033[0m chars long (volflag=rand)")
|
||||
ap2.add_argument("--nrand", metavar="NUM", type=int, default=9, help="randomized filenames length (volflag=nrand)")
|
||||
ap2.add_argument("--magic", action="store_true", help="enable filetype detection on nameless uploads (volflag=magic)")
|
||||
ap2.add_argument("--df", metavar="GiB", type=float, default=0, help="ensure \033[33mGiB\033[0m free disk space by rejecting upload requests")
|
||||
ap2.add_argument("--df", metavar="GiB", type=u, default="0", help="ensure \033[33mGiB\033[0m free disk space by rejecting upload requests; assumes gigabytes unless a unit suffix is given: [\033[32m256m\033[0m], [\033[32m4\033[0m], [\033[32m2T\033[0m] (volflag=df)")
|
||||
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
|
||||
ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; [\033[32m-1\033[0m] = forbidden/always-off, [\033[32m0\033[0m] = default-off and warn if enabled, [\033[32m1\033[0m] = default-off, [\033[32m2\033[0m] = on, [\033[32m3\033[0m] = on and disable datecheck")
|
||||
ap2.add_argument("--u2j", metavar="JOBS", type=int, default=2, help="web-client: number of file chunks to upload in parallel; 1 or 2 is good for low-latency (same-country) connections, 4-8 for android clients, 16 for cross-atlantic (max=64)")
|
||||
ap2.add_argument("--u2sz", metavar="N,N,N", type=u, default="1,64,96", help="web-client: default upload chunksize (MiB); sets \033[33mmin,default,max\033[0m in the settings gui. Each HTTP POST will aim for this size. Cloudflare max is 96. Big values are good for cross-atlantic but may increase HDD fragmentation on some FS. Disable this optimization with [\033[32m1,1,1\033[0m]")
|
||||
ap2.add_argument("--u2sort", metavar="TXT", type=u, default="s", help="upload order; [\033[32ms\033[0m]=smallest-first, [\033[32mn\033[0m]=alphabetical, [\033[32mfs\033[0m]=force-s, [\033[32mfn\033[0m]=force-n -- alphabetical is a bit slower on fiber/LAN but makes it easier to eyeball if everything went fine")
|
||||
ap2.add_argument("--write-uplog", action="store_true", help="write POST reports to textfiles in working-directory")
|
||||
|
||||
@@ -916,12 +968,12 @@ def add_network(ap):
|
||||
else:
|
||||
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-tbody", metavar="SEC", type=float, default=186.0, 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")
|
||||
ap2.add_argument("--rsp-jtr", metavar="SEC", type=float, default=0, help="debug: response delay, random duration 0..\033[33mSEC\033[0m")
|
||||
ap2.add_argument("--s-wr-slp", metavar="SEC", type=float, default=0.0, help="debug: socket write delay in seconds")
|
||||
ap2.add_argument("--rsp-slp", metavar="SEC", type=float, default=0.0, help="debug: response delay in seconds")
|
||||
ap2.add_argument("--rsp-jtr", metavar="SEC", type=float, default=0.0, help="debug: response delay, random duration 0..\033[33mSEC\033[0m")
|
||||
|
||||
|
||||
def add_tls(ap, cert_path):
|
||||
@@ -929,10 +981,10 @@ def add_tls(ap, cert_path):
|
||||
ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls -- force plaintext")
|
||||
ap2.add_argument("--https-only", action="store_true", help="disable plaintext -- force tls")
|
||||
ap2.add_argument("--cert", metavar="PATH", type=u, default=cert_path, help="path to TLS certificate")
|
||||
ap2.add_argument("--ssl-ver", metavar="LIST", type=u, help="set allowed ssl/tls versions; [\033[32mhelp\033[0m] shows available versions; default is what your python version considers safe")
|
||||
ap2.add_argument("--ciphers", metavar="LIST", type=u, help="set allowed ssl/tls ciphers; [\033[32mhelp\033[0m] shows available ciphers")
|
||||
ap2.add_argument("--ssl-ver", metavar="LIST", type=u, default="", help="set allowed ssl/tls versions; [\033[32mhelp\033[0m] shows available versions; default is what your python version considers safe")
|
||||
ap2.add_argument("--ciphers", metavar="LIST", type=u, default="", help="set allowed ssl/tls ciphers; [\033[32mhelp\033[0m] shows available ciphers")
|
||||
ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info")
|
||||
ap2.add_argument("--ssl-log", metavar="PATH", type=u, help="log master secrets for later decryption in wireshark")
|
||||
ap2.add_argument("--ssl-log", metavar="PATH", type=u, default="", help="log master secrets for later decryption in wireshark")
|
||||
|
||||
|
||||
def add_cert(ap, cert_path):
|
||||
@@ -945,12 +997,12 @@ def add_cert(ap, cert_path):
|
||||
ap2.add_argument("--crt-nolo", action="store_true", help="do not add 127.0.0.1 / localhost into cert")
|
||||
ap2.add_argument("--crt-nohn", action="store_true", help="do not add mDNS names / hostname into cert")
|
||||
ap2.add_argument("--crt-dir", metavar="PATH", default=cert_dir, help="where to save the CA cert")
|
||||
ap2.add_argument("--crt-cdays", metavar="D", type=float, default=3650, help="ca-certificate expiration time in days")
|
||||
ap2.add_argument("--crt-sdays", metavar="D", type=float, default=365, help="server-cert expiration time in days")
|
||||
ap2.add_argument("--crt-cdays", metavar="D", type=float, default=3650.0, help="ca-certificate expiration time in days")
|
||||
ap2.add_argument("--crt-sdays", metavar="D", type=float, default=365.0, help="server-cert expiration time in days")
|
||||
ap2.add_argument("--crt-cn", metavar="TXT", type=u, default="partyco", help="CA/server-cert common-name")
|
||||
ap2.add_argument("--crt-cnc", metavar="TXT", type=u, default="--crt-cn", help="override CA name")
|
||||
ap2.add_argument("--crt-cns", metavar="TXT", type=u, default="--crt-cn cpp", help="override server-cert name")
|
||||
ap2.add_argument("--crt-back", metavar="HRS", type=float, default=72, help="backdate in hours")
|
||||
ap2.add_argument("--crt-back", metavar="HRS", type=float, default=72.0, help="backdate in hours")
|
||||
ap2.add_argument("--crt-alg", metavar="S-N", type=u, default="ecdsa-256", help="algorithm and keysize; one of these: \033[32mecdsa-256 rsa-4096 rsa-2048\033[0m")
|
||||
|
||||
|
||||
@@ -960,6 +1012,8 @@ def add_auth(ap):
|
||||
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):
|
||||
@@ -989,7 +1043,7 @@ def add_zc_mdns(ap):
|
||||
ap2.add_argument("--zm-mnic", action="store_true", help="merge NICs which share subnets; assume that same subnet means same network")
|
||||
ap2.add_argument("--zm-msub", action="store_true", help="merge subnets on each NIC -- always enabled for ipv6 -- reduces network load, but gnome-gvfs clients may stop working, and clients cannot be in subnets that the server is not")
|
||||
ap2.add_argument("--zm-noneg", action="store_true", help="disable NSEC replies -- try this if some clients don't see copyparty")
|
||||
ap2.add_argument("--zm-spam", metavar="SEC", type=float, default=0, help="send unsolicited announce every \033[33mSEC\033[0m; useful if clients have IPs in a subnet which doesn't overlap with the server, or to avoid some firewall issues")
|
||||
ap2.add_argument("--zm-spam", metavar="SEC", type=float, default=0.0, help="send unsolicited announce every \033[33mSEC\033[0m; useful if clients have IPs in a subnet which doesn't overlap with the server, or to avoid some firewall issues")
|
||||
|
||||
|
||||
def add_zc_ssdp(ap):
|
||||
@@ -1004,14 +1058,15 @@ def add_zc_ssdp(ap):
|
||||
|
||||
def add_ftp(ap):
|
||||
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("--ftp", metavar="PORT", type=int, default=0, help="enable FTP server on \033[33mPORT\033[0m, for example \033[32m3921")
|
||||
ap2.add_argument("--ftps", metavar="PORT", type=int, default=0, 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="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-no-ow", action="store_true", help="if target file exists, reject upload instead of overwrite")
|
||||
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")
|
||||
ap2.add_argument("--ftp-nat", metavar="ADDR", type=u, default="", help="the NAT address to use for passive connections")
|
||||
ap2.add_argument("--ftp-pr", metavar="P-P", type=u, default="", help="the range of TCP ports to use for passive connections, for example \033[32m12000-13000")
|
||||
|
||||
|
||||
def add_webdav(ap):
|
||||
@@ -1025,14 +1080,15 @@ def add_webdav(ap):
|
||||
|
||||
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("--tftp", metavar="PORT", type=int, default=0, help="enable TFTP server on \033[33mPORT\033[0m, for example \033[32m69 \033[0mor \033[32m3969")
|
||||
ap2.add_argument("--tftp4", action="store_true", help="only listen on IPv4")
|
||||
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")
|
||||
ap2.add_argument("--tftp-pr", metavar="P-P", type=u, default="", help="the range of UDP ports to use for data transfer, for example \033[32m12000-13000")
|
||||
|
||||
|
||||
def add_smb(ap):
|
||||
@@ -1099,6 +1155,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):
|
||||
@@ -1106,7 +1164,7 @@ def add_safety(ap):
|
||||
ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js")
|
||||
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --vague-403 -nih")
|
||||
ap2.add_argument("-sss", action="store_true", help="further increase safety: Enable logging to disk, scan for dangerous symlinks.\n └─Alias of\033[32m -ss --no-dav --no-logues --no-readme -lo=cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz --ls=**,*,ln,p,r")
|
||||
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m (see \033[33m--help-ls\033[0m); example [\033[32m**,*,ln,p,r\033[0m]")
|
||||
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, default="", help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m (see \033[33m--help-ls\033[0m); example [\033[32m**,*,ln,p,r\033[0m]")
|
||||
ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)")
|
||||
ap2.add_argument("--xdev", action="store_true", help="stay within the filesystem of the volume root; do not descend into other devices (symlink or bind-mount to another HDD, ...) (volflag=xdev)")
|
||||
ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles")
|
||||
@@ -1116,7 +1174,7 @@ def add_safety(ap):
|
||||
ap2.add_argument("--vague-403", action="store_true", help="send 404 instead of 403 (security through ambiguity, very enterprise)")
|
||||
ap2.add_argument("--force-js", action="store_true", help="don't send folder listings as HTML, force clients to use the embedded json instead -- slight protection against misbehaving search engines which ignore \033[33m--no-robots\033[0m")
|
||||
ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)")
|
||||
ap2.add_argument("--logout", metavar="H", type=float, default="8086", help="logout clients after \033[33mH\033[0m hours of inactivity; [\033[32m0.0028\033[0m]=10sec, [\033[32m0.1\033[0m]=6min, [\033[32m24\033[0m]=day, [\033[32m168\033[0m]=week, [\033[32m720\033[0m]=month, [\033[32m8760\033[0m]=year)")
|
||||
ap2.add_argument("--logout", metavar="H", type=float, default=8086.0, help="logout clients after \033[33mH\033[0m hours of inactivity; [\033[32m0.0028\033[0m]=10sec, [\033[32m0.1\033[0m]=6min, [\033[32m24\033[0m]=day, [\033[32m168\033[0m]=week, [\033[32m720\033[0m]=month, [\033[32m8760\033[0m]=year)")
|
||||
ap2.add_argument("--ban-pw", metavar="N,W,B", type=u, default="9,60,1440", help="more than \033[33mN\033[0m wrong passwords in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]")
|
||||
ap2.add_argument("--ban-404", metavar="N,W,B", type=u, default="50,60,1440", help="hitting more than \033[33mN\033[0m 404's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; only affects users who cannot see directory listings because their access is either g/G/h")
|
||||
ap2.add_argument("--ban-403", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 403's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month")
|
||||
@@ -1131,13 +1189,14 @@ def add_safety(ap):
|
||||
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)")
|
||||
|
||||
|
||||
@@ -1151,7 +1210,7 @@ def add_shutdown(ap):
|
||||
def add_logging(ap):
|
||||
ap2 = ap.add_argument_group('logging options')
|
||||
ap2.add_argument("-q", action="store_true", help="quiet; disable most STDOUT messages")
|
||||
ap2.add_argument("-lo", metavar="PATH", type=u, help="logfile, example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)")
|
||||
ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile, example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)")
|
||||
ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR")
|
||||
ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR")
|
||||
ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster")
|
||||
@@ -1178,10 +1237,10 @@ def add_thumbnail(ap):
|
||||
ap2.add_argument("--no-athumb", action="store_true", help="disable audio thumbnails (spectrograms) (volflag=dathumb)")
|
||||
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)")
|
||||
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails")
|
||||
ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60, 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-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-convt", metavar="SEC", type=float, default=60.0, help="conversion timeout in seconds (volflag=convt)")
|
||||
ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=6.0, help="max memory usage (GiB) permitted by thumbnailer; not very accurate")
|
||||
ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32my\033[0m]=crop, [\033[32mn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)")
|
||||
ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)")
|
||||
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference")
|
||||
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
|
||||
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
|
||||
@@ -1198,11 +1257,14 @@ def add_thumbnail(ap):
|
||||
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips")
|
||||
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,dds,dib,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,m4a,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,ogg,okt,opus,ra,s3m,tak,tta,ulaw,wav,wma,wv,xm,xpk", help="audio formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
|
||||
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz", help="audio formats to decompress before passing to ffmpeg")
|
||||
|
||||
|
||||
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")
|
||||
@@ -1217,17 +1279,17 @@ def add_db_general(ap, hcores):
|
||||
ap2.add_argument("-e2v", action="store_true", help="verify file integrity; rehash all files and compare with db")
|
||||
ap2.add_argument("-e2vu", action="store_true", help="on hash mismatch: update the database with the new hash")
|
||||
ap2.add_argument("-e2vp", action="store_true", help="on hash mismatch: panic and quit copyparty")
|
||||
ap2.add_argument("--hist", metavar="PATH", type=u, help="where to store volume data (db, thumbs) (volflag=hist)")
|
||||
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("--hist", metavar="PATH", type=u, default="", help="where to store volume data (db, thumbs); default is a folder named \".hist\" inside each volume (volflag=hist)")
|
||||
ap2.add_argument("--no-hash", metavar="PTN", type=u, default="", help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (volflag=nohash)")
|
||||
ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scans (volflag=noidx)")
|
||||
ap2.add_argument("--no-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)")
|
||||
ap2.add_argument("--hash-mt", metavar="CORES", type=int, default=hcores, help="num cpu cores to use for file hashing; set 0 or 1 for single-core hashing")
|
||||
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="rescan filesystem for changes every \033[33mSEC\033[0m seconds; 0=off (volflag=scan)")
|
||||
ap2.add_argument("--db-act", metavar="SEC", type=float, default=10, help="defer any scheduled volume reindexing until \033[33mSEC\033[0m seconds after last db write (uploads, renames, ...)")
|
||||
ap2.add_argument("--db-act", metavar="SEC", type=float, default=10.0, help="defer any scheduled volume reindexing until \033[33mSEC\033[0m seconds after last db write (uploads, renames, ...)")
|
||||
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=45, help="search deadline -- terminate searches running for more than \033[33mSEC\033[0m seconds")
|
||||
ap2.add_argument("--srch-hits", metavar="N", type=int, default=7999, help="max search results to allow clients to fetch; 125 results will be shown initially")
|
||||
ap2.add_argument("--dotsrch", action="store_true", help="show dotfiles in search results (volflags: dotsrch | nodotsrch)")
|
||||
@@ -1259,19 +1321,39 @@ def add_txt(ap):
|
||||
ap2.add_argument("--exp-lg", metavar="V,V,V", type=u, default=DEF_EXP, help="comma/space-separated list of placeholders to expand in prologue/epilogue files (volflag=exp_lg)")
|
||||
|
||||
|
||||
def add_og(ap):
|
||||
ap2 = ap.add_argument_group('og / open graph / discord-embed options')
|
||||
ap2.add_argument("--og", action="store_true", help="disable hotlinking and return an html document instead; this is required by open-graph, but can also be useful on its own (volflag=og)")
|
||||
ap2.add_argument("--og-ua", metavar="RE", type=u, default="", help="only disable hotlinking / engage OG behavior if the useragent matches regex \033[33mRE\033[0m (volflag=og_ua)")
|
||||
ap2.add_argument("--og-tpl", metavar="PATH", type=u, default="", help="do not return the regular copyparty html, but instead load the jinja2 template at \033[33mPATH\033[0m (if path contains 'EXT' then EXT will be replaced with the requested file's extension) (volflag=og_tpl)")
|
||||
ap2.add_argument("--og-no-head", action="store_true", help="do not automatically add OG entries into <head> (useful if you're doing this yourself in a template or such) (volflag=og_no_head)")
|
||||
ap2.add_argument("--og-th", metavar="FMT", type=u, default="jf3", help="thumbnail format; j=jpeg, jf=jpeg-uncropped, jf3=jpeg-uncropped-large, w=webm, ... (volflag=og_th)")
|
||||
ap2.add_argument("--og-title", metavar="TXT", type=u, default="", help="fallback title if there is nothing in the \033[33m-e2t\033[0m database (volflag=og_title)")
|
||||
ap2.add_argument("--og-title-a", metavar="T", type=u, default="🎵 {{ artist }} - {{ title }}", help="audio title format; takes any metadata key (volflag=og_title_a)")
|
||||
ap2.add_argument("--og-title-v", metavar="T", type=u, default="{{ title }}", help="video title format; takes any metadata key (volflag=og_title_v)")
|
||||
ap2.add_argument("--og-title-i", metavar="T", type=u, default="{{ title }}", help="image title format; takes any metadata key (volflag=og_title_i)")
|
||||
ap2.add_argument("--og-s-title", action="store_true", help="force default title; do not read from tags (volflag=og_s_title)")
|
||||
ap2.add_argument("--og-desc", metavar="TXT", type=u, default="", help="description text; same for all files, disable with [\033[32m-\033[0m] (volflag=og_desc)")
|
||||
ap2.add_argument("--og-site", metavar="TXT", type=u, default="", help="sitename; defaults to \033[33m--name\033[0m, disable with [\033[32m-\033[0m] (volflag=og_site)")
|
||||
ap2.add_argument("--tcolor", metavar="RGB", type=u, default="333", help="accent color (3 or 6 hex digits); may also affect safari and/or android-chrome (volflag=tcolor)")
|
||||
ap2.add_argument("--uqe", action="store_true", help="query-string parceling; translate a request for \033[33m/foo/.uqe/BASE64\033[0m into \033[33m/foo?TEXT\033[0m, or \033[33m/foo/?TEXT\033[0m if the first character in \033[33mTEXT\033[0m is a slash. Automatically enabled for \033[33m--og\033[0m")
|
||||
|
||||
|
||||
def add_ui(ap, retry):
|
||||
ap2 = ap.add_argument_group('ui options')
|
||||
ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
|
||||
ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)")
|
||||
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language; one of the following: \033[32meng nor\033[0m")
|
||||
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..7)")
|
||||
ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed")
|
||||
ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent")
|
||||
ap2.add_argument("--sort", metavar="C,C,C", type=u, default="href", help="default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: \033[32mhref -href ext sz ts tags/Album tags/.tn\033[0m (volflag=sort)")
|
||||
ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files matching \033[33mREGEX\033[0m in file list. Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)")
|
||||
ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable")
|
||||
ap2.add_argument("--mpmc", metavar="URL", type=u, default="", help="change the mediaplayer-toggle mouse cursor; URL to a folder with {2..5}.png inside (or disable with [\033[32m.\033[0m])")
|
||||
ap2.add_argument("--js-browser", metavar="L", type=u, help="URL to additional JS to include")
|
||||
ap2.add_argument("--css-browser", metavar="L", type=u, help="URL to additional CSS to include")
|
||||
ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages")
|
||||
ap2.add_argument("--js-browser", metavar="L", type=u, default="", help="URL to additional JS to include")
|
||||
ap2.add_argument("--css-browser", metavar="L", type=u, default="", help="URL to additional CSS to include")
|
||||
ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages; can be @PATH to send the contents of a file at PATH, and/or begin with %% to render as jinja2 template (volflag=html_head)")
|
||||
ap2.add_argument("--ih", action="store_true", help="if a folder contains index.html, show that instead of the directory listing by default (can be changed in the client settings UI, or add ?v to URL for override)")
|
||||
ap2.add_argument("--textfiles", metavar="CSV", type=u, default="txt,nfo,diz,cue,readme", help="file extensions to present as plaintext")
|
||||
ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)")
|
||||
@@ -1290,14 +1372,16 @@ def add_debug(ap):
|
||||
ap2 = ap.add_argument_group('debug options')
|
||||
ap2.add_argument("--vc", action="store_true", help="verbose config file parser (explain config)")
|
||||
ap2.add_argument("--cgen", action="store_true", help="generate config file from current config (best-effort; probably buggy)")
|
||||
if hasattr(select, "poll"):
|
||||
ap2.add_argument("--no-poll", action="store_true", help="kernel-bug workaround: disable poll; use select instead (limits max num clients to ~700)")
|
||||
ap2.add_argument("--no-sendfile", action="store_true", help="kernel-bug workaround: disable sendfile; do a safe and slow read-send-loop instead")
|
||||
ap2.add_argument("--no-scandir", action="store_true", help="kernel-bug workaround: disable scandir; do a listdir + stat on each file instead")
|
||||
ap2.add_argument("--no-fastboot", action="store_true", help="wait for initial filesystem indexing before accepting client requests")
|
||||
ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead")
|
||||
ap2.add_argument("--srch-dbg", action="store_true", help="explain search processing, and do some extra expensive sanity checks")
|
||||
ap2.add_argument("--rclone-mdns", action="store_true", help="use mdns-domain instead of server-ip on /?hc")
|
||||
ap2.add_argument("--stackmon", metavar="P,S", type=u, help="write stacktrace to \033[33mP\033[0math every \033[33mS\033[0m second, for example --stackmon=\033[32m./st/%%Y-%%m/%%d/%%H%%M.xz,60")
|
||||
ap2.add_argument("--log-thrs", metavar="SEC", type=float, help="list active threads every \033[33mSEC\033[0m")
|
||||
ap2.add_argument("--stackmon", metavar="P,S", type=u, default="", help="write stacktrace to \033[33mP\033[0math every \033[33mS\033[0m second, for example --stackmon=\033[32m./st/%%Y-%%m/%%d/%%H%%M.xz,60")
|
||||
ap2.add_argument("--log-thrs", metavar="SEC", type=float, default=0.0, help="list active threads every \033[33mSEC\033[0m")
|
||||
ap2.add_argument("--log-fk", metavar="REGEX", type=u, default="", help="log filekey params for files where path matches \033[33mREGEX\033[0m; [\033[32m.\033[0m] (a single dot) = all files")
|
||||
ap2.add_argument("--bak-flips", action="store_true", help="[up2k] if a client uploads a bitflipped/corrupted chunk, store a copy according to \033[33m--bf-nc\033[0m and \033[33m--bf-dir\033[0m")
|
||||
ap2.add_argument("--bf-nc", metavar="NUM", type=int, default=200, help="bak-flips: stop if there's more than \033[33mNUM\033[0m files at \033[33m--kf-dir\033[0m already; default: 6.3 GiB max (200*32M)")
|
||||
@@ -1319,6 +1403,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,
|
||||
@@ -1350,7 +1435,7 @@ def run_argparse(
|
||||
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)
|
||||
@@ -1358,6 +1443,7 @@ def run_argparse(
|
||||
add_hooks(ap)
|
||||
add_stats(ap)
|
||||
add_txt(ap)
|
||||
add_og(ap)
|
||||
add_ui(ap, retry)
|
||||
add_admin(ap)
|
||||
add_logging(ap)
|
||||
@@ -1386,18 +1472,22 @@ def run_argparse(
|
||||
k2 = "help_" + k.replace("-", "_")
|
||||
if vars(ret)[k2]:
|
||||
lprint("# %s help page (%s)" % (k, h))
|
||||
lprint(t + "\033[0m")
|
||||
lprint(t.rstrip() + "\033[0m")
|
||||
sys.exit(0)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def main(argv: Optional[list[str]] = None) -> None:
|
||||
def main(argv: Optional[list[str]] = None, rsrc: Optional[str] = None) -> None:
|
||||
time.strptime("19970815", "%Y%m%d") # python#7980
|
||||
if WINDOWS:
|
||||
os.system("rem") # enables colors
|
||||
|
||||
init_E(E)
|
||||
|
||||
if rsrc: # pyz
|
||||
E.mod = rsrc
|
||||
|
||||
if argv is None:
|
||||
argv = sys.argv
|
||||
|
||||
@@ -1421,9 +1511,19 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
showlic()
|
||||
sys.exit(0)
|
||||
|
||||
if "--mimes" in argv:
|
||||
print("\n".join("%8s %s" % (k, v) for k, v in sorted(MIMES.items())))
|
||||
sys.exit(0)
|
||||
|
||||
if EXE:
|
||||
print("pybin: {}\n".format(pybin), end="")
|
||||
|
||||
for n, zs in enumerate(argv):
|
||||
if zs.startswith("--sfx-tpoke="):
|
||||
Daemon(sfx_tpoke, "sfx-tpoke", (zs.split("=", 1)[1],))
|
||||
argv.pop(n)
|
||||
break
|
||||
|
||||
ensure_locale()
|
||||
|
||||
ensure_webdeps()
|
||||
@@ -1484,7 +1584,7 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
if hard > 0: # -1 == infinite
|
||||
nc = min(nc, int(hard / 4))
|
||||
except:
|
||||
nc = 512
|
||||
nc = 486 # mdns/ssdp restart headroom; select() maxfd is 512 on windows
|
||||
|
||||
retry = False
|
||||
for fmtr in [RiceFormatter, RiceFormatter, Dodge11874, BasicDodge11874]:
|
||||
@@ -1529,6 +1629,9 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
if getattr(al, k1):
|
||||
setattr(al, k2, False)
|
||||
|
||||
if not HAVE_IPV6 and al.i == "::":
|
||||
al.i = "0.0.0.0"
|
||||
|
||||
al.i = al.i.split(",")
|
||||
try:
|
||||
if "-" in al.p:
|
||||
@@ -1577,6 +1680,9 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
if not hasattr(os, "sendfile"):
|
||||
al.no_sendfile = True
|
||||
|
||||
if not hasattr(select, "poll"):
|
||||
al.no_poll = True
|
||||
|
||||
# signal.signal(signal.SIGINT, sighandler)
|
||||
|
||||
SvcHub(al, dal, argv, "".join(printed)).run()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (1, 11, 2)
|
||||
CODENAME = "You Can (Not) Proceed"
|
||||
BUILD_DT = (2024, 3, 23)
|
||||
VERSION = (1, 13, 6)
|
||||
CODENAME = "race the beam"
|
||||
BUILD_DT = (2024, 7, 29)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
@@ -17,7 +17,11 @@ from .bos import bos
|
||||
from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap
|
||||
from .pwhash import PWHash
|
||||
from .util import (
|
||||
DEF_MTE,
|
||||
DEF_MTH,
|
||||
EXTS,
|
||||
IMPLICATIONS,
|
||||
MIMES,
|
||||
SQLITE_VER,
|
||||
UNPLICATIONS,
|
||||
UTC,
|
||||
@@ -473,6 +477,13 @@ class VFS(object):
|
||||
)
|
||||
# skip uhtml because it's rarely needed
|
||||
|
||||
def get_perms(self, vpath: str, uname: str) -> str:
|
||||
zbl = self.can_access(vpath, uname)
|
||||
ret = "".join(ch for ch, ok in zip("rwmdgGa.", zbl) if ok)
|
||||
if "rwmd" in ret and "a." in ret:
|
||||
ret += "A"
|
||||
return ret
|
||||
|
||||
def get(
|
||||
self,
|
||||
vpath: str,
|
||||
@@ -555,7 +566,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]
|
||||
@@ -1437,6 +1453,7 @@ class AuthSrv(object):
|
||||
elif "" not in mount:
|
||||
# there's volumes but no root; make root inaccessible
|
||||
vfs = VFS(self.log_func, "", "", AXS(), {})
|
||||
vfs.flags["tcolor"] = self.args.tcolor
|
||||
vfs.flags["d2d"] = True
|
||||
|
||||
maxdepth = 0
|
||||
@@ -1609,11 +1626,14 @@ class AuthSrv(object):
|
||||
use = True
|
||||
lim.nosub = True
|
||||
|
||||
zs = vol.flags.get("df") or (
|
||||
"{}g".format(self.args.df) if self.args.df else ""
|
||||
)
|
||||
if zs:
|
||||
zs = vol.flags.get("df") or self.args.df or ""
|
||||
if zs not in ("", "0"):
|
||||
use = True
|
||||
try:
|
||||
_ = float(zs)
|
||||
zs = "%sg" % (zs)
|
||||
except:
|
||||
pass
|
||||
lim.dfl = unhumanize(zs)
|
||||
|
||||
zs = vol.flags.get("sz")
|
||||
@@ -1681,6 +1701,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))
|
||||
|
||||
@@ -1708,7 +1742,11 @@ class AuthSrv(object):
|
||||
if self.args.e2d or "e2ds" in vol.flags:
|
||||
vol.flags["e2d"] = True
|
||||
|
||||
for ga, vf in [["no_hash", "nohash"], ["no_idx", "noidx"]]:
|
||||
for ga, vf in [
|
||||
["no_hash", "nohash"],
|
||||
["no_idx", "noidx"],
|
||||
["og_ua", "og_ua"],
|
||||
]:
|
||||
if vf in vol.flags:
|
||||
ptn = re.compile(vol.flags.pop(vf))
|
||||
else:
|
||||
@@ -1745,13 +1783,21 @@ 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")))
|
||||
|
||||
if vol.flags.get("og"):
|
||||
self.args.uqe = True
|
||||
|
||||
zs = str(vol.flags.get("tcolor", "")).lstrip("#")
|
||||
if len(zs) == 3: # fc5 => ffcc55
|
||||
vol.flags["tcolor"] = "".join([x * 2 for x in zs])
|
||||
|
||||
for k1, k2 in IMPLICATIONS:
|
||||
if k1 in vol.flags:
|
||||
@@ -2033,6 +2079,13 @@ class AuthSrv(object):
|
||||
|
||||
self.re_pwd = re.compile(zs)
|
||||
|
||||
# to ensure it propagates into tcpsrv with mp on
|
||||
if self.args.mime:
|
||||
for zs in self.args.mime:
|
||||
ext, mime = zs.split("=", 1)
|
||||
MIMES[ext] = mime
|
||||
EXTS.update({v: k for k, v in MIMES.items()})
|
||||
|
||||
def setup_pwhash(self, acct: dict[str, str]) -> None:
|
||||
self.ah = PWHash(self.args)
|
||||
if not self.ah.on:
|
||||
@@ -2223,10 +2276,11 @@ class AuthSrv(object):
|
||||
"",
|
||||
]
|
||||
|
||||
csv = set("i p".split())
|
||||
csv = set("i p th_covers zm_on zm_off zs_on zs_off".split())
|
||||
zs = "c ihead mtm mtp on403 on404 xad xar xau xiu xban xbd xbr xbu xm"
|
||||
lst = set(zs.split())
|
||||
askip = set("a v c vc cgen theme".split())
|
||||
askip = set("a v c vc cgen exp_lg exp_md theme".split())
|
||||
fskip = set("exp_lg exp_md mv_re_r mv_re_t rm_re_r rm_re_t".split())
|
||||
|
||||
# keymap from argv to vflag
|
||||
amap = vf_bmap()
|
||||
@@ -2247,11 +2301,35 @@ class AuthSrv(object):
|
||||
for k, v in args.items():
|
||||
if k in askip:
|
||||
continue
|
||||
|
||||
try:
|
||||
v = v.pattern
|
||||
if k in ("idp_gsep", "tftp_lsf"):
|
||||
v = v[1:-1] # close enough
|
||||
except:
|
||||
pass
|
||||
|
||||
skip = False
|
||||
for k2, defstr in (("mte", DEF_MTE), ("mth", DEF_MTH)):
|
||||
if k != k2:
|
||||
continue
|
||||
s1 = list(sorted(list(v)))
|
||||
s2 = list(sorted(defstr.split(",")))
|
||||
if s1 == s2:
|
||||
skip = True
|
||||
break
|
||||
v = ",".join(s1)
|
||||
|
||||
if skip:
|
||||
continue
|
||||
|
||||
if k in csv:
|
||||
v = ", ".join([str(za) for za in v])
|
||||
try:
|
||||
v2 = getattr(self.dargs, k)
|
||||
if v == v2:
|
||||
if k == "tcolor" and len(v2) == 3:
|
||||
v2 = "".join([x * 2 for x in v2])
|
||||
if v == v2 or v.replace(", ", ",") == v2:
|
||||
continue
|
||||
except:
|
||||
continue
|
||||
@@ -2310,6 +2388,7 @@ class AuthSrv(object):
|
||||
pstr += pchar
|
||||
if "g" in pstr and "G" in pstr:
|
||||
pstr = pstr.replace("g", "")
|
||||
pstr = pstr.replace("rwmd.a", "A")
|
||||
try:
|
||||
vperms[pstr].append(uname)
|
||||
except:
|
||||
@@ -2319,24 +2398,48 @@ class AuthSrv(object):
|
||||
trues = []
|
||||
vals = []
|
||||
for k, v in sorted(vol.flags.items()):
|
||||
if k in fskip:
|
||||
continue
|
||||
|
||||
try:
|
||||
v = v.pattern
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
ak = vmap[k]
|
||||
if getattr(self.args, ak) is v:
|
||||
v2 = getattr(self.args, ak)
|
||||
|
||||
try:
|
||||
v2 = v2.pattern
|
||||
except:
|
||||
pass
|
||||
|
||||
if v2 is v:
|
||||
continue
|
||||
except:
|
||||
pass
|
||||
|
||||
skip = False
|
||||
for k2, defstr in (("mte", DEF_MTE), ("mth", DEF_MTH)):
|
||||
if k != k2:
|
||||
continue
|
||||
s1 = list(sorted(list(v)))
|
||||
s2 = list(sorted(defstr.split(",")))
|
||||
if s1 == s2:
|
||||
skip = True
|
||||
break
|
||||
v = ",".join(s1)
|
||||
|
||||
if skip:
|
||||
continue
|
||||
|
||||
if k in lst:
|
||||
for ve in v:
|
||||
vals.append("{}: {}".format(k, ve))
|
||||
elif v is True:
|
||||
trues.append(k)
|
||||
elif v is not False:
|
||||
try:
|
||||
v = v.pattern
|
||||
except:
|
||||
pass
|
||||
|
||||
vals.append("{}: {}".format(k, v))
|
||||
pops = []
|
||||
for k1, k2 in IMPLICATIONS:
|
||||
@@ -2390,7 +2493,7 @@ def expand_config_file(
|
||||
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 += ".conf files; the following files/subfolders were ignored: %s"
|
||||
t = t % (fp, ", ".join(names[:8]))
|
||||
else:
|
||||
t += "files at all"
|
||||
|
||||
@@ -57,11 +57,8 @@ class BrokerMp(object):
|
||||
def shutdown(self) -> None:
|
||||
self.log("broker", "shutting down")
|
||||
for n, proc in enumerate(self.procs):
|
||||
thr = threading.Thread(
|
||||
target=proc.q_pend.put((0, "shutdown", [])),
|
||||
name="mp-shutdown-{}-{}".format(n, len(self.procs)),
|
||||
)
|
||||
thr.start()
|
||||
name = "mp-shut-%d-%d" % (n, len(self.procs))
|
||||
Daemon(proc.q_pend.put, name, ((0, "shutdown", []),))
|
||||
|
||||
with self.mutex:
|
||||
procs = self.procs
|
||||
|
||||
@@ -28,7 +28,7 @@ class ExceptionalQueue(Queue, object):
|
||||
if rv[1] == "pebkac":
|
||||
raise Pebkac(*rv[2:])
|
||||
else:
|
||||
raise Exception(rv[2])
|
||||
raise rv[2]
|
||||
|
||||
return rv
|
||||
|
||||
@@ -65,8 +65,8 @@ def try_exec(want_retval: Union[bool, int], func: Any, *args: list[Any]) -> Any:
|
||||
|
||||
return ["exception", "pebkac", ex.code, str(ex)]
|
||||
|
||||
except:
|
||||
except Exception as ex:
|
||||
if not want_retval:
|
||||
raise
|
||||
|
||||
return ["exception", "stack", traceback.format_exc()]
|
||||
return ["exception", "stack", ex]
|
||||
|
||||
@@ -6,12 +6,19 @@ 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
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from .util import RootLogger
|
||||
from .util import NamedLogger, 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:
|
||||
@@ -76,6 +83,8 @@ def _read_crt(args, fn):
|
||||
|
||||
|
||||
def _gen_ca(log: "RootLogger", args):
|
||||
nlog: "NamedLogger" = lambda msg, c=0: log("cert-gen-ca", msg, c)
|
||||
|
||||
expiry = _read_crt(args, "ca.pem")[0]
|
||||
if time.time() + args.crt_cdays * 60 * 60 * 24 * 0.1 < expiry:
|
||||
return
|
||||
@@ -105,13 +114,19 @@ 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(nlog, bname + ".key", VF)
|
||||
except:
|
||||
pass
|
||||
wrename(nlog, bname + "-key.pem", bname + ".key", VF)
|
||||
wunlink(nlog, bname + ".csr", VF)
|
||||
|
||||
log("cert", "new ca OK", 2)
|
||||
|
||||
|
||||
def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
||||
nlog: "NamedLogger" = lambda msg, c=0: log("cert-gen-srv", msg, c)
|
||||
|
||||
names = args.crt_ns.split(",") if args.crt_ns else []
|
||||
if not args.crt_exact:
|
||||
for n in names[:]:
|
||||
@@ -185,11 +200,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(nlog, bname + ".key", VF)
|
||||
except:
|
||||
pass
|
||||
os.rename(bname + "-key.pem", bname + ".key")
|
||||
os.unlink(bname + ".csr")
|
||||
wrename(nlog, bname + "-key.pem", bname + ".key", VF)
|
||||
wunlink(nlog, bname + ".csr", VF)
|
||||
|
||||
with open(os.path.join(args.crt_dir, "ca.pem"), "rb") as f:
|
||||
ca = f.read()
|
||||
|
||||
@@ -16,6 +16,7 @@ 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",
|
||||
@@ -34,10 +35,14 @@ def vf_bmap() -> dict[str, str]:
|
||||
"e2vp",
|
||||
"exp",
|
||||
"grid",
|
||||
"gsel",
|
||||
"hardlink",
|
||||
"magic",
|
||||
"no_sb_md",
|
||||
"no_sb_lg",
|
||||
"og",
|
||||
"og_no_head",
|
||||
"og_s_title",
|
||||
"rand",
|
||||
"xdev",
|
||||
"xlink",
|
||||
@@ -60,11 +65,23 @@ def vf_vmap() -> dict[str, str]:
|
||||
}
|
||||
for k in (
|
||||
"dbd",
|
||||
"html_head",
|
||||
"lg_sbf",
|
||||
"md_sbf",
|
||||
"nrand",
|
||||
"og_desc",
|
||||
"og_site",
|
||||
"og_th",
|
||||
"og_title",
|
||||
"og_title_a",
|
||||
"og_title_v",
|
||||
"og_title_i",
|
||||
"og_tpl",
|
||||
"og_ua",
|
||||
"mv_retry",
|
||||
"rm_retry",
|
||||
"sort",
|
||||
"tcolor",
|
||||
"unlist",
|
||||
"u2abort",
|
||||
"u2ts",
|
||||
@@ -79,7 +96,6 @@ def vf_cmap() -> dict[str, str]:
|
||||
for k in (
|
||||
"exp_lg",
|
||||
"exp_md",
|
||||
"html_head",
|
||||
"mte",
|
||||
"mth",
|
||||
"mtp",
|
||||
@@ -129,6 +145,7 @@ flagcats = {
|
||||
"maxb=1g,300": "max 1 GiB over 5min (suffixes: b, k, m, g, t)",
|
||||
"vmaxb=1g": "total volume size max 1 GiB (suffixes: b, k, m, g, t)",
|
||||
"vmaxn=4k": "max 4096 files in volume (suffixes: b, k, m, g, t)",
|
||||
"medialinks": "return medialinks for non-up2k uploads (not hotlinks)",
|
||||
"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",
|
||||
@@ -175,6 +192,7 @@ flagcats = {
|
||||
"dvthumb": "disables video thumbnails",
|
||||
"dathumb": "disables audio thumbnails (spectrograms)",
|
||||
"dithumb": "disables image thumbnails",
|
||||
"pngquant": "compress audio waveforms 33% better",
|
||||
"thsize": "thumbnail res; WxH",
|
||||
"crop": "center-cropping (y/n/fy/fn)",
|
||||
"th3x": "3x resolution (y/n/fy/fn)",
|
||||
@@ -197,9 +215,10 @@ flagcats = {
|
||||
},
|
||||
"client and ux": {
|
||||
"grid": "show grid/thumbnails by default",
|
||||
"gsel": "select files in grid by ctrl-click",
|
||||
"sort": "default sort order",
|
||||
"unlist": "dont list files matching REGEX",
|
||||
"html_head=TXT": "includes TXT in the <head>",
|
||||
"html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH",
|
||||
"robots": "allows indexing by search engines (default)",
|
||||
"norobots": "kindly asks search engines to leave",
|
||||
"no_sb_md": "disable js sandbox for markdown files",
|
||||
@@ -214,6 +233,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)",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
@@ -17,20 +18,26 @@ if True: # pylint: disable=using-constant-test
|
||||
|
||||
|
||||
class Fstab(object):
|
||||
def __init__(self, log: "RootLogger"):
|
||||
def __init__(self, log: "RootLogger", args: argparse.Namespace):
|
||||
self.log_func = log
|
||||
|
||||
self.warned = False
|
||||
self.trusted = False
|
||||
self.tab: Optional[VFS] = None
|
||||
self.oldtab: Optional[VFS] = None
|
||||
self.srctab = "a"
|
||||
self.cache: dict[str, str] = {}
|
||||
self.age = 0.0
|
||||
self.maxage = args.mtab_age
|
||||
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.log_func("fstab", msg, c)
|
||||
|
||||
def get(self, path: str) -> str:
|
||||
if len(self.cache) > 9000:
|
||||
self.age = time.time()
|
||||
now = time.time()
|
||||
if now - self.age > self.maxage or len(self.cache) > 9000:
|
||||
self.age = now
|
||||
self.oldtab = self.tab or self.oldtab
|
||||
self.tab = None
|
||||
self.cache = {}
|
||||
|
||||
@@ -75,7 +82,7 @@ class Fstab(object):
|
||||
self.trusted = False
|
||||
|
||||
def build_tab(self) -> None:
|
||||
self.log("building tab")
|
||||
self.log("inspecting mtab for changes")
|
||||
|
||||
sptn = r"^.*? on (.*) type ([^ ]+) \(.*"
|
||||
if MACOS:
|
||||
@@ -84,6 +91,7 @@ class Fstab(object):
|
||||
ptn = re.compile(sptn)
|
||||
so, _ = chkcmd(["mount"])
|
||||
tab1: list[tuple[str, str]] = []
|
||||
atab = []
|
||||
for ln in so.split("\n"):
|
||||
m = ptn.match(ln)
|
||||
if not m:
|
||||
@@ -91,6 +99,15 @@ class Fstab(object):
|
||||
|
||||
zs1, zs2 = m.groups()
|
||||
tab1.append((str(zs1), str(zs2)))
|
||||
atab.append(ln)
|
||||
|
||||
# keep empirically-correct values if mounttab unchanged
|
||||
srctab = "\n".join(sorted(atab))
|
||||
if srctab == self.srctab:
|
||||
self.tab = self.oldtab
|
||||
return
|
||||
|
||||
self.log("mtab has changed; reevaluating support for sparse files")
|
||||
|
||||
tab1.sort(key=lambda x: (len(x[0]), x[0]))
|
||||
path1, fs1 = tab1[0]
|
||||
@@ -99,6 +116,7 @@ class Fstab(object):
|
||||
tab.add(fs, path.lstrip("/"))
|
||||
|
||||
self.tab = tab
|
||||
self.srctab = srctab
|
||||
|
||||
def relabel(self, path: str, nval: str) -> None:
|
||||
assert self.tab
|
||||
@@ -133,7 +151,9 @@ class Fstab(object):
|
||||
self.trusted = True
|
||||
except:
|
||||
# prisonparty or other restrictive environment
|
||||
self.log("failed to build tab:\n{}".format(min_ex()), 3)
|
||||
if not self.warned:
|
||||
self.warned = True
|
||||
self.log("failed to build tab:\n{}".format(min_ex()), 3)
|
||||
self.build_fallback()
|
||||
|
||||
assert self.tab
|
||||
|
||||
@@ -19,6 +19,7 @@ from .__init__ import PY2, TYPE_CHECKING
|
||||
from .authsrv import VFS
|
||||
from .bos import bos
|
||||
from .util import (
|
||||
VF_CAREFUL,
|
||||
Daemon,
|
||||
ODict,
|
||||
Pebkac,
|
||||
@@ -30,6 +31,7 @@ from .util import (
|
||||
runhook,
|
||||
sanitize_fn,
|
||||
vjoin,
|
||||
wunlink,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -37,7 +39,7 @@ if TYPE_CHECKING:
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
import typing
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
|
||||
class FSE(FilesystemError):
|
||||
@@ -139,6 +141,9 @@ class FtpFs(AbstractedFS):
|
||||
self.listdirinfo = self.listdir
|
||||
self.chdir(".")
|
||||
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.hub.log("ftpd", msg, c)
|
||||
|
||||
def v2a(
|
||||
self,
|
||||
vpath: str,
|
||||
@@ -207,17 +212,37 @@ class FtpFs(AbstractedFS):
|
||||
w = "w" in mode or "a" in mode or "+" in mode
|
||||
|
||||
ap = self.rv2a(filename, r, w)[0]
|
||||
self.validpath(ap)
|
||||
if w:
|
||||
try:
|
||||
st = bos.stat(ap)
|
||||
td = time.time() - st.st_mtime
|
||||
need_unlink = True
|
||||
except:
|
||||
need_unlink = False
|
||||
td = 0
|
||||
|
||||
if td < -1 or td > self.args.ftp_wt:
|
||||
raise FSE("Cannot open existing file for writing")
|
||||
if w and need_unlink:
|
||||
if td >= -1 and td <= self.args.ftp_wt:
|
||||
# within permitted timeframe; unlink and accept
|
||||
do_it = True
|
||||
elif self.args.no_del or self.args.ftp_no_ow:
|
||||
# file too old, or overwrite not allowed; reject
|
||||
do_it = False
|
||||
else:
|
||||
# allow overwrite if user has delete permission
|
||||
# (avoids win2000 freaking out and deleting the server copy without uploading its own)
|
||||
try:
|
||||
self.rv2a(filename, False, True, False, True)
|
||||
do_it = True
|
||||
except:
|
||||
do_it = False
|
||||
|
||||
if not do_it:
|
||||
raise FSE("File already exists")
|
||||
|
||||
wunlink(self.log, ap, VF_CAREFUL)
|
||||
|
||||
self.validpath(ap)
|
||||
return open(fsenc(ap), mode, self.args.iobuf)
|
||||
|
||||
def chdir(self, path: str) -> None:
|
||||
@@ -282,9 +307,20 @@ class FtpFs(AbstractedFS):
|
||||
# display write-only folders as empty
|
||||
return []
|
||||
|
||||
# return list of volumes
|
||||
r = {x.split("/")[0]: 1 for x in self.hub.asrv.vfs.all_vols.keys()}
|
||||
return list(sorted(list(r.keys())))
|
||||
# return list of accessible volumes
|
||||
ret = []
|
||||
for vn in self.hub.asrv.vfs.all_vols.values():
|
||||
if "/" in vn.vpath or not vn.vpath:
|
||||
continue # only include toplevel-mounted vols
|
||||
|
||||
try:
|
||||
self.hub.asrv.vfs.get(vn.vpath, self.uname, True, False)
|
||||
ret.append(vn.vpath)
|
||||
except:
|
||||
pass
|
||||
|
||||
ret.sort()
|
||||
return ret
|
||||
|
||||
def rmdir(self, path: str) -> None:
|
||||
ap = self.rv2a(path, d=True)[0]
|
||||
@@ -434,9 +470,10 @@ class FtpHandler(FTPHandler):
|
||||
None,
|
||||
xbu,
|
||||
ap,
|
||||
vfs.canonical(rem),
|
||||
vp,
|
||||
"",
|
||||
self.uname,
|
||||
self.hub.asrv.vfs.get_perms(vp, self.uname),
|
||||
0,
|
||||
0,
|
||||
self.cli_ip,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -55,6 +55,7 @@ class HttpConn(object):
|
||||
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
|
||||
|
||||
@@ -12,7 +12,7 @@ import time
|
||||
|
||||
import queue
|
||||
|
||||
from .__init__ import ANYWIN, CORES, EXE, MACOS, TYPE_CHECKING, EnvParams
|
||||
from .__init__ import ANYWIN, CORES, EXE, MACOS, TYPE_CHECKING, EnvParams, unicode
|
||||
|
||||
try:
|
||||
MNFE = ModuleNotFoundError
|
||||
@@ -61,6 +61,7 @@ from .u2idx import U2idx
|
||||
from .util import (
|
||||
E_SCK,
|
||||
FHC,
|
||||
CachedDict,
|
||||
Daemon,
|
||||
Garda,
|
||||
Magician,
|
||||
@@ -130,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
|
||||
@@ -264,10 +266,7 @@ class HttpSrv(object):
|
||||
msg = "subscribed @ {}:{} f{} p{}".format(hip, port, fno, os.getpid())
|
||||
self.log(self.name, msg)
|
||||
|
||||
def fun() -> None:
|
||||
self.broker.say("cb_httpsrv_up")
|
||||
|
||||
threading.Thread(target=fun, name="sig-hsrv-up1").start()
|
||||
Daemon(self.broker.say, "sig-hsrv-up1", ("cb_httpsrv_up",))
|
||||
|
||||
while not self.stopping:
|
||||
if self.args.log_conn:
|
||||
@@ -336,11 +335,11 @@ class HttpSrv(object):
|
||||
|
||||
try:
|
||||
sck, saddr = srv_sck.accept()
|
||||
cip, cport = saddr[:2]
|
||||
cip = unicode(saddr[0])
|
||||
if cip.startswith("::ffff:"):
|
||||
cip = cip[7:]
|
||||
|
||||
addr = (cip, cport)
|
||||
addr = (cip, saddr[1])
|
||||
except (OSError, socket.error) as ex:
|
||||
if self.stopping:
|
||||
break
|
||||
|
||||
@@ -292,6 +292,22 @@ class MDNS(MCast):
|
||||
def run2(self) -> None:
|
||||
last_hop = time.time()
|
||||
ihop = self.args.mc_hop
|
||||
|
||||
try:
|
||||
if self.args.no_poll:
|
||||
raise Exception()
|
||||
fd2sck = {}
|
||||
srvpoll = select.poll()
|
||||
for sck in self.srv:
|
||||
fd = sck.fileno()
|
||||
fd2sck[fd] = sck
|
||||
srvpoll.register(fd, select.POLLIN)
|
||||
except Exception as ex:
|
||||
srvpoll = None
|
||||
if not self.args.no_poll:
|
||||
t = "WARNING: failed to poll(), will use select() instead: %r"
|
||||
self.log(t % (ex,), 3)
|
||||
|
||||
while self.running:
|
||||
timeout = (
|
||||
0.02 + random.random() * 0.07
|
||||
@@ -300,8 +316,13 @@ class MDNS(MCast):
|
||||
if self.unsolicited
|
||||
else (last_hop + ihop if ihop else 180)
|
||||
)
|
||||
rdy = select.select(self.srv, [], [], timeout)
|
||||
rx: list[socket.socket] = rdy[0] # type: ignore
|
||||
if srvpoll:
|
||||
pr = srvpoll.poll(timeout * 1000)
|
||||
rx = [fd2sck[x[0]] for x in pr if x[1] & select.POLLIN]
|
||||
else:
|
||||
rdy = select.select(self.srv, [], [], timeout)
|
||||
rx: list[socket.socket] = rdy[0] # type: ignore
|
||||
|
||||
self.rx4.cln()
|
||||
self.rx6.cln()
|
||||
buf = b""
|
||||
@@ -340,7 +361,7 @@ class MDNS(MCast):
|
||||
except:
|
||||
pass
|
||||
|
||||
self.srv = {}
|
||||
self.srv.clear()
|
||||
|
||||
def eat(self, buf: bytes, addr: tuple[str, int], sck: socket.socket) -> None:
|
||||
cip = addr[0]
|
||||
|
||||
@@ -179,7 +179,7 @@ class Metrics(object):
|
||||
tnbytes = 0
|
||||
tnfiles = 0
|
||||
for vpath, vol in allvols:
|
||||
cur = idx.get_cur(vol.realpath)
|
||||
cur = idx.get_cur(vol)
|
||||
if not cur:
|
||||
continue
|
||||
|
||||
|
||||
@@ -7,12 +7,15 @@ import os
|
||||
import shutil
|
||||
import subprocess as sp
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from .__init__ import ANYWIN, EXE, PY2, WINDOWS, E, unicode
|
||||
from .authsrv import VFS
|
||||
from .bos import bos
|
||||
from .util import (
|
||||
FFMPEG_URL,
|
||||
REKOBO_LKEY,
|
||||
VF_CAREFUL,
|
||||
fsenc,
|
||||
min_ex,
|
||||
pybin,
|
||||
@@ -20,12 +23,13 @@ from .util import (
|
||||
runcmd,
|
||||
sfsenc,
|
||||
uncyg,
|
||||
wunlink,
|
||||
)
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Union
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from .util import RootLogger
|
||||
from .util import NamedLogger, RootLogger
|
||||
|
||||
|
||||
def have_ff(scmd: str) -> bool:
|
||||
@@ -107,6 +111,56 @@ class MParser(object):
|
||||
raise Exception()
|
||||
|
||||
|
||||
def au_unpk(
|
||||
log: "NamedLogger", fmt_map: dict[str, str], abspath: str, vn: Optional[VFS] = None
|
||||
) -> str:
|
||||
ret = ""
|
||||
try:
|
||||
ext = abspath.split(".")[-1].lower()
|
||||
au, pk = fmt_map[ext].split(".")
|
||||
|
||||
fd, ret = tempfile.mkstemp("." + au)
|
||||
|
||||
if pk == "gz":
|
||||
import gzip
|
||||
|
||||
fi = gzip.GzipFile(abspath, mode="rb")
|
||||
|
||||
elif pk == "xz":
|
||||
import lzma
|
||||
|
||||
fi = lzma.open(abspath, "rb")
|
||||
|
||||
elif pk == "zip":
|
||||
import zipfile
|
||||
|
||||
zf = zipfile.ZipFile(abspath, "r")
|
||||
zil = zf.infolist()
|
||||
zil = [x for x in zil if x.filename.lower().split(".")[-1] == au]
|
||||
fi = zf.open(zil[0])
|
||||
|
||||
else:
|
||||
raise Exception("unknown compression %s" % (pk,))
|
||||
|
||||
with os.fdopen(fd, "wb") as fo:
|
||||
while True:
|
||||
buf = fi.read(32768)
|
||||
if not buf:
|
||||
break
|
||||
|
||||
fo.write(buf)
|
||||
|
||||
return ret
|
||||
|
||||
except Exception as ex:
|
||||
if ret:
|
||||
t = "failed to decompress audio file [%s]: %r"
|
||||
log(t % (abspath, ex))
|
||||
wunlink(log, ret, vn.flags if vn else VF_CAREFUL)
|
||||
|
||||
return abspath
|
||||
|
||||
|
||||
def ffprobe(
|
||||
abspath: str, timeout: int = 60
|
||||
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]:
|
||||
@@ -281,7 +335,7 @@ class MTag(object):
|
||||
or_ffprobe = " or FFprobe"
|
||||
|
||||
if self.backend == "mutagen":
|
||||
self.get = self.get_mutagen
|
||||
self._get = self.get_mutagen
|
||||
try:
|
||||
from mutagen import version # noqa: F401
|
||||
except:
|
||||
@@ -290,7 +344,7 @@ class MTag(object):
|
||||
|
||||
if self.backend == "ffprobe":
|
||||
self.usable = self.can_ffprobe
|
||||
self.get = self.get_ffprobe
|
||||
self._get = self.get_ffprobe
|
||||
self.prefer_mt = True
|
||||
|
||||
if not HAVE_FFPROBE:
|
||||
@@ -460,6 +514,17 @@ class MTag(object):
|
||||
|
||||
return r1
|
||||
|
||||
def get(self, abspath: str) -> dict[str, Union[str, float]]:
|
||||
ext = abspath.split(".")[-1].lower()
|
||||
if ext not in self.args.au_unpk:
|
||||
return self._get(abspath)
|
||||
|
||||
ap = au_unpk(self.log, self.args.au_unpk, abspath)
|
||||
ret = self._get(ap)
|
||||
if ap != abspath:
|
||||
wunlink(self.log, ap, VF_CAREFUL)
|
||||
return ret
|
||||
|
||||
def get_mutagen(self, abspath: str) -> dict[str, Union[str, float]]:
|
||||
ret: dict[str, tuple[int, Any]] = {}
|
||||
|
||||
@@ -551,13 +616,18 @@ 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
|
||||
|
||||
ext = abspath.split(".")[-1].lower()
|
||||
if ext in self.args.au_unpk:
|
||||
ap = au_unpk(self.log, self.args.au_unpk, abspath)
|
||||
else:
|
||||
ap = abspath
|
||||
|
||||
ret: dict[str, Any] = {}
|
||||
for tagname, parser in sorted(parsers.items(), key=lambda x: (x[1].pri, x[0])):
|
||||
try:
|
||||
cmd = [parser.bin, abspath]
|
||||
cmd = [parser.bin, ap]
|
||||
if parser.bin.endswith(".py"):
|
||||
cmd = [pybin] + cmd
|
||||
|
||||
@@ -594,4 +664,7 @@ class MTag(object):
|
||||
t = "mtag error: tagname {}, parser {}, file {} => {}"
|
||||
self.log(t.format(tagname, parser.bin, abspath, min_ex()))
|
||||
|
||||
if ap != abspath:
|
||||
wunlink(self.log, ap, VF_CAREFUL)
|
||||
|
||||
return ret
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -127,7 +127,7 @@ class SMB(object):
|
||||
self.log("smb", msg, c)
|
||||
|
||||
def start(self) -> None:
|
||||
Daemon(self.srv.start)
|
||||
Daemon(self.srv.start, "smbd")
|
||||
|
||||
def _auth_cb(self, *a, **ka):
|
||||
debug("auth-result: %s %s", a, ka)
|
||||
@@ -240,7 +240,7 @@ class SMB(object):
|
||||
|
||||
xbu = vfs.flags.get("xbu")
|
||||
if xbu and not runhook(
|
||||
self.nlog, xbu, ap, vpath, "", "", 0, 0, "1.7.6.2", 0, ""
|
||||
self.nlog, xbu, ap, vpath, "", "", "", 0, 0, "1.7.6.2", 0, ""
|
||||
):
|
||||
yeet("blocked by xbu server config: " + vpath)
|
||||
|
||||
|
||||
@@ -141,9 +141,29 @@ class SSDPd(MCast):
|
||||
self.log("stopped", 2)
|
||||
|
||||
def run2(self) -> None:
|
||||
try:
|
||||
if self.args.no_poll:
|
||||
raise Exception()
|
||||
fd2sck = {}
|
||||
srvpoll = select.poll()
|
||||
for sck in self.srv:
|
||||
fd = sck.fileno()
|
||||
fd2sck[fd] = sck
|
||||
srvpoll.register(fd, select.POLLIN)
|
||||
except Exception as ex:
|
||||
srvpoll = None
|
||||
if not self.args.no_poll:
|
||||
t = "WARNING: failed to poll(), will use select() instead: %r"
|
||||
self.log(t % (ex,), 3)
|
||||
|
||||
while self.running:
|
||||
rdy = select.select(self.srv, [], [], self.args.z_chk or 180)
|
||||
rx: list[socket.socket] = rdy[0] # type: ignore
|
||||
if srvpoll:
|
||||
pr = srvpoll.poll((self.args.z_chk or 180) * 1000)
|
||||
rx = [fd2sck[x[0]] for x in pr if x[1] & select.POLLIN]
|
||||
else:
|
||||
rdy = select.select(self.srv, [], [], self.args.z_chk or 180)
|
||||
rx: list[socket.socket] = rdy[0] # type: ignore
|
||||
|
||||
self.rxc.cln()
|
||||
buf = b""
|
||||
addr = ("0", 0)
|
||||
@@ -168,7 +188,7 @@ class SSDPd(MCast):
|
||||
except:
|
||||
pass
|
||||
|
||||
self.srv = {}
|
||||
self.srv.clear()
|
||||
|
||||
def eat(self, buf: bytes, addr: tuple[str, int]) -> None:
|
||||
cip = addr[0]
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import stat
|
||||
import tarfile
|
||||
|
||||
from queue import Queue
|
||||
|
||||
from .authsrv import AuthSrv
|
||||
from .bos import bos
|
||||
from .sutil import StreamArc, errdesc
|
||||
from .util import Daemon, fsenc, min_ex
|
||||
@@ -45,12 +45,12 @@ class StreamTar(StreamArc):
|
||||
def __init__(
|
||||
self,
|
||||
log: "NamedLogger",
|
||||
args: argparse.Namespace,
|
||||
asrv: AuthSrv,
|
||||
fgen: Generator[dict[str, Any], None, None],
|
||||
cmp: str = "",
|
||||
**kwargs: Any
|
||||
):
|
||||
super(StreamTar, self).__init__(log, args, fgen)
|
||||
super(StreamTar, self).__init__(log, asrv, fgen)
|
||||
|
||||
self.ci = 0
|
||||
self.co = 0
|
||||
@@ -148,7 +148,7 @@ class StreamTar(StreamArc):
|
||||
errors.append((f["vp"], ex))
|
||||
|
||||
if errors:
|
||||
self.errf, txt = errdesc(errors)
|
||||
self.errf, txt = errdesc(self.asrv.vfs, errors)
|
||||
self.log("\n".join(([repr(self.errf)] + txt[1:])))
|
||||
self.ser(self.errf)
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
|
||||
from .__init__ import CORES
|
||||
from .authsrv import AuthSrv, VFS
|
||||
from .bos import bos
|
||||
from .th_cli import ThumbCli
|
||||
from .util import UTC, vjoin
|
||||
from .util import UTC, vjoin, vol_san
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Generator, Optional
|
||||
@@ -21,12 +21,13 @@ class StreamArc(object):
|
||||
def __init__(
|
||||
self,
|
||||
log: "NamedLogger",
|
||||
args: argparse.Namespace,
|
||||
asrv: AuthSrv,
|
||||
fgen: Generator[dict[str, Any], None, None],
|
||||
**kwargs: Any
|
||||
):
|
||||
self.log = log
|
||||
self.args = args
|
||||
self.asrv = asrv
|
||||
self.args = asrv.args
|
||||
self.fgen = fgen
|
||||
self.stopped = False
|
||||
|
||||
@@ -81,7 +82,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])
|
||||
@@ -101,15 +104,20 @@ def enthumb(
|
||||
return f
|
||||
|
||||
|
||||
def errdesc(errors: list[tuple[str, str]]) -> tuple[dict[str, Any], list[str]]:
|
||||
def errdesc(
|
||||
vfs: VFS, errors: list[tuple[str, str]]
|
||||
) -> tuple[dict[str, Any], list[str]]:
|
||||
report = ["copyparty failed to add the following files to the archive:", ""]
|
||||
|
||||
for fn, err in errors:
|
||||
report.extend([" file: {}".format(fn), "error: {}".format(err), ""])
|
||||
|
||||
btxt = "\r\n".join(report).encode("utf-8", "replace")
|
||||
btxt = vol_san(list(vfs.all_vols.values()), btxt)
|
||||
|
||||
with tempfile.NamedTemporaryFile(prefix="copyparty-", delete=False) as tf:
|
||||
tf_path = tf.name
|
||||
tf.write("\r\n".join(report).encode("utf-8", "replace"))
|
||||
tf.write(btxt)
|
||||
|
||||
dt = datetime.now(UTC).strftime("%Y-%m%d-%H%M%S")
|
||||
|
||||
|
||||
@@ -240,6 +240,10 @@ class SvcHub(object):
|
||||
if not HAVE_FFMPEG or not HAVE_FFPROBE:
|
||||
decs.pop("ff", None)
|
||||
|
||||
# compressed formats; "s3z=s3m.zip, s3gz=s3m.gz, ..."
|
||||
zlss = [x.strip().lower().split("=", 1) for x in args.au_unpk.split(",")]
|
||||
args.au_unpk = {x[0]: x[1] for x in zlss}
|
||||
|
||||
self.args.th_dec = list(decs.keys())
|
||||
self.thumbsrv = None
|
||||
want_ff = False
|
||||
@@ -276,6 +280,13 @@ 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,))
|
||||
else:
|
||||
args.au_unpk = {}
|
||||
|
||||
args.th_poke = min(args.th_poke, args.th_maxage, args.ac_maxage)
|
||||
|
||||
zms = ""
|
||||
@@ -288,13 +299,14 @@ class SvcHub(object):
|
||||
from .ftpd import Ftpd
|
||||
|
||||
self.ftpd: Optional[Ftpd] = None
|
||||
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
|
||||
|
||||
if args.ftp or args.ftps or args.tftp:
|
||||
Daemon(self.start_ftpd, "start_tftpd")
|
||||
|
||||
if args.smb:
|
||||
@@ -383,7 +395,7 @@ class SvcHub(object):
|
||||
self.sigterm()
|
||||
|
||||
def sigterm(self) -> None:
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
self.signal_handler(signal.SIGTERM, None)
|
||||
|
||||
def cb_httpsrv_up(self) -> None:
|
||||
self.httpsrv_up += 1
|
||||
@@ -419,6 +431,12 @@ class SvcHub(object):
|
||||
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
|
||||
|
||||
@@ -461,8 +479,10 @@ class SvcHub(object):
|
||||
zsl = al.th_covers.split(",")
|
||||
zsl = [x.strip() for x in zsl]
|
||||
zsl = [x for x in zsl if x]
|
||||
al.th_covers = set(zsl)
|
||||
al.th_coversd = set(zsl + ["." + x for x in zsl])
|
||||
al.th_covers = zsl
|
||||
al.th_coversd = zsl + ["." + x for x in zsl]
|
||||
al.th_covers_set = set(al.th_covers)
|
||||
al.th_coversd_set = set(al.th_coversd)
|
||||
|
||||
for k in "c".split(" "):
|
||||
vl = getattr(al, k)
|
||||
@@ -515,7 +535,7 @@ class SvcHub(object):
|
||||
al.exp_md = odfusion(exp, al.exp_md.replace(" ", ","))
|
||||
al.exp_lg = odfusion(exp, al.exp_lg.replace(" ", ","))
|
||||
|
||||
for k in ["no_hash", "no_idx"]:
|
||||
for k in ["no_hash", "no_idx", "og_ua"]:
|
||||
ptn = getattr(self.args, k)
|
||||
if ptn:
|
||||
setattr(self.args, k, re.compile(ptn))
|
||||
@@ -539,6 +559,17 @@ 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,))
|
||||
|
||||
al.tcolor = al.tcolor.lstrip("#")
|
||||
if len(al.tcolor) == 3: # fc5 => ffcc55
|
||||
al.tcolor = "".join([x * 2 for x in al.tcolor])
|
||||
|
||||
return True
|
||||
|
||||
def _ipa2re(self, txt) -> Optional[re.Pattern]:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import calendar
|
||||
import stat
|
||||
import time
|
||||
import zlib
|
||||
|
||||
from .authsrv import AuthSrv
|
||||
from .bos import bos
|
||||
from .sutil import StreamArc, errdesc
|
||||
from .util import min_ex, sanitize_fn, spack, sunpack, yieldfile
|
||||
@@ -219,13 +219,13 @@ class StreamZip(StreamArc):
|
||||
def __init__(
|
||||
self,
|
||||
log: "NamedLogger",
|
||||
args: argparse.Namespace,
|
||||
asrv: AuthSrv,
|
||||
fgen: Generator[dict[str, Any], None, None],
|
||||
utf8: bool = False,
|
||||
pre_crc: bool = False,
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
super(StreamZip, self).__init__(log, args, fgen)
|
||||
super(StreamZip, self).__init__(log, asrv, fgen)
|
||||
|
||||
self.utf8 = utf8
|
||||
self.pre_crc = pre_crc
|
||||
@@ -302,7 +302,7 @@ class StreamZip(StreamArc):
|
||||
mbuf = b""
|
||||
|
||||
if errors:
|
||||
errf, txt = errdesc(errors)
|
||||
errf, txt = errdesc(self.asrv.vfs, errors)
|
||||
self.log("\n".join(([repr(errf)] + txt[1:])))
|
||||
for x in self.ser(errf):
|
||||
yield x
|
||||
|
||||
@@ -15,6 +15,7 @@ from .util import (
|
||||
E_ADDR_IN_USE,
|
||||
E_ADDR_NOT_AVAIL,
|
||||
E_UNREACH,
|
||||
HAVE_IPV6,
|
||||
IP6ALL,
|
||||
Netdev,
|
||||
min_ex,
|
||||
@@ -111,8 +112,10 @@ class TcpSrv(object):
|
||||
|
||||
eps = {
|
||||
"127.0.0.1": Netdev("127.0.0.1", 0, "", "local only"),
|
||||
"::1": Netdev("::1", 0, "", "local only"),
|
||||
}
|
||||
if HAVE_IPV6:
|
||||
eps["::1"] = Netdev("::1", 0, "", "local only")
|
||||
|
||||
nonlocals = [x for x in self.args.i if x not in [k.split("/")[0] for k in eps]]
|
||||
if nonlocals:
|
||||
try:
|
||||
@@ -463,6 +466,12 @@ class TcpSrv(object):
|
||||
sys.stderr.flush()
|
||||
|
||||
def _qr(self, t1: dict[str, list[int]], t2: dict[str, list[int]]) -> str:
|
||||
t2c = {zs: zli for zs, zli in t2.items() if zs in ("127.0.0.1", "::1")}
|
||||
t2b = {zs: zli for zs, zli in t2.items() if ":" in zs and zs not in t2c}
|
||||
t2 = {zs: zli for zs, zli in t2.items() if zs not in t2b and zs not in t2c}
|
||||
t2.update(t2b) # first ipv4, then ipv6...
|
||||
t2.update(t2c) # ...and finally localhost
|
||||
|
||||
ip = None
|
||||
ips = list(t1) + list(t2)
|
||||
qri = self.args.qri
|
||||
|
||||
@@ -33,7 +33,7 @@ from partftpy import (
|
||||
)
|
||||
from partftpy.TftpShared import TftpException
|
||||
|
||||
from .__init__ import EXE, TYPE_CHECKING
|
||||
from .__init__ import EXE, PY2, TYPE_CHECKING
|
||||
from .authsrv import VFS
|
||||
from .bos import bos
|
||||
from .util import BytesIO, Daemon, ODict, exclude_dotfiles, min_ex, runhook, undot
|
||||
@@ -95,7 +95,7 @@ class Tftpd(object):
|
||||
TftpServer,
|
||||
]
|
||||
cbak = []
|
||||
if not self.args.tftp_no_fast and not EXE:
|
||||
if not self.args.tftp_no_fast and not EXE and not PY2:
|
||||
try:
|
||||
ptn = re.compile(r"(^\s*)log\.debug\(.*\)$")
|
||||
for C in Cs:
|
||||
@@ -105,7 +105,7 @@ class Tftpd(object):
|
||||
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"
|
||||
t = "failed to optimize tftp code; run with --tftp-no-fast if there are issues:\n"
|
||||
self.log("tftp", t + min_ex(), 3)
|
||||
for n, zd in enumerate(cbak):
|
||||
Cs[n].__dict__ = zd
|
||||
@@ -150,11 +150,6 @@ class Tftpd(object):
|
||||
|
||||
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 = []
|
||||
@@ -168,7 +163,7 @@ class Tftpd(object):
|
||||
if "::" in ips:
|
||||
ips.append("0.0.0.0")
|
||||
|
||||
if self.args.ftp4:
|
||||
if self.args.tftp4:
|
||||
ips = [x for x in ips if ":" not in x]
|
||||
|
||||
ips = list(ODict.fromkeys(ips)) # dedup
|
||||
@@ -333,7 +328,7 @@ class Tftpd(object):
|
||||
|
||||
xbu = vfs.flags.get("xbu")
|
||||
if xbu and not runhook(
|
||||
self.nlog, xbu, ap, vpath, "", "", 0, 0, "8.3.8.7", 0, ""
|
||||
self.nlog, xbu, ap, vpath, "", "", "", 0, 0, "8.3.8.7", 0, ""
|
||||
):
|
||||
yeet("blocked by xbu server config: " + vpath)
|
||||
|
||||
|
||||
@@ -57,9 +57,10 @@ 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:
|
||||
is_vau = want_opus and ext in self.fmt_ffv
|
||||
if is_au or is_vau:
|
||||
if want_opus:
|
||||
if self.args.no_acode:
|
||||
return None
|
||||
@@ -107,6 +108,11 @@ class ThumbCli(object):
|
||||
|
||||
fmt = sfmt
|
||||
|
||||
elif fmt[:1] == "p" and not is_au and not is_vid:
|
||||
t = "cannot thumbnail [%s]: png only allowed for waveforms"
|
||||
self.log(t % (rem), 6)
|
||||
return None
|
||||
|
||||
histpath = self.asrv.vfs.histtab.get(ptop)
|
||||
if not histpath:
|
||||
self.log("no histpath for [{}]".format(ptop))
|
||||
|
||||
@@ -15,7 +15,7 @@ from queue import Queue
|
||||
from .__init__ import ANYWIN, TYPE_CHECKING
|
||||
from .authsrv import VFS
|
||||
from .bos import bos
|
||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
|
||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, au_unpk, ffprobe
|
||||
from .util import BytesIO # type: ignore
|
||||
from .util import (
|
||||
FFMPEG_URL,
|
||||
@@ -28,6 +28,7 @@ from .util import (
|
||||
runcmd,
|
||||
statdir,
|
||||
vsplit,
|
||||
wrename,
|
||||
wunlink,
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
@@ -296,25 +297,38 @@ class ThumbSrv(object):
|
||||
ext = abspath.split(".")[-1].lower()
|
||||
png_ok = False
|
||||
funs = []
|
||||
|
||||
if ext in self.args.au_unpk:
|
||||
ap_unpk = au_unpk(self.log, self.args.au_unpk, abspath, vn)
|
||||
else:
|
||||
ap_unpk = abspath
|
||||
|
||||
if not bos.path.exists(tpath):
|
||||
want_mp3 = tpath.endswith(".mp3")
|
||||
want_opus = tpath.endswith(".opus") or tpath.endswith(".caf")
|
||||
want_png = tpath.endswith(".png")
|
||||
want_au = want_mp3 or want_opus
|
||||
for lib in self.args.th_dec:
|
||||
can_au = lib == "ff" and (
|
||||
ext in self.fmt_ffa or ext in self.fmt_ffv
|
||||
)
|
||||
|
||||
if lib == "pil" and ext in self.fmt_pil:
|
||||
funs.append(self.conv_pil)
|
||||
elif lib == "vips" and ext in self.fmt_vips:
|
||||
funs.append(self.conv_vips)
|
||||
elif lib == "ff" and ext in self.fmt_ffi or ext in self.fmt_ffv:
|
||||
funs.append(self.conv_ffmpeg)
|
||||
elif lib == "ff" and ext in self.fmt_ffa:
|
||||
if tpath.endswith(".opus") or tpath.endswith(".caf"):
|
||||
elif can_au and (want_png or want_au):
|
||||
if want_opus:
|
||||
funs.append(self.conv_opus)
|
||||
elif tpath.endswith(".png"):
|
||||
elif want_mp3:
|
||||
funs.append(self.conv_mp3)
|
||||
elif want_png:
|
||||
funs.append(self.conv_waves)
|
||||
png_ok = True
|
||||
else:
|
||||
funs.append(self.conv_spec)
|
||||
|
||||
if not png_ok and tpath.endswith(".png"):
|
||||
raise Pebkac(400, "png only allowed for waveforms")
|
||||
elif lib == "ff" and (ext in self.fmt_ffi or ext in self.fmt_ffv):
|
||||
funs.append(self.conv_ffmpeg)
|
||||
elif lib == "ff" and ext in self.fmt_ffa and not want_au:
|
||||
funs.append(self.conv_spec)
|
||||
|
||||
tdir, tfn = os.path.split(tpath)
|
||||
ttpath = os.path.join(tdir, "w", tfn)
|
||||
@@ -325,7 +339,10 @@ class ThumbSrv(object):
|
||||
|
||||
for fun in funs:
|
||||
try:
|
||||
fun(abspath, ttpath, fmt, vn)
|
||||
if not png_ok and tpath.endswith(".png"):
|
||||
raise Exception("png only allowed for waveforms")
|
||||
|
||||
fun(ap_unpk, ttpath, fmt, vn)
|
||||
break
|
||||
except Exception as ex:
|
||||
msg = "{} could not create thumbnail of {}\n{}"
|
||||
@@ -343,8 +360,11 @@ class ThumbSrv(object):
|
||||
except:
|
||||
pass
|
||||
|
||||
if abspath != ap_unpk:
|
||||
wunlink(self.log, ap_unpk, vn.flags)
|
||||
|
||||
try:
|
||||
bos.rename(ttpath, tpath)
|
||||
wrename(self.log, ttpath, tpath, vn.flags)
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -581,6 +601,25 @@ class ThumbSrv(object):
|
||||
cmd += [fsenc(tpath)]
|
||||
self._run_ff(cmd, vn)
|
||||
|
||||
if "pngquant" in vn.flags:
|
||||
wtpath = tpath + ".png"
|
||||
cmd = [
|
||||
b"pngquant",
|
||||
b"--strip",
|
||||
b"--nofs",
|
||||
b"--output",
|
||||
fsenc(wtpath),
|
||||
fsenc(tpath),
|
||||
]
|
||||
ret = runcmd(cmd, timeout=vn.flags["convt"], nice=True, oom=400)[0]
|
||||
if ret:
|
||||
try:
|
||||
wunlink(self.log, wtpath, vn.flags)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
wrename(self.log, wtpath, tpath, vn.flags)
|
||||
|
||||
def conv_spec(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
if "ac" not in ret:
|
||||
@@ -637,21 +676,60 @@ class ThumbSrv(object):
|
||||
cmd += [fsenc(tpath)]
|
||||
self._run_ff(cmd, vn)
|
||||
|
||||
def conv_opus(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
if self.args.no_acode:
|
||||
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:
|
||||
tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
if "ac" not in tags:
|
||||
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),
|
||||
] + self.big_tags(rawtags) + [
|
||||
b"-map", b"0:a:0",
|
||||
b"-ar", b"44100",
|
||||
b"-ac", b"2",
|
||||
b"-c:a", b"libmp3lame",
|
||||
qk, qv,
|
||||
fsenc(tpath)
|
||||
]
|
||||
# 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 or not self.args.q_opus:
|
||||
raise Exception("disabled in server config")
|
||||
|
||||
self.wait4ram(0.2, tpath)
|
||||
tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
if "ac" not in tags:
|
||||
raise Exception("not audio")
|
||||
|
||||
try:
|
||||
dur = ret[".dur"][1]
|
||||
dur = tags[".dur"][1]
|
||||
except:
|
||||
dur = 0
|
||||
|
||||
src_opus = abspath.lower().endswith(".opus") or ret["ac"][1] == "opus"
|
||||
src_opus = abspath.lower().endswith(".opus") or tags["ac"][1] == "opus"
|
||||
want_caf = tpath.endswith(".caf")
|
||||
tmp_opus = tpath
|
||||
if want_caf:
|
||||
@@ -662,6 +740,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
|
||||
@@ -671,10 +750,10 @@ class ThumbSrv(object):
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
b"-i", fsenc(abspath),
|
||||
b"-map_metadata", b"-1",
|
||||
] + self.big_tags(rawtags) + [
|
||||
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
|
||||
@@ -697,7 +776,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)
|
||||
]
|
||||
@@ -728,6 +807,16 @@ class ThumbSrv(object):
|
||||
except:
|
||||
pass
|
||||
|
||||
def big_tags(self, raw_tags: dict[str, list[str]]) -> list[bytes]:
|
||||
ret = []
|
||||
for k, vs in raw_tags.items():
|
||||
for v in vs:
|
||||
if len(str(v)) >= 1024:
|
||||
bv = k.encode("utf-8", "replace")
|
||||
ret += [b"-metadata", bv + b"="]
|
||||
break
|
||||
return ret
|
||||
|
||||
def poke(self, tdir: str) -> None:
|
||||
if not self.poke_cd.poke(tdir):
|
||||
return
|
||||
@@ -771,7 +860,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
|
||||
|
||||
@@ -62,6 +62,17 @@ class U2idx(object):
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.log_func("u2idx", msg, c)
|
||||
|
||||
def shutdown(self) -> None:
|
||||
for cur in self.cur.values():
|
||||
db = cur.connection
|
||||
try:
|
||||
db.interrupt()
|
||||
except:
|
||||
pass
|
||||
|
||||
cur.close()
|
||||
db.close()
|
||||
|
||||
def fsearch(
|
||||
self, uname: str, vols: list[VFS], body: dict[str, Any]
|
||||
) -> list[dict[str, Any]]:
|
||||
@@ -81,14 +92,18 @@ class U2idx(object):
|
||||
except:
|
||||
raise Pebkac(500, min_ex())
|
||||
|
||||
def get_cur(self, ptop: str) -> Optional["sqlite3.Cursor"]:
|
||||
def get_cur(self, vn: VFS) -> Optional["sqlite3.Cursor"]:
|
||||
if not HAVE_SQLITE3:
|
||||
return None
|
||||
|
||||
cur = self.cur.get(ptop)
|
||||
cur = self.cur.get(vn.realpath)
|
||||
if cur:
|
||||
return cur
|
||||
|
||||
if "e2d" not in vn.flags:
|
||||
return None
|
||||
|
||||
ptop = vn.realpath
|
||||
histpath = self.asrv.vfs.histtab.get(ptop)
|
||||
if not histpath:
|
||||
self.log("no histpath for [{}]".format(ptop))
|
||||
@@ -317,7 +332,7 @@ class U2idx(object):
|
||||
ptop = vol.realpath
|
||||
flags = vol.flags
|
||||
|
||||
cur = self.get_cur(ptop)
|
||||
cur = self.get_cur(vol)
|
||||
if not cur:
|
||||
continue
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import stat
|
||||
import subprocess as sp
|
||||
import tempfile
|
||||
@@ -29,6 +28,7 @@ from .fsutil import Fstab
|
||||
from .mtag import MParser, MTag
|
||||
from .util import (
|
||||
HAVE_SQLITE3,
|
||||
VF_CAREFUL,
|
||||
SYMTIME,
|
||||
Daemon,
|
||||
MTHash,
|
||||
@@ -136,6 +136,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]] = {}
|
||||
@@ -143,7 +144,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]]] = {}
|
||||
|
||||
@@ -182,7 +183,7 @@ class Up2k(object):
|
||||
t = "could not initialize sqlite3, will use in-memory registry only"
|
||||
self.log(t, 3)
|
||||
|
||||
self.fstab = Fstab(self.log_func)
|
||||
self.fstab = Fstab(self.log_func, self.args)
|
||||
self.gen_fk = self._gen_fk if self.args.log_fk else gen_filekey
|
||||
|
||||
if self.args.hash_mt < 2:
|
||||
@@ -200,11 +201,15 @@ class Up2k(object):
|
||||
Daemon(self.deferred_init, "up2k-deferred-init")
|
||||
|
||||
def reload(self, rescan_all_vols: bool) -> None:
|
||||
"""mutex me"""
|
||||
"""mutex(main) me"""
|
||||
self.log("reload #{} scheduled".format(self.gid + 1))
|
||||
all_vols = self.asrv.vfs.all_vols
|
||||
|
||||
scan_vols = [k for k, v in all_vols.items() if v.realpath not in self.registry]
|
||||
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())
|
||||
|
||||
@@ -217,7 +222,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:
|
||||
@@ -283,10 +288,27 @@ 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.mutex.acquire(timeout=2):
|
||||
if PY2 or not self.reg_mutex.acquire(timeout=2):
|
||||
return '[{"timeout":1}]'
|
||||
|
||||
ret: list[tuple[int, str, int, int, int]] = []
|
||||
@@ -315,17 +337,25 @@ class Up2k(object):
|
||||
)
|
||||
ret.append(zt5)
|
||||
finally:
|
||||
self.mutex.release()
|
||||
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": "/" + vp, "pd": 100 - ((nn * 100) // (nh or 1)), "sz": sz}
|
||||
{
|
||||
"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, indent=0)
|
||||
return json.dumps(ret2, separators=(",\n", ": "))
|
||||
|
||||
def get_unfinished(self) -> str:
|
||||
if PY2 or not self.mutex.acquire(timeout=0.5):
|
||||
if PY2 or not self.reg_mutex.acquire(timeout=0.5):
|
||||
return ""
|
||||
|
||||
ret: dict[str, tuple[int, int]] = {}
|
||||
@@ -347,17 +377,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))
|
||||
|
||||
@@ -385,7 +415,7 @@ class Up2k(object):
|
||||
def _rescan(
|
||||
self, all_vols: dict[str, VFS], scan_vols: list[str], wait: bool, fscan: bool
|
||||
) -> str:
|
||||
"""mutex me"""
|
||||
"""mutex(main) me"""
|
||||
if not wait and self.pp:
|
||||
return "cannot initiate; scan is already in progress"
|
||||
|
||||
@@ -428,7 +458,13 @@ class Up2k(object):
|
||||
# important; not deferred by db_act
|
||||
timeout = self._check_lifetimes()
|
||||
|
||||
timeout = min(timeout, now + self._check_xiu())
|
||||
try:
|
||||
timeout = min(timeout, now + self._check_xiu())
|
||||
except Exception as ex:
|
||||
if "closed cursor" in str(ex):
|
||||
self.log("sched_rescan: lost db")
|
||||
return
|
||||
raise
|
||||
|
||||
with self.mutex:
|
||||
for vp, vol in sorted(self.asrv.vfs.all_vols.items()):
|
||||
@@ -509,7 +545,7 @@ class Up2k(object):
|
||||
nrm += 1
|
||||
|
||||
if nrm:
|
||||
self.log("{} files graduated in {}".format(nrm, vp))
|
||||
self.log("%d files graduated in /%s" % (nrm, vp))
|
||||
|
||||
if timeout < 10:
|
||||
continue
|
||||
@@ -618,7 +654,7 @@ class Up2k(object):
|
||||
return False, flags
|
||||
|
||||
ret = {k: v for k, v in flags.items() if not k.startswith("e2t")}
|
||||
if ret.keys() == flags.keys():
|
||||
if len(ret) == len(flags):
|
||||
return False, flags
|
||||
|
||||
return True, ret
|
||||
@@ -667,15 +703,15 @@ 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:
|
||||
bos.makedirs(vol.realpath) # gonna happen at snap anyways
|
||||
dir_is_empty(self.log_func, not self.args.no_scandir, vol.realpath)
|
||||
except:
|
||||
except Exception as ex:
|
||||
self.volstate[vol.vpath] = "OFFLINE (cannot access folder)"
|
||||
self.log("cannot access " + vol.realpath, c=1)
|
||||
self.log("cannot access %s: %r" % (vol.realpath, ex), c=1)
|
||||
continue
|
||||
|
||||
if scan_vols and vol.vpath not in scan_vols:
|
||||
@@ -709,7 +745,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:
|
||||
@@ -786,7 +822,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:
|
||||
@@ -800,7 +838,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
|
||||
@@ -847,6 +887,7 @@ class Up2k(object):
|
||||
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))
|
||||
@@ -869,7 +910,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())
|
||||
@@ -1001,8 +1042,11 @@ class Up2k(object):
|
||||
return None
|
||||
|
||||
def _verify_db_cache(self, cur: "sqlite3.Cursor", vpath: str) -> None:
|
||||
# check if volume config changed since last use; drop caches if so
|
||||
zsl = [vpath] + list(sorted(self.asrv.vfs.all_vols.keys()))
|
||||
# check if list of intersecting volumes changed since last use; drop caches if so
|
||||
prefix = (vpath + "/").lstrip("/")
|
||||
zsl = [x for x in self.asrv.vfs.all_vols if x.startswith(prefix)]
|
||||
zsl = [x[len(prefix) :] for x in zsl]
|
||||
zsl.sort()
|
||||
zb = hashlib.sha1("\n".join(zsl).encode("utf-8", "replace")).digest()
|
||||
vcfg = base64.urlsafe_b64encode(zb[:18]).decode("ascii")
|
||||
|
||||
@@ -1030,7 +1074,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
|
||||
|
||||
@@ -1149,6 +1195,9 @@ class Up2k(object):
|
||||
fat32 = True
|
||||
cv = ""
|
||||
|
||||
th_cvd = self.args.th_coversd
|
||||
th_cvds = self.args.th_coversd_set
|
||||
|
||||
assert self.pp and self.mem_cur
|
||||
self.pp.msg = "a%d %s" % (self.pp.n, cdir)
|
||||
|
||||
@@ -1233,12 +1282,21 @@ class Up2k(object):
|
||||
|
||||
files.append((sz, lmod, iname))
|
||||
liname = iname.lower()
|
||||
if sz and (
|
||||
iname in self.args.th_coversd
|
||||
or (
|
||||
if (
|
||||
sz
|
||||
and (
|
||||
liname in th_cvds
|
||||
or (
|
||||
not cv
|
||||
and liname.rsplit(".", 1)[-1] in CV_EXTS
|
||||
and not iname.startswith(".")
|
||||
)
|
||||
)
|
||||
and (
|
||||
not cv
|
||||
and liname.rsplit(".", 1)[-1] in CV_EXTS
|
||||
and not iname.startswith(".")
|
||||
or liname not in th_cvds
|
||||
or cv.lower() not in th_cvds
|
||||
or th_cvd.index(liname) < th_cvd.index(cv.lower())
|
||||
)
|
||||
):
|
||||
cv = iname
|
||||
@@ -1610,7 +1668,7 @@ class Up2k(object):
|
||||
|
||||
if e2vp and rewark:
|
||||
self.hub.retcode = 1
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
Daemon(self.hub.sigterm)
|
||||
raise Exception("{} files have incorrect hashes".format(len(rewark)))
|
||||
|
||||
if not e2vu or not rewark:
|
||||
@@ -1627,7 +1685,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
|
||||
@@ -1648,6 +1706,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)
|
||||
@@ -1823,7 +1882,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-")
|
||||
@@ -2157,7 +2216,7 @@ class Up2k(object):
|
||||
ip: str,
|
||||
at: float,
|
||||
) -> int:
|
||||
"""will mutex"""
|
||||
"""will mutex(main)"""
|
||||
assert self.mtag
|
||||
|
||||
try:
|
||||
@@ -2189,7 +2248,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):
|
||||
@@ -2474,28 +2533,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)
|
||||
@@ -2510,7 +2577,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"]]
|
||||
@@ -2703,6 +2770,7 @@ class Up2k(object):
|
||||
job["vtop"],
|
||||
job["host"],
|
||||
job["user"],
|
||||
self.asrv.vfs.get_perms(job["vtop"], job["user"]),
|
||||
job["lmod"],
|
||||
job["size"],
|
||||
job["addr"],
|
||||
@@ -2945,10 +3013,10 @@ class Up2k(object):
|
||||
times = (int(time.time()), int(lmod))
|
||||
bos.utime(dst, times, False)
|
||||
|
||||
def handle_chunk(
|
||||
self, ptop: str, wark: str, chash: str
|
||||
) -> tuple[int, list[int], str, float, bool]:
|
||||
with self.mutex:
|
||||
def handle_chunks(
|
||||
self, ptop: str, wark: str, chashes: list[str]
|
||||
) -> tuple[list[int], list[list[int]], str, float, bool]:
|
||||
with self.mutex, self.reg_mutex:
|
||||
self.db_act = self.vol_act[ptop] = time.time()
|
||||
job = self.registry[ptop].get(wark)
|
||||
if not job:
|
||||
@@ -2956,26 +3024,37 @@ class Up2k(object):
|
||||
self.log("unknown wark [{}], known: {}".format(wark, known))
|
||||
raise Pebkac(400, "unknown wark" + SSEELOG)
|
||||
|
||||
if chash not in job["need"]:
|
||||
msg = "chash = {} , need:\n".format(chash)
|
||||
msg += "\n".join(job["need"])
|
||||
self.log(msg)
|
||||
raise Pebkac(400, "already got that but thanks??")
|
||||
for chash in chashes:
|
||||
if chash not in job["need"]:
|
||||
msg = "chash = {} , need:\n".format(chash)
|
||||
msg += "\n".join(job["need"])
|
||||
self.log(msg)
|
||||
raise Pebkac(400, "already got that (%s) but thanks??" % (chash,))
|
||||
|
||||
nchunk = [n for n, v in enumerate(job["hash"]) if v == chash]
|
||||
if not nchunk:
|
||||
raise Pebkac(400, "unknown chunk")
|
||||
|
||||
if chash in job["busy"]:
|
||||
nh = len(job["hash"])
|
||||
idx = job["hash"].index(chash)
|
||||
t = "that chunk is already being written to:\n {}\n {} {}/{}\n {}"
|
||||
raise Pebkac(400, t.format(wark, chash, idx, nh, job["name"]))
|
||||
|
||||
path = djoin(job["ptop"], job["prel"], job["tnam"])
|
||||
if chash in job["busy"]:
|
||||
nh = len(job["hash"])
|
||||
idx = job["hash"].index(chash)
|
||||
t = "that chunk is already being written to:\n {}\n {} {}/{}\n {}"
|
||||
raise Pebkac(400, t.format(wark, chash, idx, nh, job["name"]))
|
||||
|
||||
chunksize = up2k_chunksize(job["size"])
|
||||
ofs = [chunksize * x for x in nchunk]
|
||||
|
||||
coffsets = []
|
||||
for chash in chashes:
|
||||
nchunk = [n for n, v in enumerate(job["hash"]) if v == chash]
|
||||
if not nchunk:
|
||||
raise Pebkac(400, "unknown chunk %s" % (chash))
|
||||
|
||||
ofs = [chunksize * x for x in nchunk]
|
||||
coffsets.append(ofs)
|
||||
|
||||
for ofs1, ofs2 in zip(coffsets, coffsets[1:]):
|
||||
gap = (ofs2[0] - ofs1[0]) - chunksize
|
||||
if gap:
|
||||
t = "only sibling chunks can be stitched; gap of %d bytes between offsets %d and %d in %s"
|
||||
raise Pebkac(400, t % (gap, ofs1[0], ofs2[0], job["name"]))
|
||||
|
||||
path = djoin(job["ptop"], job["prel"], job["tnam"])
|
||||
|
||||
if not job["sprs"]:
|
||||
cur_sz = bos.path.getsize(path)
|
||||
@@ -2988,18 +3067,21 @@ class Up2k(object):
|
||||
|
||||
job["poke"] = time.time()
|
||||
|
||||
return chunksize, ofs, path, job["lmod"], job["sprs"]
|
||||
return chunksize, coffsets, path, job["lmod"], job["sprs"]
|
||||
|
||||
def release_chunk(self, ptop: str, wark: str, chash: str) -> bool:
|
||||
with self.mutex:
|
||||
def release_chunks(self, ptop: str, wark: str, chashes: list[str]) -> bool:
|
||||
with self.reg_mutex:
|
||||
job = self.registry[ptop].get(wark)
|
||||
if job:
|
||||
job["busy"].pop(chash, None)
|
||||
for chash in chashes:
|
||||
job["busy"].pop(chash, None)
|
||||
|
||||
return True
|
||||
|
||||
def confirm_chunk(self, ptop: str, wark: str, chash: str) -> tuple[int, str]:
|
||||
with self.mutex:
|
||||
def confirm_chunks(
|
||||
self, ptop: str, wark: str, chashes: list[str]
|
||||
) -> tuple[int, str]:
|
||||
with self.mutex, self.reg_mutex:
|
||||
self.db_act = self.vol_act[ptop] = time.time()
|
||||
try:
|
||||
job = self.registry[ptop][wark]
|
||||
@@ -3007,14 +3089,16 @@ class Up2k(object):
|
||||
src = djoin(pdir, job["tnam"])
|
||||
dst = djoin(pdir, job["name"])
|
||||
except Exception as ex:
|
||||
return "confirm_chunk, wark, " + repr(ex) # type: ignore
|
||||
return "confirm_chunk, wark(%r)" % (ex,) # type: ignore
|
||||
|
||||
job["busy"].pop(chash, None)
|
||||
for chash in chashes:
|
||||
job["busy"].pop(chash, None)
|
||||
|
||||
try:
|
||||
job["need"].remove(chash)
|
||||
for chash in chashes:
|
||||
job["need"].remove(chash)
|
||||
except Exception as ex:
|
||||
return "confirm_chunk, chash, " + repr(ex) # type: ignore
|
||||
return "confirm_chunk, chash(%s) %r" % (chash, ex) # type: ignore
|
||||
|
||||
ret = len(job["need"])
|
||||
if ret > 0:
|
||||
@@ -3022,16 +3106,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:
|
||||
def finish_upload(self, ptop: str, wark: str, busy_aps: dict[str, int]) -> 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"])
|
||||
@@ -3044,12 +3128,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(
|
||||
@@ -3105,6 +3188,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)
|
||||
@@ -3199,16 +3283,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
|
||||
@@ -3223,6 +3314,7 @@ class Up2k(object):
|
||||
djoin(vtop, rd, fn),
|
||||
host,
|
||||
usr,
|
||||
self.asrv.vfs.get_perms(djoin(vtop, rd, fn), usr),
|
||||
int(ts),
|
||||
sz,
|
||||
ip,
|
||||
@@ -3257,15 +3349,29 @@ class Up2k(object):
|
||||
with self.rescan_cond:
|
||||
self.rescan_cond.notify_all()
|
||||
|
||||
if rd and sz and fn.lower() in self.args.th_coversd:
|
||||
if rd and sz and fn.lower() in self.args.th_coversd_set:
|
||||
# wasteful; db_add will re-index actual covers
|
||||
# but that won't catch existing files
|
||||
crd, cdn = rd.rsplit("/", 1) if "/" in rd else ("", rd)
|
||||
try:
|
||||
db.execute("delete from cv where rd=? and dn=?", (crd, cdn))
|
||||
db.execute("insert into cv values (?,?,?)", (crd, cdn, fn))
|
||||
q = "select fn from cv where rd=? and dn=?"
|
||||
db_cv = db.execute(q, (crd, cdn)).fetchone()[0]
|
||||
db_lcv = db_cv.lower()
|
||||
if db_lcv in self.args.th_coversd_set:
|
||||
idx_db = self.args.th_coversd.index(db_lcv)
|
||||
idx_fn = self.args.th_coversd.index(fn.lower())
|
||||
add_cv = idx_fn < idx_db
|
||||
else:
|
||||
add_cv = True
|
||||
except:
|
||||
pass
|
||||
add_cv = True
|
||||
|
||||
if add_cv:
|
||||
try:
|
||||
db.execute("delete from cv where rd=? and dn=?", (crd, cdn))
|
||||
db.execute("insert into cv values (?,?,?)", (crd, cdn, fn))
|
||||
except:
|
||||
pass
|
||||
|
||||
def handle_rm(
|
||||
self,
|
||||
@@ -3312,7 +3418,7 @@ class Up2k(object):
|
||||
vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0])
|
||||
vn, rem = vn.get_dbv(rem)
|
||||
ptop = vn.realpath
|
||||
with self.mutex:
|
||||
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 ""
|
||||
@@ -3320,7 +3426,10 @@ class Up2k(object):
|
||||
for wark, job in reg.items():
|
||||
if (user and user != job["user"]) or (addr and addr != job["addr"]):
|
||||
continue
|
||||
if djoin(job["prel"], job["name"]) == rem:
|
||||
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))
|
||||
@@ -3405,6 +3514,7 @@ class Up2k(object):
|
||||
vpath,
|
||||
"",
|
||||
uname,
|
||||
self.asrv.vfs.get_perms(vpath, uname),
|
||||
stl.st_mtime,
|
||||
st.st_size,
|
||||
ip,
|
||||
@@ -3416,7 +3526,7 @@ class Up2k(object):
|
||||
continue
|
||||
|
||||
n_files += 1
|
||||
with self.mutex:
|
||||
with self.mutex, self.reg_mutex:
|
||||
cur = None
|
||||
try:
|
||||
ptop = dbv.realpath
|
||||
@@ -3438,6 +3548,7 @@ class Up2k(object):
|
||||
vpath,
|
||||
"",
|
||||
uname,
|
||||
self.asrv.vfs.get_perms(vpath, uname),
|
||||
stl.st_mtime,
|
||||
st.st_size,
|
||||
ip,
|
||||
@@ -3534,6 +3645,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)
|
||||
|
||||
@@ -3569,7 +3681,18 @@ class Up2k(object):
|
||||
xar = dvn.flags.get("xar")
|
||||
if xbr:
|
||||
if not runhook(
|
||||
self.log, xbr, sabs, svp, "", uname, stl.st_mtime, st.st_size, "", 0, ""
|
||||
self.log,
|
||||
xbr,
|
||||
sabs,
|
||||
svp,
|
||||
"",
|
||||
uname,
|
||||
self.asrv.vfs.get_perms(svp, uname),
|
||||
stl.st_mtime,
|
||||
st.st_size,
|
||||
"",
|
||||
0,
|
||||
"",
|
||||
):
|
||||
t = "move blocked by xbr server config: {}".format(svp)
|
||||
self.log(t, 1)
|
||||
@@ -3594,7 +3717,20 @@ class Up2k(object):
|
||||
self.rescan_cond.notify_all()
|
||||
|
||||
if xar:
|
||||
runhook(self.log, xar, dabs, dvp, "", uname, 0, 0, "", 0, "")
|
||||
runhook(
|
||||
self.log,
|
||||
xar,
|
||||
dabs,
|
||||
dvp,
|
||||
"",
|
||||
uname,
|
||||
self.asrv.vfs.get_perms(dvp, uname),
|
||||
0,
|
||||
0,
|
||||
"",
|
||||
0,
|
||||
"",
|
||||
)
|
||||
|
||||
return "k"
|
||||
|
||||
@@ -3614,7 +3750,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)
|
||||
|
||||
@@ -3653,7 +3791,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:
|
||||
@@ -3691,7 +3829,20 @@ class Up2k(object):
|
||||
wunlink(self.log, sabs, svn.flags)
|
||||
|
||||
if xar:
|
||||
runhook(self.log, xar, dabs, dvp, "", uname, 0, 0, "", 0, "")
|
||||
runhook(
|
||||
self.log,
|
||||
xar,
|
||||
dabs,
|
||||
dvp,
|
||||
"",
|
||||
uname,
|
||||
self.asrv.vfs.get_perms(dvp, uname),
|
||||
0,
|
||||
0,
|
||||
"",
|
||||
0,
|
||||
"",
|
||||
)
|
||||
|
||||
return "k"
|
||||
|
||||
@@ -3744,7 +3895,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))
|
||||
@@ -3830,8 +3984,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)
|
||||
@@ -3978,6 +4131,7 @@ class Up2k(object):
|
||||
vp_chk,
|
||||
job["host"],
|
||||
job["user"],
|
||||
self.asrv.vfs.get_perms(vp_chk, job["user"]),
|
||||
int(job["lmod"]),
|
||||
job["size"],
|
||||
job["addr"],
|
||||
@@ -4070,7 +4224,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)
|
||||
|
||||
@@ -4138,11 +4292,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
|
||||
@@ -4211,7 +4365,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)
|
||||
@@ -4221,7 +4375,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
|
||||
|
||||
@@ -4239,7 +4393,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,
|
||||
@@ -4315,6 +4469,18 @@ class Up2k(object):
|
||||
for x in list(self.spools):
|
||||
self._unspool(x)
|
||||
|
||||
for cur in self.cur.values():
|
||||
db = cur.connection
|
||||
try:
|
||||
db.interrupt()
|
||||
except:
|
||||
pass
|
||||
|
||||
cur.close()
|
||||
db.close()
|
||||
|
||||
self.registry = {}
|
||||
|
||||
|
||||
def up2k_chunksize(filesize: int) -> int:
|
||||
chunksize = 1024 * 1024
|
||||
|
||||
@@ -35,6 +35,9 @@ from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, VT100, WINDOWS
|
||||
from .__version__ import S_BUILD_DT, S_VERSION
|
||||
from .stolen import surrogateescape
|
||||
|
||||
ub64dec = base64.urlsafe_b64decode
|
||||
ub64enc = base64.urlsafe_b64encode
|
||||
|
||||
try:
|
||||
from datetime import datetime, timezone
|
||||
|
||||
@@ -155,6 +158,18 @@ else:
|
||||
from urllib import unquote # type: ignore # pylint: disable=no-name-in-module
|
||||
|
||||
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, "::1")
|
||||
HAVE_IPV6 = True
|
||||
except:
|
||||
|
||||
def inet_pton(fam, ip):
|
||||
return socket.inet_aton(ip)
|
||||
|
||||
socket.inet_pton = inet_pton
|
||||
HAVE_IPV6 = False
|
||||
|
||||
|
||||
try:
|
||||
struct.unpack(b">i", b"idgi")
|
||||
spack = struct.pack # type: ignore
|
||||
@@ -228,6 +243,7 @@ IMPLICATIONS = [
|
||||
["e2vu", "e2v"],
|
||||
["e2vp", "e2v"],
|
||||
["e2v", "e2d"],
|
||||
["tftpvv", "tftpv"],
|
||||
["smbw", "smb"],
|
||||
["smb1", "smb"],
|
||||
["smbvvv", "smbvv"],
|
||||
@@ -355,6 +371,21 @@ APPLESAN_TXT = r"/(__MACOS|Icon\r\r)|/\.(_|DS_Store|AppleDouble|LSOverride|Docum
|
||||
APPLESAN_RE = re.compile(APPLESAN_TXT)
|
||||
|
||||
|
||||
HUMANSIZE_UNITS = ("B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB")
|
||||
|
||||
UNHUMANIZE_UNITS = {
|
||||
"b": 1,
|
||||
"k": 1024,
|
||||
"m": 1024 * 1024,
|
||||
"g": 1024 * 1024 * 1024,
|
||||
"t": 1024 * 1024 * 1024 * 1024,
|
||||
"p": 1024 * 1024 * 1024 * 1024 * 1024,
|
||||
"e": 1024 * 1024 * 1024 * 1024 * 1024 * 1024,
|
||||
}
|
||||
|
||||
VF_CAREFUL = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1}
|
||||
|
||||
|
||||
pybin = sys.executable or ""
|
||||
if EXE:
|
||||
pybin = ""
|
||||
@@ -460,13 +491,22 @@ class Daemon(threading.Thread):
|
||||
r: bool = True,
|
||||
ka: Optional[dict[Any, Any]] = None,
|
||||
) -> None:
|
||||
threading.Thread.__init__(
|
||||
self, target=target, name=name, args=a or (), kwargs=ka
|
||||
)
|
||||
threading.Thread.__init__(self, name=name)
|
||||
self.a = a or ()
|
||||
self.ka = ka or {}
|
||||
self.fun = target
|
||||
self.daemon = True
|
||||
if r:
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
if not ANYWIN and not PY2:
|
||||
signal.pthread_sigmask(
|
||||
signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM, signal.SIGUSR1]
|
||||
)
|
||||
|
||||
self.fun(*self.a, **self.ka)
|
||||
|
||||
|
||||
class Netdev(object):
|
||||
def __init__(self, ip: str, idx: int, name: str, desc: str):
|
||||
@@ -759,15 +799,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:
|
||||
@@ -779,7 +850,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:
|
||||
@@ -800,9 +871,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)
|
||||
@@ -827,6 +901,7 @@ class ProgressPrinter(threading.Thread):
|
||||
self.start()
|
||||
|
||||
def run(self) -> None:
|
||||
sigblock()
|
||||
tp = 0
|
||||
msg = None
|
||||
no_stdout = self.args.q
|
||||
@@ -1271,6 +1346,15 @@ def log_thrs(log: Callable[[str, str, int], None], ival: float, name: str) -> No
|
||||
log(name, "\033[0m \033[33m".join(tv), 3)
|
||||
|
||||
|
||||
def sigblock():
|
||||
if ANYWIN or PY2:
|
||||
return
|
||||
|
||||
signal.pthread_sigmask(
|
||||
signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM, signal.SIGUSR1]
|
||||
)
|
||||
|
||||
|
||||
def vol_san(vols: list["VFS"], txt: bytes) -> bytes:
|
||||
txt0 = txt
|
||||
for vol in vols:
|
||||
@@ -1292,10 +1376,11 @@ def vol_san(vols: list["VFS"], txt: bytes) -> bytes:
|
||||
|
||||
def min_ex(max_lines: int = 8, reverse: bool = False) -> str:
|
||||
et, ev, tb = sys.exc_info()
|
||||
stb = traceback.extract_tb(tb)
|
||||
fmt = "%s @ %d <%s>: %s"
|
||||
stb = traceback.extract_tb(tb) if tb else traceback.extract_stack()[:-1]
|
||||
fmt = "%s:%d <%s>: %s"
|
||||
ex = [fmt % (fp.split(os.sep)[-1], ln, fun, txt) for fp, ln, fun, txt in stb]
|
||||
ex.append("[%s] %s" % (et.__name__ if et else "(anonymous)", ev))
|
||||
if et or ev or tb:
|
||||
ex.append("[%s] %s" % (et.__name__ if et else "(anonymous)", ev))
|
||||
return "\n".join(ex[-max_lines:][:: -1 if reverse else 1])
|
||||
|
||||
|
||||
@@ -1748,7 +1833,7 @@ def gencookie(k: str, v: str, r: str, tls: bool, dur: int = 0, txt: str = "") ->
|
||||
|
||||
|
||||
def humansize(sz: float, terse: bool = False) -> str:
|
||||
for unit in ["B", "KiB", "MiB", "GiB", "TiB"]:
|
||||
for unit in HUMANSIZE_UNITS:
|
||||
if sz < 1024:
|
||||
break
|
||||
|
||||
@@ -1769,12 +1854,7 @@ def unhumanize(sz: str) -> int:
|
||||
pass
|
||||
|
||||
mc = sz[-1:].lower()
|
||||
mi = {
|
||||
"k": 1024,
|
||||
"m": 1024 * 1024,
|
||||
"g": 1024 * 1024 * 1024,
|
||||
"t": 1024 * 1024 * 1024 * 1024,
|
||||
}.get(mc, 1)
|
||||
mi = UNHUMANIZE_UNITS.get(mc, 1)
|
||||
return int(float(sz[:-1]) * mi)
|
||||
|
||||
|
||||
@@ -1997,6 +2077,7 @@ def vsplit(vpath: str) -> tuple[str, str]:
|
||||
return vpath.rsplit("/", 1) # type: ignore
|
||||
|
||||
|
||||
# vpath-join
|
||||
def vjoin(rd: str, fn: str) -> str:
|
||||
if rd and fn:
|
||||
return rd + "/" + fn
|
||||
@@ -2004,6 +2085,14 @@ def vjoin(rd: str, fn: str) -> str:
|
||||
return rd or fn
|
||||
|
||||
|
||||
# url-join
|
||||
def ujoin(rd: str, fn: str) -> str:
|
||||
if rd and fn:
|
||||
return rd.rstrip("/") + "/" + fn.lstrip("/")
|
||||
else:
|
||||
return rd or fn
|
||||
|
||||
|
||||
def _w8dec2(txt: bytes) -> str:
|
||||
"""decodes filesystem-bytes to wtf8"""
|
||||
return surrogateescape.decodefilename(txt)
|
||||
@@ -2125,26 +2214,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
|
||||
|
||||
@@ -2152,14 +2244,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()
|
||||
@@ -2169,15 +2266,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
|
||||
@@ -2344,6 +2471,9 @@ def build_netmap(csv: str):
|
||||
csv += ", 127.0.0.0/8, ::1/128" # loopback
|
||||
|
||||
srcs = [x.strip() for x in csv.split(",") if x.strip()]
|
||||
if not HAVE_IPV6:
|
||||
srcs = [x for x in srcs if ":" not in x]
|
||||
|
||||
cidrs = []
|
||||
for zs in srcs:
|
||||
if not zs.endswith("."):
|
||||
@@ -2381,7 +2511,7 @@ def yieldfile(fn: str, bufsz: int) -> Generator[bytes, None, None]:
|
||||
def hashcopy(
|
||||
fin: Generator[bytes, None, None],
|
||||
fout: Union[typing.BinaryIO, typing.IO[Any]],
|
||||
slp: int = 0,
|
||||
slp: float = 0,
|
||||
max_sz: int = 0,
|
||||
) -> tuple[int, str, str]:
|
||||
hashobj = hashlib.sha512()
|
||||
@@ -2409,7 +2539,8 @@ def sendfile_py(
|
||||
f: typing.BinaryIO,
|
||||
s: socket.socket,
|
||||
bufsz: int,
|
||||
slp: int,
|
||||
slp: float,
|
||||
use_poll: bool,
|
||||
) -> int:
|
||||
remains = upper - lower
|
||||
f.seek(lower)
|
||||
@@ -2437,23 +2568,32 @@ def sendfile_kern(
|
||||
f: typing.BinaryIO,
|
||||
s: socket.socket,
|
||||
bufsz: int,
|
||||
slp: int,
|
||||
slp: float,
|
||||
use_poll: bool,
|
||||
) -> int:
|
||||
out_fd = s.fileno()
|
||||
in_fd = f.fileno()
|
||||
ofs = lower
|
||||
stuck = 0.0
|
||||
if use_poll:
|
||||
poll = select.poll()
|
||||
poll.register(out_fd, select.POLLOUT)
|
||||
|
||||
while ofs < upper:
|
||||
stuck = stuck or time.time()
|
||||
try:
|
||||
req = min(2 ** 30, upper - ofs)
|
||||
select.select([], [out_fd], [], 10)
|
||||
if use_poll:
|
||||
poll.poll(10000)
|
||||
else:
|
||||
select.select([], [out_fd], [], 10)
|
||||
n = os.sendfile(out_fd, in_fd, ofs, req)
|
||||
stuck = 0
|
||||
except OSError as ex:
|
||||
# client stopped reading; do another select
|
||||
d = time.time() - stuck
|
||||
if d < 3600 and ex.errno == errno.EWOULDBLOCK:
|
||||
time.sleep(0.02)
|
||||
continue
|
||||
|
||||
n = 0
|
||||
@@ -2597,7 +2737,7 @@ def unescape_cookie(orig: str) -> str:
|
||||
|
||||
def guess_mime(url: str, fallback: str = "application/octet-stream") -> str:
|
||||
try:
|
||||
_, ext = url.rsplit(".", 1)
|
||||
ext = url.rsplit(".", 1)[1].lower()
|
||||
except:
|
||||
return fallback
|
||||
|
||||
@@ -2852,7 +2992,8 @@ def retchk(
|
||||
|
||||
def _parsehook(
|
||||
log: Optional["NamedLogger"], cmd: str
|
||||
) -> tuple[bool, bool, bool, float, dict[str, Any], str]:
|
||||
) -> tuple[str, bool, bool, bool, float, dict[str, Any], list[str]]:
|
||||
areq = ""
|
||||
chk = False
|
||||
fork = False
|
||||
jtxt = False
|
||||
@@ -2877,8 +3018,12 @@ def _parsehook(
|
||||
cap = int(arg[1:]) # 0=none 1=stdout 2=stderr 3=both
|
||||
elif arg.startswith("k"):
|
||||
kill = arg[1:] # [t]ree [m]ain [n]one
|
||||
elif arg.startswith("a"):
|
||||
areq = arg[1:] # required perms
|
||||
elif arg.startswith("i"):
|
||||
pass
|
||||
elif not arg:
|
||||
break
|
||||
else:
|
||||
t = "hook: invalid flag {} in {}"
|
||||
(log or print)(t.format(arg, ocmd))
|
||||
@@ -2905,9 +3050,11 @@ def _parsehook(
|
||||
"capture": cap,
|
||||
}
|
||||
|
||||
cmd = os.path.expandvars(os.path.expanduser(cmd))
|
||||
argv = cmd.split(",") if "," in cmd else [cmd]
|
||||
|
||||
return chk, fork, jtxt, wait, sp_ka, cmd
|
||||
argv[0] = os.path.expandvars(os.path.expanduser(argv[0]))
|
||||
|
||||
return areq, chk, fork, jtxt, wait, sp_ka, argv
|
||||
|
||||
|
||||
def runihook(
|
||||
@@ -2916,10 +3063,9 @@ def runihook(
|
||||
vol: "VFS",
|
||||
ups: list[tuple[str, int, int, str, str, str, int]],
|
||||
) -> bool:
|
||||
ocmd = cmd
|
||||
chk, fork, jtxt, wait, sp_ka, cmd = _parsehook(log, cmd)
|
||||
bcmd = [sfsenc(cmd)]
|
||||
if cmd.endswith(".py"):
|
||||
_, chk, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd)
|
||||
bcmd = [sfsenc(x) for x in acmd]
|
||||
if acmd[0].endswith(".py"):
|
||||
bcmd = [sfsenc(pybin)] + bcmd
|
||||
|
||||
vps = [vjoin(*list(s3dec(x[3], x[4]))) for x in ups]
|
||||
@@ -2944,7 +3090,7 @@ def runihook(
|
||||
|
||||
t0 = time.time()
|
||||
if fork:
|
||||
Daemon(runcmd, ocmd, [bcmd], ka=sp_ka)
|
||||
Daemon(runcmd, cmd, bcmd, ka=sp_ka)
|
||||
else:
|
||||
rc, v, err = runcmd(bcmd, **sp_ka) # type: ignore
|
||||
if chk and rc:
|
||||
@@ -2965,14 +3111,20 @@ def _runhook(
|
||||
vp: str,
|
||||
host: str,
|
||||
uname: str,
|
||||
perms: str,
|
||||
mt: float,
|
||||
sz: int,
|
||||
ip: str,
|
||||
at: float,
|
||||
txt: str,
|
||||
) -> bool:
|
||||
ocmd = cmd
|
||||
chk, fork, jtxt, wait, sp_ka, cmd = _parsehook(log, cmd)
|
||||
areq, chk, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd)
|
||||
if areq:
|
||||
for ch in areq:
|
||||
if ch not in perms:
|
||||
t = "user %s not allowed to run hook %s; need perms %s, have %s"
|
||||
log(t % (uname, cmd, areq, perms))
|
||||
return True # fallthrough to next hook
|
||||
if jtxt:
|
||||
ja = {
|
||||
"ap": ap,
|
||||
@@ -2983,21 +3135,22 @@ def _runhook(
|
||||
"at": at or time.time(),
|
||||
"host": host,
|
||||
"user": uname,
|
||||
"perms": perms,
|
||||
"txt": txt,
|
||||
}
|
||||
arg = json.dumps(ja)
|
||||
else:
|
||||
arg = txt or ap
|
||||
|
||||
acmd = [cmd, arg]
|
||||
if cmd.endswith(".py"):
|
||||
acmd += [arg]
|
||||
if acmd[0].endswith(".py"):
|
||||
acmd = [pybin] + acmd
|
||||
|
||||
bcmd = [fsenc(x) if x == ap else sfsenc(x) for x in acmd]
|
||||
|
||||
t0 = time.time()
|
||||
if fork:
|
||||
Daemon(runcmd, ocmd, [bcmd], ka=sp_ka)
|
||||
Daemon(runcmd, cmd, [bcmd], ka=sp_ka)
|
||||
else:
|
||||
rc, v, err = runcmd(bcmd, **sp_ka) # type: ignore
|
||||
if chk and rc:
|
||||
@@ -3018,6 +3171,7 @@ def runhook(
|
||||
vp: str,
|
||||
host: str,
|
||||
uname: str,
|
||||
perms: str,
|
||||
mt: float,
|
||||
sz: int,
|
||||
ip: str,
|
||||
@@ -3027,7 +3181,7 @@ def runhook(
|
||||
vp = vp.replace("\\", "/")
|
||||
for cmd in cmds:
|
||||
try:
|
||||
if not _runhook(log, cmd, ap, vp, host, uname, mt, sz, ip, at, txt):
|
||||
if not _runhook(log, cmd, ap, vp, host, uname, perms, mt, sz, ip, at, txt):
|
||||
return False
|
||||
except Exception as ex:
|
||||
(log or print)("hook: {}".format(ex))
|
||||
|
||||
@@ -29,6 +29,8 @@ window.baguetteBox = (function () {
|
||||
isOverlayVisible = false,
|
||||
touch = {}, // start-pos
|
||||
touchFlag = false, // busy
|
||||
scrollCSS = ['', ''],
|
||||
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'],
|
||||
@@ -91,6 +93,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();
|
||||
@@ -394,8 +420,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);
|
||||
}
|
||||
|
||||
@@ -452,6 +477,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);
|
||||
@@ -474,6 +500,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);
|
||||
@@ -541,6 +568,12 @@ window.baguetteBox = (function () {
|
||||
|
||||
function showOverlay(chosenImageIndex) {
|
||||
if (options.noScrollbars) {
|
||||
var a = document.documentElement.style.overflowY,
|
||||
b = document.body.style.overflowY;
|
||||
|
||||
if (a != 'hidden' || b != 'scroll')
|
||||
scrollCSS = [a, b];
|
||||
|
||||
document.documentElement.style.overflowY = 'hidden';
|
||||
document.body.style.overflowY = 'scroll';
|
||||
}
|
||||
@@ -584,15 +617,23 @@ window.baguetteBox = (function () {
|
||||
isOverlayVisible = true;
|
||||
}
|
||||
|
||||
function hideOverlay(e) {
|
||||
function hideOverlay(e, dtor) {
|
||||
ev(e);
|
||||
playvid(false);
|
||||
removeFromCache('#files');
|
||||
if (options.noScrollbars) {
|
||||
document.documentElement.style.overflowY = 'auto';
|
||||
document.body.style.overflowY = 'auto';
|
||||
document.documentElement.style.overflowY = scrollCSS[0];
|
||||
document.body.style.overflowY = scrollCSS[1];
|
||||
}
|
||||
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)
|
||||
@@ -600,11 +641,6 @@ window.baguetteBox = (function () {
|
||||
|
||||
sethash('');
|
||||
unbindEvents();
|
||||
try {
|
||||
document.exitFullscreen();
|
||||
isFullscreen = false;
|
||||
}
|
||||
catch (ex) { }
|
||||
|
||||
// Fade out and hide the overlay
|
||||
overlay.className = '';
|
||||
@@ -682,7 +718,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
|
||||
@@ -711,8 +747,11 @@ window.baguetteBox = (function () {
|
||||
});
|
||||
image.setAttribute('src', imageSrc);
|
||||
if (is_vid) {
|
||||
image.volume = clamp(fcfg_get('vol', dvol / 100), 0, 1);
|
||||
image.setAttribute('controls', 'controls');
|
||||
image.onended = vidEnd;
|
||||
image.onplay = function () { show_buttons(1); };
|
||||
image.onpause = function () { show_buttons(); };
|
||||
}
|
||||
image.alt = thumbnailElement ? thumbnailElement.alt || '' : '';
|
||||
if (options.titleTag && imageCaption)
|
||||
@@ -720,6 +759,9 @@ window.baguetteBox = (function () {
|
||||
|
||||
figure.appendChild(image);
|
||||
|
||||
if (is_vid && window.afilt)
|
||||
afilt.apply(undefined, image);
|
||||
|
||||
if (options.async && callback)
|
||||
callback();
|
||||
}
|
||||
@@ -955,6 +997,12 @@ window.baguetteBox = (function () {
|
||||
}
|
||||
}
|
||||
|
||||
function show_buttons(v) {
|
||||
clmod(ebi('bbox-btns'), 'off', v);
|
||||
clmod(btnPrev, 'off', v);
|
||||
clmod(btnNext, 'off', v);
|
||||
}
|
||||
|
||||
function bounceAnimation(direction) {
|
||||
slider.className = options.animation == 'slideIn' ? 'bounce-from-' + direction : 'eog';
|
||||
setTimeout(function () {
|
||||
@@ -1018,9 +1066,7 @@ window.baguetteBox = (function () {
|
||||
if (fx > 0.7)
|
||||
return showNextImage();
|
||||
|
||||
clmod(ebi('bbox-btns'), 'off', 't');
|
||||
clmod(btnPrev, 'off', 't');
|
||||
clmod(btnNext, 'off', 't');
|
||||
show_buttons('t');
|
||||
|
||||
if (Date.now() - ctime <= 500 && !IPHONE)
|
||||
tglfull();
|
||||
@@ -1062,6 +1108,7 @@ window.baguetteBox = (function () {
|
||||
}
|
||||
|
||||
function destroyPlugin() {
|
||||
hideOverlay(undefined, true);
|
||||
unbindEvents();
|
||||
clearCachedData();
|
||||
document.getElementsByTagName('body')[0].removeChild(ebi('bbox-overlay'));
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
--row-alt: #282828;
|
||||
|
||||
--scroll: #eb0;
|
||||
--sel-fg: var(--bg-d1);
|
||||
--sel-bg: var(--fg);
|
||||
|
||||
--a: #fc5;
|
||||
--a-b: #c90;
|
||||
@@ -267,6 +269,7 @@ html.bz {
|
||||
--btn-bg: #202231;
|
||||
--btn-h-bg: #2d2f45;
|
||||
--btn-1-bg: #ba2959;
|
||||
--btn-1-is: #f59;
|
||||
--btn-1-fg: #fff;
|
||||
--btn-1h-fg: #000;
|
||||
--txt-sh: a;
|
||||
@@ -330,6 +333,8 @@ html.c {
|
||||
}
|
||||
html.cz {
|
||||
--bgg: var(--bg-u2);
|
||||
--sel-bg: var(--bg-u5);
|
||||
--sel-fg: var(--fg);
|
||||
--srv-3: #fff;
|
||||
--u2-tab-b1: var(--bg-d3);
|
||||
}
|
||||
@@ -343,6 +348,8 @@ html.cy {
|
||||
--bg-d3: #f77;
|
||||
--bg-d2: #ff0;
|
||||
|
||||
--sel-bg: #f77;
|
||||
|
||||
--a: #fff;
|
||||
--a-hil: #fff;
|
||||
--a-h-bg: #000;
|
||||
@@ -588,8 +595,8 @@ html.dy {
|
||||
line-height: 1.2em;
|
||||
}
|
||||
::selection {
|
||||
color: var(--bg-d1);
|
||||
background: var(--fg);
|
||||
color: var(--sel-fg);
|
||||
background: var(--sel-bg);
|
||||
text-shadow: none;
|
||||
}
|
||||
html,body,tr,th,td,#files,a {
|
||||
@@ -699,12 +706,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 {
|
||||
@@ -715,7 +722,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 {
|
||||
@@ -744,6 +751,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);
|
||||
}
|
||||
@@ -1600,6 +1616,7 @@ html {
|
||||
padding: .2em .4em;
|
||||
font-size: 1.2em;
|
||||
margin: .2em;
|
||||
display: inline-block;
|
||||
white-space: pre;
|
||||
position: relative;
|
||||
top: -.12em;
|
||||
@@ -1736,6 +1753,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 {
|
||||
@@ -1773,9 +1791,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;
|
||||
@@ -3046,6 +3061,14 @@ html.b #ggrid>a {
|
||||
html.b .btn {
|
||||
top: -.1em;
|
||||
}
|
||||
html.b .btn,
|
||||
html.b #u2conf a.b,
|
||||
html.b #u2conf input[type="checkbox"]:not(:checked)+label {
|
||||
box-shadow: 0 .05em 0 var(--bg-d3) inset;
|
||||
}
|
||||
html.b .tgl.btn.on {
|
||||
box-shadow: 0 .05em 0 var(--btn-1-is) inset;
|
||||
}
|
||||
html.b #op_up2k.srch sup {
|
||||
color: #fc0;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>{{ title }}</title>
|
||||
<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">
|
||||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/browser.css?_={{ ts }}">
|
||||
{{ html_head }}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
<title>📝 {{ title }}</title>
|
||||
<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">
|
||||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
<link rel="stylesheet" href="{{ r }}/.cpr/md.css?_={{ ts }}">
|
||||
{%- if edit %}
|
||||
|
||||
@@ -607,10 +607,10 @@ function md_newline() {
|
||||
var s = linebounds(true),
|
||||
ln = s.md.substring(s.n1, s.n2),
|
||||
m1 = /^( *)([0-9]+)(\. +)/.exec(ln),
|
||||
m2 = /^[ \t>+-]*(\* )?/.exec(ln),
|
||||
m2 = /^[ \t]*[>+*-]{0,2}[ \t]/.exec(ln),
|
||||
drop = dom_src.selectionEnd - dom_src.selectionStart;
|
||||
|
||||
var pre = m2[0];
|
||||
var pre = m2 ? m2[0] : '';
|
||||
if (m1 !== null)
|
||||
pre = m1[1] + (parseInt(m1[2]) + 1) + m1[3];
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<title>📝 {{ title }}</title>
|
||||
<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">
|
||||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<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 }}">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>{{ s_doctitle }}</title>
|
||||
<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">
|
||||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/msg.css?_={{ ts }}">
|
||||
{{ html_head }}
|
||||
</head>
|
||||
|
||||
@@ -190,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;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>{{ s_doctitle }}</title>
|
||||
<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">
|
||||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/splash.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
{{ html_head }}
|
||||
@@ -94,7 +94,8 @@
|
||||
<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="hidden" name="uhash" id="uhash" value="x" />
|
||||
<input type="submit" value="Login" />
|
||||
{% if ahttps %}
|
||||
<a id="w" href="{{ ahttps }}">switch to https</a>
|
||||
|
||||
@@ -34,8 +34,14 @@ var Ls = {
|
||||
"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!",
|
||||
}
|
||||
},
|
||||
d = Ls[sread("cpp_lang", ["eng", "nor"]) || lang] || Ls.eng || Ls.nor;
|
||||
};
|
||||
|
||||
var LANGS = ["eng", "nor"];
|
||||
|
||||
if (window.langmod)
|
||||
langmod();
|
||||
|
||||
var d = Ls[sread("cpp_lang", LANGS) || lang] || Ls.eng || Ls.nor;
|
||||
|
||||
for (var k in (d || {})) {
|
||||
var f = k.slice(-1),
|
||||
@@ -66,3 +72,5 @@ if (!ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight)
|
||||
o = ebi('u');
|
||||
if (o && /[0-9]+$/.exec(o.innerHTML))
|
||||
o.innerHTML = shumantime(o.innerHTML);
|
||||
|
||||
ebi('uhash').value = '' + location.hash;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>{{ s_doctitle }}</title>
|
||||
<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">
|
||||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/splash.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
<style>ul{padding-left:1.3em}li{margin:.4em 0}</style>
|
||||
@@ -56,7 +56,7 @@
|
||||
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
|
||||
<li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<p>if you want to use the native WebDAV client in windows instead (slow and buggy), first run <a href="{{ r }}/.cpr/a/webdav-cfg.bat">webdav-cfg.bat</a> to remove the 47 MiB filesize limit (also fixes latency and password login), then connect:</p>
|
||||
<pre>
|
||||
net use <b>w:</b> http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} k /user:<b>{{ pw }}</b>{% endif %}
|
||||
@@ -64,16 +64,7 @@
|
||||
</div>
|
||||
|
||||
<div class="os lin">
|
||||
<pre>
|
||||
yum install davfs2
|
||||
{% if accs %}printf '%s\n' <b>{{ pw }}</b> k | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b>
|
||||
</pre>
|
||||
<p>make it automount on boot:</p>
|
||||
<pre>
|
||||
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>{{ pw }}</b> k" >> /etc/davfs2/secrets
|
||||
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b> davfs rw,user,uid=1000,noauto 0 0" >> /etc/fstab
|
||||
</pre>
|
||||
<p>or you can use rclone instead, which is much slower but doesn't require root (plus it keeps lastmodified on upload):</p>
|
||||
<p>rclone (v1.63 or later) is recommended:</p>
|
||||
<pre>
|
||||
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>mp</b>
|
||||
@@ -85,6 +76,16 @@
|
||||
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
|
||||
<li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li>
|
||||
</ul>
|
||||
<p>alternatively use davfs2 (requires root, is slower, forgets lastmodified-timestamp on upload):</p>
|
||||
<pre>
|
||||
yum install davfs2
|
||||
{% if accs %}printf '%s\n' <b>{{ pw }}</b> k | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b>
|
||||
</pre>
|
||||
<p>make davfs2 automount on boot:</p>
|
||||
<pre>
|
||||
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>{{ pw }}</b> k" >> /etc/davfs2/secrets
|
||||
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b> davfs rw,user,uid=1000,noauto 0 0" >> /etc/fstab
|
||||
</pre>
|
||||
<p>or the emergency alternative (gnome/gui-only):</p>
|
||||
<!-- gnome-bug: ignores vp -->
|
||||
<pre>
|
||||
@@ -104,7 +105,7 @@
|
||||
<pre>
|
||||
http{{ s }}://k:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}
|
||||
</pre>
|
||||
|
||||
|
||||
{% if s %}
|
||||
<p><em>replace <code>https</code> with <code>http</code> if it doesn't work</em></p>
|
||||
{% endif %}
|
||||
|
||||
@@ -184,6 +184,7 @@ html {
|
||||
padding: 1.5em 2em;
|
||||
border-width: .5em 0;
|
||||
}
|
||||
.logue code,
|
||||
#modalc code,
|
||||
#tt code {
|
||||
color: #eee;
|
||||
@@ -264,7 +265,11 @@ html.y #tth {
|
||||
box-shadow: 0 .3em 3em rgba(0,0,0,0.5);
|
||||
max-width: 50em;
|
||||
max-height: 30em;
|
||||
overflow: auto;
|
||||
overflow-x: auto;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
#modalc.yk {
|
||||
overflow-y: auto;
|
||||
}
|
||||
#modalc td {
|
||||
text-align: unset;
|
||||
|
||||
@@ -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 {
|
||||
@@ -658,7 +658,9 @@ function Donut(uc, st) {
|
||||
}
|
||||
|
||||
function pos() {
|
||||
return uc.fsearch ? Math.max(st.bytes.hashed, st.bytes.finished) : st.bytes.finished;
|
||||
return uc.fsearch ?
|
||||
Math.max(st.bytes.hashed, st.bytes.finished) :
|
||||
st.bytes.inflight + st.bytes.finished;
|
||||
}
|
||||
|
||||
r.on = function (ya) {
|
||||
@@ -717,7 +719,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 +781,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) {
|
||||
@@ -853,6 +855,7 @@ function up2k_init(subtle) {
|
||||
setmsg(suggest_up2k, 'msg');
|
||||
|
||||
var parallel_uploads = ebi('nthread').value = icfg_get('nthread', u2j),
|
||||
stitch_tgt = ebi('u2szg').value = icfg_get('u2sz', u2sz.split(',')[1]),
|
||||
uc = {},
|
||||
fdom_ctr = 0,
|
||||
biggest_file = 0;
|
||||
@@ -869,7 +872,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);
|
||||
|
||||
@@ -1207,7 +1210,7 @@ function up2k_init(subtle) {
|
||||
match = false;
|
||||
|
||||
if (match) {
|
||||
var msg = ['directory iterator got stuck on the following {0} items; good chance your browser is about to spinlock:<ul>'.format(missing.length)];
|
||||
var msg = ['directory iterator got stuck trying to access the following {0} items; will skip:<ul>'.format(missing.length)];
|
||||
for (var a = 0; a < Math.min(20, missing.length); a++)
|
||||
msg.push('<li>' + esc(missing[a]) + '</li>');
|
||||
|
||||
@@ -1347,9 +1350,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");
|
||||
}
|
||||
@@ -1736,6 +1739,11 @@ function up2k_init(subtle) {
|
||||
}
|
||||
}
|
||||
|
||||
if (st.bytes.inflight && (st.bytes.inflight < 0 || !st.busy.upload.length)) {
|
||||
console.log('insane inflight ' + st.bytes.inflight);
|
||||
st.bytes.inflight = 0;
|
||||
}
|
||||
|
||||
var mou_ikkai = false;
|
||||
|
||||
if (st.busy.handshake.length &&
|
||||
@@ -2178,7 +2186,7 @@ function up2k_init(subtle) {
|
||||
st.busy.head.push(t);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onerror = function () {
|
||||
xhr.onerror = xhr.ontimeout = function () {
|
||||
console.log('head onerror, retrying', t.name, t);
|
||||
if (!toast.visible)
|
||||
toast.warn(9.98, L.u_enethd + "\n\nfile: " + t.name, t);
|
||||
@@ -2222,6 +2230,7 @@ function up2k_init(subtle) {
|
||||
try { orz(e); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); }
|
||||
};
|
||||
|
||||
xhr.timeout = 34000;
|
||||
xhr.open('HEAD', t.purl + uricom_enc(t.name), true);
|
||||
xhr.send();
|
||||
}
|
||||
@@ -2247,7 +2256,7 @@ function up2k_init(subtle) {
|
||||
console.log("sending keepalive handshake", t.name, t);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onerror = function () {
|
||||
xhr.onerror = xhr.ontimeout = function () {
|
||||
if (t.t_busied != me) // t.done ok
|
||||
return console.log('zombie handshake onerror', t.name, t);
|
||||
|
||||
@@ -2374,11 +2383,23 @@ function up2k_init(subtle) {
|
||||
var arr = st.todo.upload,
|
||||
sort = arr.length && arr[arr.length - 1].nfile > t.n;
|
||||
|
||||
for (var a = 0; a < t.postlist.length; a++)
|
||||
for (var a = 0; a < t.postlist.length; a++) {
|
||||
var nparts = [], tbytes = 0, stitch = stitch_tgt;
|
||||
if (t.nojoin && t.nojoin - t.postlist.length < 6)
|
||||
stitch = 1;
|
||||
|
||||
--a;
|
||||
for (var b = 0; b < stitch; b++) {
|
||||
nparts.push(t.postlist[++a]);
|
||||
tbytes += chunksize;
|
||||
if (tbytes + chunksize > stitch * 1024 * 1024 || t.postlist[a + 1] - t.postlist[a] !== 1)
|
||||
break;
|
||||
}
|
||||
arr.push({
|
||||
'nfile': t.n,
|
||||
'npart': t.postlist[a]
|
||||
'nparts': nparts
|
||||
});
|
||||
}
|
||||
|
||||
msg = null;
|
||||
done = false;
|
||||
@@ -2387,7 +2408,7 @@ function up2k_init(subtle) {
|
||||
arr.sort(function (a, b) {
|
||||
return a.nfile < b.nfile ? -1 :
|
||||
/* */ a.nfile > b.nfile ? 1 :
|
||||
a.npart < b.npart ? -1 : 1;
|
||||
/* */ a.nparts[0] < b.nparts[0] ? -1 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2493,6 +2514,7 @@ function up2k_init(subtle) {
|
||||
|
||||
xhr.open('POST', t.purl, true);
|
||||
xhr.responseType = 'text';
|
||||
xhr.timeout = 42000;
|
||||
xhr.send(JSON.stringify(req));
|
||||
}
|
||||
|
||||
@@ -2534,7 +2556,10 @@ function up2k_init(subtle) {
|
||||
function exec_upload() {
|
||||
var upt = st.todo.upload.shift(),
|
||||
t = st.files[upt.nfile],
|
||||
npart = upt.npart,
|
||||
nparts = upt.nparts,
|
||||
pcar = nparts[0],
|
||||
pcdr = nparts[nparts.length - 1],
|
||||
snpart = pcar == pcdr ? pcar : ('' + pcar + '~' + pcdr),
|
||||
tries = 0;
|
||||
|
||||
if (t.done)
|
||||
@@ -2549,8 +2574,8 @@ function up2k_init(subtle) {
|
||||
pvis.seth(t.n, 1, "🚀 send");
|
||||
|
||||
var chunksize = get_chunksize(t.size),
|
||||
car = npart * chunksize,
|
||||
cdr = car + chunksize;
|
||||
car = pcar * chunksize,
|
||||
cdr = (pcdr + 1) * chunksize;
|
||||
|
||||
if (cdr >= t.size)
|
||||
cdr = t.size;
|
||||
@@ -2560,14 +2585,19 @@ function up2k_init(subtle) {
|
||||
var txt = unpre((xhr.response && xhr.response.err) || xhr.responseText);
|
||||
if (txt.indexOf('upload blocked by x') + 1) {
|
||||
apop(st.busy.upload, upt);
|
||||
apop(t.postlist, npart);
|
||||
for (var a = pcar; a <= pcdr; a++)
|
||||
apop(t.postlist, a);
|
||||
pvis.seth(t.n, 1, "ERROR");
|
||||
pvis.seth(t.n, 2, txt.split(/\n/)[0]);
|
||||
pvis.move(t.n, 'ng');
|
||||
return;
|
||||
}
|
||||
if (xhr.status == 200) {
|
||||
pvis.prog(t, npart, cdr - car);
|
||||
var bdone = cdr - car;
|
||||
for (var a = pcar; a <= pcdr; a++) {
|
||||
pvis.prog(t, a, Math.min(bdone, chunksize));
|
||||
bdone -= chunksize;
|
||||
}
|
||||
st.bytes.finished += cdr - car;
|
||||
st.bytes.uploaded += cdr - car;
|
||||
t.bytes_uploaded += cdr - car;
|
||||
@@ -2576,18 +2606,21 @@ function up2k_init(subtle) {
|
||||
}
|
||||
else if (txt.indexOf('already got that') + 1 ||
|
||||
txt.indexOf('already being written') + 1) {
|
||||
console.log("ignoring dupe-segment error", t.name, t);
|
||||
t.nojoin = t.postlist.length;
|
||||
console.log("ignoring dupe-segment with backoff", t.nojoin, t.name, t);
|
||||
if (!toast.visible && st.todo.upload.length < 4)
|
||||
toast.msg(10, L.u_cbusy);
|
||||
}
|
||||
else {
|
||||
xhrchk(xhr, L.u_cuerr2.format(npart, Math.ceil(t.size / chunksize), t.name), "404, target folder not found (???)", "warn", t);
|
||||
|
||||
xhrchk(xhr, L.u_cuerr2.format(snpart, Math.ceil(t.size / chunksize), t.name), "404, target folder not found (???)", "warn", t);
|
||||
chill(t);
|
||||
}
|
||||
orz2(xhr);
|
||||
}
|
||||
var orz2 = function (xhr) {
|
||||
apop(st.busy.upload, upt);
|
||||
apop(t.postlist, npart);
|
||||
for (var a = pcar; a <= pcdr; a++)
|
||||
apop(t.postlist, a);
|
||||
if (!t.postlist.length) {
|
||||
t.t_uploaded = Date.now();
|
||||
pvis.seth(t.n, 1, 'verifying');
|
||||
@@ -2601,28 +2634,38 @@ function up2k_init(subtle) {
|
||||
btot = Math.floor(st.bytes.total / 1024 / 1024);
|
||||
|
||||
xhr.upload.onprogress = function (xev) {
|
||||
var nb = xev.loaded;
|
||||
st.bytes.inflight += nb - xhr.bsent;
|
||||
var nb = xev.loaded,
|
||||
db = nb - xhr.bsent;
|
||||
|
||||
if (!db)
|
||||
return;
|
||||
|
||||
st.bytes.inflight += db;
|
||||
xhr.bsent = nb;
|
||||
pvis.prog(t, npart, nb);
|
||||
xhr.timeout = 64000 + Date.now() - xhr.t0;
|
||||
pvis.prog(t, pcar, nb);
|
||||
};
|
||||
xhr.onload = function (xev) {
|
||||
try { orz(xhr); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); }
|
||||
};
|
||||
xhr.onerror = function (xev) {
|
||||
xhr.onerror = xhr.ontimeout = function (xev) {
|
||||
if (crashed)
|
||||
return;
|
||||
|
||||
st.bytes.inflight -= (xhr.bsent || 0);
|
||||
|
||||
if (!toast.visible)
|
||||
toast.warn(9.98, L.u_cuerr.format(npart, Math.ceil(t.size / chunksize), t.name), t);
|
||||
toast.warn(9.98, L.u_cuerr.format(snpart, Math.ceil(t.size / chunksize), t.name), t);
|
||||
|
||||
console.log('chunkpit onerror,', ++tries, t.name, t);
|
||||
orz2(xhr);
|
||||
};
|
||||
var chashes = [];
|
||||
for (var a = pcar; a <= pcdr; a++)
|
||||
chashes.push(t.hash[a]);
|
||||
|
||||
xhr.open('POST', t.purl, true);
|
||||
xhr.setRequestHeader("X-Up2k-Hash", t.hash[npart]);
|
||||
xhr.setRequestHeader("X-Up2k-Hash", chashes.join(","));
|
||||
xhr.setRequestHeader("X-Up2k-Wark", t.wark);
|
||||
xhr.setRequestHeader("X-Up2k-Stat", "{0}/{1}/{2}/{3} {4}/{5} {6}".format(
|
||||
pvis.ctr.ok, pvis.ctr.ng, pvis.ctr.bz, pvis.ctr.q, btot, btot - bfin,
|
||||
@@ -2632,6 +2675,8 @@ function up2k_init(subtle) {
|
||||
xhr.overrideMimeType('Content-Type', 'application/octet-stream');
|
||||
|
||||
xhr.bsent = 0;
|
||||
xhr.t0 = Date.now();
|
||||
xhr.timeout = 42000;
|
||||
xhr.responseType = 'text';
|
||||
xhr.send(t.fobj.slice(car, cdr));
|
||||
}
|
||||
@@ -2732,13 +2777,34 @@ function up2k_init(subtle) {
|
||||
if (parallel_uploads > 16)
|
||||
parallel_uploads = 16;
|
||||
|
||||
if (parallel_uploads > 7)
|
||||
if (parallel_uploads > 6)
|
||||
toast.warn(10, L.u_maxconn);
|
||||
else if (toast.txt == L.u_maxconn)
|
||||
toast.hide();
|
||||
|
||||
obj.value = parallel_uploads;
|
||||
bumpthread({ "target": 1 });
|
||||
}
|
||||
|
||||
var read_u2sz = function () {
|
||||
var el = ebi('u2szg'), n = parseInt(el.value), dv = u2sz.split(',');
|
||||
stitch_tgt = n = (
|
||||
isNaN(n) ? dv[1] :
|
||||
n < dv[0] ? dv[0] :
|
||||
n > dv[2] ? dv[2] : n
|
||||
);
|
||||
if (n == dv[1]) sdrop('u2sz'); else swrite('u2sz', n);
|
||||
if (el.value != n) el.value = n;
|
||||
};
|
||||
ebi('u2szg').addEventListener('blur', read_u2sz);
|
||||
ebi('u2szg').onkeydown = function (e) {
|
||||
if (anymod(e)) return;
|
||||
var n = e.code == 'ArrowUp' ? 1 : e.code == 'ArrowDown' ? -1 : 0;
|
||||
if (!n) return;
|
||||
this.value = parseInt(this.value) + n;
|
||||
read_u2sz();
|
||||
}
|
||||
|
||||
function tgl_fsearch() {
|
||||
set_fsearch(!uc.fsearch);
|
||||
}
|
||||
@@ -2950,7 +3016,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);
|
||||
}
|
||||
@@ -2967,7 +3033,7 @@ function up2k_init(subtle) {
|
||||
nopenag();
|
||||
}
|
||||
|
||||
if (!window.Notification || !HTTPS)
|
||||
if (!Notification || !HTTPS)
|
||||
return nopenag();
|
||||
|
||||
if (en && Notification.permission == 'default')
|
||||
@@ -2989,7 +3055,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,13 +127,13 @@ if ((document.location + '').indexOf(',rej,') + 1)
|
||||
|
||||
try {
|
||||
console.hist = [];
|
||||
var CMAXHIST = 100;
|
||||
var CMAXHIST = MOBILE ? 9000 : 44000;
|
||||
var hook = function (t) {
|
||||
var orig = console[t].bind(console),
|
||||
cfun = function () {
|
||||
console.hist.push(Date.now() + ' ' + t + ': ' + Array.from(arguments).join(', '));
|
||||
if (console.hist.length > CMAXHIST)
|
||||
console.hist = console.hist.slice(CMAXHIST / 2);
|
||||
console.hist = console.hist.slice(CMAXHIST / 4);
|
||||
|
||||
orig.apply(console, arguments);
|
||||
};
|
||||
@@ -182,7 +179,7 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
|
||||
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;
|
||||
@@ -740,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);
|
||||
@@ -1390,10 +1396,10 @@ var tt = (function () {
|
||||
o = ctr.querySelectorAll('*[tt]');
|
||||
|
||||
for (var a = o.length - 1; a >= 0; a--) {
|
||||
o[a].onfocus = _cshow;
|
||||
o[a].onblur = _hide;
|
||||
o[a].onmouseenter = _dshow;
|
||||
o[a].onmouseleave = _hide;
|
||||
o[a].addEventListener('focus', _cshow);
|
||||
o[a].addEventListener('blur', _hide);
|
||||
o[a].addEventListener('mouseenter', _dshow);
|
||||
o[a].addEventListener('mouseleave', _hide);
|
||||
}
|
||||
r.hide();
|
||||
}
|
||||
@@ -1469,7 +1475,7 @@ var toast = (function () {
|
||||
clmod(obj, 'vis');
|
||||
r.visible = false;
|
||||
r.tag = obj;
|
||||
if (!window.WebAssembly)
|
||||
if (!WebAssembly)
|
||||
te = setTimeout(function () {
|
||||
obj.className = 'hide';
|
||||
}, 500);
|
||||
@@ -1530,9 +1536,12 @@ var modal = (function () {
|
||||
var r = {},
|
||||
q = [],
|
||||
o = null,
|
||||
scrolling = null,
|
||||
cb_up = null,
|
||||
cb_ok = null,
|
||||
cb_ng = null,
|
||||
sel_0 = 0,
|
||||
sel_1 = 0,
|
||||
tok, tng, prim, sec, ok_cancel;
|
||||
|
||||
r.load = function () {
|
||||
@@ -1548,6 +1557,7 @@ var modal = (function () {
|
||||
r.nofocus = 0;
|
||||
|
||||
r.show = function (html) {
|
||||
tt.hide();
|
||||
o = mknod('div', 'modal');
|
||||
o.innerHTML = '<table><tr><td><div id="modalc">' + html + '</div></td></tr></table>';
|
||||
document.body.appendChild(o);
|
||||
@@ -1566,11 +1576,12 @@ var modal = (function () {
|
||||
(inp || a).focus();
|
||||
if (inp)
|
||||
setTimeout(function () {
|
||||
inp.setSelectionRange(0, inp.value.length, "forward");
|
||||
inp.setSelectionRange(sel_0, sel_1, "forward");
|
||||
}, 0);
|
||||
|
||||
document.addEventListener('focus', onfocus);
|
||||
document.addEventListener('selectionchange', onselch);
|
||||
timer.add(scrollchk, 1);
|
||||
timer.add(onfocus);
|
||||
if (cb_up)
|
||||
setTimeout(cb_up, 1);
|
||||
@@ -1578,6 +1589,8 @@ var modal = (function () {
|
||||
|
||||
r.hide = function () {
|
||||
timer.rm(onfocus);
|
||||
timer.rm(scrollchk);
|
||||
scrolling = null;
|
||||
try {
|
||||
ebi('modal-ok').removeEventListener('blur', onblur);
|
||||
}
|
||||
@@ -1596,13 +1609,28 @@ var modal = (function () {
|
||||
r.hide();
|
||||
if (cb_ok)
|
||||
cb_ok(v);
|
||||
}
|
||||
};
|
||||
var ng = function (e) {
|
||||
ev(e);
|
||||
r.hide();
|
||||
if (cb_ng)
|
||||
cb_ng(null);
|
||||
}
|
||||
};
|
||||
|
||||
var scrollchk = function () {
|
||||
if (scrolling === true)
|
||||
return;
|
||||
|
||||
var o = ebi('modalc'),
|
||||
vis = o.offsetHeight,
|
||||
all = o.scrollHeight,
|
||||
nsc = 8 + vis < all;
|
||||
|
||||
if (scrolling !== nsc)
|
||||
clmod(o, 'yk', !nsc);
|
||||
|
||||
scrolling = nsc;
|
||||
};
|
||||
|
||||
var onselch = function () {
|
||||
try {
|
||||
@@ -1689,16 +1717,18 @@ var modal = (function () {
|
||||
r.show(html);
|
||||
}
|
||||
|
||||
r.prompt = function (html, v, cok, cng, fun) {
|
||||
r.prompt = function (html, v, cok, cng, fun, so0, so1) {
|
||||
q.push(function () {
|
||||
_prompt(lf2br(html), v, cok, cng, fun);
|
||||
_prompt(lf2br(html), v, cok, cng, fun, so0, so1);
|
||||
});
|
||||
next();
|
||||
}
|
||||
var _prompt = function (html, v, cok, cng, fun) {
|
||||
var _prompt = function (html, v, cok, cng, fun, so0, so1) {
|
||||
cb_ok = cok;
|
||||
cb_ng = cng === undefined ? cok : null;
|
||||
cb_up = fun;
|
||||
sel_0 = so0 || 0;
|
||||
sel_1 = so1 === undefined ? v.length : so1;
|
||||
html += '<input id="modali" type="text" ' + NOAC + ' /><div id="modalb">' + ok_cancel + '</div>';
|
||||
r.show(html);
|
||||
|
||||
@@ -1885,7 +1915,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);
|
||||
}
|
||||
@@ -2014,6 +2044,9 @@ function xhrchk(xhr, prefix, e404, lvl, tag) {
|
||||
if (xhr.status == 404)
|
||||
return toast.err(0, prefix + e404 + suf, tag);
|
||||
|
||||
if (!xhr.status && !errtxt)
|
||||
return toast.err(0, prefix + L.xhr0);
|
||||
|
||||
if (is_cf && (xhr.status == 403 || xhr.status == 503)) {
|
||||
var now = Date.now(), td = now - cf_cha_t;
|
||||
if (td < 15000)
|
||||
|
||||
@@ -5,9 +5,6 @@ a living list of upcoming features / fixes / changes, very roughly in order of p
|
||||
* maybe resumable downloads (chrome-only, jank api)
|
||||
* maybe checksum validation (return sha512 of requested range in responses, and probably also warks)
|
||||
|
||||
* [github issue #64](https://github.com/9001/copyparty/issues/64) - dirkeys 2nd season
|
||||
* popular feature request, finally time to refactor browser.js i suppose...
|
||||
|
||||
* [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/)
|
||||
|
||||
|
||||
@@ -1,8 +1,339 @@
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0722-2323 `v1.13.5` american sized
|
||||
|
||||
## new features
|
||||
|
||||
* long-distance uploads are now **twice as fast** on average 132a8350
|
||||
* boost tcp windowsize scaling by stitching together smaller chunks into bigger chonks so they fly better across the atlantic
|
||||
* i'm not kidding, on the two routes we've tested this on we gained 1.6x / 160% (from US-West to Finland) and **2.6x / 260%** (Norway to US-East)
|
||||
* files that are between 4 MiB and 256 MiB see the biggest improvement; 70% faster <= 768 MiB, 40% <= 1.5 GiB, 10% <= 6G
|
||||
* if this turns out to be buggy, disable it serverside with `--u2sz 1,1,1` or clientside in the browser-ui: `[⚙️]` -> `up2k switches` -> change `64` to `1`
|
||||
* u2c.py (CLI uploader): support stitching (☝️) + print a summary with hashing and upload speeds 987bce21
|
||||
* video files can play as audio 53f1e3c9
|
||||
* audio is extracted serverside to avoid wasting bandwidth
|
||||
* extraction is lossy (converted to opus or mp3 depending on browser)
|
||||
* togglebutton `🎧` in the gridview toolbar to enable/disable
|
||||
* new hook: [into-the-cache-it-goes.py](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks#after-upload) d26a944d
|
||||
* avoids a cloudflare bug (race condition?) where it will send truncated files to visitors on the very first load if several people simultaneously access a file that hasn't been viewed before
|
||||
|
||||
## bugfixes
|
||||
|
||||
* inline markdown/logues rendered black-on-black in firefox 54 and some other browsers from 2017 and older eeef8091
|
||||
* unintuitive folder thumbnail selection if folder contains both `Cover.jpg` and `cover.jpg` f955d2bd
|
||||
* the gridview toolbar got undocked after viewing a pic/vid dc449bf8
|
||||
|
||||
## other changes
|
||||
|
||||
* #90 recommend rclone in favor of davfs2 ef0ecf87
|
||||
* improved some error messages e565ad5f
|
||||
* added helptext exporters to generate the online [html](https://ocv.me/copyparty/helptext.html) and [txt](https://ocv.me/copyparty/helptext.txt) editions 59533990
|
||||
* mention that cloudflare is incompatible with uploading files larger than 383.9 GiB
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0716-0457 `v1.13.4` descript.ion
|
||||
|
||||
## new features
|
||||
|
||||
* "medialinks"; instead of the usual hotlink, the basic-uploader (as used by sharex and such) can return a link that opens the file in the media viewer c9281f89
|
||||
* enable for all uploads with volflag `medialinks`, or just for one upload by adding `?media` to the post url
|
||||
* thumbnails are now fully compatible with dirkeys/filekeys 52e06226
|
||||
* `--th-covers` will respect filename order, selecting the first matching filename as the folder thumbnail 1cdb1702
|
||||
* new hook: [bittorrent downloader](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks#on-message) bd3b3863 803e1565
|
||||
* hooks: d749683d
|
||||
* can be restricted to only run when user has specific permissions
|
||||
* user permissions are also included in the json message to the hook
|
||||
* new syntax to prepend args to the hook's command
|
||||
* (all this will be better documented after some additional upcoming hook-related features, see `--help-hooks` for now)
|
||||
* support `descript.ion` usenet metadata; will parse and render into directory listings when possible 927c3bce
|
||||
* directory listings are now 2% slower, eh who's keeping count anyways
|
||||
* tftp-server: 45259251
|
||||
* improved support for buggy clients
|
||||
* improved ipv6 support, especially on macos
|
||||
* improved robustness on unreliable networks
|
||||
* #85 new option `--gsel` to default-enable the client setting to select files by ctrl-clicking them in the grid 9a87ee2f
|
||||
* music player: set audio volume by scrollwheel 36d6d29a
|
||||
|
||||
## bugfixes
|
||||
|
||||
* race-the-beam (downloading an unfinished upload) could get interrupted near the end, requiring a manual resume in the browser's download manager to finish f37187a0
|
||||
* ftp-server: when accessing the root folder of servers without a root folder, it could mention inaccessible folders 84e8e1dd
|
||||
* ftp-server: uploads will automatically replace existing files if user has delete perms 0a9f4c60
|
||||
* windows 2000 expects this behavior, otherwise it'll freak out and delete stuff and then not actually upload it, nice
|
||||
* new option `--ftp-no-ow` restores old default behavior of rejecting upload if target filename exists
|
||||
* music player:
|
||||
* stop trying to recover from a corrupted file if the user already fixed it manually 55a011b9
|
||||
* support downloading the currently playing song regardless of current folder c06aa683
|
||||
* music player preloader: db6059e1
|
||||
* stop searching after 5 folders of nothing
|
||||
* don't crash playback by walking into error-pages
|
||||
* `--og` (rich discord embeds) was incompatible with viewing markdown docs d75a2c77
|
||||
* `--cgen` (configfile generator) much less jank d5de3f2f
|
||||
|
||||
## other changes
|
||||
|
||||
* mention that HTTP/2 is still usually slower than HTTP/1.1 dfe7f1d9
|
||||
* give up much sooner if a client is supposed to send a request body but isn't c549f367
|
||||
* support running copyparty as a server on windows 2000 and winXP 8c73e0cb 2fd12a83
|
||||
* updated deps 6e58514b
|
||||
* copyparty.exe: python 3.12, pillow 10.4, pyinstaller 6.9
|
||||
* dompurify 3.1.6
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0601-2324 `v1.13.3` 700+
|
||||
|
||||
## new features
|
||||
|
||||
* keep tags when transcoding music to opus/mp3 07ea629c
|
||||
* useful for batch-downloading folders with [on-the-fly transcoding](https://github.com/9001/copyparty#zip-downloads)
|
||||
* excessively large tags will be individually dropped (traktor beatmaps, cover-art, xmp)
|
||||
|
||||
## bugfixes
|
||||
|
||||
* optimization for large amounts (700+) of tcp connections / clients 07b2bf11
|
||||
* `select()` was used for non-https downloads and mdns/ssdp initialization, which would start spinning at more than 1024 FDs, so now they `poll()` when possible (so not on windows)
|
||||
* default max number of connections on windows was lowered to 486 since windows maxes out at 512 FDs
|
||||
* the markdown editor autoindent would duplicate `<hr>` 692175f5
|
||||
|
||||
## other changes
|
||||
|
||||
* #83: more intuitive behavior for `--df` and the `df` volflag 5ad65450
|
||||
* print helpful warning if OS restrictions make it impossible to persist config b629d18d
|
||||
* censor filesystem paths in the download-as-zip error summary 5919607a
|
||||
* `u2c.exe`: explain that https is disabled bef96176
|
||||
* ux: 60c96f99
|
||||
* hide lightbox buttons when a video is playing
|
||||
* move audio seekbar text down a bit so it hides less of the waveform and minute-markers
|
||||
* updated dompurify to 3.1.5 f00b9394
|
||||
* updated docker images to alpine 3.20
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0510-1431 `v1.13.2` s3xmodit.zip
|
||||
|
||||
## new features
|
||||
|
||||
* play [compressed](https://a.ocv.me/pub/demo/music/chiptunes/compressed/#af-99f0c0e4) s3xmodit chiptunes/modules c0466279
|
||||
* can now read gz/xz/zip-compressed s3m/xm/mod/it songs
|
||||
* new filetypes supported: mdz, mdgz, mdxz, s3z, s3gz, s3xz, xmz, xmgz, xmxz, itz, itgz, itxz
|
||||
* and if you need to fit even more tracks on the mixtape, [try mo3](https://a.ocv.me/pub/demo/music/chiptunes/compressed/#af-0bc9b877)
|
||||
* option to batch-convert audio waveforms 38e4fdfe
|
||||
* volflag to improve audio waveform compression with pngquant 82ce6862
|
||||
* option to add or change mappings from file-extensions to mimetypes 560d7b66
|
||||
* export and publish the `--help` text for online viewing 560d7b66
|
||||
* now available [as html](https://ocv.me/copyparty/helptext.html) and as [plaintext](https://ocv.me/copyparty/helptext.txt), includes many features not documented in the readme
|
||||
* another way to add your own UI translations 19d156ff
|
||||
|
||||
## bugfixes
|
||||
|
||||
* ensure OS signals are immediately received and processed 87c60a1e
|
||||
* things like reload and shutdown signals from systemd could get lost/stuck
|
||||
* fix mimetype detection for uppercase file extensions 565daee9
|
||||
* when clicking a `.ts` file in the gridview, don't open it as text 925c7f0a
|
||||
* ...as it's probably an mpeg transport-stream, not a typescript file
|
||||
* be less aggressive in dropping volume caches e396c5c2
|
||||
* very minor performance gain, only really relevant if you're doing something like burning a copyparty volume onto a CD
|
||||
* previously, adding or removing any volume at all was enough to drop covers cache for all volumes; now this only happens if an intersecting volume is added/removed
|
||||
|
||||
## other changes
|
||||
|
||||
* updated dompurify to 3.1.2 566cbb65
|
||||
* opengraph: add the full filename as url suffix 5c1e2390
|
||||
* so discord picks a good filename when saving an image
|
||||
|
||||
----
|
||||
|
||||
# 💾 what to download?
|
||||
| download link | is it good? | description |
|
||||
| -- | -- | -- |
|
||||
| **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** | ✅ the best 👍 | runs anywhere! only needs python |
|
||||
| [a docker image](https://github.com/9001/copyparty/blob/hovudstraum/scripts/docker/README.md) | it's ok | good if you prefer docker 🐋 |
|
||||
| [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) | ⚠️ [acceptable](https://github.com/9001/copyparty#copypartyexe) | for [win8](https://user-images.githubusercontent.com/241032/221445946-1e328e56-8c5b-44a9-8b9f-dee84d942535.png) or later; built-in thumbnailer |
|
||||
| [u2c.exe](https://github.com/9001/copyparty/releases/download/v1.13.0/u2c.exe) | ⚠️ acceptable | [CLI uploader](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) as a win7+ exe ([video](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm)) |
|
||||
| [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz) | ⚠️ acceptable | similar to the regular sfx, [mostly worse](https://github.com/9001/copyparty#zipapp) |
|
||||
| [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) | ⛔️ [dangerous](https://github.com/9001/copyparty#copypartyexe) | for [win7](https://user-images.githubusercontent.com/241032/221445944-ae85d1f4-d351-4837-b130-82cab57d6cca.png) -- never expose to the internet! |
|
||||
| [cpp-winpe64.exe](https://github.com/9001/copyparty/releases/download/v1.10.1/copyparty-winpe64.exe) | ⛔️ dangerous | runs on [64bit WinPE](https://user-images.githubusercontent.com/241032/205454984-e6b550df-3c49-486d-9267-1614078dd0dd.png), otherwise useless |
|
||||
|
||||
* except for [u2c.exe](https://github.com/9001/copyparty/releases/download/v1.13.0/u2c.exe), all of the options above are mostly equivalent
|
||||
* the zip and tar.gz files below are just source code
|
||||
* python packages are available at [PyPI](https://pypi.org/project/copyparty/#files)
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0506-0029 `v1.13.1` ctrl-v
|
||||
|
||||
## new features
|
||||
|
||||
* upload files by `ctrl-c` from OS and `ctrl-v` into browser c5f7cfc3
|
||||
* from just about any file manager (windows explorer, thunar on linux, etc.) into the copyparty web-ui
|
||||
* only files, not folders, so drag-drop is still the recommended way
|
||||
* empty folders show an "empty folder" banner fdda567f
|
||||
* opengraph / discord embeds ea270ab9 36f2c446 48a6789d b15a4ef7
|
||||
* embeds [audio with covers](https://cd.ocv.me/c/d2/d22/snowy.mp3) , [images](https://cd.ocv.me/c/d2/d22/cover.jpg) , [videos](https://cd.ocv.me/c/d2/d21/no-effect.webm) , [audio without coverart](https://cd.ocv.me/c/d2/bitconnect.mp3) (links to one of the copyparty demoservers where the feature is enabled; link those in discord to test)
|
||||
* images are currently not rendering correctly once clicked on android-discord (works on ios and in browser)
|
||||
* default-disabled because opengraph disables hotlinking by design
|
||||
* enable with `--og` and [see readme](https://github.com/9001/copyparty#opengraph) and [the --help](https://github.com/9001/copyparty/assets/241032/2dabf21e-2470-4e20-8ef0-3821b24be1b6)
|
||||
* add option to support base64-encoded url queries parceled into the url location 69517e46
|
||||
* because android-specific discord bugs prevent the use of queries in opengraph tags
|
||||
* improve server performance when downloading unfinished uploads, especially on slow storage 70a3cf36
|
||||
* add dynamic content into `<head>` using `--html-head` which now takes files and/or jinja templates as input b6cf2d30
|
||||
* `--au-vol` (default 50, same as before) sets default audio volume in percent da091aec
|
||||
* add **[copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** buildscript 27485a4c
|
||||
* support ie4 and the [version of winzip](https://a.ocv.me/pub/g/nerd-stuff/cpp/win311zip.png) you'd find on an average windows 3.11 pc 603d0ed7
|
||||
|
||||
## bugfixes
|
||||
|
||||
* when logging in from the 403 page, remember and apply the original url hash f8491970
|
||||
* the config-reset button in the control-panel didn't clear the dotfiles preference bc2c1e42
|
||||
* the search feature could discover and use stale indexes in volumes where indexing was since disabled 95d9e693
|
||||
* when in doubt, periodically recheck if filesystems support sparse files f6e693f0
|
||||
* reduces opportunities for confusion on servers with removable media (usb flashdrives)
|
||||
|
||||
----
|
||||
|
||||
this release introduces **[copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz)**, yet another way to bring copyparty where it's needed -- very limited and with many drawbacks (see [readme](https://github.com/9001/copyparty#zipapp)) but may work when the others don't
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0420-2232 `v1.13.0` race the beam
|
||||
|
||||
## new features
|
||||
|
||||
* files can be downloaded before the upload has completed ("almost like peer-to-peer")
|
||||
* watch the [release trailer](http://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm) 👌
|
||||
* if the downloader catches up with the upload, the speed is gradually slowed down so it never runs ahead
|
||||
* can be disabled with `--no-pipe`
|
||||
* option `--no-db-ip` disables storing the uploader IP in the database bf585078
|
||||
* u2c (cli uploader): option `--ow` to overwrite existing files on the server 439cb7f8
|
||||
|
||||
## bugfixes
|
||||
|
||||
* when running on windows, using the web-UI to abort an upload could fail 8c552f1a
|
||||
* rapidly PUT-uploading and then deleting files could crash the file hasher feecb3e0
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -54,8 +55,8 @@ quick outline of the up2k protocol, see [uploading](https://github.com/9001/cop
|
||||
* server creates the `wark`, an identifier for this upload
|
||||
* `sha512( salt + filesize + chunk_hashes )`
|
||||
* and a sparse file is created for the chunks to drop into
|
||||
* client uploads each chunk
|
||||
* header entries for the chunk-hash and wark
|
||||
* client sends a series of POSTs, with one or more consecutive chunks in each
|
||||
* header entries for the chunk-hashes (comma-separated) and wark
|
||||
* server writes chunks into place based on the hash
|
||||
* client does another handshake with the hashlist; server replies with OK or a list of chunks to reupload
|
||||
|
||||
@@ -133,10 +134,14 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
|
||||
| GET | `?zip=utf-8` | ...as a zip file |
|
||||
| GET | `?zip` | ...as a WinXP-compatible zip file |
|
||||
| GET | `?zip=crc` | ...as an MSDOS-compatible zip file |
|
||||
| GET | `?tar&w` | pregenerate webp thumbnails |
|
||||
| GET | `?tar&j` | pregenerate jpg thumbnails |
|
||||
| GET | `?tar&p` | pregenerate audio waveforms |
|
||||
| GET | `?ups` | show recent uploads from your IP |
|
||||
| GET | `?ups&filter=f` | ...where URL contains `f` |
|
||||
| GET | `?mime=foo` | specify return mimetype `foo` |
|
||||
| GET | `?v` | render markdown file at URL |
|
||||
| GET | `?v` | open image/video/audio in mediaplayer |
|
||||
| GET | `?txt` | get file at URL as plaintext |
|
||||
| GET | `?txt=iso-8859-1` | ...with specific charset |
|
||||
| GET | `?th` | get image/video at URL as thumbnail |
|
||||
@@ -165,6 +170,7 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
|
||||
| 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 | `?media` | `f=FILE` | ...and return medialink (not hotlink) |
|
||||
| mPOST | | `act=mkdir`, `name=foo` | create directory `foo` at URL |
|
||||
| POST | `?delete` | | delete URL recursively |
|
||||
| jPOST | `?delete` | `["/foo","/bar"]` | delete `/foo` and `/bar` recursively |
|
||||
@@ -301,19 +307,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
|
||||
|
||||
* reduce up2k roundtrips
|
||||
* start from a chunk index and just go
|
||||
* terminate client on bad data
|
||||
* not worth the effort, just throw enough conncetions at it
|
||||
* 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
|
||||
* single sha512 across all up2k chunks?
|
||||
* crypto.subtle cannot into streaming, would have to use hashwasm, expensive
|
||||
* separate sqlite table per tag
|
||||
|
||||
@@ -46,7 +46,7 @@ open up notepad and save the following as `c:\users\you\documents\party.conf` (f
|
||||
|
||||
### config explained: [global]
|
||||
|
||||
the `[global]` section accepts any config parameters you can see when running copyparty (either the exe or the sfx.py) with `--help`, so this is the same as running copyparty with arguments `--lo c:\users\you\logs\copyparty-%Y-%m%d.xz -e2dsa -e2ts --no-dedup -z -p 80,443 --theme 2 --lang nor`
|
||||
the `[global]` section accepts any config parameters [listed here](https://ocv.me/copyparty/helptext.html), also viewable by running copyparty (either the exe or the sfx.py) with `--help`, so this is the same as running copyparty with arguments `--lo c:\users\you\logs\copyparty-%Y-%m%d.xz -e2dsa -e2ts --no-dedup -z -p 80,443 --theme 2 --lang nor`
|
||||
* `lo: ~/logs/cpp-%Y-%m%d.xz` writes compressed logs (the compression will make them delayed)
|
||||
* `e2dsa` enables the upload deduplicator and file indexer, which enables searching
|
||||
* `e2ts` enables music metadata indexing, making albums / titles etc. searchable too
|
||||
|
||||
@@ -221,6 +221,11 @@ sox -DnV -r8000 -b8 -c1 /dev/shm/a.wav synth 1.1 sin 400 vol 0.02
|
||||
# play icon calibration pics
|
||||
for w in 150 170 190 210 230 250; do for h in 130 150 170 190 210; do /c/Program\ Files/ImageMagick-7.0.11-Q16-HDRI/magick.exe convert -size ${w}x${h} xc:brown -fill orange -draw "circle $((w/2)),$((h/2)) $((w/2)),$((h/3))" $w-$h.png; done; done
|
||||
|
||||
# compress chiptune modules
|
||||
mkdir gz; for f in *.*; do pigz -c11 -I100 <"$f" >gz/"$f"gz; touch -r "$f" gz/"$f"gz; done
|
||||
mkdir xz; for f in *.*; do xz -cz9 <"$f" >xz/"$f"xz; touch -r "$f" xz/"$f"xz; done
|
||||
mkdir z; for f in *.*; do 7z a -tzip -mx=9 -mm=lzma "z/${f}z" "$f" && touch -r "$f" z/"$f"z; done
|
||||
|
||||
|
||||
##
|
||||
## vscode
|
||||
|
||||
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
|
||||
@@ -47,3 +47,25 @@ and if you want to have a monospace font in the fancy markdown editor, do this:
|
||||
|
||||
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
|
||||
|
||||
|
||||
# `<head>`
|
||||
|
||||
to add stuff to the html `<head>`, for example a css `<link>` or `<meta>` tags, use either the global-option `--html-head` or the volflag `html_head`
|
||||
|
||||
if you give it the value `@ASDF` it will try to open a file named ASDF and send the text within
|
||||
|
||||
if the value starts with `%` it will assume a jinja2 template and expand it; the template has access to the `HttpCli` object through a property named `this` as well as everything in `j2a` and the stuff added by `self.j2s`; see [browser.html](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/web/browser.html) for inspiration or look under the hood in [httpcli.py](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/httpcli.py)
|
||||
|
||||
|
||||
# translations
|
||||
|
||||
add your own translations by using the english or norwegian one from `browser.js` as a template
|
||||
|
||||
the easy way is to open up and modify `browser.js` in your own installation; depending on how you installed copyparty it might be named `browser.js.gz` instead, in which case just decompress it, restart copyparty, and start editing it anyways
|
||||
|
||||
if you're running `copyparty-sfx.py` then you'll find it at `/tmp/pe-copyparty.1000/copyparty/web` (on linux) or `%TEMP%\pe-copyparty\copyparty\web` (on windows)
|
||||
* make sure to keep backups of your work religiously! since that location is volatile af
|
||||
|
||||
if editing `browser.js` is inconvenient in your setup then you can instead do this:
|
||||
* add your translation to a separate javascript file (`tl.js`) and make it load before `browser.js` with the help of `--html-head='<script src="/tl.js"></script>'`
|
||||
* as the page loads, `browser.js` will look for a function named `langmod` so define that function and make it insert your translation into the `Ls` and `LANGS` variables so it'll take effect
|
||||
|
||||
384
docs/versus.md
384
docs/versus.md
@@ -20,6 +20,7 @@ currently up to date with [awesome-selfhosted](https://github.com/awesome-selfho
|
||||
* 💾 = what copyparty offers as an alternative
|
||||
* 🔵 = similarities
|
||||
* ⚠️ = disadvantages (something copyparty does "better")
|
||||
* 🔥 = hazards
|
||||
|
||||
|
||||
## toc
|
||||
@@ -37,7 +38,7 @@ currently up to date with [awesome-selfhosted](https://github.com/awesome-selfho
|
||||
* [another matrix](#another-matrix)
|
||||
* [reviews](#reviews)
|
||||
* [copyparty](#copyparty)
|
||||
* [hfs2](#hfs2)
|
||||
* [hfs2](#hfs2) 🔥
|
||||
* [hfs3](#hfs3)
|
||||
* [nextcloud](#nextcloud)
|
||||
* [seafile](#seafile)
|
||||
@@ -48,6 +49,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)
|
||||
@@ -82,8 +84,8 @@ the table headers in the matrixes below are the different softwares, with a quic
|
||||
|
||||
the softwares,
|
||||
* `a` = [copyparty](https://github.com/9001/copyparty)
|
||||
* `b` = [hfs2](https://rejetto.com/hfs/)
|
||||
* `c` = [hfs3](https://github.com/rejetto/hfs)
|
||||
* `b` = [hfs2](https://github.com/rejetto/hfs2/) 🔥
|
||||
* `c` = [hfs3](https://rejetto.com/hfs/)
|
||||
* `d` = [nextcloud](https://github.com/nextcloud/server)
|
||||
* `e` = [seafile](https://github.com/haiwen/seafile)
|
||||
* `f` = [rclone](https://github.com/rclone/rclone), specifically `rclone serve webdav .`
|
||||
@@ -93,6 +95,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 +116,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 +143,40 @@ 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 | █ | | | | █ | █ | • | | █ | | █ | ╱ | ╱ |
|
||||
| CTRL-V from device | █ | | | █ | | | | | | | | | |
|
||||
| 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 +184,10 @@ symbol legend,
|
||||
|
||||
* `upload verification` = uploads are checksummed or otherwise confirmed to have been transferred correctly
|
||||
|
||||
* `CTRL-V from device` = press CTRL-C in Windows Explorer (or whatever) and paste into the webbrowser to upload it
|
||||
|
||||
* `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,26 +202,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 (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 | █ | | | • | • | █ | | • | • | | | ╱ |
|
||||
| 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
|
||||
@@ -222,61 +233,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
|
||||
@@ -286,6 +302,7 @@ symbol legend,
|
||||
* `file action event hooks` = run script before/after upload, move, rename, ...
|
||||
* `one-way folder sync` = like rsync, optionally deleting unexpected files at target
|
||||
* `full sync` = stateful, dropbox-like sync
|
||||
* `speed throttle` = rate limiting (per ip, per user, per connection, anything like that)
|
||||
* `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:
|
||||
@@ -302,49 +319,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
|
||||
@@ -353,21 +372,25 @@ symbol legend,
|
||||
* `undo recent uploads` = accounts without delete permissions have a time window where they can undo their own uploads
|
||||
* `a`/copyparty has teeny-tiny skips playing gapless albums depending on audio codec (opus best)
|
||||
* `b`/hfs2 has a very basic directory tree view, not showing sibling folders
|
||||
* `c`/hfs3 remarks:
|
||||
* audio playback does not continue into next song
|
||||
* `f`/rclone can do some file management (mkdir, rename, delete) when hosting througn webdav
|
||||
* `j`/filebrowser has a plaintext viewer/editor
|
||||
* `j`/filebrowser remarks:
|
||||
* audio playback does not continue into next song
|
||||
* plaintext viewer/editor
|
||||
* `k`/filegator directory tree is a modal window
|
||||
|
||||
|
||||
## 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:
|
||||
@@ -393,6 +416,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 |
|
||||
@@ -409,6 +433,7 @@ symbol legend,
|
||||
* 💾 are what copyparty offers as an alternative
|
||||
* 🔵 are similarities
|
||||
* ⚠️ are disadvantages (something copyparty does "better")
|
||||
* 🔥 are hazards
|
||||
|
||||
## [copyparty](https://github.com/9001/copyparty)
|
||||
* resumable uploads which are verified server-side
|
||||
@@ -416,8 +441,9 @@ symbol legend,
|
||||
* both of the above are surprisingly uncommon features
|
||||
* very cross-platform (python, no dependencies)
|
||||
|
||||
## [hfs2](https://rejetto.com/hfs/)
|
||||
* the OG, the legend
|
||||
## [hfs2](https://github.com/rejetto/hfs2/)
|
||||
* the OG, the legend (now replaced by [hfs3](#hfs3))
|
||||
* 🔥 hfs2 is dead and dangerous! unfixed RCE: [info](https://github.com/rejetto/hfs2/issues/44), [info](https://github.com/drapid/hfs/issues/3), [info](https://asec.ahnlab.com/en/67650/)
|
||||
* ⚠️ uploads not resumable / accelerated / integrity-checked
|
||||
* ⚠️ on cloudflare: max upload size 100 MiB
|
||||
* ⚠️ windows-only
|
||||
@@ -425,10 +451,19 @@ symbol legend,
|
||||
* vfs with gui config, per-volume permissions
|
||||
* starting to show its age, hence the rewrite:
|
||||
|
||||
## [hfs3](https://github.com/rejetto/hfs)
|
||||
## [hfs3](https://rejetto.com/hfs/)
|
||||
* nodejs; cross-platform
|
||||
* vfs with gui config, per-volume permissions
|
||||
* still early development, let's revisit later
|
||||
* 🔵 uploads are resumable
|
||||
* ⚠️ uploads are not segmented; max upload size 100 MiB on cloudflare
|
||||
* ⚠️ uploads are not accelerated (copyparty is 3x faster across the atlantic)
|
||||
* ⚠️ uploads are not integrity-checked
|
||||
* ⚠️ copies the file after upload; need twice filesize free disk space
|
||||
* ⚠️ doesn't support crazy filenames
|
||||
* ✅ config GUI
|
||||
* ✅ download counter
|
||||
* ✅ watch active connections
|
||||
* ✅ plugins
|
||||
|
||||
## [nextcloud](https://github.com/nextcloud/server)
|
||||
* php, mariadb
|
||||
@@ -482,6 +517,7 @@ symbol legend,
|
||||
* rust; cross-platform (windows, linux, macos)
|
||||
* ⚠️ uploads not resumable / accelerated / integrity-checked
|
||||
* ⚠️ on cloudflare: max upload size 100 MiB
|
||||
* ⚠️ across the atlantic, copyparty is 3x faster
|
||||
* ⚠️ doesn't support crazy filenames
|
||||
* ✅ per-url access control (copyparty is per-volume)
|
||||
* 🔵 basic but really snappy ui
|
||||
@@ -504,12 +540,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
|
||||
@@ -522,8 +560,10 @@ symbol legend,
|
||||
|
||||
## [filebrowser](https://github.com/filebrowser/filebrowser)
|
||||
* go; cross-platform (windows, linux, mac)
|
||||
* ⚠️ uploads not resumable / accelerated / integrity-checked
|
||||
* ⚠️ on cloudflare: max upload size 100 MiB
|
||||
* 🔵 uploads are resumable and segmented
|
||||
* 🔵 multiple files are uploaded in parallel, but...
|
||||
* ⚠️ big files are not accelerated (copyparty is 5x faster across the atlantic)
|
||||
* ⚠️ uploads are not integrity-checked
|
||||
* ⚠️ http only; no webdav / ftp / zeroconf
|
||||
* ⚠️ doesn't support crazy filenames
|
||||
* ⚠️ no directory tree nav
|
||||
@@ -533,12 +573,14 @@ symbol legend,
|
||||
* ⚠️ but no directory tree for navigation
|
||||
* ✅ user signup
|
||||
* ✅ command runner / remote shell
|
||||
* 🔵 supposed to have write-only folders but couldn't get it to work
|
||||
* ✅ more efficient; can handle around twice as much simultaneous traffic
|
||||
|
||||
## [filegator](https://github.com/filegator/filegator)
|
||||
* go; cross-platform (windows, linux, mac)
|
||||
* php; cross-platform (windows, linux, mac)
|
||||
* 🔵 *it has upload segmenting and acceleration*
|
||||
* ⚠️ but uploads are still not integrity-checked
|
||||
* ⚠️ on copyparty, uploads are 40x faster
|
||||
* compared to the official filegator docker example which might be bad
|
||||
* ⚠️ http only; no webdav / ftp / zeroconf
|
||||
* ⚠️ does not support symlinks
|
||||
* ⚠️ expensive download-as-zip feature
|
||||
@@ -549,6 +591,7 @@ symbol legend,
|
||||
* go; cross-platform (windows, linux, mac)
|
||||
* ⚠️ http uploads not resumable / accelerated / integrity-checked
|
||||
* ⚠️ on cloudflare: max upload size 100 MiB
|
||||
* ⚠️ across the atlantic, copyparty is 2.5x faster
|
||||
* 🔵 sftp uploads are resumable
|
||||
* ⚠️ web UI is very minimal + a bit slow
|
||||
* ⚠️ no thumbnails / image viewer / audio player
|
||||
@@ -556,6 +599,7 @@ symbol legend,
|
||||
* ⚠️ no filesystem indexing / search
|
||||
* ⚠️ doesn't run on phones, tablets
|
||||
* ⚠️ no zeroconf (mdns/ssdp)
|
||||
* ⚠️ impractical directory URLs
|
||||
* ⚠️ AGPL licensed
|
||||
* 🔵 ftp, ftps, webdav
|
||||
* ✅ sftp server
|
||||
@@ -569,6 +613,26 @@ 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)
|
||||
* ⚠️ needs root
|
||||
* ⚠️ 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
|
||||
* ⚠️ across the atlantic, uploading to copyparty is 6x faster
|
||||
* ⚠️ 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
|
||||
|
||||
@@ -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()
|
||||
@@ -49,7 +49,7 @@ thumbnails2 = ["pyvips"]
|
||||
audiotags = ["mutagen"]
|
||||
ftpd = ["pyftpdlib"]
|
||||
ftps = ["pyftpdlib", "pyopenssl"]
|
||||
tftpd = ["partftpy>=0.3.1"]
|
||||
tftpd = ["partftpy>=0.4.0"]
|
||||
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.9 \
|
||||
ver_dompf=3.1.6 \
|
||||
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 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* \
|
||||
|
||||
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
|
||||
@@ -17,3 +17,19 @@ but I don't really know what i'm doing here 💩
|
||||
`podman login docker.io`
|
||||
`podman login ghcr.io -u 9001`
|
||||
[about gchq](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry) (takes a classic token as password)
|
||||
|
||||
|
||||
## building on alpine
|
||||
|
||||
```bash
|
||||
apk add podman{,-docker}
|
||||
rc-update add cgroups
|
||||
service cgroups start
|
||||
vim /etc/containers/storage.conf # driver = "btrfs"
|
||||
modprobe tun
|
||||
echo ed:100000:65536 >/etc/subuid
|
||||
echo ed:100000:65536 >/etc/subgid
|
||||
apk add qemu-openrc qemu-tools qemu-{arm,armeb,aarch64,s390x,ppc64le}
|
||||
rc-update add qemu-binfmt
|
||||
service qemu-binfmt start
|
||||
```
|
||||
|
||||
81
scripts/help2html.py
Executable file
81
scripts/help2html.py
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import re
|
||||
import subprocess as sp
|
||||
|
||||
|
||||
# to convert the copyparty --help to html, run this in xfce4-terminal @ 140x43:
|
||||
_ = r""""
|
||||
echo; for a in '' -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -pwhash -zm; do
|
||||
./copyparty-sfx.py --help$a 2>/dev/null; printf '\n\n\n%0139d\n\n\n'; done # xfce4-terminal @ 140x43
|
||||
"""
|
||||
# click [edit] => [select all]
|
||||
# click [edit] => [copy as html]
|
||||
# and then run this script
|
||||
|
||||
|
||||
def readclip():
|
||||
cmds = [
|
||||
"xsel -ob",
|
||||
"xclip -selection CLIPBOARD -o",
|
||||
"pbpaste",
|
||||
]
|
||||
for cmd in cmds:
|
||||
try:
|
||||
return sp.check_output(cmd.split()).decode("utf-8")
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def cnv(src):
|
||||
yield '<html style="background:#222;color:#fff"><body>'
|
||||
skip_sfx = False
|
||||
in_sfx = 0
|
||||
in_salt = 0
|
||||
|
||||
while True:
|
||||
ln = next(src)
|
||||
if "<font" in ln:
|
||||
if not ln.startswith("<pre>"):
|
||||
ln = "<pre>" + ln
|
||||
yield ln
|
||||
break
|
||||
|
||||
for ln in src:
|
||||
ln = ln.rstrip()
|
||||
if re.search(r"^<font[^>]+>copyparty v[0-9]", ln):
|
||||
in_sfx = 3
|
||||
if in_sfx:
|
||||
in_sfx -= 1
|
||||
if not skip_sfx:
|
||||
yield ln
|
||||
continue
|
||||
if '">uuid:' in ln:
|
||||
ln = re.sub(r">uuid:[0-9a-f-]{36}<", ">autogenerated<", ln)
|
||||
if "-salt SALT" in ln:
|
||||
in_salt = 3
|
||||
if in_salt:
|
||||
in_salt -= 1
|
||||
t = ln
|
||||
ln = re.sub(r">[0-9a-zA-Z/+]{24}<", ">24-character-autogenerated<", ln)
|
||||
ln = re.sub(r">[0-9a-zA-Z/+]{40}<", ">40-character-autogenerated<", ln)
|
||||
if t != ln:
|
||||
in_salt = 0
|
||||
ln = ln.replace(">/home/ed/", ">~/")
|
||||
if ln.startswith("0" * 20):
|
||||
skip_sfx = True
|
||||
yield ln
|
||||
|
||||
yield "</pre>eof</body></html>"
|
||||
|
||||
|
||||
def main():
|
||||
src = readclip()
|
||||
src = re.split("0{100,200}", src[::-1], 1)[1][::-1]
|
||||
with open("helptext.html", "wb") as fo:
|
||||
for ln in cnv(iter(src.split("\n")[:-3])):
|
||||
fo.write(ln.encode("utf-8") + b"\r\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
26
scripts/help2txt.sh
Executable file
26
scripts/help2txt.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
( xsel -ob | sed -r '
|
||||
s`/home/ed/`~/`;
|
||||
s/uuid:[0-9a-f-]{36}/autogenerated/;
|
||||
s/(-salt SALT.*default: )[0-9a-zA-Z/+]{24}\)/\124-character-autogenerated)/;
|
||||
s/(-salt SALT.*default: )[0-9a-zA-Z/+]{40}\)/\140-character-autogenerated)/;
|
||||
' | awk '
|
||||
/^copyparty/{a=1} !a{next}
|
||||
/^0{20}/{b=1} b&&/^copyparty v[0-9]+\./{s=3}
|
||||
s{s-=1;next} 1' |
|
||||
head -n-6; echo eof ) >helptext.txt
|
||||
exit 0
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# end of script; below is the explanation how to use this:
|
||||
|
||||
|
||||
# first open an infinitely wide console (this is why you own an ultrawide) and copypaste this into it:
|
||||
for a in '' -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -pwhash -zm; do
|
||||
./copyparty-sfx.py --help$a 2>/dev/null; printf '\n\n\n%0255d\n\n\n'; done
|
||||
|
||||
# then copypaste all of the output by pressing ctrl-shift-a, ctrl-shift-c
|
||||
# and finally actually run this script which should produce helptext.txt
|
||||
61
scripts/make-pyz.sh
Executable file
61
scripts/make-pyz.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
echo
|
||||
|
||||
# port install gnutar gsed coreutils
|
||||
gtar=$(command -v gtar || command -v gnutar) || true
|
||||
[ ! -z "$gtar" ] && command -v gsed >/dev/null && {
|
||||
tar() { $gtar "$@"; }
|
||||
sed() { gsed "$@"; }
|
||||
command -v grealpath >/dev/null &&
|
||||
realpath() { grealpath "$@"; }
|
||||
}
|
||||
|
||||
targs=(--owner=1000 --group=1000)
|
||||
[ "$OSTYPE" = msys ] &&
|
||||
targs=()
|
||||
|
||||
[ -e copyparty/__main__.py ] || cd ..
|
||||
[ -e copyparty/__main__.py ] || {
|
||||
echo "run me from within the project root folder"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ -e sfx/copyparty/__main__.py ] || {
|
||||
echo "run ./scripts/make-sfx.py first"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
rm -rf pyz
|
||||
mkdir -p pyz
|
||||
cd pyz
|
||||
|
||||
cp -pR ../sfx/{copyparty,partftpy} .
|
||||
cp -pR ../sfx/{ftp,j2}/* .
|
||||
|
||||
ts=$(date -u +%s)
|
||||
hts=$(date -u +%Y-%m%d-%H%M%S)
|
||||
ver="$(cat ../sfx/ver)"
|
||||
|
||||
mkdir -p ../dist
|
||||
pyz_out=../dist/copyparty.pyz
|
||||
|
||||
echo creating z.tar
|
||||
( cd copyparty
|
||||
tar -cf z.tar "${targs[@]}" --numeric-owner web res
|
||||
rm -rf web res
|
||||
)
|
||||
|
||||
echo creating loader
|
||||
sed -r 's/^(VER = ).*/\1"'"$ver"'"/; s/^(STAMP = ).*/\1'$(date +%s)/ \
|
||||
<../scripts/ziploader.py \
|
||||
>__main__.py
|
||||
|
||||
echo creating pyz
|
||||
rm -f $pyz_out
|
||||
zip -9 -q -r $pyz_out *
|
||||
|
||||
echo done:
|
||||
echo " $(realpath $pyz_out)"
|
||||
@@ -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,
|
||||
@@ -101,9 +99,6 @@ pybin=$(command -v python3 || command -v python) || {
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ $CSN ] ||
|
||||
CSN=sfx
|
||||
|
||||
langs=
|
||||
use_gz=
|
||||
zopf=2560
|
||||
@@ -111,7 +106,6 @@ 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 ; ;;
|
||||
@@ -151,9 +145,9 @@ stamp=$(
|
||||
done | sort | tail -n 1 | sha1sum | cut -c-16
|
||||
)
|
||||
|
||||
rm -rf $CSN/*
|
||||
mkdir -p $CSN build
|
||||
cd $CSN
|
||||
rm -rf sfx$CSN/*
|
||||
mkdir -p sfx$CSN build
|
||||
cd sfx$CSN
|
||||
|
||||
tmpdir="$(
|
||||
printf '%s\n' "$TMPDIR" /tmp |
|
||||
@@ -225,9 +219,9 @@ necho() {
|
||||
mv pyftpdlib ftp/
|
||||
|
||||
necho collecting partftpy
|
||||
f="../build/partftpy-0.3.1.tar.gz"
|
||||
f="../build/partftpy-0.4.0.tar.gz"
|
||||
[ -e "$f" ] ||
|
||||
(url=https://files.pythonhosted.org/packages/37/79/1a1de1d3fdf27ddc9c2d55fec6552e7b8ed115258fedac6120679898b83d/partftpy-0.3.1.tar.gz;
|
||||
(url=https://files.pythonhosted.org/packages/8c/96/642bb3ddcb07a2c6764eb29aa562d1cf56877ad6c330c3c8921a5f05606d/partftpy-0.4.0.tar.gz;
|
||||
wget -O$f "$url" || curl -L "$url" >$f)
|
||||
|
||||
tar -zxf $f
|
||||
@@ -389,11 +383,13 @@ git describe --tags >/dev/null 2>/dev/null && {
|
||||
ver="$(awk '/^VERSION *= \(/ {
|
||||
gsub(/[^0-9,a-g-]/,""); gsub(/,/,"."); print; exit}' < copyparty/__version__.py)"
|
||||
|
||||
echo "$ver" >ver # pyz
|
||||
|
||||
ts=$(date -u +%s)
|
||||
hts=$(date -u +%Y-%m%d-%H%M%S) # --date=@$ts (thx osx)
|
||||
|
||||
mkdir -p ../dist
|
||||
sfx_out=../dist/copyparty-$CSN
|
||||
sfx_out=../dist/copyparty-sfx$CSN
|
||||
|
||||
echo cleanup
|
||||
find -name '*.pyc' -delete
|
||||
@@ -461,8 +457,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' |
|
||||
@@ -494,6 +490,11 @@ while IFS= read -r f; do
|
||||
tmv "$f"
|
||||
done
|
||||
|
||||
grep -rlE '^class [^(]+:' |
|
||||
while IFS= read -r f; do
|
||||
ised 's/(^class [^(:]+):/\1(object):/' "$f"
|
||||
done
|
||||
|
||||
# up2k goes from 28k to 22k laff
|
||||
awk 'BEGIN{gensub(//,"",1)}' </dev/null 2>/dev/null &&
|
||||
echo entabbening &&
|
||||
@@ -545,7 +546,7 @@ gzres() {
|
||||
}
|
||||
|
||||
|
||||
zdir="$tmpdir/cpp-mk$CSN"
|
||||
zdir="$tmpdir/cpp-mksfx$CSN"
|
||||
[ -e "$zdir/$stamp" ] || rm -rf "$zdir"
|
||||
mkdir -p "$zdir"
|
||||
echo a > "$zdir/$stamp"
|
||||
@@ -570,33 +571,6 @@ 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 partftpy magic j2 py2 py37 ftp; do find $d -type f || true; done | # strip_hints
|
||||
sed -r 's/(.*)\.(.*)/\2 \1/' | LC_ALL=C sort |
|
||||
|
||||
@@ -16,7 +16,7 @@ uname -s | grep WOW64 && m=64 || m=32
|
||||
uname -s | grep NT-10 && w10=1 || w7=1
|
||||
[ $w7 ] && [ -e up2k.sh ] && [ ! "$1" ] && ./up2k.sh
|
||||
|
||||
[ $w7 ] && pyv=37 || pyv=311
|
||||
[ $w7 ] && pyv=37 || pyv=312
|
||||
esuf=
|
||||
[ $w7 ] && [ $m = 32 ] && esuf=32
|
||||
[ $w7 ] && [ $m = 64 ] && esuf=-winpe64
|
||||
@@ -118,4 +118,15 @@ 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
|
||||
}
|
||||
|
||||
echo; read -u1 -n1 -p 'shutdown? y/n: '
|
||||
[ "$REPLY" = y ] && shutdown -s -t 1
|
||||
|
||||
@@ -1,33 +1,39 @@
|
||||
f117016b1e6a7d7e745db30d3e67f1acf7957c443a0dd301b6c5e10b8368f2aa4db6be9782d2d3f84beadd139bfeef4982e40f21ca5d9065cb794eeb0e473e82 altgraph-0.17.4-py2.py3-none-any.whl
|
||||
eda6c38fc4d813fee897e969ff9ecc5acc613df755ae63df0392217bbd67408b5c1f6c676f2bf5497b772a3eb4e1a360e1245e1c16ee83f0af555f1ab82c3977 Git-2.39.1-32-bit.exe
|
||||
6a624018f30da375581d5751eca0080edbbe37f102f643f856279fcfded3a4379fd1b6fb0661cdb2e72bbbbc726ca714a1f5990cc348df365db62bc53e4c4503 Git-2.45.2-32-bit.exe
|
||||
17ce52ba50692a9d964f57a23ac163fb74c77fdeb2ca988a6d439ae1fe91955ff43730c073af97a7b3223093ffea3479a996b9b50ee7fba0869247a56f74baa6 pefile-2023.2.7-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
|
||||
085d39ef4426aa5f097fbc484595becc16e61ca23fc7da4d2a8bba540a3b82e789e390b176c7151bdc67d01735cce22b1562cdb2e31273225a2d3e275851a4ad setuptools-70.3.0-py3-none-any.whl
|
||||
360a141928f4a7ec18a994602cbb28bbf8b5cc7c077a06ac76b54b12fa769ed95ca0333a5cf728923a8e0baeb5cc4d5e73e5b3de2666beb05eb477d8ae719093 upx-4.2.4-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
|
||||
b795abb26ba2f04f1afcfb196f21f638014b26c8186f8f488f1c2d91e8e0220962fbd259dbc9c3875222eb47fc95c73fc0606aaa6602b9ebc524809c9ba3501f requests-2.31.0-py3-none-any.whl
|
||||
5a25cb9b79bb6107f9055dc3e9f62ebc6d4d9ca2c730d824985c93cd82406b723c200d6300c5064e42ee9fc7a2853d6ec6661394f3ed7bac03750e1f2a6840d1 urllib3-1.26.17-py2.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
|
||||
cc08d0d87d184401872a2f82266d589253979b4cd02f23b51290fbb2a20082848fc72acbed8aacb74ac4af068d575ef96e66196c5068bc38fb0bcafdc7626869 requests-2.29.0-py3-none-any.whl
|
||||
fe5fee6cb8a2c68800b32353a0015e5d2e1ad1cb6e0c9e6acf86e48e5cdb5606ad465dc4485ea5fbc8701d8716a8a7f7148c57724ef9da26b0c0a76f6dbbd698 urllib3-1.26.19-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
|
||||
3253e86471e6f9fa85bfdb7684cd2f964ed6e35c6a4db87f81cca157c049bef43e66dfcae1e037b2fb904567b1e028aaeefe8983ba3255105df787406d2aa71e en_windows_7_professional_with_sp1_x86_dvd_u_677056.iso
|
||||
ab0db0283f61a5bbe44797d74546786bf41685175764a448d2e3bd629f292f1e7d829757b26be346b5044d78c9c1891736d93237cee4b1b6f5996a902c86d15f en_windows_7_professional_with_sp1_x64_dvd_u_676939.iso
|
||||
d130bfa136bd171b9972b5c281c578545f2a84a909fdf18a6d2d71dd12fb3d512a7a1fa5cf7300433adece1d306eb2f22d7278f4c90e744e04dc67ba627a82c0 future-1.0.0-py3-none-any.whl
|
||||
0b4d07434bf8d314f42893d90bce005545b44a509e7353a73cad26dc9360b44e2824218a1a74f8174d02eba87fba91baffa82c8901279a32ebc6b8386b1b4275 importlib_metadata-6.7.0-py3-none-any.whl
|
||||
9d2c31701a4d3fef553928c00528a48f9e1854ab5333528b50e358a214eba90029d687f039bcda5760b6fdf9f2de3bcf3784ae21a6374cf2a97a845d33b636c6 packaging-24.0-py3-none-any.whl
|
||||
5d7462a584105bccaa9cf376f5a8c5827ead099c813c8af7392d478a4398f373d9e8cac7bbad2db51b335411ab966b21e119b1b1234c9a7ab70c6ddfc9306da6 pip-24.0-py3-none-any.whl
|
||||
f298e34356b5590dde7477d7b3a88ad39c622a2bcf3fcd7c53870ce8384dd510f690af81b8f42e121a22d3968a767d2e07595036b2ed7049c8ef4d112bcf3a61 pyinstaller-5.13.2-py3-none-win32.whl
|
||||
ea73aa54cc6d5db20dfb127e54562dabf890e4cd6171a91b10a51af2bcfc76e1d64cbdce4546df2dcfe42b624724c85b1cd05934be2413425b1f880222727b4f pyinstaller-5.13.2-py3-none-win_amd64.whl
|
||||
2f4e3927a38cf7757bc9a1c06370d79209669a285a80f1b09cf9917137825c7022a50a56b351807e6e687e2c3a7bd7b2c5cc6daeb4d90e11920284c1a04a1cc3 pyinstaller_hooks_contrib-2023.8-py2.py3-none-any.whl
|
||||
6bb73cc2db795c59c92f2115727f5c173cacc9465af7710db9ff2f2aec2d73130d0992d0f16dcb3fac222dc15c0916562d0813b2337401022020673a4461df3d python-3.7.9-amd64.exe
|
||||
500747651c87f59f2436c5ab91207b5b657856e43d10083f3ce27efb196a2580fadd199a4209519b409920c562aaaa7dcbdfb83ed2072a43eaccae6e2d056f31 python-3.7.9.exe
|
||||
2e04acff170ca3bbceeeb18489c687126c951ec0bfd53cccfb389ba8d29a4576c1a9e8f2e5ea26c84dd21bfa2912f4e71fa72c1e2653b71e34afc0e65f1722d4 upx-4.2.2-win32.zip
|
||||
03e50aecc85914567c114e38a1777e32628ee098756f37177bc23220eab33ac7d3ff591fd162db3b4d4e34d55cee93ef0dc67af68a69c38bb1435e0768dee57e typing_extensions-4.7.1-py3-none-any.whl
|
||||
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
|
||||
e3e2e6bd511dec484dd0292f4c46c55c88a885eabf15413d53edea2dd4a4dbae1571735b9424f78c0cd7f1082476a8259f31fd3f63990f726175470f636df2b3 Jinja2-3.1.3-py3-none-any.whl
|
||||
e21495f1d473d855103fb4a243095b498ec90eb68776b0f9b48e994990534f7286c0292448e129c507e5d70409f8a05cca58b98d59ce2a815993d0a873dfc480 MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl
|
||||
0a2cd4cadf0395f0374974cd2bc2407e5cc65c111275acdffb6ecc5a2026eee9e1bb3da528b35c7f0ff4b64563a74857d5c2149051e281cc09ebd0d1968be9aa en-us_windows_10_enterprise_ltsc_2021_x64_dvd_d289cf96.iso
|
||||
16cc0c58b5df6c7040893089f3eb29c074aed61d76dae6cd628d8a89a05f6223ac5d7f3f709a12417c147594a87a94cc808d1e04a6f1e407cc41f7c9f47790d1 virtio-win-0.1.248.iso
|
||||
d1420c8417fad7888766dd26b9706a87c63e8f33dceeb8e26d0056d5127b0b3ed9272e44b4b761132d4b3320327252eab1696520488ca64c25958896b41f547b jinja2-3.1.4-py3-none-any.whl
|
||||
8e6847bcde75a2736be0214731f834bc1b5854238d703351e68bc4e74d38404b212b8568565ae22c844189e466d3fbe6024836351cb69ffb1824131387644fef MarkupSafe-2.1.5-cp312-cp312-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
|
||||
8760eab271e79256ae3bfb4af8ccc59010cb5d2eccdd74b325d1a533ae25eb127d51c2ec28ff90d449afed32dd7d6af62934fe9caaf1ae1f4d4831e948e912da pyinstaller-6.5.0-py3-none-win_amd64.whl
|
||||
e6bdbae1affd161e62fc87407c912462dfe875f535ba9f344d0c4ade13715c947cd3ae832eff60f1bad4161938311d06ac8bc9b52ef203f7b0d9de1409f052a5 python-3.11.8-amd64.exe
|
||||
729dc52f1a02bc6274d012ce33f534102975a828cba11f6029600ea40e2d23aefeb07bf4ae19f9621d0565dd03eb2635bbb97d45fb692c1f756315e8c86c5255 upx-4.2.2-win64.zip
|
||||
0203ec2551c4836696cfab0b2c9fff603352f03fa36e7476e2e1ca7ec57a3a0c24bd791fcd92f342bf817f0887854d9f072e0271c643de4b313d8c9569ba8813 packaging-24.1-py3-none-any.whl
|
||||
2be320b4191f208cdd6af183c77ba2cf460ea52164ee45ac3ff17d6dfa57acd9deff016636c2dd42a21f4f6af977d5f72df7dacf599bebcf41757272354d14c1 pillow-10.4.0-cp312-cp312-win_amd64.whl
|
||||
776378f5414efd26ec8a1cb3228a7b5fdf6afca3fa335a0e9b071266d55d9d9e66ee157c25a468a05bfa70ccd33c48b101998523fc6ff6bcf5e82a1d81ed0af8 pyinstaller-6.9.0-py3-none-win_amd64.whl
|
||||
c0af77d2a57cb063ab038dc986ed3582bc5acc8c8bd91d726101935d6388f50854ddbca26bc846ed5d1022cdee4d96242938c66f0ddc4565c36b60d691064db8 pyinstaller_hooks_contrib-2024.7-py2.py3-none-any.whl
|
||||
2f9a11ffae6d9f1ed76bf816f28812fcba71f87080b0c92e52bfccb46243118c5803a7e25dd78003ca7d66501bfcdce8ff7c691c63c0038b0d409ca3842dcc89 python-3.12.4-amd64.exe
|
||||
|
||||
@@ -34,6 +34,7 @@ https://support.microsoft.com/en-us/topic/microsoft-security-advisory-insecure-l
|
||||
see web.archive.org links below
|
||||
|
||||
# direct links to version-frozen deps
|
||||
https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-0.1.248-1/virtio-win-0.1.248.iso
|
||||
https://www.python.org/ftp/python/3.7.9/python-3.7.9-amd64.exe
|
||||
https://www.python.org/ftp/python/3.7.9/python-3.7.9.exe
|
||||
https://web.archive.org/web/20200412130846if_/https://download.microsoft.com/download/2/D/7/2D78D0DD-2802-41F5-88D6-DC1D559F206D/Windows6.1-KB2533623-x86.msu
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
after performing all the initial setup in this file,
|
||||
run ./build.sh in git-bash to build + upload the exe
|
||||
|
||||
|
||||
## ============================================================
|
||||
## first-time setup on a stock win7x32sp1 and/or win10x64 vm:
|
||||
##
|
||||
|
||||
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
|
||||
and if you don't yet have a windows vm to build in, then the
|
||||
first step will be "creating the windows VM templates" below
|
||||
|
||||
commands to start the VMs after initial setup:
|
||||
qemu-system-x86_64 -m 4096 -enable-kvm --machine q35 -cpu host -smp 4 -usb -device usb-tablet -net bridge,br=virhost0 -net nic,model=e1000e -drive file=win7-x32.qcow2,discard=unmap,detect-zeroes=unmap
|
||||
qemu-system-x86_64 -m 4096 -enable-kvm --machine q35 -cpu host -smp 4 -usb -device usb-tablet -net bridge,br=virhost0 -net nic,model=virtio -drive file=win10-e2021.qcow2,if=virtio,discard=unmap
|
||||
|
||||
|
||||
|
||||
## ============================================================
|
||||
## first-time setup in a stock win7x32sp1 and/or win10x64 vm:
|
||||
##
|
||||
|
||||
grab & install from ftp2host: Git-2.45.2-32-bit.exe
|
||||
|
||||
...and do this on the host so you can grab these notes too:
|
||||
unix2dos <~/dev/copyparty/scripts/pyinstaller/notes.txt >~/dev/pyi/notes.txt
|
||||
|
||||
|
||||
===[ copy-paste into git-bash ]================================
|
||||
uname -s | grep NT-10 && w10=1 || {
|
||||
@@ -17,38 +29,39 @@ uname -s | grep NT-10 && w10=1 || {
|
||||
fns=(
|
||||
altgraph-0.17.4-py2.py3-none-any.whl
|
||||
pefile-2023.2.7-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
|
||||
setuptools-70.3.0-py3-none-any.whl
|
||||
upx-4.2.4-win32.zip
|
||||
)
|
||||
[ $w10 ] && fns+=(
|
||||
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
|
||||
jinja2-3.1.4-py3-none-any.whl
|
||||
MarkupSafe-2.1.5-cp312-cp312-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.8-amd64.exe
|
||||
upx-4.2.2-win64.zip
|
||||
packaging-24.1-py3-none-any.whl
|
||||
pillow-10.4.0-cp312-cp312-win_amd64.whl
|
||||
pyinstaller-6.9.0-py3-none-win_amd64.whl
|
||||
pyinstaller_hooks_contrib-2024.7-py2.py3-none-any.whl
|
||||
python-3.12.4-amd64.exe
|
||||
)
|
||||
[ $w7 ] && fns+=( # u2c stuff
|
||||
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.29.0-py3-none-any.whl
|
||||
urllib3-1.26.19-py2.py3-none-any.whl
|
||||
)
|
||||
[ $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
|
||||
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
|
||||
packaging-24.0-py3-none-any.whl
|
||||
pip-24.0-py3-none-any.whl
|
||||
pyinstaller_hooks_contrib-2023.8-py2.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
|
||||
pyinstaller-5.13.2-py3-none-win64.whl
|
||||
python-3.7.9-amd64.exe
|
||||
)
|
||||
[ $w7x32 ] && fns+=(
|
||||
@@ -57,29 +70,34 @@ fns=(
|
||||
python-3.7.9.exe
|
||||
)
|
||||
dl() { curl -fkLOC- "$1" && return 0; echo "$1"; return 1; }
|
||||
cd ~/Downloads &&
|
||||
cd ~/Downloads && rm -f Git-*.exe &&
|
||||
for fn in "${fns[@]}"; do
|
||||
dl "https://192.168.123.1:3923/ro/pyi/$fn" || {
|
||||
echo ERROR; ok=; break
|
||||
}
|
||||
done
|
||||
|
||||
manually install:
|
||||
windows6.1-kb2533623 + reboot
|
||||
python-3.7.9
|
||||
|
||||
WIN7-ONLY: manually install windows6.1-kb2533623 and reboot
|
||||
|
||||
manually install python-3.99.99.exe and then delete it
|
||||
|
||||
close and reopen git-bash so python is in PATH
|
||||
|
||||
|
||||
===[ copy-paste into git-bash ]================================
|
||||
uname -s | grep NT-10 && w10=1 || w7=1
|
||||
[ $w7 ] && pyv=37 || pyv=311
|
||||
[ $w7 ] && pyv=37 || pyv=312
|
||||
appd=$(cygpath.exe "$APPDATA")
|
||||
cd ~/Downloads &&
|
||||
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 &&
|
||||
@@ -89,9 +107,65 @@ rm -f build.sh &&
|
||||
curl -fkLO https://192.168.123.1:3923/cpp/scripts/pyinstaller/build.sh &&
|
||||
{ [ $w10 ] || curl -fkLO https://192.168.123.1:3923/cpp/scripts/pyinstaller/up2k.sh; } &&
|
||||
echo ok
|
||||
# python -m pip install --user -U Pillow-9.2.0-cp37-cp37m-win32.whl
|
||||
# sed -ri 's/, bestopt, /]+bestopt+[/' $APPDATA/Python/Python37/site-packages/pyinstaller/building/utils.py
|
||||
# sed -ri 's/(^\s+bestopt = ).*/\1["--best","--lzma","--ultra-brute"]/' $APPDATA/Python/Python37/site-packages/pyinstaller/building/utils.py
|
||||
|
||||
|
||||
now is an excellent time to take another snapshot, but:
|
||||
* on win7: first do the 4g.nul thing again
|
||||
* on win10: first do a reboot so fstrim kicks in
|
||||
then shutdown and: vmsnap the.qcow2 snap2
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## ============================================================
|
||||
## creating the windows VM templates
|
||||
##
|
||||
|
||||
bash ~/dev/asm/doc/setup-virhost.sh # github:9001/asm
|
||||
truncate -s 4G ~/dev/pyi/4g.nul # win7 "trim"
|
||||
|
||||
note: if you keep accidentally killing the vm with alt-f4 then remove "-device usb-tablet" in the qemu commands below
|
||||
|
||||
# win7: don't bother with virtio stuff since win7 doesn't fstrim properly anyways (4g.nul takes care of that)
|
||||
rm -f win7-x32.qcow2
|
||||
qemu-img create -f qcow2 win7-x32.qcow2 64g
|
||||
qemu-system-x86_64 -m 4096 -enable-kvm --machine q35 -cpu host -smp 4 -usb -device usb-tablet -net bridge,br=virhost0 -net nic,model=e1000e -drive file=win7-x32.qcow2,discard=unmap,detect-zeroes=unmap \
|
||||
-cdrom ~/iso/win7-X17-59183-english-32bit-professional.iso
|
||||
|
||||
# win10: use virtio hdd and net (viostor+netkvm), but do NOT use qxl graphics (kills mouse cursor)
|
||||
rm -f win10-e2021.qcow2
|
||||
qemu-img create -f qcow2 win10-e2021.qcow2 64g
|
||||
qemu-system-x86_64 -m 4096 -enable-kvm --machine q35 -cpu host -smp 4 -usb -device usb-tablet -net bridge,br=virhost0 -net nic,model=virtio -drive file=win10-e2021.qcow2,if=virtio,discard=unmap \
|
||||
-drive file=$HOME/iso/virtio-win-0.1.248.iso,media=cdrom -cdrom $HOME/iso/en-us_windows_10_enterprise_ltsc_2021_x64_dvd_d289cf96.iso
|
||||
|
||||
tweak stuff to your preference, but also do these steps in order:
|
||||
* press ctrl-alt-g so you don't accidentally alt-f4 the vm
|
||||
* startmenu, type "sysdm.cpl" and hit Enter,
|
||||
* system protection -> configure -> disable
|
||||
* advanced > performance > advanced > virtual memory > no paging file
|
||||
* startmenu, type "cmd" and hit Ctrl-Shift-Enter, run command: powercfg /h off
|
||||
* reboot
|
||||
* make screen resolution something comfy (1440x900 is always a winner)
|
||||
* cmd.exe window-width 176 (assuming 1440x900) and buffer-height 8191
|
||||
* fix explorer settings (show hidden files and file extensions)
|
||||
* WIN10-ONLY: startmenu, device manager, install netkvm driver for ethernet
|
||||
* create ftp2host.bat on desktop with following contents:
|
||||
start explorer ftp://wark:k@192.168.123.1:3921/ro/pyi/
|
||||
* WIN7-ONLY: connect to ftp, download 4g.nul to desktop, then delete it (poor man's fstrim...)
|
||||
|
||||
and finally take snapshots of the VMs by copypasting this stuff into your shell:
|
||||
vmsnap() { zstd --long=31 -vT0 -19 <$1 >$1.$2; };
|
||||
vmsnap win7-x32.qcow2 snap1
|
||||
vmsnap win10-e2021.qcow2 snap1
|
||||
|
||||
note: vmsnap could have defragged the qcow2 as well, but
|
||||
that makes it hard to do xdelta3 memes so it's not worth it --
|
||||
but you can add this before "zstd" if you still want to:
|
||||
qemu-img convert -f qcow2 -O qcow2 $1 a.qcow2 && mv a.qcow2 $1 &&
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## ============================================================
|
||||
|
||||
@@ -25,7 +25,7 @@ grep -E '^from .ssl_ import' $APPDATA/python/python37/site-packages/urllib3/util
|
||||
echo golfing
|
||||
echo > $APPDATA/python/python37/site-packages/requests/certs.py
|
||||
sed -ri 's/^(DEFAULT_CA_BUNDLE_PATH = ).*/\1""/' $APPDATA/python/python37/site-packages/requests/utils.py
|
||||
sed -ri '/^import zipfile$/d' $APPDATA/python/python37/site-packages/requests/utils.py
|
||||
sed -ri '/^import zipfile$/d' $APPDATA/python/python37/site-packages/requests/utils.py
|
||||
sed -ri 's/"idna"//' $APPDATA/python/python37/site-packages/requests/packages.py
|
||||
sed -ri 's/import charset_normalizer.*/pass/' $APPDATA/python/python37/site-packages/requests/compat.py
|
||||
sed -ri 's/raise.*charset_normalizer.*/pass/' $APPDATA/python/python37/site-packages/requests/__init__.py
|
||||
@@ -37,6 +37,8 @@ grep -E '^from .ssl_ import' $APPDATA/python/python37/site-packages/urllib3/util
|
||||
echo golfed
|
||||
}
|
||||
|
||||
sed -ri 's/(add_argument."-t[de]",.*help=")[^"]+/\1not applicable; HTTPS is disabled in this exe/; s/for some reason/in this exe for safety reasons/' u2c.py
|
||||
|
||||
read a b _ < <(awk -F\" '/^S_VERSION =/{$0=$2;sub(/\./," ");print}' < u2c.py)
|
||||
sed -r 's/1,2,3,0/'$a,$b,0,0'/;s/1\.2\.3/'$a.$b.0/ <up2k.rc >up2k.rc2
|
||||
|
||||
@@ -45,4 +47,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
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ $f$s.py --version >/dev/null
|
||||
min=99999999
|
||||
for ((a=0; a<$parallel; a++)); do
|
||||
while [ -e .sfx-run ]; do
|
||||
CSN=sfx$a ./make-sfx.sh re "$@"
|
||||
CSN=$a ./make-sfx.sh re "$@"
|
||||
sz=$(wc -c <$f$a$s.py | awk '{print$1}')
|
||||
[ $sz -ge $min ] && continue
|
||||
mv $f$a$s.py $f$s.py.$sz
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
|
||||
if uname | grep -iE '^(msys|mingw)'; then
|
||||
pids=()
|
||||
|
||||
python -m unittest discover -s tests >/dev/null &
|
||||
pids+=($!)
|
||||
|
||||
python scripts/test/smoketest.py &
|
||||
pids+=($!)
|
||||
|
||||
for pid in ${pids[@]}; do
|
||||
wait $pid
|
||||
done
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# osx support
|
||||
gtar=$(command -v gtar || command -v gnutar) || true
|
||||
[ ! -z "$gtar" ] && command -v gfind >/dev/null && {
|
||||
|
||||
@@ -81,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,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding: latin-1
|
||||
from __future__ import print_function, unicode_literals
|
||||
import re, os, sys, time, shutil, signal, threading, tarfile, hashlib, platform, tempfile, traceback
|
||||
import re, os, sys, time, shutil, signal, tarfile, hashlib, platform, tempfile, traceback
|
||||
import subprocess as sp
|
||||
|
||||
|
||||
@@ -368,17 +368,6 @@ def get_payload():
|
||||
p = a
|
||||
|
||||
|
||||
def utime(top):
|
||||
# avoid cleaners
|
||||
files = [os.path.join(dp, p) for dp, dd, df in os.walk(top) for p in dd + df]
|
||||
while True:
|
||||
t = int(time.time())
|
||||
for f in [top] + files:
|
||||
os.utime(f, (t, t))
|
||||
|
||||
time.sleep(78123)
|
||||
|
||||
|
||||
def confirm(rv):
|
||||
msg()
|
||||
msg("retcode", rv if rv else traceback.format_exc())
|
||||
@@ -398,9 +387,7 @@ def run(tmp, j2, ftp):
|
||||
msg("sfxdir:", tmp)
|
||||
msg()
|
||||
|
||||
t = threading.Thread(target=utime, args=(tmp,))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
sys.argv.append("--sfx-tpoke=" + tmp)
|
||||
|
||||
ld = (("", ""), (j2, "j2"), (ftp, "ftp"), (not PY2, "py2"), (PY37, "py37"))
|
||||
ld = [os.path.join(tmp, b) for a, b in ld if not a]
|
||||
|
||||
@@ -47,6 +47,14 @@ def uh(top):
|
||||
|
||||
|
||||
def uh1(fp):
|
||||
try:
|
||||
uh2(fp)
|
||||
except:
|
||||
print("failed to process", fp)
|
||||
raise
|
||||
|
||||
|
||||
def uh2(fp):
|
||||
pr(".")
|
||||
cs = strip_file_to_string(fp, no_ast=True, to_empty=True)
|
||||
|
||||
|
||||
109
scripts/ziploader.py
Normal file
109
scripts/ziploader.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import atexit
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import time
|
||||
import traceback
|
||||
|
||||
|
||||
VER = None
|
||||
STAMP = None
|
||||
WINDOWS = sys.platform in ["win32", "msys"]
|
||||
|
||||
|
||||
def msg(*a, **ka):
|
||||
if a:
|
||||
a = ["[ZIP]", a[0]] + list(a[1:])
|
||||
|
||||
ka["file"] = sys.stderr
|
||||
print(*a, **ka)
|
||||
|
||||
|
||||
def utime(top):
|
||||
# avoid cleaners
|
||||
files = [os.path.join(dp, p) for dp, dd, df in os.walk(top) for p in dd + df]
|
||||
try:
|
||||
while True:
|
||||
t = int(time.time())
|
||||
for f in [top] + files:
|
||||
os.utime(f, (t, t))
|
||||
|
||||
time.sleep(78123)
|
||||
except Exception as ex:
|
||||
print("utime:", ex, f)
|
||||
|
||||
|
||||
def confirm(rv):
|
||||
msg()
|
||||
msg("retcode", rv if rv else traceback.format_exc())
|
||||
if WINDOWS:
|
||||
msg("*** hit enter to exit ***")
|
||||
try:
|
||||
input()
|
||||
except:
|
||||
pass
|
||||
|
||||
sys.exit(rv or 1)
|
||||
|
||||
|
||||
def run():
|
||||
import copyparty
|
||||
from copyparty.__main__ import main as cm
|
||||
|
||||
td = tempfile.TemporaryDirectory(prefix="")
|
||||
atexit.register(td.cleanup)
|
||||
rsrc = td.name
|
||||
|
||||
try:
|
||||
from importlib.resources import files
|
||||
|
||||
f = files(copyparty).joinpath("z.tar").open("rb")
|
||||
except:
|
||||
from importlib.resources import open_binary
|
||||
|
||||
f = open_binary("copyparty", "z.tar")
|
||||
|
||||
with tarfile.open(fileobj=f) as tf:
|
||||
try:
|
||||
tf.extractall(rsrc, filter="tar")
|
||||
except TypeError:
|
||||
tf.extractall(rsrc) # nosec (archive is safe)
|
||||
|
||||
f.close()
|
||||
f = None
|
||||
|
||||
msg(" rsrc dir:", rsrc)
|
||||
msg()
|
||||
|
||||
sys.argv.append("--sfx-tpoke=" + rsrc)
|
||||
|
||||
cm(rsrc=rsrc)
|
||||
|
||||
|
||||
def main():
|
||||
sysver = str(sys.version).replace("\n", "\n" + " " * 18)
|
||||
pktime = time.strftime("%Y-%m-%d, %H:%M:%S", time.gmtime(STAMP))
|
||||
msg()
|
||||
msg(" this is: copyparty", VER)
|
||||
msg(" packed at:", pktime, "UTC,", STAMP)
|
||||
msg("python bin:", sys.executable)
|
||||
msg("python ver:", platform.python_implementation(), sysver)
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit as ex:
|
||||
c = ex.code
|
||||
if c not in [0, -15]:
|
||||
confirm(ex.code)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except:
|
||||
confirm(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
2
setup.py
2
setup.py
@@ -141,7 +141,7 @@ args = {
|
||||
"audiotags": ["mutagen"],
|
||||
"ftpd": ["pyftpdlib"],
|
||||
"ftps": ["pyftpdlib", "pyopenssl"],
|
||||
"tftpd": ["partftpy>=0.3.1"],
|
||||
"tftpd": ["partftpy>=0.4.0"],
|
||||
"pwhash": ["argon2-cffi"],
|
||||
},
|
||||
"entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]},
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tarfile
|
||||
@@ -22,7 +23,7 @@ def hdr(query, uname):
|
||||
return (h % (query, uname)).encode("utf-8")
|
||||
|
||||
|
||||
class TestHttpCli(unittest.TestCase):
|
||||
class TestDots(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.td = tu.get_ramdisk()
|
||||
|
||||
@@ -30,7 +31,7 @@ class TestHttpCli(unittest.TestCase):
|
||||
os.chdir(tempfile.gettempdir())
|
||||
shutil.rmtree(self.td)
|
||||
|
||||
def test(self):
|
||||
def test_dots(self):
|
||||
td = os.path.join(self.td, "vfs")
|
||||
os.mkdir(td)
|
||||
os.chdir(td)
|
||||
@@ -62,7 +63,9 @@ class TestHttpCli(unittest.TestCase):
|
||||
|
||||
self.assertEqual(self.curl("?tar", "x")[1][:17], "\nJ2EOT")
|
||||
|
||||
# search
|
||||
##
|
||||
## search
|
||||
|
||||
up2k = Up2k(self)
|
||||
u2idx = U2idx(self)
|
||||
allvols = list(self.asrv.vfs.all_vols.values())
|
||||
@@ -77,7 +80,8 @@ class TestHttpCli(unittest.TestCase):
|
||||
x = " ".join(sorted([x["rp"] for x in x[0]]))
|
||||
self.assertEqual(x, ".f0 .t/.f2 .t/f2 a/da/f4 a/f3 f0 t/.f1 t/f1")
|
||||
|
||||
self.args = Cfg(v=vcfg, a=["u1:u1", "u2:u2"], dotsrch=False)
|
||||
u2idx.shutdown()
|
||||
self.args = Cfg(v=vcfg, a=["u1:u1", "u2:u2"], dotsrch=False, e2d=True)
|
||||
self.asrv = AuthSrv(self.args, self.log)
|
||||
u2idx = U2idx(self)
|
||||
|
||||
@@ -86,16 +90,266 @@ class TestHttpCli(unittest.TestCase):
|
||||
# u1 can see dotfiles in volB so they should be included
|
||||
xe = "a/da/f4 a/f3 f0 t/f1"
|
||||
self.assertEqual(x, xe)
|
||||
u2idx.shutdown()
|
||||
up2k.shutdown()
|
||||
|
||||
##
|
||||
## dirkeys
|
||||
|
||||
os.mkdir("v")
|
||||
with open("v/f1.txt", "wb") as f:
|
||||
f.write(b"a")
|
||||
os.rename("a", "v/a")
|
||||
os.rename(".b", "v/.b")
|
||||
|
||||
vcfg = [
|
||||
".::r.,u1:g,u2:c,dk",
|
||||
"v/a:v/a:r.,u1:g,u2:c,dk",
|
||||
"v/.b:v/.b:r.,u1:g,u2:c,dk",
|
||||
]
|
||||
self.args = Cfg(v=vcfg, a=["u1:u1", "u2:u2"])
|
||||
self.asrv = AuthSrv(self.args, self.log)
|
||||
zj = json.loads(self.curl("?ls", "u1")[1])
|
||||
url = "?k=" + zj["dk"]
|
||||
# should descend into folders, but not other volumes:
|
||||
self.assertEqual(self.tardir(url, "u2"), "f0 t/f1 v/f1.txt")
|
||||
|
||||
zj = json.loads(self.curl("v?ls", "u1")[1])
|
||||
url = "v?k=" + zj["dk"]
|
||||
self.assertEqual(self.tarsel(url, "u2", ["f1.txt", "a", ".b"]), "f1.txt")
|
||||
|
||||
shutil.rmtree("v")
|
||||
|
||||
def test_dk_fk(self):
|
||||
# python3 -m unittest tests.test_dots.TestDots.test_dk_fk
|
||||
|
||||
td = os.path.join(self.td, "vfs")
|
||||
os.mkdir(td)
|
||||
os.chdir(td)
|
||||
|
||||
vcfg = []
|
||||
for k in "dk dks dky fk fka dk,fk dks,fk".split():
|
||||
vcfg += ["{0}:{0}:r.,u1:g,u2:c,{0}".format(k)]
|
||||
zs = "%s/s1/s2" % (k,)
|
||||
os.makedirs(zs)
|
||||
|
||||
with open("%s/f.t1" % (k,), "wb") as f:
|
||||
f.write(b"f1")
|
||||
|
||||
with open("%s/s1/f.t2" % (k,), "wb") as f:
|
||||
f.write(b"f2")
|
||||
|
||||
with open("%s/s1/s2/f.t3" % (k,), "wb") as f:
|
||||
f.write(b"f3")
|
||||
|
||||
self.args = Cfg(v=vcfg, a=["u1:u1", "u2:u2"])
|
||||
self.asrv = AuthSrv(self.args, self.log)
|
||||
|
||||
dk = {}
|
||||
for d in "dk dks dk,fk dks,fk".split():
|
||||
zj = json.loads(self.curl("%s?ls" % (d,), "u1")[1])
|
||||
dk[d] = zj["dk"]
|
||||
|
||||
##
|
||||
## dk
|
||||
|
||||
# should not be able to access dk with wrong dirkey,
|
||||
zs = self.curl("dk?ls&k=%s" % (dk["dks"]), "u2")[1]
|
||||
self.assertEqual(zs, "\nJ2EOT")
|
||||
# so use the right key
|
||||
zs = self.curl("dk?ls&k=%s" % (dk["dk"]), "u2")[1]
|
||||
zj = json.loads(zs)
|
||||
self.assertEqual(len(zj["dirs"]), 0)
|
||||
self.assertEqual(len(zj["files"]), 1)
|
||||
self.assertEqual(zj["files"][0]["href"], "f.t1")
|
||||
|
||||
##
|
||||
## dk thumbs
|
||||
|
||||
self.assertIn('">folder</text>', self.curl("dk?th=x", "u1")[1])
|
||||
self.assertIn('">e403</text>', self.curl("dk?th=x", "u2")[1])
|
||||
|
||||
zs = "dk?th=x&k=%s" % (dk["dks"])
|
||||
self.assertIn('">e403</text>', self.curl(zs, "u2")[1])
|
||||
|
||||
zs = "dk?th=x&k=%s" % (dk["dk"])
|
||||
self.assertIn('">folder</text>', self.curl(zs, "u2")[1])
|
||||
|
||||
# fk not enabled, so this should work
|
||||
self.assertIn('">t1</text>', self.curl("dk/f.t1?th=x", "u2")[1])
|
||||
self.assertIn('">t2</text>', self.curl("dk/s1/f.t2?th=x", "u2")[1])
|
||||
|
||||
##
|
||||
## dks
|
||||
|
||||
# should not be able to access dks with wrong dirkey,
|
||||
zs = self.curl("dks?ls&k=%s" % (dk["dk"]), "u2")[1]
|
||||
self.assertEqual(zs, "\nJ2EOT")
|
||||
# so use the right key
|
||||
zs = self.curl("dks?ls&k=%s" % (dk["dks"]), "u2")[1]
|
||||
zj = json.loads(zs)
|
||||
self.assertEqual(len(zj["dirs"]), 1)
|
||||
self.assertEqual(len(zj["files"]), 1)
|
||||
self.assertEqual(zj["files"][0]["href"], "f.t1")
|
||||
# dks should return correct dirkey of subfolders;
|
||||
s1 = zj["dirs"][0]["href"]
|
||||
self.assertEqual(s1.split("/")[0], "s1")
|
||||
zs = self.curl("dks/%s&ls" % (s1), "u2")[1]
|
||||
self.assertIn('"s2/?k=', zs)
|
||||
|
||||
##
|
||||
## dks thumbs
|
||||
|
||||
self.assertIn('">folder</text>', self.curl("dks?th=x", "u1")[1])
|
||||
self.assertIn('">e403</text>', self.curl("dks?th=x", "u2")[1])
|
||||
|
||||
zs = "dks?th=x&k=%s" % (dk["dk"])
|
||||
self.assertIn('">e403</text>', self.curl(zs, "u2")[1])
|
||||
|
||||
zs = "dks?th=x&k=%s" % (dk["dks"])
|
||||
self.assertIn('">folder</text>', self.curl(zs, "u2")[1])
|
||||
|
||||
# fk not enabled, so this should work
|
||||
self.assertIn('">t1</text>', self.curl("dks/f.t1?th=x", "u2")[1])
|
||||
self.assertIn('">t2</text>', self.curl("dks/s1/f.t2?th=x", "u2")[1])
|
||||
|
||||
##
|
||||
## dky
|
||||
|
||||
# doesn't care about keys
|
||||
zs = self.curl("dky?ls&k=ok", "u2")[1]
|
||||
self.assertEqual(zs, self.curl("dky?ls", "u2")[1])
|
||||
zj = json.loads(zs)
|
||||
self.assertEqual(len(zj["dirs"]), 0)
|
||||
self.assertEqual(len(zj["files"]), 1)
|
||||
self.assertEqual(zj["files"][0]["href"], "f.t1")
|
||||
|
||||
##
|
||||
## dky thumbs
|
||||
|
||||
self.assertIn('">folder</text>', self.curl("dky?th=x", "u1")[1])
|
||||
self.assertIn('">folder</text>', self.curl("dky?th=x", "u2")[1])
|
||||
|
||||
zs = "dky?th=x&k=%s" % (dk["dk"])
|
||||
self.assertIn('">folder</text>', self.curl(zs, "u2")[1])
|
||||
|
||||
# fk not enabled, so this should work
|
||||
self.assertIn('">t1</text>', self.curl("dky/f.t1?th=x", "u2")[1])
|
||||
self.assertIn('">t2</text>', self.curl("dky/s1/f.t2?th=x", "u2")[1])
|
||||
|
||||
##
|
||||
## dk+fk
|
||||
|
||||
# should not be able to access dk with wrong dirkey,
|
||||
zs = self.curl("dk,fk?ls&k=%s" % (dk["dk"]), "u2")[1]
|
||||
self.assertEqual(zs, "\nJ2EOT")
|
||||
# so use the right key
|
||||
zs = self.curl("dk,fk?ls&k=%s" % (dk["dk,fk"]), "u2")[1]
|
||||
zj = json.loads(zs)
|
||||
self.assertEqual(len(zj["dirs"]), 0)
|
||||
self.assertEqual(len(zj["files"]), 1)
|
||||
self.assertEqual(zj["files"][0]["href"][:7], "f.t1?k=")
|
||||
|
||||
##
|
||||
## dk+fk thumbs
|
||||
|
||||
self.assertIn('">folder</text>', self.curl("dk,fk?th=x", "u1")[1])
|
||||
self.assertIn('">e403</text>', self.curl("dk,fk?th=x", "u2")[1])
|
||||
|
||||
zs = "dk,fk?th=x&k=%s" % (dk["dk"])
|
||||
self.assertIn('">e403</text>', self.curl(zs, "u2")[1])
|
||||
|
||||
zs = "dk,fk?th=x&k=%s" % (dk["dk,fk"])
|
||||
self.assertIn('">folder</text>', self.curl(zs, "u2")[1])
|
||||
|
||||
# fk enabled, so this should fail
|
||||
self.assertIn('">e404</text>', self.curl("dk,fk/f.t1?th=x", "u2")[1])
|
||||
self.assertIn('">e404</text>', self.curl("dk,fk/s1/f.t2?th=x", "u2")[1])
|
||||
|
||||
# but dk should return correct filekeys, so try that
|
||||
zs = "dk,fk/%s&th=x" % (zj["files"][0]["href"])
|
||||
self.assertIn('">t1</text>', self.curl(zs, "u2")[1])
|
||||
|
||||
##
|
||||
## dks+fk
|
||||
|
||||
# should not be able to access dk with wrong dirkey,
|
||||
zs = self.curl("dks,fk?ls&k=%s" % (dk["dk"]), "u2")[1]
|
||||
self.assertEqual(zs, "\nJ2EOT")
|
||||
# so use the right key
|
||||
zs = self.curl("dks,fk?ls&k=%s" % (dk["dks,fk"]), "u2")[1]
|
||||
zj = json.loads(zs)
|
||||
self.assertEqual(len(zj["dirs"]), 1)
|
||||
self.assertEqual(len(zj["files"]), 1)
|
||||
self.assertEqual(zj["dirs"][0]["href"][:6], "s1/?k=")
|
||||
self.assertEqual(zj["files"][0]["href"][:7], "f.t1?k=")
|
||||
|
||||
##
|
||||
## dks+fk thumbs
|
||||
|
||||
self.assertIn('">folder</text>', self.curl("dks,fk?th=x", "u1")[1])
|
||||
self.assertIn('">e403</text>', self.curl("dks,fk?th=x", "u2")[1])
|
||||
|
||||
zs = "dks,fk?th=x&k=%s" % (dk["dk"])
|
||||
self.assertIn('">e403</text>', self.curl(zs, "u2")[1])
|
||||
|
||||
zs = "dks,fk?th=x&k=%s" % (dk["dks,fk"])
|
||||
self.assertIn('">folder</text>', self.curl(zs, "u2")[1])
|
||||
|
||||
# subdir s1 without key
|
||||
zs = "dks,fk/s1/?th=x"
|
||||
self.assertIn('">e403</text>', self.curl(zs, "u2")[1])
|
||||
|
||||
# subdir s1 with bad key
|
||||
zs = "dks,fk/s1/?th=x&k=no"
|
||||
self.assertIn('">e403</text>', self.curl(zs, "u2")[1])
|
||||
|
||||
# subdir s1 with correct key
|
||||
zs = "dks,fk/%s&th=x" % (zj["dirs"][0]["href"])
|
||||
self.assertIn('">folder</text>', self.curl(zs, "u2")[1])
|
||||
|
||||
# fk enabled, so this should fail
|
||||
self.assertIn('">e404</text>', self.curl("dks,fk/f.t1?th=x", "u2")[1])
|
||||
self.assertIn('">e404</text>', self.curl("dks,fk/s1/f.t2?th=x", "u2")[1])
|
||||
|
||||
# but dk should return correct filekeys, so try that
|
||||
zs = "dks,fk/%s&th=x" % (zj["files"][0]["href"])
|
||||
self.assertIn('">t1</text>', self.curl(zs, "u2")[1])
|
||||
|
||||
# subdir
|
||||
self.assertIn('">e403</text>', self.curl("dks,fk/s1/?th=x", "u2")[1])
|
||||
self.assertEqual("\nJ2EOT", self.curl("dks,fk/s1/?ls", "u2")[1])
|
||||
zs = "dks,fk/s1%s&th=x" % (zj["files"][0]["href"])
|
||||
zs = self.curl("dks,fk?ls&k=%s" % (dk["dks,fk"]), "u2")[1]
|
||||
zj = json.loads(zs)
|
||||
url = "dks,fk/%s" % zj["dirs"][0]["href"]
|
||||
self.assertIn('"files"', self.curl(url + "&ls", "u2")[1])
|
||||
self.assertEqual("\nJ2EOT", self.curl(url + "x&ls", "u2")[1])
|
||||
|
||||
def tardir(self, url, uname):
|
||||
h, b = self.curl("/" + url + "?tar", uname, True)
|
||||
top = url.split("?")[0]
|
||||
top = ("top" if not top else top.lstrip(".").split("/")[0]) + "/"
|
||||
url += ("&" if "?" in url else "?") + "tar"
|
||||
h, b = self.curl(url, uname, True)
|
||||
tar = tarfile.open(fileobj=io.BytesIO(b), mode="r|").getnames()
|
||||
top = ("top" if not url else url.lstrip(".").split("/")[0]) + "/"
|
||||
assert len(tar) == len([x for x in tar if x.startswith(top)])
|
||||
if len(tar) != len([x for x in tar if x.startswith(top)]):
|
||||
raise Exception("bad-prefix:", tar)
|
||||
return " ".join([x[len(top) :] for x in tar])
|
||||
|
||||
def curl(self, url, uname, binary=False):
|
||||
conn = tu.VHttpConn(self.args, self.asrv, self.log, hdr(url, uname))
|
||||
def tarsel(self, url, uname, sel):
|
||||
url += ("&" if "?" in url else "?") + "tar"
|
||||
zs = '--XD\r\nContent-Disposition: form-data; name="act"\r\n\r\nzip\r\n--XD\r\nContent-Disposition: form-data; name="files"\r\n\r\n'
|
||||
zs += "\r\n".join(sel) + "\r\n--XD--\r\n"
|
||||
zb = zs.encode("utf-8")
|
||||
hdr = "POST /%s HTTP/1.1\r\nPW: %s\r\nConnection: close\r\nContent-Type: multipart/form-data; boundary=XD\r\nContent-Length: %d\r\n\r\n"
|
||||
req = (hdr % (url, uname, len(zb))).encode("utf-8") + zb
|
||||
h, b = self.curl("/" + url, uname, True, req)
|
||||
tar = tarfile.open(fileobj=io.BytesIO(b), mode="r|").getnames()
|
||||
return " ".join(tar)
|
||||
|
||||
def curl(self, url, uname, binary=False, req=b""):
|
||||
req = req or hdr(url, uname)
|
||||
conn = tu.VHttpConn(self.args, self.asrv, self.log, req)
|
||||
HttpCli(conn).run()
|
||||
if binary:
|
||||
h, b = conn.s._reply.split(b"\r\n\r\n", 1)
|
||||
@@ -104,4 +358,4 @@ class TestHttpCli(unittest.TestCase):
|
||||
return conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
|
||||
|
||||
def log(self, src, msg, c=0):
|
||||
print(msg)
|
||||
print(msg, "\033[0m")
|
||||
|
||||
@@ -6,6 +6,7 @@ import json
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from copyparty.__init__ import ANYWIN
|
||||
from copyparty.authsrv import AuthSrv
|
||||
from tests.util import Cfg
|
||||
|
||||
@@ -51,6 +52,12 @@ class TestVFS(unittest.TestCase):
|
||||
vn = self.nav(au, vp)
|
||||
self.assertNodes(vn, expected)
|
||||
|
||||
def assertApEq(self, ap, rhs):
|
||||
if ANYWIN and len(ap) > 2 and ap[1] == ":":
|
||||
ap = ap[2:].replace("\\", "/")
|
||||
|
||||
return self.assertEqual(ap, rhs)
|
||||
|
||||
def prep(self):
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
cfgdir = os.path.join(here, "res", "idp")
|
||||
@@ -70,7 +77,7 @@ class TestVFS(unittest.TestCase):
|
||||
au = AuthSrv(Cfg(c=[cfgdir + "/1.conf"], **xcfg), self.log)
|
||||
|
||||
self.assertEqual(au.vfs.vpath, "")
|
||||
self.assertEqual(au.vfs.realpath, "/")
|
||||
self.assertApEq(au.vfs.realpath, "/")
|
||||
self.assertNodes(au.vfs, ["vb"])
|
||||
self.assertNodes(au.vfs.nodes["vb"], [])
|
||||
|
||||
@@ -85,7 +92,7 @@ class TestVFS(unittest.TestCase):
|
||||
au = AuthSrv(Cfg(c=[cfgdir + "/2.conf"], **xcfg), self.log)
|
||||
|
||||
self.assertEqual(au.vfs.vpath, "")
|
||||
self.assertEqual(au.vfs.realpath, "/")
|
||||
self.assertApEq(au.vfs.realpath, "/")
|
||||
self.assertNodes(au.vfs, ["vb", "vc"])
|
||||
self.assertNodes(au.vfs.nodes["vb"], [])
|
||||
self.assertNodes(au.vfs.nodes["vc"], [])
|
||||
@@ -103,7 +110,7 @@ class TestVFS(unittest.TestCase):
|
||||
au = AuthSrv(Cfg(c=[cfgdir + "/3.conf"], **xcfg), self.log)
|
||||
|
||||
self.assertEqual(au.vfs.vpath, "")
|
||||
self.assertEqual(au.vfs.realpath, "")
|
||||
self.assertApEq(au.vfs.realpath, "")
|
||||
self.assertNodes(au.vfs, [])
|
||||
self.assertAxs(au.vfs.axs, [])
|
||||
|
||||
@@ -112,8 +119,8 @@ class TestVFS(unittest.TestCase):
|
||||
self.assertNodesAt(au, "vu", ["iua"]) # same as:
|
||||
self.assertNodes(au.vfs.nodes["vu"], ["iua"])
|
||||
self.assertNodes(au.vfs.nodes["vg"], ["iga"])
|
||||
self.assertEqual(au.vfs.nodes["vu"].realpath, "")
|
||||
self.assertEqual(au.vfs.nodes["vg"].realpath, "")
|
||||
self.assertApEq(au.vfs.nodes["vu"].realpath, "")
|
||||
self.assertApEq(au.vfs.nodes["vg"].realpath, "")
|
||||
self.assertAxs(au.vfs.axs, [])
|
||||
self.assertAxsAt(au, "vu/iua", [["iua"]]) # same as:
|
||||
self.assertAxs(self.nav(au, "vu/iua").axs, [["iua"]])
|
||||
@@ -127,7 +134,7 @@ class TestVFS(unittest.TestCase):
|
||||
au = AuthSrv(Cfg(c=[cfgdir + "/4.conf"], **xcfg), self.log)
|
||||
|
||||
self.assertEqual(au.vfs.vpath, "")
|
||||
self.assertEqual(au.vfs.realpath, "")
|
||||
self.assertApEq(au.vfs.realpath, "")
|
||||
self.assertNodes(au.vfs, ["vu"])
|
||||
self.assertNodesAt(au, "vu", ["ua", "ub"])
|
||||
self.assertAxs(au.vfs.axs, [])
|
||||
@@ -147,10 +154,10 @@ class TestVFS(unittest.TestCase):
|
||||
self.assertAxsAt(au, "vg", [])
|
||||
self.assertAxsAt(au, "vg/iga1", [["iua"]])
|
||||
self.assertAxsAt(au, "vg/iga2", [["iua", "ua"]])
|
||||
self.assertEqual(self.nav(au, "vu/ua").realpath, "/u-ua")
|
||||
self.assertEqual(self.nav(au, "vu/iua").realpath, "/u-iua")
|
||||
self.assertEqual(self.nav(au, "vg/iga1").realpath, "/g1-iga")
|
||||
self.assertEqual(self.nav(au, "vg/iga2").realpath, "/g2-iga")
|
||||
self.assertApEq(self.nav(au, "vu/ua").realpath, "/u-ua")
|
||||
self.assertApEq(self.nav(au, "vu/iua").realpath, "/u-iua")
|
||||
self.assertApEq(self.nav(au, "vg/iga1").realpath, "/g1-iga")
|
||||
self.assertApEq(self.nav(au, "vg/iga2").realpath, "/g2-iga")
|
||||
|
||||
au.idp_checkin(None, "iub", "iga")
|
||||
self.assertAxsAt(au, "vu/iua", [["iua"]])
|
||||
@@ -165,7 +172,7 @@ class TestVFS(unittest.TestCase):
|
||||
au = AuthSrv(Cfg(c=[cfgdir + "/5.conf"], **xcfg), self.log)
|
||||
|
||||
self.assertEqual(au.vfs.vpath, "")
|
||||
self.assertEqual(au.vfs.realpath, "")
|
||||
self.assertApEq(au.vfs.realpath, "")
|
||||
self.assertNodes(au.vfs, ["g", "ga", "gb"])
|
||||
self.assertAxs(au.vfs.axs, [])
|
||||
|
||||
@@ -196,7 +203,7 @@ class TestVFS(unittest.TestCase):
|
||||
|
||||
self.assertAxs(au.vfs.axs, [])
|
||||
self.assertEqual(au.vfs.vpath, "")
|
||||
self.assertEqual(au.vfs.realpath, "")
|
||||
self.assertApEq(au.vfs.realpath, "")
|
||||
self.assertNodes(au.vfs, [])
|
||||
|
||||
au.idp_checkin(None, "iua", "")
|
||||
|
||||
@@ -235,7 +235,7 @@ class TestVFS(unittest.TestCase):
|
||||
"""
|
||||
u a:123
|
||||
u asd:fgh:jkl
|
||||
|
||||
|
||||
./src
|
||||
/dst
|
||||
r a
|
||||
|
||||
@@ -43,8 +43,9 @@ if MACOS:
|
||||
|
||||
from copyparty.__init__ import E
|
||||
from copyparty.__main__ import init_E
|
||||
from copyparty.ico import Ico
|
||||
from copyparty.u2idx import U2idx
|
||||
from copyparty.util import FHC, Garda, Unrecv
|
||||
from copyparty.util import FHC, CachedDict, Garda, Unrecv
|
||||
|
||||
init_E(E)
|
||||
|
||||
@@ -110,31 +111,31 @@ class Cfg(Namespace):
|
||||
def __init__(self, a=None, v=None, c=None, **ka0):
|
||||
ka = {}
|
||||
|
||||
ex = "daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_lifetime no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw q rand smb srch_dbg stats vague_403 vc ver xdev xlink xvol"
|
||||
ex = "daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw og og_no_head og_s_title q rand smb srch_dbg stats uqe vague_403 vc ver xdev xlink xvol"
|
||||
ka.update(**{k: False for k in ex.split()})
|
||||
|
||||
ex = "dotpart dotsrch no_dhash no_fastboot no_rescan no_sendfile no_voldump re_dhash plain_ip"
|
||||
ex = "dotpart dotsrch no_dhash no_fastboot no_rescan no_sendfile no_snap no_voldump re_dhash plain_ip"
|
||||
ka.update(**{k: True for k in ex.split()})
|
||||
|
||||
ex = "ah_cli ah_gen css_browser hist js_browser no_forget no_hash no_idx nonsus_urls"
|
||||
ex = "ah_cli ah_gen css_browser hist js_browser mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua"
|
||||
ka.update(**{k: None for k in ex.split()})
|
||||
|
||||
ex = "hash_mt srch_time u2abort u2j"
|
||||
ex = "hash_mt srch_time u2abort u2j u2sz"
|
||||
ka.update(**{k: 1 for k in ex.split()})
|
||||
|
||||
ex = "reg_cap s_thead s_tbody th_convt"
|
||||
ex = "au_vol mtab_age reg_cap s_thead s_tbody th_convt"
|
||||
ka.update(**{k: 9 for k in ex.split()})
|
||||
|
||||
ex = "db_act df k304 loris re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo"
|
||||
ex = "db_act k304 loris re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo"
|
||||
ka.update(**{k: 0 for k in ex.split()})
|
||||
|
||||
ex = "ah_alg bname doctitle exit favico idp_h_usr html_head lg_sbf log_fk md_sbf name textfiles unlist vname R RS SR"
|
||||
ex = "ah_alg bname doctitle df exit favico idp_h_usr html_head lg_sbf log_fk md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i tcolor textfiles unlist vname R RS SR"
|
||||
ka.update(**{k: "" for k in ex.split()})
|
||||
|
||||
ex = "grp on403 on404 xad xar xau xban xbd xbr xbu xiu xm"
|
||||
ka.update(**{k: [] for k in ex.split()})
|
||||
|
||||
ex = "exp_lg exp_md th_coversd"
|
||||
ex = "exp_lg exp_md"
|
||||
ka.update(**{k: {} for k in ex.split()})
|
||||
|
||||
ka.update(ka0)
|
||||
@@ -145,6 +146,7 @@ class Cfg(Namespace):
|
||||
c=c,
|
||||
E=E,
|
||||
dbd="wal",
|
||||
dk_salt="b" * 16,
|
||||
fk_salt="a" * 16,
|
||||
idp_gsep=re.compile("[|:;+,]"),
|
||||
iobuf=256 * 1024,
|
||||
@@ -154,11 +156,16 @@ class Cfg(Namespace):
|
||||
mte={"a": True},
|
||||
mth={},
|
||||
mtp=[],
|
||||
mv_retry="0/0",
|
||||
rm_retry="0/0",
|
||||
s_rd_sz=256 * 1024,
|
||||
s_wr_sz=256 * 1024,
|
||||
sort="href",
|
||||
srch_hits=99999,
|
||||
th_covers=["folder.png"],
|
||||
th_coversd=["folder.png"],
|
||||
th_covers_set=set(["folder.png"]),
|
||||
th_coversd_set=set(["folder.png"]),
|
||||
th_crop="y",
|
||||
th_size="320x256",
|
||||
th_x3="n",
|
||||
@@ -243,12 +250,13 @@ class VHttpConn(object):
|
||||
self.bans = {}
|
||||
self.freshen_pwd = 0.0
|
||||
self.hsrv = VHttpSrv(args, asrv, log)
|
||||
self.ico = None
|
||||
self.ico = Ico(args)
|
||||
self.ipa_nm = None
|
||||
self.lf_url = None
|
||||
self.log_func = log
|
||||
self.log_src = "a"
|
||||
self.mutex = threading.Lock()
|
||||
self.pipes = CachedDict(1)
|
||||
self.u2mutex = threading.Lock()
|
||||
self.nbyte = 0
|
||||
self.nid = None
|
||||
@@ -257,3 +265,7 @@ class VHttpConn(object):
|
||||
self.u2fh = FHC()
|
||||
|
||||
self.get_u2idx = self.hsrv.get_u2idx
|
||||
|
||||
|
||||
if WINDOWS:
|
||||
os.system("rem")
|
||||
|
||||
Reference in New Issue
Block a user