use human-readable sizes more broadly
there was a function to "abbreviate" sizes, but it was somewhat unclear and incomplete. reuse the sizeof_fmt_* set of functions from the borg backup project (MIT licensed) to implement a more complete and flexible display that will scale up to the Yottabyte and beyond. it also supports non-IEC units (like "kibibyte", AKA 1024 bytes) if you fancy that stuff. this is a workaround for #91: it allows users to better see the size of the file that will be transfered. *some* places are still kept in bytes, most notably when receive fails to receive all bytes ("got %d bytes, wanted %d") because we may want more clarity there. text transfers also use the "bytes" suffix (instead of "B") because it will commonly not reach beyond the KiB range. note that the test suite only covers decimal (non-IEC) prefix, but it is assumed to be sufficient to be considered correct.
This commit is contained in:
parent
007d76c145
commit
047af4b27d
|
@ -7,7 +7,7 @@ from twisted.python import log
|
||||||
from ..wormhole import wormhole
|
from ..wormhole import wormhole
|
||||||
from ..transit import TransitReceiver
|
from ..transit import TransitReceiver
|
||||||
from ..errors import TransferError, WormholeClosedError
|
from ..errors import TransferError, WormholeClosedError
|
||||||
from ..util import dict_to_bytes, bytes_to_dict, bytes_to_hexstr
|
from ..util import dict_to_bytes, bytes_to_dict, bytes_to_hexstr, sizeof_fmt_iec
|
||||||
|
|
||||||
APPID = u"lothar.com/wormhole/text-or-file-xfer"
|
APPID = u"lothar.com/wormhole/text-or-file-xfer"
|
||||||
|
|
||||||
|
@ -194,8 +194,8 @@ class TwistedReceiver:
|
||||||
file_data["filename"])
|
file_data["filename"])
|
||||||
self.xfersize = file_data["filesize"]
|
self.xfersize = file_data["filesize"]
|
||||||
|
|
||||||
self._msg(u"Receiving file (%d bytes) into: %s" %
|
self._msg(u"Receiving file (%s) into: %s" %
|
||||||
(self.xfersize, os.path.basename(self.abs_destname)))
|
(sizeof_fmt_iec(self.xfersize), os.path.basename(self.abs_destname)))
|
||||||
self._ask_permission()
|
self._ask_permission()
|
||||||
tmp_destname = self.abs_destname + ".tmp"
|
tmp_destname = self.abs_destname + ".tmp"
|
||||||
return open(tmp_destname, "wb")
|
return open(tmp_destname, "wb")
|
||||||
|
@ -210,10 +210,10 @@ class TwistedReceiver:
|
||||||
file_data["dirname"])
|
file_data["dirname"])
|
||||||
self.xfersize = file_data["zipsize"]
|
self.xfersize = file_data["zipsize"]
|
||||||
|
|
||||||
self._msg(u"Receiving directory (%d bytes) into: %s/" %
|
self._msg(u"Receiving directory (%s) into: %s/" %
|
||||||
(self.xfersize, os.path.basename(self.abs_destname)))
|
(sizeof_fmt_iec(self.xfersize), os.path.basename(self.abs_destname)))
|
||||||
self._msg(u"%d files, %d bytes (uncompressed)" %
|
self._msg(u"%d files, %s (uncompressed)" %
|
||||||
(file_data["numfiles"], file_data["numbytes"]))
|
(file_data["numfiles"], sizeof_fmt_iec(file_data["numbytes"])))
|
||||||
self._ask_permission()
|
self._ask_permission()
|
||||||
return tempfile.SpooledTemporaryFile()
|
return tempfile.SpooledTemporaryFile()
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ from twisted.internet.defer import inlineCallbacks, returnValue
|
||||||
from ..errors import TransferError, WormholeClosedError
|
from ..errors import TransferError, WormholeClosedError
|
||||||
from ..wormhole import wormhole
|
from ..wormhole import wormhole
|
||||||
from ..transit import TransitSender
|
from ..transit import TransitSender
|
||||||
from ..util import dict_to_bytes, bytes_to_dict, bytes_to_hexstr
|
from ..util import dict_to_bytes, bytes_to_dict, bytes_to_hexstr, sizeof_fmt_iec
|
||||||
|
|
||||||
APPID = u"lothar.com/wormhole/text-or-file-xfer"
|
APPID = u"lothar.com/wormhole/text-or-file-xfer"
|
||||||
|
|
||||||
|
@ -167,7 +167,7 @@ class Sender:
|
||||||
text = six.moves.input("Text to send: ")
|
text = six.moves.input("Text to send: ")
|
||||||
|
|
||||||
if text is not None:
|
if text is not None:
|
||||||
print(u"Sending text message (%d bytes)" % len(text),
|
print(u"Sending text message (%s)" % sizeof_fmt_iec(len(text), suffix='bytes'),
|
||||||
file=args.stdout)
|
file=args.stdout)
|
||||||
offer = { "message": text }
|
offer = { "message": text }
|
||||||
fd_to_send = None
|
fd_to_send = None
|
||||||
|
@ -187,7 +187,8 @@ class Sender:
|
||||||
"filename": basename,
|
"filename": basename,
|
||||||
"filesize": filesize,
|
"filesize": filesize,
|
||||||
}
|
}
|
||||||
print(u"Sending %d byte file named '%s'" % (filesize, basename),
|
print(u"Sending %s file named '%s'"
|
||||||
|
% (sizeof_fmt_iec(filesize), basename),
|
||||||
file=args.stdout)
|
file=args.stdout)
|
||||||
fd_to_send = open(what, "rb")
|
fd_to_send = open(what, "rb")
|
||||||
return offer, fd_to_send
|
return offer, fd_to_send
|
||||||
|
@ -222,8 +223,8 @@ class Sender:
|
||||||
"numbytes": num_bytes,
|
"numbytes": num_bytes,
|
||||||
"numfiles": num_files,
|
"numfiles": num_files,
|
||||||
}
|
}
|
||||||
print(u"Sending directory (%d bytes compressed) named '%s'"
|
print(u"Sending directory (%s compressed) named '%s'"
|
||||||
% (filesize, basename), file=args.stdout)
|
% (sizeof_fmt_iec(filesize), basename), file=args.stdout)
|
||||||
return offer, fd_to_send
|
return offer, fd_to_send
|
||||||
|
|
||||||
raise TypeError("'%s' is neither file nor directory" % args.what)
|
raise TypeError("'%s' is neither file nor directory" % args.what)
|
||||||
|
|
|
@ -3,6 +3,7 @@ import os, time, json
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import click
|
import click
|
||||||
from .database import get_db
|
from .database import get_db
|
||||||
|
from ..util import sizeof_fmt_iec
|
||||||
|
|
||||||
def abbrev(t):
|
def abbrev(t):
|
||||||
if t is None:
|
if t is None:
|
||||||
|
@ -13,31 +14,6 @@ def abbrev(t):
|
||||||
return "%.1fms" % (t*1e3)
|
return "%.1fms" % (t*1e3)
|
||||||
return "%.1fus" % (t*1e6)
|
return "%.1fus" % (t*1e6)
|
||||||
|
|
||||||
def abbreviate_space(s, SI=True):
|
|
||||||
if s is None:
|
|
||||||
return "-"
|
|
||||||
if SI:
|
|
||||||
U = 1000.0
|
|
||||||
isuffix = "B"
|
|
||||||
else:
|
|
||||||
U = 1024.0
|
|
||||||
isuffix = "iB"
|
|
||||||
def r(count, suffix):
|
|
||||||
return "%.2f %s%s" % (count, suffix, isuffix)
|
|
||||||
|
|
||||||
if s < 1024: # 1000-1023 get emitted as bytes, even in SI mode
|
|
||||||
return "%d B" % s
|
|
||||||
if s < U*U:
|
|
||||||
return r(s/U, "k")
|
|
||||||
if s < U*U*U:
|
|
||||||
return r(s/(U*U), "M")
|
|
||||||
if s < U*U*U*U:
|
|
||||||
return r(s/(U*U*U), "G")
|
|
||||||
if s < U*U*U*U*U:
|
|
||||||
return r(s/(U*U*U*U), "T")
|
|
||||||
if s < U*U*U*U*U*U:
|
|
||||||
return r(s/(U*U*U*U*U), "P")
|
|
||||||
return r(s/(U*U*U*U*U*U), "E")
|
|
||||||
|
|
||||||
def print_event(event):
|
def print_event(event):
|
||||||
event_type, started, result, total_bytes, waiting_time, total_time = event
|
event_type, started, result, total_bytes, waiting_time, total_time = event
|
||||||
|
@ -49,7 +25,7 @@ def print_event(event):
|
||||||
abbrev(total_time),
|
abbrev(total_time),
|
||||||
abbrev(waiting_time),
|
abbrev(waiting_time),
|
||||||
abbrev(followthrough),
|
abbrev(followthrough),
|
||||||
abbreviate_space(total_bytes),
|
sizeof_fmt_iec(total_bytes),
|
||||||
time.ctime(started),
|
time.ctime(started),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
@ -108,8 +84,8 @@ def show_usage(args):
|
||||||
print(" %d events in %s (%.2f per hour)" % (total, abbrev(elapsed),
|
print(" %d events in %s (%.2f per hour)" % (total, abbrev(elapsed),
|
||||||
(3600 * total / elapsed)))
|
(3600 * total / elapsed)))
|
||||||
rate = total_transit_bytes / elapsed
|
rate = total_transit_bytes / elapsed
|
||||||
print(" %s total bytes, %sps" % (abbreviate_space(total_transit_bytes),
|
print(" %s total bytes, %sps" % (sizeof_fmt_iec(total_transit_bytes),
|
||||||
abbreviate_space(rate)))
|
sizeof_fmt_iec(rate)))
|
||||||
print("", ", ".join(["%s=%d (%d%%)" %
|
print("", ", ".join(["%s=%d (%d%%)" %
|
||||||
(k, counters[k], (100.0 * counters[k] / total))
|
(k, counters[k], (100.0 * counters[k] / total))
|
||||||
for k in sorted(counters)
|
for k in sorted(counters)
|
||||||
|
|
|
@ -9,6 +9,7 @@ from .. import __version__
|
||||||
from .common import ServerBase, config
|
from .common import ServerBase, config
|
||||||
from ..cli import cmd_send, cmd_receive
|
from ..cli import cmd_send, cmd_receive
|
||||||
from ..errors import TransferError, WrongPasswordError, WelcomeError
|
from ..errors import TransferError, WrongPasswordError, WelcomeError
|
||||||
|
from ..util import sizeof_fmt_iec
|
||||||
|
|
||||||
|
|
||||||
def build_offer(args):
|
def build_offer(args):
|
||||||
|
@ -376,8 +377,9 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
|
||||||
NL=NL)
|
NL=NL)
|
||||||
self.failUnlessEqual(send_stdout, expected)
|
self.failUnlessEqual(send_stdout, expected)
|
||||||
elif mode == "file":
|
elif mode == "file":
|
||||||
self.failUnlessIn("Sending {bytes:d} byte file named '{name}'{NL}"
|
self.failUnlessIn("Sending {size:s} file named '{name}'{NL}"
|
||||||
.format(bytes=len(message), name=send_filename,
|
.format(size=sizeof_fmt_iec(len(message)),
|
||||||
|
name=send_filename,
|
||||||
NL=NL), send_stdout)
|
NL=NL), send_stdout)
|
||||||
self.failUnlessIn("On the other computer, please run: "
|
self.failUnlessIn("On the other computer, please run: "
|
||||||
"wormhole receive{NL}"
|
"wormhole receive{NL}"
|
||||||
|
@ -402,8 +404,8 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
|
||||||
if mode == "text":
|
if mode == "text":
|
||||||
self.failUnlessEqual(receive_stdout, message+NL)
|
self.failUnlessEqual(receive_stdout, message+NL)
|
||||||
elif mode == "file":
|
elif mode == "file":
|
||||||
self.failUnlessIn("Receiving file ({bytes:d} bytes) into: {name}"
|
self.failUnlessIn("Receiving file ({size:s}) into: {name}"
|
||||||
.format(bytes=len(message),
|
.format(size=sizeof_fmt_iec(len(message)),
|
||||||
name=receive_filename), receive_stdout)
|
name=receive_filename), receive_stdout)
|
||||||
self.failUnlessIn("Received file written to ", receive_stdout)
|
self.failUnlessIn("Received file written to ", receive_stdout)
|
||||||
fn = os.path.join(receive_dir, receive_filename)
|
fn = os.path.join(receive_dir, receive_filename)
|
||||||
|
@ -411,7 +413,7 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
|
||||||
with open(fn, "r") as f:
|
with open(fn, "r") as f:
|
||||||
self.failUnlessEqual(f.read(), message)
|
self.failUnlessEqual(f.read(), message)
|
||||||
elif mode == "directory":
|
elif mode == "directory":
|
||||||
want = (r"Receiving directory \(\d+ bytes\) into: {name}/"
|
want = (r"Receiving directory \(\d+ \w+\) into: {name}/"
|
||||||
.format(name=receive_dirname))
|
.format(name=receive_dirname))
|
||||||
self.failUnless(re.search(want, receive_stdout),
|
self.failUnless(re.search(want, receive_stdout),
|
||||||
(want, receive_stdout))
|
(want, receive_stdout))
|
||||||
|
@ -511,8 +513,9 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
|
||||||
(receive_stdout, receive_stderr))
|
(receive_stdout, receive_stderr))
|
||||||
|
|
||||||
# check sender
|
# check sender
|
||||||
self.failUnlessIn("Sending {bytes:d} byte file named '{name}'{NL}"
|
self.failUnlessIn("Sending {size:s} file named '{name}'{NL}"
|
||||||
.format(bytes=len(message), name=send_filename,
|
.format(size=sizeof_fmt_iec(len(message)),
|
||||||
|
name=send_filename,
|
||||||
NL=NL), send_stdout)
|
NL=NL), send_stdout)
|
||||||
self.failUnlessIn("On the other computer, please run: "
|
self.failUnlessIn("On the other computer, please run: "
|
||||||
"wormhole receive{NL}"
|
"wormhole receive{NL}"
|
||||||
|
|
|
@ -38,3 +38,31 @@ class Utils(unittest.TestCase):
|
||||||
d = util.bytes_to_dict(b)
|
d = util.bytes_to_dict(b)
|
||||||
self.assertIsInstance(d, dict)
|
self.assertIsInstance(d, dict)
|
||||||
self.assertEqual(d, {"a": "b", "c": 2})
|
self.assertEqual(d, {"a": "b", "c": 2})
|
||||||
|
|
||||||
|
def test_size_fmt_decimal(self):
|
||||||
|
"""test the size formatting routines"""
|
||||||
|
si_size_map = {
|
||||||
|
0: '0 B', # no rounding necessary for those
|
||||||
|
1: '1 B',
|
||||||
|
142: '142 B',
|
||||||
|
999: '999 B',
|
||||||
|
1000: '1.00 kB', # rounding starts here
|
||||||
|
1001: '1.00 kB', # should be rounded away
|
||||||
|
1234: '1.23 kB', # should be rounded down
|
||||||
|
1235: '1.24 kB', # should be rounded up
|
||||||
|
1010: '1.01 kB', # rounded down as well
|
||||||
|
999990000: '999.99 MB', # rounded down
|
||||||
|
999990001: '999.99 MB', # rounded down
|
||||||
|
999995000: '1.00 GB', # rounded up to next unit
|
||||||
|
10**6: '1.00 MB', # and all the remaining units, megabytes
|
||||||
|
10**9: '1.00 GB', # gigabytes
|
||||||
|
10**12: '1.00 TB', # terabytes
|
||||||
|
10**15: '1.00 PB', # petabytes
|
||||||
|
10**18: '1.00 EB', # exabytes
|
||||||
|
10**21: '1.00 ZB', # zottabytes
|
||||||
|
10**24: '1.00 YB', # yottabytes
|
||||||
|
-1: '-1 B', # negative value
|
||||||
|
-1010: '-1.01 kB', # negative value with rounding
|
||||||
|
}
|
||||||
|
for size, fmt in si_size_map.items():
|
||||||
|
self.assertEqual(util.sizeof_fmt_decimal(size), fmt)
|
||||||
|
|
|
@ -24,3 +24,22 @@ def bytes_to_dict(b):
|
||||||
d = json.loads(b.decode("utf-8"))
|
d = json.loads(b.decode("utf-8"))
|
||||||
assert isinstance(d, dict)
|
assert isinstance(d, dict)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def sizeof_fmt(num, suffix='B', units=None, power=None, sep=' ', precision=2):
|
||||||
|
for unit in units[:-1]:
|
||||||
|
if abs(round(num, precision)) < power:
|
||||||
|
if isinstance(num, int):
|
||||||
|
return "{}{}{}{}".format(num, sep, unit, suffix)
|
||||||
|
else:
|
||||||
|
return "{:3.{}f}{}{}{}".format(num, precision, sep, unit, suffix)
|
||||||
|
num /= float(power)
|
||||||
|
return "{:.{}f}{}{}{}".format(num, precision, sep, units[-1], suffix)
|
||||||
|
|
||||||
|
|
||||||
|
def sizeof_fmt_iec(num, suffix='B', sep=' ', precision=2):
|
||||||
|
return sizeof_fmt(num, suffix=suffix, sep=sep, precision=precision, units=['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'], power=1024)
|
||||||
|
|
||||||
|
|
||||||
|
def sizeof_fmt_decimal(num, suffix='B', sep=' ', precision=2):
|
||||||
|
return sizeof_fmt(num, suffix=suffix, sep=sep, precision=precision, units=['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'], power=1000)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user