Compare commits

...
76 Commits
Author SHA1 Message Date
ed ad9994bef3 v1.20.23 2026-09-06 22:49:08 +00:00
ed e5e2e97910 archlinux: add post_upgrade 2026-09-06 22:08:42 +00:00
ed e42e01563e update pkgs to 1.20.22 2026-09-06 21:18:21 +00:00
ed de21c210c6 v1.20.22 2026-09-06 21:12:32 +00:00
ed 55969b8e54 dk: block subdir zip (GHSA-mc69-pxc8-4xf4) 2026-09-06 21:07:39 +00:00
ed b80a210e0f sftp: xvol/xdev (GHSA-3fhv-rhjw-7hrg) 2026-09-06 21:07:21 +00:00
ed 8016512064 sfx: link explanation 2026-09-06 18:22:52 +00:00
ed 72eb03bf43 exe: drop license filter 2026-09-06 15:02:32 +00:00
ed f870cf3c05 sfx: prefer ~/.cache/; closes #1517 2026-09-05 22:49:01 +00:00
ed e6c9c6fbeb sfx: golf 2026-09-05 22:43:13 +00:00
ed dd9cf6456d btrfs: nocow .hist for sqlite perf 2026-09-05 20:54:37 +00:00
ed 185087ee05 systemd: use /etc/copyparty.conf; closes #1631 2026-09-05 17:56:19 +00:00
ed 3e3401f4ed allow dk download-as-zip 2026-09-05 12:55:58 +00:00
ed a78611d8c5 dont trust zip headers 2026-09-05 11:29:50 +00:00
ed 370a05ab3c add --no-mime / volflag nomime 2026-09-05 11:00:40 +00:00
ed d7d30f268e connect: rclone obscure; closes #1632 2026-09-04 19:14:06 +00:00
ed 34229337cc the font fell off 2026-09-04 19:01:44 +00:00
ed 0767d95674 up2k: fix !e2d reload blocking uploads 2026-09-04 18:51:15 +00:00
ed f00d19c5d3 avoid binary garbage in logs;
also stricten RE_HTTP1 against false-positives
2026-09-04 18:21:20 +00:00
ed ab791921ea fix csp-nonce on chrome<62 2026-09-04 17:54:20 +00:00
ed 2cebd8ce16 i18n: 2old 2026-09-04 17:54:04 +00:00
ed 662e3c5d59 indicate F4 instantly 2026-09-04 16:08:35 +00:00
ed 3ea671ff9a iOS Safari XHR has gone deaf;
workaround for iPhone bug in
iOS 18.6.2, 26.6, 26.6.1, ...

safari devtools has the full response,
it just never bothers calling xhr.onload
and xhr.readystate is unusably racey
(final value is any of 1..3)
2026-09-02 22:21:07 +00:00
ed f792a9529c add up2k mutex ping 2026-09-02 20:23:54 +00:00
ed 4e318d5165 deadcode 2026-09-01 23:02:48 +02:00
ed 80cf7bfdc0 certs: conditional whining 2026-09-01 23:00:10 +02:00
ed 704af697c7 packaging: license: rm missing/unvendored 2026-09-01 18:46:59 +02:00
ed b854b8f596 packaging: *.gif + pre-gzip js/css;
* web/*.gif was ignored by pyproject.toml
* pre-gzip js/css in dist source tarball
* systemd/archlinux: safer default-config
* readme: "config file example" labeling
2026-09-01 14:50:57 +02:00
Danila Kamaev 936e8a3bf3 rcm: wopi (#1607) 2026-08-31 16:36:55 +02:00
yuki 7796c2d06a add jxl support for autogrid (#1610) 2026-08-31 16:24:04 +02:00
Danila Kamaev adf5f639fa translation: rus review (#1612) 2026-08-31 16:22:46 +02:00
Danila Kamaev d4a1ce48e4 thumbex: collabora example (#1617) 2026-08-31 16:19:17 +02:00
Danila Kamaev 0a957bf264 thumbex: update example link (#1629) 2026-08-31 16:14:14 +02:00
ed bdf8b793d9 http206: fix footer (last-n-bytes) reqs 2026-08-31 16:08:05 +02:00
ed 69c022f565 misc 2026-08-31 15:41:01 +02:00
ed 7984ef9232 tests: fix macos 2026-08-31 15:40:21 +02:00
ed 9de090265f hooks: phonecam-sorter.py;
written 2025-04-27, just never left the nas
2026-08-23 22:28:45 +00:00
ed 6115eb452d update pkgs to 1.20.21 2026-08-17 22:05:44 +00:00
ed 32eadc0f14 v1.20.21 2026-08-17 21:57:27 +00:00
ed 566de65f17 option to allow js in svg 2026-08-17 21:04:45 +00:00
ed 78a4ee49a2 wopi-accs + support unmapped root 2026-08-17 19:40:22 +00:00
ed 9ff6a71a2a shares: hide md-hist too 2026-08-16 22:31:09 +00:00
ed 21c2c72889 shares: harden single-file shares some more;
motivated by a bug-report that couldn't be reproduced and
also didn't make sense, a "hallucination" as they call it?
(was submitted by an autonomous bot or something, man)

so this should make no difference aside from removing footguns
2026-08-16 22:22:51 +00:00
ed 18791c5768 up2k: on tcp loss, await !busy.up b4 re-hs;
avoids parallel reschedule of inflight chunks;
http400: that chunk is already being written to:
2026-08-16 20:48:48 +00:00
ed 2be85b67a9 lint 2026-08-16 18:23:12 +00:00
ed eeb399e3c9 fix dupe logue on !js hydrate 2026-08-16 18:20:11 +00:00
ed dcc0abd696 dsel: disable on bbox 2026-08-15 21:40:04 +00:00
ed 48d9de03fe whitespace 2026-08-15 21:38:51 +00:00
ed dbc6df812b th: add extractor example 2026-08-15 20:11:24 +00:00
Danila Kamaevanded 1864805072 th: custom thumbnail extractors (#1602)
Signed-off-by: ed <s@ocv.me>
Co-authored-by: ed <s@ocv.me>
2026-08-15 19:54:47 +00:00
shermanhlc 47297475bf u2c: password in U2C_PW (#1604) 2026-08-15 13:51:09 +00:00
ed 3098cf59cb wopi stuff:
* open in new tab
* print wopi url on error
* fix helptext to <= 15 ch
2026-08-15 13:25:36 +00:00
Danila Kamaev efcf96e3e3 wopi: collaborate (#1591)
persistent file IDs for real-time collaboration
2026-08-15 13:06:45 +00:00
Danila Kamaev 92c3f32e13 wopi: fix access_token_ttl (#1587) 2026-08-15 12:47:04 +00:00
ed 14e2d79b08 ignore PRTY_CONFIG if autodiscovered to prevent dupe vols 2026-08-15 09:56:13 +00:00
ed 825f1c9456 bbox: open svg; closes #1605 2026-08-15 09:48:22 +00:00
ed f19ef033ec up2k: u2sz: allow 1 as max + detect http413 2026-08-15 09:45:13 +00:00
ed 8b6e8972fe copyparty.exe: python 3.14 2026-08-12 18:38:53 +00:00
ed fbcb773307 failed the git skillcheck 2026-08-06 22:10:18 +00:00
ed 3eb7b46d83 optimize 2026-08-06 21:08:34 +00:00
h3 4f20738b71 haproxy: add X-Forwarded-Proto to example (codeberg#6)
Reviewed-on: https://codeberg.org/9001/copyparty/pulls/6
2026-08-05 01:52:58 +02:00
ed ef75220faf update pkgs to 1.20.20 2026-08-03 22:36:36 +00:00
ed a3acbfcd5b v1.20.20 2026-08-03 22:32:26 +00:00
Danila Kamaev fdf52a8c4e wopi: fix onlyoffice opening docs in readonly mode (#1585) 2026-08-03 22:02:18 +00:00
ed b9405a3e25 mute no-rss-warning for shares 2026-08-03 21:57:22 +00:00
ed 6f924829ee detect broken csp nonce 2026-08-03 21:56:45 +00:00
Danila Kamaev 24a4090fc7 wopi: add wopi-urls option (#1580);
map different copyparty hosts to different WOPI client addresses
2026-08-03 20:30:38 +00:00
ed c0c638c917 up2k: re-hs if unknown-wark or chunk-exists;
previous behavior:

* when a chunk already existed on the server, it would
   unstitch and continue retrying each individual chunk

* upon unknown-wark (file already completed), it would
   upload the remaining scheduled chunks before bailing

the unknown-wark behavior was accidental bullshit

the chunk-exists behavior was intentional, assumed optimal for
preserving tcp window-scaling, just not when running behind a
bufferbloating reverseproxy which may disconnect the client on
a timeout before the response is delivered, and "some clients"
still follow rfc2616-8.2.4 (retries the POST, wtf...)
wasting bandwidth + maybe further worsening conditions

"some clients" is specifically firefox with the devtools not open

another firefox joke is that it doesn't read the server-response
if the server does not drain the request-body; we end up in
xhr.onerror with no idea why, must assume the worst

new behavior:

in both scenarios, drop all scheduled chunks to be uploaded and
redo handshake, to as far as possible avoid retransmissions;
firefox will still eagerly retry 4 times but whatever

u2c was/is fine; nothing to be done

depending on network conditions and physical distance, this MAY
result in lower total speed, but the average case is likely a
net-positive, wasting less bandwidth on retransmissions,
in exchange for resetting tcp window-scaling
2026-08-03 20:08:19 +00:00
ed 2c6497b644 add hotkey F4 2026-08-03 18:55:24 +00:00
ed 2c4033831f js: while(apop) 2026-08-02 23:02:44 +00:00
ed 2880117265 fix ^a in search-results; closes #1581 2026-08-02 22:41:57 +00:00
yuki 63c61b6729 nixos: fix version checker (#1582)
Nixpkgs patches many libraries to read certificate bundle mentioned in
NIX_SSL_CERT_FILE, and /nix/store is already present in the sandbox.

Related to https://github.com/NixOS/nixpkgs/issues/409848
2026-08-02 21:48:24 +00:00
edandVictor Phillips 5c157be955 show subvolume size in listings;
* descends recursively to all nested volumes
* values are static, update on volume-rescan
* closes #634 #1033 #1390

Co-authored-by: Victor Phillips <victor.m.phillips@protonmail.com>
2026-08-01 22:23:58 +00:00
Wuerfel21 9f5f9d8884 th: .kra (krita) + .ora (openraster) (#1577) 2026-07-30 18:39:31 +00:00
ed 4003a47723 multipart boundary max-len 2026-07-30 18:22:45 +00:00
ed 398fcf1d18 update pkgs to 1.20.19 2026-07-27 22:08:01 +00:00
98 changed files with 1674 additions and 606 deletions

No files matched your search

+1
View File
@@ -4,4 +4,5 @@
*.png binary
*.gif binary
*.ico binary
*.gz binary
+45 -6
View File
@@ -84,6 +84,7 @@ built in Norway 🇳🇴 with contributions from [not-norway](https://github.com
* [exclude-patterns](#exclude-patterns) - to save some time
* [filesystem guards](#filesystem-guards) - avoid traversing into other filesystems
* [periodic rescan](#periodic-rescan) - filesystem monitoring
* [u2sz](#u2sz) - configure upload-chunking
* [upload rules](#upload-rules) - set upload rules using volflags
* [compress uploads](#compress-uploads) - files can be autocompressed on upload
* [chmod and chown](#chmod-and-chown) - per-volume filesystem-permissions and ownership
@@ -137,6 +138,7 @@ built in Norway 🇳🇴 with contributions from [not-norway](https://github.com
* [client-side](#client-side) - when uploading files
* [security](#security) - there is a [discord server](https://discord.gg/25J8CdTT6G) with announcements
* [gotchas](#gotchas) - behavior that might be unexpected
* [csp nonce](#csp-nonce) - unauthorized javascript is blocked
* [cors](#cors) - cross-site request config
* [filekeys](#filekeys) - prevent filename bruteforcing
* [dirkeys](#dirkeys) - share specific folders in a volume
@@ -524,6 +526,7 @@ upgrade notes
per-folder, per-user permissions - if your setup is getting complex, consider making a [config file](./docs/example.conf) instead of using arguments
* much easier to manage, and you can modify the config at runtime with `systemctl reload copyparty` or more conveniently using the `[reload cfg]` button in the control-panel (if the user has `a`/admin in any volume)
* changes to the `[global]` config section requires a restart to take effect
* when using config-files, ignore all the `-a` and `-v` stuff below, and CTRL-F `config file example` on this page instead
a quick summary can be seen using [`--help-accounts`](https://copyparty.eu/cli/#accounts-help-page)
@@ -573,6 +576,8 @@ and if you want to use config files instead of commandline args (good!) then her
* you can also `PRTY_CONFIG=foobar.conf python copyparty-sfx.py` (convenient in docker etc)
config file example:
```yaml
[accounts]
u1: p1 # create account "u1" with password "p1"
@@ -921,7 +926,7 @@ note that since up2k has to read each file twice, `[🎈] bup` can *theoreticall
if you are resuming a massive upload and want to skip hashing the files which already finished, you can enable `turbo` in the `[⚙️] config` tab, but please read the tooltip on that button
if the server is behind a proxy which imposes a request-size limit, you can configure up2k to sneak below the limit with server-option `--u2sz` (the default is 96 MiB to support Cloudflare)
if the server is behind a proxy which imposes a request-size limit, you can configure up2k to sneak below the limit with server-option [--u2sz](#u2sz)
if you want to replace existing files on the server with new uploads by default, run with `--u2ow 2` (only works if users have the delete-permission, and can still be disabled with `🛡️` in the UI)
@@ -1825,6 +1830,23 @@ config file example:
```
## u2sz
configure upload-chunking; the default `64,96` is optimal for nearly every scenario, however:
* if you are seeing the message `bad server config; see "u2sz" in readme` then you have a proxy/CDN/whatever with a `max-request-body-size` which is too small, and you must either increase the limit in the reverseproxy config to at least 96 MiB (recommended) or reduce the target chunksize in copyparty (continue reading)
the global-option `--u2sz` is two numbers, `target,max` MiB of each chunk to send, aiming for `target` MiB and allowing up-to-and-including `max` MiB [depending on filesize](./docs/devnotes.md#list-of-chunk-sizes), and if you only specify one number then it sets that for both
cloudflare max is 96 MiB, but if you have a reverseproxy (or CDN/WAF/whatever) which is more restrictive, then try some of the following:
* `u2sz: 32` is still acceptable performance in most cases
* `u2sz: 8` is the lowest recommended value; anything lower will reduce performance over long physical distances and/or shitty routes
* `u2sz: 1` is the absolute minimum, and will be slow in many cases
the setting can also be changed in the web-UI; the textbox next to `date-chk` in the settings tab
## upload rules
set upload rules using volflags, some examples:
@@ -2176,7 +2198,7 @@ as for client-side stuff, there is [plugins for modifying UI/UX](./contrib/plugi
autologin based on IP range (CIDR) , using the global-option `--ipu`
for example, if everyone with an IP that starts with `192.168.123` should automatically log in as the user `spartacus`, then you can either specify `--ipu=192.168.123.0/24=spartacus` as a commandline option, or put this in a config file:
for example, if everyone with an IP that starts with `192.168.123` should automatically log in as the user `spartacus`, then you can either specify `--ipu=192.168.123.0/24=spartacus` as a commandline option, same as this config file example:
```yaml
[global]
@@ -2192,7 +2214,7 @@ repeat the option to map additional subnets
limit a user to certain IP ranges (CIDR) , using the global-option `--ipr`
for example, if the user `spartacus` should get rejected if they're not connecting from an IP that starts with `192.168.123` or `172.16`, then you can either specify `--ipr=192.168.123.0/24,172.16.0.0/16=spartacus` as a commandline option, or put this in a config file:
for example, if the user `spartacus` should get rejected if they're not connecting from an IP that starts with `192.168.123` or `172.16`, then you can either specify `--ipr=192.168.123.0/24,172.16.0.0/16=spartacus` as a commandline option, same as this config file example:
```yaml
[global]
@@ -2229,7 +2251,7 @@ but if you just want to let users change their own passwords, then you probably
other ways to auth by header
if you have a middleware which adds a header with a user identifier, for example tailscale's `Tailscale-User-Login: alice.m@forest.net` then you can automatically auth as `alice` by defining that mapping with `--idp-hm-usr '^Tailscale-User-Login^alice.m@forest.net^alice'` or the following config file:
if you have a middleware which adds a header with a user identifier, for example tailscale's `Tailscale-User-Login: alice.m@forest.net` then you can automatically auth as `alice` by defining that mapping with `--idp-hm-usr '^Tailscale-User-Login^alice.m@forest.net^alice'` or the following config file example:
```yaml
[global]
@@ -2550,7 +2572,7 @@ change the association of a file extension
using commandline args, you can do something like `--mime gif=image/jif` and `--mime ts=text/x.typescript` (can be specified multiple times)
in a config file, this is the same as:
that's the same as this config file example:
```yaml
[global]
@@ -2617,7 +2639,7 @@ if your distro/OS is not mentioned below, there might be some hints in the [«on
`pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/))
it comes with a [systemd service](./contrib/systemd/copyparty@.service) as well as a [user service](./contrib/systemd/copyparty-user.service), and expects to find a [config file](./contrib/systemd/copyparty.example.conf) in `/etc/copyparty/copyparty.conf` or `~/.config/copyparty/copyparty.conf`
it comes with a [systemd service](./contrib/systemd/copyparty@.service) as well as a [user service](./contrib/systemd/copyparty-user.service), and expects to find a [config file](./contrib/systemd/copyparty.example.conf) in `/etc/copyparty.conf` or `~/.config/copyparty/copyparty.conf`
after installing, start either the system service or the user service and navigate to http://127.0.0.1:3923 for further instructions (unless you already edited the config files, in which case you are good to go, probably)
@@ -3107,6 +3129,23 @@ behavior that might be unexpected
* or eliminate the problem entirely by only giving write-access to trustworthy people :^)
## csp nonce
unauthorized javascript is blocked by means of the default [content-security-policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) which enforces "script nonces", to help prevent XSS and similar
usually this Just Works and is correctly configured by default; you can stop reading this section and move on, nothing to see here
but if you are the unlucky 0.1% with a hosting-setup funky enough for this to not work as intended, then you will also be seeing this message: `Looks like javascript is broken; see "csp nonce" in the readme`
there is two possible reasons why this might happen:
1. your browser is simply too old to understand the javascript, which is not likely, because just about every browser is supported, even internet explorer 11
2. something in your hosting-setup is tampering with the responses from copyparty and destroying this security feature; the culprit is probably a misconfigured reverseproxy, a CDN, a WAF, or some other mitm, but a popular offender is **"cloudflare rocket loader"**
you can try to **temporarily** set global-options `--csp-ui no` and `--csp-dl no` and restart copyparty to check whether it is due to reason #2. Remember to refresh the webbrowser with CTRL-SHIFT-R to check properly. If that makes the problem go away, then turn them back off (**never** keep them enabled), restart copyparty, open the `Network` tab in the webbrowser, and refresh the website; you should see the `content-security-policy` header including a nonce, for example `nonce-BxcJbvdu1z3S/ycPBjVWuA==` which should also appear inside the HTML of that response like this: `<script nonce="BxcJbvdu1z3S/ycPBjVWuA==">` -- if it doesn't then that's the problem; something in your hosting stack is dangerously misconfigured
## cors
cross-site request config
+1
View File
@@ -36,6 +36,7 @@ these are `--xiu` hooks; unlike `xbu` and `xau` (which get executed on every sin
* [reject-and-explain.py](reject-and-explain.py) shows a custom error-message when it rejects an upload
* [reject-ramdisk.py](reject-ramdisk.py) rejects the upload if the destination is a ramdisk
* this hook uses the `I` flag which makes it 140x faster, but if the plugin has a bug it may crash copyparty
* [phonecam-sorter.py](phonecam-sorter.py) is for syncing backups of phone gallery into year/month folders
# more upload stuff
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
import os
import re
import sys
_ = r"""
purpose:
sync/backup phone gallery (pics/vids) to a nas which runs copyparty
what this hook does:
redirect photos/videos into date-based folders, named "%Y%m" according
to filename; that is, "/cam/sort/DCIM/D027/PXL_20250828_202820075.jpg"
will be redirected to "/cam/pics/ts/202508/PXL_20250828_202820075.jpg"
requires this setup on the server:
* /cam/ is a volume with all your pics/vids
* the phone must upload to somewhere below /cam/sort/
* photo/video filenames match the following regex:
^[a-z]{3,4}_([0-9]{6})[0-9]{2}_[0-9]{6}.*\.(jpg|mp4)$
* so for example these filenames are OK:
* PXL_20250828_202820075.jpg
* IMG_20251123_202405.jpg
* PANO_20180421_121245_03.t8o5.jpg
* VID_20250307_220250-crf34.mp4
example server config as a volflag:
-v /mnt/thedisk/gallery:cam:rwa,ed:c,e2ds,xbu=I,~/hooks/phonecam-sorter.py
example server config in a copyparty config file:
[/cam]
/mnt/thedisk/gallery
accs:
rwa: ed
flags:
e2ds
xbu: I,~/hooks/phonecam-sorter.py
command to run on phone to sync to server, sending new files only:
~/bin/u2c.py -te homenas.ca -a '$homenas.pw' https://10.0.0.2:3923/cam/sort/ ~/storage/dcim/Camera
the phone-command above assumes this setup on the phone:
* termux or another linux-like thing with access to gallery files
* homenas.ca is a textfile with the server's CA certificate
* homenas.pw is a textfile with the copyparty password inside
* ~/storage/dcim/ exists; if not, run termux-setup-storage
* ~/bin/u2c.py exists; if not, curl -LO https://10.0.0.2:3923/.cpr/a/u2c.py && chmod 755 u2c.py
unfortunate consequences:
* due to the redirection, u2c -z doesn't do anything, so every local
file is hashed every time, wasting some time and cpu on the phone
but otoh certainly avoids corruption on either side (phone/server)
"""
def main(inf):
log = inf["log"]
vdir, fn = os.path.split(inf["vp"])
if not vdir.startswith("cam/sort"):
return {"rc": 0} # 2nd call after 1st reloc; already done
m = re.match(r"^[a-z]{3,4}_([0-9]{6})[0-9]{2}_[0-9]{6}.*\.(jpg|mp4)$", fn.lower())
if not m:
return {"rc": 0} # not recognized; write to original path in /cam/sort/
ym, ext = m.groups()
if ext == "jpg":
dst = f"/cam/pics/ts/{ym}/"
elif ext == "mp4":
dst = f"/cam/vids/ts/{ym}/"
else:
return {"rc": 0}
ap = "%s%s" % (inf["ap"].split("/cam/sort/")[0], dst)
if not os.path.isdir(ap):
os.makedirs(ap)
ret = {"rc": 0, "reloc": {"vp": dst}}
return ret
if __name__ == "__main__":
main()
+55
View File
@@ -0,0 +1,55 @@
# thumbnail extractors
extract (or generate) thumbnails for custom file formats or override default thumbnail extraction for standard media files
## usage
* load plugins with `--th-extract baz=~/dev/copyparty/bin/thumbs/randomcolor.py`
* `baz` is the file extension (may be comma-separated list of extensions)
* add multiple different extractor plugins by repeating the `--th-extract` argument
## api
### in
each plugin must define a function `main(abspath, **kwargs)`:
* `abspath` is the path to a file to extract thumbnail for
* `kwargs` currently receives these keyword arguments:
* `vn` is the VFS which contains the requested file
* `th_srv` is an instance of [copyparty/th_srv](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/th_srv.py)
### out
the `main()` function must
* return `None` or raise an `Exception` in case it is unable to extract thumbnail
* return tuple `fmt, stream, offset, whence, size` otherwise:
* `fmt: str` – format (filename extension) of the thumbnail; should be one of the image formats supported by copyparty (see `--th-r-*` options)
* `stream: IO[bytes]` – binary file-like object supporting `seek()`, `read()`, `close()` methods; should contain extracted thumbnail image
* `offset: int, whence: int` – arguments to `stream.seek()`; copyparty will call `stream.seek(offset, whence)` once before starting reading the stream contents
* `size: int | None` – number of bytes to read from the `stream`; if `size` is negative or `None`, copyparty will read until the end of the stream
### notes
it is possible to extract or generate custom thumbnails for standard media files too,
e.g. run copyparty with `--th-extract mp3,m4a,aac,flac,opus=~/dev/copyparty/bin/thumbs/my_custom_mp3_th_extractor.py` and do your magic with audio file thumbnails
in case copyparty doesn't like the `fmt` option, it will `close()` the `stream` without ever calling `seek()` or `read()`
if your scenario involves heavy computation or i/o, it is better to defer the heavy parts until the first `seek()` or `read()` to avoid redundant work in case the result gets rejected based on `fmt` value
if you provide custom `stream` object:
copyparty performs buffered reading, so expect multiple `read()` calls, respect `size` argument in `read(size)` calls, and maintain correct seek position
## examples
* [randomcolor](https://github.com/9001/copyparty/blob/hovudstraum/bin/thumbs/randomcolor.py) generates a random .gif image
* [collabora](https://github.com/9001/copyparty/blob/hovudstraum/bin/thumbs/collabora.py) uses external self-hosted service (collabora online) to generate previews for various office documents, pdf and fb2 books, html, markdown, text files and some image formats; collabora is also supported as office editor in copyparty, see [wopi](https://github.com/9001/copyparty#wopi-server)
## some other known plugins seen on the internets
* [fpkg_thumb](https://github.com/kamaeff/fpkg-vault/blob/1c248de62a9ff9c4088d4b3e7962023cde9d6426/old/fpkg_thumb.py) extracts cover images from playstation4 software installation packages ("pkg" and "fpkg" files)
+125
View File
@@ -0,0 +1,125 @@
"""
CAUTION!
looks like collabora spawns separate jailed libreoffice instance to do the conversion
this may eat up too much ram in folders with many docs or with --th-pregen enabled
consider limiting resources available for collabora
e.g. for me these lines in docker-compose.yml helped alot:
deploy:
resources:
limits:
cpus: '2'
memory: '2G'
/CAUTION!
if you use collabora office editor with copyparty, you already have a thumbnailer for office formats:
https://sdk.collaboraonline.com/docs/conversion_api.html
the 'convert-to' api accepts files in multipart/form-data format
just add this extractor to copyparty with an argument like this:
--th-extract=docx,xlsx,pptx,odt,ods,pdf,fb2=~/dev/copyparty/bin/thumbs/collabora.py
and set COLLABORA_INSTANCE_URL below to your collabora instace url
full list of supported file types:
https://sdk.collaboraonline.com/docs/conversion_api.html#supported-input-formats
this is also an example of deferred IO:
no files opened and no network requests made if copyparty doesn't like the file type
"""
import os
import string
import random
import mimetypes
from urllib.error import HTTPError
from urllib.request import Request, urlopen
COLLABORA_INSTANCE_URL = "http://collabora:9980"
BOUNDARY_CHARS = string.ascii_letters + string.digits
def main(abspath, **kwargs):
boundary = "CopypartyFormData" + "".join(random.choices(BOUNDARY_CHARS, k=53))
start, end = make_headers(abspath, boundary)
size = len(start) + os.path.getsize(abspath) + len(end)
request = Request(
COLLABORA_INSTANCE_URL + "/cool/convert-to/png",
method="POST",
data=iterate(start, abspath, end),
headers={
"Content-Type": f"multipart/form-data; boundary={boundary}",
"Content-Length": str(size),
},
)
file_like = Wrapper(request)
offset, whence, length = 0, 0, None
return "png", file_like, offset, whence, length
def make_headers(abspath, boundary):
"""stuff to wrap file contents by for multipart"""
fname = "file." + abspath.rsplit(".", 1)[-1]
ftype = mimetypes.guess_file_type(fname)[0] or "application/octet-stream"
start = (
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="data"; filename="{fname}"\r\n'
f"Content-Type: {ftype}\r\n\r\n"
)
end = f"\r\n--{boundary}--"
return bytes(start, "ascii"), bytes(end, "ascii")
def iterate(start, fpath, end):
"""docs may be big so read and send them in chunks"""
yield start
with open(fpath, "rb") as f:
while True:
b = f.read(32768)
if not b:
break
yield b
yield end
class Wrapper:
"""
This file-like wrapper defers expensive work until first .seek() call
Useful to not waste resources in case copyparty decides
to throw our thumbnail away based on returned file extension
"""
def __init__(self, request):
self.request = request
self.response = None
def seek(self, offset, whence=0):
if self.response:
return 0
try:
# the heavy part:
# - reading probably big file
# - sending it over the network
# - making collabora do some work
self.response = urlopen(self.request, timeout=10)
except HTTPError as e:
e.close()
raise e
except Exception as e:
self.close()
raise e
# actual response is not seekable
# just return 0
return 0
def read(self, amount):
return self.response.read(amount)
def close(self):
if self.response:
self.response.close()
+29
View File
@@ -0,0 +1,29 @@
import io
import os
import struct
from base64 import b64decode
def create_gif() -> bytes:
head = r"R0lGODdhIAAgAIAB"
tail = r"AAD/LAAAAAAgACAAAAIehI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC5sFADs="
#color = struct.pack(">L", random.randrange(0xffffff)) # wow, way too stable
color = b"\x00" + os.urandom(3)
return b64decode(head) + color + b64decode(tail)
def main(abspath, **ka) -> Optional[tuple[str, IO[bytes], int, int, Optional[int]]]:
print("hello from randomcolor.py pretending to extract a thumb from " + abspath)
# need to return a file-like object
gif_bytes = create_gif()
file_obj = io.BytesIO(gif_bytes)
# where copyparty should seek to in the "file" to start reading from
seek_offset = 0
seek_whence = 0
filesize = None # auto; read until end
# "gif" is the file-extension to decode the file as
return "gif", file_obj, seek_offset, seek_whence, filesize
+6 -3
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
from __future__ import division, print_function, unicode_literals
S_VERSION = "2.22"
S_BUILD_DT = "2026-06-13"
S_VERSION = "2.23"
S_BUILD_DT = "2026-09-06"
"""
u2c.py: upload to copyparty
@@ -1576,7 +1576,7 @@ NOTE: if server has --usernames enabled, then password is "username:password"
ap.add_argument("url", type=unicode, help="server url, including destination folder")
ap.add_argument("files", type=files_decoder, nargs="+", help="files and/or folders to process")
ap.add_argument("-v", action="store_true", help="verbose")
ap.add_argument("-a", metavar="PASSWD", default="", help="password (or $filepath) for copyparty (is sent in header 'PW')")
ap.add_argument("-a", metavar="PASSWD", default="", help="password (or $filepath) for copyparty (is sent in header 'PW'), default is env.var U2C_PW")
ap.add_argument("--ba", metavar="PASSWD", default="", help="password (or $filepath) for basic-auth (usually not necessary)")
ap.add_argument("-s", action="store_true", help="file-search (disables upload)")
ap.add_argument("-x", type=unicode, metavar="REGEX", action="append", help="skip file if filesystem-abspath matches REGEX (option can be repeated), example: '.*/\\.hist/.*'")
@@ -1729,6 +1729,9 @@ NOTE: if server has --usernames enabled, then password is "username:password"
with open(zs[1:], "rb") as f:
setattr(ar, k, f.read().decode("utf-8").strip())
if not ar.a:
ar.a = os.environ.get("U2C_PW", "")
if ar.ba:
ar.ba = "Basic " + base64.b64encode(ar.ba.encode("utf-8")).decode("utf-8")
+4
View File
@@ -17,8 +17,12 @@ defaults
listen foo1
bind *:8081
http-request add-header X-Forwarded-Proto https if { ssl_fc }
http-request add-header X-Forwarded-Proto http unless { ssl_fc }
server srv1 127.0.0.1:3923 maxconn 512
listen foo2
bind *:8082
http-request add-header X-Forwarded-Proto https if { ssl_fc }
http-request add-header X-Forwarded-Proto http unless { ssl_fc }
server srv1 /dev/shm/party.sock maxconn 512
+1
View File
@@ -285,6 +285,7 @@ in
environment = {
PYTHONUNBUFFERED = "true";
NIX_SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
XDG_CONFIG_HOME = externalStateDir;
};
+10 -12
View File
@@ -3,7 +3,7 @@
# NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead.
pkgname=copyparty
pkgver="1.20.18"
pkgver="1.20.22"
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any")
@@ -13,23 +13,22 @@ depends=("bash" "python" "lsof" "python-jinja")
makedepends=("python-wheel" "python-setuptools" "python-build" "python-installer" "make" "pigz")
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
"cfssl: generate TLS certificates on startup"
"python-mutagen: music tags (alternative)"
"python-paramiko: sftp server",
"python-mutagen: music tags (fast/inaccurate)"
"python-pillow: thumbnails for images"
"python-pyvips: thumbnails for images (eats ram, prefer pillow)"
"python-paramiko: sftp server"
"python-pyftpdlib: ftp server"
"python-pyopenssl: ftps server (needs pyftpdlib too)"
"libkeyfinder: detection of musical keys"
"python-pyopenssl: ftps functionality"
"python-pyzmq: send zeromq messages from event-hooks"
"python-argon2-cffi: hashed passwords in config"
)
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("etc/${pkgname}/copyparty.conf" )
sha256sums=("ea1347a2e30b4261282ac4609c9b844768a925eed717ac8af1ff87a8a326508e")
backup=("etc/copyparty.conf")
install=copyparty.install
sha256sums=("14d590644943c3ed14e21093cce79e06f1ae206adeeda16032d15a7241ff4344")
build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
make
cd "${srcdir}/${pkgname}-${pkgver}"
python -m build --wheel --no-isolation
}
@@ -38,12 +37,11 @@ package() {
cd "${srcdir}/${pkgname}-${pkgver}"
python -m installer --destdir="$pkgdir" dist/*.whl
install -dm755 "${pkgdir}/etc/${pkgname}"
install -Dm755 "bin/prisonparty.sh" "${pkgdir}/usr/bin/prisonparty"
install -Dm644 "contrib/systemd/${pkgname}.conf" "${pkgdir}/etc/${pkgname}/copyparty.conf"
install -Dm644 "contrib/systemd/${pkgname}.conf" "${pkgdir}/etc/copyparty.conf"
install -Dm644 "contrib/systemd/${pkgname}@.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}@.service"
install -Dm644 "contrib/systemd/${pkgname}-user.service" "${pkgdir}/usr/lib/systemd/user/${pkgname}.service"
install -Dm644 "contrib/systemd/prisonparty@.service" "${pkgdir}/usr/lib/systemd/system/prisonparty@.service"
install -Dm644 "contrib/systemd/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md"
install -Dm644 "contrib/systemd/index.md" "${pkgdir}/var/lib/${pkgname}-jail/srv/README.md"
install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
}
+27
View File
@@ -0,0 +1,27 @@
post_upgrade() {
# v1.20.22 moved the config-file, affecting the system-service only
if [[ $(vercmp "$2" 1.20.22) -lt 0 ]]; then
oldpath="/etc/copyparty/copyparty.conf"
newpath="/etc/copyparty.conf"
echo "==> Migrating config-file from $oldpath to $newpath"
if [[ ! -f "$oldpath" ]]; then
oldpath="$oldpath.pacsave"
fi
if [[ -f "$newpath.pacnew" ]]; then
echo "==> WARNING: A config-file already existed in the new location [$newpath]. The existing file will be used as-is."
elif [[ -f "$oldpath" ]]; then
mv -v "$oldpath" "$newpath"
rmdir /etc/copyparty 2>/dev/null || true
fi
systemctl cat copyparty@.service | awk '/=PRTY_CONFIG=/ && !/=\/etc\/copyparty.conf/ {exit 1}' || {
echo "==> WARNING: The copyparty system-service has been edited by hand since installation, so it will not read config from the new default config path [$newpath]. Please use PRTY_CONFIG=$newpath if that is correct for your setup."
}
fi
}
+2 -5
View File
@@ -2,7 +2,7 @@
pkgname=copyparty
pkgver=1.20.18
pkgver=1.20.22
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any")
@@ -21,12 +21,9 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
)
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("/etc/${pkgname}.d/init" )
sha256sums=("ea1347a2e30b4261282ac4609c9b844768a925eed717ac8af1ff87a8a326508e")
sha256sums=("14d590644943c3ed14e21093cce79e06f1ae206adeeda16032d15a7241ff4344")
build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
make
cd "${srcdir}/${pkgname}-${pkgver}"
python -m build --wheel --no-isolation
}
@@ -15,7 +15,7 @@ Description=copyparty file server
Type=notify
SyslogIdentifier=copyparty
Environment=PYTHONUNBUFFERED=x
WorkingDirectory=/var/lib/copyparty-jail
WorkingDirectory=/var/lib/copyparty-jail/srv
ExecReload=/bin/kill -s USR1 $MAINPID
# user to run as + where the TLS certificate is (if any)
+12 -2
View File
@@ -1,3 +1,13 @@
this is `/var/lib/copyparty-jail`, the fallback webroot when copyparty has not yet been configured
this is `/var/lib/copyparty-jail/srv/`, the fallback webroot when copyparty has not yet been configured
please add some `*.conf` files to `/etc/copyparty.d/`
please edit the config-file and restart copyparty;
* if running as a system service: `vim /etc/copyparty.conf`
* if running as a user service: `vim $HOME/.config/copyparty/copyparty.conf`
some inspiration:
* a basic example config: https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf
* another example with focus on syntax: https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf
* and CTRL-F `config file example` here: https://github.com/9001/copyparty
the full list of configuration options can be seen at https://copyparty.eu/cli/
or by running `copyparty --help`
@@ -17,7 +17,7 @@ Description=copyparty file server
[Service]
SyslogIdentifier=prisonparty
Environment=PYTHONUNBUFFERED=x
WorkingDirectory=/var/lib/copyparty-jail
WorkingDirectory=/var/lib/copyparty-jail/srv
ExecReload=/bin/kill -s USR1 $MAINPID
# stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running
+3 -3
View File
@@ -1,5 +1,5 @@
{
"url": "https://github.com/9001/copyparty/releases/download/v1.20.18/copyparty-1.20.18.tar.gz",
"version": "1.20.18",
"hash": "sha256-6hNHouMLQmEoKsRgnJuER2ipJe7XF6yK8f+HqKMmUI4="
"url": "https://github.com/9001/copyparty/releases/download/v1.20.22/copyparty-1.20.22.tar.gz",
"version": "1.20.22",
"hash": "sha256-FNWQZElDw+0U4hCTzOeeBvGuIGre7aFgMtFackH/Q0Q="
}
-3
View File
@@ -26,9 +26,6 @@ See release at https://github.com/9001/copyparty/releases
%setup -q
%build
cd "copyparty/web"
make
cd -
%pyproject_wheel
%install
+3 -3
View File
@@ -1,5 +1,5 @@
# this will start `/usr/bin/copyparty`
# and read config from `$HOME/.config/copyparty.conf`
# and read config from `$HOME/.config/copyparty/copyparty.conf`
#
# unless you add -q to disable logging, you may want to remove the
# following line to allow buffering (slightly better performance):
@@ -11,13 +11,13 @@ Description=copyparty file server
[Service]
Type=notify
SyslogIdentifier=copyparty
WorkingDirectory=/var/lib/copyparty-jail
WorkingDirectory=/var/lib/copyparty-jail/srv
Environment=PYTHONUNBUFFERED=x
Environment=PRTY_CONFIG=%h/.config/copyparty/copyparty.conf
ExecReload=/bin/kill -s USR1 $MAINPID
# ensure there is a config
ExecStartPre=/bin/bash -c 'if [[ ! -f %h/.config/copyparty/copyparty.conf ]]; then mkdir -p %h/.config/copyparty; cp /etc/copyparty/copyparty.conf %h/.config/copyparty/copyparty.conf; fi'
ExecStartPre=/bin/bash -c 'if [[ ! -f %h/.config/copyparty/copyparty.conf ]]; then mkdir -p %h/.config/copyparty; cp -v /etc/copyparty.conf %h/.config/copyparty/; fi'
# run copyparty
ExecStart=/usr/bin/python3 /usr/bin/copyparty
+13 -11
View File
@@ -1,13 +1,15 @@
[global]
i: 127.0.0.1
[accounts]
user: password
[/]
/var/lib/copyparty-jail
/var/lib/copyparty-jail/srv/
accs:
r: *
rwdma: user
flags:
grid
r: * # everyone can [r]ead (browse/download)
# above is a very simple config;
# the webroot is /var/lib/copyparty-jail/srv/
# readable by everyone, no login required
#
# you'll want to replace this file; some inspiration:
# - a basic example config: https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf
# - another example with focus on syntax: https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf
# - and CTRL-F "config file example" here: https://github.com/9001/copyparty
# - full list of options: https://copyparty.eu/cli/
-3
View File
@@ -3,9 +3,6 @@
# vim: ft=yaml:
# put this file in /etc/
[global]
e2dsa # enable file indexing and filesystem scanning
e2ts # and enable multimedia indexing
+3 -3
View File
@@ -1,5 +1,5 @@
# this will start `/usr/bin/copyparty`
# and read config from `/etc/copyparty/copyparty.conf`
# and read config from `/etc/copyparty.conf`
#
# the %i refers to whatever you put after the copyparty@
# so with copyparty@foo.service, %i == foo
@@ -14,9 +14,9 @@ Description=copyparty file server
[Service]
Type=notify
SyslogIdentifier=copyparty
WorkingDirectory=/var/lib/copyparty-jail
WorkingDirectory=/var/lib/copyparty-jail/srv
Environment=PYTHONUNBUFFERED=x
Environment=PRTY_CONFIG=/etc/copyparty/copyparty.conf
Environment=PRTY_CONFIG=/etc/copyparty.conf
ExecReload=/bin/kill -s USR1 $MAINPID
# user to run as + where the TLS certificate is (if any)
+9 -6
View File
@@ -1,10 +1,13 @@
this is `/var/lib/copyparty-jail`, the fallback webroot when copyparty has not yet been configured
this is `/var/lib/copyparty-jail/srv/`, the fallback webroot when copyparty has not yet been configured
please edit `/etc/copyparty/copyparty.conf` (if running as a system service)
or `$HOME/.config/copyparty/copyparty.conf` if running as a user service
please edit the config-file and restart copyparty;
* if running as a system service: `vim /etc/copyparty.conf`
* if running as a user service: `vim $HOME/.config/copyparty/copyparty.conf`
a basic configuration example is available at https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf
a configuration example that explains most flags is available at https://github.com/9001/copyparty/blob/hovudstraum/docs/chungus.conf
some inspiration:
* a basic example config: https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf
* another example with focus on syntax: https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf
* and CTRL-F `config file example` here: https://github.com/9001/copyparty
the full list of configuration options can be seen at https://ocv.me/copyparty/helptext.html
the full list of configuration options can be seen at https://copyparty.eu/cli/
or by running `copyparty --help`
+3 -3
View File
@@ -1,6 +1,6 @@
# this will start `/usr/bin/copyparty`
# in a chroot, preventing accidental access elsewhere,
# and read copyparty config from `/etc/copyparty/copyparty.conf`
# and read copyparty config from `/etc/copyparty.conf`
#
# expose additional filesystem locations to copyparty
# by listing them between the last `%i` and `--`
@@ -19,9 +19,9 @@ Description=copyparty file server
[Service]
Type=notify
SyslogIdentifier=prisonparty
WorkingDirectory=/var/lib/copyparty-jail
WorkingDirectory=/var/lib/copyparty-jail/srv
Environment=PYTHONUNBUFFERED=x
Environment=PRTY_CONFIG=/etc/copyparty/copyparty.conf
Environment=PRTY_CONFIG=/etc/copyparty.conf
ExecReload=/bin/kill -s USR1 $MAINPID
# user to run as + where the TLS certificate is (if any)
+3
View File
@@ -40,6 +40,9 @@ VT100 = "--ansi" in sys.argv or (
)
# introduced in anniversary update
zs = os.environ.get("PRTY_CONFIG", "")
CFG_DEF = [zs] if zs else []
ANYWIN = WINDOWS or sys.platform in ["msys", "cygwin"]
MACOS = platform.system() == "Darwin"
+154 -16
View File
@@ -14,6 +14,7 @@ import locale
import os
import re
import select
import shutil
import socket
import sys
import threading
@@ -23,6 +24,7 @@ import uuid
from .__init__ import (
ANYWIN,
CFG_DEF,
CORES,
EXE,
MACOS,
@@ -38,7 +40,10 @@ from .__init__ import (
from .__version__ import CODENAME, S_BUILD_DT, S_VERSION
from .authsrv import expand_config_file, split_cfg_ln, upgrade_cfg_fmt
from .bos import bos
from .cert import NO_TLS
from .cfg import flagcats, onedash
from .mdns import DNS_VND
from .qrkode import VENDORED as QR_VND
from .svchub import SvcHub
from .util import (
APPLESAN_TXT,
@@ -48,6 +53,8 @@ from .util import (
DEF_MTH,
HAVE_BWRAP,
HAVE_IPV6,
HAVE_MAGIC,
IFADDR_VND,
IMPLICATIONS,
JINJA_VER,
MIKO_VER,
@@ -63,6 +70,7 @@ from .util import (
URL_BUG,
URL_PRJ,
Daemon,
absreal,
align_tab,
b64enc,
ctypes,
@@ -94,21 +102,18 @@ if PY2:
range = xrange # type: ignore
try:
if os.environ.get("PRTY_NO_TLS"):
if NO_TLS:
raise Exception()
HAVE_SSL = True
import ssl
HAVE_SSL = True
except:
HAVE_SSL = False
u = unicode
zsid = uuid.uuid4().urn[4:]
CFG_DEF = [os.environ.get("PRTY_CONFIG", "")]
if not CFG_DEF[0]:
CFG_DEF.pop()
FULL_HELP = os.environ.get("PRTY_FULL_HELP", "")
@@ -586,6 +591,17 @@ def sfx_tpoke(top: str):
lprint("<TPOKE> [%s] %r" % (f, ex))
files.remove(f)
r = os.path.dirname(top)
for d in os.listdir(r):
try:
p = r + "/" + d
f = p + "/copyparty/up2k.py"
t2 = os.path.getmtime(f)
if t - t2 > 777777 and d.startswith("pe-copyparty"):
shutil.rmtree(p)
except:
pass
time.sleep(78123)
@@ -596,12 +612,64 @@ def showlic() -> None:
except:
buf = b""
if buf:
print(buf.decode("utf-8", "replace"))
else:
if not buf:
print("no relevant license info to display")
return
lic = buf.decode("utf-8", "replace")
try:
import site
from importlib import import_module
from inspect import getsourcefile
bases = site.PREFIXES + [site.USER_BASE]
bases = [x.replace(os.sep, "/") + "/" for x in list(set(bases)) if x]
except:
bases = []
# remove missing and/or non-vendored
for url, zi, zx in (
("/dnslib/", 1, DNS_VND),
("/ifaddr/", 1, IFADDR_VND),
("/nayuki/QR", 1, QR_VND),
("/partftpy", 2, PARTFTPY_VER),
("/jinja/", 3, "jinja2"),
("/markupsafe/", 3, "jinja2"), # jinja2 sic
("/pyftpdlib/", 2, PYFTPD_VER), # heed PRTY_NO
("/pyftpdlib/", 3, "pyftpdlib"),
("/python-magic/", 1, HAVE_MAGIC), # heed PRTY_NO
("/python-magic/", 3, "magic"),
):
if url not in lic:
continue
elif zi == 1:
if zx:
continue
elif zi == 2:
if zx != "(None)":
continue
elif zi == 3:
mod = None
try:
assert bases and getsourcefile and import_module # type: ignore # !rm
mod = import_module(zx) # type: ignore
mp = getsourcefile(mod) or ""
for zs in bases:
if mp.startswith(zs):
mp = ""
if mp:
continue
except:
if mod:
continue
zs1, zs2 = lic.split(url, 1)
zs1 = zs1.rsplit("\n", 1)[0]
zs2 = zs2.split("\n\n", 1)[1]
lic = zs1 + "\n" + zs2
print(lic)
def get_sects():
return [
@@ -779,6 +847,54 @@ def get_sects():
).rstrip()
+ build_flags_desc(),
],
[
"thumb-ex",
"extract thumbnails from custom file formats with python scripts",
dedent(
"""
copyparty is able to extract thumbnails from various media formats:
images, audio, video, ebooks
if you want to extract thumbnails from unsupported file formats,
you can load a plugin, which extracts image data from custom formats
load the plugin using --args; for example \033[36m
--tx-extract iso,mdf,dmg=~/party-thumb-extractors/th_diskimg.py
\033[0m
the file must define the function \033[35mmain(abspath, **kwargs)\033[0m:
\033[35mabspath\033[0m: path to the file to extract thumbnail from
\033[35mkwargs\033[0m currently have:
\033[35mvn\033[0m: the VFS which contains the requested file
\033[35mrth_srv\033[0m: the copyparty ThumbSrv instance
if `main` couldn't extract a thumbnail, it must return None;
otherwise the return value must be a tuple:
\033[35mreturn fmt, stream, offset, whence, size\033[0m
> \033[32mfmt\033[0m: thumbnail format, should be one of the formats
supported by copyparty; e.g. "png", "jpg"
> \033[32mstream\033[0m: binary file-like object containing the thumbnail,
supporting seek(), read(), and close() methods
> \033[32moffset, whence\033[0m: arguments to seek() method;
stream.seek(offset, whence) will be called once before copyparty
starts reading from the stream
> \033[32msize\033[0m: number of bytes to read ftom the stream;
if negative, copyparty will read through the end of the stream;
you can also supply extractors for standard formats to override default
thumbnail extraction behaviour; for example \033[36m
--tx-extract mp3=~/party-thumb-extractors/th_mp3.py
\033[0mwill let you extract or generate custom thumbnails for mp3 files
\033[1;35mPS!\033[0m the folder that contains the python file should ideally
not contain many other python files, and especially nothing
with filenames that overlap with modules used by copyparty
"""
),
],
[
"handlers",
"use plugins to handle certain events",
@@ -1338,7 +1454,7 @@ def add_upload(ap):
ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; [\033[32m-1\033[0m] = forbidden/always-off, [\033[32m0\033[0m] = default-off and warn if enabled, [\033[32m1\033[0m] = default-off, [\033[32m2\033[0m] = on, [\033[32m3\033[0m] = on and disable datecheck")
ap2.add_argument("--nosubtle", metavar="N", type=int, default=0, help="when to use a wasm-hasher instead of the browser's builtin; faster on chrome, but buggy in older chrome versions. [\033[32m0\033[0m] = only when necessary (non-https), [\033[32m1\033[0m] = always (all browsers), [\033[32m2\033[0m] = always on chrome/firefox, [\033[32m3\033[0m] = always on chrome, [\033[32mN\033[0m] = chrome-version N and newer (recommendation: 137)")
ap2.add_argument("--u2j", metavar="JOBS", type=int, default=2, help="web-client: number of file chunks to upload in parallel; 1 or 2 is good when latency is low (same-country), 2~4 for android-clients, 2~6 for cross-atlantic. Max is 6 in most browsers. Big values increase network-speed but may reduce HDD-speed")
ap2.add_argument("--u2sz", metavar="N,N,N", type=u, default="1,64,96", help="web-client: default upload chunksize (MiB); sets \033[33mmin,default,max\033[0m in the settings gui. Each HTTP POST will aim for \033[33mdefault\033[0m, and never exceed \033[33mmax\033[0m. Cloudflare max is 96. Big values are good for cross-atlantic but may increase HDD fragmentation on some FS. Disable this optimization with [\033[32m1,1,1\033[0m]")
ap2.add_argument("--u2sz", metavar="N,N", type=u, default="64,96", help="web-client: default upload chunksize in MiB, either \033[33mtarget,max\033[0m or just \033[33mtarget\033[0m (sets both); this is \033[33mtarget\033[0m in the web-UI. Each HTTP POST will aim for \033[33mtarget\033[0m and never exceed \033[33mmax\033[0m. Cloudflare max is 96. Minimum is [\033[32m1\033[0m] and will be slow. Lowest recommended is [\033[32m8\033[0m]. Big values are good for cross-atlantic but may increase HDD fragmentation on some FS. Disable this optimization with [\033[32mno\033[0m]")
ap2.add_argument("--u2ow", metavar="NUM", type=int, default=0, help="web-client: default setting for when to replace/overwrite existing files; [\033[32m0\033[0m]=never, [\033[32m1\033[0m]=if-client-newer, [\033[32m2\033[0m]=always (volflag=u2ow)")
ap2.add_argument("--u2sort", metavar="TXT", type=u, default="s", help="upload order; [\033[32ms\033[0m]=smallest-first, [\033[32mn\033[0m]=alphabetical, [\033[32mfs\033[0m]=force-s, [\033[32mfn\033[0m]=force-n -- alphabetical is a bit slower on fiber/LAN but makes it easier to eyeball if everything went fine")
ap2.add_argument("--write-uplog", action="store_true", help="write POST reports to textfiles in working-directory")
@@ -1585,8 +1701,10 @@ def add_opds(ap):
def add_wopi(ap):
ap2 = ap.add_argument_group("WOPI options")
ap2.add_argument("--wopi", action="store_true", help="enable integration with office suites using WOPI")
ap2.add_argument("--wopi-accs", metavar="TXT", type=u, default="", help="restrict wopi to this comma-separated list of usernames; default is everyone")
ap2.add_argument("--wopi-api", metavar="URL", type=u, default="", help="URL that the WOPI-client should use to communicate with copyparty; default is same as user's webbrowser. Example: [\033[32mhttps://party.example.com/\033[0m]")
ap2.add_argument("--wopi-url", metavar="URL", type=u, default="", help="URL to your WOPI client; the host of e.g. Collabora Online. Example: [\033[32mhttps://code.example.com/\033[0m]")
ap2.add_argument("--wopi-urls", metavar="H=U", type=u, action="append", help="\033[34mREPEATABLE:\033[0m maps http \033[33mH\033[0mOST copyparty being accessed by to specific WOPI client instance \033[33mU\033[0mRL; falls back to \033[33m--wopi-url\033[0m; examples: [\033[32mparty.public.com=https://office.public.com/\033[0m], [\033[32mparty.internal.net:8443=https://office.internal.net:8443/\033[0m]")
ap2.add_argument("--wopi-crt", metavar="TXT", type=u, default="", help="if \033[33m--wopi-url\033[0m is selfsigned: path to ca.pem or cert.pem to expect/verify (can be [\033[32mno\033[0m] for full-yolo)")
ap2.add_argument("--wopi-crt-icn", action="store_true", help="if \033[33m--wopi-url\033[0m is selfsigned: ignore the CN (server ip/name) in cert")
ap2.add_argument("--wopi-ttl", metavar="SEC", type=int, default=1800, help="session lifetime; allow editing for this many seconds (default is 30 min)")
@@ -1631,6 +1749,7 @@ def add_yolo(ap):
ap2 = ap.add_argument_group("yolo options")
ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests; \033[1;31mDANGEROUS\033[0m / LAN-only")
ap2.add_argument("--cookie-lax", action="store_true", help="allow cookies from other domains (if you follow a link from another website into your server, you will arrive logged-in); this reduces protection against CSRF")
ap2.add_argument("--allow-svg-js", action="store_true", help="allow svg images to execute javascript; default-disabled because ~nobody wants it (volflag=allow_svg_js)")
ap2.add_argument("--no-fnugg", action="store_true", help="disable the smoketest for caching-related issues in the web-UI")
ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET -- \033[1;31mDANGEROUS\033[0m, removes csrf protection")
ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)")
@@ -1706,6 +1825,7 @@ def add_safety(ap):
ap2.add_argument("--csp-dl", metavar="TXT", default="", help="content-security-policy to apply for static files (volflag=csp_dl)")
ap2.add_argument("--no-script", action="store_true", help="disables javascript in html files; helps prevent XSS but kills interactive websites; this will override \033[33m--csp-dl\033[0m with [\033[32mscript-src 'none'\033[0m] (volflag=noscript)")
ap2.add_argument("--no-html", action="store_true", help="show html-files as plain text; helps prevent XSS but kills websites/blogs, also enables --no-script (volflag=nohtml)")
ap2.add_argument("--no-mime", action="store_true", help="disallow changing the response mimetype with url-parameter ?mime=... (volflag=nomime)")
ap2.add_argument("--vague-403", action="store_true", help="send 404 instead of 403 (security through ambiguity, very enterprise). \033[1;31mWARNING:\033[0m Not compatible with WebDAV")
ap2.add_argument("--force-js", action="store_true", help="don't send folder listings as HTML, force clients to use the embedded json instead -- slight protection against misbehaving search engines which ignore \033[33m--no-robots\033[0m")
ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)")
@@ -1736,7 +1856,7 @@ def add_safety(ap):
ap2.add_argument("--use-bwrap", metavar="TXT", type=u, default="n", help=argparse.SUPPRESS)
def add_salt(ap, fk_salt, dk_salt, ah_salt):
def add_salt(ap, fk_salt, dk_salt, ah_salt, wopi_salt):
ap2 = ap.add_argument_group("salting options")
ap2.add_argument("--ah-alg", metavar="ALG", type=u, default="none", help="account-pw hashing algorithm; one of these, best to worst: \033[32margon2 scrypt sha2 none\033[0m (each optionally followed by alg-specific comma-sep. config)")
ap2.add_argument("--ah-salt", metavar="SALT", type=u, default=ah_salt, help="account-pw salt; ignored if \033[33m--ah-alg\033[0m is none (default)")
@@ -1745,6 +1865,7 @@ def add_salt(ap, fk_salt, dk_salt, ah_salt):
ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files")
ap2.add_argument("--dk-salt", metavar="SALT", type=u, default=dk_salt, help="per-directory accesskey salt; used to generate unpredictable URLs to share folders with users who only have the 'get' permission")
ap2.add_argument("--warksalt", metavar="SALT", type=u, default="hunter2", help="up2k file-hash salt; serves no purpose, no reason to change this (but delete all databases if you do)")
ap2.add_argument("--wopi-salt", metavar="TXT", type=u, default=wopi_salt, help="WOPI file ID salt; used by \033[33m--wopi\033[0m for persistent file IDs")
ap2.add_argument("--show-ah-salt", action="store_true", help="on startup, print the effective value of \033[33m--ah-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)")
ap2.add_argument("--show-fk-salt", action="store_true", help="on startup, print the effective value of \033[33m--fk-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)")
ap2.add_argument("--show-dk-salt", action="store_true", help="on startup, print the effective value of \033[33m--dk-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)")
@@ -1832,14 +1953,25 @@ def add_thumbnail(ap):
# https://github.com/libvips/libvips
# https://stackoverflow.com/a/47612661
# 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,cbz,dcx,dds,dib,emf,eps,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,jxl,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,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,jxl,kra,ora,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="3fr,arw,avif,cr2,cr3,crw,dcr,dng,erf,exr,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,jp2,jpeg,jpg,jpx,jxl,k25,kdc,mdc,mef,mrw,nef,nii,nrw,orf,pfm,pgm,png,ppm,raf,raw,rw2,sr2,srf,srw,svg,tif,tiff,webp,x3f", help="image formats to decode using pyvips")
ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="3fr,arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mdc,mef,mos,mrw,nef,nrw,orf,pef,raf,raw,rw2,sr2,srf,srw,x3f", help="image formats to decode using rawpy (if available) or libraw's dcraw_emu")
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,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,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,kra,ora,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,bcstm,bfstm,brstm,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,m4b,m4r,mdgz,mdxz,mdz,mka,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,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("--th-pil-add", metavar="T", type=u, default="", help="add or remove image formats to decode using pillow listed in \033[33m--th-r-pil\033[0m; example: [\033[32mfb2,pdf,-psd,-bmp\033[0m] (add fb2 and pdf, remove psd and bmp)")
ap2.add_argument("--th-vips-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for pyvips")
ap2.add_argument("--th-raw-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for rawpy/libraw")
ap2.add_argument("--th-ffi-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for ffmpeg images")
ap2.add_argument("--th-ffv-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for ffmpeg videos")
ap2.add_argument("--th-ffa-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for ffmpeg audio")
ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)")
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, epub=jpg.epub", help="audio/image 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, epub=jpg.epub, kra=png.kra, ora=png.ora", help="audio/image formats to decompress before passing to ffmpeg")
ap2.add_argument("--th-extract", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m list of file extensions to thumbnail using a custom plugin (a python script); example: [\033[32mmdf,iso,dmg=/thumbs/diskimg.py\033[0m], see --help-thumb-ex")
ap2.add_argument("--th-extr-sz", metavar="M", type=int, default=16, help="max num megabytes to allow \033[33m--th-extract\033[0m plugins to extract from each file")
ap2.add_argument("--hot-th-extr", action="store_true", help="recompile extractors on each thumbnail extraction -- expensive but convenient when hacking on stuff")
def add_transcoding(ap):
@@ -1896,6 +2028,7 @@ def add_db_general(ap, hcores):
ap2.add_argument("--no-forget", action="store_true", help="never forget indexed files, even when deleted from disk -- makes it impossible to ever upload the same file twice -- only useful for offloading uploads to a cloud service or something (volflag=noforget)")
ap2.add_argument("--forget-ip", metavar="MIN", type=int, default=0, help="remove uploader-IP from database (and make unpost impossible) \033[33mMIN\033[0m minutes after upload, for GDPR reasons. Default [\033[32m0\033[0m] is never-forget. [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month. (volflag=forget_ip)")
ap2.add_argument("--dbd", metavar="PROFILE", default="wal", help="database durability profile; sets the tradeoff between robustness and speed, see \033[33m--help-dbd\033[0m (volflag=dbd)")
ap2.add_argument("--hist-cow", action="store_true", help="btrfs-only: histpaths (db/thumbs) are nocow by default; if you really need Copy-on-write then enable this (volflag=hist_cow)")
ap2.add_argument("--xlink", action="store_true", help="on upload: check all volumes for dupes, not just the target volume (probably buggy, not recommended) (volflag=xlink)")
ap2.add_argument("--hash-mt", metavar="CORES", type=int, default=hcores, help="num cpu cores to use for file hashing; set 0 or 1 for single-core hashing")
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="rescan filesystem for changes every \033[33mSEC\033[0m seconds; 0=off (volflag=scan)")
@@ -2077,6 +2210,7 @@ def run_argparse(
fk_salt = get_salt("fk", 18)
dk_salt = get_salt("dk", 30)
ah_salt = get_salt("ah", 18)
wopi_salt = get_salt("wopi", 18)
# alpine peaks at 5 threads for some reason,
# all others scale past that (but try to avoid SMT),
@@ -2113,7 +2247,7 @@ def run_argparse(
add_opds(ap)
add_wopi(ap)
add_safety(ap)
add_salt(ap, fk_salt, dk_salt, ah_salt)
add_salt(ap, fk_salt, dk_salt, ah_salt, wopi_salt)
add_optouts(ap)
add_shutdown(ap)
add_yolo(ap)
@@ -2216,6 +2350,9 @@ def main(argv: Optional[list[str]] = None) -> None:
argv.pop(n)
break
if CFG_DEF:
CFG_DEF[0] = absreal(CFG_DEF[0])
ensure_locale()
ensure_webdeps()
@@ -2370,7 +2507,8 @@ def main(argv: Optional[list[str]] = None) -> None:
if al.ciphers:
configure_ssl_ciphers(al)
else:
warn("ssl module does not exist; cannot enable https")
if not al.http_only and not NO_TLS:
warn("ssl module does not exist; cannot enable https")
al.http_only = True
if PY2 and WINDOWS and al.e2d:
+2 -2
View File
@@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (1, 20, 19)
VERSION = (1, 20, 23)
CODENAME = "sftp is fine too"
BUILD_DT = (2026, 7, 27)
BUILD_DT = (2026, 9, 6)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
+32 -4
View File
@@ -13,7 +13,7 @@ import threading
import time
from datetime import datetime
from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, WINDOWS, E
from .__init__ import ANYWIN, CFG_DEF, MACOS, PY2, TYPE_CHECKING, WINDOWS, E
from .bos import bos
from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap
from .pwhash import PWHash
@@ -25,6 +25,7 @@ from .util import (
FN_EMB,
HAVE_SQLITE3,
IMPLICATIONS,
LOG,
META_NOBOTS,
MIMES,
SQLITE_VER,
@@ -2039,6 +2040,15 @@ class AuthSrv(object):
umap[usr].sort()
setattr(vfs, "a" + perm, umap)
if self.args.wopi:
zsl = unames[:]
if self.args.wopi_accs:
zsl = [x.strip() for x in self.args.wopi_accs.split(",")]
axs = AXS(zsl, zsl, None, zsl)
vn = VFS(self.log_func, "", "wopi", "", axs, self.vf0())
vn.flags["unlistcr"] = vn.flags["unlistcw"] = True
vfs.nodes["wopi"] = vfs.all_nodes["wopi"] = vn
for vol in vfs.all_nodes.values():
za = vol.axs
vol.uaxs = {
@@ -2378,6 +2388,7 @@ class AuthSrv(object):
vol.flags["dvthumb"] = True
vol.flags["dathumb"] = True
vol.flags["dithumb"] = True
vol.flags["dethumb"] = True
have_fk = False
for vol in vfs.all_nodes.values():
@@ -2660,10 +2671,11 @@ class AuthSrv(object):
if head_s and not head_s.endswith("\n"):
head_s += "\n"
zs2 = "Content-Security-Policy: %s\r\n"
zs = vol.flags.get("csp_ui", "")
csp_ui = "Content-Security-Policy: %s\r\n" % (zs,) if zs else ""
csp_ui = zs2 % (zs,) if zs.lower() not in ("", "no") else ""
zs = vol.flags.get("csp_dl", "")
csp_dl = "Content-Security-Policy: %s\r\n" % (zs,) if zs else ""
csp_dl = zs2 % (zs,) if zs.lower() not in ("", "no") else ""
zs = "X-Content-Type-Options: nosniff\r\n"
if "norobots" in vol.flags:
@@ -2761,7 +2773,8 @@ class AuthSrv(object):
for k in drop:
t = 'cannot enable [%s] for volume "/%s" because this requires one of the following: e2d / e2ds / e2dsa (either as volflag or global-option)'
self.log(t % (k, vol.vpath), 1)
if not (enshare and vol.vpath.startswith(shrs)):
self.log(t % (k, vol.vpath), 1)
vol.flags.pop(k)
zi = vol.flags.get("lifetime") or 0
@@ -3210,6 +3223,13 @@ class AuthSrv(object):
shn.dbpath = o_vn.dbpath
shn.histpath = o_vn.histpath
zs = "assert_root daw dk dks dky e2ds e2dsa e2ts e2tsr fk fka landmark opds rss scan th_pregen"
for zs2 in zs.split(" "):
shn.flags.pop(zs2, None)
zs = "srch_excl srch_re_dots srch_re_nodot th_coversd th_coversl"
for zs2 in zs.split(" "):
shn.flags[zs2] = []
# root.all_aps doesn't include any shares, so make a copy where the
# share appears in all abspaths it can provide (for example for chk_ap)
ap = shn.realpath
@@ -3239,6 +3259,9 @@ class AuthSrv(object):
# transplant shadowing into shares
for vn in shv.nodes.values():
svn, srem = vn.shr_src # type: ignore
if "show_hist" not in vn.flags:
zvn = VFS(self.log_func, "", "", "", AXS(), self.vf0())
vn.nodes[".hist"] = zvn
if srem:
continue # free branch, safe
ap = svn.canonical(srem)
@@ -4077,6 +4100,11 @@ def expand_config_file(
if fp2 in ipath:
continue
if CFG_DEF and CFG_DEF[0] == fp2:
t = "ignoring PRTY_CONFIG because it was also autodiscovered by %s -> %s"
LOG[0]("root", t % (ipath, fp))
continue
expand_config_file(log, shenvexp, ret, fp2, ipath)
return
+8 -3
View File
@@ -1,3 +1,6 @@
# coding: utf-8
from __future__ import division, print_function, unicode_literals
import calendar
import errno
import json
@@ -8,6 +11,7 @@ import time
from .__init__ import ANYWIN
from .util import Netdev, atomic_move, load_resource, runcmd, wunlink
NO_TLS = bool(os.environ.get("PRTY_NO_TLS"))
HAVE_CFSSL = not os.environ.get("PRTY_NO_CFSSL")
if True: # pylint: disable=using-constant-test
@@ -70,8 +74,9 @@ def ensure_cert(log: "RootLogger", args) -> None:
with open(args.cert, "rb") as f:
active_cert = f.read()
if active_cert == cert_insec:
t = "using default TLS certificate; https will be insecure:\033[36m {}"
log("cert", t.format(args.cert), 3)
t = "using default TLS certificate; %s will be insecure:\033[36m %s"
t2 = "https" if not args.ftps else "ftps"
log("cert", t % (t2, args.cert), 3)
except:
pass
@@ -252,7 +257,7 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
def gencert(log: "RootLogger", args, netdevs: dict[str, Netdev]):
global HAVE_CFSSL
if args.http_only:
if args.http_only and not args.ftps:
return
if args.no_crt or args.certkey or not HAVE_CFSSL:
+7
View File
@@ -22,6 +22,7 @@ def vf_bmap() -> dict[str, str]:
"no_dupe_m": "nodupem",
"no_forget": "noforget",
"no_html": "nohtml",
"no_mime": "nomime",
"no_pipe": "nopipe",
"no_robots": "norobots",
"no_script": "noscript",
@@ -33,6 +34,7 @@ def vf_bmap() -> dict[str, str]:
"vol_or_crash": "assert_root",
}
for k in (
"allow_svg_js",
"dedup",
"dlni",
"dothidden",
@@ -50,6 +52,7 @@ def vf_bmap() -> dict[str, str]:
"grid",
"gsel",
"hardlink",
"hist_cow",
"magic",
"md_no_br",
"no_db_ip",
@@ -297,6 +300,7 @@ flagcats = {
"no_db_ip": "never store uploader-IP in the db; disables unpost",
"fat32": "avoid excessive reindexing on android sdcardfs",
"dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
"hist_cow": "btrfs: keep copy-on-write enabled on db/thumbs",
"xlink": "cross-volume dupe detection / linking (dangerous)",
"xdev": "do not descend into other filesystems",
"xvol": "do not follow symlinks leaving the volume root",
@@ -316,6 +320,7 @@ flagcats = {
"dvthumb": "disables video thumbnails",
"dathumb": "disables audio thumbnails (spectrograms)",
"dithumb": "disables image thumbnails",
"dethumb": "disables custom thumbnails (--th-extract)",
"pngquant": "compress audio waveforms 33% better",
"thsize": "thumbnail res; WxH",
"crop": "center-cropping (y/n/fy/fn)",
@@ -384,7 +389,9 @@ flagcats = {
"csp_ui": "content-security-policy for the web-UI",
"csp_dl": "content-security-policy for static files",
"nohtml": "return html and markdown as text/html",
"nomime": "disallow ?mime to change response type",
"noscript": "disable most javascript by use of CSP",
"allow_svg_js": "allow javascript in svg images",
"ui_noacci": "hide account-info in the UI",
"ui_nocpla": "hide cpanel-link in the UI",
"ui_nolbar": "hide link-bar in the UI",
+138 -61
View File
@@ -34,7 +34,7 @@ from .__init__ import ANYWIN, PY2, RES, RESM, TYPE_CHECKING, EnvParams, unicode
from .__version__ import S_VERSION
from .authsrv import LEELOO_DALLAS, VFS # typechk
from .bos import bos
from .qrkode import QrCode, qr2svg, qrgen
from .qrkode import qr2svg, qrgen
from .star import StreamTar
from .sutil import StreamArc, gfilter
from .szip import StreamZip
@@ -130,6 +130,7 @@ from .util import (
vsplit,
wunlink,
yieldfile,
zip_fi,
)
if True: # pylint: disable=using-constant-test
@@ -202,12 +203,13 @@ A_FILE = os.stat_result(
)
RE_CC = re.compile(r"[\x00-\x1f\x7f]") # search always faster
RE_CCM = re.compile(r"^[^\x00-\x1f\x7f]*$") # except when it isnt
RE_USAFE = re.compile(r'[\x00-\x1f\x7f<>"]') # search always faster
RE_HSAFE = re.compile(r"[\x00-\x1f\x7f<>\"'&]") # search always much faster
RE_HOST = re.compile(r"[^][0-9a-zA-Z.:_-]") # search faster <=17ch
RE_MHOST = re.compile(r"^[][0-9a-zA-Z.:_-]+$") # match faster >=18ch
RE_K = re.compile(r"[^0-9a-zA-Z_-]") # search faster <=17ch
RE_HTTP1 = re.compile(r"(GET|HEAD|POST|PUT) [^ ]+ HTTP/1.1$")
RE_HTTP1 = re.compile(r"(GET|HEAD|POST|PUT) /[^ ]* HTTP/1.1$")
RE_HR = re.compile(r"[<>\"'&]")
RE_MDV = re.compile(r"(.*)\.([0-9]+\.[0-9]{3})(\.[Mm][Dd])$")
RE_RSS_KW = re.compile(r"(\{[^} ]+\})")
@@ -384,14 +386,17 @@ class HttpCli(object):
return False
try:
if not RE_CCM.match("".join(headerlines)):
raise Exception()
self.mode, self.req, self.http_ver = headerlines[0].split(" ")
# normalize incoming headers to lowercase;
# outgoing headers however are Correct-Case
for header_line in headerlines[1:]:
k, zs = header_line.split(":", 1)
for ln in headerlines[1:]:
k, zs = ln.split(":", 1)
self.headers[k.lower()] = zs.strip()
if zs.endswith(" HTTP/1.1") and RE_HTTP1.search(zs):
if ln.endswith(" HTTP/1.1") and RE_HTTP1.search(ln):
raise Exception()
except:
headerlines = [repr(x) for x in headerlines]
@@ -849,6 +854,11 @@ class HttpCli(object):
self.asrv.vfs.get(
wopi_f["vp"], uname, True, True, False, self.args.wopi_wdel
)
vn, rem = self.asrv.vfs.get(
self.vpath, uname, True, True, False, self.args.wopi_wdel
)
if vn.vpath != "wopi":
raise Exception("bad url")
self.uname = uname
except Exception as ex:
self.conn.hsrv.wopi_files.pop(wopi_a, None)
@@ -1155,14 +1165,17 @@ class HttpCli(object):
for k, zs in list(self.out_headers.items()) + self.out_headerlist:
response.append("%s: %s" % (k, zs))
ptn_cc = RE_CC
for zs in response:
m = ptn_cc.search(zs)
if m:
t = "malicious user; Cc in out-hdr; req(%r) hdr(%r) => %r"
self.log(t % (self.req, zs, zs[m.span()[0] :]), 1)
self.cbonk(self.conn.hsrv.gmal, zs, "cc_hdr", "Cc in out-hdr")
raise Pebkac(999)
if not RE_CCM.match("".join(response)):
ta = (self.req, response, "?")
for zs in response:
m = RE_CC.search(zs)
if m:
ta = (self.req, zs, zs[m.span()[0] :])
break
t = "malicious user; Cc in out-hdr; req(%r) hdr(%r) => %r"
self.log(t % ta, 1)
self.cbonk(self.conn.hsrv.gmal, zs, "cc_hdr", "Cc in out-hdr")
raise Pebkac(999)
response.append(self.vn.flags[oh_k].replace("{{ js_nonce }}", self.js_nonce))
@@ -1578,13 +1591,17 @@ class HttpCli(object):
if "wopi" in self.uparam:
return self.tx_wopi()
if self.vpath.startswith("wopi"):
if self.vn.vpath == "wopi":
return self.tx_wopi_api()
return self.tx_browser()
def tx_wopi_api(self) -> bool:
atoken = self.uparam["access_token"]
try:
atoken = self.uparam["access_token"]
except:
raise Pebkac(400, "wopi access_token is mandatory")
session = self.conn.hsrv.wopi_files[atoken]
if self.do_log:
self.log(" `-- wopi: %r" % (session["vp"],))
@@ -1620,6 +1637,7 @@ class HttpCli(object):
return self.tx_404()
def tx_wopi(self) -> bool:
self.asrv.vfs.get("wopi", self.uname, True, True, False, self.args.wopi_wdel)
vpath = vjoin(self.vpath, self.uparam["wopi"])
vfs, rem = self.asrv.vfs.get(
vpath, self.uname, True, True, False, self.args.wopi_wdel
@@ -1649,7 +1667,8 @@ class HttpCli(object):
raise Pebkac(500, "too many wopi sessions")
if not found:
atoken = ub64enc(os.urandom(18)).decode("ascii") # 18 = 144b = 24c
file_id = ub64enc(os.urandom(15)).decode("ascii") # 15 = 120b = 20c
zb = (vpath + self.args.wopi_salt).encode("utf-8", "replace")
file_id = ub64enc(hashlib.sha512(zb).digest()[:15]).decode("ascii")
wopi_files[atoken] = session = {
"vp": vpath,
"uname": self.uname,
@@ -1657,7 +1676,9 @@ class HttpCli(object):
"expires": time.time() + self.args.wopi_ttl,
}
xml = "?"
assert atoken and session # type: ignore # !rm
xml = url = "?"
try:
from .dxml import parse_xml
@@ -1673,14 +1694,18 @@ class HttpCli(object):
uo_kw["context"] = ctx
url = self.args.wopi_url.rstrip("/") + "/hosting/discovery"
wopi_urls = dict(x.lower().split("=", 1) for x in self.args.wopi_urls or [])
url = wopi_urls.get(self.host.lower(), self.args.wopi_url).rstrip("/")
url += "/hosting/discovery"
buf = urlopen(url, **uo_kw).read()
xml = buf.decode("ascii", "replace").lower()
enc = self.get_xml_enc(xml)
xml = buf.decode(enc, "replace")
xroot = parse_xml(xml)
ext = vpath.split(".")[-1]
url = xroot.find(".//action[@ext='%s'][@urlsrc]" % (ext,)).get("urlsrc")
url = xroot.find(
".//action[@ext='%s'][@name='edit'][@urlsrc]" % (ext,)
).get("urlsrc")
if not url.endswith(("?", "&")):
url += "&" if "?" in url else "?"
url += "WOPISrc="
@@ -1691,7 +1716,8 @@ class HttpCli(object):
url += quotep(zs + "/wopi/files/" + session["file_id"])
except:
del wopi_files[atoken] # dont reuse an atoken wopi-client doesnt like
self.log("reading WOPI-client response failed; %s\n%s" % (min_ex(), xml), 3)
t = "reading WOPI-client response from %s failed; %s\n%s"
self.log(t % (url, min_ex(), xml), 3)
raise Pebkac(500, "wopi error (see fileserver log)")
html = self.j2s(
@@ -1699,7 +1725,7 @@ class HttpCli(object):
title=self.uparam["wopi"],
url=url,
atoken=atoken,
ttl=session["expires"],
ttl=int(session["expires"] * 1000),
).encode("utf-8", "replace")
self.reply(html, 200, "text/html; charset=utf-8")
@@ -1896,9 +1922,7 @@ class HttpCli(object):
try:
with zipfile.ZipFile(abspath, "r") as zf:
zi = zf.getinfo(inner_path)
if zi.file_size >= maxsz:
raise Pebkac(404, "zip bomb defused")
zi = zip_fi(zf, inner_path, maxsz)
with zf.open(zi, "r") as fi:
mime = guess_mime(inner_path)
if mime not in SAFE_MIMES and "nohtml" in self.vn.flags:
@@ -3127,7 +3151,8 @@ class HttpCli(object):
v = self.uparam[k]
if self._use_dirkey(self.vn, ""):
is_dk = self._use_dirkey(self.vn, "")
if is_dk:
vn = self.vn
rem = self.rem
else:
@@ -3139,6 +3164,9 @@ class HttpCli(object):
items = zs.replace("\r", "").split("\n")
items = [unquotep(x) for x in items if items]
if is_dk:
zss = set(vn_ls_regfile(self.args, vn, rem))
items = [x for x in items if x in zss]
self.parser.drop()
return self.tx_zip(k, v, "", vn, rem, items)
@@ -3328,7 +3356,7 @@ class HttpCli(object):
try:
postsize = remains = int(self.headers["content-length"])
except:
raise Pebkac(400, "you must supply a content-length for binary POST")
raise Pebkac(411)
if self.args.wopi and self.vpath.startswith("wopi"):
return self.rx_wopi(postsize)
@@ -3545,7 +3573,11 @@ class HttpCli(object):
return True
def rx_wopi(self, postsize: int) -> bool:
atoken = self.uparam["access_token"]
try:
atoken = self.uparam["access_token"]
except:
raise Pebkac(400, "wopi access_token is mandatory")
session = self.conn.hsrv.wopi_files[atoken]
if self.do_log:
self.log(" `-- wopi: %r" % (session["vp"],))
@@ -3554,8 +3586,7 @@ class HttpCli(object):
if not self.vpath.startswith(zs):
return self.tx_404()
vpath = self.conn.hsrv.wopi_files[self.uparam["access_token"]]["vp"]
vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True)
vfs, rem = self.asrv.vfs.get(session["vp"], self.uname, False, True)
vpath = vjoin(vfs.vpath, rem)
ap = vfs.canonical(rem)
st = bos.stat(ap)
@@ -4370,7 +4401,7 @@ class HttpCli(object):
mfile2 = "{}.{:.3f}.{}".format(fname, srv_lastmod, fext)
dp = ""
hist_cfg = dbv.flags["md_hist"]
hist_cfg = vfs.flags["md_hist"]
if hist_cfg == "v":
vrd = vsplit(vrem)[0]
zb = hashlib.sha512(afsenc(vrd)).digest()
@@ -4393,7 +4424,7 @@ class HttpCli(object):
pass
if dp:
atomic_move(self.log, fp, os.path.join(dp, mfile2), vfs.flags)
nmax = dbv.flags["md_nhist"]
nmax = vfs.flags["md_nhist"]
if nmax:
zs = r"%s\.[0-9]+\.[0-9]{3}\.%s"
ptn = re.compile(zs % (re.escape(fname), re.escape(fext)))
@@ -4953,14 +4984,15 @@ class HttpCli(object):
a, b = hrange.split("=", 1)[1].split("-")
if b.strip():
upper = 1 + int(b.strip())
else:
upper = file_sz
if a.strip():
lower = int(a.strip())
else:
lower = 0
if b.strip():
upper = int(b.strip()) + 1
else:
lower = 1 + file_sz - upper
upper = file_sz
if upper > file_sz:
@@ -5012,15 +5044,20 @@ class HttpCli(object):
if "txt" in self.uparam:
mime = "text/plain; charset={}".format(self.uparam["txt"] or "utf-8")
elif "mime" in self.uparam:
elif "mime" in self.uparam and "nomime" not in self.vn.flags:
mime = str(self.uparam.get("mime"))
elif "rmagic" in self.vn.flags:
mime = guess_mime(req_path, fs_path)
else:
mime = guess_mime(cdis)
if mime not in SAFE_MIMES and "nohtml" in self.vn.flags and oh_k != "oh_g":
mime = safe_mime(mime)
if mime not in SAFE_MIMES:
if "nohtml" in self.vn.flags and oh_k != "oh_g":
mime = safe_mime(mime)
elif mime == "image/svg+xml" and "allow_svg_js" not in self.vn.flags:
oh_k = "oh_g"
if "nonce-" not in self.vn.flags[oh_k]:
mime = safe_mime(mime)
self.out_headers["Accept-Ranges"] = "bytes"
logmsg += unicode(status) + logtail
@@ -6145,6 +6182,8 @@ class HttpCli(object):
continue
try:
dvn, drem = vfs.get(vjoin(top, x), self.uname, False, False)
if not dvn.realpath and not dvn.nodes:
continue
if (
self.uname not in dvn.axs.uread
and self.uname not in dvn.axs.uwrite
@@ -6867,11 +6906,17 @@ class HttpCli(object):
return True
def handle_fs_abrt(self):
rk = self.uparam["fs_abrt"]
if rk == "ping":
self.conn.hsrv.broker.ask("up2k.handle_fs_abrt", rk)
self.reply(b"pong")
return True
if self.args.no_fs_abrt:
t = "aborting an ongoing copy/move is disabled in server config"
raise Pebkac(403, t)
self.conn.hsrv.broker.say("up2k.handle_fs_abrt", self.uparam["fs_abrt"])
self.conn.hsrv.broker.say("up2k.handle_fs_abrt", rk)
self.loud_reply("aborting", status=200)
return True
@@ -7074,10 +7119,10 @@ class HttpCli(object):
and (use_filekey or use_dirkey or (not is_dir and "fk" not in vn.flags))
):
if th_fmt is not None:
nothumb = "dthumb" in dbv.flags
nothumb = "dthumb" in vn.flags
if is_dir:
vrem = vrem.rstrip("/")
cvs = dbv.flags["th_coversl"]
cvs = vn.flags["th_coversl"]
if nothumb or not cvs:
pass
elif icur and vrem:
@@ -7375,10 +7420,10 @@ class HttpCli(object):
for k in ["zip", "tar"]:
v = self.uparam.get(k)
if v is not None and (not add_og or not og_fn):
filt = []
if is_dk and "dks" not in vn.flags:
t = "server config does not allow download-as-zip/tar; only dk is specified, need dks too"
raise Pebkac(403, t)
return self.tx_zip(k, v, self.vpath, vn, rem, [])
filt = set(vn_ls_regfile(self.args, vn, rem))
return self.tx_zip(k, v, self.vpath, vn, rem, filt)
fsroot, vfs_ls, vfs_virt = vn.ls(
rem,
@@ -7441,20 +7486,18 @@ class HttpCli(object):
dirs = []
files = []
ptn_hr = RE_HR
use_abs_url = is_opds or (
vpath and not is_ls and not is_js and not self.trailing_slash
)
base = ""
if is_opds or (vpath and not (is_ls or is_js or self.trailing_slash)):
if is_opds:
base = self.args.SRS
if vpath:
base += vpath + "/"
else:
base = "/" + vpath + "/"
for fn in ls_names:
base = ""
href = fn
if use_abs_url:
if is_opds:
base = self.args.SRS
if vpath:
base += vpath + "/"
else:
base = "/" + vpath + "/"
href = base + fn
href = base + fn
if fn in vfs_virt:
fspath = vfs_virt[fn].realpath
@@ -7624,6 +7667,26 @@ class HttpCli(object):
(fe["sz"], fe["tags"][".files"]) = hit
except:
pass # 404 or mojibake
if vfs_virt:
q = "select sz, nf from ds where rd='' limit 1"
try:
for fe in [x for x in dirs if x["name"] in vfs_virt]:
if ".files" not in fe["tags"]:
fe["tags"][".files"] = 0
vols = [vn.nodes[fe["name"]]]
while vols:
vn2 = vols.pop()
if self.uname not in vn2.axs.uread:
continue
vols += list(vn2.nodes.values())
if vn2.dbv not in (vn2, None):
continue
hit = idx.get_cur(vn2).execute(q).fetchone()
if hit:
fe["sz"] += hit[0]
fe["tags"][".files"] += hit[1]
except:
pass
taglist = [k for k in lmte if k in tagset]
else:
@@ -7805,7 +7868,7 @@ class HttpCli(object):
).urn
item["iso8601"] = "%sZ" % (item["dt"].replace(" ", "T"),)
if "rmagic" in self.vn.flags:
if "rmagic" in vf:
ap = "%s/%s" % (fsroot, item["name"])
item["mime"] = guess_mime(item["name"], ap)
else:
@@ -7815,8 +7878,8 @@ class HttpCli(object):
if (
not self.args.th_no_jpg
and self.thumbcli
and "dthumb" not in dbv.flags
and "dithumb" not in dbv.flags
and "dthumb" not in vf
and "dithumb" not in vf
):
item["jpeg_thumb_href"] = href + "&th=jf"
item["jpeg_thumb_href_hires"] = item["jpeg_thumb_href"] + "3"
@@ -7869,6 +7932,10 @@ class HttpCli(object):
ext in self.thumbcli.fmt_pil
or ext in self.thumbcli.fmt_vips
or ext in self.thumbcli.fmt_ffi
or (
ext in self.thumbcli.thumbable
and ext not in self.thumbcli.thumbable_native
)
)
is_vid = ext in self.thumbcli.fmt_ffv
is_au = ext in self.thumbcli.fmt_ffa
@@ -8006,3 +8073,13 @@ class HttpCli(object):
html = self.j2s(tpl, **j2a)
self.reply(html.encode("utf-8", "replace"))
return True
def vn_ls_regfile(args: argparse.Namespace, vn: VFS, rem: str) -> list[str]:
_, ls, _ = vn.ls(
rem,
"",
not args.no_scandir,
[[False, False]],
)
return [x[0] for x in ls if stat.S_ISREG(x[1].st_mode)]
+11 -10
View File
@@ -2,28 +2,29 @@
from __future__ import division, print_function, unicode_literals
import argparse # typechk
import os
import re
import socket
import threading # typechk
import time
try:
if os.environ.get("PRTY_NO_TLS"):
raise Exception()
HAVE_SSL = True
import ssl
except:
HAVE_SSL = False
from . import util as Util
from .__init__ import TYPE_CHECKING, EnvParams
from .authsrv import AuthSrv # typechk
from .cert import NO_TLS
from .httpcli import HttpCli
from .u2idx import U2idx
from .util import HMaccas, NetMap, min_ex, shut_socket
try:
if NO_TLS:
raise Exception()
import ssl
HAVE_SSL = True
except:
HAVE_SSL = False
if True: # pylint: disable=using-constant-test
from typing import Optional, Pattern, Union
+1 -2
View File
@@ -2,7 +2,6 @@
from __future__ import division, print_function, unicode_literals
import hashlib
import math
import os
import re
import socket
@@ -180,7 +179,7 @@ class HttpSrv(object):
self.u2idx_free: dict[str, U2idx] = {}
self.u2idx_n = 0
self.wopi_files: dict[str, dict[str, str]] = {}
self.wopi_files: dict[str, dict[str, Any]] = {}
assert jinja2 # type: ignore # !rm
env = jinja2.Environment()
-1
View File
@@ -51,7 +51,6 @@ except ImportError:
SRV,
TXT,
A,
Bimap,
DNSHeader,
DNSQuestion,
DNSRecord,
+20 -7
View File
@@ -30,6 +30,9 @@ from .util import (
sfsenc,
uncyg,
wunlink,
zip_fi,
zip_lim,
zip_readf,
)
if True: # pylint: disable=using-constant-test
@@ -174,7 +177,7 @@ def au_unpk(
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])
fi = zf.open(zip_lim(zil[0], maxsz))
elif pk == "cbz":
import zipfile
@@ -188,11 +191,18 @@ def au_unpk(
t = "cbz: %d files, %d hits" % (nf, len(znil))
if not znil:
raise Exception("no images inside cbz")
using = sorted(znil)[0][1].filename
zi = sorted(znil)[0][1]
if znil:
t += ", using " + using
t += ", using " + zi.filename
log(t)
fi = zf.open(using)
fi = zf.open(zip_lim(zi, maxsz))
elif pk == "kra" or pk == "ora":
import zipfile
zf = zipfile.ZipFile(abspath, "r")
zi = zip_fi(zf, "mergedimage.png", maxsz)
fi = zf.open(zi)
elif pk == "epub":
fi = get_cover_from_epub(log, abspath)
@@ -432,7 +442,8 @@ def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]
with zipfile.ZipFile(abspath, "r") as z:
# First open the container file to find the package document (.opf file)
try:
container_root = parse_xml(z.read("META-INF/container.xml").decode())
zb = zip_readf(z, "META-INF/container.xml", 2 << 20)
container_root = parse_xml(zb.decode())
except KeyError:
log("epub: no container file found in %s" % (abspath,))
return None
@@ -446,7 +457,8 @@ def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]
# Then open the first package document to find the path of the cover image
try:
package_root = parse_xml(z.read(rootfile_path).decode())
zb = zip_readf(z, rootfile_path, 2 << 20)
package_root = parse_xml(zb.decode())
except KeyError:
log("epub: no package document found in %s" % (abspath,))
return None
@@ -470,7 +482,8 @@ def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]
adjusted_cover_path = urljoin(rootfile_path, coverimage_path)
try:
return z.open(adjusted_cover_path)
zi = zip_fi(z, adjusted_cover_path, 2 << 24)
return z.open(zi)
except KeyError:
t = "epub: cover specified in package document, but doesn't exist: %s"
log(t % (adjusted_cover_path,))
+2 -1
View File
@@ -14,8 +14,9 @@ try:
if os.environ.get("PRTY_NO_ARGON2"):
raise Exception()
HAVE_ARGON2 = True
from argon2 import exceptions as argon2ex
HAVE_ARGON2 = True
except:
HAVE_ARGON2 = False
+1 -3
View File
@@ -8,7 +8,6 @@ import os
import select
import socket
import time
from threading import ExceptHookArgs
import paramiko
import paramiko.common
@@ -496,8 +495,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
smode = "rb"
try:
vn, rem = self.asrv.vfs.get(vp, self.uname, rd, wr)
ap = vn.canonical(rem, False)
ap, vn, rem = self.v2a(vp, rd, wr)
vf = vn.flags
except Pebkac as ex:
t = "denied open file [%s], iflag=%s, read=%s, write=%s: %s"
+1 -1
View File
@@ -12,7 +12,7 @@ from types import SimpleNamespace
from .__init__ import ANYWIN, EXE, TYPE_CHECKING
from .authsrv import LEELOO_DALLAS, VFS
from .bos import bos
from .util import Daemon, absreal, min_ex, pybin, runhook, vjoin
from .util import Daemon, absreal, min_ex, pybin, runhook
if True: # pylint: disable=using-constant-test
from typing import Any, Union
-1
View File
@@ -5,7 +5,6 @@ import errno
import re
import select
import socket
import time
from .__init__ import TYPE_CHECKING
from .multicast import MC_Sck, MCast
+41 -9
View File
@@ -81,7 +81,6 @@ from .util import (
alltrace,
build_netmap,
expat_ver,
fsenc,
gzip,
html_escape,
load_ipr,
@@ -133,6 +132,19 @@ VER_SHARES_DB = 2
CVE_SEVS = {"low": 1, "medium": 2, "moderate": 2, "high": 3, "critical": 4}
def _build_th_fset(f: str, fadd: str) -> set[str]:
fset = {x for x in f.replace(" ", "").split(",")}
rmset = {""}
for x in fadd.replace(" ", "").split(","):
if x.startswith("-"):
rmset.add(x.lstrip("-"))
else:
fset.add(x)
return fset - rmset
class SvcHub(object):
"""
Hosts all services which cannot be parallelized due to reliance on monolithic resources.
@@ -441,6 +453,20 @@ class SvcHub(object):
zlss = [x.strip().lower().split("=", 1) for x in args.au_unpk.split(",")]
args.au_unpk = {x[0]: x[1] for x in zlss}
args.th_r_pil = _build_th_fset(args.th_r_pil, args.th_pil_add)
args.th_r_vips = _build_th_fset(args.th_r_vips, args.th_vips_add)
args.th_r_raw = _build_th_fset(args.th_r_raw, args.th_raw_add)
args.th_r_ffi = _build_th_fset(args.th_r_ffi, args.th_ffi_add)
args.th_r_ffv = _build_th_fset(args.th_r_ffv, args.th_ffv_add)
args.th_r_ffa = _build_th_fset(args.th_r_ffa, args.th_ffa_add)
th_extract = args.th_extract or []
args.th_extract = {}
for arg in th_extract:
exts, script = arg.split("=", 1)
for ext in exts.split(","):
args.th_extract[ext.strip().lower()] = script
self.args.th_dec = list(decs.keys())
self.thumbsrv = None
want_ff = False
@@ -478,7 +504,7 @@ class SvcHub(object):
t = "invalid mp3 transcoding quality [%s] specified; only supports [0] to disable, a CBR value such as [192k], or a CQ/CRF value such as [v2]"
raise Exception(t % (args.q_mp3,))
else:
zss = set(args.th_r_ffa.split(",") + args.th_r_ffv.split(","))
zss = args.th_r_ffa | args.th_r_ffv
args.au_unpk = {
k: v for k, v in args.au_unpk.items() if v.split(".")[0] not in zss
}
@@ -1127,6 +1153,10 @@ class SvcHub(object):
t = "not listening on any ip-addresses (only unix-sockets and/or FDs); cannot enable zeroconf/mdns/ssdp as requested"
self.log("root", t, 3)
if al.wopi and not al.wopi_url and not al.wopi_urls:
self.log("root", "disabling wopi; need either wopi-url or wopi-urls", 1)
al.wopi = False
if self.args.wopi or not self.args.no_dav:
from .dxml import DXML_OK
@@ -1383,18 +1413,20 @@ class SvcHub(object):
self.args.name_html = zs
zs = al.u2sz
if zs in ("no", "1,1,1"):
zs = "0"
zsl = [x.strip() for x in zs.split(",")]
if len(zsl) not in (1, 3):
t = "invalid --u2sz; must be either one number, or a comma-separated list of three numbers (min,default,max)"
if len(zsl) not in (1, 2):
t = "invalid --u2sz; must be either one number, or a comma-separated list of two numbers (target,max)"
raise Exception(t)
if len(zsl) < 3:
zsl = ["1", zs, zs]
zi2 = 1
if len(zsl) < 2:
zsl = [zs, zs]
zi2 = 0
for zs in zsl:
zi = int(zs)
# arbitrary constraint (anything above 2 GiB is probably unintended)
if zi < 1 or zi > 2047:
raise Exception("invalid --u2sz; minimum is 1, max is 2047")
if zi < 0 or zi > 2047:
raise Exception("invalid --u2sz; minimum is 0, max is 2047")
if zi < zi2:
raise Exception("invalid --u2sz; values must be equal or ascending")
zi2 = zi
-1
View File
@@ -22,7 +22,6 @@ from .util import (
Netdev,
atomic_move,
chkcmd,
get_adapters,
list_nics,
min_ex,
sunpack,
+6 -1
View File
@@ -34,6 +34,7 @@ class ThumbCli(object):
self.cooldown = Cooldown(self.args.th_poke) if self.args.th_poke else None
self.thumbable = c["thumbable"]
self.thumbable_native = c["thumbable_native"]
self.fmt_pil = c["pil"]
self.fmt_vips = c["vips"]
self.fmt_raw = c["raw"]
@@ -59,6 +60,10 @@ class ThumbCli(object):
if is_vid and "dvthumb" in dbv.flags:
return None
is_custom = ext not in self.thumbable_native
if is_custom and "dethumb" in dbv.flags:
return None
want_opus = fmt in EXTS_AC
is_au = ext in self.fmt_ffa
is_vau = want_opus and ext in self.fmt_ffv
@@ -76,7 +81,7 @@ class ThumbCli(object):
elif want_opus:
return None
is_img = not is_vid and not is_au
is_img = not is_vid and not is_au and not is_custom
if is_img and "dithumb" in dbv.flags:
return None
+102 -32
View File
@@ -38,6 +38,7 @@ from .util import (
afsenc,
atomic_move,
fsenc,
loadpy,
min_ex,
runcmd,
statdir,
@@ -323,24 +324,12 @@ class ThumbSrv(object):
if self.args.th_clean:
Daemon(self.cleaner, "thumb.cln")
(
self.fmt_pil,
self.fmt_vips,
self.fmt_raw,
self.fmt_ffi,
self.fmt_ffv,
self.fmt_ffa,
) = [
set(y.split(","))
for y in [
self.args.th_r_pil,
self.args.th_r_vips,
self.args.th_r_raw,
self.args.th_r_ffi,
self.args.th_r_ffv,
self.args.th_r_ffa,
]
]
self.fmt_pil = self.args.th_r_pil
self.fmt_vips = self.args.th_r_vips
self.fmt_raw = self.args.th_r_raw
self.fmt_ffi = self.args.th_r_ffi
self.fmt_ffv = self.args.th_r_ffv
self.fmt_ffa = self.args.th_r_ffa
if not H_PIL_HEIF:
for f in "heif heifs heic heics".split(" "):
@@ -359,23 +348,26 @@ class ThumbSrv(object):
self.fmt_pil.discard(f)
self.thumbable: set[str] = set()
self.thumbable_native: set[str] = set()
self._build_thumbable()
def _build_thumbable(self) -> None:
self.thumbable.clear()
self.thumbable_native.clear()
if "pil" in self.args.th_dec:
self.thumbable |= self.fmt_pil
self.thumbable_native |= self.fmt_pil
if "vips" in self.args.th_dec:
self.thumbable |= self.fmt_vips
self.thumbable_native |= self.fmt_vips
if "raw" in self.args.th_dec:
self.thumbable |= self.fmt_raw
self.thumbable_native |= self.fmt_raw
if "ff" in self.args.th_dec:
for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]:
self.thumbable |= zss
self.thumbable_native |= zss
self.thumbable = self.thumbable_native | set(self.args.th_extract)
def _log(self, msg: str, c: Union[int, str] = 0) -> None:
self.log_func("thumb", msg, c)
@@ -467,6 +459,7 @@ class ThumbSrv(object):
def getcfg(self) -> dict[str, set[str]]:
return {
"thumbable": self.thumbable,
"thumbable_native": self.thumbable_native,
"pil": self.fmt_pil,
"vips": self.fmt_vips,
"raw": self.fmt_raw,
@@ -549,19 +542,36 @@ class ThumbSrv(object):
png_ok = False
funs = []
tex = tpath.rsplit(".", 1)[-1]
want_mp3 = tex == "mp3"
want_opus = tex in ("opus", "owa", "caf")
want_flac = tex == "flac"
want_wav = tex == "wav"
want_png = tex == "png"
want_au = want_mp3 or want_opus or want_flac or want_wav
ap_extr = abspath
if (
ext in self.args.th_extract
and not want_au
and not want_png
and "dethumb" not in vn.flags
):
ap_extr = self.run_extractor(self.args.th_extract[ext], abspath, vn)
if ap_extr:
ext = ap_extr.rsplit(".", 1)[-1]
if ext in self.args.au_unpk:
ap_unpk = au_unpk(self.log, self.args.au_unpk, abspath, vn)
ap_unpk = au_unpk(self.log, self.args.au_unpk, ap_extr, vn)
elif ext in self.thumbable_native:
ap_unpk = ap_extr
else:
ap_unpk = abspath
ap_unpk = ""
if ap_extr and ap_extr != ap_unpk and ap_extr != abspath:
wunlink(self.log, ap_extr, vn.flags)
if ap_unpk and not bos.path.exists(tpath):
tex = tpath.rsplit(".", 1)[-1]
want_mp3 = tex == "mp3"
want_opus = tex in ("opus", "owa", "caf")
want_flac = tex == "flac"
want_wav = tex == "wav"
want_png = tex == "png"
want_au = want_mp3 or want_opus or want_flac or want_wav
for lib in self.args.th_dec:
can_au = lib == "ff" and (
ext in self.fmt_ffa or ext in self.fmt_ffv
@@ -669,6 +679,66 @@ class ThumbSrv(object):
with self.mutex:
self.nthr -= 1
def run_extractor(self, extr: str, abspath: str, vn: VFS) -> str:
try:
mod = loadpy(extr, self.args.hot_th_extr)
except Exception as ex:
self.log("extractor import failed; " + min_ex(), 1)
return ""
fd = 0
ret = ""
stream = None
try:
res = mod.main(abspath, vn=vn, th_srv=self)
if not res:
t = "extractor %r returned no data for file %r"
self.log(t % (extr, abspath))
return ""
outext, stream, offset, whence, size = res
if (
outext not in self.thumbable_native
or outext in self.fmt_ffa
or outext in self.fmt_ffv
):
t = "extractor %r returned unsupported format %r"
self.log(t % (extr, outext))
return ""
remains = size if size is not None and size >= 0 else -1
stream.seek(offset, whence)
fd, ret = tempfile.mkstemp("." + outext)
bufsz = rsz = self.args.iobuf
maxsz = self.args.th_extr_sz * 1048576
outsz = 0
with os.fdopen(fd, "wb", rsz) as f:
fd = 0
while True:
if remains < rsz and remains >= 0:
bufsz = remains
buf = stream.read(bufsz)
if not buf:
break
remains -= len(buf)
outsz += len(buf)
if outsz >= maxsz:
raise Exception("too large")
f.write(buf)
return ret
except Exception as e:
if fd:
os.close(fd)
if ret:
wunlink(self.log, ret, vn.flags)
t = "failed to extract thumbnail from %r: %s"
self.log(t % (abspath, min_ex()))
return ""
finally:
if stream:
stream.close()
def fancy_pillow(self, im: "Image.Image", fmt: str, vn: VFS) -> "Image.Image":
# exif_transpose is expensive (loads full image + unconditional copy)
res = self.getres(vn, fmt)
+21 -7
View File
@@ -4,12 +4,9 @@ from __future__ import division, print_function, unicode_literals
import errno
import hashlib
import json
import math
import os
import re
import shutil
import stat
import subprocess as sp
import sys
import tempfile
import threading
@@ -55,6 +52,7 @@ from .util import (
ren_open,
rmdirs,
rmdirs_up,
runcmd,
runhook,
runihook,
s2hms,
@@ -177,6 +175,7 @@ class Up2k(object):
self.pp: Optional[ProgressPrinter] = None
self.rescan_cond = threading.Condition()
self.need_rescan: set[str] = set()
self.have_e2d = False
self.db_act = 0.0
self.reg_mutex = threading.Lock()
@@ -256,6 +255,10 @@ class Up2k(object):
with self.reload_mutex:
if self.reload_flag < n:
self.reload_flag = n
if not self.have_e2d and not self.reloading:
self.reloading = True
Daemon(self._reload_thr)
return
with self.rescan_cond:
self.rescan_cond.notify_all()
@@ -293,7 +296,7 @@ class Up2k(object):
def deferred_init(self) -> None:
all_vols = self.asrv.vfs.all_vols
have_e2d = self.init_indexes(all_vols, [], False)
self.init_indexes(all_vols, [], False)
if self.stop:
# up-mt consistency not guaranteed if init is interrupted;
@@ -311,7 +314,7 @@ class Up2k(object):
return
Daemon(self._snapshot, "up2k-snapshot")
if have_e2d:
if self.have_e2d:
Daemon(self._hasher, "up2k-hasher")
Daemon(self._sched_rescan, "up2k-rescan")
if self.mtag:
@@ -894,7 +897,7 @@ class Up2k(object):
def init_indexes(
self, all_vols: dict[str, VFS], scan_vols: list[str], fscan: bool, gid: int = 0
) -> bool:
) -> None:
if not gid:
with self.mutex:
gid = self.gid
@@ -1131,7 +1134,7 @@ class Up2k(object):
else:
self.unpp()
return have_e2d
self.have_e2d = self.have_e2d or have_e2d
def register_vpath(
self, ptop: str, flags: dict[str, Any]
@@ -1294,6 +1297,14 @@ class Up2k(object):
self.log(t % (vpath, ex), 1)
return None
if dir_is_empty(self.log_func, not self.args.no_scandir, histpath) and not (
ANYWIN or UNIX or "hist_cow" in flags
):
try:
runcmd([b"chattr", b"+C", fsenc(histpath)], 1)
except:
pass
try:
cur = self._open_db_wd(db_path)
@@ -4136,6 +4147,9 @@ class Up2k(object):
pass
def handle_fs_abrt(self, akey: str) -> None:
if akey == "ping":
with self.mutex, self.reg_mutex:
return
self.abrt_key = akey
def handle_rm(
+43 -7
View File
@@ -10,7 +10,6 @@ import hashlib
import hmac
import json
import logging
import math
import mimetypes
import os
import platform
@@ -180,7 +179,10 @@ try:
raise ImportError()
from .stolen.ifaddr import get_adapters
IFADDR_VND = True
except ImportError:
IFADDR_VND = False
from ifaddr import get_adapters
HAVE_IFADDR = True
@@ -195,10 +197,10 @@ try:
if os.environ.get("PRTY_NO_SQLITE"):
raise Exception()
HAVE_SQLITE3 = True
import sqlite3
assert hasattr(sqlite3, "connect") # graalpy
HAVE_SQLITE3 = True
except:
HAVE_SQLITE3 = False
@@ -213,8 +215,9 @@ try:
if os.environ.get("PRTY_NO_PSUTIL"):
raise Exception()
HAVE_PSUTIL = True
import psutil
HAVE_PSUTIL = True
except:
HAVE_PSUTIL = False
@@ -225,8 +228,10 @@ try:
raise Exception()
import magic
HAVE_MAGIC = True
except:
pass
HAVE_MAGIC = False
if os.environ.get("PRTY_MODSPEC"):
from inspect import getsourcefile
@@ -352,7 +357,7 @@ RE_MEMAVAIL = re.compile("^MemAvailable:.* kB")
if PY2:
def umktrans(s1, s2):
def umktrans(s1: str, s2: str):
return {ord(c1): ord(c2) for c1, c2 in zip(s1, s2)}
else:
@@ -2150,7 +2155,8 @@ class MultipartParser(object):
boundary = get_boundary(self.headers)
if boundary.startswith('"') and boundary.endswith('"'):
boundary = boundary[1:-1] # dillo uses quotes
self.log("boundary=%r" % (boundary,))
if len(boundary) > 72: # rfc-2046 <=70
raise Pebkac(400, "boundary 2big: %d" % (len(boundary),))
# spec says there might be junk before the first boundary,
# can't have the leading \r\n if that's not the case
@@ -2216,7 +2222,7 @@ def read_header(sr: Unrecv, t_idle: int, t_tot: int) -> list[str]:
raise Pebkac(
400,
"protocol error while reading headers",
log=ret.decode("utf-8", "replace"),
log=repr(ret.decode("utf-8", "replace")),
)
ofs = ret.find(b"\r\n\r\n")
@@ -4393,6 +4399,36 @@ def gzip_file_orig_sz(f) -> int:
return sunpack(b"I", rv)[0] # type: ignore
def zip_fi(zf, fp, max_sz):
zi = zf.getinfo(fp)
if max_sz and zi.file_size > max_sz:
raise Pebkac(404, "zip bomb defused")
return zi
def zip_lim(zi, max_sz):
if zi.file_size > max_sz:
raise Pebkac(404, "zip bomb defused")
return zi
def zip_read(zf, zi, max_sz):
ret = b""
with zf.open(zi) as f:
while True:
buf = f.read(max_sz)
if not buf:
break
ret += buf
if len(ret) >= max_sz:
raise Pebkac(404, "zip bomb defused")
return ret
def zip_readf(zf, zp, max_sz):
return zip_read(zf, zip_fi(zf, zp, max_sz), max_sz)
def align_tab(lines: list[str]) -> list[str]:
rows = []
ncols = 0
+2 -1
View File
@@ -7,7 +7,8 @@ pk: $(addsuffix .gz, $(wildcard tl/*.js *.js *.css) \
un: $(addsuffix .un, $(wildcard tl/*.gz *.gz a/*.gz))
tl/%.js.gz: tl/%.js
./Makefile.s1 <$< | pigz -c11 -J 34 -I 573 >$@
sh Makefile.s1 <$< | pigz -c11 -J 34 -I 573 >$@
pigz -dc <$@ | grep -q ^Ls
touch -r $< $@
rm $<
+4
View File
@@ -1768,6 +1768,10 @@ html.y #tree.nowrap .ntree a+a:hover {
top: .3em;
z-index: 9;
}
.dumb_loader_thing.ns {
animation: none;
opacity: 1;
}
#dlt_t {
left: 0;
}
+2
View File
@@ -121,6 +121,8 @@
<h2 id="wfp"><a href="{{ r }}/?h" id="goh">control-panel</a></h2>
<p id="js_bork">Looks like javascript is broken; see "csp nonce" section in readme</p>
<a href="#" id="repl">π</a>
</div>
+42 -23
View File
@@ -46,6 +46,7 @@ if (1)
["ctrl-V", "paste (move/copy) here"],
["Y", "download selected"],
["F2", "rename selected"],
["F4", "update/refresh"],
"file-list-sel",
["space", "toggle file selection"],
@@ -582,6 +583,7 @@ if (1)
"u_ancient": 'your browser is impressively ancient -- maybe you should <a href="#" id="u2nah">use bup instead</a>',
"u_nowork": "need firefox 53+ or chrome 57+ or iOS 11+",
"tail_2old": "need firefox 105+ or chrome 71+ or iOS 14.5+",
"b2old": 'your browser is too old',
"u_nodrop": 'your browser is too old for drag-and-drop uploading',
"u_notdir": "that's not a folder!\n\nyour browser is too old,\nplease try dragdrop instead",
"u_uri": "to dragdrop images from other browser windows,\nplease drop it onto the big upload button",
@@ -665,6 +667,7 @@ if (1)
"rc_pla": "play as audio",
"rc_txt": "open in textfile viewer",
"rc_md": "open in markdown viewer",
"rc_wopi": "open in office editor",
"rc_dl": "download",
"rc_zip": "download as archive",
"rc_cpl": "copy link",
@@ -757,7 +760,7 @@ if (window.glang && navigator.languages && !/\bcplng=/.test(document.cookie))
if (window.stop)
window.stop();
document.body.innerHTML = 'Loading ' + n;
setck("cplng=" + n, location.reload.bind(location));
setck("cplng=" + n, relod);
crashed = true;
throw 1;
}
@@ -1111,6 +1114,7 @@ ebi('rcm').innerHTML = (
'<a href="#" id="rpla">' + L.rc_pla + '</a>' +
'<a href="#" id="rtxt">' + L.rc_txt + '</a>' +
'<a href="#" id="rmd">' + L.rc_md + '</a>' +
'<a href="#" id="rwopi">' + L.rc_wopi + '</a>' +
'<div id="rs1" class="sep"></div>' +
'<a href="#" id="rcpl">' + L.rc_cpl + '</a>' +
'<a href="#" id="rdl">' + L.rc_dl + '</a>' +
@@ -1245,8 +1249,8 @@ check_image_support('jxl', "data:image/jxl;base64,/woIAAAMABKIAgC4AF3lEgA=");
var img_re = APPLE ?
/\.(a?png|avif|bmp|gif|hei[cf]s?|jpe?g|jfif|svg|webp|webm|mkv|mp4|m4v|mov)(\?|$)/i :
/\.(a?png|avif|bmp|gif|jpe?g|jfif|svg|webp|webm|mkv|mp4|m4v|mov)(\?|$)/i;
/\.(a?png|avif|bmp|gif|hei[cf]s?|jpe?g|jfif|jxl|svg|webp|webm|mkv|mp4|m4v|mov)(\?|$)/i :
/\.(a?png|avif|bmp|gif|jpe?g|jfif|jxl|svg|webp|webm|mkv|mp4|m4v|mov)(\?|$)/i;
var wopi_set = !window.have_wopi ? null :
new Set('odt fodt ott doc docx dotx rtf odm ods fods ots xls xlsx odp fodp otp ppt pptx ppsx odg fodg otg odf'.split(' '));
@@ -1791,7 +1795,7 @@ function MPlayer() {
continue;
tid = tid.slice(1);
if (r.tracks[tid])
if (r.tracks[tid])
order.push(tid);
}
r.order = order;
@@ -5482,7 +5486,7 @@ var showfile = (function () {
r.tgltail = function () {
if (!window.TextDecoderStream) {
bcfg_set('taildoc', r.taildoc = false);
return toast.err(10, L.tail_2old);
return toast.err(10, L.b2old + ';\n' + L.tail_2old);
}
r.show(r.url, true);
};
@@ -5693,8 +5697,8 @@ var thegrid = (function () {
fid = oth.getAttribute('id'),
aplay = ebi('a' + fid),
atext = ebi('t' + fid),
is_txt = atext && !/\.ts$/.test(href) && showfile.getlang(href),
is_img = img_re.test(href),
is_txt = atext && !is_img && !/\.ts$/.test(href) && showfile.getlang(href),
is_dir = href.endsWith('/'),
is_srch = !!ebi('unsearch'),
in_tree = is_dir && treectl.find(oth.textContent.slice(0, -1)),
@@ -6438,6 +6442,11 @@ var ahotkeys = function (e) {
if (k == 'F2')
return fileman.rename();
if (k == 'F4') {
enspin('t', 1);
return treectl.goto();
}
if (!treectl.hidden && (!sh || !thegrid.en)) {
if (kl == 'a')
return QS('#twig').click();
@@ -7692,7 +7701,7 @@ var treectl = (function () {
if (wopi_set && wopi_set.has(tn.ext))
tn.lead = '<a href="?wopi=' + bhref +
'" rel="nofollow" name="' + hname + '">📄</a>';
'" rel="nofollow" target="_blank" name="' + hname + '">📄</a>';
if (tn.lead == '-')
tn.lead = '<a href="?doc=' + bhref + '" id="t' + id +
@@ -7786,6 +7795,7 @@ var treectl = (function () {
r.hydrate = function () {
qsr('#bbsw');
qsr('#js_bork');
srvinf = ebi('srv_info').innerHTML.slice(6, -7);
if (ls0 === null) {
r.ls_cb = showfile.addlinks;
@@ -7990,12 +8000,12 @@ var treectl = (function () {
})();
function enspin(i) {
function enspin(i, ns) {
i = 'dlt_' + i;
if (ebi(i))
return;
var d = mknod('div', i, SPINNER);
d.className = 'dumb_loader_thing';
d.className = 'dumb_loader_thing' + ( ns? ' ns' : '');
if (SPINNER_CSS)
d.style.cssText = SPINNER_CSS;
document.body.appendChild(d);
@@ -8621,7 +8631,7 @@ var setfszf = (function () {
setck('cplng=' + lang);
freshen();
var t = L.tt == 'English' ? '' : Ls.eng.lang_set;
modal.confirm(L.lang_set + "\n\n" + t, location.reload.bind(location), null);
modal.confirm(L.lang_set + "\n\n" + t, relod, null);
}
freshen();
@@ -8872,7 +8882,7 @@ var msel = (function () {
r.evsel = function (e, fun) {
ev(e);
r.so = r.pr = null;
var trs = QSA('#files tbody tr');
var trs = QSA('#files tbody tr' + (ebi('unsearch') ? ':not(.srch_hdr)' : ''));
for (var a = 0, aa = trs.length; a < aa; a++)
clmod(trs[a], 'sel', fun);
r.selui();
@@ -9179,7 +9189,7 @@ function show_md(md, name, div, url, depth) {
if (depth) {
clmod(div, 'raw', 1);
div.textContent = "--[ " + name + " ]---------\r\n" + md;
return toast.warn(10, errmsg + (WebAssembly ? 'failed to load marked.js' : 'your browser is too old'));
return toast.warn(10, errmsg + (WebAssembly ? 'failed to load marked.js' : L.b2old));
}
wfp_debounce.n--;
@@ -9332,6 +9342,7 @@ function sandbox(tgt, rules, allow, cls, html) {
fr.setAttribute('sandbox', rules ? 'allow-' + rules.replace(/ /g, ' allow-') : '');
fr.setAttribute('allow', allow);
fr.setAttribute('srcdoc', html);
tgt.innerHTML = '';
tgt.appendChild(fr);
treectl.sb_msg = true;
return true;
@@ -9827,6 +9838,7 @@ var rcm = (function () {
case 'pla': play('f-' + selFile.id); break;
case 'txt': showfile.show(selFile.name); break;
case 'md': location = selFile.path + (has(selFile.path, '?') ? '&v' : '?v'); break;
case 'wopi': window.open('?wopi=' + selFile.name, '_blank').focus(); break;
case 'cpl': cliptxt(selFile.url, function() {toast.ok(2, L.clipped)}); break;
case 'dl': ebi('seldl').click(); break;
case 'zip': ebi('selzip').click(); break;
@@ -9887,13 +9899,20 @@ var rcm = (function () {
var has_sel = msel.getsel().length;
var has_clip = fileman.clip.length;
var ext = (function() {
if (!selFile.name) return null;
var di = selFile.name.lastIndexOf('.');
if (di < 1) return null;
return selFile.name.slice(di + 1).toLowerCase();
})();
clmod(ebi('ropn'), 'hide', !selFile.path);
clmod(ebi('rply'), 'hide', selFile.type != 'gf' && selFile.type != 'af');
clmod(ebi('rpla'), 'hide', selFile.type != 'gf');
clmod(ebi('rtxt'), 'hide', !selFile.id);
clmod(ebi('rs1'), 'hide', !selFile.path);
clmod(ebi('rmd'), 'hide', !selFile.name || selFile.name.slice(-3) != ".md");
clmod(ebi('rmd'), 'hide', !selFile.name || ext != "md");
clmod(ebi('rwopi'), 'hide', !selFile.name || !wopi_set || !wopi_set.has(ext));
clmod(ebi('rcpl'), 'hide', !selFile.path);
clmod(ebi('rdl'), 'hide', !has_sel);
clmod(ebi('rzip'), 'hide', !has_sel);
@@ -10025,19 +10044,19 @@ function reload_browser() {
var selbox = null;
var ttimer = null;
var lpdelay = 250;
var lpdelay = 250;
var mvthresh = 44;
function unbox() {
qsr('.selbox');
ebi('gfiles').style.removeProperty('pointer-events')
ebi('wrap').style.removeProperty('user-select')
if (selbox) {
console.log(selbox)
window.getSelection().removeAllRanges();
}
is_selma = false;
dragging = false;
fwrap = null;
@@ -10070,7 +10089,7 @@ function reload_browser() {
function sel_start(e) {
if (e.button !== 0 && e.type !== 'touchstart') return;
if (!thegrid.en || !treectl.dsel) return;
if (e.target.closest('#widget,#ops,.opview,.doc')) return;
if (e.target.closest('#widget,#ops,.opview,.doc,.bbox-open')) return;
if (e.target.closest('#gfiles'))
ebi('gfiles').style.userSelect = "none";
@@ -10080,7 +10099,7 @@ function reload_browser() {
starty = pos.y;
is_selma = true;
ttimer = null;
if (e.type === 'touchstart') {
ttimer = setTimeout(function() {
ttimer = null;
@@ -10088,7 +10107,7 @@ function reload_browser() {
}, lpdelay);
}
}
function start_drag() {
if (dragging) return;
@@ -10099,7 +10118,7 @@ function reload_browser() {
ebi('gfiles').style.pointerEvents = 'none';
}
function sel_move(e) {
if (!is_selma) return;
var pos = getpp(e);
@@ -10116,7 +10135,7 @@ function reload_browser() {
if (!dragging && dist > mvthresh && !window.getSelection().toString()) {
if (e.target instanceof Element)
fwrap = e.target.closest('#wrap');
if (fwrap)
if (fwrap)
fwrap.style.userSelect = 'none';
else return;
start_drag();
@@ -10165,7 +10184,7 @@ function reload_browser() {
}
});
}
dsel_init();
})();
@@ -10208,7 +10227,7 @@ var mpss = (function() {
var gain = afilt.ssg.gain;
var duration = ae.duration || 0;
var slimit = duration * (config.sthresh / 100);
var elimit = duration * (1 - (config.etresh / 100));
var in_limits = ae.currentTime < slimit || ae.currentTime > elimit;
+3
View File
@@ -325,6 +325,9 @@ var modpoll = new Modpoll();
window.onbeforeunload = function (e) {
if (window.gtfo)
return;
if ((ebi("save").className + '').indexOf('disabled') >= 0)
return; //nice (todo)
+10 -10
View File
@@ -54,7 +54,7 @@
<div class="os win">
<p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p>
<pre>
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
rclone config create --obscure {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s --network-mode {{ aname }}-dav:{{ rvp }} <b>W:</b>
</pre>
<ul>
@@ -77,7 +77,7 @@
<div class="os lin">
<p>rclone (v1.63 or later) is recommended:</p>
<pre>
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
rclone config create --obscure {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>mp</b>
</pre>
<ul>
@@ -145,9 +145,9 @@
<p>connect with plaintext FTP:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
rclone config create --obscure {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
{%- else %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
rclone config create --obscure {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s --network-mode {{ aname }}-ftp:{{ rvp }} <b>W:</b>
</pre>
@@ -156,9 +156,9 @@
<p>connect with TLS-encrypted FTPS:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
rclone config create --obscure {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
{%- else %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
rclone config create --obscure {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s --network-mode {{ aname }}-ftps:{{ rvp }} <b>W:</b>
</pre>
@@ -184,9 +184,9 @@
<p>connect with plaintext FTP:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
rclone config create --obscure {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
{%- else %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
rclone config create --obscure {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>mp</b>
</pre>
@@ -195,9 +195,9 @@
<p>connect with TLS-encrypted FTPS:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
rclone config create --obscure {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
{%- else %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
rclone config create --obscure {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>mp</b>
</pre>
+3
View File
@@ -39,6 +39,7 @@ Ls.chi = {
["ctrl-V", "粘贴(移动/复制)到此处"],
["Y", "下载选中项"],
["F2", "重命名选中项"],
["F4", "刷新"], //m
"文件列表选择",
["space", "切换文件选择"],
@@ -576,6 +577,7 @@ Ls.chi = {
"u_ancient": '你还在用远古浏览器,也许你应该 <a href="#" id="u2nah">改用 bup</a>',
"u_nowork": "需要 Firefox 53+ 或 Chrome 57+ 或 iOS 11+",
"tail_2old": "需要 Firefox 105+ 或 Chrome 71+ 或 iOS 14.5+",
"b2old": '浏览器版本太低', //m
"u_nodrop": '浏览器版本低,不支持通过拖动文件到窗口来上传文件',
"u_notdir": "这不是文件夹!\n\n你的浏览器太老了,\n请尝试将文件夹拖入窗口",
"u_uri": "要从其他浏览器窗口拖放图片,\n请将其拖放到大上传按钮上",
@@ -659,6 +661,7 @@ Ls.chi = {
"rc_pla": "作为音频播放",
"rc_txt": "在文本文件查看器中打开",
"rc_md": "在 markdown 查看器中打开",
"rc_wopi": "在 office 编辑器中打开", //m
"rc_dl": "下载",
"rc_zip": "下载为压缩包",
"rc_cpl": "复制链接",
+3
View File
@@ -39,6 +39,7 @@ Ls.cze = {
["ctrl-V", "vložit (přesunout/kopírovat) zde"],
["Y", "stáhnout vybrané"],
["F2", "přejmenovat vybrané"],
["F4", "obnovit"], //m
"výběr souborů",
["space", "přepnout výběr souboru"],
@@ -579,6 +580,7 @@ Ls.cze = {
"u_ancient": "váš prohlížeč je úctyhodně starý -- možná byste měli <a href=\"#\" id=\"u2nah\">použít bup</a>",
"u_nowork": "vyžadován firefox 53+ nebo chrome 57+ nebo iOS 11+",
"tail_2old": "vyžadován firefox 105+ nebo chrome 71+ nebo iOS 14.5+",
"b2old": "váš prohlížeč je příliš starý", //m
"u_nodrop": "váš prohlížeč je příliš starý pro nahrávání přetažením (drag-and-drop)",
"u_notdir": "toto není složka!\n\nváš prohlížeč je příliš starý,\nzkuste prosím soubory přetáhnout",
"u_uri": "pro přetažení obrázků z jiných oken prohlížeče,\nje prosím přetáhněte na velké tlačítko pro nahrávání",
@@ -662,6 +664,7 @@ Ls.cze = {
"rc_pla": "přehrát jako zvuk", //m
"rc_txt": "otevřít v prohlížeči souborů", //m
"rc_md": "otevřít v textovém editoru", //m
"rc_wopi": "otevřít v office editoru", //m
"rc_dl": "stáhnout", //m
"rc_zip": "stáhnout jako archiv", //m
"rc_cpl": "kopírovat odkaz", //m
+3
View File
@@ -39,6 +39,7 @@ Ls.deu = {
["STRG-V", "Zwischenablage hier einfügen"],
["Y", "Auswahl herunterladen"],
["F2", "Auswahl umbenennen"],
["F4", "Aktualisieren"], //m
"file-list-sel",
["LEER", "Dateiauswahl aktivieren"],
@@ -575,6 +576,7 @@ Ls.deu = {
"u_ancient": 'Dein Browser ist verdammt antik -- vielleicht solltest du <a href="#" id="u2nah">stattdessen bup benutzen</a>',
"u_nowork": "Benötigt Firefox 53+ oder Chrome 57+ oder iOS 11+",
"tail_2old": "Benötigt Firefox 105+ oder Chrome 71+ oder iOS 14.5+",
"b2old": 'Dein Browser ist zu alt', //m
"u_nodrop": 'Dein Browser ist zu alt für Drag-and-Drop Uploads',
"u_notdir": "Das ist kein Ordner!\n\nDein Browser ist zu alt,\nversuch stattdessen dragdrop",
"u_uri": "Um Bilder per Drag-and-Drop aus anderen Browserfenstern hochzuladen,\nlass' sie bitte über dem grossen Upload-Button fallen",
@@ -658,6 +660,7 @@ Ls.deu = {
"rc_pla": "als Audio abspielen",
"rc_txt": "als Text öffnen",
"rc_md": "im Texteditor öffnen",
"rc_wopi": "im office-editor öffnen", //m
"rc_dl": "herunterladen",
"rc_zip": "als Archiv herunterladen",
"rc_cpl": "Link kopieren", //m
+3
View File
@@ -39,6 +39,7 @@ Ls.epo = {
["stir-V", "alglui (movi/kopii) ĉi tien"],
["Y", "elŝuti elektitajn"],
["F2", "alinomi elektitajn"],
["F4", "refreŝigi"], //m
"file-list-sel",
["spacoklavo", "baskuli elektadon de dosieroj"],
@@ -575,6 +576,7 @@ Ls.epo = {
"u_ancient": 'via retumilo estas vere antikva -- eble vi devus <a href="#" id="u2nah">uzi alŝutilon bup anstataŭe</a>',
"u_nowork": "Firefox 53+ aŭ Chrome 57+ aŭ iOS 11+ necesas",
"tail_2old": "Firefox 105+ aŭ Chrome 71+ aŭ iOS 14.5+ necesas",
"b2old": 'via retumilo estas tro malnova', //m
"u_nodrop": 'via retumilo estas tro malnova por ŝova-kaj-demeta alŝutado',
"u_notdir": "tio ne estas dosierujo!\n\nvia retumilo estas tro malnova,\nbonvolu ŝovu kaj demetu anstataŭe",
"u_uri": "por ŝovi-kaj-demeti bildon de aliaj fenestroj de retumiloj,\nbonvolu demeti ĝin sur la grandan alŝut-butonon",
@@ -658,6 +660,7 @@ Ls.epo = {
"rc_pla": "ludi kiel aŭdiaĵo",
"rc_txt": "malfermi per tekstovidilo",
"rc_md": "malfermi per tekstoredaktilo",
"rc_wopi": "malfermi per office-redaktilo", //m
"rc_dl": "elŝuti",
"rc_zip": "elŝuti kiel arkivo",
"rc_cpl": "kopii ligilon",
+3
View File
@@ -39,6 +39,7 @@ Ls.fin = {
["ctrl-V", "siirrä tai kopioi tähän"],
["Y", "lataa valitut"],
["F2", "uudelleennimeä valitut"],
["F4", "päivitä"], //m
"file-list-sel",
["space", "vaihda tiedostonvalintatilaan"],
@@ -575,6 +576,7 @@ Ls.fin = {
"u_ancient": 'selaimesi on ns. vaikuttavan ikivanha -- kannattais varmaan <a href="#" id="u2nah">käyttää bup:ia tän sijaan</a>',
"u_nowork": "tarvitaan firefox 53+ tai chrome 57+ tai iOS 11+",
"tail_2old": "tarvitaan firefox 105+ tai chrome 71+ tai iOS 14.5+",
"b2old": 'selaimesi on liian vanha', //m
"u_nodrop": 'selaimesi on liian vanha vedä-ja-pudota lataamiseen',
"u_notdir": "tuo ei ole hakemisto!\n\nselaimesi on liian vanha,\nkokeile sen sijaan 'vedä-pudota'-tekniikkaa.",
"u_uri": "'vedä-pudottaaksesi' kuvia muista selainikkunoista,\npudota se isoon latausnapppiin",
@@ -658,6 +660,7 @@ Ls.fin = {
"rc_pla": "toista äänitiedostona",
"rc_txt": "avaa tekstinäkymässä",
"rc_md": "avaa markdown-näkymässä",
"rc_wopi": "avaa office-editorissa", //m
"rc_dl": "lataa",
"rc_zip": "lataa arkistona",
"rc_cpl": "kopioi linkki",
+3
View File
@@ -39,6 +39,7 @@ Ls.fra = {
["ctrl-V", "coller (déplacer/copier) ici"],
["Y", "télécharger la sélection"],
["F2", "renomer la sélection"],
["F4", "actualiser"], //m
"file-list-sel",
["Espace", "activer la sélection de fichiers"],
@@ -575,6 +576,7 @@ Ls.fra = {
"u_ancient": 'votre navigateur est impressionnamment ancien -- vous devriez peut-être <a href="#" id="u2nah">utiliser bup à la place</a>',
"u_nowork": "nécessite firefox 53+ ou chrome 57+ ou iOS 11+",
"tail_2old": "nécessite firefox 105+ ou chrome 71+ ou iOS 14.5+",
"b2old": 'votre navigateur est trop ancien', //m
"u_nodrop": 'votre navigateur est trop ancien pour le téléversement par glisser-déposer',
"u_notdir": "ce n'est pas un dossier!\n\nvotre navigateur est trop ancien,\nveuillez essayer le glisser-déposer à la place",
"u_uri": "pour glisser-déposer des images depuis d'autres fenêtres de navigateur,\nveuillez les déposer sur le gros bouton de téléversement",
@@ -658,6 +660,7 @@ Ls.fra = {
"rc_pla": "Lire comme audio", //m
"rc_txt": "ouvrir dans le visionneur de fichiers", //m
"rc_md": "ouvrir dans l’éditeur de texte", //m
"rc_wopi": "ouvrir dans l’éditeur office", //m
"rc_dl": "télécharger", //m
"rc_zip": "télécharger comme archive", //m
"rc_cpl": "copier le lien", //m
+3
View File
@@ -39,6 +39,7 @@ Ls.grc = {
["ctrl-V", "επικόλληση (μετακίνηση/αντιγραφή) εδώ"],
["Y", "λήψη επιλεγμένων"],
["F2", "μετονομασία επιλεγμένων"],
["F4", "ανανέωση"], //m
"λίστα αρχείων",
["space", "εναλλαγή επιλογής αρχείου"],
@@ -575,6 +576,7 @@ Ls.grc = {
"u_ancient": 'ο browser σου είναι εντυπωσιακά απαρχαιωμένος — ίσως να <a href="#" id="u2nah">χρησιμοποιήσεις το bup αντί γι\' αυτό</a>',
"u_nowork": "χρειάζεται firefox 53+ ή chrome 57+ ή iOS 11+",
"tail_2old": "χρειάζεται firefox 105+ ή chrome 71+ ή iOS 14.5+",
"b2old": 'ο browser σου είναι πολύ παλιός', //m
"u_nodrop": "ο browser σου είναι πολύ παλιός για drag&amp;drop μεταφορτώσεις",
"u_notdir": "αυτός δεν είναι φάκελος!\n\nο browser σου είναι πολύ παλιός,\nδοκίμασε drag&amp;drop αντ' αυτού",
"u_uri": "για να κάνεις drag&amp;drop εικόνων από άλλα παράθυρα browser,\nρίξ' τες πάνω στο μεγάλο κουμπί μεταφόρτωσης",
@@ -658,6 +660,7 @@ Ls.grc = {
"rc_pla": "αναπαραγωγή ως ήχος", //m
"rc_txt": "άνοιγμα στον προβολέα αρχείων", //m
"rc_md": "άνοιγμα στον επεξεργαστή κειμένου", //m
"rc_wopi": "άνοιγμα στον office επεξεργαστή", //m
"rc_dl": "λήψη", //m
"rc_zip": "λήψη ως αρχείο", //m
"rc_cpl": "ἀντίγραφε σύνδεσμον", //m
+3
View File
@@ -36,6 +36,7 @@ Ls.hun = {
['ctrl-V', 'beillesztés ide'],
['Y', 'kijelöltek letöltése'],
['F2', 'átnevezés'],
["F4", "frissítés"], //m
'kijelölés',
['space', 'fájl kijelölése'],
@@ -576,6 +577,7 @@ Ls.hun = {
"u_ancient": 'lenyűgözően őskori a böngésződ -- talán <a href="#" id="u2nah">használd inkább az egyszerű uploader-t</a>',
"u_nowork": 'legalább Firefox 53, Chrome 57 vagy iOS 11 kell',
"tail_2old": 'legalább Firefox 105, Chrome 71 vagy iOS 14.5 kell',
"b2old": 'a böngésződ túl öreg', //m
"u_nodrop": 'a böngésződ túl öreg a drag-and-drop-hoz',
"u_notdir": 'ez nem egy mappa!\n\na böngésződ túl öreg,\npróbáld meg inkább behúzni',
"u_uri": 'képek behúzásához más ablakból,\ndobd rá a nagy feltöltés gombra',
@@ -659,6 +661,7 @@ Ls.hun = {
"rc_pla": 'lejátszás audióként',
"rc_txt": 'megnyitás szövegként',
"rc_md": 'megnyitás markdownként',
"rc_wopi": "megnyitás office szerkesztőben", //m
"rc_dl": 'letöltés',
"rc_zip": 'letöltés archívumként',
"rc_cpl": 'link másolása',
+3
View File
@@ -39,6 +39,7 @@ Ls.ita = {
["ctrl-V", "incolla (sposta/copia) qui"],
["Y", "scarica selezionati"],
["F2", "rinomina selezionati"],
["F4", "aggiorna"], //m
"file-list-sel",
["spazio", "alterna selezione file"],
@@ -575,6 +576,7 @@ Ls.ita = {
"u_ancient": 'il tuo browser è incredibilmente antico -- forse dovresti <a href="#" id="u2nah">usare bup invece</a>',
"u_nowork": "serve firefox 53+ o chrome 57+ o iOS 11+",
"tail_2old": "serve firefox 105+ o chrome 71+ o iOS 14.5+",
"b2old": 'il tuo browser è troppo vecchio', //m
"u_nodrop": 'il tuo browser è troppo vecchio per il caricamento drag-and-drop',
"u_notdir": "quella non è una cartella!\n\nil tuo browser è troppo vecchio,\nprova dragdrop invece",
"u_uri": "per trascinare immagini da altre finestre del browser,\nrilasciale sul pulsante upload grande",
@@ -658,6 +660,7 @@ Ls.ita = {
"rc_pla": "riproduci come audio", //m
"rc_txt": "apri nel visualizzatore di file", //m
"rc_md": "apri nell’editor di testo", //m
"rc_wopi": "apri nell’editor office", //m
"rc_dl": "scarica", //m
"rc_zip": "scarica come archivio", //m
"rc_cpl": "copia link", //m
+3
View File
@@ -39,6 +39,7 @@ Ls.jpn = {
["ctrl-V", "ここに貼り付け(移動/コピー)"],
["Y", "選択した項目をダウンロード"],
["F2", "選択した項目の名前を変更"],
["F4", "更新"], //m
"file-list-sel",
["space", "ファイル選択の切り替え"],
@@ -575,6 +576,7 @@ Ls.jpn = {
"u_ancient": '現在のブラウザは驚くほど古いです -- <a href="#" id="u2nah">代わりにbup</a>を使った方がいいかもしれません',
"u_nowork": "Firefox 53以降、Chrome 57以降、またはiOS 11以降が必要です",
"tail_2old": "Firefox 105以降、Chrome 71以降、またはiOS 14.5以降が必要です",
"b2old": '現在のブラウザは古すぎます', //m
"u_nodrop": '現在のブラウザは古すぎてドラッグ&ドロップによるアップロードに対応していません',
"u_notdir": "それはフォルダではありません!\n\nブラウザが古すぎです、\n代わりにドラッグドロップを試してください",
"u_uri": "他のブラウザウィンドウから画像をドラッグアンドドロップするには、\n大きなアップロードボタンにドロップしてください",
@@ -658,6 +660,7 @@ Ls.jpn = {
"rc_pla": "オーディオとして再生",
"rc_txt": "ファイルビューアで開く",
"rc_md": "markdownエディターで開く",
"rc_wopi": "officeエディターで開く", //m
"rc_dl": "ダウンロード",
"rc_zip": "圧縮してダウンロード",
"rc_cpl": "リンクをコピー",
+3
View File
@@ -39,6 +39,7 @@ Ls.kor = {
["ctrl-V", "여기에 붙여넣기 (이동/복사)"],
["Y", "선택 항목 다운로드"],
["F2", "선택 항목 이름 바꾸기"],
["F4", "새로 고침"], //m
"파일 목록 선택",
["space", "파일 선택/해제"],
@@ -575,6 +576,7 @@ Ls.kor = {
"u_ancient": '브라우저가 정말 오래되었네요 -- 아마도 <a href="#" id="u2nah">bup을 대신 사용</a>해야 할 것 같습니다',
"u_nowork": "Firefox 53+, Chrome 57+ 또는 iOS 11+가 필요합니다",
"tail_2old": "Firefox 105+, Chrome 71+ 또는 iOS 14.5+가 필요합니다",
"b2old": '브라우저가 너무 오래되었습니다', //m
"u_nodrop": '브라우저가 너무 오래되어 드래그 앤 드롭 업로드를 지원하지 않습니다',
"u_notdir": '폴더가 아닙니다!\n\n브라우저가 너무 오래되었습니다,\n대신 드래그드롭을 시도해보세요',
"u_uri": '다른 브라우저 창에서 이미지를 드래그드롭하려면,\n큰 업로드 버튼 위로 떨어뜨려주세요',
@@ -658,6 +660,7 @@ Ls.kor = {
"rc_pla": "오디오로 재생", //m
"rc_txt": "파일 뷰어에서 열기", //m
"rc_md": "텍스트 편집기에서 열기", //m
"rc_wopi": "office 편집기에서 열기", //m
"rc_dl": "다운로드", //m
"rc_zip": "압축 파일로 다운로드", //m
"rc_cpl": "링크 복사", //m
+3
View File
@@ -39,6 +39,7 @@ Ls.nld = {
["ctrl-V", "Hier plakken (verplaatsen/kopieëren)"],
["Y", "Download geselecteerde"],
["F2", "Hernoem geselecteerde"],
["F4", "vernieuwen"], //m
"bestand-lijst-selectie",
["space", "wissel bestand selectie"],
@@ -575,6 +576,7 @@ Ls.nld = {
"u_ancient": 'Je browser is indrukwekkend oud -- misschien moet je <a href="#" id="u2nah">in plaats daarvan bup gebruiken</a>',
"u_nowork": "Je moet firefox 53+ of chrome 57+ of iOS 11+ hebben",
"tail_2old": "Je moet firefox 105+ of chrome 71+ of iOS 14.5+ hebben",
"b2old": 'Je browser is te oud', //m
"u_nodrop": 'Je browser is te oud voor uploaden via slepen en neerzetten',
"u_notdir": "Dat is geen map!\n\nuw browser is te oud,\nprobeer in plaats daarvan sleep en neerzetten",
"u_uri": "Om afbeeldingen te slepen vanuit andere browser tabblad,\nplaats deze dan op de grote uploadknop",
@@ -658,6 +660,7 @@ Ls.nld = {
"rc_pla": "Afspelen als audio", //m
"rc_txt": "Openen in bestandsviewer", //m
"rc_md": "Openen in teksteditor", //m
"rc_wopi": "Openen in office-editor", //m
"rc_dl": "Downloaden", //m
"rc_zip": "Downloaden als archief", //m
"rc_cpl": "Link kopiëren", //m
+3
View File
@@ -36,6 +36,7 @@ Ls.nno = {
["ctrl-V", "lim inn (flytt/kopiér)"],
["Y", "last ned valde"],
["F2", "endre namn på valde"],
["F4", "oppdater"],
"filmarkering",
["space", "markér fil"],
@@ -572,6 +573,7 @@ Ls.nno = {
"u_ancient": 'nettlesaren din er prehistorisk -- mulig du burde <a href="#" id="u2nah">bruke bup i staden for</a>',
"u_nowork": "krev firefox 53+, chrome 57+, eller iOS 11+",
"tail_2old": "krev firefox 105+, chrome 71+, eller iOS 14.5+",
"b2old": 'nettlesaren din er for gamal',
"u_nodrop": 'nettlesaren din er for gamal åt å laste opp filer ved å drage dei inn i vindauget',
"u_notdir": "mottok ikkje mappa!\n\nnettlesaren din er for gamal,\nprøv å drage mappa inn i vindauget i staden for",
"u_uri": "for å laste opp bilder frå andre nettlesarvindauge,\nslipp bildet rett på den store last-opp-knappen",
@@ -655,6 +657,7 @@ Ls.nno = {
"rc_pla": "spel av som lyd",
"rc_txt": "opne i filvisar",
"rc_md": "opne i tekstredigerar",
"rc_wopi": "opne i office-redigerar",
"rc_dl": "Last ned",
"rc_zip": "Last ned som arkiv",
"rc_cpl": "kopier lenke",
+3
View File
@@ -36,6 +36,7 @@ Ls.nor = {
["ctrl-V", "lim inn (flytt/kopiér)"],
["Y", "last ned valgte"],
["F2", "endre navn på valgte"],
["F4", "oppdater"],
"filmarkering",
["space", "marker fil"],
@@ -572,6 +573,7 @@ Ls.nor = {
"u_ancient": 'nettleseren din er prehistorisk -- mulig du burde <a href="#" id="u2nah">bruke bup istedenfor</a>',
"u_nowork": "krever firefox 53+, chrome 57+, eller iOS 11+",
"tail_2old": "krever firefox 105+, chrome 71+, eller iOS 14.5+",
"b2old": 'nettleseren din er for gammel',
"u_nodrop": 'nettleseren din er for gammel til å laste opp filer ved å dra dem inn i vinduet',
"u_notdir": "mottok ikke mappen!\n\nnettleseren din er for gammel,\nprøv å dra mappen inn i vinduet istedenfor",
"u_uri": "for å laste opp bilder ifra andre nettleservinduer,\nslipp bildet rett på den store last-opp-knappen",
@@ -655,6 +657,7 @@ Ls.nor = {
"rc_pla": "spill av som lyd",
"rc_txt": "åpne i filviser",
"rc_md": "åpne i teksteditor",
"rc_wopi": "åpne i office-editor",
"rc_dl": "Last ned",
"rc_zip": "Last ned som arkiv",
"rc_cpl": "kopier lenke",
+3
View File
@@ -39,6 +39,7 @@ Ls.pol = {
["ctrl-V", "wklej (przenieś/skopiuj) tutaj"],
["Y", "pobierz zaznaczone"],
["F2", "zmień nazwę zaznaczonych"],
["F4", "odśwież"], //m
"file-list-sel",
["spacja", "przełącz zaznaczanie plików"],
@@ -578,6 +579,7 @@ Ls.pol = {
"u_ancient": 'twoja przeglądarka jest niezwykle zabytkowa -- powinieneś zamiast tego <a href="#" id="u2nah">użyć bup</a>',
"u_nowork": "wymaga Firefox 53+, Chrome 57+ lub iOS 11+",
"tail_2old": "wymaga Firefox 105+, Chrome 71+ lub iOS 14.5+",
"b2old": 'ta przeglądarka jest za stara', //m
"u_nodrop": 'ta przeglądarka jest za stara, nie wspiera przesyłania "przeciągnij i upuść"',
"u_notdir": "to nie jest folder!\n\nta przeglądarka jest za stara\nspróbuj przeciągnąć i upuścić",
"u_uri": "aby przeciągnąć i upuścić obrazy z innych okien przeglądarki,\nupuść je na duży przycisk przesyłania",
@@ -661,6 +663,7 @@ Ls.pol = {
"rc_pla": "odtwórz jako dźwięk", //m
"rc_txt": "otwórz w przeglądarce plików", //m
"rc_md": "otwórz w edytorze tekstu", //m
"rc_wopi": "otwórz w edytorze office", //m
"rc_dl": "pobierz", //m
"rc_zip": "pobierz jako archiwum", //m
"rc_cpl": "kopiuj link", //m
+3
View File
@@ -39,6 +39,7 @@ Ls.por = {
["ctrl-V", "colar (mover/copiar) aqui"],
["Y", "baixar selecionado"],
["F2", "renomear selecionado"],
["F4", "atualizar"], //m
"seleção de lista de arquivos",
["espaço", "alternar seleção de arquivo"],
@@ -575,6 +576,7 @@ Ls.por = {
"u_ancient": 'seu navegador é impressionantemente antigo -- talvez você devesse <a href="#" id="u2nah">usar o bup em vez disso</a>',
"u_nowork": "precisa do firefox 53+ ou chrome 57+ ou iOS 11+",
"tail_2old": "precisa do firefox 105+ ou chrome 71+ ou iOS 14.5+",
"b2old": 'seu navegador é muito antigo', //m
"u_nodrop": 'seu navegador é muito antigo para upload de arrastar e soltar',
"u_notdir": "isso não é uma pasta!\n\nseu navegador é muito antigo,\npor favor, tente arrastar e soltar em vez disso",
"u_uri": "para arrastar e soltar imagens de outras janelas do navegador,\npor favor, solte-as no grande botão de upload",
@@ -658,6 +660,7 @@ Ls.por = {
"rc_pla": "reproduzir como áudio",
"rc_txt": "abrir no leitor de texto",
"rc_md": "abrir no leitor markdown",
"rc_wopi": "abrir no editor office", //m
"rc_dl": "baixar",
"rc_zip": "baixar compactado",
"rc_cpl": "copiar link",
+62 -59
View File
@@ -39,6 +39,7 @@ Ls.rus = {
["ctrl-V", "вставить (переместить/копировать) сюда"],
["Y", "скачать выделенное"],
["F2", "переименовать выделенное"],
["F4", "обновить"],
"выделение файлов",
["пробел", "выделить/снять выделение с текущего файла"],
@@ -84,8 +85,8 @@ Ls.rus = {
["M", "закрыть файл"],
["E", "отредактировать файл"],
["S", "выделить файл"],
["Y", "скачать текстовый файл"], //m
["⇧ J", "приукрасить json"], //m
["Y", "скачать текстовый файл"],
["⇧ J", "форматировать json для чтения"],
]
],
@@ -111,14 +112,14 @@ Ls.rus = {
"gou": 'родительская папка">вверх',
"gon": 'следующая папка">след',
"logout": "Выйти ",
"login": "Войти", //m
"login": "Войти",
"access": " доступ",
"ot_close": "закрыть подменю",
"ot_search": "`искать файлы по атрибутам, пути / имени, музыкальным тегам или любой другой комбинации из следующих конструкций$N$N`foo bar` = обязано содержать «foo» И «bar»,$N`foo -bar` = обязано содержать «foo», но не «bar»,$N`^yana .opus$` = начинается с «yana» и имеет расширение «opus»$N`&quot;try unite&quot;` = содержит именно «try unite»$N$Nформат времени задаётся по стандарту iso-8601, например$N`2009-12-31` или `2020-09-12 23:30:00`",
"ot_unpost": "unpost: удалить ваши недавние загрузки и отменить незавершённые",
"ot_bup": "bup: легковесный загрузчик файлов, поддерживает даже netscape 4.0",
"ot_mkdir": "mkdir: создать новую папку",
"ot_md": "new-file: создать новый текстовый файл", //m
"ot_md": "new-file: создать новый текстовый файл",
"ot_msg": "msg: отправить сообщение в лог сервера",
"ot_mp": "настройка медиаплеера",
"ot_cfg": "остальные настройки",
@@ -127,7 +128,7 @@ Ls.rus = {
"ot_noie": 'Пожалуйста, используйте Chrome / Firefox / Edge',
"ab_mkdir": "создать папку",
"ab_mkdoc": "создать текстовый файл", //m
"ab_mkdoc": "создать текстовый файл",
"ab_msg": "отправить сообщение в лог сервера",
"ay_path": "перейти к папкам",
@@ -155,7 +156,7 @@ Ls.rus = {
"ul_par": "параллельные загрузки:",
"ut_rand": "случайные имена файлов",
"ut_u2ts": "копировать время последнего изменения$Nиз вашей файловой системы на сервер\">📅",
"ut_ow": "перезаписывать существующие файлы на сервере?$N🛡️: нет (для повторяющихся файлов будут создаваться новые имена)$N🕒: перезаписать файлы с датой изменения старее, чем у загружаемых$N♻️: всегда перезаписывать (если файлы различаются по содержанию)$N⏭️: безусловно пропускать все существующие файлы", //m
"ut_ow": "перезаписывать существующие файлы на сервере?$N🛡️: нет (для повторяющихся файлов будут создаваться новые имена)$N🕒: перезаписать файлы с датой изменения старее, чем у загружаемых$N♻️: всегда перезаписывать (если файлы различаются по содержанию)$N⏭️: безусловно пропускать все существующие файлы",
"ut_mt": "продолжать хешировать другие файлы во время загрузки$N$Nесть смысл отключить при медленном диске или процессоре",
"ut_ask": 'требовать подтверждения перед началом загрузки">💭',
"ut_pot": "улучшить скорость загрузки на слабых устройства$Nс помощью упрощения интерфейса",
@@ -206,7 +207,7 @@ Ls.rus = {
"u_nav_b": '<a href="#" id="modal-ok">Файлы</a><a href="#" id="modal-ng">Одна папка</a>',
"cl_opts": "переключатели",
"cl_hfsz": "размер файла", //m
"cl_hfsz": "размер файла",
"cl_themes": "тема",
"cl_langs": "язык",
"cl_ziptype": "архивация папок",
@@ -214,27 +215,27 @@ Ls.rus = {
"cl_favico": "иконка",
"cl_bigdir": "бол. папки",
"cl_hsort": "#сорт.",
"cl_keytype": "схема горячих клавиш",
"cl_keytype": "музыкальная нотация",
"cl_hiddenc": "скрытые столбцы",
"cl_hidec": "скрыть",
"cl_reset": "сбросить",
"cl_hpick": "нажмите на заголовки столбцов, чтобы скрыть их в таблице ниже",
"cl_hcancel": "скрытие столбца отменено",
"cl_rcm": "контекстное меню", //m
"cl_gauto": "авто田", //m
"cl_rcm": "контекстное меню",
"cl_gauto": "авто田",
"ct_grid": '田 сетка',
"ct_ttips": '◔ ◡ ◔">ℹ️ подсказки',
"ct_thumb": 'переключение между иконками и миниатюрами в режиме сетки$NГорячая клавиша: T">🖼️ миниат.',
"ct_csel": 'держите CTRL или SHIFT для выделения файлов в режиме сетки">выбор',
"ct_dsel": 'использовать выделение перетаскиванием в режиме сетки">перетащить', //m
"ct_dl": 'принудительная загрузка (не показывать встроенно) при щелчке по файлу">dl', //m
"ct_dsel": 'выделять файлы перетаскиванием в режиме сетки">выделение',
"ct_dl": 'скачивать файл при клике вместо предпросмотра">dl',
"ct_ihop": 'показывать последний открытый файл после закрытия просмотрщика изображений">g⮯',
"ct_dots": 'показывать скрытые файлы (если есть доступ)">скрыт.',
"ct_qdel": 'спрашивать подтверждение только один раз перед удалением файлов">быстр. удал.',
"ct_dir1st": 'разместить папки над файлами">📁 сверху',
"ct_nsort": 'сортировка по числам$N(например, файл с &gt;code&lt;2&gt;/code&lt; в начале названия идёт перед &gt;code&lt;11&gt;/code&lt;)">нат. сорт.',
"ct_utc": 'используйте UTC для всех временных меток">UTC', //m
"ct_utc": 'показывать все даты в UTC">UTC',
"ct_readme": 'показывать содержимое README.md в описании папки">📜 ридми',
"ct_idxh": 'показывать страницу index.html в текущей папке вместо интерфейса">htm',
"ct_sbars": 'показывать полосы прокрутки">⟊',
@@ -276,8 +277,8 @@ Ls.rus = {
"tt_dynt": "автоматическое расширение панели",
"tt_wrap": "перенос слов",
"tt_hover": "раскрывать обрезанные строки при наведении$N( ломает скроллинг, если $N&nbsp; курсор не в пустоте слева )",
"tt_gauto": "показывать как сетку или список в зависимости от содержимого папки", //m
"tt_gathr": "использовать сетку, если этот процент файлов — изображения/видео", //m
"tt_gauto": "показывать сетку или список в зависимости от содержимого папки",
"tt_gathr": "использовать сетку, если этот процент файлов — изображения/видео",
"ml_pmode": "в конце папки...",
"ml_btns": "команды",
@@ -286,7 +287,7 @@ Ls.rus = {
"ml_tint": "затемн.",
"ml_eq": "эквалайзер",
"ml_drc": "компрессор",
"ml_ss": "пропускать тишину", //m
"ml_ss": "пропускать тишину",
"mt_loop": "повторять один трек\">🔁",
"mt_one": "остановить после этого трека\">1️⃣",
@@ -315,8 +316,8 @@ Ls.rus = {
"mt_c2owa": "opus-weba, для iOS 17.5 и выше\">owa",
"mt_c2caf": "opus-caf, для iOS 11-17\">caf",
"mt_c2mp3": "для очень старых устройств\">mp3",
"mt_c2flac": "лучшее качество звука, но большие файлы\">flac", //m
"mt_c2wav": "не сжатое воспроизведение (ещё больше)\">wav", //m
"mt_c2flac": "лучшее качество звука, но большие файлы\">flac",
"mt_c2wav": "несжатое воспроизведение (файлы ещё больше)\">wav",
"mt_c2ok": "хороший выбор",
"mt_c2nd": "это не рекомендованный вариант формата для вашего устройства, но сойдёт",
"mt_c2ng": "не похоже, что ваше устройство поддерживает этот формат, но давайте попробуем и узнаем наверняка",
@@ -324,11 +325,11 @@ Ls.rus = {
"mt_tint": "непрозрачность фона (0-100) на полосе воспроизведения$N$Nделает буферизацию менее отвлекающей",
"mt_eq": "`включить эквалайзер$N$Nboost `0` = стандартная громкость$N$Nwidth `1 &nbsp;` = обычное стерео$Nwidth `0.5` = микширование левого и правого каналов на 50%$Nwidth `0 &nbsp;` = моно$N$Nboost `-0.8` и width `10` = удаление голоса :^)$N$Nвключённый эквалайзер полностью убирает задержку между треками, поэтому следует его включить со всеми значениями на 0 (кроме width = 1), если вам нужно бесшовное воспроизведение",
"mt_drc": "включить компрессор; также включит эквалайзер для баланса вселенной, так что выставьте всё на 0 кроме width, если он вам не нужен$N$Nпонижает громкость при волне выше значения dB в tresh; каждый dB в ratio равен одному dB на выходе, так что стандартные значения tresh = -24 и ratio = 12 сделают так, что звук никогда не будет громче -22 dB. При таком раскладе можно поставить boost в эквалайзере на 0.8 или даже 1.8 при значении atk = 0 и огромном rls вроде 90 (работает только в Firefox, rls не может быть выше 1 в других браузерах)$N$N(загляните в википедию, там всё объяснено подробнее)",
"mt_ss": "`включает пропуск тишины; умножает скорость на `уск` возле начала/конца, когда громкость ниже `гром` и позиция в первых `нач`% или последних `кон`%", //m
"mt_ssvt": "порог громкости (0-255)\">гром", //m
"mt_ssts": "активный порог (% трека, начало)\">нач", //m
"mt_sste": "активный порог (% трека, конец)\">кон", //m
"mt_sssm": "множитель скорости воспроизведения\">уск", //m
"mt_ss": "`включает пропуск тишины; умножает скорость на `уск` возле начала/конца, когда громкость ниже `гром` и позиция в первых `нач`% или последних `кон`%",
"mt_ssvt": "порог громкости (0-255)\">гром",
"mt_ssts": "активный порог (% трека, начало)\">нач",
"mt_sste": "активный порог (% трека, конец)\">кон",
"mt_sssm": "множитель ускорения\">уск",
"mb_play": "играть",
"mm_hashplay": "воспроизвести этот музыкальный файл?",
@@ -345,7 +346,7 @@ Ls.rus = {
"mm_eunk": "Неопознанная ошибка",
"mm_e404": "Не удалось воспроизвести аудио; ошибка 404: Файл не найден.",
"mm_e403": "Не удалось воспроизвести аудио; ошибка 403: Доступ запрещён.\n\nПопробуйте перезагрузить страницу, возможно, ваша сессия истекла",
"mm_e415": "Не удалось воспроизвести аудио; ошибка 415: Сбой преобразования файла; проверьте логи сервера.", //m
"mm_e415": "Не удалось воспроизвести аудио; ошибка 415: Сбой транскодирования файла; проверьте логи сервера.",
"mm_e500": "Не удалось воспроизвести аудио; ошибка 500: Проверьте логи сервера.",
"mm_e5xx": "Не удалось воспроизвести аудио; ошибка сервера ",
"mm_nof": "больше аудио-файлов не найдено",
@@ -365,7 +366,7 @@ Ls.rus = {
"f_anota": "только {0} из {1} файлов было выделено;\nчтобы выделить всё папку, отмотайте до низа",
"f_dls": 'ссылки на файлы в данной папке были\nзаменены ссылками на скачивание',
"f_dl_nd": 'пропуск папки (используйте загрузку zip/tar вместо этого):\n', //m
"f_dl_nd": 'пропуск папки (для папок используйте загрузку в виде архива):\n',
"f_partial": "Чтобы безопасно скачать файл, который в текущий момент загружается, нажмите на файл с таким же названием, но без расширения <code>.PARTIAL</code>. Пожалуйста, нажмите Отмена или ESC, чтобы сделать это.\n\nПри нажатии OK / Enter, вы скачаете этот временный файл, который с огромной вероятностью содержит лишь неполные данные.",
@@ -434,16 +435,16 @@ Ls.rus = {
"fcc_warn": 'скопировано {0} файлов в буфер\n\nно только <b>эта</b> вкладка браузера может их вставить\n(поскольку выделение оказалось настолько огромным)',
"fp_apply": "использовать эти имена",
"fp_skip": "пропустить конфликты", //m
"fp_skip": "пропустить конфликты",
"fp_ecut": "сначала вырезать или скопировать только некоторые файлы / папки\n\nучтите: вы можете вырезать / вставлять файлы между вкладками",
"fp_ename": "{0} файлов невозможно перенести сюда, потому что их имена уже заняты. Введите имена ниже, чтобы продолжить, или оставьте поля пустыми (\"пропустить конфликты\"), чтобы пропустить:", //m
"fcp_ename": "{0} файлов невозможно скопировать сюда, потому что их имена уже заняты. Введите имена ниже, чтобы продолжить, или оставьте поля пустыми (\"пропустить конфликты\"), чтобы пропустить:", //m
"fp_ename": "{0} файлов невозможно перенести сюда, потому что их имена уже заняты. Введите имена ниже, чтобы продолжить, или оставьте поля пустыми (\"пропустить конфликты\"), чтобы пропустить:",
"fcp_ename": "{0} файлов невозможно скопировать сюда, потому что их имена уже заняты. Введите имена ниже, чтобы продолжить, или оставьте поля пустыми (\"пропустить конфликты\"), чтобы пропустить:",
"fp_emore": "есть ещё коллизии имён, которые требуется исправить",
"fp_ok": "успешно перенесено",
"fcp_ok": "успешно скопировано",
"fp_busy": "перемещаю {0} файлов...\n\n{1}",
"fcp_busy": "копирую {0} файлов...\n\n{1}",
"fp_abrt": "прерывание...", //m
"fp_abrt": "отмена...",
"fp_err": "ошибка перемещения:\n",
"fcp_err": "ошибка копирования:\n",
"fp_confirm": "переместить эти {0} файлов сюда?",
@@ -455,9 +456,9 @@ Ls.rus = {
"fp_both_b": '<a href="#" id="modal-ok">Переместить</a><a href="#" id="modal-ng">Загрузить</a>',
"fcp_both_b": '<a href="#" id="modal-ok">Скопировать</a><a href="#" id="modal-ng">Загрузить</a>',
"mk_noname": "введите имя в текстовое поле слева перед тем, как это делать :p",
"nmd_i1": "вы также можете указать нужное расширение, например <code>.md</code>", //m
"nmd_i2": "вы можете создавать только файлы <code>.{0}</code>, так как у вас нет разрешения на удаление", //m
"mk_noname": "сначала введите имя в текстовое поле слева :p",
"nmd_i1": "вы также можете указать нужное расширение, например <code>.md</code>",
"nmd_i2": "вы можете создавать только файлы <code>.{0}</code>, так как у вас нет разрешения на удаление",
"tv_load": "Загружаю текстовый документ:\n\n{0}\n\n{1}% ({2} из {3} МиБ загружено)",
"tv_xe1": "не удалось загрузить текстовый файл:\n\nошибка ",
@@ -468,7 +469,7 @@ Ls.rus = {
"tvt_prev": "показать предыдущий документ$NГорячая клавиша: i\">⬆ пред",
"tvt_next": "показать следующий документ$NГорячая клавиша: K\">⬇ след",
"tvt_sel": "выбрать документ &nbsp; ( для вырезания / копирования / удаления / ... )$NГорячая клавиша: S\">выд",
"tvt_j": "приукрасить json$NГорячая клавиша: shift-J\">j", //m
"tvt_j": "форматировать json для чтения$NГорячая клавиша: shift-J\">j",
"tvt_edit": "открыть документ в текстовом редакторе$NГорячая клавиша: E\">✏️ изменить",
"tvt_tail": "проверять файл на изменения; показывать новые строки в реальном времени\">📡 обновлять",
"tvt_wrap": "перенос слов\">↵",
@@ -575,6 +576,7 @@ Ls.rus = {
"u_ancient": 'у вас действительно антикварный браузер -- возможно, стоит <a href="#" id="u2nah">использовать bup</a>',
"u_nowork": "требуется firefox 53+, chrome 57+ или iOS 11+",
"tail_2old": "требуется firefox 105+, chrome 71+ или iOS 14.5+",
"b2old": 'ваш браузер слишком старый', //m
"u_nodrop": 'ваш браузер слишком старый для загрузки через перетаскивание',
"u_notdir": "это не папка!\n\nваш браузер слишком старый,\nиспользуйте перетаскивание",
"u_uri": "чтобы перетащить картинку из других окон браузера,\nотпустите её на большую кнопку загрузки",
@@ -653,25 +655,26 @@ Ls.rus = {
"ur_um": "Завершено;\n{0} успешно,\n{1} ошибок, извините",
"ur_sm": "Завершено;\n{0} файлов найдено на сервере,\n{1} файлов НЕ найдено на сервере",
"rc_opn": "открыть", //m
"rc_ply": "воспроизвести", //m
"rc_pla": "воспроизвести как аудио", //m
"rc_txt": "открыть в просмотрщике файлов", //m
"rc_md": "открыть в текстовом редакторе", //m
"rc_dl": "скачать", //m
"rc_zip": "скачать как архив", //m
"rc_cpl": "копировать ссылку", //m
"rc_del": "удалить", //m
"rc_cut": "вырезать", //m
"rc_cpy": "копировать", //m
"rc_pst": "вставить", //m
"rc_rnm": "переименовать", //m
"rc_nfo": "новая папка", //m
"rc_nfi": "новый файл", //m
"rc_sal": "выбрать всё", //m
"rc_sin": "инвертировать выделение", //m
"rc_shf": "поделиться этой папкой", //m
"rc_shs": "поделиться выделенным", //m
"rc_opn": "открыть",
"rc_ply": "воспроизвести",
"rc_pla": "воспроизвести как аудио",
"rc_txt": "открыть в просмотрщике файлов",
"rc_md": "открыть в текстовом редакторе",
"rc_wopi": "открыть в офисном редакторе",
"rc_dl": "скачать",
"rc_zip": "скачать как архив",
"rc_cpl": "копировать ссылку",
"rc_del": "удалить",
"rc_cut": "вырезать",
"rc_cpy": "копировать",
"rc_pst": "вставить",
"rc_rnm": "переименовать",
"rc_nfo": "новая папка",
"rc_nfi": "новый файл",
"rc_sal": "выбрать всё",
"rc_sin": "инвертировать выделение",
"rc_shf": "поделиться этой папкой",
"rc_shs": "поделиться выделенным",
"lang_set": "перезагрузить страницу, чтобы применить изменения?",
@@ -691,11 +694,11 @@ Ls.rus = {
"j1": "включённый k304 будет отключать вас при получении HTTP 304, что может помочь при работе с некоторыми глючными прокси (перестают загружаться страницы), <em>но</em> это также сделает работу клиента медленнее",
"k1": "сбросить локальные настройки",
"l1": "авторизуйтесь для других опций:",
"ls3": "войти", //m
"lu4": "имя пользователя", //m
"lp4": "пароль", //m
"lo3": "выйти из “{0}” везде", //m
"lo2": "это завершит сеанс во всех браузерах", //m
"ls3": "войти",
"lu4": "имя пользователя",
"lp4": "пароль",
"lo3": "выйти из аккаунта “{0}” везде",
"lo2": "это завершит сеанс во всех браузерах",
"m1": "с возвращением,",
"n1": "404 не найдено &nbsp;┐( ´ -`)┌",
"o1": 'или у вас нет доступа -- попробуйте авторизоваться или <a href="' + SR + '/?h">вернуться на главную</a>',
@@ -714,8 +717,8 @@ Ls.rus = {
"ta1": "сначала введите свой новый пароль",
"ta2": "повторите новый пароль:",
"ta3": "опечатка; попробуйте снова",
"nop": "ОШИБКА: Пароль не может быть пустым", //m
"nou": "ОШИБКА: Имя пользователя и/или пароль не могут быть пустыми", //m
"nop": "ОШИБКА: Пароль не может быть пустым",
"nou": "ОШИБКА: Имя пользователя и/или пароль не могут быть пустыми",
"aa1": "входящие файлы:",
"ab1": "отключить no304",
"ac1": "включить no304",
+3
View File
@@ -39,6 +39,7 @@ Ls.spa = {
["ctrl-V", "pegar (mover/copiar) aquí"],
["Y", "descargar seleccionados"],
["F2", "renombrar seleccionados"],
["F4", "actualizar"], //m
"selección en lista de archivos",
["space", "alternar selección de archivo"],
@@ -574,6 +575,7 @@ Ls.spa = {
"u_ancient": "tu navegador es impresionantemente antiguo -- quizás deberías <a href=\"#\" id=\"u2nah\">usar bup en su lugar</a>",
"u_nowork": "se necesita firefox 53+ o chrome 57+ o iOS 11+",
"tail_2old": "se necesita firefox 105+ o chrome 71+ o iOS 14.5+",
"b2old": 'tu navegador es demasiado antiguo', //m
"u_nodrop": "tu navegador es demasiado antiguo para subir arrastrando y soltando",
"u_notdir": "¡eso no es una carpeta!\n\ntu navegador es demasiado antiguo,\npor favor intenta arrastrar y soltar en su lugar",
"u_uri": "para arrastrar y soltar imágenes desde otras ventanas del navegador,\npor favor suéltalas sobre el gran botón de subida",
@@ -657,6 +659,7 @@ Ls.spa = {
"rc_pla": "reproducir como audio", //m
"rc_txt": "abrir en el visor de archivos", //m
"rc_md": "abrir en el editor de texto", //m
"rc_wopi": "abrir en el editor de office", //m
"rc_dl": "descargar", //m
"rc_zip": "descargar como archivo", //m
"rc_cpl": "copiar enlace", //m
+3
View File
@@ -39,6 +39,7 @@ Ls.swe = {
["ctrl-V", "klistra in (kopiera/flytta) hit"],
["Y", "ladda ner urval"],
["F2", "byt namn på urval"],
["F4", "uppdatera"], //m
"välja filer",
["Blanksteg", "växla val av fil"],
@@ -575,6 +576,7 @@ Ls.swe = {
"u_ancient": 'din webbläsare är imponerande uråldrig -- du kanske borde <a href="#" id="u2nah">använda bup istället</a>',
"u_nowork": "firefox 53+ eller chrome 57+ eller iOS 11+ krävs",
"tail_2old": "firefox 105+ eller chrome 71+ eller iOS 14.5+ krävs",
"b2old": 'din webbläsare är för gammal',
"u_nodrop": 'din webbläsare är för gammal för dra-och-släpp-uppladdning',
"u_notdir": "det där är ingen mapp!\n\ndin webbläsare är för gammal,\nprova dra-och-släpp istället",
"u_uri": "släpp bilder från andra webbläsarfönster på den stora\nuppladdningsknappen för att ladda upp dem",
@@ -658,6 +660,7 @@ Ls.swe = {
"rc_pla": "spela upp som ljud", //m
"rc_txt": "öppna i filvisare", //m
"rc_md": "öppna i textredigerare", //m
"rc_wopi": "öppna i office-redigerare", //m
"rc_dl": "Ladda ner", //m
"rc_zip": "Ladda ner som arkiv", //m
"rc_cpl": "kopiera länk", //m
+3
View File
@@ -39,6 +39,7 @@ Ls.tur = {
["ctrl-V", "buraya yapıştır (taşı/kopyala)"],
["Y", "seçileni indir"],
["F2", "seçileni yeniden adlandır"],
["F4", "yenile"], //m
"dosya yönetimi seçimleri",
["boşluk", "seçimi değiştir"],
@@ -575,6 +576,7 @@ Ls.tur = {
"u_ancient": 'tarayıcınız resmen fosilleşmiş -- belki de <a href="#" id="u2nah">bup kullanmalısınız</a>',
"u_nowork": "firefox 53+ veya chrome 57+ veya iOS 11+ gerekiyor",
"tail_2old": "firefox 105+ veya chrome 71+ veya iOS 14.5+ gerekiyor",
"b2old": 'tarayıcınız çok eski', //m
"u_nodrop": 'tarayıcınız sürükleyip bırakmak için çok eski',
"u_notdir": "bu bir klasör değil!\n\ntarayıcınız çok eski,\nlütfen bunun yerine sürükleyip bırakmayı deneyin",
"u_uri": "başka tarayıcı pencerelerinden sürükle-bırak ile yükleme yapmak için,\nlütfen büyük yükleme düğmesinin üstüne bırakın",
@@ -658,6 +660,7 @@ Ls.tur = {
"rc_pla": "ses olarak oynat", //m
"rc_txt": "dosya görüntüleyicide aç", //m
"rc_md": "metin düzenleyicide aç", //m
"rc_wopi": "office düzenleyicide aç", //m
"rc_dl": "i̇ndir", //m
"rc_zip": "arşiv olarak indir", //m
"rc_cpl": "bağlantıyı kopyala", //m
+3
View File
@@ -39,6 +39,7 @@ Ls.ukr = {
["ctrl-V", "вставити (перемістити/копіювати) сюди"],
["Y", "завантажити вибране"],
["F2", "перейменувати вибране"],
["F4", "оновити"], //m
"вибір файлів у списку",
["space", "перемкнути вибір файлу"],
@@ -575,6 +576,7 @@ Ls.ukr = {
"u_ancient": 'ваш браузер вражаюче старий -- можливо, вам слід <a href="#" id="u2nah">використовувати bup замість цього</a>',
"u_nowork": "потрібен firefox 53+ або chrome 57+ або iOS 11+",
"tail_2old": "потрібен firefox 105+ або chrome 71+ або iOS 14.5+",
"b2old": 'ваш браузер занадто старий', //m
"u_nodrop": 'ваш браузер занадто старий для перетягування завантажень',
"u_notdir": "це не папка!\n\nваш браузер занадто старий,\nбудь ласка, спробуйте перетягування замість цього",
"u_uri": "щоб перетягнути зображення з інших вікон браузера,\nбудь ласка, перетягніть його на велику кнопку завантаження",
@@ -658,6 +660,7 @@ Ls.ukr = {
"rc_pla": "відтворити як аудіо", //m
"rc_txt": "відкрити у переглядачі файлів", //m
"rc_md": "відкрити в текстовому редакторі", //m
"rc_wopi": "вiдкрити в офiсному редакторi",
"rc_dl": "завантажити", //m
"rc_zip": "завантажити як архів", //m
"rc_cpl": "копіювати посилання", //m
+3
View File
@@ -36,6 +36,7 @@ Ls.vie = {
["ctrl-V", "dán (di chuyển/sao chép) tại đây"],
["Y", "tải xuống mục đã chọn"],
["F2", "đổi tên mục đã chọn"],
["F4", "làm mới"], //m
"file-list-sel",
["space", "chuyển đổi chọn tệp"],
@@ -584,6 +585,7 @@ Ls.vie = {
"u_ancient": "trình duyệt của bạn quá cũ; bạn có thể <a href=\"#\" id=\"u2nah\">dùng bup</a> thay thế",
"u_nowork": "cần Firefox 53+, Chrome 57+ hoặc iOS 11+",
"tail_2old": "cần Firefox 105+, Chrome 71+ hoặc iOS 14.5+",
"b2old": 'trình duyệt của bạn quá cũ', //m
"u_nodrop": "trình duyệt của bạn quá cũ để dùng kéo thả khi tải lên",
"u_notdir": "đây không phải thư mục\n\ntrình duyệt của bạn quá cũ,\nvui lòng thử dùng dragdrop",
@@ -689,6 +691,7 @@ Ls.vie = {
"rc_pla": "phát dưới dạng âm thanh", //m
"rc_txt": "mở trong trình xem tệp", //m
"rc_md": "mở trong trình soạn thảo văn bản", //m
"rc_wopi": "mở trong trình soạn thảo office", //m
"rc_dl": "tải xuống", //m
"rc_zip": "tải xuống dưới dạng gói nén", //m
"rc_cpl": "sao chép liên kết", //m
+105 -23
View File
@@ -888,9 +888,8 @@ function up2k_init(subtle) {
setmsg(suggest_up2k, 'msg');
var u2szs = u2sz.split(','),
u2sz_min = parseInt(u2szs[0]),
u2sz_tgt = parseInt(u2szs[1]),
u2sz_max = parseInt(u2szs[2]);
u2sz_tgt = parseInt(u2szs[0]),
u2sz_max = parseInt(u2szs[1]);
var parallel_uploads = ebi('nthread').value = icfg_get('nthread', u2j),
stitch_tgt = ebi('u2szg').value = icfg_get('u2sz', u2sz_tgt),
@@ -980,6 +979,7 @@ function up2k_init(subtle) {
"u": 0,
"t": 0
},
"xhr": [],
"car": 0,
"nre": 0,
"slow_io": null,
@@ -998,6 +998,20 @@ function up2k_init(subtle) {
});
}
function unqueue_up(t) {
t.postlist = [];
var arr = st.todo.upload;
for (var a = arr.length - 1; a >= 0; a--)
if (arr[a].nfile == t.n)
arr.splice(a, 1);
}
function upq_has(q, t) {
for (var a = 0, aa = q.length; a < aa; a++)
if (q[a].nfile == t.n)
return true;
}
var pvis = new U2pvis("bz", '#u2cards', uc, st),
donut = new Donut(uc, st);
@@ -1900,6 +1914,32 @@ function up2k_init(subtle) {
}
}
for (var a = st.xhr.length - 1; a >= 0; a--) {
var xhr = st.xhr[a];
if (!xhr.status || !xhr.responseText)
continue;
if (xhr.is_js)
try {
JSON.parse(xhr.responseText);
}
catch (ex) {
continue;
}
xhr.tdeaf = xhr.tdeaf || now;
if (!st.deaf && now - xhr.tdeaf < 3000)
continue;
if (!st.deaf)
console.log('xhr: bad apple');
xhr.onerror = xhr.ontimeout = null;
xhr.onload();
xhr.abort();
st.deaf = true;
}
if (st.bytes.inflight && (st.bytes.inflight < 0 || !st.busy.upload.length)) {
console.log('insane inflight ' + st.bytes.inflight);
st.bytes.inflight = 0;
@@ -2452,16 +2492,18 @@ function up2k_init(subtle) {
st.busy.head.push(t);
var xhr = new XMLHttpRequest();
var xhr = new XHR();
xhr.onerror = xhr.ontimeout = function () {
console.log('head onerror, retrying', t.name, t);
if (!toast.visible)
toast.warn(9.98, L.u_enethd + "\n\nfile: " + esc(t.name), t);
apop(st.xhr, xhr);
apop(st.busy.head, t);
st.todo.head.unshift(t);
};
function orz(e) {
apop(st.xhr, xhr);
if (t.done)
return console.log('done; skip head2', t.name, t);
@@ -2500,6 +2542,7 @@ function up2k_init(subtle) {
xhr.open('HEAD', t.purl + uricom_enc(t.name), true);
xhr.timeout = 34000;
xhr.send();
st.xhr.push(xhr);
}
/////
@@ -2512,6 +2555,7 @@ function up2k_init(subtle) {
keepalive = t.keepalive,
me = Date.now();
while (apop(st.todo.handshake, t));
if (t.done)
return console.log('done; skip hs', t.name, t);
@@ -2525,8 +2569,9 @@ function up2k_init(subtle) {
if (!t.srch && !t.t_handshake)
pvis.seth(t.n, 2, L.u_hs);
var xhr = new XMLHttpRequest();
var xhr = new XHR();
xhr.onerror = xhr.ontimeout = function () {
apop(st.xhr, xhr);
if (t.t_busied != me) // t.done ok
return console.log('zombie handshake onerror', t.name, t);
@@ -2539,6 +2584,7 @@ function up2k_init(subtle) {
t.keepalive = keepalive;
};
var orz = function (e) {
apop(st.xhr, xhr);
if (t.t_busied != me || t.done)
return console.log('zombie handshake onload', t.name, t);
@@ -2630,7 +2676,7 @@ function up2k_init(subtle) {
cbd.push(a == cdr_idx ? cdr_sz : chunksize);
}
t.postlist = [];
unqueue_up(t);
t.wark = response.wark;
var missing = response.hash;
for (var a = 0; a < missing.length; a++) {
@@ -2668,7 +2714,7 @@ function up2k_init(subtle) {
ccs += stp; stp *= 2;
}
ocs = Math.floor(ocs / 1024 / 1024);
t.stitch_sz = Math.min(ocs, stitch_tgt);
t.stitch_sz = Math.min(ocs, stitch_tgt || 1);
}
for (var a = 0; a < t.postlist.length; a++) {
@@ -2824,9 +2870,11 @@ function up2k_init(subtle) {
xhr.open('POST', t.purl, true);
xhr.responseType = 'text';
xhr.is_js = true;
xhr.timeout = 42000 + (t.srch || t.t_uploaded ? 0 :
(t.size / (1048 * 20))); // safededup 20M/s hdd
xhr.send(JSON.stringify(req));
st.xhr.push(xhr);
}
/////
@@ -2857,9 +2905,8 @@ function up2k_init(subtle) {
if (upf.sprs)
return true;
for (var a = 0, aa = st.busy.upload.length; a < aa; a++)
if (st.busy.upload[a].nfile == upt.nfile)
return false;
if (upq_has(st.busy.upload, upf))
return false;
return true;
}
@@ -2870,7 +2917,8 @@ function up2k_init(subtle) {
nparts = upt.nparts,
pcar = nparts[0],
pcdr = nparts[nparts.length - 1],
maxsz = (u2sz_max > 1 ? u2sz_max : 2040) * 1024 * 1024;
maxmsz = stitch_tgt == u2sz_tgt ? u2sz_max : stitch_tgt,
maxsz = (maxmsz > 0 ? maxmsz : 2040) * 1024 * 1024;
if (t.done)
return console.log('done; skip chunk', t.name, t);
@@ -2915,12 +2963,12 @@ function up2k_init(subtle) {
pcar == pcdr ? pcar : ('' + pcar + '~' + pcdr);
var orz = function (xhr) {
apop(st.xhr, xhr);
st.bytes.inflight -= xhr.bsent;
var txt = unpre((xhr.response && xhr.response.err) || xhr.responseText);
if (txt.indexOf('upload blocked by x') + 1) {
apop(st.busy.upload, upt);
for (var a = pcar; a <= pcdr; a++)
apop(t.postlist, a);
unqueue_up(t);
pvis.seth(t.n, 1, "ERROR");
pvis.seth(t.n, 2, txt.split(/\n/)[0]);
pvis.move(t.n, 'ng');
@@ -2943,8 +2991,13 @@ function up2k_init(subtle) {
st.etac.u++;
st.etac.t++;
}
else if (txt.indexOf('already got that') + 1 ||
txt.indexOf('already being written') + 1) {
else if (xhr.status == 413) {
modal.die('bad server config; see "u2sz" in readme');
}
else if (txt.indexOf('already got that') + 1) {
unqueue_up(t);
}
else if (txt.indexOf('already being written') + 1) {
t.nojoin = t.nojoin || t.postlist.length;
console.log("ignoring dupe-segment with backoff", t.nojoin, t.name, t);
if (!toast.visible && st.todo.upload.length < 4)
@@ -2952,23 +3005,30 @@ function up2k_init(subtle) {
}
else {
xhrchk(xhr, L.u_cuerr2.format(snpart, Math.ceil(t.size / chunksize), esc(t.name)), "404, target folder not found (???)", "warn", t);
if (txt.indexOf('unknown wark') == 0) {
t.cooldown = t.coolmul = 0;
unqueue_up(t);
}
chill(t);
}
orz2(xhr);
}
var orz2 = function (xhr) {
apop(st.xhr, xhr);
apop(st.busy.upload, upt);
for (var a = pcar; a <= pcdr; a++)
apop(t.postlist, a);
if (!t.postlist.length) {
if (!t.postlist.length && !upq_has(st.busy.upload, t)) {
t.t_uploaded = Date.now();
pvis.seth(t.n, 1, 'verifying');
st.todo.handshake.unshift(t);
if (!has(st.busy.handshake, t)) {
pvis.seth(t.n, 1, 'verifying');
st.todo.handshake.unshift(t);
}
}
tasker();
}
function do_send() {
var xhr = new XMLHttpRequest(),
var xhr = new XHR(),
bfin = Math.floor(st.bytes.finished / 1024 / 1024),
btot = Math.floor(st.bytes.total / 1024 / 1024);
@@ -2981,8 +3041,13 @@ function up2k_init(subtle) {
st.bytes.inflight += db;
xhr.bsent = nb;
var n = 64;
if (nb == xhr.bsize)
n = st.deaf ? 5 : 9;
if (!IE)
xhr.timeout = 64000 + Date.now() - xhr.t0;
xhr.timeout = n * 1000 + Date.now() - xhr.t0;
pvis.prog(t, pcar, nb);
};
xhr.onload = function (xev) {
@@ -2995,9 +3060,13 @@ function up2k_init(subtle) {
st.bytes.inflight -= (xhr.bsent || 0);
xhr.bsent = 0;
if (st.deaf)
t.cooldown = t.coolmul = 0;
if (!toast.visible)
toast.warn(9.98, L.u_cuerr.format(snpart, Math.ceil(t.size / chunksize), esc(t.name)), t);
unqueue_up(t); // maybe unknown wark (ff drops undrained rsp)
t.nojoin = t.nojoin || t.postlist.length; // maybe rproxy postsize limit
console.log('chunkpit onerror,', t.name, t);
orz2(xhr);
@@ -3028,11 +3097,21 @@ function up2k_init(subtle) {
if (xhr.overrideMimeType)
xhr.overrideMimeType('Content-Type', 'application/octet-stream');
if (st.deaf)
xhr.onreadystatechange = function (xev) {
if (xhr.status == 200) {
xhr.onreadystatechange = null;
xhr.onload(xev);
}
};
xhr.bsent = 0;
xhr.bsize = cdr - car;
xhr.t0 = Date.now();
xhr.timeout = 1000 * (IE ? 1234 : 42);
xhr.responseType = 'text';
xhr.send(t.fobj.slice(car, cdr));
st.xhr.push(xhr);
}
do_send();
}
@@ -3146,7 +3225,7 @@ function up2k_init(subtle) {
var el = ebi('u2szg'), n = parseInt(el.value);
stitch_tgt = n = (
isNaN(n) ? u2sz_tgt :
n < u2sz_min ? u2sz_min :
n < 0 ? 0 :
n > u2sz_max ? u2sz_max : n
);
if (n == u2sz_tgt) sdrop('u2sz'); else swrite('u2sz', n);
@@ -3340,7 +3419,7 @@ function up2k_init(subtle) {
flag = up2k_flagbus();
}
catch (ex) {
toast.err(5, "not supported on your browser:\n" + esc(basenames(ex)));
toast.err(5, L.b2old + ";\n" + esc(basenames(ex)));
bcfg_set('flag_en', uc.flag_en = false);
}
}
@@ -3383,7 +3462,7 @@ function up2k_init(subtle) {
function set_nosubtle(v) {
if (!WebAssembly)
return toast.err(10, L.u_nowork);
modal.confirm(L.lang_set, location.reload.bind(location), null);
modal.confirm(L.lang_set, relod, null);
}
function set_upnag(en) {
@@ -3454,6 +3533,9 @@ function up2k_init(subtle) {
function warn_uploader_busy(e) {
if (window.gtfo)
return;
e.preventDefault();
e.returnValue = '';
return "upload in progress, click abort and use the file-tree to navigate instead";
+26 -5
View File
@@ -46,6 +46,9 @@ var wah = '',
if (!window.WebAssembly || !WebAssembly.Memory)
window.WebAssembly = false;
if (!window.AbortController)
window.AbortController = false;
if (!window.Notification || !Notification.permission)
window.Notification = false;
@@ -108,6 +111,13 @@ function qsr(sel) {
}
function relod() {
crashed = 1;
window.gtfo = 1;
location.href = location.href;
}
// error handler for mobile devices
function esc(txt) {
return txt.replace(/[&"<>]/g, function (c) {
@@ -289,7 +299,7 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
var x = ebi('exh_wipecfg');
if (x) x.onclick = function () {
localStorage.clear();
location.reload();
relod();
};
x = ebi('exh_ignex'); if (x) x.onclick = ignex;
x = ebi('exh_ignexa'); if (x) x.onclick = ignexa;
@@ -443,8 +453,11 @@ function import_js(url, cb, ecb) {
var head = document.head || document.getElementsByTagName('head')[0];
var script = mknod('script');
script.type = 'text/javascript';
if (window.JS_NONCE)
if (window.JS_NONCE) {
script.nonce = JS_NONCE;
if (!AbortController)
script.setAttribute('nonce', JS_NONCE);
}
script.src = url + '?_=' + (window.TS || 'a');
script.onload = cb;
script.onerror = ecb || function () {
@@ -1199,8 +1212,10 @@ function has(haystack, needle) {
function apop(arr, v) {
var ofs = arr.indexOf(v);
if (ofs !== -1)
if (ofs !== -1) {
arr.splice(ofs, 1);
return true;
}
}
@@ -1466,7 +1481,7 @@ function Debounce(delay) {
};
r.rm = function (fun) {
apop(r.q, fun);
while (apop(r.q, fun));
};
r.run = function () {
@@ -1526,7 +1541,7 @@ var timer = (function () {
};
r.rm = function (fun) {
apop(r.q, fun);
while (apop(r.q, fun));
};
var doevents = function () {
@@ -2019,6 +2034,12 @@ var modal = (function () {
q.shift()();
}
r.die = function (html) {
crashed = 1;
if (r.busy)
r.hide();
_alert(lf2br(html), relod);
};
r.alert = function (html, cb, fun) {
q.push(function () {
_alert(lf2br(html), cb, fun);
+1 -1
View File
@@ -27,7 +27,7 @@ body {
<div style="display: none">
<form action="{{ url }}" enctype="multipart/form-data" method="post" target="w" id="submit-form">
<input name="access_token" value="{{ atoken }}" type="hidden" id="access-token"/>
<input name="access_token_ttl" value="{{ ttl }}000" type="hidden" id="access-token_ttl"/>
<input name="access_token_ttl" value="{{ ttl }}" type="hidden" id="access-token_ttl"/>
<input type="submit" value="" />
</form>
</div>
+153
View File
@@ -1,3 +1,156 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-0906-2112 `v1.20.22` hello fedora
## 🧪 new features
* iPhone: new bug in iOS breaks uploading; add workaround 3ea671ff
* apple broke XHR/fetch in a recent iOS version by introducing some wtf race-conditions in response handling; under high network load, the browser simply forgets to tell js that there's a response, so we're basically flying blind
* this workaround makes iOS uploads 50x faster than before but still not perfect (impossible given the situation); will be stuttery until apple fixes iOS
* apple will probably fix it very soon given the severity of the bug, but at least one copyparty user is forever-stuck on iOS-18.x which will never be fixed by apple, so a workaround is justified
* #1617 new plugin to thumbnail office documents with collabora (thx @kamaeff!) d4a1ce48
* new hook: [phonecam-sorter.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/phonecam-sorter.py) to automate organizing of pics/vids synced from phone to nas 9de09026
* dirkeys: allow non-recursive download-as-zip with just `dk` 3e3401f4
* `--no-mime` / volflag `nomime` disables `?mime=` for specifying custom response mimetype 370a05ab
* btrfs-specific: nocow .hist to improve sqlite performance dd9cf645
## 🩹 bugfixes
* two low-severity vulns in different components, but surprisingly similar synopses:
* GHSA-mc69-pxc8-4xf4 dirkeys (volflag `dk`) did not prevent descending into subdirs if an attacker could guess the name of the subdir 55969b8e
* GHSA-3fhv-rhjw-7hrg sftp did not fully enforce volflags xvol/xdev; an attacker could read a file inside the symlink destination if they could guess the name inside b80a210e
* not important enough to be listed in "recent important news", but will be detected by the (default-disabled) [version-checker](https://github.com/9001/copyparty/#version-checker)
* #1628 fix http206 range-request for last-n-bytes bdf8b793
* #1610 autogrid didn't count jxl images (thx @sylfn!) 7796c2d0
* when running without `e2d`, a config-reload would block uploads 0767d956
* really old chrome versions (before ver.62) was only able to upload over https ab791921
## 🔧 other changes
* #1632 connect-page: adjust rclone commands to support long passwords d7d30f26
* packaging: fix jank in source tarballs b854b8f5
* packaging: don't list licenses of unvendored modules 704af697
* #1631 systemd-examples: move config to `/etc/copyparty.conf` 185087ee
* reduce binary-garbage in logs from scrapers/scanners f00d19c5
* sfx: prefer `~/.cache/` (set `PRTY_XD=/tmp` to override) f870cf3c
* sfx: mention https://copyparty.eu/sfx-wtf/ in the header 80165120
* reduce complaining in log about default/unsafe tls-certs when not relevant 80cf7bfd
## 🌠 fun facts
* #887 copyparty has been packaged for Fedora 44! And EPEL-10 is on the way too... Thx @supakeen o/
* [verified at RevSpace NL](https://a.ocv.me/pub/g/2026/09/20260901_151949.jpg?cache)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-0817-2157 `v1.20.21` thumbex
## 🧪 new features
* #1602 custom thumbnail extractors; [docs/example](https://github.com/9001/copyparty/tree/hovudstraum/bin/thumbs) (thx @kamaeff!) 18648050 dbc6df81
* #1604 [u2c](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy): password can be provided in env-var `U2C_PW` (thx @shermanhlc!) 47297475
* wopi: option [--wopi-accs](https://copyparty.eu/cli/#g-wopi-accs) to limit who's able to use the feature 78a4ee49
* also fixes wopi on servers where user does not have read/write-access to root volume
* also restricts the token to just that one file; good if the wopi-client is some cloud thing that shouldn't be trusted
* #1591 wopi: use persistent file-ID which is necessary for real-time collab (thx @kamaeff!) efcf96e3
* #1605 the lightbox can show svg images now 825f1c94
## 🩹 bugfixes
* up2k: client could waste a little bandwidth while recovering from a network glitch 18791c57
* if `PRTY_CONFIG` is set to a config-file that is also autodetected, then explain the misconfiguration instead of crashing like before 14e2d79b
* wopi: fix session-timeout hint to clients (thx @kamaeff!) 92c3f32e
* js: fix chance of duplicate prologue on very first page visit eeb399e3
* js: fix panic on image dragdrop out of the browser window dcc0abd6
## 🔧 other changes
* up2k: client now detects when server or reverseproxy is incorrectly configured with an impractically small request-body-size-limit, crashing the website with [an explanation](https://github.com/9001/copyparty/#u2sz) how to fix it f19ef033
* also allows setting the chunksize all the way down to 1 megabyte when absolutely necessary (bad idea, slow)
* new option [--allow-svg-js](https://copyparty.eu/cli/#g-allow-svg-js) if you really want that 566de65f
* shares: harden single-file shares some more 21c2c728 9ff6a71a
* just removing footguns (motivated by a bug-report that was a false-positive)
* copyparty.exe: upgrade to python 3.14.7 from 3.13.14 8b6e8972
* larger and slightly faster (compensated for the size bloat by making the text-image-generator more shitty)
## 🌠 fun facts
* the [thumbex example](https://github.com/9001/copyparty/blob/hovudstraum/bin/thumbs/randomcolor.py) is also a cool example how relevant the "pseudo" in PRNG can be; with `random.randrange` instead of `os.urandom`, [first run](https://a.ocv.me/pub/g/2026/08/Screenshot_2026-08-15_19-56-35.png?cache) followed by restarting copyparty and [another run](https://a.ocv.me/pub/g/2026/08/Screenshot_2026-08-15_19-56-38.png?cache)...heh
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-0803-2232 `v1.20.20` more wopi
## 🧪 new features
* #1574 #1585 wopi: probably support onlyoffice as wopi-client (thx @kamaeff!) fdf52a8c
* #1580 wopi: `--wopi-urls` to choose a different wopi-client url based on current domain (thx @kamaeff!) 24a4090f
* #634 #1033 #1390 correct size of volumes in directory listings (thx @vmattphillips!) 5c157be9
* #1577 thumbnails for krita `.kra` and openraster `.ora` images (thx @Wuerfel21!) 9f5f9d88
* hotkey F4 to reload/refresh the directory listing 2c6497b6
## 🩹 bugfixes
* #1581 ctrl-a in search results 28801172
## 🔧 other changes
* show an explanation in the web-UI if javascript is broken due to misconfigured hosting-stack (the ["csp nonce"](https://github.com/9001/copyparty/#csp-nonce) stuff) 6f924829
* improve upload performance when running behind a buggy or bufferbloating reverseproxy c0c638c9
* #1582 Nixos: fix version checker (thx @sylfn!) 63c61b67
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-0727-2202 `v1.20.19` SECURITY: fix FTP upload outside volume
## ⚠️ ATTN: this release fixes a vulnerability in FTP and FTPS (not SFTP)
the FTP-server (default-disabled) would allow uploading to any folder that the copyparty process had permission to write to, but with certain limitations; see [GHSA-phv8-wgjp-g4p9](https://github.com/9001/copyparty/security/advisories/GHSA-phv8-wgjp-g4p9)
## recent important news
* [v1.20.19 (2026-07-27)](https://github.com/9001/copyparty/releases/tag/v1.20.19) fixed an FTP-server vuln (upload outside defined volumes)
* [v1.20.17 (2026-07-06)](https://github.com/9001/copyparty/releases/tag/v1.20.17) fixed a vuln when a volume has both filekeys and dirkeys enabled
* [v1.20.17 (2026-07-06)](https://github.com/9001/copyparty/releases/tag/v1.20.17) introduced csp nonces, possibly breaking some javascript-based plugins
## 🧪 new features
* #1495 wopi integration -- edit office documents in the web-ui (thx @brandon-doornbos!) d57bb0c7 10db4236
* currently only works with collabora online as wopi client #1574 and there's no docs/examples #1575
* thumbnails: use embedded cover-image in videos when available 59524018
* thumbnails: folder-thumbs can be disabled with `th-covers: no` (volflag or global) 856fada3
* #1555 macos: add [--srch-nfkc](https://copyparty.eu/cli/#g-srch-nfkc) to fix searching for filenames / paths in CJK languages a7c99094
* reduces search performance to around 30% when enabled
* hooks: `xbr` / `xar` did not include old/new abspath as parameters; now they do c122e103
## 🩹 bugfixes
* ftp: fix [GHSA-phv8-wgjp-g4p9](https://github.com/9001/copyparty/security/advisories/GHSA-phv8-wgjp-g4p9) b331bb1c
* #1563 moving files between volumes could fail depending on OS and underlying filesystem c70dc7ac
* fix drag-drop uploading in certain glitchy KDE environments (thx @tilse!) daf144af
* hooks: `xiu` crashed if the fork-flag was set (thx @stackxp!) aa862353
* hooks: `xau` without json-flag would be given the wark (file hash) instead of the abspath bae77b90
* shares: fix markdown-viewer (`?v`) inside shares 6a9437b7
* ftp: fix logging from `xbu` hooks 9912a951
* fix slow boot if a volume had lots of files in its toplevel folder cdb474c8
* python2.7: fix multithreaded file-hashing 0f2040c3
## 🔧 other changes
* #1556 the libvips thumbnailer was demoted to last-fallback due to frequently using excessive amounts of ram ed0be42a
* if [-lo](https://copyparty.eu/cli/#g-lo) points to an existing file, it will now be appended to instead of overwritten depending on [--rlo](https://copyparty.eu/cli/#g-help-rlo) b6abc33f
* #1530 nixos: the nix package now uses `ffmpeg-headless` instead of `ffmpeg-full` (thx @nyakase!) fface524
* slightly longer session cookie (was 20, now 24 chars) 537a99df
* Windows-specific:
* add a warning regarding the risks of DLL-hijacking when relevant bb40804f
* fix some trivial PATH-related footguns cea97ac6
* faster creation of sparse files bc45299b
* fix detection of filesystem characteristics 6226858b
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-0709-2254 `v1.20.18` bwrap off
+54
View File
@@ -0,0 +1,54 @@
drop tcp mid-chunk and force browser to re-hs before it can be retried
ok with subchunks as of writing; 6thr, u2sz=1
303M file: --s-wr-slp 0.02 --iobuf 131072
33.5M file: --s-wr-slp 0.1 --iobuf 131072
--- a/copyparty/httpcli.py
+++ b/copyparty/httpcli.py
@@ -234,2 +234,3 @@ ZIP_XCODE_L = _build_zip_xcode()
ZIP_XCODE_S = set(ZIP_XCODE_L)
+hcblk = ""
@@ -3154,2 +3155,3 @@ class HttpCli(object):
def handle_post_json(self) -> bool:
+ global hcblk
try:
@@ -3248,2 +3250,3 @@ class HttpCli(object):
# not to protect u2fh, but to prevent handshakes while files are closing
+ hcblk = ""
with self.u2mutex:
@@ -3335,2 +3338,3 @@ class HttpCli(object):
def handle_post_binary(self) -> bool:
+ global hcblk
try:
@@ -3452,2 +3456,4 @@ class HttpCli(object):
)
+ if chash == hcblk:
+ raise Exception("fhdsafhad re")
post_sz, _, sha_b64 = hashcopy(
@@ -3513,3 +3519,6 @@ class HttpCli(object):
self.u2fh.put(path, f)
- except:
+ except Exception as ex:
+ if "fhdsafhad" in str(ex):
+ hcblk = chashes[0]
+ self.sr.s.close()
# maybe busted handle (eg. disk went full)
--- a/copyparty/util.py
+++ b/copyparty/util.py
@@ -3414,2 +3414,3 @@ def eol_conv(
+hcn = 0
def hashcopy(
@@ -3421,2 +3422,4 @@ def hashcopy(
) -> tuple[int, str, str]:
+ global hcn
+ print(hcn)
if not hashobj:
@@ -3429,2 +3432,5 @@ def hashcopy(
+ hcn += 1
+ if hcn in (24, 121, 300, 600, 900):
+ raise Exception("fhdsafhad " + str(tlen))
hashobj.update(buf)
+1
View File
@@ -97,6 +97,7 @@ copyparty = [
"web/*.css",
"web/*.html",
"web/*.xml",
"web/*.gif",
"web/tl/*.js",
"web/tl/*.gz",
"web/a/*.txt",
-173
View File
@@ -1,173 +0,0 @@
#!/bin/bash
repacker=1
set -e
# -- download latest copyparty (source.tgz and sfx),
# -- build minimal sfx versions,
# -- create a .tar.gz bundle
#
# convenient for deploying updates to inconvenient locations
# (and those are usually linux so bash is good inaff)
# (but that said this even has macos support)
#
# output summary (filesizes and contents):
#
# 550760 copyparty-extras/sfx-full/copyparty-sfx.py
# `- original unmodified sfx from github
#
# 572923 copyparty-extras/sfx-full/copyparty-sfx-gz.py
# `- unmodified but recompressed from bzip2 to gzip
#
# 353975 copyparty-extras/sfx-ent/copyparty-sfx.py
# 376934 copyparty-extras/sfx-ent/copyparty-sfx-gz.py
# `- removed iOS ogg/opus/vorbis audio decoder,
# removed the audio tray mouse cursor,
# "enterprise edition"
#
# 270004 copyparty-extras/sfx-lite/copyparty-sfx.py
# 293159 copyparty-extras/sfx-lite/copyparty-sfx-gz.py
# `- also removed the codemirror markdown editor
# and the text-viewer syntax hilighting,
# only essential features remaining
#
# 646297 copyparty-extras/copyparty-1.0.14.tar.gz
# 4823 copyparty-extras/copyparty-repack.sh
# `- source files from github
#
# 23663 copyparty-extras/u2c.py
# `- standalone utility to upload or search for files
#
# 32280 copyparty-extras/partyfuse.py
# `- standalone to mount a URL as a local read-only filesystem
#
# 270004 copyparty
# `- minimal binary, same as sfx-lite/copyparty-sfx.py
command -v gnutar && tar() { gnutar "$@"; }
command -v gtar && tar() { gtar "$@"; }
command -v gsed && sed() { gsed "$@"; }
td="$(mktemp -d)"
od="$(pwd)"
cd "$td"
pwd
dl_text() {
command -v curl >/dev/null && exec curl "$@"
exec wget -O- "$@"
}
dl_files() {
command -v curl >/dev/null && exec curl -L --remote-name-all "$@"
exec wget "$@"
}
export -f dl_files
# if cache exists, use that instead of bothering github
cache="$od/.copyparty-repack.cache"
[ -e "$cache" ] &&
tar -xf "$cache" ||
{
# get download links from github
dl_text https://api.github.com/repos/9001/copyparty/releases/latest |
(
# prefer jq if available
jq -r '.assets[]|select(.name|test("-sfx|tar.gz")).browser_download_url' ||
# fallback to awk (sorry)
awk -F\" '/"browser_download_url".*(\.tar\.gz|-sfx\.)/ {print$4}'
) |
grep -E '(sfx\.py|tar\.gz)$' |
tee /dev/stderr |
tr -d '\r' | tr '\n' '\0' |
xargs -0 bash -c 'dl_files "$@"' _
tar -czf "$cache" *
}
# move src into copyparty-extras/,
# move sfx into copyparty-extras/sfx-full/
mkdir -p copyparty-extras/sfx-{full,ent,lite}
mv copyparty-sfx.* copyparty-extras/sfx-full/
mv copyparty-*.tar.gz copyparty-extras/
# unpack the source code
( cd copyparty-extras/
tar -xf *.tar.gz
)
# use repacker from release if that is newer
p_other=copyparty-extras/copyparty-*/scripts/copyparty-repack.sh
other=$(awk -F= 'BEGIN{v=-1} NR<10&&/^repacker=/{v=$NF} END{print v}' <$p_other)
[ $repacker -lt $other ] &&
cat $p_other >"$od/$0" && cd "$od" && rm -rf "$td" && exec "$0" "$@"
# now drop the cache
rm -f "$cache"
# fix permissions
chmod 755 \
copyparty-extras/sfx-full/* \
copyparty-extras/copyparty-*/{scripts,bin}/*
# extract the sfx
( cd copyparty-extras/sfx-full/
./copyparty-sfx.py --version
)
repack() {
# do the repack
(cd copyparty-extras/copyparty-*/
./scripts/make-sfx.sh $2
)
# put new sfx into copyparty-extras/$name/,
( cd copyparty-extras/
mv copyparty-*/dist/* $1/
)
}
repack sfx-full "re gz"
repack sfx-ent "re"
repack sfx-ent "re gz"
repack sfx-lite "re no-cm no-hl"
repack sfx-lite "re no-cm no-hl gz"
# move fuse and up2k clients into copyparty-extras/,
# copy lite-sfx.py to ./copyparty,
# delete extracted source code
( cd copyparty-extras/
mv copyparty-*/bin/u2c.py .
mv copyparty-*/bin/partyfuse.py .
cp -pv sfx-lite/copyparty-sfx.py ../copyparty
rm -rf copyparty-{0..9}*.*.*{0..9}
)
# and include the repacker itself too
cp -av "$od/$0" copyparty-extras/ ||
cp -av "$0" copyparty-extras/ ||
true
# create the bundle
printf '\n\n'
fn=copyparty-$(date +%Y-%m%d-%H%M%S).tgz
tar -czvf "$od/$fn" *
cd "$od"
rm -rf "$td"
echo
echo "done, here's your bundle:"
ls -al "$fn"
+1 -1
View File
@@ -72,7 +72,7 @@ exec /usr/bin/python3 -m copyparty "$@"
EOF
# unpack sfx and dive in
python3 copyparty-sfx.py --version
PRTY_XD=/tmp python3 copyparty-sfx.py --version
cd /tmp/pe-copyparty.0
# steal the stuff we need
+1 -1
View File
@@ -29,7 +29,7 @@ done
getver() {
ver=$(
python3 ../../dist/copyparty-sfx.py --version 2>/dev/null |
PRTY_XD=/tmp python3 ../../dist/copyparty-sfx.py --version 2>/dev/null |
awk '/^copyparty v/{sub(/-.*/,"");sub(/v/,"");print$2;exit}'
)
echo $ver | grep -E '[0-9]\.[0-9]' || {
+1 -1
View File
@@ -15,7 +15,7 @@ command -v gsort >/dev/null && {
cd ../dist
kwds='-bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -urlform -exp -rlo -ls -dbd -chmod -pwhash -zm'
kwds='-bind -accounts -auth -auth-ord -flags -thumb-ex -handlers -hooks -idp -urlform -exp -rlo -ls -dbd -chmod -pwhash -zm'
export PRTY_FULL_HELP=1
export PRTY_NO_PARAMIKO=1
+1 -1
View File
@@ -74,7 +74,7 @@ def cnv(src):
continue
if '">uuid:' in ln:
ln = re.sub(r">uuid:[0-9a-f-]{36}<", ">autogenerated<", ln)
if "-salt SALT" in ln:
if "-salt " in ln and ("-salt SALT" in ln or "-salt TXT" in ln):
in_salt = 3
if in_salt:
in_salt -= 1
+2 -2
View File
@@ -10,8 +10,8 @@ command -v gsort >/dev/null && {
( ( HLPTXT=CAT && cat || xsel -ob ) | sed -r '
s`/home/ed/`~/`;
s/uuid:[0-9a-f-]{36}/autogenerated/;
s/(-salt SALT.*default: )[0-9a-zA-Z/+]{24}\)/\124-character-autogenerated)/;
s/(-salt SALT.*default: )[0-9a-zA-Z/+]{40}\)/\140-character-autogenerated)/;
s/(-salt (SALT|TXT).*default: )[0-9a-zA-Z/+]{24}\)/\124-character-autogenerated)/;
s/(-salt (SALT|TXT).*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/;
+2 -1
View File
@@ -193,6 +193,8 @@ tmpdir="$(
printf '%s\n' "$TMPDIR" /tmp |
awk '/./ {print; exit}'
)"
[ -e ~/.cache ] && tmpdir=~/.cache
[ "$PRTY_XD" ] && tmpdir="$PRTY_XD"
necho() {
printf '\033[G%s ... \033[K' "$*"
@@ -513,7 +515,6 @@ unhelpg() {
[ $no_hl ] &&
rm -rf copyparty/web/deps/prism*
rm -f copyparty/web/deps/orbitron.woff2 # todo:uiv15
[ $no_fnt ] && {
rm -f copyparty/web/deps/scp.woff2
f=copyparty/web/ui.css
+7 -1
View File
@@ -68,7 +68,7 @@ scripts/genlic.py "$rls_dir/copyparty/res/COPYING.txt"
cd "$rls_dir"
find -type d -exec chmod 755 '{}' \+
find -type f -exec chmod 644 '{}' \+
find -type f -exec chmod u+rw,go+r '{}' \+
commaver="$(
printf '%s\n' "$ver" |
@@ -99,6 +99,12 @@ rm -f \
cp -pv LICENSE LICENSE.txt
mv setup.py{,.disabled}
( cd copyparty/web
make -j$(nproc)
rm Makefile*
printf 'all:\n\t@echo "NOTE: no longer necessary to run copyparty/web/Makefile when building from source tarball"\n' >Makefile
)
# the regular cleanup memes
find -name '*.pyc' -delete
find -name __pycache__ -delete
+8 -1
View File
@@ -16,7 +16,7 @@ uname -s | grep WOW64 && m=64 || m=32
uname -s | grep NT-10 && w10=1 || w7=1
[ $w7 ] && [ -e up2k.sh ] && [ ! "$1" ] && ./up2k.sh
[ $w7 ] && pyv=37 || pyv=313
[ $w7 ] && pyv=37 || pyv=314
[ $w7 ] && sfx=en || sfx=sfx
esuf=
[ $w7 ] && [ $m = 32 ] && esuf=32
@@ -42,6 +42,7 @@ dl https://192.168.123.1:3923/cpp/scripts/pyinstaller/loader.rc
[ $st_en ] && [ $st_en -ge $st_sfx ] || sfx=sfx
}
export PRTY_XD=$TEMP
rm -rf $TEMP/pe-copyparty*
python copyparty-$sfx.py --version
@@ -67,6 +68,7 @@ rm -rf mods/magic/
sed -ri 's/\binspect\b/os/' $spkgs/jinja2/$f.py
done
}
af '/print\(lic/{s=0}!s;/lic = buf.deco/{s=1}' mods/copyparty/__main__.py
sed -ri /fork_process/d mods/pyftpdlib/servers.py
af '/^class _Base/{s=1}!s' mods/pyftpdlib/authorizers.py
@@ -100,9 +102,14 @@ excl=(
)
[ $w10 ] && excl+=(
_pyrepl
bz2 # 51k
compression.zstd # 195k
concurrent.interpreters # 12k
distutils
setuptools
ftplib # 23k
PIL._avif
PIL._imagingft # 861k (freetype)
PIL.ImageQt
PIL.ImageShow
PIL.ImageTk
+11 -11
View File
@@ -1,7 +1,7 @@
f117016b1e6a7d7e745db30d3e67f1acf7957c443a0dd301b6c5e10b8368f2aa4db6be9782d2d3f84beadd139bfeef4982e40f21ca5d9065cb794eeb0e473e82 altgraph-0.17.4-py2.py3-none-any.whl
76e42806c52319081201d9fd9101827d36d9dbdd34e04f8a48736f2e3b37d68e79e8f52e43260d3eaaa46fe149d95b54ef1355af24361d0bf190dced93cd77bb altgraph-0.17.5-py2.py3-none-any.whl
17ce52ba50692a9d964f57a23ac163fb74c77fdeb2ca988a6d439ae1fe91955ff43730c073af97a7b3223093ffea3479a996b9b50ee7fba0869247a56f74baa6 pefile-2023.2.7-py3-none-any.whl
b297ff66ec50cf5a1abcf07d6ac949644c5150ba094ffac974c5d27c81574c3e97ed814a47547f4b03a4c83ea0fb8f026433fca06a3f08e32742dc5c024f3d07 pywin32_ctypes-0.2.3-py3-none-any.whl
fd88d00c7231f9fcbe0fb9037217b6ae5dcb5b06a98be56243400542671a32f003d594b2e1852f77d316e9327ad4c643321e1f9f3ea20cc8500ac5bba8ae170e upx-5.0.2-win32.zip
76724b04d6fdfadb46f27b6fd5753429d597642d1035ff7bf52fe1b6f5bc18f129567b738a40436adf03afd3a38708dc268de3c1b5d4ef9a89968335805ead72 upx-5.2.0-win32.zip
# win7
d9d30ff960365307833425e340bf0a642fd54e35155f18ff1d9746bc1ecf8ecb864aad24948735e8fd994503b659dc8968e6ba337eeb4568217ebdf97dd292dd Git-2.46.2-32-bit.exe
3253e86471e6f9fa85bfdb7684cd2f964ed6e35c6a4db87f81cca157c049bef43e66dfcae1e037b2fb904567b1e028aaeefe8983ba3255105df787406d2aa71e en_windows_7_professional_with_sp1_x86_dvd_u_677056.iso
@@ -21,15 +21,15 @@ ea73aa54cc6d5db20dfb127e54562dabf890e4cd6171a91b10a51af2bcfc76e1d64cbdce4546df2d
479a63e14586ab2f2228208116fc149ed8ee7b1e4ff360754f5bda4bf765c61af2e04b5ef123976623d04df4976b7886e0445647269da81436bd0a7b5671d361 windows6.1-kb2533623-x86.msu
ac96786e5d35882e0c5b724794329c9125c2b86ae7847f17acfc49f0d294312c6afc1c3f248655de3f0ccb4ca426d7957d02ba702f4a15e9fcd7e2c314e72c19 zipp-3.15.0-py3-none-any.whl
# win10
f4b4e330995ebe96c0bd06e16e5b26062ece9473f06d369775aa68eab261dedcf32dfdd159acaa227502bbf9fa2cd8bbe57cddb89fc6f2196fef7a9ed5a80ae9 Git-2.51.0-64-bit.exe
ef723f34ce8c0c1f8ef9eb7746f77e4ab6129aa7c7dcf501bee56f522ff2c57ff013f9a69db141ad04c9d1b90b4f1bbd22b6028218e3ae68eb4efec861d63f65 Git-2.55.0.3-64-bit.exe
0a2cd4cadf0395f0374974cd2bc2407e5cc65c111275acdffb6ecc5a2026eee9e1bb3da528b35c7f0ff4b64563a74857d5c2149051e281cc09ebd0d1968be9aa en-us_windows_10_enterprise_ltsc_2021_x64_dvd_d289cf96.iso
4f13070cc9241fa342deab4ebfac360565030580ff77b6e5f1951a64627621e5da4abfd30e1e46ca8bae2bb7dd4ff98141aff424142c9629a5876a61283962e5 virtio-win-0.1.285.iso
9a7f40edc6f9209a2acd23793f3cbd6213c94f36064048cb8bf6eb04f1bdb2c2fe991cb09f77fe8b13e5cd85c618ef23573e79813b2fef899ab2f290cd129779 jinja2-3.1.6-py3-none-any.whl
00731cfdd9d5c12efef04a7161c90c1e5ed1dc4677aa88a1d4054aff836f3430df4da5262ed4289c21637358a9e10e5df16f76743cbf5a29bb3a44b146c19cf3 MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl
8a6e2b13a2ec4ef914a5d62aad3db6464d45e525a82e07f6051ed10474eae959069e165dba011aefb8207cdfd55391d73d6f06362c7eb247b08763106709526e mutagen-1.47.0-py3-none-any.whl
a726fb46cce24f781fc8b55a3e6dea0a884ebc3b2b400ea74aa02333699f4955a5dc1e2ec5927ac72f35a624401f3f3b442882ba1cc4cadaf9c88558b5b8bdae packaging-25.0-py3-none-any.whl
efcb9da8e6f50cb5ea8e1d41e631e89b6f852b68a2568619a6ec151267e7fac94aba1e36317341c4fc5df822215b7ab9dd6b33cfb738e1b8423d4f0f6cff9525 pillow-12.3.0-cp313-cp313-win_amd64.whl
b9b98714dfca6fa80b0b3f222965724d63be9c54d19435d1fe768e07016913d6db8d6e043fcb185b55a9bd6fe370a80cf961814fc096046a5f4640d99ed575ef pyinstaller-6.15.0-py3-none-win_amd64.whl
cad0f7cf39de691813b1d4abc7d33f8bda99a87d9c5886039b814752e8690364150da26fb61b3e28d5698ff57a90e6dcd619ed2b64b04f72b5aadb75e201bdb0 pyinstaller_hooks_contrib-2025.8-py3-none-any.whl
d0d23fd4ae8900764df664803844155994b253ed449b9ed54ea020fbfa7aaea8800ca1928e4a7f26b9278613f8bce27ef9a424cfcc4ca15551c7e4e1b78f3991 python-3.13.14-amd64.exe
2a0420f7faaa33d2132b82895a8282688030e939db0225ad8abb95a47bdb87b45318f10985fc3cee271a9121441c1526caa363d7f2e4a4b18b1a674068766e87 setuptools-80.9.0-py3-none-any.whl
52adad30ac608695a6345468144048e5134696313ab775ef72f41ca082f8fa19953e2b9924fb5970f829bf9188183fe616b3942c4705eeddfe4ce223a5940186 markupsafe-3.0.3-cp314-cp314-win_amd64.whl
c7668214758cb79a6ba0f7dbffb4b1b6f7cbf98d8ab6cbe77ab42c891916de9339bf17e3de3c3f2d20f6065b49f97c6d521419cc0a2b6c6b8940cf4317172d71 mutagen-1.48.1-py3-none-any.whl
e9492eedaac7cfddde6d84389ef7df86c68275e8f29d838fc431d9bf379401bf6d82ec70ac02461995982613c80592cd8de74c16a3654d7d8fdc06f96057d6e7 packaging-26.3-py3-none-any.whl
8ea9946d8484a6de833a97d3cd545c2b67475207944856281cffb2c513ab14bf50e2b9d31d7cca8f50a6ca8b6711fb8e45cbadabd566cab28f7d58ee058dc650 pillow-12.3.0-cp314-cp314-win_amd64.whl
6f7ed5dc47ade9ad56f6f0499bc5d8eb485bd454d47189133297fa5aa633cf70dad18656cd673318c664f96097c39003d94d9c9703c8337d62840b4564695a04 pyinstaller-6.21.0-py3-none-win_amd64.whl
0a7c1ae7063a2ee46a1c5c057fe73838d7dc5c59800e56037b3fb9dbd1c363915375f989160e44f21436e1071713682bb14473aa8112128beb670f5b110dc8e2 pyinstaller_hooks_contrib-2026.6-py3-none-any.whl
a4adbefaa216571ade7fda561ed3404c762ff03029696ce1745fd4e23b066cb49b1fef99792ce3a594a04f9260a2f05a9e2f851bcc83ed4404f00ce05709c56a python-3.14.7-amd64.exe
bfda5996cdf908edf971df38421ed154e2f2c58f38731ad3be22a88e78c933c64efd35fbc6daf82845dc0b59b5211e925038b430749b2c851887096578c68219 setuptools-83.0.0-py3-none-any.whl
Binary file not shown.

Before

Width:  |  Height:  |  Size: 554 B

After

Width:  |  Height:  |  Size: 555 B

+12 -12
View File
@@ -29,21 +29,21 @@ uname -s | grep NT-10 && w10=1 || {
w7=1; uname -s | grep WOW64 && w7x64=1 || w7x32=1
}
fns=(
altgraph-0.17.4-py2.py3-none-any.whl
altgraph-0.17.5-py2.py3-none-any.whl
pefile-2023.2.7-py3-none-any.whl
pywin32_ctypes-0.2.3-py3-none-any.whl
upx-5.0.2-win32.zip
upx-5.2.0-win32.zip
)
[ $w10 ] && fns+=(
jinja2-3.1.6-py3-none-any.whl
MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl
mutagen-1.47.0-py3-none-any.whl
packaging-25.0-py3-none-any.whl
pillow-12.3.0-cp313-cp313-win_amd64.whl
pyinstaller-6.15.0-py3-none-win_amd64.whl
pyinstaller_hooks_contrib-2025.8-py3-none-any.whl
python-3.13.14-amd64.exe
setuptools-80.9.0-py3-none-any.whl
markupsafe-3.0.3-cp314-cp314-win_amd64.whl
mutagen-1.48.1-py3-none-any.whl
packaging-26.3-py3-none-any.whl
pillow-12.3.0-cp314-cp314-win_amd64.whl
pyinstaller-6.21.0-py3-none-win_amd64.whl
pyinstaller_hooks_contrib-2026.6-py3-none-any.whl
python-3.14.7-amd64.exe
setuptools-83.0.0-py3-none-any.whl
)
[ $w7 ] && fns+=(
future-1.0.0-py3-none-any.whl
@@ -83,7 +83,7 @@ close and reopen git-bash so python is in PATH
===[ copy-paste into git-bash ]================================
uname -s | grep NT-10 && w10=1 || w7=1
[ $w7 ] && pyv=37 || pyv=313
[ $w7 ] && pyv=37 || pyv=314
appd=$(cygpath.exe "$APPDATA")
cd ~/Downloads &&
yes | unzip upx-*-win32.zip &&
@@ -91,7 +91,7 @@ mv upx-*/upx.exe . &&
python -m ensurepip &&
{ [ $w10 ] || python -m pip install --user -U pip-*.whl; } &&
python -m pip install --user -U packaging-*.whl &&
{ [ $w7 ] || python -m pip install --user -U {setuptools,mutagen,pillow,jinja2,MarkupSafe}-*.whl; } &&
{ [ $w7 ] || python -m pip install --user -U {setuptools,mutagen,pillow,jinja2,markupsafe}-*.whl; } &&
{ [ $w10 ] || python -m pip install --user -U future-*.whl importlib_metadata-*.whl typing_extensions-*.whl zipp-*.whl; } &&
python -m pip install --user -U pyinstaller-*.whl pefile-*.whl pywin32_ctypes-*.whl pyinstaller_hooks_contrib-*.whl altgraph-*.whl &&
sed -ri 's/--lzma/--best/' $appd/Python/Python$pyv/site-packages/pyinstaller/building/utils.py &&
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

+2
View File
@@ -19,6 +19,8 @@ v=$1; shift
[ "$v" = sfx ] &&
rls= || rls=1
export PRTY_XD=/tmp
[ $rls ] && {
printf '%s\n' "$v" | grep -qE '^[0-9\.]+$' || exit 1
grep -E "(${v//./, })" ../copyparty/__version__.py || exit 1
+2
View File
@@ -1,6 +1,8 @@
#!/bin/bash
set -ex
export PRTY_NO_VIPS=1
if uname | grep -iE '^(msys|mingw)'; then
pids=()
+34 -43
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# coding: latin-1
# coding: L1
from __future__ import division, print_function, unicode_literals
import re, os, sys, time, shutil, signal, tarfile, hashlib, platform, tempfile, traceback
import subprocess as sp
@@ -25,33 +25,27 @@ import subprocess as sp
"""
to edit this file, use HxD or "vim -b"
(there is compressed stuff at the end)
run me with python 2.7 or 3.3+ to unpack and run copyparty
there's zero binaries! just plaintext python scripts all the way down
so you can easily unpack the archive and inspect it for shady stuff
the archive data is attached after the b"\n# eof\n" archive marker,
b"?0" decodes to b"\x00"
b"?n" decodes to b"\n"
b"?r" decodes to b"\r"
b"??" decodes to b"?"
this is an SFX (self-extracting) tar.gz archive;
see https://copyparty.eu/sfx-wtf/
"""
# set by make-sfx.sh
# fmt: # set by make-sfx.sh
VER = None
SIZE = None
CKSUM = None
STAMP = None
PY2 = sys.version_info < (3,)
PY37 = sys.version_info > (3, 7)
WINDOWS = sys.platform in ["win32", "msys"]
sys.dont_write_bytecode = True
me = os.path.abspath(os.path.realpath(__file__))
OP = os.path
ENV = os.environ
v = sys.version_info
PY2 = v < (3,)
PY37 = v > (3, 7)
WINDOWS = sys.platform in ["win32", "msys"]
me = OP.abspath(OP.realpath(__file__))
def eprint(*a, **ka):
@@ -234,7 +228,7 @@ def encode(data, size, cksum, ver, ts):
def makesfx(tar_src, ver, ts):
sz = os.path.getsize(tar_src)
sz = OP.getsize(tar_src)
cksum = hashfile(tar_src)
encode(yieldfile(tar_src), sz, cksum, ver, ts)
@@ -253,7 +247,7 @@ def u8(gen):
def yieldfile(fn):
s = 64 * 1024
s = 0x10000
with open(fn, "rb", s * 4) as f:
for block in iter(lambda: f.read(s), b""):
yield block
@@ -267,18 +261,19 @@ def hashfile(fn):
return h.hexdigest()[:24]
def unpack():
"""unpacks the tar yielded by `data`"""
def unpack(top):
"""extracts the archive"""
name = "pe-copyparty"
try:
name += "." + str(os.geteuid())
name += ".%s" % os.geteuid()
except:
pass
tag = "v" + str(STAMP)
top = tempfile.gettempdir()
opj = os.path.join
ofe = os.path.exists
tag = "v%s" % STAMP
opj = OP.join
ofe = OP.exists
if not ofe(top):
os.mkdir(top)
final = opj(top, name)
san = opj(final, "copyparty/up2k.py")
for suf in range(0, 9001):
@@ -309,7 +304,7 @@ def unpack():
raise Exception(t % (CKSUM, SIZE, ck, sz))
with tarfile.open(tar, "r:bz2") as tf:
# this is safe against traversal
# this is traversal-safe
# skip 1
# since it will never process user-provided data;
# the only possible input is a single tar.bz2
@@ -333,22 +328,13 @@ def unpack():
pass
try:
if os.path.islink(final):
if OP.islink(final):
os.remove(final)
else:
shutil.rmtree(final)
except:
pass
for fn in u8(os.listdir(top)):
if fn.startswith(name) and fn != withpid:
try:
old = opj(top, fn)
if time.time() - os.path.getmtime(old) > 86400:
shutil.rmtree(old)
except:
pass
try:
os.symlink(mine, final)
except:
@@ -362,14 +348,14 @@ def unpack():
def get_payload():
"""yields the binary data attached to script"""
"""decodes the binary data"""
with open(me, "rb") as f:
buf = f.read().rstrip(b"\r\n")
ptn = b"\n# eof\n#"
a = buf.find(ptn)
if a < 0:
raise Exception("could not find archive marker")
raise Exception("corrupt sfx: no eof marker")
esc = {b"??": b"?", b"?r": b"\r", b"?n": b"\n", b"?0": b"\x00"}
buf = buf[a + len(ptn) :].replace(b"\n#", b"")
@@ -409,7 +395,7 @@ def run(tmp, j2, ftp):
sys.argv.append("--sfx-tpoke=" + tmp)
ld = (("", ""), (j2, "j2"), (ftp, "ftp"), (not PY2, "py2"), (PY37, "py37"))
ld = [os.path.join(tmp, b) for a, b in ld if not a]
ld = [OP.join(tmp, b) for a, b in ld if not a]
# skip 1
# enable this to dynamically remove type hints at startup,
@@ -432,8 +418,7 @@ def run_i(ld):
for x in ld:
sys.path.insert(0, x)
e = os.environ
e["PRTY_NO_IMPRESO"] = "1"
ENV["PRTY_NO_IMPRESO"] = "1"
from copyparty.__main__ import main as p
@@ -490,7 +475,13 @@ def main():
# skip 0
tmp = os.path.realpath(unpack())
try:
if WINDOWS:
x
z = unpack(ENV.get("PRTY_XD") or OP.expanduser("~/.cache"))
except:
z = unpack(tempfile.gettempdir())
tmp = OP.realpath(z)
try:
from jinja2 import __version__ as j2
+3
View File
@@ -71,6 +71,7 @@ Ls.hmn = {
["ctrl-V", "paste (move/copy) here"],
["Y", "download selected"],
["F2", "rename selected"],
["F4", "update/refresh"],
"file-list-sel",
["space", "toggle file selection"],
@@ -605,6 +606,7 @@ Ls.hmn = {
"u_ancient": 'your browser is impressively ancient -- maybe you should <a href="#" id="u2nah">use bup instead</a>',
"u_nowork": "need firefox 53+ or chrome 57+ or iOS 11+",
"tail_2old": "need firefox 105+ or chrome 71+ or iOS 14.5+",
"b2old": 'your browser is too old',
"u_nodrop": 'your browser is too old for drag-and-drop uploading',
"u_notdir": "that's not a folder!\n\nyour browser is too old,\nplease try dragdrop instead",
"u_uri": "to dragdrop images from other browser windows,\nplease drop it onto the big upload button",
@@ -688,6 +690,7 @@ Ls.hmn = {
"rc_pla": "play as audio",
"rc_txt": "open in textfile viewer",
"rc_md": "open in markdown viewer",
"rc_wopi": "open in office editor",
"rc_dl": "download",
"rc_zip": "download as archive",
"rc_cpl": "copy link",
+25
View File
@@ -104,6 +104,22 @@ class TestHttpCli(unittest.TestCase):
eprint("\033[33m{}\n# {}\033[0m".format(ret, furl))
self.fail()
if tctr == 1:
# http206
full = ret
self.hcurl(furl, "Range: bytes=0-0", full[:1])
self.hcurl(furl, "Range: bytes=0-4", full[0:5])
self.hcurl(furl, "Range: bytes=1-4", full[1:5])
self.hcurl(furl, "Range: bytes=-1", full[-1:])
self.hcurl(furl, "Range: bytes=-7", full[-7:])
self.hcurl(furl, "Range: bytes=0-", full)
self.hcurl(furl, "Range: bytes=1-", full[1:])
self.hcurl(furl, "Range: bytes=2-", full[2:])
for zs in ("0--1", "--1", "-0", ""):
h, _ = self.hcurl(furl, "Range: bytes=" + zs, None)
self.assertEqual(h.split(" ")[1], "416")
return
# file browser: html
h, ret = self.curl(durl)
res = "'{}'".format(self.fn) in ret
@@ -243,6 +259,15 @@ class TestHttpCli(unittest.TestCase):
return conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
def hcurl(self, url, hdr, expected):
zs = "GET /%s HTTP/1.1\r\n%s\r\nCookie: cppwd=o\r\nConnection: close\r\n\r\n"
conn = self.conn.setbuf((zs % (url, hdr)).encode("utf-8"))
HttpCli(conn).run()
h, rsp = conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
if expected is not None:
self.assertEqual(rsp, expected)
return h, rsp
def propfind(self, url, depth=1):
zs = "PROPFIND /%s HTTP/1.1\r\nDepth: %d\r\nPW: o\r\nConnection: close\r\n\r\n"
buf = zs % (url, depth)
+2 -2
View File
@@ -145,10 +145,10 @@ class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None, **ka0):
ka = {}
ex = "allow_flac allow_wav chpw cookie_lax daw dav_auth dav_mac dav_rt dlni dothidden e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only http_no_tcp ih ihead localtime log_badxml magic md_no_br nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_dupe_m no_fnugg no_html no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_script no_tail no_tarcmp no_thumb no_vthumb no_u2abrt no_zip no_zls nrand nsort nw og og_no_head og_s_title ohead opds q rand re_dirsz reflink rm_partial rmagic rss show_hist smb srch_dbg srch_excl srch_icase stats ui_noacci ui_nocpla ui_noctxb ui_nolbar ui_nombar ui_nonav ui_notree ui_norepl ui_nosrvi uqe usernames vague_403 vc ver vol_nospawn vol_or_crash wo_up_readme wopi write_uplog xdev xlink xvol zipmaxu zs"
ex = "allow_flac allow_wav allow_svg_js chpw cookie_lax daw dav_auth dav_mac dav_rt dlni dothidden e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only http_no_tcp ih ihead localtime log_badxml magic md_no_br nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_dupe_m no_fnugg no_html no_lifetime no_logues no_mime no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_script no_tail no_tarcmp no_thumb no_vthumb no_u2abrt no_zip no_zls nrand nsort nw og og_no_head og_s_title ohead opds q rand re_dirsz reflink rm_partial rmagic rss show_hist smb srch_dbg srch_excl srch_nfkc srch_icase stats ui_noacci ui_nocpla ui_noctxb ui_nolbar ui_nombar ui_nonav ui_notree ui_norepl ui_nosrvi uqe usernames vague_403 vc ver vol_nospawn vol_or_crash wo_up_readme wopi write_uplog xdev xlink xvol zipmaxu zs"
ka.update(**{k: False for k in ex.split()})
ex = "dav_inf dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump wram re_dhash see_dots plain_ip"
ex = "dav_inf dedup dotpart dotsrch hist_cow hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump wram re_dhash see_dots plain_ip"
ka.update(**{k: True for k in ex.split()})
ex = "ah_cli ah_gen css_browser dbpath hist ipu js_browser js_other lf_url mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua ua_nodoc ua_nozip"