support rclone as fuse client

This commit is contained in:
ed 2020-10-25 08:04:41 +01:00
parent 90a5cb5e59
commit b967a92f69
4 changed files with 206 additions and 24 deletions

View file

@ -128,7 +128,7 @@ def main():
) )
ap.add_argument("-i", metavar="IP", type=str, default="0.0.0.0", help="ip to bind") ap.add_argument("-i", metavar="IP", type=str, default="0.0.0.0", help="ip to bind")
ap.add_argument("-p", metavar="PORT", type=int, default=3923, help="port to bind") ap.add_argument("-p", metavar="PORT", type=int, default=3923, help="port to bind")
ap.add_argument("-nc", metavar="NUM", type=int, default=16, help="max num clients") ap.add_argument("-nc", metavar="NUM", type=int, default=64, help="max num clients")
ap.add_argument( ap.add_argument(
"-j", metavar="CORES", type=int, default=1, help="max num cpu cores" "-j", metavar="CORES", type=int, default=1, help="max num cpu cores"
) )

View file

@ -16,9 +16,6 @@ from .util import * # noqa # pylint: disable=unused-wildcard-import
if not PY2: if not PY2:
unicode = str unicode = str
from html import escape as html_escape
else:
from cgi import escape as html_escape # pylint: disable=no-name-in-module
class HttpCli(object): class HttpCli(object):
@ -125,6 +122,11 @@ class HttpCli(object):
self.uparam = uparam self.uparam = uparam
self.vpath = unquotep(vpath) self.vpath = unquotep(vpath)
ua = self.headers.get("user-agent", "")
if ua.startswith("rclone/"):
uparam["raw"] = True
uparam["dots"] = True
try: try:
if self.mode in ["GET", "HEAD"]: if self.mode in ["GET", "HEAD"]:
return self.handle_get() and self.keepalive return self.handle_get() and self.keepalive
@ -141,7 +143,7 @@ class HttpCli(object):
try: try:
# self.log("pebkac at httpcli.run #2: " + repr(ex)) # self.log("pebkac at httpcli.run #2: " + repr(ex))
self.keepalive = self._check_nonfatal(ex) self.keepalive = self._check_nonfatal(ex)
self.loud_reply(str(ex), status=ex.code) self.loud_reply("{}: {}".format(str(ex), self.vpath), status=ex.code)
return self.keepalive return self.keepalive
except Pebkac: except Pebkac:
return False return False
@ -180,7 +182,8 @@ class HttpCli(object):
self.send_headers(len(body), status, mime, headers) self.send_headers(len(body), status, mime, headers)
try: try:
self.s.sendall(body) if self.mode != "HEAD":
self.s.sendall(body)
except: except:
raise Pebkac(400, "client d/c while replying body") raise Pebkac(400, "client d/c while replying body")
@ -188,7 +191,7 @@ class HttpCli(object):
def loud_reply(self, body, *args, **kwargs): def loud_reply(self, body, *args, **kwargs):
self.log(body.rstrip()) self.log(body.rstrip())
self.reply(b"<pre>" + body.encode("utf-8"), *list(args), **kwargs) self.reply(b"<pre>" + body.encode("utf-8") + b"\r\n", *list(args), **kwargs)
def handle_get(self): def handle_get(self):
logmsg = "{:4} {}".format(self.mode, self.req) logmsg = "{:4} {}".format(self.mode, self.req)
@ -493,7 +496,7 @@ class HttpCli(object):
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/") vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
html = self.conn.tpl_msg.render( html = self.conn.tpl_msg.render(
h2='<a href="/{}">go to /{}</a>'.format( h2='<a href="/{}">go to /{}</a>'.format(
quotep(vpath), html_escape(vpath, quote=False) quotep(vpath), html_escape(vpath)
), ),
pre="aight", pre="aight",
click=True, click=True,
@ -527,7 +530,7 @@ class HttpCli(object):
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/") vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
html = self.conn.tpl_msg.render( html = self.conn.tpl_msg.render(
h2='<a href="/{}?edit">go to /{}?edit</a>'.format( h2='<a href="/{}?edit">go to /{}?edit</a>'.format(
quotep(vpath), html_escape(vpath, quote=False) quotep(vpath), html_escape(vpath)
), ),
pre="aight", pre="aight",
click=True, click=True,
@ -621,7 +624,7 @@ class HttpCli(object):
html = self.conn.tpl_msg.render( html = self.conn.tpl_msg.render(
h2='<a href="/{}">return to /{}</a>'.format( h2='<a href="/{}">return to /{}</a>'.format(
quotep(self.vpath), html_escape(self.vpath, quote=False) quotep(self.vpath), html_escape(self.vpath)
), ),
pre=msg, pre=msg,
) )
@ -938,7 +941,7 @@ class HttpCli(object):
targs = { targs = {
"edit": "edit" in self.uparam, "edit": "edit" in self.uparam,
"title": html_escape(self.vpath, quote=False), "title": html_escape(self.vpath),
"lastmod": int(ts_md * 1000), "lastmod": int(ts_md * 1000),
"md": "", "md": "",
} }
@ -979,7 +982,7 @@ class HttpCli(object):
else: else:
vpath += "/" + node vpath += "/" + node
vpnodes.append([quotep(vpath) + "/", html_escape(node, quote=False)]) vpnodes.append([quotep(vpath) + "/", html_escape(node)])
vn, rem = self.auth.vfs.get( vn, rem = self.auth.vfs.get(
self.vpath, self.uname, self.readable, self.writable self.vpath, self.uname, self.readable, self.writable
@ -1054,7 +1057,7 @@ class HttpCli(object):
dt = datetime.utcfromtimestamp(inf.st_mtime) dt = datetime.utcfromtimestamp(inf.st_mtime)
dt = dt.strftime("%Y-%m-%d %H:%M:%S") dt = dt.strftime("%Y-%m-%d %H:%M:%S")
item = [margin, quotep(href), html_escape(fn, quote=False), sz, dt] item = [margin, quotep(href), html_escape(fn), sz, dt]
if is_dir: if is_dir:
dirs.append(item) dirs.append(item)
else: else:
@ -1119,7 +1122,7 @@ class HttpCli(object):
ts=ts, ts=ts,
prologue=logues[0], prologue=logues[0],
epilogue=logues[1], epilogue=logues[1],
title=html_escape(self.vpath, quote=False), title=html_escape(self.vpath),
srv_info="</span> /// <span>".join(srv_info), srv_info="</span> /// <span>".join(srv_info),
) )
self.reply(html.encode("utf-8", "replace")) self.reply(html.encode("utf-8", "replace"))

View file

@ -335,18 +335,18 @@ def read_header(sr):
def humansize(sz, terse=False): def humansize(sz, terse=False):
for unit in ['B', 'KiB', 'MiB', 'GiB', 'TiB']: for unit in ["B", "KiB", "MiB", "GiB", "TiB"]:
if sz < 1024: if sz < 1024:
break break
sz /= 1024. sz /= 1024.0
ret = ' '.join([str(sz)[:4].rstrip('.'), unit]) ret = " ".join([str(sz)[:4].rstrip("."), unit])
if not terse: if not terse:
return ret return ret
return ret.replace('iB', '').replace(' ', '') return ret.replace("iB", "").replace(" ", "")
def undot(path): def undot(path):
@ -398,6 +398,21 @@ def exclude_dotfiles(filepaths):
yield fpath yield fpath
def html_escape(s, quote=False):
"""html.escape but also newlines"""
s = (
s.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\r", "&#13;")
.replace("\n", "&#10;")
)
if quote:
s = s.replace('"', "&quot;").replace("'", "&#x27;")
return s
def quotep(txt): def quotep(txt):
"""url quoter which deals with bytes correctly""" """url quoter which deals with bytes correctly"""
btxt = w8enc(txt) btxt = w8enc(txt)
@ -412,8 +427,8 @@ def quotep(txt):
def unquotep(txt): def unquotep(txt):
"""url unquoter which deals with bytes correctly""" """url unquoter which deals with bytes correctly"""
btxt = w8enc(txt) btxt = w8enc(txt)
unq1 = btxt.replace(b"+", b" ") # btxt = btxt.replace(b"+", b" ")
unq2 = unquote(unq1) unq2 = unquote(btxt)
return w8dec(unq2) return w8dec(unq2)

164
scripts/speedtest-fs.py Normal file
View file

@ -0,0 +1,164 @@
#!/usr/bin/env python
import os
import sys
import stat
import time
import signal
import traceback
import threading
from queue import Queue
"""speedtest-fs: filesystem performance estimate"""
__author__ = "ed <copyparty@ocv.me>"
__copyright__ = 2020
__license__ = "MIT"
__url__ = "https://github.com/9001/copyparty/"
def get_spd(nbyte, nsec):
if not nsec:
return "0.000 MB 0.000 sec 0.000 MB/s"
mb = nbyte / (1024 * 1024.0)
spd = mb / nsec
return f"{mb:.3f} MB {nsec:.3f} sec {spd:.3f} MB/s"
class Inf(object):
def __init__(self, t0):
self.msgs = []
self.errors = []
self.reports = []
self.mtx_msgs = threading.Lock()
self.mtx_reports = threading.Lock()
self.n_byte = 0
self.n_sec = 0
self.n_done = 0
self.t0 = t0
thr = threading.Thread(target=self.print_msgs)
thr.daemon = True
thr.start()
def msg(self, fn, n_read):
with self.mtx_msgs:
self.msgs.append(f"{fn} {n_read}")
def err(self, fn):
with self.mtx_reports:
self.errors.append(f"{fn}\n{traceback.format_exc()}")
def print_msgs(self):
while True:
time.sleep(0.02)
with self.mtx_msgs:
msgs = self.msgs
self.msgs = []
if not msgs:
continue
msgs = msgs[-64:]
msgs = [f"{get_spd(self.n_byte, self.n_sec)} {x}" for x in msgs]
print("\n".join(msgs))
def report(self, fn, n_byte, n_sec):
with self.mtx_reports:
self.reports.append([n_byte, n_sec, fn])
self.n_byte += n_byte
self.n_sec += n_sec
def done(self):
with self.mtx_reports:
self.n_done += 1
def get_files(dir_path):
for fn in os.listdir(dir_path):
fn = os.path.join(dir_path, fn)
st = os.stat(fn).st_mode
if stat.S_ISDIR(st):
yield from get_files(fn)
if stat.S_ISREG(st):
yield fn
def worker(q, inf, read_sz):
while True:
fn = q.get()
if not fn:
break
n_read = 0
try:
t0 = time.time()
with open(fn, "rb") as f:
while True:
buf = f.read(read_sz)
if not buf:
break
n_read += len(buf)
inf.msg(fn, n_read)
inf.report(fn, n_read, time.time() - t0)
except:
inf.err(fn)
inf.done()
def sighandler(signo, frame):
os._exit(0)
def main():
signal.signal(signal.SIGINT, sighandler)
root = "."
if len(sys.argv) > 1:
root = sys.argv[1]
t0 = time.time()
q = Queue(256)
inf = Inf(t0)
num_threads = 8
read_sz = 32 * 1024
for _ in range(num_threads):
thr = threading.Thread(target=worker, args=(q, inf, read_sz,))
thr.daemon = True
thr.start()
for fn in get_files(root):
q.put(fn)
for _ in range(num_threads):
q.put(None)
while inf.n_done < num_threads:
time.sleep(0.1)
t2 = time.time()
print("\n")
log = inf.reports
log.sort()
for nbyte, nsec, fn in log[-64:]:
print(f"{get_spd(nbyte, nsec)} {fn}")
print()
print("\n".join(inf.errors))
print(get_spd(inf.n_byte, t2 - t0))
if __name__ == "__main__":
main()