Compare commits

..

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

160 changed files with 1898 additions and 22386 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.**

3
.gitignore vendored
View file

@ -43,6 +43,3 @@ scripts/docker/*.err
# nix build output link # nix build output link
result result
# IDEA config
.idea/

View file

@ -1,21 +1,8 @@
* **found a bug?** [create an issue!](https://github.com/9001/copyparty/issues) or let me know in the [discord](https://discord.gg/25J8CdTT6G) :> * do something cool
* **fixed a bug?** create a PR or post a patch! big thx in advance :>
* **have a cool idea?** let's discuss it! anywhere's fine, you choose.
but please: really tho, send a PR or an issue or whatever, all appreciated, anything goes, just behave aight 👍👍
# do not use AI / LMM when writing code
copyparty is 100% organic, free-range, human-written software!
> ⚠ you are now entering a no-copilot zone
the *only* place where LMM/AI *may* be accepted is for [localization](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) if you are fluent and have confirmed that the translation is accurate.
sorry for the harsh tone, but this is important to me 🙏
but to be more specific,
# contribution ideas # contribution ideas
@ -41,8 +28,6 @@ aside from documentation and ideas, some other things that would be cool to have
* **translations** -- the copyparty web-UI has translations for english and norwegian at the top of [browser.js](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/web/browser.js); if you'd like to add a translation for another language then that'd be welcome! and if that language has a grammar that doesn't fit into the way the strings are assembled, then we'll fix that as we go :> * **translations** -- the copyparty web-UI has translations for english and norwegian at the top of [browser.js](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/web/browser.js); if you'd like to add a translation for another language then that'd be welcome! and if that language has a grammar that doesn't fit into the way the strings are assembled, then we'll fix that as we go :>
* but please note that support for [RTL (Right-to-Left) languages](https://en.wikipedia.org/wiki/Right-to-left_script) is currently not planned, since the javascript is a bit too jank for that
* **UI ideas** -- at some point I was thinking of rewriting the UI in react/preact/something-not-vanilla-javascript, but I'll admit the comfiness of not having any build stage combined with raw performance has kinda convinced me otherwise :p but I'd be very open to ideas on how the UI could be improved, or be more intuitive. * **UI ideas** -- at some point I was thinking of rewriting the UI in react/preact/something-not-vanilla-javascript, but I'll admit the comfiness of not having any build stage combined with raw performance has kinda convinced me otherwise :p but I'd be very open to ideas on how the UI could be improved, or be more intuitive.
* **docker improvements** -- I don't really know what I'm doing when it comes to containers, so I'm sure there's a *huge* room for improvement here, mainly regarding how you're supposed to use the container with kubernetes / docker-compose / any of the other popular ways to do things. At some point I swear I'll start learning about docker so I can pick up clach04's [docker-compose draft](https://github.com/9001/copyparty/issues/38) and learn how that stuff ticks, unless someone beats me to it! * **docker improvements** -- I don't really know what I'm doing when it comes to containers, so I'm sure there's a *huge* room for improvement here, mainly regarding how you're supposed to use the container with kubernetes / docker-compose / any of the other popular ways to do things. At some point I swear I'll start learning about docker so I can pick up clach04's [docker-compose draft](https://github.com/9001/copyparty/issues/38) and learn how that stuff ticks, unless someone beats me to it!

585
README.md

File diff suppressed because it is too large Load diff

View file

@ -78,6 +78,3 @@ cd /mnt/nas/music/.hist
# [`prisonparty.sh`](prisonparty.sh) # [`prisonparty.sh`](prisonparty.sh)
* run copyparty in a chroot, preventing any accidental file access * run copyparty in a chroot, preventing any accidental file access
* creates bindmounts for /bin, /lib, and so on, see `sysdirs=` * creates bindmounts for /bin, /lib, and so on, see `sysdirs=`
# [`bubbleparty.sh`](bubbleparty.sh)
* run copyparty in an isolated process, preventing any accidental file access and more

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

@ -20,8 +20,6 @@ each plugin must define a `main()` which takes 3 arguments;
## on404 ## on404
* [redirect.py](redirect.py) sends an HTTP 301 or 302, redirecting the client to another page/file
* [randpic.py](randpic.py) redirects `/foo/bar/randpic.jpg` to a random pic in `/foo/bar/`
* [sorry.py](answer.py) replies with a custom message instead of the usual 404 * [sorry.py](answer.py) replies with a custom message instead of the usual 404
* [nooo.py](nooo.py) replies with an endless noooooooooooooo * [nooo.py](nooo.py) replies with an endless noooooooooooooo
* [never404.py](never404.py) 100% guarantee that 404 will never be a thing again as it automatically creates dummy files whenever necessary * [never404.py](never404.py) 100% guarantee that 404 will never be a thing again as it automatically creates dummy files whenever necessary

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

@ -14,8 +14,6 @@ run copyparty with `--help-hooks` for usage details / hook type explanations (xm
* [discord-announce.py](discord-announce.py) announces new uploads on discord using webhooks ([example](https://user-images.githubusercontent.com/241032/215304439-1c1cb3c8-ec6f-4c17-9f27-81f969b1811a.png)) * [discord-announce.py](discord-announce.py) announces new uploads on discord using webhooks ([example](https://user-images.githubusercontent.com/241032/215304439-1c1cb3c8-ec6f-4c17-9f27-81f969b1811a.png))
* [reject-mimetype.py](reject-mimetype.py) rejects uploads unless the mimetype is acceptable * [reject-mimetype.py](reject-mimetype.py) rejects uploads unless the mimetype is acceptable
* [into-the-cache-it-goes.py](into-the-cache-it-goes.py) avoids bugs in caching proxies by immediately downloading each file that is uploaded * [into-the-cache-it-goes.py](into-the-cache-it-goes.py) avoids bugs in caching proxies by immediately downloading each file that is uploaded
* [podcast-normalizer.py](podcast-normalizer.py) creates a second file with dynamic-range-compression whenever an audio file is uploaded
* good example of the `idx` [hook effect](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hook-effects) to tell copyparty about additional files to scan/index
# upload batches # upload batches
@ -27,11 +25,9 @@ these are `--xiu` hooks; unlike `xbu` and `xau` (which get executed on every sin
# before upload # before upload
* [reject-extension.py](reject-extension.py) rejects uploads if they match a list of file extensions * [reject-extension.py](reject-extension.py) rejects uploads if they match a list of file extensions
* [reloc-by-ext.py](reloc-by-ext.py) redirects an upload to another destination based on the file extension * [reloc-by-ext.py](reloc-by-ext.py) redirects an upload to another destination based on the file extension
* good example of the `reloc` [hook effect](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hook-effects)
# 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 * [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 * [msg-log.py](msg-log.py) is a guestbook; logs messages to a doc in the same folder

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

@ -71,9 +71,6 @@ def main():
## selecting it inside the print at the end: ## selecting it inside the print at the end:
## ##
# move all uploads to one specific folder
into_junk = {"vp": "/junk"}
# create a subfolder named after the filetype and move it into there # create a subfolder named after the filetype and move it into there
into_subfolder = {"vp": ext} into_subfolder = {"vp": ext}
@ -95,8 +92,8 @@ def main():
by_category = {} # no action by_category = {} # no action
# now choose the default effect to apply; can be any of these: # now choose the default effect to apply; can be any of these:
# into_junk into_subfolder into_toplevel into_sibling by_category # into_subfolder into_toplevel into_sibling by_category
effect = into_sibling effect = {"vp": "/junk"}
## ##
## but we can keep going, adding more speicifc rules ## but we can keep going, adding more speicifc rules

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

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

@ -22,8 +22,6 @@ set -e
# modifies the keyfinder python lib to load the .so in ~/pe # modifies the keyfinder python lib to load the .so in ~/pe
export FORCE_COLOR=1
linux=1 linux=1
win= win=
@ -189,14 +187,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

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
S_VERSION = "2.11" S_VERSION = "2.8"
S_BUILD_DT = "2025-05-18" S_BUILD_DT = "2025-01-21"
""" """
u2c.py: upload to copyparty u2c.py: upload to copyparty
@ -52,7 +52,6 @@ if PY2:
sys.dont_write_bytecode = True sys.dont_write_bytecode = True
bytes = str bytes = str
files_decoder = lambda s: unicode(s, "utf8")
else: else:
from urllib.parse import quote_from_bytes as quote from urllib.parse import quote_from_bytes as quote
from urllib.parse import unquote_to_bytes as unquote from urllib.parse import unquote_to_bytes as unquote
@ -62,7 +61,6 @@ else:
from queue import Queue from queue import Queue
unicode = str unicode = str
files_decoder = unicode
WTF8 = "replace" if PY2 else "surrogateescape" WTF8 = "replace" if PY2 else "surrogateescape"
@ -236,10 +234,6 @@ CLEN = "Content-Length"
web = None # type: HCli web = None # type: HCli
links = [] # type: list[str]
linkmtx = threading.Lock()
linkfile = None
class File(object): class File(object):
"""an up2k upload task; represents a single file""" """an up2k upload task; represents a single file"""
@ -767,29 +761,6 @@ def get_hashlist(file, pcb, mth):
file.kchunks[k] = [v1, v2] file.kchunks[k] = [v1, v2]
def printlink(ar, purl, name, fk):
if not name:
url = purl # srch
else:
name = quotep(name.encode("utf-8", WTF8)).decode("utf-8")
if fk:
url = "%s%s?k=%s" % (purl, name, fk)
else:
url = "%s%s" % (purl, name)
url = "%s/%s" % (ar.burl, url.lstrip("/"))
with linkmtx:
if ar.u:
links.append(url)
if ar.ud:
print(url)
if linkfile:
zs = "%s\n" % (url,)
zb = zs.encode("utf-8", "replace")
linkfile.write(zb)
def handshake(ar, file, search): def handshake(ar, file, search):
# type: (argparse.Namespace, File, bool) -> tuple[list[str], bool] # type: (argparse.Namespace, File, bool) -> tuple[list[str], bool]
""" """
@ -809,9 +780,7 @@ def handshake(ar, file, search):
else: else:
if ar.touch: if ar.touch:
req["umod"] = True req["umod"] = True
if ar.owo: if ar.ow:
req["replace"] = "mt"
elif ar.ow:
req["replace"] = True req["replace"] = True
file.recheck = False file.recheck = False
@ -863,17 +832,12 @@ def handshake(ar, file, search):
raise Exception(txt) raise Exception(txt)
if search: if search:
if ar.uon and r["hits"]:
printlink(ar, r["hits"][0]["rp"], "", "")
return r["hits"], False return r["hits"], False
file.url = quotep(r["purl"].encode("utf-8", WTF8)).decode("utf-8") file.url = quotep(r["purl"].encode("utf-8", WTF8)).decode("utf-8")
file.name = r["name"] file.name = r["name"]
file.wark = r["wark"] file.wark = r["wark"]
if ar.uon and not r["hash"]:
printlink(ar, file.url, r["name"], r.get("fk"))
return r["hash"], r["sprs"] return r["hash"], r["sprs"]
@ -1291,7 +1255,7 @@ class Ctl(object):
if self.ar.jw: if self.ar.jw:
print("%s %s" % (wark, vp)) print("%s %s" % (wark, vp))
else: else:
zd = datetime.datetime.fromtimestamp(max(0, file.lmod), UTC) zd = datetime.datetime.fromtimestamp(file.lmod, UTC)
dt = "%04d-%02d-%02d %02d:%02d:%02d" % ( dt = "%04d-%02d-%02d %02d:%02d:%02d" % (
zd.year, zd.year,
zd.month, zd.month,
@ -1508,7 +1472,7 @@ class APF(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFor
def main(): def main():
global web, linkfile global web
time.strptime("19970815", "%Y%m%d") # python#7980 time.strptime("19970815", "%Y%m%d") # python#7980
"".encode("idna") # python#29288 "".encode("idna") # python#29288
@ -1534,7 +1498,7 @@ source file/folder selection uses rsync syntax, meaning that:
""") """)
ap.add_argument("url", type=unicode, help="server url, including destination folder") ap.add_argument("url", type=unicode, help="server url, including destination folder")
ap.add_argument("files", type=files_decoder, nargs="+", help="files and/or folders to process") ap.add_argument("files", type=unicode, nargs="+", help="files and/or folders to process")
ap.add_argument("-v", action="store_true", help="verbose") ap.add_argument("-v", action="store_true", help="verbose")
ap.add_argument("-a", metavar="PASSWD", help="password or $filepath") ap.add_argument("-a", metavar="PASSWD", help="password or $filepath")
ap.add_argument("-s", action="store_true", help="file-search (disables upload)") ap.add_argument("-s", action="store_true", help="file-search (disables upload)")
@ -1542,15 +1506,9 @@ source file/folder selection uses rsync syntax, meaning that:
ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible") ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible")
ap.add_argument("--touch", action="store_true", help="if last-modified timestamps differ, push local to server (need write+delete perms)") ap.add_argument("--touch", action="store_true", help="if last-modified timestamps differ, push local to server (need write+delete perms)")
ap.add_argument("--ow", action="store_true", help="overwrite existing files instead of autorenaming") ap.add_argument("--ow", action="store_true", help="overwrite existing files instead of autorenaming")
ap.add_argument("--owo", action="store_true", help="overwrite existing files if server-file is older")
ap.add_argument("--spd", action="store_true", help="print speeds for each file") ap.add_argument("--spd", action="store_true", help="print speeds for each file")
ap.add_argument("--version", action="store_true", help="show version and exit") ap.add_argument("--version", action="store_true", help="show version and exit")
ap = app.add_argument_group("print links")
ap.add_argument("-u", action="store_true", help="print list of download-links after all uploads finished")
ap.add_argument("-ud", action="store_true", help="print download-link after each upload finishes")
ap.add_argument("-uf", type=unicode, metavar="PATH", help="print list of download-links to file")
ap = app.add_argument_group("compatibility") ap = app.add_argument_group("compatibility")
ap.add_argument("--cls", action="store_true", help="clear screen before start") ap.add_argument("--cls", action="store_true", help="clear screen before start")
ap.add_argument("--rh", type=int, metavar="TRIES", default=0, help="resolve server hostname before upload (good for buggy networks, but TLS certs will break)") ap.add_argument("--rh", type=int, metavar="TRIES", default=0, help="resolve server hostname before upload (good for buggy networks, but TLS certs will break)")
@ -1636,10 +1594,6 @@ source file/folder selection uses rsync syntax, meaning that:
ar.x = "|".join(ar.x or []) ar.x = "|".join(ar.x or [])
setattr(ar, "wlist", ar.url == "-") setattr(ar, "wlist", ar.url == "-")
setattr(ar, "uon", ar.u or ar.ud or ar.uf)
if ar.uf:
linkfile = open(ar.uf, "wb")
for k in "dl dr drd wlist".split(): for k in "dl dr drd wlist".split():
errs = [] errs = []
@ -1702,12 +1656,6 @@ source file/folder selection uses rsync syntax, meaning that:
ar.z = True ar.z = True
ctl = Ctl(ar, ctl.stats) ctl = Ctl(ar, ctl.stats)
if links:
print()
print("\n".join(links))
if linkfile:
linkfile.close()
if ctl.errs: if ctl.errs:
print("WARNING: %d errors" % (ctl.errs)) print("WARNING: %d errors" % (ctl.errs))

View file

@ -23,9 +23,6 @@ run this script with "pull" and run copyparty with this:
run this script with "rep" and run copyparty with this: run this script with "rep" and run copyparty with this:
--xm t3,zmq:req:tcp://localhost:5555 --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
""" """
@ -59,9 +56,7 @@ def rep_server():
sck.bind("tcp://*:5555") sck.bind("tcp://*:5555")
while True: while True:
print("copyparty says %r" % (sck.recv_string(),)) print("copyparty says %r" % (sck.recv_string(),))
reply = b"thx" sck.send(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 "" mode = sys.argv[1].lower() if len(sys.argv) > 1 else ""

View file

@ -50,9 +50,6 @@
* give a 3rd argument to install it to your copyparty config * give a 3rd argument to install it to your copyparty config
* systemd service at [`systemd/cfssl.service`](systemd/cfssl.service) * systemd service at [`systemd/cfssl.service`](systemd/cfssl.service)
### [`zfs-tune.py`](zfs-tune.py)
* optimizes databases for optimal performance when stored on a zfs filesystem; also see [openzfs docs](https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Workload%20Tuning.html#database-workloads) and specifically the SQLite subsection
# OS integration # OS integration
init-scripts to start copyparty as a service init-scripts to start copyparty as a service
* [`systemd/copyparty.service`](systemd/copyparty.service) runs the sfx normally * [`systemd/copyparty.service`](systemd/copyparty.service) runs the sfx normally

View file

@ -2,38 +2,19 @@
# not accept more consecutive clients than what copyparty is able to; # not accept more consecutive clients than what copyparty is able to;
# nginx default is 512 (worker_processes 1, worker_connections 512) # nginx default is 512 (worker_processes 1, worker_connections 512)
# #
# ======================================================================
#
# to reverse-proxy a specific path/subpath/location below a domain
# (rather than a complete subdomain), for example "/qw/er", you must
# run copyparty with --rp-loc /qw/as and also change the following:
# location / {
# proxy_pass http://cpp_tcp;
# to this:
# location /qw/er/ {
# proxy_pass http://cpp_tcp/qw/er/;
#
# ======================================================================
#
# rarely, in some extreme usecases, it can be good to add -j0 # rarely, in some extreme usecases, it can be good to add -j0
# (40'000 requests per second, or 20gbps upload/download in parallel) # (40'000 requests per second, or 20gbps upload/download in parallel)
# but this is usually counterproductive and slightly buggy # but this is usually counterproductive and slightly buggy
# #
# ======================================================================
#
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1 # on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
# #
# ====================================================================== # if you are behind cloudflare (or another protection service),
#
# if you are behind cloudflare (or another CDN/WAF/protection service),
# remember to reject all connections which are not coming from your # remember to reject all connections which are not coming from your
# protection service -- for cloudflare in particular, you can # protection service -- for cloudflare in particular, you can
# generate the list of permitted IP ranges like so: # generate the list of permitted IP ranges like so:
# (curl -s https://www.cloudflare.com/ips-v{4,6} | sed 's/^/allow /; s/$/;/'; echo; echo "deny all;") > /etc/nginx/cloudflare-only.conf # (curl -s https://www.cloudflare.com/ips-v{4,6} | sed 's/^/allow /; s/$/;/'; echo; echo "deny all;") > /etc/nginx/cloudflare-only.conf
# #
# and then enable it below by uncomenting the cloudflare-only.conf line # and then enable it below by uncomenting the cloudflare-only.conf line
#
# ======================================================================
upstream cpp_tcp { upstream cpp_tcp {
@ -85,13 +66,13 @@ server {
proxy_buffer_size 16k; proxy_buffer_size 16k;
proxy_busy_buffers_size 24k; proxy_busy_buffers_size 24k;
proxy_set_header Connection "Keep-Alive";
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# NOTE: with cloudflare you want this X-Forwarded-For instead: # NOTE: with cloudflare you want this instead:
#proxy_set_header X-Forwarded-For $http_cf_connecting_ip; #proxy_set_header X-Forwarded-For $http_cf_connecting_ip;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "Keep-Alive";
} }
} }

View file

@ -1,13 +1,9 @@
{ { 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
@ -19,8 +15,7 @@ let
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
@ -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,28 +79,22 @@ 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 = { options = {
passwordFile = mkOption { passwordFile = mkOption {
type = types.str; type = types.str;
@ -146,9 +105,7 @@ in
example = "/run/keys/copyparty/ed"; example = "/run/keys/copyparty/ed";
}; };
}; };
} }));
)
);
description = '' description = ''
A set of copyparty accounts to create. A set of copyparty accounts to create.
''; '';
@ -161,13 +118,10 @@ in
}; };
volumes = mkOption { volumes = mkOption {
type = types.attrsOf ( type = types.attrsOf (types.submodule ({ ... }: {
types.submodule (
{ ... }:
{
options = { options = {
path = mkOption { path = mkOption {
type = types.path; type = types.str;
description = '' description = ''
Path of a directory to share. Path of a directory to share.
''; '';
@ -226,16 +180,12 @@ in
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,63 +204,52 @@ in
}; };
}; };
config = mkIf cfg.enable ( config = mkIf cfg.enable {
let
command = "${getExe cfg.package} -c ${runtimeConfigPath}";
in
{
systemd.services.copyparty = { systemd.services.copyparty = {
description = "http file sharing hub"; description = "http file sharing hub";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
environment = { environment = {
PYTHONUNBUFFERED = "true"; PYTHONUNBUFFERED = "true";
XDG_CONFIG_HOME = externalStateDir; XDG_CONFIG_HOME = "${home}/.config";
}; };
preStart = preStart = let
let replaceSecretCommand = name: attrs:
replaceSecretCommand = "${getExe pkgs.replace-secret} '${
name: attrs: passwordPlaceholder name
"${getExe pkgs.replace-secret} '${passwordPlaceholder name}' '${attrs.passwordFile}' ${runtimeConfigPath}"; }' '${attrs.passwordFile}' ${runtimeConfigPath}";
in in ''
''
set -euo pipefail set -euo pipefail
install -m 600 ${configFile} ${runtimeConfigPath} install -m 600 ${configFile} ${runtimeConfigPath}
${concatStringsSep "\n" (mapAttrsToList replaceSecretCommand cfg.accounts)} ${concatStringsSep "\n"
(mapAttrsToList replaceSecretCommand cfg.accounts)}
''; '';
serviceConfig = { serviceConfig = {
Type = "simple"; Type = "simple";
ExecStart = command; ExecStart = "${getExe cfg.package} -c ${runtimeConfigPath}";
# Hardening options # Hardening options
User = cfg.user; User = "copyparty";
Group = cfg.group; Group = "copyparty";
RuntimeDirectory = [ "copyparty" ]; RuntimeDirectory = name;
RuntimeDirectoryMode = "0700"; RuntimeDirectoryMode = "0700";
StateDirectory = [ "copyparty" ]; StateDirectory = [ name "${name}/data" "${name}/.config" ];
StateDirectoryMode = "0700"; StateDirectoryMode = "0700";
CacheDirectory = lib.mkIf (cfg.settings ? hist) [ "copyparty" ]; WorkingDirectory = home;
CacheDirectoryMode = lib.mkIf (cfg.settings ? hist) "0700"; TemporaryFileSystem = "/:ro";
WorkingDirectory = externalStateDir;
BindReadOnlyPaths = [ BindReadOnlyPaths = [
"/nix/store" "/nix/store"
"-/etc/resolv.conf" "-/etc/resolv.conf"
"-/etc/nsswitch.conf" "-/etc/nsswitch.conf"
"-/etc/group"
"-/etc/hosts" "-/etc/hosts"
"-/etc/localtime" "-/etc/localtime"
] ++ (mapAttrsToList (k: v: "-${v.passwordFile}") cfg.accounts); ] ++ (mapAttrsToList (k: v: "-${v.passwordFile}") cfg.accounts);
BindPaths = BindPaths = [ home ] ++ (mapAttrsToList (k: v: v.path) cfg.volumes);
(if cfg.settings ? hist then [ cfg.settings.hist ] else [ ]) # Would re-mount paths ignored by temporary root
++ [ externalStateDir ]
++ (mapAttrsToList (k: v: v.path) cfg.volumes);
#ProtectSystem = "strict"; #ProtectSystem = "strict";
# Note that unlike what 'ro' implies, ProtectHome = true;
# 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; PrivateTmp = true;
PrivateDevices = true; PrivateDevices = true;
ProtectKernelTunables = true; ProtectKernelTunables = true;
@ -330,46 +269,15 @@ in
NoNewPrivileges = true; NoNewPrivileges = true;
LockPersonality = true; LockPersonality = true;
RestrictRealtime = true; RestrictRealtime = true;
MemoryDenyWriteExecute = true;
}; };
}; };
# ensure volumes exist: users.groups.copyparty = { };
systemd.tmpfiles.settings."copyparty" = ( users.users.copyparty = {
lib.attrsets.mapAttrs' (
name: value:
lib.attrsets.nameValuePair (value.path) {
d = {
#: in front of things means it wont change it if the directory already exists.
group = ":${cfg.group}";
user = ":${cfg.user}";
mode = ":755";
};
}
) cfg.volumes
);
users.groups.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") { };
users.users.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") {
description = "Service user for copyparty"; description = "Service user for copyparty";
group = "copyparty"; group = "copyparty";
home = externalStateDir; home = home;
isSystemUser = true; isSystemUser = true;
}; };
environment.systemPackages = lib.mkIf cfg.mkHashWrapper [ };
(pkgs.writeShellScriptBin "copyparty-hash" ''
set -a # automatically export variables
# set same environment variables as the systemd service
${lib.pipe config.systemd.services.copyparty.environment [
(lib.filterAttrs (n: v: v != null && n != "PATH"))
(lib.mapAttrs (_: v: "${v}"))
(lib.toShellVars)
]}
PATH=${config.systemd.services.copyparty.environment.PATH}:$PATH
exec ${command} --ah-cli
'')
];
}
);
} }

View file

@ -1,48 +1,57 @@
# Maintainer: icxes <dev.null@need.moe> # Maintainer: icxes <dev.null@need.moe>
# Contributor: Morgan Adamiec <morganamilo@archlinux.org>
# NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead.
pkgname=copyparty pkgname=copyparty
pkgver="1.19.4" pkgver="1.16.8"
pkgrel=1 pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any") arch=("any")
url="https://github.com/9001/${pkgname}" url="https://github.com/9001/${pkgname}"
license=('MIT') license=('MIT')
depends=("bash" "python" "lsof" "python-jinja") depends=("python" "lsof" "python-jinja")
makedepends=("python-wheel" "python-setuptools" "python-build" "python-installer" "make" "pigz") makedepends=("python-wheel" "python-setuptools" "python-build" "python-installer" "make" "pigz")
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags" optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
"cfssl: generate TLS certificates on startup" "cfssl: generate TLS certificates on startup (pointless when reverse-proxied)"
"python-mutagen: music tags (alternative)" "python-mutagen: music tags (alternative)"
"python-pillow: thumbnails for images" "python-pillow: thumbnails for images"
"python-pyvips: thumbnails for images (higher quality, faster, uses more ram)" "python-pyvips: thumbnails for images (higher quality, faster, uses more ram)"
"libkeyfinder: detection of musical keys" "libkeyfinder-git: detection of musical keys"
"qm-vamp-plugins: BPM detection"
"python-pyopenssl: ftps functionality" "python-pyopenssl: ftps functionality"
"python-pyzmq: send zeromq messages from event-hooks" "python-pyzmq: send zeromq messages from event-hooks"
"python-argon2-cffi: hashed passwords in config" "python-argon2-cffi: hashed passwords in config"
"python-impacket-git: smb support (bad idea)"
) )
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("etc/${pkgname}/copyparty.conf" ) backup=("etc/${pkgname}.d/init" )
sha256sums=("b0e84a78eb2701cb7447b6023afcec280c550617dde67b6f0285bb23483111eb") sha256sums=("37598eb1712fd9bb53aa2f564cbb62a559a32a4f1751a8fbe782af26205d87d2")
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,44 +0,0 @@
# Contributor: Beethoven <beethovenisadog@protonmail.com>
pkgname=copyparty
pkgver=1.19.4
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any")
url="https://github.com/9001/${pkgname}"
license=('MIT')
depends=("bash" "python3" "lsof" "python3-jinja2")
makedepends=("python3-wheel" "python3-setuptools" "python3-build" "python3-installer" "make" "pigz")
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
"golang-cfssl: generate TLS certificates on startup"
"python3-mutagen: music tags (alternative)"
"python3-pil: thumbnails for images"
"python3-openssl: ftps functionality"
"python3-zmq: send zeromq messages from event-hooks"
"python3-argon2: hashed passwords in config"
)
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("/etc/${pkgname}.d/init" )
sha256sums=("b0e84a78eb2701cb7447b6023afcec280c550617dde67b6f0285bb23483111eb")
build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
make
cd "${srcdir}/${pkgname}-${pkgver}"
python -m build --wheel --no-isolation
}
package() {
cd "${srcdir}/${pkgname}-${pkgver}"
python -m installer --destdir="$pkgdir" dist/*.whl
install -dm755 "${pkgdir}/etc/${pkgname}.d"
install -Dm755 "bin/prisonparty.sh" "${pkgdir}/usr/bin/prisonparty"
install -Dm644 "contrib/package/makedeb-mpr/${pkgname}.conf" "${pkgdir}/etc/${pkgname}.d/init"
install -Dm644 "contrib/package/makedeb-mpr/${pkgname}.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}.service"
install -Dm644 "contrib/package/makedeb-mpr/prisonparty.service" "${pkgdir}/usr/lib/systemd/system/prisonparty.service"
install -Dm644 "contrib/package/makedeb-mpr/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md"
install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
}

View file

@ -1,24 +1,4 @@
{ { lib, stdenv, makeWrapper, fetchurl, utillinux, python, jinja2, impacket, pyftpdlib, pyopenssl, argon2-cffi, pillow, pyvips, pyzmq, ffmpeg, mutagen,
lib,
buildPythonApplication,
fetchurl,
util-linux,
python,
setuptools,
jinja2,
impacket,
pyopenssl,
cfssl,
argon2-cffi,
pillow,
pyvips,
pyzmq,
ffmpeg,
mutagen,
pyftpdlib,
magic,
partftpy,
fusepy, # for partyfuse
# use argon2id-hashed passwords in config files (sha2 is always available) # use argon2id-hashed passwords in config files (sha2 is always available)
withHashedPasswords ? true, withHashedPasswords ? true,
@ -44,50 +24,22 @@
# send ZeroMQ messages from event-hooks # send ZeroMQ messages from event-hooks
withZeroMQ ? true, withZeroMQ ? true,
# enable FTP server
withFTP ? true,
# enable FTPS support in the FTP server # enable FTPS support in the FTP server
withFTPS ? false, withFTPS ? false,
# enable TFTP server
withTFTP ? false,
# samba/cifs server; dangerous and buggy, enable if you really need it # samba/cifs server; dangerous and buggy, enable if you really need it
withSMB ? false, withSMB ? false,
# enables filetype detection for nameless uploads
withMagic ? false,
# extra packages to add to the PATH
extraPackages ? [ ],
# function that accepts a python packageset and returns a list of packages to
# be added to the python venv. useful for scripts and such that require
# additional dependencies
extraPythonPackages ? (_p: [ ]),
}: }:
let let
pinData = lib.importJSON ./pin.json; pinData = lib.importJSON ./pin.json;
runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg); pyEnv = python.withPackages (ps:
in with ps; [
buildPythonApplication {
pname = "copyparty";
inherit (pinData) version;
src = fetchurl {
inherit (pinData) url hash;
};
dependencies =
[
jinja2 jinja2
fusepy
] ]
++ lib.optional withSMB impacket ++ lib.optional withSMB impacket
++ lib.optional withFTP pyftpdlib
++ lib.optional withFTPS pyopenssl ++ lib.optional withFTPS pyopenssl
++ lib.optional withTFTP partftpy
++ lib.optional withCertgen cfssl ++ lib.optional withCertgen cfssl
++ lib.optional withThumbnails pillow ++ lib.optional withThumbnails pillow
++ lib.optional withFastThumbnails pyvips ++ lib.optional withFastThumbnails pyvips
@ -95,24 +47,21 @@ buildPythonApplication {
++ lib.optional withBasicAudioMetadata mutagen ++ lib.optional withBasicAudioMetadata mutagen
++ lib.optional withHashedPasswords argon2-cffi ++ lib.optional withHashedPasswords argon2-cffi
++ lib.optional withZeroMQ pyzmq ++ lib.optional withZeroMQ pyzmq
++ lib.optional withMagic magic );
++ (extraPythonPackages python.pkgs); in stdenv.mkDerivation {
makeWrapperArgs = [ "--prefix PATH : ${lib.makeBinPath runtimeDeps}" ]; pname = "copyparty";
version = pinData.version;
pyproject = true; src = fetchurl {
build-system = [ url = pinData.url;
setuptools hash = pinData.hash;
];
meta = {
description = "Turn almost any device into a file server";
longDescription = ''
Portable file server with accelerated resumable uploads, dedup, WebDAV,
FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps
'';
homepage = "https://github.com/9001/copyparty";
changelog = "https://github.com/9001/copyparty/releases/tag/v${pinData.version}";
license = lib.licenses.mit;
mainProgram = "copyparty";
sourceProvenance = [ lib.sourceTypes.fromSource ];
}; };
buildInputs = [ makeWrapper ];
dontUnpack = true;
dontBuild = true;
installPhase = ''
install -Dm755 $src $out/share/copyparty-sfx.py
makeWrapper ${pyEnv.interpreter} $out/bin/copyparty \
--set PATH '${lib.makeBinPath ([ utillinux ] ++ lib.optional withMediaProcessing ffmpeg)}:$PATH' \
--add-flags "$out/share/copyparty-sfx.py"
'';
} }

View file

@ -1,5 +1,5 @@
{ {
"url": "https://github.com/9001/copyparty/releases/download/v1.19.4/copyparty-1.19.4.tar.gz", "url": "https://github.com/9001/copyparty/releases/download/v1.16.8/copyparty-sfx.py",
"version": "1.19.4", "version": "1.16.8",
"hash": "sha256-sOhKeOsnAct0R7YCOvzsKAxVBhfd5ntvAoW7I0gxEes=" "hash": "sha256-WhFkvXaowXHSihzKkiQaIjKKPqeeb/murXRIg9WoGRI="
} }

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,7 +15,6 @@ save one of these as `.epilogue.html` inside a folder to customize it:
point `--js-browser` to one of these by URL: point `--js-browser` to one of these by URL:
* [`minimal-up2k.js`](minimal-up2k.js) is similar to the above `minimal-up2k.html` except it applies globally to all write-only folders * [`minimal-up2k.js`](minimal-up2k.js) is similar to the above `minimal-up2k.html` except it applies globally to all write-only folders
* [`quickmove.js`](quickmove.js) adds a hotkey to move selected files into a subfolder
* [`up2k-hooks.js`](up2k-hooks.js) lets you specify a ruleset for files to skip uploading * [`up2k-hooks.js`](up2k-hooks.js) lets you specify a ruleset for files to skip uploading
* [`up2k-hook-ytid.js`](up2k-hook-ytid.js) is a more specific example checking youtube-IDs against some API * [`up2k-hook-ytid.js`](up2k-hook-ytid.js) is a more specific example checking youtube-IDs against some API

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

@ -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 +1,42 @@
# not actually YAML but lets pretend:
# -*- mode: yaml -*-
# vim: ft=yaml:
# put this file in /etc/
[global] [global]
i: 127.0.0.1 e2dsa # enable file indexing and filesystem scanning
e2ts # and enable multimedia indexing
ansi # and colors in log messages
# disable logging to stdout/journalctl and log to a file instead;
# $LOGS_DIRECTORY is usually /var/log/copyparty (comes from systemd)
# and copyparty replaces %Y-%m%d with Year-MonthDay, so the
# full path will be something like /var/log/copyparty/2023-1130.txt
# (note: enable compression by adding .xz at the end)
q, lo: $LOGS_DIRECTORY/%Y-%m%d.log
# p: 80,443,3923 # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE)
# i: 127.0.0.1 # only allow connections from localhost (reverse-proxies)
# ftp: 3921 # enable ftp server on port 3921
# p: 3939 # listen on another port
# df: 16 # stop accepting uploads if less than 16 GB free disk space
# ver # show copyparty version in the controlpanel
# grid # show thumbnails/grid-view by default
# theme: 2 # monokai
# name: datasaver # change the server-name that's displayed in the browser
# stats, nos-dup # enable the prometheus endpoint, but disable the dupes counter (too slow)
# no-robots, force-js # make it harder for search engines to read your server
[accounts] [accounts]
user: password ed: wark # username: password
[/]
/var/lib/copyparty-jail [/] # create a volume at "/" (the webroot), which will
/mnt # share the contents of the "/mnt" folder
accs: accs:
r: * rw: * # everyone gets read-write access, but
rwdma: user rwmda: ed # the user "ed" gets read-write-move-delete-admin
flags:
grid

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,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,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,18 +1,5 @@
# ./traefik --configFile=copyparty.yaml # ./traefik --experimental.fastproxy=true --entrypoints.web.address=:8080 --providers.file.filename=copyparty.yaml
entryPoints:
web:
address: :8080
transport:
# don't disconnect during big uploads
respondingTimeouts:
readTimeout: "0s"
log:
level: DEBUG
providers:
file:
# WARNING: must be same filename as current file
filename: "copyparty.yaml"
http: http:
services: services:
service-cpp: service-cpp:

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

@ -63,6 +63,10 @@ web/browser.js
web/browser2.html web/browser2.html
web/cf.html web/cf.html
web/copyparty.gif web/copyparty.gif
web/dd/2.png
web/dd/3.png
web/dd/4.png
web/dd/5.png
web/deps/busy.mp3 web/deps/busy.mp3
web/deps/easymde.css web/deps/easymde.css
web/deps/easymde.js web/deps/easymde.js
@ -76,7 +80,6 @@ web/deps/prismd.css
web/deps/scp.woff2 web/deps/scp.woff2
web/deps/sha512.ac.js web/deps/sha512.ac.js
web/deps/sha512.hw.js web/deps/sha512.hw.js
web/idp.html
web/iiam.gif web/iiam.gif
web/md.css web/md.css
web/md.html web/md.html

View file

@ -40,7 +40,6 @@ from .cfg import flagcats, onedash
from .svchub import SvcHub from .svchub import SvcHub
from .util import ( from .util import (
APPLESAN_TXT, APPLESAN_TXT,
BAD_BOTS,
DEF_EXP, DEF_EXP,
DEF_MTE, DEF_MTE,
DEF_MTH, DEF_MTH,
@ -53,20 +52,19 @@ from .util import (
PYFTPD_VER, PYFTPD_VER,
RAM_AVAIL, RAM_AVAIL,
RAM_TOTAL, RAM_TOTAL,
RE_ANSI,
SQLITE_VER, SQLITE_VER,
UNPLICATIONS, UNPLICATIONS,
URL_BUG, URL_BUG,
URL_PRJ, URL_PRJ,
Daemon, Daemon,
align_tab, align_tab,
ansi_re,
b64enc, b64enc,
dedent, dedent,
has_resource, has_resource,
load_resource, load_resource,
min_ex, min_ex,
pybin, pybin,
read_utf8,
termsize, termsize,
wrap, wrap,
) )
@ -93,10 +91,6 @@ u = unicode
printed: list[str] = [] printed: list[str] = []
zsid = uuid.uuid4().urn[4:] zsid = uuid.uuid4().urn[4:]
CFG_DEF = [os.environ.get("PRTY_CONFIG", "")]
if not CFG_DEF[0]:
CFG_DEF.pop()
class RiceFormatter(argparse.HelpFormatter): class RiceFormatter(argparse.HelpFormatter):
def __init__(self, *args: Any, **kwargs: Any) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None:
@ -171,7 +165,7 @@ def lprint(*a: Any, **ka: Any) -> None:
txt: str = " ".join(unicode(x) for x in a) + eol txt: str = " ".join(unicode(x) for x in a) + eol
printed.append(txt) printed.append(txt)
if not VT100: if not VT100:
txt = RE_ANSI.sub("", txt) txt = ansi_re.sub("", txt)
print(txt, end="", **ka) print(txt, end="", **ka)
@ -232,23 +226,7 @@ def init_E(EE: EnvParams) -> None:
if E.mod.endswith("__init__"): if E.mod.endswith("__init__"):
E.mod = os.path.dirname(E.mod) E.mod = os.path.dirname(E.mod)
try: if sys.platform == "win32":
p = os.environ.get("XDG_CONFIG_HOME")
if not p:
raise Exception()
if p.startswith("~"):
p = os.path.expanduser(p)
p = os.path.abspath(os.path.realpath(p))
p = os.path.join(p, "copyparty")
if not os.path.isdir(p):
os.mkdir(p)
os.listdir(p)
except:
p = ""
if p:
E.cfg = p
elif sys.platform == "win32":
bdir = os.environ.get("APPDATA") or os.environ.get("TEMP") or "." bdir = os.environ.get("APPDATA") or os.environ.get("TEMP") or "."
E.cfg = os.path.normpath(bdir + "/copyparty") E.cfg = os.path.normpath(bdir + "/copyparty")
elif sys.platform == "darwin": elif sys.platform == "darwin":
@ -277,7 +255,8 @@ def get_srvname(verbose) -> str:
if verbose: if verbose:
lprint("using hostname from {}\n".format(fp)) lprint("using hostname from {}\n".format(fp))
try: try:
return read_utf8(None, fp, True).strip() with open(fp, "rb") as f:
ret = f.read().decode("utf-8", "replace").strip()
except: except:
ret = "" ret = ""
namelen = 5 namelen = 5
@ -286,17 +265,46 @@ def get_srvname(verbose) -> str:
ret = re.sub("[234567=]", "", ret)[:namelen] ret = re.sub("[234567=]", "", ret)[:namelen]
with open(fp, "wb") as f: with open(fp, "wb") as f:
f.write(ret.encode("utf-8") + b"\n") f.write(ret.encode("utf-8") + b"\n")
return ret return ret
def get_salt(name: str, nbytes: int) -> str: def get_fk_salt() -> str:
fp = os.path.join(E.cfg, "%s-salt.txt" % (name,)) fp = os.path.join(E.cfg, "fk-salt.txt")
try: try:
return read_utf8(None, fp, True).strip() with open(fp, "rb") as f:
ret = f.read().strip()
except: except:
ret = b64enc(os.urandom(nbytes)) ret = b64enc(os.urandom(18))
with open(fp, "wb") as f: with open(fp, "wb") as f:
f.write(ret + b"\n") f.write(ret + b"\n")
return ret.decode("utf-8")
def get_dk_salt() -> str:
fp = os.path.join(E.cfg, "dk-salt.txt")
try:
with open(fp, "rb") as f:
ret = f.read().strip()
except:
ret = b64enc(os.urandom(30))
with open(fp, "wb") as f:
f.write(ret + b"\n")
return ret.decode("utf-8")
def get_ah_salt() -> str:
fp = os.path.join(E.cfg, "ah-salt.txt")
try:
with open(fp, "rb") as f:
ret = f.read().strip()
except:
ret = b64enc(os.urandom(18))
with open(fp, "wb") as f:
f.write(ret + b"\n")
return ret.decode("utf-8") return ret.decode("utf-8")
@ -436,29 +444,6 @@ def args_from_cfg(cfg_path: str) -> list[str]:
return ret return ret
def expand_cfg(argv) -> list[str]:
if CFG_DEF:
supp = args_from_cfg(CFG_DEF[0])
argv = argv[:1] + supp + argv[1:]
n = 0
while n < len(argv):
v1 = argv[n]
v1v = v1[2:].lstrip("=")
try:
v2 = argv[n + 1]
except:
v2 = ""
n += 1
if v1 == "-c" and v2 and os.path.isfile(v2):
n += 1
argv = argv[:n] + args_from_cfg(v2) + argv[n:]
elif v1.startswith("-c") and v1v and os.path.isfile(v1v):
argv = argv[:n] + args_from_cfg(v1v) + argv[n:]
return argv
def sighandler(sig: Optional[int] = None, frame: Optional[FrameType] = None) -> None: def sighandler(sig: Optional[int] = None, frame: Optional[FrameType] = None) -> None:
msg = [""] * 5 msg = [""] * 5
for th in threading.enumerate(): for th in threading.enumerate():
@ -559,7 +544,7 @@ def get_sects():
dedent( dedent(
""" """
\033[33m-i\033[0m takes a comma-separated list of interfaces to listen on; \033[33m-i\033[0m takes a comma-separated list of interfaces to listen on;
IP-addresses, unix-sockets, and/or open file descriptors IP-addresses and/or unix-sockets (Unix Domain Sockets)
the default (\033[32m-i ::\033[0m) means all IPv4 and IPv6 addresses the default (\033[32m-i ::\033[0m) means all IPv4 and IPv6 addresses
@ -574,20 +559,17 @@ def get_sects():
when running behind a reverse-proxy, it's recommended to when running behind a reverse-proxy, it's recommended to
use unix-sockets for improved performance and security; use unix-sockets for improved performance and security;
\033[32m-i unix:770:www:\033[33m/dev/shm/party.sock\033[0m listens on \033[32m-i unix:770:www:\033[33m/tmp/a.sock\033[0m listens on \033[33m/tmp/a.sock\033[0m with
\033[33m/dev/shm/party.sock\033[0m with permissions \033[33m0770\033[0m; permissions \033[33m0770\033[0m; only accessible to members of the \033[33mwww\033[0m
only accessible to members of the \033[33mwww\033[0m group. group. This is the best approach. Alternatively,
This is the best approach. Alternatively,
\033[32m-i unix:777:\033[33m/dev/shm/party.sock\033[0m sets perms \033[33m0777\033[0m so anyone \033[32m-i unix:777:\033[33m/tmp/a.sock\033[0m sets perms \033[33m0777\033[0m so anyone can
can access it; bad unless it's inside a restricted folder access it; bad unless it's inside a restricted folder
\033[32m-i unix:\033[33m/dev/shm/party.sock\033[0m keeps umask-defined permission \033[32m-i unix:\033[33m/tmp/a.sock\033[0m keeps umask-defined permissions
(usually \033[33m0600\033[0m) and the same user/group as copyparty (usually \033[33m0600\033[0m) and the same user/group as copyparty
\033[32m-i fd:\033[33m3\033[0m uses the socket passed to copyparty on file descriptor 3 \033[33m-p\033[0m (tcp ports) is ignored for unix sockets
\033[33m-p\033[0m (tcp ports) is ignored for unix-sockets and FDs
""" """
), ),
], ],
@ -603,7 +585,7 @@ def get_sects():
--grp takes groupname:username1,username2,... --grp takes groupname:username1,username2,...
and groupnames can be used instead of usernames in -v and groupnames can be used instead of usernames in -v
by prefixing the groupname with @ by prefixing the groupname with %
list of permissions: list of permissions:
"r" (read): list folder contents, download files "r" (read): list folder contents, download files
@ -632,41 +614,8 @@ def get_sects():
if no accounts or volumes are configured, if no accounts or volumes are configured,
current folder will be read/write for everyone current folder will be read/write for everyone
the group @acct will always have every user with an account
(the name of that group can be changed with --grp-all)
consider the config file for more flexible account/volume management, consider the config file for more flexible account/volume management,
including dynamic reload at runtime (and being more readable w) including dynamic reload at runtime (and being more readable w)
see \033[32m--help-auth\033[0m for ways to provide the password in requests;
see \033[32m--help-idp\033[0m for replacing it with SSO and auth-middlewares
"""
),
],
[
"auth",
"how to login from a client",
dedent(
"""
different ways to provide the password so you become authenticated:
login with the ui:
go to \033[36mhttp://127.0.0.1:3923/?h\033[0m and login there
send the password in the '\033[36mPW\033[0m' http-header:
\033[36mPW: \033[35mhunter2\033[0m
or if you have \033[33m--accounts\033[0m enabled,
\033[36mPW: \033[35med:hunter2\033[0m
send the password in the URL itself:
\033[36mhttp://127.0.0.1:3923/\033[35m?pw=hunter2\033[0m
or if you have \033[33m--accounts\033[0m enabled,
\033[36mhttp://127.0.0.1:3923/\033[35m?pw=ed:hunter2\033[0m
use basic-authentication:
\033[36mhttp://\033[35med:hunter2\033[36m@127.0.0.1:3923/\033[0m
which should be the same as this header:
\033[36mAuthorization: Basic \033[35mZWQ6aHVudGVyMg==\033[0m
""" """
), ),
], ],
@ -818,36 +767,6 @@ def get_sects():
the upload speed can easily drop to 10% for small files)""" the upload speed can easily drop to 10% for small files)"""
), ),
], ],
[
"idp",
"replacing the login system with fancy middleware",
dedent(
"""
if you already have a centralized service which handles
user-authentication for other services already, you can
integrate copyparty with that for automatic login
if the middleware is providing the username in an http-header
named '\033[35mtheUsername\033[0m' then do this: \033[36m--idp-h-usr theUsername\033[0m
if the middleware is providing a list of groups in the header
named '\033[35mtheGroups\033[0m' then do this: \033[36m--idp-h-grp theGroup\033[0m
if the list of groups is separated by '\033[35m%\033[0m' then \033[36m--idp-gsep %\033[0m
if the middleware is providing a header named '\033[35mAccount\033[0m'
and the value is '\033[35malice@forest.net\033[0m' but the username is
actually '\033[35mmarisa\033[0m' then do this for each user:
\033[36m--idp-hm-usr ^Account^alice@forest.net^marisa\033[0m
(the separator '\033[35m^\033[0m' can be any character)
make ABSOLUTELY SURE that the header can only be set by your
middleware and not by clients! and, as an extra precaution,
send a header named '\033[36mfinalmasterspark\033[0m' (a secret keyword)
and then \033[36m--idp-h-key finalmasterspark\033[0m to require that
"""
),
],
[ [
"urlform", "urlform",
"how to handle url-form POSTs", "how to handle url-form POSTs",
@ -856,22 +775,11 @@ def get_sects():
values for --urlform: values for --urlform:
\033[36mstash\033[35m dumps the data to file and returns length + checksum \033[36mstash\033[35m dumps the data to file and returns length + checksum
\033[36msave,get\033[35m dumps to file and returns the page like a GET \033[36msave,get\033[35m dumps to file and returns the page like a GET
\033[36mprint \033[35m prints the data to log and returns an error \033[36mprint,get\033[35m prints the data in the log and returns GET
\033[36mprint,xm \033[35m prints the data to log and returns --xm output (leave out the ",get" to return an error instead)\033[0m
\033[36mprint,get\033[35m prints the data to log and returns GET\033[0m
note that the \033[35m--xm\033[0m hook will only run if \033[35m--urlform\033[0m is note that the \033[35m--xm\033[0m hook will only run if \033[35m--urlform\033[0m
either \033[36mprint\033[0m or \033[36mprint,get\033[0m or the default \033[36mprint,xm\033[0m is either \033[36mprint\033[0m or the default \033[36mprint,get\033[0m
if an \033[35m--xm\033[0m hook returns text, then
the response code will be HTTP 202;
http/get responses will be HTTP 200
if there are multiple \033[35m--xm\033[0m hooks defined, then
the first hook that produced output is returned
if there are no \033[35m--xm\033[0m hooks defined, then the default
\033[36mprint,xm\033[0m behaves like \033[36mprint,get\033[0m (returning html)
""" """
), ),
], ],
@ -956,43 +864,6 @@ def get_sects():
""" """
), ),
], ],
[
"chmod",
"file/folder permissions",
dedent(
"""
global-option \033[33m--chmod-f\033[0m and volflag \033[33mchmod_f\033[0m specifies the unix-permission to use when creating a new file
similarly, \033[33m--chmod-d\033[0m and \033[33mchmod_d\033[0m sets the directory/folder perm
the value is a three-digit octal number such as \033[32m755\033[0m, \033[32m750\033[0m, \033[32m644\033[0m, etc.
first digit = "User"; permission for the unix-user
second digit = "Group"; permission for the unix-group
third digit = "Other"; permission for all other users/groups
for files:
\033[32m0\033[0m = \033[35m---\033[0m = no access
\033[32m1\033[0m = \033[35m--x\033[0m = can execute the file as a program
\033[32m2\033[0m = \033[35m-w-\033[0m = can write
\033[32m3\033[0m = \033[35m-wx\033[0m = can write and execute
\033[32m4\033[0m = \033[35mr--\033[0m = can read
\033[32m5\033[0m = \033[35mr-x\033[0m = can read and execute
\033[32m6\033[0m = \033[35mrw-\033[0m = can read and write
\033[32m7\033[0m = \033[35mrwx\033[0m = can read, write, execute
for directories/folders:
\033[32m0\033[0m = \033[35m---\033[0m = no access
\033[32m1\033[0m = \033[35m--x\033[0m = can read files in folder but not list contents
\033[32m2\033[0m = \033[35m-w-\033[0m = n/a
\033[32m3\033[0m = \033[35m-wx\033[0m = can create files but not list
\033[32m4\033[0m = \033[35mr--\033[0m = can list, but not read/write
\033[32m5\033[0m = \033[35mr-x\033[0m = can list and read files
\033[32m6\033[0m = \033[35mrw-\033[0m = n/a
\033[32m7\033[0m = \033[35mrwx\033[0m = can read, write, list
"""
),
],
[ [
"pwhash", "pwhash",
"password hashing", "password hashing",
@ -1004,9 +875,6 @@ def get_sects():
copyparty will also hash and print any passwords that are non-hashed copyparty will also hash and print any passwords that are non-hashed
(password which do not start with '+') and then terminate afterwards (password which do not start with '+') and then terminate afterwards
if you have enabled --usernames then the password
must be provided as username:password for hashing
\033[36m--ah-alg\033[0m specifies the hashing algorithm and a \033[36m--ah-alg\033[0m specifies the hashing algorithm and a
list of optional comma-separated arguments: list of optional comma-separated arguments:
@ -1084,36 +952,33 @@ def build_flags_desc():
def add_general(ap, nc, srvname): def add_general(ap, nc, srvname):
ap2 = ap.add_argument_group("general options") ap2 = ap.add_argument_group('general options')
ap2.add_argument("-c", metavar="PATH", type=u, default=CFG_DEF, action="append", help="\033[34mREPEATABLE:\033[0m add config file") ap2.add_argument("-c", metavar="PATH", type=u, action="append", help="add config file")
ap2.add_argument("-nc", metavar="NUM", type=int, default=nc, help="max num clients") ap2.add_argument("-nc", metavar="NUM", type=int, default=nc, help="max num clients")
ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores, 0=all") ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores, 0=all")
ap2.add_argument("-a", metavar="ACCT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add account, \033[33mUSER\033[0m:\033[33mPASS\033[0m; example [\033[32med:wark\033[0m]") ap2.add_argument("-a", metavar="ACCT", type=u, action="append", help="add account, \033[33mUSER\033[0m:\033[33mPASS\033[0m; example [\033[32med:wark\033[0m]")
ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add volume, \033[33mSRC\033[0m:\033[33mDST\033[0m:\033[33mFLAG\033[0m; examples [\033[32m.::r\033[0m], [\033[32m/mnt/nas/music:/music:r:aed\033[0m], see --help-accounts") ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="add volume, \033[33mSRC\033[0m:\033[33mDST\033[0m:\033[33mFLAG\033[0m; examples [\033[32m.::r\033[0m], [\033[32m/mnt/nas/music:/music:r:aed\033[0m], see --help-accounts")
ap2.add_argument("--grp", metavar="G:N,N", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add group, \033[33mNAME\033[0m:\033[33mUSER1\033[0m,\033[33mUSER2\033[0m,\033[33m...\033[0m; example [\033[32madmins:ed,foo,bar\033[0m]") ap2.add_argument("--grp", metavar="G:N,N", type=u, action="append", help="add group, \033[33mNAME\033[0m:\033[33mUSER1\033[0m,\033[33mUSER2\033[0m,\033[33m...\033[0m; example [\033[32madmins:ed,foo,bar\033[0m]")
ap2.add_argument("--usernames", action="store_true", help="require username and password for login; default is just password")
ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files (volflag=dots)") ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files (volflag=dots)")
ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,xm", help="how to handle url-form POSTs; see \033[33m--help-urlform\033[0m") ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,get", help="how to handle url-form POSTs; see \033[33m--help-urlform\033[0m")
ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="server terminal title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]") ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="server terminal title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]")
ap2.add_argument("--name", metavar="TXT", type=u, default=srvname, help="server name (displayed topleft in browser and in mDNS)") ap2.add_argument("--name", metavar="TXT", type=u, default=srvname, help="server name (displayed topleft in browser and in mDNS)")
ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="\033[34mREPEATABLE:\033[0m map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]") ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]")
ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit") ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit")
ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)")
ap2.add_argument("--license", action="store_true", help="show licenses and exit") ap2.add_argument("--license", action="store_true", help="show licenses and exit")
ap2.add_argument("--version", action="store_true", help="show versions and exit") ap2.add_argument("--version", action="store_true", help="show versions and exit")
def add_qr(ap, tty): def add_qr(ap, tty):
ap2 = ap.add_argument_group("qr options") ap2 = ap.add_argument_group('qr options')
ap2.add_argument("--qr", action="store_true", help="show QR-code on startup") ap2.add_argument("--qr", action="store_true", help="show http:// QR-code on startup")
ap2.add_argument("--qrs", action="store_true", help="change the QR-code URL to https://") ap2.add_argument("--qrs", action="store_true", help="show https:// QR-code on startup")
ap2.add_argument("--qrl", metavar="PATH", type=u, default="", help="location to include in the url, for example [\033[32mpriv/?pw=hunter2\033[0m]") ap2.add_argument("--qrl", metavar="PATH", type=u, default="", help="location to include in the url, for example [\033[32mpriv/?pw=hunter2\033[0m]")
ap2.add_argument("--qri", metavar="PREFIX", type=u, default="", help="select IP which starts with \033[33mPREFIX\033[0m; [\033[32m.\033[0m] to force default IP when mDNS URL would have been used instead") ap2.add_argument("--qri", metavar="PREFIX", type=u, default="", help="select IP which starts with \033[33mPREFIX\033[0m; [\033[32m.\033[0m] to force default IP when mDNS URL would have been used instead")
ap2.add_argument("--qr-fg", metavar="COLOR", type=int, default=0 if tty else 16, help="foreground; try [\033[32m0\033[0m] or [\033[32m-1\033[0m] if the qr-code is unreadable") ap2.add_argument("--qr-fg", metavar="COLOR", type=int, default=0 if tty else 16, help="foreground; try [\033[32m0\033[0m] if the qr-code is unreadable")
ap2.add_argument("--qr-bg", metavar="COLOR", type=int, default=229, help="background (white=255)") ap2.add_argument("--qr-bg", metavar="COLOR", type=int, default=229, help="background (white=255)")
ap2.add_argument("--qrp", metavar="CELLS", type=int, default=4, help="padding (spec says 4 or more, but 1 is usually fine)") ap2.add_argument("--qrp", metavar="CELLS", type=int, default=4, help="padding (spec says 4 or more, but 1 is usually fine)")
ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)") ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)")
ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr")
def add_fs(ap): def add_fs(ap):
@ -1127,7 +992,7 @@ def add_fs(ap):
def add_share(ap): def add_share(ap):
db_path = os.path.join(E.cfg, "shares.db") db_path = os.path.join(E.cfg, "shares.db")
ap2 = ap.add_argument_group("share-url options") ap2 = ap.add_argument_group('share-url options')
ap2.add_argument("--shr", metavar="DIR", type=u, default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]") ap2.add_argument("--shr", metavar="DIR", type=u, default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]")
ap2.add_argument("--shr-db", metavar="FILE", type=u, default=db_path, help="database to store shares in") ap2.add_argument("--shr-db", metavar="FILE", type=u, default=db_path, help="database to store shares in")
ap2.add_argument("--shr-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to view/delete any share") ap2.add_argument("--shr-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to view/delete any share")
@ -1136,28 +1001,19 @@ def add_share(ap):
def add_upload(ap): def add_upload(ap):
ap2 = ap.add_argument_group("upload options") ap2 = ap.add_argument_group('upload options')
ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless \033[33m-ed\033[0m") ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless \033[33m-ed\033[0m")
ap2.add_argument("--plain-ip", action="store_true", help="when avoiding filename collisions by appending the uploader's ip to the filename: append the plaintext ip instead of salting and hashing the ip") ap2.add_argument("--plain-ip", action="store_true", help="when avoiding filename collisions by appending the uploader's ip to the filename: append the plaintext ip instead of salting and hashing the ip")
ap2.add_argument("--put-name", metavar="TXT", type=u, default="put-{now.6f}-{cip}.bin", help="filename for nameless uploads (when uploader doesn't provide a name); default is [\033[32mput-UNIXTIME-IP.bin\033[0m] (the \033[32m.6f\033[0m means six decimal places) (volflag=put_name)")
ap2.add_argument("--put-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for PUT/WebDAV uploads: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=put_ck)")
ap2.add_argument("--bup-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for bup/basic-uploader: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=bup_ck)")
ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled, default=12h") ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled, default=12h")
ap2.add_argument("--unp-who", metavar="NUM", type=int, default=1, help="clients can undo recent uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=unp_who)")
ap2.add_argument("--u2abort", metavar="NUM", type=int, default=1, help="clients can abort incomplete uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=u2abort)") ap2.add_argument("--u2abort", metavar="NUM", type=int, default=1, help="clients can abort incomplete uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=u2abort)")
ap2.add_argument("--blank-wt", metavar="SEC", type=int, default=300, help="file write grace period (any client can write to a blank file last-modified more recently than \033[33mSEC\033[0m seconds ago)") ap2.add_argument("--blank-wt", metavar="SEC", type=int, default=300, help="file write grace period (any client can write to a blank file last-modified more recently than \033[33mSEC\033[0m seconds ago)")
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600") ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600")
ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload (bad idea to enable this on windows and/or cow filesystems)") ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload (bad idea to enable this on windows and/or cow filesystems)")
ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even when it might be dangerous (multiprocessing, filesystems lacking sparse-files support, ...)") ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even when it might be dangerous (multiprocessing, filesystems lacking sparse-files support, ...)")
ap2.add_argument("--chmod-f", metavar="UGO", type=u, default="", help="unix file permissions to use when creating files; default is probably 644 (OS-decided), see --help-chmod. Examples: [\033[32m644\033[0m] = owner-RW + all-R, [\033[32m755\033[0m] = owner-RWX + all-RX, [\033[32m777\033[0m] = full-yolo (volflag=chmod_f)")
ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)")
ap2.add_argument("--uid", metavar="N", type=int, default=-1, help="unix user-id to chown new files/folders to; default = -1 = do-not-change (volflag=uid)")
ap2.add_argument("--gid", metavar="N", type=int, default=-1, help="unix group-id to chown new files/folders to; default = -1 = do-not-change (volflag=gid)")
ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)") ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)")
ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)") ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)")
ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)") ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)")
ap2.add_argument("--hardlink-only", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made (volflag=hardlinkonly)") ap2.add_argument("--hardlink-only", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made (volflag=hardlinkonly)")
ap2.add_argument("--reflink", action="store_true", help="enable reflink-based dedup; will fallback on full copies when that is impossible (non-CoW filesystem) (volflag=reflink)")
ap2.add_argument("--no-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)") ap2.add_argument("--no-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)")
ap2.add_argument("--no-clone", action="store_true", help="do not use existing data on disk to satisfy dupe uploads; reduces server HDD reads in exchange for much more network load (volflag=noclone)") ap2.add_argument("--no-clone", action="store_true", help="do not use existing data on disk to satisfy dupe uploads; reduces server HDD reads in exchange for much more network load (volflag=noclone)")
ap2.add_argument("--no-snap", action="store_true", help="disable snapshots -- forget unfinished uploads on shutdown; don't create .hist/up2k.snap files -- abandoned/interrupted uploads must be cleaned up manually") ap2.add_argument("--no-snap", action="store_true", help="disable snapshots -- forget unfinished uploads on shutdown; don't create .hist/up2k.snap files -- abandoned/interrupted uploads must be cleaned up manually")
@ -1170,30 +1026,26 @@ def add_upload(ap):
ap2.add_argument("--df", metavar="GiB", type=u, default="0", help="ensure \033[33mGiB\033[0m free disk space by rejecting upload requests; assumes gigabytes unless a unit suffix is given: [\033[32m256m\033[0m], [\033[32m4\033[0m], [\033[32m2T\033[0m] (volflag=df)") ap2.add_argument("--df", metavar="GiB", type=u, default="0", help="ensure \033[33mGiB\033[0m free disk space by rejecting upload requests; assumes gigabytes unless a unit suffix is given: [\033[32m256m\033[0m], [\033[32m4\033[0m], [\033[32m2T\033[0m] (volflag=df)")
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files") ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; [\033[32m-1\033[0m] = forbidden/always-off, [\033[32m0\033[0m] = default-off and warn if enabled, [\033[32m1\033[0m] = default-off, [\033[32m2\033[0m] = on, [\033[32m3\033[0m] = on and disable datecheck") ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; [\033[32m-1\033[0m] = forbidden/always-off, [\033[32m0\033[0m] = default-off and warn if enabled, [\033[32m1\033[0m] = default-off, [\033[32m2\033[0m] = on, [\033[32m3\033[0m] = on and disable datecheck")
ap2.add_argument("--nosubtle", metavar="N", type=int, default=0, help="when to use a wasm-hasher instead of the browser's builtin; faster on chrome, but buggy in older chrome versions. [\033[32m0\033[0m] = only when necessary (non-https), [\033[32m1\033[0m] = always (all browsers), [\033[32m2\033[0m] = always on chrome/firefox, [\033[32m3\033[0m] = always on chrome, [\033[32mN\033[0m] = chrome-version N and newer (recommendation: 137)") ap2.add_argument("--u2j", metavar="JOBS", type=int, default=2, help="web-client: number of file chunks to upload in parallel; 1 or 2 is good for low-latency (same-country) connections, 4-8 for android clients, 16 for cross-atlantic (max=64)")
ap2.add_argument("--u2j", metavar="JOBS", type=int, default=2, help="web-client: number of file chunks to upload in parallel; 1 or 2 is good when latency is low (same-country), 2~4 for android-clients, 2~6 for cross-atlantic. Max is 6 in most browsers. Big values increase network-speed but may reduce HDD-speed")
ap2.add_argument("--u2sz", metavar="N,N,N", type=u, default="1,64,96", help="web-client: default upload chunksize (MiB); sets \033[33mmin,default,max\033[0m in the settings gui. Each HTTP POST will aim for \033[33mdefault\033[0m, and never exceed \033[33mmax\033[0m. Cloudflare max is 96. Big values are good for cross-atlantic but may increase HDD fragmentation on some FS. Disable this optimization with [\033[32m1,1,1\033[0m]") ap2.add_argument("--u2sz", metavar="N,N,N", type=u, default="1,64,96", help="web-client: default upload chunksize (MiB); sets \033[33mmin,default,max\033[0m in the settings gui. Each HTTP POST will aim for \033[33mdefault\033[0m, and never exceed \033[33mmax\033[0m. Cloudflare max is 96. Big values are good for cross-atlantic but may increase HDD fragmentation on some FS. Disable this optimization with [\033[32m1,1,1\033[0m]")
ap2.add_argument("--u2ow", metavar="NUM", type=int, default=0, help="web-client: default setting for when to replace/overwrite existing files; [\033[32m0\033[0m]=never, [\033[32m1\033[0m]=if-client-newer, [\033[32m2\033[0m]=always (volflag=u2ow)")
ap2.add_argument("--u2sort", metavar="TXT", type=u, default="s", help="upload order; [\033[32ms\033[0m]=smallest-first, [\033[32mn\033[0m]=alphabetical, [\033[32mfs\033[0m]=force-s, [\033[32mfn\033[0m]=force-n -- alphabetical is a bit slower on fiber/LAN but makes it easier to eyeball if everything went fine") ap2.add_argument("--u2sort", metavar="TXT", type=u, default="s", help="upload order; [\033[32ms\033[0m]=smallest-first, [\033[32mn\033[0m]=alphabetical, [\033[32mfs\033[0m]=force-s, [\033[32mfn\033[0m]=force-n -- alphabetical is a bit slower on fiber/LAN but makes it easier to eyeball if everything went fine")
ap2.add_argument("--write-uplog", action="store_true", help="write POST reports to textfiles in working-directory") ap2.add_argument("--write-uplog", action="store_true", help="write POST reports to textfiles in working-directory")
def add_network(ap): def add_network(ap):
ap2 = ap.add_argument_group("network options") ap2 = ap.add_argument_group('network options')
ap2.add_argument("-i", metavar="IP", type=u, default="::", help="IPs and/or unix-sockets to listen on (comma-separated list; see \033[33m--help-bind\033[0m). Default: all IPv4 and IPv6") ap2.add_argument("-i", metavar="IP", type=u, default="::", help="IPs and/or unix-sockets to listen on (see \033[33m--help-bind\033[0m). Default: all IPv4 and IPv6")
ap2.add_argument("-p", metavar="PORT", type=u, default="3923", help="ports to listen on (comma/range); ignored for unix-sockets") ap2.add_argument("-p", metavar="PORT", type=u, default="3923", help="ports to listen on (comma/range); ignored for unix-sockets")
ap2.add_argument("--ll", action="store_true", help="include link-local IPv4/IPv6 in mDNS replies, even if the NIC has routable IPs (breaks some mDNS clients)") ap2.add_argument("--ll", action="store_true", help="include link-local IPv4/IPv6 in mDNS replies, even if the NIC has routable IPs (breaks some mDNS clients)")
ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=9999999, help="which ip to associate clients with; [\033[32m0\033[0m]=tcp, [\033[32m1\033[0m]=origin (first x-fwd, unsafe), [\033[32m-1\033[0m]=closest-proxy, [\033[32m-2\033[0m]=second-hop, [\033[32m-3\033[0m]=third-hop") ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to associate clients with; [\033[32m0\033[0m]=tcp, [\033[32m1\033[0m]=origin (first x-fwd, unsafe), [\033[32m2\033[0m]=outermost-proxy, [\033[32m3\033[0m]=second-proxy, [\033[32m-1\033[0m]=closest-proxy")
ap2.add_argument("--xff-hdr", metavar="NAME", type=u, default="x-forwarded-for", help="if reverse-proxied, which http header to read the client's real ip from") ap2.add_argument("--xff-hdr", metavar="NAME", type=u, default="x-forwarded-for", help="if reverse-proxied, which http header to read the client's real ip from")
ap2.add_argument("--xff-src", metavar="CIDR", type=u, default="127.0.0.0/8, ::1/128", help="list of trusted reverse-proxy CIDRs (comma-separated); only accept the real-ip header (\033[33m--xff-hdr\033[0m) and IdP headers if the incoming connection is from an IP within either of these subnets. Specify [\033[32mlan\033[0m] to allow all LAN / private / non-internet IPs. Can be disabled with [\033[32many\033[0m] if you are behind cloudflare (or similar) and are using \033[32m--xff-hdr=cf-connecting-ip\033[0m (or similar)") ap2.add_argument("--xff-src", metavar="CIDR", type=u, default="127.0.0.0/8, ::1/128", help="comma-separated list of trusted reverse-proxy CIDRs; only accept the real-ip header (\033[33m--xff-hdr\033[0m) and IdP headers if the incoming connection is from an IP within either of these subnets. Specify [\033[32mlan\033[0m] to allow all LAN / private / non-internet IPs. Can be disabled with [\033[32many\033[0m] if you are behind cloudflare (or similar) and are using \033[32m--xff-hdr=cf-connecting-ip\033[0m (or similar)")
ap2.add_argument("--ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]") ap2.add_argument("--ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m; examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]")
ap2.add_argument("--rp-loc", metavar="PATH", type=u, default="", help="if reverse-proxying on a location instead of a dedicated domain/subdomain, provide the base location here; example: [\033[32m/foo/bar\033[0m]") ap2.add_argument("--rp-loc", metavar="PATH", type=u, default="", help="if reverse-proxying on a location instead of a dedicated domain/subdomain, provide the base location here; example: [\033[32m/foo/bar\033[0m]")
if ANYWIN: if ANYWIN:
ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances") ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances")
elif not MACOS: else:
ap2.add_argument("--freebind", action="store_true", help="allow listening on IPs which do not yet exist, for example if the network interfaces haven't finished going up. Only makes sense for IPs other than '0.0.0.0', '127.0.0.1', '::', and '::1'. May require running as root (unless net.ipv6.ip_nonlocal_bind)") ap2.add_argument("--freebind", action="store_true", help="allow listening on IPs which do not yet exist, for example if the network interfaces haven't finished going up. Only makes sense for IPs other than '0.0.0.0', '127.0.0.1', '::', and '::1'. May require running as root (unless net.ipv6.ip_nonlocal_bind)")
ap2.add_argument("--wr-h-eps", metavar="PATH", type=u, default="", help="write list of listening-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started")
ap2.add_argument("--wr-h-aon", metavar="PATH", type=u, default="", help="write list of accessible-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started")
ap2.add_argument("--s-thead", metavar="SEC", type=int, default=120, help="socket timeout (read request header)") ap2.add_argument("--s-thead", metavar="SEC", type=int, default=120, help="socket timeout (read request header)")
ap2.add_argument("--s-tbody", metavar="SEC", type=float, default=128.0, help="socket timeout (read/write request/response bodies). Use 60 on fast servers (default is extremely safe). Disable with 0 if reverse-proxied for a 2%% speed boost") ap2.add_argument("--s-tbody", metavar="SEC", type=float, default=128.0, help="socket timeout (read/write request/response bodies). Use 60 on fast servers (default is extremely safe). Disable with 0 if reverse-proxied for a 2%% speed boost")
ap2.add_argument("--s-rd-sz", metavar="B", type=int, default=256*1024, help="socket read size in bytes (indirectly affects filesystem writes; recommendation: keep equal-to or lower-than \033[33m--iobuf\033[0m)") ap2.add_argument("--s-rd-sz", metavar="B", type=int, default=256*1024, help="socket read size in bytes (indirectly affects filesystem writes; recommendation: keep equal-to or lower-than \033[33m--iobuf\033[0m)")
@ -1204,10 +1056,10 @@ def add_network(ap):
def add_tls(ap, cert_path): def add_tls(ap, cert_path):
ap2 = ap.add_argument_group("SSL/TLS options") ap2 = ap.add_argument_group('SSL/TLS options')
ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls -- force plaintext") ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls -- force plaintext")
ap2.add_argument("--https-only", action="store_true", help="disable plaintext -- force tls") ap2.add_argument("--https-only", action="store_true", help="disable plaintext -- force tls")
ap2.add_argument("--cert", metavar="PATH", type=u, default=cert_path, help="path to file containing a concatenation of TLS key and certificate chain") ap2.add_argument("--cert", metavar="PATH", type=u, default=cert_path, help="path to TLS certificate")
ap2.add_argument("--ssl-ver", metavar="LIST", type=u, default="", help="set allowed ssl/tls versions; [\033[32mhelp\033[0m] shows available versions; default is what your python version considers safe") ap2.add_argument("--ssl-ver", metavar="LIST", type=u, default="", help="set allowed ssl/tls versions; [\033[32mhelp\033[0m] shows available versions; default is what your python version considers safe")
ap2.add_argument("--ciphers", metavar="LIST", type=u, default="", help="set allowed ssl/tls ciphers; [\033[32mhelp\033[0m] shows available ciphers") ap2.add_argument("--ciphers", metavar="LIST", type=u, default="", help="set allowed ssl/tls ciphers; [\033[32mhelp\033[0m] shows available ciphers")
ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info") ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info")
@ -1216,7 +1068,7 @@ def add_tls(ap, cert_path):
def add_cert(ap, cert_path): def add_cert(ap, cert_path):
cert_dir = os.path.dirname(cert_path) cert_dir = os.path.dirname(cert_path)
ap2 = ap.add_argument_group("TLS certificate generator options") ap2 = ap.add_argument_group('TLS certificate generator options')
ap2.add_argument("--no-crt", action="store_true", help="disable automatic certificate creation") ap2.add_argument("--no-crt", action="store_true", help="disable automatic certificate creation")
ap2.add_argument("--crt-ns", metavar="N,N", type=u, default="", help="comma-separated list of FQDNs (domains) to add into the certificate") ap2.add_argument("--crt-ns", metavar="N,N", type=u, default="", help="comma-separated list of FQDNs (domains) to add into the certificate")
ap2.add_argument("--crt-exact", action="store_true", help="do not add wildcard entries for each \033[33m--crt-ns\033[0m") ap2.add_argument("--crt-exact", action="store_true", help="do not add wildcard entries for each \033[33m--crt-ns\033[0m")
@ -1234,35 +1086,25 @@ def add_cert(ap, cert_path):
def add_auth(ap): def add_auth(ap):
idp_db = os.path.join(E.cfg, "idp.db")
ses_db = os.path.join(E.cfg, "sessions.db") ses_db = os.path.join(E.cfg, "sessions.db")
ap2 = ap.add_argument_group("IdP / identity provider / user authentication options") ap2 = ap.add_argument_group('IdP / identity provider / user authentication options')
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy") ap2.add_argument("--idp-h-usr", metavar="HN", type=u, default="", help="bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
ap2.add_argument("--idp-hm-usr", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp")
ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control") ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control")
ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present") ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present")
ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m") ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m")
ap2.add_argument("--idp-db", metavar="PATH", type=u, default=idp_db, help="where to store the known IdP users/groups (if you run multiple copyparty instances, make sure they use different DBs)")
ap2.add_argument("--idp-store", metavar="N", type=int, default=1, help="how to use \033[33m--idp-db\033[0m; [\033[32m0\033[0m] = entirely disable, [\033[32m1\033[0m] = write-only (effectively disabled), [\033[32m2\033[0m] = remember users, [\033[32m3\033[0m] = remember users and groups.\nNOTE: Will remember and restore the IdP-volumes of all users for all eternity if set to 2 or 3, even when user is deleted from your IdP")
ap2.add_argument("--idp-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to use /?idp (the cache management UI)")
ap2.add_argument("--idp-cookie", metavar="S", type=int, default=0, help="generate a session-token for IdP users which is written to cookie \033[33mcppws\033[0m (or \033[33mcppwd\033[0m if plaintext), to reduce the load on the IdP server, lifetime \033[33mS\033[0m seconds.\n └─note: The expiration time is a client hint only; the actual lifetime of the session-token is infinite (until next restart with \033[33m--ses-db\033[0m wiped)")
ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app") ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app")
ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins") ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins")
ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)") ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)")
ap2.add_argument("--ses-len", metavar="CHARS", type=int, default=20, help="session key length; default is 120 bits ((20//4)*4*6)") ap2.add_argument("--ses-len", metavar="CHARS", type=int, default=20, help="session key length; default is 120 bits ((20//4)*4*6)")
ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies") ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies")
ap2.add_argument("--grp-all", metavar="NAME", type=u, default="acct", help="the name of the auto-generated group which contains every username which is known") ap2.add_argument("--ipu", metavar="CIDR=USR", type=u, action="append", help="users with IP matching \033[33mCIDR\033[0m are auto-authenticated as username \033[33mUSR\033[0m; example: [\033[32m172.16.24.0/24=dave]")
ap2.add_argument("--ipu", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m users with IP matching \033[33mCIDR\033[0m are auto-authenticated as username \033[33mUSR\033[0m; example: [\033[32m172.16.24.0/24=dave]")
ap2.add_argument("--ipr", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m username \033[33mUSR\033[0m can only connect from an IP matching one or more \033[33mCIDR\033[0m (comma-sep.); example: [\033[32m192.168.123.0/24,172.16.0.0/16=dave]")
ap2.add_argument("--have-idp-hdrs", type=u, default="", help=argparse.SUPPRESS)
ap2.add_argument("--have-ipu-or-ipr", type=u, default="", help=argparse.SUPPRESS)
def add_chpw(ap): def add_chpw(ap):
db_path = os.path.join(E.cfg, "chpw.json") db_path = os.path.join(E.cfg, "chpw.json")
ap2 = ap.add_argument_group("user-changeable passwords options") ap2 = ap.add_argument_group('user-changeable passwords options')
ap2.add_argument("--chpw", action="store_true", help="allow users to change their own passwords") ap2.add_argument("--chpw", action="store_true", help="allow users to change their own passwords")
ap2.add_argument("--chpw-no", metavar="U,U,U", type=u, action="append", help="\033[34mREPEATABLE:\033[0m do not allow password-changes for this comma-separated list of usernames") ap2.add_argument("--chpw-no", metavar="U,U,U", type=u, action="append", help="do not allow password-changes for this comma-separated list of usernames")
ap2.add_argument("--chpw-db", metavar="PATH", type=u, default=db_path, help="where to store the passwords database (if you run multiple copyparty instances, make sure they use different DBs)") ap2.add_argument("--chpw-db", metavar="PATH", type=u, default=db_path, help="where to store the passwords database (if you run multiple copyparty instances, make sure they use different DBs)")
ap2.add_argument("--chpw-len", metavar="N", type=int, default=8, help="minimum password length") ap2.add_argument("--chpw-len", metavar="N", type=int, default=8, help="minimum password length")
ap2.add_argument("--chpw-v", metavar="LVL", type=int, default=2, help="verbosity of summary on config load [\033[32m0\033[0m] = nothing at all, [\033[32m1\033[0m] = number of users, [\033[32m2\033[0m] = list users with default-pw, [\033[32m3\033[0m] = list all users") ap2.add_argument("--chpw-v", metavar="LVL", type=int, default=2, help="verbosity of summary on config load [\033[32m0\033[0m] = nothing at all, [\033[32m1\033[0m] = number of users, [\033[32m2\033[0m] = list users with default-pw, [\033[32m3\033[0m] = list all users")
@ -1294,7 +1136,6 @@ def add_zc_mdns(ap):
ap2.add_argument("--zm-lh", metavar="PATH", type=u, default="", help="link a specific folder for http shares") ap2.add_argument("--zm-lh", metavar="PATH", type=u, default="", help="link a specific folder for http shares")
ap2.add_argument("--zm-lf", metavar="PATH", type=u, default="", help="link a specific folder for ftp shares") ap2.add_argument("--zm-lf", metavar="PATH", type=u, default="", help="link a specific folder for ftp shares")
ap2.add_argument("--zm-ls", metavar="PATH", type=u, default="", help="link a specific folder for smb shares") ap2.add_argument("--zm-ls", metavar="PATH", type=u, default="", help="link a specific folder for smb shares")
ap2.add_argument("--zm-fqdn", metavar="FQDN", type=u, default="--name.local", help="the domain to announce; NOTE: using anything other than .local is nonstandard and could cause problems")
ap2.add_argument("--zm-mnic", action="store_true", help="merge NICs which share subnets; assume that same subnet means same network") ap2.add_argument("--zm-mnic", action="store_true", help="merge NICs which share subnets; assume that same subnet means same network")
ap2.add_argument("--zm-msub", action="store_true", help="merge subnets on each NIC -- always enabled for ipv6 -- reduces network load, but gnome-gvfs clients may stop working, and clients cannot be in subnets that the server is not") ap2.add_argument("--zm-msub", action="store_true", help="merge subnets on each NIC -- always enabled for ipv6 -- reduces network load, but gnome-gvfs clients may stop working, and clients cannot be in subnets that the server is not")
ap2.add_argument("--zm-noneg", action="store_true", help="disable NSEC replies -- try this if some clients don't see copyparty") ap2.add_argument("--zm-noneg", action="store_true", help="disable NSEC replies -- try this if some clients don't see copyparty")
@ -1312,12 +1153,12 @@ def add_zc_ssdp(ap):
def add_ftp(ap): def add_ftp(ap):
ap2 = ap.add_argument_group("FTP options (TCP only)") ap2 = ap.add_argument_group('FTP options (TCP only)')
ap2.add_argument("--ftp", metavar="PORT", type=int, default=0, help="enable FTP server on \033[33mPORT\033[0m, for example \033[32m3921") ap2.add_argument("--ftp", metavar="PORT", type=int, default=0, help="enable FTP server on \033[33mPORT\033[0m, for example \033[32m3921")
ap2.add_argument("--ftps", metavar="PORT", type=int, default=0, help="enable FTPS server on \033[33mPORT\033[0m, for example \033[32m3990") ap2.add_argument("--ftps", metavar="PORT", type=int, default=0, help="enable FTPS server on \033[33mPORT\033[0m, for example \033[32m3990")
ap2.add_argument("--ftpv", action="store_true", help="verbose") ap2.add_argument("--ftpv", action="store_true", help="verbose")
ap2.add_argument("--ftp4", action="store_true", help="only listen on IPv4") ap2.add_argument("--ftp4", action="store_true", help="only listen on IPv4")
ap2.add_argument("--ftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]") ap2.add_argument("--ftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m; specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]")
ap2.add_argument("--ftp-no-ow", action="store_true", help="if target file exists, reject upload instead of overwrite") ap2.add_argument("--ftp-no-ow", action="store_true", help="if target file exists, reject upload instead of overwrite")
ap2.add_argument("--ftp-wt", metavar="SEC", type=int, default=7, help="grace period for resuming interrupted uploads (any client can write to any file last-modified more recently than \033[33mSEC\033[0m seconds ago)") ap2.add_argument("--ftp-wt", metavar="SEC", type=int, default=7, help="grace period for resuming interrupted uploads (any client can write to any file last-modified more recently than \033[33mSEC\033[0m seconds ago)")
ap2.add_argument("--ftp-nat", metavar="ADDR", type=u, default="", help="the NAT address to use for passive connections") ap2.add_argument("--ftp-nat", metavar="ADDR", type=u, default="", help="the NAT address to use for passive connections")
@ -1325,17 +1166,16 @@ def add_ftp(ap):
def add_webdav(ap): def add_webdav(ap):
ap2 = ap.add_argument_group("WebDAV options") ap2 = ap.add_argument_group('WebDAV options')
ap2.add_argument("--daw", action="store_true", help="enable full write support, even if client may not be webdav. \033[1;31mWARNING:\033[0m This has side-effects -- PUT-operations will now \033[1;31mOVERWRITE\033[0m existing files, rather than inventing new filenames to avoid loss of data. You might want to instead set this as a volflag where needed. By not setting this flag, uploaded files can get written to a filename which the client does not expect (which might be okay, depending on client)") ap2.add_argument("--daw", action="store_true", help="enable full write support, even if client may not be webdav. \033[1;31mWARNING:\033[0m This has side-effects -- PUT-operations will now \033[1;31mOVERWRITE\033[0m existing files, rather than inventing new filenames to avoid loss of data. You might want to instead set this as a volflag where needed. By not setting this flag, uploaded files can get written to a filename which the client does not expect (which might be okay, depending on client)")
ap2.add_argument("--dav-inf", action="store_true", help="allow depth:infinite requests (recursive file listing); extremely server-heavy but required for spec compliance -- luckily few clients rely on this") ap2.add_argument("--dav-inf", action="store_true", help="allow depth:infinite requests (recursive file listing); extremely server-heavy but required for spec compliance -- luckily few clients rely on this")
ap2.add_argument("--dav-mac", action="store_true", help="disable apple-garbage filter -- allow macos to create junk files (._* and .DS_Store, .Spotlight-*, .fseventsd, .Trashes, .AppleDouble, __MACOS)") ap2.add_argument("--dav-mac", action="store_true", help="disable apple-garbage filter -- allow macos to create junk files (._* and .DS_Store, .Spotlight-*, .fseventsd, .Trashes, .AppleDouble, __MACOS)")
ap2.add_argument("--dav-rt", action="store_true", help="show symlink-destination's lastmodified instead of the link itself; always enabled for recursive listings (volflag=davrt)") ap2.add_argument("--dav-rt", action="store_true", help="show symlink-destination's lastmodified instead of the link itself; always enabled for recursive listings (volflag=davrt)")
ap2.add_argument("--dav-auth", action="store_true", help="force auth for all folders (required by davfs2 when only some folders are world-readable) (volflag=davauth)") ap2.add_argument("--dav-auth", action="store_true", help="force auth for all folders (required by davfs2 when only some folders are world-readable) (volflag=davauth)")
ap2.add_argument("--dav-ua1", metavar="PTN", type=u, default=r" kioworker/", help="regex of tricky user-agents which expect 401 from GET requests; disable with [\033[32mno\033[0m] or blank")
def add_tftp(ap): def add_tftp(ap):
ap2 = ap.add_argument_group("TFTP options (UDP only)") ap2 = ap.add_argument_group('TFTP options (UDP only)')
ap2.add_argument("--tftp", metavar="PORT", type=int, default=0, help="enable TFTP server on \033[33mPORT\033[0m, for example \033[32m69 \033[0mor \033[32m3969") ap2.add_argument("--tftp", metavar="PORT", type=int, default=0, help="enable TFTP server on \033[33mPORT\033[0m, for example \033[32m69 \033[0mor \033[32m3969")
ap2.add_argument("--tftp4", action="store_true", help="only listen on IPv4") ap2.add_argument("--tftp4", action="store_true", help="only listen on IPv4")
ap2.add_argument("--tftpv", action="store_true", help="verbose") ap2.add_argument("--tftpv", action="store_true", help="verbose")
@ -1343,12 +1183,12 @@ def add_tftp(ap):
ap2.add_argument("--tftp-no-fast", action="store_true", help="debug: disable optimizations") ap2.add_argument("--tftp-no-fast", action="store_true", help="debug: disable optimizations")
ap2.add_argument("--tftp-lsf", metavar="PTN", type=u, default="\\.?(dir|ls)(\\.txt)?", help="return a directory listing if a file with this name is requested and it does not exist; defaults matches .ls, dir, .dir.txt, ls.txt, ...") ap2.add_argument("--tftp-lsf", metavar="PTN", type=u, default="\\.?(dir|ls)(\\.txt)?", help="return a directory listing if a file with this name is requested and it does not exist; defaults matches .ls, dir, .dir.txt, ls.txt, ...")
ap2.add_argument("--tftp-nols", action="store_true", help="if someone tries to download a directory, return an error instead of showing its directory listing") ap2.add_argument("--tftp-nols", action="store_true", help="if someone tries to download a directory, return an error instead of showing its directory listing")
ap2.add_argument("--tftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]") ap2.add_argument("--tftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m; specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]")
ap2.add_argument("--tftp-pr", metavar="P-P", type=u, default="", help="the range of UDP ports to use for data transfer, for example \033[32m12000-13000") ap2.add_argument("--tftp-pr", metavar="P-P", type=u, default="", help="the range of UDP ports to use for data transfer, for example \033[32m12000-13000")
def add_smb(ap): def add_smb(ap):
ap2 = ap.add_argument_group("SMB/CIFS options") ap2 = ap.add_argument_group('SMB/CIFS options')
ap2.add_argument("--smb", action="store_true", help="enable smb (read-only) -- this requires running copyparty as root on linux and macos unless \033[33m--smb-port\033[0m is set above 1024 and your OS does port-forwarding from 445 to that.\n\033[1;31mWARNING:\033[0m this protocol is DANGEROUS and buggy! Never expose to the internet!") ap2.add_argument("--smb", action="store_true", help="enable smb (read-only) -- this requires running copyparty as root on linux and macos unless \033[33m--smb-port\033[0m is set above 1024 and your OS does port-forwarding from 445 to that.\n\033[1;31mWARNING:\033[0m this protocol is DANGEROUS and buggy! Never expose to the internet!")
ap2.add_argument("--smbw", action="store_true", help="enable write support (please dont)") ap2.add_argument("--smbw", action="store_true", help="enable write support (please dont)")
ap2.add_argument("--smb1", action="store_true", help="disable SMBv2, only enable SMBv1 (CIFS)") ap2.add_argument("--smb1", action="store_true", help="disable SMBv2, only enable SMBv1 (CIFS)")
@ -1362,30 +1202,30 @@ def add_smb(ap):
def add_handlers(ap): def add_handlers(ap):
ap2 = ap.add_argument_group("handlers (see --help-handlers)") ap2 = ap.add_argument_group('handlers (see --help-handlers)')
ap2.add_argument("--on404", metavar="PY", type=u, action="append", help="\033[34mREPEATABLE:\033[0m handle 404s by executing \033[33mPY\033[0m file") ap2.add_argument("--on404", metavar="PY", type=u, action="append", help="handle 404s by executing \033[33mPY\033[0m file")
ap2.add_argument("--on403", metavar="PY", type=u, action="append", help="\033[34mREPEATABLE:\033[0m handle 403s by executing \033[33mPY\033[0m file") ap2.add_argument("--on403", metavar="PY", type=u, action="append", help="handle 403s by executing \033[33mPY\033[0m file")
ap2.add_argument("--hot-handlers", action="store_true", help="recompile handlers on each request -- expensive but convenient when hacking on stuff") ap2.add_argument("--hot-handlers", action="store_true", help="recompile handlers on each request -- expensive but convenient when hacking on stuff")
def add_hooks(ap): def add_hooks(ap):
ap2 = ap.add_argument_group("event hooks (see --help-hooks)") ap2 = ap.add_argument_group('event hooks (see --help-hooks)')
ap2.add_argument("--xbu", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m before a file upload starts") ap2.add_argument("--xbu", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file upload starts")
ap2.add_argument("--xau", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after a file upload finishes") ap2.add_argument("--xau", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after a file upload finishes")
ap2.add_argument("--xiu", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after all uploads finish and volume is idle") ap2.add_argument("--xiu", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after all uploads finish and volume is idle")
ap2.add_argument("--xbc", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m before a file copy") ap2.add_argument("--xbc", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file copy")
ap2.add_argument("--xac", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after a file copy") ap2.add_argument("--xac", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after a file copy")
ap2.add_argument("--xbr", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m before a file move/rename") ap2.add_argument("--xbr", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file move/rename")
ap2.add_argument("--xar", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after a file move/rename") ap2.add_argument("--xar", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after a file move/rename")
ap2.add_argument("--xbd", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m before a file delete") ap2.add_argument("--xbd", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file delete")
ap2.add_argument("--xad", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after a file delete") ap2.add_argument("--xad", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after a file delete")
ap2.add_argument("--xm", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m on message") ap2.add_argument("--xm", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m on message")
ap2.add_argument("--xban", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m if someone gets banned (pw/404/403/url)") ap2.add_argument("--xban", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m if someone gets banned (pw/404/403/url)")
ap2.add_argument("--hook-v", action="store_true", help="verbose hooks") ap2.add_argument("--hook-v", action="store_true", help="verbose hooks")
def add_stats(ap): def add_stats(ap):
ap2 = ap.add_argument_group("grafana/prometheus metrics endpoint") ap2 = ap.add_argument_group('grafana/prometheus metrics endpoint')
ap2.add_argument("--stats", action="store_true", help="enable openmetrics at /.cpr/metrics for admin accounts") ap2.add_argument("--stats", action="store_true", help="enable openmetrics at /.cpr/metrics for admin accounts")
ap2.add_argument("--nos-hdd", action="store_true", help="disable disk-space metrics (used/free space)") ap2.add_argument("--nos-hdd", action="store_true", help="disable disk-space metrics (used/free space)")
ap2.add_argument("--nos-vol", action="store_true", help="disable volume size metrics (num files, total bytes, vmaxb/vmaxn)") ap2.add_argument("--nos-vol", action="store_true", help="disable volume size metrics (num files, total bytes, vmaxb/vmaxn)")
@ -1395,45 +1235,34 @@ def add_stats(ap):
def add_yolo(ap): def add_yolo(ap):
ap2 = ap.add_argument_group("yolo options") ap2 = ap.add_argument_group('yolo options')
ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests") ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests")
ap2.add_argument("--cookie-lax", action="store_true", help="allow cookies from other domains (if you follow a link from another website into your server, you will arrive logged-in); this reduces protection against CSRF")
ap2.add_argument("--no-fnugg", action="store_true", help="disable the smoketest for caching-related issues in the web-UI")
ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET") ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET")
ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)")
def add_optouts(ap): def add_optouts(ap):
ap2 = ap.add_argument_group("opt-outs") ap2 = ap.add_argument_group('opt-outs')
ap2.add_argument("-nw", action="store_true", help="never write anything to disk (debug/benchmark)") ap2.add_argument("-nw", action="store_true", help="never write anything to disk (debug/benchmark)")
ap2.add_argument("--keep-qem", action="store_true", help="do not disable quick-edit-mode on windows (it is disabled to avoid accidental text selection in the terminal window, as this would pause execution)") ap2.add_argument("--keep-qem", action="store_true", help="do not disable quick-edit-mode on windows (it is disabled to avoid accidental text selection in the terminal window, as this would pause execution)")
ap2.add_argument("--no-dav", action="store_true", help="disable webdav support") ap2.add_argument("--no-dav", action="store_true", help="disable webdav support")
ap2.add_argument("--no-del", action="store_true", help="disable delete operations") ap2.add_argument("--no-del", action="store_true", help="disable delete operations")
ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations") ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations")
ap2.add_argument("--no-cp", action="store_true", help="disable copy operations") ap2.add_argument("--no-cp", action="store_true", help="disable copy operations")
ap2.add_argument("--no-fs-abrt", action="store_true", help="disable ability to abort ongoing copy/move")
ap2.add_argument("-nth", action="store_true", help="no title hostname; don't show \033[33m--name\033[0m in <title>") ap2.add_argument("-nth", action="store_true", help="no title hostname; don't show \033[33m--name\033[0m in <title>")
ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI") ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI")
ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI") ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI")
ap2.add_argument("-nb", action="store_true", help="no powered-by-copyparty branding in UI") ap2.add_argument("-nb", action="store_true", help="no powered-by-copyparty branding in UI")
ap2.add_argument("--zipmaxn", metavar="N", type=u, default="0", help="reject download-as-zip if more than \033[33mN\033[0m files in total; optionally takes a unit suffix: [\033[32m256\033[0m], [\033[32m9K\033[0m], [\033[32m4G\033[0m] (volflag=zipmaxn)") ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
ap2.add_argument("--zipmaxs", metavar="SZ", type=u, default="0", help="reject download-as-zip if total download size exceeds \033[33mSZ\033[0m bytes; optionally takes a unit suffix: [\033[32m256M\033[0m], [\033[32m4G\033[0m], [\033[32m2T\033[0m] (volflag=zipmaxs)")
ap2.add_argument("--zipmaxt", metavar="TXT", type=u, default="", help="custom errormessage when download size exceeds max (volflag=zipmaxt)")
ap2.add_argument("--zipmaxu", action="store_true", help="authenticated users bypass the zip size limit (volflag=zipmaxu)")
ap2.add_argument("--zip-who", metavar="LVL", type=int, default=3, help="who can download as zip/tar? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=authenticated-with-read-access, [\033[32m3\033[0m]=everyone-with-read-access (volflag=zip_who)\n\033[1;31mWARNING:\033[0m if a nested volume has a more restrictive value than a parent volume, then this will be \033[33mignored\033[0m if the download is initiated from the parent, more lenient volume")
ap2.add_argument("--ua-nozip", metavar="PTN", type=u, default=BAD_BOTS, help="regex of user-agents to reject from download-as-zip/tar; disable with [\033[32mno\033[0m] or blank")
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar; same as \033[33m--zip-who=0\033[0m")
ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)") ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)")
ap2.add_argument("--no-lifetime", action="store_true", help="do not allow clients (or server config) to schedule an upload to be deleted after a given time") ap2.add_argument("--no-lifetime", action="store_true", help="do not allow clients (or server config) to schedule an upload to be deleted after a given time")
ap2.add_argument("--no-pipe", action="store_true", help="disable race-the-beam (lockstep download of files which are currently being uploaded) (volflag=nopipe)") ap2.add_argument("--no-pipe", action="store_true", help="disable race-the-beam (lockstep download of files which are currently being uploaded) (volflag=nopipe)")
ap2.add_argument("--no-tail", action="store_true", help="disable streaming a growing files with ?tail (volflag=notail)") ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader IPs into the database")
ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader-IP into the database; will also disable unpost, you may want \033[32m--forget-ip\033[0m instead (volflag=no_db_ip)")
def add_safety(ap): def add_safety(ap):
ap2 = ap.add_argument_group("safety options") ap2 = ap.add_argument_group('safety options')
ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js") ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js")
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav requires login, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --dav-auth --vague-403 -nih") ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --vague-403 -nih")
ap2.add_argument("-sss", action="store_true", help="further increase safety: Enable logging to disk, scan for dangerous symlinks.\n └─Alias of\033[32m -ss --no-dav --no-logues --no-readme -lo=cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz --ls=**,*,ln,p,r") ap2.add_argument("-sss", action="store_true", help="further increase safety: Enable logging to disk, scan for dangerous symlinks.\n └─Alias of\033[32m -ss --no-dav --no-logues --no-readme -lo=cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz --ls=**,*,ln,p,r")
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, default="", help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m (see \033[33m--help-ls\033[0m); example [\033[32m**,*,ln,p,r\033[0m]") ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, default="", help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m (see \033[33m--help-ls\033[0m); example [\033[32m**,*,ln,p,r\033[0m]")
ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)") ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)")
@ -1447,7 +1276,6 @@ def add_safety(ap):
ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)") ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)")
ap2.add_argument("--logout", metavar="H", type=float, default=8086.0, help="logout clients after \033[33mH\033[0m hours of inactivity; [\033[32m0.0028\033[0m]=10sec, [\033[32m0.1\033[0m]=6min, [\033[32m24\033[0m]=day, [\033[32m168\033[0m]=week, [\033[32m720\033[0m]=month, [\033[32m8760\033[0m]=year)") ap2.add_argument("--logout", metavar="H", type=float, default=8086.0, help="logout clients after \033[33mH\033[0m hours of inactivity; [\033[32m0.0028\033[0m]=10sec, [\033[32m0.1\033[0m]=6min, [\033[32m24\033[0m]=day, [\033[32m168\033[0m]=week, [\033[32m720\033[0m]=month, [\033[32m8760\033[0m]=year)")
ap2.add_argument("--ban-pw", metavar="N,W,B", type=u, default="9,60,1440", help="more than \033[33mN\033[0m wrong passwords in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]") ap2.add_argument("--ban-pw", metavar="N,W,B", type=u, default="9,60,1440", help="more than \033[33mN\033[0m wrong passwords in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]")
ap2.add_argument("--ban-pwc", metavar="N,W,B", type=u, default="5,60,1440", help="more than \033[33mN\033[0m password-changes in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]")
ap2.add_argument("--ban-404", metavar="N,W,B", type=u, default="50,60,1440", help="hitting more than \033[33mN\033[0m 404's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; only affects users who cannot see directory listings because their access is either g/G/h") ap2.add_argument("--ban-404", metavar="N,W,B", type=u, default="50,60,1440", help="hitting more than \033[33mN\033[0m 404's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; only affects users who cannot see directory listings because their access is either g/G/h")
ap2.add_argument("--ban-403", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 403's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month") ap2.add_argument("--ban-403", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 403's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month")
ap2.add_argument("--ban-422", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 422's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (invalid requests, attempted exploits ++)") ap2.add_argument("--ban-422", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 422's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (invalid requests, attempted exploits ++)")
@ -1455,8 +1283,6 @@ def add_safety(ap):
ap2.add_argument("--sus-urls", metavar="R", type=u, default=r"\.php$|(^|/)wp-(admin|content|includes)/", help="URLs which are considered sus / eligible for banning; disable with blank or [\033[32mno\033[0m]") ap2.add_argument("--sus-urls", metavar="R", type=u, default=r"\.php$|(^|/)wp-(admin|content|includes)/", help="URLs which are considered sus / eligible for banning; disable with blank or [\033[32mno\033[0m]")
ap2.add_argument("--nonsus-urls", metavar="R", type=u, default=r"^(favicon\.ico|robots\.txt)$|^apple-touch-icon|^\.well-known", help="harmless URLs ignored from 404-bans; disable with blank or [\033[32mno\033[0m]") ap2.add_argument("--nonsus-urls", metavar="R", type=u, default=r"^(favicon\.ico|robots\.txt)$|^apple-touch-icon|^\.well-known", help="harmless URLs ignored from 404-bans; disable with blank or [\033[32mno\033[0m]")
ap2.add_argument("--early-ban", action="store_true", help="if a client is banned, reject its connection as soon as possible; not a good idea to enable when proxied behind cloudflare since it could ban your reverse-proxy") ap2.add_argument("--early-ban", action="store_true", help="if a client is banned, reject its connection as soon as possible; not a good idea to enable when proxied behind cloudflare since it could ban your reverse-proxy")
ap2.add_argument("--cookie-nmax", metavar="N", type=int, default=50, help="reject HTTP-request from client if they send more than N cookies")
ap2.add_argument("--cookie-cmax", metavar="N", type=int, default=8192, help="reject HTTP-request from client if more than N characters in Cookie header")
ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for \033[33mMIN\033[0m minutes (and also kill its active connections) -- disable with 0") ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for \033[33mMIN\033[0m minutes (and also kill its active connections) -- disable with 0")
ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]") ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]")
ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives") ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives")
@ -1464,7 +1290,7 @@ def add_safety(ap):
def add_salt(ap, fk_salt, dk_salt, ah_salt): def add_salt(ap, fk_salt, dk_salt, ah_salt):
ap2 = ap.add_argument_group("salting options") ap2 = ap.add_argument_group('salting options')
ap2.add_argument("--ah-alg", metavar="ALG", type=u, default="none", help="account-pw hashing algorithm; one of these, best to worst: \033[32margon2 scrypt sha2 none\033[0m (each optionally followed by alg-specific comma-sep. config)") ap2.add_argument("--ah-alg", metavar="ALG", type=u, default="none", help="account-pw hashing algorithm; one of these, best to worst: \033[32margon2 scrypt sha2 none\033[0m (each optionally followed by alg-specific comma-sep. config)")
ap2.add_argument("--ah-salt", metavar="SALT", type=u, default=ah_salt, help="account-pw salt; ignored if \033[33m--ah-alg\033[0m is none (default)") ap2.add_argument("--ah-salt", metavar="SALT", type=u, default=ah_salt, help="account-pw salt; ignored if \033[33m--ah-alg\033[0m is none (default)")
ap2.add_argument("--ah-gen", metavar="PW", type=u, default="", help="generate hashed password for \033[33mPW\033[0m, or read passwords from STDIN if \033[33mPW\033[0m is [\033[32m-\033[0m]") ap2.add_argument("--ah-gen", metavar="PW", type=u, default="", help="generate hashed password for \033[33mPW\033[0m, or read passwords from STDIN if \033[33mPW\033[0m is [\033[32m-\033[0m]")
@ -1472,29 +1298,26 @@ def add_salt(ap, fk_salt, dk_salt, ah_salt):
ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files") ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files")
ap2.add_argument("--dk-salt", metavar="SALT", type=u, default=dk_salt, help="per-directory accesskey salt; used to generate unpredictable URLs to share folders with users who only have the 'get' permission") ap2.add_argument("--dk-salt", metavar="SALT", type=u, default=dk_salt, help="per-directory accesskey salt; used to generate unpredictable URLs to share folders with users who only have the 'get' permission")
ap2.add_argument("--warksalt", metavar="SALT", type=u, default="hunter2", help="up2k file-hash salt; serves no purpose, no reason to change this (but delete all databases if you do)") ap2.add_argument("--warksalt", metavar="SALT", type=u, default="hunter2", help="up2k file-hash salt; serves no purpose, no reason to change this (but delete all databases if you do)")
ap2.add_argument("--show-ah-salt", action="store_true", help="on startup, print the effective value of \033[33m--ah-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)")
ap2.add_argument("--show-fk-salt", action="store_true", help="on startup, print the effective value of \033[33m--fk-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)")
ap2.add_argument("--show-dk-salt", action="store_true", help="on startup, print the effective value of \033[33m--dk-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)")
def add_shutdown(ap): def add_shutdown(ap):
ap2 = ap.add_argument_group("shutdown options") ap2 = ap.add_argument_group('shutdown options')
ap2.add_argument("--ign-ebind", action="store_true", help="continue running even if it's impossible to listen on some of the requested endpoints") ap2.add_argument("--ign-ebind", action="store_true", help="continue running even if it's impossible to listen on some of the requested endpoints")
ap2.add_argument("--ign-ebind-all", action="store_true", help="continue running even if it's impossible to receive connections at all") ap2.add_argument("--ign-ebind-all", action="store_true", help="continue running even if it's impossible to receive connections at all")
ap2.add_argument("--exit", metavar="WHEN", type=u, default="", help="shutdown after \033[33mWHEN\033[0m has finished; [\033[32mcfg\033[0m] config parsing, [\033[32midx\033[0m] volscan + multimedia indexing") ap2.add_argument("--exit", metavar="WHEN", type=u, default="", help="shutdown after \033[33mWHEN\033[0m has finished; [\033[32mcfg\033[0m] config parsing, [\033[32midx\033[0m] volscan + multimedia indexing")
def add_logging(ap): def add_logging(ap):
ap2 = ap.add_argument_group("logging options") ap2 = ap.add_argument_group('logging options')
ap2.add_argument("-q", action="store_true", help="quiet; disable most STDOUT messages") ap2.add_argument("-q", action="store_true", help="quiet; disable most STDOUT messages")
ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile; use .txt for plaintext or .xz for compressed. Example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)") ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile, example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)")
ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR") ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR")
ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR") ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR")
ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster") ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster")
ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup") ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup")
ap2.add_argument("--log-utc", action="store_true", help="do not use local timezone; assume the TZ env-var is UTC (tiny bit faster)") ap2.add_argument("--log-utc", action="store_true", help="do not use local timezone; assume the TZ env-var is UTC (tiny bit faster)")
ap2.add_argument("--log-tdec", metavar="N", type=int, default=3, help="timestamp resolution / number of timestamp decimals") ap2.add_argument("--log-tdec", metavar="N", type=int, default=3, help="timestamp resolution / number of timestamp decimals")
ap2.add_argument("--log-badpwd", metavar="N", type=int, default=2, help="log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed") ap2.add_argument("--log-badpwd", metavar="N", type=int, default=1, help="log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed")
ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs") ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs")
ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling") ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling")
ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all") ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
@ -1503,32 +1326,30 @@ def add_logging(ap):
def add_admin(ap): def add_admin(ap):
ap2 = ap.add_argument_group("admin panel options") ap2 = ap.add_argument_group('admin panel options')
ap2.add_argument("--no-reload", action="store_true", help="disable ?reload=cfg (reload users/volumes/volflags from config file)") ap2.add_argument("--no-reload", action="store_true", help="disable ?reload=cfg (reload users/volumes/volflags from config file)")
ap2.add_argument("--no-rescan", action="store_true", help="disable ?scan (volume reindexing)") ap2.add_argument("--no-rescan", action="store_true", help="disable ?scan (volume reindexing)")
ap2.add_argument("--no-stack", action="store_true", help="disable ?stack (list all stacks)") ap2.add_argument("--no-stack", action="store_true", help="disable ?stack (list all stacks)")
ap2.add_argument("--no-ups-page", action="store_true", help="disable ?ru (list of recent uploads)") ap2.add_argument("--no-ups-page", action="store_true", help="disable ?ru (list of recent uploads)")
ap2.add_argument("--no-up-list", action="store_true", help="don't show list of incoming files in controlpanel") ap2.add_argument("--no-up-list", action="store_true", help="don't show list of incoming files in controlpanel")
ap2.add_argument("--dl-list", metavar="LVL", type=int, default=2, help="who can see active downloads in the controlpanel? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=everyone") ap2.add_argument("--dl-list", metavar="LVL", type=int, default=2, help="who can see active downloads in the controlpanel? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=everyone")
ap2.add_argument("--ups-who", metavar="LVL", type=int, default=2, help="who can see recent uploads on the ?ru page? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=everyone (volflag=ups_who)")
ap2.add_argument("--ups-when", action="store_true", help="let everyone see upload timestamps on the ?ru page, not just admins") ap2.add_argument("--ups-when", action="store_true", help="let everyone see upload timestamps on the ?ru page, not just admins")
def add_thumbnail(ap): def add_thumbnail(ap):
th_ram = (RAM_AVAIL or RAM_TOTAL or 9) * 0.6 th_ram = (RAM_AVAIL or RAM_TOTAL or 9) * 0.6
th_ram = int(max(min(th_ram, 6), 0.3) * 10) / 10 th_ram = int(max(min(th_ram, 6), 1) * 10) / 10
ap2 = ap.add_argument_group("thumbnail options") ap2 = ap.add_argument_group('thumbnail options')
ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails (volflag=dthumb)") ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails (volflag=dthumb)")
ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails (volflag=dvthumb)") ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails (volflag=dvthumb)")
ap2.add_argument("--no-athumb", action="store_true", help="disable audio thumbnails (spectrograms) (volflag=dathumb)") ap2.add_argument("--no-athumb", action="store_true", help="disable audio thumbnails (spectrograms) (volflag=dathumb)")
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)") ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)")
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails") ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails")
ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="convert-to-image timeout in seconds (volflag=convt)") ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="conversion timeout in seconds (volflag=convt)")
ap2.add_argument("--ac-convt", metavar="SEC", type=float, default=150.0, help="convert-to-audio timeout in seconds (volflag=aconvt)")
ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=th_ram, help="max memory usage (GiB) permitted by thumbnailer; not very accurate") ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=th_ram, help="max memory usage (GiB) permitted by thumbnailer; not very accurate")
ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32my\033[0m]=crop, [\033[32mn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)") ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32my\033[0m]=crop, [\033[32mn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)")
ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)") ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)")
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,raw,ff", help="image decoders, in order of preference") ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference")
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output") ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output") ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg output for video thumbs (avoids issues on some FFmpeg builds)") ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg output for video thumbs (avoids issues on some FFmpeg builds)")
@ -1537,47 +1358,29 @@ def add_thumbnail(ap):
ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled") ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled")
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than \033[33m--th-poke\033[0m seconds will get deleted every \033[33m--th-clean\033[0m seconds") ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than \033[33m--th-poke\033[0m seconds will get deleted every \033[33m--th-clean\033[0m seconds")
ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern") ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern")
ap2.add_argument("--th-spec-p", metavar="N", type=u, default=1, help="for music, do spectrograms or embedded coverart? [\033[32m0\033[0m]=only-art, [\033[32m1\033[0m]=prefer-art, [\033[32m2\033[0m]=only-spec")
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
# https://github.com/libvips/libvips # https://github.com/libvips/libvips
# https://stackoverflow.com/a/47612661
# ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:' # ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:'
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow") ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow")
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips") ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips")
ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mrw,nef,orf,pef,raf,raw,sr2,srf,x3f", help="image formats to decode using rawpy") ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg") ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg") ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)") ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz", help="audio/image formats to decompress before passing to ffmpeg")
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz, epub=jpg.epub", help="audio/image formats to decompress before passing to ffmpeg")
def add_transcoding(ap): def add_transcoding(ap):
ap2 = ap.add_argument_group("transcoding options") ap2 = ap.add_argument_group('transcoding options')
ap2.add_argument("--q-opus", metavar="KBPS", type=int, default=128, help="target bitrate for transcoding to opus; set 0 to disable") ap2.add_argument("--q-opus", metavar="KBPS", type=int, default=128, help="target bitrate for transcoding to opus; set 0 to disable")
ap2.add_argument("--q-mp3", metavar="QUALITY", type=u, default="q2", help="target quality for transcoding to mp3, for example [\033[32m192k\033[0m] (CBR) or [\033[32mq0\033[0m] (CQ/CRF, q0=maxquality, q9=smallest); set 0 to disable") ap2.add_argument("--q-mp3", metavar="QUALITY", type=u, default="q2", help="target quality for transcoding to mp3, for example [\033[32m192k\033[0m] (CBR) or [\033[32mq0\033[0m] (CQ/CRF, q0=maxquality, q9=smallest); set 0 to disable")
ap2.add_argument("--allow-wav", action="store_true", help="allow transcoding to wav (lossless, uncompressed)")
ap2.add_argument("--allow-flac", action="store_true", help="allow transcoding to flac (lossless, compressed)")
ap2.add_argument("--no-caf", action="store_true", help="disable transcoding to caf-opus (affects iOS v12~v17), will use mp3 instead")
ap2.add_argument("--no-owa", action="store_true", help="disable transcoding to webm-opus (iOS v18 and later), will use mp3 instead")
ap2.add_argument("--no-acode", action="store_true", help="disable audio transcoding") ap2.add_argument("--no-acode", action="store_true", help="disable audio transcoding")
ap2.add_argument("--no-bacode", action="store_true", help="disable batch audio transcoding by folder download (zip/tar)") ap2.add_argument("--no-bacode", action="store_true", help="disable batch audio transcoding by folder download (zip/tar)")
ap2.add_argument("--ac-maxage", metavar="SEC", type=int, default=86400, help="delete cached transcode output after \033[33mSEC\033[0m seconds") ap2.add_argument("--ac-maxage", metavar="SEC", type=int, default=86400, help="delete cached transcode output after \033[33mSEC\033[0m seconds")
def add_tail(ap):
ap2 = ap.add_argument_group("tailing options (realtime streaming of a growing file)")
ap2.add_argument("--tail-who", metavar="LVL", type=int, default=2, help="who can tail? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=authenticated-with-read-access, [\033[32m3\033[0m]=everyone-with-read-access (volflag=tail_who)")
ap2.add_argument("--tail-cmax", metavar="N", type=int, default=64, help="do not allow starting a new tail if more than \033[33mN\033[0m active downloads")
ap2.add_argument("--tail-tmax", metavar="SEC", type=float, default=0, help="terminate connection after \033[33mSEC\033[0m seconds; [\033[32m0\033[0m]=never (volflag=tail_tmax)")
ap2.add_argument("--tail-rate", metavar="SEC", type=float, default=0.2, help="check for new data every \033[33mSEC\033[0m seconds (volflag=tail_rate)")
ap2.add_argument("--tail-ka", metavar="SEC", type=float, default=3.0, help="send a zerobyte if connection is idle for \033[33mSEC\033[0m seconds to prevent disconnect")
ap2.add_argument("--tail-fd", metavar="SEC", type=float, default=1.0, help="check if file was replaced (new fd) if idle for \033[33mSEC\033[0m seconds (volflag=tail_fd)")
def add_rss(ap): def add_rss(ap):
ap2 = ap.add_argument_group("RSS options") ap2 = ap.add_argument_group('RSS options')
ap2.add_argument("--rss", action="store_true", help="enable RSS output (experimental) (volflag=rss)") ap2.add_argument("--rss", action="store_true", help="enable RSS output (experimental)")
ap2.add_argument("--rss-nf", metavar="HITS", type=int, default=250, help="default number of files to return (url-param 'nf')") ap2.add_argument("--rss-nf", metavar="HITS", type=int, default=250, help="default number of files to return (url-param 'nf')")
ap2.add_argument("--rss-fext", metavar="E,E", type=u, default="", help="default list of file extensions to include (url-param 'fext'); blank=all") ap2.add_argument("--rss-fext", metavar="E,E", type=u, default="", help="default list of file extensions to include (url-param 'fext'); blank=all")
ap2.add_argument("--rss-sort", metavar="ORD", type=u, default="m", help="default sort order (url-param 'sort'); [\033[32mm\033[0m]=last-modified [\033[32mu\033[0m]=upload-time [\033[32mn\033[0m]=filename [\033[32ms\033[0m]=filesize; Uppercase=oldest-first. Note that upload-time is 0 for non-uploaded files") ap2.add_argument("--rss-sort", metavar="ORD", type=u, default="m", help="default sort order (url-param 'sort'); [\033[32mm\033[0m]=last-modified [\033[32mu\033[0m]=upload-time [\033[32mn\033[0m]=filename [\033[32ms\033[0m]=filesize; Uppercase=oldest-first. Note that upload-time is 0 for non-uploaded files")
@ -1585,7 +1388,7 @@ def add_rss(ap):
def add_db_general(ap, hcores): def add_db_general(ap, hcores):
noidx = APPLESAN_TXT if MACOS else "" noidx = APPLESAN_TXT if MACOS else ""
ap2 = ap.add_argument_group("general db options") ap2 = ap.add_argument_group('general db options')
ap2.add_argument("-e2d", action="store_true", help="enable up2k database; this enables file search, upload-undo, improves deduplication") ap2.add_argument("-e2d", action="store_true", help="enable up2k database; this enables file search, upload-undo, improves deduplication")
ap2.add_argument("-e2ds", action="store_true", help="scan writable folders for new files on startup; sets \033[33m-e2d\033[0m") ap2.add_argument("-e2ds", action="store_true", help="scan writable folders for new files on startup; sets \033[33m-e2d\033[0m")
ap2.add_argument("-e2dsa", action="store_true", help="scans all folders on startup; sets \033[33m-e2ds\033[0m") ap2.add_argument("-e2dsa", action="store_true", help="scans all folders on startup; sets \033[33m-e2ds\033[0m")
@ -1593,15 +1396,13 @@ def add_db_general(ap, hcores):
ap2.add_argument("-e2vu", action="store_true", help="on hash mismatch: update the database with the new hash") ap2.add_argument("-e2vu", action="store_true", help="on hash mismatch: update the database with the new hash")
ap2.add_argument("-e2vp", action="store_true", help="on hash mismatch: panic and quit copyparty") ap2.add_argument("-e2vp", action="store_true", help="on hash mismatch: panic and quit copyparty")
ap2.add_argument("--hist", metavar="PATH", type=u, default="", help="where to store volume data (db, thumbs); default is a folder named \".hist\" inside each volume (volflag=hist)") ap2.add_argument("--hist", metavar="PATH", type=u, default="", help="where to store volume data (db, thumbs); default is a folder named \".hist\" inside each volume (volflag=hist)")
ap2.add_argument("--dbpath", metavar="PATH", type=u, default="", help="override where the volume databases are to be placed; default is the same as \033[33m--hist\033[0m (volflag=dbpath)") ap2.add_argument("--no-hash", metavar="PTN", type=u, default="", help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (volflag=nohash)")
ap2.add_argument("--no-hash", metavar="PTN", type=u, default="", help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (must be specified as one big regex, not multiple times) (volflag=nohash)") ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scans (volflag=noidx)")
ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scan (must be specified as one big regex, not multiple times) (volflag=noidx)")
ap2.add_argument("--no-dirsz", action="store_true", help="do not show total recursive size of folders in listings, show inode size instead; slightly faster (volflag=nodirsz)") ap2.add_argument("--no-dirsz", action="store_true", help="do not show total recursive size of folders in listings, show inode size instead; slightly faster (volflag=nodirsz)")
ap2.add_argument("--re-dirsz", action="store_true", help="if the directory-sizes in the UI are bonkers, use this along with \033[33m-e2dsa\033[0m to rebuild the index from scratch") ap2.add_argument("--re-dirsz", action="store_true", help="if the directory-sizes in the UI are bonkers, use this along with \033[33m-e2dsa\033[0m to rebuild the index from scratch")
ap2.add_argument("--no-dhash", action="store_true", help="disable rescan acceleration; do full database integrity check -- makes the db ~5%% smaller and bootup/rescans 3~10x slower") ap2.add_argument("--no-dhash", action="store_true", help="disable rescan acceleration; do full database integrity check -- makes the db ~5%% smaller and bootup/rescans 3~10x slower")
ap2.add_argument("--re-dhash", action="store_true", help="force a cache rebuild on startup; enable this once if it gets out of sync (should never be necessary)") ap2.add_argument("--re-dhash", action="store_true", help="force a cache rebuild on startup; enable this once if it gets out of sync (should never be necessary)")
ap2.add_argument("--no-forget", action="store_true", help="never forget indexed files, even when deleted from disk -- makes it impossible to ever upload the same file twice -- only useful for offloading uploads to a cloud service or something (volflag=noforget)") ap2.add_argument("--no-forget", action="store_true", help="never forget indexed files, even when deleted from disk -- makes it impossible to ever upload the same file twice -- only useful for offloading uploads to a cloud service or something (volflag=noforget)")
ap2.add_argument("--forget-ip", metavar="MIN", type=int, default=0, help="remove uploader-IP from database (and make unpost impossible) \033[33mMIN\033[0m minutes after upload, for GDPR reasons. Default [\033[32m0\033[0m] is never-forget. [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month. (volflag=forget_ip)")
ap2.add_argument("--dbd", metavar="PROFILE", default="wal", help="database durability profile; sets the tradeoff between robustness and speed, see \033[33m--help-dbd\033[0m (volflag=dbd)") ap2.add_argument("--dbd", metavar="PROFILE", default="wal", help="database durability profile; sets the tradeoff between robustness and speed, see \033[33m--help-dbd\033[0m (volflag=dbd)")
ap2.add_argument("--xlink", action="store_true", help="on upload: check all volumes for dupes, not just the target volume (probably buggy, not recommended) (volflag=xlink)") ap2.add_argument("--xlink", action="store_true", help="on upload: check all volumes for dupes, not just the target volume (probably buggy, not recommended) (volflag=xlink)")
ap2.add_argument("--hash-mt", metavar="CORES", type=int, default=hcores, help="num cpu cores to use for file hashing; set 0 or 1 for single-core hashing") ap2.add_argument("--hash-mt", metavar="CORES", type=int, default=hcores, help="num cpu cores to use for file hashing; set 0 or 1 for single-core hashing")
@ -1614,7 +1415,7 @@ def add_db_general(ap, hcores):
def add_db_metadata(ap): def add_db_metadata(ap):
ap2 = ap.add_argument_group("metadata db options") ap2 = ap.add_argument_group('metadata db options')
ap2.add_argument("-e2t", action="store_true", help="enable metadata indexing; makes it possible to search for artist/title/codec/resolution/...") ap2.add_argument("-e2t", action="store_true", help="enable metadata indexing; makes it possible to search for artist/title/codec/resolution/...")
ap2.add_argument("-e2ts", action="store_true", help="scan newly discovered files for metadata on startup; sets \033[33m-e2t\033[0m") ap2.add_argument("-e2ts", action="store_true", help="scan newly discovered files for metadata on startup; sets \033[33m-e2t\033[0m")
ap2.add_argument("-e2tsr", action="store_true", help="delete all metadata from DB and do a full rescan; sets \033[33m-e2ts\033[0m") ap2.add_argument("-e2tsr", action="store_true", help="delete all metadata from DB and do a full rescan; sets \033[33m-e2ts\033[0m")
@ -1624,26 +1425,23 @@ def add_db_metadata(ap):
ap2.add_argument("--mtag-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for tag scanning") ap2.add_argument("--mtag-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for tag scanning")
ap2.add_argument("--mtag-v", action="store_true", help="verbose tag scanning; print errors from mtp subprocesses and such") ap2.add_argument("--mtag-v", action="store_true", help="verbose tag scanning; print errors from mtp subprocesses and such")
ap2.add_argument("--mtag-vv", action="store_true", help="debug mtp settings and mutagen/FFprobe parsers") ap2.add_argument("--mtag-vv", action="store_true", help="debug mtp settings and mutagen/FFprobe parsers")
ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add/replace metadata mapping") ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="add/replace metadata mapping")
ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.); either an entire replacement list, or add/remove stuff on the default-list with +foo or /bar", default=DEF_MTE) ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.); either an entire replacement list, or add/remove stuff on the default-list with +foo or /bar", default=DEF_MTE)
ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.); assign/add/remove same as \033[33m-mte\033[0m", default=DEF_MTH) ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.); assign/add/remove same as \033[33m-mte\033[0m", default=DEF_MTH)
ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m read tag \033[33mM\033[0m using program \033[33mBIN\033[0m to parse the file") ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="read tag \033[33mM\033[0m using program \033[33mBIN\033[0m to parse the file")
def add_txt(ap): def add_txt(ap):
ap2 = ap.add_argument_group("textfile options") ap2 = ap.add_argument_group('textfile options')
ap2.add_argument("--md-hist", metavar="TXT", type=u, default="s", help="where to store old version of markdown files; [\033[32ms\033[0m]=subfolder, [\033[32mv\033[0m]=volume-histpath, [\033[32mn\033[0m]=nope/disabled (volflag=md_hist)")
ap2.add_argument("--txt-eol", metavar="TYPE", type=u, default="", help="enable EOL conversion when writing documents; supported: CRLF, LF (volflag=txt_eol)")
ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="the textfile editor will check for serverside changes every \033[33mSEC\033[0m seconds") ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="the textfile editor will check for serverside changes every \033[33mSEC\033[0m seconds")
ap2.add_argument("-emp", action="store_true", help="enable markdown plugins -- neat but dangerous, big XSS risk") ap2.add_argument("-emp", action="store_true", help="enable markdown plugins -- neat but dangerous, big XSS risk")
ap2.add_argument("--exp", action="store_true", help="enable textfile expansion -- replace {{self.ip}} and such; see \033[33m--help-exp\033[0m (volflag=exp)") ap2.add_argument("--exp", action="store_true", help="enable textfile expansion -- replace {{self.ip}} and such; see \033[33m--help-exp\033[0m (volflag=exp)")
ap2.add_argument("--exp-md", metavar="V,V,V", type=u, default=DEF_EXP, help="comma/space-separated list of placeholders to expand in markdown files; add/remove stuff on the default list with +hdr_foo or /vf.scan (volflag=exp_md)") ap2.add_argument("--exp-md", metavar="V,V,V", type=u, default=DEF_EXP, help="comma/space-separated list of placeholders to expand in markdown files; add/remove stuff on the default list with +hdr_foo or /vf.scan (volflag=exp_md)")
ap2.add_argument("--exp-lg", metavar="V,V,V", type=u, default=DEF_EXP, help="comma/space-separated list of placeholders to expand in prologue/epilogue files (volflag=exp_lg)") ap2.add_argument("--exp-lg", metavar="V,V,V", type=u, default=DEF_EXP, help="comma/space-separated list of placeholders to expand in prologue/epilogue files (volflag=exp_lg)")
ap2.add_argument("--ua-nodoc", metavar="PTN", type=u, default=BAD_BOTS, help="regex of user-agents to reject from viewing documents through ?doc=[...]; disable with [\033[32mno\033[0m] or blank")
def add_og(ap): def add_og(ap):
ap2 = ap.add_argument_group("og / open graph / discord-embed options") ap2 = ap.add_argument_group('og / open graph / discord-embed options')
ap2.add_argument("--og", action="store_true", help="disable hotlinking and return an html document instead; this is required by open-graph, but can also be useful on its own (volflag=og)") ap2.add_argument("--og", action="store_true", help="disable hotlinking and return an html document instead; this is required by open-graph, but can also be useful on its own (volflag=og)")
ap2.add_argument("--og-ua", metavar="RE", type=u, default="", help="only disable hotlinking / engage OG behavior if the useragent matches regex \033[33mRE\033[0m (volflag=og_ua)") ap2.add_argument("--og-ua", metavar="RE", type=u, default="", help="only disable hotlinking / engage OG behavior if the useragent matches regex \033[33mRE\033[0m (volflag=og_ua)")
ap2.add_argument("--og-tpl", metavar="PATH", type=u, default="", help="do not return the regular copyparty html, but instead load the jinja2 template at \033[33mPATH\033[0m (if path contains 'EXT' then EXT will be replaced with the requested file's extension) (volflag=og_tpl)") ap2.add_argument("--og-tpl", metavar="PATH", type=u, default="", help="do not return the regular copyparty html, but instead load the jinja2 template at \033[33mPATH\033[0m (if path contains 'EXT' then EXT will be replaced with the requested file's extension) (volflag=og_tpl)")
@ -1661,25 +1459,19 @@ def add_og(ap):
def add_ui(ap, retry): def add_ui(ap, retry):
THEMES = 10 ap2 = ap.add_argument_group('ui options')
ap2 = ap.add_argument_group("ui options")
ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)") ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)") ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)")
ap2.add_argument("--localtime", action="store_true", help="default to local timezone instead of UTC") ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language; one of the following: \033[32meng nor chi\033[0m")
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language, for example \033[32meng\033[0m / \033[32mnor\033[0m / ...") ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..7)")
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..%d)" % (THEMES - 1,)) ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed")
ap2.add_argument("--themes", metavar="NUM", type=int, default=THEMES, help="number of themes installed")
ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent") ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent")
ap2.add_argument("--sort", metavar="C,C,C", type=u, default="href", help="default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: \033[32mhref -href ext sz ts tags/Album tags/.tn\033[0m (volflag=sort)") ap2.add_argument("--sort", metavar="C,C,C", type=u, default="href", help="default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: \033[32mhref -href ext sz ts tags/Album tags/.tn\033[0m (volflag=sort)")
ap2.add_argument("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)") ap2.add_argument("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)")
ap2.add_argument("--hsortn", metavar="N", type=int, default=2, help="number of sorting rules to include in media URLs by default (volflag=hsortn)") ap2.add_argument("--hsortn", metavar="N", type=int, default=2, help="number of sorting rules to include in media URLs by default (volflag=hsortn)")
ap2.add_argument("--see-dots", action="store_true", help="default-enable seeing dotfiles; only takes effect if user has the necessary permissions") ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files matching \033[33mREGEX\033[0m in file list. Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)")
ap2.add_argument("--qdel", metavar="LVL", type=int, default=2, help="number of confirmations to show when deleting files (2/1/0)")
ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files/folders matching \033[33mREGEX\033[0m in file list. WARNING: Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)")
ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable") ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable")
ap2.add_argument("--ext-th", metavar="E=VP", type=u, action="append", help="\033[34mREPEATABLE:\033[0m use thumbnail-image \033[33mVP\033[0m for file-extension \033[33mE\033[0m, example: [\033[32mexe=/.res/exe.png\033[0m] (volflag=ext_th)") ap2.add_argument("--mpmc", metavar="URL", type=u, default="", help="change the mediaplayer-toggle mouse cursor; URL to a folder with {2..5}.png inside (or disable with [\033[32m.\033[0m])")
ap2.add_argument("--mpmc", type=u, default="", help=argparse.SUPPRESS)
ap2.add_argument("--spinner", metavar="TXT", type=u, default="🌲", help="\033[33memoji\033[0m or \033[33memoji,css\033[0m Example: [\033[32m🥖,padding:0\033[0m]")
ap2.add_argument("--css-browser", metavar="L", type=u, default="", help="URL to additional CSS to include in the filebrowser html") ap2.add_argument("--css-browser", metavar="L", type=u, default="", help="URL to additional CSS to include in the filebrowser html")
ap2.add_argument("--js-browser", metavar="L", type=u, default="", help="URL to additional JS to include in the filebrowser html") ap2.add_argument("--js-browser", metavar="L", type=u, default="", help="URL to additional JS to include in the filebrowser html")
ap2.add_argument("--js-other", metavar="L", type=u, default="", help="URL to additional JS to include in all other pages") ap2.add_argument("--js-other", metavar="L", type=u, default="", help="URL to additional JS to include in all other pages")
@ -1689,22 +1481,20 @@ def add_ui(ap, retry):
ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)") ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)")
ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents") ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)") ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-nb\033[0m") ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-np\033[0m")
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)") ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)")
ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on") ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on") ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
ap2.add_argument("--ctl-re", metavar="SEC", type=int, default=1, help="the controlpanel Refresh-button will autorefresh every SEC; [\033[32m0\033[0m] = just once")
ap2.add_argument("--md-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to allow in the iframe 'sandbox' attribute for README.md docs (volflag=md_sbf); see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox") ap2.add_argument("--md-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to allow in the iframe 'sandbox' attribute for README.md docs (volflag=md_sbf); see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox")
ap2.add_argument("--lg-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to allow in the iframe 'sandbox' attribute for prologue/epilogue docs (volflag=lg_sbf)") ap2.add_argument("--lg-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to allow in the iframe 'sandbox' attribute for prologue/epilogue docs (volflag=lg_sbf)")
ap2.add_argument("--md-sba", metavar="TXT", type=u, default="", help="the value of the iframe 'allow' attribute for README.md docs, for example [\033[32mfullscreen\033[0m] (volflag=md_sba)") ap2.add_argument("--md-sba", metavar="TXT", type=u, default="", help="the value of the iframe 'allow' attribute for README.md docs, for example [\033[32mfullscreen\033[0m] (volflag=md_sba)")
ap2.add_argument("--lg-sba", metavar="TXT", type=u, default="", help="the value of the iframe 'allow' attribute for prologue/epilogue docs (volflag=lg_sba); see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy#iframes") ap2.add_argument("--lg-sba", metavar="TXT", type=u, default="", help="the value of the iframe 'allow' attribute for prologue/epilogue docs (volflag=lg_sba); see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy#iframes")
ap2.add_argument("--no-sb-md", action="store_true", help="don't sandbox README/PREADME.md documents (volflags: no_sb_md | sb_md)") ap2.add_argument("--no-sb-md", action="store_true", help="don't sandbox README/PREADME.md documents (volflags: no_sb_md | sb_md)")
ap2.add_argument("--no-sb-lg", action="store_true", help="don't sandbox prologue/epilogue docs (volflags: no_sb_lg | sb_lg); enables non-js support") ap2.add_argument("--no-sb-lg", action="store_true", help="don't sandbox prologue/epilogue docs (volflags: no_sb_lg | sb_lg); enables non-js support")
ap2.add_argument("--have-unlistc", action="store_true", help=argparse.SUPPRESS)
def add_debug(ap): def add_debug(ap):
ap2 = ap.add_argument_group("debug options") ap2 = ap.add_argument_group('debug options')
ap2.add_argument("--vc", action="store_true", help="verbose config file parser (explain config)") ap2.add_argument("--vc", action="store_true", help="verbose config file parser (explain config)")
ap2.add_argument("--cgen", action="store_true", help="generate config file from current config (best-effort; probably buggy)") ap2.add_argument("--cgen", action="store_true", help="generate config file from current config (best-effort; probably buggy)")
ap2.add_argument("--deps", action="store_true", help="list information about detected optional dependencies") ap2.add_argument("--deps", action="store_true", help="list information about detected optional dependencies")
@ -1742,9 +1532,9 @@ def run_argparse(
cert_path = os.path.join(E.cfg, "cert.pem") cert_path = os.path.join(E.cfg, "cert.pem")
fk_salt = get_salt("fk", 18) fk_salt = get_fk_salt()
dk_salt = get_salt("dk", 30) dk_salt = get_dk_salt()
ah_salt = get_salt("ah", 18) ah_salt = get_ah_salt()
# alpine peaks at 5 threads for some reason, # alpine peaks at 5 threads for some reason,
# all others scale past that (but try to avoid SMT), # all others scale past that (but try to avoid SMT),
@ -1786,7 +1576,6 @@ def run_argparse(
add_hooks(ap) add_hooks(ap)
add_stats(ap) add_stats(ap)
add_txt(ap) add_txt(ap)
add_tail(ap)
add_og(ap) add_og(ap)
add_ui(ap, retry) add_ui(ap, retry)
add_admin(ap) add_admin(ap)
@ -1869,7 +1658,16 @@ def main(argv: Optional[list[str]] = None) -> None:
ensure_webdeps() ensure_webdeps()
argv = expand_cfg(argv) for k, v in zip(argv[1:], argv[2:]):
if k == "-c" and os.path.isfile(v):
supp = args_from_cfg(v)
argv.extend(supp)
for k in argv[1:]:
v = k[2:]
if k.startswith("-c") and v and os.path.isfile(v):
supp = args_from_cfg(v)
argv.extend(supp)
deprecated: list[tuple[str, str]] = [ deprecated: list[tuple[str, str]] = [
("--salt", "--warksalt"), ("--salt", "--warksalt"),
@ -1895,7 +1693,7 @@ def main(argv: Optional[list[str]] = None) -> None:
argv[idx] = nk + ov argv[idx] = nk + ov
time.sleep(2) time.sleep(2)
da = len(argv) == 1 and not CFG_DEF da = len(argv) == 1
try: try:
if da: if da:
argv.extend(["--qr"]) argv.extend(["--qr"])

View file

@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
VERSION = (1, 19, 4) VERSION = (1, 16, 9)
CODENAME = "usernames" CODENAME = "COPYparty"
BUILD_DT = (2025, 8, 17) BUILD_DT = (2025, 1, 22)
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:
if os.path.isdir(bname):
break
todo.append(bname)
bname = os.path.dirname(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: try:
os.mkdir(zb, mode) os.makedirs(bname, mode)
if chown:
os.chown(zb, vf["uid"], vf["gid"])
except:
if os.path.isdir(zb):
continue
raise
return True return True
except:
if not exist_ok or not os.path.isdir(bname):
raise
return False
def mkdir(p: str, mode: int = 0o755) -> None: def mkdir(p: str, mode: int = 0o755) -> None:

View file

@ -1,11 +1,13 @@
import calendar import calendar
import errno import errno
import filecmp
import json import json
import os import os
import shutil
import time import time
from .__init__ import ANYWIN from .__init__ import ANYWIN
from .util import Netdev, atomic_move, load_resource, runcmd, wunlink from .util import Netdev, load_resource, runcmd, wrename, wunlink
HAVE_CFSSL = not os.environ.get("PRTY_NO_CFSSL") HAVE_CFSSL = not os.environ.get("PRTY_NO_CFSSL")
@ -120,7 +122,7 @@ def _gen_ca(log: "RootLogger", args):
wunlink(nlog, bname + ".key", VF) wunlink(nlog, bname + ".key", VF)
except: except:
pass pass
atomic_move(nlog, bname + "-key.pem", bname + ".key", VF) wrename(nlog, bname + "-key.pem", bname + ".key", VF)
wunlink(nlog, bname + ".csr", VF) wunlink(nlog, bname + ".csr", VF)
log("cert", "new ca OK", 2) log("cert", "new ca OK", 2)
@ -213,7 +215,7 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
wunlink(nlog, bname + ".key", VF) wunlink(nlog, bname + ".key", VF)
except: except:
pass pass
atomic_move(nlog, bname + "-key.pem", bname + ".key", VF) wrename(nlog, bname + "-key.pem", bname + ".key", VF)
wunlink(nlog, bname + ".csr", VF) wunlink(nlog, bname + ".csr", VF)
with open(os.path.join(args.crt_dir, "ca.pem"), "rb") as f: with open(os.path.join(args.crt_dir, "ca.pem"), "rb") as f:

View file

@ -5,9 +5,6 @@ from __future__ import print_function, unicode_literals
zs = "a c e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vp e2vu ed emp i j lo mcr mte mth mtm mtp nb nc nid nih nth nw p q s ss sss v z zv" zs = "a c e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vp e2vu ed emp i j lo mcr mte mth mtm mtp nb nc nid nih nth nw p q s ss sss v z zv"
onedash = set(zs.split()) onedash = set(zs.split())
# verify that all volflags are documented here:
# grep volflag= __main__.py | sed -r 's/.*volflag=//;s/\).*//' | sort | uniq | while IFS= read -r x; do grep -E "\"$x(=[^ \"]+)?\": \"" cfg.py || printf '%s\n' "$x"; done
def vf_bmap() -> dict[str, str]: def vf_bmap() -> dict[str, str]:
"""argv-to-volflag: simple bools""" """argv-to-volflag: simple bools"""
@ -22,7 +19,6 @@ def vf_bmap() -> dict[str, str]:
"no_forget": "noforget", "no_forget": "noforget",
"no_pipe": "nopipe", "no_pipe": "nopipe",
"no_robots": "norobots", "no_robots": "norobots",
"no_tail": "notail",
"no_thumb": "dthumb", "no_thumb": "dthumb",
"no_vthumb": "dvthumb", "no_vthumb": "dvthumb",
"no_athumb": "dathumb", "no_athumb": "dathumb",
@ -44,7 +40,6 @@ def vf_bmap() -> dict[str, str]:
"gsel", "gsel",
"hardlink", "hardlink",
"magic", "magic",
"no_db_ip",
"no_sb_md", "no_sb_md",
"no_sb_lg", "no_sb_lg",
"nsort", "nsort",
@ -52,14 +47,10 @@ def vf_bmap() -> dict[str, str]:
"og_no_head", "og_no_head",
"og_s_title", "og_s_title",
"rand", "rand",
"reflink",
"rmagic",
"rss", "rss",
"wo_up_readme",
"xdev", "xdev",
"xlink", "xlink",
"xvol", "xvol",
"zipmaxu",
): ):
ret[k] = k ret[k] = k
return ret return ret
@ -68,7 +59,6 @@ def vf_bmap() -> dict[str, str]:
def vf_vmap() -> dict[str, str]: def vf_vmap() -> dict[str, str]:
"""argv-to-volflag: simple values""" """argv-to-volflag: simple values"""
ret = { ret = {
"ac_convt": "aconvt",
"no_hash": "nohash", "no_hash": "nohash",
"no_idx": "noidx", "no_idx": "noidx",
"re_maxage": "scan", "re_maxage": "scan",
@ -79,20 +69,14 @@ def vf_vmap() -> dict[str, str]:
"th_x3": "th3x", "th_x3": "th3x",
} }
for k in ( for k in (
"bup_ck",
"chmod_d",
"chmod_f",
"dbd", "dbd",
"forget_ip",
"hsortn", "hsortn",
"html_head", "html_head",
"lg_sbf", "lg_sbf",
"md_sbf", "md_sbf",
"lg_sba", "lg_sba",
"md_sba", "md_sba",
"md_hist",
"nrand", "nrand",
"u2ow",
"og_desc", "og_desc",
"og_site", "og_site",
"og_th", "og_th",
@ -102,29 +86,13 @@ def vf_vmap() -> dict[str, str]:
"og_title_i", "og_title_i",
"og_tpl", "og_tpl",
"og_ua", "og_ua",
"put_ck",
"put_name",
"mv_retry", "mv_retry",
"rm_retry", "rm_retry",
"sort", "sort",
"tail_fd",
"tail_rate",
"tail_tmax",
"tail_who",
"tcolor", "tcolor",
"th_spec_p",
"txt_eol",
"unlist", "unlist",
"u2abort", "u2abort",
"u2ts", "u2ts",
"uid",
"gid",
"unp_who",
"ups_who",
"zip_who",
"zipmaxn",
"zipmaxs",
"zipmaxt",
): ):
ret[k] = k ret[k] = k
return ret return ret
@ -136,7 +104,6 @@ def vf_cmap() -> dict[str, str]:
for k in ( for k in (
"exp_lg", "exp_lg",
"exp_md", "exp_md",
"ext_th",
"mte", "mte",
"mth", "mth",
"mtp", "mtp",
@ -175,24 +142,15 @@ flagcats = {
"dedup": "enable symlink-based file deduplication", "dedup": "enable symlink-based file deduplication",
"hardlink": "enable hardlink-based file deduplication,\nwith fallback on symlinks when that is impossible", "hardlink": "enable hardlink-based file deduplication,\nwith fallback on symlinks when that is impossible",
"hardlinkonly": "dedup with hardlink only, never symlink;\nmake a full copy if hardlink is impossible", "hardlinkonly": "dedup with hardlink only, never symlink;\nmake a full copy if hardlink is impossible",
"reflink": "enable reflink-based file deduplication,\nwith fallback on full copy when that is impossible",
"safededup": "verify on-disk data before using it for dedup", "safededup": "verify on-disk data before using it for dedup",
"noclone": "take dupe data from clients, even if available on HDD", "noclone": "take dupe data from clients, even if available on HDD",
"nodupe": "rejects existing files (instead of linking/cloning them)", "nodupe": "rejects existing files (instead of linking/cloning them)",
"chmod_d=755": "unix-permission for new dirs/folders",
"chmod_f=644": "unix-permission for new files",
"uid=573": "change owner of new files/folders to unix-user 573",
"gid=999": "change owner of new files/folders to unix-group 999",
"sparse": "force use of sparse files, mainly for s3-backed storage", "sparse": "force use of sparse files, mainly for s3-backed storage",
"nosparse": "deny use of sparse files, mainly for slow storage", "nosparse": "deny use of sparse files, mainly for slow storage",
"daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files", "daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
"nosub": "forces all uploads into the top folder of the vfs", "nosub": "forces all uploads into the top folder of the vfs",
"magic": "enables filetype detection for nameless uploads", "magic": "enables filetype detection for nameless uploads",
"put_name": "fallback filename for nameless uploads", "gz": "allows server-side gzip of uploads with ?gz (also c,xz)",
"put_ck": "default checksum-hasher for PUT/WebDAV uploads",
"bup_ck": "default checksum-hasher for bup/basic uploads",
"gz": "allows server-side gzip compression of uploads with ?gz",
"xz": "allows server-side lzma compression of uploads with ?xz",
"pk": "forces server-side compression, optional arg: xz,9", "pk": "forces server-side compression, optional arg: xz,9",
}, },
"upload rules": { "upload rules": {
@ -201,10 +159,8 @@ flagcats = {
"vmaxb=1g": "total volume size max 1 GiB (suffixes: b, k, m, g, t)", "vmaxb=1g": "total volume size max 1 GiB (suffixes: b, k, m, g, t)",
"vmaxn=4k": "max 4096 files in volume (suffixes: b, k, m, g, t)", "vmaxn=4k": "max 4096 files in volume (suffixes: b, k, m, g, t)",
"medialinks": "return medialinks for non-up2k uploads (not hotlinks)", "medialinks": "return medialinks for non-up2k uploads (not hotlinks)",
"wo_up_readme": "write-only users can upload logues without getting renamed",
"rand": "force randomized filenames, 9 chars long by default", "rand": "force randomized filenames, 9 chars long by default",
"nrand=N": "randomized filenames are N chars long", "nrand=N": "randomized filenames are N chars long",
"u2ow=N": "overwrite existing files? 0=no 1=if-older 2=always",
"u2ts=fc": "[f]orce [c]lient-last-modified or [u]pload-time", "u2ts=fc": "[f]orce [c]lient-last-modified or [u]pload-time",
"u2abort=1": "allow aborting unfinished uploads? 0=no 1=strict 2=ip-chk 3=acct-chk", "u2abort=1": "allow aborting unfinished uploads? 0=no 1=strict 2=ip-chk 3=acct-chk",
"sz=1k-3m": "allow filesizes between 1 KiB and 3MiB", "sz=1k-3m": "allow filesizes between 1 KiB and 3MiB",
@ -221,24 +177,17 @@ flagcats = {
"e2dsa": "scans all folders for new files on startup; also sets -e2d", "e2dsa": "scans all folders for new files on startup; also sets -e2d",
"e2t": "enable multimedia indexing; makes it possible to search for tags", "e2t": "enable multimedia indexing; makes it possible to search for tags",
"e2ts": "scan existing files for tags on startup; also sets -e2t", "e2ts": "scan existing files for tags on startup; also sets -e2t",
"e2tsr": "delete all metadata from DB (full rescan); also sets -e2ts", "e2tsa": "delete all metadata from DB (full rescan); also sets -e2ts",
"d2ts": "disables metadata collection for existing files", "d2ts": "disables metadata collection for existing files",
"e2v": "verify integrity on startup by hashing files and comparing to db",
"e2vu": "when e2v fails, update the db (assume on-disk files are good)",
"e2vp": "when e2v fails, panic and quit copyparty",
"d2ds": "disables onboot indexing, overrides -e2ds*", "d2ds": "disables onboot indexing, overrides -e2ds*",
"d2t": "disables metadata collection, overrides -e2t*", "d2t": "disables metadata collection, overrides -e2t*",
"d2v": "disables file verification, overrides -e2v*", "d2v": "disables file verification, overrides -e2v*",
"d2d": "disables all database stuff, overrides -e2*", "d2d": "disables all database stuff, overrides -e2*",
"hist=/tmp/cdb": "puts thumbnails and indexes at that location", "hist=/tmp/cdb": "puts thumbnails and indexes at that location",
"dbpath=/tmp/cdb": "puts indexes at that location",
"landmark=foo": "disable db if file foo doesn't exist",
"scan=60": "scan for new files every 60sec, same as --re-maxage", "scan=60": "scan for new files every 60sec, same as --re-maxage",
"nohash=\\.iso$": "skips hashing file contents if path matches *.iso", "nohash=\\.iso$": "skips hashing file contents if path matches *.iso",
"noidx=\\.iso$": "fully ignores the contents at paths matching *.iso", "noidx=\\.iso$": "fully ignores the contents at paths matching *.iso",
"noforget": "don't forget files when deleted from disk", "noforget": "don't forget files when deleted from disk",
"forget_ip=43200": "forget uploader-IP after 30 days (GDPR)",
"no_db_ip": "never store uploader-IP in the db; disables unpost",
"fat32": "avoid excessive reindexing on android sdcardfs", "fat32": "avoid excessive reindexing on android sdcardfs",
"dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff", "dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
"xlink": "cross-volume dupe detection / linking (dangerous)", "xlink": "cross-volume dupe detection / linking (dangerous)",
@ -249,8 +198,6 @@ flagcats = {
"srch_excl": "exclude search results with URL matching this regex", "srch_excl": "exclude search results with URL matching this regex",
}, },
'database, audio tags\n"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ...': { 'database, audio tags\n"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ...': {
"mte=artist,title": "media-tags to index/display",
"mth=fmt,res,ac": "media-tags to hide by default",
"mtp=.bpm=f,audio-bpm.py": 'uses the "audio-bpm.py" program to\ngenerate ".bpm" tags from uploads (f = overwrite tags)', "mtp=.bpm=f,audio-bpm.py": 'uses the "audio-bpm.py" program to\ngenerate ".bpm" tags from uploads (f = overwrite tags)',
"mtp=ahash,vhash=media-hash.py": "collects two tags at once", "mtp=ahash,vhash=media-hash.py": "collects two tags at once",
}, },
@ -263,10 +210,7 @@ flagcats = {
"thsize": "thumbnail res; WxH", "thsize": "thumbnail res; WxH",
"crop": "center-cropping (y/n/fy/fn)", "crop": "center-cropping (y/n/fy/fn)",
"th3x": "3x resolution (y/n/fy/fn)", "th3x": "3x resolution (y/n/fy/fn)",
"convt": "convert-to-image timeout in seconds", "convt": "conversion timeout in seconds",
"aconvt": "convert-to-audio timeout in seconds",
"th_spec_p=1": "make spectrograms? 0=never 1=fallback 2=always",
"ext_th=s=/b.png": "use /b.png as thumbnail for file-extension s",
}, },
"handlers\n(better explained in --help-handlers)": { "handlers\n(better explained in --help-handlers)": {
"on404=PY": "handle 404s by executing PY file", "on404=PY": "handle 404s by executing PY file",
@ -289,16 +233,10 @@ flagcats = {
"grid": "show grid/thumbnails by default", "grid": "show grid/thumbnails by default",
"gsel": "select files in grid by ctrl-click", "gsel": "select files in grid by ctrl-click",
"sort": "default sort order", "sort": "default sort order",
"nsort": "natural-sort of leading digits in filenames",
"hsortn": "number of sort-rules to add to media URLs",
"unlist": "dont list files matching REGEX", "unlist": "dont list files matching REGEX",
"html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH", "html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH",
"tcolor=#fc0": "theme color (a hint for webbrowsers, discord, etc.)",
"nodirsz": "don't show total folder size",
"robots": "allows indexing by search engines (default)", "robots": "allows indexing by search engines (default)",
"norobots": "kindly asks search engines to leave", "norobots": "kindly asks search engines to leave",
"unlistcr": "don't list read-access in controlpanel",
"unlistcw": "don't list write-access in controlpanel",
"no_sb_md": "disable js sandbox for markdown files", "no_sb_md": "disable js sandbox for markdown files",
"no_sb_lg": "disable js sandbox for prologue/epilogue", "no_sb_lg": "disable js sandbox for prologue/epilogue",
"sb_md": "enable js sandbox for markdown files (default)", "sb_md": "enable js sandbox for markdown files (default)",
@ -309,51 +247,10 @@ flagcats = {
"lg_sba": "value of iframe allow-prop for *logue-sandbox", "lg_sba": "value of iframe allow-prop for *logue-sandbox",
"nohtml": "return html and markdown as text/html", "nohtml": "return html and markdown as text/html",
}, },
"opengraph (discord embeds)": {
"og": "enable OG (disables hotlinking)",
"og_site": "sitename; defaults to --name, disable with '-'",
"og_desc": "description text for all files; disable with '-'",
"og_th=jf": "thumbnail format; j / jf / jf3 / w / w3 / ...",
"og_title_a": "audio title format; default: {{ artist }} - {{ title }}",
"og_title_v": "video title format; default: {{ title }}",
"og_title_i": "image title format; default: {{ title }}",
"og_title=foo": "fallback title if there's nothing in the db",
"og_s_title": "force default title; do not read from tags",
"og_tpl": "custom html; see --og-tpl in --help",
"og_no_head": "you want to add tags manually with og_tpl",
"og_ua": "if defined: only send OG html if useragent matches this regex",
},
"textfiles": {
"md_hist": "where to put markdown backups; s=subfolder, v=volHist, n=nope",
"exp": "enable textfile expansion; see --help-exp",
"exp_md": "placeholders to expand in markdown files; see --help",
"exp_lg": "placeholders to expand in prologue/epilogue; see --help",
"txt_eol=lf": "enable EOL conversion when writing docs (LF or CRLF)",
},
"tailing": {
"notail": "disable ?tail (download a growing file continuously)",
"tail_fd=1": "check if file was replaced (new fd) every 1 sec",
"tail_rate=0.2": "check for new data every 0.2 sec",
"tail_tmax=30": "kill connection after 30 sec",
"tail_who=2": "restrict ?tail access (1=admins,2=authed,3=everyone)",
},
"others": { "others": {
"dots": "allow all users with read-access to\nenable the option to show dotfiles in listings", "dots": "allow all users with read-access to\nenable the option to show dotfiles in listings",
"fk=8": 'generates per-file accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes', "fk=8": 'generates per-file accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes',
"fka=8": 'generates slightly weaker per-file accesskeys,\nwhich are then required at the "g" permission;\nnot affected by filesize or inode numbers', "fka=8": 'generates slightly weaker per-file accesskeys,\nwhich are then required at the "g" permission;\nnot affected by filesize or inode numbers',
"dk=8": 'generates per-directory accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes',
"dks": "per-directory accesskeys allow browsing into subdirs",
"dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so',
"rss": "allow '?rss' URL suffix (experimental)",
"rmagic": "expensive analysis for mimetype accuracy",
"unp_who=2": "unpost only if same... 1=ip+name, 2=ip, 3=name",
"ups_who=2": "restrict viewing the list of recent uploads",
"zip_who=2": "restrict access to download-as-zip/tar",
"zipmaxn=9k": "reject download-as-zip if more than 9000 files",
"zipmaxs=2g": "reject download-as-zip if size over 2 GiB",
"zipmaxt=no": "reply with 'no' if download-as-zip exceeds max",
"zipmaxu": "zip-size-limit does not apply to authenticated users",
"nopipe": "disable race-the-beam (download unfinished uploads)",
"mv_retry": "ms-windows: timeout for renaming busy files", "mv_retry": "ms-windows: timeout for renaming busy files",
"rm_retry": "ms-windows: timeout for deleting busy files", "rm_retry": "ms-windows: timeout for deleting busy files",
"davauth": "ask webdav clients to login for all folders", "davauth": "ask webdav clients to login for all folders",
@ -363,10 +260,3 @@ flagcats = {
flagdescs = {k.split("=")[0]: v for tab in flagcats.values() for k, v in tab.items()} flagdescs = {k.split("=")[0]: v for tab in flagcats.values() for k, v in tab.items()}
if True: # so it gets removed in release-builds
for fun in [vf_bmap, vf_cmap, vf_vmap]:
for k in fun().values():
if k not in flagdescs:
raise Exception("undocumented volflag: " + k)

View file

@ -65,9 +65,6 @@ DXMLParser = _DXMLParser
def parse_xml(txt: str) -> ET.Element: def parse_xml(txt: str) -> ET.Element:
"""
Parse XML into an xml.etree.ElementTree.Element while defusing some unsafe parts.
"""
parser = DXMLParser() parser = DXMLParser()
parser.feed(txt) parser.feed(txt)
return parser.close() # type: ignore return parser.close() # type: ignore

View file

@ -78,7 +78,7 @@ class Fstab(object):
return vid return vid
def build_fallback(self) -> None: def build_fallback(self) -> None:
self.tab = VFS(self.log_func, "idk", "/", "/", AXS(), {}) self.tab = VFS(self.log_func, "idk", "/", AXS(), {})
self.trusted = False self.trusted = False
def build_tab(self) -> None: def build_tab(self) -> None:
@ -111,10 +111,9 @@ class Fstab(object):
tab1.sort(key=lambda x: (len(x[0]), x[0])) tab1.sort(key=lambda x: (len(x[0]), x[0]))
path1, fs1 = tab1[0] path1, fs1 = tab1[0]
tab = VFS(self.log_func, fs1, path1, path1, AXS(), {}) tab = VFS(self.log_func, fs1, path1, AXS(), {})
for path, fs in tab1[1:]: for path, fs in tab1[1:]:
zs = path.lstrip("/") tab.add(fs, path.lstrip("/"))
tab.add(fs, zs, zs)
self.tab = tab self.tab = tab
self.srctab = srctab self.srctab = srctab
@ -131,10 +130,9 @@ class Fstab(object):
if not self.trusted: if not self.trusted:
# no mtab access; have to build as we go # no mtab access; have to build as we go
if "/" in rem: if "/" in rem:
zs = os.path.join(vn.vpath, rem.split("/")[0]) self.tab.add("idk", os.path.join(vn.vpath, rem.split("/")[0]))
self.tab.add("idk", zs, zs)
if rem: if rem:
self.tab.add(nval, path, path) self.tab.add(nval, path)
else: else:
vn.realpath = nval vn.realpath = nval

View file

@ -19,7 +19,6 @@ from .__init__ import PY2, TYPE_CHECKING
from .authsrv import VFS from .authsrv import VFS
from .bos import bos from .bos import bos
from .util import ( from .util import (
FN_EMB,
VF_CAREFUL, VF_CAREFUL,
Daemon, Daemon,
ODict, ODict,
@ -31,7 +30,6 @@ from .util import (
relchk, relchk,
runhook, runhook,
sanitize_fn, sanitize_fn,
set_fperms,
vjoin, vjoin,
wunlink, wunlink,
) )
@ -83,12 +81,7 @@ class FtpAuth(DummyAuthorizer):
uname = "*" uname = "*"
if username != "anonymous": if username != "anonymous":
uname = "" uname = ""
if args.usernames: for zs in (password, username):
alts = ["%s:%s" % (username, password)]
else:
alts = password, username
for zs in alts:
zs = asrv.iacct.get(asrv.ah.hash(zs), "") zs = asrv.iacct.get(asrv.ah.hash(zs), "")
if zs: if zs:
uname = zs uname = zs
@ -96,10 +89,6 @@ class FtpAuth(DummyAuthorizer):
if args.ipu and uname == "*": if args.ipu and uname == "*":
uname = args.ipu_iu[args.ipu_nm.map(ip)] uname = args.ipu_iu[args.ipu_nm.map(ip)]
if args.ipr and uname in args.ipr_u:
if not args.ipr_u[uname].map(ip):
logging.warning("username [%s] rejected by --ipr", uname)
uname = "*"
if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)): if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):
g = self.hub.gpwd g = self.hub.gpwd
@ -181,16 +170,6 @@ class FtpFs(AbstractedFS):
fn = sanitize_fn(fn or "", "") fn = sanitize_fn(fn or "", "")
vpath = vjoin(rd, fn) vpath = vjoin(rd, fn)
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d) vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
if (
w
and fn.lower() in FN_EMB
and self.h.uname not in vfs.axs.uread
and "wo_up_readme" not in vfs.flags
):
fn = "_wo_" + fn
vpath = vjoin(rd, fn)
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
if not vfs.realpath: if not vfs.realpath:
t = "No filesystem mounted at [{}]" t = "No filesystem mounted at [{}]"
raise FSE(t.format(vpath)) raise FSE(t.format(vpath))
@ -239,7 +218,7 @@ class FtpFs(AbstractedFS):
r = "r" in mode r = "r" in mode
w = "w" in mode or "a" in mode or "+" in mode w = "w" in mode or "a" in mode or "+" in mode
ap, vfs, _ = self.rv2a(filename, r, w) ap = self.rv2a(filename, r, w)[0]
self.validpath(ap) self.validpath(ap)
if w: if w:
try: try:
@ -271,11 +250,7 @@ class FtpFs(AbstractedFS):
wunlink(self.log, ap, VF_CAREFUL) wunlink(self.log, ap, VF_CAREFUL)
ret = open(fsenc(ap), mode, self.args.iobuf) return open(fsenc(ap), mode, self.args.iobuf)
if w and "fperms" in vfs.flags:
set_fperms(ret, vfs.flags)
return ret
def chdir(self, path: str) -> None: def chdir(self, path: str) -> None:
nwd = join(self.cwd, path) nwd = join(self.cwd, path)
@ -289,12 +264,9 @@ class FtpFs(AbstractedFS):
# returning 550 is library-default and suitable # returning 550 is library-default and suitable
raise FSE("No such file or directory") raise FSE("No such file or directory")
if vfs.realpath:
avfs = vfs.chk_ap(ap, st) avfs = vfs.chk_ap(ap, st)
if not avfs: if not avfs:
raise FSE("Permission denied", 1) raise FSE("Permission denied", 1)
else:
avfs = vfs
self.cwd = nwd self.cwd = nwd
( (
@ -309,8 +281,8 @@ class FtpFs(AbstractedFS):
) = avfs.can_access("", self.h.uname) ) = avfs.can_access("", self.h.uname)
def mkdir(self, path: str) -> None: def mkdir(self, path: str) -> None:
ap, vfs, _ = self.rv2a(path, w=True) ap = self.rv2a(path, w=True)[0]
bos.makedirs(ap, vf=vfs.flags) # filezilla expects this bos.makedirs(ap) # filezilla expects this
def listdir(self, path: str) -> list[str]: def listdir(self, path: str) -> list[str]:
vpath = join(self.cwd, path) vpath = join(self.cwd, path)
@ -409,12 +381,8 @@ class FtpFs(AbstractedFS):
return st return st
def utime(self, path: str, timeval: float) -> None: def utime(self, path: str, timeval: float) -> None:
try:
ap = self.rv2a(path, w=True)[0] ap = self.rv2a(path, w=True)[0]
return bos.utime(ap, (int(time.time()), int(timeval))) return bos.utime(ap, (timeval, timeval))
except Exception as ex:
logging.error("ftp.utime: %s, %r", ex, ex)
raise
def lstat(self, path: str) -> os.stat_result: def lstat(self, path: str) -> os.stat_result:
ap = self.rv2a(path)[0] ap = self.rv2a(path)[0]
@ -503,11 +471,7 @@ class FtpHandler(FTPHandler):
def ftp_STOR(self, file: str, mode: str = "w") -> Any: def ftp_STOR(self, file: str, mode: str = "w") -> Any:
# Optional[str] # Optional[str]
vp = join(self.fs.cwd, file).lstrip("/") vp = join(self.fs.cwd, file).lstrip("/")
try:
ap, vfs, rem = self.fs.v2a(vp, w=True) ap, vfs, rem = self.fs.v2a(vp, w=True)
except Exception as ex:
self.respond("550 %s" % (ex,), logging.info)
return
self.vfs_map[ap] = vp self.vfs_map[ap] = vp
xbu = vfs.flags.get("xbu") xbu = vfs.flags.get("xbu")
if xbu and not runhook( if xbu and not runhook(
@ -627,7 +591,7 @@ class Ftpd(object):
if "::" in ips: if "::" in ips:
ips.append("0.0.0.0") ips.append("0.0.0.0")
ips = [x for x in ips if not x.startswith(("unix:", "fd:"))] ips = [x for x in ips if "unix:" not in x]
if self.args.ftp4: if self.args.ftp4:
ips = [x for x in ips if ":" not in x] ips = [x for x in ips if ":" not in x]

File diff suppressed because it is too large Load diff

View file

@ -224,6 +224,3 @@ class HttpConn(object):
if self.u2idx: if self.u2idx:
self.hsrv.put_u2idx(str(self.addr), self.u2idx) self.hsrv.put_u2idx(str(self.addr), self.u2idx)
self.u2idx = None self.u2idx = None
if self.rproxy:
self.set_rproxy()

View file

@ -70,7 +70,6 @@ from .util import (
build_netmap, build_netmap,
has_resource, has_resource,
ipnorm, ipnorm,
load_ipr,
load_ipu, load_ipu,
load_resource, load_resource,
min_ex, min_ex,
@ -124,7 +123,6 @@ class HttpSrv(object):
self.nm = NetMap([], []) self.nm = NetMap([], [])
self.ssdp: Optional["SSDPr"] = None self.ssdp: Optional["SSDPr"] = None
self.gpwd = Garda(self.args.ban_pw) self.gpwd = Garda(self.args.ban_pw)
self.gpwc = Garda(self.args.ban_pwc)
self.g404 = Garda(self.args.ban_404) self.g404 = Garda(self.args.ban_404)
self.g403 = Garda(self.args.ban_403) self.g403 = Garda(self.args.ban_403)
self.g422 = Garda(self.args.ban_422, False) self.g422 = Garda(self.args.ban_422, False)
@ -177,7 +175,6 @@ class HttpSrv(object):
"browser", "browser",
"browser2", "browser2",
"cf", "cf",
"idp",
"md", "md",
"mde", "mde",
"msg", "msg",
@ -194,11 +191,6 @@ class HttpSrv(object):
else: else:
self.ipu_iu = self.ipu_nm = None self.ipu_iu = self.ipu_nm = None
if self.args.ipr:
self.ipr = load_ipr(self.log, self.args.ipr)
else:
self.ipr = None
self.ipa_nm = build_netmap(self.args.ipa) self.ipa_nm = build_netmap(self.args.ipa)
self.xff_nm = build_netmap(self.args.xff_src) self.xff_nm = build_netmap(self.args.xff_src)
self.xff_lan = build_netmap("lan") self.xff_lan = build_netmap("lan")
@ -321,8 +313,6 @@ class HttpSrv(object):
Daemon(self.broker.say, "sig-hsrv-up1", ("cb_httpsrv_up",)) Daemon(self.broker.say, "sig-hsrv-up1", ("cb_httpsrv_up",))
saddr = ("", 0) # fwd-decl for `except TypeError as ex:`
while not self.stopping: while not self.stopping:
if self.args.log_conn: if self.args.log_conn:
self.log(self.name, "|%sC-ncli" % ("-" * 1,), c="90") self.log(self.name, "|%sC-ncli" % ("-" * 1,), c="90")
@ -330,8 +320,7 @@ class HttpSrv(object):
spins = 0 spins = 0
while self.ncli >= self.nclimax: while self.ncli >= self.nclimax:
if not spins: if not spins:
t = "at connection limit (global-option 'nc'); waiting" self.log(self.name, "at connection limit; waiting", 3)
self.log(self.name, t, 3)
spins += 1 spins += 1
time.sleep(0.1) time.sleep(0.1)
@ -405,19 +394,6 @@ class HttpSrv(object):
self.log(self.name, "accept({}): {}".format(fno, ex), c=6) self.log(self.name, "accept({}): {}".format(fno, ex), c=6)
time.sleep(0.02) time.sleep(0.02)
continue continue
except TypeError as ex:
# on macOS, accept() may return a None saddr if blocked by LittleSnitch;
# unicode(saddr[0]) ==> TypeError: 'NoneType' object is not subscriptable
if tcp and not saddr:
t = "accept(%s): failed to accept connection from client due to firewall or network issue"
self.log(self.name, t % (fno,), c=3)
try:
sck.close() # type: ignore
except:
pass
time.sleep(0.02)
continue
raise
if self.args.log_conn: if self.args.log_conn:
t = "|{}C-acc2 \033[0;36m{} \033[3{}m{}".format( t = "|{}C-acc2 \033[0;36m{} \033[3{}m{}".format(

View file

@ -94,21 +94,10 @@ class Ico(object):
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 100 {}" xmlns="http://www.w3.org/2000/svg"><g> <svg version="1.1" viewBox="0 0 100 {}" xmlns="http://www.w3.org/2000/svg"><g>
<rect width="100%" height="100%" fill="#{}" /> <rect width="100%" height="100%" fill="#{}" />
<text x="50%" y="{}" dominant-baseline="middle" text-anchor="middle" xml:space="preserve" <text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" xml:space="preserve"
fill="#{}" font-family="monospace" font-size="14px" style="letter-spacing:.5px">{}</text> fill="#{}" font-family="monospace" font-size="14px" style="letter-spacing:.5px">{}</text>
</g></svg> </g></svg>
""" """
svg = svg.format(h, c[:6], c[6:], html_escape(ext, True))
txt = html_escape(ext, True)
if "\n" in txt:
lines = txt.split("\n")
n = len(lines)
y = "20%" if n == 2 else "10%" if n == 3 else "0"
zs = '<tspan x="50%%" dy="1.2em">%s</tspan>'
txt = "".join([zs % (x,) for x in lines])
else:
y = "50%"
svg = svg.format(h, c[:6], y, c[6:], txt)
return "image/svg+xml", svg.encode("utf-8") return "image/svg+xml", svg.encode("utf-8")

View file

@ -76,8 +76,7 @@ class MDNS(MCast):
if not self.args.zm_nwa_1: if not self.args.zm_nwa_1:
set_avahi_379() set_avahi_379()
zs = self.args.zm_fqdn or (self.args.name + ".local") zs = self.args.name + ".local."
zs = zs.replace("--name", self.args.name).rstrip(".") + "."
zs = zs.encode("ascii", "replace").decode("ascii", "replace") zs = zs.encode("ascii", "replace").decode("ascii", "replace")
self.hn = "-".join(x for x in zs.split("?") if x) or ( self.hn = "-".join(x for x in zs.split("?") if x) or (
"vault-{}".format(random.randint(1, 255)) "vault-{}".format(random.randint(1, 255))

View file

@ -18,7 +18,7 @@ class Metrics(object):
def tx(self, cli: "HttpCli") -> bool: def tx(self, cli: "HttpCli") -> bool:
if not cli.avol: if not cli.avol:
raise Pebkac(403, "'stats' not allowed for user " + cli.uname) raise Pebkac(403, "not allowed for user " + cli.uname)
args = cli.args args = cli.args
if not args.stats: if not args.stats:

View file

@ -18,7 +18,6 @@ from .util import (
REKOBO_LKEY, REKOBO_LKEY,
VF_CAREFUL, VF_CAREFUL,
fsenc, fsenc,
gzip,
min_ex, min_ex,
pybin, pybin,
retchk, retchk,
@ -29,7 +28,7 @@ from .util import (
) )
if True: # pylint: disable=using-constant-test if True: # pylint: disable=using-constant-test
from typing import IO, Any, Optional, Union from typing import Any, Optional, Union
from .util import NamedLogger, RootLogger from .util import NamedLogger, RootLogger
@ -67,8 +66,6 @@ HAVE_FFPROBE = not os.environ.get("PRTY_NO_FFPROBE") and have_ff("ffprobe")
CBZ_PICS = set("png jpg jpeg gif bmp tga tif tiff webp avif".split()) CBZ_PICS = set("png jpg jpeg gif bmp tga tif tiff webp avif".split())
CBZ_01 = re.compile(r"(^|[^0-9v])0+[01]\b") CBZ_01 = re.compile(r"(^|[^0-9v])0+[01]\b")
FMT_AU = set("mp3 ogg flac wav".split())
class MParser(object): class MParser(object):
def __init__(self, cmdline: str) -> None: def __init__(self, cmdline: str) -> None:
@ -141,6 +138,8 @@ def au_unpk(
fd, ret = tempfile.mkstemp("." + au) fd, ret = tempfile.mkstemp("." + au)
if pk == "gz": if pk == "gz":
import gzip
fi = gzip.GzipFile(abspath, mode="rb") fi = gzip.GzipFile(abspath, mode="rb")
elif pk == "xz": elif pk == "xz":
@ -168,16 +167,12 @@ def au_unpk(
znil = [x for x in znil if "cover" in x[0]] or znil znil = [x for x in znil if "cover" in x[0]] or znil
znil = [x for x in znil if CBZ_01.search(x[0])] or znil znil = [x for x in znil if CBZ_01.search(x[0])] or znil
t = "cbz: %d files, %d hits" % (nf, len(znil)) t = "cbz: %d files, %d hits" % (nf, len(znil))
using = sorted(znil)[0][1].filename
if znil: if znil:
t += ", using " + using t += ", using " + znil[0][1].filename
log(t) log(t)
if not znil: if not znil:
raise Exception("no images inside cbz") raise Exception("no images inside cbz")
fi = zf.open(using) fi = zf.open(znil[0][1])
elif pk == "epub":
fi = get_cover_from_epub(log, abspath)
else: else:
raise Exception("unknown compression %s" % (pk,)) raise Exception("unknown compression %s" % (pk,))
@ -208,7 +203,7 @@ def au_unpk(
def ffprobe( def ffprobe(
abspath: str, timeout: int = 60 abspath: str, timeout: int = 60
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]: ) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]:
cmd = [ cmd = [
b"ffprobe", b"ffprobe",
b"-hide_banner", b"-hide_banner",
@ -222,17 +217,8 @@ def ffprobe(
return parse_ffprobe(so) return parse_ffprobe(so)
def parse_ffprobe( def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]:
txt: str, """ffprobe -show_format -show_streams"""
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:
"""
txt: output from ffprobe -show_format -show_streams
returns:
* normalized tags
* original/raw tags
* list of streams
* format props
"""
streams = [] streams = []
fmt = {} fmt = {}
g = {} g = {}
@ -256,7 +242,7 @@ def parse_ffprobe(
ret: dict[str, Any] = {} # processed ret: dict[str, Any] = {} # processed
md: dict[str, list[Any]] = {} # raw tags md: dict[str, list[Any]] = {} # raw tags
is_audio = fmt.get("format_name") in FMT_AU is_audio = fmt.get("format_name") in ["mp3", "ogg", "flac", "wav"]
if fmt.get("filename", "").split(".")[-1].lower() in ["m4a", "aac"]: if fmt.get("filename", "").split(".")[-1].lower() in ["m4a", "aac"]:
is_audio = True is_audio = True
@ -284,8 +270,6 @@ def parse_ffprobe(
["channel_layout", "chs"], ["channel_layout", "chs"],
["sample_rate", ".hz"], ["sample_rate", ".hz"],
["bit_rate", ".aq"], ["bit_rate", ".aq"],
["bits_per_sample", ".bps"],
["bits_per_raw_sample", ".bprs"],
["duration", ".dur"], ["duration", ".dur"],
] ]
@ -325,7 +309,7 @@ def parse_ffprobe(
ret[rk] = v1 ret[rk] = v1
if ret.get("vc") == "ansi": # shellscript if ret.get("vc") == "ansi": # shellscript
return {}, {}, [], {} return {}, {}
for strm in streams: for strm in streams:
for sk, sv in strm.items(): for sk, sv in strm.items():
@ -374,77 +358,7 @@ def parse_ffprobe(
zero = int("0") zero = int("0")
zd = {k: (zero, v) for k, v in ret.items()} zd = {k: (zero, v) for k, v in ret.items()}
return zd, md, streams, fmt return zd, md
def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]:
import zipfile
from .dxml import parse_xml
try:
from urlparse import urljoin # Python2
except ImportError:
from urllib.parse import urljoin # Python3
with zipfile.ZipFile(abspath, "r") as z:
# First open the container file to find the package document (.opf file)
try:
container_root = parse_xml(z.read("META-INF/container.xml").decode())
except KeyError:
log("epub: no container file found in %s" % (abspath,))
return None
# https://www.w3.org/TR/epub-33/#sec-container.xml-rootfile-elem
container_ns = {"": "urn:oasis:names:tc:opendocument:xmlns:container"}
# One file could contain multiple package documents, default to the first one
rootfile_path = container_root.find("./rootfiles/rootfile", container_ns).get(
"full-path"
)
# Then open the first package document to find the path of the cover image
try:
package_root = parse_xml(z.read(rootfile_path).decode())
except KeyError:
log("epub: no package document found in %s" % (abspath,))
return None
# https://www.w3.org/TR/epub-33/#sec-package-doc
package_ns = {"": "http://www.idpf.org/2007/opf"}
# https://www.w3.org/TR/epub-33/#sec-cover-image
coverimage_path_node = package_root.find(
"./manifest/item[@properties='cover-image']", package_ns
)
if coverimage_path_node is not None:
coverimage_path = coverimage_path_node.get("href")
else:
# This might be an EPUB2 file, try the legacy way of specifying covers
coverimage_path = _get_cover_from_epub2(log, package_root, package_ns)
# This url is either absolute (in the .epub) or relative to the package document
adjusted_cover_path = urljoin(rootfile_path, coverimage_path)
return z.open(adjusted_cover_path)
def _get_cover_from_epub2(
log: "NamedLogger", package_root, package_ns
) -> Optional[str]:
# <meta name="cover" content="id-to-cover-image"> in <metadata>, then
# <item> in <manifest>
cover_id = package_root.find("./metadata/meta[@name='cover']", package_ns).get(
"content"
)
if not cover_id:
return None
for node in package_root.iterfind("./manifest/item", package_ns):
if node.get("id") == cover_id:
cover_path = node.get("href")
return cover_path
return None
class MTag(object): class MTag(object):
@ -715,7 +629,7 @@ class MTag(object):
if not bos.path.isfile(abspath): if not bos.path.isfile(abspath):
return {} return {}
ret, md, _, _ = ffprobe(abspath, self.args.mtag_to) ret, md = ffprobe(abspath, self.args.mtag_to)
if self.args.mtag_vv: if self.args.mtag_vv:
for zd in (ret, dict(md)): for zd in (ret, dict(md)):

View file

@ -163,7 +163,6 @@ class MCast(object):
sck.settimeout(None) sck.settimeout(None)
sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try: try:
# safe for this purpose; https://lwn.net/Articles/853637/
sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
except: except:
pass pass
@ -183,7 +182,11 @@ class MCast(object):
srv.ips[oth_ip.split("/")[0]] = ipaddress.ip_network(oth_ip, False) srv.ips[oth_ip.split("/")[0]] = ipaddress.ip_network(oth_ip, False)
# gvfs breaks if a linklocal ip appears in a dns reply # gvfs breaks if a linklocal ip appears in a dns reply
ll = {k: v for k, v in srv.ips.items() if k.startswith(("169.254", "fe80"))} ll = {
k: v
for k, v in srv.ips.items()
if k.startswith("169.254") or k.startswith("fe80")
}
rt = {k: v for k, v in srv.ips.items() if k not in ll} rt = {k: v for k, v in srv.ips.items() if k not in ll}
if self.args.ll or not rt: if self.args.ll or not rt:

View file

@ -15,7 +15,7 @@ try:
raise Exception() raise Exception()
HAVE_ARGON2 = True HAVE_ARGON2 = True
from argon2 import exceptions as argon2ex from argon2 import __version__ as argon2ver
except: except:
HAVE_ARGON2 = False HAVE_ARGON2 = False
@ -147,10 +147,6 @@ class PWHash(object):
def cli(self) -> None: def cli(self) -> None:
import getpass import getpass
if self.args.usernames:
t = "since you have enabled --usernames, please provide username:password"
print(t)
while True: while True:
try: try:
p1 = getpass.getpass("password> ") p1 = getpass.getpass("password> ")

View file

@ -320,7 +320,7 @@ class SMB(object):
self.hub.up2k.handle_mv(uname, "1.7.6.2", vp1, vp2) self.hub.up2k.handle_mv(uname, "1.7.6.2", vp1, vp2)
try: try:
bos.makedirs(ap2, vf=vfs2.flags) bos.makedirs(ap2)
except: except:
pass pass
@ -334,7 +334,7 @@ class SMB(object):
t = "blocked mkdir (no-write-acc %s): /%s @%s" t = "blocked mkdir (no-write-acc %s): /%s @%s"
yeet(t % (vfs.axs.uwrite, vpath, uname)) yeet(t % (vfs.axs.uwrite, vpath, uname))
return bos.mkdir(ap, vfs.flags["chmod_d"]) return bos.mkdir(ap)
def _stat(self, vpath: str, *a: Any, **ka: Any) -> os.stat_result: def _stat(self, vpath: str, *a: Any, **ka: Any) -> os.stat_result:
try: try:

View file

@ -17,9 +17,6 @@ if True: # pylint: disable=using-constant-test
from .util import NamedLogger from .util import NamedLogger
TAR_NO_OPUS = set("aac|m4a|mp3|oga|ogg|opus|wma".split("|"))
class StreamArc(object): class StreamArc(object):
def __init__( def __init__(
self, self,
@ -85,7 +82,9 @@ def enthumb(
) -> dict[str, Any]: ) -> dict[str, Any]:
rem = f["vp"] rem = f["vp"]
ext = rem.rsplit(".", 1)[-1].lower() ext = rem.rsplit(".", 1)[-1].lower()
if (fmt == "mp3" and ext == "mp3") or (fmt == "opus" and ext in TAR_NO_OPUS): if (fmt == "mp3" and ext == "mp3") or (
fmt == "opus" and ext in "aac|m4a|mp3|ogg|opus|wma".split("|")
):
raise Exception() raise Exception()
vp = vjoin(vtop, rem.split("/", 1)[1]) vp = vjoin(vtop, rem.split("/", 1)[1])

View file

@ -2,8 +2,8 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import argparse import argparse
import atexit
import errno import errno
import gzip
import logging import logging
import os import os
import re import re
@ -28,7 +28,6 @@ if True: # pylint: disable=using-constant-test
from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode
from .authsrv import BAD_CFG, AuthSrv from .authsrv import BAD_CFG, AuthSrv
from .bos import bos
from .cert import ensure_cert from .cert import ensure_cert
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN
from .pwhash import HAVE_ARGON2 from .pwhash import HAVE_ARGON2
@ -39,7 +38,6 @@ from .th_srv import (
HAVE_FFPROBE, HAVE_FFPROBE,
HAVE_HEIF, HAVE_HEIF,
HAVE_PIL, HAVE_PIL,
HAVE_RAW,
HAVE_VIPS, HAVE_VIPS,
HAVE_WEBP, HAVE_WEBP,
ThumbSrv, ThumbSrv,
@ -53,7 +51,6 @@ from .util import (
HAVE_PSUTIL, HAVE_PSUTIL,
HAVE_SQLITE3, HAVE_SQLITE3,
HAVE_ZMQ, HAVE_ZMQ,
RE_ANSI,
URL_BUG, URL_BUG,
UTC, UTC,
VERSIONS, VERSIONS,
@ -63,25 +60,19 @@ from .util import (
HMaccas, HMaccas,
ODict, ODict,
alltrace, alltrace,
ansi_re,
build_netmap, build_netmap,
expat_ver, expat_ver,
gzip,
load_ipr,
load_ipu, load_ipu,
lock_file,
min_ex, min_ex,
mp, mp,
odfusion, odfusion,
pybin, pybin,
start_log_thrs, start_log_thrs,
start_stackmon, start_stackmon,
termsize,
ub64enc, ub64enc,
) )
if HAVE_SQLITE3:
import sqlite3
if TYPE_CHECKING: if TYPE_CHECKING:
try: try:
from .mdns import MDNS from .mdns import MDNS
@ -93,11 +84,6 @@ if PY2:
range = xrange # type: ignore range = xrange # type: ignore
VER_IDP_DB = 1
VER_SESSION_DB = 1
VER_SHARES_DB = 2
class SvcHub(object): class SvcHub(object):
""" """
Hosts all services which cannot be parallelized due to reliance on monolithic resources. Hosts all services which cannot be parallelized due to reliance on monolithic resources.
@ -156,7 +142,6 @@ class SvcHub(object):
args.no_del = True args.no_del = True
args.no_mv = True args.no_mv = True
args.hardlink = True args.hardlink = True
args.dav_auth = True
args.vague_403 = True args.vague_403 = True
args.nih = True args.nih = True
@ -173,7 +158,6 @@ class SvcHub(object):
# for non-http clients (ftp, tftp) # for non-http clients (ftp, tftp)
self.bans: dict[str, int] = {} self.bans: dict[str, int] = {}
self.gpwd = Garda(self.args.ban_pw) self.gpwd = Garda(self.args.ban_pw)
self.gpwc = Garda(self.args.ban_pwc)
self.g404 = Garda(self.args.ban_404) self.g404 = Garda(self.args.ban_404)
self.g403 = Garda(self.args.ban_403) self.g403 = Garda(self.args.ban_403)
self.g422 = Garda(self.args.ban_422, False) self.g422 = Garda(self.args.ban_422, False)
@ -202,14 +186,8 @@ class SvcHub(object):
if not args.use_fpool and args.j != 1: if not args.use_fpool and args.j != 1:
args.no_fpool = True args.no_fpool = True
t = "multithreading enabled with -j {}, so disabling fpool -- this can reduce upload performance on some filesystems, and make some antivirus-softwares " t = "multithreading enabled with -j {}, so disabling fpool -- this can reduce upload performance on some filesystems"
c = 0 self.log("root", t.format(args.j))
if ANYWIN:
t += "(especially Microsoft Defender) stress your CPU and HDD severely during big uploads"
c = 3
else:
t += "consume more resources (CPU/HDD) than normal"
self.log("root", t.format(args.j), c)
if not args.no_fpool and args.j != 1: if not args.no_fpool and args.j != 1:
t = "WARNING: ignoring --use-fpool because multithreading (-j{}) is enabled" t = "WARNING: ignoring --use-fpool because multithreading (-j{}) is enabled"
@ -245,7 +223,7 @@ class SvcHub(object):
t = "WARNING: --th-ram-max is very small (%.2f GiB); will not be able to %s" t = "WARNING: --th-ram-max is very small (%.2f GiB); will not be able to %s"
self.log("root", t % (args.th_ram_max, zs), 3) self.log("root", t % (args.th_ram_max, zs), 3)
if args.chpw and args.have_idp_hdrs: if args.chpw and args.idp_h_usr:
t = "ERROR: user-changeable passwords is incompatible with IdP/identity-providers; you must disable either --chpw or --idp-h-usr" t = "ERROR: user-changeable passwords is incompatible with IdP/identity-providers; you must disable either --chpw or --idp-h-usr"
self.log("root", t, 1) self.log("root", t, 1)
raise Exception(t) raise Exception(t)
@ -261,24 +239,8 @@ class SvcHub(object):
setattr(args, "ipu_iu", iu) setattr(args, "ipu_iu", iu)
setattr(args, "ipu_nm", nm) setattr(args, "ipu_nm", nm)
if args.ipr:
ipr = load_ipr(self.log, args.ipr, True)
setattr(args, "ipr_u", ipr)
for zs in "ah_salt fk_salt dk_salt".split():
if getattr(args, "show_%s" % (zs,)):
self.log("root", "effective %s is %s" % (zs, getattr(args, zs)))
if args.ah_cli or args.ah_gen:
args.idp_store = 0
args.no_ses = True
args.shr = ""
if args.idp_store and args.have_idp_hdrs:
self.setup_db("idp")
if not self.args.no_ses: if not self.args.no_ses:
self.setup_db("ses") self.setup_session_db()
args.shr1 = "" args.shr1 = ""
if args.shr: if args.shr:
@ -330,8 +292,6 @@ class SvcHub(object):
decs.pop("vips", None) decs.pop("vips", None)
if not HAVE_PIL: if not HAVE_PIL:
decs.pop("pil", None) decs.pop("pil", None)
if not HAVE_RAW:
decs.pop("raw", None)
if not HAVE_FFMPEG or not HAVE_FFPROBE: if not HAVE_FFMPEG or not HAVE_FFPROBE:
decs.pop("ff", None) decs.pop("ff", None)
@ -438,95 +398,34 @@ class SvcHub(object):
getattr(args, zs).mutex = threading.Lock() getattr(args, zs).mutex = threading.Lock()
except: except:
pass pass
if args.ipr:
for nm in args.ipr_u.values():
nm.mutex = threading.Lock()
def _db_onfail_ses(self) -> None:
self.args.no_ses = True
def _db_onfail_idp(self) -> None:
self.args.idp_store = 0
def setup_db(self, which: str) -> None:
"""
the "non-mission-critical" databases; if something looks broken then just nuke it
"""
if which == "ses":
native_ver = VER_SESSION_DB
db_path = self.args.ses_db
desc = "sessions-db"
pathopt = "ses-db"
sanchk_q = "select count(*) from us"
createfun = self._create_session_db
failfun = self._db_onfail_ses
elif which == "idp":
native_ver = VER_IDP_DB
db_path = self.args.idp_db
desc = "idp-db"
pathopt = "idp-db"
sanchk_q = "select count(*) from us"
createfun = self._create_idp_db
failfun = self._db_onfail_idp
else:
raise Exception("unknown cachetype")
if not db_path.endswith(".db"):
zs = "config option --%s (the %s) was configured to [%s] which is invalid; must be a filepath ending with .db"
self.log("root", zs % (pathopt, desc, db_path), 1)
raise Exception(BAD_CFG)
def setup_session_db(self) -> None:
if not HAVE_SQLITE3: if not HAVE_SQLITE3:
failfun() self.args.no_ses = True
if which == "ses": t = "WARNING: sqlite3 not available; disabling sessions, will use plaintext passwords in cookies"
zs = "disabling sessions, will use plaintext passwords in cookies" self.log("root", t, 3)
elif which == "idp":
zs = "disabling idp-db, will be unable to remember IdP-volumes after a restart"
self.log("root", "WARNING: sqlite3 not available; %s" % (zs,), 3)
return return
assert sqlite3 # type: ignore # !rm import sqlite3
db_lock = db_path + ".lock"
try:
create = not os.path.getsize(db_path)
except:
create = True create = True
zs = "creating new" if create else "opening" db_path = self.args.ses_db
self.log("root", "%s %s %s" % (zs, desc, db_path)) self.log("root", "opening sessions-db %s" % (db_path,))
for n in range(2):
for tries in range(2):
sver = 0
try: try:
db = sqlite3.connect(db_path) db = sqlite3.connect(db_path)
cur = db.cursor() cur = db.cursor()
try: try:
zs = "select v from kv where k='sver'" cur.execute("select count(*) from us").fetchone()
sver = cur.execute(zs).fetchall()[0][0] create = False
if sver > native_ver:
zs = "this version of copyparty only understands %s v%d and older; the db is v%d"
raise Exception(zs % (desc, native_ver, sver))
cur.execute(sanchk_q).fetchone()
except:
if sver:
raise
sver = createfun(cur)
err = self._verify_db(
cur, which, pathopt, db_path, desc, sver, native_ver
)
if err:
tries = 99
self.args.no_ses = True
self.log("root", err, 3)
break break
except:
pass
except Exception as ex: except Exception as ex:
if tries or sver > native_ver: if n:
raise raise
t = "%s is unusable; deleting and recreating: %r" t = "sessions-db corrupt; deleting and recreating: %r"
self.log("root", t % (desc, ex), 3) self.log("root", t % (ex,), 3)
try: try:
cur.close() # type: ignore cur.close() # type: ignore
except: except:
@ -535,13 +434,8 @@ class SvcHub(object):
db.close() # type: ignore db.close() # type: ignore
except: except:
pass pass
try:
os.unlink(db_lock)
except:
pass
os.unlink(db_path) os.unlink(db_path)
def _create_session_db(self, cur: "sqlite3.Cursor") -> int:
sch = [ sch = [
r"create table kv (k text, v int)", r"create table kv (k text, v int)",
r"create table us (un text, si text, t0 int)", r"create table us (un text, si text, t0 int)",
@ -551,74 +445,17 @@ class SvcHub(object):
r"create index us_t0 on us(t0)", r"create index us_t0 on us(t0)",
r"insert into kv values ('sver', 1)", r"insert into kv values ('sver', 1)",
] ]
assert db # type: ignore # !rm
assert cur # type: ignore # !rm
if create:
for cmd in sch: for cmd in sch:
cur.execute(cmd) cur.execute(cmd)
self.log("root", "created new sessions-db") self.log("root", "created new sessions-db")
return 1
def _create_idp_db(self, cur: "sqlite3.Cursor") -> int:
sch = [
r"create table kv (k text, v int)",
r"create table us (un text, gs text)",
# username, groups
r"create index us_un on us(un)",
r"insert into kv values ('sver', 1)",
]
for cmd in sch:
cur.execute(cmd)
self.log("root", "created new idp-db")
return 1
def _verify_db(
self,
cur: "sqlite3.Cursor",
which: str,
pathopt: str,
db_path: str,
desc: str,
sver: int,
native_ver: int,
) -> str:
# ensure writable (maybe owned by other user)
db = cur.connection
try:
zil = cur.execute("select v from kv where k='pid'").fetchall()
if len(zil) > 1:
raise Exception()
owner = zil[0][0]
except:
owner = 0
if which == "ses":
cons = "Will now disable sessions and instead use plaintext passwords in cookies."
elif which == "idp":
cons = "Each IdP-volume will not become available until its associated user sends their first request."
else:
raise Exception()
if not lock_file(db_path + ".lock"):
t = "the %s [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --%s or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. %s"
return t % (desc, db_path, owner, pathopt, cons)
vars = (("pid", os.getpid()), ("ts", int(time.time() * 1000)))
if owner:
# wear-estimate: 2 cells; offsets 0x10, 0x50, 0x19720
for k, v in vars:
cur.execute("update kv set v=? where k=?", (v, k))
else:
# wear-estimate: 3~4 cells; offsets 0x10, 0x50, 0x19180, 0x19710, 0x36000, 0x360b0, 0x36b90
for k, v in vars:
cur.execute("insert into kv values(?, ?)", (k, v))
if sver < native_ver:
cur.execute("delete from kv where k='sver'")
cur.execute("insert into kv values('sver',?)", (native_ver,))
db.commit() db.commit()
cur.close() cur.close()
db.close() db.close()
return ""
def setup_share_db(self) -> None: def setup_share_db(self) -> None:
al = self.args al = self.args
@ -627,7 +464,7 @@ class SvcHub(object):
al.shr = "" al.shr = ""
return return
assert sqlite3 # type: ignore # !rm import sqlite3
al.shr = al.shr.strip("/") al.shr = al.shr.strip("/")
if "/" in al.shr or not al.shr: if "/" in al.shr or not al.shr:
@ -638,48 +475,34 @@ class SvcHub(object):
al.shr = "/%s/" % (al.shr,) al.shr = "/%s/" % (al.shr,)
al.shr1 = al.shr[1:] al.shr1 = al.shr[1:]
# policy:
# the shares-db is important, so panic if something is wrong
db_path = self.args.shr_db
db_lock = db_path + ".lock"
try:
create = not os.path.getsize(db_path)
except:
create = True create = True
zs = "creating new" if create else "opening" modified = False
self.log("root", "%s shares-db %s" % (zs, db_path)) db_path = self.args.shr_db
self.log("root", "opening shares-db %s" % (db_path,))
sver = 0 for n in range(2):
try: try:
db = sqlite3.connect(db_path) db = sqlite3.connect(db_path)
cur = db.cursor() cur = db.cursor()
if not create:
zs = "select v from kv where k='sver'"
sver = cur.execute(zs).fetchall()[0][0]
if sver > VER_SHARES_DB:
zs = "this version of copyparty only understands shares-db v%d and older; the db is v%d"
raise Exception(zs % (VER_SHARES_DB, sver))
cur.execute("select count(*) from sh").fetchone()
except Exception as ex:
t = "could not open shares-db; will now panic...\nthe following database must be repaired or deleted before you can launch copyparty:\n%s\n\nERROR: %s\n\nadditional details:\n%s\n"
self.log("root", t % (db_path, ex, min_ex()), 1)
raise
try: try:
zil = cur.execute("select v from kv where k='pid'").fetchall() cur.execute("select count(*) from sh").fetchone()
if len(zil) > 1: create = False
raise Exception() break
owner = zil[0][0]
except: except:
owner = 0 pass
except Exception as ex:
if not lock_file(db_lock): if n:
t = "the shares-db [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --shr-db or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. Will now panic." raise
t = t % (db_path, owner) t = "shares-db corrupt; deleting and recreating: %r"
self.log("root", t, 1) self.log("root", t % (ex,), 3)
raise Exception(t) try:
cur.close() # type: ignore
except:
pass
try:
db.close() # type: ignore
except:
pass
os.unlink(db_path)
sch1 = [ sch1 = [
r"create table kv (k text, v int)", r"create table kv (k text, v int)",
@ -691,37 +514,34 @@ class SvcHub(object):
r"create index sf_k on sf(k)", r"create index sf_k on sf(k)",
r"create index sh_k on sh(k)", r"create index sh_k on sh(k)",
r"create index sh_t1 on sh(t1)", r"create index sh_t1 on sh(t1)",
r"insert into kv values ('sver', 2)",
] ]
assert db # type: ignore # !rm assert db # type: ignore # !rm
assert cur # type: ignore # !rm assert cur # type: ignore # !rm
if not sver: if create:
sver = VER_SHARES_DB dver = 2
modified = True
for cmd in sch1 + sch2: for cmd in sch1 + sch2:
cur.execute(cmd) cur.execute(cmd)
self.log("root", "created new shares-db") self.log("root", "created new shares-db")
else:
(dver,) = cur.execute("select v from kv where k = 'sver'").fetchall()[0]
if sver == 1: if dver == 1:
modified = True
for cmd in sch2: for cmd in sch2:
cur.execute(cmd) cur.execute(cmd)
cur.execute("update sh set st = 0") cur.execute("update sh set st = 0")
self.log("root", "shares-db schema upgrade ok") self.log("root", "shares-db schema upgrade ok")
if sver < VER_SHARES_DB: if modified:
cur.execute("delete from kv where k='sver'") for cmd in [
cur.execute("insert into kv values('sver',?)", (VER_SHARES_DB,)) r"delete from kv where k = 'sver'",
r"insert into kv values ('sver', %d)" % (2,),
vars = (("pid", os.getpid()), ("ts", int(time.time() * 1000))) ]:
if owner: cur.execute(cmd)
# wear-estimate: same as sessions-db
for k, v in vars:
cur.execute("update kv set v=? where k=?", (v, k))
else:
for k, v in vars:
cur.execute("insert into kv values(?, ?)", (k, v))
db.commit() db.commit()
cur.close() cur.close()
db.close() db.close()
@ -786,39 +606,6 @@ class SvcHub(object):
def sigterm(self) -> None: def sigterm(self) -> None:
self.signal_handler(signal.SIGTERM, None) self.signal_handler(signal.SIGTERM, None)
def sticky_qr(self) -> None:
tw, th = termsize()
zs1, qr = self.tcpsrv.qr.split("\n", 1)
url, colr = zs1.split(" ", 1)
nl = len(qr.split("\n")) # numlines
lp = 3 if nl * 2 + 4 < tw else 0 # leftpad
lp0 = lp
if self.args.qr_pin == 2:
url = ""
else:
while lp and (nl + lp) * 2 + len(url) + 1 > tw:
lp -= 1
if (nl + lp) * 2 + len(url) + 1 > tw:
qr = url + "\n" + qr
url = ""
nl += 1
lp = lp0
sh = 1 + th - nl
if lp:
zs = " " * lp
qr = zs + qr.replace("\n", "\n" + zs)
if url:
url = "%s\033[%d;%dH%s\033[0m" % (colr, sh + 1, (nl + lp) * 2, url)
qr = colr + qr
def unlock():
print("\033[s\033[r\033[u", file=sys.stderr)
atexit.register(unlock)
t = "%s\033[%dA" % ("\n" * nl, nl)
t = "%s\033[s\033[1;%dr\033[%dH%s%s\033[u" % (t, sh - 1, sh, qr, url)
self.pr(t, file=sys.stderr)
def cb_httpsrv_up(self) -> None: def cb_httpsrv_up(self) -> None:
self.httpsrv_up += 1 self.httpsrv_up += 1
if self.httpsrv_up != self.broker.num_workers: if self.httpsrv_up != self.broker.num_workers:
@ -831,9 +618,6 @@ class SvcHub(object):
break break
if self.tcpsrv.qr: if self.tcpsrv.qr:
if self.args.qr_pin:
self.sticky_qr()
else:
self.log("qr-code", self.tcpsrv.qr) self.log("qr-code", self.tcpsrv.qr)
else: else:
self.log("root", "workers OK\n") self.log("root", "workers OK\n")
@ -861,7 +645,6 @@ class SvcHub(object):
(HAVE_ZMQ, "pyzmq", "send zeromq messages from event-hooks"), (HAVE_ZMQ, "pyzmq", "send zeromq messages from event-hooks"),
(HAVE_HEIF, "pillow-heif", "read .heif images with pillow (rarely useful)"), (HAVE_HEIF, "pillow-heif", "read .heif images with pillow (rarely useful)"),
(HAVE_AVIF, "pillow-avif", "read .avif images with pillow (rarely useful)"), (HAVE_AVIF, "pillow-avif", "read .avif images with pillow (rarely useful)"),
(HAVE_RAW, "rawpy", "read RAW images"),
] ]
if ANYWIN: if ANYWIN:
to_check += [ to_check += [
@ -896,11 +679,19 @@ class SvcHub(object):
t += ", " t += ", "
t += "\033[0mNG: \033[35m" + sng t += "\033[0mNG: \033[35m" + sng
t += "\033[0m, see --deps (this is fine btw)" t += "\033[0m, see --deps"
self.log("optional-dependencies", t, 6) self.log("dependencies", t, 6)
def _check_env(self) -> None: def _check_env(self) -> None:
al = self.args try:
files = os.listdir(E.cfg)
except:
files = []
hits = [x for x in files if x.lower().endswith(".conf")]
if hits:
t = "WARNING: found config files in [%s]: %s\n config files are not expected here, and will NOT be loaded (unless your setup is intentionally hella funky)"
self.log("root", t % (E.cfg, ", ".join(hits)), 3)
if self.args.no_bauth: if self.args.no_bauth:
t = "WARNING: --no-bauth disables support for the Android app; you may want to use --bauth-last instead" t = "WARNING: --no-bauth disables support for the Android app; you may want to use --bauth-last instead"
@ -908,21 +699,6 @@ class SvcHub(object):
if self.args.bauth_last: if self.args.bauth_last:
self.log("root", "WARNING: ignoring --bauth-last due to --no-bauth", 3) self.log("root", "WARNING: ignoring --bauth-last due to --no-bauth", 3)
have_tcp = False
for zs in al.i:
if not zs.startswith(("unix:", "fd:")):
have_tcp = True
if not have_tcp:
zb = False
zs = "z zm zm4 zm6 zmv zmvv zs zsv zv"
for zs in zs.split():
if getattr(al, zs, False):
setattr(al, zs, False)
zb = True
if zb:
t = "not listening on any ip-addresses (only unix-sockets and/or FDs); cannot enable zeroconf/mdns/ssdp as requested"
self.log("root", t, 3)
if not self.args.no_dav: if not self.args.no_dav:
from .dxml import DXML_OK from .dxml import DXML_OK
@ -987,20 +763,13 @@ class SvcHub(object):
vl = [os.path.expandvars(os.path.expanduser(x)) for x in vl] vl = [os.path.expandvars(os.path.expanduser(x)) for x in vl]
setattr(al, k, vl) setattr(al, k, vl)
for k in "lo hist dbpath ssl_log".split(" "): for k in "lo hist ssl_log".split(" "):
vs = getattr(al, k) vs = getattr(al, k)
if vs: if vs:
vs = os.path.expandvars(os.path.expanduser(vs)) vs = os.path.expandvars(os.path.expanduser(vs))
setattr(al, k, vs) setattr(al, k, vs)
for k in "idp_adm".split(" "): for k in "sus_urls nonsus_urls".split(" "):
vs = getattr(al, k)
vsa = [x.strip() for x in vs.split(",")]
vsa = [x.lower() for x in vsa if x]
setattr(al, k + "_set", set(vsa))
zs = "dav_ua1 sus_urls nonsus_urls ua_nodoc ua_nozip"
for k in zs.split(" "):
vs = getattr(al, k) vs = getattr(al, k)
if not vs or vs == "no": if not vs or vs == "no":
setattr(al, k, None) setattr(al, k, None)
@ -1020,23 +789,10 @@ class SvcHub(object):
al.sus_urls = None al.sus_urls = None
al.xff_hdr = al.xff_hdr.lower() al.xff_hdr = al.xff_hdr.lower()
al.idp_h_usr = [x.lower() for x in al.idp_h_usr or []] al.idp_h_usr = al.idp_h_usr.lower()
al.idp_h_grp = al.idp_h_grp.lower() al.idp_h_grp = al.idp_h_grp.lower()
al.idp_h_key = al.idp_h_key.lower() al.idp_h_key = al.idp_h_key.lower()
al.idp_hm_usr_p = {}
for zs0 in al.idp_hm_usr or []:
try:
sep = zs0[:1]
hn, zs1, zs2 = zs0[1:].split(sep)
hn = hn.lower()
if hn in al.idp_hm_usr_p:
al.idp_hm_usr_p[hn][zs1] = zs2
else:
al.idp_hm_usr_p[hn] = {zs1: zs2}
except:
raise Exception("invalid --idp-hm-usr [%s]" % (zs0,))
al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa, True) al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa, True)
al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa, True) al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa, True)
@ -1081,8 +837,6 @@ class SvcHub(object):
except: except:
raise Exception("invalid --mv-retry [%s]" % (self.args.mv_retry,)) raise Exception("invalid --mv-retry [%s]" % (self.args.mv_retry,))
al.js_utc = "false" if al.localtime else "true"
al.tcolor = al.tcolor.lstrip("#") al.tcolor = al.tcolor.lstrip("#")
if len(al.tcolor) == 3: # fc5 => ffcc55 if len(al.tcolor) == 3: # fc5 => ffcc55
al.tcolor = "".join([x * 2 for x in al.tcolor]) al.tcolor = "".join([x * 2 for x in al.tcolor])
@ -1177,7 +931,7 @@ class SvcHub(object):
fn = sel_fn fn = sel_fn
try: try:
bos.makedirs(os.path.dirname(fn)) os.makedirs(os.path.dirname(fn))
except: except:
pass pass
@ -1194,9 +948,6 @@ class SvcHub(object):
lh = codecs.open(fn, "w", encoding="utf-8", errors="replace") lh = codecs.open(fn, "w", encoding="utf-8", errors="replace")
if getattr(self.args, "free_umask", False):
os.fchmod(lh.fileno(), 0o644)
argv = [pybin] + self.argv argv = [pybin] + self.argv
if hasattr(shlex, "quote"): if hasattr(shlex, "quote"):
argv = [shlex.quote(x) for x in argv] argv = [shlex.quote(x) for x in argv]
@ -1464,18 +1215,11 @@ class SvcHub(object):
fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n" fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n"
if self.no_ansi: if self.no_ansi:
if c == 1: fmt = "%s %-21s %s\n"
fmt = "%s %-21s CRIT: %s\n"
elif c == 3:
fmt = "%s %-21s WARN: %s\n"
elif c == 6:
fmt = "%s %-21s BTW: %s\n"
else:
fmt = "%s %-21s LOG: %s\n"
if "\033" in msg: if "\033" in msg:
msg = RE_ANSI.sub("", msg) msg = ansi_re.sub("", msg)
if "\033" in src: if "\033" in src:
src = RE_ANSI.sub("", src) src = ansi_re.sub("", src)
elif c: elif c:
if isinstance(c, int): if isinstance(c, int):
msg = "\033[3%sm%s\033[0m" % (c, msg) msg = "\033[3%sm%s\033[0m" % (c, msg)
@ -1516,7 +1260,7 @@ class SvcHub(object):
raise raise
def check_mp_support(self) -> str: def check_mp_support(self) -> str:
if MACOS and not os.environ.get("PRTY_FORCE_MP"): if MACOS:
return "multiprocessing is wonky on mac osx;" return "multiprocessing is wonky on mac osx;"
elif sys.version_info < (3, 3): elif sys.version_info < (3, 3):
return "need python 3.3 or newer for multiprocessing;" return "need python 3.3 or newer for multiprocessing;"
@ -1536,7 +1280,7 @@ class SvcHub(object):
return False return False
try: try:
if mp.cpu_count() <= 1 and not os.environ.get("PRTY_FORCE_MP"): if mp.cpu_count() <= 1:
raise Exception() raise Exception()
except: except:
self.log("svchub", "only one CPU detected; multiprocessing disabled") self.log("svchub", "only one CPU detected; multiprocessing disabled")

View file

@ -4,11 +4,12 @@ from __future__ import print_function, unicode_literals
import calendar import calendar
import stat import stat
import time import time
import zlib
from .authsrv import AuthSrv from .authsrv import AuthSrv
from .bos import bos from .bos import bos
from .sutil import StreamArc, errdesc from .sutil import StreamArc, errdesc
from .util import min_ex, sanitize_fn, spack, sunpack, yieldfile, zlib from .util import min_ex, sanitize_fn, spack, sunpack, yieldfile
if True: # pylint: disable=using-constant-test if True: # pylint: disable=using-constant-test
from typing import Any, Generator, Optional from typing import Any, Generator, Optional
@ -54,7 +55,6 @@ def gen_fdesc(sz: int, crc32: int, z64: bool) -> bytes:
def gen_hdr( def gen_hdr(
h_pos: Optional[int], h_pos: Optional[int],
z64: bool,
fn: str, fn: str,
sz: int, sz: int,
lastmod: int, lastmod: int,
@ -71,6 +71,7 @@ def gen_hdr(
# appnote 4.5 / zip 3.0 (2008) / unzip 6.0 (2009) says to add z64 # appnote 4.5 / zip 3.0 (2008) / unzip 6.0 (2009) says to add z64
# extinfo for values which exceed H, but that becomes an off-by-one # extinfo for values which exceed H, but that becomes an off-by-one
# (can't tell if it was clamped or exactly maxval), make it obvious # (can't tell if it was clamped or exactly maxval), make it obvious
z64 = sz >= 0xFFFFFFFF
z64v = [sz, sz] if z64 else [] z64v = [sz, sz] if z64 else []
if h_pos and h_pos >= 0xFFFFFFFF: if h_pos and h_pos >= 0xFFFFFFFF:
# central, also consider ptr to original header # central, also consider ptr to original header
@ -244,7 +245,6 @@ class StreamZip(StreamArc):
sz = st.st_size sz = st.st_size
ts = st.st_mtime ts = st.st_mtime
h_pos = self.pos
crc = 0 crc = 0
if self.pre_crc: if self.pre_crc:
@ -253,12 +253,8 @@ class StreamZip(StreamArc):
crc &= 0xFFFFFFFF crc &= 0xFFFFFFFF
# some unzip-programs expect a 64bit data-descriptor h_pos = self.pos
# even if the only 32bit-exceeding value is the offset, buf = gen_hdr(None, name, sz, ts, self.utf8, crc, self.pre_crc)
# so force that by placeholdering the filesize too
z64 = h_pos >= 0xFFFFFFFF or sz >= 0xFFFFFFFF
buf = gen_hdr(None, z64, name, sz, ts, self.utf8, crc, self.pre_crc)
yield self._ct(buf) yield self._ct(buf)
for buf in yieldfile(src, self.args.iobuf): for buf in yieldfile(src, self.args.iobuf):
@ -271,6 +267,8 @@ class StreamZip(StreamArc):
self.items.append((name, sz, ts, crc, h_pos)) self.items.append((name, sz, ts, crc, h_pos))
z64 = sz >= 4 * 1024 * 1024 * 1024
if z64 or not self.pre_crc: if z64 or not self.pre_crc:
buf = gen_fdesc(sz, crc, z64) buf = gen_fdesc(sz, crc, z64)
yield self._ct(buf) yield self._ct(buf)
@ -309,8 +307,7 @@ class StreamZip(StreamArc):
cdir_pos = self.pos cdir_pos = self.pos
for name, sz, ts, crc, h_pos in self.items: for name, sz, ts, crc, h_pos in self.items:
z64 = h_pos >= 0xFFFFFFFF or sz >= 0xFFFFFFFF buf = gen_hdr(h_pos, name, sz, ts, self.utf8, crc, self.pre_crc)
buf = gen_hdr(h_pos, z64, name, sz, ts, self.utf8, crc, self.pre_crc)
mbuf += self._ct(buf) mbuf += self._ct(buf)
if len(mbuf) >= 16384: if len(mbuf) >= 16384:
yield mbuf yield mbuf

View file

@ -25,8 +25,8 @@ from .util import (
termsize, termsize,
) )
if True: # pylint: disable=using-constant-test if True:
from typing import Generator, Optional, Union from typing import Generator, Union
if TYPE_CHECKING: if TYPE_CHECKING:
from .svchub import SvcHub from .svchub import SvcHub
@ -151,15 +151,9 @@ class TcpSrv(object):
if just_ll or self.args.ll: if just_ll or self.args.ll:
ll_ok.add(ip.split("/")[0]) ll_ok.add(ip.split("/")[0])
listening_on = []
for ip, ports in sorted(ok.items()):
for port in sorted(ports):
listening_on.append("%s %s" % (ip, port))
qr1: dict[str, list[int]] = {} qr1: dict[str, list[int]] = {}
qr2: dict[str, list[int]] = {} qr2: dict[str, list[int]] = {}
msgs = [] msgs = []
accessible_on = []
title_tab: dict[str, dict[str, int]] = {} title_tab: dict[str, dict[str, int]] = {}
title_vars = [x[1:] for x in self.args.wintitle.split(" ") if x.startswith("$")] title_vars = [x[1:] for x in self.args.wintitle.split(" ") if x.startswith("$")]
t = "available @ {}://{}:{}/ (\033[33m{}\033[0m)" t = "available @ {}://{}:{}/ (\033[33m{}\033[0m)"
@ -175,10 +169,6 @@ class TcpSrv(object):
): ):
continue continue
zs = "%s %s" % (ip, port)
if zs not in accessible_on:
accessible_on.append(zs)
proto = " http" proto = " http"
if self.args.http_only: if self.args.http_only:
pass pass
@ -229,14 +219,6 @@ class TcpSrv(object):
else: else:
print("\n", end="") print("\n", end="")
for fn, ls in (
(self.args.wr_h_eps, listening_on),
(self.args.wr_h_aon, accessible_on),
):
if fn:
with open(fn, "wb") as f:
f.write(("\n".join(ls)).encode("utf-8"))
if self.args.qr or self.args.qrs: if self.args.qr or self.args.qrs:
self.qr = self._qr(qr1, qr2) self.qr = self._qr(qr1, qr2)
@ -245,10 +227,8 @@ class TcpSrv(object):
def _listen(self, ip: str, port: int) -> None: def _listen(self, ip: str, port: int) -> None:
uds_perm = uds_gid = -1 uds_perm = uds_gid = -1
bound: Optional[socket.socket] = None
tcp = False
if "unix:" in ip: if "unix:" in ip:
tcp = False
ipv = socket.AF_UNIX ipv = socket.AF_UNIX
uds = ip.split(":") uds = ip.split(":")
ip = uds[-1] ip = uds[-1]
@ -261,12 +241,7 @@ class TcpSrv(object):
import grp import grp
uds_gid = grp.getgrnam(uds[2]).gr_gid uds_gid = grp.getgrnam(uds[2]).gr_gid
elif "fd:" in ip:
fd = ip[3:]
bound = socket.socket(fileno=int(fd))
tcp = bound.proto == socket.IPPROTO_TCP
ipv = bound.family
elif ":" in ip: elif ":" in ip:
tcp = True tcp = True
ipv = socket.AF_INET6 ipv = socket.AF_INET6
@ -274,7 +249,7 @@ class TcpSrv(object):
tcp = True tcp = True
ipv = socket.AF_INET ipv = socket.AF_INET
srv = bound or socket.socket(ipv, socket.SOCK_STREAM) srv = socket.socket(ipv, socket.SOCK_STREAM)
if not ANYWIN or self.args.reuseaddr: if not ANYWIN or self.args.reuseaddr:
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@ -289,13 +264,9 @@ class TcpSrv(object):
except: except:
pass # will create another ipv4 socket instead pass # will create another ipv4 socket instead
if getattr(self.args, "freebind", False): if not ANYWIN and self.args.freebind:
srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1) srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1)
if bound:
self.srv.append(srv)
return
try: try:
if tcp: if tcp:
srv.bind((ip, port)) srv.bind((ip, port))
@ -448,7 +419,7 @@ class TcpSrv(object):
def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]: def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]:
from .stolen.ifaddr import get_adapters from .stolen.ifaddr import get_adapters
listen_ips = [x for x in listen_ips if not x.startswith(("unix:", "fd:"))] listen_ips = [x for x in listen_ips if "unix:" not in x]
nics = get_adapters(True) nics = get_adapters(True)
eps: dict[str, Netdev] = {} eps: dict[str, Netdev] = {}
@ -577,7 +548,7 @@ class TcpSrv(object):
ip = None ip = None
ips = list(t1) + list(t2) ips = list(t1) + list(t2)
qri = self.args.qri qri = self.args.qri
if self.args.zm and not qri and ips: if self.args.zm and not qri:
name = self.args.name + ".local" name = self.args.name + ".local"
t1[name] = next(v for v in (t1 or t2).values()) t1[name] = next(v for v in (t1 or t2).values())
ips = [name] + ips ips = [name] + ips
@ -594,7 +565,8 @@ class TcpSrv(object):
if not ip: if not ip:
return "" return ""
hip = "[%s]" % (ip,) if ":" in ip else ip if ":" in ip:
ip = "[{}]".format(ip)
if self.args.http_only: if self.args.http_only:
https = "" https = ""
@ -606,7 +578,7 @@ class TcpSrv(object):
ports = t1.get(ip, t2.get(ip, [])) ports = t1.get(ip, t2.get(ip, []))
dport = 443 if https else 80 dport = 443 if https else 80
port = "" if dport in ports or not ports else ":{}".format(ports[0]) port = "" if dport in ports or not ports else ":{}".format(ports[0])
txt = "http{}://{}{}/{}".format(https, hip, port, self.args.qrl) txt = "http{}://{}{}/{}".format(https, ip, port, self.args.qrl)
btxt = txt.encode("utf-8") btxt = txt.encode("utf-8")
if PY2: if PY2:
@ -614,10 +586,6 @@ class TcpSrv(object):
fg = self.args.qr_fg fg = self.args.qr_fg
bg = self.args.qr_bg bg = self.args.qr_bg
nocolor = fg == -1
if nocolor:
fg = 0
pad = self.args.qrp pad = self.args.qrp
zoom = self.args.qrz zoom = self.args.qrz
qrc = QrCode.encode_binary(btxt) qrc = QrCode.encode_binary(btxt)
@ -645,8 +613,6 @@ class TcpSrv(object):
qr = qr.replace("\n", "\033[K\n") + "\033[K" # win10do qr = qr.replace("\n", "\033[K\n") + "\033[K" # win10do
cc = " \033[0;38;5;{0};47;48;5;{1}m" if fg else " \033[0;30;47m" cc = " \033[0;38;5;{0};47;48;5;{1}m" if fg else " \033[0;30;47m"
if nocolor:
cc = " \033[0m"
t = cc + "\n{2}\033[999G\033[0m\033[J" t = cc + "\n{2}\033[999G\033[0m\033[J"
t = t.format(fg, bg, qr) t = t.format(fg, bg, qr)
if ANYWIN: if ANYWIN:

View file

@ -36,20 +36,7 @@ from partftpy.TftpShared import TftpException
from .__init__ import EXE, PY2, TYPE_CHECKING from .__init__ import EXE, PY2, TYPE_CHECKING
from .authsrv import VFS from .authsrv import VFS
from .bos import bos from .bos import bos
from .util import ( from .util import UTC, BytesIO, Daemon, ODict, exclude_dotfiles, min_ex, runhook, undot
FN_EMB,
UTC,
BytesIO,
Daemon,
ODict,
exclude_dotfiles,
min_ex,
runhook,
set_fperms,
undot,
vjoin,
vsplit,
)
if True: # pylint: disable=using-constant-test if True: # pylint: disable=using-constant-test
from typing import Any, Union from typing import Any, Union
@ -179,7 +166,7 @@ class Tftpd(object):
if "::" in ips: if "::" in ips:
ips.append("0.0.0.0") ips.append("0.0.0.0")
ips = [x for x in ips if not x.startswith(("unix:", "fd:"))] ips = [x for x in ips if "unix:" not in x]
if self.args.tftp4: if self.args.tftp4:
ips = [x for x in ips if ":" not in x] ips = [x for x in ips if ":" not in x]
@ -257,25 +244,16 @@ class Tftpd(object):
for srv in srvs: for srv in srvs:
srv.stop() srv.stop()
def _v2a( def _v2a(self, caller: str, vpath: str, perms: list, *a: Any) -> tuple[VFS, str]:
self, caller: str, vpath: str, perms: list, *a: Any
) -> tuple[VFS, str, str]:
vpath = vpath.replace("\\", "/").lstrip("/") vpath = vpath.replace("\\", "/").lstrip("/")
if not perms: if not perms:
perms = [True, True] perms = [True, True]
debug('%s("%s", %s) %s\033[K\033[0m', caller, vpath, str(a), perms) debug('%s("%s", %s) %s\033[K\033[0m', caller, vpath, str(a), perms)
vfs, rem = self.asrv.vfs.get(vpath, "*", *perms) vfs, rem = self.asrv.vfs.get(vpath, "*", *perms)
if perms[1] and "*" not in vfs.axs.uread and "wo_up_readme" not in vfs.flags:
zs, fn = vsplit(vpath)
if fn.lower() in FN_EMB:
vpath = vjoin(zs, "_wo_" + fn)
vfs, rem = self.asrv.vfs.get(vpath, "*", *perms)
if not vfs.realpath: if not vfs.realpath:
raise Exception("unmapped vfs") raise Exception("unmapped vfs")
return vfs, vfs.canonical(rem)
return vfs, vpath, vfs.canonical(rem)
def _ls(self, vpath: str, raddress: str, rport: int, force=False) -> Any: def _ls(self, vpath: str, raddress: str, rport: int, force=False) -> Any:
# generate file listing if vpath is dir.txt and return as file object # generate file listing if vpath is dir.txt and return as file object
@ -285,7 +263,6 @@ class Tftpd(object):
if not ptn or not ptn.match(fn.lower()): if not ptn or not ptn.match(fn.lower()):
return None return None
tsdt = datetime.fromtimestamp
vn, rem = self.asrv.vfs.get(vpath, "*", True, False) vn, rem = self.asrv.vfs.get(vpath, "*", True, False)
fsroot, vfs_ls, vfs_virt = vn.ls( fsroot, vfs_ls, vfs_virt = vn.ls(
rem, rem,
@ -298,7 +275,7 @@ class Tftpd(object):
dirs1 = [(v.st_mtime, v.st_size, k + "/") for k, v in vfs_ls if k in dnames] dirs1 = [(v.st_mtime, v.st_size, k + "/") for k, v in vfs_ls if k in dnames]
fils1 = [(v.st_mtime, v.st_size, k) for k, v in vfs_ls if k not in dnames] fils1 = [(v.st_mtime, v.st_size, k) for k, v in vfs_ls if k not in dnames]
real1 = dirs1 + fils1 real1 = dirs1 + fils1
realt = [(tsdt(max(0, mt), UTC), sz, fn) for mt, sz, fn in real1] realt = [(datetime.fromtimestamp(mt, UTC), sz, fn) for mt, sz, fn in real1]
reals = [ reals = [
( (
"%04d-%02d-%02d %02d:%02d:%02d" "%04d-%02d-%02d %02d:%02d:%02d"
@ -354,7 +331,7 @@ class Tftpd(object):
else: else:
raise Exception("bad mode %s" % (mode,)) raise Exception("bad mode %s" % (mode,))
vfs, vpath, ap = self._v2a("open", vpath, [rd, wr]) vfs, ap = self._v2a("open", vpath, [rd, wr])
if wr: if wr:
if "*" not in vfs.axs.uwrite: if "*" not in vfs.axs.uwrite:
yeet("blocked write; folder not world-writable: /%s" % (vpath,)) yeet("blocked write; folder not world-writable: /%s" % (vpath,))
@ -388,24 +365,18 @@ class Tftpd(object):
if not a: if not a:
a = (self.args.iobuf,) a = (self.args.iobuf,)
ret = open(ap, mode, *a, **ka) return open(ap, mode, *a, **ka)
if wr and "fperms" in vfs.flags:
set_fperms(ret, vfs.flags)
return ret
def _mkdir(self, vpath: str, *a) -> None: def _mkdir(self, vpath: str, *a) -> None:
vfs, _, ap = self._v2a("mkdir", vpath, [False, True]) vfs, ap = self._v2a("mkdir", vpath, [])
if "*" not in vfs.axs.uwrite: if "*" not in vfs.axs.uwrite:
yeet("blocked mkdir; folder not world-writable: /%s" % (vpath,)) yeet("blocked mkdir; folder not world-writable: /%s" % (vpath,))
bos.mkdir(ap, vfs.flags["chmod_d"]) return bos.mkdir(ap)
if "chown" in vfs.flags:
bos.chown(ap, vfs.flags["uid"], vfs.flags["gid"])
def _unlink(self, vpath: str) -> None: def _unlink(self, vpath: str) -> None:
# return bos.unlink(self._v2a("stat", vpath, *a)[1]) # return bos.unlink(self._v2a("stat", vpath, *a)[1])
vfs, _, ap = self._v2a("delete", vpath, [True, False, False, True]) vfs, ap = self._v2a("delete", vpath, [True, False, False, True])
try: try:
inf = bos.stat(ap) inf = bos.stat(ap)
@ -429,7 +400,7 @@ class Tftpd(object):
def _p_exists(self, vpath: str) -> bool: def _p_exists(self, vpath: str) -> bool:
try: try:
ap = self._v2a("p.exists", vpath, [False, False])[2] ap = self._v2a("p.exists", vpath, [False, False])[1]
bos.stat(ap) bos.stat(ap)
return True return True
except: except:
@ -437,7 +408,7 @@ class Tftpd(object):
def _p_isdir(self, vpath: str) -> bool: def _p_isdir(self, vpath: str) -> bool:
try: try:
st = bos.stat(self._v2a("p.isdir", vpath, [False, False])[2]) st = bos.stat(self._v2a("p.isdir", vpath, [False, False])[1])
ret = stat.S_ISDIR(st.st_mode) ret = stat.S_ISDIR(st.st_mode)
return ret return ret
except: except:

View file

@ -1,15 +1,13 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import errno
import os import os
import stat
from .__init__ import TYPE_CHECKING from .__init__ import TYPE_CHECKING
from .authsrv import VFS from .authsrv import VFS
from .bos import bos from .bos import bos
from .th_srv import EXTS_AC, HAVE_WEBP, thumb_path from .th_srv import HAVE_WEBP, thumb_path
from .util import Cooldown, Pebkac from .util import Cooldown
if True: # pylint: disable=using-constant-test if True: # pylint: disable=using-constant-test
from typing import Optional, Union from typing import Optional, Union
@ -18,9 +16,6 @@ if TYPE_CHECKING:
from .httpsrv import HttpSrv from .httpsrv import HttpSrv
IOERROR = "reading the file was denied by the server os; either due to filesystem permissions, selinux, apparmor, or similar:\n%r"
class ThumbCli(object): class ThumbCli(object):
def __init__(self, hsrv: "HttpSrv") -> None: def __init__(self, hsrv: "HttpSrv") -> None:
self.broker = hsrv.broker self.broker = hsrv.broker
@ -36,15 +31,11 @@ class ThumbCli(object):
if not c: if not c:
raise Exception() raise Exception()
except: except:
c = { c = {k: set() for k in ["thumbable", "pil", "vips", "ffi", "ffv", "ffa"]}
k: set()
for k in ["thumbable", "pil", "vips", "raw", "ffi", "ffv", "ffa"]
}
self.thumbable = c["thumbable"] self.thumbable = c["thumbable"]
self.fmt_pil = c["pil"] self.fmt_pil = c["pil"]
self.fmt_vips = c["vips"] self.fmt_vips = c["vips"]
self.fmt_raw = c["raw"]
self.fmt_ffi = c["ffi"] self.fmt_ffi = c["ffi"]
self.fmt_ffv = c["ffv"] self.fmt_ffv = c["ffv"]
self.fmt_ffa = c["ffa"] self.fmt_ffa = c["ffa"]
@ -66,17 +57,13 @@ class ThumbCli(object):
if is_vid and "dvthumb" in dbv.flags: if is_vid and "dvthumb" in dbv.flags:
return None return None
want_opus = fmt in EXTS_AC want_opus = fmt in ("opus", "caf", "mp3")
is_au = ext in self.fmt_ffa is_au = ext in self.fmt_ffa
is_vau = want_opus and ext in self.fmt_ffv is_vau = want_opus and ext in self.fmt_ffv
if is_au or is_vau: if is_au or is_vau:
if want_opus: if want_opus:
if self.args.no_acode: if self.args.no_acode:
return None return None
elif fmt == "caf" and self.args.no_caf:
fmt = "mp3"
elif fmt == "owa" and self.args.no_owa:
fmt = "mp3"
else: else:
if "dathumb" in dbv.flags: if "dathumb" in dbv.flags:
return None return None
@ -92,7 +79,7 @@ class ThumbCli(object):
if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg", "png"]: if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg", "png"]:
return os.path.join(ptop, rem) return os.path.join(ptop, rem)
if fmt[:1] in "jw" and fmt != "wav": if fmt[:1] in "jw":
sfmt = fmt[:1] sfmt = fmt[:1]
if sfmt == "j" and self.args.th_no_jpg: if sfmt == "j" and self.args.th_no_jpg:
@ -133,7 +120,7 @@ class ThumbCli(object):
tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa) tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa)
tpaths = [tpath] tpaths = [tpath]
if fmt[:1] == "w" and fmt != "wav": if fmt == "w":
# also check for jpg (maybe webp is unavailable) # also check for jpg (maybe webp is unavailable)
tpaths.append(tpath.rsplit(".", 1)[0] + ".jpg") tpaths.append(tpath.rsplit(".", 1)[0] + ".jpg")
@ -166,22 +153,8 @@ class ThumbCli(object):
if abort: if abort:
return None return None
ap = os.path.join(ptop, rem) if not bos.path.getsize(os.path.join(ptop, rem)):
try:
st = bos.stat(ap)
if not st.st_size or not stat.S_ISREG(st.st_mode):
return None return None
with open(ap, "rb", 4) as f:
if not f.read(4):
raise Exception()
except OSError as ex:
if ex.errno == errno.ENOENT:
raise Pebkac(404)
else:
raise Pebkac(500, IOERROR % (ex,))
except Exception as ex:
raise Pebkac(500, IOERROR % (ex,))
x = self.broker.ask("thumbsrv.get", ptop, rem, mtime, fmt) x = self.broker.ask("thumbsrv.get", ptop, rem, mtime, fmt)
return x.get() # type: ignore return x.get() # type: ignore

View file

@ -2,13 +2,10 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import hashlib import hashlib
import io
import logging import logging
import os import os
import re
import shutil import shutil
import subprocess as sp import subprocess as sp
import tempfile
import threading import threading
import time import time
@ -21,22 +18,21 @@ from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, au_unpk, ffprobe
from .util import BytesIO # type: ignore from .util import BytesIO # type: ignore
from .util import ( from .util import (
FFMPEG_URL, FFMPEG_URL,
VF_CAREFUL,
Cooldown, Cooldown,
Daemon, Daemon,
afsenc, afsenc,
atomic_move,
fsenc, fsenc,
min_ex, min_ex,
runcmd, runcmd,
statdir, statdir,
ub64enc, ub64enc,
vsplit, vsplit,
wrename,
wunlink, wunlink,
) )
if True: # pylint: disable=using-constant-test if True: # pylint: disable=using-constant-test
from typing import Any, Optional, Union from typing import Optional, Union
if TYPE_CHECKING: if TYPE_CHECKING:
from .svchub import SvcHub from .svchub import SvcHub
@ -50,13 +46,6 @@ HAVE_HEIF = False
HAVE_AVIF = False HAVE_AVIF = False
HAVE_WEBP = False HAVE_WEBP = False
EXTS_TH = set(["jpg", "webp", "png"])
EXTS_AC = set(["opus", "owa", "caf", "mp3", "flac", "wav"])
EXTS_SPEC_SAFE = set("aif aiff flac mp3 opus wav".split())
PTN_TS = re.compile("^-?[0-9a-f]{8,10}$")
try: try:
if os.environ.get("PRTY_NO_PIL"): if os.environ.get("PRTY_NO_PIL"):
raise Exception() raise Exception()
@ -86,9 +75,6 @@ try:
if os.environ.get("PRTY_NO_PIL_HEIF"): if os.environ.get("PRTY_NO_PIL_HEIF"):
raise Exception() raise Exception()
try:
from pillow_heif import register_heif_opener
except ImportError:
from pyheif_pillow_opener import register_heif_opener from pyheif_pillow_opener import register_heif_opener
register_heif_opener() register_heif_opener()
@ -100,10 +86,6 @@ try:
if os.environ.get("PRTY_NO_PIL_AVIF"): if os.environ.get("PRTY_NO_PIL_AVIF"):
raise Exception() raise Exception()
if ".avif" in Image.registered_extensions():
HAVE_AVIF = True
raise Exception()
import pillow_avif # noqa: F401 # pylint: disable=unused-import import pillow_avif # noqa: F401 # pylint: disable=unused-import
HAVE_AVIF = True HAVE_AVIF = True
@ -116,28 +98,14 @@ except:
try: try:
if os.environ.get("PRTY_NO_VIPS"): if os.environ.get("PRTY_NO_VIPS"):
raise ImportError() raise Exception()
HAVE_VIPS = True HAVE_VIPS = True
import pyvips import pyvips
logging.getLogger("pyvips").setLevel(logging.WARNING) logging.getLogger("pyvips").setLevel(logging.WARNING)
except Exception as e:
HAVE_VIPS = False
if not isinstance(e, ImportError):
logging.warning("libvips found, but failed to load: " + str(e))
try:
if os.environ.get("PRTY_NO_RAW"):
raise Exception()
HAVE_RAW = True
import rawpy
logging.getLogger("rawpy").setLevel(logging.WARNING)
except: except:
HAVE_RAW = False HAVE_VIPS = False
th_dir_cache = {} th_dir_cache = {}
@ -171,7 +139,7 @@ def thumb_path(histpath: str, rem: str, mtime: float, fmt: str, ffa: set[str]) -
h = hashlib.sha512(afsenc(fn)).digest() h = hashlib.sha512(afsenc(fn)).digest()
fn = ub64enc(h).decode("ascii")[:24] fn = ub64enc(h).decode("ascii")[:24]
if fmt in EXTS_AC: if fmt in ("opus", "caf", "mp3"):
cat = "ac" cat = "ac"
else: else:
fc = fmt[:1] fc = fmt[:1]
@ -192,15 +160,12 @@ class ThumbSrv(object):
self.mutex = threading.Lock() self.mutex = threading.Lock()
self.busy: dict[str, list[threading.Condition]] = {} self.busy: dict[str, list[threading.Condition]] = {}
self.untemp: dict[str, list[str]] = {}
self.ram: dict[str, float] = {} self.ram: dict[str, float] = {}
self.memcond = threading.Condition(self.mutex) self.memcond = threading.Condition(self.mutex)
self.stopping = False self.stopping = False
self.rm_nullthumbs = True # forget failed conversions on startup self.rm_nullthumbs = True # forget failed conversions on startup
self.nthr = max(1, self.args.th_mt) self.nthr = max(1, self.args.th_mt)
self.exts_spec_unsafe = set(self.args.th_spec_cnv.split(","))
self.q: Queue[Optional[tuple[str, str, str, VFS]]] = Queue(self.nthr * 4) self.q: Queue[Optional[tuple[str, str, str, VFS]]] = Queue(self.nthr * 4)
for n in range(self.nthr): for n in range(self.nthr):
Daemon(self.worker, "thumb-{}-{}".format(n, self.nthr)) Daemon(self.worker, "thumb-{}-{}".format(n, self.nthr))
@ -223,19 +188,11 @@ class ThumbSrv(object):
if self.args.th_clean: if self.args.th_clean:
Daemon(self.cleaner, "thumb.cln") Daemon(self.cleaner, "thumb.cln")
( self.fmt_pil, self.fmt_vips, self.fmt_ffi, self.fmt_ffv, self.fmt_ffa = [
self.fmt_pil,
self.fmt_vips,
self.fmt_raw,
self.fmt_ffi,
self.fmt_ffv,
self.fmt_ffa,
) = [
set(y.split(",")) set(y.split(","))
for y in [ for y in [
self.args.th_r_pil, self.args.th_r_pil,
self.args.th_r_vips, self.args.th_r_vips,
self.args.th_r_raw,
self.args.th_r_ffi, self.args.th_r_ffi,
self.args.th_r_ffv, self.args.th_r_ffv,
self.args.th_r_ffa, self.args.th_r_ffa,
@ -258,9 +215,6 @@ class ThumbSrv(object):
if "vips" in self.args.th_dec: if "vips" in self.args.th_dec:
self.thumbable |= self.fmt_vips self.thumbable |= self.fmt_vips
if "raw" in self.args.th_dec:
self.thumbable |= self.fmt_raw
if "ff" in self.args.th_dec: if "ff" in self.args.th_dec:
for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]: for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]:
self.thumbable |= zss self.thumbable |= zss
@ -298,8 +252,7 @@ class ThumbSrv(object):
self.log("joined waiting room for %r" % (tpath,)) self.log("joined waiting room for %r" % (tpath,))
except: except:
thdir = os.path.dirname(tpath) thdir = os.path.dirname(tpath)
chmod = bos.MKD_700 if self.args.free_umask else bos.MKD_755 bos.makedirs(os.path.join(thdir, "w"))
bos.makedirs(os.path.join(thdir, "w"), vf=chmod)
inf_path = os.path.join(thdir, "dir.txt") inf_path = os.path.join(thdir, "dir.txt")
if not bos.path.exists(inf_path): if not bos.path.exists(inf_path):
@ -314,7 +267,7 @@ class ThumbSrv(object):
vn = next((x for x in allvols if x.realpath == ptop), None) vn = next((x for x in allvols if x.realpath == ptop), None)
if not vn: if not vn:
self.log("ptop %r not in %s" % (ptop, allvols), 3) self.log("ptop %r not in %s" % (ptop, allvols), 3)
vn = self.asrv.vfs.all_aps[0][1][0] vn = self.asrv.vfs.all_aps[0][1]
self.q.put((abspath, tpath, fmt, vn)) self.q.put((abspath, tpath, fmt, vn))
self.log("conv %r :%s \033[0m%r" % (tpath, fmt, abspath), 6) self.log("conv %r :%s \033[0m%r" % (tpath, fmt, abspath), 6)
@ -342,7 +295,6 @@ class ThumbSrv(object):
"thumbable": self.thumbable, "thumbable": self.thumbable,
"pil": self.fmt_pil, "pil": self.fmt_pil,
"vips": self.fmt_vips, "vips": self.fmt_vips,
"raw": self.fmt_raw,
"ffi": self.fmt_ffi, "ffi": self.fmt_ffi,
"ffv": self.fmt_ffv, "ffv": self.fmt_ffv,
"ffa": self.fmt_ffa, "ffa": self.fmt_ffa,
@ -382,13 +334,10 @@ class ThumbSrv(object):
ap_unpk = abspath ap_unpk = abspath
if not bos.path.exists(tpath): if not bos.path.exists(tpath):
tex = tpath.rsplit(".", 1)[-1] want_mp3 = tpath.endswith(".mp3")
want_mp3 = tex == "mp3" want_opus = tpath.endswith(".opus") or tpath.endswith(".caf")
want_opus = tex in ("opus", "owa", "caf") want_png = tpath.endswith(".png")
want_flac = tex == "flac" want_au = want_mp3 or want_opus
want_wav = tex == "wav"
want_png = tex == "png"
want_au = want_mp3 or want_opus or want_flac or want_wav
for lib in self.args.th_dec: for lib in self.args.th_dec:
can_au = lib == "ff" and ( can_au = lib == "ff" and (
ext in self.fmt_ffa or ext in self.fmt_ffv ext in self.fmt_ffa or ext in self.fmt_ffv
@ -398,17 +347,11 @@ class ThumbSrv(object):
funs.append(self.conv_pil) funs.append(self.conv_pil)
elif lib == "vips" and ext in self.fmt_vips: elif lib == "vips" and ext in self.fmt_vips:
funs.append(self.conv_vips) funs.append(self.conv_vips)
elif lib == "raw" and ext in self.fmt_raw:
funs.append(self.conv_raw)
elif can_au and (want_png or want_au): elif can_au and (want_png or want_au):
if want_opus: if want_opus:
funs.append(self.conv_opus) funs.append(self.conv_opus)
elif want_mp3: elif want_mp3:
funs.append(self.conv_mp3) funs.append(self.conv_mp3)
elif want_flac:
funs.append(self.conv_flac)
elif want_wav:
funs.append(self.conv_wav)
elif want_png: elif want_png:
funs.append(self.conv_waves) funs.append(self.conv_waves)
png_ok = True png_ok = True
@ -438,12 +381,8 @@ class ThumbSrv(object):
self.log(msg, c) self.log(msg, c)
if getattr(ex, "returncode", 0) != 321: if getattr(ex, "returncode", 0) != 321:
if fun == funs[-1]: if fun == funs[-1]:
try:
with open(ttpath, "wb") as _: with open(ttpath, "wb") as _:
pass pass
except Exception as ex:
t = "failed to create the file [%s]: %r"
self.log(t % (ttpath, ex), 3)
else: else:
# ffmpeg may spawn empty files on windows # ffmpeg may spawn empty files on windows
try: try:
@ -455,25 +394,14 @@ class ThumbSrv(object):
wunlink(self.log, ap_unpk, vn.flags) wunlink(self.log, ap_unpk, vn.flags)
try: try:
atomic_move(self.log, ttpath, tpath, vn.flags) wrename(self.log, ttpath, tpath, vn.flags)
except Exception as ex: except:
if not os.path.exists(tpath):
t = "failed to move [%s] to [%s]: %r"
self.log(t % (ttpath, tpath, ex), 3)
pass pass
untemp = []
with self.mutex: with self.mutex:
subs = self.busy[tpath] subs = self.busy[tpath]
del self.busy[tpath] del self.busy[tpath]
self.ram.pop(ttpath, None) self.ram.pop(ttpath, None)
untemp = self.untemp.pop(ttpath, None) or []
for ap in untemp:
try:
wunlink(self.log, ap, VF_CAREFUL)
except:
pass
for x in subs: for x in subs:
with x: with x:
@ -512,7 +440,9 @@ class ThumbSrv(object):
return im return im
def conv_image_pil(self, im: "Image.Image", tpath: str, fmt: str, vn: VFS) -> None: def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath)
with Image.open(fsenc(abspath)) as im:
try: try:
im = self.fancy_pillow(im, fmt, vn) im = self.fancy_pillow(im, fmt, vn)
except Exception as ex: except Exception as ex:
@ -540,11 +470,6 @@ class ThumbSrv(object):
im.save(tpath, **args) im.save(tpath, **args)
def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath)
with Image.open(fsenc(abspath)) as im:
self.conv_image_pil(im, tpath, fmt, vn)
def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath) self.wait4ram(0.2, tpath)
crops = ["centre", "none"] crops = ["centre", "none"]
@ -566,53 +491,9 @@ class ThumbSrv(object):
assert img # type: ignore # !rm assert img # type: ignore # !rm
img.write_to_file(tpath, Q=40) img.write_to_file(tpath, Q=40)
def conv_raw(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath)
with rawpy.imread(abspath) as raw:
thumb = raw.extract_thumb()
if thumb.format == rawpy.ThumbFormat.JPEG and tpath.endswith(".jpg"):
# if we have a jpg thumbnail and no webp output is available,
# just write the jpg directly (it'll be the wrong size, but it's fast)
with open(tpath, "wb") as f:
f.write(thumb.data)
if HAVE_VIPS:
crops = ["centre", "none"]
if "f" in fmt:
crops = ["none"]
w, h = self.getres(vn, fmt)
kw = {"height": h, "size": "down", "intent": "relative"}
for c in crops:
try:
kw["crop"] = c
if thumb.format == rawpy.ThumbFormat.BITMAP:
img = pyvips.Image.new_from_array(
thumb.data, interpretation="rgb"
)
img = img.thumbnail_image(w, **kw)
else:
img = pyvips.Image.thumbnail_buffer(thumb.data, w, **kw)
break
except:
if c == crops[-1]:
raise
assert img # type: ignore # !rm
img.write_to_file(tpath, Q=40)
elif HAVE_PIL:
if thumb.format == rawpy.ThumbFormat.BITMAP:
im = Image.fromarray(thumb.data, "RGB")
else:
im = Image.open(io.BytesIO(thumb.data))
self.conv_image_pil(im, tpath, fmt, vn)
else:
raise Exception(
"either pil or vips is needed to process embedded bitmap thumbnails in raw files"
)
def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath) self.wait4ram(0.2, tpath)
ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if not ret: if not ret:
return return
@ -623,17 +504,6 @@ class ThumbSrv(object):
dur = ret[".dur"][1] if ".dur" in ret else 4 dur = ret[".dur"][1] if ".dur" in ret else 4
seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")] seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")]
self._ffmpeg_im(abspath, tpath, fmt, vn, seek, b"0:v:0")
def _ffmpeg_im(
self,
abspath: str,
tpath: str,
fmt: str,
vn: VFS,
seek: list[bytes],
imap: bytes,
) -> None:
scale = "scale={0}:{1}:force_original_aspect_ratio=" scale = "scale={0}:{1}:force_original_aspect_ratio="
if "f" in fmt: if "f" in fmt:
scale += "decrease,setsar=1:1" scale += "decrease,setsar=1:1"
@ -652,7 +522,7 @@ class ThumbSrv(object):
cmd += seek cmd += seek
cmd += [ cmd += [
b"-i", fsenc(abspath), b"-i", fsenc(abspath),
b"-map", imap, b"-map", b"0:v:0",
b"-vf", bscale, b"-vf", bscale,
b"-frames:v", b"1", b"-frames:v", b"1",
b"-metadata:s:v:0", b"rotate=0", b"-metadata:s:v:0", b"rotate=0",
@ -673,11 +543,11 @@ class ThumbSrv(object):
] ]
cmd += [fsenc(tpath)] cmd += [fsenc(tpath)]
self._run_ff(cmd, vn, "convt") self._run_ff(cmd, vn)
def _run_ff(self, cmd: list[bytes], vn: VFS, kto: str, oom: int = 400) -> None: def _run_ff(self, cmd: list[bytes], vn: VFS, oom: int = 400) -> None:
# self.log((b" ".join(cmd)).decode("utf-8")) # self.log((b" ".join(cmd)).decode("utf-8"))
ret, _, serr = runcmd(cmd, timeout=vn.flags[kto], nice=True, oom=oom) ret, _, serr = runcmd(cmd, timeout=vn.flags["convt"], nice=True, oom=oom)
if not ret: if not ret:
return return
@ -721,7 +591,7 @@ class ThumbSrv(object):
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1])) raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
def conv_waves(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: def conv_waves(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in ret: if "ac" not in ret:
raise Exception("not audio") raise Exception("not audio")
@ -759,7 +629,7 @@ class ThumbSrv(object):
# fmt: on # fmt: on
cmd += [fsenc(tpath)] cmd += [fsenc(tpath)]
self._run_ff(cmd, vn, "convt") self._run_ff(cmd, vn)
if "pngquant" in vn.flags: if "pngquant" in vn.flags:
wtpath = tpath + ".png" wtpath = tpath + ".png"
@ -778,70 +648,22 @@ class ThumbSrv(object):
except: except:
pass pass
else: else:
atomic_move(self.log, wtpath, tpath, vn.flags) wrename(self.log, wtpath, tpath, vn.flags)
def conv_emb_cv(
self, abspath: str, tpath: str, fmt: str, vn: VFS, strm: dict[str, Any]
) -> None:
self.wait4ram(0.2, tpath)
self._ffmpeg_im(
abspath, tpath, fmt, vn, [], b"0:" + strm["index"].encode("ascii")
)
def conv_spec(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: def conv_spec(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
ret, raw, strms, ctnr = ffprobe(abspath, int(vn.flags["convt"] / 2)) ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in ret: if "ac" not in ret:
raise Exception("not audio") raise Exception("not audio")
want_spec = vn.flags.get("th_spec_p", 1)
if want_spec < 2:
for strm in strms:
if (
strm.get("codec_type") == "video"
and strm.get("DISPOSITION:attached_pic") == "1"
):
return self.conv_emb_cv(abspath, tpath, fmt, vn, strm)
if not want_spec:
raise Exception("spectrograms forbidden by volflag")
fext = abspath.split(".")[-1].lower()
# https://trac.ffmpeg.org/ticket/10797 # https://trac.ffmpeg.org/ticket/10797
# expect 1 GiB every 600 seconds when duration is tricky; # expect 1 GiB every 600 seconds when duration is tricky;
# simple filetypes are generally safer so let's special-case those # simple filetypes are generally safer so let's special-case those
coeff = 1800 if fext in EXTS_SPEC_SAFE else 600 safe = ("flac", "wav", "aif", "aiff", "opus")
dur = ret[".dur"][1] if ".dur" in ret else 900 coeff = 1800 if abspath.split(".")[-1].lower() in safe else 600
dur = ret[".dur"][1] if ".dur" in ret else 300
need = 0.2 + dur / coeff need = 0.2 + dur / coeff
self.wait4ram(need, tpath) self.wait4ram(need, tpath)
infile = abspath
if dur >= 900 or fext in self.exts_spec_unsafe:
with tempfile.NamedTemporaryFile(suffix=".spec.flac", delete=False) as f:
f.write(b"h")
infile = f.name
try:
self.untemp[tpath].append(infile)
except:
self.untemp[tpath] = [infile]
# fmt: off
cmd = [
b"ffmpeg",
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
b"-i", fsenc(abspath),
b"-map", b"0:a:0",
b"-ac", b"1",
b"-ar", b"48000",
b"-sample_fmt", b"s16",
b"-t", b"900",
b"-y", fsenc(infile),
]
# fmt: on
self._run_ff(cmd, vn, "convt")
fc = "[0:a:0]aresample=48000{},showspectrumpic=s=" fc = "[0:a:0]aresample=48000{},showspectrumpic=s="
if "3" in fmt: if "3" in fmt:
fc += "1280x1024,crop=1420:1056:70:48[o]" fc += "1280x1024,crop=1420:1056:70:48[o]"
@ -861,7 +683,7 @@ class ThumbSrv(object):
b"-nostdin", b"-nostdin",
b"-v", b"error", b"-v", b"error",
b"-hide_banner", b"-hide_banner",
b"-i", fsenc(infile), b"-i", fsenc(abspath),
b"-filter_complex", fc.encode("utf-8"), b"-filter_complex", fc.encode("utf-8"),
b"-map", b"[o]", b"-map", b"[o]",
b"-frames:v", b"1", b"-frames:v", b"1",
@ -882,7 +704,7 @@ class ThumbSrv(object):
] ]
cmd += [fsenc(tpath)] cmd += [fsenc(tpath)]
self._run_ff(cmd, vn, "convt") self._run_ff(cmd, vn)
def conv_mp3(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: def conv_mp3(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
quality = self.args.q_mp3.lower() quality = self.args.q_mp3.lower()
@ -890,7 +712,7 @@ class ThumbSrv(object):
raise Exception("disabled in server config") raise Exception("disabled in server config")
self.wait4ram(0.2, tpath) self.wait4ram(0.2, tpath)
tags, rawtags, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in tags: if "ac" not in tags:
raise Exception("not audio") raise Exception("not audio")
@ -921,148 +743,36 @@ class ThumbSrv(object):
fsenc(tpath) fsenc(tpath)
] ]
# fmt: on # fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300) self._run_ff(cmd, vn, oom=300)
def conv_flac(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
if self.args.no_acode or not self.args.allow_flac:
raise Exception("flac not permitted in server config")
self.wait4ram(0.2, tpath)
tags, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in tags:
raise Exception("not audio")
self.log("conv2 flac", 6)
# fmt: off
cmd = [
b"ffmpeg",
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
b"-i", fsenc(abspath),
b"-map", b"0:a:0",
b"-c:a", b"flac",
fsenc(tpath)
]
# fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300)
def conv_wav(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
if self.args.no_acode or not self.args.allow_wav:
raise Exception("wav not permitted in server config")
self.wait4ram(0.2, tpath)
tags, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in tags:
raise Exception("not audio")
bits = tags[".bps"][1]
if bits == 0.0:
bits = tags[".bprs"][1]
codec = b"pcm_s32le"
if bits <= 16.0:
codec = b"pcm_s16le"
elif bits <= 24.0:
codec = b"pcm_s24le"
self.log("conv2 wav", 6)
# fmt: off
cmd = [
b"ffmpeg",
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
b"-i", fsenc(abspath),
b"-map", b"0:a:0",
b"-c:a", codec,
fsenc(tpath)
]
# fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300)
def conv_opus(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: def conv_opus(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
if self.args.no_acode or not self.args.q_opus: if self.args.no_acode or not self.args.q_opus:
raise Exception("disabled in server config") raise Exception("disabled in server config")
self.wait4ram(0.2, tpath) self.wait4ram(0.2, tpath)
tags, rawtags, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in tags: if "ac" not in tags:
raise Exception("not audio") raise Exception("not audio")
sq = "%dk" % (self.args.q_opus,)
bq = sq.encode("ascii")
if tags["ac"][1] == "opus":
enc = "-c:a copy"
else:
enc = "-c:a libopus -b:a " + sq
fun = self._conv_caf if fmt == "caf" else self._conv_owa
fun(abspath, tpath, tags, rawtags, enc, bq, vn)
def _conv_owa(
self,
abspath: str,
tpath: str,
tags: dict[str, tuple[int, Any]],
rawtags: dict[str, list[Any]],
enc: str,
bq: bytes,
vn: VFS,
) -> None:
if tpath.endswith(".owa"):
container = b"webm"
tagset = [b"-map_metadata", b"-1"]
else:
container = b"opus"
tagset = self.big_tags(rawtags)
self.log("conv2 %s [%s]" % (container, enc), 6)
benc = enc.encode("ascii").split(b" ")
# fmt: off
cmd = [
b"ffmpeg",
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
b"-i", fsenc(abspath),
] + tagset + [
b"-map", b"0:a:0",
] + benc + [
b"-f", container,
fsenc(tpath)
]
# fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300)
def _conv_caf(
self,
abspath: str,
tpath: str,
tags: dict[str, tuple[int, Any]],
rawtags: dict[str, list[Any]],
enc: str,
bq: bytes,
vn: VFS,
) -> None:
tmp_opus = tpath + ".opus"
try:
wunlink(self.log, tmp_opus, vn.flags)
except:
pass
try: try:
dur = tags[".dur"][1] dur = tags[".dur"][1]
except: except:
dur = 0 dur = 0
self.log("conv2 caf-tmp [%s]" % (enc,), 6) src_opus = abspath.lower().endswith(".opus") or tags["ac"][1] == "opus"
benc = enc.encode("ascii").split(b" ") want_caf = tpath.endswith(".caf")
tmp_opus = tpath
if want_caf:
tmp_opus = tpath + ".opus"
try:
wunlink(self.log, tmp_opus, vn.flags)
except:
pass
caf_src = abspath if src_opus else tmp_opus
bq = ("%dk" % (self.args.q_opus,)).encode("ascii")
if not want_caf or not src_opus:
# fmt: off # fmt: off
cmd = [ cmd = [
b"ffmpeg", b"ffmpeg",
@ -1070,24 +780,21 @@ class ThumbSrv(object):
b"-v", b"error", b"-v", b"error",
b"-hide_banner", b"-hide_banner",
b"-i", fsenc(abspath), b"-i", fsenc(abspath),
b"-map_metadata", b"-1", ] + self.big_tags(rawtags) + [
b"-map", b"0:a:0", b"-map", b"0:a:0",
] + benc + [ b"-c:a", b"libopus",
b"-f", b"opus", b"-b:a", bq,
fsenc(tmp_opus) fsenc(tmp_opus)
] ]
# fmt: on # fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300) self._run_ff(cmd, vn, oom=300)
# iOS fails to play some "insufficiently complex" files # iOS fails to play some "insufficiently complex" files
# (average file shorter than 8 seconds), so of course we # (average file shorter than 8 seconds), so of course we
# fix that by mixing in some inaudible pink noise :^) # fix that by mixing in some inaudible pink noise :^)
# 6.3 sec seems like the cutoff so lets do 7, and # 6.3 sec seems like the cutoff so lets do 7, and
# 7 sec of psyqui-musou.opus @ 3:50 is 174 KiB # 7 sec of psyqui-musou.opus @ 3:50 is 174 KiB
sz = bos.path.getsize(tmp_opus) if want_caf and (dur < 20 or bos.path.getsize(caf_src) < 256 * 1024):
if dur < 20 or sz < 256 * 1024:
zs = bq.decode("ascii")
self.log("conv2 caf-transcode; dur=%d sz=%d q=%s" % (dur, sz, zs), 6)
# fmt: off # fmt: off
cmd = [ cmd = [
b"ffmpeg", b"ffmpeg",
@ -1104,18 +811,17 @@ class ThumbSrv(object):
fsenc(tpath) fsenc(tpath)
] ]
# fmt: on # fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300) self._run_ff(cmd, vn, oom=300)
else: elif want_caf:
# simple remux should be safe # simple remux should be safe
self.log("conv2 caf-remux; dur=%d sz=%d" % (dur, sz), 6)
# fmt: off # fmt: off
cmd = [ cmd = [
b"ffmpeg", b"ffmpeg",
b"-nostdin", b"-nostdin",
b"-v", b"error", b"-v", b"error",
b"-hide_banner", b"-hide_banner",
b"-i", fsenc(tmp_opus), b"-i", fsenc(abspath if src_opus else tmp_opus),
b"-map_metadata", b"-1", b"-map_metadata", b"-1",
b"-map", b"0:a:0", b"-map", b"0:a:0",
b"-c:a", b"copy", b"-c:a", b"copy",
@ -1123,8 +829,9 @@ class ThumbSrv(object):
fsenc(tpath) fsenc(tpath)
] ]
# fmt: on # fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300) self._run_ff(cmd, vn, oom=300)
if tmp_opus != tpath:
try: try:
wunlink(self.log, tmp_opus, vn.flags) wunlink(self.log, tmp_opus, vn.flags)
except: except:
@ -1184,7 +891,7 @@ class ThumbSrv(object):
def _clean(self, cat: str, thumbpath: str) -> int: def _clean(self, cat: str, thumbpath: str) -> int:
# self.log("cln {}".format(thumbpath)) # self.log("cln {}".format(thumbpath))
exts = EXTS_TH if cat == "th" else EXTS_AC exts = ["jpg", "webp", "png"] if cat == "th" else ["opus", "caf", "mp3"]
maxage = getattr(self.args, cat + "_maxage") maxage = getattr(self.args, cat + "_maxage")
now = time.time() now = time.time()
prev_b64 = None prev_b64 = None
@ -1225,8 +932,6 @@ class ThumbSrv(object):
# thumb file # thumb file
try: try:
b64, ts, ext = f.split(".") b64, ts, ext = f.split(".")
if len(ts) > 8 and PTN_TS.match(ts):
ts = "yeahokay"
if len(b64) != 24 or len(ts) != 8 or ext not in exts: if len(b64) != 24 or len(ts) != 8 or ext not in exts:
raise Exception() raise Exception()
except: except:

View file

@ -134,9 +134,9 @@ class U2idx(object):
assert sqlite3 # type: ignore # !rm assert sqlite3 # type: ignore # !rm
ptop = vn.realpath ptop = vn.realpath
histpath = self.asrv.vfs.dbpaths.get(ptop) histpath = self.asrv.vfs.histtab.get(ptop)
if not histpath: if not histpath:
self.log("no dbpath for %r" % (ptop,)) self.log("no histpath for %r" % (ptop,))
return None return None
db_path = os.path.join(histpath, "up2k.db") db_path = os.path.join(histpath, "up2k.db")
@ -391,7 +391,7 @@ class U2idx(object):
fk_alg = 2 if "fka" in flags else 1 fk_alg = 2 if "fka" in flags else 1
c = cur.execute(uq, tuple(vuv)) c = cur.execute(uq, tuple(vuv))
for hit in c: for hit in c:
w, ts, sz, rd, fn = hit[:5] w, ts, sz, rd, fn, ip, at = hit[:7]
if rd.startswith("//") or fn.startswith("//"): if rd.startswith("//") or fn.startswith("//"):
rd, fn = s3dec(rd, fn) rd, fn = s3dec(rd, fn)

View file

@ -2,6 +2,7 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import errno import errno
import gzip
import hashlib import hashlib
import json import json
import math import math
@ -41,7 +42,6 @@ from .util import (
fsenc, fsenc,
gen_filekey, gen_filekey,
gen_filekey_dbg, gen_filekey_dbg,
gzip,
hidedir, hidedir,
humansize, humansize,
min_ex, min_ex,
@ -77,7 +77,7 @@ except:
if HAVE_SQLITE3: if HAVE_SQLITE3:
import sqlite3 import sqlite3
DB_VER = 6 DB_VER = 5
if True: # pylint: disable=using-constant-test if True: # pylint: disable=using-constant-test
from typing import Any, Optional, Pattern, Union from typing import Any, Optional, Pattern, Union
@ -86,10 +86,7 @@ if TYPE_CHECKING:
from .svchub import SvcHub from .svchub import SvcHub
zsg = "avif,avifs,bmp,gif,heic,heics,heif,heifs,ico,j2p,j2k,jp2,jpeg,jpg,jpx,png,tga,tif,tiff,webp" zsg = "avif,avifs,bmp,gif,heic,heics,heif,heifs,ico,j2p,j2k,jp2,jpeg,jpg,jpx,png,tga,tif,tiff,webp"
ICV_EXTS = set(zsg.split(",")) CV_EXTS = set(zsg.split(","))
zsg = "3gp,asf,av1,avc,avi,flv,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,vob,webm,wmv"
VCV_EXTS = set(zsg.split(","))
zsg = "nohash noidx xdev xvol" zsg = "nohash noidx xdev xvol"
VF_AFFECTS_INDEXING = set(zsg.split(" ")) VF_AFFECTS_INDEXING = set(zsg.split(" "))
@ -97,7 +94,7 @@ VF_AFFECTS_INDEXING = set(zsg.split(" "))
SBUSY = "cannot receive uploads right now;\nserver busy with %s.\nPlease wait; the client will retry..." SBUSY = "cannot receive uploads right now;\nserver busy with %s.\nPlease wait; the client will retry..."
HINT_HISTPATH = "you could try moving the database to another location (preferably an SSD or NVME drive) using either the --hist argument (global option for all volumes), or the hist volflag (just for this volume), or, if you want to keep the thumbnails in the current location and only move the database itself, then use --dbpath or volflag dbpath" HINT_HISTPATH = "you could try moving the database to another location (preferably an SSD or NVME drive) using either the --hist argument (global option for all volumes), or the hist volflag (just for this volume)"
NULLSTAT = os.stat_result((0, -1, -1, 0, 0, 0, 0, 0, 0, 0)) NULLSTAT = os.stat_result((0, -1, -1, 0, 0, 0, 0, 0, 0, 0))
@ -144,7 +141,6 @@ class Up2k(object):
self.salt = self.args.warksalt self.salt = self.args.warksalt
self.r_hash = re.compile("^[0-9a-zA-Z_-]{44}$") self.r_hash = re.compile("^[0-9a-zA-Z_-]{44}$")
self.abrt_key = ""
self.gid = 0 self.gid = 0
self.gt0 = 0 self.gt0 = 0
@ -376,12 +372,11 @@ class Up2k(object):
if ineed == ihash or not ineed: if ineed == ihash or not ineed:
continue continue
poke = job["poke"]
zt = ( zt = (
ineed / ihash, ineed / ihash,
job["size"], job["size"],
int(job.get("t0c", poke)), int(job["t0c"]),
int(poke), int(job["poke"]),
djoin(vtop, job["prel"], job["name"]), djoin(vtop, job["prel"], job["name"]),
) )
ret.append(zt) ret.append(zt)
@ -404,14 +399,12 @@ class Up2k(object):
return "{}" return "{}"
def get_unfinished_by_user(self, uname, ip) -> dict[str, Any]: def get_unfinished_by_user(self, uname, ip) -> str:
# returns dict due to ExceptionalQueue
if PY2 or not self.reg_mutex.acquire(timeout=2): if PY2 or not self.reg_mutex.acquire(timeout=2):
return {"timeout": 1} return '[{"timeout":1}]'
ret: list[tuple[int, str, int, int, int]] = [] ret: list[tuple[int, str, int, int, int]] = []
userset = set([(uname or "\n"), "*"]) userset = set([(uname or "\n"), "*"])
n = 1000
try: try:
for ptop, tab2 in self.registry.items(): for ptop, tab2 in self.registry.items():
cfg = self.flags.get(ptop, {}).get("u2abort", 1) cfg = self.flags.get(ptop, {}).get("u2abort", 1)
@ -426,6 +419,7 @@ class Up2k(object):
or (addr and addr != job["addr"]) or (addr and addr != job["addr"])
): ):
continue continue
zt5 = ( zt5 = (
int(job["t0"]), int(job["t0"]),
djoin(job["vtop"], job["prel"], job["name"]), djoin(job["vtop"], job["prel"], job["name"]),
@ -434,9 +428,6 @@ class Up2k(object):
len(job["hash"]), len(job["hash"]),
) )
ret.append(zt5) ret.append(zt5)
n -= 1
if not n:
break
finally: finally:
self.reg_mutex.release() self.reg_mutex.release()
@ -453,7 +444,7 @@ class Up2k(object):
} }
for (at, vp, sz, nn, nh) in ret for (at, vp, sz, nn, nh) in ret
] ]
return {"f": ret2} return json.dumps(ret2, separators=(",\n", ": "))
def get_unfinished(self) -> str: def get_unfinished(self) -> str:
if PY2 or not self.reg_mutex.acquire(timeout=0.5): if PY2 or not self.reg_mutex.acquire(timeout=0.5):
@ -566,7 +557,6 @@ class Up2k(object):
else: else:
# important; not deferred by db_act # important; not deferred by db_act
timeout = self._check_lifetimes() timeout = self._check_lifetimes()
timeout = min(self._check_forget_ip(), timeout)
try: try:
if self.args.shr: if self.args.shr:
timeout = min(self._check_shares(), timeout) timeout = min(self._check_shares(), timeout)
@ -627,43 +617,6 @@ class Up2k(object):
for v in vols: for v in vols:
volage[v] = now volage[v] = now
def _check_forget_ip(self) -> float:
now = time.time()
timeout = now + 9001
for vp, vol in sorted(self.vfs.all_vols.items()):
maxage = vol.flags["forget_ip"]
if not maxage:
continue
cur = self.cur.get(vol.realpath)
if not cur:
continue
cutoff = now - maxage * 60
for _ in range(2):
q = "select ip, at from up where ip > '' order by +at limit 1"
hits = cur.execute(q).fetchall()
if not hits:
break
remains = hits[0][1] - cutoff
if remains > 0:
timeout = min(timeout, now + remains)
break
q = "update up set ip = '' where ip > '' and at <= %d"
cur.execute(q % (cutoff,))
zi = cur.rowcount
cur.connection.commit()
t = "forget-ip(%d) removed %d IPs from db [/%s]"
self.log(t % (maxage, zi, vol.vpath))
timeout = min(timeout, now + 900)
return timeout
def _check_lifetimes(self) -> float: def _check_lifetimes(self) -> float:
now = time.time() now = time.time()
timeout = now + 9001 timeout = now + 9001
@ -903,7 +856,7 @@ class Up2k(object):
self.iacct = self.asrv.iacct self.iacct = self.asrv.iacct
self.grps = self.asrv.grps self.grps = self.asrv.grps
have_e2d = self.args.have_idp_hdrs or self.args.chpw or self.args.shr have_e2d = self.args.idp_h_usr or self.args.chpw or self.args.shr
vols = list(all_vols.values()) vols = list(all_vols.values())
t0 = time.time() t0 = time.time()
@ -924,8 +877,7 @@ class Up2k(object):
# only need to protect register_vpath but all in one go feels right # only need to protect register_vpath but all in one go feels right
for vol in vols: for vol in vols:
try: try:
# mkdir gonna happen at snap anyways; bos.makedirs(vol.realpath) # gonna happen at snap anyways
bos.makedirs(vol.realpath, vf=vol.flags)
dir_is_empty(self.log_func, not self.args.no_scandir, vol.realpath) dir_is_empty(self.log_func, not self.args.no_scandir, vol.realpath)
except Exception as ex: except Exception as ex:
self.volstate[vol.vpath] = "OFFLINE (cannot access folder)" self.volstate[vol.vpath] = "OFFLINE (cannot access folder)"
@ -1106,9 +1058,9 @@ class Up2k(object):
self, ptop: str, flags: dict[str, Any] self, ptop: str, flags: dict[str, Any]
) -> Optional[tuple["sqlite3.Cursor", str]]: ) -> Optional[tuple["sqlite3.Cursor", str]]:
"""mutex(main,reg) me""" """mutex(main,reg) me"""
histpath = self.vfs.dbpaths.get(ptop) histpath = self.vfs.histtab.get(ptop)
if not histpath: if not histpath:
self.log("no dbpath for %r" % (ptop,)) self.log("no histpath for %r" % (ptop,))
return None return None
db_path = os.path.join(histpath, "up2k.db") db_path = os.path.join(histpath, "up2k.db")
@ -1129,7 +1081,7 @@ class Up2k(object):
ft = "\033[0;32m{}{:.0}" ft = "\033[0;32m{}{:.0}"
ff = "\033[0;35m{}{:.0}" ff = "\033[0;35m{}{:.0}"
fv = "\033[0;36m{}:\033[90m{}" fv = "\033[0;36m{}:\033[90m{}"
zs = "ext_th_d html_head put_name2 mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v" zs = "html_head mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot"
fx = set(zs.split()) fx = set(zs.split())
fd = vf_bmap() fd = vf_bmap()
fd.update(vf_cmap()) fd.update(vf_cmap())
@ -1151,20 +1103,6 @@ class Up2k(object):
del fl[k1] del fl[k1]
else: else:
fl[k1] = ",".join(x for x in fl[k1]) fl[k1] = ",".join(x for x in fl[k1])
if fl["chmod_d"] == int(self.args.chmod_d, 8):
fl.pop("chmod_d")
try:
if fl["chmod_f"] == int(self.args.chmod_f or "-1", 8):
fl.pop("chmod_f")
except:
pass
for k in ("chmod_f", "chmod_d"):
try:
fl[k] = "%o" % (fl[k])
except:
pass
a = [ a = [
(ft if v is True else ff if v is False else fv).format(k, str(v)) (ft if v is True else ff if v is False else fv).format(k, str(v))
for k, v in fl.items() for k, v in fl.items()
@ -1368,15 +1306,12 @@ class Up2k(object):
] ]
excl += [absreal(x) for x in excl] excl += [absreal(x) for x in excl]
excl += list(self.vfs.histtab.values()) excl += list(self.vfs.histtab.values())
excl += list(self.vfs.dbpaths.values())
if WINDOWS: if WINDOWS:
excl = [x.replace("/", "\\") for x in excl] excl = [x.replace("/", "\\") for x in excl]
else: else:
# ~/.wine/dosdevices/z:/ and such # ~/.wine/dosdevices/z:/ and such
excl.extend(("/dev", "/proc", "/run", "/sys")) excl.extend(("/dev", "/proc", "/run", "/sys"))
excl = list({k: 1 for k in excl})
if self.args.re_dirsz: if self.args.re_dirsz:
db.c.execute("delete from ds") db.c.execute("delete from ds")
db.n += 1 db.n += 1
@ -1388,10 +1323,6 @@ class Up2k(object):
t = "volume /%s at [%s] is empty; will not be indexed as this could be due to an offline filesystem" t = "volume /%s at [%s] is empty; will not be indexed as this could be due to an offline filesystem"
self.log(t % (vol.vpath, rtop), 6) self.log(t % (vol.vpath, rtop), 6)
return True, False return True, False
if not vol.check_landmarks():
t = "volume /%s at [%s] will not be indexed due to bad landmarks"
self.log(t % (vol.vpath, rtop), 6)
return True, False
n_add, _, _ = self._build_dir( n_add, _, _ = self._build_dir(
db, db,
@ -1483,7 +1414,7 @@ class Up2k(object):
unreg: list[str] = [] unreg: list[str] = []
files: list[tuple[int, int, str]] = [] files: list[tuple[int, int, str]] = []
fat32 = True fat32 = True
cv = vcv = "" cv = ""
th_cvd = self.args.th_coversd th_cvd = self.args.th_coversd
th_cvds = self.args.th_coversd_set th_cvds = self.args.th_coversd_set
@ -1578,24 +1509,25 @@ class Up2k(object):
rsz += sz rsz += sz
files.append((sz, lmod, iname)) files.append((sz, lmod, iname))
if sz:
liname = iname.lower() liname = iname.lower()
ext = liname.rsplit(".", 1)[-1]
if ( if (
sz
and (
liname in th_cvds liname in th_cvds
or (not cv and ext in ICV_EXTS and not iname.startswith(".")) or (
) and ( not cv
and liname.rsplit(".", 1)[-1] in CV_EXTS
and not iname.startswith(".")
)
)
and (
not cv not cv
or liname not in th_cvds or liname not in th_cvds
or cv.lower() not in th_cvds or cv.lower() not in th_cvds
or th_cvd.index(liname) < th_cvd.index(cv.lower()) or th_cvd.index(liname) < th_cvd.index(cv.lower())
)
): ):
cv = iname cv = iname
elif not vcv and ext in VCV_EXTS and not iname.startswith("."):
vcv = iname
if not cv:
cv = vcv
if not self.args.no_dirsz: if not self.args.no_dirsz:
tnf += len(files) tnf += len(files)
@ -1655,7 +1587,7 @@ class Up2k(object):
abspath = cdirs + fn abspath = cdirs + fn
nohash = reh.search(abspath) if reh else False nohash = reh.search(abspath) if reh else False
sql = "select w, mt, sz, ip, at, un from up where rd = ? and fn = ?" sql = "select w, mt, sz, ip, at from up where rd = ? and fn = ?"
try: try:
c = db.c.execute(sql, (rd, fn)) c = db.c.execute(sql, (rd, fn))
except: except:
@ -1664,7 +1596,7 @@ class Up2k(object):
in_db = list(c.fetchall()) in_db = list(c.fetchall())
if in_db: if in_db:
self.pp.n -= 1 self.pp.n -= 1
dw, dts, dsz, ip, at, un = in_db[0] dw, dts, dsz, ip, at = in_db[0]
if len(in_db) > 1: if len(in_db) > 1:
t = "WARN: multiple entries: %r => %r |%d|\n%r" t = "WARN: multiple entries: %r => %r |%d|\n%r"
rep_db = "\n".join([repr(x) for x in in_db]) rep_db = "\n".join([repr(x) for x in in_db])
@ -1677,9 +1609,6 @@ class Up2k(object):
if dts == lmod and dsz == sz and (nohash or dw[0] != "#" or not sz): if dts == lmod and dsz == sz and (nohash or dw[0] != "#" or not sz):
continue continue
if un is None:
un = ""
t = "reindex %r => %r mtime(%s/%s) size(%s/%s)" t = "reindex %r => %r mtime(%s/%s) size(%s/%s)"
self.log(t % (top, rp, dts, lmod, dsz, sz)) self.log(t % (top, rp, dts, lmod, dsz, sz))
self.db_rm(db.c, rd, fn, 0) self.db_rm(db.c, rd, fn, 0)
@ -1690,7 +1619,6 @@ class Up2k(object):
dw = "" dw = ""
ip = "" ip = ""
at = 0 at = 0
un = ""
self.pp.msg = "a%d %s" % (self.pp.n, abspath) self.pp.msg = "a%d %s" % (self.pp.n, abspath)
@ -1716,10 +1644,9 @@ class Up2k(object):
if dw and dw != wark: if dw and dw != wark:
ip = "" ip = ""
at = 0 at = 0
un = ""
# skip upload hooks by not providing vflags # skip upload hooks by not providing vflags
self.db_add(db.c, {}, rd, fn, lmod, sz, "", "", wark, wark, "", un, ip, at) self.db_add(db.c, {}, rd, fn, lmod, sz, "", "", wark, wark, "", "", ip, at)
db.n += 1 db.n += 1
db.nf += 1 db.nf += 1
tfa += 1 tfa += 1
@ -2152,12 +2079,11 @@ class Up2k(object):
return -1 return -1
w = bw[:-1].decode("ascii") w = bw[:-1].decode("ascii")
w16 = w[:16]
with self.mutex: with self.mutex:
try: try:
q = "select rd, fn, ip, at, un from up where substr(w,1,16)=? and +w=?" q = "select rd, fn, ip, at from up where substr(w,1,16)=? and +w=?"
rd, fn, ip, at, un = cur.execute(q, (w16, w)).fetchone() rd, fn, ip, at = cur.execute(q, (w[:16], w)).fetchone()
except: except:
# file modified/deleted since spooling # file modified/deleted since spooling
continue continue
@ -2166,25 +2092,18 @@ class Up2k(object):
rd, fn = s3dec(rd, fn) rd, fn = s3dec(rd, fn)
if "mtp" in flags: if "mtp" in flags:
q = "select 1 from mt where w=? and +k='t:mtp' limit 1"
if cur.execute(q, (w16,)).fetchone():
continue
q = "insert into mt values (?,'t:mtp','a')" q = "insert into mt values (?,'t:mtp','a')"
cur.execute(q, (w16,)) cur.execute(q, (w[:16],))
abspath = djoin(ptop, rd, fn) abspath = djoin(ptop, rd, fn)
self.pp.msg = "c%d %s" % (nq, abspath) self.pp.msg = "c%d %s" % (nq, abspath)
if not mpool: if not mpool:
n_tags = self._tagscan_file(cur, entags, w, abspath, ip, at, un) n_tags = self._tagscan_file(cur, entags, w, abspath, ip, at)
else:
if ip:
oth_tags = {"up_ip": ip, "up_at": at}
else: else:
oth_tags = {} oth_tags = {}
if ip:
oth_tags["up_ip"] = ip
if at:
oth_tags["up_at"] = at
if un:
oth_tags["up_by"] = un
mpool.put(Mpqe({}, entags, w, abspath, oth_tags)) mpool.put(Mpqe({}, entags, w, abspath, oth_tags))
with self.mutex: with self.mutex:
@ -2230,7 +2149,7 @@ class Up2k(object):
return tf, -1 return tf, -1
if flt == 1: if flt == 1:
q = "select 1 from mt where w=? and +k != 't:mtp'" q = "select w from mt where w = ?"
if c2.execute(q, (row[0][:16],)).fetchone(): if c2.execute(q, (row[0][:16],)).fetchone():
continue continue
@ -2340,8 +2259,8 @@ class Up2k(object):
if w in in_progress: if w in in_progress:
continue continue
q = "select rd, fn, ip, at, un from up where substr(w,1,16)=? limit 1" q = "select rd, fn, ip, at from up where substr(w,1,16)=? limit 1"
rd, fn, ip, at, un = cur.execute(q, (w,)).fetchone() rd, fn, ip, at = cur.execute(q, (w,)).fetchone()
rd, fn = s3dec(rd, fn) rd, fn = s3dec(rd, fn)
abspath = djoin(ptop, rd, fn) abspath = djoin(ptop, rd, fn)
@ -2365,10 +2284,7 @@ class Up2k(object):
if ip: if ip:
oth_tags["up_ip"] = ip oth_tags["up_ip"] = ip
if at:
oth_tags["up_at"] = at oth_tags["up_at"] = at
if un:
oth_tags["up_by"] = un
jobs.append(Mpqe(parsers, set(), w, abspath, oth_tags)) jobs.append(Mpqe(parsers, set(), w, abspath, oth_tags))
in_progress[w] = True in_progress[w] = True
@ -2557,7 +2473,6 @@ class Up2k(object):
abspath: str, abspath: str,
ip: str, ip: str,
at: float, at: float,
un: Optional[str],
) -> int: ) -> int:
"""will mutex(main)""" """will mutex(main)"""
assert self.mtag # !rm assert self.mtag # !rm
@ -2578,10 +2493,7 @@ class Up2k(object):
if ip: if ip:
tags["up_ip"] = ip tags["up_ip"] = ip
if at:
tags["up_at"] = at tags["up_at"] = at
if un:
tags["up_by"] = un
with self.mutex: with self.mutex:
return self._tag_file(write_cur, entags, wark, abspath, tags) return self._tag_file(write_cur, entags, wark, abspath, tags)
@ -2685,19 +2597,16 @@ class Up2k(object):
if not existed and ver is None: if not existed and ver is None:
return self._try_create_db(db_path, cur) return self._try_create_db(db_path, cur)
for upver in (4, 5): if ver == 4:
if ver != upver:
continue
try: try:
t = "creating backup before upgrade: " t = "creating backup before upgrade: "
cur = self._backup_db(db_path, cur, ver, t) cur = self._backup_db(db_path, cur, ver, t)
getattr(self, "_upgrade_v%d" % (upver,))(cur) self._upgrade_v4(cur)
ver += 1 # type: ignore ver = 5
except: except:
self.log("WARN: failed to upgrade from v%d" % (ver,), 3) self.log("WARN: failed to upgrade from v4", 3)
if ver == DB_VER: if ver == DB_VER:
# these no longer serve their intended purpose but they're great as additional sanchks
self._add_dhash_tab(cur) self._add_dhash_tab(cur)
self._add_xiu_tab(cur) self._add_xiu_tab(cur)
self._add_cv_tab(cur) self._add_cv_tab(cur)
@ -2799,7 +2708,7 @@ class Up2k(object):
idx = r"create index up_w on up(w)" idx = r"create index up_w on up(w)"
for cmd in [ for cmd in [
r"create table up (w text, mt int, sz int, rd text, fn text, ip text, at int, un text)", r"create table up (w text, mt int, sz int, rd text, fn text, ip text, at int)",
r"create index up_vp on up(rd, fn)", r"create index up_vp on up(rd, fn)",
r"create index up_fn on up(fn)", r"create index up_fn on up(fn)",
r"create index up_ip on up(ip)", r"create index up_ip on up(ip)",
@ -2832,15 +2741,6 @@ class Up2k(object):
cur.connection.commit() cur.connection.commit()
def _upgrade_v5(self, cur: "sqlite3.Cursor") -> None:
for cmd in [
r"alter table up add column un text",
r"update kv set v=6 where k='sver'",
]:
cur.execute(cmd)
cur.connection.commit()
def _add_dhash_tab(self, cur: "sqlite3.Cursor") -> None: def _add_dhash_tab(self, cur: "sqlite3.Cursor") -> None:
# v5 -> v5a # v5 -> v5a
try: try:
@ -2862,7 +2762,7 @@ class Up2k(object):
# v5a -> v5b # v5a -> v5b
# store rd+fn rather than warks to support nohash vols # store rd+fn rather than warks to support nohash vols
try: try:
cur.execute("select c, w, rd, fn from iu limit 1").fetchone() cur.execute("select ws, rd, fn from iu limit 1").fetchone()
return return
except: except:
pass pass
@ -2980,6 +2880,7 @@ class Up2k(object):
if ptop not in self.registry: if ptop not in self.registry:
raise Pebkac(410, "location unavailable") raise Pebkac(410, "location unavailable")
cj["name"] = sanitize_fn(cj["name"], "")
cj["poke"] = now = self.db_act = self.vol_act[ptop] = time.time() cj["poke"] = now = self.db_act = self.vol_act[ptop] = time.time()
wark = dwark = self._get_wark(cj) wark = dwark = self._get_wark(cj)
job = None job = None
@ -3015,14 +2916,9 @@ class Up2k(object):
self.salt, cj["size"], cj["lmod"], cj["prel"], cj["name"] self.salt, cj["size"], cj["lmod"], cj["prel"], cj["name"]
) )
zi = cj["lmod"] if vfs.flags.get("up_ts", "") == "fu" or not cj["lmod"]:
bad_mt = zi <= 0 or zi > 0xAAAAAAAA
if bad_mt or vfs.flags.get("up_ts", "") == "fu":
# force upload time rather than last-modified # force upload time rather than last-modified
cj["lmod"] = int(time.time()) cj["lmod"] = int(time.time())
if zi and bad_mt:
t = "ignoring impossible last-modified time from client: %s"
self.log(t % (zi,), 6)
alts: list[tuple[int, int, dict[str, Any], "sqlite3.Cursor", str, str]] = [] alts: list[tuple[int, int, dict[str, Any], "sqlite3.Cursor", str, str]] = []
for ptop, cur in vols: for ptop, cur in vols:
@ -3038,7 +2934,7 @@ class Up2k(object):
argv = [dwark[:16], dwark] argv = [dwark[:16], dwark]
c2 = cur.execute(q, tuple(argv)) c2 = cur.execute(q, tuple(argv))
for _, dtime, dsize, dp_dir, dp_fn, ip, at, _ in c2: for _, dtime, dsize, dp_dir, dp_fn, ip, at in c2:
if dp_dir.startswith("//") or dp_fn.startswith("//"): if dp_dir.startswith("//") or dp_fn.startswith("//"):
dp_dir, dp_fn = s3dec(dp_dir, dp_fn) dp_dir, dp_fn = s3dec(dp_dir, dp_fn)
@ -3290,16 +3186,14 @@ class Up2k(object):
if hr.get("reloc"): if hr.get("reloc"):
x = pathmod(self.vfs, dst, vp, hr["reloc"]) x = pathmod(self.vfs, dst, vp, hr["reloc"])
if x: if x:
ud1 = (vfs.vpath, job["prel"], job["name"]) zvfs = vfs
pdir, _, job["name"], (vfs, rem) = x pdir, _, job["name"], (vfs, rem) = x
dst = os.path.join(pdir, job["name"]) dst = os.path.join(pdir, job["name"])
job["vcfg"] = vfs.flags job["vcfg"] = vfs.flags
job["ptop"] = vfs.realpath job["ptop"] = vfs.realpath
job["vtop"] = vfs.vpath job["vtop"] = vfs.vpath
job["prel"] = rem job["prel"] = rem
job["name"] = sanitize_fn(job["name"], "") if zvfs.vpath != vfs.vpath:
ud2 = (vfs.vpath, job["prel"], job["name"])
if ud1 != ud2:
# print(json.dumps(job, sort_keys=True, indent=4)) # print(json.dumps(job, sort_keys=True, indent=4))
job["hash"] = cj["hash"] job["hash"] = cj["hash"]
self.log("xbu reloc1:%d..." % (depth,), 6) self.log("xbu reloc1:%d..." % (depth,), 6)
@ -3344,7 +3238,7 @@ class Up2k(object):
reg, reg,
"up2k._get_volsize", "up2k._get_volsize",
) )
bos.makedirs(ap2, vf=vfs.flags) bos.makedirs(ap2)
vfs.lim.nup(cj["addr"]) vfs.lim.nup(cj["addr"])
vfs.lim.bup(cj["addr"], cj["size"]) vfs.lim.bup(cj["addr"], cj["size"])
@ -3441,26 +3335,16 @@ class Up2k(object):
return fname return fname
fp = djoin(fdir, fname) fp = djoin(fdir, fname)
if job.get("replace") and bos.path.exists(fp):
ow = job.get("replace") and bos.path.exists(fp)
if ow and "mt" in str(job["replace"]).lower():
mts = bos.stat(fp).st_mtime
mtc = job["lmod"]
if mtc < mts:
t = "will not overwrite; server %d sec newer than client; %d > %d %r"
self.log(t % (mts - mtc, mts, mtc, fp))
ow = False
ptop = job["ptop"]
vf = self.flags.get(ptop) or {}
if ow:
self.log("replacing existing file at %r" % (fp,)) self.log("replacing existing file at %r" % (fp,))
cur = None cur = None
ptop = job["ptop"]
vf = self.flags.get(ptop) or {}
st = bos.stat(fp) st = bos.stat(fp)
try: try:
vrel = vjoin(job["prel"], fname) vrel = vjoin(job["prel"], fname)
xlink = bool(vf.get("xlink")) xlink = bool(vf.get("xlink"))
cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, vrel) cur, wark, _, _, _, _ = self._find_from_vpath(ptop, vrel)
self._forget_file(ptop, vrel, cur, wark, True, st.st_size, xlink) self._forget_file(ptop, vrel, cur, wark, True, st.st_size, xlink)
except Exception as ex: except Exception as ex:
self.log("skipping replace-relink: %r" % (ex,)) self.log("skipping replace-relink: %r" % (ex,))
@ -3475,13 +3359,8 @@ class Up2k(object):
else: else:
dip = self.hub.iphash.s(ip) dip = self.hub.iphash.s(ip)
f, ret = ren_open( suffix = "-%.6f-%s" % (ts, dip)
fname, f, ret = ren_open(fname, "wb", fdir=fdir, suffix=suffix)
"wb",
fdir=fdir,
suffix="-%.6f-%s" % (ts, dip),
vf=vf,
)
f.close() f.close()
return ret return ret
@ -3494,7 +3373,6 @@ class Up2k(object):
rm: bool = False, rm: bool = False,
lmod: float = 0, lmod: float = 0,
fsrc: Optional[str] = None, fsrc: Optional[str] = None,
is_mv: bool = False,
) -> None: ) -> None:
if src == dst or (fsrc and fsrc == dst): if src == dst or (fsrc and fsrc == dst):
t = "symlinking a file to itself?? orig(%s) fsrc(%s) link(%s)" t = "symlinking a file to itself?? orig(%s) fsrc(%s) link(%s)"
@ -3511,9 +3389,7 @@ class Up2k(object):
linked = False linked = False
try: try:
if "reflink" in flags: if not flags.get("dedup"):
raise Exception("reflink")
if not is_mv and not flags.get("dedup"):
raise Exception("dedup is disabled in config") raise Exception("dedup is disabled in config")
lsrc = src lsrc = src
@ -3569,7 +3445,6 @@ class Up2k(object):
linked = True linked = True
except Exception as ex: except Exception as ex:
if str(ex) != "reflink":
self.log("cannot link; creating copy: " + repr(ex)) self.log("cannot link; creating copy: " + repr(ex))
if bos.path.isfile(src): if bos.path.isfile(src):
csrc = src csrc = src
@ -3780,9 +3655,8 @@ class Up2k(object):
if self.idx_wark(vflags, *z2): if self.idx_wark(vflags, *z2):
del self.registry[ptop][wark] del self.registry[ptop][wark]
else: else:
for k in "host tnam busy sprs poke".split(): for k in "host tnam busy sprs poke t0c".split():
del job[k] del job[k]
job.pop("t0c", None)
job["t0"] = int(job["t0"]) job["t0"] = int(job["t0"])
job["hash"] = [] job["hash"] = []
job["done"] = 1 job["done"] = 1
@ -3915,16 +3789,16 @@ class Up2k(object):
db_ip = "" db_ip = ""
else: else:
# plugins may expect this to look like an actual IP # plugins may expect this to look like an actual IP
db_ip = "1.1.1.1" if "no_db_ip" in vflags else ip db_ip = "1.1.1.1" if self.args.no_db_ip else ip
sql = "insert into up values (?,?,?,?,?,?,?,?)" sql = "insert into up values (?,?,?,?,?,?,?)"
v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0), usr) v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0))
try: try:
db.execute(sql, v) db.execute(sql, v)
except: except:
assert self.mem_cur # !rm assert self.mem_cur # !rm
rd, fn = s3enc(self.mem_cur, rd, fn) rd, fn = s3enc(self.mem_cur, rd, fn)
v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0), usr) v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0))
db.execute(sql, v) db.execute(sql, v)
self.volsize[db] += sz self.volsize[db] += sz
@ -4016,9 +3890,6 @@ class Up2k(object):
except: except:
pass pass
def handle_fs_abrt(self, akey: str) -> None:
self.abrt_key = akey
def handle_rm( def handle_rm(
self, self,
uname: str, uname: str,
@ -4065,7 +3936,7 @@ class Up2k(object):
vn, rem = vn0.get_dbv(rem0) vn, rem = vn0.get_dbv(rem0)
ptop = vn.realpath ptop = vn.realpath
with self.mutex, self.reg_mutex: with self.mutex, self.reg_mutex:
abrt_cfg = vn.flags.get("u2abort", 1) abrt_cfg = self.flags.get(ptop, {}).get("u2abort", 1)
addr = (ip or "\n") if abrt_cfg in (1, 2) else "" addr = (ip or "\n") if abrt_cfg in (1, 2) else ""
user = ((uname or "\n"), "*") if abrt_cfg in (1, 3) else None user = ((uname or "\n"), "*") if abrt_cfg in (1, 3) else None
reg = self.registry.get(ptop, {}) if abrt_cfg else {} reg = self.registry.get(ptop, {}) if abrt_cfg else {}
@ -4086,22 +3957,17 @@ class Up2k(object):
if partial: if partial:
dip = ip dip = ip
dat = time.time() dat = time.time()
dun = uname
un_cfg = 1
else: else:
un_cfg = vn.flags["unp_who"] if not self.args.unpost:
if not self.args.unpost or not un_cfg:
t = "the unpost feature is disabled in server config" t = "the unpost feature is disabled in server config"
raise Pebkac(400, t) raise Pebkac(400, t)
_, _, _, _, dip, dat, dun = self._find_from_vpath(ptop, rem) _, _, _, _, dip, dat = self._find_from_vpath(ptop, rem)
t = "you cannot delete this: " t = "you cannot delete this: "
if not dip: if not dip:
t += "file not found" t += "file not found"
elif dip != ip and un_cfg in (1, 2): elif dip != ip:
t += "not uploaded by (You)"
elif dun != uname and un_cfg in (1, 3):
t += "not uploaded by (You)" t += "not uploaded by (You)"
elif dat < time.time() - self.args.unpost: elif dat < time.time() - self.args.unpost:
t += "uploaded too long ago" t += "uploaded too long ago"
@ -4190,7 +4056,7 @@ class Up2k(object):
try: try:
ptop = dbv.realpath ptop = dbv.realpath
xlink = bool(dbv.flags.get("xlink")) xlink = bool(dbv.flags.get("xlink"))
cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, volpath) cur, wark, _, _, _, _ = self._find_from_vpath(ptop, volpath)
self._forget_file( self._forget_file(
ptop, volpath, cur, wark, True, st.st_size, xlink ptop, volpath, cur, wark, True, st.st_size, xlink
) )
@ -4233,7 +4099,7 @@ class Up2k(object):
return n_files, ok + ok2, ng + ng2 return n_files, ok + ok2, ng + ng2
def handle_cp(self, abrt: str, uname: str, ip: str, svp: str, dvp: str) -> str: def handle_cp(self, uname: str, ip: str, svp: str, dvp: str) -> str:
if svp == dvp or dvp.startswith(svp + "/"): if svp == dvp or dvp.startswith(svp + "/"):
raise Pebkac(400, "cp: cannot copy parent into subfolder") raise Pebkac(400, "cp: cannot copy parent into subfolder")
@ -4280,8 +4146,6 @@ class Up2k(object):
dvpf = dvp + svpf[len(svp) :] dvpf = dvp + svpf[len(svp) :]
self._cp_file(uname, ip, svpf, dvpf, curs) self._cp_file(uname, ip, svpf, dvpf, curs)
if abrt and abrt == self.abrt_key:
raise Pebkac(400, "filecopy aborted by http-api")
for v in curs: for v in curs:
v.connection.commit() v.connection.commit()
@ -4349,9 +4213,9 @@ class Up2k(object):
self.log(t, 1) self.log(t, 1)
raise Pebkac(405, t) raise Pebkac(405, t)
bos.makedirs(os.path.dirname(dabs), vf=dvn.flags) bos.makedirs(os.path.dirname(dabs))
c1, w, ftime_, fsize_, ip, at, un = self._find_from_vpath( c1, w, ftime_, fsize_, ip, at = self._find_from_vpath(
svn_dbv.realpath, srem_dbv svn_dbv.realpath, srem_dbv
) )
c2 = self.cur.get(dvn.realpath) c2 = self.cur.get(dvn.realpath)
@ -4376,7 +4240,7 @@ class Up2k(object):
w, w,
w, w,
"", "",
un or "", "",
ip or "", ip or "",
at or 0, at or 0,
) )
@ -4449,7 +4313,7 @@ class Up2k(object):
return "k" return "k"
def handle_mv(self, abrt: str, uname: str, ip: str, svp: str, dvp: str) -> str: def handle_mv(self, uname: str, ip: str, svp: str, dvp: str) -> str:
if svp == dvp or dvp.startswith(svp + "/"): if svp == dvp or dvp.startswith(svp + "/"):
raise Pebkac(400, "mv: cannot move parent into subfolder") raise Pebkac(400, "mv: cannot move parent into subfolder")
@ -4504,8 +4368,6 @@ class Up2k(object):
dvpf = dvp + svpf[len(svp) :] dvpf = dvp + svpf[len(svp) :]
self._mv_file(uname, ip, svpf, dvpf, curs) self._mv_file(uname, ip, svpf, dvpf, curs)
if abrt and abrt == self.abrt_key:
raise Pebkac(400, "filemove aborted by http-api")
for v in curs: for v in curs:
v.connection.commit() v.connection.commit()
@ -4527,10 +4389,7 @@ class Up2k(object):
vp = vjoin(dvp, rem) vp = vjoin(dvp, rem)
try: try:
dvn, drem = self.vfs.get(vp, uname, False, True) dvn, drem = self.vfs.get(vp, uname, False, True)
dap = dvn.canonical(drem) bos.mkdir(dvn.canonical(drem))
bos.mkdir(dap, dvn.flags["chmod_d"])
if "chown" in dvn.flags:
bos.chown(dap, dvn.flags["uid"], dvn.flags["gid"])
except: except:
pass pass
@ -4600,7 +4459,7 @@ class Up2k(object):
is_xvol = svn.realpath != dvn.realpath is_xvol = svn.realpath != dvn.realpath
bos.makedirs(os.path.dirname(dabs), vf=dvn.flags) bos.makedirs(os.path.dirname(dabs))
if is_dirlink: if is_dirlink:
dlabs = absreal(sabs) dlabs = absreal(sabs)
@ -4637,7 +4496,7 @@ class Up2k(object):
return "k" return "k"
c1, w, ftime_, fsize_, ip, at, un = self._find_from_vpath(svn.realpath, srem) c1, w, ftime_, fsize_, ip, at = self._find_from_vpath(svn.realpath, srem)
c2 = self.cur.get(dvn.realpath) c2 = self.cur.get(dvn.realpath)
has_dupes = False has_dupes = False
@ -4671,7 +4530,7 @@ class Up2k(object):
w, w,
w, w,
"", "",
un or "", "",
ip or "", ip or "",
at or 0, at or 0,
) )
@ -4689,7 +4548,7 @@ class Up2k(object):
dlink = bos.readlink(sabs) dlink = bos.readlink(sabs)
dlink = os.path.join(os.path.dirname(sabs), dlink) dlink = os.path.join(os.path.dirname(sabs), dlink)
dlink = bos.path.abspath(dlink) dlink = bos.path.abspath(dlink)
self._symlink(dlink, dabs, dvn.flags, lmod=ftime, is_mv=True) self._symlink(dlink, dabs, dvn.flags, lmod=ftime)
wunlink(self.log, sabs, svn.flags) wunlink(self.log, sabs, svn.flags)
else: else:
atomic_move(self.log, sabs, dabs, svn.flags) atomic_move(self.log, sabs, dabs, svn.flags)
@ -4769,16 +4628,15 @@ class Up2k(object):
Optional[str], Optional[str],
Optional[int], Optional[int],
Optional[int], Optional[int],
str, Optional[str],
Optional[int], Optional[int],
str,
]: ]:
cur = self.cur.get(ptop) cur = self.cur.get(ptop)
if not cur: if not cur:
return None, None, None, None, "", None, "" return None, None, None, None, None, None
rd, fn = vsplit(vrem) rd, fn = vsplit(vrem)
q = "select w, mt, sz, ip, at, un from up where rd=? and fn=? limit 1" q = "select w, mt, sz, ip, at from up where rd=? and fn=? limit 1"
try: try:
c = cur.execute(q, (rd, fn)) c = cur.execute(q, (rd, fn))
except: except:
@ -4787,9 +4645,9 @@ class Up2k(object):
hit = c.fetchone() hit = c.fetchone()
if hit: if hit:
wark, ftime, fsize, ip, at, un = hit wark, ftime, fsize, ip, at = hit
return cur, wark, ftime, fsize, ip, at, un return cur, wark, ftime, fsize, ip, at
return cur, None, None, None, "", None, "" return cur, None, None, None, None, None
def _forget_file( def _forget_file(
self, self,
@ -4909,7 +4767,7 @@ class Up2k(object):
flags = self.flags.get(ptop) or {} flags = self.flags.get(ptop) or {}
atomic_move(self.log, sabs, slabs, flags) atomic_move(self.log, sabs, slabs, flags)
bos.utime(slabs, (int(time.time()), int(mt)), False) bos.utime(slabs, (int(time.time()), int(mt)), False)
self._symlink(slabs, sabs, flags, False, is_mv=True) self._symlink(slabs, sabs, flags, False)
full[slabs] = (ptop, rem) full[slabs] = (ptop, rem)
sabs = slabs sabs = slabs
@ -4968,9 +4826,7 @@ class Up2k(object):
# (for example a volume with symlinked dupes but no --dedup); # (for example a volume with symlinked dupes but no --dedup);
# fsrc=sabs is then a source that currently resolves to copy # fsrc=sabs is then a source that currently resolves to copy
self._symlink( self._symlink(dabs, alink, flags, False, lmod=lmod or 0, fsrc=sabs)
dabs, alink, flags, False, lmod=lmod or 0, fsrc=sabs, is_mv=True
)
return len(full) + len(links) return len(full) + len(links)
@ -5078,15 +4934,13 @@ class Up2k(object):
if hr.get("reloc"): if hr.get("reloc"):
x = pathmod(self.vfs, ap_chk, vp_chk, hr["reloc"]) x = pathmod(self.vfs, ap_chk, vp_chk, hr["reloc"])
if x: if x:
ud1 = (vfs.vpath, job["prel"], job["name"]) zvfs = vfs
pdir, _, job["name"], (vfs, rem) = x pdir, _, job["name"], (vfs, rem) = x
job["vcfg"] = vf = vfs.flags job["vcfg"] = vf = vfs.flags
job["ptop"] = vfs.realpath job["ptop"] = vfs.realpath
job["vtop"] = vfs.vpath job["vtop"] = vfs.vpath
job["prel"] = rem job["prel"] = rem
job["name"] = sanitize_fn(job["name"], "") if zvfs.vpath != vfs.vpath:
ud2 = (vfs.vpath, job["prel"], job["name"])
if ud1 != ud2:
self.log("xbu reloc2:%d..." % (depth,), 6) self.log("xbu reloc2:%d..." % (depth,), 6)
return self._handle_json(job, depth + 1) return self._handle_json(job, depth + 1)
@ -5108,13 +4962,8 @@ class Up2k(object):
else: else:
dip = self.hub.iphash.s(job["addr"]) dip = self.hub.iphash.s(job["addr"])
f, job["tnam"] = ren_open( suffix = "-%.6f-%s" % (job["t0"], dip)
tnam, f, job["tnam"] = ren_open(tnam, "wb", fdir=pdir, suffix=suffix)
"wb",
fdir=pdir,
suffix="-%.6f-%s" % (job["t0"], dip),
vf=vf,
)
try: try:
abspath = djoin(pdir, job["tnam"]) abspath = djoin(pdir, job["tnam"])
sprs = job["sprs"] sprs = job["sprs"]
@ -5195,7 +5044,7 @@ class Up2k(object):
def _snap_reg(self, ptop: str, reg: dict[str, dict[str, Any]]) -> None: def _snap_reg(self, ptop: str, reg: dict[str, dict[str, Any]]) -> None:
now = time.time() now = time.time()
histpath = self.vfs.dbpaths.get(ptop) histpath = self.vfs.histtab.get(ptop)
if not histpath: if not histpath:
return return

View file

@ -31,17 +31,6 @@ from collections import Counter
from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network
from queue import Queue from queue import Queue
try:
from zlib_ng import gzip_ng as gzip
from zlib_ng import zlib_ng as zlib
sys.modules["gzip"] = gzip
# sys.modules["zlib"] = zlib
# `- somehow makes tarfile 3% slower with default malloc, and barely faster with mimalloc
except:
import gzip
import zlib
from .__init__ import ( from .__init__ import (
ANYWIN, ANYWIN,
EXE, EXE,
@ -105,7 +94,6 @@ def _ens(want: str) -> tuple[int, ...]:
# WSAENOTSOCK - no longer a socket # WSAENOTSOCK - no longer a socket
# EUNATCH - can't assign requested address (wifi down) # EUNATCH - can't assign requested address (wifi down)
E_SCK = _ens("ENOTCONN EUNATCH EBADF WSAENOTSOCK WSAECONNRESET") E_SCK = _ens("ENOTCONN EUNATCH EBADF WSAENOTSOCK WSAECONNRESET")
E_SCK_WR = _ens("EPIPE ESHUTDOWN EBADFD")
E_ADDR_NOT_AVAIL = _ens("EADDRNOTAVAIL WSAEADDRNOTAVAIL") E_ADDR_NOT_AVAIL = _ens("EADDRNOTAVAIL WSAEADDRNOTAVAIL")
E_ADDR_IN_USE = _ens("EADDRINUSE WSAEADDRINUSE") E_ADDR_IN_USE = _ens("EADDRINUSE WSAEADDRINUSE")
E_ACCESS = _ens("EACCES WSAEACCES") E_ACCESS = _ens("EACCES WSAEACCES")
@ -114,15 +102,9 @@ E_UNREACH = _ens("EHOSTUNREACH WSAEHOSTUNREACH ENETUNREACH WSAENETUNREACH")
IP6ALL = "0:0:0:0:0:0:0:0" IP6ALL = "0:0:0:0:0:0:0:0"
try:
import fcntl
HAVE_FCNTL = True
except:
HAVE_FCNTL = False
try: try:
import ctypes import ctypes
import fcntl
import termios import termios
except: except:
pass pass
@ -154,16 +136,6 @@ try:
except: except:
HAVE_PSUTIL = False HAVE_PSUTIL = False
try:
if os.environ.get("PRTY_NO_MAGIC") or (
ANYWIN and not os.environ.get("PRTY_FORCE_MAGIC")
):
raise Exception()
import magic
except:
pass
if True: # pylint: disable=using-constant-test if True: # pylint: disable=using-constant-test
import types import types
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
@ -186,6 +158,8 @@ if True: # pylint: disable=using-constant-test
if TYPE_CHECKING: if TYPE_CHECKING:
import magic
from .authsrv import VFS from .authsrv import VFS
from .broker_util import BrokerCli from .broker_util import BrokerCli
from .up2k import Up2k from .up2k import Up2k
@ -243,18 +217,7 @@ except:
BITNESS = struct.calcsize("P") * 8 BITNESS = struct.calcsize("P") * 8
RE_ANSI = re.compile("\033\\[[^mK]*[mK]") ansi_re = re.compile("\033\\[[^mK]*[mK]")
RE_HTML_SH = re.compile(r"[<>&$?`\"';]")
RE_CTYPE = re.compile(r"^content-type: *([^; ]+)", re.IGNORECASE)
RE_CDISP = re.compile(r"^content-disposition: *([^; ]+)", re.IGNORECASE)
RE_CDISP_FIELD = re.compile(
r'^content-disposition:(?: *|.*; *)name="([^"]+)"', re.IGNORECASE
)
RE_CDISP_FILE = re.compile(
r'^content-disposition:(?: *|.*; *)filename="(.*)"', re.IGNORECASE
)
RE_MEMTOTAL = re.compile("^MemTotal:.* kB")
RE_MEMAVAIL = re.compile("^MemAvailable:.* kB")
BOS_SEP = ("%s" % (os.sep,)).encode("ascii") BOS_SEP = ("%s" % (os.sep,)).encode("ascii")
@ -271,9 +234,6 @@ SYMTIME = PY36 and os.utime in os.supports_follow_symlinks
META_NOBOTS = '<meta name="robots" content="noindex, nofollow">\n' META_NOBOTS = '<meta name="robots" content="noindex, nofollow">\n'
# smart enough to understand javascript while also ignoring rel="nofollow"
BAD_BOTS = r"Barkrowler|bingbot|BLEXBot|Googlebot|GoogleOther|GPTBot|PetalBot|SeekportBot|SemrushBot|YandexBot"
FFMPEG_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z" FFMPEG_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z"
URL_PRJ = "https://github.com/9001/copyparty" URL_PRJ = "https://github.com/9001/copyparty"
@ -283,7 +243,6 @@ URL_BUG = URL_PRJ + "/issues/new?labels=bug&template=bug_report.md"
HTTPCODE = { HTTPCODE = {
200: "OK", 200: "OK",
201: "Created", 201: "Created",
202: "Accepted",
204: "No Content", 204: "No Content",
206: "Partial Content", 206: "Partial Content",
207: "Multi-Status", 207: "Multi-Status",
@ -371,7 +330,6 @@ DAV_ALLPROPS = set(DAV_ALLPROP_L)
MIMES = { MIMES = {
"opus": "audio/ogg; codecs=opus", "opus": "audio/ogg; codecs=opus",
"owa": "audio/webm; codecs=opus",
} }
@ -399,9 +357,6 @@ application swf=x-shockwave-flash m3u=vnd.apple.mpegurl db3=vnd.sqlite3 sqlite=v
text ass=plain ssa=plain text ass=plain ssa=plain
image jpg=jpeg xpm=x-xpixmap psd=vnd.adobe.photoshop jpf=jpx tif=tiff ico=x-icon djvu=vnd.djvu image jpg=jpeg xpm=x-xpixmap psd=vnd.adobe.photoshop jpf=jpx tif=tiff ico=x-icon djvu=vnd.djvu
image heic=heic-sequence heif=heif-sequence hdr=vnd.radiance svg=svg+xml image heic=heic-sequence heif=heif-sequence hdr=vnd.radiance svg=svg+xml
image arw=x-sony-arw cr2=x-canon-cr2 crw=x-canon-crw dcr=x-kodak-dcr dng=x-adobe-dng erf=x-epson-erf
image k25=x-kodak-k25 kdc=x-kodak-kdc mrw=x-minolta-mrw nef=x-nikon-nef orf=x-olympus-orf
image pef=x-pentax-pef raf=x-fuji-raf raw=x-panasonic-raw sr2=x-sony-sr2 srf=x-sony-srf x3f=x-sigma-x3f
audio caf=x-caf mp3=mpeg m4a=mp4 mid=midi mpc=musepack aif=aiff au=basic qcp=qcelp audio caf=x-caf mp3=mpeg m4a=mp4 mid=midi mpc=musepack aif=aiff au=basic qcp=qcelp
video mkv=x-matroska mov=quicktime avi=x-msvideo m4v=x-m4v ts=mp2t video mkv=x-matroska mov=quicktime avi=x-msvideo m4v=x-m4v ts=mp2t
video asf=x-ms-asf flv=x-flv 3gp=3gpp 3g2=3gpp2 rmvb=vnd.rn-realmedia-vbr video asf=x-ms-asf flv=x-flv 3gp=3gpp 3g2=3gpp2 rmvb=vnd.rn-realmedia-vbr
@ -491,22 +446,18 @@ UNHUMANIZE_UNITS = {
VF_CAREFUL = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1} VF_CAREFUL = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1}
FN_EMB = set([".prologue.html", ".epilogue.html", "readme.md", "preadme.md"])
def read_ram() -> tuple[float, float]: def read_ram() -> tuple[float, float]:
# NOTE: apparently no need to consider /sys/fs/cgroup/memory.max
# (cgroups2) since the limit is synced to /proc/meminfo
a = b = 0 a = b = 0
try: try:
with open("/proc/meminfo", "rb", 0x10000) as f: with open("/proc/meminfo", "rb", 0x10000) as f:
zsl = f.read(0x10000).decode("ascii", "replace").split("\n") zsl = f.read(0x10000).decode("ascii", "replace").split("\n")
p = RE_MEMTOTAL p = re.compile("^MemTotal:.* kB")
zs = next((x for x in zsl if p.match(x))) zs = next((x for x in zsl if p.match(x)))
a = int((int(zs.split()[1]) / 0x100000) * 100) / 100 a = int((int(zs.split()[1]) / 0x100000) * 100) / 100
p = RE_MEMAVAIL p = re.compile("^MemAvailable:.* kB")
zs = next((x for x in zsl if p.match(x))) zs = next((x for x in zsl if p.match(x)))
b = int((int(zs.split()[1]) / 0x100000) * 100) / 100 b = int((int(zs.split()[1]) / 0x100000) * 100) / 100
except: except:
@ -641,38 +592,6 @@ except Exception as ex:
print("using fallback base64 codec due to %r" % (ex,)) print("using fallback base64 codec due to %r" % (ex,))
class NotUTF8(Exception):
pass
def read_utf8(log: Optional["NamedLogger"], ap: Union[str, bytes], strict: bool) -> str:
with open(ap, "rb") as f:
buf = f.read()
try:
return buf.decode("utf-8", "strict")
except UnicodeDecodeError as ex:
eo = ex.start
eb = buf[eo : eo + 1]
if not strict:
t = "WARNING: The file [%s] is not using the UTF-8 character encoding; some characters in the file will be skipped/ignored. The first unreadable character was byte %r at offset %d. Please convert this file to UTF-8 by opening the file in your text-editor and saving it as UTF-8."
t = t % (ap, eb, eo)
if log:
log(t, 3)
else:
print(t)
return buf.decode("utf-8", "replace")
t = "ERROR: The file [%s] is not using the UTF-8 character encoding, and cannot be loaded. The first unreadable character was byte %r at offset %d. Please convert this file to UTF-8 by opening the file in your text-editor and saving it as UTF-8."
t = t % (ap, eb, eo)
if log:
log(t, 3)
else:
print(t)
raise NotUTF8(t)
class Daemon(threading.Thread): class Daemon(threading.Thread):
def __init__( def __init__(
self, self,
@ -1279,6 +1198,8 @@ class Magician(object):
self.magic: Optional["magic.Magic"] = None self.magic: Optional["magic.Magic"] = None
def ext(self, fpath: str) -> str: def ext(self, fpath: str) -> str:
import magic
try: try:
if self.bad_magic: if self.bad_magic:
raise Exception() raise Exception()
@ -1496,6 +1417,8 @@ def stackmon(fp: str, ival: float, suffix: str) -> None:
buf = st.encode("utf-8", "replace") buf = st.encode("utf-8", "replace")
if fp.endswith(".gz"): if fp.endswith(".gz"):
import gzip
# 2459b 2304b 2241b 2202b 2194b 2191b lv3..8 # 2459b 2304b 2241b 2202b 2194b 2191b lv3..8
# 0.06s 0.08s 0.11s 0.13s 0.16s 0.19s # 0.06s 0.08s 0.11s 0.13s 0.16s 0.19s
buf = gzip.compress(buf, compresslevel=6) buf = gzip.compress(buf, compresslevel=6)
@ -1575,12 +1498,6 @@ def vol_san(vols: list["VFS"], txt: bytes) -> bytes:
txt = txt.replace(bap.replace(b"\\", b"\\\\"), bvp) txt = txt.replace(bap.replace(b"\\", b"\\\\"), bvp)
txt = txt.replace(bhp.replace(b"\\", b"\\\\"), bvph) txt = txt.replace(bhp.replace(b"\\", b"\\\\"), bvph)
if vol.histpath != vol.dbpath:
bdp = vol.dbpath.encode("utf-8")
bdph = b"$db(/" + bvp + b")"
txt = txt.replace(bdp, bdph)
txt = txt.replace(bdp.replace(b"\\", b"\\\\"), bdph)
if txt != txt0: if txt != txt0:
txt += b"\r\nNOTE: filepaths sanitized; see serverlog for correct values" txt += b"\r\nNOTE: filepaths sanitized; see serverlog for correct values"
@ -1601,8 +1518,6 @@ def ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str
fun = kwargs.pop("fun", open) fun = kwargs.pop("fun", open)
fdir = kwargs.pop("fdir", None) fdir = kwargs.pop("fdir", None)
suffix = kwargs.pop("suffix", None) suffix = kwargs.pop("suffix", None)
vf = kwargs.pop("vf", None)
fperms = vf and "fperms" in vf
if fname == os.devnull: if fname == os.devnull:
return fun(fname, *args, **kwargs), fname return fun(fname, *args, **kwargs), fname
@ -1646,11 +1561,6 @@ def ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str
fp2 = os.path.join(fdir, fp2) fp2 = os.path.join(fdir, fp2)
with open(fsenc(fp2), "wb") as f2: with open(fsenc(fp2), "wb") as f2:
f2.write(orig_name.encode("utf-8")) f2.write(orig_name.encode("utf-8"))
if fperms:
set_fperms(f2, vf)
if fperms:
set_fperms(f, vf)
return f, fname return f, fname
@ -1712,10 +1622,14 @@ class MultipartParser(object):
self.args = args self.args = args
self.headers = http_headers self.headers = http_headers
self.re_ctype = RE_CTYPE self.re_ctype = re.compile(r"^content-type: *([^; ]+)", re.IGNORECASE)
self.re_cdisp = RE_CDISP self.re_cdisp = re.compile(r"^content-disposition: *([^; ]+)", re.IGNORECASE)
self.re_cdisp_field = RE_CDISP_FIELD self.re_cdisp_field = re.compile(
self.re_cdisp_file = RE_CDISP_FILE r'^content-disposition:(?: *|.*; *)name="([^"]+)"', re.IGNORECASE
)
self.re_cdisp_file = re.compile(
r'^content-disposition:(?: *|.*; *)filename="(.*)"', re.IGNORECASE
)
self.boundary = b"" self.boundary = b""
self.gen: Optional[ self.gen: Optional[
@ -1987,7 +1901,7 @@ def rand_name(fdir: str, fn: str, rnd: int) -> str:
return fn return fn
def _gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str: def gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:
if alg == 1: if alg == 1:
zs = "%s %s %s %s" % (salt, fspath, fsize, inode) zs = "%s %s %s %s" % (salt, fspath, fsize, inode)
else: else:
@ -1997,13 +1911,6 @@ def _gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> st
return ub64enc(hashlib.sha512(zb).digest()).decode("ascii") return ub64enc(hashlib.sha512(zb).digest()).decode("ascii")
def _gen_filekey_w(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:
return _gen_filekey(alg, salt, fspath.replace("/", "\\"), fsize, inode)
gen_filekey = _gen_filekey_w if ANYWIN else _gen_filekey
def gen_filekey_dbg( def gen_filekey_dbg(
alg: int, alg: int,
salt: str, salt: str,
@ -2050,25 +1957,15 @@ def formatdate(ts: Optional[float] = None) -> str:
return RFC2822 % (WKDAYS[wd], d, MONTHS[mo - 1], y, h, mi, s) return RFC2822 % (WKDAYS[wd], d, MONTHS[mo - 1], y, h, mi, s)
def gencookie( def gencookie(k: str, v: str, r: str, tls: bool, dur: int = 0, txt: str = "") -> str:
k: str, v: str, r: str, lax: bool, tls: bool, dur: int = 0, txt: str = ""
) -> str:
v = v.replace("%", "%25").replace(";", "%3B") v = v.replace("%", "%25").replace(";", "%3B")
if dur: if dur:
exp = formatdate(time.time() + dur) exp = formatdate(time.time() + dur)
else: else:
exp = "Fri, 15 Aug 1997 01:00:00 GMT" exp = "Fri, 15 Aug 1997 01:00:00 GMT"
t = "%s=%s; Path=/%s; Expires=%s%s%s; SameSite=%s" t = "%s=%s; Path=/%s; Expires=%s%s%s; SameSite=Lax"
return t % ( return t % (k, v, r, exp, "; Secure" if tls else "", txt)
k,
v,
r,
exp,
"; Secure" if tls else "",
txt,
"Lax" if lax else "Strict",
)
def humansize(sz: float, terse: bool = False) -> str: def humansize(sz: float, terse: bool = False) -> str:
@ -2257,16 +2154,6 @@ def find_prefix(ips: list[str], cidrs: list[str]) -> list[str]:
return ret return ret
def html_sh_esc(s: str) -> str:
s = re.sub(RE_HTML_SH, "_", s).replace(" ", "%20")
s = s.replace("\r", "_").replace("\n", "_")
return s
def json_hesc(s: str) -> str:
return s.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")
def html_escape(s: str, quot: bool = False, crlf: bool = False) -> str: def html_escape(s: str, quot: bool = False, crlf: bool = False) -> str:
"""html.escape but also newlines""" """html.escape but also newlines"""
s = s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;") s = s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
@ -2399,21 +2286,6 @@ def ujoin(rd: str, fn: str) -> str:
return rd or fn return rd or fn
def str_anchor(txt) -> tuple[int, str]:
if not txt:
return 0, ""
txt = txt.lower()
a = txt.startswith("^")
b = txt.endswith("$")
if not b:
if not a:
return 1, txt # ~
return 2, txt[1:] # ^
if not a:
return 3, txt[:-1] # $
return 4, txt[1:-1] # ^$
def log_reloc( def log_reloc(
log: "NamedLogger", log: "NamedLogger",
re: dict[str, str], re: dict[str, str],
@ -2462,11 +2334,11 @@ def pathmod(
# try to map abspath to vpath # try to map abspath to vpath
np = np.replace("/", os.sep) np = np.replace("/", os.sep)
for vn_ap, vns in vfs.all_aps: for vn_ap, vn in vfs.all_aps:
if not np.startswith(vn_ap): if not np.startswith(vn_ap):
continue continue
zs = np[len(vn_ap) :].replace(os.sep, "/") zs = np[len(vn_ap) :].replace(os.sep, "/")
nvp = vjoin(vns[0].vpath, zs) nvp = vjoin(vn.vpath, zs)
break break
if nvp == "\n": if nvp == "\n":
@ -2601,14 +2473,6 @@ def lsof(log: "NamedLogger", abspath: str) -> None:
log("lsof failed; " + min_ex(), 3) log("lsof failed; " + min_ex(), 3)
def set_fperms(f: Union[typing.BinaryIO, typing.IO[Any]], vf: dict[str, Any]) -> None:
fno = f.fileno()
if "chmod_f" in vf:
os.fchmod(fno, vf["chmod_f"])
if "chown" in vf:
os.fchown(fno, vf["uid"], vf["gid"])
def _fs_mvrm( def _fs_mvrm(
log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any] log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any]
) -> bool: ) -> bool:
@ -2657,11 +2521,6 @@ def _fs_mvrm(
now = time.time() now = time.time()
if ex.errno == errno.ENOENT: if ex.errno == errno.ENOENT:
return False return False
if not attempt and ex.errno == errno.EXDEV:
t = "using copy+delete (%s)\n %s\n %s"
log(t % (ex.strerror, src, dst))
osfun = shutil.move
continue
if now - t0 > maxtime or attempt == 90209: if now - t0 > maxtime or attempt == 90209:
raise raise
if not attempt: if not attempt:
@ -2686,18 +2545,15 @@ def atomic_move(log: "NamedLogger", src: str, dst: str, flags: dict[str, Any]) -
elif flags.get("mv_re_t"): elif flags.get("mv_re_t"):
_fs_mvrm(log, src, dst, True, flags) _fs_mvrm(log, src, dst, True, flags)
else: else:
try:
os.replace(bsrc, bdst) os.replace(bsrc, bdst)
except OSError as ex:
if ex.errno != errno.EXDEV:
raise def wrename(log: "NamedLogger", src: str, dst: str, flags: dict[str, Any]) -> bool:
t = "using copy+delete (%s);\n %s\n %s" if not flags.get("mv_re_t"):
log(t % (ex.strerror, src, dst)) os.rename(fsenc(src), fsenc(dst))
try: return True
os.unlink(bdst)
except: return _fs_mvrm(log, src, dst, False, flags)
pass
shutil.move(bsrc, bdst)
def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool: def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
@ -2708,7 +2564,7 @@ def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
return _fs_mvrm(log, abspath, "", False, flags) return _fs_mvrm(log, abspath, "", False, flags)
def get_df(abspath: str, prune: bool) -> tuple[int, int, str]: def get_df(abspath: str, prune: bool) -> tuple[Optional[int], Optional[int], str]:
try: try:
ap = fsenc(abspath) ap = fsenc(abspath)
while prune and not os.path.isdir(ap) and BOS_SEP in ap: while prune and not os.path.isdir(ap) and BOS_SEP in ap:
@ -2719,22 +2575,17 @@ def get_df(abspath: str, prune: bool) -> tuple[int, int, str]:
assert ctypes # type: ignore # !rm assert ctypes # type: ignore # !rm
abspath = fsdec(ap) abspath = fsdec(ap)
bfree = ctypes.c_ulonglong(0) bfree = ctypes.c_ulonglong(0)
btotal = ctypes.c_ulonglong(0)
bavail = ctypes.c_ulonglong(0)
ctypes.windll.kernel32.GetDiskFreeSpaceExW( # type: ignore ctypes.windll.kernel32.GetDiskFreeSpaceExW( # type: ignore
ctypes.c_wchar_p(abspath), ctypes.c_wchar_p(abspath), None, None, ctypes.pointer(bfree)
ctypes.pointer(bavail),
ctypes.pointer(btotal),
ctypes.pointer(bfree),
) )
return (bavail.value, btotal.value, "") return (bfree.value, None, "")
else: else:
sv = os.statvfs(ap) sv = os.statvfs(ap)
free = sv.f_frsize * sv.f_bavail free = sv.f_frsize * sv.f_bfree
total = sv.f_frsize * sv.f_blocks total = sv.f_frsize * sv.f_blocks
return (free, total, "") return (free, total, "")
except Exception as ex: except Exception as ex:
return (0, 0, repr(ex)) return (None, None, repr(ex))
if not ANYWIN and not MACOS: if not ANYWIN and not MACOS:
@ -2954,27 +2805,6 @@ def load_ipu(
return ip_u, nm return ip_u, nm
def load_ipr(
log: "RootLogger", iprs: list[str], defer_mutex: bool = False
) -> dict[str, NetMap]:
ret = {}
for ipr in iprs:
try:
zs, uname = ipr.split("=")
cidrs = zs.split(",")
except:
t = "\n invalid value %r for argument --ipr; must be CIDR[,CIDR[,...]]=UNAME (192.168.0.0/16=amelia)"
raise Exception(t % (ipr,))
try:
nm = NetMap(["::"], cidrs, True, True, defer_mutex)
except Exception as ex:
t = "failed to translate --ipr into netmap, probably due to invalid config: %r"
log("root", t % (ex,), 1)
raise
ret[uname] = nm
return ret
def yieldfile(fn: str, bufsz: int) -> Generator[bytes, None, None]: def yieldfile(fn: str, bufsz: int) -> Generator[bytes, None, None]:
readsz = min(bufsz, 128 * 1024) readsz = min(bufsz, 128 * 1024)
with open(fsenc(fn), "rb", bufsz) as f: with open(fsenc(fn), "rb", bufsz) as f:
@ -3006,17 +2836,6 @@ def justcopy(
return tlen, "checksum-disabled", "checksum-disabled" return tlen, "checksum-disabled", "checksum-disabled"
def eol_conv(
fin: Generator[bytes, None, None], conv: str
) -> Generator[bytes, None, None]:
crlf = conv.lower() == "crlf"
for buf in fin:
buf = buf.replace(b"\r", b"")
if crlf:
buf = buf.replace(b"\n", b"\r\n")
yield buf
def hashcopy( def hashcopy(
fin: Generator[bytes, None, None], fin: Generator[bytes, None, None],
fout: Union[typing.BinaryIO, typing.IO[Any]], fout: Union[typing.BinaryIO, typing.IO[Any]],
@ -3263,13 +3082,11 @@ def unescape_cookie(orig: str) -> str:
return "".join(ret) return "".join(ret)
def guess_mime( def guess_mime(url: str, fallback: str = "application/octet-stream") -> str:
url: str, path: str = "", fallback: str = "application/octet-stream"
) -> str:
try: try:
ext = url.rsplit(".", 1)[1].lower() ext = url.rsplit(".", 1)[1].lower()
except: except:
ext = "" return fallback
ret = MIMES.get(ext) ret = MIMES.get(ext)
@ -3277,16 +3094,6 @@ def guess_mime(
x = mimetypes.guess_type(url) x = mimetypes.guess_type(url)
ret = "application/{}".format(x[1]) if x[1] else x[0] ret = "application/{}".format(x[1]) if x[1] else x[0]
if not ret and path:
try:
with open(fsenc(path), "rb", 0) as f:
ret = magic.from_buffer(f.read(4096), mime=True)
if ret.startswith("text/htm"):
# avoid serving up HTML content unless there was actually a .html extension
ret = "text/plain"
except Exception as ex:
pass
if not ret: if not ret:
ret = fallback ret = fallback
@ -3602,7 +3409,7 @@ def runihook(
verbose: bool, verbose: bool,
cmd: str, cmd: str,
vol: "VFS", vol: "VFS",
ups: list[tuple[str, int, int, str, str, str, int, str]], ups: list[tuple[str, int, int, str, str, str, int]],
) -> bool: ) -> bool:
_, chk, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd) _, chk, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd)
bcmd = [sfsenc(x) for x in acmd] bcmd = [sfsenc(x) for x in acmd]
@ -3673,7 +3480,7 @@ def _zmq_hook(
msg: str, msg: str,
wait: float, wait: float,
sp_ka: dict[str, Any], sp_ka: dict[str, Any],
) -> tuple[int, str]: ) -> str:
import zmq import zmq
try: try:
@ -3684,7 +3491,6 @@ def _zmq_hook(
mtx = ZMQ["mtx"] mtx = ZMQ["mtx"]
ret = "" ret = ""
nret = 0
t0 = time.time() t0 = time.time()
if verbose and log: if verbose and log:
log("hook(%s) %r entering zmq-main-lock" % (src, cmd), 6) log("hook(%s) %r entering zmq-main-lock" % (src, cmd), 6)
@ -3711,21 +3517,18 @@ def _zmq_hook(
if mode == "pub": if mode == "pub":
sck = ctx.socket(zmq.PUB) sck = ctx.socket(zmq.PUB)
sck.setsockopt(zmq.LINGER, 0)
sck.bind(uri) sck.bind(uri)
time.sleep(1) # give clients time to connect; avoids losing first msg time.sleep(1) # give clients time to connect; avoids losing first msg
elif mode == "push": elif mode == "push":
sck = ctx.socket(zmq.PUSH) sck = ctx.socket(zmq.PUSH)
sck.bind(uri)
if timeout: if timeout:
sck.SNDTIMEO = int(timeout * 1000) sck.SNDTIMEO = int(timeout * 1000)
sck.setsockopt(zmq.LINGER, 0)
sck.bind(uri)
elif mode == "req": elif mode == "req":
sck = ctx.socket(zmq.REQ) sck = ctx.socket(zmq.REQ)
sck.connect(uri)
if timeout: if timeout:
sck.RCVTIMEO = int(timeout * 1000) sck.RCVTIMEO = int(timeout * 1000)
sck.setsockopt(zmq.LINGER, 0)
sck.connect(uri)
else: else:
raise Exception() raise Exception()
@ -3746,10 +3549,6 @@ def _zmq_hook(
log("hook(%s) %r awaiting ack from req" % (src, cmd), 6) log("hook(%s) %r awaiting ack from req" % (src, cmd), 6)
try: try:
ret = sck.recv().decode("utf-8", "replace") ret = sck.recv().decode("utf-8", "replace")
if ret.startswith("return "):
m = re.search("^return ([0-9]+)", ret[:12])
if m:
nret = int(m.group(1))
except: except:
sck.close() sck.close()
del ZMQ[cmd] # bad state; must reset del ZMQ[cmd] # bad state; must reset
@ -3763,7 +3562,7 @@ def _zmq_hook(
if wait > 0: if wait > 0:
time.sleep(wait) time.sleep(wait)
return nret, ret return ret
def _runhook( def _runhook(
@ -3810,9 +3609,12 @@ def _runhook(
arg = txt or ap arg = txt or ap
if acmd[0].startswith("zmq:"): if acmd[0].startswith("zmq:"):
zi, zs = _zmq_hook(log, verbose, src, acmd[0][4:].lower(), arg, wait, sp_ka) zs = "zmq-error"
if zi: try:
raise Exception("zmq says %d" % (zi,)) zs = _zmq_hook(log, verbose, src, acmd[0][4:].lower(), arg, wait, sp_ka)
except Exception as ex:
if log:
log("zeromq failed: %r" % (ex,))
return {"rc": 0, "stdout": zs} return {"rc": 0, "stdout": zs}
acmd += [arg] acmd += [arg]
@ -3894,8 +3696,6 @@ def runhook(
elif k in ret: elif k in ret:
if k == "rc" and v: if k == "rc" and v:
ret[k] = v ret[k] = v
elif k == "stdout" and v and not ret[k]:
ret[k] = v
else: else:
ret[k] = v ret[k] = v
except Exception as ex: except Exception as ex:
@ -4079,75 +3879,8 @@ def hidedir(dp) -> None:
pass pass
_flocks = {}
def _lock_file_noop(ap: str) -> bool:
return True
def _lock_file_ioctl(ap: str) -> bool:
assert fcntl # type: ignore # !rm
try: try:
fd = _flocks.pop(ap) if sys.version_info < (3, 10):
os.close(fd)
except:
pass
fd = os.open(ap, os.O_RDWR | os.O_CREAT, 438)
# NOTE: the fcntl.lockf identifier is (pid,node);
# the lock will be dropped if os.close(os.open(ap))
# is performed anywhere else in this thread
try:
fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
_flocks[ap] = fd
return True
except Exception as ex:
eno = getattr(ex, "errno", -1)
try:
os.close(fd)
except:
pass
if eno in (errno.EAGAIN, errno.EACCES):
return False
print("WARNING: unexpected errno %d from fcntl.lockf; %r" % (eno, ex))
return True
def _lock_file_windows(ap: str) -> bool:
try:
import msvcrt
try:
fd = _flocks.pop(ap)
os.close(fd)
except:
pass
fd = os.open(ap, os.O_RDWR | os.O_CREAT, 438)
msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
return True
except Exception as ex:
eno = getattr(ex, "errno", -1)
if eno == errno.EACCES:
return False
print("WARNING: unexpected errno %d from msvcrt.locking; %r" % (eno, ex))
return True
if os.environ.get("PRTY_NO_DB_LOCK"):
lock_file = _lock_file_noop
elif ANYWIN:
lock_file = _lock_file_windows
elif HAVE_FCNTL:
lock_file = _lock_file_ioctl
else:
lock_file = _lock_file_noop
try:
if sys.version_info < (3, 10) or os.environ.get("PRTY_NO_IMPRESO"):
# py3.8 doesn't have .files # py3.8 doesn't have .files
# py3.9 has broken .is_file # py3.9 has broken .is_file
raise ImportError() raise ImportError()
@ -4247,12 +3980,7 @@ def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]:
stream = codecs.getreader(enc)(stream) stream = codecs.getreader(enc)(stream)
return stream return stream
ap = os.path.join(E.mod, name) return open(os.path.join(E.mod, name), mode, encoding=enc)
if PY2:
return codecs.open(ap, "r", encoding=enc) # type: ignore
return open(ap, mode, encoding=enc)
class Pebkac(Exception): class Pebkac(Exception):
@ -4284,22 +4012,9 @@ class WrongPostKey(Pebkac):
self.datagen = datagen self.datagen = datagen
_: Any = ( _: Any = (mp, BytesIO, quote, unquote, SQLITE_VER, JINJA_VER, PYFTPD_VER, PARTFTPY_VER)
gzip,
mp,
zlib,
BytesIO,
quote,
unquote,
SQLITE_VER,
JINJA_VER,
PYFTPD_VER,
PARTFTPY_VER,
)
__all__ = [ __all__ = [
"gzip",
"mp", "mp",
"zlib",
"BytesIO", "BytesIO",
"quote", "quote",
"unquote", "unquote",

View file

@ -48,7 +48,6 @@ window.baguetteBox = (function () {
var onFSC = function (e) { var onFSC = function (e) {
isFullscreen = !!document.fullscreenElement; isFullscreen = !!document.fullscreenElement;
clmod(document.documentElement, 'bb_fsc', isFullscreen);
}; };
var overlayClickHandler = function (e) { var overlayClickHandler = function (e) {
@ -403,7 +402,7 @@ window.baguetteBox = (function () {
if (isFullscreen) if (isFullscreen)
document.exitFullscreen(); document.exitFullscreen();
else else
ebi('bbox-overlay').requestFullscreen(); (vid() || ebi('bbox-overlay')).requestFullscreen();
} }
catch (ex) { catch (ex) {
if (IPHONE) if (IPHONE)
@ -593,7 +592,9 @@ window.baguetteBox = (function () {
preloadPrev(currentIndex); preloadPrev(currentIndex);
}); });
show_buttons(0); clmod(ebi('bbox-btns'), 'off');
clmod(btnPrev, 'off');
clmod(btnNext, 'off');
updateOffset(); updateOffset();
overlay.style.display = 'block'; overlay.style.display = 'block';
@ -632,9 +633,6 @@ window.baguetteBox = (function () {
catch (ex) { } catch (ex) { }
isFullscreen = false; isFullscreen = false;
if (toast.tag == 'bb-ded')
toast.hide();
if (dtor || overlay.style.display === 'none') if (dtor || overlay.style.display === 'none')
return; return;
@ -670,7 +668,6 @@ window.baguetteBox = (function () {
if (v == keep) if (v == keep)
continue; continue;
unbind(v, 'error', lerr);
v.src = ''; v.src = '';
v.load(); v.load();
@ -698,28 +695,6 @@ window.baguetteBox = (function () {
} }
} }
function lerr() {
var t;
try {
t = this.getAttribute('src');
t = uricom_dec(t.split('/').pop().split('?')[0]);
}
catch (ex) { }
t = 'Failed to open ' + (t?t:'file');
console.log('bb-ded', t);
t += '\n\nEither the file is corrupt, or your browser does not understand the file format or codec';
try {
t += "\n\nerr#" + this.error.code + ", " + this.error.message;
}
catch (ex) { }
this.ded = esc(t);
if (this === vidimg())
toast.err(20, this.ded, 'bb-ded');
}
function loadImage(index, callback) { function loadImage(index, callback) {
var imageContainer = imagesElements[index]; var imageContainer = imagesElements[index];
var galleryItem = currentGallery[index]; var galleryItem = currentGallery[index];
@ -764,8 +739,7 @@ window.baguetteBox = (function () {
var image = mknod(is_vid ? 'video' : 'img'); var image = mknod(is_vid ? 'video' : 'img');
clmod(imageContainer, 'vid', is_vid); clmod(imageContainer, 'vid', is_vid);
bind(image, 'error', lerr); image.addEventListener(is_vid ? 'loadedmetadata' : 'load', function () {
bind(image, is_vid ? 'loadedmetadata' : 'load', function () {
// Remove loader element // Remove loader element
qsr('#baguette-img-' + index + ' .bbox-spinner'); qsr('#baguette-img-' + index + ' .bbox-spinner');
if (!options.async && callback) if (!options.async && callback)
@ -775,8 +749,6 @@ window.baguetteBox = (function () {
if (is_vid) { if (is_vid) {
image.volume = clamp(fcfg_get('vol', dvol / 100), 0, 1); image.volume = clamp(fcfg_get('vol', dvol / 100), 0, 1);
image.setAttribute('controls', 'controls'); image.setAttribute('controls', 'controls');
image.setAttribute('playsinline', '1');
// ios ignores poster
image.onended = vidEnd; image.onended = vidEnd;
image.onplay = function () { show_buttons(1); }; image.onplay = function () { show_buttons(1); };
image.onpause = function () { show_buttons(); }; image.onpause = function () { show_buttons(); };
@ -844,12 +816,6 @@ window.baguetteBox = (function () {
}); });
updateOffset(); updateOffset();
var im = vidimg();
if (im && im.ded)
toast.err(20, im.ded, 'bb-ded');
else if (toast.tag == 'bb-ded')
toast.hide();
if (options.animation == 'none') if (options.animation == 'none')
unvid(vid()); unvid(vid());
else else

File diff suppressed because it is too large Load diff

View file

@ -109,7 +109,7 @@
{%- for f in files %} {%- for f in files %}
<tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td> <tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td>
{%- if f.tags is defined %} {%- if f.tags is defined %}
{%- for k in taglist %}<td>{{ f.tags[k]|e }}</td>{%- endfor %} {%- for k in taglist %}<td>{{ f.tags[k] }}</td>{%- endfor %}
{%- endif %}<td>{{ f.ext }}</td><td>{{ f.dt }}</td></tr> {%- endif %}<td>{{ f.ext }}</td><td>{{ f.dt }}</td></tr>
{%- endfor %} {%- endfor %}
@ -124,7 +124,9 @@
</div> </div>
{%- if srv_info %}
<div id="srv_info"><span>{{ srv_info }}</span></div> <div id="srv_info"><span>{{ srv_info }}</span></div>
{%- endif %}
<div id="widget"></div> <div id="widget"></div>

File diff suppressed because it is too large Load diff

BIN
copyparty/web/dd/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

BIN
copyparty/web/dd/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

BIN
copyparty/web/dd/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 B

BIN
copyparty/web/dd/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

View file

View file

@ -1,55 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ s_doctitle }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.8">
<meta name="robots" content="noindex, nofollow">
<meta name="theme-color" content="#{{ tcolor }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/shares.css?_={{ ts }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
{{ html_head }}
</head>
<body>
<div id="wrap">
<a href="{{ r }}/?idp">refresh</a>
<a href="{{ r }}/?h">control-panel</a>
<table id="tab"><thead><tr>
<th>forget</th>
<th>user</th>
<th>groups</th>
</tr></thead><tbody>
{% for un, gn in rows %}
<tr>
<td><a href="{{ r }}/?idp=rm={{ un|e }}">forget</a></td>
<td>{{ un|e }}</td>
<td>{{ gn|e }}</td>
</tr>
{% endfor %}
</tbody></table>
{% if not rows %}
(there are no IdP users in the cache)
{% endif %}
</div>
<a href="#" id="repl">π</a>
<script>
var SR="{{ r }}",
lang="{{ lang }}",
dfavico="{{ favico }}";
var STG = window.localStorage;
document.documentElement.className = (STG && STG.cpp_thm) || "{{ this.args.theme }}";
</script>
<script src="{{ r }}/.cpr/util.js?_={{ ts }}"></script>
{%- if js %}
<script src="{{ js }}_={{ ts }}"></script>
{%- endif %}
</body>
</html>

View file

@ -23,7 +23,8 @@ var dbg = function () { };
// dodge browser issues // dodge browser issues
(function () { (function () {
if (UA.indexOf(') Gecko/') !== -1 && /Linux| Mac /.exec(UA)) { var ua = navigator.userAgent;
if (ua.indexOf(') Gecko/') !== -1 && /Linux| Mac /.exec(ua)) {
// necessary on ff-68.7 at least // necessary on ff-68.7 at least
var s = mknod('style'); var s = mknod('style');
s.innerHTML = '@page { margin: .5in .6in .8in .6in; }'; s.innerHTML = '@page { margin: .5in .6in .8in .6in; }';

View file

@ -255,7 +255,7 @@ function Modpoll() {
} }
console.log('modpoll...'); console.log('modpoll...');
var url = (location + '').split('?')[0] + '?_=' + Date.now(); var url = (document.location + '').split('?')[0] + '?_=' + Date.now();
var xhr = new XHR(); var xhr = new XHR();
xhr.open('GET', url, true); xhr.open('GET', url, true);
xhr.responseType = 'text'; xhr.responseType = 'text';
@ -346,7 +346,7 @@ function save(e) {
fd.append("lastmod", (force ? -1 : last_modified)); fd.append("lastmod", (force ? -1 : last_modified));
fd.append("body", txt); fd.append("body", txt);
var url = (location + '').split('?')[0]; var url = (document.location + '').split('?')[0];
var xhr = new XHR(); var xhr = new XHR();
xhr.open('POST', url, true); xhr.open('POST', url, true);
xhr.responseType = 'text'; xhr.responseType = 'text';
@ -404,7 +404,7 @@ function save_cb() {
function run_savechk(lastmod, txt, btn, ntry) { function run_savechk(lastmod, txt, btn, ntry) {
// download the saved doc from the server and compare // download the saved doc from the server and compare
var url = (location + '').split('?')[0] + '?_=' + Date.now(); var url = (document.location + '').split('?')[0] + '?_=' + Date.now();
var xhr = new XHR(); var xhr = new XHR();
xhr.open('GET', url, true); xhr.open('GET', url, true);
xhr.responseType = 'text'; xhr.responseType = 'text';
@ -450,7 +450,7 @@ function savechk_cb() {
// firefox bug: initial selection offset isn't cleared properly through js // firefox bug: initial selection offset isn't cleared properly through js
var ff_clearsel = (function () { var ff_clearsel = (function () {
if (UA.indexOf(') Gecko/') === -1) if (navigator.userAgent.indexOf(') Gecko/') === -1)
return function () { } return function () { }
return function () { return function () {
@ -1082,24 +1082,22 @@ action_stack = (function () {
if (from[p1] != to[p2]) if (from[p1] != to[p2])
break; break;
if (car > ++p1) if (car > ++p1) {
car = p1; car = p1;
}
var txt = from.substring(car, p1) var txt = from.substring(car, p1)
return { return {
car: car, car: car,
cdr: p2 + (car && 1), cdr: ++p2,
txt: txt, txt: txt,
cpos: cpos cpos: cpos
}; };
} }
var undiff = function (from, change) { var undiff = function (from, change) {
var t1 = from.substring(0, change.car),
t2 = from.substring(change.cdr);
return { return {
txt: t1 + change.txt + t2, txt: from.substring(0, change.car) + change.txt + from.substring(change.cdr),
cpos: change.cpos cpos: change.cpos
}; };
} }

View file

@ -6,7 +6,7 @@ var dom_doc = ebi('m');
var dom_md = ebi('mt'); var dom_md = ebi('mt');
(function () { (function () {
var n = location + ''; var n = document.location + '';
n = (n.slice(n.indexOf('//') + 2).split('?')[0] + '?v').split('/'); n = (n.slice(n.indexOf('//') + 2).split('?')[0] + '?v').split('/');
n[0] = 'top'; n[0] = 'top';
var loc = []; var loc = [];
@ -113,7 +113,7 @@ function save(mde) {
fd.append("lastmod", (force ? -1 : last_modified)); fd.append("lastmod", (force ? -1 : last_modified));
fd.append("body", txt); fd.append("body", txt);
var url = (location + '').split('?')[0]; var url = (document.location + '').split('?')[0];
var xhr = new XHR(); var xhr = new XHR();
xhr.open('POST', url, true); xhr.open('POST', url, true);
xhr.responseType = 'text'; xhr.responseType = 'text';
@ -166,7 +166,7 @@ function save_cb() {
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512); //alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
// download the saved doc from the server and compare // download the saved doc from the server and compare
var url = (location + '').split('?')[0] + '?_=' + Date.now(); var url = (document.location + '').split('?')[0] + '?_=' + Date.now();
var xhr = new XHR(); var xhr = new XHR();
xhr.open('GET', url, true); xhr.open('GET', url, true);
xhr.responseType = 'text'; xhr.responseType = 'text';

View file

@ -19,14 +19,20 @@
<a href="{{ r }}/?h">control-panel</a> <a href="{{ r }}/?h">control-panel</a>
&nbsp; Filter: <input type="text" id="filter" size="20" placeholder="documents/passwords" /> &nbsp; Filter: <input type="text" id="filter" size="20" placeholder="documents/passwords" />
&nbsp; <span id="hits"></span> &nbsp; <span id="hits"></span>
<div id="tw"></div> <table id="tab"><thead><tr>
<th>size</th>
<th>who</th>
<th>when</th>
<th>age</th>
<th>dir</th>
<th>file</th>
</tr></thead><tbody id="tb"></tbody></table>
</div> </div>
<a href="#" id="repl">π</a> <a href="#" id="repl">π</a>
<script> <script>
var SR="{{ r }}", var SR="{{ r }}",
lang="{{ lang }}", lang="{{ lang }}",
dutc={{ this.args.js_utc }},
dfavico="{{ favico }}"; dfavico="{{ favico }}";
var STG = window.localStorage; var STG = window.localStorage;

View file

@ -1,6 +1,5 @@
function render() { function render() {
var html = ['<table id="tab"><thead><tr><th>size</th><th>who</th><th>ip</th><th>when</th><th>age</th><th>dir</th><th>file</th></tr></thead><tbody>']; var ups = V.ups, now = V.now, html = [];
var ups = V.ups, now = V.now;
ebi('filter').value = V.filter; ebi('filter').value = V.filter;
ebi('hits').innerHTML = 'showing ' + ups.length + ' files'; ebi('hits').innerHTML = 'showing ' + ups.length + ' files';
@ -11,12 +10,11 @@ function render() {
fn = esc(uricom_dec(vsp[1])), fn = esc(uricom_dec(vsp[1])),
at = f.at, at = f.at,
td = now - f.at, td = now - f.at,
ts = !at ? '(?)' : unix2ui(at), ts = !at ? '(?)' : unix2iso(at),
sa = !at ? '(?)' : td > 60 ? shumantime(td) : (td + 's'), sa = !at ? '(?)' : td > 60 ? shumantime(td) : (td + 's'),
sz = ('' + f.sz).replace(/\B(?=(\d{3})+(?!\d))/g, " "); sz = ('' + f.sz).replace(/\B(?=(\d{3})+(?!\d))/g, " ");
html.push('<tr><td>' + sz + html.push('<tr><td>' + sz +
'</td><td>' + (f.un || '') +
'</td><td>' + f.ip + '</td><td>' + f.ip +
'</td><td>' + ts + '</td><td>' + ts +
'</td><td>' + sa + '</td><td>' + sa +
@ -28,8 +26,7 @@ function render() {
var t = V.filter ? ' matching the filter' : ''; var t = V.filter ? ' matching the filter' : '';
html = ['<tr><td colspan="6">there are no uploads' + t + '</td></tr>']; html = ['<tr><td colspan="6">there are no uploads' + t + '</td></tr>'];
} }
html.push('</tbody></table>'); ebi('tb').innerHTML = html.join('');
ebi('tw').innerHTML = html.join('\n');
} }
render(); render();
@ -49,7 +46,7 @@ function ask(e) {
V = JSON.parse(this.responseText) V = JSON.parse(this.responseText)
} }
catch (ex) { catch (ex) {
ebi('tw').innerHTML = 'failed to decode server response as json: <pre>' + esc(this.responseText) + '</pre>'; ebi('tb').innerHTML = '<tr><td colspan="6">failed to decode server response as json: <pre>' + esc(this.responseText) + '</pre></td></tr>';
return; return;
} }
render(); render();

View file

@ -66,7 +66,6 @@
var SR="{{ r }}", var SR="{{ r }}",
shr="{{ shr }}", shr="{{ shr }}",
lang="{{ lang }}", lang="{{ lang }}",
dutc={{ this.args.js_utc }},
dfavico="{{ favico }}"; dfavico="{{ favico }}";
var STG = window.localStorage; var STG = window.localStorage;

View file

@ -3,7 +3,7 @@ for (var a = 0; a < t.length; a++)
t[a].onclick = rm; t[a].onclick = rm;
function rm() { function rm() {
var u = SR + '/?eshare=rm&skey=' + uricom_enc(this.getAttribute('k')), var u = SR + shr + uricom_enc(this.getAttribute('k')) + '?eshare=rm',
xhr = new XHR(); xhr = new XHR();
xhr.open('POST', u, true); xhr.open('POST', u, true);
@ -13,7 +13,7 @@ function rm() {
function bump() { function bump() {
var k = this.closest('tr').getElementsByTagName('a')[2].getAttribute('k'), var k = this.closest('tr').getElementsByTagName('a')[2].getAttribute('k'),
u = SR + '/?skey=' + uricom_enc(k) + '&eshare=' + this.value, u = SR + shr + uricom_enc(k) + '?eshare=' + this.value,
xhr = new XHR(); xhr = new XHR();
xhr.open('POST', u, true); xhr.open('POST', u, true);
@ -25,7 +25,7 @@ function cb() {
if (this.status !== 200) if (this.status !== 200)
return modal.alert('<h6>server error</h6>' + esc(unpre(this.responseText))); return modal.alert('<h6>server error</h6>' + esc(unpre(this.responseText)));
location = '?shares'; document.location = '?shares';
} }
function qr(e) { function qr(e) {
@ -64,7 +64,7 @@ function showqr(href) {
for (var b = 7; b < 9; b++) { for (var b = 7; b < 9; b++) {
var v = buf[ibuf++]; var v = buf[ibuf++];
tr[a].cells[b].innerHTML = tr[a].cells[b].innerHTML =
v ? unix2ui(v).replace(' ', ',&nbsp;') : 'never'; v ? unix2iso(v).replace(' ', ',&nbsp;') : 'never';
} }
for (var a = 0; a < tr.length; a++) for (var a = 0; a < tr.length; a++)

View file

@ -24,7 +24,6 @@ h1 {
li { li {
margin: 1em 0; margin: 1em 0;
} }
#lo,
a { a {
color: #047; color: #047;
background: #fff; background: #fff;
@ -48,7 +47,6 @@ td a {
float: right; float: right;
margin: -.2em 0 0 .8em; margin: -.2em 0 0 .8em;
} }
#lo,
.logout, .logout,
a.r { a.r {
color: #c04; color: #c04;
@ -178,14 +176,12 @@ html.z {
html.z h1 { html.z h1 {
border-color: #777; border-color: #777;
} }
html.z #lo,
html.z a { html.z a {
color: #fff; color: #fff;
background: #057; background: #057;
border-color: #37a; border-color: #37a;
} }
html.z .logout, html.z .logout,
html.z #lo,
html.z a.r { html.z a.r {
background: #804; background: #804;
border-color: #c28; border-color: #c28;

View file

@ -15,14 +15,14 @@
<body> <body>
<div id="wrap"> <div id="wrap">
{%- if not in_shr %} {%- if not in_shr %}
<a id="a" href="{{ r }}/?h{{ re }}" class="af">refresh</a> <a id="a" href="{{ r }}/?h" class="af">refresh</a>
<a id="v" href="{{ r }}/?hc" class="af">connect</a> <a id="v" href="{{ r }}/?hc" class="af">connect</a>
{%- if this.uname == '*' %} {%- if this.uname == '*' %}
<p id="b">howdy stranger &nbsp; <small>(you're not logged in)</small></p> <p id="b">howdy stranger &nbsp; <small>(you're not logged in)</small></p>
{%- else %} {%- else %}
<a id="c" href="{{ r }}/?pw=x" class="logout">logout</a> <a id="c" href="{{ r }}/?pw=x" class="logout">logout</a>
<p><span id="m">welcome back,</span> <strong id="un">{{ this.uname|e }}</strong></p> <p><span id="m">welcome back,</span> <strong>{{ this.uname|e }}</strong></p>
{%- endif %} {%- endif %}
{%- endif %} {%- endif %}
@ -120,14 +120,9 @@
<div> <div>
<form id="lf" method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}"> <form id="lf" method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}">
<input type="hidden" id="la" name="act" value="login" /> <input type="hidden" id="la" name="act" value="login" />
{% if this.args.usernames %}
<input type="text" id="lu" name="uname" placeholder=" username" size="12" />
<input type="password" id="lp" name="cppwd" placeholder=" password" size="12" />
{% else %}
<input type="password" id="lp" name="cppwd" placeholder=" password" /> <input type="password" id="lp" name="cppwd" placeholder=" password" />
{% endif %}
<input type="hidden" name="uhash" id="uhash" value="x" /> <input type="hidden" name="uhash" id="uhash" value="x" />
<input type="submit" id="ls" value="login" /> <input type="submit" id="ls" value="Login" />
{% if chpw %} {% if chpw %}
<a id="x" href="#">change password</a> <a id="x" href="#">change password</a>
{% endif %} {% endif %}
@ -140,10 +135,6 @@
<h1 id="cc">other stuff:</h1> <h1 id="cc">other stuff:</h1>
<ul> <ul>
{%- if this.uname in this.args.idp_adm_set %}
<li><a id="ag" href="{{ r }}/?idp">view idp cache</a></li>
{% endif %}
{%- if this.uname != '*' and this.args.shr %} {%- if this.uname != '*' and this.args.shr %}
<li><a id="y" href="{{ r }}/?shares">edit shares</a></li> <li><a id="y" href="{{ r }}/?shares">edit shares</a></li>
{% endif %} {% endif %}
@ -168,13 +159,6 @@
<li><a id="af" href="{{ r }}/?ru">show recent uploads</a></li> <li><a id="af" href="{{ r }}/?ru">show recent uploads</a></li>
<li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li> <li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li>
{%- if this.uname != '*' %}
<li><form method="post" enctype="multipart/form-data">
<input type="hidden" name="act" value="logout" />
<input type="submit" id="lo" value="logout “{{ this.uname|e }}” everywhere" />
</form></li>
{% endif %}
</ul> </ul>
</div> </div>

View file

@ -1,5 +1,3 @@
// please add translations in alphabetic order, but keep "nor" and "eng" first
// (lines ending with //m are machine translations)
var Ls = { var Ls = {
"nor": { "nor": {
"a1": "oppdater", "a1": "oppdater",
@ -17,11 +15,6 @@ var Ls = {
"j1": "k304 bryter tilkoplingen for hver HTTP 304. Dette hjelper mot visse mellomtjenere som kan sette seg fast / plutselig slutter å laste sider, men det reduserer også ytelsen betydelig", "j1": "k304 bryter tilkoplingen for hver HTTP 304. Dette hjelper mot visse mellomtjenere som kan sette seg fast / plutselig slutter å laste sider, men det reduserer også ytelsen betydelig",
"k1": "nullstill innstillinger", "k1": "nullstill innstillinger",
"l1": "logg inn:", "l1": "logg inn:",
"ls3": "logg inn",
"lu4": "brukernavn",
"lp4": "passord",
"lo3": "logg ut “{0}” overalt",
"lo2": "avslutter økten på alle nettlesere",
"m1": "velkommen tilbake,", "m1": "velkommen tilbake,",
"n1": "404: filen finnes ikke &nbsp;┐( ´ -`)┌", "n1": "404: filen finnes ikke &nbsp;┐( ´ -`)┌",
"o1": 'eller kanskje du ikke har tilgang? prøv et passord eller <a href="' + SR + '/?h">gå hjem</a>', "o1": 'eller kanskje du ikke har tilgang? prøv et passord eller <a href="' + SR + '/?h">gå hjem</a>',
@ -46,18 +39,17 @@ var Ls = {
"ad1": "no304 stopper all bruk av cache. Hvis ikke k304 var nok, prøv denne. Vil mangedoble dataforbruk!", "ad1": "no304 stopper all bruk av cache. Hvis ikke k304 var nok, prøv denne. Vil mangedoble dataforbruk!",
"ae1": "utgående:", "ae1": "utgående:",
"af1": "vis nylig opplastede filer", "af1": "vis nylig opplastede filer",
"ag1": "vis kjente IdP-brukere",
}, },
"eng": { "eng": {
"d2": "shows the state of all active threads", "d2": "shows the state of all active threads",
"e2": "reload config files (accounts/volumes/volflags),$Nand rescan all e2ds volumes$N$Nnote: any changes to global settings$Nrequire a full restart to take effect", "e2": "reload config files (accounts/volumes/volflags),$Nand rescan all e2ds volumes$N$Nnote: any changes to global settings$Nrequire a full restart to take effect",
"lo2": "ends the session on all browsers",
"u2": "time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds", "u2": "time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds",
"v2": "use this server as a local HDD", "v2": "use this server as a local HDD",
"ta1": "fill in your new password first", "ta1": "fill in your new password first",
"ta2": "repeat to confirm new password:", "ta2": "repeat to confirm new password:",
"ta3": "found a typo; please try again", "ta3": "found a typo; please try again",
}, },
"chi": { "chi": {
"a1": "更新", "a1": "更新",
"b1": "你好 &nbsp; <small>(你尚未登录)</small>", "b1": "你好 &nbsp; <small>(你尚未登录)</small>",
@ -74,11 +66,6 @@ var Ls = {
"j1": "k304 会在每个 HTTP 304 时断开连接。这有助于避免某些代理服务器卡住或突然停止加载页面,但也会显著降低性能。", "j1": "k304 会在每个 HTTP 304 时断开连接。这有助于避免某些代理服务器卡住或突然停止加载页面,但也会显著降低性能。",
"k1": "重置设置", "k1": "重置设置",
"l1": "登录:", "l1": "登录:",
"ls3": "登录", //m
"lu4": "用户名", //m
"lp4": "密码", //m
"lo3": "在所有地方注销 {0}", //m
"lo2": "这将结束在所有浏览器中的会话", //m
"m1": "欢迎回来,", "m1": "欢迎回来,",
"n1": "404: 文件不存在 &nbsp;┐( ´ -`)┌", "n1": "404: 文件不存在 &nbsp;┐( ´ -`)┌",
"o1": '或者你可能没有权限?尝试输入密码或 <a href="' + SR + '/?h">回家</a>', "o1": '或者你可能没有权限?尝试输入密码或 <a href="' + SR + '/?h">回家</a>',
@ -103,662 +90,7 @@ var Ls = {
"ad1": "启用 no304 将禁用所有缓存;如果 k304 不够,可以尝试此选项。这将消耗大量的网络流量!", //m "ad1": "启用 no304 将禁用所有缓存;如果 k304 不够,可以尝试此选项。这将消耗大量的网络流量!", //m
"ae1": "正在下载:", //m "ae1": "正在下载:", //m
"af1": "显示最近上传的文件", //m "af1": "显示最近上传的文件", //m
"ag1": "查看已知 IdP 用户", //m }
},
"cze": {
"a1": "obnovit",
"b1": "ahoj cizinče &nbsp; <small>(nejsi přihlášen)</small>",
"c1": "odhlásit se",
"d1": "vypsat zásobníku",
"d2": "zobrazit stav všech aktivních vláken",
"e1": "znovu načíst konfiguraci",
"e2": "znovu načíst konfigurační soubory (accounts/volumes/volflags),$Na prohledat všechny e2ds úložiště$N$Npoznámka: všechny změny globálních nastavení$Nvyžadují úplné restartování, aby se projevily",
"f1": "můžeš procházet:",
"g1": "můžeš nahrávat do:",
"cc1": "další věci:",
"h1": "zakázat k304",
"i1": "povolit k304",
"j1": "povolení k304 odpojí vašeho klienta při každém HTTP 304, což může zabránit některým chybovým proxy serverům, aby se zasekly (náhle nenačítaly stránky), <em>ale</em> také to obecně zpomalí věci",
"k1": "resetovat nastavení klienta",
"l1": "přihlaste se pro více:",
"ls3": "přihlásit se", //m
"lu4": "uživatelské jméno", //m
"lp4": "heslo", //m
"lo3": "odhlásit “{0}” všude", //m
"lo2": "tímto ukončíte relaci ve všech prohlížečích", //m
"m1": "vítej zpět,",
"n1": "404 nenalezeno &nbsp;┐( ´ -`)┌",
"o1": 'nebo možná nemáš přístup -- zkus heslo nebo <a href="' + SR + '/?h">jdi domů</a>',
"p1": "403 zakázáno &nbsp;~┻━┻",
"q1": 'použij heslo nebo <a href="' + SR + '/?h">jdi domů</a>',
"r1": "jdi domů",
".s1": "znovu prohledat",
"t1": "akce",
"u2": "čas od posledního zápisu na server$N( upload / rename / ... )$N$N17d = 17 dní$N1h23 = 1 hodina 23 minut$N4m56 = 4 minuty 56 sekund",
"v1": "připojit",
"v2": "použít tento server jako místní HDD",
"w1": "přepnout na https",
"x1": "změnit heslo",
"y1": "upravit sdílení",
"z1": "odblokovat toto sdílení:",
"ta1": "nejprve vyplňte své nové heslo",
"ta2": "zopakujte pro potvrzení nového hesla:",
"ta3": "nalezen překlep; zkuste to prosím znovu",
"aa1": "příchozí soubory:",
"ab1": "deaktivovat no304",
"ac1": "povolit no304",
"ad1": "povolení no304 deaktivuje veškeré mezipaměti; zkuste to, pokud k304 nestačilo. To ovšem zapříčíní obrovské množství síťového provozu!",
"ae1": "aktivní stahování:",
"af1": "zobrazit nedávné nahrávání",
},
"deu": {
"a1": "Neu laden",
"b1": "Tach, wie geht's? &nbsp; <small>(Du bist nicht angemeldet)</small>",
"c1": "Abmelden",
"d1": "Zustand",
"d2": "Zeigt den Zustand aller aktiven Threads",
"e1": "Config neu laden",
"e2": "Konfigurationsdatei neu laden (Accounts/Volumes/VolFlags)$Nund scannt alle e2ds-Volumes$N$NBeachte: Jegliche Änderung an globalen Einstellungen$Nbenötigt einen Neustart zum Anwenden",
"f1": "Du kannst lesen:",
"g1": "Du kannst hochladen nach:",
"cc1": "Andere Dinge:",
"h1": "k304 deaktivieren",
"i1": "k304 aktivieren",
"j1": "k304 trennt die Clientverbindung bei jedem HTTP 304, was Bugs mit problematischen Proxies vorbeugen kann (z.B. nicht ladenden Seiten), macht Dinge aber generell langsamer",
"k1": "Client-Einstellungen zurücksetzen",
"l1": "Melde dich an für mehr:",
"ls3": "Anmelden", //m
"lu4": "Benutzername", //m
"lp4": "Passwort", //m
"lo3": "“{0}” überall abmelden", //m
"lo2": "Dies beendet die Sitzung in allen Browsern", //m
"m1": "Willkommen zurück,",
"n1": "404 Nicht gefunden &nbsp;┐( ´ -`)┌",
"o1": 'or maybe you don\'t have access -- try a password or <a href="' + SR + '/?h">go home</a>',
"p1": "403 Verboten &nbsp;~┻━┻",
"q1": 'Benutze ein Passwort oder <a href="' + SR + '/?h">gehe zur Homepage</a>',
"r1": "Gehe zur Homepage",
".s1": "Neu scannen",
"t1": "Aktion",
"u2": "time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds",
"v1": "Verbinden",
"v2": "Benutze diesen Server als lokale Festplatte",
"w1": "Zu HTTPS wechseln",
"x1": "Passwort ändern",
"y1": "Shares bearbeiten",
"z1": "Share entsperren:",
"ta1": "Trage zuerst dein Passwort ein",
"ta2": "Wiederhole dein Passwort zur Bestätigung:",
"ta3": "Da stimmt etwas nicht; probier's nochmal",
"aa1": "Eingehende Dateien:",
"ab1": "no304 deaktivieren",
"ac1": "no304 aktivieren",
"ad1": "Das Aktivieren von no304 deaktiviert jegliche Form von Caching; probier dies, wenn k304 nicht genug war. Dies verschwendet eine grosse Menge Netzwerk-Traffic!",
"ae1": "Aktive Downloads:",
"af1": "Zeige neue Uploads",
},
"fin": {
"a1": "päivitä",
"b1": "hei sie muukalainen &nbsp; <small>(et ole kirjautunut sisään)</small>",
"c1": "kirjaudu ulos",
"d1": "tulosta pinojälki",
"d2": "näytä kaikkien aktiivisten säikeiden tila",
"e1": "päivitä konffit",
"e2": "lataa konfiguraatiotiedostot uudelleen (käyttäjätilit/asemat/asemaflagit),$Nja skannaa kaikki e2ds asemat uudelleen$N$Nhuom: kaikki global-asetuksiin$Ntehdyt muutokset vaativat täyden$Nuudelleenkäynnistyksen",
"f1": "voit selata näitä:",
"g1": "voit ladata näihin:",
"cc1": "muuta:",
"h1": "poista k304 käytöstä",
"i1": "ota k304 käyttöön",
"j1": "k304 katkaisee yhteytesi jokaisella HTTP 304:llä, mikä voi estää joitain bugisia välityspalvelimia jumittumasta/lopettamasta sivujen lataamista, <em>mutta</em> se myös vähentää suorituskykyä",
"k1": "nollaa asetukset",
"l1": "kirjaudu sisään:",
"ls3": "kirjaudu sisään", //m
"lu4": "käyttäjätunnus", //m
"lp4": "salasana", //m
"lo3": "kirjaa “{0}” ulos kaikkialta", //m
"lo2": "tämä lopettaa istunnon kaikissa selaimissa", //m
"m1": "tervetuloa takaisin,",
"n1": "404: ei löytynyt mitään &nbsp;┐( ´ -`)┌",
"o1": 'tai ehkä sinulla ei vain ole käyttöoikeuksia? kokeile salasanaa tai <a href="' + SR + '/?h">mene kotiin</a>',
"p1": "403: pääsy kielletty &nbsp;~┻━┻",
"q1": 'kokeile salasanaa tai <a href="' + SR + '/?h">mene kotiin</a>',
"r1": "mene kotiin",
".s1": "uudelleenkartoita",
"t1": "toiminto",
"u2": "aika viimeisestä palvelimen kirjoituksesta$N( lataus / uudelleennimeäminen / tms. )$N$N17d = 17 päivää$N1h23 = 1 tunti 23 minuuttia$N4m56 = 4 minuuttia 56 sekuntia",
"v1": "yhdistä",
"v2": "käytä tätä palvelinta paikallisena kiintolevynä",
"w1": "vaihda https:ään",
"x1": "vaihda salasana",
"y1": "muokkaa jakoja",
"z1": "avaa tämä jako:",
"ta1": "täytä ensin uusi salasana",
"ta2": "toista vahvistaaksesi uuden salasanan:",
"ta3": "löytyi kirjoitusvirhe; yritä uudelleen",
"aa1": "saapuvat:",
"ab1": "poista no304 käytöstä",
"ac1": "ota no304 käyttöön",
"ad1": "no304:n lopettaa välimuistin käytön kokonaan; kokeile tätä jos k304 ei riittänyt. Tuhlaa valtavan määrän verkkoliikennettä!",
"ae1": "lähtevät:",
"af1": "näytä viimeaikaiset lataukset",
"ag1": "näytä tunnetut IdP-käyttäjät",
},
"fra": {
"a1": "rafraîchir",
"b1": "salut étranger &nbsp; <small>(vous n'êtes pas connecté.)</small>",
"c1": "déconnexion",
"d1": "vidange de la pile",
"d2": "affiche l'état de tous les threads actifs",
"e1": "recharger la configuration",
"e2": "recharger le fichier de configuration (comptes/volumes/indicateurs de volume),$Net rescanner tous les volumes e2ds$N$Nnote : n'importe quel changement aux paramètres globaux$Nnécessite un redémarrage complet pour prendre effet",
"f1": "vous pouvez naviguer :",
"g1": "vous pouvez télécharger sur :",
"cc1": "autres choses :",
"h1": "désactiver k304",
"i1": "activer k304",
"j1": "activer k304 va déconnecter votre client sur chaque HTTP 304, ce qui peut éviter à certains proxies défectueux de rester bloqués (les pages ne se chargent soudainement plus), <em>mais</em> cela ralentira également les choses en général",
"k1": "réinitialiser les paramètres du client",
"l1": "connectez-vous pour en savoir plus :",
"ls3": "se connecter", //m
"lu4": "nom d'utilisateur", //m
"lp4": "mot de passe", //m
"lo3": "déconnecter “{0}” partout", //m
"lo2": "cela mettra fin à la session sur tous les navigateurs", //m
"m1": "heureux de vous revoir,",
"n1": "404 introuvable &nbsp;┐( ´ -`)┌",
"o1": 'ou peut-être que vous n\'y avez pas accès -- essayer un mot de passe ou <a href="' + SR + '/?h">aller à la page d\'accueil</a>',
"p1": "403 interdit &nbsp;~┻━┻",
"q1": 'utiliser un mot de passe ou <a href="' + SR + '/?h">aller à la page d\'accueil</a>',
"r1": "aller à la page d\'accueil",
".s1": "rescanner",
"t1": "action",
"u2": "temps écoulé depuis la dernière écriture sur le serveur$N(téléchargement/renommage/...)$N$N17j = 17 jours$N1h23 = 1 heure 23 minutes$N4m56 = 4 minutes 56 secondes",
"v1": "connecter",
"v2": "utilisez ce serveur en tant que disque dur local",
"w1": "passer à https",
"x1": "changer mot de passe",
"y1": "modifier les partages",
"z1": "déverrouiller ce partage :",
"ta1": "entrez d'abord votre nouveau mot de passe",
"ta2": "répétez pour confirmer le nouveau mot de passe :",
"ta3": "une faute de frappe a été détectée ; veuillez réessayer.",
"aa1": "fichiers entrants :",
"ab1": "désactiver no304",
"ac1": "activer no304",
"ad1": "l'activation de no304 désactivera toute mise en cache ; essayez ceci si k304 n'était pas suffisant. Cela va générer un trafic réseau considérable !",
"ae1": "téléchargements actifs :",
"af1": "afficher les derniers téléchargements",
},
"grc": {
"a1": "ανανέωση",
"b1": "γεια σου ξένε! &nbsp; <small>(δεν είσαι συνδεδεμένος)</small>",
"c1": "αποσύνδεση",
"d1": "λίστα διεργασιών",
"d2": "εμφανίζει την κατάσταση όλων των ενεργών διεργασιών",
"e1": "επαναφόρτωση του cfg",
"e2": "φορτώνει ξανά τα αρχεία ρυθμίσεων (λογαριασμοί/τόμοι/volflags),$Nκαι κάνει επανεξέταση όλων των τόμων e2ds$N$Nσημείωση: οποιαδήποτε αλλαγή στις καθολικές ρυθμίσεις$Nαπαιτεί πλήρη επανεκκίνηση για να εφαρμοστεί",
"f1": "μπορείς να περιηγηθείς:",
"g1": "μπορείς να εκτελέσεις μεταφόρτωση σε:",
"cc1": "άλλα πράγματα:",
"h1": "απενεργοποίση k304",
"i1": "ενεργοποίηση k304",
"j1": "η ενεργοποίηση του k304 θα αποσυνδέσει το πρόγραμμα πελάτη σου σε κάθε HTTP 304, κάτι που μπορεί να αποτρέψει κάποια προβληματικά proxies από το να κολλάνε (να μην φορτώνουν ξαφνικά σελίδες), <em>αλλά</em> θα κάνει τα πράγματα, γενικά πιο αργά",
"k1": "επαναφορά ρυθμίσεων στο πρόγραμμα πελάτη",
"l1": "συνδέσου για περισσότερα:",
"ls3": "σύνδεση", //m
"lu4": "όνομα χρήστη", //m
"lp4": "κωδικός πρόσβασης", //m
"lo3": "αποσύνδεση του “{0}” από παντού", //m
"lo2": "αυτό θα τερματίσει τη συνεδρία σε όλους τους περιηγητές", //m
"m1": "καλώς ήρθες,",
"n1": "404 δεν βρέθηκε &nbsp;┐( ´ -`)┌",
"o1": '´η μήπως δεν έχεις πρόσβαση -- δοκίμασε έναν κωδικό <a href="' + SR + '/?h">πήγαινε στην αρχική</a>',
"p1": "403 απαγορευμένο &nbsp;~┻━┻",
"q1": 'δοκίμασε έναν κωδικό <a href="' + SR + '/?h">πήγαινε στην αρχική</a>',
"r1": "πίσω στην αρχική",
".s1": "επανάληψη σάρωσης",
"t1": "ενέργεια",
"u2": "χρόνος από την τελευταία εγγραφή του διακομιστή$N( μεταφόρτωση / μετονομασία / ... )$N$N17d = 17 days$N1ω23 = 1 ώρα 23 λεπτά$N4λ56 = 4 λεπτά 56 δευτερόλεπτα",
"v1": "σύνδεση",
"v2": "χρησιμοποίησε αυτόν το διακομιστή σαν τοπικό δίσκο",
"w1": "εναλλαγή σε https",
"x1": "αλλαγή κωδικού",
"y1": "επεξεργασία κοινόχρηστων φακέλων",
"z1": "ξεκλείδωμα αυτού του κοινόχρηστου φακέλου:",
"ta1": "συμπλήρωσε πρώτα το νέο σου κωδικό",
"ta2": "επανέλαβε για να επιβεβαιώσεις το νέο κωδικό:",
"ta3": "βρέθηκε τυπογραφικό λάθος· δοκίμασε ξανά",
"aa1": "εισερχόμενα αρχεία:",
"ab1": "απενεργοποίηση no304",
"ac1": "ενεργοποίηση no304",
"ad1": "η ενεργοποίηση του no304 θα απενεργοποιήσει όλη την προσωρινή αποθήκευση· δοκίμασέ το αν το k304 δεν ήταν αρκετό. Προσοχή, θα σπαταλήσει τεράστιο όγκο δικτυακής κίνησης!",
"ae1": "ενεργές μεταφορτώσεις:",
"af1": "προβολή πρόσφατων μεταφορτώσεων",
},
"ita": {
"a1": "aggiorna",
"b1": "ciao &nbsp; <small>(non sei connesso)</small>",
"c1": "disconnetti",
"d1": "stato",
"d2": "mostra lo stato di tutti i thread attivi",
"e1": "ricarica configurazione",
"e2": "ricarica i file di configurazione (account/volumi/flag dei volumi),\n e riesegue la scansione di tutti i volumi e2ds.\n\nNota: qualsiasi modifica alle impostazioni globali richiede un riavvio completo per avere effetto",
"f1": "puoi visualizzare:",
"g1": "puoi caricare su:",
"cc1": "altro:",
"h1": "disattiva k304",
"i1": "attiva k304",
"j1": "k304 interrompe la connessione per ogni HTTP 304. Questo aiuta contro alcuni proxy difettosi che possono bloccarsi o smettere improvvisamente di caricare pagine, ma riduce notevolmente le prestazioni",
"k1": "resetta impostazioni",
"l1": "accedi:",
"ls3": "accedi", //m
"lu4": "nome utente", //m
"lp4": "password", //m
"lo3": "disconnetti “{0}” ovunque", //m
"lo2": "questo terminerà la sessione su tutti i browser", //m
"m1": "bentornato,",
"n1": "404: file non trovato &nbsp;┐( ´ -`)┌",
"o1": "oppure forse non hai accesso? prova una password o <a href=\"SR/?h\">torna alla home</a>",
"p1": "403: accesso negato &nbsp;~┻━┻",
"q1": "prova una password o <a href=\"SR/?h\">torna alla home</a>",
"r1": "torna alla home",
".s1": "mappa",
"t1": "azione",
"u2": "tempo dall'ultima scrittura sul server\n (caricamento / rinomina / ...)\n\n17d = 17 giorni\n1h23 = 1 ora 23 minuti\n4m56 = 4 minuti 56 secondi",
"v1": "connetti",
"v2": "usa questo server come un disco locale",
"w1": "passa a https",
"x1": "cambia password",
"y1": "le tue condivisioni",
"z1": "sblocca area:",
"ta1": "devi prima inserire una nuova password",
"ta2": "ripeti per confermare la nuova password:",
"ta3": "errore di digitazione; riprova",
"aa1": "in arrivo:",
"ab1": "disattiva no304",
"ac1": "attiva no304",
"ad1": "no304 disabilita completamente la cache. Se k304 non è sufficiente, prova questa opzione. Aumenterà notevolmente il consumo di dati!",
"ae1": "in uscita:",
"af1": "mostra i file caricati di recente",
"ag1": "mostra utenti IdP conosciuti"
},
"kor": {
"a1": "새로고침",
"b1": "어이 친구! 처음 보는 얼굴인데? &nbsp; <small>(로그인되어 있지 않습니다)</small>",
"c1": "로그아웃",
"d1": "스택 덤프하기",
"d2": "모든 활성 스레드의 상태를 표시합니다",
"e1": "설정 다시 불러오기",
"e2": "설정 파일(계정/볼륨/볼륨 플래그)을 다시 불러오고,$N모든 e2ds 볼륨을 다시 스캔합니다$N$N참고: 전역 설정에 대한 변경 사항은$N적용하려면 전체 재시작이 필요합니다",
"f1": "탐색 가능한 곳:",
"g1": "업로드 가능한 곳:",
"cc1": "기타 항목:",
"h1": "k304 비활성화",
"i1": "k304 활성화",
"j1": "k304를 활성화하면 모든 HTTP 304 응답 시 클라이언트 연결이 끊어집니다. 이는 일부 프록시가 멈추는 현상(갑자기 페이지가 로드되지 않음)을 방지할 수 있지만, <em>대신 전반적인 속도는 느려집니다.</em>",
"k1": "클라이언트 설정 초기화",
"l1": "로그인하기:",
"ls3": "로그인", //m
"lu4": "사용자 이름", //m
"lp4": "비밀번호", //m
"lo3": "{0}을(를) 모든 곳에서 로그아웃", //m
"lo2": "이 작업은 모든 브라우저에서 세션을 종료합니다", //m
"m1": "또 오셨네요,",
"n1": "404 찾을 수 없음 &nbsp;┐( ´ -`)┌",
"o1": "또는 접근 권한이 없을 수 있습니다. 비밀번호를 입력하거나 <a href=\"' + SR + '/?h\">홈으로 이동</a>하세요",
"p1": "403 접근 금지 &nbsp;~┻━┻",
"q1": "비밀번호를 입력하거나 <a href=\"' + SR + '/?h\">홈으로 이동</a>하세요",
"r1": "홈으로 이동",
".s1": "다시 스캔",
"t1": "작업",
"u2": "서버에 마지막으로 쓰기 작업을 한 후 경과된 시간$N(업로드 / 이름 변경 / 등등...)$N$N17d = 17일$N1h23 = 1시간 23분$N4m56 = 4분 56초",
"v1": "연결",
"v2": "이 서버를 로컬 하드디스크처럼 사용하기",
"w1": "HTTPS로 전환",
"x1": "비밀번호 변경",
"y1": "공유 설정",
"z1": "이 공유 잠금해제:",
"ta1": "새 비밀번호를 먼저 입력하세요",
"ta2": "새 비밀번호 확인을 위해 다시 입력하세요:",
"ta3": "오타가 있습니다. 다시 시도해주세요",
"aa1": "수신 중인 파일:",
"ab1": "no304 비활성화",
"ac1": "no304 활성화",
"ad1": "no304를 활성화하면 모든 캐싱이 비활성화됩니다. k304로 충분하지 않은 경우 시도해보세요. 네트워크 트래픽이 대량으로 낭비됩니다!",
"ae1": "활성 다운로드:",
"af1": "최근 업로드 보기",
"ag1": "IdP 캐시 보기"
},
"nld": {
"a1": "Update",
"b1": "Hallo, hoe gaat het met jou? &nbsp; <small>(Je bent niet ingelogd)</small>",
"c1": "Uitloggen",
"d1": "Voorwaarde",
"d2": "Toont de status van alle actieve threads",
"e1": "Configuratie opnieuw laden.",
"e2": "Leest configuratiebestanden opnieuw in$N(accounts, volumes, volumeschakelaars)$Nen brengt alle e2ds-volumes in kaart$N$Nopmerking: veranderingen in globale parameters$Nvereist een volledige herstart van de server",
"f1": "Je kan het volgende lezen:",
"g1": "Je kan naar het volgende uploaden:",
"cc1": "Schakelaars en dergelijke:",
"h1": "k304 uitschakelen",
"i1": "k304 inschakelen",
"j1": "k304 verbreekt de verbinding voor elke HTTP 304. Dit helpt tegen bepaalde proxy servers die kunnen vastlopen/plotseling stoppen met het laden van pagina's, maar het vermindert ook de prestaties aanzienlijk",
"k1": "Instellingen resetten",
"l1": "Inloggen:",
"ls3": "inloggen", //m
"lu4": "gebruikersnaam", //m
"lp4": "wachtwoord", //m
"lo3": "“{0}” overal afmelden", //m
"lo2": "dit zal de sessie in alle browsers beëindigen", //m
"m1": "Welkom terug,",
"n1": "404: bestand bestaat niet &nbsp;┐( ´ -`)┌",
"o1": 'of misschien heb je geen toegang? probeer een wachtwoord of <a href="' + SR + '/?h">ga naar startscherm</a>',
"p1": "403: toegang geweigerd &nbsp;~┻━┻",
"q1": 'Probeer een wachtwoord of <a href="' + SR + '/?h">ga naar startscherm</a>',
"r1": "Ga naar startscherm",
".s1": "Kaart",
"t1": "Actie",
"u2": "Tijd sinds iemand voor het laatst naar de server schreef$N( upload / naamswijziging / ... )$N$N17d = 17 dagen$N1h23 = 1 uur 23 minuten$N4m56 = 4 minuten 56 secondes",
"v1": "Verbinden",
"v2": "Gebruik deze server als een lokale harde schijf",
"w1": "Overschakelen naar https",
"x1": "Wachtwoord wijzigen",
"y1": "Jou gedeelde items",
"z1": "Ontgrendel gebied:",
"ta1": "Je moet eerst een nieuw wachtwoord invoeren",
"ta2": "Herhaal om nieuw wachtwoord te bevestigen:",
"ta3": "Typefout gevonden; probeer het opnieuw",
"aa1": "Inkomend:",
"ab1": "Schakel nr. 304 uit",
"ac1": "Schakel nr. 304 in",
"ad1": "Nr. 304 stopt al het cachegebruik. Als k304 niet voldoende was, probeer dan deze. Vermenigvuldigt het dataverbruik.!",
"ae1": "Uitgaand:",
"af1": "Recent geüploade bestanden weergeven",
"ag1": "Bekende IdP-gebruikers weergeven",
},
"nno": {
"a1": "oppdatér",
"b1": "heisann &nbsp; <small>(du er ikkje logga inn)</small>",
"c1": "logg ut",
"d1": "tilstand",
"d2": "vis tilstanden åt alle trådar",
"e1": "last innst.",
"e2": "les inn konfigurasjonsfiler på nytt$N(kontoer, volum, volumbrytarar)$Nog kartlegg alle e2ds-volum$N$Nmerk: endringer i globale parametrar$Nkrev ein full restart for å gjelde",
"f1": "du kan sjå på:",
"g1": "du kan laste opp åt:",
"cc1": "brytarar og slikt:",
"h1": "skru av k304",
"i1": "skru på k304",
"j1": "k304 bryt tilkoplinga for kvar HTTP 304. Dette hjelp mot visse mellomtjenarar som kan sette seg fast / plutselig sluttar å laste sider, men det sett óg ytinga ned betydelig",
"k1": "nullstill innstillinger",
"l1": "logg inn:",
"ls3": "logg inn",
"lu4": "brukarnamn",
"lp4": "passord",
"lo3": "logg ut “{0}” overalt",
"lo2": "avslutt økta på alle nettlesarar",
"m1": "velkomen attende,",
"n1": "404: filen finnast ikkje &nbsp;┐( ´ -`)┌",
"o1": 'eller kanskje du ikkje har høve? prøv eit passord eller <a href="' + SR + '/?h">gå heim</a>',
"p1": "403: tilgang nektet &nbsp;~┻━┻",
"q1": 'prøv eit passord eller <a href="' + SR + '/?h">gå heim</a>',
"r1": "gå heim",
".s1": "kartlegg",
"t1": "handling",
"u2": "tid sidan nokon sist skreiv åt serveren$N( opplastning / namnendring / ... )$N$N17d = 17 dagar$N1h23 = 1 time 23 minutt$N4m56 = 4 minutt 56 sekund",
"v1": "kople åt",
"v2": "bruk denne serveren som ein lokal harddisk",
"w1": "bytt åt https",
"x1": "bytt passord",
"y1": "dine delinger",
"z1": "lås opp område:",
"ta1": "du må skrive eit nytt passord først",
"ta2": "gjenta for å stadfeste nytt passord:",
"ta3": "fant ein skrivefeil; vennligst prøv igjen",
"aa1": "innkommande:",
"ab1": "skru av no304",
"ac1": "skru på no304",
"ad1": "no304 stoppar all bruk av cache. Hvis ikkje k304 var nok, prøv denne. Vil mangedoble dataforbruk!",
"ae1": "utgående:",
"af1": "vis nylig opplasta filer",
"ag1": "vis kjente IdP-brukarar",
},
"pol": {
"a1": "odśwież",
"b1": "witaj, nieznajomy &nbsp; <small>(nie jesteś zalogowany)</small>",
"c1": "wyloguj się",
"d1": "zrzut stosu",
"d2": "pokazuje status wszystkich aktywnych wątków",
"e1": "przeładuj konfigurację",
"e2": "przeładuj pliki konfiguracyjne (konta/wolumeny/flagi wolumenów),$Ni przeskanuje wszystkie wolumeny e2ds$N$Nnotka: zmiany konfiguracji globalnej$Nwymagają pełnego uruchomienia ponownie serwera, aby zaczęły obowiązywać",
"f1": "możesz przeglądać:",
"g1": "możesz przesyłać do:",
"cc1": "inne:",
"h1": "wyłącz k304",
"i1": "włącz k304",
"j1": "włączenie k304 będzie odłączało klienta przy każdorazowym otrzymaniu kodu HTTP 304, co może zapobiec wieszaniu się wadliwych proxy, <em>ale</em> spowolni ogólne działanie",
"k1": "zresetuj ustawienia klienta",
"l1": "zaloguj się po więcej:",
"ls3": "zaloguj się", //m
"lu4": "nazwa użytkownika", //m
"lp4": "hasło", //m
"lo3": "wyloguj “{0}” wszędzie", //m
"lo2": "spowoduje to zakończenie sesji we wszystkich przeglądarkach", //m
"m1": "Witaj,",
"n1": "404 nie znaleziono &nbsp;┐( ´ -`)┌",
"o1": 'lub możesz nie mieć dostępu -- spróbuj wprowadzić hasło lub <a href="' + SR + '/?h">przejdź do strony głównej</a>',
"p1": "403 odmowa dostępu &nbsp;~┻━┻",
"q1": 'użyj hasła lub <a href="' + SR + '/?h">przejdź do strony głównej</a>',
"r1": "idź do strony głównej",
".s1": "przeskanuj ponownie",
"t1": "akcje",
"u2": "czas od ostatniej interakcji z serwerem$N( przesyłania / zmiany nazwy / ... )$N$N17d = 17 dni$N1h23 = 1 godzina 23 minuty$N4m56 = 4 minuty 56 sekund",
"v1": "połącz",
"v2": "używaj tego serwera jako dysku lokalnego",
"w1": "przejdź na HTTPS",
"x1": "zmień hasło",
"y1": "edytuj udostępnione",
"z1": "odblokuj udostępnienie:",
"ta1": "najpierw wprowadź nowe hasło",
"ta2": "powtórz hasło dla potwierdzenia:",
"ta3": "znaleziono literówkę, spróbuj ponownie",
"aa1": "pliki przychodzące:",
"ab1": "wyłącz no304",
"ac1": "włącz no304",
"ad1": "włączenie no304 wyłączy przechowywanie jakiejkolwiek pamięci podręcznej. Zmarnuje to olbrzymią ilość ruchu sieciowego!",
"ae1": "trwające pobierania:",
"af1": "pokaż ostatnio przesłane pliki",
"ag1": "pokaż znanych użytkowników IdP",
},
"spa": {
"a1": "actualizar",
"b1": "hola &nbsp; <small>(no has iniciado sesión)</small>",
"c1": "cerrar sesión",
"d1": "volcar estado de la pila",
"d2": "muestra el estado de todos los hilos activos",
"e1": "recargar configuración",
"e2": "recargar archivos de configuración (cuentas/volúmenes/indicadores de vol.),$Ny reescanear todos los volúmenes e2ds$N$Nnota: cualquier cambio en la configuración global$Nrequiere un reinicio completo para surtir efecto",
"f1": "puedes explorar:",
"g1": "puedes subir a:",
"cc1": "otras cosas:",
"h1": "desactivar k304",
"i1": "activar k304",
"j1": "activar k304 desconectará tu cliente en cada HTTP 304, lo que puede evitar que algunos proxies con errores se atasquen (dejando de cargar páginas de repente), <em>pero</em> también ralentizará las cosas en general",
"k1": "restablecer config. de cliente",
"l1": "inicia sesión para más:",
"ls3": "iniciar sesión", //m
"lu4": "nombre de usuario", //m
"lp4": "contraseña", //m
"lo3": "cerrar sesión de “{0}” en todas partes", //m
"lo2": "esto finalizará la sesión en todos los navegadores", //m
"m1": "bienvenido de nuevo,",
"n1": "404 no encontrado &nbsp;┐( ´ -`)┌",
"o1": '¿o quizás no tienes acceso? -- prueba con una contraseña o <a href=\"' + SR + '/?h\">vuelve al inicio</a>',
"p1": "403 prohibido &nbsp;~┻━┻",
"q1": 'usa una contraseña o <a href=\"' + SR + '/?h\">vuelve al inicio</a>',
"r1": "ir al inicio",
".s1": "reescanear",
"t1": "acción",
"u2": "tiempo desde la última escritura en el servidor$N( subida / renombrar / ... )$N$N17d = 17 días$N1h23 = 1 hora 23 minutos$N4m56 = 4 minutos 56 segundos",
"v1": "conectar",
"v2": "usar este servidor como un disco duro local",
"w1": "cambiar a https",
"x1": "cambiar contraseña",
"y1": "editar recursos compartidos",
"z1": "desbloquear este recurso compartido:",
"ta1": "primero escribe tu nueva contraseña",
"ta2": "repite para confirmar la nueva contraseña:",
"ta3": "hay un error; por favor, inténtalo de nuevo",
"aa1": "archivos entrantes:",
"ab1": "desactivar no304",
"ac1": "activar no304",
"ad1": "activar no304 desactivará todo el almacenamiento en caché; prueba esto si k304 no fue suficiente. ¡Esto desperdiciará una gran cantidad de tráfico de red!",
"ae1": "descargas activas:",
"af1": "mostrar subidas recientes",
"ag1": "mostrar usuarios IdP conocidos"
},
"swe": {
"a1": "uppdatera",
"b1": "tjena främling &nbsp; <small>(du är inte inloggad)</small>",
"c1": "logga ut",
"d1": "dumpa stacken",
"d2": "visar tillståndet på alla aktiva trådar",
"e1": "ladda om konfig.",
"e2": "ladda om konfigurationsfiler (konton/volymer/volflaggor),$Noch skanna om alla e2ds-volymer$N$Nobs.: ändrade globala inställningar$Nkräver en fullständig omstart",
"f1": "du kan bläddra:",
"g1": "du kan ladda upp till:",
"cc1": "annat:",
"h1": "avaktivera k304",
"i1": "aktivera k304",
"j1": "med k304 aktiverad kommer klienten att koppla bort sig vid varje HTTP 304-fel, vilket kan hindra vissa buggiga proxyservrar från att fastna (sidor slutar ladda), <em>men</em> saker kommer också att bli långsammare i allmänhet",
"k1": "återställ klientinställningar",
"l1": "logga in för att se mer:",
"ls3": "logga in", //m
"lu4": "användarnamn", //m
"lp4": "lösenord", //m
"lo3": "logga ut “{0}” överallt", //m
"lo2": "avsluta sessionen i alla webbläsare", //m
"m1": "välkommen tillbaka,",
"n1": "404 hittades inte &nbsp;┐( ´ -`)┌",
"o1": 'eller så har du kanske inte tillgång -- prova ett lösenord eller <a href="' + SR + '/?h">åk hem</a>',
"p1": "403 nekat &nbsp;~┻━┻",
"q1": 'använd ett lösenord eller <a href="' + SR + '/?h">åk hem</a>',
"r1": "åk hem",
".s1": "skanna om",
"t1": "åtgärd",
"u2": "tid sedan senaste serverskrivning$N( uppladdning / namnbyte / ... )$N$N17d = 17 dagar$N1h23 = 1 timme 23 minuter$N4m56 = 4 minuter 56 sekunder",
"v1": "koppla upp",
"v2": "använd denna server som en lokal disk",
"w1": "byt till https",
"x1": "byt lösenord",
"y1": "redigera utdelningar",
"z1": "lås upp denna utdelning:",
"ta1": "fyll i ditt nya lösenord",
"ta2": "upprepa det nya lösenordet:",
"ta3": "det blev fel; vänligen försök igen",
"aa1": "inkommande filer:",
"ab1": "avaktivera no304",
"ac1": "aktivera no304",
"ad1": "detta stänger av all cachning; prova detta om k304 inte räckte till. Detta kommer att slösa enorma mängder nätverkstrafik!",
"ae1": "aktiva nedladdningar:",
"af1": "visa senaste uppladdningar",
"ag1": "visa idp-cache"
},
"ukr": {
"a1": "оновити",
"b1": "привітик, незнайомцю &nbsp; <small>(ви не авторизовані)</small>",
"c1": "вийти",
"d1": "трасування стека",
"d2": "показує стан усіх активних потоків",
"e1": "перезавантажити конфіг",
"e2": "перезавантажити файли конфігурації (облікові записи/томи/прапорці),$Nта пересканувати всі томи e2ds$N$Nувага: будь-які зміни глобальних налаштувань$Nвимагають повного перезапуску",
"f1": "ви можете бачити:",
"g1": "ви можете завантажувати файли в:",
"cc1": "всяка всячина:",
"h1": "вимкнути k304",
"i1": "увімкнути k304",
"j1": "увімкнення k304 буде відключати ваш клієнт при кожному HTTP 304, що може запобігти зависанню деяких глючних проксі (раптово перестають завантажувати сторінки), <em>але</em> це також зробить усе повільнішим загалом",
"k1": "скинути налаштування клієнта",
"l1": "авторизуйтесь для інших опцій:",
"ls3": "увійти", //m
"lu4": "ім'я користувача", //m
"lp4": "пароль", //m
"lo3": "вийти з облікового запису “{0}” всюди", //m
"lo2": "це завершить сеанс у всіх браузерах", //m
"m1": "з поверненням,",
"n1": "404 не знайдено &nbsp;┐( ´ -`)┌",
"o1": 'або у вас немає доступу -- спробуйте авторизуватися або <a href="' + SR + '/?h">повернутися на головну</a>',
"p1": "403 доступ заборонений &nbsp;~┻━┻",
"q1": 'авторизуйтесь або <a href="' + SR + '/?h">поверніться на головну</a>',
"r1": "повернутися на головну",
".s1": "пересканувати",
"t1": "дія",
"u2": "час з останнього запису сервера$N( завантаження / перейменування / ... )$N$N17d = 17 днів$N1h23 = 1 година 23 хвилини$N4m56 = 4 хвилини 56 секунд",
"v1": "підключити",
"v2": "використовувати цей сервер як локальний HDD",
"w1": "перейти на https",
"x1": "змінити пароль",
"y1": "керування доступом",
"z1": "розблокувати:",
"ta1": "спочатку заповніть ваш новий пароль",
"ta2": "повторіть для підтвердження нового пароля:",
"ta3": "описка; спробуйте знову",
"aa1": "вхідні файли:",
"ab1": "вимкнути no304",
"ac1": "увімкнути no304",
"ad1": "увімкнення no304 вимкне все кешування; спробуйте це, якщо k304 було недостатньо. Це витратить величезну кількість мережевого трафіку!",
"ae1": "активні завантаження:",
"af1": "показати нещодавні завантаження",
"ag1": "показати відомих IdP-користувачів"
},
"rus": {
"a1": "обновить",
"b1": "приветик, незнакомец &nbsp; <small>(вы не авторизованы)</small>",
"c1": "выйти",
"d1": "трассировка стека",
"d2": "показывает состояние всех активных потоков",
"e1": "перезагрузить конфиг",
"e2": "перезагрузить файлы конфига (аккаунты/хранилища/флаги),$Nи пересканировать все хранилища с флагом e2ds$N$Nвнимание: изменения глобальных настроек$Nтребуют полного перезапуска сервера",
"f1": "вы можете видеть:",
"g1": "вы можете загружать файлы в:",
"cc1": "всякая всячина:",
"h1": "отключить k304",
"i1": "включить k304",
"j1": "включённый k304 будет отключать вас при получении HTTP 304, что может помочь при работе с некоторыми глючными прокси (перестают загружаться страницы), <em>но</em> это также сделает работу клиента медленнее",
"k1": "сбросить локальные настройки",
"l1": "авторизуйтесь для других опций:",
"ls3": "войти", //m
"lu4": "имя пользователя", //m
"lp4": "пароль", //m
"lo3": "выйти из “{0}” везде", //m
"lo2": "это завершит сеанс во всех браузерах", //m
"m1": "с возвращением,",
"n1": "404 не найдено &nbsp;┐( ´ -`)┌",
"o1": 'или у вас нет доступа -- попробуйте авторизоваться или <a href="' + SR + '/?h">вернуться на главную</a>',
"p1": "403 доступ запрещён &nbsp;~┻━┻",
"q1": 'авторизуйтесь или <a href="' + SR + '/?h">вернитесь на главную</a>',
"r1": "вернуться на главную",
".s1": "пересканировать",
"t1": "действия",
"u2": "время с последней записи на сервер$N( загрузка / переименование / ... )$N$N17d = 17 дней$N1h23 = 1 час 23 минут$N4m56 = 4 минут 56 секунд",
"v1": "подключить",
"v2": "использовать сервер как локальный диск",
"w1": "перейти на https",
"x1": "поменять пароль",
"y1": "управление доступом",
"z1": "разблокировать:",
"ta1": "сначала введите свой новый пароль",
"ta2": "повторите новый пароль:",
"ta3": "опечатка; попробуйте снова",
"aa1": "входящие файлы:",
"ab1": "отключить no304",
"ac1": "включить no304",
"ad1": "включённый no304 полностью отключит хеширование; используйте, если k304 не помог. Сильно увеличит объём трафика!",
"ae1": "активные скачивания:",
"af1": "показать недавние загрузки",
"ag1": "показать известных IdP-пользователей",
},
}; };
if (window.langmod) if (window.langmod)
@ -777,14 +109,7 @@ for (var k in (d || {})) {
o[a].innerHTML = d[k]; o[a].innerHTML = d[k];
else if (f == 2) else if (f == 2)
o[a].setAttribute("tt", d[k]); o[a].setAttribute("tt", d[k]);
else if (f == 3)
o[a].setAttribute("value", d[k]);
else if (f == 4)
o[a].setAttribute("placeholder", " " + d[k]);
} }
var o1 = ebi('lo'), o2 = ebi('un');
if (o1 && o2 && d.lo3)
o1.setAttribute("value", d.lo3.format(o2.textContent));
try { try {
if (is_idp) { if (is_idp) {
@ -796,8 +121,8 @@ try {
catch (ex) { } catch (ex) { }
tt.init(); tt.init();
var o = QS('input[name="uname"]') || QS('input[name="cppwd"]'); var o = QS('input[name="cppwd"]');
if (!MOBILE && !ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight) if (!ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight)
o.focus(); o.focus();
o = ebi('u'); o = ebi('u');
@ -806,9 +131,6 @@ if (o && /[0-9]+$/.exec(o.innerHTML))
ebi('uhash').value = '' + location.hash; ebi('uhash').value = '' + location.hash;
if (/\&re=/.test('' + location))
ebi('a').className = 'af g';
(function() { (function() {
if (!ebi('x')) if (!ebi('x'))
return; return;

View file

@ -36,13 +36,12 @@
<span class="os lin mac"> <span class="os lin mac">
{% if accs %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint {% if accs %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint
</span> </span>
{% if accs %}<a href="#" id="setpw">use real password</a>{% endif %} <a href="#" id="setpw">use real password</a>
<a href="#" id="qr">show qr</a>
</p> </p>
{% if args.have_idp_hdrs %} {% if args.idp_h_usr %}
<p style="line-height:2em"><b>WARNING:</b> this server is using IdP-based authentication, so this stuff may not work as advertised. Depending on server config, these commands can probably only be used to access areas which don't require authentication, unless you auth using any non-IdP accounts defined in the copyparty config. Please see <a href="https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients">the IdP docs</a></p> <p style="line-height:2em"><b>WARNING:</b> this server is using IdP-based authentication, so this stuff may not work as advertised. Depending on server config, these commands can probably only be used to access areas which don't require authentication, unless you auth using any non-IdP accounts defined in the copyparty config. Please see <a href="https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients">the IdP docs</a></p>
{% endif %} {% endif %}
@ -102,7 +101,6 @@
gio mount -a dav{{ s }}://{{ ep }}/{{ rvp }} gio mount -a dav{{ s }}://{{ ep }}/{{ rvp }}
{%- endif %} {%- endif %}
</pre> </pre>
<p>on KDE Dolphin, use <code>webdav{{ s }}://{{ ep }}/{{ rvp }}</code></p>
</div> </div>
<div class="os mac"> <div class="os mac">
@ -241,26 +239,14 @@
<div class="os win"> <div class="os win">
<h1>ShareX</h1> <h1>ShareX</h1>
<p>to upload screenshots using ShareX <a href="https://getsharex.com/">v15+</a>, save this as <code>copyparty.sxcu</code> and run it:</p> <p>to upload screenshots using ShareX <a href="https://github.com/ShareX/ShareX/releases/tag/v12.1.1">v12</a> or <a href="https://getsharex.com/">v15+</a>, save this as <code>copyparty.sxcu</code> and run it:</p>
<pre class="dl" name="copyparty.sxcu">
{ "Version": "15.0.0", "Name": "copyparty",
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
"Headers": {
{% if accs %}"pw": "<b>{{ pw }}</b>", {% endif %}"accept": "url"
},
"DestinationType": "ImageUploader, TextUploader, FileUploader",
"Body": "MultipartFormData", "URL": "{response}",
"RequestMethod": "POST", "FileFormName": "f" }
</pre>
<p>for ShareX <a href="https://github.com/ShareX/ShareX/releases/tag/v12.1.1">v12</a> specifically, save this as <code>copyparty.sxcu</code> and run it:</p>
<pre class="dl" name="copyparty.sxcu"> <pre class="dl" name="copyparty.sxcu">
{ "Name": "copyparty", { "Name": "copyparty",
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}", "RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
"Headers": { "Headers": {
{% if accs %}"pw": "<b>{{ pw }}</b>", {% endif %}"accept": "url" {% if accs %}"pw": "<b>{{ pw }}</b>",{% endif %}
"accept": "url"
}, },
"DestinationType": "ImageUploader, TextUploader, FileUploader", "DestinationType": "ImageUploader, TextUploader, FileUploader",
"FileFormName": "f" } "FileFormName": "f" }

View file

@ -1,3 +1,11 @@
function QSA(x) {
return document.querySelectorAll(x);
}
var LINUX = /Linux/.test(navigator.userAgent),
MACOS = /[^a-z]mac ?os/i.test(navigator.userAgent),
WINDOWS = /Windows/.test(navigator.userAgent);
var oa = QSA('pre'); var oa = QSA('pre');
for (var a = 0; a < oa.length; a++) { for (var a = 0; a < oa.length; a++) {
var html = oa[a].innerHTML, var html = oa[a].innerHTML,
@ -49,14 +57,12 @@ function setos(os) {
setos(WINDOWS ? 'win' : LINUX ? 'lin' : MACOS ? 'mac' : 'idk'); setos(WINDOWS ? 'win' : LINUX ? 'lin' : MACOS ? 'mac' : 'idk');
var pw = ''; ebi('setpw').onclick = function (e) {
function setpw(e) {
ev(e); ev(e);
modal.prompt('password:', '', function (v) { modal.prompt('password:', '', function (v) {
if (!v) if (!v)
return; return;
pw = v;
var pw0 = ebi('pw0').innerHTML, var pw0 = ebi('pw0').innerHTML,
oa = QSA('b'); oa = QSA('b');
@ -67,14 +73,3 @@ function setpw(e) {
add_dls(); add_dls();
}); });
} }
if (ebi('setpw'))
ebi('setpw').onclick = setpw;
ebi('qr').onclick = function () {
var url = ('' + location).split('?')[0];
if (pw)
url += '?pw=' + pw;
var txt = esc(url) + '<img class="b64" width="100" height="100" src="' + addq(url, 'qr') + '" />';
modal.alert(txt);
};

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