Compare commits

...

15 Commits

Author SHA1 Message Date
ed
e0d975e36a v0.10.19 2021-05-14 00:00:15 +02:00
ed
cfeb15259f not careful enough 2021-05-13 23:29:15 +02:00
ed
3b3f8fc8fb careful rice 2021-05-13 23:00:51 +02:00
ed
88bd2c084c misc 2021-05-13 22:58:36 +02:00
ed
bd367389b0 broke windows 2021-05-13 22:58:23 +02:00
ed
58ba71a76f option to hide incomplete uploads 2021-05-13 22:56:52 +02:00
ed
d03e34d55d v0.10.18 2021-05-13 17:42:06 +02:00
ed
24f239a46c ui tweaks 2021-05-13 17:41:14 +02:00
ed
2c0826f85a conditional sections in volume listing 2021-05-13 17:24:37 +02:00
ed
c061461d01 fix md perm reqs + dyn up2k modeset 2021-05-13 17:22:31 +02:00
ed
e7982a04fe explicit redirect to single non-roots 2021-05-13 16:54:31 +02:00
ed
33b91a7513 set password cookie expiration 2021-05-13 16:23:28 +02:00
ed
9bb1323e44 rclone faster + query params correctness 2021-05-13 16:02:30 +02:00
ed
e62bb807a5 better 2021-05-13 01:36:14 +02:00
ed
3fc0d2cc4a better 2021-05-13 00:43:25 +02:00
16 changed files with 127 additions and 88 deletions

2
.vscode/launch.py vendored
View File

@@ -12,7 +12,7 @@ sys.path.insert(0, os.getcwd())
import jstyleson import jstyleson
from copyparty.__main__ import main as copyparty from copyparty.__main__ import main as copyparty
with open(".vscode/launch.json", "r") as f: with open(".vscode/launch.json", "r", encoding="utf-8") as f:
tj = f.read() tj = f.read()
oj = jstyleson.loads(tj) oj = jstyleson.loads(tj)

View File

@@ -307,7 +307,7 @@ copyparty can invoke external programs to collect additional metadata for files
# browser support # browser support
![copyparty-ie4-fs8](https://user-images.githubusercontent.com/241032/116009043-a1909d80-a617-11eb-9140-037ad6604899.png) ![copyparty-ie4-fs8](https://user-images.githubusercontent.com/241032/118192791-fb31fe00-b446-11eb-9647-898ea8efc1f7.png)
`ie` = internet-explorer, `ff` = firefox, `c` = chrome, `iOS` = iPhone/iPad, `Andr` = Android `ie` = internet-explorer, `ff` = firefox, `c` = chrome, `iOS` = iPhone/iPad, `Andr` = Android

View File

@@ -244,6 +244,7 @@ def run_argparse(argv, formatter):
ap.add_argument("-nw", action="store_true", help="disable writes (benchmark)") 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("-nih", action="store_true", help="no info hostname")
ap.add_argument("-nid", action="store_true", help="no info disk-usage") 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("--no-zip", action="store_true", help="disable download as zip/tar") ap.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
ap.add_argument("--no-sendfile", action="store_true", help="disable sendfile (for debugging)") ap.add_argument("--no-sendfile", action="store_true", help="disable sendfile (for debugging)")
ap.add_argument("--no-scandir", action="store_true", help="disable scandir (for debugging)") ap.add_argument("--no-scandir", action="store_true", help="disable scandir (for debugging)")

View File

@@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
VERSION = (0, 10, 17) VERSION = (0, 10, 19)
CODENAME = "zip it" CODENAME = "zip it"
BUILD_DT = (2021, 5, 12) BUILD_DT = (2021, 5, 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

@@ -120,29 +120,33 @@ class HttpCli(object):
else: else:
uparam[k.lower()] = False uparam[k.lower()] = False
self.ouparam = {k: v for k, v in uparam.items()}
cookies = self.headers.get("cookie") or {}
if cookies:
cookies = [x.split("=", 1) for x in cookies.split(";") if "=" in x]
cookies = {k.strip(): unescape_cookie(v) for k, v in cookies}
for kc, ku in [["cppwd", "pw"], ["b", "b"]]:
if kc in cookies and ku not in uparam:
uparam[ku] = cookies[kc]
self.uparam = uparam self.uparam = uparam
self.cookies = cookies
self.vpath = unquotep(vpath) self.vpath = unquotep(vpath)
pwd = None pwd = uparam.get("pw")
if "cookie" in self.headers:
cookies = self.headers["cookie"].split(";")
for k, v in [x.split("=", 1) for x in cookies]:
if k.strip() != "cppwd":
continue
pwd = unescape_cookie(v)
break
pwd = uparam.get("pw", pwd)
self.uname = self.auth.iuser.get(pwd, "*") self.uname = self.auth.iuser.get(pwd, "*")
if self.uname: if self.uname:
self.rvol = self.auth.vfs.user_tree(self.uname, readable=True) self.rvol = self.auth.vfs.user_tree(self.uname, readable=True)
self.wvol = self.auth.vfs.user_tree(self.uname, writable=True) self.wvol = self.auth.vfs.user_tree(self.uname, writable=True)
ua = self.headers.get("user-agent", "") ua = self.headers.get("user-agent", "")
if ua.startswith("rclone/"): self.is_rclone = ua.startswith("rclone/")
if self.is_rclone:
uparam["raw"] = False uparam["raw"] = False
uparam["dots"] = False uparam["dots"] = False
uparam["b"] = False
cookies["b"] = False
try: try:
if self.mode in ["GET", "HEAD"]: if self.mode in ["GET", "HEAD"]:
@@ -218,7 +222,14 @@ class HttpCli(object):
removing anything in rm, adding pairs in add removing anything in rm, adding pairs in add
""" """
kv = {k: v for k, v in self.uparam.items() if k not in rm} if self.is_rclone:
return ""
kv = {
k: v
for k, v in self.uparam.items()
if k not in rm and self.cookies.get(k) != v
}
kv.update(add) kv.update(add)
if not kv: if not kv:
return "" return ""
@@ -226,6 +237,22 @@ class HttpCli(object):
r = ["{}={}".format(k, quotep(v)) if v else k for k, v in kv.items()] r = ["{}={}".format(k, quotep(v)) if v else k for k, v in kv.items()]
return "?" + "&".join(r) return "?" + "&".join(r)
def redirect(self, vpath, suf="", msg="aight", flavor="go to", use302=False):
html = self.j2(
"msg",
h2='<a href="/{}">{} /{}</a>'.format(
quotep(vpath) + suf, flavor, html_escape(vpath, crlf=True) + suf
),
pre=msg,
click=True,
).encode("utf-8", "replace")
if use302:
h = {"Location": "/" + vpath, "Cache-Control": "no-cache"}
self.reply(html, status=302, headers=h)
else:
self.reply(html)
def handle_get(self): def handle_get(self):
logmsg = "{:4} {}".format(self.mode, self.req) logmsg = "{:4} {}".format(self.mode, self.req)
@@ -248,16 +275,18 @@ class HttpCli(object):
return self.tx_tree() return self.tx_tree()
# conditional redirect to single volumes # conditional redirect to single volumes
if self.vpath == "" and not self.uparam: if self.vpath == "" and not self.ouparam:
nread = len(self.rvol) nread = len(self.rvol)
nwrite = len(self.wvol) nwrite = len(self.wvol)
if nread + nwrite == 1 or (self.rvol == self.wvol and nread == 1): if nread + nwrite == 1 or (self.rvol == self.wvol and nread == 1):
if nread == 1: if nread == 1:
self.vpath = self.rvol[0] vpath = self.rvol[0]
else: else:
self.vpath = self.wvol[0] vpath = self.wvol[0]
self.absolute_urls = True if self.vpath != vpath:
self.redirect(vpath, flavor="redirecting to", use302=True)
return True
self.readable, self.writable = self.conn.auth.vfs.can_access( self.readable, self.writable = self.conn.auth.vfs.can_access(
self.vpath, self.uname self.vpath, self.uname
@@ -645,13 +674,16 @@ class HttpCli(object):
if pwd in self.auth.iuser: if pwd in self.auth.iuser:
msg = "login ok" msg = "login ok"
dt = datetime.utcfromtimestamp(time.time() + 60 * 60 * 24 * 365)
exp = dt.strftime("%a, %d %b %Y %H:%M:%S GMT")
else: else:
msg = "naw dude" msg = "naw dude"
pwd = "x" # nosec pwd = "x" # nosec
exp = "Fri, 15 Aug 1997 01:00:00 GMT"
h = {"Set-Cookie": "cppwd={}; Path=/; SameSite=Lax".format(pwd)} ck = "cppwd={}; Path=/; Expires={}; SameSite=Lax".format(pwd, exp)
html = self.j2("msg", h1=msg, h2='<a href="/">ack</a>', redir="/") html = self.j2("msg", h1=msg, h2='<a href="/">ack</a>', redir="/")
self.reply(html.encode("utf-8"), headers=h) self.reply(html.encode("utf-8"), headers={"Set-Cookie": ck})
return True return True
def handle_mkdir(self): def handle_mkdir(self):
@@ -680,14 +712,7 @@ class HttpCli(object):
raise Pebkac(500, "mkdir failed, check the logs") raise Pebkac(500, "mkdir failed, check the logs")
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/") vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
esc_paths = [quotep(vpath), html_escape(vpath, crlf=True)] self.redirect(vpath)
html = self.j2(
"msg",
h2='<a href="/{}">go to /{}</a>'.format(*esc_paths),
pre="aight",
click=True,
)
self.reply(html.encode("utf-8", "replace"))
return True return True
def handle_new_md(self): def handle_new_md(self):
@@ -714,15 +739,7 @@ class HttpCli(object):
f.write(b"`GRUNNUR`\n") f.write(b"`GRUNNUR`\n")
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/") vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
html = self.j2( self.redirect(vpath, "?edit")
"msg",
h2='<a href="/{}?edit">go to /{}?edit</a>'.format(
quotep(vpath), html_escape(vpath)
),
pre="aight",
click=True,
)
self.reply(html.encode("utf-8", "replace"))
return True return True
def handle_plain_upload(self): def handle_plain_upload(self):
@@ -772,12 +789,16 @@ class HttpCli(object):
except Pebkac: except Pebkac:
if fname != os.devnull: if fname != os.devnull:
fp = os.path.join(fdir, fname) fp = os.path.join(fdir, fname)
fp2 = fp
if self.args.dotpart:
fp2 = os.path.join(fdir, "." + fname)
suffix = ".PARTIAL" suffix = ".PARTIAL"
try: try:
os.rename(fsenc(fp), fsenc(fp + suffix)) os.rename(fsenc(fp), fsenc(fp2 + suffix))
except: except:
fp = fp[: -len(suffix)] fp2 = fp2[: -len(suffix) - 1]
os.rename(fsenc(fp), fsenc(fp + suffix)) os.rename(fsenc(fp), fsenc(fp2 + suffix))
raise raise
@@ -823,14 +844,7 @@ class HttpCli(object):
).encode("utf-8") ).encode("utf-8")
) )
html = self.j2( self.redirect(self.vpath, msg=msg, flavor="return to")
"msg",
h2='<a href="/{}">return to /{}</a>'.format(
quotep(self.vpath), html_escape(self.vpath)
),
pre=msg,
)
self.reply(html.encode("utf-8", "replace"))
self.parser.drop() self.parser.drop()
return True return True
@@ -930,13 +944,14 @@ class HttpCli(object):
return True return True
def _chk_lastmod(self, file_ts): def _chk_lastmod(self, file_ts):
date_fmt = "%a, %d %b %Y %H:%M:%S GMT"
file_dt = datetime.utcfromtimestamp(file_ts) file_dt = datetime.utcfromtimestamp(file_ts)
file_lastmod = file_dt.strftime("%a, %d %b %Y %H:%M:%S GMT") file_lastmod = file_dt.strftime(date_fmt)
cli_lastmod = self.headers.get("if-modified-since") cli_lastmod = self.headers.get("if-modified-since")
if cli_lastmod: if cli_lastmod:
try: try:
cli_dt = time.strptime(cli_lastmod, "%a, %d %b %Y %H:%M:%S GMT") cli_dt = time.strptime(cli_lastmod, date_fmt)
cli_ts = calendar.timegm(cli_dt) cli_ts = calendar.timegm(cli_dt)
return file_lastmod, int(file_ts) > int(cli_ts) return file_lastmod, int(file_ts) > int(cli_ts)
except Exception as ex: except Exception as ex:

View File

@@ -1198,6 +1198,9 @@ class Up2k(object):
# raise Exception("aaa") # raise Exception("aaa")
tnam = job["name"] + ".PARTIAL" tnam = job["name"] + ".PARTIAL"
if self.args.dotpart:
tnam = "." + tnam
suffix = ".{:.6f}-{}".format(job["t0"], job["addr"]) suffix = ".{:.6f}-{}".format(job["t0"], job["addr"])
with ren_open(tnam, "wb", fdir=pdir, suffix=suffix) as f: with ren_open(tnam, "wb", fdir=pdir, suffix=suffix) as f:
f, job["tnam"] = f["orz"] f, job["tnam"] = f["orz"]

View File

@@ -49,6 +49,7 @@ HTTPCODE = {
200: "OK", 200: "OK",
204: "No Content", 204: "No Content",
206: "Partial Content", 206: "Partial Content",
302: "Found",
304: "Not Modified", 304: "Not Modified",
400: "Bad Request", 400: "Bad Request",
403: "Forbidden", 403: "Forbidden",
@@ -592,8 +593,8 @@ def sanitize_fn(fn, ok="", bad=[]):
["?", ""], ["?", ""],
["*", ""], ["*", ""],
] ]
for bad, good in [x for x in remap if x[0] not in ok]: for a, b in [x for x in remap if x[0] not in ok]:
fn = fn.replace(bad, good) fn = fn.replace(a, b)
bad.extend(["con", "prn", "aux", "nul"]) bad.extend(["con", "prn", "aux", "nul"])
for n in range(1, 10): for n in range(1, 10):

View File

@@ -21,7 +21,7 @@
{%- endif %} {%- endif %}
<a href="#" data-perm="write" data-dest="bup" data-desc="bup: basic uploader, even supports netscape 4.0">🎈</a> <a href="#" data-perm="write" data-dest="bup" data-desc="bup: basic uploader, even supports netscape 4.0">🎈</a>
<a href="#" data-perm="write" data-dest="mkdir" data-desc="mkdir: create a new directory">📂</a> <a href="#" data-perm="write" data-dest="mkdir" data-desc="mkdir: create a new directory">📂</a>
<a href="#" data-perm="write" data-dest="new_md" data-desc="new-md: create a new markdown document">📝</a> <a href="#" data-perm="read write" data-dest="new_md" data-desc="new-md: create a new markdown document">📝</a>
<a href="#" data-perm="write" data-dest="msg" data-desc="msg: send a message to the server log">📟</a> <a href="#" data-perm="write" data-dest="msg" data-desc="msg: send a message to the server log">📟</a>
<a href="#" data-dest="cfg" data-desc="configuration options">⚙️</a> <a href="#" data-dest="cfg" data-desc="configuration options">⚙️</a>
<div id="opdesc"></div> <div id="opdesc"></div>

View File

@@ -1307,21 +1307,20 @@ function apply_perms(perms) {
perms = perms || []; perms = perms || [];
var o = QSA('#ops>a[data-perm]'); var o = QSA('#ops>a[data-perm]');
for (var a = 0; a < o.length; a++) for (var a = 0; a < o.length; a++) {
o[a].style.display = 'none'; var display = 'inline';
var needed = o[a].getAttribute('data-perm').split(' ');
for (var a = 0; a < perms.length; a++) { for (var b = 0; b < needed.length; b++) {
o = QSA('#ops>a[data-perm="' + perms[a] + '"]'); if (!has(perms, needed[b])) {
for (var b = 0; b < o.length; b++) display = 'none';
o[b].style.display = 'inline'; }
}
o[a].style.display = display;
} }
var act = QS('#ops>a.act'); var act = QS('#ops>a.act');
if (act) { if (act && act.style.display === 'none')
var areq = act.getAttribute('data-perm'); goto();
if (areq && !has(perms, areq))
goto();
}
document.body.setAttribute('perms', perms.join(' ')); document.body.setAttribute('perms', perms.join(' '));

View File

@@ -6,6 +6,11 @@
<title>{{ title }}</title> <title>{{ title }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.8"> <meta name="viewport" content="width=device-width, initial-scale=0.8">
<style>
html{font-family:sans-serif}
td{border:1px solid #999;border-width:1px 1px 0 0;padding:0 5px}
a{display:block}
</style>
</head> </head>
<body> <body>
@@ -49,7 +54,7 @@
<div>{{ logues[1] }}</div><br /> <div>{{ logues[1] }}</div><br />
{%- endif %} {%- endif %}
<h2><a href="{{ url_suf }}&amp;h">control-panel</a></h2> <h2><a href="{{ url_suf }}{{ url_suf and '&amp;' or '?' }}h">control-panel</a></h2>
</body> </body>
</html> </html>

View File

@@ -13,19 +13,23 @@
<div id="wrap"> <div id="wrap">
<p>hello {{ this.uname }}</p> <p>hello {{ this.uname }}</p>
{%- if rvol %}
<h1>you can browse these:</h1> <h1>you can browse these:</h1>
<ul> <ul>
{% for mp in rvol %} {% for mp in rvol %}
<li><a href="/{{ mp }}{{ url_suf }}">/{{ mp }}</a></li> <li><a href="/{{ mp }}{{ url_suf }}">/{{ mp }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{%- endif %}
{%- if wvol %}
<h1>you can upload to:</h1> <h1>you can upload to:</h1>
<ul> <ul>
{% for mp in wvol %} {% for mp in wvol %}
<li><a href="/{{ mp }}{{ url_suf }}">/{{ mp }}</a></li> <li><a href="/{{ mp }}{{ url_suf }}">/{{ mp }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{%- endif %}
<h1>login for more:</h1> <h1>login for more:</h1>
<ul> <ul>

View File

@@ -1309,15 +1309,19 @@ function up2k_init(have_crypto) {
} }
function set_fsearch(new_state) { function set_fsearch(new_state) {
var perms = document.body.getAttribute('perms'), var perms = (document.body.getAttribute('perms') + '').split(' '),
read_only = false; fixed = false;
if (!ebi('fsearch')) { if (!ebi('fsearch')) {
new_state = false; new_state = false;
} }
else if (perms && perms.indexOf('write') === -1) { else if (!has(perms, 'write')) {
new_state = true; new_state = true;
read_only = true; fixed = true;
}
else if (!has(perms, 'read')) {
new_state = false;
fixed = true;
} }
if (new_state !== undefined) { if (new_state !== undefined) {
@@ -1326,7 +1330,7 @@ function up2k_init(have_crypto) {
} }
try { try {
QS('label[for="fsearch"]').style.opacity = read_only ? '0' : '1'; QS('label[for="fsearch"]').style.display = QS('#fsearch').style.display = fixed ? 'none' : '';
} }
catch (ex) { } catch (ex) { }

View File

@@ -22,6 +22,7 @@
#u2err.msg { #u2err.msg {
color: #999; color: #999;
padding: .5em; padding: .5em;
font-size: .9em;
} }
#u2btn { #u2btn {
color: #eee; color: #eee;

View File

@@ -36,7 +36,7 @@
<table id="u2conf"> <table id="u2conf">
<tr> <tr>
<td>parallel uploads:</td> <td><br />parallel uploads:</td>
<td rowspan="2"> <td rowspan="2">
<input type="checkbox" id="multitask" /> <input type="checkbox" id="multitask" />
<label for="multitask" alt="continue hashing other files while uploading">🏃</label> <label for="multitask" alt="continue hashing other files while uploading">🏃</label>
@@ -61,7 +61,7 @@
<td> <td>
<a href="#" id="nthread_sub">&ndash;</a><input <a href="#" id="nthread_sub">&ndash;</a><input
class="txtbox" id="nthread" value="2"/><a class="txtbox" id="nthread" value="2"/><a
href="#" id="nthread_add">+</a> href="#" id="nthread_add">+</a><br />&nbsp;
</td> </td>
</tr> </tr>
</table> </table>

View File

@@ -1,6 +1,5 @@
<!-- <!--
save this as .epilogue.html inside a save this as .epilogue.html inside a write-only folder to declutter the UI
write-only folder to declutter the UI
--> -->
<style> <style>
@@ -13,17 +12,20 @@
#u2cards /* and the upload progress tabs */ #u2cards /* and the upload progress tabs */
{display:none!important} /* do it! */ {display: none !important} /* do it! */
/* add some margins because now it's weird */ /* add some margins because now it's weird */
.opview {margin-top: 2.5em} .opview {margin-top: 2.5em}
#op_up2k {margin-top: 5em} #op_up2k {margin-top: 3em}
/* and embiggen the upload button */ /* and embiggen the upload button */
#u2conf #u2btn, #u2btn {padding:1.5em 0} #u2conf #u2btn, #u2btn {padding:1.5em 0}
/* adjust the button area a bit */
#u2conf.has_btn {width: 35em !important; margin: 5em auto}
</style> </style>
<a href="#" onclick="this.parentNode.innerHTML='';">show advanced options</a> <a href="#" onclick="this.parentNode.innerHTML='';">show advanced options</a>

View File

@@ -17,14 +17,15 @@ __license__ = "MIT"
__url__ = "https://github.com/9001/copyparty/" __url__ = "https://github.com/9001/copyparty/"
def get_spd(nbyte, nsec): def get_spd(nbyte, nfiles, nsec):
if not nsec: if not nsec:
return "0.000 MB 0.000 sec 0.000 MB/s" return "0.000 MB 0 files 0.000 sec 0.000 MB/s 0.000 f/s"
mb = nbyte / (1024 * 1024.0) mb = nbyte / (1024 * 1024.0)
spd = mb / nsec spd = mb / nsec
nspd = nfiles / nsec
return f"{mb:.3f} MB {nsec:.3f} sec {spd:.3f} MB/s" return f"{mb:.3f} MB {nfiles} files {nsec:.3f} sec {spd:.3f} MB/s {nspd:.3f} f/s"
class Inf(object): class Inf(object):
@@ -36,6 +37,7 @@ class Inf(object):
self.mtx_reports = threading.Lock() self.mtx_reports = threading.Lock()
self.n_byte = 0 self.n_byte = 0
self.n_file = 0
self.n_sec = 0 self.n_sec = 0
self.n_done = 0 self.n_done = 0
self.t0 = t0 self.t0 = t0
@@ -63,7 +65,8 @@ class Inf(object):
continue continue
msgs = msgs[-64:] msgs = msgs[-64:]
msgs = [f"{get_spd(self.n_byte, self.n_sec)} {x}" for x in msgs] spd = get_spd(self.n_byte, len(self.reports), self.n_sec)
msgs = [f"{spd} {x}" for x in msgs]
print("\n".join(msgs)) print("\n".join(msgs))
def report(self, fn, n_byte, n_sec): def report(self, fn, n_byte, n_sec):
@@ -131,8 +134,9 @@ def main():
num_threads = 8 num_threads = 8
read_sz = 32 * 1024 read_sz = 32 * 1024
targs = (q, inf, read_sz)
for _ in range(num_threads): for _ in range(num_threads):
thr = threading.Thread(target=worker, args=(q, inf, read_sz,)) thr = threading.Thread(target=worker, args=targs)
thr.daemon = True thr.daemon = True
thr.start() thr.start()
@@ -151,14 +155,14 @@ def main():
log = inf.reports log = inf.reports
log.sort() log.sort()
for nbyte, nsec, fn in log[-64:]: for nbyte, nsec, fn in log[-64:]:
print(f"{get_spd(nbyte, nsec)} {fn}") spd = get_spd(nbyte, len(log), nsec)
print(f"{spd} {fn}")
print() print()
print("\n".join(inf.errors)) print("\n".join(inf.errors))
print(get_spd(inf.n_byte, t2 - t0)) print(get_spd(inf.n_byte, len(log), t2 - t0))
if __name__ == "__main__": if __name__ == "__main__":
main() main()