Compare commits

...

26 Commits

Author SHA1 Message Date
ed
734e9d3874 v1.0.8 2021-10-04 22:50:06 +02:00
ed
bd5cfc2f1b fix filedrop with fallback hashers 2021-10-04 22:37:35 +02:00
ed
89f88ee78c more obvious dropzones 2021-10-04 22:34:05 +02:00
ed
b2ae14695a show multiple filesearch hits 2021-10-04 21:53:28 +02:00
ed
19d86b44d9 less verbose debug toasts 2021-10-04 21:35:25 +02:00
ed
85be62e38b audioplayer: minute-mark text on progressbar 2021-10-04 21:26:26 +02:00
ed
80f3d90200 better focus outlines 2021-10-04 20:54:07 +02:00
ed
0249fa6e75 fix tests 2021-10-03 19:59:47 +02:00
ed
2d0696e048 allow appending mte in volflags 2021-10-03 19:35:51 +02:00
ed
ff32ec515e add mtp plugin cksum.py 2021-10-03 19:35:20 +02:00
ed
a6935b0293 allow uploading empty files 2021-10-02 23:34:12 +02:00
ed
63eb08ba9f u2cli: nobody asked for python2.6 support so here you go w 2021-10-02 00:36:41 +02:00
ed
e5b67d2b3a u2cli: add eta, errorhandling, better windows support 2021-10-01 22:31:24 +02:00
ed
9e10af6885 make the 404/403 vagueness optional 2021-10-01 19:51:51 +02:00
ed
42bc9115d2 hide logues in search results 2021-10-01 19:33:49 +02:00
ed
0a569ce413 readme: add bash client examples 2021-10-01 19:27:21 +02:00
ed
9a16639a61 u2cli: add webm 2021-10-01 02:25:22 +02:00
ed
57953c68c6 u2cli: add vt100 status panel 2021-10-01 02:10:03 +02:00
ed
088d08963f u2cli: add multithreading 2021-10-01 00:33:45 +02:00
ed
7bc8196821 u2cli: add file-search 2021-09-30 19:36:47 +02:00
ed
7715299dd3 dont show entire web pages in toasts 2021-09-30 19:35:56 +02:00
ed
b8ac9b7994 u2cli: connection reuse for lower latency 2021-09-28 00:14:45 +02:00
ed
98e7d8f728 more docstrings 2021-09-27 23:52:36 +02:00
ed
e7fd871ffe add up2k.py 2021-09-27 23:28:34 +02:00
ed
14aab62f32 fix current-directory hilight 2021-09-27 20:55:05 +02:00
ed
cb81fe962c v1.0.7 2021-09-26 20:15:21 +02:00
21 changed files with 1048 additions and 116 deletions

4
.gitignore vendored
View File

@@ -20,3 +20,7 @@ sfx/
# derived
copyparty/web/deps/
srv/
# state/logs
up.*.txt
.hist/

View File

@@ -769,6 +769,14 @@ interact with copyparty using non-browser clients
* `chunk(){ curl -b cppwd=wark -T- http://127.0.0.1:3923/;}`
`chunk <movie.mkv`
* bash: when curl and wget is not available or too boring
* `(printf 'PUT /junk?pw=wark HTTP/1.1\r\n\r\n'; cat movie.mkv) | nc 127.0.0.1 3923`
* `(printf 'PUT / HTTP/1.1\r\n\r\n'; cat movie.mkv) >/dev/tcp/127.0.0.1/3923`
* python: [up2k.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) is a command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm)
* file uploads, file-search, autoresume of aborted/broken uploads
* see [./bin/README.md#up2kpy](bin/README.md#up2kpy)
* FUSE: mount a copyparty server as a local filesystem
* cross-platform python client available in [./bin/](bin/)
* [rclone](https://rclone.org/) as client can give ~5x performance, see [./docs/rclone.md](docs/rclone.md)

View File

@@ -1,3 +1,11 @@
# [`up2k.py`](up2k.py)
* command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm)
* file uploads, file-search, autoresume of aborted/broken uploads
* faster than browsers
* early beta, if something breaks just restart it
# [`copyparty-fuse.py`](copyparty-fuse.py)
* mount a copyparty server as a local filesystem (read-only)
* **supports Windows!** -- expect `194 MiB/s` sequential read
@@ -47,6 +55,7 @@ you could replace winfsp with [dokan](https://github.com/dokan-dev/dokany/releas
* copyparty can Popen programs like these during file indexing to collect additional metadata
# [`dbtool.py`](dbtool.py)
upgrade utility which can show db info and help transfer data between databases, for example when a new version of copyparty is incompatible with the old DB and automatically rebuilds the DB from scratch, but you have some really expensive `-mtp` parsers and want to copy over the tags from the old db
@@ -63,6 +72,7 @@ cd /mnt/nas/music/.hist
```
# [`prisonparty.sh`](prisonparty.sh)
* run copyparty in a chroot, preventing any accidental file access
* creates bindmounts for /bin, /lib, and so on, see `sysdirs=`

View File

@@ -10,6 +10,7 @@ some of these rely on libraries which are not MIT-compatible
these do not have any problematic dependencies:
* [cksum.py](./cksum.py) computes various checksums
* [exe.py](./exe.py) grabs metadata from .exe and .dll files (example for retrieving multiple tags with one parser)
* [wget.py](./wget.py) lets you download files by POSTing URLs to copyparty

89
bin/mtag/cksum.py Executable file
View File

@@ -0,0 +1,89 @@
#!/usr/bin/env python3
import sys
import json
import zlib
import struct
import base64
import hashlib
try:
from copyparty.util import fsenc
except:
def fsenc(p):
return p
"""
calculates various checksums for uploads,
usage: -mtp crc32,md5,sha1,sha256b=bin/mtag/cksum.py
"""
def main():
config = "crc32 md5 md5b sha1 sha1b sha256 sha256b sha512/240 sha512b/240"
# b suffix = base64 encoded
# slash = truncate to n bits
known = {
"md5": hashlib.md5,
"sha1": hashlib.sha1,
"sha256": hashlib.sha256,
"sha512": hashlib.sha512,
}
config = config.split()
hashers = {
k: v()
for k, v in known.items()
if k in [x.split("/")[0].rstrip("b") for x in known]
}
crc32 = 0 if "crc32" in config else None
with open(fsenc(sys.argv[1]), "rb", 512 * 1024) as f:
while True:
buf = f.read(64 * 1024)
if not buf:
break
for x in hashers.values():
x.update(buf)
if crc32 is not None:
crc32 = zlib.crc32(buf, crc32)
ret = {}
for s in config:
alg = s.split("/")[0]
b64 = alg.endswith("b")
alg = alg.rstrip("b")
if alg in hashers:
v = hashers[alg].digest()
elif alg == "crc32":
v = crc32
if v < 0:
v &= 2 ** 32 - 1
v = struct.pack(">L", v)
else:
raise Exception("what is {}".format(s))
if "/" in s:
v = v[: int(int(s.split("/")[1]) / 8)]
if b64:
v = base64.b64encode(v).decode("ascii").rstrip("=")
else:
try:
v = v.hex()
except:
import binascii
v = binascii.hexlify(v)
ret[s] = v
print(json.dumps(ret, indent=4))
if __name__ == "__main__":
main()

723
bin/up2k.py Executable file
View File

@@ -0,0 +1,723 @@
#!/usr/bin/env python3
from __future__ import print_function, unicode_literals
"""
up2k.py: upload to copyparty
2021-10-04, v0.7, ed <irc.rizon.net>, MIT-Licensed
https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py
- dependencies: requests
- supports python 2.6, 2.7, and 3.3 through 3.10
- almost zero error-handling
- but if something breaks just try again and it'll autoresume
"""
import os
import sys
import stat
import math
import time
import atexit
import signal
import base64
import hashlib
import argparse
import platform
import threading
import requests
import datetime
# from copyparty/__init__.py
PY2 = sys.version_info[0] == 2
if PY2:
from Queue import Queue
sys.dont_write_bytecode = True
bytes = str
else:
from queue import Queue
unicode = str
VT100 = platform.system() != "Windows"
req_ses = requests.Session()
class File(object):
"""an up2k upload task; represents a single file"""
def __init__(self, top, rel, size, lmod):
self.top = top # type: bytes
self.rel = rel.replace(b"\\", b"/") # type: bytes
self.size = size # type: int
self.lmod = lmod # type: float
self.abs = os.path.join(top, rel) # type: bytes
self.name = self.rel.split(b"/")[-1].decode("utf-8", "replace") # type: str
# set by get_hashlist
self.cids = [] # type: list[tuple[str, int, int]] # [ hash, ofs, sz ]
self.kchunks = {} # type: dict[str, tuple[int, int]] # hash: [ ofs, sz ]
# set by handshake
self.ucids = [] # type: list[str] # chunks which need to be uploaded
self.wark = None # type: str
self.url = None # type: str
# set by upload
self.up_b = 0 # type: int
self.up_c = 0 # type: int
# m = "size({}) lmod({}) top({}) rel({}) abs({}) name({})\n"
# eprint(m.format(self.size, self.lmod, self.top, self.rel, self.abs, self.name))
class FileSlice(object):
"""file-like object providing a fixed window into a file"""
def __init__(self, file, cid):
# type: (File, str) -> FileSlice
self.car, self.len = file.kchunks[cid]
self.cdr = self.car + self.len
self.ofs = 0 # type: int
self.f = open(file.abs, "rb", 512 * 1024)
self.f.seek(self.car)
# https://stackoverflow.com/questions/4359495/what-is-exactly-a-file-like-object-in-python
# IOBase, RawIOBase, BufferedIOBase
funs = "close closed __enter__ __exit__ __iter__ isatty __next__ readable seekable writable"
try:
for fun in funs.split():
setattr(self, fun, getattr(self.f, fun))
except:
pass # py27 probably
def tell(self):
return self.ofs
def seek(self, ofs, wh=0):
if wh == 1:
ofs = self.ofs + ofs
elif wh == 2:
ofs = self.len + ofs # provided ofs is negative
if ofs < 0:
ofs = 0
elif ofs >= self.len:
ofs = self.len - 1
self.ofs = ofs
self.f.seek(self.car + ofs)
def read(self, sz):
sz = min(sz, self.len - self.ofs)
ret = self.f.read(sz)
self.ofs += len(ret)
return ret
def eprint(*a, **ka):
ka["file"] = sys.stderr
ka["end"] = ""
if not PY2:
ka["flush"] = True
print(*a, **ka)
if PY2:
sys.stderr.flush()
def termsize():
import os
env = os.environ
def ioctl_GWINSZ(fd):
try:
import fcntl, termios, struct, os
cr = struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234"))
except:
return
return cr
cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
if not cr:
try:
fd = os.open(os.ctermid(), os.O_RDONLY)
cr = ioctl_GWINSZ(fd)
os.close(fd)
except:
pass
if not cr:
try:
cr = (env["LINES"], env["COLUMNS"])
except:
cr = (25, 80)
return int(cr[1]), int(cr[0])
class CTermsize(object):
def __init__(self):
self.ev = False
self.margin = None
self.g = None
self.w, self.h = termsize()
try:
signal.signal(signal.SIGWINCH, self.ev_sig)
except:
return
thr = threading.Thread(target=self.worker)
thr.daemon = True
thr.start()
def worker(self):
while True:
time.sleep(0.5)
if not self.ev:
continue
self.ev = False
self.w, self.h = termsize()
if self.margin is not None:
self.scroll_region(self.margin)
def ev_sig(self, *a, **ka):
self.ev = True
def scroll_region(self, margin):
self.margin = margin
if margin is None:
self.g = None
eprint("\033[s\033[r\033[u")
else:
self.g = 1 + self.h - margin
m = "{0}\033[{1}A".format("\n" * margin, margin)
eprint("{0}\033[s\033[1;{1}r\033[u".format(m, self.g - 1))
ss = CTermsize()
def statdir(top):
"""non-recursive listing of directory contents, along with stat() info"""
if hasattr(os, "scandir"):
with os.scandir(top) as dh:
for fh in dh:
yield [os.path.join(top, fh.name), fh.stat()]
else:
for name in os.listdir(top):
abspath = os.path.join(top, name)
yield [abspath, os.stat(abspath)]
def walkdir(top):
"""recursive statdir"""
for ap, inf in sorted(statdir(top)):
if stat.S_ISDIR(inf.st_mode):
for x in walkdir(ap):
yield x
else:
yield ap, inf
def walkdirs(tops):
"""recursive statdir for a list of tops, yields [top, relpath, stat]"""
for top in tops:
if os.path.isdir(top):
for ap, inf in walkdir(top):
yield top, ap[len(top) + 1 :], inf
else:
sep = "{0}".format(os.sep).encode("ascii")
d, n = top.rsplit(sep, 1)
yield d, n, os.stat(top)
# from copyparty/util.py
def humansize(sz, terse=False):
"""picks a sensible unit for the given extent"""
for unit in ["B", "KiB", "MiB", "GiB", "TiB"]:
if sz < 1024:
break
sz /= 1024.0
ret = " ".join([str(sz)[:4].rstrip("."), unit])
if not terse:
return ret
return ret.replace("iB", "").replace(" ", "")
# from copyparty/up2k.py
def up2k_chunksize(filesize):
"""gives The correct chunksize for up2k hashing"""
chunksize = 1024 * 1024
stepsize = 512 * 1024
while True:
for mul in [1, 2]:
nchunks = math.ceil(filesize * 1.0 / chunksize)
if nchunks <= 256 or chunksize >= 32 * 1024 * 1024:
return chunksize
chunksize += stepsize
stepsize *= mul
# mostly from copyparty/up2k.py
def get_hashlist(file, pcb):
# type: (File, any) -> None
"""generates the up2k hashlist from file contents, inserts it into `file`"""
chunk_sz = up2k_chunksize(file.size)
file_rem = file.size
file_ofs = 0
ret = []
with open(file.abs, "rb", 512 * 1024) as f:
while file_rem > 0:
hashobj = hashlib.sha512()
chunk_sz = chunk_rem = min(chunk_sz, file_rem)
while chunk_rem > 0:
buf = f.read(min(chunk_rem, 64 * 1024))
if not buf:
raise Exception("EOF at " + str(f.tell()))
hashobj.update(buf)
chunk_rem -= len(buf)
digest = hashobj.digest()[:33]
digest = base64.urlsafe_b64encode(digest).decode("utf-8")
ret.append([digest, file_ofs, chunk_sz])
file_ofs += chunk_sz
file_rem -= chunk_sz
if pcb:
pcb(file, file_ofs)
file.cids = ret
file.kchunks = {}
for k, v1, v2 in ret:
file.kchunks[k] = [v1, v2]
def handshake(req_ses, url, file, pw, search):
# type: (requests.Session, str, File, any, bool) -> List[str]
"""
performs a handshake with the server; reply is:
if search, a list of search results
otherwise, a list of chunks to upload
"""
req = {
"hash": [x[0] for x in file.cids],
"name": file.name,
"lmod": file.lmod,
"size": file.size,
}
if search:
req["srch"] = 1
headers = {"Content-Type": "text/plain"} # wtf ed
if pw:
headers["Cookie"] = "=".join(["cppwd", pw])
if file.url:
url = file.url
elif b"/" in file.rel:
url += file.rel.rsplit(b"/", 1)[0].decode("utf-8", "replace")
while True:
try:
r = req_ses.post(url, headers=headers, json=req)
break
except:
eprint("handshake failed, retry...\n")
time.sleep(1)
try:
r = r.json()
except:
raise Exception(r.text)
if search:
return r["hits"]
try:
pre, url = url.split("://")
pre += "://"
except:
pre = ""
file.url = pre + url.split("/")[0] + r["purl"]
file.name = r["name"]
file.wark = r["wark"]
return r["hash"]
def upload(req_ses, file, cid, pw):
# type: (requests.Session, File, str, any) -> None
"""upload one specific chunk, `cid` (a chunk-hash)"""
headers = {
"X-Up2k-Hash": cid,
"X-Up2k-Wark": file.wark,
"Content-Type": "application/octet-stream",
}
if pw:
headers["Cookie"] = "=".join(["cppwd", pw])
f = FileSlice(file, cid)
try:
r = req_ses.post(file.url, headers=headers, data=f)
if not r:
raise Exception(repr(r))
_ = r.content
finally:
f.f.close()
class Daemon(threading.Thread):
def __init__(self, *a, **ka):
threading.Thread.__init__(self, *a, **ka)
self.daemon = True
class Ctl(object):
"""
this will be the coordinator which runs everything in parallel
(hashing, handshakes, uploads) but right now it's p dumb
"""
def __init__(self, ar):
self.ar = ar
ar.files = [
os.path.abspath(os.path.realpath(x.encode("utf-8"))) for x in ar.files
]
ar.url = ar.url.rstrip("/") + "/"
if "://" not in ar.url:
ar.url = "http://" + ar.url
eprint("\nscanning {0} locations\n".format(len(ar.files)))
nfiles = 0
nbytes = 0
for _, _, inf in walkdirs(ar.files):
nfiles += 1
nbytes += inf.st_size
eprint("found {0} files, {1}\n\n".format(nfiles, humansize(nbytes)))
self.nfiles = nfiles
self.nbytes = nbytes
if ar.td:
req_ses.verify = False
if ar.te:
req_ses.verify = ar.te
self.filegen = walkdirs(ar.files)
if ar.safe:
self.safe()
else:
self.fancy()
def safe(self):
"""minimal basic slow boring fallback codepath"""
search = self.ar.s
for nf, (top, rel, inf) in enumerate(self.filegen):
file = File(top, rel, inf.st_size, inf.st_mtime)
upath = file.abs.decode("utf-8", "replace")
print("{0} {1}\n hash...".format(self.nfiles - nf, upath))
get_hashlist(file, None)
while True:
print(" hs...")
hs = handshake(req_ses, self.ar.url, file, self.ar.a, search)
if search:
if hs:
for hit in hs:
print(" found: {0}{1}".format(self.ar.url, hit["rp"]))
else:
print(" NOT found")
break
file.ucids = hs
if not hs:
break
print("{0} {1}".format(self.nfiles - nf, upath))
ncs = len(hs)
for nc, cid in enumerate(hs):
print(" {0} up {1}".format(ncs - nc, cid))
upload(req_ses, file, cid, self.ar.a)
print(" ok!")
def fancy(self):
self.hash_f = 0
self.hash_c = 0
self.hash_b = 0
self.up_f = 0
self.up_c = 0
self.up_b = 0
self.up_br = 0
self.hasher_busy = 1
self.handshaker_busy = 0
self.uploader_busy = 0
self.t0 = time.time()
self.t0_up = None
self.spd = None
self.mutex = threading.Lock()
self.q_handshake = Queue() # type: Queue[File]
self.q_recheck = Queue() # type: Queue[File] # partial upload exists [...]
self.q_upload = Queue() # type: Queue[tuple[File, str]]
self.st_hash = [None, "(idle, starting...)"] # type: tuple[File, int]
self.st_up = [None, "(idle, starting...)"] # type: tuple[File, int]
if VT100:
atexit.register(self.cleanup_vt100)
ss.scroll_region(3)
Daemon(target=self.hasher).start()
for _ in range(self.ar.j):
Daemon(target=self.handshaker).start()
Daemon(target=self.uploader).start()
idles = 0
while idles < 3:
time.sleep(0.07)
with self.mutex:
if (
self.q_handshake.empty()
and self.q_upload.empty()
and not self.hasher_busy
and not self.handshaker_busy
and not self.uploader_busy
):
idles += 1
else:
idles = 0
if VT100:
maxlen = ss.w - len(str(self.nfiles)) - 14
txt = "\033[s\033[{0}H".format(ss.g)
for y, k, st, f in [
[0, "hash", self.st_hash, self.hash_f],
[1, "send", self.st_up, self.up_f],
]:
txt += "\033[{0}H{1}:".format(ss.g + y, k)
file, arg = st
if not file:
txt += " {0}\033[K".format(arg)
else:
if y:
p = 100 * file.up_b / file.size
else:
p = 100 * arg / file.size
name = file.abs.decode("utf-8", "replace")[-maxlen:]
if "/" in name:
name = "\033[36m{0}\033[0m/{1}".format(*name.rsplit("/", 1))
m = "{0:6.1f}% {1} {2}\033[K"
txt += m.format(p, self.nfiles - f, name)
txt += "\033[{0}H ".format(ss.g + 2)
else:
txt = " "
if not self.up_br:
spd = self.hash_b / (time.time() - self.t0)
eta = (self.nbytes - self.hash_b) / (spd + 1)
else:
spd = self.up_br / (time.time() - self.t0_up)
spd = self.spd = (self.spd or spd) * 0.9 + spd * 0.1
eta = (self.nbytes - self.up_b) / (spd + 1)
spd = humansize(spd)
eta = str(datetime.timedelta(seconds=int(eta)))
left = humansize(self.nbytes - self.up_b)
tail = "\033[K\033[u" if VT100 else "\r"
m = "eta: {0} @ {1}/s, {2} left".format(eta, spd, left)
eprint(txt + "\033]0;{0}\033\\\r{1}{2}".format(m, m, tail))
def cleanup_vt100(self):
ss.scroll_region(None)
eprint("\033[J\033]0;\033\\")
def cb_hasher(self, file, ofs):
self.st_hash = [file, ofs]
def hasher(self):
for top, rel, inf in self.filegen:
file = File(top, rel, inf.st_size, inf.st_mtime)
while True:
with self.mutex:
if (
self.hash_b - self.up_b < 1024 * 1024 * 128
and self.hash_c - self.up_c < 64
and (
not self.ar.nh
or (
self.q_upload.empty()
and self.q_handshake.empty()
and not self.uploader_busy
)
)
):
break
time.sleep(0.05)
get_hashlist(file, self.cb_hasher)
with self.mutex:
self.hash_f += 1
self.hash_c += len(file.cids)
self.hash_b += file.size
self.q_handshake.put(file)
self.hasher_busy = 0
self.st_hash = [None, "(finished)"]
def handshaker(self):
search = self.ar.s
q = self.q_handshake
while True:
file = q.get()
if not file:
if q == self.q_handshake:
q = self.q_recheck
q.put(None)
continue
self.q_upload.put(None)
break
with self.mutex:
self.handshaker_busy += 1
upath = file.abs.decode("utf-8", "replace")
try:
hs = handshake(req_ses, self.ar.url, file, self.ar.a, search)
except Exception as ex:
if q == self.q_handshake and "<pre>partial upload exists" in str(ex):
self.q_recheck.put(file)
hs = []
else:
raise
if search:
if hs:
for hit in hs:
m = "found: {0}\n {1}{2}\n"
print(m.format(upath, self.ar.url, hit["rp"]), end="")
else:
print("NOT found: {0}\n".format(upath), end="")
with self.mutex:
self.up_f += 1
self.up_c += len(file.cids)
self.up_b += file.size
self.handshaker_busy -= 1
continue
with self.mutex:
if not hs:
# all chunks done
self.up_f += 1
self.up_c += len(file.cids) - file.up_c
self.up_b += file.size - file.up_b
if hs and file.up_c:
# some chunks failed
self.up_c -= len(hs)
file.up_c -= len(hs)
for cid in hs:
sz = file.kchunks[cid][1]
self.up_b -= sz
file.up_b -= sz
file.ucids = hs
self.handshaker_busy -= 1
if not hs:
print("uploaded {0}".format(upath))
for cid in hs:
self.q_upload.put([file, cid])
def uploader(self):
while True:
task = self.q_upload.get()
if not task:
self.st_up = [None, "(finished)"]
break
with self.mutex:
self.uploader_busy += 1
self.t0_up = self.t0_up or time.time()
file, cid = task
try:
upload(req_ses, file, cid, self.ar.a)
except:
eprint("upload failed, retry...\n")
pass # handshake will fix it
with self.mutex:
sz = file.kchunks[cid][1]
file.ucids = [x for x in file.ucids if x != cid]
if not file.ucids:
self.q_handshake.put(file)
self.st_up = [file, cid]
file.up_b += sz
self.up_b += sz
self.up_br += sz
file.up_c += 1
self.up_c += 1
self.uploader_busy -= 1
def main():
time.strptime("19970815", "%Y%m%d") # python#7980
if not VT100:
os.system("rem") # enables colors
# fmt: off
ap = app = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
ap.add_argument("url", type=unicode, help="server url, including destination folder")
ap.add_argument("files", type=unicode, nargs="+", help="files and/or folders to process")
ap.add_argument("-a", metavar="PASSWORD", help="password")
ap.add_argument("-s", action="store_true", help="file-search (disables upload)")
ap = app.add_argument_group("performance tweaks")
ap.add_argument("-j", type=int, metavar="THREADS", default=4, help="parallel connections")
ap.add_argument("-nh", action="store_true", help="disable hashing while uploading")
ap.add_argument("--safe", action="store_true", help="use simple fallback approach")
ap = app.add_argument_group("tls")
ap.add_argument("-te", metavar="PEM_FILE", help="certificate to expect/verify")
ap.add_argument("-td", action="store_true", help="disable certificate check")
# fmt: on
Ctl(app.parse_args())
if __name__ == "__main__":
main()

0
bin/up2k.sh Executable file → Normal file
View File

View File

@@ -378,6 +378,7 @@ def run_argparse(argv, formatter):
ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to make something a dotfile")
ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings")
ap2.add_argument("--no-readme", action="store_true", help="disable rendering readme.md into directory listings")
ap2.add_argument("--vague-403", action="store_true", help="send 404 instead of 403 (security through ambiguity, very enterprise)")
ap2 = ap.add_argument_group('logging options')
ap2.add_argument("-q", action="store_true", help="quiet")

View File

@@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (1, 0, 6)
VERSION = (1, 0, 8)
CODENAME = "sufficient"
BUILD_DT = (2021, 9, 26)
BUILD_DT = (2021, 10, 4)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View File

@@ -356,7 +356,7 @@ class VFS(object):
if not dbv:
return self, vrem
vrem = [self.vpath[len(dbv.vpath) + 1 :], vrem]
vrem = [self.vpath[len(dbv.vpath) :].lstrip("/"), vrem]
vrem = "/".join([x for x in vrem if x])
return dbv, vrem
@@ -880,6 +880,10 @@ class AuthSrv(object):
# default tag cfgs if unset
if "mte" not in vol.flags:
vol.flags["mte"] = self.args.mte
elif vol.flags["mte"].startswith("+"):
vol.flags["mte"] = ",".join(
x for x in [self.args.mte, vol.flags["mte"][1:]] if x
)
if "mth" not in vol.flags:
vol.flags["mth"] = self.args.mth

View File

@@ -389,7 +389,7 @@ class HttpCli(object):
if not self.can_read and not self.can_write and not self.can_get:
if self.vpath:
self.log("inaccessible: [{}]".format(self.vpath))
return self.tx_404()
return self.tx_404(True)
self.uparam["h"] = False
@@ -1565,7 +1565,7 @@ class HttpCli(object):
if not self.can_write:
if "edit" in self.uparam or "edit2" in self.uparam:
return self.tx_404()
return self.tx_404(True)
tpl = "mde" if "edit2" in self.uparam else "md"
html_path = os.path.join(E.mod, "web", "{}.html".format(tpl))
@@ -1667,8 +1667,14 @@ class HttpCli(object):
self.reply(html.encode("utf-8"))
return True
def tx_404(self):
m = '<h1>404 not found &nbsp;┐( ´ -`)┌</h1><p>or maybe you don\'t have access -- try logging in or <a href="/?h">go home</a></p>'
def tx_404(self, is_403=False):
if self.args.vague_403:
m = '<h1>404 not found &nbsp;┐( ´ -`)┌</h1><p>or maybe you don\'t have access -- try logging in or <a href="/?h">go home</a></p>'
elif is_403:
m = '<h1>403 forbiddena &nbsp;~┻━┻</h1><p>you\'ll have to log in or <a href="/?h">go home</a></p>'
else:
m = '<h1>404 not found &nbsp;┐( ´ -`)┌</h1><p><a href="/?h">go home</a></p>'
html = self.j2("splash", this=self, qvpath=quotep(self.vpath), msg=m)
self.reply(html.encode("utf-8"), status=404)
return True
@@ -1895,7 +1901,7 @@ class HttpCli(object):
return self.tx_file(abspath)
elif is_dir and not self.can_read and not self.can_write:
return self.tx_404()
return self.tx_404(True)
srv_info = []
@@ -2000,7 +2006,7 @@ class HttpCli(object):
return True
if not stat.S_ISDIR(st.st_mode):
return self.tx_404()
return self.tx_404(True)
if "zip" in self.uparam or "tar" in self.uparam:
raise Pebkac(403)

View File

@@ -6,6 +6,7 @@ import os
import time
import threading
from datetime import datetime
from operator import itemgetter
from .__init__ import ANYWIN, unicode
from .util import absreal, s3dec, Pebkac, min_ex, gen_filekey, quotep
@@ -292,9 +293,13 @@ class U2idx(object):
# undupe hits from multiple metadata keys
if len(ret) > 1:
ret = [ret[0]] + [
y for x, y in zip(ret[:-1], ret[1:]) if x["rp"] != y["rp"]
y
for x, y in zip(ret[:-1], ret[1:])
if x["rp"].split("?")[0] != y["rp"].split("?")[0]
]
ret.sort(key=itemgetter("rp"))
return ret, list(taglist.keys())
def terminator(self, identifier, done_flag):

View File

@@ -1362,53 +1362,56 @@ class Up2k(object):
# del self.registry[ptop][wark]
return ret, dst
# windows cant rename open files
if not ANYWIN or src == dst:
self.finish_upload(ptop, wark)
# windows cant rename open files
if not ANYWIN or src == dst:
self._finish_upload(ptop, wark)
return ret, dst
def finish_upload(self, ptop, wark):
with self.mutex:
try:
job = self.registry[ptop][wark]
pdir = os.path.join(job["ptop"], job["prel"])
src = os.path.join(pdir, job["tnam"])
dst = os.path.join(pdir, job["name"])
except Exception as ex:
return "finish_upload, wark, " + repr(ex)
self._finish_upload(ptop, wark)
# self.log("--- " + wark + " " + dst + " finish_upload atomic " + dst, 4)
atomic_move(src, dst)
def _finish_upload(self, ptop, wark):
try:
job = self.registry[ptop][wark]
pdir = os.path.join(job["ptop"], job["prel"])
src = os.path.join(pdir, job["tnam"])
dst = os.path.join(pdir, job["name"])
except Exception as ex:
return "finish_upload, wark, " + repr(ex)
if ANYWIN:
a = [dst, job["size"], (int(time.time()), int(job["lmod"]))]
self.lastmod_q.put(a)
# self.log("--- " + wark + " " + dst + " finish_upload atomic " + dst, 4)
atomic_move(src, dst)
a = [job[x] for x in "ptop wark prel name lmod size addr".split()]
a += [job.get("at") or time.time()]
if self.idx_wark(*a):
# self.log("pop " + wark + " " + dst + " finish_upload idx_wark", 4)
del self.registry[ptop][wark]
# in-memory registry is reserved for unfinished uploads
if ANYWIN:
a = [dst, job["size"], (int(time.time()), int(job["lmod"]))]
self.lastmod_q.put(a)
dupes = self.dupesched.pop(dst, [])
if not dupes:
return
a = [job[x] for x in "ptop wark prel name lmod size addr".split()]
a += [job.get("at") or time.time()]
if self.idx_wark(*a):
# self.log("pop " + wark + " " + dst + " finish_upload idx_wark", 4)
del self.registry[ptop][wark]
# in-memory registry is reserved for unfinished uploads
cur = self.cur.get(ptop)
for rd, fn in dupes:
d2 = os.path.join(ptop, rd, fn)
if os.path.exists(d2):
continue
dupes = self.dupesched.pop(dst, [])
if not dupes:
return
self._symlink(dst, d2)
if cur:
self.db_rm(cur, rd, fn)
self.db_add(cur, wark, rd, fn, *a[-4:])
cur = self.cur.get(ptop)
for rd, fn in dupes:
d2 = os.path.join(ptop, rd, fn)
if os.path.exists(d2):
continue
self._symlink(dst, d2)
if cur:
cur.connection.commit()
self.db_rm(cur, rd, fn)
self.db_add(cur, wark, rd, fn, *a[-4:])
if cur:
cur.connection.commit()
def idx_wark(self, ptop, wark, rd, fn, lmod, sz, ip, at):
cur = self.cur.get(ptop)
@@ -1768,7 +1771,13 @@ class Up2k(object):
except:
cj["lmod"] = int(time.time())
wark = up2k_wark_from_hashlist(self.salt, cj["size"], cj["hash"])
if cj["hash"]:
wark = up2k_wark_from_hashlist(self.salt, cj["size"], cj["hash"])
else:
wark = up2k_wark_from_metadata(
self.salt, cj["size"], cj["lmod"], cj["prel"], cj["name"]
)
return wark
def _hashlist_from_file(self, path):
@@ -1811,6 +1820,8 @@ class Up2k(object):
if self.args.nw:
job["tnam"] = tnam
if not job["hash"]:
del self.registry[job["ptop"]][job["wark"]]
return
suffix = ".{:.6f}-{}".format(job["t0"], job["addr"])
@@ -1827,8 +1838,12 @@ class Up2k(object):
except:
self.log("could not sparse [{}]".format(fp), 3)
f.seek(job["size"] - 1)
f.write(b"e")
if job["hash"]:
f.seek(job["size"] - 1)
f.write(b"e")
if not job["hash"]:
self._finish_upload(job["ptop"], job["wark"])
def _lastmodder(self):
while True:

View File

@@ -165,6 +165,7 @@ a, #files tbody div a:last-child {
.logue {
padding: .2em 1.5em;
}
.logue.hidden,
.logue:empty {
display: none;
}
@@ -1602,7 +1603,7 @@ html.light #bbox-overlay figcaption a {
border-radius: .5em;
border-width: 1vw;
color: #fff;
transition: all 0.2s;
transition: all 0.12s;
}
#drops .dropdesc.hl.ok {
border-color: #fff;
@@ -1623,6 +1624,16 @@ html.light #bbox-overlay figcaption a {
vertical-align: middle;
text-align: center;
}
#drops .dropdesc>div>div {
position: absolute;
top: 40%;
top: calc(50% - .5em);
left: -.8em;
}
#drops .dropdesc>div>div+div {
left: auto;
right: -.8em;
}
#drops .dropzone {
z-index: 80386;
height: 50%;

View File

@@ -133,8 +133,8 @@ ebi('op_up2k').innerHTML = (
var o = mknod('div');
o.innerHTML = (
'<div id="drops">\n' +
' <div class="dropdesc" id="up_zd"><div>🚀 Upload<br /><span></span></div></div>\n' +
' <div class="dropdesc" id="srch_zd"><div>🔎 Search<br /><span></span></div></div>\n' +
' <div class="dropdesc" id="up_zd"><div>🚀 Upload<br /><span></span><div>🚀</div><div>🚀</div></div></div>\n' +
' <div class="dropdesc" id="srch_zd"><div>🔎 Search<br /><span></span><div>🔎</div><div>🔎</div></div></div>\n' +
' <div class="dropzone" id="up_dz" v="up_zd"></div>\n' +
' <div class="dropzone" id="srch_dz" v="srch_zd"></div>\n' +
'</div>'
@@ -731,6 +731,12 @@ var pbar = (function () {
for (var p = 1, mins = adur / 60; p <= mins; p++)
pctx.fillRect(Math.floor(sm * p * 60), 0, 2, pc.h);
pctx.font = '.5em sans-serif';
pctx.fillStyle = light ? 'rgba(0,64,0,0.9)' : 'rgba(192,255,96,1)';
for (var p = 1, mins = adur / 60; p <= mins; p++) {
pctx.fillText(p, Math.floor(sm * p * 60 + 3), pc.h / 3);
}
pctx.fillStyle = light ? 'rgba(0,0,0,1)' : 'rgba(255,255,255,1)';
for (var p = 1, mins = adur / 600; p <= mins; p++)
pctx.fillRect(Math.floor(sm * p * 600), 0, 2, pc.h);
@@ -1347,7 +1353,7 @@ function play(tid, is_ev, seek, call_depth) {
mp.au = mp.au_ogvjs = new OGVPlayer();
}
catch (ex) {
return toast.err(30, 'your browser cannot play ogg/vorbis/opus\n\n' + ex +
return toast.err(30, 'your browser cannot play ogg/vorbis/opus\n\n' + basenames(ex) +
'\n\n<a href="#" onclick="new OGVPlayer();">click here</a> for a full crash report');
}
attempt_play = is_ev;
@@ -1439,7 +1445,7 @@ function play(tid, is_ev, seek, call_depth) {
return true;
}
catch (ex) {
toast.err(0, esc('playback failed: ' + ex));
toast.err(0, esc('playback failed: ' + basenames(ex)));
}
setclass(oid, 'play');
setTimeout(next_song, 500);
@@ -1473,7 +1479,7 @@ function evau_error(e) {
err += '\n\nFile: «' + uricom_dec(eplaya.src.split('/').slice(-1)[0])[0] + '»';
toast.warn(15, esc(err + ''));
toast.warn(15, esc(basenames(err)));
}
@@ -3228,12 +3234,12 @@ var treectl = (function () {
}
function reload_tree() {
var cdir = get_evpath(),
var cdir = get_vpath(),
links = QSA('#treeul a+a'),
nowrap = QS('#tree.nowrap') && QS('#hovertree.on');
for (var a = 0, aa = links.length; a < aa; a++) {
var href = links[a].getAttribute('href');
var href = uricom_dec(links[a].getAttribute('href'))[0];
links[a].setAttribute('class', href == cdir ? 'hl' : '');
links[a].onclick = treego;
links[a].onmouseenter = nowrap ? menter : null;
@@ -4364,7 +4370,7 @@ var unpost = (function () {
for (var a = n; a < n2; a++)
if (QS('#op_unpost a.n' + a))
req.push(r.files[a].vp);
req.push(uricom_dec(r.files[a].vp)[0]);
var links = QSA('#op_unpost a.n' + n);
for (var a = 0, aa = links.length; a < aa; a++) {
@@ -4452,6 +4458,9 @@ function reload_browser(not_mp) {
makeSortable(ebi('files'), mp.read_order.bind(mp));
}
for (var a = 0; a < 2; a++)
clmod(ebi(a ? 'pro' : 'epi'), 'hidden', ebi('unsearch'));
if (window['up2k'])
up2k.set_fsearch();

View File

@@ -258,6 +258,16 @@ html.light #pctl *:focus,
html.light .btn:focus {
box-shadow: 0 .1em .2em #037 inset;
}
input[type="text"]:focus,
input:not([type]):focus,
textarea:focus {
box-shadow: 0 .1em .3em #fc0, 0 -.1em .3em #fc0;
}
html.light input[type="text"]:focus,
html.light input:not([type]):focus,
html.light textarea:focus {
box-shadow: 0 .1em .3em #037, 0 -.1em .3em #037;
}

View File

@@ -512,9 +512,13 @@ function up2k_init(subtle) {
// chrome<37 firefox<34 edge<12 opera<24 safari<7
shame = 'your browser is impressively ancient';
var got_deps = false;
function got_deps() {
return subtle || window.asmCrypto || window.hashwasm;
}
var loading_deps = false;
function init_deps() {
if (!got_deps && !subtle && !window.asmCrypto) {
if (!loading_deps && !got_deps()) {
var fn = 'sha512.' + sha_js + '.js';
showmodal('<h1>loading ' + fn + '</h1><h2>since ' + shame + '</h2><h4>thanks chrome</h4>');
import_js('/.cpr/deps/' + fn, unmodal);
@@ -525,7 +529,7 @@ function up2k_init(subtle) {
ebi('u2foot').innerHTML = 'seems like ' + shame + ' so do that if you want more performance <span style="color:#' +
(sha_js == 'ac' ? 'c84">(expecting 20' : '8a5">(but dont worry too much, expect 100') + ' MiB/s)</span>';
}
got_deps = true;
loading_deps = true;
}
if (perms.length && !has(perms, 'read') && has(perms, 'write'))
@@ -744,11 +748,14 @@ function up2k_init(subtle) {
more_one_file();
var bad_files = [],
nil_files = [],
good_files = [],
dirs = [];
for (var a = 0; a < files.length; a++) {
var fobj = files[a];
var fobj = files[a],
dst = good_files;
if (is_itemlist) {
if (fobj.kind !== 'file')
continue;
@@ -765,16 +772,15 @@ function up2k_init(subtle) {
}
try {
if (fobj.size < 1)
throw 1;
dst = nil_files;
}
catch (ex) {
bad_files.push(fobj.name);
continue;
dst = bad_files;
}
good_files.push([fobj, fobj.name]);
dst.push([fobj, fobj.name]);
}
if (dirs) {
return read_dirs(null, [], dirs, good_files, bad_files);
return read_dirs(null, [], dirs, good_files, nil_files, bad_files);
}
}
@@ -788,7 +794,7 @@ function up2k_init(subtle) {
}
var rd_missing_ref = [];
function read_dirs(rd, pf, dirs, good, bad, spins) {
function read_dirs(rd, pf, dirs, good, nil, bad, spins) {
spins = spins || 0;
if (++spins == 5)
rd_missing_ref = rd_flatten(pf, dirs);
@@ -809,7 +815,7 @@ function up2k_init(subtle) {
msg.push('<li>' + esc(missing[a]) + '</li>');
return modal.alert(msg.join('') + '</ul>', function () {
read_dirs(rd, [], [], good, bad, spins);
read_dirs(rd, [], [], good, nil, bad, spins);
});
}
spins = 0;
@@ -817,11 +823,11 @@ function up2k_init(subtle) {
if (!dirs.length) {
if (!pf.length)
return gotallfiles(good, bad);
return gotallfiles(good, nil, bad);
console.log("retry pf, " + pf.length);
setTimeout(function () {
read_dirs(rd, pf, dirs, good, bad, spins);
read_dirs(rd, pf, dirs, good, nil, bad, spins);
}, 50);
return;
}
@@ -843,14 +849,15 @@ function up2k_init(subtle) {
pf.push(name);
dn.file(function (fobj) {
apop(pf, name);
var dst = good;
try {
if (fobj.size > 0) {
good.push([fobj, name]);
return;
}
if (fobj.size < 1)
dst = nil;
}
catch (ex) { }
bad.push(name);
catch (ex) {
dst = bad;
}
dst.push([fobj, name]);
});
}
ngot += 1;
@@ -859,23 +866,33 @@ function up2k_init(subtle) {
dirs.shift();
rd = null;
}
return read_dirs(rd, pf, dirs, good, bad, spins);
return read_dirs(rd, pf, dirs, good, nil, bad, spins);
});
}
function gotallfiles(good_files, bad_files) {
function gotallfiles(good_files, nil_files, bad_files) {
var ntot = good_files.concat(nil_files, bad_files).length;
if (bad_files.length) {
var ntot = bad_files.length + good_files.length,
msg = 'These {0} files (of {1} total) were skipped because they are empty:\n'.format(bad_files.length, ntot);
var msg = 'These {0} files (of {1} total) were skipped, possibly due to filesystem permissions:\n'.format(bad_files.length, ntot);
for (var a = 0, aa = Math.min(20, bad_files.length); a < aa; a++)
msg += '-- ' + bad_files[a] + '\n';
if (good_files.length - bad_files.length <= 1 && ANDROID)
msg += '\nFirefox-Android has a bug which prevents selecting multiple files. Try selecting one file at a time. For more info, see firefox bug 1456557';
msg += '-- ' + bad_files[a][1] + '\n';
msg += '\nMaybe it works better if you select just one file';
return modal.alert(msg, function () {
gotallfiles(good_files, []);
gotallfiles(good_files, nil_files, []);
});
}
if (nil_files.length) {
var msg = 'These {0} files (of {1} total) are blank/empty; upload them anyways?\n'.format(nil_files.length, ntot);
for (var a = 0, aa = Math.min(20, nil_files.length); a < aa; a++)
msg += '-- ' + nil_files[a][1] + '\n';
msg += '\nMaybe it works better if you select just one file';
return modal.confirm(msg, function () {
gotallfiles(good_files.concat(nil_files), [], []);
}, function () {
gotallfiles(good_files, [], []);
});
}
@@ -921,7 +938,7 @@ function up2k_init(subtle) {
"t0": now,
"fobj": fobj,
"name": name,
"size": fobj.size,
"size": fobj.size || 0,
"lmod": lmod / 1000,
"purl": fdir,
"done": false,
@@ -946,7 +963,9 @@ function up2k_init(subtle) {
st.bytes.total += fobj.size;
st.files.push(entry);
if (uc.turbo)
if (!entry.size)
push_t(st.todo.handshake, entry);
else if (uc.turbo)
push_t(st.todo.head, entry);
else
push_t(st.todo.hash, entry);
@@ -1117,7 +1136,7 @@ function up2k_init(subtle) {
if (running)
return;
if (crashed)
if (crashed || !got_deps())
return defer();
running = true;
@@ -1545,15 +1564,18 @@ function up2k_init(subtle) {
}
else {
smsg = 'found';
var hit = response.hits[0],
msg = linksplit(hit.rp).join(''),
tr = unix2iso(hit.ts),
tu = unix2iso(t.lmod),
diff = parseInt(t.lmod) - parseInt(hit.ts),
cdiff = (Math.abs(diff) <= 2) ? '3c0' : 'f0b',
sdiff = '<span style="color:#' + cdiff + '">diff ' + diff;
var msg = [];
for (var a = 0, aa = Math.min(20, response.hits.length); a < aa; a++) {
var hit = response.hits[a],
tr = unix2iso(hit.ts),
tu = unix2iso(t.lmod),
diff = parseInt(t.lmod) - parseInt(hit.ts),
cdiff = (Math.abs(diff) <= 2) ? '3c0' : 'f0b',
sdiff = '<span style="color:#' + cdiff + '">diff ' + diff;
msg += '<br /><small>' + tr + ' (srv), ' + tu + ' (You), ' + sdiff + '</span></span>';
msg.push(linksplit(hit.rp).join('') + '<br /><small>' + tr + ' (srv), ' + tu + ' (You), ' + sdiff + '</small></span>');
}
msg = msg.join('<br />\n');
}
pvis.seth(t.n, 2, msg);
pvis.seth(t.n, 1, smsg);
@@ -1938,7 +1960,7 @@ function up2k_init(subtle) {
flag = up2k_flagbus();
}
catch (ex) {
toast.err(5, "not supported on your browser:\n" + ex);
toast.err(5, "not supported on your browser:\n" + esc(basenames(ex)));
bcfg_set('flag_en', false);
}
}

View File

@@ -29,18 +29,24 @@ function esc(txt) {
}[c];
});
}
window.onunhandledrejection = function (e) {
var err = e.reason;
try {
err += '\n' + e.reason.stack;
}
catch (e) { }
console.log("REJ: " + err);
try {
toast.warn(30, err);
}
catch (e) { }
};
function basenames(txt) {
return (txt + '').replace(/https?:\/\/[^ \/]+\//g, '/').replace(/js\?_=[a-zA-Z]{4}/g, 'js');
}
if ((document.location + '').indexOf(',rej,') + 1)
window.onunhandledrejection = function (e) {
var err = e.reason;
try {
err += '\n' + e.reason.stack;
}
catch (e) { }
err = basenames(err);
console.log("REJ: " + err);
try {
toast.warn(30, err);
}
catch (e) { }
};
try {
console.hist = [];
var hook = function (t) {
@@ -151,7 +157,7 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
);
document.head.appendChild(s);
}
exbox.innerHTML = html.join('\n').replace(/https?:\/\/[^ \/]+\//g, '/').replace(/js\?_=[a-zA-Z]{4}/g, 'js').replace(/<ghi>/, 'https://github.com/9001/copyparty/issues/new?labels=bug&template=bug_report.md');
exbox.innerHTML = basenames(html.join('\n')).replace(/<ghi>/, 'https://github.com/9001/copyparty/issues/new?labels=bug&template=bug_report.md');
exbox.style.display = 'block';
}
catch (e) {
@@ -241,7 +247,9 @@ function import_js(url, cb) {
script.src = url;
script.onload = cb;
script.onerror = function () {
toast.err(0, 'Failed to load module:\n' + url);
var m = 'Failed to load module:\n' + url;
console.log(m);
toast.err(0, m);
};
head.appendChild(script);
}
@@ -905,6 +913,9 @@ var toast = (function () {
if (sec)
te = setTimeout(r.hide, sec * 1000);
if (txt.indexOf('<body>') + 1)
txt = txt.slice(0, txt.indexOf('<')) + ' [...]';
obj.innerHTML = '<a href="#" id="toastc">x</a><div id="toastb">' + lf2br(txt) + '</div>';
obj.className = cl;
sec += obj.offsetWidth;
@@ -1046,7 +1057,7 @@ var modal = (function () {
}
function _confirm(html, cok, cng, fun) {
cb_ok = cok;
cb_ng = cng === undefined ? cok : null;
cb_ng = cng === undefined ? cok : cng;
cb_up = fun;
html += '<div id="modalb">' + ok_cancel + '</div>';
r.show(html);

View File

@@ -162,7 +162,7 @@ brew install python@2
pip install virtualenv
# readme toc
cat README.md | awk 'function pr() { if (!h) {return}; if (/^ *[*!#]/||!s) {printf "%s\n",h;h=0;return}; if (/.../) {printf "%s - %s\n",h,$0;h=0}; }; /^#/{s=1;pr()} /^#* *(file indexing|install on android|dev env setup|just the sfx|complete release|optional gpl stuff)|`$/{s=0} /^#/{lv=length($1);sub(/[^ ]+ /,"");bab=$0;gsub(/ /,"-",bab); h=sprintf("%" ((lv-1)*4+1) "s [%s](#%s)", "*",$0,bab);next} !h{next} {sub(/ .*/,"");sub(/[:,]$/,"")} {pr()}' > toc; grep -E '^## readme toc' -B1000 -A2 <README.md >p1; grep -E '^## quickstart' -B2 -A999999 <README.md >p2; (cat p1; grep quickstart -A1000 <toc; cat p2) >README.md
cat README.md | awk 'function pr() { if (!h) {return}; if (/^ *[*!#]/||!s) {printf "%s\n",h;h=0;return}; if (/.../) {printf "%s - %s\n",h,$0;h=0}; }; /^#/{s=1;pr()} /^#* *(file indexing|install on android|dev env setup|just the sfx|complete release|optional gpl stuff)|`$/{s=0} /^#/{lv=length($1);sub(/[^ ]+ /,"");bab=$0;gsub(/ /,"-",bab); h=sprintf("%" ((lv-1)*4+1) "s [%s](#%s)", "*",$0,bab);next} !h{next} {sub(/ .*/,"");sub(/[:,]$/,"")} {pr()}' > toc; grep -E '^## readme toc' -B1000 -A2 <README.md >p1; grep -E '^## quickstart' -B2 -A999999 <README.md >p2; (cat p1; grep quickstart -A1000 <toc; cat p2) >README.md; rm p1 p2 toc
# fix firefox phantom breakpoints,
# suggestions from bugtracker, doesnt work (debugger is not attachable)

View File

@@ -114,7 +114,7 @@ args = {
"install_requires": ["jinja2"],
"extras_require": {"thumbnails": ["Pillow"], "audiotags": ["mutagen"]},
"entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]},
"scripts": ["bin/copyparty-fuse.py"],
"scripts": ["bin/copyparty-fuse.py", "bin/up2k.py"],
"cmdclass": {"clean2": clean2},
}

View File

@@ -3,6 +3,7 @@ import sys
import time
import shutil
import jinja2
import threading
import tempfile
import platform
import subprocess as sp
@@ -28,7 +29,7 @@ if MACOS:
# 25% faster; until any tests do symlink stuff
from copyparty.util import Unrecv
from copyparty.util import Unrecv, FHC
def runcmd(argv):
@@ -132,8 +133,10 @@ class VHttpConn(object):
self.log_src = "a"
self.lf_url = None
self.hsrv = VHttpSrv()
self.u2fh = FHC()
self.mutex = threading.Lock()
self.nreq = 0
self.nbyte = 0
self.ico = None
self.thumbcli = None
self.t0 = time.time()
self.t0 = time.time()