Compare commits

...
120 Commits
Author SHA1 Message Date
Bart Oostveen 1af7efeca8 fix(nix): separate makeWrapperArgs properly (#1676)
* fix(nix): separate makeWrapperArgs properly

Since Python packages are now built with `__structuredAttrs = true;` in nixpkgs, this is neccesary.

See also: https://github.com/NixOS/nixpkgs/commit/a8383b633210925071f3d7c680c8d901939c7fd5
See also: https://github.com/NixOS/nixpkgs/pull/554025

* chore(nix): nixfmt
2026-10-21 08:09:00 +03:00
ed 8b4593c9a2 update pkgs to 1.20.26 2026-10-14 23:30:13 +00:00
ed 7ccebb6948 v1.20.26 2026-10-14 23:21:55 +00:00
ed ed0fcd608f th: fix .raw if not pil jxl 2026-10-14 21:55:12 +00:00
ed 9811cd3101 thx faelnor 2026-10-14 21:54:58 +00:00
ed fa8913c712 dumb 2026-10-14 21:53:36 +00:00
ed 4bd64d7456 shadowing: less false-positives 2026-10-14 21:51:52 +00:00
ed 7ce483e970 xvol: ls filter 2026-10-14 21:50:40 +00:00
ed 82eab3d2f8 xvol/xdev (GHSA-hg99-jqfp-rf76) 2026-10-14 21:49:04 +00:00
ed 0dc1cb3291 docker: drop openjpeg 2026-10-09 00:26:10 +00:00
ed 1a87dc3218 update pkgs to 1.20.25 2026-10-06 00:44:54 +00:00
ed dc3adc0f4d v1.20.25 2026-10-06 00:35:29 +00:00
ed 5f00c1631c return to ?b=u; closes #1659 2026-10-06 00:33:31 +00:00
ed e59fc05e73 add --sftp-chattr 2026-10-05 23:47:51 +00:00
ed e8de421174 shadowing + casefolding (GHSA-3c88-p299-7hvr) 2026-10-05 23:46:39 +00:00
ed 36aaa24974 file-rename instant preview on open 2026-10-05 23:02:30 +00:00
ed 5e0a88e355 ban-pw basic-auth; closes #1660 2026-10-05 22:36:30 +00:00
ed 4b283b4bfd bbox: fix #1650 (el null) 2026-10-05 22:32:49 +00:00
ed 64f574fe94 u2c: allow \ in filenames 2026-10-05 22:26:18 +00:00
Michael Rosenberg a31870cbd3 dk/fk constant-time (#1658) 2026-10-05 22:01:30 +00:00
ed 74b17374c2 DELETE: say 404 on enoent; closes #1656 2026-10-03 22:10:17 +00:00
ed 670e86228c tests: support new coreutils or whatever 2026-10-03 22:08:10 +00:00
ed 1341b3421f --smb now requres --hack-me-bro 2026-10-03 22:01:49 +00:00
ed 5961228c93 deadcode 2026-10-03 21:44:14 +00:00
ed 28f9f6f002 allow \ in filenames on linux/macos;
now possible to upload 09. t+pazolite - I still think about y%#'()_*{\\"'&36)%("'$&''&(15y!!!_??*`~.flac
2026-10-03 21:43:47 +00:00
ed 186cbf939f sftp: add fastloader;
startup on an 800mhz armv5 exceeded the 30sec timeout
2026-10-02 03:29:12 +00:00
ed 080935788b redup: skip conclusive hard/ref; closes #1652 2026-09-30 00:05:02 +00:00
ed c6918c2ce6 tests: macos 2026-09-30 00:15:08 +02:00
ed ab2299e3db add acao-re; closes #1498 2026-09-27 19:12:01 +00:00
edandTil Schmitter 334dccc55e --ih: fix nav to webroot
Co-authored-by: Til Schmitter <til.schmitter@gmail.com>
2026-09-27 18:23:08 +00:00
ed 8d7aa4f860 cosmetic 2026-09-27 18:20:47 +00:00
ed f73b37c6fa pipe-dl progress on /?h 2026-09-27 18:11:29 +00:00
ed 731dbbff84 u2c: fix exfat's http502 allergy;
addendum to 23e00c0a04
2026-09-26 23:43:26 +00:00
ed 23e00c0a04 up2k-js: fix exfat's http502 allergy;
normally, to avoid excessive handshaking, unexpected server errors are
handled by continuing onto the next chunk and dealing with it at eof

this doesn't work on filesystems that dont support sparse files, so
discard the chunk-queue and re-handshake to keep it sequential
2026-09-26 22:12:15 +00:00
ed 107dec9ed1 ifaddr: winxp fix 2026-09-24 19:31:21 +00:00
willie6666 f445bfa862 shares: delete sf entries too (#1647)
Signed-off-by: willie6666 <89621369+willie6666@users.noreply.github.com>
2026-09-20 13:12:56 +00:00
ed aff083fb28 update pkgs to 1.20.24 2026-09-19 21:39:09 +00:00
ed 5ccc999f39 v1.20.24 2026-09-19 21:33:56 +00:00
ed 16a3f5d51a add --help-redup + failsafe 2026-09-19 20:48:28 +00:00
ed 35af7c9fef js: add msel fn 2026-09-19 19:14:06 +00:00
ed 4d1f2a9301 js: fix contrived crash (closes #1516);
view textfile, sel, delete, sel, boom
2026-09-19 19:13:41 +00:00
ed 229159027b add --help-dedup 2026-09-19 17:43:51 +00:00
ed ec4a95e1a5 up2k nosub; closes #1611 2026-09-19 17:42:39 +00:00
ed eb4f1dddce lint 2026-09-18 20:59:16 +00:00
Mica Whiteanded 097b83056e textfile-viewer: parse shebang (#1643)
detect filetype based on #!/interpreter

---------

Signed-off-by: ed <s@ocv.me>
Co-authored-by: ed <s@ocv.me>
2026-09-18 19:47:09 +00:00
ed c688bb2e46 xiu: add trailing newline 2026-09-17 22:00:24 +00:00
ed c92435c2d2 add -i no 2026-09-17 21:53:50 +00:00
ed 22afaa7586 nocow histpath iff btrfs 2026-09-17 21:46:12 +00:00
ed 5ddbce31fc xlink-reflink only same-fs;
avoid waking other disks which wont work anyways
2026-09-17 21:24:10 +00:00
ed ec8ec0f1a6 un-cargocult bools 2026-09-17 21:12:46 +00:00
ed 558f6aee44 shallowclone up2k.flags;
update flags on config-reload while keeping up2k coherent
2026-09-17 19:56:58 +00:00
ed 6867ccc511 fix PRTY_NO_IFADDR + cleanup 2026-09-16 22:38:23 +00:00
ed 947d73eeb6 check xlink on copy 2026-09-16 22:35:11 +00:00
ed 735d6e00de up2k: reflink is xlink-safe 2026-09-16 22:34:19 +00:00
ed fd4f434192 add --redup 2026-09-16 22:33:38 +00:00
ed 971a62cc89 up2k: iOS: reduce jitter;
* xhr.abort() to close/free tcp
* onfoo=null fixes done-flicker
2026-09-15 19:29:47 +00:00
ed bfc3cff848 unsanctioned: sourceforge 2026-09-08 06:01:30 +02:00
ed 8967726f61 update pkgs to 1.20.23 2026-09-06 22:53:14 +00:00
ed ad9994bef3 v1.20.23 2026-09-06 22:49:08 +00:00
ed e5e2e97910 archlinux: add post_upgrade 2026-09-06 22:08:42 +00:00
ed e42e01563e update pkgs to 1.20.22 2026-09-06 21:18:21 +00:00
ed de21c210c6 v1.20.22 2026-09-06 21:12:32 +00:00
ed 55969b8e54 dk: block subdir zip (GHSA-mc69-pxc8-4xf4) 2026-09-06 21:07:39 +00:00
ed b80a210e0f sftp: xvol/xdev (GHSA-3fhv-rhjw-7hrg) 2026-09-06 21:07:21 +00:00
ed 8016512064 sfx: link explanation 2026-09-06 18:22:52 +00:00
ed 72eb03bf43 exe: drop license filter 2026-09-06 15:02:32 +00:00
ed f870cf3c05 sfx: prefer ~/.cache/; closes #1517 2026-09-05 22:49:01 +00:00
ed e6c9c6fbeb sfx: golf 2026-09-05 22:43:13 +00:00
ed dd9cf6456d btrfs: nocow .hist for sqlite perf 2026-09-05 20:54:37 +00:00
ed 185087ee05 systemd: use /etc/copyparty.conf; closes #1631 2026-09-05 17:56:19 +00:00
ed 3e3401f4ed allow dk download-as-zip 2026-09-05 12:55:58 +00:00
ed a78611d8c5 dont trust zip headers 2026-09-05 11:29:50 +00:00
ed 370a05ab3c add --no-mime / volflag nomime 2026-09-05 11:00:40 +00:00
ed d7d30f268e connect: rclone obscure; closes #1632 2026-09-04 19:14:06 +00:00
ed 34229337cc the font fell off 2026-09-04 19:01:44 +00:00
ed 0767d95674 up2k: fix !e2d reload blocking uploads 2026-09-04 18:51:15 +00:00
ed f00d19c5d3 avoid binary garbage in logs;
also stricten RE_HTTP1 against false-positives
2026-09-04 18:21:20 +00:00
ed ab791921ea fix csp-nonce on chrome<62 2026-09-04 17:54:20 +00:00
ed 2cebd8ce16 i18n: 2old 2026-09-04 17:54:04 +00:00
ed 662e3c5d59 indicate F4 instantly 2026-09-04 16:08:35 +00:00
ed 3ea671ff9a iOS Safari XHR has gone deaf;
workaround for iPhone bug in
iOS 18.6.2, 26.6, 26.6.1, ...

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

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

No files matched your search

+1
View File
@@ -4,4 +4,5 @@
*.png binary
*.gif binary
*.ico binary
*.gz binary
+40 -10
View File
@@ -84,6 +84,7 @@ built in Norway 🇳🇴 with contributions from [not-norway](https://github.com
* [exclude-patterns](#exclude-patterns) - to save some time
* [filesystem guards](#filesystem-guards) - avoid traversing into other filesystems
* [periodic rescan](#periodic-rescan) - filesystem monitoring
* [u2sz](#u2sz) - configure upload-chunking
* [upload rules](#upload-rules) - set upload rules using volflags
* [compress uploads](#compress-uploads) - files can be autocompressed on upload
* [chmod and chown](#chmod-and-chown) - per-volume filesystem-permissions and ownership
@@ -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
+1
View File
@@ -36,6 +36,7 @@ these are `--xiu` hooks; unlike `xbu` and `xau` (which get executed on every sin
* [reject-and-explain.py](reject-and-explain.py) shows a custom error-message when it rejects an upload
* [reject-ramdisk.py](reject-ramdisk.py) rejects the upload if the destination is a ramdisk
* this hook uses the `I` flag which makes it 140x faster, but if the plugin has a bug it may crash copyparty
* [phonecam-sorter.py](phonecam-sorter.py) is for syncing backups of phone gallery into year/month folders
# more upload stuff
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
import os
import re
import sys
_ = r"""
purpose:
sync/backup phone gallery (pics/vids) to a nas which runs copyparty
what this hook does:
redirect photos/videos into date-based folders, named "%Y%m" according
to filename; that is, "/cam/sort/DCIM/D027/PXL_20250828_202820075.jpg"
will be redirected to "/cam/pics/ts/202508/PXL_20250828_202820075.jpg"
requires this setup on the server:
* /cam/ is a volume with all your pics/vids
* the phone must upload to somewhere below /cam/sort/
* photo/video filenames match the following regex:
^[a-z]{3,4}_([0-9]{6})[0-9]{2}_[0-9]{6}.*\.(jpg|mp4)$
* so for example these filenames are OK:
* PXL_20250828_202820075.jpg
* IMG_20251123_202405.jpg
* PANO_20180421_121245_03.t8o5.jpg
* VID_20250307_220250-crf34.mp4
example server config as a volflag:
-v /mnt/thedisk/gallery:cam:rwa,ed:c,e2ds,xbu=I,~/hooks/phonecam-sorter.py
example server config in a copyparty config file:
[/cam]
/mnt/thedisk/gallery
accs:
rwa: ed
flags:
e2ds
xbu: I,~/hooks/phonecam-sorter.py
command to run on phone to sync to server, sending new files only:
~/bin/u2c.py -te homenas.ca -a '$homenas.pw' https://10.0.0.2:3923/cam/sort/ ~/storage/dcim/Camera
the phone-command above assumes this setup on the phone:
* termux or another linux-like thing with access to gallery files
* homenas.ca is a textfile with the server's CA certificate
* homenas.pw is a textfile with the copyparty password inside
* ~/storage/dcim/ exists; if not, run termux-setup-storage
* ~/bin/u2c.py exists; if not, curl -LO https://10.0.0.2:3923/.cpr/a/u2c.py && chmod 755 u2c.py
unfortunate consequences:
* due to the redirection, u2c -z doesn't do anything, so every local
file is hashed every time, wasting some time and cpu on the phone
but otoh certainly avoids corruption on either side (phone/server)
"""
def main(inf):
log = inf["log"]
vdir, fn = os.path.split(inf["vp"])
if not vdir.startswith("cam/sort"):
return {"rc": 0} # 2nd call after 1st reloc; already done
m = re.match(r"^[a-z]{3,4}_([0-9]{6})[0-9]{2}_[0-9]{6}.*\.(jpg|mp4)$", fn.lower())
if not m:
return {"rc": 0} # not recognized; write to original path in /cam/sort/
ym, ext = m.groups()
if ext == "jpg":
dst = f"/cam/pics/ts/{ym}/"
elif ext == "mp4":
dst = f"/cam/vids/ts/{ym}/"
else:
return {"rc": 0}
ap = "%s%s" % (inf["ap"].split("/cam/sort/")[0], dst)
if not os.path.isdir(ap):
os.makedirs(ap)
ret = {"rc": 0, "reloc": {"vp": dst}}
return ret
if __name__ == "__main__":
main()
+55
View File
@@ -0,0 +1,55 @@
# thumbnail extractors
extract (or generate) thumbnails for custom file formats or override default thumbnail extraction for standard media files
## usage
* load plugins with `--th-extract baz=~/dev/copyparty/bin/thumbs/randomcolor.py`
* `baz` is the file extension (may be comma-separated list of extensions)
* add multiple different extractor plugins by repeating the `--th-extract` argument
## api
### in
each plugin must define a function `main(abspath, **kwargs)`:
* `abspath` is the path to a file to extract thumbnail for
* `kwargs` currently receives these keyword arguments:
* `vn` is the VFS which contains the requested file
* `th_srv` is an instance of [copyparty/th_srv](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/th_srv.py)
### out
the `main()` function must
* return `None` or raise an `Exception` in case it is unable to extract thumbnail
* return tuple `fmt, stream, offset, whence, size` otherwise:
* `fmt: str` – format (filename extension) of the thumbnail; should be one of the image formats supported by copyparty (see `--th-r-*` options)
* `stream: IO[bytes]` – binary file-like object supporting `seek()`, `read()`, `close()` methods; should contain extracted thumbnail image
* `offset: int, whence: int` – arguments to `stream.seek()`; copyparty will call `stream.seek(offset, whence)` once before starting reading the stream contents
* `size: int | None` – number of bytes to read from the `stream`; if `size` is negative or `None`, copyparty will read until the end of the stream
### notes
it is possible to extract or generate custom thumbnails for standard media files too,
e.g. run copyparty with `--th-extract mp3,m4a,aac,flac,opus=~/dev/copyparty/bin/thumbs/my_custom_mp3_th_extractor.py` and do your magic with audio file thumbnails
in case copyparty doesn't like the `fmt` option, it will `close()` the `stream` without ever calling `seek()` or `read()`
if your scenario involves heavy computation or i/o, it is better to defer the heavy parts until the first `seek()` or `read()` to avoid redundant work in case the result gets rejected based on `fmt` value
if you provide custom `stream` object:
copyparty performs buffered reading, so expect multiple `read()` calls, respect `size` argument in `read(size)` calls, and maintain correct seek position
## examples
* [randomcolor](https://github.com/9001/copyparty/blob/hovudstraum/bin/thumbs/randomcolor.py) generates a random .gif image
* [collabora](https://github.com/9001/copyparty/blob/hovudstraum/bin/thumbs/collabora.py) uses external self-hosted service (collabora online) to generate previews for various office documents, pdf and fb2 books, html, markdown, text files and some image formats; collabora is also supported as office editor in copyparty, see [wopi](https://github.com/9001/copyparty#wopi-server)
## some other known plugins seen on the internets
* [fpkg_thumb](https://github.com/kamaeff/fpkg-vault/blob/1c248de62a9ff9c4088d4b3e7962023cde9d6426/old/fpkg_thumb.py) extracts cover images from playstation4 software installation packages ("pkg" and "fpkg" files)
+125
View File
@@ -0,0 +1,125 @@
"""
CAUTION!
looks like collabora spawns separate jailed libreoffice instance to do the conversion
this may eat up too much ram in folders with many docs or with --th-pregen enabled
consider limiting resources available for collabora
e.g. for me these lines in docker-compose.yml helped alot:
deploy:
resources:
limits:
cpus: '2'
memory: '2G'
/CAUTION!
if you use collabora office editor with copyparty, you already have a thumbnailer for office formats:
https://sdk.collaboraonline.com/docs/conversion_api.html
the 'convert-to' api accepts files in multipart/form-data format
just add this extractor to copyparty with an argument like this:
--th-extract=docx,xlsx,pptx,odt,ods,pdf,fb2=~/dev/copyparty/bin/thumbs/collabora.py
and set COLLABORA_INSTANCE_URL below to your collabora instace url
full list of supported file types:
https://sdk.collaboraonline.com/docs/conversion_api.html#supported-input-formats
this is also an example of deferred IO:
no files opened and no network requests made if copyparty doesn't like the file type
"""
import os
import string
import random
import mimetypes
from urllib.error import HTTPError
from urllib.request import Request, urlopen
COLLABORA_INSTANCE_URL = "http://collabora:9980"
BOUNDARY_CHARS = string.ascii_letters + string.digits
def main(abspath, **kwargs):
boundary = "CopypartyFormData" + "".join(random.choices(BOUNDARY_CHARS, k=53))
start, end = make_headers(abspath, boundary)
size = len(start) + os.path.getsize(abspath) + len(end)
request = Request(
COLLABORA_INSTANCE_URL + "/cool/convert-to/png",
method="POST",
data=iterate(start, abspath, end),
headers={
"Content-Type": f"multipart/form-data; boundary={boundary}",
"Content-Length": str(size),
},
)
file_like = Wrapper(request)
offset, whence, length = 0, 0, None
return "png", file_like, offset, whence, length
def make_headers(abspath, boundary):
"""stuff to wrap file contents by for multipart"""
fname = "file." + abspath.rsplit(".", 1)[-1]
ftype = mimetypes.guess_file_type(fname)[0] or "application/octet-stream"
start = (
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="data"; filename="{fname}"\r\n'
f"Content-Type: {ftype}\r\n\r\n"
)
end = f"\r\n--{boundary}--"
return bytes(start, "ascii"), bytes(end, "ascii")
def iterate(start, fpath, end):
"""docs may be big so read and send them in chunks"""
yield start
with open(fpath, "rb") as f:
while True:
b = f.read(32768)
if not b:
break
yield b
yield end
class Wrapper:
"""
This file-like wrapper defers expensive work until first .seek() call
Useful to not waste resources in case copyparty decides
to throw our thumbnail away based on returned file extension
"""
def __init__(self, request):
self.request = request
self.response = None
def seek(self, offset, whence=0):
if self.response:
return 0
try:
# the heavy part:
# - reading probably big file
# - sending it over the network
# - making collabora do some work
self.response = urlopen(self.request, timeout=10)
except HTTPError as e:
e.close()
raise e
except Exception as e:
self.close()
raise e
# actual response is not seekable
# just return 0
return 0
def read(self, amount):
return self.response.read(amount)
def close(self):
if self.response:
self.response.close()
+29
View File
@@ -0,0 +1,29 @@
import io
import os
import struct
from base64 import b64decode
def create_gif() -> bytes:
head = r"R0lGODdhIAAgAIAB"
tail = r"AAD/LAAAAAAgACAAAAIehI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC5sFADs="
#color = struct.pack(">L", random.randrange(0xffffff)) # wow, way too stable
color = b"\x00" + os.urandom(3)
return b64decode(head) + color + b64decode(tail)
def main(abspath, **ka) -> Optional[tuple[str, IO[bytes], int, int, Optional[int]]]:
print("hello from randomcolor.py pretending to extract a thumb from " + abspath)
# need to return a file-like object
gif_bytes = create_gif()
file_obj = io.BytesIO(gif_bytes)
# where copyparty should seek to in the "file" to start reading from
seek_offset = 0
seek_whence = 0
filesize = None # auto; read until end
# "gif" is the file-extension to decode the file as
return "gif", file_obj, seek_offset, seek_whence, filesize
+28 -13
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
from __future__ import division, print_function, unicode_literals
S_VERSION = "2.22"
S_BUILD_DT = "2026-06-13"
S_VERSION = "2.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&lt&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")
+4
View File
@@ -17,8 +17,12 @@ defaults
listen foo1
bind *:8081
http-request add-header X-Forwarded-Proto https if { ssl_fc }
http-request add-header X-Forwarded-Proto http unless { ssl_fc }
server srv1 127.0.0.1:3923 maxconn 512
listen foo2
bind *:8082
http-request add-header X-Forwarded-Proto https if { ssl_fc }
http-request add-header X-Forwarded-Proto http unless { ssl_fc }
server srv1 /dev/shm/party.sock maxconn 512
+10 -12
View File
@@ -3,7 +3,7 @@
# NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead.
pkgname=copyparty
pkgver="1.20.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"
}
+27
View File
@@ -0,0 +1,27 @@
post_upgrade() {
# v1.20.22 moved the config-file, affecting the system-service only
if [[ $(vercmp "$2" 1.20.22) -lt 0 ]]; then
oldpath="/etc/copyparty/copyparty.conf"
newpath="/etc/copyparty.conf"
echo "==> Migrating config-file from $oldpath to $newpath"
if [[ ! -f "$oldpath" ]]; then
oldpath="$oldpath.pacsave"
fi
if [[ -f "$newpath.pacnew" ]]; then
echo "==> WARNING: A config-file already existed in the new location [$newpath]. The existing file will be used as-is."
elif [[ -f "$oldpath" ]]; then
mv -v "$oldpath" "$newpath"
rmdir /etc/copyparty 2>/dev/null || true
fi
systemctl cat copyparty@.service | awk '/=PRTY_CONFIG=/ && !/=\/etc\/copyparty.conf/ {exit 1}' || {
echo "==> WARNING: The copyparty system-service has been edited by hand since installation, so it will not read config from the new default config path [$newpath]. Please use PRTY_CONFIG=$newpath if that is correct for your setup."
}
fi
}
+2 -5
View File
@@ -2,7 +2,7 @@
pkgname=copyparty
pkgver=1.20.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)
+12 -2
View File
@@ -1,3 +1,13 @@
this is `/var/lib/copyparty-jail`, the fallback webroot when copyparty has not yet been configured
this is `/var/lib/copyparty-jail/srv/`, the fallback webroot when copyparty has not yet been configured
please add some `*.conf` files to `/etc/copyparty.d/`
please edit the config-file and restart copyparty;
* if running as a system service: `vim /etc/copyparty.conf`
* if running as a user service: `vim $HOME/.config/copyparty/copyparty.conf`
some inspiration:
* a basic example config: https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf
* another example with focus on syntax: https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf
* and CTRL-F `config file example` here: https://github.com/9001/copyparty
the full list of configuration options can be seen at https://copyparty.eu/cli/
or by running `copyparty --help`
@@ -17,7 +17,7 @@ Description=copyparty file server
[Service]
SyslogIdentifier=prisonparty
Environment=PYTHONUNBUFFERED=x
WorkingDirectory=/var/lib/copyparty-jail
WorkingDirectory=/var/lib/copyparty-jail/srv
ExecReload=/bin/kill -s USR1 $MAINPID
# stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running
+36 -27
View File
@@ -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 = [
+3 -3
View File
@@ -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="
}
-3
View File
@@ -26,9 +26,6 @@ See release at https://github.com/9001/copyparty/releases
%setup -q
%build
cd "copyparty/web"
make
cd -
%pyproject_wheel
%install
+3 -3
View File
@@ -1,5 +1,5 @@
# this will start `/usr/bin/copyparty`
# and read config from `$HOME/.config/copyparty.conf`
# and read config from `$HOME/.config/copyparty/copyparty.conf`
#
# unless you add -q to disable logging, you may want to remove the
# following line to allow buffering (slightly better performance):
@@ -11,13 +11,13 @@ Description=copyparty file server
[Service]
Type=notify
SyslogIdentifier=copyparty
WorkingDirectory=/var/lib/copyparty-jail
WorkingDirectory=/var/lib/copyparty-jail/srv
Environment=PYTHONUNBUFFERED=x
Environment=PRTY_CONFIG=%h/.config/copyparty/copyparty.conf
ExecReload=/bin/kill -s USR1 $MAINPID
# ensure there is a config
ExecStartPre=/bin/bash -c 'if [[ ! -f %h/.config/copyparty/copyparty.conf ]]; then mkdir -p %h/.config/copyparty; cp /etc/copyparty/copyparty.conf %h/.config/copyparty/copyparty.conf; fi'
ExecStartPre=/bin/bash -c 'if [[ ! -f %h/.config/copyparty/copyparty.conf ]]; then mkdir -p %h/.config/copyparty; cp -v /etc/copyparty.conf %h/.config/copyparty/; fi'
# run copyparty
ExecStart=/usr/bin/python3 /usr/bin/copyparty
+13 -11
View File
@@ -1,13 +1,15 @@
[global]
i: 127.0.0.1
[accounts]
user: password
[/]
/var/lib/copyparty-jail
/var/lib/copyparty-jail/srv/
accs:
r: *
rwdma: user
flags:
grid
r: * # everyone can [r]ead (browse/download)
# above is a very simple config;
# the webroot is /var/lib/copyparty-jail/srv/
# readable by everyone, no login required
#
# you'll want to replace this file; some inspiration:
# - a basic example config: https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf
# - another example with focus on syntax: https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf
# - and CTRL-F "config file example" here: https://github.com/9001/copyparty
# - full list of options: https://copyparty.eu/cli/
-3
View File
@@ -3,9 +3,6 @@
# vim: ft=yaml:
# put this file in /etc/
[global]
e2dsa # enable file indexing and filesystem scanning
e2ts # and enable multimedia indexing
+3 -3
View File
@@ -1,5 +1,5 @@
# this will start `/usr/bin/copyparty`
# and read config from `/etc/copyparty/copyparty.conf`
# and read config from `/etc/copyparty.conf`
#
# the %i refers to whatever you put after the copyparty@
# so with copyparty@foo.service, %i == foo
@@ -14,9 +14,9 @@ Description=copyparty file server
[Service]
Type=notify
SyslogIdentifier=copyparty
WorkingDirectory=/var/lib/copyparty-jail
WorkingDirectory=/var/lib/copyparty-jail/srv
Environment=PYTHONUNBUFFERED=x
Environment=PRTY_CONFIG=/etc/copyparty/copyparty.conf
Environment=PRTY_CONFIG=/etc/copyparty.conf
ExecReload=/bin/kill -s USR1 $MAINPID
# user to run as + where the TLS certificate is (if any)
+9 -6
View File
@@ -1,10 +1,13 @@
this is `/var/lib/copyparty-jail`, the fallback webroot when copyparty has not yet been configured
this is `/var/lib/copyparty-jail/srv/`, the fallback webroot when copyparty has not yet been configured
please edit `/etc/copyparty/copyparty.conf` (if running as a system service)
or `$HOME/.config/copyparty/copyparty.conf` if running as a user service
please edit the config-file and restart copyparty;
* if running as a system service: `vim /etc/copyparty.conf`
* if running as a user service: `vim $HOME/.config/copyparty/copyparty.conf`
a basic configuration example is available at https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf
a configuration example that explains most flags is available at https://github.com/9001/copyparty/blob/hovudstraum/docs/chungus.conf
some inspiration:
* a basic example config: https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf
* another example with focus on syntax: https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf
* and CTRL-F `config file example` here: https://github.com/9001/copyparty
the full list of configuration options can be seen at https://ocv.me/copyparty/helptext.html
the full list of configuration options can be seen at https://copyparty.eu/cli/
or by running `copyparty --help`
+3 -3
View File
@@ -1,6 +1,6 @@
# this will start `/usr/bin/copyparty`
# in a chroot, preventing accidental access elsewhere,
# and read copyparty config from `/etc/copyparty/copyparty.conf`
# and read copyparty config from `/etc/copyparty.conf`
#
# expose additional filesystem locations to copyparty
# by listing them between the last `%i` and `--`
@@ -19,9 +19,9 @@ Description=copyparty file server
[Service]
Type=notify
SyslogIdentifier=prisonparty
WorkingDirectory=/var/lib/copyparty-jail
WorkingDirectory=/var/lib/copyparty-jail/srv
Environment=PYTHONUNBUFFERED=x
Environment=PRTY_CONFIG=/etc/copyparty/copyparty.conf
Environment=PRTY_CONFIG=/etc/copyparty.conf
ExecReload=/bin/kill -s USR1 $MAINPID
# user to run as + where the TLS certificate is (if any)
+3
View File
@@ -40,6 +40,9 @@ VT100 = "--ansi" in sys.argv or (
)
# introduced in anniversary update
zs = os.environ.get("PRTY_CONFIG", "")
CFG_DEF = [zs] if zs else []
ANYWIN = WINDOWS or sys.platform in ["msys", "cygwin"]
MACOS = platform.system() == "Darwin"
+238 -26
View File
@@ -14,6 +14,7 @@ import locale
import os
import re
import select
import shutil
import socket
import sys
import threading
@@ -23,6 +24,7 @@ import uuid
from .__init__ import (
ANYWIN,
CFG_DEF,
CORES,
EXE,
MACOS,
@@ -38,7 +40,10 @@ from .__init__ import (
from .__version__ import CODENAME, S_BUILD_DT, S_VERSION
from .authsrv import expand_config_file, split_cfg_ln, upgrade_cfg_fmt
from .bos import bos
from .cert import NO_TLS
from .cfg import flagcats, onedash
from .mdns import DNS_VND
from .qrkode import VENDORED as QR_VND
from .svchub import SvcHub
from .util import (
APPLESAN_TXT,
@@ -48,6 +53,8 @@ from .util import (
DEF_MTH,
HAVE_BWRAP,
HAVE_IPV6,
HAVE_MAGIC,
IFADDR_VND,
IMPLICATIONS,
JINJA_VER,
MIKO_VER,
@@ -63,6 +70,7 @@ from .util import (
URL_BUG,
URL_PRJ,
Daemon,
absreal,
align_tab,
b64enc,
ctypes,
@@ -94,21 +102,18 @@ if PY2:
range = xrange # type: ignore
try:
if os.environ.get("PRTY_NO_TLS"):
if NO_TLS:
raise Exception()
HAVE_SSL = True
import ssl
HAVE_SSL = True
except:
HAVE_SSL = False
u = unicode
zsid = uuid.uuid4().urn[4:]
CFG_DEF = [os.environ.get("PRTY_CONFIG", "")]
if not CFG_DEF[0]:
CFG_DEF.pop()
FULL_HELP = os.environ.get("PRTY_FULL_HELP", "")
@@ -586,6 +591,17 @@ def sfx_tpoke(top: str):
lprint("<TPOKE> [%s] %r" % (f, ex))
files.remove(f)
r = os.path.dirname(top)
for d in os.listdir(r):
try:
p = r + "/" + d
f = p + "/copyparty/up2k.py"
t2 = os.path.getmtime(f)
if t - t2 > 777777 and d.startswith("pe-copyparty"):
shutil.rmtree(p)
except:
pass
time.sleep(78123)
@@ -596,12 +612,64 @@ def showlic() -> None:
except:
buf = b""
if buf:
print(buf.decode("utf-8", "replace"))
else:
if not buf:
print("no relevant license info to display")
return
lic = buf.decode("utf-8", "replace")
try:
import site
from importlib import import_module
from inspect import getsourcefile
bases = site.PREFIXES + [site.USER_BASE]
bases = [x.replace(os.sep, "/") + "/" for x in list(set(bases)) if x]
except:
bases = []
# remove missing and/or non-vendored
for url, zi, zx in (
("/dnslib/", 1, DNS_VND),
("/ifaddr/", 1, IFADDR_VND),
("/nayuki/QR", 1, QR_VND),
("/partftpy", 2, PARTFTPY_VER),
("/jinja/", 3, "jinja2"),
("/markupsafe/", 3, "jinja2"), # jinja2 sic
("/pyftpdlib/", 2, PYFTPD_VER), # heed PRTY_NO
("/pyftpdlib/", 3, "pyftpdlib"),
("/python-magic/", 1, HAVE_MAGIC), # heed PRTY_NO
("/python-magic/", 3, "magic"),
):
if url not in lic:
continue
elif zi == 1:
if zx:
continue
elif zi == 2:
if zx != "(None)":
continue
elif zi == 3:
mod = None
try:
assert bases and getsourcefile and import_module # type: ignore # !rm
mod = import_module(zx) # type: ignore
mp = getsourcefile(mod) or ""
for zs in bases:
if mp.startswith(zs):
mp = ""
if mp:
continue
except:
if mod:
continue
zs1, zs2 = lic.split(url, 1)
zs1 = zs1.rsplit("\n", 1)[0]
zs2 = zs2.split("\n\n", 1)[1]
lic = zs1 + "\n" + zs2
print(lic)
def get_sects():
return [
@@ -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:
+2 -2
View File
@@ -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
View File
@@ -13,7 +13,7 @@ import threading
import time
from datetime import datetime
from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, WINDOWS, E
from .__init__ import ANYWIN, CFG_DEF, MACOS, PY2, TYPE_CHECKING, WINDOWS, E
from .bos import bos
from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap
from .pwhash import PWHash
@@ -25,6 +25,7 @@ from .util import (
FN_EMB,
HAVE_SQLITE3,
IMPLICATIONS,
LOG,
META_NOBOTS,
MIMES,
SQLITE_VER,
@@ -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
View File
@@ -1,3 +1,6 @@
# coding: utf-8
from __future__ import division, print_function, unicode_literals
import calendar
import errno
import json
@@ -8,6 +11,7 @@ import time
from .__init__ import ANYWIN
from .util import Netdev, atomic_move, load_resource, runcmd, wunlink
NO_TLS = bool(os.environ.get("PRTY_NO_TLS"))
HAVE_CFSSL = not os.environ.get("PRTY_NO_CFSSL")
if True: # pylint: disable=using-constant-test
@@ -70,8 +74,9 @@ def ensure_cert(log: "RootLogger", args) -> None:
with open(args.cert, "rb") as f:
active_cert = f.read()
if active_cert == cert_insec:
t = "using default TLS certificate; https will be insecure:\033[36m {}"
log("cert", t.format(args.cert), 3)
t = "using default TLS certificate; %s will be insecure:\033[36m %s"
t2 = "https" if not args.ftps else "ftps"
log("cert", t % (t2, args.cert), 3)
except:
pass
@@ -252,7 +257,7 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
def gencert(log: "RootLogger", args, netdevs: dict[str, Netdev]):
global HAVE_CFSSL
if args.http_only:
if args.http_only and not args.ftps:
return
if args.no_crt or args.certkey or not HAVE_CFSSL:
+11
View File
@@ -22,6 +22,7 @@ def vf_bmap() -> dict[str, str]:
"no_dupe_m": "nodupem",
"no_forget": "noforget",
"no_html": "nohtml",
"no_mime": "nomime",
"no_pipe": "nopipe",
"no_robots": "norobots",
"no_script": "noscript",
@@ -33,6 +34,7 @@ def vf_bmap() -> dict[str, str]:
"vol_or_crash": "assert_root",
}
for k in (
"allow_svg_js",
"dedup",
"dlni",
"dothidden",
@@ -50,6 +52,7 @@ def vf_bmap() -> dict[str, str]:
"grid",
"gsel",
"hardlink",
"hist_cow",
"magic",
"md_no_br",
"no_db_ip",
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -2,28 +2,29 @@
from __future__ import division, print_function, unicode_literals
import argparse # typechk
import os
import re
import socket
import threading # typechk
import time
try:
if os.environ.get("PRTY_NO_TLS"):
raise Exception()
HAVE_SSL = True
import ssl
except:
HAVE_SSL = False
from . import util as Util
from .__init__ import TYPE_CHECKING, EnvParams
from .authsrv import AuthSrv # typechk
from .cert import NO_TLS
from .httpcli import HttpCli
from .u2idx import U2idx
from .util import HMaccas, NetMap, min_ex, shut_socket
try:
if NO_TLS:
raise Exception()
import ssl
HAVE_SSL = True
except:
HAVE_SSL = False
if True: # pylint: disable=using-constant-test
from typing import Optional, Pattern, Union
+1 -2
View File
@@ -2,7 +2,6 @@
from __future__ import division, print_function, unicode_literals
import hashlib
import math
import os
import re
import socket
@@ -180,7 +179,7 @@ class HttpSrv(object):
self.u2idx_free: dict[str, U2idx] = {}
self.u2idx_n = 0
self.wopi_files: dict[str, dict[str, str]] = {}
self.wopi_files: dict[str, dict[str, Any]] = {}
assert jinja2 # type: ignore # !rm
env = jinja2.Environment()
-1
View File
@@ -51,7 +51,6 @@ except ImportError:
SRV,
TXT,
A,
Bimap,
DNSHeader,
DNSQuestion,
DNSRecord,
+15 -8
View File
@@ -30,6 +30,9 @@ from .util import (
sfsenc,
uncyg,
wunlink,
zip_fi,
zip_lim,
zip_readf,
)
if True: # pylint: disable=using-constant-test
@@ -174,7 +177,7 @@ def au_unpk(
zil = [x for x in zil if x.filename.lower().split(".")[-1] == au]
if not zil:
raise Exception("no audio inside zip")
fi = zf.open(zil[0])
fi = zf.open(zip_lim(zil[0], maxsz))
elif pk == "cbz":
import zipfile
@@ -188,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
View File
@@ -14,8 +14,9 @@ try:
if os.environ.get("PRTY_NO_ARGON2"):
raise Exception()
HAVE_ARGON2 = True
from argon2 import exceptions as argon2ex
HAVE_ARGON2 = True
except:
HAVE_ARGON2 = False
+55 -29
View File
@@ -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
View File
@@ -12,7 +12,7 @@ from types import SimpleNamespace
from .__init__ import ANYWIN, EXE, TYPE_CHECKING
from .authsrv import LEELOO_DALLAS, VFS
from .bos import bos
from .util import Daemon, absreal, min_ex, pybin, runhook, vjoin
from .util import Daemon, absreal, min_ex, pybin, runhook
if True: # pylint: disable=using-constant-test
from typing import Any, Union
-1
View File
@@ -5,7 +5,6 @@ import errno
import re
import select
import socket
import time
from .__init__ import TYPE_CHECKING
from .multicast import MC_Sck, MCast
+8 -1
View File
@@ -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
View File
@@ -81,7 +81,6 @@ from .util import (
alltrace,
build_netmap,
expat_ver,
fsenc,
gzip,
html_escape,
load_ipr,
@@ -133,6 +132,19 @@ VER_SHARES_DB = 2
CVE_SEVS = {"low": 1, "medium": 2, "moderate": 2, "high": 3, "critical": 4}
def _build_th_fset(f: str, fadd: str) -> set[str]:
fset = {x for x in f.replace(" ", "").split(",")}
rmset = {""}
for x in fadd.replace(" ", "").split(","):
if x.startswith("-"):
rmset.add(x.lstrip("-"))
else:
fset.add(x)
return fset - rmset
class SvcHub(object):
"""
Hosts all services which cannot be parallelized due to reliance on monolithic resources.
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -34,6 +34,7 @@ class ThumbCli(object):
self.cooldown = Cooldown(self.args.th_poke) if self.args.th_poke else None
self.thumbable = c["thumbable"]
self.thumbable_native = c["thumbable_native"]
self.fmt_pil = c["pil"]
self.fmt_vips = c["vips"]
self.fmt_raw = c["raw"]
@@ -59,6 +60,10 @@ class ThumbCli(object):
if is_vid and "dvthumb" in dbv.flags:
return None
is_custom = ext not in self.thumbable_native
if is_custom and "dethumb" in dbv.flags:
return None
want_opus = fmt in EXTS_AC
is_au = ext in self.fmt_ffa
is_vau = want_opus and ext in self.fmt_ffv
@@ -76,7 +81,7 @@ class ThumbCli(object):
elif want_opus:
return None
is_img = not is_vid and not is_au
is_img = not is_vid and not is_au and not is_custom
if is_img and "dithumb" in dbv.flags:
return None
@@ -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
View File
@@ -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
View File
@@ -4,12 +4,9 @@ from __future__ import division, print_function, unicode_literals
import errno
import hashlib
import json
import math
import os
import re
import shutil
import stat
import subprocess as sp
import sys
import tempfile
import threading
@@ -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
View File
@@ -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
+2 -1
View File
@@ -7,7 +7,8 @@ pk: $(addsuffix .gz, $(wildcard tl/*.js *.js *.css) \
un: $(addsuffix .un, $(wildcard tl/*.gz *.gz a/*.gz))
tl/%.js.gz: tl/%.js
./Makefile.s1 <$< | pigz -c11 -J 34 -I 573 >$@
sh Makefile.s1 <$< | pigz -c11 -J 34 -I 573 >$@
pigz -dc <$@ | grep -q ^Ls
touch -r $< $@
rm $<
+1
View File
@@ -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) {
+4
View File
@@ -1768,6 +1768,10 @@ html.y #tree.nowrap .ntree a+a:hover {
top: .3em;
z-index: 9;
}
.dumb_loader_thing.ns {
animation: none;
opacity: 1;
}
#dlt_t {
left: 0;
}
+70 -36
View File
@@ -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;
+3
View File
@@ -325,6 +325,9 @@ var modpoll = new Modpoll();
window.onbeforeunload = function (e) {
if (window.gtfo)
return;
if ((ebi("save").className + '').indexOf('disabled') >= 0)
return; //nice (todo)
+10 -10
View File
@@ -54,7 +54,7 @@
<div class="os win">
<p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p>
<pre>
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
rclone config create --obscure {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s --network-mode {{ aname }}-dav:{{ rvp }} <b>W:</b>
</pre>
<ul>
@@ -77,7 +77,7 @@
<div class="os lin">
<p>rclone (v1.63 or later) is recommended:</p>
<pre>
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
rclone config create --obscure {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>mp</b>
</pre>
<ul>
@@ -145,9 +145,9 @@
<p>connect with plaintext FTP:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
rclone config create --obscure {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
{%- else %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
rclone config create --obscure {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s --network-mode {{ aname }}-ftp:{{ rvp }} <b>W:</b>
</pre>
@@ -156,9 +156,9 @@
<p>connect with TLS-encrypted FTPS:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
rclone config create --obscure {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
{%- else %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
rclone config create --obscure {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s --network-mode {{ aname }}-ftps:{{ rvp }} <b>W:</b>
</pre>
@@ -184,9 +184,9 @@
<p>connect with plaintext FTP:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
rclone config create --obscure {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
{%- else %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
rclone config create --obscure {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>mp</b>
</pre>
@@ -195,9 +195,9 @@
<p>connect with TLS-encrypted FTPS:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
rclone config create --obscure {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
{%- else %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
rclone config create --obscure {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>mp</b>
</pre>
+2
View File
@@ -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": "复制链接",
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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
+2
View File
@@ -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&amp;drop μεταφορτώσεις",
"u_notdir": "αυτός δεν είναι φάκελος!\n\nο browser σου είναι πολύ παλιός,\nδοκίμασε drag&amp;drop αντ' αυτού",
"u_uri": "για να κάνεις drag&amp;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
+2
View File
@@ -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',
+2
View File
@@ -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
+2
View File
@@ -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": "リンクをコピー",
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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`&quot;try unite&quot;` = содержит именно «try unite»$N$Nформат времени задаётся по стандарту iso-8601, например$N`2009-12-31` или `2020-09-12 23:30:00`",
"ot_unpost": "unpost: удалить ваши недавние загрузки и отменить незавершённые",
"ot_bup": "bup: легковесный загрузчик файлов, поддерживает даже netscape 4.0",
"ot_mkdir": "mkdir: создать новую папку",
"ot_md": "new-file: создать новый текстовый файл", //m
"ot_md": "new-file: создать новый текстовый файл",
"ot_msg": "msg: отправить сообщение в лог сервера",
"ot_mp": "настройка медиаплеера",
"ot_cfg": "остальные настройки",
@@ -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(например, файл с &gt;code&lt;2&gt;/code&lt; в начале названия идёт перед &gt;code&lt;11&gt;/code&lt;)">нат. сорт.',
"ct_utc": 'используйте UTC для всех временных меток">UTC', //m
"ct_utc": 'показывать все даты в UTC">UTC',
"ct_readme": 'показывать содержимое README.md в описании папки">📜 ридми',
"ct_idxh": 'показывать страницу index.html в текущей папке вместо интерфейса">htm',
"ct_sbars": 'показывать полосы прокрутки">⟊',
@@ -277,8 +277,8 @@ Ls.rus = {
"tt_dynt": "автоматическое расширение панели",
"tt_wrap": "перенос слов",
"tt_hover": "раскрывать обрезанные строки при наведении$N( ломает скроллинг, если $N&nbsp; курсор не в пустоте слева )",
"tt_gauto": "показывать как сетку или список в зависимости от содержимого папки", //m
"tt_gathr": "использовать сетку, если этот процент файлов — изображения/видео", //m
"tt_gauto": "показывать сетку или список в зависимости от содержимого папки",
"tt_gathr": "использовать сетку, если этот процент файлов — изображения/видео",
"ml_pmode": "в конце папки...",
"ml_btns": "команды",
@@ -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 &nbsp;` = обычное стерео$Nwidth `0.5` = микширование левого и правого каналов на 50%$Nwidth `0 &nbsp;` = моно$N$Nboost `-0.8` и width `10` = удаление голоса :^)$N$Nвключённый эквалайзер полностью убирает задержку между треками, поэтому следует его включить со всеми значениями на 0 (кроме width = 1), если вам нужно бесшовное воспроизведение",
"mt_drc": "включить компрессор; также включит эквалайзер для баланса вселенной, так что выставьте всё на 0 кроме width, если он вам не нужен$N$Nпонижает громкость при волне выше значения dB в tresh; каждый dB в ratio равен одному dB на выходе, так что стандартные значения tresh = -24 и ratio = 12 сделают так, что звук никогда не будет громче -22 dB. При таком раскладе можно поставить boost в эквалайзере на 0.8 или даже 1.8 при значении atk = 0 и огромном rls вроде 90 (работает только в Firefox, rls не может быть выше 1 в других браузерах)$N$N(загляните в википедию, там всё объяснено подробнее)",
"mt_ss": "`включает пропуск тишины; умножает скорость на `уск` возле начала/конца, когда громкость ниже `гром` и позиция в первых `нач`% или последних `кон`%", //m
"mt_ssvt": "порог громкости (0-255)\">гром", //m
"mt_ssts": "активный порог (% трека, начало)\">нач", //m
"mt_sste": "активный порог (% трека, конец)\">кон", //m
"mt_sssm": "множитель скорости воспроизведения\">уск", //m
"mt_ss": "`включает пропуск тишины; умножает скорость на `уск` возле начала/конца, когда громкость ниже `гром` и позиция в первых `нач`% или последних `кон`%",
"mt_ssvt": "порог громкости (0-255)\">гром",
"mt_ssts": "активный порог (% трека, начало)\">нач",
"mt_sste": "активный порог (% трека, конец)\">кон",
"mt_sssm": "множитель ускорения\">уск",
"mb_play": "играть",
"mm_hashplay": "воспроизвести этот музыкальный файл?",
@@ -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": "выбрать документ &nbsp; ( для вырезания / копирования / удаления / ... )$NГорячая клавиша: S\">выд",
"tvt_j": "приукрасить json$NГорячая клавиша: shift-J\">j", //m
"tvt_j": "форматировать json для чтения$NГорячая клавиша: shift-J\">j",
"tvt_edit": "открыть документ в текстовом редакторе$NГорячая клавиша: E\">✏️ изменить",
"tvt_tail": "проверять файл на изменения; показывать новые строки в реальном времени\">📡 обновлять",
"tvt_wrap": "перенос слов\">↵",
@@ -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 не найдено &nbsp;┐( ´ -`)┌",
"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",
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
View File
@@ -888,9 +888,8 @@ function up2k_init(subtle) {
setmsg(suggest_up2k, 'msg');
var u2szs = u2sz.split(','),
u2sz_min = parseInt(u2szs[0]),
u2sz_tgt = parseInt(u2szs[1]),
u2sz_max = parseInt(u2szs[2]);
u2sz_tgt = parseInt(u2szs[0]),
u2sz_max = parseInt(u2szs[1]);
var parallel_uploads = ebi('nthread').value = icfg_get('nthread', u2j),
stitch_tgt = ebi('u2szg').value = icfg_get('u2sz', u2sz_tgt),
@@ -980,6 +979,7 @@ function up2k_init(subtle) {
"u": 0,
"t": 0
},
"xhr": [],
"car": 0,
"nre": 0,
"slow_io": null,
@@ -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
View File
@@ -46,6 +46,9 @@ var wah = '',
if (!window.WebAssembly || !WebAssembly.Memory)
window.WebAssembly = false;
if (!window.AbortController)
window.AbortController = false;
if (!window.Notification || !Notification.permission)
window.Notification = false;
@@ -108,6 +111,13 @@ function qsr(sel) {
}
function relod() {
crashed = 1;
window.gtfo = 1;
location.href = location.href;
}
// error handler for mobile devices
function esc(txt) {
return txt.replace(/[&"<>]/g, function (c) {
@@ -289,7 +299,7 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
var x = ebi('exh_wipecfg');
if (x) x.onclick = function () {
localStorage.clear();
location.reload();
relod();
};
x = ebi('exh_ignex'); if (x) x.onclick = ignex;
x = ebi('exh_ignexa'); if (x) x.onclick = ignexa;
@@ -443,8 +453,11 @@ function import_js(url, cb, ecb) {
var head = document.head || document.getElementsByTagName('head')[0];
var script = mknod('script');
script.type = 'text/javascript';
if (window.JS_NONCE)
if (window.JS_NONCE) {
script.nonce = JS_NONCE;
if (!AbortController)
script.setAttribute('nonce', JS_NONCE);
}
script.src = url + '?_=' + (window.TS || 'a');
script.onload = cb;
script.onerror = ecb || function () {
@@ -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);
+1 -1
View File
@@ -27,7 +27,7 @@ body {
<div style="display: none">
<form action="{{ url }}" enctype="multipart/form-data" method="post" target="w" id="submit-form">
<input name="access_token" value="{{ atoken }}" type="hidden" id="access-token"/>
<input name="access_token_ttl" value="{{ ttl }}000" type="hidden" id="access-token_ttl"/>
<input name="access_token_ttl" value="{{ ttl }}" type="hidden" id="access-token_ttl"/>
<input type="submit" value="" />
</form>
</div>
+188
View File
@@ -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
+54
View File
@@ -0,0 +1,54 @@
drop tcp mid-chunk and force browser to re-hs before it can be retried
ok with subchunks as of writing; 6thr, u2sz=1
303M file: --s-wr-slp 0.02 --iobuf 131072
33.5M file: --s-wr-slp 0.1 --iobuf 131072
--- a/copyparty/httpcli.py
+++ b/copyparty/httpcli.py
@@ -234,2 +234,3 @@ ZIP_XCODE_L = _build_zip_xcode()
ZIP_XCODE_S = set(ZIP_XCODE_L)
+hcblk = ""
@@ -3154,2 +3155,3 @@ class HttpCli(object):
def handle_post_json(self) -> bool:
+ global hcblk
try:
@@ -3248,2 +3250,3 @@ class HttpCli(object):
# not to protect u2fh, but to prevent handshakes while files are closing
+ hcblk = ""
with self.u2mutex:
@@ -3335,2 +3338,3 @@ class HttpCli(object):
def handle_post_binary(self) -> bool:
+ global hcblk
try:
@@ -3452,2 +3456,4 @@ class HttpCli(object):
)
+ if chash == hcblk:
+ raise Exception("fhdsafhad re")
post_sz, _, sha_b64 = hashcopy(
@@ -3513,3 +3519,6 @@ class HttpCli(object):
self.u2fh.put(path, f)
- except:
+ except Exception as ex:
+ if "fhdsafhad" in str(ex):
+ hcblk = chashes[0]
+ self.sr.s.close()
# maybe busted handle (eg. disk went full)
--- a/copyparty/util.py
+++ b/copyparty/util.py
@@ -3414,2 +3414,3 @@ def eol_conv(
+hcn = 0
def hashcopy(
@@ -3421,2 +3422,4 @@ def hashcopy(
) -> tuple[int, str, str]:
+ global hcn
+ print(hcn)
if not hashobj:
@@ -3429,2 +3432,5 @@ def hashcopy(
+ hcn += 1
+ if hcn in (24, 121, 300, 600, 900):
+ raise Exception("fhdsafhad " + str(tlen))
hashobj.update(buf)
+1
View File
@@ -97,6 +97,7 @@ copyparty = [
"web/*.css",
"web/*.html",
"web/*.xml",
"web/*.gif",
"web/tl/*.js",
"web/tl/*.gz",
"web/a/*.txt",
+1 -1
View File
@@ -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
-173
View File
@@ -1,173 +0,0 @@
#!/bin/bash
repacker=1
set -e
# -- download latest copyparty (source.tgz and sfx),
# -- build minimal sfx versions,
# -- create a .tar.gz bundle
#
# convenient for deploying updates to inconvenient locations
# (and those are usually linux so bash is good inaff)
# (but that said this even has macos support)
#
# output summary (filesizes and contents):
#
# 550760 copyparty-extras/sfx-full/copyparty-sfx.py
# `- original unmodified sfx from github
#
# 572923 copyparty-extras/sfx-full/copyparty-sfx-gz.py
# `- unmodified but recompressed from bzip2 to gzip
#
# 353975 copyparty-extras/sfx-ent/copyparty-sfx.py
# 376934 copyparty-extras/sfx-ent/copyparty-sfx-gz.py
# `- removed iOS ogg/opus/vorbis audio decoder,
# removed the audio tray mouse cursor,
# "enterprise edition"
#
# 270004 copyparty-extras/sfx-lite/copyparty-sfx.py
# 293159 copyparty-extras/sfx-lite/copyparty-sfx-gz.py
# `- also removed the codemirror markdown editor
# and the text-viewer syntax hilighting,
# only essential features remaining
#
# 646297 copyparty-extras/copyparty-1.0.14.tar.gz
# 4823 copyparty-extras/copyparty-repack.sh
# `- source files from github
#
# 23663 copyparty-extras/u2c.py
# `- standalone utility to upload or search for files
#
# 32280 copyparty-extras/partyfuse.py
# `- standalone to mount a URL as a local read-only filesystem
#
# 270004 copyparty
# `- minimal binary, same as sfx-lite/copyparty-sfx.py
command -v gnutar && tar() { gnutar "$@"; }
command -v gtar && tar() { gtar "$@"; }
command -v gsed && sed() { gsed "$@"; }
td="$(mktemp -d)"
od="$(pwd)"
cd "$td"
pwd
dl_text() {
command -v curl >/dev/null && exec curl "$@"
exec wget -O- "$@"
}
dl_files() {
command -v curl >/dev/null && exec curl -L --remote-name-all "$@"
exec wget "$@"
}
export -f dl_files
# if cache exists, use that instead of bothering github
cache="$od/.copyparty-repack.cache"
[ -e "$cache" ] &&
tar -xf "$cache" ||
{
# get download links from github
dl_text https://api.github.com/repos/9001/copyparty/releases/latest |
(
# prefer jq if available
jq -r '.assets[]|select(.name|test("-sfx|tar.gz")).browser_download_url' ||
# fallback to awk (sorry)
awk -F\" '/"browser_download_url".*(\.tar\.gz|-sfx\.)/ {print$4}'
) |
grep -E '(sfx\.py|tar\.gz)$' |
tee /dev/stderr |
tr -d '\r' | tr '\n' '\0' |
xargs -0 bash -c 'dl_files "$@"' _
tar -czf "$cache" *
}
# move src into copyparty-extras/,
# move sfx into copyparty-extras/sfx-full/
mkdir -p copyparty-extras/sfx-{full,ent,lite}
mv copyparty-sfx.* copyparty-extras/sfx-full/
mv copyparty-*.tar.gz copyparty-extras/
# unpack the source code
( cd copyparty-extras/
tar -xf *.tar.gz
)
# use repacker from release if that is newer
p_other=copyparty-extras/copyparty-*/scripts/copyparty-repack.sh
other=$(awk -F= 'BEGIN{v=-1} NR<10&&/^repacker=/{v=$NF} END{print v}' <$p_other)
[ $repacker -lt $other ] &&
cat $p_other >"$od/$0" && cd "$od" && rm -rf "$td" && exec "$0" "$@"
# now drop the cache
rm -f "$cache"
# fix permissions
chmod 755 \
copyparty-extras/sfx-full/* \
copyparty-extras/copyparty-*/{scripts,bin}/*
# extract the sfx
( cd copyparty-extras/sfx-full/
./copyparty-sfx.py --version
)
repack() {
# do the repack
(cd copyparty-extras/copyparty-*/
./scripts/make-sfx.sh $2
)
# put new sfx into copyparty-extras/$name/,
( cd copyparty-extras/
mv copyparty-*/dist/* $1/
)
}
repack sfx-full "re gz"
repack sfx-ent "re"
repack sfx-ent "re gz"
repack sfx-lite "re no-cm no-hl"
repack sfx-lite "re no-cm no-hl gz"
# move fuse and up2k clients into copyparty-extras/,
# copy lite-sfx.py to ./copyparty,
# delete extracted source code
( cd copyparty-extras/
mv copyparty-*/bin/u2c.py .
mv copyparty-*/bin/partyfuse.py .
cp -pv sfx-lite/copyparty-sfx.py ../copyparty
rm -rf copyparty-{0..9}*.*.*{0..9}
)
# and include the repacker itself too
cp -av "$od/$0" copyparty-extras/ ||
cp -av "$0" copyparty-extras/ ||
true
# create the bundle
printf '\n\n'
fn=copyparty-$(date +%Y-%m%d-%H%M%S).tgz
tar -czvf "$od/$fn" *
cd "$od"
rm -rf "$td"
echo
echo "done, here's your bundle:"
ls -al "$fn"
+1 -1
View File
@@ -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 \
+9 -1
View File
@@ -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
+1 -1
View File
@@ -29,7 +29,7 @@ done
getver() {
ver=$(
python3 ../../dist/copyparty-sfx.py --version 2>/dev/null |
PRTY_XD=/tmp python3 ../../dist/copyparty-sfx.py --version 2>/dev/null |
awk '/^copyparty v/{sub(/-.*/,"");sub(/v/,"");print$2;exit}'
)
echo $ver | grep -E '[0-9]\.[0-9]' || {
+1 -1
View File
@@ -15,7 +15,7 @@ command -v gsort >/dev/null && {
cd ../dist
kwds='-bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -urlform -exp -rlo -ls -dbd -chmod -pwhash -zm'
kwds='-bind -accounts -auth -auth-ord -flags -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
+2 -2
View File
@@ -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
View File
@@ -10,8 +10,8 @@ command -v gsort >/dev/null && {
( ( HLPTXT=CAT && cat || xsel -ob ) | sed -r '
s`/home/ed/`~/`;
s/uuid:[0-9a-f-]{36}/autogenerated/;
s/(-salt SALT.*default: )[0-9a-zA-Z/+]{24}\)/\124-character-autogenerated)/;
s/(-salt SALT.*default: )[0-9a-zA-Z/+]{40}\)/\140-character-autogenerated)/;
s/(-salt (SALT|TXT).*default: )[0-9a-zA-Z/+]{24}\)/\124-character-autogenerated)/;
s/(-salt (SALT|TXT).*default: )[0-9a-zA-Z/+]{40}\)/\140-character-autogenerated)/;
s/(--name TXT.*default: )[^)]+/\1hostname/;
s/(--hash-mt CORES.*default: )[0-9]+/\1numCores if 5 or less/;
s/(--mtag-mt|th-mt)( CORES.*default: )[0-9]+/\1\2numCores/;
@@ -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
View File
@@ -193,6 +193,8 @@ tmpdir="$(
printf '%s\n' "$TMPDIR" /tmp |
awk '/./ {print; exit}'
)"
[ -e ~/.cache ] && tmpdir=~/.cache
[ "$PRTY_XD" ] && tmpdir="$PRTY_XD"
necho() {
printf '\033[G%s ... \033[K' "$*"
@@ -513,7 +515,6 @@ unhelpg() {
[ $no_hl ] &&
rm -rf copyparty/web/deps/prism*
rm -f copyparty/web/deps/orbitron.woff2 # todo:uiv15
[ $no_fnt ] && {
rm -f copyparty/web/deps/scp.woff2
f=copyparty/web/ui.css
+7 -1
View File
@@ -68,7 +68,7 @@ scripts/genlic.py "$rls_dir/copyparty/res/COPYING.txt"
cd "$rls_dir"
find -type d -exec chmod 755 '{}' \+
find -type f -exec chmod 644 '{}' \+
find -type f -exec chmod u+rw,go+r '{}' \+
commaver="$(
printf '%s\n' "$ver" |
@@ -99,6 +99,12 @@ rm -f \
cp -pv LICENSE LICENSE.txt
mv setup.py{,.disabled}
( cd copyparty/web
make -j$(nproc)
rm Makefile*
printf 'all:\n\t@echo "NOTE: no longer necessary to run copyparty/web/Makefile when building from source tarball"\n' >Makefile
)
# the regular cleanup memes
find -name '*.pyc' -delete
find -name __pycache__ -delete
+8 -1
View File
@@ -16,7 +16,7 @@ uname -s | grep WOW64 && m=64 || m=32
uname -s | grep NT-10 && w10=1 || w7=1
[ $w7 ] && [ -e up2k.sh ] && [ ! "$1" ] && ./up2k.sh
[ $w7 ] && pyv=37 || pyv=313
[ $w7 ] && pyv=37 || pyv=314
[ $w7 ] && sfx=en || sfx=sfx
esuf=
[ $w7 ] && [ $m = 32 ] && esuf=32
@@ -42,6 +42,7 @@ dl https://192.168.123.1:3923/cpp/scripts/pyinstaller/loader.rc
[ $st_en ] && [ $st_en -ge $st_sfx ] || sfx=sfx
}
export PRTY_XD=$TEMP
rm -rf $TEMP/pe-copyparty*
python copyparty-$sfx.py --version
@@ -67,6 +68,7 @@ rm -rf mods/magic/
sed -ri 's/\binspect\b/os/' $spkgs/jinja2/$f.py
done
}
af '/print\(lic/{s=0}!s;/lic = buf.deco/{s=1}' mods/copyparty/__main__.py
sed -ri /fork_process/d mods/pyftpdlib/servers.py
af '/^class _Base/{s=1}!s' mods/pyftpdlib/authorizers.py
@@ -100,9 +102,14 @@ excl=(
)
[ $w10 ] && excl+=(
_pyrepl
bz2 # 51k
compression.zstd # 195k
concurrent.interpreters # 12k
distutils
setuptools
ftplib # 23k
PIL._avif
PIL._imagingft # 861k (freetype)
PIL.ImageQt
PIL.ImageShow
PIL.ImageTk
+11 -11
View File
@@ -1,7 +1,7 @@
f117016b1e6a7d7e745db30d3e67f1acf7957c443a0dd301b6c5e10b8368f2aa4db6be9782d2d3f84beadd139bfeef4982e40f21ca5d9065cb794eeb0e473e82 altgraph-0.17.4-py2.py3-none-any.whl
76e42806c52319081201d9fd9101827d36d9dbdd34e04f8a48736f2e3b37d68e79e8f52e43260d3eaaa46fe149d95b54ef1355af24361d0bf190dced93cd77bb altgraph-0.17.5-py2.py3-none-any.whl
17ce52ba50692a9d964f57a23ac163fb74c77fdeb2ca988a6d439ae1fe91955ff43730c073af97a7b3223093ffea3479a996b9b50ee7fba0869247a56f74baa6 pefile-2023.2.7-py3-none-any.whl
b297ff66ec50cf5a1abcf07d6ac949644c5150ba094ffac974c5d27c81574c3e97ed814a47547f4b03a4c83ea0fb8f026433fca06a3f08e32742dc5c024f3d07 pywin32_ctypes-0.2.3-py3-none-any.whl
fd88d00c7231f9fcbe0fb9037217b6ae5dcb5b06a98be56243400542671a32f003d594b2e1852f77d316e9327ad4c643321e1f9f3ea20cc8500ac5bba8ae170e upx-5.0.2-win32.zip
76724b04d6fdfadb46f27b6fd5753429d597642d1035ff7bf52fe1b6f5bc18f129567b738a40436adf03afd3a38708dc268de3c1b5d4ef9a89968335805ead72 upx-5.2.0-win32.zip
# win7
d9d30ff960365307833425e340bf0a642fd54e35155f18ff1d9746bc1ecf8ecb864aad24948735e8fd994503b659dc8968e6ba337eeb4568217ebdf97dd292dd Git-2.46.2-32-bit.exe
3253e86471e6f9fa85bfdb7684cd2f964ed6e35c6a4db87f81cca157c049bef43e66dfcae1e037b2fb904567b1e028aaeefe8983ba3255105df787406d2aa71e en_windows_7_professional_with_sp1_x86_dvd_u_677056.iso
@@ -21,15 +21,15 @@ ea73aa54cc6d5db20dfb127e54562dabf890e4cd6171a91b10a51af2bcfc76e1d64cbdce4546df2d
479a63e14586ab2f2228208116fc149ed8ee7b1e4ff360754f5bda4bf765c61af2e04b5ef123976623d04df4976b7886e0445647269da81436bd0a7b5671d361 windows6.1-kb2533623-x86.msu
ac96786e5d35882e0c5b724794329c9125c2b86ae7847f17acfc49f0d294312c6afc1c3f248655de3f0ccb4ca426d7957d02ba702f4a15e9fcd7e2c314e72c19 zipp-3.15.0-py3-none-any.whl
# win10
f4b4e330995ebe96c0bd06e16e5b26062ece9473f06d369775aa68eab261dedcf32dfdd159acaa227502bbf9fa2cd8bbe57cddb89fc6f2196fef7a9ed5a80ae9 Git-2.51.0-64-bit.exe
ef723f34ce8c0c1f8ef9eb7746f77e4ab6129aa7c7dcf501bee56f522ff2c57ff013f9a69db141ad04c9d1b90b4f1bbd22b6028218e3ae68eb4efec861d63f65 Git-2.55.0.3-64-bit.exe
0a2cd4cadf0395f0374974cd2bc2407e5cc65c111275acdffb6ecc5a2026eee9e1bb3da528b35c7f0ff4b64563a74857d5c2149051e281cc09ebd0d1968be9aa en-us_windows_10_enterprise_ltsc_2021_x64_dvd_d289cf96.iso
4f13070cc9241fa342deab4ebfac360565030580ff77b6e5f1951a64627621e5da4abfd30e1e46ca8bae2bb7dd4ff98141aff424142c9629a5876a61283962e5 virtio-win-0.1.285.iso
9a7f40edc6f9209a2acd23793f3cbd6213c94f36064048cb8bf6eb04f1bdb2c2fe991cb09f77fe8b13e5cd85c618ef23573e79813b2fef899ab2f290cd129779 jinja2-3.1.6-py3-none-any.whl
00731cfdd9d5c12efef04a7161c90c1e5ed1dc4677aa88a1d4054aff836f3430df4da5262ed4289c21637358a9e10e5df16f76743cbf5a29bb3a44b146c19cf3 MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl
8a6e2b13a2ec4ef914a5d62aad3db6464d45e525a82e07f6051ed10474eae959069e165dba011aefb8207cdfd55391d73d6f06362c7eb247b08763106709526e mutagen-1.47.0-py3-none-any.whl
a726fb46cce24f781fc8b55a3e6dea0a884ebc3b2b400ea74aa02333699f4955a5dc1e2ec5927ac72f35a624401f3f3b442882ba1cc4cadaf9c88558b5b8bdae packaging-25.0-py3-none-any.whl
efcb9da8e6f50cb5ea8e1d41e631e89b6f852b68a2568619a6ec151267e7fac94aba1e36317341c4fc5df822215b7ab9dd6b33cfb738e1b8423d4f0f6cff9525 pillow-12.3.0-cp313-cp313-win_amd64.whl
b9b98714dfca6fa80b0b3f222965724d63be9c54d19435d1fe768e07016913d6db8d6e043fcb185b55a9bd6fe370a80cf961814fc096046a5f4640d99ed575ef pyinstaller-6.15.0-py3-none-win_amd64.whl
cad0f7cf39de691813b1d4abc7d33f8bda99a87d9c5886039b814752e8690364150da26fb61b3e28d5698ff57a90e6dcd619ed2b64b04f72b5aadb75e201bdb0 pyinstaller_hooks_contrib-2025.8-py3-none-any.whl
d0d23fd4ae8900764df664803844155994b253ed449b9ed54ea020fbfa7aaea8800ca1928e4a7f26b9278613f8bce27ef9a424cfcc4ca15551c7e4e1b78f3991 python-3.13.14-amd64.exe
2a0420f7faaa33d2132b82895a8282688030e939db0225ad8abb95a47bdb87b45318f10985fc3cee271a9121441c1526caa363d7f2e4a4b18b1a674068766e87 setuptools-80.9.0-py3-none-any.whl
52adad30ac608695a6345468144048e5134696313ab775ef72f41ca082f8fa19953e2b9924fb5970f829bf9188183fe616b3942c4705eeddfe4ce223a5940186 markupsafe-3.0.3-cp314-cp314-win_amd64.whl
c7668214758cb79a6ba0f7dbffb4b1b6f7cbf98d8ab6cbe77ab42c891916de9339bf17e3de3c3f2d20f6065b49f97c6d521419cc0a2b6c6b8940cf4317172d71 mutagen-1.48.1-py3-none-any.whl
e9492eedaac7cfddde6d84389ef7df86c68275e8f29d838fc431d9bf379401bf6d82ec70ac02461995982613c80592cd8de74c16a3654d7d8fdc06f96057d6e7 packaging-26.3-py3-none-any.whl
8ea9946d8484a6de833a97d3cd545c2b67475207944856281cffb2c513ab14bf50e2b9d31d7cca8f50a6ca8b6711fb8e45cbadabd566cab28f7d58ee058dc650 pillow-12.3.0-cp314-cp314-win_amd64.whl
6f7ed5dc47ade9ad56f6f0499bc5d8eb485bd454d47189133297fa5aa633cf70dad18656cd673318c664f96097c39003d94d9c9703c8337d62840b4564695a04 pyinstaller-6.21.0-py3-none-win_amd64.whl
0a7c1ae7063a2ee46a1c5c057fe73838d7dc5c59800e56037b3fb9dbd1c363915375f989160e44f21436e1071713682bb14473aa8112128beb670f5b110dc8e2 pyinstaller_hooks_contrib-2026.6-py3-none-any.whl
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

+12 -12
View File
@@ -29,21 +29,21 @@ uname -s | grep NT-10 && w10=1 || {
w7=1; uname -s | grep WOW64 && w7x64=1 || w7x32=1
}
fns=(
altgraph-0.17.4-py2.py3-none-any.whl
altgraph-0.17.5-py2.py3-none-any.whl
pefile-2023.2.7-py3-none-any.whl
pywin32_ctypes-0.2.3-py3-none-any.whl
upx-5.0.2-win32.zip
upx-5.2.0-win32.zip
)
[ $w10 ] && fns+=(
jinja2-3.1.6-py3-none-any.whl
MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl
mutagen-1.47.0-py3-none-any.whl
packaging-25.0-py3-none-any.whl
pillow-12.3.0-cp313-cp313-win_amd64.whl
pyinstaller-6.15.0-py3-none-win_amd64.whl
pyinstaller_hooks_contrib-2025.8-py3-none-any.whl
python-3.13.14-amd64.exe
setuptools-80.9.0-py3-none-any.whl
markupsafe-3.0.3-cp314-cp314-win_amd64.whl
mutagen-1.48.1-py3-none-any.whl
packaging-26.3-py3-none-any.whl
pillow-12.3.0-cp314-cp314-win_amd64.whl
pyinstaller-6.21.0-py3-none-win_amd64.whl
pyinstaller_hooks_contrib-2026.6-py3-none-any.whl
python-3.14.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

+2
View File
@@ -19,6 +19,8 @@ v=$1; shift
[ "$v" = sfx ] &&
rls= || rls=1
export PRTY_XD=/tmp
[ $rls ] && {
printf '%s\n' "$v" | grep -qE '^[0-9\.]+$' || exit 1
grep -E "(${v//./, })" ../copyparty/__version__.py || exit 1
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff. Show more