Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85a637af09 | ||
|
|
043e3c7dd6 | ||
|
|
8f59afb159 | ||
|
|
77f1e51444 | ||
|
|
22fc4bb938 | ||
|
|
50c7bba6ea | ||
|
|
551d99b71b | ||
|
|
b54b7213a7 | ||
|
|
a14943c8de | ||
|
|
a10cad54fc | ||
|
|
8568b7702a | ||
|
|
5d8cb34885 | ||
|
|
8d248333e8 | ||
|
|
99e2ef7f33 | ||
|
|
e767230383 | ||
|
|
90601314d6 | ||
|
|
9c5eac1274 | ||
|
|
50905439e4 | ||
|
|
a0c1239246 | ||
|
|
b8e851c332 | ||
|
|
baaf2eb24d | ||
|
|
e197895c10 | ||
|
|
cb75efa05d | ||
|
|
8b0cf2c982 | ||
|
|
fc7d9e1f9c | ||
|
|
10caafa34c | ||
|
|
22cc22225a | ||
|
|
22dff4b0e5 |
40
README.md
40
README.md
@@ -66,12 +66,14 @@ turn almost any device into a file server with resumable uploads/downloads using
|
|||||||
* [file parser plugins](#file-parser-plugins) - provide custom parsers to index additional tags
|
* [file parser plugins](#file-parser-plugins) - provide custom parsers to index additional tags
|
||||||
* [event hooks](#event-hooks) - trigger a program on uploads, renames etc ([examples](./bin/hooks/))
|
* [event hooks](#event-hooks) - trigger a program on uploads, renames etc ([examples](./bin/hooks/))
|
||||||
* [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/))
|
* [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/))
|
||||||
|
* [handlers](#handlers) - redefine behavior with plugins ([examples](./bin/handlers/))
|
||||||
* [hiding from google](#hiding-from-google) - tell search engines you dont wanna be indexed
|
* [hiding from google](#hiding-from-google) - tell search engines you dont wanna be indexed
|
||||||
* [themes](#themes)
|
* [themes](#themes)
|
||||||
* [complete examples](#complete-examples)
|
* [complete examples](#complete-examples)
|
||||||
* [reverse-proxy](#reverse-proxy) - running copyparty next to other websites
|
* [reverse-proxy](#reverse-proxy) - running copyparty next to other websites
|
||||||
* [packages](#packages) - the party might be closer than you think
|
* [packages](#packages) - the party might be closer than you think
|
||||||
* [arch package](#arch-package) - now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
|
* [arch package](#arch-package) - now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
|
||||||
|
* [fedora package](#fedora-package) - now [available on copr-pypi](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/)
|
||||||
* [nix package](#nix-package) - `nix profile install github:9001/copyparty`
|
* [nix package](#nix-package) - `nix profile install github:9001/copyparty`
|
||||||
* [nixos module](#nixos-module)
|
* [nixos module](#nixos-module)
|
||||||
* [browser support](#browser-support) - TLDR: yes
|
* [browser support](#browser-support) - TLDR: yes
|
||||||
@@ -85,6 +87,7 @@ turn almost any device into a file server with resumable uploads/downloads using
|
|||||||
* [security](#security) - some notes on hardening
|
* [security](#security) - some notes on hardening
|
||||||
* [gotchas](#gotchas) - behavior that might be unexpected
|
* [gotchas](#gotchas) - behavior that might be unexpected
|
||||||
* [cors](#cors) - cross-site request config
|
* [cors](#cors) - cross-site request config
|
||||||
|
* [password hashing](#password-hashing) - you can hash passwords
|
||||||
* [https](#https) - both HTTP and HTTPS are accepted
|
* [https](#https) - both HTTP and HTTPS are accepted
|
||||||
* [recovering from crashes](#recovering-from-crashes)
|
* [recovering from crashes](#recovering-from-crashes)
|
||||||
* [client crashes](#client-crashes)
|
* [client crashes](#client-crashes)
|
||||||
@@ -343,6 +346,7 @@ permissions:
|
|||||||
* `d` (delete): delete files/folders
|
* `d` (delete): delete files/folders
|
||||||
* `g` (get): only download files, cannot see folder contents or zip/tar
|
* `g` (get): only download files, cannot see folder contents or zip/tar
|
||||||
* `G` (upget): same as `g` except uploaders get to see their own filekeys (see `fk` in examples below)
|
* `G` (upget): same as `g` except uploaders get to see their own filekeys (see `fk` in examples below)
|
||||||
|
* `a` (admin): can see uploader IPs
|
||||||
|
|
||||||
examples:
|
examples:
|
||||||
* add accounts named u1, u2, u3 with passwords p1, p2, p3: `-a u1:p1 -a u2:p2 -a u3:p3`
|
* add accounts named u1, u2, u3 with passwords p1, p2, p3: `-a u1:p1 -a u2:p2 -a u3:p3`
|
||||||
@@ -980,6 +984,8 @@ set upload rules using volflags, some examples:
|
|||||||
|
|
||||||
* `:c,sz=1k-3m` sets allowed filesize between 1 KiB and 3 MiB inclusive (suffixes: `b`, `k`, `m`, `g`)
|
* `:c,sz=1k-3m` sets allowed filesize between 1 KiB and 3 MiB inclusive (suffixes: `b`, `k`, `m`, `g`)
|
||||||
* `:c,df=4g` block uploads if there would be less than 4 GiB free disk space afterwards
|
* `:c,df=4g` block uploads if there would be less than 4 GiB free disk space afterwards
|
||||||
|
* `:c,vmaxb=1g` block uploads if total volume size would exceed 1 GiB afterwards
|
||||||
|
* `:c,vmaxn=4k` block uploads if volume would contain more than 4096 files afterwards
|
||||||
* `:c,nosub` disallow uploading into subdirectories; goes well with `rotn` and `rotf`:
|
* `:c,nosub` disallow uploading into subdirectories; goes well with `rotn` and `rotf`:
|
||||||
* `:c,rotn=1000,2` moves uploads into subfolders, up to 1000 files in each folder before making a new one, two levels deep (must be at least 1)
|
* `:c,rotn=1000,2` moves uploads into subfolders, up to 1000 files in each folder before making a new one, two levels deep (must be at least 1)
|
||||||
* `:c,rotf=%Y/%m/%d/%H` enforces files to be uploaded into a structure of subfolders according to that date format
|
* `:c,rotf=%Y/%m/%d/%H` enforces files to be uploaded into a structure of subfolders according to that date format
|
||||||
@@ -1123,6 +1129,13 @@ note that this is way more complicated than the new [event hooks](#event-hooks)
|
|||||||
note that it will occupy the parsing threads, so fork anything expensive (or set `kn` to have copyparty fork it for you) -- otoh if you want to intentionally queue/singlethread you can combine it with `--mtag-mt 1`
|
note that it will occupy the parsing threads, so fork anything expensive (or set `kn` to have copyparty fork it for you) -- otoh if you want to intentionally queue/singlethread you can combine it with `--mtag-mt 1`
|
||||||
|
|
||||||
|
|
||||||
|
## handlers
|
||||||
|
|
||||||
|
redefine behavior with plugins ([examples](./bin/handlers/))
|
||||||
|
|
||||||
|
replace 404 and 403 errors with something completely different (that's it for now)
|
||||||
|
|
||||||
|
|
||||||
## hiding from google
|
## hiding from google
|
||||||
|
|
||||||
tell search engines you dont wanna be indexed, either using the good old [robots.txt](https://www.robotstxt.org/robotstxt.html) or through copyparty settings:
|
tell search engines you dont wanna be indexed, either using the good old [robots.txt](https://www.robotstxt.org/robotstxt.html) or through copyparty settings:
|
||||||
@@ -1225,6 +1238,19 @@ the party might be closer than you think
|
|||||||
now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
|
now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
|
||||||
|
|
||||||
|
|
||||||
|
## fedora package
|
||||||
|
|
||||||
|
now [available on copr-pypi](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/) , maintained autonomously -- [track record](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/package/python-copyparty/) seems OK
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dnf copr enable @copr/PyPI
|
||||||
|
dnf install python3-copyparty # just a minimal install, or...
|
||||||
|
dnf install python3-{copyparty,pillow,argon2-cffi,pyftpdlib,pyOpenSSL} ffmpeg-free # with recommended deps
|
||||||
|
```
|
||||||
|
|
||||||
|
this *may* also work on RHEL but [I'm not paying IBM to verify that](https://www.jeffgeerling.com/blog/2023/dear-red-hat-are-you-dumb)
|
||||||
|
|
||||||
|
|
||||||
## nix package
|
## nix package
|
||||||
|
|
||||||
`nix profile install github:9001/copyparty`
|
`nix profile install github:9001/copyparty`
|
||||||
@@ -1511,6 +1537,7 @@ some notes on hardening
|
|||||||
|
|
||||||
* set `--rproxy 0` if your copyparty is directly facing the internet (not through a reverse-proxy)
|
* set `--rproxy 0` if your copyparty is directly facing the internet (not through a reverse-proxy)
|
||||||
* cors doesn't work right otherwise
|
* cors doesn't work right otherwise
|
||||||
|
* if you allow anonymous uploads or otherwise don't trust the contents of a volume, you can prevent XSS with volflag `nohtml`
|
||||||
|
|
||||||
safety profiles:
|
safety profiles:
|
||||||
|
|
||||||
@@ -1566,6 +1593,17 @@ by default, except for `GET` and `HEAD` operations, all requests must either:
|
|||||||
cors can be configured with `--acao` and `--acam`, or the protections entirely disabled with `--allow-csrf`
|
cors can be configured with `--acao` and `--acam`, or the protections entirely disabled with `--allow-csrf`
|
||||||
|
|
||||||
|
|
||||||
|
## password hashing
|
||||||
|
|
||||||
|
you can hash passwords before putting them into config files / providing them as arguments; see `--help-pwhash` for all the details
|
||||||
|
|
||||||
|
`--ah-alg argon2` enables it, and if you have any plaintext passwords then it'll print the hashed versions on startup so you can replace them
|
||||||
|
|
||||||
|
optionally also specify `--ah-cli` to enter an interactive mode where it will hash passwords without ever writing the plaintext ones to disk
|
||||||
|
|
||||||
|
the default configs take about 0.4 sec and 256 MiB RAM to process a new password on a decent laptop
|
||||||
|
|
||||||
|
|
||||||
## https
|
## https
|
||||||
|
|
||||||
both HTTP and HTTPS are accepted by default, but letting a [reverse proxy](#reverse-proxy) handle the https/tls/ssl would be better (probably more secure by default)
|
both HTTP and HTTPS are accepted by default, but letting a [reverse proxy](#reverse-proxy) handle the https/tls/ssl would be better (probably more secure by default)
|
||||||
@@ -1613,6 +1651,8 @@ mandatory deps:
|
|||||||
|
|
||||||
install these to enable bonus features
|
install these to enable bonus features
|
||||||
|
|
||||||
|
enable hashed passwords in config: `argon2-cffi`
|
||||||
|
|
||||||
enable ftp-server:
|
enable ftp-server:
|
||||||
* for just plaintext FTP, `pyftpdlib` (is built into the SFX)
|
* for just plaintext FTP, `pyftpdlib` (is built into the SFX)
|
||||||
* with TLS encryption, `pyftpdlib pyopenssl`
|
* with TLS encryption, `pyftpdlib pyopenssl`
|
||||||
|
|||||||
35
bin/handlers/README.md
Normal file
35
bin/handlers/README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
replace the standard 404 / 403 responses with plugins
|
||||||
|
|
||||||
|
|
||||||
|
# usage
|
||||||
|
|
||||||
|
load plugins either globally with `--on404 ~/dev/copyparty/bin/handlers/sorry.py` or for a specific volume with `:c,on404=~/handlers/sorry.py`
|
||||||
|
|
||||||
|
|
||||||
|
# api
|
||||||
|
|
||||||
|
each plugin must define a `main()` which takes 3 arguments;
|
||||||
|
|
||||||
|
* `cli` is an instance of [copyparty/httpcli.py](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/httpcli.py) (the monstrosity itself)
|
||||||
|
* `vn` is the VFS which overlaps with the requested URL, and
|
||||||
|
* `rem` is the URL remainder below the VFS mountpoint
|
||||||
|
* so `vn.vpath + rem` == `cli.vpath` == original request
|
||||||
|
|
||||||
|
|
||||||
|
# examples
|
||||||
|
|
||||||
|
## on404
|
||||||
|
|
||||||
|
* [sorry.py](answer.py) replies with a custom message instead of the usual 404
|
||||||
|
* [nooo.py](nooo.py) replies with an endless noooooooooooooo
|
||||||
|
* [never404.py](never404.py) 100% guarantee that 404 will never be a thing again as it automatically creates dummy files whenever necessary
|
||||||
|
* [caching-proxy.py](caching-proxy.py) transforms copyparty into a squid/varnish knockoff
|
||||||
|
|
||||||
|
## on403
|
||||||
|
|
||||||
|
* [ip-ok.py](ip-ok.py) disables security checks if client-ip is 1.2.3.4
|
||||||
|
|
||||||
|
|
||||||
|
# notes
|
||||||
|
|
||||||
|
* on403 only works for trivial stuff (basic http access) since I haven't been able to think of any good usecases for it (was just easy to add while doing on404)
|
||||||
36
bin/handlers/caching-proxy.py
Executable file
36
bin/handlers/caching-proxy.py
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
# assume each requested file exists on another webserver and
|
||||||
|
# download + mirror them as they're requested
|
||||||
|
# (basically pretend we're warnish)
|
||||||
|
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from copyparty.httpcli import HttpCli
|
||||||
|
|
||||||
|
|
||||||
|
def main(cli: "HttpCli", vn, rem):
|
||||||
|
url = "https://mirrors.edge.kernel.org/alpine/" + rem
|
||||||
|
abspath = os.path.join(vn.realpath, rem)
|
||||||
|
|
||||||
|
# sneaky trick to preserve a requests-session between downloads
|
||||||
|
# so it doesn't have to spend ages reopening https connections;
|
||||||
|
# luckily we can stash it inside the copyparty client session,
|
||||||
|
# name just has to be definitely unused so "hacapo_req_s" it is
|
||||||
|
req_s = getattr(cli.conn, "hacapo_req_s", None) or requests.Session()
|
||||||
|
setattr(cli.conn, "hacapo_req_s", req_s)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(abspath), exist_ok=True)
|
||||||
|
with req_s.get(url, stream=True, timeout=69) as r:
|
||||||
|
r.raise_for_status()
|
||||||
|
with open(abspath, "wb", 64 * 1024) as f:
|
||||||
|
for buf in r.iter_content(chunk_size=64 * 1024):
|
||||||
|
f.write(buf)
|
||||||
|
except:
|
||||||
|
os.unlink(abspath)
|
||||||
|
return "false"
|
||||||
|
|
||||||
|
return "retry"
|
||||||
6
bin/handlers/ip-ok.py
Executable file
6
bin/handlers/ip-ok.py
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
# disable permission checks and allow access if client-ip is 1.2.3.4
|
||||||
|
|
||||||
|
|
||||||
|
def main(cli, vn, rem):
|
||||||
|
if cli.ip == "1.2.3.4":
|
||||||
|
return "allow"
|
||||||
11
bin/handlers/never404.py
Executable file
11
bin/handlers/never404.py
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
# create a dummy file and let copyparty return it
|
||||||
|
|
||||||
|
|
||||||
|
def main(cli, vn, rem):
|
||||||
|
print("hello", cli.ip)
|
||||||
|
|
||||||
|
abspath = vn.canonical(rem)
|
||||||
|
with open(abspath, "wb") as f:
|
||||||
|
f.write(b"404? not on MY watch!")
|
||||||
|
|
||||||
|
return "retry"
|
||||||
16
bin/handlers/nooo.py
Executable file
16
bin/handlers/nooo.py
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
# reply with an endless "noooooooooooooooooooooooo"
|
||||||
|
|
||||||
|
|
||||||
|
def say_no():
|
||||||
|
yield b"n"
|
||||||
|
while True:
|
||||||
|
yield b"o" * 4096
|
||||||
|
|
||||||
|
|
||||||
|
def main(cli, vn, rem):
|
||||||
|
cli.send_headers(None, 404, "text/plain")
|
||||||
|
|
||||||
|
for chunk in say_no():
|
||||||
|
cli.s.sendall(chunk)
|
||||||
|
|
||||||
|
return "false"
|
||||||
7
bin/handlers/sorry.py
Executable file
7
bin/handlers/sorry.py
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
# sends a custom response instead of the usual 404
|
||||||
|
|
||||||
|
|
||||||
|
def main(cli, vn, rem):
|
||||||
|
msg = f"sorry {cli.ip} but {cli.vpath} doesn't exist"
|
||||||
|
|
||||||
|
return str(cli.reply(msg.encode("utf-8"), 404, "text/plain"))
|
||||||
@@ -24,6 +24,15 @@ these do not have any problematic dependencies at all:
|
|||||||
* also available as an [event hook](../hooks/wget.py)
|
* also available as an [event hook](../hooks/wget.py)
|
||||||
|
|
||||||
|
|
||||||
|
## dangerous plugins
|
||||||
|
|
||||||
|
plugins in this section should only be used with appropriate precautions:
|
||||||
|
|
||||||
|
* [very-bad-idea.py](./very-bad-idea.py) combined with [meadup.js](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/meadup.js) converts copyparty into a janky yet extremely flexible chromecast clone
|
||||||
|
* also adds a virtual keyboard by @steinuil to the basic-upload tab for comfy couch crowd control
|
||||||
|
* anything uploaded through the [android app](https://github.com/9001/party-up) (files or links) are executed on the server, meaning anyone can infect your PC with malware... so protect this with a password and keep it on a LAN!
|
||||||
|
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
|
|
||||||
run [`install-deps.sh`](install-deps.sh) to build/install most dependencies required by these programs (supports windows/linux/macos)
|
run [`install-deps.sh`](install-deps.sh) to build/install most dependencies required by these programs (supports windows/linux/macos)
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
WARNING -- DANGEROUS PLUGIN --
|
||||||
|
if someone is able to upload files to a copyparty which is
|
||||||
|
running this plugin, they can execute malware on your machine
|
||||||
|
so please keep this on a LAN and protect it with a password
|
||||||
|
|
||||||
use copyparty as a chromecast replacement:
|
use copyparty as a chromecast replacement:
|
||||||
* post a URL and it will open in the default browser
|
* post a URL and it will open in the default browser
|
||||||
* upload a file and it will open in the default application
|
* upload a file and it will open in the default application
|
||||||
@@ -10,16 +15,17 @@ use copyparty as a chromecast replacement:
|
|||||||
|
|
||||||
the android app makes it a breeze to post pics and links:
|
the android app makes it a breeze to post pics and links:
|
||||||
https://github.com/9001/party-up/releases
|
https://github.com/9001/party-up/releases
|
||||||
(iOS devices have to rely on the web-UI)
|
|
||||||
|
|
||||||
goes without saying, but this is HELLA DANGEROUS,
|
iOS devices can use the web-UI or the shortcut instead:
|
||||||
GIVES RCE TO ANYONE WHO HAVE UPLOAD PERMISSIONS
|
https://github.com/9001/copyparty#ios-shortcuts
|
||||||
|
|
||||||
example copyparty config to use this:
|
example copyparty config to use this;
|
||||||
--urlform save,get -v.::w:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,c0,bin/mtag/very-bad-idea.py
|
lets the user "kevin" with password "hunter2" use this plugin:
|
||||||
|
-a kevin:hunter2 --urlform save,get -v.::w,kevin:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,c0,bin/mtag/very-bad-idea.py
|
||||||
|
|
||||||
recommended deps:
|
recommended deps:
|
||||||
apt install xdotool libnotify-bin
|
apt install xdotool libnotify-bin mpv
|
||||||
|
python3 -m pip install --user -U streamlink yt-dlp
|
||||||
https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/meadup.js
|
https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/meadup.js
|
||||||
|
|
||||||
and you probably want `twitter-unmute.user.js` from the res folder
|
and you probably want `twitter-unmute.user.js` from the res folder
|
||||||
@@ -63,8 +69,10 @@ set -e
|
|||||||
EOF
|
EOF
|
||||||
chmod 755 /usr/local/bin/chromium-browser
|
chmod 755 /usr/local/bin/chromium-browser
|
||||||
|
|
||||||
# start the server (note: replace `-v.::rw:` with `-v.::w:` to disallow retrieving uploaded stuff)
|
# start the server
|
||||||
cd ~/Downloads; python3 copyparty-sfx.py --urlform save,get -v.::rw:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,very-bad-idea.py
|
# note 1: replace hunter2 with a better password to access the server
|
||||||
|
# note 2: replace `-v.::rw` with `-v.::w` to disallow retrieving uploaded stuff
|
||||||
|
cd ~/Downloads; python3 copyparty-sfx.py -a kevin:hunter2 --urlform save,get -v.::rw,kevin:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,very-bad-idea.py
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -72,11 +80,23 @@ cd ~/Downloads; python3 copyparty-sfx.py --urlform save,get -v.::rw:c,e2d,e2t,mt
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import shutil
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
from urllib.parse import unquote_to_bytes as unquote
|
from urllib.parse import unquote_to_bytes as unquote
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
have_mpv = shutil.which("mpv")
|
||||||
|
have_vlc = shutil.which("vlc")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
if len(sys.argv) > 2 and sys.argv[1] == "x":
|
||||||
|
# invoked on commandline for testing;
|
||||||
|
# python3 very-bad-idea.py x msg=https://youtu.be/dQw4w9WgXcQ
|
||||||
|
txt = " ".join(sys.argv[2:])
|
||||||
|
txt = quote(txt.replace(" ", "+"))
|
||||||
|
return open_post(txt.encode("utf-8"))
|
||||||
|
|
||||||
fp = os.path.abspath(sys.argv[1])
|
fp = os.path.abspath(sys.argv[1])
|
||||||
with open(fp, "rb") as f:
|
with open(fp, "rb") as f:
|
||||||
txt = f.read(4096)
|
txt = f.read(4096)
|
||||||
@@ -92,7 +112,7 @@ def open_post(txt):
|
|||||||
try:
|
try:
|
||||||
k, v = txt.split(" ", 1)
|
k, v = txt.split(" ", 1)
|
||||||
except:
|
except:
|
||||||
open_url(txt)
|
return open_url(txt)
|
||||||
|
|
||||||
if k == "key":
|
if k == "key":
|
||||||
sp.call(["xdotool", "key"] + v.split(" "))
|
sp.call(["xdotool", "key"] + v.split(" "))
|
||||||
@@ -128,6 +148,17 @@ def open_url(txt):
|
|||||||
# else:
|
# else:
|
||||||
# sp.call(["xdotool", "getactivewindow", "windowminimize"]) # minimizes the focused windo
|
# sp.call(["xdotool", "getactivewindow", "windowminimize"]) # minimizes the focused windo
|
||||||
|
|
||||||
|
# mpv is probably smart enough to use streamlink automatically
|
||||||
|
if try_mpv(txt):
|
||||||
|
print("mpv got it")
|
||||||
|
return
|
||||||
|
|
||||||
|
# or maybe streamlink would be a good choice to open this
|
||||||
|
if try_streamlink(txt):
|
||||||
|
print("streamlink got it")
|
||||||
|
return
|
||||||
|
|
||||||
|
# nope,
|
||||||
# close any error messages:
|
# close any error messages:
|
||||||
sp.call(["xdotool", "search", "--name", "Error", "windowclose"])
|
sp.call(["xdotool", "search", "--name", "Error", "windowclose"])
|
||||||
# sp.call(["xdotool", "key", "ctrl+alt+d"]) # doesnt work at all
|
# sp.call(["xdotool", "key", "ctrl+alt+d"]) # doesnt work at all
|
||||||
@@ -136,4 +167,39 @@ def open_url(txt):
|
|||||||
sp.call(["xdg-open", txt])
|
sp.call(["xdg-open", txt])
|
||||||
|
|
||||||
|
|
||||||
|
def try_mpv(url):
|
||||||
|
t0 = time.time()
|
||||||
|
try:
|
||||||
|
print("trying mpv...")
|
||||||
|
sp.check_call(["mpv", "--fs", url])
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
# if it ran for 15 sec it probably succeeded and terminated
|
||||||
|
t = time.time()
|
||||||
|
return t - t0 > 15
|
||||||
|
|
||||||
|
|
||||||
|
def try_streamlink(url):
|
||||||
|
t0 = time.time()
|
||||||
|
try:
|
||||||
|
import streamlink
|
||||||
|
|
||||||
|
print("trying streamlink...")
|
||||||
|
streamlink.Streamlink().resolve_url(url)
|
||||||
|
|
||||||
|
if have_mpv:
|
||||||
|
args = "-m streamlink -p mpv -a --fs"
|
||||||
|
else:
|
||||||
|
args = "-m streamlink"
|
||||||
|
|
||||||
|
cmd = [sys.executable] + args.split() + [url, "best"]
|
||||||
|
t0 = time.time()
|
||||||
|
sp.check_call(cmd)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
# if it ran for 10 sec it probably succeeded and terminated
|
||||||
|
t = time.time()
|
||||||
|
return t - t0 > 10
|
||||||
|
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ in {
|
|||||||
"d" (delete): permanently delete files and folders
|
"d" (delete): permanently delete files and folders
|
||||||
"g" (get): download files, but cannot see folder contents
|
"g" (get): download files, but cannot see folder contents
|
||||||
"G" (upget): "get", but can see filekeys of their own uploads
|
"G" (upget): "get", but can see filekeys of their own uploads
|
||||||
|
"a" (upget): can see uploader IPs
|
||||||
|
|
||||||
For example: "rwmd"
|
For example: "rwmd"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Maintainer: icxes <dev.null@need.moe>
|
# Maintainer: icxes <dev.null@need.moe>
|
||||||
pkgname=copyparty
|
pkgname=copyparty
|
||||||
pkgver="1.7.2"
|
pkgver="1.8.1"
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Portable file sharing hub"
|
pkgdesc="Portable file sharing hub"
|
||||||
arch=("any")
|
arch=("any")
|
||||||
@@ -15,11 +15,12 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
|
|||||||
"libkeyfinder-git: detection of musical keys"
|
"libkeyfinder-git: detection of musical keys"
|
||||||
"qm-vamp-plugins: BPM detection"
|
"qm-vamp-plugins: BPM detection"
|
||||||
"python-pyopenssl: ftps functionality"
|
"python-pyopenssl: ftps functionality"
|
||||||
|
"python-argon2_cffi: hashed passwords in config"
|
||||||
"python-impacket-git: smb support (bad idea)"
|
"python-impacket-git: smb support (bad idea)"
|
||||||
)
|
)
|
||||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||||
backup=("etc/${pkgname}.d/init" )
|
backup=("etc/${pkgname}.d/init" )
|
||||||
sha256sums=("fb261d45ce7cf146a3f620d1e3109eb5c584f8950e61a872e2d92d7b7447bae0")
|
sha256sums=("f43da11ba5d1d5adf99ad642bf068042c46c23d408e7ed17b025065121abab94")
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
{ lib, stdenv, makeWrapper, fetchurl, utillinux, python, jinja2, impacket, pyftpdlib, pyopenssl, pillow, pyvips, ffmpeg, mutagen,
|
{ lib, stdenv, makeWrapper, fetchurl, utillinux, python, jinja2, impacket, pyftpdlib, pyopenssl, argon2-cffi, pillow, pyvips, ffmpeg, mutagen,
|
||||||
|
|
||||||
|
# use argon2id-hashed passwords in config files (sha2 is always available)
|
||||||
|
withHashedPasswords ? true,
|
||||||
|
|
||||||
# create thumbnails with Pillow; faster than FFmpeg / MediaProcessing
|
# create thumbnails with Pillow; faster than FFmpeg / MediaProcessing
|
||||||
withThumbnails ? true,
|
withThumbnails ? true,
|
||||||
@@ -35,6 +38,7 @@ let
|
|||||||
++ lib.optional withFastThumbnails pyvips
|
++ lib.optional withFastThumbnails pyvips
|
||||||
++ lib.optional withMediaProcessing ffmpeg
|
++ lib.optional withMediaProcessing ffmpeg
|
||||||
++ lib.optional withBasicAudioMetadata mutagen
|
++ lib.optional withBasicAudioMetadata mutagen
|
||||||
|
++ lib.optional withHashedPasswords argon2-cffi
|
||||||
);
|
);
|
||||||
in stdenv.mkDerivation {
|
in stdenv.mkDerivation {
|
||||||
pname = "copyparty";
|
pname = "copyparty";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"url": "https://github.com/9001/copyparty/releases/download/v1.7.2/copyparty-sfx.py",
|
"url": "https://github.com/9001/copyparty/releases/download/v1.8.1/copyparty-sfx.py",
|
||||||
"version": "1.7.2",
|
"version": "1.8.1",
|
||||||
"hash": "sha256-h22sRRO/bpydgRVmSVD05ZLuzsUxBCWU3izt9Eg9bf0="
|
"hash": "sha256-0Lf5djrgGAM+wwZP66GtSXkmRnIp3tij8j7cANeoE7o="
|
||||||
}
|
}
|
||||||
@@ -247,13 +247,26 @@ def get_fk_salt(cert_path) -> str:
|
|||||||
ret = f.read().strip()
|
ret = f.read().strip()
|
||||||
except:
|
except:
|
||||||
if os.path.exists(cert_path):
|
if os.path.exists(cert_path):
|
||||||
print("salt from cert")
|
zi = os.path.getmtime(cert_path)
|
||||||
return unicode(os.path.getmtime(cert_path))
|
ret = "{}".format(zi).encode("utf-8")
|
||||||
else:
|
else:
|
||||||
print("salt from os.random")
|
|
||||||
ret = base64.b64encode(os.urandom(18))
|
ret = base64.b64encode(os.urandom(18))
|
||||||
with open(fp, "wb") as f:
|
|
||||||
f.write(ret + b"\n")
|
with open(fp, "wb") as f:
|
||||||
|
f.write(ret + b"\n")
|
||||||
|
|
||||||
|
return ret.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def get_ah_salt() -> str:
|
||||||
|
fp = os.path.join(E.cfg, "ah-salt.txt")
|
||||||
|
try:
|
||||||
|
with open(fp, "rb") as f:
|
||||||
|
ret = f.read().strip()
|
||||||
|
except:
|
||||||
|
ret = base64.b64encode(os.urandom(18))
|
||||||
|
with open(fp, "wb") as f:
|
||||||
|
f.write(ret + b"\n")
|
||||||
|
|
||||||
return ret.decode("utf-8")
|
return ret.decode("utf-8")
|
||||||
|
|
||||||
@@ -479,6 +492,7 @@ def get_sects():
|
|||||||
"d" (delete): permanently delete files and folders
|
"d" (delete): permanently delete files and folders
|
||||||
"g" (get): download files, but cannot see folder contents
|
"g" (get): download files, but cannot see folder contents
|
||||||
"G" (upget): "get", but can see filekeys of their own uploads
|
"G" (upget): "get", but can see filekeys of their own uploads
|
||||||
|
"a" (admin): can see uploader IPs
|
||||||
|
|
||||||
too many volflags to list here, see --help-flags
|
too many volflags to list here, see --help-flags
|
||||||
|
|
||||||
@@ -515,6 +529,50 @@ def get_sects():
|
|||||||
).rstrip()
|
).rstrip()
|
||||||
+ build_flags_desc(),
|
+ build_flags_desc(),
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"handlers",
|
||||||
|
"use plugins to handle certain events",
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
|
usually copyparty returns a \033[33m404\033[0m if a file does not exist, and
|
||||||
|
\033[33m403\033[0m if a user tries to access a file they don't have access to
|
||||||
|
|
||||||
|
you can load a plugin which will be invoked right before this
|
||||||
|
happens, and the plugin can choose to override this behavior
|
||||||
|
|
||||||
|
load the plugin using --args or volflags; for example \033[36m
|
||||||
|
--on404 ~/partyhandlers/not404.py
|
||||||
|
-v .::r:c,on404=~/partyhandlers/not404.py
|
||||||
|
\033[0m
|
||||||
|
the file must define the function \033[35mmain(cli,vn,rem)\033[0m:
|
||||||
|
\033[35mcli\033[0m: the copyparty HttpCli instance
|
||||||
|
\033[35mvn\033[0m: the VFS which overlaps with the requested URL
|
||||||
|
\033[35mrem\033[0m: the remainder of the URL below the VFS mountpoint
|
||||||
|
|
||||||
|
`main` must return a string; one of the following:
|
||||||
|
|
||||||
|
> \033[32m"true"\033[0m: the plugin has responded to the request,
|
||||||
|
and the TCP connection should be kept open
|
||||||
|
|
||||||
|
> \033[32m"false"\033[0m: the plugin has responded to the request,
|
||||||
|
and the TCP connection should be terminated
|
||||||
|
|
||||||
|
> \033[32m"retry"\033[0m: the plugin has done something to resolve the 404
|
||||||
|
situation, and copyparty should reattempt reading the file.
|
||||||
|
if it still fails, a regular 404 will be returned
|
||||||
|
|
||||||
|
> \033[32m"allow"\033[0m: should ignore the insufficient permissions
|
||||||
|
and let the client continue anyways
|
||||||
|
|
||||||
|
> \033[32m""\033[0m: the plugin has not handled the request;
|
||||||
|
try the next plugin or return the usual 404 or 403
|
||||||
|
|
||||||
|
\033[1;35mPS!\033[0m the folder that contains the python file should ideally
|
||||||
|
not contain many other python files, and especially nothing
|
||||||
|
with filenames that overlap with modules used by copyparty
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"hooks",
|
"hooks",
|
||||||
"execute commands before/after various events",
|
"execute commands before/after various events",
|
||||||
@@ -529,6 +587,7 @@ def get_sects():
|
|||||||
\033[36mxbd\033[35m executes CMD before a file delete
|
\033[36mxbd\033[35m executes CMD before a file delete
|
||||||
\033[36mxad\033[35m executes CMD after a file delete
|
\033[36mxad\033[35m executes CMD after a file delete
|
||||||
\033[36mxm\033[35m executes CMD on message
|
\033[36mxm\033[35m executes CMD on message
|
||||||
|
\033[36mxban\033[35m executes CMD if someone gets banned
|
||||||
\033[0m
|
\033[0m
|
||||||
can be defined as --args or volflags; for example \033[36m
|
can be defined as --args or volflags; for example \033[36m
|
||||||
--xau notify-send
|
--xau notify-send
|
||||||
@@ -564,6 +623,9 @@ def get_sects():
|
|||||||
executed program on STDIN instead of as argv arguments, and
|
executed program on STDIN instead of as argv arguments, and
|
||||||
it also includes the wark (file-id/hash) as a json property
|
it also includes the wark (file-id/hash) as a json property
|
||||||
|
|
||||||
|
\033[36mxban\033[0m can be used to overrule / cancel a user ban event;
|
||||||
|
if the program returns 0 (true/OK) then the ban will NOT happen
|
||||||
|
|
||||||
except for \033[36mxm\033[0m, only one hook / one action can run at a time,
|
except for \033[36mxm\033[0m, only one hook / one action can run at a time,
|
||||||
so it's recommended to use the \033[36mf\033[0m flag unless you really need
|
so it's recommended to use the \033[36mf\033[0m flag unless you really need
|
||||||
to wait for the hook to finish before continuing (without \033[36mf\033[0m
|
to wait for the hook to finish before continuing (without \033[36mf\033[0m
|
||||||
@@ -622,6 +684,38 @@ def get_sects():
|
|||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"pwhash",
|
||||||
|
"password hashing",
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
|
when \033[36m--ah-alg\033[0m is not the default [\033[32mnone\033[0m], all account passwords must be hashed
|
||||||
|
|
||||||
|
passwords can be hashed on the commandline with \033[36m--ah-gen\033[0m, but copyparty will also hash and print any passwords that are non-hashed (password which do not start with '+') and then terminate afterwards
|
||||||
|
|
||||||
|
\033[36m--ah-alg\033[0m specifies the hashing algorithm and a list of optional comma-separated arguments:
|
||||||
|
|
||||||
|
\033[36m--ah-alg argon2\033[0m # which is the same as:
|
||||||
|
\033[36m--ah-alg argon2,3,256,4,19\033[0m
|
||||||
|
use argon2id with timecost 3, 256 MiB, 4 threads, version 19 (0x13/v1.3)
|
||||||
|
|
||||||
|
\033[36m--ah-alg scrypt\033[0m # which is the same as:
|
||||||
|
\033[36m--ah-alg scrypt,13,2,8,4\033[0m
|
||||||
|
use scrypt with cost 2**13, 2 iterations, blocksize 8, 4 threads
|
||||||
|
|
||||||
|
\033[36m--ah-alg sha2\033[0m # which is the same as:
|
||||||
|
\033[36m--ah-alg sha2,424242\033[0m
|
||||||
|
use sha2-512 with 424242 iterations
|
||||||
|
|
||||||
|
recommended: \033[32m--ah-alg argon2\033[0m
|
||||||
|
(takes about 0.4 sec and 256M RAM to process a new password)
|
||||||
|
|
||||||
|
argon2 needs python-package argon2-cffi,
|
||||||
|
scrypt needs openssl,
|
||||||
|
sha2 is always available
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -730,6 +824,7 @@ def add_cert(ap, cert_path):
|
|||||||
ap2.add_argument("--crt-exact", action="store_true", help="do not add wildcard entries for each --crt-ns")
|
ap2.add_argument("--crt-exact", action="store_true", help="do not add wildcard entries for each --crt-ns")
|
||||||
ap2.add_argument("--crt-noip", action="store_true", help="do not add autodetected IP addresses into cert")
|
ap2.add_argument("--crt-noip", action="store_true", help="do not add autodetected IP addresses into cert")
|
||||||
ap2.add_argument("--crt-nolo", action="store_true", help="do not add 127.0.0.1 / localhost into cert")
|
ap2.add_argument("--crt-nolo", action="store_true", help="do not add 127.0.0.1 / localhost into cert")
|
||||||
|
ap2.add_argument("--crt-nohn", action="store_true", help="do not add mDNS names / hostname into cert")
|
||||||
ap2.add_argument("--crt-dir", metavar="PATH", default=cert_dir, help="where to save the CA cert")
|
ap2.add_argument("--crt-dir", metavar="PATH", default=cert_dir, help="where to save the CA cert")
|
||||||
ap2.add_argument("--crt-cdays", metavar="D", type=float, default=3650, help="ca-certificate expiration time in days")
|
ap2.add_argument("--crt-cdays", metavar="D", type=float, default=3650, help="ca-certificate expiration time in days")
|
||||||
ap2.add_argument("--crt-sdays", metavar="D", type=float, default=365, help="server-cert expiration time in days")
|
ap2.add_argument("--crt-sdays", metavar="D", type=float, default=365, help="server-cert expiration time in days")
|
||||||
@@ -812,6 +907,13 @@ def add_smb(ap):
|
|||||||
ap2.add_argument("--smbvvv", action="store_true", help="verbosest")
|
ap2.add_argument("--smbvvv", action="store_true", help="verbosest")
|
||||||
|
|
||||||
|
|
||||||
|
def add_handlers(ap):
|
||||||
|
ap2 = ap.add_argument_group('handlers (see --help-handlers)')
|
||||||
|
ap2.add_argument("--on404", metavar="PY", type=u, action="append", help="handle 404s by executing PY file")
|
||||||
|
ap2.add_argument("--on403", metavar="PY", type=u, action="append", help="handle 403s by executing PY file")
|
||||||
|
ap2.add_argument("--hot-handlers", action="store_true", help="reload handlers on each request -- expensive but convenient when hacking on stuff")
|
||||||
|
|
||||||
|
|
||||||
def add_hooks(ap):
|
def add_hooks(ap):
|
||||||
ap2 = ap.add_argument_group('event hooks (see --help-hooks)')
|
ap2 = ap.add_argument_group('event hooks (see --help-hooks)')
|
||||||
ap2.add_argument("--xbu", metavar="CMD", type=u, action="append", help="execute CMD before a file upload starts")
|
ap2.add_argument("--xbu", metavar="CMD", type=u, action="append", help="execute CMD before a file upload starts")
|
||||||
@@ -822,6 +924,7 @@ def add_hooks(ap):
|
|||||||
ap2.add_argument("--xbd", metavar="CMD", type=u, action="append", help="execute CMD before a file delete")
|
ap2.add_argument("--xbd", metavar="CMD", type=u, action="append", help="execute CMD before a file delete")
|
||||||
ap2.add_argument("--xad", metavar="CMD", type=u, action="append", help="execute CMD after a file delete")
|
ap2.add_argument("--xad", metavar="CMD", type=u, action="append", help="execute CMD after a file delete")
|
||||||
ap2.add_argument("--xm", metavar="CMD", type=u, action="append", help="execute CMD on message")
|
ap2.add_argument("--xm", metavar="CMD", type=u, action="append", help="execute CMD on message")
|
||||||
|
ap2.add_argument("--xban", metavar="CMD", type=u, action="append", help="execute CMD if someone gets banned (pw/404)")
|
||||||
|
|
||||||
|
|
||||||
def add_yolo(ap):
|
def add_yolo(ap):
|
||||||
@@ -844,7 +947,7 @@ def add_optouts(ap):
|
|||||||
ap2.add_argument("--no-lifetime", action="store_true", help="disable automatic deletion of uploads after a certain time (as specified by the 'lifetime' volflag)")
|
ap2.add_argument("--no-lifetime", action="store_true", help="disable automatic deletion of uploads after a certain time (as specified by the 'lifetime' volflag)")
|
||||||
|
|
||||||
|
|
||||||
def add_safety(ap, fk_salt):
|
def add_safety(ap):
|
||||||
ap2 = ap.add_argument_group('safety options')
|
ap2 = ap.add_argument_group('safety options')
|
||||||
ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js")
|
ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js")
|
||||||
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --vague-403 --ban-404=50,60,1440 -nih")
|
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --vague-403 --ban-404=50,60,1440 -nih")
|
||||||
@@ -852,8 +955,6 @@ def add_safety(ap, fk_salt):
|
|||||||
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m; example [\033[32m**,*,ln,p,r\033[0m]")
|
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m; example [\033[32m**,*,ln,p,r\033[0m]")
|
||||||
ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)")
|
ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)")
|
||||||
ap2.add_argument("--xdev", action="store_true", help="stay within the filesystem of the volume root; do not descend into other devices (symlink or bind-mount to another HDD, ...) (volflag=xdev)")
|
ap2.add_argument("--xdev", action="store_true", help="stay within the filesystem of the volume root; do not descend into other devices (symlink or bind-mount to another HDD, ...) (volflag=xdev)")
|
||||||
ap2.add_argument("--salt", type=u, default="hunter2", help="up2k file-hash salt; serves no purpose, no reason to change this (but delete all databases if you do)")
|
|
||||||
ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files -- this one DOES matter")
|
|
||||||
ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles")
|
ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles")
|
||||||
ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to make something a dotfile")
|
ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to make something a dotfile")
|
||||||
ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings")
|
ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings")
|
||||||
@@ -870,6 +971,16 @@ def add_safety(ap, fk_salt):
|
|||||||
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 described in --acao)")
|
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 described in --acao)")
|
||||||
|
|
||||||
|
|
||||||
|
def add_salt(ap, fk_salt, ah_salt):
|
||||||
|
ap2 = ap.add_argument_group('salting options')
|
||||||
|
ap2.add_argument("--ah-alg", metavar="ALG", type=u, default="none", help="account-pw hashing algorithm; one of these, best to worst: argon2 scrypt sha2 none (each optionally followed by alg-specific comma-sep. config)")
|
||||||
|
ap2.add_argument("--ah-salt", metavar="SALT", type=u, default=ah_salt, help="account-pw salt; ignored if --ah-alg is none (default)")
|
||||||
|
ap2.add_argument("--ah-gen", metavar="PW", type=u, default="", help="generate hashed password for \033[33mPW\033[0m, or read passwords from STDIN if \033[33mPW\033[0m is [\033[32m-\033[0m]")
|
||||||
|
ap2.add_argument("--ah-cli", action="store_true", help="interactive shell which hashes passwords without ever storing or displaying the original passwords")
|
||||||
|
ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files")
|
||||||
|
ap2.add_argument("--warksalt", metavar="SALT", type=u, default="hunter2", help="up2k file-hash salt; serves no purpose, no reason to change this (but delete all databases if you do)")
|
||||||
|
|
||||||
|
|
||||||
def add_shutdown(ap):
|
def add_shutdown(ap):
|
||||||
ap2 = ap.add_argument_group('shutdown options')
|
ap2 = ap.add_argument_group('shutdown options')
|
||||||
ap2.add_argument("--ign-ebind", action="store_true", help="continue running even if it's impossible to listen on some of the requested endpoints")
|
ap2.add_argument("--ign-ebind", action="store_true", help="continue running even if it's impossible to listen on some of the requested endpoints")
|
||||||
@@ -902,10 +1013,10 @@ def add_thumbnail(ap):
|
|||||||
ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails (volflag=dthumb)")
|
ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails (volflag=dthumb)")
|
||||||
ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails (volflag=dvthumb)")
|
ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails (volflag=dvthumb)")
|
||||||
ap2.add_argument("--no-athumb", action="store_true", help="disable audio thumbnails (spectrograms) (volflag=dathumb)")
|
ap2.add_argument("--no-athumb", action="store_true", help="disable audio thumbnails (spectrograms) (volflag=dathumb)")
|
||||||
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res")
|
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)")
|
||||||
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails")
|
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails")
|
||||||
ap2.add_argument("--th-convt", metavar="SEC", type=int, default=60, help="conversion timeout in seconds")
|
ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60, help="conversion timeout in seconds (volflag=convt)")
|
||||||
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image")
|
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image (volflag=nocrop)")
|
||||||
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference")
|
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", 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-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-webp", action="store_true", help="disable webp output")
|
||||||
@@ -968,7 +1079,7 @@ def add_db_metadata(ap):
|
|||||||
ap2.add_argument("--mtag-vv", action="store_true", help="debug mtp settings and mutagen/ffprobe parsers")
|
ap2.add_argument("--mtag-vv", action="store_true", help="debug mtp settings and mutagen/ffprobe parsers")
|
||||||
ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="add/replace metadata mapping")
|
ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="add/replace metadata mapping")
|
||||||
ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.)",
|
ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.)",
|
||||||
default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash")
|
default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash,up_ip,.up_at")
|
||||||
ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.)",
|
ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.)",
|
||||||
default=".vq,.aq,vc,ac,fmt,res,.fps")
|
default=".vq,.aq,vc,ac,fmt,res,.fps")
|
||||||
ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="read tag M using program BIN to parse the file")
|
ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="read tag M using program BIN to parse the file")
|
||||||
@@ -980,7 +1091,7 @@ def add_ui(ap, retry):
|
|||||||
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language")
|
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language")
|
||||||
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use")
|
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use")
|
||||||
ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed")
|
ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed")
|
||||||
ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files matching REGEX in file list. Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\.(js|css)$\033[0m] (volflag=unlist)")
|
ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files matching REGEX in file list. Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)")
|
||||||
ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable")
|
ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable")
|
||||||
ap2.add_argument("--mpmc", metavar="URL", type=u, default="", help="change the mediaplayer-toggle mouse cursor; URL to a folder with {2..5}.png inside (or disable with [\033[32m.\033[0m])")
|
ap2.add_argument("--mpmc", metavar="URL", type=u, default="", help="change the mediaplayer-toggle mouse cursor; URL to a folder with {2..5}.png inside (or disable with [\033[32m.\033[0m])")
|
||||||
ap2.add_argument("--js-browser", metavar="L", type=u, help="URL to additional JS to include")
|
ap2.add_argument("--js-browser", metavar="L", type=u, help="URL to additional JS to include")
|
||||||
@@ -1030,6 +1141,7 @@ def run_argparse(
|
|||||||
cert_path = os.path.join(E.cfg, "cert.pem")
|
cert_path = os.path.join(E.cfg, "cert.pem")
|
||||||
|
|
||||||
fk_salt = get_fk_salt(cert_path)
|
fk_salt = get_fk_salt(cert_path)
|
||||||
|
ah_salt = get_ah_salt()
|
||||||
|
|
||||||
hcores = min(CORES, 4) # optimal on py3.11 @ r5-4500U
|
hcores = min(CORES, 4) # optimal on py3.11 @ r5-4500U
|
||||||
|
|
||||||
@@ -1053,10 +1165,12 @@ def run_argparse(
|
|||||||
add_ftp(ap)
|
add_ftp(ap)
|
||||||
add_webdav(ap)
|
add_webdav(ap)
|
||||||
add_smb(ap)
|
add_smb(ap)
|
||||||
add_safety(ap, fk_salt)
|
add_safety(ap)
|
||||||
|
add_salt(ap, fk_salt, ah_salt)
|
||||||
add_optouts(ap)
|
add_optouts(ap)
|
||||||
add_shutdown(ap)
|
add_shutdown(ap)
|
||||||
add_yolo(ap)
|
add_yolo(ap)
|
||||||
|
add_handlers(ap)
|
||||||
add_hooks(ap)
|
add_hooks(ap)
|
||||||
add_ui(ap, retry)
|
add_ui(ap, retry)
|
||||||
add_admin(ap)
|
add_admin(ap)
|
||||||
@@ -1138,16 +1252,22 @@ def main(argv: Optional[list[str]] = None) -> None:
|
|||||||
supp = args_from_cfg(v)
|
supp = args_from_cfg(v)
|
||||||
argv.extend(supp)
|
argv.extend(supp)
|
||||||
|
|
||||||
deprecated: list[tuple[str, str]] = []
|
deprecated: list[tuple[str, str]] = [("--salt", "--warksalt")]
|
||||||
for dk, nk in deprecated:
|
for dk, nk in deprecated:
|
||||||
try:
|
idx = -1
|
||||||
idx = argv.index(dk)
|
ov = ""
|
||||||
except:
|
for n, k in enumerate(argv):
|
||||||
|
if k == dk or k.startswith(dk + "="):
|
||||||
|
idx = n
|
||||||
|
if "=" in k:
|
||||||
|
ov = "=" + k.split("=", 1)[1]
|
||||||
|
|
||||||
|
if idx < 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
msg = "\033[1;31mWARNING:\033[0;1m\n {} \033[0;33mwas replaced with\033[0;1m {} \033[0;33mand will be removed\n\033[0m"
|
msg = "\033[1;31mWARNING:\033[0;1m\n {} \033[0;33mwas replaced with\033[0;1m {} \033[0;33mand will be removed\n\033[0m"
|
||||||
lprint(msg.format(dk, nk))
|
lprint(msg.format(dk, nk))
|
||||||
argv[idx] = nk
|
argv[idx] = nk + ov
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
da = len(argv) == 1
|
da = len(argv) == 1
|
||||||
@@ -1198,7 +1318,7 @@ def main(argv: Optional[list[str]] = None) -> None:
|
|||||||
elif not al.no_ansi:
|
elif not al.no_ansi:
|
||||||
al.ansi = VT100
|
al.ansi = VT100
|
||||||
|
|
||||||
if WINDOWS and not al.keep_qem:
|
if WINDOWS and not al.keep_qem and not al.ah_cli:
|
||||||
try:
|
try:
|
||||||
disable_quickedit()
|
disable_quickedit()
|
||||||
except:
|
except:
|
||||||
@@ -1223,11 +1343,9 @@ def main(argv: Optional[list[str]] = None) -> None:
|
|||||||
if re.match("c[^,]", opt):
|
if re.match("c[^,]", opt):
|
||||||
mod = True
|
mod = True
|
||||||
na.append("c," + opt[1:])
|
na.append("c," + opt[1:])
|
||||||
elif re.sub("^[rwmdgG]*", "", opt) and "," not in opt:
|
elif re.sub("^[rwmdgGa]*", "", opt) and "," not in opt:
|
||||||
mod = True
|
mod = True
|
||||||
perm = opt[0]
|
perm = opt[0]
|
||||||
if perm == "a":
|
|
||||||
perm = "rw"
|
|
||||||
na.append(perm + "," + opt[1:])
|
na.append(perm + "," + opt[1:])
|
||||||
else:
|
else:
|
||||||
na.append(opt)
|
na.append(opt)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
VERSION = (1, 7, 4)
|
VERSION = (1, 8, 2)
|
||||||
CODENAME = "unlinked"
|
CODENAME = "argon"
|
||||||
BUILD_DT = (2023, 6, 11)
|
BUILD_DT = (2023, 7, 14)
|
||||||
|
|
||||||
S_VERSION = ".".join(map(str, VERSION))
|
S_VERSION = ".".join(map(str, VERSION))
|
||||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from datetime import datetime
|
|||||||
from .__init__ import ANYWIN, TYPE_CHECKING, WINDOWS
|
from .__init__ import ANYWIN, TYPE_CHECKING, WINDOWS
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap
|
from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap
|
||||||
|
from .pwhash import PWHash
|
||||||
from .util import (
|
from .util import (
|
||||||
IMPLICATIONS,
|
IMPLICATIONS,
|
||||||
META_NOBOTS,
|
META_NOBOTS,
|
||||||
@@ -40,7 +41,10 @@ if True: # pylint: disable=using-constant-test
|
|||||||
from .util import NamedLogger, RootLogger
|
from .util import NamedLogger, RootLogger
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
pass
|
from .broker_mp import BrokerMp
|
||||||
|
from .broker_thr import BrokerThr
|
||||||
|
from .broker_util import BrokerCli
|
||||||
|
|
||||||
# Vflags: TypeAlias = dict[str, str | bool | float | list[str]]
|
# Vflags: TypeAlias = dict[str, str | bool | float | list[str]]
|
||||||
# Vflags: TypeAlias = dict[str, Any]
|
# Vflags: TypeAlias = dict[str, Any]
|
||||||
# Mflags: TypeAlias = dict[str, Vflags]
|
# Mflags: TypeAlias = dict[str, Vflags]
|
||||||
@@ -58,6 +62,7 @@ class AXS(object):
|
|||||||
udel: Optional[Union[list[str], set[str]]] = None,
|
udel: Optional[Union[list[str], set[str]]] = None,
|
||||||
uget: Optional[Union[list[str], set[str]]] = None,
|
uget: Optional[Union[list[str], set[str]]] = None,
|
||||||
upget: Optional[Union[list[str], set[str]]] = None,
|
upget: Optional[Union[list[str], set[str]]] = None,
|
||||||
|
uadmin: Optional[Union[list[str], set[str]]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.uread: set[str] = set(uread or [])
|
self.uread: set[str] = set(uread or [])
|
||||||
self.uwrite: set[str] = set(uwrite or [])
|
self.uwrite: set[str] = set(uwrite or [])
|
||||||
@@ -65,14 +70,11 @@ class AXS(object):
|
|||||||
self.udel: set[str] = set(udel or [])
|
self.udel: set[str] = set(udel or [])
|
||||||
self.uget: set[str] = set(uget or [])
|
self.uget: set[str] = set(uget or [])
|
||||||
self.upget: set[str] = set(upget or [])
|
self.upget: set[str] = set(upget or [])
|
||||||
|
self.uadmin: set[str] = set(uadmin or [])
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "AXS(%s)" % (
|
ks = "uread uwrite umove udel uget upget uadmin".split()
|
||||||
", ".join(
|
return "AXS(%s)" % (", ".join("%s=%r" % (k, self.__dict__[k]) for k in ks),)
|
||||||
"%s=%r" % (k, self.__dict__[k])
|
|
||||||
for k in "uread uwrite umove udel uget upget".split()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Lim(object):
|
class Lim(object):
|
||||||
@@ -90,6 +92,8 @@ class Lim(object):
|
|||||||
self.dfl = 0 # free disk space limit
|
self.dfl = 0 # free disk space limit
|
||||||
self.dft = 0 # last-measured time
|
self.dft = 0 # last-measured time
|
||||||
self.dfv = 0 # currently free
|
self.dfv = 0 # currently free
|
||||||
|
self.vbmax = 0 # volume bytes max
|
||||||
|
self.vnmax = 0 # volume max num files
|
||||||
|
|
||||||
self.smin = 0 # filesize min
|
self.smin = 0 # filesize min
|
||||||
self.smax = 0 # filesize max
|
self.smax = 0 # filesize max
|
||||||
@@ -119,8 +123,11 @@ class Lim(object):
|
|||||||
ip: str,
|
ip: str,
|
||||||
rem: str,
|
rem: str,
|
||||||
sz: int,
|
sz: int,
|
||||||
|
ptop: str,
|
||||||
abspath: str,
|
abspath: str,
|
||||||
|
broker: Optional[Union["BrokerCli", "BrokerMp", "BrokerThr"]] = None,
|
||||||
reg: Optional[dict[str, dict[str, Any]]] = None,
|
reg: Optional[dict[str, dict[str, Any]]] = None,
|
||||||
|
volgetter: str = "up2k.get_volsize",
|
||||||
) -> tuple[str, str]:
|
) -> tuple[str, str]:
|
||||||
if reg is not None and self.reg is None:
|
if reg is not None and self.reg is None:
|
||||||
self.reg = reg
|
self.reg = reg
|
||||||
@@ -131,6 +138,7 @@ class Lim(object):
|
|||||||
self.chk_rem(rem)
|
self.chk_rem(rem)
|
||||||
if sz != -1:
|
if sz != -1:
|
||||||
self.chk_sz(sz)
|
self.chk_sz(sz)
|
||||||
|
self.chk_vsz(broker, ptop, sz, volgetter)
|
||||||
self.chk_df(abspath, sz) # side effects; keep last-ish
|
self.chk_df(abspath, sz) # side effects; keep last-ish
|
||||||
|
|
||||||
ap2, vp2 = self.rot(abspath)
|
ap2, vp2 = self.rot(abspath)
|
||||||
@@ -146,6 +154,25 @@ class Lim(object):
|
|||||||
if self.smax and sz > self.smax:
|
if self.smax and sz > self.smax:
|
||||||
raise Pebkac(400, "file too big")
|
raise Pebkac(400, "file too big")
|
||||||
|
|
||||||
|
def chk_vsz(
|
||||||
|
self,
|
||||||
|
broker: Optional[Union["BrokerCli", "BrokerMp", "BrokerThr"]],
|
||||||
|
ptop: str,
|
||||||
|
sz: int,
|
||||||
|
volgetter: str = "up2k.get_volsize",
|
||||||
|
) -> None:
|
||||||
|
if not broker or not self.vbmax + self.vnmax:
|
||||||
|
return
|
||||||
|
|
||||||
|
x = broker.ask(volgetter, ptop)
|
||||||
|
nbytes, nfiles = x.get()
|
||||||
|
|
||||||
|
if self.vbmax and self.vbmax < nbytes + sz:
|
||||||
|
raise Pebkac(400, "volume has exceeded max size")
|
||||||
|
|
||||||
|
if self.vnmax and self.vnmax < nfiles + 1:
|
||||||
|
raise Pebkac(400, "volume has exceeded max num.files")
|
||||||
|
|
||||||
def chk_df(self, abspath: str, sz: int, already_written: bool = False) -> None:
|
def chk_df(self, abspath: str, sz: int, already_written: bool = False) -> None:
|
||||||
if not self.dfl:
|
if not self.dfl:
|
||||||
return
|
return
|
||||||
@@ -266,7 +293,7 @@ class Lim(object):
|
|||||||
|
|
||||||
self.bupc[ip] = mark
|
self.bupc[ip] = mark
|
||||||
if mark >= self.bmax:
|
if mark >= self.bmax:
|
||||||
raise Pebkac(429, "ingress saturated")
|
raise Pebkac(429, "upload size limit exceeded")
|
||||||
|
|
||||||
|
|
||||||
class VFS(object):
|
class VFS(object):
|
||||||
@@ -406,8 +433,8 @@ class VFS(object):
|
|||||||
|
|
||||||
def can_access(
|
def can_access(
|
||||||
self, vpath: str, uname: str
|
self, vpath: str, uname: str
|
||||||
) -> tuple[bool, bool, bool, bool, bool, bool]:
|
) -> tuple[bool, bool, bool, bool, bool, bool, bool]:
|
||||||
"""can Read,Write,Move,Delete,Get,Upget"""
|
"""can Read,Write,Move,Delete,Get,Upget,Admin"""
|
||||||
if vpath:
|
if vpath:
|
||||||
vn, _ = self._find(undot(vpath))
|
vn, _ = self._find(undot(vpath))
|
||||||
else:
|
else:
|
||||||
@@ -421,6 +448,7 @@ class VFS(object):
|
|||||||
uname in c.udel or "*" in c.udel,
|
uname in c.udel or "*" in c.udel,
|
||||||
uname in c.uget or "*" in c.uget,
|
uname in c.uget or "*" in c.uget,
|
||||||
uname in c.upget or "*" in c.upget,
|
uname in c.upget or "*" in c.upget,
|
||||||
|
uname in c.uadmin or "*" in c.uadmin,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(
|
def get(
|
||||||
@@ -729,6 +757,7 @@ class AuthSrv(object):
|
|||||||
warn_anonwrite: bool = True,
|
warn_anonwrite: bool = True,
|
||||||
dargs: Optional[argparse.Namespace] = None,
|
dargs: Optional[argparse.Namespace] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
self.ah = PWHash(args)
|
||||||
self.args = args
|
self.args = args
|
||||||
self.dargs = dargs or args
|
self.dargs = dargs or args
|
||||||
self.log_func = log_func
|
self.log_func = log_func
|
||||||
@@ -914,7 +943,7 @@ class AuthSrv(object):
|
|||||||
try:
|
try:
|
||||||
self._l(ln, 5, "volume access config:")
|
self._l(ln, 5, "volume access config:")
|
||||||
sk, sv = ln.split(":")
|
sk, sv = ln.split(":")
|
||||||
if re.sub("[rwmdgG]", "", sk) or not sk:
|
if re.sub("[rwmdgGa]", "", sk) or not sk:
|
||||||
err = "invalid accs permissions list; "
|
err = "invalid accs permissions list; "
|
||||||
raise Exception(err)
|
raise Exception(err)
|
||||||
if " " in re.sub(", *", "", sv).strip():
|
if " " in re.sub(", *", "", sv).strip():
|
||||||
@@ -923,7 +952,7 @@ class AuthSrv(object):
|
|||||||
self._read_vol_str(sk, sv.replace(" ", ""), daxs[vp], mflags[vp])
|
self._read_vol_str(sk, sv.replace(" ", ""), daxs[vp], mflags[vp])
|
||||||
continue
|
continue
|
||||||
except:
|
except:
|
||||||
err += "accs entries must be 'rwmdgG: user1, user2, ...'"
|
err += "accs entries must be 'rwmdgGa: user1, user2, ...'"
|
||||||
raise Exception(err)
|
raise Exception(err)
|
||||||
|
|
||||||
if cat == catf:
|
if cat == catf:
|
||||||
@@ -959,7 +988,7 @@ class AuthSrv(object):
|
|||||||
def _read_vol_str(
|
def _read_vol_str(
|
||||||
self, lvl: str, uname: str, axs: AXS, flags: dict[str, Any]
|
self, lvl: str, uname: str, axs: AXS, flags: dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
if lvl.strip("crwmdgG"):
|
if lvl.strip("crwmdgGa"):
|
||||||
raise Exception("invalid volflag: {},{}".format(lvl, uname))
|
raise Exception("invalid volflag: {},{}".format(lvl, uname))
|
||||||
|
|
||||||
if lvl == "c":
|
if lvl == "c":
|
||||||
@@ -991,6 +1020,7 @@ class AuthSrv(object):
|
|||||||
("g", axs.uget),
|
("g", axs.uget),
|
||||||
("G", axs.uget),
|
("G", axs.uget),
|
||||||
("G", axs.upget),
|
("G", axs.upget),
|
||||||
|
("a", axs.uadmin),
|
||||||
]: # b bb bbb
|
]: # b bb bbb
|
||||||
if ch in lvl:
|
if ch in lvl:
|
||||||
if un == "*":
|
if un == "*":
|
||||||
@@ -1017,7 +1047,8 @@ class AuthSrv(object):
|
|||||||
flags[name] = True
|
flags[name] = True
|
||||||
return
|
return
|
||||||
|
|
||||||
if name not in "mtp xbu xau xiu xbr xar xbd xad xm".split():
|
zs = "mtp on403 on404 xbu xau xiu xbr xar xbd xad xm xban"
|
||||||
|
if name not in zs.split():
|
||||||
if value is True:
|
if value is True:
|
||||||
t = "└─add volflag [{}] = {} ({})"
|
t = "└─add volflag [{}] = {} ({})"
|
||||||
else:
|
else:
|
||||||
@@ -1062,7 +1093,7 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
if self.args.v:
|
if self.args.v:
|
||||||
# list of src:dst:permset:permset:...
|
# list of src:dst:permset:permset:...
|
||||||
# permset is <rwmdgG>[,username][,username] or <c>,<flag>[=args]
|
# permset is <rwmdgGa>[,username][,username] or <c>,<flag>[=args]
|
||||||
for v_str in self.args.v:
|
for v_str in self.args.v:
|
||||||
m = re_vol.match(v_str)
|
m = re_vol.match(v_str)
|
||||||
if not m:
|
if not m:
|
||||||
@@ -1106,6 +1137,8 @@ class AuthSrv(object):
|
|||||||
self.log("\n{0}\n{1}{0}".format(t, "\n".join(slns)))
|
self.log("\n{0}\n{1}{0}".format(t, "\n".join(slns)))
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
self.setup_pwhash(acct)
|
||||||
|
|
||||||
# case-insensitive; normalize
|
# case-insensitive; normalize
|
||||||
if WINDOWS:
|
if WINDOWS:
|
||||||
cased = {}
|
cased = {}
|
||||||
@@ -1164,7 +1197,15 @@ class AuthSrv(object):
|
|||||||
all_users = {}
|
all_users = {}
|
||||||
missing_users = {}
|
missing_users = {}
|
||||||
for axs in daxs.values():
|
for axs in daxs.values():
|
||||||
for d in [axs.uread, axs.uwrite, axs.umove, axs.udel, axs.uget, axs.upget]:
|
for d in [
|
||||||
|
axs.uread,
|
||||||
|
axs.uwrite,
|
||||||
|
axs.umove,
|
||||||
|
axs.udel,
|
||||||
|
axs.uget,
|
||||||
|
axs.upget,
|
||||||
|
axs.uadmin,
|
||||||
|
]:
|
||||||
for usr in d:
|
for usr in d:
|
||||||
all_users[usr] = 1
|
all_users[usr] = 1
|
||||||
if usr != "*" and usr not in acct:
|
if usr != "*" and usr not in acct:
|
||||||
@@ -1290,6 +1331,16 @@ class AuthSrv(object):
|
|||||||
use = True
|
use = True
|
||||||
lim.bmax, lim.bwin = [unhumanize(x) for x in zs.split(",")]
|
lim.bmax, lim.bwin = [unhumanize(x) for x in zs.split(",")]
|
||||||
|
|
||||||
|
zs = vol.flags.get("vmaxb")
|
||||||
|
if zs:
|
||||||
|
use = True
|
||||||
|
lim.vbmax = unhumanize(zs)
|
||||||
|
|
||||||
|
zs = vol.flags.get("vmaxn")
|
||||||
|
if zs:
|
||||||
|
use = True
|
||||||
|
lim.vnmax = unhumanize(zs)
|
||||||
|
|
||||||
if use:
|
if use:
|
||||||
vol.lim = lim
|
vol.lim = lim
|
||||||
|
|
||||||
@@ -1378,6 +1429,10 @@ class AuthSrv(object):
|
|||||||
if k in vol.flags:
|
if k in vol.flags:
|
||||||
vol.flags[k] = int(vol.flags[k])
|
vol.flags[k] = int(vol.flags[k])
|
||||||
|
|
||||||
|
for k in ("convt",):
|
||||||
|
if k in vol.flags:
|
||||||
|
vol.flags[k] = float(vol.flags[k])
|
||||||
|
|
||||||
for k1, k2 in IMPLICATIONS:
|
for k1, k2 in IMPLICATIONS:
|
||||||
if k1 in vol.flags:
|
if k1 in vol.flags:
|
||||||
vol.flags[k2] = True
|
vol.flags[k2] = True
|
||||||
@@ -1403,8 +1458,8 @@ class AuthSrv(object):
|
|||||||
vol.flags["mth"] = self.args.mth
|
vol.flags["mth"] = self.args.mth
|
||||||
|
|
||||||
# append additive args from argv to volflags
|
# append additive args from argv to volflags
|
||||||
hooks = "xbu xau xiu xbr xar xbd xad xm".split()
|
hooks = "xbu xau xiu xbr xar xbd xad xm xban".split()
|
||||||
for name in ["mtp"] + hooks:
|
for name in "mtp on404 on403".split() + hooks:
|
||||||
self._read_volflag(vol.flags, name, getattr(self.args, name), True)
|
self._read_volflag(vol.flags, name, getattr(self.args, name), True)
|
||||||
|
|
||||||
for hn in hooks:
|
for hn in hooks:
|
||||||
@@ -1426,6 +1481,10 @@ class AuthSrv(object):
|
|||||||
hfs = [x for x in hfs if x != "f"]
|
hfs = [x for x in hfs if x != "f"]
|
||||||
ocmd = ",".join(hfs + [cmd])
|
ocmd = ",".join(hfs + [cmd])
|
||||||
|
|
||||||
|
if "c" not in hfs and "f" not in hfs and hn == "xban":
|
||||||
|
hfs = ["c"] + hfs
|
||||||
|
ocmd = ",".join(hfs + [cmd])
|
||||||
|
|
||||||
ncmds.append(ocmd)
|
ncmds.append(ocmd)
|
||||||
vol.flags[hn] = ncmds
|
vol.flags[hn] = ncmds
|
||||||
|
|
||||||
@@ -1536,6 +1595,10 @@ class AuthSrv(object):
|
|||||||
self.log(t, 1)
|
self.log(t, 1)
|
||||||
errors = True
|
errors = True
|
||||||
|
|
||||||
|
if self.args.smb and self.ah.on and acct:
|
||||||
|
self.log("--smb can only be used when --ah-alg is none", 1)
|
||||||
|
errors = True
|
||||||
|
|
||||||
for vol in vfs.all_vols.values():
|
for vol in vfs.all_vols.values():
|
||||||
for k in list(vol.flags.keys()):
|
for k in list(vol.flags.keys()):
|
||||||
if re.match("^-[^-]+$", k):
|
if re.match("^-[^-]+$", k):
|
||||||
@@ -1561,6 +1624,7 @@ class AuthSrv(object):
|
|||||||
["delete", "udel"],
|
["delete", "udel"],
|
||||||
[" get", "uget"],
|
[" get", "uget"],
|
||||||
[" upget", "upget"],
|
[" upget", "upget"],
|
||||||
|
["uadmin", "uadmin"],
|
||||||
]:
|
]:
|
||||||
u = list(sorted(getattr(zv.axs, attr)))
|
u = list(sorted(getattr(zv.axs, attr)))
|
||||||
u = ", ".join("\033[35meverybody\033[0m" if x == "*" else x for x in u)
|
u = ", ".join("\033[35meverybody\033[0m" if x == "*" else x for x in u)
|
||||||
@@ -1605,7 +1669,51 @@ class AuthSrv(object):
|
|||||||
self.re_pwd = None
|
self.re_pwd = None
|
||||||
pwds = [re.escape(x) for x in self.iacct.keys()]
|
pwds = [re.escape(x) for x in self.iacct.keys()]
|
||||||
if pwds:
|
if pwds:
|
||||||
self.re_pwd = re.compile("=(" + "|".join(pwds) + ")([]&; ]|$)")
|
if self.ah.on:
|
||||||
|
zs = r"(\[H\] pw:.*|[?&]pw=)([^&]+)"
|
||||||
|
else:
|
||||||
|
zs = r"(\[H\] pw:.*|=)(" + "|".join(pwds) + r")([]&; ]|$)"
|
||||||
|
|
||||||
|
self.re_pwd = re.compile(zs)
|
||||||
|
|
||||||
|
def setup_pwhash(self, acct: dict[str, str]) -> None:
|
||||||
|
self.ah = PWHash(self.args)
|
||||||
|
if not self.ah.on:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.args.ah_cli:
|
||||||
|
self.ah.cli()
|
||||||
|
sys.exit()
|
||||||
|
elif self.args.ah_gen == "-":
|
||||||
|
self.ah.stdin()
|
||||||
|
sys.exit()
|
||||||
|
elif self.args.ah_gen:
|
||||||
|
print(self.ah.hash(self.args.ah_gen))
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
if not acct:
|
||||||
|
return
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
for uname, pw in list(acct.items())[:]:
|
||||||
|
if pw.startswith("+") and len(pw) == 33:
|
||||||
|
continue
|
||||||
|
|
||||||
|
changed = True
|
||||||
|
hpw = self.ah.hash(pw)
|
||||||
|
acct[uname] = hpw
|
||||||
|
t = "hashed password for account {}: {}"
|
||||||
|
self.log(t.format(uname, hpw), 3)
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
return
|
||||||
|
|
||||||
|
lns = []
|
||||||
|
for uname, pw in acct.items():
|
||||||
|
lns.append(" {}: {}".format(uname, pw))
|
||||||
|
|
||||||
|
t = "please use the following hashed passwords in your config:\n{}"
|
||||||
|
self.log(t.format("\n".join(lns)), 3)
|
||||||
|
|
||||||
def chk_sqlite_threadsafe(self) -> str:
|
def chk_sqlite_threadsafe(self) -> str:
|
||||||
v = SQLITE_VER[-1:]
|
v = SQLITE_VER[-1:]
|
||||||
@@ -1662,10 +1770,19 @@ class AuthSrv(object):
|
|||||||
raise Exception("volume not found: " + zs)
|
raise Exception("volume not found: " + zs)
|
||||||
|
|
||||||
self.log(str({"users": users, "vols": vols, "flags": flags}))
|
self.log(str({"users": users, "vols": vols, "flags": flags}))
|
||||||
t = "/{}: read({}) write({}) move({}) del({}) get({}) upget({})"
|
t = "/{}: read({}) write({}) move({}) del({}) get({}) upget({}) uadmin({})"
|
||||||
for k, zv in self.vfs.all_vols.items():
|
for k, zv in self.vfs.all_vols.items():
|
||||||
vc = zv.axs
|
vc = zv.axs
|
||||||
vs = [k, vc.uread, vc.uwrite, vc.umove, vc.udel, vc.uget, vc.upget]
|
vs = [
|
||||||
|
k,
|
||||||
|
vc.uread,
|
||||||
|
vc.uwrite,
|
||||||
|
vc.umove,
|
||||||
|
vc.udel,
|
||||||
|
vc.uget,
|
||||||
|
vc.upget,
|
||||||
|
vc.uadmin,
|
||||||
|
]
|
||||||
self.log(t.format(*vs))
|
self.log(t.format(*vs))
|
||||||
|
|
||||||
flag_v = "v" in flags
|
flag_v = "v" in flags
|
||||||
@@ -1745,7 +1862,8 @@ class AuthSrv(object):
|
|||||||
]
|
]
|
||||||
|
|
||||||
csv = set("i p".split())
|
csv = set("i p".split())
|
||||||
lst = set("c ihead mtm mtp xad xar xau xiu xbd xbr xbu xm".split())
|
zs = "c ihead mtm mtp on403 on404 xad xar xau xiu xban xbd xbr xbu xm"
|
||||||
|
lst = set(zs.split())
|
||||||
askip = set("a v c vc cgen theme".split())
|
askip = set("a v c vc cgen theme".split())
|
||||||
|
|
||||||
# keymap from argv to vflag
|
# keymap from argv to vflag
|
||||||
@@ -1804,6 +1922,7 @@ class AuthSrv(object):
|
|||||||
"d": "udel",
|
"d": "udel",
|
||||||
"g": "uget",
|
"g": "uget",
|
||||||
"G": "upget",
|
"G": "upget",
|
||||||
|
"a": "uadmin",
|
||||||
}
|
}
|
||||||
users = {}
|
users = {}
|
||||||
for pkey in perms.values():
|
for pkey in perms.values():
|
||||||
@@ -2000,7 +2119,7 @@ def upgrade_cfg_fmt(
|
|||||||
else:
|
else:
|
||||||
sn = sn.replace(",", ", ")
|
sn = sn.replace(",", ", ")
|
||||||
ret.append(" " + sn)
|
ret.append(" " + sn)
|
||||||
elif sn[:1] in "rwmdgG":
|
elif sn[:1] in "rwmdgGa":
|
||||||
if cat != catx:
|
if cat != catx:
|
||||||
cat = catx
|
cat = catx
|
||||||
ret.append(cat)
|
ret.append(cat)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import queue
|
|||||||
|
|
||||||
from .__init__ import CORES, TYPE_CHECKING
|
from .__init__ import CORES, TYPE_CHECKING
|
||||||
from .broker_mpw import MpWorker
|
from .broker_mpw import MpWorker
|
||||||
from .broker_util import try_exec
|
from .broker_util import ExceptionalQueue, try_exec
|
||||||
from .util import Daemon, mp
|
from .util import Daemon, mp
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -107,6 +107,19 @@ class BrokerMp(object):
|
|||||||
if retq_id:
|
if retq_id:
|
||||||
proc.q_pend.put((retq_id, "retq", rv))
|
proc.q_pend.put((retq_id, "retq", rv))
|
||||||
|
|
||||||
|
def ask(self, dest: str, *args: Any) -> ExceptionalQueue:
|
||||||
|
|
||||||
|
# new non-ipc invoking managed service in hub
|
||||||
|
obj = self.hub
|
||||||
|
for node in dest.split("."):
|
||||||
|
obj = getattr(obj, node)
|
||||||
|
|
||||||
|
rv = try_exec(True, obj, *args)
|
||||||
|
|
||||||
|
retq = ExceptionalQueue(1)
|
||||||
|
retq.put(rv)
|
||||||
|
return retq
|
||||||
|
|
||||||
def say(self, dest: str, *args: Any) -> None:
|
def say(self, dest: str, *args: Any) -> None:
|
||||||
"""
|
"""
|
||||||
send message to non-hub component in other process,
|
send message to non-hub component in other process,
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import os
|
|
||||||
import errno
|
|
||||||
import time
|
|
||||||
import json
|
|
||||||
import shutil
|
|
||||||
import filecmp
|
|
||||||
import calendar
|
import calendar
|
||||||
|
import errno
|
||||||
|
import filecmp
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
|
|
||||||
from .util import runcmd, Netdev
|
from .util import Netdev, runcmd
|
||||||
|
|
||||||
|
|
||||||
HAVE_CFSSL = True
|
HAVE_CFSSL = True
|
||||||
|
|
||||||
|
if True: # pylint: disable=using-constant-test
|
||||||
|
from .util import RootLogger
|
||||||
|
|
||||||
|
|
||||||
def ensure_cert(log: "RootLogger", args) -> None:
|
def ensure_cert(log: "RootLogger", args) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -119,6 +121,9 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
|||||||
names.append(ip.split("/")[0])
|
names.append(ip.split("/")[0])
|
||||||
if args.crt_nolo:
|
if args.crt_nolo:
|
||||||
names = [x for x in names if x not in ("localhost", "127.0.0.1", "::1")]
|
names = [x for x in names if x not in ("localhost", "127.0.0.1", "::1")]
|
||||||
|
if not args.crt_nohn:
|
||||||
|
names.append(args.name)
|
||||||
|
names.append(args.name + ".local")
|
||||||
if not names:
|
if not names:
|
||||||
names = ["127.0.0.1"]
|
names = ["127.0.0.1"]
|
||||||
if "127.0.0.1" in names or "::1" in names:
|
if "127.0.0.1" in names or "::1" in names:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ def vf_bmap() -> dict[str, str]:
|
|||||||
"no_dedup": "copydupes",
|
"no_dedup": "copydupes",
|
||||||
"no_dupe": "nodupe",
|
"no_dupe": "nodupe",
|
||||||
"no_forget": "noforget",
|
"no_forget": "noforget",
|
||||||
|
"th_no_crop": "nocrop",
|
||||||
"dav_auth": "davauth",
|
"dav_auth": "davauth",
|
||||||
"dav_rt": "davrt",
|
"dav_rt": "davrt",
|
||||||
}
|
}
|
||||||
@@ -40,8 +41,8 @@ def vf_bmap() -> dict[str, str]:
|
|||||||
|
|
||||||
def vf_vmap() -> dict[str, str]:
|
def vf_vmap() -> dict[str, str]:
|
||||||
"""argv-to-volflag: simple values"""
|
"""argv-to-volflag: simple values"""
|
||||||
ret = {}
|
ret = {"th_convt": "convt", "th_size": "thsize"}
|
||||||
for k in ("lg_sbf", "md_sbf", "unlist"):
|
for k in ("dbd", "lg_sbf", "md_sbf", "nrand", "unlist"):
|
||||||
ret[k] = k
|
ret[k] = k
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ def vf_vmap() -> dict[str, str]:
|
|||||||
def vf_cmap() -> dict[str, str]:
|
def vf_cmap() -> dict[str, str]:
|
||||||
"""argv-to-volflag: complex/lists"""
|
"""argv-to-volflag: complex/lists"""
|
||||||
ret = {}
|
ret = {}
|
||||||
for k in ("dbd", "html_head", "mte", "mth", "nrand"):
|
for k in ("html_head", "mte", "mth"):
|
||||||
ret[k] = k
|
ret[k] = k
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -78,7 +79,9 @@ flagcats = {
|
|||||||
},
|
},
|
||||||
"upload rules": {
|
"upload rules": {
|
||||||
"maxn=250,600": "max 250 uploads over 15min",
|
"maxn=250,600": "max 250 uploads over 15min",
|
||||||
"maxb=1g,300": "max 1 GiB over 5min (suffixes: b, k, m, g)",
|
"maxb=1g,300": "max 1 GiB over 5min (suffixes: b, k, m, g, t)",
|
||||||
|
"vmaxb=1g": "total volume size max 1 GiB (suffixes: b, k, m, g, t)",
|
||||||
|
"vmaxn=4k": "max 4096 files in volume (suffixes: b, k, m, g, t)",
|
||||||
"rand": "force randomized filenames, 9 chars long by default",
|
"rand": "force randomized filenames, 9 chars long by default",
|
||||||
"nrand=N": "randomized filenames are N chars long",
|
"nrand=N": "randomized filenames are N chars long",
|
||||||
"sz=1k-3m": "allow filesizes between 1 KiB and 3MiB",
|
"sz=1k-3m": "allow filesizes between 1 KiB and 3MiB",
|
||||||
@@ -122,6 +125,13 @@ flagcats = {
|
|||||||
"dvthumb": "disables video thumbnails",
|
"dvthumb": "disables video thumbnails",
|
||||||
"dathumb": "disables audio thumbnails (spectrograms)",
|
"dathumb": "disables audio thumbnails (spectrograms)",
|
||||||
"dithumb": "disables image thumbnails",
|
"dithumb": "disables image thumbnails",
|
||||||
|
"thsize": "thumbnail res; WxH",
|
||||||
|
"nocrop": "disable center-cropping",
|
||||||
|
"convt": "conversion timeout in seconds",
|
||||||
|
},
|
||||||
|
"handlers\n(better explained in --help-handlers)": {
|
||||||
|
"on404=PY": "handle 404s by executing PY file",
|
||||||
|
"on403=PY": "handle 403s by executing PY file",
|
||||||
},
|
},
|
||||||
"event hooks\n(better explained in --help-hooks)": {
|
"event hooks\n(better explained in --help-hooks)": {
|
||||||
"xbu=CMD": "execute CMD before a file upload starts",
|
"xbu=CMD": "execute CMD before a file upload starts",
|
||||||
@@ -132,6 +142,7 @@ flagcats = {
|
|||||||
"xbd=CMD": "execute CMD before a file delete",
|
"xbd=CMD": "execute CMD before a file delete",
|
||||||
"xad=CMD": "execute CMD after a file delete",
|
"xad=CMD": "execute CMD after a file delete",
|
||||||
"xm=CMD": "execute CMD on message",
|
"xm=CMD": "execute CMD on message",
|
||||||
|
"xban=CMD": "execute CMD if someone gets banned",
|
||||||
},
|
},
|
||||||
"client and ux": {
|
"client and ux": {
|
||||||
"grid": "show grid/thumbnails by default",
|
"grid": "show grid/thumbnails by default",
|
||||||
@@ -145,6 +156,7 @@ flagcats = {
|
|||||||
"sb_lg": "enable js sandbox for prologue/epilogue (default)",
|
"sb_lg": "enable js sandbox for prologue/epilogue (default)",
|
||||||
"md_sbf": "list of markdown-sandbox safeguards to disable",
|
"md_sbf": "list of markdown-sandbox safeguards to disable",
|
||||||
"lg_sbf": "list of *logue-sandbox safeguards to disable",
|
"lg_sbf": "list of *logue-sandbox safeguards to disable",
|
||||||
|
"nohtml": "return html and markdown as text/html",
|
||||||
},
|
},
|
||||||
"others": {
|
"others": {
|
||||||
"fk=8": 'generates per-file accesskeys,\nwhich will then be required at the "g" permission',
|
"fk=8": 'generates per-file accesskeys,\nwhich will then be required at the "g" permission',
|
||||||
|
|||||||
@@ -79,10 +79,13 @@ class FtpAuth(DummyAuthorizer):
|
|||||||
raise AuthenticationFailed("banned")
|
raise AuthenticationFailed("banned")
|
||||||
|
|
||||||
asrv = self.hub.asrv
|
asrv = self.hub.asrv
|
||||||
if username == "anonymous":
|
uname = "*"
|
||||||
uname = "*"
|
if username != "anonymous":
|
||||||
else:
|
for zs in (password, username):
|
||||||
uname = asrv.iacct.get(password, "") or asrv.iacct.get(username, "") or "*"
|
zs = asrv.iacct.get(asrv.ah.hash(zs), "")
|
||||||
|
if zs:
|
||||||
|
uname = zs
|
||||||
|
break
|
||||||
|
|
||||||
if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):
|
if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):
|
||||||
g = self.hub.gpwd
|
g = self.hub.gpwd
|
||||||
@@ -131,6 +134,7 @@ class FtpFs(AbstractedFS):
|
|||||||
|
|
||||||
self.can_read = self.can_write = self.can_move = False
|
self.can_read = self.can_write = self.can_move = False
|
||||||
self.can_delete = self.can_get = self.can_upget = False
|
self.can_delete = self.can_get = self.can_upget = False
|
||||||
|
self.can_admin = False
|
||||||
|
|
||||||
self.listdirinfo = self.listdir
|
self.listdirinfo = self.listdir
|
||||||
self.chdir(".")
|
self.chdir(".")
|
||||||
@@ -165,7 +169,7 @@ class FtpFs(AbstractedFS):
|
|||||||
if not avfs:
|
if not avfs:
|
||||||
raise FSE(t.format(vpath), 1)
|
raise FSE(t.format(vpath), 1)
|
||||||
|
|
||||||
cr, cw, cm, cd, _, _ = avfs.can_access("", self.h.uname)
|
cr, cw, cm, cd, _, _, _ = avfs.can_access("", self.h.uname)
|
||||||
if r and not cr or w and not cw or m and not cm or d and not cd:
|
if r and not cr or w and not cw or m and not cm or d and not cd:
|
||||||
raise FSE(t.format(vpath), 1)
|
raise FSE(t.format(vpath), 1)
|
||||||
|
|
||||||
@@ -240,6 +244,7 @@ class FtpFs(AbstractedFS):
|
|||||||
self.can_delete,
|
self.can_delete,
|
||||||
self.can_get,
|
self.can_get,
|
||||||
self.can_upget,
|
self.can_upget,
|
||||||
|
self.can_admin,
|
||||||
) = avfs.can_access("", self.h.uname)
|
) = avfs.can_access("", self.h.uname)
|
||||||
|
|
||||||
def mkdir(self, path: str) -> None:
|
def mkdir(self, path: str) -> None:
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ from .util import (
|
|||||||
Pebkac,
|
Pebkac,
|
||||||
UnrecvEOF,
|
UnrecvEOF,
|
||||||
alltrace,
|
alltrace,
|
||||||
|
absreal,
|
||||||
atomic_move,
|
atomic_move,
|
||||||
exclude_dotfiles,
|
exclude_dotfiles,
|
||||||
fsenc,
|
fsenc,
|
||||||
@@ -58,6 +59,7 @@ from .util import (
|
|||||||
html_escape,
|
html_escape,
|
||||||
humansize,
|
humansize,
|
||||||
ipnorm,
|
ipnorm,
|
||||||
|
loadpy,
|
||||||
min_ex,
|
min_ex,
|
||||||
quotep,
|
quotep,
|
||||||
rand_name,
|
rand_name,
|
||||||
@@ -136,6 +138,8 @@ class HttpCli(object):
|
|||||||
self.uparam: dict[str, str] = {}
|
self.uparam: dict[str, str] = {}
|
||||||
self.cookies: dict[str, str] = {}
|
self.cookies: dict[str, str] = {}
|
||||||
self.avn: Optional[VFS] = None
|
self.avn: Optional[VFS] = None
|
||||||
|
self.vn = self.asrv.vfs
|
||||||
|
self.rem = " "
|
||||||
self.vpath = " "
|
self.vpath = " "
|
||||||
self.uname = " "
|
self.uname = " "
|
||||||
self.pw = " "
|
self.pw = " "
|
||||||
@@ -152,6 +156,7 @@ class HttpCli(object):
|
|||||||
self.can_delete = False
|
self.can_delete = False
|
||||||
self.can_get = False
|
self.can_get = False
|
||||||
self.can_upget = False
|
self.can_upget = False
|
||||||
|
self.can_admin = False
|
||||||
# post
|
# post
|
||||||
self.parser: Optional[MultipartParser] = None
|
self.parser: Optional[MultipartParser] = None
|
||||||
# end placeholders
|
# end placeholders
|
||||||
@@ -173,13 +178,16 @@ class HttpCli(object):
|
|||||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||||
ptn = self.asrv.re_pwd
|
ptn = self.asrv.re_pwd
|
||||||
if ptn and ptn.search(msg):
|
if ptn and ptn.search(msg):
|
||||||
msg = ptn.sub(self.unpwd, msg)
|
if self.asrv.ah.on:
|
||||||
|
msg = ptn.sub("\033[7m pw \033[27m", msg)
|
||||||
|
else:
|
||||||
|
msg = ptn.sub(self.unpwd, msg)
|
||||||
|
|
||||||
self.log_func(self.log_src, msg, c)
|
self.log_func(self.log_src, msg, c)
|
||||||
|
|
||||||
def unpwd(self, m: Match[str]) -> str:
|
def unpwd(self, m: Match[str]) -> str:
|
||||||
a, b = m.groups()
|
a, b, c = m.groups()
|
||||||
return "=\033[7m {} \033[27m{}".format(self.asrv.iacct[a], b)
|
return "{}\033[7m {} \033[27m{}".format(a, self.asrv.iacct[b], c)
|
||||||
|
|
||||||
def _check_nonfatal(self, ex: Pebkac, post: bool) -> bool:
|
def _check_nonfatal(self, ex: Pebkac, post: bool) -> bool:
|
||||||
if post:
|
if post:
|
||||||
@@ -383,13 +391,14 @@ class HttpCli(object):
|
|||||||
zs = base64.b64decode(zb).decode("utf-8")
|
zs = base64.b64decode(zb).decode("utf-8")
|
||||||
# try "pwd", "x:pwd", "pwd:x"
|
# try "pwd", "x:pwd", "pwd:x"
|
||||||
for bauth in [zs] + zs.split(":", 1)[::-1]:
|
for bauth in [zs] + zs.split(":", 1)[::-1]:
|
||||||
if self.asrv.iacct.get(bauth):
|
hpw = self.asrv.ah.hash(bauth)
|
||||||
|
if self.asrv.iacct.get(hpw):
|
||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.pw = uparam.get("pw") or self.headers.get("pw") or bauth or cookie_pw
|
self.pw = uparam.get("pw") or self.headers.get("pw") or bauth or cookie_pw
|
||||||
self.uname = self.asrv.iacct.get(self.pw) or "*"
|
self.uname = self.asrv.iacct.get(self.asrv.ah.hash(self.pw)) or "*"
|
||||||
self.rvol = self.asrv.vfs.aread[self.uname]
|
self.rvol = self.asrv.vfs.aread[self.uname]
|
||||||
self.wvol = self.asrv.vfs.awrite[self.uname]
|
self.wvol = self.asrv.vfs.awrite[self.uname]
|
||||||
self.mvol = self.asrv.vfs.amove[self.uname]
|
self.mvol = self.asrv.vfs.amove[self.uname]
|
||||||
@@ -426,10 +435,13 @@ class HttpCli(object):
|
|||||||
self.can_delete,
|
self.can_delete,
|
||||||
self.can_get,
|
self.can_get,
|
||||||
self.can_upget,
|
self.can_upget,
|
||||||
|
self.can_admin,
|
||||||
) = (
|
) = (
|
||||||
avn.can_access("", self.uname) if avn else [False] * 6
|
avn.can_access("", self.uname) if avn else [False] * 6
|
||||||
)
|
)
|
||||||
self.avn = avn
|
self.avn = avn
|
||||||
|
self.vn = vn
|
||||||
|
self.rem = rem
|
||||||
|
|
||||||
self.s.settimeout(self.args.s_tbody or None)
|
self.s.settimeout(self.args.s_tbody or None)
|
||||||
|
|
||||||
@@ -565,9 +577,8 @@ class HttpCli(object):
|
|||||||
|
|
||||||
# default to utf8 html if no content-type is set
|
# default to utf8 html if no content-type is set
|
||||||
if not mime:
|
if not mime:
|
||||||
mime = self.out_headers.get("Content-Type", "text/html; charset=utf-8")
|
mime = self.out_headers.get("Content-Type") or "text/html; charset=utf-8"
|
||||||
|
|
||||||
assert mime
|
|
||||||
self.out_headers["Content-Type"] = mime
|
self.out_headers["Content-Type"] = mime
|
||||||
|
|
||||||
for k, zs in list(self.out_headers.items()) + self.out_headerlist:
|
for k, zs in list(self.out_headers.items()) + self.out_headerlist:
|
||||||
@@ -592,8 +603,22 @@ class HttpCli(object):
|
|||||||
if g.lim:
|
if g.lim:
|
||||||
bonk, ip = g.bonk(self.ip, self.vpath)
|
bonk, ip = g.bonk(self.ip, self.vpath)
|
||||||
if bonk:
|
if bonk:
|
||||||
self.log("client banned: 404s", 1)
|
xban = self.vn.flags.get("xban")
|
||||||
self.conn.hsrv.bans[ip] = bonk
|
if not xban or not runhook(
|
||||||
|
self.log,
|
||||||
|
xban,
|
||||||
|
self.vn.canonical(self.rem),
|
||||||
|
self.vpath,
|
||||||
|
self.host,
|
||||||
|
self.uname,
|
||||||
|
time.time(),
|
||||||
|
0,
|
||||||
|
self.ip,
|
||||||
|
time.time(),
|
||||||
|
"404",
|
||||||
|
):
|
||||||
|
self.log("client banned: 404s", 1)
|
||||||
|
self.conn.hsrv.bans[ip] = bonk
|
||||||
|
|
||||||
if volsan:
|
if volsan:
|
||||||
vols = list(self.asrv.vfs.all_vols.values())
|
vols = list(self.asrv.vfs.all_vols.values())
|
||||||
@@ -755,7 +780,14 @@ class HttpCli(object):
|
|||||||
self.reply(b"", 301, headers=h)
|
self.reply(b"", 301, headers=h)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
static_path = os.path.join(self.E.mod, "web/", self.vpath[5:])
|
path_base = os.path.join(self.E.mod, "web")
|
||||||
|
static_path = absreal(os.path.join(path_base, self.vpath[5:]))
|
||||||
|
if not static_path.startswith(path_base):
|
||||||
|
t = "attempted path traversal [{}] => [{}]"
|
||||||
|
self.log(t.format(self.vpath, static_path), 1)
|
||||||
|
self.tx_404()
|
||||||
|
return False
|
||||||
|
|
||||||
return self.tx_file(static_path)
|
return self.tx_file(static_path)
|
||||||
|
|
||||||
if "cf_challenge" in self.uparam:
|
if "cf_challenge" in self.uparam:
|
||||||
@@ -763,11 +795,27 @@ class HttpCli(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
if not self.can_read and not self.can_write and not self.can_get:
|
if not self.can_read and not self.can_write and not self.can_get:
|
||||||
if self.vpath:
|
t = "@{} has no access to [{}]"
|
||||||
self.log("inaccessible: [{}]".format(self.vpath))
|
self.log(t.format(self.uname, self.vpath))
|
||||||
return self.tx_404(True)
|
|
||||||
|
|
||||||
self.uparam["h"] = ""
|
if "on403" in self.vn.flags:
|
||||||
|
ret = self.on40x(self.vn.flags["on403"], self.vn, self.rem)
|
||||||
|
if ret == "true":
|
||||||
|
return True
|
||||||
|
elif ret == "false":
|
||||||
|
return False
|
||||||
|
elif ret == "allow":
|
||||||
|
self.log("plugin override; access permitted")
|
||||||
|
self.can_read = self.can_write = self.can_move = True
|
||||||
|
self.can_delete = self.can_get = self.can_upget = True
|
||||||
|
self.can_admin = True
|
||||||
|
else:
|
||||||
|
return self.tx_404(True)
|
||||||
|
else:
|
||||||
|
if self.vpath:
|
||||||
|
return self.tx_404(True)
|
||||||
|
|
||||||
|
self.uparam["h"] = ""
|
||||||
|
|
||||||
if "tree" in self.uparam:
|
if "tree" in self.uparam:
|
||||||
return self.tx_tree()
|
return self.tx_tree()
|
||||||
@@ -1018,9 +1066,6 @@ class HttpCli(object):
|
|||||||
|
|
||||||
from .dxml import mkenod, mktnod, parse_xml
|
from .dxml import mkenod, mktnod, parse_xml
|
||||||
|
|
||||||
self.asrv.vfs.get(self.vpath, self.uname, False, False)
|
|
||||||
# abspath = vn.dcanonical(rem)
|
|
||||||
|
|
||||||
buf = b""
|
buf = b""
|
||||||
for rbuf in self.get_body_reader()[0]:
|
for rbuf in self.get_body_reader()[0]:
|
||||||
buf += rbuf
|
buf += rbuf
|
||||||
@@ -1077,8 +1122,7 @@ class HttpCli(object):
|
|||||||
|
|
||||||
from .dxml import mkenod, mktnod, parse_xml
|
from .dxml import mkenod, mktnod, parse_xml
|
||||||
|
|
||||||
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
|
abspath = self.vn.dcanonical(self.rem)
|
||||||
abspath = vn.dcanonical(rem)
|
|
||||||
|
|
||||||
buf = b""
|
buf = b""
|
||||||
for rbuf in self.get_body_reader()[0]:
|
for rbuf in self.get_body_reader()[0]:
|
||||||
@@ -1292,20 +1336,17 @@ class HttpCli(object):
|
|||||||
plain = zb.decode("utf-8", "replace")
|
plain = zb.decode("utf-8", "replace")
|
||||||
if buf.startswith(b"msg="):
|
if buf.startswith(b"msg="):
|
||||||
plain = plain[4:]
|
plain = plain[4:]
|
||||||
vfs, rem = self.asrv.vfs.get(
|
xm = self.vn.flags.get("xm")
|
||||||
self.vpath, self.uname, False, False
|
|
||||||
)
|
|
||||||
xm = vfs.flags.get("xm")
|
|
||||||
if xm:
|
if xm:
|
||||||
runhook(
|
runhook(
|
||||||
self.log,
|
self.log,
|
||||||
xm,
|
xm,
|
||||||
vfs.canonical(rem),
|
self.vn.canonical(self.rem),
|
||||||
self.vpath,
|
self.vpath,
|
||||||
self.host,
|
self.host,
|
||||||
self.uname,
|
self.uname,
|
||||||
time.time(),
|
time.time(),
|
||||||
len(xm),
|
len(buf),
|
||||||
self.ip,
|
self.ip,
|
||||||
time.time(),
|
time.time(),
|
||||||
plain,
|
plain,
|
||||||
@@ -1358,7 +1399,9 @@ class HttpCli(object):
|
|||||||
lim = vfs.get_dbv(rem)[0].lim
|
lim = vfs.get_dbv(rem)[0].lim
|
||||||
fdir = vfs.canonical(rem)
|
fdir = vfs.canonical(rem)
|
||||||
if lim:
|
if lim:
|
||||||
fdir, rem = lim.all(self.ip, rem, remains, fdir)
|
fdir, rem = lim.all(
|
||||||
|
self.ip, rem, remains, vfs.realpath, fdir, self.conn.hsrv.broker
|
||||||
|
)
|
||||||
|
|
||||||
fn = None
|
fn = None
|
||||||
if rem and not self.trailing_slash and not bos.path.isdir(fdir):
|
if rem and not self.trailing_slash and not bos.path.isdir(fdir):
|
||||||
@@ -1491,6 +1534,7 @@ class HttpCli(object):
|
|||||||
lim.bup(self.ip, post_sz)
|
lim.bup(self.ip, post_sz)
|
||||||
try:
|
try:
|
||||||
lim.chk_sz(post_sz)
|
lim.chk_sz(post_sz)
|
||||||
|
lim.chk_vsz(self.conn.hsrv.broker, vfs.realpath, post_sz)
|
||||||
except:
|
except:
|
||||||
bos.unlink(path)
|
bos.unlink(path)
|
||||||
raise
|
raise
|
||||||
@@ -1965,7 +2009,7 @@ class HttpCli(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def get_pwd_cookie(self, pwd: str) -> str:
|
def get_pwd_cookie(self, pwd: str) -> str:
|
||||||
if pwd in self.asrv.iacct:
|
if self.asrv.ah.hash(pwd) in self.asrv.iacct:
|
||||||
msg = "login ok"
|
msg = "login ok"
|
||||||
dur = int(60 * 60 * self.args.logout)
|
dur = int(60 * 60 * self.args.logout)
|
||||||
else:
|
else:
|
||||||
@@ -1974,8 +2018,22 @@ class HttpCli(object):
|
|||||||
if g.lim:
|
if g.lim:
|
||||||
bonk, ip = g.bonk(self.ip, pwd)
|
bonk, ip = g.bonk(self.ip, pwd)
|
||||||
if bonk:
|
if bonk:
|
||||||
self.log("client banned: invalid passwords", 1)
|
xban = self.vn.flags.get("xban")
|
||||||
self.conn.hsrv.bans[ip] = bonk
|
if not xban or not runhook(
|
||||||
|
self.log,
|
||||||
|
xban,
|
||||||
|
self.vn.canonical(self.rem),
|
||||||
|
self.vpath,
|
||||||
|
self.host,
|
||||||
|
self.uname,
|
||||||
|
time.time(),
|
||||||
|
0,
|
||||||
|
self.ip,
|
||||||
|
time.time(),
|
||||||
|
"pw",
|
||||||
|
):
|
||||||
|
self.log("client banned: invalid passwords", 1)
|
||||||
|
self.conn.hsrv.bans[ip] = bonk
|
||||||
|
|
||||||
msg = "naw dude"
|
msg = "naw dude"
|
||||||
pwd = "x" # nosec
|
pwd = "x" # nosec
|
||||||
@@ -2101,7 +2159,9 @@ class HttpCli(object):
|
|||||||
lim = vfs.get_dbv(rem)[0].lim
|
lim = vfs.get_dbv(rem)[0].lim
|
||||||
fdir_base = vfs.canonical(rem)
|
fdir_base = vfs.canonical(rem)
|
||||||
if lim:
|
if lim:
|
||||||
fdir_base, rem = lim.all(self.ip, rem, -1, fdir_base)
|
fdir_base, rem = lim.all(
|
||||||
|
self.ip, rem, -1, vfs.realpath, fdir_base, self.conn.hsrv.broker
|
||||||
|
)
|
||||||
upload_vpath = "{}/{}".format(vfs.vpath, rem).strip("/")
|
upload_vpath = "{}/{}".format(vfs.vpath, rem).strip("/")
|
||||||
if not nullwrite:
|
if not nullwrite:
|
||||||
bos.makedirs(fdir_base)
|
bos.makedirs(fdir_base)
|
||||||
@@ -2194,6 +2254,7 @@ class HttpCli(object):
|
|||||||
try:
|
try:
|
||||||
lim.chk_df(tabspath, sz, True)
|
lim.chk_df(tabspath, sz, True)
|
||||||
lim.chk_sz(sz)
|
lim.chk_sz(sz)
|
||||||
|
lim.chk_vsz(self.conn.hsrv.broker, vfs.realpath, sz)
|
||||||
lim.chk_bup(self.ip)
|
lim.chk_bup(self.ip)
|
||||||
lim.chk_nup(self.ip)
|
lim.chk_nup(self.ip)
|
||||||
except:
|
except:
|
||||||
@@ -2369,7 +2430,7 @@ class HttpCli(object):
|
|||||||
fp = vfs.canonical(rp)
|
fp = vfs.canonical(rp)
|
||||||
lim = vfs.get_dbv(rem)[0].lim
|
lim = vfs.get_dbv(rem)[0].lim
|
||||||
if lim:
|
if lim:
|
||||||
fp, rp = lim.all(self.ip, rp, clen, fp)
|
fp, rp = lim.all(self.ip, rp, clen, vfs.realpath, fp, self.conn.hsrv.broker)
|
||||||
bos.makedirs(fp)
|
bos.makedirs(fp)
|
||||||
|
|
||||||
fp = os.path.join(fp, fn)
|
fp = os.path.join(fp, fn)
|
||||||
@@ -2440,6 +2501,25 @@ class HttpCli(object):
|
|||||||
if p_field != "body":
|
if p_field != "body":
|
||||||
raise Pebkac(400, "expected body, got {}".format(p_field))
|
raise Pebkac(400, "expected body, got {}".format(p_field))
|
||||||
|
|
||||||
|
xbu = vfs.flags.get("xbu")
|
||||||
|
if xbu:
|
||||||
|
if not runhook(
|
||||||
|
self.log,
|
||||||
|
xbu,
|
||||||
|
fp,
|
||||||
|
self.vpath,
|
||||||
|
self.host,
|
||||||
|
self.uname,
|
||||||
|
time.time(),
|
||||||
|
0,
|
||||||
|
self.ip,
|
||||||
|
time.time(),
|
||||||
|
"",
|
||||||
|
):
|
||||||
|
t = "save blocked by xbu server config"
|
||||||
|
self.log(t, 1)
|
||||||
|
raise Pebkac(403, t)
|
||||||
|
|
||||||
if bos.path.exists(fp):
|
if bos.path.exists(fp):
|
||||||
bos.unlink(fp)
|
bos.unlink(fp)
|
||||||
|
|
||||||
@@ -2451,6 +2531,7 @@ class HttpCli(object):
|
|||||||
lim.bup(self.ip, sz)
|
lim.bup(self.ip, sz)
|
||||||
try:
|
try:
|
||||||
lim.chk_sz(sz)
|
lim.chk_sz(sz)
|
||||||
|
lim.chk_vsz(self.conn.hsrv.broker, vfs.realpath, sz)
|
||||||
except:
|
except:
|
||||||
bos.unlink(fp)
|
bos.unlink(fp)
|
||||||
raise
|
raise
|
||||||
@@ -2459,6 +2540,39 @@ class HttpCli(object):
|
|||||||
new_lastmod3 = int(new_lastmod * 1000)
|
new_lastmod3 = int(new_lastmod * 1000)
|
||||||
sha512 = sha512[:56]
|
sha512 = sha512[:56]
|
||||||
|
|
||||||
|
xau = vfs.flags.get("xau")
|
||||||
|
if xau and not runhook(
|
||||||
|
self.log,
|
||||||
|
xau,
|
||||||
|
fp,
|
||||||
|
self.vpath,
|
||||||
|
self.host,
|
||||||
|
self.uname,
|
||||||
|
new_lastmod,
|
||||||
|
sz,
|
||||||
|
self.ip,
|
||||||
|
new_lastmod,
|
||||||
|
"",
|
||||||
|
):
|
||||||
|
t = "save blocked by xau server config"
|
||||||
|
self.log(t, 1)
|
||||||
|
os.unlink(fp)
|
||||||
|
raise Pebkac(403, t)
|
||||||
|
|
||||||
|
vfs, rem = vfs.get_dbv(rem)
|
||||||
|
self.conn.hsrv.broker.say(
|
||||||
|
"up2k.hash_file",
|
||||||
|
vfs.realpath,
|
||||||
|
vfs.vpath,
|
||||||
|
vfs.flags,
|
||||||
|
vsplit(rem)[0],
|
||||||
|
fn,
|
||||||
|
self.ip,
|
||||||
|
new_lastmod,
|
||||||
|
self.uname,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
response = json.dumps(
|
response = json.dumps(
|
||||||
{"ok": True, "lastmod": new_lastmod3, "size": sz, "sha512": sha512}
|
{"ok": True, "lastmod": new_lastmod3, "size": sz, "sha512": sha512}
|
||||||
)
|
)
|
||||||
@@ -2648,6 +2762,9 @@ class HttpCli(object):
|
|||||||
else:
|
else:
|
||||||
mime = guess_mime(req_path)
|
mime = guess_mime(req_path)
|
||||||
|
|
||||||
|
if "nohtml" in self.vn.flags and "html" in mime:
|
||||||
|
mime = "text/plain; charset=utf-8"
|
||||||
|
|
||||||
self.out_headers["Accept-Ranges"] = "bytes"
|
self.out_headers["Accept-Ranges"] = "bytes"
|
||||||
self.send_headers(length=upper - lower, status=status, mime=mime)
|
self.send_headers(length=upper - lower, status=status, mime=mime)
|
||||||
|
|
||||||
@@ -2993,6 +3110,20 @@ class HttpCli(object):
|
|||||||
self.reply(html.encode("utf-8"), status=rc)
|
self.reply(html.encode("utf-8"), status=rc)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def on40x(self, mods: list[str], vn: VFS, rem: str) -> str:
|
||||||
|
for mpath in mods:
|
||||||
|
try:
|
||||||
|
mod = loadpy(mpath, self.args.hot_handlers)
|
||||||
|
except Exception as ex:
|
||||||
|
self.log("import failed: {!r}".format(ex))
|
||||||
|
continue
|
||||||
|
|
||||||
|
ret = mod.main(self, vn, rem)
|
||||||
|
if ret:
|
||||||
|
return ret.lower()
|
||||||
|
|
||||||
|
return "" # unhandled / fallthrough
|
||||||
|
|
||||||
def scanvol(self) -> bool:
|
def scanvol(self) -> bool:
|
||||||
if not self.can_read or not self.can_write:
|
if not self.can_read or not self.can_write:
|
||||||
raise Pebkac(403, "not allowed for user " + self.uname)
|
raise Pebkac(403, "not allowed for user " + self.uname)
|
||||||
@@ -3303,14 +3434,29 @@ class HttpCli(object):
|
|||||||
|
|
||||||
vpnodes.append([quotep(vpath) + "/", html_escape(node, crlf=True)])
|
vpnodes.append([quotep(vpath) + "/", html_escape(node, crlf=True)])
|
||||||
|
|
||||||
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
|
vn = self.vn
|
||||||
|
rem = self.rem
|
||||||
abspath = vn.dcanonical(rem)
|
abspath = vn.dcanonical(rem)
|
||||||
dbv, vrem = vn.get_dbv(rem)
|
dbv, vrem = vn.get_dbv(rem)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
st = bos.stat(abspath)
|
st = bos.stat(abspath)
|
||||||
except:
|
except:
|
||||||
return self.tx_404()
|
if "on404" not in vn.flags:
|
||||||
|
return self.tx_404()
|
||||||
|
|
||||||
|
ret = self.on40x(vn.flags["on404"], vn, rem)
|
||||||
|
if ret == "true":
|
||||||
|
return True
|
||||||
|
elif ret == "false":
|
||||||
|
return False
|
||||||
|
elif ret == "retry":
|
||||||
|
try:
|
||||||
|
st = bos.stat(abspath)
|
||||||
|
except:
|
||||||
|
return self.tx_404()
|
||||||
|
else:
|
||||||
|
return self.tx_404()
|
||||||
|
|
||||||
if rem.startswith(".hist/up2k.") or (
|
if rem.startswith(".hist/up2k.") or (
|
||||||
rem.endswith("/dir.txt") and rem.startswith(".hist/th/")
|
rem.endswith("/dir.txt") and rem.startswith(".hist/th/")
|
||||||
@@ -3385,8 +3531,14 @@ class HttpCli(object):
|
|||||||
self.log("wrong filekey, want {}, got {}".format(correct, got))
|
self.log("wrong filekey, want {}, got {}".format(correct, got))
|
||||||
return self.tx_404()
|
return self.tx_404()
|
||||||
|
|
||||||
if abspath.endswith(".md") and (
|
if (
|
||||||
"v" in self.uparam or "edit" in self.uparam or "edit2" in self.uparam
|
abspath.endswith(".md")
|
||||||
|
and "nohtml" not in vn.flags
|
||||||
|
and (
|
||||||
|
"v" in self.uparam
|
||||||
|
or "edit" in self.uparam
|
||||||
|
or "edit2" in self.uparam
|
||||||
|
)
|
||||||
):
|
):
|
||||||
return self.tx_md(abspath)
|
return self.tx_md(abspath)
|
||||||
|
|
||||||
@@ -3427,6 +3579,8 @@ class HttpCli(object):
|
|||||||
perms.append("get")
|
perms.append("get")
|
||||||
if self.can_upget:
|
if self.can_upget:
|
||||||
perms.append("upget")
|
perms.append("upget")
|
||||||
|
if self.can_admin:
|
||||||
|
perms.append("admin")
|
||||||
|
|
||||||
url_suf = self.urlq({}, ["k"])
|
url_suf = self.urlq({}, ["k"])
|
||||||
is_ls = "ls" in self.uparam
|
is_ls = "ls" in self.uparam
|
||||||
@@ -3678,22 +3832,33 @@ class HttpCli(object):
|
|||||||
if vn != dbv:
|
if vn != dbv:
|
||||||
_, rd = vn.get_dbv(rd)
|
_, rd = vn.get_dbv(rd)
|
||||||
|
|
||||||
|
erd_efn = (rd, fn)
|
||||||
q = "select mt.k, mt.v from up inner join mt on mt.w = substr(up.w,1,16) where up.rd = ? and up.fn = ? and +mt.k != 'x'"
|
q = "select mt.k, mt.v from up inner join mt on mt.w = substr(up.w,1,16) where up.rd = ? and up.fn = ? and +mt.k != 'x'"
|
||||||
try:
|
try:
|
||||||
r = icur.execute(q, (rd, fn))
|
r = icur.execute(q, erd_efn)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
if "database is locked" in str(ex):
|
if "database is locked" in str(ex):
|
||||||
break
|
break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
args = s3enc(idx.mem_cur, rd, fn)
|
erd_efn = s3enc(idx.mem_cur, rd, fn)
|
||||||
r = icur.execute(q, args)
|
r = icur.execute(q, erd_efn)
|
||||||
except:
|
except:
|
||||||
t = "tag read error, {}/{}\n{}"
|
t = "tag read error, {}/{}\n{}"
|
||||||
self.log(t.format(rd, fn, min_ex()))
|
self.log(t.format(rd, fn, min_ex()))
|
||||||
break
|
break
|
||||||
|
|
||||||
fe["tags"] = {k: v for k, v in r}
|
fe["tags"] = {k: v for k, v in r}
|
||||||
|
|
||||||
|
if self.can_admin:
|
||||||
|
q = "select ip, at from up where rd=? and fn=?"
|
||||||
|
try:
|
||||||
|
zs1, zs2 = icur.execute(q, erd_efn).fetchone()
|
||||||
|
fe["tags"]["up_ip"] = zs1
|
||||||
|
fe["tags"][".up_at"] = zs2
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
_ = [tagset.add(k) for k in fe["tags"]]
|
_ = [tagset.add(k) for k in fe["tags"]]
|
||||||
|
|
||||||
if icur:
|
if icur:
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ class Ico(object):
|
|||||||
def get(self, ext: str, as_thumb: bool, chrome: bool) -> tuple[str, bytes]:
|
def get(self, ext: str, as_thumb: bool, chrome: bool) -> tuple[str, bytes]:
|
||||||
"""placeholder to make thumbnails not break"""
|
"""placeholder to make thumbnails not break"""
|
||||||
|
|
||||||
zb = hashlib.sha1(ext.encode("utf-8")).digest()[2:4]
|
bext = ext.encode("ascii", "replace")
|
||||||
|
ext = bext.decode("utf-8")
|
||||||
|
zb = hashlib.sha1(bext).digest()[2:4]
|
||||||
if PY2:
|
if PY2:
|
||||||
zb = [ord(x) for x in zb]
|
zb = [ord(x) for x in zb]
|
||||||
|
|
||||||
@@ -33,7 +35,7 @@ class Ico(object):
|
|||||||
h = int(100 / (float(sw) / float(sh)))
|
h = int(100 / (float(sw) / float(sh)))
|
||||||
w = 100
|
w = 100
|
||||||
|
|
||||||
if chrome and as_thumb:
|
if chrome:
|
||||||
# cannot handle more than ~2000 unique SVGs
|
# cannot handle more than ~2000 unique SVGs
|
||||||
if HAVE_PIL:
|
if HAVE_PIL:
|
||||||
# svg: 3s, cache: 6s, this: 8s
|
# svg: 3s, cache: 6s, this: 8s
|
||||||
@@ -43,8 +45,19 @@ class Ico(object):
|
|||||||
w = 64
|
w = 64
|
||||||
img = Image.new("RGB", (w, h), "#" + c[:6])
|
img = Image.new("RGB", (w, h), "#" + c[:6])
|
||||||
pb = ImageDraw.Draw(img)
|
pb = ImageDraw.Draw(img)
|
||||||
tw, th = pb.textsize(ext)
|
try:
|
||||||
pb.text(((w - tw) // 2, (h - th) // 2), ext, fill="#" + c[6:])
|
_, _, tw, th = pb.textbbox((0, 0), ext)
|
||||||
|
except:
|
||||||
|
tw, th = pb.textsize(ext)
|
||||||
|
|
||||||
|
tw += len(ext)
|
||||||
|
cw = tw // len(ext)
|
||||||
|
x = ((w - tw) // 2) - (cw * 2) // 3
|
||||||
|
fill = "#" + c[6:]
|
||||||
|
for ch in ext:
|
||||||
|
pb.text((x, (h - th) // 2), " %s " % (ch,), fill=fill)
|
||||||
|
x += cw
|
||||||
|
|
||||||
img = img.resize((w * 3, h * 3), Image.NEAREST)
|
img = img.resize((w * 3, h * 3), Image.NEAREST)
|
||||||
|
|
||||||
buf = BytesIO()
|
buf = BytesIO()
|
||||||
|
|||||||
145
copyparty/pwhash.py
Normal file
145
copyparty/pwhash.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from .__init__ import unicode
|
||||||
|
|
||||||
|
|
||||||
|
class PWHash(object):
|
||||||
|
def __init__(self, args: argparse.Namespace):
|
||||||
|
self.args = args
|
||||||
|
|
||||||
|
try:
|
||||||
|
alg, ac = args.ah_alg.split(",")
|
||||||
|
except:
|
||||||
|
alg = args.ah_alg
|
||||||
|
ac = {}
|
||||||
|
|
||||||
|
if alg == "none":
|
||||||
|
alg = ""
|
||||||
|
|
||||||
|
self.alg = alg
|
||||||
|
self.ac = ac
|
||||||
|
if not alg:
|
||||||
|
self.on = False
|
||||||
|
self.hash = unicode
|
||||||
|
return
|
||||||
|
|
||||||
|
self.on = True
|
||||||
|
self.salt = args.ah_salt.encode("utf-8")
|
||||||
|
self.cache: dict[str, str] = {}
|
||||||
|
self.mutex = threading.Lock()
|
||||||
|
self.hash = self._cache_hash
|
||||||
|
|
||||||
|
if alg == "sha2":
|
||||||
|
self._hash = self._gen_sha2
|
||||||
|
elif alg == "scrypt":
|
||||||
|
self._hash = self._gen_scrypt
|
||||||
|
elif alg == "argon2":
|
||||||
|
self._hash = self._gen_argon2
|
||||||
|
else:
|
||||||
|
t = "unsupported password hashing algorithm [{}], must be one of these: argon2 scrypt sha2 none"
|
||||||
|
raise Exception(t.format(alg))
|
||||||
|
|
||||||
|
def _cache_hash(self, plain: str) -> str:
|
||||||
|
with self.mutex:
|
||||||
|
try:
|
||||||
|
return self.cache[plain]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not plain:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if len(plain) > 255:
|
||||||
|
raise Exception("password too long")
|
||||||
|
|
||||||
|
if len(self.cache) > 9000:
|
||||||
|
self.cache = {}
|
||||||
|
|
||||||
|
ret = self._hash(plain)
|
||||||
|
self.cache[plain] = ret
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def _gen_sha2(self, plain: str) -> str:
|
||||||
|
its = int(self.ac[0]) if self.ac else 424242
|
||||||
|
bplain = plain.encode("utf-8")
|
||||||
|
ret = b"\n"
|
||||||
|
for _ in range(its):
|
||||||
|
ret = hashlib.sha512(self.salt + bplain + ret).digest()
|
||||||
|
|
||||||
|
return "+" + base64.urlsafe_b64encode(ret[:24]).decode("utf-8")
|
||||||
|
|
||||||
|
def _gen_scrypt(self, plain: str) -> str:
|
||||||
|
cost = 2 << 13
|
||||||
|
its = 2
|
||||||
|
blksz = 8
|
||||||
|
para = 4
|
||||||
|
try:
|
||||||
|
cost = 2 << int(self.ac[0])
|
||||||
|
its = int(self.ac[1])
|
||||||
|
blksz = int(self.ac[2])
|
||||||
|
para = int(self.ac[3])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ret = plain.encode("utf-8")
|
||||||
|
for _ in range(its):
|
||||||
|
ret = hashlib.scrypt(ret, salt=self.salt, n=cost, r=blksz, p=para, dklen=24)
|
||||||
|
|
||||||
|
return "+" + base64.urlsafe_b64encode(ret).decode("utf-8")
|
||||||
|
|
||||||
|
def _gen_argon2(self, plain: str) -> str:
|
||||||
|
from argon2.low_level import Type as ArgonType
|
||||||
|
from argon2.low_level import hash_secret
|
||||||
|
|
||||||
|
time_cost = 3
|
||||||
|
mem_cost = 256
|
||||||
|
parallelism = 4
|
||||||
|
version = 19
|
||||||
|
try:
|
||||||
|
time_cost = int(self.ac[0])
|
||||||
|
mem_cost = int(self.ac[1])
|
||||||
|
parallelism = int(self.ac[2])
|
||||||
|
version = int(self.ac[3])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
bplain = plain.encode("utf-8")
|
||||||
|
|
||||||
|
bret = hash_secret(
|
||||||
|
secret=bplain,
|
||||||
|
salt=self.salt,
|
||||||
|
time_cost=time_cost,
|
||||||
|
memory_cost=mem_cost * 1024,
|
||||||
|
parallelism=parallelism,
|
||||||
|
hash_len=24,
|
||||||
|
type=ArgonType.ID,
|
||||||
|
version=version,
|
||||||
|
)
|
||||||
|
ret = bret.split(b"$")[-1].decode("utf-8")
|
||||||
|
return "+" + ret.replace("/", "_").replace("+", "-")
|
||||||
|
|
||||||
|
def stdin(self) -> None:
|
||||||
|
while True:
|
||||||
|
ln = sys.stdin.readline().strip()
|
||||||
|
if not ln:
|
||||||
|
break
|
||||||
|
print(self.hash(ln))
|
||||||
|
|
||||||
|
def cli(self) -> None:
|
||||||
|
import getpass
|
||||||
|
|
||||||
|
while True:
|
||||||
|
p1 = getpass.getpass("password> ")
|
||||||
|
p2 = getpass.getpass("again or just hit ENTER> ")
|
||||||
|
if p2 and p1 != p2:
|
||||||
|
print("\033[31minputs don't match; try again\033[0m", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
print(self.hash(p1))
|
||||||
|
print()
|
||||||
@@ -30,6 +30,7 @@ if True: # pylint: disable=using-constant-test
|
|||||||
|
|
||||||
from .__init__ import ANYWIN, EXE, MACOS, TYPE_CHECKING, EnvParams, unicode
|
from .__init__ import ANYWIN, EXE, MACOS, TYPE_CHECKING, EnvParams, unicode
|
||||||
from .authsrv import AuthSrv
|
from .authsrv import AuthSrv
|
||||||
|
from .cert import ensure_cert
|
||||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE
|
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE
|
||||||
from .tcpsrv import TcpSrv
|
from .tcpsrv import TcpSrv
|
||||||
from .th_srv import HAVE_PIL, HAVE_VIPS, HAVE_WEBP, ThumbSrv
|
from .th_srv import HAVE_PIL, HAVE_VIPS, HAVE_WEBP, ThumbSrv
|
||||||
@@ -239,7 +240,8 @@ class SvcHub(object):
|
|||||||
if args.ftp or args.ftps:
|
if args.ftp or args.ftps:
|
||||||
from .ftpd import Ftpd
|
from .ftpd import Ftpd
|
||||||
|
|
||||||
self.ftpd = Ftpd(self)
|
self.ftpd: Optional[Ftpd] = None
|
||||||
|
Daemon(self.start_ftpd, "start_ftpd")
|
||||||
zms += "f" if args.ftp else "F"
|
zms += "f" if args.ftp else "F"
|
||||||
|
|
||||||
if args.smb:
|
if args.smb:
|
||||||
@@ -269,6 +271,28 @@ class SvcHub(object):
|
|||||||
|
|
||||||
self.broker = Broker(self)
|
self.broker = Broker(self)
|
||||||
|
|
||||||
|
def start_ftpd(self) -> None:
|
||||||
|
time.sleep(30)
|
||||||
|
if self.ftpd:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.restart_ftpd()
|
||||||
|
|
||||||
|
def restart_ftpd(self) -> None:
|
||||||
|
if not hasattr(self, "ftpd"):
|
||||||
|
return
|
||||||
|
|
||||||
|
from .ftpd import Ftpd
|
||||||
|
|
||||||
|
if self.ftpd:
|
||||||
|
return # todo
|
||||||
|
|
||||||
|
if not os.path.exists(self.args.cert):
|
||||||
|
ensure_cert(self.log, self.args)
|
||||||
|
|
||||||
|
self.ftpd = Ftpd(self)
|
||||||
|
self.log("root", "started FTPd")
|
||||||
|
|
||||||
def thr_httpsrv_up(self) -> None:
|
def thr_httpsrv_up(self) -> None:
|
||||||
time.sleep(1 if self.args.ign_ebind_all else 5)
|
time.sleep(1 if self.args.ign_ebind_all else 5)
|
||||||
expected = self.broker.num_workers * self.tcpsrv.nsrv
|
expected = self.broker.num_workers * self.tcpsrv.nsrv
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import sys
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
|
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
|
||||||
from .stolen.qrcodegen import QrCode
|
|
||||||
from .cert import gencert
|
from .cert import gencert
|
||||||
|
from .stolen.qrcodegen import QrCode
|
||||||
from .util import (
|
from .util import (
|
||||||
E_ACCESS,
|
E_ACCESS,
|
||||||
E_ADDR_IN_USE,
|
E_ADDR_IN_USE,
|
||||||
@@ -297,6 +297,7 @@ class TcpSrv(object):
|
|||||||
self.hub.broker.say("set_netdevs", self.netdevs)
|
self.hub.broker.say("set_netdevs", self.netdevs)
|
||||||
self.hub.start_zeroconf()
|
self.hub.start_zeroconf()
|
||||||
gencert(self.log, self.args, self.netdevs)
|
gencert(self.log, self.args, self.netdevs)
|
||||||
|
self.hub.restart_ftpd()
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
self.stopping = True
|
self.stopping = True
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import time
|
|||||||
from queue import Queue
|
from queue import Queue
|
||||||
|
|
||||||
from .__init__ import ANYWIN, TYPE_CHECKING
|
from .__init__ import ANYWIN, TYPE_CHECKING
|
||||||
|
from .authsrv import VFS
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
|
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
|
||||||
from .util import (
|
from .util import (
|
||||||
@@ -110,8 +111,6 @@ class ThumbSrv(object):
|
|||||||
self.args = hub.args
|
self.args = hub.args
|
||||||
self.log_func = hub.log
|
self.log_func = hub.log
|
||||||
|
|
||||||
res = hub.args.th_size.split("x")
|
|
||||||
self.res = tuple([int(x) for x in res])
|
|
||||||
self.poke_cd = Cooldown(self.args.th_poke)
|
self.poke_cd = Cooldown(self.args.th_poke)
|
||||||
|
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
@@ -119,7 +118,7 @@ class ThumbSrv(object):
|
|||||||
self.stopping = False
|
self.stopping = False
|
||||||
self.nthr = max(1, self.args.th_mt)
|
self.nthr = max(1, self.args.th_mt)
|
||||||
|
|
||||||
self.q: Queue[Optional[tuple[str, str]]] = Queue(self.nthr * 4)
|
self.q: Queue[Optional[tuple[str, str, VFS]]] = Queue(self.nthr * 4)
|
||||||
for n in range(self.nthr):
|
for n in range(self.nthr):
|
||||||
Daemon(self.worker, "thumb-{}-{}".format(n, self.nthr))
|
Daemon(self.worker, "thumb-{}-{}".format(n, self.nthr))
|
||||||
|
|
||||||
@@ -184,6 +183,10 @@ class ThumbSrv(object):
|
|||||||
with self.mutex:
|
with self.mutex:
|
||||||
return not self.nthr
|
return not self.nthr
|
||||||
|
|
||||||
|
def getres(self, vn: VFS) -> tuple[int, int]:
|
||||||
|
w, h = vn.flags["thsize"].split("x")
|
||||||
|
return int(w), int(h)
|
||||||
|
|
||||||
def get(self, ptop: str, rem: str, mtime: float, fmt: str) -> Optional[str]:
|
def get(self, ptop: str, rem: str, mtime: float, fmt: str) -> Optional[str]:
|
||||||
histpath = self.asrv.vfs.histtab.get(ptop)
|
histpath = self.asrv.vfs.histtab.get(ptop)
|
||||||
if not histpath:
|
if not histpath:
|
||||||
@@ -211,7 +214,13 @@ class ThumbSrv(object):
|
|||||||
do_conv = True
|
do_conv = True
|
||||||
|
|
||||||
if do_conv:
|
if do_conv:
|
||||||
self.q.put((abspath, tpath))
|
allvols = list(self.asrv.vfs.all_vols.values())
|
||||||
|
vn = next((x for x in allvols if x.realpath == ptop), None)
|
||||||
|
if not vn:
|
||||||
|
self.log("ptop [{}] not in {}".format(ptop, allvols), 3)
|
||||||
|
vn = self.asrv.vfs.all_aps[0][1]
|
||||||
|
|
||||||
|
self.q.put((abspath, tpath, vn))
|
||||||
self.log("conv {} \033[0m{}".format(tpath, abspath), c=6)
|
self.log("conv {} \033[0m{}".format(tpath, abspath), c=6)
|
||||||
|
|
||||||
while not self.stopping:
|
while not self.stopping:
|
||||||
@@ -248,7 +257,7 @@ class ThumbSrv(object):
|
|||||||
if not task:
|
if not task:
|
||||||
break
|
break
|
||||||
|
|
||||||
abspath, tpath = task
|
abspath, tpath, vn = task
|
||||||
ext = abspath.split(".")[-1].lower()
|
ext = abspath.split(".")[-1].lower()
|
||||||
png_ok = False
|
png_ok = False
|
||||||
funs = []
|
funs = []
|
||||||
@@ -281,7 +290,7 @@ class ThumbSrv(object):
|
|||||||
|
|
||||||
for fun in funs:
|
for fun in funs:
|
||||||
try:
|
try:
|
||||||
fun(abspath, ttpath)
|
fun(abspath, ttpath, vn)
|
||||||
break
|
break
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
msg = "{} could not create thumbnail of {}\n{}"
|
msg = "{} could not create thumbnail of {}\n{}"
|
||||||
@@ -315,9 +324,10 @@ class ThumbSrv(object):
|
|||||||
with self.mutex:
|
with self.mutex:
|
||||||
self.nthr -= 1
|
self.nthr -= 1
|
||||||
|
|
||||||
def fancy_pillow(self, im: "Image.Image") -> "Image.Image":
|
def fancy_pillow(self, im: "Image.Image", vn: VFS) -> "Image.Image":
|
||||||
# exif_transpose is expensive (loads full image + unconditional copy)
|
# exif_transpose is expensive (loads full image + unconditional copy)
|
||||||
r = max(*self.res) * 2
|
res = self.getres(vn)
|
||||||
|
r = max(*res) * 2
|
||||||
im.thumbnail((r, r), resample=Image.LANCZOS)
|
im.thumbnail((r, r), resample=Image.LANCZOS)
|
||||||
try:
|
try:
|
||||||
k = next(k for k, v in ExifTags.TAGS.items() if v == "Orientation")
|
k = next(k for k, v in ExifTags.TAGS.items() if v == "Orientation")
|
||||||
@@ -331,23 +341,23 @@ class ThumbSrv(object):
|
|||||||
if rot in rots:
|
if rot in rots:
|
||||||
im = im.transpose(rots[rot])
|
im = im.transpose(rots[rot])
|
||||||
|
|
||||||
if self.args.th_no_crop:
|
if "nocrop" in vn.flags:
|
||||||
im.thumbnail(self.res, resample=Image.LANCZOS)
|
im.thumbnail(res, resample=Image.LANCZOS)
|
||||||
else:
|
else:
|
||||||
iw, ih = im.size
|
iw, ih = im.size
|
||||||
dw, dh = self.res
|
dw, dh = res
|
||||||
res = (min(iw, dw), min(ih, dh))
|
res = (min(iw, dw), min(ih, dh))
|
||||||
im = ImageOps.fit(im, res, method=Image.LANCZOS)
|
im = ImageOps.fit(im, res, method=Image.LANCZOS)
|
||||||
|
|
||||||
return im
|
return im
|
||||||
|
|
||||||
def conv_pil(self, abspath: str, tpath: str) -> None:
|
def conv_pil(self, abspath: str, tpath: str, vn: VFS) -> None:
|
||||||
with Image.open(fsenc(abspath)) as im:
|
with Image.open(fsenc(abspath)) as im:
|
||||||
try:
|
try:
|
||||||
im = self.fancy_pillow(im)
|
im = self.fancy_pillow(im, vn)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.log("fancy_pillow {}".format(ex), "90")
|
self.log("fancy_pillow {}".format(ex), "90")
|
||||||
im.thumbnail(self.res)
|
im.thumbnail(self.getres(vn))
|
||||||
|
|
||||||
fmts = ["RGB", "L"]
|
fmts = ["RGB", "L"]
|
||||||
args = {"quality": 40}
|
args = {"quality": 40}
|
||||||
@@ -370,12 +380,12 @@ class ThumbSrv(object):
|
|||||||
|
|
||||||
im.save(tpath, **args)
|
im.save(tpath, **args)
|
||||||
|
|
||||||
def conv_vips(self, abspath: str, tpath: str) -> None:
|
def conv_vips(self, abspath: str, tpath: str, vn: VFS) -> None:
|
||||||
crops = ["centre", "none"]
|
crops = ["centre", "none"]
|
||||||
if self.args.th_no_crop:
|
if "nocrop" in vn.flags:
|
||||||
crops = ["none"]
|
crops = ["none"]
|
||||||
|
|
||||||
w, h = self.res
|
w, h = self.getres(vn)
|
||||||
kw = {"height": h, "size": "down", "intent": "relative"}
|
kw = {"height": h, "size": "down", "intent": "relative"}
|
||||||
|
|
||||||
for c in crops:
|
for c in crops:
|
||||||
@@ -389,8 +399,8 @@ class ThumbSrv(object):
|
|||||||
|
|
||||||
img.write_to_file(tpath, Q=40)
|
img.write_to_file(tpath, Q=40)
|
||||||
|
|
||||||
def conv_ffmpeg(self, abspath: str, tpath: str) -> None:
|
def conv_ffmpeg(self, abspath: str, tpath: str, vn: VFS) -> None:
|
||||||
ret, _ = ffprobe(abspath, int(self.args.th_convt / 2))
|
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||||
if not ret:
|
if not ret:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -402,12 +412,13 @@ class ThumbSrv(object):
|
|||||||
seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")]
|
seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")]
|
||||||
|
|
||||||
scale = "scale={0}:{1}:force_original_aspect_ratio="
|
scale = "scale={0}:{1}:force_original_aspect_ratio="
|
||||||
if self.args.th_no_crop:
|
if "nocrop" in vn.flags:
|
||||||
scale += "decrease,setsar=1:1"
|
scale += "decrease,setsar=1:1"
|
||||||
else:
|
else:
|
||||||
scale += "increase,crop={0}:{1},setsar=1:1"
|
scale += "increase,crop={0}:{1},setsar=1:1"
|
||||||
|
|
||||||
bscale = scale.format(*list(self.res)).encode("utf-8")
|
res = self.getres(vn)
|
||||||
|
bscale = scale.format(*list(res)).encode("utf-8")
|
||||||
# fmt: off
|
# fmt: off
|
||||||
cmd = [
|
cmd = [
|
||||||
b"ffmpeg",
|
b"ffmpeg",
|
||||||
@@ -439,11 +450,11 @@ class ThumbSrv(object):
|
|||||||
]
|
]
|
||||||
|
|
||||||
cmd += [fsenc(tpath)]
|
cmd += [fsenc(tpath)]
|
||||||
self._run_ff(cmd)
|
self._run_ff(cmd, vn)
|
||||||
|
|
||||||
def _run_ff(self, cmd: list[bytes]) -> None:
|
def _run_ff(self, cmd: list[bytes], vn: VFS) -> None:
|
||||||
# self.log((b" ".join(cmd)).decode("utf-8"))
|
# self.log((b" ".join(cmd)).decode("utf-8"))
|
||||||
ret, _, serr = runcmd(cmd, timeout=self.args.th_convt)
|
ret, _, serr = runcmd(cmd, timeout=vn.flags["convt"])
|
||||||
if not ret:
|
if not ret:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -486,8 +497,8 @@ class ThumbSrv(object):
|
|||||||
self.log(t + txt, c=c)
|
self.log(t + txt, c=c)
|
||||||
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
|
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
|
||||||
|
|
||||||
def conv_waves(self, abspath: str, tpath: str) -> None:
|
def conv_waves(self, abspath: str, tpath: str, vn: VFS) -> None:
|
||||||
ret, _ = ffprobe(abspath, int(self.args.th_convt / 2))
|
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||||
if "ac" not in ret:
|
if "ac" not in ret:
|
||||||
raise Exception("not audio")
|
raise Exception("not audio")
|
||||||
|
|
||||||
@@ -512,10 +523,10 @@ class ThumbSrv(object):
|
|||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
cmd += [fsenc(tpath)]
|
cmd += [fsenc(tpath)]
|
||||||
self._run_ff(cmd)
|
self._run_ff(cmd, vn)
|
||||||
|
|
||||||
def conv_spec(self, abspath: str, tpath: str) -> None:
|
def conv_spec(self, abspath: str, tpath: str, vn: VFS) -> None:
|
||||||
ret, _ = ffprobe(abspath, int(self.args.th_convt / 2))
|
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||||
if "ac" not in ret:
|
if "ac" not in ret:
|
||||||
raise Exception("not audio")
|
raise Exception("not audio")
|
||||||
|
|
||||||
@@ -555,13 +566,13 @@ class ThumbSrv(object):
|
|||||||
]
|
]
|
||||||
|
|
||||||
cmd += [fsenc(tpath)]
|
cmd += [fsenc(tpath)]
|
||||||
self._run_ff(cmd)
|
self._run_ff(cmd, vn)
|
||||||
|
|
||||||
def conv_opus(self, abspath: str, tpath: str) -> None:
|
def conv_opus(self, abspath: str, tpath: str, vn: VFS) -> None:
|
||||||
if self.args.no_acode:
|
if self.args.no_acode:
|
||||||
raise Exception("disabled in server config")
|
raise Exception("disabled in server config")
|
||||||
|
|
||||||
ret, _ = ffprobe(abspath, int(self.args.th_convt / 2))
|
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||||
if "ac" not in ret:
|
if "ac" not in ret:
|
||||||
raise Exception("not audio")
|
raise Exception("not audio")
|
||||||
|
|
||||||
@@ -597,7 +608,7 @@ class ThumbSrv(object):
|
|||||||
fsenc(tmp_opus)
|
fsenc(tmp_opus)
|
||||||
]
|
]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
self._run_ff(cmd)
|
self._run_ff(cmd, vn)
|
||||||
|
|
||||||
# iOS fails to play some "insufficiently complex" files
|
# iOS fails to play some "insufficiently complex" files
|
||||||
# (average file shorter than 8 seconds), so of course we
|
# (average file shorter than 8 seconds), so of course we
|
||||||
@@ -621,7 +632,7 @@ class ThumbSrv(object):
|
|||||||
fsenc(tpath)
|
fsenc(tpath)
|
||||||
]
|
]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
self._run_ff(cmd)
|
self._run_ff(cmd, vn)
|
||||||
|
|
||||||
elif want_caf:
|
elif want_caf:
|
||||||
# simple remux should be safe
|
# simple remux should be safe
|
||||||
@@ -639,7 +650,7 @@ class ThumbSrv(object):
|
|||||||
fsenc(tpath)
|
fsenc(tpath)
|
||||||
]
|
]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
self._run_ff(cmd)
|
self._run_ff(cmd, vn)
|
||||||
|
|
||||||
if tmp_opus != tpath:
|
if tmp_opus != tpath:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class U2idx(object):
|
|||||||
|
|
||||||
fsize = body["size"]
|
fsize = body["size"]
|
||||||
fhash = body["hash"]
|
fhash = body["hash"]
|
||||||
wark = up2k_wark_from_hashlist(self.args.salt, fsize, fhash)
|
wark = up2k_wark_from_hashlist(self.args.warksalt, fsize, fhash)
|
||||||
|
|
||||||
uq = "substr(w,1,16) = ? and w = ?"
|
uq = "substr(w,1,16) = ? and w = ?"
|
||||||
uv: list[Union[str, int]] = [wark[:16], wark]
|
uv: list[Union[str, int]] = [wark[:16], wark]
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from queue import Queue
|
|||||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, WINDOWS
|
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, WINDOWS
|
||||||
from .authsrv import LEELOO_DALLAS, VFS, AuthSrv
|
from .authsrv import LEELOO_DALLAS, VFS, AuthSrv
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
|
from .cfg import vf_bmap, vf_vmap
|
||||||
from .fsutil import Fstab
|
from .fsutil import Fstab
|
||||||
from .mtag import MParser, MTag
|
from .mtag import MParser, MTag
|
||||||
from .util import (
|
from .util import (
|
||||||
@@ -41,6 +42,7 @@ from .util import (
|
|||||||
gen_filekey,
|
gen_filekey,
|
||||||
gen_filekey_dbg,
|
gen_filekey_dbg,
|
||||||
hidedir,
|
hidedir,
|
||||||
|
humansize,
|
||||||
min_ex,
|
min_ex,
|
||||||
quotep,
|
quotep,
|
||||||
rand_name,
|
rand_name,
|
||||||
@@ -56,6 +58,7 @@ from .util import (
|
|||||||
sfsenc,
|
sfsenc,
|
||||||
spack,
|
spack,
|
||||||
statdir,
|
statdir,
|
||||||
|
unhumanize,
|
||||||
vjoin,
|
vjoin,
|
||||||
vsplit,
|
vsplit,
|
||||||
w8b64dec,
|
w8b64dec,
|
||||||
@@ -110,7 +113,7 @@ class Up2k(object):
|
|||||||
self.args = hub.args
|
self.args = hub.args
|
||||||
self.log_func = hub.log
|
self.log_func = hub.log
|
||||||
|
|
||||||
self.salt = self.args.salt
|
self.salt = self.args.warksalt
|
||||||
self.r_hash = re.compile("^[0-9a-zA-Z_-]{44}$")
|
self.r_hash = re.compile("^[0-9a-zA-Z_-]{44}$")
|
||||||
|
|
||||||
self.gid = 0
|
self.gid = 0
|
||||||
@@ -125,6 +128,8 @@ class Up2k(object):
|
|||||||
self.registry: dict[str, dict[str, dict[str, Any]]] = {}
|
self.registry: dict[str, dict[str, dict[str, Any]]] = {}
|
||||||
self.flags: dict[str, dict[str, Any]] = {}
|
self.flags: dict[str, dict[str, Any]] = {}
|
||||||
self.droppable: dict[str, list[str]] = {}
|
self.droppable: dict[str, list[str]] = {}
|
||||||
|
self.volnfiles: dict["sqlite3.Cursor", int] = {}
|
||||||
|
self.volsize: dict["sqlite3.Cursor", int] = {}
|
||||||
self.volstate: dict[str, str] = {}
|
self.volstate: dict[str, str] = {}
|
||||||
self.vol_act: dict[str, float] = {}
|
self.vol_act: dict[str, float] = {}
|
||||||
self.busy_aps: set[str] = set()
|
self.busy_aps: set[str] = set()
|
||||||
@@ -195,7 +200,8 @@ class Up2k(object):
|
|||||||
if self.stop:
|
if self.stop:
|
||||||
# up-mt consistency not guaranteed if init is interrupted;
|
# up-mt consistency not guaranteed if init is interrupted;
|
||||||
# drop caches for a full scan on next boot
|
# drop caches for a full scan on next boot
|
||||||
self._drop_caches()
|
with self.mutex:
|
||||||
|
self._drop_caches()
|
||||||
|
|
||||||
if self.pp:
|
if self.pp:
|
||||||
self.pp.end = True
|
self.pp.end = True
|
||||||
@@ -261,6 +267,20 @@ class Up2k(object):
|
|||||||
}
|
}
|
||||||
return json.dumps(ret, indent=4)
|
return json.dumps(ret, indent=4)
|
||||||
|
|
||||||
|
def get_volsize(self, ptop: str) -> tuple[int, int]:
|
||||||
|
with self.mutex:
|
||||||
|
return self._get_volsize(ptop)
|
||||||
|
|
||||||
|
def _get_volsize(self, ptop: str) -> tuple[int, int]:
|
||||||
|
cur = self.cur[ptop]
|
||||||
|
nbytes = self.volsize[cur]
|
||||||
|
nfiles = self.volnfiles[cur]
|
||||||
|
for j in list(self.registry.get(ptop, {}).values()):
|
||||||
|
nbytes += j["size"]
|
||||||
|
nfiles += 1
|
||||||
|
|
||||||
|
return (nbytes, nfiles)
|
||||||
|
|
||||||
def rescan(
|
def rescan(
|
||||||
self, all_vols: dict[str, VFS], scan_vols: list[str], wait: bool, fscan: bool
|
self, all_vols: dict[str, VFS], scan_vols: list[str], wait: bool, fscan: bool
|
||||||
) -> str:
|
) -> str:
|
||||||
@@ -575,7 +595,8 @@ class Up2k(object):
|
|||||||
|
|
||||||
if self.args.re_dhash or [zv for zv in vols if "e2tsr" in zv.flags]:
|
if self.args.re_dhash or [zv for zv in vols if "e2tsr" in zv.flags]:
|
||||||
self.args.re_dhash = False
|
self.args.re_dhash = False
|
||||||
self._drop_caches()
|
with self.mutex:
|
||||||
|
self._drop_caches()
|
||||||
|
|
||||||
for vol in vols:
|
for vol in vols:
|
||||||
if self.stop:
|
if self.stop:
|
||||||
@@ -739,8 +760,9 @@ class Up2k(object):
|
|||||||
ff = "\033[0;35m{}{:.0}"
|
ff = "\033[0;35m{}{:.0}"
|
||||||
fv = "\033[0;36m{}:\033[90m{}"
|
fv = "\033[0;36m{}:\033[90m{}"
|
||||||
fx = set(("html_head",))
|
fx = set(("html_head",))
|
||||||
fdl = ("dbd", "lg_sbf", "md_sbf", "mte", "mth", "mtp", "nrand", "rand")
|
fd = vf_bmap()
|
||||||
fd = {x: x for x in fdl}
|
fd.update(vf_vmap())
|
||||||
|
fd = {v: k for k, v in fd.items()}
|
||||||
fl = {
|
fl = {
|
||||||
k: v
|
k: v
|
||||||
for k, v in flags.items()
|
for k, v in flags.items()
|
||||||
@@ -751,6 +773,9 @@ class Up2k(object):
|
|||||||
for k, v in fl.items()
|
for k, v in fl.items()
|
||||||
if k not in fx
|
if k not in fx
|
||||||
]
|
]
|
||||||
|
if not a:
|
||||||
|
a = ["\033[90mall-default"]
|
||||||
|
|
||||||
if a:
|
if a:
|
||||||
vpath = "?"
|
vpath = "?"
|
||||||
for k, v in self.asrv.vfs.all_vols.items():
|
for k, v in self.asrv.vfs.all_vols.items():
|
||||||
@@ -810,6 +835,8 @@ class Up2k(object):
|
|||||||
try:
|
try:
|
||||||
cur = self._open_db(db_path)
|
cur = self._open_db(db_path)
|
||||||
self.cur[ptop] = cur
|
self.cur[ptop] = cur
|
||||||
|
self.volsize[cur] = 0
|
||||||
|
self.volnfiles[cur] = 0
|
||||||
|
|
||||||
# speeds measured uploading 520 small files on a WD20SPZX (SMR 2.5" 5400rpm 4kb)
|
# speeds measured uploading 520 small files on a WD20SPZX (SMR 2.5" 5400rpm 4kb)
|
||||||
dbd = flags["dbd"]
|
dbd = flags["dbd"]
|
||||||
@@ -917,6 +944,24 @@ class Up2k(object):
|
|||||||
|
|
||||||
db.c.connection.commit()
|
db.c.connection.commit()
|
||||||
|
|
||||||
|
if vol.flags.get("vmaxb") or vol.flags.get("vmaxn"):
|
||||||
|
zs = "select count(sz), sum(sz) from up"
|
||||||
|
vn, vb = db.c.execute(zs).fetchone()
|
||||||
|
vb = vb or 0
|
||||||
|
vb += vn * 2048
|
||||||
|
self.volsize[db.c] = vb
|
||||||
|
self.volnfiles[db.c] = vn
|
||||||
|
vmaxb = unhumanize(vol.flags.get("vmaxb") or "0")
|
||||||
|
vmaxn = unhumanize(vol.flags.get("vmaxn") or "0")
|
||||||
|
t = "{} / {} ( {} / {} files) in {}".format(
|
||||||
|
humansize(vb, True),
|
||||||
|
humansize(vmaxb, True),
|
||||||
|
humansize(vn, True).rstrip("B"),
|
||||||
|
humansize(vmaxn, True).rstrip("B"),
|
||||||
|
vol.realpath,
|
||||||
|
)
|
||||||
|
self.log(t)
|
||||||
|
|
||||||
return True, bool(n_add or n_rm or do_vac)
|
return True, bool(n_add or n_rm or do_vac)
|
||||||
|
|
||||||
def _build_dir(
|
def _build_dir(
|
||||||
@@ -1092,7 +1137,7 @@ class Up2k(object):
|
|||||||
top, rp, dts, lmod, dsz, sz
|
top, rp, dts, lmod, dsz, sz
|
||||||
)
|
)
|
||||||
self.log(t)
|
self.log(t)
|
||||||
self.db_rm(db.c, rd, fn)
|
self.db_rm(db.c, rd, fn, 0)
|
||||||
ret += 1
|
ret += 1
|
||||||
db.n += 1
|
db.n += 1
|
||||||
in_db = []
|
in_db = []
|
||||||
@@ -1175,7 +1220,7 @@ class Up2k(object):
|
|||||||
rm_files = [x for x in hits if x not in seen_files]
|
rm_files = [x for x in hits if x not in seen_files]
|
||||||
n_rm = len(rm_files)
|
n_rm = len(rm_files)
|
||||||
for fn in rm_files:
|
for fn in rm_files:
|
||||||
self.db_rm(db.c, rd, fn)
|
self.db_rm(db.c, rd, fn, 0)
|
||||||
|
|
||||||
if n_rm:
|
if n_rm:
|
||||||
self.log("forgot {} deleted files".format(n_rm))
|
self.log("forgot {} deleted files".format(n_rm))
|
||||||
@@ -2284,7 +2329,9 @@ class Up2k(object):
|
|||||||
if lost:
|
if lost:
|
||||||
c2 = None
|
c2 = None
|
||||||
for cur, dp_dir, dp_fn in lost:
|
for cur, dp_dir, dp_fn in lost:
|
||||||
self.db_rm(cur, dp_dir, dp_fn)
|
t = "forgetting deleted file: /{}"
|
||||||
|
self.log(t.format(vjoin(vjoin(vfs.vpath, dp_dir), dp_fn)))
|
||||||
|
self.db_rm(cur, dp_dir, dp_fn, cj["size"])
|
||||||
if c2 and c2 != cur:
|
if c2 and c2 != cur:
|
||||||
c2.connection.commit()
|
c2.connection.commit()
|
||||||
|
|
||||||
@@ -2418,7 +2465,14 @@ class Up2k(object):
|
|||||||
|
|
||||||
if vfs.lim:
|
if vfs.lim:
|
||||||
ap2, cj["prel"] = vfs.lim.all(
|
ap2, cj["prel"] = vfs.lim.all(
|
||||||
cj["addr"], cj["prel"], cj["size"], ap1, reg
|
cj["addr"],
|
||||||
|
cj["prel"],
|
||||||
|
cj["size"],
|
||||||
|
cj["ptop"],
|
||||||
|
ap1,
|
||||||
|
self.hub.broker,
|
||||||
|
reg,
|
||||||
|
"up2k._get_volsize",
|
||||||
)
|
)
|
||||||
bos.makedirs(ap2)
|
bos.makedirs(ap2)
|
||||||
vfs.lim.nup(cj["addr"])
|
vfs.lim.nup(cj["addr"])
|
||||||
@@ -2736,7 +2790,7 @@ class Up2k(object):
|
|||||||
|
|
||||||
self._symlink(dst, d2, self.flags[ptop], lmod=lmod)
|
self._symlink(dst, d2, self.flags[ptop], lmod=lmod)
|
||||||
if cur:
|
if cur:
|
||||||
self.db_rm(cur, rd, fn)
|
self.db_rm(cur, rd, fn, job["size"])
|
||||||
self.db_add(cur, vflags, rd, fn, lmod, *z2[3:])
|
self.db_add(cur, vflags, rd, fn, lmod, *z2[3:])
|
||||||
|
|
||||||
if cur:
|
if cur:
|
||||||
@@ -2779,7 +2833,7 @@ class Up2k(object):
|
|||||||
|
|
||||||
self.db_act = self.vol_act[ptop] = time.time()
|
self.db_act = self.vol_act[ptop] = time.time()
|
||||||
try:
|
try:
|
||||||
self.db_rm(cur, rd, fn)
|
self.db_rm(cur, rd, fn, sz)
|
||||||
self.db_add(
|
self.db_add(
|
||||||
cur,
|
cur,
|
||||||
vflags,
|
vflags,
|
||||||
@@ -2809,13 +2863,17 @@ class Up2k(object):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def db_rm(self, db: "sqlite3.Cursor", rd: str, fn: str) -> None:
|
def db_rm(self, db: "sqlite3.Cursor", rd: str, fn: str, sz: int) -> None:
|
||||||
sql = "delete from up where rd = ? and fn = ?"
|
sql = "delete from up where rd = ? and fn = ?"
|
||||||
try:
|
try:
|
||||||
db.execute(sql, (rd, fn))
|
r = db.execute(sql, (rd, fn))
|
||||||
except:
|
except:
|
||||||
assert self.mem_cur
|
assert self.mem_cur
|
||||||
db.execute(sql, s3enc(self.mem_cur, rd, fn))
|
r = db.execute(sql, s3enc(self.mem_cur, rd, fn))
|
||||||
|
|
||||||
|
if r.rowcount:
|
||||||
|
self.volsize[db] -= sz
|
||||||
|
self.volnfiles[db] -= 1
|
||||||
|
|
||||||
def db_add(
|
def db_add(
|
||||||
self,
|
self,
|
||||||
@@ -2844,6 +2902,9 @@ class Up2k(object):
|
|||||||
v = (wark, int(ts), sz, rd, fn, ip or "", int(at or 0))
|
v = (wark, int(ts), sz, rd, fn, ip or "", int(at or 0))
|
||||||
db.execute(sql, v)
|
db.execute(sql, v)
|
||||||
|
|
||||||
|
self.volsize[db] += sz
|
||||||
|
self.volnfiles[db] += 1
|
||||||
|
|
||||||
xau = False if skip_xau else vflags.get("xau")
|
xau = False if skip_xau else vflags.get("xau")
|
||||||
dst = djoin(ptop, rd, fn)
|
dst = djoin(ptop, rd, fn)
|
||||||
if xau and not runhook(
|
if xau and not runhook(
|
||||||
@@ -2939,7 +3000,8 @@ class Up2k(object):
|
|||||||
permsets = [[False, True]]
|
permsets = [[False, True]]
|
||||||
vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0])
|
vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0])
|
||||||
vn, rem = vn.get_dbv(rem)
|
vn, rem = vn.get_dbv(rem)
|
||||||
_, _, _, _, dip, dat = self._find_from_vpath(vn.realpath, rem)
|
with self.mutex:
|
||||||
|
_, _, _, _, dip, dat = self._find_from_vpath(vn.realpath, rem)
|
||||||
|
|
||||||
t = "you cannot delete this: "
|
t = "you cannot delete this: "
|
||||||
if not dip:
|
if not dip:
|
||||||
@@ -2991,12 +3053,12 @@ class Up2k(object):
|
|||||||
break
|
break
|
||||||
|
|
||||||
abspath = djoin(adir, fn)
|
abspath = djoin(adir, fn)
|
||||||
|
st = bos.stat(abspath)
|
||||||
volpath = "{}/{}".format(vrem, fn).strip("/")
|
volpath = "{}/{}".format(vrem, fn).strip("/")
|
||||||
vpath = "{}/{}".format(dbv.vpath, volpath).strip("/")
|
vpath = "{}/{}".format(dbv.vpath, volpath).strip("/")
|
||||||
self.log("rm {}\n {}".format(vpath, abspath))
|
self.log("rm {}\n {}".format(vpath, abspath))
|
||||||
_ = dbv.get(volpath, uname, *permsets[0])
|
_ = dbv.get(volpath, uname, *permsets[0])
|
||||||
if xbd:
|
if xbd:
|
||||||
st = bos.stat(abspath)
|
|
||||||
if not runhook(
|
if not runhook(
|
||||||
self.log,
|
self.log,
|
||||||
xbd,
|
xbd,
|
||||||
@@ -3020,14 +3082,26 @@ class Up2k(object):
|
|||||||
try:
|
try:
|
||||||
ptop = dbv.realpath
|
ptop = dbv.realpath
|
||||||
cur, wark, _, _, _, _ = self._find_from_vpath(ptop, volpath)
|
cur, wark, _, _, _, _ = self._find_from_vpath(ptop, volpath)
|
||||||
self._forget_file(ptop, volpath, cur, wark, True)
|
self._forget_file(ptop, volpath, cur, wark, True, st.st_size)
|
||||||
finally:
|
finally:
|
||||||
if cur:
|
if cur:
|
||||||
cur.connection.commit()
|
cur.connection.commit()
|
||||||
|
|
||||||
bos.unlink(abspath)
|
bos.unlink(abspath)
|
||||||
if xad:
|
if xad:
|
||||||
runhook(self.log, xad, abspath, vpath, "", uname, 0, 0, ip, 0, "")
|
runhook(
|
||||||
|
self.log,
|
||||||
|
xad,
|
||||||
|
abspath,
|
||||||
|
vpath,
|
||||||
|
"",
|
||||||
|
uname,
|
||||||
|
st.st_mtime,
|
||||||
|
st.st_size,
|
||||||
|
ip,
|
||||||
|
0,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
if is_dir:
|
if is_dir:
|
||||||
ok, ng = rmdirs(self.log_func, scandir, True, atop, 1)
|
ok, ng = rmdirs(self.log_func, scandir, True, atop, 1)
|
||||||
@@ -3203,7 +3277,7 @@ class Up2k(object):
|
|||||||
if c2 and c2 != c1:
|
if c2 and c2 != c1:
|
||||||
self._copy_tags(c1, c2, w)
|
self._copy_tags(c1, c2, w)
|
||||||
|
|
||||||
self._forget_file(svn.realpath, srem, c1, w, c1 != c2)
|
self._forget_file(svn.realpath, srem, c1, w, c1 != c2, fsize)
|
||||||
self._relink(w, svn.realpath, srem, dabs)
|
self._relink(w, svn.realpath, srem, dabs)
|
||||||
curs.add(c1)
|
curs.add(c1)
|
||||||
|
|
||||||
@@ -3279,6 +3353,7 @@ class Up2k(object):
|
|||||||
cur: Optional["sqlite3.Cursor"],
|
cur: Optional["sqlite3.Cursor"],
|
||||||
wark: Optional[str],
|
wark: Optional[str],
|
||||||
drop_tags: bool,
|
drop_tags: bool,
|
||||||
|
sz: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""forgets file in db, fixes symlinks, does not delete"""
|
"""forgets file in db, fixes symlinks, does not delete"""
|
||||||
srd, sfn = vsplit(vrem)
|
srd, sfn = vsplit(vrem)
|
||||||
@@ -3293,7 +3368,7 @@ class Up2k(object):
|
|||||||
q = "delete from mt where w=?"
|
q = "delete from mt where w=?"
|
||||||
cur.execute(q, (wark[:16],))
|
cur.execute(q, (wark[:16],))
|
||||||
|
|
||||||
self.db_rm(cur, srd, sfn)
|
self.db_rm(cur, srd, sfn, sz)
|
||||||
|
|
||||||
reg = self.registry.get(ptop)
|
reg = self.registry.get(ptop)
|
||||||
if reg:
|
if reg:
|
||||||
|
|||||||
@@ -1626,7 +1626,12 @@ def unhumanize(sz: str) -> int:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
mc = sz[-1:].lower()
|
mc = sz[-1:].lower()
|
||||||
mi = {"k": 1024, "m": 1024 * 1024, "g": 1024 * 1024 * 1024}.get(mc, 1)
|
mi = {
|
||||||
|
"k": 1024,
|
||||||
|
"m": 1024 * 1024,
|
||||||
|
"g": 1024 * 1024 * 1024,
|
||||||
|
"t": 1024 * 1024 * 1024 * 1024,
|
||||||
|
}.get(mc, 1)
|
||||||
return int(float(sz[:-1]) * mi)
|
return int(float(sz[:-1]) * mi)
|
||||||
|
|
||||||
|
|
||||||
@@ -2422,7 +2427,7 @@ def killtree(root: int) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def runcmd(
|
def runcmd(
|
||||||
argv: Union[list[bytes], list[str]], timeout: Optional[int] = None, **ka: Any
|
argv: Union[list[bytes], list[str]], timeout: Optional[float] = None, **ka: Any
|
||||||
) -> tuple[int, str, str]:
|
) -> tuple[int, str, str]:
|
||||||
kill = ka.pop("kill", "t") # [t]ree [m]ain [n]one
|
kill = ka.pop("kill", "t") # [t]ree [m]ain [n]one
|
||||||
capture = ka.pop("capture", 3) # 0=none 1=stdout 2=stderr 3=both
|
capture = ka.pop("capture", 3) # 0=none 1=stdout 2=stderr 3=both
|
||||||
@@ -2475,7 +2480,7 @@ def chkcmd(argv: Union[list[bytes], list[str]], **ka: Any) -> tuple[str, str]:
|
|||||||
return sout, serr
|
return sout, serr
|
||||||
|
|
||||||
|
|
||||||
def mchkcmd(argv: Union[list[bytes], list[str]], timeout: int = 10) -> None:
|
def mchkcmd(argv: Union[list[bytes], list[str]], timeout: float = 10) -> None:
|
||||||
if PY2:
|
if PY2:
|
||||||
with open(os.devnull, "wb") as f:
|
with open(os.devnull, "wb") as f:
|
||||||
rv = sp.call(argv, stdout=f, stderr=f)
|
rv = sp.call(argv, stdout=f, stderr=f)
|
||||||
@@ -2722,6 +2727,34 @@ def runhook(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def loadpy(ap: str, hot: bool) -> Any:
|
||||||
|
"""
|
||||||
|
a nice can of worms capable of causing all sorts of bugs
|
||||||
|
depending on what other inconveniently named files happen
|
||||||
|
to be in the same folder
|
||||||
|
"""
|
||||||
|
if ap.startswith("~"):
|
||||||
|
ap = os.path.expanduser(ap)
|
||||||
|
|
||||||
|
mdir, mfile = os.path.split(absreal(ap))
|
||||||
|
mname = mfile.rsplit(".", 1)[0]
|
||||||
|
sys.path.insert(0, mdir)
|
||||||
|
|
||||||
|
if PY2:
|
||||||
|
mod = __import__(mname)
|
||||||
|
if hot:
|
||||||
|
reload(mod)
|
||||||
|
else:
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
mod = importlib.import_module(mname)
|
||||||
|
if hot:
|
||||||
|
importlib.reload(mod)
|
||||||
|
|
||||||
|
sys.path.remove(mdir)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
def gzip_orig_sz(fn: str) -> int:
|
def gzip_orig_sz(fn: str) -> int:
|
||||||
with open(fsenc(fn), "rb") as f:
|
with open(fsenc(fn), "rb") as f:
|
||||||
f.seek(-4, 2)
|
f.seek(-4, 2)
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
--u2-sbtn-b1: #999;
|
--u2-sbtn-b1: #999;
|
||||||
--u2-txt-bg: var(--bg-u5);
|
--u2-txt-bg: var(--bg-u5);
|
||||||
--u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1));
|
--u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1));
|
||||||
|
--u2-tab-b1: rgba(128,128,128,0.8);
|
||||||
--u2-tab-1-fg: #fd7;
|
--u2-tab-1-fg: #fd7;
|
||||||
--u2-tab-1-bg: linear-gradient(to bottom, var(#353), var(--bg) 80%);
|
--u2-tab-1-bg: linear-gradient(to bottom, var(#353), var(--bg) 80%);
|
||||||
--u2-tab-1-b1: #7c5;
|
--u2-tab-1-b1: #7c5;
|
||||||
@@ -270,6 +271,7 @@ html.bz {
|
|||||||
--btn-1h-fg: #000;
|
--btn-1h-fg: #000;
|
||||||
--txt-sh: a;
|
--txt-sh: a;
|
||||||
|
|
||||||
|
--u2-tab-b1: var(--bg-u5);
|
||||||
--u2-tab-1-fg: var(--fg-max);
|
--u2-tab-1-fg: var(--fg-max);
|
||||||
--u2-tab-1-bg: var(--bg);
|
--u2-tab-1-bg: var(--bg);
|
||||||
|
|
||||||
@@ -329,6 +331,7 @@ html.c {
|
|||||||
html.cz {
|
html.cz {
|
||||||
--bgg: var(--bg-u2);
|
--bgg: var(--bg-u2);
|
||||||
--srv-3: #fff;
|
--srv-3: #fff;
|
||||||
|
--u2-tab-b1: var(--bg-d3);
|
||||||
}
|
}
|
||||||
html.cy {
|
html.cy {
|
||||||
--fg: #fff;
|
--fg: #fff;
|
||||||
@@ -411,10 +414,11 @@ html.dz {
|
|||||||
--op-aa-bg: var(--bg-d2);
|
--op-aa-bg: var(--bg-d2);
|
||||||
--op-a-sh: rgba(0,0,0,0.5);
|
--op-a-sh: rgba(0,0,0,0.5);
|
||||||
|
|
||||||
--u2-btn-b1: #999;
|
--u2-btn-b1: var(--fg-weak);
|
||||||
--u2-sbtn-b1: #999;
|
--u2-sbtn-b1: var(--fg-weak);
|
||||||
--u2-txt-bg: var(--bg-u5);
|
--u2-txt-bg: var(--bg-u5);
|
||||||
--u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1));
|
--u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1));
|
||||||
|
--u2-tab-b1: var(--fg-weak);
|
||||||
--u2-tab-1-fg: #fff;
|
--u2-tab-1-fg: #fff;
|
||||||
--u2-tab-1-bg: linear-gradient(to bottom, var(#353), var(--bg) 80%);
|
--u2-tab-1-bg: linear-gradient(to bottom, var(#353), var(--bg) 80%);
|
||||||
--u2-tab-1-b1: #7c5;
|
--u2-tab-1-b1: #7c5;
|
||||||
@@ -423,6 +427,12 @@ html.dz {
|
|||||||
--u2-b-fg: #fff;
|
--u2-b-fg: #fff;
|
||||||
--u2-b1-bg: #3a3;
|
--u2-b1-bg: #3a3;
|
||||||
--u2-b2-bg: #3a3;
|
--u2-b2-bg: #3a3;
|
||||||
|
--u2-o-bg: var(--btn-bg);
|
||||||
|
--u2-o-b1: var(--bg-u5);
|
||||||
|
--u2-o-h-bg: var(--fg-weak);
|
||||||
|
--u2-o-1-bg: var(--fg-weak);
|
||||||
|
--u2-o-1-b1: var(--a);
|
||||||
|
--u2-o-1h-bg: var(--a);
|
||||||
--u2-inf-bg: #07a;
|
--u2-inf-bg: #07a;
|
||||||
--u2-inf-b1: #0be;
|
--u2-inf-b1: #0be;
|
||||||
--u2-ok-bg: #380;
|
--u2-ok-bg: #380;
|
||||||
@@ -2465,7 +2475,7 @@ html.y #bbox-overlay figcaption a {
|
|||||||
width: 21em;
|
width: 21em;
|
||||||
}
|
}
|
||||||
#u2cards {
|
#u2cards {
|
||||||
padding: 1em 1em .3em 1em;
|
padding: 1em 1em .42em 1em;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -2490,7 +2500,8 @@ html.y #bbox-overlay figcaption a {
|
|||||||
#u2cards a {
|
#u2cards a {
|
||||||
padding: .2em 1em;
|
padding: .2em 1em;
|
||||||
background: var(--u2-tab-bg);
|
background: var(--u2-tab-bg);
|
||||||
border: 1px solid rgba(128,128,128,0.8);
|
border: 1px solid #999;
|
||||||
|
border-color: var(--u2-tab-b1);
|
||||||
border-width: 0 0 1px 0;
|
border-width: 0 0 1px 0;
|
||||||
}
|
}
|
||||||
#u2cards a:first-child {
|
#u2cards a:first-child {
|
||||||
|
|||||||
@@ -3080,6 +3080,8 @@ function eval_hash() {
|
|||||||
// compact media player
|
// compact media player
|
||||||
function setacmp() {
|
function setacmp() {
|
||||||
clmod(ebi('widget'), 'cmp', props.mcmp);
|
clmod(ebi('widget'), 'cmp', props.mcmp);
|
||||||
|
pbar.onresize();
|
||||||
|
vbar.onresize();
|
||||||
}
|
}
|
||||||
bcfg_bind(props, 'mcmp', 'au_compact', false, setacmp);
|
bcfg_bind(props, 'mcmp', 'au_compact', false, setacmp);
|
||||||
setacmp();
|
setacmp();
|
||||||
@@ -5778,14 +5780,18 @@ var treectl = (function () {
|
|||||||
|
|
||||||
for (var b = 0; b < res.taglist.length; b++) {
|
for (var b = 0; b < res.taglist.length; b++) {
|
||||||
var k = res.taglist[b],
|
var k = res.taglist[b],
|
||||||
v = (tn.tags || {})[k] || "";
|
v = (tn.tags || {})[k] || "",
|
||||||
|
sv = null;
|
||||||
|
|
||||||
if (k == ".dur") {
|
if (k == ".dur")
|
||||||
var sv = v ? s2ms(v) : "";
|
sv = v ? s2ms(v) : "";
|
||||||
ln[ln.length - 1] += '</td><td sortv="' + v + '">' + sv;
|
else if (k == ".up_at")
|
||||||
|
sv = v ? unix2iso(v) : "";
|
||||||
|
else {
|
||||||
|
ln.push(v);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
ln.push(v);
|
ln[ln.length - 1] += '</td><td sortv="' + v + '">' + sv;
|
||||||
}
|
}
|
||||||
ln = ln.concat([tn.ext, unix2iso(tn.ts)]).join('</td><td>');
|
ln = ln.concat([tn.ext, unix2iso(tn.ts)]).join('</td><td>');
|
||||||
html.push(ln + '</td></tr>');
|
html.push(ln + '</td></tr>');
|
||||||
@@ -6064,7 +6070,7 @@ function apply_perms(res) {
|
|||||||
|
|
||||||
var axs = [],
|
var axs = [],
|
||||||
aclass = '>',
|
aclass = '>',
|
||||||
chk = ['read', 'write', 'move', 'delete', 'get'];
|
chk = ['read', 'write', 'move', 'delete', 'get', 'admin'];
|
||||||
|
|
||||||
for (var a = 0; a < chk.length; a++)
|
for (var a = 0; a < chk.length; a++)
|
||||||
if (has(perms, chk[a]))
|
if (has(perms, chk[a]))
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ html {
|
|||||||
#toastb {
|
#toastb {
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 1px;
|
padding: .1em;
|
||||||
}
|
}
|
||||||
#toast.scroll #toastb {
|
#toast.scroll #toastb {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
|||||||
@@ -159,8 +159,8 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
|
|||||||
window.onerror = undefined;
|
window.onerror = undefined;
|
||||||
var html = [
|
var html = [
|
||||||
'<h1>you hit a bug!</h1>',
|
'<h1>you hit a bug!</h1>',
|
||||||
'<p style="font-size:1.3em;margin:0">try to <a href="#" onclick="localStorage.clear();location.reload();">reset copyparty settings</a> if you are stuck here, or <a href="#" onclick="ignex();">ignore this</a> / <a href="#" onclick="ignex(true);">ignore all</a> / <a href="?b=u">basic</a></p>',
|
'<p style="font-size:1.3em;margin:0;line-height:2em">try to <a href="#" onclick="localStorage.clear();location.reload();">reset copyparty settings</a> if you are stuck here, or <a href="#" onclick="ignex();">ignore this</a> / <a href="#" onclick="ignex(true);">ignore all</a> / <a href="?b=u">basic</a></p>',
|
||||||
'<p style="color:#fff">please send me a screenshot arigathanks gozaimuch: <a href="<ghi>" target="_blank">github issue</a></p>',
|
'<p style="color:#fff">please send me a screenshot arigathanks gozaimuch: <a href="<ghi>" target="_blank">new github issue</a></p>',
|
||||||
'<p class="b">' + esc(url + ' @' + lineNo + ':' + columnNo), '<br />' + esc(String(msg)).replace(/\n/g, '<br />') + '</p>',
|
'<p class="b">' + esc(url + ' @' + lineNo + ':' + columnNo), '<br />' + esc(String(msg)).replace(/\n/g, '<br />') + '</p>',
|
||||||
'<p><b>UA:</b> ' + esc(navigator.userAgent + '')
|
'<p><b>UA:</b> ' + esc(navigator.userAgent + '')
|
||||||
];
|
];
|
||||||
@@ -225,7 +225,7 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
|
|||||||
'#exbox{background:#222;color:#ddd;font-family:sans-serif;font-size:0.8em;padding:0 1em 1em 1em;z-index:80386;position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%;overflow:auto;width:calc(100% - 2em)} ' +
|
'#exbox{background:#222;color:#ddd;font-family:sans-serif;font-size:0.8em;padding:0 1em 1em 1em;z-index:80386;position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%;overflow:auto;width:calc(100% - 2em)} ' +
|
||||||
'#exbox,#exbox *{line-height:1.5em;overflow-wrap:break-word} ' +
|
'#exbox,#exbox *{line-height:1.5em;overflow-wrap:break-word} ' +
|
||||||
'#exbox code{color:#bf7;background:#222;padding:.1em;margin:.2em;font-size:1.1em;font-family:monospace,monospace} ' +
|
'#exbox code{color:#bf7;background:#222;padding:.1em;margin:.2em;font-size:1.1em;font-family:monospace,monospace} ' +
|
||||||
'#exbox a{text-decoration:underline;color:#fc0} ' +
|
'#exbox a{text-decoration:underline;color:#fc0;background:#222;border:none} ' +
|
||||||
'#exbox h1{margin:.5em 1em 0 0;padding:0} ' +
|
'#exbox h1{margin:.5em 1em 0 0;padding:0} ' +
|
||||||
'#exbox p.b{border-top:1px solid #999;margin:1em 0 0 0;font-size:1em} ' +
|
'#exbox p.b{border-top:1px solid #999;margin:1em 0 0 0;font-size:1em} ' +
|
||||||
'#exbox ul, #exbox li {margin:0 0 0 .5em;padding:0} ' +
|
'#exbox ul, #exbox li {margin:0 0 0 .5em;padding:0} ' +
|
||||||
@@ -1786,16 +1786,17 @@ function xhrchk(xhr, prefix, e404, lvl, tag) {
|
|||||||
if (xhr.status < 400 && xhr.status >= 200)
|
if (xhr.status < 400 && xhr.status >= 200)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (xhr.status == 403)
|
var errtxt = (xhr.response && xhr.response.err) || xhr.responseText,
|
||||||
|
fun = toast[lvl || 'err'],
|
||||||
|
is_cf = /[Cc]loud[f]lare|>Just a mo[m]ent|#cf-b[u]bbles|Chec[k]ing your br[o]wser|\/chall[e]nge-platform|"chall[e]nge-error|nable Ja[v]aScript and cook/.test(errtxt);
|
||||||
|
|
||||||
|
if (xhr.status == 403 && !is_cf)
|
||||||
return toast.err(0, prefix + (L && L.xhr403 || "403: access denied\n\ntry pressing F5, maybe you got logged out"), tag);
|
return toast.err(0, prefix + (L && L.xhr403 || "403: access denied\n\ntry pressing F5, maybe you got logged out"), tag);
|
||||||
|
|
||||||
if (xhr.status == 404)
|
if (xhr.status == 404)
|
||||||
return toast.err(0, prefix + e404, tag);
|
return toast.err(0, prefix + e404, tag);
|
||||||
|
|
||||||
var errtxt = (xhr.response && xhr.response.err) || xhr.responseText,
|
if (is_cf && (xhr.status == 403 || xhr.status == 503)) {
|
||||||
fun = toast[lvl || 'err'];
|
|
||||||
|
|
||||||
if (xhr.status == 503 && /[Cc]loud[f]lare|>Just a mo[m]ent|#cf-b[u]bbles|Chec[k]ing your br[o]wser/.test(errtxt)) {
|
|
||||||
var now = Date.now(), td = now - cf_cha_t;
|
var now = Date.now(), td = now - cf_cha_t;
|
||||||
if (td < 15000)
|
if (td < 15000)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,3 +1,76 @@
|
|||||||
|
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||||
|
# 2023-0707-2220 `v1.8.1` in case of 404
|
||||||
|
|
||||||
|
## new features
|
||||||
|
* [handlers](https://github.com/9001/copyparty/tree/hovudstraum/bin/handlers); change the behavior of 404 / 403 with plugins
|
||||||
|
* makes it possible to use copyparty as a [caching proxy](https://github.com/9001/copyparty/blob/hovudstraum/bin/handlers/caching-proxy.py)
|
||||||
|
* #42 add mpv + streamlink support to [very-bad-idea](https://github.com/9001/copyparty/tree/hovudstraum/bin/mtag#dangerous-plugins)
|
||||||
|
* add support for Pillow 10
|
||||||
|
* also improved text rendering in icons
|
||||||
|
* mention the [fedora package](https://github.com/9001/copyparty#fedora-package) in the readme
|
||||||
|
|
||||||
|
## bugfixes
|
||||||
|
* theme 6 (hacker) didn't show the state of some toggle-switches
|
||||||
|
* windows: keep quickedit enabled when hashing passwords interactively
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||||
|
# 2023-0626-0005 `v1.8.0` argon
|
||||||
|
|
||||||
|
News: if you use rclone as a copyparty webdav client, upgrading to [rclone v1.63](https://github.com/rclone/rclone/releases/tag/v1.63.0) (just released) will give you [a huge speed boost](https://github.com/rclone/rclone/pull/6897) for small files
|
||||||
|
|
||||||
|
## new features
|
||||||
|
* #39 hashed passwords
|
||||||
|
* instead of keeping plaintext account passwords in config files, you can now store hashed ones instead
|
||||||
|
* `--ah-alg` specifies algorithm; best to worst: `argon2`, `scrypt`, `sha2`, or the default `none`
|
||||||
|
* the default settings of each algorithm takes `0.4 sec` to hash a password, and argon2 eats `256 MiB` RAM
|
||||||
|
* can be adjusted with optional comma-separated args after the algorithm name; see `--help-pwhash`
|
||||||
|
* `--ah-salt` is the [static salt](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hashed-passwords) for all passwords, and is autogenerated-and-persisted if not specified
|
||||||
|
* `--ah-cli` switches copyparty into a shell where you can hash passwords interactively
|
||||||
|
* but copyparty will also autoconvert any unhashed passwords on startup and give you the values to insert into the config anyways
|
||||||
|
* #40 volume size limit
|
||||||
|
* volflag `vmaxb` specifies max size of a volume
|
||||||
|
* volflag `vmaxn` specifies max number of files in a volume
|
||||||
|
* example: `-v [...]:c,vmaxb=900g:c,vmaxn=20k` blocks uploads if the volume reaches 900 GiB or a total of 20480 files
|
||||||
|
* good alternative to `--df` since it works per-volume
|
||||||
|
|
||||||
|
## bugfixes
|
||||||
|
* autogenerated TLS certs didn't include the mDNS name
|
||||||
|
|
||||||
|
## other changes
|
||||||
|
* improved cloudflare challenge detection
|
||||||
|
* markdown edits will now trigger upload hooks
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||||
|
# 2023-0611-0814 `v1.7.6` NO_COLOR
|
||||||
|
|
||||||
|
## new features
|
||||||
|
* #31 `--grid` shows thumbnails instead of file-list by default
|
||||||
|
* #28 `--unlist` regex-exclude files from browser listings
|
||||||
|
* for example `--unlist '\.(js|css)$'` hides all `.js` and `.css` files
|
||||||
|
* **purely cosmetic!** the files are still fully accessible, and still appear in API calls
|
||||||
|
* auto-generate TLS certificates on startup / network-change
|
||||||
|
* mostly good for LAN, requires [cfssl](https://github.com/cloudflare/cfssl/releases/latest), can be disabled with `--no-crt`
|
||||||
|
* creates a self-signed CA and certs with SANs of all detected server IPs
|
||||||
|
* so it's still recommended to use a reverse-proxy / letsencrypt for WAN servers
|
||||||
|
* the default `--fk-salt` is now much stronger
|
||||||
|
* all existing installations will keep the previously selected seed -- you can choose to upgrade by deleting `~/.config/copyparty/cert.pem` but this will change all filekeys / per-file passwords
|
||||||
|
* the `NO_COLOR` environment-variable is now supported, removing colors from stdout
|
||||||
|
* see https://no-color.org/ and more importantly https://youtu.be/biW5UVGkPMA?t=150
|
||||||
|
* `--ansi` and `--no-ansi` can also be used to force-enable/disable colored output
|
||||||
|
* #33 disable colors when stdout is redirected to a pipe/file -- by @clach04
|
||||||
|
* #32 simplify building sfx from source
|
||||||
|
* upgraded [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) to [python 3.11.4](https://pythoninsider.blogspot.com/2023/06/python-3114-31012-3917-3817-3717-and.html)
|
||||||
|
|
||||||
|
## bugfixes
|
||||||
|
* #30 `--ftps` didn't work without `--ftp`
|
||||||
|
* tiny css bug in light themes (opaque thumbnail controls)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||||
# 2023-0513-0000 `v1.7.2` hard resolve
|
# 2023-0513-0000 `v1.7.2` hard resolve
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
* [future plans](#future-plans) - some improvement ideas
|
* [future plans](#future-plans) - some improvement ideas
|
||||||
* [design](#design)
|
* [design](#design)
|
||||||
* [up2k](#up2k) - quick outline of the up2k protocol
|
* [up2k](#up2k) - quick outline of the up2k protocol
|
||||||
* [why not tus](#why-not-tus) - I didn't know about [tus](https://tus.io/)
|
* [why not tus](#why-not-tus) - I didn't know about [tus](https://tus.io/)
|
||||||
* [why chunk-hashes](#why-chunk-hashes) - a single sha512 would be better, right?
|
* [why chunk-hashes](#why-chunk-hashes) - a single sha512 would be better, right?
|
||||||
|
* [hashed passwords](#hashed-passwords) - regarding the curious decisions
|
||||||
* [http api](#http-api)
|
* [http api](#http-api)
|
||||||
* [read](#read)
|
* [read](#read)
|
||||||
* [write](#write)
|
* [write](#write)
|
||||||
@@ -68,14 +69,14 @@ regarding the frequent server log message during uploads;
|
|||||||
* on this http connection, `2.77 GiB` transferred, `102.9 MiB/s` average, `948` chunks handled
|
* on this http connection, `2.77 GiB` transferred, `102.9 MiB/s` average, `948` chunks handled
|
||||||
* client says `4` uploads OK, `0` failed, `3` busy, `1` queued, `10042 MiB` total size, `7198 MiB` and `00:01:09` left
|
* client says `4` uploads OK, `0` failed, `3` busy, `1` queued, `10042 MiB` total size, `7198 MiB` and `00:01:09` left
|
||||||
|
|
||||||
## why not tus
|
### why not tus
|
||||||
|
|
||||||
I didn't know about [tus](https://tus.io/) when I made this, but:
|
I didn't know about [tus](https://tus.io/) when I made this, but:
|
||||||
* up2k has the advantage that it supports parallel uploading of non-contiguous chunks straight into the final file -- [tus does a merge at the end](https://tus.io/protocols/resumable-upload.html#concatenation) which is slow and taxing on the server HDD / filesystem (unless i'm misunderstanding)
|
* up2k has the advantage that it supports parallel uploading of non-contiguous chunks straight into the final file -- [tus does a merge at the end](https://tus.io/protocols/resumable-upload.html#concatenation) which is slow and taxing on the server HDD / filesystem (unless i'm misunderstanding)
|
||||||
* up2k has the slight disadvantage of requiring the client to hash the entire file before an upload can begin, but this has the benefit of immediately skipping duplicate files
|
* up2k has the slight disadvantage of requiring the client to hash the entire file before an upload can begin, but this has the benefit of immediately skipping duplicate files
|
||||||
* and the hashing happens in a separate thread anyways so it's usually not a bottleneck
|
* and the hashing happens in a separate thread anyways so it's usually not a bottleneck
|
||||||
|
|
||||||
## why chunk-hashes
|
### why chunk-hashes
|
||||||
|
|
||||||
a single sha512 would be better, right?
|
a single sha512 would be better, right?
|
||||||
|
|
||||||
@@ -92,6 +93,15 @@ hashwasm would solve the streaming issue but reduces hashing speed for sha512 (x
|
|||||||
* blake2 might be a better choice since xxh is non-cryptographic, but that gets ~15 MiB/s on slower androids
|
* blake2 might be a better choice since xxh is non-cryptographic, but that gets ~15 MiB/s on slower androids
|
||||||
|
|
||||||
|
|
||||||
|
# hashed passwords
|
||||||
|
|
||||||
|
regarding the curious decisions
|
||||||
|
|
||||||
|
there is a static salt for all passwords;
|
||||||
|
* because most copyparty APIs allow users to authenticate using only their password, making the username unknown, so impossible to do per-account salts
|
||||||
|
* the drawback of this is that an attacker can bruteforce all accounts in parallel, however most copyparty instances only have a handful of accounts in the first place, and it can be compensated by increasing the hashing cost anyways
|
||||||
|
|
||||||
|
|
||||||
# http api
|
# http api
|
||||||
|
|
||||||
* table-column `params` = URL parameters; `?foo=bar&qux=...`
|
* table-column `params` = URL parameters; `?foo=bar&qux=...`
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ thumbnails2 = ["pyvips"]
|
|||||||
audiotags = ["mutagen"]
|
audiotags = ["mutagen"]
|
||||||
ftpd = ["pyftpdlib"]
|
ftpd = ["pyftpdlib"]
|
||||||
ftps = ["pyftpdlib", "pyopenssl"]
|
ftps = ["pyftpdlib", "pyopenssl"]
|
||||||
|
pwhash = ["argon2-cffi"]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
copyparty = "copyparty.__main__:main"
|
copyparty = "copyparty.__main__:main"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ ENV PYTHONPYCACHEPREFIX=/tmp/pyc
|
|||||||
|
|
||||||
RUN apk --no-cache add !pyc \
|
RUN apk --no-cache add !pyc \
|
||||||
wget \
|
wget \
|
||||||
py3-pillow \
|
py3-argon2-cffi py3-pillow \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
&& rm -rf /tmp/pyc \
|
&& rm -rf /tmp/pyc \
|
||||||
&& mkdir /cfg /w \
|
&& mkdir /cfg /w \
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ COPY i/bin/mtag/audio-bpm.py /mtag/
|
|||||||
COPY i/bin/mtag/audio-key.py /mtag/
|
COPY i/bin/mtag/audio-key.py /mtag/
|
||||||
RUN apk add -U !pyc \
|
RUN apk add -U !pyc \
|
||||||
wget \
|
wget \
|
||||||
py3-pillow py3-pip py3-cffi \
|
py3-argon2-cffi py3-pillow py3-pip py3-cffi \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
vips-jxl vips-heif vips-poppler vips-magick \
|
vips-jxl vips-heif vips-poppler vips-magick \
|
||||||
py3-numpy fftw libsndfile \
|
py3-numpy fftw libsndfile \
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ ENV PYTHONPYCACHEPREFIX=/tmp/pyc
|
|||||||
|
|
||||||
RUN apk --no-cache add !pyc \
|
RUN apk --no-cache add !pyc \
|
||||||
wget \
|
wget \
|
||||||
py3-pillow py3-mutagen \
|
py3-argon2-cffi py3-pillow py3-mutagen \
|
||||||
&& rm -rf /tmp/pyc \
|
&& rm -rf /tmp/pyc \
|
||||||
&& mkdir /cfg /w \
|
&& mkdir /cfg /w \
|
||||||
&& chmod 777 /cfg /w \
|
&& chmod 777 /cfg /w \
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ ENV PYTHONPYCACHEPREFIX=/tmp/pyc
|
|||||||
|
|
||||||
RUN apk add -U !pyc \
|
RUN apk add -U !pyc \
|
||||||
wget \
|
wget \
|
||||||
py3-pillow py3-pip py3-cffi \
|
py3-argon2-cffi py3-pillow py3-pip py3-cffi \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
vips-jxl vips-heif vips-poppler vips-magick \
|
vips-jxl vips-heif vips-poppler vips-magick \
|
||||||
&& apk add -t .bd \
|
&& apk add -t .bd \
|
||||||
|
|||||||
@@ -26,5 +26,5 @@ ba91ab0518c61eff13e5612d9e6b532940813f6b56e6ed81ea6c7c4d45acee4d98136a383a250675
|
|||||||
00558cca2e0ac813d404252f6e5aeacb50546822ecb5d0570228b8ddd29d94e059fbeb6b90393dee5abcddaca1370aca784dc9b095cbb74e980b3c024767fb24 Jinja2-3.1.2-py3-none-any.whl
|
00558cca2e0ac813d404252f6e5aeacb50546822ecb5d0570228b8ddd29d94e059fbeb6b90393dee5abcddaca1370aca784dc9b095cbb74e980b3c024767fb24 Jinja2-3.1.2-py3-none-any.whl
|
||||||
7f8f4daa4f4f2dbf24cdd534b2952ee3fba6334eb42b37465ccda3aa1cccc3d6204aa6bfffb8a83bf42ec59c702b5b5247d4c8ee0d4df906334ae53072ef8c4c MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl
|
7f8f4daa4f4f2dbf24cdd534b2952ee3fba6334eb42b37465ccda3aa1cccc3d6204aa6bfffb8a83bf42ec59c702b5b5247d4c8ee0d4df906334ae53072ef8c4c MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl
|
||||||
4a20aeb52d4fde6aabcba05ee261595eeb5482c72ee27332690f34dd6e7a49c0b3ba3813202ac15c9d21e29f1cd803f2e79ccc1c45ec314fcd0a937016bcbc56 mutagen-1.46.0-py3-none-any.whl
|
4a20aeb52d4fde6aabcba05ee261595eeb5482c72ee27332690f34dd6e7a49c0b3ba3813202ac15c9d21e29f1cd803f2e79ccc1c45ec314fcd0a937016bcbc56 mutagen-1.46.0-py3-none-any.whl
|
||||||
78414808cb9a5fa74e7b23360b8f46147952530e3cc78a3ad4b80be3e26598080537ac691a1be1f35b7428a22c1f65a6adf45986da2752fbe9d9819d77a58bf8 Pillow-9.5.0-cp311-cp311-win_amd64.whl
|
926d408a886059a75cf12706fa061146f9f042b27fb6e65be7d49f398ed23fb0227639d84804586ac014c6bcf7d08cd86a09c1a20793d341aa0802d3d32a546b Pillow-10.0.0-cp311-cp311-win_amd64.whl
|
||||||
a48ee8992eee60a0d620dced71b9f96596f5dd510e3024015aca55884cdb3f9e2405734bfc13f3f40b79106a77bc442cce02ac4c8f5d16207448052b368fd52a python-3.11.4-amd64.exe
|
a48ee8992eee60a0d620dced71b9f96596f5dd510e3024015aca55884cdb3f9e2405734bfc13f3f40b79106a77bc442cce02ac4c8f5d16207448052b368fd52a python-3.11.4-amd64.exe
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ copyparty/ico.py,
|
|||||||
copyparty/mdns.py,
|
copyparty/mdns.py,
|
||||||
copyparty/mtag.py,
|
copyparty/mtag.py,
|
||||||
copyparty/multicast.py,
|
copyparty/multicast.py,
|
||||||
|
copyparty/pwhash.py,
|
||||||
copyparty/res,
|
copyparty/res,
|
||||||
copyparty/res/__init__.py,
|
copyparty/res/__init__.py,
|
||||||
copyparty/res/COPYING.txt,
|
copyparty/res/COPYING.txt,
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ cat $f | awk '
|
|||||||
h=0
|
h=0
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/```/{o=!o}
|
||||||
|
o{next}
|
||||||
/^#/{s=1;rs=0;pr()}
|
/^#/{s=1;rs=0;pr()}
|
||||||
/^#* *(nix package)/{rs=1}
|
/^#* *(nix package)/{rs=1}
|
||||||
/^#* *(install on android|dev env setup|just the sfx|complete release|optional gpl stuff|nixos module)|`$/{s=rs}
|
/^#* *(install on android|dev env setup|just the sfx|complete release|optional gpl stuff|nixos module)|`$/{s=rs}
|
||||||
|
|||||||
1
setup.py
1
setup.py
@@ -140,6 +140,7 @@ args = {
|
|||||||
"audiotags": ["mutagen"],
|
"audiotags": ["mutagen"],
|
||||||
"ftpd": ["pyftpdlib"],
|
"ftpd": ["pyftpdlib"],
|
||||||
"ftps": ["pyftpdlib", "pyopenssl"],
|
"ftps": ["pyftpdlib", "pyopenssl"],
|
||||||
|
"pwhash": ["argon2-cffi"],
|
||||||
},
|
},
|
||||||
"entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]},
|
"entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]},
|
||||||
"scripts": ["bin/partyfuse.py", "bin/u2c.py"],
|
"scripts": ["bin/partyfuse.py", "bin/u2c.py"],
|
||||||
|
|||||||
@@ -178,9 +178,9 @@ class TestVFS(unittest.TestCase):
|
|||||||
self.assertEqual(n.realpath, os.path.join(td, "a"))
|
self.assertEqual(n.realpath, os.path.join(td, "a"))
|
||||||
self.assertAxs(n.axs.uread, ["*"])
|
self.assertAxs(n.axs.uread, ["*"])
|
||||||
self.assertAxs(n.axs.uwrite, [])
|
self.assertAxs(n.axs.uwrite, [])
|
||||||
perm_na = (False, False, False, False, False, False)
|
perm_na = (False, False, False, False, False, False, False)
|
||||||
perm_rw = (True, True, False, False, False, False)
|
perm_rw = (True, True, False, False, False, False, False)
|
||||||
perm_ro = (True, False, False, False, False, False)
|
perm_ro = (True, False, False, False, False, False, False)
|
||||||
self.assertEqual(vfs.can_access("/", "*"), perm_na)
|
self.assertEqual(vfs.can_access("/", "*"), perm_na)
|
||||||
self.assertEqual(vfs.can_access("/", "k"), perm_rw)
|
self.assertEqual(vfs.can_access("/", "k"), perm_rw)
|
||||||
self.assertEqual(vfs.can_access("/a", "*"), perm_ro)
|
self.assertEqual(vfs.can_access("/a", "*"), perm_ro)
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class Cfg(Namespace):
|
|||||||
def __init__(self, a=None, v=None, c=None):
|
def __init__(self, a=None, v=None, c=None):
|
||||||
ka = {}
|
ka = {}
|
||||||
|
|
||||||
ex = "daw dav_auth dav_inf dav_mac dav_rt dotsrch e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_thumb no_vthumb no_zip nrand nw rand vc xdev xlink xvol"
|
ex = "daw dav_auth dav_inf dav_mac dav_rt dotsrch e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_thumb no_vthumb no_zip nrand nw rand smb th_no_crop vc xdev xlink xvol"
|
||||||
ka.update(**{k: False for k in ex.split()})
|
ka.update(**{k: False for k in ex.split()})
|
||||||
|
|
||||||
ex = "dotpart no_rescan no_sendfile no_voldump plain_ip"
|
ex = "dotpart no_rescan no_sendfile no_voldump plain_ip"
|
||||||
@@ -107,16 +107,16 @@ class Cfg(Namespace):
|
|||||||
ex = "css_browser hist js_browser no_forget no_hash no_idx"
|
ex = "css_browser hist js_browser no_forget no_hash no_idx"
|
||||||
ka.update(**{k: None for k in ex.split()})
|
ka.update(**{k: None for k in ex.split()})
|
||||||
|
|
||||||
ex = "s_thead s_tbody"
|
ex = "s_thead s_tbody th_convt"
|
||||||
ka.update(**{k: 9 for k in ex.split()})
|
ka.update(**{k: 9 for k in ex.split()})
|
||||||
|
|
||||||
ex = "df loris re_maxage rproxy rsp_jtr rsp_slp s_wr_slp theme themes turbo"
|
ex = "df loris re_maxage rproxy rsp_jtr rsp_slp s_wr_slp theme themes turbo"
|
||||||
ka.update(**{k: 0 for k in ex.split()})
|
ka.update(**{k: 0 for k in ex.split()})
|
||||||
|
|
||||||
ex = "doctitle favico html_head lg_sbf log_fk md_sbf mth textfiles unlist R RS SR"
|
ex = "ah_alg doctitle favico html_head lg_sbf log_fk md_sbf mth textfiles unlist R RS SR"
|
||||||
ka.update(**{k: "" for k in ex.split()})
|
ka.update(**{k: "" for k in ex.split()})
|
||||||
|
|
||||||
ex = "xad xar xau xbd xbr xbu xiu xm"
|
ex = "on403 on404 xad xar xau xban xbd xbr xbu xiu xm"
|
||||||
ka.update(**{k: [] for k in ex.split()})
|
ka.update(**{k: [] for k in ex.split()})
|
||||||
|
|
||||||
super(Cfg, self).__init__(
|
super(Cfg, self).__init__(
|
||||||
@@ -126,6 +126,7 @@ class Cfg(Namespace):
|
|||||||
E=E,
|
E=E,
|
||||||
dbd="wal",
|
dbd="wal",
|
||||||
s_wr_sz=512 * 1024,
|
s_wr_sz=512 * 1024,
|
||||||
|
th_size="320x256",
|
||||||
unpost=600,
|
unpost=600,
|
||||||
u2sort="s",
|
u2sort="s",
|
||||||
mtp=[],
|
mtp=[],
|
||||||
|
|||||||
Reference in New Issue
Block a user