""" Ported to Python 3. """ import os from sys import stdout as _sys_stdout from urllib.parse import urlencode import json from .common import BaseOptions from allmydata.scripts.common import get_default_nodedir from allmydata.scripts.common_http import BadResponse from allmydata.util.abbreviate import abbreviate_space, abbreviate_time from allmydata.util.encodingutil import argv_to_abspath _print = print def print(*args, **kwargs): """ Builtin ``print``-alike that will even write unicode which cannot be encoded using the specified output file's encoding. This differs from the builtin print in that it will use the "replace" encoding error handler and then write the result whereas builtin print uses the "strict" encoding error handler. """ out = kwargs.pop("file", None) if out is None: out = _sys_stdout encoding = out.encoding or "ascii" def ensafe(o): if isinstance(o, str): return o.encode(encoding, errors="replace").decode(encoding) return o return _print( *(ensafe(a) for a in args), file=out, **kwargs ) def _get_request_parameters_for_fragment(options, fragment, method, post_args): """ Get parameters for ``do_http`` for requesting the given fragment. :return dict: A dictionary suitable for use as keyword arguments to ``do_http``. """ nodeurl = options['node-url'] if nodeurl.endswith('/'): nodeurl = nodeurl[:-1] url = u'%s/%s' % (nodeurl, fragment) if method == 'POST': if post_args is None: raise ValueError("Must pass post_args= for POST method") body = urlencode(post_args) else: body = '' if post_args is not None: raise ValueError("post_args= only valid for POST method") return dict( method=method, url=url, body=body.encode("utf-8"), ) def _handle_response_for_fragment(resp, nodeurl): """ Inspect an HTTP response and return the parsed payload, if possible. """ if isinstance(resp, BadResponse): # specifically NOT using format_http_error() here because the # URL is pretty sensitive (we're doing /uri/). raise RuntimeError( "Failed to get json from '%s': %s" % (nodeurl, resp.error) ) data = resp.read() parsed = json.loads(data) if parsed is None: raise RuntimeError("No data from '%s'" % (nodeurl,)) return parsed def pretty_progress(percent, size=10, output_ascii=False): """ Displays a unicode or ascii based progress bar of a certain length. Should we just depend on a library instead? (Originally from txtorcon) """ curr = int(percent / 100.0 * size) part = (percent / (100.0 / size)) - curr if output_ascii: part = int(part * 4) part = '.oO%'[part] block_chr = '#' else: block_chr = u'\u2588' # there are 8 unicode characters for vertical-bars/horiz-bars part = int(part * 8) # unicode 0x2581 -> 2589 are vertical bar chunks, like rainbarf uses # and following are narrow -> wider bars part = chr(0x258f - part) # for smooth bar # part = chr(0x2581 + part) # for neater-looking thing # hack for 100+ full so we don't print extra really-narrow/high bar if percent >= 100.0: part = '' curr = int(curr) return '%s%s%s' % ((block_chr * curr), part, (' ' * (size - curr - 1))) OP_MAP = { 'upload': ' put ', 'download': ' get ', 'retrieve': 'retr ', 'publish': ' pub ', 'mapupdate': 'mapup', 'unknown': ' ??? ', } def _render_active_upload(op): total = ( op['progress-hash'] + op['progress-ciphertext'] + op['progress-encode-push'] ) / 3.0 * 100.0 return { u"op_type": u" put ", u"total": "{:3.0f}".format(total), u"progress_bar": u"{}".format(pretty_progress(total, size=15)), u"storage-index-string": op["storage-index-string"], u"status": op["status"], } def _render_active_download(op): return { u"op_type": u" get ", u"total": op["progress"], u"progress_bar": u"{}".format(pretty_progress(op['progress'] * 100.0, size=15)), u"storage-index-string": op["storage-index-string"], u"status": op["status"], } def _render_active_generic(op): return { u"op_type": OP_MAP[op["type"]], u"progress_bar": u"", u"total": u"???", u"storage-index-string": op["storage-index-string"], u"status": op["status"], } active_renderers = { "upload": _render_active_upload, "download": _render_active_download, "publish": _render_active_generic, "retrieve": _render_active_generic, "mapupdate": _render_active_generic, "unknown": _render_active_generic, } def render_active(stdout, status_data): active = status_data.get('active', None) if not active: print(u"No active operations.", file=stdout) return header = u"\u2553 {:<5} \u2565 {:<26} \u2565 {:<22} \u2565 {}".format( "type", "storage index", "progress", "status message", ) header_bar = u"\u255f\u2500{}\u2500\u256b\u2500{}\u2500\u256b\u2500{}\u2500\u256b\u2500{}".format( u'\u2500' * 5, u'\u2500' * 26, u'\u2500' * 22, u'\u2500' * 20, ) line_template = ( u"\u2551 {op_type} " u"\u2551 {storage-index-string} " u"\u2551 {progress_bar:15} " u"({total}%) " u"\u2551 {status}" ) footer_bar = u"\u2559\u2500{}\u2500\u2568\u2500{}\u2500\u2568\u2500{}\u2500\u2568\u2500{}".format( u'\u2500' * 5, u'\u2500' * 26, u'\u2500' * 22, u'\u2500' * 20, ) print(u"Active operations:", file=stdout) print(header, file=stdout) print(header_bar, file=stdout) for op in active: print(line_template.format( **active_renderers[op["type"]](op) )) print(footer_bar, file=stdout) def _render_recent_generic(op): return { u"op_type": OP_MAP[op["type"]], u"storage-index-string": op["storage-index-string"], u"nice_size": abbreviate_space(op["total-size"]), u"status": op["status"], } def _render_recent_mapupdate(op): return { u"op_type": u"mapup", u"storage-index-string": op["storage-index-string"], u"nice_size": op["mode"], u"status": op["status"], } recent_renderers = { "upload": _render_recent_generic, "download": _render_recent_generic, "publish": _render_recent_generic, "retrieve": _render_recent_generic, "mapupdate": _render_recent_mapupdate, "unknown": _render_recent_generic, } def render_recent(verbose, stdout, status_data): recent = status_data.get('recent', None) if not recent: print(u"No recent operations.", file=stdout) header = u"\u2553 {:<5} \u2565 {:<26} \u2565 {:<10} \u2565 {}".format( "type", "storage index", "size", "status message", ) line_template = ( u"\u2551 {op_type} " u"\u2551 {storage-index-string} " u"\u2551 {nice_size:<10} " u"\u2551 {status}" ) footer = u"\u2559\u2500{}\u2500\u2568\u2500{}\u2500\u2568\u2500{}\u2500\u2568\u2500{}".format( u'\u2500' * 5, u'\u2500' * 26, u'\u2500' * 10, u'\u2500' * 20, ) non_verbose_ops = ('upload', 'download') recent = [op for op in status_data['recent'] if op['type'] in non_verbose_ops] print(u"\nRecent operations:", file=stdout) if len(recent) or verbose: print(header, file=stdout) ops_to_show = status_data['recent'] if verbose else recent for op in ops_to_show: print(line_template.format( **recent_renderers[op["type"]](op) )) if len(recent) or verbose: print(footer, file=stdout) skipped = len(status_data['recent']) - len(ops_to_show) if not verbose and skipped: print(u" Skipped {} non-upload/download operations; use --verbose to see".format(skipped), file=stdout) def do_status(options, do_http=None): if do_http is None: from allmydata.scripts.common_http import do_http nodedir = options["node-directory"] with open(os.path.join(nodedir, u'private', u'api_auth_token'), 'r') as f: token = f.read().strip() with open(os.path.join(nodedir, u'node.url'), 'r') as f: options['node-url'] = f.read().strip() # do *all* our data-retrievals first in case there's an error try: status_data = _handle_response_for_fragment( do_http(**_get_request_parameters_for_fragment( options, 'status?t=json', method='POST', post_args=dict( t='json', token=token, ), )), options['node-url'], ) statistics_data = _handle_response_for_fragment( do_http(**_get_request_parameters_for_fragment( options, 'statistics?t=json', method='POST', post_args=dict( t='json', token=token, ), )), options['node-url'], ) except Exception as e: print(u"failed to retrieve data: %s" % str(e), file=options.stderr) return 2 downloaded_bytes = statistics_data['counters'].get('downloader.bytes_downloaded', 0) downloaded_files = statistics_data['counters'].get('downloader.files_downloaded', 0) uploaded_bytes = statistics_data['counters'].get('uploader.bytes_uploaded', 0) uploaded_files = statistics_data['counters'].get('uploader.files_uploaded', 0) print(u"Statistics (for last {}):".format(abbreviate_time(statistics_data['stats']['node.uptime'])), file=options.stdout) print(u" uploaded {} in {} files".format(abbreviate_space(uploaded_bytes), uploaded_files), file=options.stdout) print(u" downloaded {} in {} files".format(abbreviate_space(downloaded_bytes), downloaded_files), file=options.stdout) print(u"", file=options.stdout) render_active(options.stdout, status_data) render_recent(options['verbose'], options.stdout, status_data) # open question: should we return non-zero if there were no # operations at all to display? return 0 class TahoeStatusCommand(BaseOptions): optFlags = [ ["verbose", "v", "Include publish, retrieve, mapupdate in ops"], ] def postOptions(self): if self.parent['node-directory']: self['node-directory'] = argv_to_abspath(self.parent['node-directory']) else: self['node-directory'] = get_default_nodedir() def getSynopsis(self): return "Usage: tahoe [global-options] status [options]" def getUsage(self, width=None): t = BaseOptions.getUsage(self, width) t += "Various status information" return t subCommands = [ ["status", None, TahoeStatusCommand, "Status."], ]