mirror of
https://github.com/9001/copyparty.git
synced 2025-08-18 09:22:31 -06:00
Compare commits
No commits in common. "hovudstraum" and "v1.13.1" have entirely different histories.
hovudstrau
...
v1.13.1
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -8,42 +8,33 @@ assignees: '9001'
|
||||||
---
|
---
|
||||||
|
|
||||||
NOTE:
|
NOTE:
|
||||||
**please use english, or include an english translation.** aside from that,
|
|
||||||
all of the below are optional, consider them as inspiration, delete and rewrite at will, thx md
|
all of the below are optional, consider them as inspiration, delete and rewrite at will, thx md
|
||||||
|
|
||||||
|
|
||||||
### Describe the bug
|
**Describe the bug**
|
||||||
a description of what the bug is
|
a description of what the bug is
|
||||||
|
|
||||||
### To Reproduce
|
**To Reproduce**
|
||||||
List of steps to reproduce the issue, or, if it's hard to reproduce, then at least a detailed explanation of what you did to run into it
|
List of steps to reproduce the issue, or, if it's hard to reproduce, then at least a detailed explanation of what you did to run into it
|
||||||
|
|
||||||
### Expected behavior
|
**Expected behavior**
|
||||||
a description of what you expected to happen
|
a description of what you expected to happen
|
||||||
|
|
||||||
### Screenshots
|
**Screenshots**
|
||||||
if applicable, add screenshots to help explain your problem, such as the kickass crashpage :^)
|
if applicable, add screenshots to help explain your problem, such as the kickass crashpage :^)
|
||||||
|
|
||||||
### Server details (if you are using docker/podman)
|
**Server details**
|
||||||
remove the ones that are not relevant:
|
if the issue is possibly on the server-side, then mention some of the following:
|
||||||
* **server OS / version:**
|
* server OS / version:
|
||||||
* **how you're running copyparty:** (docker/podman/something-else)
|
* python version:
|
||||||
* **docker image:** (variant, version, and arch if you know)
|
* copyparty arguments:
|
||||||
* **copyparty arguments and/or config-file:**
|
* filesystem (`lsblk -f` on linux):
|
||||||
|
|
||||||
### Server details (if you're NOT using docker/podman)
|
**Client details**
|
||||||
remove the ones that are not relevant:
|
|
||||||
* **server OS / version:**
|
|
||||||
* **what copyparty did you grab:** (sfx/exe/pip/arch/...)
|
|
||||||
* **how you're running it:** (in a terminal, as a systemd-service, ...)
|
|
||||||
* run copyparty with `--version` and grab the last 3 lines (they start with `copyparty`, `CPython`, `sqlite`) and paste them below this line:
|
|
||||||
* **copyparty arguments and/or config-file:**
|
|
||||||
|
|
||||||
### Client details
|
|
||||||
if the issue is possibly on the client-side, then mention some of the following:
|
if the issue is possibly on the client-side, then mention some of the following:
|
||||||
* the device type and model:
|
* the device type and model:
|
||||||
* OS version:
|
* OS version:
|
||||||
* browser version:
|
* browser version:
|
||||||
|
|
||||||
### Additional context
|
**Additional context**
|
||||||
any other context about the problem here
|
any other context about the problem here
|
||||||
|
|
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -7,8 +7,6 @@ assignees: '9001'
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
NOTE:
|
|
||||||
**please use english, or include an english translation.** aside from that,
|
|
||||||
all of the below are optional, consider them as inspiration, delete and rewrite at will
|
all of the below are optional, consider them as inspiration, delete and rewrite at will
|
||||||
|
|
||||||
**is your feature request related to a problem? Please describe.**
|
**is your feature request related to a problem? Please describe.**
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -30,7 +30,6 @@ copyparty/res/COPYING.txt
|
||||||
copyparty/web/deps/
|
copyparty/web/deps/
|
||||||
srv/
|
srv/
|
||||||
scripts/docker/i/
|
scripts/docker/i/
|
||||||
scripts/deps-docker/uncomment.py
|
|
||||||
contrib/package/arch/pkg/
|
contrib/package/arch/pkg/
|
||||||
contrib/package/arch/src/
|
contrib/package/arch/src/
|
||||||
|
|
||||||
|
@ -43,6 +42,3 @@ scripts/docker/*.err
|
||||||
|
|
||||||
# nix build output link
|
# nix build output link
|
||||||
result
|
result
|
||||||
|
|
||||||
# IDEA config
|
|
||||||
.idea/
|
|
||||||
|
|
24
.vscode/settings.json
vendored
24
.vscode/settings.json
vendored
|
@ -22,9 +22,6 @@
|
||||||
"terminal.ansiBrightCyan": "#9cf0ed",
|
"terminal.ansiBrightCyan": "#9cf0ed",
|
||||||
"terminal.ansiBrightWhite": "#ffffff",
|
"terminal.ansiBrightWhite": "#ffffff",
|
||||||
},
|
},
|
||||||
"python.terminal.activateEnvironment": false,
|
|
||||||
"python.analysis.enablePytestSupport": false,
|
|
||||||
"python.analysis.typeCheckingMode": "standard",
|
|
||||||
"python.testing.pytestEnabled": false,
|
"python.testing.pytestEnabled": false,
|
||||||
"python.testing.unittestEnabled": true,
|
"python.testing.unittestEnabled": true,
|
||||||
"python.testing.unittestArgs": [
|
"python.testing.unittestArgs": [
|
||||||
|
@ -34,8 +31,23 @@
|
||||||
"-p",
|
"-p",
|
||||||
"test_*.py"
|
"test_*.py"
|
||||||
],
|
],
|
||||||
// python3 -m isort --py=27 --profile=black ~/dev/copyparty/{copyparty,tests}/*.py && python3 -m black -t py27 ~/dev/copyparty/{copyparty,tests,bin}/*.py $(find ~/dev/copyparty/copyparty/stolen -iname '*.py')
|
"python.linting.pylintEnabled": true,
|
||||||
"editor.formatOnSave": false,
|
"python.linting.flake8Enabled": true,
|
||||||
|
"python.linting.banditEnabled": true,
|
||||||
|
"python.linting.mypyEnabled": true,
|
||||||
|
"python.linting.flake8Args": [
|
||||||
|
"--max-line-length=120",
|
||||||
|
"--ignore=E722,F405,E203,W503,W293,E402,E501,E128,E226",
|
||||||
|
],
|
||||||
|
"python.linting.banditArgs": [
|
||||||
|
"--ignore=B104,B110,B112"
|
||||||
|
],
|
||||||
|
// python3 -m isort --py=27 --profile=black copyparty/
|
||||||
|
"python.formatting.provider": "none",
|
||||||
|
"[python]": {
|
||||||
|
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||||
|
},
|
||||||
|
"editor.formatOnSave": true,
|
||||||
"[html]": {
|
"[html]": {
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"editor.autoIndent": "keep",
|
"editor.autoIndent": "keep",
|
||||||
|
@ -46,4 +58,6 @@
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
"*.makefile": "makefile"
|
"*.makefile": "makefile"
|
||||||
},
|
},
|
||||||
|
"python.linting.enabled": true,
|
||||||
|
"python.pythonPath": "/usr/bin/python3"
|
||||||
}
|
}
|
|
@ -1,21 +1,8 @@
|
||||||
* **found a bug?** [create an issue!](https://github.com/9001/copyparty/issues) or let me know in the [discord](https://discord.gg/25J8CdTT6G) :>
|
* do something cool
|
||||||
* **fixed a bug?** create a PR or post a patch! big thx in advance :>
|
|
||||||
* **have a cool idea?** let's discuss it! anywhere's fine, you choose.
|
|
||||||
|
|
||||||
but please:
|
really tho, send a PR or an issue or whatever, all appreciated, anything goes, just behave aight 👍👍
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# do not use AI / LMM when writing code
|
|
||||||
|
|
||||||
copyparty is 100% organic, free-range, human-written software!
|
|
||||||
|
|
||||||
> ⚠ you are now entering a no-copilot zone
|
|
||||||
|
|
||||||
the *only* place where LMM/AI *may* be accepted is for [localization](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) if you are fluent and have confirmed that the translation is accurate.
|
|
||||||
|
|
||||||
sorry for the harsh tone, but this is important to me 🙏
|
|
||||||
|
|
||||||
|
but to be more specific,
|
||||||
|
|
||||||
|
|
||||||
# contribution ideas
|
# contribution ideas
|
||||||
|
@ -41,8 +28,6 @@ aside from documentation and ideas, some other things that would be cool to have
|
||||||
|
|
||||||
* **translations** -- the copyparty web-UI has translations for english and norwegian at the top of [browser.js](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/web/browser.js); if you'd like to add a translation for another language then that'd be welcome! and if that language has a grammar that doesn't fit into the way the strings are assembled, then we'll fix that as we go :>
|
* **translations** -- the copyparty web-UI has translations for english and norwegian at the top of [browser.js](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/web/browser.js); if you'd like to add a translation for another language then that'd be welcome! and if that language has a grammar that doesn't fit into the way the strings are assembled, then we'll fix that as we go :>
|
||||||
|
|
||||||
* but please note that support for [RTL (Right-to-Left) languages](https://en.wikipedia.org/wiki/Right-to-left_script) is currently not planned, since the javascript is a bit too jank for that
|
|
||||||
|
|
||||||
* **UI ideas** -- at some point I was thinking of rewriting the UI in react/preact/something-not-vanilla-javascript, but I'll admit the comfiness of not having any build stage combined with raw performance has kinda convinced me otherwise :p but I'd be very open to ideas on how the UI could be improved, or be more intuitive.
|
* **UI ideas** -- at some point I was thinking of rewriting the UI in react/preact/something-not-vanilla-javascript, but I'll admit the comfiness of not having any build stage combined with raw performance has kinda convinced me otherwise :p but I'd be very open to ideas on how the UI could be improved, or be more intuitive.
|
||||||
|
|
||||||
* **docker improvements** -- I don't really know what I'm doing when it comes to containers, so I'm sure there's a *huge* room for improvement here, mainly regarding how you're supposed to use the container with kubernetes / docker-compose / any of the other popular ways to do things. At some point I swear I'll start learning about docker so I can pick up clach04's [docker-compose draft](https://github.com/9001/copyparty/issues/38) and learn how that stuff ticks, unless someone beats me to it!
|
* **docker improvements** -- I don't really know what I'm doing when it comes to containers, so I'm sure there's a *huge* room for improvement here, mainly regarding how you're supposed to use the container with kubernetes / docker-compose / any of the other popular ways to do things. At some point I swear I'll start learning about docker so I can pick up clach04's [docker-compose draft](https://github.com/9001/copyparty/issues/38) and learn how that stuff ticks, unless someone beats me to it!
|
||||||
|
|
|
@ -15,18 +15,22 @@ produces a chronological list of all uploads by collecting info from up2k databa
|
||||||
# [`partyfuse.py`](partyfuse.py)
|
# [`partyfuse.py`](partyfuse.py)
|
||||||
* mount a copyparty server as a local filesystem (read-only)
|
* mount a copyparty server as a local filesystem (read-only)
|
||||||
* **supports Windows!** -- expect `194 MiB/s` sequential read
|
* **supports Windows!** -- expect `194 MiB/s` sequential read
|
||||||
* **supports Linux** -- expect `600 MiB/s` sequential read
|
* **supports Linux** -- expect `117 MiB/s` sequential read
|
||||||
* **supports macos** -- expect `85 MiB/s` sequential read
|
* **supports macos** -- expect `85 MiB/s` sequential read
|
||||||
|
|
||||||
|
filecache is default-on for windows and macos;
|
||||||
|
* macos readsize is 64kB, so speed ~32 MiB/s without the cache
|
||||||
|
* windows readsize varies by software; explorer=1M, pv=32k
|
||||||
|
|
||||||
note that copyparty should run with `-ed` to enable dotfiles (hidden otherwise)
|
note that copyparty should run with `-ed` to enable dotfiles (hidden otherwise)
|
||||||
|
|
||||||
and consider using [../docs/rclone.md](../docs/rclone.md) instead; usually a bit faster, especially on windows
|
also consider using [../docs/rclone.md](../docs/rclone.md) instead for 5x performance
|
||||||
|
|
||||||
|
|
||||||
## to run this on windows:
|
## to run this on windows:
|
||||||
* install [winfsp](https://github.com/billziss-gh/winfsp/releases/latest) and [python 3](https://www.python.org/downloads/)
|
* install [winfsp](https://github.com/billziss-gh/winfsp/releases/latest) and [python 3](https://www.python.org/downloads/)
|
||||||
* [x] add python 3.x to PATH (it asks during install)
|
* [x] add python 3.x to PATH (it asks during install)
|
||||||
* `python -m pip install --user fusepy` (or grab a copy of `fuse.py` from the `connect` page on your copyparty, and keep it in the same folder)
|
* `python -m pip install --user fusepy`
|
||||||
* `python ./partyfuse.py n: http://192.168.1.69:3923/`
|
* `python ./partyfuse.py n: http://192.168.1.69:3923/`
|
||||||
|
|
||||||
10% faster in [msys2](https://www.msys2.org/), 700% faster if debug prints are enabled:
|
10% faster in [msys2](https://www.msys2.org/), 700% faster if debug prints are enabled:
|
||||||
|
@ -78,6 +82,3 @@ cd /mnt/nas/music/.hist
|
||||||
# [`prisonparty.sh`](prisonparty.sh)
|
# [`prisonparty.sh`](prisonparty.sh)
|
||||||
* run copyparty in a chroot, preventing any accidental file access
|
* run copyparty in a chroot, preventing any accidental file access
|
||||||
* creates bindmounts for /bin, /lib, and so on, see `sysdirs=`
|
* creates bindmounts for /bin, /lib, and so on, see `sysdirs=`
|
||||||
|
|
||||||
# [`bubbleparty.sh`](bubbleparty.sh)
|
|
||||||
* run copyparty in an isolated process, preventing any accidental file access and more
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
# usage: ./bubbleparty.sh ./copyparty-sfx.py ....
|
|
||||||
bwrap \
|
|
||||||
--unshare-all \
|
|
||||||
--ro-bind /usr /usr \
|
|
||||||
--ro-bind /bin /bin \
|
|
||||||
--ro-bind /lib /lib \
|
|
||||||
--ro-bind /etc/resolv.conf /etc/resolv.conf \
|
|
||||||
--dev-bind /dev /dev \
|
|
||||||
--dir /tmp \
|
|
||||||
--dir /var \
|
|
||||||
--bind $(pwd) $(pwd) \
|
|
||||||
--share-net \
|
|
||||||
--die-with-parent \
|
|
||||||
--file 11 /etc/passwd \
|
|
||||||
--file 12 /etc/group \
|
|
||||||
"$@" \
|
|
||||||
11< <(getent passwd $(id -u) 65534) \
|
|
||||||
12< <(getent group $(id -g) 65534)
|
|
|
@ -20,8 +20,6 @@ each plugin must define a `main()` which takes 3 arguments;
|
||||||
|
|
||||||
## on404
|
## on404
|
||||||
|
|
||||||
* [redirect.py](redirect.py) sends an HTTP 301 or 302, redirecting the client to another page/file
|
|
||||||
* [randpic.py](randpic.py) redirects `/foo/bar/randpic.jpg` to a random pic in `/foo/bar/`
|
|
||||||
* [sorry.py](answer.py) replies with a custom message instead of the usual 404
|
* [sorry.py](answer.py) replies with a custom message instead of the usual 404
|
||||||
* [nooo.py](nooo.py) replies with an endless noooooooooooooo
|
* [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
|
* [never404.py](never404.py) 100% guarantee that 404 will never be a thing again as it automatically creates dummy files whenever necessary
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
import os
|
|
||||||
import random
|
|
||||||
from urllib.parse import quote
|
|
||||||
|
|
||||||
|
|
||||||
# assuming /foo/bar/ is a valid URL but /foo/bar/randpic.png does not exist,
|
|
||||||
# hijack the 404 with a redirect to a random pic in that folder
|
|
||||||
#
|
|
||||||
# thx to lia & kipu for the idea
|
|
||||||
|
|
||||||
|
|
||||||
def main(cli, vn, rem):
|
|
||||||
req_fn = rem.split("/")[-1]
|
|
||||||
if not cli.can_read or not req_fn.startswith("randpic"):
|
|
||||||
return
|
|
||||||
|
|
||||||
req_abspath = vn.canonical(rem)
|
|
||||||
req_ap_dir = os.path.dirname(req_abspath)
|
|
||||||
files_in_dir = os.listdir(req_ap_dir)
|
|
||||||
|
|
||||||
if "." in req_fn:
|
|
||||||
file_ext = "." + req_fn.split(".")[-1]
|
|
||||||
files_in_dir = [x for x in files_in_dir if x.lower().endswith(file_ext)]
|
|
||||||
|
|
||||||
if not files_in_dir:
|
|
||||||
return
|
|
||||||
|
|
||||||
selected_file = random.choice(files_in_dir)
|
|
||||||
|
|
||||||
req_url = "/".join([vn.vpath, rem]).strip("/")
|
|
||||||
req_dir = req_url.rsplit("/", 1)[0]
|
|
||||||
new_url = "/".join([req_dir, quote(selected_file)]).strip("/")
|
|
||||||
|
|
||||||
cli.reply(b"redirecting...", 302, headers={"Location": "/" + new_url})
|
|
||||||
return "true"
|
|
|
@ -1,52 +0,0 @@
|
||||||
# if someone hits a 404, redirect them to another location
|
|
||||||
|
|
||||||
|
|
||||||
def send_http_302_temporary_redirect(cli, new_path):
|
|
||||||
"""
|
|
||||||
replies with an HTTP 302, which is a temporary redirect;
|
|
||||||
"new_path" can be any of the following:
|
|
||||||
- "http://a.com/" would redirect to another website,
|
|
||||||
- "/foo/bar" would redirect to /foo/bar on the same server;
|
|
||||||
note the leading '/' in the location which is important
|
|
||||||
"""
|
|
||||||
cli.reply(b"redirecting...", 302, headers={"Location": new_path})
|
|
||||||
|
|
||||||
|
|
||||||
def send_http_301_permanent_redirect(cli, new_path):
|
|
||||||
"""
|
|
||||||
replies with an HTTP 301, which is a permanent redirect;
|
|
||||||
otherwise identical to send_http_302_temporary_redirect
|
|
||||||
"""
|
|
||||||
cli.reply(b"redirecting...", 301, headers={"Location": new_path})
|
|
||||||
|
|
||||||
|
|
||||||
def send_errorpage_with_redirect_link(cli, new_path):
|
|
||||||
"""
|
|
||||||
replies with a website explaining that the page has moved;
|
|
||||||
"new_path" must be an absolute location on the same server
|
|
||||||
but without a leading '/', so for example "foo/bar"
|
|
||||||
would redirect to "/foo/bar"
|
|
||||||
"""
|
|
||||||
cli.redirect(new_path, click=False, msg="this page has moved")
|
|
||||||
|
|
||||||
|
|
||||||
def main(cli, vn, rem):
|
|
||||||
"""
|
|
||||||
this is the function that gets called by copyparty;
|
|
||||||
note that vn.vpath and cli.vpath does not have a leading '/'
|
|
||||||
so we're adding the slash in the debug messages below
|
|
||||||
"""
|
|
||||||
print(f"this client just hit a 404: {cli.ip}")
|
|
||||||
print(f"they were accessing this volume: /{vn.vpath}")
|
|
||||||
print(f"and the original request-path (straight from the URL) was /{cli.vpath}")
|
|
||||||
print(f"...which resolves to the following filesystem path: {vn.canonical(rem)}")
|
|
||||||
|
|
||||||
new_path = "/foo/bar/"
|
|
||||||
print(f"will now redirect the client to {new_path}")
|
|
||||||
|
|
||||||
# uncomment one of these:
|
|
||||||
send_http_302_temporary_redirect(cli, new_path)
|
|
||||||
#send_http_301_permanent_redirect(cli, new_path)
|
|
||||||
#send_errorpage_with_redirect_link(cli, new_path)
|
|
||||||
|
|
||||||
return "true"
|
|
|
@ -2,7 +2,7 @@ standalone programs which are executed by copyparty when an event happens (uploa
|
||||||
|
|
||||||
these programs either take zero arguments, or a filepath (the affected file), or a json message with filepath + additional info
|
these programs either take zero arguments, or a filepath (the affected file), or a json message with filepath + additional info
|
||||||
|
|
||||||
run copyparty with `--help-hooks` for usage details / hook type explanations (xm/xbu/xau/xiu/xbc/xac/xbr/xar/xbd/xad/xban)
|
run copyparty with `--help-hooks` for usage details / hook type explanations (xbu/xau/xiu/xbr/xar/xbd/xad)
|
||||||
|
|
||||||
> **note:** in addition to event hooks (the stuff described here), copyparty has another api to run your programs/scripts while providing way more information such as audio tags / video codecs / etc and optionally daisychaining data between scripts in a processing pipeline; if that's what you want then see [mtp plugins](../mtag/) instead
|
> **note:** in addition to event hooks (the stuff described here), copyparty has another api to run your programs/scripts while providing way more information such as audio tags / video codecs / etc and optionally daisychaining data between scripts in a processing pipeline; if that's what you want then see [mtp plugins](../mtag/) instead
|
||||||
|
|
||||||
|
@ -13,9 +13,6 @@ run copyparty with `--help-hooks` for usage details / hook type explanations (xm
|
||||||
* [image-noexif.py](image-noexif.py) removes image exif by overwriting / directly editing the uploaded file
|
* [image-noexif.py](image-noexif.py) removes image exif by overwriting / directly editing the uploaded file
|
||||||
* [discord-announce.py](discord-announce.py) announces new uploads on discord using webhooks ([example](https://user-images.githubusercontent.com/241032/215304439-1c1cb3c8-ec6f-4c17-9f27-81f969b1811a.png))
|
* [discord-announce.py](discord-announce.py) announces new uploads on discord using webhooks ([example](https://user-images.githubusercontent.com/241032/215304439-1c1cb3c8-ec6f-4c17-9f27-81f969b1811a.png))
|
||||||
* [reject-mimetype.py](reject-mimetype.py) rejects uploads unless the mimetype is acceptable
|
* [reject-mimetype.py](reject-mimetype.py) rejects uploads unless the mimetype is acceptable
|
||||||
* [into-the-cache-it-goes.py](into-the-cache-it-goes.py) avoids bugs in caching proxies by immediately downloading each file that is uploaded
|
|
||||||
* [podcast-normalizer.py](podcast-normalizer.py) creates a second file with dynamic-range-compression whenever an audio file is uploaded
|
|
||||||
* good example of the `idx` [hook effect](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hook-effects) to tell copyparty about additional files to scan/index
|
|
||||||
|
|
||||||
|
|
||||||
# upload batches
|
# upload batches
|
||||||
|
@ -26,12 +23,7 @@ these are `--xiu` hooks; unlike `xbu` and `xau` (which get executed on every sin
|
||||||
|
|
||||||
# before upload
|
# before upload
|
||||||
* [reject-extension.py](reject-extension.py) rejects uploads if they match a list of file extensions
|
* [reject-extension.py](reject-extension.py) rejects uploads if they match a list of file extensions
|
||||||
* [reloc-by-ext.py](reloc-by-ext.py) redirects an upload to another destination based on the file extension
|
|
||||||
* good example of the `reloc` [hook effect](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hook-effects)
|
|
||||||
|
|
||||||
|
|
||||||
# on message
|
# on message
|
||||||
* [wget.py](wget.py) lets you download files by POSTing URLs to copyparty
|
* [wget.py](wget.py) lets you download files by POSTing URLs to copyparty
|
||||||
* [qbittorrent-magnet.py](qbittorrent-magnet.py) starts downloading a torrent if you post a magnet url
|
|
||||||
* [usb-eject.py](usb-eject.py) adds web-UI buttons to safe-remove usb flashdrives shared through copyparty
|
|
||||||
* [msg-log.py](msg-log.py) is a guestbook; logs messages to a doc in the same folder
|
|
||||||
|
|
|
@ -12,28 +12,19 @@ announces a new upload on discord
|
||||||
example usage as global config:
|
example usage as global config:
|
||||||
--xau f,t5,j,bin/hooks/discord-announce.py
|
--xau f,t5,j,bin/hooks/discord-announce.py
|
||||||
|
|
||||||
parameters explained,
|
|
||||||
xau = execute after upload
|
|
||||||
f = fork; don't delay other hooks while this is running
|
|
||||||
t5 = timeout if it's still running after 5 sec
|
|
||||||
j = this hook needs upload information as json (not just the filename)
|
|
||||||
|
|
||||||
example usage as a volflag (per-volume config):
|
example usage as a volflag (per-volume config):
|
||||||
-v srv/inc:inc:r:rw,ed:c,xau=f,t5,j,bin/hooks/discord-announce.py
|
-v srv/inc:inc:r:rw,ed:c,xau=f,t5,j,bin/hooks/discord-announce.py
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
(share filesystem-path srv/inc as volume /inc,
|
(share filesystem-path srv/inc as volume /inc,
|
||||||
readable by everyone, read-write for user 'ed',
|
readable by everyone, read-write for user 'ed',
|
||||||
running this plugin on all uploads with the params explained above)
|
running this plugin on all uploads with the params listed below)
|
||||||
|
|
||||||
example usage as a volflag in a copyparty config file:
|
parameters explained,
|
||||||
[/inc]
|
xbu = execute after upload
|
||||||
srv/inc
|
f = fork; don't wait for it to finish
|
||||||
accs:
|
t5 = timeout if it's still running after 5 sec
|
||||||
r: *
|
j = provide upload information as json; not just the filename
|
||||||
rw: ed
|
|
||||||
flags:
|
|
||||||
xau: f,t5,j,bin/hooks/discord-announce.py
|
|
||||||
|
|
||||||
replace "xau" with "xbu" to announce Before upload starts instead of After completion
|
replace "xau" with "xbu" to announce Before upload starts instead of After completion
|
||||||
|
|
||||||
|
|
|
@ -1,140 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import shutil
|
|
||||||
import platform
|
|
||||||
import subprocess as sp
|
|
||||||
from urllib.parse import quote
|
|
||||||
|
|
||||||
|
|
||||||
_ = r"""
|
|
||||||
try to avoid race conditions in caching proxies
|
|
||||||
(primarily cloudflare, but probably others too)
|
|
||||||
by means of the most obvious solution possible:
|
|
||||||
|
|
||||||
just as each file has finished uploading, use
|
|
||||||
the server's external URL to download the file
|
|
||||||
so that it ends up in the cache, warm and snug
|
|
||||||
|
|
||||||
this intentionally delays the upload response
|
|
||||||
as it waits for the file to finish downloading
|
|
||||||
before copyparty is allowed to return the URL
|
|
||||||
|
|
||||||
NOTE: you must edit this script before use,
|
|
||||||
replacing https://example.com with your URL
|
|
||||||
|
|
||||||
NOTE: if the files are only accessible with a
|
|
||||||
password and/or filekey, you must also add
|
|
||||||
a cromulent password in the PASSWORD field
|
|
||||||
|
|
||||||
NOTE: needs either wget, curl, or "requests":
|
|
||||||
python3 -m pip install --user -U requests
|
|
||||||
|
|
||||||
|
|
||||||
example usage as global config:
|
|
||||||
--xau j,t10,bin/hooks/into-the-cache-it-goes.py
|
|
||||||
|
|
||||||
parameters explained,
|
|
||||||
xau = execute after upload
|
|
||||||
j = this hook needs upload information as json (not just the filename)
|
|
||||||
t10 = abort download and continue if it takes longer than 10sec
|
|
||||||
|
|
||||||
example usage as a volflag (per-volume config):
|
|
||||||
-v srv/inc:inc:r:rw,ed:c,xau=j,t10,bin/hooks/into-the-cache-it-goes.py
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
(share filesystem-path srv/inc as volume /inc,
|
|
||||||
readable by everyone, read-write for user 'ed',
|
|
||||||
running this plugin on all uploads with params explained above)
|
|
||||||
|
|
||||||
example usage as a volflag in a copyparty config file:
|
|
||||||
[/inc]
|
|
||||||
srv/inc
|
|
||||||
accs:
|
|
||||||
r: *
|
|
||||||
rw: ed
|
|
||||||
flags:
|
|
||||||
xau: j,t10,bin/hooks/into-the-cache-it-goes.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
# replace this with your site's external URL
|
|
||||||
# (including the :portnumber if necessary)
|
|
||||||
SITE_URL = "https://example.com"
|
|
||||||
|
|
||||||
# if downloading is protected by passwords or filekeys,
|
|
||||||
# specify a valid password between the quotes below:
|
|
||||||
PASSWORD = ""
|
|
||||||
|
|
||||||
# if file is larger than this, skip download
|
|
||||||
MAX_MEGABYTES = 8
|
|
||||||
|
|
||||||
# =============== END OF CONFIG ===============
|
|
||||||
|
|
||||||
|
|
||||||
WINDOWS = platform.system() == "Windows"
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
fun = download_with_python
|
|
||||||
if shutil.which("curl"):
|
|
||||||
fun = download_with_curl
|
|
||||||
elif shutil.which("wget"):
|
|
||||||
fun = download_with_wget
|
|
||||||
|
|
||||||
inf = json.loads(sys.argv[1])
|
|
||||||
|
|
||||||
if inf["sz"] > 1024 * 1024 * MAX_MEGABYTES:
|
|
||||||
print("[into-the-cache] file is too large; will not download")
|
|
||||||
return
|
|
||||||
|
|
||||||
file_url = "/"
|
|
||||||
if inf["vp"]:
|
|
||||||
file_url += inf["vp"] + "/"
|
|
||||||
file_url += inf["ap"].replace("\\", "/").split("/")[-1]
|
|
||||||
file_url = SITE_URL.rstrip("/") + quote(file_url, safe=b"/")
|
|
||||||
|
|
||||||
print("[into-the-cache] %s(%s)" % (fun.__name__, file_url))
|
|
||||||
fun(file_url, PASSWORD.strip())
|
|
||||||
|
|
||||||
print("[into-the-cache] Download OK")
|
|
||||||
|
|
||||||
|
|
||||||
def download_with_curl(url, pw):
|
|
||||||
cmd = ["curl"]
|
|
||||||
|
|
||||||
if pw:
|
|
||||||
cmd += ["-HPW:%s" % (pw,)]
|
|
||||||
|
|
||||||
nah = sp.DEVNULL
|
|
||||||
sp.check_call(cmd + [url], stdout=nah, stderr=nah)
|
|
||||||
|
|
||||||
|
|
||||||
def download_with_wget(url, pw):
|
|
||||||
cmd = ["wget", "-O"]
|
|
||||||
|
|
||||||
cmd += ["nul" if WINDOWS else "/dev/null"]
|
|
||||||
|
|
||||||
if pw:
|
|
||||||
cmd += ["--header=PW:%s" % (pw,)]
|
|
||||||
|
|
||||||
nah = sp.DEVNULL
|
|
||||||
sp.check_call(cmd + [url], stdout=nah, stderr=nah)
|
|
||||||
|
|
||||||
|
|
||||||
def download_with_python(url, pw):
|
|
||||||
import requests
|
|
||||||
|
|
||||||
headers = {}
|
|
||||||
if pw:
|
|
||||||
headers["PW"] = pw
|
|
||||||
|
|
||||||
with requests.get(url, headers=headers, stream=True) as r:
|
|
||||||
r.raise_for_status()
|
|
||||||
for _ in r.iter_content(chunk_size=1024 * 256):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
|
@ -14,32 +14,19 @@ except:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
_ = r"""
|
"""
|
||||||
use copyparty as a dumb messaging server / guestbook thing;
|
use copyparty as a dumb messaging server / guestbook thing;
|
||||||
accepts guestbook entries from 📟 (message-to-server-log) in the web-ui
|
|
||||||
initially contributed by @clach04 in https://github.com/9001/copyparty/issues/35 (thanks!)
|
initially contributed by @clach04 in https://github.com/9001/copyparty/issues/35 (thanks!)
|
||||||
|
|
||||||
example usage as global config:
|
Sample usage:
|
||||||
|
|
||||||
python copyparty-sfx.py --xm j,bin/hooks/msg-log.py
|
python copyparty-sfx.py --xm j,bin/hooks/msg-log.py
|
||||||
|
|
||||||
parameters explained,
|
Where:
|
||||||
xm = execute on message (📟)
|
|
||||||
j = this hook needs message information as json (not just the message-text)
|
|
||||||
|
|
||||||
example usage as a volflag (per-volume config):
|
xm = execute on message-to-server-log
|
||||||
python copyparty-sfx.py -v srv/log:log:r:c,xm=j,bin/hooks/msg-log.py
|
j = provide message information as json; not just the text - this script REQUIRES json
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
t10 = timeout and kill download after 10 secs
|
||||||
|
|
||||||
(share filesystem-path srv/log as volume /log, readable by everyone,
|
|
||||||
running this plugin on all messages with the params explained above)
|
|
||||||
|
|
||||||
example usage as a volflag in a copyparty config file:
|
|
||||||
[/log]
|
|
||||||
srv/log
|
|
||||||
accs:
|
|
||||||
r: *
|
|
||||||
flags:
|
|
||||||
xm: j,bin/hooks/msg-log.py
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,121 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import subprocess as sp
|
|
||||||
|
|
||||||
|
|
||||||
_ = r"""
|
|
||||||
sends all uploaded audio files through an aggressive
|
|
||||||
dynamic-range-compressor to even out the volume levels
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
ffmpeg
|
|
||||||
|
|
||||||
being an xau hook, this gets eXecuted After Upload completion
|
|
||||||
but before copyparty has started hashing/indexing the file, so
|
|
||||||
we'll create a second normalized copy in a subfolder and tell
|
|
||||||
copyparty to hash/index that additional file as well
|
|
||||||
|
|
||||||
example usage as global config:
|
|
||||||
-e2d -e2t --xau j,c1,bin/hooks/podcast-normalizer.py
|
|
||||||
|
|
||||||
parameters explained,
|
|
||||||
e2d/e2t = enable database and metadata indexing
|
|
||||||
xau = execute after upload
|
|
||||||
j = this hook needs upload information as json (not just the filename)
|
|
||||||
c1 = this hook returns json on stdout, so tell copyparty to read that
|
|
||||||
|
|
||||||
example usage as a volflag (per-volume config):
|
|
||||||
-v srv/inc/pods:inc/pods:r:rw,ed:c,xau=j,c1,bin/hooks/podcast-normalizer.py
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
(share fs-path srv/inc/pods at URL /inc/pods,
|
|
||||||
readable by all, read-write for user ed,
|
|
||||||
running this xau (exec-after-upload) plugin for all uploaded files)
|
|
||||||
|
|
||||||
example usage as a volflag in a copyparty config file:
|
|
||||||
[/inc/pods]
|
|
||||||
srv/inc/pods
|
|
||||||
accs:
|
|
||||||
r: *
|
|
||||||
rw: ed
|
|
||||||
flags:
|
|
||||||
e2d # enables file indexing
|
|
||||||
e2t # metadata tags too
|
|
||||||
xau: j,c1,bin/hooks/podcast-normalizer.py
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
########################################################################
|
|
||||||
### CONFIG
|
|
||||||
|
|
||||||
# filetypes to process; ignores everything else
|
|
||||||
EXTS = "mp3 flac ogg oga opus m4a aac wav wma"
|
|
||||||
|
|
||||||
# the name of the subdir to put the normalized files in
|
|
||||||
SUBDIR = "normalized"
|
|
||||||
|
|
||||||
########################################################################
|
|
||||||
|
|
||||||
|
|
||||||
# try to enable support for crazy filenames
|
|
||||||
try:
|
|
||||||
from copyparty.util import fsenc
|
|
||||||
except:
|
|
||||||
|
|
||||||
def fsenc(p):
|
|
||||||
return p.encode("utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# read info from copyparty
|
|
||||||
inf = json.loads(sys.argv[1])
|
|
||||||
vpath = inf["vp"]
|
|
||||||
abspath = inf["ap"]
|
|
||||||
|
|
||||||
# check if the file-extension is on the to-be-processed list
|
|
||||||
ext = abspath.lower().split(".")[-1]
|
|
||||||
if ext not in EXTS.split():
|
|
||||||
return
|
|
||||||
|
|
||||||
# jump into the folder where the file was uploaded
|
|
||||||
# and create the subfolder to place the normalized copy inside
|
|
||||||
dirpath, filename = os.path.split(abspath)
|
|
||||||
os.chdir(fsenc(dirpath))
|
|
||||||
os.makedirs(SUBDIR, exist_ok=True)
|
|
||||||
|
|
||||||
# the input and output filenames to give ffmpeg
|
|
||||||
fname_in = fsenc(f"./{filename}")
|
|
||||||
fname_out = fsenc(f"{SUBDIR}/{filename}.opus")
|
|
||||||
|
|
||||||
# fmt: off
|
|
||||||
# create and run the ffmpeg command
|
|
||||||
cmd = [
|
|
||||||
b"ffmpeg",
|
|
||||||
b"-nostdin",
|
|
||||||
b"-hide_banner",
|
|
||||||
b"-i", fname_in,
|
|
||||||
b"-af", b"dynaudnorm=f=100:g=9", # the normalizer config
|
|
||||||
b"-c:a", b"libopus",
|
|
||||||
b"-b:a", b"128k",
|
|
||||||
fname_out,
|
|
||||||
]
|
|
||||||
# fmt: on
|
|
||||||
sp.check_output(cmd)
|
|
||||||
|
|
||||||
# and finally, tell copyparty about the new file
|
|
||||||
# so it appears in the database and rss-feed:
|
|
||||||
vpath = f"{SUBDIR}/{filename}.opus"
|
|
||||||
print(json.dumps({"idx": {"vp": [vpath]}}))
|
|
||||||
|
|
||||||
# (it's fine to give it a relative path like that; it gets
|
|
||||||
# resolved relative to the folder the file was uploaded into)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
try:
|
|
||||||
main()
|
|
||||||
except Exception as ex:
|
|
||||||
print("podcast-normalizer failed; %r" % (ex,))
|
|
|
@ -1,128 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import shutil
|
|
||||||
import subprocess as sp
|
|
||||||
|
|
||||||
|
|
||||||
_ = r"""
|
|
||||||
start downloading a torrent by POSTing a magnet URL to copyparty,
|
|
||||||
for example using 📟 (message-to-server-log) in the web-ui
|
|
||||||
|
|
||||||
by default it will download the torrent to the folder you were in
|
|
||||||
when you pasted the magnet into the message-to-server-log field
|
|
||||||
|
|
||||||
you can optionally specify another location by adding a whitespace
|
|
||||||
after the magnet URL followed by the name of the subfolder to DL into,
|
|
||||||
or for example "anime/airing" would download to /srv/media/anime/airing
|
|
||||||
because the keyword "anime" is in the DESTS config below
|
|
||||||
|
|
||||||
needs python3
|
|
||||||
|
|
||||||
example usage as global config (not a good idea):
|
|
||||||
python copyparty-sfx.py --xm aw,f,j,t60,bin/hooks/qbittorrent-magnet.py
|
|
||||||
|
|
||||||
parameters explained,
|
|
||||||
xm = execute on message (📟)
|
|
||||||
aw = only users with write-access can use this
|
|
||||||
f = fork; don't delay other hooks while this is running
|
|
||||||
j = provide message information as json (not just the text)
|
|
||||||
t60 = abort if qbittorrent has to think about it for more than 1 min
|
|
||||||
|
|
||||||
example usage as a volflag (per-volume config, much better):
|
|
||||||
-v srv/qb:qb:A,ed:c,xm=aw,f,j,t60,bin/hooks/qbittorrent-magnet.py
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
(share filesystem-path srv/qb as volume /qb with Admin for user 'ed',
|
|
||||||
running this plugin on all messages with the params explained above)
|
|
||||||
|
|
||||||
example usage as a volflag in a copyparty config file:
|
|
||||||
[/qb]
|
|
||||||
srv/qb
|
|
||||||
accs:
|
|
||||||
A: ed
|
|
||||||
flags:
|
|
||||||
xm: aw,f,j,t60,bin/hooks/qbittorrent-magnet.py
|
|
||||||
|
|
||||||
the volflag examples only kicks in if you send the torrent magnet
|
|
||||||
while you're in the /qb folder (or any folder below there)
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
# list of usernames to allow
|
|
||||||
ALLOWLIST = [ "ed", "morpheus" ]
|
|
||||||
|
|
||||||
|
|
||||||
# list of destination aliases to translate into full filesystem
|
|
||||||
# paths; takes effect if the first folder component in the
|
|
||||||
# custom download location matches anything in this dict
|
|
||||||
DESTS = {
|
|
||||||
"iso": "/srv/pub/linux-isos",
|
|
||||||
"anime": "/srv/media/anime",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
inf = json.loads(sys.argv[1])
|
|
||||||
url = inf["txt"]
|
|
||||||
if not url.lower().startswith("magnet:?"):
|
|
||||||
# not a magnet, abort
|
|
||||||
return
|
|
||||||
|
|
||||||
if inf["user"] not in ALLOWLIST:
|
|
||||||
print("🧲 denied for user", inf["user"])
|
|
||||||
return
|
|
||||||
|
|
||||||
# might as well run the command inside the filesystem folder
|
|
||||||
# which matches the URL that the magnet message was sent to
|
|
||||||
os.chdir(inf["ap"])
|
|
||||||
|
|
||||||
# is there is a custom download location in the url?
|
|
||||||
dst = ""
|
|
||||||
if " " in url:
|
|
||||||
url, dst = url.split(" ", 1)
|
|
||||||
|
|
||||||
# is the location in the predefined list of locations?
|
|
||||||
parts = dst.replace("\\", "/").split("/")
|
|
||||||
if parts[0] in DESTS:
|
|
||||||
dst = os.path.join(DESTS[parts[0]], *(parts[1:]))
|
|
||||||
|
|
||||||
else:
|
|
||||||
# nope, so download to the current folder instead;
|
|
||||||
# comment the dst line below to instead use the default
|
|
||||||
# download location from your qbittorrent settings
|
|
||||||
dst = inf["ap"]
|
|
||||||
pass
|
|
||||||
|
|
||||||
# archlinux has a -nox suffix for qbittorrent if headless
|
|
||||||
# so check if we should be using that
|
|
||||||
if shutil.which("qbittorrent-nox"):
|
|
||||||
torrent_bin = "qbittorrent-nox"
|
|
||||||
else:
|
|
||||||
torrent_bin = "qbittorrent"
|
|
||||||
|
|
||||||
# the command to add a new torrent, adjust if necessary
|
|
||||||
cmd = [torrent_bin, url]
|
|
||||||
if dst:
|
|
||||||
cmd += ["--save-path=%s" % (dst,)]
|
|
||||||
|
|
||||||
# if copyparty and qbittorrent are running as different users
|
|
||||||
# you may have to do something like the following
|
|
||||||
# (assuming qbittorrent* is nopasswd-allowed in sudoers):
|
|
||||||
#
|
|
||||||
# cmd = ["sudo", "-u", "qbitter"] + cmd
|
|
||||||
|
|
||||||
print("🧲", cmd)
|
|
||||||
|
|
||||||
try:
|
|
||||||
sp.check_call(cmd)
|
|
||||||
except:
|
|
||||||
print("🧲 FAILED TO ADD", url)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
|
@ -1,130 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
_ = r"""
|
|
||||||
relocate/redirect incoming uploads according to file extension or name
|
|
||||||
|
|
||||||
example usage as global config:
|
|
||||||
--xbu j,c1,bin/hooks/reloc-by-ext.py
|
|
||||||
|
|
||||||
parameters explained,
|
|
||||||
xbu = execute before upload
|
|
||||||
j = this hook needs upload information as json (not just the filename)
|
|
||||||
c1 = this hook returns json on stdout, so tell copyparty to read that
|
|
||||||
|
|
||||||
example usage as a volflag (per-volume config):
|
|
||||||
-v srv/inc:inc:r:rw,ed:c,xbu=j,c1,bin/hooks/reloc-by-ext.py
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
(share filesystem-path srv/inc as volume /inc,
|
|
||||||
readable by everyone, read-write for user 'ed',
|
|
||||||
running this plugin on all uploads with the params explained above)
|
|
||||||
|
|
||||||
example usage as a volflag in a copyparty config file:
|
|
||||||
[/inc]
|
|
||||||
srv/inc
|
|
||||||
accs:
|
|
||||||
r: *
|
|
||||||
rw: ed
|
|
||||||
flags:
|
|
||||||
xbu: j,c1,bin/hooks/reloc-by-ext.py
|
|
||||||
|
|
||||||
note: this could also work as an xau hook (after-upload), but
|
|
||||||
because it doesn't need to read the file contents its better
|
|
||||||
as xbu (before-upload) since that's safer / less buggy,
|
|
||||||
and only xbu works with up2k (dragdrop into browser)
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
PICS = "avif bmp gif heic heif jpeg jpg jxl png psd qoi tga tif tiff webp"
|
|
||||||
VIDS = "3gp asf avi flv mkv mov mp4 mpeg mpeg2 mpegts mpg mpg2 nut ogm ogv rm ts vob webm wmv"
|
|
||||||
MUSIC = "aac aif aiff alac amr ape dfpwm flac m4a mp3 ogg opus ra tak tta wav wma wv"
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
inf = json.loads(sys.argv[1])
|
|
||||||
vdir, fn = os.path.split(inf["vp"])
|
|
||||||
|
|
||||||
try:
|
|
||||||
fn, ext = fn.rsplit(".", 1)
|
|
||||||
except:
|
|
||||||
# no file extension; pretend it's "bin"
|
|
||||||
ext = "bin"
|
|
||||||
|
|
||||||
ext = ext.lower()
|
|
||||||
|
|
||||||
# this function must end by printing the action to perform;
|
|
||||||
# that's handled by the print(json.dumps(... at the bottom
|
|
||||||
#
|
|
||||||
# the action can contain the following keys:
|
|
||||||
# "vp" is the folder URL to move the upload to,
|
|
||||||
# "ap" is the filesystem-path to move it to (but "vp" is safer),
|
|
||||||
# "fn" overrides the final filename to use
|
|
||||||
|
|
||||||
##
|
|
||||||
## some example actions to take; pick one by
|
|
||||||
## selecting it inside the print at the end:
|
|
||||||
##
|
|
||||||
|
|
||||||
# move all uploads to one specific folder
|
|
||||||
into_junk = {"vp": "/junk"}
|
|
||||||
|
|
||||||
# create a subfolder named after the filetype and move it into there
|
|
||||||
into_subfolder = {"vp": ext}
|
|
||||||
|
|
||||||
# move it into a toplevel folder named after the filetype
|
|
||||||
into_toplevel = {"vp": "/" + ext}
|
|
||||||
|
|
||||||
# move it into a filetype-named folder next to the target folder
|
|
||||||
into_sibling = {"vp": "../" + ext}
|
|
||||||
|
|
||||||
# move images into "/just/pics", vids into "/just/vids",
|
|
||||||
# music into "/just/tunes", and anything else as-is
|
|
||||||
if ext in PICS.split():
|
|
||||||
by_category = {"vp": "/just/pics"}
|
|
||||||
elif ext in VIDS.split():
|
|
||||||
by_category = {"vp": "/just/vids"}
|
|
||||||
elif ext in MUSIC.split():
|
|
||||||
by_category = {"vp": "/just/tunes"}
|
|
||||||
else:
|
|
||||||
by_category = {} # no action
|
|
||||||
|
|
||||||
# now choose the default effect to apply; can be any of these:
|
|
||||||
# into_junk into_subfolder into_toplevel into_sibling by_category
|
|
||||||
effect = into_sibling
|
|
||||||
|
|
||||||
##
|
|
||||||
## but we can keep going, adding more speicifc rules
|
|
||||||
## which can take precedence, replacing the fallback
|
|
||||||
## effect we just specified:
|
|
||||||
##
|
|
||||||
|
|
||||||
fn = fn.lower() # lowercase filename to make this easier
|
|
||||||
|
|
||||||
if "screenshot" in fn:
|
|
||||||
effect = {"vp": "/ss"}
|
|
||||||
if "mpv_" in fn:
|
|
||||||
effect = {"vp": "/anishots"}
|
|
||||||
elif "debian" in fn or "biebian" in fn:
|
|
||||||
effect = {"vp": "/linux-ISOs"}
|
|
||||||
elif re.search(r"ep(isode |\.)?[0-9]", fn):
|
|
||||||
effect = {"vp": "/podcasts"}
|
|
||||||
|
|
||||||
# regex lets you grab a part of the matching
|
|
||||||
# text and use that in the upload path:
|
|
||||||
m = re.search(r"\b(op|ed)([^a-z]|$)", fn)
|
|
||||||
if m:
|
|
||||||
# the regex matched; use "anime-op" or "anime-ed"
|
|
||||||
effect = {"vp": "/anime-" + m[1]}
|
|
||||||
|
|
||||||
# aaand DO IT
|
|
||||||
print(json.dumps({"reloc": effect}))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
|
@ -1,62 +0,0 @@
|
||||||
// see usb-eject.py for usage
|
|
||||||
|
|
||||||
function usbclick() {
|
|
||||||
var o = QS('#treeul a[dst="/usb/"]') || QS('#treepar a[dst="/usb/"]');
|
|
||||||
if (o)
|
|
||||||
o.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
function eject_cb() {
|
|
||||||
var t = ('' + this.responseText).trim();
|
|
||||||
if (t.indexOf('can be safely unplugged') < 0 && t.indexOf('Device can be removed') < 0)
|
|
||||||
return toast.err(30, 'usb eject failed:\n\n' + t);
|
|
||||||
|
|
||||||
toast.ok(5, esc(t.replace(/ - /g, '\n\n')).trim());
|
|
||||||
usbclick(); setTimeout(usbclick, 10);
|
|
||||||
};
|
|
||||||
|
|
||||||
function add_eject_2(a) {
|
|
||||||
var aw = a.getAttribute('href').split(/\//g);
|
|
||||||
if (aw.length != 4 || aw[3])
|
|
||||||
return;
|
|
||||||
|
|
||||||
var v = aw[2],
|
|
||||||
k = 'umount_' + v;
|
|
||||||
|
|
||||||
for (var b = 0; b < 9; b++) {
|
|
||||||
var o = ebi(k);
|
|
||||||
if (!o)
|
|
||||||
break;
|
|
||||||
o.parentNode.removeChild(o);
|
|
||||||
}
|
|
||||||
|
|
||||||
a.appendChild(mknod('span', k, '⏏'), a);
|
|
||||||
o = ebi(k);
|
|
||||||
o.style.cssText = 'position:absolute; right:1em; margin-top:-.2em; font-size:1.3em';
|
|
||||||
o.onclick = function (e) {
|
|
||||||
ev(e);
|
|
||||||
var xhr = new XHR();
|
|
||||||
xhr.open('POST', get_evpath(), true);
|
|
||||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8');
|
|
||||||
xhr.send('msg=' + uricom_enc(':usb-eject:' + v + ':'));
|
|
||||||
xhr.onload = xhr.onerror = eject_cb;
|
|
||||||
toast.inf(10, "ejecting " + v + "...");
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
function add_eject() {
|
|
||||||
var o = QSA('#treeul a[href^="/usb/"]') || QSA('#treepar a[href^="/usb/"]');
|
|
||||||
for (var a = o.length - 1; a > 0; a--)
|
|
||||||
add_eject_2(o[a]);
|
|
||||||
};
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
var f0 = treectl.rendertree;
|
|
||||||
treectl.rendertree = function (res, ts, top0, dst, rst) {
|
|
||||||
var ret = f0(res, ts, top0, dst, rst);
|
|
||||||
add_eject();
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
setTimeout(add_eject, 50);
|
|
|
@ -1,62 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import os
|
|
||||||
import stat
|
|
||||||
import subprocess as sp
|
|
||||||
import sys
|
|
||||||
from urllib.parse import unquote_to_bytes as unquote
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
if you've found yourself using copyparty to serve flashdrives on a LAN
|
|
||||||
and your only wish is that the web-UI had a button to unmount / safely
|
|
||||||
remove those flashdrives, then boy howdy are you in the right place :D
|
|
||||||
|
|
||||||
put usb-eject.js in the webroot (or somewhere else http-accessible)
|
|
||||||
then run copyparty with these args:
|
|
||||||
|
|
||||||
-v /run/media/egon:/usb:A:c,hist=/tmp/junk
|
|
||||||
--xm=c1,bin/hooks/usb-eject.py
|
|
||||||
--js-browser=/usb-eject.js
|
|
||||||
|
|
||||||
which does the following respectively,
|
|
||||||
|
|
||||||
* share all of /run/media/egon as /usb with admin for everyone
|
|
||||||
and put the histpath somewhere it won't cause trouble
|
|
||||||
* run the usb-eject hook with stdout redirect to the web-ui
|
|
||||||
* add the complementary usb-eject.js to the browser
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
MOUNT_BASE = b"/run/media/egon/"
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
try:
|
|
||||||
label = sys.argv[1].split(":usb-eject:")[1].split(":")[0]
|
|
||||||
mp = MOUNT_BASE + unquote(label)
|
|
||||||
# print("ejecting [%s]... " % (mp,), end="")
|
|
||||||
mp = os.path.abspath(os.path.realpath(mp))
|
|
||||||
st = os.lstat(mp)
|
|
||||||
if not stat.S_ISDIR(st.st_mode) or not mp.startswith(MOUNT_BASE):
|
|
||||||
raise Exception("not a regular directory")
|
|
||||||
|
|
||||||
# if you're running copyparty as root (thx for the faith)
|
|
||||||
# you'll need something like this to make dbus talkative
|
|
||||||
cmd = b"sudo -u egon DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus gio mount -e"
|
|
||||||
|
|
||||||
# but if copyparty and the ui-session is running
|
|
||||||
# as the same user (good) then this is plenty
|
|
||||||
cmd = b"gio mount -e"
|
|
||||||
|
|
||||||
cmd = cmd.split(b" ") + [mp]
|
|
||||||
ret = sp.check_output(cmd).decode("utf-8", "replace")
|
|
||||||
print(ret.strip() or (label + " can be safely unplugged"))
|
|
||||||
|
|
||||||
except Exception as ex:
|
|
||||||
print("unmount failed: %r" % (ex,))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
|
@ -9,38 +9,25 @@ import subprocess as sp
|
||||||
_ = r"""
|
_ = r"""
|
||||||
use copyparty as a file downloader by POSTing URLs as
|
use copyparty as a file downloader by POSTing URLs as
|
||||||
application/x-www-form-urlencoded (for example using the
|
application/x-www-form-urlencoded (for example using the
|
||||||
📟 message-to-server-log in the web-ui)
|
message/pager function on the website)
|
||||||
|
|
||||||
example usage as global config:
|
example usage as global config:
|
||||||
--xm aw,f,j,t3600,bin/hooks/wget.py
|
--xm f,j,t3600,bin/hooks/wget.py
|
||||||
|
|
||||||
parameters explained,
|
|
||||||
xm = execute on message-to-server-log
|
|
||||||
aw = only users with write-access can use this
|
|
||||||
f = fork; don't delay other hooks while this is running
|
|
||||||
j = provide message information as json (not just the text)
|
|
||||||
c3 = mute all output
|
|
||||||
t3600 = timeout and abort download after 1 hour
|
|
||||||
|
|
||||||
example usage as a volflag (per-volume config):
|
example usage as a volflag (per-volume config):
|
||||||
-v srv/inc:inc:r:rw,ed:c,xm=aw,f,j,t3600,bin/hooks/wget.py
|
-v srv/inc:inc:r:rw,ed:c,xm=f,j,t3600,bin/hooks/wget.py
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
(share filesystem-path srv/inc as volume /inc,
|
(share filesystem-path srv/inc as volume /inc,
|
||||||
readable by everyone, read-write for user 'ed',
|
readable by everyone, read-write for user 'ed',
|
||||||
running this plugin on all messages with the params explained above)
|
running this plugin on all messages with the params listed below)
|
||||||
|
|
||||||
example usage as a volflag in a copyparty config file:
|
parameters explained,
|
||||||
[/inc]
|
xm = execute on message-to-server-log
|
||||||
srv/inc
|
f = fork so it doesn't block uploads
|
||||||
accs:
|
j = provide message information as json; not just the text
|
||||||
r: *
|
c3 = mute all output
|
||||||
rw: ed
|
t3600 = timeout and kill download after 1 hour
|
||||||
flags:
|
|
||||||
xm: aw,f,j,t3600,bin/hooks/wget.py
|
|
||||||
|
|
||||||
the volflag examples only kicks in if you send the message
|
|
||||||
while you're in the /inc folder (or any folder below there)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -31,9 +31,6 @@ 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
|
* [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
|
* 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!
|
* 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!
|
||||||
* [kamelåså](https://github.com/steinuil/kameloso) is a much better (and MUCH safer) alternative to this plugin
|
|
||||||
* powered by [chicken-curry-banana-pineapple-peanut pizza](https://a.ocv.me/pub/g/i/2025/01/298437ce-8351-4c8c-861c-fa131d217999.jpg?cache) so you know it's good
|
|
||||||
* and, unlike this plugin, kamelåså even has windows support (nice)
|
|
||||||
|
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
|
|
|
@ -2,15 +2,11 @@
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
|
import zlib
|
||||||
import struct
|
import struct
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
try:
|
|
||||||
from zlib_ng import zlib_ng as zlib
|
|
||||||
except:
|
|
||||||
import zlib
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from copyparty.util import fsenc
|
from copyparty.util import fsenc
|
||||||
except:
|
except:
|
||||||
|
|
|
@ -22,8 +22,6 @@ set -e
|
||||||
# modifies the keyfinder python lib to load the .so in ~/pe
|
# modifies the keyfinder python lib to load the .so in ~/pe
|
||||||
|
|
||||||
|
|
||||||
export FORCE_COLOR=1
|
|
||||||
|
|
||||||
linux=1
|
linux=1
|
||||||
|
|
||||||
win=
|
win=
|
||||||
|
@ -188,15 +186,12 @@ install_keyfinder() {
|
||||||
echo "so not found at $sop"
|
echo "so not found at $sop"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
x=${-//[^x]/}; set -x; cat /etc/alpine-release
|
|
||||||
# rm -rf /Users/ed/Library/Python/3.9/lib/python/site-packages/*keyfinder*
|
# rm -rf /Users/ed/Library/Python/3.9/lib/python/site-packages/*keyfinder*
|
||||||
CFLAGS="-I$h/pe/keyfinder/include -I/opt/local/include -I/usr/include/ffmpeg" \
|
CFLAGS="-I$h/pe/keyfinder/include -I/opt/local/include -I/usr/include/ffmpeg" \
|
||||||
CXXFLAGS="-I$h/pe/keyfinder/include -I/opt/local/include -I/usr/include/ffmpeg" \
|
|
||||||
LDFLAGS="-L$h/pe/keyfinder/lib -L$h/pe/keyfinder/lib64 -L/opt/local/lib" \
|
LDFLAGS="-L$h/pe/keyfinder/lib -L$h/pe/keyfinder/lib64 -L/opt/local/lib" \
|
||||||
PKG_CONFIG_PATH="/c/msys64/mingw64/lib/pkgconfig:$h/pe/keyfinder/lib/pkgconfig" \
|
PKG_CONFIG_PATH=/c/msys64/mingw64/lib/pkgconfig \
|
||||||
$pybin -m pip install --user keyfinder
|
$pybin -m pip install --user keyfinder
|
||||||
[ "$x" ] || set +x
|
|
||||||
|
|
||||||
pypath="$($pybin -c 'import keyfinder; print(keyfinder.__file__)')"
|
pypath="$($pybin -c 'import keyfinder; print(keyfinder.__file__)')"
|
||||||
for pyso in "${pypath%/*}"/*.so; do
|
for pyso in "${pypath%/*}"/*.so; do
|
||||||
|
|
|
@ -6,11 +6,6 @@ WARNING -- DANGEROUS PLUGIN --
|
||||||
running this plugin, they can execute malware on your machine
|
running this plugin, they can execute malware on your machine
|
||||||
so please keep this on a LAN and protect it with a password
|
so please keep this on a LAN and protect it with a password
|
||||||
|
|
||||||
here is a MUCH BETTER ALTERNATIVE (which also works on Windows):
|
|
||||||
https://github.com/steinuil/kameloso
|
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
|
|
||||||
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
|
||||||
|
|
670
bin/partyfuse.py
670
bin/partyfuse.py
File diff suppressed because it is too large
Load diff
937
bin/u2c.py
937
bin/u2c.py
File diff suppressed because it is too large
Load diff
|
@ -1,76 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import zmq
|
|
||||||
|
|
||||||
"""
|
|
||||||
zmq-recv.py: demo zmq receiver
|
|
||||||
2025-01-22, v1.0, ed <irc.rizon.net>, MIT-Licensed
|
|
||||||
https://github.com/9001/copyparty/blob/hovudstraum/bin/zmq-recv.py
|
|
||||||
|
|
||||||
basic zmq-server to receive events from copyparty; try one of
|
|
||||||
the below and then "send a message to serverlog" in the web-ui:
|
|
||||||
|
|
||||||
1) dumb fire-and-forget to any and all listeners;
|
|
||||||
run this script with "sub" and run copyparty with this:
|
|
||||||
--xm zmq:pub:tcp://*:5556
|
|
||||||
|
|
||||||
2) one lucky listener gets the message, blocks if no listeners:
|
|
||||||
run this script with "pull" and run copyparty with this:
|
|
||||||
--xm t3,zmq:push:tcp://*:5557
|
|
||||||
|
|
||||||
3) blocking syn/ack mode, client must ack each message;
|
|
||||||
run this script with "rep" and run copyparty with this:
|
|
||||||
--xm t3,zmq:req:tcp://localhost:5555
|
|
||||||
|
|
||||||
note: to conditionally block uploads based on message contents,
|
|
||||||
use rep_server to answer with "return 1" and run copyparty with
|
|
||||||
--xau t3,c,zmq:req:tcp://localhost:5555
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
ctx = zmq.Context()
|
|
||||||
|
|
||||||
|
|
||||||
def sub_server():
|
|
||||||
# PUB/SUB allows any number of servers/clients, and
|
|
||||||
# messages are fire-and-forget
|
|
||||||
sck = ctx.socket(zmq.SUB)
|
|
||||||
sck.connect("tcp://localhost:5556")
|
|
||||||
sck.setsockopt_string(zmq.SUBSCRIBE, "")
|
|
||||||
while True:
|
|
||||||
print("copyparty says %r" % (sck.recv_string(),))
|
|
||||||
|
|
||||||
|
|
||||||
def pull_server():
|
|
||||||
# PUSH/PULL allows any number of servers/clients, and
|
|
||||||
# each message is sent to a exactly one PULL client
|
|
||||||
sck = ctx.socket(zmq.PULL)
|
|
||||||
sck.connect("tcp://localhost:5557")
|
|
||||||
while True:
|
|
||||||
print("copyparty says %r" % (sck.recv_string(),))
|
|
||||||
|
|
||||||
|
|
||||||
def rep_server():
|
|
||||||
# REP/REQ is a server/client pair where each message must be
|
|
||||||
# acked by the other before another message can be sent, so
|
|
||||||
# copyparty will do a blocking-wait for the ack
|
|
||||||
sck = ctx.socket(zmq.REP)
|
|
||||||
sck.bind("tcp://*:5555")
|
|
||||||
while True:
|
|
||||||
print("copyparty says %r" % (sck.recv_string(),))
|
|
||||||
reply = b"thx"
|
|
||||||
# reply = b"return 1" # non-zero to block an upload
|
|
||||||
sck.send(reply)
|
|
||||||
|
|
||||||
|
|
||||||
mode = sys.argv[1].lower() if len(sys.argv) > 1 else ""
|
|
||||||
|
|
||||||
if mode == "sub":
|
|
||||||
sub_server()
|
|
||||||
elif mode == "pull":
|
|
||||||
pull_server()
|
|
||||||
elif mode == "rep":
|
|
||||||
rep_server()
|
|
||||||
else:
|
|
||||||
print("specify mode as first argument: SUB | PULL | REP")
|
|
|
@ -12,21 +12,13 @@
|
||||||
* assumes the webserver and copyparty is running on the same server/IP
|
* assumes the webserver and copyparty is running on the same server/IP
|
||||||
* modify `10.13.1.1` as necessary if you wish to support browsers without javascript
|
* modify `10.13.1.1` as necessary if you wish to support browsers without javascript
|
||||||
|
|
||||||
### [`sharex.sxcu`](sharex.sxcu) - Windows screenshot uploader
|
### [`sharex.sxcu`](sharex.sxcu)
|
||||||
* [sharex](https://getsharex.com/) config file to upload screenshots and grab the URL
|
* sharex config file to upload screenshots and grab the URL
|
||||||
* `RequestURL`: full URL to the target folder
|
* `RequestURL`: full URL to the target folder
|
||||||
* `pw`: password (remove the `pw` line if anon-write)
|
* `pw`: password (remove the `pw` line if anon-write)
|
||||||
* the `act:bput` thing is optional since copyparty v1.9.29
|
* the `act:bput` thing is optional since copyparty v1.9.29
|
||||||
* using an older sharex version, maybe sharex v12.1.1 for example? dw fam i got your back 👉😎👉 [`sharex12.sxcu`](sharex12.sxcu)
|
* using an older sharex version, maybe sharex v12.1.1 for example? dw fam i got your back 👉😎👉 [`sharex12.sxcu`](sharex12.sxcu)
|
||||||
|
|
||||||
### [`ishare.iscu`](ishare.iscu) - MacOS screenshot uploader
|
|
||||||
* [ishare](https://isharemac.app/) config file to upload screenshots and grab the URL
|
|
||||||
* `RequestURL`: full URL to the target folder
|
|
||||||
* `pw`: password (remove the `pw` line if anon-write)
|
|
||||||
|
|
||||||
### [`flameshot.sh`](flameshot.sh) - Linux screenshot uploader
|
|
||||||
* takes a screenshot with [flameshot](https://flameshot.org/) on Linux, uploads it, and writes the URL to clipboard
|
|
||||||
|
|
||||||
### [`send-to-cpp.contextlet.json`](send-to-cpp.contextlet.json)
|
### [`send-to-cpp.contextlet.json`](send-to-cpp.contextlet.json)
|
||||||
* browser integration, kind of? custom rightclick actions and stuff
|
* browser integration, kind of? custom rightclick actions and stuff
|
||||||
* rightclick a pic and send it to copyparty straight from your browser
|
* rightclick a pic and send it to copyparty straight from your browser
|
||||||
|
@ -50,9 +42,6 @@
|
||||||
* give a 3rd argument to install it to your copyparty config
|
* give a 3rd argument to install it to your copyparty config
|
||||||
* systemd service at [`systemd/cfssl.service`](systemd/cfssl.service)
|
* systemd service at [`systemd/cfssl.service`](systemd/cfssl.service)
|
||||||
|
|
||||||
### [`zfs-tune.py`](zfs-tune.py)
|
|
||||||
* optimizes databases for optimal performance when stored on a zfs filesystem; also see [openzfs docs](https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Workload%20Tuning.html#database-workloads) and specifically the SQLite subsection
|
|
||||||
|
|
||||||
# OS integration
|
# OS integration
|
||||||
init-scripts to start copyparty as a service
|
init-scripts to start copyparty as a service
|
||||||
* [`systemd/copyparty.service`](systemd/copyparty.service) runs the sfx normally
|
* [`systemd/copyparty.service`](systemd/copyparty.service) runs the sfx normally
|
||||||
|
@ -61,10 +50,5 @@ init-scripts to start copyparty as a service
|
||||||
* [`openrc/copyparty`](openrc/copyparty)
|
* [`openrc/copyparty`](openrc/copyparty)
|
||||||
|
|
||||||
# Reverse-proxy
|
# Reverse-proxy
|
||||||
copyparty supports running behind another webserver
|
copyparty has basic support for running behind another webserver
|
||||||
* [`apache/copyparty.conf`](apache/copyparty.conf)
|
* [`nginx/copyparty.conf`](nginx/copyparty.conf)
|
||||||
* [`haproxy/copyparty.conf`](haproxy/copyparty.conf)
|
|
||||||
* [`lighttpd/subdomain.conf`](lighttpd/subdomain.conf)
|
|
||||||
* [`lighttpd/subpath.conf`](lighttpd/subpath.conf)
|
|
||||||
* [`nginx/copyparty.conf`](nginx/copyparty.conf) -- recommended
|
|
||||||
* [`traefik/copyparty.yaml`](traefik/copyparty.yaml)
|
|
||||||
|
|
|
@ -1,29 +1,14 @@
|
||||||
# if you would like to use unix-sockets (recommended),
|
# when running copyparty behind a reverse proxy,
|
||||||
# you must run copyparty with one of the following:
|
# the following arguments are recommended:
|
||||||
#
|
#
|
||||||
# -i unix:777:/dev/shm/party.sock
|
# -i 127.0.0.1 only accept connections from nginx
|
||||||
# -i unix:777:/dev/shm/party.sock,127.0.0.1
|
|
||||||
#
|
#
|
||||||
# if you are doing location-based proxying (such as `/stuff` below)
|
# if you are doing location-based proxying (such as `/stuff` below)
|
||||||
# you must run copyparty with --rp-loc=stuff
|
# you must run copyparty with --rp-loc=stuff
|
||||||
#
|
#
|
||||||
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
|
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
|
||||||
|
|
||||||
|
|
||||||
LoadModule proxy_module modules/mod_proxy.so
|
LoadModule proxy_module modules/mod_proxy.so
|
||||||
|
ProxyPass "/stuff" "http://127.0.0.1:3923/stuff"
|
||||||
|
# do not specify ProxyPassReverse
|
||||||
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
|
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
|
||||||
# NOTE: do not specify ProxyPassReverse
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
## then, enable one of the below:
|
|
||||||
|
|
||||||
# use subdomain proxying to unix-socket (best)
|
|
||||||
ProxyPass "/" "unix:///dev/shm/party.sock|http://whatever/"
|
|
||||||
|
|
||||||
# use subdomain proxying to 127.0.0.1 (slower)
|
|
||||||
#ProxyPass "/" "http://127.0.0.1:3923/"
|
|
||||||
|
|
||||||
# use subpath proxying to 127.0.0.1 (slow and maybe buggy)
|
|
||||||
#ProxyPass "/stuff" "http://127.0.0.1:3923/stuff"
|
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# take a screenshot with flameshot and send it to copyparty;
|
|
||||||
# the image url will be placed on your clipboard
|
|
||||||
|
|
||||||
password=wark
|
|
||||||
url=https://a.ocv.me/up/
|
|
||||||
filename=$(date +%Y-%m%d-%H%M%S).png
|
|
||||||
|
|
||||||
flameshot gui -s -r |
|
|
||||||
curl -T- $url$filename?pw=$password |
|
|
||||||
tail -n 1 |
|
|
||||||
xsel -ib
|
|
|
@ -1,24 +0,0 @@
|
||||||
# this config is essentially two separate examples;
|
|
||||||
#
|
|
||||||
# foo1 connects to copyparty using tcp, and
|
|
||||||
# foo2 uses unix-sockets for 27% higher performance
|
|
||||||
#
|
|
||||||
# to use foo2 you must run copyparty with one of the following:
|
|
||||||
#
|
|
||||||
# -i unix:777:/dev/shm/party.sock
|
|
||||||
# -i unix:777:/dev/shm/party.sock,127.0.0.1
|
|
||||||
|
|
||||||
defaults
|
|
||||||
mode http
|
|
||||||
option forwardfor
|
|
||||||
timeout connect 1s
|
|
||||||
timeout client 610s
|
|
||||||
timeout server 610s
|
|
||||||
|
|
||||||
listen foo1
|
|
||||||
bind *:8081
|
|
||||||
server srv1 127.0.0.1:3923 maxconn 512
|
|
||||||
|
|
||||||
listen foo2
|
|
||||||
bind *:8082
|
|
||||||
server srv1 /dev/shm/party.sock maxconn 512
|
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"Name": "copyparty",
|
|
||||||
"RequestURL": "http://127.0.0.1:3923/screenshots/",
|
|
||||||
"Headers": {
|
|
||||||
"pw": "PUT_YOUR_PASSWORD_HERE_MY_DUDE",
|
|
||||||
"accept": "json"
|
|
||||||
},
|
|
||||||
"FileFormName": "f",
|
|
||||||
"ResponseURL": "{{fileurl}}"
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
# example usage for benchmarking:
|
|
||||||
#
|
|
||||||
# taskset -c 1 lighttpd -Df ~/dev/copyparty/contrib/lighttpd/subdomain.conf
|
|
||||||
#
|
|
||||||
# lighttpd can connect to copyparty using either tcp (127.0.0.1)
|
|
||||||
# or a unix-socket, but unix-sockets are 37% faster because
|
|
||||||
# lighttpd doesn't reuse tcp connections, so we're doing unix-sockets
|
|
||||||
#
|
|
||||||
# this means we must run copyparty with one of the following:
|
|
||||||
#
|
|
||||||
# -i unix:777:/dev/shm/party.sock
|
|
||||||
# -i unix:777:/dev/shm/party.sock,127.0.0.1
|
|
||||||
#
|
|
||||||
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
|
|
||||||
|
|
||||||
server.port = 80
|
|
||||||
server.document-root = "/var/empty"
|
|
||||||
server.upload-dirs = ( "/dev/shm", "/tmp" )
|
|
||||||
server.modules = ( "mod_proxy" )
|
|
||||||
proxy.forwarded = ( "for" => 1, "proto" => 1 )
|
|
||||||
proxy.server = ( "" => ( ( "host" => "/dev/shm/party.sock" ) ) )
|
|
||||||
|
|
||||||
# if you really need to use tcp instead of unix-sockets, do this instead:
|
|
||||||
#proxy.server = ( "" => ( ( "host" => "127.0.0.1", "port" => "3923" ) ) )
|
|
|
@ -1,31 +0,0 @@
|
||||||
# example usage for benchmarking:
|
|
||||||
#
|
|
||||||
# taskset -c 1 lighttpd -Df ~/dev/copyparty/contrib/lighttpd/subpath.conf
|
|
||||||
#
|
|
||||||
# lighttpd can connect to copyparty using either tcp (127.0.0.1)
|
|
||||||
# or a unix-socket, but unix-sockets are 37% faster because
|
|
||||||
# lighttpd doesn't reuse tcp connections, so we're doing unix-sockets
|
|
||||||
#
|
|
||||||
# this means we must run copyparty with one of the following:
|
|
||||||
#
|
|
||||||
# -i unix:777:/dev/shm/party.sock
|
|
||||||
# -i unix:777:/dev/shm/party.sock,127.0.0.1
|
|
||||||
#
|
|
||||||
# also since this example proxies a subpath instead of the
|
|
||||||
# recommended subdomain-proxying, we must also specify this:
|
|
||||||
#
|
|
||||||
# --rp-loc files
|
|
||||||
#
|
|
||||||
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
|
|
||||||
|
|
||||||
server.port = 80
|
|
||||||
server.document-root = "/var/empty"
|
|
||||||
server.upload-dirs = ( "/dev/shm", "/tmp" )
|
|
||||||
server.modules = ( "mod_proxy" )
|
|
||||||
$HTTP["url"] =~ "^/files" {
|
|
||||||
proxy.forwarded = ( "for" => 1, "proto" => 1 )
|
|
||||||
proxy.server = ( "" => ( ( "host" => "/dev/shm/party.sock" ) ) )
|
|
||||||
|
|
||||||
# if you really need to use tcp instead of unix-sockets, do this instead:
|
|
||||||
#proxy.server = ( "" => ( ( "host" => "127.0.0.1", "port" => "3923" ) ) )
|
|
||||||
}
|
|
|
@ -1,67 +1,29 @@
|
||||||
# look for "max clients:" when starting copyparty, as nginx should
|
# when running copyparty behind a reverse proxy,
|
||||||
# not accept more consecutive clients than what copyparty is able to;
|
# the following arguments are recommended:
|
||||||
|
#
|
||||||
|
# -i 127.0.0.1 only accept connections from nginx
|
||||||
|
#
|
||||||
|
# -nc must match or exceed the webserver's max number of concurrent clients;
|
||||||
|
# copyparty default is 1024 if OS permits it (see "max clients:" on startup),
|
||||||
# nginx default is 512 (worker_processes 1, worker_connections 512)
|
# nginx default is 512 (worker_processes 1, worker_connections 512)
|
||||||
#
|
#
|
||||||
# ======================================================================
|
# you may also consider adding -j0 for CPU-intensive configurations
|
||||||
#
|
# (5'000 requests per second, or 20gbps upload/download in parallel)
|
||||||
# to reverse-proxy a specific path/subpath/location below a domain
|
|
||||||
# (rather than a complete subdomain), for example "/qw/er", you must
|
|
||||||
# run copyparty with --rp-loc /qw/as and also change the following:
|
|
||||||
# location / {
|
|
||||||
# proxy_pass http://cpp_tcp;
|
|
||||||
# to this:
|
|
||||||
# location /qw/er/ {
|
|
||||||
# proxy_pass http://cpp_tcp/qw/er/;
|
|
||||||
#
|
|
||||||
# ======================================================================
|
|
||||||
#
|
|
||||||
# rarely, in some extreme usecases, it can be good to add -j0
|
|
||||||
# (40'000 requests per second, or 20gbps upload/download in parallel)
|
|
||||||
# but this is usually counterproductive and slightly buggy
|
|
||||||
#
|
|
||||||
# ======================================================================
|
|
||||||
#
|
#
|
||||||
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
|
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
|
||||||
#
|
#
|
||||||
# ======================================================================
|
# if you are behind cloudflare (or another protection service),
|
||||||
#
|
|
||||||
# if you are behind cloudflare (or another CDN/WAF/protection service),
|
|
||||||
# remember to reject all connections which are not coming from your
|
# remember to reject all connections which are not coming from your
|
||||||
# protection service -- for cloudflare in particular, you can
|
# protection service -- for cloudflare in particular, you can
|
||||||
# generate the list of permitted IP ranges like so:
|
# generate the list of permitted IP ranges like so:
|
||||||
# (curl -s https://www.cloudflare.com/ips-v{4,6} | sed 's/^/allow /; s/$/;/'; echo; echo "deny all;") > /etc/nginx/cloudflare-only.conf
|
# (curl -s https://www.cloudflare.com/ips-v{4,6} | sed 's/^/allow /; s/$/;/'; echo; echo "deny all;") > /etc/nginx/cloudflare-only.conf
|
||||||
#
|
#
|
||||||
# and then enable it below by uncomenting the cloudflare-only.conf line
|
# and then enable it below by uncomenting the cloudflare-only.conf line
|
||||||
#
|
|
||||||
# ======================================================================
|
|
||||||
|
|
||||||
|
|
||||||
upstream cpp_tcp {
|
|
||||||
# alternative 1: connect to copyparty using tcp;
|
|
||||||
# cpp_uds is slightly faster and more secure, but
|
|
||||||
# cpp_tcp is easier to setup and "just works"
|
|
||||||
# ...you should however restrict copyparty to only
|
|
||||||
# accept connections from nginx by adding these args:
|
|
||||||
# -i 127.0.0.1
|
|
||||||
|
|
||||||
|
upstream cpp {
|
||||||
server 127.0.0.1:3923 fail_timeout=1s;
|
server 127.0.0.1:3923 fail_timeout=1s;
|
||||||
keepalive 1;
|
keepalive 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
upstream cpp_uds {
|
|
||||||
# alternative 2: unix-socket, aka. "unix domain socket";
|
|
||||||
# 5-10% faster, and better isolation from other software,
|
|
||||||
# but there must be at least one unix-group which both
|
|
||||||
# nginx and copyparty is a member of; if that group is
|
|
||||||
# "www" then run copyparty with the following args:
|
|
||||||
# -i unix:770:www:/dev/shm/party.sock
|
|
||||||
|
|
||||||
server unix:/dev/shm/party.sock fail_timeout=1s;
|
|
||||||
keepalive 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
listen [::]:443 ssl;
|
listen [::]:443 ssl;
|
||||||
|
@ -72,30 +34,24 @@ server {
|
||||||
#include /etc/nginx/cloudflare-only.conf;
|
#include /etc/nginx/cloudflare-only.conf;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
# recommendation: replace cpp_tcp with cpp_uds below
|
proxy_pass http://cpp;
|
||||||
proxy_pass http://cpp_tcp;
|
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
# disable buffering (next 4 lines)
|
# disable buffering (next 4 lines)
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
client_max_body_size 0;
|
client_max_body_size 0;
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
proxy_request_buffering off;
|
proxy_request_buffering off;
|
||||||
# improve download speed from 600 to 1500 MiB/s
|
|
||||||
proxy_buffers 32 8k;
|
|
||||||
proxy_buffer_size 16k;
|
|
||||||
proxy_busy_buffers_size 24k;
|
|
||||||
|
|
||||||
proxy_set_header Connection "Keep-Alive";
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
# NOTE: with cloudflare you want this X-Forwarded-For instead:
|
# NOTE: with cloudflare you want this instead:
|
||||||
#proxy_set_header X-Forwarded-For $http_cf_connecting_ip;
|
#proxy_set_header X-Forwarded-For $http_cf_connecting_ip;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Connection "Keep-Alive";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# default client_max_body_size (1M) blocks uploads larger than 256 MiB
|
# default client_max_body_size (1M) blocks uploads larger than 256 MiB
|
||||||
client_max_body_size 1024M;
|
client_max_body_size 1024M;
|
||||||
client_header_timeout 610m;
|
client_header_timeout 610m;
|
||||||
|
|
|
@ -1,28 +1,23 @@
|
||||||
{
|
{ config, pkgs, lib, ... }:
|
||||||
config,
|
|
||||||
pkgs,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
with lib;
|
with lib;
|
||||||
|
|
||||||
let
|
let
|
||||||
mkKeyValue =
|
mkKeyValue = key: value:
|
||||||
key: value:
|
|
||||||
if value == true then
|
if value == true then
|
||||||
# sets with a true boolean value are coerced to just the key name
|
# sets with a true boolean value are coerced to just the key name
|
||||||
key
|
key
|
||||||
else if value == false then
|
else if value == false then
|
||||||
# or omitted completely when false
|
# or omitted completely when false
|
||||||
""
|
""
|
||||||
else
|
else
|
||||||
(generators.mkKeyValueDefault { inherit mkValueString; } ": " key value);
|
(generators.mkKeyValueDefault { inherit mkValueString; } ": " key value);
|
||||||
|
|
||||||
mkAttrsString = value: (generators.toKeyValue { inherit mkKeyValue; } value);
|
mkAttrsString = value: (generators.toKeyValue { inherit mkKeyValue; } value);
|
||||||
|
|
||||||
mkValueString =
|
mkValueString = value:
|
||||||
value:
|
|
||||||
if isList value then
|
if isList value then
|
||||||
(concatStringsSep "," (map mkValueString value))
|
(concatStringsSep ", " (map mkValueString value))
|
||||||
else if isAttrs value then
|
else if isAttrs value then
|
||||||
"\n" + (mkAttrsString value)
|
"\n" + (mkAttrsString value)
|
||||||
else
|
else
|
||||||
|
@ -54,14 +49,13 @@ let
|
||||||
${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)}
|
${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
name = "copyparty";
|
||||||
cfg = config.services.copyparty;
|
cfg = config.services.copyparty;
|
||||||
configFile = pkgs.writeText "copyparty.conf" configStr;
|
configFile = pkgs.writeText "${name}.conf" configStr;
|
||||||
runtimeConfigPath = "/run/copyparty/copyparty.conf";
|
runtimeConfigPath = "/run/${name}/${name}.conf";
|
||||||
externalCacheDir = "/var/cache/copyparty";
|
home = "/var/lib/${name}";
|
||||||
externalStateDir = "/var/lib/copyparty";
|
defaultShareDir = "${home}/data";
|
||||||
defaultShareDir = "${externalStateDir}/data";
|
in {
|
||||||
in
|
|
||||||
{
|
|
||||||
options.services.copyparty = {
|
options.services.copyparty = {
|
||||||
enable = mkEnableOption "web-based file manager";
|
enable = mkEnableOption "web-based file manager";
|
||||||
|
|
||||||
|
@ -74,35 +68,6 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
mkHashWrapper = mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = true;
|
|
||||||
description = ''
|
|
||||||
Make a shell script wrapper called 'copyparty-hash' with all options set here,
|
|
||||||
that launches the hashing cli.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
user = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "copyparty";
|
|
||||||
description = ''
|
|
||||||
The user that copyparty will run under.
|
|
||||||
|
|
||||||
If changed from default, you are responsible for making sure the user exists.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
group = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "copyparty";
|
|
||||||
description = ''
|
|
||||||
The group that copyparty will run under.
|
|
||||||
|
|
||||||
If changed from default, you are responsible for making sure the user exists.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
openFilesLimit = mkOption {
|
openFilesLimit = mkOption {
|
||||||
default = 4096;
|
default = 4096;
|
||||||
type = types.either types.int types.str;
|
type = types.either types.int types.str;
|
||||||
|
@ -114,41 +79,33 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Global settings to apply.
|
Global settings to apply.
|
||||||
Directly maps to values in the [global] section of the copyparty config.
|
Directly maps to values in the [global] section of the copyparty config.
|
||||||
Cannot set "c" or "hist", those are set by this module.
|
|
||||||
See `${getExe cfg.package} --help` for more details.
|
See `${getExe cfg.package} --help` for more details.
|
||||||
'';
|
'';
|
||||||
default = {
|
default = {
|
||||||
i = "127.0.0.1";
|
i = "127.0.0.1";
|
||||||
no-reload = true;
|
no-reload = true;
|
||||||
hist = externalCacheDir;
|
|
||||||
};
|
};
|
||||||
example = literalExpression ''
|
example = literalExpression ''
|
||||||
{
|
{
|
||||||
i = "0.0.0.0";
|
i = "0.0.0.0";
|
||||||
no-reload = true;
|
no-reload = true;
|
||||||
hist = ${externalCacheDir};
|
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
accounts = mkOption {
|
accounts = mkOption {
|
||||||
type = types.attrsOf (
|
type = types.attrsOf (types.submodule ({ ... }: {
|
||||||
types.submodule (
|
options = {
|
||||||
{ ... }:
|
passwordFile = mkOption {
|
||||||
{
|
type = types.str;
|
||||||
options = {
|
description = ''
|
||||||
passwordFile = mkOption {
|
Runtime file path to a file containing the user password.
|
||||||
type = types.str;
|
Must be readable by the copyparty user.
|
||||||
description = ''
|
'';
|
||||||
Runtime file path to a file containing the user password.
|
example = "/run/keys/copyparty/ed";
|
||||||
Must be readable by the copyparty user.
|
};
|
||||||
'';
|
};
|
||||||
example = "/run/keys/copyparty/ed";
|
}));
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
description = ''
|
description = ''
|
||||||
A set of copyparty accounts to create.
|
A set of copyparty accounts to create.
|
||||||
'';
|
'';
|
||||||
|
@ -161,81 +118,74 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
volumes = mkOption {
|
volumes = mkOption {
|
||||||
type = types.attrsOf (
|
type = types.attrsOf (types.submodule ({ ... }: {
|
||||||
types.submodule (
|
options = {
|
||||||
{ ... }:
|
path = mkOption {
|
||||||
{
|
type = types.str;
|
||||||
options = {
|
description = ''
|
||||||
path = mkOption {
|
Path of a directory to share.
|
||||||
type = types.path;
|
'';
|
||||||
description = ''
|
};
|
||||||
Path of a directory to share.
|
access = mkOption {
|
||||||
'';
|
type = types.attrs;
|
||||||
};
|
description = ''
|
||||||
access = mkOption {
|
Attribute list of permissions and the users to apply them to.
|
||||||
type = types.attrs;
|
|
||||||
description = ''
|
|
||||||
Attribute list of permissions and the users to apply them to.
|
|
||||||
|
|
||||||
The key must be a string containing any combination of allowed permission:
|
The key must be a string containing any combination of allowed permission:
|
||||||
"r" (read): list folder contents, download files
|
"r" (read): list folder contents, download files
|
||||||
"w" (write): upload files; need "r" to see the uploads
|
"w" (write): upload files; need "r" to see the uploads
|
||||||
"m" (move): move files and folders; need "w" at destination
|
"m" (move): move files and folders; need "w" at destination
|
||||||
"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
|
||||||
"h" (html): "get", but folders return their index.html
|
"h" (html): "get", but folders return their index.html
|
||||||
"a" (admin): can see uploader IPs, config-reload
|
"a" (admin): can see uploader IPs, config-reload
|
||||||
|
|
||||||
For example: "rwmd"
|
For example: "rwmd"
|
||||||
|
|
||||||
The value must be one of:
|
The value must be one of:
|
||||||
an account name, defined in `accounts`
|
an account name, defined in `accounts`
|
||||||
a list of account names
|
a list of account names
|
||||||
"*", which means "any account"
|
"*", which means "any account"
|
||||||
'';
|
'';
|
||||||
example = literalExpression ''
|
example = literalExpression ''
|
||||||
{
|
{
|
||||||
# wG = write-upget = see your own uploads only
|
# wG = write-upget = see your own uploads only
|
||||||
wG = "*";
|
wG = "*";
|
||||||
# read-write-modify-delete for users "ed" and "k"
|
# read-write-modify-delete for users "ed" and "k"
|
||||||
rwmd = ["ed" "k"];
|
rwmd = ["ed" "k"];
|
||||||
};
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
flags = mkOption {
|
'';
|
||||||
type = types.attrs;
|
};
|
||||||
description = ''
|
flags = mkOption {
|
||||||
Attribute list of volume flags to apply.
|
type = types.attrs;
|
||||||
See `${getExe cfg.package} --help-flags` for more details.
|
description = ''
|
||||||
'';
|
Attribute list of volume flags to apply.
|
||||||
example = literalExpression ''
|
See `${getExe cfg.package} --help-flags` for more details.
|
||||||
{
|
'';
|
||||||
# "fk" enables filekeys (necessary for upget permission) (4 chars long)
|
example = literalExpression ''
|
||||||
fk = 4;
|
{
|
||||||
# scan for new files every 60sec
|
# "fk" enables filekeys (necessary for upget permission) (4 chars long)
|
||||||
scan = 60;
|
fk = 4;
|
||||||
# volflag "e2d" enables the uploads database
|
# scan for new files every 60sec
|
||||||
e2d = true;
|
scan = 60;
|
||||||
# "d2t" disables multimedia parsers (in case the uploads are malicious)
|
# volflag "e2d" enables the uploads database
|
||||||
d2t = true;
|
e2d = true;
|
||||||
# skips hashing file contents if path matches *.iso
|
# "d2t" disables multimedia parsers (in case the uploads are malicious)
|
||||||
nohash = "\.iso$";
|
d2t = true;
|
||||||
};
|
# skips hashing file contents if path matches *.iso
|
||||||
'';
|
nohash = "\.iso$";
|
||||||
default = { };
|
|
||||||
};
|
};
|
||||||
};
|
'';
|
||||||
}
|
default = { };
|
||||||
)
|
};
|
||||||
);
|
};
|
||||||
|
}));
|
||||||
description = "A set of copyparty volumes to create";
|
description = "A set of copyparty volumes to create";
|
||||||
default = {
|
default = {
|
||||||
"/" = {
|
"/" = {
|
||||||
path = defaultShareDir;
|
path = defaultShareDir;
|
||||||
access = {
|
access = { r = "*"; };
|
||||||
r = "*";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
example = literalExpression ''
|
example = literalExpression ''
|
||||||
|
@ -254,122 +204,80 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable (
|
config = mkIf cfg.enable {
|
||||||
let
|
systemd.services.copyparty = {
|
||||||
command = "${getExe cfg.package} -c ${runtimeConfigPath}";
|
description = "http file sharing hub";
|
||||||
in
|
wantedBy = [ "multi-user.target" ];
|
||||||
{
|
|
||||||
systemd.services.copyparty = {
|
|
||||||
description = "http file sharing hub";
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
|
|
||||||
environment = {
|
environment = {
|
||||||
PYTHONUNBUFFERED = "true";
|
PYTHONUNBUFFERED = "true";
|
||||||
XDG_CONFIG_HOME = externalStateDir;
|
XDG_CONFIG_HOME = "${home}/.config";
|
||||||
};
|
|
||||||
|
|
||||||
preStart =
|
|
||||||
let
|
|
||||||
replaceSecretCommand =
|
|
||||||
name: attrs:
|
|
||||||
"${getExe pkgs.replace-secret} '${passwordPlaceholder name}' '${attrs.passwordFile}' ${runtimeConfigPath}";
|
|
||||||
in
|
|
||||||
''
|
|
||||||
set -euo pipefail
|
|
||||||
install -m 600 ${configFile} ${runtimeConfigPath}
|
|
||||||
${concatStringsSep "\n" (mapAttrsToList replaceSecretCommand cfg.accounts)}
|
|
||||||
'';
|
|
||||||
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "simple";
|
|
||||||
ExecStart = command;
|
|
||||||
# Hardening options
|
|
||||||
User = cfg.user;
|
|
||||||
Group = cfg.group;
|
|
||||||
RuntimeDirectory = [ "copyparty" ];
|
|
||||||
RuntimeDirectoryMode = "0700";
|
|
||||||
StateDirectory = [ "copyparty" ];
|
|
||||||
StateDirectoryMode = "0700";
|
|
||||||
CacheDirectory = lib.mkIf (cfg.settings ? hist) [ "copyparty" ];
|
|
||||||
CacheDirectoryMode = lib.mkIf (cfg.settings ? hist) "0700";
|
|
||||||
WorkingDirectory = externalStateDir;
|
|
||||||
BindReadOnlyPaths = [
|
|
||||||
"/nix/store"
|
|
||||||
"-/etc/resolv.conf"
|
|
||||||
"-/etc/nsswitch.conf"
|
|
||||||
"-/etc/group"
|
|
||||||
"-/etc/hosts"
|
|
||||||
"-/etc/localtime"
|
|
||||||
] ++ (mapAttrsToList (k: v: "-${v.passwordFile}") cfg.accounts);
|
|
||||||
BindPaths =
|
|
||||||
(if cfg.settings ? hist then [ cfg.settings.hist ] else [ ])
|
|
||||||
++ [ externalStateDir ]
|
|
||||||
++ (mapAttrsToList (k: v: v.path) cfg.volumes);
|
|
||||||
# ProtectSystem = "strict";
|
|
||||||
# Note that unlike what 'ro' implies,
|
|
||||||
# this actually makes it impossible to read anything in the root FS,
|
|
||||||
# except for things explicitly mounted via `RuntimeDirectory`, `StateDirectory`, `CacheDirectory`, and `BindReadOnlyPaths`.
|
|
||||||
# This is because TemporaryFileSystem creates a *new* *empty* filesystem for the process, so only bindmounts are visible.
|
|
||||||
TemporaryFileSystem = "/:ro";
|
|
||||||
PrivateTmp = true;
|
|
||||||
PrivateDevices = true;
|
|
||||||
ProtectKernelTunables = true;
|
|
||||||
ProtectControlGroups = true;
|
|
||||||
RestrictSUIDSGID = true;
|
|
||||||
PrivateMounts = true;
|
|
||||||
ProtectKernelModules = true;
|
|
||||||
ProtectKernelLogs = true;
|
|
||||||
ProtectHostname = true;
|
|
||||||
ProtectClock = true;
|
|
||||||
ProtectProc = "invisible";
|
|
||||||
ProcSubset = "pid";
|
|
||||||
RestrictNamespaces = true;
|
|
||||||
RemoveIPC = true;
|
|
||||||
UMask = "0077";
|
|
||||||
LimitNOFILE = cfg.openFilesLimit;
|
|
||||||
NoNewPrivileges = true;
|
|
||||||
LockPersonality = true;
|
|
||||||
RestrictRealtime = true;
|
|
||||||
MemoryDenyWriteExecute = true;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
# ensure volumes exist:
|
preStart = let
|
||||||
systemd.tmpfiles.settings."copyparty" = (
|
replaceSecretCommand = name: attrs:
|
||||||
lib.attrsets.mapAttrs' (
|
"${getExe pkgs.replace-secret} '${
|
||||||
name: value:
|
passwordPlaceholder name
|
||||||
lib.attrsets.nameValuePair (value.path) {
|
}' '${attrs.passwordFile}' ${runtimeConfigPath}";
|
||||||
d = {
|
in ''
|
||||||
#: in front of things means it wont change it if the directory already exists.
|
set -euo pipefail
|
||||||
group = ":${cfg.group}";
|
install -m 600 ${configFile} ${runtimeConfigPath}
|
||||||
user = ":${cfg.user}";
|
${concatStringsSep "\n"
|
||||||
mode = ":755";
|
(mapAttrsToList replaceSecretCommand cfg.accounts)}
|
||||||
};
|
'';
|
||||||
}
|
|
||||||
) cfg.volumes
|
|
||||||
);
|
|
||||||
|
|
||||||
users.groups.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") { };
|
serviceConfig = {
|
||||||
users.users.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") {
|
Type = "simple";
|
||||||
description = "Service user for copyparty";
|
ExecStart = "${getExe cfg.package} -c ${runtimeConfigPath}";
|
||||||
group = "copyparty";
|
|
||||||
home = externalStateDir;
|
# Hardening options
|
||||||
isSystemUser = true;
|
User = "copyparty";
|
||||||
|
Group = "copyparty";
|
||||||
|
RuntimeDirectory = name;
|
||||||
|
RuntimeDirectoryMode = "0700";
|
||||||
|
StateDirectory = [ name "${name}/data" "${name}/.config" ];
|
||||||
|
StateDirectoryMode = "0700";
|
||||||
|
WorkingDirectory = home;
|
||||||
|
TemporaryFileSystem = "/:ro";
|
||||||
|
BindReadOnlyPaths = [
|
||||||
|
"/nix/store"
|
||||||
|
"-/etc/resolv.conf"
|
||||||
|
"-/etc/nsswitch.conf"
|
||||||
|
"-/etc/hosts"
|
||||||
|
"-/etc/localtime"
|
||||||
|
] ++ (mapAttrsToList (k: v: "-${v.passwordFile}") cfg.accounts);
|
||||||
|
BindPaths = [ home ] ++ (mapAttrsToList (k: v: v.path) cfg.volumes);
|
||||||
|
# Would re-mount paths ignored by temporary root
|
||||||
|
#ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
PrivateDevices = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
PrivateMounts = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectKernelLogs = true;
|
||||||
|
ProtectHostname = true;
|
||||||
|
ProtectClock = true;
|
||||||
|
ProtectProc = "invisible";
|
||||||
|
ProcSubset = "pid";
|
||||||
|
RestrictNamespaces = true;
|
||||||
|
RemoveIPC = true;
|
||||||
|
UMask = "0077";
|
||||||
|
LimitNOFILE = cfg.openFilesLimit;
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
LockPersonality = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
};
|
};
|
||||||
environment.systemPackages = lib.mkIf cfg.mkHashWrapper [
|
};
|
||||||
(pkgs.writeShellScriptBin "copyparty-hash" ''
|
|
||||||
set -a # automatically export variables
|
|
||||||
# set same environment variables as the systemd service
|
|
||||||
${lib.pipe config.systemd.services.copyparty.environment [
|
|
||||||
(lib.filterAttrs (n: v: v != null && n != "PATH"))
|
|
||||||
(lib.mapAttrs (_: v: "${v}"))
|
|
||||||
(lib.toShellVars)
|
|
||||||
]}
|
|
||||||
PATH=${config.systemd.services.copyparty.environment.PATH}:$PATH
|
|
||||||
|
|
||||||
exec ${command} --ah-cli
|
users.groups.copyparty = { };
|
||||||
'')
|
users.users.copyparty = {
|
||||||
];
|
description = "Service user for copyparty";
|
||||||
}
|
group = "copyparty";
|
||||||
);
|
home = home;
|
||||||
|
isSystemUser = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,48 +1,56 @@
|
||||||
# Maintainer: icxes <dev.null@need.moe>
|
# Maintainer: icxes <dev.null@need.moe>
|
||||||
# Contributor: Morgan Adamiec <morganamilo@archlinux.org>
|
|
||||||
# NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead.
|
|
||||||
|
|
||||||
pkgname=copyparty
|
pkgname=copyparty
|
||||||
pkgver="1.19.4"
|
pkgver="1.13.0"
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||||
arch=("any")
|
arch=("any")
|
||||||
url="https://github.com/9001/${pkgname}"
|
url="https://github.com/9001/${pkgname}"
|
||||||
license=('MIT')
|
license=('MIT')
|
||||||
depends=("bash" "python" "lsof" "python-jinja")
|
depends=("python" "lsof" "python-jinja")
|
||||||
makedepends=("python-wheel" "python-setuptools" "python-build" "python-installer" "make" "pigz")
|
makedepends=("python-wheel" "python-setuptools" "python-build" "python-installer" "make" "pigz")
|
||||||
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
|
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
|
||||||
"cfssl: generate TLS certificates on startup"
|
"cfssl: generate TLS certificates on startup (pointless when reverse-proxied)"
|
||||||
"python-mutagen: music tags (alternative)"
|
"python-mutagen: music tags (alternative)"
|
||||||
"python-pillow: thumbnails for images"
|
"python-pillow: thumbnails for images"
|
||||||
"python-pyvips: thumbnails for images (higher quality, faster, uses more ram)"
|
"python-pyvips: thumbnails for images (higher quality, faster, uses more ram)"
|
||||||
"libkeyfinder: detection of musical keys"
|
"libkeyfinder-git: detection of musical keys"
|
||||||
"python-pyopenssl: ftps functionality"
|
"qm-vamp-plugins: BPM detection"
|
||||||
"python-pyzmq: send zeromq messages from event-hooks"
|
"python-pyopenssl: ftps functionality"
|
||||||
"python-argon2-cffi: hashed passwords in config"
|
"python-argon2_cffi: hashed passwords in config"
|
||||||
|
"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}/copyparty.conf" )
|
backup=("etc/${pkgname}.d/init" )
|
||||||
sha256sums=("b0e84a78eb2701cb7447b6023afcec280c550617dde67b6f0285bb23483111eb")
|
sha256sums=("9ad9ea5d4bdf947ed39f4ae571219d3528794c2ec6d4f470a5f3737899787e03")
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
|
|
||||||
make
|
|
||||||
|
|
||||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||||
python -m build --wheel --no-isolation
|
|
||||||
|
pushd copyparty/web
|
||||||
|
make -j$(nproc)
|
||||||
|
rm Makefile
|
||||||
|
popd
|
||||||
|
|
||||||
|
python3 -m build -wn
|
||||||
}
|
}
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||||
python -m installer --destdir="$pkgdir" dist/*.whl
|
python3 -m installer -d "$pkgdir" dist/*.whl
|
||||||
|
|
||||||
install -dm755 "${pkgdir}/etc/${pkgname}"
|
install -dm755 "${pkgdir}/etc/${pkgname}.d"
|
||||||
install -Dm755 "bin/prisonparty.sh" "${pkgdir}/usr/bin/prisonparty"
|
install -Dm755 "bin/prisonparty.sh" "${pkgdir}/usr/bin/prisonparty"
|
||||||
install -Dm644 "contrib/systemd/${pkgname}.conf" "${pkgdir}/etc/${pkgname}/copyparty.conf"
|
install -Dm644 "contrib/package/arch/${pkgname}.conf" "${pkgdir}/etc/${pkgname}.d/init"
|
||||||
install -Dm644 "contrib/systemd/${pkgname}@.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}@.service"
|
install -Dm644 "contrib/package/arch/${pkgname}.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}.service"
|
||||||
install -Dm644 "contrib/systemd/${pkgname}-user.service" "${pkgdir}/usr/lib/systemd/user/${pkgname}.service"
|
install -Dm644 "contrib/package/arch/prisonparty.service" "${pkgdir}/usr/lib/systemd/system/prisonparty.service"
|
||||||
install -Dm644 "contrib/systemd/prisonparty@.service" "${pkgdir}/usr/lib/systemd/system/prisonparty@.service"
|
install -Dm644 "contrib/package/arch/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md"
|
||||||
install -Dm644 "contrib/systemd/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md"
|
|
||||||
install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||||
|
|
||||||
|
find /etc/${pkgname}.d -iname '*.conf' 2>/dev/null | grep -qE . && return
|
||||||
|
echo "┏━━━━━━━━━━━━━━━──-"
|
||||||
|
echo "┃ Configure ${pkgname} by adding .conf files into /etc/${pkgname}.d/"
|
||||||
|
echo "┃ and maybe copy+edit one of the following to /etc/systemd/system/:"
|
||||||
|
echo "┣━♦ /usr/lib/systemd/system/${pkgname}.service (standard)"
|
||||||
|
echo "┣━♦ /usr/lib/systemd/system/prisonparty.service (chroot)"
|
||||||
|
echo "┗━━━━━━━━━━━━━━━──-"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
# Contributor: Beethoven <beethovenisadog@protonmail.com>
|
|
||||||
|
|
||||||
|
|
||||||
pkgname=copyparty
|
|
||||||
pkgver=1.19.4
|
|
||||||
pkgrel=1
|
|
||||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
|
||||||
arch=("any")
|
|
||||||
url="https://github.com/9001/${pkgname}"
|
|
||||||
license=('MIT')
|
|
||||||
depends=("bash" "python3" "lsof" "python3-jinja2")
|
|
||||||
makedepends=("python3-wheel" "python3-setuptools" "python3-build" "python3-installer" "make" "pigz")
|
|
||||||
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
|
|
||||||
"golang-cfssl: generate TLS certificates on startup"
|
|
||||||
"python3-mutagen: music tags (alternative)"
|
|
||||||
"python3-pil: thumbnails for images"
|
|
||||||
"python3-openssl: ftps functionality"
|
|
||||||
"python3-zmq: send zeromq messages from event-hooks"
|
|
||||||
"python3-argon2: hashed passwords in config"
|
|
||||||
)
|
|
||||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
|
||||||
backup=("/etc/${pkgname}.d/init" )
|
|
||||||
sha256sums=("b0e84a78eb2701cb7447b6023afcec280c550617dde67b6f0285bb23483111eb")
|
|
||||||
|
|
||||||
build() {
|
|
||||||
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
|
|
||||||
make
|
|
||||||
|
|
||||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
|
||||||
python -m build --wheel --no-isolation
|
|
||||||
}
|
|
||||||
|
|
||||||
package() {
|
|
||||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
|
||||||
python -m installer --destdir="$pkgdir" dist/*.whl
|
|
||||||
|
|
||||||
install -dm755 "${pkgdir}/etc/${pkgname}.d"
|
|
||||||
install -Dm755 "bin/prisonparty.sh" "${pkgdir}/usr/bin/prisonparty"
|
|
||||||
install -Dm644 "contrib/package/makedeb-mpr/${pkgname}.conf" "${pkgdir}/etc/${pkgname}.d/init"
|
|
||||||
install -Dm644 "contrib/package/makedeb-mpr/${pkgname}.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}.service"
|
|
||||||
install -Dm644 "contrib/package/makedeb-mpr/prisonparty.service" "${pkgdir}/usr/lib/systemd/system/prisonparty.service"
|
|
||||||
install -Dm644 "contrib/package/makedeb-mpr/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md"
|
|
||||||
install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
|
||||||
}
|
|
|
@ -1,118 +1,63 @@
|
||||||
{
|
{ lib, stdenv, makeWrapper, fetchurl, utillinux, python, jinja2, impacket, pyftpdlib, pyopenssl, argon2-cffi, pillow, pyvips, ffmpeg, mutagen,
|
||||||
lib,
|
|
||||||
buildPythonApplication,
|
|
||||||
fetchurl,
|
|
||||||
util-linux,
|
|
||||||
python,
|
|
||||||
setuptools,
|
|
||||||
jinja2,
|
|
||||||
impacket,
|
|
||||||
pyopenssl,
|
|
||||||
cfssl,
|
|
||||||
argon2-cffi,
|
|
||||||
pillow,
|
|
||||||
pyvips,
|
|
||||||
pyzmq,
|
|
||||||
ffmpeg,
|
|
||||||
mutagen,
|
|
||||||
pyftpdlib,
|
|
||||||
magic,
|
|
||||||
partftpy,
|
|
||||||
fusepy, # for partyfuse
|
|
||||||
|
|
||||||
# use argon2id-hashed passwords in config files (sha2 is always available)
|
# use argon2id-hashed passwords in config files (sha2 is always available)
|
||||||
withHashedPasswords ? true,
|
withHashedPasswords ? true,
|
||||||
|
|
||||||
# generate TLS certificates on startup (pointless when reverse-proxied)
|
# generate TLS certificates on startup (pointless when reverse-proxied)
|
||||||
withCertgen ? false,
|
withCertgen ? false,
|
||||||
|
|
||||||
# create thumbnails with Pillow; faster than FFmpeg / MediaProcessing
|
# create thumbnails with Pillow; faster than FFmpeg / MediaProcessing
|
||||||
withThumbnails ? true,
|
withThumbnails ? true,
|
||||||
|
|
||||||
# create thumbnails with PyVIPS; even faster, uses more memory
|
# create thumbnails with PyVIPS; even faster, uses more memory
|
||||||
# -- can be combined with Pillow to support more filetypes
|
# -- can be combined with Pillow to support more filetypes
|
||||||
withFastThumbnails ? false,
|
withFastThumbnails ? false,
|
||||||
|
|
||||||
# enable FFmpeg; thumbnails for most filetypes (also video and audio), extract audio metadata, transcode audio to opus
|
# enable FFmpeg; thumbnails for most filetypes (also video and audio), extract audio metadata, transcode audio to opus
|
||||||
# -- possibly dangerous if you allow anonymous uploads, since FFmpeg has a huge attack surface
|
# -- possibly dangerous if you allow anonymous uploads, since FFmpeg has a huge attack surface
|
||||||
# -- can be combined with Thumbnails and/or FastThumbnails, since FFmpeg is slower than both
|
# -- can be combined with Thumbnails and/or FastThumbnails, since FFmpeg is slower than both
|
||||||
withMediaProcessing ? true,
|
withMediaProcessing ? true,
|
||||||
|
|
||||||
# if MediaProcessing is not enabled, you probably want this instead (less accurate, but much safer and faster)
|
# if MediaProcessing is not enabled, you probably want this instead (less accurate, but much safer and faster)
|
||||||
withBasicAudioMetadata ? false,
|
withBasicAudioMetadata ? false,
|
||||||
|
|
||||||
# send ZeroMQ messages from event-hooks
|
# enable FTPS support in the FTP server
|
||||||
withZeroMQ ? true,
|
withFTPS ? false,
|
||||||
|
|
||||||
# enable FTP server
|
# samba/cifs server; dangerous and buggy, enable if you really need it
|
||||||
withFTP ? true,
|
withSMB ? false,
|
||||||
|
|
||||||
# enable FTPS support in the FTP server
|
|
||||||
withFTPS ? false,
|
|
||||||
|
|
||||||
# enable TFTP server
|
|
||||||
withTFTP ? false,
|
|
||||||
|
|
||||||
# samba/cifs server; dangerous and buggy, enable if you really need it
|
|
||||||
withSMB ? false,
|
|
||||||
|
|
||||||
# enables filetype detection for nameless uploads
|
|
||||||
withMagic ? false,
|
|
||||||
|
|
||||||
# extra packages to add to the PATH
|
|
||||||
extraPackages ? [ ],
|
|
||||||
|
|
||||||
# function that accepts a python packageset and returns a list of packages to
|
|
||||||
# be added to the python venv. useful for scripts and such that require
|
|
||||||
# additional dependencies
|
|
||||||
extraPythonPackages ? (_p: [ ]),
|
|
||||||
|
|
||||||
}:
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
pinData = lib.importJSON ./pin.json;
|
pinData = lib.importJSON ./pin.json;
|
||||||
runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg);
|
pyEnv = python.withPackages (ps:
|
||||||
in
|
with ps; [
|
||||||
buildPythonApplication {
|
|
||||||
pname = "copyparty";
|
|
||||||
inherit (pinData) version;
|
|
||||||
src = fetchurl {
|
|
||||||
inherit (pinData) url hash;
|
|
||||||
};
|
|
||||||
dependencies =
|
|
||||||
[
|
|
||||||
jinja2
|
jinja2
|
||||||
fusepy
|
|
||||||
]
|
]
|
||||||
++ lib.optional withSMB impacket
|
++ lib.optional withSMB impacket
|
||||||
++ lib.optional withFTP pyftpdlib
|
|
||||||
++ lib.optional withFTPS pyopenssl
|
++ lib.optional withFTPS pyopenssl
|
||||||
++ lib.optional withTFTP partftpy
|
|
||||||
++ lib.optional withCertgen cfssl
|
++ lib.optional withCertgen cfssl
|
||||||
++ lib.optional withThumbnails pillow
|
++ lib.optional withThumbnails pillow
|
||||||
++ 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
|
++ lib.optional withHashedPasswords argon2-cffi
|
||||||
++ lib.optional withZeroMQ pyzmq
|
);
|
||||||
++ lib.optional withMagic magic
|
in stdenv.mkDerivation {
|
||||||
++ (extraPythonPackages python.pkgs);
|
pname = "copyparty";
|
||||||
makeWrapperArgs = [ "--prefix PATH : ${lib.makeBinPath runtimeDeps}" ];
|
version = pinData.version;
|
||||||
|
src = fetchurl {
|
||||||
pyproject = true;
|
url = pinData.url;
|
||||||
build-system = [
|
hash = pinData.hash;
|
||||||
setuptools
|
|
||||||
];
|
|
||||||
meta = {
|
|
||||||
description = "Turn almost any device into a file server";
|
|
||||||
longDescription = ''
|
|
||||||
Portable file server with accelerated resumable uploads, dedup, WebDAV,
|
|
||||||
FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps
|
|
||||||
'';
|
|
||||||
homepage = "https://github.com/9001/copyparty";
|
|
||||||
changelog = "https://github.com/9001/copyparty/releases/tag/v${pinData.version}";
|
|
||||||
license = lib.licenses.mit;
|
|
||||||
mainProgram = "copyparty";
|
|
||||||
sourceProvenance = [ lib.sourceTypes.fromSource ];
|
|
||||||
};
|
};
|
||||||
|
buildInputs = [ makeWrapper ];
|
||||||
|
dontUnpack = true;
|
||||||
|
dontBuild = true;
|
||||||
|
installPhase = ''
|
||||||
|
install -Dm755 $src $out/share/copyparty-sfx.py
|
||||||
|
makeWrapper ${pyEnv.interpreter} $out/bin/copyparty \
|
||||||
|
--set PATH '${lib.makeBinPath ([ utillinux ] ++ lib.optional withMediaProcessing ffmpeg)}:$PATH' \
|
||||||
|
--add-flags "$out/share/copyparty-sfx.py"
|
||||||
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"url": "https://github.com/9001/copyparty/releases/download/v1.19.4/copyparty-1.19.4.tar.gz",
|
"url": "https://github.com/9001/copyparty/releases/download/v1.13.0/copyparty-sfx.py",
|
||||||
"version": "1.19.4",
|
"version": "1.13.0",
|
||||||
"hash": "sha256-sOhKeOsnAct0R7YCOvzsKAxVBhfd5ntvAoW7I0gxEes="
|
"hash": "sha256-3/ttK5BXAA5F1MztahiQp32QSSZqhuh3V6xb12T/6nM="
|
||||||
}
|
}
|
|
@ -11,14 +11,14 @@ import base64
|
||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
import sys
|
import sys
|
||||||
import tarfile
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
OUTPUT_FILE = Path("pin.json")
|
OUTPUT_FILE = Path("pin.json")
|
||||||
TARGET_ASSET = lambda version: f"copyparty-{version}.tar.gz"
|
TARGET_ASSET = "copyparty-sfx.py"
|
||||||
HASH_TYPE = "sha256"
|
HASH_TYPE = "sha256"
|
||||||
LATEST_RELEASE_URL = "https://api.github.com/repos/9001/copyparty/releases/latest"
|
LATEST_RELEASE_URL = "https://api.github.com/repos/9001/copyparty/releases/latest"
|
||||||
DOWNLOAD_URL = lambda version: f"https://github.com/9001/copyparty/releases/download/v{version}/{TARGET_ASSET(version)}"
|
DOWNLOAD_URL = lambda version: f"https://github.com/9001/copyparty/releases/download/v{version}/{TARGET_ASSET}"
|
||||||
|
|
||||||
|
|
||||||
def get_formatted_hash(binary):
|
def get_formatted_hash(binary):
|
||||||
|
@ -29,13 +29,11 @@ def get_formatted_hash(binary):
|
||||||
return f"{HASH_TYPE}-{encoded_hash}"
|
return f"{HASH_TYPE}-{encoded_hash}"
|
||||||
|
|
||||||
|
|
||||||
def version_from_tar_gz(path):
|
def version_from_sfx(binary):
|
||||||
with tarfile.open(path) as tarball:
|
result = re.search(b'^VER = "(.*)"$', binary, re.MULTILINE)
|
||||||
release_name = tarball.getmembers()[0].name
|
if result:
|
||||||
prefix = "copyparty-"
|
return result.groups(1)[0].decode("ascii")
|
||||||
|
|
||||||
if release_name.startswith(prefix):
|
|
||||||
return release_name.replace(prefix, "")
|
|
||||||
raise ValueError("version not found in provided file")
|
raise ValueError("version not found in provided file")
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,7 +42,7 @@ def remote_release_pin():
|
||||||
|
|
||||||
response = requests.get(LATEST_RELEASE_URL).json()
|
response = requests.get(LATEST_RELEASE_URL).json()
|
||||||
version = response["tag_name"].lstrip("v")
|
version = response["tag_name"].lstrip("v")
|
||||||
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET(version)][0]
|
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET][0]
|
||||||
download_url = asset_info["browser_download_url"]
|
download_url = asset_info["browser_download_url"]
|
||||||
asset = requests.get(download_url)
|
asset = requests.get(download_url)
|
||||||
formatted_hash = get_formatted_hash(asset.content)
|
formatted_hash = get_formatted_hash(asset.content)
|
||||||
|
@ -54,9 +52,10 @@ def remote_release_pin():
|
||||||
|
|
||||||
|
|
||||||
def local_release_pin(path):
|
def local_release_pin(path):
|
||||||
version = version_from_tar_gz(path)
|
asset = path.read_bytes()
|
||||||
|
version = version_from_sfx(asset)
|
||||||
download_url = DOWNLOAD_URL(version)
|
download_url = DOWNLOAD_URL(version)
|
||||||
formatted_hash = get_formatted_hash(path.read_bytes())
|
formatted_hash = get_formatted_hash(asset)
|
||||||
|
|
||||||
result = {"url": download_url, "version": version, "hash": formatted_hash}
|
result = {"url": download_url, "version": version, "hash": formatted_hash}
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
{
|
|
||||||
lib,
|
|
||||||
buildPythonPackage,
|
|
||||||
fetchurl,
|
|
||||||
setuptools,
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
pinData = lib.importJSON ./pin.json;
|
|
||||||
in
|
|
||||||
|
|
||||||
buildPythonPackage rec {
|
|
||||||
pname = "partftpy";
|
|
||||||
inherit (pinData) version;
|
|
||||||
pyproject = true;
|
|
||||||
|
|
||||||
src = fetchurl {
|
|
||||||
inherit (pinData) url hash;
|
|
||||||
};
|
|
||||||
|
|
||||||
build-system = [ setuptools ];
|
|
||||||
|
|
||||||
pythonImportsCheck = [ "partftpy.TftpServer" ];
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
description = "Pure Python TFTP library (copyparty edition)";
|
|
||||||
homepage = "https://github.com/9001/partftpy";
|
|
||||||
changelog = "https://github.com/9001/partftpy/releases/tag/${version}";
|
|
||||||
license = lib.licenses.mit;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"url": "https://github.com/9001/partftpy/releases/download/v0.4.0/partftpy-0.4.0.tar.gz",
|
|
||||||
"version": "0.4.0",
|
|
||||||
"hash": "sha256-5Q2zyuJ892PGZmb+YXg0ZPW/DK8RDL1uE0j5HPd4We0="
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# Update the Nix package pin
|
|
||||||
#
|
|
||||||
# Usage: ./update.sh
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
import hashlib
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
OUTPUT_FILE = Path("pin.json")
|
|
||||||
TARGET_ASSET = lambda version: f"partftpy-{version}.tar.gz"
|
|
||||||
HASH_TYPE = "sha256"
|
|
||||||
LATEST_RELEASE_URL = "https://api.github.com/repos/9001/partftpy/releases/latest"
|
|
||||||
|
|
||||||
|
|
||||||
def get_formatted_hash(binary):
|
|
||||||
hasher = hashlib.new("sha256")
|
|
||||||
hasher.update(binary)
|
|
||||||
asset_hash = hasher.digest()
|
|
||||||
encoded_hash = base64.b64encode(asset_hash).decode("ascii")
|
|
||||||
return f"{HASH_TYPE}-{encoded_hash}"
|
|
||||||
|
|
||||||
|
|
||||||
def remote_release_pin():
|
|
||||||
import requests
|
|
||||||
|
|
||||||
response = requests.get(LATEST_RELEASE_URL).json()
|
|
||||||
version = response["tag_name"].lstrip("v")
|
|
||||||
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET(version)][0]
|
|
||||||
download_url = asset_info["browser_download_url"]
|
|
||||||
asset = requests.get(download_url)
|
|
||||||
formatted_hash = get_formatted_hash(asset.content)
|
|
||||||
|
|
||||||
result = {"url": download_url, "version": version, "hash": formatted_hash}
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
result = remote_release_pin()
|
|
||||||
|
|
||||||
print(result)
|
|
||||||
json_result = json.dumps(result, indent=4)
|
|
||||||
OUTPUT_FILE.write_text(json_result)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
|
@ -1,62 +0,0 @@
|
||||||
Name: copyparty
|
|
||||||
Version: $pkgver
|
|
||||||
Release: $pkgrel
|
|
||||||
License: MIT
|
|
||||||
Group: Utilities
|
|
||||||
URL: https://github.com/9001/copyparty
|
|
||||||
Source0: copyparty-$pkgver.tar.gz
|
|
||||||
Summary: File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++
|
|
||||||
BuildArch: noarch
|
|
||||||
BuildRequires: python3, python3-devel, pyproject-rpm-macros, python-setuptools, python-wheel, make
|
|
||||||
Requires: python3, (python3-jinja2 or python-jinja2), lsof
|
|
||||||
Recommends: ffmpeg, (golang-github-cloudflare-cfssl or cfssl), python-mutagen, python-pillow, python-pyvips
|
|
||||||
Recommends: qm-vamp-plugins, python-argon2-cffi, (python-pyopenssl or pyopenssl), python-impacket
|
|
||||||
|
|
||||||
%description
|
|
||||||
Portable file server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps
|
|
||||||
|
|
||||||
See release at https://github.com/9001/copyparty/releases
|
|
||||||
|
|
||||||
%global debug_package %{nil}
|
|
||||||
|
|
||||||
%generate_buildrequires
|
|
||||||
%pyproject_buildrequires
|
|
||||||
|
|
||||||
%prep
|
|
||||||
%setup -q
|
|
||||||
|
|
||||||
%build
|
|
||||||
cd "copyparty/web"
|
|
||||||
make
|
|
||||||
cd -
|
|
||||||
%pyproject_wheel
|
|
||||||
|
|
||||||
%install
|
|
||||||
mkdir -p %{buildroot}%{_bindir}
|
|
||||||
mkdir -p %{buildroot}%{_libdir}/systemd/{system,user}
|
|
||||||
mkdir -p %{buildroot}/etc/%{name}
|
|
||||||
mkdir -p %{buildroot}/var/lib/%{name}-jail
|
|
||||||
mkdir -p %{buildroot}%{_datadir}/licenses/%{name}
|
|
||||||
|
|
||||||
%pyproject_install
|
|
||||||
%pyproject_save_files copyparty
|
|
||||||
|
|
||||||
install -m 0755 bin/prisonparty.sh %{buildroot}%{_bindir}/prisonpary.sh
|
|
||||||
install -m 0644 contrib/systemd/%{name}.conf %{buildroot}/etc/%{name}/%{name}.conf
|
|
||||||
install -m 0644 contrib/systemd/%{name}@.service %{buildroot}%{_libdir}/systemd/system/%{name}@.service
|
|
||||||
install -m 0644 contrib/systemd/%{name}-user.service %{buildroot}%{_libdir}/systemd/user/%{name}.service
|
|
||||||
install -m 0644 contrib/systemd/prisonparty@.service %{buildroot}%{_libdir}/systemd/system/prisonparty@.service
|
|
||||||
install -m 0644 contrib/systemd/index.md %{buildroot}/var/lib/%{name}-jail/README.md
|
|
||||||
install -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE
|
|
||||||
|
|
||||||
%files -n copyparty -f %{pyproject_files}
|
|
||||||
%license LICENSE
|
|
||||||
%{_bindir}/copyparty
|
|
||||||
%{_bindir}/partyfuse
|
|
||||||
%{_bindir}/u2c
|
|
||||||
%{_bindir}/prisonpary.sh
|
|
||||||
/etc/%{name}/%{name}.conf
|
|
||||||
%{_libdir}/systemd/system/%{name}@.service
|
|
||||||
%{_libdir}/systemd/user/%{name}.service
|
|
||||||
%{_libdir}/systemd/system/prisonparty@.service
|
|
||||||
/var/lib/%{name}-jail/README.md
|
|
|
@ -15,19 +15,11 @@ save one of these as `.epilogue.html` inside a folder to customize it:
|
||||||
point `--js-browser` to one of these by URL:
|
point `--js-browser` to one of these by URL:
|
||||||
|
|
||||||
* [`minimal-up2k.js`](minimal-up2k.js) is similar to the above `minimal-up2k.html` except it applies globally to all write-only folders
|
* [`minimal-up2k.js`](minimal-up2k.js) is similar to the above `minimal-up2k.html` except it applies globally to all write-only folders
|
||||||
* [`quickmove.js`](quickmove.js) adds a hotkey to move selected files into a subfolder
|
|
||||||
* [`up2k-hooks.js`](up2k-hooks.js) lets you specify a ruleset for files to skip uploading
|
* [`up2k-hooks.js`](up2k-hooks.js) lets you specify a ruleset for files to skip uploading
|
||||||
* [`up2k-hook-ytid.js`](up2k-hook-ytid.js) is a more specific example checking youtube-IDs against some API
|
* [`up2k-hook-ytid.js`](up2k-hook-ytid.js) is a more specific example checking youtube-IDs against some API
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## example any-js
|
|
||||||
point `--js-browser` and/or `--js-other` to one of these by URL:
|
|
||||||
|
|
||||||
* [`banner.js`](banner.js) shows a very enterprise [legal-banner](https://github.com/user-attachments/assets/8ae8e087-b209-449c-b08d-74e040f0284b)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## example browser-css
|
## example browser-css
|
||||||
point `--css-browser` to one of these by URL:
|
point `--css-browser` to one of these by URL:
|
||||||
|
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
(function() {
|
|
||||||
|
|
||||||
// usage: copy this to '.banner.js' in your webroot,
|
|
||||||
// and run copyparty with the following arguments:
|
|
||||||
// --js-browser /.banner.js --js-other /.banner.js
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// had to pick the most chuuni one as the default
|
|
||||||
var bannertext = '' +
|
|
||||||
'<h3>You are accessing a U.S. Government (USG) Information System (IS) that is provided for USG-authorized use only.</h3>' +
|
|
||||||
'<p>By using this IS (which includes any device attached to this IS), you consent to the following conditions:</p>' +
|
|
||||||
'<ul>' +
|
|
||||||
'<li>The USG routinely intercepts and monitors communications on this IS for purposes including, but not limited to, penetration testing, COMSEC monitoring, network operations and defense, personnel misconduct (PM), law enforcement (LE), and counterintelligence (CI) investigations.</li>' +
|
|
||||||
'<li>At any time, the USG may inspect and seize data stored on this IS.</li>' +
|
|
||||||
'<li>Communications using, or data stored on, this IS are not private, are subject to routine monitoring, interception, and search, and may be disclosed or used for any USG-authorized purpose.</li>' +
|
|
||||||
'<li>This IS includes security measures (e.g., authentication and access controls) to protect USG interests -- not for your personal benefit or privacy.</li>' +
|
|
||||||
'<li>Notwithstanding the above, using this IS does not constitute consent to PM, LE or CI investigative searching or monitoring of the content of privileged communications, or work product, related to personal representation or services by attorneys, psychotherapists, or clergy, and their assistants. Such communications and work product are private and confidential. See User Agreement for details.</li>' +
|
|
||||||
'</ul>';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// fancy div to insert into pages
|
|
||||||
function bannerdiv(border) {
|
|
||||||
var ret = mknod('div', null, bannertext);
|
|
||||||
if (border)
|
|
||||||
ret.setAttribute("style", "border:1em solid var(--fg); border-width:.3em 0; margin:3em 0");
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// keep all of these false and then selectively enable them in the if-blocks below
|
|
||||||
var show_msgbox = false,
|
|
||||||
login_top = false,
|
|
||||||
top = false,
|
|
||||||
bottom = false,
|
|
||||||
top_bordered = false,
|
|
||||||
bottom_bordered = false;
|
|
||||||
|
|
||||||
if (QS("h1#cc") && QS("a#k")) {
|
|
||||||
// this is the controlpanel
|
|
||||||
// (you probably want to keep just one of these enabled)
|
|
||||||
show_msgbox = true;
|
|
||||||
login_top = true;
|
|
||||||
bottom = true;
|
|
||||||
}
|
|
||||||
else if (ebi("swin") && ebi("smac")) {
|
|
||||||
// this is the connect-page, same deal here
|
|
||||||
show_msgbox = true;
|
|
||||||
top_bordered = true;
|
|
||||||
bottom_bordered = true;
|
|
||||||
}
|
|
||||||
else if (ebi("op_cfg") || ebi("div#mw") ) {
|
|
||||||
// we're running in the main filebrowser (op_cfg) or markdown-viewer/editor (div#mw),
|
|
||||||
// fragile pages which break if you do something too fancy
|
|
||||||
show_msgbox = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// shows a fullscreen messagebox; works on all pages
|
|
||||||
if (show_msgbox) {
|
|
||||||
var now = Math.floor(Date.now() / 1000),
|
|
||||||
last_shown = sread("bannerts") || 0;
|
|
||||||
|
|
||||||
// 60 * 60 * 17 = 17 hour cooldown
|
|
||||||
if (now - last_shown > 60 * 60 * 17) {
|
|
||||||
swrite("bannerts", now);
|
|
||||||
modal.confirm(bannertext, null, function () {
|
|
||||||
location = 'https://this-page-intentionally-left-blank.org/';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// show a message on the page footer; only works on the connect-page
|
|
||||||
if (top || top_bordered) {
|
|
||||||
var dst = ebi('wrap');
|
|
||||||
dst.insertBefore(bannerdiv(top_bordered), dst.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
// show a message on the page footer; only works on the controlpanel and connect-page
|
|
||||||
if (bottom || bottom_bordered) {
|
|
||||||
ebi('wrap').appendChild(bannerdiv(bottom_bordered));
|
|
||||||
}
|
|
||||||
|
|
||||||
// show a message on the top of the page; only works on the controlpanel
|
|
||||||
if (login_top) {
|
|
||||||
var dst = QS('h1');
|
|
||||||
dst.parentNode.insertBefore(bannerdiv(false), dst);
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
|
|
@ -1,117 +0,0 @@
|
||||||
// USAGE:
|
|
||||||
// place this file somewhere in the webroot and then
|
|
||||||
// python3 -m copyparty --js-browser /.res/graft-thumbs.js
|
|
||||||
//
|
|
||||||
// DESCRIPTION:
|
|
||||||
// this is a gridview plugin which, for each file in a folder,
|
|
||||||
// looks for another file with the same filename (but with a
|
|
||||||
// different file extension)
|
|
||||||
//
|
|
||||||
// if one of those files is an image and the other is not,
|
|
||||||
// then this plugin assumes the image is a "sidecar thumbnail"
|
|
||||||
// for the other file, and it will graft the image thumbnail
|
|
||||||
// onto the non-image file (for example an mp3)
|
|
||||||
//
|
|
||||||
// optional feature 1, default-enabled:
|
|
||||||
// the image-file is then hidden from the directory listing
|
|
||||||
//
|
|
||||||
// optional feature 2, default-enabled:
|
|
||||||
// when clicking the audio file, the image will also open
|
|
||||||
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
|
|
||||||
// `graft_thumbs` assumes the gridview has just been rendered;
|
|
||||||
// it looks for sidecars, and transplants those thumbnails onto
|
|
||||||
// the other file with the same basename (filename sans extension)
|
|
||||||
|
|
||||||
var graft_thumbs = function () {
|
|
||||||
if (!thegrid.en)
|
|
||||||
return; // not in grid mode
|
|
||||||
|
|
||||||
var files = msel.getall(),
|
|
||||||
pairs = {};
|
|
||||||
|
|
||||||
console.log(files);
|
|
||||||
|
|
||||||
for (var a = 0; a < files.length; a++) {
|
|
||||||
var file = files[a],
|
|
||||||
is_pic = /\.(jpe?g|png|gif|webp)$/i.exec(file.vp),
|
|
||||||
is_audio = re_au_all.exec(file.vp),
|
|
||||||
basename = file.vp.replace(/\.[^\.]+$/, ""),
|
|
||||||
entry = pairs[basename];
|
|
||||||
|
|
||||||
if (!entry)
|
|
||||||
// first time seeing this basename; create a new entry in pairs
|
|
||||||
entry = pairs[basename] = {};
|
|
||||||
|
|
||||||
if (is_pic)
|
|
||||||
entry.thumb = file;
|
|
||||||
else if (is_audio)
|
|
||||||
entry.audio = file;
|
|
||||||
}
|
|
||||||
|
|
||||||
var basenames = Object.keys(pairs);
|
|
||||||
for (var a = 0; a < basenames.length; a++)
|
|
||||||
(function(a) {
|
|
||||||
var pair = pairs[basenames[a]];
|
|
||||||
|
|
||||||
if (!pair.thumb || !pair.audio)
|
|
||||||
return; // not a matching pair of files
|
|
||||||
|
|
||||||
var img_thumb = QS('#ggrid a[ref="' + pair.thumb.id + '"] img[onload]'),
|
|
||||||
img_audio = QS('#ggrid a[ref="' + pair.audio.id + '"] img[onload]');
|
|
||||||
|
|
||||||
if (!img_thumb || !img_audio)
|
|
||||||
return; // something's wrong... let's bail
|
|
||||||
|
|
||||||
// alright, graft the thumb...
|
|
||||||
img_audio.src = img_thumb.src;
|
|
||||||
|
|
||||||
// ...and hide the sidecar
|
|
||||||
img_thumb.closest('a').style.display = 'none';
|
|
||||||
|
|
||||||
// ...and add another onclick-handler to the audio,
|
|
||||||
// so it also opens the pic while playing the song
|
|
||||||
img_audio.addEventListener('click', function() {
|
|
||||||
img_thumb.click();
|
|
||||||
return false; // let it bubble to the next listener
|
|
||||||
});
|
|
||||||
|
|
||||||
})(a);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ...and then the trick! near the end of loadgrid,
|
|
||||||
// thegrid.bagit is called to initialize the baguettebox
|
|
||||||
// (image/video gallery); this is the perfect function to
|
|
||||||
// "hook" (hijack) so we can run our code :^)
|
|
||||||
|
|
||||||
// need to grab a backup of the original function first,
|
|
||||||
var orig_func = thegrid.bagit;
|
|
||||||
|
|
||||||
// and then replace it with our own:
|
|
||||||
thegrid.bagit = function (isrc) {
|
|
||||||
|
|
||||||
if (isrc !== '#ggrid')
|
|
||||||
// we only want to modify the grid, so
|
|
||||||
// let the original function handle this one
|
|
||||||
return orig_func(isrc);
|
|
||||||
|
|
||||||
graft_thumbs();
|
|
||||||
|
|
||||||
// when changing directories, the grid is
|
|
||||||
// rendered before msel returns the correct
|
|
||||||
// filenames, so schedule another run:
|
|
||||||
setTimeout(graft_thumbs, 1);
|
|
||||||
|
|
||||||
// and finally, call the original thegrid.bagit function
|
|
||||||
return orig_func(isrc);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ls0) {
|
|
||||||
// the server included an initial listing json (ls0),
|
|
||||||
// so the grid has already been rendered without our hook
|
|
||||||
graft_thumbs();
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
|
|
@ -12,23 +12,6 @@ almost the same as minimal-up2k.html except this one...:
|
||||||
|
|
||||||
-- looks slightly better
|
-- looks slightly better
|
||||||
|
|
||||||
|
|
||||||
========================
|
|
||||||
== USAGE INSTRUCTIONS ==
|
|
||||||
|
|
||||||
1. create a volume which anyone can read from (if you haven't already)
|
|
||||||
2. copy this file into that volume, so anyone can download it
|
|
||||||
3. enable the plugin by telling the webbrowser to load this file;
|
|
||||||
assuming the URL to the public volume is /res/, and
|
|
||||||
assuming you're using config-files, then add this to your config:
|
|
||||||
|
|
||||||
[global]
|
|
||||||
js-browser: /res/minimal-up2k.js
|
|
||||||
|
|
||||||
alternatively, if you're not using config-files, then
|
|
||||||
add the following commandline argument instead:
|
|
||||||
--js-browser=/res/minimal-up2k.js
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var u2min = `
|
var u2min = `
|
||||||
|
|
|
@ -1,140 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
|
|
||||||
// USAGE:
|
|
||||||
// place this file somewhere in the webroot,
|
|
||||||
// for example in a folder named ".res" to hide it, and then
|
|
||||||
// python3 copyparty-sfx.py -v .::A --js-browser /.res/quickmove.js
|
|
||||||
//
|
|
||||||
// DESCRIPTION:
|
|
||||||
// the command above launches copyparty with one single volume;
|
|
||||||
// ".::A" = current folder as webroot, and everyone has Admin
|
|
||||||
//
|
|
||||||
// the plugin adds hotkey "W" which moves all selected files
|
|
||||||
// into a subfolder named "foobar" inside the current folder
|
|
||||||
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
|
|
||||||
var action_to_perform = ask_for_confirmation_and_then_move;
|
|
||||||
// this decides what the new hotkey should do;
|
|
||||||
// ask_for_confirmation_and_then_move = show a yes/no box,
|
|
||||||
// move_selected_files = just move the files immediately
|
|
||||||
|
|
||||||
var move_destination = "foobar";
|
|
||||||
// this is the target folder to move files to;
|
|
||||||
// by default it is a subfolder of the current folder,
|
|
||||||
// but it can also be an absolute path like "/foo/bar"
|
|
||||||
|
|
||||||
// ===
|
|
||||||
// === END OF CONFIG
|
|
||||||
// ===
|
|
||||||
|
|
||||||
var main_hotkey_handler, // copyparty's original hotkey handler
|
|
||||||
plugin_enabler, // timer to engage this plugin when safe
|
|
||||||
files_to_move; // list of files to move
|
|
||||||
|
|
||||||
function ask_for_confirmation_and_then_move() {
|
|
||||||
var num_files = msel.getsel().length,
|
|
||||||
msg = "move the selected " + num_files + " files?";
|
|
||||||
|
|
||||||
if (!num_files)
|
|
||||||
return toast.warn(2, 'no files were selected to be moved');
|
|
||||||
|
|
||||||
modal.confirm(msg, move_selected_files, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
function move_selected_files() {
|
|
||||||
var selection = msel.getsel();
|
|
||||||
|
|
||||||
if (!selection.length)
|
|
||||||
return toast.warn(2, 'no files were selected to be moved');
|
|
||||||
|
|
||||||
if (thegrid.bbox) {
|
|
||||||
// close image/video viewer
|
|
||||||
thegrid.bbox = null;
|
|
||||||
baguetteBox.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
files_to_move = [];
|
|
||||||
for (var a = 0; a < selection.length; a++)
|
|
||||||
files_to_move.push(selection[a].vp);
|
|
||||||
|
|
||||||
move_next_file();
|
|
||||||
}
|
|
||||||
|
|
||||||
function move_next_file() {
|
|
||||||
var num_files = files_to_move.length,
|
|
||||||
filepath = files_to_move.pop(),
|
|
||||||
filename = vsplit(filepath)[1];
|
|
||||||
|
|
||||||
toast.inf(10, "moving " + num_files + " files...\n\n" + filename);
|
|
||||||
|
|
||||||
var dst = move_destination;
|
|
||||||
|
|
||||||
if (!dst.endsWith('/'))
|
|
||||||
// must have a trailing slash, so add it
|
|
||||||
dst += '/';
|
|
||||||
|
|
||||||
if (!dst.startsWith('/'))
|
|
||||||
// destination is a relative path, so prefix current folder path
|
|
||||||
dst = get_evpath() + dst;
|
|
||||||
|
|
||||||
// and finally append the filename
|
|
||||||
dst += '/' + filename;
|
|
||||||
|
|
||||||
// prepare the move-request to be sent
|
|
||||||
var xhr = new XHR();
|
|
||||||
xhr.onload = xhr.onerror = function() {
|
|
||||||
if (this.status !== 201)
|
|
||||||
return toast.err(30, 'move failed: ' + esc(this.responseText));
|
|
||||||
|
|
||||||
if (files_to_move.length)
|
|
||||||
return move_next_file(); // still more files to go
|
|
||||||
|
|
||||||
toast.ok(1, 'move OK');
|
|
||||||
treectl.goto(); // reload the folder contents
|
|
||||||
};
|
|
||||||
xhr.open('POST', filepath + '?move=' + dst);
|
|
||||||
xhr.send();
|
|
||||||
}
|
|
||||||
|
|
||||||
function our_hotkey_handler(e) {
|
|
||||||
// bail if either ALT, CTRL, or SHIFT is pressed
|
|
||||||
if (e.altKey || e.shiftKey || e.isComposing || ctrl(e))
|
|
||||||
return main_hotkey_handler(e); // let copyparty handle this keystroke
|
|
||||||
|
|
||||||
var key_name = (e.code || e.key) + '',
|
|
||||||
ae = document.activeElement,
|
|
||||||
aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : '';
|
|
||||||
|
|
||||||
// check the current aet (active element type),
|
|
||||||
// only continue if one of the following currently has input focus:
|
|
||||||
// nothing | link | button | table-row | table-cell | div | text
|
|
||||||
if (aet && !/^(a|button|tr|td|div|pre)$/.test(aet))
|
|
||||||
return main_hotkey_handler(e); // let copyparty handle this keystroke
|
|
||||||
|
|
||||||
if (key_name == 'KeyW') {
|
|
||||||
// okay, this one's for us... do the thing
|
|
||||||
action_to_perform();
|
|
||||||
return ev(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return main_hotkey_handler(e); // let copyparty handle this keystroke
|
|
||||||
}
|
|
||||||
|
|
||||||
function enable_plugin() {
|
|
||||||
if (!window.hotkeys_attached)
|
|
||||||
return console.log('quickmove is waiting for the page to finish loading');
|
|
||||||
|
|
||||||
clearInterval(plugin_enabler);
|
|
||||||
main_hotkey_handler = document.onkeydown;
|
|
||||||
document.onkeydown = our_hotkey_handler;
|
|
||||||
console.log('quickmove is now enabled');
|
|
||||||
}
|
|
||||||
|
|
||||||
// copyparty doesn't enable its hotkeys until the page
|
|
||||||
// has finished loading, so we'll wait for that too
|
|
||||||
plugin_enabler = setInterval(enable_plugin, 100);
|
|
||||||
|
|
||||||
})();
|
|
|
@ -1,26 +0,0 @@
|
||||||
# this will start `/usr/bin/copyparty`
|
|
||||||
# and read config from `$HOME/.config/copyparty.conf`
|
|
||||||
#
|
|
||||||
# unless you add -q to disable logging, you may want to remove the
|
|
||||||
# following line to allow buffering (slightly better performance):
|
|
||||||
# Environment=PYTHONUNBUFFERED=x
|
|
||||||
|
|
||||||
[Unit]
|
|
||||||
Description=copyparty file server
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=notify
|
|
||||||
SyslogIdentifier=copyparty
|
|
||||||
WorkingDirectory=/var/lib/copyparty-jail
|
|
||||||
Environment=PYTHONUNBUFFERED=x
|
|
||||||
Environment=PRTY_CONFIG=%h/.config/copyparty/copyparty.conf
|
|
||||||
ExecReload=/bin/kill -s USR1 $MAINPID
|
|
||||||
|
|
||||||
# ensure there is a config
|
|
||||||
ExecStartPre=/bin/bash -c 'if [[ ! -f %h/.config/copyparty/copyparty.conf ]]; then mkdir -p %h/.config/copyparty; cp /etc/copyparty/copyparty.conf %h/.config/copyparty/copyparty.conf; fi'
|
|
||||||
|
|
||||||
# run copyparty
|
|
||||||
ExecStart=/usr/bin/python3 /usr/bin/copyparty
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=default.target
|
|
|
@ -1,13 +1,42 @@
|
||||||
|
# not actually YAML but lets pretend:
|
||||||
|
# -*- mode: yaml -*-
|
||||||
|
# vim: ft=yaml:
|
||||||
|
|
||||||
|
|
||||||
|
# put this file in /etc/
|
||||||
|
|
||||||
|
|
||||||
[global]
|
[global]
|
||||||
i: 127.0.0.1
|
e2dsa # enable file indexing and filesystem scanning
|
||||||
|
e2ts # and enable multimedia indexing
|
||||||
|
ansi # and colors in log messages
|
||||||
|
|
||||||
|
# disable logging to stdout/journalctl and log to a file instead;
|
||||||
|
# $LOGS_DIRECTORY is usually /var/log/copyparty (comes from systemd)
|
||||||
|
# and copyparty replaces %Y-%m%d with Year-MonthDay, so the
|
||||||
|
# full path will be something like /var/log/copyparty/2023-1130.txt
|
||||||
|
# (note: enable compression by adding .xz at the end)
|
||||||
|
q, lo: $LOGS_DIRECTORY/%Y-%m%d.log
|
||||||
|
|
||||||
|
# p: 80,443,3923 # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE)
|
||||||
|
# i: 127.0.0.1 # only allow connections from localhost (reverse-proxies)
|
||||||
|
# ftp: 3921 # enable ftp server on port 3921
|
||||||
|
# p: 3939 # listen on another port
|
||||||
|
# df: 16 # stop accepting uploads if less than 16 GB free disk space
|
||||||
|
# ver # show copyparty version in the controlpanel
|
||||||
|
# grid # show thumbnails/grid-view by default
|
||||||
|
# theme: 2 # monokai
|
||||||
|
# name: datasaver # change the server-name that's displayed in the browser
|
||||||
|
# stats, nos-dup # enable the prometheus endpoint, but disable the dupes counter (too slow)
|
||||||
|
# no-robots, force-js # make it harder for search engines to read your server
|
||||||
|
|
||||||
|
|
||||||
[accounts]
|
[accounts]
|
||||||
user: password
|
ed: wark # username: password
|
||||||
|
|
||||||
[/]
|
|
||||||
/var/lib/copyparty-jail
|
[/] # create a volume at "/" (the webroot), which will
|
||||||
|
/mnt # share the contents of the "/mnt" folder
|
||||||
accs:
|
accs:
|
||||||
r: *
|
rw: * # everyone gets read-write access, but
|
||||||
rwdma: user
|
rwmda: ed # the user "ed" gets read-write-move-delete-admin
|
||||||
flags:
|
|
||||||
grid
|
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
# not actually YAML but lets pretend:
|
|
||||||
# -*- mode: yaml -*-
|
|
||||||
# vim: ft=yaml:
|
|
||||||
|
|
||||||
|
|
||||||
# put this file in /etc/
|
|
||||||
|
|
||||||
|
|
||||||
[global]
|
|
||||||
e2dsa # enable file indexing and filesystem scanning
|
|
||||||
e2ts # and enable multimedia indexing
|
|
||||||
ansi # and colors in log messages
|
|
||||||
|
|
||||||
# disable logging to stdout/journalctl and log to a file instead;
|
|
||||||
# $LOGS_DIRECTORY is usually /var/log/copyparty (comes from systemd)
|
|
||||||
# and copyparty replaces %Y-%m%d with Year-MonthDay, so the
|
|
||||||
# full path will be something like /var/log/copyparty/2023-1130.txt
|
|
||||||
# (note: enable compression by adding .xz at the end)
|
|
||||||
q, lo: $LOGS_DIRECTORY/%Y-%m%d.log
|
|
||||||
|
|
||||||
# p: 80,443,3923 # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE)
|
|
||||||
# i: 127.0.0.1 # only allow connections from localhost (reverse-proxies)
|
|
||||||
# ftp: 3921 # enable ftp server on port 3921
|
|
||||||
# p: 3939 # listen on another port
|
|
||||||
# df: 16 # stop accepting uploads if less than 16 GB free disk space
|
|
||||||
# ver # show copyparty version in the controlpanel
|
|
||||||
# grid # show thumbnails/grid-view by default
|
|
||||||
# theme: 2 # monokai
|
|
||||||
# name: datasaver # change the server-name that's displayed in the browser
|
|
||||||
# stats, nos-dup # enable the prometheus endpoint, but disable the dupes counter (too slow)
|
|
||||||
# no-robots, force-js # make it harder for search engines to read your server
|
|
||||||
|
|
||||||
|
|
||||||
[accounts]
|
|
||||||
ed: wark # username: password
|
|
||||||
|
|
||||||
|
|
||||||
[/] # create a volume at "/" (the webroot), which will
|
|
||||||
/mnt # share the contents of the "/mnt" folder
|
|
||||||
accs:
|
|
||||||
rw: * # everyone gets read-write access, but
|
|
||||||
rwmda: ed # the user "ed" gets read-write-move-delete-admin
|
|
|
@ -4,7 +4,7 @@
|
||||||
#
|
#
|
||||||
# installation:
|
# installation:
|
||||||
# wget https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py -O /usr/local/bin/copyparty-sfx.py
|
# wget https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py -O /usr/local/bin/copyparty-sfx.py
|
||||||
# useradd -r -s /sbin/nologin -m -d /var/lib/copyparty copyparty
|
# useradd -r -s /sbin/nologin -d /var/lib/copyparty copyparty
|
||||||
# firewall-cmd --permanent --add-port=3923/tcp # --zone=libvirt
|
# firewall-cmd --permanent --add-port=3923/tcp # --zone=libvirt
|
||||||
# firewall-cmd --reload
|
# firewall-cmd --reload
|
||||||
# cp -pv copyparty.service /etc/systemd/system/
|
# cp -pv copyparty.service /etc/systemd/system/
|
||||||
|
@ -12,18 +12,11 @@
|
||||||
# restorecon -vr /etc/systemd/system/copyparty.service # on fedora/rhel
|
# restorecon -vr /etc/systemd/system/copyparty.service # on fedora/rhel
|
||||||
# systemctl daemon-reload && systemctl enable --now copyparty
|
# systemctl daemon-reload && systemctl enable --now copyparty
|
||||||
#
|
#
|
||||||
# every time you edit this file, you must "systemctl daemon-reload"
|
|
||||||
# for the changes to take effect and then "systemctl restart copyparty"
|
|
||||||
#
|
|
||||||
# if it fails to start, first check this: systemctl status copyparty
|
# if it fails to start, first check this: systemctl status copyparty
|
||||||
# then try starting it while viewing logs:
|
# then try starting it while viewing logs:
|
||||||
# journalctl -fan 100
|
# journalctl -fan 100
|
||||||
# tail -Fn 100 /var/log/copyparty/$(date +%Y-%m%d.log)
|
# tail -Fn 100 /var/log/copyparty/$(date +%Y-%m%d.log)
|
||||||
#
|
#
|
||||||
# if you run into any issues, for example thumbnails not working,
|
|
||||||
# try removing the "some quick hardening" section and then please
|
|
||||||
# let me know if that actually helped so we can look into it
|
|
||||||
#
|
|
||||||
# you may want to:
|
# you may want to:
|
||||||
# - change "User=copyparty" and "/var/lib/copyparty/" to another user
|
# - change "User=copyparty" and "/var/lib/copyparty/" to another user
|
||||||
# - edit /etc/copyparty.conf to configure copyparty
|
# - edit /etc/copyparty.conf to configure copyparty
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
# this will start `/usr/bin/copyparty`
|
|
||||||
# and read config from `/etc/copyparty/copyparty.conf`
|
|
||||||
#
|
|
||||||
# the %i refers to whatever you put after the copyparty@
|
|
||||||
# so with copyparty@foo.service, %i == foo
|
|
||||||
#
|
|
||||||
# unless you add -q to disable logging, you may want to remove the
|
|
||||||
# following line to allow buffering (slightly better performance):
|
|
||||||
# Environment=PYTHONUNBUFFERED=x
|
|
||||||
|
|
||||||
[Unit]
|
|
||||||
Description=copyparty file server
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=notify
|
|
||||||
SyslogIdentifier=copyparty
|
|
||||||
WorkingDirectory=/var/lib/copyparty-jail
|
|
||||||
Environment=PYTHONUNBUFFERED=x
|
|
||||||
Environment=PRTY_CONFIG=/etc/copyparty/copyparty.conf
|
|
||||||
ExecReload=/bin/kill -s USR1 $MAINPID
|
|
||||||
|
|
||||||
# user to run as + where the TLS certificate is (if any)
|
|
||||||
User=%i
|
|
||||||
Environment=XDG_CONFIG_HOME=/home/%i/.config
|
|
||||||
|
|
||||||
# run copyparty
|
|
||||||
ExecStart=/usr/bin/python3 /usr/bin/copyparty
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
|
@ -1,10 +0,0 @@
|
||||||
this is `/var/lib/copyparty-jail`, the fallback webroot when copyparty has not yet been configured
|
|
||||||
|
|
||||||
please edit `/etc/copyparty/copyparty.conf` (if running as a system service)
|
|
||||||
or `$HOME/.config/copyparty/copyparty.conf` if running as a user service
|
|
||||||
|
|
||||||
a basic configuration example is available at https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf
|
|
||||||
a configuration example that explains most flags is available at https://github.com/9001/copyparty/blob/hovudstraum/docs/chungus.conf
|
|
||||||
|
|
||||||
the full list of configuration options can be seen at https://ocv.me/copyparty/helptext.html
|
|
||||||
or by running `copyparty --help`
|
|
|
@ -1,38 +0,0 @@
|
||||||
# this will start `/usr/bin/copyparty`
|
|
||||||
# in a chroot, preventing accidental access elsewhere,
|
|
||||||
# and read copyparty config from `/etc/copyparty/copyparty.conf`
|
|
||||||
#
|
|
||||||
# expose additional filesystem locations to copyparty
|
|
||||||
# by listing them between the last `%i` and `--`
|
|
||||||
#
|
|
||||||
# `%i %i` = user/group to run copyparty as; can be IDs (1000 1000)
|
|
||||||
# the %i refers to whatever you put after the prisonparty@
|
|
||||||
# so with prisonparty@foo.service, %i == foo
|
|
||||||
#
|
|
||||||
# unless you add -q to disable logging, you may want to remove the
|
|
||||||
# following line to allow buffering (slightly better performance):
|
|
||||||
# Environment=PYTHONUNBUFFERED=x
|
|
||||||
|
|
||||||
[Unit]
|
|
||||||
Description=copyparty file server
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=notify
|
|
||||||
SyslogIdentifier=prisonparty
|
|
||||||
WorkingDirectory=/var/lib/copyparty-jail
|
|
||||||
Environment=PYTHONUNBUFFERED=x
|
|
||||||
Environment=PRTY_CONFIG=/etc/copyparty/copyparty.conf
|
|
||||||
ExecReload=/bin/kill -s USR1 $MAINPID
|
|
||||||
|
|
||||||
# user to run as + where the TLS certificate is (if any)
|
|
||||||
User=%i
|
|
||||||
Environment=XDG_CONFIG_HOME=/home/%i/.config
|
|
||||||
|
|
||||||
# run copyparty
|
|
||||||
ExecStart=/bin/bash /usr/bin/prisonparty /var/lib/copyparty-jail %i %i \
|
|
||||||
/etc/copyparty \
|
|
||||||
-- \
|
|
||||||
/usr/bin/python3 /usr/bin/copyparty
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
|
@ -1,116 +0,0 @@
|
||||||
/* copy bsod.* into a folder named ".themes" in your webroot and then
|
|
||||||
--themes=10 --theme=9 --css-browser=/.themes/bsod.css
|
|
||||||
*/
|
|
||||||
|
|
||||||
html.ey {
|
|
||||||
--w2: #3d7bbc;
|
|
||||||
--w3: #5fcbec;
|
|
||||||
|
|
||||||
--fg: #fff;
|
|
||||||
--fg-max: #fff;
|
|
||||||
--fg-weak: var(--w3);
|
|
||||||
|
|
||||||
--bg: #2067b2;
|
|
||||||
--bg-d3: var(--bg);
|
|
||||||
--bg-d2: var(--w2);
|
|
||||||
--bg-d1: var(--fg-weak);
|
|
||||||
--bg-u2: var(--bg);
|
|
||||||
--bg-u3: var(--bg);
|
|
||||||
--bg-u5: var(--w2);
|
|
||||||
|
|
||||||
--tab-alt: var(--fg-weak);
|
|
||||||
--row-alt: var(--w2);
|
|
||||||
|
|
||||||
--scroll: var(--w3);
|
|
||||||
|
|
||||||
--a: #fff;
|
|
||||||
--a-b: #fff;
|
|
||||||
--a-hil: #fff;
|
|
||||||
--a-h-bg: var(--fg-weak);
|
|
||||||
--a-dark: var(--a);
|
|
||||||
--a-gray: var(--fg-weak);
|
|
||||||
|
|
||||||
--btn-fg: var(--a);
|
|
||||||
--btn-bg: var(--w2);
|
|
||||||
--btn-h-fg: var(--w2);
|
|
||||||
--btn-1-fg: var(--bg);
|
|
||||||
--btn-1-bg: var(--a);
|
|
||||||
--txt-sh: a;
|
|
||||||
--txt-bg: var(--w2);
|
|
||||||
|
|
||||||
--u2-b1-bg: var(--w2);
|
|
||||||
--u2-b2-bg: var(--w2);
|
|
||||||
--u2-txt-bg: var(--w2);
|
|
||||||
--u2-tab-bg: a;
|
|
||||||
--u2-tab-1-bg: var(--w2);
|
|
||||||
|
|
||||||
--sort-1: var(--a);
|
|
||||||
--sort-1: var(--fg-weak);
|
|
||||||
|
|
||||||
--tree-bg: var(--bg);
|
|
||||||
|
|
||||||
--g-b1: a;
|
|
||||||
--g-b2: a;
|
|
||||||
--g-f-bg: var(--w2);
|
|
||||||
|
|
||||||
--f-sh1: 0.1;
|
|
||||||
--f-sh2: 0.02;
|
|
||||||
--f-sh3: 0.1;
|
|
||||||
--f-h-b1: a;
|
|
||||||
|
|
||||||
--srv-1: var(--a);
|
|
||||||
--srv-3: var(--a);
|
|
||||||
|
|
||||||
--mp-sh: a;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.ey {
|
|
||||||
background: url('bsod.png') top 5em right 4.5em no-repeat fixed var(--bg);
|
|
||||||
}
|
|
||||||
html.ey body#b {
|
|
||||||
background: var(--bg); /*sandbox*/
|
|
||||||
}
|
|
||||||
html.ey #ops {
|
|
||||||
margin: 1.7em 1.5em 0 1.5em;
|
|
||||||
border-radius: .3em;
|
|
||||||
border-width: 1px 0;
|
|
||||||
}
|
|
||||||
html.ey #ops a {
|
|
||||||
text-shadow: 1px 1px 0 rgba(0,0,0,0.5);
|
|
||||||
}
|
|
||||||
html.ey .opbox {
|
|
||||||
margin: 1.5em 0 0 0;
|
|
||||||
}
|
|
||||||
html.ey #tree {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
html.ey #tt {
|
|
||||||
border-color: var(--w2);
|
|
||||||
background: var(--w2);
|
|
||||||
}
|
|
||||||
html.ey .mdo a {
|
|
||||||
background: none;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
html.ey .mdo pre,
|
|
||||||
html.ey .mdo code {
|
|
||||||
color: #fff;
|
|
||||||
background: var(--w2);
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
html.ey .mdo h1,
|
|
||||||
html.ey .mdo h2 {
|
|
||||||
background: none;
|
|
||||||
border-color: var(--w2);
|
|
||||||
}
|
|
||||||
html.ey .mdo ul ul,
|
|
||||||
html.ey .mdo ul ol,
|
|
||||||
html.ey .mdo ol ul,
|
|
||||||
html.ey .mdo ol ol {
|
|
||||||
border-color: var(--w2);
|
|
||||||
}
|
|
||||||
html.ey .mdo p>em,
|
|
||||||
html.ey .mdo li>em,
|
|
||||||
html.ey .mdo td>em {
|
|
||||||
color: #fd0;
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.2 KiB |
|
@ -1,25 +0,0 @@
|
||||||
# ./traefik --configFile=copyparty.yaml
|
|
||||||
|
|
||||||
entryPoints:
|
|
||||||
web:
|
|
||||||
address: :8080
|
|
||||||
transport:
|
|
||||||
# don't disconnect during big uploads
|
|
||||||
respondingTimeouts:
|
|
||||||
readTimeout: "0s"
|
|
||||||
log:
|
|
||||||
level: DEBUG
|
|
||||||
providers:
|
|
||||||
file:
|
|
||||||
# WARNING: must be same filename as current file
|
|
||||||
filename: "copyparty.yaml"
|
|
||||||
http:
|
|
||||||
services:
|
|
||||||
service-cpp:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://127.0.0.1:3923/"
|
|
||||||
routers:
|
|
||||||
my-router:
|
|
||||||
rule: "PathPrefix(`/`)"
|
|
||||||
service: service-cpp
|
|
|
@ -1,107 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sqlite3
|
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
when the up2k-database is stored on a zfs volume, this may give
|
|
||||||
slightly higher performance (actual gains not measured yet)
|
|
||||||
|
|
||||||
NOTE: must be applied in combination with the related advice in the openzfs documentation;
|
|
||||||
https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Workload%20Tuning.html#database-workloads
|
|
||||||
and see specifically the SQLite subsection
|
|
||||||
|
|
||||||
it is assumed that all databases are stored in a single location,
|
|
||||||
for example with `--hist /var/store/hists`
|
|
||||||
|
|
||||||
three alternatives for running this script:
|
|
||||||
|
|
||||||
1. copy it into /var/store/hists and run "python3 zfs-tune.py s"
|
|
||||||
(s = modify all databases below folder containing script)
|
|
||||||
|
|
||||||
2. cd into /var/store/hists and run "python3 ~/zfs-tune.py w"
|
|
||||||
(w = modify all databases below current working directory)
|
|
||||||
|
|
||||||
3. python3 ~/zfs-tune.py /var/store/hists
|
|
||||||
|
|
||||||
if you use docker, run copyparty with `--hist /cfg/hists`, copy this script into /cfg, and run this:
|
|
||||||
podman run --rm -it --entrypoint /usr/bin/python3 ghcr.io/9001/copyparty-ac /cfg/zfs-tune.py s
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
PAGESIZE = 65536
|
|
||||||
|
|
||||||
|
|
||||||
# borrowed from copyparty; short efficient stacktrace for errors
|
|
||||||
def min_ex(max_lines: int = 8, reverse: bool = False) -> str:
|
|
||||||
et, ev, tb = sys.exc_info()
|
|
||||||
stb = traceback.extract_tb(tb) if tb else traceback.extract_stack()[:-1]
|
|
||||||
fmt = "%s:%d <%s>: %s"
|
|
||||||
ex = [fmt % (fp.split(os.sep)[-1], ln, fun, txt) for fp, ln, fun, txt in stb]
|
|
||||||
if et or ev or tb:
|
|
||||||
ex.append("[%s] %s" % (et.__name__ if et else "(anonymous)", ev))
|
|
||||||
return "\n".join(ex[-max_lines:][:: -1 if reverse else 1])
|
|
||||||
|
|
||||||
|
|
||||||
def set_pagesize(db_path):
|
|
||||||
try:
|
|
||||||
# check current page_size
|
|
||||||
with sqlite3.connect(db_path) as db:
|
|
||||||
v = db.execute("pragma page_size").fetchone()[0]
|
|
||||||
if v == PAGESIZE:
|
|
||||||
print(" `-- OK")
|
|
||||||
return
|
|
||||||
|
|
||||||
# https://www.sqlite.org/pragma.html#pragma_page_size
|
|
||||||
# `- disable wal; set pagesize; vacuum
|
|
||||||
# (copyparty will reenable wal if necessary)
|
|
||||||
|
|
||||||
with sqlite3.connect(db_path) as db:
|
|
||||||
db.execute("pragma journal_mode=delete")
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
with sqlite3.connect(db_path) as db:
|
|
||||||
db.execute(f"pragma page_size = {PAGESIZE}")
|
|
||||||
db.execute("vacuum")
|
|
||||||
|
|
||||||
print(" `-- new pagesize OK")
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
err = min_ex().replace("\n", "\n -- ")
|
|
||||||
print(f"FAILED: {db_path}\n -- {err}")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
top = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
cwd = os.path.abspath(os.getcwd())
|
|
||||||
try:
|
|
||||||
x = sys.argv[1]
|
|
||||||
except:
|
|
||||||
print(f"""
|
|
||||||
this script takes one mandatory argument:
|
|
||||||
specify 's' to start recursing from folder containing this script file ({top})
|
|
||||||
specify 'w' to start recursing from the current working directory ({cwd})
|
|
||||||
specify a path to start recursing from there
|
|
||||||
""")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if x.lower() == "w":
|
|
||||||
top = cwd
|
|
||||||
elif x.lower() != "s":
|
|
||||||
top = x
|
|
||||||
|
|
||||||
for dirpath, dirs, files in os.walk(top):
|
|
||||||
for fname in files:
|
|
||||||
if not fname.endswith(".db"):
|
|
||||||
continue
|
|
||||||
db_path = os.path.join(dirpath, fname)
|
|
||||||
print(db_path)
|
|
||||||
set_pagesize(db_path)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
|
@ -16,10 +16,9 @@ except:
|
||||||
TYPE_CHECKING = False
|
TYPE_CHECKING = False
|
||||||
|
|
||||||
if True:
|
if True:
|
||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable
|
||||||
|
|
||||||
PY2 = sys.version_info < (3,)
|
PY2 = sys.version_info < (3,)
|
||||||
PY36 = sys.version_info > (3, 6)
|
|
||||||
if not PY2:
|
if not PY2:
|
||||||
unicode: Callable[[Any], str] = str
|
unicode: Callable[[Any], str] = str
|
||||||
else:
|
else:
|
||||||
|
@ -51,61 +50,6 @@ try:
|
||||||
except:
|
except:
|
||||||
CORES = (os.cpu_count() if hasattr(os, "cpu_count") else 0) or 2
|
CORES = (os.cpu_count() if hasattr(os, "cpu_count") else 0) or 2
|
||||||
|
|
||||||
# all embedded resources to be retrievable over http
|
|
||||||
zs = """
|
|
||||||
web/a/partyfuse.py
|
|
||||||
web/a/u2c.py
|
|
||||||
web/a/webdav-cfg.bat
|
|
||||||
web/baguettebox.js
|
|
||||||
web/browser.css
|
|
||||||
web/browser.html
|
|
||||||
web/browser.js
|
|
||||||
web/browser2.html
|
|
||||||
web/cf.html
|
|
||||||
web/copyparty.gif
|
|
||||||
web/deps/busy.mp3
|
|
||||||
web/deps/easymde.css
|
|
||||||
web/deps/easymde.js
|
|
||||||
web/deps/marked.js
|
|
||||||
web/deps/fuse.py
|
|
||||||
web/deps/mini-fa.css
|
|
||||||
web/deps/mini-fa.woff
|
|
||||||
web/deps/prism.css
|
|
||||||
web/deps/prism.js
|
|
||||||
web/deps/prismd.css
|
|
||||||
web/deps/scp.woff2
|
|
||||||
web/deps/sha512.ac.js
|
|
||||||
web/deps/sha512.hw.js
|
|
||||||
web/idp.html
|
|
||||||
web/iiam.gif
|
|
||||||
web/md.css
|
|
||||||
web/md.html
|
|
||||||
web/md.js
|
|
||||||
web/md2.css
|
|
||||||
web/md2.js
|
|
||||||
web/mde.css
|
|
||||||
web/mde.html
|
|
||||||
web/mde.js
|
|
||||||
web/msg.css
|
|
||||||
web/msg.html
|
|
||||||
web/rups.css
|
|
||||||
web/rups.html
|
|
||||||
web/rups.js
|
|
||||||
web/shares.css
|
|
||||||
web/shares.html
|
|
||||||
web/shares.js
|
|
||||||
web/splash.css
|
|
||||||
web/splash.html
|
|
||||||
web/splash.js
|
|
||||||
web/svcs.html
|
|
||||||
web/svcs.js
|
|
||||||
web/ui.css
|
|
||||||
web/up2k.js
|
|
||||||
web/util.js
|
|
||||||
web/w.hash.js
|
|
||||||
"""
|
|
||||||
RES = set(zs.strip().split("\n"))
|
|
||||||
|
|
||||||
|
|
||||||
class EnvParams(object):
|
class EnvParams(object):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,8 +1,8 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
VERSION = (1, 19, 4)
|
VERSION = (1, 13, 1)
|
||||||
CODENAME = "usernames"
|
CODENAME = "race the beam"
|
||||||
BUILD_DT = (2025, 8, 17)
|
BUILD_DT = (2024, 5, 6)
|
||||||
|
|
||||||
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)
|
||||||
|
|
1368
copyparty/authsrv.py
1368
copyparty/authsrv.py
File diff suppressed because it is too large
Load diff
|
@ -9,11 +9,8 @@ from . import path as path
|
||||||
if True: # pylint: disable=using-constant-test
|
if True: # pylint: disable=using-constant-test
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
MKD_755 = {"chmod_d": 0o755}
|
_ = (path,)
|
||||||
MKD_700 = {"chmod_d": 0o700}
|
__all__ = ["path"]
|
||||||
|
|
||||||
_ = (path, MKD_755, MKD_700)
|
|
||||||
__all__ = ["path", "MKD_755", "MKD_700"]
|
|
||||||
|
|
||||||
# grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c
|
# grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c
|
||||||
# printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')"
|
# printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')"
|
||||||
|
@ -23,39 +20,19 @@ def chmod(p: str, mode: int) -> None:
|
||||||
return os.chmod(fsenc(p), mode)
|
return os.chmod(fsenc(p), mode)
|
||||||
|
|
||||||
|
|
||||||
def chown(p: str, uid: int, gid: int) -> None:
|
|
||||||
return os.chown(fsenc(p), uid, gid)
|
|
||||||
|
|
||||||
|
|
||||||
def listdir(p: str = ".") -> list[str]:
|
def listdir(p: str = ".") -> list[str]:
|
||||||
return [fsdec(x) for x in os.listdir(fsenc(p))]
|
return [fsdec(x) for x in os.listdir(fsenc(p))]
|
||||||
|
|
||||||
|
|
||||||
def makedirs(name: str, vf: dict[str, Any] = MKD_755, exist_ok: bool = True) -> bool:
|
def makedirs(name: str, mode: int = 0o755, exist_ok: bool = True) -> bool:
|
||||||
# os.makedirs does 777 for all but leaf; this does mode on all
|
|
||||||
todo = []
|
|
||||||
bname = fsenc(name)
|
bname = fsenc(name)
|
||||||
while bname:
|
try:
|
||||||
if os.path.isdir(bname):
|
os.makedirs(bname, mode)
|
||||||
break
|
return True
|
||||||
todo.append(bname)
|
except:
|
||||||
bname = os.path.dirname(bname)
|
if not exist_ok or not os.path.isdir(bname):
|
||||||
if not todo:
|
|
||||||
if not exist_ok:
|
|
||||||
os.mkdir(bname) # to throw
|
|
||||||
return False
|
|
||||||
mode = vf["chmod_d"]
|
|
||||||
chown = "chown" in vf
|
|
||||||
for zb in todo[::-1]:
|
|
||||||
try:
|
|
||||||
os.mkdir(zb, mode)
|
|
||||||
if chown:
|
|
||||||
os.chown(zb, vf["uid"], vf["gid"])
|
|
||||||
except:
|
|
||||||
if os.path.isdir(zb):
|
|
||||||
continue
|
|
||||||
raise
|
raise
|
||||||
return True
|
return False
|
||||||
|
|
||||||
|
|
||||||
def mkdir(p: str, mode: int = 0o755) -> None:
|
def mkdir(p: str, mode: int = 0o755) -> None:
|
||||||
|
|
|
@ -9,14 +9,14 @@ 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 ExceptionalQueue, NotExQueue, 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:
|
||||||
from .svchub import SvcHub
|
from .svchub import SvcHub
|
||||||
|
|
||||||
if True: # pylint: disable=using-constant-test
|
if True: # pylint: disable=using-constant-test
|
||||||
from typing import Any, Union
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
class MProcess(mp.Process):
|
class MProcess(mp.Process):
|
||||||
|
@ -43,9 +43,6 @@ class BrokerMp(object):
|
||||||
self.procs = []
|
self.procs = []
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
|
|
||||||
self.retpend: dict[int, Any] = {}
|
|
||||||
self.retpend_mutex = threading.Lock()
|
|
||||||
|
|
||||||
self.num_workers = self.args.j or CORES
|
self.num_workers = self.args.j or CORES
|
||||||
self.log("broker", "booting {} subprocesses".format(self.num_workers))
|
self.log("broker", "booting {} subprocesses".format(self.num_workers))
|
||||||
for n in range(1, self.num_workers + 1):
|
for n in range(1, self.num_workers + 1):
|
||||||
|
@ -57,13 +54,14 @@ class BrokerMp(object):
|
||||||
self.procs.append(proc)
|
self.procs.append(proc)
|
||||||
proc.start()
|
proc.start()
|
||||||
|
|
||||||
Daemon(self.periodic, "mp-periodic")
|
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
self.log("broker", "shutting down")
|
self.log("broker", "shutting down")
|
||||||
for n, proc in enumerate(self.procs):
|
for n, proc in enumerate(self.procs):
|
||||||
name = "mp-shut-%d-%d" % (n, len(self.procs))
|
thr = threading.Thread(
|
||||||
Daemon(proc.q_pend.put, name, ((0, "shutdown", []),))
|
target=proc.q_pend.put((0, "shutdown", [])),
|
||||||
|
name="mp-shutdown-{}-{}".format(n, len(self.procs)),
|
||||||
|
)
|
||||||
|
thr.start()
|
||||||
|
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
procs = self.procs
|
procs = self.procs
|
||||||
|
@ -81,10 +79,6 @@ class BrokerMp(object):
|
||||||
for _, proc in enumerate(self.procs):
|
for _, proc in enumerate(self.procs):
|
||||||
proc.q_pend.put((0, "reload", []))
|
proc.q_pend.put((0, "reload", []))
|
||||||
|
|
||||||
def reload_sessions(self) -> None:
|
|
||||||
for _, proc in enumerate(self.procs):
|
|
||||||
proc.q_pend.put((0, "reload_sessions", []))
|
|
||||||
|
|
||||||
def collector(self, proc: MProcess) -> None:
|
def collector(self, proc: MProcess) -> None:
|
||||||
"""receive message from hub in other process"""
|
"""receive message from hub in other process"""
|
||||||
while True:
|
while True:
|
||||||
|
@ -95,10 +89,8 @@ class BrokerMp(object):
|
||||||
self.log(*args)
|
self.log(*args)
|
||||||
|
|
||||||
elif dest == "retq":
|
elif dest == "retq":
|
||||||
with self.retpend_mutex:
|
# response from previous ipc call
|
||||||
retq = self.retpend.pop(retq_id)
|
raise Exception("invalid broker_mp usage")
|
||||||
|
|
||||||
retq.put(args[0])
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# new ipc invoking managed service in hub
|
# new ipc invoking managed service in hub
|
||||||
|
@ -115,7 +107,8 @@ 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) -> Union[ExceptionalQueue, NotExQueue]:
|
def ask(self, dest: str, *args: Any) -> ExceptionalQueue:
|
||||||
|
|
||||||
# new non-ipc invoking managed service in hub
|
# new non-ipc invoking managed service in hub
|
||||||
obj = self.hub
|
obj = self.hub
|
||||||
for node in dest.split("."):
|
for node in dest.split("."):
|
||||||
|
@ -127,30 +120,17 @@ class BrokerMp(object):
|
||||||
retq.put(rv)
|
retq.put(rv)
|
||||||
return retq
|
return retq
|
||||||
|
|
||||||
def wask(self, dest: str, *args: Any) -> list[Union[ExceptionalQueue, NotExQueue]]:
|
|
||||||
# call from hub to workers
|
|
||||||
ret = []
|
|
||||||
for p in self.procs:
|
|
||||||
retq = ExceptionalQueue(1)
|
|
||||||
retq_id = id(retq)
|
|
||||||
with self.retpend_mutex:
|
|
||||||
self.retpend[retq_id] = retq
|
|
||||||
|
|
||||||
p.q_pend.put((retq_id, dest, list(args)))
|
|
||||||
ret.append(retq)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
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,
|
||||||
returns a Queue object which eventually contains the response if want_retval
|
returns a Queue object which eventually contains the response if want_retval
|
||||||
(not-impl here since nothing uses it yet)
|
(not-impl here since nothing uses it yet)
|
||||||
"""
|
"""
|
||||||
if dest == "httpsrv.listen":
|
if dest == "listen":
|
||||||
for p in self.procs:
|
for p in self.procs:
|
||||||
p.q_pend.put((0, dest, [args[0], len(self.procs)]))
|
p.q_pend.put((0, dest, [args[0], len(self.procs)]))
|
||||||
|
|
||||||
elif dest == "httpsrv.set_netdevs":
|
elif dest == "set_netdevs":
|
||||||
for p in self.procs:
|
for p in self.procs:
|
||||||
p.q_pend.put((0, dest, list(args)))
|
p.q_pend.put((0, dest, list(args)))
|
||||||
|
|
||||||
|
@ -159,19 +139,3 @@ class BrokerMp(object):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise Exception("what is " + str(dest))
|
raise Exception("what is " + str(dest))
|
||||||
|
|
||||||
def periodic(self) -> None:
|
|
||||||
while True:
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
tdli = {}
|
|
||||||
tdls = {}
|
|
||||||
qs = self.wask("httpsrv.read_dls")
|
|
||||||
for q in qs:
|
|
||||||
qr = q.get()
|
|
||||||
dli, dls = qr
|
|
||||||
tdli.update(dli)
|
|
||||||
tdls.update(dls)
|
|
||||||
tdl = (tdli, tdls)
|
|
||||||
for p in self.procs:
|
|
||||||
p.q_pend.put((0, "httpsrv.write_dls", tdl))
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import queue
|
||||||
|
|
||||||
from .__init__ import ANYWIN
|
from .__init__ import ANYWIN
|
||||||
from .authsrv import AuthSrv
|
from .authsrv import AuthSrv
|
||||||
from .broker_util import BrokerCli, ExceptionalQueue, NotExQueue
|
from .broker_util import BrokerCli, ExceptionalQueue
|
||||||
from .httpsrv import HttpSrv
|
from .httpsrv import HttpSrv
|
||||||
from .util import FAKE_MP, Daemon, HMaccas
|
from .util import FAKE_MP, Daemon, HMaccas
|
||||||
|
|
||||||
|
@ -82,40 +82,35 @@ class MpWorker(BrokerCli):
|
||||||
while True:
|
while True:
|
||||||
retq_id, dest, args = self.q_pend.get()
|
retq_id, dest, args = self.q_pend.get()
|
||||||
|
|
||||||
if dest == "retq":
|
# self.logw("work: [{}]".format(d[0]))
|
||||||
# response from previous ipc call
|
|
||||||
with self.retpend_mutex:
|
|
||||||
retq = self.retpend.pop(retq_id)
|
|
||||||
|
|
||||||
retq.put(args)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if dest == "shutdown":
|
if dest == "shutdown":
|
||||||
self.httpsrv.shutdown()
|
self.httpsrv.shutdown()
|
||||||
self.logw("ok bye")
|
self.logw("ok bye")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
return
|
return
|
||||||
|
|
||||||
if dest == "reload":
|
elif dest == "reload":
|
||||||
self.logw("mpw.asrv reloading")
|
self.logw("mpw.asrv reloading")
|
||||||
self.asrv.reload()
|
self.asrv.reload()
|
||||||
self.logw("mpw.asrv reloaded")
|
self.logw("mpw.asrv reloaded")
|
||||||
continue
|
|
||||||
|
|
||||||
if dest == "reload_sessions":
|
elif dest == "listen":
|
||||||
with self.asrv.mutex:
|
self.httpsrv.listen(args[0], args[1])
|
||||||
self.asrv.load_sessions()
|
|
||||||
continue
|
|
||||||
|
|
||||||
obj = self
|
elif dest == "set_netdevs":
|
||||||
for node in dest.split("."):
|
self.httpsrv.set_netdevs(args[0])
|
||||||
obj = getattr(obj, node)
|
|
||||||
|
|
||||||
rv = obj(*args) # type: ignore
|
elif dest == "retq":
|
||||||
if retq_id:
|
# response from previous ipc call
|
||||||
self.say("retq", rv, retq_id=retq_id)
|
with self.retpend_mutex:
|
||||||
|
retq = self.retpend.pop(retq_id)
|
||||||
|
|
||||||
def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]:
|
retq.put(args)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception("what is " + str(dest))
|
||||||
|
|
||||||
|
def ask(self, dest: str, *args: Any) -> ExceptionalQueue:
|
||||||
retq = ExceptionalQueue(1)
|
retq = ExceptionalQueue(1)
|
||||||
retq_id = id(retq)
|
retq_id = id(retq)
|
||||||
with self.retpend_mutex:
|
with self.retpend_mutex:
|
||||||
|
@ -124,5 +119,5 @@ class MpWorker(BrokerCli):
|
||||||
self.q_yield.put((retq_id, dest, list(args)))
|
self.q_yield.put((retq_id, dest, list(args)))
|
||||||
return retq
|
return retq
|
||||||
|
|
||||||
def say(self, dest: str, *args: Any, retq_id=0) -> None:
|
def say(self, dest: str, *args: Any) -> None:
|
||||||
self.q_yield.put((retq_id, dest, list(args)))
|
self.q_yield.put((0, dest, list(args)))
|
||||||
|
|
|
@ -5,7 +5,7 @@ import os
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from .__init__ import TYPE_CHECKING
|
from .__init__ import TYPE_CHECKING
|
||||||
from .broker_util import BrokerCli, ExceptionalQueue, NotExQueue
|
from .broker_util import BrokerCli, ExceptionalQueue, try_exec
|
||||||
from .httpsrv import HttpSrv
|
from .httpsrv import HttpSrv
|
||||||
from .util import HMaccas
|
from .util import HMaccas
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ if TYPE_CHECKING:
|
||||||
from .svchub import SvcHub
|
from .svchub import SvcHub
|
||||||
|
|
||||||
if True: # pylint: disable=using-constant-test
|
if True: # pylint: disable=using-constant-test
|
||||||
from typing import Any, Union
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
class BrokerThr(BrokerCli):
|
class BrokerThr(BrokerCli):
|
||||||
|
@ -34,7 +34,6 @@ class BrokerThr(BrokerCli):
|
||||||
self.iphash = HMaccas(os.path.join(self.args.E.cfg, "iphash"), 8)
|
self.iphash = HMaccas(os.path.join(self.args.E.cfg, "iphash"), 8)
|
||||||
self.httpsrv = HttpSrv(self, None)
|
self.httpsrv = HttpSrv(self, None)
|
||||||
self.reload = self.noop
|
self.reload = self.noop
|
||||||
self.reload_sessions = self.noop
|
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
# self.log("broker", "shutting down")
|
# self.log("broker", "shutting down")
|
||||||
|
@ -43,21 +42,26 @@ class BrokerThr(BrokerCli):
|
||||||
def noop(self) -> None:
|
def noop(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]:
|
def ask(self, dest: str, *args: Any) -> ExceptionalQueue:
|
||||||
|
|
||||||
# new ipc invoking managed service in hub
|
# new ipc invoking managed service in hub
|
||||||
obj = self.hub
|
obj = self.hub
|
||||||
for node in dest.split("."):
|
for node in dest.split("."):
|
||||||
obj = getattr(obj, node)
|
obj = getattr(obj, node)
|
||||||
|
|
||||||
return NotExQueue(obj(*args)) # type: ignore
|
rv = try_exec(True, obj, *args)
|
||||||
|
|
||||||
|
# pretend we're broker_mp
|
||||||
|
retq = ExceptionalQueue(1)
|
||||||
|
retq.put(rv)
|
||||||
|
return retq
|
||||||
|
|
||||||
def say(self, dest: str, *args: Any) -> None:
|
def say(self, dest: str, *args: Any) -> None:
|
||||||
if dest == "httpsrv.listen":
|
if dest == "listen":
|
||||||
self.httpsrv.listen(args[0], 1)
|
self.httpsrv.listen(args[0], 1)
|
||||||
return
|
return
|
||||||
|
|
||||||
if dest == "httpsrv.set_netdevs":
|
if dest == "set_netdevs":
|
||||||
self.httpsrv.set_netdevs(args[0])
|
self.httpsrv.set_netdevs(args[0])
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -66,4 +70,4 @@ class BrokerThr(BrokerCli):
|
||||||
for node in dest.split("."):
|
for node in dest.split("."):
|
||||||
obj = getattr(obj, node)
|
obj = getattr(obj, node)
|
||||||
|
|
||||||
obj(*args) # type: ignore
|
try_exec(False, obj, *args)
|
||||||
|
|
|
@ -28,23 +28,11 @@ class ExceptionalQueue(Queue, object):
|
||||||
if rv[1] == "pebkac":
|
if rv[1] == "pebkac":
|
||||||
raise Pebkac(*rv[2:])
|
raise Pebkac(*rv[2:])
|
||||||
else:
|
else:
|
||||||
raise rv[2]
|
raise Exception(rv[2])
|
||||||
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
|
||||||
class NotExQueue(object):
|
|
||||||
"""
|
|
||||||
BrokerThr uses this instead of ExceptionalQueue; 7x faster
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, rv: Any) -> None:
|
|
||||||
self.rv = rv
|
|
||||||
|
|
||||||
def get(self) -> Any:
|
|
||||||
return self.rv
|
|
||||||
|
|
||||||
|
|
||||||
class BrokerCli(object):
|
class BrokerCli(object):
|
||||||
"""
|
"""
|
||||||
helps mypy understand httpsrv.broker but still fails a few levels deeper,
|
helps mypy understand httpsrv.broker but still fails a few levels deeper,
|
||||||
|
@ -60,7 +48,7 @@ class BrokerCli(object):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]:
|
def ask(self, dest: str, *args: Any) -> ExceptionalQueue:
|
||||||
return ExceptionalQueue(1)
|
return ExceptionalQueue(1)
|
||||||
|
|
||||||
def say(self, dest: str, *args: Any) -> None:
|
def say(self, dest: str, *args: Any) -> None:
|
||||||
|
@ -77,8 +65,8 @@ def try_exec(want_retval: Union[bool, int], func: Any, *args: list[Any]) -> Any:
|
||||||
|
|
||||||
return ["exception", "pebkac", ex.code, str(ex)]
|
return ["exception", "pebkac", ex.code, str(ex)]
|
||||||
|
|
||||||
except Exception as ex:
|
except:
|
||||||
if not want_retval:
|
if not want_retval:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
return ["exception", "stack", ex]
|
return ["exception", "stack", traceback.format_exc()]
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import calendar
|
import calendar
|
||||||
import errno
|
import errno
|
||||||
|
import filecmp
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from .__init__ import ANYWIN
|
from .__init__ import ANYWIN
|
||||||
from .util import Netdev, atomic_move, load_resource, runcmd, wunlink
|
from .util import Netdev, runcmd, wrename, wunlink
|
||||||
|
|
||||||
HAVE_CFSSL = not os.environ.get("PRTY_NO_CFSSL")
|
HAVE_CFSSL = True
|
||||||
|
|
||||||
if True: # pylint: disable=using-constant-test
|
if True: # pylint: disable=using-constant-test
|
||||||
from .util import NamedLogger, RootLogger
|
from .util import RootLogger
|
||||||
|
|
||||||
|
|
||||||
if ANYWIN:
|
if ANYWIN:
|
||||||
|
@ -27,15 +29,13 @@ def ensure_cert(log: "RootLogger", args) -> None:
|
||||||
|
|
||||||
i feel awful about this and so should they
|
i feel awful about this and so should they
|
||||||
"""
|
"""
|
||||||
with load_resource(args.E, "res/insecure.pem") as f:
|
cert_insec = os.path.join(args.E.mod, "res/insecure.pem")
|
||||||
cert_insec = f.read()
|
|
||||||
cert_appdata = os.path.join(args.E.cfg, "cert.pem")
|
cert_appdata = os.path.join(args.E.cfg, "cert.pem")
|
||||||
if not os.path.isfile(args.cert):
|
if not os.path.isfile(args.cert):
|
||||||
if cert_appdata != args.cert:
|
if cert_appdata != args.cert:
|
||||||
raise Exception("certificate file does not exist: " + args.cert)
|
raise Exception("certificate file does not exist: " + args.cert)
|
||||||
|
|
||||||
with open(args.cert, "wb") as f:
|
shutil.copy(cert_insec, args.cert)
|
||||||
f.write(cert_insec)
|
|
||||||
|
|
||||||
with open(args.cert, "rb") as f:
|
with open(args.cert, "rb") as f:
|
||||||
buf = f.read()
|
buf = f.read()
|
||||||
|
@ -50,9 +50,7 @@ def ensure_cert(log: "RootLogger", args) -> None:
|
||||||
raise Exception(m + "private key must appear before server certificate")
|
raise Exception(m + "private key must appear before server certificate")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(args.cert, "rb") as f:
|
if filecmp.cmp(args.cert, cert_insec):
|
||||||
active_cert = f.read()
|
|
||||||
if active_cert == cert_insec:
|
|
||||||
t = "using default TLS certificate; https will be insecure:\033[36m {}"
|
t = "using default TLS certificate; https will be insecure:\033[36m {}"
|
||||||
log("cert", t.format(args.cert), 3)
|
log("cert", t.format(args.cert), 3)
|
||||||
except:
|
except:
|
||||||
|
@ -85,8 +83,6 @@ def _read_crt(args, fn):
|
||||||
|
|
||||||
|
|
||||||
def _gen_ca(log: "RootLogger", args):
|
def _gen_ca(log: "RootLogger", args):
|
||||||
nlog: "NamedLogger" = lambda msg, c=0: log("cert-gen-ca", msg, c)
|
|
||||||
|
|
||||||
expiry = _read_crt(args, "ca.pem")[0]
|
expiry = _read_crt(args, "ca.pem")[0]
|
||||||
if time.time() + args.crt_cdays * 60 * 60 * 24 * 0.1 < expiry:
|
if time.time() + args.crt_cdays * 60 * 60 * 24 * 0.1 < expiry:
|
||||||
return
|
return
|
||||||
|
@ -117,18 +113,16 @@ def _gen_ca(log: "RootLogger", args):
|
||||||
|
|
||||||
bname = os.path.join(args.crt_dir, "ca")
|
bname = os.path.join(args.crt_dir, "ca")
|
||||||
try:
|
try:
|
||||||
wunlink(nlog, bname + ".key", VF)
|
wunlink(log, bname + ".key", VF)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
atomic_move(nlog, bname + "-key.pem", bname + ".key", VF)
|
wrename(log, bname + "-key.pem", bname + ".key", VF)
|
||||||
wunlink(nlog, bname + ".csr", VF)
|
wunlink(log, bname + ".csr", VF)
|
||||||
|
|
||||||
log("cert", "new ca OK", 2)
|
log("cert", "new ca OK", 2)
|
||||||
|
|
||||||
|
|
||||||
def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
||||||
nlog: "NamedLogger" = lambda msg, c=0: log("cert-gen-srv", msg, c)
|
|
||||||
|
|
||||||
names = args.crt_ns.split(",") if args.crt_ns else []
|
names = args.crt_ns.split(",") if args.crt_ns else []
|
||||||
if not args.crt_exact:
|
if not args.crt_exact:
|
||||||
for n in names[:]:
|
for n in names[:]:
|
||||||
|
@ -153,22 +147,14 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
||||||
raise Exception("no useable cert found")
|
raise Exception("no useable cert found")
|
||||||
|
|
||||||
expired = time.time() + args.crt_sdays * 60 * 60 * 24 * 0.5 > expiry
|
expired = time.time() + args.crt_sdays * 60 * 60 * 24 * 0.5 > expiry
|
||||||
if expired:
|
cert_insec = os.path.join(args.E.mod, "res/insecure.pem")
|
||||||
raise Exception("old server-cert has expired")
|
|
||||||
|
|
||||||
for n in names:
|
for n in names:
|
||||||
if n not in inf["sans"]:
|
if n not in inf["sans"]:
|
||||||
raise Exception("does not have {}".format(n))
|
raise Exception("does not have {}".format(n))
|
||||||
|
if expired:
|
||||||
with load_resource(args.E, "res/insecure.pem") as f:
|
raise Exception("old server-cert has expired")
|
||||||
cert_insec = f.read()
|
if not filecmp.cmp(args.cert, cert_insec):
|
||||||
|
|
||||||
with open(args.cert, "rb") as f:
|
|
||||||
active_cert = f.read()
|
|
||||||
|
|
||||||
if active_cert and active_cert != cert_insec:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log("cert", "will create new server-cert; {}".format(ex))
|
log("cert", "will create new server-cert; {}".format(ex))
|
||||||
|
|
||||||
|
@ -210,11 +196,11 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
||||||
|
|
||||||
bname = os.path.join(args.crt_dir, "srv")
|
bname = os.path.join(args.crt_dir, "srv")
|
||||||
try:
|
try:
|
||||||
wunlink(nlog, bname + ".key", VF)
|
wunlink(log, bname + ".key", VF)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
atomic_move(nlog, bname + "-key.pem", bname + ".key", VF)
|
wrename(log, bname + "-key.pem", bname + ".key", VF)
|
||||||
wunlink(nlog, bname + ".csr", VF)
|
wunlink(log, bname + ".csr", VF)
|
||||||
|
|
||||||
with open(os.path.join(args.crt_dir, "ca.pem"), "rb") as f:
|
with open(os.path.join(args.crt_dir, "ca.pem"), "rb") as f:
|
||||||
ca = f.read()
|
ca = f.read()
|
||||||
|
|
156
copyparty/cfg.py
156
copyparty/cfg.py
|
@ -2,12 +2,9 @@
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
# awk -F\" '/add_argument\("-[^-]/{print(substr($2,2))}' copyparty/__main__.py | sort | tr '\n' ' '
|
# awk -F\" '/add_argument\("-[^-]/{print(substr($2,2))}' copyparty/__main__.py | sort | tr '\n' ' '
|
||||||
zs = "a c e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vp e2vu ed emp i j lo mcr mte mth mtm mtp nb nc nid nih nth nw p q s ss sss v z zv"
|
zs = "a c e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vp e2vu ed emp i j lo mcr mte mth mtm mtp nb nc nid nih nw p q s ss sss v z zv"
|
||||||
onedash = set(zs.split())
|
onedash = set(zs.split())
|
||||||
|
|
||||||
# verify that all volflags are documented here:
|
|
||||||
# grep volflag= __main__.py | sed -r 's/.*volflag=//;s/\).*//' | sort | uniq | while IFS= read -r x; do grep -E "\"$x(=[^ \"]+)?\": \"" cfg.py || printf '%s\n' "$x"; done
|
|
||||||
|
|
||||||
|
|
||||||
def vf_bmap() -> dict[str, str]:
|
def vf_bmap() -> dict[str, str]:
|
||||||
"""argv-to-volflag: simple bools"""
|
"""argv-to-volflag: simple bools"""
|
||||||
|
@ -15,20 +12,17 @@ def vf_bmap() -> dict[str, str]:
|
||||||
"dav_auth": "davauth",
|
"dav_auth": "davauth",
|
||||||
"dav_rt": "davrt",
|
"dav_rt": "davrt",
|
||||||
"ed": "dots",
|
"ed": "dots",
|
||||||
"hardlink_only": "hardlinkonly",
|
"never_symlink": "neversymlink",
|
||||||
"no_clone": "noclone",
|
"no_dedup": "copydupes",
|
||||||
"no_dirsz": "nodirsz",
|
|
||||||
"no_dupe": "nodupe",
|
"no_dupe": "nodupe",
|
||||||
"no_forget": "noforget",
|
"no_forget": "noforget",
|
||||||
"no_pipe": "nopipe",
|
"no_pipe": "nopipe",
|
||||||
"no_robots": "norobots",
|
"no_robots": "norobots",
|
||||||
"no_tail": "notail",
|
|
||||||
"no_thumb": "dthumb",
|
"no_thumb": "dthumb",
|
||||||
"no_vthumb": "dvthumb",
|
"no_vthumb": "dvthumb",
|
||||||
"no_athumb": "dathumb",
|
"no_athumb": "dathumb",
|
||||||
}
|
}
|
||||||
for k in (
|
for k in (
|
||||||
"dedup",
|
|
||||||
"dotsrch",
|
"dotsrch",
|
||||||
"e2d",
|
"e2d",
|
||||||
"e2ds",
|
"e2ds",
|
||||||
|
@ -41,25 +35,17 @@ def vf_bmap() -> dict[str, str]:
|
||||||
"e2vp",
|
"e2vp",
|
||||||
"exp",
|
"exp",
|
||||||
"grid",
|
"grid",
|
||||||
"gsel",
|
|
||||||
"hardlink",
|
"hardlink",
|
||||||
"magic",
|
"magic",
|
||||||
"no_db_ip",
|
|
||||||
"no_sb_md",
|
"no_sb_md",
|
||||||
"no_sb_lg",
|
"no_sb_lg",
|
||||||
"nsort",
|
|
||||||
"og",
|
"og",
|
||||||
"og_no_head",
|
"og_no_head",
|
||||||
"og_s_title",
|
"og_s_title",
|
||||||
"rand",
|
"rand",
|
||||||
"reflink",
|
|
||||||
"rmagic",
|
|
||||||
"rss",
|
|
||||||
"wo_up_readme",
|
|
||||||
"xdev",
|
"xdev",
|
||||||
"xlink",
|
"xlink",
|
||||||
"xvol",
|
"xvol",
|
||||||
"zipmaxu",
|
|
||||||
):
|
):
|
||||||
ret[k] = k
|
ret[k] = k
|
||||||
return ret
|
return ret
|
||||||
|
@ -68,31 +54,20 @@ 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 = {
|
||||||
"ac_convt": "aconvt",
|
|
||||||
"no_hash": "nohash",
|
"no_hash": "nohash",
|
||||||
"no_idx": "noidx",
|
"no_idx": "noidx",
|
||||||
"re_maxage": "scan",
|
"re_maxage": "scan",
|
||||||
"safe_dedup": "safededup",
|
|
||||||
"th_convt": "convt",
|
"th_convt": "convt",
|
||||||
"th_size": "thsize",
|
"th_size": "thsize",
|
||||||
"th_crop": "crop",
|
"th_crop": "crop",
|
||||||
"th_x3": "th3x",
|
"th_x3": "th3x",
|
||||||
}
|
}
|
||||||
for k in (
|
for k in (
|
||||||
"bup_ck",
|
|
||||||
"chmod_d",
|
|
||||||
"chmod_f",
|
|
||||||
"dbd",
|
"dbd",
|
||||||
"forget_ip",
|
|
||||||
"hsortn",
|
|
||||||
"html_head",
|
"html_head",
|
||||||
"lg_sbf",
|
"lg_sbf",
|
||||||
"md_sbf",
|
"md_sbf",
|
||||||
"lg_sba",
|
|
||||||
"md_sba",
|
|
||||||
"md_hist",
|
|
||||||
"nrand",
|
"nrand",
|
||||||
"u2ow",
|
|
||||||
"og_desc",
|
"og_desc",
|
||||||
"og_site",
|
"og_site",
|
||||||
"og_th",
|
"og_th",
|
||||||
|
@ -102,29 +77,13 @@ def vf_vmap() -> dict[str, str]:
|
||||||
"og_title_i",
|
"og_title_i",
|
||||||
"og_tpl",
|
"og_tpl",
|
||||||
"og_ua",
|
"og_ua",
|
||||||
"put_ck",
|
|
||||||
"put_name",
|
|
||||||
"mv_retry",
|
"mv_retry",
|
||||||
"rm_retry",
|
"rm_retry",
|
||||||
"sort",
|
"sort",
|
||||||
"tail_fd",
|
|
||||||
"tail_rate",
|
|
||||||
"tail_tmax",
|
|
||||||
"tail_who",
|
|
||||||
"tcolor",
|
"tcolor",
|
||||||
"th_spec_p",
|
|
||||||
"txt_eol",
|
|
||||||
"unlist",
|
"unlist",
|
||||||
"u2abort",
|
"u2abort",
|
||||||
"u2ts",
|
"u2ts",
|
||||||
"uid",
|
|
||||||
"gid",
|
|
||||||
"unp_who",
|
|
||||||
"ups_who",
|
|
||||||
"zip_who",
|
|
||||||
"zipmaxn",
|
|
||||||
"zipmaxs",
|
|
||||||
"zipmaxt",
|
|
||||||
):
|
):
|
||||||
ret[k] = k
|
ret[k] = k
|
||||||
return ret
|
return ret
|
||||||
|
@ -136,16 +95,13 @@ def vf_cmap() -> dict[str, str]:
|
||||||
for k in (
|
for k in (
|
||||||
"exp_lg",
|
"exp_lg",
|
||||||
"exp_md",
|
"exp_md",
|
||||||
"ext_th",
|
|
||||||
"mte",
|
"mte",
|
||||||
"mth",
|
"mth",
|
||||||
"mtp",
|
"mtp",
|
||||||
"xac",
|
|
||||||
"xad",
|
"xad",
|
||||||
"xar",
|
"xar",
|
||||||
"xau",
|
"xau",
|
||||||
"xban",
|
"xban",
|
||||||
"xbc",
|
|
||||||
"xbd",
|
"xbd",
|
||||||
"xbr",
|
"xbr",
|
||||||
"xbu",
|
"xbu",
|
||||||
|
@ -172,27 +128,15 @@ permdescs = {
|
||||||
|
|
||||||
flagcats = {
|
flagcats = {
|
||||||
"uploads, general": {
|
"uploads, general": {
|
||||||
"dedup": "enable symlink-based file deduplication",
|
"nodupe": "rejects existing files (instead of symlinking them)",
|
||||||
"hardlink": "enable hardlink-based file deduplication,\nwith fallback on symlinks when that is impossible",
|
"hardlink": "does dedup with hardlinks instead of symlinks",
|
||||||
"hardlinkonly": "dedup with hardlink only, never symlink;\nmake a full copy if hardlink is impossible",
|
"neversymlink": "disables symlink fallback; full copy instead",
|
||||||
"reflink": "enable reflink-based file deduplication,\nwith fallback on full copy when that is impossible",
|
"copydupes": "disables dedup, always saves full copies of dupes",
|
||||||
"safededup": "verify on-disk data before using it for dedup",
|
|
||||||
"noclone": "take dupe data from clients, even if available on HDD",
|
|
||||||
"nodupe": "rejects existing files (instead of linking/cloning them)",
|
|
||||||
"chmod_d=755": "unix-permission for new dirs/folders",
|
|
||||||
"chmod_f=644": "unix-permission for new files",
|
|
||||||
"uid=573": "change owner of new files/folders to unix-user 573",
|
|
||||||
"gid=999": "change owner of new files/folders to unix-group 999",
|
|
||||||
"sparse": "force use of sparse files, mainly for s3-backed storage",
|
"sparse": "force use of sparse files, mainly for s3-backed storage",
|
||||||
"nosparse": "deny use of sparse files, mainly for slow storage",
|
|
||||||
"daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
|
"daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
|
||||||
"nosub": "forces all uploads into the top folder of the vfs",
|
"nosub": "forces all uploads into the top folder of the vfs",
|
||||||
"magic": "enables filetype detection for nameless uploads",
|
"magic": "enables filetype detection for nameless uploads",
|
||||||
"put_name": "fallback filename for nameless uploads",
|
"gz": "allows server-side gzip of uploads with ?gz (also c,xz)",
|
||||||
"put_ck": "default checksum-hasher for PUT/WebDAV uploads",
|
|
||||||
"bup_ck": "default checksum-hasher for bup/basic uploads",
|
|
||||||
"gz": "allows server-side gzip compression of uploads with ?gz",
|
|
||||||
"xz": "allows server-side lzma compression of uploads with ?xz",
|
|
||||||
"pk": "forces server-side compression, optional arg: xz,9",
|
"pk": "forces server-side compression, optional arg: xz,9",
|
||||||
},
|
},
|
||||||
"upload rules": {
|
"upload rules": {
|
||||||
|
@ -200,11 +144,8 @@ flagcats = {
|
||||||
"maxb=1g,300": "max 1 GiB over 5min (suffixes: b, k, m, g, t)",
|
"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)",
|
"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)",
|
"vmaxn=4k": "max 4096 files in volume (suffixes: b, k, m, g, t)",
|
||||||
"medialinks": "return medialinks for non-up2k uploads (not hotlinks)",
|
|
||||||
"wo_up_readme": "write-only users can upload logues without getting renamed",
|
|
||||||
"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",
|
||||||
"u2ow=N": "overwrite existing files? 0=no 1=if-older 2=always",
|
|
||||||
"u2ts=fc": "[f]orce [c]lient-last-modified or [u]pload-time",
|
"u2ts=fc": "[f]orce [c]lient-last-modified or [u]pload-time",
|
||||||
"u2abort=1": "allow aborting unfinished uploads? 0=no 1=strict 2=ip-chk 3=acct-chk",
|
"u2abort=1": "allow aborting unfinished uploads? 0=no 1=strict 2=ip-chk 3=acct-chk",
|
||||||
"sz=1k-3m": "allow filesizes between 1 KiB and 3MiB",
|
"sz=1k-3m": "allow filesizes between 1 KiB and 3MiB",
|
||||||
|
@ -216,41 +157,31 @@ flagcats = {
|
||||||
"lifetime=3600": "uploads are deleted after 1 hour",
|
"lifetime=3600": "uploads are deleted after 1 hour",
|
||||||
},
|
},
|
||||||
"database, general": {
|
"database, general": {
|
||||||
"e2d": "enable database; makes files searchable + enables upload-undo",
|
"e2d": "enable database; makes files searchable + enables upload dedup",
|
||||||
"e2ds": "scan writable folders for new files on startup; also sets -e2d",
|
"e2ds": "scan writable folders for new files on startup; also sets -e2d",
|
||||||
"e2dsa": "scans all folders for new files on startup; also sets -e2d",
|
"e2dsa": "scans all folders for new files on startup; also sets -e2d",
|
||||||
"e2t": "enable multimedia indexing; makes it possible to search for tags",
|
"e2t": "enable multimedia indexing; makes it possible to search for tags",
|
||||||
"e2ts": "scan existing files for tags on startup; also sets -e2t",
|
"e2ts": "scan existing files for tags on startup; also sets -e2t",
|
||||||
"e2tsr": "delete all metadata from DB (full rescan); also sets -e2ts",
|
"e2tsa": "delete all metadata from DB (full rescan); also sets -e2ts",
|
||||||
"d2ts": "disables metadata collection for existing files",
|
"d2ts": "disables metadata collection for existing files",
|
||||||
"e2v": "verify integrity on startup by hashing files and comparing to db",
|
|
||||||
"e2vu": "when e2v fails, update the db (assume on-disk files are good)",
|
|
||||||
"e2vp": "when e2v fails, panic and quit copyparty",
|
|
||||||
"d2ds": "disables onboot indexing, overrides -e2ds*",
|
"d2ds": "disables onboot indexing, overrides -e2ds*",
|
||||||
"d2t": "disables metadata collection, overrides -e2t*",
|
"d2t": "disables metadata collection, overrides -e2t*",
|
||||||
"d2v": "disables file verification, overrides -e2v*",
|
"d2v": "disables file verification, overrides -e2v*",
|
||||||
"d2d": "disables all database stuff, overrides -e2*",
|
"d2d": "disables all database stuff, overrides -e2*",
|
||||||
"hist=/tmp/cdb": "puts thumbnails and indexes at that location",
|
"hist=/tmp/cdb": "puts thumbnails and indexes at that location",
|
||||||
"dbpath=/tmp/cdb": "puts indexes at that location",
|
|
||||||
"landmark=foo": "disable db if file foo doesn't exist",
|
|
||||||
"scan=60": "scan for new files every 60sec, same as --re-maxage",
|
"scan=60": "scan for new files every 60sec, same as --re-maxage",
|
||||||
"nohash=\\.iso$": "skips hashing file contents if path matches *.iso",
|
"nohash=\\.iso$": "skips hashing file contents if path matches *.iso",
|
||||||
"noidx=\\.iso$": "fully ignores the contents at paths matching *.iso",
|
"noidx=\\.iso$": "fully ignores the contents at paths matching *.iso",
|
||||||
"noforget": "don't forget files when deleted from disk",
|
"noforget": "don't forget files when deleted from disk",
|
||||||
"forget_ip=43200": "forget uploader-IP after 30 days (GDPR)",
|
|
||||||
"no_db_ip": "never store uploader-IP in the db; disables unpost",
|
|
||||||
"fat32": "avoid excessive reindexing on android sdcardfs",
|
"fat32": "avoid excessive reindexing on android sdcardfs",
|
||||||
"dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
|
"dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
|
||||||
"xlink": "cross-volume dupe detection / linking (dangerous)",
|
"xlink": "cross-volume dupe detection / linking",
|
||||||
"xdev": "do not descend into other filesystems",
|
"xdev": "do not descend into other filesystems",
|
||||||
"xvol": "do not follow symlinks leaving the volume root",
|
"xvol": "do not follow symlinks leaving the volume root",
|
||||||
"dotsrch": "show dotfiles in search results",
|
"dotsrch": "show dotfiles in search results",
|
||||||
"nodotsrch": "hide dotfiles in search results (default)",
|
"nodotsrch": "hide dotfiles in search results (default)",
|
||||||
"srch_excl": "exclude search results with URL matching this regex",
|
|
||||||
},
|
},
|
||||||
'database, audio tags\n"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ...': {
|
'database, audio tags\n"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ...': {
|
||||||
"mte=artist,title": "media-tags to index/display",
|
|
||||||
"mth=fmt,res,ac": "media-tags to hide by default",
|
|
||||||
"mtp=.bpm=f,audio-bpm.py": 'uses the "audio-bpm.py" program to\ngenerate ".bpm" tags from uploads (f = overwrite tags)',
|
"mtp=.bpm=f,audio-bpm.py": 'uses the "audio-bpm.py" program to\ngenerate ".bpm" tags from uploads (f = overwrite tags)',
|
||||||
"mtp=ahash,vhash=media-hash.py": "collects two tags at once",
|
"mtp=ahash,vhash=media-hash.py": "collects two tags at once",
|
||||||
},
|
},
|
||||||
|
@ -259,14 +190,10 @@ 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",
|
||||||
"pngquant": "compress audio waveforms 33% better",
|
|
||||||
"thsize": "thumbnail res; WxH",
|
"thsize": "thumbnail res; WxH",
|
||||||
"crop": "center-cropping (y/n/fy/fn)",
|
"crop": "center-cropping (y/n/fy/fn)",
|
||||||
"th3x": "3x resolution (y/n/fy/fn)",
|
"th3x": "3x resolution (y/n/fy/fn)",
|
||||||
"convt": "convert-to-image timeout in seconds",
|
"convt": "conversion timeout in seconds",
|
||||||
"aconvt": "convert-to-audio timeout in seconds",
|
|
||||||
"th_spec_p=1": "make spectrograms? 0=never 1=fallback 2=always",
|
|
||||||
"ext_th=s=/b.png": "use /b.png as thumbnail for file-extension s",
|
|
||||||
},
|
},
|
||||||
"handlers\n(better explained in --help-handlers)": {
|
"handlers\n(better explained in --help-handlers)": {
|
||||||
"on404=PY": "handle 404s by executing PY file",
|
"on404=PY": "handle 404s by executing PY file",
|
||||||
|
@ -276,8 +203,6 @@ flagcats = {
|
||||||
"xbu=CMD": "execute CMD before a file upload starts",
|
"xbu=CMD": "execute CMD before a file upload starts",
|
||||||
"xau=CMD": "execute CMD after a file upload finishes",
|
"xau=CMD": "execute CMD after a file upload finishes",
|
||||||
"xiu=CMD": "execute CMD after all uploads finish and volume is idle",
|
"xiu=CMD": "execute CMD after all uploads finish and volume is idle",
|
||||||
"xbc=CMD": "execute CMD before a file copy",
|
|
||||||
"xac=CMD": "execute CMD after a file copy",
|
|
||||||
"xbr=CMD": "execute CMD before a file rename/move",
|
"xbr=CMD": "execute CMD before a file rename/move",
|
||||||
"xar=CMD": "execute CMD after a file rename/move",
|
"xar=CMD": "execute CMD after a file rename/move",
|
||||||
"xbd=CMD": "execute CMD before a file delete",
|
"xbd=CMD": "execute CMD before a file delete",
|
||||||
|
@ -287,73 +212,23 @@ flagcats = {
|
||||||
},
|
},
|
||||||
"client and ux": {
|
"client and ux": {
|
||||||
"grid": "show grid/thumbnails by default",
|
"grid": "show grid/thumbnails by default",
|
||||||
"gsel": "select files in grid by ctrl-click",
|
|
||||||
"sort": "default sort order",
|
"sort": "default sort order",
|
||||||
"nsort": "natural-sort of leading digits in filenames",
|
|
||||||
"hsortn": "number of sort-rules to add to media URLs",
|
|
||||||
"unlist": "dont list files matching REGEX",
|
"unlist": "dont list files matching REGEX",
|
||||||
"html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH",
|
"html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH",
|
||||||
"tcolor=#fc0": "theme color (a hint for webbrowsers, discord, etc.)",
|
|
||||||
"nodirsz": "don't show total folder size",
|
|
||||||
"robots": "allows indexing by search engines (default)",
|
"robots": "allows indexing by search engines (default)",
|
||||||
"norobots": "kindly asks search engines to leave",
|
"norobots": "kindly asks search engines to leave",
|
||||||
"unlistcr": "don't list read-access in controlpanel",
|
|
||||||
"unlistcw": "don't list write-access in controlpanel",
|
|
||||||
"no_sb_md": "disable js sandbox for markdown files",
|
"no_sb_md": "disable js sandbox for markdown files",
|
||||||
"no_sb_lg": "disable js sandbox for prologue/epilogue",
|
"no_sb_lg": "disable js sandbox for prologue/epilogue",
|
||||||
"sb_md": "enable js sandbox for markdown files (default)",
|
"sb_md": "enable js sandbox for markdown files (default)",
|
||||||
"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",
|
||||||
"md_sba": "value of iframe allow-prop for markdown-sandbox",
|
|
||||||
"lg_sba": "value of iframe allow-prop for *logue-sandbox",
|
|
||||||
"nohtml": "return html and markdown as text/html",
|
"nohtml": "return html and markdown as text/html",
|
||||||
},
|
},
|
||||||
"opengraph (discord embeds)": {
|
|
||||||
"og": "enable OG (disables hotlinking)",
|
|
||||||
"og_site": "sitename; defaults to --name, disable with '-'",
|
|
||||||
"og_desc": "description text for all files; disable with '-'",
|
|
||||||
"og_th=jf": "thumbnail format; j / jf / jf3 / w / w3 / ...",
|
|
||||||
"og_title_a": "audio title format; default: {{ artist }} - {{ title }}",
|
|
||||||
"og_title_v": "video title format; default: {{ title }}",
|
|
||||||
"og_title_i": "image title format; default: {{ title }}",
|
|
||||||
"og_title=foo": "fallback title if there's nothing in the db",
|
|
||||||
"og_s_title": "force default title; do not read from tags",
|
|
||||||
"og_tpl": "custom html; see --og-tpl in --help",
|
|
||||||
"og_no_head": "you want to add tags manually with og_tpl",
|
|
||||||
"og_ua": "if defined: only send OG html if useragent matches this regex",
|
|
||||||
},
|
|
||||||
"textfiles": {
|
|
||||||
"md_hist": "where to put markdown backups; s=subfolder, v=volHist, n=nope",
|
|
||||||
"exp": "enable textfile expansion; see --help-exp",
|
|
||||||
"exp_md": "placeholders to expand in markdown files; see --help",
|
|
||||||
"exp_lg": "placeholders to expand in prologue/epilogue; see --help",
|
|
||||||
"txt_eol=lf": "enable EOL conversion when writing docs (LF or CRLF)",
|
|
||||||
},
|
|
||||||
"tailing": {
|
|
||||||
"notail": "disable ?tail (download a growing file continuously)",
|
|
||||||
"tail_fd=1": "check if file was replaced (new fd) every 1 sec",
|
|
||||||
"tail_rate=0.2": "check for new data every 0.2 sec",
|
|
||||||
"tail_tmax=30": "kill connection after 30 sec",
|
|
||||||
"tail_who=2": "restrict ?tail access (1=admins,2=authed,3=everyone)",
|
|
||||||
},
|
|
||||||
"others": {
|
"others": {
|
||||||
"dots": "allow all users with read-access to\nenable the option to show dotfiles in listings",
|
"dots": "allow all users with read-access to\nenable the option to show dotfiles in listings",
|
||||||
"fk=8": 'generates per-file accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes',
|
"fk=8": 'generates per-file accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes',
|
||||||
"fka=8": 'generates slightly weaker per-file accesskeys,\nwhich are then required at the "g" permission;\nnot affected by filesize or inode numbers',
|
"fka=8": 'generates slightly weaker per-file accesskeys,\nwhich are then required at the "g" permission;\nnot affected by filesize or inode numbers',
|
||||||
"dk=8": 'generates per-directory accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes',
|
|
||||||
"dks": "per-directory accesskeys allow browsing into subdirs",
|
|
||||||
"dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so',
|
|
||||||
"rss": "allow '?rss' URL suffix (experimental)",
|
|
||||||
"rmagic": "expensive analysis for mimetype accuracy",
|
|
||||||
"unp_who=2": "unpost only if same... 1=ip+name, 2=ip, 3=name",
|
|
||||||
"ups_who=2": "restrict viewing the list of recent uploads",
|
|
||||||
"zip_who=2": "restrict access to download-as-zip/tar",
|
|
||||||
"zipmaxn=9k": "reject download-as-zip if more than 9000 files",
|
|
||||||
"zipmaxs=2g": "reject download-as-zip if size over 2 GiB",
|
|
||||||
"zipmaxt=no": "reply with 'no' if download-as-zip exceeds max",
|
|
||||||
"zipmaxu": "zip-size-limit does not apply to authenticated users",
|
|
||||||
"nopipe": "disable race-the-beam (download unfinished uploads)",
|
|
||||||
"mv_retry": "ms-windows: timeout for renaming busy files",
|
"mv_retry": "ms-windows: timeout for renaming busy files",
|
||||||
"rm_retry": "ms-windows: timeout for deleting busy files",
|
"rm_retry": "ms-windows: timeout for deleting busy files",
|
||||||
"davauth": "ask webdav clients to login for all folders",
|
"davauth": "ask webdav clients to login for all folders",
|
||||||
|
@ -363,10 +238,3 @@ flagcats = {
|
||||||
|
|
||||||
|
|
||||||
flagdescs = {k.split("=")[0]: v for tab in flagcats.values() for k, v in tab.items()}
|
flagdescs = {k.split("=")[0]: v for tab in flagcats.values() for k, v in tab.items()}
|
||||||
|
|
||||||
|
|
||||||
if True: # so it gets removed in release-builds
|
|
||||||
for fun in [vf_bmap, vf_cmap, vf_vmap]:
|
|
||||||
for k in fun().values():
|
|
||||||
if k not in flagdescs:
|
|
||||||
raise Exception("undocumented volflag: " + k)
|
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
# coding: utf-8
|
|
||||||
from __future__ import print_function, unicode_literals
|
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
import sys
|
import sys
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
@ -11,10 +8,6 @@ if True: # pylint: disable=using-constant-test
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
|
||||||
class BadXML(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def get_ET() -> ET.XMLParser:
|
def get_ET() -> ET.XMLParser:
|
||||||
pn = "xml.etree.ElementTree"
|
pn = "xml.etree.ElementTree"
|
||||||
cn = "_elementtree"
|
cn = "_elementtree"
|
||||||
|
@ -41,7 +34,7 @@ def get_ET() -> ET.XMLParser:
|
||||||
XMLParser: ET.XMLParser = get_ET()
|
XMLParser: ET.XMLParser = get_ET()
|
||||||
|
|
||||||
|
|
||||||
class _DXMLParser(XMLParser): # type: ignore
|
class DXMLParser(XMLParser): # type: ignore
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
tb = ET.TreeBuilder()
|
tb = ET.TreeBuilder()
|
||||||
super(DXMLParser, self).__init__(target=tb)
|
super(DXMLParser, self).__init__(target=tb)
|
||||||
|
@ -56,57 +49,16 @@ class _DXMLParser(XMLParser): # type: ignore
|
||||||
raise BadXML("{}, {}".format(a, ka))
|
raise BadXML("{}, {}".format(a, ka))
|
||||||
|
|
||||||
|
|
||||||
class _NG(XMLParser): # type: ignore
|
class BadXML(Exception):
|
||||||
def __int__(self) -> None:
|
pass
|
||||||
raise BadXML("dxml selftest failed")
|
|
||||||
|
|
||||||
|
|
||||||
DXMLParser = _DXMLParser
|
|
||||||
|
|
||||||
|
|
||||||
def parse_xml(txt: str) -> ET.Element:
|
def parse_xml(txt: str) -> ET.Element:
|
||||||
"""
|
|
||||||
Parse XML into an xml.etree.ElementTree.Element while defusing some unsafe parts.
|
|
||||||
"""
|
|
||||||
parser = DXMLParser()
|
parser = DXMLParser()
|
||||||
parser.feed(txt)
|
parser.feed(txt)
|
||||||
return parser.close() # type: ignore
|
return parser.close() # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def selftest() -> bool:
|
|
||||||
qbe = r"""<!DOCTYPE d [
|
|
||||||
<!ENTITY a "nice_bakuretsu">
|
|
||||||
]>
|
|
||||||
<root>&a;&a;&a;</root>"""
|
|
||||||
|
|
||||||
emb = r"""<!DOCTYPE d [
|
|
||||||
<!ENTITY a SYSTEM "file:///etc/hostname">
|
|
||||||
]>
|
|
||||||
<root>&a;</root>"""
|
|
||||||
|
|
||||||
# future-proofing; there's never been any known vulns
|
|
||||||
# regarding DTDs and ET.XMLParser, but might as well
|
|
||||||
# block them since webdav-clients don't use them
|
|
||||||
dtd = r"""<!DOCTYPE d SYSTEM "a.dtd">
|
|
||||||
<root>a</root>"""
|
|
||||||
|
|
||||||
for txt in (qbe, emb, dtd):
|
|
||||||
try:
|
|
||||||
parse_xml(txt)
|
|
||||||
t = "WARNING: dxml selftest failed:\n%s\n"
|
|
||||||
print(t % (txt,), file=sys.stderr)
|
|
||||||
return False
|
|
||||||
except BadXML:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
DXML_OK = selftest()
|
|
||||||
if not DXML_OK:
|
|
||||||
DXMLParser = _NG
|
|
||||||
|
|
||||||
|
|
||||||
def mktnod(name: str, text: str) -> ET.Element:
|
def mktnod(name: str, text: str) -> ET.Element:
|
||||||
el = ET.Element(name)
|
el = ET.Element(name)
|
||||||
el.text = text
|
el.text = text
|
||||||
|
|
|
@ -9,12 +9,12 @@ import time
|
||||||
from .__init__ import ANYWIN, MACOS
|
from .__init__ import ANYWIN, MACOS
|
||||||
from .authsrv import AXS, VFS
|
from .authsrv import AXS, VFS
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
from .util import chkcmd, min_ex, undot
|
from .util import chkcmd, min_ex
|
||||||
|
|
||||||
if True: # pylint: disable=using-constant-test
|
if True: # pylint: disable=using-constant-test
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
from .util import RootLogger, undot
|
from .util import RootLogger
|
||||||
|
|
||||||
|
|
||||||
class Fstab(object):
|
class Fstab(object):
|
||||||
|
@ -42,17 +42,17 @@ class Fstab(object):
|
||||||
self.cache = {}
|
self.cache = {}
|
||||||
|
|
||||||
fs = "ext4"
|
fs = "ext4"
|
||||||
msg = "failed to determine filesystem at %r; assuming %s\n%s"
|
msg = "failed to determine filesystem at [{}]; assuming {}\n{}"
|
||||||
|
|
||||||
if ANYWIN:
|
if ANYWIN:
|
||||||
fs = "vfat"
|
fs = "vfat"
|
||||||
try:
|
try:
|
||||||
path = self._winpath(path)
|
path = self._winpath(path)
|
||||||
except:
|
except:
|
||||||
self.log(msg % (path, fs, min_ex()), 3)
|
self.log(msg.format(path, fs, min_ex()), 3)
|
||||||
return fs
|
return fs
|
||||||
|
|
||||||
path = undot(path)
|
path = path.lstrip("/")
|
||||||
try:
|
try:
|
||||||
return self.cache[path]
|
return self.cache[path]
|
||||||
except:
|
except:
|
||||||
|
@ -61,11 +61,11 @@ class Fstab(object):
|
||||||
try:
|
try:
|
||||||
fs = self.get_w32(path) if ANYWIN else self.get_unix(path)
|
fs = self.get_w32(path) if ANYWIN else self.get_unix(path)
|
||||||
except:
|
except:
|
||||||
self.log(msg % (path, fs, min_ex()), 3)
|
self.log(msg.format(path, fs, min_ex()), 3)
|
||||||
|
|
||||||
fs = fs.lower()
|
fs = fs.lower()
|
||||||
self.cache[path] = fs
|
self.cache[path] = fs
|
||||||
self.log("found %s at %r" % (fs, path))
|
self.log("found {} at {}".format(fs, path))
|
||||||
return fs
|
return fs
|
||||||
|
|
||||||
def _winpath(self, path: str) -> str:
|
def _winpath(self, path: str) -> str:
|
||||||
|
@ -78,7 +78,7 @@ class Fstab(object):
|
||||||
return vid
|
return vid
|
||||||
|
|
||||||
def build_fallback(self) -> None:
|
def build_fallback(self) -> None:
|
||||||
self.tab = VFS(self.log_func, "idk", "/", "/", AXS(), {})
|
self.tab = VFS(self.log_func, "idk", "/", AXS(), {})
|
||||||
self.trusted = False
|
self.trusted = False
|
||||||
|
|
||||||
def build_tab(self) -> None:
|
def build_tab(self) -> None:
|
||||||
|
@ -111,30 +111,28 @@ class Fstab(object):
|
||||||
|
|
||||||
tab1.sort(key=lambda x: (len(x[0]), x[0]))
|
tab1.sort(key=lambda x: (len(x[0]), x[0]))
|
||||||
path1, fs1 = tab1[0]
|
path1, fs1 = tab1[0]
|
||||||
tab = VFS(self.log_func, fs1, path1, path1, AXS(), {})
|
tab = VFS(self.log_func, fs1, path1, AXS(), {})
|
||||||
for path, fs in tab1[1:]:
|
for path, fs in tab1[1:]:
|
||||||
zs = path.lstrip("/")
|
tab.add(fs, path.lstrip("/"))
|
||||||
tab.add(fs, zs, zs)
|
|
||||||
|
|
||||||
self.tab = tab
|
self.tab = tab
|
||||||
self.srctab = srctab
|
self.srctab = srctab
|
||||||
|
|
||||||
def relabel(self, path: str, nval: str) -> None:
|
def relabel(self, path: str, nval: str) -> None:
|
||||||
assert self.tab # !rm
|
assert self.tab
|
||||||
self.cache = {}
|
self.cache = {}
|
||||||
if ANYWIN:
|
if ANYWIN:
|
||||||
path = self._winpath(path)
|
path = self._winpath(path)
|
||||||
|
|
||||||
path = undot(path)
|
path = path.lstrip("/")
|
||||||
ptn = re.compile(r"^[^\\/]*")
|
ptn = re.compile(r"^[^\\/]*")
|
||||||
vn, rem = self.tab._find(path)
|
vn, rem = self.tab._find(path)
|
||||||
if not self.trusted:
|
if not self.trusted:
|
||||||
# no mtab access; have to build as we go
|
# no mtab access; have to build as we go
|
||||||
if "/" in rem:
|
if "/" in rem:
|
||||||
zs = os.path.join(vn.vpath, rem.split("/")[0])
|
self.tab.add("idk", os.path.join(vn.vpath, rem.split("/")[0]))
|
||||||
self.tab.add("idk", zs, zs)
|
|
||||||
if rem:
|
if rem:
|
||||||
self.tab.add(nval, path, path)
|
self.tab.add(nval, path)
|
||||||
else:
|
else:
|
||||||
vn.realpath = nval
|
vn.realpath = nval
|
||||||
|
|
||||||
|
@ -158,7 +156,7 @@ class Fstab(object):
|
||||||
self.log("failed to build tab:\n{}".format(min_ex()), 3)
|
self.log("failed to build tab:\n{}".format(min_ex()), 3)
|
||||||
self.build_fallback()
|
self.build_fallback()
|
||||||
|
|
||||||
assert self.tab # !rm
|
assert self.tab
|
||||||
ret = self.tab._find(path)[0]
|
ret = self.tab._find(path)[0]
|
||||||
if self.trusted or path == ret.vpath:
|
if self.trusted or path == ret.vpath:
|
||||||
return ret.realpath.split("/")[0]
|
return ret.realpath.split("/")[0]
|
||||||
|
@ -169,6 +167,6 @@ class Fstab(object):
|
||||||
if not self.tab:
|
if not self.tab:
|
||||||
self.build_fallback()
|
self.build_fallback()
|
||||||
|
|
||||||
assert self.tab # !rm
|
assert self.tab
|
||||||
ret = self.tab._find(path)[0]
|
ret = self.tab._find(path)[0]
|
||||||
return ret.realpath
|
return ret.realpath
|
||||||
|
|
|
@ -19,8 +19,6 @@ from .__init__ import PY2, TYPE_CHECKING
|
||||||
from .authsrv import VFS
|
from .authsrv import VFS
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
from .util import (
|
from .util import (
|
||||||
FN_EMB,
|
|
||||||
VF_CAREFUL,
|
|
||||||
Daemon,
|
Daemon,
|
||||||
ODict,
|
ODict,
|
||||||
Pebkac,
|
Pebkac,
|
||||||
|
@ -31,9 +29,7 @@ from .util import (
|
||||||
relchk,
|
relchk,
|
||||||
runhook,
|
runhook,
|
||||||
sanitize_fn,
|
sanitize_fn,
|
||||||
set_fperms,
|
|
||||||
vjoin,
|
vjoin,
|
||||||
wunlink,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -41,10 +37,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
if True: # pylint: disable=using-constant-test
|
if True: # pylint: disable=using-constant-test
|
||||||
import typing
|
import typing
|
||||||
from typing import Any, Optional, Union
|
from typing import Any, Optional
|
||||||
|
|
||||||
if PY2:
|
|
||||||
range = xrange # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
class FSE(FilesystemError):
|
class FSE(FilesystemError):
|
||||||
|
@ -78,29 +71,16 @@ class FtpAuth(DummyAuthorizer):
|
||||||
else:
|
else:
|
||||||
raise AuthenticationFailed("banned")
|
raise AuthenticationFailed("banned")
|
||||||
|
|
||||||
args = self.hub.args
|
|
||||||
asrv = self.hub.asrv
|
asrv = self.hub.asrv
|
||||||
uname = "*"
|
uname = "*"
|
||||||
if username != "anonymous":
|
if username != "anonymous":
|
||||||
uname = ""
|
uname = ""
|
||||||
if args.usernames:
|
for zs in (password, username):
|
||||||
alts = ["%s:%s" % (username, password)]
|
|
||||||
else:
|
|
||||||
alts = password, username
|
|
||||||
|
|
||||||
for zs in alts:
|
|
||||||
zs = asrv.iacct.get(asrv.ah.hash(zs), "")
|
zs = asrv.iacct.get(asrv.ah.hash(zs), "")
|
||||||
if zs:
|
if zs:
|
||||||
uname = zs
|
uname = zs
|
||||||
break
|
break
|
||||||
|
|
||||||
if args.ipu and uname == "*":
|
|
||||||
uname = args.ipu_iu[args.ipu_nm.map(ip)]
|
|
||||||
if args.ipr and uname in args.ipr_u:
|
|
||||||
if not args.ipr_u[uname].map(ip):
|
|
||||||
logging.warning("username [%s] rejected by --ipr", uname)
|
|
||||||
uname = "*"
|
|
||||||
|
|
||||||
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
|
||||||
if g.lim:
|
if g.lim:
|
||||||
|
@ -159,9 +139,6 @@ class FtpFs(AbstractedFS):
|
||||||
self.listdirinfo = self.listdir
|
self.listdirinfo = self.listdir
|
||||||
self.chdir(".")
|
self.chdir(".")
|
||||||
|
|
||||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
|
||||||
self.hub.log("ftpd", msg, c)
|
|
||||||
|
|
||||||
def v2a(
|
def v2a(
|
||||||
self,
|
self,
|
||||||
vpath: str,
|
vpath: str,
|
||||||
|
@ -178,19 +155,9 @@ class FtpFs(AbstractedFS):
|
||||||
t = "Unsupported characters in [{}]"
|
t = "Unsupported characters in [{}]"
|
||||||
raise FSE(t.format(vpath), 1)
|
raise FSE(t.format(vpath), 1)
|
||||||
|
|
||||||
fn = sanitize_fn(fn or "", "")
|
fn = sanitize_fn(fn or "", "", [".prologue.html", ".epilogue.html"])
|
||||||
vpath = vjoin(rd, fn)
|
vpath = vjoin(rd, fn)
|
||||||
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
|
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
|
||||||
if (
|
|
||||||
w
|
|
||||||
and fn.lower() in FN_EMB
|
|
||||||
and self.h.uname not in vfs.axs.uread
|
|
||||||
and "wo_up_readme" not in vfs.flags
|
|
||||||
):
|
|
||||||
fn = "_wo_" + fn
|
|
||||||
vpath = vjoin(rd, fn)
|
|
||||||
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
|
|
||||||
|
|
||||||
if not vfs.realpath:
|
if not vfs.realpath:
|
||||||
t = "No filesystem mounted at [{}]"
|
t = "No filesystem mounted at [{}]"
|
||||||
raise FSE(t.format(vpath))
|
raise FSE(t.format(vpath))
|
||||||
|
@ -239,43 +206,19 @@ class FtpFs(AbstractedFS):
|
||||||
r = "r" in mode
|
r = "r" in mode
|
||||||
w = "w" in mode or "a" in mode or "+" in mode
|
w = "w" in mode or "a" in mode or "+" in mode
|
||||||
|
|
||||||
ap, vfs, _ = self.rv2a(filename, r, w)
|
ap = self.rv2a(filename, r, w)[0]
|
||||||
self.validpath(ap)
|
|
||||||
if w:
|
if w:
|
||||||
try:
|
try:
|
||||||
st = bos.stat(ap)
|
st = bos.stat(ap)
|
||||||
td = time.time() - st.st_mtime
|
td = time.time() - st.st_mtime
|
||||||
need_unlink = True
|
|
||||||
except:
|
except:
|
||||||
need_unlink = False
|
|
||||||
td = 0
|
td = 0
|
||||||
|
|
||||||
if w and need_unlink:
|
if td < -1 or td > self.args.ftp_wt:
|
||||||
if td >= -1 and td <= self.args.ftp_wt:
|
raise FSE("Cannot open existing file for writing")
|
||||||
# within permitted timeframe; unlink and accept
|
|
||||||
do_it = True
|
|
||||||
elif self.args.no_del or self.args.ftp_no_ow:
|
|
||||||
# file too old, or overwrite not allowed; reject
|
|
||||||
do_it = False
|
|
||||||
else:
|
|
||||||
# allow overwrite if user has delete permission
|
|
||||||
# (avoids win2000 freaking out and deleting the server copy without uploading its own)
|
|
||||||
try:
|
|
||||||
self.rv2a(filename, False, True, False, True)
|
|
||||||
do_it = True
|
|
||||||
except:
|
|
||||||
do_it = False
|
|
||||||
|
|
||||||
if not do_it:
|
self.validpath(ap)
|
||||||
raise FSE("File already exists")
|
return open(fsenc(ap), mode, self.args.iobuf)
|
||||||
|
|
||||||
wunlink(self.log, ap, VF_CAREFUL)
|
|
||||||
|
|
||||||
ret = open(fsenc(ap), mode, self.args.iobuf)
|
|
||||||
if w and "fperms" in vfs.flags:
|
|
||||||
set_fperms(ret, vfs.flags)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def chdir(self, path: str) -> None:
|
def chdir(self, path: str) -> None:
|
||||||
nwd = join(self.cwd, path)
|
nwd = join(self.cwd, path)
|
||||||
|
@ -289,12 +232,9 @@ class FtpFs(AbstractedFS):
|
||||||
# returning 550 is library-default and suitable
|
# returning 550 is library-default and suitable
|
||||||
raise FSE("No such file or directory")
|
raise FSE("No such file or directory")
|
||||||
|
|
||||||
if vfs.realpath:
|
avfs = vfs.chk_ap(ap, st)
|
||||||
avfs = vfs.chk_ap(ap, st)
|
if not avfs:
|
||||||
if not avfs:
|
raise FSE("Permission denied", 1)
|
||||||
raise FSE("Permission denied", 1)
|
|
||||||
else:
|
|
||||||
avfs = vfs
|
|
||||||
|
|
||||||
self.cwd = nwd
|
self.cwd = nwd
|
||||||
(
|
(
|
||||||
|
@ -309,8 +249,8 @@ class FtpFs(AbstractedFS):
|
||||||
) = avfs.can_access("", self.h.uname)
|
) = avfs.can_access("", self.h.uname)
|
||||||
|
|
||||||
def mkdir(self, path: str) -> None:
|
def mkdir(self, path: str) -> None:
|
||||||
ap, vfs, _ = self.rv2a(path, w=True)
|
ap = self.rv2a(path, w=True)[0]
|
||||||
bos.makedirs(ap, vf=vfs.flags) # filezilla expects this
|
bos.makedirs(ap) # filezilla expects this
|
||||||
|
|
||||||
def listdir(self, path: str) -> list[str]:
|
def listdir(self, path: str) -> list[str]:
|
||||||
vpath = join(self.cwd, path)
|
vpath = join(self.cwd, path)
|
||||||
|
@ -324,7 +264,6 @@ class FtpFs(AbstractedFS):
|
||||||
self.uname,
|
self.uname,
|
||||||
not self.args.no_scandir,
|
not self.args.no_scandir,
|
||||||
[[True, False], [False, True]],
|
[[True, False], [False, True]],
|
||||||
throw=True,
|
|
||||||
)
|
)
|
||||||
vfs_ls = [x[0] for x in vfs_ls1]
|
vfs_ls = [x[0] for x in vfs_ls1]
|
||||||
vfs_ls.extend(vfs_virt.keys())
|
vfs_ls.extend(vfs_virt.keys())
|
||||||
|
@ -343,20 +282,9 @@ class FtpFs(AbstractedFS):
|
||||||
# display write-only folders as empty
|
# display write-only folders as empty
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# return list of accessible volumes
|
# return list of volumes
|
||||||
ret = []
|
r = {x.split("/")[0]: 1 for x in self.hub.asrv.vfs.all_vols.keys()}
|
||||||
for vn in self.hub.asrv.vfs.all_vols.values():
|
return list(sorted(list(r.keys())))
|
||||||
if "/" in vn.vpath or not vn.vpath:
|
|
||||||
continue # only include toplevel-mounted vols
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.hub.asrv.vfs.get(vn.vpath, self.uname, True, False)
|
|
||||||
ret.append(vn.vpath)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
ret.sort()
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def rmdir(self, path: str) -> None:
|
def rmdir(self, path: str) -> None:
|
||||||
ap = self.rv2a(path, d=True)[0]
|
ap = self.rv2a(path, d=True)[0]
|
||||||
|
@ -386,7 +314,7 @@ class FtpFs(AbstractedFS):
|
||||||
svp = join(self.cwd, src).lstrip("/")
|
svp = join(self.cwd, src).lstrip("/")
|
||||||
dvp = join(self.cwd, dst).lstrip("/")
|
dvp = join(self.cwd, dst).lstrip("/")
|
||||||
try:
|
try:
|
||||||
self.hub.up2k.handle_mv(self.uname, self.h.cli_ip, svp, dvp)
|
self.hub.up2k.handle_mv(self.uname, svp, dvp)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise FSE(str(ex))
|
raise FSE(str(ex))
|
||||||
|
|
||||||
|
@ -409,12 +337,8 @@ class FtpFs(AbstractedFS):
|
||||||
return st
|
return st
|
||||||
|
|
||||||
def utime(self, path: str, timeval: float) -> None:
|
def utime(self, path: str, timeval: float) -> None:
|
||||||
try:
|
ap = self.rv2a(path, w=True)[0]
|
||||||
ap = self.rv2a(path, w=True)[0]
|
return bos.utime(ap, (timeval, timeval))
|
||||||
return bos.utime(ap, (int(time.time()), int(timeval)))
|
|
||||||
except Exception as ex:
|
|
||||||
logging.error("ftp.utime: %s, %r", ex, ex)
|
|
||||||
raise
|
|
||||||
|
|
||||||
def lstat(self, path: str) -> os.stat_result:
|
def lstat(self, path: str) -> os.stat_result:
|
||||||
ap = self.rv2a(path)[0]
|
ap = self.rv2a(path)[0]
|
||||||
|
@ -503,28 +427,20 @@ class FtpHandler(FTPHandler):
|
||||||
def ftp_STOR(self, file: str, mode: str = "w") -> Any:
|
def ftp_STOR(self, file: str, mode: str = "w") -> Any:
|
||||||
# Optional[str]
|
# Optional[str]
|
||||||
vp = join(self.fs.cwd, file).lstrip("/")
|
vp = join(self.fs.cwd, file).lstrip("/")
|
||||||
try:
|
ap, vfs, rem = self.fs.v2a(vp, w=True)
|
||||||
ap, vfs, rem = self.fs.v2a(vp, w=True)
|
|
||||||
except Exception as ex:
|
|
||||||
self.respond("550 %s" % (ex,), logging.info)
|
|
||||||
return
|
|
||||||
self.vfs_map[ap] = vp
|
self.vfs_map[ap] = vp
|
||||||
xbu = vfs.flags.get("xbu")
|
xbu = vfs.flags.get("xbu")
|
||||||
if xbu and not runhook(
|
if xbu and not runhook(
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
self.hub.up2k,
|
|
||||||
"xbu.ftpd",
|
|
||||||
xbu,
|
xbu,
|
||||||
ap,
|
ap,
|
||||||
vp,
|
vfs.canonical(rem),
|
||||||
"",
|
"",
|
||||||
self.uname,
|
self.uname,
|
||||||
self.hub.asrv.vfs.get_perms(vp, self.uname),
|
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
self.cli_ip,
|
self.cli_ip,
|
||||||
time.time(),
|
0,
|
||||||
"",
|
"",
|
||||||
):
|
):
|
||||||
raise FSE("Upload blocked by xbu server config")
|
raise FSE("Upload blocked by xbu server config")
|
||||||
|
@ -627,15 +543,9 @@ class Ftpd(object):
|
||||||
if "::" in ips:
|
if "::" in ips:
|
||||||
ips.append("0.0.0.0")
|
ips.append("0.0.0.0")
|
||||||
|
|
||||||
ips = [x for x in ips if not x.startswith(("unix:", "fd:"))]
|
|
||||||
|
|
||||||
if self.args.ftp4:
|
if self.args.ftp4:
|
||||||
ips = [x for x in ips if ":" not in x]
|
ips = [x for x in ips if ":" not in x]
|
||||||
|
|
||||||
if not ips:
|
|
||||||
lgr.fatal("cannot start ftp-server; no compatible IPs in -i")
|
|
||||||
return
|
|
||||||
|
|
||||||
ips = list(ODict.fromkeys(ips)) # dedup
|
ips = list(ODict.fromkeys(ips)) # dedup
|
||||||
|
|
||||||
ioloop = IOLoop()
|
ioloop = IOLoop()
|
||||||
|
|
3486
copyparty/httpcli.py
3486
copyparty/httpcli.py
File diff suppressed because it is too large
Load diff
|
@ -9,9 +9,6 @@ import threading # typechk
|
||||||
import time
|
import time
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if os.environ.get("PRTY_NO_TLS"):
|
|
||||||
raise Exception()
|
|
||||||
|
|
||||||
HAVE_SSL = True
|
HAVE_SSL = True
|
||||||
import ssl
|
import ssl
|
||||||
except:
|
except:
|
||||||
|
@ -59,8 +56,6 @@ class HttpConn(object):
|
||||||
self.asrv: AuthSrv = hsrv.asrv # mypy404
|
self.asrv: AuthSrv = hsrv.asrv # mypy404
|
||||||
self.u2fh: Util.FHC = hsrv.u2fh # mypy404
|
self.u2fh: Util.FHC = hsrv.u2fh # mypy404
|
||||||
self.pipes: Util.CachedDict = hsrv.pipes # mypy404
|
self.pipes: Util.CachedDict = hsrv.pipes # mypy404
|
||||||
self.ipu_iu: Optional[dict[str, str]] = hsrv.ipu_iu
|
|
||||||
self.ipu_nm: Optional[NetMap] = hsrv.ipu_nm
|
|
||||||
self.ipa_nm: Optional[NetMap] = hsrv.ipa_nm
|
self.ipa_nm: Optional[NetMap] = hsrv.ipa_nm
|
||||||
self.xff_nm: Optional[NetMap] = hsrv.xff_nm
|
self.xff_nm: Optional[NetMap] = hsrv.xff_nm
|
||||||
self.xff_lan: NetMap = hsrv.xff_lan # type: ignore
|
self.xff_lan: NetMap = hsrv.xff_lan # type: ignore
|
||||||
|
@ -105,6 +100,9 @@ class HttpConn(object):
|
||||||
self.log_src = ("%s \033[%dm%d" % (ip, color, self.addr[1])).ljust(26)
|
self.log_src = ("%s \033[%dm%d" % (ip, color, self.addr[1])).ljust(26)
|
||||||
return self.log_src
|
return self.log_src
|
||||||
|
|
||||||
|
def respath(self, res_name: str) -> str:
|
||||||
|
return os.path.join(self.E.mod, "web", res_name)
|
||||||
|
|
||||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||||
self.log_func(self.log_src, msg, c)
|
self.log_func(self.log_src, msg, c)
|
||||||
|
|
||||||
|
@ -164,7 +162,6 @@ class HttpConn(object):
|
||||||
|
|
||||||
self.log_src = self.log_src.replace("[36m", "[35m")
|
self.log_src = self.log_src.replace("[36m", "[35m")
|
||||||
try:
|
try:
|
||||||
assert ssl # type: ignore # !rm
|
|
||||||
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||||
ctx.load_cert_chain(self.args.cert)
|
ctx.load_cert_chain(self.args.cert)
|
||||||
if self.args.ssl_ver:
|
if self.args.ssl_ver:
|
||||||
|
@ -190,7 +187,7 @@ class HttpConn(object):
|
||||||
|
|
||||||
if self.args.ssl_dbg and hasattr(self.s, "shared_ciphers"):
|
if self.args.ssl_dbg and hasattr(self.s, "shared_ciphers"):
|
||||||
ciphers = self.s.shared_ciphers()
|
ciphers = self.s.shared_ciphers()
|
||||||
assert ciphers # !rm
|
assert ciphers
|
||||||
overlap = [str(y[::-1]) for y in ciphers]
|
overlap = [str(y[::-1]) for y in ciphers]
|
||||||
self.log("TLS cipher overlap:" + "\n".join(overlap))
|
self.log("TLS cipher overlap:" + "\n".join(overlap))
|
||||||
for k, v in [
|
for k, v in [
|
||||||
|
@ -224,6 +221,3 @@ class HttpConn(object):
|
||||||
if self.u2idx:
|
if self.u2idx:
|
||||||
self.hsrv.put_u2idx(str(self.addr), self.u2idx)
|
self.hsrv.put_u2idx(str(self.addr), self.u2idx)
|
||||||
self.u2idx = None
|
self.u2idx = None
|
||||||
|
|
||||||
if self.rproxy:
|
|
||||||
self.set_rproxy()
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import hashlib
|
import base64
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -12,7 +12,7 @@ import time
|
||||||
|
|
||||||
import queue
|
import queue
|
||||||
|
|
||||||
from .__init__ import ANYWIN, CORES, EXE, MACOS, PY2, TYPE_CHECKING, EnvParams, unicode
|
from .__init__ import ANYWIN, CORES, EXE, MACOS, TYPE_CHECKING, EnvParams
|
||||||
|
|
||||||
try:
|
try:
|
||||||
MNFE = ModuleNotFoundError
|
MNFE = ModuleNotFoundError
|
||||||
|
@ -67,39 +67,23 @@ from .util import (
|
||||||
Magician,
|
Magician,
|
||||||
Netdev,
|
Netdev,
|
||||||
NetMap,
|
NetMap,
|
||||||
|
absreal,
|
||||||
build_netmap,
|
build_netmap,
|
||||||
has_resource,
|
|
||||||
ipnorm,
|
ipnorm,
|
||||||
load_ipr,
|
|
||||||
load_ipu,
|
|
||||||
load_resource,
|
|
||||||
min_ex,
|
min_ex,
|
||||||
shut_socket,
|
shut_socket,
|
||||||
spack,
|
spack,
|
||||||
start_log_thrs,
|
start_log_thrs,
|
||||||
start_stackmon,
|
start_stackmon,
|
||||||
ub64enc,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .authsrv import VFS
|
|
||||||
from .broker_util import BrokerCli
|
from .broker_util import BrokerCli
|
||||||
from .ssdp import SSDPr
|
from .ssdp import SSDPr
|
||||||
|
|
||||||
if True: # pylint: disable=using-constant-test
|
if True: # pylint: disable=using-constant-test
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
if PY2:
|
|
||||||
range = xrange # type: ignore
|
|
||||||
|
|
||||||
if not hasattr(socket, "AF_UNIX"):
|
|
||||||
setattr(socket, "AF_UNIX", -9001)
|
|
||||||
|
|
||||||
|
|
||||||
def load_jinja2_resource(E: EnvParams, name: str):
|
|
||||||
with load_resource(E, "web/" + name, "r") as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
|
|
||||||
class HttpSrv(object):
|
class HttpSrv(object):
|
||||||
"""
|
"""
|
||||||
|
@ -124,7 +108,6 @@ class HttpSrv(object):
|
||||||
self.nm = NetMap([], [])
|
self.nm = NetMap([], [])
|
||||||
self.ssdp: Optional["SSDPr"] = None
|
self.ssdp: Optional["SSDPr"] = None
|
||||||
self.gpwd = Garda(self.args.ban_pw)
|
self.gpwd = Garda(self.args.ban_pw)
|
||||||
self.gpwc = Garda(self.args.ban_pwc)
|
|
||||||
self.g404 = Garda(self.args.ban_404)
|
self.g404 = Garda(self.args.ban_404)
|
||||||
self.g403 = Garda(self.args.ban_403)
|
self.g403 = Garda(self.args.ban_403)
|
||||||
self.g422 = Garda(self.args.ban_422, False)
|
self.g422 = Garda(self.args.ban_422, False)
|
||||||
|
@ -133,12 +116,6 @@ class HttpSrv(object):
|
||||||
self.bans: dict[str, int] = {}
|
self.bans: dict[str, int] = {}
|
||||||
self.aclose: dict[str, int] = {}
|
self.aclose: dict[str, int] = {}
|
||||||
|
|
||||||
dli: dict[str, tuple[float, int, "VFS", str, str]] = {} # info
|
|
||||||
dls: dict[str, tuple[float, int]] = {} # state
|
|
||||||
self.dli = self.tdli = dli
|
|
||||||
self.dls = self.tdls = dls
|
|
||||||
self.iiam = '<img src="%s.cpr/iiam.gif?cache=i" />' % (self.args.SRS,)
|
|
||||||
|
|
||||||
self.bound: set[tuple[str, int]] = set()
|
self.bound: set[tuple[str, int]] = set()
|
||||||
self.name = "hsrv" + nsuf
|
self.name = "hsrv" + nsuf
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
|
@ -154,7 +131,6 @@ class HttpSrv(object):
|
||||||
self.t_periodic: Optional[threading.Thread] = None
|
self.t_periodic: Optional[threading.Thread] = None
|
||||||
|
|
||||||
self.u2fh = FHC()
|
self.u2fh = FHC()
|
||||||
self.u2sc: dict[str, tuple[int, "hashlib._Hash"]] = {}
|
|
||||||
self.pipes = CachedDict(0.2)
|
self.pipes = CachedDict(0.2)
|
||||||
self.metrics = Metrics(self)
|
self.metrics = Metrics(self)
|
||||||
self.nreq = 0
|
self.nreq = 0
|
||||||
|
@ -170,39 +146,23 @@ class HttpSrv(object):
|
||||||
self.u2idx_free: dict[str, U2idx] = {}
|
self.u2idx_free: dict[str, U2idx] = {}
|
||||||
self.u2idx_n = 0
|
self.u2idx_n = 0
|
||||||
|
|
||||||
assert jinja2 # type: ignore # !rm
|
|
||||||
env = jinja2.Environment()
|
env = jinja2.Environment()
|
||||||
env.loader = jinja2.FunctionLoader(lambda f: load_jinja2_resource(self.E, f))
|
env.loader = jinja2.FileSystemLoader(os.path.join(self.E.mod, "web"))
|
||||||
jn = [
|
jn = ["splash", "svcs", "browser", "browser2", "msg", "md", "mde", "cf"]
|
||||||
"browser",
|
|
||||||
"browser2",
|
|
||||||
"cf",
|
|
||||||
"idp",
|
|
||||||
"md",
|
|
||||||
"mde",
|
|
||||||
"msg",
|
|
||||||
"rups",
|
|
||||||
"shares",
|
|
||||||
"splash",
|
|
||||||
"svcs",
|
|
||||||
]
|
|
||||||
self.j2 = {x: env.get_template(x + ".html") for x in jn}
|
self.j2 = {x: env.get_template(x + ".html") for x in jn}
|
||||||
self.prism = has_resource(self.E, "web/deps/prism.js.gz")
|
zs = os.path.join(self.E.mod, "web", "deps", "prism.js.gz")
|
||||||
|
self.prism = os.path.exists(zs)
|
||||||
if self.args.ipu:
|
|
||||||
self.ipu_iu, self.ipu_nm = load_ipu(self.log, self.args.ipu)
|
|
||||||
else:
|
|
||||||
self.ipu_iu = self.ipu_nm = None
|
|
||||||
|
|
||||||
if self.args.ipr:
|
|
||||||
self.ipr = load_ipr(self.log, self.args.ipr)
|
|
||||||
else:
|
|
||||||
self.ipr = None
|
|
||||||
|
|
||||||
self.ipa_nm = build_netmap(self.args.ipa)
|
self.ipa_nm = build_netmap(self.args.ipa)
|
||||||
self.xff_nm = build_netmap(self.args.xff_src)
|
self.xff_nm = build_netmap(self.args.xff_src)
|
||||||
self.xff_lan = build_netmap("lan")
|
self.xff_lan = build_netmap("lan")
|
||||||
|
|
||||||
|
self.statics: set[str] = set()
|
||||||
|
self._build_statics()
|
||||||
|
|
||||||
|
self.ptn_cc = re.compile(r"[\x00-\x1f]")
|
||||||
|
self.ptn_hsafe = re.compile(r"[\x00-\x1f<>\"'&]")
|
||||||
|
|
||||||
self.mallow = "GET HEAD POST PUT DELETE OPTIONS".split()
|
self.mallow = "GET HEAD POST PUT DELETE OPTIONS".split()
|
||||||
if not self.args.no_dav:
|
if not self.args.no_dav:
|
||||||
zs = "PROPFIND PROPPATCH LOCK UNLOCK MKCOL COPY MOVE"
|
zs = "PROPFIND PROPPATCH LOCK UNLOCK MKCOL COPY MOVE"
|
||||||
|
@ -217,9 +177,6 @@ class HttpSrv(object):
|
||||||
self.start_threads(4)
|
self.start_threads(4)
|
||||||
|
|
||||||
if nid:
|
if nid:
|
||||||
self.tdli = {}
|
|
||||||
self.tdls = {}
|
|
||||||
|
|
||||||
if self.args.stackmon:
|
if self.args.stackmon:
|
||||||
start_stackmon(self.args.stackmon, nid)
|
start_stackmon(self.args.stackmon, nid)
|
||||||
|
|
||||||
|
@ -236,6 +193,14 @@ class HttpSrv(object):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _build_statics(self) -> None:
|
||||||
|
for dp, _, df in os.walk(os.path.join(self.E.mod, "web")):
|
||||||
|
for fn in df:
|
||||||
|
ap = absreal(os.path.join(dp, fn))
|
||||||
|
self.statics.add(ap)
|
||||||
|
if ap.endswith(".gz"):
|
||||||
|
self.statics.add(ap[:-3])
|
||||||
|
|
||||||
def set_netdevs(self, netdevs: dict[str, Netdev]) -> None:
|
def set_netdevs(self, netdevs: dict[str, Netdev]) -> None:
|
||||||
ips = set()
|
ips = set()
|
||||||
for ip, _ in self.bound:
|
for ip, _ in self.bound:
|
||||||
|
@ -256,7 +221,7 @@ class HttpSrv(object):
|
||||||
if self.args.log_htp:
|
if self.args.log_htp:
|
||||||
self.log(self.name, "workers -= {} = {}".format(n, self.tp_nthr), 6)
|
self.log(self.name, "workers -= {} = {}".format(n, self.tp_nthr), 6)
|
||||||
|
|
||||||
assert self.tp_q # !rm
|
assert self.tp_q
|
||||||
for _ in range(n):
|
for _ in range(n):
|
||||||
self.tp_q.put(None)
|
self.tp_q.put(None)
|
||||||
|
|
||||||
|
@ -275,24 +240,15 @@ class HttpSrv(object):
|
||||||
return
|
return
|
||||||
|
|
||||||
def listen(self, sck: socket.socket, nlisteners: int) -> None:
|
def listen(self, sck: socket.socket, nlisteners: int) -> None:
|
||||||
tcp = sck.family != socket.AF_UNIX
|
|
||||||
|
|
||||||
if self.args.j != 1:
|
if self.args.j != 1:
|
||||||
# lost in the pickle; redefine
|
# lost in the pickle; redefine
|
||||||
if not ANYWIN or self.args.reuseaddr:
|
if not ANYWIN or self.args.reuseaddr:
|
||||||
sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
|
||||||
if tcp:
|
sck.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||||
sck.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
||||||
|
|
||||||
sck.settimeout(None) # < does not inherit, ^ opts above do
|
sck.settimeout(None) # < does not inherit, ^ opts above do
|
||||||
|
|
||||||
if tcp:
|
ip, port = sck.getsockname()[:2]
|
||||||
ip, port = sck.getsockname()[:2]
|
|
||||||
else:
|
|
||||||
ip = re.sub(r"\.[0-9]+$", "", sck.getsockname().split("/")[-1])
|
|
||||||
port = 0
|
|
||||||
|
|
||||||
self.srvs.append(sck)
|
self.srvs.append(sck)
|
||||||
self.bound.add((ip, port))
|
self.bound.add((ip, port))
|
||||||
self.nclimax = math.ceil(self.args.nc * 1.0 / nlisteners)
|
self.nclimax = math.ceil(self.args.nc * 1.0 / nlisteners)
|
||||||
|
@ -304,24 +260,16 @@ class HttpSrv(object):
|
||||||
|
|
||||||
def thr_listen(self, srv_sck: socket.socket) -> None:
|
def thr_listen(self, srv_sck: socket.socket) -> None:
|
||||||
"""listens on a shared tcp server"""
|
"""listens on a shared tcp server"""
|
||||||
|
ip, port = srv_sck.getsockname()[:2]
|
||||||
fno = srv_sck.fileno()
|
fno = srv_sck.fileno()
|
||||||
if srv_sck.family == socket.AF_UNIX:
|
hip = "[{}]".format(ip) if ":" in ip else ip
|
||||||
ip = re.sub(r"\.[0-9]+$", "", srv_sck.getsockname())
|
msg = "subscribed @ {}:{} f{} p{}".format(hip, port, fno, os.getpid())
|
||||||
msg = "subscribed @ %s f%d p%d" % (ip, fno, os.getpid())
|
|
||||||
ip = ip.split("/")[-1]
|
|
||||||
port = 0
|
|
||||||
tcp = False
|
|
||||||
else:
|
|
||||||
tcp = True
|
|
||||||
ip, port = srv_sck.getsockname()[:2]
|
|
||||||
hip = "[%s]" % (ip,) if ":" in ip else ip
|
|
||||||
msg = "subscribed @ %s:%d f%d p%d" % (hip, port, fno, os.getpid())
|
|
||||||
|
|
||||||
self.log(self.name, msg)
|
self.log(self.name, msg)
|
||||||
|
|
||||||
Daemon(self.broker.say, "sig-hsrv-up1", ("cb_httpsrv_up",))
|
def fun() -> None:
|
||||||
|
self.broker.say("cb_httpsrv_up")
|
||||||
|
|
||||||
saddr = ("", 0) # fwd-decl for `except TypeError as ex:`
|
threading.Thread(target=fun, name="sig-hsrv-up1").start()
|
||||||
|
|
||||||
while not self.stopping:
|
while not self.stopping:
|
||||||
if self.args.log_conn:
|
if self.args.log_conn:
|
||||||
|
@ -330,8 +278,7 @@ class HttpSrv(object):
|
||||||
spins = 0
|
spins = 0
|
||||||
while self.ncli >= self.nclimax:
|
while self.ncli >= self.nclimax:
|
||||||
if not spins:
|
if not spins:
|
||||||
t = "at connection limit (global-option 'nc'); waiting"
|
self.log(self.name, "at connection limit; waiting", 3)
|
||||||
self.log(self.name, t, 3)
|
|
||||||
|
|
||||||
spins += 1
|
spins += 1
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
@ -391,13 +338,11 @@ class HttpSrv(object):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sck, saddr = srv_sck.accept()
|
sck, saddr = srv_sck.accept()
|
||||||
if tcp:
|
cip, cport = saddr[:2]
|
||||||
cip = unicode(saddr[0])
|
if cip.startswith("::ffff:"):
|
||||||
if cip.startswith("::ffff:"):
|
cip = cip[7:]
|
||||||
cip = cip[7:]
|
|
||||||
addr = (cip, saddr[1])
|
addr = (cip, cport)
|
||||||
else:
|
|
||||||
addr = ("127.8.3.7", sck.fileno())
|
|
||||||
except (OSError, socket.error) as ex:
|
except (OSError, socket.error) as ex:
|
||||||
if self.stopping:
|
if self.stopping:
|
||||||
break
|
break
|
||||||
|
@ -405,19 +350,6 @@ class HttpSrv(object):
|
||||||
self.log(self.name, "accept({}): {}".format(fno, ex), c=6)
|
self.log(self.name, "accept({}): {}".format(fno, ex), c=6)
|
||||||
time.sleep(0.02)
|
time.sleep(0.02)
|
||||||
continue
|
continue
|
||||||
except TypeError as ex:
|
|
||||||
# on macOS, accept() may return a None saddr if blocked by LittleSnitch;
|
|
||||||
# unicode(saddr[0]) ==> TypeError: 'NoneType' object is not subscriptable
|
|
||||||
if tcp and not saddr:
|
|
||||||
t = "accept(%s): failed to accept connection from client due to firewall or network issue"
|
|
||||||
self.log(self.name, t % (fno,), c=3)
|
|
||||||
try:
|
|
||||||
sck.close() # type: ignore
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
time.sleep(0.02)
|
|
||||||
continue
|
|
||||||
raise
|
|
||||||
|
|
||||||
if self.args.log_conn:
|
if self.args.log_conn:
|
||||||
t = "|{}C-acc2 \033[0;36m{} \033[3{}m{}".format(
|
t = "|{}C-acc2 \033[0;36m{} \033[3{}m{}".format(
|
||||||
|
@ -466,7 +398,7 @@ class HttpSrv(object):
|
||||||
)
|
)
|
||||||
|
|
||||||
def thr_poolw(self) -> None:
|
def thr_poolw(self) -> None:
|
||||||
assert self.tp_q # !rm
|
assert self.tp_q
|
||||||
while True:
|
while True:
|
||||||
task = self.tp_q.get()
|
task = self.tp_q.get()
|
||||||
if not task:
|
if not task:
|
||||||
|
@ -578,8 +510,8 @@ class HttpSrv(object):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# spack gives 4 lsb, take 3 lsb, get 4 ch
|
v = base64.urlsafe_b64encode(spack(b">xxL", int(v)))
|
||||||
self.cb_v = ub64enc(spack(b">L", int(v))[1:]).decode("ascii")
|
self.cb_v = v.decode("ascii")[-4:]
|
||||||
self.cb_ts = time.time()
|
self.cb_ts = time.time()
|
||||||
return self.cb_v
|
return self.cb_v
|
||||||
|
|
||||||
|
@ -610,32 +542,3 @@ class HttpSrv(object):
|
||||||
ident += "a"
|
ident += "a"
|
||||||
|
|
||||||
self.u2idx_free[ident] = u2idx
|
self.u2idx_free[ident] = u2idx
|
||||||
|
|
||||||
def read_dls(
|
|
||||||
self,
|
|
||||||
) -> tuple[
|
|
||||||
dict[str, tuple[float, int, str, str, str]], dict[str, tuple[float, int]]
|
|
||||||
]:
|
|
||||||
"""
|
|
||||||
mp-broker asking for local dl-info + dl-state;
|
|
||||||
reduce overhead by sending just the vfs vpath
|
|
||||||
"""
|
|
||||||
dli = {k: (a, b, c.vpath, d, e) for k, (a, b, c, d, e) in self.dli.items()}
|
|
||||||
return (dli, self.dls)
|
|
||||||
|
|
||||||
def write_dls(
|
|
||||||
self,
|
|
||||||
sdli: dict[str, tuple[float, int, str, str, str]],
|
|
||||||
dls: dict[str, tuple[float, int]],
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
mp-broker pushing total dl-info + dl-state;
|
|
||||||
swap out the vfs vpath with the vfs node
|
|
||||||
"""
|
|
||||||
dli: dict[str, tuple[float, int, "VFS", str, str]] = {}
|
|
||||||
for k, (a, b, c, d, e) in sdli.items():
|
|
||||||
vn = self.asrv.vfs.all_nodes[c]
|
|
||||||
dli[k] = (a, b, vn, d, e)
|
|
||||||
|
|
||||||
self.tdli = dli
|
|
||||||
self.tdls = dls
|
|
||||||
|
|
|
@ -74,7 +74,7 @@ class Ico(object):
|
||||||
try:
|
try:
|
||||||
_, _, tw, th = pb.textbbox((0, 0), ext)
|
_, _, tw, th = pb.textbbox((0, 0), ext)
|
||||||
except:
|
except:
|
||||||
tw, th = pb.textsize(ext) # type: ignore
|
tw, th = pb.textsize(ext)
|
||||||
|
|
||||||
tw += len(ext)
|
tw += len(ext)
|
||||||
cw = tw // len(ext)
|
cw = tw // len(ext)
|
||||||
|
@ -94,21 +94,10 @@ class Ico(object):
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg version="1.1" viewBox="0 0 100 {}" xmlns="http://www.w3.org/2000/svg"><g>
|
<svg version="1.1" viewBox="0 0 100 {}" xmlns="http://www.w3.org/2000/svg"><g>
|
||||||
<rect width="100%" height="100%" fill="#{}" />
|
<rect width="100%" height="100%" fill="#{}" />
|
||||||
<text x="50%" y="{}" dominant-baseline="middle" text-anchor="middle" xml:space="preserve"
|
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" xml:space="preserve"
|
||||||
fill="#{}" font-family="monospace" font-size="14px" style="letter-spacing:.5px">{}</text>
|
fill="#{}" font-family="monospace" font-size="14px" style="letter-spacing:.5px">{}</text>
|
||||||
</g></svg>
|
</g></svg>
|
||||||
"""
|
"""
|
||||||
|
svg = svg.format(h, c[:6], c[6:], html_escape(ext, True))
|
||||||
txt = html_escape(ext, True)
|
|
||||||
if "\n" in txt:
|
|
||||||
lines = txt.split("\n")
|
|
||||||
n = len(lines)
|
|
||||||
y = "20%" if n == 2 else "10%" if n == 3 else "0"
|
|
||||||
zs = '<tspan x="50%%" dy="1.2em">%s</tspan>'
|
|
||||||
txt = "".join([zs % (x,) for x in lines])
|
|
||||||
else:
|
|
||||||
y = "50%"
|
|
||||||
|
|
||||||
svg = svg.format(h, c[:6], y, c[6:], txt)
|
|
||||||
|
|
||||||
return "image/svg+xml", svg.encode("utf-8")
|
return "image/svg+xml", svg.encode("utf-8")
|
||||||
|
|
|
@ -25,7 +25,6 @@ from .stolen.dnslib import (
|
||||||
DNSHeader,
|
DNSHeader,
|
||||||
DNSQuestion,
|
DNSQuestion,
|
||||||
DNSRecord,
|
DNSRecord,
|
||||||
set_avahi_379,
|
|
||||||
)
|
)
|
||||||
from .util import CachedSet, Daemon, Netdev, list_ips, min_ex
|
from .util import CachedSet, Daemon, Netdev, list_ips, min_ex
|
||||||
|
|
||||||
|
@ -73,11 +72,7 @@ class MDNS(MCast):
|
||||||
self.ngen = ngen
|
self.ngen = ngen
|
||||||
self.ttl = 300
|
self.ttl = 300
|
||||||
|
|
||||||
if not self.args.zm_nwa_1:
|
zs = self.args.name + ".local."
|
||||||
set_avahi_379()
|
|
||||||
|
|
||||||
zs = self.args.zm_fqdn or (self.args.name + ".local")
|
|
||||||
zs = zs.replace("--name", self.args.name).rstrip(".") + "."
|
|
||||||
zs = zs.encode("ascii", "replace").decode("ascii", "replace")
|
zs = zs.encode("ascii", "replace").decode("ascii", "replace")
|
||||||
self.hn = "-".join(x for x in zs.split("?") if x) or (
|
self.hn = "-".join(x for x in zs.split("?") if x) or (
|
||||||
"vault-{}".format(random.randint(1, 255))
|
"vault-{}".format(random.randint(1, 255))
|
||||||
|
@ -297,22 +292,6 @@ class MDNS(MCast):
|
||||||
def run2(self) -> None:
|
def run2(self) -> None:
|
||||||
last_hop = time.time()
|
last_hop = time.time()
|
||||||
ihop = self.args.mc_hop
|
ihop = self.args.mc_hop
|
||||||
|
|
||||||
try:
|
|
||||||
if self.args.no_poll:
|
|
||||||
raise Exception()
|
|
||||||
fd2sck = {}
|
|
||||||
srvpoll = select.poll()
|
|
||||||
for sck in self.srv:
|
|
||||||
fd = sck.fileno()
|
|
||||||
fd2sck[fd] = sck
|
|
||||||
srvpoll.register(fd, select.POLLIN)
|
|
||||||
except Exception as ex:
|
|
||||||
srvpoll = None
|
|
||||||
if not self.args.no_poll:
|
|
||||||
t = "WARNING: failed to poll(), will use select() instead: %r"
|
|
||||||
self.log(t % (ex,), 3)
|
|
||||||
|
|
||||||
while self.running:
|
while self.running:
|
||||||
timeout = (
|
timeout = (
|
||||||
0.02 + random.random() * 0.07
|
0.02 + random.random() * 0.07
|
||||||
|
@ -321,13 +300,8 @@ class MDNS(MCast):
|
||||||
if self.unsolicited
|
if self.unsolicited
|
||||||
else (last_hop + ihop if ihop else 180)
|
else (last_hop + ihop if ihop else 180)
|
||||||
)
|
)
|
||||||
if srvpoll:
|
rdy = select.select(self.srv, [], [], timeout)
|
||||||
pr = srvpoll.poll(timeout * 1000)
|
rx: list[socket.socket] = rdy[0] # type: ignore
|
||||||
rx = [fd2sck[x[0]] for x in pr if x[1] & select.POLLIN]
|
|
||||||
else:
|
|
||||||
rdy = select.select(self.srv, [], [], timeout)
|
|
||||||
rx: list[socket.socket] = rdy[0] # type: ignore
|
|
||||||
|
|
||||||
self.rx4.cln()
|
self.rx4.cln()
|
||||||
self.rx6.cln()
|
self.rx6.cln()
|
||||||
buf = b""
|
buf = b""
|
||||||
|
@ -341,9 +315,6 @@ class MDNS(MCast):
|
||||||
self.log("stopped", 2)
|
self.log("stopped", 2)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.args.zm_no_pe:
|
|
||||||
continue
|
|
||||||
|
|
||||||
t = "{} {} \033[33m|{}| {}\n{}".format(
|
t = "{} {} \033[33m|{}| {}\n{}".format(
|
||||||
self.srv[sck].name, addr, len(buf), repr(buf)[2:-1], min_ex()
|
self.srv[sck].name, addr, len(buf), repr(buf)[2:-1], min_ex()
|
||||||
)
|
)
|
||||||
|
@ -369,7 +340,7 @@ class MDNS(MCast):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.srv.clear()
|
self.srv = {}
|
||||||
|
|
||||||
def eat(self, buf: bytes, addr: tuple[str, int], sck: socket.socket) -> None:
|
def eat(self, buf: bytes, addr: tuple[str, int], sck: socket.socket) -> None:
|
||||||
cip = addr[0]
|
cip = addr[0]
|
||||||
|
|
|
@ -18,7 +18,7 @@ class Metrics(object):
|
||||||
|
|
||||||
def tx(self, cli: "HttpCli") -> bool:
|
def tx(self, cli: "HttpCli") -> bool:
|
||||||
if not cli.avol:
|
if not cli.avol:
|
||||||
raise Pebkac(403, "'stats' not allowed for user " + cli.uname)
|
raise Pebkac(403, "not allowed for user " + cli.uname)
|
||||||
|
|
||||||
args = cli.args
|
args = cli.args
|
||||||
if not args.stats:
|
if not args.stats:
|
||||||
|
@ -72,9 +72,6 @@ class Metrics(object):
|
||||||
v = "{:.3f}".format(self.hsrv.t0)
|
v = "{:.3f}".format(self.hsrv.t0)
|
||||||
addug("cpp_boot_unixtime", "seconds", v, t)
|
addug("cpp_boot_unixtime", "seconds", v, t)
|
||||||
|
|
||||||
t = "number of active downloads"
|
|
||||||
addg("cpp_active_dl", str(len(self.hsrv.tdls)), t)
|
|
||||||
|
|
||||||
t = "number of open http(s) client connections"
|
t = "number of open http(s) client connections"
|
||||||
addg("cpp_http_conns", str(self.hsrv.ncli), t)
|
addg("cpp_http_conns", str(self.hsrv.ncli), t)
|
||||||
|
|
||||||
|
@ -91,7 +88,7 @@ class Metrics(object):
|
||||||
addg("cpp_total_bans", str(self.hsrv.nban), t)
|
addg("cpp_total_bans", str(self.hsrv.nban), t)
|
||||||
|
|
||||||
if not args.nos_vst:
|
if not args.nos_vst:
|
||||||
x = self.hsrv.broker.ask("up2k.get_state", True, "")
|
x = self.hsrv.broker.ask("up2k.get_state")
|
||||||
vs = json.loads(x.get())
|
vs = json.loads(x.get())
|
||||||
|
|
||||||
nvidle = 0
|
nvidle = 0
|
||||||
|
@ -131,7 +128,7 @@ class Metrics(object):
|
||||||
addbh("cpp_disk_size_bytes", "total HDD size of volume")
|
addbh("cpp_disk_size_bytes", "total HDD size of volume")
|
||||||
addbh("cpp_disk_free_bytes", "free HDD space in volume")
|
addbh("cpp_disk_free_bytes", "free HDD space in volume")
|
||||||
for vpath, vol in allvols:
|
for vpath, vol in allvols:
|
||||||
free, total, _ = get_df(vol.realpath, False)
|
free, total = get_df(vol.realpath)
|
||||||
if free is None or total is None:
|
if free is None or total is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
|
@ -4,45 +4,28 @@ from __future__ import print_function, unicode_literals
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
|
|
||||||
from .__init__ import ANYWIN, EXE, PY2, WINDOWS, E, unicode
|
from .__init__ import ANYWIN, EXE, PY2, WINDOWS, E, unicode
|
||||||
from .authsrv import VFS
|
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
from .util import (
|
from .util import (
|
||||||
FFMPEG_URL,
|
FFMPEG_URL,
|
||||||
REKOBO_LKEY,
|
REKOBO_LKEY,
|
||||||
VF_CAREFUL,
|
|
||||||
fsenc,
|
fsenc,
|
||||||
gzip,
|
|
||||||
min_ex,
|
min_ex,
|
||||||
pybin,
|
pybin,
|
||||||
retchk,
|
retchk,
|
||||||
runcmd,
|
runcmd,
|
||||||
sfsenc,
|
sfsenc,
|
||||||
uncyg,
|
uncyg,
|
||||||
wunlink,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if True: # pylint: disable=using-constant-test
|
if True: # pylint: disable=using-constant-test
|
||||||
from typing import IO, Any, Optional, Union
|
from typing import Any, Union
|
||||||
|
|
||||||
from .util import NamedLogger, RootLogger
|
from .util import RootLogger
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
if os.environ.get("PRTY_NO_MUTAGEN"):
|
|
||||||
raise Exception()
|
|
||||||
|
|
||||||
from mutagen import version # noqa: F401
|
|
||||||
|
|
||||||
HAVE_MUTAGEN = True
|
|
||||||
except:
|
|
||||||
HAVE_MUTAGEN = False
|
|
||||||
|
|
||||||
|
|
||||||
def have_ff(scmd: str) -> bool:
|
def have_ff(scmd: str) -> bool:
|
||||||
|
@ -61,13 +44,8 @@ def have_ff(scmd: str) -> bool:
|
||||||
return bool(shutil.which(scmd))
|
return bool(shutil.which(scmd))
|
||||||
|
|
||||||
|
|
||||||
HAVE_FFMPEG = not os.environ.get("PRTY_NO_FFMPEG") and have_ff("ffmpeg")
|
HAVE_FFMPEG = have_ff("ffmpeg")
|
||||||
HAVE_FFPROBE = not os.environ.get("PRTY_NO_FFPROBE") and have_ff("ffprobe")
|
HAVE_FFPROBE = have_ff("ffprobe")
|
||||||
|
|
||||||
CBZ_PICS = set("png jpg jpeg gif bmp tga tif tiff webp avif".split())
|
|
||||||
CBZ_01 = re.compile(r"(^|[^0-9v])0+[01]\b")
|
|
||||||
|
|
||||||
FMT_AU = set("mp3 ogg flac wav".split())
|
|
||||||
|
|
||||||
|
|
||||||
class MParser(object):
|
class MParser(object):
|
||||||
|
@ -129,86 +107,9 @@ class MParser(object):
|
||||||
raise Exception()
|
raise Exception()
|
||||||
|
|
||||||
|
|
||||||
def au_unpk(
|
|
||||||
log: "NamedLogger", fmt_map: dict[str, str], abspath: str, vn: Optional[VFS] = None
|
|
||||||
) -> str:
|
|
||||||
ret = ""
|
|
||||||
maxsz = 1024 * 1024 * 64
|
|
||||||
try:
|
|
||||||
ext = abspath.split(".")[-1].lower()
|
|
||||||
au, pk = fmt_map[ext].split(".")
|
|
||||||
|
|
||||||
fd, ret = tempfile.mkstemp("." + au)
|
|
||||||
|
|
||||||
if pk == "gz":
|
|
||||||
fi = gzip.GzipFile(abspath, mode="rb")
|
|
||||||
|
|
||||||
elif pk == "xz":
|
|
||||||
import lzma
|
|
||||||
|
|
||||||
fi = lzma.open(abspath, "rb")
|
|
||||||
|
|
||||||
elif pk == "zip":
|
|
||||||
import zipfile
|
|
||||||
|
|
||||||
zf = zipfile.ZipFile(abspath, "r")
|
|
||||||
zil = zf.infolist()
|
|
||||||
zil = [x for x in zil if x.filename.lower().split(".")[-1] == au]
|
|
||||||
if not zil:
|
|
||||||
raise Exception("no audio inside zip")
|
|
||||||
fi = zf.open(zil[0])
|
|
||||||
|
|
||||||
elif pk == "cbz":
|
|
||||||
import zipfile
|
|
||||||
|
|
||||||
zf = zipfile.ZipFile(abspath, "r")
|
|
||||||
znil = [(x.filename.lower(), x) for x in zf.infolist()]
|
|
||||||
nf = len(znil)
|
|
||||||
znil = [x for x in znil if x[0].split(".")[-1] in CBZ_PICS]
|
|
||||||
znil = [x for x in znil if "cover" in x[0]] or znil
|
|
||||||
znil = [x for x in znil if CBZ_01.search(x[0])] or znil
|
|
||||||
t = "cbz: %d files, %d hits" % (nf, len(znil))
|
|
||||||
using = sorted(znil)[0][1].filename
|
|
||||||
if znil:
|
|
||||||
t += ", using " + using
|
|
||||||
log(t)
|
|
||||||
if not znil:
|
|
||||||
raise Exception("no images inside cbz")
|
|
||||||
fi = zf.open(using)
|
|
||||||
|
|
||||||
elif pk == "epub":
|
|
||||||
fi = get_cover_from_epub(log, abspath)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise Exception("unknown compression %s" % (pk,))
|
|
||||||
|
|
||||||
fsz = 0
|
|
||||||
with os.fdopen(fd, "wb") as fo:
|
|
||||||
while True:
|
|
||||||
buf = fi.read(32768)
|
|
||||||
if not buf:
|
|
||||||
break
|
|
||||||
|
|
||||||
fsz += len(buf)
|
|
||||||
if fsz > maxsz:
|
|
||||||
raise Exception("zipbomb defused")
|
|
||||||
|
|
||||||
fo.write(buf)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
except Exception as ex:
|
|
||||||
if ret:
|
|
||||||
t = "failed to decompress audio file %r: %r"
|
|
||||||
log(t % (abspath, ex))
|
|
||||||
wunlink(log, ret, vn.flags if vn else VF_CAREFUL)
|
|
||||||
|
|
||||||
return abspath
|
|
||||||
|
|
||||||
|
|
||||||
def ffprobe(
|
def ffprobe(
|
||||||
abspath: str, timeout: int = 60
|
abspath: str, timeout: int = 60
|
||||||
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:
|
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]:
|
||||||
cmd = [
|
cmd = [
|
||||||
b"ffprobe",
|
b"ffprobe",
|
||||||
b"-hide_banner",
|
b"-hide_banner",
|
||||||
|
@ -222,17 +123,8 @@ def ffprobe(
|
||||||
return parse_ffprobe(so)
|
return parse_ffprobe(so)
|
||||||
|
|
||||||
|
|
||||||
def parse_ffprobe(
|
def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]:
|
||||||
txt: str,
|
"""ffprobe -show_format -show_streams"""
|
||||||
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
txt: output from ffprobe -show_format -show_streams
|
|
||||||
returns:
|
|
||||||
* normalized tags
|
|
||||||
* original/raw tags
|
|
||||||
* list of streams
|
|
||||||
* format props
|
|
||||||
"""
|
|
||||||
streams = []
|
streams = []
|
||||||
fmt = {}
|
fmt = {}
|
||||||
g = {}
|
g = {}
|
||||||
|
@ -256,7 +148,7 @@ def parse_ffprobe(
|
||||||
ret: dict[str, Any] = {} # processed
|
ret: dict[str, Any] = {} # processed
|
||||||
md: dict[str, list[Any]] = {} # raw tags
|
md: dict[str, list[Any]] = {} # raw tags
|
||||||
|
|
||||||
is_audio = fmt.get("format_name") in FMT_AU
|
is_audio = fmt.get("format_name") in ["mp3", "ogg", "flac", "wav"]
|
||||||
if fmt.get("filename", "").split(".")[-1].lower() in ["m4a", "aac"]:
|
if fmt.get("filename", "").split(".")[-1].lower() in ["m4a", "aac"]:
|
||||||
is_audio = True
|
is_audio = True
|
||||||
|
|
||||||
|
@ -284,8 +176,6 @@ def parse_ffprobe(
|
||||||
["channel_layout", "chs"],
|
["channel_layout", "chs"],
|
||||||
["sample_rate", ".hz"],
|
["sample_rate", ".hz"],
|
||||||
["bit_rate", ".aq"],
|
["bit_rate", ".aq"],
|
||||||
["bits_per_sample", ".bps"],
|
|
||||||
["bits_per_raw_sample", ".bprs"],
|
|
||||||
["duration", ".dur"],
|
["duration", ".dur"],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -325,7 +215,7 @@ def parse_ffprobe(
|
||||||
ret[rk] = v1
|
ret[rk] = v1
|
||||||
|
|
||||||
if ret.get("vc") == "ansi": # shellscript
|
if ret.get("vc") == "ansi": # shellscript
|
||||||
return {}, {}, [], {}
|
return {}, {}
|
||||||
|
|
||||||
for strm in streams:
|
for strm in streams:
|
||||||
for sk, sv in strm.items():
|
for sk, sv in strm.items():
|
||||||
|
@ -374,77 +264,7 @@ def parse_ffprobe(
|
||||||
zero = int("0")
|
zero = int("0")
|
||||||
zd = {k: (zero, v) for k, v in ret.items()}
|
zd = {k: (zero, v) for k, v in ret.items()}
|
||||||
|
|
||||||
return zd, md, streams, fmt
|
return zd, md
|
||||||
|
|
||||||
|
|
||||||
def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]:
|
|
||||||
import zipfile
|
|
||||||
|
|
||||||
from .dxml import parse_xml
|
|
||||||
|
|
||||||
try:
|
|
||||||
from urlparse import urljoin # Python2
|
|
||||||
except ImportError:
|
|
||||||
from urllib.parse import urljoin # Python3
|
|
||||||
|
|
||||||
with zipfile.ZipFile(abspath, "r") as z:
|
|
||||||
# First open the container file to find the package document (.opf file)
|
|
||||||
try:
|
|
||||||
container_root = parse_xml(z.read("META-INF/container.xml").decode())
|
|
||||||
except KeyError:
|
|
||||||
log("epub: no container file found in %s" % (abspath,))
|
|
||||||
return None
|
|
||||||
|
|
||||||
# https://www.w3.org/TR/epub-33/#sec-container.xml-rootfile-elem
|
|
||||||
container_ns = {"": "urn:oasis:names:tc:opendocument:xmlns:container"}
|
|
||||||
# One file could contain multiple package documents, default to the first one
|
|
||||||
rootfile_path = container_root.find("./rootfiles/rootfile", container_ns).get(
|
|
||||||
"full-path"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Then open the first package document to find the path of the cover image
|
|
||||||
try:
|
|
||||||
package_root = parse_xml(z.read(rootfile_path).decode())
|
|
||||||
except KeyError:
|
|
||||||
log("epub: no package document found in %s" % (abspath,))
|
|
||||||
return None
|
|
||||||
|
|
||||||
# https://www.w3.org/TR/epub-33/#sec-package-doc
|
|
||||||
package_ns = {"": "http://www.idpf.org/2007/opf"}
|
|
||||||
# https://www.w3.org/TR/epub-33/#sec-cover-image
|
|
||||||
coverimage_path_node = package_root.find(
|
|
||||||
"./manifest/item[@properties='cover-image']", package_ns
|
|
||||||
)
|
|
||||||
if coverimage_path_node is not None:
|
|
||||||
coverimage_path = coverimage_path_node.get("href")
|
|
||||||
else:
|
|
||||||
# This might be an EPUB2 file, try the legacy way of specifying covers
|
|
||||||
coverimage_path = _get_cover_from_epub2(log, package_root, package_ns)
|
|
||||||
|
|
||||||
# This url is either absolute (in the .epub) or relative to the package document
|
|
||||||
adjusted_cover_path = urljoin(rootfile_path, coverimage_path)
|
|
||||||
|
|
||||||
return z.open(adjusted_cover_path)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_cover_from_epub2(
|
|
||||||
log: "NamedLogger", package_root, package_ns
|
|
||||||
) -> Optional[str]:
|
|
||||||
# <meta name="cover" content="id-to-cover-image"> in <metadata>, then
|
|
||||||
# <item> in <manifest>
|
|
||||||
cover_id = package_root.find("./metadata/meta[@name='cover']", package_ns).get(
|
|
||||||
"content"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not cover_id:
|
|
||||||
return None
|
|
||||||
|
|
||||||
for node in package_root.iterfind("./manifest/item", package_ns):
|
|
||||||
if node.get("id") == cover_id:
|
|
||||||
cover_path = node.get("href")
|
|
||||||
return cover_path
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class MTag(object):
|
class MTag(object):
|
||||||
|
@ -461,14 +281,16 @@ class MTag(object):
|
||||||
or_ffprobe = " or FFprobe"
|
or_ffprobe = " or FFprobe"
|
||||||
|
|
||||||
if self.backend == "mutagen":
|
if self.backend == "mutagen":
|
||||||
self._get = self.get_mutagen
|
self.get = self.get_mutagen
|
||||||
if not HAVE_MUTAGEN:
|
try:
|
||||||
|
from mutagen import version # noqa: F401
|
||||||
|
except:
|
||||||
self.log("could not load Mutagen, trying FFprobe instead", c=3)
|
self.log("could not load Mutagen, trying FFprobe instead", c=3)
|
||||||
self.backend = "ffprobe"
|
self.backend = "ffprobe"
|
||||||
|
|
||||||
if self.backend == "ffprobe":
|
if self.backend == "ffprobe":
|
||||||
self.usable = self.can_ffprobe
|
self.usable = self.can_ffprobe
|
||||||
self._get = self.get_ffprobe
|
self.get = self.get_ffprobe
|
||||||
self.prefer_mt = True
|
self.prefer_mt = True
|
||||||
|
|
||||||
if not HAVE_FFPROBE:
|
if not HAVE_FFPROBE:
|
||||||
|
@ -588,7 +410,7 @@ class MTag(object):
|
||||||
sv = str(zv).split("/")[0].strip().lstrip("0")
|
sv = str(zv).split("/")[0].strip().lstrip("0")
|
||||||
ret[sk] = sv or 0
|
ret[sk] = sv or 0
|
||||||
|
|
||||||
# normalize key notation to rekobo
|
# normalize key notation to rkeobo
|
||||||
okey = ret.get("key")
|
okey = ret.get("key")
|
||||||
if okey:
|
if okey:
|
||||||
key = str(okey).replace(" ", "").replace("maj", "").replace("min", "m")
|
key = str(okey).replace(" ", "").replace("maj", "").replace("min", "m")
|
||||||
|
@ -638,17 +460,6 @@ class MTag(object):
|
||||||
|
|
||||||
return r1
|
return r1
|
||||||
|
|
||||||
def get(self, abspath: str) -> dict[str, Union[str, float]]:
|
|
||||||
ext = abspath.split(".")[-1].lower()
|
|
||||||
if ext not in self.args.au_unpk:
|
|
||||||
return self._get(abspath)
|
|
||||||
|
|
||||||
ap = au_unpk(self.log, self.args.au_unpk, abspath)
|
|
||||||
ret = self._get(ap)
|
|
||||||
if ap != abspath:
|
|
||||||
wunlink(self.log, ap, VF_CAREFUL)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def get_mutagen(self, abspath: str) -> dict[str, Union[str, float]]:
|
def get_mutagen(self, abspath: str) -> dict[str, Union[str, float]]:
|
||||||
ret: dict[str, tuple[int, Any]] = {}
|
ret: dict[str, tuple[int, Any]] = {}
|
||||||
|
|
||||||
|
@ -668,7 +479,7 @@ class MTag(object):
|
||||||
raise Exception()
|
raise Exception()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
if self.args.mtag_v:
|
if self.args.mtag_v:
|
||||||
self.log("mutagen-err [%s] @ %r" % (ex, abspath), "90")
|
self.log("mutagen-err [{}] @ [{}]".format(ex, abspath), "90")
|
||||||
|
|
||||||
return self.get_ffprobe(abspath) if self.can_ffprobe else {}
|
return self.get_ffprobe(abspath) if self.can_ffprobe else {}
|
||||||
|
|
||||||
|
@ -702,7 +513,7 @@ class MTag(object):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if k == ".aq":
|
if k == ".aq":
|
||||||
v /= 1000 # type: ignore
|
v /= 1000
|
||||||
|
|
||||||
if k == "ac" and v.startswith("mp4a.40."):
|
if k == "ac" and v.startswith("mp4a.40."):
|
||||||
v = "aac"
|
v = "aac"
|
||||||
|
@ -715,7 +526,7 @@ class MTag(object):
|
||||||
if not bos.path.isfile(abspath):
|
if not bos.path.isfile(abspath):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
ret, md, _, _ = ffprobe(abspath, self.args.mtag_to)
|
ret, md = ffprobe(abspath, self.args.mtag_to)
|
||||||
|
|
||||||
if self.args.mtag_vv:
|
if self.args.mtag_vv:
|
||||||
for zd in (ret, dict(md)):
|
for zd in (ret, dict(md)):
|
||||||
|
@ -742,16 +553,10 @@ class MTag(object):
|
||||||
except:
|
except:
|
||||||
raise # might be expected outside cpython
|
raise # might be expected outside cpython
|
||||||
|
|
||||||
ext = abspath.split(".")[-1].lower()
|
|
||||||
if ext in self.args.au_unpk:
|
|
||||||
ap = au_unpk(self.log, self.args.au_unpk, abspath)
|
|
||||||
else:
|
|
||||||
ap = abspath
|
|
||||||
|
|
||||||
ret: dict[str, Any] = {}
|
ret: dict[str, Any] = {}
|
||||||
for tagname, parser in sorted(parsers.items(), key=lambda x: (x[1].pri, x[0])):
|
for tagname, parser in sorted(parsers.items(), key=lambda x: (x[1].pri, x[0])):
|
||||||
try:
|
try:
|
||||||
cmd = [parser.bin, ap]
|
cmd = [parser.bin, abspath]
|
||||||
if parser.bin.endswith(".py"):
|
if parser.bin.endswith(".py"):
|
||||||
cmd = [pybin] + cmd
|
cmd = [pybin] + cmd
|
||||||
|
|
||||||
|
@ -785,10 +590,7 @@ class MTag(object):
|
||||||
ret[tag] = zj[tag]
|
ret[tag] = zj[tag]
|
||||||
except:
|
except:
|
||||||
if self.args.mtag_v:
|
if self.args.mtag_v:
|
||||||
t = "mtag error: tagname %r, parser %r, file %r => %r"
|
t = "mtag error: tagname {}, parser {}, file {} => {}"
|
||||||
self.log(t % (tagname, parser.bin, abspath, min_ex()), 6)
|
self.log(t.format(tagname, parser.bin, abspath, min_ex()))
|
||||||
|
|
||||||
if ap != abspath:
|
|
||||||
wunlink(self.log, ap, VF_CAREFUL)
|
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
|
@ -163,7 +163,6 @@ class MCast(object):
|
||||||
sck.settimeout(None)
|
sck.settimeout(None)
|
||||||
sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
try:
|
try:
|
||||||
# safe for this purpose; https://lwn.net/Articles/853637/
|
|
||||||
sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
@ -183,7 +182,11 @@ class MCast(object):
|
||||||
srv.ips[oth_ip.split("/")[0]] = ipaddress.ip_network(oth_ip, False)
|
srv.ips[oth_ip.split("/")[0]] = ipaddress.ip_network(oth_ip, False)
|
||||||
|
|
||||||
# gvfs breaks if a linklocal ip appears in a dns reply
|
# gvfs breaks if a linklocal ip appears in a dns reply
|
||||||
ll = {k: v for k, v in srv.ips.items() if k.startswith(("169.254", "fe80"))}
|
ll = {
|
||||||
|
k: v
|
||||||
|
for k, v in srv.ips.items()
|
||||||
|
if k.startswith("169.254") or k.startswith("fe80")
|
||||||
|
}
|
||||||
rt = {k: v for k, v in srv.ips.items() if k not in ll}
|
rt = {k: v for k, v in srv.ips.items() if k not in ll}
|
||||||
|
|
||||||
if self.args.ll or not rt:
|
if self.args.ll or not rt:
|
||||||
|
|
|
@ -4,33 +4,27 @@ from __future__ import print_function, unicode_literals
|
||||||
import argparse
|
import argparse
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from .__init__ import unicode
|
from .__init__ import unicode
|
||||||
|
|
||||||
try:
|
|
||||||
if os.environ.get("PRTY_NO_ARGON2"):
|
|
||||||
raise Exception()
|
|
||||||
|
|
||||||
HAVE_ARGON2 = True
|
|
||||||
from argon2 import exceptions as argon2ex
|
|
||||||
except:
|
|
||||||
HAVE_ARGON2 = False
|
|
||||||
|
|
||||||
|
|
||||||
class PWHash(object):
|
class PWHash(object):
|
||||||
def __init__(self, args: argparse.Namespace):
|
def __init__(self, args: argparse.Namespace):
|
||||||
self.args = args
|
self.args = args
|
||||||
|
|
||||||
zsl = args.ah_alg.split(",")
|
try:
|
||||||
alg = zsl[0]
|
alg, ac = args.ah_alg.split(",")
|
||||||
|
except:
|
||||||
|
alg = args.ah_alg
|
||||||
|
ac = {}
|
||||||
|
|
||||||
if alg == "none":
|
if alg == "none":
|
||||||
alg = ""
|
alg = ""
|
||||||
|
|
||||||
self.alg = alg
|
self.alg = alg
|
||||||
self.ac = zsl[1:]
|
self.ac = ac
|
||||||
if not alg:
|
if not alg:
|
||||||
self.on = False
|
self.on = False
|
||||||
self.hash = unicode
|
self.hash = unicode
|
||||||
|
@ -86,23 +80,17 @@ class PWHash(object):
|
||||||
its = 2
|
its = 2
|
||||||
blksz = 8
|
blksz = 8
|
||||||
para = 4
|
para = 4
|
||||||
ramcap = 0 # openssl 1.1 = 32 MiB
|
|
||||||
try:
|
try:
|
||||||
cost = 2 << int(self.ac[0])
|
cost = 2 << int(self.ac[0])
|
||||||
its = int(self.ac[1])
|
its = int(self.ac[1])
|
||||||
blksz = int(self.ac[2])
|
blksz = int(self.ac[2])
|
||||||
para = int(self.ac[3])
|
para = int(self.ac[3])
|
||||||
ramcap = int(self.ac[4]) * 1024 * 1024
|
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
cfg = {"salt": self.salt, "n": cost, "r": blksz, "p": para, "dklen": 24}
|
|
||||||
if ramcap:
|
|
||||||
cfg["maxmem"] = ramcap
|
|
||||||
|
|
||||||
ret = plain.encode("utf-8")
|
ret = plain.encode("utf-8")
|
||||||
for _ in range(its):
|
for _ in range(its):
|
||||||
ret = hashlib.scrypt(ret, **cfg)
|
ret = hashlib.scrypt(ret, salt=self.salt, n=cost, r=blksz, p=para, dklen=24)
|
||||||
|
|
||||||
return "+" + base64.urlsafe_b64encode(ret).decode("utf-8")
|
return "+" + base64.urlsafe_b64encode(ret).decode("utf-8")
|
||||||
|
|
||||||
|
@ -147,10 +135,6 @@ class PWHash(object):
|
||||||
def cli(self) -> None:
|
def cli(self) -> None:
|
||||||
import getpass
|
import getpass
|
||||||
|
|
||||||
if self.args.usernames:
|
|
||||||
t = "since you have enabled --usernames, please provide username:password"
|
|
||||||
print(t)
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
p1 = getpass.getpass("password> ")
|
p1 = getpass.getpass("password> ")
|
||||||
|
|
|
@ -12,7 +12,7 @@ from types import SimpleNamespace
|
||||||
from .__init__ import ANYWIN, EXE, TYPE_CHECKING
|
from .__init__ import ANYWIN, EXE, TYPE_CHECKING
|
||||||
from .authsrv import LEELOO_DALLAS, VFS
|
from .authsrv import LEELOO_DALLAS, VFS
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
from .util import Daemon, absreal, min_ex, pybin, runhook, vjoin
|
from .util import Daemon, min_ex, pybin, runhook
|
||||||
|
|
||||||
if True: # pylint: disable=using-constant-test
|
if True: # pylint: disable=using-constant-test
|
||||||
from typing import Any, Union
|
from typing import Any, Union
|
||||||
|
@ -127,7 +127,7 @@ class SMB(object):
|
||||||
self.log("smb", msg, c)
|
self.log("smb", msg, c)
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
Daemon(self.srv.start, "smbd")
|
Daemon(self.srv.start)
|
||||||
|
|
||||||
def _auth_cb(self, *a, **ka):
|
def _auth_cb(self, *a, **ka):
|
||||||
debug("auth-result: %s %s", a, ka)
|
debug("auth-result: %s %s", a, ka)
|
||||||
|
@ -151,8 +151,6 @@ class SMB(object):
|
||||||
def _uname(self) -> str:
|
def _uname(self) -> str:
|
||||||
if self.noacc:
|
if self.noacc:
|
||||||
return LEELOO_DALLAS
|
return LEELOO_DALLAS
|
||||||
if not self.asrv.acct:
|
|
||||||
return "*"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# you found it! my single worst bit of code so far
|
# you found it! my single worst bit of code so far
|
||||||
|
@ -189,9 +187,7 @@ class SMB(object):
|
||||||
|
|
||||||
debug('%s("%s", %s) %s @%s\033[K\033[0m', caller, vpath, str(a), perms, uname)
|
debug('%s("%s", %s) %s @%s\033[K\033[0m', caller, vpath, str(a), perms, uname)
|
||||||
vfs, rem = self.asrv.vfs.get(vpath, uname, *perms)
|
vfs, rem = self.asrv.vfs.get(vpath, uname, *perms)
|
||||||
if not vfs.realpath:
|
return vfs, vfs.canonical(rem)
|
||||||
raise Exception("unmapped vfs")
|
|
||||||
return vfs, vjoin(vfs.realpath, rem)
|
|
||||||
|
|
||||||
def _listdir(self, vpath: str, *a: Any, **ka: Any) -> list[str]:
|
def _listdir(self, vpath: str, *a: Any, **ka: Any) -> list[str]:
|
||||||
vpath = vpath.replace("\\", "/").lstrip("/")
|
vpath = vpath.replace("\\", "/").lstrip("/")
|
||||||
|
@ -199,8 +195,6 @@ class SMB(object):
|
||||||
uname = self._uname()
|
uname = self._uname()
|
||||||
# debug('listdir("%s", %s) @%s\033[K\033[0m', vpath, str(a), uname)
|
# debug('listdir("%s", %s) @%s\033[K\033[0m', vpath, str(a), uname)
|
||||||
vfs, rem = self.asrv.vfs.get(vpath, uname, False, False)
|
vfs, rem = self.asrv.vfs.get(vpath, uname, False, False)
|
||||||
if not vfs.realpath:
|
|
||||||
raise Exception("unmapped vfs")
|
|
||||||
_, vfs_ls, vfs_virt = vfs.ls(
|
_, vfs_ls, vfs_virt = vfs.ls(
|
||||||
rem, uname, not self.args.no_scandir, [[False, False]]
|
rem, uname, not self.args.no_scandir, [[False, False]]
|
||||||
)
|
)
|
||||||
|
@ -215,7 +209,7 @@ class SMB(object):
|
||||||
sz = 112 * 2 # ['.', '..']
|
sz = 112 * 2 # ['.', '..']
|
||||||
for n, fn in enumerate(ls):
|
for n, fn in enumerate(ls):
|
||||||
if sz >= 64000:
|
if sz >= 64000:
|
||||||
t = "listing only %d of %d files (%d byte) in /%s for performance; see --smb-nwa-1"
|
t = "listing only %d of %d files (%d byte) in /%s; see impacket#1433"
|
||||||
warning(t, n, len(ls), sz, vpath)
|
warning(t, n, len(ls), sz, vpath)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -244,26 +238,11 @@ class SMB(object):
|
||||||
t = "blocked write (no-write-acc %s): /%s @%s"
|
t = "blocked write (no-write-acc %s): /%s @%s"
|
||||||
yeet(t % (vfs.axs.uwrite, vpath, uname))
|
yeet(t % (vfs.axs.uwrite, vpath, uname))
|
||||||
|
|
||||||
ap = absreal(ap)
|
|
||||||
xbu = vfs.flags.get("xbu")
|
xbu = vfs.flags.get("xbu")
|
||||||
if xbu and not runhook(
|
if xbu and not runhook(
|
||||||
self.nlog,
|
self.nlog, xbu, ap, vpath, "", "", 0, 0, "1.7.6.2", 0, ""
|
||||||
None,
|
|
||||||
self.hub.up2k,
|
|
||||||
"xbu.smb",
|
|
||||||
xbu,
|
|
||||||
ap,
|
|
||||||
vpath,
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
"1.7.6.2",
|
|
||||||
time.time(),
|
|
||||||
"",
|
|
||||||
):
|
):
|
||||||
yeet("blocked by xbu server config: %r" % (vpath,))
|
yeet("blocked by xbu server config: " + vpath)
|
||||||
|
|
||||||
ret = bos.open(ap, flags, *a, mode=chmod, **ka)
|
ret = bos.open(ap, flags, *a, mode=chmod, **ka)
|
||||||
if wr:
|
if wr:
|
||||||
|
@ -318,9 +297,9 @@ class SMB(object):
|
||||||
t = "blocked rename (no-move-acc %s): /%s @%s"
|
t = "blocked rename (no-move-acc %s): /%s @%s"
|
||||||
yeet(t % (vfs1.axs.umove, vp1, uname))
|
yeet(t % (vfs1.axs.umove, vp1, uname))
|
||||||
|
|
||||||
self.hub.up2k.handle_mv(uname, "1.7.6.2", vp1, vp2)
|
self.hub.up2k.handle_mv(uname, vp1, vp2)
|
||||||
try:
|
try:
|
||||||
bos.makedirs(ap2, vf=vfs2.flags)
|
bos.makedirs(ap2)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -334,7 +313,7 @@ class SMB(object):
|
||||||
t = "blocked mkdir (no-write-acc %s): /%s @%s"
|
t = "blocked mkdir (no-write-acc %s): /%s @%s"
|
||||||
yeet(t % (vfs.axs.uwrite, vpath, uname))
|
yeet(t % (vfs.axs.uwrite, vpath, uname))
|
||||||
|
|
||||||
return bos.mkdir(ap, vfs.flags["chmod_d"])
|
return bos.mkdir(ap)
|
||||||
|
|
||||||
def _stat(self, vpath: str, *a: Any, **ka: Any) -> os.stat_result:
|
def _stat(self, vpath: str, *a: Any, **ka: Any) -> os.stat_result:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -5,11 +5,11 @@ import errno
|
||||||
import re
|
import re
|
||||||
import select
|
import select
|
||||||
import socket
|
import socket
|
||||||
import time
|
from email.utils import formatdate
|
||||||
|
|
||||||
from .__init__ import TYPE_CHECKING
|
from .__init__ import TYPE_CHECKING
|
||||||
from .multicast import MC_Sck, MCast
|
from .multicast import MC_Sck, MCast
|
||||||
from .util import CachedSet, formatdate, html_escape, min_ex
|
from .util import CachedSet, html_escape, min_ex
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .broker_util import BrokerCli
|
from .broker_util import BrokerCli
|
||||||
|
@ -84,7 +84,7 @@ class SSDPr(object):
|
||||||
name = self.args.doctitle
|
name = self.args.doctitle
|
||||||
zs = zs.strip().format(c(ubase), c(url), c(name), c(self.args.zsid))
|
zs = zs.strip().format(c(ubase), c(url), c(name), c(self.args.zsid))
|
||||||
hc.reply(zs.encode("utf-8", "replace"))
|
hc.reply(zs.encode("utf-8", "replace"))
|
||||||
return False # close connection
|
return False # close connectino
|
||||||
|
|
||||||
|
|
||||||
class SSDPd(MCast):
|
class SSDPd(MCast):
|
||||||
|
@ -141,29 +141,9 @@ class SSDPd(MCast):
|
||||||
self.log("stopped", 2)
|
self.log("stopped", 2)
|
||||||
|
|
||||||
def run2(self) -> None:
|
def run2(self) -> None:
|
||||||
try:
|
|
||||||
if self.args.no_poll:
|
|
||||||
raise Exception()
|
|
||||||
fd2sck = {}
|
|
||||||
srvpoll = select.poll()
|
|
||||||
for sck in self.srv:
|
|
||||||
fd = sck.fileno()
|
|
||||||
fd2sck[fd] = sck
|
|
||||||
srvpoll.register(fd, select.POLLIN)
|
|
||||||
except Exception as ex:
|
|
||||||
srvpoll = None
|
|
||||||
if not self.args.no_poll:
|
|
||||||
t = "WARNING: failed to poll(), will use select() instead: %r"
|
|
||||||
self.log(t % (ex,), 3)
|
|
||||||
|
|
||||||
while self.running:
|
while self.running:
|
||||||
if srvpoll:
|
rdy = select.select(self.srv, [], [], self.args.z_chk or 180)
|
||||||
pr = srvpoll.poll((self.args.z_chk or 180) * 1000)
|
rx: list[socket.socket] = rdy[0] # type: ignore
|
||||||
rx = [fd2sck[x[0]] for x in pr if x[1] & select.POLLIN]
|
|
||||||
else:
|
|
||||||
rdy = select.select(self.srv, [], [], self.args.z_chk or 180)
|
|
||||||
rx: list[socket.socket] = rdy[0] # type: ignore
|
|
||||||
|
|
||||||
self.rxc.cln()
|
self.rxc.cln()
|
||||||
buf = b""
|
buf = b""
|
||||||
addr = ("0", 0)
|
addr = ("0", 0)
|
||||||
|
@ -188,7 +168,7 @@ class SSDPd(MCast):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.srv.clear()
|
self.srv = {}
|
||||||
|
|
||||||
def eat(self, buf: bytes, addr: tuple[str, int]) -> None:
|
def eat(self, buf: bytes, addr: tuple[str, int]) -> None:
|
||||||
cip = addr[0]
|
cip = addr[0]
|
||||||
|
@ -229,7 +209,7 @@ CONFIGID.UPNP.ORG: 1
|
||||||
|
|
||||||
"""
|
"""
|
||||||
v4 = srv.ip.replace("::ffff:", "")
|
v4 = srv.ip.replace("::ffff:", "")
|
||||||
zs = zs.format(formatdate(), v4, srv.hport, self.args.zsid)
|
zs = zs.format(formatdate(usegmt=True), v4, srv.hport, self.args.zsid)
|
||||||
zb = zs[1:].replace("\n", "\r\n").encode("utf-8", "replace")
|
zb = zs[1:].replace("\n", "\r\n").encode("utf-8", "replace")
|
||||||
srv.sck.sendto(zb, addr[:2])
|
srv.sck.sendto(zb, addr[:2])
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import argparse
|
||||||
import re
|
import re
|
||||||
import stat
|
import stat
|
||||||
import tarfile
|
import tarfile
|
||||||
|
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
|
|
||||||
from .authsrv import AuthSrv
|
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
from .sutil import StreamArc, errdesc
|
from .sutil import StreamArc, errdesc
|
||||||
from .util import Daemon, fsenc, min_ex
|
from .util import Daemon, fsenc, min_ex
|
||||||
|
@ -45,12 +45,12 @@ class StreamTar(StreamArc):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
log: "NamedLogger",
|
log: "NamedLogger",
|
||||||
asrv: AuthSrv,
|
args: argparse.Namespace,
|
||||||
fgen: Generator[dict[str, Any], None, None],
|
fgen: Generator[dict[str, Any], None, None],
|
||||||
cmp: str = "",
|
cmp: str = "",
|
||||||
**kwargs: Any
|
**kwargs: Any
|
||||||
):
|
):
|
||||||
super(StreamTar, self).__init__(log, asrv, fgen)
|
super(StreamTar, self).__init__(log, args, fgen)
|
||||||
|
|
||||||
self.ci = 0
|
self.ci = 0
|
||||||
self.co = 0
|
self.co = 0
|
||||||
|
@ -148,7 +148,7 @@ class StreamTar(StreamArc):
|
||||||
errors.append((f["vp"], ex))
|
errors.append((f["vp"], ex))
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
self.errf, txt = errdesc(self.asrv.vfs, errors)
|
self.errf, txt = errdesc(errors)
|
||||||
self.log("\n".join(([repr(self.errf)] + txt[1:])))
|
self.log("\n".join(([repr(self.errf)] + txt[1:])))
|
||||||
self.ser(self.errf)
|
self.ser(self.errf)
|
||||||
|
|
||||||
|
|
|
@ -8,16 +8,10 @@ from itertools import chain
|
||||||
from .bimap import Bimap, BimapError
|
from .bimap import Bimap, BimapError
|
||||||
from .bit import get_bits, set_bits
|
from .bit import get_bits, set_bits
|
||||||
from .buffer import BufferError
|
from .buffer import BufferError
|
||||||
from .label import DNSBuffer, DNSLabel, set_avahi_379
|
from .label import DNSBuffer, DNSLabel
|
||||||
from .ranges import IP4, IP6, H, I, check_bytes
|
from .ranges import IP4, IP6, H, I, check_bytes
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
range = xrange
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class DNSError(Exception):
|
class DNSError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -426,7 +420,7 @@ class RR(object):
|
||||||
if rdlength:
|
if rdlength:
|
||||||
rdata = RDMAP.get(QTYPE.get(rtype), RD).parse(buffer, rdlength)
|
rdata = RDMAP.get(QTYPE.get(rtype), RD).parse(buffer, rdlength)
|
||||||
else:
|
else:
|
||||||
rdata = RD(b"a")
|
rdata = ""
|
||||||
return cls(rname, rtype, rclass, ttl, rdata)
|
return cls(rname, rtype, rclass, ttl, rdata)
|
||||||
except (BufferError, BimapError) as e:
|
except (BufferError, BimapError) as e:
|
||||||
raise DNSError("Error unpacking RR [offset=%d]: %s" % (buffer.offset, e))
|
raise DNSError("Error unpacking RR [offset=%d]: %s" % (buffer.offset, e))
|
||||||
|
|
|
@ -11,23 +11,6 @@ LDH = set(range(33, 127))
|
||||||
ESCAPE = re.compile(r"\\([0-9][0-9][0-9])")
|
ESCAPE = re.compile(r"\\([0-9][0-9][0-9])")
|
||||||
|
|
||||||
|
|
||||||
avahi_379 = 0
|
|
||||||
|
|
||||||
|
|
||||||
def set_avahi_379():
|
|
||||||
global avahi_379
|
|
||||||
avahi_379 = 1
|
|
||||||
|
|
||||||
|
|
||||||
def log_avahi_379(args):
|
|
||||||
global avahi_379
|
|
||||||
if avahi_379 == 2:
|
|
||||||
return
|
|
||||||
avahi_379 = 2
|
|
||||||
t = "Invalid pointer in DNSLabel [offset=%d,pointer=%d,length=%d];\n\033[35m NOTE: this is probably avahi-bug #379, packet corruption in Avahi's mDNS-reflection feature. Copyparty has a workaround and is OK, but other devices need either --zm4 or --zm6"
|
|
||||||
raise BufferError(t % args)
|
|
||||||
|
|
||||||
|
|
||||||
class DNSLabelError(Exception):
|
class DNSLabelError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -113,11 +96,8 @@ class DNSBuffer(Buffer):
|
||||||
)
|
)
|
||||||
if pointer < self.offset:
|
if pointer < self.offset:
|
||||||
self.offset = pointer
|
self.offset = pointer
|
||||||
elif avahi_379:
|
|
||||||
log_avahi_379((self.offset, pointer, len(self.data)))
|
|
||||||
label.extend(b"a")
|
|
||||||
break
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
raise BufferError(
|
raise BufferError(
|
||||||
"Invalid pointer in DNSLabel [offset=%d,pointer=%d,length=%d]"
|
"Invalid pointer in DNSLabel [offset=%d,pointer=%d,length=%d]"
|
||||||
% (self.offset, pointer, len(self.data))
|
% (self.offset, pointer, len(self.data))
|
||||||
|
|
|
@ -11,21 +11,7 @@ import os
|
||||||
|
|
||||||
from ._shared import IP, Adapter
|
from ._shared import IP, Adapter
|
||||||
|
|
||||||
|
if os.name == "nt":
|
||||||
def nope(include_unconfigured=False):
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
S390X = os.uname().machine == "s390x"
|
|
||||||
except:
|
|
||||||
S390X = False
|
|
||||||
|
|
||||||
|
|
||||||
if os.environ.get("PRTY_NO_IFADDR") or S390X:
|
|
||||||
# s390x deadlocks at libc.getifaddrs
|
|
||||||
get_adapters = nope
|
|
||||||
elif os.name == "nt":
|
|
||||||
from ._win32 import get_adapters
|
from ._win32 import get_adapters
|
||||||
elif os.name == "posix":
|
elif os.name == "posix":
|
||||||
from ._posix import get_adapters
|
from ._posix import get_adapters
|
||||||
|
|
|
@ -17,7 +17,6 @@ if not PY2:
|
||||||
U: Callable[[str], str] = str
|
U: Callable[[str], str] = str
|
||||||
else:
|
else:
|
||||||
U = unicode # noqa: F821 # pylint: disable=undefined-variable,self-assigning-variable
|
U = unicode # noqa: F821 # pylint: disable=undefined-variable,self-assigning-variable
|
||||||
range = xrange # noqa: F821 # pylint: disable=undefined-variable,self-assigning-variable
|
|
||||||
|
|
||||||
|
|
||||||
class Adapter(object):
|
class Adapter(object):
|
||||||
|
|
|
@ -16,11 +16,6 @@ if True: # pylint: disable=using-constant-test
|
||||||
|
|
||||||
from typing import Callable, List, Optional, Tuple, Union
|
from typing import Callable, List, Optional, Tuple, Union
|
||||||
|
|
||||||
try:
|
|
||||||
range = xrange
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def num_char_count_bits(ver: int) -> int:
|
def num_char_count_bits(ver: int) -> int:
|
||||||
return 16 if (ver + 7) // 17 else 8
|
return 16 if (ver + 7) // 17 else 8
|
||||||
|
@ -594,20 +589,3 @@ def _get_bit(x: int, i: int) -> bool:
|
||||||
|
|
||||||
class DataTooLongError(ValueError):
|
class DataTooLongError(ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def qr2svg(qr: QrCode, border: int) -> str:
|
|
||||||
parts: list[str] = []
|
|
||||||
for y in range(qr.size):
|
|
||||||
sy = border + y
|
|
||||||
for x in range(qr.size):
|
|
||||||
if qr.modules[y][x]:
|
|
||||||
parts.append("M%d,%dh1v1h-1z" % (border + x, sy))
|
|
||||||
t = """\
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 {0} {0}" stroke="none">
|
|
||||||
<rect width="100%" height="100%" fill="#F7F7F7"/>
|
|
||||||
<path d="{1}" fill="#111111"/>
|
|
||||||
</svg>
|
|
||||||
"""
|
|
||||||
return t.format(qr.size + border * 2, " ".join(parts))
|
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import argparse
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from .__init__ import CORES
|
from .__init__ import CORES
|
||||||
from .authsrv import VFS, AuthSrv
|
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
from .th_cli import ThumbCli
|
from .th_cli import ThumbCli
|
||||||
from .util import UTC, vjoin, vol_san
|
from .util import UTC, vjoin
|
||||||
|
|
||||||
if True: # pylint: disable=using-constant-test
|
if True: # pylint: disable=using-constant-test
|
||||||
from typing import Any, Generator, Optional
|
from typing import Any, Generator, Optional
|
||||||
|
@ -17,20 +17,16 @@ if True: # pylint: disable=using-constant-test
|
||||||
from .util import NamedLogger
|
from .util import NamedLogger
|
||||||
|
|
||||||
|
|
||||||
TAR_NO_OPUS = set("aac|m4a|mp3|oga|ogg|opus|wma".split("|"))
|
|
||||||
|
|
||||||
|
|
||||||
class StreamArc(object):
|
class StreamArc(object):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
log: "NamedLogger",
|
log: "NamedLogger",
|
||||||
asrv: AuthSrv,
|
args: argparse.Namespace,
|
||||||
fgen: Generator[dict[str, Any], None, None],
|
fgen: Generator[dict[str, Any], None, None],
|
||||||
**kwargs: Any
|
**kwargs: Any
|
||||||
):
|
):
|
||||||
self.log = log
|
self.log = log
|
||||||
self.asrv = asrv
|
self.args = args
|
||||||
self.args = asrv.args
|
|
||||||
self.fgen = fgen
|
self.fgen = fgen
|
||||||
self.stopped = False
|
self.stopped = False
|
||||||
|
|
||||||
|
@ -85,7 +81,9 @@ def enthumb(
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
rem = f["vp"]
|
rem = f["vp"]
|
||||||
ext = rem.rsplit(".", 1)[-1].lower()
|
ext = rem.rsplit(".", 1)[-1].lower()
|
||||||
if (fmt == "mp3" and ext == "mp3") or (fmt == "opus" and ext in TAR_NO_OPUS):
|
if (fmt == "mp3" and ext == "mp3") or (
|
||||||
|
fmt == "opus" and ext in "aac|m4a|mp3|ogg|opus|wma".split("|")
|
||||||
|
):
|
||||||
raise Exception()
|
raise Exception()
|
||||||
|
|
||||||
vp = vjoin(vtop, rem.split("/", 1)[1])
|
vp = vjoin(vtop, rem.split("/", 1)[1])
|
||||||
|
@ -105,20 +103,15 @@ def enthumb(
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
||||||
def errdesc(
|
def errdesc(errors: list[tuple[str, str]]) -> tuple[dict[str, Any], list[str]]:
|
||||||
vfs: VFS, errors: list[tuple[str, str]]
|
|
||||||
) -> tuple[dict[str, Any], list[str]]:
|
|
||||||
report = ["copyparty failed to add the following files to the archive:", ""]
|
report = ["copyparty failed to add the following files to the archive:", ""]
|
||||||
|
|
||||||
for fn, err in errors:
|
for fn, err in errors:
|
||||||
report.extend([" file: %r" % (fn,), "error: %s" % (err,), ""])
|
report.extend([" file: {}".format(fn), "error: {}".format(err), ""])
|
||||||
|
|
||||||
btxt = "\r\n".join(report).encode("utf-8", "replace")
|
|
||||||
btxt = vol_san(list(vfs.all_vols.values()), btxt)
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(prefix="copyparty-", delete=False) as tf:
|
with tempfile.NamedTemporaryFile(prefix="copyparty-", delete=False) as tf:
|
||||||
tf_path = tf.name
|
tf_path = tf.name
|
||||||
tf.write(btxt)
|
tf.write("\r\n".join(report).encode("utf-8", "replace"))
|
||||||
|
|
||||||
dt = datetime.now(UTC).strftime("%Y-%m%d-%H%M%S")
|
dt = datetime.now(UTC).strftime("%Y-%m%d-%H%M%S")
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,14 +1,15 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import argparse
|
||||||
import calendar
|
import calendar
|
||||||
import stat
|
import stat
|
||||||
import time
|
import time
|
||||||
|
import zlib
|
||||||
|
|
||||||
from .authsrv import AuthSrv
|
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
from .sutil import StreamArc, errdesc
|
from .sutil import StreamArc, errdesc
|
||||||
from .util import min_ex, sanitize_fn, spack, sunpack, yieldfile, zlib
|
from .util import min_ex, sanitize_fn, spack, sunpack, yieldfile
|
||||||
|
|
||||||
if True: # pylint: disable=using-constant-test
|
if True: # pylint: disable=using-constant-test
|
||||||
from typing import Any, Generator, Optional
|
from typing import Any, Generator, Optional
|
||||||
|
@ -36,7 +37,9 @@ def dostime2unix(buf: bytes) -> int:
|
||||||
|
|
||||||
|
|
||||||
def unixtime2dos(ts: int) -> bytes:
|
def unixtime2dos(ts: int) -> bytes:
|
||||||
dy, dm, dd, th, tm, ts, _, _, _ = time.gmtime(ts + 1)
|
tt = time.gmtime(ts + 1)
|
||||||
|
dy, dm, dd, th, tm, ts = list(tt)[:6]
|
||||||
|
|
||||||
bd = ((dy - 1980) << 9) + (dm << 5) + dd
|
bd = ((dy - 1980) << 9) + (dm << 5) + dd
|
||||||
bt = (th << 11) + (tm << 5) + ts // 2
|
bt = (th << 11) + (tm << 5) + ts // 2
|
||||||
try:
|
try:
|
||||||
|
@ -54,7 +57,6 @@ def gen_fdesc(sz: int, crc32: int, z64: bool) -> bytes:
|
||||||
|
|
||||||
def gen_hdr(
|
def gen_hdr(
|
||||||
h_pos: Optional[int],
|
h_pos: Optional[int],
|
||||||
z64: bool,
|
|
||||||
fn: str,
|
fn: str,
|
||||||
sz: int,
|
sz: int,
|
||||||
lastmod: int,
|
lastmod: int,
|
||||||
|
@ -71,6 +73,7 @@ def gen_hdr(
|
||||||
# appnote 4.5 / zip 3.0 (2008) / unzip 6.0 (2009) says to add z64
|
# appnote 4.5 / zip 3.0 (2008) / unzip 6.0 (2009) says to add z64
|
||||||
# extinfo for values which exceed H, but that becomes an off-by-one
|
# extinfo for values which exceed H, but that becomes an off-by-one
|
||||||
# (can't tell if it was clamped or exactly maxval), make it obvious
|
# (can't tell if it was clamped or exactly maxval), make it obvious
|
||||||
|
z64 = sz >= 0xFFFFFFFF
|
||||||
z64v = [sz, sz] if z64 else []
|
z64v = [sz, sz] if z64 else []
|
||||||
if h_pos and h_pos >= 0xFFFFFFFF:
|
if h_pos and h_pos >= 0xFFFFFFFF:
|
||||||
# central, also consider ptr to original header
|
# central, also consider ptr to original header
|
||||||
|
@ -99,12 +102,12 @@ def gen_hdr(
|
||||||
|
|
||||||
# spec says to put zeros when !crc if bit3 (streaming)
|
# spec says to put zeros when !crc if bit3 (streaming)
|
||||||
# however infozip does actual sz and it even works on winxp
|
# however infozip does actual sz and it even works on winxp
|
||||||
# (same reasoning for z64 extradata later)
|
# (same reasning for z64 extradata later)
|
||||||
vsz = 0xFFFFFFFF if z64 else sz
|
vsz = 0xFFFFFFFF if z64 else sz
|
||||||
ret += spack(b"<LL", vsz, vsz)
|
ret += spack(b"<LL", vsz, vsz)
|
||||||
|
|
||||||
# windows support (the "?" replace below too)
|
# windows support (the "?" replace below too)
|
||||||
fn = sanitize_fn(fn, "/")
|
fn = sanitize_fn(fn, "/", [])
|
||||||
bfn = fn.encode("utf-8" if utf8 else "cp437", "replace").replace(b"?", b"_")
|
bfn = fn.encode("utf-8" if utf8 else "cp437", "replace").replace(b"?", b"_")
|
||||||
|
|
||||||
# add ntfs (0x24) and/or unix (0x10) extrafields for utc, add z64 if requested
|
# add ntfs (0x24) and/or unix (0x10) extrafields for utc, add z64 if requested
|
||||||
|
@ -216,13 +219,13 @@ class StreamZip(StreamArc):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
log: "NamedLogger",
|
log: "NamedLogger",
|
||||||
asrv: AuthSrv,
|
args: argparse.Namespace,
|
||||||
fgen: Generator[dict[str, Any], None, None],
|
fgen: Generator[dict[str, Any], None, None],
|
||||||
utf8: bool = False,
|
utf8: bool = False,
|
||||||
pre_crc: bool = False,
|
pre_crc: bool = False,
|
||||||
**kwargs: Any
|
**kwargs: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
super(StreamZip, self).__init__(log, asrv, fgen)
|
super(StreamZip, self).__init__(log, args, fgen)
|
||||||
|
|
||||||
self.utf8 = utf8
|
self.utf8 = utf8
|
||||||
self.pre_crc = pre_crc
|
self.pre_crc = pre_crc
|
||||||
|
@ -244,7 +247,6 @@ class StreamZip(StreamArc):
|
||||||
|
|
||||||
sz = st.st_size
|
sz = st.st_size
|
||||||
ts = st.st_mtime
|
ts = st.st_mtime
|
||||||
h_pos = self.pos
|
|
||||||
|
|
||||||
crc = 0
|
crc = 0
|
||||||
if self.pre_crc:
|
if self.pre_crc:
|
||||||
|
@ -253,12 +255,8 @@ class StreamZip(StreamArc):
|
||||||
|
|
||||||
crc &= 0xFFFFFFFF
|
crc &= 0xFFFFFFFF
|
||||||
|
|
||||||
# some unzip-programs expect a 64bit data-descriptor
|
h_pos = self.pos
|
||||||
# even if the only 32bit-exceeding value is the offset,
|
buf = gen_hdr(None, name, sz, ts, self.utf8, crc, self.pre_crc)
|
||||||
# so force that by placeholdering the filesize too
|
|
||||||
z64 = h_pos >= 0xFFFFFFFF or sz >= 0xFFFFFFFF
|
|
||||||
|
|
||||||
buf = gen_hdr(None, z64, name, sz, ts, self.utf8, crc, self.pre_crc)
|
|
||||||
yield self._ct(buf)
|
yield self._ct(buf)
|
||||||
|
|
||||||
for buf in yieldfile(src, self.args.iobuf):
|
for buf in yieldfile(src, self.args.iobuf):
|
||||||
|
@ -271,6 +269,8 @@ class StreamZip(StreamArc):
|
||||||
|
|
||||||
self.items.append((name, sz, ts, crc, h_pos))
|
self.items.append((name, sz, ts, crc, h_pos))
|
||||||
|
|
||||||
|
z64 = sz >= 4 * 1024 * 1024 * 1024
|
||||||
|
|
||||||
if z64 or not self.pre_crc:
|
if z64 or not self.pre_crc:
|
||||||
buf = gen_fdesc(sz, crc, z64)
|
buf = gen_fdesc(sz, crc, z64)
|
||||||
yield self._ct(buf)
|
yield self._ct(buf)
|
||||||
|
@ -302,15 +302,14 @@ class StreamZip(StreamArc):
|
||||||
mbuf = b""
|
mbuf = b""
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
errf, txt = errdesc(self.asrv.vfs, errors)
|
errf, txt = errdesc(errors)
|
||||||
self.log("\n".join(([repr(errf)] + txt[1:])))
|
self.log("\n".join(([repr(errf)] + txt[1:])))
|
||||||
for x in self.ser(errf):
|
for x in self.ser(errf):
|
||||||
yield x
|
yield x
|
||||||
|
|
||||||
cdir_pos = self.pos
|
cdir_pos = self.pos
|
||||||
for name, sz, ts, crc, h_pos in self.items:
|
for name, sz, ts, crc, h_pos in self.items:
|
||||||
z64 = h_pos >= 0xFFFFFFFF or sz >= 0xFFFFFFFF
|
buf = gen_hdr(h_pos, name, sz, ts, self.utf8, crc, self.pre_crc)
|
||||||
buf = gen_hdr(h_pos, z64, name, sz, ts, self.utf8, crc, self.pre_crc)
|
|
||||||
mbuf += self._ct(buf)
|
mbuf += self._ct(buf)
|
||||||
if len(mbuf) >= 16384:
|
if len(mbuf) >= 16384:
|
||||||
yield mbuf
|
yield mbuf
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue