Compare commits
126 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7caecf77c | ||
|
|
1fe30363c7 | ||
|
|
54a7256c8d | ||
|
|
8e8e4ff132 | ||
|
|
1dace72092 | ||
|
|
3a5c1d9faf | ||
|
|
f38c754301 | ||
|
|
fff38f484d | ||
|
|
95390b655f | ||
|
|
5967c421ca | ||
|
|
b8b5214f44 | ||
|
|
cdd3b67a5c | ||
|
|
28c9de3f6a | ||
|
|
f3b9bfc114 | ||
|
|
c9eba39edd | ||
|
|
40a1c7116e | ||
|
|
c03af9cfcc | ||
|
|
c4cbc32cc5 | ||
|
|
1231ce199e | ||
|
|
e0cac6fd99 | ||
|
|
d9db1534b1 | ||
|
|
6a0aaaf069 | ||
|
|
4c04798aa5 | ||
|
|
3f84b0a015 | ||
|
|
917380ddbb | ||
|
|
d9ae067e52 | ||
|
|
b2e8bf6e89 | ||
|
|
170cbe98c5 | ||
|
|
c94f662095 | ||
|
|
0987dcfb1c | ||
|
|
6920c01d4a | ||
|
|
cc0cc8cdf0 | ||
|
|
fb13969798 | ||
|
|
278258ee9f | ||
|
|
9e542cf86b | ||
|
|
244e952f79 | ||
|
|
aa2a8fa223 | ||
|
|
467acb47bf | ||
|
|
0c0d6b2bfc | ||
|
|
ce0e5be406 | ||
|
|
65ce4c90fa | ||
|
|
9897a08d09 | ||
|
|
f5753ba720 | ||
|
|
fcf32a935b | ||
|
|
ec50788987 | ||
|
|
ac0a2da3b5 | ||
|
|
9f84dc42fe | ||
|
|
21f9304235 | ||
|
|
5cedd22bbd | ||
|
|
c0dacbc4dd | ||
|
|
dd6e9ea70c | ||
|
|
87598dcd7f | ||
|
|
3bb7b677f8 | ||
|
|
988a7223f4 | ||
|
|
7f044372fa | ||
|
|
552897abbc | ||
|
|
946a8c5baa | ||
|
|
888b31aa92 | ||
|
|
e2dec2510f | ||
|
|
da5ad2ab9f | ||
|
|
eaa4b04a22 | ||
|
|
3051b13108 | ||
|
|
4c4e48bab7 | ||
|
|
01a3eb29cb | ||
|
|
73f7249c5f | ||
|
|
18c6559199 | ||
|
|
e66ece993f | ||
|
|
0686860624 | ||
|
|
24ce46b380 | ||
|
|
a49bf81ff2 | ||
|
|
64501fd7f1 | ||
|
|
db3c0b0907 | ||
|
|
edda117a7a | ||
|
|
cdface0dd5 | ||
|
|
be6afe2d3a | ||
|
|
9163780000 | ||
|
|
d7aa7dfe64 | ||
|
|
f1decb531d | ||
|
|
99399c698b | ||
|
|
1f5f42f216 | ||
|
|
9082c4702f | ||
|
|
6cedcfbf77 | ||
|
|
8a631f045e | ||
|
|
a6a2ee5b6b | ||
|
|
016708276c | ||
|
|
4cfdc4c513 | ||
|
|
0f257c9308 | ||
|
|
c8104b6e78 | ||
|
|
1a1d731043 | ||
|
|
c5a000d2ae | ||
|
|
94d1924fa9 | ||
|
|
6c1cf68bca | ||
|
|
395af051bd | ||
|
|
42fd66675e | ||
|
|
21a3f3699b | ||
|
|
d168b2acac | ||
|
|
2ce8233921 | ||
|
|
697a4fa8a4 | ||
|
|
2f83c6c7d1 | ||
|
|
127f414e9c | ||
|
|
33c4ccffab | ||
|
|
bafe7f5a09 | ||
|
|
baf41112d1 | ||
|
|
a90dde94e1 | ||
|
|
7dfbfc7227 | ||
|
|
b10843d051 | ||
|
|
520ac8f4dc | ||
|
|
537a6e50e9 | ||
|
|
2d0cbdf1a8 | ||
|
|
5afb562aa3 | ||
|
|
db069c3d4a | ||
|
|
fae40c7e2f | ||
|
|
0c43b592dc | ||
|
|
2ab8924e2d | ||
|
|
0e31cfa784 | ||
|
|
8f7ffcf350 | ||
|
|
9c8507a0fd | ||
|
|
e9b2cab088 | ||
|
|
d3ccacccb1 | ||
|
|
df386c8fbc | ||
|
|
4d15dd6e17 | ||
|
|
56a0499636 | ||
|
|
10fc4768e8 | ||
|
|
2b63d7d10d | ||
|
|
1f177528c1 | ||
|
|
fc3bbb70a3 |
138
README.md
138
README.md
@@ -48,6 +48,7 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
* [shares](#shares) - share a file or folder by creating a temporary link
|
||||
* [batch rename](#batch-rename) - select some files and press `F2` to bring up the rename UI
|
||||
* [rss feeds](#rss-feeds) - monitor a folder with your RSS reader
|
||||
* [recent uploads](#recent-uploads) - list all recent uploads
|
||||
* [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
|
||||
@@ -79,6 +80,7 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
* [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload
|
||||
* [file parser plugins](#file-parser-plugins) - provide custom parsers to index additional tags
|
||||
* [event hooks](#event-hooks) - trigger a program on uploads, renames etc ([examples](./bin/hooks/))
|
||||
* [zeromq](#zeromq) - event-hooks can send zeromq messages
|
||||
* [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/))
|
||||
* [handlers](#handlers) - redefine behavior with plugins ([examples](./bin/handlers/))
|
||||
* [ip auth](#ip-auth) - autologin based on IP range (CIDR)
|
||||
@@ -91,6 +93,7 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
* [listen on port 80 and 443](#listen-on-port-80-and-443) - become a *real* webserver
|
||||
* [reverse-proxy](#reverse-proxy) - running copyparty next to other websites
|
||||
* [real-ip](#real-ip) - teaching copyparty how to see client IPs
|
||||
* [reverse-proxy performance](#reverse-proxy-performance)
|
||||
* [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
|
||||
@@ -139,6 +142,7 @@ just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/
|
||||
* or if you cannot install python, you can use [copyparty.exe](#copypartyexe) instead
|
||||
* or install [on arch](#arch-package) ╱ [on NixOS](#nixos-module) ╱ [through nix](#nix-package)
|
||||
* or if you are on android, [install copyparty in termux](#install-on-android)
|
||||
* or maybe you have a [synology nas / dsm](./docs/synology-dsm.md)
|
||||
* 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:
|
||||
@@ -339,6 +343,9 @@ same order here too
|
||||
|
||||
* [Chrome issue 1352210](https://bugs.chromium.org/p/chromium/issues/detail?id=1352210) -- plaintext http may be faster at filehashing than https (but also extremely CPU-intensive)
|
||||
|
||||
* [Chrome issue 383568268](https://issues.chromium.org/issues/383568268) -- filereaders in webworkers can OOM / crash the browser-tab
|
||||
* copyparty has a workaround which seems to work well enough
|
||||
|
||||
* [Firefox issue 1790500](https://bugzilla.mozilla.org/show_bug.cgi?id=1790500) -- entire browser can crash after uploading ~4000 small files
|
||||
|
||||
* Android: music playback randomly stops due to [battery usage settings](#fix-unreliable-playback-on-android)
|
||||
@@ -346,10 +353,19 @@ same order here too
|
||||
* iPhones: the volume control doesn't work because [apple doesn't want it to](https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html#//apple_ref/doc/uid/TP40009523-CH5-SW11)
|
||||
* `AudioContext` will probably never be a viable workaround as apple introduces new issues faster than they fix current ones
|
||||
|
||||
* iPhones: music volume goes on a rollercoaster during song changes
|
||||
* nothing I can do about it because `AudioContext` is still broken in safari
|
||||
|
||||
* iPhones: the preload feature (in the media-player-options tab) can cause a tiny audio glitch 20sec before the end of each song, but disabling it may cause worse iOS bugs to appear instead
|
||||
* just a hunch, but disabling preloading may cause playback to stop entirely, or possibly mess with bluetooth speakers
|
||||
* tried to add a tooltip regarding this but looks like apple broke my tooltips
|
||||
|
||||
* iPhones: preloaded awo files make safari log MEDIA_ERR_NETWORK errors as playback starts, but the song plays just fine so eh whatever
|
||||
* awo, opus-weba, is apple's new take on opus support, replacing opus-caf which was technically limited to cbr opus
|
||||
|
||||
* iPhones: preloading another awo file may cause playback to stop
|
||||
* can be somewhat mitigated with `mp.au.play()` in `mp.onpreload` but that can hit a race condition in safari that starts playing the same audio object twice in parallel...
|
||||
|
||||
* Windows: folders cannot be accessed if the name ends with `.`
|
||||
* python or windows bug
|
||||
|
||||
@@ -609,8 +625,8 @@ select which type of archive you want in the `[⚙️] config` tab:
|
||||
| `pax` | `?tar=pax` | pax-format tar, futureproof, not as fast |
|
||||
| `tgz` | `?tar=gz` | gzip compressed gnu-tar (slow), for `curl \| tar -xvz` |
|
||||
| `txz` | `?tar=xz` | gnu-tar with xz / lzma compression (v.slow) |
|
||||
| `zip` | `?zip=utf8` | works everywhere, glitchy filenames on win7 and older |
|
||||
| `zip_dos` | `?zip` | traditional cp437 (no unicode) to fix glitchy filenames |
|
||||
| `zip` | `?zip` | works everywhere, glitchy filenames on win7 and older |
|
||||
| `zip_dos` | `?zip=dos` | traditional cp437 (no unicode) to fix glitchy filenames |
|
||||
| `zip_crc` | `?zip=crc` | cp437 with crc32 computed early for truly ancient software |
|
||||
|
||||
* gzip default level is `3` (0=fast, 9=best), change with `?tar=gz:9`
|
||||
@@ -618,7 +634,7 @@ select which type of archive you want in the `[⚙️] config` tab:
|
||||
* bz2 default level is `2` (1=fast, 9=best), change with `?tar=bz2:9`
|
||||
* hidden files ([dotfiles](#dotfiles)) are excluded unless account is allowed to list them
|
||||
* `up2k.db` and `dir.txt` is always excluded
|
||||
* bsdtar supports streaming unzipping: `curl foo?zip=utf8 | bsdtar -xv`
|
||||
* bsdtar supports streaming unzipping: `curl foo?zip | bsdtar -xv`
|
||||
* good, because copyparty's zip is faster than tar on small files
|
||||
* `zip_crc` will take longer to download since the server has to read each file twice
|
||||
* this is only to support MS-DOS PKZIP v2.04g (october 1993) and older
|
||||
@@ -642,7 +658,7 @@ 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)
|
||||
* upload using [curl, sharex, ishare, ...](#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
|
||||
@@ -714,7 +730,7 @@ files go into `[ok]` if they exist (and you get a link to where it is), otherwis
|
||||
|
||||
### unpost
|
||||
|
||||
undo/delete accidental uploads
|
||||
undo/delete accidental uploads using the `[🧯]` tab in the UI
|
||||
|
||||

|
||||
|
||||
@@ -873,6 +889,19 @@ url parameters:
|
||||
* uppercase = reverse-sort; `M` = oldest file first
|
||||
|
||||
|
||||
## recent uploads
|
||||
|
||||
list all recent uploads by clicking "show recent uploads" in the controlpanel
|
||||
|
||||
will show uploader IP and upload-time if the visitor has the admin permission
|
||||
|
||||
* global-option `--ups-when` makes upload-time visible to all users, and not just admins
|
||||
|
||||
* global-option `--ups-who` (volflag `ups_who`) specifies who gets access (0=nobody, 1=admins, 2=everyone), default=2
|
||||
|
||||
note that the [🧯 unpost](#unpost) feature is better suited for viewing *your own* recent uploads, as it includes the option to undo/delete them
|
||||
|
||||
|
||||
## media player
|
||||
|
||||
plays almost every audio format there is (if the server has FFmpeg installed for on-demand transcoding)
|
||||
@@ -909,6 +938,11 @@ open the `[🎺]` media-player-settings tab to configure it,
|
||||
* `[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`
|
||||
* "transcode to":
|
||||
* `[opus]` produces an `opus` whenever transcoding is necessary (the best choice on Android and PCs)
|
||||
* `[awo]` is `opus` in a `weba` file, good for iPhones (iOS 17.5 and newer) but Apple is still fixing some state-confusion bugs as of iOS 18.2.1
|
||||
* `[caf]` is `opus` in a `caf` file, good for iPhones (iOS 11 through 17), technically unsupported by Apple but works for the mos tpart
|
||||
* `[mp3]` -- the myth, the legend, the undying master of mediocre sound quality that definitely works everywhere
|
||||
* "tint" reduces the contrast of the playback bar
|
||||
|
||||
|
||||
@@ -1089,6 +1123,8 @@ on macos, connect from finder:
|
||||
|
||||
in order to grant full write-access to webdav clients, the volflag `daw` must be set and the account must also have delete-access (otherwise the client won't be allowed to replace the contents of existing files, which is how webdav works)
|
||||
|
||||
> note: if you have enabled [IdP authentication](#identity-providers) then that may cause issues for some/most webdav clients; see [the webdav section in the IdP docs](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients)
|
||||
|
||||
|
||||
### connecting to webdav from windows
|
||||
|
||||
@@ -1097,11 +1133,12 @@ using the GUI (winXP or later):
|
||||
* on winXP only, click the `Sign up for online storage` hyperlink instead and put the URL there
|
||||
* providing your password as the username is recommended; the password field can be anything or empty
|
||||
|
||||
known client bugs:
|
||||
the webdav client that's built into windows has the following list of bugs; you can avoid all of these by connecting with rclone instead:
|
||||
* win7+ doesn't actually send the password to the server when reauthenticating after a reboot unless you first try to login with an incorrect password and then switch to the correct password
|
||||
* or just type your password into the username field instead to get around it entirely
|
||||
* connecting to a folder which allows anonymous read will make writing impossible, as windows has decided it doesn't need to login
|
||||
* workaround: connect twice; first to a folder which requires auth, then to the folder you actually want, and leave both of those mounted
|
||||
* or set the server-option `--dav-auth` to force password-auth for all webdav clients
|
||||
* win7+ may open a new tcp connection for every file and sometimes forgets to close them, eventually needing a reboot
|
||||
* maybe NIC-related (??), happens with win10-ltsc on e1000e but not virtio
|
||||
* windows cannot access folders which contain filenames with invalid unicode or forbidden characters (`<>:"/\|?*`), or names ending with `.`
|
||||
@@ -1268,7 +1305,7 @@ note:
|
||||
|
||||
### exclude-patterns
|
||||
|
||||
to save some time, you can provide a regex pattern for filepaths to only index by filename/path/size/last-modified (and not the hash of the file contents) by setting `--no-hash \.iso$` or the volflag `:c,nohash=\.iso$`, this has the following consequences:
|
||||
to save some time, you can provide a regex pattern for filepaths to only index by filename/path/size/last-modified (and not the hash of the file contents) by setting `--no-hash '\.iso$'` or the volflag `:c,nohash=\.iso$`, this has the following consequences:
|
||||
* initial indexing is way faster, especially when the volume is on a network disk
|
||||
* makes it impossible to [file-search](#file-search)
|
||||
* if someone uploads the same file contents, the upload will not be detected as a dupe, so it will not get symlinked or rejected
|
||||
@@ -1279,6 +1316,8 @@ similarly, you can fully ignore files/folders using `--no-idx [...]` and `:c,noi
|
||||
|
||||
if you set `--no-hash [...]` globally, you can enable hashing for specific volumes using flag `:c,nohash=`
|
||||
|
||||
to exclude certain filepaths from search-results, use `--srch-excl` or volflag `srch_excl` instead of `--no-idx`, for example `--srch-excl 'password|logs/[0-9]'`
|
||||
|
||||
### filesystem guards
|
||||
|
||||
avoid traversing into other filesystems using `--xdev` / volflag `:c,xdev`, skipping any symlinks or bind-mounts to another HDD for example
|
||||
@@ -1436,6 +1475,23 @@ there's a bunch of flags and stuff, see `--help-hooks`
|
||||
if you want to write your own hooks, see [devnotes](./docs/devnotes.md#event-hooks)
|
||||
|
||||
|
||||
### zeromq
|
||||
|
||||
event-hooks can send zeromq messages instead of running programs
|
||||
|
||||
to send a 0mq message every time a file is uploaded,
|
||||
|
||||
* `--xau zmq:pub:tcp://*:5556` sends a PUB to any/all connected SUB clients
|
||||
* `--xau t3,zmq:push:tcp://*:5557` sends a PUSH to exactly one connected PULL client
|
||||
* `--xau t3,j,zmq:req:tcp://localhost:5555` sends a REQ to the connected REP client
|
||||
|
||||
the PUSH and REQ examples have `t3` (timeout after 3 seconds) because they block if there's no clients to talk to
|
||||
|
||||
* the REQ example does `t3,j` to send extended upload-info as json instead of just the filesystem-path
|
||||
|
||||
see [zmq-recv.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/zmq-recv.py) if you need something to receive the messages with
|
||||
|
||||
|
||||
### upload events
|
||||
|
||||
the older, more powerful approach ([examples](./bin/mtag/)):
|
||||
@@ -1485,7 +1541,9 @@ replace copyparty passwords with oauth and such
|
||||
|
||||
you can disable the built-in password-based login system, and instead replace it with a separate piece of software (an identity provider) which will then handle authenticating / authorizing of users; this makes it possible to login with passkeys / fido2 / webauthn / yubikey / ldap / active directory / oauth / many other single-sign-on contraptions
|
||||
|
||||
a popular choice is [Authelia](https://www.authelia.com/) (config-file based), another one is [authentik](https://goauthentik.io/) (GUI-based, more complex)
|
||||
* the regular config-defined users will be used as a fallback for requests which don't include a valid (trusted) IdP username header
|
||||
|
||||
some popular identity providers are [Authelia](https://www.authelia.com/) (config-file based) and [authentik](https://goauthentik.io/) (GUI-based, more complex)
|
||||
|
||||
there is a [docker-compose example](./docs/examples/docker/idp-authelia-traefik) which is hopefully a good starting point (alternatively see [./docs/idp.md](./docs/idp.md) if you're the DIY type)
|
||||
|
||||
@@ -1521,12 +1579,16 @@ connecting to an aws s3 bucket and similar
|
||||
|
||||
there is no built-in support for this, but you can use FUSE-software such as [rclone](https://rclone.org/) / [geesefs](https://github.com/yandex-cloud/geesefs) / [JuiceFS](https://juicefs.com/en/) to first mount your cloud storage as a local disk, and then let copyparty use (a folder in) that disk as a volume
|
||||
|
||||
you may experience poor upload performance this way, but that can sometimes be fixed by specifying the volflag `sparse` to force the use of sparse files; this has improved the upload speeds from `1.5 MiB/s` to over `80 MiB/s` in one case, but note that you are also more likely to discover funny bugs in your FUSE software this way, so buckle up
|
||||
you will probably get decent speeds with the default config, however most likely restricted to using one TCP connection per file, so the upload-client won't be able to send multiple chunks in parallel
|
||||
|
||||
> before [v1.13.5](https://github.com/9001/copyparty/releases/tag/v1.13.5) it was recommended to use the volflag `sparse` to force-allow multiple chunks in parallel; this would improve the upload-speed from `1.5 MiB/s` to over `80 MiB/s` at the risk of provoking latent bugs in S3 or JuiceFS. But v1.13.5 added chunk-stitching, so this is now probably much less important. On the contrary, `nosparse` *may* now increase performance in some cases. Please try all three options (default, `sparse`, `nosparse`) as the optimal choice depends on your network conditions and software stack (both the FUSE-driver and cloud-server)
|
||||
|
||||
someone has also tested geesefs in combination with [gocryptfs](https://nuetzlich.net/gocryptfs/) with surprisingly good results, getting 60 MiB/s upload speeds on a gbit line, but JuiceFS won with 80 MiB/s using its built-in encryption
|
||||
|
||||
you may improve performance by specifying larger values for `--iobuf` / `--s-rd-sz` / `--s-wr-sz`
|
||||
|
||||
> if you've experimented with this and made interesting observations, please share your findings so we can add a section with specific recommendations :-)
|
||||
|
||||
|
||||
## hiding from google
|
||||
|
||||
@@ -1649,10 +1711,16 @@ some reverse proxies (such as [Caddy](https://caddyserver.com/)) can automatical
|
||||
|
||||
for improved security (and a 10% performance boost) consider listening on a unix-socket with `-i unix:770:www:/tmp/party.sock` (permission `770` means only members of group `www` can access it)
|
||||
|
||||
example webserver configs:
|
||||
example webserver / reverse-proxy configs:
|
||||
|
||||
* [nginx config](contrib/nginx/copyparty.conf) -- entire domain/subdomain
|
||||
* [apache2 config](contrib/apache/copyparty.conf) -- location-based
|
||||
* [apache config](contrib/apache/copyparty.conf)
|
||||
* caddy uds: `caddy reverse-proxy --from :8080 --to unix///dev/shm/party.sock`
|
||||
* caddy tcp: `caddy reverse-proxy --from :8081 --to http://127.0.0.1:3923`
|
||||
* [haproxy config](contrib/haproxy/copyparty.conf)
|
||||
* [lighttpd subdomain](contrib/lighttpd/subdomain.conf) -- entire domain/subdomain
|
||||
* [lighttpd subpath](contrib/lighttpd/subpath.conf) -- location-based (not optimal, but in case you need it)
|
||||
* [nginx config](contrib/nginx/copyparty.conf) -- recommended
|
||||
* [traefik config](contrib/traefik/copyparty.yaml)
|
||||
|
||||
|
||||
### real-ip
|
||||
@@ -1664,6 +1732,38 @@ if you (and maybe everybody else) keep getting a message that says `thank you fo
|
||||
for most common setups, there should be a helpful message in the server-log explaining what to do, but see [docs/xff.md](docs/xff.md) if you want to learn more, including a quick hack to **just make it work** (which is **not** recommended, but hey...)
|
||||
|
||||
|
||||
### reverse-proxy performance
|
||||
|
||||
most reverse-proxies support connecting to copyparty either using uds/unix-sockets (`/dev/shm/party.sock`, faster/recommended) or using tcp (`127.0.0.1`)
|
||||
|
||||
with copyparty listening on a uds / unix-socket / unix-domain-socket and the reverse-proxy connecting to that:
|
||||
|
||||
| index.html | upload | download | software |
|
||||
| ------------ | ----------- | ----------- | -------- |
|
||||
| 28'900 req/s | 6'900 MiB/s | 7'400 MiB/s | no-proxy |
|
||||
| 18'750 req/s | 3'500 MiB/s | 2'370 MiB/s | haproxy |
|
||||
| 9'900 req/s | 3'750 MiB/s | 2'200 MiB/s | caddy |
|
||||
| 18'700 req/s | 2'200 MiB/s | 1'570 MiB/s | nginx |
|
||||
| 9'700 req/s | 1'750 MiB/s | 1'830 MiB/s | apache |
|
||||
| 9'900 req/s | 1'300 MiB/s | 1'470 MiB/s | lighttpd |
|
||||
|
||||
when connecting the reverse-proxy to `127.0.0.1` instead (the basic and/or old-fasioned way), speeds are a bit worse:
|
||||
|
||||
| index.html | upload | download | software |
|
||||
| ------------ | ----------- | ----------- | -------- |
|
||||
| 21'200 req/s | 5'700 MiB/s | 6'700 MiB/s | no-proxy |
|
||||
| 14'500 req/s | 1'700 MiB/s | 2'170 MiB/s | haproxy |
|
||||
| 11'100 req/s | 2'750 MiB/s | 2'000 MiB/s | traefik |
|
||||
| 8'400 req/s | 2'300 MiB/s | 1'950 MiB/s | caddy |
|
||||
| 13'400 req/s | 1'100 MiB/s | 1'480 MiB/s | nginx |
|
||||
| 8'400 req/s | 1'000 MiB/s | 1'000 MiB/s | apache |
|
||||
| 6'500 req/s | 1'270 MiB/s | 1'500 MiB/s | lighttpd |
|
||||
|
||||
in summary, `haproxy > caddy > traefik > nginx > apache > lighttpd`, and use uds when possible (traefik does not support it yet)
|
||||
|
||||
* if these results are bullshit because my config exampels are bad, please submit corrections!
|
||||
|
||||
|
||||
## prometheus
|
||||
|
||||
metrics/stats can be enabled at URL `/.cpr/metrics` for grafana / prometheus / etc (openmetrics 1.0.0)
|
||||
@@ -1977,7 +2077,8 @@ interact with copyparty using non-browser clients
|
||||
* can be downloaded from copyparty: controlpanel -> connect -> [partyfuse.py](http://127.0.0.1:3923/.cpr/a/partyfuse.py)
|
||||
* [rclone](https://rclone.org/) as client can give ~5x performance, see [./docs/rclone.md](docs/rclone.md)
|
||||
|
||||
* sharex (screenshot utility): see [./contrib/sharex.sxcu](contrib/#sharexsxcu)
|
||||
* sharex (screenshot utility): see [./contrib/sharex.sxcu](./contrib/#sharexsxcu)
|
||||
* and for screenshots on macos, see [./contrib/ishare.iscu](./contrib/#ishareiscu)
|
||||
* and for screenshots on linux, see [./contrib/flameshot.sh](./contrib/flameshot.sh)
|
||||
|
||||
* contextlet (web browser integration); see [contrib contextlet](contrib/#send-to-cppcontextletjson)
|
||||
@@ -2241,13 +2342,13 @@ mandatory deps:
|
||||
|
||||
install these to enable bonus features
|
||||
|
||||
enable hashed passwords in config: `argon2-cffi`
|
||||
enable [hashed passwords](#password-hashing) in config: `argon2-cffi`
|
||||
|
||||
enable ftp-server:
|
||||
enable [ftp-server](#ftp-server):
|
||||
* for just plaintext FTP, `pyftpdlib` (is built into the SFX)
|
||||
* with TLS encryption, `pyftpdlib pyopenssl`
|
||||
|
||||
enable music tags:
|
||||
enable [music tags](#metadata-from-audio-files):
|
||||
* either `mutagen` (fast, pure-python, skips a few tags, makes copyparty GPL? idk)
|
||||
* or `ffprobe` (20x slower, more accurate, possibly dangerous depending on your distro and users)
|
||||
|
||||
@@ -2258,8 +2359,9 @@ enable [thumbnails](#thumbnails) of...
|
||||
* **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin`
|
||||
* **JPEG XL pictures:** `pyvips` or `ffmpeg`
|
||||
|
||||
enable [smb](#smb-server) support (**not** recommended):
|
||||
* `impacket==0.12.0`
|
||||
enable sending [zeromq messages](#zeromq) from event-hooks: `pyzmq`
|
||||
|
||||
enable [smb](#smb-server) support (**not** recommended): `impacket==0.12.0`
|
||||
|
||||
`pyvips` gives higher quality thumbnails than `Pillow` and is 320% faster, using 270% more ram: `sudo apt install libvips42 && python3 -m pip install --user -U pyvips`
|
||||
|
||||
|
||||
@@ -30,4 +30,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
|
||||
* [usb-eject.py](usb-eject.py) adds web-UI buttons to safe-remove usb flashdrives shared through copyparty
|
||||
* [msg-log.py](msg-log.py) is a guestbook; logs messages to a doc in the same folder
|
||||
|
||||
54
bin/hooks/usb-eject.js
Normal file
54
bin/hooks/usb-eject.js
Normal file
@@ -0,0 +1,54 @@
|
||||
// see usb-eject.py for usage
|
||||
|
||||
function usbclick() {
|
||||
QS('#treeul a[href="/usb/"]').click();
|
||||
}
|
||||
|
||||
function eject_cb() {
|
||||
var t = this.responseText;
|
||||
if (t.indexOf('can be safely unplugged') < 0 && t.indexOf('Device can be removed') < 0)
|
||||
return toast.err(30, 'usb eject failed:\n\n' + t);
|
||||
|
||||
toast.ok(5, esc(t.replace(/ - /g, '\n\n')));
|
||||
usbclick(); setTimeout(usbclick, 10);
|
||||
};
|
||||
|
||||
function add_eject_2(a) {
|
||||
var aw = a.getAttribute('href').split(/\//g);
|
||||
if (aw.length != 4 || aw[3])
|
||||
return;
|
||||
|
||||
var v = aw[2],
|
||||
k = 'umount_' + v;
|
||||
|
||||
qsr('#' + k);
|
||||
a.appendChild(mknod('span', k, '⏏'), a);
|
||||
|
||||
var o = ebi(k);
|
||||
o.style.cssText = 'position:absolute; right:1em; margin-top:-.2em; font-size:1.3em';
|
||||
o.onclick = function (e) {
|
||||
ev(e);
|
||||
var xhr = new XHR();
|
||||
xhr.open('POST', get_evpath(), true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8');
|
||||
xhr.send('msg=' + uricom_enc(':usb-eject:' + v + ':'));
|
||||
xhr.onload = xhr.onerror = eject_cb;
|
||||
toast.inf(10, "ejecting " + v + "...");
|
||||
};
|
||||
};
|
||||
|
||||
function add_eject() {
|
||||
for (var a of QSA('#treeul a[href^="/usb/"]'))
|
||||
add_eject_2(a);
|
||||
};
|
||||
|
||||
(function() {
|
||||
var f0 = treectl.rendertree;
|
||||
treectl.rendertree = function (res, ts, top0, dst, rst) {
|
||||
var ret = f0(res, ts, top0, dst, rst);
|
||||
add_eject();
|
||||
return ret;
|
||||
};
|
||||
})();
|
||||
|
||||
setTimeout(add_eject, 50);
|
||||
49
bin/hooks/usb-eject.py
Normal file
49
bin/hooks/usb-eject.py
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import stat
|
||||
import subprocess as sp
|
||||
import sys
|
||||
|
||||
|
||||
"""
|
||||
if you've found yourself using copyparty to serve flashdrives on a LAN
|
||||
and your only wish is that the web-UI had a button to unmount / safely
|
||||
remove those flashdrives, then boy howdy are you in the right place :D
|
||||
|
||||
put usb-eject.js in the webroot (or somewhere else http-accessible)
|
||||
then run copyparty with these args:
|
||||
|
||||
-v /run/media/ed:/usb:A:c,hist=/tmp/junk
|
||||
--xm=c1,bin/hooks/usb-eject.py
|
||||
--js-browser=/usb-eject.js
|
||||
|
||||
which does the following respectively,
|
||||
|
||||
* share all of /run/media/ed as /usb with admin for everyone
|
||||
and put the histpath somewhere it won't cause trouble
|
||||
* run the usb-eject hook with stdout redirect to the web-ui
|
||||
* add the complementary usb-eject.js to the browser
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
label = sys.argv[1].split(":usb-eject:")[1].split(":")[0]
|
||||
mp = "/run/media/ed/" + label
|
||||
# print("ejecting [%s]... " % (mp,), end="")
|
||||
mp = os.path.abspath(os.path.realpath(mp.encode("utf-8")))
|
||||
st = os.lstat(mp)
|
||||
if not stat.S_ISDIR(st.st_mode):
|
||||
raise Exception("not a regular directory")
|
||||
|
||||
cmd = [b"gio", b"mount", b"-e", mp]
|
||||
print(sp.check_output(cmd).decode("utf-8", "replace").strip())
|
||||
|
||||
except Exception as ex:
|
||||
print("unmount failed: %r" % (ex,))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -31,6 +31,9 @@ plugins in this section should only be used with appropriate precautions:
|
||||
* [very-bad-idea.py](./very-bad-idea.py) combined with [meadup.js](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/meadup.js) converts copyparty into a janky yet extremely flexible chromecast clone
|
||||
* also adds a virtual keyboard by @steinuil to the basic-upload tab for comfy couch crowd control
|
||||
* anything uploaded through the [android app](https://github.com/9001/party-up) (files or links) are executed on the server, meaning anyone can infect your PC with malware... so protect this with a password and keep it on a LAN!
|
||||
* [kamelåså](https://github.com/steinuil/kameloso) is a much better (and MUCH safer) alternative to this plugin
|
||||
* powered by [chicken-curry-banana-pineapple-peanut pizza](https://a.ocv.me/pub/g/i/2025/01/298437ce-8351-4c8c-861c-fa131d217999.jpg?cache) so you know it's good
|
||||
* and, unlike this plugin, kamelåså even has windows support (nice)
|
||||
|
||||
|
||||
# dependencies
|
||||
|
||||
@@ -6,6 +6,11 @@ WARNING -- DANGEROUS PLUGIN --
|
||||
running this plugin, they can execute malware on your machine
|
||||
so please keep this on a LAN and protect it with a password
|
||||
|
||||
here is a MUCH BETTER ALTERNATIVE (which also works on Windows):
|
||||
https://github.com/steinuil/kameloso
|
||||
|
||||
----------------------------------------------------------------------
|
||||
|
||||
use copyparty as a chromecast replacement:
|
||||
* post a URL and it will open in the default browser
|
||||
* upload a file and it will open in the default application
|
||||
|
||||
115
bin/u2c.py
115
bin/u2c.py
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
S_VERSION = "2.6"
|
||||
S_BUILD_DT = "2024-11-10"
|
||||
S_VERSION = "2.9"
|
||||
S_BUILD_DT = "2025-01-27"
|
||||
|
||||
"""
|
||||
u2c.py: upload to copyparty
|
||||
@@ -234,6 +234,10 @@ CLEN = "Content-Length"
|
||||
|
||||
web = None # type: HCli
|
||||
|
||||
links = [] # type: list[str]
|
||||
linkmtx = threading.Lock()
|
||||
linkfile = None
|
||||
|
||||
|
||||
class File(object):
|
||||
"""an up2k upload task; represents a single file"""
|
||||
@@ -761,6 +765,29 @@ def get_hashlist(file, pcb, mth):
|
||||
file.kchunks[k] = [v1, v2]
|
||||
|
||||
|
||||
def printlink(ar, purl, name, fk):
|
||||
if not name:
|
||||
url = purl # srch
|
||||
else:
|
||||
name = quotep(name.encode("utf-8", WTF8)).decode("utf-8")
|
||||
if fk:
|
||||
url = "%s%s?k=%s" % (purl, name, fk)
|
||||
else:
|
||||
url = "%s%s" % (purl, name)
|
||||
|
||||
url = "%s/%s" % (ar.burl, url.lstrip("/"))
|
||||
|
||||
with linkmtx:
|
||||
if ar.u:
|
||||
links.append(url)
|
||||
if ar.ud:
|
||||
print(url)
|
||||
if linkfile:
|
||||
zs = "%s\n" % (url,)
|
||||
zb = zs.encode("utf-8", "replace")
|
||||
linkfile.write(zb)
|
||||
|
||||
|
||||
def handshake(ar, file, search):
|
||||
# type: (argparse.Namespace, File, bool) -> tuple[list[str], bool]
|
||||
"""
|
||||
@@ -832,12 +859,17 @@ def handshake(ar, file, search):
|
||||
raise Exception(txt)
|
||||
|
||||
if search:
|
||||
if ar.uon and r["hits"]:
|
||||
printlink(ar, r["hits"][0]["rp"], "", "")
|
||||
return r["hits"], False
|
||||
|
||||
file.url = quotep(r["purl"].encode("utf-8", WTF8)).decode("utf-8")
|
||||
file.name = r["name"]
|
||||
file.wark = r["wark"]
|
||||
|
||||
if ar.uon and not r["hash"]:
|
||||
printlink(ar, file.url, r["name"], r.get("fk"))
|
||||
|
||||
return r["hash"], r["sprs"]
|
||||
|
||||
|
||||
@@ -1033,8 +1065,8 @@ class Ctl(object):
|
||||
handshake(self.ar, file, False)
|
||||
|
||||
def _fancy(self):
|
||||
atexit.register(self.cleanup_vt100)
|
||||
if VT100 and not self.ar.ns:
|
||||
atexit.register(self.cleanup_vt100)
|
||||
ss.scroll_region(3)
|
||||
|
||||
Daemon(self.hasher)
|
||||
@@ -1042,6 +1074,7 @@ class Ctl(object):
|
||||
Daemon(self.handshaker)
|
||||
Daemon(self.uploader)
|
||||
|
||||
last_sp = -1
|
||||
while True:
|
||||
with self.exit_cond:
|
||||
self.exit_cond.wait(0.07)
|
||||
@@ -1080,6 +1113,12 @@ class Ctl(object):
|
||||
else:
|
||||
txt = " "
|
||||
|
||||
if not VT100: # OSC9;4 (taskbar-progress)
|
||||
sp = int(self.up_b * 100 / self.nbytes) or 1
|
||||
if last_sp != sp:
|
||||
last_sp = sp
|
||||
txt += "\033]9;4;1;%d\033\\" % (sp,)
|
||||
|
||||
if not self.up_br:
|
||||
spd = self.hash_b / ((time.time() - self.t0) or 1)
|
||||
eta = (self.nbytes - self.hash_b) / (spd or 1)
|
||||
@@ -1090,11 +1129,15 @@ class Ctl(object):
|
||||
|
||||
spd = humansize(spd)
|
||||
self.eta = str(datetime.timedelta(seconds=int(eta)))
|
||||
if eta > 2591999:
|
||||
self.eta = self.eta.split(",")[0] # truncate HH:MM:SS
|
||||
sleft = humansize(self.nbytes - self.up_b)
|
||||
nleft = self.nfiles - self.up_f
|
||||
tail = "\033[K\033[u" if VT100 and not self.ar.ns else "\r"
|
||||
|
||||
t = "%s eta @ %s/s, %s, %d# left\033[K" % (self.eta, spd, sleft, nleft)
|
||||
t = "%s eta @ %s/s, %s, %d# left" % (self.eta, spd, sleft, nleft)
|
||||
if not self.hash_b:
|
||||
t = " now hashing..."
|
||||
eprint(txt + "\033]0;{0}\033\\\r{0}{1}".format(t, tail))
|
||||
|
||||
if self.ar.wlist:
|
||||
@@ -1115,7 +1158,10 @@ class Ctl(object):
|
||||
handshake(self.ar, file, False)
|
||||
|
||||
def cleanup_vt100(self):
|
||||
ss.scroll_region(None)
|
||||
if VT100:
|
||||
ss.scroll_region(None)
|
||||
else:
|
||||
eprint("\033]9;4;0\033\\")
|
||||
eprint("\033[J\033]0;\033\\")
|
||||
|
||||
def cb_hasher(self, file, ofs):
|
||||
@@ -1130,7 +1176,9 @@ class Ctl(object):
|
||||
isdir = stat.S_ISDIR(inf.st_mode)
|
||||
if self.ar.z or self.ar.drd:
|
||||
rd = rel if isdir else os.path.dirname(rel)
|
||||
srd = rd.decode("utf-8", "replace").replace("\\", "/")
|
||||
srd = rd.decode("utf-8", "replace").replace("\\", "/").rstrip("/")
|
||||
if srd:
|
||||
srd += "/"
|
||||
if prd != rd:
|
||||
prd = rd
|
||||
ls = {}
|
||||
@@ -1165,11 +1213,11 @@ class Ctl(object):
|
||||
bnames = [x for x in ls if x not in lnodes and x != b".hist"]
|
||||
vpath = self.ar.url.split("://")[-1].split("/", 1)[-1]
|
||||
names = [x.decode("utf-8", WTF8) for x in bnames]
|
||||
locs = [vpath + srd + "/" + x for x in names]
|
||||
locs = [vpath + srd + x for x in names]
|
||||
while locs:
|
||||
req = locs
|
||||
while req:
|
||||
print("DELETING ~%s/#%s" % (srd, len(req)))
|
||||
print("DELETING ~%s#%s" % (srd, len(req)))
|
||||
body = json.dumps(req).encode("utf-8")
|
||||
sc, txt = web.req(
|
||||
"POST", self.ar.url + "?delete", {}, body, MJ
|
||||
@@ -1233,7 +1281,7 @@ class Ctl(object):
|
||||
for n, zsii in enumerate(file.cids)
|
||||
]
|
||||
print("chs: %s\n%s" % (vp, "\n".join(zsl)))
|
||||
zsl = [self.ar.wsalt, str(file.size)] + [x[0] for x in file.kchunks]
|
||||
zsl = [self.ar.wsalt, str(file.size)] + [x[0] for x in file.cids]
|
||||
zb = hashlib.sha512("\n".join(zsl).encode("utf-8")).digest()[:33]
|
||||
wark = ub64enc(zb).decode("utf-8")
|
||||
if self.ar.jw:
|
||||
@@ -1456,7 +1504,7 @@ class APF(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFor
|
||||
|
||||
|
||||
def main():
|
||||
global web
|
||||
global web, linkfile
|
||||
|
||||
time.strptime("19970815", "%Y%m%d") # python#7980
|
||||
"".encode("idna") # python#29288
|
||||
@@ -1493,6 +1541,11 @@ source file/folder selection uses rsync syntax, meaning that:
|
||||
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("print links")
|
||||
ap.add_argument("-u", action="store_true", help="print list of download-links after all uploads finished")
|
||||
ap.add_argument("-ud", action="store_true", help="print download-link after each upload finishes")
|
||||
ap.add_argument("-uf", type=unicode, metavar="PATH", help="print list of download-links to file")
|
||||
|
||||
ap = app.add_argument_group("compatibility")
|
||||
ap.add_argument("--cls", action="store_true", help="clear screen before start")
|
||||
ap.add_argument("--rh", type=int, metavar="TRIES", default=0, help="resolve server hostname before upload (good for buggy networks, but TLS certs will break)")
|
||||
@@ -1534,6 +1587,38 @@ source file/folder selection uses rsync syntax, meaning that:
|
||||
except:
|
||||
pass
|
||||
|
||||
# msys2 doesn't uncygpath absolute paths with whitespace
|
||||
if not VT100:
|
||||
zsl = []
|
||||
for fn in ar.files:
|
||||
if re.search("^/[a-z]/", fn):
|
||||
fn = r"%s:\%s" % (fn[1:2], fn[3:])
|
||||
zsl.append(fn.replace("/", "\\"))
|
||||
ar.files = zsl
|
||||
|
||||
fok = []
|
||||
fng = []
|
||||
for fn in ar.files:
|
||||
if os.path.exists(fn):
|
||||
fok.append(fn)
|
||||
elif VT100:
|
||||
fng.append(fn)
|
||||
else:
|
||||
# windows leaves glob-expansion to the invoked process... okayyy let's get to work
|
||||
from glob import glob
|
||||
|
||||
fns = glob(fn)
|
||||
if fns:
|
||||
fok.extend(fns)
|
||||
else:
|
||||
fng.append(fn)
|
||||
|
||||
if fng:
|
||||
t = "some files/folders were not found:\n %s"
|
||||
raise Exception(t % ("\n ".join(fng),))
|
||||
|
||||
ar.files = fok
|
||||
|
||||
if ar.drd:
|
||||
ar.dr = True
|
||||
|
||||
@@ -1546,6 +1631,10 @@ source file/folder selection uses rsync syntax, meaning that:
|
||||
ar.x = "|".join(ar.x or [])
|
||||
|
||||
setattr(ar, "wlist", ar.url == "-")
|
||||
setattr(ar, "uon", ar.u or ar.ud or ar.uf)
|
||||
|
||||
if ar.uf:
|
||||
linkfile = open(ar.uf, "wb")
|
||||
|
||||
for k in "dl dr drd wlist".split():
|
||||
errs = []
|
||||
@@ -1608,6 +1697,12 @@ source file/folder selection uses rsync syntax, meaning that:
|
||||
ar.z = True
|
||||
ctl = Ctl(ar, ctl.stats)
|
||||
|
||||
if links:
|
||||
print()
|
||||
print("\n".join(links))
|
||||
if linkfile:
|
||||
linkfile.close()
|
||||
|
||||
if ctl.errs:
|
||||
print("WARNING: %d errors" % (ctl.errs))
|
||||
|
||||
|
||||
76
bin/zmq-recv.py
Executable file
76
bin/zmq-recv.py
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import zmq
|
||||
|
||||
"""
|
||||
zmq-recv.py: demo zmq receiver
|
||||
2025-01-22, v1.0, ed <irc.rizon.net>, MIT-Licensed
|
||||
https://github.com/9001/copyparty/blob/hovudstraum/bin/zmq-recv.py
|
||||
|
||||
basic zmq-server to receive events from copyparty; try one of
|
||||
the below and then "send a message to serverlog" in the web-ui:
|
||||
|
||||
1) dumb fire-and-forget to any and all listeners;
|
||||
run this script with "sub" and run copyparty with this:
|
||||
--xm zmq:pub:tcp://*:5556
|
||||
|
||||
2) one lucky listener gets the message, blocks if no listeners:
|
||||
run this script with "pull" and run copyparty with this:
|
||||
--xm t3,zmq:push:tcp://*:5557
|
||||
|
||||
3) blocking syn/ack mode, client must ack each message;
|
||||
run this script with "rep" and run copyparty with this:
|
||||
--xm t3,zmq:req:tcp://localhost:5555
|
||||
|
||||
note: to conditionally block uploads based on message contents,
|
||||
use rep_server to answer with "return 1" and run copyparty with
|
||||
--xau t3,c,zmq:req:tcp://localhost:5555
|
||||
"""
|
||||
|
||||
|
||||
ctx = zmq.Context()
|
||||
|
||||
|
||||
def sub_server():
|
||||
# PUB/SUB allows any number of servers/clients, and
|
||||
# messages are fire-and-forget
|
||||
sck = ctx.socket(zmq.SUB)
|
||||
sck.connect("tcp://localhost:5556")
|
||||
sck.setsockopt_string(zmq.SUBSCRIBE, "")
|
||||
while True:
|
||||
print("copyparty says %r" % (sck.recv_string(),))
|
||||
|
||||
|
||||
def pull_server():
|
||||
# PUSH/PULL allows any number of servers/clients, and
|
||||
# each message is sent to a exactly one PULL client
|
||||
sck = ctx.socket(zmq.PULL)
|
||||
sck.connect("tcp://localhost:5557")
|
||||
while True:
|
||||
print("copyparty says %r" % (sck.recv_string(),))
|
||||
|
||||
|
||||
def rep_server():
|
||||
# REP/REQ is a server/client pair where each message must be
|
||||
# acked by the other before another message can be sent, so
|
||||
# copyparty will do a blocking-wait for the ack
|
||||
sck = ctx.socket(zmq.REP)
|
||||
sck.bind("tcp://*:5555")
|
||||
while True:
|
||||
print("copyparty says %r" % (sck.recv_string(),))
|
||||
reply = b"thx"
|
||||
# reply = b"return 1" # non-zero to block an upload
|
||||
sck.send(reply)
|
||||
|
||||
|
||||
mode = sys.argv[1].lower() if len(sys.argv) > 1 else ""
|
||||
|
||||
if mode == "sub":
|
||||
sub_server()
|
||||
elif mode == "pull":
|
||||
pull_server()
|
||||
elif mode == "rep":
|
||||
rep_server()
|
||||
else:
|
||||
print("specify mode as first argument: SUB | PULL | REP")
|
||||
@@ -12,14 +12,19 @@
|
||||
* assumes the webserver and copyparty is running on the same server/IP
|
||||
* modify `10.13.1.1` as necessary if you wish to support browsers without javascript
|
||||
|
||||
### [`sharex.sxcu`](sharex.sxcu)
|
||||
* sharex config file to upload screenshots and grab the URL
|
||||
### [`sharex.sxcu`](sharex.sxcu) - Windows screenshot uploader
|
||||
* [sharex](https://getsharex.com/) config file to upload screenshots and grab the URL
|
||||
* `RequestURL`: full URL to the target folder
|
||||
* `pw`: password (remove the `pw` line if anon-write)
|
||||
* the `act:bput` thing is optional since copyparty v1.9.29
|
||||
* using an older sharex version, maybe sharex v12.1.1 for example? dw fam i got your back 👉😎👉 [`sharex12.sxcu`](sharex12.sxcu)
|
||||
|
||||
### [`flameshot.sh`](flameshot.sh)
|
||||
### [`ishare.iscu`](ishare.iscu) - MacOS screenshot uploader
|
||||
* [ishare](https://isharemac.app/) config file to upload screenshots and grab the URL
|
||||
* `RequestURL`: full URL to the target folder
|
||||
* `pw`: password (remove the `pw` line if anon-write)
|
||||
|
||||
### [`flameshot.sh`](flameshot.sh) - Linux screenshot uploader
|
||||
* takes a screenshot with [flameshot](https://flameshot.org/) on Linux, uploads it, and writes the URL to clipboard
|
||||
|
||||
### [`send-to-cpp.contextlet.json`](send-to-cpp.contextlet.json)
|
||||
@@ -53,5 +58,10 @@ init-scripts to start copyparty as a service
|
||||
* [`openrc/copyparty`](openrc/copyparty)
|
||||
|
||||
# Reverse-proxy
|
||||
copyparty has basic support for running behind another webserver
|
||||
* [`nginx/copyparty.conf`](nginx/copyparty.conf)
|
||||
copyparty supports running behind another webserver
|
||||
* [`apache/copyparty.conf`](apache/copyparty.conf)
|
||||
* [`haproxy/copyparty.conf`](haproxy/copyparty.conf)
|
||||
* [`lighttpd/subdomain.conf`](lighttpd/subdomain.conf)
|
||||
* [`lighttpd/subpath.conf`](lighttpd/subpath.conf)
|
||||
* [`nginx/copyparty.conf`](nginx/copyparty.conf) -- recommended
|
||||
* [`traefik/copyparty.yaml`](traefik/copyparty.yaml)
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
# when running copyparty behind a reverse proxy,
|
||||
# the following arguments are recommended:
|
||||
# if you would like to use unix-sockets (recommended),
|
||||
# you must run copyparty with one of the following:
|
||||
#
|
||||
# -i 127.0.0.1 only accept connections from nginx
|
||||
# -i unix:777:/dev/shm/party.sock
|
||||
# -i unix:777:/dev/shm/party.sock,127.0.0.1
|
||||
#
|
||||
# if you are doing location-based proxying (such as `/stuff` below)
|
||||
# you must run copyparty with --rp-loc=stuff
|
||||
#
|
||||
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
|
||||
|
||||
|
||||
LoadModule proxy_module modules/mod_proxy.so
|
||||
ProxyPass "/stuff" "http://127.0.0.1:3923/stuff"
|
||||
# do not specify ProxyPassReverse
|
||||
|
||||
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
|
||||
# NOTE: do not specify ProxyPassReverse
|
||||
|
||||
|
||||
##
|
||||
## then, enable one of the below:
|
||||
|
||||
# use subdomain proxying to unix-socket (best)
|
||||
ProxyPass "/" "unix:///dev/shm/party.sock|http://whatever/"
|
||||
|
||||
# use subdomain proxying to 127.0.0.1 (slower)
|
||||
#ProxyPass "/" "http://127.0.0.1:3923/"
|
||||
|
||||
# use subpath proxying to 127.0.0.1 (slow and maybe buggy)
|
||||
#ProxyPass "/stuff" "http://127.0.0.1:3923/stuff"
|
||||
|
||||
24
contrib/haproxy/copyparty.conf
Normal file
24
contrib/haproxy/copyparty.conf
Normal file
@@ -0,0 +1,24 @@
|
||||
# this config is essentially two separate examples;
|
||||
#
|
||||
# foo1 connects to copyparty using tcp, and
|
||||
# foo2 uses unix-sockets for 27% higher performance
|
||||
#
|
||||
# to use foo2 you must run copyparty with one of the following:
|
||||
#
|
||||
# -i unix:777:/dev/shm/party.sock
|
||||
# -i unix:777:/dev/shm/party.sock,127.0.0.1
|
||||
|
||||
defaults
|
||||
mode http
|
||||
option forwardfor
|
||||
timeout connect 1s
|
||||
timeout client 610s
|
||||
timeout server 610s
|
||||
|
||||
listen foo1
|
||||
bind *:8081
|
||||
server srv1 127.0.0.1:3923 maxconn 512
|
||||
|
||||
listen foo2
|
||||
bind *:8082
|
||||
server srv1 /dev/shm/party.sock maxconn 512
|
||||
10
contrib/ishare.iscu
Normal file
10
contrib/ishare.iscu
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Name": "copyparty",
|
||||
"RequestURL": "http://127.0.0.1:3923/screenshots/",
|
||||
"Headers": {
|
||||
"pw": "PUT_YOUR_PASSWORD_HERE_MY_DUDE",
|
||||
"accept": "json"
|
||||
},
|
||||
"FileFormName": "f",
|
||||
"ResponseURL": "{{fileurl}}"
|
||||
}
|
||||
24
contrib/lighttpd/subdomain.conf
Normal file
24
contrib/lighttpd/subdomain.conf
Normal file
@@ -0,0 +1,24 @@
|
||||
# example usage for benchmarking:
|
||||
#
|
||||
# taskset -c 1 lighttpd -Df ~/dev/copyparty/contrib/lighttpd/subdomain.conf
|
||||
#
|
||||
# lighttpd can connect to copyparty using either tcp (127.0.0.1)
|
||||
# or a unix-socket, but unix-sockets are 37% faster because
|
||||
# lighttpd doesn't reuse tcp connections, so we're doing unix-sockets
|
||||
#
|
||||
# this means we must run copyparty with one of the following:
|
||||
#
|
||||
# -i unix:777:/dev/shm/party.sock
|
||||
# -i unix:777:/dev/shm/party.sock,127.0.0.1
|
||||
#
|
||||
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
|
||||
|
||||
server.port = 80
|
||||
server.document-root = "/var/empty"
|
||||
server.upload-dirs = ( "/dev/shm", "/tmp" )
|
||||
server.modules = ( "mod_proxy" )
|
||||
proxy.forwarded = ( "for" => 1, "proto" => 1 )
|
||||
proxy.server = ( "" => ( ( "host" => "/dev/shm/party.sock" ) ) )
|
||||
|
||||
# if you really need to use tcp instead of unix-sockets, do this instead:
|
||||
#proxy.server = ( "" => ( ( "host" => "127.0.0.1", "port" => "3923" ) ) )
|
||||
31
contrib/lighttpd/subpath.conf
Normal file
31
contrib/lighttpd/subpath.conf
Normal file
@@ -0,0 +1,31 @@
|
||||
# example usage for benchmarking:
|
||||
#
|
||||
# taskset -c 1 lighttpd -Df ~/dev/copyparty/contrib/lighttpd/subpath.conf
|
||||
#
|
||||
# lighttpd can connect to copyparty using either tcp (127.0.0.1)
|
||||
# or a unix-socket, but unix-sockets are 37% faster because
|
||||
# lighttpd doesn't reuse tcp connections, so we're doing unix-sockets
|
||||
#
|
||||
# this means we must run copyparty with one of the following:
|
||||
#
|
||||
# -i unix:777:/dev/shm/party.sock
|
||||
# -i unix:777:/dev/shm/party.sock,127.0.0.1
|
||||
#
|
||||
# also since this example proxies a subpath instead of the
|
||||
# recommended subdomain-proxying, we must also specify this:
|
||||
#
|
||||
# --rp-loc files
|
||||
#
|
||||
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
|
||||
|
||||
server.port = 80
|
||||
server.document-root = "/var/empty"
|
||||
server.upload-dirs = ( "/dev/shm", "/tmp" )
|
||||
server.modules = ( "mod_proxy" )
|
||||
$HTTP["url"] =~ "^/files" {
|
||||
proxy.forwarded = ( "for" => 1, "proto" => 1 )
|
||||
proxy.server = ( "" => ( ( "host" => "/dev/shm/party.sock" ) ) )
|
||||
|
||||
# if you really need to use tcp instead of unix-sockets, do this instead:
|
||||
#proxy.server = ( "" => ( ( "host" => "127.0.0.1", "port" => "3923" ) ) )
|
||||
}
|
||||
@@ -36,9 +36,9 @@ upstream cpp_uds {
|
||||
# but there must be at least one unix-group which both
|
||||
# nginx and copyparty is a member of; if that group is
|
||||
# "www" then run copyparty with the following args:
|
||||
# -i unix:770:www:/tmp/party.sock
|
||||
# -i unix:770:www:/dev/shm/party.sock
|
||||
|
||||
server unix:/tmp/party.sock fail_timeout=1s;
|
||||
server unix:/dev/shm/party.sock fail_timeout=1s;
|
||||
keepalive 1;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@ server {
|
||||
client_max_body_size 0;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
# improve download speed from 600 to 1500 MiB/s
|
||||
proxy_buffers 32 8k;
|
||||
proxy_buffer_size 16k;
|
||||
proxy_busy_buffers_size 24k;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: icxes <dev.null@need.moe>
|
||||
pkgname=copyparty
|
||||
pkgver="1.15.10"
|
||||
pkgver="1.16.10"
|
||||
pkgrel=1
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||
arch=("any")
|
||||
@@ -16,12 +16,13 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
|
||||
"libkeyfinder-git: detection of musical keys"
|
||||
"qm-vamp-plugins: BPM detection"
|
||||
"python-pyopenssl: ftps functionality"
|
||||
"python-argon2_cffi: hashed passwords in config"
|
||||
"python-pyzmq: send zeromq messages from event-hooks"
|
||||
"python-argon2-cffi: hashed passwords in config"
|
||||
"python-impacket-git: smb support (bad idea)"
|
||||
)
|
||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||
backup=("etc/${pkgname}.d/init" )
|
||||
sha256sums=("070d5bdebe57c247427ceea6b9029f93097e4b8996cabfc59d0ec248d063b993")
|
||||
sha256sums=("a33f5df985f5c6e8da717e913eb070eb488263759ee1a2668b3b26837e29a944")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{ lib, stdenv, makeWrapper, fetchurl, utillinux, python, jinja2, impacket, pyftpdlib, pyopenssl, argon2-cffi, pillow, pyvips, ffmpeg, mutagen,
|
||||
{ lib, stdenv, makeWrapper, fetchurl, utillinux, python, jinja2, impacket, pyftpdlib, pyopenssl, argon2-cffi, pillow, pyvips, pyzmq, ffmpeg, mutagen,
|
||||
|
||||
# use argon2id-hashed passwords in config files (sha2 is always available)
|
||||
withHashedPasswords ? true,
|
||||
@@ -21,6 +21,9 @@ withMediaProcessing ? true,
|
||||
# if MediaProcessing is not enabled, you probably want this instead (less accurate, but much safer and faster)
|
||||
withBasicAudioMetadata ? false,
|
||||
|
||||
# send ZeroMQ messages from event-hooks
|
||||
withZeroMQ ? true,
|
||||
|
||||
# enable FTPS support in the FTP server
|
||||
withFTPS ? false,
|
||||
|
||||
@@ -43,6 +46,7 @@ let
|
||||
++ lib.optional withMediaProcessing ffmpeg
|
||||
++ lib.optional withBasicAudioMetadata mutagen
|
||||
++ lib.optional withHashedPasswords argon2-cffi
|
||||
++ lib.optional withZeroMQ pyzmq
|
||||
);
|
||||
in stdenv.mkDerivation {
|
||||
pname = "copyparty";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.15.10/copyparty-sfx.py",
|
||||
"version": "1.15.10",
|
||||
"hash": "sha256-9InxXpCfgnsvcNdRwWhQ74TpI24Osdr0lN0IwIULL3I="
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.16.10/copyparty-sfx.py",
|
||||
"version": "1.16.10",
|
||||
"hash": "sha256-4CK/491U/fdor+McO94nYsL39g73WQ7i8loUYVrbiZA="
|
||||
}
|
||||
12
contrib/traefik/copyparty.yaml
Normal file
12
contrib/traefik/copyparty.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
# ./traefik --experimental.fastproxy=true --entrypoints.web.address=:8080 --providers.file.filename=copyparty.yaml
|
||||
|
||||
http:
|
||||
services:
|
||||
service-cpp:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://127.0.0.1:3923/"
|
||||
routers:
|
||||
my-router:
|
||||
rule: "PathPrefix(`/`)"
|
||||
service: service-cpp
|
||||
@@ -91,6 +91,9 @@ web/mde.html
|
||||
web/mde.js
|
||||
web/msg.css
|
||||
web/msg.html
|
||||
web/rups.css
|
||||
web/rups.html
|
||||
web/rups.js
|
||||
web/shares.css
|
||||
web/shares.html
|
||||
web/shares.js
|
||||
|
||||
@@ -54,6 +54,8 @@ from .util import (
|
||||
RAM_TOTAL,
|
||||
SQLITE_VER,
|
||||
UNPLICATIONS,
|
||||
URL_BUG,
|
||||
URL_PRJ,
|
||||
Daemon,
|
||||
align_tab,
|
||||
ansi_re,
|
||||
@@ -332,17 +334,16 @@ def ensure_webdeps() -> None:
|
||||
if has_resource(E, "web/deps/mini-fa.woff"):
|
||||
return
|
||||
|
||||
warn(
|
||||
"""could not find webdeps;
|
||||
t = """could not find webdeps;
|
||||
if you are running the sfx, or exe, or pypi package, or docker image,
|
||||
then this is a bug! Please let me know so I can fix it, thanks :-)
|
||||
https://github.com/9001/copyparty/issues/new?labels=bug&template=bug_report.md
|
||||
%s
|
||||
|
||||
however, if you are a dev, or running copyparty from source, and you want
|
||||
full client functionality, you will need to build or obtain the webdeps:
|
||||
https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#building
|
||||
%s/blob/hovudstraum/docs/devnotes.md#building
|
||||
"""
|
||||
)
|
||||
warn(t % (URL_BUG, URL_PRJ))
|
||||
|
||||
|
||||
def configure_ssl_ver(al: argparse.Namespace) -> None:
|
||||
@@ -739,6 +740,10 @@ def get_sects():
|
||||
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[36m--xau zmq:pub:tcp://*:5556\033[35m announces uploads on zeromq;
|
||||
\033[36m--xau t3,zmq:push:tcp://*:5557\033[35m also works, and you can
|
||||
\033[36m--xau t3,j,zmq:req:tcp://localhost:5555\033[35m too for example
|
||||
\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
|
||||
@@ -770,11 +775,22 @@ def get_sects():
|
||||
values for --urlform:
|
||||
\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)\033[0m
|
||||
\033[36mprint \033[35m prints the data to log and returns an error
|
||||
\033[36mprint,xm \033[35m prints the data to log and returns --xm output
|
||||
\033[36mprint,get\033[35m prints the data to log and returns GET\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
|
||||
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 \033[36mprint,get\033[0m or the default \033[36mprint,xm\033[0m
|
||||
|
||||
if an \033[35m--xm\033[0m hook returns text, then
|
||||
the response code will be HTTP 202;
|
||||
http/get responses will be HTTP 200
|
||||
|
||||
if there are multiple \033[35m--xm\033[0m hooks defined, then
|
||||
the first hook that produced output is returned
|
||||
|
||||
if there are no \033[35m--xm\033[0m hooks defined, then the default
|
||||
\033[36mprint,xm\033[0m behaves like \033[36mprint,get\033[0m (returning html)
|
||||
"""
|
||||
),
|
||||
],
|
||||
@@ -878,8 +894,9 @@ def get_sects():
|
||||
use argon2id with timecost 3, 256 MiB, 4 threads, version 19 (0x13/v1.3)
|
||||
|
||||
\033[36m--ah-alg scrypt\033[0m # which is the same as:
|
||||
\033[36m--ah-alg scrypt,13,2,8,4\033[0m
|
||||
use scrypt with cost 2**13, 2 iterations, blocksize 8, 4 threads
|
||||
\033[36m--ah-alg scrypt,13,2,8,4,32\033[0m
|
||||
use scrypt with cost 2**13, 2 iterations, blocksize 8, 4 threads,
|
||||
and allow using up to 32 MiB RAM (ram=cost*blksz roughly)
|
||||
|
||||
\033[36m--ah-alg sha2\033[0m # which is the same as:
|
||||
\033[36m--ah-alg sha2,424242\033[0m
|
||||
@@ -954,7 +971,7 @@ def add_general(ap, nc, srvname):
|
||||
ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="add volume, \033[33mSRC\033[0m:\033[33mDST\033[0m:\033[33mFLAG\033[0m; examples [\033[32m.::r\033[0m], [\033[32m/mnt/nas/music:/music:r:aed\033[0m], see --help-accounts")
|
||||
ap2.add_argument("--grp", metavar="G:N,N", type=u, action="append", help="add group, \033[33mNAME\033[0m:\033[33mUSER1\033[0m,\033[33mUSER2\033[0m,\033[33m...\033[0m; example [\033[32madmins:ed,foo,bar\033[0m]")
|
||||
ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files (volflag=dots)")
|
||||
ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,get", help="how to handle url-form POSTs; see \033[33m--help-urlform\033[0m")
|
||||
ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,xm", help="how to handle url-form POSTs; see \033[33m--help-urlform\033[0m")
|
||||
ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="server terminal title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]")
|
||||
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]")
|
||||
@@ -1082,7 +1099,7 @@ def add_cert(ap, cert_path):
|
||||
def add_auth(ap):
|
||||
ses_db = os.path.join(E.cfg, "sessions.db")
|
||||
ap2 = ap.add_argument_group('IdP / identity provider / user authentication options')
|
||||
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, default="", help="bypass the copyparty authentication checks and assume the request-header \033[33mHN\033[0m contains the username of the requesting user (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
|
||||
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, default="", help="bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
|
||||
ap2.add_argument("--idp-h-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")
|
||||
@@ -1123,6 +1140,8 @@ def add_zc_mdns(ap):
|
||||
ap2.add_argument("--zm6", action="store_true", help="IPv6 only")
|
||||
ap2.add_argument("--zmv", action="store_true", help="verbose mdns")
|
||||
ap2.add_argument("--zmvv", action="store_true", help="verboser mdns")
|
||||
ap2.add_argument("--zm-no-pe", action="store_true", help="mute parser errors (invalid incoming MDNS packets)")
|
||||
ap2.add_argument("--zm-nwa-1", action="store_true", help="disable workaround for avahi-bug #379 (corruption in Avahi's mDNS reflection feature)")
|
||||
ap2.add_argument("--zms", metavar="dhf", type=u, default="", help="list of services to announce -- d=webdav h=http f=ftp s=smb -- lowercase=plaintext uppercase=TLS -- default: all enabled services except http/https (\033[32mDdfs\033[0m if \033[33m--ftp\033[0m and \033[33m--smb\033[0m is set, \033[32mDd\033[0m otherwise)")
|
||||
ap2.add_argument("--zm-ld", metavar="PATH", type=u, default="", help="link a specific folder for webdav shares")
|
||||
ap2.add_argument("--zm-lh", metavar="PATH", type=u, default="", help="link a specific folder for http shares")
|
||||
@@ -1247,7 +1266,6 @@ 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-up-list", action="store_true", help="don't show list of incoming files in controlpanel")
|
||||
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")
|
||||
|
||||
@@ -1315,7 +1333,7 @@ def add_logging(ap):
|
||||
ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling")
|
||||
ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
|
||||
ap2.add_argument("--ohead", metavar="HEADER", type=u, action='append', help="print response \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
|
||||
ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|\?th=[wj]$|/\.(_|ql_|DS_Store$|localized$)", help="dont log URLs matching regex \033[33mRE\033[0m")
|
||||
ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|[?&]th=[wjp]|/\.(_|ql_|DS_Store$|localized$)", help="dont log URLs matching regex \033[33mRE\033[0m")
|
||||
|
||||
|
||||
def add_admin(ap):
|
||||
@@ -1323,7 +1341,11 @@ def add_admin(ap):
|
||||
ap2.add_argument("--no-reload", action="store_true", help="disable ?reload=cfg (reload users/volumes/volflags from config file)")
|
||||
ap2.add_argument("--no-rescan", action="store_true", help="disable ?scan (volume reindexing)")
|
||||
ap2.add_argument("--no-stack", action="store_true", help="disable ?stack (list all stacks)")
|
||||
ap2.add_argument("--no-ups-page", action="store_true", help="disable ?ru (list of recent uploads)")
|
||||
ap2.add_argument("--no-up-list", action="store_true", help="don't show list of incoming files in controlpanel")
|
||||
ap2.add_argument("--dl-list", metavar="LVL", type=int, default=2, help="who can see active downloads in the controlpanel? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=everyone")
|
||||
ap2.add_argument("--ups-who", metavar="LVL", type=int, default=2, help="who can see recent uploads on the ?ru page? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=everyone (volflag=ups_who)")
|
||||
ap2.add_argument("--ups-when", action="store_true", help="let everyone see upload timestamps on the ?ru page, not just admins")
|
||||
|
||||
|
||||
def add_thumbnail(ap):
|
||||
@@ -1351,18 +1373,20 @@ def add_thumbnail(ap):
|
||||
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
|
||||
# https://github.com/libvips/libvips
|
||||
# ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:'
|
||||
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,dcx,dds,dib,emf,eps,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow")
|
||||
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow")
|
||||
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips")
|
||||
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,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-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,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")
|
||||
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz", help="audio/image formats to decompress before passing to ffmpeg")
|
||||
|
||||
|
||||
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-caf", action="store_true", help="disable transcoding to caf-opus (affects iOS v12~v17), will use mp3 instead")
|
||||
ap2.add_argument("--no-owa", action="store_true", help="disable transcoding to webm-opus (iOS v18 and later), will use mp3 instead")
|
||||
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")
|
||||
@@ -1400,6 +1424,7 @@ def add_db_general(ap, hcores):
|
||||
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("--srch-excl", metavar="PTN", type=u, default="", help="regex: exclude files from search results if the file-URL matches \033[33mPTN\033[0m (case-sensitive). Example: [\033[32mpassword|logs/[0-9]\033[0m] any URL containing 'password' or 'logs/DIGIT' (volflag=srch_excl)")
|
||||
ap2.add_argument("--dotsrch", action="store_true", help="show dotfiles in search results (volflags: dotsrch | nodotsrch)")
|
||||
|
||||
|
||||
@@ -1456,6 +1481,8 @@ def add_ui(ap, retry):
|
||||
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("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)")
|
||||
ap2.add_argument("--hsortn", metavar="N", type=int, default=2, help="number of sorting rules to include in media URLs by default (volflag=hsortn)")
|
||||
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])")
|
||||
@@ -1468,12 +1495,14 @@ def add_ui(ap, retry):
|
||||
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)")
|
||||
ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
|
||||
ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
|
||||
ap2.add_argument("--pb-url", metavar="URL", type=u, default="https://github.com/9001/copyparty", help="powered-by link; disable with \033[33m-np\033[0m")
|
||||
ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-np\033[0m")
|
||||
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)")
|
||||
ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
|
||||
ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
|
||||
ap2.add_argument("--md-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to ALLOW for README.md docs (volflag=md_sbf); see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox")
|
||||
ap2.add_argument("--lg-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to ALLOW for prologue/epilogue docs (volflag=lg_sbf)")
|
||||
ap2.add_argument("--md-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to allow in the iframe 'sandbox' attribute for README.md docs (volflag=md_sbf); see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox")
|
||||
ap2.add_argument("--lg-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to allow in the iframe 'sandbox' attribute for prologue/epilogue docs (volflag=lg_sbf)")
|
||||
ap2.add_argument("--md-sba", metavar="TXT", type=u, default="", help="the value of the iframe 'allow' attribute for README.md docs, for example [\033[32mfullscreen\033[0m] (volflag=md_sba)")
|
||||
ap2.add_argument("--lg-sba", metavar="TXT", type=u, default="", help="the value of the iframe 'allow' attribute for prologue/epilogue docs (volflag=lg_sba); see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy#iframes")
|
||||
ap2.add_argument("--no-sb-md", action="store_true", help="don't sandbox README/PREADME.md documents (volflags: no_sb_md | sb_md)")
|
||||
ap2.add_argument("--no-sb-lg", action="store_true", help="don't sandbox prologue/epilogue docs (volflags: no_sb_lg | sb_lg); enables non-js support")
|
||||
|
||||
@@ -1499,6 +1528,7 @@ def add_debug(ap):
|
||||
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)")
|
||||
ap2.add_argument("--bf-dir", metavar="PATH", type=u, default="bf", help="bak-flips: store corrupted chunks at \033[33mPATH\033[0m; default: folder named 'bf' wherever copyparty was started")
|
||||
ap2.add_argument("--bf-log", metavar="PATH", type=u, default="", help="bak-flips: log corruption info to a textfile at \033[33mPATH\033[0m")
|
||||
ap2.add_argument("--no-cfg-cmt-warn", action="store_true", help=argparse.SUPPRESS)
|
||||
|
||||
|
||||
# fmt: on
|
||||
@@ -1731,7 +1761,7 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
except:
|
||||
lprint("\nfailed to disable quick-edit-mode:\n" + min_ex() + "\n")
|
||||
|
||||
if al.ansi:
|
||||
if not al.ansi:
|
||||
al.wintitle = ""
|
||||
|
||||
# propagate implications
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (1, 16, 0)
|
||||
VERSION = (1, 16, 11)
|
||||
CODENAME = "COPYparty"
|
||||
BUILD_DT = (2024, 11, 10)
|
||||
BUILD_DT = (2025, 1, 27)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
@@ -212,7 +212,7 @@ class Lim(object):
|
||||
|
||||
df, du, err = get_df(abspath, True)
|
||||
if err:
|
||||
t = "failed to read disk space usage for [%s]: %s"
|
||||
t = "failed to read disk space usage for %r: %s"
|
||||
self.log(t % (abspath, err), 3)
|
||||
self.dfv = 0xAAAAAAAAA # 42.6 GiB
|
||||
else:
|
||||
@@ -367,18 +367,21 @@ class VFS(object):
|
||||
self.ahtml: dict[str, list[str]] = {}
|
||||
self.aadmin: dict[str, list[str]] = {}
|
||||
self.adot: dict[str, list[str]] = {}
|
||||
self.all_vols: dict[str, VFS] = {}
|
||||
self.js_ls = {}
|
||||
self.js_htm = ""
|
||||
|
||||
if realpath:
|
||||
rp = realpath + ("" if realpath.endswith(os.sep) else os.sep)
|
||||
vp = vpath + ("/" if vpath else "")
|
||||
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
|
||||
self.all_vols = {vpath: self} # flattened recursive
|
||||
self.all_nodes = {vpath: self} # also jumpvols
|
||||
self.all_aps = [(rp, self)]
|
||||
self.all_vps = [(vp, self)]
|
||||
else:
|
||||
self.histpath = ""
|
||||
self.all_vols = {}
|
||||
self.all_nodes = {}
|
||||
self.all_aps = []
|
||||
self.all_vps = []
|
||||
|
||||
@@ -396,9 +399,11 @@ class VFS(object):
|
||||
def get_all_vols(
|
||||
self,
|
||||
vols: dict[str, "VFS"],
|
||||
nodes: dict[str, "VFS"],
|
||||
aps: list[tuple[str, "VFS"]],
|
||||
vps: list[tuple[str, "VFS"]],
|
||||
) -> None:
|
||||
nodes[self.vpath] = self
|
||||
if self.realpath:
|
||||
vols[self.vpath] = self
|
||||
rp = self.realpath
|
||||
@@ -408,7 +413,7 @@ class VFS(object):
|
||||
vps.append((vp, self))
|
||||
|
||||
for v in self.nodes.values():
|
||||
v.get_all_vols(vols, aps, vps)
|
||||
v.get_all_vols(vols, nodes, aps, vps)
|
||||
|
||||
def add(self, src: str, dst: str) -> "VFS":
|
||||
"""get existing, or add new path to the vfs"""
|
||||
@@ -521,7 +526,7 @@ class VFS(object):
|
||||
"""returns [vfsnode,fs_remainder] if user has the requested permissions"""
|
||||
if relchk(vpath):
|
||||
if self.log:
|
||||
self.log("vfs", "invalid relpath [{}]".format(vpath))
|
||||
self.log("vfs", "invalid relpath %r @%s" % (vpath, uname))
|
||||
raise Pebkac(422)
|
||||
|
||||
cvpath = undot(vpath)
|
||||
@@ -538,11 +543,11 @@ class VFS(object):
|
||||
if req and uname not in d and uname != LEELOO_DALLAS:
|
||||
if vpath != cvpath and vpath != "." and self.log:
|
||||
ap = vn.canonical(rem)
|
||||
t = "{} has no {} in [{}] => [{}] => [{}]"
|
||||
self.log("vfs", t.format(uname, msg, vpath, cvpath, ap), 6)
|
||||
t = "%s has no %s in %r => %r => %r"
|
||||
self.log("vfs", t % (uname, msg, vpath, cvpath, ap), 6)
|
||||
|
||||
t = 'you don\'t have %s-access in "/%s" or below "/%s"'
|
||||
raise Pebkac(err, t % (msg, cvpath, vn.vpath))
|
||||
t = "you don't have %s-access in %r or below %r"
|
||||
raise Pebkac(err, t % (msg, "/" + cvpath, "/" + vn.vpath))
|
||||
|
||||
return vn, rem
|
||||
|
||||
@@ -653,7 +658,7 @@ class VFS(object):
|
||||
seen: list[str],
|
||||
uname: str,
|
||||
permsets: list[list[bool]],
|
||||
wantdots: bool,
|
||||
wantdots: int,
|
||||
scandir: bool,
|
||||
lstat: bool,
|
||||
subvols: bool = True,
|
||||
@@ -688,8 +693,8 @@ class VFS(object):
|
||||
and fsroot in seen
|
||||
):
|
||||
if self.log:
|
||||
t = "bailing from symlink loop,\n prev: {}\n curr: {}\n from: {}/{}"
|
||||
self.log("vfs.walk", t.format(seen[-1], fsroot, self.vpath, rem), 3)
|
||||
t = "bailing from symlink loop,\n prev: %r\n curr: %r\n from: %r / %r"
|
||||
self.log("vfs.walk", t % (seen[-1], fsroot, self.vpath, rem), 3)
|
||||
return
|
||||
|
||||
if "xdev" in self.flags or "xvol" in self.flags:
|
||||
@@ -701,7 +706,7 @@ class VFS(object):
|
||||
rm1.append(le)
|
||||
_ = [vfs_ls.remove(x) for x in rm1] # type: ignore
|
||||
|
||||
dots_ok = wantdots and uname in dbv.axs.udot
|
||||
dots_ok = wantdots and (wantdots == 2 or uname in dbv.axs.udot)
|
||||
if not dots_ok:
|
||||
vfs_ls = [x for x in vfs_ls if "/." not in "/" + x[0]]
|
||||
|
||||
@@ -755,7 +760,7 @@ class VFS(object):
|
||||
# if single folder: the folder itself is the top-level item
|
||||
folder = "" if flt or not wrap else (vpath.split("/")[-1].lstrip(".") or "top")
|
||||
|
||||
g = self.walk(folder, vrem, [], uname, [[True, False]], True, scandir, False)
|
||||
g = self.walk(folder, vrem, [], uname, [[True, False]], 1, scandir, False)
|
||||
for _, _, vpath, apath, files, rd, vd in g:
|
||||
if flt:
|
||||
files = [x for x in files if x[0] in flt]
|
||||
@@ -813,8 +818,8 @@ class VFS(object):
|
||||
|
||||
if vdev != st.st_dev:
|
||||
if self.log:
|
||||
t = "xdev: {}[{}] => {}[{}]"
|
||||
self.log("vfs", t.format(vdev, self.realpath, st.st_dev, ap), 3)
|
||||
t = "xdev: %s[%r] => %s[%r]"
|
||||
self.log("vfs", t % (vdev, self.realpath, st.st_dev, ap), 3)
|
||||
|
||||
return None
|
||||
|
||||
@@ -824,7 +829,7 @@ class VFS(object):
|
||||
return vn
|
||||
|
||||
if self.log:
|
||||
self.log("vfs", "xvol: [{}]".format(ap), 3)
|
||||
self.log("vfs", "xvol: %r" % (ap,), 3)
|
||||
|
||||
return None
|
||||
|
||||
@@ -909,7 +914,7 @@ class AuthSrv(object):
|
||||
|
||||
self.idp_accs[uname] = gnames
|
||||
|
||||
t = "reinitializing due to new user from IdP: [%s:%s]"
|
||||
t = "reinitializing due to new user from IdP: [%r:%r]"
|
||||
self.log(t % (uname, gnames), 3)
|
||||
|
||||
if not broker:
|
||||
@@ -1535,10 +1540,11 @@ class AuthSrv(object):
|
||||
|
||||
assert vfs # type: ignore
|
||||
vfs.all_vols = {}
|
||||
vfs.all_nodes = {}
|
||||
vfs.all_aps = []
|
||||
vfs.all_vps = []
|
||||
vfs.get_all_vols(vfs.all_vols, vfs.all_aps, vfs.all_vps)
|
||||
for vol in vfs.all_vols.values():
|
||||
vfs.get_all_vols(vfs.all_vols, vfs.all_nodes, vfs.all_aps, vfs.all_vps)
|
||||
for vol in vfs.all_nodes.values():
|
||||
vol.all_aps.sort(key=lambda x: len(x[0]), reverse=True)
|
||||
vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
|
||||
vol.root = vfs
|
||||
@@ -1562,7 +1568,7 @@ class AuthSrv(object):
|
||||
continue
|
||||
|
||||
if self.args.shr_v:
|
||||
t = "loading %s share [%s] by [%s] => [%s]"
|
||||
t = "loading %s share %r by %r => %r"
|
||||
self.log(t % (s_pr, s_k, s_un, s_vp))
|
||||
|
||||
if s_pw:
|
||||
@@ -1589,7 +1595,7 @@ class AuthSrv(object):
|
||||
|
||||
vfs.nodes[shr] = vfs.all_vols[shr] = shv
|
||||
for vol in shv.nodes.values():
|
||||
vfs.all_vols[vol.vpath] = vol
|
||||
vfs.all_vols[vol.vpath] = vfs.all_nodes[vol.vpath] = vol
|
||||
vol.get_dbv = vol._get_share_src
|
||||
vol.ls = vol._ls_nope
|
||||
|
||||
@@ -1732,7 +1738,19 @@ class AuthSrv(object):
|
||||
|
||||
self.log("\n\n".join(ta) + "\n", c=3)
|
||||
|
||||
vfs.histtab = {zv.realpath: zv.histpath for zv in vfs.all_vols.values()}
|
||||
rhisttab = {}
|
||||
vfs.histtab = {}
|
||||
for zv in vfs.all_vols.values():
|
||||
histp = zv.histpath
|
||||
is_shr = shr and zv.vpath.split("/")[0] == shr
|
||||
if histp and not is_shr and histp in rhisttab:
|
||||
zv2 = rhisttab[histp]
|
||||
t = "invalid config; multiple volumes share the same histpath (database location):\n histpath: %s\n volume 1: /%s [%s]\n volume 2: %s [%s]"
|
||||
t = t % (histp, zv2.vpath, zv2.realpath, zv.vpath, zv.realpath)
|
||||
self.log(t, 1)
|
||||
raise Exception(t)
|
||||
rhisttab[histp] = zv
|
||||
vfs.histtab[zv.realpath] = histp
|
||||
|
||||
for vol in vfs.all_vols.values():
|
||||
lim = Lim(self.log_func)
|
||||
@@ -1747,7 +1765,7 @@ class AuthSrv(object):
|
||||
use = True
|
||||
try:
|
||||
_ = float(zs)
|
||||
zs = "%sg" % (zs)
|
||||
zs = "%sg" % (zs,)
|
||||
except:
|
||||
pass
|
||||
lim.dfl = unhumanize(zs)
|
||||
@@ -1791,12 +1809,12 @@ class AuthSrv(object):
|
||||
vol.lim = lim
|
||||
|
||||
if self.args.no_robots:
|
||||
for vol in vfs.all_vols.values():
|
||||
for vol in vfs.all_nodes.values():
|
||||
# volflag "robots" overrides global "norobots", allowing indexing by search engines for this vol
|
||||
if not vol.flags.get("robots"):
|
||||
vol.flags["norobots"] = True
|
||||
|
||||
for vol in vfs.all_vols.values():
|
||||
for vol in vfs.all_nodes.values():
|
||||
if self.args.no_vthumb:
|
||||
vol.flags["dvthumb"] = True
|
||||
if self.args.no_athumb:
|
||||
@@ -1808,13 +1826,17 @@ class AuthSrv(object):
|
||||
vol.flags["dithumb"] = True
|
||||
|
||||
have_fk = False
|
||||
for vol in vfs.all_vols.values():
|
||||
for vol in vfs.all_nodes.values():
|
||||
fk = vol.flags.get("fk")
|
||||
fka = vol.flags.get("fka")
|
||||
if fka and not fk:
|
||||
fk = fka
|
||||
if fk:
|
||||
vol.flags["fk"] = int(fk) if fk is not True else 8
|
||||
fk = 8 if fk is True else int(fk)
|
||||
if fk > 72:
|
||||
t = "max filekey-length is 72; volume /%s specified %d (anything higher than 16 is pointless btw)"
|
||||
raise Exception(t % (vol.vpath, fk))
|
||||
vol.flags["fk"] = fk
|
||||
have_fk = True
|
||||
|
||||
dk = vol.flags.get("dk")
|
||||
@@ -1840,7 +1862,7 @@ class AuthSrv(object):
|
||||
zs = os.path.join(E.cfg, "fk-salt.txt")
|
||||
self.log(t % (fk_len, 16, zs), 3)
|
||||
|
||||
for vol in vfs.all_vols.values():
|
||||
for vol in vfs.all_nodes.values():
|
||||
if "pk" in vol.flags and "gz" not in vol.flags and "xz" not in vol.flags:
|
||||
vol.flags["gz"] = False # def.pk
|
||||
|
||||
@@ -1851,7 +1873,7 @@ class AuthSrv(object):
|
||||
|
||||
all_mte = {}
|
||||
errors = False
|
||||
for vol in vfs.all_vols.values():
|
||||
for vol in vfs.all_nodes.values():
|
||||
if (self.args.e2ds and vol.axs.uwrite) or self.args.e2dsa:
|
||||
vol.flags["e2ds"] = True
|
||||
|
||||
@@ -1862,6 +1884,7 @@ class AuthSrv(object):
|
||||
["no_hash", "nohash"],
|
||||
["no_idx", "noidx"],
|
||||
["og_ua", "og_ua"],
|
||||
["srch_excl", "srch_excl"],
|
||||
]:
|
||||
if vf in vol.flags:
|
||||
ptn = re.compile(vol.flags.pop(vf))
|
||||
@@ -2068,8 +2091,24 @@ class AuthSrv(object):
|
||||
self.log(t.format(mtp), 1)
|
||||
errors = True
|
||||
|
||||
have_daw = False
|
||||
for vol in vfs.all_vols.values():
|
||||
re1: Optional[re.Pattern] = vol.flags.get("srch_excl")
|
||||
excl = [re1.pattern] if re1 else []
|
||||
|
||||
vpaths = []
|
||||
vtop = vol.vpath
|
||||
for vp2 in vfs.all_vols.keys():
|
||||
if vp2.startswith((vtop + "/").lstrip("/")) and vtop != vp2:
|
||||
vpaths.append(re.escape(vp2[len(vtop) :].lstrip("/")))
|
||||
if vpaths:
|
||||
excl.append("^(%s)/" % ("|".join(vpaths),))
|
||||
|
||||
vol.flags["srch_re_dots"] = re.compile("|".join(excl or ["^$"]))
|
||||
excl.extend([r"^\.", r"/\."])
|
||||
vol.flags["srch_re_nodot"] = re.compile("|".join(excl))
|
||||
|
||||
have_daw = False
|
||||
for vol in vfs.all_nodes.values():
|
||||
daw = vol.flags.get("daw") or self.args.daw
|
||||
if daw:
|
||||
vol.flags["daw"] = True
|
||||
@@ -2084,13 +2123,12 @@ class AuthSrv(object):
|
||||
self.log("--smb can only be used when --ah-alg is none", 1)
|
||||
errors = True
|
||||
|
||||
for vol in vfs.all_vols.values():
|
||||
for vol in vfs.all_nodes.values():
|
||||
for k in list(vol.flags.keys()):
|
||||
if re.match("^-[^-]+$", k):
|
||||
vol.flags.pop(k[1:], None)
|
||||
vol.flags.pop(k)
|
||||
|
||||
for vol in vfs.all_vols.values():
|
||||
if vol.flags.get("dots"):
|
||||
for name in vol.axs.uread:
|
||||
vol.axs.udot.add(name)
|
||||
@@ -2147,11 +2185,11 @@ class AuthSrv(object):
|
||||
if not self.args.no_voldump:
|
||||
self.log(t)
|
||||
|
||||
if have_e2d:
|
||||
if have_e2d or self.args.idp_h_usr:
|
||||
t = self.chk_sqlite_threadsafe()
|
||||
if t:
|
||||
self.log("\n\033[{}\033[0m\n".format(t))
|
||||
|
||||
if have_e2d:
|
||||
if not have_e2t:
|
||||
t = "hint: enable multimedia indexing (artist/title/...) with argument -e2ts"
|
||||
self.log(t, 6)
|
||||
@@ -2232,6 +2270,11 @@ class AuthSrv(object):
|
||||
for x, y in vfs.all_vols.items()
|
||||
if x != shr and not x.startswith(shrs)
|
||||
}
|
||||
vfs.all_nodes = {
|
||||
x: y
|
||||
for x, y in vfs.all_nodes.items()
|
||||
if x != shr and not x.startswith(shrs)
|
||||
}
|
||||
|
||||
assert db and cur and cur2 and shv # type: ignore
|
||||
for row in cur.execute("select * from sh"):
|
||||
@@ -2284,6 +2327,74 @@ class AuthSrv(object):
|
||||
cur.close()
|
||||
db.close()
|
||||
|
||||
self.js_ls = {}
|
||||
self.js_htm = {}
|
||||
for vn in self.vfs.all_nodes.values():
|
||||
vf = vn.flags
|
||||
vn.js_ls = {
|
||||
"idx": "e2d" in vf,
|
||||
"itag": "e2t" in vf,
|
||||
"dnsort": "nsort" in vf,
|
||||
"dhsortn": vf["hsortn"],
|
||||
"dsort": vf["sort"],
|
||||
"dcrop": vf["crop"],
|
||||
"dth3x": vf["th3x"],
|
||||
"u2ts": vf["u2ts"],
|
||||
"frand": bool(vf.get("rand")),
|
||||
"lifetime": vf.get("lifetime") or 0,
|
||||
"unlist": vf.get("unlist") or "",
|
||||
"sb_lg": "" if "no_sb_lg" in vf else (vf.get("lg_sbf") or "y"),
|
||||
}
|
||||
js_htm = {
|
||||
"s_name": self.args.bname,
|
||||
"have_up2k_idx": "e2d" in vf,
|
||||
"have_acode": not self.args.no_acode,
|
||||
"have_shr": self.args.shr,
|
||||
"have_zip": not self.args.no_zip,
|
||||
"have_mv": not self.args.no_mv,
|
||||
"have_del": not self.args.no_del,
|
||||
"have_unpost": int(self.args.unpost),
|
||||
"have_emp": self.args.emp,
|
||||
"sb_md": "" if "no_sb_md" in vf else (vf.get("md_sbf") or "y"),
|
||||
"sba_md": vf.get("md_sba") or "",
|
||||
"sba_lg": vf.get("lg_sba") or "",
|
||||
"txt_ext": self.args.textfiles.replace(",", " "),
|
||||
"def_hcols": list(vf.get("mth") or []),
|
||||
"unlist0": vf.get("unlist") or "",
|
||||
"dgrid": "grid" in vf,
|
||||
"dgsel": "gsel" in vf,
|
||||
"dnsort": "nsort" in vf,
|
||||
"dhsortn": vf["hsortn"],
|
||||
"dsort": vf["sort"],
|
||||
"dcrop": vf["crop"],
|
||||
"dth3x": vf["th3x"],
|
||||
"dvol": self.args.au_vol,
|
||||
"idxh": int(self.args.ih),
|
||||
"themes": self.args.themes,
|
||||
"turbolvl": self.args.turbo,
|
||||
"u2j": self.args.u2j,
|
||||
"u2sz": self.args.u2sz,
|
||||
"u2ts": vf["u2ts"],
|
||||
"frand": bool(vf.get("rand")),
|
||||
"lifetime": vn.js_ls["lifetime"],
|
||||
"u2sort": self.args.u2sort,
|
||||
}
|
||||
vn.js_htm = json.dumps(js_htm)
|
||||
|
||||
vols = list(vfs.all_nodes.values())
|
||||
if enshare:
|
||||
assert shv # type: ignore # !rm
|
||||
vols.append(shv)
|
||||
vols.extend(list(shv.nodes.values()))
|
||||
|
||||
for vol in vols:
|
||||
dbv = vol.get_dbv("")[0]
|
||||
vol.js_ls = vol.js_ls or dbv.js_ls or {}
|
||||
vol.js_htm = vol.js_htm or dbv.js_htm or "{}"
|
||||
|
||||
zs = str(vol.flags.get("tcolor") or self.args.tcolor)
|
||||
vol.flags["tcolor"] = zs.lstrip("#")
|
||||
|
||||
def load_sessions(self, quiet=False) -> None:
|
||||
# mutex me
|
||||
if self.args.no_ses:
|
||||
@@ -2434,7 +2545,7 @@ class AuthSrv(object):
|
||||
return
|
||||
|
||||
elif self.args.chpw_v == 2:
|
||||
t = "chpw: %d changed" % (len(uok))
|
||||
t = "chpw: %d changed" % (len(uok),)
|
||||
if urst:
|
||||
t += ", \033[0munchanged:\033[35m %s" % (", ".join(list(urst)))
|
||||
|
||||
@@ -2592,7 +2703,7 @@ class AuthSrv(object):
|
||||
[],
|
||||
u,
|
||||
[[True, False]],
|
||||
True,
|
||||
1,
|
||||
not self.args.no_scandir,
|
||||
False,
|
||||
False,
|
||||
@@ -2913,6 +3024,19 @@ def expand_config_file(
|
||||
|
||||
ret.append("#\033[36m closed{}\033[0m".format(ipath))
|
||||
|
||||
zsl = []
|
||||
for ln in ret:
|
||||
zs = ln.split(" #")[0]
|
||||
if " #" in zs and zs.split("#")[0].strip():
|
||||
zsl.append(ln)
|
||||
if zsl and "no-cfg-cmt-warn" not in "\n".join(ret):
|
||||
t = "\033[33mWARNING: there is less than two spaces before the # in the following config lines, so instead of assuming that this is a comment, the whole line will become part of the config value:\n\n>>> %s\n\nif you are familiar with this and would like to mute this warning, specify the global-option no-cfg-cmt-warn\n\033[0m"
|
||||
t = t % ("\n>>> ".join(zsl),)
|
||||
if log:
|
||||
log(t)
|
||||
else:
|
||||
print(t, file=sys.stderr)
|
||||
|
||||
|
||||
def upgrade_cfg_fmt(
|
||||
log: Optional["NamedLogger"], args: argparse.Namespace, orig: list[str], cfg_fp: str
|
||||
|
||||
@@ -42,6 +42,7 @@ def vf_bmap() -> dict[str, str]:
|
||||
"magic",
|
||||
"no_sb_md",
|
||||
"no_sb_lg",
|
||||
"nsort",
|
||||
"og",
|
||||
"og_no_head",
|
||||
"og_s_title",
|
||||
@@ -69,9 +70,12 @@ def vf_vmap() -> dict[str, str]:
|
||||
}
|
||||
for k in (
|
||||
"dbd",
|
||||
"hsortn",
|
||||
"html_head",
|
||||
"lg_sbf",
|
||||
"md_sbf",
|
||||
"lg_sba",
|
||||
"md_sba",
|
||||
"nrand",
|
||||
"og_desc",
|
||||
"og_site",
|
||||
@@ -89,6 +93,7 @@ def vf_vmap() -> dict[str, str]:
|
||||
"unlist",
|
||||
"u2abort",
|
||||
"u2ts",
|
||||
"ups_who",
|
||||
):
|
||||
ret[k] = k
|
||||
return ret
|
||||
@@ -142,6 +147,7 @@ flagcats = {
|
||||
"noclone": "take dupe data from clients, even if available on HDD",
|
||||
"nodupe": "rejects existing files (instead of linking/cloning them)",
|
||||
"sparse": "force use of sparse files, mainly for s3-backed storage",
|
||||
"nosparse": "deny use of sparse files, mainly for slow storage",
|
||||
"daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
|
||||
"nosub": "forces all uploads into the top folder of the vfs",
|
||||
"magic": "enables filetype detection for nameless uploads",
|
||||
@@ -190,6 +196,7 @@ flagcats = {
|
||||
"xvol": "do not follow symlinks leaving the volume root",
|
||||
"dotsrch": "show dotfiles in search results",
|
||||
"nodotsrch": "hide dotfiles in search results (default)",
|
||||
"srch_excl": "exclude search results with URL matching this regex",
|
||||
},
|
||||
'database, audio tags\n"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ...': {
|
||||
"mtp=.bpm=f,audio-bpm.py": 'uses the "audio-bpm.py" program to\ngenerate ".bpm" tags from uploads (f = overwrite tags)',
|
||||
@@ -237,6 +244,8 @@ flagcats = {
|
||||
"sb_lg": "enable js sandbox for prologue/epilogue (default)",
|
||||
"md_sbf": "list of markdown-sandbox safeguards to disable",
|
||||
"lg_sbf": "list of *logue-sandbox safeguards to disable",
|
||||
"md_sba": "value of iframe allow-prop for markdown-sandbox",
|
||||
"lg_sba": "value of iframe allow-prop for *logue-sandbox",
|
||||
"nohtml": "return html and markdown as text/html",
|
||||
},
|
||||
"others": {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
@@ -8,6 +11,10 @@ if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
class BadXML(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def get_ET() -> ET.XMLParser:
|
||||
pn = "xml.etree.ElementTree"
|
||||
cn = "_elementtree"
|
||||
@@ -34,7 +41,7 @@ def get_ET() -> ET.XMLParser:
|
||||
XMLParser: ET.XMLParser = get_ET()
|
||||
|
||||
|
||||
class DXMLParser(XMLParser): # type: ignore
|
||||
class _DXMLParser(XMLParser): # type: ignore
|
||||
def __init__(self) -> None:
|
||||
tb = ET.TreeBuilder()
|
||||
super(DXMLParser, self).__init__(target=tb)
|
||||
@@ -49,8 +56,12 @@ class DXMLParser(XMLParser): # type: ignore
|
||||
raise BadXML("{}, {}".format(a, ka))
|
||||
|
||||
|
||||
class BadXML(Exception):
|
||||
pass
|
||||
class _NG(XMLParser): # type: ignore
|
||||
def __int__(self) -> None:
|
||||
raise BadXML("dxml selftest failed")
|
||||
|
||||
|
||||
DXMLParser = _DXMLParser
|
||||
|
||||
|
||||
def parse_xml(txt: str) -> ET.Element:
|
||||
@@ -59,6 +70,40 @@ def parse_xml(txt: str) -> ET.Element:
|
||||
return parser.close() # type: ignore
|
||||
|
||||
|
||||
def selftest() -> bool:
|
||||
qbe = r"""<!DOCTYPE d [
|
||||
<!ENTITY a "nice_bakuretsu">
|
||||
]>
|
||||
<root>&a;&a;&a;</root>"""
|
||||
|
||||
emb = r"""<!DOCTYPE d [
|
||||
<!ENTITY a SYSTEM "file:///etc/hostname">
|
||||
]>
|
||||
<root>&a;</root>"""
|
||||
|
||||
# future-proofing; there's never been any known vulns
|
||||
# regarding DTDs and ET.XMLParser, but might as well
|
||||
# block them since webdav-clients don't use them
|
||||
dtd = r"""<!DOCTYPE d SYSTEM "a.dtd">
|
||||
<root>a</root>"""
|
||||
|
||||
for txt in (qbe, emb, dtd):
|
||||
try:
|
||||
parse_xml(txt)
|
||||
t = "WARNING: dxml selftest failed:\n%s\n"
|
||||
print(t % (txt,), file=sys.stderr)
|
||||
return False
|
||||
except BadXML:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
|
||||
DXML_OK = selftest()
|
||||
if not DXML_OK:
|
||||
DXMLParser = _NG
|
||||
|
||||
|
||||
def mktnod(name: str, text: str) -> ET.Element:
|
||||
el = ET.Element(name)
|
||||
el.text = text
|
||||
|
||||
@@ -42,14 +42,14 @@ class Fstab(object):
|
||||
self.cache = {}
|
||||
|
||||
fs = "ext4"
|
||||
msg = "failed to determine filesystem at [{}]; assuming {}\n{}"
|
||||
msg = "failed to determine filesystem at %r; assuming %s\n%s"
|
||||
|
||||
if ANYWIN:
|
||||
fs = "vfat"
|
||||
try:
|
||||
path = self._winpath(path)
|
||||
except:
|
||||
self.log(msg.format(path, fs, min_ex()), 3)
|
||||
self.log(msg % (path, fs, min_ex()), 3)
|
||||
return fs
|
||||
|
||||
path = undot(path)
|
||||
@@ -61,11 +61,11 @@ class Fstab(object):
|
||||
try:
|
||||
fs = self.get_w32(path) if ANYWIN else self.get_unix(path)
|
||||
except:
|
||||
self.log(msg.format(path, fs, min_ex()), 3)
|
||||
self.log(msg % (path, fs, min_ex()), 3)
|
||||
|
||||
fs = fs.lower()
|
||||
self.cache[path] = fs
|
||||
self.log("found {} at {}".format(fs, path))
|
||||
self.log("found %s at %r" % (fs, path))
|
||||
return fs
|
||||
|
||||
def _winpath(self, path: str) -> str:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -135,7 +135,7 @@ class HttpSrv(object):
|
||||
dls: dict[str, tuple[float, int]] = {} # state
|
||||
self.dli = self.tdli = dli
|
||||
self.dls = self.tdls = dls
|
||||
self.iiam = '<img src="%s.cpr/iiam.gif" />' % (self.args.SRS,)
|
||||
self.iiam = '<img src="%s.cpr/iiam.gif?cache=i" />' % (self.args.SRS,)
|
||||
|
||||
self.bound: set[tuple[str, int]] = set()
|
||||
self.name = "hsrv" + nsuf
|
||||
@@ -172,15 +172,16 @@ class HttpSrv(object):
|
||||
env = jinja2.Environment()
|
||||
env.loader = jinja2.FunctionLoader(lambda f: load_jinja2_resource(self.E, f))
|
||||
jn = [
|
||||
"splash",
|
||||
"shares",
|
||||
"svcs",
|
||||
"browser",
|
||||
"browser2",
|
||||
"msg",
|
||||
"cf",
|
||||
"md",
|
||||
"mde",
|
||||
"cf",
|
||||
"msg",
|
||||
"rups",
|
||||
"shares",
|
||||
"splash",
|
||||
"svcs",
|
||||
]
|
||||
self.j2 = {x: env.get_template(x + ".html") for x in jn}
|
||||
self.prism = has_resource(self.E, "web/deps/prism.js.gz")
|
||||
@@ -194,10 +195,6 @@ class HttpSrv(object):
|
||||
self.xff_nm = build_netmap(self.args.xff_src)
|
||||
self.xff_lan = build_netmap("lan")
|
||||
|
||||
self.ptn_cc = re.compile(r"[\x00-\x1f]")
|
||||
self.ptn_hsafe = re.compile(r"[\x00-\x1f<>\"'&]")
|
||||
self.uparam_cc_ok = set("doc move tree".split())
|
||||
|
||||
self.mallow = "GET HEAD POST PUT DELETE OPTIONS".split()
|
||||
if not self.args.no_dav:
|
||||
zs = "PROPFIND PROPPATCH LOCK UNLOCK MKCOL COPY MOVE"
|
||||
@@ -613,7 +610,7 @@ class HttpSrv(object):
|
||||
"""
|
||||
dli: dict[str, tuple[float, int, "VFS", str, str]] = {}
|
||||
for k, (a, b, c, d, e) in sdli.items():
|
||||
vn = self.asrv.vfs.all_vols[c]
|
||||
vn = self.asrv.vfs.all_nodes[c]
|
||||
dli[k] = (a, b, vn, d, e)
|
||||
|
||||
self.tdli = dli
|
||||
|
||||
@@ -25,6 +25,7 @@ from .stolen.dnslib import (
|
||||
DNSHeader,
|
||||
DNSQuestion,
|
||||
DNSRecord,
|
||||
set_avahi_379,
|
||||
)
|
||||
from .util import CachedSet, Daemon, Netdev, list_ips, min_ex
|
||||
|
||||
@@ -72,6 +73,9 @@ class MDNS(MCast):
|
||||
self.ngen = ngen
|
||||
self.ttl = 300
|
||||
|
||||
if not self.args.zm_nwa_1:
|
||||
set_avahi_379()
|
||||
|
||||
zs = self.args.name + ".local."
|
||||
zs = zs.encode("ascii", "replace").decode("ascii", "replace")
|
||||
self.hn = "-".join(x for x in zs.split("?") if x) or (
|
||||
@@ -336,6 +340,9 @@ class MDNS(MCast):
|
||||
self.log("stopped", 2)
|
||||
return
|
||||
|
||||
if self.args.zm_no_pe:
|
||||
continue
|
||||
|
||||
t = "{} {} \033[33m|{}| {}\n{}".format(
|
||||
self.srv[sck].name, addr, len(buf), repr(buf)[2:-1], min_ex()
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import print_function, unicode_literals
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess as sp
|
||||
import sys
|
||||
@@ -62,6 +63,9 @@ def have_ff(scmd: str) -> bool:
|
||||
HAVE_FFMPEG = not os.environ.get("PRTY_NO_FFMPEG") and have_ff("ffmpeg")
|
||||
HAVE_FFPROBE = not os.environ.get("PRTY_NO_FFPROBE") and have_ff("ffprobe")
|
||||
|
||||
CBZ_PICS = set("png jpg jpeg gif bmp tga tif tiff webp avif".split())
|
||||
CBZ_01 = re.compile(r"(^|[^0-9v])0+[01]\b")
|
||||
|
||||
|
||||
class MParser(object):
|
||||
def __init__(self, cmdline: str) -> None:
|
||||
@@ -126,6 +130,7 @@ def au_unpk(
|
||||
log: "NamedLogger", fmt_map: dict[str, str], abspath: str, vn: Optional[VFS] = None
|
||||
) -> str:
|
||||
ret = ""
|
||||
maxsz = 1024 * 1024 * 64
|
||||
try:
|
||||
ext = abspath.split(".")[-1].lower()
|
||||
au, pk = fmt_map[ext].split(".")
|
||||
@@ -148,24 +153,48 @@ def au_unpk(
|
||||
zf = zipfile.ZipFile(abspath, "r")
|
||||
zil = zf.infolist()
|
||||
zil = [x for x in zil if x.filename.lower().split(".")[-1] == au]
|
||||
if not zil:
|
||||
raise Exception("no audio inside zip")
|
||||
fi = zf.open(zil[0])
|
||||
|
||||
elif pk == "cbz":
|
||||
import zipfile
|
||||
|
||||
zf = zipfile.ZipFile(abspath, "r")
|
||||
znil = [(x.filename.lower(), x) for x in zf.infolist()]
|
||||
nf = len(znil)
|
||||
znil = [x for x in znil if x[0].split(".")[-1] in CBZ_PICS]
|
||||
znil = [x for x in znil if "cover" in x[0]] or znil
|
||||
znil = [x for x in znil if CBZ_01.search(x[0])] or znil
|
||||
t = "cbz: %d files, %d hits" % (nf, len(znil))
|
||||
if znil:
|
||||
t += ", using " + znil[0][1].filename
|
||||
log(t)
|
||||
if not znil:
|
||||
raise Exception("no images inside cbz")
|
||||
fi = zf.open(znil[0][1])
|
||||
|
||||
else:
|
||||
raise Exception("unknown compression %s" % (pk,))
|
||||
|
||||
fsz = 0
|
||||
with os.fdopen(fd, "wb") as fo:
|
||||
while True:
|
||||
buf = fi.read(32768)
|
||||
if not buf:
|
||||
break
|
||||
|
||||
fsz += len(buf)
|
||||
if fsz > maxsz:
|
||||
raise Exception("zipbomb defused")
|
||||
|
||||
fo.write(buf)
|
||||
|
||||
return ret
|
||||
|
||||
except Exception as ex:
|
||||
if ret:
|
||||
t = "failed to decompress audio file [%s]: %r"
|
||||
t = "failed to decompress audio file %r: %r"
|
||||
log(t % (abspath, ex))
|
||||
wunlink(log, ret, vn.flags if vn else VF_CAREFUL)
|
||||
|
||||
@@ -553,7 +582,7 @@ class MTag(object):
|
||||
raise Exception()
|
||||
except Exception as ex:
|
||||
if self.args.mtag_v:
|
||||
self.log("mutagen-err [{}] @ [{}]".format(ex, abspath), "90")
|
||||
self.log("mutagen-err [%s] @ %r" % (ex, abspath), "90")
|
||||
|
||||
return self.get_ffprobe(abspath) if self.can_ffprobe else {}
|
||||
|
||||
@@ -670,8 +699,8 @@ class MTag(object):
|
||||
ret[tag] = zj[tag]
|
||||
except:
|
||||
if self.args.mtag_v:
|
||||
t = "mtag error: tagname {}, parser {}, file {} => {}"
|
||||
self.log(t.format(tagname, parser.bin, abspath, min_ex()))
|
||||
t = "mtag error: tagname %r, parser %r, file %r => %r"
|
||||
self.log(t % (tagname, parser.bin, abspath, min_ex()), 6)
|
||||
|
||||
if ap != abspath:
|
||||
wunlink(self.log, ap, VF_CAREFUL)
|
||||
|
||||
@@ -24,17 +24,13 @@ class PWHash(object):
|
||||
def __init__(self, args: argparse.Namespace):
|
||||
self.args = args
|
||||
|
||||
try:
|
||||
alg, ac = args.ah_alg.split(",")
|
||||
except:
|
||||
alg = args.ah_alg
|
||||
ac = {}
|
||||
|
||||
zsl = args.ah_alg.split(",")
|
||||
alg = zsl[0]
|
||||
if alg == "none":
|
||||
alg = ""
|
||||
|
||||
self.alg = alg
|
||||
self.ac = ac
|
||||
self.ac = zsl[1:]
|
||||
if not alg:
|
||||
self.on = False
|
||||
self.hash = unicode
|
||||
@@ -90,17 +86,23 @@ class PWHash(object):
|
||||
its = 2
|
||||
blksz = 8
|
||||
para = 4
|
||||
ramcap = 0 # openssl 1.1 = 32 MiB
|
||||
try:
|
||||
cost = 2 << int(self.ac[0])
|
||||
its = int(self.ac[1])
|
||||
blksz = int(self.ac[2])
|
||||
para = int(self.ac[3])
|
||||
ramcap = int(self.ac[4]) * 1024 * 1024
|
||||
except:
|
||||
pass
|
||||
|
||||
cfg = {"salt": self.salt, "n": cost, "r": blksz, "p": para, "dklen": 24}
|
||||
if ramcap:
|
||||
cfg["maxmem"] = ramcap
|
||||
|
||||
ret = plain.encode("utf-8")
|
||||
for _ in range(its):
|
||||
ret = hashlib.scrypt(ret, salt=self.salt, n=cost, r=blksz, p=para, dklen=24)
|
||||
ret = hashlib.scrypt(ret, **cfg)
|
||||
|
||||
return "+" + base64.urlsafe_b64encode(ret).decode("utf-8")
|
||||
|
||||
|
||||
@@ -263,7 +263,7 @@ class SMB(object):
|
||||
time.time(),
|
||||
"",
|
||||
):
|
||||
yeet("blocked by xbu server config: " + vpath)
|
||||
yeet("blocked by xbu server config: %r" % (vpath,))
|
||||
|
||||
ret = bos.open(ap, flags, *a, mode=chmod, **ka)
|
||||
if wr:
|
||||
|
||||
@@ -8,7 +8,7 @@ from itertools import chain
|
||||
from .bimap import Bimap, BimapError
|
||||
from .bit import get_bits, set_bits
|
||||
from .buffer import BufferError
|
||||
from .label import DNSBuffer, DNSLabel
|
||||
from .label import DNSBuffer, DNSLabel, set_avahi_379
|
||||
from .ranges import IP4, IP6, H, I, check_bytes
|
||||
|
||||
|
||||
@@ -426,7 +426,7 @@ class RR(object):
|
||||
if rdlength:
|
||||
rdata = RDMAP.get(QTYPE.get(rtype), RD).parse(buffer, rdlength)
|
||||
else:
|
||||
rdata = ""
|
||||
rdata = RD(b"a")
|
||||
return cls(rname, rtype, rclass, ttl, rdata)
|
||||
except (BufferError, BimapError) as e:
|
||||
raise DNSError("Error unpacking RR [offset=%d]: %s" % (buffer.offset, e))
|
||||
|
||||
@@ -11,6 +11,23 @@ LDH = set(range(33, 127))
|
||||
ESCAPE = re.compile(r"\\([0-9][0-9][0-9])")
|
||||
|
||||
|
||||
avahi_379 = 0
|
||||
|
||||
|
||||
def set_avahi_379():
|
||||
global avahi_379
|
||||
avahi_379 = 1
|
||||
|
||||
|
||||
def log_avahi_379(args):
|
||||
global avahi_379
|
||||
if avahi_379 == 2:
|
||||
return
|
||||
avahi_379 = 2
|
||||
t = "Invalid pointer in DNSLabel [offset=%d,pointer=%d,length=%d];\n\033[35m NOTE: this is probably avahi-bug #379, packet corruption in Avahi's mDNS-reflection feature. Copyparty has a workaround and is OK, but other devices need either --zm4 or --zm6"
|
||||
raise BufferError(t % args)
|
||||
|
||||
|
||||
class DNSLabelError(Exception):
|
||||
pass
|
||||
|
||||
@@ -96,8 +113,11 @@ class DNSBuffer(Buffer):
|
||||
)
|
||||
if pointer < self.offset:
|
||||
self.offset = pointer
|
||||
elif avahi_379:
|
||||
log_avahi_379((self.offset, pointer, len(self.data)))
|
||||
label.extend(b"a")
|
||||
break
|
||||
else:
|
||||
|
||||
raise BufferError(
|
||||
"Invalid pointer in DNSLabel [offset=%d,pointer=%d,length=%d]"
|
||||
% (self.offset, pointer, len(self.data))
|
||||
|
||||
@@ -110,7 +110,7 @@ def errdesc(
|
||||
report = ["copyparty failed to add the following files to the archive:", ""]
|
||||
|
||||
for fn, err in errors:
|
||||
report.extend([" file: {}".format(fn), "error: {}".format(err), ""])
|
||||
report.extend([" file: %r" % (fn,), "error: %s" % (err,), ""])
|
||||
|
||||
btxt = "\r\n".join(report).encode("utf-8", "replace")
|
||||
btxt = vol_san(list(vfs.all_vols.values()), btxt)
|
||||
|
||||
@@ -50,6 +50,8 @@ from .util import (
|
||||
FFMPEG_URL,
|
||||
HAVE_PSUTIL,
|
||||
HAVE_SQLITE3,
|
||||
HAVE_ZMQ,
|
||||
URL_BUG,
|
||||
UTC,
|
||||
VERSIONS,
|
||||
Daemon,
|
||||
@@ -60,6 +62,7 @@ from .util import (
|
||||
alltrace,
|
||||
ansi_re,
|
||||
build_netmap,
|
||||
expat_ver,
|
||||
load_ipu,
|
||||
min_ex,
|
||||
mp,
|
||||
@@ -639,6 +642,7 @@ class SvcHub(object):
|
||||
(HAVE_FFPROBE, "ffprobe", t_ff + ", read audio/media tags"),
|
||||
(HAVE_MUTAGEN, "mutagen", "read audio tags (ffprobe is better but slower)"),
|
||||
(HAVE_ARGON2, "argon2", "secure password hashing (advanced users only)"),
|
||||
(HAVE_ZMQ, "pyzmq", "send zeromq messages from event-hooks"),
|
||||
(HAVE_HEIF, "pillow-heif", "read .heif images with pillow (rarely useful)"),
|
||||
(HAVE_AVIF, "pillow-avif", "read .avif images with pillow (rarely useful)"),
|
||||
]
|
||||
@@ -695,6 +699,15 @@ class SvcHub(object):
|
||||
if self.args.bauth_last:
|
||||
self.log("root", "WARNING: ignoring --bauth-last due to --no-bauth", 3)
|
||||
|
||||
if not self.args.no_dav:
|
||||
from .dxml import DXML_OK
|
||||
|
||||
if not DXML_OK:
|
||||
if not self.args.no_dav:
|
||||
self.args.no_dav = True
|
||||
t = "WARNING:\nDisabling WebDAV support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n"
|
||||
self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1)
|
||||
|
||||
def _process_config(self) -> bool:
|
||||
al = self.args
|
||||
|
||||
@@ -793,7 +806,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", "og_ua"]:
|
||||
for k in ["no_hash", "no_idx", "og_ua", "srch_excl"]:
|
||||
ptn = getattr(self.args, k)
|
||||
if ptn:
|
||||
setattr(self.args, k, re.compile(ptn))
|
||||
|
||||
@@ -402,17 +402,17 @@ class TcpSrv(object):
|
||||
if not netdevs:
|
||||
continue
|
||||
|
||||
added = "nothing"
|
||||
removed = "nothing"
|
||||
add = []
|
||||
rem = []
|
||||
for k, v in netdevs.items():
|
||||
if k not in self.netdevs:
|
||||
added = "{} = {}".format(k, v)
|
||||
add.append("\n\033[32m added %s = %s" % (k, v))
|
||||
for k, v in self.netdevs.items():
|
||||
if k not in netdevs:
|
||||
removed = "{} = {}".format(k, v)
|
||||
rem.append("\n\033[33mremoved %s = %s" % (k, v))
|
||||
|
||||
t = "network change detected:\n added {}\033[0;33m\nremoved {}"
|
||||
self.log("tcpsrv", t.format(added, removed), 3)
|
||||
t = "network change detected:%s%s"
|
||||
self.log("tcpsrv", t % ("".join(add), "".join(rem)), 3)
|
||||
self.netdevs = netdevs
|
||||
self._distribute_netdevs()
|
||||
|
||||
|
||||
@@ -357,7 +357,7 @@ class Tftpd(object):
|
||||
time.time(),
|
||||
"",
|
||||
):
|
||||
yeet("blocked by xbu server config: " + vpath)
|
||||
yeet("blocked by xbu server config: %r" % (vpath,))
|
||||
|
||||
if not self.args.tftp_nols and bos.path.isdir(ap):
|
||||
return self._ls(vpath, "", 0, True)
|
||||
|
||||
@@ -6,7 +6,7 @@ import os
|
||||
from .__init__ import TYPE_CHECKING
|
||||
from .authsrv import VFS
|
||||
from .bos import bos
|
||||
from .th_srv import HAVE_WEBP, thumb_path
|
||||
from .th_srv import EXTS_AC, HAVE_WEBP, thumb_path
|
||||
from .util import Cooldown
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
@@ -57,13 +57,17 @@ class ThumbCli(object):
|
||||
if is_vid and "dvthumb" in dbv.flags:
|
||||
return None
|
||||
|
||||
want_opus = fmt in ("opus", "caf", "mp3")
|
||||
want_opus = fmt in EXTS_AC
|
||||
is_au = ext in self.fmt_ffa
|
||||
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
|
||||
elif fmt == "caf" and self.args.no_caf:
|
||||
fmt = "mp3"
|
||||
elif fmt == "owa" and self.args.no_owa:
|
||||
fmt = "mp3"
|
||||
else:
|
||||
if "dathumb" in dbv.flags:
|
||||
return None
|
||||
@@ -109,13 +113,13 @@ 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)
|
||||
t = "cannot thumbnail %r: 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))
|
||||
self.log("no histpath for %r" % (ptop,))
|
||||
return None
|
||||
|
||||
tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa)
|
||||
|
||||
@@ -32,7 +32,7 @@ from .util import (
|
||||
)
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Optional, Union
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .svchub import SvcHub
|
||||
@@ -46,6 +46,9 @@ HAVE_HEIF = False
|
||||
HAVE_AVIF = False
|
||||
HAVE_WEBP = False
|
||||
|
||||
EXTS_TH = set(["jpg", "webp", "png"])
|
||||
EXTS_AC = set(["opus", "owa", "caf", "mp3"])
|
||||
|
||||
try:
|
||||
if os.environ.get("PRTY_NO_PIL"):
|
||||
raise Exception()
|
||||
@@ -139,7 +142,7 @@ def thumb_path(histpath: str, rem: str, mtime: float, fmt: str, ffa: set[str]) -
|
||||
h = hashlib.sha512(afsenc(fn)).digest()
|
||||
fn = ub64enc(h).decode("ascii")[:24]
|
||||
|
||||
if fmt in ("opus", "caf", "mp3"):
|
||||
if fmt in EXTS_AC:
|
||||
cat = "ac"
|
||||
else:
|
||||
fc = fmt[:1]
|
||||
@@ -239,7 +242,7 @@ class ThumbSrv(object):
|
||||
def get(self, ptop: str, rem: str, mtime: float, fmt: str) -> Optional[str]:
|
||||
histpath = self.asrv.vfs.histtab.get(ptop)
|
||||
if not histpath:
|
||||
self.log("no histpath for [{}]".format(ptop))
|
||||
self.log("no histpath for %r" % (ptop,))
|
||||
return None
|
||||
|
||||
tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa)
|
||||
@@ -249,7 +252,7 @@ class ThumbSrv(object):
|
||||
with self.mutex:
|
||||
try:
|
||||
self.busy[tpath].append(cond)
|
||||
self.log("joined waiting room for %s" % (tpath,))
|
||||
self.log("joined waiting room for %r" % (tpath,))
|
||||
except:
|
||||
thdir = os.path.dirname(tpath)
|
||||
bos.makedirs(os.path.join(thdir, "w"))
|
||||
@@ -266,11 +269,11 @@ class ThumbSrv(object):
|
||||
allvols = list(self.asrv.vfs.all_vols.values())
|
||||
vn = next((x for x in allvols if x.realpath == ptop), None)
|
||||
if not vn:
|
||||
self.log("ptop [{}] not in {}".format(ptop, allvols), 3)
|
||||
self.log("ptop %r not in %s" % (ptop, allvols), 3)
|
||||
vn = self.asrv.vfs.all_aps[0][1]
|
||||
|
||||
self.q.put((abspath, tpath, fmt, vn))
|
||||
self.log("conv {} :{} \033[0m{}".format(tpath, fmt, abspath), c=6)
|
||||
self.log("conv %r :%s \033[0m%r" % (tpath, fmt, abspath), 6)
|
||||
|
||||
while not self.stopping:
|
||||
with self.mutex:
|
||||
@@ -334,9 +337,10 @@ class ThumbSrv(object):
|
||||
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")
|
||||
tex = tpath.rsplit(".", 1)[-1]
|
||||
want_mp3 = tex == "mp3"
|
||||
want_opus = tex in ("opus", "owa", "caf")
|
||||
want_png = tex == "png"
|
||||
want_au = want_mp3 or want_opus
|
||||
for lib in self.args.th_dec:
|
||||
can_au = lib == "ff" and (
|
||||
@@ -375,8 +379,8 @@ class ThumbSrv(object):
|
||||
fun(ap_unpk, ttpath, fmt, vn)
|
||||
break
|
||||
except Exception as ex:
|
||||
msg = "{} could not create thumbnail of {}\n{}"
|
||||
msg = msg.format(fun.__name__, abspath, min_ex())
|
||||
msg = "%s could not create thumbnail of %r\n%s"
|
||||
msg = msg % (fun.__name__, abspath, min_ex())
|
||||
c: Union[str, int] = 1 if "<Signals.SIG" in msg else "90"
|
||||
self.log(msg, c)
|
||||
if getattr(ex, "returncode", 0) != 321:
|
||||
@@ -754,47 +758,102 @@ class ThumbSrv(object):
|
||||
if "ac" not in tags:
|
||||
raise Exception("not audio")
|
||||
|
||||
sq = "%dk" % (self.args.q_opus,)
|
||||
bq = sq.encode("ascii")
|
||||
if tags["ac"][1] == "opus":
|
||||
enc = "-c:a copy"
|
||||
else:
|
||||
enc = "-c:a libopus -b:a " + sq
|
||||
|
||||
fun = self._conv_caf if fmt == "caf" else self._conv_owa
|
||||
|
||||
fun(abspath, tpath, tags, rawtags, enc, bq, vn)
|
||||
|
||||
def _conv_owa(
|
||||
self,
|
||||
abspath: str,
|
||||
tpath: str,
|
||||
tags: dict[str, tuple[int, Any]],
|
||||
rawtags: dict[str, list[Any]],
|
||||
enc: str,
|
||||
bq: bytes,
|
||||
vn: VFS,
|
||||
) -> None:
|
||||
if tpath.endswith(".owa"):
|
||||
container = b"webm"
|
||||
tagset = [b"-map_metadata", b"-1"]
|
||||
else:
|
||||
container = b"opus"
|
||||
tagset = self.big_tags(rawtags)
|
||||
|
||||
self.log("conv2 %s [%s]" % (container, enc), 6)
|
||||
benc = enc.encode("ascii").split(b" ")
|
||||
|
||||
# fmt: off
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
b"-i", fsenc(abspath),
|
||||
] + tagset + [
|
||||
b"-map", b"0:a:0",
|
||||
] + benc + [
|
||||
b"-f", container,
|
||||
fsenc(tpath)
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, oom=300)
|
||||
|
||||
def _conv_caf(
|
||||
self,
|
||||
abspath: str,
|
||||
tpath: str,
|
||||
tags: dict[str, tuple[int, Any]],
|
||||
rawtags: dict[str, list[Any]],
|
||||
enc: str,
|
||||
bq: bytes,
|
||||
vn: VFS,
|
||||
) -> None:
|
||||
tmp_opus = tpath + ".opus"
|
||||
try:
|
||||
wunlink(self.log, tmp_opus, vn.flags)
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
dur = tags[".dur"][1]
|
||||
except:
|
||||
dur = 0
|
||||
|
||||
src_opus = abspath.lower().endswith(".opus") or tags["ac"][1] == "opus"
|
||||
want_caf = tpath.endswith(".caf")
|
||||
tmp_opus = tpath
|
||||
if want_caf:
|
||||
tmp_opus = tpath + ".opus"
|
||||
try:
|
||||
wunlink(self.log, tmp_opus, vn.flags)
|
||||
except:
|
||||
pass
|
||||
self.log("conv2 caf-tmp [%s]" % (enc,), 6)
|
||||
benc = enc.encode("ascii").split(b" ")
|
||||
|
||||
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
|
||||
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"-c:a", b"libopus",
|
||||
b"-b:a", bq,
|
||||
fsenc(tmp_opus)
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, oom=300)
|
||||
# fmt: off
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
b"-i", fsenc(abspath),
|
||||
b"-map_metadata", b"-1",
|
||||
b"-map", b"0:a:0",
|
||||
] + benc + [
|
||||
b"-f", b"opus",
|
||||
fsenc(tmp_opus)
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, oom=300)
|
||||
|
||||
# iOS fails to play some "insufficiently complex" files
|
||||
# (average file shorter than 8 seconds), so of course we
|
||||
# fix that by mixing in some inaudible pink noise :^)
|
||||
# 6.3 sec seems like the cutoff so lets do 7, and
|
||||
# 7 sec of psyqui-musou.opus @ 3:50 is 174 KiB
|
||||
if want_caf and (dur < 20 or bos.path.getsize(caf_src) < 256 * 1024):
|
||||
sz = bos.path.getsize(tmp_opus)
|
||||
if dur < 20 or sz < 256 * 1024:
|
||||
zs = bq.decode("ascii")
|
||||
self.log("conv2 caf-transcode; dur=%d sz=%d q=%s" % (dur, sz, zs), 6)
|
||||
# fmt: off
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
@@ -813,15 +872,16 @@ class ThumbSrv(object):
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, oom=300)
|
||||
|
||||
elif want_caf:
|
||||
else:
|
||||
# simple remux should be safe
|
||||
self.log("conv2 caf-remux; dur=%d sz=%d" % (dur, sz), 6)
|
||||
# fmt: off
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
b"-i", fsenc(abspath if src_opus else tmp_opus),
|
||||
b"-i", fsenc(tmp_opus),
|
||||
b"-map_metadata", b"-1",
|
||||
b"-map", b"0:a:0",
|
||||
b"-c:a", b"copy",
|
||||
@@ -831,11 +891,10 @@ class ThumbSrv(object):
|
||||
# fmt: on
|
||||
self._run_ff(cmd, vn, oom=300)
|
||||
|
||||
if tmp_opus != tpath:
|
||||
try:
|
||||
wunlink(self.log, tmp_opus, vn.flags)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
wunlink(self.log, tmp_opus, vn.flags)
|
||||
except:
|
||||
pass
|
||||
|
||||
def big_tags(self, raw_tags: dict[str, list[str]]) -> list[bytes]:
|
||||
ret = []
|
||||
@@ -891,7 +950,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", "mp3"]
|
||||
exts = EXTS_TH if cat == "th" else EXTS_AC
|
||||
maxage = getattr(self.args, cat + "_maxage")
|
||||
now = time.time()
|
||||
prev_b64 = None
|
||||
|
||||
@@ -70,6 +70,9 @@ class U2idx(object):
|
||||
self.log_func("u2idx", msg, c)
|
||||
|
||||
def shutdown(self) -> None:
|
||||
if not HAVE_SQLITE3:
|
||||
return
|
||||
|
||||
for cur in self.cur.values():
|
||||
db = cur.connection
|
||||
try:
|
||||
@@ -80,6 +83,12 @@ class U2idx(object):
|
||||
cur.close()
|
||||
db.close()
|
||||
|
||||
for cur in (self.mem_cur, self.sh_cur):
|
||||
if cur:
|
||||
db = cur.connection
|
||||
cur.close()
|
||||
db.close()
|
||||
|
||||
def fsearch(
|
||||
self, uname: str, vols: list[VFS], body: dict[str, Any]
|
||||
) -> list[dict[str, Any]]:
|
||||
@@ -127,7 +136,7 @@ class U2idx(object):
|
||||
ptop = vn.realpath
|
||||
histpath = self.asrv.vfs.histtab.get(ptop)
|
||||
if not histpath:
|
||||
self.log("no histpath for [{}]".format(ptop))
|
||||
self.log("no histpath for %r" % (ptop,))
|
||||
return None
|
||||
|
||||
db_path = os.path.join(histpath, "up2k.db")
|
||||
@@ -142,7 +151,7 @@ class U2idx(object):
|
||||
db = sqlite3.connect(uri, timeout=2, uri=True, check_same_thread=False)
|
||||
cur = db.cursor()
|
||||
cur.execute('pragma table_info("up")').fetchone()
|
||||
self.log("ro: {}".format(db_path))
|
||||
self.log("ro: %r" % (db_path,))
|
||||
except:
|
||||
self.log("could not open read-only: {}\n{}".format(uri, min_ex()))
|
||||
# may not fail until the pragma so unset it
|
||||
@@ -152,7 +161,7 @@ class U2idx(object):
|
||||
# on windows, this steals the write-lock from up2k.deferred_init --
|
||||
# seen on win 10.0.17763.2686, py 3.10.4, sqlite 3.37.2
|
||||
cur = sqlite3.connect(db_path, timeout=2, check_same_thread=False).cursor()
|
||||
self.log("opened {}".format(db_path))
|
||||
self.log("opened %r" % (db_path,))
|
||||
|
||||
self.cur[ptop] = cur
|
||||
return cur
|
||||
@@ -315,7 +324,8 @@ class U2idx(object):
|
||||
sort: bool,
|
||||
lim: int,
|
||||
) -> tuple[list[dict[str, Any]], list[str], bool]:
|
||||
if self.args.srch_dbg:
|
||||
dbg = self.args.srch_dbg
|
||||
if dbg:
|
||||
t = "searching across all %s volumes in which the user has 'r' (full read access):\n %s"
|
||||
zs = "\n ".join(["/%s = %s" % (x.vpath, x.realpath) for x in vols])
|
||||
self.log(t % (len(vols), zs), 5)
|
||||
@@ -358,14 +368,14 @@ class U2idx(object):
|
||||
if not cur:
|
||||
continue
|
||||
|
||||
excl = []
|
||||
for vp2 in self.asrv.vfs.all_vols.keys():
|
||||
if vp2.startswith((vtop + "/").lstrip("/")) and vtop != vp2:
|
||||
excl.append(vp2[len(vtop) :].lstrip("/"))
|
||||
dots = flags.get("dotsrch") and uname in vol.axs.udot
|
||||
zs = "srch_re_dots" if dots else "srch_re_nodot"
|
||||
rex: re.Pattern = flags.get(zs) # type: ignore
|
||||
|
||||
if self.args.srch_dbg:
|
||||
t = "searching in volume /%s (%s), excludelist %s"
|
||||
self.log(t % (vtop, ptop, excl), 5)
|
||||
if dbg:
|
||||
t = "searching in volume /%s (%s), excluding %s"
|
||||
self.log(t % (vtop, ptop, rex.pattern), 5)
|
||||
rex_cfg: Optional[re.Pattern] = flags.get("srch_excl")
|
||||
|
||||
self.active_cur = cur
|
||||
|
||||
@@ -378,7 +388,6 @@ class U2idx(object):
|
||||
|
||||
sret = []
|
||||
fk = flags.get("fk")
|
||||
dots = flags.get("dotsrch") and uname in vol.axs.udot
|
||||
fk_alg = 2 if "fka" in flags else 1
|
||||
c = cur.execute(uq, tuple(vuv))
|
||||
for hit in c:
|
||||
@@ -387,20 +396,23 @@ class U2idx(object):
|
||||
if rd.startswith("//") or fn.startswith("//"):
|
||||
rd, fn = s3dec(rd, fn)
|
||||
|
||||
if rd in excl or any([x for x in excl if rd.startswith(x + "/")]):
|
||||
if self.args.srch_dbg:
|
||||
zs = vjoin(vjoin(vtop, rd), fn)
|
||||
t = "database inconsistency in volume '/%s'; ignoring: %s"
|
||||
self.log(t % (vtop, zs), 1)
|
||||
vp = vjoin(vjoin(vtop, rd), fn)
|
||||
|
||||
if vp in seen_rps:
|
||||
continue
|
||||
|
||||
rp = quotep("/".join([x for x in [vtop, rd, fn] if x]))
|
||||
if not dots and "/." in ("/" + rp):
|
||||
continue
|
||||
|
||||
if rp in seen_rps:
|
||||
if rex.search(vp):
|
||||
if dbg:
|
||||
if rex_cfg and rex_cfg.search(vp): # type: ignore
|
||||
self.log("filtered by srch_excl: %s" % (vp,), 6)
|
||||
elif not dots and "/." in ("/" + vp):
|
||||
pass
|
||||
else:
|
||||
t = "database inconsistency in volume '/%s'; ignoring: %s"
|
||||
self.log(t % (vtop, vp), 1)
|
||||
continue
|
||||
|
||||
rp = quotep(vp)
|
||||
if not fk:
|
||||
suf = ""
|
||||
else:
|
||||
@@ -422,7 +434,7 @@ class U2idx(object):
|
||||
if lim < 0:
|
||||
break
|
||||
|
||||
if self.args.srch_dbg:
|
||||
if dbg:
|
||||
t = "in volume '/%s': hit: %s"
|
||||
self.log(t % (vtop, rp), 5)
|
||||
|
||||
@@ -452,7 +464,7 @@ class U2idx(object):
|
||||
ret.extend(sret)
|
||||
# print("[{}] {}".format(ptop, sret))
|
||||
|
||||
if self.args.srch_dbg:
|
||||
if dbg:
|
||||
t = "in volume '/%s': got %d hits, %d total so far"
|
||||
self.log(t % (vtop, len(sret), len(ret)), 5)
|
||||
|
||||
|
||||
@@ -88,16 +88,23 @@ if TYPE_CHECKING:
|
||||
zsg = "avif,avifs,bmp,gif,heic,heics,heif,heifs,ico,j2p,j2k,jp2,jpeg,jpg,jpx,png,tga,tif,tiff,webp"
|
||||
CV_EXTS = set(zsg.split(","))
|
||||
|
||||
zsg = "nohash noidx xdev xvol"
|
||||
VF_AFFECTS_INDEXING = set(zsg.split(" "))
|
||||
|
||||
|
||||
SBUSY = "cannot receive uploads right now;\nserver busy with %s.\nPlease wait; the client will retry..."
|
||||
|
||||
HINT_HISTPATH = "you could try moving the database to another location (preferably an SSD or NVME drive) using either the --hist argument (global option for all volumes), or the hist volflag (just for this volume)"
|
||||
|
||||
|
||||
NULLSTAT = os.stat_result((0, -1, -1, 0, 0, 0, 0, 0, 0, 0))
|
||||
|
||||
|
||||
class Dbw(object):
|
||||
def __init__(self, c: "sqlite3.Cursor", n: int, t: float) -> None:
|
||||
def __init__(self, c: "sqlite3.Cursor", n: int, nf: int, t: float) -> None:
|
||||
self.c = c
|
||||
self.n = n
|
||||
self.nf = nf
|
||||
self.t = t
|
||||
|
||||
|
||||
@@ -787,8 +794,8 @@ class Up2k(object):
|
||||
if ccd != cd:
|
||||
continue
|
||||
|
||||
self.log("xiu: {}# {}".format(len(wrfs), cmd))
|
||||
runihook(self.log, cmd, vol, ups)
|
||||
self.log("xiu: %d# %r" % (len(wrfs), cmd))
|
||||
runihook(self.log, self.args.hook_v, cmd, vol, ups)
|
||||
|
||||
def _vis_job_progress(self, job: dict[str, Any]) -> str:
|
||||
perc = 100 - (len(job["need"]) * 100.0 / (len(job["hash"]) or 1))
|
||||
@@ -849,9 +856,9 @@ class Up2k(object):
|
||||
self.iacct = self.asrv.iacct
|
||||
self.grps = self.asrv.grps
|
||||
|
||||
have_e2d = self.args.idp_h_usr or self.args.chpw or self.args.shr
|
||||
vols = list(all_vols.values())
|
||||
t0 = time.time()
|
||||
have_e2d = False
|
||||
|
||||
if self.no_expr_idx:
|
||||
modified = False
|
||||
@@ -1053,7 +1060,7 @@ class Up2k(object):
|
||||
"""mutex(main,reg) me"""
|
||||
histpath = self.vfs.histtab.get(ptop)
|
||||
if not histpath:
|
||||
self.log("no histpath for [{}]".format(ptop))
|
||||
self.log("no histpath for %r" % (ptop,))
|
||||
return None
|
||||
|
||||
db_path = os.path.join(histpath, "up2k.db")
|
||||
@@ -1074,7 +1081,8 @@ 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", "mv_re_t", "mv_re_r"))
|
||||
zs = "html_head mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot"
|
||||
fx = set(zs.split())
|
||||
fd = vf_bmap()
|
||||
fd.update(vf_cmap())
|
||||
fd.update(vf_vmap())
|
||||
@@ -1111,6 +1119,7 @@ class Up2k(object):
|
||||
reg = {}
|
||||
drp = None
|
||||
emptylist = []
|
||||
dotpart = "." if self.args.dotpart else ""
|
||||
snap = os.path.join(histpath, "up2k.snap")
|
||||
if bos.path.exists(snap):
|
||||
with gzip.GzipFile(snap, "rb") as f:
|
||||
@@ -1123,6 +1132,8 @@ class Up2k(object):
|
||||
except:
|
||||
pass
|
||||
|
||||
reg = reg2 # diff-golf
|
||||
|
||||
if reg2 and "dwrk" not in reg2[next(iter(reg2))]:
|
||||
for job in reg2.values():
|
||||
job["dwrk"] = job["wark"]
|
||||
@@ -1130,7 +1141,8 @@ class Up2k(object):
|
||||
rm = []
|
||||
for k, job in reg2.items():
|
||||
job["ptop"] = ptop
|
||||
if "done" in job:
|
||||
is_done = "done" in job
|
||||
if is_done:
|
||||
job["need"] = job["hash"] = emptylist
|
||||
else:
|
||||
if "need" not in job:
|
||||
@@ -1138,22 +1150,32 @@ class Up2k(object):
|
||||
if "hash" not in job:
|
||||
job["hash"] = []
|
||||
|
||||
fp = djoin(ptop, job["prel"], job["name"])
|
||||
if is_done:
|
||||
fp = djoin(ptop, job["prel"], job["name"])
|
||||
else:
|
||||
fp = djoin(ptop, job["prel"], dotpart + job["name"] + ".PARTIAL")
|
||||
|
||||
if bos.path.exists(fp):
|
||||
reg[k] = job
|
||||
if "done" in job:
|
||||
if is_done:
|
||||
continue
|
||||
job["poke"] = time.time()
|
||||
job["busy"] = {}
|
||||
else:
|
||||
self.log("ign deleted file in snap: [{}]".format(fp))
|
||||
self.log("ign deleted file in snap: %r" % (fp,))
|
||||
if not n4g:
|
||||
rm.append(k)
|
||||
continue
|
||||
|
||||
for x in rm:
|
||||
del reg2[x]
|
||||
|
||||
# optimize pre-1.15.4 entries
|
||||
if next((x for x in reg.values() if "done" in x and "poke" in x), None):
|
||||
zsl = "host tnam busy sprs poke t0c".split()
|
||||
for job in reg.values():
|
||||
if "done" in job:
|
||||
for k in zsl:
|
||||
job.pop(k, None)
|
||||
|
||||
if drp is None:
|
||||
drp = [k for k, v in reg.items() if not v["need"]]
|
||||
else:
|
||||
@@ -1231,10 +1253,17 @@ class Up2k(object):
|
||||
def _verify_db_cache(self, cur: "sqlite3.Cursor", vpath: str) -> None:
|
||||
# check if list of intersecting volumes changed since last use; drop caches if so
|
||||
prefix = (vpath + "/").lstrip("/")
|
||||
zsl = [x for x in self.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()
|
||||
vps = [x for x in self.vfs.all_vols if x.startswith(prefix)]
|
||||
vps.sort()
|
||||
seed = [x[len(prefix) :] for x in vps]
|
||||
|
||||
# also consider volflags which affect indexing
|
||||
for vp in vps:
|
||||
vf = self.vfs.all_vols[vp].flags
|
||||
vf = {k: v for k, v in vf.items() if k in VF_AFFECTS_INDEXING}
|
||||
seed.append(str(sorted(vf.items())))
|
||||
|
||||
zb = hashlib.sha1("\n".join(seed).encode("utf-8", "replace")).digest()
|
||||
vcfg = ub64enc(zb[:18]).decode("ascii")
|
||||
|
||||
c = cur.execute("select v from kv where k = 'volcfg'")
|
||||
@@ -1267,7 +1296,7 @@ class Up2k(object):
|
||||
assert reg and self.pp # !rm
|
||||
cur, db_path = reg
|
||||
|
||||
db = Dbw(cur, 0, time.time())
|
||||
db = Dbw(cur, 0, 0, time.time())
|
||||
self.pp.n = next(db.c.execute("select count(w) from up"))[0]
|
||||
|
||||
excl = [
|
||||
@@ -1319,7 +1348,7 @@ class Up2k(object):
|
||||
self.hub.log_stacks()
|
||||
|
||||
if db.n:
|
||||
self.log("commit {} new files".format(db.n))
|
||||
self.log("commit %d new files; %d updates" % (db.nf, db.n))
|
||||
|
||||
if self.args.no_dhash:
|
||||
if db.c.execute("select d from dh").fetchone():
|
||||
@@ -1371,12 +1400,12 @@ class Up2k(object):
|
||||
xvol: bool,
|
||||
) -> tuple[int, int, int]:
|
||||
if xvol and not rcdir.startswith(top):
|
||||
self.log("skip xvol: [{}] -> [{}]".format(cdir, rcdir), 6)
|
||||
self.log("skip xvol: %r -> %r" % (cdir, rcdir), 6)
|
||||
return 0, 0, 0
|
||||
|
||||
if rcdir in seen:
|
||||
t = "bailing from symlink loop,\n prev: {}\n curr: {}\n from: {}"
|
||||
self.log(t.format(seen[-1], rcdir, cdir), 3)
|
||||
t = "bailing from symlink loop,\n prev: %r\n curr: %r\n from: %r"
|
||||
self.log(t % (seen[-1], rcdir, cdir), 3)
|
||||
return 0, 0, 0
|
||||
|
||||
# total-files-added, total-num-files, recursive-size
|
||||
@@ -1432,7 +1461,7 @@ class Up2k(object):
|
||||
and inf.st_dev != dev
|
||||
and not (ANYWIN and bos.stat(rap).st_dev == dev)
|
||||
):
|
||||
self.log("skip xdev {}->{}: {}".format(dev, inf.st_dev, abspath), 6)
|
||||
self.log("skip xdev %s->%s: %r" % (dev, inf.st_dev, abspath), 6)
|
||||
continue
|
||||
if abspath in excl or rap in excl:
|
||||
unreg.append(rp)
|
||||
@@ -1461,10 +1490,10 @@ class Up2k(object):
|
||||
tnf += i2
|
||||
rsz += i3
|
||||
except:
|
||||
t = "failed to index subdir [{}]:\n{}"
|
||||
self.log(t.format(abspath, min_ex()), c=1)
|
||||
t = "failed to index subdir %r:\n%s"
|
||||
self.log(t % (abspath, min_ex()), 1)
|
||||
elif not stat.S_ISREG(inf.st_mode):
|
||||
self.log("skip type-0%o file [%s]" % (inf.st_mode, abspath))
|
||||
self.log("skip type-0%o file %r" % (inf.st_mode, abspath))
|
||||
else:
|
||||
# self.log("file: {}".format(abspath))
|
||||
if rp.endswith(".PARTIAL") and time.time() - lmod < 60:
|
||||
@@ -1547,7 +1576,7 @@ class Up2k(object):
|
||||
db.c.execute("insert into cv values (?,?,?)", (crd, cdn, cv))
|
||||
db.n += 1
|
||||
except Exception as ex:
|
||||
self.log("cover {}/{} failed: {}".format(rd, cv, ex), 6)
|
||||
self.log("cover %r/%r failed: %s" % (rd, cv, ex), 6)
|
||||
|
||||
seen_files = set([x[2] for x in files]) # for dropcheck
|
||||
for sz, lmod, fn in files:
|
||||
@@ -1569,9 +1598,9 @@ class Up2k(object):
|
||||
self.pp.n -= 1
|
||||
dw, dts, dsz, ip, at = in_db[0]
|
||||
if len(in_db) > 1:
|
||||
t = "WARN: multiple entries: [{}] => [{}] |{}|\n{}"
|
||||
t = "WARN: multiple entries: %r => %r |%d|\n%r"
|
||||
rep_db = "\n".join([repr(x) for x in in_db])
|
||||
self.log(t.format(top, rp, len(in_db), rep_db))
|
||||
self.log(t % (top, rp, len(in_db), rep_db))
|
||||
dts = -1
|
||||
|
||||
if fat32 and abs(dts - lmod) == 1:
|
||||
@@ -1580,10 +1609,8 @@ class Up2k(object):
|
||||
if dts == lmod and dsz == sz and (nohash or dw[0] != "#" or not sz):
|
||||
continue
|
||||
|
||||
t = "reindex [{}] => [{}] mtime({}/{}) size({}/{})".format(
|
||||
top, rp, dts, lmod, dsz, sz
|
||||
)
|
||||
self.log(t)
|
||||
t = "reindex %r => %r mtime(%s/%s) size(%s/%s)"
|
||||
self.log(t % (top, rp, dts, lmod, dsz, sz))
|
||||
self.db_rm(db.c, rd, fn, 0)
|
||||
tfa += 1
|
||||
db.n += 1
|
||||
@@ -1599,14 +1626,14 @@ class Up2k(object):
|
||||
wark = up2k_wark_from_metadata(self.salt, sz, lmod, rd, fn)
|
||||
else:
|
||||
if sz > 1024 * 1024:
|
||||
self.log("file: {}".format(abspath))
|
||||
self.log("file: %r" % (abspath,))
|
||||
|
||||
try:
|
||||
hashes, _ = self._hashlist_from_file(
|
||||
abspath, "a{}, ".format(self.pp.n)
|
||||
)
|
||||
except Exception as ex:
|
||||
self.log("hash: {} @ [{}]".format(repr(ex), abspath))
|
||||
self.log("hash: %r @ %r" % (ex, abspath))
|
||||
continue
|
||||
|
||||
if not hashes:
|
||||
@@ -1621,12 +1648,13 @@ class Up2k(object):
|
||||
# skip upload hooks by not providing vflags
|
||||
self.db_add(db.c, {}, rd, fn, lmod, sz, "", "", wark, wark, "", "", ip, at)
|
||||
db.n += 1
|
||||
db.nf += 1
|
||||
tfa += 1
|
||||
td = time.time() - db.t
|
||||
if db.n >= 4096 or td >= 60:
|
||||
self.log("commit {} new files".format(db.n))
|
||||
self.log("commit %d new files; %d updates" % (db.nf, db.n))
|
||||
db.c.connection.commit()
|
||||
db.n = 0
|
||||
db.n = db.nf = 0
|
||||
db.t = time.time()
|
||||
|
||||
if not self.args.no_dhash:
|
||||
@@ -1639,7 +1667,7 @@ class Up2k(object):
|
||||
# drop shadowed folders
|
||||
for sh_rd in unreg:
|
||||
n = 0
|
||||
q = "select count(w) from up where (rd=? or rd like ?||'/%') and +at == 0"
|
||||
q = "select count(w) from up where (rd=? or rd like ?||'/%')"
|
||||
for sh_erd in [sh_rd, "//" + w8b64enc(sh_rd)]:
|
||||
try:
|
||||
erd_erd = (sh_erd, sh_erd)
|
||||
@@ -1651,13 +1679,13 @@ class Up2k(object):
|
||||
assert erd_erd # type: ignore # !rm
|
||||
|
||||
if n:
|
||||
t = "forgetting {} shadowed autoindexed files in [{}] > [{}]"
|
||||
self.log(t.format(n, top, sh_rd))
|
||||
t = "forgetting %d shadowed autoindexed files in %r > %r"
|
||||
self.log(t % (n, top, sh_rd))
|
||||
|
||||
q = "delete from dh where (d = ? or d like ?||'/%')"
|
||||
db.c.execute(q, erd_erd)
|
||||
|
||||
q = "delete from up where (rd=? or rd like ?||'/%') and +at == 0"
|
||||
q = "delete from up where (rd=? or rd like ?||'/%')"
|
||||
db.c.execute(q, erd_erd)
|
||||
tfa += n
|
||||
|
||||
@@ -1849,7 +1877,7 @@ class Up2k(object):
|
||||
stl = bos.lstat(abspath)
|
||||
st = bos.stat(abspath) if stat.S_ISLNK(stl.st_mode) else stl
|
||||
except Exception as ex:
|
||||
self.log("missing file: %s" % (abspath,), 3)
|
||||
self.log("missing file: %r" % (abspath,), 3)
|
||||
f404.append((drd, dfn, w))
|
||||
continue
|
||||
|
||||
@@ -1860,12 +1888,12 @@ class Up2k(object):
|
||||
w2 = up2k_wark_from_metadata(self.salt, sz2, mt2, rd, fn)
|
||||
else:
|
||||
if sz2 > 1024 * 1024 * 32:
|
||||
self.log("file: {}".format(abspath))
|
||||
self.log("file: %r" % (abspath,))
|
||||
|
||||
try:
|
||||
hashes, _ = self._hashlist_from_file(abspath, pf)
|
||||
except Exception as ex:
|
||||
self.log("hash: {} @ [{}]".format(repr(ex), abspath))
|
||||
self.log("hash: %r @ %r" % (ex, abspath))
|
||||
continue
|
||||
|
||||
if not hashes:
|
||||
@@ -1885,9 +1913,8 @@ class Up2k(object):
|
||||
|
||||
rewark.append((drd, dfn, w2, sz2, mt2))
|
||||
|
||||
t = "hash mismatch: {}\n db: {} ({} byte, {})\n fs: {} ({} byte, {})"
|
||||
t = t.format(abspath, w, sz, mt, w2, sz2, mt2)
|
||||
self.log(t, 1)
|
||||
t = "hash mismatch: %r\n db: %s (%d byte, %d)\n fs: %s (%d byte, %d)"
|
||||
self.log(t % (abspath, w, sz, mt, w2, sz2, mt2), 1)
|
||||
|
||||
if e2vp and (rewark or f404):
|
||||
self.hub.retcode = 1
|
||||
@@ -2435,7 +2462,7 @@ class Up2k(object):
|
||||
q.task_done()
|
||||
|
||||
def _log_tag_err(self, parser: Any, abspath: str, ex: Any) -> None:
|
||||
msg = "{} failed to read tags from {}:\n{}".format(parser, abspath, ex)
|
||||
msg = "%s failed to read tags from %r:\n%s" % (parser, abspath, ex)
|
||||
self.log(msg.lstrip(), c=1 if "<Signals.SIG" in msg else 3)
|
||||
|
||||
def _tagscan_file(
|
||||
@@ -2929,7 +2956,7 @@ class Up2k(object):
|
||||
raise Exception()
|
||||
except Exception as ex:
|
||||
if n4g:
|
||||
st = os.stat_result((0, -1, -1, 0, 0, 0, 0, 0, 0, 0))
|
||||
st = NULLSTAT
|
||||
else:
|
||||
lost.append((cur, dp_dir, dp_fn))
|
||||
continue
|
||||
@@ -2975,11 +3002,11 @@ class Up2k(object):
|
||||
job = rj
|
||||
break
|
||||
else:
|
||||
self.log("asserting contents of %s" % (orig_ap,))
|
||||
self.log("asserting contents of %r" % (orig_ap,))
|
||||
hashes2, st = self._hashlist_from_file(orig_ap)
|
||||
wark2 = up2k_wark_from_hashlist(self.salt, st.st_size, hashes2)
|
||||
if dwark != wark2:
|
||||
t = "will not dedup (fs index desync): fs=%s, db=%s, file: %s\n%s"
|
||||
t = "will not dedup (fs index desync): fs=%s, db=%s, file: %r\n%s"
|
||||
self.log(t % (wark2, dwark, orig_ap, rj))
|
||||
lost.append(dupe[3:])
|
||||
continue
|
||||
@@ -2991,14 +3018,14 @@ class Up2k(object):
|
||||
if wark in reg:
|
||||
del reg[wark]
|
||||
job["hash"] = job["need"] = []
|
||||
job["done"] = True
|
||||
job["done"] = 1
|
||||
job["busy"] = {}
|
||||
|
||||
if lost:
|
||||
c2 = None
|
||||
for cur, dp_dir, dp_fn in lost:
|
||||
t = "forgetting desynced db entry: /{}"
|
||||
self.log(t.format(vjoin(vjoin(vfs.vpath, dp_dir), dp_fn)))
|
||||
t = "forgetting desynced db entry: %r"
|
||||
self.log(t % ("/" + vjoin(vjoin(vfs.vpath, dp_dir), dp_fn)))
|
||||
self.db_rm(cur, dp_dir, dp_fn, cj["size"])
|
||||
if c2 and c2 != cur:
|
||||
c2.connection.commit()
|
||||
@@ -3027,8 +3054,8 @@ class Up2k(object):
|
||||
except:
|
||||
# missing; restart
|
||||
if not self.args.nw and not n4g:
|
||||
t = "forgetting deleted partial upload at {}"
|
||||
self.log(t.format(path))
|
||||
t = "forgetting deleted partial upload at %r"
|
||||
self.log(t % (path,))
|
||||
del reg[wark]
|
||||
break
|
||||
|
||||
@@ -3039,19 +3066,25 @@ class Up2k(object):
|
||||
pass
|
||||
|
||||
elif st.st_size != rj["size"]:
|
||||
t = "will not dedup (fs index desync): {}, size fs={} db={}, mtime fs={} db={}, file: {}\n{}"
|
||||
t = t.format(
|
||||
wark, st.st_size, rj["size"], st.st_mtime, rj["lmod"], path, rj
|
||||
t = "will not dedup (fs index desync): %s, size fs=%d db=%d, mtime fs=%d db=%d, file: %r\n%s"
|
||||
t = t % (
|
||||
wark,
|
||||
st.st_size,
|
||||
rj["size"],
|
||||
st.st_mtime,
|
||||
rj["lmod"],
|
||||
path,
|
||||
rj,
|
||||
)
|
||||
self.log(t)
|
||||
del reg[wark]
|
||||
|
||||
elif inc_ap != orig_ap and not data_ok and "done" in reg[wark]:
|
||||
self.log("asserting contents of %s" % (orig_ap,))
|
||||
self.log("asserting contents of %r" % (orig_ap,))
|
||||
hashes2, _ = self._hashlist_from_file(orig_ap)
|
||||
wark2 = up2k_wark_from_hashlist(self.salt, st.st_size, hashes2)
|
||||
if wark != wark2:
|
||||
t = "will not dedup (fs index desync): fs=%s, idx=%s, file: %s\n%s"
|
||||
t = "will not dedup (fs index desync): fs=%s, idx=%s, file: %r\n%s"
|
||||
self.log(t % (wark2, wark, orig_ap, rj))
|
||||
del reg[wark]
|
||||
|
||||
@@ -3068,7 +3101,7 @@ class Up2k(object):
|
||||
vsrc = djoin(job["vtop"], job["prel"], job["name"])
|
||||
vsrc = vsrc.replace("\\", "/") # just for prints anyways
|
||||
if "done" not in job:
|
||||
self.log("unfinished:\n {0}\n {1}".format(src, dst))
|
||||
self.log("unfinished:\n %r\n %r" % (src, dst))
|
||||
err = "partial upload exists at a different location; please resume uploading here instead:\n"
|
||||
err += "/" + quotep(vsrc) + " "
|
||||
|
||||
@@ -3077,14 +3110,15 @@ class Up2k(object):
|
||||
if cur:
|
||||
dupe = (cj["prel"], cj["name"], cj["lmod"])
|
||||
try:
|
||||
self.dupesched[src].append(dupe)
|
||||
if dupe not in self.dupesched[src]:
|
||||
self.dupesched[src].append(dupe)
|
||||
except:
|
||||
self.dupesched[src] = [dupe]
|
||||
|
||||
raise Pebkac(422, err)
|
||||
|
||||
elif "nodupe" in vfs.flags:
|
||||
self.log("dupe-reject:\n {0}\n {1}".format(src, dst))
|
||||
self.log("dupe-reject:\n %r\n %r" % (src, dst))
|
||||
err = "upload rejected, file already exists:\n"
|
||||
err += "/" + quotep(vsrc) + " "
|
||||
raise Pebkac(409, err)
|
||||
@@ -3146,7 +3180,7 @@ class Up2k(object):
|
||||
"",
|
||||
)
|
||||
if not hr:
|
||||
t = "upload blocked by xbu server config: %s" % (dst,)
|
||||
t = "upload blocked by xbu server config: %r" % (dst,)
|
||||
self.log(t, 1)
|
||||
raise Pebkac(403, t)
|
||||
if hr.get("reloc"):
|
||||
@@ -3287,7 +3321,7 @@ class Up2k(object):
|
||||
times = (int(time.time()), int(cj["lmod"]))
|
||||
bos.utime(ap, times, False)
|
||||
|
||||
self.log("touched %s from %d to %d" % (ap, job["lmod"], cj["lmod"]))
|
||||
self.log("touched %r from %d to %d" % (ap, job["lmod"], cj["lmod"]))
|
||||
except Exception as ex:
|
||||
self.log("umod failed, %r" % (ex,), 3)
|
||||
|
||||
@@ -3302,7 +3336,7 @@ class Up2k(object):
|
||||
|
||||
fp = djoin(fdir, fname)
|
||||
if job.get("replace") and bos.path.exists(fp):
|
||||
self.log("replacing existing file at {}".format(fp))
|
||||
self.log("replacing existing file at %r" % (fp,))
|
||||
cur = None
|
||||
ptop = job["ptop"]
|
||||
vf = self.flags.get(ptop) or {}
|
||||
@@ -3345,9 +3379,9 @@ class Up2k(object):
|
||||
raise Exception(t % (src, fsrc, dst))
|
||||
|
||||
if verbose:
|
||||
t = "linking dupe:\n point-to: {0}\n link-loc: {1}"
|
||||
t = "linking dupe:\n point-to: {0!r}\n link-loc: {1!r}"
|
||||
if fsrc:
|
||||
t += "\n data-src: {2}"
|
||||
t += "\n data-src: {2!r}"
|
||||
self.log(t.format(src, dst, fsrc))
|
||||
|
||||
if self.args.nw:
|
||||
@@ -3417,7 +3451,7 @@ class Up2k(object):
|
||||
elif fsrc and bos.path.isfile(fsrc):
|
||||
csrc = fsrc
|
||||
else:
|
||||
t = "BUG: no valid sources to link from! orig(%s) fsrc(%s) link(%s)"
|
||||
t = "BUG: no valid sources to link from! orig(%r) fsrc(%r) link(%r)"
|
||||
self.log(t, 1)
|
||||
raise Exception(t % (src, fsrc, dst))
|
||||
shutil.copy2(fsenc(csrc), fsenc(dst))
|
||||
@@ -3595,15 +3629,12 @@ class Up2k(object):
|
||||
atomic_move(self.log, src, dst, vflags)
|
||||
|
||||
times = (int(time.time()), int(job["lmod"]))
|
||||
self.log(
|
||||
"no more chunks, setting times {} ({}) on {}".format(
|
||||
times, bos.path.getsize(dst), dst
|
||||
)
|
||||
)
|
||||
t = "no more chunks, setting times %s (%d) on %r"
|
||||
self.log(t % (times, bos.path.getsize(dst), dst))
|
||||
try:
|
||||
bos.utime(dst, times)
|
||||
except:
|
||||
self.log("failed to utime ({}, {})".format(dst, times))
|
||||
self.log("failed to utime (%r, %s)" % (dst, times))
|
||||
|
||||
zs = "prel name lmod size ptop vtop wark dwrk host user addr"
|
||||
z2 = [job[x] for x in zs.split()]
|
||||
@@ -3920,7 +3951,7 @@ class Up2k(object):
|
||||
if jrem == rem:
|
||||
if job["ptop"] != ptop:
|
||||
t = "job.ptop [%s] != vol.ptop [%s] ??"
|
||||
raise Exception(t % (job["ptop"] != ptop))
|
||||
raise Exception(t % (job["ptop"], ptop))
|
||||
partial = vn.canonical(vjoin(job["prel"], job["tnam"]))
|
||||
break
|
||||
if partial:
|
||||
@@ -3960,7 +3991,7 @@ class Up2k(object):
|
||||
if is_dir:
|
||||
# note: deletion inside shares would require a rewrite here;
|
||||
# shares necessitate get_dbv which is incompatible with walk
|
||||
g = vn0.walk("", rem0, [], uname, permsets, True, scandir, True)
|
||||
g = vn0.walk("", rem0, [], uname, permsets, 2, scandir, True)
|
||||
if unpost:
|
||||
raise Pebkac(400, "cannot unpost folders")
|
||||
elif stat.S_ISLNK(st.st_mode) or stat.S_ISREG(st.st_mode):
|
||||
@@ -3968,7 +3999,7 @@ class Up2k(object):
|
||||
vpath_dir = vsplit(vpath)[0]
|
||||
g = [(vn, voldir, vpath_dir, adir, [(fn, 0)], [], {})] # type: ignore
|
||||
else:
|
||||
self.log("rm: skip type-0%o file [%s]" % (st.st_mode, atop))
|
||||
self.log("rm: skip type-0%o file %r" % (st.st_mode, atop))
|
||||
return 0, [], []
|
||||
|
||||
xbd = vn.flags.get("xbd")
|
||||
@@ -3992,7 +4023,7 @@ class Up2k(object):
|
||||
|
||||
volpath = ("%s/%s" % (vrem, fn)).strip("/")
|
||||
vpath = ("%s/%s" % (dbv.vpath, volpath)).strip("/")
|
||||
self.log("rm %s\n %s" % (vpath, abspath))
|
||||
self.log("rm %r\n %r" % (vpath, abspath))
|
||||
if not unpost:
|
||||
# recursion-only sanchk
|
||||
_ = dbv.get(volpath, uname, *permsets[0])
|
||||
@@ -4015,8 +4046,8 @@ class Up2k(object):
|
||||
time.time(),
|
||||
"",
|
||||
):
|
||||
t = "delete blocked by xbd server config: {}"
|
||||
self.log(t.format(abspath), 1)
|
||||
t = "delete blocked by xbd server config: %r"
|
||||
self.log(t % (abspath,), 1)
|
||||
continue
|
||||
|
||||
n_files += 1
|
||||
@@ -4073,6 +4104,7 @@ class Up2k(object):
|
||||
raise Pebkac(400, "cp: cannot copy parent into subfolder")
|
||||
|
||||
svn, srem = self.vfs.get(svp, uname, True, False)
|
||||
dvn, drem = self.vfs.get(dvp, uname, False, True)
|
||||
svn_dbv, _ = svn.get_dbv(srem)
|
||||
sabs = svn.canonical(srem, False)
|
||||
curs: set["sqlite3.Cursor"] = set()
|
||||
@@ -4095,8 +4127,12 @@ class Up2k(object):
|
||||
permsets = [[True, False]]
|
||||
scandir = not self.args.no_scandir
|
||||
|
||||
# if user can see dotfiles in target volume, only include
|
||||
# dots from source vols where user also has the dot perm
|
||||
dots = 1 if uname in dvn.axs.udot else 2
|
||||
|
||||
# don't use svn_dbv; would skip subvols due to _ls `if not rem:`
|
||||
g = svn.walk("", srem, [], uname, permsets, True, scandir, True)
|
||||
g = svn.walk("", srem, [], uname, permsets, dots, scandir, True)
|
||||
with self.mutex:
|
||||
try:
|
||||
for dbv, vrem, _, atop, files, rd, vd in g:
|
||||
@@ -4105,7 +4141,7 @@ class Up2k(object):
|
||||
svpf = "/".join(x for x in [dbv.vpath, vrem, fn[0]] if x)
|
||||
if not svpf.startswith(svp + "/"): # assert
|
||||
self.log(min_ex(), 1)
|
||||
t = "cp: bug at %s, top %s%s"
|
||||
t = "cp: bug at %r, top %r%s"
|
||||
raise Pebkac(500, t % (svpf, svp, SEESLOG))
|
||||
|
||||
dvpf = dvp + svpf[len(svp) :]
|
||||
@@ -4145,7 +4181,7 @@ class Up2k(object):
|
||||
except:
|
||||
pass # broken symlink; keep as-is
|
||||
elif not stat.S_ISREG(st.st_mode):
|
||||
self.log("skipping type-0%o file [%s]" % (st.st_mode, sabs))
|
||||
self.log("skipping type-0%o file %r" % (st.st_mode, sabs))
|
||||
return ""
|
||||
else:
|
||||
is_link = False
|
||||
@@ -4173,7 +4209,7 @@ class Up2k(object):
|
||||
time.time(),
|
||||
"",
|
||||
):
|
||||
t = "copy blocked by xbr server config: {}".format(svp)
|
||||
t = "copy blocked by xbr server config: %r" % (svp,)
|
||||
self.log(t, 1)
|
||||
raise Pebkac(405, t)
|
||||
|
||||
@@ -4210,7 +4246,7 @@ class Up2k(object):
|
||||
)
|
||||
curs.add(c2)
|
||||
else:
|
||||
self.log("not found in src db: [{}]".format(svp))
|
||||
self.log("not found in src db: %r" % (svp,))
|
||||
|
||||
try:
|
||||
if is_link and st != stl:
|
||||
@@ -4227,7 +4263,7 @@ class Up2k(object):
|
||||
if ex.errno != errno.EXDEV:
|
||||
raise
|
||||
|
||||
self.log("using plain copy (%s):\n %s\n %s" % (ex.strerror, sabs, dabs))
|
||||
self.log("using plain copy (%s):\n %r\n %r" % (ex.strerror, sabs, dabs))
|
||||
b1, b2 = fsenc(sabs), fsenc(dabs)
|
||||
is_link = os.path.islink(b1) # due to _relink
|
||||
try:
|
||||
@@ -4308,13 +4344,13 @@ class Up2k(object):
|
||||
scandir = not self.args.no_scandir
|
||||
|
||||
# following symlinks is too scary
|
||||
g = svn.walk("", srem, [], uname, permsets, True, scandir, True)
|
||||
g = svn.walk("", srem, [], uname, permsets, 2, scandir, True)
|
||||
for dbv, vrem, _, atop, files, rd, vd in g:
|
||||
if dbv != jail:
|
||||
# fail early (prevent partial moves)
|
||||
raise Pebkac(400, "mv: source folder contains other volumes")
|
||||
|
||||
g = svn.walk("", srem, [], uname, permsets, True, scandir, True)
|
||||
g = svn.walk("", srem, [], uname, permsets, 2, scandir, True)
|
||||
with self.mutex:
|
||||
try:
|
||||
for dbv, vrem, _, atop, files, rd, vd in g:
|
||||
@@ -4327,7 +4363,7 @@ class Up2k(object):
|
||||
svpf = "/".join(x for x in [dbv.vpath, vrem, fn[0]] if x)
|
||||
if not svpf.startswith(svp + "/"): # assert
|
||||
self.log(min_ex(), 1)
|
||||
t = "mv: bug at %s, top %s%s"
|
||||
t = "mv: bug at %r, top %r%s"
|
||||
raise Pebkac(500, t % (svpf, svp, SEESLOG))
|
||||
|
||||
dvpf = dvp + svpf[len(svp) :]
|
||||
@@ -4346,7 +4382,7 @@ class Up2k(object):
|
||||
for ap in reversed(zsl):
|
||||
if not ap.startswith(sabs):
|
||||
self.log(min_ex(), 1)
|
||||
t = "mv_d: bug at %s, top %s%s"
|
||||
t = "mv_d: bug at %r, top %r%s"
|
||||
raise Pebkac(500, t % (ap, sabs, SEESLOG))
|
||||
|
||||
rem = ap[len(sabs) :].replace(os.sep, "/").lstrip("/")
|
||||
@@ -4417,7 +4453,7 @@ class Up2k(object):
|
||||
time.time(),
|
||||
"",
|
||||
):
|
||||
t = "move blocked by xbr server config: {}".format(svp)
|
||||
t = "move blocked by xbr server config: %r" % (svp,)
|
||||
self.log(t, 1)
|
||||
raise Pebkac(405, t)
|
||||
|
||||
@@ -4427,8 +4463,8 @@ class Up2k(object):
|
||||
|
||||
if is_dirlink:
|
||||
dlabs = absreal(sabs)
|
||||
t = "moving symlink from [{}] to [{}], target [{}]"
|
||||
self.log(t.format(sabs, dabs, dlabs))
|
||||
t = "moving symlink from %r to %r, target %r"
|
||||
self.log(t % (sabs, dabs, dlabs))
|
||||
mt = bos.path.getmtime(sabs, False)
|
||||
wunlink(self.log, sabs, svn.flags)
|
||||
self._symlink(dlabs, dabs, dvn.flags, False, lmod=mt)
|
||||
@@ -4500,7 +4536,7 @@ class Up2k(object):
|
||||
)
|
||||
curs.add(c2)
|
||||
else:
|
||||
self.log("not found in src db: [{}]".format(svp))
|
||||
self.log("not found in src db: %r" % (svp,))
|
||||
|
||||
try:
|
||||
if is_xvol and has_dupes:
|
||||
@@ -4521,7 +4557,7 @@ class Up2k(object):
|
||||
if ex.errno != errno.EXDEV:
|
||||
raise
|
||||
|
||||
self.log("using copy+delete (%s):\n %s\n %s" % (ex.strerror, sabs, dabs))
|
||||
self.log("using copy+delete (%s):\n %r\n %r" % (ex.strerror, sabs, dabs))
|
||||
b1, b2 = fsenc(sabs), fsenc(dabs)
|
||||
is_link = os.path.islink(b1) # due to _relink
|
||||
try:
|
||||
@@ -4629,7 +4665,7 @@ class Up2k(object):
|
||||
"""
|
||||
srd, sfn = vsplit(vrem)
|
||||
has_dupes = False
|
||||
self.log("forgetting {}".format(vrem))
|
||||
self.log("forgetting %r" % (vrem,))
|
||||
if wark and cur:
|
||||
self.log("found {} in db".format(wark))
|
||||
if drop_tags:
|
||||
@@ -4660,6 +4696,13 @@ class Up2k(object):
|
||||
t = "forgetting partial upload {} ({})"
|
||||
p = self._vis_job_progress(job)
|
||||
self.log(t.format(wark, p))
|
||||
|
||||
src = djoin(ptop, vrem)
|
||||
zi = len(self.dupesched.pop(src, []))
|
||||
if zi:
|
||||
t = "...and forgetting %d links in dupesched"
|
||||
self.log(t % (zi,))
|
||||
|
||||
assert wark
|
||||
del reg[wark]
|
||||
|
||||
@@ -4698,7 +4741,7 @@ class Up2k(object):
|
||||
dvrem = vjoin(rd, fn).strip("/")
|
||||
if ptop != sptop or srem != dvrem:
|
||||
dupes.append([ptop, dvrem])
|
||||
self.log("found {} dupe: [{}] {}".format(wark, ptop, dvrem))
|
||||
self.log("found %s dupe: %r %r" % (wark, ptop, dvrem))
|
||||
|
||||
if not dupes:
|
||||
return 0
|
||||
@@ -4711,7 +4754,7 @@ class Up2k(object):
|
||||
d = links if bos.path.islink(ap) else full
|
||||
d[ap] = (ptop, vp)
|
||||
except:
|
||||
self.log("relink: not found: [{}]".format(ap))
|
||||
self.log("relink: not found: %r" % (ap,))
|
||||
|
||||
# self.log("full:\n" + "\n".join(" {:90}: {}".format(*x) for x in full.items()))
|
||||
# self.log("links:\n" + "\n".join(" {:90}: {}".format(*x) for x in links.items()))
|
||||
@@ -4719,7 +4762,7 @@ class Up2k(object):
|
||||
# deleting final remaining full copy; swap it with a symlink
|
||||
slabs = list(sorted(links.keys()))[0]
|
||||
ptop, rem = links.pop(slabs)
|
||||
self.log("linkswap [{}] and [{}]".format(sabs, slabs))
|
||||
self.log("linkswap %r and %r" % (sabs, slabs))
|
||||
mt = bos.path.getmtime(slabs, False)
|
||||
flags = self.flags.get(ptop) or {}
|
||||
atomic_move(self.log, sabs, slabs, flags)
|
||||
@@ -4754,7 +4797,7 @@ class Up2k(object):
|
||||
|
||||
zs = absreal(alink)
|
||||
if ldst != zs:
|
||||
t = "relink because computed != actual destination:\n %s\n %s"
|
||||
t = "relink because computed != actual destination:\n %r\n %r"
|
||||
self.log(t % (ldst, zs), 3)
|
||||
ldst = zs
|
||||
faulty = True
|
||||
@@ -4769,7 +4812,7 @@ class Up2k(object):
|
||||
t = "relink because symlink verification failed: %s; %r"
|
||||
self.log(t % (ex, ex), 3)
|
||||
|
||||
self.log("relinking [%s] to [%s]" % (alink, dabs))
|
||||
self.log("relinking %r to %r" % (alink, dabs))
|
||||
flags = self.flags.get(parts[0]) or {}
|
||||
try:
|
||||
lmod = bos.path.getmtime(alink, False)
|
||||
@@ -4862,7 +4905,8 @@ class Up2k(object):
|
||||
except:
|
||||
pass
|
||||
|
||||
xbu = self.flags[job["ptop"]].get("xbu")
|
||||
vf = self.flags[job["ptop"]]
|
||||
xbu = vf.get("xbu")
|
||||
ap_chk = djoin(pdir, job["name"])
|
||||
vp_chk = djoin(job["vtop"], job["prel"], job["name"])
|
||||
if xbu:
|
||||
@@ -4884,7 +4928,7 @@ class Up2k(object):
|
||||
"",
|
||||
)
|
||||
if not hr:
|
||||
t = "upload blocked by xbu server config: {}".format(vp_chk)
|
||||
t = "upload blocked by xbu server config: %r" % (vp_chk,)
|
||||
self.log(t, 1)
|
||||
raise Pebkac(403, t)
|
||||
if hr.get("reloc"):
|
||||
@@ -4892,7 +4936,7 @@ class Up2k(object):
|
||||
if x:
|
||||
zvfs = vfs
|
||||
pdir, _, job["name"], (vfs, rem) = x
|
||||
job["vcfg"] = vfs.flags
|
||||
job["vcfg"] = vf = vfs.flags
|
||||
job["ptop"] = vfs.realpath
|
||||
job["vtop"] = vfs.vpath
|
||||
job["prel"] = rem
|
||||
@@ -4934,7 +4978,7 @@ class Up2k(object):
|
||||
try:
|
||||
sp.check_call(["fsutil", "sparse", "setflag", abspath])
|
||||
except:
|
||||
self.log("could not sparse [{}]".format(abspath), 3)
|
||||
self.log("could not sparse %r" % (abspath,), 3)
|
||||
relabel = True
|
||||
sprs = False
|
||||
|
||||
@@ -4942,8 +4986,13 @@ class Up2k(object):
|
||||
fs = self.fstab.get(pdir)
|
||||
if fs == "ok":
|
||||
pass
|
||||
elif "sparse" in self.flags[job["ptop"]]:
|
||||
t = "volflag 'sparse' is forcing use of sparse files for uploads to [%s]"
|
||||
elif "nosparse" in vf:
|
||||
t = "volflag 'nosparse' is preventing creation of sparse files for uploads to [%s]"
|
||||
self.log(t % (job["ptop"],))
|
||||
relabel = True
|
||||
sprs = False
|
||||
elif "sparse" in vf:
|
||||
t = "volflag 'sparse' is forcing creation of sparse files for uploads to [%s]"
|
||||
self.log(t % (job["ptop"],))
|
||||
relabel = True
|
||||
else:
|
||||
@@ -5121,7 +5170,7 @@ class Up2k(object):
|
||||
self._tag_file(cur, entags, wark, abspath, tags)
|
||||
cur.connection.commit()
|
||||
|
||||
self.log("tagged {} ({}+{})".format(abspath, ntags1, len(tags) - ntags1))
|
||||
self.log("tagged %r (%d+%d)" % (abspath, ntags1, len(tags) - ntags1))
|
||||
|
||||
def _hasher(self) -> None:
|
||||
with self.hashq_mutex:
|
||||
@@ -5140,7 +5189,7 @@ class Up2k(object):
|
||||
if not self._hash_t(task) and self.stop:
|
||||
return
|
||||
except Exception as ex:
|
||||
self.log("failed to hash %s: %s" % (task, ex), 1)
|
||||
self.log("failed to hash %r: %s" % (task, ex), 1)
|
||||
|
||||
def _hash_t(
|
||||
self, task: tuple[str, str, dict[str, Any], str, str, str, float, str, bool]
|
||||
@@ -5152,7 +5201,7 @@ class Up2k(object):
|
||||
return True
|
||||
|
||||
abspath = djoin(ptop, rd, fn)
|
||||
self.log("hashing " + abspath)
|
||||
self.log("hashing %r" % (abspath,))
|
||||
inf = bos.stat(abspath)
|
||||
if not inf.st_size:
|
||||
wark = up2k_wark_from_metadata(
|
||||
@@ -5245,7 +5294,7 @@ class Up2k(object):
|
||||
|
||||
x = pathmod(self.vfs, "", req_vp, {"vp": fvp, "fn": fn})
|
||||
if not x:
|
||||
t = "hook_fx(%s): failed to resolve %s based on %s"
|
||||
t = "hook_fx(%s): failed to resolve %r based on %r"
|
||||
self.log(t % (act, fvp, req_vp))
|
||||
continue
|
||||
|
||||
@@ -5297,6 +5346,11 @@ class Up2k(object):
|
||||
cur.close()
|
||||
db.close()
|
||||
|
||||
if self.mem_cur:
|
||||
db = self.mem_cur.connection
|
||||
self.mem_cur.close()
|
||||
db.close()
|
||||
|
||||
self.registry = {}
|
||||
|
||||
|
||||
|
||||
@@ -120,6 +120,13 @@ try:
|
||||
except:
|
||||
HAVE_SQLITE3 = False
|
||||
|
||||
try:
|
||||
import importlib.util
|
||||
|
||||
HAVE_ZMQ = bool(importlib.util.find_spec("zmq"))
|
||||
except:
|
||||
HAVE_ZMQ = False
|
||||
|
||||
try:
|
||||
if os.environ.get("PRTY_NO_PSUTIL"):
|
||||
raise Exception()
|
||||
@@ -229,9 +236,14 @@ META_NOBOTS = '<meta name="robots" content="noindex, nofollow">\n'
|
||||
|
||||
FFMPEG_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z"
|
||||
|
||||
URL_PRJ = "https://github.com/9001/copyparty"
|
||||
|
||||
URL_BUG = URL_PRJ + "/issues/new?labels=bug&template=bug_report.md"
|
||||
|
||||
HTTPCODE = {
|
||||
200: "OK",
|
||||
201: "Created",
|
||||
202: "Accepted",
|
||||
204: "No Content",
|
||||
206: "Partial Content",
|
||||
207: "Multi-Status",
|
||||
@@ -319,6 +331,7 @@ DAV_ALLPROPS = set(DAV_ALLPROP_L)
|
||||
|
||||
MIMES = {
|
||||
"opus": "audio/ogg; codecs=opus",
|
||||
"owa": "audio/webm; codecs=opus",
|
||||
}
|
||||
|
||||
|
||||
@@ -491,6 +504,15 @@ def py_desc() -> str:
|
||||
)
|
||||
|
||||
|
||||
def expat_ver() -> str:
|
||||
try:
|
||||
import pyexpat
|
||||
|
||||
return ".".join([str(x) for x in pyexpat.version_info])
|
||||
except:
|
||||
return "?"
|
||||
|
||||
|
||||
def _sqlite_ver() -> str:
|
||||
assert sqlite3 # type: ignore # !rm
|
||||
try:
|
||||
@@ -1024,7 +1046,7 @@ class ProgressPrinter(threading.Thread):
|
||||
now = time.time()
|
||||
if msg and now - tp > 10:
|
||||
tp = now
|
||||
self.log("progress: %s" % (msg,), 6)
|
||||
self.log("progress: %r" % (msg,), 6)
|
||||
|
||||
if no_stdout:
|
||||
continue
|
||||
@@ -1626,7 +1648,7 @@ class MultipartParser(object):
|
||||
(only the fallback non-js uploader relies on these filenames)
|
||||
"""
|
||||
for ln in read_header(self.sr, 2, 2592000):
|
||||
self.log(ln)
|
||||
self.log(repr(ln))
|
||||
|
||||
m = self.re_ctype.match(ln)
|
||||
if m:
|
||||
@@ -1917,11 +1939,11 @@ def gen_filekey_dbg(
|
||||
if p2 != fspath:
|
||||
raise Exception()
|
||||
except:
|
||||
t = "maybe wrong abspath for filekey;\norig: {}\nreal: {}"
|
||||
log(t.format(fspath, p2), 1)
|
||||
t = "maybe wrong abspath for filekey;\norig: %r\nreal: %r"
|
||||
log(t % (fspath, p2), 1)
|
||||
|
||||
t = "fk({}) salt({}) size({}) inode({}) fspath({}) at({})"
|
||||
log(t.format(ret[:8], salt, fsize, inode, fspath, ctx), 5)
|
||||
t = "fk(%s) salt(%s) size(%d) inode(%d) fspath(%r) at(%s)"
|
||||
log(t % (ret[:8], salt, fsize, inode, fspath, ctx), 5)
|
||||
|
||||
return ret
|
||||
|
||||
@@ -2277,7 +2299,7 @@ def log_reloc(
|
||||
rem: str,
|
||||
) -> None:
|
||||
nap, nvp, nfn, (nvn, nrem) = pm
|
||||
t = "reloc %s:\nold ap [%s]\nnew ap [%s\033[36m/%s\033[0m]\nold vp [%s]\nnew vp [%s\033[36m/%s\033[0m]\nold fn [%s]\nnew fn [%s]\nold vfs [%s]\nnew vfs [%s]\nold rem [%s]\nnew rem [%s]"
|
||||
t = "reloc %s:\nold ap %r\nnew ap %r\033[36m/%r\033[0m\nold vp %r\nnew vp %r\033[36m/%r\033[0m\nold fn %r\nnew fn %r\nold vfs %r\nnew vfs %r\nold rem %r\nnew rem %r"
|
||||
log(t % (re, ap, nap, nfn, vp, nvp, nfn, fn, nfn, vn.vpath, nvn.vpath, rem, nrem))
|
||||
|
||||
|
||||
@@ -2448,7 +2470,7 @@ def lsof(log: "NamedLogger", abspath: str) -> None:
|
||||
try:
|
||||
rc, so, se = runcmd([b"lsof", b"-R", fsenc(abspath)], timeout=45)
|
||||
zs = (so.strip() + "\n" + se.strip()).strip()
|
||||
log("lsof {} = {}\n{}".format(abspath, rc, zs), 3)
|
||||
log("lsof %r = %s\n%s" % (abspath, rc, zs), 3)
|
||||
except:
|
||||
log("lsof failed; " + min_ex(), 3)
|
||||
|
||||
@@ -2484,17 +2506,17 @@ def _fs_mvrm(
|
||||
for attempt in range(90210):
|
||||
try:
|
||||
if ino and os.stat(bsrc).st_ino != ino:
|
||||
t = "src inode changed; aborting %s %s"
|
||||
t = "src inode changed; aborting %s %r"
|
||||
log(t % (act, src), 1)
|
||||
return False
|
||||
if (dst and not atomic) and os.path.exists(bdst):
|
||||
t = "something appeared at dst; aborting rename [%s] ==> [%s]"
|
||||
t = "something appeared at dst; aborting rename %r ==> %r"
|
||||
log(t % (src, dst), 1)
|
||||
return False
|
||||
osfun(*args)
|
||||
if attempt:
|
||||
now = time.time()
|
||||
t = "%sd in %.2f sec, attempt %d: %s"
|
||||
t = "%sd in %.2f sec, attempt %d: %r"
|
||||
log(t % (act, now - t0, attempt + 1, src))
|
||||
return True
|
||||
except OSError as ex:
|
||||
@@ -2506,7 +2528,7 @@ def _fs_mvrm(
|
||||
if not attempt:
|
||||
if not PY2:
|
||||
ino = os.stat(bsrc).st_ino
|
||||
t = "%s failed (err.%d); retrying for %d sec: [%s]"
|
||||
t = "%s failed (err.%d); retrying for %d sec: %r"
|
||||
log(t % (act, ex.errno, maxtime + 0.99, src))
|
||||
|
||||
time.sleep(chill)
|
||||
@@ -2709,12 +2731,29 @@ def build_netmap(csv: str, defer_mutex: bool = False):
|
||||
if csv in ("any", "all", "no", ",", ""):
|
||||
return None
|
||||
|
||||
if csv in ("lan", "local", "private", "prvt"):
|
||||
csv = "10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fd00::/8" # lan
|
||||
csv += ", 169.254.0.0/16, fe80::/10" # link-local
|
||||
csv += ", 127.0.0.0/8, ::1/128" # loopback
|
||||
|
||||
srcs = [x.strip() for x in csv.split(",") if x.strip()]
|
||||
|
||||
expanded_shorthands = False
|
||||
for shorthand in ("lan", "local", "private", "prvt"):
|
||||
if shorthand in srcs:
|
||||
if not expanded_shorthands:
|
||||
srcs += [
|
||||
# lan:
|
||||
"10.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
"fd00::/8",
|
||||
# link-local:
|
||||
"169.254.0.0/16",
|
||||
"fe80::/10",
|
||||
# loopback:
|
||||
"127.0.0.0/8",
|
||||
"::1/128",
|
||||
]
|
||||
expanded_shorthands = True
|
||||
|
||||
srcs.remove(shorthand)
|
||||
|
||||
if not HAVE_IPV6:
|
||||
srcs = [x for x in srcs if ":" not in x]
|
||||
|
||||
@@ -2779,6 +2818,26 @@ def yieldfile(fn: str, bufsz: int) -> Generator[bytes, None, None]:
|
||||
yield buf
|
||||
|
||||
|
||||
def justcopy(
|
||||
fin: Generator[bytes, None, None],
|
||||
fout: Union[typing.BinaryIO, typing.IO[Any]],
|
||||
hashobj: Optional["hashlib._Hash"],
|
||||
max_sz: int,
|
||||
slp: float,
|
||||
) -> tuple[int, str, str]:
|
||||
tlen = 0
|
||||
for buf in fin:
|
||||
tlen += len(buf)
|
||||
if max_sz and tlen > max_sz:
|
||||
continue
|
||||
|
||||
fout.write(buf)
|
||||
if slp:
|
||||
time.sleep(slp)
|
||||
|
||||
return tlen, "checksum-disabled", "checksum-disabled"
|
||||
|
||||
|
||||
def hashcopy(
|
||||
fin: Generator[bytes, None, None],
|
||||
fout: Union[typing.BinaryIO, typing.IO[Any]],
|
||||
@@ -3349,6 +3408,7 @@ def _parsehook(
|
||||
|
||||
def runihook(
|
||||
log: Optional["NamedLogger"],
|
||||
verbose: bool,
|
||||
cmd: str,
|
||||
vol: "VFS",
|
||||
ups: list[tuple[str, int, int, str, str, str, int]],
|
||||
@@ -3378,6 +3438,17 @@ def runihook(
|
||||
else:
|
||||
sp_ka["sin"] = b"\n".join(fsenc(x) for x in aps)
|
||||
|
||||
if acmd[0].startswith("zmq:"):
|
||||
try:
|
||||
msg = sp_ka["sin"].decode("utf-8", "replace")
|
||||
_zmq_hook(log, verbose, "xiu", acmd[0][4:].lower(), msg, wait, sp_ka)
|
||||
if verbose and log:
|
||||
log("hook(xiu) %r OK" % (cmd,), 6)
|
||||
except Exception as ex:
|
||||
if log:
|
||||
log("zeromq failed: %r" % (ex,))
|
||||
return True
|
||||
|
||||
t0 = time.time()
|
||||
if fork:
|
||||
Daemon(runcmd, cmd, bcmd, ka=sp_ka)
|
||||
@@ -3387,15 +3458,126 @@ def runihook(
|
||||
retchk(rc, bcmd, err, log, 5)
|
||||
return False
|
||||
|
||||
wait -= time.time() - t0
|
||||
if wait > 0:
|
||||
time.sleep(wait)
|
||||
if wait:
|
||||
wait -= time.time() - t0
|
||||
if wait > 0:
|
||||
time.sleep(wait)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
ZMQ = {}
|
||||
ZMQ_DESC = {
|
||||
"pub": "fire-and-forget to all/any connected SUB-clients",
|
||||
"push": "fire-and-forget to one of the connected PULL-clients",
|
||||
"req": "send messages to a REP-server and blocking-wait for ack",
|
||||
}
|
||||
|
||||
|
||||
def _zmq_hook(
|
||||
log: Optional["NamedLogger"],
|
||||
verbose: bool,
|
||||
src: str,
|
||||
cmd: str,
|
||||
msg: str,
|
||||
wait: float,
|
||||
sp_ka: dict[str, Any],
|
||||
) -> tuple[int, str]:
|
||||
import zmq
|
||||
|
||||
try:
|
||||
mtx = ZMQ["mtx"]
|
||||
except:
|
||||
ZMQ["mtx"] = threading.Lock()
|
||||
time.sleep(0.1)
|
||||
mtx = ZMQ["mtx"]
|
||||
|
||||
ret = ""
|
||||
nret = 0
|
||||
t0 = time.time()
|
||||
if verbose and log:
|
||||
log("hook(%s) %r entering zmq-main-lock" % (src, cmd), 6)
|
||||
|
||||
with mtx:
|
||||
try:
|
||||
mode, sck, mtx = ZMQ[cmd]
|
||||
except:
|
||||
mode, uri = cmd.split(":", 1)
|
||||
try:
|
||||
desc = ZMQ_DESC[mode]
|
||||
if log:
|
||||
t = "libzmq(%s) pyzmq(%s) init(%s); %s"
|
||||
log(t % (zmq.zmq_version(), zmq.__version__, cmd, desc))
|
||||
except:
|
||||
raise Exception("the only supported ZMQ modes are REQ PUB PUSH")
|
||||
|
||||
try:
|
||||
ctx = ZMQ["ctx"]
|
||||
except:
|
||||
ctx = ZMQ["ctx"] = zmq.Context()
|
||||
|
||||
timeout = sp_ka["timeout"]
|
||||
|
||||
if mode == "pub":
|
||||
sck = ctx.socket(zmq.PUB)
|
||||
sck.setsockopt(zmq.LINGER, 0)
|
||||
sck.bind(uri)
|
||||
time.sleep(1) # give clients time to connect; avoids losing first msg
|
||||
elif mode == "push":
|
||||
sck = ctx.socket(zmq.PUSH)
|
||||
if timeout:
|
||||
sck.SNDTIMEO = int(timeout * 1000)
|
||||
sck.setsockopt(zmq.LINGER, 0)
|
||||
sck.bind(uri)
|
||||
elif mode == "req":
|
||||
sck = ctx.socket(zmq.REQ)
|
||||
if timeout:
|
||||
sck.RCVTIMEO = int(timeout * 1000)
|
||||
sck.setsockopt(zmq.LINGER, 0)
|
||||
sck.connect(uri)
|
||||
else:
|
||||
raise Exception()
|
||||
|
||||
mtx = threading.Lock()
|
||||
ZMQ[cmd] = (mode, sck, mtx)
|
||||
|
||||
if verbose and log:
|
||||
log("hook(%s) %r entering socket-lock" % (src, cmd), 6)
|
||||
|
||||
with mtx:
|
||||
if verbose and log:
|
||||
log("hook(%s) %r sending |%d|" % (src, cmd, len(msg)), 6)
|
||||
|
||||
sck.send_string(msg) # PUSH can safely timeout here
|
||||
|
||||
if mode == "req":
|
||||
if verbose and log:
|
||||
log("hook(%s) %r awaiting ack from req" % (src, cmd), 6)
|
||||
try:
|
||||
ret = sck.recv().decode("utf-8", "replace")
|
||||
if ret.startswith("return "):
|
||||
m = re.search("^return ([0-9]+)", ret[:12])
|
||||
if m:
|
||||
nret = int(m.group(1))
|
||||
except:
|
||||
sck.close()
|
||||
del ZMQ[cmd] # bad state; must reset
|
||||
raise Exception("ack timeout; zmq socket killed")
|
||||
|
||||
if ret and log:
|
||||
log("hook(%s) %r ACK: %r" % (src, cmd, ret), 6)
|
||||
|
||||
if wait:
|
||||
wait -= time.time() - t0
|
||||
if wait > 0:
|
||||
time.sleep(wait)
|
||||
|
||||
return nret, ret
|
||||
|
||||
|
||||
def _runhook(
|
||||
log: Optional["NamedLogger"],
|
||||
verbose: bool,
|
||||
src: str,
|
||||
cmd: str,
|
||||
ap: str,
|
||||
@@ -3436,6 +3618,12 @@ def _runhook(
|
||||
else:
|
||||
arg = txt or ap
|
||||
|
||||
if acmd[0].startswith("zmq:"):
|
||||
zi, zs = _zmq_hook(log, verbose, src, acmd[0][4:].lower(), arg, wait, sp_ka)
|
||||
if zi:
|
||||
raise Exception("zmq says %d" % (zi,))
|
||||
return {"rc": 0, "stdout": zs}
|
||||
|
||||
acmd += [arg]
|
||||
if acmd[0].endswith(".py"):
|
||||
acmd = [pybin] + acmd
|
||||
@@ -3464,9 +3652,10 @@ def _runhook(
|
||||
except:
|
||||
ret = {"rc": rc, "stdout": v}
|
||||
|
||||
wait -= time.time() - t0
|
||||
if wait > 0:
|
||||
time.sleep(wait)
|
||||
if wait:
|
||||
wait -= time.time() - t0
|
||||
if wait > 0:
|
||||
time.sleep(wait)
|
||||
|
||||
return ret
|
||||
|
||||
@@ -3489,17 +3678,17 @@ def runhook(
|
||||
txt: str,
|
||||
) -> dict[str, Any]:
|
||||
assert broker or up2k # !rm
|
||||
asrv = (broker or up2k).asrv
|
||||
args = (broker or up2k).args
|
||||
verbose = args.hook_v
|
||||
vp = vp.replace("\\", "/")
|
||||
ret = {"rc": 0}
|
||||
for cmd in cmds:
|
||||
try:
|
||||
hr = _runhook(
|
||||
log, src, cmd, ap, vp, host, uname, perms, mt, sz, ip, at, txt
|
||||
log, verbose, src, cmd, ap, vp, host, uname, perms, mt, sz, ip, at, txt
|
||||
)
|
||||
if log and args.hook_v:
|
||||
log("hook(%s) [%s] => \033[32m%s" % (src, cmd, hr), 6)
|
||||
if verbose and log:
|
||||
log("hook(%s) %r => \033[32m%s" % (src, cmd, hr), 6)
|
||||
if not hr:
|
||||
return {}
|
||||
for k, v in hr.items():
|
||||
@@ -3514,6 +3703,8 @@ def runhook(
|
||||
elif k in ret:
|
||||
if k == "rc" and v:
|
||||
ret[k] = v
|
||||
elif k == "stdout" and v and not ret[k]:
|
||||
ret[k] = v
|
||||
else:
|
||||
ret[k] = v
|
||||
except Exception as ex:
|
||||
|
||||
@@ -32,7 +32,7 @@ window.baguetteBox = (function () {
|
||||
scrollCSS = ['', ''],
|
||||
scrollTimer = 0,
|
||||
re_i = /^[^?]+\.(a?png|avif|bmp|gif|heif|jpe?g|jfif|svg|webp)(\?|$)/i,
|
||||
re_v = /^[^?]+\.(webm|mkv|mp4|m4v)(\?|$)/i,
|
||||
re_v = /^[^?]+\.(webm|mkv|mp4|m4v|mov)(\?|$)/i,
|
||||
anims = ['slideIn', 'fadeIn', 'none'],
|
||||
data = {}, // all galleries
|
||||
imagesElements = [],
|
||||
|
||||
@@ -188,7 +188,6 @@ html.y {
|
||||
--srv-1: #555;
|
||||
--srv-2: #c83;
|
||||
--srv-3: #c0a;
|
||||
--srv-3b: rgba(255,68,204,0.6);
|
||||
|
||||
--tree-bg: #fff;
|
||||
|
||||
@@ -286,6 +285,7 @@ html.bz {
|
||||
--f-h-b1: #34384e;
|
||||
--mp-sh: #11121d;
|
||||
/*--mp-b-bg: #2c3044;*/
|
||||
--f-play-bg: var(--btn-1-bg);
|
||||
}
|
||||
html.by {
|
||||
--bg: #f2f2f2;
|
||||
@@ -389,8 +389,6 @@ html.cy {
|
||||
}
|
||||
html.dz {
|
||||
--fg: #4d4;
|
||||
--fg-max: #fff;
|
||||
--fg2-max: #fff;
|
||||
--fg-weak: #2a2;
|
||||
|
||||
--bg-u6: #020;
|
||||
@@ -400,11 +398,9 @@ html.dz {
|
||||
--bg-u2: #020;
|
||||
--bg-u1: #020;
|
||||
--bg: #010;
|
||||
--bgg: var(--bg);
|
||||
--bg-d1: #000;
|
||||
--bg-d2: #020;
|
||||
--bg-d3: #000;
|
||||
--bg-max: #000;
|
||||
|
||||
--tab-alt: #6f6;
|
||||
--row-alt: #030;
|
||||
@@ -417,45 +413,21 @@ html.dz {
|
||||
--a-dark: #afa;
|
||||
--a-gray: #2a2;
|
||||
|
||||
--btn-fg: var(--a);
|
||||
--btn-bg: rgba(64,128,64,0.15);
|
||||
--btn-h-fg: var(--a-hil);
|
||||
--btn-h-bg: #050;
|
||||
--btn-1-fg: #000;
|
||||
--btn-1-bg: #4f4;
|
||||
--btn-1h-fg: var(--btn-1-fg);
|
||||
--btn-1h-bg: #3f3;
|
||||
--btn-bs: 0 0 0 .1em #080 inset;
|
||||
--btn-1-bs: a;
|
||||
|
||||
--chk-fg: var(--tab-alt);
|
||||
--txt-sh: var(--bg-d2);
|
||||
--txt-bg: var(--btn-bg);
|
||||
|
||||
--op-aa-fg: var(--a);
|
||||
--op-aa-bg: var(--bg-d2);
|
||||
--op-a-sh: rgba(0,0,0,0.5);
|
||||
|
||||
--u2-btn-b1: var(--fg-weak);
|
||||
--u2-sbtn-b1: var(--fg-weak);
|
||||
--u2-txt-bg: var(--bg-u5);
|
||||
--u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1));
|
||||
--u2-tab-b1: var(--fg-weak);
|
||||
--u2-tab-1-fg: #fff;
|
||||
--u2-tab-1-bg: linear-gradient(to bottom, #151, var(--bg) 80%);
|
||||
--u2-tab-1-b1: #7c5;
|
||||
--u2-tab-1-b2: #583;
|
||||
--u2-tab-1-sh: #280;
|
||||
--u2-b-fg: #fff;
|
||||
--u2-b1-bg: #3a3;
|
||||
--u2-b2-bg: #3a3;
|
||||
--u2-inf-bg: #07a;
|
||||
--u2-inf-b1: #0be;
|
||||
--u2-ok-bg: #380;
|
||||
--u2-ok-b1: #8e4;
|
||||
--u2-err-bg: #900;
|
||||
--u2-err-b1: #d06;
|
||||
--ud-b1: #888;
|
||||
|
||||
--sort-1: #fff;
|
||||
--sort-2: #3f3;
|
||||
@@ -467,47 +439,12 @@ html.dz {
|
||||
|
||||
--tree-bg: #010;
|
||||
|
||||
--g-play-bg: #750;
|
||||
--g-play-b1: #c90;
|
||||
--g-play-b2: #da4;
|
||||
--g-play-sh: #b83;
|
||||
|
||||
--g-sel-fg: #fff;
|
||||
--g-sel-bg: #925;
|
||||
--g-sel-b1: #c37;
|
||||
--g-sel-sh: #b36;
|
||||
--g-fsel-bg: #d39;
|
||||
--g-fsel-b1: #d48;
|
||||
--g-fsel-ts: #804;
|
||||
--g-fg: var(--a-hil);
|
||||
--g-bg: var(--bg-u2);
|
||||
--g-b1: var(--bg-u4);
|
||||
--g-b2: var(--bg-u5);
|
||||
--g-g1: var(--bg-u2);
|
||||
--g-g2: var(--bg-u5);
|
||||
--g-f-bg: var(--bg-u4);
|
||||
--g-f-b1: var(--bg-u5);
|
||||
--g-f-fg: var(--a-hil);
|
||||
--g-sh: rgba(0,0,0,0.3);
|
||||
|
||||
--f-sh1: 0.33;
|
||||
--f-sh2: 0.02;
|
||||
--f-sh3: 0.2;
|
||||
--f-h-b1: #3b3;
|
||||
|
||||
--f-play-bg: #fc5;
|
||||
--f-play-fg: #000;
|
||||
--f-sel-sh: #fc0;
|
||||
--f-gray: #999;
|
||||
|
||||
--fm-off: #f6c;
|
||||
--mp-sh: var(--bg-d3);
|
||||
|
||||
--err-fg: #fff;
|
||||
--err-bg: #a20;
|
||||
--err-b1: #f00;
|
||||
--err-ts: #500;
|
||||
|
||||
text-shadow: none;
|
||||
font-family: 'scp', monospace, monospace;
|
||||
font-family: var(--font-mono), 'scp', monospace, monospace;
|
||||
@@ -2848,6 +2785,7 @@ html.b #u2conf a.b:hover {
|
||||
padding-left: .2em;
|
||||
}
|
||||
.fsearch_explain {
|
||||
color: var(--a-dark);
|
||||
padding-left: .7em;
|
||||
font-size: 1.1em;
|
||||
line-height: 0;
|
||||
|
||||
@@ -131,17 +131,16 @@
|
||||
<div id="widget"></div>
|
||||
|
||||
<script>
|
||||
var SR = {{ r|tojson }},
|
||||
var SR = "{{ r }}",
|
||||
CGV1 = {{ cgv1 }},
|
||||
CGV = {{ cgv|tojson }},
|
||||
TS = "{{ ts }}",
|
||||
dtheme = "{{ dtheme }}",
|
||||
srvinf = "{{ srv_info }}",
|
||||
s_name = "{{ s_name }}",
|
||||
lang = "{{ lang }}",
|
||||
dfavico = "{{ favico }}",
|
||||
have_tags_idx = {{ have_tags_idx|tojson }},
|
||||
have_tags_idx = {{ have_tags_idx }},
|
||||
sb_lg = "{{ sb_lg }}",
|
||||
txt_ext = "{{ txt_ext }}",
|
||||
logues = {{ logues|tojson if sb_lg else "[]" }},
|
||||
ls0 = {{ ls0|tojson }};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
var XHR = XMLHttpRequest,
|
||||
img_re = /\.(a?png|avif|bmp|gif|heif|jpe?g|jfif|svg|webp|webm|mkv|mp4|m4v)(\?|$)/i;
|
||||
img_re = /\.(a?png|avif|bmp|gif|heif|jpe?g|jfif|svg|webp|webm|mkv|mp4|m4v|mov)(\?|$)/i;
|
||||
|
||||
var Ls = {
|
||||
"eng": {
|
||||
@@ -206,6 +206,7 @@ var Ls = {
|
||||
"cl_uopts": "up2k switches",
|
||||
"cl_favico": "favicon",
|
||||
"cl_bigdir": "big dirs",
|
||||
"cl_hsort": "#sort",
|
||||
"cl_keytype": "key notation",
|
||||
"cl_hiddenc": "hidden columns",
|
||||
"cl_hidec": "hide",
|
||||
@@ -240,7 +241,7 @@ var Ls = {
|
||||
"cut_nag": "OS notification when upload completes$N(only if the browser or tab is not active)",
|
||||
"cut_sfx": "audible alert when upload completes$N(only if the browser or tab is not active)",
|
||||
|
||||
"cut_mt": "use multithreading to accelerate file hashing$N$Nthis uses web-workers and requires$Nmore RAM (up to 512 MiB extra)$N$N30% faster https, 4.5x faster http,$Nand 5.3x faster on android phones\">mt",
|
||||
"cut_mt": "use multithreading to accelerate file hashing$N$Nthis uses web-workers and requires$Nmore RAM (up to 512 MiB extra)$N$Nmakes https 30% faster, http 4.5x faster\">mt",
|
||||
|
||||
"cft_text": "favicon text (blank and refresh to disable)",
|
||||
"cft_fg": "foreground color",
|
||||
@@ -248,6 +249,7 @@ var Ls = {
|
||||
|
||||
"cdt_lim": "max number of files to show in a folder",
|
||||
"cdt_ask": "when scrolling to the bottom,$Ninstead of loading more files,$Nask what to do",
|
||||
"cdt_hsort": "how many sorting rules (<code>,sorthref</code>) to include in media-URLs. Setting this to 0 will also ignore sorting-rules included in media links when clicking them",
|
||||
|
||||
"tt_entree": "show navpane (directory tree sidebar)$NHotkey: B",
|
||||
"tt_detree": "show breadcrumbs$NHotkey: B",
|
||||
@@ -261,6 +263,7 @@ var Ls = {
|
||||
"ml_pmode": "at end of folder...",
|
||||
"ml_btns": "cmds",
|
||||
"ml_tcode": "transcode",
|
||||
"ml_tcode2": "transcode to",
|
||||
"ml_tint": "tint",
|
||||
"ml_eq": "audio equalizer",
|
||||
"ml_drc": "dynamic range compressor",
|
||||
@@ -284,6 +287,14 @@ var Ls = {
|
||||
"mt_cflac": "convert flac / wav to opus\">flac",
|
||||
"mt_caac": "convert aac / m4a to opus\">aac",
|
||||
"mt_coth": "convert all others (not mp3) to opus\">oth",
|
||||
"mt_c2opus": "best choice for desktops, laptops, android\">opus",
|
||||
"mt_c2owa": "opus-weba, for iOS 17.5 and newer\">owa",
|
||||
"mt_c2caf": "opus-caf, for iOS 11 through 17\">caf",
|
||||
"mt_c2mp3": "use this on very old devices\">mp3",
|
||||
"mt_c2ok": "nice, good choice",
|
||||
"mt_c2nd": "that's not the recommended output format for your device, but that's fine",
|
||||
"mt_c2ng": "your device does not seem to support this output format, but let's try anyways",
|
||||
"mt_xowa": "there are bugs in iOS preventing background playback using this format; please use caf or mp3 instead",
|
||||
"mt_tint": "background level (0-100) on the seekbar$Nto make buffering less distracting",
|
||||
"mt_eq": "enables the equalizer and gain control;$N$Nboost <code>0</code> = standard 100% volume (unmodified)$N$Nwidth <code>1 </code> = standard stereo (unmodified)$Nwidth <code>0.5</code> = 50% left-right crossfeed$Nwidth <code>0 </code> = mono$N$Nboost <code>-0.8</code> & width <code>10</code> = vocal removal :^)$N$Nenabling the equalizer makes gapless albums fully gapless, so leave it on with all the values at zero (except width = 1) if you care about that",
|
||||
"mt_drc": "enables the dynamic range compressor (volume flattener / brickwaller); will also enable EQ to balance the spaghetti, so set all EQ fields except for 'width' to 0 if you don't want it$N$Nlowers the volume of audio above THRESHOLD dB; for every RATIO dB past THRESHOLD there is 1 dB of output, so default values of tresh -24 and ratio 12 means it should never get louder than -22 dB and it is safe to increase the equalizer boost to 0.8, or even 1.8 with ATK 0 and a huge RLS like 90 (only works in firefox; RLS is max 1 in other browsers)$N$N(see wikipedia, they explain it much better)",
|
||||
@@ -521,6 +532,7 @@ var Ls = {
|
||||
"u_pott": "<p>files: <b>{0}</b> finished, <b>{1}</b> failed, <b>{2}</b> busy, <b>{3}</b> queued</p>",
|
||||
"u_ever": "this is the basic uploader; up2k needs at least<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1",
|
||||
"u_su2k": 'this is the basic uploader; <a href="#" id="u2yea">up2k</a> is better',
|
||||
"u_uput": 'optimize for speed (skip checksum)',
|
||||
"u_ewrite": 'you do not have write-access to this folder',
|
||||
"u_eread": 'you do not have read-access to this folder',
|
||||
"u_enoi": 'file-search is not enabled in server config',
|
||||
@@ -539,6 +551,7 @@ var Ls = {
|
||||
"u_hashdone": 'hashing done',
|
||||
"u_hashing": 'hash',
|
||||
"u_hs": 'handshaking...',
|
||||
"u_started": "the files are now being uploaded; see [🚀]",
|
||||
"u_dupdefer": "duplicate; will be processed after all other files",
|
||||
"u_actx": "click this text to prevent loss of<br />performance when switching to other windows/tabs",
|
||||
"u_fixed": "OK! Fixed it 👍",
|
||||
@@ -574,6 +587,7 @@ var Ls = {
|
||||
"ue_la": 'you are currently logged in as "{0}"',
|
||||
"ue_sr": 'you are currently in file-search mode\n\nswitch to upload-mode by clicking the magnifying glass 🔎 (next to the big SEARCH button), and try uploading again\n\nsorry',
|
||||
"ue_ta": 'try uploading again, it should work now',
|
||||
"ue_ab": "this file is already being uploaded into another folder, and that upload must be completed before the file can be uploaded elsewhere.\n\nYou can abort and forget the initial upload using the top-left 🧯",
|
||||
"ur_1uo": "OK: File uploaded successfully",
|
||||
"ur_auo": "OK: All {0} files uploaded successfully",
|
||||
"ur_1so": "OK: File found on server",
|
||||
@@ -790,6 +804,7 @@ var Ls = {
|
||||
"cl_uopts": "up2k-brytere",
|
||||
"cl_favico": "favicon",
|
||||
"cl_bigdir": "store mapper",
|
||||
"cl_hsort": "#sort",
|
||||
"cl_keytype": "notasjon for musikalsk dur",
|
||||
"cl_hiddenc": "skjulte kolonner",
|
||||
"cl_hidec": "skjul",
|
||||
@@ -824,7 +839,7 @@ var Ls = {
|
||||
"cut_nag": "meldingsvarsel når opplastning er ferdig$N(kun on nettleserfanen ikke er synlig)",
|
||||
"cut_sfx": "lydvarsel når opplastning er ferdig$N(kun on nettleserfanen ikke er synlig)",
|
||||
|
||||
"cut_mt": "raskere befaring ved å bruke hele CPU'en$N$Ndenne funksjonen anvender web-workers$Nog krever mer RAM (opptil 512 MiB ekstra)$N$N30% raskere https, 4.5x raskere http,$Nog 5.3x raskere på android-telefoner\">mt",
|
||||
"cut_mt": "raskere befaring ved å bruke hele CPU'en$N$Ndenne funksjonen anvender web-workers$Nog krever mer RAM (opptil 512 MiB ekstra)$N$Ngjør https 30% raskere, http 4.5x raskere\">mt",
|
||||
|
||||
"cft_text": "ikontekst (blank ut og last siden på nytt for å deaktivere)",
|
||||
"cft_fg": "farge",
|
||||
@@ -832,6 +847,7 @@ var Ls = {
|
||||
|
||||
"cdt_lim": "maks antall filer å vise per mappe",
|
||||
"cdt_ask": "vis knapper for å laste flere filer nederst på siden istedenfor å gradvis laste mer av mappen når man scroller ned",
|
||||
"cdt_hsort": "antall sorterings-regler (<code>,sorthref</code>) som skal inkluderes når media-URL'er genereres. Hvis denne er 0 så vil sorterings-regler i URL'er hverken bli generert eller lest",
|
||||
|
||||
"tt_entree": "bytt til mappehierarki$NSnarvei: B",
|
||||
"tt_detree": "bytt til tradisjonell sti-visning$NSnarvei: B",
|
||||
@@ -845,6 +861,7 @@ var Ls = {
|
||||
"ml_pmode": "ved enden av mappen",
|
||||
"ml_btns": "knapper",
|
||||
"ml_tcode": "konvertering",
|
||||
"ml_tcode2": "konverter til",
|
||||
"ml_tint": "tint",
|
||||
"ml_eq": "audio equalizer (tonejustering)",
|
||||
"ml_drc": "compressor (volum-utjevning)",
|
||||
@@ -868,6 +885,14 @@ var Ls = {
|
||||
"mt_cflac": "konverter flac / wav-filer til opus\">flac",
|
||||
"mt_caac": "konverter aac / m4a-filer til to opus\">aac",
|
||||
"mt_coth": "konverter alt annet (men ikke mp3) til opus\">andre",
|
||||
"mt_c2opus": "det beste valget for alle PCer og Android\">opus",
|
||||
"mt_c2owa": "opus-weba, for iOS 17.5 og nyere\">owa",
|
||||
"mt_c2caf": "opus-caf, for iOS 11 tilogmed 17\">caf",
|
||||
"mt_c2mp3": "bra valg for steinalder-utstyr (slår aldri feil)\">mp3",
|
||||
"mt_c2ok": "bra valg!",
|
||||
"mt_c2nd": "ikke det foretrukne valget for din enhet, men funker sikkert greit",
|
||||
"mt_c2ng": "ser virkelig ikke ut som enheten din takler dette formatet... men ok, vi prøver",
|
||||
"mt_xowa": "iOS har fortsatt problemer med avspilling av owa-musikk i bakgrunnen. Bruk caf eller mp3 istedenfor",
|
||||
"mt_tint": "nivå av bakgrunnsfarge på søkestripa (0-100),$Ngjør oppdateringer mindre distraherende",
|
||||
"mt_eq": "aktiver tonekontroll og forsterker;$N$Nboost <code>0</code> = normal volumskala$N$Nwidth <code>1 </code> = normal stereo$Nwidth <code>0.5</code> = 50% blanding venstre-høyre$Nwidth <code>0 </code> = mono$N$Nboost <code>-0.8</code> & width <code>10</code> = instrumental :^)$N$Nreduserer også dødtid imellom sangfiler",
|
||||
"mt_drc": "aktiver volum-utjevning (dynamic range compressor); vil også aktivere tonejustering, så sett alle EQ-feltene bortsett fra 'width' til 0 hvis du ikke vil ha noe EQ$N$Nfilteret vil dempe volumet på alt som er høyere enn TRESH dB; for hver RATIO dB over grensen er det 1dB som treffer høyttalerne, så standardverdiene tresh -24 og ratio 12 skal bety at volumet ikke går høyere enn -22 dB, slik at man trygt kan øke boost-verdien i equalizer'n til rundt 0.8, eller 1.8 kombinert med ATK 0 og RLS 90 (bare mulig i firefox; andre nettlesere tar ikke høyere RLS enn 1)$N$Nwikipedia forklarer dette mye bedre forresten",
|
||||
@@ -1105,6 +1130,7 @@ var Ls = {
|
||||
"u_pott": "<p>filer: <b>{0}</b> ferdig, <b>{1}</b> feilet, <b>{2}</b> behandles, <b>{3}</b> i kø</p>",
|
||||
"u_ever": "dette er den primitive opplasteren; up2k krever minst:<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1",
|
||||
"u_su2k": 'dette er den primitive opplasteren; <a href="#" id="u2yea">up2k</a> er bedre',
|
||||
"u_uput": 'litt raskere (uten sha512)',
|
||||
"u_ewrite": 'du har ikke skrivetilgang i denne mappen',
|
||||
"u_eread": 'du har ikke lesetilgang i denne mappen',
|
||||
"u_enoi": 'filsøk er deaktivert i serverkonfigurasjonen',
|
||||
@@ -1123,6 +1149,7 @@ var Ls = {
|
||||
"u_hashdone": 'befaring ferdig',
|
||||
"u_hashing": 'les',
|
||||
"u_hs": 'serveren tenker...',
|
||||
"u_started": "filene blir nå lastet opp 🚀",
|
||||
"u_dupdefer": "duplikat; vil bli håndtert til slutt",
|
||||
"u_actx": "klikk her for å forhindre tap av<br />ytelse ved bytte til andre vinduer/faner",
|
||||
"u_fixed": "OK! Løste seg 👍",
|
||||
@@ -1158,6 +1185,7 @@ var Ls = {
|
||||
"ue_la": 'du er logget inn som "{0}"',
|
||||
"ue_sr": 'du er i filsøk-modus\n\nbytt til opplastning ved å klikke på forstørrelsesglasset 🔎 (ved siden av den store FILSØK-knappen) og prøv igjen\n\nsorry',
|
||||
"ue_ta": 'prøv å laste opp igjen, det burde funke nå',
|
||||
"ue_ab": "den samme filen er allerede under opplastning til en annen mappe, og den må fullføres der før filen kan lastes opp andre steder.\n\nDu kan avbryte og glemme den påbegynte opplastningen ved hjelp av 🧯 oppe til venstre",
|
||||
"ur_1uo": "OK: Filen ble lastet opp",
|
||||
"ur_auo": "OK: Alle {0} filene ble lastet opp",
|
||||
"ur_1so": "OK: Filen ble funnet på serveren",
|
||||
@@ -1374,6 +1402,7 @@ var Ls = {
|
||||
"cl_uopts": "up2k 开关",
|
||||
"cl_favico": "网站图标",
|
||||
"cl_bigdir": "最大目录数",
|
||||
"cl_hsort": "#sort", //m
|
||||
"cl_keytype": "键位符号",
|
||||
"cl_hiddenc": "隐藏列",
|
||||
"cl_hidec": "隐藏",
|
||||
@@ -1408,7 +1437,7 @@ var Ls = {
|
||||
"cut_nag": "上传完成时的操作系统通知$N(仅当浏览器或标签页不活跃时)",
|
||||
"cut_sfx": "上传完成时的声音警报$N(仅当浏览器或标签页不活跃时)",
|
||||
|
||||
"cut_mt": "使用多线程加速文件哈希$N$N这使用 Web Worker 并且需要更多内存(额外最多 512 MiB)$N$N比https快30%,http快4.5倍,比Android 手机快5.3倍\">mt",
|
||||
"cut_mt": "使用多线程加速文件哈希$N$N这使用 Web Worker 并且需要更多内存(额外最多 512 MiB)$N$N这使得 https 快 30%,http 快 4.5 倍\">mt",
|
||||
|
||||
"cft_text": "网站图标文本(为空并刷新以禁用)",
|
||||
"cft_fg": "前景色",
|
||||
@@ -1416,6 +1445,7 @@ var Ls = {
|
||||
|
||||
"cdt_lim": "文件夹中显示的最大文件数",
|
||||
"cdt_ask": "滚动到底部时,$N不会加载更多文件,$N而是询问你该怎么做",
|
||||
"cdt_hsort": "包含在媒体 URL 中的排序规则 (<code>,sorthref</code>) 数量。将其设置为 0 时,点击媒体链接时也会忽略排序规则。", //m
|
||||
|
||||
"tt_entree": "显示导航面板(目录树侧边栏)$N快捷键: B",
|
||||
"tt_detree": "显示面包屑导航$N快捷键: B",
|
||||
@@ -1429,6 +1459,7 @@ var Ls = {
|
||||
"ml_pmode": "在文件夹末尾时...",
|
||||
"ml_btns": "命令",
|
||||
"ml_tcode": "转码",
|
||||
"ml_tcode2": "转换为", //m
|
||||
"ml_tint": "透明度",
|
||||
"ml_eq": "音频均衡器",
|
||||
"ml_drc": "动态范围压缩器",
|
||||
@@ -1452,6 +1483,14 @@ var Ls = {
|
||||
"mt_cflac": "将 flac / wav 转换为 opus\">flac",
|
||||
"mt_caac": "将 aac / m4a 转换为 opus\">aac",
|
||||
"mt_coth": "将所有其他(不是 mp3)转换为 opus\">oth",
|
||||
"mt_c2opus": "适合桌面电脑、笔记本电脑和安卓设备的最佳选择\">opus", //m
|
||||
"mt_c2owa": "opus-weba(适用于 iOS 17.5 及更新版本)\">owa", //m
|
||||
"mt_c2caf": "opus-caf(适用于 iOS 11 到 iOS 17)\">caf", //m
|
||||
"mt_c2mp3": "适用于非常旧的设备\">mp3", //m
|
||||
"mt_c2ok": "不错的选择!", //m
|
||||
"mt_c2nd": "这不是您的设备推荐的输出格式,但应该没问题。", //m
|
||||
"mt_c2ng": "您的设备似乎不支持此输出格式,不过我们还是试试看吧。", //m
|
||||
"mt_xowa": "iOS 系统仍存在无法后台播放 owa 音乐的错误,请改用 caf 或 mp3 格式。", //m
|
||||
"mt_tint": "在进度条上设置背景级别(0-100)",
|
||||
"mt_eq": "启用均衡器和增益控制;$N$Nboost <code>0</code> = 标准 100% 音量(默认)$N$Nwidth <code>1 </code> = 标准立体声(默认)$Nwidth <code>0.5</code> = 50% 左右交叉反馈$Nwidth <code>0 </code> = 单声道$N$Nboost <code>-0.8</code> & width <code>10</code> = 人声移除 )$N$N启用均衡器使无缝专辑完全无缝,所以如果你在乎这一点,请保持启用,所有值设为零(除了宽度 = 1)",
|
||||
"mt_drc": "启用动态范围压缩器(音量平滑器 / 限幅器);还会启用均衡器以平衡音频,因此如果你不想要它,请将均衡器字段除了 '宽度' 外的所有字段设置为 0$N$N降低 THRESHOLD dB 以上的音频的音量;每超过 THRESHOLD dB 的 RATIO 会有 1 dB 输出,所以默认值 tresh -24 和 ratio 12 意味着它的音量不应超过 -22 dB,可以安全地将均衡器增益提高到 0.8,甚至在 ATK 0 和 RLS 如 90 的情况下提高到 1.8(仅在 Firefox 中有效;其他浏览器中 RLS 最大为 1)$N$N(见维基百科,他们解释得更好)",
|
||||
@@ -1689,6 +1728,7 @@ var Ls = {
|
||||
"u_pott": "<p>个文件: <b>{0}</b> 已完成, <b>{1}</b> 失败, <b>{2}</b> 正在处理, <b>{3}</b> 排队中</p>",
|
||||
"u_ever": "这是基本的上传工具; up2k 需要至少<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1",
|
||||
"u_su2k": '这是基本的上传工具;<a href="#" id="u2yea">up2k</a> 更好',
|
||||
"u_uput": '提高速度(跳过校验和)',
|
||||
"u_ewrite": '你对这个文件夹没有写入权限',
|
||||
"u_eread": '你对这个文件夹没有读取权限',
|
||||
"u_enoi": '文件搜索在服务器配置中未启用',
|
||||
@@ -1707,6 +1747,7 @@ var Ls = {
|
||||
"u_hashdone": '哈希完成',
|
||||
"u_hashing": '哈希',
|
||||
"u_hs": '正在等待服务器...',
|
||||
"u_started": "文件现在正在上传 🚀", //m
|
||||
"u_dupdefer": "这是一个重复文件。它将在所有其他文件上传后进行处理",
|
||||
"u_actx": "单击此文本以防止切换到其他窗口/选项卡时性能下降",
|
||||
"u_fixed": "好! 已修复 👍",
|
||||
@@ -1742,6 +1783,7 @@ var Ls = {
|
||||
"ue_la": '你当前以 "{0}" 登录',
|
||||
"ue_sr": '你当前处于文件搜索模式\n\n通过点击大搜索按钮旁边的放大镜 🔎 切换到上传模式,然后重试上传\n\n抱歉',
|
||||
"ue_ta": '尝试再次上传,现在应该能正常工作',
|
||||
"ue_ab": "这份文件正在上传到另一个文件夹,必须完成该上传后,才能将文件上传到其他位置。\n\n您可以通过左上角的🧯中止并忘记该上传。", //m
|
||||
"ur_1uo": "成功:文件上传成功",
|
||||
"ur_auo": "成功:所有 {0} 个文件上传成功",
|
||||
"ur_1so": "成功:文件在服务器上找到",
|
||||
@@ -1935,6 +1977,10 @@ ebi('op_up2k').innerHTML = (
|
||||
|
||||
ebi('wrap').insertBefore(mknod('div', 'lazy'), ebi('epi'));
|
||||
|
||||
var x = ebi('bbsw');
|
||||
x.parentNode.insertBefore(mknod('div', null,
|
||||
'<input type="checkbox" id="uput" name="uput"><label for="uput">' + L.u_uput + '</label>'), x);
|
||||
|
||||
|
||||
(function () {
|
||||
var o = mknod('div');
|
||||
@@ -2014,6 +2060,13 @@ ebi('op_cfg').innerHTML = (
|
||||
' </td>\n' +
|
||||
' </div>\n' +
|
||||
'</div>\n' +
|
||||
'<div>\n' +
|
||||
' <h3>' + L.cl_hsort + '</h3>\n' +
|
||||
' <div>\n' +
|
||||
' <input type="text" id="hsortn" value="" ' + NOAC + ' style="width:3em" tt="' + L.cdt_hsort + '" />' +
|
||||
' </td>\n' +
|
||||
' </div>\n' +
|
||||
'</div>\n' +
|
||||
'<div><h3>' + L.cl_keytype + '</h3><div id="key_notation"></div></div>\n' +
|
||||
'<div><h3>' + L.cl_hiddenc + ' ' + (MOBILE ? '<a href="#" id="hcolsh">' + L.cl_hidec + '</a> / ' : '') + '<a href="#" id="hcolsr">' + L.cl_reset + '</a></h3><div id="hcols"></div></div>'
|
||||
);
|
||||
@@ -2178,6 +2231,37 @@ if (window.og_fn) {
|
||||
}
|
||||
|
||||
|
||||
var hsortn = ebi('hsortn').value = icfg_get('hsortn', dhsortn);
|
||||
ebi('hsortn').oninput = function (e) {
|
||||
var n = parseInt(this.value);
|
||||
swrite('hsortn', hsortn = (isNum(n) ? n : dhsortn));
|
||||
};
|
||||
(function() {
|
||||
var args = ('' + hash0).split(/,sort/g);
|
||||
if (args.length < 2)
|
||||
return;
|
||||
|
||||
var ret = [];
|
||||
for (var a = 1; a < args.length; a++) {
|
||||
var t = '', n = 1, z = args[a].split(',')[0];
|
||||
if (z.startsWith('-')) {
|
||||
z = z.slice(1);
|
||||
n = -1;
|
||||
}
|
||||
if (z == "sz" || z.indexOf('/.') + 1)
|
||||
t = "int";
|
||||
ret.push([z, n, t]);
|
||||
}
|
||||
n = Math.min(ret.length, hsortn);
|
||||
if (n) {
|
||||
var cmp = jread('fsort', []);
|
||||
if (JSON.stringify(ret.slice(0, n) !=
|
||||
JSON.stringify(cmp.slice(0, n))))
|
||||
jwrite('fsort', ret);
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
var mpl = (function () {
|
||||
var have_mctl = 'mediaSession' in navigator && window.MediaMetadata;
|
||||
|
||||
@@ -2212,6 +2296,12 @@ var mpl = (function () {
|
||||
'<a href="#" id="ac_flac" class="tgl btn" tt="' + L.mt_cflac + '</a>' +
|
||||
'<a href="#" id="ac_aac" class="tgl btn" tt="' + L.mt_caac + '</a>' +
|
||||
'<a href="#" id="ac_oth" class="tgl btn" tt="' + L.mt_coth + '</a>' +
|
||||
'</div></div>' +
|
||||
'<div><h3>' + L.ml_tcode2 + '</h3><div>' +
|
||||
'<a href="#" id="ac2opus" class="tgl btn" tt="' + L.mt_c2opus + '</a>' +
|
||||
'<a href="#" id="ac2owa" class="tgl btn" tt="' + L.mt_c2owa + '</a>' +
|
||||
'<a href="#" id="ac2caf" class="tgl btn" tt="' + L.mt_c2caf + '</a>' +
|
||||
'<a href="#" id="ac2mp3" class="tgl btn" tt="' + L.mt_c2mp3 + '</a>' +
|
||||
'</div></div>'
|
||||
) : '') +
|
||||
|
||||
@@ -2319,7 +2409,7 @@ var mpl = (function () {
|
||||
c = r.ac_flac;
|
||||
else if (/\.(aac|m4a)$/i.exec(cs))
|
||||
c = r.ac_aac;
|
||||
else if (/\.(ogg|opus)$/i.exec(cs) && !can_ogg)
|
||||
else if (/\.(ogg|opus)$/i.exec(cs) && (!can_ogg || mpl.ac2 == 'mp3'))
|
||||
c = true;
|
||||
else if (re_au_native.exec(cs))
|
||||
c = false;
|
||||
@@ -2327,7 +2417,56 @@ var mpl = (function () {
|
||||
if (!c)
|
||||
return url;
|
||||
|
||||
return addq(url, 'th=' + (can_ogg ? 'opus' : (IPHONE || MACOS) ? 'caf' : 'mp3'));
|
||||
return addq(url, 'th=' + r.ac2);
|
||||
};
|
||||
|
||||
r.set_ac2 = function () {
|
||||
r.init_ac2(this.getAttribute('id').split('ac2')[1]);
|
||||
};
|
||||
|
||||
r.init_ac2 = function (v) {
|
||||
if (!window.have_acode) {
|
||||
r.ac2 = 'opus';
|
||||
return;
|
||||
}
|
||||
|
||||
var dv = can_ogg ? 'opus' : can_caf ? 'caf' : 'mp3',
|
||||
fmts = ['opus', 'owa', 'caf', 'mp3'],
|
||||
btns = [];
|
||||
|
||||
if (v === dv)
|
||||
toast.ok(5, L.mt_c2ok);
|
||||
else if (v)
|
||||
toast.inf(10, L.mt_c2nd);
|
||||
|
||||
if ((v == 'opus' && !can_ogg) ||
|
||||
(v == 'caf' && !can_caf) ||
|
||||
(v == 'owa' && !can_owa))
|
||||
toast.warn(15, L.mt_c2ng);
|
||||
|
||||
if (v == 'owa' && IPHONE)
|
||||
toast.err(30, L.mt_xowa);
|
||||
|
||||
for (var a = 0; a < fmts.length; a++) {
|
||||
var btn = ebi('ac2' + fmts[a]);
|
||||
if (!btn)
|
||||
return console.log('!btn', fmts[a]);
|
||||
btn.onclick = r.set_ac2;
|
||||
btns.push(btn);
|
||||
}
|
||||
if (!IPHONE)
|
||||
btns[1].style.display = btns[2].style.display = 'none';
|
||||
|
||||
if (v)
|
||||
swrite('acode2', v);
|
||||
else
|
||||
v = dv;
|
||||
|
||||
v = sread('acode2', fmts) || v;
|
||||
for (var a = 0; a < fmts.length; a++)
|
||||
clmod(btns[a], 'on', fmts[a] == v)
|
||||
|
||||
r.ac2 = v;
|
||||
};
|
||||
|
||||
r.pp = function () {
|
||||
@@ -2426,6 +2565,7 @@ var mpl = (function () {
|
||||
r.unbuffer = function (url) {
|
||||
if (mp.au2 && (!url || mp.au2.rsrc == url)) {
|
||||
mp.au2.src = mp.au2.rsrc = '';
|
||||
mp.au2.ld = 0; //owa
|
||||
mp.au2.load();
|
||||
}
|
||||
if (!url)
|
||||
@@ -2436,11 +2576,22 @@ var mpl = (function () {
|
||||
})();
|
||||
|
||||
|
||||
var can_ogg = true;
|
||||
var za,
|
||||
can_ogg = true,
|
||||
can_owa = false,
|
||||
can_caf = IPHONE && !/ OS ([1-9]|1[01])_/.test(UA);
|
||||
try {
|
||||
can_ogg = new Audio().canPlayType('audio/ogg; codecs=opus') === 'probably';
|
||||
za = new Audio();
|
||||
can_ogg = za.canPlayType('audio/ogg; codecs=opus') === 'probably';
|
||||
can_owa = za.canPlayType('audio/webm; codecs=opus') === 'probably';
|
||||
}
|
||||
catch (ex) { }
|
||||
za = null;
|
||||
|
||||
if (can_owa && IPHONE && / OS ([1-9]|1[0-7])_/.test(UA))
|
||||
can_owa = false;
|
||||
|
||||
mpl.init_ac2();
|
||||
|
||||
|
||||
var re_au_native = (can_ogg || have_acode) ? /\.(aac|flac|m4a|mp3|ogg|opus|wav)$/i : /\.(aac|flac|m4a|mp3|wav)$/i,
|
||||
@@ -2613,7 +2764,8 @@ function MPlayer() {
|
||||
});
|
||||
|
||||
r.nopause();
|
||||
r.au2.onloadeddata = r.au2.onloadedmetadata = r.nopause;
|
||||
r.au2.ld = 0; //owa
|
||||
r.au2.onloadeddata = r.au2.onloadedmetadata = r.onpreload;
|
||||
r.au2.preload = "auto";
|
||||
r.au2.src = r.au2.rsrc = url;
|
||||
|
||||
@@ -2628,6 +2780,11 @@ function MPlayer() {
|
||||
r.cd_pause = Date.now();
|
||||
};
|
||||
|
||||
r.onpreload = function () {
|
||||
r.nopause();
|
||||
this.ld++;
|
||||
};
|
||||
|
||||
r.init_fau = function () {
|
||||
if (r.fau || !mpl.fau)
|
||||
return;
|
||||
@@ -2928,7 +3085,7 @@ var pbar = (function () {
|
||||
return;
|
||||
|
||||
pctx.fillStyle = light ? 'rgba(0,0,0,0.5)' : 'rgba(255,255,255,0.5)';
|
||||
var m = /[?&]th=(opus|caf|mp3)/.exec('' + mp.au.rsrc),
|
||||
var m = /[?&]th=(opus|owa|caf|mp3)/.exec('' + mp.au.rsrc),
|
||||
txt = mp.au.ded ? L.mm_playerr.replace(':', ' ;_;') :
|
||||
m ? L.mm_bconv.format(m[1]) : L.mm_bload;
|
||||
|
||||
@@ -3007,6 +3164,7 @@ var vbar = (function () {
|
||||
can = r.can.can;
|
||||
ctx = r.can.ctx;
|
||||
ctx.font = '.7em sans-serif';
|
||||
ctx.fontVariantCaps = 'small-caps';
|
||||
w = r.can.w;
|
||||
h = r.can.h;
|
||||
r.draw();
|
||||
@@ -3032,18 +3190,18 @@ var vbar = (function () {
|
||||
ctx.fillStyle = grad2; ctx.fillRect(0, 0, w, h);
|
||||
ctx.fillStyle = grad1; ctx.fillRect(0, 0, w * mp.vol, h);
|
||||
|
||||
if (Date.now() - lastv > 1000)
|
||||
return;
|
||||
var vt = 'volume ' + Math.floor(mp.vol * 100),
|
||||
tw = ctx.measureText(vt).width,
|
||||
x = w * mp.vol - tw - 8,
|
||||
li = dy;
|
||||
|
||||
var vt = Math.floor(mp.vol * 100),
|
||||
tw = ctx.measureText(vt).width;
|
||||
|
||||
var li = dy;
|
||||
if (mp.vol < 0.05)
|
||||
if (mp.vol < 0.5) {
|
||||
x += tw + 16;
|
||||
li = !li;
|
||||
}
|
||||
|
||||
ctx.fillStyle = li ? '#fff' : '#210';
|
||||
ctx.fillText(vt, Math.max(4, w * mp.vol - tw - 8), h / 3 * 2);
|
||||
ctx.fillText(vt, x, h / 3 * 2);
|
||||
|
||||
clearTimeout(untext);
|
||||
untext = setTimeout(r.draw, 1000);
|
||||
@@ -3883,16 +4041,20 @@ function play(tid, is_ev, seek) {
|
||||
mp.au = mp.au2;
|
||||
mp.au2 = t;
|
||||
t.onerror = t.onprogress = t.onended = null;
|
||||
t.ld = 0; //owa
|
||||
mp.au.onerror = evau_error;
|
||||
mp.au.onprogress = pbar.drawpos;
|
||||
mp.au.onplaying = mpui.progress_updater;
|
||||
mp.au.onloadeddata = mp.au.onloadedmetadata = mp.nopause;
|
||||
mp.au.onended = next_song;
|
||||
t = mp.au.currentTime;
|
||||
if (isNum(t) && t > 0.1)
|
||||
mp.au.currentTime = 0;
|
||||
}
|
||||
else
|
||||
else {
|
||||
console.log('get ' + url.split('/').pop());
|
||||
mp.au.src = mp.au.rsrc = url;
|
||||
}
|
||||
|
||||
mp.au.osrc = mp.tracks[tid];
|
||||
afilt.apply();
|
||||
@@ -3935,7 +4097,7 @@ function play(tid, is_ev, seek) {
|
||||
var o = ebi(oid);
|
||||
o.setAttribute('id', 'thx_js');
|
||||
if (mpl.aplay)
|
||||
sethash(oid);
|
||||
sethash(oid + getsort());
|
||||
o.setAttribute('id', oid);
|
||||
}
|
||||
|
||||
@@ -3978,6 +4140,14 @@ function evau_error(e) {
|
||||
err = L.mm_eabrt;
|
||||
break;
|
||||
case eplaya.error.MEDIA_ERR_NETWORK:
|
||||
if (IPHONE && eplaya.ld === 1 && mpl.ac2 == 'owa' && !eplaya.paused && !eplaya.currentTime) {
|
||||
eplaya.ded = 0;
|
||||
if (!mpl.owaw) {
|
||||
mpl.owaw = 1;
|
||||
console.log('ignored iOS bug; spurious error sent in parallel with preloaded songs starting to play just fine');
|
||||
}
|
||||
return;
|
||||
}
|
||||
err = L.mm_enet;
|
||||
break;
|
||||
case eplaya.error.MEDIA_ERR_DECODE:
|
||||
@@ -4138,6 +4308,12 @@ function eval_hash() {
|
||||
if (!im)
|
||||
return toast.warn(10, L.im_hnf);
|
||||
|
||||
if (thegrid.sel)
|
||||
setTimeout(function () {
|
||||
thegrid.sel = true;
|
||||
}, 1);
|
||||
|
||||
thegrid.sel = false;
|
||||
im.click();
|
||||
im.scrollIntoView();
|
||||
}, 50);
|
||||
@@ -4204,10 +4380,17 @@ function eval_hash() {
|
||||
}
|
||||
bcfg_bind(props, 'mcmp', 'au_compact', false, setacmp);
|
||||
setacmp();
|
||||
|
||||
// toggle bup checksums
|
||||
ebi('uput').onchange = function() {
|
||||
QS('#op_bup input[name="act"]').value = this.checked ? 'uput' : 'bput';
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
function read_dsort(txt) {
|
||||
dnsort = dnsort ? 1 : 0;
|
||||
clmod(ebi('nsort'), 'on', (sread('nsort') || dnsort) == 1);
|
||||
try {
|
||||
var zt = (('' + txt).trim() || 'href').split(/,+/g);
|
||||
dsort = [];
|
||||
@@ -4231,14 +4414,29 @@ function read_dsort(txt) {
|
||||
read_dsort(dsort);
|
||||
|
||||
|
||||
function getsort() {
|
||||
var ret = '',
|
||||
sopts = jread('fsort');
|
||||
|
||||
sopts = sopts && sopts.length ? sopts : dsort;
|
||||
|
||||
for (var a = 0; a < Math.min(hsortn, sopts.length); a++)
|
||||
ret += ',sort' + (sopts[a][1] < 0 ? '-' : '') + sopts[a][0];
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
function sortfiles(nodes) {
|
||||
if (!nodes.length)
|
||||
return nodes;
|
||||
|
||||
var sopts = jread('fsort', jcp(dsort)),
|
||||
var sopts = jread('fsort'),
|
||||
dir1st = sread('dir1st') !== '0';
|
||||
|
||||
var collator = sread('nsort') != 1 ? null :
|
||||
sopts = sopts && sopts.length ? sopts : jcp(dsort);
|
||||
|
||||
var collator = !clgot(ebi('nsort'), 'on') ? null :
|
||||
new Intl.Collator([], {numeric: true});
|
||||
|
||||
try {
|
||||
@@ -4256,6 +4454,8 @@ function sortfiles(nodes) {
|
||||
if (!name)
|
||||
continue;
|
||||
|
||||
name = name.toLowerCase();
|
||||
|
||||
if (name == 'ts')
|
||||
typ = 'int';
|
||||
|
||||
@@ -4664,8 +4864,8 @@ var fileman = (function () {
|
||||
toast.err(9, msg);
|
||||
return;
|
||||
}
|
||||
surl = surl.slice(15);
|
||||
var txt = esc(surl) + '<img class="b64" src="' + surl + '?qr" />';
|
||||
surl = surl.slice(15).trim();
|
||||
var txt = esc(surl) + '<img class="b64" width="100" height="100" src="' + surl + '?qr" />';
|
||||
modal.confirm(txt + L.fs_ok, function() {
|
||||
cliptxt(surl, function () {
|
||||
toast.ok(2, L.clipped);
|
||||
@@ -5293,7 +5493,7 @@ var fileman = (function () {
|
||||
if (!t.dst)
|
||||
return paster();
|
||||
|
||||
toast.show('inf r', 0, esc((r.ccp ? L.fcp_busy : L.fp_busy).format(f.length + 1, uricom_dec(f.src))));
|
||||
toast.show('inf r', 0, esc((r.ccp ? L.fcp_busy : L.fp_busy).format(f.length + 1, uricom_dec(t.src))));
|
||||
|
||||
var xhr = new XHR(),
|
||||
act = r.ccp ? '?copy=' : '?move=',
|
||||
@@ -5937,7 +6137,7 @@ var thegrid = (function () {
|
||||
fid = oth.getAttribute('id'),
|
||||
aplay = ebi('a' + fid),
|
||||
atext = ebi('t' + fid),
|
||||
is_txt = atext && showfile.getlang(href) && !/\.ts$/.test(href),
|
||||
is_txt = atext && !/\.ts$/.test(href) && showfile.getlang(href),
|
||||
is_img = img_re.test(href),
|
||||
is_dir = href.endsWith('/'),
|
||||
is_srch = !!ebi('unsearch'),
|
||||
@@ -6183,7 +6383,7 @@ var thegrid = (function () {
|
||||
esc(uricom_dec(h.split('/').pop())) + '</a>';
|
||||
},
|
||||
onChange: function (i) {
|
||||
sethash('g' + r.bbox[i].imageElement.getAttribute('ref'));
|
||||
sethash('g' + r.bbox[i].imageElement.getAttribute('ref') + getsort());
|
||||
}
|
||||
});
|
||||
r.bbox = br[0][0];
|
||||
@@ -7026,7 +7226,7 @@ var treectl = (function () {
|
||||
xhr.open('GET', SR + '/?setck=dots=' + (v ? 'y' : ''), true);
|
||||
xhr.send();
|
||||
});
|
||||
bcfg_bind(r, 'nsort', 'nsort', false, resort);
|
||||
bcfg_bind(r, 'nsort', 'nsort', dnsort, resort);
|
||||
bcfg_bind(r, 'dir1st', 'dir1st', true, resort);
|
||||
setwrap(bcfg_bind(r, 'wtree', 'wraptree', true, setwrap));
|
||||
setwrap(bcfg_bind(r, 'parpane', 'parpane', true, onscroll));
|
||||
@@ -7267,12 +7467,12 @@ var treectl = (function () {
|
||||
xhr.rst = rst;
|
||||
xhr.ts = Date.now();
|
||||
xhr.open('GET', addq(dst, 'tree=' + top + (r.dots ? '&dots' : '') + k), true);
|
||||
xhr.onload = xhr.onerror = recvtree;
|
||||
xhr.onload = xhr.onerror = r.recvtree;
|
||||
xhr.send();
|
||||
enspin('#tree');
|
||||
}
|
||||
|
||||
function recvtree() {
|
||||
r.recvtree = function () {
|
||||
if (!xhrchk(this, L.tl_xe1, L.tl_xe2))
|
||||
return;
|
||||
|
||||
@@ -7282,10 +7482,10 @@ var treectl = (function () {
|
||||
catch (ex) {
|
||||
return toast.err(30, "bad <code>?tree</code> reply;\nexpected json, got this:\n\n" + esc(this.responseText + ''));
|
||||
}
|
||||
rendertree(res, this.ts, this.top, this.dst, this.rst);
|
||||
}
|
||||
r.rendertree(res, this.ts, this.top, this.dst, this.rst);
|
||||
};
|
||||
|
||||
function rendertree(res, ts, top0, dst, rst) {
|
||||
r.rendertree = function (res, ts, top0, dst, rst) {
|
||||
var cur = ebi('treeul').getAttribute('ts');
|
||||
if (cur && parseInt(cur) > ts + 20 && QS('#treeul>li>a+a')) {
|
||||
console.log("reject tree; " + cur + " / " + (ts - cur));
|
||||
@@ -7339,7 +7539,7 @@ var treectl = (function () {
|
||||
console.log("dir_cb failed", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function reload_tree() {
|
||||
var cdir = r.nextdir || get_vpath(),
|
||||
@@ -7510,6 +7710,7 @@ var treectl = (function () {
|
||||
|
||||
try {
|
||||
var res = JSON.parse(this.responseText);
|
||||
Object.assign(res, res.cfg);
|
||||
}
|
||||
catch (ex) {
|
||||
if (r.ls_cb) {
|
||||
@@ -7532,6 +7733,7 @@ var treectl = (function () {
|
||||
if (res.files[a].tags === undefined)
|
||||
res.files[a].tags = {};
|
||||
|
||||
dnsort = res.dnsort;
|
||||
read_dsort(res.dsort);
|
||||
dcrop = res.dcrop;
|
||||
dth3x = res.dth3x;
|
||||
@@ -7559,7 +7761,7 @@ var treectl = (function () {
|
||||
dirs.push(dn);
|
||||
}
|
||||
|
||||
rendertree({ "a": dirs }, this.ts, ".", get_evpath() + (dk ? '?k=' + dk : ''));
|
||||
r.rendertree({ "a": dirs }, this.ts, ".", get_evpath() + (dk ? '?k=' + dk : ''));
|
||||
}
|
||||
|
||||
r.gentab(this.top, res);
|
||||
@@ -7577,9 +7779,9 @@ var treectl = (function () {
|
||||
if (lg1 === Ls.eng.f_empty)
|
||||
lg1 = L.f_empty;
|
||||
|
||||
sandbox(ebi('pro'), sb_lg, '', lg0);
|
||||
sandbox(ebi('pro'), sb_lg, sba_lg,'', lg0);
|
||||
if (dirchg)
|
||||
sandbox(ebi('epi'), sb_lg, '', lg1);
|
||||
sandbox(ebi('epi'), sb_lg, sba_lg, '', lg1);
|
||||
|
||||
clmod(ebi('pro'), 'mdo');
|
||||
clmod(ebi('epi'), 'mdo');
|
||||
@@ -7590,7 +7792,7 @@ var treectl = (function () {
|
||||
if (md1)
|
||||
show_readme(md1, 1);
|
||||
else if (!dirchg)
|
||||
sandbox(ebi('epi'), sb_lg, '', lg1);
|
||||
sandbox(ebi('epi'), sb_lg, sba_lg, '', lg1);
|
||||
|
||||
if (this.hpush && !this.back) {
|
||||
var ofs = ebi('wrap').offsetTop;
|
||||
@@ -7775,6 +7977,7 @@ var treectl = (function () {
|
||||
r.ls_cb = showfile.addlinks;
|
||||
return r.reqls(get_evpath(), false, undefined, true);
|
||||
}
|
||||
ls0.unlist = unlist0;
|
||||
|
||||
var top = get_evpath();
|
||||
if (r.chk_index_html(top, ls0))
|
||||
@@ -8544,8 +8747,8 @@ var arcfmt = (function () {
|
||||
["pax", "tar=pax", L.fz_pax],
|
||||
["tgz", "tar=gz", L.fz_targz],
|
||||
["txz", "tar=xz", L.fz_tarxz],
|
||||
["zip", "zip=utf8", L.fz_zip8],
|
||||
["zip_dos", "zip", L.fz_zipd],
|
||||
["zip", "zip", L.fz_zip8],
|
||||
["zip_dos", "zip=dos", L.fz_zipd],
|
||||
["zip_crc", "zip=crc", L.fz_zipc]
|
||||
];
|
||||
|
||||
@@ -8937,14 +9140,18 @@ var msel = (function () {
|
||||
function cb() {
|
||||
xhrchk(this, L.fsm_xe1, L.fsm_xe2);
|
||||
|
||||
if (this.status < 200 || this.status > 201) {
|
||||
if (this.status < 200 || this.status > 202) {
|
||||
sf.textContent = 'error: ' + hunpre(this.responseText);
|
||||
return;
|
||||
}
|
||||
|
||||
tb.value = '';
|
||||
clmod(sf, 'vis');
|
||||
sf.textContent = 'sent: "' + this.msg + '"';
|
||||
var txt = 'sent: <code>' + esc(this.msg) + '</code>';
|
||||
if (this.status == 202)
|
||||
txt += '<br /> got: <code>' + esc(this.responseText) + '</code>';
|
||||
|
||||
sf.innerHTML = txt;
|
||||
setTimeout(function () {
|
||||
treectl.goto();
|
||||
}, 100);
|
||||
@@ -9065,7 +9272,7 @@ function show_md(md, name, div, url, depth) {
|
||||
if (!have_emp)
|
||||
md_html = DOMPurify.sanitize(md_html);
|
||||
|
||||
if (sandbox(div, sb_md, 'mdo', md_html))
|
||||
if (sandbox(div, sb_md, sba_md, 'mdo', md_html))
|
||||
return;
|
||||
|
||||
ext = md_plug.post;
|
||||
@@ -9116,7 +9323,7 @@ function show_readme(md, n) {
|
||||
var tgt = ebi(n ? 'epi' : 'pro');
|
||||
|
||||
if (!treectl.ireadme)
|
||||
return sandbox(tgt, '', '', 'a');
|
||||
return sandbox(tgt, '', '', '', 'a');
|
||||
|
||||
show_md(md, n ? 'README.md' : 'PREADME.md', tgt);
|
||||
}
|
||||
@@ -9125,7 +9332,7 @@ for (var a = 0; a < readmes.length; a++)
|
||||
show_readme(readmes[a], a);
|
||||
|
||||
|
||||
function sandbox(tgt, rules, cls, html) {
|
||||
function sandbox(tgt, rules, allow, cls, html) {
|
||||
if (!treectl.ireadme) {
|
||||
tgt.innerHTML = html ? L.md_off : '';
|
||||
return;
|
||||
@@ -9181,6 +9388,7 @@ function sandbox(tgt, rules, cls, html) {
|
||||
var fr = mknod('iframe');
|
||||
fr.setAttribute('title', 'folder ' + tid + 'logue');
|
||||
fr.setAttribute('sandbox', rules ? 'allow-' + rules.replace(/ /g, ' allow-') : '');
|
||||
fr.setAttribute('allow', allow);
|
||||
fr.setAttribute('srcdoc', html);
|
||||
tgt.appendChild(fr);
|
||||
treectl.sb_msg = true;
|
||||
@@ -9230,8 +9438,8 @@ if (sb_lg && logues.length) {
|
||||
if (logues[1] === Ls.eng.f_empty)
|
||||
logues[1] = L.f_empty;
|
||||
|
||||
sandbox(ebi('pro'), sb_lg, '', logues[0]);
|
||||
sandbox(ebi('epi'), sb_lg, '', logues[1]);
|
||||
sandbox(ebi('pro'), sb_lg, sba_lg, '', logues[0]);
|
||||
sandbox(ebi('epi'), sb_lg, sba_lg, '', logues[1]);
|
||||
}
|
||||
|
||||
|
||||
@@ -9381,7 +9589,23 @@ var unpost = (function () {
|
||||
toast.ok(5, this.responseText);
|
||||
|
||||
if (!QS('#op_unpost a[me]'))
|
||||
ebi(goto_unpost());
|
||||
goto_unpost();
|
||||
|
||||
var fi = window.up2k && up2k.st.files;
|
||||
if (fi && fi.length < 9) {
|
||||
for (var a = 0; a < fi.length; a++) {
|
||||
var f = fi[a];
|
||||
if (!f.done && (f.rechecks || f.want_recheck) &&
|
||||
!has(up2k.st.todo.handshake, f) &&
|
||||
!has(up2k.st.busy.handshake, f)
|
||||
) {
|
||||
up2k.st.todo.handshake.push(f);
|
||||
up2k.ui.seth(f.n, 2, L.u_hashdone);
|
||||
up2k.ui.seth(f.n, 1, '📦 wait');
|
||||
up2k.ui.move(f.n, 'bz');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ct.onclick = function (e) {
|
||||
|
||||
@@ -128,9 +128,9 @@ write markdown (most html is 🙆 too)
|
||||
|
||||
<script>
|
||||
|
||||
var SR = {{ r|tojson }},
|
||||
var SR = "{{ r }}",
|
||||
last_modified = {{ lastmod }},
|
||||
have_emp = {{ have_emp|tojson }},
|
||||
have_emp = {{ "true" if have_emp else "false" }},
|
||||
dfavico = "{{ favico }}";
|
||||
|
||||
var md_opt = {
|
||||
|
||||
@@ -17,14 +17,13 @@ var chromedbg = function () { console.log(arguments); }
|
||||
var dbg = function () { };
|
||||
|
||||
// replace dbg with the real deal here or in the console:
|
||||
// dbg = chromedbg
|
||||
// dbg = console.log
|
||||
// dbg = chromedbg;
|
||||
// dbg = console.log;
|
||||
|
||||
|
||||
// dodge browser issues
|
||||
(function () {
|
||||
var ua = navigator.userAgent;
|
||||
if (ua.indexOf(') Gecko/') !== -1 && /Linux| Mac /.exec(ua)) {
|
||||
if (UA.indexOf(') Gecko/') !== -1 && /Linux| Mac /.exec(UA)) {
|
||||
// necessary on ff-68.7 at least
|
||||
var s = mknod('style');
|
||||
s.innerHTML = '@page { margin: .5in .6in .8in .6in; }';
|
||||
|
||||
@@ -450,7 +450,7 @@ function savechk_cb() {
|
||||
|
||||
// firefox bug: initial selection offset isn't cleared properly through js
|
||||
var ff_clearsel = (function () {
|
||||
if (navigator.userAgent.indexOf(') Gecko/') === -1)
|
||||
if (UA.indexOf(') Gecko/') === -1)
|
||||
return function () { }
|
||||
|
||||
return function () {
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
<a href="#" id="repl">π</a>
|
||||
<script>
|
||||
|
||||
var SR = {{ r|tojson }},
|
||||
var SR = "{{ r }}",
|
||||
last_modified = {{ lastmod }},
|
||||
have_emp = {{ have_emp|tojson }},
|
||||
have_emp = {{ "true" if have_emp else "false" }},
|
||||
dfavico = "{{ favico }}";
|
||||
|
||||
var md_opt = {
|
||||
|
||||
107
copyparty/web/rups.css
Normal file
107
copyparty/web/rups.css
Normal file
@@ -0,0 +1,107 @@
|
||||
html {
|
||||
color: #333;
|
||||
background: #f7f7f7;
|
||||
font-family: sans-serif;
|
||||
font-family: var(--font-main), sans-serif;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
#wrap {
|
||||
margin: 2em auto;
|
||||
padding: 0 1em 3em 1em;
|
||||
line-height: 2.3em;
|
||||
}
|
||||
a {
|
||||
color: #047;
|
||||
background: #fff;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid #8ab;
|
||||
border-radius: .2em;
|
||||
padding: .2em .6em;
|
||||
margin: 0 .3em;
|
||||
}
|
||||
#wrap td a {
|
||||
margin: 0;
|
||||
line-height: 1em;
|
||||
display: inline-block;
|
||||
white-space: initial;
|
||||
font-family: var(--font-main), sans-serif;
|
||||
}
|
||||
#repl {
|
||||
border: none;
|
||||
background: none;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
position: fixed;
|
||||
bottom: .25em;
|
||||
left: .2em;
|
||||
}
|
||||
#wrap table {
|
||||
border-collapse: collapse;
|
||||
position: relative;
|
||||
margin-top: 2em;
|
||||
}
|
||||
#wrap th {
|
||||
top: -1px;
|
||||
position: sticky;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
#wrap td {
|
||||
font-family: var(--font-mono), monospace, monospace;
|
||||
white-space: pre; /*date*/
|
||||
overflow: hidden; /*ipv6*/
|
||||
}
|
||||
#wrap th:first-child,
|
||||
#wrap td:first-child {
|
||||
text-align: right;
|
||||
}
|
||||
#wrap td,
|
||||
#wrap th {
|
||||
text-align: left;
|
||||
padding: .3em .6em;
|
||||
max-width: 30vw;
|
||||
}
|
||||
#wrap tr:hover td {
|
||||
background: #ddd;
|
||||
box-shadow: 0 -1px 0 rgba(128, 128, 128, 0.5) inset;
|
||||
}
|
||||
#wrap th:first-child,
|
||||
#wrap td:first-child {
|
||||
border-radius: .5em 0 0 .5em;
|
||||
}
|
||||
#wrap th:last-child,
|
||||
#wrap td:last-child {
|
||||
border-radius: 0 .5em .5em 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
html.z {
|
||||
background: #222;
|
||||
color: #ccc;
|
||||
}
|
||||
html.bz {
|
||||
background: #11121d;
|
||||
color: #bbd;
|
||||
}
|
||||
html.z a {
|
||||
color: #fff;
|
||||
background: #057;
|
||||
border-color: #37a;
|
||||
}
|
||||
html.z input[type=text] {
|
||||
color: #ddd;
|
||||
background: #223;
|
||||
border: none;
|
||||
border-bottom: 1px solid #fc5;
|
||||
border-radius: .2em;
|
||||
padding: .2em .3em;
|
||||
}
|
||||
html.z #wrap th {
|
||||
background: #222;
|
||||
}
|
||||
html.bz #wrap th {
|
||||
background: #223;
|
||||
}
|
||||
html.z #wrap tr:hover td {
|
||||
background: #000;
|
||||
}
|
||||
50
copyparty/web/rups.html
Normal file
50
copyparty/web/rups.html
Normal file
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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="robots" content="noindex, nofollow">
|
||||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/rups.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
{{ html_head }}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<a href="#" id="re">refresh</a>
|
||||
<a href="{{ r }}/?h">control-panel</a>
|
||||
Filter: <input type="text" id="filter" size="20" placeholder="documents/passwords" />
|
||||
<span id="hits"></span>
|
||||
<table id="tab"><thead><tr>
|
||||
<th>size</th>
|
||||
<th>who</th>
|
||||
<th>when</th>
|
||||
<th>age</th>
|
||||
<th>dir</th>
|
||||
<th>file</th>
|
||||
</tr></thead><tbody id="tb"></tbody></table>
|
||||
</div>
|
||||
<a href="#" id="repl">π</a>
|
||||
<script>
|
||||
|
||||
var SR="{{ r }}",
|
||||
lang="{{ lang }}",
|
||||
dfavico="{{ favico }}";
|
||||
|
||||
var STG = window.localStorage;
|
||||
document.documentElement.className = (STG && STG.cpp_thm) || "{{ this.args.theme }}";
|
||||
|
||||
</script>
|
||||
<script src="{{ r }}/.cpr/util.js?_={{ ts }}"></script>
|
||||
<script>var V={{ v }};</script>
|
||||
<script src="{{ r }}/.cpr/rups.js?_={{ ts }}"></script>
|
||||
{%- if js %}
|
||||
<script src="{{ js }}_={{ ts }}"></script>
|
||||
{%- endif %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
66
copyparty/web/rups.js
Normal file
66
copyparty/web/rups.js
Normal file
@@ -0,0 +1,66 @@
|
||||
function render() {
|
||||
var ups = V.ups, now = V.now, html = [];
|
||||
ebi('filter').value = V.filter;
|
||||
ebi('hits').innerHTML = 'showing ' + ups.length + ' files';
|
||||
|
||||
for (var a = 0; a < ups.length; a++) {
|
||||
var f = ups[a],
|
||||
vsp = vsplit(f.vp.split('?')[0]),
|
||||
dn = esc(uricom_dec(vsp[0])),
|
||||
fn = esc(uricom_dec(vsp[1])),
|
||||
at = f.at,
|
||||
td = now - f.at,
|
||||
ts = !at ? '(?)' : unix2iso(at),
|
||||
sa = !at ? '(?)' : td > 60 ? shumantime(td) : (td + 's'),
|
||||
sz = ('' + f.sz).replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
||||
|
||||
html.push('<tr><td>' + sz +
|
||||
'</td><td>' + f.ip +
|
||||
'</td><td>' + ts +
|
||||
'</td><td>' + sa +
|
||||
'</td><td><a href="' + vsp[0] + '">' + dn +
|
||||
'</a></td><td><a href="' + f.vp + '">' + fn +
|
||||
'</a></td></tr>');
|
||||
}
|
||||
if (!ups.length) {
|
||||
var t = V.filter ? ' matching the filter' : '';
|
||||
html = ['<tr><td colspan="6">there are no uploads' + t + '</td></tr>'];
|
||||
}
|
||||
ebi('tb').innerHTML = html.join('');
|
||||
}
|
||||
render();
|
||||
|
||||
var ti;
|
||||
function ask(e) {
|
||||
ev(e);
|
||||
clearTimeout(ti);
|
||||
ebi('hits').innerHTML = 'Loading...';
|
||||
|
||||
var xhr = new XHR(),
|
||||
filter = unsmart(ebi('filter').value);
|
||||
|
||||
hist_replace(get_evpath().split('?')[0] + '?ru&filter=' + uricom_enc(filter));
|
||||
|
||||
xhr.onload = xhr.onerror = function () {
|
||||
try {
|
||||
V = JSON.parse(this.responseText)
|
||||
}
|
||||
catch (ex) {
|
||||
ebi('tb').innerHTML = '<tr><td colspan="6">failed to decode server response as json: <pre>' + esc(this.responseText) + '</pre></td></tr>';
|
||||
return;
|
||||
}
|
||||
render();
|
||||
};
|
||||
xhr.open('GET', SR + '/?ru&j&filter=' + uricom_enc(filter), true);
|
||||
xhr.send();
|
||||
}
|
||||
ebi('re').onclick = ask;
|
||||
ebi('filter').oninput = function () {
|
||||
clearTimeout(ti);
|
||||
ti = setTimeout(ask, 500);
|
||||
ebi('hits').innerHTML = '...';
|
||||
};
|
||||
ebi('filter').onkeydown = function (e) {
|
||||
if (('' + e.key).endsWith('Enter'))
|
||||
ask();
|
||||
};
|
||||
@@ -44,9 +44,10 @@ a {
|
||||
bottom: .25em;
|
||||
left: .2em;
|
||||
}
|
||||
table {
|
||||
#wrap table {
|
||||
border-collapse: collapse;
|
||||
position: relative;
|
||||
position: relative;
|
||||
margin-top: 2em;
|
||||
}
|
||||
th {
|
||||
top: -1px;
|
||||
@@ -62,6 +63,14 @@ th {
|
||||
#wrap td+td+td+td+td+td+td+td {
|
||||
font-family: var(--font-mono), monospace, monospace;
|
||||
}
|
||||
#wrap th:first-child,
|
||||
#wrap td:first-child {
|
||||
border-radius: .5em 0 0 .5em;
|
||||
}
|
||||
#wrap th:last-child,
|
||||
#wrap td:last-child {
|
||||
border-radius: 0 .5em .5em 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -81,3 +90,6 @@ html.bz {
|
||||
color: #bbd;
|
||||
background: #11121d;
|
||||
}
|
||||
html.bz th {
|
||||
background: #223;
|
||||
}
|
||||
|
||||
@@ -6,6 +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="robots" content="noindex, nofollow">
|
||||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/shares.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
@@ -14,8 +15,8 @@
|
||||
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<a id="a" href="{{ r }}/?shares" class="af">refresh</a>
|
||||
<a id="a" href="{{ r }}/?h" class="af">control-panel</a>
|
||||
<a href="{{ r }}/?shares">refresh</a>
|
||||
<a href="{{ r }}/?h">control-panel</a>
|
||||
|
||||
<span>axs = perms (read,write,move,delet)</span>
|
||||
<span>nf = numFiles (0=dir)</span>
|
||||
@@ -58,9 +59,11 @@
|
||||
{% if not rows %}
|
||||
(you don't have any active shares btw)
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="#" id="repl">π</a>
|
||||
<script>
|
||||
|
||||
var SR = {{ r|tojson }},
|
||||
var SR="{{ r }}",
|
||||
shr="{{ shr }}",
|
||||
lang="{{ lang }}",
|
||||
dfavico="{{ favico }}";
|
||||
|
||||
@@ -45,7 +45,7 @@ function qr(e) {
|
||||
|
||||
function showqr(href) {
|
||||
var vhref = href.replace('?qr&', '?').replace('?qr', '');
|
||||
modal.alert(esc(vhref) + '<img class="b64" src="' + href + '" />');
|
||||
modal.alert(esc(vhref) + '<img class="b64" width="100" height="100" src="' + href + '" />');
|
||||
}
|
||||
|
||||
(function() {
|
||||
|
||||
@@ -94,6 +94,9 @@ table {
|
||||
.vols th:empty {
|
||||
padding: 0;
|
||||
}
|
||||
.vols img {
|
||||
margin: -4px 0;
|
||||
}
|
||||
.num {
|
||||
border-right: 1px solid #bbb;
|
||||
}
|
||||
|
||||
@@ -157,6 +157,7 @@
|
||||
<blockquote id="ad">enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!</blockquote></li>
|
||||
{% endif %}
|
||||
|
||||
<li><a id="af" href="{{ r }}/?ru">show recent uploads</a></li>
|
||||
<li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li>
|
||||
</ul>
|
||||
|
||||
@@ -167,7 +168,7 @@
|
||||
{%- endif %}
|
||||
<script>
|
||||
|
||||
var SR = {{ r|tojson }},
|
||||
var SR="{{ r }}",
|
||||
lang="{{ lang }}",
|
||||
dfavico="{{ favico }}";
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ var Ls = {
|
||||
"ac1": "skru på no304",
|
||||
"ad1": "no304 stopper all bruk av cache. Hvis ikke k304 var nok, prøv denne. Vil mangedoble dataforbruk!",
|
||||
"ae1": "utgående:",
|
||||
"af1": "vis nylig opplastede filer",
|
||||
},
|
||||
"eng": {
|
||||
"d2": "shows the state of all active threads",
|
||||
@@ -88,6 +89,7 @@ var Ls = {
|
||||
"ac1": "开启 k304",
|
||||
"ad1": "启用 no304 将禁用所有缓存;如果 k304 不够,可以尝试此选项。这将消耗大量的网络流量!", //m
|
||||
"ae1": "正在下载:", //m
|
||||
"af1": "显示最近上传的文件", //m
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/splash.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
<style>ul{padding-left:1.3em}li{margin:.4em 0}</style>
|
||||
<style>ul{padding-left:1.3em}li{margin:.4em 0}.txa{float:right;margin:0 0 0 1em}</style>
|
||||
{{ html_head }}
|
||||
</head>
|
||||
|
||||
@@ -31,15 +31,22 @@
|
||||
<br />
|
||||
<span class="os win lin mac">placeholders:</span>
|
||||
<span class="os win">
|
||||
{% if accs %}<code><b>{{ pw }}</b></code>=password, {% endif %}<code><b>W:</b></code>=mountpoint
|
||||
{% if accs %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>W:</b></code>=mountpoint
|
||||
</span>
|
||||
<span class="os lin mac">
|
||||
{% if accs %}<code><b>{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint
|
||||
{% if accs %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint
|
||||
</span>
|
||||
<a href="#" id="setpw">use real password</a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
{% if args.idp_h_usr %}
|
||||
<p style="line-height:2em"><b>WARNING:</b> this server is using IdP-based authentication, so this stuff may not work as advertised. Depending on server config, these commands can probably only be used to access areas which don't require authentication, unless you auth using any non-IdP accounts defined in the copyparty config. Please see <a href="https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients">the IdP docs</a></p>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
{% if not args.no_dav %}
|
||||
<h1>WebDAV</h1>
|
||||
|
||||
@@ -53,7 +60,6 @@
|
||||
{% if s %}
|
||||
<li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li>
|
||||
{% endif %}
|
||||
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
|
||||
<li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li>
|
||||
</ul>
|
||||
|
||||
@@ -137,7 +143,6 @@
|
||||
{% if args.ftps %}
|
||||
<li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li>
|
||||
{% endif %}
|
||||
<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 FTP client in windows instead (please dont), press <code>win+R</code> and run this command:</p>
|
||||
@@ -231,11 +236,65 @@
|
||||
|
||||
|
||||
|
||||
<div class="os win">
|
||||
<h1>ShareX</h1>
|
||||
|
||||
<p>to upload screenshots using ShareX <a href="https://github.com/ShareX/ShareX/releases/tag/v12.1.1">v12</a> or <a href="https://getsharex.com/">v15+</a>, save this as <code>copyparty.sxcu</code> and run it:</p>
|
||||
|
||||
<pre class="dl" name="copyparty.sxcu">
|
||||
{ "Name": "copyparty",
|
||||
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
|
||||
"Headers": {
|
||||
{% if accs %}"pw": "<b>{{ pw }}</b>",{% endif %}
|
||||
"accept": "url"
|
||||
},
|
||||
"DestinationType": "ImageUploader, TextUploader, FileUploader",
|
||||
"FileFormName": "f" }
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="os mac">
|
||||
<h1>ishare</h1>
|
||||
|
||||
<p>to upload screenshots using <a href="https://isharemac.app/">ishare</a>, save this as <code>copyparty.iscu</code> and run it:</p>
|
||||
|
||||
<pre class="dl" name="copyparty.iscu">
|
||||
{ "Name": "copyparty",
|
||||
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
|
||||
"Headers": {
|
||||
{% if accs %}"pw": "<b>{{ pw }}</b>",{% endif %}
|
||||
"accept": "json"
|
||||
},
|
||||
"ResponseURL": "{{ '{{fileurl}}' }}",
|
||||
"FileFormName": "f" }
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="os lin">
|
||||
<h1>flameshot</h1>
|
||||
|
||||
<p>to upload screenshots using <a href="https://flameshot.org/">flameshot</a>, save this as <code>flameshot.sh</code> and run it:</p>
|
||||
|
||||
<pre class="dl" name="flameshot.sh">
|
||||
#!/bin/bash
|
||||
pw="<b>{{ pw }}</b>"
|
||||
url="http{{ s }}://{{ ep }}/{{ rvp }}"
|
||||
filename="$(date +%Y-%m%d-%H%M%S).png"
|
||||
flameshot gui -s -r | curl -sT- "$url$filename?want=url&pw=$pw" | xsel -ib
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<a href="#" id="repl">π</a>
|
||||
<script>
|
||||
|
||||
var SR = {{ r|tojson }},
|
||||
var SR="{{ r }}",
|
||||
lang="{{ lang }}",
|
||||
dfavico="{{ favico }}";
|
||||
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
function QSA(x) {
|
||||
return document.querySelectorAll(x);
|
||||
}
|
||||
var LINUX = /Linux/.test(navigator.userAgent),
|
||||
MACOS = /[^a-z]mac ?os/i.test(navigator.userAgent),
|
||||
WINDOWS = /Windows/.test(navigator.userAgent);
|
||||
|
||||
|
||||
var oa = QSA('pre');
|
||||
for (var a = 0; a < oa.length; a++) {
|
||||
var html = oa[a].innerHTML,
|
||||
@@ -15,6 +7,21 @@ for (var a = 0; a < oa.length; a++) {
|
||||
oa[a].innerHTML = html.replace(rd, '$1').replace(/[ \r\n]+$/, '').replace(/\r?\n/g, '<br />');
|
||||
}
|
||||
|
||||
function add_dls() {
|
||||
oa = QSA('pre.dl');
|
||||
for (var a = 0; a < oa.length; a++) {
|
||||
var an = 'ta' + a,
|
||||
o = ebi(an) || mknod('a', an, 'download');
|
||||
|
||||
oa[a].setAttribute('id', 'tx' + a);
|
||||
oa[a].parentNode.insertBefore(o, oa[a]);
|
||||
o.setAttribute('download', oa[a].getAttribute('name'));
|
||||
o.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(oa[a].innerText));
|
||||
clmod(o, 'txa', 1);
|
||||
}
|
||||
}
|
||||
add_dls();
|
||||
|
||||
|
||||
oa = QSA('.ossel a');
|
||||
for (var a = 0; a < oa.length; a++)
|
||||
@@ -40,3 +47,21 @@ function setos(os) {
|
||||
}
|
||||
|
||||
setos(WINDOWS ? 'win' : LINUX ? 'lin' : MACOS ? 'mac' : 'idk');
|
||||
|
||||
|
||||
ebi('setpw').onclick = function (e) {
|
||||
ev(e);
|
||||
modal.prompt('password:', '', function (v) {
|
||||
if (!v)
|
||||
return;
|
||||
|
||||
var pw0 = ebi('pw0').innerHTML,
|
||||
oa = QSA('b');
|
||||
|
||||
for (var a = 0; a < oa.length; a++)
|
||||
if (oa[a].innerHTML == pw0)
|
||||
oa[a].textContent = v;
|
||||
|
||||
add_dls();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -322,6 +322,8 @@ html.y #tth {
|
||||
margin: .1em auto;
|
||||
width: 60%;
|
||||
height: 60%;
|
||||
background: #999;
|
||||
background: rgba(128,128,128,0.2);
|
||||
}
|
||||
#modalb {
|
||||
position: sticky;
|
||||
|
||||
@@ -695,8 +695,9 @@ function Donut(uc, st) {
|
||||
}
|
||||
|
||||
if (++r.tc >= 10) {
|
||||
var s = r.eta === null ? 'paused' : r.eta > 60 ? shumantime(r.eta) : (r.eta + 's');
|
||||
wintitle("{0}%, {1}, #{2}, ".format(
|
||||
f2f(v * 100 / t, 1), shumantime(r.eta), st.files.length - st.nfile.upload), true);
|
||||
f2f(v * 100 / t, 1), s, st.files.length - st.nfile.upload), true);
|
||||
r.tc = 0;
|
||||
}
|
||||
|
||||
@@ -880,7 +881,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', !!WebAssembly && (!subtle || !CHROME || MOBILE || VCHROME >= 107), set_hashw);
|
||||
bcfg_bind(uc, 'hashw', 'hashw', !!WebAssembly && !(CHROME && MOBILE) && (!subtle || !CHROME), set_hashw);
|
||||
bcfg_bind(uc, 'upnag', 'upnag', false, set_upnag);
|
||||
bcfg_bind(uc, 'upsfx', 'upsfx', false, set_upsfx);
|
||||
|
||||
@@ -968,7 +969,7 @@ function up2k_init(subtle) {
|
||||
ud = function () { ebi('dir' + fdom_ctr).click(); };
|
||||
|
||||
// too buggy on chrome <= 72
|
||||
var m = / Chrome\/([0-9]+)\./.exec(navigator.userAgent);
|
||||
var m = / Chrome\/([0-9]+)\./.exec(UA);
|
||||
if (m && parseInt(m[1]) < 73)
|
||||
return uf();
|
||||
|
||||
@@ -1359,7 +1360,15 @@ function up2k_init(subtle) {
|
||||
draw_each = good_files.length < 50;
|
||||
|
||||
if (WebAssembly && !hws.length) {
|
||||
for (var a = 0; a < Math.min(navigator.hardwareConcurrency || 4, 16); a++)
|
||||
var nw = Math.min(navigator.hardwareConcurrency || 4, 16);
|
||||
|
||||
if (CHROME) {
|
||||
// chrome-bug 383568268 // #124
|
||||
nw = Math.max(1, (nw > 4 ? 4 : (nw - 1)));
|
||||
nw = (subtle && !MOBILE && nw > 2) ? 2 : nw;
|
||||
}
|
||||
|
||||
for (var a = 0; a < nw; a++)
|
||||
hws.push(new Worker(SR + '/.cpr/w.hash.js?_=' + TS));
|
||||
|
||||
if (!subtle)
|
||||
@@ -1556,8 +1565,10 @@ function up2k_init(subtle) {
|
||||
if (nhash) {
|
||||
st.time.hashing += td;
|
||||
t.push(['u2etah', st.bytes.hashed, st.bytes.hashed, st.time.hashing]);
|
||||
if (uc.fsearch)
|
||||
if (uc.fsearch) {
|
||||
st.time.busy += td;
|
||||
t.push(['u2etat', st.bytes.hashed, st.bytes.hashed, st.time.hashing]);
|
||||
}
|
||||
}
|
||||
|
||||
var b_up = st.bytes.inflight + st.bytes.uploaded,
|
||||
@@ -1961,32 +1972,84 @@ function up2k_init(subtle) {
|
||||
nchunk = 0,
|
||||
chunksize = get_chunksize(t.size),
|
||||
nchunks = Math.ceil(t.size / chunksize),
|
||||
csz_mib = chunksize / 1048576,
|
||||
tread = t.t_hashing,
|
||||
cache_buf = null,
|
||||
cache_car = 0,
|
||||
cache_cdr = 0,
|
||||
hashers = 0,
|
||||
hashtab = {};
|
||||
|
||||
// resolving subtle.digest w/o worker takes 1sec on blur if the actx hack breaks
|
||||
var use_workers = hws.length && !hws_ng && uc.hashw && (nchunks > 1 || document.visibilityState == 'hidden'),
|
||||
hash_par = (!subtle && !use_workers) ? 0 : csz_mib < 48 ? 2 : csz_mib < 96 ? 1 : 0;
|
||||
|
||||
pvis.setab(t.n, nchunks);
|
||||
pvis.move(t.n, 'bz');
|
||||
|
||||
if (hws.length && !hws_ng && uc.hashw && (nchunks > 1 || document.visibilityState == 'hidden'))
|
||||
// resolving subtle.digest w/o worker takes 1sec on blur if the actx hack breaks
|
||||
if (use_workers)
|
||||
return wexec_hash(t, chunksize, nchunks);
|
||||
|
||||
var segm_next = function () {
|
||||
if (nchunk >= nchunks || bpend)
|
||||
return false;
|
||||
|
||||
var reader = new FileReader(),
|
||||
nch = nchunk++,
|
||||
var nch = nchunk++,
|
||||
car = nch * chunksize,
|
||||
cdr = Math.min(chunksize + car, t.size);
|
||||
|
||||
st.bytes.hashed += cdr - car;
|
||||
st.etac.h++;
|
||||
|
||||
var orz = function (e) {
|
||||
bpend--;
|
||||
segm_next();
|
||||
hash_calc(nch, e.target.result);
|
||||
if (MOBILE && CHROME && st.slow_io === null && nch == 1 && cdr - car >= 1024 * 512) {
|
||||
var spd = Math.floor((cdr - car) / (Date.now() + 1 - tread));
|
||||
st.slow_io = spd < 40 * 1024;
|
||||
console.log('spd {0}, slow: {1}'.format(spd, st.slow_io));
|
||||
}
|
||||
|
||||
if (cdr <= cache_cdr && car >= cache_car) {
|
||||
try {
|
||||
var ofs = car - cache_car,
|
||||
ofs2 = ofs + (cdr - car),
|
||||
buf = cache_buf.subarray(ofs, ofs2);
|
||||
|
||||
hash_calc(nch, buf);
|
||||
}
|
||||
catch (ex) {
|
||||
vis_exh(ex + '', 'up2k.js', '', '', ex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var reader = new FileReader(),
|
||||
fr_cdr = cdr;
|
||||
|
||||
if (st.slow_io) {
|
||||
var step = cdr - car,
|
||||
tgt = 48 * 1048576;
|
||||
|
||||
while (step && fr_cdr - car < tgt)
|
||||
fr_cdr += step;
|
||||
if (fr_cdr - car > tgt && fr_cdr > cdr)
|
||||
fr_cdr -= step;
|
||||
if (fr_cdr > t.size)
|
||||
fr_cdr = t.size;
|
||||
}
|
||||
|
||||
var orz = function (e) {
|
||||
bpend = 0;
|
||||
var buf = e.target.result;
|
||||
if (fr_cdr > cdr) {
|
||||
cache_buf = new Uint8Array(buf);
|
||||
cache_car = car;
|
||||
cache_cdr = fr_cdr;
|
||||
buf = cache_buf.subarray(0, cdr - car);
|
||||
}
|
||||
if (hashers < hash_par)
|
||||
segm_next();
|
||||
|
||||
hash_calc(nch, buf);
|
||||
};
|
||||
reader.onload = function (e) {
|
||||
try { orz(e); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); }
|
||||
};
|
||||
@@ -2013,17 +2076,20 @@ function up2k_init(subtle) {
|
||||
|
||||
toast.err(0, 'y o u b r o k e i t\nfile: ' + esc(t.name + '') + '\nerror: ' + err);
|
||||
};
|
||||
bpend++;
|
||||
reader.readAsArrayBuffer(t.fobj.slice(car, cdr));
|
||||
bpend = 1;
|
||||
tread = Date.now();
|
||||
reader.readAsArrayBuffer(t.fobj.slice(car, fr_cdr));
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
var hash_calc = function (nch, buf) {
|
||||
hashers++;
|
||||
var orz = function (hashbuf) {
|
||||
var hslice = new Uint8Array(hashbuf).subarray(0, 33),
|
||||
b64str = buf2b64(hslice);
|
||||
|
||||
hashers--;
|
||||
hashtab[nch] = b64str;
|
||||
t.hash.push(nch);
|
||||
pvis.hashed(t);
|
||||
@@ -2406,6 +2472,9 @@ function up2k_init(subtle) {
|
||||
msg = 'done';
|
||||
|
||||
if (t.postlist.length) {
|
||||
if (t.rechecks && QS('#opa_del.act'))
|
||||
toast.inf(30, L.u_started, L.u_unpt);
|
||||
|
||||
var arr = st.todo.upload,
|
||||
sort = arr.length && arr[arr.length - 1].nfile > t.n;
|
||||
|
||||
@@ -2516,8 +2585,13 @@ function up2k_init(subtle) {
|
||||
if (!t.rechecks && (err_pend || err_srcb)) {
|
||||
t.rechecks = 0;
|
||||
t.want_recheck = true;
|
||||
err = L.u_dupdefer;
|
||||
cls = 'defer';
|
||||
if (st.busy.upload.length || st.busy.handshake.length || st.bytes.uploaded) {
|
||||
err = L.u_dupdefer;
|
||||
cls = 'defer';
|
||||
}
|
||||
}
|
||||
if (err_pend) {
|
||||
err += ' <a href="#" onclick="toast.inf(60, L.ue_ab);" class="fsearch_explain">(' + L.u_expl + ')</a>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3025,7 +3099,7 @@ function up2k_init(subtle) {
|
||||
new_state = false;
|
||||
fixed = true;
|
||||
}
|
||||
if (new_state === undefined)
|
||||
if (new_state === undefined && preferred === undefined)
|
||||
new_state = can_write ? false : have_up2k_idx ? true : undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,17 @@ if (!window.console || !console.log)
|
||||
"log": function (msg) { }
|
||||
};
|
||||
|
||||
if (!Object.assign)
|
||||
Object.assign = function (a, b) {
|
||||
for (var k in b)
|
||||
a[k] = b[k];
|
||||
};
|
||||
|
||||
if (window.CGV1)
|
||||
Object.assign(window, window.CGV1);
|
||||
|
||||
if (window.CGV)
|
||||
for (var k in CGV)
|
||||
window[k] = CGV[k];
|
||||
Object.assign(window, window.CGV);
|
||||
|
||||
|
||||
var wah = '',
|
||||
@@ -22,14 +29,15 @@ var wah = '',
|
||||
HTTPS = ('' + location).indexOf('https:') === 0,
|
||||
TOUCH = 'ontouchstart' in window,
|
||||
MOBILE = TOUCH,
|
||||
CHROME = !!window.chrome,
|
||||
CHROME = !!window.chrome, // safari=false
|
||||
VCHROME = CHROME ? 1 : 0,
|
||||
IE = /Trident\//.test(navigator.userAgent),
|
||||
FIREFOX = ('netscape' in window) && / rv:/.test(navigator.userAgent),
|
||||
IPHONE = TOUCH && /iPhone|iPad|iPod/i.test(navigator.userAgent),
|
||||
LINUX = /Linux/.test(navigator.userAgent),
|
||||
MACOS = /[^a-z]mac ?os/i.test(navigator.userAgent),
|
||||
WINDOWS = /Windows/.test(navigator.userAgent);
|
||||
UA = '' + navigator.userAgent,
|
||||
IE = /Trident\//.test(UA),
|
||||
FIREFOX = ('netscape' in window) && / rv:/.test(UA),
|
||||
IPHONE = TOUCH && /iPhone|iPad|iPod/i.test(UA),
|
||||
LINUX = /Linux/.test(UA),
|
||||
MACOS = /Macintosh/.test(UA),
|
||||
WINDOWS = /Windows/.test(UA);
|
||||
|
||||
if (!window.WebAssembly || !WebAssembly.Memory)
|
||||
window.WebAssembly = false;
|
||||
@@ -189,7 +197,7 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
|
||||
'<p style="font-size:1.3em;margin:0;line-height:2em">try to <a href="#" onclick="localStorage.clear();location.reload();">reset copyparty settings</a> if you are stuck here, or <a href="#" onclick="ignex();">ignore this</a> / <a href="#" onclick="ignex(true);">ignore all</a> / <a href="?b=u">basic</a></p>',
|
||||
'<p style="color:#fff">please send me a screenshot arigathanks gozaimuch: <a href="<ghi>" target="_blank">new github issue</a></p>',
|
||||
'<p class="b">' + esc(url + ' @' + lineNo + ':' + columnNo), '<br />' + esc(msg).replace(/\n/g, '<br />') + '</p>',
|
||||
'<p><b>UA:</b> ' + esc(navigator.userAgent + '')
|
||||
'<p><b>UA:</b> ' + esc(UA)
|
||||
];
|
||||
|
||||
try {
|
||||
@@ -571,7 +579,9 @@ function yscroll() {
|
||||
|
||||
function showsort(tab) {
|
||||
var v, vn, v1, v2, th = tab.tHead,
|
||||
sopts = jread('fsort', jcp(dsort));
|
||||
sopts = jread('fsort');
|
||||
|
||||
sopts = sopts && sopts.length ? sopts : dsort;
|
||||
|
||||
th && (th = th.rows[0]) && (th = th.cells);
|
||||
|
||||
@@ -608,10 +618,13 @@ function sortTable(table, col, cb) {
|
||||
tr = Array.prototype.slice.call(tb.rows, 0),
|
||||
i, reverse = /s0[^r]/.exec(th[col].className + ' ') ? -1 : 1;
|
||||
|
||||
var stype = th[col].getAttribute('sort');
|
||||
var kname = th[col].getAttribute('name'),
|
||||
stype = th[col].getAttribute('sort');
|
||||
try {
|
||||
var nrules = [], rules = jread("fsort", []);
|
||||
rules.unshift([th[col].getAttribute('name'), reverse, stype || '']);
|
||||
var nrules = [],
|
||||
rules = kname == 'href' ? [] : jread("fsort", []);
|
||||
|
||||
rules.unshift([kname, reverse, stype || '']);
|
||||
for (var a = 0; a < rules.length; a++) {
|
||||
var add = true;
|
||||
for (var b = 0; b < a; b++)
|
||||
@@ -874,6 +887,11 @@ if (window.Number && Number.isFinite)
|
||||
|
||||
function f2f(val, nd) {
|
||||
// 10.toFixed(1) returns 10.00 for certain values of 10
|
||||
if (!isNum(val)) {
|
||||
val = parseFloat(val);
|
||||
if (!isNum(val))
|
||||
val = 999;
|
||||
}
|
||||
val = (val * Math.pow(10, nd)).toFixed(0).split('.')[0];
|
||||
return nd ? (val.slice(0, -nd) || '0') + '.' + val.slice(-nd) : val;
|
||||
}
|
||||
@@ -970,11 +988,33 @@ function apop(arr, v) {
|
||||
}
|
||||
|
||||
|
||||
function jcp(obj) {
|
||||
function jcp1(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
|
||||
function jcp2(src) {
|
||||
if (Array.isArray(src)) {
|
||||
var ret = [];
|
||||
for (var a = 0; a < src.length; ++a) {
|
||||
var sub = src[a];
|
||||
ret.push((sub === null) ? sub : (sub instanceof Date) ? new Date(sub.valueOf()) : (typeof sub === 'object') ? jcp2(sub) : sub);
|
||||
}
|
||||
} else {
|
||||
var ret = {};
|
||||
for (var key in src) {
|
||||
var sub = src[key];
|
||||
ret[key] = sub === null ? sub : (sub instanceof Date) ? new Date(sub.valueOf()) : (typeof sub === 'object') ? jcp2(sub) : sub;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
||||
// jcp1 50% faster on android-chrome, jcp2 7x everywhere else
|
||||
var jcp = MOBILE && CHROME ? jcp1 : jcp2;
|
||||
|
||||
|
||||
function sdrop(key) {
|
||||
try {
|
||||
STG.removeItem(key);
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
## [`changelog.md`](changelog.md)
|
||||
* occasionally grabbed from github release notes
|
||||
|
||||
## [`synology-dsm.md`](synology-dsm.md)
|
||||
* running copyparty on a synology nas
|
||||
|
||||
## [`devnotes.md`](devnotes.md)
|
||||
* technical stuff
|
||||
|
||||
|
||||
@@ -1,3 +1,327 @@
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0125-1809 `v1.16.10` iOS9 is fine too
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* support audio playback on *really old* apple devices c9eba39e
|
||||
* will now transcode to mp3 when necessary, since iOS didn't support opus-in-caf before iOS 11
|
||||
* support audio playback on *future* apple devices 28c9de3f 95390b65
|
||||
* iOS 17.5 introduced support for opus-in-weba (like webp just audio instead) and, unlike caf, this intentionally supports vbr-opus (awesome)
|
||||
* ...but the current code in iOS is too buggy, so this new format is default-disabled and we'll stick to caf for now fff38f48
|
||||
* ZeroMQ event-hooks can reject uploads 3a5c1d9f
|
||||
* see [the example zmq listener](https://github.com/9001/copyparty/blob/1dace720/bin/zmq-recv.py#L26-L28)
|
||||
* chat with ZeroMQ event-hooks from javascript cdd3b67a
|
||||
* replies from ZMQ REP servers are included in the msg-to-log responses
|
||||
* which makes [this joke](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/usb-eject.py) possible f38c7543
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* nope
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* option to restrict the recent-uploads listing to admins-only b8b5214f
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0122-2326 `v1.16.9` ZeroMQ says hello
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* event-hooks can send zeromq / zmq / 0mq messages; see [readme](https://github.com/9001/copyparty#zeromq) or `--help-hooks` for examples d9db1534
|
||||
* new volflags to specify the [allow-tag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy#iframes) of the markdown/logue sandbox, to allow fullscreen and such (see `--help-flags`) 6a0aaaf0
|
||||
* new volflag `nosparse` for possibly-better performance in very rare and specific scenarios 917380dd
|
||||
* only enable this if you're uploading to s3 or something like that, and do plenty of benchmarking to make sure that it actually improved performance instead of making it worse
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* restrict max-length of filekeys to 72 characters e0cac6fd
|
||||
* the hash-calculator mode of the commandline uploader produced incorrect whole-file hashes 4c04798a
|
||||
* each chunk (`--chs`) was okay, but the final sum was not
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* selftest the xml-parser on startup with malicious xml b2e8bf6e
|
||||
* just in case a future python-version suddenly makes it unsafe somehow
|
||||
* disable some features if a dangerously misconfigured reverseproxy is detected 3f84b0a0
|
||||
* the download-as-zip feature now defaults to utf8 filenames 1231ce19
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0111-1611 `v1.16.8` android boost
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* 10x faster file hashing in android-chrome ec507889
|
||||
* on a recent pixel, speed went from 13 to 139 MiB/s
|
||||
* android's sandboxing makes small reads expensive, so do bigger reads instead
|
||||
* so the browser-tab will use more RAM on android now, maybe around 200 MiB
|
||||
* this only affects chrome-based browsers on android, not firefox
|
||||
* PUT/multipart uploads: request-header `Accept: json` makes it return json instead of html, just like `?j` ce0e5be4
|
||||
* add config examples for [ishare](https://isharemac.app/), a MacOS screenshot utility inspired by ShareX 0c0d6b2b
|
||||
* also includes a bug-workaround for [ishare#107](https://github.com/castdrian/ishare/issues/107) - copyparty will now include a toplevel json property `fileurl` in the response if exactly one file was uploaded
|
||||
* the [connect-page](https://a.ocv.me/?hc) generates an appropriate `copyparty.iscu` for ishare; [it looks like this](https://github.com/user-attachments/assets/820730ad-2319-4912-8eb2-733755a4cf54)
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* fix a potential upload deadlock when...
|
||||
* ...the database (`-e2d`) is **not** enabled for any volume, and...
|
||||
* ...either the shares feature, or user-changeable passwords, is enabled 9e542cf8
|
||||
* when loading the partial-uploads registry on startup, a cosmetic desync could occur 467acb47
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* remove some deprecated properties in partial-upload metadata aa2a8fa2
|
||||
* v1.15.7 is now the oldest version which still has any chance of reading a modern up2k.snap
|
||||
* #129 added howto: [using webdav when copyparty is behind IdP](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients) -- thanks @wuast94 !
|
||||
* added howto: [install copyparty on a synology nas](https://github.com/9001/copyparty/blob/hovudstraum/docs/synology-dsm.md) 21f93042
|
||||
* more examples in the connect-page: 278258ee fb139697
|
||||
* config-file for sharex on windows
|
||||
* config-file for ishare on macos
|
||||
* script for flameshot on linux
|
||||
* #75 add recommendation to use the [kamelåså project](https://github.com/steinuil/kameloso) instead of copyparty's [very-bad-idea.py](https://github.com/9001/copyparty/tree/hovudstraum/bin/mtag#dangerous-plugins) 9f84dc42
|
||||
* more reverse-proxy examples (haproxy, lighttpd, traefik, caddy) and improved nginx performance ac0a2da3
|
||||
* readme has a [performance comparison](https://github.com/9001/copyparty?tab=readme-ov-file#reverse-proxy-performance) -- `haproxy > caddy > traefik > nginx > apache > lighttpd`
|
||||
* copyparty.exe: updated pillow 244e952f
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-1223-0005 `v1.16.7` an idp fix for xmas
|
||||
|
||||
# ☃️🎄 **there is still time** 🎅🎁
|
||||
|
||||
❄️❄️❄️ please [enjoy some appropriate music](https://a.ocv.me/pub/demo/music/.bonus/#af-55d4554d) -- you'll probably like this more than the idp thing honestly ❄️❄️❄️
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* more improvements to the recent-uploads feature 87598dcd
|
||||
* move html rendering to clientside
|
||||
* any changes to the filter-text applies in real-time
|
||||
* loads 50% faster, reduces server-load by 30%
|
||||
* inhibits search engines from indexing it
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* using idp without e2d could mess with uploads dd6e9ea7
|
||||
* u2c (commandline uploader): fix window title 946a8c5b
|
||||
* mDNS/SSDP: fix incorrect log colors when multiple primary IPs are lost 552897ab
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* ui: make it more obvious that the volume-control is a volume-control 7f044372
|
||||
* copyparty.exe: update deps (jinja2, markupsafe, pyinstaller) c0dacbc4
|
||||
* improve safety of custom plugins 988a7223
|
||||
* if you've made your own plugins which expect certain values (host-header, filekeys) to be html-safe, then you'll want to upgrade
|
||||
* also fixes rss-feed xml if password contains special characters
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-1219-0037 `v1.16.6` merry \x58mas
|
||||
|
||||
# ☃️🎄 **it is time** 🎅🎁
|
||||
|
||||
❄️❄️❄️ please [enjoy some appropriate music](https://a.ocv.me/pub/demo/music/.bonus/#af-55d4554d) (trust me on this one, you won't regret it) ❄️❄️❄️
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* [list of recent uploads](https://a.ocv.me/?ru) eaa4b04a
|
||||
* new button in the controlpanel; can be disabled with `--no-ups-page`
|
||||
* only users with the dot-permission can see dotfiles
|
||||
* only admins can see uploader-ip and upload-times
|
||||
* enable `--ups-when` to let all users see upload-times
|
||||
* #125 log decoded request-URLs 73f7249c
|
||||
* non-ascii filenames would make the accesslog a wall of `%E5%B9%BB%E6%83%B3%E9%83%B7` so print [the decoded URL](https://github.com/user-attachments/assets/9d411183-30f3-4cb2-a880-84cf18011183) in addition to the original one, which is left as-is for debugging purposes
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* #126 improve dotfile handling 4c4e48ba
|
||||
* was impossible to delete a folder which contained hidden files if the user did not have the permission to see hidden files
|
||||
* would also affect moving, renaming, copying folders, in which case the dotfiles would not be carried over to the new location
|
||||
* now, dotfiles are always deleted, and always moved/copied into a new destination, on the condition that this is safe -- if the user has the dotfile permission in the target loocation but not in the source location, the dotfiles will be left behind to avoid accidentally making then browsable
|
||||
* ux: cosmetic eta/idle-timer fixes 01a3eb29
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* warn on ambiguous comments in config files da5ad2ab
|
||||
* avoid writing mojibake to the log 3051b131
|
||||
* use `\x`-encoding for unprintable text
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-1211-2236 `v1.16.5` 4chrome
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #124 add workaround for a chrome bug (crash during upload) 24ce46b3
|
||||
* chrome and chromium-based browsers could OOM
|
||||
* https://issues.chromium.org/issues/383568268
|
||||
|
||||
* #122 "hybrid IdP", regular users can still auth while [IdP](https://github.com/9001/copyparty#identity-providers) is enabled 64501fd7
|
||||
* previously, enabling IdP would entirely disable password-based login
|
||||
* now, password-auth is attempted for requests without a valid IdP header
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* the terminal window title would only change if `--no-ansi` was specified, which is exactly the opposite of what it should be (and now is) doing db3c0b09
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* mDNS: better log messages when several IPs are added/removed a49bf81f
|
||||
* webdeps: update dompurify 06868606
|
||||
|
||||
----
|
||||
|
||||
this release includes a build of [copyparty-winpe64.exe](https://github.com/9001/copyparty/releases/download/v1.16.5/copyparty-winpe64.exe) since the last one was [almost a year ago](https://github.com/9001/copyparty/releases/tag/v1.10.1)
|
||||
|
||||
* winpe64.exe is only for *very* specific usecases, you almost definitely *do not* want to download it, please just grab the regular [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) instead (works on all 64bit machines running win8 or newer)
|
||||
|
||||
* the only difference between winpe64.exe and [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) is that winpe64.exe works in the win7x64 PE (rescue-env), which makes it *almost* entirely useless, and every bit as dangerous to use as copyparty32.exe
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-1207-0024 `v1.16.4` ux is hard
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* improve the upload ui so it explains how to abort an unfinished upload when someone uploads to the wrong folder by accident be6afe2d
|
||||
* also reduces serverload slightly when cloning an incoming file to multiple destinations
|
||||
* u2c (commandline uploader): windows improvements 91637800
|
||||
* now supports globbing (filename wildcards) on windows
|
||||
* progressbar in the windows taskbar (requires conemu or the "new windows terminal")
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-1204-0003 `v1.16.3` 120%
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #120 add option `--srch-excl` and volflag `srch_excl` for excluding certain paths from search results 697a4fa8
|
||||
* mDNS: add workaround for https://github.com/avahi/avahi/issues/379 6c1cf68b 94d1924f
|
||||
* Avahi mDNS Reflection, sometimes used in intricate LAN setups, doesn't understand NSEC records and corrupts them
|
||||
* the workaround makes copyparty able to read the corrupted packets, but clients without a similar workaround will require either `--zm4` or `--zm6` so copyparty doesn't include the usual NSEC records
|
||||
* this is mentioned in a very loud warning in the logs when necessary
|
||||
* mDNS: option to silently ignore buggy devices instead of spamming the log with parser errors 395af051
|
||||
* webdav: support listing unmapped root with infinite recursion (Depth:0) 21a3f369
|
||||
* embed current sort config into media URLs (gallery/music) 0f257c93 4cfdc4c5 01670827
|
||||
* ensures that anyone clicking your link will see the files in the same order as you
|
||||
* can be confgured serverside (`--hsortn`, volflag `hsortn`) and clientside (`#sort` in settings)
|
||||
* URL and UI options to disable checksum calculation of PUT, bup, basic uploads c5a000d2
|
||||
* also allows [choosing either md5, sha1, sha256, or blake2](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#write) instead of the default sha512
|
||||
* can give uploads a nice speed boost when copyparty is running on a potato
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* webdav: more correct login challenge 2ce82339
|
||||
* the previous behavior could make some clients reluctant to send the password
|
||||
* #120 forget metadata of all files (including uploads) when shadowed d168b2ac
|
||||
* thanks to @Gremious for all the debugging to narrow this down!
|
||||
* #120 drop volume caches if relevant config is changed (mainly indexing filters) 2f83c6c7
|
||||
* #121 couldn't access arbitrary toplevel files from accounts with `h` permission 1f5f42f2
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* exclude thumbnails from accesslog by default 9082c470
|
||||
* filesearch: show a final summary of time-elapsed and average hashing speed 8a631f04
|
||||
* improve phrasing of debug messages during indexing at startup 127f414e
|
||||
* `--license` no longer depends on opensource.org at build time 33c4ccff
|
||||
* update deps 6cedcfbf
|
||||
* copyparty.exe: python 3.12.7 => 3.12.8
|
||||
* webdeps: hashwasm, dompurify
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-1123-2336 `v1.16.2` webdav upload fix
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* add `--nsort` and volflag `nsort` to default-enable natural sort of filenames with leading digits 8f7ffcf3
|
||||
* video-player: support `.mov` files which contain browser-native codecs 2d0cbdf1
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* #119 v1.16.0 broke webdav uploads from rclone and possibly other clients 7dfbfc72
|
||||
* a collection of webdav unittests will be added soon to prevent similar issues in the future
|
||||
* #118 ip-ranges can be mixed with `lan` when specifying the list of trusted proxies for `x-forwarded-for` with `--xff-src`
|
||||
* found and fixed by @codemicro (thx!) 0e31cfa7
|
||||
* ux:
|
||||
* in the grid-view, markdown files would open in the generic text viewer 520ac8f4
|
||||
* qr-codes (create-share, view-share) didn't render on chrome db069c3d
|
||||
* qr-codes could cause layout-shifting 5afb562a
|
||||
* fix layout-shifting for ongoing downloads in controlpanel 9c8507a0
|
||||
* cosmetic eta jank b10843d0
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* upto 7% faster folder listings due to refactoring for more ux knobs 0c43b592
|
||||
* fix resource leaks (only affected tests/debug) 2ab8924e
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-1115-2218 `v1.16.1` cbz thumbnails
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* thumbnails of .cbz manga archives 4d15dd6e
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* when running with `-j0`, download-ETA could break in complex volume layouts 10fc4768
|
||||
* linking to the image gallery didn't quite work if multiselect was enabled 56a04996
|
||||
* password-hashing parameters (cpu/ram cost) could not be customized 1f177528
|
||||
* the defaults must be perfect considering nobody ever tried changing them ¯\\_(ツ)_/¯
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* add intentional crash on startup if two volumes are configured to use the same histpath 2b63d7d1
|
||||
* prevents funky deadlocks and an eventual database loss in case of a no-thoughts-head-empty moment, purely hypothetical of course 🗿
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-1110-1932 `v1.16.0` COPYparty
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #46 #115 copy/paste files and folders cacec9c1
|
||||
* cut/paste still exists, but now you can copy too
|
||||
* with a UI to rename files in case of filename collisions 56317b00
|
||||
* files are created according to the dedup settings in the target volume (either full copies or symlinks/hardlinks)
|
||||
* show currently active downloads in the controlpanel 8aba5aed
|
||||
* can be made admin-only with `--dl-list=1` or disabled with `--dl-list=0`
|
||||
* hides filenames of hidden files, and files from volumes where the viewer doesn't have access
|
||||
* #114 async reinit on new [IdP users](https://github.com/9001/copyparty#identity-providers) 44ee07f0
|
||||
* new IdP users can now always auth, even while a filesystem reindex is running
|
||||
* ux:
|
||||
* remember batch-rename settings from last time 6a8d5e17
|
||||
* URL parameters to force grid/thumbs on/off 5718caa9
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* folders that fail to list due to a corrupt HDD/filesystem will now return a 404 instead of an empty listing 119e88d8
|
||||
* also fixes similar issues in u2c and partyfuse
|
||||
* u2c (commandline uploader): detect and adapt to proxies with short connection keepalives c784e528
|
||||
* ui/ux:
|
||||
* show the "switch-to-https" button in 404-messages too efd8a32e
|
||||
* the folder-loading indicator could steal keyboard focus d9962f65
|
||||
* hotkey-help was very trigger-happy 71d9e010
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* choose more conservative defaults when server has less than 1 GiB RAM 2bf9055c
|
||||
* runs okay down to 128 MiB, but thumbnails die below 256 MiB
|
||||
* update the [comparison to similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) after years of optimizations on both sides 0ce7cf5e
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-1027-0751 `v1.15.10` temporary upload links
|
||||
|
||||
|
||||
48
docs/chunksizes.py
Executable file
48
docs/chunksizes.py
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# there's far better ways to do this but its 4am and i dont wanna think
|
||||
|
||||
# just pypy it my dude
|
||||
|
||||
import math
|
||||
|
||||
def humansize(sz, terse=False):
|
||||
for unit in ["B", "KiB", "MiB", "GiB", "TiB"]:
|
||||
if sz < 1024:
|
||||
break
|
||||
|
||||
sz /= 1024.0
|
||||
|
||||
ret = " ".join([str(sz)[:4].rstrip("."), unit])
|
||||
|
||||
if not terse:
|
||||
return ret
|
||||
|
||||
return ret.replace("iB", "").replace(" ", "")
|
||||
|
||||
|
||||
def up2k_chunksize(filesize):
|
||||
chunksize = 1024 * 1024
|
||||
stepsize = 512 * 1024
|
||||
while True:
|
||||
for mul in [1, 2]:
|
||||
nchunks = math.ceil(filesize * 1.0 / chunksize)
|
||||
if nchunks <= 256 or (chunksize >= 32 * 1024 * 1024 and nchunks <= 4096):
|
||||
return chunksize
|
||||
|
||||
chunksize += stepsize
|
||||
stepsize *= mul
|
||||
|
||||
|
||||
def main():
|
||||
prev = 1048576
|
||||
n = n0 = 524288
|
||||
while True:
|
||||
csz = up2k_chunksize(n)
|
||||
if csz > prev:
|
||||
print(f"| {n-n0:>18_} | {humansize(n-n0):>8} | {prev:>13_} | {humansize(prev):>8} |".replace("_", " "))
|
||||
prev = csz
|
||||
n += n0
|
||||
|
||||
|
||||
main()
|
||||
@@ -6,6 +6,7 @@
|
||||
* [up2k](#up2k) - quick outline of the up2k protocol
|
||||
* [why not tus](#why-not-tus) - I didn't know about [tus](https://tus.io/)
|
||||
* [why chunk-hashes](#why-chunk-hashes) - a single sha512 would be better, right?
|
||||
* [list of chunk-sizes](#list-of-chunk-sizes) - specific chunksizes are enforced
|
||||
* [hashed passwords](#hashed-passwords) - regarding the curious decisions
|
||||
* [http api](#http-api)
|
||||
* [read](#read)
|
||||
@@ -95,6 +96,44 @@ hashwasm would solve the streaming issue but reduces hashing speed for sha512 (x
|
||||
|
||||
* blake2 might be a better choice since xxh is non-cryptographic, but that gets ~15 MiB/s on slower androids
|
||||
|
||||
### list of chunk-sizes
|
||||
|
||||
specific chunksizes are enforced depending on total filesize
|
||||
|
||||
each pair of filesize/chunksize is the largest filesize which will use its listed chunksize; a 512 MiB file will use chunksize 2 MiB, but if the file is one byte larger than 512 MiB then it becomes 3 MiB
|
||||
|
||||
for the purpose of performance (or dodging arbitrary proxy limitations), it is possible to upload combined and/or partial chunks using stitching and/or subchunks respectively
|
||||
|
||||
| filesize | filesize | chunksize | chunksz |
|
||||
| -----------------: | -------: | ------------: | ------: |
|
||||
| 268 435 456 | 256 MiB | 1 048 576 | 1.0 MiB |
|
||||
| 402 653 184 | 384 MiB | 1 572 864 | 1.5 MiB |
|
||||
| 536 870 912 | 512 MiB | 2 097 152 | 2.0 MiB |
|
||||
| 805 306 368 | 768 MiB | 3 145 728 | 3.0 MiB |
|
||||
| 1 073 741 824 | 1.0 GiB | 4 194 304 | 4.0 MiB |
|
||||
| 1 610 612 736 | 1.5 GiB | 6 291 456 | 6.0 MiB |
|
||||
| 2 147 483 648 | 2.0 GiB | 8 388 608 | 8.0 MiB |
|
||||
| 3 221 225 472 | 3.0 GiB | 12 582 912 | 12 MiB |
|
||||
| 4 294 967 296 | 4.0 GiB | 16 777 216 | 16 MiB |
|
||||
| 6 442 450 944 | 6.0 GiB | 25 165 824 | 24 MiB |
|
||||
| 137 438 953 472 | 128 GiB | 33 554 432 | 32 MiB |
|
||||
| 206 158 430 208 | 192 GiB | 50 331 648 | 48 MiB |
|
||||
| 274 877 906 944 | 256 GiB | 67 108 864 | 64 MiB |
|
||||
| 412 316 860 416 | 384 GiB | 100 663 296 | 96 MiB |
|
||||
| 549 755 813 888 | 512 GiB | 134 217 728 | 128 MiB |
|
||||
| 824 633 720 832 | 768 GiB | 201 326 592 | 192 MiB |
|
||||
| 1 099 511 627 776 | 1.0 TiB | 268 435 456 | 256 MiB |
|
||||
| 1 649 267 441 664 | 1.5 TiB | 402 653 184 | 384 MiB |
|
||||
| 2 199 023 255 552 | 2.0 TiB | 536 870 912 | 512 MiB |
|
||||
| 3 298 534 883 328 | 3.0 TiB | 805 306 368 | 768 MiB |
|
||||
| 4 398 046 511 104 | 4.0 TiB | 1 073 741 824 | 1.0 GiB |
|
||||
| 6 597 069 766 656 | 6.0 TiB | 1 610 612 736 | 1.5 GiB |
|
||||
| 8 796 093 022 208 | 8.0 TiB | 2 147 483 648 | 2.0 GiB |
|
||||
| 13 194 139 533 312 | 12.0 TiB | 3 221 225 472 | 3.0 GiB |
|
||||
| 17 592 186 044 416 | 16.0 TiB | 4 294 967 296 | 4.0 GiB |
|
||||
| 26 388 279 066 624 | 24.0 TiB | 6 442 450 944 | 6.0 GiB |
|
||||
| 35 184 372 088 832 | 32.0 TiB | 8 589 934 592 | 8.0 GiB |
|
||||
|
||||
|
||||
# hashed passwords
|
||||
|
||||
@@ -133,8 +172,8 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
|
||||
| GET | `?tar=xz:9` | ...as an xz-level-9 gnu-tar file |
|
||||
| GET | `?tar=pax` | ...as a pax-tar file |
|
||||
| GET | `?tar=pax,xz` | ...as an xz-level-1 pax-tar file |
|
||||
| GET | `?zip=utf-8` | ...as a zip file |
|
||||
| GET | `?zip` | ...as a WinXP-compatible zip file |
|
||||
| GET | `?zip` | ...as a zip file |
|
||||
| GET | `?zip=dos` | ...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 |
|
||||
@@ -143,6 +182,9 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
|
||||
| GET | `?dls` | show active downloads (do this as admin) |
|
||||
| GET | `?ups` | show recent uploads from your IP |
|
||||
| GET | `?ups&filter=f` | ...where URL contains `f` |
|
||||
| GET | `?ru` | show all recent uploads |
|
||||
| GET | `?ru&filter=f` | ...where URL contains `f` |
|
||||
| GET | `?ru&j` | ...as json |
|
||||
| GET | `?mime=foo` | specify return mimetype `foo` |
|
||||
| GET | `?v` | render markdown file at URL |
|
||||
| GET | `?v` | open image/video/audio in mediaplayer |
|
||||
@@ -170,10 +212,15 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
|
||||
| method | params | body | result |
|
||||
|--|--|--|--|
|
||||
| PUT | | (binary data) | upload into file at URL |
|
||||
| PUT | `?j` | (binary data) | ...and reply with json |
|
||||
| PUT | `?ck` | (binary data) | upload without checksum gen (faster) |
|
||||
| PUT | `?ck=md5` | (binary data) | return md5 instead of sha512 |
|
||||
| PUT | `?gz` | (binary data) | compress with gzip and write into file at URL |
|
||||
| PUT | `?xz` | (binary data) | compress with xz and write into file at URL |
|
||||
| mPOST | | `f=FILE` | upload `FILE` into the folder at URL |
|
||||
| mPOST | `?j` | `f=FILE` | ...and reply with json |
|
||||
| mPOST | `?ck` | `f=FILE` | ...and disable checksum gen (faster) |
|
||||
| mPOST | `?ck=md5` | `f=FILE` | ...and return md5 instead of sha512 |
|
||||
| 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 |
|
||||
@@ -190,8 +237,15 @@ upload modifiers:
|
||||
| http-header | url-param | effect |
|
||||
|--|--|--|
|
||||
| `Accept: url` | `want=url` | return just the file URL |
|
||||
| `Accept: json` | `want=json` | return upload info as json; same as `?j` |
|
||||
| `Rand: 4` | `rand=4` | generate random filename with 4 characters |
|
||||
| `Life: 30` | `life=30` | delete file after 30 seconds |
|
||||
| `CK: no` | `ck` | disable serverside checksum (maybe faster) |
|
||||
| `CK: md5` | `ck=md5` | return md5 checksum instead of sha512 |
|
||||
| `CK: sha1` | `ck=sha1` | return sha1 checksum |
|
||||
| `CK: sha256` | `ck=sha256` | return sha256 checksum |
|
||||
| `CK: b2` | `ck=b2` | return blake2b checksum |
|
||||
| `CK: b2s` | `ck=b2s` | return blake2s checksum |
|
||||
|
||||
* `life` only has an effect if the volume has a lifetime, and the volume lifetime must be greater than the file's
|
||||
|
||||
@@ -288,6 +342,7 @@ python3 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
pip install jinja2 strip_hints # MANDATORY
|
||||
pip install argon2-cffi # password hashing
|
||||
pip install pyzmq # send 0mq from hooks
|
||||
pip install mutagen # audio metadata
|
||||
pip install pyftpdlib # ftp server
|
||||
pip install partftpy # tftp server
|
||||
|
||||
22
docs/idp.md
22
docs/idp.md
@@ -20,3 +20,25 @@ this means that, if an IdP volume is located inside a folder that is readable by
|
||||
and likewise -- if the IdP volume is inside a folder that is only accessible by certain users, but the IdP volume is configured to allow access from unauthenticated users, then the contents of the volume will NOT be accessible until it is revived
|
||||
|
||||
until this limitation is fixed (if ever), it is recommended to place IdP volumes inside an appropriate parent volume, so they can inherit acceptable permissions until their revival; see the "strategic volumes" at the bottom of [./examples/docker/idp/copyparty.conf](./examples/docker/idp/copyparty.conf)
|
||||
|
||||
|
||||
## Connecting webdav clients
|
||||
|
||||
If you use only idp and want to connect via rclone you have to adapt a few things.
|
||||
The following steps are for Authelia, but should be easy adaptable to other IdPs and clients. There may be better/smarter ways to do this, but this is a known solution.
|
||||
|
||||
1. Add a rule for your domain and set it to one factor
|
||||
```
|
||||
rules:
|
||||
- domain: 'sub.domain.tld'
|
||||
policy: one_factor
|
||||
```
|
||||
2. After you created your rclone config find its location with `rclone config file` and add the headers option to it, change the string to `username:password` base64 encoded. Make sure to set the right url location, otherwise you will get a 401 from copyparty.
|
||||
```
|
||||
[servername-dav]
|
||||
type = webdav
|
||||
url = https://sub.domain.tld/u/user/priv/
|
||||
vendor = owncloud
|
||||
pacer_min_sleep = 0.01ms
|
||||
headers = Proxy-Authorization,basic base64encodedstring==
|
||||
```
|
||||
@@ -259,6 +259,12 @@ for d in /usr /var; do find $d -type f -size +30M 2>/dev/null; done | while IFS=
|
||||
for f in {0..255}; do echo $f; truncate -s 256M $f; b1=$(printf '%02x' $f); for o in {0..255}; do b2=$(printf '%02x' $o); printf "\x$b1\x$b2" | dd of=$f bs=2 seek=$((o*1024*1024)) conv=notrunc 2>/dev/null; done; done
|
||||
# create 6.06G file with 16 bytes of unique data at start+end of each 32M chunk
|
||||
sz=6509559808; truncate -s $sz f; csz=33554432; sz=$((sz/16)); step=$((csz/16)); ofs=0; while [ $ofs -lt $sz ]; do dd if=/dev/urandom of=f bs=16 count=2 seek=$ofs conv=notrunc iflag=fullblock; [ $ofs = 0 ] && ofs=$((ofs+step-1)) || ofs=$((ofs+step)); done
|
||||
# same but for chunksizes 16M (3.1G), 24M (4.1G), 48M (128.1G)
|
||||
sz=3321225472; csz=16777216;
|
||||
sz=4394967296; csz=25165824;
|
||||
sz=6509559808; csz=33554432;
|
||||
sz=138438953472; csz=50331648;
|
||||
f=csz-$csz; truncate -s $sz $f; sz=$((sz/16)); step=$((csz/16)); ofs=0; while [ $ofs -lt $sz ]; do dd if=/dev/urandom of=$f bs=16 count=2 seek=$ofs conv=notrunc iflag=fullblock; [ $ofs = 0 ] && ofs=$((ofs+step-1)) || ofs=$((ofs+step)); done
|
||||
|
||||
# py2 on osx
|
||||
brew install python@2
|
||||
|
||||
140
docs/synology-dsm.md
Normal file
140
docs/synology-dsm.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# running copyparty on synology dsm nas
|
||||
|
||||

|
||||
|
||||
this has been tested on a `Synology ds218+` NAS with 1 SHR storage-pool and 1 volume, but the same steps should work in more advanced setups too
|
||||
|
||||
verified on DSM 7.1 and 7.2, but not on 6.x since my flea-market ds218+ refuses to install it for some reason
|
||||
|
||||
|
||||
|
||||
# ok let's go
|
||||
|
||||
go to controlpanel -> shared-folders, and create the following shared-folders if you don't already have appropriate ones:
|
||||
|
||||
* a shared-folder for configuration files, preferably on SSD if you have one
|
||||
|
||||
* one or more shared-folders for your actual data/media to share
|
||||
|
||||
(btw, when you create the shared-folders, it asks whether you want to enable data checksum and file compression, i would recommend both)
|
||||
|
||||
the rest of this doc assumes that these two shared-folders are named `configs` and `media1`, and that you made an empty folder inside the `configs` shared-folder named `cpp`
|
||||
|
||||
* your copyparty config file (see below) should be named `something.conf` directly inside that cpp folder, for example `/configs/cpp/copyparty.conf`
|
||||
|
||||
* during first start, copyparty will create a folder there named `copyparty`, in other words `/configs/cpp/copyparty` which you should leave alone; that's where copyparty stores its indexes and other runtime config
|
||||
|
||||
|
||||
|
||||
## recommended copyparty config
|
||||
|
||||
open the Package Center and install `Text Editor` (by Synology Inc.) to create and edit your copyparty config:
|
||||
|
||||

|
||||
|
||||
* note the `copyparty` and `hist` folders in that screenshot which are autogenerated by copyparty and to be left alone
|
||||
|
||||
```yaml
|
||||
[global]
|
||||
e2d, e2t # remember uploads & read media tags
|
||||
rss, daw, ver # some other nice-to-have features
|
||||
#dedup # you may want this, or maybe not
|
||||
hist: /cfg/hist # don't pollute the shared-folder
|
||||
name: synology # shows in the browser, can be anything
|
||||
|
||||
[accounts]
|
||||
ed: wark # username ed, password wark
|
||||
|
||||
[/] # share the following at the webroot:
|
||||
/w # the "/w" docker-volume (the shared-folder)
|
||||
accs:
|
||||
A: ed # give Admin to username ed
|
||||
|
||||
# hide the synology system files by creating a hidden volume
|
||||
[/@eaDir]
|
||||
/w/@eaDir
|
||||
```
|
||||
|
||||
if you ever change the copyparty config file, then [restart the container](https://ocv.me/copyparty/doc/pics/dsm71-02.png) to make the changes take effect
|
||||
|
||||
okay now continue with one of these:
|
||||
|
||||
* [DSM v7.2 or newer](#dsm-v72-or-newer)
|
||||
|
||||
* [all older DSM versions](#dsm-v6x-dsm-v71x-or-older)
|
||||
|
||||
|
||||
|
||||
# DSM v7.2 or newer
|
||||
|
||||
`Docker` was replaced by `Container Manager` in DSM v7.2 but they're almost the same thing;
|
||||
|
||||
* open the `Package Center` and install the [Container Manager package](https://ocv.me/copyparty/doc/pics/dsm72-01.png) by `Docker Inc.`
|
||||
* open the `Container Manager` app
|
||||
* go to the `Registry` tab and search for `copyparty`
|
||||
* [doubleclick copyparty/ac](https://ocv.me/copyparty/doc/pics/dsm72-02.png) and keep the [default `latest`](https://ocv.me/copyparty/doc/pics/dsm72-03.png) when it asks you which tag to use
|
||||
* switch to the `Container` tab and click `Create`
|
||||
* [choose `copyparty/ac:latest`](https://ocv.me/copyparty/doc/pics/dsm72-04.png) and click `Next`
|
||||
|
||||
finally, in the [Advanced Settings](https://ocv.me/copyparty/doc/pics/dsm72-05.png) window,
|
||||
|
||||
* under `Port Settings`, type `3923` into the `Local Port` textbox
|
||||
* click `Add Folder` and select `/configs/cpp` on your nas (the `cpp` folder in the `configs` shared-folder), and change `Mount path` to `/cfg`
|
||||
* click `Add Folder` and select `/media1` on your nas (the shared-folder that copyparty can share in its web-UI) and change `Mount path` to `/w`
|
||||
* if you are adding multiple shared-folders for media, then the `Mount path` of the 2nd folder should be something like `/w/share2` or `/w/music`
|
||||
|
||||
copyparty will launch and become available at http://192.168.1.9:3923/ (assuming `192.168.1.9` is your nas ip)
|
||||
|
||||
|
||||
# DSM v6.x, DSM v7.1.x or older
|
||||
|
||||
if you're using DSM 7.1 or older, then you don't have [Container Manager](https://www.synology.com/en-global/dsm/packages/ContainerManager) yet and you'll have to use [Docker](https://www.synology.com/en-global/dsm/packages/Docker?os_ver=6.2&search=docker) instead. Here's how:
|
||||
|
||||
* open the `Package Center` and install the [Docker package](https://ocv.me/copyparty/doc/pics/dsm71-01.png) by `Docker Inc.`
|
||||
* open the `Docker` app
|
||||
* go to the `Registry` tab and search for `copyparty`
|
||||
* [doubleclick copyparty/ac](https://ocv.me/copyparty/doc/pics/dsm71-02.png) and keep the [default `latest`](https://ocv.me/copyparty/doc/pics/dsm71-03.png) when it asks you which tag to use
|
||||
* switch to the `Container` tab and click `Create`
|
||||
* [choose `copyparty/ac:latest`](https://ocv.me/copyparty/doc/pics/dsm71-04.png) and `Next`
|
||||
* in the [Network](https://ocv.me/copyparty/doc/pics/dsm71-05.png) window, keep the default `Use the selected networks: [x] bridge`
|
||||
* in the [General Settings](https://ocv.me/copyparty/doc/pics/dsm71-06.png) window, just keep everything default (in other words, everything disabled)
|
||||
* in the [Port Settings](https://ocv.me/copyparty/doc/pics/dsm71-07.png) window, change `Local Port` to `3923` (or choose something else, but it cannot be the default `Auto`)
|
||||
|
||||
finally, in the [Volume Settings](https://ocv.me/copyparty/doc/pics/dsm71-08.png) window, add a docker volume for copyparty config, and at least one volume for media-files which copyparty can share in its web-UI
|
||||
|
||||
* click `Add Folder` and select `/configs/cpp` on your nas (the `cpp` folder in the `configs` shared-folder), and change `Mount path` to `/cfg`
|
||||
* click `Add Folder` and select `/media1` on your nas (the shared-folder that copyparty can share in its web-UI) and change `Mount path` to `/w`
|
||||
* if you are adding multiple shared-folders for media, then the `Mount path` of the 2nd folder should be something like `/w/share2` or `/w/music`
|
||||
|
||||
copyparty will launch and become available at http://192.168.1.9:3923/ (assuming `192.168.1.9` is your nas ip)
|
||||
|
||||
|
||||
# misc notes
|
||||
|
||||
note that if you only want to share some folders inside your data volume, and not all of it, then you can either give copyparty the whole shared-folder anyways and control/restrict access in the copyparty config file (recommended), or you can add each folder as a new docker volume (not as flexible)
|
||||
|
||||
|
||||
|
||||
## regarding ram usage
|
||||
|
||||
the ram usage indicator in both `Docker` and `Container Manager` is misleading because it also counts the kernel disk cache which makes the number insanely high -- the synology resource monitor shows the correct values, usually less than 100 MiB
|
||||
|
||||
to see the actual memory usage by copyparty, see `Resource Monitor` -> `Task Manager` -> `Processes` and look at the `Private Memory` of `python3` which is probably copyparty
|
||||
|
||||
|
||||
|
||||
## regarding performance
|
||||
|
||||
when uploading files to the synology nas with the respective web-UIs,
|
||||
|
||||
* `File Station` does about 16 MiB/s,
|
||||
|
||||
* `Synology Drive Server` does about 50 MiB/s; deceivingly fast upload speeds at first, but when the file is fully uploaded, there is a lengthy "processing" step at the end, reducing the average speed to about 50% of the initial
|
||||
|
||||
* copyparty maxes the HDD write-speeds, 99 MiB/s
|
||||
|
||||
when uploading to the synology nas over webdav,
|
||||
|
||||
* `WebDAV Server` by `Synology Inc.` in the Package Center does 86 MiB/s
|
||||
|
||||
* copyparty does 79 MiB/s; the NAS CPU is a bottleneck because copyparty verifies the upload checksum while `WebDAV Server` doesn't
|
||||
@@ -279,7 +279,7 @@ symbol legend,
|
||||
| per-file passwords | █ | | | █ | █ | | █ | | █ | | | | █ |
|
||||
| unmap subfolders | █ | | █ | | | | █ | | | █ | ╱ | • | |
|
||||
| index.html blocks list | ╱ | | | | | | █ | | | • | | | |
|
||||
| write-only folders | █ | | █ | | | | | | | | █ | █ | |
|
||||
| write-only folders | █ | | █ | | █ | | | | | | █ | █ | |
|
||||
| files stored as-is | █ | █ | █ | █ | | █ | █ | | | █ | █ | █ | █ |
|
||||
| file versioning | | | | █ | █ | | | | | | | | |
|
||||
| file encryption | | | | █ | █ | █ | | | | | | █ | |
|
||||
@@ -507,7 +507,6 @@ symbol legend,
|
||||
* ⚠️ uploads not resumable / accelerated / integrity-checked
|
||||
* ⚠️ on cloudflare: max upload size 100 MiB
|
||||
* ⚠️ uploading small files is slow; `4.7` files per sec (copyparty does `670`/sec, 140x faster)
|
||||
* ⚠️ no write-only / upload-only folders
|
||||
* ⚠️ big folders cannot be zip-downloaded
|
||||
* ⚠️ http/webdav only; no ftp, zeroconf
|
||||
* ⚠️ less awesome music player
|
||||
@@ -593,6 +592,7 @@ symbol legend,
|
||||
* ✅ user signup
|
||||
* ✅ command runner / remote shell
|
||||
* ✅ more efficient; can handle around twice as much simultaneous traffic
|
||||
* note: keep an eye on [gtsteffaniak's fork](https://github.com/gtsteffaniak/filebrowser)
|
||||
|
||||
## [filegator](https://github.com/filegator/filegator)
|
||||
* php; cross-platform (windows, linux, mac)
|
||||
|
||||
@@ -52,6 +52,7 @@ ftpd = ["pyftpdlib"]
|
||||
ftps = ["pyftpdlib", "pyopenssl"]
|
||||
tftpd = ["partftpy>=0.4.0"]
|
||||
pwhash = ["argon2-cffi"]
|
||||
zeromq = ["pyzmq"]
|
||||
|
||||
[project.scripts]
|
||||
copyparty = "copyparty.__main__:main"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
FROM alpine:3.18
|
||||
WORKDIR /z
|
||||
ENV ver_asmcrypto=c72492f4a66e17a0e5dd8ad7874de354f3ccdaa5 \
|
||||
ver_hashwasm=4.10.0 \
|
||||
ver_hashwasm=4.12.0 \
|
||||
ver_marked=4.3.0 \
|
||||
ver_dompf=3.1.7 \
|
||||
ver_dompf=3.2.3 \
|
||||
ver_mde=2.18.0 \
|
||||
ver_codemirror=5.65.16 \
|
||||
ver_codemirror=5.65.18 \
|
||||
ver_fontawesome=5.13.0 \
|
||||
ver_prism=1.29.0 \
|
||||
ver_zopfli=1.0.3
|
||||
@@ -16,7 +16,7 @@ ENV ver_asmcrypto=c72492f4a66e17a0e5dd8ad7874de354f3ccdaa5 \
|
||||
# https://github.com/codemirror/codemirror5/releases
|
||||
# https://github.com/cure53/DOMPurify/releases
|
||||
# https://github.com/Daninet/hash-wasm/releases
|
||||
# https://github.com/openpgpjs/asmcrypto.js
|
||||
# https://github.com/openpgpjs/asmcrypto.js/commits/main/
|
||||
# https://github.com/google/zopfli/tags
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ ENV XDG_CONFIG_HOME=/cfg
|
||||
|
||||
RUN apk --no-cache add !pyc \
|
||||
tzdata wget \
|
||||
py3-jinja2 py3-argon2-cffi py3-pillow \
|
||||
py3-jinja2 py3-argon2-cffi py3-pyzmq py3-pillow \
|
||||
ffmpeg
|
||||
|
||||
COPY i/dist/copyparty-sfx.py innvikler.sh ./
|
||||
|
||||
@@ -12,7 +12,8 @@ COPY i/bin/mtag/audio-bpm.py /mtag/
|
||||
COPY i/bin/mtag/audio-key.py /mtag/
|
||||
RUN apk add -U !pyc \
|
||||
tzdata wget \
|
||||
py3-jinja2 py3-argon2-cffi py3-pillow py3-pip py3-cffi \
|
||||
py3-jinja2 py3-argon2-cffi py3-pyzmq py3-pillow \
|
||||
py3-pip py3-cffi \
|
||||
ffmpeg \
|
||||
vips-jxl vips-heif vips-poppler vips-magick \
|
||||
py3-numpy fftw libsndfile \
|
||||
|
||||
@@ -9,7 +9,8 @@ ENV XDG_CONFIG_HOME=/cfg
|
||||
|
||||
RUN apk add -U !pyc \
|
||||
tzdata wget \
|
||||
py3-jinja2 py3-argon2-cffi py3-pillow py3-pip py3-cffi \
|
||||
py3-jinja2 py3-argon2-cffi py3-pyzmq py3-pillow \
|
||||
py3-pip py3-cffi \
|
||||
ffmpeg \
|
||||
vips-jxl vips-heif vips-poppler vips-magick \
|
||||
&& apk add -t .bd \
|
||||
|
||||
40
scripts/genlic.py
Executable file
40
scripts/genlic.py
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import re, os, sys, codecs
|
||||
|
||||
outfile = os.path.realpath(sys.argv[1])
|
||||
|
||||
os.chdir(os.path.dirname(__file__))
|
||||
|
||||
with open("../docs/lics.txt", "rb") as f:
|
||||
s = f.read().decode("utf-8").rstrip("\n") + "\n\n\n\n"
|
||||
s = re.sub("\nC: ", "\nCopyright (c) ", s)
|
||||
s = re.sub("\nL: ", "\nLicense: ", s)
|
||||
ret = s.split("\n")
|
||||
|
||||
lics = [
|
||||
"MIT License",
|
||||
"BSD 2-Clause License",
|
||||
"BSD 3-Clause License",
|
||||
"SIL Open Font License v1.1",
|
||||
]
|
||||
|
||||
for n, lic in enumerate(lics, 1):
|
||||
with open("lics/%d.r13" % (n,), "rb") as f:
|
||||
s = f.read().decode("utf-8")
|
||||
s = codecs.decode(s, "rot_13")
|
||||
s = "\n--- %s ---\n\n%s" % (lic, s)
|
||||
ret.extend(s.split("\n"))
|
||||
|
||||
for n, ln in enumerate(ret):
|
||||
if not ln.startswith("--- "):
|
||||
continue
|
||||
pad = " " * ((80 - len(ln)) // 2)
|
||||
ln = "%s\033[07m%s\033[0m" % (pad, ln)
|
||||
ret[n] = ln
|
||||
|
||||
ret.append("")
|
||||
ret.append("")
|
||||
|
||||
with open(outfile, "wb") as f:
|
||||
f.write(("\n".join(ret)).encode("utf-8"))
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
outfile="$($(command -v realpath || command -v grealpath) "$1")"
|
||||
|
||||
[ -e genlic.sh ] || cd scripts
|
||||
[ -e genlic.sh ]
|
||||
|
||||
f=../build/mit.txt
|
||||
[ -e $f ] ||
|
||||
curl https://opensource.org/licenses/MIT |
|
||||
awk '/div>/{o=0}o>1;o{o++}/;COPYRIGHT HOLDER/{o=1}' |
|
||||
awk '{gsub(/<[^>]+>/,"")};1' >$f
|
||||
|
||||
f=../build/isc.txt
|
||||
[ -e $f ] ||
|
||||
curl https://opensource.org/licenses/ISC |
|
||||
awk '/div>/{o=0}o>2;o{o++}/;OWNER/{o=1}' |
|
||||
awk '{gsub(/<[^>]+>/,"")};/./{b=0}!/./{b++}b>1{next}1' >$f
|
||||
|
||||
f=../build/2bsd.txt
|
||||
[ -e $f ] ||
|
||||
curl https://opensource.org/licenses/BSD-2-Clause |
|
||||
awk '/div>/{o=0}o>1;o{o++}/HOLDER/{o=1}' |
|
||||
awk '{gsub(/<[^>]+>/,"")};1' >$f
|
||||
|
||||
f=../build/3bsd.txt
|
||||
[ -e $f ] ||
|
||||
curl https://opensource.org/licenses/BSD-3-Clause |
|
||||
awk '/div>/{o=0}o>1;o{o++}/HOLDER/{o=1}' |
|
||||
awk '{gsub(/<[^>]+>/,"")};1' >$f
|
||||
|
||||
f=../build/ofl.txt
|
||||
[ -e $f ] ||
|
||||
curl https://opensource.org/licenses/OFL-1.1 |
|
||||
awk '/PREAMBLE/{o=1}/sil\.org/{o=0}!o{next}/./{printf "%s ",$0;next}{print"\n"}' |
|
||||
awk '{gsub(/<[^>]+>/,"");gsub(/^\s+/,"");gsub(/&/,"\\&")}/./{b=0}!/./{b++}b>1{next}1' >$f
|
||||
|
||||
(sed -r 's/^L: /License: /;s/^C: /Copyright (c) /' <../docs/lics.txt
|
||||
printf '\n\n--- MIT License ---\n\n'; cat ../build/mit.txt
|
||||
printf '\n\n--- ISC License ---\n\n'; cat ../build/isc.txt
|
||||
printf '\n\n--- BSD 2-Clause License ---\n\n'; cat ../build/2bsd.txt
|
||||
printf '\n\n--- BSD 3-Clause License ---\n\n'; cat ../build/3bsd.txt
|
||||
printf '\n\n--- SIL Open Font License v1.1 ---\n\n'; cat ../build/ofl.txt
|
||||
) |
|
||||
while IFS= read -r x; do
|
||||
[ "${x:0:4}" = "--- " ] || {
|
||||
printf '%s\n' "$x"
|
||||
continue
|
||||
}
|
||||
n=${#x}
|
||||
p=$(( (80-n)/2 ))
|
||||
printf "%${p}s\033[07m%s\033[0m\n" "" "$x"
|
||||
done > "$outfile"
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import re
|
||||
import socket
|
||||
import subprocess as sp
|
||||
|
||||
|
||||
@@ -25,13 +26,21 @@ def readclip():
|
||||
return sp.check_output(cmd.split()).decode("utf-8")
|
||||
except:
|
||||
pass
|
||||
raise Exception("need one of these: xsel xclip pbpaste")
|
||||
|
||||
|
||||
def cnv(src):
|
||||
hostname = str(socket.gethostname()).split(".")[0]
|
||||
|
||||
yield '<!DOCTYPE html>'
|
||||
yield '<html style="background:#222;color:#fff"><body>'
|
||||
skip_sfx = False
|
||||
in_sfx = 0
|
||||
in_salt = 0
|
||||
in_name = 0
|
||||
in_cores = 0
|
||||
in_hash_mt = False
|
||||
in_th_ram_max = 0
|
||||
|
||||
while True:
|
||||
ln = next(src)
|
||||
@@ -43,6 +52,7 @@ def cnv(src):
|
||||
|
||||
for ln in src:
|
||||
ln = ln.rstrip()
|
||||
t = ln
|
||||
if re.search(r"^<font[^>]+>copyparty v[0-9]", ln):
|
||||
in_sfx = 3
|
||||
if in_sfx:
|
||||
@@ -56,11 +66,39 @@ def cnv(src):
|
||||
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
|
||||
if "--name TXT" in ln:
|
||||
in_name = 3
|
||||
if in_name:
|
||||
in_name -= 1
|
||||
ln = ln.replace(">" + hostname + "<", ">hostname<")
|
||||
if t != ln:
|
||||
in_name = 0
|
||||
if "--hash-mt CORES" in ln:
|
||||
in_cores = 3
|
||||
in_hash_mt = True
|
||||
if "--mtag-mt CORES" in ln or "--th-mt CORES" in ln:
|
||||
in_cores = 3
|
||||
if in_cores:
|
||||
in_cores -= 1
|
||||
zs = ">numCores"
|
||||
if in_hash_mt:
|
||||
zs += " if 5 or less"
|
||||
ln = re.sub(r">[0-9]{1,2}<", zs + "<", ln)
|
||||
if t != ln:
|
||||
in_cores = 0
|
||||
in_hash_mt = False
|
||||
if "--th-ram-max GB" in ln:
|
||||
in_th_ram_max = 3
|
||||
if in_th_ram_max:
|
||||
in_th_ram_max -= 1
|
||||
ln = re.sub(r">[0-9]{1,2}\.[0-9]<", ">dynamic<", ln)
|
||||
if t != ln:
|
||||
in_th_ram_max = 0
|
||||
|
||||
ln = ln.replace(">/home/ed/", ">~/")
|
||||
if ln.startswith("0" * 20):
|
||||
skip_sfx = True
|
||||
|
||||
@@ -6,6 +6,10 @@ 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)/;
|
||||
s/(--name TXT.*default: )[^)]+/\1hostname/;
|
||||
s/(--hash-mt CORES.*default: )[0-9]+/\1numCores if 5 or less/;
|
||||
s/(--mtag-mt|th-mt)( CORES.*default: )[0-9]+/\1\2numCores/;
|
||||
s/(--th-ram-max GB.*default: )[0-9\.]+/\1dynamic/;
|
||||
' | awk '
|
||||
/^copyparty/{a=1} !a{next}
|
||||
/^0{20}/{b=1} b&&/^copyparty v[0-9]+\./{s=3}
|
||||
|
||||
5
scripts/lics/1.r13
Normal file
5
scripts/lics/1.r13
Normal file
@@ -0,0 +1,5 @@
|
||||
Crezvffvba vf urerol tenagrq, serr bs punetr, gb nal crefba bognvavat n pbcl bs guvf fbsgjner naq nffbpvngrq qbphzragngvba svyrf (gur "Fbsgjner"), gb qrny va gur Fbsgjner jvgubhg erfgevpgvba, vapyhqvat jvgubhg yvzvgngvba gur evtugf gb hfr, pbcl, zbqvsl, zretr, choyvfu, qvfgevohgr, fhoyvprafr, naq/be fryy pbcvrf bs gur Fbsgjner, naq gb crezvg crefbaf gb jubz gur Fbsgjner vf sheavfurq gb qb fb, fhowrpg gb gur sbyybjvat pbaqvgvbaf:
|
||||
|
||||
Gur nobir pbclevtug abgvpr naq guvf crezvffvba abgvpr funyy or vapyhqrq va nyy pbcvrf be fhofgnagvny cbegvbaf bs gur Fbsgjner.
|
||||
|
||||
GUR FBSGJNER VF CEBIVQRQ "NF VF", JVGUBHG JNEENAGL BS NAL XVAQ, RKCERFF BE VZCYVRQ, VAPYHQVAT OHG ABG YVZVGRQ GB GUR JNEENAGVRF BS ZREPUNAGNOVYVGL, SVGARFF SBE N CNEGVPHYNE CHECBFR NAQ ABAVASEVATRZRAG. VA AB RIRAG FUNYY GUR NHGUBEF BE PBCLEVTUG UBYQREF OR YVNOYR SBE NAL PYNVZ, QNZNTRF BE BGURE YVNOVYVGL, JURGURE VA NA NPGVBA BS PBAGENPG, GBEG BE BGUREJVFR, NEVFVAT SEBZ, BHG BS BE VA PBAARPGVBA JVGU GUR FBSGJNER BE GUR HFR BE BGURE QRNYVATF VA GUR FBSGJNER.
|
||||
7
scripts/lics/2.r13
Normal file
7
scripts/lics/2.r13
Normal file
@@ -0,0 +1,7 @@
|
||||
Erqvfgevohgvba naq hfr va fbhepr naq ovanel sbezf, jvgu be jvgubhg zbqvsvpngvba, ner crezvggrq cebivqrq gung gur sbyybjvat pbaqvgvbaf ner zrg:
|
||||
|
||||
1. Erqvfgevohgvbaf bs fbhepr pbqr zhfg ergnva gur nobir pbclevtug abgvpr, guvf yvfg bs pbaqvgvbaf naq gur sbyybjvat qvfpynvzre.
|
||||
|
||||
2. Erqvfgevohgvbaf va ovanel sbez zhfg ercebqhpr gur nobir pbclevtug abgvpr, guvf yvfg bs pbaqvgvbaf naq gur sbyybjvat qvfpynvzre va gur qbphzragngvba naq/be bgure zngrevnyf cebivqrq jvgu gur qvfgevohgvba.
|
||||
|
||||
GUVF FBSGJNER VF CEBIVQRQ OL GUR PBCLEVTUG UBYQREF NAQ PBAGEVOHGBEF "NF VF" NAQ NAL RKCERFF BE VZCYVRQ JNEENAGVRF, VAPYHQVAT, OHG ABG YVZVGRQ GB, GUR VZCYVRQ JNEENAGVRF BS ZREPUNAGNOVYVGL NAQ SVGARFF SBE N CNEGVPHYNE CHECBFR NER QVFPYNVZRQ. VA AB RIRAG FUNYY GUR PBCLEVTUG UBYQRE BE PBAGEVOHGBEF OR YVNOYR SBE NAL QVERPG, VAQVERPG, VAPVQRAGNY, FCRPVNY, RKRZCYNEL, BE PBAFRDHRAGVNY QNZNTRF (VAPYHQVAT, OHG ABG YVZVGRQ GB, CEBPHERZRAG BS FHOFGVGHGR TBBQF BE FREIVPRF; YBFF BS HFR, QNGN, BE CEBSVGF; BE OHFVARFF VAGREEHCGVBA) UBJRIRE PNHFRQ NAQ BA NAL GURBEL BS YVNOVYVGL, JURGURE VA PBAGENPG, FGEVPG YVNOVYVGL, BE GBEG (VAPYHQVAT ARTYVTRAPR BE BGUREJVFR) NEVFVAT VA NAL JNL BHG BS GUR HFR BS GUVF FBSGJNER, RIRA VS NQIVFRQ BS GUR CBFFVOVYVGL BS FHPU QNZNTR.
|
||||
9
scripts/lics/3.r13
Normal file
9
scripts/lics/3.r13
Normal file
@@ -0,0 +1,9 @@
|
||||
Erqvfgevohgvba naq hfr va fbhepr naq ovanel sbezf, jvgu be jvgubhg zbqvsvpngvba, ner crezvggrq cebivqrq gung gur sbyybjvat pbaqvgvbaf ner zrg:
|
||||
|
||||
1. Erqvfgevohgvbaf bs fbhepr pbqr zhfg ergnva gur nobir pbclevtug abgvpr, guvf yvfg bs pbaqvgvbaf naq gur sbyybjvat qvfpynvzre.
|
||||
|
||||
2. Erqvfgevohgvbaf va ovanel sbez zhfg ercebqhpr gur nobir pbclevtug abgvpr, guvf yvfg bs pbaqvgvbaf naq gur sbyybjvat qvfpynvzre va gur qbphzragngvba naq/be bgure zngrevnyf cebivqrq jvgu gur qvfgevohgvba.
|
||||
|
||||
3. Arvgure gur anzr bs gur pbclevtug ubyqre abe gur anzrf bs vgf pbagevohgbef znl or hfrq gb raqbefr be cebzbgr cebqhpgf qrevirq sebz guvf fbsgjner jvgubhg fcrpvsvp cevbe jevggra crezvffvba.
|
||||
|
||||
GUVF FBSGJNER VF CEBIVQRQ OL GUR PBCLEVTUG UBYQREF NAQ PBAGEVOHGBEF "NF VF" NAQ NAL RKCERFF BE VZCYVRQ JNEENAGVRF, VAPYHQVAT, OHG ABG YVZVGRQ GB, GUR VZCYVRQ JNEENAGVRF BS ZREPUNAGNOVYVGL NAQ SVGARFF SBE N CNEGVPHYNE CHECBFR NER QVFPYNVZRQ. VA AB RIRAG FUNYY GUR PBCLEVTUG UBYQRE BE PBAGEVOHGBEF OR YVNOYR SBE NAL QVERPG, VAQVERPG, VAPVQRAGNY, FCRPVNY, RKRZCYNEL, BE PBAFRDHRAGVNY QNZNTRF (VAPYHQVAT, OHG ABG YVZVGRQ GB, CEBPHERZRAG BS FHOFGVGHGR TBBQF BE FREIVPRF; YBFF BS HFR, QNGN, BE CEBSVGF; BE OHFVARFF VAGREEHCGVBA) UBJRIRE PNHFRQ NAQ BA NAL GURBEL BS YVNOVYVGL, JURGURE VA PBAGENPG, FGEVPG YVNOVYVGL, BE GBEG (VAPYHQVAT ARTYVTRAPR BE BGUREJVFR) NEVFVAT VA NAL JNL BHG BS GUR HFR BS GUVF FBSGJNER, RIRA VS NQIVFRQ BS GUR CBFFVOVYVGL BS FHPU QNZNTR.
|
||||
39
scripts/lics/4.r13
Normal file
39
scripts/lics/4.r13
Normal file
@@ -0,0 +1,39 @@
|
||||
CERNZOYR
|
||||
|
||||
Gur tbnyf bs gur Bcra Sbag Yvprafr (BSY) ner gb fgvzhyngr jbeyqjvqr qrirybczrag bs pbyynobengvir sbag cebwrpgf, gb fhccbeg gur sbag perngvba rssbegf bs npnqrzvp naq yvathvfgvp pbzzhavgvrf, naq gb cebivqr n serr naq bcra senzrjbex va juvpu sbagf znl or funerq naq vzcebirq va cnegarefuvc jvgu bguref.
|
||||
|
||||
Gur BSY nyybjf gur yvprafrq sbagf gb or hfrq, fghqvrq, zbqvsvrq naq erqvfgevohgrq serryl nf ybat nf gurl ner abg fbyq ol gurzfryirf. Gur sbagf, vapyhqvat nal qrevingvir jbexf, pna or ohaqyrq, rzorqqrq, erqvfgevohgrq naq/be fbyq jvgu nal fbsgjner cebivqrq gung nal erfreirq anzrf ner abg hfrq ol qrevingvir jbexf. Gur sbagf naq qrevingvirf, ubjrire, pnaabg or eryrnfrq haqre nal bgure glcr bs yvprafr. Gur erdhverzrag sbe sbagf gb erznva haqre guvf yvprafr qbrf abg nccyl gb nal qbphzrag perngrq hfvat gur sbagf be gurve qrevingvirf.
|
||||
|
||||
QRSVAVGVBAF
|
||||
|
||||
"Sbag Fbsgjner" ersref gb gur frg bs svyrf eryrnfrq ol gur Pbclevtug Ubyqre(f) haqre guvf yvprafr naq pyrneyl znexrq nf fhpu. Guvf znl vapyhqr fbhepr svyrf, ohvyq fpevcgf naq qbphzragngvba.
|
||||
|
||||
"Erfreirq Sbag Anzr" ersref gb nal anzrf fcrpvsvrq nf fhpu nsgre gur pbclevtug fgngrzrag(f).
|
||||
|
||||
"Bevtvany Irefvba" ersref gb gur pbyyrpgvba bs Sbag Fbsgjner pbzcbaragf nf qvfgevohgrq ol gur Pbclevtug Ubyqre(f).
|
||||
|
||||
"Zbqvsvrq Irefvba" ersref gb nal qrevingvir znqr ol nqqvat gb, qryrgvat, be fhofgvghgvat - va cneg be va jubyr - nal bs gur pbzcbaragf bs gur Bevtvany Irefvba, ol punatvat sbezngf be ol cbegvat gur Sbag Fbsgjner gb n arj raivebazrag.
|
||||
|
||||
"Nhgube" ersref gb nal qrfvtare, ratvarre, cebtenzzre, grpuavpny jevgre be bgure crefba jub pbagevohgrq gb gur Sbag Fbsgjner.
|
||||
|
||||
CREZVFFVBA & PBAQVGVBAF
|
||||
|
||||
Crezvffvba vf urerol tenagrq, serr bs punetr, gb nal crefba bognvavat n pbcl bs gur Sbag Fbsgjner, gb hfr, fghql, pbcl, zretr, rzorq, zbqvsl, erqvfgevohgr, naq fryy zbqvsvrq naq hazbqvsvrq pbcvrf bs gur Sbag Fbsgjner, fhowrpg gb gur sbyybjvat pbaqvgvbaf:
|
||||
|
||||
1) Arvgure gur Sbag Fbsgjner abe nal bs vgf vaqvivqhny pbzcbaragf, va Bevtvany be Zbqvsvrq Irefvbaf, znl or fbyq ol vgfrys.
|
||||
|
||||
2) Bevtvany be Zbqvsvrq Irefvbaf bs gur Sbag Fbsgjner znl or ohaqyrq, erqvfgevohgrq naq/be fbyq jvgu nal fbsgjner, cebivqrq gung rnpu pbcl pbagnvaf gur nobir pbclevtug abgvpr naq guvf yvprafr. Gurfr pna or vapyhqrq rvgure nf fgnaq-nybar grkg svyrf, uhzna-ernqnoyr urnqref be va gur nccebcevngr znpuvar-ernqnoyr zrgnqngn svryqf jvguva grkg be ovanel svyrf nf ybat nf gubfr svryqf pna or rnfvyl ivrjrq ol gur hfre.
|
||||
|
||||
3) Ab Zbqvsvrq Irefvba bs gur Sbag Fbsgjner znl hfr gur Erfreirq Sbag Anzr(f) hayrff rkcyvpvg jevggra crezvffvba vf tenagrq ol gur pbeerfcbaqvat Pbclevtug Ubyqre. Guvf erfgevpgvba bayl nccyvrf gb gur cevznel sbag anzr nf cerfragrq gb gur hfref.
|
||||
|
||||
4) Gur anzr(f) bs gur Pbclevtug Ubyqre(f) be gur Nhgube(f) bs gur Sbag Fbsgjner funyy abg or hfrq gb cebzbgr, raqbefr be nqiregvfr nal Zbqvsvrq Irefvba, rkprcg gb npxabjyrqtr gur pbagevohgvba(f) bs gur Pbclevtug Ubyqre(f) naq gur Nhgube(f) be jvgu gurve rkcyvpvg jevggra crezvffvba.
|
||||
|
||||
5) Gur Sbag Fbsgjner, zbqvsvrq be hazbqvsvrq, va cneg be va jubyr, zhfg or qvfgevohgrq ragveryl haqre guvf yvprafr, naq zhfg abg or qvfgevohgrq haqre nal bgure yvprafr. Gur erdhverzrag sbe sbagf gb erznva haqre guvf yvprafr qbrf abg nccyl gb nal qbphzrag perngrq hfvat gur Sbag Fbsgjner.
|
||||
|
||||
GREZVANGVBA
|
||||
|
||||
Guvf yvprafr orpbzrf ahyy naq ibvq vs nal bs gur nobir pbaqvgvbaf ner abg zrg.
|
||||
|
||||
QVFPYNVZRE
|
||||
|
||||
GUR SBAG FBSGJNER VF CEBIVQRQ "NF VF", JVGUBHG JNEENAGL BS NAL XVAQ, RKCERFF BE VZCYVRQ, VAPYHQVAT OHG ABG YVZVGRQ GB NAL JNEENAGVRF BS ZREPUNAGNOVYVGL, SVGARFF SBE N CNEGVPHYNE CHECBFR NAQ ABAVASEVATRZRAG BS PBCLEVTUG, CNGRAG, GENQRZNEX, BE BGURE EVTUG. VA AB RIRAG FUNYY GUR PBCLEVTUG UBYQRE OR YVNOYR SBE NAL PYNVZ, QNZNTRF BE BGURE YVNOVYVGL, VAPYHQVAT NAL TRARENY, FCRPVNY, VAQVERPG, VAPVQRAGNY, BE PBAFRDHRAGVNY QNZNTRF, JURGURE VA NA NPGVBA BS PBAGENPG, GBEG BE BGUREJVFR, NEVFVAT SEBZ, BHG BS GUR HFR BE VANOVYVGL GB HFR GUR SBAG FBSGJNER BE SEBZ BGURE QRNYVATF VA GUR SBAG FBSGJNER.
|
||||
3
scripts/lics/README.md
Normal file
3
scripts/lics/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
these are foss licenses in rot13 so scanners don't think copyparty isn't mit
|
||||
|
||||
1=mit 2=2bsd 3=3bsd 4=ofl
|
||||
12
scripts/lics/rot.py
Executable file
12
scripts/lics/rot.py
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os, codecs
|
||||
|
||||
for fn in os.listdir("."):
|
||||
if not fn.endswith(".txt"):
|
||||
continue
|
||||
with open(fn, "rb") as f:
|
||||
s = f.read().decode("utf-8")
|
||||
b = codecs.encode(s, "rot_13").encode("utf-8")
|
||||
with open(fn.replace("txt", "r13"), "wb") as f:
|
||||
f.write(b)
|
||||
@@ -100,14 +100,13 @@ load_env || {
|
||||
# cleanup
|
||||
rm -rf unt build/pypi
|
||||
|
||||
# grab licenses
|
||||
scripts/genlic.sh copyparty/res/COPYING.txt
|
||||
# generate license list
|
||||
scripts/genlic.py copyparty/res/COPYING.txt
|
||||
|
||||
# clean-ish packaging env
|
||||
rm -rf build/pypi
|
||||
mkdir -p build/pypi
|
||||
cp -pR pyproject.toml README.md LICENSE copyparty contrib bin scripts/strip_hints build/pypi/
|
||||
tar -c docs/lics.txt scripts/genlic.sh build/*.txt | tar -xC build/pypi/
|
||||
cd build/pypi
|
||||
|
||||
# delete junk
|
||||
|
||||
@@ -308,8 +308,7 @@ necho() {
|
||||
# remove type hints before build instead
|
||||
(cd copyparty; PYTHONPATH="..:$PYTHONPATH" "$pybin" ../../scripts/strip_hints/a.py; rm uh)
|
||||
|
||||
licfile=$(realpath copyparty/res/COPYING.txt)
|
||||
(cd ../scripts; ./genlic.sh "$licfile")
|
||||
(cd ../scripts; ./genlic.py ../copyparty/res/COPYING.txt)
|
||||
}
|
||||
|
||||
[ ! -e copyparty/web/deps/mini-fa.woff ] && [ $dl_wd ] && {
|
||||
|
||||
@@ -64,7 +64,7 @@ git archive hovudstraum | tar -xC "$rls_dir"
|
||||
echo ">>> export untracked deps"
|
||||
tar -c copyparty/web/deps | tar -xC "$rls_dir"
|
||||
|
||||
scripts/genlic.sh "$rls_dir/copyparty/res/COPYING.txt"
|
||||
scripts/genlic.py "$rls_dir/copyparty/res/COPYING.txt"
|
||||
|
||||
cd "$rls_dir"
|
||||
find -type d -exec chmod 755 '{}' \+
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
f117016b1e6a7d7e745db30d3e67f1acf7957c443a0dd301b6c5e10b8368f2aa4db6be9782d2d3f84beadd139bfeef4982e40f21ca5d9065cb794eeb0e473e82 altgraph-0.17.4-py2.py3-none-any.whl
|
||||
6a624018f30da375581d5751eca0080edbbe37f102f643f856279fcfded3a4379fd1b6fb0661cdb2e72bbbbc726ca714a1f5990cc348df365db62bc53e4c4503 Git-2.45.2-32-bit.exe
|
||||
17ce52ba50692a9d964f57a23ac163fb74c77fdeb2ca988a6d439ae1fe91955ff43730c073af97a7b3223093ffea3479a996b9b50ee7fba0869247a56f74baa6 pefile-2023.2.7-py3-none-any.whl
|
||||
749a473646c6d4c7939989649733d4c7699fd1c359c27046bf5bc9c070d1a4b8b986bbc65f60d7da725baf16dbfdd75a4c2f5bb8335f2cb5685073f5fee5c2d1 pywin32_ctypes-0.2.2-py3-none-any.whl
|
||||
b297ff66ec50cf5a1abcf07d6ac949644c5150ba094ffac974c5d27c81574c3e97ed814a47547f4b03a4c83ea0fb8f026433fca06a3f08e32742dc5c024f3d07 pywin32_ctypes-0.2.3-py3-none-any.whl
|
||||
085d39ef4426aa5f097fbc484595becc16e61ca23fc7da4d2a8bba540a3b82e789e390b176c7151bdc67d01735cce22b1562cdb2e31273225a2d3e275851a4ad setuptools-70.3.0-py3-none-any.whl
|
||||
360a141928f4a7ec18a994602cbb28bbf8b5cc7c077a06ac76b54b12fa769ed95ca0333a5cf728923a8e0baeb5cc4d5e73e5b3de2666beb05eb477d8ae719093 upx-4.2.4-win32.zip
|
||||
# win7
|
||||
@@ -23,11 +23,11 @@ ac96786e5d35882e0c5b724794329c9125c2b86ae7847f17acfc49f0d294312c6afc1c3f248655de
|
||||
# win10
|
||||
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
|
||||
18b9e8cfa682da51da1b682612652030bd7f10e4a1d5ea5220ab32bde734b0e6fe1c7dbd903ac37928c0171fd45d5ca602952054de40a4e55e9ed596279516b5 jinja2-3.1.5-py3-none-any.whl
|
||||
6df21f0da408a89f6504417c7cdf9aaafe4ed88cfa13e9b8fa8414f604c0401f885a04bbad0484dc51a29284af5d1548e33c6cc6bfb9896d9992c1b1074f332d MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl
|
||||
8a6e2b13a2ec4ef914a5d62aad3db6464d45e525a82e07f6051ed10474eae959069e165dba011aefb8207cdfd55391d73d6f06362c7eb247b08763106709526e mutagen-1.47.0-py3-none-any.whl
|
||||
0203ec2551c4836696cfab0b2c9fff603352f03fa36e7476e2e1ca7ec57a3a0c24bd791fcd92f342bf817f0887854d9f072e0271c643de4b313d8c9569ba8813 packaging-24.1-py3-none-any.whl
|
||||
2be320b4191f208cdd6af183c77ba2cf460ea52164ee45ac3ff17d6dfa57acd9deff016636c2dd42a21f4f6af977d5f72df7dacf599bebcf41757272354d14c1 pillow-10.4.0-cp312-cp312-win_amd64.whl
|
||||
896ddddbd4b85e86e0600cb65eb4c07fbc7f3802d47e7f660411e20b5500831469b97ed4770f25820f4e75cbfac40308da624fd86d4f62e578149d5c276a9cde pyinstaller-6.10.0-py3-none-win_amd64.whl
|
||||
873781decaeef07f6a79b0ed8b9f35f3fa534a1ea0d866991e40278a10818fa5b60c70b0d5828971b045364f1099694cd1e5d5d60d480acb93fcfbfbced4a09e pyinstaller_hooks_contrib-2024.8-py3-none-any.whl
|
||||
912b710007c7b29f29c0097aff8f825412166eed7777a7cef135b14316e8fff31b5df56d26d835d8ca090468cc0e914730f201a56caa3dd6dbef2f91088942b1 python-3.12.7-amd64.exe
|
||||
12d7921dc7dfd8a4b0ea0fa2bae8f1354fcdd59ece3d7f4e075aed631f9ba791dc142c70b1ccd1e6287c43139df1db26bd57a7a217c8da3a77326036495cdb57 pillow-11.1.0-cp312-cp312-win_amd64.whl
|
||||
f0463895e9aee97f31a2003323de235fed1b26289766dc0837261e3f4a594a31162b69e9adbb0e9a31e2e2d4b5f25c762ed1669553df7dc89a8ba4f85d297873 pyinstaller-6.11.1-py3-none-win_amd64.whl
|
||||
d550a0a14428386945533de2220c4c2e37c0c890fc51a600f626c6ca90a32d39572c121ec04c157ba3a8d6601cb021f8433d871b5c562a3d342c804fffec90c1 pyinstaller_hooks_contrib-2024.11-py3-none-any.whl
|
||||
0f623c9ab52d050283e97a986ba626d86b04cd02fa7ffdf352740576940b142b264709abadb5d875c90f625b28103d7210b900e0d77f12c1c140108bd2a159aa python-3.12.8-amd64.exe
|
||||
|
||||
@@ -38,7 +38,7 @@ fns=(
|
||||
MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl
|
||||
mutagen-1.47.0-py3-none-any.whl
|
||||
packaging-24.1-py3-none-any.whl
|
||||
pillow-10.4.0-cp312-cp312-win_amd64.whl
|
||||
pillow-11.1.0-cp312-cp312-win_amd64.whl
|
||||
pyinstaller-6.10.0-py3-none-win_amd64.whl
|
||||
pyinstaller_hooks_contrib-2024.8-py3-none-any.whl
|
||||
python-3.12.7-amd64.exe
|
||||
|
||||
@@ -105,6 +105,9 @@ copyparty/web/mde.html,
|
||||
copyparty/web/mde.js,
|
||||
copyparty/web/msg.css,
|
||||
copyparty/web/msg.html,
|
||||
copyparty/web/rups.css,
|
||||
copyparty/web/rups.html,
|
||||
copyparty/web/rups.js,
|
||||
copyparty/web/shares.css,
|
||||
copyparty/web/shares.html,
|
||||
copyparty/web/shares.js,
|
||||
|
||||
@@ -80,6 +80,7 @@ var tl_cpanel = {
|
||||
"ac1": "enable no304",
|
||||
"ad1": "enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!",
|
||||
"ae1": "active downloads:",
|
||||
"af1": "show recent uploads",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -291,6 +292,7 @@ var tl_browser = {
|
||||
"cl_uopts": "up2k switches",
|
||||
"cl_favico": "favicon",
|
||||
"cl_bigdir": "big dirs",
|
||||
"cl_hsort": "#sort",
|
||||
"cl_keytype": "key notation",
|
||||
"cl_hiddenc": "hidden columns",
|
||||
"cl_hidec": "hide",
|
||||
@@ -333,6 +335,7 @@ var tl_browser = {
|
||||
|
||||
"cdt_lim": "max number of files to show in a folder",
|
||||
"cdt_ask": "when scrolling to the bottom,$Ninstead of loading more files,$Nask what to do",
|
||||
"cdt_hsort": "how many sorting rules (<code>,sorthref</code>) to include in media-URLs. Setting this to 0 will also ignore sorting-rules included in media links when clicking them",
|
||||
|
||||
"tt_entree": "show navpane (directory tree sidebar)$NHotkey: B",
|
||||
"tt_detree": "show breadcrumbs$NHotkey: B",
|
||||
@@ -414,7 +417,7 @@ var tl_browser = {
|
||||
"fr_emore": "select at least one item to rename",
|
||||
"fd_emore": "select at least one item to delete",
|
||||
"fc_emore": "select at least one item to cut",
|
||||
"fcp_emore": "select at least one item to copy",
|
||||
"fcp_emore": "select at least one item to copy to clipboard",
|
||||
|
||||
"fs_sc": "share the folder you're in",
|
||||
"fs_ss": "share the selected files",
|
||||
@@ -470,9 +473,11 @@ var tl_browser = {
|
||||
"fcc_ok": "copied {0} items to clipboard",
|
||||
"fcc_warn": 'copied {0} items to clipboard\n\nbut: only <b>this</b> browser-tab can paste them\n(since the selection is so absolutely massive)',
|
||||
|
||||
"fp_apply": "use these names",
|
||||
"fp_ecut": "first cut or copy some files / folders to paste / move\n\nnote: you can cut / paste across different browser tabs",
|
||||
"fp_ename": "these {0} items cannot be moved here (names already exist):",
|
||||
"fcp_ename": "these {0} items cannot be copied here (names already exist):",
|
||||
"fp_ename": "{0} items cannot be moved here because the names are already taken. Give them new names below to continue, or blank the name to skip them:",
|
||||
"fcp_ename": "{0} items cannot be copied here because the names are already taken. Give them new names below to continue, or blank the name to skip them:",
|
||||
"fp_emore": "there are still some filename collisions left to fix",
|
||||
"fp_ok": "move OK",
|
||||
"fcp_ok": "copy OK",
|
||||
"fp_busy": "moving {0} items...\n\n{1}",
|
||||
@@ -604,6 +609,7 @@ var tl_browser = {
|
||||
"u_pott": "<p>files: <b>{0}</b> finished, <b>{1}</b> failed, <b>{2}</b> busy, <b>{3}</b> queued</p>",
|
||||
"u_ever": "this is the basic uploader; up2k needs at least<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1",
|
||||
"u_su2k": 'this is the basic uploader; <a href="#" id="u2yea">up2k</a> is better',
|
||||
"u_uput": 'optimize for speed (skip checksum)',
|
||||
"u_ewrite": 'you do not have write-access to this folder',
|
||||
"u_eread": 'you do not have read-access to this folder',
|
||||
"u_enoi": 'file-search is not enabled in server config',
|
||||
@@ -622,6 +628,7 @@ var tl_browser = {
|
||||
"u_hashdone": 'hashing done',
|
||||
"u_hashing": 'hash',
|
||||
"u_hs": 'handshaking...',
|
||||
"u_started": "the files are now being uploaded; see [🚀]",
|
||||
"u_dupdefer": "duplicate; will be processed after all other files",
|
||||
"u_actx": "click this text to prevent loss of<br />performance when switching to other windows/tabs",
|
||||
"u_fixed": "OK! Fixed it 👍",
|
||||
@@ -657,6 +664,7 @@ var tl_browser = {
|
||||
"ue_la": 'you are currently logged in as "{0}"',
|
||||
"ue_sr": 'you are currently in file-search mode\n\nswitch to upload-mode by clicking the magnifying glass 🔎 (next to the big SEARCH button), and try uploading again\n\nsorry',
|
||||
"ue_ta": 'try uploading again, it should work now',
|
||||
"ue_ab": "this file is already being uploaded into another folder, and that upload must be completed before the file can be uploaded elsewhere.\n\nYou can abort and forget the initial upload using the top-left 🧯",
|
||||
"ur_1uo": "OK: File uploaded successfully",
|
||||
"ur_auo": "OK: All {0} files uploaded successfully",
|
||||
"ur_1so": "OK: File found on server",
|
||||
|
||||
@@ -114,6 +114,8 @@ var tl_cpanel = {{
|
||||
"ab1": "disable no304",
|
||||
"ac1": "enable no304",
|
||||
"ad1": "enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!",
|
||||
"ae1": "active downloads:",
|
||||
"af1": "show recent uploads",
|
||||
}},
|
||||
}};
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ cat $f | awk '
|
||||
o{next}
|
||||
/^#/{s=1;rs=0;pr()}
|
||||
/^#* *(nix package)/{rs=1}
|
||||
/^#* *(themes|install on android|dev env setup|just the sfx|complete release|optional gpl stuff|nixos module)|```/{s=rs}
|
||||
/^#* *(themes|install on android|dev env setup|just the sfx|complete release|optional gpl stuff|nixos module|reverse-proxy perf)|```/{s=rs}
|
||||
/^#/{
|
||||
lv=length($1);
|
||||
sub(/[^ ]+ /,"");
|
||||
|
||||
1
setup.py
1
setup.py
@@ -144,6 +144,7 @@ args = {
|
||||
"ftps": ["pyftpdlib", "pyopenssl"],
|
||||
"tftpd": ["partftpy>=0.4.0"],
|
||||
"pwhash": ["argon2-cffi"],
|
||||
"zeromq": ["pyzmq"],
|
||||
},
|
||||
"entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]},
|
||||
"scripts": ["bin/partyfuse.py", "bin/u2c.py"],
|
||||
|
||||
@@ -14,11 +14,13 @@ from tests import util as tu
|
||||
from tests.util import Cfg
|
||||
|
||||
|
||||
class TestDedup(unittest.TestCase):
|
||||
class TestDedup(tu.TC):
|
||||
def setUp(self):
|
||||
self.td = tu.get_ramdisk()
|
||||
|
||||
def tearDown(self):
|
||||
if self.conn:
|
||||
self.conn.shutdown()
|
||||
os.chdir(tempfile.gettempdir())
|
||||
shutil.rmtree(self.td)
|
||||
|
||||
@@ -62,7 +64,6 @@ class TestDedup(unittest.TestCase):
|
||||
|
||||
self.conn = None
|
||||
self.fstab = None
|
||||
self.ctr = 0 # 2304
|
||||
for dedup, act_exp in product(tc_dedup, tcs):
|
||||
action, expect = act_exp.split(" ", 1)
|
||||
t = "dedup:%s action:%s" % (dedup, action)
|
||||
@@ -91,7 +92,7 @@ class TestDedup(unittest.TestCase):
|
||||
HttpCli(self.conn.setbuf(buf)).run()
|
||||
ret = self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
|
||||
print("CP <-- ", ret)
|
||||
self.assertIn(" 201 Created", ret[0])
|
||||
self.assertStart("HTTP/1.1 201 Created\r", ret[0])
|
||||
self.assertEqual("k\r\n", ret[1])
|
||||
return ret
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from tests import util as tu
|
||||
from tests.util import Cfg
|
||||
|
||||
|
||||
class TestDedup(unittest.TestCase):
|
||||
class TestDedup(tu.TC):
|
||||
def setUp(self):
|
||||
self.td = tu.get_ramdisk()
|
||||
|
||||
@@ -34,6 +34,8 @@ class TestDedup(unittest.TestCase):
|
||||
]
|
||||
|
||||
def tearDown(self):
|
||||
if self.conn:
|
||||
self.conn.shutdown()
|
||||
os.chdir(tempfile.gettempdir())
|
||||
shutil.rmtree(self.td)
|
||||
|
||||
@@ -71,7 +73,7 @@ class TestDedup(unittest.TestCase):
|
||||
sfn, hs = self.do_post_hs(dn, fns[0], f1, True)
|
||||
for fn in fns[1:]:
|
||||
h, b = self.handshake(dn, fn, f1)
|
||||
self.assertIn(" 422 Unpro", h)
|
||||
self.assertStart("HTTP/1.1 422 Unpro", h)
|
||||
self.assertIn("a different location;", b)
|
||||
self.do_post_data(dn, fns[0], f1, True, sfn, hs)
|
||||
if not e2d:
|
||||
@@ -156,10 +158,10 @@ class TestDedup(unittest.TestCase):
|
||||
rm = cms[irm]
|
||||
dn, fn, _ = rm
|
||||
h, b = self.curl("%s/%s?delete" % (dn, fn), meth="POST")
|
||||
self.assertIn(" 200 OK", h)
|
||||
self.assertStart("HTTP/1.1 200 OK", h)
|
||||
self.assertIn("deleted 1 files", b)
|
||||
h, b = self.curl("%s/%s" % (dn, fn))
|
||||
self.assertIn(" 404 Not Fo", h)
|
||||
self.assertStart("HTTP/1.1 404 Not Fo", h)
|
||||
for cm in cms:
|
||||
if cm == rm:
|
||||
continue
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user