Compare commits

..

No commits in common. "hovudstraum" and "v1.7.4" have entirely different histories.

306 changed files with 6799 additions and 53402 deletions

View file

@ -8,42 +8,33 @@ assignees: '9001'
--- ---
NOTE: NOTE:
**please use english, or include an english translation.** aside from that,
all of the below are optional, consider them as inspiration, delete and rewrite at will, thx md all of the below are optional, consider them as inspiration, delete and rewrite at will, thx md
### Describe the bug **Describe the bug**
a description of what the bug is a description of what the bug is
### To Reproduce **To Reproduce**
List of steps to reproduce the issue, or, if it's hard to reproduce, then at least a detailed explanation of what you did to run into it List of steps to reproduce the issue, or, if it's hard to reproduce, then at least a detailed explanation of what you did to run into it
### Expected behavior **Expected behavior**
a description of what you expected to happen a description of what you expected to happen
### Screenshots **Screenshots**
if applicable, add screenshots to help explain your problem, such as the kickass crashpage :^) if applicable, add screenshots to help explain your problem, such as the kickass crashpage :^)
### Server details (if you are using docker/podman) **Server details**
remove the ones that are not relevant: if the issue is possibly on the server-side, then mention some of the following:
* **server OS / version:** * server OS / version:
* **how you're running copyparty:** (docker/podman/something-else) * python version:
* **docker image:** (variant, version, and arch if you know) * copyparty arguments:
* **copyparty arguments and/or config-file:** * filesystem (`lsblk -f` on linux):
### Server details (if you're NOT using docker/podman) **Client details**
remove the ones that are not relevant:
* **server OS / version:**
* **what copyparty did you grab:** (sfx/exe/pip/arch/...)
* **how you're running it:** (in a terminal, as a systemd-service, ...)
* run copyparty with `--version` and grab the last 3 lines (they start with `copyparty`, `CPython`, `sqlite`) and paste them below this line:
* **copyparty arguments and/or config-file:**
### Client details
if the issue is possibly on the client-side, then mention some of the following: if the issue is possibly on the client-side, then mention some of the following:
* the device type and model: * the device type and model:
* OS version: * OS version:
* browser version: * browser version:
### Additional context **Additional context**
any other context about the problem here any other context about the problem here

View file

@ -7,8 +7,6 @@ assignees: '9001'
--- ---
NOTE:
**please use english, or include an english translation.** aside from that,
all of the below are optional, consider them as inspiration, delete and rewrite at will all of the below are optional, consider them as inspiration, delete and rewrite at will
**is your feature request related to a problem? Please describe.** **is your feature request related to a problem? Please describe.**

5
.gitignore vendored
View file

@ -12,7 +12,6 @@ copyparty.egg-info/
/dist/ /dist/
/py2/ /py2/
/sfx* /sfx*
/pyz/
/unt/ /unt/
/log/ /log/
@ -30,7 +29,6 @@ copyparty/res/COPYING.txt
copyparty/web/deps/ copyparty/web/deps/
srv/ srv/
scripts/docker/i/ scripts/docker/i/
scripts/deps-docker/uncomment.py
contrib/package/arch/pkg/ contrib/package/arch/pkg/
contrib/package/arch/src/ contrib/package/arch/src/
@ -43,6 +41,3 @@ scripts/docker/*.err
# nix build output link # nix build output link
result result
# IDEA config
.idea/

7
.vscode/launch.json vendored
View file

@ -9,17 +9,14 @@
"console": "integratedTerminal", "console": "integratedTerminal",
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"justMyCode": false, "justMyCode": false,
"env": {
"PYDEVD_DISABLE_FILE_VALIDATION": "1",
"PYTHONWARNINGS": "always", //error
},
"args": [ "args": [
//"-nw", //"-nw",
"-ed", "-ed",
"-emp", "-emp",
"-e2dsa", "-e2dsa",
"-e2ts", "-e2ts",
"-mtp=.bpm=f,bin/mtag/audio-bpm.py", "-mtp",
".bpm=f,bin/mtag/audio-bpm.py",
"-aed:wark", "-aed:wark",
"-vsrv::r:rw,ed:c,dupe", "-vsrv::r:rw,ed:c,dupe",
"-vdist:dist:r" "-vdist:dist:r"

2
.vscode/launch.py vendored
View file

@ -41,7 +41,7 @@ if sfx:
argv = [sys.executable, sfx] + argv argv = [sys.executable, sfx] + argv
sp.check_call(argv) sp.check_call(argv)
elif re.search(" -j ?[0-9]", " ".join(argv)): elif re.search(" -j ?[0-9]", " ".join(argv)):
argv = [sys.executable, "-Wa", "-m", "copyparty"] + argv argv = [sys.executable, "-m", "copyparty"] + argv
sp.check_call(argv) sp.check_call(argv)
else: else:
sys.path.insert(0, os.getcwd()) sys.path.insert(0, os.getcwd())

24
.vscode/settings.json vendored
View file

@ -22,9 +22,6 @@
"terminal.ansiBrightCyan": "#9cf0ed", "terminal.ansiBrightCyan": "#9cf0ed",
"terminal.ansiBrightWhite": "#ffffff", "terminal.ansiBrightWhite": "#ffffff",
}, },
"python.terminal.activateEnvironment": false,
"python.analysis.enablePytestSupport": false,
"python.analysis.typeCheckingMode": "standard",
"python.testing.pytestEnabled": false, "python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true, "python.testing.unittestEnabled": true,
"python.testing.unittestArgs": [ "python.testing.unittestArgs": [
@ -34,8 +31,23 @@
"-p", "-p",
"test_*.py" "test_*.py"
], ],
// python3 -m isort --py=27 --profile=black ~/dev/copyparty/{copyparty,tests}/*.py && python3 -m black -t py27 ~/dev/copyparty/{copyparty,tests,bin}/*.py $(find ~/dev/copyparty/copyparty/stolen -iname '*.py') "python.linting.pylintEnabled": true,
"editor.formatOnSave": false, "python.linting.flake8Enabled": true,
"python.linting.banditEnabled": true,
"python.linting.mypyEnabled": true,
"python.linting.flake8Args": [
"--max-line-length=120",
"--ignore=E722,F405,E203,W503,W293,E402,E501,E128,E226",
],
"python.linting.banditArgs": [
"--ignore=B104,B110,B112"
],
// python3 -m isort --py=27 --profile=black copyparty/
"python.formatting.provider": "none",
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"editor.formatOnSave": true,
"[html]": { "[html]": {
"editor.formatOnSave": false, "editor.formatOnSave": false,
"editor.autoIndent": "keep", "editor.autoIndent": "keep",
@ -46,4 +58,6 @@
"files.associations": { "files.associations": {
"*.makefile": "makefile" "*.makefile": "makefile"
}, },
"python.linting.enabled": true,
"python.pythonPath": "/usr/bin/python3"
} }

1
.vscode/tasks.json vendored
View file

@ -11,7 +11,6 @@
"type": "shell", "type": "shell",
"command": "${config:python.pythonPath}", "command": "${config:python.pythonPath}",
"args": [ "args": [
"-Wa", //-We
".vscode/launch.py" ".vscode/launch.py"
] ]
} }

View file

@ -1,58 +1,3 @@
* **found a bug?** [create an issue!](https://github.com/9001/copyparty/issues) or let me know in the [discord](https://discord.gg/25J8CdTT6G) :> * do something cool
* **fixed a bug?** create a PR or post a patch! big thx in advance :>
* **have a cool idea?** let's discuss it! anywhere's fine, you choose.
but please: really tho, send a PR or an issue or whatever, all appreciated, anything goes, just behave aight
# do not use AI / LMM when writing code
copyparty is 100% organic, free-range, human-written software!
> ⚠ you are now entering a no-copilot zone
the *only* place where LMM/AI *may* be accepted is for [localization](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) if you are fluent and have confirmed that the translation is accurate.
sorry for the harsh tone, but this is important to me 🙏
# contribution ideas
## documentation
I think we can agree that the documentation leaves a LOT to be desired. I've realized I'm not exactly qualified for this 😅 but maybe the [soon-to-come setup GUI](https://github.com/9001/copyparty/issues/57) will make this more manageable. The best documentation is the one that never had to be written, right? :> so I suppose we can give this a wait-and-see approach for a bit longer.
## crazy ideas & features
assuming they won't cause too much problems or side-effects :>
i think someone was working on a way to list directories over DNS for example...
if you wanna have a go at coding it up yourself then maybe mention the idea on discord before you get too far, otherwise just go nuts 👍
## others
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 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.
* **docker improvements** -- I don't really know what I'm doing when it comes to containers, so I'm sure there's a *huge* room for improvement here, mainly regarding how you're supposed to use the container with kubernetes / docker-compose / any of the other popular ways to do things. At some point I swear I'll start learning about docker so I can pick up clach04's [docker-compose draft](https://github.com/9001/copyparty/issues/38) and learn how that stuff ticks, unless someone beats me to it!
* **packaging** for various linux distributions -- this could either be as simple as just plopping the sfx.py in the right place and calling that from systemd (the archlinux package [originally did this](https://github.com/9001/copyparty/pull/18)); maybe with a small config-file which would cause copyparty to load settings from `/etc/copyparty.d` (like the [archlinux package](https://github.com/9001/copyparty/tree/hovudstraum/contrib/package/arch) does with `copyparty.conf`), or it could be a proper installation of the copyparty python package into /usr/lib or similar (the archlinux package [eventually went for this approach](https://github.com/9001/copyparty/pull/26))
* [fpm](https://github.com/jordansissel/fpm) can probably help with the technical part of it, but someone needs to handle distro relations :-)
* **software integration** -- I'm sure there's a lot of usecases where copyparty could complement something else, or the other way around, so any ideas or any work in this regard would be dope. This doesn't necessarily have to be code inside copyparty itself;
* [hooks](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks) -- these are small programs which are called by copyparty when certain things happen (files are uploaded, someone hits a 404, etc.), and could be a fun way to add support for more usecases
* [parser plugins](https://github.com/9001/copyparty/tree/hovudstraum/bin/mtag) -- if you want to have copyparty analyze and index metadata for some oddball file-formats, then additional plugins would be neat :>

View file

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2019 ed <oss@ocv.me> Copyright (c) 2019 ed
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

1629
README.md

File diff suppressed because it is too large Load diff

View file

@ -15,18 +15,22 @@ produces a chronological list of all uploads by collecting info from up2k databa
# [`partyfuse.py`](partyfuse.py) # [`partyfuse.py`](partyfuse.py)
* mount a copyparty server as a local filesystem (read-only) * mount a copyparty server as a local filesystem (read-only)
* **supports Windows!** -- expect `194 MiB/s` sequential read * **supports Windows!** -- expect `194 MiB/s` sequential read
* **supports Linux** -- expect `600 MiB/s` sequential read * **supports Linux** -- expect `117 MiB/s` sequential read
* **supports macos** -- expect `85 MiB/s` sequential read * **supports macos** -- expect `85 MiB/s` sequential read
filecache is default-on for windows and macos;
* macos readsize is 64kB, so speed ~32 MiB/s without the cache
* windows readsize varies by software; explorer=1M, pv=32k
note that copyparty should run with `-ed` to enable dotfiles (hidden otherwise) note that copyparty should run with `-ed` to enable dotfiles (hidden otherwise)
and consider using [../docs/rclone.md](../docs/rclone.md) instead; usually a bit faster, especially on windows also consider using [../docs/rclone.md](../docs/rclone.md) instead for 5x performance
## to run this on windows: ## to run this on windows:
* install [winfsp](https://github.com/billziss-gh/winfsp/releases/latest) and [python 3](https://www.python.org/downloads/) * install [winfsp](https://github.com/billziss-gh/winfsp/releases/latest) and [python 3](https://www.python.org/downloads/)
* [x] add python 3.x to PATH (it asks during install) * [x] add python 3.x to PATH (it asks during install)
* `python -m pip install --user fusepy` (or grab a copy of `fuse.py` from the `connect` page on your copyparty, and keep it in the same folder) * `python -m pip install --user fusepy`
* `python ./partyfuse.py n: http://192.168.1.69:3923/` * `python ./partyfuse.py n: http://192.168.1.69:3923/`
10% faster in [msys2](https://www.msys2.org/), 700% faster if debug prints are enabled: 10% faster in [msys2](https://www.msys2.org/), 700% faster if debug prints are enabled:
@ -78,6 +82,3 @@ cd /mnt/nas/music/.hist
# [`prisonparty.sh`](prisonparty.sh) # [`prisonparty.sh`](prisonparty.sh)
* run copyparty in a chroot, preventing any accidental file access * run copyparty in a chroot, preventing any accidental file access
* creates bindmounts for /bin, /lib, and so on, see `sysdirs=` * creates bindmounts for /bin, /lib, and so on, see `sysdirs=`
# [`bubbleparty.sh`](bubbleparty.sh)
* run copyparty in an isolated process, preventing any accidental file access and more

View file

@ -1,19 +0,0 @@
#!/bin/sh
# usage: ./bubbleparty.sh ./copyparty-sfx.py ....
bwrap \
--unshare-all \
--ro-bind /usr /usr \
--ro-bind /bin /bin \
--ro-bind /lib /lib \
--ro-bind /etc/resolv.conf /etc/resolv.conf \
--dev-bind /dev /dev \
--dir /tmp \
--dir /var \
--bind $(pwd) $(pwd) \
--share-net \
--die-with-parent \
--file 11 /etc/passwd \
--file 12 /etc/group \
"$@" \
11< <(getent passwd $(id -u) 65534) \
12< <(getent group $(id -g) 65534)

View file

@ -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()
@ -207,7 +207,7 @@ def examples():
def main(): def main():
global NC, BY_PATH # pylint: disable=global-statement global NC, BY_PATH
os.system("") os.system("")
print() print()
@ -282,8 +282,7 @@ def main():
if ver == "corrupt": if ver == "corrupt":
die("{} database appears to be corrupt, sorry") die("{} database appears to be corrupt, sorry")
iver = int(ver) if ver < DB_VER1 or ver > DB_VER2:
if iver < DB_VER1 or iver > DB_VER2:
m = f"{n} db is version {ver}, this tool only supports versions between {DB_VER1} and {DB_VER2}, please upgrade it with copyparty first" m = f"{n} db is version {ver}, this tool only supports versions between {DB_VER1} and {DB_VER2}, please upgrade it with copyparty first"
die(m) die(m)

View file

@ -1,37 +0,0 @@
replace the standard 404 / 403 responses with plugins
# usage
load plugins either globally with `--on404 ~/dev/copyparty/bin/handlers/sorry.py` or for a specific volume with `:c,on404=~/handlers/sorry.py`
# api
each plugin must define a `main()` which takes 3 arguments;
* `cli` is an instance of [copyparty/httpcli.py](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/httpcli.py) (the monstrosity itself)
* `vn` is the VFS which overlaps with the requested URL, and
* `rem` is the URL remainder below the VFS mountpoint
* so `vn.vpath + rem` == `cli.vpath` == original request
# examples
## on404
* [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
* [nooo.py](nooo.py) replies with an endless noooooooooooooo
* [never404.py](never404.py) 100% guarantee that 404 will never be a thing again as it automatically creates dummy files whenever necessary
* [caching-proxy.py](caching-proxy.py) transforms copyparty into a squid/varnish knockoff
## on403
* [ip-ok.py](ip-ok.py) disables security checks if client-ip is 1.2.3.4
# notes
* on403 only works for trivial stuff (basic http access) since I haven't been able to think of any good usecases for it (was just easy to add while doing on404)

View file

@ -1,36 +0,0 @@
# assume each requested file exists on another webserver and
# download + mirror them as they're requested
# (basically pretend we're warnish)
import os
import requests
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from copyparty.httpcli import HttpCli
def main(cli: "HttpCli", vn, rem):
url = "https://mirrors.edge.kernel.org/alpine/" + rem
abspath = os.path.join(vn.realpath, rem)
# sneaky trick to preserve a requests-session between downloads
# so it doesn't have to spend ages reopening https connections;
# luckily we can stash it inside the copyparty client session,
# name just has to be definitely unused so "hacapo_req_s" it is
req_s = getattr(cli.conn, "hacapo_req_s", None) or requests.Session()
setattr(cli.conn, "hacapo_req_s", req_s)
try:
os.makedirs(os.path.dirname(abspath), exist_ok=True)
with req_s.get(url, stream=True, timeout=69) as r:
r.raise_for_status()
with open(abspath, "wb", 64 * 1024) as f:
for buf in r.iter_content(chunk_size=64 * 1024):
f.write(buf)
except:
os.unlink(abspath)
return "false"
return "retry"

View file

@ -1,6 +0,0 @@
# disable permission checks and allow access if client-ip is 1.2.3.4
def main(cli, vn, rem):
if cli.ip == "1.2.3.4":
return "allow"

View file

@ -1,11 +0,0 @@
# create a dummy file and let copyparty return it
def main(cli, vn, rem):
print("hello", cli.ip)
abspath = vn.canonical(rem)
with open(abspath, "wb") as f:
f.write(b"404? not on MY watch!")
return "retry"

View file

@ -1,16 +0,0 @@
# reply with an endless "noooooooooooooooooooooooo"
def say_no():
yield b"n"
while True:
yield b"o" * 4096
def main(cli, vn, rem):
cli.send_headers(None, 404, "text/plain")
for chunk in say_no():
cli.s.sendall(chunk)
return "false"

View file

@ -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"

View file

@ -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"

View file

@ -1,7 +0,0 @@
# sends a custom response instead of the usual 404
def main(cli, vn, rem):
msg = f"sorry {cli.ip} but {cli.vpath} doesn't exist"
return str(cli.reply(msg.encode("utf-8"), 404, "text/plain"))

View file

@ -2,7 +2,7 @@ standalone programs which are executed by copyparty when an event happens (uploa
these programs either take zero arguments, or a filepath (the affected file), or a json message with filepath + additional info these programs either take zero arguments, or a filepath (the affected file), or a json message with filepath + additional info
run copyparty with `--help-hooks` for usage details / hook type explanations (xm/xbu/xau/xiu/xbc/xac/xbr/xar/xbd/xad/xban) run copyparty with `--help-hooks` for usage details / hook type explanations (xbu/xau/xiu/xbr/xar/xbd/xad)
> **note:** in addition to event hooks (the stuff described here), copyparty has another api to run your programs/scripts while providing way more information such as audio tags / video codecs / etc and optionally daisychaining data between scripts in a processing pipeline; if that's what you want then see [mtp plugins](../mtag/) instead > **note:** in addition to event hooks (the stuff described here), copyparty has another api to run your programs/scripts while providing way more information such as audio tags / video codecs / etc and optionally daisychaining data between scripts in a processing pipeline; if that's what you want then see [mtp plugins](../mtag/) instead
@ -13,9 +13,6 @@ run copyparty with `--help-hooks` for usage details / hook type explanations (xm
* [image-noexif.py](image-noexif.py) removes image exif by overwriting / directly editing the uploaded file * [image-noexif.py](image-noexif.py) removes image exif by overwriting / directly editing the uploaded file
* [discord-announce.py](discord-announce.py) announces new uploads on discord using webhooks ([example](https://user-images.githubusercontent.com/241032/215304439-1c1cb3c8-ec6f-4c17-9f27-81f969b1811a.png)) * [discord-announce.py](discord-announce.py) announces new uploads on discord using webhooks ([example](https://user-images.githubusercontent.com/241032/215304439-1c1cb3c8-ec6f-4c17-9f27-81f969b1811a.png))
* [reject-mimetype.py](reject-mimetype.py) rejects uploads unless the mimetype is acceptable * [reject-mimetype.py](reject-mimetype.py) rejects uploads unless the mimetype is acceptable
* [into-the-cache-it-goes.py](into-the-cache-it-goes.py) avoids bugs in caching proxies by immediately downloading each file that is uploaded
* [podcast-normalizer.py](podcast-normalizer.py) creates a second file with dynamic-range-compression whenever an audio file is uploaded
* good example of the `idx` [hook effect](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hook-effects) to tell copyparty about additional files to scan/index
# upload batches # upload batches
@ -26,12 +23,7 @@ these are `--xiu` hooks; unlike `xbu` and `xau` (which get executed on every sin
# before upload # before upload
* [reject-extension.py](reject-extension.py) rejects uploads if they match a list of file extensions * [reject-extension.py](reject-extension.py) rejects uploads if they match a list of file extensions
* [reloc-by-ext.py](reloc-by-ext.py) redirects an upload to another destination based on the file extension
* good example of the `reloc` [hook effect](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hook-effects)
# on message # on message
* [wget.py](wget.py) lets you download files by POSTing URLs to copyparty * [wget.py](wget.py) lets you download files by POSTing URLs to copyparty
* [qbittorrent-magnet.py](qbittorrent-magnet.py) starts downloading a torrent if you post a magnet url
* [usb-eject.py](usb-eject.py) adds web-UI buttons to safe-remove usb flashdrives shared through copyparty
* [msg-log.py](msg-log.py) is a guestbook; logs messages to a doc in the same folder

View file

@ -12,28 +12,19 @@ announces a new upload on discord
example usage as global config: example usage as global config:
--xau f,t5,j,bin/hooks/discord-announce.py --xau f,t5,j,bin/hooks/discord-announce.py
parameters explained,
xau = execute after upload
f = fork; don't delay other hooks while this is running
t5 = timeout if it's still running after 5 sec
j = this hook needs upload information as json (not just the filename)
example usage as a volflag (per-volume config): example usage as a volflag (per-volume config):
-v srv/inc:inc:r:rw,ed:c,xau=f,t5,j,bin/hooks/discord-announce.py -v srv/inc:inc:r:rw,ed:c,xau=f,t5,j,bin/hooks/discord-announce.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(share filesystem-path srv/inc as volume /inc, (share filesystem-path srv/inc as volume /inc,
readable by everyone, read-write for user 'ed', readable by everyone, read-write for user 'ed',
running this plugin on all uploads with the params explained above) running this plugin on all uploads with the params listed below)
example usage as a volflag in a copyparty config file: parameters explained,
[/inc] xbu = execute after upload
srv/inc f = fork; don't wait for it to finish
accs: t5 = timeout if it's still running after 5 sec
r: * j = provide upload information as json; not just the filename
rw: ed
flags:
xau: f,t5,j,bin/hooks/discord-announce.py
replace "xau" with "xbu" to announce Before upload starts instead of After completion replace "xau" with "xbu" to announce Before upload starts instead of After completion

View file

@ -1,140 +0,0 @@
#!/usr/bin/env python3
import sys
import json
import shutil
import platform
import subprocess as sp
from urllib.parse import quote
_ = r"""
try to avoid race conditions in caching proxies
(primarily cloudflare, but probably others too)
by means of the most obvious solution possible:
just as each file has finished uploading, use
the server's external URL to download the file
so that it ends up in the cache, warm and snug
this intentionally delays the upload response
as it waits for the file to finish downloading
before copyparty is allowed to return the URL
NOTE: you must edit this script before use,
replacing https://example.com with your URL
NOTE: if the files are only accessible with a
password and/or filekey, you must also add
a cromulent password in the PASSWORD field
NOTE: needs either wget, curl, or "requests":
python3 -m pip install --user -U requests
example usage as global config:
--xau j,t10,bin/hooks/into-the-cache-it-goes.py
parameters explained,
xau = execute after upload
j = this hook needs upload information as json (not just the filename)
t10 = abort download and continue if it takes longer than 10sec
example usage as a volflag (per-volume config):
-v srv/inc:inc:r:rw,ed:c,xau=j,t10,bin/hooks/into-the-cache-it-goes.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(share filesystem-path srv/inc as volume /inc,
readable by everyone, read-write for user 'ed',
running this plugin on all uploads with params explained above)
example usage as a volflag in a copyparty config file:
[/inc]
srv/inc
accs:
r: *
rw: ed
flags:
xau: j,t10,bin/hooks/into-the-cache-it-goes.py
"""
# replace this with your site's external URL
# (including the :portnumber if necessary)
SITE_URL = "https://example.com"
# if downloading is protected by passwords or filekeys,
# specify a valid password between the quotes below:
PASSWORD = ""
# if file is larger than this, skip download
MAX_MEGABYTES = 8
# =============== END OF CONFIG ===============
WINDOWS = platform.system() == "Windows"
def main():
fun = download_with_python
if shutil.which("curl"):
fun = download_with_curl
elif shutil.which("wget"):
fun = download_with_wget
inf = json.loads(sys.argv[1])
if inf["sz"] > 1024 * 1024 * MAX_MEGABYTES:
print("[into-the-cache] file is too large; will not download")
return
file_url = "/"
if inf["vp"]:
file_url += inf["vp"] + "/"
file_url += inf["ap"].replace("\\", "/").split("/")[-1]
file_url = SITE_URL.rstrip("/") + quote(file_url, safe=b"/")
print("[into-the-cache] %s(%s)" % (fun.__name__, file_url))
fun(file_url, PASSWORD.strip())
print("[into-the-cache] Download OK")
def download_with_curl(url, pw):
cmd = ["curl"]
if pw:
cmd += ["-HPW:%s" % (pw,)]
nah = sp.DEVNULL
sp.check_call(cmd + [url], stdout=nah, stderr=nah)
def download_with_wget(url, pw):
cmd = ["wget", "-O"]
cmd += ["nul" if WINDOWS else "/dev/null"]
if pw:
cmd += ["--header=PW:%s" % (pw,)]
nah = sp.DEVNULL
sp.check_call(cmd + [url], stdout=nah, stderr=nah)
def download_with_python(url, pw):
import requests
headers = {}
if pw:
headers["PW"] = pw
with requests.get(url, headers=headers, stream=True) as r:
r.raise_for_status()
for _ in r.iter_content(chunk_size=1024 * 256):
pass
if __name__ == "__main__":
main()

View file

@ -1,136 +0,0 @@
#!/usr/bin/env python
# coding: utf-8
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
from __future__ import print_function, unicode_literals
import json
import os
import sys
import time
try:
from datetime import datetime, timezone
except:
from datetime import datetime
_ = r"""
use copyparty as a dumb messaging server / guestbook thing;
accepts guestbook entries from 📟 (message-to-server-log) in the web-ui
initially contributed by @clach04 in https://github.com/9001/copyparty/issues/35 (thanks!)
example usage as global config:
python copyparty-sfx.py --xm j,bin/hooks/msg-log.py
parameters explained,
xm = execute on message (📟)
j = this hook needs message information as json (not just the message-text)
example usage as a volflag (per-volume config):
python copyparty-sfx.py -v srv/log:log:r:c,xm=j,bin/hooks/msg-log.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^
(share filesystem-path srv/log as volume /log, readable by everyone,
running this plugin on all messages with the params explained above)
example usage as a volflag in a copyparty config file:
[/log]
srv/log
accs:
r: *
flags:
xm: j,bin/hooks/msg-log.py
"""
# output filename
FILENAME = os.environ.get("COPYPARTY_MESSAGE_FILENAME", "") or "README.md"
# set True to write in descending order (newest message at top of file);
# note that this becomes very slow/expensive as the file gets bigger
DESCENDING = True
# the message template; the following parameters are provided by copyparty and can be referenced below:
# 'ap' = absolute filesystem path where the message was posted
# 'vp' = virtual path (URL 'path') where the message was posted
# 'mt' = 'at' = unix-timestamp when the message was posted
# 'datetime' = ISO-8601 time when the message was posted
# 'sz' = message size in bytes
# 'host' = the server hostname which the user was accessing (URL 'host')
# 'user' = username (if logged in), otherwise '*'
# 'txt' = the message text itself
# (uncomment the print(msg_info) to see if additional information has been introduced by copyparty since this was written)
TEMPLATE = """
🕒 %(datetime)s, 👤 %(user)s @ %(ip)s
%(txt)s
"""
def write_ascending(filepath, msg_text):
with open(filepath, "a", encoding="utf-8", errors="replace") as outfile:
outfile.write(msg_text)
def write_descending(filepath, msg_text):
lockpath = filepath + ".lock"
got_it = False
for _ in range(16):
try:
os.mkdir(lockpath)
got_it = True
break
except:
time.sleep(0.1)
continue
if not got_it:
return sys.exit(1)
try:
oldpath = filepath + ".old"
os.rename(filepath, oldpath)
with open(oldpath, "r", encoding="utf-8", errors="replace") as infile, open(
filepath, "w", encoding="utf-8", errors="replace"
) as outfile:
outfile.write(msg_text)
while True:
buf = infile.read(4096)
if not buf:
break
outfile.write(buf)
finally:
try:
os.unlink(oldpath)
except:
pass
os.rmdir(lockpath)
def main(argv=None):
if argv is None:
argv = sys.argv
msg_info = json.loads(sys.argv[1])
# print(msg_info)
try:
dt = datetime.fromtimestamp(msg_info["at"], timezone.utc)
except:
dt = datetime.utcfromtimestamp(msg_info["at"])
msg_info["datetime"] = dt.strftime("%Y-%m-%d, %H:%M:%S")
msg_text = TEMPLATE % msg_info
filepath = os.path.join(msg_info["ap"], FILENAME)
if DESCENDING and os.path.exists(filepath):
write_descending(filepath, msg_text)
else:
write_ascending(filepath, msg_text)
print(msg_text)
if __name__ == "__main__":
main()

View file

@ -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

View file

@ -4,7 +4,7 @@ import json
import os import os
import sys import sys
import subprocess as sp import subprocess as sp
from datetime import datetime, timezone from datetime import datetime
from plyer import notification from plyer import notification
@ -43,8 +43,7 @@ def main():
fp = inf["ap"] fp = inf["ap"]
sz = humansize(inf["sz"]) sz = humansize(inf["sz"])
dp, fn = os.path.split(fp) dp, fn = os.path.split(fp)
dt = datetime.fromtimestamp(inf["mt"], timezone.utc) mt = datetime.utcfromtimestamp(inf["mt"]).strftime("%Y-%m-%d %H:%M:%S")
mt = dt.strftime("%Y-%m-%d %H:%M:%S")
msg = f"{fn} ({sz})\n📁 {dp}" msg = f"{fn} ({sz})\n📁 {dp}"
title = "File received" title = "File received"

View file

@ -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,))

View file

@ -1,128 +0,0 @@
#!/usr/bin/env python3
# coding: utf-8
import os
import sys
import json
import shutil
import subprocess as sp
_ = r"""
start downloading a torrent by POSTing a magnet URL to copyparty,
for example using 📟 (message-to-server-log) in the web-ui
by default it will download the torrent to the folder you were in
when you pasted the magnet into the message-to-server-log field
you can optionally specify another location by adding a whitespace
after the magnet URL followed by the name of the subfolder to DL into,
or for example "anime/airing" would download to /srv/media/anime/airing
because the keyword "anime" is in the DESTS config below
needs python3
example usage as global config (not a good idea):
python copyparty-sfx.py --xm aw,f,j,t60,bin/hooks/qbittorrent-magnet.py
parameters explained,
xm = execute on message (📟)
aw = only users with write-access can use this
f = fork; don't delay other hooks while this is running
j = provide message information as json (not just the text)
t60 = abort if qbittorrent has to think about it for more than 1 min
example usage as a volflag (per-volume config, much better):
-v srv/qb:qb:A,ed:c,xm=aw,f,j,t60,bin/hooks/qbittorrent-magnet.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(share filesystem-path srv/qb as volume /qb with Admin for user 'ed',
running this plugin on all messages with the params explained above)
example usage as a volflag in a copyparty config file:
[/qb]
srv/qb
accs:
A: ed
flags:
xm: aw,f,j,t60,bin/hooks/qbittorrent-magnet.py
the volflag examples only kicks in if you send the torrent magnet
while you're in the /qb folder (or any folder below there)
"""
# list of usernames to allow
ALLOWLIST = [ "ed", "morpheus" ]
# list of destination aliases to translate into full filesystem
# paths; takes effect if the first folder component in the
# custom download location matches anything in this dict
DESTS = {
"iso": "/srv/pub/linux-isos",
"anime": "/srv/media/anime",
}
def main():
inf = json.loads(sys.argv[1])
url = inf["txt"]
if not url.lower().startswith("magnet:?"):
# not a magnet, abort
return
if inf["user"] not in ALLOWLIST:
print("🧲 denied for user", inf["user"])
return
# might as well run the command inside the filesystem folder
# which matches the URL that the magnet message was sent to
os.chdir(inf["ap"])
# is there is a custom download location in the url?
dst = ""
if " " in url:
url, dst = url.split(" ", 1)
# is the location in the predefined list of locations?
parts = dst.replace("\\", "/").split("/")
if parts[0] in DESTS:
dst = os.path.join(DESTS[parts[0]], *(parts[1:]))
else:
# nope, so download to the current folder instead;
# comment the dst line below to instead use the default
# download location from your qbittorrent settings
dst = inf["ap"]
pass
# archlinux has a -nox suffix for qbittorrent if headless
# so check if we should be using that
if shutil.which("qbittorrent-nox"):
torrent_bin = "qbittorrent-nox"
else:
torrent_bin = "qbittorrent"
# the command to add a new torrent, adjust if necessary
cmd = [torrent_bin, url]
if dst:
cmd += ["--save-path=%s" % (dst,)]
# if copyparty and qbittorrent are running as different users
# you may have to do something like the following
# (assuming qbittorrent* is nopasswd-allowed in sudoers):
#
# cmd = ["sudo", "-u", "qbitter"] + cmd
print("🧲", cmd)
try:
sp.check_call(cmd)
except:
print("🧲 FAILED TO ADD", url)
if __name__ == "__main__":
main()

View file

@ -1,130 +0,0 @@
#!/usr/bin/env python3
import json
import os
import re
import sys
_ = r"""
relocate/redirect incoming uploads according to file extension or name
example usage as global config:
--xbu j,c1,bin/hooks/reloc-by-ext.py
parameters explained,
xbu = execute before upload
j = this hook needs upload information as json (not just the filename)
c1 = this hook returns json on stdout, so tell copyparty to read that
example usage as a volflag (per-volume config):
-v srv/inc:inc:r:rw,ed:c,xbu=j,c1,bin/hooks/reloc-by-ext.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(share filesystem-path srv/inc as volume /inc,
readable by everyone, read-write for user 'ed',
running this plugin on all uploads with the params explained above)
example usage as a volflag in a copyparty config file:
[/inc]
srv/inc
accs:
r: *
rw: ed
flags:
xbu: j,c1,bin/hooks/reloc-by-ext.py
note: this could also work as an xau hook (after-upload), but
because it doesn't need to read the file contents its better
as xbu (before-upload) since that's safer / less buggy,
and only xbu works with up2k (dragdrop into browser)
"""
PICS = "avif bmp gif heic heif jpeg jpg jxl png psd qoi tga tif tiff webp"
VIDS = "3gp asf avi flv mkv mov mp4 mpeg mpeg2 mpegts mpg mpg2 nut ogm ogv rm ts vob webm wmv"
MUSIC = "aac aif aiff alac amr ape dfpwm flac m4a mp3 ogg opus ra tak tta wav wma wv"
def main():
inf = json.loads(sys.argv[1])
vdir, fn = os.path.split(inf["vp"])
try:
fn, ext = fn.rsplit(".", 1)
except:
# no file extension; pretend it's "bin"
ext = "bin"
ext = ext.lower()
# this function must end by printing the action to perform;
# that's handled by the print(json.dumps(... at the bottom
#
# the action can contain the following keys:
# "vp" is the folder URL to move the upload to,
# "ap" is the filesystem-path to move it to (but "vp" is safer),
# "fn" overrides the final filename to use
##
## some example actions to take; pick one by
## selecting it inside the print at the end:
##
# move all uploads to one specific folder
into_junk = {"vp": "/junk"}
# create a subfolder named after the filetype and move it into there
into_subfolder = {"vp": ext}
# move it into a toplevel folder named after the filetype
into_toplevel = {"vp": "/" + ext}
# move it into a filetype-named folder next to the target folder
into_sibling = {"vp": "../" + ext}
# move images into "/just/pics", vids into "/just/vids",
# music into "/just/tunes", and anything else as-is
if ext in PICS.split():
by_category = {"vp": "/just/pics"}
elif ext in VIDS.split():
by_category = {"vp": "/just/vids"}
elif ext in MUSIC.split():
by_category = {"vp": "/just/tunes"}
else:
by_category = {} # no action
# now choose the default effect to apply; can be any of these:
# into_junk into_subfolder into_toplevel into_sibling by_category
effect = into_sibling
##
## but we can keep going, adding more speicifc rules
## which can take precedence, replacing the fallback
## effect we just specified:
##
fn = fn.lower() # lowercase filename to make this easier
if "screenshot" in fn:
effect = {"vp": "/ss"}
if "mpv_" in fn:
effect = {"vp": "/anishots"}
elif "debian" in fn or "biebian" in fn:
effect = {"vp": "/linux-ISOs"}
elif re.search(r"ep(isode |\.)?[0-9]", fn):
effect = {"vp": "/podcasts"}
# regex lets you grab a part of the matching
# text and use that in the upload path:
m = re.search(r"\b(op|ed)([^a-z]|$)", fn)
if m:
# the regex matched; use "anime-op" or "anime-ed"
effect = {"vp": "/anime-" + m[1]}
# aaand DO IT
print(json.dumps({"reloc": effect}))
if __name__ == "__main__":
main()

View file

@ -1,62 +0,0 @@
// see usb-eject.py for usage
function usbclick() {
var o = QS('#treeul a[dst="/usb/"]') || QS('#treepar a[dst="/usb/"]');
if (o)
o.click();
}
function eject_cb() {
var t = ('' + this.responseText).trim();
if (t.indexOf('can be safely unplugged') < 0 && t.indexOf('Device can be removed') < 0)
return toast.err(30, 'usb eject failed:\n\n' + t);
toast.ok(5, esc(t.replace(/ - /g, '\n\n')).trim());
usbclick(); setTimeout(usbclick, 10);
};
function add_eject_2(a) {
var aw = a.getAttribute('href').split(/\//g);
if (aw.length != 4 || aw[3])
return;
var v = aw[2],
k = 'umount_' + v;
for (var b = 0; b < 9; b++) {
var o = ebi(k);
if (!o)
break;
o.parentNode.removeChild(o);
}
a.appendChild(mknod('span', k, '⏏'), a);
o = ebi(k);
o.style.cssText = 'position:absolute; right:1em; margin-top:-.2em; font-size:1.3em';
o.onclick = function (e) {
ev(e);
var xhr = new XHR();
xhr.open('POST', get_evpath(), true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8');
xhr.send('msg=' + uricom_enc(':usb-eject:' + v + ':'));
xhr.onload = xhr.onerror = eject_cb;
toast.inf(10, "ejecting " + v + "...");
};
};
function add_eject() {
var o = QSA('#treeul a[href^="/usb/"]') || QSA('#treepar a[href^="/usb/"]');
for (var a = o.length - 1; a > 0; a--)
add_eject_2(o[a]);
};
(function() {
var f0 = treectl.rendertree;
treectl.rendertree = function (res, ts, top0, dst, rst) {
var ret = f0(res, ts, top0, dst, rst);
add_eject();
return ret;
};
})();
setTimeout(add_eject, 50);

View file

@ -1,62 +0,0 @@
#!/usr/bin/env python3
import os
import stat
import subprocess as sp
import sys
from urllib.parse import unquote_to_bytes as unquote
"""
if you've found yourself using copyparty to serve flashdrives on a LAN
and your only wish is that the web-UI had a button to unmount / safely
remove those flashdrives, then boy howdy are you in the right place :D
put usb-eject.js in the webroot (or somewhere else http-accessible)
then run copyparty with these args:
-v /run/media/egon:/usb:A:c,hist=/tmp/junk
--xm=c1,bin/hooks/usb-eject.py
--js-browser=/usb-eject.js
which does the following respectively,
* share all of /run/media/egon as /usb with admin for everyone
and put the histpath somewhere it won't cause trouble
* run the usb-eject hook with stdout redirect to the web-ui
* add the complementary usb-eject.js to the browser
"""
MOUNT_BASE = b"/run/media/egon/"
def main():
try:
label = sys.argv[1].split(":usb-eject:")[1].split(":")[0]
mp = MOUNT_BASE + unquote(label)
# print("ejecting [%s]... " % (mp,), end="")
mp = os.path.abspath(os.path.realpath(mp))
st = os.lstat(mp)
if not stat.S_ISDIR(st.st_mode) or not mp.startswith(MOUNT_BASE):
raise Exception("not a regular directory")
# if you're running copyparty as root (thx for the faith)
# you'll need something like this to make dbus talkative
cmd = b"sudo -u egon DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus gio mount -e"
# but if copyparty and the ui-session is running
# as the same user (good) then this is plenty
cmd = b"gio mount -e"
cmd = cmd.split(b" ") + [mp]
ret = sp.check_output(cmd).decode("utf-8", "replace")
print(ret.strip() or (label + " can be safely unplugged"))
except Exception as ex:
print("unmount failed: %r" % (ex,))
if __name__ == "__main__":
main()

View file

@ -9,38 +9,25 @@ import subprocess as sp
_ = r""" _ = r"""
use copyparty as a file downloader by POSTing URLs as use copyparty as a file downloader by POSTing URLs as
application/x-www-form-urlencoded (for example using the application/x-www-form-urlencoded (for example using the
📟 message-to-server-log in the web-ui) message/pager function on the website)
example usage as global config: example usage as global config:
--xm aw,f,j,t3600,bin/hooks/wget.py --xm f,j,t3600,bin/hooks/wget.py
parameters explained,
xm = execute on message-to-server-log
aw = only users with write-access can use this
f = fork; don't delay other hooks while this is running
j = provide message information as json (not just the text)
c3 = mute all output
t3600 = timeout and abort download after 1 hour
example usage as a volflag (per-volume config): example usage as a volflag (per-volume config):
-v srv/inc:inc:r:rw,ed:c,xm=aw,f,j,t3600,bin/hooks/wget.py -v srv/inc:inc:r:rw,ed:c,xm=f,j,t3600,bin/hooks/wget.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(share filesystem-path srv/inc as volume /inc, (share filesystem-path srv/inc as volume /inc,
readable by everyone, read-write for user 'ed', readable by everyone, read-write for user 'ed',
running this plugin on all messages with the params explained above) running this plugin on all messages with the params listed below)
example usage as a volflag in a copyparty config file: parameters explained,
[/inc] xm = execute on message-to-server-log
srv/inc f = fork so it doesn't block uploads
accs: j = provide message information as json; not just the text
r: * c3 = mute all output
rw: ed t3600 = timeout and kill download after 1 hour
flags:
xm: aw,f,j,t3600,bin/hooks/wget.py
the volflag examples only kicks in if you send the message
while you're in the /inc folder (or any folder below there)
""" """
@ -50,10 +37,6 @@ def main():
if "://" not in url: if "://" not in url:
url = "https://" + url url = "https://" + url
proto = url.split("://")[0].lower()
if proto not in ("http", "https", "ftp", "ftps"):
raise Exception("bad proto {}".format(proto))
os.chdir(inf["ap"]) os.chdir(inf["ap"])
name = url.split("?")[0].split("/")[-1] name = url.split("?")[0].split("/")[-1]
@ -66,7 +49,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()

View file

@ -3,7 +3,7 @@
import hashlib import hashlib
import json import json
import sys import sys
from datetime import datetime, timezone from datetime import datetime
_ = r""" _ = r"""
@ -43,11 +43,8 @@ except:
return p return p
UTC = timezone.utc
def humantime(ts): def humantime(ts):
return datetime.fromtimestamp(ts, UTC).strftime("%Y-%m-%d %H:%M:%S") return datetime.utcfromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
def find_files_root(inf): def find_files_root(inf):
@ -99,7 +96,7 @@ def main():
ret.append("# {} files, {} bytes total".format(len(inf), total_sz)) ret.append("# {} files, {} bytes total".format(len(inf), total_sz))
ret.append("") ret.append("")
ftime = datetime.now(UTC).strftime("%Y-%m%d-%H%M%S.%f") ftime = datetime.utcnow().strftime("%Y-%m%d-%H%M%S.%f")
fp = "{}xfer-{}.sha512".format(inf[0]["ap"][:di], ftime) fp = "{}xfer-{}.sha512".format(inf[0]["ap"][:di], ftime)
with open(fsenc(fp), "wb") as f: with open(fsenc(fp), "wb") as f:
f.write("\n".join(ret).encode("utf-8", "replace")) f.write("\n".join(ret).encode("utf-8", "replace"))

View file

@ -24,18 +24,6 @@ these do not have any problematic dependencies at all:
* also available as an [event hook](../hooks/wget.py) * also available as an [event hook](../hooks/wget.py)
## dangerous plugins
plugins in this section should only be used with appropriate precautions:
* [very-bad-idea.py](./very-bad-idea.py) combined with [meadup.js](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/meadup.js) converts copyparty into a janky yet extremely flexible chromecast clone
* also adds a virtual keyboard by @steinuil to the basic-upload tab for comfy couch crowd control
* anything uploaded through the [android app](https://github.com/9001/party-up) (files or links) are executed on the server, meaning anyone can infect your PC with malware... so protect this with a password and keep it on a LAN!
* [kamelåså](https://github.com/steinuil/kameloso) is a much better (and MUCH safer) alternative to this plugin
* powered by [chicken-curry-banana-pineapple-peanut pizza](https://a.ocv.me/pub/g/i/2025/01/298437ce-8351-4c8c-861c-fa131d217999.jpg?cache) so you know it's good
* and, unlike this plugin, kamelåså even has windows support (nice)
# dependencies # dependencies
run [`install-deps.sh`](install-deps.sh) to build/install most dependencies required by these programs (supports windows/linux/macos) run [`install-deps.sh`](install-deps.sh) to build/install most dependencies required by these programs (supports windows/linux/macos)

View file

@ -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:

View file

@ -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,

View file

@ -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",

View file

@ -7,7 +7,6 @@ set -e
# linux/alpine: requires gcc g++ make cmake patchelf {python3,ffmpeg,fftw,libsndfile}-dev py3-{wheel,pip} py3-numpy{,-dev} # linux/alpine: requires gcc g++ make cmake patchelf {python3,ffmpeg,fftw,libsndfile}-dev py3-{wheel,pip} py3-numpy{,-dev}
# linux/debian: requires libav{codec,device,filter,format,resample,util}-dev {libfftw3,python3,libsndfile1}-dev python3-{numpy,pip} vamp-{plugin-sdk,examples} patchelf cmake # linux/debian: requires libav{codec,device,filter,format,resample,util}-dev {libfftw3,python3,libsndfile1}-dev python3-{numpy,pip} vamp-{plugin-sdk,examples} patchelf cmake
# linux/fedora: requires gcc gcc-c++ make cmake patchelf {python3,ffmpeg,fftw,libsndfile}-devel python3-numpy vamp-plugin-sdk qm-vamp-plugins # linux/fedora: requires gcc gcc-c++ make cmake patchelf {python3,ffmpeg,fftw,libsndfile}-devel python3-numpy vamp-plugin-sdk qm-vamp-plugins
# linux/arch: requires gcc make cmake patchelf python3 ffmpeg fftw libsndfile python-{numpy,wheel,pip,setuptools}
# win64: requires msys2-mingw64 environment # win64: requires msys2-mingw64 environment
# macos: requires macports # macos: requires macports
# #
@ -22,8 +21,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=
@ -189,14 +186,11 @@ install_keyfinder() {
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
@ -228,31 +222,27 @@ install_vamp() {
# use msys2 in mingw-w64 mode # use msys2 in mingw-w64 mode
# pacman -S --needed mingw-w64-x86_64-{ffmpeg,python,python-pip,vamp-plugin-sdk} # pacman -S --needed mingw-w64-x86_64-{ffmpeg,python,python-pip,vamp-plugin-sdk}
$pybin -m pip install --user vamp || { $pybin -m pip install --user vamp
printf '\n\033[7malright, trying something else...\033[0m\n'
$pybin -m pip install --user --no-build-isolation vamp
}
cd "$td" cd "$td"
echo '#include <vamp-sdk/Plugin.h>' | g++ -x c++ -c -o /dev/null - || [ -e ~/pe/vamp-sdk ] || { echo '#include <vamp-sdk/Plugin.h>' | g++ -x c++ -c -o /dev/null - || [ -e ~/pe/vamp-sdk ] || {
printf '\033[33mcould not find the vamp-sdk, building from source\033[0m\n' printf '\033[33mcould not find the vamp-sdk, building from source\033[0m\n'
(dl_files yolo https://ocv.me/mirror/vamp-plugin-sdk-2.10.0.tar.gz) (dl_files yolo https://code.soundsoftware.ac.uk/attachments/download/2588/vamp-plugin-sdk-2.9.0.tar.gz)
sha512sum -c <( sha512sum -c <(
echo "153b7f2fa01b77c65ad393ca0689742d66421017fd5931d216caa0fcf6909355fff74706fabbc062a3a04588a619c9b515a1dae00f21a57afd97902a355c48ed -" echo "7ef7f837d19a08048b059e0da408373a7964ced452b290fae40b85d6d70ca9000bcfb3302cd0b4dc76cf2a848528456f78c1ce1ee0c402228d812bd347b6983b -"
) <vamp-plugin-sdk-2.10.0.tar.gz ) <vamp-plugin-sdk-2.9.0.tar.gz
tar -xf vamp-plugin-sdk-2.10.0.tar.gz tar -xf vamp-plugin-sdk-2.9.0.tar.gz
rm -- *.tar.gz rm -- *.tar.gz
ls -al ls -al
cd vamp-plugin-sdk-* cd vamp-plugin-sdk-*
printf '%s\n' "int main(int argc, char **argv) { return 0; }" > host/vamp-simple-host.cpp ./configure --prefix=$HOME/pe/vamp-sdk
./configure --disable-programs --prefix=$HOME/pe/vamp-sdk
make -j1 install make -j1 install
} }
cd "$td" cd "$td"
have_beatroot || { have_beatroot || {
printf '\033[33mcould not find the vamp beatroot plugin, building from source\033[0m\n' printf '\033[33mcould not find the vamp beatroot plugin, building from source\033[0m\n'
(dl_files yolo https://ocv.me/mirror/beatroot-vamp-v1.0.tar.gz) (dl_files yolo https://code.soundsoftware.ac.uk/attachments/download/885/beatroot-vamp-v1.0.tar.gz)
sha512sum -c <( sha512sum -c <(
echo "1f444d1d58ccf565c0adfe99f1a1aa62789e19f5071e46857e2adfbc9d453037bc1c4dcb039b02c16240e9b97f444aaff3afb625c86aa2470233e711f55b6874 -" echo "1f444d1d58ccf565c0adfe99f1a1aa62789e19f5071e46857e2adfbc9d453037bc1c4dcb039b02c16240e9b97f444aaff3afb625c86aa2470233e711f55b6874 -"
) <beatroot-vamp-v1.0.tar.gz ) <beatroot-vamp-v1.0.tar.gz
@ -260,9 +250,8 @@ install_vamp() {
rm -- *.tar.gz rm -- *.tar.gz
cd beatroot-vamp-v1.0 cd beatroot-vamp-v1.0
[ -e ~/pe/vamp-sdk ] && [ -e ~/pe/vamp-sdk ] &&
sed -ri 's`^(CFLAGS :=.*)`\1 -I'$HOME'/pe/vamp-sdk/include`' Makefile.linux || sed -ri 's`^(CFLAGS :=.*)`\1 -I'$HOME'/pe/vamp-sdk/include`' Makefile.linux
sed -ri 's`^(CFLAGS :=.*)`\1 -I/usr/include/vamp-sdk`' Makefile.linux make -f Makefile.linux -j4 LDFLAGS=-L$HOME/pe/vamp-sdk/lib
make -f Makefile.linux -j4 LDFLAGS="-L$HOME/pe/vamp-sdk/lib -L/usr/lib64"
# /home/ed/vamp /home/ed/.vamp /usr/local/lib/vamp # /home/ed/vamp /home/ed/.vamp /usr/local/lib/vamp
mkdir ~/vamp mkdir ~/vamp
cp -pv beatroot-vamp.* ~/vamp/ cp -pv beatroot-vamp.* ~/vamp/

View file

@ -1,16 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
WARNING -- DANGEROUS PLUGIN --
if someone is able to upload files to a copyparty which is
running this plugin, they can execute malware on your machine
so please keep this on a LAN and protect it with a password
here is a MUCH BETTER ALTERNATIVE (which also works on Windows):
https://github.com/steinuil/kameloso
----------------------------------------------------------------------
use copyparty as a chromecast replacement: use copyparty as a chromecast replacement:
* post a URL and it will open in the default browser * post a URL and it will open in the default browser
* upload a file and it will open in the default application * upload a file and it will open in the default application
@ -20,17 +10,16 @@ use copyparty as a chromecast replacement:
the android app makes it a breeze to post pics and links: the android app makes it a breeze to post pics and links:
https://github.com/9001/party-up/releases https://github.com/9001/party-up/releases
(iOS devices have to rely on the web-UI)
iOS devices can use the web-UI or the shortcut instead: goes without saying, but this is HELLA DANGEROUS,
https://github.com/9001/copyparty#ios-shortcuts GIVES RCE TO ANYONE WHO HAVE UPLOAD PERMISSIONS
example copyparty config to use this; example copyparty config to use this:
lets the user "kevin" with password "hunter2" use this plugin: --urlform save,get -v.::w:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,c0,bin/mtag/very-bad-idea.py
-a kevin:hunter2 --urlform save,get -v.::w,kevin:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,c0,bin/mtag/very-bad-idea.py
recommended deps: recommended deps:
apt install xdotool libnotify-bin mpv apt install xdotool libnotify-bin
python3 -m pip install --user -U streamlink yt-dlp
https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/meadup.js https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/meadup.js
and you probably want `twitter-unmute.user.js` from the res folder and you probably want `twitter-unmute.user.js` from the res folder
@ -74,10 +63,8 @@ set -e
EOF EOF
chmod 755 /usr/local/bin/chromium-browser chmod 755 /usr/local/bin/chromium-browser
# start the server # start the server (note: replace `-v.::rw:` with `-v.::w:` to disallow retrieving uploaded stuff)
# note 1: replace hunter2 with a better password to access the server cd ~/Downloads; python3 copyparty-sfx.py --urlform save,get -v.::rw:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,very-bad-idea.py
# note 2: replace `-v.::rw` with `-v.::w` to disallow retrieving uploaded stuff
cd ~/Downloads; python3 copyparty-sfx.py -a kevin:hunter2 --urlform save,get -v.::rw,kevin:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,very-bad-idea.py
""" """
@ -85,23 +72,11 @@ cd ~/Downloads; python3 copyparty-sfx.py -a kevin:hunter2 --urlform save,get -v.
import os import os
import sys import sys
import time import time
import shutil
import subprocess as sp import subprocess as sp
from urllib.parse import unquote_to_bytes as unquote from urllib.parse import unquote_to_bytes as unquote
from urllib.parse import quote
have_mpv = shutil.which("mpv")
have_vlc = shutil.which("vlc")
def main(): def main():
if len(sys.argv) > 2 and sys.argv[1] == "x":
# invoked on commandline for testing;
# python3 very-bad-idea.py x msg=https://youtu.be/dQw4w9WgXcQ
txt = " ".join(sys.argv[2:])
txt = quote(txt.replace(" ", "+"))
return open_post(txt.encode("utf-8"))
fp = os.path.abspath(sys.argv[1]) fp = os.path.abspath(sys.argv[1])
with open(fp, "rb") as f: with open(fp, "rb") as f:
txt = f.read(4096) txt = f.read(4096)
@ -117,7 +92,7 @@ def open_post(txt):
try: try:
k, v = txt.split(" ", 1) k, v = txt.split(" ", 1)
except: except:
return open_url(txt) open_url(txt)
if k == "key": if k == "key":
sp.call(["xdotool", "key"] + v.split(" ")) sp.call(["xdotool", "key"] + v.split(" "))
@ -153,17 +128,6 @@ def open_url(txt):
# else: # else:
# sp.call(["xdotool", "getactivewindow", "windowminimize"]) # minimizes the focused windo # sp.call(["xdotool", "getactivewindow", "windowminimize"]) # minimizes the focused windo
# mpv is probably smart enough to use streamlink automatically
if try_mpv(txt):
print("mpv got it")
return
# or maybe streamlink would be a good choice to open this
if try_streamlink(txt):
print("streamlink got it")
return
# nope,
# close any error messages: # close any error messages:
sp.call(["xdotool", "search", "--name", "Error", "windowclose"]) sp.call(["xdotool", "search", "--name", "Error", "windowclose"])
# sp.call(["xdotool", "key", "ctrl+alt+d"]) # doesnt work at all # sp.call(["xdotool", "key", "ctrl+alt+d"]) # doesnt work at all
@ -172,39 +136,4 @@ def open_url(txt):
sp.call(["xdg-open", txt]) sp.call(["xdg-open", txt])
def try_mpv(url):
t0 = time.time()
try:
print("trying mpv...")
sp.check_call(["mpv", "--fs", url])
return True
except:
# if it ran for 15 sec it probably succeeded and terminated
t = time.time()
return t - t0 > 15
def try_streamlink(url):
t0 = time.time()
try:
import streamlink
print("trying streamlink...")
streamlink.Streamlink().resolve_url(url)
if have_mpv:
args = "-m streamlink -p mpv -a --fs"
else:
args = "-m streamlink"
cmd = [sys.executable] + args.split() + [url, "best"]
t0 = time.time()
sp.check_call(cmd)
return True
except:
# if it ran for 10 sec it probably succeeded and terminated
t = time.time()
return t - t0 > 10
main() main()

View file

@ -65,10 +65,6 @@ def main():
if "://" not in url: if "://" not in url:
url = "https://" + url url = "https://" + url
proto = url.split("://")[0].lower()
if proto not in ("http", "https", "ftp", "ftps"):
raise Exception("bad proto {}".format(proto))
os.chdir(fdir) os.chdir(fdir)
name = url.split("?")[0].split("/")[-1] name = url.split("?")[0].split("/")[-1]
@ -84,7 +80,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)

File diff suppressed because it is too large Load diff

View file

@ -20,13 +20,12 @@ import sys
import base64 import base64
import sqlite3 import sqlite3
import argparse import argparse
from datetime import datetime, timezone from datetime import datetime
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
FS_ENCODING = sys.getfilesystemencoding() FS_ENCODING = sys.getfilesystemencoding()
UTC = timezone.utc
class APF(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): class APF(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter):
@ -156,10 +155,11 @@ th {
link = txt.decode("utf-8")[4:] link = txt.decode("utf-8")[4:]
sz = "{:,}".format(sz) sz = "{:,}".format(sz)
dt = datetime.fromtimestamp(at if at > 0 else mt, UTC)
v = [ v = [
w[:16], w[:16],
dt.strftime("%Y-%m-%d %H:%M:%S"), datetime.utcfromtimestamp(at if at > 0 else mt).strftime(
"%Y-%m-%d %H:%M:%S"
),
sz, sz,
imap.get(ip, ip), imap.get(ip, ip),
] ]

View file

@ -12,13 +12,13 @@ done
help() { cat <<'EOF' help() { cat <<'EOF'
usage: usage:
./prisonparty.sh <ROOTDIR> <USER|UID> <GROUP|GID> [VOLDIR [VOLDIR...]] -- python3 copyparty-sfx.py [...] ./prisonparty.sh <ROOTDIR> <UID> <GID> [VOLDIR [VOLDIR...]] -- python3 copyparty-sfx.py [...]
example: example:
./prisonparty.sh /var/lib/copyparty-jail cpp cpp /mnt/nas/music -- python3 copyparty-sfx.py -v /mnt/nas/music::rwmd ./prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt/nas/music -- python3 copyparty-sfx.py -v /mnt/nas/music::rwmd
example for running straight from source (instead of using an sfx): example for running straight from source (instead of using an sfx):
PYTHONPATH=$PWD ./prisonparty.sh /var/lib/copyparty-jail cpp cpp /mnt/nas/music -- python3 -um copyparty -v /mnt/nas/music::rwmd PYTHONPATH=$PWD ./prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt/nas/music -- python3 -um copyparty -v /mnt/nas/music::rwmd
note that if you have python modules installed as --user (such as bpm/key detectors), note that if you have python modules installed as --user (such as bpm/key detectors),
you should add /home/foo/.local as a VOLDIR you should add /home/foo/.local as a VOLDIR
@ -28,16 +28,6 @@ exit 1
} }
errs=
for c in awk chroot dirname getent lsof mknod mount realpath sed sort stat uniq; do
command -v $c >/dev/null || {
echo ERROR: command not found: $c
errs=1
}
done
[ $errs ] && exit 1
# read arguments # read arguments
trap help EXIT trap help EXIT
jail="$(realpath "$1")"; shift jail="$(realpath "$1")"; shift
@ -68,18 +58,11 @@ cpp="$1"; shift
} }
trap - EXIT trap - EXIT
usr="$(getent passwd $uid | cut -d: -f1)"
[ "$usr" ] || { echo "ERROR invalid username/uid $uid"; exit 1; }
uid="$(getent passwd $uid | cut -d: -f3)"
grp="$(getent group $gid | cut -d: -f1)"
[ "$grp" ] || { echo "ERROR invalid groupname/gid $gid"; exit 1; }
gid="$(getent group $gid | cut -d: -f3)"
# debug/vis # debug/vis
echo echo
echo "chroot-dir = $jail" echo "chroot-dir = $jail"
echo "user:group = $uid:$gid ($usr:$grp)" echo "user:group = $uid:$gid"
echo " copyparty = $cpp" echo " copyparty = $cpp"
echo echo
printf '\033[33m%s\033[0m\n' "copyparty can access these folders and all their subdirectories:" printf '\033[33m%s\033[0m\n' "copyparty can access these folders and all their subdirectories:"
@ -97,39 +80,34 @@ jail="${jail%/}"
# bind-mount system directories and volumes # bind-mount system directories and volumes
for a in {1..30}; do mkdir "$jail/.prisonlock" && break; sleep 0.1; done
printf '%s\n' "${sysdirs[@]}" "${vols[@]}" | sed -r 's`/$``' | LC_ALL=C sort | uniq | printf '%s\n' "${sysdirs[@]}" "${vols[@]}" | sed -r 's`/$``' | LC_ALL=C sort | uniq |
while IFS= read -r v; do while IFS= read -r v; do
[ -e "$v" ] || { [ -e "$v" ] || {
printf '\033[1;31mfolder does not exist:\033[0m %s\n' "$v" printf '\033[1;31mfolder does not exist:\033[0m %s\n' "$v"
continue continue
} }
i1=$(stat -c%D.%i "$v/" 2>/dev/null || echo a) i1=$(stat -c%D.%i "$v" 2>/dev/null || echo a)
i2=$(stat -c%D.%i "$jail$v/" 2>/dev/null || echo b) i2=$(stat -c%D.%i "$jail$v" 2>/dev/null || echo b)
# echo "v [$v] i1 [$i1] i2 [$i2]"
[ $i1 = $i2 ] && continue [ $i1 = $i2 ] && continue
mount | grep -qF " $jail$v " && echo wtf $i1 $i2 $v && continue
mkdir -p "$jail$v" mkdir -p "$jail$v"
mount --bind "$v" "$jail$v" mount --bind "$v" "$jail$v"
done done
rmdir "$jail/.prisonlock" || true
cln() { cln() {
trap - EXIT rv=$?
wait -f -n $p && rv=0 || rv=$? wait -f -p rv $p || true
cd / cd /
echo "stopping chroot..." echo "stopping chroot..."
for a in {1..30}; do mkdir "$jail/.prisonlock" && break; sleep 0.1; done lsof "$jail" | grep -F "$jail" &&
lsof "$jail" 2>/dev/null | grep -F "$jail" &&
echo "chroot is in use; will not unmount" || echo "chroot is in use; will not unmount" ||
{ {
mount | grep -F " on $jail" | mount | grep -F " on $jail" |
awk '{sub(/ type .*/,"");sub(/.* on /,"");print}' | awk '{sub(/ type .*/,"");sub(/.* on /,"");print}' |
LC_ALL=C sort -r | while IFS= read -r v; do LC_ALL=C sort -r | tee /dev/stderr | tr '\n' '\0' | xargs -r0 umount
umount "$v" && echo "umount OK: $v"
done
} }
rmdir "$jail/.prisonlock" || true
exit $rv exit $rv
} }
trap cln EXIT trap cln EXIT
@ -150,8 +128,8 @@ chmod 777 "$jail/tmp"
# run copyparty # run copyparty
export HOME="$(getent passwd $uid | cut -d: -f6)" export HOME=$(getent passwd $uid | cut -d: -f6)
export USER="$usr" export USER=$(getent passwd $uid | cut -d: -f1)
export LOGNAME="$USER" export LOGNAME="$USER"
#echo "pybin [$pybin]" #echo "pybin [$pybin]"
#echo "pyarg [$pyarg]" #echo "pyarg [$pyarg]"
@ -159,5 +137,5 @@ export LOGNAME="$USER"
chroot --userspec=$uid:$gid "$jail" "$pybin" $pyarg "$cpp" "$@" & chroot --userspec=$uid:$gid "$jail" "$pybin" $pyarg "$cpp" "$@" &
p=$! p=$!
trap 'kill -USR1 $p' USR1 trap 'kill -USR1 $p' USR1
trap 'trap - INT TERM; kill $p' INT TERM trap 'kill $p' INT TERM
wait wait

1015
bin/u2c.py

File diff suppressed because it is too large Load diff

View file

@ -66,7 +66,7 @@ def main():
ofs = ln.find("{") ofs = ln.find("{")
j = json.loads(ln[ofs:]) j = json.loads(ln[ofs:])
except: except:
continue pass
w = j["wark"] w = j["wark"]
if db.execute("select w from up where w = ?", (w,)).fetchone(): if db.execute("select w from up where w = ?", (w,)).fetchone():

View file

@ -1,76 +0,0 @@
#!/usr/bin/env python3
import sys
import zmq
"""
zmq-recv.py: demo zmq receiver
2025-01-22, v1.0, ed <irc.rizon.net>, MIT-Licensed
https://github.com/9001/copyparty/blob/hovudstraum/bin/zmq-recv.py
basic zmq-server to receive events from copyparty; try one of
the below and then "send a message to serverlog" in the web-ui:
1) dumb fire-and-forget to any and all listeners;
run this script with "sub" and run copyparty with this:
--xm zmq:pub:tcp://*:5556
2) one lucky listener gets the message, blocks if no listeners:
run this script with "pull" and run copyparty with this:
--xm t3,zmq:push:tcp://*:5557
3) blocking syn/ack mode, client must ack each message;
run this script with "rep" and run copyparty with this:
--xm t3,zmq:req:tcp://localhost:5555
note: to conditionally block uploads based on message contents,
use rep_server to answer with "return 1" and run copyparty with
--xau t3,c,zmq:req:tcp://localhost:5555
"""
ctx = zmq.Context()
def sub_server():
# PUB/SUB allows any number of servers/clients, and
# messages are fire-and-forget
sck = ctx.socket(zmq.SUB)
sck.connect("tcp://localhost:5556")
sck.setsockopt_string(zmq.SUBSCRIBE, "")
while True:
print("copyparty says %r" % (sck.recv_string(),))
def pull_server():
# PUSH/PULL allows any number of servers/clients, and
# each message is sent to a exactly one PULL client
sck = ctx.socket(zmq.PULL)
sck.connect("tcp://localhost:5557")
while True:
print("copyparty says %r" % (sck.recv_string(),))
def rep_server():
# REP/REQ is a server/client pair where each message must be
# acked by the other before another message can be sent, so
# copyparty will do a blocking-wait for the ack
sck = ctx.socket(zmq.REP)
sck.bind("tcp://*:5555")
while True:
print("copyparty says %r" % (sck.recv_string(),))
reply = b"thx"
# reply = b"return 1" # non-zero to block an upload
sck.send(reply)
mode = sys.argv[1].lower() if len(sys.argv) > 1 else ""
if mode == "sub":
sub_server()
elif mode == "pull":
pull_server()
elif mode == "rep":
rep_server()
else:
print("specify mode as first argument: SUB | PULL | REP")

View file

@ -12,25 +12,15 @@
* assumes the webserver and copyparty is running on the same server/IP * assumes the webserver and copyparty is running on the same server/IP
* modify `10.13.1.1` as necessary if you wish to support browsers without javascript * modify `10.13.1.1` as necessary if you wish to support browsers without javascript
### [`sharex.sxcu`](sharex.sxcu) - Windows screenshot uploader ### [`sharex.sxcu`](sharex.sxcu)
* [sharex](https://getsharex.com/) config file to upload screenshots and grab the URL * sharex config file to upload screenshots and grab the URL
* `RequestURL`: full URL to the target folder
* `pw`: password (remove the `pw` line if anon-write)
* the `act:bput` thing is optional since copyparty v1.9.29
* using an older sharex version, maybe sharex v12.1.1 for example? dw fam i got your back 👉😎👉 [`sharex12.sxcu`](sharex12.sxcu)
### [`ishare.iscu`](ishare.iscu) - MacOS screenshot uploader
* [ishare](https://isharemac.app/) config file to upload screenshots and grab the URL
* `RequestURL`: full URL to the target folder * `RequestURL`: full URL to the target folder
* `pw`: password (remove the `pw` line if anon-write) * `pw`: password (remove the `pw` line if anon-write)
### [`flameshot.sh`](flameshot.sh) - Linux screenshot uploader however if your copyparty is behind a reverse-proxy, you may want to use [`sharex-html.sxcu`](sharex-html.sxcu) instead:
* takes a screenshot with [flameshot](https://flameshot.org/) on Linux, uploads it, and writes the URL to clipboard * `RequestURL`: full URL to the target folder
* `URL`: full URL to the root folder (with trailing slash) followed by `$regex:1|1$`
### [`send-to-cpp.contextlet.json`](send-to-cpp.contextlet.json) * `pw`: password (remove `Parameters` if anon-write)
* browser integration, kind of? custom rightclick actions and stuff
* rightclick a pic and send it to copyparty straight from your browser
* for the [contextlet](https://addons.mozilla.org/en-US/firefox/addon/contextlets/) firefox extension
### [`media-osd-bgone.ps1`](media-osd-bgone.ps1) ### [`media-osd-bgone.ps1`](media-osd-bgone.ps1)
* disables the [windows OSD popup](https://user-images.githubusercontent.com/241032/122821375-0e08df80-d2dd-11eb-9fd9-184e8aacf1d0.png) (the thing on the left) which appears every time you hit media hotkeys to adjust volume or change song while playing music with the copyparty web-ui, or most other audio players really * disables the [windows OSD popup](https://user-images.githubusercontent.com/241032/122821375-0e08df80-d2dd-11eb-9fd9-184e8aacf1d0.png) (the thing on the left) which appears every time you hit media hotkeys to adjust volume or change song while playing music with the copyparty web-ui, or most other audio players really
@ -50,9 +40,6 @@
* give a 3rd argument to install it to your copyparty config * give a 3rd argument to install it to your copyparty config
* systemd service at [`systemd/cfssl.service`](systemd/cfssl.service) * systemd service at [`systemd/cfssl.service`](systemd/cfssl.service)
### [`zfs-tune.py`](zfs-tune.py)
* optimizes databases for optimal performance when stored on a zfs filesystem; also see [openzfs docs](https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Workload%20Tuning.html#database-workloads) and specifically the SQLite subsection
# OS integration # OS integration
init-scripts to start copyparty as a service init-scripts to start copyparty as a service
* [`systemd/copyparty.service`](systemd/copyparty.service) runs the sfx normally * [`systemd/copyparty.service`](systemd/copyparty.service) runs the sfx normally
@ -61,10 +48,5 @@ init-scripts to start copyparty as a service
* [`openrc/copyparty`](openrc/copyparty) * [`openrc/copyparty`](openrc/copyparty)
# Reverse-proxy # Reverse-proxy
copyparty supports running behind another webserver copyparty has basic support for running behind another webserver
* [`apache/copyparty.conf`](apache/copyparty.conf) * [`nginx/copyparty.conf`](nginx/copyparty.conf)
* [`haproxy/copyparty.conf`](haproxy/copyparty.conf)
* [`lighttpd/subdomain.conf`](lighttpd/subdomain.conf)
* [`lighttpd/subpath.conf`](lighttpd/subpath.conf)
* [`nginx/copyparty.conf`](nginx/copyparty.conf) -- recommended
* [`traefik/copyparty.yaml`](traefik/copyparty.yaml)

View file

@ -1,29 +1,14 @@
# if you would like to use unix-sockets (recommended), # when running copyparty behind a reverse proxy,
# you must run copyparty with one of the following: # the following arguments are recommended:
# #
# -i unix:777:/dev/shm/party.sock # -i 127.0.0.1 only accept connections from nginx
# -i unix:777:/dev/shm/party.sock,127.0.0.1
# #
# if you are doing location-based proxying (such as `/stuff` below) # if you are doing location-based proxying (such as `/stuff` below)
# you must run copyparty with --rp-loc=stuff # you must run copyparty with --rp-loc=stuff
# #
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1 # on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
LoadModule proxy_module modules/mod_proxy.so LoadModule proxy_module modules/mod_proxy.so
ProxyPass "/stuff" "http://127.0.0.1:3923/stuff"
# do not specify ProxyPassReverse
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME} RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
# NOTE: do not specify ProxyPassReverse
##
## then, enable one of the below:
# use subdomain proxying to unix-socket (best)
ProxyPass "/" "unix:///dev/shm/party.sock|http://whatever/"
# use subdomain proxying to 127.0.0.1 (slower)
#ProxyPass "/" "http://127.0.0.1:3923/"
# use subpath proxying to 127.0.0.1 (slow and maybe buggy)
#ProxyPass "/stuff" "http://127.0.0.1:3923/stuff"

View file

@ -1,14 +0,0 @@
#!/bin/bash
set -e
# take a screenshot with flameshot and send it to copyparty;
# the image url will be placed on your clipboard
password=wark
url=https://a.ocv.me/up/
filename=$(date +%Y-%m%d-%H%M%S).png
flameshot gui -s -r |
curl -T- $url$filename?pw=$password |
tail -n 1 |
xsel -ib

View file

@ -1,24 +0,0 @@
# this config is essentially two separate examples;
#
# foo1 connects to copyparty using tcp, and
# foo2 uses unix-sockets for 27% higher performance
#
# to use foo2 you must run copyparty with one of the following:
#
# -i unix:777:/dev/shm/party.sock
# -i unix:777:/dev/shm/party.sock,127.0.0.1
defaults
mode http
option forwardfor
timeout connect 1s
timeout client 610s
timeout server 610s
listen foo1
bind *:8081
server srv1 127.0.0.1:3923 maxconn 512
listen foo2
bind *:8082
server srv1 /dev/shm/party.sock maxconn 512

View file

@ -26,8 +26,8 @@ a {
<script> <script>
var a = document.getElementById('redir'), var a = document.getElementById('redir'),
proto = location.protocol.indexOf('https') === 0 ? 'https' : 'http', proto = window.location.protocol.indexOf('https') === 0 ? 'https' : 'http',
loc = location.hostname || '127.0.0.1', loc = window.location.hostname || '127.0.0.1',
port = a.getAttribute('href').split(':').pop().split('/')[0], port = a.getAttribute('href').split(':').pop().split('/')[0],
url = proto + '://' + loc + ':' + port + '/'; url = proto + '://' + loc + ':' + port + '/';
@ -35,7 +35,7 @@ a.setAttribute('href', url);
document.getElementById('desc').innerHTML = 'redirecting to'; document.getElementById('desc').innerHTML = 'redirecting to';
setTimeout(function() { setTimeout(function() {
location.href = url; window.location.href = url;
}, 500); }, 500);
</script> </script>

View file

@ -1,10 +0,0 @@
{
"Name": "copyparty",
"RequestURL": "http://127.0.0.1:3923/screenshots/",
"Headers": {
"pw": "PUT_YOUR_PASSWORD_HERE_MY_DUDE",
"accept": "json"
},
"FileFormName": "f",
"ResponseURL": "{{fileurl}}"
}

View file

@ -1,24 +0,0 @@
# example usage for benchmarking:
#
# taskset -c 1 lighttpd -Df ~/dev/copyparty/contrib/lighttpd/subdomain.conf
#
# lighttpd can connect to copyparty using either tcp (127.0.0.1)
# or a unix-socket, but unix-sockets are 37% faster because
# lighttpd doesn't reuse tcp connections, so we're doing unix-sockets
#
# this means we must run copyparty with one of the following:
#
# -i unix:777:/dev/shm/party.sock
# -i unix:777:/dev/shm/party.sock,127.0.0.1
#
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
server.port = 80
server.document-root = "/var/empty"
server.upload-dirs = ( "/dev/shm", "/tmp" )
server.modules = ( "mod_proxy" )
proxy.forwarded = ( "for" => 1, "proto" => 1 )
proxy.server = ( "" => ( ( "host" => "/dev/shm/party.sock" ) ) )
# if you really need to use tcp instead of unix-sockets, do this instead:
#proxy.server = ( "" => ( ( "host" => "127.0.0.1", "port" => "3923" ) ) )

View file

@ -1,31 +0,0 @@
# example usage for benchmarking:
#
# taskset -c 1 lighttpd -Df ~/dev/copyparty/contrib/lighttpd/subpath.conf
#
# lighttpd can connect to copyparty using either tcp (127.0.0.1)
# or a unix-socket, but unix-sockets are 37% faster because
# lighttpd doesn't reuse tcp connections, so we're doing unix-sockets
#
# this means we must run copyparty with one of the following:
#
# -i unix:777:/dev/shm/party.sock
# -i unix:777:/dev/shm/party.sock,127.0.0.1
#
# also since this example proxies a subpath instead of the
# recommended subdomain-proxying, we must also specify this:
#
# --rp-loc files
#
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
server.port = 80
server.document-root = "/var/empty"
server.upload-dirs = ( "/dev/shm", "/tmp" )
server.modules = ( "mod_proxy" )
$HTTP["url"] =~ "^/files" {
proxy.forwarded = ( "for" => 1, "proto" => 1 )
proxy.server = ( "" => ( ( "host" => "/dev/shm/party.sock" ) ) )
# if you really need to use tcp instead of unix-sockets, do this instead:
#proxy.server = ( "" => ( ( "host" => "127.0.0.1", "port" => "3923" ) ) )
}

View file

@ -1,101 +1,44 @@
# look for "max clients:" when starting copyparty, as nginx should # when running copyparty behind a reverse proxy,
# not accept more consecutive clients than what copyparty is able to; # the following arguments are recommended:
#
# -i 127.0.0.1 only accept connections from nginx
#
# -nc must match or exceed the webserver's max number of concurrent clients;
# copyparty default is 1024 if OS permits it (see "max clients:" on startup),
# nginx default is 512 (worker_processes 1, worker_connections 512) # nginx default is 512 (worker_processes 1, worker_connections 512)
# #
# ====================================================================== # you may also consider adding -j0 for CPU-intensive configurations
# # (5'000 requests per second, or 20gbps upload/download in parallel)
# to reverse-proxy a specific path/subpath/location below a domain
# (rather than a complete subdomain), for example "/qw/er", you must
# run copyparty with --rp-loc /qw/as and also change the following:
# location / {
# proxy_pass http://cpp_tcp;
# to this:
# location /qw/er/ {
# proxy_pass http://cpp_tcp/qw/er/;
#
# ======================================================================
#
# rarely, in some extreme usecases, it can be good to add -j0
# (40'000 requests per second, or 20gbps upload/download in parallel)
# but this is usually counterproductive and slightly buggy
#
# ======================================================================
# #
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1 # on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
#
# ======================================================================
#
# if you are behind cloudflare (or another CDN/WAF/protection service),
# remember to reject all connections which are not coming from your
# protection service -- for cloudflare in particular, you can
# 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
#
# and then enable it below by uncommenting the cloudflare-only.conf line
#
# ======================================================================
upstream cpp {
upstream cpp_tcp { server 127.0.0.1:3923;
# alternative 1: connect to copyparty using tcp;
# cpp_uds is slightly faster and more secure, but
# cpp_tcp is easier to setup and "just works"
# ...you should however restrict copyparty to only
# accept connections from nginx by adding these args:
# -i 127.0.0.1
server 127.0.0.1:3923 fail_timeout=1s;
keepalive 1; keepalive 1;
} }
upstream cpp_uds {
# alternative 2: unix-socket, aka. "unix domain socket";
# 5-10% faster, and better isolation from other software,
# but there must be at least one unix-group which both
# nginx and copyparty is a member of; if that group is
# "www" then run copyparty with the following args:
# -i unix:770:www:/dev/shm/party.sock
server unix:/dev/shm/party.sock fail_timeout=1s;
keepalive 1;
}
server { server {
listen 443 ssl; listen 443 ssl;
listen [::]:443 ssl; listen [::]:443 ssl;
server_name fs.example.com; server_name fs.example.com;
# uncomment the following line to reject non-cloudflare connections, ensuring client IPs cannot be spoofed:
#include /etc/nginx/cloudflare-only.conf;
location / { location / {
# recommendation: replace cpp_tcp with cpp_uds below proxy_pass http://cpp;
proxy_pass http://cpp_tcp;
proxy_redirect off; proxy_redirect off;
# disable buffering (next 4 lines) # disable buffering (next 4 lines)
proxy_http_version 1.1; proxy_http_version 1.1;
client_max_body_size 0; client_max_body_size 0;
proxy_buffering off; proxy_buffering off;
proxy_request_buffering off; proxy_request_buffering off;
# improve download speed from 600 to 1500 MiB/s
proxy_buffers 32 8k;
proxy_buffer_size 16k;
proxy_busy_buffers_size 24k;
proxy_set_header Connection "Keep-Alive";
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# NOTE: with cloudflare you want this X-Forwarded-For instead: proxy_set_header X-Forwarded-Proto $scheme;
#proxy_set_header X-Forwarded-For $http_cf_connecting_ip; proxy_set_header Connection "Keep-Alive";
} }
} }
# default client_max_body_size (1M) blocks uploads larger than 256 MiB # default client_max_body_size (1M) blocks uploads larger than 256 MiB
client_max_body_size 1024M; client_max_body_size 1024M;
client_header_timeout 610m; client_header_timeout 610m;

View file

@ -1,28 +1,23 @@
{ { config, pkgs, lib, ... }:
config,
pkgs,
lib,
...
}:
with lib; with lib;
let let
mkKeyValue = mkKeyValue = key: value:
key: value:
if value == true then if value == true then
# sets with a true boolean value are coerced to just the key name # sets with a true boolean value are coerced to just the key name
key key
else if value == false then else if value == false then
# or omitted completely when false # or omitted completely when false
"" ""
else else
(generators.mkKeyValueDefault { inherit mkValueString; } ": " key value); (generators.mkKeyValueDefault { inherit mkValueString; } ": " key value);
mkAttrsString = value: (generators.toKeyValue { inherit mkKeyValue; } value); mkAttrsString = value: (generators.toKeyValue { inherit mkKeyValue; } value);
mkValueString = mkValueString = value:
value:
if isList value then if isList value then
(concatStringsSep "," (map mkValueString value)) (concatStringsSep ", " (map mkValueString value))
else if isAttrs value then else if isAttrs value then
"\n" + (mkAttrsString value) "\n" + (mkAttrsString value)
else else
@ -54,14 +49,13 @@ let
${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)} ${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)}
''; '';
name = "copyparty";
cfg = config.services.copyparty; cfg = config.services.copyparty;
configFile = pkgs.writeText "copyparty.conf" configStr; configFile = pkgs.writeText "${name}.conf" configStr;
runtimeConfigPath = "/run/copyparty/copyparty.conf"; runtimeConfigPath = "/run/${name}/${name}.conf";
externalCacheDir = "/var/cache/copyparty"; home = "/var/lib/${name}";
externalStateDir = "/var/lib/copyparty"; defaultShareDir = "${home}/data";
defaultShareDir = "${externalStateDir}/data"; in {
in
{
options.services.copyparty = { options.services.copyparty = {
enable = mkEnableOption "web-based file manager"; enable = mkEnableOption "web-based file manager";
@ -74,35 +68,6 @@ in
''; '';
}; };
mkHashWrapper = mkOption {
type = types.bool;
default = true;
description = ''
Make a shell script wrapper called 'copyparty-hash' with all options set here,
that launches the hashing cli.
'';
};
user = mkOption {
type = types.str;
default = "copyparty";
description = ''
The user that copyparty will run under.
If changed from default, you are responsible for making sure the user exists.
'';
};
group = mkOption {
type = types.str;
default = "copyparty";
description = ''
The group that copyparty will run under.
If changed from default, you are responsible for making sure the user exists.
'';
};
openFilesLimit = mkOption { openFilesLimit = mkOption {
default = 4096; default = 4096;
type = types.either types.int types.str; type = types.either types.int types.str;
@ -114,41 +79,33 @@ in
description = '' description = ''
Global settings to apply. Global settings to apply.
Directly maps to values in the [global] section of the copyparty config. Directly maps to values in the [global] section of the copyparty config.
Cannot set "c" or "hist", those are set by this module.
See `${getExe cfg.package} --help` for more details. See `${getExe cfg.package} --help` for more details.
''; '';
default = { default = {
i = "127.0.0.1"; i = "127.0.0.1";
no-reload = true; no-reload = true;
hist = externalCacheDir;
}; };
example = literalExpression '' example = literalExpression ''
{ {
i = "0.0.0.0"; i = "0.0.0.0";
no-reload = true; no-reload = true;
hist = ${externalCacheDir};
} }
''; '';
}; };
accounts = mkOption { accounts = mkOption {
type = types.attrsOf ( type = types.attrsOf (types.submodule ({ ... }: {
types.submodule ( options = {
{ ... }: passwordFile = mkOption {
{ type = types.str;
options = { description = ''
passwordFile = mkOption { Runtime file path to a file containing the user password.
type = types.str; Must be readable by the copyparty user.
description = '' '';
Runtime file path to a file containing the user password. example = "/run/keys/copyparty/ed";
Must be readable by the copyparty user. };
''; };
example = "/run/keys/copyparty/ed"; }));
};
};
}
)
);
description = '' description = ''
A set of copyparty accounts to create. A set of copyparty accounts to create.
''; '';
@ -161,81 +118,72 @@ in
}; };
volumes = mkOption { volumes = mkOption {
type = types.attrsOf ( type = types.attrsOf (types.submodule ({ ... }: {
types.submodule ( options = {
{ ... }: path = mkOption {
{ type = types.str;
options = { description = ''
path = mkOption { Path of a directory to share.
type = types.path; '';
description = '' };
Path of a directory to share. access = mkOption {
''; type = types.attrs;
}; description = ''
access = mkOption { Attribute list of permissions and the users to apply them to.
type = types.attrs;
description = ''
Attribute list of permissions and the users to apply them to.
The key must be a string containing any combination of allowed permission: The key must be a string containing any combination of allowed permission:
"r" (read): list folder contents, download files "r" (read): list folder contents, download files
"w" (write): upload files; need "r" to see the uploads "w" (write): upload files; need "r" to see the uploads
"m" (move): move files and folders; need "w" at destination "m" (move): move files and folders; need "w" at destination
"d" (delete): permanently delete files and folders "d" (delete): permanently delete files and folders
"g" (get): download files, but cannot see folder contents "g" (get): download files, but cannot see folder contents
"G" (upget): "get", but can see filekeys of their own uploads "G" (upget): "get", but can see filekeys of their own uploads
"h" (html): "get", but folders return their index.html
"a" (admin): can see uploader IPs, config-reload
For example: "rwmd" For example: "rwmd"
The value must be one of: The value must be one of:
an account name, defined in `accounts` an account name, defined in `accounts`
a list of account names a list of account names
"*", which means "any account" "*", which means "any account"
''; '';
example = literalExpression '' example = literalExpression ''
{ {
# wG = write-upget = see your own uploads only # wG = write-upget = see your own uploads only
wG = "*"; wG = "*";
# read-write-modify-delete for users "ed" and "k" # read-write-modify-delete for users "ed" and "k"
rwmd = ["ed" "k"]; rwmd = ["ed" "k"];
};
'';
}; };
flags = mkOption { '';
type = types.attrs; };
description = '' flags = mkOption {
Attribute list of volume flags to apply. type = types.attrs;
See `${getExe cfg.package} --help-flags` for more details. description = ''
''; Attribute list of volume flags to apply.
example = literalExpression '' See `${getExe cfg.package} --help-flags` for more details.
{ '';
# "fk" enables filekeys (necessary for upget permission) (4 chars long) example = literalExpression ''
fk = 4; {
# scan for new files every 60sec # "fk" enables filekeys (necessary for upget permission) (4 chars long)
scan = 60; fk = 4;
# volflag "e2d" enables the uploads database # scan for new files every 60sec
e2d = true; scan = 60;
# "d2t" disables multimedia parsers (in case the uploads are malicious) # volflag "e2d" enables the uploads database
d2t = true; e2d = true;
# skips hashing file contents if path matches *.iso # "d2t" disables multimedia parsers (in case the uploads are malicious)
nohash = "\.iso$"; d2t = true;
}; # skips hashing file contents if path matches *.iso
''; nohash = "\.iso$";
default = { };
}; };
}; '';
} default = { };
) };
); };
}));
description = "A set of copyparty volumes to create"; description = "A set of copyparty volumes to create";
default = { default = {
"/" = { "/" = {
path = defaultShareDir; path = defaultShareDir;
access = { access = { r = "*"; };
r = "*";
};
}; };
}; };
example = literalExpression '' example = literalExpression ''
@ -254,122 +202,80 @@ in
}; };
}; };
config = mkIf cfg.enable ( config = mkIf cfg.enable {
let systemd.services.copyparty = {
command = "${getExe cfg.package} -c ${runtimeConfigPath}"; description = "http file sharing hub";
in wantedBy = [ "multi-user.target" ];
{
systemd.services.copyparty = {
description = "http file sharing hub";
wantedBy = [ "multi-user.target" ];
environment = { environment = {
PYTHONUNBUFFERED = "true"; PYTHONUNBUFFERED = "true";
XDG_CONFIG_HOME = externalStateDir; XDG_CONFIG_HOME = "${home}/.config";
};
preStart =
let
replaceSecretCommand =
name: attrs:
"${getExe pkgs.replace-secret} '${passwordPlaceholder name}' '${attrs.passwordFile}' ${runtimeConfigPath}";
in
''
set -euo pipefail
install -m 600 ${configFile} ${runtimeConfigPath}
${concatStringsSep "\n" (mapAttrsToList replaceSecretCommand cfg.accounts)}
'';
serviceConfig = {
Type = "simple";
ExecStart = command;
# Hardening options
User = cfg.user;
Group = cfg.group;
RuntimeDirectory = [ "copyparty" ];
RuntimeDirectoryMode = "0700";
StateDirectory = [ "copyparty" ];
StateDirectoryMode = "0700";
CacheDirectory = lib.mkIf (cfg.settings ? hist) [ "copyparty" ];
CacheDirectoryMode = lib.mkIf (cfg.settings ? hist) "0700";
WorkingDirectory = externalStateDir;
BindReadOnlyPaths = [
"/nix/store"
"-/etc/resolv.conf"
"-/etc/nsswitch.conf"
"-/etc/group"
"-/etc/hosts"
"-/etc/localtime"
] ++ (mapAttrsToList (k: v: "-${v.passwordFile}") cfg.accounts);
BindPaths =
(if cfg.settings ? hist then [ cfg.settings.hist ] else [ ])
++ [ externalStateDir ]
++ (mapAttrsToList (k: v: v.path) cfg.volumes);
# ProtectSystem = "strict";
# Note that unlike what 'ro' implies,
# this actually makes it impossible to read anything in the root FS,
# except for things explicitly mounted via `RuntimeDirectory`, `StateDirectory`, `CacheDirectory`, and `BindReadOnlyPaths`.
# This is because TemporaryFileSystem creates a *new* *empty* filesystem for the process, so only bindmounts are visible.
TemporaryFileSystem = "/:ro";
PrivateTmp = true;
PrivateDevices = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
RestrictSUIDSGID = true;
PrivateMounts = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectHostname = true;
ProtectClock = true;
ProtectProc = "invisible";
ProcSubset = "pid";
RestrictNamespaces = true;
RemoveIPC = true;
UMask = "0077";
LimitNOFILE = cfg.openFilesLimit;
NoNewPrivileges = true;
LockPersonality = true;
RestrictRealtime = true;
MemoryDenyWriteExecute = true;
};
}; };
# ensure volumes exist: preStart = let
systemd.tmpfiles.settings."copyparty" = ( replaceSecretCommand = name: attrs:
lib.attrsets.mapAttrs' ( "${getExe pkgs.replace-secret} '${
name: value: passwordPlaceholder name
lib.attrsets.nameValuePair (value.path) { }' '${attrs.passwordFile}' ${runtimeConfigPath}";
d = { in ''
#: in front of things means it wont change it if the directory already exists. set -euo pipefail
group = ":${cfg.group}"; install -m 600 ${configFile} ${runtimeConfigPath}
user = ":${cfg.user}"; ${concatStringsSep "\n"
mode = ":755"; (mapAttrsToList replaceSecretCommand cfg.accounts)}
}; '';
}
) cfg.volumes
);
users.groups.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") { }; serviceConfig = {
users.users.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") { Type = "simple";
description = "Service user for copyparty"; ExecStart = "${getExe cfg.package} -c ${runtimeConfigPath}";
group = "copyparty";
home = externalStateDir; # Hardening options
isSystemUser = true; User = "copyparty";
Group = "copyparty";
RuntimeDirectory = name;
RuntimeDirectoryMode = "0700";
StateDirectory = [ name "${name}/data" "${name}/.config" ];
StateDirectoryMode = "0700";
WorkingDirectory = home;
TemporaryFileSystem = "/:ro";
BindReadOnlyPaths = [
"/nix/store"
"-/etc/resolv.conf"
"-/etc/nsswitch.conf"
"-/etc/hosts"
"-/etc/localtime"
] ++ (mapAttrsToList (k: v: "-${v.passwordFile}") cfg.accounts);
BindPaths = [ home ] ++ (mapAttrsToList (k: v: v.path) cfg.volumes);
# Would re-mount paths ignored by temporary root
#ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
RestrictSUIDSGID = true;
PrivateMounts = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectHostname = true;
ProtectClock = true;
ProtectProc = "invisible";
ProcSubset = "pid";
RestrictNamespaces = true;
RemoveIPC = true;
UMask = "0077";
LimitNOFILE = cfg.openFilesLimit;
NoNewPrivileges = true;
LockPersonality = true;
RestrictRealtime = true;
}; };
environment.systemPackages = lib.mkIf cfg.mkHashWrapper [ };
(pkgs.writeShellScriptBin "copyparty-hash" ''
set -a # automatically export variables
# set same environment variables as the systemd service
${lib.pipe config.systemd.services.copyparty.environment [
(lib.filterAttrs (n: v: v != null && n != "PATH"))
(lib.mapAttrs (_: v: "${v}"))
(lib.toShellVars)
]}
PATH=${config.systemd.services.copyparty.environment.PATH}:$PATH
exec ${command} --ah-cli users.groups.copyparty = { };
'') users.users.copyparty = {
]; description = "Service user for copyparty";
} group = "copyparty";
); home = home;
isSystemUser = true;
};
};
} }

View file

@ -1,48 +1,54 @@
# 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.5" pkgver="1.7.2"
pkgrel=1 pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" pkgdesc="Portable file sharing hub"
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"
"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"
"qm-vamp-plugins: BPM detection"
"python-pyopenssl: ftps functionality" "python-pyopenssl: ftps functionality"
"python-pyzmq: send zeromq messages from event-hooks" "python-impacket-git: smb support (bad idea)"
"python-argon2-cffi: hashed passwords in config"
) )
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=("366e0e73d0e322e1dba9315baa328a6cd488f272cb431b9187da326361c2d7da") sha256sums=("fb261d45ce7cf146a3f620d1e3109eb5c584f8950e61a872e2d92d7b7447bae0")
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 "┗━━━━━━━━━━━━━━━──-"
} }

View file

@ -1,11 +1,11 @@
# this will start `/usr/bin/copyparty-sfx.py` # this will start `/usr/bin/copyparty-sfx.py`
# in a chroot, preventing accidental access elsewhere, # in a chroot, preventing accidental access elsewhere
# and read copyparty config from `/etc/copyparty.d/*.conf` # and read config from `/etc/copyparty.d/*.conf`
# #
# expose additional filesystem locations to copyparty # expose additional filesystem locations to copyparty
# by listing them between the last `cpp` and `--` # by listing them between the last `1000` and `--`
# #
# `cpp cpp` = user/group to run copyparty as; can be IDs (1000 1000) # `1000 1000` = what user to run copyparty as
# #
# unless you add -q to disable logging, you may want to remove the # unless you add -q to disable logging, you may want to remove the
# following line to allow buffering (slightly better performance): # following line to allow buffering (slightly better performance):
@ -24,9 +24,7 @@ ExecReload=/bin/kill -s USR1 $MAINPID
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=/bin/bash /usr/bin/prisonparty /var/lib/copyparty-jail cpp cpp \ ExecStart=/bin/bash /usr/bin/prisonparty /var/lib/copyparty-jail 1000 1000 /etc/copyparty.d -- \
/etc/copyparty.d \
-- \
/usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init /usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init
[Install] [Install]

View file

@ -1,44 +0,0 @@
# Contributor: Beethoven <beethovenisadog@protonmail.com>
pkgname=copyparty
pkgver=1.19.5
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=("366e0e73d0e322e1dba9315baa328a6cd488f272cb431b9187da326361c2d7da")
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"
}

View file

@ -1,118 +1,55 @@
{ { lib, stdenv, makeWrapper, fetchurl, utillinux, python, jinja2, impacket, pyftpdlib, pyopenssl, pillow, pyvips, ffmpeg, mutagen,
lib,
buildPythonApplication,
fetchurl,
util-linux,
python,
setuptools,
jinja2,
impacket,
pyopenssl,
cfssl,
argon2-cffi,
pillow,
pyvips,
pyzmq,
ffmpeg,
mutagen,
pyftpdlib,
magic,
partftpy,
fusepy, # for partyfuse
# use argon2id-hashed passwords in config files (sha2 is always available) # create thumbnails with Pillow; faster than FFmpeg / MediaProcessing
withHashedPasswords ? true, withThumbnails ? true,
# generate TLS certificates on startup (pointless when reverse-proxied) # create thumbnails with PyVIPS; even faster, uses more memory
withCertgen ? false, # -- can be combined with Pillow to support more filetypes
withFastThumbnails ? false,
# create thumbnails with Pillow; faster than FFmpeg / MediaProcessing # enable FFmpeg; thumbnails for most filetypes (also video and audio), extract audio metadata, transcode audio to opus
withThumbnails ? true, # -- 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
withMediaProcessing ? true,
# create thumbnails with PyVIPS; even faster, uses more memory # if MediaProcessing is not enabled, you probably want this instead (less accurate, but much safer and faster)
# -- can be combined with Pillow to support more filetypes withBasicAudioMetadata ? false,
withFastThumbnails ? false,
# enable FFmpeg; thumbnails for most filetypes (also video and audio), extract audio metadata, transcode audio to opus # enable FTPS support in the FTP server
# -- possibly dangerous if you allow anonymous uploads, since FFmpeg has a huge attack surface withFTPS ? false,
# -- can be combined with Thumbnails and/or FastThumbnails, since FFmpeg is slower than both
withMediaProcessing ? true,
# if MediaProcessing is not enabled, you probably want this instead (less accurate, but much safer and faster) # samba/cifs server; dangerous and buggy, enable if you really need it
withBasicAudioMetadata ? false, withSMB ? false,
# send ZeroMQ messages from event-hooks
withZeroMQ ? true,
# enable FTP server
withFTP ? true,
# enable FTPS support in the FTP server
withFTPS ? false,
# enable TFTP server
withTFTP ? false,
# samba/cifs server; dangerous and buggy, enable if you really need it
withSMB ? false,
# enables filetype detection for nameless uploads
withMagic ? false,
# extra packages to add to the PATH
extraPackages ? [ ],
# function that accepts a python packageset and returns a list of packages to
# be added to the python venv. useful for scripts and such that require
# additional dependencies
extraPythonPackages ? (_p: [ ]),
}: }:
let let
pinData = lib.importJSON ./pin.json; pinData = lib.importJSON ./pin.json;
runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg); pyEnv = python.withPackages (ps:
in with ps; [
buildPythonApplication {
pname = "copyparty";
inherit (pinData) version;
src = fetchurl {
inherit (pinData) url hash;
};
dependencies =
[
jinja2 jinja2
fusepy
] ]
++ lib.optional withSMB impacket ++ lib.optional withSMB impacket
++ lib.optional withFTP pyftpdlib
++ lib.optional withFTPS pyopenssl ++ lib.optional withFTPS pyopenssl
++ lib.optional withTFTP partftpy
++ lib.optional withCertgen cfssl
++ lib.optional withThumbnails pillow ++ lib.optional withThumbnails pillow
++ lib.optional withFastThumbnails pyvips ++ lib.optional withFastThumbnails pyvips
++ lib.optional withMediaProcessing ffmpeg ++ lib.optional withMediaProcessing ffmpeg
++ lib.optional withBasicAudioMetadata mutagen ++ lib.optional withBasicAudioMetadata mutagen
++ lib.optional withHashedPasswords argon2-cffi );
++ lib.optional withZeroMQ pyzmq in stdenv.mkDerivation {
++ lib.optional withMagic magic pname = "copyparty";
++ (extraPythonPackages python.pkgs); version = pinData.version;
makeWrapperArgs = [ "--prefix PATH : ${lib.makeBinPath runtimeDeps}" ]; src = fetchurl {
url = pinData.url;
pyproject = true; hash = pinData.hash;
build-system = [
setuptools
];
meta = {
description = "Turn almost any device into a file server";
longDescription = ''
Portable file server with accelerated resumable uploads, dedup, WebDAV,
FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps
'';
homepage = "https://github.com/9001/copyparty";
changelog = "https://github.com/9001/copyparty/releases/tag/v${pinData.version}";
license = lib.licenses.mit;
mainProgram = "copyparty";
sourceProvenance = [ lib.sourceTypes.fromSource ];
}; };
buildInputs = [ makeWrapper ];
dontUnpack = true;
dontBuild = true;
installPhase = ''
install -Dm755 $src $out/share/copyparty-sfx.py
makeWrapper ${pyEnv.interpreter} $out/bin/copyparty \
--set PATH '${lib.makeBinPath ([ utillinux ] ++ lib.optional withMediaProcessing ffmpeg)}:$PATH' \
--add-flags "$out/share/copyparty-sfx.py"
'';
} }

View file

@ -1,5 +1,5 @@
{ {
"url": "https://github.com/9001/copyparty/releases/download/v1.19.5/copyparty-1.19.5.tar.gz", "url": "https://github.com/9001/copyparty/releases/download/v1.7.2/copyparty-sfx.py",
"version": "1.19.5", "version": "1.7.2",
"hash": "sha256-Nm4Oc9DjIuHbqTFbqjKKbNSI8nLLQxuRh9oyY2HC19o=" "hash": "sha256-h22sRRO/bpydgRVmSVD05ZLuzsUxBCWU3izt9Eg9bf0="
} }

View file

@ -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

View file

@ -1,11 +0,0 @@
final: prev: {
copyparty = final.python3.pkgs.callPackage ./copyparty {
ffmpeg = final.ffmpeg-full;
};
python3 = prev.python3.override {
packageOverrides = pyFinal: pyPrev: {
partftpy = pyFinal.callPackage ./partftpy { };
};
};
}

View file

@ -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;
};
}

View file

@ -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="
}

View file

@ -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()

View file

@ -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

View file

@ -15,19 +15,11 @@ save one of these as `.epilogue.html` inside a folder to customize it:
point `--js-browser` to one of these by URL: point `--js-browser` to one of these by URL:
* [`minimal-up2k.js`](minimal-up2k.js) is similar to the above `minimal-up2k.html` except it applies globally to all write-only folders * [`minimal-up2k.js`](minimal-up2k.js) is similar to the above `minimal-up2k.html` except it applies globally to all write-only folders
* [`quickmove.js`](quickmove.js) adds a hotkey to move selected files into a subfolder
* [`up2k-hooks.js`](up2k-hooks.js) lets you specify a ruleset for files to skip uploading * [`up2k-hooks.js`](up2k-hooks.js) lets you specify a ruleset for files to skip uploading
* [`up2k-hook-ytid.js`](up2k-hook-ytid.js) is a more specific example checking youtube-IDs against some API * [`up2k-hook-ytid.js`](up2k-hook-ytid.js) is a more specific example checking youtube-IDs against some API
## example any-js
point `--js-browser` and/or `--js-other` to one of these by URL:
* [`banner.js`](banner.js) shows a very enterprise [legal-banner](https://github.com/user-attachments/assets/8ae8e087-b209-449c-b08d-74e040f0284b)
## example browser-css ## example browser-css
point `--css-browser` to one of these by URL: point `--css-browser` to one of these by URL:

View file

@ -1,93 +0,0 @@
(function() {
// usage: copy this to '.banner.js' in your webroot,
// and run copyparty with the following arguments:
// --js-browser /.banner.js --js-other /.banner.js
// had to pick the most chuuni one as the default
var bannertext = '' +
'<h3>You are accessing a U.S. Government (USG) Information System (IS) that is provided for USG-authorized use only.</h3>' +
'<p>By using this IS (which includes any device attached to this IS), you consent to the following conditions:</p>' +
'<ul>' +
'<li>The USG routinely intercepts and monitors communications on this IS for purposes including, but not limited to, penetration testing, COMSEC monitoring, network operations and defense, personnel misconduct (PM), law enforcement (LE), and counterintelligence (CI) investigations.</li>' +
'<li>At any time, the USG may inspect and seize data stored on this IS.</li>' +
'<li>Communications using, or data stored on, this IS are not private, are subject to routine monitoring, interception, and search, and may be disclosed or used for any USG-authorized purpose.</li>' +
'<li>This IS includes security measures (e.g., authentication and access controls) to protect USG interests -- not for your personal benefit or privacy.</li>' +
'<li>Notwithstanding the above, using this IS does not constitute consent to PM, LE or CI investigative searching or monitoring of the content of privileged communications, or work product, related to personal representation or services by attorneys, psychotherapists, or clergy, and their assistants. Such communications and work product are private and confidential. See User Agreement for details.</li>' +
'</ul>';
// fancy div to insert into pages
function bannerdiv(border) {
var ret = mknod('div', null, bannertext);
if (border)
ret.setAttribute("style", "border:1em solid var(--fg); border-width:.3em 0; margin:3em 0");
return ret;
}
// keep all of these false and then selectively enable them in the if-blocks below
var show_msgbox = false,
login_top = false,
top = false,
bottom = false,
top_bordered = false,
bottom_bordered = false;
if (QS("h1#cc") && QS("a#k")) {
// this is the controlpanel
// (you probably want to keep just one of these enabled)
show_msgbox = true;
login_top = true;
bottom = true;
}
else if (ebi("swin") && ebi("smac")) {
// this is the connect-page, same deal here
show_msgbox = true;
top_bordered = true;
bottom_bordered = true;
}
else if (ebi("op_cfg") || ebi("div#mw") ) {
// we're running in the main filebrowser (op_cfg) or markdown-viewer/editor (div#mw),
// fragile pages which break if you do something too fancy
show_msgbox = true;
}
// shows a fullscreen messagebox; works on all pages
if (show_msgbox) {
var now = Math.floor(Date.now() / 1000),
last_shown = sread("bannerts") || 0;
// 60 * 60 * 17 = 17 hour cooldown
if (now - last_shown > 60 * 60 * 17) {
swrite("bannerts", now);
modal.confirm(bannertext, null, function () {
location = 'https://this-page-intentionally-left-blank.org/';
});
}
}
// show a message on the page footer; only works on the connect-page
if (top || top_bordered) {
var dst = ebi('wrap');
dst.insertBefore(bannerdiv(top_bordered), dst.firstChild);
}
// show a message on the page footer; only works on the controlpanel and connect-page
if (bottom || bottom_bordered) {
ebi('wrap').appendChild(bannerdiv(bottom_bordered));
}
// show a message on the top of the page; only works on the controlpanel
if (login_top) {
var dst = QS('h1');
dst.parentNode.insertBefore(bannerdiv(false), dst);
}
})();

View file

@ -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();
}
})();

View file

@ -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 = `

View file

@ -1,140 +0,0 @@
"use strict";
// USAGE:
// place this file somewhere in the webroot,
// for example in a folder named ".res" to hide it, and then
// python3 copyparty-sfx.py -v .::A --js-browser /.res/quickmove.js
//
// DESCRIPTION:
// the command above launches copyparty with one single volume;
// ".::A" = current folder as webroot, and everyone has Admin
//
// the plugin adds hotkey "W" which moves all selected files
// into a subfolder named "foobar" inside the current folder
(function() {
var action_to_perform = ask_for_confirmation_and_then_move;
// this decides what the new hotkey should do;
// ask_for_confirmation_and_then_move = show a yes/no box,
// move_selected_files = just move the files immediately
var move_destination = "foobar";
// this is the target folder to move files to;
// by default it is a subfolder of the current folder,
// but it can also be an absolute path like "/foo/bar"
// ===
// === END OF CONFIG
// ===
var main_hotkey_handler, // copyparty's original hotkey handler
plugin_enabler, // timer to engage this plugin when safe
files_to_move; // list of files to move
function ask_for_confirmation_and_then_move() {
var num_files = msel.getsel().length,
msg = "move the selected " + num_files + " files?";
if (!num_files)
return toast.warn(2, 'no files were selected to be moved');
modal.confirm(msg, move_selected_files, null);
}
function move_selected_files() {
var selection = msel.getsel();
if (!selection.length)
return toast.warn(2, 'no files were selected to be moved');
if (thegrid.bbox) {
// close image/video viewer
thegrid.bbox = null;
baguetteBox.destroy();
}
files_to_move = [];
for (var a = 0; a < selection.length; a++)
files_to_move.push(selection[a].vp);
move_next_file();
}
function move_next_file() {
var num_files = files_to_move.length,
filepath = files_to_move.pop(),
filename = vsplit(filepath)[1];
toast.inf(10, "moving " + num_files + " files...\n\n" + filename);
var dst = move_destination;
if (!dst.endsWith('/'))
// must have a trailing slash, so add it
dst += '/';
if (!dst.startsWith('/'))
// destination is a relative path, so prefix current folder path
dst = get_evpath() + dst;
// and finally append the filename
dst += '/' + filename;
// prepare the move-request to be sent
var xhr = new XHR();
xhr.onload = xhr.onerror = function() {
if (this.status !== 201)
return toast.err(30, 'move failed: ' + esc(this.responseText));
if (files_to_move.length)
return move_next_file(); // still more files to go
toast.ok(1, 'move OK');
treectl.goto(); // reload the folder contents
};
xhr.open('POST', filepath + '?move=' + dst);
xhr.send();
}
function our_hotkey_handler(e) {
// bail if either ALT, CTRL, or SHIFT is pressed
if (e.altKey || e.shiftKey || e.isComposing || ctrl(e))
return main_hotkey_handler(e); // let copyparty handle this keystroke
var key_name = (e.code || e.key) + '',
ae = document.activeElement,
aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : '';
// check the current aet (active element type),
// only continue if one of the following currently has input focus:
// nothing | link | button | table-row | table-cell | div | text
if (aet && !/^(a|button|tr|td|div|pre)$/.test(aet))
return main_hotkey_handler(e); // let copyparty handle this keystroke
if (key_name == 'KeyW') {
// okay, this one's for us... do the thing
action_to_perform();
return ev(e);
}
return main_hotkey_handler(e); // let copyparty handle this keystroke
}
function enable_plugin() {
if (!window.hotkeys_attached)
return console.log('quickmove is waiting for the page to finish loading');
clearInterval(plugin_enabler);
main_hotkey_handler = document.onkeydown;
document.onkeydown = our_hotkey_handler;
console.log('quickmove is now enabled');
}
// copyparty doesn't enable its hotkeys until the page
// has finished loading, so we'll wait for that too
plugin_enabler = setInterval(enable_plugin, 100);
})();

View file

@ -10,7 +10,7 @@ name="copyparty"
rcvar="copyparty_enable" rcvar="copyparty_enable"
copyparty_user="copyparty" copyparty_user="copyparty"
copyparty_args="-e2dsa -v /storage:/storage:r" # change as you see fit copyparty_args="-e2dsa -v /storage:/storage:r" # change as you see fit
copyparty_command="/usr/local/bin/python3.9 /usr/local/copyparty/copyparty-sfx.py ${copyparty_args}" copyparty_command="/usr/local/bin/python3.8 /usr/local/copyparty/copyparty-sfx.py ${copyparty_args}"
pidfile="/var/run/copyparty/${name}.pid" pidfile="/var/run/copyparty/${name}.pid"
command="/usr/sbin/daemon" command="/usr/sbin/daemon"
command_args="-P ${pidfile} -r -f ${copyparty_command}" command_args="-P ${pidfile} -r -f ${copyparty_command}"

View file

@ -1,11 +0,0 @@
{
"code": "// https://addons.mozilla.org/en-US/firefox/addon/contextlets/\n// https://github.com/davidmhammond/contextlets\n\nvar url = 'http://partybox.local:3923/';\nvar pw = 'wark';\n\nvar xhr = new XMLHttpRequest();\nxhr.msg = this.info.linkUrl || this.info.srcUrl;\nxhr.open('POST', url, true);\nxhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8');\nxhr.setRequestHeader('PW', pw);\nxhr.send('msg=' + xhr.msg);\n",
"contexts": [
"link"
],
"icons": null,
"patterns": "",
"scope": "background",
"title": "send to cpp",
"type": "normal"
}

View file

@ -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 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

19
contrib/sharex-html.sxcu Normal file
View file

@ -0,0 +1,19 @@
{
"Version": "13.5.0",
"Name": "copyparty-html",
"DestinationType": "ImageUploader",
"RequestMethod": "POST",
"RequestURL": "http://127.0.0.1:3923/sharex",
"Parameters": {
"pw": "wark"
},
"Body": "MultipartFormData",
"Arguments": {
"act": "bput"
},
"FileFormName": "f",
"RegexList": [
"bytes // <a href=\"/([^\"]+)\""
],
"URL": "http://127.0.0.1:3923/$regex:1|1$"
}

View file

@ -1,19 +1,17 @@
{ {
"Version": "15.0.0", "Version": "13.5.0",
"Name": "copyparty", "Name": "copyparty",
"DestinationType": "ImageUploader", "DestinationType": "ImageUploader",
"RequestMethod": "POST", "RequestMethod": "POST",
"RequestURL": "http://127.0.0.1:3923/sharex", "RequestURL": "http://127.0.0.1:3923/sharex",
"Parameters": { "Parameters": {
"pw": "wark",
"j": null "j": null
}, },
"Headers": {
"pw": "PUT_YOUR_PASSWORD_HERE_MY_DUDE"
},
"Body": "MultipartFormData", "Body": "MultipartFormData",
"Arguments": { "Arguments": {
"act": "bput" "act": "bput"
}, },
"FileFormName": "f", "FileFormName": "f",
"URL": "{json:files[0].url}" "URL": "$json:files[0].url$"
} }

View file

@ -1,13 +0,0 @@
{
"Name": "copyparty",
"DestinationType": "ImageUploader, TextUploader, FileUploader",
"RequestURL": "http://127.0.0.1:3923/sharex",
"FileFormName": "f",
"Arguments": {
"act": "bput"
},
"Headers": {
"accept": "url",
"pw": "PUT_YOUR_PASSWORD_HERE_MY_DUDE"
}
}

View file

@ -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

View file

@ -1,13 +0,0 @@
[global]
i: 127.0.0.1
[accounts]
user: password
[/]
/var/lib/copyparty-jail
accs:
r: *
rwdma: user
flags:
grid

View file

@ -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

View file

@ -1,34 +1,28 @@
# this will start `/usr/local/bin/copyparty-sfx.py` and # this will start `/usr/local/bin/copyparty-sfx.py`
# read copyparty config from `/etc/copyparty.conf`, for example: # and share '/mnt' with anonymous read+write
# https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.conf
# #
# installation: # installation:
# wget https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py -O /usr/local/bin/copyparty-sfx.py # wget https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py -O /usr/local/bin/copyparty-sfx.py
# useradd -r -s /sbin/nologin -m -d /var/lib/copyparty copyparty
# firewall-cmd --permanent --add-port=3923/tcp # --zone=libvirt
# firewall-cmd --reload
# cp -pv copyparty.service /etc/systemd/system/ # cp -pv copyparty.service /etc/systemd/system/
# cp -pv copyparty.conf /etc/
# restorecon -vr /etc/systemd/system/copyparty.service # on fedora/rhel # restorecon -vr /etc/systemd/system/copyparty.service # on fedora/rhel
# firewall-cmd --permanent --add-port={80,443,3923}/tcp # --zone=libvirt
# firewall-cmd --reload
# systemctl daemon-reload && systemctl enable --now copyparty # systemctl daemon-reload && systemctl enable --now copyparty
# #
# every time you edit this file, you must "systemctl daemon-reload"
# for the changes to take effect and then "systemctl restart copyparty"
#
# if it fails to start, first check this: systemctl status copyparty # if it fails to start, first check this: systemctl status copyparty
# then try starting it while viewing logs: # then try starting it while viewing logs: journalctl -fan 100
# journalctl -fan 100
# tail -Fn 100 /var/log/copyparty/$(date +%Y-%m%d.log)
#
# if you run into any issues, for example thumbnails not working,
# try removing the "some quick hardening" section and then please
# let me know if that actually helped so we can look into it
# #
# you may want to: # you may want to:
# - change "User=copyparty" and "/var/lib/copyparty/" to another user # change "User=cpp" and "/home/cpp/" to another user
# - edit /etc/copyparty.conf to configure copyparty # remove the nft lines to only listen on port 3923
# and in the ExecStart= line: # and in the ExecStart= line:
# - change '/usr/bin/python3' to another interpreter # change '/usr/bin/python3' to another interpreter
# change '/mnt::rw' to another location or permission-set
# add '-q' to disable logging on busy servers
# add '-i 127.0.0.1' to only allow local connections
# add '-e2dsa' to enable filesystem scanning + indexing
# add '-e2ts' to enable metadata indexing
# remove '--ansi' to disable colored logs
# #
# with `Type=notify`, copyparty will signal systemd when it is ready to # with `Type=notify`, copyparty will signal systemd when it is ready to
# accept connections; correctly delaying units depending on copyparty. # accept connections; correctly delaying units depending on copyparty.
@ -36,9 +30,11 @@
# python disabling line-buffering, so messages are out-of-order: # python disabling line-buffering, so messages are out-of-order:
# https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png # https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png
# #
######################################################################## # unless you add -q to disable logging, you may want to remove the
######################################################################## # following line to allow buffering (slightly better performance):
# Environment=PYTHONUNBUFFERED=x
#
# keep ExecStartPre before ExecStart, at least on rhel8
[Unit] [Unit]
Description=copyparty file server Description=copyparty file server
@ -48,52 +44,23 @@ Type=notify
SyslogIdentifier=copyparty SyslogIdentifier=copyparty
Environment=PYTHONUNBUFFERED=x Environment=PYTHONUNBUFFERED=x
ExecReload=/bin/kill -s USR1 $MAINPID ExecReload=/bin/kill -s USR1 $MAINPID
PermissionsStartOnly=true
## user to run as + where the TLS certificate is (if any) # user to run as + where the TLS certificate is (if any)
## User=cpp
User=copyparty Environment=XDG_CONFIG_HOME=/home/cpp/.config
Group=copyparty
WorkingDirectory=/var/lib/copyparty
Environment=XDG_CONFIG_HOME=/var/lib/copyparty/.config
## OPTIONAL: allow copyparty to listen on low ports (like 80/443); # OPTIONAL: setup forwarding from ports 80 and 443 to port 3923
## you need to uncomment the "p: 80,443,3923" in the config too ExecStartPre=+/bin/bash -c 'nft -n -a list table nat | awk "/ to :3923 /{print\$NF}" | xargs -rL1 nft delete rule nat prerouting handle; true'
## ------------------------------------------------------------ ExecStartPre=+nft add table ip nat
## a slightly safer alternative is to enable partyalone.service ExecStartPre=+nft -- add chain ip nat prerouting { type nat hook prerouting priority -100 \; }
## which does portforwarding with nftables instead, but an even ExecStartPre=+nft add rule ip nat prerouting tcp dport 80 redirect to :3923
## better option is to use a reverse-proxy (nginx/caddy/...) ExecStartPre=+nft add rule ip nat prerouting tcp dport 443 redirect to :3923
##
AmbientCapabilities=CAP_NET_BIND_SERVICE
## some quick hardening; TODO port more from the nixos package # stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running
## ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
MemoryMax=50%
MemorySwapMax=50%
ProtectClock=true
ProtectControlGroups=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=invisible
RemoveIPC=true
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
## create a directory for logfiles; # copyparty settings
## this defines $LOGS_DIRECTORY which is used in copyparty.conf ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py --ansi -e2d -v /mnt::rw
##
LogsDirectory=copyparty
## finally, start copyparty and give it the config file:
##
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -c /etc/copyparty.conf
# NOTE: if you installed copyparty from an OS package repo (nice)
# then you probably want something like this instead:
#ExecStart=/usr/bin/copyparty -c /etc/copyparty.conf
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View file

@ -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

View file

@ -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`

View file

@ -1,5 +1,5 @@
# this will start `/usr/local/bin/copyparty-sfx.py` # this will start `/usr/local/bin/copyparty-sfx.py`
# in a chroot, preventing accidental access elsewhere, # in a chroot, preventing accidental access elsewhere
# and share '/mnt' with anonymous read+write # and share '/mnt' with anonymous read+write
# #
# installation: # installation:
@ -7,9 +7,9 @@
# 2) cp -pv prisonparty.service /etc/systemd/system && systemctl enable --now prisonparty # 2) cp -pv prisonparty.service /etc/systemd/system && systemctl enable --now prisonparty
# #
# expose additional filesystem locations to copyparty # expose additional filesystem locations to copyparty
# by listing them between the last `cpp` and `--` # by listing them between the last `1000` and `--`
# #
# `cpp cpp` = user/group to run copyparty as; can be IDs (1000 1000) # `1000 1000` = what user to run copyparty as
# #
# you may want to: # you may want to:
# change '/mnt::rw' to another location or permission-set # change '/mnt::rw' to another location or permission-set
@ -32,9 +32,7 @@ ExecReload=/bin/kill -s USR1 $MAINPID
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=/bin/bash /usr/local/bin/prisonparty.sh /var/lib/copyparty-jail cpp cpp \ ExecStart=/bin/bash /usr/local/bin/prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt -- \
/mnt \
-- \
/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::rw /usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::rw
[Install] [Install]

View file

@ -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

View file

@ -1,116 +0,0 @@
/* copy bsod.* into a folder named ".themes" in your webroot and then
--themes=10 --theme=9 --css-browser=/.themes/bsod.css
*/
html.ey {
--w2: #3d7bbc;
--w3: #5fcbec;
--fg: #fff;
--fg-max: #fff;
--fg-weak: var(--w3);
--bg: #2067b2;
--bg-d3: var(--bg);
--bg-d2: var(--w2);
--bg-d1: var(--fg-weak);
--bg-u2: var(--bg);
--bg-u3: var(--bg);
--bg-u5: var(--w2);
--tab-alt: var(--fg-weak);
--row-alt: var(--w2);
--scroll: var(--w3);
--a: #fff;
--a-b: #fff;
--a-hil: #fff;
--a-h-bg: var(--fg-weak);
--a-dark: var(--a);
--a-gray: var(--fg-weak);
--btn-fg: var(--a);
--btn-bg: var(--w2);
--btn-h-fg: var(--w2);
--btn-1-fg: var(--bg);
--btn-1-bg: var(--a);
--txt-sh: a;
--txt-bg: var(--w2);
--u2-b1-bg: var(--w2);
--u2-b2-bg: var(--w2);
--u2-txt-bg: var(--w2);
--u2-tab-bg: a;
--u2-tab-1-bg: var(--w2);
--sort-1: var(--a);
--sort-1: var(--fg-weak);
--tree-bg: var(--bg);
--g-b1: a;
--g-b2: a;
--g-f-bg: var(--w2);
--f-sh1: 0.1;
--f-sh2: 0.02;
--f-sh3: 0.1;
--f-h-b1: a;
--srv-1: var(--a);
--srv-3: var(--a);
--mp-sh: a;
}
html.ey {
background: url('bsod.png') top 5em right 4.5em no-repeat fixed var(--bg);
}
html.ey body#b {
background: var(--bg); /*sandbox*/
}
html.ey #ops {
margin: 1.7em 1.5em 0 1.5em;
border-radius: .3em;
border-width: 1px 0;
}
html.ey #ops a {
text-shadow: 1px 1px 0 rgba(0,0,0,0.5);
}
html.ey .opbox {
margin: 1.5em 0 0 0;
}
html.ey #tree {
box-shadow: none;
}
html.ey #tt {
border-color: var(--w2);
background: var(--w2);
}
html.ey .mdo a {
background: none;
text-decoration: underline;
}
html.ey .mdo pre,
html.ey .mdo code {
color: #fff;
background: var(--w2);
border: none;
}
html.ey .mdo h1,
html.ey .mdo h2 {
background: none;
border-color: var(--w2);
}
html.ey .mdo ul ul,
html.ey .mdo ul ol,
html.ey .mdo ol ul,
html.ey .mdo ol ol {
border-color: var(--w2);
}
html.ey .mdo p>em,
html.ey .mdo li>em,
html.ey .mdo td>em {
color: #fd0;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1,25 +0,0 @@
# ./traefik --configFile=copyparty.yaml
entryPoints:
web:
address: :8080
transport:
# don't disconnect during big uploads
respondingTimeouts:
readTimeout: "0s"
log:
level: DEBUG
providers:
file:
# WARNING: must be same filename as current file
filename: "copyparty.yaml"
http:
services:
service-cpp:
loadBalancer:
servers:
- url: "http://127.0.0.1:3923/"
routers:
my-router:
rule: "PathPrefix(`/`)"
service: service-cpp

View file

@ -1,2 +0,0 @@
rem run copyparty.exe on machines with busted environment variables
cmd /v /c "set TMP=\tmp && copyparty.exe"

View file

@ -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()

View file

@ -16,15 +16,14 @@ except:
TYPE_CHECKING = False TYPE_CHECKING = False
if True: if True:
from typing import Any, Callable, Optional from typing import Any, Callable
PY2 = sys.version_info < (3,) PY2 = sys.version_info < (3,)
PY36 = sys.version_info > (3, 6)
if not PY2: if not PY2:
unicode: Callable[[Any], str] = str unicode: Callable[[Any], str] = str
else: else:
sys.dont_write_bytecode = True sys.dont_write_bytecode = True
unicode = unicode # type: ignore unicode = unicode # noqa: F821 # pylint: disable=undefined-variable,self-assigning-variable
WINDOWS: Any = ( WINDOWS: Any = (
[int(x) for x in platform.version().split(".")] [int(x) for x in platform.version().split(".")]
@ -51,67 +50,13 @@ try:
except: except:
CORES = (os.cpu_count() if hasattr(os, "cpu_count") else 0) or 2 CORES = (os.cpu_count() if hasattr(os, "cpu_count") else 0) or 2
# all embedded resources to be retrievable over http
zs = """
web/a/partyfuse.py
web/a/u2c.py
web/a/webdav-cfg.bat
web/baguettebox.js
web/browser.css
web/browser.html
web/browser.js
web/browser2.html
web/cf.html
web/copyparty.gif
web/deps/busy.mp3
web/deps/easymde.css
web/deps/easymde.js
web/deps/marked.js
web/deps/fuse.py
web/deps/mini-fa.css
web/deps/mini-fa.woff
web/deps/prism.css
web/deps/prism.js
web/deps/prismd.css
web/deps/scp.woff2
web/deps/sha512.ac.js
web/deps/sha512.hw.js
web/idp.html
web/iiam.gif
web/md.css
web/md.html
web/md.js
web/md2.css
web/md2.js
web/mde.css
web/mde.html
web/mde.js
web/msg.css
web/msg.html
web/rups.css
web/rups.html
web/rups.js
web/shares.css
web/shares.html
web/shares.js
web/splash.css
web/splash.html
web/splash.js
web/svcs.html
web/svcs.js
web/ui.css
web/up2k.js
web/util.js
web/w.hash.js
"""
RES = set(zs.strip().split("\n"))
class EnvParams(object): class EnvParams(object):
def __init__(self) -> None: def __init__(self) -> None:
self.t0 = time.time() self.t0 = time.time()
self.mod = "" self.mod = ""
self.cfg = "" self.cfg = ""
self.ox = getattr(sys, "oxidized", None)
E = EnvParams() E = EnvParams()

1359
copyparty/__main__.py Normal file → Executable file

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
VERSION = (1, 19, 5) VERSION = (1, 7, 4)
CODENAME = "usernames" CODENAME = "unlinked"
BUILD_DT = (2025, 8, 21) BUILD_DT = (2023, 6, 11)
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)

File diff suppressed because it is too large Load diff

View file

@ -9,11 +9,8 @@ from . import path as path
if True: # pylint: disable=using-constant-test if True: # pylint: disable=using-constant-test
from typing import Any, Optional from typing import Any, Optional
MKD_755 = {"chmod_d": 0o755} _ = (path,)
MKD_700 = {"chmod_d": 0o700} __all__ = ["path"]
_ = (path, MKD_755, MKD_700)
__all__ = ["path", "MKD_755", "MKD_700"]
# grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c # grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c
# printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')" # printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')"
@ -23,39 +20,19 @@ def chmod(p: str, mode: int) -> None:
return os.chmod(fsenc(p), mode) return os.chmod(fsenc(p), mode)
def chown(p: str, uid: int, gid: int) -> None:
return os.chown(fsenc(p), uid, gid)
def listdir(p: str = ".") -> list[str]: def listdir(p: str = ".") -> list[str]:
return [fsdec(x) for x in os.listdir(fsenc(p))] return [fsdec(x) for x in os.listdir(fsenc(p))]
def makedirs(name: str, vf: dict[str, Any] = MKD_755, exist_ok: bool = True) -> bool: def makedirs(name: str, mode: int = 0o755, exist_ok: bool = True) -> bool:
# os.makedirs does 777 for all but leaf; this does mode on all
todo = []
bname = fsenc(name) bname = fsenc(name)
while bname: try:
if os.path.isdir(bname): os.makedirs(bname, mode)
break return True
todo.append(bname) except:
bname = os.path.dirname(bname) if not exist_ok or not os.path.isdir(bname):
if not todo:
if not exist_ok:
os.mkdir(bname) # to throw
return False
mode = vf["chmod_d"]
chown = "chown" in vf
for zb in todo[::-1]:
try:
os.mkdir(zb, mode)
if chown:
os.chown(zb, vf["uid"], vf["gid"])
except:
if os.path.isdir(zb):
continue
raise raise
return True return False
def mkdir(p: str, mode: int = 0o755) -> None: def mkdir(p: str, mode: int = 0o755) -> None:
@ -66,10 +43,6 @@ def open(p: str, *a, **ka) -> int:
return os.open(fsenc(p), *a, **ka) return os.open(fsenc(p), *a, **ka)
def readlink(p: str) -> str:
return fsdec(os.readlink(fsenc(p)))
def rename(src: str, dst: str) -> None: def rename(src: str, dst: str) -> None:
return os.rename(fsenc(src), fsenc(dst)) return os.rename(fsenc(src), fsenc(dst))

Some files were not shown because too many files have changed in this diff Show more