import os
+import urllib
+from sys import stderr
from types import NoneType
from cStringIO import StringIO
+from datetime import datetime
+
+import humanize
+import simplejson
from twisted.python import usage
import tahoe_mv
from allmydata.util.encodingutil import argv_to_abspath, argv_to_unicode, to_str, \
quote_local_unicode_path
+from allmydata.scripts.common_http import do_http, format_http_success, \
+ format_http_error, BadResponse
from allmydata.util import fileutil
from allmydata.util import configutil
from allmydata import uri
+
INVITE_SEPARATOR = "+"
class CreateOptions(BasedirOptions):
nickname = None
synopsis = ""
stdin = StringIO("")
+
def parseArgs(self):
BasedirOptions.parseArgs(self)
node_url_file = os.path.join(self['node-directory'], u"node.url")
- self['node-url'] = open(node_url_file, "r").read().strip()
+ with open(node_url_file, "r") as f:
+ self['node-url'] = f.read().strip()
+
+
+def _get_json_for_fragment(options, fragment):
+ nodeurl = options['node-url']
+ if nodeurl.endswith('/'):
+ nodeurl = nodeurl[:-1]
+
+ url = u'%s/%s' % (nodeurl, fragment)
+ resp = do_http(method, url)
+ if isinstance(resp, BadResponse):
+ # specifically NOT using format_http_error() here because the
+ # URL is pretty sensitive (we're doing /uri/<key>).
+ raise RuntimeError(
+ "Failed to get json from '%s': %s" % (nodeurl, resp.error)
+ )
+
+ data = resp.read()
+ parsed = simplejson.loads(data)
+ if not parsed:
+ raise RuntimeError("No data from '%s'" % (nodeurl,))
+ return parsed
+
+
+def _get_json_for_cap(options, cap):
+ return _get_json_for_fragment(
+ options,
+ 'uri/%s?t=json' % urllib.quote(cap),
+ )
+
+def _print_item_status(item, now, longest):
+ paddedname = (' ' * (longest - len(item['path']))) + item['path']
+ if 'failure_at' in item:
+ ts = datetime.fromtimestamp(item['started_at'])
+ prog = 'Failed %s (%s)' % (humanize.naturaltime(now - ts), ts)
+ elif item['percent_done'] < 100.0:
+ if 'started_at' not in item:
+ prog = 'not yet started'
+ else:
+ so_far = now - datetime.fromtimestamp(item['started_at'])
+ if so_far.seconds > 0.0:
+ rate = item['percent_done'] / so_far.seconds
+ if rate != 0:
+ time_left = (100.0 - item['percent_done']) / rate
+ prog = '%2.1f%% done, around %s left' % (
+ item['percent_done'],
+ humanize.naturaldelta(time_left),
+ )
+ else:
+ time_left = None
+ prog = '%2.1f%% done' % (item['percent_done'],)
+ else:
+ prog = 'just started'
+ else:
+ prog = ''
+ for verb in ['finished', 'started', 'queued']:
+ keyname = verb + '_at'
+ if keyname in item:
+ when = datetime.fromtimestamp(item[keyname])
+ prog = '%s %s' % (verb, humanize.naturaltime(now - when))
+ break
+
+ print " %s: %s" % (paddedname, prog)
def status(options):
- # XXX todo: use http interface to ask about our magic-folder upload status
+ nodedir = options["node-directory"]
+ with open(os.path.join(nodedir, u"private", u"magic_folder_dircap")) as f:
+ dmd_cap = f.read().strip()
+ with open(os.path.join(nodedir, u"private", u"collective_dircap")) as f:
+ collective_readcap = f.read().strip()
+
+ try:
+ captype, dmd = _get_json_for_cap(options, dmd_cap)
+ if captype != 'dirnode':
+ print >>stderr, "magic_folder_dircap isn't a directory capability"
+ return 2
+ except RuntimeError as e:
+ print >>stderr, str(e)
+ return 1
+
+ now = datetime.now()
+
+ print "Local files:"
+ for (name, child) in dmd['children'].items():
+ captype, meta = child
+ status = 'good'
+ size = meta['size']
+ created = datetime.fromtimestamp(meta['metadata']['tahoe']['linkcrtime'])
+ version = meta['metadata']['version']
+ nice_size = humanize.naturalsize(size)
+ nice_created = humanize.naturaltime(now - created)
+ if captype != 'filenode':
+ print "%20s: error, should be a filecap" % name
+ continue
+ print " %s (%s): %s, version=%s, created %s" % (name, nice_size, status, version, nice_created)
+
+ captype, collective = _get_json_for_cap(options, collective_readcap)
+ print
+ print "Remote files:"
+ for (name, data) in collective['children'].items():
+ if data[0] != 'dirnode':
+ print "Error: '%s': expected a dirnode, not '%s'" % (name, data[0])
+ print " %s's remote:" % name
+ dmd = _get_json_for_cap(options, data[1]['ro_uri'])
+ if dmd[0] != 'dirnode':
+ print "Error: should be a dirnode"
+ continue
+ for (n, d) in dmd[1]['children'].items():
+ if d[0] != 'filenode':
+ print "Error: expected '%s' to be a filenode." % (n,)
+
+ meta = d[1]
+ status = 'good'
+ size = meta['size']
+ created = datetime.fromtimestamp(meta['metadata']['tahoe']['linkcrtime'])
+ version = meta['metadata']['version']
+ nice_size = humanize.naturalsize(size)
+ nice_created = humanize.naturaltime(now - created)
+ print " %s (%s): %s, version=%s, created %s" % (n, nice_size, status, version, nice_created)
+
+ magicdata = _get_json_for_fragment(options, 'magic_folder?t=json')
+ if len(magicdata):
+ uploads = [item for item in magicdata if item['kind'] == 'upload']
+ downloads = [item for item in magicdata if item['kind'] == 'download']
+ longest = max([len(item['path']) for item in magicdata])
+
+ if True: # maybe --show-completed option or something?
+ uploads = [item for item in uploads if item['status'] != 'success']
+ downloads = [item for item in downloads if item['status'] != 'success']
+
+ if len(uploads):
+ print
+ print "Uploads:"
+ for item in uploads:
+ _print_item_status(item, now, longest)
+
+ if len(downloads):
+ print
+ print "Downloads:"
+ for item in downloads:
+ _print_item_status(item, now, longest)
+
+ for item in magicdata:
+ if item['status'] == 'failure':
+ print "Failed:", item
+
return 0
+
class MagicFolderCommand(BaseOptions):
subCommands = [
["create", None, CreateOptions, "Create a Magic Folder."],
["invite", None, InviteOptions, "Invite someone to a Magic Folder."],
["join", None, JoinOptions, "Join a Magic Folder."],
["leave", None, LeaveOptions, "Leave a Magic Folder."],
+ ["status", None, StatusOptions, "Display stutus of uploads/downloads."],
]
def postOptions(self):
if not hasattr(self, 'subOptions'):
"invite": invite,
"join": join,
"leave": leave,
+ "status": status,
}
def do_magic_folder(options):