Compare commits

...

10 Commits

Author SHA1 Message Date
ed
fc024f789d v0.11.14 2021-06-14 03:05:50 +02:00
ed
473e773aea fix deadlock 2021-06-14 00:55:11 +00:00
ed
48a2e1a353 add threadwatcher 2021-06-14 01:57:18 +02:00
ed
6da63fbd79 up2k-cli: recover from lost handshakes 2021-06-14 01:01:06 +02:00
ed
5bec37fcee fix cosmetic login glitch 2021-06-14 00:28:08 +02:00
ed
3fd0ba0a31 oh right its the other way around 2021-06-13 22:49:55 +02:00
ed
241a143366 add --rproxy for explicit proxy level 2021-06-13 22:22:31 +02:00
ed
a537064da7 custom-css example to add filetype icons 2021-06-13 00:49:28 +02:00
ed
f3dfd24c92 v0.11.13 2021-06-12 20:37:05 +02:00
ed
fa0a7f50bb add image gallery 2021-06-12 20:25:08 +02:00
19 changed files with 1020 additions and 88 deletions

View File

@@ -106,6 +106,7 @@ summary: all planned features work! now please enjoy the bloatening
* ☑ images using Pillow * ☑ images using Pillow
* ☑ videos using FFmpeg * ☑ videos using FFmpeg
* ☑ cache eviction (max-age; maybe max-size eventually) * ☑ cache eviction (max-age; maybe max-size eventually)
* ☑ image gallery
* ☑ SPA (browse while uploading) * ☑ SPA (browse while uploading)
* if you use the file-tree on the left only, not folders in the file list * if you use the file-tree on the left only, not folders in the file list
* server indexing * server indexing

View File

@@ -23,7 +23,7 @@ from textwrap import dedent
from .__init__ import E, WINDOWS, VT100, PY2 from .__init__ import E, WINDOWS, VT100, PY2
from .__version__ import S_VERSION, S_BUILD_DT, CODENAME from .__version__ import S_VERSION, S_BUILD_DT, CODENAME
from .svchub import SvcHub from .svchub import SvcHub
from .util import py_desc, align_tab, IMPLICATIONS from .util import py_desc, align_tab, IMPLICATIONS, alltrace
HAVE_SSL = True HAVE_SSL = True
try: try:
@@ -182,6 +182,16 @@ def sighandler(sig=None, frame=None):
print("\n".join(msg)) print("\n".join(msg))
def stackmon(fp, ival):
ctr = 0
while True:
ctr += 1
time.sleep(ival)
st = "{}, {}\n{}".format(ctr, time.time(), alltrace())
with open(fp, "wb") as f:
f.write(st.encode("utf-8", "replace"))
def run_argparse(argv, formatter): def run_argparse(argv, formatter):
ap = argparse.ArgumentParser( ap = argparse.ArgumentParser(
formatter_class=formatter, formatter_class=formatter,
@@ -222,10 +232,6 @@ def run_argparse(argv, formatter):
"print,get" prints the data in the log and returns GET "print,get" prints the data in the log and returns GET
(leave out the ",get" to return an error instead) (leave out the ",get" to return an error instead)
--ciphers help = available ssl/tls ciphers,
--ssl-ver help = available ssl/tls versions,
default is what python considers safe, usually >= TLS1
values for --ls: values for --ls:
"USR" is a user to browse as; * is anonymous, ** is all users "USR" is a user to browse as; * is anonymous, ** is all users
"VOL" is a single volume to scan, default is * (all vols) "VOL" is a single volume to scan, default is * (all vols)
@@ -244,27 +250,45 @@ def run_argparse(argv, formatter):
) )
# fmt: off # fmt: off
ap.add_argument("-c", metavar="PATH", type=str, action="append", help="add config file") ap.add_argument("-c", metavar="PATH", type=str, action="append", help="add config file")
ap.add_argument("-i", metavar="IP", type=str, default="0.0.0.0", help="ip to bind (comma-sep.)")
ap.add_argument("-p", metavar="PORT", type=str, default="3923", help="ports to bind (comma/range)")
ap.add_argument("-nc", metavar="NUM", type=int, default=64, help="max num clients") ap.add_argument("-nc", metavar="NUM", type=int, default=64, help="max num clients")
ap.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores") ap.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores")
ap.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account") ap.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account, USER:PASS; example [ed:wark")
ap.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume") ap.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume, SRC:DST:FLAG; example [.::r], [/mnt/nas/music:/music:r:aed")
ap.add_argument("-q", action="store_true", help="quiet")
ap.add_argument("-ed", action="store_true", help="enable ?dots") ap.add_argument("-ed", action="store_true", help="enable ?dots")
ap.add_argument("-emp", action="store_true", help="enable markdown plugins") ap.add_argument("-emp", action="store_true", help="enable markdown plugins")
ap.add_argument("-mcr", metavar="SEC", type=int, default=60, help="md-editor mod-chk rate") ap.add_argument("-mcr", metavar="SEC", type=int, default=60, help="md-editor mod-chk rate")
ap.add_argument("-nw", action="store_true", help="disable writes (benchmark)")
ap.add_argument("-nih", action="store_true", help="no info hostname")
ap.add_argument("-nid", action="store_true", help="no info disk-usage")
ap.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads") ap.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads")
ap.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
ap.add_argument("--sparse", metavar="MiB", type=int, default=4, help="up2k min.size threshold (mswin-only)") ap.add_argument("--sparse", metavar="MiB", type=int, default=4, help="up2k min.size threshold (mswin-only)")
ap.add_argument("--urlform", metavar="MODE", type=str, default="print,get", help="how to handle url-forms") ap.add_argument("--urlform", metavar="MODE", type=str, default="print,get", help="how to handle url-forms; examples: [stash], [save,get]")
ap.add_argument("--salt", type=str, default="hunter2", help="up2k file-hash salt")
ap2 = ap.add_argument_group('appearance options') ap2 = ap.add_argument_group('network options')
ap2.add_argument("--css-browser", metavar="L", help="URL to additional CSS to include") ap2.add_argument("-i", metavar="IP", type=str, default="0.0.0.0", help="ip to bind (comma-sep.)")
ap2.add_argument("-p", metavar="PORT", type=str, default="3923", help="ports to bind (comma/range)")
ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to keep; 0 = tcp, 1 = origin (first x-fwd), 2 = cloudflare, 3 = nginx, -1 = closest proxy")
ap2 = ap.add_argument_group('SSL/TLS options')
ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls")
ap2.add_argument("--https-only", action="store_true", help="disable plaintext")
ap2.add_argument("--ssl-ver", metavar="LIST", type=str, help="set allowed ssl/tls versions; [help] shows available versions; default is what your python version considers safe")
ap2.add_argument("--ciphers", metavar="LIST", help="set allowed ssl/tls ciphers; [help] shows available ciphers")
ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info")
ap2.add_argument("--ssl-log", metavar="PATH", help="log master secrets")
ap2 = ap.add_argument_group('opt-outs')
ap2.add_argument("-nw", action="store_true", help="disable writes (benchmark)")
ap2.add_argument("-nih", action="store_true", help="no info hostname")
ap2.add_argument("-nid", action="store_true", help="no info disk-usage")
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
ap2 = ap.add_argument_group('safety options')
ap2.add_argument("--ls", metavar="U[,V[,F]]", help="scan all volumes; arguments USER,VOL,FLAGS; example [**,*,ln,p,r]")
ap2.add_argument("--salt", type=str, default="hunter2", help="up2k file-hash salt")
ap2 = ap.add_argument_group('logging options')
ap2.add_argument("-q", action="store_true", help="quiet")
ap2.add_argument("--log-conn", action="store_true", help="print tcp-server msgs")
ap2.add_argument("--ihead", metavar="HEADER", action='append', help="dump incoming header")
ap2.add_argument("--lf-url", metavar="RE", type=str, default=r"^/\.cpr/|\?th=[wj]$", help="dont log URLs matching")
ap2 = ap.add_argument_group('admin panel options') ap2 = ap.add_argument_group('admin panel options')
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)")
@@ -299,22 +323,14 @@ def run_argparse(argv, formatter):
ap2.add_argument("-mtp", metavar="M=[f,]bin", action="append", type=str, help="read tag M using bin") ap2.add_argument("-mtp", metavar="M=[f,]bin", action="append", type=str, help="read tag M using bin")
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=30, help="search deadline") ap2.add_argument("--srch-time", metavar="SEC", type=int, default=30, help="search deadline")
ap2 = ap.add_argument_group('SSL/TLS options') ap2 = ap.add_argument_group('appearance options')
ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls") ap2.add_argument("--css-browser", metavar="L", help="URL to additional CSS to include")
ap2.add_argument("--https-only", action="store_true", help="disable plaintext")
ap2.add_argument("--ssl-ver", metavar="LIST", type=str, help="ssl/tls versions to allow")
ap2.add_argument("--ciphers", metavar="LIST", help="set allowed ciphers")
ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info")
ap2.add_argument("--ssl-log", metavar="PATH", help="log master secrets")
ap2 = ap.add_argument_group('debug options') ap2 = ap.add_argument_group('debug options')
ap2.add_argument("--ls", metavar="U[,V[,F]]", help="scan all volumes")
ap2.add_argument("--log-conn", action="store_true", help="print tcp-server msgs")
ap2.add_argument("--no-sendfile", action="store_true", help="disable sendfile") ap2.add_argument("--no-sendfile", action="store_true", help="disable sendfile")
ap2.add_argument("--no-scandir", action="store_true", help="disable scandir") ap2.add_argument("--no-scandir", action="store_true", help="disable scandir")
ap2.add_argument("--no-fastboot", action="store_true", help="wait for up2k indexing") ap2.add_argument("--no-fastboot", action="store_true", help="wait for up2k indexing")
ap2.add_argument("--ihead", metavar="HEADER", action='append', help="dump incoming header") ap2.add_argument("--stackmon", metavar="P,S", help="write stacktrace to Path every S second")
ap2.add_argument("--lf-url", metavar="RE", type=str, default=r"^/\.cpr/|\?th=[wj]$", help="dont log URLs matching")
return ap.parse_args(args=argv[1:]) return ap.parse_args(args=argv[1:])
# fmt: on # fmt: on
@@ -354,6 +370,16 @@ def main(argv=None):
except AssertionError: except AssertionError:
al = run_argparse(argv, Dodge11874) al = run_argparse(argv, Dodge11874)
if al.stackmon:
fp, f = al.stackmon.rsplit(",", 1)
f = int(f)
t = threading.Thread(
target=stackmon,
args=(fp, f),
)
t.daemon = True
t.start()
# propagate implications # propagate implications
for k1, k2 in IMPLICATIONS: for k1, k2 in IMPLICATIONS:
if getattr(al, k1): if getattr(al, k1):

View File

@@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
VERSION = (0, 11, 12) VERSION = (0, 11, 14)
CODENAME = "the grid" CODENAME = "the grid"
BUILD_DT = (2021, 6, 12) BUILD_DT = (2021, 6, 14)
S_VERSION = ".".join(map(str, VERSION)) S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View File

@@ -104,10 +104,21 @@ class HttpCli(object):
v = self.headers.get("connection", "").lower() v = self.headers.get("connection", "").lower()
self.keepalive = not v.startswith("close") and self.http_ver != "HTTP/1.0" self.keepalive = not v.startswith("close") and self.http_ver != "HTTP/1.0"
v = self.headers.get("x-forwarded-for", None) n = self.args.rproxy
if v is not None and self.conn.addr[0] in ["127.0.0.1", "::1"]: if n:
self.ip = v.split(",")[0] v = self.headers.get("x-forwarded-for")
self.log_src = self.conn.set_rproxy(self.ip) if v and self.conn.addr[0] in ["127.0.0.1", "::1"]:
if n > 0:
n -= 1
vs = v.split(",")
try:
self.ip = vs[n].strip()
except:
self.ip = vs[-1].strip()
self.log("rproxy={} oob x-fwd {}".format(self.args.rproxy, v), c=3)
self.log_src = self.conn.set_rproxy(self.ip)
if self.args.ihead: if self.args.ihead:
keys = self.args.ihead keys = self.args.ihead
@@ -1423,33 +1434,8 @@ class HttpCli(object):
if self.args.no_stack: if self.args.no_stack:
raise Pebkac(403, "disabled by argv") raise Pebkac(403, "disabled by argv")
threads = {} ret = "<pre>{}\n{}".format(time.time(), alltrace())
names = dict([(t.ident, t.name) for t in threading.enumerate()]) self.reply(ret.encode("utf-8"))
for tid, stack in sys._current_frames().items():
name = "{} ({:x})".format(names.get(tid), tid)
threads[name] = stack
rret = []
bret = []
for name, stack in sorted(threads.items()):
ret = ["\n\n# {}".format(name)]
pad = None
for fn, lno, name, line in traceback.extract_stack(stack):
fn = os.sep.join(fn.split(os.sep)[-3:])
ret.append('File: "{}", line {}, in {}'.format(fn, lno, name))
if line:
ret.append(" " + str(line.strip()))
if "self.not_empty.wait()" in line:
pad = " " * 4
if pad:
bret += [ret[0]] + [pad + x for x in ret[1:]]
else:
rret += ret
ret = rret + bret
ret = ("<pre>" + "\n".join(ret)).encode("utf-8")
self.reply(ret)
def tx_tree(self): def tx_tree(self):
top = self.uparam["tree"] or "" top = self.uparam["tree"] or ""

View File

@@ -154,7 +154,8 @@ class ThumbSrv(object):
histpath = self.asrv.vfs.histtab[ptop] histpath = self.asrv.vfs.histtab[ptop]
tpath = thumb_path(histpath, rem, mtime, fmt) tpath = thumb_path(histpath, rem, mtime, fmt)
abspath = os.path.join(ptop, rem) abspath = os.path.join(ptop, rem)
cond = threading.Condition() cond = threading.Condition(self.mutex)
do_conv = False
with self.mutex: with self.mutex:
try: try:
self.busy[tpath].append(cond) self.busy[tpath].append(cond)
@@ -172,8 +173,11 @@ class ThumbSrv(object):
f.write(fsenc(os.path.dirname(abspath))) f.write(fsenc(os.path.dirname(abspath)))
self.busy[tpath] = [cond] self.busy[tpath] = [cond]
self.q.put([abspath, tpath]) do_conv = True
self.log("conv {} \033[0m{}".format(tpath, abspath), c=6)
if do_conv:
self.q.put([abspath, tpath])
self.log("conv {} \033[0m{}".format(tpath, abspath), c=6)
while not self.stopping: while not self.stopping:
with self.mutex: with self.mutex:
@@ -181,7 +185,7 @@ class ThumbSrv(object):
break break
with cond: with cond:
cond.wait() cond.wait(3)
try: try:
st = os.stat(tpath) st = os.stat(tpath)

View File

@@ -1022,7 +1022,7 @@ class Up2k(object):
now = time.time() now = time.time()
job = None job = None
with self.mutex: with self.mutex:
cur = self.cur.get(cj["ptop"], None) cur = self.cur.get(cj["ptop"])
reg = self.registry[cj["ptop"]] reg = self.registry[cj["ptop"]]
if cur: if cur:
if self.no_expr_idx: if self.no_expr_idx:
@@ -1180,7 +1180,7 @@ class Up2k(object):
def handle_chunk(self, ptop, wark, chash): def handle_chunk(self, ptop, wark, chash):
with self.mutex: with self.mutex:
job = self.registry[ptop].get(wark, None) job = self.registry[ptop].get(wark)
if not job: if not job:
known = " ".join([x for x in self.registry[ptop].keys()]) known = " ".join([x for x in self.registry[ptop].keys()])
self.log("unknown wark [{}], known: {}".format(wark, known)) self.log("unknown wark [{}], known: {}".format(wark, known))
@@ -1245,7 +1245,7 @@ class Up2k(object):
return ret, dst return ret, dst
def idx_wark(self, ptop, wark, rd, fn, lmod, sz): def idx_wark(self, ptop, wark, rd, fn, lmod, sz):
cur = self.cur.get(ptop, None) cur = self.cur.get(ptop)
if not cur: if not cur:
return False return False
@@ -1414,7 +1414,7 @@ class Up2k(object):
newest = max(x["poke"] for _, x in reg.items()) if reg else 0 newest = max(x["poke"] for _, x in reg.items()) if reg else 0
etag = [len(reg), newest] etag = [len(reg), newest]
if etag == prev.get(ptop, None): if etag == prev.get(ptop):
return return
try: try:

View File

@@ -254,6 +254,34 @@ def trace(*args, **kwargs):
nuprint(msg) nuprint(msg)
def alltrace():
threads = {}
names = dict([(t.ident, t.name) for t in threading.enumerate()])
for tid, stack in sys._current_frames().items():
name = "{} ({:x})".format(names.get(tid), tid)
threads[name] = stack
rret = []
bret = []
for name, stack in sorted(threads.items()):
ret = ["\n\n# {}".format(name)]
pad = None
for fn, lno, name, line in traceback.extract_stack(stack):
fn = os.sep.join(fn.split(os.sep)[-3:])
ret.append('File: "{}", line {}, in {}'.format(fn, lno, name))
if line:
ret.append(" " + str(line.strip()))
if "self.not_empty.wait()" in line:
pad = " " * 4
if pad:
bret += [ret[0]] + [pad + x for x in ret[1:]]
else:
rret += ret
return "\n".join(rret + bret)
def min_ex(): def min_ex():
et, ev, tb = sys.exc_info() et, ev, tb = sys.exc_info()
tb = traceback.extract_tb(tb, 2) tb = traceback.extract_tb(tb, 2)

View File

@@ -0,0 +1,583 @@
/*!
* baguetteBox.js
* @author feimosi
* @version 1.11.1-mod
* @url https://github.com/feimosi/baguetteBox.js
*/
window.baguetteBox = (function () {
'use strict';
var options = {},
defaults = {
captions: true,
buttons: 'auto',
noScrollbars: false,
bodyClass: 'baguetteBox-open',
titleTag: false,
async: false,
preload: 2,
animation: 'slideIn',
afterShow: null,
afterHide: null,
onChange: null,
},
overlay, slider, previousButton, nextButton, closeButton,
currentGallery = [],
currentIndex = 0,
isOverlayVisible = false,
touch = {}, // start-pos
touchFlag = false, // busy
regex = /.+\.(gif|jpe?g|png|webp)/i,
data = {}, // all galleries
imagesElements = [],
documentLastFocus = null;
var overlayClickHandler = function (event) {
if (event.target.id.indexOf('baguette-img') !== -1) {
hideOverlay();
}
};
var touchstartHandler = function (event) {
touch.count++;
if (touch.count > 1) {
touch.multitouch = true;
}
touch.startX = event.changedTouches[0].pageX;
touch.startY = event.changedTouches[0].pageY;
};
var touchmoveHandler = function (event) {
if (touchFlag || touch.multitouch) {
return;
}
event.preventDefault ? event.preventDefault() : event.returnValue = false;
var touchEvent = event.touches[0] || event.changedTouches[0];
if (touchEvent.pageX - touch.startX > 40) {
touchFlag = true;
showPreviousImage();
} else if (touchEvent.pageX - touch.startX < -40) {
touchFlag = true;
showNextImage();
} else if (touch.startY - touchEvent.pageY > 100) {
hideOverlay();
}
};
var touchendHandler = function () {
touch.count--;
if (touch.count <= 0) {
touch.multitouch = false;
}
touchFlag = false;
};
var contextmenuHandler = function () {
touchendHandler();
};
var trapFocusInsideOverlay = function (event) {
if (overlay.style.display === 'block' && (overlay.contains && !overlay.contains(event.target))) {
event.stopPropagation();
initFocus();
}
};
function run(selector, userOptions) {
buildOverlay();
removeFromCache(selector);
return bindImageClickListeners(selector, userOptions);
}
function bindImageClickListeners(selector, userOptions) {
var galleryNodeList = document.querySelectorAll(selector);
var selectorData = {
galleries: [],
nodeList: galleryNodeList
};
data[selector] = selectorData;
[].forEach.call(galleryNodeList, function (galleryElement) {
if (userOptions && userOptions.filter) {
regex = userOptions.filter;
}
var tagsNodeList = [];
if (galleryElement.tagName === 'A') {
tagsNodeList = [galleryElement];
} else {
tagsNodeList = galleryElement.getElementsByTagName('a');
}
tagsNodeList = [].filter.call(tagsNodeList, function (element) {
if (element.className.indexOf(userOptions && userOptions.ignoreClass) === -1) {
return regex.test(element.href);
}
});
if (tagsNodeList.length === 0) {
return;
}
var gallery = [];
[].forEach.call(tagsNodeList, function (imageElement, imageIndex) {
var imageElementClickHandler = function (event) {
if (event && event.ctrlKey)
return true;
event.preventDefault ? event.preventDefault() : event.returnValue = false;
prepareOverlay(gallery, userOptions);
showOverlay(imageIndex);
};
var imageItem = {
eventHandler: imageElementClickHandler,
imageElement: imageElement
};
bind(imageElement, 'click', imageElementClickHandler);
gallery.push(imageItem);
});
selectorData.galleries.push(gallery);
});
return selectorData.galleries;
}
function clearCachedData() {
for (var selector in data) {
if (data.hasOwnProperty(selector)) {
removeFromCache(selector);
}
}
}
function removeFromCache(selector) {
if (!data.hasOwnProperty(selector)) {
return;
}
var galleries = data[selector].galleries;
[].forEach.call(galleries, function (gallery) {
[].forEach.call(gallery, function (imageItem) {
unbind(imageItem.imageElement, 'click', imageItem.eventHandler);
});
if (currentGallery === gallery) {
currentGallery = [];
}
});
delete data[selector];
}
function buildOverlay() {
overlay = ebi('baguetteBox-overlay');
if (overlay) {
slider = ebi('baguetteBox-slider');
previousButton = ebi('previous-button');
nextButton = ebi('next-button');
closeButton = ebi('close-button');
return;
}
overlay = mknod('div');
overlay.setAttribute('role', 'dialog');
overlay.id = 'baguetteBox-overlay';
document.getElementsByTagName('body')[0].appendChild(overlay);
slider = mknod('div');
slider.id = 'baguetteBox-slider';
overlay.appendChild(slider);
previousButton = mknod('button');
previousButton.setAttribute('type', 'button');
previousButton.id = 'previous-button';
previousButton.setAttribute('aria-label', 'Previous');
previousButton.innerHTML = '&lt;';
overlay.appendChild(previousButton);
nextButton = mknod('button');
nextButton.setAttribute('type', 'button');
nextButton.id = 'next-button';
nextButton.setAttribute('aria-label', 'Next');
nextButton.innerHTML = '&gt;';
overlay.appendChild(nextButton);
closeButton = mknod('button');
closeButton.setAttribute('type', 'button');
closeButton.id = 'close-button';
closeButton.setAttribute('aria-label', 'Close');
closeButton.innerHTML = '&times;';
overlay.appendChild(closeButton);
previousButton.className = nextButton.className = closeButton.className = 'baguetteBox-button';
bindEvents();
}
function keyDownHandler(event) {
switch (event.keyCode) {
case 37: // Left
showPreviousImage();
break;
case 39: // Right
showNextImage();
break;
case 27: // Esc
hideOverlay();
break;
case 36: // Home
showFirstImage(event);
break;
case 35: // End
showLastImage(event);
break;
}
}
var passiveSupp = false;
try {
var opts = {
get passive() {
passiveSupp = true;
return false;
}
};
window.addEventListener('test', null, opts);
window.removeEventListener('test', null, opts);
}
catch (ex) {
passiveSupp = false;
}
var passiveEvent = passiveSupp ? { passive: false } : null;
var nonPassiveEvent = passiveSupp ? { passive: true } : null;
function bindEvents() {
bind(overlay, 'click', overlayClickHandler);
bind(previousButton, 'click', showPreviousImage);
bind(nextButton, 'click', showNextImage);
bind(closeButton, 'click', hideOverlay);
bind(slider, 'contextmenu', contextmenuHandler);
bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
bind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
bind(overlay, 'touchend', touchendHandler);
bind(document, 'focus', trapFocusInsideOverlay, true);
}
function unbindEvents() {
unbind(overlay, 'click', overlayClickHandler);
unbind(previousButton, 'click', showPreviousImage);
unbind(nextButton, 'click', showNextImage);
unbind(closeButton, 'click', hideOverlay);
unbind(slider, 'contextmenu', contextmenuHandler);
unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
unbind(overlay, 'touchend', touchendHandler);
unbind(document, 'focus', trapFocusInsideOverlay, true);
}
function prepareOverlay(gallery, userOptions) {
if (currentGallery === gallery) {
return;
}
currentGallery = gallery;
setOptions(userOptions);
slider.innerHTML = '';
imagesElements.length = 0;
var imagesFiguresIds = [];
var imagesCaptionsIds = [];
for (var i = 0, fullImage; i < gallery.length; i++) {
fullImage = mknod('div');
fullImage.className = 'full-image';
fullImage.id = 'baguette-img-' + i;
imagesElements.push(fullImage);
imagesFiguresIds.push('baguetteBox-figure-' + i);
imagesCaptionsIds.push('baguetteBox-figcaption-' + i);
slider.appendChild(imagesElements[i]);
}
overlay.setAttribute('aria-labelledby', imagesFiguresIds.join(' '));
overlay.setAttribute('aria-describedby', imagesCaptionsIds.join(' '));
}
function setOptions(newOptions) {
if (!newOptions) {
newOptions = {};
}
for (var item in defaults) {
options[item] = defaults[item];
if (typeof newOptions[item] !== 'undefined') {
options[item] = newOptions[item];
}
}
slider.style.transition = (options.animation === 'fadeIn' ? 'opacity .4s ease' :
options.animation === 'slideIn' ? '' : 'none');
if (options.buttons === 'auto' && ('ontouchstart' in window || currentGallery.length === 1)) {
options.buttons = false;
}
previousButton.style.display = nextButton.style.display = (options.buttons ? '' : 'none');
}
function showOverlay(chosenImageIndex) {
if (options.noScrollbars) {
document.documentElement.style.overflowY = 'hidden';
document.body.style.overflowY = 'scroll';
}
if (overlay.style.display === 'block') {
return;
}
bind(document, 'keydown', keyDownHandler);
currentIndex = chosenImageIndex;
touch = {
count: 0,
startX: null,
startY: null
};
loadImage(currentIndex, function () {
preloadNext(currentIndex);
preloadPrev(currentIndex);
});
updateOffset();
overlay.style.display = 'block';
// Fade in overlay
setTimeout(function () {
overlay.className = 'visible';
if (options.bodyClass && document.body.classList) {
document.body.classList.add(options.bodyClass);
}
if (options.afterShow) {
options.afterShow();
}
}, 50);
if (options.onChange) {
options.onChange(currentIndex, imagesElements.length);
}
documentLastFocus = document.activeElement;
initFocus();
isOverlayVisible = true;
}
function initFocus() {
if (options.buttons) {
previousButton.focus();
} else {
closeButton.focus();
}
}
function hideOverlay(e) {
ev(e);
if (options.noScrollbars) {
document.documentElement.style.overflowY = 'auto';
document.body.style.overflowY = 'auto';
}
if (overlay.style.display === 'none') {
return;
}
unbind(document, 'keydown', keyDownHandler);
// Fade out and hide the overlay
overlay.className = '';
setTimeout(function () {
overlay.style.display = 'none';
if (options.bodyClass && document.body.classList) {
document.body.classList.remove(options.bodyClass);
}
if (options.afterHide) {
options.afterHide();
}
documentLastFocus && documentLastFocus.focus();
isOverlayVisible = false;
}, 500);
}
function loadImage(index, callback) {
var imageContainer = imagesElements[index];
var galleryItem = currentGallery[index];
if (typeof imageContainer === 'undefined' || typeof galleryItem === 'undefined') {
return; // out-of-bounds or gallery dirty
}
if (imageContainer.getElementsByTagName('img')[0]) {
// image is loaded, cb and bail
if (callback) {
callback();
}
return;
}
var imageElement = galleryItem.imageElement,
imageSrc = imageElement.href,
thumbnailElement = imageElement.getElementsByTagName('img')[0],
imageCaption = typeof options.captions === 'function' ?
options.captions.call(currentGallery, imageElement) :
imageElement.getAttribute('data-caption') || imageElement.title;
var figure = mknod('figure');
figure.id = 'baguetteBox-figure-' + index;
figure.innerHTML = '<div class="baguetteBox-spinner">' +
'<div class="baguetteBox-double-bounce1"></div>' +
'<div class="baguetteBox-double-bounce2"></div>' +
'</div>';
if (options.captions && imageCaption) {
var figcaption = mknod('figcaption');
figcaption.id = 'baguetteBox-figcaption-' + index;
figcaption.innerHTML = imageCaption;
figure.appendChild(figcaption);
}
imageContainer.appendChild(figure);
var image = mknod('img');
image.onload = function () {
// Remove loader element
var spinner = document.querySelector('#baguette-img-' + index + ' .baguetteBox-spinner');
figure.removeChild(spinner);
if (!options.async && callback) {
callback();
}
};
image.setAttribute('src', imageSrc);
image.alt = thumbnailElement ? thumbnailElement.alt || '' : '';
if (options.titleTag && imageCaption) {
image.title = imageCaption;
}
figure.appendChild(image);
if (options.async && callback) {
callback();
}
}
function showNextImage(e) {
ev(e);
return show(currentIndex + 1);
}
function showPreviousImage(e) {
ev(e);
return show(currentIndex - 1);
}
function showFirstImage(event) {
if (event) {
event.preventDefault();
}
return show(0);
}
function showLastImage(event) {
if (event) {
event.preventDefault();
}
return show(currentGallery.length - 1);
}
/**
* Move the gallery to a specific index
* @param `index` {number} - the position of the image
* @param `gallery` {array} - gallery which should be opened, if omitted assumes the currently opened one
* @return {boolean} - true on success or false if the index is invalid
*/
function show(index, gallery) {
if (!isOverlayVisible && index >= 0 && index < gallery.length) {
prepareOverlay(gallery, options);
showOverlay(index);
return true;
}
if (index < 0) {
if (options.animation) {
bounceAnimation('left');
}
return false;
}
if (index >= imagesElements.length) {
if (options.animation) {
bounceAnimation('right');
}
return false;
}
currentIndex = index;
loadImage(currentIndex, function () {
preloadNext(currentIndex);
preloadPrev(currentIndex);
});
updateOffset();
if (options.onChange) {
options.onChange(currentIndex, imagesElements.length);
}
return true;
}
/**
* Triggers the bounce animation
* @param {('left'|'right')} direction - Direction of the movement
*/
function bounceAnimation(direction) {
slider.className = 'bounce-from-' + direction;
setTimeout(function () {
slider.className = '';
}, 400);
}
function updateOffset() {
var offset = -currentIndex * 100 + '%';
if (options.animation === 'fadeIn') {
slider.style.opacity = 0;
setTimeout(function () {
slider.style.transform = 'translate3d(' + offset + ',0,0)';
slider.style.opacity = 1;
}, 400);
} else {
slider.style.transform = 'translate3d(' + offset + ',0,0)';
}
}
function preloadNext(index) {
if (index - currentIndex >= options.preload) {
return;
}
loadImage(index + 1, function () {
preloadNext(index + 1);
});
}
function preloadPrev(index) {
if (currentIndex - index >= options.preload) {
return;
}
loadImage(index - 1, function () {
preloadPrev(index - 1);
});
}
function bind(element, event, callback, options) {
element.addEventListener(event, callback, options);
}
function unbind(element, event, callback, options) {
element.removeEventListener(event, callback, options);
}
function destroyPlugin() {
unbindEvents();
clearCachedData();
unbind(document, 'keydown', keyDownHandler);
document.getElementsByTagName('body')[0].removeChild(ebi('baguetteBox-overlay'));
data = {};
currentGallery = [];
currentIndex = 0;
}
return {
run: run,
show: show,
showNext: showNextImage,
showPrevious: showPreviousImage,
hide: hideOverlay,
destroy: destroyPlugin
};
})();

View File

@@ -750,9 +750,12 @@ input[type="checkbox"]:checked+label {
font-family: monospace, monospace; font-family: monospace, monospace;
line-height: 2em; line-height: 2em;
} }
#griden.on+#thumbs { #thumbs {
opacity: .3; opacity: .3;
} }
#griden.on+#thumbs {
opacity: 1;
}
#ghead { #ghead {
background: #3c3c3c; background: #3c3c3c;
border: 1px solid #444; border: 1px solid #444;
@@ -801,8 +804,7 @@ html.light #ghead {
content: '📂'; content: '📂';
line-height: 0; line-height: 0;
font-size: 2em; font-size: 2em;
display: inline-block; margin: -.7em .1em -.5em -.3em;
margin: -.7em 0 -.5em -.3em;
} }
#ggrid a:hover { #ggrid a:hover {
background: #444; background: #444;
@@ -1028,4 +1030,161 @@ html.light #tree::-webkit-scrollbar {
} }
#tree::-webkit-scrollbar-thumb { #tree::-webkit-scrollbar-thumb {
background: #da0; background: #da0;
} }
#baguetteBox-overlay {
display: none;
opacity: 0;
position: fixed;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000000;
background: rgba(0, 0, 0, 0.8);
transition: opacity .3s ease;
}
#baguetteBox-overlay.visible {
opacity: 1;
}
#baguetteBox-overlay .full-image {
display: inline-block;
position: relative;
width: 100%;
height: 100%;
text-align: center;
}
#baguetteBox-overlay .full-image figure {
display: inline;
margin: 0;
height: 100%;
}
#baguetteBox-overlay .full-image img {
display: inline-block;
width: auto;
height: auto;
max-height: 100%;
max-width: 100%;
vertical-align: middle;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
}
#baguetteBox-overlay .full-image figcaption {
display: block;
position: absolute;
bottom: 0;
width: 100%;
text-align: center;
line-height: 1.8;
white-space: normal;
color: #ccc;
}
#baguetteBox-overlay figcaption a {
background: rgba(0, 0, 0, 0.6);
border-radius: .4em;
padding: .3em .6em;
}
#baguetteBox-overlay .full-image:before {
content: "";
display: inline-block;
height: 50%;
width: 1px;
margin-right: -1px;
}
#baguetteBox-slider {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
white-space: nowrap;
transition: left .2s ease, transform .2s ease;
}
#baguetteBox-slider.bounce-from-right {
animation: bounceFromRight .4s ease-out;
}
#baguetteBox-slider.bounce-from-left {
animation: bounceFromLeft .4s ease-out;
}
@keyframes bounceFromRight {
0% {margin-left: 0}
50% {margin-left: -30px}
100% {margin-left: 0}
}
@keyframes bounceFromLeft {
0% {margin-left: 0}
50% {margin-left: 30px}
100% {margin-left: 0}
}
.baguetteBox-button#next-button,
.baguetteBox-button#previous-button {
top: 50%;
top: calc(50% - 30px);
width: 44px;
height: 60px;
}
.baguetteBox-button {
position: absolute;
cursor: pointer;
outline: none;
padding: 0;
margin: 0;
border: 0;
border-radius: 15%;
background: rgba(50, 50, 50, 0.5);
color: #ddd;
font: 1.6em sans-serif;
transition: background-color .3s ease;
}
.baguetteBox-button:focus,
.baguetteBox-button:hover {
background: rgba(50, 50, 50, 0.9);
}
#next-button {
right: 2%;
}
#previous-button {
left: 2%;
}
#close-button {
top: 20px;
right: 2%;
width: 30px;
height: 30px;
}
.baguetteBox-button svg {
position: absolute;
left: 0;
top: 0;
}
.baguetteBox-spinner {
width: 40px;
height: 40px;
display: inline-block;
position: absolute;
top: 50%;
left: 50%;
margin-top: -20px;
margin-left: -20px;
}
.baguetteBox-double-bounce1,
.baguetteBox-double-bounce2 {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #fff;
opacity: .6;
position: absolute;
top: 0;
left: 0;
animation: bounce 2s infinite ease-in-out;
}
.baguetteBox-double-bounce2 {
animation-delay: -1s;
}
@keyframes bounce {
0%, 100% {transform: scale(0)}
50% {transform: scale(1)}
}

View File

@@ -709,8 +709,9 @@ function autoplay_blocked(seek) {
var thegrid = (function () { var thegrid = (function () {
var lfiles = ebi('files'); var lfiles = ebi('files'),
var gfiles = document.createElement('div'); gfiles = document.createElement('div');
gfiles.setAttribute('id', 'gfiles'); gfiles.setAttribute('id', 'gfiles');
gfiles.style.display = 'none'; gfiles.style.display = 'none';
gfiles.innerHTML = ( gfiles.innerHTML = (
@@ -732,7 +733,8 @@ var thegrid = (function () {
'en': bcfg_get('griden', false), 'en': bcfg_get('griden', false),
'sel': bcfg_get('gridsel', false), 'sel': bcfg_get('gridsel', false),
'sz': fcfg_get('gridsz', 10), 'sz': fcfg_get('gridsz', 10),
'isdirty': true 'isdirty': true,
'bbox': null
}; };
ebi('thumbs').onclick = function (e) { ebi('thumbs').onclick = function (e) {
@@ -891,9 +893,37 @@ var thegrid = (function () {
lfiles.style.display = 'none'; lfiles.style.display = 'none';
gfiles.style.display = 'block'; gfiles.style.display = 'block';
ebi('ggrid').innerHTML = html.join('\n'); ebi('ggrid').innerHTML = html.join('\n');
r.bagit();
r.loadsel(); r.loadsel();
} }
r.bagit = function () {
if (!window.baguetteBox)
return;
if (r.bbox)
baguetteBox.destroy();
r.bbox = baguetteBox.run('#ggrid', {
captions: function (g) {
var idx = -1,
h = '' + g;
for (var a = 0; a < r.bbox.length; a++)
if (r.bbox[a].imageElement == g)
idx = a;
return '<a download href="' + h +
'">' + (idx + 1) + ' / ' + r.bbox.length + ' -- ' +
esc(uricom_dec(h.split('/').slice(-1)[0])[0]) + '</a>';
}
})[0];
};
setTimeout(function () {
import_js('/.cpr/baguettebox.js', r.bagit);
}, 1);
if (r.en) { if (r.en) {
loadgrid(); loadgrid();
} }

View File

@@ -59,7 +59,7 @@
<h1>login for more:</h1> <h1>login for more:</h1>
<ul> <ul>
<form method="post" enctype="multipart/form-data" action="/{{ url_suf }}"> <form method="post" enctype="multipart/form-data" action="/">
<input type="hidden" name="act" value="login" /> <input type="hidden" name="act" value="login" />
<input type="password" name="cppwd" /> <input type="password" name="cppwd" />
<input type="submit" value="Login" /> <input type="submit" value="Login" />

View File

@@ -804,6 +804,14 @@ function up2k_init(subtle) {
var mou_ikkai = false; var mou_ikkai = false;
if (st.busy.handshake.length > 0 &&
st.busy.handshake[0].busied < Date.now() - 30 * 1000
) {
console.log("retrying stuck handshake");
var t = st.busy.handshake.shift();
st.todo.handshake.unshift(t);
}
if (st.todo.handshake.length > 0 && if (st.todo.handshake.length > 0 &&
st.busy.handshake.length == 0 && ( st.busy.handshake.length == 0 && (
st.todo.handshake[0].t4 || ( st.todo.handshake[0].t4 || (
@@ -1019,11 +1027,27 @@ function up2k_init(subtle) {
// //
function exec_handshake() { function exec_handshake() {
var t = st.todo.handshake.shift(); var t = st.todo.handshake.shift(),
me = Date.now();
st.busy.handshake.push(t); st.busy.handshake.push(t);
t.busied = me;
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.onerror = function () {
if (t.busied != me) {
console.log('zombie handshake onerror,', t);
return;
}
console.log('handshake onerror, retrying');
st.busy.handshake.splice(st.busy.handshake.indexOf(t), 1);
st.todo.handshake.unshift(t);
};
xhr.onload = function (e) { xhr.onload = function (e) {
if (t.busied != me) {
console.log('zombie handshake onload,', t);
return;
}
if (xhr.status == 200) { if (xhr.status == 200) {
var response = JSON.parse(xhr.responseText); var response = JSON.parse(xhr.responseText);

View File

@@ -1,8 +1,19 @@
## [`minimal-up2k.html`](minimal-up2k.html) # example `.epilogue.html`
* save as `.epilogue.html` inside a folder to [simplify the ui](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png) save one of these as `.epilogue.html` inside a folder to customize it:
## [`browser.css`](browser.css) * [`minimal-up2k.html`](minimal-up2k.html) will [simplify the upload ui](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png)
* example for `--css-browser`
# example browser-css
point `--css-browser` to one of these by URL:
* [`browser.css`](browser.css) changes the background
* [`browser-icons.css`](browser-icons.css) adds filetype icons
# other stuff
## [`rclone.md`](rclone.md) ## [`rclone.md`](rclone.md)
* notes on using rclone as a fuse client/server * notes on using rclone as a fuse client/server

68
docs/browser-icons.css Normal file
View File

@@ -0,0 +1,68 @@
/* put filetype icons inline with text
#ggrid>a>span:before,
#ggrid>a>span.dir:before {
display: inline;
line-height: 0;
font-size: 1.7em;
margin: -.7em .1em -.5em -.6em;
}
*/
/* move folder icons top-left */
#ggrid>a>span.dir:before {
content: initial;
}
#ggrid>a[href$="/"]:before {
content: '📂';
display: block;
position: absolute;
margin: -.1em -.4em;
text-shadow: 0 0 .1em #000;
font-size: 2em;
}
/* put filetype icons top-left */
#ggrid>a:before {
display: block;
position: absolute;
margin: -.1em -.4em;
text-shadow: 0 0 .1em #000;
font-size: 2em;
}
/* video */
#ggrid>a:is(
[href$=".mkv"i],
[href$=".mp4"i],
[href$=".webm"i],
):before {
content: '📺';
}
/* audio */
#ggrid>a:is(
[href$=".mp3"i],
[href$=".ogg"i],
[href$=".opus"i],
[href$=".flac"i],
[href$=".m4a"i],
[href$=".aac"i],
):before {
content: '🎵';
}
/* image */
#ggrid>a:is(
[href$=".jpg"i],
[href$=".jpeg"i],
[href$=".png"i],
[href$=".gif"i],
[href$=".webp"i],
):before {
content: '🎨';
}

View File

@@ -1,5 +1,5 @@
html { html {
background: url('/wp/wallhaven-mdjrqy.jpg') center / cover no-repeat fixed; background: #333 url('/wp/wallhaven-mdjrqy.jpg') center / cover no-repeat fixed;
} }
#files th { #files th {
background: rgba(32, 32, 32, 0.9) !important; background: rgba(32, 32, 32, 0.9) !important;
@@ -12,7 +12,7 @@ html {
html.light { html.light {
background: url('/wp/wallhaven-dpxl6l.png') center / cover no-repeat fixed; background: #eee url('/wp/wallhaven-dpxl6l.png') center / cover no-repeat fixed;
} }
html.light #files th { html.light #files th {
background: rgba(255, 255, 255, 0.9) !important; background: rgba(255, 255, 255, 0.9) !important;

View File

@@ -86,6 +86,9 @@ var t=[]; var b=document.location.href.split('#')[0].slice(0, -1); document.quer
# get the size and video-id of all youtube vids in folder, assuming filename ends with -id.ext, and create a copyparty search query # get the size and video-id of all youtube vids in folder, assuming filename ends with -id.ext, and create a copyparty search query
find -maxdepth 1 -printf '%s %p\n' | sort -n | awk '!/-([0-9a-zA-Z_-]{11})\.(mkv|mp4|webm)$/{next} {sub(/\.[^\.]+$/,"");n=length($0);v=substr($0,n-10);print $1, v}' | tee /dev/stderr | awk 'BEGIN {p="("} {printf("%s name like *-%s.* ",p,$2);p="or"} END {print ")\n"}' | cat >&2 find -maxdepth 1 -printf '%s %p\n' | sort -n | awk '!/-([0-9a-zA-Z_-]{11})\.(mkv|mp4|webm)$/{next} {sub(/\.[^\.]+$/,"");n=length($0);v=substr($0,n-10);print $1, v}' | tee /dev/stderr | awk 'BEGIN {p="("} {printf("%s name like *-%s.* ",p,$2);p="or"} END {print ")\n"}' | cat >&2
# unique stacks in a stackdump
f=a; rm -rf stacks; mkdir stacks; grep -E '^#' $f | while IFS= read -r n; do awk -v n="$n" '!$0{o=0} o; $0==n{o=1}' <$f >stacks/f; h=$(sha1sum <stacks/f | cut -c-16); mv stacks/f stacks/$h-"$n"; done ; find stacks/ | sort | uniq -cw24
## ##
## sqlite3 stuff ## sqlite3 stuff

View File

@@ -208,7 +208,7 @@ find | grep -E '\.css$' | while IFS= read -r f; do
} }
!/\}$/ {printf "%s",$0;next} !/\}$/ {printf "%s",$0;next}
1 1
' <$f | gsed 's/;\}$/}/' >t ' <$f | sed 's/;\}$/}/' >t
tmv "$f" tmv "$f"
done done
find | grep -E '\.(js|html)$' | while IFS= read -r f; do find | grep -E '\.(js|html)$' | while IFS= read -r f; do
@@ -223,7 +223,7 @@ command -v pigz &&
pk='gzip' pk='gzip'
echo "$pk" echo "$pk"
find | grep -E '\.(js|css)$' | while IFS= read -r f; do find | grep -E '\.(js|css)$' | grep -vF /deps/ | while IFS= read -r f; do
echo -n . echo -n .
$pk "$f" $pk "$f"
done done

View File

@@ -28,6 +28,7 @@ class Cfg(Namespace):
a=a, a=a,
v=v, v=v,
c=c, c=c,
rproxy=0,
ed=False, ed=False,
no_zip=False, no_zip=False,
no_scandir=False, no_scandir=False,
@@ -39,6 +40,7 @@ class Cfg(Namespace):
mte="a", mte="a",
hist=None, hist=None,
no_hash=False, no_hash=False,
css_browser=None,
**{k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr".split()} **{k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr".split()}
) )

View File

@@ -18,7 +18,14 @@ from copyparty import util
class Cfg(Namespace): class Cfg(Namespace):
def __init__(self, a=[], v=[], c=None): def __init__(self, a=[], v=[], c=None):
ex = {k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr".split()} ex = {k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr".split()}
ex2 = {"mtp": [], "mte": "a", "hist": None, "no_hash": False} ex2 = {
"mtp": [],
"mte": "a",
"hist": None,
"no_hash": False,
"css_browser": None,
"rproxy": 0,
}
ex.update(ex2) ex.update(ex2)
super(Cfg, self).__init__(a=a, v=v, c=c, **ex) super(Cfg, self).__init__(a=a, v=v, c=c, **ex)