mirror of
https://github.com/9001/copyparty.git
synced 2026-08-15 06:43:20 -06:00
Compare commits
48 commits
v1.20.18
...
hovudstrau
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbcb773307 | ||
|
|
3eb7b46d83 | ||
|
|
4f20738b71 | ||
|
|
ef75220faf | ||
|
|
a3acbfcd5b | ||
|
|
fdf52a8c4e | ||
|
|
b9405a3e25 | ||
|
|
6f924829ee | ||
|
|
24a4090fc7 | ||
|
|
c0c638c917 | ||
|
|
2c6497b644 | ||
|
|
2c4033831f | ||
|
|
2880117265 | ||
|
|
63c61b6729 | ||
|
|
5c157be955 | ||
|
|
9f5f9d8884 | ||
|
|
4003a47723 | ||
|
|
398fcf1d18 | ||
|
|
ac4be555f6 | ||
|
|
75c7502c34 | ||
|
|
9912a95166 | ||
|
|
b331bb1c8c | ||
|
|
c122e103bd | ||
|
|
6226858b89 | ||
|
|
cea97ac6fd | ||
|
|
bc45299b86 | ||
|
|
bb40804f2d | ||
|
|
c70dc7ac56 | ||
|
|
b6abc33fb3 | ||
|
|
537a99df52 | ||
|
|
cdb474c8ec | ||
|
|
0f2040c383 | ||
|
|
e49f6cca17 | ||
|
|
10db4236bf | ||
|
|
d57bb0c701 | ||
|
|
fface524c3 | ||
|
|
bae77b9078 | ||
|
|
b2e4b1c46c | ||
|
|
4543ab31d3 | ||
|
|
856fada3c7 | ||
|
|
aa86235360 | ||
|
|
6a9437b77c | ||
|
|
5952401815 | ||
|
|
ed0be42a3b | ||
|
|
daf144af61 | ||
|
|
a7c99094dd | ||
|
|
daee3e8500 | ||
|
|
96bafb8f4a |
1
.gitattributes
vendored
1
.gitattributes
vendored
|
|
@ -4,4 +4,5 @@
|
|||
|
||||
*.png binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.gz binary
|
||||
|
|
|
|||
39
README.md
39
README.md
|
|
@ -76,6 +76,7 @@ built in Norway 🇳🇴 with contributions from [not-norway](https://github.com
|
|||
* [connecting to webdav from windows](#connecting-to-webdav-from-windows) - using the GUI
|
||||
* [tftp server](#tftp-server) - a TFTP server (read/write) can be started using `--tftp 3969`
|
||||
* [smb server](#smb-server) - unsafe, slow, not recommended for wan
|
||||
* [wopi server](#wopi-server) - edit office documents in the web-ui
|
||||
* [browser ux](#browser-ux) - tweaking the ui
|
||||
* [opengraph](#opengraph) - discord and social-media embeds
|
||||
* [file deduplication](#file-deduplication) - enable symlink-based upload deduplication
|
||||
|
|
@ -136,6 +137,7 @@ built in Norway 🇳🇴 with contributions from [not-norway](https://github.com
|
|||
* [client-side](#client-side) - when uploading files
|
||||
* [security](#security) - there is a [discord server](https://discord.gg/25J8CdTT6G) with announcements
|
||||
* [gotchas](#gotchas) - behavior that might be unexpected
|
||||
* [csp nonce](#csp-nonce) - unauthorized javascript is blocked
|
||||
* [cors](#cors) - cross-site request config
|
||||
* [filekeys](#filekeys) - prevent filename bruteforcing
|
||||
* [dirkeys](#dirkeys) - share specific folders in a volume
|
||||
|
|
@ -773,7 +775,7 @@ press `g` or `田` to toggle grid-view instead of the file listing and `t` togg
|
|||

|
||||
|
||||
it does static images with Pillow / pyvips / FFmpeg, and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how dangerous your users are
|
||||
* pyvips is 3x faster than Pillow, Pillow is 3x faster than FFmpeg
|
||||
* Pillow is 3x faster (and safer) than FFmpeg
|
||||
* disable thumbnails for specific volumes with volflag `dthumb` for all, or `dvthumb` / `dathumb` / `dithumb` for video/audio/images only
|
||||
* for installing FFmpeg on windows, see [optional dependencies](#optional-dependencies)
|
||||
|
||||
|
|
@ -782,6 +784,7 @@ audio files are converted into spectrograms using FFmpeg unless you `--no-athumb
|
|||
images with the following names (see `--th-covers`) become the thumbnail of the folder they're in: `folder.png`, `folder.jpg`, `cover.png`, `cover.jpg`
|
||||
* the order is significant, so if both `cover.png` and `folder.jpg` exist in a folder, it will pick the first matching `--th-covers` entry (`folder.jpg`)
|
||||
* and, if you enable [file indexing](#file-indexing), it will also try those names as dotfiles (`.folder.jpg` and so), and then fallback on the first picture in the folder (if it has any pictures at all)
|
||||
* disable folderthumbs with `--th-covers no`
|
||||
|
||||
enabling `multiselect` lets you click files to select them, and then shift-click another file for range-select
|
||||
* `multiselect` is mostly intended for phones/tablets, but the `sel` option in the `[⚙️] settings` tab is better suited for desktop use, allowing selection by CTRL-clicking and range-selection with SHIFT-click, all without affecting regular clicking
|
||||
|
|
@ -1337,7 +1340,8 @@ for the above example to work, add the commandline argument `-e2ts` to also scan
|
|||
# server config
|
||||
|
||||
using arguments or config files, or a mix of both:
|
||||
* config files (`-c some.conf`) can set additional commandline arguments; see [./docs/example.conf](docs/example.conf) and [./docs/example2.conf](docs/example2.conf)
|
||||
* using a config-file (`-c some.conf`) is best, easier to read/maintain; see [./docs/example.conf](docs/example.conf)
|
||||
* for complex and intricate setups, see how [./docs/example2.conf](docs/example2.conf) includes additional config-files/folders with `% copyparty.d` which loads [./docs/copyparty.d/some.conf](docs/copyparty.d/some.conf)
|
||||
* `kill -s USR1` (same as `systemctl reload copyparty`) to reload accounts and volumes from config files without restarting
|
||||
* or click 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
|
||||
|
|
@ -1629,6 +1633,15 @@ authenticate with one of the following:
|
|||
* username `$password`, password `k`
|
||||
|
||||
|
||||
## wopi server
|
||||
|
||||
edit office documents in the web-ui
|
||||
|
||||
needs a wopi client to work, and the only supported wopi client is a selfhosted collabora online server; eventually onlyoffice will probably also be supported: [issue #1574](https://github.com/9001/copyparty/issues/1574)
|
||||
|
||||
no further documentation or examples yet, [issue #1575](https://github.com/9001/copyparty/issues/1575)
|
||||
|
||||
|
||||
## browser ux
|
||||
|
||||
tweaking the ui
|
||||
|
|
@ -2575,6 +2588,7 @@ buggy feature? rip it out by setting any of the following environment variables
|
|||
| `PRTY_NO_IPV6` | disable some ipv6 support (should not be necessary since windows 2000) |
|
||||
| `PRTY_NO_LZMA` | disable streaming xz compression of incoming uploads |
|
||||
| `PRTY_NO_MP` | disable all use of the python `multiprocessing` module (actual multithreading, cpu-count for parsers/thumbnailers) |
|
||||
| `PRTY_NO_UNIDATA` | disable loading the unicodedata c-extension; saves 400k ram, breaks `--srch-nfkc` |
|
||||
| `PRTY_NO_SQLITE` | disable all database-related functionality (file indexing, metadata indexing, most file deduplication logic) |
|
||||
| `PRTY_NO_TLS` | disable native HTTPS support; if you still want to accept HTTPS connections then TLS must now be terminated by a reverse-proxy |
|
||||
| `PRTY_NO_TPOKE` | disable systemd-tmpfilesd avoider |
|
||||
|
|
@ -2650,8 +2664,6 @@ requires a [flake-enabled](https://nixos.wiki/wiki/Flakes) installation of nix
|
|||
|
||||
some recommended dependencies are enabled by default; [override the package](https://github.com/9001/copyparty/blob/hovudstraum/contrib/package/nix/copyparty/default.nix#L3-L22) if you want to add/remove some features/deps
|
||||
|
||||
`ffmpeg-full` was chosen over `ffmpeg-headless` mainly because we need `withWebp` (and `withOpenmpt` is also nice) and being able to use a cached build felt more important than optimizing for size at the time -- PRs welcome if you disagree 👍
|
||||
|
||||
|
||||
## nixos module
|
||||
|
||||
|
|
@ -3096,6 +3108,23 @@ behavior that might be unexpected
|
|||
* or eliminate the problem entirely by only giving write-access to trustworthy people :^)
|
||||
|
||||
|
||||
## csp nonce
|
||||
|
||||
unauthorized javascript is blocked by means of the default [content-security-policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) which enforces "script nonces", to help prevent XSS and similar
|
||||
|
||||
usually this Just Works and is correctly configured by default; you can stop reading this section and move on, nothing to see here
|
||||
|
||||
but if you are the unlucky 0.1% with a hosting-setup funky enough for this to not work as intended, then you will also be seeing this message: `Looks like javascript is broken; see "csp nonce" in the readme`
|
||||
|
||||
there is two possible reasons why this might happen:
|
||||
|
||||
1. your browser is simply too old to understand the javascript, which is not likely, because just about every browser is supported, even internet explorer 11
|
||||
|
||||
2. something in your hosting-setup is tampering with the responses from copyparty and destroying this security feature; the culprit is probably a misconfigured reverseproxy, a CDN, a WAF, or some other mitm, but a popular offender is **"cloudflare rocket loader"**
|
||||
|
||||
you can try to **temporarily** set global-options `--csp-ui no` and `--csp-dl no` and restart copyparty to check whether it is due to reason #2. Remember to refresh the webbrowser with CTRL-SHIFT-R to check properly. If that makes the problem go away, then turn them back off (**never** keep them enabled), restart copyparty, open the `Network` tab in the webbrowser, and refresh the website; you should see the `content-security-policy` header including a nonce, for example `nonce-BxcJbvdu1z3S/ycPBjVWuA==` which should also appear inside the HTML of that response like this: `<script nonce="BxcJbvdu1z3S/ycPBjVWuA==">` -- if it doesn't then that's the problem; something in your hosting stack is dangerously misconfigured
|
||||
|
||||
|
||||
## cors
|
||||
|
||||
cross-site request config
|
||||
|
|
@ -3239,7 +3268,7 @@ enable sending [zeromq messages](#zeromq) from event-hooks: `pyzmq`
|
|||
|
||||
enable [smb](#smb-server) support (**not** recommended): `impacket==0.13.0`
|
||||
|
||||
`pyvips` gives higher quality thumbnails than `Pillow` and is 320% faster, using 270% more ram
|
||||
`pyvips` adds support for some additional image formats, but can use extreme amounts of RAM
|
||||
* to install `pyvips` on Linux: `sudo apt install libvips42 && python3 -m pip install --user -U pyvips`
|
||||
* to install `pyvips` on windows: `pip install --user -U "pyvips[binary]"`
|
||||
|
||||
|
|
|
|||
|
|
@ -626,7 +626,7 @@ class Gateway(object):
|
|||
"st_atime": ts,
|
||||
"st_mtime": ts,
|
||||
"st_ctime": ts,
|
||||
"st_blocks": int((sz + 511) / 512),
|
||||
"st_blocks": -int(-sz // 512),
|
||||
}
|
||||
|
||||
def stat_file(self, ts, sz):
|
||||
|
|
@ -638,7 +638,7 @@ class Gateway(object):
|
|||
"st_atime": ts,
|
||||
"st_mtime": ts,
|
||||
"st_ctime": ts,
|
||||
"st_blocks": int((sz + 511) / 512),
|
||||
"st_blocks": -int(-sz // 512),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
S_VERSION = "2.22"
|
||||
S_BUILD_DT = "2026-06-13"
|
||||
|
|
@ -398,7 +398,7 @@ class MTHash(object):
|
|||
self.csz = chunksz
|
||||
|
||||
chunks = {}
|
||||
nchunks = int(math.ceil(fsz / chunksz))
|
||||
nchunks = -int(-fsz // chunksz)
|
||||
for nch in range(nchunks):
|
||||
self.work_q.put(nch)
|
||||
|
||||
|
|
@ -725,7 +725,7 @@ def up2k_chunksize(filesize):
|
|||
stepsize = 512 * 1024
|
||||
while True:
|
||||
for mul in [1, 2]:
|
||||
nchunks = math.ceil(filesize * 1.0 / chunksize)
|
||||
nchunks = -int(-filesize // chunksize)
|
||||
if nchunks <= 256 or (chunksize >= 32 * 1024 * 1024 and nchunks <= 4096):
|
||||
return chunksize
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -285,6 +285,7 @@ in
|
|||
|
||||
environment = {
|
||||
PYTHONUNBUFFERED = "true";
|
||||
NIX_SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
|
||||
XDG_CONFIG_HOME = externalStateDir;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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.17"
|
||||
pkgver="1.20.20"
|
||||
pkgrel=1
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||
arch=("any")
|
||||
|
|
@ -16,7 +16,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
|
|||
"python-mutagen: music tags (alternative)"
|
||||
"python-paramiko: sftp server",
|
||||
"python-pillow: thumbnails for images"
|
||||
"python-pyvips: thumbnails for images (higher quality, faster, uses more ram)"
|
||||
"python-pyvips: thumbnails for images (eats ram, prefer pillow)"
|
||||
"libkeyfinder: detection of musical keys"
|
||||
"python-pyopenssl: ftps functionality"
|
||||
"python-pyzmq: send zeromq messages from event-hooks"
|
||||
|
|
@ -24,7 +24,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
|
|||
)
|
||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||
backup=("etc/${pkgname}/copyparty.conf" )
|
||||
sha256sums=("949553aeffea942a0ddabd3e12d28a643fea862d78930bb36e0accc66c4c9475")
|
||||
sha256sums=("a31f9e4a43e56608765e108353140784d50181c37e705e9ce8ed9f3899f57dfa")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
|
||||
pkgname=copyparty
|
||||
pkgver=1.20.17
|
||||
pkgver=1.20.20
|
||||
pkgrel=1
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||
arch=("any")
|
||||
|
|
@ -21,7 +21,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
|
|||
)
|
||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||
backup=("/etc/${pkgname}.d/init" )
|
||||
sha256sums=("949553aeffea942a0ddabd3e12d28a643fea862d78930bb36e0accc66c4c9475")
|
||||
sha256sums=("a31f9e4a43e56608765e108353140784d50181c37e705e9ce8ed9f3899f57dfa")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
# create thumbnails with Pillow; faster than FFmpeg / MediaProcessing
|
||||
withThumbnails ? true,
|
||||
|
||||
# create thumbnails with PyVIPS; even faster, uses more memory
|
||||
# create thumbnails with PyVIPS; uses more memory, pillow is preferred
|
||||
# -- can be combined with Pillow to support more filetypes
|
||||
withFastThumbnails ? false,
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.20.17/copyparty-1.20.17.tar.gz",
|
||||
"version": "1.20.17",
|
||||
"hash": "sha256-lJVTrv/qlCoN2r0+EtKKZD/qhi14kwuzbgrMxmxMlHU="
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.20.20/copyparty-1.20.20.tar.gz",
|
||||
"version": "1.20.20",
|
||||
"hash": "sha256-ox+eSkPlZgh2XhCDUxQHhNUBgcN+cF6c6O2fOJn1ffo="
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ let
|
|||
withMagic = true;
|
||||
};
|
||||
|
||||
call = attrs: final.python3.pkgs.callPackage ./copyparty ({ ffmpeg = final.ffmpeg-full; } // attrs);
|
||||
call = attrs: final.python3.pkgs.callPackage ./copyparty ({ ffmpeg = final.ffmpeg-headless; } // attrs);
|
||||
in
|
||||
{
|
||||
copyparty = call { stable = true; };
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import platform
|
||||
|
|
@ -135,6 +135,7 @@ web/tl/vie.js
|
|||
web/ui.css
|
||||
web/up2k.js
|
||||
web/util.js
|
||||
web/wopi.html
|
||||
"""
|
||||
RES = set(zs.strip().split("\n"))
|
||||
RESM = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
"""copyparty: http file sharing hub (py2/py3)"""
|
||||
__author__ = "ed <copyparty@ocv.me>"
|
||||
|
|
@ -1042,7 +1042,12 @@ def get_sects():
|
|||
.1 = when necessary, append a dot followed by a single digit
|
||||
.1! = counter is always added, even when not necessary
|
||||
-3 = a hyphen followed by three-digit counter
|
||||
no = disable counter; overwrite existing logfile
|
||||
no = disable counter; append to existing file
|
||||
del = delete existing logfile and create new
|
||||
over = overwrite existing logfile
|
||||
|
||||
append is not possible for .xz-compressed logfiles;
|
||||
if logfile is *.xz and rlo is no then rlo will be .1
|
||||
"""
|
||||
),
|
||||
],
|
||||
|
|
@ -1329,7 +1334,7 @@ def add_upload(ap):
|
|||
ap2.add_argument("--nrand", metavar="NUM", type=int, default=9, help="randomized filenames length (volflag=nrand)")
|
||||
ap2.add_argument("--magic", action="store_true", help="enable filetype detection on nameless uploads (volflag=magic)")
|
||||
ap2.add_argument("--df", metavar="GiB", type=u, default="0", help="ensure \033[33mGiB\033[0m free disk space by rejecting upload requests; assumes gigabytes unless a unit suffix is given: [\033[32m256m\033[0m], [\033[32m4\033[0m], [\033[32m2T\033[0m] (volflag=df)")
|
||||
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
|
||||
ap2.add_argument("--sparse", metavar="MiB", type=int, default=1, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
|
||||
ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; [\033[32m-1\033[0m] = forbidden/always-off, [\033[32m0\033[0m] = default-off and warn if enabled, [\033[32m1\033[0m] = default-off, [\033[32m2\033[0m] = on, [\033[32m3\033[0m] = on and disable datecheck")
|
||||
ap2.add_argument("--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")
|
||||
|
|
@ -1427,7 +1432,7 @@ def add_auth(ap):
|
|||
ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app")
|
||||
ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins")
|
||||
ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)")
|
||||
ap2.add_argument("--ses-len", metavar="CHARS", type=int, default=20, help="session key length; default is 120 bits ((20//4)*4*6)")
|
||||
ap2.add_argument("--ses-len", metavar="CHARS", type=int, default=24, help="session key length; default is 144 bits ((24//4)*4*6)")
|
||||
ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies")
|
||||
ap2.add_argument("--grp-all", metavar="NAME", type=u, default="acct", help="the name of the auto-generated group which contains every username which is known")
|
||||
ap2.add_argument("--ipu", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m users with IP matching \033[33mCIDR\033[0m are auto-authenticated as username \033[33mUSR\033[0m; example: [\033[32m172.16.24.0/24=dave]")
|
||||
|
|
@ -1577,6 +1582,18 @@ def add_opds(ap):
|
|||
ap2.add_argument("--opds-exts", metavar="T,T", type=u, default="epub,cbz,pdf", help="file formats to list in OPDS feeds; leave empty to show everything (volflag=opds_exts)")
|
||||
|
||||
|
||||
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-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-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)")
|
||||
ap2.add_argument("--wopi-wdel", action="store_true", help="require permissions read+write+delete for writing to file; default is just read+write")
|
||||
|
||||
|
||||
def add_handlers(ap):
|
||||
ap2 = ap.add_argument_group("handlers (see --help-handlers)")
|
||||
ap2.add_argument("--on404", metavar="PY", type=u, action="append", help="\033[34mREPEATABLE:\033[0m handle 404s by executing \033[33mPY\033[0m file")
|
||||
|
|
@ -1711,6 +1728,8 @@ def add_safety(ap):
|
|||
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("--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)")
|
||||
if FULL_HELP or (not ANYWIN and not UNIX):
|
||||
ap2.add_argument("--th-bwrap", metavar="CMD", type=u, default=th_bwrap, help="optional bwrap sandbox command for FFmpeg and dcraw (Linux-only)")
|
||||
ap2.add_argument("--use-bwrap", metavar="TXT", type=u, default="n", help="a/n/f; [\033[32ma\033[0m]=auto (yes if the program 'bwrap' exists (assumes it works)), [\033[32mn\033[0m]=no (assumes bwrap is broken), [\033[32mf\033[0m]=force (disables FFmpeg if bwrap unavailable)")
|
||||
|
|
@ -1744,7 +1763,7 @@ def add_logging(ap):
|
|||
ap2.add_argument("-q", action="store_true", help="quiet; disable most STDOUT messages")
|
||||
ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile; use .txt for plaintext or .xz for compressed. Example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)")
|
||||
ap2.add_argument("--flo", metavar="N", type=int, default=1, help="log format for \033[33m-lo\033[0m; [\033[32m1\033[0m]=classic/colors, [\033[32m2\033[0m]=no-color")
|
||||
ap2.add_argument("--rlo", metavar="TXT", type=u, default=".1", help="logrotate counter format; see \033[33m--help-rlo\033[0m")
|
||||
ap2.add_argument("--rlo", metavar="TXT", type=u, default="no", help="logrotate counter format; see \033[33m--help-rlo\033[0m")
|
||||
ap2.add_argument("--logrot-sig", metavar="S", type=u, default="", help="immediately logrotate when unix-signal \033[33mS\033[0m is received; examples: [\033[32mSIGHUP\033[0m], [\033[32mHUP\033[0m], [\033[32m1\033[0m]")
|
||||
ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR")
|
||||
ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR")
|
||||
|
|
@ -1795,7 +1814,7 @@ def add_thumbnail(ap):
|
|||
ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)")
|
||||
ap2.add_argument("--th-qv", metavar="N", type=int, default=40, help="webp/jpg thumbnail quality (10~90); higher is larger filesize and better quality (volflag=th_qv)")
|
||||
ap2.add_argument("--th-qvx", metavar="N", type=int, default=64, help="jxl thumbnail quality (10~90); higher is larger filesize and better quality (volflag=th_qvx)")
|
||||
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,raw,pil,ff", help="image decoders, in order of preference")
|
||||
ap2.add_argument("--th-dec", metavar="LIBS", default="raw,pil,ff,vips", help="image decoders, in order of preference")
|
||||
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
|
||||
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
|
||||
ap2.add_argument("--th-no-jxl", action="store_true", help="disable jpeg-xl output")
|
||||
|
|
@ -1807,21 +1826,21 @@ def add_thumbnail(ap):
|
|||
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than \033[33m--th-poke\033[0m seconds will get deleted every \033[33m--th-clean\033[0m seconds")
|
||||
ap2.add_argument("--th-pregen", metavar="F,F", type=u, default="", help="pregenerate thumbnails on startup; \033[33mF,F\033[0m is comma-separated list of formats; example: [\033[32mj,jf,w,w3,wf,wf3,x,xf\033[0m] NOTE: remember to set \033[33m--th-maxage 123456789\033[0m (volflag=th_pregen)")
|
||||
ap2.add_argument("--th-pre-rl", metavar="SEC", type=int, default=30, help="while pregen is running, ratelimit the thumbnailer logger to one message every \033[33mSEC\033[0m seconds (only works with \033[33m-j1\033[0m); set 0 to disable ratelimit")
|
||||
ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern")
|
||||
ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern (volflag=th_covers)")
|
||||
ap2.add_argument("--th-spec-p", metavar="N", type=u, default=1, help="for music, do spectrograms or embedded coverart? [\033[32m0\033[0m]=only-art, [\033[32m1\033[0m]=prefer-art, [\033[32m2\033[0m]=only-spec")
|
||||
ap2.add_argument("--th-spec-fl", action="store_true", help="generate spectrograms with logarithmic frequency scale instead of linear")
|
||||
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
|
||||
# https://github.com/libvips/libvips
|
||||
# https://stackoverflow.com/a/47612661
|
||||
# ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:'
|
||||
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow")
|
||||
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,jxl,kra,ora,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow")
|
||||
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="3fr,arw,avif,cr2,cr3,crw,dcr,dng,erf,exr,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,jp2,jpeg,jpg,jpx,jxl,k25,kdc,mdc,mef,mrw,nef,nii,nrw,orf,pfm,pgm,png,ppm,raf,raw,rw2,sr2,srf,srw,svg,tif,tiff,webp,x3f", help="image formats to decode using pyvips")
|
||||
ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="3fr,arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mdc,mef,mos,mrw,nef,nrw,orf,pef,raf,raw,rw2,sr2,srf,srw,x3f", help="image formats to decode using rawpy (if available) or libraw's dcraw_emu")
|
||||
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,kra,ora,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bcstm,bfstm,brstm,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,m4b,m4r,mdgz,mdxz,mdz,mka,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)")
|
||||
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz, epub=jpg.epub", help="audio/image formats to decompress before passing to ffmpeg")
|
||||
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz, epub=jpg.epub, kra=png.kra, ora=png.ora", help="audio/image formats to decompress before passing to ffmpeg")
|
||||
|
||||
|
||||
def add_transcoding(ap):
|
||||
|
|
@ -1882,7 +1901,8 @@ def add_db_general(ap, hcores):
|
|||
ap2.add_argument("--hash-mt", metavar="CORES", type=int, default=hcores, help="num cpu cores to use for file hashing; set 0 or 1 for single-core hashing")
|
||||
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="rescan filesystem for changes every \033[33mSEC\033[0m seconds; 0=off (volflag=scan)")
|
||||
ap2.add_argument("--db-act", metavar="SEC", type=float, default=10.0, help="defer any scheduled volume reindexing until \033[33mSEC\033[0m seconds after last db write (uploads, renames, ...)")
|
||||
ap2.add_argument("--srch-icase", action="store_true", help="case-insensitive search for all unicode characters (the default is icase for just ascii). NOTE: will make searches much slower (around 4x), and NOTE: only applies to filenames/paths, not tags")
|
||||
ap2.add_argument("--srch-icase", action="store_true", help="case-insensitive search for all unicode characters (the default is icase for just ascii). NOTE: will make searches much slower (around 3x), and NOTE: only applies to filenames/paths, not tags")
|
||||
ap2.add_argument("--srch-nfkc", action="store_true", help="case-insensitive and unicode-normalization-insensitive search (NFC/NFD) for filenames/paths; slightly slower than \033[33m--srch-icase\033[0m (about 15%% slower / 87%% as fast)")
|
||||
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=45, help="search deadline -- terminate searches running for more than \033[33mSEC\033[0m seconds")
|
||||
ap2.add_argument("--srch-hits", metavar="N", type=int, default=7999, help="max search results to allow clients to fetch; 125 results will be shown initially")
|
||||
ap2.add_argument("--srch-excl", metavar="PTN", type=u, default="", help="regex: exclude files from search results if the file-URL matches \033[33mPTN\033[0m (case-sensitive). Example: [\033[32mpassword|logs/[0-9]\033[0m] any URL containing 'password' or 'logs/DIGIT' (volflag=srch_excl)")
|
||||
|
|
@ -2092,6 +2112,7 @@ def run_argparse(
|
|||
add_tftp(ap)
|
||||
add_smb(ap)
|
||||
add_opds(ap)
|
||||
add_wopi(ap)
|
||||
add_safety(ap)
|
||||
add_salt(ap, fk_salt, dk_salt, ah_salt)
|
||||
add_optouts(ap)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# coding: utf-8
|
||||
|
||||
VERSION = (1, 20, 18)
|
||||
VERSION = (1, 20, 20)
|
||||
CODENAME = "sftp is fine too"
|
||||
BUILD_DT = (2026, 7, 9)
|
||||
BUILD_DT = (2026, 8, 4)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
|
|
@ -2612,6 +2612,13 @@ class AuthSrv(object):
|
|||
t = "WARNING: volume [/%s]: invalid value specified for ext-th: %s"
|
||||
self.log(t % (vol.vpath, etv), 3)
|
||||
|
||||
zsl = [x.strip() for x in vol.flags["th_covers"].split(",")]
|
||||
zsl = [x for x in zsl if x not in ("", "no")]
|
||||
vol.flags["th_coversl"] = zsl
|
||||
vol.flags["th_coversd"] = zsl + ["." + x for x in zsl]
|
||||
vol.flags["th_covers_set"] = set(vol.flags["th_coversl"])
|
||||
vol.flags["th_coversd_set"] = set(vol.flags["th_coversd"])
|
||||
|
||||
zsl = [x.strip() for x in vol.flags["rw_edit"].split(",")]
|
||||
zsl = [x for x in zsl if x]
|
||||
vol.flags["rw_edit"] = ",".join(zsl)
|
||||
|
|
@ -2653,10 +2660,11 @@ class AuthSrv(object):
|
|||
if head_s and not head_s.endswith("\n"):
|
||||
head_s += "\n"
|
||||
|
||||
zs2 = "Content-Security-Policy: %s\r\n"
|
||||
zs = vol.flags.get("csp_ui", "")
|
||||
csp_ui = "Content-Security-Policy: %s\r\n" % (zs,) if zs else ""
|
||||
csp_ui = zs2 % (zs,) if zs.lower() not in ("", "no") else ""
|
||||
zs = vol.flags.get("csp_dl", "")
|
||||
csp_dl = "Content-Security-Policy: %s\r\n" % (zs,) if zs else ""
|
||||
csp_dl = zs2 % (zs,) if zs.lower() not in ("", "no") else ""
|
||||
|
||||
zs = "X-Content-Type-Options: nosniff\r\n"
|
||||
if "norobots" in vol.flags:
|
||||
|
|
@ -2754,7 +2762,8 @@ class AuthSrv(object):
|
|||
|
||||
for k in drop:
|
||||
t = 'cannot enable [%s] for volume "/%s" because this requires one of the following: e2d / e2ds / e2dsa (either as volflag or global-option)'
|
||||
self.log(t % (k, vol.vpath), 1)
|
||||
if not (enshare and vp.startswith(shrs)):
|
||||
self.log(t % (k, vol.vpath), 1)
|
||||
vol.flags.pop(k)
|
||||
|
||||
zi = vol.flags.get("lifetime") or 0
|
||||
|
|
@ -2863,12 +2872,13 @@ class AuthSrv(object):
|
|||
continue
|
||||
try:
|
||||
bos.makedirs(vol.realpath, vf=vol.flags)
|
||||
files = os.listdir(vol.realpath)
|
||||
for fn in files:
|
||||
for fn, _ in statdir(
|
||||
self.log_func, not self.args.no_scandir, False, vol.realpath, True
|
||||
):
|
||||
fn2 = fn.lower()
|
||||
if fn == fn2:
|
||||
fn2 = fn.upper()
|
||||
if fn == fn2 or fn2 in files:
|
||||
if fn == fn2:
|
||||
continue
|
||||
is_ci = os.path.exists(os.path.join(vol.realpath, fn2))
|
||||
ccs = "y" if is_ci else "n"
|
||||
|
|
@ -3037,6 +3047,10 @@ class AuthSrv(object):
|
|||
t = "hint: enable upload deduplication with --dedup (but see readme for consequences)"
|
||||
self.log(t, 6)
|
||||
|
||||
if MACOS and not self.args.srch_nfkc:
|
||||
t = "hint: enable --srch-nfkc to improve unicode filename/path search on MacOS"
|
||||
self.log(t, 6)
|
||||
|
||||
zv, _ = vfs.get("/", "*", False, False)
|
||||
zs = zv.realpath.lower()
|
||||
if zs in ("/", "c:\\") or zs.startswith(r"c:\windows"):
|
||||
|
|
@ -3047,6 +3061,8 @@ class AuthSrv(object):
|
|||
zv, _ = vfs.get("", "*", False, True, err=999)
|
||||
if self.warn_anonwrite and verbosity > 4 and os.getcwd() == zv.realpath:
|
||||
t = "anyone can write to the current directory: {}\n"
|
||||
if ANYWIN:
|
||||
t += "/!\\ NOTE: because you are using Windows, this is kinda dangerous (DLL-hijacking); you should configure accounts and volumes if this is accessible from an untrusted network\n"
|
||||
self.log(t.format(zv.realpath), c=1)
|
||||
|
||||
self.warn_anonwrite = False
|
||||
|
|
@ -3240,6 +3256,7 @@ class AuthSrv(object):
|
|||
|
||||
self.js_ls = {}
|
||||
self.js_htm = ""
|
||||
self.md_htm = ""
|
||||
for vp, vn in self.vfs.all_nodes.items():
|
||||
if enshare and vp.startswith(shrs):
|
||||
continue # propagates later in this func
|
||||
|
|
@ -3331,6 +3348,8 @@ class AuthSrv(object):
|
|||
zs2 = getattr(self.args, zs, "")
|
||||
if zs2:
|
||||
js_htm[zs] = zs2
|
||||
if self.args.wopi:
|
||||
js_htm["have_wopi"] = True
|
||||
|
||||
zs = "have_emp md_no_br"
|
||||
md_htm = {x: js_htm[x] for x in zs.split(" ")}
|
||||
|
|
@ -3356,6 +3375,7 @@ class AuthSrv(object):
|
|||
dbv = vol.get_dbv("")[0]
|
||||
vol.js_ls = vol.js_ls or dbv.js_ls or {}
|
||||
vol.js_htm = vol.js_htm or dbv.js_htm or "{}"
|
||||
vol.md_htm = vol.md_htm or dbv.md_htm or "{}"
|
||||
|
||||
zs = str(vol.flags.get("tcolor") or self.args.tcolor)
|
||||
vol.flags["tcolor"] = zs.lstrip("#")
|
||||
|
|
@ -3783,12 +3803,12 @@ class AuthSrv(object):
|
|||
"",
|
||||
]
|
||||
|
||||
csv = set("i p th_covers zm_on zm_off zs_on zs_off".split())
|
||||
csv = set("i p zm_on zm_off zs_on zs_off".split())
|
||||
zs = "c ihead ohead mtm mtp on403 on404 xac xad xar xau xiu xban xbc xbd xbr xbu xm"
|
||||
lst = set(zs.split())
|
||||
askip = set("a v c vc cgen exp_lg exp_md theme".split())
|
||||
|
||||
t = "exp_lg exp_md ext_th_d mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot"
|
||||
t = "emb_all emb_lgs emb_mds exp_lg exp_md ext_th_d ls_q_m 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"
|
||||
fskip = set(t.split())
|
||||
|
||||
# keymap from argv to vflag
|
||||
|
|
@ -3915,6 +3935,9 @@ class AuthSrv(object):
|
|||
except:
|
||||
pass
|
||||
|
||||
if k in ("chmod_d", "chmod_f"):
|
||||
v = "%o" % (v,)
|
||||
|
||||
try:
|
||||
ak = vmap[k]
|
||||
v2 = getattr(self.args, ak)
|
||||
|
|
@ -3924,7 +3947,7 @@ class AuthSrv(object):
|
|||
except:
|
||||
pass
|
||||
|
||||
if v2 is v:
|
||||
if v2 == v:
|
||||
continue
|
||||
except:
|
||||
pass
|
||||
|
|
@ -3968,7 +3991,7 @@ class AuthSrv(object):
|
|||
|
||||
def derive_args(args: argparse.Namespace) -> None:
|
||||
args.have_idp_hdrs = bool(args.idp_h_usr or args.idp_hm_usr)
|
||||
args.have_ipu_or_ipr = bool(args.ipu or args.ipr)
|
||||
args.have_ipu_or_ipr = bool(args.ipu or args.ipr or args.wopi)
|
||||
|
||||
|
||||
def n_du_who(s: str) -> int:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import time
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import threading
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
# awk -F\" '/add_argument\("-[^-]/{print(substr($2,2))}' copyparty/__main__.py | sort | tr '\n' ' '
|
||||
zs = "a c e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vp e2vu ed emp i j lo mcr mte mth mtm mtp nb nc nid nih nth nw p q s ss sss v z zv"
|
||||
|
|
@ -157,6 +157,7 @@ def vf_vmap() -> dict[str, str]:
|
|||
"tail_tmax",
|
||||
"tail_who",
|
||||
"tcolor",
|
||||
"th_covers",
|
||||
"th_pregen",
|
||||
"th_qv",
|
||||
"th_qvx",
|
||||
|
|
@ -321,6 +322,7 @@ flagcats = {
|
|||
"th3x": "3x resolution (y/n/fy/fn)",
|
||||
"th_qv=40": "webp/jpg thumbnail quality (10~90)",
|
||||
"th_qvx=40": "jxl thumbnail quality (10~90)",
|
||||
"th_covers=a.jpg,a.png": "filenames to prefer as folder-thumbs",
|
||||
"convt": "convert-to-image timeout in seconds",
|
||||
"aconvt": "convert-to-audio timeout in seconds",
|
||||
"th_spec_p=1": "make spectrograms? 0=never 1=fallback 2=always",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import json
|
||||
|
|
@ -248,7 +248,7 @@ def ramdisk_chk(asrv: AuthSrv) -> None:
|
|||
t2 = ["\n volume=[/%s], abspath=%r, type=%s, root=%r" % x for x in mods]
|
||||
log("vfs", t + "".join(t2) + "\n", 1)
|
||||
|
||||
assume = "mac" if MACOS else "lin"
|
||||
assume = "mac" if MACOS else "win" if ANYWIN else "lin"
|
||||
for vol in asrv.vfs.all_nodes.values():
|
||||
if not vol.realpath or vol.flags.get("is_file"):
|
||||
continue
|
||||
|
|
@ -263,3 +263,4 @@ def ramdisk_chk(asrv: AuthSrv) -> None:
|
|||
htm = json.loads(vol.js_htm)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import errno
|
||||
|
|
@ -149,6 +149,7 @@ class FtpFs(AbstractedFS):
|
|||
self.hub: "SvcHub" = cmd_channel.hub
|
||||
self.args = cmd_channel.args
|
||||
self.uname = cmd_channel.uname
|
||||
self.log_src = "%s:FTP" % (cmd_channel.cli_ip,)
|
||||
|
||||
self.cwd = "/" # pyftpdlib convention of leading slash
|
||||
self.root = "/var/lib/empty"
|
||||
|
|
@ -156,8 +157,8 @@ class FtpFs(AbstractedFS):
|
|||
self.listdirinfo = self.listdir
|
||||
self.chdir(".")
|
||||
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.hub.log("ftpd", msg, c)
|
||||
def clog(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.hub.log(self.log_src, msg, c)
|
||||
|
||||
def v2a(
|
||||
self,
|
||||
|
|
@ -238,6 +239,8 @@ class FtpFs(AbstractedFS):
|
|||
return True
|
||||
|
||||
def open(self, filename: str, mode: str) -> typing.IO[Any]:
|
||||
self.clog("open(%r,%r) @%s" % (filename, mode, self.uname))
|
||||
|
||||
r = "r" in mode
|
||||
w = "w" in mode or "a" in mode or "+" in mode
|
||||
|
||||
|
|
@ -255,7 +258,7 @@ class FtpFs(AbstractedFS):
|
|||
xbu = vfs.flags.get("xbu")
|
||||
if xbu:
|
||||
hr = runhook(
|
||||
self.log,
|
||||
self.clog,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.ftp",
|
||||
|
|
@ -275,7 +278,7 @@ class FtpFs(AbstractedFS):
|
|||
if t or hr.get("rc") != 0:
|
||||
if not t:
|
||||
t = "upload blocked by xbu server config: %r" % (filename,)
|
||||
self.log(t, 3)
|
||||
self.clog(t, 3)
|
||||
raise FSE(t)
|
||||
|
||||
if w and need_unlink: # type: ignore # !rm
|
||||
|
|
@ -300,12 +303,13 @@ class FtpFs(AbstractedFS):
|
|||
|
||||
# Don't unlink file for append mode
|
||||
elif "a" not in mode:
|
||||
wunlink(self.log, ap, VF_CAREFUL)
|
||||
wunlink(self.clog, ap, VF_CAREFUL)
|
||||
|
||||
ret = open(fsenc(ap), mode, self.args.iobuf)
|
||||
if w and "fperms" in vfs.flags:
|
||||
set_fperms(ret, vfs.flags)
|
||||
|
||||
self.clog(" `-- %r" % (ap,))
|
||||
return ret
|
||||
|
||||
def chdir(self, path: str) -> None:
|
||||
|
|
@ -331,10 +335,12 @@ class FtpFs(AbstractedFS):
|
|||
self.cwd = nwd
|
||||
|
||||
def mkdir(self, path: str) -> None:
|
||||
self.clog("mkdir(%r) @%s" % (path, self.uname))
|
||||
ap, vfs, _ = self.rv2a(path, w=True)
|
||||
bos.makedirs(ap, vf=vfs.flags) # filezilla expects this
|
||||
|
||||
def listdir(self, path: str) -> list[str]:
|
||||
self.clog("ls(%r) @%s" % (path, self.uname))
|
||||
vpath = join(self.cwd, path)
|
||||
try:
|
||||
ap, vfs, rem = self.v2a(vpath, True, False)
|
||||
|
|
@ -384,6 +390,7 @@ class FtpFs(AbstractedFS):
|
|||
return ret
|
||||
|
||||
def rmdir(self, path: str) -> None:
|
||||
self.clog("rmdir(%r) @%s" % (path, self.uname))
|
||||
ap = self.rv2a(path, d=True)[0]
|
||||
try:
|
||||
bos.rmdir(ap)
|
||||
|
|
@ -392,6 +399,7 @@ class FtpFs(AbstractedFS):
|
|||
raise
|
||||
|
||||
def remove(self, path: str) -> None:
|
||||
self.clog("rm(%r) @%s" % (path, self.uname))
|
||||
if self.args.no_del:
|
||||
raise FSE("The delete feature is disabled in server config")
|
||||
|
||||
|
|
@ -402,6 +410,7 @@ class FtpFs(AbstractedFS):
|
|||
raise FSE(str(ex))
|
||||
|
||||
def rename(self, src: str, dst: str) -> None:
|
||||
self.clog("mv(%r,%r) @%s" % (src, dst, self.uname))
|
||||
if self.args.no_mv:
|
||||
raise FSE("The rename/move feature is disabled in server config")
|
||||
|
||||
|
|
@ -431,6 +440,7 @@ class FtpFs(AbstractedFS):
|
|||
return st
|
||||
|
||||
def utime(self, path: str, timeval: float) -> None:
|
||||
self.clog("utime(%r,%r) @%s" % (path, timeval, self.uname))
|
||||
ap = self.rv2a(path, w=True)[0]
|
||||
bos.utime_c(logging.warning, ap, int(timeval), False)
|
||||
|
||||
|
|
@ -438,6 +448,9 @@ class FtpFs(AbstractedFS):
|
|||
ap = self.rv2a(path)[0]
|
||||
return bos.stat(ap)
|
||||
|
||||
def readlink(self, *a) -> None:
|
||||
raise OSError(errno.EINVAL, "nope")
|
||||
|
||||
def isfile(self, path: str) -> bool:
|
||||
try:
|
||||
st = self.stat(path)
|
||||
|
|
@ -480,9 +493,13 @@ class FtpFs(AbstractedFS):
|
|||
def get_user_by_uid(self, uid: int) -> str:
|
||||
return "root"
|
||||
|
||||
def get_group_by_uid(self, gid: int) -> str:
|
||||
def get_group_by_gid(self, gid: int) -> str:
|
||||
return "root"
|
||||
|
||||
def mkstemp(self, *a, **ka):
|
||||
self.clog("mkstemp%r @%s" % (a, self.uname))
|
||||
raise FSE("nope")
|
||||
|
||||
|
||||
class FtpHandler(FTPHandler):
|
||||
abstracted_fs = FtpFs
|
||||
|
|
@ -511,6 +528,7 @@ class FtpHandler(FTPHandler):
|
|||
return
|
||||
|
||||
self.cli_ip = cip
|
||||
self.log_src = "%s:FTP" % (cip,)
|
||||
|
||||
# abspath->vpath mapping to resolve log_transfer paths
|
||||
self.vfs_map: dict[str, str] = {}
|
||||
|
|
@ -518,6 +536,15 @@ class FtpHandler(FTPHandler):
|
|||
# reduce non-debug logging
|
||||
self.log_cmds_list = [x for x in self.log_cmds_list if x not in ("CWD", "XCWD")]
|
||||
|
||||
def clog(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.hub.log(self.log_src, msg, c)
|
||||
|
||||
def ftp_NOOP(self, *a) -> None:
|
||||
self.respond("200 pong", logging.debug)
|
||||
|
||||
def ftp_STOU(self, *a) -> None:
|
||||
self.respond("550 nope", logging.warning)
|
||||
|
||||
def ftp_STOR(self, file: str, mode: str = "w") -> Any:
|
||||
# Optional[str]
|
||||
vp = join(self.fs.cwd, file).lstrip("/")
|
||||
|
|
@ -530,7 +557,7 @@ class FtpHandler(FTPHandler):
|
|||
xbu = vfs.flags.get("xbu")
|
||||
if xbu:
|
||||
hr = runhook(
|
||||
None,
|
||||
self.clog,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.ftpd",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse # typechk
|
||||
import copy
|
||||
|
|
@ -30,7 +30,7 @@ try:
|
|||
except:
|
||||
pass
|
||||
|
||||
from .__init__ import ANYWIN, RES, RESM, TYPE_CHECKING, EnvParams, unicode
|
||||
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
|
||||
|
|
@ -149,6 +149,11 @@ if True: # pylint: disable=using-constant-test
|
|||
if TYPE_CHECKING:
|
||||
from .httpconn import HttpConn
|
||||
|
||||
if PY2:
|
||||
from urllib2 import urlopen
|
||||
else:
|
||||
from urllib.request import urlopen
|
||||
|
||||
if not hasattr(socket, "AF_UNIX"):
|
||||
setattr(socket, "AF_UNIX", -9001)
|
||||
|
||||
|
|
@ -171,6 +176,27 @@ RSS_SORT = {"m": "mt", "u": "at", "n": "fn", "s": "sz"}
|
|||
ACODE2_FMT = set(["opus", "owa", "caf", "mp3", "flac", "wav"])
|
||||
IDX_HTML = set(["index.htm", "index.html"])
|
||||
|
||||
# coolwsd-26.04.2.3 discovery editnew; browser.js wopi_set is superset of this
|
||||
WOPI_NEW = (
|
||||
".odt",
|
||||
".fodt",
|
||||
".ott",
|
||||
".docx",
|
||||
".dotx",
|
||||
".rtf",
|
||||
".odm",
|
||||
".ods",
|
||||
".fods",
|
||||
".ots",
|
||||
".xlsx",
|
||||
".odp",
|
||||
".fodp",
|
||||
".otp",
|
||||
".pptx",
|
||||
".fodg",
|
||||
".otg",
|
||||
)
|
||||
|
||||
A_FILE = os.stat_result(
|
||||
(0o644, -1, -1, 1, 1000, 1000, 8, 0x39230101, 0x39230101, 0x39230101)
|
||||
)
|
||||
|
|
@ -813,6 +839,22 @@ class HttpCli(object):
|
|||
if not ipr[self.uname].map(self.ip):
|
||||
self.log("username [%s] rejected by --ipr" % (self.uname,), 3)
|
||||
self.uname = "*"
|
||||
if self.args.wopi and "access_token" in self.uparam:
|
||||
wopi_a = self.uparam["access_token"]
|
||||
try:
|
||||
wopi_f = self.conn.hsrv.wopi_files[wopi_a]
|
||||
if wopi_f["expires"] < time.time():
|
||||
raise Exception("expired")
|
||||
uname = wopi_f["uname"]
|
||||
self.asrv.vfs.get(
|
||||
wopi_f["vp"], uname, True, True, False, self.args.wopi_wdel
|
||||
)
|
||||
self.uname = uname
|
||||
except Exception as ex:
|
||||
self.conn.hsrv.wopi_files.pop(wopi_a, None)
|
||||
self.cbonk(self.conn.hsrv.gpwd, wopi_a, "wopi", "bad wopi tokens")
|
||||
self.loud_reply("bad wopi token %s (%s)" % (wopi_a, ex), status=400)
|
||||
return False
|
||||
|
||||
self.rvol = self.asrv.vfs.aread[self.uname]
|
||||
self.wvol = self.asrv.vfs.awrite[self.uname]
|
||||
|
|
@ -1532,8 +1574,141 @@ class HttpCli(object):
|
|||
if "rss" in self.uparam:
|
||||
return self.tx_rss()
|
||||
|
||||
if self.args.wopi:
|
||||
if "wopi" in self.uparam:
|
||||
return self.tx_wopi()
|
||||
|
||||
if self.vpath.startswith("wopi"):
|
||||
return self.tx_wopi_api()
|
||||
|
||||
return self.tx_browser()
|
||||
|
||||
def tx_wopi_api(self) -> bool:
|
||||
atoken = self.uparam["access_token"]
|
||||
session = self.conn.hsrv.wopi_files[atoken]
|
||||
if self.do_log:
|
||||
self.log(" `-- wopi: %r" % (session["vp"],))
|
||||
|
||||
zs = "wopi/files/%s" % (session["file_id"],)
|
||||
if not self.vpath.startswith(zs):
|
||||
return self.tx_404()
|
||||
query = self.vpath[len(zs) :]
|
||||
|
||||
vfs, rem = self.asrv.vfs.get(session["vp"], self.uname, True, True)
|
||||
vpath = vjoin(vfs.vpath, rem)
|
||||
ap = vfs.canonical(rem)
|
||||
if query.startswith("/contents"):
|
||||
return self.tx_file("oh_f", ap)
|
||||
else:
|
||||
st = bos.stat(ap)
|
||||
file_info = {
|
||||
"BaseFileName": vpath.split("/")[-1],
|
||||
"Size": st.st_size,
|
||||
"OwnerId": self.uname,
|
||||
"UserId": self.uname,
|
||||
"UserFriendlyName": self.uname,
|
||||
"UserCanWrite": True,
|
||||
"UserCanNotWriteRelative": True,
|
||||
"LastModifiedTime": time.strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ", time.gmtime(st.st_mtime)
|
||||
),
|
||||
}
|
||||
ret = json.dumps(file_info).encode("utf-8", "replace")
|
||||
self.reply(ret, 200, "application/json; charset=utf-8")
|
||||
return True
|
||||
|
||||
return self.tx_404()
|
||||
|
||||
def tx_wopi(self) -> bool:
|
||||
vpath = vjoin(self.vpath, self.uparam["wopi"])
|
||||
vfs, rem = self.asrv.vfs.get(
|
||||
vpath, self.uname, True, True, False, self.args.wopi_wdel
|
||||
)
|
||||
if not bos.path.isfile(vfs.canonical(rem)):
|
||||
return self.tx_404()
|
||||
|
||||
wopi_files = self.conn.hsrv.wopi_files
|
||||
found = None
|
||||
rm = []
|
||||
with self.conn.hsrv.mutex:
|
||||
now = time.time()
|
||||
for atoken, session in wopi_files.items():
|
||||
if session["expires"] < now:
|
||||
rm.append(atoken)
|
||||
continue
|
||||
if session["vp"] != vpath or session["uname"] != self.uname:
|
||||
continue
|
||||
if session["expires"] - now < self.args.wopi_ttl * 0.9:
|
||||
rm.append(atoken)
|
||||
continue
|
||||
found = session
|
||||
break
|
||||
for zs in rm:
|
||||
del wopi_files[zs]
|
||||
if len(wopi_files) > 9000: # about 6 MiB
|
||||
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
|
||||
wopi_files[atoken] = session = {
|
||||
"vp": vpath,
|
||||
"uname": self.uname,
|
||||
"file_id": file_id,
|
||||
"expires": time.time() + self.args.wopi_ttl,
|
||||
}
|
||||
|
||||
xml = "?"
|
||||
try:
|
||||
from .dxml import parse_xml
|
||||
|
||||
uo_kw = {}
|
||||
if self.args.wopi_crt:
|
||||
import ssl
|
||||
|
||||
if self.args.wopi_crt == "no":
|
||||
ctx = ssl._create_unverified_context()
|
||||
else:
|
||||
ctx = ssl.create_default_context(cafile=self.args.wopi_crt)
|
||||
ctx.check_hostname = not self.args.wopi_crt_icn
|
||||
|
||||
uo_kw["context"] = ctx
|
||||
|
||||
wopi_urls = dict(x.lower().split("=", 1) for x in self.args.wopi_urls or [])
|
||||
url = wopi_urls.get(self.host.lower(), self.args.wopi_url).rstrip("/")
|
||||
url += "/hosting/discovery"
|
||||
buf = urlopen(url, **uo_kw).read()
|
||||
xml = buf.decode("ascii", "replace").lower()
|
||||
enc = self.get_xml_enc(xml)
|
||||
xml = buf.decode(enc, "replace")
|
||||
xroot = parse_xml(xml)
|
||||
ext = vpath.split(".")[-1]
|
||||
url = xroot.find(
|
||||
".//action[@ext='%s'][@name='edit'][@urlsrc]" % (ext,)
|
||||
).get("urlsrc")
|
||||
if not url.endswith(("?", "&")):
|
||||
url += "&" if "?" in url else "?"
|
||||
url += "WOPISrc="
|
||||
if self.args.wopi_api:
|
||||
zs = self.args.wopi_api.rstrip("/")
|
||||
else:
|
||||
zs = ("https://" if self.is_https else "http://") + self.host
|
||||
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)
|
||||
raise Pebkac(500, "wopi error (see fileserver log)")
|
||||
|
||||
html = self.j2s(
|
||||
"wopi",
|
||||
title=self.uparam["wopi"],
|
||||
url=url,
|
||||
atoken=atoken,
|
||||
ttl=session["expires"],
|
||||
).encode("utf-8", "replace")
|
||||
|
||||
self.reply(html, 200, "text/html; charset=utf-8")
|
||||
return True
|
||||
|
||||
def tx_rss(self) -> bool:
|
||||
if self.do_log:
|
||||
self.log("RSS %s @%s" % (self.req, self.uname))
|
||||
|
|
@ -2387,6 +2562,9 @@ class HttpCli(object):
|
|||
|
||||
raise Pebkac(405, "POST(%r) is disabled in server config" % (ctype,))
|
||||
|
||||
if self.args.wopi and self.vpath.startswith("wopi"):
|
||||
return self.handle_post_binary()
|
||||
|
||||
raise Pebkac(405, "don't know how to handle POST(%r)" % (ctype,))
|
||||
|
||||
def handle_smsg(self) -> bool:
|
||||
|
|
@ -3156,6 +3334,9 @@ class HttpCli(object):
|
|||
except:
|
||||
raise Pebkac(400, "you must supply a content-length for binary POST")
|
||||
|
||||
if self.args.wopi and self.vpath.startswith("wopi"):
|
||||
return self.rx_wopi(postsize)
|
||||
|
||||
try:
|
||||
chashes = self.headers["x-up2k-hash"].split(",")
|
||||
wark = self.headers["x-up2k-wark"]
|
||||
|
|
@ -3367,6 +3548,47 @@ class HttpCli(object):
|
|||
self.reply(b"thank")
|
||||
return True
|
||||
|
||||
def rx_wopi(self, postsize: int) -> bool:
|
||||
atoken = self.uparam["access_token"]
|
||||
session = self.conn.hsrv.wopi_files[atoken]
|
||||
if self.do_log:
|
||||
self.log(" `-- wopi: %r" % (session["vp"],))
|
||||
|
||||
zs = "wopi/files/%s/contents" % (session["file_id"],)
|
||||
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)
|
||||
ap = vfs.canonical(rem)
|
||||
st = bos.stat(ap)
|
||||
|
||||
if "x-cool-wopi-timestamp" in self.headers:
|
||||
zs = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(st.st_mtime))
|
||||
if self.headers["x-cool-wopi-timestamp"] != zs:
|
||||
self.reply(json.dumps({"COOLStatusCode": 1010}).encode("utf-8"), 409)
|
||||
return True
|
||||
|
||||
buf = b""
|
||||
for rbuf in self.get_body_reader()[0]:
|
||||
buf += rbuf
|
||||
if not rbuf:
|
||||
break
|
||||
|
||||
if len(buf) != postsize:
|
||||
t = "wopi post with incorrect length; expected %d, got %d"
|
||||
raise Pebkac(400, t % (postsize, len(buf)))
|
||||
|
||||
with open(ap, "wb") as file:
|
||||
file.write(buf)
|
||||
|
||||
st = bos.stat(ap)
|
||||
zs = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(st.st_mtime))
|
||||
ret = json.dumps({"LastModifiedTime": zs}).encode("utf-8", "replace")
|
||||
self.reply(ret, 200, "application/json; charset=utf-8")
|
||||
return True
|
||||
|
||||
def handle_chpw(self) -> bool:
|
||||
assert self.parser # !rm
|
||||
if self.args.usernames:
|
||||
|
|
@ -3521,7 +3743,7 @@ class HttpCli(object):
|
|||
def _mkdir(self, vpath: str, dav: bool = False) -> bool:
|
||||
nullwrite = self.args.nw
|
||||
self.gctx = vpath
|
||||
vpath = undot(vpath)
|
||||
vpath = sanitize_vpath(undot(vpath))
|
||||
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")
|
||||
|
|
@ -3627,6 +3849,9 @@ class HttpCli(object):
|
|||
True,
|
||||
)
|
||||
|
||||
if self.args.wopi and sanitized.endswith(WOPI_NEW):
|
||||
return self.redirect(vjoin(vfs.vpath, rem), "?wopi=" + quotep(sanitized))
|
||||
|
||||
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
|
||||
self.redirect(vpath, "?edit")
|
||||
return True
|
||||
|
|
@ -6856,7 +7081,8 @@ class HttpCli(object):
|
|||
nothumb = "dthumb" in dbv.flags
|
||||
if is_dir:
|
||||
vrem = vrem.rstrip("/")
|
||||
if nothumb:
|
||||
cvs = dbv.flags["th_coversl"]
|
||||
if nothumb or not cvs:
|
||||
pass
|
||||
elif icur and vrem:
|
||||
q = "select fn from cv where rd=? and dn=?"
|
||||
|
|
@ -6873,7 +7099,7 @@ class HttpCli(object):
|
|||
except:
|
||||
pass
|
||||
else:
|
||||
for fn in self.args.th_covers:
|
||||
for fn in cvs:
|
||||
fp = os.path.join(abspath, fn)
|
||||
try:
|
||||
st = bos.stat(fp)
|
||||
|
|
@ -7219,20 +7445,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
|
||||
|
|
@ -7402,6 +7626,26 @@ class HttpCli(object):
|
|||
(fe["sz"], fe["tags"][".files"]) = hit
|
||||
except:
|
||||
pass # 404 or mojibake
|
||||
if vfs_virt:
|
||||
q = "select sz, nf from ds where rd='' limit 1"
|
||||
try:
|
||||
for fe in [x for x in dirs if x["name"] in vfs_virt]:
|
||||
if ".files" not in fe["tags"]:
|
||||
fe["tags"][".files"] = 0
|
||||
vols = [vn.nodes[fe["name"]]]
|
||||
while vols:
|
||||
vn2 = vols.pop()
|
||||
if self.uname not in vn2.axs.uread:
|
||||
continue
|
||||
vols += list(vn2.nodes.values())
|
||||
if vn2.dbv not in (vn2, None):
|
||||
continue
|
||||
hit = idx.get_cur(vn2).execute(q).fetchone()
|
||||
if hit:
|
||||
fe["sz"] += hit[0]
|
||||
fe["tags"][".files"] += hit[1]
|
||||
except:
|
||||
pass
|
||||
|
||||
taglist = [k for k in lmte if k in tagset]
|
||||
else:
|
||||
|
|
@ -7636,7 +7880,7 @@ class HttpCli(object):
|
|||
self.conn.hsrv.j2[tpl] = j2env.get_template(tname)
|
||||
thumb = ""
|
||||
is_pic = is_vid = is_au = False
|
||||
for fn in self.args.th_coversd:
|
||||
for fn in vn.flags["th_coversd"]:
|
||||
if fn in lnames:
|
||||
thumb = lnames[fn]
|
||||
break
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse # typechk
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import hashlib
|
||||
import math
|
||||
|
|
@ -180,6 +180,8 @@ class HttpSrv(object):
|
|||
self.u2idx_free: dict[str, U2idx] = {}
|
||||
self.u2idx_n = 0
|
||||
|
||||
self.wopi_files: dict[str, dict[str, str]] = {}
|
||||
|
||||
assert jinja2 # type: ignore # !rm
|
||||
env = jinja2.Environment()
|
||||
env.loader = jinja2.FunctionLoader(lambda f: load_jinja2_resource(self.E, f))
|
||||
|
|
@ -195,6 +197,7 @@ class HttpSrv(object):
|
|||
"shares",
|
||||
"splash",
|
||||
"svcs",
|
||||
"wopi",
|
||||
]
|
||||
self.j2 = {x: env.get_template(x + ".html") for x in jn}
|
||||
self.j2["opds"] = env.get_template("opds.xml")
|
||||
|
|
@ -315,7 +318,7 @@ class HttpSrv(object):
|
|||
|
||||
self.srvs.append(sck)
|
||||
self.bound.add((ip, port))
|
||||
self.nclimax = math.ceil(self.args.nc * 1.0 / nlisteners)
|
||||
self.nclimax = -int(-self.args.nc // nlisteners)
|
||||
Daemon(
|
||||
self.thr_listen,
|
||||
"httpsrv-n{}-listen-{}-{}".format(self.nid or "0", ip, port),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse # typechk
|
||||
import colorsys
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import errno
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import json
|
||||
import time
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import json
|
||||
|
|
@ -17,7 +17,9 @@ from .util import (
|
|||
FFMPEG_URL,
|
||||
LOG,
|
||||
REKOBO_LKEY,
|
||||
SCWD,
|
||||
VF_CAREFUL,
|
||||
absreal,
|
||||
expand_osenv_c,
|
||||
fsenc,
|
||||
gzip,
|
||||
|
|
@ -47,10 +49,10 @@ except:
|
|||
HAVE_MUTAGEN = False
|
||||
|
||||
|
||||
def have_ff(name: str) -> bytes:
|
||||
def have_ff(name: str) -> list[bytes]:
|
||||
uname = name.upper()
|
||||
if os.environ.get("PRTY_NO_" + uname):
|
||||
return b""
|
||||
return []
|
||||
|
||||
ebin = os.environ.get("PRTY_%s_BIN" % (uname,))
|
||||
try:
|
||||
|
|
@ -66,11 +68,12 @@ def have_ff(name: str) -> bytes:
|
|||
bcmd = scmd.encode("utf-8")
|
||||
try:
|
||||
sp.Popen([bcmd, b"-version"], stdout=sp.PIPE, stderr=sp.PIPE).communicate()
|
||||
return bcmd
|
||||
return [bcmd]
|
||||
except:
|
||||
return b""
|
||||
return []
|
||||
else:
|
||||
return (shutil.which(scmd) or "").encode("utf-8")
|
||||
ret = shutil.which(scmd)
|
||||
return [absreal(ret).encode("utf-8")] if ret else []
|
||||
|
||||
|
||||
HAVE_FFMPEG = have_ff("ffmpeg")
|
||||
|
|
@ -191,6 +194,12 @@ def au_unpk(
|
|||
log(t)
|
||||
fi = zf.open(using)
|
||||
|
||||
elif pk == "kra" or pk == "ora":
|
||||
import zipfile
|
||||
|
||||
zf = zipfile.ZipFile(abspath, "r")
|
||||
fi = zf.open("mergedimage.png")
|
||||
|
||||
elif pk == "epub":
|
||||
fi = get_cover_from_epub(log, abspath)
|
||||
assert fi # !rm
|
||||
|
|
@ -247,14 +256,14 @@ def ffprobe(
|
|||
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:
|
||||
# ffprobe -hide_banner -show_streams -show_format --
|
||||
bap = fsenc(abspath)
|
||||
cmd = bwrap(HAVE_FFPROBE, bap, b"") + [
|
||||
cmd = bwrap(HAVE_FFPROBE[0], bap, b"") + [
|
||||
b"-hide_banner",
|
||||
b"-show_streams",
|
||||
b"-show_format",
|
||||
b"--",
|
||||
bap,
|
||||
]
|
||||
rc, so, se = runcmd(cmd, timeout=timeout, nice=True, oom=200)
|
||||
rc, so, se = runcmd(cmd, cwd=SCWD, timeout=timeout, nice=True, oom=200)
|
||||
if rc and TH_BWRAP:
|
||||
bwrap_fail(se)
|
||||
retchk(rc, cmd, se)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import socket
|
||||
import time
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import errno
|
||||
import hashlib
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import errno
|
||||
import re
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import re
|
||||
import stat
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
|
|
@ -81,6 +81,7 @@ from .util import (
|
|||
alltrace,
|
||||
build_netmap,
|
||||
expat_ver,
|
||||
fsenc,
|
||||
gzip,
|
||||
html_escape,
|
||||
load_ipr,
|
||||
|
|
@ -233,10 +234,14 @@ class SvcHub(object):
|
|||
|
||||
self.lo1 = self.lo2 = ""
|
||||
if args.lo:
|
||||
do_xz = args.lo.replace("%R", "").lower().endswith(".xz")
|
||||
if "%R" not in args.lo:
|
||||
args.lo += "%R"
|
||||
if args.rlo in ("", "no"):
|
||||
args.rlo = ""
|
||||
if do_xz and not args.rlo:
|
||||
args.rlo = ".1"
|
||||
if args.rlo in ("", "del", "over"):
|
||||
args.lo = args.lo.replace("%R", "")
|
||||
try:
|
||||
self.lo1, self.lo2 = args.lo.split("%R")
|
||||
|
|
@ -396,6 +401,9 @@ class SvcHub(object):
|
|||
self.asrv = AuthSrv(self.args, self.log, dargs=self.dargs)
|
||||
ramdisk_chk(self.asrv)
|
||||
|
||||
if ANYWIN and not self.args.unsafe_tools:
|
||||
self._check_toolpaths()
|
||||
|
||||
if args.cgen:
|
||||
self.asrv.cgen()
|
||||
|
||||
|
|
@ -1021,6 +1029,18 @@ class SvcHub(object):
|
|||
if zb:
|
||||
Daemon(self.s6_notify, "s6-notify", (zb,))
|
||||
|
||||
def _check_toolpaths(self) -> None:
|
||||
for vol in self.asrv.vfs.all_vols.values():
|
||||
if not vol.realpath or not vol.axs.uwrite:
|
||||
continue
|
||||
ap_vol = (vol.realpath + os.sep).encode("utf-8")
|
||||
for zsl in (HAVE_FFMPEG, HAVE_FFPROBE, HAVE_DCRAW):
|
||||
if zsl and zsl[0].startswith(ap_vol):
|
||||
zs = zsl[0].decode("utf-8", "replace")
|
||||
t = "will not use [%s] because it is inside a writable volume [/%s] => [%s] and --unsafe-tools is not enabled"
|
||||
self.log("root", t % (zs, vol.vpath, vol.realpath), 3)
|
||||
del zsl[0]
|
||||
|
||||
def _feature_test(self) -> None:
|
||||
fok = []
|
||||
fng = []
|
||||
|
|
@ -1029,8 +1049,8 @@ class SvcHub(object):
|
|||
# fmt: off
|
||||
to_check = [
|
||||
(HAVE_SQLITE3, "sqlite", "sessions and file/media indexing"),
|
||||
(HAVE_PIL, "pillow", "image thumbnails (plenty fast)"),
|
||||
(HAVE_VIPS, "vips", "image thumbnails (faster, eats more ram)"),
|
||||
(HAVE_PIL, "pillow", "image thumbnails (good and fast)"),
|
||||
(HAVE_VIPS, "vips", "image thumbnails (eats ram; will prefer pillow)"),
|
||||
(H_PIL_WEBP, "pillow-webp", "create thumbnails as webp files"),
|
||||
(HAVE_FFMPEG, "ffmpeg", t_ff + ", good-but-slow image thumbnails"),
|
||||
(HAVE_FFPROBE, "ffprobe", t_ff + ", read audio/media tags"),
|
||||
|
|
@ -1107,14 +1127,19 @@ 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 not self.args.no_dav:
|
||||
if self.args.wopi or not self.args.no_dav:
|
||||
from .dxml import DXML_OK
|
||||
|
||||
if not DXML_OK:
|
||||
if not self.args.no_dav:
|
||||
self.args.no_dav = True
|
||||
t = "WARNING:\nDisabling WebDAV support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n"
|
||||
self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1)
|
||||
self.args.wopi = False
|
||||
self.args.no_dav = True
|
||||
t = "WARNING:\nDisabling WebDAV and WOPI support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n"
|
||||
self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1)
|
||||
|
||||
if self.args.wopi and self.args.j != 1:
|
||||
self.args.wopi = False
|
||||
t = "WARNING: Disabling --wopi because -j is not 1 (the default and recommended -j value)"
|
||||
self.log("root", t, 1)
|
||||
|
||||
if (
|
||||
not E.scfg
|
||||
|
|
@ -1183,14 +1208,6 @@ class SvcHub(object):
|
|||
if al.rsp_jtr:
|
||||
al.rsp_slp = 0.000001
|
||||
|
||||
zsl = al.th_covers.split(",")
|
||||
zsl = [x.strip() for x in zsl]
|
||||
zsl = [x for x in zsl if x]
|
||||
al.th_covers = zsl
|
||||
al.th_coversd = zsl + ["." + x for x in zsl]
|
||||
al.th_covers_set = set(al.th_covers)
|
||||
al.th_coversd_set = set(al.th_coversd)
|
||||
|
||||
zs = al.use_bwrap.strip().lower()
|
||||
if zs == "a":
|
||||
if not HAVE_BWRAP:
|
||||
|
|
@ -1446,7 +1463,7 @@ class SvcHub(object):
|
|||
def _setup_logfile(self) -> None:
|
||||
base_fn = fn = self._logname()
|
||||
sel_fn = fn + self.lo2
|
||||
do_xz = sel_fn.lower().endswith(".xz")
|
||||
do_xz = sel_fn.replace("%R", "").lower().endswith(".xz")
|
||||
if "%R" in self.args.lo:
|
||||
# yup this is a race; if started sufficiently concurrently, two
|
||||
# copyparties can grab the same logfile (considered and ignored)
|
||||
|
|
@ -1462,18 +1479,31 @@ class SvcHub(object):
|
|||
except:
|
||||
pass
|
||||
|
||||
if self.args.rlo in ("del", "over"):
|
||||
tmode = "wt"
|
||||
bmode = "w"
|
||||
else:
|
||||
tmode = "at"
|
||||
bmode = "a"
|
||||
|
||||
if self.args.rlo == "del":
|
||||
try:
|
||||
bos.unlink(fn)
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
if do_xz:
|
||||
import lzma
|
||||
|
||||
lh = lzma.open(fn, "wt", encoding="utf-8", errors="replace", preset=0)
|
||||
lh = lzma.open(fn, tmode, encoding="utf-8", errors="replace", preset=0)
|
||||
self.args.no_logflush = True
|
||||
else:
|
||||
lh = open(fn, "wt", encoding="utf-8", errors="replace")
|
||||
lh = open(fn, tmode, encoding="utf-8", errors="replace")
|
||||
except:
|
||||
import codecs
|
||||
|
||||
lh = codecs.open(fn, "w", encoding="utf-8", errors="replace")
|
||||
lh = codecs.open(fn, bmode, encoding="utf-8", errors="replace")
|
||||
|
||||
if getattr(self.args, "free_umask", False):
|
||||
os.fchmod(lh.fileno(), 0o644)
|
||||
|
|
@ -1488,6 +1518,8 @@ class SvcHub(object):
|
|||
printed = "".join(lprinted) + msg
|
||||
t = "t0: {:.3f}\nargv: {}\n\n{}"
|
||||
lh.write(t.format(self.E.t0, " ".join(argv), printed))
|
||||
if not self.args.no_logflush:
|
||||
lh.flush()
|
||||
self.logf = lh
|
||||
self.logf_base_fn = base_fn
|
||||
print(msg, end="")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import calendar
|
||||
import stat
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import re
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
try:
|
||||
from types import SimpleNamespace
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import errno
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
|
|
@ -31,6 +31,7 @@ from .mtag import (
|
|||
from .util import BytesIO # type: ignore
|
||||
from .util import (
|
||||
FFMPEG_URL,
|
||||
SCWD,
|
||||
VF_CAREFUL,
|
||||
Cooldown,
|
||||
Daemon,
|
||||
|
|
@ -774,7 +775,7 @@ class ThumbSrv(object):
|
|||
self.wait4ram(0.6, tpath)
|
||||
bap = fsenc(abspath)
|
||||
# fmt: off
|
||||
cmd = bwrap(HAVE_DCRAW, bap, b"") + [
|
||||
cmd = bwrap(HAVE_DCRAW[0], bap, b"") + [
|
||||
b"-h", # halfsize
|
||||
b"-o", b"1", # srgb
|
||||
b"-s", b"0", # first frame
|
||||
|
|
@ -782,7 +783,7 @@ class ThumbSrv(object):
|
|||
bap,
|
||||
]
|
||||
# fmt: on
|
||||
p = sp.Popen(cmd, stdout=sp.PIPE)
|
||||
p = sp.Popen(cmd, stdout=sp.PIPE, cwd=SCWD)
|
||||
try:
|
||||
if HAVE_PIL:
|
||||
self.conv_image_pil(Image.open(p.stdout), tpath, fmt, vn)
|
||||
|
|
@ -833,7 +834,7 @@ class ThumbSrv(object):
|
|||
|
||||
def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
self.wait4ram(0.2, tpath)
|
||||
ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
ret, raw, strms, ctnr = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
if not ret:
|
||||
return
|
||||
|
||||
|
|
@ -841,6 +842,13 @@ class ThumbSrv(object):
|
|||
# audio in a video trenchcoat
|
||||
return self.conv_spec(abspath, tpath, fmt, vn)
|
||||
|
||||
for strm in strms:
|
||||
if (
|
||||
strm.get("codec_type") == "video"
|
||||
and strm.get("DISPOSITION:attached_pic") == "1"
|
||||
):
|
||||
return self.conv_emb_cv(abspath, tpath, fmt, vn, strm)
|
||||
|
||||
ext = abspath.rsplit(".")[-1].lower()
|
||||
if ext in ["h264", "h265"] or ext in self.fmt_ffi:
|
||||
seek: list[bytes] = []
|
||||
|
|
@ -870,7 +878,7 @@ class ThumbSrv(object):
|
|||
bap_in = fsenc(abspath)
|
||||
bap_out = fsenc(tpath)
|
||||
# fmt: off
|
||||
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner"
|
||||
|
|
@ -911,7 +919,7 @@ class ThumbSrv(object):
|
|||
|
||||
def _run_ff(self, cmd: list[bytes], vn: VFS, kto: str, oom: int = 400) -> None:
|
||||
# self.log((b" ".join(cmd)).decode("utf-8"))
|
||||
ret, _, serr = runcmd(cmd, timeout=vn.flags[kto], nice=True, oom=oom)
|
||||
ret, _, serr = runcmd(cmd, cwd=SCWD, timeout=vn.flags[kto], nice=True, oom=oom)
|
||||
if not ret:
|
||||
return
|
||||
|
||||
|
|
@ -1013,7 +1021,7 @@ class ThumbSrv(object):
|
|||
bap_out = fsenc(tpath)
|
||||
|
||||
# fmt: off
|
||||
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
|
|
@ -1094,7 +1102,7 @@ class ThumbSrv(object):
|
|||
bap_out = fsenc(infile)
|
||||
|
||||
# fmt: off
|
||||
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
|
|
@ -1128,7 +1136,7 @@ class ThumbSrv(object):
|
|||
bap_out = fsenc(tpath)
|
||||
|
||||
# fmt: off
|
||||
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
|
|
@ -1166,7 +1174,7 @@ class ThumbSrv(object):
|
|||
# to not support opus then it's probably also super picky
|
||||
|
||||
# fmt: off
|
||||
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
|
|
@ -1197,7 +1205,7 @@ class ThumbSrv(object):
|
|||
bap_out = fsenc(tpath)
|
||||
|
||||
# fmt: off
|
||||
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
|
|
@ -1234,7 +1242,7 @@ class ThumbSrv(object):
|
|||
bap_out = fsenc(tpath)
|
||||
|
||||
# fmt: off
|
||||
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
|
|
@ -1297,7 +1305,7 @@ class ThumbSrv(object):
|
|||
bap_out = fsenc(tpath)
|
||||
|
||||
# fmt: off
|
||||
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
|
|
@ -1340,7 +1348,7 @@ class ThumbSrv(object):
|
|||
bap_out = fsenc(tmp_opus)
|
||||
|
||||
# fmt: off
|
||||
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
|
|
@ -1367,7 +1375,7 @@ class ThumbSrv(object):
|
|||
bap_in = fsenc(abspath)
|
||||
bap_out = fsenc(tpath)
|
||||
# fmt: off
|
||||
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
|
|
@ -1389,7 +1397,7 @@ class ThumbSrv(object):
|
|||
bap_in = fsenc(tmp_opus)
|
||||
bap_out = fsenc(tpath)
|
||||
# fmt: off
|
||||
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
|
||||
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import calendar
|
||||
import os
|
||||
|
|
@ -28,6 +28,14 @@ from .util import (
|
|||
if HAVE_SQLITE3:
|
||||
import sqlite3
|
||||
|
||||
try:
|
||||
if os.environ.get("PRTY_NO_UNIDATA"):
|
||||
raise Exception()
|
||||
|
||||
import unicodedata
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
from pathlib import Path
|
||||
except:
|
||||
|
|
@ -43,6 +51,14 @@ if PY2:
|
|||
range = xrange # type: ignore
|
||||
|
||||
|
||||
def norm_icase(txt: str) -> str:
|
||||
return txt.casefold() if txt else txt
|
||||
|
||||
|
||||
def norm_nfkc(txt: str) -> str:
|
||||
return unicodedata.normalize("NFKC", txt).casefold() if txt else txt
|
||||
|
||||
|
||||
class U2idx(object):
|
||||
def __init__(self, hsrv: "HttpSrv") -> None:
|
||||
self.log_func = hsrv.log
|
||||
|
|
@ -55,6 +71,10 @@ class U2idx(object):
|
|||
return
|
||||
|
||||
if self.args.srch_icase:
|
||||
if self.args.srch_nfkc:
|
||||
self.normfun = norm_nfkc
|
||||
else:
|
||||
self.normfun = norm_icase
|
||||
self._open_db = self._open_db_icase
|
||||
else:
|
||||
self._open_db = self._open_db_std
|
||||
|
|
@ -82,7 +102,7 @@ class U2idx(object):
|
|||
|
||||
def _open_db_icase(self, *args, **kwargs):
|
||||
db = self._open_db_std(*args, **kwargs)
|
||||
db.create_function("casefold", 1, lambda x: x.casefold() if x else x)
|
||||
db.create_function("casefold", 1, self.normfun)
|
||||
return db
|
||||
|
||||
def shutdown(self) -> None:
|
||||
|
|
@ -323,7 +343,7 @@ class U2idx(object):
|
|||
|
||||
if icase and "casefold(" in q:
|
||||
try:
|
||||
v = unicode(v).casefold()
|
||||
v = self.normfun(unicode(v))
|
||||
except:
|
||||
v = unicode(v).lower()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import errno
|
||||
import hashlib
|
||||
|
|
@ -74,6 +74,7 @@ from .util import (
|
|||
vsplit,
|
||||
w8b64dec,
|
||||
w8b64enc,
|
||||
winsparse,
|
||||
wunlink,
|
||||
)
|
||||
|
||||
|
|
@ -1159,7 +1160,7 @@ class Up2k(object):
|
|||
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 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 zipmax zipmaxn_v zipmaxs_v"
|
||||
fx = set(zs.split())
|
||||
fd = vf_bmap()
|
||||
fd.update(vf_cmap())
|
||||
|
|
@ -1431,6 +1432,8 @@ class Up2k(object):
|
|||
rtop,
|
||||
rei,
|
||||
reh,
|
||||
vol.flags["th_coversd"],
|
||||
vol.flags["th_coversd_set"],
|
||||
n4g,
|
||||
ffat,
|
||||
[],
|
||||
|
|
@ -1508,6 +1511,8 @@ class Up2k(object):
|
|||
rcdir: str,
|
||||
rei: Optional[Pattern[str]],
|
||||
reh: Optional[Pattern[str]],
|
||||
th_cvd: list[str],
|
||||
th_cvds: set[str],
|
||||
n4g: bool,
|
||||
ffat: bool,
|
||||
seen: list[str],
|
||||
|
|
@ -1533,8 +1538,6 @@ class Up2k(object):
|
|||
cv = vcv = acv = ""
|
||||
e_d = {}
|
||||
|
||||
th_cvd = self.args.th_coversd
|
||||
th_cvds = self.args.th_coversd_set
|
||||
scan_pr_s = self.args.scan_pr_s
|
||||
|
||||
assert self.pp and self.mem_cur # !rm
|
||||
|
|
@ -1601,6 +1604,8 @@ class Up2k(object):
|
|||
rap,
|
||||
rei,
|
||||
reh,
|
||||
th_cvd,
|
||||
th_cvds,
|
||||
n4g,
|
||||
fat32,
|
||||
seen,
|
||||
|
|
@ -1631,7 +1636,7 @@ class Up2k(object):
|
|||
|
||||
rsz += sz
|
||||
files.append((sz, lmod, iname))
|
||||
if sz:
|
||||
if sz and th_cvd:
|
||||
liname = iname.lower()
|
||||
ext = liname.rsplit(".", 1)[-1]
|
||||
if (
|
||||
|
|
@ -4117,31 +4122,7 @@ class Up2k(object):
|
|||
with self.rescan_cond:
|
||||
self.rescan_cond.notify_all()
|
||||
|
||||
if rd and sz and fn.lower() in self.args.th_coversd_set:
|
||||
# wasteful; db_add will re-index actual covers
|
||||
# but that won't catch existing files
|
||||
crd, cdn = rd.rsplit("/", 1) if "/" in rd else ("", rd)
|
||||
try:
|
||||
q = "select fn from cv where rd=? and dn=?"
|
||||
db_cv = db.execute(q, (crd, cdn)).fetchone()[0]
|
||||
db_lcv = db_cv.lower()
|
||||
if db_lcv in self.args.th_coversd_set:
|
||||
idx_db = self.args.th_coversd.index(db_lcv)
|
||||
idx_fn = self.args.th_coversd.index(fn.lower())
|
||||
add_cv = idx_fn < idx_db
|
||||
else:
|
||||
add_cv = True
|
||||
except:
|
||||
add_cv = True
|
||||
|
||||
if add_cv:
|
||||
try:
|
||||
db.execute("delete from cv where rd=? and dn=?", (crd, cdn))
|
||||
db.execute("insert into cv values (?,?,?)", (crd, cdn, fn))
|
||||
except:
|
||||
pass
|
||||
|
||||
if "nodirsz" not in vflags:
|
||||
if vflags and "nodirsz" not in vflags:
|
||||
try:
|
||||
q = "update ds set nf=nf+1, sz=sz+? where rd=?"
|
||||
q2 = "insert into ds values(?,?,1)"
|
||||
|
|
@ -4754,7 +4735,7 @@ class Up2k(object):
|
|||
fsize,
|
||||
ip,
|
||||
time.time(),
|
||||
None,
|
||||
[dabs],
|
||||
)
|
||||
t = hr.get("rejectmsg") or ""
|
||||
if t or hr.get("rc") != 0:
|
||||
|
|
@ -4797,7 +4778,7 @@ class Up2k(object):
|
|||
fsize,
|
||||
ip,
|
||||
time.time(),
|
||||
None,
|
||||
[sabs],
|
||||
)
|
||||
|
||||
return "k"
|
||||
|
|
@ -4927,7 +4908,7 @@ class Up2k(object):
|
|||
fsize,
|
||||
ip,
|
||||
time.time(),
|
||||
None,
|
||||
[sabs],
|
||||
)
|
||||
|
||||
return "k"
|
||||
|
|
@ -5335,9 +5316,12 @@ class Up2k(object):
|
|||
and self.args.sparse * 1024 * 1024 <= sz
|
||||
):
|
||||
try:
|
||||
sp.check_call(["fsutil", "sparse", "setflag", abspath])
|
||||
except:
|
||||
self.log("could not sparse %r" % (abspath,), 3)
|
||||
winsparse(f)
|
||||
# some kernels/etc are buggy; force sync:
|
||||
f.close()
|
||||
f = open(abspath, "rb+")
|
||||
except Exception as ex:
|
||||
self.log("could not sparse %r: %s" % (abspath, ex), 3)
|
||||
relabel = True
|
||||
sprs = False
|
||||
|
||||
|
|
@ -5724,7 +5708,7 @@ def up2k_chunksize(filesize: int) -> int:
|
|||
stepsize = 512 * 1024
|
||||
while True:
|
||||
for mul in [1, 2]:
|
||||
nchunks = math.ceil(filesize * 1.0 / chunksize)
|
||||
nchunks = -int(-filesize // chunksize)
|
||||
if nchunks <= 256 or (chunksize >= 32 * 1024 * 1024 and nchunks <= 4096):
|
||||
return chunksize
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
|
|
@ -429,6 +429,7 @@ IMPLICATIONS = [
|
|||
["e2vu", "e2v"],
|
||||
["e2vp", "e2v"],
|
||||
["e2v", "e2d"],
|
||||
["srch_nfkc", "srch_icase"],
|
||||
["hardlink_only", "hardlink"],
|
||||
["hardlink", "dedup"],
|
||||
["tftpvv", "tftpv"],
|
||||
|
|
@ -677,6 +678,12 @@ except:
|
|||
HAVE_BWRAP = ""
|
||||
|
||||
|
||||
if ANYWIN:
|
||||
SCWD = os.environ["systemroot"]
|
||||
else:
|
||||
SCWD = None
|
||||
|
||||
|
||||
def py_desc() -> str:
|
||||
interp = platform.python_implementation()
|
||||
py_ver = ".".join([str(x) for x in sys.version_info])
|
||||
|
|
@ -1354,7 +1361,7 @@ class MTHash(object):
|
|||
self.csz = chunksz
|
||||
|
||||
chunks: dict[int, tuple[str, int, int]] = {}
|
||||
nchunks = int(math.ceil(fsz / chunksz))
|
||||
nchunks = -int(-fsz // chunksz)
|
||||
for nch in range(nchunks):
|
||||
self.work_q.put(nch)
|
||||
|
||||
|
|
@ -2143,7 +2150,8 @@ class MultipartParser(object):
|
|||
boundary = get_boundary(self.headers)
|
||||
if boundary.startswith('"') and boundary.endswith('"'):
|
||||
boundary = boundary[1:-1] # dillo uses quotes
|
||||
self.log("boundary=%r" % (boundary,))
|
||||
if len(boundary) > 72: # rfc-2046 <=70
|
||||
raise Pebkac(400, "boundary 2big: %d" % (len(boundary),))
|
||||
|
||||
# spec says there might be junk before the first boundary,
|
||||
# can't have the leading \r\n if that's not the case
|
||||
|
|
@ -2947,6 +2955,25 @@ def trystat_shutil_copy2(log: "NamedLogger", src: bytes, dst: bytes) -> bytes:
|
|||
raise
|
||||
|
||||
|
||||
def trystat_shutil_move(log: "NamedLogger", src: bytes, dst: bytes) -> bytes:
|
||||
try:
|
||||
return shutil.move(src, dst)
|
||||
except:
|
||||
# ignore failed mtime on linux+ntfs; for example:
|
||||
# shutil.py:437 <copy2>: copystat(src, dst, follow_symlinks=follow_symlinks)
|
||||
# shutil.py:376 <copystat>: lookup("utime")(dst, ns=(st.st_atime_ns, st.st_mtime_ns),
|
||||
# [PermissionError] [Errno 1] Operation not permitted, '/windows/_videos'
|
||||
_, _, tb = sys.exc_info()
|
||||
for _, _, fun, _ in traceback.extract_tb(tb):
|
||||
if fun == "copystat":
|
||||
if log:
|
||||
t = "warning: failed to retain some file attributes (timestamp and/or permissions) during move from %r to %r:\n%s"
|
||||
log(t % (src, dst, min_ex()), 3)
|
||||
os.unlink(src)
|
||||
return dst # close enough
|
||||
raise
|
||||
|
||||
|
||||
def _fs_mvrm(
|
||||
log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any]
|
||||
) -> bool:
|
||||
|
|
@ -2998,7 +3025,9 @@ def _fs_mvrm(
|
|||
if not attempt and ex.errno == errno.EXDEV:
|
||||
t = "using copy+delete (%s)\n %s\n %s"
|
||||
log(t % (ex.strerror, src, dst))
|
||||
osfun = shutil.move
|
||||
if osfun in (os.replace, os.rename):
|
||||
args.insert(0, log)
|
||||
osfun = trystat_shutil_move
|
||||
continue
|
||||
if now - t0 > maxtime or attempt == 90209:
|
||||
raise
|
||||
|
|
@ -3035,7 +3064,7 @@ def atomic_move(log: "NamedLogger", src: str, dst: str, flags: dict[str, Any]) -
|
|||
os.unlink(bdst)
|
||||
except:
|
||||
pass
|
||||
shutil.move(bsrc, bdst) # type: ignore
|
||||
trystat_shutil_move(log, bsrc, bdst) # type: ignore
|
||||
|
||||
|
||||
def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
|
||||
|
|
@ -4032,7 +4061,7 @@ def runihook(
|
|||
|
||||
t0 = time.time()
|
||||
if fork:
|
||||
Daemon(runcmd, cmd, bcmd, ka=sp_ka)
|
||||
Daemon(runcmd, cmd, [bcmd], ka=sp_ka)
|
||||
else:
|
||||
rc, v, err = runcmd(bcmd, **sp_ka) # type: ignore
|
||||
if chk and rc:
|
||||
|
|
@ -4198,6 +4227,10 @@ def _runhook(
|
|||
if src in ("xm", "xban"):
|
||||
ja["txt"] = txt[0]
|
||||
ja["body"] = txt[1]
|
||||
elif src == "xbr":
|
||||
ja["ap_to"] = txt[0]
|
||||
elif src in ("xar.ln", "xar.mv"):
|
||||
ja["ap_from"] = txt[0]
|
||||
else:
|
||||
ja["wark"] = txt[0] # acshually the dwark but less confusing
|
||||
if imp:
|
||||
|
|
@ -4206,7 +4239,7 @@ def _runhook(
|
|||
return mod.main(ja)
|
||||
arg = json.dumps(ja)
|
||||
else:
|
||||
arg = txt[0] if txt else ap
|
||||
arg = txt[0] if txt and src in ("xm", "xban") else ap
|
||||
|
||||
if acmd[0].startswith("zmq:"):
|
||||
zi, zs = _zmq_hook(log, verbose, src, acmd[0][4:].lower(), arg, wait, sp_ka)
|
||||
|
|
@ -4501,6 +4534,18 @@ def hidedir(dp) -> None:
|
|||
pass
|
||||
|
||||
|
||||
def winsparse(f: typing.BinaryIO) -> None:
|
||||
assert ctypes # !rm
|
||||
assert wk32 # !rm
|
||||
import msvcrt
|
||||
|
||||
fh = msvcrt.get_osfhandle(f.fileno())
|
||||
if not wk32.DeviceIoControl(
|
||||
fh, 0x900C4, None, 0, None, 0, ctypes.byref(ctypes.c_ulong()), None
|
||||
):
|
||||
raise ctypes.WinError(ctypes.get_last_error())
|
||||
|
||||
|
||||
_flocks = {}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -121,6 +121,8 @@
|
|||
|
||||
<h2 id="wfp"><a href="{{ r }}/?h" id="goh">control-panel</a></h2>
|
||||
|
||||
<p id="js_bork">Looks like javascript is broken; see "csp nonce" section in readme</p>
|
||||
|
||||
<a href="#" id="repl">π</a>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ if (1)
|
|||
["ctrl-V", "paste (move/copy) here"],
|
||||
["Y", "download selected"],
|
||||
["F2", "rename selected"],
|
||||
["F4", "update/refresh"],
|
||||
|
||||
"file-list-sel",
|
||||
["space", "toggle file selection"],
|
||||
|
|
@ -1248,6 +1249,9 @@ 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;
|
||||
|
||||
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(' '));
|
||||
|
||||
|
||||
function set_files_html(html) {
|
||||
var files = ebi('files');
|
||||
|
|
@ -4058,6 +4062,11 @@ var fileman = (function () {
|
|||
}
|
||||
shui.style.display = 'block';
|
||||
|
||||
var wr = has(perms, 'write') && !fns.length;
|
||||
var nope = ['admin', 'move', 'delete', 'upget'];
|
||||
if (!wr)
|
||||
nope.push('write');
|
||||
|
||||
var html = [
|
||||
'<div>',
|
||||
'<table>',
|
||||
|
|
@ -4078,10 +4087,10 @@ var fileman = (function () {
|
|||
'<tr><td>perms</td><td class="sh_axs">',
|
||||
];
|
||||
for (var a = 0; a < perms.length; a++)
|
||||
if (!has(['admin', 'move', 'delete', 'upget'], perms[a]))
|
||||
if (!has(nope, perms[a]))
|
||||
html.push('<a href="#" class="tgl btn">' + perms[a] + '</a>');
|
||||
|
||||
if (has(perms, 'write'))
|
||||
if (wr)
|
||||
html.push('<a href="#" class="btn">write-only</a>');
|
||||
|
||||
html.push('</td></tr></div');
|
||||
|
|
@ -6430,6 +6439,9 @@ var ahotkeys = function (e) {
|
|||
if (k == 'F2')
|
||||
return fileman.rename();
|
||||
|
||||
if (k == 'F4')
|
||||
return treectl.goto();
|
||||
|
||||
if (!treectl.hidden && (!sh || !thegrid.en)) {
|
||||
if (kl == 'a')
|
||||
return QS('#twig').click();
|
||||
|
|
@ -7681,6 +7693,11 @@ var treectl = (function () {
|
|||
tn.href = addq(tn.href, 'v');
|
||||
}
|
||||
|
||||
|
||||
if (wopi_set && wopi_set.has(tn.ext))
|
||||
tn.lead = '<a href="?wopi=' + bhref +
|
||||
'" rel="nofollow" name="' + hname + '">📄</a>';
|
||||
|
||||
if (tn.lead == '-')
|
||||
tn.lead = '<a href="?doc=' + bhref + '" id="t' + id +
|
||||
'" rel="nofollow" class="doc' + (lang ? ' bri' : '') +
|
||||
|
|
@ -7773,6 +7790,7 @@ var treectl = (function () {
|
|||
|
||||
r.hydrate = function () {
|
||||
qsr('#bbsw');
|
||||
qsr('#js_bork');
|
||||
srvinf = ebi('srv_info').innerHTML.slice(6, -7);
|
||||
if (ls0 === null) {
|
||||
r.ls_cb = showfile.addlinks;
|
||||
|
|
@ -8859,7 +8877,7 @@ var msel = (function () {
|
|||
r.evsel = function (e, fun) {
|
||||
ev(e);
|
||||
r.so = r.pr = null;
|
||||
var trs = QSA('#files tbody tr');
|
||||
var trs = QSA('#files tbody tr' + (ebi('unsearch') ? ':not(.srch_hdr)' : ''));
|
||||
for (var a = 0, aa = trs.length; a < aa; a++)
|
||||
clmod(trs[a], 'sel', fun);
|
||||
r.selui();
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Ls.chi = {
|
|||
["ctrl-V", "粘贴(移动/复制)到此处"],
|
||||
["Y", "下载选中项"],
|
||||
["F2", "重命名选中项"],
|
||||
["F4", "刷新"], //m
|
||||
|
||||
"文件列表选择",
|
||||
["space", "切换文件选择"],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Ls.cze = {
|
|||
["ctrl-V", "vložit (přesunout/kopírovat) zde"],
|
||||
["Y", "stáhnout vybrané"],
|
||||
["F2", "přejmenovat vybrané"],
|
||||
["F4", "obnovit"], //m
|
||||
|
||||
"výběr souborů",
|
||||
["space", "přepnout výběr souboru"],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Ls.deu = {
|
|||
["STRG-V", "Zwischenablage hier einfügen"],
|
||||
["Y", "Auswahl herunterladen"],
|
||||
["F2", "Auswahl umbenennen"],
|
||||
["F4", "Aktualisieren"], //m
|
||||
|
||||
"file-list-sel",
|
||||
["LEER", "Dateiauswahl aktivieren"],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Ls.epo = {
|
|||
["stir-V", "alglui (movi/kopii) ĉi tien"],
|
||||
["Y", "elŝuti elektitajn"],
|
||||
["F2", "alinomi elektitajn"],
|
||||
["F4", "refreŝigi"], //m
|
||||
|
||||
"file-list-sel",
|
||||
["spacoklavo", "baskuli elektadon de dosieroj"],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Ls.fin = {
|
|||
["ctrl-V", "siirrä tai kopioi tähän"],
|
||||
["Y", "lataa valitut"],
|
||||
["F2", "uudelleennimeä valitut"],
|
||||
["F4", "päivitä"], //m
|
||||
|
||||
"file-list-sel",
|
||||
["space", "vaihda tiedostonvalintatilaan"],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Ls.fra = {
|
|||
["ctrl-V", "coller (déplacer/copier) ici"],
|
||||
["Y", "télécharger la sélection"],
|
||||
["F2", "renomer la sélection"],
|
||||
["F4", "actualiser"], //m
|
||||
|
||||
"file-list-sel",
|
||||
["Espace", "activer la sélection de fichiers"],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Ls.grc = {
|
|||
["ctrl-V", "επικόλληση (μετακίνηση/αντιγραφή) εδώ"],
|
||||
["Y", "λήψη επιλεγμένων"],
|
||||
["F2", "μετονομασία επιλεγμένων"],
|
||||
["F4", "ανανέωση"], //m
|
||||
|
||||
"λίστα αρχείων",
|
||||
["space", "εναλλαγή επιλογής αρχείου"],
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ Ls.hun = {
|
|||
['ctrl-V', 'beillesztés ide'],
|
||||
['Y', 'kijelöltek letöltése'],
|
||||
['F2', 'átnevezés'],
|
||||
["F4", "frissítés"], //m
|
||||
|
||||
'kijelölés',
|
||||
['space', 'fájl kijelölése'],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Ls.ita = {
|
|||
["ctrl-V", "incolla (sposta/copia) qui"],
|
||||
["Y", "scarica selezionati"],
|
||||
["F2", "rinomina selezionati"],
|
||||
["F4", "aggiorna"], //m
|
||||
|
||||
"file-list-sel",
|
||||
["spazio", "alterna selezione file"],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Ls.jpn = {
|
|||
["ctrl-V", "ここに貼り付け(移動/コピー)"],
|
||||
["Y", "選択した項目をダウンロード"],
|
||||
["F2", "選択した項目の名前を変更"],
|
||||
["F4", "更新"], //m
|
||||
|
||||
"file-list-sel",
|
||||
["space", "ファイル選択の切り替え"],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Ls.kor = {
|
|||
["ctrl-V", "여기에 붙여넣기 (이동/복사)"],
|
||||
["Y", "선택 항목 다운로드"],
|
||||
["F2", "선택 항목 이름 바꾸기"],
|
||||
["F4", "새로 고침"], //m
|
||||
|
||||
"파일 목록 선택",
|
||||
["space", "파일 선택/해제"],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Ls.nld = {
|
|||
["ctrl-V", "Hier plakken (verplaatsen/kopieëren)"],
|
||||
["Y", "Download geselecteerde"],
|
||||
["F2", "Hernoem geselecteerde"],
|
||||
["F4", "vernieuwen"], //m
|
||||
|
||||
"bestand-lijst-selectie",
|
||||
["space", "wissel bestand selectie"],
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ Ls.nno = {
|
|||
["ctrl-V", "lim inn (flytt/kopiér)"],
|
||||
["Y", "last ned valde"],
|
||||
["F2", "endre namn på valde"],
|
||||
["F4", "oppdater"],
|
||||
|
||||
"filmarkering",
|
||||
["space", "markér fil"],
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ Ls.nor = {
|
|||
["ctrl-V", "lim inn (flytt/kopiér)"],
|
||||
["Y", "last ned valgte"],
|
||||
["F2", "endre navn på valgte"],
|
||||
["F4", "oppdater"],
|
||||
|
||||
"filmarkering",
|
||||
["space", "marker fil"],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Ls.pol = {
|
|||
["ctrl-V", "wklej (przenieś/skopiuj) tutaj"],
|
||||
["Y", "pobierz zaznaczone"],
|
||||
["F2", "zmień nazwę zaznaczonych"],
|
||||
["F4", "odśwież"], //m
|
||||
|
||||
"file-list-sel",
|
||||
["spacja", "przełącz zaznaczanie plików"],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Ls.por = {
|
|||
["ctrl-V", "colar (mover/copiar) aqui"],
|
||||
["Y", "baixar selecionado"],
|
||||
["F2", "renomear selecionado"],
|
||||
["F4", "atualizar"], //m
|
||||
|
||||
"seleção de lista de arquivos",
|
||||
["espaço", "alternar seleção de arquivo"],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Ls.rus = {
|
|||
["ctrl-V", "вставить (переместить/копировать) сюда"],
|
||||
["Y", "скачать выделенное"],
|
||||
["F2", "переименовать выделенное"],
|
||||
["F4", "обновить"], //m
|
||||
|
||||
"выделение файлов",
|
||||
["пробел", "выделить/снять выделение с текущего файла"],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Ls.spa = {
|
|||
["ctrl-V", "pegar (mover/copiar) aquí"],
|
||||
["Y", "descargar seleccionados"],
|
||||
["F2", "renombrar seleccionados"],
|
||||
["F4", "actualizar"], //m
|
||||
|
||||
"selección en lista de archivos",
|
||||
["space", "alternar selección de archivo"],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Ls.swe = {
|
|||
["ctrl-V", "klistra in (kopiera/flytta) hit"],
|
||||
["Y", "ladda ner urval"],
|
||||
["F2", "byt namn på urval"],
|
||||
["F4", "uppdatera"], //m
|
||||
|
||||
"välja filer",
|
||||
["Blanksteg", "växla val av fil"],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Ls.tur = {
|
|||
["ctrl-V", "buraya yapıştır (taşı/kopyala)"],
|
||||
["Y", "seçileni indir"],
|
||||
["F2", "seçileni yeniden adlandır"],
|
||||
["F4", "yenile"], //m
|
||||
|
||||
"dosya yönetimi seçimleri",
|
||||
["boşluk", "seçimi değiştir"],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Ls.ukr = {
|
|||
["ctrl-V", "вставити (перемістити/копіювати) сюди"],
|
||||
["Y", "завантажити вибране"],
|
||||
["F2", "перейменувати вибране"],
|
||||
["F4", "оновити"], //m
|
||||
|
||||
"вибір файлів у списку",
|
||||
["space", "перемкнути вибір файлу"],
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ Ls.vie = {
|
|||
["ctrl-V", "dán (di chuyển/sao chép) tại đây"],
|
||||
["Y", "tải xuống mục đã chọn"],
|
||||
["F2", "đổi tên mục đã chọn"],
|
||||
["F4", "làm mới"], //m
|
||||
|
||||
"file-list-sel",
|
||||
["space", "chuyển đổi chọn tệp"],
|
||||
|
|
|
|||
|
|
@ -998,6 +998,14 @@ function up2k_init(subtle) {
|
|||
});
|
||||
}
|
||||
|
||||
function unqueue_up(t) {
|
||||
t.postlist = [];
|
||||
var arr = st.todo.upload;
|
||||
for (var a = arr.length - 1; a >= 0; a--)
|
||||
if (arr[a].nfile == t.n)
|
||||
arr.splice(a, 1);
|
||||
}
|
||||
|
||||
var pvis = new U2pvis("bz", '#u2cards', uc, st),
|
||||
donut = new Donut(uc, st);
|
||||
|
||||
|
|
@ -1069,25 +1077,28 @@ function up2k_init(subtle) {
|
|||
return onovercmn(this, e, true);
|
||||
}
|
||||
function onovercmn(self, e, btn) {
|
||||
var ok = false;
|
||||
try {
|
||||
var ok = false, dt = e.dataTransfer.types;
|
||||
for (var a = 0; a < dt.length; a++)
|
||||
var dt = e.dataTransfer.types;
|
||||
|
||||
for (var a = 0; a < dt.length && !ok; a++)
|
||||
if (dt[a] == 'Files')
|
||||
ok = true;
|
||||
else if (dt[a] == 'text/uri-list') {
|
||||
if (btn) {
|
||||
|
||||
for (var a = 0; a < dt.length && !ok; a++)
|
||||
if (dt[a] == 'text/uri-list') {
|
||||
if (btn)
|
||||
ok = true;
|
||||
if (toast.txt == L.u_uri)
|
||||
toast.hide();
|
||||
}
|
||||
else
|
||||
return toast.inf(10, L.u_uri) || true;
|
||||
}
|
||||
|
||||
if (!ok)
|
||||
return true;
|
||||
if (toast.txt == L.u_uri && ok)
|
||||
toast.hide();
|
||||
}
|
||||
catch (ex) { }
|
||||
if (!ok)
|
||||
return true;
|
||||
|
||||
ev(e);
|
||||
try {
|
||||
|
|
@ -2509,6 +2520,7 @@ function up2k_init(subtle) {
|
|||
keepalive = t.keepalive,
|
||||
me = Date.now();
|
||||
|
||||
while (apop(st.todo.handshake, t));
|
||||
if (t.done)
|
||||
return console.log('done; skip hs', t.name, t);
|
||||
|
||||
|
|
@ -2627,7 +2639,7 @@ function up2k_init(subtle) {
|
|||
cbd.push(a == cdr_idx ? cdr_sz : chunksize);
|
||||
}
|
||||
|
||||
t.postlist = [];
|
||||
unqueue_up(t);
|
||||
t.wark = response.wark;
|
||||
var missing = response.hash;
|
||||
for (var a = 0; a < missing.length; a++) {
|
||||
|
|
@ -2916,8 +2928,7 @@ function up2k_init(subtle) {
|
|||
var txt = unpre((xhr.response && xhr.response.err) || xhr.responseText);
|
||||
if (txt.indexOf('upload blocked by x') + 1) {
|
||||
apop(st.busy.upload, upt);
|
||||
for (var a = pcar; a <= pcdr; a++)
|
||||
apop(t.postlist, a);
|
||||
unqueue_up(t);
|
||||
pvis.seth(t.n, 1, "ERROR");
|
||||
pvis.seth(t.n, 2, txt.split(/\n/)[0]);
|
||||
pvis.move(t.n, 'ng');
|
||||
|
|
@ -2940,8 +2951,10 @@ function up2k_init(subtle) {
|
|||
st.etac.u++;
|
||||
st.etac.t++;
|
||||
}
|
||||
else if (txt.indexOf('already got that') + 1 ||
|
||||
txt.indexOf('already being written') + 1) {
|
||||
else if (txt.indexOf('already got that') + 1) {
|
||||
unqueue_up(t);
|
||||
}
|
||||
else if (txt.indexOf('already being written') + 1) {
|
||||
t.nojoin = t.nojoin || t.postlist.length;
|
||||
console.log("ignoring dupe-segment with backoff", t.nojoin, t.name, t);
|
||||
if (!toast.visible && st.todo.upload.length < 4)
|
||||
|
|
@ -2949,6 +2962,10 @@ function up2k_init(subtle) {
|
|||
}
|
||||
else {
|
||||
xhrchk(xhr, L.u_cuerr2.format(snpart, Math.ceil(t.size / chunksize), esc(t.name)), "404, target folder not found (???)", "warn", t);
|
||||
if (txt.indexOf('unknown wark') == 0) {
|
||||
t.cooldown = t.coolmul = 0;
|
||||
unqueue_up(t);
|
||||
}
|
||||
chill(t);
|
||||
}
|
||||
orz2(xhr);
|
||||
|
|
@ -2959,8 +2976,10 @@ function up2k_init(subtle) {
|
|||
apop(t.postlist, a);
|
||||
if (!t.postlist.length) {
|
||||
t.t_uploaded = Date.now();
|
||||
pvis.seth(t.n, 1, 'verifying');
|
||||
st.todo.handshake.unshift(t);
|
||||
if (!has(st.busy.handshake, t)) {
|
||||
pvis.seth(t.n, 1, 'verifying');
|
||||
st.todo.handshake.unshift(t);
|
||||
}
|
||||
}
|
||||
tasker();
|
||||
}
|
||||
|
|
@ -2995,6 +3014,7 @@ function up2k_init(subtle) {
|
|||
if (!toast.visible)
|
||||
toast.warn(9.98, L.u_cuerr.format(snpart, Math.ceil(t.size / chunksize), esc(t.name)), t);
|
||||
|
||||
unqueue_up(t); // maybe unknown wark (ff drops undrained rsp)
|
||||
t.nojoin = t.nojoin || t.postlist.length; // maybe rproxy postsize limit
|
||||
console.log('chunkpit onerror,', t.name, t);
|
||||
orz2(xhr);
|
||||
|
|
|
|||
|
|
@ -1199,8 +1199,10 @@ function has(haystack, needle) {
|
|||
|
||||
function apop(arr, v) {
|
||||
var ofs = arr.indexOf(v);
|
||||
if (ofs !== -1)
|
||||
if (ofs !== -1) {
|
||||
arr.splice(ofs, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1466,7 +1468,7 @@ function Debounce(delay) {
|
|||
};
|
||||
|
||||
r.rm = function (fun) {
|
||||
apop(r.q, fun);
|
||||
while (apop(r.q, fun));
|
||||
};
|
||||
|
||||
r.run = function () {
|
||||
|
|
@ -1526,7 +1528,7 @@ var timer = (function () {
|
|||
};
|
||||
|
||||
r.rm = function (fun) {
|
||||
apop(r.q, fun);
|
||||
while (apop(r.q, fun));
|
||||
};
|
||||
|
||||
var doevents = function () {
|
||||
|
|
|
|||
42
copyparty/web/wopi.html
Normal file
42
copyparty/web/wopi.html
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" id="ht_wopi">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>{{ title|e }}</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<style>
|
||||
|
||||
body,#w {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
</style></head><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 type="submit" value="" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<iframe id="w" name="w" allow="clipboard-read *; clipboard-write *; fullscreen *"></iframe>
|
||||
|
||||
<script nonce="{{ js_nonce }}">
|
||||
document.getElementById("submit-form").submit();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
@ -1,3 +1,78 @@
|
|||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2026-0727-2202 `v1.20.19` SECURITY: fix FTP upload outside volume
|
||||
|
||||
## ⚠️ ATTN: this release fixes a vulnerability in FTP and FTPS (not SFTP)
|
||||
|
||||
the FTP-server (default-disabled) would allow uploading to any folder that the copyparty process had permission to write to, but with certain limitations; see [GHSA-phv8-wgjp-g4p9](https://github.com/9001/copyparty/security/advisories/GHSA-phv8-wgjp-g4p9)
|
||||
|
||||
## recent important news
|
||||
|
||||
* [v1.20.19 (2026-07-27)](https://github.com/9001/copyparty/releases/tag/v1.20.19) fixed an FTP-server vuln (upload outside defined volumes)
|
||||
* [v1.20.17 (2026-07-06)](https://github.com/9001/copyparty/releases/tag/v1.20.17) fixed a vuln when a volume has both filekeys and dirkeys enabled
|
||||
* [v1.20.17 (2026-07-06)](https://github.com/9001/copyparty/releases/tag/v1.20.17) introduced csp nonces, possibly breaking some javascript-based plugins
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #1495 wopi integration -- edit office documents in the web-ui (thx @brandon-doornbos!) d57bb0c7 10db4236
|
||||
* currently only works with collabora online as wopi client #1574 and there's no docs/examples #1575
|
||||
* thumbnails: use embedded cover-image in videos when available 59524018
|
||||
* thumbnails: folder-thumbs can be disabled with `th-covers: no` (volflag or global) 856fada3
|
||||
* #1555 macos: add [--srch-nfkc](https://copyparty.eu/cli/#g-srch-nfkc) to fix searching for filenames / paths in CJK languages a7c99094
|
||||
* reduces search performance to around 30% when enabled
|
||||
* hooks: `xbr` / `xar` did not include old/new abspath as parameters; now they do c122e103
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* ftp: fix [GHSA-phv8-wgjp-g4p9](https://github.com/9001/copyparty/security/advisories/GHSA-phv8-wgjp-g4p9) b331bb1c
|
||||
* #1563 moving files between volumes could fail depending on OS and underlying filesystem c70dc7ac
|
||||
* fix drag-drop uploading in certain glitchy KDE environments (thx @tilse!) daf144af
|
||||
* hooks: `xiu` crashed if the fork-flag was set (thx @stackxp!) aa862353
|
||||
* hooks: `xau` without json-flag would be given the wark (file hash) instead of the abspath bae77b90
|
||||
* shares: fix markdown-viewer (`?v`) inside shares 6a9437b7
|
||||
* ftp: fix logging from `xbu` hooks 9912a951
|
||||
* fix slow boot if a volume had lots of files in its toplevel folder cdb474c8
|
||||
* python2.7: fix multithreaded file-hashing 0f2040c3
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* #1556 the libvips thumbnailer was demoted to last-fallback due to frequently using excessive amounts of ram ed0be42a
|
||||
* if [-lo](https://copyparty.eu/cli/#g-lo) points to an existing file, it will now be appended to instead of overwritten depending on [--rlo](https://copyparty.eu/cli/#g-help-rlo) b6abc33f
|
||||
* #1530 nixos: the nix package now uses `ffmpeg-headless` instead of `ffmpeg-full` (thx @nyakase!) fface524
|
||||
* slightly longer session cookie (was 20, now 24 chars) 537a99df
|
||||
* Windows-specific:
|
||||
* add a warning regarding the risks of DLL-hijacking when relevant bb40804f
|
||||
* fix some trivial PATH-related footguns cea97ac6
|
||||
* faster creation of sparse files bc45299b
|
||||
* fix detection of filesystem characteristics 6226858b
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2026-0709-2254 `v1.20.18` bwrap off
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* [shadowing](https://github.com/9001/copyparty/#shadowing): filesystem-path `//NULL` will fully unmap a volume from the server's filesystem (no more `/var/lib/empties/4` jank) 88846061
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* fix a false-positive warning on startup 12878e6c1cbf4417da42d46230dff686dd48760b
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* #1552 #1551 default-disabled the bwrap ffmpeg sandbox; 85a8cc99
|
||||
* sandboxing can fail in too many creative ways (funky linux distros with funky filesystem layouts and policies) so autoconfiguring bwrap is not feasible
|
||||
* to enable it, set `use-bwrap: y` after adjusting [th-bwrap](https://copyparty.eu/cli/#g-th-bwrap) to match your OS/env
|
||||
* see the default `th-bwrap` value in `--help` on your server for a best-effort guess
|
||||
* always rotate (never overwrite) `-lo` logfiles on startup unless `--rlo no` 29e145f5
|
||||
* thumbnails: save a tiny bit of I/O by not refreshing expiration-times if `th-clean` (expiration) is disabled anyways e15f2927
|
||||
|
||||
## 🌠 fun facts
|
||||
|
||||
* no
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2026-0706-1937 `v1.20.17` SECURITY: fix dirkeys
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ def up2k_chunksize(filesize):
|
|||
stepsize = 512 * 1024
|
||||
while True:
|
||||
for mul in [1, 2]:
|
||||
nchunks = math.ceil(filesize * 1.0 / chunksize)
|
||||
nchunks = -int(-filesize // chunksize)
|
||||
if nchunks <= 256 or (chunksize >= 32 * 1024 * 1024 and nchunks <= 4096):
|
||||
return chunksize
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# you can include additional config like this
|
||||
# (the space after the % is important)
|
||||
#
|
||||
# specify either a single file or a folder with .conf files;
|
||||
# since copyparty.d is a folder, it'll include all *.conf
|
||||
# files inside (not recursively) in alphabetical order
|
||||
# (not necessarily same as numerical/natural order)
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ with image size after installation and when gzipped
|
|||
* [`min`](https://hub.docker.com/r/copyparty/min) (57 MiB, 20 gz) is just copyparty itself
|
||||
* [`im`](https://hub.docker.com/r/copyparty/im) (70 MiB, 25 gz) can thumbnail images with pillow, parse media files with mutagen
|
||||
* [`ac` (163 MiB, 56 gz)](https://hub.docker.com/r/copyparty/ac) is `im` plus ffmpeg for video/audio thumbs + audio transcoding + better tags
|
||||
* [`iv`](https://hub.docker.com/r/copyparty/iv) (211 MiB, 73 gz) is `ac` plus vips for faster heif / avic / jxl thumbnails
|
||||
* [`iv`](https://hub.docker.com/r/copyparty/iv) (211 MiB, 73 gz) is `ac` plus vips for more thumbnail formats
|
||||
* [`dj`](https://hub.docker.com/r/copyparty/dj) (309 MiB, 104 gz) is `iv` plus beatroot/keyfinder to detect musical keys and bpm
|
||||
|
||||
[`ac` is recommended](https://hub.docker.com/r/copyparty/ac) since the additional features available in `iv` and `dj` are rarely useful
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ command -v gsort >/dev/null && {
|
|||
|
||||
cd ../dist
|
||||
|
||||
kwds='-bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -urlform -exp -ls -dbd -chmod -pwhash -zm'
|
||||
kwds='-bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -urlform -exp -rlo -ls -dbd -chmod -pwhash -zm'
|
||||
|
||||
export PRTY_FULL_HELP=1
|
||||
export PRTY_NO_PARAMIKO=1
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ rm -rf mods/magic/
|
|||
sed -ri '/(bccache|PackageLoader)/d' mods/jinja2/__init__.py
|
||||
af '/^class/{s=0}/^class PackageLoader/{s=1}!s' mods/jinja2/loaders.py
|
||||
sed -ri 's/from url.*Request, urlopen.*/pass/' mods/copyparty/svchub.py
|
||||
sed -ri 's/from urllib.* import urlopen.*/pass/' mods/copyparty/httpcli.py
|
||||
sed -ri 's/(.*"--vc-.*, help=).*/\1argparse.SUPPRESS)/' mods/copyparty/__main__.py
|
||||
}
|
||||
[ $w10 ] && {
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 554 B After Width: | Height: | Size: 555 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
|
@ -141,3 +141,4 @@ copyparty/web/tl/vie.js,
|
|||
copyparty/web/ui.css,
|
||||
copyparty/web/up2k.js,
|
||||
copyparty/web/util.js,
|
||||
copyparty/web/wopi.html,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
# coding: latin-1
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import re, os, sys, time, shutil, signal, tarfile, hashlib, platform, tempfile, traceback
|
||||
import subprocess as sp
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ Ls.hmn = {
|
|||
["ctrl-V", "paste (move/copy) here"],
|
||||
["Y", "download selected"],
|
||||
["F2", "rename selected"],
|
||||
["F4", "update/refresh"],
|
||||
|
||||
"file-list-sel",
|
||||
["space", "toggle file selection"],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import io
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import json
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import io
|
||||
import json
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import re
|
||||
import unittest
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import io
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import json
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import re
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import json
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import json
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import unittest
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import json
|
||||
import os
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue