mirror of
https://github.com/9001/copyparty.git
synced 2026-11-10 12:28:23 +00:00
Compare commits
120
Commits
v1.20.20
...
hovudstraum
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1af7efeca8 | ||
|
|
8b4593c9a2 | ||
|
|
7ccebb6948 | ||
|
|
ed0fcd608f | ||
|
|
9811cd3101 | ||
|
|
fa8913c712 | ||
|
|
4bd64d7456 | ||
|
|
7ce483e970 | ||
|
|
82eab3d2f8 | ||
|
|
0dc1cb3291 | ||
|
|
1a87dc3218 | ||
|
|
dc3adc0f4d | ||
|
|
5f00c1631c | ||
|
|
e59fc05e73 | ||
|
|
e8de421174 | ||
|
|
36aaa24974 | ||
|
|
5e0a88e355 | ||
|
|
4b283b4bfd | ||
|
|
64f574fe94 | ||
|
|
a31870cbd3 | ||
|
|
74b17374c2 | ||
|
|
670e86228c | ||
|
|
1341b3421f | ||
|
|
5961228c93 | ||
|
|
28f9f6f002 | ||
|
|
186cbf939f | ||
|
|
080935788b | ||
|
|
c6918c2ce6 | ||
|
|
ab2299e3db | ||
|
|
334dccc55e | ||
|
|
8d7aa4f860 | ||
|
|
f73b37c6fa | ||
|
|
731dbbff84 | ||
|
|
23e00c0a04 | ||
|
|
107dec9ed1 | ||
|
|
f445bfa862 | ||
|
|
aff083fb28 | ||
|
|
5ccc999f39 | ||
|
|
16a3f5d51a | ||
|
|
35af7c9fef | ||
|
|
4d1f2a9301 | ||
|
|
229159027b | ||
|
|
ec4a95e1a5 | ||
|
|
eb4f1dddce | ||
|
|
097b83056e | ||
|
|
c688bb2e46 | ||
|
|
c92435c2d2 | ||
|
|
22afaa7586 | ||
|
|
5ddbce31fc | ||
|
|
ec8ec0f1a6 | ||
|
|
558f6aee44 | ||
|
|
6867ccc511 | ||
|
|
947d73eeb6 | ||
|
|
735d6e00de | ||
|
|
fd4f434192 | ||
|
|
971a62cc89 | ||
|
|
bfc3cff848 | ||
|
|
8967726f61 | ||
|
|
ad9994bef3 | ||
|
|
e5e2e97910 | ||
|
|
e42e01563e | ||
|
|
de21c210c6 | ||
|
|
55969b8e54 | ||
|
|
b80a210e0f | ||
|
|
8016512064 | ||
|
|
72eb03bf43 | ||
|
|
f870cf3c05 | ||
|
|
e6c9c6fbeb | ||
|
|
dd9cf6456d | ||
|
|
185087ee05 | ||
|
|
3e3401f4ed | ||
|
|
a78611d8c5 | ||
|
|
370a05ab3c | ||
|
|
d7d30f268e | ||
|
|
34229337cc | ||
|
|
0767d95674 | ||
|
|
f00d19c5d3 | ||
|
|
ab791921ea | ||
|
|
2cebd8ce16 | ||
|
|
662e3c5d59 | ||
|
|
3ea671ff9a | ||
|
|
f792a9529c | ||
|
|
4e318d5165 | ||
|
|
80cf7bfdc0 | ||
|
|
704af697c7 | ||
|
|
b854b8f596 | ||
|
|
936e8a3bf3 | ||
|
|
7796c2d06a | ||
|
|
adf5f639fa | ||
|
|
d4a1ce48e4 | ||
|
|
0a957bf264 | ||
|
|
bdf8b793d9 | ||
|
|
69c022f565 | ||
|
|
7984ef9232 | ||
|
|
9de090265f | ||
|
|
6115eb452d | ||
|
|
32eadc0f14 | ||
|
|
566de65f17 | ||
|
|
78a4ee49a2 | ||
|
|
9ff6a71a2a | ||
|
|
21c2c72889 | ||
|
|
18791c5768 | ||
|
|
2be85b67a9 | ||
|
|
eeb399e3c9 | ||
|
|
dcc0abd696 | ||
|
|
48d9de03fe | ||
|
|
dbc6df812b | ||
|
|
1864805072 | ||
|
|
47297475bf | ||
|
|
3098cf59cb | ||
|
|
efcf96e3e3 | ||
|
|
92c3f32e13 | ||
|
|
14e2d79b08 | ||
|
|
825f1c9456 | ||
|
|
f19ef033ec | ||
|
|
8b6e8972fe | ||
|
|
fbcb773307 | ||
|
|
3eb7b46d83 | ||
|
|
4f20738b71 | ||
|
|
ef75220faf |
No files matched your search
@@ -4,4 +4,5 @@
|
||||
|
||||
*.png binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.gz binary
|
||||
@@ -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
|
||||
@@ -218,6 +219,8 @@ other places to download copyparty from (non-github links):
|
||||
* https://copyparty.eu/enz = https://copyparty.eu/copyparty-en.pyz = the enterprise pyz
|
||||
* https://copyparty.eu/cli = online cli helptext
|
||||
|
||||
DO NOT download from [sourceforge](https://copyparty.eu/unsanctioned/sourceforge.txt)
|
||||
|
||||
|
||||
### at home
|
||||
|
||||
@@ -525,6 +528,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)
|
||||
|
||||
@@ -568,12 +572,14 @@ if you want to grant access to all users who are logged in, the group `acct` wil
|
||||
* to do the opposite, granting access to everyone who is NOT logged in. `*,-@acct` does the trick, for example `-v /srv/welcome:welcome:r,*,-@acct`
|
||||
* single users can also be subtracted from a group: `@admins,-james`
|
||||
|
||||
anyone trying to bruteforce a password gets banned according to `--ban-pw`; default is 24h ban for 9 failed attempts in 1 hour
|
||||
anyone trying to bruteforce a password gets banned according to `--ban-pw` (assuming [real-ip](#real-ip) is configured correctly); default is 24h ban for 9 failed attempts in 1 hour
|
||||
|
||||
and if you want to use config files instead of commandline args (good!) then here's the same examples as a configfile; save it as `foobar.conf` and use it like this: `python copyparty-sfx.py -c foobar.conf`
|
||||
|
||||
* you can also `PRTY_CONFIG=foobar.conf python copyparty-sfx.py` (convenient in docker etc)
|
||||
|
||||
config file example:
|
||||
|
||||
```yaml
|
||||
[accounts]
|
||||
u1: p1 # create account "u1" with password "p1"
|
||||
@@ -616,6 +622,8 @@ and if you want to use config files instead of commandline args (good!) then her
|
||||
|
||||
hiding specific subfolders by mounting another volume on top of them
|
||||
|
||||
> **WARNING:** shadowing is NOT bulletproof when the underlying filesystem is casefolding (case-insensitive), in other words DON'T rely on this when copyparty is running on Windows and/or MacOS. This is also unsafe when combined with certain filesystems on Linux, especially FAT32/exFAT. Pay attention to the volume-listing in the startup log; `casechk:n` indicates probably-OK, but if it says `casechk:y` then DO NOT trust shadowing to fully prevent access to a file/subfolder.
|
||||
|
||||
for example `-v /mnt::r -v /var/empty:web/certs:` (note: no permissions) mounts the server folder `/mnt` as the webroot, but another volume is mounted at `/web/certs` -- so visitors can only see the contents of `/mnt` and `/mnt/web` (at URLs `/` and `/web`), but not `/mnt/web/certs` because URL `/web/certs` is mapped to `/var/empty`
|
||||
|
||||
to fully unmap it from the filesystem, specify `//NULL` instead of a real path such as `/var/empty`, so for example `-v /mnt::r -v //NULL:web/certs:` ensures `/web/certs` will never be accessible by anyone
|
||||
@@ -922,7 +930,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)
|
||||
|
||||
@@ -1797,12 +1805,17 @@ avoid traversing into other filesystems using `--xdev` / volflag `:c,xdev`, ski
|
||||
|
||||
and/or you can `--xvol` / `:c,xvol` to ignore all symlinks leaving the volume's top directory, but still allow bind-mounts pointing elsewhere
|
||||
|
||||
* symlinks are permitted with `xvol` if they point into another volume where the user also has some sort of access, keeping permissions from outer/initial volume
|
||||
* symlinks are permitted with `xvol` if they point into another volume where the user also has some sort of access
|
||||
* usually keeps permissions from outer/initial volume, but don't take that for granted
|
||||
|
||||
these options will reduce performance; unlikely worst-case estimates are 14% reduction for directory listings, 35% for download-as-tar
|
||||
these options will reduce performance; unlikely worst-case estimates are 27% reduction for directory listings, 35% for download-as-tar
|
||||
|
||||
* if the underlying storage is [a network disk](#using-the-cloud-as-storage) then xvol/xdev will be **very** slow (not to mention pointless/useless)
|
||||
|
||||
as of copyparty v1.7.0 these options also prevent file access at runtime -- in previous versions it was just hints for the indexer
|
||||
|
||||
> **WARNING:** the `xvol` and `xdev` options should NOT be relied on for security; these are originally and primarily intended for the [e2ds indexer](#file-indexing). Certain underlying filesystems (especially FUSE-based and casefolding ones) will be problematic, and symlinks inherently have various TOCTOUs, and there's probably also bugs in copyparty where xvol/xdev is not fully enforced for certain APIs. The developer does not care about these options and they do not receive much testing.
|
||||
|
||||
### periodic rescan
|
||||
|
||||
filesystem monitoring; if copyparty is not the only software doing stuff on your filesystem, you may want to enable periodic rescans to keep the index up to date
|
||||
@@ -1826,6 +1839,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:
|
||||
@@ -2177,7 +2207,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]
|
||||
@@ -2193,7 +2223,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]
|
||||
@@ -2230,7 +2260,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]
|
||||
@@ -2551,7 +2581,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]
|
||||
@@ -2618,7 +2648,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)
|
||||
|
||||
@@ -3024,7 +3054,7 @@ below are some tweaks roughly ordered by usefulness:
|
||||
* and pypy can sometimes crash on startup with `-j0` (TODO make issue)
|
||||
|
||||
* if you are running the copyparty server **on Windows or Macos:**
|
||||
* `--casechk=n` makes it much faster, but also awakens [the usual surprises](https://github.com/9001/copyparty/issues/781) you expect from a case-insensitive filesystem
|
||||
* `--casechk=n` makes it much faster, but [reduces security](https://github.com/9001/copyparty/security/advisories/GHSA-3c88-p299-7hvr) and awakens [the usual surprises](https://github.com/9001/copyparty/issues/781) you expect from a case-insensitive filesystem
|
||||
* this is the same as `casechk: n` in a config-file
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+85
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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
|
||||
+28
-13
@@ -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.25"
|
||||
S_BUILD_DT = "2026-10-05"
|
||||
|
||||
"""
|
||||
u2c.py: upload to copyparty
|
||||
@@ -67,6 +67,8 @@ else:
|
||||
|
||||
WTF8 = "replace" if PY2 else "surrogateescape"
|
||||
|
||||
SEP = "{0}".format(os.sep).encode("ascii")
|
||||
|
||||
VT100 = platform.system() != "Windows"
|
||||
|
||||
|
||||
@@ -249,7 +251,7 @@ class File(object):
|
||||
|
||||
def __init__(self, top, rel, size, lmod):
|
||||
self.top = top # type: bytes
|
||||
self.rel = rel.replace(b"\\", b"/") # type: bytes
|
||||
self.rel = rel.replace(SEP, b"/") # type: bytes
|
||||
self.size = size # type: int
|
||||
self.lmod = lmod # type: float
|
||||
|
||||
@@ -267,6 +269,7 @@ class File(object):
|
||||
self.wark = "" # type: str
|
||||
self.url = "" # type: str
|
||||
self.nhs = 0 # type: int
|
||||
self.t_hs = 0.0 # type: float
|
||||
|
||||
# set by upload
|
||||
self.t0_up = 0.0 # type: float
|
||||
@@ -652,7 +655,7 @@ def walkdir(err, top, excl, seen):
|
||||
|
||||
def walkdirs(err, tops, excl):
|
||||
"""recursive statdir for a list of tops, yields [top, relpath, stat]"""
|
||||
sep = "{0}".format(os.sep).encode("ascii")
|
||||
sep = SEP
|
||||
if not VT100:
|
||||
excl = excl.replace("/", r"\\")
|
||||
za = []
|
||||
@@ -904,6 +907,7 @@ def handshake(ar, file, search):
|
||||
file.url = quotep(r["purl"].encode("utf-8", WTF8)).decode("utf-8")
|
||||
file.name = r["name"]
|
||||
file.wark = r["wark"]
|
||||
file.t_hs = time.time()
|
||||
|
||||
if ar.uon and not r["hash"]:
|
||||
printlink(ar, file.url, r["name"], r.get("fk"))
|
||||
@@ -1027,7 +1031,7 @@ class Ctl(object):
|
||||
self.up_b = 0 # num bytes handled
|
||||
self.up_br = 0 # num bytes actually transferred
|
||||
self.uploader_busy = 0
|
||||
self.serialized = False
|
||||
self.oneconn = False
|
||||
|
||||
self.t0 = time.time()
|
||||
self.t0_up = None
|
||||
@@ -1213,14 +1217,15 @@ class Ctl(object):
|
||||
|
||||
def hasher(self):
|
||||
ptn = re.compile(self.ar.x.encode("utf-8"), re.I) if self.ar.x else None
|
||||
sep = "{0}".format(os.sep).encode("ascii")
|
||||
usep = os.sep
|
||||
sep = SEP
|
||||
prd = None
|
||||
ls = {}
|
||||
for top, rel, inf in self.filegen:
|
||||
isdir = stat.S_ISDIR(inf.st_mode)
|
||||
if self.ar.z or self.ar.drd:
|
||||
rd = rel if isdir else os.path.dirname(rel)
|
||||
srd = rd.decode("utf-8", "replace").replace("\\", "/").rstrip("/")
|
||||
srd = rd.decode("utf-8", "replace").replace(usep, "/").rstrip("/")
|
||||
if srd:
|
||||
srd += "/"
|
||||
if prd != rd:
|
||||
@@ -1230,7 +1235,7 @@ class Ctl(object):
|
||||
print(" ls ~{0}".format(srd))
|
||||
zt = (
|
||||
self.ar.vtop,
|
||||
quotep(rd.replace(b"\\", b"/")).decode("utf-8"),
|
||||
quotep(rd.replace(sep, b"/")).decode("utf-8"),
|
||||
)
|
||||
sc, txt = web.req("GET", "%s%s?ls<&dots" % zt, {})
|
||||
if sc >= 400:
|
||||
@@ -1403,10 +1408,10 @@ class Ctl(object):
|
||||
self.recheck.append(file)
|
||||
|
||||
with self.mutex:
|
||||
if hs and not sprs and not self.serialized:
|
||||
t = "server filesystem does not support sparse files; serializing uploads\n"
|
||||
if hs and not sprs and not self.oneconn:
|
||||
t = "server filesystem does not support sparse files; cannot parallelize uploads\n"
|
||||
eprint(t)
|
||||
self.serialized = True
|
||||
self.oneconn = True
|
||||
for _ in range(self.ar.j - 1):
|
||||
self.q_upload.put(None)
|
||||
if not hs:
|
||||
@@ -1418,7 +1423,7 @@ class Ctl(object):
|
||||
if not file.recheck:
|
||||
self.up_done(file)
|
||||
|
||||
if hs and file.up_c:
|
||||
if hs and file.up_c and not self.oneconn:
|
||||
# some chunks failed
|
||||
self.up_c -= len(hs)
|
||||
file.up_c -= len(hs)
|
||||
@@ -1476,6 +1481,7 @@ class Ctl(object):
|
||||
self.q_upload.put(fsl)
|
||||
|
||||
def uploader(self):
|
||||
drop_hs = (0.0, "")
|
||||
while True:
|
||||
fsl = self.q_upload.get()
|
||||
if not fsl:
|
||||
@@ -1494,6 +1500,9 @@ class Ctl(object):
|
||||
cids = fsl.cids
|
||||
self.last_file = file
|
||||
|
||||
if self.oneconn and drop_hs == (file.t_hs, file.wark):
|
||||
continue
|
||||
|
||||
with self.mutex:
|
||||
if not self.uploader_busy:
|
||||
self.at_upr = time.time()
|
||||
@@ -1519,6 +1528,9 @@ class Ctl(object):
|
||||
t = "upload failed, retrying: %s #%s+%d (%s)\n"
|
||||
eprint(t % (file.name, cids[0][:8], len(cids) - 1, ex))
|
||||
file.cd = time.time() + self.ar.cd
|
||||
if self.oneconn:
|
||||
file.ucids = []
|
||||
drop_hs = (file.t_hs, file.wark)
|
||||
# handshake will fix it
|
||||
|
||||
with self.mutex:
|
||||
@@ -1576,7 +1588,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 +1741,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")
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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.19"
|
||||
pkgver="1.20.26"
|
||||
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=("834d97d637575cb9b2d53052790b6512f462b3241dff489cf641539ef624fd6c")
|
||||
backup=("etc/copyparty.conf")
|
||||
install=copyparty.install
|
||||
sha256sums=("ac7ea05bde8d431b32cc2ff618f2754e46bd967bf10deaa6654dac9a449a6ff9")
|
||||
|
||||
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"
|
||||
}
|
||||
@@ -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,7 +2,7 @@
|
||||
|
||||
|
||||
pkgname=copyparty
|
||||
pkgver=1.20.19
|
||||
pkgver=1.20.26
|
||||
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=("834d97d637575cb9b2d53052790b6512f462b3241dff489cf641539ef624fd6c")
|
||||
sha256sums=("ac7ea05bde8d431b32cc2ff618f2754e46bd967bf10deaa6654dac9a449a6ff9")
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -91,19 +91,20 @@ let
|
||||
day = builtins.substring 6 2 lastModifiedDate;
|
||||
};
|
||||
# ex: "1970" "1" "1"
|
||||
dateStringsShort = builtins.mapAttrs (_: val: toString (lib.toIntBase10 val)) dateStringsZeroPrefixed;
|
||||
dateStringsShort = builtins.mapAttrs (
|
||||
_: val: toString (lib.toIntBase10 val)
|
||||
) dateStringsZeroPrefixed;
|
||||
unstableVersion =
|
||||
if copypartyFlake == null then
|
||||
"${pinData.version}-unstable"
|
||||
else
|
||||
with dateStringsZeroPrefixed; "${pinData.version}-unstable-${year}-${month}-${day}"
|
||||
;
|
||||
with dateStringsZeroPrefixed; "${pinData.version}-unstable-${year}-${month}-${day}";
|
||||
version = if stable then pinData.version else unstableVersion;
|
||||
stableSrc = fetchurl {
|
||||
inherit (pinData) url hash;
|
||||
};
|
||||
root = ../../../..;
|
||||
unstableSrc = nix-gitignore.gitignoreSource [] root;
|
||||
unstableSrc = nix-gitignore.gitignoreSource [ ] root;
|
||||
src = if stable then stableSrc else unstableSrc;
|
||||
rev = copypartyFlake.shortRev or copypartyFlake.dirtyShortRev or "unknown";
|
||||
unstableCodename = "unstable" + (lib.optionalString (copypartyFlake != null) "-${rev}");
|
||||
@@ -125,30 +126,38 @@ buildPythonApplication {
|
||||
old_src_folder="''${folders[0]}"
|
||||
cp -r "$old_src_folder"/copyparty/web/deps copyparty/web/deps
|
||||
sed -i 's/^CODENAME =.*$/CODENAME = "${unstableCodename}"/' copyparty/__version__.py
|
||||
${lib.optionalString (copypartyFlake != null) (with dateStringsShort; ''
|
||||
sed -i 's/^BUILD_DT =.*$/BUILD_DT = (${year}, ${month}, ${day})/' copyparty/__version__.py
|
||||
'')}
|
||||
${lib.optionalString (copypartyFlake != null) (
|
||||
with dateStringsShort;
|
||||
''
|
||||
sed -i 's/^BUILD_DT =.*$/BUILD_DT = (${year}, ${month}, ${day})/' copyparty/__version__.py
|
||||
''
|
||||
)}
|
||||
'';
|
||||
dependencies =
|
||||
[
|
||||
jinja2
|
||||
fusepy
|
||||
]
|
||||
++ lib.optional withSMB impacket
|
||||
++ lib.optional withSFTP paramiko
|
||||
++ lib.optional withFTP pyftpdlib
|
||||
++ lib.optional withFTPS pyopenssl
|
||||
++ lib.optional withTFTP partftpy
|
||||
++ lib.optional withCertgen cfssl
|
||||
++ lib.optional withThumbnails pillow
|
||||
++ lib.optional withFastThumbnails pyvips
|
||||
++ lib.optional withMediaProcessing ffmpeg
|
||||
++ lib.optional withBasicAudioMetadata mutagen
|
||||
++ lib.optional withHashedPasswords argon2-cffi
|
||||
++ lib.optional withZeroMQ pyzmq
|
||||
++ lib.optional withMagic magic
|
||||
++ (extraPythonPackages python.pkgs);
|
||||
makeWrapperArgs = [ "--prefix PATH : ${lib.makeBinPath runtimeDeps}" ];
|
||||
dependencies = [
|
||||
jinja2
|
||||
fusepy
|
||||
]
|
||||
++ lib.optional withSMB impacket
|
||||
++ lib.optional withSFTP paramiko
|
||||
++ lib.optional withFTP pyftpdlib
|
||||
++ lib.optional withFTPS pyopenssl
|
||||
++ lib.optional withTFTP partftpy
|
||||
++ lib.optional withCertgen cfssl
|
||||
++ lib.optional withThumbnails pillow
|
||||
++ lib.optional withFastThumbnails pyvips
|
||||
++ lib.optional withMediaProcessing ffmpeg
|
||||
++ lib.optional withBasicAudioMetadata mutagen
|
||||
++ lib.optional withHashedPasswords argon2-cffi
|
||||
++ lib.optional withZeroMQ pyzmq
|
||||
++ lib.optional withMagic magic
|
||||
++ (extraPythonPackages python.pkgs);
|
||||
|
||||
makeWrapperArgs = [
|
||||
"--prefix"
|
||||
"PATH"
|
||||
":"
|
||||
(lib.makeBinPath runtimeDeps)
|
||||
];
|
||||
|
||||
pyproject = true;
|
||||
build-system = [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.20.19/copyparty-1.20.19.tar.gz",
|
||||
"version": "1.20.19",
|
||||
"hash": "sha256-g02X1jdXXLmy1TBSeQtlEvRisyQd/0ic9kFTnvYk/Ww="
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.20.26/copyparty-1.20.26.tar.gz",
|
||||
"version": "1.20.26",
|
||||
"hash": "sha256-rH6gW96NQxsyzC/2GPJ1Tka9lnvxDeqmZU2smkSab/k="
|
||||
}
|
||||
@@ -26,9 +26,6 @@ See release at https://github.com/9001/copyparty/releases
|
||||
%setup -q
|
||||
|
||||
%build
|
||||
cd "copyparty/web"
|
||||
make
|
||||
cd -
|
||||
%pyproject_wheel
|
||||
|
||||
%install
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,9 +3,6 @@
|
||||
# vim: ft=yaml:
|
||||
|
||||
|
||||
# put this file in /etc/
|
||||
|
||||
|
||||
[global]
|
||||
e2dsa # enable file indexing and filesystem scanning
|
||||
e2ts # and enable multimedia indexing
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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`
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
+238
-26
@@ -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 [
|
||||
@@ -619,6 +687,7 @@ def get_sects():
|
||||
\033[32m-i 127.0.0.1\033[0m listens on IPv4 localhost only
|
||||
\033[32m-i 127.1\033[0m listens on IPv4 localhost only
|
||||
\033[32m-i 127.1,192.168.123.1\033[0m = IPv4 localhost and 192.168.123.1
|
||||
\033[32m-i no\033[0m disable webserver
|
||||
|
||||
\033[33m-p\033[0m takes a comma-separated list of tcp ports to listen on;
|
||||
the default is \033[32m-p 3923\033[0m but as root you can \033[32m-p 80,443,3923\033[0m
|
||||
@@ -779,6 +848,122 @@ def get_sects():
|
||||
).rstrip()
|
||||
+ build_flags_desc(),
|
||||
],
|
||||
[
|
||||
"dedup",
|
||||
"file deduplication on upload",
|
||||
dedent(
|
||||
"""
|
||||
when just \033[36m--dedup\033[0m or volflag "\033[36mdedup\033[0m" is enabled, files will be
|
||||
deduplicated by means of \033[1msymlinks,\033[0m which demand some caution:
|
||||
|
||||
the available dedup approaches:
|
||||
|
||||
* symlinks work on most filesystems, but if another software
|
||||
moves either the symlink itself or the file that it points to,
|
||||
the link will break and the file "disappears";
|
||||
only safe if only copyparty is used to rename/move/delete
|
||||
|
||||
* hardlinks work on Linux filesystems and \033[1mlook\033[0m like regular files,
|
||||
except if the contents of one copy of that file is edited, then
|
||||
this will affect \033[1mall the dupes too!\033[0m
|
||||
and they share the same last-modified-time
|
||||
|
||||
* reflinks only work on btrfs/zfs/xfs but is the safest/best choice;
|
||||
deduplicated files behave just like regular files,
|
||||
and can be modified without affecting other copies/dupes
|
||||
|
||||
to change approach, add one of the following in addition to "\033[36mdedup\033[0m":
|
||||
|
||||
\033[36m--reflink\033[0m or volflag "\033[36mreflink\033[0m"
|
||||
if reflink is not possible (unsupp/cross-fs), a full copy is made
|
||||
|
||||
\033[36m--hardlink-only\033[0m or volflag "\033[36mhardlinkonly\033[0m"
|
||||
if hardlink is not possible (unsupp/cross-fs), a full copy is made
|
||||
|
||||
\033[36m--hardlink\033[0m or volflag "\033[36mhardlink\033[0m"
|
||||
if hardlink is not possible (unsupp/cross-fs), a symlink is made
|
||||
|
||||
note: dedup works best when combined with e2ds or e2dsa
|
||||
|
||||
to deduplicate existing files on-disk, see \033[36m--help-redup\033[0m
|
||||
"""
|
||||
),
|
||||
],
|
||||
[
|
||||
"redup",
|
||||
"deduplicating existing files on disk",
|
||||
dedent(
|
||||
"""
|
||||
\033[36m--redup\033[0m can convert on-disk files between deduplication-approaches,
|
||||
for example turning redundant full copies into reflinks to save space
|
||||
|
||||
converts \033[1mall files\033[0m in each volume! not just copyparty-created ones
|
||||
|
||||
example: --redup \033[32mno,sym,hard\033[0m=\033[35mref\033[0m will find all files of type
|
||||
\033[32mfullcopy/symlink/hardlink\033[0m, and convert them into \033[35mreflinks\033[0m
|
||||
(see \033[36m--help-dedup\033[0m for a summary on the types)
|
||||
|
||||
converting \033[1mfrom\033[0m symlinks/hardlinks is fairly fast and simple;
|
||||
in this case, the obvious target type/approach is reflinks
|
||||
(which only works on btrfs/zfs/xfs)
|
||||
|
||||
converting \033[1mto\033[0m symlinks or hardlinks is \033[3mpossible\033[0m, but
|
||||
not-recommended due to risks explained in \033[36m--help-dedup\033[0m
|
||||
(interaction with other softwares can result in data loss)
|
||||
|
||||
converting \033[1mfrom\033[0m 'no' (full redundant copies) is hella slow;
|
||||
checksums each file to avoid corruption from bad RAM
|
||||
"""
|
||||
),
|
||||
],
|
||||
[
|
||||
"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",
|
||||
@@ -1096,9 +1281,9 @@ def get_sects():
|
||||
"file/folder permissions",
|
||||
dedent(
|
||||
"""
|
||||
global-option \033[33m--chmod-f\033[0m and volflag \033[33mchmod_f\033[0m specifies the unix-permission to use when creating a new file
|
||||
global-option \033[33m--chmod-f\033[0m and volflag "\033[33mchmod_f\033[0m" specifies the unix-permission to use when creating a new file
|
||||
|
||||
similarly, \033[33m--chmod-d\033[0m and \033[33mchmod_d\033[0m sets the directory/folder perm
|
||||
similarly, \033[33m--chmod-d\033[0m and "\033[33mchmod_d\033[0m" sets the directory/folder perm
|
||||
|
||||
the value is a three- or four-digit octal number such as \033[32m755\033[0m, \033[32m0644\033[0m, \033[32m2750\033[0m, etc.
|
||||
|
||||
@@ -1265,8 +1450,8 @@ def add_qr(ap, tty):
|
||||
ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)")
|
||||
ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr")
|
||||
ap2.add_argument("--qr-wait", metavar="SEC", type=float, default=0, help="wait \033[33mSEC\033[0m before printing the qr-code to the log")
|
||||
ap2.add_argument("--qr-every", metavar="SEC", type=float, default=0, help="print the qr-code every \033[33mSEC\033[0m (try this with/without --qr-pin in case of issues)")
|
||||
ap2.add_argument("--qr-winch", metavar="SEC", type=float, default=0, help="when --qr-pin is enabled, check for terminal size change every \033[33mSEC\033[0m")
|
||||
ap2.add_argument("--qr-every", metavar="SEC", type=float, default=0, help="print the qr-code every \033[33mSEC\033[0m (try this with/without \033[33m--qr-pin\033[0m in case of issues)")
|
||||
ap2.add_argument("--qr-winch", metavar="SEC", type=float, default=0, help="when \033[33m--qr-pin\033[0m is enabled, check for terminal size change every \033[33mSEC\033[0m")
|
||||
ap2.add_argument("--qr-file", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m write qr-code to file.\n └─To create txt or svg, \033[33mTXT\033[0m is Filepath:Zoom:Pad, for example [\033[32mqr.txt:1:2\033[0m]\n └─To create png or gif, \033[33mTXT\033[0m is Filepath:Zoom:Pad:Foreground:Background, for example [\033[32mqr.png:8:2:333333:ffcc55\033[0m], or [\033[32mqr.png:8:2::ffcc55\033[0m] for transparent")
|
||||
ap2.add_argument("--qr-stdout", action="store_true", help="always display the QR-code on STDOUT in the terminal, even if \033[33m-q\033[0m")
|
||||
ap2.add_argument("--qr-stderr", action="store_true", help="always display the QR-code on STDERR in the terminal, even if \033[33m-q\033[0m")
|
||||
@@ -1311,16 +1496,18 @@ def add_upload(ap):
|
||||
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600")
|
||||
ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload (bad idea to enable this on windows and/or cow filesystems)")
|
||||
ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even when it might be dangerous (multiprocessing, filesystems lacking sparse-files support, ...)")
|
||||
ap2.add_argument("--chmod-f", metavar="UGO", type=u, default="", help="unix file permissions to use when creating files; default is probably 644 (OS-decided), see --help-chmod. Examples: [\033[32m644\033[0m] = owner-RW + all-R, [\033[32m755\033[0m] = owner-RWX + all-RX, [\033[32m777\033[0m] = full-yolo (volflag=chmod_f)")
|
||||
ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m2750\033[0m] = setgid + owner-RW + group-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)")
|
||||
ap2.add_argument("--chmod-f", metavar="UGO", type=u, default="", help="unix file permissions to use when creating files; default is probably 644 (OS-decided), see \033[33m--help-chmod\033[0m. Examples: [\033[32m644\033[0m] = owner-RW + all-R, [\033[32m755\033[0m] = owner-RWX + all-RX, [\033[32m777\033[0m] = full-yolo (volflag=chmod_f)")
|
||||
ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see \033[33m--help-chmod\033[0m. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m2750\033[0m] = setgid + owner-RW + group-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)")
|
||||
ap2.add_argument("--uid", metavar="N", type=int, default=-1, help="unix user-id to chown new files/folders to; default = -1 = do-not-change (volflag=uid)")
|
||||
ap2.add_argument("--gid", metavar="N", type=int, default=-1, help="unix group-id to chown new files/folders to; default = -1 = do-not-change (volflag=gid)")
|
||||
ap2.add_argument("--wram", action="store_true", help="allow uploading even if a volume is inside a ramdisk, meaning that all data will be lost on the next server reboot (volflag=wram)")
|
||||
ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)")
|
||||
ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication; see \033[33m--help-dedup\033[0m (volflag=dedup)")
|
||||
ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)")
|
||||
ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)")
|
||||
ap2.add_argument("--hardlink-only", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made (volflag=hardlinkonly)")
|
||||
ap2.add_argument("--reflink", action="store_true", help="enable reflink-based dedup; will fallback on full copies when that is impossible (non-CoW filesystem) (volflag=reflink)")
|
||||
ap2.add_argument("--redup", metavar="A[,B]=T", help="convert dedup-type for existing files; \033[33mA,B\033[0m is dedup-types to convert from (no/ref/sym/hard), \033[33mT\033[0m is target type; converting from sym/hard is fast-ish, from no/ref is slooow, to sym/hard is jank; see \033[33m--help-redup\033[0m. Example: [\033[32msym,hard=ref\033[0m] (volflag=redup)")
|
||||
ap2.add_argument("--redup-dry", action="store_true", help="dry-run; makes \033[33m--redup\033[0m not apply changes (volflag=redup_dry)")
|
||||
ap2.add_argument("--no-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)")
|
||||
ap2.add_argument("--no-dupe-m", action="store_true", help="also reject dupes when moving a file into another volume (volflag=nodupem)")
|
||||
ap2.add_argument("--no-clone", action="store_true", help="do not use existing data on disk to satisfy dupe uploads; reduces server HDD reads in exchange for much more network load (volflag=noclone)")
|
||||
@@ -1338,7 +1525,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")
|
||||
@@ -1414,7 +1601,7 @@ def add_auth(ap):
|
||||
ses_db = os.path.join(E.cfg, "sessions.db")
|
||||
ap2 = ap.add_argument_group("IdP / identity provider / user authentication options")
|
||||
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
|
||||
ap2.add_argument("--idp-hm-usr", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mT\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp")
|
||||
ap2.add_argument("--idp-hm-usr", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mT\033[0m is provided, and its value exists in a mapping defined by this option; see \033[33m--help-idp\033[0m")
|
||||
ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control")
|
||||
ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present")
|
||||
ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m")
|
||||
@@ -1426,7 +1613,7 @@ def add_auth(ap):
|
||||
ap2.add_argument("--idp-login", metavar="L", type=u, default="", help="replace all login-buttons with a link to URL \033[33mL\033[0m (unless \033[32mpw\033[0m is in \033[33m--auth-ord\033[0m then both will be shown); [\033[32m{dst}\033[0m] expands to url of current page")
|
||||
ap2.add_argument("--idp-login-t", metavar="T", type=u, default="Login with SSO", help="the label/text for the idp-login button")
|
||||
ap2.add_argument("--idp-logout", metavar="L", type=u, default="", help="replace all logout-buttons with a link to URL \033[33mL\033[0m")
|
||||
ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see --help-auth-ord")
|
||||
ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see \033[33m--help-auth-ord\033[0m")
|
||||
ap2.add_argument("--pw-hdr", metavar="NAME", type=u, default="pw", help="lowercase name of password-header (NAME: foo); \033[1;31mWARNING:\033[0m Changing this will break support for many clients")
|
||||
ap2.add_argument("--pw-urlp", metavar="NAME", type=u, default="pw", help="lowercase name of password url-param (?NAME=foo); \033[1;31mWARNING:\033[0m Changing this will break support for many clients")
|
||||
ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app")
|
||||
@@ -1512,6 +1699,8 @@ def add_sftp(ap):
|
||||
ap2.add_argument("--sftp-pw", action="store_true", help="allow password-authentication with sftp (not just ssh-keys)")
|
||||
ap2.add_argument("--sftp-anon", metavar="TXT", type=u, default="", help="allow anonymous/unauthenticated connections with \033[33mTXT\033[0m as username")
|
||||
ap2.add_argument("--sftp-hostk", metavar="FP", type=u, default=E.cfg, help="path to folder with hostkeys, for example 'ssh_host_rsa_key'; missing keys will be generated")
|
||||
ap2.add_argument("--sftp-fastldr", action="store_true", help="speedhack for 'loading hostkeys' on old CPUs; only use with known-good hostkeys")
|
||||
ap2.add_argument("--sftp-chattr", metavar="T", default="s", help="which attributes can be modified with chattr? [\033[32ms\033[0m]=size, [\033[32mt\033[0m]=mtime, [\033[32mp\033[0m]=permissions, [\033[32mu\033[0m]=uid/gid. Example: [\033[32mstpu\033[0m]")
|
||||
ap2.add_argument("--sftp-banner", metavar="T", type=u, default="", help="bannertext to send when someone connects; can be @filepath")
|
||||
ap2.add_argument("--sftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m / \033[33m--ipar\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]")
|
||||
ap2.add_argument("--sftp-hs-t", metavar="SEC", type=int, default=15, help="connection handshake timeout in seconds")
|
||||
@@ -1585,9 +1774,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="HOST=URL", type=u, action="append", help="\033[34mREPEATABLE:\033[0m maps http \033[33mHOST\033[0m copyparty being accessed by to specific WOPI client instance \033[33mURL\033[0m; 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-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)")
|
||||
@@ -1630,8 +1820,10 @@ def add_stats(ap):
|
||||
|
||||
def add_yolo(ap):
|
||||
ap2 = ap.add_argument_group("yolo options")
|
||||
ap2.add_argument("--hack-me-bro", action="store_true", help="allow enabling dangerously buggy protocols such as \033[33m--smb\033[0m / \033[33m--tftp\033[0m")
|
||||
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,7 +1898,8 @@ def add_safety(ap):
|
||||
ap2.add_argument("--csp-ui", metavar="TXT", default="script-src 'unsafe-eval' 'nonce-{{ js_nonce }}'; worker-src 'self'", help="content-security-policy to apply for the web-UI; default helps prevent XSS by blocking <script> / onclick / ... (volflag=csp_ui)")
|
||||
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-html", action="store_true", help="show html-files as plain text; helps prevent XSS but kills websites/blogs, also enables \033[33m--no-script\033[0m (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)")
|
||||
@@ -1727,6 +1920,7 @@ def add_safety(ap):
|
||||
ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for \033[33mMIN\033[0m minutes (and also kill its active connections) -- disable with 0")
|
||||
ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]")
|
||||
ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives")
|
||||
ap2.add_argument("--acao-re", metavar="PTN", type=u, default="", help="Access-Control-Allow-Origin; supplement to \033[33m--acao\033[0m; allow request if http-header 'Origin' matches either \033[33m--acao\033[0m or regex-\033[33mPTN\033[0m; ominous example: [\033[32m^moz-extension://.*\033[0m]")
|
||||
ap2.add_argument("--acam", metavar="V[,V]", type=u, default="GET,HEAD", help="Access-Control-Allow-Methods; list of methods to accept from offsite ('*' behaves like \033[33m--acao\033[0m's description)")
|
||||
if FULL_HELP or ANYWIN:
|
||||
ap2.add_argument("--unsafe-tools", action="store_true", help="windows-only: allow running programs (ffmpeg, dcraw_emu, ...) when their location is inside a folder that can be uploaded to (dangerous due to DLL-hijacking)")
|
||||
@@ -1737,7 +1931,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)")
|
||||
@@ -1746,6 +1940,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)")
|
||||
@@ -1839,8 +2034,19 @@ def add_thumbnail(ap):
|
||||
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, 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 \033[33m--help-thumb-ex\033[0m")
|
||||
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):
|
||||
@@ -1897,6 +2103,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)")
|
||||
@@ -1994,7 +2201,7 @@ def add_ui(ap, retry: int):
|
||||
ap2.add_argument("--js-browser", metavar="L", type=u, default="", help="URL to additional JS to include in the filebrowser html")
|
||||
ap2.add_argument("--js-other", metavar="L", type=u, default="", help="URL to additional JS to include in all other pages")
|
||||
ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); can be @PATH to send the contents of a file at PATH, and/or begin with %% to render as jinja2 template; \033[32m<script>\033[0m will not work, use \033[32m<script nonce=\"{{ js_nonce }}\">\033[0m (volflag=html_head)")
|
||||
ap2.add_argument("--html-head-s", metavar="T", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); similar to (and can be combined with) --html-head but only accepts static text (volflag=html_head_s)")
|
||||
ap2.add_argument("--html-head-s", metavar="T", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); similar to (and can be combined with) \033[33m--html-head\033[0m but only accepts static text (volflag=html_head_s)")
|
||||
ap2.add_argument("--ih", action="store_true", help="if a folder contains index.html, show that instead of the directory listing by default (can be changed in the client settings UI, or add ?v to URL for override)")
|
||||
ap2.add_argument("--textfiles", metavar="CSV", type=u, default="txt,nfo,diz,cue,readme", help="file extensions to present as plaintext")
|
||||
ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)")
|
||||
@@ -2005,7 +2212,7 @@ def add_ui(ap, retry: int):
|
||||
ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
|
||||
ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
|
||||
ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-nb\033[0m")
|
||||
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m). This is the same as --ver-who all")
|
||||
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m). This is the same as \033[33m--ver-who all\033[0m")
|
||||
ap2.add_argument("--ver-who", metavar="TXT", type=u, default="no", help="only show version for: [\033[32ma\033[0m]=admin-permission-anywhere, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone")
|
||||
ap2.add_argument("--du-who", metavar="TXT", type=u, default="all", help="only show disk usage for: [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mrw\033[0m]=read-write, [\033[32mw\033[0m]=write, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone (volflag=du_who)")
|
||||
ap2.add_argument("--ver-iwho", type=int, default=0, help=argparse.SUPPRESS)
|
||||
@@ -2078,6 +2285,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),
|
||||
@@ -2114,7 +2322,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)
|
||||
@@ -2217,6 +2425,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()
|
||||
@@ -2371,7 +2582,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:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (1, 20, 20)
|
||||
VERSION = (1, 20, 26)
|
||||
CODENAME = "sftp is fine too"
|
||||
BUILD_DT = (2026, 8, 4)
|
||||
BUILD_DT = (2026, 10, 14)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
+201
-9
@@ -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,
|
||||
@@ -41,6 +42,7 @@ from .util import (
|
||||
odfusion,
|
||||
read_utf8,
|
||||
relchk,
|
||||
sanitize_fn,
|
||||
statdir,
|
||||
ub64enc,
|
||||
uncyg,
|
||||
@@ -106,6 +108,7 @@ SEESLOG = " (see fileserver log for details)"
|
||||
SSEELOG = " ({})".format(SEE_LOG)
|
||||
BAD_CFG = "invalid config; {}".format(SEE_LOG)
|
||||
SBADCFG = " ({})".format(BAD_CFG)
|
||||
REDUP_E2 = "WARNING: [/%s] needs 'e2ds' for redup from 'ref' and/or to 'sym'"
|
||||
|
||||
PTN_U_GRP = re.compile(r"\$\{u(%[+-][^}]+)\}")
|
||||
PTN_G_GRP = re.compile(r"\$\{g(%[+-][^}]+)\}")
|
||||
@@ -424,6 +427,7 @@ class VFS(object):
|
||||
self.root = self
|
||||
self.dev = 0 # st_dev
|
||||
self.nodes: dict[str, VFS] = {} # child nodes
|
||||
self.lnodes: Optional[dict[bytes, VFS]] = None # lowercase
|
||||
self.histtab: dict[str, str] = {} # all realpath->histpath
|
||||
self.dbpaths: dict[str, str] = {} # all realpath->dbpath
|
||||
self.dbv: Optional[VFS] = None # closest full/non-jump parent
|
||||
@@ -466,6 +470,7 @@ class VFS(object):
|
||||
self.canonical = self._canonical_null
|
||||
self.dcanonical = self._dcanonical_null
|
||||
|
||||
self.get = self._get
|
||||
self.get_dbv = self._get_dbv
|
||||
self.ls = self._ls
|
||||
|
||||
@@ -573,6 +578,11 @@ class VFS(object):
|
||||
if name in self.nodes:
|
||||
return self.nodes[name]._find(rem)
|
||||
|
||||
if self.lnodes:
|
||||
zs = name.casefold().encode("ascii", "ignore")
|
||||
if zs in self.lnodes:
|
||||
return self.lnodes[zs]._find(rem)
|
||||
|
||||
return self, vpath
|
||||
|
||||
def can_access(
|
||||
@@ -594,7 +604,7 @@ class VFS(object):
|
||||
ret += "A"
|
||||
return ret
|
||||
|
||||
def get(
|
||||
def _get(
|
||||
self,
|
||||
vpath: str,
|
||||
uname: str,
|
||||
@@ -636,6 +646,42 @@ class VFS(object):
|
||||
|
||||
return vn, rem
|
||||
|
||||
def _get_xvd(
|
||||
self,
|
||||
vpath: str,
|
||||
uname: str,
|
||||
will_read: bool,
|
||||
will_write: bool,
|
||||
will_move: bool = False,
|
||||
will_del: bool = False,
|
||||
will_get: bool = False,
|
||||
err: int = 403,
|
||||
) -> tuple["VFS", str]:
|
||||
zvr = self._get(
|
||||
vpath, uname, will_read, will_write, will_move, will_del, will_get, err
|
||||
)
|
||||
if not zvr:
|
||||
return None
|
||||
vn, rem = zvr
|
||||
if "xvd" in vn.flags and vn.realpath:
|
||||
avn = vn.chk_ap(vn.canonical(rem))
|
||||
if avn and avn.vpath == vn.vpath:
|
||||
return vn, rem
|
||||
if (
|
||||
not avn
|
||||
or (will_read and uname not in avn.axs.uread)
|
||||
or (will_write and uname not in avn.axs.uwrite)
|
||||
or (will_move and uname not in avn.axs.umove)
|
||||
or (will_del and uname not in avn.axs.udel)
|
||||
or (will_get and uname not in avn.axs.uget)
|
||||
):
|
||||
if not err:
|
||||
return None
|
||||
raise Pebkac(
|
||||
err, "permission denied for [/%s]" % (vjoin(vn.vpath, rem))
|
||||
)
|
||||
return vn, rem
|
||||
|
||||
def _get_share_src(self, vrem: str) -> tuple["VFS", str]:
|
||||
src = self.shr_src
|
||||
if not src:
|
||||
@@ -671,8 +717,6 @@ class VFS(object):
|
||||
if lfn == zs.lower():
|
||||
hit = zs
|
||||
break
|
||||
if not hit:
|
||||
return True # NFC/NFD or something, can't be helped either way
|
||||
if self.log:
|
||||
t = "returning 404 due to underlying case-insensitive filesystem:\n http-req: %r\n local-fs: %r"
|
||||
self.log("vfs", t % (fn, hit))
|
||||
@@ -763,6 +807,32 @@ class VFS(object):
|
||||
real = [x for x in real if os.path.basename(x[0]) in self.shr_files]
|
||||
return abspath, real, {}
|
||||
|
||||
def _ls_xvd(
|
||||
self,
|
||||
rem: str,
|
||||
uname: str,
|
||||
scandir: bool,
|
||||
permsets: list[list[bool]],
|
||||
lstat: bool = False,
|
||||
throw: bool = False,
|
||||
) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]:
|
||||
dirpath, real0, virt_vis = self._ls(rem, uname, scandir, permsets, True, throw)
|
||||
lns = [x for x in real0 if stat.S_ISLNK(x[1].st_mode)]
|
||||
if not lns:
|
||||
return dirpath, real0, virt_vis
|
||||
real = [x for x in real0 if not stat.S_ISLNK(x[1].st_mode)]
|
||||
ps = (permsets[0] + [False] * 5)[:5] # rwmdg
|
||||
for fn, _ in lns:
|
||||
ap = absreal(os.path.join(dirpath, fn))
|
||||
vn = self.chk_ap(ap)
|
||||
try:
|
||||
if vn.get("", uname, *ps):
|
||||
real.append((fn, bos.stat(ap)))
|
||||
except:
|
||||
pass
|
||||
real.sort()
|
||||
return dirpath, real, virt_vis
|
||||
|
||||
def _ls(
|
||||
self,
|
||||
rem: str,
|
||||
@@ -784,6 +854,12 @@ class VFS(object):
|
||||
if not rem:
|
||||
# no vfs nodes in the list of real inodes
|
||||
real = [x for x in real if x[0] not in self.nodes]
|
||||
if self.lnodes:
|
||||
real = [
|
||||
x
|
||||
for x in real
|
||||
if x[0].casefold().encode("ascii", "ignore") not in self.lnodes
|
||||
]
|
||||
|
||||
dbv = self.dbv or self
|
||||
for name, vn2 in sorted(self.nodes.items()):
|
||||
@@ -857,12 +933,16 @@ class VFS(object):
|
||||
self.log("vfs.walk", t % (seen[-1], fsroot, self.vpath, rem), 3)
|
||||
return
|
||||
|
||||
if "xdev" in self.flags or "xvol" in self.flags:
|
||||
if "xvd" in self.flags:
|
||||
ps = (permsets[0] + [False] * 5)[:5] # rwmdg
|
||||
rm1 = []
|
||||
for le in vfs_ls:
|
||||
ap = absreal(os.path.join(fsroot, le[0]))
|
||||
vn2 = self.chk_ap(ap)
|
||||
if not vn2 or not vn2.get("", uname, True, False, err=0):
|
||||
try:
|
||||
if not vn2.get("", uname, *ps):
|
||||
raise Exception()
|
||||
except:
|
||||
rm1.append(le)
|
||||
_ = [vfs_ls.remove(x) for x in rm1] # type: ignore
|
||||
|
||||
@@ -949,6 +1029,22 @@ class VFS(object):
|
||||
for d in [{"vp": v, "ap": a, "st": n} for v, a, n in ret2]:
|
||||
yield d
|
||||
|
||||
def sanitize_fn(self, fn: str, rem: Any = None) -> str:
|
||||
fn = sanitize_fn(fn, self.flags["fsnt"])
|
||||
if rem:
|
||||
return fn
|
||||
if fn in self.nodes or (
|
||||
self.lnodes and fn.casefold().encode("ascii", "ignore") in self.lnodes
|
||||
):
|
||||
raise Pebkac(400, "bad filename")
|
||||
return fn
|
||||
|
||||
def chk_fn(self, fn: str) -> None:
|
||||
if fn in self.nodes or (
|
||||
self.lnodes and fn.casefold().encode("ascii", "ignore") in self.lnodes
|
||||
):
|
||||
raise Pebkac(400, "bad filename")
|
||||
|
||||
def chk_ap(self, ap: str, st: Optional[os.stat_result] = None) -> Optional["VFS"]:
|
||||
aps = ap + os.sep
|
||||
if "xdev" in self.flags and not ANYWIN:
|
||||
@@ -2039,6 +2135,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 +2483,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():
|
||||
@@ -2762,7 +2868,7 @@ 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)'
|
||||
if not (enshare and vp.startswith(shrs)):
|
||||
if not (enshare and vol.vpath.startswith(shrs)):
|
||||
self.log(t % (k, vol.vpath), 1)
|
||||
vol.flags.pop(k)
|
||||
|
||||
@@ -2904,6 +3010,17 @@ class AuthSrv(object):
|
||||
if ccs == "y":
|
||||
vol.flags["bcasechk"] = True
|
||||
|
||||
ptn = re.compile(r"^(,no|,sym|,hard|,ref)+=(sym|hard|ref)$")
|
||||
ptn2 = re.compile(r"(no|ref).*=|=sym")
|
||||
for vol in vfs.all_nodes.values():
|
||||
zs = vol.flags.get("redup")
|
||||
if zs and not ptn.match("," + zs):
|
||||
t = "invalid redup for volume /%s: %r"
|
||||
self.log(t % (vol.vpath, zs), 1)
|
||||
errors = True
|
||||
if zs and "e2ds" not in vol.flags and ptn2.search(zs):
|
||||
self.log(REDUP_E2 % (vol.vpath,), 3)
|
||||
|
||||
tags = self.args.mtp or []
|
||||
tags = [x.split("=")[0] for x in tags]
|
||||
tags = [y for x in tags for y in x.split(",")]
|
||||
@@ -2929,6 +3046,14 @@ class AuthSrv(object):
|
||||
excl.extend([r"^\.", r"/\."])
|
||||
vol.flags["srch_re_nodot"] = re.compile("|".join(excl))
|
||||
|
||||
for vol in list(vfs.all_nodes.values()):
|
||||
if "xvd" in vol.flags:
|
||||
for vol in vfs.all_nodes.values():
|
||||
vol.get = vol._get_xvd
|
||||
if vol.ls.__name__ == "_ls":
|
||||
vol.ls = vol._ls_xvd
|
||||
break
|
||||
|
||||
have_daw = False
|
||||
for vol in vfs.all_nodes.values():
|
||||
daw = vol.flags.get("daw") or self.args.daw
|
||||
@@ -2946,6 +3071,9 @@ class AuthSrv(object):
|
||||
errors = True
|
||||
|
||||
for vol in vfs.all_nodes.values():
|
||||
for zs in vf_bmap().values():
|
||||
if vol.flags.get(zs) in (None, False, ""):
|
||||
vol.flags.pop(zs, None)
|
||||
for k in list(vol.flags.keys()):
|
||||
if re.match("^-[^-]+$", k):
|
||||
vol.flags.pop(k)
|
||||
@@ -2960,6 +3088,22 @@ class AuthSrv(object):
|
||||
for name in vol.axs.uread:
|
||||
vol.axs.udot.add(name)
|
||||
|
||||
zi = 1
|
||||
zs = "; you must also enable --hack-me-bro to accept the total loss of security"
|
||||
if self.args.hack_me_bro:
|
||||
zi = 3
|
||||
zs = ""
|
||||
|
||||
if self.args.smb:
|
||||
t = "smb is enabled; this has serious security issues which will not be fixed"
|
||||
self.log(t + zs, zi)
|
||||
if zs:
|
||||
errors = True
|
||||
|
||||
if self.args.tftp:
|
||||
t = "tftp is enabled; primitive protocol with primitive access-restrictions"
|
||||
self.log(t, 3)
|
||||
|
||||
if errors:
|
||||
sys.exit(1)
|
||||
|
||||
@@ -3212,6 +3356,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
|
||||
@@ -3241,6 +3392,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)
|
||||
@@ -3254,6 +3408,39 @@ class AuthSrv(object):
|
||||
cur.close()
|
||||
db.close()
|
||||
|
||||
zb1 = zb2 = False
|
||||
nodes = [self.vfs]
|
||||
while nodes:
|
||||
vn = nodes.pop()
|
||||
nodes += list(vn.nodes.values())
|
||||
if (
|
||||
not vn.realpath
|
||||
or "bcasechk" not in vn.flags
|
||||
or (not vn.axs.uread and not vn.axs.uwrite and not vn.axs.uget)
|
||||
):
|
||||
continue
|
||||
if ".hist" in vn.nodes and "e2d" in vn.flags:
|
||||
zb1 = True
|
||||
for zs, vnn in vn.nodes.items():
|
||||
if zs != ".hist" and (vnn.realpath or "").startswith(vn.realpath):
|
||||
t = "nested volume on casefolding filesystem: [/%s] inside [/%s]"
|
||||
self.log(t % (vnn.vpath, vn.vpath), 3)
|
||||
zb2 = True
|
||||
if PY2:
|
||||
continue
|
||||
vn.lnodes = {
|
||||
k.casefold().encode("ascii", "ignore"): v for k, v in vn.nodes.items()
|
||||
}
|
||||
if zb2 and PY2:
|
||||
t = "SECURITY WARNING: due to python2, shadowing will NOT provide much security:"
|
||||
self.log(t, 1)
|
||||
if zb2:
|
||||
t = "SECURITY WARNING: you have a case-insensitive / casefolding filesystem, and you also have nested volumes (subvolumes). This combination is NOT 100% bulletproof; it *might* be possible for someone to sneakily access a subvolume they're not supposed to have access to, if they have access to the parent volume."
|
||||
self.log(t, 3)
|
||||
if zb1:
|
||||
t = "you have a case-insensitive / casefolding filesystem, so it is recommended to set --hist C:\\copyparty-data\\ or another safe place"
|
||||
self.log(t, 3)
|
||||
|
||||
self.js_ls = {}
|
||||
self.js_htm = ""
|
||||
self.md_htm = ""
|
||||
@@ -3273,7 +3460,7 @@ class AuthSrv(object):
|
||||
"dth3x": vf["th3x"],
|
||||
"u2ts": vf["u2ts"],
|
||||
"shr_who": vf["shr_who"],
|
||||
"frand": bool(vf.get("rand")),
|
||||
"frand": "rand" in vf,
|
||||
"lifetime": vf.get("lifetime") or 0,
|
||||
"unlist": vf.get("unlist") or "",
|
||||
"sb_lg": "" if "no_sb_lg" in vf else (vf.get("lg_sbf") or "y"),
|
||||
@@ -3331,7 +3518,7 @@ class AuthSrv(object):
|
||||
"u2sz": self.args.u2sz,
|
||||
"u2ts": vf["u2ts"],
|
||||
"u2ow": vf["u2ow"],
|
||||
"frand": bool(vf.get("rand")),
|
||||
"frand": "rand" in vf,
|
||||
"lifetime": vn.js_ls["lifetime"],
|
||||
"u2sort": self.args.u2sort,
|
||||
}
|
||||
@@ -4079,6 +4266,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
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
@@ -63,6 +66,7 @@ def vf_bmap() -> dict[str, str]:
|
||||
"og_s_title",
|
||||
"opds",
|
||||
"rand",
|
||||
"redup_dry",
|
||||
"reflink",
|
||||
"rm_partial",
|
||||
"rmagic",
|
||||
@@ -143,6 +147,7 @@ def vf_vmap() -> dict[str, str]:
|
||||
"put_ck",
|
||||
"put_name",
|
||||
"readmes",
|
||||
"redup",
|
||||
"rotf_tz",
|
||||
"mv_retry",
|
||||
"rm_retry",
|
||||
@@ -226,6 +231,8 @@ flagcats = {
|
||||
"hardlinkonly": "dedup with hardlink only, never symlink;\nmake a full copy if hardlink is impossible",
|
||||
"reflink": "enable reflink-based file deduplication,\nwith fallback on full copy when that is impossible",
|
||||
"safededup": "verify on-disk data before using it for dedup",
|
||||
"redup=A,B=T": "convert dedup-type for existing files",
|
||||
"redup_dry": "dry-run mode for redup",
|
||||
"noclone": "take dupe data from clients, even if available on HDD",
|
||||
"nodupe": "rejects existing files (instead of linking/cloning them)",
|
||||
"nodupem": "rejects existing files during moves as well",
|
||||
@@ -297,6 +304,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 +324,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 +393,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",
|
||||
|
||||
+20
-1
@@ -10,7 +10,7 @@ import time
|
||||
from .__init__ import ANYWIN, FREEBSD, MACOS, UNIX
|
||||
from .authsrv import AXS, VFS, AuthSrv
|
||||
from .bos import bos
|
||||
from .util import chkcmd, json_hesc, min_ex, undot
|
||||
from .util import chkcmd, fsenc, json_hesc, min_ex, runcmd, undot
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Optional, Union
|
||||
@@ -18,6 +18,11 @@ if True: # pylint: disable=using-constant-test
|
||||
from .util import RootLogger, undot
|
||||
|
||||
|
||||
PTN_FILEFRAG = re.compile(
|
||||
r"^\s*[0-9]+:\s*[0-9]+\.\.\s*[0-9]+:\s*([0-9]+)\.\.\s*([0-9]+):\s*[0-9]+:\s*(.*)$"
|
||||
)
|
||||
|
||||
|
||||
class Fstab(object):
|
||||
def __init__(self, log: "RootLogger", args: argparse.Namespace, verbose: bool):
|
||||
self.log_func = log
|
||||
@@ -264,3 +269,17 @@ def ramdisk_chk(asrv: AuthSrv) -> None:
|
||||
vol.flags["fsnt"] = vol.js_ls["fsnt"] = htm["fsnt"] = fs
|
||||
vol.js_htm = json_hesc(json.dumps(htm))
|
||||
# md_htm is dontcare; only relevant for up2k request body
|
||||
|
||||
|
||||
def filefrag(ap: str) -> list[str]:
|
||||
rc, so, se = runcmd([b"filefrag", b"-v", fsenc(ap)], timeout=10)
|
||||
ptn = PTN_FILEFRAG
|
||||
ret = []
|
||||
for ln in so.split("\n"):
|
||||
m = ptn.match(ln)
|
||||
if not m:
|
||||
continue
|
||||
a, b, s = m.groups()
|
||||
if "shared" in s.strip().split(","):
|
||||
ret.append((a, b))
|
||||
return ret
|
||||
+22
-23
@@ -15,7 +15,7 @@ from pyftpdlib.handlers import FTPHandler
|
||||
from pyftpdlib.ioloop import IOLoop
|
||||
from pyftpdlib.servers import FTPServer
|
||||
|
||||
from .__init__ import PY2, TYPE_CHECKING
|
||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING
|
||||
from .authsrv import VFS
|
||||
from .bos import bos
|
||||
from .util import (
|
||||
@@ -29,7 +29,6 @@ from .util import (
|
||||
ipnorm,
|
||||
pybin,
|
||||
read_utf8,
|
||||
relchk,
|
||||
runhook,
|
||||
sanitize_fn,
|
||||
set_fperms,
|
||||
@@ -169,16 +168,23 @@ class FtpFs(AbstractedFS):
|
||||
d: bool = False,
|
||||
) -> tuple[str, VFS, str]:
|
||||
try:
|
||||
vpath = vpath.replace("\\", "/").strip("/")
|
||||
rd, fn = os.path.split(vpath)
|
||||
if relchk(rd):
|
||||
logging.warning("malicious vpath: %s", vpath)
|
||||
t = "Unsupported characters in [{}]"
|
||||
raise FSE(t.format(vpath), 1)
|
||||
# because:
|
||||
# * ftp-spec doesn't define sep
|
||||
# * windows filename limitations
|
||||
# need to allow/translate illegal fn,
|
||||
# but require filesystem-correct rd
|
||||
if ANYWIN or ("\\" in vpath and "/" not in vpath):
|
||||
vpath = vpath.replace("\\", "/")
|
||||
vpath = vpath.strip("/\\")
|
||||
if ANYWIN:
|
||||
rd, fn = os.path.split(vpath)
|
||||
fn = sanitize_fn(fn, "win")
|
||||
vpath = vjoin(rd, fn)
|
||||
# end of windows jank
|
||||
|
||||
fn = sanitize_fn(fn or "")
|
||||
vpath = vjoin(rd, fn)
|
||||
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
|
||||
vpath = vjoin(vfs.vpath, rem)
|
||||
rd, fn = os.path.split(vpath)
|
||||
if (
|
||||
w
|
||||
and fn.lower() in vfs.flags["emb_all"]
|
||||
@@ -193,18 +199,7 @@ class FtpFs(AbstractedFS):
|
||||
t = "No filesystem mounted at [{}]"
|
||||
raise FSE(t.format(vpath))
|
||||
|
||||
if "xdev" in vfs.flags or "xvol" in vfs.flags:
|
||||
ap = vfs.canonical(rem)
|
||||
avfs = vfs.chk_ap(ap)
|
||||
t = "Permission denied in [{}]"
|
||||
if not avfs:
|
||||
raise FSE(t.format(vpath), 1)
|
||||
|
||||
cr, cw, cm, cd, _, _, _, _, _ = avfs.uaxs[self.h.uname]
|
||||
if r and not cr or w and not cw or m and not cm or d and not cd:
|
||||
raise FSE(t.format(vpath), 1)
|
||||
else:
|
||||
ap = vfs.canonical(rem, False)
|
||||
ap = vfs.canonical(rem, False)
|
||||
|
||||
if "bcasechk" in vfs.flags and not vfs.casechk(rem, True):
|
||||
raise FSE("No such file or directory", 1)
|
||||
@@ -314,7 +309,11 @@ class FtpFs(AbstractedFS):
|
||||
|
||||
def chdir(self, path: str) -> None:
|
||||
nwd = join(self.cwd, path)
|
||||
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
|
||||
try:
|
||||
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
|
||||
except Exception as ex:
|
||||
raise FSE(str(ex))
|
||||
|
||||
if not vfs.realpath:
|
||||
self.cwd = nwd
|
||||
return
|
||||
|
||||
+169
-92
@@ -17,6 +17,7 @@ import threading # typechk
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from hmac import compare_digest as ct_eq
|
||||
from operator import itemgetter
|
||||
|
||||
import jinja2 # typechk
|
||||
@@ -34,7 +35,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
|
||||
@@ -104,13 +105,11 @@ from .util import (
|
||||
read_socket_chunked,
|
||||
read_socket_unbounded,
|
||||
read_utf8,
|
||||
relchk,
|
||||
ren_open,
|
||||
runhook,
|
||||
s2hms,
|
||||
s3enc,
|
||||
safe_mime,
|
||||
sanitize_fn,
|
||||
sanitize_vpath,
|
||||
sendfile_kern,
|
||||
sendfile_py,
|
||||
@@ -130,6 +129,7 @@ from .util import (
|
||||
vsplit,
|
||||
wunlink,
|
||||
yieldfile,
|
||||
zip_fi,
|
||||
)
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
@@ -202,12 +202,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 +385,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]
|
||||
@@ -733,6 +737,7 @@ class HttpCli(object):
|
||||
hpw = self.asrv.ah.hash(bauth)
|
||||
if self.asrv.iacct.get(hpw):
|
||||
break
|
||||
bauth = zs
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -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)
|
||||
@@ -875,10 +885,9 @@ class HttpCli(object):
|
||||
uparam["b"] = ""
|
||||
cookies["b"] = ""
|
||||
|
||||
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
|
||||
if vn.realpath and ("xdev" in vn.flags or "xvol" in vn.flags):
|
||||
ap = vn.canonical(rem)
|
||||
avn = vn.chk_ap(ap)
|
||||
vn, rem = self.asrv.vfs._get(self.vpath, self.uname, False, False)
|
||||
if vn.realpath and "xvd" in vn.flags:
|
||||
avn = vn.chk_ap(vn.canonical(rem))
|
||||
else:
|
||||
avn = vn
|
||||
|
||||
@@ -1155,14 +1164,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))
|
||||
|
||||
@@ -1365,6 +1377,7 @@ class HttpCli(object):
|
||||
if (
|
||||
self.args.pw_hdr in ih
|
||||
or re.sub(r"(:[0-9]{1,5})?/?$", "", origin) in good_origins
|
||||
or (self.args.acao_re and self.args.acao_re.match(origin))
|
||||
):
|
||||
good_origin = True
|
||||
bad_hdrs = ("",)
|
||||
@@ -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
|
||||
|
||||
@@ -1695,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(
|
||||
@@ -1703,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")
|
||||
@@ -1900,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:
|
||||
@@ -2454,6 +2474,9 @@ class HttpCli(object):
|
||||
if "%" in self.req:
|
||||
self.log(" `-- %r" % (self.vpath,))
|
||||
|
||||
if not self.avn:
|
||||
raise Pebkac(403, "no access")
|
||||
|
||||
if self.headers.get("expect", "").lower() == "100-continue":
|
||||
try:
|
||||
self.s.sendall(b"HTTP/1.1 100 Continue\r\n\r\n")
|
||||
@@ -2714,7 +2737,7 @@ class HttpCli(object):
|
||||
if rnd:
|
||||
fn = rand_name(fdir, fn, rnd)
|
||||
|
||||
fn = sanitize_fn(fn or "")
|
||||
fn = vfs.sanitize_fn(fn or "", rem)
|
||||
|
||||
path = os.path.join(fdir, fn)
|
||||
|
||||
@@ -3131,7 +3154,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:
|
||||
@@ -3143,6 +3167,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)
|
||||
@@ -3190,19 +3217,20 @@ class HttpCli(object):
|
||||
if "delete" in self.uparam:
|
||||
return self.handle_rm(body)
|
||||
|
||||
name = undot(body["name"])
|
||||
if "/" in name:
|
||||
raise Pebkac(400, "your client is old; press CTRL-SHIFT-R and try again")
|
||||
name = body["name"]
|
||||
if "/" in name or name in (".", ".."):
|
||||
raise Pebkac(400, "filename contains '/' or is invalid")
|
||||
|
||||
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||
fsnt = vfs.flags["fsnt"]
|
||||
if fsnt != "lin":
|
||||
vpath = self.vpath
|
||||
fsnt = self.vn.flags["fsnt"]
|
||||
if not VPTL_OS and fsnt != "lin":
|
||||
tl = VPTL_WIN if fsnt == "win" else VPTL_MAC
|
||||
rem = rem.translate(tl)
|
||||
name = name.translate(tl)
|
||||
vpath = vpath.translate(tl)
|
||||
|
||||
vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True)
|
||||
name = vfs.sanitize_fn(name, rem)
|
||||
dbv, vrem = vfs.get_dbv(rem)
|
||||
|
||||
name = sanitize_fn(name)
|
||||
if (
|
||||
not self.can_read
|
||||
and self.can_write
|
||||
@@ -3227,6 +3255,8 @@ class HttpCli(object):
|
||||
dst = vfs.canonical(rem)
|
||||
try:
|
||||
if not bos.path.isdir(dst):
|
||||
if "nosub" in vfs.flags:
|
||||
raise Pebkac(500, "no subdirectories allowed")
|
||||
bos.makedirs(dst, vf=vfs.flags)
|
||||
except OSError as ex:
|
||||
self.log("makedirs failed %r" % (dst,))
|
||||
@@ -3238,6 +3268,8 @@ class HttpCli(object):
|
||||
raise Pebkac(400, "some file got your folder name")
|
||||
|
||||
raise Pebkac(500, min_ex())
|
||||
except Pebkac:
|
||||
raise
|
||||
except:
|
||||
raise Pebkac(500, min_ex())
|
||||
|
||||
@@ -3332,7 +3364,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)
|
||||
@@ -3549,7 +3581,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"],))
|
||||
@@ -3558,9 +3594,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)
|
||||
vpath = vjoin(vfs.vpath, rem)
|
||||
vfs, rem = self.asrv.vfs.get(session["vp"], self.uname, False, True)
|
||||
ap = vfs.canonical(rem)
|
||||
st = bos.stat(ap)
|
||||
|
||||
@@ -3743,12 +3777,11 @@ class HttpCli(object):
|
||||
def _mkdir(self, vpath: str, dav: bool = False) -> bool:
|
||||
nullwrite = self.args.nw
|
||||
self.gctx = vpath
|
||||
vpath = sanitize_vpath(undot(vpath))
|
||||
vpath = sanitize_vpath(undot(vpath), self.vn.flags["fsnt"])
|
||||
vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True)
|
||||
if "nosub" in vfs.flags:
|
||||
raise Pebkac(403, "mkdir is forbidden below this folder")
|
||||
|
||||
rem = sanitize_vpath(rem)
|
||||
fn = vfs.canonical(rem)
|
||||
|
||||
if not nullwrite:
|
||||
@@ -3795,10 +3828,12 @@ class HttpCli(object):
|
||||
t = "you can only create %s files because you don't have the delete-permission"
|
||||
raise Pebkac(400, t % (vfs.flags["rw_edit"].replace(",", "/")))
|
||||
|
||||
sanitized = sanitize_fn(new_file)
|
||||
sanitized = vfs.sanitize_fn(new_file, rem)
|
||||
fdir = vfs.canonical(rem)
|
||||
fn = os.path.join(fdir, sanitized)
|
||||
|
||||
self.asrv.vfs.get(vjoins(vfs.vpath, rem, sanitized), self.uname, False, True)
|
||||
|
||||
for hn in ("xbu", "xau"):
|
||||
xxu = vfs.flags.get(hn)
|
||||
if xxu:
|
||||
@@ -3861,7 +3896,7 @@ class HttpCli(object):
|
||||
rnd = 0
|
||||
else:
|
||||
rnd = int(self.uparam.get("rand") or self.headers.get("rand") or 0)
|
||||
if vfs.flags.get("rand"): # force-enable
|
||||
if "rand" in vfs.flags: # force-enable
|
||||
rnd = max(rnd, vfs.flags["nrand"])
|
||||
|
||||
zs = self.uparam.get("life", self.headers.get("life", ""))
|
||||
@@ -3950,12 +3985,19 @@ class HttpCli(object):
|
||||
# fallthrough
|
||||
|
||||
fdir = fdir_base
|
||||
fname = sanitize_fn(p_file or "")
|
||||
fname = vfs.sanitize_fn(p_file or "", rem)
|
||||
suffix = "-%.6f-%s" % (time.time(), dip)
|
||||
if p_file and not nullwrite:
|
||||
if rnd:
|
||||
fname = rand_name(fdir, fname, rnd)
|
||||
|
||||
self.can_delete = (
|
||||
self.uname
|
||||
in self.asrv.vfs.get(
|
||||
vjoin(upload_vpath, fname), self.uname, False, True
|
||||
)[0].axs.udel
|
||||
)
|
||||
|
||||
abspath = os.path.join(fdir, fname)
|
||||
open_args = {"fdir": fdir, "suffix": suffix, "vf": vfs.flags}
|
||||
|
||||
@@ -4277,6 +4319,7 @@ class HttpCli(object):
|
||||
else:
|
||||
self.redirect(
|
||||
self.vpath,
|
||||
suf="?b=u" if self.ouparam.get("b") == "u" else "",
|
||||
msg=msg + suf,
|
||||
flavor="return to",
|
||||
click=False,
|
||||
@@ -4312,7 +4355,7 @@ class HttpCli(object):
|
||||
bos.makedirs(fp, vf=vfs.flags)
|
||||
|
||||
fp = os.path.join(fp, fn)
|
||||
rem = "{}/{}".format(rp, fn).strip("/")
|
||||
rem = vjoin(rp, fn)
|
||||
dbv, vrem = vfs.get_dbv(rem)
|
||||
|
||||
if not self.can_delete and (
|
||||
@@ -4330,6 +4373,8 @@ class HttpCli(object):
|
||||
self.reply(response.encode("utf-8"))
|
||||
return True
|
||||
|
||||
self.asrv.vfs.get(vjoin(vfs.vpath, rem), self.uname, True, True)
|
||||
|
||||
srv_lastmod = -1.0
|
||||
srv_lastmod3 = -1
|
||||
try:
|
||||
@@ -4374,7 +4419,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()
|
||||
@@ -4397,7 +4442,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)))
|
||||
@@ -4555,7 +4600,7 @@ class HttpCli(object):
|
||||
ap = vn.canonical(self.rem)
|
||||
|
||||
zs = self.gen_fk(2, self.args.dk_salt, ap, 0, 0)[:dk_len]
|
||||
if req == zs:
|
||||
if ct_eq(req, zs):
|
||||
return True
|
||||
|
||||
t = "wrong dirkey, want %s, got %s\n vp: %r\n ap: %r"
|
||||
@@ -4583,7 +4628,7 @@ class HttpCli(object):
|
||||
alg, self.args.fk_salt, ap, st.st_size, 0 if ANYWIN else st.st_ino
|
||||
)[:fk_len]
|
||||
|
||||
if req == zs:
|
||||
if ct_eq(req, zs):
|
||||
return True
|
||||
|
||||
t = "wrong filekey, want %s, got %s\n vp: %r\n ap: %r"
|
||||
@@ -4957,14 +5002,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:
|
||||
@@ -5016,15 +5062,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
|
||||
@@ -5230,6 +5281,8 @@ class HttpCli(object):
|
||||
self.send_headers("oh_f", length=upper - lower, status=status, mime=mime)
|
||||
wr_slp = self.args.s_wr_slp
|
||||
wr_sz = self.args.s_wr_sz
|
||||
dls = self.conn.hsrv.dls
|
||||
dl_id = self.dl_id
|
||||
file_size = job["size"]
|
||||
chunk_size = up2k_chunksize(file_size)
|
||||
num_need = -1
|
||||
@@ -5249,7 +5302,7 @@ class HttpCli(object):
|
||||
if job:
|
||||
self.pipes.set(req_path, job)
|
||||
|
||||
if not job:
|
||||
if not job or not job["hash"]:
|
||||
t = "pipe: OK, upload has finished; yeeting remainder"
|
||||
self.log(t, 2)
|
||||
data_end = file_size
|
||||
@@ -5346,6 +5399,9 @@ class HttpCli(object):
|
||||
broken = True
|
||||
break
|
||||
|
||||
if dl_id:
|
||||
dls[dl_id] = (time.time(), lower)
|
||||
|
||||
if lower < upper and not broken:
|
||||
with open_nolock(req_path, "rb") as f:
|
||||
remains = sendfile_py(
|
||||
@@ -5357,8 +5413,8 @@ class HttpCli(object):
|
||||
wr_sz,
|
||||
wr_slp,
|
||||
not self.args.no_poll,
|
||||
self.conn.hsrv.dls,
|
||||
self.dl_id,
|
||||
dls,
|
||||
dl_id,
|
||||
)
|
||||
|
||||
spd = self._spd((upper - lower) - remains)
|
||||
@@ -6149,6 +6205,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
|
||||
@@ -6625,6 +6683,7 @@ class HttpCli(object):
|
||||
act = self.uparam["eshare"]
|
||||
if act == "rm":
|
||||
cur.execute("delete from sh where k = ?", (skey,))
|
||||
cur.execute("delete from sf where k = ?", (skey,))
|
||||
if skey in self.asrv.vfs.nodes[self.args.shr.strip("/")].nodes:
|
||||
reload = True
|
||||
else:
|
||||
@@ -6871,11 +6930,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
|
||||
|
||||
@@ -7038,17 +7103,16 @@ class HttpCli(object):
|
||||
e2d = "e2d" in vn.flags
|
||||
e2t = "e2t" in vn.flags
|
||||
|
||||
og_fn = ""
|
||||
add_og = "og" in vn.flags
|
||||
if add_og:
|
||||
if "th" in self.uparam or "raw" in self.uparam or "opds" in self.uparam:
|
||||
add_og = False
|
||||
elif vn.flags["og_ua"]:
|
||||
add_og = vn.flags["og_ua"].search(self.ua)
|
||||
og_fn = ""
|
||||
|
||||
if "v" in self.uparam:
|
||||
add_og = True
|
||||
og_fn = ""
|
||||
|
||||
if "b" in self.uparam and "norobots" not in vn.flags:
|
||||
self.out_headers["X-Robots-Tag"] = "noindex, nofollow"
|
||||
@@ -7078,10 +7142,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:
|
||||
@@ -7220,7 +7284,7 @@ class HttpCli(object):
|
||||
if use_dirkey:
|
||||
is_dk = True
|
||||
elif self.can_get and "doc" in self.uparam:
|
||||
zs = vjoin(self.vpath, self.uparam["doc"]) + "?v"
|
||||
zs = vjoin(self.vpath, self.uparam["doc"].split("//")[-1]) + "?v"
|
||||
return self.redirect(zs, flavor="redirecting to", use302=True)
|
||||
elif not self.can_write:
|
||||
return self.tx_404(True)
|
||||
@@ -7379,10 +7443,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,
|
||||
@@ -7445,20 +7509,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
|
||||
@@ -7571,6 +7633,7 @@ class HttpCli(object):
|
||||
return self.tx_file("oh_f", ap) # is no-cache
|
||||
|
||||
if icur:
|
||||
assert idx # type: ignore # !rm
|
||||
mte = vn.flags.get("mte") or {}
|
||||
tagset: set[str] = set()
|
||||
rd = vrem
|
||||
@@ -7829,7 +7892,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:
|
||||
@@ -7839,8 +7902,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"
|
||||
@@ -7893,6 +7956,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
|
||||
@@ -7935,7 +8002,7 @@ class HttpCli(object):
|
||||
else:
|
||||
j2a["og_url"] = j2a["og_raw"] = url_base
|
||||
|
||||
if not vn.flags.get("og_no_head"):
|
||||
if "og_no_head" not in vn.flags:
|
||||
ogh = {"twitter:card": "summary"}
|
||||
|
||||
title = str(vn.flags.get("og_title") or "")
|
||||
@@ -8004,7 +8071,7 @@ class HttpCli(object):
|
||||
while title.endswith(" - "):
|
||||
title = title[:3]
|
||||
|
||||
if vn.flags.get("og_s_title") or not title:
|
||||
if "og_s_title" in vn.flags or not title:
|
||||
title = str(vn.flags.get("og_title") or "")
|
||||
|
||||
for tag, hname in tagmap.items():
|
||||
@@ -8030,3 +8097,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
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -51,7 +51,6 @@ except ImportError:
|
||||
SRV,
|
||||
TXT,
|
||||
A,
|
||||
Bimap,
|
||||
DNSHeader,
|
||||
DNSQuestion,
|
||||
DNSRecord,
|
||||
|
||||
+15
-8
@@ -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,17 +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")
|
||||
fi = zf.open("mergedimage.png")
|
||||
zi = zip_fi(zf, "mergedimage.png", maxsz)
|
||||
fi = zf.open(zi)
|
||||
|
||||
elif pk == "epub":
|
||||
fi = get_cover_from_epub(log, abspath)
|
||||
@@ -438,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
|
||||
@@ -452,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
|
||||
@@ -476,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
@@ -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
|
||||
|
||||
|
||||
+55
-29
@@ -8,7 +8,6 @@ import os
|
||||
import select
|
||||
import socket
|
||||
import time
|
||||
from threading import ExceptHookArgs
|
||||
|
||||
import paramiko
|
||||
import paramiko.common
|
||||
@@ -38,7 +37,6 @@ from .util import (
|
||||
ipnorm,
|
||||
min_ex,
|
||||
read_utf8,
|
||||
relchk,
|
||||
runhook,
|
||||
sanitize_fn,
|
||||
ub64enc,
|
||||
@@ -56,6 +54,23 @@ if True: # pylint: disable=using-constant-test
|
||||
SATTR = paramiko.sftp_attr.SFTPAttributes
|
||||
|
||||
|
||||
try:
|
||||
from paramiko.rsakey import serialization
|
||||
|
||||
_load_der_pk = serialization.load_der_private_key
|
||||
|
||||
def fastloader(*a, **ka):
|
||||
ka0 = ka.copy()
|
||||
try:
|
||||
ka["unsafe_skip_rsa_key_validation"] = True
|
||||
return _load_der_pk(*a, **ka)
|
||||
except:
|
||||
return _load_der_pk(*a, **ka0)
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class SSH_Srv(paramiko.ServerInterface):
|
||||
def __init__(self, hub: "SvcHub", addr: Any):
|
||||
self.hub = hub
|
||||
@@ -296,6 +311,17 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
|
||||
if self.uname == LEELOO_DALLAS:
|
||||
raise Exception("send her back")
|
||||
|
||||
cm = 0
|
||||
for zs, zi in (
|
||||
("s", SATTR.FLAG_SIZE),
|
||||
("t", SATTR.FLAG_AMTIME),
|
||||
("p", SATTR.FLAG_PERMISSIONS),
|
||||
("u", SATTR.FLAG_UIDGID),
|
||||
):
|
||||
if zs in self.args.sftp_chattr:
|
||||
cm |= zi
|
||||
self.chattr_mask = cm
|
||||
|
||||
self.vols = [
|
||||
vp
|
||||
for vp, vn in self.asrv.vfs.all_vols.items()
|
||||
@@ -321,15 +347,23 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
|
||||
m: bool = False,
|
||||
d: bool = False,
|
||||
) -> tuple[str, VFS, str]:
|
||||
vpath = vpath.replace(os.sep, "/").strip("/")
|
||||
rd, fn = os.path.split(vpath)
|
||||
if relchk(rd):
|
||||
self.log("malicious vpath: %s", vpath)
|
||||
raise Exception("Unsupported characters in [%s]" % (vpath,))
|
||||
# because:
|
||||
# * maybe some clients use \
|
||||
# * windows filename limitations
|
||||
# need to allow/translate illegal fn,
|
||||
# but require filesystem-correct rd
|
||||
if ANYWIN or ("\\" in vpath and "/" not in vpath):
|
||||
vpath = vpath.replace("\\", "/")
|
||||
vpath = vpath.strip("/\\")
|
||||
if ANYWIN:
|
||||
rd, fn = os.path.split(vpath)
|
||||
fn = sanitize_fn(fn, "win")
|
||||
vpath = vjoin(rd, fn)
|
||||
# end of windows jank
|
||||
|
||||
fn = sanitize_fn(fn or "")
|
||||
vpath = vjoin(rd, fn)
|
||||
vn, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
|
||||
vpath = vjoin(vn.vpath, rem)
|
||||
rd, fn = os.path.split(vpath)
|
||||
if (
|
||||
w
|
||||
and fn.lower() in vn.flags["emb_all"]
|
||||
@@ -344,18 +378,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
|
||||
# return "", vn, rem
|
||||
raise OSError(errno.ENOENT, "no filesystem mounted at [/%s]" % (vpath,))
|
||||
|
||||
if "xdev" in vn.flags or "xvol" in vn.flags:
|
||||
ap = vn.canonical(rem)
|
||||
avn = vn.chk_ap(ap)
|
||||
t = "Permission denied in [{}]"
|
||||
if not avn:
|
||||
raise OSError(errno.EPERM, "permission denied in [/%s]" % (vpath,))
|
||||
|
||||
cr, cw, cm, cd, _, _, _, _, _ = avn.uaxs[self.uname]
|
||||
if r and not cr or w and not cw or m and not cm or d and not cd:
|
||||
raise OSError(errno.EPERM, "permission denied in [/%s]" % (vpath,))
|
||||
else:
|
||||
ap = vn.canonical(rem, False)
|
||||
ap = vn.canonical(rem, False)
|
||||
|
||||
if "bcasechk" in vn.flags and not vn.casechk(rem, True):
|
||||
raise OSError(errno.ENOENT, "file does not exist case-sensitively")
|
||||
@@ -496,8 +519,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"
|
||||
@@ -669,8 +691,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
|
||||
def _mkdir(self, vp: str, attr: SATTR) -> int:
|
||||
self.log("mkdir(%s)" % (vp,))
|
||||
try:
|
||||
vn, rem = self.asrv.vfs.get(vp, self.uname, False, True)
|
||||
ap = vn.canonical(rem, False)
|
||||
ap, vn, _ = self.v2a(vp, w=True)
|
||||
bos.makedirs(ap, vf=vn.flags) # filezilla expects this
|
||||
if attr is not None:
|
||||
paramiko.SFTPServer.set_file_attr(ap, attr)
|
||||
@@ -693,8 +714,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
|
||||
def _rmdir(self, vp: str) -> int:
|
||||
self.log("rmdir(%s)" % (vp,))
|
||||
try:
|
||||
vn, rem = self.asrv.vfs.get(vp, self.uname, False, False, will_del=True)
|
||||
ap = os.path.join(vn.realpath, rem)
|
||||
ap, _, _ = self.v2a(vp, d=True)
|
||||
bos.rmdir(ap)
|
||||
return SFTP_OK
|
||||
except Pebkac as ex:
|
||||
@@ -715,8 +735,8 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
|
||||
def _chattr(self, vp: str, attr: SATTR) -> int:
|
||||
self.log("chattr(%s, %s)" % (vp, attr))
|
||||
try:
|
||||
vn, rem = self.asrv.vfs.get(vp, self.uname, False, True, will_del=True)
|
||||
ap = os.path.join(vn.realpath, rem)
|
||||
attr._flags &= self.chattr_mask
|
||||
ap, _, _ = self.v2a(vp, w=True, d=True)
|
||||
paramiko.SFTPServer.set_file_attr(ap, attr)
|
||||
return SFTP_OK
|
||||
except Pebkac as ex:
|
||||
@@ -776,6 +796,10 @@ class Sftpd(object):
|
||||
self.log("cannot start sftp-server; no compatible IPs in -i", 1)
|
||||
return
|
||||
|
||||
if args.sftp_fastldr:
|
||||
serialization.load_der_private_key = fastloader # type: ignore
|
||||
else:
|
||||
self.log("loading hostkeys...")
|
||||
self.hostkeys = []
|
||||
hostkeytypes = (
|
||||
("ed25519", "Ed25519Key", {}), # best
|
||||
@@ -803,6 +827,8 @@ class Sftpd(object):
|
||||
self.hostkeys.append(pkey)
|
||||
if args.sftpv:
|
||||
self.log("loaded hostkey %r" % (pkey,))
|
||||
if args.sftp_fastldr:
|
||||
serialization.load_der_private_key = _load_der_pk # type: ignore
|
||||
|
||||
ips = list(ODict.fromkeys(ips)) # dedup
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import ctypes
|
||||
import sys
|
||||
from ctypes import wintypes
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
@@ -9,6 +10,9 @@ if True: # pylint: disable=using-constant-test
|
||||
|
||||
from . import _shared as shared
|
||||
|
||||
WINVER = sys.getwindowsversion()
|
||||
LE_XP = WINVER.major < 6
|
||||
|
||||
NO_ERROR = 0
|
||||
ERROR_BUFFER_OVERFLOW = 111
|
||||
MAX_ADAPTER_NAME_LENGTH = 256
|
||||
@@ -83,7 +87,10 @@ def enumerate_interfaces_of_adapter(
|
||||
t = "sockaddr_to_ip({}) returned None"
|
||||
raise Exception(t.format(address.Address.lpSockaddr))
|
||||
|
||||
network_prefix = address.OnLinkPrefixLength
|
||||
if LE_XP:
|
||||
network_prefix = 16 if str(ip).startswith("169.254") else 24
|
||||
else:
|
||||
network_prefix = address.OnLinkPrefixLength
|
||||
yield shared.IP(ip, network_prefix, nice_name)
|
||||
|
||||
|
||||
|
||||
+60
-37
@@ -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.
|
||||
@@ -162,6 +174,7 @@ class SvcHub(object):
|
||||
self.is_dut = False # running in unittest; always False
|
||||
self.stopping = False
|
||||
self.stopped = False
|
||||
self.init_mutex = threading.Lock()
|
||||
self.reload_mutex = threading.Lock()
|
||||
self.retcode = 0
|
||||
self.httpsrv_up = 0
|
||||
@@ -441,6 +454,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 +505,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
|
||||
}
|
||||
@@ -509,9 +536,6 @@ class SvcHub(object):
|
||||
|
||||
self.tftpd: Optional[Tftpd] = None
|
||||
|
||||
if args.sftp or args.ftp or args.ftps or args.tftp:
|
||||
Daemon(self.start_ftpd, "start_tftpd")
|
||||
|
||||
if args.smb:
|
||||
# impacket.dcerpc is noisy about listen timeouts
|
||||
sto = socket.getdefaulttimeout()
|
||||
@@ -835,29 +859,19 @@ class SvcHub(object):
|
||||
cur.close()
|
||||
db.close()
|
||||
|
||||
def start_ftpd(self) -> None:
|
||||
time.sleep(30)
|
||||
|
||||
if hasattr(self, "sftpd") and not self.sftpd:
|
||||
self.restart_sftpd()
|
||||
|
||||
if hasattr(self, "ftpd") and not self.ftpd:
|
||||
self.restart_ftpd()
|
||||
|
||||
if hasattr(self, "tftpd") and not self.tftpd:
|
||||
self.restart_tftpd()
|
||||
|
||||
def restart_sftpd(self) -> None:
|
||||
if not hasattr(self, "sftpd"):
|
||||
return
|
||||
|
||||
from .sftpd import Sftpd
|
||||
|
||||
if self.sftpd:
|
||||
return # todo
|
||||
with self.init_mutex:
|
||||
if self.sftpd:
|
||||
return # todo
|
||||
|
||||
self.sftpd = Sftpd(self)
|
||||
self.sftpd.run()
|
||||
|
||||
self.sftpd = Sftpd(self)
|
||||
self.sftpd.run()
|
||||
self.log("root", "started SFTPd")
|
||||
|
||||
def restart_ftpd(self) -> None:
|
||||
@@ -866,13 +880,15 @@ class SvcHub(object):
|
||||
|
||||
from .ftpd import Ftpd
|
||||
|
||||
if self.ftpd:
|
||||
return # todo
|
||||
with self.init_mutex:
|
||||
if self.ftpd:
|
||||
return # todo
|
||||
|
||||
if not os.path.exists(self.args.cert):
|
||||
ensure_cert(self.log, self.args)
|
||||
if not os.path.exists(self.args.cert):
|
||||
ensure_cert(self.log, self.args)
|
||||
|
||||
self.ftpd = Ftpd(self)
|
||||
|
||||
self.ftpd = Ftpd(self)
|
||||
self.log("root", "started FTPd")
|
||||
|
||||
def restart_tftpd(self) -> None:
|
||||
@@ -881,10 +897,11 @@ class SvcHub(object):
|
||||
|
||||
from .tftpd import Tftpd
|
||||
|
||||
if self.tftpd:
|
||||
return # todo
|
||||
with self.init_mutex:
|
||||
if self.tftpd:
|
||||
return # todo
|
||||
|
||||
self.tftpd = Tftpd(self)
|
||||
self.tftpd = Tftpd(self)
|
||||
|
||||
def thr_httpsrv_up(self) -> None:
|
||||
time.sleep(1 if self.args.ign_ebind_all else 5)
|
||||
@@ -1127,6 +1144,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
|
||||
|
||||
@@ -1260,7 +1281,7 @@ class SvcHub(object):
|
||||
|
||||
TH_BWRAP[:] = al.th_bwrap_b
|
||||
|
||||
zs = "dav_ua1 lf_url sus_urls nonsus_urls ua_nodav ua_nodoc ua_nozip"
|
||||
zs = "acao_re dav_ua1 lf_url sus_urls nonsus_urls ua_nodav ua_nodoc ua_nozip"
|
||||
for k in zs.split(" "):
|
||||
vs = getattr(al, k)
|
||||
if not vs or vs == "no":
|
||||
@@ -1383,18 +1404,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
-1
@@ -104,7 +104,7 @@ def gen_hdr(
|
||||
ret += spack(b"<LL", vsz, vsz)
|
||||
|
||||
# windows support (the "?" replace below too)
|
||||
fn = sanitize_to(fn, VPTL_WIN)
|
||||
fn = sanitize_to(fn, VPTL_WIN, "win")
|
||||
bfn = fn.encode("utf-8" if utf8 else "cp437", "replace").replace(b"?", b"_")
|
||||
|
||||
# add ntfs (0x24) and/or unix (0x10) extrafields for utc, add z64 if requested
|
||||
|
||||
+3
-1
@@ -22,7 +22,6 @@ from .util import (
|
||||
Netdev,
|
||||
atomic_move,
|
||||
chkcmd,
|
||||
get_adapters,
|
||||
list_nics,
|
||||
min_ex,
|
||||
sunpack,
|
||||
@@ -88,6 +87,9 @@ class TcpSrv(object):
|
||||
successful_binds = 0
|
||||
try:
|
||||
for ipa in ips:
|
||||
if ipa == "no":
|
||||
self.args.ign_ebind_all = True
|
||||
continue
|
||||
try:
|
||||
self._listen(ipa, port)
|
||||
ok[ipa].append(port)
|
||||
|
||||
+5
-1
@@ -275,7 +275,11 @@ class Tftpd(object):
|
||||
if not vfs.realpath:
|
||||
raise Exception("unmapped vfs")
|
||||
|
||||
return vfs, vpath, vfs.canonical(rem)
|
||||
ap = vfs.canonical(rem)
|
||||
if "bcasechk" in vfs.flags and not vfs.casechk(rem, True):
|
||||
raise Exception("file not found")
|
||||
|
||||
return vfs, vpath, ap
|
||||
|
||||
def _ls(self, vpath: str, raddress: str, rport: int, force=False) -> Any:
|
||||
# generate file listing if vpath is dir.txt and return as file object
|
||||
|
||||
+9
-2
@@ -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
|
||||
|
||||
@@ -97,7 +102,9 @@ class ThumbCli(object):
|
||||
):
|
||||
sfmt = "j"
|
||||
|
||||
if sfmt == "x" and not self.can_jxl:
|
||||
if sfmt == "x" and (
|
||||
not self.can_jxl or (not H_PIL_JXL and ext in self.fmt_raw)
|
||||
):
|
||||
sfmt = "w"
|
||||
|
||||
vf_crop = dbv.flags["crop"]
|
||||
|
||||
+104
-34
@@ -38,6 +38,7 @@ from .util import (
|
||||
afsenc,
|
||||
atomic_move,
|
||||
fsenc,
|
||||
loadpy,
|
||||
min_ex,
|
||||
runcmd,
|
||||
statdir,
|
||||
@@ -200,11 +201,11 @@ try:
|
||||
# reduces glibc RAM usage from 4.7 to 3.5 GiB ...yep, still bonkers
|
||||
os.environ["VIPS_CONCURRENCY"] = "1"
|
||||
|
||||
HAVE_VIPS = True
|
||||
import pyvips
|
||||
|
||||
pyvips.cache_set_max(0)
|
||||
logging.getLogger("pyvips").setLevel(logging.WARNING)
|
||||
HAVE_VIPS = True
|
||||
except Exception as e:
|
||||
HAVE_VIPS = False
|
||||
if not isinstance(e, ImportError):
|
||||
@@ -218,10 +219,10 @@ try:
|
||||
if PRTY_NO_RAWPY:
|
||||
raise Exception()
|
||||
|
||||
HAVE_RAWPY = True
|
||||
import rawpy
|
||||
|
||||
logging.getLogger("rawpy").setLevel(logging.WARNING)
|
||||
HAVE_RAWPY = True
|
||||
except:
|
||||
HAVE_RAWPY = False
|
||||
|
||||
@@ -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)
|
||||
|
||||
+225
-50
@@ -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
|
||||
@@ -20,10 +17,10 @@ from copy import deepcopy
|
||||
from queue import Queue
|
||||
|
||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, UNIX, WINDOWS, E
|
||||
from .authsrv import LEELOO_DALLAS, SEESLOG, VFS, AuthSrv
|
||||
from .authsrv import LEELOO_DALLAS, REDUP_E2, SEESLOG, VFS, AuthSrv
|
||||
from .bos import bos
|
||||
from .cfg import vf_bmap, vf_cmap, vf_vmap
|
||||
from .fsutil import Fstab
|
||||
from .fsutil import Fstab, filefrag
|
||||
from .mtag import MParser, MTag
|
||||
from .util import (
|
||||
E_FS_CRIT,
|
||||
@@ -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:
|
||||
@@ -884,17 +887,14 @@ class Up2k(object):
|
||||
|
||||
def _expr_idx_filter(self, flags: dict[str, Any]) -> tuple[bool, dict[str, Any]]:
|
||||
if not self.no_expr_idx:
|
||||
return False, flags
|
||||
return False, dict(flags)
|
||||
|
||||
ret = {k: v for k, v in flags.items() if not k.startswith("e2t")}
|
||||
if len(ret) == len(flags):
|
||||
return False, flags
|
||||
|
||||
return True, ret
|
||||
return len(ret) != len(flags), flags
|
||||
|
||||
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
|
||||
@@ -909,7 +909,7 @@ class Up2k(object):
|
||||
|
||||
with self.mutex:
|
||||
if gid != self.gid:
|
||||
return False
|
||||
return
|
||||
|
||||
if self.pp:
|
||||
continue
|
||||
@@ -948,6 +948,9 @@ class Up2k(object):
|
||||
with self.mutex, self.reg_mutex:
|
||||
# only need to protect register_vpath but all in one go feels right
|
||||
for vol in vols:
|
||||
if vol.realpath:
|
||||
self.flags[vol.realpath] = dict(vol.flags)
|
||||
|
||||
if bos.path.isfile(vol.realpath):
|
||||
self.volstate[vol.vpath] = "online (just-a-file)"
|
||||
t = "NOTE: volume [/%s] is a file, not a folder"
|
||||
@@ -997,6 +1000,21 @@ class Up2k(object):
|
||||
with self.mutex, self.reg_mutex:
|
||||
self._drop_caches()
|
||||
|
||||
def setstate(vol, n):
|
||||
t = ""
|
||||
if n < 1 and "e2v" in vol.flags:
|
||||
t += "+e2v"
|
||||
if n < 2 and vol.flags.get("redup"):
|
||||
t += "+redup"
|
||||
if n < 3 and "e2ts" in vol.flags:
|
||||
t += "+tags"
|
||||
|
||||
if t:
|
||||
t = "online (%s pending)" % (t[1:],)
|
||||
else:
|
||||
t = "online, idle"
|
||||
self.volstate[vol.vpath] = t
|
||||
|
||||
for vol in vols:
|
||||
if self.stop or gid != self.gid:
|
||||
break
|
||||
@@ -1013,26 +1031,16 @@ class Up2k(object):
|
||||
if vac:
|
||||
need_vac[vol] = True
|
||||
|
||||
if "e2v" in vol.flags:
|
||||
t = "online (integrity-check pending)"
|
||||
elif "e2ts" in vol.flags:
|
||||
t = "online (tags pending)"
|
||||
else:
|
||||
t = "online, idle"
|
||||
|
||||
self.volstate[vol.vpath] = t
|
||||
setstate(vol, 0)
|
||||
|
||||
self._unblock()
|
||||
|
||||
# file contents verification
|
||||
for vol in vols:
|
||||
if self.stop:
|
||||
break
|
||||
|
||||
if "e2v" not in vol.flags:
|
||||
if self.stop or "e2v" not in vol.flags:
|
||||
continue
|
||||
|
||||
t = "online (verifying integrity)"
|
||||
t = "online (doing e2v)"
|
||||
self.volstate[vol.vpath] = t
|
||||
self.log("{} [{}]".format(t, vol.realpath))
|
||||
|
||||
@@ -1041,12 +1049,23 @@ class Up2k(object):
|
||||
self.log("modified {} entries in the db".format(nmod), 3)
|
||||
need_vac[vol] = True
|
||||
|
||||
if "e2ts" in vol.flags:
|
||||
t = "online (tags pending)"
|
||||
else:
|
||||
t = "online, idle"
|
||||
setstate(vol, 1)
|
||||
|
||||
# convert dedup type
|
||||
for vol in vols:
|
||||
if self.stop or not vol.flags.get("redup"):
|
||||
continue
|
||||
|
||||
t = "online (doing redup)"
|
||||
self.volstate[vol.vpath] = t
|
||||
self.log("{} [{}]".format(t, vol.realpath))
|
||||
|
||||
try:
|
||||
with self.mutex, self.reg_mutex:
|
||||
self._redup(vol, vols)
|
||||
except:
|
||||
self.log("redup failed: " + min_ex(), 1)
|
||||
setstate(vol, 2)
|
||||
|
||||
# open the rest + do any e2ts(a)
|
||||
needed_mutagen = False
|
||||
@@ -1081,7 +1100,7 @@ class Up2k(object):
|
||||
cur.execute("vacuum")
|
||||
|
||||
if self.stop:
|
||||
return False
|
||||
return
|
||||
|
||||
for vol in all_vols.values():
|
||||
if vol.flags["dbd"] == "acid":
|
||||
@@ -1111,7 +1130,7 @@ class Up2k(object):
|
||||
self.log("checkpoint failed: {}".format(ex), 3)
|
||||
|
||||
if self.stop:
|
||||
return False
|
||||
return
|
||||
|
||||
self.pp.end = True
|
||||
|
||||
@@ -1131,7 +1150,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]
|
||||
@@ -1155,12 +1174,12 @@ class Up2k(object):
|
||||
vpath = k
|
||||
|
||||
_, flags = self._expr_idx_filter(flags)
|
||||
n4g = bool(flags.get("noforget"))
|
||||
n4g = "noforget" in flags
|
||||
|
||||
ft = "\033[0;32m{}{:.0}"
|
||||
ff = "\033[0;35m{}{:.0}"
|
||||
fv = "\033[0;36m{}:\033[90m{}"
|
||||
zs = "bcasechk du_iwho emb_all emb_lgs emb_mds ext_th_d html_head html_head_d html_head_s ls_q_m put_name2 mv_re_r mv_re_t rm_re_r rm_re_t oh_f oh_g rw_edit_set srch_re_dots srch_re_nodot th_coversd th_coversl th_covers_set th_coversd_set zipmax zipmaxn_v zipmaxs_v"
|
||||
zs = "bcasechk du_iwho emb_all emb_lgs emb_mds ext_th_d html_head html_head_d html_head_s ls_q_m put_name2 mv_re_r mv_re_t rm_re_r rm_re_t oh_f oh_g rw_edit_set srch_re_dots srch_re_nodot th_coversd th_coversl th_covers_set th_coversd_set xvd zipmax zipmaxn_v zipmaxs_v"
|
||||
fx = set(zs.split())
|
||||
fd = vf_bmap()
|
||||
fd.update(vf_cmap())
|
||||
@@ -1294,6 +1313,17 @@ class Up2k(object):
|
||||
self.log(t % (vpath, ex), 1)
|
||||
return None
|
||||
|
||||
if not (
|
||||
ANYWIN or UNIX or "hist_cow" in flags or "up2k.db" in os.listdir(histpath)
|
||||
):
|
||||
try:
|
||||
if self.fstab.get(histpath)[0] != "btrfs":
|
||||
raise Exception()
|
||||
runcmd([b"chattr", b"-c", fsenc(histpath)], 1) # compression and...
|
||||
runcmd([b"chattr", b"+C", fsenc(histpath)], 1) # no-cow are incompat
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
cur = self._open_db_wd(db_path)
|
||||
|
||||
@@ -1377,14 +1407,15 @@ class Up2k(object):
|
||||
top = vol.realpath
|
||||
rei = vol.flags.get("noidx")
|
||||
reh = vol.flags.get("nohash")
|
||||
n4g = bool(vol.flags.get("noforget"))
|
||||
n4g = "noforget" in vol.flags
|
||||
ffat = "fat32" in vol.flags
|
||||
cst = bos.stat(top)
|
||||
dev = cst.st_dev if vol.flags.get("xdev") else 0
|
||||
dev = cst.st_dev if "xdev" in vol.flags else 0
|
||||
|
||||
with self.mutex:
|
||||
with self.reg_mutex:
|
||||
reg = self.register_vpath(top, vol.flags)
|
||||
self.flags[vol.realpath] = dict(vol.flags)
|
||||
|
||||
assert reg and self.pp # !rm
|
||||
cur, db_path = reg
|
||||
@@ -1439,7 +1470,7 @@ class Up2k(object):
|
||||
[],
|
||||
cst,
|
||||
dev,
|
||||
bool(vol.flags.get("xvol")),
|
||||
"xvol" in vol.flags,
|
||||
)
|
||||
if not n4g:
|
||||
n_rm = self._drop_lost(db.c, top, excl)
|
||||
@@ -2077,6 +2108,138 @@ class Up2k(object):
|
||||
|
||||
return len(rewark) + len(f404)
|
||||
|
||||
def _redup(self, vol: VFS, vols: list[VFS]) -> None:
|
||||
logmsg = "redup: replacing %r with %r"
|
||||
dry = "redup_dry" in vol.flags
|
||||
if dry:
|
||||
logmsg += " #dry"
|
||||
|
||||
zs, to = vol.flags["redup"].split("=")
|
||||
zsl = zs.split(",")
|
||||
cref = "no" in zsl or "ref" in zsl
|
||||
csym = "sym" in zsl
|
||||
chard = "hard" in zsl
|
||||
chard0 = chard
|
||||
|
||||
vf = vol.flags.copy()
|
||||
vf["dedup"] = 1
|
||||
for zs in "hardlink hardlinkonly reflink".split():
|
||||
vf.pop(zs, None)
|
||||
|
||||
if to == "ref":
|
||||
vf["reflink"] = 1
|
||||
elif to == "hard":
|
||||
vf["hardlinkonly"] = vf["hardlink"] = 1
|
||||
elif to == "sym":
|
||||
pass
|
||||
else:
|
||||
raise Exception("target type not implemented: " + to)
|
||||
|
||||
if (csym or chard) and to != "sym":
|
||||
self.log("redup: stage 1 begin")
|
||||
if to == "hard":
|
||||
chard = False
|
||||
for x in vol.walk("", "", [], LEELOO_DALLAS, [[]], 2, True, True, True):
|
||||
vn, _, _, atop, lsf, lsd, lsv = x
|
||||
if vn is not vol:
|
||||
lsd[:] = []
|
||||
lsv.clear()
|
||||
continue
|
||||
for fn, st in lsf:
|
||||
if (csym and stat.S_ISLNK(st.st_mode)) or (
|
||||
chard and st.st_nlink > 1
|
||||
):
|
||||
ap = os.path.join(atop, fn)
|
||||
self.log(logmsg % (ap, absreal(ap)))
|
||||
if dry:
|
||||
continue
|
||||
ap2 = tempfile.NamedTemporaryFile(
|
||||
prefix="r,", dir=atop, delete=False
|
||||
).name
|
||||
self._symlink(ap, ap2, vf, True, True, st.st_mtime)
|
||||
st1 = bos.stat(ap)
|
||||
st2 = bos.lstat(ap2)
|
||||
if st1.st_size != st2.st_size or stat.S_ISLNK(st2.st_mode):
|
||||
wunlink(self.log, ap2, vf)
|
||||
raise Exception("redup: clone failed; giving up")
|
||||
wunlink(self.log, ap, vf)
|
||||
bos.rename(ap2, ap)
|
||||
|
||||
if cref or to == "sym":
|
||||
self.log("redup: stage 2 begin")
|
||||
if "e2ds" not in vol.flags:
|
||||
self.log(REDUP_E2 % (vol.vpath,), 3)
|
||||
cur = self.cur[vol.realpath]
|
||||
curs = [(vol, cur.connection.cursor())]
|
||||
if to != "sym":
|
||||
for v2 in vols:
|
||||
if vol is not v2 and v2.realpath in self.cur:
|
||||
curs += [(v2, self.cur[v2.realpath])]
|
||||
nrem = cur.execute("select count(w) from up").fetchone()[0]
|
||||
self.log("redup [/%s]: %d files left" % (vol.vpath, nrem))
|
||||
q2 = "select rd, fn from up where substr(w,1,16) = ? and w=?"
|
||||
for w, rd, fn in cur.execute("select w, rd, fn from up"):
|
||||
w16 = w[:16]
|
||||
for v2, c2 in curs:
|
||||
hits = c2.execute(q2, (w16, w)).fetchall()
|
||||
if not hits:
|
||||
continue
|
||||
rd2, fn2 = hits[0]
|
||||
if fn == fn2 and rd == rd2 and vol is v2:
|
||||
continue
|
||||
apt = ""
|
||||
try:
|
||||
rd, fn = s3dec(rd, fn)
|
||||
rd2, fn2 = s3dec(rd2, fn2)
|
||||
fp1 = os.path.join(vol.realpath, rd, fn)
|
||||
fp2 = os.path.join(v2.realpath, rd2, fn2)
|
||||
st1 = bos.lstat(fp1)
|
||||
if fp1 == fp2 or stat.S_ISLNK(st1.st_mode):
|
||||
continue # self(?), or redup failed during walk
|
||||
if to == "hard" and st1.st_nlink >= len(hits):
|
||||
continue
|
||||
ap1 = absreal(fp1)
|
||||
ap2 = absreal(fp2)
|
||||
st2 = bos.lstat(ap2)
|
||||
if stat.S_ISLNK(st2.st_mode):
|
||||
continue # dead hit
|
||||
if to == "ref" and ap1 != ap2:
|
||||
try:
|
||||
frag1 = filefrag(ap1)
|
||||
if frag1:
|
||||
frag2 = filefrag(ap2)
|
||||
if frag1 == frag2:
|
||||
continue
|
||||
except Exception as ex:
|
||||
self.log("redup: filefrag failed (OK); %r" % (ex,))
|
||||
for ap in (ap1,) if ap1 == ap2 else (ap1, ap2):
|
||||
self.log("redup: integrity-checking %r" % (ap,))
|
||||
zsl, st = self._hashlist_from_file(ap)
|
||||
w2 = up2k_wark_from_hashlist(self.salt, st.st_size, zsl)
|
||||
if w2 != w:
|
||||
t = "db desync:\n%s %r\n%s db"
|
||||
raise Exception(t % (w2, ap, w))
|
||||
|
||||
self.log(logmsg % (ap1, ap2))
|
||||
if dry:
|
||||
continue
|
||||
apt = tempfile.NamedTemporaryFile(
|
||||
prefix="r,", dir=os.path.dirname(fp1), delete=False
|
||||
).name
|
||||
self._symlink(ap2, apt, vf, True, True, st1.st_mtime)
|
||||
zsl, st = self._hashlist_from_file(apt)
|
||||
w2 = up2k_wark_from_hashlist(self.salt, st.st_size, zsl)
|
||||
if w != w2:
|
||||
raise Exception("bad checksum after copy?!")
|
||||
wunlink(self.log, ap1, vf)
|
||||
bos.rename(apt, ap1)
|
||||
break
|
||||
except Exception as ex:
|
||||
self.log("redup: skipping match due to " + str(ex))
|
||||
if apt and os.path.exists(apt):
|
||||
os.unlink(apt)
|
||||
continue
|
||||
|
||||
def _build_tags_index(self, vol: VFS) -> tuple[int, int, bool]:
|
||||
ptop = vol.realpath
|
||||
with self.mutex, self.reg_mutex:
|
||||
@@ -3064,17 +3227,20 @@ class Up2k(object):
|
||||
jcur = self.cur.get(ptop)
|
||||
reg = self.registry[ptop]
|
||||
vfs = self.vfs.all_vols[cj["vtop"]]
|
||||
n4g = bool(vfs.flags.get("noforget"))
|
||||
noclone = bool(vfs.flags.get("noclone"))
|
||||
rand = vfs.flags.get("rand") or cj.get("rand")
|
||||
n4g = "noforget" in vfs.flags
|
||||
noclone = "noclone" in vfs.flags
|
||||
rand = "rand" in vfs.flags or cj.get("rand")
|
||||
lost: list[tuple["sqlite3.Cursor", str, str]] = []
|
||||
|
||||
safe_dedup = vfs.flags.get("safededup") or 50
|
||||
data_ok = safe_dedup < 10 or n4g
|
||||
|
||||
vols = [(ptop, jcur)] if jcur else []
|
||||
if vfs.flags.get("xlink"):
|
||||
if vols and ("xlink" in vfs.flags or "reflink" in vfs.flags):
|
||||
vols += [(k, v) for k, v in self.cur.items() if k != ptop]
|
||||
if "xlink" not in vfs.flags:
|
||||
zs = self.fstab.get(ptop)[1]
|
||||
vols = [x for x in vols if self.fstab.get(x[0])[1] == zs]
|
||||
|
||||
if noclone:
|
||||
wark = up2k_wark_from_metadata(
|
||||
@@ -3384,7 +3550,7 @@ class Up2k(object):
|
||||
job["ptop"] = vfs.realpath
|
||||
job["vtop"] = vfs.vpath
|
||||
job["prel"] = rem
|
||||
job["name"] = sanitize_fn(job["name"])
|
||||
job["name"] = vfs.sanitize_fn(job["name"], rem)
|
||||
ud2 = (vfs.vpath, job["prel"], job["name"])
|
||||
if ud1 != ud2:
|
||||
# print(json.dumps(job, sort_keys=True, indent=4))
|
||||
@@ -3564,7 +3730,7 @@ class Up2k(object):
|
||||
st = bos.stat(fp)
|
||||
try:
|
||||
vrel = vjoin(job["prel"], fname)
|
||||
xlink = bool(vf.get("xlink"))
|
||||
xlink = "xlink" in vf
|
||||
cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, vrel)
|
||||
self._forget_file(ptop, vrel, vf, cur, wark, True, st.st_size, xlink)
|
||||
except Exception as ex:
|
||||
@@ -3619,7 +3785,7 @@ class Up2k(object):
|
||||
if rm and bos.path.exists(dst):
|
||||
wunlink(self.log, dst, flags)
|
||||
|
||||
if not is_mv and not flags.get("dedup"):
|
||||
if not is_mv and "dedup" not in flags:
|
||||
raise Exception("dedup is disabled in config")
|
||||
|
||||
if "reflink" in flags:
|
||||
@@ -4136,6 +4302,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(
|
||||
@@ -4240,7 +4409,7 @@ class Up2k(object):
|
||||
is_dir = stat.S_ISDIR(st.st_mode)
|
||||
except:
|
||||
# NOTE: "file not found" *sftpd
|
||||
raise Pebkac(400, "file not found on disk (already deleted?)")
|
||||
raise Pebkac(404, "file not found on disk (already deleted?)")
|
||||
|
||||
if "bcasechk" in vn.flags and not vn.casechk(rem, False):
|
||||
raise Pebkac(400, "file does not exist case-sensitively")
|
||||
@@ -4317,7 +4486,7 @@ class Up2k(object):
|
||||
cur = None
|
||||
try:
|
||||
ptop = dbv.realpath
|
||||
xlink = bool(dbv.flags.get("xlink"))
|
||||
xlink = "xlink" in dbv.flags
|
||||
cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, volpath)
|
||||
self._forget_file(
|
||||
ptop, volpath, dbv.flags, cur, wark, True, st.st_size, xlink
|
||||
@@ -4521,6 +4690,12 @@ class Up2k(object):
|
||||
self.log("not found in src db: %r" % (svp,))
|
||||
|
||||
try:
|
||||
if svn_dbv != dvn and not (
|
||||
"xlink" in dvn.flags
|
||||
or "reflink" in dvn.flags
|
||||
or "hardlinkonly" in dvn.flags
|
||||
):
|
||||
raise OSError(errno.EXDEV, "different volumes")
|
||||
if is_link and st != stl:
|
||||
# relink non-broken symlinks to still work after the move,
|
||||
# but only resolve 1st level to maintain relativity
|
||||
@@ -4798,7 +4973,7 @@ class Up2k(object):
|
||||
raise Pebkac(400, t)
|
||||
self._copy_tags(c1, c2, w)
|
||||
|
||||
xlink = bool(svn.flags.get("xlink"))
|
||||
xlink = "xlink" in svn.flags
|
||||
|
||||
with self.reg_mutex:
|
||||
has_dupes = self._forget_file(
|
||||
@@ -5264,7 +5439,7 @@ class Up2k(object):
|
||||
job["ptop"] = vfs.realpath
|
||||
job["vtop"] = vfs.vpath
|
||||
job["prel"] = rem
|
||||
job["name"] = sanitize_fn(job["name"])
|
||||
job["name"] = vfs.sanitize_fn(job["name"], rem)
|
||||
ud2 = (vfs.vpath, job["prel"], job["name"])
|
||||
if ud1 != ud2:
|
||||
job["vcfg"] = vf
|
||||
|
||||
+102
-30
@@ -10,7 +10,6 @@ import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import mimetypes
|
||||
import os
|
||||
import platform
|
||||
@@ -180,12 +179,16 @@ 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
|
||||
except:
|
||||
HAVE_IFADDR = False
|
||||
IFADDR_VND = False
|
||||
|
||||
def get_adapters(include_unconfigured=False):
|
||||
return []
|
||||
@@ -195,10 +198,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 +216,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 +229,10 @@ try:
|
||||
raise Exception()
|
||||
|
||||
import magic
|
||||
|
||||
HAVE_MAGIC = True
|
||||
except:
|
||||
pass
|
||||
HAVE_MAGIC = False
|
||||
|
||||
if os.environ.get("PRTY_MODSPEC"):
|
||||
from inspect import getsourcefile
|
||||
@@ -283,7 +289,6 @@ try:
|
||||
raise Exception()
|
||||
|
||||
socket.inet_pton(socket.AF_INET6, "::1")
|
||||
HAVE_IPV6 = True
|
||||
|
||||
if GRAAL:
|
||||
try:
|
||||
@@ -298,13 +303,15 @@ try:
|
||||
return _inet_pton(fam, ip)
|
||||
|
||||
socket.inet_pton = inet_pton
|
||||
|
||||
HAVE_IPV6 = True
|
||||
except:
|
||||
HAVE_IPV6 = False
|
||||
|
||||
def inet_pton(fam, ip):
|
||||
return socket.inet_aton(ip)
|
||||
|
||||
socket.inet_pton = inet_pton
|
||||
HAVE_IPV6 = False
|
||||
|
||||
|
||||
try:
|
||||
@@ -352,19 +359,25 @@ 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:
|
||||
umktrans = str.maketrans
|
||||
|
||||
FNTL_WIN = umktrans('<>:|?*"\\/', "<>:|?*"\/")
|
||||
VPTL_WIN = umktrans('<>:|?*"\\', "<>:|?*"\")
|
||||
APTL_WIN = umktrans('<>:|?*"/', "<>:|?*"/")
|
||||
FNTL_MAC = VPTL_MAC = APTL_MAC = umktrans(":", ":")
|
||||
FNTL_OS = FNTL_WIN if ANYWIN else FNTL_MAC if MACOS else None
|
||||
VPTL_MAC = umktrans(":", ":")
|
||||
VPTL_OS = VPTL_WIN if ANYWIN else VPTL_MAC if MACOS else None
|
||||
APTL_OS = APTL_WIN if ANYWIN else APTL_MAC if MACOS else None
|
||||
|
||||
|
||||
BADNAMES = []
|
||||
if ANYWIN:
|
||||
BADNAMES += ["con", "prn", "aux", "nul"]
|
||||
BADNAMES += ["com%s" % (n,) for n in range(1, 10)]
|
||||
BADNAMES += ["lpt%s" % (n,) for n in range(1, 10)]
|
||||
BADNAMES += ["com%s" % (n,) for n in ("¹", "²", "³")]
|
||||
BADNAMES += ["lpt%s" % (n,) for n in ("¹", "²", "³")]
|
||||
BADNAMESET = set(BADNAMES)
|
||||
|
||||
|
||||
BOS_SEP = ("%s" % (os.sep,)).encode("ascii")
|
||||
@@ -445,6 +458,7 @@ IMPLICATIONS = [
|
||||
["no_logue", "no_logues"], # user-typo
|
||||
["nologues", "no_logues"], # user-typo
|
||||
["nologue", "no_logues"], # user-typo
|
||||
["nw", "redup_dry"],
|
||||
["plainlogue", "plainlogues"], # user-typo
|
||||
["plainreadmes", "plainreadme"], # user-typo
|
||||
["noscript", "plainlogues"],
|
||||
@@ -455,6 +469,8 @@ IMPLICATIONS = [
|
||||
["smbvvv", "smbvv"],
|
||||
["smbvv", "smbv"],
|
||||
["smbv", "smb"],
|
||||
["xdev", "xvd"],
|
||||
["xvol", "xvd"],
|
||||
["zv", "zmv"],
|
||||
["zv", "zsv"],
|
||||
["z", "zm"],
|
||||
@@ -2217,7 +2233,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")
|
||||
@@ -2421,7 +2437,7 @@ def uncyg(path: str) -> str:
|
||||
return "%s:\\%s" % (path[1], path[3:])
|
||||
|
||||
|
||||
def undot(path: str) -> str:
|
||||
def _undot(path: str) -> str:
|
||||
ret: list[str] = []
|
||||
for node in path.split("/"):
|
||||
if node == "." or not node:
|
||||
@@ -2437,30 +2453,56 @@ def undot(path: str) -> str:
|
||||
return "/".join(ret)
|
||||
|
||||
|
||||
def sanitize_fn(fn: str) -> str:
|
||||
fn = fn.replace("\\", "/").split("/")[-1]
|
||||
if APTL_OS:
|
||||
fn = sanitize_to(fn, APTL_OS)
|
||||
def _undot_win(path: str) -> str:
|
||||
ret: list[str] = []
|
||||
for node in path.replace("\\", "/").split("/"):
|
||||
if node == "..":
|
||||
if ret:
|
||||
ret.pop()
|
||||
continue
|
||||
|
||||
node = node.rstrip(". ")
|
||||
if node:
|
||||
ret.append(node)
|
||||
|
||||
return "/".join(ret)
|
||||
|
||||
|
||||
if ANYWIN:
|
||||
undot = _undot_win
|
||||
else:
|
||||
undot = _undot
|
||||
|
||||
|
||||
def sanitize_fn(fn: str, fsnt: Optional[str] = None) -> str:
|
||||
fn = fn.replace("\x00", "_").split("/")[-1]
|
||||
if fsnt and fsnt != "lin":
|
||||
fn = fn.translate(VPTL_WIN if fsnt == "win" or ANYWIN else VPTL_MAC)
|
||||
fn2 = fn.replace("\\", "/")
|
||||
if ANYWIN or fsnt == "win" or "/../" in fn2:
|
||||
fn = fn2.split("/")[-1].rstrip(". ")
|
||||
if VPTL_OS:
|
||||
fn = sanitize_to(fn, VPTL_OS)
|
||||
return fn.strip()
|
||||
|
||||
|
||||
def sanitize_to(fn: str, tl: dict[int, int]) -> str:
|
||||
def sanitize_to(fn: str, tl: dict[int, int], fsnt: Optional[str] = None) -> str:
|
||||
fn = fn.translate(tl)
|
||||
if ANYWIN:
|
||||
bad = ["con", "prn", "aux", "nul"]
|
||||
for n in range(1, 10):
|
||||
bad += ("com%s lpt%s" % (n, n)).split(" ")
|
||||
|
||||
if fn.lower().split(".")[0] in bad:
|
||||
if ANYWIN or fsnt == "win":
|
||||
fn = fn.rstrip(". ")
|
||||
if fn.lower().split(".")[0].strip() in BADNAMESET:
|
||||
fn = "_" + fn
|
||||
return fn
|
||||
|
||||
|
||||
def sanitize_vpath(vp: str) -> str:
|
||||
if not APTL_OS:
|
||||
def sanitize_vpath(vp: str, fsnt: Optional[str] = None) -> str:
|
||||
vptl = VPTL_OS
|
||||
if fsnt and fsnt != "lin":
|
||||
vptl = VPTL_WIN if fsnt == "win" or ANYWIN else VPTL_MAC
|
||||
if not vptl:
|
||||
return vp
|
||||
parts = vp.replace(os.sep, "/").split("/")
|
||||
ret = [sanitize_to(x, APTL_OS) for x in parts]
|
||||
ret = [sanitize_to(x, vptl, fsnt) for x in parts]
|
||||
return "/".join(ret)
|
||||
|
||||
|
||||
@@ -4044,9 +4086,9 @@ def runihook(
|
||||
}
|
||||
for x, vp, ap in zip(ups, vps, aps)
|
||||
]
|
||||
sp_ka["sin"] = json.dumps(ja).encode("utf-8", "replace")
|
||||
sp_ka["sin"] = json.dumps(ja).encode("utf-8", "replace") + b"\n"
|
||||
else:
|
||||
sp_ka["sin"] = b"\n".join(fsenc(x) for x in aps)
|
||||
sp_ka["sin"] = b"\n".join(fsenc(x) for x in aps) + b"\n"
|
||||
|
||||
if acmd[0].startswith("zmq:"):
|
||||
try:
|
||||
@@ -4394,6 +4436,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
|
||||
|
||||
@@ -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 $<
|
||||
|
||||
|
||||
@@ -705,6 +705,7 @@ window.baguetteBox = (function () {
|
||||
unbind(overlay, 'touchend', touchendHandler);
|
||||
unbind(document, 'focus', trapFocusInsideOverlay, true);
|
||||
timer.rm(rotn);
|
||||
clearTimeout(unrot_timer);
|
||||
}
|
||||
|
||||
function prepareOverlay(gallery, userOptions, cbz) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+70
-36
@@ -583,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",
|
||||
@@ -666,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",
|
||||
@@ -758,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;
|
||||
}
|
||||
@@ -1112,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>' +
|
||||
@@ -1246,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(' '));
|
||||
@@ -1792,7 +1795,7 @@ function MPlayer() {
|
||||
continue;
|
||||
|
||||
tid = tid.slice(1);
|
||||
if (r.tracks[tid])
|
||||
if (r.tracks[tid])
|
||||
order.push(tid);
|
||||
}
|
||||
r.order = order;
|
||||
@@ -3556,7 +3559,7 @@ function eval_hash() {
|
||||
if (hash0 && window.og_fn) {
|
||||
var all = msel.getall(), mi;
|
||||
for (var a = 0; a < all.length; a++)
|
||||
if (og_fn == uricom_dec(vsplit(all[a].vp)[1].split('?')[0])) {
|
||||
if (og_fn == all[a].fn) {
|
||||
mi = all[a];
|
||||
break;
|
||||
}
|
||||
@@ -4093,7 +4096,7 @@ var fileman = (function () {
|
||||
if (wr)
|
||||
html.push('<a href="#" class="btn">write-only</a>');
|
||||
|
||||
html.push('</td></tr></div');
|
||||
html.push('</td></tr><table></div>');
|
||||
shui.innerHTML = html.join('\n');
|
||||
|
||||
var sh_rand = ebi('sh_rand'),
|
||||
@@ -4251,9 +4254,6 @@ var fileman = (function () {
|
||||
s2d[a] = all.indexOf(sel[a]);
|
||||
|
||||
var vp = sel[a].vp;
|
||||
if (vp.endsWith('/'))
|
||||
vp = vp.slice(0, -1);
|
||||
|
||||
var vsp = vsplit(vp);
|
||||
if (base != vsp[0])
|
||||
return toast.err(0, esc('bug:\n' + base + '\n' + vsp[0]));
|
||||
@@ -4572,6 +4572,8 @@ var fileman = (function () {
|
||||
xhr.onload = xhr.onerror = rename_cb;
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
ire.oninput();
|
||||
};
|
||||
|
||||
r.delete = function (e) {
|
||||
@@ -4598,6 +4600,9 @@ var fileman = (function () {
|
||||
if (err !== 'xbd')
|
||||
toast.ok(2, L.fd_ok);
|
||||
|
||||
if (QS('#seldoc.sel'))
|
||||
thegrid.setvis(true);
|
||||
|
||||
treectl.goto();
|
||||
return;
|
||||
}
|
||||
@@ -5074,11 +5079,22 @@ var showfile = (function () {
|
||||
r.nmap = {
|
||||
'dockerfile': 'docker'
|
||||
};
|
||||
r.pmap = {
|
||||
'sh': 'bash',
|
||||
'ash': 'bash',
|
||||
'dash': 'bash',
|
||||
'luajit': 'lua',
|
||||
'node': 'js',
|
||||
};
|
||||
var x = txt_ext + ' ans c cfg conf cpp cs css diff glsl go html ini java js json jsx kt kts latex less lisp lua makefile md nasm nim nix py r rss rb ruby sass scss sql svg swift tex toml ts vhdl xml yaml zig';
|
||||
x = x.split(/ +/g);
|
||||
for (var a = 0; a < x.length; a++)
|
||||
if (!r.map["." + x[a]])
|
||||
r.map["." + x[a]] = x[a];
|
||||
x = 'awk bash lua make perl python ruby swift';
|
||||
x = x.split(/ +/g);
|
||||
for (var a = 0; a < x.length; a++)
|
||||
r.pmap[x[a]] = x[a];
|
||||
|
||||
r.sname = function (srch) {
|
||||
return srch.split(/[?&]doc=/)[1].split('&')[0];
|
||||
@@ -5087,7 +5103,7 @@ var showfile = (function () {
|
||||
if (window.og_fn) {
|
||||
var ext = og_fn.split(/\./g).pop();
|
||||
if (r.map['.' + ext])
|
||||
hist_replace(get_evpath() + '?doc=' + og_fn);
|
||||
hist_replace(get_evpath() + '?doc=' + uricom_enc(og_fn));
|
||||
}
|
||||
|
||||
window.Prism = { 'manual': true };
|
||||
@@ -5098,7 +5114,7 @@ var showfile = (function () {
|
||||
var m = /[?&]doc=([^&]+)/.exec(location.search);
|
||||
if (m) {
|
||||
setTimeout(function () {
|
||||
r.show(uricom_dec(m[1]), true);
|
||||
r.show(m[1], true);
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
@@ -5118,11 +5134,17 @@ var showfile = (function () {
|
||||
return !!/[?&]doc=/.exec(location.search);
|
||||
};
|
||||
|
||||
r.getlang = function (fn) {
|
||||
r.getlang = function (fn, txt) {
|
||||
fn = fn.toLowerCase();
|
||||
var ext = fn.slice(fn.lastIndexOf('.'));
|
||||
return r.map[ext] || r.nmap[fn];
|
||||
}
|
||||
var prog = txt && txt.startsWith('#!') && txt.split('\n')[0].replace(
|
||||
'#!/usr/bin/env', '').replace(
|
||||
'#!/usr/bin/', '').replace(
|
||||
'#!/bin/', '').trim().split(' ')[0];
|
||||
if (prog)
|
||||
prog = r.pmap[prog] || r.pmap[prog.replace(/[0-9].*/, '')];
|
||||
return r.map[ext] || r.nmap[fn] || prog;
|
||||
};
|
||||
|
||||
r.addlinks = function () {
|
||||
r.files = [];
|
||||
@@ -5135,7 +5157,7 @@ var showfile = (function () {
|
||||
if (!lang)
|
||||
continue;
|
||||
|
||||
r.files.push({ 'id': link.id, 'name': uricom_dec(fn) });
|
||||
r.files.push({ 'id': link.id, 'name': link.fn });
|
||||
|
||||
var ah = ebi(link.id),
|
||||
td = ah.closest('tr').getElementsByTagName('td')[0];
|
||||
@@ -5271,7 +5293,7 @@ var showfile = (function () {
|
||||
txt = doc[2],
|
||||
name = url.split('?')[0].split('/').pop(),
|
||||
tname = uricom_dec(name),
|
||||
lang = r.getlang(name),
|
||||
lang = r.getlang(name, txt),
|
||||
is_md = lang == 'md';
|
||||
|
||||
ebi('files').style.display = ebi('gfiles').style.display = ebi('lazy').style.display = ebi('pro').style.display = ebi('epi').style.display = 'none';
|
||||
@@ -5460,7 +5482,7 @@ var showfile = (function () {
|
||||
sel = false;
|
||||
|
||||
for (var b = 0; b < sels.length; b++)
|
||||
if (vsplit(sels[b].vp)[1] == lin)
|
||||
if (sels[b].fn == lin)
|
||||
sel = true;
|
||||
|
||||
clmod(lis[a], 'hl', lin == fn);
|
||||
@@ -5483,7 +5505,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);
|
||||
};
|
||||
@@ -5694,8 +5716,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)),
|
||||
@@ -6439,8 +6461,10 @@ var ahotkeys = function (e) {
|
||||
if (k == 'F2')
|
||||
return fileman.rename();
|
||||
|
||||
if (k == 'F4')
|
||||
if (k == 'F4') {
|
||||
enspin('t', 1);
|
||||
return treectl.goto();
|
||||
}
|
||||
|
||||
if (!treectl.hidden && (!sh || !thegrid.en)) {
|
||||
if (kl == 'a')
|
||||
@@ -7696,7 +7720,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 +
|
||||
@@ -7995,12 +8019,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);
|
||||
@@ -8626,7 +8650,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();
|
||||
@@ -8763,6 +8787,7 @@ var msel = (function () {
|
||||
item.id = links[a].getAttribute('id');
|
||||
item.sel = clgot(links[a].closest('tr'), 'sel');
|
||||
item.vp = href.indexOf('/') !== -1 ? href : vbase + href;
|
||||
item.fn = uricom_dec(href.split('/').pop());
|
||||
|
||||
if (dk) {
|
||||
var m = /[?&](k=[^&#]+)/.exec(qhref);
|
||||
@@ -9184,7 +9209,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--;
|
||||
@@ -9337,6 +9362,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;
|
||||
@@ -9832,6 +9858,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;
|
||||
@@ -9892,13 +9919,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);
|
||||
@@ -10030,19 +10064,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;
|
||||
@@ -10075,7 +10109,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";
|
||||
@@ -10085,7 +10119,7 @@ function reload_browser() {
|
||||
starty = pos.y;
|
||||
is_selma = true;
|
||||
ttimer = null;
|
||||
|
||||
|
||||
if (e.type === 'touchstart') {
|
||||
ttimer = setTimeout(function() {
|
||||
ttimer = null;
|
||||
@@ -10093,7 +10127,7 @@ function reload_browser() {
|
||||
}, lpdelay);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function start_drag() {
|
||||
if (dragging) return;
|
||||
|
||||
@@ -10104,7 +10138,7 @@ function reload_browser() {
|
||||
|
||||
ebi('gfiles').style.pointerEvents = 'none';
|
||||
}
|
||||
|
||||
|
||||
function sel_move(e) {
|
||||
if (!is_selma) return;
|
||||
var pos = getpp(e);
|
||||
@@ -10121,7 +10155,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();
|
||||
@@ -10170,7 +10204,7 @@ function reload_browser() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
dsel_init();
|
||||
})();
|
||||
|
||||
@@ -10213,7 +10247,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;
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -577,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请将其拖放到大上传按钮上",
|
||||
@@ -660,6 +661,7 @@ Ls.chi = {
|
||||
"rc_pla": "作为音频播放",
|
||||
"rc_txt": "在文本文件查看器中打开",
|
||||
"rc_md": "在 markdown 查看器中打开",
|
||||
"rc_wopi": "在 office 编辑器中打开", //m
|
||||
"rc_dl": "下载",
|
||||
"rc_zip": "下载为压缩包",
|
||||
"rc_cpl": "复制链接",
|
||||
|
||||
@@ -580,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í",
|
||||
@@ -663,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
|
||||
|
||||
@@ -576,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",
|
||||
@@ -659,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
|
||||
|
||||
@@ -576,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",
|
||||
@@ -659,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",
|
||||
|
||||
@@ -576,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",
|
||||
@@ -659,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",
|
||||
|
||||
@@ -576,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",
|
||||
@@ -659,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
|
||||
|
||||
@@ -576,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&drop μεταφορτώσεις",
|
||||
"u_notdir": "αυτός δεν είναι φάκελος!\n\nο browser σου είναι πολύ παλιός,\nδοκίμασε drag&drop αντ' αυτού",
|
||||
"u_uri": "για να κάνεις drag&drop εικόνων από άλλα παράθυρα browser,\nρίξ' τες πάνω στο μεγάλο κουμπί μεταφόρτωσης",
|
||||
@@ -659,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
|
||||
|
||||
@@ -577,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',
|
||||
@@ -660,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',
|
||||
|
||||
@@ -576,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",
|
||||
@@ -659,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
|
||||
|
||||
@@ -576,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大きなアップロードボタンにドロップしてください",
|
||||
@@ -659,6 +660,7 @@ Ls.jpn = {
|
||||
"rc_pla": "オーディオとして再生",
|
||||
"rc_txt": "ファイルビューアで開く",
|
||||
"rc_md": "markdownエディターで開く",
|
||||
"rc_wopi": "officeエディターで開く", //m
|
||||
"rc_dl": "ダウンロード",
|
||||
"rc_zip": "圧縮してダウンロード",
|
||||
"rc_cpl": "リンクをコピー",
|
||||
|
||||
@@ -576,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큰 업로드 버튼 위로 떨어뜨려주세요',
|
||||
@@ -659,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
|
||||
|
||||
@@ -576,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",
|
||||
@@ -659,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
|
||||
|
||||
@@ -573,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",
|
||||
@@ -656,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",
|
||||
|
||||
@@ -573,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",
|
||||
@@ -656,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",
|
||||
|
||||
@@ -579,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",
|
||||
@@ -662,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
|
||||
|
||||
@@ -576,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",
|
||||
@@ -659,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
-60
@@ -39,7 +39,7 @@ Ls.rus = {
|
||||
["ctrl-V", "вставить (переместить/копировать) сюда"],
|
||||
["Y", "скачать выделенное"],
|
||||
["F2", "переименовать выделенное"],
|
||||
["F4", "обновить"], //m
|
||||
["F4", "обновить"],
|
||||
|
||||
"выделение файлов",
|
||||
["пробел", "выделить/снять выделение с текущего файла"],
|
||||
@@ -85,8 +85,8 @@ Ls.rus = {
|
||||
["M", "закрыть файл"],
|
||||
["E", "отредактировать файл"],
|
||||
["S", "выделить файл"],
|
||||
["Y", "скачать текстовый файл"], //m
|
||||
["⇧ J", "приукрасить json"], //m
|
||||
["Y", "скачать текстовый файл"],
|
||||
["⇧ J", "форматировать json для чтения"],
|
||||
]
|
||||
],
|
||||
|
||||
@@ -112,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`"try unite"` = содержит именно «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": "остальные настройки",
|
||||
@@ -128,7 +128,7 @@ Ls.rus = {
|
||||
"ot_noie": 'Пожалуйста, используйте Chrome / Firefox / Edge',
|
||||
|
||||
"ab_mkdir": "создать папку",
|
||||
"ab_mkdoc": "создать текстовый файл", //m
|
||||
"ab_mkdoc": "создать текстовый файл",
|
||||
"ab_msg": "отправить сообщение в лог сервера",
|
||||
|
||||
"ay_path": "перейти к папкам",
|
||||
@@ -156,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с помощью упрощения интерфейса",
|
||||
@@ -207,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": "архивация папок",
|
||||
@@ -215,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(например, файл с >code<2>/code< в начале названия идёт перед >code<11>/code<)">нат. сорт.',
|
||||
"ct_utc": 'используйте UTC для всех временных меток">UTC', //m
|
||||
"ct_utc": 'показывать все даты в UTC">UTC',
|
||||
"ct_readme": 'показывать содержимое README.md в описании папки">📜 ридми',
|
||||
"ct_idxh": 'показывать страницу index.html в текущей папке вместо интерфейса">htm',
|
||||
"ct_sbars": 'показывать полосы прокрутки">⟊',
|
||||
@@ -277,8 +277,8 @@ Ls.rus = {
|
||||
"tt_dynt": "автоматическое расширение панели",
|
||||
"tt_wrap": "перенос слов",
|
||||
"tt_hover": "раскрывать обрезанные строки при наведении$N( ломает скроллинг, если $N курсор не в пустоте слева )",
|
||||
"tt_gauto": "показывать как сетку или список в зависимости от содержимого папки", //m
|
||||
"tt_gathr": "использовать сетку, если этот процент файлов — изображения/видео", //m
|
||||
"tt_gauto": "показывать сетку или список в зависимости от содержимого папки",
|
||||
"tt_gathr": "использовать сетку, если этот процент файлов — изображения/видео",
|
||||
|
||||
"ml_pmode": "в конце папки...",
|
||||
"ml_btns": "команды",
|
||||
@@ -287,7 +287,7 @@ Ls.rus = {
|
||||
"ml_tint": "затемн.",
|
||||
"ml_eq": "эквалайзер",
|
||||
"ml_drc": "компрессор",
|
||||
"ml_ss": "пропускать тишину", //m
|
||||
"ml_ss": "пропускать тишину",
|
||||
|
||||
"mt_loop": "повторять один трек\">🔁",
|
||||
"mt_one": "остановить после этого трека\">1️⃣",
|
||||
@@ -316,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": "не похоже, что ваше устройство поддерживает этот формат, но давайте попробуем и узнаем наверняка",
|
||||
@@ -325,11 +325,11 @@ Ls.rus = {
|
||||
"mt_tint": "непрозрачность фона (0-100) на полосе воспроизведения$N$Nделает буферизацию менее отвлекающей",
|
||||
"mt_eq": "`включить эквалайзер$N$Nboost `0` = стандартная громкость$N$Nwidth `1 ` = обычное стерео$Nwidth `0.5` = микширование левого и правого каналов на 50%$Nwidth `0 ` = моно$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": "воспроизвести этот музыкальный файл?",
|
||||
@@ -346,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": "больше аудио-файлов не найдено",
|
||||
@@ -366,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, вы скачаете этот временный файл, который с огромной вероятностью содержит лишь неполные данные.",
|
||||
|
||||
@@ -435,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} файлов сюда?",
|
||||
@@ -456,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ошибка ",
|
||||
@@ -469,7 +469,7 @@ Ls.rus = {
|
||||
"tvt_prev": "показать предыдущий документ$NГорячая клавиша: i\">⬆ пред",
|
||||
"tvt_next": "показать следующий документ$NГорячая клавиша: K\">⬇ след",
|
||||
"tvt_sel": "выбрать документ ( для вырезания / копирования / удаления / ... )$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": "перенос слов\">↵",
|
||||
@@ -576,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отпустите её на большую кнопку загрузки",
|
||||
@@ -654,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": "перезагрузить страницу, чтобы применить изменения?",
|
||||
|
||||
@@ -692,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 не найдено ┐( ´ -`)┌",
|
||||
"o1": 'или у вас нет доступа -- попробуйте авторизоваться или <a href="' + SR + '/?h">вернуться на главную</a>',
|
||||
@@ -715,8 +717,8 @@ Ls.rus = {
|
||||
"ta1": "сначала введите свой новый пароль",
|
||||
"ta2": "повторите новый пароль:",
|
||||
"ta3": "опечатка; попробуйте снова",
|
||||
"nop": "ОШИБКА: Пароль не может быть пустым", //m
|
||||
"nou": "ОШИБКА: Имя пользователя и/или пароль не могут быть пустыми", //m
|
||||
"nop": "ОШИБКА: Пароль не может быть пустым",
|
||||
"nou": "ОШИБКА: Имя пользователя и/или пароль не могут быть пустыми",
|
||||
"aa1": "входящие файлы:",
|
||||
"ab1": "отключить no304",
|
||||
"ac1": "включить no304",
|
||||
|
||||
@@ -575,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",
|
||||
@@ -658,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
|
||||
|
||||
@@ -576,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",
|
||||
@@ -659,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
|
||||
|
||||
@@ -576,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",
|
||||
@@ -659,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
|
||||
|
||||
@@ -576,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будь ласка, перетягніть його на велику кнопку завантаження",
|
||||
@@ -659,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
|
||||
|
||||
@@ -585,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",
|
||||
|
||||
@@ -690,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
|
||||
|
||||
+87
-17
@@ -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,
|
||||
@@ -1006,6 +1006,17 @@ function up2k_init(subtle) {
|
||||
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;
|
||||
}
|
||||
|
||||
function unxhr(xhr) {
|
||||
xhr.onload = xhr.onerror = xhr.ontimeout = null;
|
||||
apop(st.xhr, xhr);
|
||||
}
|
||||
|
||||
var pvis = new U2pvis("bz", '#u2cards', uc, st),
|
||||
donut = new Donut(uc, st);
|
||||
|
||||
@@ -1908,6 +1919,30 @@ 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');
|
||||
|
||||
st.deaf = true;
|
||||
xhr.onload();
|
||||
}
|
||||
|
||||
if (st.bytes.inflight && (st.bytes.inflight < 0 || !st.busy.upload.length)) {
|
||||
console.log('insane inflight ' + st.bytes.inflight);
|
||||
st.bytes.inflight = 0;
|
||||
@@ -2460,16 +2495,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);
|
||||
|
||||
unxhr(xhr);
|
||||
apop(st.busy.head, t);
|
||||
st.todo.head.unshift(t);
|
||||
};
|
||||
function orz(e) {
|
||||
unxhr(xhr);
|
||||
if (t.done)
|
||||
return console.log('done; skip head2', t.name, t);
|
||||
|
||||
@@ -2508,6 +2545,7 @@ function up2k_init(subtle) {
|
||||
xhr.open('HEAD', t.purl + uricom_enc(t.name), true);
|
||||
xhr.timeout = 34000;
|
||||
xhr.send();
|
||||
st.xhr.push(xhr);
|
||||
}
|
||||
|
||||
/////
|
||||
@@ -2534,8 +2572,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 () {
|
||||
unxhr(xhr);
|
||||
if (t.t_busied != me) // t.done ok
|
||||
return console.log('zombie handshake onerror', t.name, t);
|
||||
|
||||
@@ -2548,6 +2587,7 @@ function up2k_init(subtle) {
|
||||
t.keepalive = keepalive;
|
||||
};
|
||||
var orz = function (e) {
|
||||
unxhr(xhr);
|
||||
if (t.t_busied != me || t.done)
|
||||
return console.log('zombie handshake onload', t.name, t);
|
||||
|
||||
@@ -2677,7 +2717,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++) {
|
||||
@@ -2833,9 +2873,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);
|
||||
}
|
||||
|
||||
/////
|
||||
@@ -2866,9 +2908,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;
|
||||
}
|
||||
@@ -2879,7 +2920,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);
|
||||
@@ -2924,6 +2966,7 @@ function up2k_init(subtle) {
|
||||
pcar == pcdr ? pcar : ('' + pcar + '~' + pcdr);
|
||||
|
||||
var orz = function (xhr) {
|
||||
unxhr(xhr);
|
||||
st.bytes.inflight -= xhr.bsent;
|
||||
var txt = unpre((xhr.response && xhr.response.err) || xhr.responseText);
|
||||
if (txt.indexOf('upload blocked by x') + 1) {
|
||||
@@ -2951,6 +2994,9 @@ function up2k_init(subtle) {
|
||||
st.etac.u++;
|
||||
st.etac.t++;
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -2962,7 +3008,7 @@ 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) {
|
||||
if (!t.sprs || txt.indexOf('unknown wark') == 0) {
|
||||
t.cooldown = t.coolmul = 0;
|
||||
unqueue_up(t);
|
||||
}
|
||||
@@ -2971,10 +3017,13 @@ function up2k_init(subtle) {
|
||||
orz2(xhr);
|
||||
}
|
||||
var orz2 = function (xhr) {
|
||||
unxhr(xhr);
|
||||
if (st.deaf)
|
||||
xhr.abort();
|
||||
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();
|
||||
if (!has(st.busy.handshake, t)) {
|
||||
pvis.seth(t.n, 1, 'verifying');
|
||||
@@ -2984,7 +3033,7 @@ function up2k_init(subtle) {
|
||||
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);
|
||||
|
||||
@@ -2997,8 +3046,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) {
|
||||
@@ -3011,6 +3065,9 @@ 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);
|
||||
|
||||
@@ -3045,11 +3102,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();
|
||||
}
|
||||
@@ -3163,7 +3230,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);
|
||||
@@ -3357,7 +3424,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);
|
||||
}
|
||||
}
|
||||
@@ -3400,7 +3467,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) {
|
||||
@@ -3471,6 +3538,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";
|
||||
|
||||
+23
-4
@@ -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 () {
|
||||
@@ -840,7 +853,7 @@ function vjoin(p1, p2) {
|
||||
if (!p2)
|
||||
p2 = '';
|
||||
|
||||
if (p1.endsWith('/'))
|
||||
if (p1 != '/' && p1.endsWith('/'))
|
||||
p1 = p1.slice(0, -1);
|
||||
|
||||
if (p2.startsWith('/'))
|
||||
@@ -852,7 +865,7 @@ function vjoin(p1, p2) {
|
||||
if (!p2)
|
||||
return p1;
|
||||
|
||||
return p1 + '/' + p2;
|
||||
return p1 == '/' ? p1 + p2 : p1 + '/' + p2;
|
||||
}
|
||||
|
||||
|
||||
@@ -2021,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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,3 +1,191 @@
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2026-1006-0035 `v1.20.25` SECURITY: shadowing on windows/mac
|
||||
|
||||
## ⚠️ ATTN: this release mitigates a Windows/MacOS-specific issue with shadowing
|
||||
|
||||
[GHSA-3c88-p299-7hvr](https://github.com/9001/copyparty/security/advisories/GHSA-3c88-p299-7hvr): due to windows/macos having case-insensitive (casefolding) filesystems, [shadowing](https://github.com/9001/copyparty/#shadowing) is not bulletproof, even with this fix
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #1498 [--acao-re](https://copyparty.eu/cli/#g-acao-re) for allowing cross-domain access with more complex rules ab2299e3
|
||||
* #1652 [redup](https://copyparty.eu/cli/#redup-help-page): detect and skip hardlinks / reflinks which are already optimally deduplicated 08093578
|
||||
* [--sftp-fastldr](https://copyparty.eu/cli/#g-sftp-fastldr) to improve startup time on extremely old hardware 186cbf93
|
||||
* [--sftp-chattr](https://copyparty.eu/cli/#g-sftp-chattr) to restrict chattr to certain properties e59fc05e
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* GHSA-3c88-p299-7hvr e8de4211
|
||||
* for further context, torvalds has some [technically accurate and colourful opinions](https://lore.kernel.org/lkml/CAHk-=wjajMJyoTv2KZdpVRoPn0LFZ94Loci37WLVXmMxDbLOjg@mail.gmail.com/) (nsfw) on how casefolding filesystems are an ongoing mistake
|
||||
* on a more serious note, [the readme was updated](https://github.com/9001/copyparty/#shadowing)
|
||||
* fix uploads struggling to resume from a network glitch if the server filesystem is FAT32 or exFAT 23e00c0a 731dbbff
|
||||
* #1647 shares: fix cleanup of single-file shares when manually deleted before expiration (thx @willie6666!) f445bfa8
|
||||
* #1660 ban-pw now more aggressive on failed basic-auth (thx @PAStheLoD!) 5e0a88e3
|
||||
* #1656 webdav: return 404 instead of 400 when file didn't exist 74b17374
|
||||
* fix navigating to an index.html webroot (thx @tilse!) 334dccc5
|
||||
* fix IP-address detection on Windows XP 107dec9e
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* #1658 constant-time verification of filekeys/dirkeys (thx @rozbb!) a31870cb
|
||||
* prevent enabling the smb server unless `--hack-me-bro` is also enabled because the smb-server was intentionally made super-insecure so it would work with ancient outdated equipment in emergencies and should never be used otherwise 1341b342
|
||||
* now possible to upload [09. t+pazolite - I still think about y%#'()_*{\\"'&36)%("'$&''&(15y!!!_??*`~.flac](https://www.youtube.com/watch?v=btPU_rluFSE) (dont click that link on acid) 28f9f6f0 64f574fe
|
||||
|
||||
## 🌠 fun facts
|
||||
|
||||
* [thx t+paz](https://a.ocv.me/pub/g/2026/10/Screenshot_20261002-221751_1.png?cache)
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2026-0919-2133 `v1.20.24` redup
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* new option [--redup](https://copyparty.eu/cli/#g-redup) (global or volflag) to convert local files between various dedup approaches fd4f4341 22915902 16a3f5d5
|
||||
* turn full copies into reflinks/symlinks/hardlinks or convert between those
|
||||
* run it with `--redup-dry` (dryrun) to check for unexpected consequences
|
||||
* WARNING: maybe take a snapshot of your filesystem before enabling this; brand new code with little testing, read [--help-redup](https://copyparty.eu/cli/#redup-help-page) and use with care
|
||||
* seems fine for converting between symlinks and reflinks, probably hardlinks too
|
||||
* #1643 textfile-viewer can detect filetype from shebang (thx @botahamec!) 097b8305
|
||||
* when dedup with [reflink](https://copyparty.eu/cli/#g-reflink) is enabled, then also dedup between volumes when possible (since reflink makes that safe) 735d6e00 5ddbce31
|
||||
* option [-i no](https://copyparty.eu/cli/#bind-help-page) to launch copyparty without the webserver, for indexing / testing / whatever c92435c2
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* if `--dedup` was enabled, then copying a file from one volume to another could dedup (create a symlink/hardlink/reflink) instead of a full copy 947d73ee
|
||||
* mainly an issue for portable storage such as flashdrives, and the main reason why `--redup` was added
|
||||
* the list of filenames given as input to xiu hooks did not have a trailing newline c688bb2e
|
||||
* #1611 volflag `nosub` would still result in empty folders from up2k ec4a95e1
|
||||
* fix `PRTY_NO_IFADDR` 6867ccc5
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* iOS: further improved upload-speeds on buggy iOS versions 971a62cc
|
||||
* looks like apple is working on fixing this bug; iOS v27 made https way more stable but http is still just as broken, so improved the workaround a bit more
|
||||
* added [--help-dedup](https://copyparty.eu/cli/#dedup-help-page) and [--help-redup](https://copyparty.eu/cli/#redup-help-page) 22915902 16a3f5d5
|
||||
* add a warning that the sourceforge project is not official and probably malware bfc3cff8
|
||||
|
||||
## 🌠 fun facts
|
||||
|
||||
* the [railgun roulette vip-mix](https://www.youtube.com/watch?v=mv9BmrzkiJc) has the same bpm as the [turn-signal on a honda jazz](https://a.ocv.me/pub/stuff/music/railgun-roulette-honda-remix.opus)
|
||||
* on that note, [this fantasy-OPNA remake](https://www.youtube.com/watch?v=hv2hr8aPBMU) is too awesome to leave out
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2026-0906-2249 `v1.20.23` rcm once again
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* the custom right-click-menu didn't like being enabled ad9994be
|
||||
* this release is a hotfix for that; see v1.20.22 for all the other new stuff
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 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
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -97,6 +97,7 @@ copyparty = [
|
||||
"web/*.css",
|
||||
"web/*.html",
|
||||
"web/*.xml",
|
||||
"web/*.gif",
|
||||
"web/tl/*.js",
|
||||
"web/tl/*.gz",
|
||||
"web/a/*.txt",
|
||||
|
||||
@@ -49,7 +49,7 @@ echo warming up cache
|
||||
cat 1 >/dev/null
|
||||
|
||||
echo ok lets go
|
||||
$pybin "$sfx" -p39204 -e2dsa --dbd=yolo --exit=idx -lo=t -q "$@" && err= || err=$?
|
||||
$pybin "$sfx" -i no -e2dsa --dbd=yolo --exit=idx -lo=t -q "$@" && err= || err=$?
|
||||
[ $win ] && [ $err = 15 ] && err= # sigterm doesn't hook on windows, ah whatever
|
||||
[ $err ] && echo ERROR $err && exit $err
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -2,7 +2,7 @@ FROM alpine:3.23
|
||||
WORKDIR /z
|
||||
ENV ver_hashwasm=4.12.0 \
|
||||
ver_marked=4.3.0 \
|
||||
ver_dompf=3.4.6 \
|
||||
ver_dompf=3.4.17 \
|
||||
ver_mde=2.18.0 \
|
||||
ver_codemirror=5.65.18 \
|
||||
ver_fontawesome=5.13.0 \
|
||||
|
||||
@@ -38,6 +38,14 @@ f=/z/whl/zlib_ng-0.5.1-cp312-cp312-linux_$(cat /etc/apk/arch).whl
|
||||
}
|
||||
rm -rf /z/whl
|
||||
|
||||
# drop openjpeg; unmaintained, has vulns
|
||||
(cd /usr/lib/python3.*/site-packages/PIL
|
||||
sed -ri /jpg_2000/d features.py
|
||||
sed -ri /Jpeg2KIm/d __init__.py
|
||||
sed -ri /Jpeg2KIm/d Image.py
|
||||
rm -f Jpeg2KImagePlugin.py
|
||||
) || true
|
||||
|
||||
# cleanup for flavors with python build steps (dj/iv)
|
||||
rm -rf /var/cache/apk/* /root/.cache
|
||||
|
||||
@@ -72,7 +80,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
|
||||
|
||||
@@ -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
@@ -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 -dedup -redup -thumb-ex -handlers -hooks -idp -urlform -exp -rlo -ls -dbd -chmod -pwhash -zm'
|
||||
|
||||
export PRTY_FULL_HELP=1
|
||||
export PRTY_NO_PARAMIKO=1
|
||||
|
||||
@@ -8,7 +8,7 @@ import subprocess as sp
|
||||
|
||||
# to convert the copyparty --help to html, run this in xfce4-terminal @ 140x43:
|
||||
_ = r""""
|
||||
echo; for a in '' -bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -urlform -exp -ls -dbd -chmod -pwhash -zm; do
|
||||
echo; for a in '' -bind -accounts -auth -auth-ord -flags -dedup -redup -thumb-ex -handlers -hooks -idp -urlform -exp -rlo -ls -dbd -chmod -pwhash -zm; do
|
||||
./copyparty-sfx.py --help$a 2>/dev/null; printf '\n\n\n%0139d\n\n\n'; done # xfce4-terminal @ 140x43
|
||||
"""
|
||||
# click [edit] => [select all]
|
||||
@@ -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
|
||||
|
||||
+3
-3
@@ -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/;
|
||||
@@ -29,7 +29,7 @@ exit 0
|
||||
|
||||
|
||||
# first open an infinitely wide console (this is why you own an ultrawide) and copypaste this into it:
|
||||
for a in '' -bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -urlform -exp -ls -dbd -chmod -pwhash -zm; do
|
||||
for a in '' -bind -accounts -auth -auth-ord -flags -dedup -redup -thumb-ex -handlers -hooks -idp -urlform -exp -rlo -ls -dbd -chmod -pwhash -zm; do
|
||||
./copyparty-sfx.py --help$a 2>/dev/null; printf '\n\n\n%0255d\n\n\n'; done
|
||||
|
||||
# then copypaste all of the output by pressing ctrl-shift-a, ctrl-shift-c
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
ccbbc7ed972a8ae3f9ace7728cf5cbff8dcb15ed2dfcd105131641f5b041cecc90adc2fc25f36333e0a2be8e56a6f702fb064da6e6fbb78cc2393333ad3e5b6f python-3.14.8-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 |
@@ -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.8-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 |
@@ -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
|
||||
|
||||
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user