mirror of
https://github.com/9001/copyparty.git
synced 2025-12-07 13:52:15 -07:00
Compare commits
No commits in common. "hovudstraum" and "v1.16.10" have entirely different histories.
hovudstrau
...
v1.16.10
44
.github/ISSUE_TEMPLATE/bug_report.md
vendored
44
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -7,48 +7,34 @@ 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:
|
||||||
|
|
||||||
### The rest of the stack
|
**Additional context**
|
||||||
if you are connecting directly to copyparty then that's cool, otherwise please mention everything else between copyparty and the browser (reverseproxy, tunnels, etc.)
|
|
||||||
|
|
||||||
### Server log
|
|
||||||
if the issue might be server-related, include everything that appears in the copyparty log during startup, and also anything else you think might be relevant
|
|
||||||
|
|
||||||
### Additional context
|
|
||||||
any other context about the problem here
|
any other context about the problem here
|
||||||
|
|
|
||||||
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -7,9 +7,7 @@ assignees: '9001'
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- NOTE:
|
all of the below are optional, consider them as inspiration, delete and rewrite at will
|
||||||
**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 -->
|
|
||||||
|
|
||||||
**is your feature request related to a problem? Please describe.**
|
**is your feature request related to a problem? Please describe.**
|
||||||
a description of what the problem is, for example, `I'm always frustrated when [...]` or `Why is it not possible to [...]`
|
a description of what the problem is, for example, `I'm always frustrated when [...]` or `Why is it not possible to [...]`
|
||||||
|
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -43,7 +43,3 @@ scripts/docker/*.err
|
||||||
|
|
||||||
# nix build output link
|
# nix build output link
|
||||||
result
|
result
|
||||||
result-*
|
|
||||||
|
|
||||||
# IDEA config
|
|
||||||
.idea/
|
|
||||||
|
|
|
||||||
52
.vscode/launch.json
vendored
52
.vscode/launch.json
vendored
|
|
@ -3,7 +3,7 @@
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Run copyparty",
|
"name": "Run copyparty",
|
||||||
"type": "debugpy",
|
"type": "python",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "copyparty",
|
"module": "copyparty",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
|
|
@ -11,46 +11,30 @@
|
||||||
"justMyCode": false,
|
"justMyCode": false,
|
||||||
"env": {
|
"env": {
|
||||||
"PYDEVD_DISABLE_FILE_VALIDATION": "1",
|
"PYDEVD_DISABLE_FILE_VALIDATION": "1",
|
||||||
"PYTHONWARNINGS": "always" //error
|
"PYTHONWARNINGS": "always", //error
|
||||||
},
|
},
|
||||||
"args": [
|
"args": [
|
||||||
//"-nw", // no-write; for testing uploads without writing to disk
|
//"-nw",
|
||||||
//"-q", // quiet; speedboost when console output is not needed
|
"-ed",
|
||||||
|
"-emp",
|
||||||
// # increase debugger performance:
|
|
||||||
//"no-htp",
|
|
||||||
//"hash-mt=0",
|
|
||||||
//"mtag-mt=1",
|
|
||||||
//"th-mt=1",
|
|
||||||
|
|
||||||
// # listen for FTP and TFTP
|
|
||||||
"--ftp=3921",
|
|
||||||
"--ftp-pr=12000-12099",
|
|
||||||
"--tftp=3969",
|
|
||||||
|
|
||||||
// # listen on all IPv6, all IPv4, and unix-socket
|
|
||||||
"-i::,unix:777:a.sock",
|
|
||||||
|
|
||||||
// # misc
|
|
||||||
"--dedup",
|
|
||||||
"-e2dsa",
|
"-e2dsa",
|
||||||
"-e2ts",
|
"-e2ts",
|
||||||
"--rss",
|
"-mtp=.bpm=f,bin/mtag/audio-bpm.py",
|
||||||
"--shr=/shr",
|
|
||||||
"--stats",
|
|
||||||
"-z",
|
|
||||||
|
|
||||||
// # users + volumes
|
|
||||||
"-aed:wark",
|
"-aed:wark",
|
||||||
"-vdist:dist:r",
|
"-vsrv::r:rw,ed:c,dupe",
|
||||||
"-vsrv::r:rw,ed",
|
"-vdist:dist:r"
|
||||||
"-vsrv/junk:junk:r:A,ed",
|
|
||||||
"--ver"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "No debug",
|
||||||
|
"preLaunchTask": "no_dbg",
|
||||||
|
"type": "python",
|
||||||
|
//"request": "attach", "port": 42069
|
||||||
|
// fork: nc -l 42069 </dev/null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Run active unit test",
|
"name": "Run active unit test",
|
||||||
"type": "debugpy",
|
"type": "python",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "unittest",
|
"module": "unittest",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
|
|
@ -67,6 +51,6 @@
|
||||||
"program": "${file}",
|
"program": "${file}",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"justMyCode": false
|
"justMyCode": false
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -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 / LLM when writing code
|
|
||||||
|
|
||||||
copyparty is 100% organic, free-range, human-written software!
|
|
||||||
|
|
||||||
> ⚠ you are now entering a no-copilot zone
|
|
||||||
|
|
||||||
the *only* place where LLM/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
|
||||||
|
|
@ -39,9 +26,7 @@ if you wanna have a go at coding it up yourself then maybe mention the idea on d
|
||||||
|
|
||||||
aside from documentation and ideas, some other things that would be cool to have some help with is:
|
aside from documentation and ideas, some other things that would be cool to have some help with is:
|
||||||
|
|
||||||
* **translations** -- the copyparty web-UI has translations in [copyparty/web/tl](https://github.com/9001/copyparty/tree/hovudstraum/copyparty/web/tl); if you'd like to [add a translation](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) 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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
# Security Policy
|
# Security Policy
|
||||||
|
|
||||||
if you hit something extra juicy pls let me know on one of the following:
|
if you hit something extra juicy pls let me know on either of the following
|
||||||
* email -- `copyparty@ocv.ze` except `ze` should be `me`
|
* email -- `copyparty@ocv.ze` except `ze` should be `me`
|
||||||
|
* [mastodon dm](https://layer8.space/@tripflag) -- `@tripflag@layer8.space`
|
||||||
* [github private vulnerability report](https://github.com/9001/copyparty/security/advisories/new), wow that form is complicated
|
* [github private vulnerability report](https://github.com/9001/copyparty/security/advisories/new), wow that form is complicated
|
||||||
|
* [twitter dm](https://twitter.com/tripflag) (if im somehow not banned yet)
|
||||||
|
|
||||||
no bug bounties sorry! all i can offer is greetz in the release notes
|
no bug bounties sorry! all i can offer is greetz in the release notes
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# [`u2c.py`](u2c.py)
|
# [`u2c.py`](u2c.py)
|
||||||
* command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm)
|
* command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm)
|
||||||
* file uploads, file-search, autoresume of aborted/broken uploads
|
* file uploads, file-search, autoresume of aborted/broken uploads
|
||||||
* [sync local folder to server](https://github.com/9001/copyparty/#folder-sync)
|
* sync local folder to server
|
||||||
* generally faster than browsers
|
* generally faster than browsers
|
||||||
* if something breaks just restart it
|
* if something breaks just restart it
|
||||||
|
|
||||||
|
|
@ -78,6 +78,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/bash
|
|
||||||
# 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)
|
|
||||||
|
|
@ -8,7 +8,7 @@ import sqlite3
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
DB_VER1 = 3
|
DB_VER1 = 3
|
||||||
DB_VER2 = 6
|
DB_VER2 = 5
|
||||||
|
|
||||||
BY_PATH = None
|
BY_PATH = None
|
||||||
NC = None
|
NC = None
|
||||||
|
|
@ -39,7 +39,7 @@ def ls(db):
|
||||||
print(f"{nfiles} files")
|
print(f"{nfiles} files")
|
||||||
print(f"{ntags} tags\n")
|
print(f"{ntags} tags\n")
|
||||||
|
|
||||||
print("number of occurrences for each tag,")
|
print("number of occurences for each tag,")
|
||||||
print(" 'x' = file has no tags")
|
print(" 'x' = file has no tags")
|
||||||
print(" 't:mtp' = the mtp flag (file not mtp processed yet)")
|
print(" 't:mtp' = the mtp flag (file not mtp processed yet)")
|
||||||
print()
|
print()
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -4,11 +4,6 @@ these programs either take zero arguments, or a filepath (the affected file), or
|
||||||
|
|
||||||
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 (xm/xbu/xau/xiu/xbc/xac/xbr/xar/xbd/xad/xban)
|
||||||
|
|
||||||
in particular, if a hook is loaded into copyparty with the hook-flag `c` ("check") then its exit-code controls the action that launched the hook:
|
|
||||||
* exit-code `0` = allow the action, and/or continue running the next hook
|
|
||||||
* exit-code `100` = allow the action, and stop running any remaining consecutive hooks
|
|
||||||
* anything else = reject/prevent the original action, and don't run the remaining hooks
|
|
||||||
|
|
||||||
> **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
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -19,8 +14,6 @@ in particular, if a hook is loaded into copyparty with the hook-flag `c` ("check
|
||||||
* [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
|
* [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
|
||||||
|
|
@ -32,20 +25,10 @@ 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
|
* [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)
|
|
||||||
* [reject-and-explain.py](reject-and-explain.py) shows a custom error-message when it rejects an upload
|
|
||||||
* [reject-ramdisk.py](reject-ramdisk.py) rejects the upload if the destination is a ramdisk
|
|
||||||
* this hook uses the `I` flag which makes it 140x faster, but if the plugin has a bug it may crash copyparty
|
|
||||||
|
|
||||||
|
|
||||||
# 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
|
||||||
* [wget-i.py](wget-i.py) is an import-safe modification of this hook (starts 140x faster, but higher chance of bugs)
|
|
||||||
* [qbittorrent-magnet.py](qbittorrent-magnet.py) starts downloading a torrent if you post a magnet url
|
* [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
|
* [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
|
* [msg-log.py](msg-log.py) is a guestbook; logs messages to a doc in the same folder
|
||||||
|
|
||||||
|
|
||||||
# general concept demos
|
|
||||||
* [import-me.py](import-me.py) shows how the `I` flag makes the hook 140x faster (but you need to be Very Careful when writing the plugin)
|
|
||||||
* [wget-i.py](wget-i.py) is an import-safe modification of [wget.py](wget.py)
|
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
_ = r"""
|
|
||||||
the fastest hook in the west
|
|
||||||
(runs directly inside copyparty, not as a subprocess)
|
|
||||||
|
|
||||||
example usage as global config:
|
|
||||||
--xbu I,bin/hooks/import-me.py
|
|
||||||
|
|
||||||
example usage as a volflag (per-volume config):
|
|
||||||
-v srv/inc:inc:r:rw,ed:c,xbu=I,bin/hooks/import-me.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 listed below)
|
|
||||||
|
|
||||||
example usage as a volflag in a copyparty config file:
|
|
||||||
[/inc]
|
|
||||||
srv/inc
|
|
||||||
accs:
|
|
||||||
r: *
|
|
||||||
rw: ed
|
|
||||||
flags:
|
|
||||||
xbu: I,bin/hooks/import-me.py
|
|
||||||
|
|
||||||
parameters explained,
|
|
||||||
I = import; do not fork / subprocess
|
|
||||||
|
|
||||||
IMPORTANT NOTE:
|
|
||||||
because this hook is running inside copyparty, you need to
|
|
||||||
be EXCEPTIONALLY CAREFUL to avoid side-effects, for example
|
|
||||||
DO NOT os.chdir() or anything like that, and also make sure
|
|
||||||
that the name of this file is unique (cannot be the same as
|
|
||||||
an existing python module/library)
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def main(ka: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
# "ka" is a dictionary with info from copyparty...
|
|
||||||
|
|
||||||
# but because we are running inside copyparty, we don't need such courtesies;
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
cf = inspect.currentframe().f_back.f_back.f_back
|
|
||||||
t = "hello from hook; I am able to peek into copyparty's memory like so:\n function name: %s\n variables:\n %s\n"
|
|
||||||
t2 = "\n ".join([("%r: %r" % (k, v))[:99] for k, v in cf.f_locals.items()][:9])
|
|
||||||
logger = ka["log"]
|
|
||||||
logger(t % (cf.f_code, t2))
|
|
||||||
|
|
||||||
# must return a dictionary with:
|
|
||||||
# "rc": the retcode; 0 is ok
|
|
||||||
return {"rc": 0}
|
|
||||||
|
|
@ -9,7 +9,7 @@ from plyer import notification
|
||||||
_ = r"""
|
_ = r"""
|
||||||
show os notification on upload; works on windows, linux, macos, android
|
show os notification on upload; works on windows, linux, macos, android
|
||||||
|
|
||||||
dependencies:
|
depdencies:
|
||||||
windows: python3 -m pip install --user -U plyer
|
windows: python3 -m pip install --user -U plyer
|
||||||
linux: python3 -m pip install --user -U plyer
|
linux: python3 -m pip install --user -U plyer
|
||||||
macos: python3 -m pip install --user -U plyer pyobjus
|
macos: python3 -m pip install --user -U plyer pyobjus
|
||||||
|
|
|
||||||
|
|
@ -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,60 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
_ = r"""
|
|
||||||
reject file upload (with a nice explanation why)
|
|
||||||
|
|
||||||
example usage as global config:
|
|
||||||
--xbu j,c1,bin/hooks/reject-and-explain.py
|
|
||||||
|
|
||||||
example usage as a volflag (per-volume config):
|
|
||||||
-v srv/inc:inc:r:rw,ed:c,xbu=j,c1,bin/hooks/reject-and-explain.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 listed below)
|
|
||||||
|
|
||||||
example usage as a volflag in a copyparty config file:
|
|
||||||
[/inc]
|
|
||||||
srv/inc
|
|
||||||
accs:
|
|
||||||
r: *
|
|
||||||
rw: ed
|
|
||||||
flags:
|
|
||||||
xbu: j,c1,bin/hooks/reject-and-explain.py
|
|
||||||
|
|
||||||
parameters explained,
|
|
||||||
xbu = execute-before-upload (can also be 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
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
inf = json.loads(sys.argv[1])
|
|
||||||
vdir, fn = os.path.split(inf["vp"])
|
|
||||||
print("inf[vp] = %r" % (inf["vp"],), file=sys.stderr)
|
|
||||||
|
|
||||||
# the following is what decides if we'll accept the upload or reject it:
|
|
||||||
# we check if the upload-folder url matches the following regex-pattern:
|
|
||||||
ok = re.search(r"(^|/)day[0-9]+$", vdir, re.IGNORECASE)
|
|
||||||
|
|
||||||
if ok:
|
|
||||||
# allow the upload
|
|
||||||
print("{}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# the upload was rejected; display the following errortext:
|
|
||||||
errmsg = "Files can only be uploaded into a folder named 'DayN' where N is a number, for example 'Day573'. This file was REJECTED: "
|
|
||||||
errmsg += inf["vp"] # if you want to mention the file's url
|
|
||||||
print(json.dumps({"rejectmsg": errmsg}))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
from argparse import Namespace
|
|
||||||
|
|
||||||
from jinja2.nodes import Name
|
|
||||||
from copyparty.fsutil import Fstab
|
|
||||||
from typing import Any, Optional
|
|
||||||
|
|
||||||
|
|
||||||
_ = r"""
|
|
||||||
reject an upload if the target folder is on a ramdisk; useful when you
|
|
||||||
have a volume where some folders inside are ramdisks but others aren't
|
|
||||||
|
|
||||||
example usage as global config:
|
|
||||||
--xbu I,bin/hooks/reject-ramdisk.py
|
|
||||||
|
|
||||||
example usage as a volflag (per-volume config):
|
|
||||||
-v srv/inc:inc:r:rw,ed:c,xbu=I,bin/hooks/reject-ramdisk.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 listed below)
|
|
||||||
|
|
||||||
example usage as a volflag in a copyparty config file:
|
|
||||||
[/inc]
|
|
||||||
srv/inc
|
|
||||||
accs:
|
|
||||||
r: *
|
|
||||||
rw: ed
|
|
||||||
flags:
|
|
||||||
xbu: I,bin/hooks/reject-ramdisk.py
|
|
||||||
|
|
||||||
parameters explained,
|
|
||||||
I = import; do not fork / subprocess
|
|
||||||
|
|
||||||
IMPORTANT NOTE:
|
|
||||||
because this hook is imported inside copyparty, you need to
|
|
||||||
be EXCEPTIONALLY CAREFUL to avoid side-effects, for example
|
|
||||||
DO NOT os.chdir() or anything like that, and also make sure
|
|
||||||
that the name of this file is unique (cannot be the same as
|
|
||||||
an existing python module/library)
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
mutex = threading.Lock()
|
|
||||||
fstab: Optional[Fstab] = None
|
|
||||||
|
|
||||||
|
|
||||||
def main(ka: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
global fstab
|
|
||||||
with mutex:
|
|
||||||
log = ka["log"] # this is a copyparty NamedLogger function
|
|
||||||
if not fstab:
|
|
||||||
log("<HOOK:RAMDISK> creating fstab", 6)
|
|
||||||
args = Namespace()
|
|
||||||
args.mtab_age = 1 # cache the filesystem info for 1 sec
|
|
||||||
fstab = Fstab(log, args, False)
|
|
||||||
|
|
||||||
ap = ka["ap"] # abspath the upload is going to
|
|
||||||
fs, mp = fstab.get(ap) # figure out what the filesystem is
|
|
||||||
ramdisk = fs in ("tmpfs", "overlay") # looks like a ramdisk?
|
|
||||||
|
|
||||||
# log("<HOOK:RAMDISK> fs=%r" % (fs,))
|
|
||||||
|
|
||||||
if ramdisk:
|
|
||||||
t = "Upload REJECTED because destination is a ramdisk"
|
|
||||||
return {"rc": 1, "rejectmsg": t}
|
|
||||||
|
|
||||||
return {"rc": 0}
|
|
||||||
|
|
@ -71,9 +71,6 @@ def main():
|
||||||
## selecting it inside the print at the end:
|
## 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
|
# create a subfolder named after the filetype and move it into there
|
||||||
into_subfolder = {"vp": ext}
|
into_subfolder = {"vp": ext}
|
||||||
|
|
||||||
|
|
@ -95,8 +92,8 @@ def main():
|
||||||
by_category = {} # no action
|
by_category = {} # no action
|
||||||
|
|
||||||
# now choose the default effect to apply; can be any of these:
|
# now choose the default effect to apply; can be any of these:
|
||||||
# into_junk into_subfolder into_toplevel into_sibling by_category
|
# into_subfolder into_toplevel into_sibling by_category
|
||||||
effect = into_sibling
|
effect = {"vp": "/junk"}
|
||||||
|
|
||||||
##
|
##
|
||||||
## but we can keep going, adding more speicifc rules
|
## but we can keep going, adding more speicifc rules
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,15 @@
|
||||||
// see usb-eject.py for usage
|
// see usb-eject.py for usage
|
||||||
|
|
||||||
function usbclick() {
|
function usbclick() {
|
||||||
var o = QS('#treeul a[dst="/usb/"]') || QS('#treepar a[dst="/usb/"]');
|
QS('#treeul a[href="/usb/"]').click();
|
||||||
if (o)
|
|
||||||
o.click();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function eject_cb() {
|
function eject_cb() {
|
||||||
var t = ('' + this.responseText).trim();
|
var t = this.responseText;
|
||||||
if (t.indexOf('can be safely unplugged') < 0 && t.indexOf('Device can be removed') < 0)
|
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);
|
return toast.err(30, 'usb eject failed:\n\n' + t);
|
||||||
|
|
||||||
toast.ok(5, esc(t.replace(/ - /g, '\n\n')).trim());
|
toast.ok(5, esc(t.replace(/ - /g, '\n\n')));
|
||||||
usbclick(); setTimeout(usbclick, 10);
|
usbclick(); setTimeout(usbclick, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -23,15 +21,10 @@ function add_eject_2(a) {
|
||||||
var v = aw[2],
|
var v = aw[2],
|
||||||
k = 'umount_' + v;
|
k = 'umount_' + v;
|
||||||
|
|
||||||
for (var b = 0; b < 9; b++) {
|
qsr('#' + k);
|
||||||
var o = ebi(k);
|
|
||||||
if (!o)
|
|
||||||
break;
|
|
||||||
o.parentNode.removeChild(o);
|
|
||||||
}
|
|
||||||
|
|
||||||
a.appendChild(mknod('span', k, '⏏'), a);
|
a.appendChild(mknod('span', k, '⏏'), a);
|
||||||
o = ebi(k);
|
|
||||||
|
var o = ebi(k);
|
||||||
o.style.cssText = 'position:absolute; right:1em; margin-top:-.2em; font-size:1.3em';
|
o.style.cssText = 'position:absolute; right:1em; margin-top:-.2em; font-size:1.3em';
|
||||||
o.onclick = function (e) {
|
o.onclick = function (e) {
|
||||||
ev(e);
|
ev(e);
|
||||||
|
|
@ -45,9 +38,8 @@ function add_eject_2(a) {
|
||||||
};
|
};
|
||||||
|
|
||||||
function add_eject() {
|
function add_eject() {
|
||||||
var o = QSA('#treeul a[href^="/usb/"]') || QSA('#treepar a[href^="/usb/"]');
|
for (var a of QSA('#treeul a[href^="/usb/"]'))
|
||||||
for (var a = o.length - 1; a > 0; a--)
|
add_eject_2(a);
|
||||||
add_eject_2(o[a]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import os
|
||||||
import stat
|
import stat
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
import sys
|
import sys
|
||||||
from urllib.parse import unquote_to_bytes as unquote
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -15,13 +14,13 @@ 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)
|
put usb-eject.js in the webroot (or somewhere else http-accessible)
|
||||||
then run copyparty with these args:
|
then run copyparty with these args:
|
||||||
|
|
||||||
-v /run/media/egon:/usb:A:c,hist=/tmp/junk
|
-v /run/media/ed:/usb:A:c,hist=/tmp/junk
|
||||||
--xm=c1,bin/hooks/usb-eject.py
|
--xm=c1,bin/hooks/usb-eject.py
|
||||||
--js-browser=/usb-eject.js
|
--js-browser=/usb-eject.js
|
||||||
|
|
||||||
which does the following respectively,
|
which does the following respectively,
|
||||||
|
|
||||||
* share all of /run/media/egon as /usb with admin for everyone
|
* share all of /run/media/ed as /usb with admin for everyone
|
||||||
and put the histpath somewhere it won't cause trouble
|
and put the histpath somewhere it won't cause trouble
|
||||||
* run the usb-eject hook with stdout redirect to the web-ui
|
* run the usb-eject hook with stdout redirect to the web-ui
|
||||||
* add the complementary usb-eject.js to the browser
|
* add the complementary usb-eject.js to the browser
|
||||||
|
|
@ -29,33 +28,18 @@ which does the following respectively,
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
MOUNT_BASE = b"/run/media/egon/"
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
try:
|
||||||
msg = sys.argv[1]
|
label = sys.argv[1].split(":usb-eject:")[1].split(":")[0]
|
||||||
if msg.startswith("upload-queue-empty;"):
|
mp = "/run/media/ed/" + label
|
||||||
return
|
|
||||||
label = msg.split(":usb-eject:")[1].split(":")[0]
|
|
||||||
mp = MOUNT_BASE + unquote(label)
|
|
||||||
# print("ejecting [%s]... " % (mp,), end="")
|
# print("ejecting [%s]... " % (mp,), end="")
|
||||||
mp = os.path.abspath(os.path.realpath(mp))
|
mp = os.path.abspath(os.path.realpath(mp.encode("utf-8")))
|
||||||
st = os.lstat(mp)
|
st = os.lstat(mp)
|
||||||
if not stat.S_ISDIR(st.st_mode) or not mp.startswith(MOUNT_BASE):
|
if not stat.S_ISDIR(st.st_mode):
|
||||||
raise Exception("not a regular directory")
|
raise Exception("not a regular directory")
|
||||||
|
|
||||||
# if you're running copyparty as root (thx for the faith)
|
cmd = [b"gio", b"mount", b"-e", mp]
|
||||||
# you'll need something like this to make dbus talkative
|
print(sp.check_output(cmd).decode("utf-8", "replace").strip())
|
||||||
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:
|
except Exception as ex:
|
||||||
print("unmount failed: %r" % (ex,))
|
print("unmount failed: %r" % (ex,))
|
||||||
|
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
import subprocess as sp
|
|
||||||
|
|
||||||
|
|
||||||
_ = r"""
|
|
||||||
use copyparty as a file downloader by POSTing URLs as
|
|
||||||
application/x-www-form-urlencoded (for example using the
|
|
||||||
📟 message-to-server-log in the web-ui)
|
|
||||||
|
|
||||||
this hook is a modified copy of wget.py, modified to
|
|
||||||
make it import-safe so it can be run with the 'I' flag,
|
|
||||||
which speeds up the startup time of the hook by 140x
|
|
||||||
|
|
||||||
example usage as global config:
|
|
||||||
--xm aw,I,bin/hooks/wget-i.py
|
|
||||||
|
|
||||||
parameters explained,
|
|
||||||
xm = execute on message-to-server-log
|
|
||||||
aw = only users with write-access can use this
|
|
||||||
I = import; do not fork / subprocess
|
|
||||||
|
|
||||||
example usage as a volflag (per-volume config):
|
|
||||||
-v srv/inc:inc:r:rw,ed:c,xm=aw,I,bin/hooks/wget.py
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
(share filesystem-path srv/inc as volume /inc,
|
|
||||||
readable by everyone, read-write for user 'ed',
|
|
||||||
running this plugin on all messages with the params explained above)
|
|
||||||
|
|
||||||
example usage as a volflag in a copyparty config file:
|
|
||||||
[/inc]
|
|
||||||
srv/inc
|
|
||||||
accs:
|
|
||||||
r: *
|
|
||||||
rw: ed
|
|
||||||
flags:
|
|
||||||
xm: aw,I,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)
|
|
||||||
|
|
||||||
IMPORTANT NOTE:
|
|
||||||
because this hook uses the 'I' flag to run inside copyparty,
|
|
||||||
many other flags will not work (f,j,c3,t3600 as seen in the
|
|
||||||
original wget.py), and furthermore + more importantly we
|
|
||||||
need to be EXCEPTIONALLY CAREFUL to avoid side-effects, so
|
|
||||||
the os.chdir has been replaced with cwd=dirpath for example
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def do_stuff(inf):
|
|
||||||
"""
|
|
||||||
worker function which is executed in another thread to
|
|
||||||
avoid blocking copyparty while the download is running,
|
|
||||||
since we cannot use the 'f,t3600' hook-flags with 'I'
|
|
||||||
"""
|
|
||||||
|
|
||||||
# first things first; grab the logger-function which copyparty is letting us borrow
|
|
||||||
log = inf["log"]
|
|
||||||
|
|
||||||
url = inf["txt"]
|
|
||||||
if url.startswith("upload-queue-empty;"):
|
|
||||||
return
|
|
||||||
|
|
||||||
if "://" not in url:
|
|
||||||
url = "https://" + url
|
|
||||||
|
|
||||||
proto = url.split("://")[0].lower()
|
|
||||||
if proto not in ("http", "https", "ftp", "ftps"):
|
|
||||||
raise Exception("bad proto {}".format(proto))
|
|
||||||
|
|
||||||
dirpath = inf["ap"]
|
|
||||||
|
|
||||||
name = url.split("?")[0].split("/")[-1]
|
|
||||||
msg = "-- DOWNLOADING " + name
|
|
||||||
log(msg)
|
|
||||||
tfn = os.path.join(dirpath, msg)
|
|
||||||
open(tfn, "wb").close()
|
|
||||||
|
|
||||||
cmd = ["wget", "--trust-server-names", "-nv", "--", url]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# two things to note here:
|
|
||||||
# - cannot use the `c3` hook-flag with `I` so mute output with stdout=sp.DEVNULL instead;
|
|
||||||
# - MUST NOT use os.chdir with 'I' so use cwd=dirpath instead
|
|
||||||
sp.check_call(cmd, cwd=dirpath, stdout=sp.DEVNULL)
|
|
||||||
except:
|
|
||||||
t = "-- FAILED TO DOWNLOAD " + name
|
|
||||||
log(t, 3) # 3=yellow=warning
|
|
||||||
open(os.path.join(dirpath, t), "wb").close()
|
|
||||||
raise # have copyparty scream about the details in the log
|
|
||||||
|
|
||||||
os.unlink(tfn)
|
|
||||||
|
|
||||||
|
|
||||||
def main(inf):
|
|
||||||
threading.Thread(target=do_stuff, args=(inf,), daemon=True).start()
|
|
||||||
|
|
@ -47,9 +47,6 @@ while you're in the /inc folder (or any folder below there)
|
||||||
def main():
|
def main():
|
||||||
inf = json.loads(sys.argv[1])
|
inf = json.loads(sys.argv[1])
|
||||||
url = inf["txt"]
|
url = inf["txt"]
|
||||||
if url.startswith("upload-queue-empty;"):
|
|
||||||
return
|
|
||||||
|
|
||||||
if "://" not in url:
|
if "://" not in url:
|
||||||
url = "https://" + url
|
url = "https://" + url
|
||||||
|
|
||||||
|
|
@ -69,7 +66,7 @@ def main():
|
||||||
try:
|
try:
|
||||||
sp.check_call(cmd)
|
sp.check_call(cmd)
|
||||||
except:
|
except:
|
||||||
t = "-- FAILED TO DOWNLOAD " + name
|
t = "-- FAILED TO DONWLOAD " + name
|
||||||
print(f"{t}\n", end="")
|
print(f"{t}\n", end="")
|
||||||
open(t, "wb").close()
|
open(t, "wb").close()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,3 @@ instead of affecting all volumes, you can set the options for just one volume li
|
||||||
* `:c,mtp=key=f,audio-key.py`
|
* `:c,mtp=key=f,audio-key.py`
|
||||||
* `:c,mtp=.bpm=f,audio-bpm.py`
|
* `:c,mtp=.bpm=f,audio-bpm.py`
|
||||||
* `:c,mtp=ahash,vhash=f,media-hash.py`
|
* `:c,mtp=ahash,vhash=f,media-hash.py`
|
||||||
|
|
||||||
|
|
||||||
# tips & tricks
|
|
||||||
|
|
||||||
* to delete tags for all files below `blog*` and rescan that, `sqlite3 .hist/up2k.db "delete from mt where w in (select substr(w,1,16) from up where rd like 'blog%')";`
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from copyparty.util import fsenc, runcmd
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
uses exiftool to geotag images based on embedded gps coordinates in exif data
|
|
||||||
|
|
||||||
adds four new metadata keys:
|
|
||||||
.gps_lat = latitute
|
|
||||||
.gps_lon = longitude
|
|
||||||
.masl = meters above sea level
|
|
||||||
city = "city, subregion, region"
|
|
||||||
|
|
||||||
usage: -mtp .masl,.gps_lat,.gps_lon,city=ad,t10,bin/mtag/geotag.py
|
|
||||||
|
|
||||||
example: https://a.ocv.me/pub/blog/j7/8/?grid=0
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
cmd = b"exiftool -api geolocation -n".split(b" ")
|
|
||||||
rc, so, se = runcmd(cmd + [fsenc(sys.argv[1])])
|
|
||||||
ptn = re.compile("([^:]*[^ :]) *: (.*)")
|
|
||||||
city = ["", "", ""]
|
|
||||||
ret = {}
|
|
||||||
for ln in so.split("\n"):
|
|
||||||
m = ptn.match(ln)
|
|
||||||
if not m:
|
|
||||||
continue
|
|
||||||
k, v = m.groups()
|
|
||||||
if k == "Geolocation City":
|
|
||||||
city[2] = v
|
|
||||||
elif k == "Geolocation Subregion":
|
|
||||||
city[1] = v
|
|
||||||
elif k == "Geolocation Region":
|
|
||||||
city[0] = v
|
|
||||||
elif k == "GPS Latitude":
|
|
||||||
ret[".gps_lat"] = "%.04f" % (float(v),)
|
|
||||||
elif k == "GPS Longitude":
|
|
||||||
ret[".gps_lon"] = "%.04f" % (float(v),)
|
|
||||||
elif k == "GPS Altitude":
|
|
||||||
ret[".masl"] = str(int(float(v)))
|
|
||||||
v = ", ".join(city).strip(", ")
|
|
||||||
if v:
|
|
||||||
ret["city"] = v
|
|
||||||
print(json.dumps(ret))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -7,7 +7,7 @@ example copyparty config to use this:
|
||||||
--urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=guestbook=t10,ad,p,bin/mtag/guestbook-read.py:mte=+guestbook
|
--urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=guestbook=t10,ad,p,bin/mtag/guestbook-read.py:mte=+guestbook
|
||||||
|
|
||||||
explained:
|
explained:
|
||||||
for realpath srv/hello (served at /hello), write-only for everyone,
|
for realpath srv/hello (served at /hello), write-only for eveyrone,
|
||||||
enable file analysis on upload (e2ts),
|
enable file analysis on upload (e2ts),
|
||||||
use mtp plugin "bin/mtag/guestbook-read.py" to provide metadata tag "guestbook",
|
use mtp plugin "bin/mtag/guestbook-read.py" to provide metadata tag "guestbook",
|
||||||
do this on all uploads regardless of extension,
|
do this on all uploads regardless of extension,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ example copyparty config to use this:
|
||||||
--urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=xgb=ebin,t10,ad,p,bin/mtag/guestbook.py:mte=+xgb
|
--urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=xgb=ebin,t10,ad,p,bin/mtag/guestbook.py:mte=+xgb
|
||||||
|
|
||||||
explained:
|
explained:
|
||||||
for realpath srv/hello (served at /hello),write-only for everyone,
|
for realpath srv/hello (served at /hello),write-only for eveyrone,
|
||||||
enable file analysis on upload (e2ts),
|
enable file analysis on upload (e2ts),
|
||||||
use mtp plugin "bin/mtag/guestbook.py" to provide metadata tag "xgb",
|
use mtp plugin "bin/mtag/guestbook.py" to provide metadata tag "xgb",
|
||||||
do this on all uploads with the file extension "bin",
|
do this on all uploads with the file extension "bin",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ def main():
|
||||||
# on success, delete the .bin file which contains the URL
|
# on success, delete the .bin file which contains the URL
|
||||||
os.unlink(fp)
|
os.unlink(fp)
|
||||||
except:
|
except:
|
||||||
open("-- FAILED TO DOWNLOAD " + name, "wb").close()
|
open("-- FAILED TO DONWLOAD " + name, "wb").close()
|
||||||
|
|
||||||
os.unlink(tfn)
|
os.unlink(tfn)
|
||||||
print(url)
|
print(url)
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ __copyright__ = 2019
|
||||||
__license__ = "MIT"
|
__license__ = "MIT"
|
||||||
__url__ = "https://github.com/9001/copyparty/"
|
__url__ = "https://github.com/9001/copyparty/"
|
||||||
|
|
||||||
S_VERSION = "2.1"
|
S_VERSION = "2.0"
|
||||||
S_BUILD_DT = "2025-09-06"
|
S_BUILD_DT = "2024-10-01"
|
||||||
|
|
||||||
"""
|
"""
|
||||||
mount a copyparty server (local or remote) as a filesystem
|
mount a copyparty server (local or remote) as a filesystem
|
||||||
|
|
@ -99,7 +99,7 @@ except:
|
||||||
elif MACOS:
|
elif MACOS:
|
||||||
libfuse = "install https://osxfuse.github.io/"
|
libfuse = "install https://osxfuse.github.io/"
|
||||||
else:
|
else:
|
||||||
libfuse = "apt install libfuse2\n modprobe fuse"
|
libfuse = "apt install libfuse3-3\n modprobe fuse"
|
||||||
|
|
||||||
m = """\033[33m
|
m = """\033[33m
|
||||||
could not import fuse; these may help:
|
could not import fuse; these may help:
|
||||||
|
|
@ -359,7 +359,7 @@ class Gateway(object):
|
||||||
def sendreq(self, meth, path, headers, **kwargs):
|
def sendreq(self, meth, path, headers, **kwargs):
|
||||||
tid = get_tid()
|
tid = get_tid()
|
||||||
if self.password:
|
if self.password:
|
||||||
headers["PW"] = self.password
|
headers["Cookie"] = "=".join(["cppwd", self.password])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
c = self.getconn(tid)
|
c = self.getconn(tid)
|
||||||
|
|
@ -902,7 +902,9 @@ class CPPF(Operations):
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def _readdir(self, path, fh=None):
|
def _readdir(self, path, fh=None):
|
||||||
dbg("dircache miss")
|
path = path.strip("/")
|
||||||
|
dbg("readdir %r [%s]", path, fh)
|
||||||
|
|
||||||
ret = self.gw.listdir(path)
|
ret = self.gw.listdir(path)
|
||||||
if not self.n_dircache:
|
if not self.n_dircache:
|
||||||
return ret
|
return ret
|
||||||
|
|
@ -912,17 +914,11 @@ class CPPF(Operations):
|
||||||
self.dircache.append(cn)
|
self.dircache.append(cn)
|
||||||
self.clean_dircache()
|
self.clean_dircache()
|
||||||
|
|
||||||
|
# import pprint; pprint.pprint(ret)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def readdir(self, path, fh=None):
|
def readdir(self, path, fh=None):
|
||||||
dbg("readdir %r [%s]", path, fh)
|
return [".", ".."] + list(self._readdir(path, fh))
|
||||||
path = path.strip("/")
|
|
||||||
cn = self.get_cached_dir(path)
|
|
||||||
if cn:
|
|
||||||
ret = cn.data
|
|
||||||
else:
|
|
||||||
ret = self._readdir(path, fh)
|
|
||||||
return [".", ".."] + list(ret)
|
|
||||||
|
|
||||||
def read(self, path, length, offset, fh=None):
|
def read(self, path, length, offset, fh=None):
|
||||||
req_max = 1024 * 1024 * 8
|
req_max = 1024 * 1024 * 8
|
||||||
|
|
@ -997,6 +993,7 @@ class CPPF(Operations):
|
||||||
if cn:
|
if cn:
|
||||||
dents = cn.data
|
dents = cn.data
|
||||||
else:
|
else:
|
||||||
|
dbg("cache miss")
|
||||||
dents = self._readdir(dirpath)
|
dents = self._readdir(dirpath)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -1144,15 +1141,10 @@ def main():
|
||||||
if WINDOWS:
|
if WINDOWS:
|
||||||
examples.append("http://192.168.1.69:3923/music/ M:")
|
examples.append("http://192.168.1.69:3923/music/ M:")
|
||||||
|
|
||||||
epi = "example:" + ex_pre + ex_pre.join(examples)
|
|
||||||
epi += """\n
|
|
||||||
NOTE: if server has --usernames enabled, then password is "username:password"
|
|
||||||
"""
|
|
||||||
|
|
||||||
ap = argparse.ArgumentParser(
|
ap = argparse.ArgumentParser(
|
||||||
formatter_class=TheArgparseFormatter,
|
formatter_class=TheArgparseFormatter,
|
||||||
description="mount a copyparty server as a local filesystem -- " + ver,
|
description="mount a copyparty server as a local filesystem -- " + ver,
|
||||||
epilog=epi,
|
epilog="example:" + ex_pre + ex_pre.join(examples),
|
||||||
)
|
)
|
||||||
# fmt: off
|
# fmt: off
|
||||||
ap.add_argument("base_url", type=str, help="remote copyparty URL to mount")
|
ap.add_argument("base_url", type=str, help="remote copyparty URL to mount")
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ chmod 777 "$jail/tmp"
|
||||||
|
|
||||||
|
|
||||||
# create a dev
|
# create a dev
|
||||||
(cd "$jail"; mkdir -p dev; cd dev
|
(cd $jail; mkdir -p dev; cd dev
|
||||||
[ -e null ] || mknod -m 666 null c 1 3
|
[ -e null ] || mknod -m 666 null c 1 3
|
||||||
[ -e zero ] || mknod -m 666 zero c 1 5
|
[ -e zero ] || mknod -m 666 zero c 1 5
|
||||||
[ -e random ] || mknod -m 444 random c 1 8
|
[ -e random ] || mknod -m 444 random c 1 8
|
||||||
|
|
|
||||||
90
bin/u2c.py
90
bin/u2c.py
|
|
@ -1,8 +1,8 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
S_VERSION = "2.15"
|
S_VERSION = "2.8"
|
||||||
S_BUILD_DT = "2025-10-25"
|
S_BUILD_DT = "2025-01-21"
|
||||||
|
|
||||||
"""
|
"""
|
||||||
u2c.py: upload to copyparty
|
u2c.py: upload to copyparty
|
||||||
|
|
@ -10,7 +10,7 @@ u2c.py: upload to copyparty
|
||||||
https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py
|
https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py
|
||||||
|
|
||||||
- dependencies: no
|
- dependencies: no
|
||||||
- supports python 2.6, 2.7, and 3.3 through 3.14
|
- supports python 2.6, 2.7, and 3.3 through 3.12
|
||||||
- if something breaks just try again and it'll autoresume
|
- if something breaks just try again and it'll autoresume
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -52,7 +52,6 @@ if PY2:
|
||||||
|
|
||||||
sys.dont_write_bytecode = True
|
sys.dont_write_bytecode = True
|
||||||
bytes = str
|
bytes = str
|
||||||
files_decoder = lambda s: unicode(s, "utf8")
|
|
||||||
else:
|
else:
|
||||||
from urllib.parse import quote_from_bytes as quote
|
from urllib.parse import quote_from_bytes as quote
|
||||||
from urllib.parse import unquote_to_bytes as unquote
|
from urllib.parse import unquote_to_bytes as unquote
|
||||||
|
|
@ -62,7 +61,6 @@ else:
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
|
|
||||||
unicode = str
|
unicode = str
|
||||||
files_decoder = unicode
|
|
||||||
|
|
||||||
|
|
||||||
WTF8 = "replace" if PY2 else "surrogateescape"
|
WTF8 = "replace" if PY2 else "surrogateescape"
|
||||||
|
|
@ -232,15 +230,10 @@ class HCli(object):
|
||||||
|
|
||||||
MJ = "application/json"
|
MJ = "application/json"
|
||||||
MO = "application/octet-stream"
|
MO = "application/octet-stream"
|
||||||
MM = "application/x-www-form-urlencoded"
|
|
||||||
CLEN = "Content-Length"
|
CLEN = "Content-Length"
|
||||||
|
|
||||||
web = None # type: HCli
|
web = None # type: HCli
|
||||||
|
|
||||||
links = [] # type: list[str]
|
|
||||||
linkmtx = threading.Lock()
|
|
||||||
linkfile = None
|
|
||||||
|
|
||||||
|
|
||||||
class File(object):
|
class File(object):
|
||||||
"""an up2k upload task; represents a single file"""
|
"""an up2k upload task; represents a single file"""
|
||||||
|
|
@ -591,10 +584,9 @@ def undns(url):
|
||||||
|
|
||||||
def _scd(err, top):
|
def _scd(err, top):
|
||||||
"""non-recursive listing of directory contents, along with stat() info"""
|
"""non-recursive listing of directory contents, along with stat() info"""
|
||||||
top_ = os.path.join(top, b"")
|
|
||||||
with os.scandir(top) as dh:
|
with os.scandir(top) as dh:
|
||||||
for fh in dh:
|
for fh in dh:
|
||||||
abspath = top_ + fh.name
|
abspath = os.path.join(top, fh.name)
|
||||||
try:
|
try:
|
||||||
yield [abspath, fh.stat()]
|
yield [abspath, fh.stat()]
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
|
@ -603,9 +595,8 @@ def _scd(err, top):
|
||||||
|
|
||||||
def _lsd(err, top):
|
def _lsd(err, top):
|
||||||
"""non-recursive listing of directory contents, along with stat() info"""
|
"""non-recursive listing of directory contents, along with stat() info"""
|
||||||
top_ = os.path.join(top, b"")
|
|
||||||
for name in os.listdir(top):
|
for name in os.listdir(top):
|
||||||
abspath = top_ + name
|
abspath = os.path.join(top, name)
|
||||||
try:
|
try:
|
||||||
yield [abspath, os.stat(abspath)]
|
yield [abspath, os.stat(abspath)]
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
|
@ -680,7 +671,7 @@ def walkdirs(err, tops, excl):
|
||||||
yield stop, ap[len(stop) :].lstrip(sep), inf
|
yield stop, ap[len(stop) :].lstrip(sep), inf
|
||||||
else:
|
else:
|
||||||
d, n = top.rsplit(sep, 1)
|
d, n = top.rsplit(sep, 1)
|
||||||
yield d or b"/", n, os.stat(top)
|
yield d, n, os.stat(top)
|
||||||
|
|
||||||
|
|
||||||
# mostly from copyparty/util.py
|
# mostly from copyparty/util.py
|
||||||
|
|
@ -770,29 +761,6 @@ def get_hashlist(file, pcb, mth):
|
||||||
file.kchunks[k] = [v1, v2]
|
file.kchunks[k] = [v1, v2]
|
||||||
|
|
||||||
|
|
||||||
def printlink(ar, purl, name, fk):
|
|
||||||
if not name:
|
|
||||||
url = purl # srch
|
|
||||||
else:
|
|
||||||
name = quotep(name.encode("utf-8", WTF8)).decode("utf-8")
|
|
||||||
if fk:
|
|
||||||
url = "%s%s?k=%s" % (purl, name, fk)
|
|
||||||
else:
|
|
||||||
url = "%s%s" % (purl, name)
|
|
||||||
|
|
||||||
url = "%s/%s" % (ar.burl, url.lstrip("/"))
|
|
||||||
|
|
||||||
with linkmtx:
|
|
||||||
if ar.u:
|
|
||||||
links.append(url)
|
|
||||||
if ar.ud:
|
|
||||||
print(url)
|
|
||||||
if linkfile:
|
|
||||||
zs = "%s\n" % (url,)
|
|
||||||
zb = zs.encode("utf-8", "replace")
|
|
||||||
linkfile.write(zb)
|
|
||||||
|
|
||||||
|
|
||||||
def handshake(ar, file, search):
|
def handshake(ar, file, search):
|
||||||
# type: (argparse.Namespace, File, bool) -> tuple[list[str], bool]
|
# type: (argparse.Namespace, File, bool) -> tuple[list[str], bool]
|
||||||
"""
|
"""
|
||||||
|
|
@ -812,9 +780,7 @@ def handshake(ar, file, search):
|
||||||
else:
|
else:
|
||||||
if ar.touch:
|
if ar.touch:
|
||||||
req["umod"] = True
|
req["umod"] = True
|
||||||
if ar.owo:
|
if ar.ow:
|
||||||
req["replace"] = "mt"
|
|
||||||
elif ar.ow:
|
|
||||||
req["replace"] = True
|
req["replace"] = True
|
||||||
|
|
||||||
file.recheck = False
|
file.recheck = False
|
||||||
|
|
@ -866,17 +832,12 @@ def handshake(ar, file, search):
|
||||||
raise Exception(txt)
|
raise Exception(txt)
|
||||||
|
|
||||||
if search:
|
if search:
|
||||||
if ar.uon and r["hits"]:
|
|
||||||
printlink(ar, r["hits"][0]["rp"], "", "")
|
|
||||||
return r["hits"], False
|
return r["hits"], False
|
||||||
|
|
||||||
file.url = quotep(r["purl"].encode("utf-8", WTF8)).decode("utf-8")
|
file.url = quotep(r["purl"].encode("utf-8", WTF8)).decode("utf-8")
|
||||||
file.name = r["name"]
|
file.name = r["name"]
|
||||||
file.wark = r["wark"]
|
file.wark = r["wark"]
|
||||||
|
|
||||||
if ar.uon and not r["hash"]:
|
|
||||||
printlink(ar, file.url, r["name"], r.get("fk"))
|
|
||||||
|
|
||||||
return r["hash"], r["sprs"]
|
return r["hash"], r["sprs"]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -980,7 +941,6 @@ class Ctl(object):
|
||||||
self.nfiles, self.nbytes = self.stats
|
self.nfiles, self.nbytes = self.stats
|
||||||
self.filegen = walkdirs([], ar.files, ar.x)
|
self.filegen = walkdirs([], ar.files, ar.x)
|
||||||
self.recheck = [] # type: list[File]
|
self.recheck = [] # type: list[File]
|
||||||
self.last_file = None
|
|
||||||
|
|
||||||
if ar.safe:
|
if ar.safe:
|
||||||
self._safe()
|
self._safe()
|
||||||
|
|
@ -1017,11 +977,6 @@ class Ctl(object):
|
||||||
|
|
||||||
self._fancy()
|
self._fancy()
|
||||||
|
|
||||||
file = self.last_file
|
|
||||||
if self.up_br and file:
|
|
||||||
zs = quotep(file.name.encode("utf-8", WTF8))
|
|
||||||
web.req("POST", file.url, {}, b"msg=upload-queue-empty;" + zs, MM)
|
|
||||||
|
|
||||||
self.ok = not self.errs
|
self.ok = not self.errs
|
||||||
|
|
||||||
def _safe(self):
|
def _safe(self):
|
||||||
|
|
@ -1232,7 +1187,9 @@ class Ctl(object):
|
||||||
while req:
|
while req:
|
||||||
print("DELETING ~%s#%s" % (srd, len(req)))
|
print("DELETING ~%s#%s" % (srd, len(req)))
|
||||||
body = json.dumps(req).encode("utf-8")
|
body = json.dumps(req).encode("utf-8")
|
||||||
sc, txt = web.req("POST", "/?delete", {}, body, MJ)
|
sc, txt = web.req(
|
||||||
|
"POST", self.ar.url + "?delete", {}, body, MJ
|
||||||
|
)
|
||||||
if sc == 413 and "json 2big" in txt:
|
if sc == 413 and "json 2big" in txt:
|
||||||
print(" (delete request too big; slicing...)")
|
print(" (delete request too big; slicing...)")
|
||||||
req = req[: len(req) // 2]
|
req = req[: len(req) // 2]
|
||||||
|
|
@ -1298,7 +1255,7 @@ class Ctl(object):
|
||||||
if self.ar.jw:
|
if self.ar.jw:
|
||||||
print("%s %s" % (wark, vp))
|
print("%s %s" % (wark, vp))
|
||||||
else:
|
else:
|
||||||
zd = datetime.datetime.fromtimestamp(max(0, file.lmod), UTC)
|
zd = datetime.datetime.fromtimestamp(file.lmod, UTC)
|
||||||
dt = "%04d-%02d-%02d %02d:%02d:%02d" % (
|
dt = "%04d-%02d-%02d %02d:%02d:%02d" % (
|
||||||
zd.year,
|
zd.year,
|
||||||
zd.month,
|
zd.month,
|
||||||
|
|
@ -1460,7 +1417,6 @@ class Ctl(object):
|
||||||
|
|
||||||
file = fsl.file
|
file = fsl.file
|
||||||
cids = fsl.cids
|
cids = fsl.cids
|
||||||
self.last_file = file
|
|
||||||
|
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
if not self.uploader_busy:
|
if not self.uploader_busy:
|
||||||
|
|
@ -1516,7 +1472,7 @@ class APF(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFor
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
global web, linkfile
|
global web
|
||||||
|
|
||||||
time.strptime("19970815", "%Y%m%d") # python#7980
|
time.strptime("19970815", "%Y%m%d") # python#7980
|
||||||
"".encode("idna") # python#29288
|
"".encode("idna") # python#29288
|
||||||
|
|
@ -1535,14 +1491,14 @@ def main():
|
||||||
|
|
||||||
# fmt: off
|
# fmt: off
|
||||||
ap = app = argparse.ArgumentParser(formatter_class=APF, description="copyparty up2k uploader / filesearch tool " + ver, epilog="""
|
ap = app = argparse.ArgumentParser(formatter_class=APF, description="copyparty up2k uploader / filesearch tool " + ver, epilog="""
|
||||||
NOTE: source file/folder selection uses rsync syntax, meaning that:
|
NOTE:
|
||||||
|
source file/folder selection uses rsync syntax, meaning that:
|
||||||
"foo" uploads the entire folder to URL/foo/
|
"foo" uploads the entire folder to URL/foo/
|
||||||
"foo/" uploads the CONTENTS of the folder into URL/
|
"foo/" uploads the CONTENTS of the folder into URL/
|
||||||
NOTE: if server has --usernames enabled, then password is "username:password"
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
ap.add_argument("url", type=unicode, help="server url, including destination folder")
|
ap.add_argument("url", type=unicode, help="server url, including destination folder")
|
||||||
ap.add_argument("files", type=files_decoder, nargs="+", help="files and/or folders to process")
|
ap.add_argument("files", type=unicode, nargs="+", help="files and/or folders to process")
|
||||||
ap.add_argument("-v", action="store_true", help="verbose")
|
ap.add_argument("-v", action="store_true", help="verbose")
|
||||||
ap.add_argument("-a", metavar="PASSWD", help="password or $filepath")
|
ap.add_argument("-a", metavar="PASSWD", help="password or $filepath")
|
||||||
ap.add_argument("-s", action="store_true", help="file-search (disables upload)")
|
ap.add_argument("-s", action="store_true", help="file-search (disables upload)")
|
||||||
|
|
@ -1550,15 +1506,9 @@ NOTE: if server has --usernames enabled, then password is "username:password"
|
||||||
ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible")
|
ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible")
|
||||||
ap.add_argument("--touch", action="store_true", help="if last-modified timestamps differ, push local to server (need write+delete perms)")
|
ap.add_argument("--touch", action="store_true", help="if last-modified timestamps differ, push local to server (need write+delete perms)")
|
||||||
ap.add_argument("--ow", action="store_true", help="overwrite existing files instead of autorenaming")
|
ap.add_argument("--ow", action="store_true", help="overwrite existing files instead of autorenaming")
|
||||||
ap.add_argument("--owo", action="store_true", help="overwrite existing files if server-file is older")
|
|
||||||
ap.add_argument("--spd", action="store_true", help="print speeds for each file")
|
ap.add_argument("--spd", action="store_true", help="print speeds for each file")
|
||||||
ap.add_argument("--version", action="store_true", help="show version and exit")
|
ap.add_argument("--version", action="store_true", help="show version and exit")
|
||||||
|
|
||||||
ap = app.add_argument_group("print links")
|
|
||||||
ap.add_argument("-u", action="store_true", help="print list of download-links after all uploads finished")
|
|
||||||
ap.add_argument("-ud", action="store_true", help="print download-link after each upload finishes")
|
|
||||||
ap.add_argument("-uf", type=unicode, metavar="PATH", help="print list of download-links to file")
|
|
||||||
|
|
||||||
ap = app.add_argument_group("compatibility")
|
ap = app.add_argument_group("compatibility")
|
||||||
ap.add_argument("--cls", action="store_true", help="clear screen before start")
|
ap.add_argument("--cls", action="store_true", help="clear screen before start")
|
||||||
ap.add_argument("--rh", type=int, metavar="TRIES", default=0, help="resolve server hostname before upload (good for buggy networks, but TLS certs will break)")
|
ap.add_argument("--rh", type=int, metavar="TRIES", default=0, help="resolve server hostname before upload (good for buggy networks, but TLS certs will break)")
|
||||||
|
|
@ -1644,10 +1594,6 @@ NOTE: if server has --usernames enabled, then password is "username:password"
|
||||||
ar.x = "|".join(ar.x or [])
|
ar.x = "|".join(ar.x or [])
|
||||||
|
|
||||||
setattr(ar, "wlist", ar.url == "-")
|
setattr(ar, "wlist", ar.url == "-")
|
||||||
setattr(ar, "uon", ar.u or ar.ud or ar.uf)
|
|
||||||
|
|
||||||
if ar.uf:
|
|
||||||
linkfile = open(ar.uf, "wb")
|
|
||||||
|
|
||||||
for k in "dl dr drd wlist".split():
|
for k in "dl dr drd wlist".split():
|
||||||
errs = []
|
errs = []
|
||||||
|
|
@ -1710,12 +1656,6 @@ NOTE: if server has --usernames enabled, then password is "username:password"
|
||||||
ar.z = True
|
ar.z = True
|
||||||
ctl = Ctl(ar, ctl.stats)
|
ctl = Ctl(ar, ctl.stats)
|
||||||
|
|
||||||
if links:
|
|
||||||
print()
|
|
||||||
print("\n".join(links))
|
|
||||||
if linkfile:
|
|
||||||
linkfile.close()
|
|
||||||
|
|
||||||
if ctl.errs:
|
if ctl.errs:
|
||||||
print("WARNING: %d errors" % (ctl.errs))
|
print("WARNING: %d errors" % (ctl.errs))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,6 @@ def rep_server():
|
||||||
print("copyparty says %r" % (sck.recv_string(),))
|
print("copyparty says %r" % (sck.recv_string(),))
|
||||||
reply = b"thx"
|
reply = b"thx"
|
||||||
# reply = b"return 1" # non-zero to block an upload
|
# reply = b"return 1" # non-zero to block an upload
|
||||||
# reply = b'{"rc":1}' # or as json, that's fine too
|
|
||||||
# reply = b'{"rejectmsg":"naw dude"}' # or custom message
|
|
||||||
sck.send(reply)
|
sck.send(reply)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,6 @@
|
||||||
* works on windows, linux and macos
|
* works on windows, linux and macos
|
||||||
* assumes `copyparty-sfx.py` was renamed to `copyparty.py` in the same folder as `copyparty.bat`
|
* assumes `copyparty-sfx.py` was renamed to `copyparty.py` in the same folder as `copyparty.bat`
|
||||||
|
|
||||||
### [`setup-ashell.sh`](setup-ashell.sh)
|
|
||||||
* run copyparty on an iPhone/iPad using [a-Shell](https://holzschu.github.io/a-Shell_iOS/)
|
|
||||||
* not very useful due to limitations in iOS:
|
|
||||||
* not able to share all of your phone's storage
|
|
||||||
* cannot run in the background
|
|
||||||
|
|
||||||
### [`index.html`](index.html)
|
### [`index.html`](index.html)
|
||||||
* drop-in redirect from an httpd to copyparty
|
* drop-in redirect from an httpd to copyparty
|
||||||
* assumes the webserver and copyparty is running on the same server/IP
|
* assumes the webserver and copyparty is running on the same server/IP
|
||||||
|
|
@ -56,9 +50,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
|
||||||
|
|
|
||||||
|
|
@ -2,38 +2,19 @@
|
||||||
# not accept more consecutive clients than what copyparty is able to;
|
# not accept more consecutive clients than what copyparty is able to;
|
||||||
# nginx default is 512 (worker_processes 1, worker_connections 512)
|
# nginx default is 512 (worker_processes 1, worker_connections 512)
|
||||||
#
|
#
|
||||||
# ======================================================================
|
|
||||||
#
|
|
||||||
# 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
|
# rarely, in some extreme usecases, it can be good to add -j0
|
||||||
# (40'000 requests per second, or 20gbps upload/download in parallel)
|
# (40'000 requests per second, or 20gbps upload/download in parallel)
|
||||||
# but this is usually counterproductive and slightly buggy
|
# 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 uncommenting the cloudflare-only.conf line
|
# and then enable it below by uncomenting the cloudflare-only.conf line
|
||||||
#
|
|
||||||
# ======================================================================
|
|
||||||
|
|
||||||
|
|
||||||
upstream cpp_tcp {
|
upstream cpp_tcp {
|
||||||
|
|
@ -85,13 +66,13 @@ server {
|
||||||
proxy_buffer_size 16k;
|
proxy_buffer_size 16k;
|
||||||
proxy_busy_buffers_size 24k;
|
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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -48,24 +43,19 @@ let
|
||||||
|
|
||||||
accountsWithPlaceholders = mapAttrs (name: attrs: passwordPlaceholder name);
|
accountsWithPlaceholders = mapAttrs (name: attrs: passwordPlaceholder name);
|
||||||
|
|
||||||
volumesWithoutVariables = filterAttrs (k: v: !(hasInfix "\${" v.path)) cfg.volumes;
|
|
||||||
|
|
||||||
configStr = ''
|
configStr = ''
|
||||||
${mkSection "global" cfg.settings}
|
${mkSection "global" cfg.settings}
|
||||||
${cfg.globalExtraConfig}
|
|
||||||
${mkSection "accounts" (accountsWithPlaceholders cfg.accounts)}
|
${mkSection "accounts" (accountsWithPlaceholders cfg.accounts)}
|
||||||
${mkSection "groups" cfg.groups}
|
|
||||||
${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";
|
||||||
|
|
||||||
|
|
@ -78,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;
|
||||||
|
|
@ -118,47 +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};
|
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
globalExtraConfig = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "";
|
|
||||||
description = "Appended to the end of the [global] section verbatim. This is useful for flags which are used in a repeating manner (e.g. ipu: 255.255.255.1=user) which can't be repeated in the settings = {} attribute set.";
|
|
||||||
};
|
|
||||||
|
|
||||||
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.
|
||||||
'';
|
'';
|
||||||
|
|
@ -170,95 +117,75 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
groups = mkOption {
|
|
||||||
type = types.attrsOf (types.listOf types.str);
|
|
||||||
description = ''
|
|
||||||
A set of copyparty groups to create and the users that should be part of each group.
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
example = literalExpression ''
|
|
||||||
{
|
|
||||||
group_name = [ "user1" "user2" ];
|
|
||||||
};
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
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 ''
|
||||||
|
|
@ -277,136 +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) volumesWithoutVariables);
|
|
||||||
# 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 = ":${
|
(mapAttrsToList replaceSecretCommand cfg.accounts)}
|
||||||
# Use volume permissions if set
|
'';
|
||||||
if (value.flags ? chmod_d) then
|
|
||||||
value.flags.chmod_d
|
|
||||||
# Else, use global permission if set
|
|
||||||
else if (cfg.settings ? chmod-d) then
|
|
||||||
cfg.settings.chmod-d
|
|
||||||
# Else, use the default permission
|
|
||||||
else
|
|
||||||
"755"
|
|
||||||
}";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
) volumesWithoutVariables
|
|
||||||
);
|
|
||||||
|
|
||||||
users.groups = lib.mkIf (cfg.group == "copyparty") {
|
serviceConfig = {
|
||||||
copyparty = { };
|
Type = "simple";
|
||||||
};
|
ExecStart = "${getExe cfg.package} -c ${runtimeConfigPath}";
|
||||||
users.users = lib.mkIf (cfg.user == "copyparty") {
|
|
||||||
copyparty = {
|
|
||||||
description = "Service user for copyparty";
|
|
||||||
group = cfg.group;
|
|
||||||
home = externalStateDir;
|
|
||||||
isSystemUser = 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
|
# Hardening options
|
||||||
'')
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
users.groups.copyparty = { };
|
||||||
|
users.users.copyparty = {
|
||||||
|
description = "Service user for copyparty";
|
||||||
|
group = "copyparty";
|
||||||
|
home = home;
|
||||||
|
isSystemUser = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,57 @@
|
||||||
# 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.21"
|
pkgver="1.16.9"
|
||||||
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-pyzmq: send zeromq messages from event-hooks"
|
||||||
|
"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=("44723a823f218e52aaec6075695973a75b8663c9202c80fd73f48e52c61acd42")
|
sha256sums=("3e8f3c24c699aa41e0d51db6d781e453979c77abc34c919063b5bddd64d27bb0")
|
||||||
|
|
||||||
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 "┗━━━━━━━━━━━━━━━──-"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ Environment=XDG_CONFIG_HOME=/home/cpp/.config
|
||||||
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
||||||
|
|
||||||
# run copyparty
|
# run copyparty
|
||||||
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty -c /etc/copyparty.d/init
|
ExecStart=/usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
# Contributor: Beethoven <beethovenisadog@protonmail.com>
|
|
||||||
|
|
||||||
|
|
||||||
pkgname=copyparty
|
|
||||||
pkgver=1.19.21
|
|
||||||
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=("44723a823f218e52aaec6075695973a75b8663c9202c80fd73f48e52c61acd42")
|
|
||||||
|
|
||||||
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,139 +1,45 @@
|
||||||
{
|
{ lib, stdenv, makeWrapper, fetchurl, utillinux, python, jinja2, impacket, pyftpdlib, pyopenssl, argon2-cffi, pillow, pyvips, pyzmq, 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
|
# send ZeroMQ messages from event-hooks
|
||||||
withZeroMQ ? true,
|
withZeroMQ ? true,
|
||||||
|
|
||||||
# enable FTP server
|
# enable FTPS support in the FTP server
|
||||||
withFTP ? true,
|
withFTPS ? false,
|
||||||
|
|
||||||
# enable FTPS support in the FTP server
|
# samba/cifs server; dangerous and buggy, enable if you really need it
|
||||||
withFTPS ? false,
|
withSMB ? 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: [ ]),
|
|
||||||
|
|
||||||
# to build stable + unstable with the same file
|
|
||||||
stable ? true,
|
|
||||||
|
|
||||||
# for commit date, only used when stable = false
|
|
||||||
copypartyFlake ? null,
|
|
||||||
|
|
||||||
nix-gitignore,
|
|
||||||
}:
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
pinData = lib.importJSON ./pin.json;
|
pinData = lib.importJSON ./pin.json;
|
||||||
runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg);
|
pyEnv = python.withPackages (ps:
|
||||||
inherit (copypartyFlake) lastModifiedDate;
|
with ps; [
|
||||||
# ex: "1970" "01" "01"
|
|
||||||
dateStringsZeroPrefixed = {
|
|
||||||
year = builtins.substring 0 4 lastModifiedDate;
|
|
||||||
month = builtins.substring 4 2 lastModifiedDate;
|
|
||||||
day = builtins.substring 6 2 lastModifiedDate;
|
|
||||||
};
|
|
||||||
# ex: "1970" "1" "1"
|
|
||||||
dateStringsShort = builtins.mapAttrs (_: val: toString (lib.toIntBase10 val)) dateStringsZeroPrefixed;
|
|
||||||
unstableVersion =
|
|
||||||
if copypartyFlake == null then
|
|
||||||
"${pinData.version}-unstable"
|
|
||||||
else
|
|
||||||
with dateStringsZeroPrefixed; "${pinData.version}-unstable-${year}-${month}-${day}"
|
|
||||||
;
|
|
||||||
version = if stable then pinData.version else unstableVersion;
|
|
||||||
stableSrc = fetchurl {
|
|
||||||
inherit (pinData) url hash;
|
|
||||||
};
|
|
||||||
root = ../../../..;
|
|
||||||
unstableSrc = nix-gitignore.gitignoreSource [] root;
|
|
||||||
src = if stable then stableSrc else unstableSrc;
|
|
||||||
rev = copypartyFlake.shortRev or copypartyFlake.dirtyShortRev or "unknown";
|
|
||||||
unstableCodename = "unstable" + (lib.optionalString (copypartyFlake != null) "-${rev}");
|
|
||||||
in
|
|
||||||
buildPythonApplication {
|
|
||||||
pname = "copyparty";
|
|
||||||
inherit version src;
|
|
||||||
postPatch = lib.optionalString (!stable) ''
|
|
||||||
old_src="$(mktemp -d)"
|
|
||||||
tar -C "$old_src" -xf ${stableSrc}
|
|
||||||
declare -a folders
|
|
||||||
folders=("$old_src"/*)
|
|
||||||
count_folders="''${#folders[@]}"
|
|
||||||
if [[ $count_folders != 1 ]]; then
|
|
||||||
declare -p folders
|
|
||||||
echo "Expected 1 folder, found $count_folders" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
old_src_folder="''${folders[0]}"
|
|
||||||
cp -r "$old_src_folder"/copyparty/web/deps copyparty/web/deps
|
|
||||||
sed -i 's/^CODENAME =.*$/CODENAME = "${unstableCodename}"/' copyparty/__version__.py
|
|
||||||
${lib.optionalString (copypartyFlake != null) (with dateStringsShort; ''
|
|
||||||
sed -i 's/^BUILD_DT =.*$/BUILD_DT = (${year}, ${month}, ${day})/' copyparty/__version__.py
|
|
||||||
'')}
|
|
||||||
'';
|
|
||||||
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
|
||||||
|
|
@ -141,24 +47,21 @@ buildPythonApplication {
|
||||||
++ lib.optional withBasicAudioMetadata mutagen
|
++ lib.optional withBasicAudioMetadata mutagen
|
||||||
++ lib.optional withHashedPasswords argon2-cffi
|
++ lib.optional withHashedPasswords argon2-cffi
|
||||||
++ lib.optional withZeroMQ pyzmq
|
++ lib.optional withZeroMQ pyzmq
|
||||||
++ lib.optional withMagic magic
|
);
|
||||||
++ (extraPythonPackages python.pkgs);
|
in stdenv.mkDerivation {
|
||||||
makeWrapperArgs = [ "--prefix PATH : ${lib.makeBinPath runtimeDeps}" ];
|
pname = "copyparty";
|
||||||
|
version = pinData.version;
|
||||||
pyproject = true;
|
src = fetchurl {
|
||||||
build-system = [
|
url = pinData.url;
|
||||||
setuptools
|
hash = pinData.hash;
|
||||||
];
|
|
||||||
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.21/copyparty-1.19.21.tar.gz",
|
"url": "https://github.com/9001/copyparty/releases/download/v1.16.9/copyparty-sfx.py",
|
||||||
"version": "1.19.21",
|
"version": "1.16.9",
|
||||||
"hash": "sha256-RHI6gj8hjlKq7GB1aVlzp1uGY8kgLID9c/SOUsYazUI="
|
"hash": "sha256-456L3IHzf8ups3L9pTBZJMQjML8AlsQI66HZohDyEIA="
|
||||||
}
|
}
|
||||||
|
|
@ -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,31 +0,0 @@
|
||||||
final: prev:
|
|
||||||
let
|
|
||||||
fullAttrs = {
|
|
||||||
withHashedPasswords = true;
|
|
||||||
withCertgen = true;
|
|
||||||
withThumbnails = true;
|
|
||||||
withFastThumbnails = true;
|
|
||||||
withMediaProcessing = true;
|
|
||||||
withBasicAudioMetadata = true;
|
|
||||||
withZeroMQ = true;
|
|
||||||
withFTP = true;
|
|
||||||
withFTPS = true;
|
|
||||||
withTFTP = true;
|
|
||||||
withSMB = true;
|
|
||||||
withMagic = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
call = attrs: final.python3.pkgs.callPackage ./copyparty ({ ffmpeg = final.ffmpeg-full; } // attrs);
|
|
||||||
in
|
|
||||||
{
|
|
||||||
copyparty = call { stable = true; };
|
|
||||||
copyparty-unstable = call { stable = false; };
|
|
||||||
copyparty-full = call (fullAttrs // { stable = true; });
|
|
||||||
copyparty-unstable-full = call (fullAttrs // { stable = false; });
|
|
||||||
|
|
||||||
python3 = prev.python3.override {
|
|
||||||
packageOverrides = pyFinal: pyPrev: {
|
|
||||||
partftpy = pyFinal.callPackage ./partftpy { };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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,7 +15,6 @@ 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
|
||||||
|
|
||||||
|
|
@ -39,9 +38,3 @@ point `--css-browser` to one of these by URL:
|
||||||
|
|
||||||
* turns copyparty into chromecast just more flexible (and probably way more buggy)
|
* turns copyparty into chromecast just more flexible (and probably way more buggy)
|
||||||
* usage: put the js somewhere in the webroot and `--js-browser /memes/meadup.js`
|
* usage: put the js somewhere in the webroot and `--js-browser /memes/meadup.js`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# junk
|
|
||||||
|
|
||||||
* [**rave.js**](./rave.js): april-fools joke, [demo (epilepsy warning)](https://cd.ocv.me/b/d2/d21/#af-9b927c42,sorthref), not maintained, very buggy
|
|
||||||
|
|
@ -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 (anymod(e))
|
|
||||||
return main_hotkey_handler(e); // let copyparty handle this keystroke
|
|
||||||
|
|
||||||
var keycode = (e.key || e.code) + '',
|
|
||||||
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 (keycode == 'w' || keycode == '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,173 +0,0 @@
|
||||||
# copyparty with Podman and Systemd
|
|
||||||
|
|
||||||
Use this configuration if you want to run copyparty in a Podman container, with the reliability of running the container under a systemd service.
|
|
||||||
|
|
||||||
Documentation for `.container` files can be found in the [Container unit](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#container-units-container) docs. Systemd does not understand `.container` files natively, so Podman converts these to `.service` files with a [systemd-generator](https://www.freedesktop.org/software/systemd/man/latest/systemd.generator.html). This process is transparent, but sometimes needs to be debugged in case your `.container` file is malformed. There are instructions to debug the systemd generator in the Troubleshooting section below.
|
|
||||||
|
|
||||||
To run copyparty in this way, you must already have podman installed. To install Podman, see: https://podman.io/docs/installation
|
|
||||||
|
|
||||||
There is a sample configuration file in the same directory as this file (`copyparty.conf`).
|
|
||||||
|
|
||||||
## Run the container as root
|
|
||||||
|
|
||||||
Running the container as the root user is easy to set up, but less secure. There are instructions in the next section to run the container as a rootless user if you'd rather run the container like that.
|
|
||||||
|
|
||||||
First, change this line in the `copyparty.container` file to reflect the directory you want to share. By default, it shares `/mnt/` but you'll probably want to change that.
|
|
||||||
|
|
||||||
```
|
|
||||||
# Change /mnt to something you want to share
|
|
||||||
Volume=/mnt:/w:z
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that you can select the owner and group of this volume by changing the `uid:` and `gid:` of the volume in `copyparty.conf`, but for simplicity let's assume you want it to be owned by `root:root`.
|
|
||||||
|
|
||||||
To install and start copyparty with Podman and systemd as the root user, run the following:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
sudo mkdir -pv /etc/systemd/container/ /etc/copyparty/
|
|
||||||
sudo cp -v copyparty.container /etc/systemd/containers/
|
|
||||||
sudo cp -v copyparty.conf /etc/copyparty/
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl start copyparty
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: You can't "enable" this kind of Podman service. The `[Install]` section of the `.container` file effectively handles enabling the service so that it starts when the server reboots.
|
|
||||||
|
|
||||||
You can see the status of the service with:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
sudo systemctl status -a copyparty
|
|
||||||
```
|
|
||||||
|
|
||||||
You can see (and follow) the logs with either of these commands:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
sudo podman logs -f copyparty
|
|
||||||
|
|
||||||
# -a is required or else you'll get output like: copyparty[549025]: [649B blob data]
|
|
||||||
sudo journalctl -a -f -u copyparty
|
|
||||||
```
|
|
||||||
|
|
||||||
## Run the container as a non-root user
|
|
||||||
|
|
||||||
This configuration is more secure, but is more involved and requires ensuring files have proper permissions. You will need a root user account to do some of this setup.
|
|
||||||
|
|
||||||
First, you need a user to run the container as. In this example we'll create a "podman" user with UID=1001 and GID=1001.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
sudo groupadd -g 1001 podman
|
|
||||||
sudo useradd -u 1001 -m podman
|
|
||||||
sudo usermod -aG podman podman
|
|
||||||
sudo loginctl enable-linger podman
|
|
||||||
# Set a strong password for this user
|
|
||||||
sudo -u podman passwd
|
|
||||||
```
|
|
||||||
|
|
||||||
The `enable-linger` command allows the podman user to run systemd user services that persist even when the user is not logged in. You could use a user that already exists in the system to run this service as, just make sure to run `loginctl enable-linger USERNAME` for that user.
|
|
||||||
|
|
||||||
Next, change these lines in the `copyparty.container` file to reflect the config directory and the directory you want to share. By default, the config shares `/home/podman/copyparty/sharing/` but you'll probably want to change this:
|
|
||||||
|
|
||||||
```
|
|
||||||
# Change to reflect your non-root user's home directory
|
|
||||||
Volume=/home/podman/copyparty/config:/cfg:z
|
|
||||||
|
|
||||||
# Change to the directory you want to share
|
|
||||||
Volume=/home/podman/copyparty/sharing:/w:z
|
|
||||||
```
|
|
||||||
|
|
||||||
Make sure the podman user has read/write access to both of these directories.
|
|
||||||
|
|
||||||
Next, **log in to the server as the podman user**.
|
|
||||||
|
|
||||||
To install and start copyparty as the non-root podman user, run the following:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
mkdir -pv /home/podman/.config/containers/systemd/ /home/podman/copyparty/config
|
|
||||||
cp -v copyparty.container /home/podman/.config/containers/systemd/copyparty.container
|
|
||||||
cp -v copyparty.conf /home/podman/copyparty/config
|
|
||||||
systemctl --user daemon-reload
|
|
||||||
systemctl --user start copyparty
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important note: Never use `sudo` with `systemctl --user`!**
|
|
||||||
|
|
||||||
You can check the status of the user service with:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
systemctl --user status -a copyparty
|
|
||||||
```
|
|
||||||
|
|
||||||
You can see (and follow) the logs with:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
podman logs -f copyparty
|
|
||||||
|
|
||||||
journalctl --user -a -f -u copyparty
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
If the container fails to start, and you've modified the `.container` service, it's likely that your `.container` file failed to be translated into a `.service` file. You can debug the podman service generator with this command:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
sudo /usr/lib/systemd/system-generators/podman-system-generator --dryrun
|
|
||||||
```
|
|
||||||
|
|
||||||
## Allowing Traffic from Outside your Server
|
|
||||||
|
|
||||||
To allow traffic on port 3923 of your server, you should run:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
sudo firewall-cmd --permanent --add-port=3923/tcp
|
|
||||||
sudo firewall-cmd --reload
|
|
||||||
```
|
|
||||||
|
|
||||||
Otherwise, you won't be able to access the copyparty server from anywhere other than the server itself.
|
|
||||||
|
|
||||||
## Updating copyparty
|
|
||||||
|
|
||||||
To update the version of copyparty used in the container, you can:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# If root:
|
|
||||||
sudo podman pull docker.io/copyparty/ac:latest
|
|
||||||
sudo systemctl restart copyparty
|
|
||||||
|
|
||||||
# If non-root:
|
|
||||||
podman pull docker.io/copyparty/ac:latest
|
|
||||||
systemctl --user restart copyparty
|
|
||||||
```
|
|
||||||
|
|
||||||
Or, you can change the pinned version of the image in the `[Container]` section of the `.container` file and run:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# If root:
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl restart copyparty
|
|
||||||
|
|
||||||
# If non-root:
|
|
||||||
systemctl --user daemon-reload
|
|
||||||
systemctl --user restart copyparty
|
|
||||||
```
|
|
||||||
|
|
||||||
Podman will pull the image you've specified when restarting. If you have it set to `:latest`, Podman does not know to re-pull the container.
|
|
||||||
|
|
||||||
### Enabling auto-update
|
|
||||||
|
|
||||||
Alternatively, you can enable auto-updates by un-commenting this line:
|
|
||||||
|
|
||||||
```
|
|
||||||
# AutoUpdate=registry
|
|
||||||
```
|
|
||||||
|
|
||||||
You will also need to enable the [podman auto-updater service](https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html) with:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# If root:
|
|
||||||
sudo systemctl enable podman-auto-update.timer podman-auto-update.service
|
|
||||||
|
|
||||||
# If non-root:
|
|
||||||
systemctl --user enable podman-auto-update.timer podman-auto-update.service
|
|
||||||
```
|
|
||||||
|
|
||||||
This works best if you always want the latest version of copyparty. The auto-updater runs once every 24 hours.
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
[global]
|
|
||||||
e2dsa # enable file indexing and filesystem scanning
|
|
||||||
e2ts # and enable multimedia indexing
|
|
||||||
ansi # and colors in log messages
|
|
||||||
|
|
||||||
# uncomment the line starting with q, lo: to log to a file instead of stdout/journalctl;
|
|
||||||
# $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
|
|
||||||
/w # share the contents of the "/w" folder
|
|
||||||
accs:
|
|
||||||
rw: * # everyone gets read-write access, but
|
|
||||||
rwmda: ed # the user "ed" gets read-write-move-delete-admin
|
|
||||||
# uid: 1000 # If you're running as root, you can change the owner of this volume here
|
|
||||||
# gid: 1000 # If you're running as root, you can change the group of this volume here
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
[Container]
|
|
||||||
# It's recommended to replace :latest with a specific version
|
|
||||||
# for example: docker.io/copyparty/ac:1.19.15
|
|
||||||
Image=docker.io/copyparty/ac:latest
|
|
||||||
ContainerName=copyparty
|
|
||||||
|
|
||||||
# Uncomment to enable auto-updates
|
|
||||||
# AutoUpdate=registry
|
|
||||||
|
|
||||||
# Environment variables
|
|
||||||
# enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram)
|
|
||||||
Environment=LD_PRELOAD=/usr/lib/libmimalloc-secure.so.NOPE
|
|
||||||
# ensures log-messages are not delayed (but can reduce speed a tiny bit)
|
|
||||||
Environment=PYTHONUNBUFFERED=1
|
|
||||||
|
|
||||||
# Ports
|
|
||||||
PublishPort=3923:3923
|
|
||||||
|
|
||||||
|
|
||||||
# Volumes (PLEASE LOOK!)
|
|
||||||
|
|
||||||
# Rootful setup:
|
|
||||||
# Leave as-is
|
|
||||||
# Non-root setup:
|
|
||||||
# Change /etc/copyparty to /home/<USER>/copyparty/config
|
|
||||||
Volume=/etc/copyparty:/cfg:z
|
|
||||||
|
|
||||||
# Rootful setup:
|
|
||||||
# Change /mnt to the directory you want to share
|
|
||||||
# Non-root setup:
|
|
||||||
# Change /mnt to something owned by your user, e.g., /home/<USER>/copyparty/sharing:/w:z
|
|
||||||
Volume=/mnt:/w:z
|
|
||||||
|
|
||||||
|
|
||||||
# Give the container time to stop in case the thumbnailer is still running.
|
|
||||||
# It's allowed to continue finishing up for 10s after the shutdown signal, give it a 5s buffer
|
|
||||||
StopTimeout=15
|
|
||||||
|
|
||||||
# hide it from logs with "/._" so it matches the default --lf-url filter
|
|
||||||
HealthCmd="wget --spider -q 127.0.0.1:3923/?reset=/._"
|
|
||||||
HealthInterval=1m
|
|
||||||
HealthTimeout=2s
|
|
||||||
HealthRetries=5
|
|
||||||
HealthStartPeriod=15s
|
|
||||||
|
|
||||||
[Unit]
|
|
||||||
After=default.target
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
# Start by default on boot
|
|
||||||
WantedBy=default.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
# Give the container time to start in case it needs to pull the image
|
|
||||||
TimeoutStartSec=600
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# this script will install copyparty onto an iOS device (iPhone/iPad)
|
|
||||||
#
|
|
||||||
# step 1: install a-Shell:
|
|
||||||
# https://apps.apple.com/us/app/a-shell/id1473805438
|
|
||||||
#
|
|
||||||
# step 2: copypaste the following command into a-Shell:
|
|
||||||
# curl -L https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh
|
|
||||||
#
|
|
||||||
# step 3: launch copyparty with this command: cpp
|
|
||||||
#
|
|
||||||
# if you ever want to upgrade copyparty, just repeat step 2
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
cd "$HOME/Documents"
|
|
||||||
curl -Locopyparty https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# create the config file? (cannot use heredoc because body too large)
|
|
||||||
[ -e cpc ] || {
|
|
||||||
echo '[global]' >cpc
|
|
||||||
echo ' p: 80, 443, 3923 # enable http and https on these ports' >>cpc
|
|
||||||
echo ' e2dsa # enable file indexing and filesystem scanning' >>cpc
|
|
||||||
echo ' e2ts # and enable multimedia indexing' >>cpc
|
|
||||||
echo ' ver # show copyparty version in the controlpanel' >>cpc
|
|
||||||
echo ' qrz: 2 # enable qr-code and make it big' >>cpc
|
|
||||||
echo ' qrp: 1 # reduce qr-code padding' >>cpc
|
|
||||||
echo ' qr-fg: -1 # optimize for basic/simple terminals' >>cpc
|
|
||||||
echo ' qr-wait: 0.3 # less chance of getting scrolled away' >>cpc
|
|
||||||
echo '' >>cpc
|
|
||||||
echo ' # enable these by uncommenting them:' >>cpc
|
|
||||||
echo ' # ftp: 21 # enable ftp server on port 21' >>cpc
|
|
||||||
echo ' # tftp: 69 # enable tftp server on port 69' >>cpc
|
|
||||||
echo '' >>cpc
|
|
||||||
echo '[/]' >>cpc
|
|
||||||
echo ' ~/Documents' >>cpc
|
|
||||||
echo ' accs:' >>cpc
|
|
||||||
echo ' A: *' >>cpc
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# create the launcher?
|
|
||||||
[ -e cpp ] || {
|
|
||||||
echo '#!/bin/sh' >cpp
|
|
||||||
echo '' >>cpp
|
|
||||||
echo '# change the font so the qr-code draws correctly:' >>cpp
|
|
||||||
echo 'config -n "Menlo" # name' >>cpp
|
|
||||||
echo 'config -s 8 # size' >>cpp
|
|
||||||
echo '' >>cpp
|
|
||||||
echo '# launch copyparty' >>cpp
|
|
||||||
echo 'exec copyparty -c cpc "$@"' >>cpp
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
chmod 755 copyparty cpp
|
|
||||||
echo
|
|
||||||
echo =================================
|
|
||||||
echo
|
|
||||||
echo 'okay, all done!'
|
|
||||||
echo
|
|
||||||
echo 'you can edit your config'
|
|
||||||
echo 'with this command: vim cpc'
|
|
||||||
echo
|
|
||||||
echo 'you can run copyparty'
|
|
||||||
echo 'with this command: cpp'
|
|
||||||
echo
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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,18 +1,5 @@
|
||||||
# ./traefik --configFile=copyparty.yaml
|
# ./traefik --experimental.fastproxy=true --entrypoints.web.address=:8080 --providers.file.filename=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:
|
http:
|
||||||
services:
|
services:
|
||||||
service-cpp:
|
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()
|
|
||||||
|
|
@ -55,7 +55,7 @@ except:
|
||||||
zs = """
|
zs = """
|
||||||
web/a/partyfuse.py
|
web/a/partyfuse.py
|
||||||
web/a/u2c.py
|
web/a/u2c.py
|
||||||
web/a/webdav-cfg.txt
|
web/a/webdav-cfg.bat
|
||||||
web/baguettebox.js
|
web/baguettebox.js
|
||||||
web/browser.css
|
web/browser.css
|
||||||
web/browser.html
|
web/browser.html
|
||||||
|
|
@ -63,6 +63,10 @@ web/browser.js
|
||||||
web/browser2.html
|
web/browser2.html
|
||||||
web/cf.html
|
web/cf.html
|
||||||
web/copyparty.gif
|
web/copyparty.gif
|
||||||
|
web/dd/2.png
|
||||||
|
web/dd/3.png
|
||||||
|
web/dd/4.png
|
||||||
|
web/dd/5.png
|
||||||
web/deps/busy.mp3
|
web/deps/busy.mp3
|
||||||
web/deps/easymde.css
|
web/deps/easymde.css
|
||||||
web/deps/easymde.js
|
web/deps/easymde.js
|
||||||
|
|
@ -76,7 +80,6 @@ web/deps/prismd.css
|
||||||
web/deps/scp.woff2
|
web/deps/scp.woff2
|
||||||
web/deps/sha512.ac.js
|
web/deps/sha512.ac.js
|
||||||
web/deps/sha512.hw.js
|
web/deps/sha512.hw.js
|
||||||
web/idp.html
|
|
||||||
web/iiam.gif
|
web/iiam.gif
|
||||||
web/md.css
|
web/md.css
|
||||||
web/md.html
|
web/md.html
|
||||||
|
|
@ -88,7 +91,6 @@ web/mde.html
|
||||||
web/mde.js
|
web/mde.js
|
||||||
web/msg.css
|
web/msg.css
|
||||||
web/msg.html
|
web/msg.html
|
||||||
web/opds.xml
|
|
||||||
web/rups.css
|
web/rups.css
|
||||||
web/rups.html
|
web/rups.html
|
||||||
web/rups.js
|
web/rups.js
|
||||||
|
|
@ -100,45 +102,19 @@ web/splash.html
|
||||||
web/splash.js
|
web/splash.js
|
||||||
web/svcs.html
|
web/svcs.html
|
||||||
web/svcs.js
|
web/svcs.js
|
||||||
web/tl/chi.js
|
|
||||||
web/tl/cze.js
|
|
||||||
web/tl/deu.js
|
|
||||||
web/tl/epo.js
|
|
||||||
web/tl/fin.js
|
|
||||||
web/tl/fra.js
|
|
||||||
web/tl/grc.js
|
|
||||||
web/tl/ita.js
|
|
||||||
web/tl/kor.js
|
|
||||||
web/tl/nld.js
|
|
||||||
web/tl/nno.js
|
|
||||||
web/tl/nor.js
|
|
||||||
web/tl/pol.js
|
|
||||||
web/tl/por.js
|
|
||||||
web/tl/rus.js
|
|
||||||
web/tl/spa.js
|
|
||||||
web/tl/swe.js
|
|
||||||
web/tl/tur.js
|
|
||||||
web/tl/ukr.js
|
|
||||||
web/ui.css
|
web/ui.css
|
||||||
web/up2k.js
|
web/up2k.js
|
||||||
web/util.js
|
web/util.js
|
||||||
web/w.hash.js
|
web/w.hash.js
|
||||||
"""
|
"""
|
||||||
RES = set(zs.strip().split("\n"))
|
RES = set(zs.strip().split("\n"))
|
||||||
RESM = {
|
|
||||||
"web/a/partyfuse.txt": "web/a/partyfuse.py",
|
|
||||||
"web/a/u2c.txt": "web/a/u2c.py",
|
|
||||||
"web/a/webdav-cfg.bat": "web/a/webdav-cfg.txt",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class EnvParams(object):
|
class EnvParams(object):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.t0 = time.time()
|
self.t0 = time.time()
|
||||||
self.mod = ""
|
self.mod = ""
|
||||||
self.mod_ = ""
|
|
||||||
self.cfg = ""
|
self.cfg = ""
|
||||||
self.scfg = True
|
|
||||||
|
|
||||||
|
|
||||||
E = EnvParams()
|
E = EnvParams()
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,8 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
VERSION = (1, 19, 21)
|
VERSION = (1, 16, 10)
|
||||||
CODENAME = "usernames"
|
CODENAME = "COPYparty"
|
||||||
BUILD_DT = (2025, 12, 2)
|
BUILD_DT = (2025, 1, 25)
|
||||||
|
|
||||||
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)
|
||||||
|
|
|
||||||
1131
copyparty/authsrv.py
1131
copyparty/authsrv.py
File diff suppressed because it is too large
Load diff
|
|
@ -2,22 +2,15 @@
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import time
|
|
||||||
|
|
||||||
from ..util import SYMTIME, fsdec, fsenc
|
from ..util import SYMTIME, fsdec, fsenc
|
||||||
from . import path as path
|
from . import path as path
|
||||||
|
|
||||||
if True: # pylint: disable=using-constant-test
|
if True: # pylint: disable=using-constant-test
|
||||||
from typing import Any, Optional, Union
|
from typing import Any, Optional
|
||||||
|
|
||||||
from ..util import NamedLogger
|
_ = (path,)
|
||||||
|
__all__ = ["path"]
|
||||||
MKD_755 = {"chmod_d": 0o755}
|
|
||||||
MKD_700 = {"chmod_d": 0o700}
|
|
||||||
UTIME_CLAMPS = ((max, -2147483647), (max, 1), (min, 4294967294), (min, 2147483646))
|
|
||||||
|
|
||||||
_ = (path, MKD_755, MKD_700, UTIME_CLAMPS)
|
|
||||||
__all__ = ["path", "MKD_755", "MKD_700", "UTIME_CLAMPS"]
|
|
||||||
|
|
||||||
# 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/.$//')"
|
||||||
|
|
@ -27,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) or bname in todo:
|
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:
|
||||||
|
|
@ -103,44 +76,6 @@ def utime(
|
||||||
return os.utime(fsenc(p), times)
|
return os.utime(fsenc(p), times)
|
||||||
|
|
||||||
|
|
||||||
def utime_c(
|
|
||||||
log: Union["NamedLogger", Any],
|
|
||||||
p: str,
|
|
||||||
ts: int,
|
|
||||||
follow_symlinks: bool = True,
|
|
||||||
throw: bool = False,
|
|
||||||
) -> Optional[int]:
|
|
||||||
clamp = 0
|
|
||||||
ov = ts
|
|
||||||
bp = fsenc(p)
|
|
||||||
now = int(time.time())
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
if SYMTIME:
|
|
||||||
os.utime(bp, (now, ts), follow_symlinks=follow_symlinks)
|
|
||||||
else:
|
|
||||||
os.utime(bp, (now, ts))
|
|
||||||
if clamp:
|
|
||||||
t = "filesystem rejected utime(%r); clamped %s to %s"
|
|
||||||
log(t % (p, ov, ts))
|
|
||||||
return ts
|
|
||||||
except Exception as ex:
|
|
||||||
pv = ts
|
|
||||||
while clamp < len(UTIME_CLAMPS):
|
|
||||||
fun, cv = UTIME_CLAMPS[clamp]
|
|
||||||
ts = fun(ts, cv)
|
|
||||||
clamp += 1
|
|
||||||
if ts != pv:
|
|
||||||
break
|
|
||||||
if clamp >= len(UTIME_CLAMPS):
|
|
||||||
if throw:
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
t = "could not utime(%r) to %s; %s, %r"
|
|
||||||
log(t % (p, ov, ex, ex))
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
if hasattr(os, "lstat"):
|
if hasattr(os, "lstat"):
|
||||||
|
|
||||||
def lstat(p: str) -> os.stat_result:
|
def lstat(p: str) -> os.stat_result:
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ 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, NotExQueue
|
||||||
from .fsutil import ramdisk_chk
|
|
||||||
from .httpsrv import HttpSrv
|
from .httpsrv import HttpSrv
|
||||||
from .util import FAKE_MP, Daemon, HMaccas
|
from .util import FAKE_MP, Daemon, HMaccas
|
||||||
|
|
||||||
|
|
@ -57,7 +56,6 @@ class MpWorker(BrokerCli):
|
||||||
|
|
||||||
# starting to look like a good idea
|
# starting to look like a good idea
|
||||||
self.asrv = AuthSrv(args, None, False)
|
self.asrv = AuthSrv(args, None, False)
|
||||||
ramdisk_chk(self.asrv)
|
|
||||||
|
|
||||||
# instantiate all services here (TODO: inheritance?)
|
# instantiate all services here (TODO: inheritance?)
|
||||||
self.iphash = HMaccas(os.path.join(self.args.E.cfg, "iphash"), 8)
|
self.iphash = HMaccas(os.path.join(self.args.E.cfg, "iphash"), 8)
|
||||||
|
|
@ -101,7 +99,6 @@ class MpWorker(BrokerCli):
|
||||||
if dest == "reload":
|
if dest == "reload":
|
||||||
self.logw("mpw.asrv reloading")
|
self.logw("mpw.asrv reloading")
|
||||||
self.asrv.reload()
|
self.asrv.reload()
|
||||||
ramdisk_chk(self.asrv)
|
|
||||||
self.logw("mpw.asrv reloaded")
|
self.logw("mpw.asrv reloaded")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import traceback
|
||||||
|
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import calendar
|
import calendar
|
||||||
import errno
|
import errno
|
||||||
|
import filecmp
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
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, load_resource, runcmd, wrename, wunlink
|
||||||
|
|
||||||
HAVE_CFSSL = not os.environ.get("PRTY_NO_CFSSL")
|
HAVE_CFSSL = not os.environ.get("PRTY_NO_CFSSL")
|
||||||
|
|
||||||
|
|
@ -20,19 +21,6 @@ else:
|
||||||
VF = {"mv_re_t": 0, "rm_re_t": 0}
|
VF = {"mv_re_t": 0, "rm_re_t": 0}
|
||||||
|
|
||||||
|
|
||||||
def _sp_err(exe, what, rc, so, se, sin):
|
|
||||||
try:
|
|
||||||
zs = shutil.which(exe)
|
|
||||||
except:
|
|
||||||
zs = "<?>"
|
|
||||||
try:
|
|
||||||
zi = os.path.getsize(zs)
|
|
||||||
except:
|
|
||||||
zi = 0
|
|
||||||
t = "failed to %s; error %s using %s (%s):\n STDOUT: %s\n STDERR: %s\n STDIN: %s\n"
|
|
||||||
raise Exception(t % (what, rc, zs, zi, so, se, sin.decode("utf-8")))
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_cert(log: "RootLogger", args) -> None:
|
def ensure_cert(log: "RootLogger", args) -> None:
|
||||||
"""
|
"""
|
||||||
the default cert (and the entire TLS support) is only here to enable the
|
the default cert (and the entire TLS support) is only here to enable the
|
||||||
|
|
@ -121,20 +109,20 @@ def _gen_ca(log: "RootLogger", args):
|
||||||
cmd = "cfssl gencert -initca -"
|
cmd = "cfssl gencert -initca -"
|
||||||
rc, so, se = runcmd(cmd.split(), 30, sin=sin)
|
rc, so, se = runcmd(cmd.split(), 30, sin=sin)
|
||||||
if rc:
|
if rc:
|
||||||
_sp_err("cfssl", "create ca-cert", rc, so, se, sin)
|
raise Exception("failed to create ca-cert: {}, {}".format(rc, se), 3)
|
||||||
|
|
||||||
cmd = "cfssljson -bare ca"
|
cmd = "cfssljson -bare ca"
|
||||||
sin = so.encode("utf-8")
|
sin = so.encode("utf-8")
|
||||||
rc, so, se = runcmd(cmd.split(), 10, sin=sin, cwd=args.crt_dir)
|
rc, so, se = runcmd(cmd.split(), 10, sin=sin, cwd=args.crt_dir)
|
||||||
if rc:
|
if rc:
|
||||||
_sp_err("cfssljson", "translate ca-cert", rc, so, se, sin)
|
raise Exception("failed to translate ca-cert: {}, {}".format(rc, se), 3)
|
||||||
|
|
||||||
bname = os.path.join(args.crt_dir, "ca")
|
bname = os.path.join(args.crt_dir, "ca")
|
||||||
try:
|
try:
|
||||||
wunlink(nlog, bname + ".key", VF)
|
wunlink(nlog, bname + ".key", VF)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
atomic_move(nlog, bname + "-key.pem", bname + ".key", VF)
|
wrename(nlog, bname + "-key.pem", bname + ".key", VF)
|
||||||
wunlink(nlog, bname + ".csr", VF)
|
wunlink(nlog, bname + ".csr", VF)
|
||||||
|
|
||||||
log("cert", "new ca OK", 2)
|
log("cert", "new ca OK", 2)
|
||||||
|
|
@ -144,7 +132,6 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
||||||
nlog: "NamedLogger" = lambda msg, c=0: log("cert-gen-srv", msg, c)
|
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 []
|
||||||
names = [x.strip() for x in names]
|
|
||||||
if not args.crt_exact:
|
if not args.crt_exact:
|
||||||
for n in names[:]:
|
for n in names[:]:
|
||||||
names.append("*.{}".format(n))
|
names.append("*.{}".format(n))
|
||||||
|
|
@ -215,20 +202,20 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
||||||
acmd = cmd.split() + ["-hostname=" + ",".join(names), "-"]
|
acmd = cmd.split() + ["-hostname=" + ",".join(names), "-"]
|
||||||
rc, so, se = runcmd(acmd, 30, sin=sin, cwd=args.crt_dir)
|
rc, so, se = runcmd(acmd, 30, sin=sin, cwd=args.crt_dir)
|
||||||
if rc:
|
if rc:
|
||||||
_sp_err("cfssl", "create cert", rc, so, se, sin)
|
raise Exception("failed to create cert: {}, {}".format(rc, se))
|
||||||
|
|
||||||
cmd = "cfssljson -bare srv"
|
cmd = "cfssljson -bare srv"
|
||||||
sin = so.encode("utf-8")
|
sin = so.encode("utf-8")
|
||||||
rc, so, se = runcmd(cmd.split(), 10, sin=sin, cwd=args.crt_dir)
|
rc, so, se = runcmd(cmd.split(), 10, sin=sin, cwd=args.crt_dir)
|
||||||
if rc:
|
if rc:
|
||||||
_sp_err("cfssljson", "translate cert", rc, so, se, sin)
|
raise Exception("failed to translate cert: {}, {}".format(rc, se))
|
||||||
|
|
||||||
bname = os.path.join(args.crt_dir, "srv")
|
bname = os.path.join(args.crt_dir, "srv")
|
||||||
try:
|
try:
|
||||||
wunlink(nlog, bname + ".key", VF)
|
wunlink(nlog, bname + ".key", VF)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
atomic_move(nlog, bname + "-key.pem", bname + ".key", VF)
|
wrename(nlog, bname + "-key.pem", bname + ".key", VF)
|
||||||
wunlink(nlog, bname + ".csr", VF)
|
wunlink(nlog, 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:
|
||||||
|
|
|
||||||
160
copyparty/cfg.py
160
copyparty/cfg.py
|
|
@ -5,9 +5,6 @@ from __future__ import print_function, unicode_literals
|
||||||
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 nth 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"""
|
||||||
|
|
@ -19,18 +16,15 @@ def vf_bmap() -> dict[str, str]:
|
||||||
"no_clone": "noclone",
|
"no_clone": "noclone",
|
||||||
"no_dirsz": "nodirsz",
|
"no_dirsz": "nodirsz",
|
||||||
"no_dupe": "nodupe",
|
"no_dupe": "nodupe",
|
||||||
"no_dupe_m": "nodupem",
|
|
||||||
"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",
|
"dedup",
|
||||||
"dlni",
|
|
||||||
"dotsrch",
|
"dotsrch",
|
||||||
"e2d",
|
"e2d",
|
||||||
"e2ds",
|
"e2ds",
|
||||||
|
|
@ -46,35 +40,17 @@ def vf_bmap() -> dict[str, str]:
|
||||||
"gsel",
|
"gsel",
|
||||||
"hardlink",
|
"hardlink",
|
||||||
"magic",
|
"magic",
|
||||||
"md_no_br",
|
|
||||||
"no_db_ip",
|
|
||||||
"no_sb_md",
|
"no_sb_md",
|
||||||
"no_sb_lg",
|
"no_sb_lg",
|
||||||
"nsort",
|
"nsort",
|
||||||
"og",
|
"og",
|
||||||
"og_no_head",
|
"og_no_head",
|
||||||
"og_s_title",
|
"og_s_title",
|
||||||
"opds",
|
|
||||||
"rand",
|
"rand",
|
||||||
"reflink",
|
|
||||||
"rm_partial",
|
|
||||||
"rmagic",
|
|
||||||
"rss",
|
"rss",
|
||||||
"ui_noacci",
|
|
||||||
"ui_nocpla",
|
|
||||||
"ui_nolbar",
|
|
||||||
"ui_nombar",
|
|
||||||
"ui_nonav",
|
|
||||||
"ui_notree",
|
|
||||||
"ui_norepl",
|
|
||||||
"ui_nosrvi",
|
|
||||||
"ui_noctxb",
|
|
||||||
"wo_up_readme",
|
|
||||||
"wram",
|
|
||||||
"xdev",
|
"xdev",
|
||||||
"xlink",
|
"xlink",
|
||||||
"xvol",
|
"xvol",
|
||||||
"zipmaxu",
|
|
||||||
):
|
):
|
||||||
ret[k] = k
|
ret[k] = k
|
||||||
return ret
|
return ret
|
||||||
|
|
@ -83,7 +59,6 @@ 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",
|
||||||
|
|
@ -94,24 +69,14 @@ def vf_vmap() -> dict[str, str]:
|
||||||
"th_x3": "th3x",
|
"th_x3": "th3x",
|
||||||
}
|
}
|
||||||
for k in (
|
for k in (
|
||||||
"bup_ck",
|
|
||||||
"casechk",
|
|
||||||
"chmod_d",
|
|
||||||
"chmod_f",
|
|
||||||
"dbd",
|
"dbd",
|
||||||
"du_who",
|
|
||||||
"ufavico",
|
|
||||||
"forget_ip",
|
|
||||||
"hsortn",
|
"hsortn",
|
||||||
"html_head",
|
"html_head",
|
||||||
"html_head_s",
|
|
||||||
"lg_sbf",
|
"lg_sbf",
|
||||||
"md_sbf",
|
"md_sbf",
|
||||||
"lg_sba",
|
"lg_sba",
|
||||||
"md_sba",
|
"md_sba",
|
||||||
"md_hist",
|
|
||||||
"nrand",
|
"nrand",
|
||||||
"u2ow",
|
|
||||||
"og_desc",
|
"og_desc",
|
||||||
"og_site",
|
"og_site",
|
||||||
"og_th",
|
"og_th",
|
||||||
|
|
@ -121,31 +86,14 @@ def vf_vmap() -> dict[str, str]:
|
||||||
"og_title_i",
|
"og_title_i",
|
||||||
"og_tpl",
|
"og_tpl",
|
||||||
"og_ua",
|
"og_ua",
|
||||||
"opds_exts",
|
|
||||||
"put_ck",
|
|
||||||
"put_name",
|
|
||||||
"mv_retry",
|
"mv_retry",
|
||||||
"rm_retry",
|
"rm_retry",
|
||||||
"shr_who",
|
|
||||||
"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",
|
"ups_who",
|
||||||
"zip_who",
|
|
||||||
"zipmaxn",
|
|
||||||
"zipmaxs",
|
|
||||||
"zipmaxt",
|
|
||||||
):
|
):
|
||||||
ret[k] = k
|
ret[k] = k
|
||||||
return ret
|
return ret
|
||||||
|
|
@ -157,7 +105,6 @@ 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",
|
||||||
|
|
@ -196,27 +143,15 @@ flagcats = {
|
||||||
"dedup": "enable symlink-based file deduplication",
|
"dedup": "enable symlink-based file deduplication",
|
||||||
"hardlink": "enable hardlink-based file deduplication,\nwith fallback on symlinks when that is impossible",
|
"hardlink": "enable hardlink-based file deduplication,\nwith fallback on symlinks when that is impossible",
|
||||||
"hardlinkonly": "dedup with hardlink only, never symlink;\nmake a full copy if hardlink is impossible",
|
"hardlinkonly": "dedup with hardlink only, never symlink;\nmake a full copy if hardlink is impossible",
|
||||||
"reflink": "enable reflink-based file deduplication,\nwith fallback on full copy when that is impossible",
|
|
||||||
"safededup": "verify on-disk data before using it for dedup",
|
"safededup": "verify on-disk data before using it for dedup",
|
||||||
"noclone": "take dupe data from clients, even if available on HDD",
|
"noclone": "take dupe data from clients, even if available on HDD",
|
||||||
"nodupe": "rejects existing files (instead of linking/cloning them)",
|
"nodupe": "rejects existing files (instead of linking/cloning them)",
|
||||||
"nodupem": "rejects existing files during moves as well",
|
|
||||||
"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",
|
|
||||||
"wram": "allow uploading into ramdisks",
|
|
||||||
"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",
|
"nosparse": "deny use of sparse files, mainly for slow storage",
|
||||||
"rm_partial": "delete unfinished uploads from HDD when they timeout",
|
|
||||||
"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": {
|
||||||
|
|
@ -225,10 +160,8 @@ flagcats = {
|
||||||
"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)",
|
"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",
|
||||||
|
|
@ -237,7 +170,6 @@ flagcats = {
|
||||||
"upload rotation\n(moves all uploads into the specified folder structure)": {
|
"upload rotation\n(moves all uploads into the specified folder structure)": {
|
||||||
"rotn=100,3": "3 levels of subfolders with 100 entries in each",
|
"rotn=100,3": "3 levels of subfolders with 100 entries in each",
|
||||||
"rotf=%Y-%m/%d-%H": "date-formatted organizing",
|
"rotf=%Y-%m/%d-%H": "date-formatted organizing",
|
||||||
"rotf_tz=Europe/Oslo": "timezone (default=UTC)",
|
|
||||||
"lifetime=3600": "uploads are deleted after 1 hour",
|
"lifetime=3600": "uploads are deleted after 1 hour",
|
||||||
},
|
},
|
||||||
"database, general": {
|
"database, general": {
|
||||||
|
|
@ -246,27 +178,19 @@ flagcats = {
|
||||||
"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",
|
||||||
"casechk=auto": "actively prevent case-insensitive filesystem? y/n",
|
|
||||||
"xlink": "cross-volume dupe detection / linking (dangerous)",
|
"xlink": "cross-volume dupe detection / linking (dangerous)",
|
||||||
"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",
|
||||||
|
|
@ -275,8 +199,6 @@ flagcats = {
|
||||||
"srch_excl": "exclude search results with URL matching this regex",
|
"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",
|
||||||
},
|
},
|
||||||
|
|
@ -289,10 +211,7 @@ flagcats = {
|
||||||
"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",
|
||||||
|
|
@ -315,20 +234,10 @@ flagcats = {
|
||||||
"grid": "show grid/thumbnails by default",
|
"grid": "show grid/thumbnails by default",
|
||||||
"gsel": "select files in grid by ctrl-click",
|
"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",
|
|
||||||
"ufavico=URL": "per-volume favicon (.ico/png/gif/svg)",
|
|
||||||
"unlist": "dont list files matching REGEX",
|
"unlist": "dont list files matching REGEX",
|
||||||
"dlni": "force-download (no-inline) files on click",
|
|
||||||
"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",
|
||||||
"html_head_s=TXT": "additional static text in the html <head>",
|
|
||||||
"tcolor=#fc0": "theme color (a hint for webbrowsers, discord, etc.)",
|
|
||||||
"nodirsz": "don't show total folder size",
|
|
||||||
"du_who=all": "show disk-usage info to everyone",
|
|
||||||
"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)",
|
||||||
|
|
@ -338,67 +247,11 @@ flagcats = {
|
||||||
"md_sba": "value of iframe allow-prop for markdown-sandbox",
|
"md_sba": "value of iframe allow-prop for markdown-sandbox",
|
||||||
"lg_sba": "value of iframe allow-prop for *logue-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",
|
||||||
"ui_noacci": "hide account-info in the UI",
|
|
||||||
"ui_nocpla": "hide cpanel-link in the UI",
|
|
||||||
"ui_nolbar": "hide link-bar in the UI",
|
|
||||||
"ui_nombar": "hide top-menu in the UI",
|
|
||||||
"ui_nonav": "hide navpane+breadcrumbs in the UI",
|
|
||||||
"ui_notree": "hide navpane in the UI",
|
|
||||||
"ui_norepl": "hide repl-button in the UI",
|
|
||||||
"ui_nosrvi": "hide server-info in the UI",
|
|
||||||
"ui_noctxb": "hide context-buttons in the UI",
|
|
||||||
},
|
|
||||||
"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",
|
|
||||||
},
|
|
||||||
"opds": {
|
|
||||||
"opds": "enable OPDS",
|
|
||||||
"opds_exts": "file formats to list in OPDS feeds; leave empty to show everything",
|
|
||||||
},
|
|
||||||
"textfiles": {
|
|
||||||
"md_no_br": "newline only on double-newline or two tailing spaces",
|
|
||||||
"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",
|
|
||||||
"shr_who=auth": "who can create shares? no/auth/a",
|
|
||||||
"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",
|
||||||
|
|
@ -408,10 +261,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)
|
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,6 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from .__init__ import ANYWIN, MACOS
|
from .__init__ import ANYWIN, MACOS
|
||||||
from .authsrv import AXS, VFS, AuthSrv
|
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, undot
|
||||||
|
|
||||||
|
|
@ -18,25 +18,22 @@ if True: # pylint: disable=using-constant-test
|
||||||
|
|
||||||
|
|
||||||
class Fstab(object):
|
class Fstab(object):
|
||||||
def __init__(self, log: "RootLogger", args: argparse.Namespace, verbose: bool):
|
def __init__(self, log: "RootLogger", args: argparse.Namespace):
|
||||||
self.log_func = log
|
self.log_func = log
|
||||||
self.verbose = verbose
|
|
||||||
|
|
||||||
self.warned = False
|
self.warned = False
|
||||||
self.trusted = False
|
self.trusted = False
|
||||||
self.tab: Optional[VFS] = None
|
self.tab: Optional[VFS] = None
|
||||||
self.oldtab: Optional[VFS] = None
|
self.oldtab: Optional[VFS] = None
|
||||||
self.srctab = "a"
|
self.srctab = "a"
|
||||||
self.cache: dict[str, tuple[str, str]] = {}
|
self.cache: dict[str, str] = {}
|
||||||
self.age = 0.0
|
self.age = 0.0
|
||||||
self.maxage = args.mtab_age
|
self.maxage = args.mtab_age
|
||||||
|
|
||||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||||
if not c or self.verbose:
|
|
||||||
return
|
|
||||||
self.log_func("fstab", msg, c)
|
self.log_func("fstab", msg, c)
|
||||||
|
|
||||||
def get(self, path: str) -> tuple[str, str]:
|
def get(self, path: str) -> str:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now - self.age > self.maxage or len(self.cache) > 9000:
|
if now - self.age > self.maxage or len(self.cache) > 9000:
|
||||||
self.age = now
|
self.age = now
|
||||||
|
|
@ -44,7 +41,6 @@ class Fstab(object):
|
||||||
self.tab = None
|
self.tab = None
|
||||||
self.cache = {}
|
self.cache = {}
|
||||||
|
|
||||||
mp = ""
|
|
||||||
fs = "ext4"
|
fs = "ext4"
|
||||||
msg = "failed to determine filesystem at %r; assuming %s\n%s"
|
msg = "failed to determine filesystem at %r; assuming %s\n%s"
|
||||||
|
|
||||||
|
|
@ -54,7 +50,7 @@ class Fstab(object):
|
||||||
path = self._winpath(path)
|
path = self._winpath(path)
|
||||||
except:
|
except:
|
||||||
self.log(msg % (path, fs, min_ex()), 3)
|
self.log(msg % (path, fs, min_ex()), 3)
|
||||||
return fs, ""
|
return fs
|
||||||
|
|
||||||
path = undot(path)
|
path = undot(path)
|
||||||
try:
|
try:
|
||||||
|
|
@ -63,14 +59,14 @@ class Fstab(object):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fs, mp = 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 % (path, fs, min_ex()), 3)
|
||||||
|
|
||||||
fs = fs.lower()
|
fs = fs.lower()
|
||||||
self.cache[path] = (fs, mp)
|
self.cache[path] = fs
|
||||||
self.log("found %s at %r, %r" % (fs, mp, path))
|
self.log("found %s at %r" % (fs, path))
|
||||||
return fs, mp
|
return fs
|
||||||
|
|
||||||
def _winpath(self, path: str) -> str:
|
def _winpath(self, path: str) -> str:
|
||||||
# try to combine volume-label + st_dev (vsn)
|
# try to combine volume-label + st_dev (vsn)
|
||||||
|
|
@ -82,58 +78,42 @@ 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 _from_sp_mount(self) -> dict[str, str]:
|
def build_tab(self) -> None:
|
||||||
|
self.log("inspecting mtab for changes")
|
||||||
|
|
||||||
sptn = r"^.*? on (.*) type ([^ ]+) \(.*"
|
sptn = r"^.*? on (.*) type ([^ ]+) \(.*"
|
||||||
if MACOS:
|
if MACOS:
|
||||||
sptn = r"^.*? on (.*) \(([^ ]+), .*"
|
sptn = r"^.*? on (.*) \(([^ ]+), .*"
|
||||||
|
|
||||||
ptn = re.compile(sptn)
|
ptn = re.compile(sptn)
|
||||||
so, _ = chkcmd(["mount"])
|
so, _ = chkcmd(["mount"])
|
||||||
dtab: dict[str, str] = {}
|
tab1: list[tuple[str, str]] = []
|
||||||
|
atab = []
|
||||||
for ln in so.split("\n"):
|
for ln in so.split("\n"):
|
||||||
m = ptn.match(ln)
|
m = ptn.match(ln)
|
||||||
if not m:
|
if not m:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
zs1, zs2 = m.groups()
|
zs1, zs2 = m.groups()
|
||||||
dtab[str(zs1)] = str(zs2)
|
tab1.append((str(zs1), str(zs2)))
|
||||||
|
atab.append(ln)
|
||||||
return dtab
|
|
||||||
|
|
||||||
def _from_proc(self) -> dict[str, str]:
|
|
||||||
ret: dict[str, str] = {}
|
|
||||||
with open("/proc/self/mounts", "rb", 262144) as f:
|
|
||||||
src = f.read(262144).decode("utf-8", "replace").split("\n")
|
|
||||||
for zsl in [x.split(" ") for x in src]:
|
|
||||||
if len(zsl) < 3:
|
|
||||||
continue
|
|
||||||
zs = zsl[1]
|
|
||||||
zs = zs.replace("\\011", "\t").replace("\\040", " ").replace("\\134", "\\")
|
|
||||||
ret[zs] = zsl[2]
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def build_tab(self) -> None:
|
|
||||||
self.log("inspecting mtab for changes")
|
|
||||||
dtab = self._from_sp_mount() if MACOS else self._from_proc()
|
|
||||||
|
|
||||||
# keep empirically-correct values if mounttab unchanged
|
# keep empirically-correct values if mounttab unchanged
|
||||||
srctab = str(sorted(dtab.items()))
|
srctab = "\n".join(sorted(atab))
|
||||||
if srctab == self.srctab:
|
if srctab == self.srctab:
|
||||||
self.tab = self.oldtab
|
self.tab = self.oldtab
|
||||||
return
|
return
|
||||||
|
|
||||||
self.log("mtab has changed; reevaluating support for sparse files")
|
self.log("mtab has changed; reevaluating support for sparse files")
|
||||||
|
|
||||||
tab1 = list(dtab.items())
|
|
||||||
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
|
||||||
|
|
@ -150,10 +130,9 @@ class Fstab(object):
|
||||||
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
|
||||||
|
|
||||||
|
|
@ -165,7 +144,7 @@ class Fstab(object):
|
||||||
vn.realpath = ptn.sub(nval, vn.realpath)
|
vn.realpath = ptn.sub(nval, vn.realpath)
|
||||||
visit.extend(list(vn.nodes.values()))
|
visit.extend(list(vn.nodes.values()))
|
||||||
|
|
||||||
def get_unix(self, path: str) -> tuple[str, str]:
|
def get_unix(self, path: str) -> str:
|
||||||
if not self.tab:
|
if not self.tab:
|
||||||
try:
|
try:
|
||||||
self.build_tab()
|
self.build_tab()
|
||||||
|
|
@ -174,50 +153,20 @@ class Fstab(object):
|
||||||
# prisonparty or other restrictive environment
|
# prisonparty or other restrictive environment
|
||||||
if not self.warned:
|
if not self.warned:
|
||||||
self.warned = True
|
self.warned = True
|
||||||
t = "failed to associate fs-mounts with the VFS (this is fine):\n%s"
|
self.log("failed to build tab:\n{}".format(min_ex()), 3)
|
||||||
self.log(t % (min_ex(),), 6)
|
|
||||||
self.build_fallback()
|
self.build_fallback()
|
||||||
|
|
||||||
assert self.tab # !rm
|
assert self.tab # !rm
|
||||||
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], ret.vpath
|
return ret.realpath.split("/")[0]
|
||||||
else:
|
else:
|
||||||
return "idk", ""
|
return "idk"
|
||||||
|
|
||||||
def get_w32(self, path: str) -> tuple[str, str]:
|
def get_w32(self, path: str) -> str:
|
||||||
if not self.tab:
|
if not self.tab:
|
||||||
self.build_fallback()
|
self.build_fallback()
|
||||||
|
|
||||||
assert self.tab # !rm
|
assert self.tab # !rm
|
||||||
ret = self.tab._find(path)[0]
|
ret = self.tab._find(path)[0]
|
||||||
return ret.realpath, ""
|
return ret.realpath
|
||||||
|
|
||||||
|
|
||||||
def ramdisk_chk(asrv: AuthSrv) -> None:
|
|
||||||
# should have been in authsrv but that's a circular import
|
|
||||||
mods = []
|
|
||||||
ramfs = ("tmpfs", "overlay")
|
|
||||||
log = asrv.log_func or print
|
|
||||||
fstab = Fstab(log, asrv.args, False)
|
|
||||||
for vn in asrv.vfs.all_nodes.values():
|
|
||||||
if not vn.axs.uwrite or "wram" in vn.flags:
|
|
||||||
continue
|
|
||||||
ap = vn.realpath
|
|
||||||
if not ap or os.path.isfile(ap):
|
|
||||||
continue
|
|
||||||
fs, mp = fstab.get(ap)
|
|
||||||
mp = "/" + mp.strip("/")
|
|
||||||
if fs == "tmpfs" or (mp == "/" and fs in ramfs):
|
|
||||||
mods.append((vn.vpath, ap, fs, mp))
|
|
||||||
vn.axs.uwrite.clear()
|
|
||||||
vn.axs.umove.clear()
|
|
||||||
for un, ztsp in list(vn.uaxs.items()):
|
|
||||||
zsl = list(ztsp)
|
|
||||||
zsl[1] = False
|
|
||||||
zsl[2] = False
|
|
||||||
vn.uaxs[un] = zsl
|
|
||||||
if mods:
|
|
||||||
t = "WARNING: write-access was removed from the following volumes because they are not mapped to an actual HDD for storage! All uploaded data would live in RAM only, and all uploaded files would be LOST on next reboot. To allow uploading and ignore this hazard, enable the 'wram' option (global/volflag). List of affected volumes:"
|
|
||||||
t2 = ["\n volume=[/%s], abspath=%r, type=%s, root=%r" % x for x in mods]
|
|
||||||
log("vfs", t + "".join(t2) + "\n", 1)
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +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,
|
VF_CAREFUL,
|
||||||
Daemon,
|
Daemon,
|
||||||
ODict,
|
ODict,
|
||||||
|
|
@ -31,7 +30,6 @@ from .util import (
|
||||||
relchk,
|
relchk,
|
||||||
runhook,
|
runhook,
|
||||||
sanitize_fn,
|
sanitize_fn,
|
||||||
set_fperms,
|
|
||||||
vjoin,
|
vjoin,
|
||||||
wunlink,
|
wunlink,
|
||||||
)
|
)
|
||||||
|
|
@ -68,13 +66,13 @@ class FtpAuth(DummyAuthorizer):
|
||||||
if ip.startswith("::ffff:"):
|
if ip.startswith("::ffff:"):
|
||||||
ip = ip[7:]
|
ip = ip[7:]
|
||||||
|
|
||||||
ipn = ipnorm(ip)
|
ip = ipnorm(ip)
|
||||||
bans = self.hub.bans
|
bans = self.hub.bans
|
||||||
if ipn in bans:
|
if ip in bans:
|
||||||
rt = bans[ipn] - time.time()
|
rt = bans[ip] - time.time()
|
||||||
if rt < 0:
|
if rt < 0:
|
||||||
logging.info("client unbanned")
|
logging.info("client unbanned")
|
||||||
del bans[ipn]
|
del bans[ip]
|
||||||
else:
|
else:
|
||||||
raise AuthenticationFailed("banned")
|
raise AuthenticationFailed("banned")
|
||||||
|
|
||||||
|
|
@ -83,12 +81,7 @@ class FtpAuth(DummyAuthorizer):
|
||||||
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
|
||||||
|
|
@ -96,10 +89,6 @@ class FtpAuth(DummyAuthorizer):
|
||||||
|
|
||||||
if args.ipu and uname == "*":
|
if args.ipu and uname == "*":
|
||||||
uname = args.ipu_iu[args.ipu_nm.map(ip)]
|
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
|
||||||
|
|
@ -152,6 +141,10 @@ class FtpFs(AbstractedFS):
|
||||||
self.cwd = "/" # pyftpdlib convention of leading slash
|
self.cwd = "/" # pyftpdlib convention of leading slash
|
||||||
self.root = "/var/lib/empty"
|
self.root = "/var/lib/empty"
|
||||||
|
|
||||||
|
self.can_read = self.can_write = self.can_move = False
|
||||||
|
self.can_delete = self.can_get = self.can_upget = False
|
||||||
|
self.can_admin = self.can_dot = False
|
||||||
|
|
||||||
self.listdirinfo = self.listdir
|
self.listdirinfo = self.listdir
|
||||||
self.chdir(".")
|
self.chdir(".")
|
||||||
|
|
||||||
|
|
@ -177,16 +170,6 @@ class FtpFs(AbstractedFS):
|
||||||
fn = sanitize_fn(fn or "", "")
|
fn = sanitize_fn(fn or "", "")
|
||||||
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))
|
||||||
|
|
@ -198,13 +181,10 @@ class FtpFs(AbstractedFS):
|
||||||
if not avfs:
|
if not avfs:
|
||||||
raise FSE(t.format(vpath), 1)
|
raise FSE(t.format(vpath), 1)
|
||||||
|
|
||||||
cr, cw, cm, cd, _, _, _, _, _ = avfs.uaxs[self.h.uname]
|
cr, cw, cm, cd, _, _, _, _ = avfs.can_access("", self.h.uname)
|
||||||
if r and not cr or w and not cw or m and not cm or d and not cd:
|
if r and not cr or w and not cw or m and not cm or d and not cd:
|
||||||
raise FSE(t.format(vpath), 1)
|
raise FSE(t.format(vpath), 1)
|
||||||
|
|
||||||
if "bcasechk" in vfs.flags and not vfs.casechk(rem, True):
|
|
||||||
raise FSE("No such file or directory", 1)
|
|
||||||
|
|
||||||
return os.path.join(vfs.realpath, rem), vfs, rem
|
return os.path.join(vfs.realpath, rem), vfs, rem
|
||||||
except Pebkac as ex:
|
except Pebkac as ex:
|
||||||
raise FSE(str(ex))
|
raise FSE(str(ex))
|
||||||
|
|
@ -217,7 +197,7 @@ class FtpFs(AbstractedFS):
|
||||||
m: bool = False,
|
m: bool = False,
|
||||||
d: bool = False,
|
d: bool = False,
|
||||||
) -> tuple[str, VFS, str]:
|
) -> tuple[str, VFS, str]:
|
||||||
return self.v2a(join(self.cwd, vpath), r, w, m, d)
|
return self.v2a(os.path.join(self.cwd, vpath), r, w, m, d)
|
||||||
|
|
||||||
def ftp2fs(self, ftppath: str) -> str:
|
def ftp2fs(self, ftppath: str) -> str:
|
||||||
# return self.v2a(ftppath)
|
# return self.v2a(ftppath)
|
||||||
|
|
@ -238,7 +218,7 @@ 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)
|
self.validpath(ap)
|
||||||
if w:
|
if w:
|
||||||
try:
|
try:
|
||||||
|
|
@ -250,9 +230,8 @@ class FtpFs(AbstractedFS):
|
||||||
td = 0
|
td = 0
|
||||||
|
|
||||||
if w and need_unlink:
|
if w and need_unlink:
|
||||||
assert td # type: ignore # !rm
|
|
||||||
if td >= -1 and td <= self.args.ftp_wt:
|
if td >= -1 and td <= self.args.ftp_wt:
|
||||||
# within permitted timeframe; allow overwrite or resume
|
# within permitted timeframe; unlink and accept
|
||||||
do_it = True
|
do_it = True
|
||||||
elif self.args.no_del or self.args.ftp_no_ow:
|
elif self.args.no_del or self.args.ftp_no_ow:
|
||||||
# file too old, or overwrite not allowed; reject
|
# file too old, or overwrite not allowed; reject
|
||||||
|
|
@ -269,23 +248,13 @@ class FtpFs(AbstractedFS):
|
||||||
if not do_it:
|
if not do_it:
|
||||||
raise FSE("File already exists")
|
raise FSE("File already exists")
|
||||||
|
|
||||||
# Don't unlink file for append mode
|
wunlink(self.log, ap, VF_CAREFUL)
|
||||||
elif "a" not in mode:
|
|
||||||
wunlink(self.log, ap, VF_CAREFUL)
|
|
||||||
|
|
||||||
ret = open(fsenc(ap), mode, self.args.iobuf)
|
return 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)
|
||||||
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
|
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
|
||||||
if not vfs.realpath:
|
|
||||||
self.cwd = nwd
|
|
||||||
return
|
|
||||||
|
|
||||||
ap = vfs.canonical(rem)
|
ap = vfs.canonical(rem)
|
||||||
try:
|
try:
|
||||||
st = bos.stat(ap)
|
st = bos.stat(ap)
|
||||||
|
|
@ -300,10 +269,20 @@ class FtpFs(AbstractedFS):
|
||||||
raise FSE("Permission denied", 1)
|
raise FSE("Permission denied", 1)
|
||||||
|
|
||||||
self.cwd = nwd
|
self.cwd = nwd
|
||||||
|
(
|
||||||
|
self.can_read,
|
||||||
|
self.can_write,
|
||||||
|
self.can_move,
|
||||||
|
self.can_delete,
|
||||||
|
self.can_get,
|
||||||
|
self.can_upget,
|
||||||
|
self.can_admin,
|
||||||
|
self.can_dot,
|
||||||
|
) = 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)
|
||||||
|
|
@ -322,7 +301,7 @@ class FtpFs(AbstractedFS):
|
||||||
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())
|
||||||
|
|
||||||
if self.uname not in vfs.axs.udot:
|
if not self.can_dot:
|
||||||
vfs_ls = exclude_dotfiles(vfs_ls)
|
vfs_ls = exclude_dotfiles(vfs_ls)
|
||||||
|
|
||||||
vfs_ls.sort()
|
vfs_ls.sort()
|
||||||
|
|
@ -370,13 +349,16 @@ class FtpFs(AbstractedFS):
|
||||||
raise FSE(str(ex))
|
raise FSE(str(ex))
|
||||||
|
|
||||||
def rename(self, src: str, dst: str) -> None:
|
def rename(self, src: str, dst: str) -> None:
|
||||||
|
if not self.can_move:
|
||||||
|
raise FSE("Not allowed for user " + self.h.uname)
|
||||||
|
|
||||||
if self.args.no_mv:
|
if self.args.no_mv:
|
||||||
raise FSE("The rename/move feature is disabled in server config")
|
raise FSE("The rename/move feature is disabled in server config")
|
||||||
|
|
||||||
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, self.h.cli_ip, svp, dvp)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise FSE(str(ex))
|
raise FSE(str(ex))
|
||||||
|
|
||||||
|
|
@ -400,7 +382,7 @@ class FtpFs(AbstractedFS):
|
||||||
|
|
||||||
def utime(self, path: str, timeval: float) -> None:
|
def utime(self, path: str, timeval: float) -> None:
|
||||||
ap = self.rv2a(path, w=True)[0]
|
ap = self.rv2a(path, w=True)[0]
|
||||||
bos.utime_c(logging.warning, ap, int(timeval), False)
|
return bos.utime(ap, (timeval, timeval))
|
||||||
|
|
||||||
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]
|
||||||
|
|
@ -489,37 +471,27 @@ 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:
|
if xbu and not runhook(
|
||||||
hr = runhook(
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
self.hub.up2k,
|
||||||
self.hub.up2k,
|
"xbu.ftpd",
|
||||||
"xbu.ftpd",
|
xbu,
|
||||||
xbu,
|
ap,
|
||||||
ap,
|
vp,
|
||||||
vp,
|
"",
|
||||||
"",
|
self.uname,
|
||||||
self.uname,
|
self.hub.asrv.vfs.get_perms(vp, self.uname),
|
||||||
self.hub.asrv.vfs.get_perms(vp, self.uname),
|
0,
|
||||||
0,
|
0,
|
||||||
0,
|
self.cli_ip,
|
||||||
self.cli_ip,
|
time.time(),
|
||||||
time.time(),
|
"",
|
||||||
None,
|
):
|
||||||
)
|
raise FSE("Upload blocked by xbu server config")
|
||||||
t = hr.get("rejectmsg") or ""
|
|
||||||
if t or hr.get("rc") != 0:
|
|
||||||
if not t:
|
|
||||||
t = "Upload blocked by xbu server config: %r" % (vp,)
|
|
||||||
self.respond("550 %s" % (t,), logging.info)
|
|
||||||
return
|
|
||||||
|
|
||||||
# print("ftp_STOR: {} {} => {}".format(vp, mode, ap))
|
# print("ftp_STOR: {} {} => {}".format(vp, mode, ap))
|
||||||
ret = FTPHandler.ftp_STOR(self, file, mode)
|
ret = FTPHandler.ftp_STOR(self, file, mode)
|
||||||
|
|
@ -619,7 +591,7 @@ 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:"))]
|
ips = [x for x in ips if "unix:" not in x]
|
||||||
|
|
||||||
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]
|
||||||
|
|
|
||||||
2024
copyparty/httpcli.py
2024
copyparty/httpcli.py
File diff suppressed because it is too large
Load diff
|
|
@ -224,6 +224,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()
|
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,6 @@ from .util import (
|
||||||
build_netmap,
|
build_netmap,
|
||||||
has_resource,
|
has_resource,
|
||||||
ipnorm,
|
ipnorm,
|
||||||
load_ipr,
|
|
||||||
load_ipu,
|
load_ipu,
|
||||||
load_resource,
|
load_resource,
|
||||||
min_ex,
|
min_ex,
|
||||||
|
|
@ -124,7 +123,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)
|
||||||
|
|
@ -177,7 +175,6 @@ class HttpSrv(object):
|
||||||
"browser",
|
"browser",
|
||||||
"browser2",
|
"browser2",
|
||||||
"cf",
|
"cf",
|
||||||
"idp",
|
|
||||||
"md",
|
"md",
|
||||||
"mde",
|
"mde",
|
||||||
"msg",
|
"msg",
|
||||||
|
|
@ -187,7 +184,6 @@ class HttpSrv(object):
|
||||||
"svcs",
|
"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.j2["opds"] = env.get_template("opds.xml")
|
|
||||||
self.prism = has_resource(self.E, "web/deps/prism.js.gz")
|
self.prism = has_resource(self.E, "web/deps/prism.js.gz")
|
||||||
|
|
||||||
if self.args.ipu:
|
if self.args.ipu:
|
||||||
|
|
@ -195,11 +191,6 @@ class HttpSrv(object):
|
||||||
else:
|
else:
|
||||||
self.ipu_iu = self.ipu_nm = None
|
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")
|
||||||
|
|
@ -322,8 +313,6 @@ class HttpSrv(object):
|
||||||
|
|
||||||
Daemon(self.broker.say, "sig-hsrv-up1", ("cb_httpsrv_up",))
|
Daemon(self.broker.say, "sig-hsrv-up1", ("cb_httpsrv_up",))
|
||||||
|
|
||||||
saddr = ("", 0) # fwd-decl for `except TypeError as ex:`
|
|
||||||
|
|
||||||
while not self.stopping:
|
while not self.stopping:
|
||||||
if self.args.log_conn:
|
if self.args.log_conn:
|
||||||
self.log(self.name, "|%sC-ncli" % ("-" * 1,), c="90")
|
self.log(self.name, "|%sC-ncli" % ("-" * 1,), c="90")
|
||||||
|
|
@ -331,8 +320,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)
|
||||||
|
|
@ -383,8 +371,8 @@ class HttpSrv(object):
|
||||||
if nloris < nconn / 2:
|
if nloris < nconn / 2:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
t = "slow%s (idle-conn): %s banned for %d min" # slowloris
|
t = "slowloris (idle-conn): {} banned for {} min"
|
||||||
self.log(self.name, t % ("loris", ip, self.args.loris), 1)
|
self.log(self.name, t.format(ip, self.args.loris, nclose), 1)
|
||||||
self.bans[ip] = int(time.time() + self.args.loris * 60)
|
self.bans[ip] = int(time.time() + self.args.loris * 60)
|
||||||
|
|
||||||
if self.args.log_conn:
|
if self.args.log_conn:
|
||||||
|
|
@ -406,19 +394,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(
|
||||||
|
|
@ -572,7 +547,7 @@ class HttpSrv(object):
|
||||||
|
|
||||||
v = self.E.t0
|
v = self.E.t0
|
||||||
try:
|
try:
|
||||||
with os.scandir(self.E.mod_ + "web") as dh:
|
with os.scandir(os.path.join(self.E.mod, "web")) as dh:
|
||||||
for fh in dh:
|
for fh in dh:
|
||||||
inf = fh.stat()
|
inf = fh.stat()
|
||||||
v = max(v, inf.st_mtime)
|
v = max(v, inf.st_mtime)
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import errno
|
import errno
|
||||||
import os
|
|
||||||
import random
|
import random
|
||||||
import select
|
import select
|
||||||
import socket
|
import socket
|
||||||
|
|
@ -13,52 +12,22 @@ from ipaddress import IPv4Network, IPv6Network
|
||||||
from .__init__ import TYPE_CHECKING
|
from .__init__ import TYPE_CHECKING
|
||||||
from .__init__ import unicode as U
|
from .__init__ import unicode as U
|
||||||
from .multicast import MC_Sck, MCast
|
from .multicast import MC_Sck, MCast
|
||||||
from .util import IP6_LL, CachedSet, Daemon, Netdev, list_ips, min_ex
|
from .stolen.dnslib import AAAA
|
||||||
|
from .stolen.dnslib import CLASS as DC
|
||||||
try:
|
from .stolen.dnslib import (
|
||||||
if os.getenv("PRTY_SYS_ALL") or os.getenv("PRTY_SYS_DNSLIB"):
|
NSEC,
|
||||||
raise ImportError()
|
PTR,
|
||||||
from .stolen.dnslib import (
|
QTYPE,
|
||||||
AAAA,
|
RR,
|
||||||
)
|
SRV,
|
||||||
from .stolen.dnslib import CLASS as DC
|
TXT,
|
||||||
from .stolen.dnslib import (
|
A,
|
||||||
NSEC,
|
DNSHeader,
|
||||||
PTR,
|
DNSQuestion,
|
||||||
QTYPE,
|
DNSRecord,
|
||||||
RR,
|
set_avahi_379,
|
||||||
SRV,
|
)
|
||||||
TXT,
|
from .util import CachedSet, Daemon, Netdev, list_ips, min_ex
|
||||||
A,
|
|
||||||
DNSHeader,
|
|
||||||
DNSQuestion,
|
|
||||||
DNSRecord,
|
|
||||||
set_avahi_379,
|
|
||||||
)
|
|
||||||
|
|
||||||
DNS_VND = True
|
|
||||||
except ImportError:
|
|
||||||
DNS_VND = False
|
|
||||||
from dnslib import (
|
|
||||||
AAAA,
|
|
||||||
)
|
|
||||||
from dnslib import CLASS as DC
|
|
||||||
from dnslib import (
|
|
||||||
NSEC,
|
|
||||||
PTR,
|
|
||||||
QTYPE,
|
|
||||||
RR,
|
|
||||||
SRV,
|
|
||||||
TXT,
|
|
||||||
A,
|
|
||||||
Bimap,
|
|
||||||
DNSHeader,
|
|
||||||
DNSQuestion,
|
|
||||||
DNSRecord,
|
|
||||||
)
|
|
||||||
|
|
||||||
DC.forward[0x8001] = "F_IN"
|
|
||||||
DC.reverse["F_IN"] = 0x8001
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .svchub import SvcHub
|
from .svchub import SvcHub
|
||||||
|
|
@ -66,11 +35,6 @@ if TYPE_CHECKING:
|
||||||
if True: # pylint: disable=using-constant-test
|
if True: # pylint: disable=using-constant-test
|
||||||
from typing import Any, Optional, Union
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
if os.getenv("PRTY_MODSPEC"):
|
|
||||||
from inspect import getsourcefile
|
|
||||||
|
|
||||||
print("PRTY_MODSPEC: dnslib:", getsourcefile(A))
|
|
||||||
|
|
||||||
|
|
||||||
MDNS4 = "224.0.0.251"
|
MDNS4 = "224.0.0.251"
|
||||||
MDNS6 = "ff02::fb"
|
MDNS6 = "ff02::fb"
|
||||||
|
|
@ -109,11 +73,10 @@ class MDNS(MCast):
|
||||||
self.ngen = ngen
|
self.ngen = ngen
|
||||||
self.ttl = 300
|
self.ttl = 300
|
||||||
|
|
||||||
if not self.args.zm_nwa_1 and DNS_VND:
|
if not self.args.zm_nwa_1:
|
||||||
set_avahi_379()
|
set_avahi_379()
|
||||||
|
|
||||||
zs = self.args.zm_fqdn or (self.args.name + ".local")
|
zs = 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))
|
||||||
|
|
@ -136,14 +99,9 @@ class MDNS(MCast):
|
||||||
self.log_func(self.logsrc, msg, c)
|
self.log_func(self.logsrc, msg, c)
|
||||||
|
|
||||||
def build_svcs(self) -> tuple[dict[str, dict[str, Any]], set[str]]:
|
def build_svcs(self) -> tuple[dict[str, dict[str, Any]], set[str]]:
|
||||||
ar = self.args
|
|
||||||
zms = self.args.zms
|
zms = self.args.zms
|
||||||
|
http = {"port": 80 if 80 in self.args.p else self.args.p[0]}
|
||||||
zi = ar.zm_http
|
https = {"port": 443 if 443 in self.args.p else self.args.p[0]}
|
||||||
http = {"port": zi if zi != -1 else 80 if 80 in ar.p else ar.p[0]}
|
|
||||||
zi = ar.zm_https
|
|
||||||
https = {"port": zi if zi != -1 else 443 if 443 in ar.p else ar.p[0]}
|
|
||||||
|
|
||||||
webdav = http.copy()
|
webdav = http.copy()
|
||||||
webdavs = https.copy()
|
webdavs = https.copy()
|
||||||
webdav["u"] = webdavs["u"] = "u" # KDE requires username
|
webdav["u"] = webdavs["u"] = "u" # KDE requires username
|
||||||
|
|
@ -168,16 +126,16 @@ class MDNS(MCast):
|
||||||
|
|
||||||
svcs: dict[str, dict[str, Any]] = {}
|
svcs: dict[str, dict[str, Any]] = {}
|
||||||
|
|
||||||
if "d" in zms and http["port"]:
|
if "d" in zms:
|
||||||
svcs["_webdav._tcp.local."] = webdav
|
svcs["_webdav._tcp.local."] = webdav
|
||||||
|
|
||||||
if "D" in zms and https["port"]:
|
if "D" in zms:
|
||||||
svcs["_webdavs._tcp.local."] = webdavs
|
svcs["_webdavs._tcp.local."] = webdavs
|
||||||
|
|
||||||
if "h" in zms and http["port"]:
|
if "h" in zms:
|
||||||
svcs["_http._tcp.local."] = http
|
svcs["_http._tcp.local."] = http
|
||||||
|
|
||||||
if "H" in zms and https["port"]:
|
if "H" in zms:
|
||||||
svcs["_https._tcp.local."] = https
|
svcs["_https._tcp.local."] = https
|
||||||
|
|
||||||
if "f" in zms.lower():
|
if "f" in zms.lower():
|
||||||
|
|
@ -416,7 +374,7 @@ class MDNS(MCast):
|
||||||
cip = addr[0]
|
cip = addr[0]
|
||||||
v6 = ":" in cip
|
v6 = ":" in cip
|
||||||
if (cip.startswith("169.254") and not self.ll_ok) or (
|
if (cip.startswith("169.254") and not self.ll_ok) or (
|
||||||
v6 and not cip.startswith(IP6_LL)
|
v6 and not cip.startswith("fe80")
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,10 @@ class Metrics(object):
|
||||||
self.hsrv = hsrv
|
self.hsrv = hsrv
|
||||||
|
|
||||||
def tx(self, cli: "HttpCli") -> bool:
|
def tx(self, cli: "HttpCli") -> bool:
|
||||||
args = cli.args
|
if not cli.avol:
|
||||||
if not cli.avol and cli.uname.lower() not in args.stats_u_set:
|
raise Pebkac(403, "not allowed for user " + cli.uname)
|
||||||
raise Pebkac(403, "'stats' not allowed for user " + cli.uname)
|
|
||||||
|
|
||||||
|
args = cli.args
|
||||||
if not args.stats:
|
if not args.stats:
|
||||||
raise Pebkac(403, "the stats feature is not enabled in server config")
|
raise Pebkac(403, "the stats feature is not enabled in server config")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ from .util import (
|
||||||
REKOBO_LKEY,
|
REKOBO_LKEY,
|
||||||
VF_CAREFUL,
|
VF_CAREFUL,
|
||||||
fsenc,
|
fsenc,
|
||||||
gzip,
|
|
||||||
min_ex,
|
min_ex,
|
||||||
pybin,
|
pybin,
|
||||||
retchk,
|
retchk,
|
||||||
|
|
@ -29,7 +28,7 @@ from .util import (
|
||||||
)
|
)
|
||||||
|
|
||||||
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, Optional, Union
|
||||||
|
|
||||||
from .util import NamedLogger, RootLogger
|
from .util import NamedLogger, RootLogger
|
||||||
|
|
||||||
|
|
@ -67,8 +66,6 @@ HAVE_FFPROBE = not os.environ.get("PRTY_NO_FFPROBE") and have_ff("ffprobe")
|
||||||
CBZ_PICS = set("png jpg jpeg gif bmp tga tif tiff webp avif".split())
|
CBZ_PICS = set("png jpg jpeg gif bmp tga tif tiff webp avif".split())
|
||||||
CBZ_01 = re.compile(r"(^|[^0-9v])0+[01]\b")
|
CBZ_01 = re.compile(r"(^|[^0-9v])0+[01]\b")
|
||||||
|
|
||||||
FMT_AU = set("mp3 ogg flac wav".split())
|
|
||||||
|
|
||||||
|
|
||||||
class MParser(object):
|
class MParser(object):
|
||||||
def __init__(self, cmdline: str) -> None:
|
def __init__(self, cmdline: str) -> None:
|
||||||
|
|
@ -141,6 +138,8 @@ def au_unpk(
|
||||||
fd, ret = tempfile.mkstemp("." + au)
|
fd, ret = tempfile.mkstemp("." + au)
|
||||||
|
|
||||||
if pk == "gz":
|
if pk == "gz":
|
||||||
|
import gzip
|
||||||
|
|
||||||
fi = gzip.GzipFile(abspath, mode="rb")
|
fi = gzip.GzipFile(abspath, mode="rb")
|
||||||
|
|
||||||
elif pk == "xz":
|
elif pk == "xz":
|
||||||
|
|
@ -168,17 +167,12 @@ def au_unpk(
|
||||||
znil = [x for x in znil if "cover" in x[0]] or znil
|
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
|
znil = [x for x in znil if CBZ_01.search(x[0])] or znil
|
||||||
t = "cbz: %d files, %d hits" % (nf, len(znil))
|
t = "cbz: %d files, %d hits" % (nf, len(znil))
|
||||||
|
if znil:
|
||||||
|
t += ", using " + znil[0][1].filename
|
||||||
|
log(t)
|
||||||
if not znil:
|
if not znil:
|
||||||
raise Exception("no images inside cbz")
|
raise Exception("no images inside cbz")
|
||||||
using = sorted(znil)[0][1].filename
|
fi = zf.open(znil[0][1])
|
||||||
if znil:
|
|
||||||
t += ", using " + using
|
|
||||||
log(t)
|
|
||||||
fi = zf.open(using)
|
|
||||||
|
|
||||||
elif pk == "epub":
|
|
||||||
fi = get_cover_from_epub(log, abspath)
|
|
||||||
assert fi # !rm
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise Exception("unknown compression %s" % (pk,))
|
raise Exception("unknown compression %s" % (pk,))
|
||||||
|
|
@ -200,17 +194,16 @@ def au_unpk(
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
if ret:
|
if ret:
|
||||||
t = "failed to decompress file %r: %r"
|
t = "failed to decompress audio file %r: %r"
|
||||||
log(t % (abspath, ex))
|
log(t % (abspath, ex))
|
||||||
wunlink(log, ret, vn.flags if vn else VF_CAREFUL)
|
wunlink(log, ret, vn.flags if vn else VF_CAREFUL)
|
||||||
return ""
|
|
||||||
|
|
||||||
return abspath
|
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",
|
||||||
|
|
@ -224,17 +217,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 = {}
|
||||||
|
|
@ -258,7 +242,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
|
||||||
|
|
||||||
|
|
@ -286,8 +270,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"],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -327,7 +309,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():
|
||||||
|
|
@ -376,83 +358,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)
|
|
||||||
|
|
||||||
if not coverimage_path:
|
|
||||||
raise Exception("no cover inside epub")
|
|
||||||
|
|
||||||
# This url is either absolute (in the .epub) or relative to the package document
|
|
||||||
adjusted_cover_path = urljoin(rootfile_path, coverimage_path)
|
|
||||||
|
|
||||||
try:
|
|
||||||
return z.open(adjusted_cover_path)
|
|
||||||
except KeyError:
|
|
||||||
t = "epub: cover specified in package document, but doesn't exist: %s"
|
|
||||||
log(t % (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>
|
|
||||||
xn = package_root.find("./metadata/meta[@name='cover']", package_ns)
|
|
||||||
cover_id = xn.get("content") if xn is not None else None
|
|
||||||
|
|
||||||
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):
|
||||||
|
|
@ -518,6 +424,7 @@ class MTag(object):
|
||||||
"album-artist",
|
"album-artist",
|
||||||
"tpe2",
|
"tpe2",
|
||||||
"aart",
|
"aart",
|
||||||
|
"conductor",
|
||||||
"organization",
|
"organization",
|
||||||
"band",
|
"band",
|
||||||
],
|
],
|
||||||
|
|
@ -651,9 +558,6 @@ class MTag(object):
|
||||||
return self._get(abspath)
|
return self._get(abspath)
|
||||||
|
|
||||||
ap = au_unpk(self.log, self.args.au_unpk, abspath)
|
ap = au_unpk(self.log, self.args.au_unpk, abspath)
|
||||||
if not ap:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
ret = self._get(ap)
|
ret = self._get(ap)
|
||||||
if ap != abspath:
|
if ap != abspath:
|
||||||
wunlink(self.log, ap, VF_CAREFUL)
|
wunlink(self.log, ap, VF_CAREFUL)
|
||||||
|
|
@ -725,7 +629,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)):
|
||||||
|
|
@ -759,9 +663,6 @@ class MTag(object):
|
||||||
ap = abspath
|
ap = abspath
|
||||||
|
|
||||||
ret: dict[str, Any] = {}
|
ret: dict[str, Any] = {}
|
||||||
if not ap:
|
|
||||||
return ret
|
|
||||||
|
|
||||||
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, ap]
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from ipaddress import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from .__init__ import MACOS, TYPE_CHECKING
|
from .__init__ import MACOS, TYPE_CHECKING
|
||||||
from .util import IP6_LL, IP64_LL, Daemon, Netdev, find_prefix, min_ex, spack
|
from .util import Daemon, Netdev, find_prefix, min_ex, spack
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .svchub import SvcHub
|
from .svchub import SvcHub
|
||||||
|
|
@ -96,10 +96,7 @@ class MCast(object):
|
||||||
def create_servers(self) -> list[str]:
|
def create_servers(self) -> list[str]:
|
||||||
bound: list[str] = []
|
bound: list[str] = []
|
||||||
netdevs = self.hub.tcpsrv.netdevs
|
netdevs = self.hub.tcpsrv.netdevs
|
||||||
blist = self.hub.tcpsrv.bound
|
ips = [x[0] for x in self.hub.tcpsrv.bound]
|
||||||
if self.args.http_no_tcp:
|
|
||||||
blist = self.hub.tcpsrv.seen_eps
|
|
||||||
ips = [x[0] for x in blist]
|
|
||||||
|
|
||||||
if "::" in ips:
|
if "::" in ips:
|
||||||
ips = [x for x in ips if x != "::"] + list(
|
ips = [x for x in ips if x != "::"] + list(
|
||||||
|
|
@ -148,7 +145,7 @@ class MCast(object):
|
||||||
all_selected = ips[:]
|
all_selected = ips[:]
|
||||||
|
|
||||||
# discard non-linklocal ipv6
|
# discard non-linklocal ipv6
|
||||||
ips = [x for x in ips if ":" not in x or x.startswith(IP6_LL)]
|
ips = [x for x in ips if ":" not in x or x.startswith("fe80")]
|
||||||
|
|
||||||
if not ips:
|
if not ips:
|
||||||
raise NoIPs()
|
raise NoIPs()
|
||||||
|
|
@ -166,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
|
||||||
|
|
@ -186,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(IP64_LL)}
|
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:
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ try:
|
||||||
raise Exception()
|
raise Exception()
|
||||||
|
|
||||||
HAVE_ARGON2 = True
|
HAVE_ARGON2 = True
|
||||||
from argon2 import exceptions as argon2ex
|
from argon2 import __version__ as argon2ver
|
||||||
except:
|
except:
|
||||||
HAVE_ARGON2 = False
|
HAVE_ARGON2 = False
|
||||||
|
|
||||||
|
|
@ -25,7 +25,6 @@ class PWHash(object):
|
||||||
self.args = args
|
self.args = args
|
||||||
|
|
||||||
zsl = args.ah_alg.split(",")
|
zsl = args.ah_alg.split(",")
|
||||||
zsl = [x.strip() for x in zsl]
|
|
||||||
alg = zsl[0]
|
alg = zsl[0]
|
||||||
if alg == "none":
|
if alg == "none":
|
||||||
alg = ""
|
alg = ""
|
||||||
|
|
@ -148,10 +147,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> ")
|
||||||
|
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
# coding: utf-8
|
|
||||||
from __future__ import print_function, unicode_literals
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
try:
|
|
||||||
if os.getenv("PRTY_SYS_ALL") or os.getenv("PRTY_SYS_QRCG"):
|
|
||||||
raise ImportError()
|
|
||||||
from .stolen.qrcodegen import QrCode
|
|
||||||
|
|
||||||
qrgen = QrCode.encode_binary
|
|
||||||
VENDORED = True
|
|
||||||
except ImportError:
|
|
||||||
VENDORED = False
|
|
||||||
from qrcodegen import QrCode
|
|
||||||
|
|
||||||
if os.getenv("PRTY_MODSPEC"):
|
|
||||||
from inspect import getsourcefile
|
|
||||||
|
|
||||||
print("PRTY_MODSPEC: qrcode:", getsourcefile(QrCode))
|
|
||||||
|
|
||||||
if True: # pylint: disable=using-constant-test
|
|
||||||
import typing
|
|
||||||
from typing import Any, Optional, Sequence, Union
|
|
||||||
|
|
||||||
|
|
||||||
if not VENDORED:
|
|
||||||
|
|
||||||
def _qrgen(data: Union[bytes, Sequence[int]]) -> "QrCode":
|
|
||||||
ret = None
|
|
||||||
V = QrCode.Ecc
|
|
||||||
for e in [V.HIGH, V.QUARTILE, V.MEDIUM, V.LOW]:
|
|
||||||
qr = QrCode.encode_binary(data, e)
|
|
||||||
qr.size = qr._size
|
|
||||||
qr.modules = qr._modules
|
|
||||||
if not ret or ret.size > qr.size:
|
|
||||||
ret = qr
|
|
||||||
return ret
|
|
||||||
|
|
||||||
qrgen = _qrgen
|
|
||||||
|
|
||||||
|
|
||||||
def qr2txt(qr: QrCode, zoom: int = 1, pad: int = 4) -> str:
|
|
||||||
tab = qr.modules
|
|
||||||
sz = qr.size
|
|
||||||
if sz % 2 and zoom == 1:
|
|
||||||
tab.append([False] * sz)
|
|
||||||
|
|
||||||
tab = [[False] * sz] * pad + tab + [[False] * sz] * pad
|
|
||||||
tab = [[False] * pad + x + [False] * pad for x in tab]
|
|
||||||
|
|
||||||
rows: list[str] = []
|
|
||||||
if zoom == 1:
|
|
||||||
for y in range(0, len(tab), 2):
|
|
||||||
row = ""
|
|
||||||
for x in range(len(tab[y])):
|
|
||||||
v = 2 if tab[y][x] else 0
|
|
||||||
v += 1 if tab[y + 1][x] else 0
|
|
||||||
row += " ▄▀█"[v]
|
|
||||||
rows.append(row)
|
|
||||||
else:
|
|
||||||
for tr in tab:
|
|
||||||
row = ""
|
|
||||||
for zb in tr:
|
|
||||||
row += " █"[int(zb)] * 2
|
|
||||||
rows.append(row)
|
|
||||||
|
|
||||||
return "\n".join(rows)
|
|
||||||
|
|
||||||
|
|
||||||
def qr2png(
|
|
||||||
qr: QrCode,
|
|
||||||
zoom: int,
|
|
||||||
pad: int,
|
|
||||||
bg: Optional[tuple[int, int, int]],
|
|
||||||
fg: Optional[tuple[int, int, int]],
|
|
||||||
ap: str,
|
|
||||||
) -> None:
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
tab = qr.modules
|
|
||||||
sz = qr.size
|
|
||||||
psz = sz + pad * 2
|
|
||||||
if bg:
|
|
||||||
img = Image.new("RGB", (psz, psz), bg)
|
|
||||||
else:
|
|
||||||
img = Image.new("RGBA", (psz, psz), (0, 0, 0, 0))
|
|
||||||
fg = (fg[0], fg[1], fg[2], 255)
|
|
||||||
for y in range(sz):
|
|
||||||
for x in range(sz):
|
|
||||||
if tab[y][x]:
|
|
||||||
img.putpixel((x + pad, y + pad), fg)
|
|
||||||
if zoom != 1:
|
|
||||||
img = img.resize((sz * zoom, sz * zoom), Image.Resampling.NEAREST)
|
|
||||||
img.save(ap)
|
|
||||||
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
@ -246,29 +246,24 @@ class SMB(object):
|
||||||
|
|
||||||
ap = absreal(ap)
|
ap = absreal(ap)
|
||||||
xbu = vfs.flags.get("xbu")
|
xbu = vfs.flags.get("xbu")
|
||||||
if xbu:
|
if xbu and not runhook(
|
||||||
hr = runhook(
|
self.nlog,
|
||||||
self.nlog,
|
None,
|
||||||
None,
|
self.hub.up2k,
|
||||||
self.hub.up2k,
|
"xbu.smb",
|
||||||
"xbu.smb",
|
xbu,
|
||||||
xbu,
|
ap,
|
||||||
ap,
|
vpath,
|
||||||
vpath,
|
"",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
"",
|
0,
|
||||||
0,
|
0,
|
||||||
0,
|
"1.7.6.2",
|
||||||
"1.7.6.2",
|
time.time(),
|
||||||
time.time(),
|
"",
|
||||||
None,
|
):
|
||||||
)
|
yeet("blocked by xbu server config: %r" % (vpath,))
|
||||||
t = hr.get("rejectmsg") or ""
|
|
||||||
if t or hr.get("rc") != 0:
|
|
||||||
if not t:
|
|
||||||
t = "blocked by xbu server config: %r" % (vpath,)
|
|
||||||
yeet(t)
|
|
||||||
|
|
||||||
ret = bos.open(ap, flags, *a, mode=chmod, **ka)
|
ret = bos.open(ap, flags, *a, mode=chmod, **ka)
|
||||||
if wr:
|
if wr:
|
||||||
|
|
@ -323,9 +318,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, "1.7.6.2", vp1, vp2)
|
||||||
try:
|
try:
|
||||||
bos.makedirs(ap2, vf=vfs2.flags)
|
bos.makedirs(ap2)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -339,7 +334,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:
|
||||||
|
|
@ -378,7 +373,7 @@ class SMB(object):
|
||||||
t = "blocked utime (no-write-acc %s): /%s @%s"
|
t = "blocked utime (no-write-acc %s): /%s @%s"
|
||||||
yeet(t % (vfs.axs.uwrite, vpath, uname))
|
yeet(t % (vfs.axs.uwrite, vpath, uname))
|
||||||
|
|
||||||
bos.utime_c(info, ap, int(times[1]), False)
|
return bos.utime(ap, times)
|
||||||
|
|
||||||
def _p_exists(self, vpath: str) -> bool:
|
def _p_exists(self, vpath: str) -> bool:
|
||||||
# ap = "?"
|
# ap = "?"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
# https://github.com/nayuki/QR-Code-generator/blob/daa3114/python/qrcodegen.py
|
# https://github.com/nayuki/QR-Code-generator/blob/daa3114/python/qrcodegen.py
|
||||||
# the original ^ is extremely well commented so refer to that for explanations
|
# the original ^ is extremely well commented so refer to that for explanations
|
||||||
|
|
||||||
# hacks: binary-only, auto-ecc, py2-compat
|
# hacks: binary-only, auto-ecc, render, py2-compat
|
||||||
|
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
|
@ -173,6 +173,33 @@ class QrCode(object):
|
||||||
self._apply_mask(msk) # Apply the final choice of mask
|
self._apply_mask(msk) # Apply the final choice of mask
|
||||||
self._draw_format_bits(msk) # Overwrite old format bits
|
self._draw_format_bits(msk) # Overwrite old format bits
|
||||||
|
|
||||||
|
def render(self, zoom=1, pad=4) -> str:
|
||||||
|
tab = self.modules
|
||||||
|
sz = self.size
|
||||||
|
if sz % 2 and zoom == 1:
|
||||||
|
tab.append([False] * sz)
|
||||||
|
|
||||||
|
tab = [[False] * sz] * pad + tab + [[False] * sz] * pad
|
||||||
|
tab = [[False] * pad + x + [False] * pad for x in tab]
|
||||||
|
|
||||||
|
rows: list[str] = []
|
||||||
|
if zoom == 1:
|
||||||
|
for y in range(0, len(tab), 2):
|
||||||
|
row = ""
|
||||||
|
for x in range(len(tab[y])):
|
||||||
|
v = 2 if tab[y][x] else 0
|
||||||
|
v += 1 if tab[y + 1][x] else 0
|
||||||
|
row += " ▄▀█"[v]
|
||||||
|
rows.append(row)
|
||||||
|
else:
|
||||||
|
for tr in tab:
|
||||||
|
row = ""
|
||||||
|
for zb in tr:
|
||||||
|
row += " █"[int(zb)] * 2
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
return "\n".join(rows)
|
||||||
|
|
||||||
def _draw_function_patterns(self) -> None:
|
def _draw_function_patterns(self) -> None:
|
||||||
# Draw horizontal and vertical timing patterns
|
# Draw horizontal and vertical timing patterns
|
||||||
for i in range(self.size):
|
for i in range(self.size):
|
||||||
|
|
@ -567,3 +594,20 @@ 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))
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,6 @@ 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,
|
||||||
|
|
@ -85,7 +82,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])
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import atexit
|
|
||||||
import errno
|
import errno
|
||||||
|
import gzip
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
@ -27,10 +27,8 @@ if True: # pylint: disable=using-constant-test
|
||||||
from typing import Any, Optional, Union
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode
|
from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode
|
||||||
from .authsrv import BAD_CFG, AuthSrv, derive_args, n_du_who, n_ver_who
|
from .authsrv import BAD_CFG, AuthSrv
|
||||||
from .bos import bos
|
|
||||||
from .cert import ensure_cert
|
from .cert import ensure_cert
|
||||||
from .fsutil import ramdisk_chk
|
|
||||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN
|
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN
|
||||||
from .pwhash import HAVE_ARGON2
|
from .pwhash import HAVE_ARGON2
|
||||||
from .tcpsrv import TcpSrv
|
from .tcpsrv import TcpSrv
|
||||||
|
|
@ -40,7 +38,6 @@ from .th_srv import (
|
||||||
HAVE_FFPROBE,
|
HAVE_FFPROBE,
|
||||||
HAVE_HEIF,
|
HAVE_HEIF,
|
||||||
HAVE_PIL,
|
HAVE_PIL,
|
||||||
HAVE_RAW,
|
|
||||||
HAVE_VIPS,
|
HAVE_VIPS,
|
||||||
HAVE_WEBP,
|
HAVE_WEBP,
|
||||||
ThumbSrv,
|
ThumbSrv,
|
||||||
|
|
@ -54,7 +51,6 @@ from .util import (
|
||||||
HAVE_PSUTIL,
|
HAVE_PSUTIL,
|
||||||
HAVE_SQLITE3,
|
HAVE_SQLITE3,
|
||||||
HAVE_ZMQ,
|
HAVE_ZMQ,
|
||||||
RE_ANSI,
|
|
||||||
URL_BUG,
|
URL_BUG,
|
||||||
UTC,
|
UTC,
|
||||||
VERSIONS,
|
VERSIONS,
|
||||||
|
|
@ -64,26 +60,19 @@ from .util import (
|
||||||
HMaccas,
|
HMaccas,
|
||||||
ODict,
|
ODict,
|
||||||
alltrace,
|
alltrace,
|
||||||
|
ansi_re,
|
||||||
build_netmap,
|
build_netmap,
|
||||||
expat_ver,
|
expat_ver,
|
||||||
gzip,
|
|
||||||
html_escape,
|
|
||||||
load_ipr,
|
|
||||||
load_ipu,
|
load_ipu,
|
||||||
lock_file,
|
|
||||||
min_ex,
|
min_ex,
|
||||||
mp,
|
mp,
|
||||||
odfusion,
|
odfusion,
|
||||||
pybin,
|
pybin,
|
||||||
start_log_thrs,
|
start_log_thrs,
|
||||||
start_stackmon,
|
start_stackmon,
|
||||||
termsize,
|
|
||||||
ub64enc,
|
ub64enc,
|
||||||
)
|
)
|
||||||
|
|
||||||
if HAVE_SQLITE3:
|
|
||||||
import sqlite3
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
try:
|
try:
|
||||||
from .mdns import MDNS
|
from .mdns import MDNS
|
||||||
|
|
@ -95,11 +84,6 @@ if PY2:
|
||||||
range = xrange # type: ignore
|
range = xrange # type: ignore
|
||||||
|
|
||||||
|
|
||||||
VER_IDP_DB = 1
|
|
||||||
VER_SESSION_DB = 1
|
|
||||||
VER_SHARES_DB = 2
|
|
||||||
|
|
||||||
|
|
||||||
class SvcHub(object):
|
class SvcHub(object):
|
||||||
"""
|
"""
|
||||||
Hosts all services which cannot be parallelized due to reliance on monolithic resources.
|
Hosts all services which cannot be parallelized due to reliance on monolithic resources.
|
||||||
|
|
@ -136,7 +120,6 @@ class SvcHub(object):
|
||||||
self.nsigs = 3
|
self.nsigs = 3
|
||||||
self.retcode = 0
|
self.retcode = 0
|
||||||
self.httpsrv_up = 0
|
self.httpsrv_up = 0
|
||||||
self.qr_tsz = None
|
|
||||||
|
|
||||||
self.log_mutex = threading.Lock()
|
self.log_mutex = threading.Lock()
|
||||||
self.cday = 0
|
self.cday = 0
|
||||||
|
|
@ -158,8 +141,7 @@ class SvcHub(object):
|
||||||
args.unpost = 0
|
args.unpost = 0
|
||||||
args.no_del = True
|
args.no_del = True
|
||||||
args.no_mv = True
|
args.no_mv = True
|
||||||
args.reflink = True
|
args.hardlink = True
|
||||||
args.dav_auth = True
|
|
||||||
args.vague_403 = True
|
args.vague_403 = True
|
||||||
args.nih = True
|
args.nih = True
|
||||||
|
|
||||||
|
|
@ -176,7 +158,6 @@ class SvcHub(object):
|
||||||
# for non-http clients (ftp, tftp)
|
# for non-http clients (ftp, tftp)
|
||||||
self.bans: dict[str, int] = {}
|
self.bans: dict[str, int] = {}
|
||||||
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)
|
||||||
|
|
@ -205,14 +186,8 @@ class SvcHub(object):
|
||||||
|
|
||||||
if not args.use_fpool and args.j != 1:
|
if not args.use_fpool and args.j != 1:
|
||||||
args.no_fpool = True
|
args.no_fpool = True
|
||||||
t = "multithreading enabled with -j {}, so disabling fpool -- this can reduce upload performance on some filesystems, and make some antivirus-softwares "
|
t = "multithreading enabled with -j {}, so disabling fpool -- this can reduce upload performance on some filesystems"
|
||||||
c = 0
|
self.log("root", t.format(args.j))
|
||||||
if ANYWIN:
|
|
||||||
t += "(especially Microsoft Defender) stress your CPU and HDD severely during big uploads"
|
|
||||||
c = 3
|
|
||||||
else:
|
|
||||||
t += "consume more resources (CPU/HDD) than normal"
|
|
||||||
self.log("root", t.format(args.j), c)
|
|
||||||
|
|
||||||
if not args.no_fpool and args.j != 1:
|
if not args.no_fpool and args.j != 1:
|
||||||
t = "WARNING: ignoring --use-fpool because multithreading (-j{}) is enabled"
|
t = "WARNING: ignoring --use-fpool because multithreading (-j{}) is enabled"
|
||||||
|
|
@ -248,8 +223,8 @@ class SvcHub(object):
|
||||||
t = "WARNING: --th-ram-max is very small (%.2f GiB); will not be able to %s"
|
t = "WARNING: --th-ram-max is very small (%.2f GiB); will not be able to %s"
|
||||||
self.log("root", t % (args.th_ram_max, zs), 3)
|
self.log("root", t % (args.th_ram_max, zs), 3)
|
||||||
|
|
||||||
if args.chpw and args.have_idp_hdrs and "pw" not in args.auth_ord.split(","):
|
if args.chpw and args.idp_h_usr:
|
||||||
t = "ERROR: user-changeable passwords is not compatible with your current configuration. Choose one of these options to fix it:\n option1: disable --chpw\n option2: remove all use of IdP features; --idp-*\n option3: change --auth-ord to something like pw,idp,ipu"
|
t = "ERROR: user-changeable passwords is incompatible with IdP/identity-providers; you must disable either --chpw or --idp-h-usr"
|
||||||
self.log("root", t, 1)
|
self.log("root", t, 1)
|
||||||
raise Exception(t)
|
raise Exception(t)
|
||||||
|
|
||||||
|
|
@ -264,24 +239,8 @@ class SvcHub(object):
|
||||||
setattr(args, "ipu_iu", iu)
|
setattr(args, "ipu_iu", iu)
|
||||||
setattr(args, "ipu_nm", nm)
|
setattr(args, "ipu_nm", nm)
|
||||||
|
|
||||||
if args.ipr:
|
|
||||||
ipr = load_ipr(self.log, args.ipr, True)
|
|
||||||
setattr(args, "ipr_u", ipr)
|
|
||||||
|
|
||||||
for zs in "ah_salt fk_salt dk_salt".split():
|
|
||||||
if getattr(args, "show_%s" % (zs,)):
|
|
||||||
self.log("root", "effective %s is %s" % (zs, getattr(args, zs)))
|
|
||||||
|
|
||||||
if args.ah_cli or args.ah_gen:
|
|
||||||
args.idp_store = 0
|
|
||||||
args.no_ses = True
|
|
||||||
args.shr = ""
|
|
||||||
|
|
||||||
if args.idp_store and args.have_idp_hdrs:
|
|
||||||
self.setup_db("idp")
|
|
||||||
|
|
||||||
if not self.args.no_ses:
|
if not self.args.no_ses:
|
||||||
self.setup_db("ses")
|
self.setup_session_db()
|
||||||
|
|
||||||
args.shr1 = ""
|
args.shr1 = ""
|
||||||
if args.shr:
|
if args.shr:
|
||||||
|
|
@ -291,17 +250,6 @@ class SvcHub(object):
|
||||||
ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)]
|
ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)]
|
||||||
args.theme = "{0}{1} {0} {1}".format(ch, bri)
|
args.theme = "{0}{1} {0} {1}".format(ch, bri)
|
||||||
|
|
||||||
if args.no_stack:
|
|
||||||
args.stack_who = "no"
|
|
||||||
|
|
||||||
if args.nid:
|
|
||||||
args.du_who = "no"
|
|
||||||
args.du_iwho = n_du_who(args.du_who)
|
|
||||||
|
|
||||||
if args.ver and args.ver_who == "no":
|
|
||||||
args.ver_who = "all"
|
|
||||||
args.ver_iwho = n_ver_who(args.ver_who)
|
|
||||||
|
|
||||||
if args.nih:
|
if args.nih:
|
||||||
args.vname = ""
|
args.vname = ""
|
||||||
args.doctitle = args.doctitle.replace(" @ --name", "")
|
args.doctitle = args.doctitle.replace(" @ --name", "")
|
||||||
|
|
@ -315,7 +263,6 @@ class SvcHub(object):
|
||||||
|
|
||||||
# initiate all services to manage
|
# initiate all services to manage
|
||||||
self.asrv = AuthSrv(self.args, self.log, dargs=self.dargs)
|
self.asrv = AuthSrv(self.args, self.log, dargs=self.dargs)
|
||||||
ramdisk_chk(self.asrv)
|
|
||||||
|
|
||||||
if args.cgen:
|
if args.cgen:
|
||||||
self.asrv.cgen()
|
self.asrv.cgen()
|
||||||
|
|
@ -340,13 +287,11 @@ class SvcHub(object):
|
||||||
|
|
||||||
self._feature_test()
|
self._feature_test()
|
||||||
|
|
||||||
decs = {k.strip(): 1 for k in self.args.th_dec.split(",")}
|
decs = {k: 1 for k in self.args.th_dec.split(",")}
|
||||||
if not HAVE_VIPS:
|
if not HAVE_VIPS:
|
||||||
decs.pop("vips", None)
|
decs.pop("vips", None)
|
||||||
if not HAVE_PIL:
|
if not HAVE_PIL:
|
||||||
decs.pop("pil", None)
|
decs.pop("pil", None)
|
||||||
if not HAVE_RAW:
|
|
||||||
decs.pop("raw", None)
|
|
||||||
if not HAVE_FFMPEG or not HAVE_FFPROBE:
|
if not HAVE_FFMPEG or not HAVE_FFPROBE:
|
||||||
decs.pop("ff", None)
|
decs.pop("ff", None)
|
||||||
|
|
||||||
|
|
@ -395,10 +340,7 @@ class SvcHub(object):
|
||||||
t = "invalid mp3 transcoding quality [%s] specified; only supports [0] to disable, a CBR value such as [192k], or a CQ/CRF value such as [v2]"
|
t = "invalid mp3 transcoding quality [%s] specified; only supports [0] to disable, a CBR value such as [192k], or a CQ/CRF value such as [v2]"
|
||||||
raise Exception(t % (args.q_mp3,))
|
raise Exception(t % (args.q_mp3,))
|
||||||
else:
|
else:
|
||||||
zss = set(args.th_r_ffa.split(",") + args.th_r_ffv.split(","))
|
args.au_unpk = {}
|
||||||
args.au_unpk = {
|
|
||||||
k: v for k, v in args.au_unpk.items() if v.split(".")[0] not in zss
|
|
||||||
}
|
|
||||||
|
|
||||||
args.th_poke = min(args.th_poke, args.th_maxage, args.ac_maxage)
|
args.th_poke = min(args.th_poke, args.th_maxage, args.ac_maxage)
|
||||||
|
|
||||||
|
|
@ -451,100 +393,39 @@ class SvcHub(object):
|
||||||
|
|
||||||
# create netmaps early to avoid firewall gaps,
|
# create netmaps early to avoid firewall gaps,
|
||||||
# but the mutex blocks multiprocessing startup
|
# but the mutex blocks multiprocessing startup
|
||||||
for zs in "ipu_nm ftp_ipa_nm tftp_ipa_nm".split():
|
for zs in "ipu_iu ftp_ipa_nm tftp_ipa_nm".split():
|
||||||
try:
|
try:
|
||||||
getattr(args, zs).mutex = threading.Lock()
|
getattr(args, zs).mutex = threading.Lock()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
if args.ipr:
|
|
||||||
for nm in args.ipr_u.values():
|
|
||||||
nm.mutex = threading.Lock()
|
|
||||||
|
|
||||||
def _db_onfail_ses(self) -> None:
|
|
||||||
self.args.no_ses = True
|
|
||||||
|
|
||||||
def _db_onfail_idp(self) -> None:
|
|
||||||
self.args.idp_store = 0
|
|
||||||
|
|
||||||
def setup_db(self, which: str) -> None:
|
|
||||||
"""
|
|
||||||
the "non-mission-critical" databases; if something looks broken then just nuke it
|
|
||||||
"""
|
|
||||||
if which == "ses":
|
|
||||||
native_ver = VER_SESSION_DB
|
|
||||||
db_path = self.args.ses_db
|
|
||||||
desc = "sessions-db"
|
|
||||||
pathopt = "ses-db"
|
|
||||||
sanchk_q = "select count(*) from us"
|
|
||||||
createfun = self._create_session_db
|
|
||||||
failfun = self._db_onfail_ses
|
|
||||||
elif which == "idp":
|
|
||||||
native_ver = VER_IDP_DB
|
|
||||||
db_path = self.args.idp_db
|
|
||||||
desc = "idp-db"
|
|
||||||
pathopt = "idp-db"
|
|
||||||
sanchk_q = "select count(*) from us"
|
|
||||||
createfun = self._create_idp_db
|
|
||||||
failfun = self._db_onfail_idp
|
|
||||||
else:
|
|
||||||
raise Exception("unknown cachetype")
|
|
||||||
|
|
||||||
if not db_path.endswith(".db"):
|
|
||||||
zs = "config option --%s (the %s) was configured to [%s] which is invalid; must be a filepath ending with .db"
|
|
||||||
self.log("root", zs % (pathopt, desc, db_path), 1)
|
|
||||||
raise Exception(BAD_CFG)
|
|
||||||
|
|
||||||
|
def setup_session_db(self) -> None:
|
||||||
if not HAVE_SQLITE3:
|
if not HAVE_SQLITE3:
|
||||||
failfun()
|
self.args.no_ses = True
|
||||||
if which == "ses":
|
t = "WARNING: sqlite3 not available; disabling sessions, will use plaintext passwords in cookies"
|
||||||
zs = "disabling sessions, will use plaintext passwords in cookies"
|
self.log("root", t, 3)
|
||||||
elif which == "idp":
|
|
||||||
zs = "disabling idp-db, will be unable to remember IdP-volumes after a restart"
|
|
||||||
self.log("root", "WARNING: sqlite3 not available; %s" % (zs,), 3)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
assert sqlite3 # type: ignore # !rm
|
import sqlite3
|
||||||
|
|
||||||
db_lock = db_path + ".lock"
|
create = True
|
||||||
try:
|
db_path = self.args.ses_db
|
||||||
create = not os.path.getsize(db_path)
|
self.log("root", "opening sessions-db %s" % (db_path,))
|
||||||
except:
|
for n in range(2):
|
||||||
create = True
|
|
||||||
zs = "creating new" if create else "opening"
|
|
||||||
self.log("root", "%s %s %s" % (zs, desc, db_path))
|
|
||||||
|
|
||||||
for tries in range(2):
|
|
||||||
sver = 0
|
|
||||||
try:
|
try:
|
||||||
db = sqlite3.connect(db_path)
|
db = sqlite3.connect(db_path)
|
||||||
cur = db.cursor()
|
cur = db.cursor()
|
||||||
try:
|
try:
|
||||||
zs = "select v from kv where k='sver'"
|
cur.execute("select count(*) from us").fetchone()
|
||||||
sver = cur.execute(zs).fetchall()[0][0]
|
create = False
|
||||||
if sver > native_ver:
|
break
|
||||||
zs = "this version of copyparty only understands %s v%d and older; the db is v%d"
|
|
||||||
raise Exception(zs % (desc, native_ver, sver))
|
|
||||||
|
|
||||||
cur.execute(sanchk_q).fetchone()
|
|
||||||
except:
|
except:
|
||||||
if sver:
|
pass
|
||||||
raise
|
|
||||||
sver = createfun(cur)
|
|
||||||
|
|
||||||
err = self._verify_db(
|
|
||||||
cur, which, pathopt, db_path, desc, sver, native_ver
|
|
||||||
)
|
|
||||||
if err:
|
|
||||||
tries = 99
|
|
||||||
self.args.no_ses = True
|
|
||||||
self.log("root", err, 3)
|
|
||||||
break
|
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
if tries or sver > native_ver:
|
if n:
|
||||||
raise
|
raise
|
||||||
t = "%s is unusable; deleting and recreating: %r"
|
t = "sessions-db corrupt; deleting and recreating: %r"
|
||||||
self.log("root", t % (desc, ex), 3)
|
self.log("root", t % (ex,), 3)
|
||||||
try:
|
try:
|
||||||
cur.close() # type: ignore
|
cur.close() # type: ignore
|
||||||
except:
|
except:
|
||||||
|
|
@ -553,13 +434,8 @@ class SvcHub(object):
|
||||||
db.close() # type: ignore
|
db.close() # type: ignore
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
try:
|
|
||||||
os.unlink(db_lock)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
os.unlink(db_path)
|
os.unlink(db_path)
|
||||||
|
|
||||||
def _create_session_db(self, cur: "sqlite3.Cursor") -> int:
|
|
||||||
sch = [
|
sch = [
|
||||||
r"create table kv (k text, v int)",
|
r"create table kv (k text, v int)",
|
||||||
r"create table us (un text, si text, t0 int)",
|
r"create table us (un text, si text, t0 int)",
|
||||||
|
|
@ -569,74 +445,17 @@ class SvcHub(object):
|
||||||
r"create index us_t0 on us(t0)",
|
r"create index us_t0 on us(t0)",
|
||||||
r"insert into kv values ('sver', 1)",
|
r"insert into kv values ('sver', 1)",
|
||||||
]
|
]
|
||||||
for cmd in sch:
|
|
||||||
cur.execute(cmd)
|
|
||||||
self.log("root", "created new sessions-db")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def _create_idp_db(self, cur: "sqlite3.Cursor") -> int:
|
assert db # type: ignore # !rm
|
||||||
sch = [
|
assert cur # type: ignore # !rm
|
||||||
r"create table kv (k text, v int)",
|
if create:
|
||||||
r"create table us (un text, gs text)",
|
for cmd in sch:
|
||||||
# username, groups
|
cur.execute(cmd)
|
||||||
r"create index us_un on us(un)",
|
self.log("root", "created new sessions-db")
|
||||||
r"insert into kv values ('sver', 1)",
|
db.commit()
|
||||||
]
|
|
||||||
for cmd in sch:
|
|
||||||
cur.execute(cmd)
|
|
||||||
self.log("root", "created new idp-db")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def _verify_db(
|
|
||||||
self,
|
|
||||||
cur: "sqlite3.Cursor",
|
|
||||||
which: str,
|
|
||||||
pathopt: str,
|
|
||||||
db_path: str,
|
|
||||||
desc: str,
|
|
||||||
sver: int,
|
|
||||||
native_ver: int,
|
|
||||||
) -> str:
|
|
||||||
# ensure writable (maybe owned by other user)
|
|
||||||
db = cur.connection
|
|
||||||
|
|
||||||
try:
|
|
||||||
zil = cur.execute("select v from kv where k='pid'").fetchall()
|
|
||||||
if len(zil) > 1:
|
|
||||||
raise Exception()
|
|
||||||
owner = zil[0][0]
|
|
||||||
except:
|
|
||||||
owner = 0
|
|
||||||
|
|
||||||
if which == "ses":
|
|
||||||
cons = "Will now disable sessions and instead use plaintext passwords in cookies."
|
|
||||||
elif which == "idp":
|
|
||||||
cons = "Each IdP-volume will not become available until its associated user sends their first request."
|
|
||||||
else:
|
|
||||||
raise Exception()
|
|
||||||
|
|
||||||
if not lock_file(db_path + ".lock"):
|
|
||||||
t = "the %s [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --%s or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. %s"
|
|
||||||
return t % (desc, db_path, owner, pathopt, cons)
|
|
||||||
|
|
||||||
vars = (("pid", os.getpid()), ("ts", int(time.time() * 1000)))
|
|
||||||
if owner:
|
|
||||||
# wear-estimate: 2 cells; offsets 0x10, 0x50, 0x19720
|
|
||||||
for k, v in vars:
|
|
||||||
cur.execute("update kv set v=? where k=?", (v, k))
|
|
||||||
else:
|
|
||||||
# wear-estimate: 3~4 cells; offsets 0x10, 0x50, 0x19180, 0x19710, 0x36000, 0x360b0, 0x36b90
|
|
||||||
for k, v in vars:
|
|
||||||
cur.execute("insert into kv values(?, ?)", (k, v))
|
|
||||||
|
|
||||||
if sver < native_ver:
|
|
||||||
cur.execute("delete from kv where k='sver'")
|
|
||||||
cur.execute("insert into kv values('sver',?)", (native_ver,))
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
cur.close()
|
cur.close()
|
||||||
db.close()
|
db.close()
|
||||||
return ""
|
|
||||||
|
|
||||||
def setup_share_db(self) -> None:
|
def setup_share_db(self) -> None:
|
||||||
al = self.args
|
al = self.args
|
||||||
|
|
@ -645,7 +464,7 @@ class SvcHub(object):
|
||||||
al.shr = ""
|
al.shr = ""
|
||||||
return
|
return
|
||||||
|
|
||||||
assert sqlite3 # type: ignore # !rm
|
import sqlite3
|
||||||
|
|
||||||
al.shr = al.shr.strip("/")
|
al.shr = al.shr.strip("/")
|
||||||
if "/" in al.shr or not al.shr:
|
if "/" in al.shr or not al.shr:
|
||||||
|
|
@ -656,48 +475,34 @@ class SvcHub(object):
|
||||||
al.shr = "/%s/" % (al.shr,)
|
al.shr = "/%s/" % (al.shr,)
|
||||||
al.shr1 = al.shr[1:]
|
al.shr1 = al.shr[1:]
|
||||||
|
|
||||||
# policy:
|
create = True
|
||||||
# the shares-db is important, so panic if something is wrong
|
modified = False
|
||||||
|
|
||||||
db_path = self.args.shr_db
|
db_path = self.args.shr_db
|
||||||
db_lock = db_path + ".lock"
|
self.log("root", "opening shares-db %s" % (db_path,))
|
||||||
try:
|
for n in range(2):
|
||||||
create = not os.path.getsize(db_path)
|
try:
|
||||||
except:
|
db = sqlite3.connect(db_path)
|
||||||
create = True
|
cur = db.cursor()
|
||||||
zs = "creating new" if create else "opening"
|
try:
|
||||||
self.log("root", "%s shares-db %s" % (zs, db_path))
|
cur.execute("select count(*) from sh").fetchone()
|
||||||
|
create = False
|
||||||
sver = 0
|
break
|
||||||
try:
|
except:
|
||||||
db = sqlite3.connect(db_path)
|
pass
|
||||||
cur = db.cursor()
|
except Exception as ex:
|
||||||
if not create:
|
if n:
|
||||||
zs = "select v from kv where k='sver'"
|
raise
|
||||||
sver = cur.execute(zs).fetchall()[0][0]
|
t = "shares-db corrupt; deleting and recreating: %r"
|
||||||
if sver > VER_SHARES_DB:
|
self.log("root", t % (ex,), 3)
|
||||||
zs = "this version of copyparty only understands shares-db v%d and older; the db is v%d"
|
try:
|
||||||
raise Exception(zs % (VER_SHARES_DB, sver))
|
cur.close() # type: ignore
|
||||||
|
except:
|
||||||
cur.execute("select count(*) from sh").fetchone()
|
pass
|
||||||
except Exception as ex:
|
try:
|
||||||
t = "could not open shares-db; will now panic...\nthe following database must be repaired or deleted before you can launch copyparty:\n%s\n\nERROR: %s\n\nadditional details:\n%s\n"
|
db.close() # type: ignore
|
||||||
self.log("root", t % (db_path, ex, min_ex()), 1)
|
except:
|
||||||
raise
|
pass
|
||||||
|
os.unlink(db_path)
|
||||||
try:
|
|
||||||
zil = cur.execute("select v from kv where k='pid'").fetchall()
|
|
||||||
if len(zil) > 1:
|
|
||||||
raise Exception()
|
|
||||||
owner = zil[0][0]
|
|
||||||
except:
|
|
||||||
owner = 0
|
|
||||||
|
|
||||||
if not lock_file(db_lock):
|
|
||||||
t = "the shares-db [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --shr-db or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. Will now panic."
|
|
||||||
t = t % (db_path, owner)
|
|
||||||
self.log("root", t, 1)
|
|
||||||
raise Exception(t)
|
|
||||||
|
|
||||||
sch1 = [
|
sch1 = [
|
||||||
r"create table kv (k text, v int)",
|
r"create table kv (k text, v int)",
|
||||||
|
|
@ -709,37 +514,34 @@ class SvcHub(object):
|
||||||
r"create index sf_k on sf(k)",
|
r"create index sf_k on sf(k)",
|
||||||
r"create index sh_k on sh(k)",
|
r"create index sh_k on sh(k)",
|
||||||
r"create index sh_t1 on sh(t1)",
|
r"create index sh_t1 on sh(t1)",
|
||||||
r"insert into kv values ('sver', 2)",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
assert db # type: ignore # !rm
|
assert db # type: ignore # !rm
|
||||||
assert cur # type: ignore # !rm
|
assert cur # type: ignore # !rm
|
||||||
if not sver:
|
if create:
|
||||||
sver = VER_SHARES_DB
|
dver = 2
|
||||||
|
modified = True
|
||||||
for cmd in sch1 + sch2:
|
for cmd in sch1 + sch2:
|
||||||
cur.execute(cmd)
|
cur.execute(cmd)
|
||||||
self.log("root", "created new shares-db")
|
self.log("root", "created new shares-db")
|
||||||
|
else:
|
||||||
|
(dver,) = cur.execute("select v from kv where k = 'sver'").fetchall()[0]
|
||||||
|
|
||||||
if sver == 1:
|
if dver == 1:
|
||||||
|
modified = True
|
||||||
for cmd in sch2:
|
for cmd in sch2:
|
||||||
cur.execute(cmd)
|
cur.execute(cmd)
|
||||||
cur.execute("update sh set st = 0")
|
cur.execute("update sh set st = 0")
|
||||||
self.log("root", "shares-db schema upgrade ok")
|
self.log("root", "shares-db schema upgrade ok")
|
||||||
|
|
||||||
if sver < VER_SHARES_DB:
|
if modified:
|
||||||
cur.execute("delete from kv where k='sver'")
|
for cmd in [
|
||||||
cur.execute("insert into kv values('sver',?)", (VER_SHARES_DB,))
|
r"delete from kv where k = 'sver'",
|
||||||
|
r"insert into kv values ('sver', %d)" % (2,),
|
||||||
|
]:
|
||||||
|
cur.execute(cmd)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
vars = (("pid", os.getpid()), ("ts", int(time.time() * 1000)))
|
|
||||||
if owner:
|
|
||||||
# wear-estimate: same as sessions-db
|
|
||||||
for k, v in vars:
|
|
||||||
cur.execute("update kv set v=? where k=?", (v, k))
|
|
||||||
else:
|
|
||||||
for k, v in vars:
|
|
||||||
cur.execute("insert into kv values(?, ?)", (k, v))
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
cur.close()
|
cur.close()
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
@ -804,84 +606,6 @@ class SvcHub(object):
|
||||||
def sigterm(self) -> None:
|
def sigterm(self) -> None:
|
||||||
self.signal_handler(signal.SIGTERM, None)
|
self.signal_handler(signal.SIGTERM, None)
|
||||||
|
|
||||||
def sticky_qr(self) -> None:
|
|
||||||
self._sticky_qr()
|
|
||||||
|
|
||||||
def _unsticky_qr(self, flush=True) -> None:
|
|
||||||
print("\033[s\033[J\033[r\033[u", file=sys.stderr, end="")
|
|
||||||
if flush:
|
|
||||||
sys.stderr.flush()
|
|
||||||
|
|
||||||
def _sticky_qr(self, force: bool = False) -> None:
|
|
||||||
sz = termsize()
|
|
||||||
if self.qr_tsz == sz:
|
|
||||||
if not force:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
force = False
|
|
||||||
|
|
||||||
if self.qr_tsz:
|
|
||||||
self._unsticky_qr(False)
|
|
||||||
else:
|
|
||||||
atexit.register(self._unsticky_qr)
|
|
||||||
|
|
||||||
tw, th = self.qr_tsz = sz
|
|
||||||
zs1, qr = self.tcpsrv.qr.split("\n", 1)
|
|
||||||
url, colr = zs1.split(" ", 1)
|
|
||||||
nl = len(qr.split("\n")) # numlines
|
|
||||||
lp = 3 if nl * 2 + 4 < tw else 0 # leftpad
|
|
||||||
lp0 = lp
|
|
||||||
if self.args.qr_pin == 2:
|
|
||||||
url = ""
|
|
||||||
else:
|
|
||||||
while lp and (nl + lp) * 2 + len(url) + 1 > tw:
|
|
||||||
lp -= 1
|
|
||||||
if (nl + lp) * 2 + len(url) + 1 > tw:
|
|
||||||
qr = url + "\n" + qr
|
|
||||||
url = ""
|
|
||||||
nl += 1
|
|
||||||
lp = lp0
|
|
||||||
sh = 1 + th - nl
|
|
||||||
if lp:
|
|
||||||
zs = " " * lp
|
|
||||||
qr = zs + qr.replace("\n", "\n" + zs)
|
|
||||||
if url:
|
|
||||||
url = "%s\033[%d;%dH%s\033[0m" % (colr, sh + 1, (nl + lp) * 2, url)
|
|
||||||
qr = colr + qr
|
|
||||||
|
|
||||||
t = "%s\033[%dA" % ("\n" * nl, nl)
|
|
||||||
t = "%s\033[s\033[1;%dr\033[%dH%s%s\033[u" % (t, sh - 1, sh, qr, url)
|
|
||||||
if not force:
|
|
||||||
self.log("qr", "sticky-qrcode %sx%s,%s" % (tw, th, sh), 6)
|
|
||||||
self.pr(t, file=sys.stderr, end="")
|
|
||||||
|
|
||||||
def _qr_thr(self):
|
|
||||||
qr = self.tcpsrv.qr
|
|
||||||
w8 = self.args.qr_wait
|
|
||||||
if w8:
|
|
||||||
time.sleep(w8)
|
|
||||||
self.log("qr-code", qr)
|
|
||||||
if self.args.qr_stdout:
|
|
||||||
self.pr(self.tcpsrv.qr)
|
|
||||||
if self.args.qr_stderr:
|
|
||||||
self.pr(self.tcpsrv.qr, file=sys.stderr)
|
|
||||||
w8 = self.args.qr_every
|
|
||||||
msg = "%s\033[%dA" % (qr, len(qr.split("\n")))
|
|
||||||
while w8:
|
|
||||||
time.sleep(w8)
|
|
||||||
if self.stopping:
|
|
||||||
break
|
|
||||||
if self.args.qr_pin:
|
|
||||||
self._sticky_qr(True)
|
|
||||||
else:
|
|
||||||
self.log("qr-code", msg)
|
|
||||||
w8 = self.args.qr_winch
|
|
||||||
while w8:
|
|
||||||
time.sleep(w8)
|
|
||||||
if self.stopping:
|
|
||||||
break
|
|
||||||
self._sticky_qr()
|
|
||||||
|
|
||||||
def cb_httpsrv_up(self) -> None:
|
def cb_httpsrv_up(self) -> None:
|
||||||
self.httpsrv_up += 1
|
self.httpsrv_up += 1
|
||||||
if self.httpsrv_up != self.broker.num_workers:
|
if self.httpsrv_up != self.broker.num_workers:
|
||||||
|
|
@ -894,17 +618,7 @@ class SvcHub(object):
|
||||||
break
|
break
|
||||||
|
|
||||||
if self.tcpsrv.qr:
|
if self.tcpsrv.qr:
|
||||||
if self.args.qr_pin:
|
self.log("qr-code", self.tcpsrv.qr)
|
||||||
self.sticky_qr()
|
|
||||||
if self.args.qr_wait or self.args.qr_every or self.args.qr_winch:
|
|
||||||
Daemon(self._qr_thr, "qr")
|
|
||||||
else:
|
|
||||||
if not self.args.qr_pin:
|
|
||||||
self.log("qr-code", self.tcpsrv.qr)
|
|
||||||
if self.args.qr_stdout:
|
|
||||||
self.pr(self.tcpsrv.qr)
|
|
||||||
if self.args.qr_stderr:
|
|
||||||
self.pr(self.tcpsrv.qr, file=sys.stderr)
|
|
||||||
else:
|
else:
|
||||||
self.log("root", "workers OK\n")
|
self.log("root", "workers OK\n")
|
||||||
|
|
||||||
|
|
@ -931,7 +645,6 @@ class SvcHub(object):
|
||||||
(HAVE_ZMQ, "pyzmq", "send zeromq messages from event-hooks"),
|
(HAVE_ZMQ, "pyzmq", "send zeromq messages from event-hooks"),
|
||||||
(HAVE_HEIF, "pillow-heif", "read .heif images with pillow (rarely useful)"),
|
(HAVE_HEIF, "pillow-heif", "read .heif images with pillow (rarely useful)"),
|
||||||
(HAVE_AVIF, "pillow-avif", "read .avif images with pillow (rarely useful)"),
|
(HAVE_AVIF, "pillow-avif", "read .avif images with pillow (rarely useful)"),
|
||||||
(HAVE_RAW, "rawpy", "read RAW images"),
|
|
||||||
]
|
]
|
||||||
if ANYWIN:
|
if ANYWIN:
|
||||||
to_check += [
|
to_check += [
|
||||||
|
|
@ -966,11 +679,19 @@ class SvcHub(object):
|
||||||
t += ", "
|
t += ", "
|
||||||
t += "\033[0mNG: \033[35m" + sng
|
t += "\033[0mNG: \033[35m" + sng
|
||||||
|
|
||||||
t += "\033[0m, see --deps (this is fine btw)"
|
t += "\033[0m, see --deps"
|
||||||
self.log("optional-dependencies", t, 6)
|
self.log("dependencies", t, 6)
|
||||||
|
|
||||||
def _check_env(self) -> None:
|
def _check_env(self) -> None:
|
||||||
al = self.args
|
try:
|
||||||
|
files = os.listdir(E.cfg)
|
||||||
|
except:
|
||||||
|
files = []
|
||||||
|
|
||||||
|
hits = [x for x in files if x.lower().endswith(".conf")]
|
||||||
|
if hits:
|
||||||
|
t = "WARNING: found config files in [%s]: %s\n config files are not expected here, and will NOT be loaded (unless your setup is intentionally hella funky)"
|
||||||
|
self.log("root", t % (E.cfg, ", ".join(hits)), 3)
|
||||||
|
|
||||||
if self.args.no_bauth:
|
if self.args.no_bauth:
|
||||||
t = "WARNING: --no-bauth disables support for the Android app; you may want to use --bauth-last instead"
|
t = "WARNING: --no-bauth disables support for the Android app; you may want to use --bauth-last instead"
|
||||||
|
|
@ -978,21 +699,6 @@ class SvcHub(object):
|
||||||
if self.args.bauth_last:
|
if self.args.bauth_last:
|
||||||
self.log("root", "WARNING: ignoring --bauth-last due to --no-bauth", 3)
|
self.log("root", "WARNING: ignoring --bauth-last due to --no-bauth", 3)
|
||||||
|
|
||||||
have_tcp = False
|
|
||||||
for zs in al.i:
|
|
||||||
if not zs.startswith(("unix:", "fd:")):
|
|
||||||
have_tcp = True
|
|
||||||
if not have_tcp:
|
|
||||||
zb = False
|
|
||||||
zs = "z zm zm4 zm6 zmv zmvv zs zsv zv"
|
|
||||||
for zs in zs.split():
|
|
||||||
if getattr(al, zs, False):
|
|
||||||
setattr(al, zs, False)
|
|
||||||
zb = True
|
|
||||||
if zb:
|
|
||||||
t = "not listening on any ip-addresses (only unix-sockets and/or FDs); cannot enable zeroconf/mdns/ssdp as requested"
|
|
||||||
self.log("root", t, 3)
|
|
||||||
|
|
||||||
if not self.args.no_dav:
|
if not self.args.no_dav:
|
||||||
from .dxml import DXML_OK
|
from .dxml import DXML_OK
|
||||||
|
|
||||||
|
|
@ -1002,24 +708,6 @@ class SvcHub(object):
|
||||||
t = "WARNING:\nDisabling WebDAV support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n"
|
t = "WARNING:\nDisabling WebDAV support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n"
|
||||||
self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1)
|
self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1)
|
||||||
|
|
||||||
if not E.scfg and not al.unsafe_state and not os.getenv("PRTY_UNSAFE_STATE"):
|
|
||||||
t = "because runtime config is currently being stored in an untrusted emergency-fallback location. Please fix your environment so either XDG_CONFIG_HOME or ~/.config can be used instead, or disable this safeguard with --unsafe-state or env-var PRTY_UNSAFE_STATE=1."
|
|
||||||
if not al.no_ses:
|
|
||||||
al.no_ses = True
|
|
||||||
t2 = "A consequence of this misconfiguration is that passwords will now be sent in the HTTP-header of every request!"
|
|
||||||
self.log("root", "WARNING:\nWill disable sessions %s %s" % (t, t2), 1)
|
|
||||||
if al.idp_store == 1:
|
|
||||||
al.idp_store = 0
|
|
||||||
self.log("root", "WARNING:\nDisabling --idp-store %s" % (t,), 3)
|
|
||||||
if al.idp_store:
|
|
||||||
t2 = "ERROR: Cannot enable --idp-store %s" % (t,)
|
|
||||||
self.log("root", t2, 1)
|
|
||||||
raise Exception(t2)
|
|
||||||
if al.shr:
|
|
||||||
t2 = "ERROR: Cannot enable shares %s" % (t,)
|
|
||||||
self.log("root", t2, 1)
|
|
||||||
raise Exception(t2)
|
|
||||||
|
|
||||||
def _process_config(self) -> bool:
|
def _process_config(self) -> bool:
|
||||||
al = self.args
|
al = self.args
|
||||||
|
|
||||||
|
|
@ -1075,20 +763,13 @@ class SvcHub(object):
|
||||||
vl = [os.path.expandvars(os.path.expanduser(x)) for x in vl]
|
vl = [os.path.expandvars(os.path.expanduser(x)) for x in vl]
|
||||||
setattr(al, k, vl)
|
setattr(al, k, vl)
|
||||||
|
|
||||||
for k in "lo hist dbpath ssl_log".split(" "):
|
for k in "lo hist ssl_log".split(" "):
|
||||||
vs = getattr(al, k)
|
vs = getattr(al, k)
|
||||||
if vs:
|
if vs:
|
||||||
vs = os.path.expandvars(os.path.expanduser(vs))
|
vs = os.path.expandvars(os.path.expanduser(vs))
|
||||||
setattr(al, k, vs)
|
setattr(al, k, vs)
|
||||||
|
|
||||||
for k in "idp_adm stats_u".split(" "):
|
for k in "sus_urls nonsus_urls".split(" "):
|
||||||
vs = getattr(al, k)
|
|
||||||
vsa = [x.strip() for x in vs.split(",")]
|
|
||||||
vsa = [x.lower() for x in vsa if x]
|
|
||||||
setattr(al, k + "_set", set(vsa))
|
|
||||||
|
|
||||||
zs = "dav_ua1 sus_urls nonsus_urls ua_nodoc ua_nozip"
|
|
||||||
for k in zs.split(" "):
|
|
||||||
vs = getattr(al, k)
|
vs = getattr(al, k)
|
||||||
if not vs or vs == "no":
|
if not vs or vs == "no":
|
||||||
setattr(al, k, None)
|
setattr(al, k, None)
|
||||||
|
|
@ -1108,23 +789,10 @@ class SvcHub(object):
|
||||||
al.sus_urls = None
|
al.sus_urls = None
|
||||||
|
|
||||||
al.xff_hdr = al.xff_hdr.lower()
|
al.xff_hdr = al.xff_hdr.lower()
|
||||||
al.idp_h_usr = [x.lower() for x in al.idp_h_usr or []]
|
al.idp_h_usr = al.idp_h_usr.lower()
|
||||||
al.idp_h_grp = al.idp_h_grp.lower()
|
al.idp_h_grp = al.idp_h_grp.lower()
|
||||||
al.idp_h_key = al.idp_h_key.lower()
|
al.idp_h_key = al.idp_h_key.lower()
|
||||||
|
|
||||||
al.idp_hm_usr_p = {}
|
|
||||||
for zs0 in al.idp_hm_usr or []:
|
|
||||||
try:
|
|
||||||
sep = zs0[:1]
|
|
||||||
hn, zs1, zs2 = zs0[1:].split(sep)
|
|
||||||
hn = hn.lower()
|
|
||||||
if hn in al.idp_hm_usr_p:
|
|
||||||
al.idp_hm_usr_p[hn][zs1] = zs2
|
|
||||||
else:
|
|
||||||
al.idp_hm_usr_p[hn] = {zs1: zs2}
|
|
||||||
except:
|
|
||||||
raise Exception("invalid --idp-hm-usr [%s]" % (zs0,))
|
|
||||||
|
|
||||||
al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa, True)
|
al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa, True)
|
||||||
al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa, True)
|
al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa, True)
|
||||||
|
|
||||||
|
|
@ -1169,21 +837,12 @@ class SvcHub(object):
|
||||||
except:
|
except:
|
||||||
raise Exception("invalid --mv-retry [%s]" % (self.args.mv_retry,))
|
raise Exception("invalid --mv-retry [%s]" % (self.args.mv_retry,))
|
||||||
|
|
||||||
al.js_utc = "false" if al.localtime else "true"
|
|
||||||
|
|
||||||
al.tcolor = al.tcolor.lstrip("#")
|
al.tcolor = al.tcolor.lstrip("#")
|
||||||
if len(al.tcolor) == 3: # fc5 => ffcc55
|
if len(al.tcolor) == 3: # fc5 => ffcc55
|
||||||
al.tcolor = "".join([x * 2 for x in al.tcolor])
|
al.tcolor = "".join([x * 2 for x in al.tcolor])
|
||||||
|
|
||||||
if self.args.name_url:
|
|
||||||
zs = html_escape(self.args.name_url, True, True)
|
|
||||||
zs = '<a href="%s">%s</a>' % (zs, self.args.name)
|
|
||||||
else:
|
|
||||||
zs = self.args.name
|
|
||||||
self.args.name_html = zs
|
|
||||||
|
|
||||||
zs = al.u2sz
|
zs = al.u2sz
|
||||||
zsl = [x.strip() for x in zs.split(",")]
|
zsl = zs.split(",")
|
||||||
if len(zsl) not in (1, 3):
|
if len(zsl) not in (1, 3):
|
||||||
t = "invalid --u2sz; must be either one number, or a comma-separated list of three numbers (min,default,max)"
|
t = "invalid --u2sz; must be either one number, or a comma-separated list of three numbers (min,default,max)"
|
||||||
raise Exception(t)
|
raise Exception(t)
|
||||||
|
|
@ -1200,7 +859,6 @@ class SvcHub(object):
|
||||||
zi2 = zi
|
zi2 = zi
|
||||||
al.u2sz = ",".join(zsl)
|
al.u2sz = ",".join(zsl)
|
||||||
|
|
||||||
derive_args(al)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _ipa2re(self, txt) -> Optional[re.Pattern]:
|
def _ipa2re(self, txt) -> Optional[re.Pattern]:
|
||||||
|
|
@ -1273,7 +931,7 @@ class SvcHub(object):
|
||||||
|
|
||||||
fn = sel_fn
|
fn = sel_fn
|
||||||
try:
|
try:
|
||||||
bos.makedirs(os.path.dirname(fn))
|
os.makedirs(os.path.dirname(fn))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -1290,9 +948,6 @@ class SvcHub(object):
|
||||||
|
|
||||||
lh = codecs.open(fn, "w", encoding="utf-8", errors="replace")
|
lh = codecs.open(fn, "w", encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
if getattr(self.args, "free_umask", False):
|
|
||||||
os.fchmod(lh.fileno(), 0o644)
|
|
||||||
|
|
||||||
argv = [pybin] + self.argv
|
argv = [pybin] + self.argv
|
||||||
if hasattr(shlex, "quote"):
|
if hasattr(shlex, "quote"):
|
||||||
argv = [shlex.quote(x) for x in argv]
|
argv = [shlex.quote(x) for x in argv]
|
||||||
|
|
@ -1376,7 +1031,6 @@ class SvcHub(object):
|
||||||
with self.reload_mutex:
|
with self.reload_mutex:
|
||||||
self.log("root", "reloading config")
|
self.log("root", "reloading config")
|
||||||
self.asrv.reload(9 if up2k else 4)
|
self.asrv.reload(9 if up2k else 4)
|
||||||
ramdisk_chk(self.asrv)
|
|
||||||
if up2k:
|
if up2k:
|
||||||
self.up2k.reload(rescan_all_vols)
|
self.up2k.reload(rescan_all_vols)
|
||||||
t += "; volumes are now reinitializing"
|
t += "; volumes are now reinitializing"
|
||||||
|
|
@ -1561,18 +1215,11 @@ class SvcHub(object):
|
||||||
|
|
||||||
fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n"
|
fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n"
|
||||||
if self.no_ansi:
|
if self.no_ansi:
|
||||||
if c == 1:
|
fmt = "%s %-21s %s\n"
|
||||||
fmt = "%s %-21s CRIT: %s\n"
|
|
||||||
elif c == 3:
|
|
||||||
fmt = "%s %-21s WARN: %s\n"
|
|
||||||
elif c == 6:
|
|
||||||
fmt = "%s %-21s BTW: %s\n"
|
|
||||||
else:
|
|
||||||
fmt = "%s %-21s LOG: %s\n"
|
|
||||||
if "\033" in msg:
|
if "\033" in msg:
|
||||||
msg = RE_ANSI.sub("", msg)
|
msg = ansi_re.sub("", msg)
|
||||||
if "\033" in src:
|
if "\033" in src:
|
||||||
src = RE_ANSI.sub("", src)
|
src = ansi_re.sub("", src)
|
||||||
elif c:
|
elif c:
|
||||||
if isinstance(c, int):
|
if isinstance(c, int):
|
||||||
msg = "\033[3%sm%s\033[0m" % (c, msg)
|
msg = "\033[3%sm%s\033[0m" % (c, msg)
|
||||||
|
|
@ -1613,7 +1260,7 @@ class SvcHub(object):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def check_mp_support(self) -> str:
|
def check_mp_support(self) -> str:
|
||||||
if MACOS and not os.environ.get("PRTY_FORCE_MP"):
|
if MACOS:
|
||||||
return "multiprocessing is wonky on mac osx;"
|
return "multiprocessing is wonky on mac osx;"
|
||||||
elif sys.version_info < (3, 3):
|
elif sys.version_info < (3, 3):
|
||||||
return "need python 3.3 or newer for multiprocessing;"
|
return "need python 3.3 or newer for multiprocessing;"
|
||||||
|
|
@ -1633,7 +1280,7 @@ class SvcHub(object):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if mp.cpu_count() <= 1 and not os.environ.get("PRTY_FORCE_MP"):
|
if mp.cpu_count() <= 1:
|
||||||
raise Exception()
|
raise Exception()
|
||||||
except:
|
except:
|
||||||
self.log("svchub", "only one CPU detected; multiprocessing disabled")
|
self.log("svchub", "only one CPU detected; multiprocessing disabled")
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,12 @@ from __future__ import print_function, unicode_literals
|
||||||
import calendar
|
import calendar
|
||||||
import stat
|
import stat
|
||||||
import time
|
import time
|
||||||
|
import zlib
|
||||||
|
|
||||||
from .authsrv import AuthSrv
|
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
|
||||||
|
|
@ -54,7 +55,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 +71,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
|
||||||
|
|
@ -244,7 +245,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 +253,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 +267,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)
|
||||||
|
|
@ -309,8 +307,7 @@ class StreamZip(StreamArc):
|
||||||
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -9,26 +9,24 @@ import time
|
||||||
|
|
||||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
|
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
|
||||||
from .cert import gencert
|
from .cert import gencert
|
||||||
from .qrkode import QrCode, qr2png, qr2svg, qr2txt, qrgen
|
from .stolen.qrcodegen import QrCode
|
||||||
from .util import (
|
from .util import (
|
||||||
E_ACCESS,
|
E_ACCESS,
|
||||||
E_ADDR_IN_USE,
|
E_ADDR_IN_USE,
|
||||||
E_ADDR_NOT_AVAIL,
|
E_ADDR_NOT_AVAIL,
|
||||||
E_UNREACH,
|
E_UNREACH,
|
||||||
HAVE_IPV6,
|
HAVE_IPV6,
|
||||||
IP6_LL,
|
|
||||||
IP6ALL,
|
IP6ALL,
|
||||||
VF_CAREFUL,
|
VF_CAREFUL,
|
||||||
Netdev,
|
Netdev,
|
||||||
atomic_move,
|
atomic_move,
|
||||||
get_adapters,
|
|
||||||
min_ex,
|
min_ex,
|
||||||
sunpack,
|
sunpack,
|
||||||
termsize,
|
termsize,
|
||||||
)
|
)
|
||||||
|
|
||||||
if True: # pylint: disable=using-constant-test
|
if True:
|
||||||
from typing import Generator, Optional, Union
|
from typing import Generator, Union
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .svchub import SvcHub
|
from .svchub import SvcHub
|
||||||
|
|
@ -60,7 +58,6 @@ class TcpSrv(object):
|
||||||
self.stopping = False
|
self.stopping = False
|
||||||
self.srv: list[socket.socket] = []
|
self.srv: list[socket.socket] = []
|
||||||
self.bound: list[tuple[str, int]] = []
|
self.bound: list[tuple[str, int]] = []
|
||||||
self.seen_eps: list[tuple[str, int]] = [] # also skipped by uds-only
|
|
||||||
self.netdevs: dict[str, Netdev] = {}
|
self.netdevs: dict[str, Netdev] = {}
|
||||||
self.netlist = ""
|
self.netlist = ""
|
||||||
self.nsrv = 0
|
self.nsrv = 0
|
||||||
|
|
@ -143,31 +140,25 @@ class TcpSrv(object):
|
||||||
# keep IPv6 LL-only nics
|
# keep IPv6 LL-only nics
|
||||||
ll_ok: set[str] = set()
|
ll_ok: set[str] = set()
|
||||||
for ip, nd in self.netdevs.items():
|
for ip, nd in self.netdevs.items():
|
||||||
if not ip.startswith(IP6_LL):
|
if not ip.startswith("fe80"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
just_ll = True
|
just_ll = True
|
||||||
for ip2, nd2 in self.netdevs.items():
|
for ip2, nd2 in self.netdevs.items():
|
||||||
if nd == nd2 and ":" in ip2 and not ip2.startswith(IP6_LL):
|
if nd == nd2 and ":" in ip2 and not ip2.startswith("fe80"):
|
||||||
just_ll = False
|
just_ll = False
|
||||||
|
|
||||||
if just_ll or self.args.ll:
|
if just_ll or self.args.ll:
|
||||||
ll_ok.add(ip.split("/")[0])
|
ll_ok.add(ip.split("/")[0])
|
||||||
|
|
||||||
listening_on = []
|
|
||||||
for ip, ports in sorted(ok.items()):
|
|
||||||
for port in sorted(ports):
|
|
||||||
listening_on.append("%s %s" % (ip, port))
|
|
||||||
|
|
||||||
qr1: dict[str, list[int]] = {}
|
qr1: dict[str, list[int]] = {}
|
||||||
qr2: dict[str, list[int]] = {}
|
qr2: dict[str, list[int]] = {}
|
||||||
msgs = []
|
msgs = []
|
||||||
accessible_on = []
|
|
||||||
title_tab: dict[str, dict[str, int]] = {}
|
title_tab: dict[str, dict[str, int]] = {}
|
||||||
title_vars = [x[1:] for x in self.args.wintitle.split(" ") if x.startswith("$")]
|
title_vars = [x[1:] for x in self.args.wintitle.split(" ") if x.startswith("$")]
|
||||||
t = "available @ {}://{}:{}/ (\033[33m{}\033[0m)"
|
t = "available @ {}://{}:{}/ (\033[33m{}\033[0m)"
|
||||||
for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
|
for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
|
||||||
if ip.startswith(IP6_LL) and ip not in ll_ok:
|
if ip.startswith("fe80") and ip not in ll_ok:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for port in sorted(self.args.p):
|
for port in sorted(self.args.p):
|
||||||
|
|
@ -178,10 +169,6 @@ class TcpSrv(object):
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
zs = "%s %s" % (ip, port)
|
|
||||||
if zs not in accessible_on:
|
|
||||||
accessible_on.append(zs)
|
|
||||||
|
|
||||||
proto = " http"
|
proto = " http"
|
||||||
if self.args.http_only:
|
if self.args.http_only:
|
||||||
pass
|
pass
|
||||||
|
|
@ -232,14 +219,6 @@ class TcpSrv(object):
|
||||||
else:
|
else:
|
||||||
print("\n", end="")
|
print("\n", end="")
|
||||||
|
|
||||||
for fn, ls in (
|
|
||||||
(self.args.wr_h_eps, listening_on),
|
|
||||||
(self.args.wr_h_aon, accessible_on),
|
|
||||||
):
|
|
||||||
if fn:
|
|
||||||
with open(fn, "wb") as f:
|
|
||||||
f.write(("\n".join(ls)).encode("utf-8"))
|
|
||||||
|
|
||||||
if self.args.qr or self.args.qrs:
|
if self.args.qr or self.args.qrs:
|
||||||
self.qr = self._qr(qr1, qr2)
|
self.qr = self._qr(qr1, qr2)
|
||||||
|
|
||||||
|
|
@ -248,10 +227,8 @@ class TcpSrv(object):
|
||||||
|
|
||||||
def _listen(self, ip: str, port: int) -> None:
|
def _listen(self, ip: str, port: int) -> None:
|
||||||
uds_perm = uds_gid = -1
|
uds_perm = uds_gid = -1
|
||||||
bound: Optional[socket.socket] = None
|
|
||||||
tcp = False
|
|
||||||
|
|
||||||
if "unix:" in ip:
|
if "unix:" in ip:
|
||||||
|
tcp = False
|
||||||
ipv = socket.AF_UNIX
|
ipv = socket.AF_UNIX
|
||||||
uds = ip.split(":")
|
uds = ip.split(":")
|
||||||
ip = uds[-1]
|
ip = uds[-1]
|
||||||
|
|
@ -264,12 +241,7 @@ class TcpSrv(object):
|
||||||
import grp
|
import grp
|
||||||
|
|
||||||
uds_gid = grp.getgrnam(uds[2]).gr_gid
|
uds_gid = grp.getgrnam(uds[2]).gr_gid
|
||||||
elif "fd:" in ip:
|
|
||||||
fd = ip[3:]
|
|
||||||
bound = socket.socket(fileno=int(fd))
|
|
||||||
|
|
||||||
tcp = bound.proto == socket.IPPROTO_TCP
|
|
||||||
ipv = bound.family
|
|
||||||
elif ":" in ip:
|
elif ":" in ip:
|
||||||
tcp = True
|
tcp = True
|
||||||
ipv = socket.AF_INET6
|
ipv = socket.AF_INET6
|
||||||
|
|
@ -277,7 +249,7 @@ class TcpSrv(object):
|
||||||
tcp = True
|
tcp = True
|
||||||
ipv = socket.AF_INET
|
ipv = socket.AF_INET
|
||||||
|
|
||||||
srv = bound or socket.socket(ipv, socket.SOCK_STREAM)
|
srv = socket.socket(ipv, socket.SOCK_STREAM)
|
||||||
|
|
||||||
if not ANYWIN or self.args.reuseaddr:
|
if not ANYWIN or self.args.reuseaddr:
|
||||||
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
|
@ -292,28 +264,17 @@ class TcpSrv(object):
|
||||||
except:
|
except:
|
||||||
pass # will create another ipv4 socket instead
|
pass # will create another ipv4 socket instead
|
||||||
|
|
||||||
if getattr(self.args, "freebind", False):
|
if not ANYWIN and self.args.freebind:
|
||||||
srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1)
|
srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1)
|
||||||
|
|
||||||
if bound:
|
|
||||||
self.srv.append(srv)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if tcp:
|
if tcp:
|
||||||
if self.args.http_no_tcp:
|
|
||||||
self.seen_eps.append((ip, port))
|
|
||||||
return
|
|
||||||
srv.bind((ip, port))
|
srv.bind((ip, port))
|
||||||
else:
|
else:
|
||||||
if ANYWIN or self.args.rm_sck:
|
if ANYWIN or self.args.rm_sck:
|
||||||
if os.path.exists(ip):
|
if os.path.exists(ip):
|
||||||
os.unlink(ip)
|
os.unlink(ip)
|
||||||
srv.bind(ip)
|
srv.bind(ip)
|
||||||
if uds_gid != -1:
|
|
||||||
os.chown(ip, -1, uds_gid)
|
|
||||||
if uds_perm != -1:
|
|
||||||
os.chmod(ip, uds_perm)
|
|
||||||
else:
|
else:
|
||||||
tf = "%s.%d" % (ip, os.getpid())
|
tf = "%s.%d" % (ip, os.getpid())
|
||||||
if os.path.exists(tf):
|
if os.path.exists(tf):
|
||||||
|
|
@ -414,7 +375,6 @@ class TcpSrv(object):
|
||||||
|
|
||||||
self.srv = srvs
|
self.srv = srvs
|
||||||
self.bound = bound
|
self.bound = bound
|
||||||
self.seen_eps = list(set(self.seen_eps + bound))
|
|
||||||
self.nsrv = len(srvs)
|
self.nsrv = len(srvs)
|
||||||
self._distribute_netdevs()
|
self._distribute_netdevs()
|
||||||
|
|
||||||
|
|
@ -457,7 +417,9 @@ class TcpSrv(object):
|
||||||
self._distribute_netdevs()
|
self._distribute_netdevs()
|
||||||
|
|
||||||
def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]:
|
def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]:
|
||||||
listen_ips = [x for x in listen_ips if not x.startswith(("unix:", "fd:"))]
|
from .stolen.ifaddr import get_adapters
|
||||||
|
|
||||||
|
listen_ips = [x for x in listen_ips if "unix:" not in x]
|
||||||
|
|
||||||
nics = get_adapters(True)
|
nics = get_adapters(True)
|
||||||
eps: dict[str, Netdev] = {}
|
eps: dict[str, Netdev] = {}
|
||||||
|
|
@ -586,7 +548,7 @@ class TcpSrv(object):
|
||||||
ip = None
|
ip = None
|
||||||
ips = list(t1) + list(t2)
|
ips = list(t1) + list(t2)
|
||||||
qri = self.args.qri
|
qri = self.args.qri
|
||||||
if self.args.zm and not qri and ips:
|
if self.args.zm and not qri:
|
||||||
name = self.args.name + ".local"
|
name = self.args.name + ".local"
|
||||||
t1[name] = next(v for v in (t1 or t2).values())
|
t1[name] = next(v for v in (t1 or t2).values())
|
||||||
ips = [name] + ips
|
ips = [name] + ips
|
||||||
|
|
@ -603,7 +565,8 @@ class TcpSrv(object):
|
||||||
if not ip:
|
if not ip:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
hip = "[%s]" % (ip,) if ":" in ip else ip
|
if ":" in ip:
|
||||||
|
ip = "[{}]".format(ip)
|
||||||
|
|
||||||
if self.args.http_only:
|
if self.args.http_only:
|
||||||
https = ""
|
https = ""
|
||||||
|
|
@ -615,7 +578,7 @@ class TcpSrv(object):
|
||||||
ports = t1.get(ip, t2.get(ip, []))
|
ports = t1.get(ip, t2.get(ip, []))
|
||||||
dport = 443 if https else 80
|
dport = 443 if https else 80
|
||||||
port = "" if dport in ports or not ports else ":{}".format(ports[0])
|
port = "" if dport in ports or not ports else ":{}".format(ports[0])
|
||||||
txt = "http{}://{}{}/{}".format(https, hip, port, self.args.qrl)
|
txt = "http{}://{}{}/{}".format(https, ip, port, self.args.qrl)
|
||||||
|
|
||||||
btxt = txt.encode("utf-8")
|
btxt = txt.encode("utf-8")
|
||||||
if PY2:
|
if PY2:
|
||||||
|
|
@ -623,17 +586,9 @@ class TcpSrv(object):
|
||||||
|
|
||||||
fg = self.args.qr_fg
|
fg = self.args.qr_fg
|
||||||
bg = self.args.qr_bg
|
bg = self.args.qr_bg
|
||||||
nocolor = fg == -1
|
|
||||||
if nocolor:
|
|
||||||
fg = 0
|
|
||||||
|
|
||||||
pad = self.args.qrp
|
pad = self.args.qrp
|
||||||
zoom = self.args.qrz
|
zoom = self.args.qrz
|
||||||
qrc = qrgen(btxt)
|
qrc = QrCode.encode_binary(btxt)
|
||||||
|
|
||||||
for zs in self.args.qr_file or []:
|
|
||||||
self._qr2file(qrc, zs)
|
|
||||||
|
|
||||||
if zoom == 0:
|
if zoom == 0:
|
||||||
try:
|
try:
|
||||||
tw, th = termsize()
|
tw, th = termsize()
|
||||||
|
|
@ -642,15 +597,13 @@ class TcpSrv(object):
|
||||||
except:
|
except:
|
||||||
zoom = 1
|
zoom = 1
|
||||||
|
|
||||||
qr = qr2txt(qrc, zoom, pad)
|
qr = qrc.render(zoom, pad)
|
||||||
if self.args.no_ansi:
|
if self.args.no_ansi:
|
||||||
return "{}\n{}".format(txt, qr)
|
return "{}\n{}".format(txt, qr)
|
||||||
|
|
||||||
halfc = "\033[40;48;5;{0}m{1}\033[47;48;5;{2}m"
|
halfc = "\033[40;48;5;{0}m{1}\033[47;48;5;{2}m"
|
||||||
if not fg:
|
if not fg:
|
||||||
halfc = "\033[0;40m{1}\033[0;47m"
|
halfc = "\033[0;40m{1}\033[0;47m"
|
||||||
if nocolor:
|
|
||||||
halfc = "\033[0;7m{1}\033[0m"
|
|
||||||
|
|
||||||
def ansify(m: re.Match) -> str:
|
def ansify(m: re.Match) -> str:
|
||||||
return halfc.format(fg, " " * len(m.group(1)), bg)
|
return halfc.format(fg, " " * len(m.group(1)), bg)
|
||||||
|
|
@ -660,8 +613,6 @@ class TcpSrv(object):
|
||||||
|
|
||||||
qr = qr.replace("\n", "\033[K\n") + "\033[K" # win10do
|
qr = qr.replace("\n", "\033[K\n") + "\033[K" # win10do
|
||||||
cc = " \033[0;38;5;{0};47;48;5;{1}m" if fg else " \033[0;30;47m"
|
cc = " \033[0;38;5;{0};47;48;5;{1}m" if fg else " \033[0;30;47m"
|
||||||
if nocolor:
|
|
||||||
cc = " \033[0m"
|
|
||||||
t = cc + "\n{2}\033[999G\033[0m\033[J"
|
t = cc + "\n{2}\033[999G\033[0m\033[J"
|
||||||
t = t.format(fg, bg, qr)
|
t = t.format(fg, bg, qr)
|
||||||
if ANYWIN:
|
if ANYWIN:
|
||||||
|
|
@ -669,29 +620,3 @@ class TcpSrv(object):
|
||||||
t = t.replace("\n", "`\n`")
|
t = t.replace("\n", "`\n`")
|
||||||
|
|
||||||
return txt + t
|
return txt + t
|
||||||
|
|
||||||
def _qr2file(self, qrc: QrCode, txt: str):
|
|
||||||
if ".txt:" in txt or ".svg:" in txt:
|
|
||||||
ap, zs1, zs2 = txt.rsplit(":", 2)
|
|
||||||
bg = fg = ""
|
|
||||||
else:
|
|
||||||
ap, zs1, zs2, bg, fg = txt.rsplit(":", 4)
|
|
||||||
zoom = int(zs1)
|
|
||||||
pad = int(zs2)
|
|
||||||
|
|
||||||
if ap.endswith(".txt"):
|
|
||||||
if zoom not in (1, 2):
|
|
||||||
raise Exception("invalid zoom for qr.txt; must be 1 or 2")
|
|
||||||
with open(ap, "wb") as f:
|
|
||||||
f.write(qr2txt(qrc, zoom, pad).encode("utf-8"))
|
|
||||||
elif ap.endswith(".svg"):
|
|
||||||
with open(ap, "wb") as f:
|
|
||||||
f.write(qr2svg(qrc, pad).encode("utf-8"))
|
|
||||||
else:
|
|
||||||
qr2png(qrc, zoom, pad, self._h2i(bg), self._h2i(fg), ap)
|
|
||||||
|
|
||||||
def _h2i(self, hs):
|
|
||||||
try:
|
|
||||||
return tuple(int(hs[i : i + 2], 16) for i in (0, 2, 4))
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
|
||||||
|
|
@ -36,20 +36,7 @@ from partftpy.TftpShared import TftpException
|
||||||
from .__init__ import EXE, PY2, TYPE_CHECKING
|
from .__init__ import EXE, 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 UTC, BytesIO, Daemon, ODict, exclude_dotfiles, min_ex, runhook, undot
|
||||||
FN_EMB,
|
|
||||||
UTC,
|
|
||||||
BytesIO,
|
|
||||||
Daemon,
|
|
||||||
ODict,
|
|
||||||
exclude_dotfiles,
|
|
||||||
min_ex,
|
|
||||||
runhook,
|
|
||||||
set_fperms,
|
|
||||||
undot,
|
|
||||||
vjoin,
|
|
||||||
vsplit,
|
|
||||||
)
|
|
||||||
|
|
||||||
if True: # pylint: disable=using-constant-test
|
if True: # pylint: disable=using-constant-test
|
||||||
from typing import Any, Union
|
from typing import Any, Union
|
||||||
|
|
@ -179,7 +166,7 @@ class Tftpd(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:"))]
|
ips = [x for x in ips if "unix:" not in x]
|
||||||
|
|
||||||
if self.args.tftp4:
|
if self.args.tftp4:
|
||||||
ips = [x for x in ips if ":" not in x]
|
ips = [x for x in ips if ":" not in x]
|
||||||
|
|
@ -257,25 +244,16 @@ class Tftpd(object):
|
||||||
for srv in srvs:
|
for srv in srvs:
|
||||||
srv.stop()
|
srv.stop()
|
||||||
|
|
||||||
def _v2a(
|
def _v2a(self, caller: str, vpath: str, perms: list, *a: Any) -> tuple[VFS, str]:
|
||||||
self, caller: str, vpath: str, perms: list, *a: Any
|
|
||||||
) -> tuple[VFS, str, str]:
|
|
||||||
vpath = vpath.replace("\\", "/").lstrip("/")
|
vpath = vpath.replace("\\", "/").lstrip("/")
|
||||||
if not perms:
|
if not perms:
|
||||||
perms = [True, True]
|
perms = [True, True]
|
||||||
|
|
||||||
debug('%s("%s", %s) %s\033[K\033[0m', caller, vpath, str(a), perms)
|
debug('%s("%s", %s) %s\033[K\033[0m', caller, vpath, str(a), perms)
|
||||||
vfs, rem = self.asrv.vfs.get(vpath, "*", *perms)
|
vfs, rem = self.asrv.vfs.get(vpath, "*", *perms)
|
||||||
if perms[1] and "*" not in vfs.axs.uread and "wo_up_readme" not in vfs.flags:
|
|
||||||
zs, fn = vsplit(vpath)
|
|
||||||
if fn.lower() in FN_EMB:
|
|
||||||
vpath = vjoin(zs, "_wo_" + fn)
|
|
||||||
vfs, rem = self.asrv.vfs.get(vpath, "*", *perms)
|
|
||||||
|
|
||||||
if not vfs.realpath:
|
if not vfs.realpath:
|
||||||
raise Exception("unmapped vfs")
|
raise Exception("unmapped vfs")
|
||||||
|
return vfs, vfs.canonical(rem)
|
||||||
return vfs, vpath, vfs.canonical(rem)
|
|
||||||
|
|
||||||
def _ls(self, vpath: str, raddress: str, rport: int, force=False) -> Any:
|
def _ls(self, vpath: str, raddress: str, rport: int, force=False) -> Any:
|
||||||
# generate file listing if vpath is dir.txt and return as file object
|
# generate file listing if vpath is dir.txt and return as file object
|
||||||
|
|
@ -285,7 +263,6 @@ class Tftpd(object):
|
||||||
if not ptn or not ptn.match(fn.lower()):
|
if not ptn or not ptn.match(fn.lower()):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
tsdt = datetime.fromtimestamp
|
|
||||||
vn, rem = self.asrv.vfs.get(vpath, "*", True, False)
|
vn, rem = self.asrv.vfs.get(vpath, "*", True, False)
|
||||||
fsroot, vfs_ls, vfs_virt = vn.ls(
|
fsroot, vfs_ls, vfs_virt = vn.ls(
|
||||||
rem,
|
rem,
|
||||||
|
|
@ -298,7 +275,7 @@ class Tftpd(object):
|
||||||
dirs1 = [(v.st_mtime, v.st_size, k + "/") for k, v in vfs_ls if k in dnames]
|
dirs1 = [(v.st_mtime, v.st_size, k + "/") for k, v in vfs_ls if k in dnames]
|
||||||
fils1 = [(v.st_mtime, v.st_size, k) for k, v in vfs_ls if k not in dnames]
|
fils1 = [(v.st_mtime, v.st_size, k) for k, v in vfs_ls if k not in dnames]
|
||||||
real1 = dirs1 + fils1
|
real1 = dirs1 + fils1
|
||||||
realt = [(tsdt(max(0, mt), UTC), sz, fn) for mt, sz, fn in real1]
|
realt = [(datetime.fromtimestamp(mt, UTC), sz, fn) for mt, sz, fn in real1]
|
||||||
reals = [
|
reals = [
|
||||||
(
|
(
|
||||||
"%04d-%02d-%02d %02d:%02d:%02d"
|
"%04d-%02d-%02d %02d:%02d:%02d"
|
||||||
|
|
@ -354,7 +331,7 @@ class Tftpd(object):
|
||||||
else:
|
else:
|
||||||
raise Exception("bad mode %s" % (mode,))
|
raise Exception("bad mode %s" % (mode,))
|
||||||
|
|
||||||
vfs, vpath, ap = self._v2a("open", vpath, [rd, wr])
|
vfs, ap = self._v2a("open", vpath, [rd, wr])
|
||||||
if wr:
|
if wr:
|
||||||
if "*" not in vfs.axs.uwrite:
|
if "*" not in vfs.axs.uwrite:
|
||||||
yeet("blocked write; folder not world-writable: /%s" % (vpath,))
|
yeet("blocked write; folder not world-writable: /%s" % (vpath,))
|
||||||
|
|
@ -363,29 +340,24 @@ class Tftpd(object):
|
||||||
yeet("blocked write; folder not world-deletable: /%s" % (vpath,))
|
yeet("blocked write; folder not world-deletable: /%s" % (vpath,))
|
||||||
|
|
||||||
xbu = vfs.flags.get("xbu")
|
xbu = vfs.flags.get("xbu")
|
||||||
if xbu:
|
if xbu and not runhook(
|
||||||
hr = runhook(
|
self.nlog,
|
||||||
self.nlog,
|
None,
|
||||||
None,
|
self.hub.up2k,
|
||||||
self.hub.up2k,
|
"xbu.tftpd",
|
||||||
"xbu.tftpd",
|
xbu,
|
||||||
xbu,
|
ap,
|
||||||
ap,
|
vpath,
|
||||||
vpath,
|
"",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
"",
|
0,
|
||||||
0,
|
0,
|
||||||
0,
|
"8.3.8.7",
|
||||||
"8.3.8.7",
|
time.time(),
|
||||||
time.time(),
|
"",
|
||||||
None,
|
):
|
||||||
)
|
yeet("blocked by xbu server config: %r" % (vpath,))
|
||||||
t = hr.get("rejectmsg") or ""
|
|
||||||
if t or hr.get("rc") != 0:
|
|
||||||
if not t:
|
|
||||||
t = "upload blocked by xbu server config: %r" % (vpath,)
|
|
||||||
yeet(t)
|
|
||||||
|
|
||||||
if not self.args.tftp_nols and bos.path.isdir(ap):
|
if not self.args.tftp_nols and bos.path.isdir(ap):
|
||||||
return self._ls(vpath, "", 0, True)
|
return self._ls(vpath, "", 0, True)
|
||||||
|
|
@ -393,24 +365,18 @@ class Tftpd(object):
|
||||||
if not a:
|
if not a:
|
||||||
a = (self.args.iobuf,)
|
a = (self.args.iobuf,)
|
||||||
|
|
||||||
ret = open(ap, mode, *a, **ka)
|
return open(ap, mode, *a, **ka)
|
||||||
if wr and "fperms" in vfs.flags:
|
|
||||||
set_fperms(ret, vfs.flags)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def _mkdir(self, vpath: str, *a) -> None:
|
def _mkdir(self, vpath: str, *a) -> None:
|
||||||
vfs, _, ap = self._v2a("mkdir", vpath, [False, True])
|
vfs, ap = self._v2a("mkdir", vpath, [])
|
||||||
if "*" not in vfs.axs.uwrite:
|
if "*" not in vfs.axs.uwrite:
|
||||||
yeet("blocked mkdir; folder not world-writable: /%s" % (vpath,))
|
yeet("blocked mkdir; folder not world-writable: /%s" % (vpath,))
|
||||||
|
|
||||||
bos.mkdir(ap, vfs.flags["chmod_d"])
|
return bos.mkdir(ap)
|
||||||
if "chown" in vfs.flags:
|
|
||||||
bos.chown(ap, vfs.flags["uid"], vfs.flags["gid"])
|
|
||||||
|
|
||||||
def _unlink(self, vpath: str) -> None:
|
def _unlink(self, vpath: str) -> None:
|
||||||
# return bos.unlink(self._v2a("stat", vpath, *a)[1])
|
# return bos.unlink(self._v2a("stat", vpath, *a)[1])
|
||||||
vfs, _, ap = self._v2a("delete", vpath, [True, False, False, True])
|
vfs, ap = self._v2a("delete", vpath, [True, False, False, True])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
inf = bos.stat(ap)
|
inf = bos.stat(ap)
|
||||||
|
|
@ -434,7 +400,7 @@ class Tftpd(object):
|
||||||
|
|
||||||
def _p_exists(self, vpath: str) -> bool:
|
def _p_exists(self, vpath: str) -> bool:
|
||||||
try:
|
try:
|
||||||
ap = self._v2a("p.exists", vpath, [False, False])[2]
|
ap = self._v2a("p.exists", vpath, [False, False])[1]
|
||||||
bos.stat(ap)
|
bos.stat(ap)
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
|
|
@ -442,7 +408,7 @@ class Tftpd(object):
|
||||||
|
|
||||||
def _p_isdir(self, vpath: str) -> bool:
|
def _p_isdir(self, vpath: str) -> bool:
|
||||||
try:
|
try:
|
||||||
st = bos.stat(self._v2a("p.isdir", vpath, [False, False])[2])
|
st = bos.stat(self._v2a("p.isdir", vpath, [False, False])[1])
|
||||||
ret = stat.S_ISDIR(st.st_mode)
|
ret = stat.S_ISDIR(st.st_mode)
|
||||||
return ret
|
return ret
|
||||||
except:
|
except:
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import errno
|
|
||||||
import os
|
import os
|
||||||
import stat
|
|
||||||
|
|
||||||
from .__init__ import TYPE_CHECKING
|
from .__init__ import TYPE_CHECKING
|
||||||
from .authsrv import VFS
|
from .authsrv import VFS
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
from .th_srv import EXTS_AC, HAVE_WEBP, thumb_path
|
from .th_srv import EXTS_AC, HAVE_WEBP, thumb_path
|
||||||
from .util import Cooldown, Pebkac
|
from .util import Cooldown
|
||||||
|
|
||||||
if True: # pylint: disable=using-constant-test
|
if True: # pylint: disable=using-constant-test
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
@ -18,9 +16,6 @@ if TYPE_CHECKING:
|
||||||
from .httpsrv import HttpSrv
|
from .httpsrv import HttpSrv
|
||||||
|
|
||||||
|
|
||||||
IOERROR = "reading the file was denied by the server os; either due to filesystem permissions, selinux, apparmor, or similar:\n%r"
|
|
||||||
|
|
||||||
|
|
||||||
class ThumbCli(object):
|
class ThumbCli(object):
|
||||||
def __init__(self, hsrv: "HttpSrv") -> None:
|
def __init__(self, hsrv: "HttpSrv") -> None:
|
||||||
self.broker = hsrv.broker
|
self.broker = hsrv.broker
|
||||||
|
|
@ -36,15 +31,11 @@ class ThumbCli(object):
|
||||||
if not c:
|
if not c:
|
||||||
raise Exception()
|
raise Exception()
|
||||||
except:
|
except:
|
||||||
c = {
|
c = {k: set() for k in ["thumbable", "pil", "vips", "ffi", "ffv", "ffa"]}
|
||||||
k: set()
|
|
||||||
for k in ["thumbable", "pil", "vips", "raw", "ffi", "ffv", "ffa"]
|
|
||||||
}
|
|
||||||
|
|
||||||
self.thumbable = c["thumbable"]
|
self.thumbable = c["thumbable"]
|
||||||
self.fmt_pil = c["pil"]
|
self.fmt_pil = c["pil"]
|
||||||
self.fmt_vips = c["vips"]
|
self.fmt_vips = c["vips"]
|
||||||
self.fmt_raw = c["raw"]
|
|
||||||
self.fmt_ffi = c["ffi"]
|
self.fmt_ffi = c["ffi"]
|
||||||
self.fmt_ffv = c["ffv"]
|
self.fmt_ffv = c["ffv"]
|
||||||
self.fmt_ffa = c["ffa"]
|
self.fmt_ffa = c["ffa"]
|
||||||
|
|
@ -92,7 +83,7 @@ class ThumbCli(object):
|
||||||
if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg", "png"]:
|
if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg", "png"]:
|
||||||
return os.path.join(ptop, rem)
|
return os.path.join(ptop, rem)
|
||||||
|
|
||||||
if fmt[:1] in "jw" and fmt != "wav":
|
if fmt[:1] in "jw":
|
||||||
sfmt = fmt[:1]
|
sfmt = fmt[:1]
|
||||||
|
|
||||||
if sfmt == "j" and self.args.th_no_jpg:
|
if sfmt == "j" and self.args.th_no_jpg:
|
||||||
|
|
@ -133,7 +124,7 @@ class ThumbCli(object):
|
||||||
|
|
||||||
tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa)
|
tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa)
|
||||||
tpaths = [tpath]
|
tpaths = [tpath]
|
||||||
if fmt[:1] == "w" and fmt != "wav":
|
if fmt == "w":
|
||||||
# also check for jpg (maybe webp is unavailable)
|
# also check for jpg (maybe webp is unavailable)
|
||||||
tpaths.append(tpath.rsplit(".", 1)[0] + ".jpg")
|
tpaths.append(tpath.rsplit(".", 1)[0] + ".jpg")
|
||||||
|
|
||||||
|
|
@ -166,22 +157,8 @@ class ThumbCli(object):
|
||||||
if abort:
|
if abort:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
ap = os.path.join(ptop, rem)
|
if not bos.path.getsize(os.path.join(ptop, rem)):
|
||||||
try:
|
return None
|
||||||
st = bos.stat(ap)
|
|
||||||
if not st.st_size or not stat.S_ISREG(st.st_mode):
|
|
||||||
return None
|
|
||||||
|
|
||||||
with open(ap, "rb", 4) as f:
|
|
||||||
if not f.read(4):
|
|
||||||
raise Exception()
|
|
||||||
except OSError as ex:
|
|
||||||
if ex.errno == errno.ENOENT:
|
|
||||||
raise Pebkac(404)
|
|
||||||
else:
|
|
||||||
raise Pebkac(500, IOERROR % (ex,))
|
|
||||||
except Exception as ex:
|
|
||||||
raise Pebkac(500, IOERROR % (ex,))
|
|
||||||
|
|
||||||
x = self.broker.ask("thumbsrv.get", ptop, rem, mtime, fmt)
|
x = self.broker.ask("thumbsrv.get", ptop, rem, mtime, fmt)
|
||||||
return x.get() # type: ignore
|
return x.get() # type: ignore
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,10 @@
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import io
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
import tempfile
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
@ -21,17 +18,16 @@ from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, au_unpk, ffprobe
|
||||||
from .util import BytesIO # type: ignore
|
from .util import BytesIO # type: ignore
|
||||||
from .util import (
|
from .util import (
|
||||||
FFMPEG_URL,
|
FFMPEG_URL,
|
||||||
VF_CAREFUL,
|
|
||||||
Cooldown,
|
Cooldown,
|
||||||
Daemon,
|
Daemon,
|
||||||
afsenc,
|
afsenc,
|
||||||
atomic_move,
|
|
||||||
fsenc,
|
fsenc,
|
||||||
min_ex,
|
min_ex,
|
||||||
runcmd,
|
runcmd,
|
||||||
statdir,
|
statdir,
|
||||||
ub64enc,
|
ub64enc,
|
||||||
vsplit,
|
vsplit,
|
||||||
|
wrename,
|
||||||
wunlink,
|
wunlink,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -51,11 +47,7 @@ HAVE_AVIF = False
|
||||||
HAVE_WEBP = False
|
HAVE_WEBP = False
|
||||||
|
|
||||||
EXTS_TH = set(["jpg", "webp", "png"])
|
EXTS_TH = set(["jpg", "webp", "png"])
|
||||||
EXTS_AC = set(["opus", "owa", "caf", "mp3", "flac", "wav"])
|
EXTS_AC = set(["opus", "owa", "caf", "mp3"])
|
||||||
EXTS_SPEC_SAFE = set("aif aiff flac mp3 opus wav".split())
|
|
||||||
|
|
||||||
PTN_TS = re.compile("^-?[0-9a-f]{8,10}$")
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if os.environ.get("PRTY_NO_PIL"):
|
if os.environ.get("PRTY_NO_PIL"):
|
||||||
|
|
@ -86,10 +78,7 @@ try:
|
||||||
if os.environ.get("PRTY_NO_PIL_HEIF"):
|
if os.environ.get("PRTY_NO_PIL_HEIF"):
|
||||||
raise Exception()
|
raise Exception()
|
||||||
|
|
||||||
try:
|
from pyheif_pillow_opener import register_heif_opener
|
||||||
from pillow_heif import register_heif_opener
|
|
||||||
except ImportError:
|
|
||||||
from pyheif_pillow_opener import register_heif_opener
|
|
||||||
|
|
||||||
register_heif_opener()
|
register_heif_opener()
|
||||||
HAVE_HEIF = True
|
HAVE_HEIF = True
|
||||||
|
|
@ -100,10 +89,6 @@ try:
|
||||||
if os.environ.get("PRTY_NO_PIL_AVIF"):
|
if os.environ.get("PRTY_NO_PIL_AVIF"):
|
||||||
raise Exception()
|
raise Exception()
|
||||||
|
|
||||||
if ".avif" in Image.registered_extensions():
|
|
||||||
HAVE_AVIF = True
|
|
||||||
raise Exception()
|
|
||||||
|
|
||||||
import pillow_avif # noqa: F401 # pylint: disable=unused-import
|
import pillow_avif # noqa: F401 # pylint: disable=unused-import
|
||||||
|
|
||||||
HAVE_AVIF = True
|
HAVE_AVIF = True
|
||||||
|
|
@ -116,28 +101,14 @@ except:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if os.environ.get("PRTY_NO_VIPS"):
|
if os.environ.get("PRTY_NO_VIPS"):
|
||||||
raise ImportError()
|
raise Exception()
|
||||||
|
|
||||||
HAVE_VIPS = True
|
HAVE_VIPS = True
|
||||||
import pyvips
|
import pyvips
|
||||||
|
|
||||||
logging.getLogger("pyvips").setLevel(logging.WARNING)
|
logging.getLogger("pyvips").setLevel(logging.WARNING)
|
||||||
except Exception as e:
|
|
||||||
HAVE_VIPS = False
|
|
||||||
if not isinstance(e, ImportError):
|
|
||||||
logging.warning("libvips found, but failed to load: " + str(e))
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
if os.environ.get("PRTY_NO_RAW"):
|
|
||||||
raise Exception()
|
|
||||||
|
|
||||||
HAVE_RAW = True
|
|
||||||
import rawpy
|
|
||||||
|
|
||||||
logging.getLogger("rawpy").setLevel(logging.WARNING)
|
|
||||||
except:
|
except:
|
||||||
HAVE_RAW = False
|
HAVE_VIPS = False
|
||||||
|
|
||||||
|
|
||||||
th_dir_cache = {}
|
th_dir_cache = {}
|
||||||
|
|
@ -192,15 +163,12 @@ class ThumbSrv(object):
|
||||||
|
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
self.busy: dict[str, list[threading.Condition]] = {}
|
self.busy: dict[str, list[threading.Condition]] = {}
|
||||||
self.untemp: dict[str, list[str]] = {}
|
|
||||||
self.ram: dict[str, float] = {}
|
self.ram: dict[str, float] = {}
|
||||||
self.memcond = threading.Condition(self.mutex)
|
self.memcond = threading.Condition(self.mutex)
|
||||||
self.stopping = False
|
self.stopping = False
|
||||||
self.rm_nullthumbs = True # forget failed conversions on startup
|
self.rm_nullthumbs = True # forget failed conversions on startup
|
||||||
self.nthr = max(1, self.args.th_mt)
|
self.nthr = max(1, self.args.th_mt)
|
||||||
|
|
||||||
self.exts_spec_unsafe = set(self.args.th_spec_cnv.split(","))
|
|
||||||
|
|
||||||
self.q: Queue[Optional[tuple[str, str, str, VFS]]] = Queue(self.nthr * 4)
|
self.q: Queue[Optional[tuple[str, str, str, VFS]]] = Queue(self.nthr * 4)
|
||||||
for n in range(self.nthr):
|
for n in range(self.nthr):
|
||||||
Daemon(self.worker, "thumb-{}-{}".format(n, self.nthr))
|
Daemon(self.worker, "thumb-{}-{}".format(n, self.nthr))
|
||||||
|
|
@ -223,19 +191,11 @@ class ThumbSrv(object):
|
||||||
if self.args.th_clean:
|
if self.args.th_clean:
|
||||||
Daemon(self.cleaner, "thumb.cln")
|
Daemon(self.cleaner, "thumb.cln")
|
||||||
|
|
||||||
(
|
self.fmt_pil, self.fmt_vips, self.fmt_ffi, self.fmt_ffv, self.fmt_ffa = [
|
||||||
self.fmt_pil,
|
|
||||||
self.fmt_vips,
|
|
||||||
self.fmt_raw,
|
|
||||||
self.fmt_ffi,
|
|
||||||
self.fmt_ffv,
|
|
||||||
self.fmt_ffa,
|
|
||||||
) = [
|
|
||||||
set(y.split(","))
|
set(y.split(","))
|
||||||
for y in [
|
for y in [
|
||||||
self.args.th_r_pil,
|
self.args.th_r_pil,
|
||||||
self.args.th_r_vips,
|
self.args.th_r_vips,
|
||||||
self.args.th_r_raw,
|
|
||||||
self.args.th_r_ffi,
|
self.args.th_r_ffi,
|
||||||
self.args.th_r_ffv,
|
self.args.th_r_ffv,
|
||||||
self.args.th_r_ffa,
|
self.args.th_r_ffa,
|
||||||
|
|
@ -258,9 +218,6 @@ class ThumbSrv(object):
|
||||||
if "vips" in self.args.th_dec:
|
if "vips" in self.args.th_dec:
|
||||||
self.thumbable |= self.fmt_vips
|
self.thumbable |= self.fmt_vips
|
||||||
|
|
||||||
if "raw" in self.args.th_dec:
|
|
||||||
self.thumbable |= self.fmt_raw
|
|
||||||
|
|
||||||
if "ff" in self.args.th_dec:
|
if "ff" in self.args.th_dec:
|
||||||
for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]:
|
for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]:
|
||||||
self.thumbable |= zss
|
self.thumbable |= zss
|
||||||
|
|
@ -270,9 +227,6 @@ class ThumbSrv(object):
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
self.stopping = True
|
self.stopping = True
|
||||||
Daemon(self._fire_sentinels, "thumbstopper")
|
|
||||||
|
|
||||||
def _fire_sentinels(self):
|
|
||||||
for _ in range(self.nthr):
|
for _ in range(self.nthr):
|
||||||
self.q.put(None)
|
self.q.put(None)
|
||||||
|
|
||||||
|
|
@ -301,8 +255,7 @@ class ThumbSrv(object):
|
||||||
self.log("joined waiting room for %r" % (tpath,))
|
self.log("joined waiting room for %r" % (tpath,))
|
||||||
except:
|
except:
|
||||||
thdir = os.path.dirname(tpath)
|
thdir = os.path.dirname(tpath)
|
||||||
chmod = bos.MKD_700 if self.args.free_umask else bos.MKD_755
|
bos.makedirs(os.path.join(thdir, "w"))
|
||||||
bos.makedirs(os.path.join(thdir, "w"), vf=chmod)
|
|
||||||
|
|
||||||
inf_path = os.path.join(thdir, "dir.txt")
|
inf_path = os.path.join(thdir, "dir.txt")
|
||||||
if not bos.path.exists(inf_path):
|
if not bos.path.exists(inf_path):
|
||||||
|
|
@ -317,7 +270,7 @@ class ThumbSrv(object):
|
||||||
vn = next((x for x in allvols if x.realpath == ptop), None)
|
vn = next((x for x in allvols if x.realpath == ptop), None)
|
||||||
if not vn:
|
if not vn:
|
||||||
self.log("ptop %r not in %s" % (ptop, allvols), 3)
|
self.log("ptop %r not in %s" % (ptop, allvols), 3)
|
||||||
vn = self.asrv.vfs.all_aps[0][1][0]
|
vn = self.asrv.vfs.all_aps[0][1]
|
||||||
|
|
||||||
self.q.put((abspath, tpath, fmt, vn))
|
self.q.put((abspath, tpath, fmt, vn))
|
||||||
self.log("conv %r :%s \033[0m%r" % (tpath, fmt, abspath), 6)
|
self.log("conv %r :%s \033[0m%r" % (tpath, fmt, abspath), 6)
|
||||||
|
|
@ -345,7 +298,6 @@ class ThumbSrv(object):
|
||||||
"thumbable": self.thumbable,
|
"thumbable": self.thumbable,
|
||||||
"pil": self.fmt_pil,
|
"pil": self.fmt_pil,
|
||||||
"vips": self.fmt_vips,
|
"vips": self.fmt_vips,
|
||||||
"raw": self.fmt_raw,
|
|
||||||
"ffi": self.fmt_ffi,
|
"ffi": self.fmt_ffi,
|
||||||
"ffv": self.fmt_ffv,
|
"ffv": self.fmt_ffv,
|
||||||
"ffa": self.fmt_ffa,
|
"ffa": self.fmt_ffa,
|
||||||
|
|
@ -384,14 +336,12 @@ class ThumbSrv(object):
|
||||||
else:
|
else:
|
||||||
ap_unpk = abspath
|
ap_unpk = abspath
|
||||||
|
|
||||||
if ap_unpk and not bos.path.exists(tpath):
|
if not bos.path.exists(tpath):
|
||||||
tex = tpath.rsplit(".", 1)[-1]
|
tex = tpath.rsplit(".", 1)[-1]
|
||||||
want_mp3 = tex == "mp3"
|
want_mp3 = tex == "mp3"
|
||||||
want_opus = tex in ("opus", "owa", "caf")
|
want_opus = tex in ("opus", "owa", "caf")
|
||||||
want_flac = tex == "flac"
|
|
||||||
want_wav = tex == "wav"
|
|
||||||
want_png = tex == "png"
|
want_png = tex == "png"
|
||||||
want_au = want_mp3 or want_opus or want_flac or want_wav
|
want_au = want_mp3 or want_opus
|
||||||
for lib in self.args.th_dec:
|
for lib in self.args.th_dec:
|
||||||
can_au = lib == "ff" and (
|
can_au = lib == "ff" and (
|
||||||
ext in self.fmt_ffa or ext in self.fmt_ffv
|
ext in self.fmt_ffa or ext in self.fmt_ffv
|
||||||
|
|
@ -401,17 +351,11 @@ class ThumbSrv(object):
|
||||||
funs.append(self.conv_pil)
|
funs.append(self.conv_pil)
|
||||||
elif lib == "vips" and ext in self.fmt_vips:
|
elif lib == "vips" and ext in self.fmt_vips:
|
||||||
funs.append(self.conv_vips)
|
funs.append(self.conv_vips)
|
||||||
elif lib == "raw" and ext in self.fmt_raw:
|
|
||||||
funs.append(self.conv_raw)
|
|
||||||
elif can_au and (want_png or want_au):
|
elif can_au and (want_png or want_au):
|
||||||
if want_opus:
|
if want_opus:
|
||||||
funs.append(self.conv_opus)
|
funs.append(self.conv_opus)
|
||||||
elif want_mp3:
|
elif want_mp3:
|
||||||
funs.append(self.conv_mp3)
|
funs.append(self.conv_mp3)
|
||||||
elif want_flac:
|
|
||||||
funs.append(self.conv_flac)
|
|
||||||
elif want_wav:
|
|
||||||
funs.append(self.conv_wav)
|
|
||||||
elif want_png:
|
elif want_png:
|
||||||
funs.append(self.conv_waves)
|
funs.append(self.conv_waves)
|
||||||
png_ok = True
|
png_ok = True
|
||||||
|
|
@ -427,14 +371,12 @@ class ThumbSrv(object):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
conv_ok = False
|
|
||||||
for fun in funs:
|
for fun in funs:
|
||||||
try:
|
try:
|
||||||
if not png_ok and tpath.endswith(".png"):
|
if not png_ok and tpath.endswith(".png"):
|
||||||
raise Exception("png only allowed for waveforms")
|
raise Exception("png only allowed for waveforms")
|
||||||
|
|
||||||
fun(ap_unpk, ttpath, fmt, vn)
|
fun(ap_unpk, ttpath, fmt, vn)
|
||||||
conv_ok = True
|
|
||||||
break
|
break
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
msg = "%s could not create thumbnail of %r\n%s"
|
msg = "%s could not create thumbnail of %r\n%s"
|
||||||
|
|
@ -443,12 +385,8 @@ class ThumbSrv(object):
|
||||||
self.log(msg, c)
|
self.log(msg, c)
|
||||||
if getattr(ex, "returncode", 0) != 321:
|
if getattr(ex, "returncode", 0) != 321:
|
||||||
if fun == funs[-1]:
|
if fun == funs[-1]:
|
||||||
try:
|
with open(ttpath, "wb") as _:
|
||||||
with open(ttpath, "wb") as _:
|
pass
|
||||||
pass
|
|
||||||
except Exception as ex:
|
|
||||||
t = "failed to create the file [%s]: %r"
|
|
||||||
self.log(t % (ttpath, ex), 3)
|
|
||||||
else:
|
else:
|
||||||
# ffmpeg may spawn empty files on windows
|
# ffmpeg may spawn empty files on windows
|
||||||
try:
|
try:
|
||||||
|
|
@ -456,33 +394,18 @@ class ThumbSrv(object):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if abspath != ap_unpk and ap_unpk:
|
if abspath != ap_unpk:
|
||||||
wunlink(self.log, ap_unpk, vn.flags)
|
wunlink(self.log, ap_unpk, vn.flags)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
atomic_move(self.log, ttpath, tpath, vn.flags)
|
wrename(self.log, ttpath, tpath, vn.flags)
|
||||||
except Exception as ex:
|
except:
|
||||||
if conv_ok and not os.path.exists(tpath):
|
pass
|
||||||
t = "failed to move [%s] to [%s]: %r"
|
|
||||||
self.log(t % (ttpath, tpath, ex), 3)
|
|
||||||
elif not conv_ok:
|
|
||||||
try:
|
|
||||||
open(tpath, "ab").close()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
untemp = []
|
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
subs = self.busy[tpath]
|
subs = self.busy[tpath]
|
||||||
del self.busy[tpath]
|
del self.busy[tpath]
|
||||||
self.ram.pop(ttpath, None)
|
self.ram.pop(ttpath, None)
|
||||||
untemp = self.untemp.pop(ttpath, None) or []
|
|
||||||
|
|
||||||
for ap in untemp:
|
|
||||||
try:
|
|
||||||
wunlink(self.log, ap, VF_CAREFUL)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
for x in subs:
|
for x in subs:
|
||||||
with x:
|
with x:
|
||||||
|
|
@ -521,38 +444,35 @@ class ThumbSrv(object):
|
||||||
|
|
||||||
return im
|
return im
|
||||||
|
|
||||||
def conv_image_pil(self, im: "Image.Image", tpath: str, fmt: str, vn: VFS) -> None:
|
|
||||||
try:
|
|
||||||
im = self.fancy_pillow(im, fmt, vn)
|
|
||||||
except Exception as ex:
|
|
||||||
self.log("fancy_pillow {}".format(ex), "90")
|
|
||||||
im.thumbnail(self.getres(vn, fmt))
|
|
||||||
|
|
||||||
fmts = ["RGB", "L"]
|
|
||||||
args = {"quality": 40}
|
|
||||||
|
|
||||||
if tpath.endswith(".webp"):
|
|
||||||
# quality 80 = pillow-default
|
|
||||||
# quality 75 = ffmpeg-default
|
|
||||||
# method 0 = pillow-default, fast
|
|
||||||
# method 4 = ffmpeg-default
|
|
||||||
# method 6 = max, slow
|
|
||||||
fmts.extend(("RGBA", "LA"))
|
|
||||||
args["method"] = 6
|
|
||||||
else:
|
|
||||||
# default q = 75
|
|
||||||
args["progressive"] = True
|
|
||||||
|
|
||||||
if im.mode not in fmts:
|
|
||||||
# print("conv {}".format(im.mode))
|
|
||||||
im = im.convert("RGB")
|
|
||||||
|
|
||||||
im.save(tpath, **args)
|
|
||||||
|
|
||||||
def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||||
self.wait4ram(0.2, tpath)
|
self.wait4ram(0.2, tpath)
|
||||||
with Image.open(fsenc(abspath)) as im:
|
with Image.open(fsenc(abspath)) as im:
|
||||||
self.conv_image_pil(im, tpath, fmt, vn)
|
try:
|
||||||
|
im = self.fancy_pillow(im, fmt, vn)
|
||||||
|
except Exception as ex:
|
||||||
|
self.log("fancy_pillow {}".format(ex), "90")
|
||||||
|
im.thumbnail(self.getres(vn, fmt))
|
||||||
|
|
||||||
|
fmts = ["RGB", "L"]
|
||||||
|
args = {"quality": 40}
|
||||||
|
|
||||||
|
if tpath.endswith(".webp"):
|
||||||
|
# quality 80 = pillow-default
|
||||||
|
# quality 75 = ffmpeg-default
|
||||||
|
# method 0 = pillow-default, fast
|
||||||
|
# method 4 = ffmpeg-default
|
||||||
|
# method 6 = max, slow
|
||||||
|
fmts.extend(("RGBA", "LA"))
|
||||||
|
args["method"] = 6
|
||||||
|
else:
|
||||||
|
# default q = 75
|
||||||
|
args["progressive"] = True
|
||||||
|
|
||||||
|
if im.mode not in fmts:
|
||||||
|
# print("conv {}".format(im.mode))
|
||||||
|
im = im.convert("RGB")
|
||||||
|
|
||||||
|
im.save(tpath, **args)
|
||||||
|
|
||||||
def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||||
self.wait4ram(0.2, tpath)
|
self.wait4ram(0.2, tpath)
|
||||||
|
|
@ -575,53 +495,9 @@ class ThumbSrv(object):
|
||||||
assert img # type: ignore # !rm
|
assert img # type: ignore # !rm
|
||||||
img.write_to_file(tpath, Q=40)
|
img.write_to_file(tpath, Q=40)
|
||||||
|
|
||||||
def conv_raw(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
|
||||||
self.wait4ram(0.2, tpath)
|
|
||||||
with rawpy.imread(abspath) as raw:
|
|
||||||
thumb = raw.extract_thumb()
|
|
||||||
if thumb.format == rawpy.ThumbFormat.JPEG and tpath.endswith(".jpg"):
|
|
||||||
# if we have a jpg thumbnail and no webp output is available,
|
|
||||||
# just write the jpg directly (it'll be the wrong size, but it's fast)
|
|
||||||
with open(tpath, "wb") as f:
|
|
||||||
f.write(thumb.data)
|
|
||||||
if HAVE_VIPS:
|
|
||||||
crops = ["centre", "none"]
|
|
||||||
if "f" in fmt:
|
|
||||||
crops = ["none"]
|
|
||||||
w, h = self.getres(vn, fmt)
|
|
||||||
kw = {"height": h, "size": "down", "intent": "relative"}
|
|
||||||
|
|
||||||
for c in crops:
|
|
||||||
try:
|
|
||||||
kw["crop"] = c
|
|
||||||
if thumb.format == rawpy.ThumbFormat.BITMAP:
|
|
||||||
img = pyvips.Image.new_from_array(
|
|
||||||
thumb.data, interpretation="rgb"
|
|
||||||
)
|
|
||||||
img = img.thumbnail_image(w, **kw)
|
|
||||||
else:
|
|
||||||
img = pyvips.Image.thumbnail_buffer(thumb.data, w, **kw)
|
|
||||||
break
|
|
||||||
except:
|
|
||||||
if c == crops[-1]:
|
|
||||||
raise
|
|
||||||
|
|
||||||
assert img # type: ignore # !rm
|
|
||||||
img.write_to_file(tpath, Q=40)
|
|
||||||
elif HAVE_PIL:
|
|
||||||
if thumb.format == rawpy.ThumbFormat.BITMAP:
|
|
||||||
im = Image.fromarray(thumb.data, "RGB")
|
|
||||||
else:
|
|
||||||
im = Image.open(io.BytesIO(thumb.data))
|
|
||||||
self.conv_image_pil(im, tpath, fmt, vn)
|
|
||||||
else:
|
|
||||||
raise Exception(
|
|
||||||
"either pil or vips is needed to process embedded bitmap thumbnails in raw files"
|
|
||||||
)
|
|
||||||
|
|
||||||
def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||||
self.wait4ram(0.2, tpath)
|
self.wait4ram(0.2, tpath)
|
||||||
ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||||
if not ret:
|
if not ret:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -632,17 +508,6 @@ class ThumbSrv(object):
|
||||||
dur = ret[".dur"][1] if ".dur" in ret else 4
|
dur = ret[".dur"][1] if ".dur" in ret else 4
|
||||||
seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")]
|
seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")]
|
||||||
|
|
||||||
self._ffmpeg_im(abspath, tpath, fmt, vn, seek, b"0:v:0")
|
|
||||||
|
|
||||||
def _ffmpeg_im(
|
|
||||||
self,
|
|
||||||
abspath: str,
|
|
||||||
tpath: str,
|
|
||||||
fmt: str,
|
|
||||||
vn: VFS,
|
|
||||||
seek: list[bytes],
|
|
||||||
imap: bytes,
|
|
||||||
) -> None:
|
|
||||||
scale = "scale={0}:{1}:force_original_aspect_ratio="
|
scale = "scale={0}:{1}:force_original_aspect_ratio="
|
||||||
if "f" in fmt:
|
if "f" in fmt:
|
||||||
scale += "decrease,setsar=1:1"
|
scale += "decrease,setsar=1:1"
|
||||||
|
|
@ -661,7 +526,7 @@ class ThumbSrv(object):
|
||||||
cmd += seek
|
cmd += seek
|
||||||
cmd += [
|
cmd += [
|
||||||
b"-i", fsenc(abspath),
|
b"-i", fsenc(abspath),
|
||||||
b"-map", imap,
|
b"-map", b"0:v:0",
|
||||||
b"-vf", bscale,
|
b"-vf", bscale,
|
||||||
b"-frames:v", b"1",
|
b"-frames:v", b"1",
|
||||||
b"-metadata:s:v:0", b"rotate=0",
|
b"-metadata:s:v:0", b"rotate=0",
|
||||||
|
|
@ -682,16 +547,16 @@ class ThumbSrv(object):
|
||||||
]
|
]
|
||||||
|
|
||||||
cmd += [fsenc(tpath)]
|
cmd += [fsenc(tpath)]
|
||||||
self._run_ff(cmd, vn, "convt")
|
self._run_ff(cmd, vn)
|
||||||
|
|
||||||
def _run_ff(self, cmd: list[bytes], vn: VFS, kto: str, oom: int = 400) -> None:
|
def _run_ff(self, cmd: list[bytes], vn: VFS, oom: int = 400) -> None:
|
||||||
# self.log((b" ".join(cmd)).decode("utf-8"))
|
# self.log((b" ".join(cmd)).decode("utf-8"))
|
||||||
ret, _, serr = runcmd(cmd, timeout=vn.flags[kto], nice=True, oom=oom)
|
ret, _, serr = runcmd(cmd, timeout=vn.flags["convt"], nice=True, oom=oom)
|
||||||
if not ret:
|
if not ret:
|
||||||
return
|
return
|
||||||
|
|
||||||
c: Union[str, int] = "90"
|
c: Union[str, int] = "90"
|
||||||
t = "FFmpeg failed (probably a corrupt file):\n"
|
t = "FFmpeg failed (probably a corrupt video file):\n"
|
||||||
if (
|
if (
|
||||||
(not self.args.th_ff_jpg or time.time() - int(self.args.th_ff_jpg) < 60)
|
(not self.args.th_ff_jpg or time.time() - int(self.args.th_ff_jpg) < 60)
|
||||||
and cmd[-1].lower().endswith(b".webp")
|
and cmd[-1].lower().endswith(b".webp")
|
||||||
|
|
@ -730,7 +595,7 @@ class ThumbSrv(object):
|
||||||
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
|
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
|
||||||
|
|
||||||
def conv_waves(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
def conv_waves(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||||
ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||||
if "ac" not in ret:
|
if "ac" not in ret:
|
||||||
raise Exception("not audio")
|
raise Exception("not audio")
|
||||||
|
|
||||||
|
|
@ -768,7 +633,7 @@ class ThumbSrv(object):
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
cmd += [fsenc(tpath)]
|
cmd += [fsenc(tpath)]
|
||||||
self._run_ff(cmd, vn, "convt")
|
self._run_ff(cmd, vn)
|
||||||
|
|
||||||
if "pngquant" in vn.flags:
|
if "pngquant" in vn.flags:
|
||||||
wtpath = tpath + ".png"
|
wtpath = tpath + ".png"
|
||||||
|
|
@ -787,70 +652,22 @@ class ThumbSrv(object):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
atomic_move(self.log, wtpath, tpath, vn.flags)
|
wrename(self.log, wtpath, tpath, vn.flags)
|
||||||
|
|
||||||
def conv_emb_cv(
|
|
||||||
self, abspath: str, tpath: str, fmt: str, vn: VFS, strm: dict[str, Any]
|
|
||||||
) -> None:
|
|
||||||
self.wait4ram(0.2, tpath)
|
|
||||||
self._ffmpeg_im(
|
|
||||||
abspath, tpath, fmt, vn, [], b"0:" + strm["index"].encode("ascii")
|
|
||||||
)
|
|
||||||
|
|
||||||
def conv_spec(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
def conv_spec(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||||
ret, raw, strms, ctnr = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||||
if "ac" not in ret:
|
if "ac" not in ret:
|
||||||
raise Exception("not audio")
|
raise Exception("not audio")
|
||||||
|
|
||||||
want_spec = vn.flags.get("th_spec_p", 1)
|
|
||||||
if want_spec < 2:
|
|
||||||
for strm in strms:
|
|
||||||
if (
|
|
||||||
strm.get("codec_type") == "video"
|
|
||||||
and strm.get("DISPOSITION:attached_pic") == "1"
|
|
||||||
):
|
|
||||||
return self.conv_emb_cv(abspath, tpath, fmt, vn, strm)
|
|
||||||
|
|
||||||
if not want_spec:
|
|
||||||
raise Exception("spectrograms forbidden by volflag")
|
|
||||||
|
|
||||||
fext = abspath.split(".")[-1].lower()
|
|
||||||
|
|
||||||
# https://trac.ffmpeg.org/ticket/10797
|
# https://trac.ffmpeg.org/ticket/10797
|
||||||
# expect 1 GiB every 600 seconds when duration is tricky;
|
# expect 1 GiB every 600 seconds when duration is tricky;
|
||||||
# simple filetypes are generally safer so let's special-case those
|
# simple filetypes are generally safer so let's special-case those
|
||||||
coeff = 1800 if fext in EXTS_SPEC_SAFE else 600
|
safe = ("flac", "wav", "aif", "aiff", "opus")
|
||||||
dur = ret[".dur"][1] if ".dur" in ret else 900
|
coeff = 1800 if abspath.split(".")[-1].lower() in safe else 600
|
||||||
|
dur = ret[".dur"][1] if ".dur" in ret else 300
|
||||||
need = 0.2 + dur / coeff
|
need = 0.2 + dur / coeff
|
||||||
self.wait4ram(need, tpath)
|
self.wait4ram(need, tpath)
|
||||||
|
|
||||||
infile = abspath
|
|
||||||
if dur >= 900 or fext in self.exts_spec_unsafe:
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".spec.flac", delete=False) as f:
|
|
||||||
f.write(b"h")
|
|
||||||
infile = f.name
|
|
||||||
try:
|
|
||||||
self.untemp[tpath].append(infile)
|
|
||||||
except:
|
|
||||||
self.untemp[tpath] = [infile]
|
|
||||||
|
|
||||||
# fmt: off
|
|
||||||
cmd = [
|
|
||||||
b"ffmpeg",
|
|
||||||
b"-nostdin",
|
|
||||||
b"-v", b"error",
|
|
||||||
b"-hide_banner",
|
|
||||||
b"-i", fsenc(abspath),
|
|
||||||
b"-map", b"0:a:0",
|
|
||||||
b"-ac", b"1",
|
|
||||||
b"-ar", b"48000",
|
|
||||||
b"-sample_fmt", b"s16",
|
|
||||||
b"-t", b"900",
|
|
||||||
b"-y", fsenc(infile),
|
|
||||||
]
|
|
||||||
# fmt: on
|
|
||||||
self._run_ff(cmd, vn, "convt")
|
|
||||||
|
|
||||||
fc = "[0:a:0]aresample=48000{},showspectrumpic=s="
|
fc = "[0:a:0]aresample=48000{},showspectrumpic=s="
|
||||||
if "3" in fmt:
|
if "3" in fmt:
|
||||||
fc += "1280x1024,crop=1420:1056:70:48[o]"
|
fc += "1280x1024,crop=1420:1056:70:48[o]"
|
||||||
|
|
@ -870,7 +687,7 @@ class ThumbSrv(object):
|
||||||
b"-nostdin",
|
b"-nostdin",
|
||||||
b"-v", b"error",
|
b"-v", b"error",
|
||||||
b"-hide_banner",
|
b"-hide_banner",
|
||||||
b"-i", fsenc(infile),
|
b"-i", fsenc(abspath),
|
||||||
b"-filter_complex", fc.encode("utf-8"),
|
b"-filter_complex", fc.encode("utf-8"),
|
||||||
b"-map", b"[o]",
|
b"-map", b"[o]",
|
||||||
b"-frames:v", b"1",
|
b"-frames:v", b"1",
|
||||||
|
|
@ -891,7 +708,7 @@ class ThumbSrv(object):
|
||||||
]
|
]
|
||||||
|
|
||||||
cmd += [fsenc(tpath)]
|
cmd += [fsenc(tpath)]
|
||||||
self._run_ff(cmd, vn, "convt")
|
self._run_ff(cmd, vn)
|
||||||
|
|
||||||
def conv_mp3(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
def conv_mp3(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||||
quality = self.args.q_mp3.lower()
|
quality = self.args.q_mp3.lower()
|
||||||
|
|
@ -899,7 +716,7 @@ class ThumbSrv(object):
|
||||||
raise Exception("disabled in server config")
|
raise Exception("disabled in server config")
|
||||||
|
|
||||||
self.wait4ram(0.2, tpath)
|
self.wait4ram(0.2, tpath)
|
||||||
tags, rawtags, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||||
if "ac" not in tags:
|
if "ac" not in tags:
|
||||||
raise Exception("not audio")
|
raise Exception("not audio")
|
||||||
|
|
||||||
|
|
@ -930,74 +747,14 @@ class ThumbSrv(object):
|
||||||
fsenc(tpath)
|
fsenc(tpath)
|
||||||
]
|
]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
self._run_ff(cmd, vn, oom=300)
|
||||||
|
|
||||||
def conv_flac(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
|
||||||
if self.args.no_acode or not self.args.allow_flac:
|
|
||||||
raise Exception("flac not permitted in server config")
|
|
||||||
|
|
||||||
self.wait4ram(0.2, tpath)
|
|
||||||
tags, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
|
||||||
if "ac" not in tags:
|
|
||||||
raise Exception("not audio")
|
|
||||||
|
|
||||||
self.log("conv2 flac", 6)
|
|
||||||
|
|
||||||
# fmt: off
|
|
||||||
cmd = [
|
|
||||||
b"ffmpeg",
|
|
||||||
b"-nostdin",
|
|
||||||
b"-v", b"error",
|
|
||||||
b"-hide_banner",
|
|
||||||
b"-i", fsenc(abspath),
|
|
||||||
b"-map", b"0:a:0",
|
|
||||||
b"-c:a", b"flac",
|
|
||||||
fsenc(tpath)
|
|
||||||
]
|
|
||||||
# fmt: on
|
|
||||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
|
||||||
|
|
||||||
def conv_wav(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
|
||||||
if self.args.no_acode or not self.args.allow_wav:
|
|
||||||
raise Exception("wav not permitted in server config")
|
|
||||||
|
|
||||||
self.wait4ram(0.2, tpath)
|
|
||||||
tags, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
|
||||||
if "ac" not in tags:
|
|
||||||
raise Exception("not audio")
|
|
||||||
|
|
||||||
bits = tags[".bps"][1]
|
|
||||||
if bits == 0.0:
|
|
||||||
bits = tags[".bprs"][1]
|
|
||||||
|
|
||||||
codec = b"pcm_s32le"
|
|
||||||
if bits <= 16.0:
|
|
||||||
codec = b"pcm_s16le"
|
|
||||||
elif bits <= 24.0:
|
|
||||||
codec = b"pcm_s24le"
|
|
||||||
|
|
||||||
self.log("conv2 wav", 6)
|
|
||||||
|
|
||||||
# fmt: off
|
|
||||||
cmd = [
|
|
||||||
b"ffmpeg",
|
|
||||||
b"-nostdin",
|
|
||||||
b"-v", b"error",
|
|
||||||
b"-hide_banner",
|
|
||||||
b"-i", fsenc(abspath),
|
|
||||||
b"-map", b"0:a:0",
|
|
||||||
b"-c:a", codec,
|
|
||||||
fsenc(tpath)
|
|
||||||
]
|
|
||||||
# fmt: on
|
|
||||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
|
||||||
|
|
||||||
def conv_opus(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
def conv_opus(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||||
if self.args.no_acode or not self.args.q_opus:
|
if self.args.no_acode or not self.args.q_opus:
|
||||||
raise Exception("disabled in server config")
|
raise Exception("disabled in server config")
|
||||||
|
|
||||||
self.wait4ram(0.2, tpath)
|
self.wait4ram(0.2, tpath)
|
||||||
tags, rawtags, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||||
if "ac" not in tags:
|
if "ac" not in tags:
|
||||||
raise Exception("not audio")
|
raise Exception("not audio")
|
||||||
|
|
||||||
|
|
@ -1046,7 +803,7 @@ class ThumbSrv(object):
|
||||||
fsenc(tpath)
|
fsenc(tpath)
|
||||||
]
|
]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
self._run_ff(cmd, vn, oom=300)
|
||||||
|
|
||||||
def _conv_caf(
|
def _conv_caf(
|
||||||
self,
|
self,
|
||||||
|
|
@ -1086,7 +843,7 @@ class ThumbSrv(object):
|
||||||
fsenc(tmp_opus)
|
fsenc(tmp_opus)
|
||||||
]
|
]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
self._run_ff(cmd, vn, oom=300)
|
||||||
|
|
||||||
# iOS fails to play some "insufficiently complex" files
|
# iOS fails to play some "insufficiently complex" files
|
||||||
# (average file shorter than 8 seconds), so of course we
|
# (average file shorter than 8 seconds), so of course we
|
||||||
|
|
@ -1113,7 +870,7 @@ class ThumbSrv(object):
|
||||||
fsenc(tpath)
|
fsenc(tpath)
|
||||||
]
|
]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
self._run_ff(cmd, vn, oom=300)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# simple remux should be safe
|
# simple remux should be safe
|
||||||
|
|
@ -1132,7 +889,7 @@ class ThumbSrv(object):
|
||||||
fsenc(tpath)
|
fsenc(tpath)
|
||||||
]
|
]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
self._run_ff(cmd, vn, "aconvt", oom=300)
|
self._run_ff(cmd, vn, oom=300)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
wunlink(self.log, tmp_opus, vn.flags)
|
wunlink(self.log, tmp_opus, vn.flags)
|
||||||
|
|
@ -1234,8 +991,6 @@ class ThumbSrv(object):
|
||||||
# thumb file
|
# thumb file
|
||||||
try:
|
try:
|
||||||
b64, ts, ext = f.split(".")
|
b64, ts, ext = f.split(".")
|
||||||
if len(ts) > 8 and PTN_TS.match(ts):
|
|
||||||
ts = "yeahokay"
|
|
||||||
if len(b64) != 24 or len(ts) != 8 or ext not in exts:
|
if len(b64) != 24 or len(ts) != 8 or ext not in exts:
|
||||||
raise Exception()
|
raise Exception()
|
||||||
except:
|
except:
|
||||||
|
|
|
||||||
|
|
@ -53,11 +53,6 @@ class U2idx(object):
|
||||||
self.log("your python does not have sqlite3; searching will be disabled")
|
self.log("your python does not have sqlite3; searching will be disabled")
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.args.srch_icase:
|
|
||||||
self._open_db = self._open_db_icase
|
|
||||||
else:
|
|
||||||
self._open_db = self._open_db_std
|
|
||||||
|
|
||||||
assert sqlite3 # type: ignore # !rm
|
assert sqlite3 # type: ignore # !rm
|
||||||
|
|
||||||
self.active_id = ""
|
self.active_id = ""
|
||||||
|
|
@ -74,16 +69,6 @@ class U2idx(object):
|
||||||
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("u2idx", msg, c)
|
self.log_func("u2idx", msg, c)
|
||||||
|
|
||||||
def _open_db_std(self, *args, **kwargs):
|
|
||||||
assert sqlite3 # type: ignore # !rm
|
|
||||||
kwargs["check_same_thread"] = False
|
|
||||||
return sqlite3.connect(*args, **kwargs)
|
|
||||||
|
|
||||||
def _open_db_icase(self, *args, **kwargs):
|
|
||||||
db = self._open_db_std(*args, **kwargs)
|
|
||||||
db.create_function("casefold", 1, lambda x: x.casefold() if x else x)
|
|
||||||
return db
|
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
if not HAVE_SQLITE3:
|
if not HAVE_SQLITE3:
|
||||||
return
|
return
|
||||||
|
|
@ -149,9 +134,9 @@ class U2idx(object):
|
||||||
assert sqlite3 # type: ignore # !rm
|
assert sqlite3 # type: ignore # !rm
|
||||||
|
|
||||||
ptop = vn.realpath
|
ptop = vn.realpath
|
||||||
histpath = self.asrv.vfs.dbpaths.get(ptop)
|
histpath = self.asrv.vfs.histtab.get(ptop)
|
||||||
if not histpath:
|
if not histpath:
|
||||||
self.log("no dbpath for %r" % (ptop,))
|
self.log("no histpath for %r" % (ptop,))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
db_path = os.path.join(histpath, "up2k.db")
|
db_path = os.path.join(histpath, "up2k.db")
|
||||||
|
|
@ -163,7 +148,8 @@ class U2idx(object):
|
||||||
uri = ""
|
uri = ""
|
||||||
try:
|
try:
|
||||||
uri = "{}?mode=ro&nolock=1".format(Path(db_path).as_uri())
|
uri = "{}?mode=ro&nolock=1".format(Path(db_path).as_uri())
|
||||||
cur = self._open_db(uri, timeout=2, uri=True).cursor()
|
db = sqlite3.connect(uri, timeout=2, uri=True, check_same_thread=False)
|
||||||
|
cur = db.cursor()
|
||||||
cur.execute('pragma table_info("up")').fetchone()
|
cur.execute('pragma table_info("up")').fetchone()
|
||||||
self.log("ro: %r" % (db_path,))
|
self.log("ro: %r" % (db_path,))
|
||||||
except:
|
except:
|
||||||
|
|
@ -174,7 +160,7 @@ class U2idx(object):
|
||||||
if not cur:
|
if not cur:
|
||||||
# on windows, this steals the write-lock from up2k.deferred_init --
|
# on windows, this steals the write-lock from up2k.deferred_init --
|
||||||
# seen on win 10.0.17763.2686, py 3.10.4, sqlite 3.37.2
|
# seen on win 10.0.17763.2686, py 3.10.4, sqlite 3.37.2
|
||||||
cur = self._open_db(db_path, timeout=2).cursor()
|
cur = sqlite3.connect(db_path, timeout=2, check_same_thread=False).cursor()
|
||||||
self.log("opened %r" % (db_path,))
|
self.log("opened %r" % (db_path,))
|
||||||
|
|
||||||
self.cur[ptop] = cur
|
self.cur[ptop] = cur
|
||||||
|
|
@ -187,8 +173,6 @@ class U2idx(object):
|
||||||
if not HAVE_SQLITE3:
|
if not HAVE_SQLITE3:
|
||||||
return [], [], False
|
return [], [], False
|
||||||
|
|
||||||
icase = self.args.srch_icase
|
|
||||||
|
|
||||||
q = ""
|
q = ""
|
||||||
v: Union[str, int] = ""
|
v: Union[str, int] = ""
|
||||||
va: list[Union[str, int]] = []
|
va: list[Union[str, int]] = []
|
||||||
|
|
@ -196,7 +180,6 @@ class U2idx(object):
|
||||||
is_key = True
|
is_key = True
|
||||||
is_size = False
|
is_size = False
|
||||||
is_date = False
|
is_date = False
|
||||||
is_wark = False
|
|
||||||
field_end = "" # closing parenthesis or whatever
|
field_end = "" # closing parenthesis or whatever
|
||||||
kw_key = ["(", ")", "and ", "or ", "not "]
|
kw_key = ["(", ")", "and ", "or ", "not "]
|
||||||
kw_val = ["==", "=", "!=", ">", ">=", "<", "<=", "like "]
|
kw_val = ["==", "=", "!=", ">", ">=", "<", "<=", "like "]
|
||||||
|
|
@ -215,8 +198,6 @@ class U2idx(object):
|
||||||
is_key = kw in kw_key
|
is_key = kw in kw_key
|
||||||
uq = uq[len(kw) :]
|
uq = uq[len(kw) :]
|
||||||
ok = True
|
ok = True
|
||||||
if is_wark:
|
|
||||||
kw = "= "
|
|
||||||
q += kw
|
q += kw
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -251,17 +232,9 @@ class U2idx(object):
|
||||||
elif v == "path":
|
elif v == "path":
|
||||||
v = "trim(?||up.rd,'/')"
|
v = "trim(?||up.rd,'/')"
|
||||||
va.append("\nrd")
|
va.append("\nrd")
|
||||||
if icase:
|
|
||||||
v = "casefold(%s)" % (v,)
|
|
||||||
|
|
||||||
elif v == "name":
|
elif v == "name":
|
||||||
v = "up.fn"
|
v = "up.fn"
|
||||||
if icase:
|
|
||||||
v = "casefold(%s)" % (v,)
|
|
||||||
|
|
||||||
elif v == "w":
|
|
||||||
v = "substr(up.w,1,16)"
|
|
||||||
is_wark = True
|
|
||||||
|
|
||||||
elif v == "tags" or ptn_mt.match(v):
|
elif v == "tags" or ptn_mt.match(v):
|
||||||
have_mt = True
|
have_mt = True
|
||||||
|
|
@ -274,7 +247,7 @@ class U2idx(object):
|
||||||
v = "exists(select 1 from mt where mt.w = mtw and " + vq
|
v = "exists(select 1 from mt where mt.w = mtw and " + vq
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise Pebkac(400, "invalid key %r" % (v,))
|
raise Pebkac(400, "invalid key [{}]".format(v))
|
||||||
|
|
||||||
q += v + " "
|
q += v + " "
|
||||||
continue
|
continue
|
||||||
|
|
@ -303,14 +276,6 @@ class U2idx(object):
|
||||||
is_size = False
|
is_size = False
|
||||||
v = int(float(v) * 1024 * 1024)
|
v = int(float(v) * 1024 * 1024)
|
||||||
|
|
||||||
elif is_wark:
|
|
||||||
is_wark = False
|
|
||||||
v = v.strip("*")
|
|
||||||
if len(v) > 16:
|
|
||||||
v = v[:16]
|
|
||||||
if len(v) < 16:
|
|
||||||
raise Pebkac(400, "w/filehash must be 16+ chars")
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if v.startswith("*"):
|
if v.startswith("*"):
|
||||||
head = "'%'||"
|
head = "'%'||"
|
||||||
|
|
@ -320,12 +285,6 @@ class U2idx(object):
|
||||||
tail = "||'%'"
|
tail = "||'%'"
|
||||||
v = v[:-1]
|
v = v[:-1]
|
||||||
|
|
||||||
if icase and "casefold(" in q:
|
|
||||||
try:
|
|
||||||
v = unicode(v).casefold()
|
|
||||||
except:
|
|
||||||
v = unicode(v).lower()
|
|
||||||
|
|
||||||
q += " {}?{} ".format(head, tail)
|
q += " {}?{} ".format(head, tail)
|
||||||
va.append(v)
|
va.append(v)
|
||||||
is_key = True
|
is_key = True
|
||||||
|
|
@ -360,7 +319,7 @@ class U2idx(object):
|
||||||
uname: str,
|
uname: str,
|
||||||
vols: list[VFS],
|
vols: list[VFS],
|
||||||
uq: str,
|
uq: str,
|
||||||
uv: Union[list[str], list[Union[str, int]]],
|
uv: list[Union[str, int]],
|
||||||
have_mt: bool,
|
have_mt: bool,
|
||||||
sort: bool,
|
sort: bool,
|
||||||
lim: int,
|
lim: int,
|
||||||
|
|
@ -432,7 +391,7 @@ class U2idx(object):
|
||||||
fk_alg = 2 if "fka" in flags else 1
|
fk_alg = 2 if "fka" in flags else 1
|
||||||
c = cur.execute(uq, tuple(vuv))
|
c = cur.execute(uq, tuple(vuv))
|
||||||
for hit in c:
|
for hit in c:
|
||||||
w, ts, sz, rd, fn = hit[:5]
|
w, ts, sz, rd, fn, ip, at = hit[:7]
|
||||||
|
|
||||||
if rd.startswith("//") or fn.startswith("//"):
|
if rd.startswith("//") or fn.startswith("//"):
|
||||||
rd, fn = s3dec(rd, fn)
|
rd, fn = s3dec(rd, fn)
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue