]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blobdiff - src/allmydata/web/directory.py
wui: use standard time format (#1077)
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / web / directory.py
index 5c45d960baa687cf5dbd848210959aa4386af220..29b57620ba2048aa8d3ad55f7802b1878cc0492c 100644 (file)
@@ -1,58 +1,71 @@
 
 import simplejson
 import urllib
-import time
 
+from zope.interface import implements
 from twisted.internet import defer
+from twisted.internet.interfaces import IPushProducer
 from twisted.python.failure import Failure
-from twisted.web import http, html
-from nevow import url, rend, tags as T
+from twisted.web import http
+from nevow import url, rend, inevow, tags as T
 from nevow.inevow import IRequest
 
-from foolscap.eventual import fireEventually
+from foolscap.api import fireEventually
 
 from allmydata.util import base32
-from allmydata.uri import from_string_verifier, from_string_dirnode, \
-     CHKFileVerifierURI
-from allmydata.interfaces import IDirectoryNode, IFileNode, IMutableFileNode, \
-     ExistingChildError
-from allmydata.web.common import text_plain, WebError, IClient, \
-     boolean_of_arg, get_arg, should_create_intermediate_directories, \
-     getxmlfile, RenderMixin
+from allmydata.util.encodingutil import to_str
+from allmydata.uri import from_string_dirnode
+from allmydata.interfaces import IDirectoryNode, IFileNode, IFilesystemNode, \
+     IImmutableFileNode, IMutableFileNode, ExistingChildError, \
+     NoSuchChildError, EmptyPathnameComponentError, SDMF_VERSION, MDMF_VERSION
+from allmydata.blacklist import ProhibitedNode
+from allmydata.monitor import Monitor, OperationCancelledError
+from allmydata import dirnode
+from allmydata.web.common import text_plain, WebError, \
+     IOpHandleTable, NeedOperationHandleError, \
+     boolean_of_arg, get_arg, get_root, parse_replace_arg, \
+     should_create_intermediate_directories, \
+     getxmlfile, RenderMixin, humanize_failure, convert_children_json, \
+     get_format, get_mutable_type, get_filenode_metadata, render_time
 from allmydata.web.filenode import ReplaceMeMixin, \
      FileNodeHandler, PlaceHolderNodeHandler
-from allmydata.web.checker_results import CheckerResults, \
-     CheckAndRepairResults, DeepCheckResults, DeepCheckAndRepairResults
+from allmydata.web.check_results import CheckResultsRenderer, \
+     CheckAndRepairResultsRenderer, DeepCheckResultsRenderer, \
+     DeepCheckAndRepairResultsRenderer, LiteralCheckResultsRenderer
+from allmydata.web.info import MoreInfo
+from allmydata.web.operations import ReloadMixin
+from allmydata.web.check_results import json_check_results, \
+     json_check_and_repair_results
 
 class BlockingFileError(Exception):
     # TODO: catch and transform
     """We cannot auto-create a parent directory, because there is a file in
     the way"""
 
-def make_handler_for(node, parentnode=None, name=None):
+def make_handler_for(node, client, parentnode=None, name=None):
     if parentnode:
         assert IDirectoryNode.providedBy(parentnode)
     if IFileNode.providedBy(node):
-        return FileNodeHandler(node, parentnode, name)
-    if IMutableFileNode.providedBy(node):
-        return FileNodeHandler(node, parentnode, name)
+        return FileNodeHandler(client, node, parentnode, name)
     if IDirectoryNode.providedBy(node):
-        return DirectoryNodeHandler(node, parentnode, name)
-    raise WebError("Cannot provide handler for '%s'" % node)
+        return DirectoryNodeHandler(client, node, parentnode, name)
+    return UnknownNodeHandler(client, node, parentnode, name)
 
 class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
     addSlash = True
 
-    def __init__(self, node, parentnode=None, name=None):
+    def __init__(self, client, node, parentnode=None, name=None):
         rend.Page.__init__(self)
+        self.client = client
         assert node
         self.node = node
         self.parentnode = parentnode
         self.name = name
 
     def childFactory(self, ctx, name):
-        req = IRequest(ctx)
         name = name.decode("utf-8")
+        if not name:
+            raise EmptyPathnameComponentError()
         d = self.node.get(name)
         d.addBoth(self.got_child, ctx, name)
         # got_child returns a handler resource: FileNodeHandler or
@@ -68,7 +81,7 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
         t = get_arg(req, "t", "").strip()
         if isinstance(node_or_failure, Failure):
             f = node_or_failure
-            f.trap(KeyError)
+            f.trap(NoSuchChildError)
             # No child by this name. What should we do about it?
             if DEBUG: print "no child", name
             if DEBUG: print "postpath", req.postpath
@@ -77,17 +90,35 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
                 if should_create_intermediate_directories(req):
                     # create intermediate directories
                     if DEBUG: print " making intermediate directory"
-                    d = self.node.create_empty_directory(name)
-                    d.addCallback(make_handler_for, self.node, name)
+                    d = self.node.create_subdirectory(name)
+                    d.addCallback(make_handler_for,
+                                  self.client, self.node, name)
                     return d
             else:
                 if DEBUG: print " terminal"
                 # terminal node
-                if (method,t) in [ ("POST","mkdir"), ("PUT","mkdir") ]:
+                if (method,t) in [ ("POST","mkdir"), ("PUT","mkdir"),
+                                   ("POST", "mkdir-with-children"),
+                                   ("POST", "mkdir-immutable") ]:
                     if DEBUG: print " making final directory"
                     # final directory
-                    d = self.node.create_empty_directory(name)
-                    d.addCallback(make_handler_for, self.node, name)
+                    kids = {}
+                    if t in ("mkdir-with-children", "mkdir-immutable"):
+                        req.content.seek(0)
+                        kids_json = req.content.read()
+                        kids = convert_children_json(self.client.nodemaker,
+                                                     kids_json)
+                    file_format = get_format(req, None)
+                    mutable = True
+                    mt = get_mutable_type(file_format)
+                    if t == "mkdir-immutable":
+                        mutable = False
+
+                    d = self.node.create_subdirectory(name, kids,
+                                                      mutable=mutable,
+                                                      mutable_version=mt)
+                    d.addCallback(make_handler_for,
+                                  self.client, self.node, name)
                     return d
                 if (method,t) in ( ("PUT",""), ("PUT","uri"), ):
                     if DEBUG: print " PUT, making leaf placeholder"
@@ -96,10 +127,10 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
                     # since that's the leaf node that we're about to create.
                     # We make a dummy one, which will respond to the PUT
                     # request by replacing itself.
-                    return PlaceHolderNodeHandler(self.node, name)
+                    return PlaceHolderNodeHandler(self.client, self.node, name)
             if DEBUG: print " 404"
             # otherwise, we just return a no-such-child error
-            return rend.FourOhFour()
+            return f
 
         node = node_or_failure
         if nonterminal and should_create_intermediate_directories(req):
@@ -111,7 +142,7 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
                                "a file was in the way" % name,
                                http.CONFLICT)
         if DEBUG: print "good child"
-        return make_handler_for(node, self.node, name)
+        return make_handler_for(node, self.client, self.node, name)
 
     def render_DELETE(self, ctx):
         assert self.parentnode and self.name
@@ -120,27 +151,32 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
         return d
 
     def render_GET(self, ctx):
-        client = IClient(ctx)
         req = IRequest(ctx)
         # This is where all of the directory-related ?t=* code goes.
         t = get_arg(req, "t", "").strip()
+
+        # t=info contains variable ophandles, t=rename-form contains the name
+        # of the child being renamed. Neither is allowed an ETag.
+        FIXED_OUTPUT_TYPES =  ["", "json", "uri", "readonly-uri"]
+        if not self.node.is_mutable() and t in FIXED_OUTPUT_TYPES:
+            si = self.node.get_storage_index()
+            if si and req.setETag('DIR:%s-%s' % (base32.b2a(si), t or "")):
+                return ""
+
         if not t:
             # render the directory as HTML, using the docFactory and Nevow's
             # whole templating thing.
-            return DirectoryAsHTML(self.node)
+            return DirectoryAsHTML(self.node,
+                                   self.client.mutable_file_default)
 
         if t == "json":
             return DirectoryJSONMetadata(ctx, self.node)
+        if t == "info":
+            return MoreInfo(self.node)
         if t == "uri":
             return DirectoryURI(ctx, self.node)
         if t == "readonly-uri":
             return DirectoryReadonlyURI(ctx, self.node)
-        if t == "manifest":
-            return Manifest(self.node)
-        if t == "deep-size":
-            return DeepSize(ctx, self.node)
-        if t == "deep-stats":
-            return DeepStats(ctx, self.node)
         if t == 'rename-form':
             return RenameForm(self.node)
 
@@ -149,7 +185,8 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
     def render_PUT(self, ctx):
         req = IRequest(ctx)
         t = get_arg(req, "t", "").strip()
-        replace = boolean_of_arg(get_arg(req, "replace", "true"))
+        replace = parse_replace_arg(get_arg(req, "replace", "true"))
+
         if t == "mkdir":
             # our job was done by the traversal/create-intermediate-directory
             # process that got us here.
@@ -159,7 +196,7 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
                 # they're trying to set_uri and that name is already occupied
                 # (by us).
                 raise ExistingChildError()
-            d = self.parentnode.replace_me_with_a_childcap(ctx, replace)
+            d = self.replace_me_with_a_childcap(req, self.client, replace)
             # TODO: results
             return d
 
@@ -168,25 +205,38 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
     def render_POST(self, ctx):
         req = IRequest(ctx)
         t = get_arg(req, "t", "").strip()
+
         if t == "mkdir":
             d = self._POST_mkdir(req)
-        elif t == "mkdir-p":
-            # TODO: docs, tests
-            d = self._POST_mkdir_p(req)
+        elif t == "mkdir-with-children":
+            d = self._POST_mkdir_with_children(req)
+        elif t == "mkdir-immutable":
+            d = self._POST_mkdir_immutable(req)
         elif t == "upload":
             d = self._POST_upload(ctx) # this one needs the context
         elif t == "uri":
             d = self._POST_uri(req)
-        elif t == "delete":
-            d = self._POST_delete(req)
+        elif t == "delete" or t == "unlink":
+            d = self._POST_unlink(req)
         elif t == "rename":
             d = self._POST_rename(req)
+        elif t == "relink":
+            d = self._POST_relink(req)
         elif t == "check":
             d = self._POST_check(req)
-        elif t == "deep-check":
-            d = self._POST_deep_check(req)
-        elif t == "set_children":
-            # TODO: docs
+        elif t == "start-deep-check":
+            d = self._POST_start_deep_check(ctx)
+        elif t == "stream-deep-check":
+            d = self._POST_stream_deep_check(ctx)
+        elif t == "start-manifest":
+            d = self._POST_start_manifest(ctx)
+        elif t == "start-deep-size":
+            d = self._POST_start_deep_size(ctx)
+        elif t == "start-deep-stats":
+            d = self._POST_start_deep_stats(ctx)
+        elif t == "stream-manifest":
+            d = self._POST_stream_manifest(ctx)
+        elif t == "set_children" or t == "set-children":
             d = self._POST_set_children(req)
         else:
             raise WebError("POST to a directory with bad t=%s" % t)
@@ -204,34 +254,45 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
             return defer.succeed(self.node.get_uri()) # TODO: urlencode
         name = name.decode("utf-8")
         replace = boolean_of_arg(get_arg(req, "replace", "true"))
-        d = self.node.create_empty_directory(name, overwrite=replace)
+        kids = {}
+        mt = get_mutable_type(get_format(req, None))
+        d = self.node.create_subdirectory(name, kids, overwrite=replace,
+                                          mutable_version=mt)
         d.addCallback(lambda child: child.get_uri()) # TODO: urlencode
         return d
 
-    def _POST_mkdir_p(self, req):
-        path = get_arg(req, "path")
-        if not path:
-            raise WebError("mkdir-p requires a path")
-        path_ = tuple([seg.decode("utf-8") for seg in path.split('/') if seg ])
-        # TODO: replace
-        d = self._get_or_create_directories(self.node, path_)
-        d.addCallback(lambda node: node.get_uri())
+    def _POST_mkdir_with_children(self, req):
+        name = get_arg(req, "name", "")
+        if not name:
+            # our job is done, it was handled by the code in got_child
+            # which created the final directory (i.e. us)
+            return defer.succeed(self.node.get_uri()) # TODO: urlencode
+        name = name.decode("utf-8")
+        # TODO: decide on replace= behavior, see #903
+        #replace = boolean_of_arg(get_arg(req, "replace", "false"))
+        req.content.seek(0)
+        kids_json = req.content.read()
+        kids = convert_children_json(self.client.nodemaker, kids_json)
+        mt = get_mutable_type(get_format(req, None))
+        d = self.node.create_subdirectory(name, kids, overwrite=False,
+                                          mutable_version=mt)
+        d.addCallback(lambda child: child.get_uri()) # TODO: urlencode
         return d
 
-    def _get_or_create_directories(self, node, path):
-        if not IDirectoryNode.providedBy(node):
-            # unfortunately it is too late to provide the name of the
-            # blocking directory in the error message.
-            raise BlockingFileError("cannot create directory because there "
-                                    "is a file in the way")
-        if not path:
-            return defer.succeed(node)
-        d = node.get(path[0])
-        def _maybe_create(f):
-            f.trap(KeyError)
-            return node.create_empty_directory(path[0])
-        d.addErrback(_maybe_create)
-        d.addCallback(self._get_or_create_directories, path[1:])
+    def _POST_mkdir_immutable(self, req):
+        name = get_arg(req, "name", "")
+        if not name:
+            # our job is done, it was handled by the code in got_child
+            # which created the final directory (i.e. us)
+            return defer.succeed(self.node.get_uri()) # TODO: urlencode
+        name = name.decode("utf-8")
+        # TODO: decide on replace= behavior, see #903
+        #replace = boolean_of_arg(get_arg(req, "replace", "false"))
+        req.content.seek(0)
+        kids_json = req.content.read()
+        kids = convert_children_json(self.client.nodemaker, kids_json)
+        d = self.node.create_subdirectory(name, kids, overwrite=False, mutable=False)
+        d.addCallback(lambda child: child.get_uri()) # TODO: urlencode
         return d
 
     def _POST_upload(self, ctx):
@@ -261,12 +322,12 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
         def _maybe_got_node(node_or_failure):
             if isinstance(node_or_failure, Failure):
                 f = node_or_failure
-                f.trap(KeyError)
+                f.trap(NoSuchChildError)
                 # create a placeholder which will see POST t=upload
-                return PlaceHolderNodeHandler(self.node, name)
+                return PlaceHolderNodeHandler(self.client, self.node, name)
             else:
                 node = node_or_failure
-                return make_handler_for(node, self.node, name)
+                return make_handler_for(node, self.client, self.node, name)
         d.addBoth(_maybe_got_node)
         # now we have a placeholder or a filenodehandler, and we can just
         # delegate to it. We could return the resource back out of
@@ -284,79 +345,207 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
             raise WebError("set-uri requires a name")
         charset = get_arg(req, "_charset", "utf-8")
         name = name.decode(charset)
-        replace = boolean_of_arg(get_arg(req, "replace", "true"))
-        d = self.node.set_uri(name, childcap, overwrite=replace)
+        replace = parse_replace_arg(get_arg(req, "replace", "true"))
+
+        # We mustn't pass childcap for the readcap argument because we don't
+        # know whether it is a read cap. Passing a read cap as the writecap
+        # argument will work (it ends up calling NodeMaker.create_from_cap,
+        # which derives a readcap if necessary and possible).
+        d = self.node.set_uri(name, childcap, None, overwrite=replace)
         d.addCallback(lambda res: childcap)
         return d
 
-    def _POST_delete(self, req):
+    def _POST_unlink(self, req):
         name = get_arg(req, "name")
         if name is None:
             # apparently an <input type="hidden" name="name" value="">
             # won't show up in the resulting encoded form.. the 'name'
-            # field is completely missing. So to allow deletion of an
-            # empty file, we have to pretend that None means ''. The only
-            # downide of this is a slightly confusing error message if
-            # someone does a POST without a name= field. For our own HTML
-            # thisn't a big deal, because we create the 'delete' POST
-            # buttons ourselves.
+            # field is completely missing. So to allow unlinking of a
+            # child with a name that is the empty string, we have to
+            # pretend that None means ''. The only downside of this is
+            # a slightly confusing error message if someone does a POST
+            # without a name= field. For our own HTML this isn't a big
+            # deal, because we create the 'unlink' POST buttons ourselves.
             name = ''
         charset = get_arg(req, "_charset", "utf-8")
         name = name.decode(charset)
         d = self.node.delete(name)
-        d.addCallback(lambda res: "thing deleted")
+        d.addCallback(lambda res: "thing unlinked")
         return d
 
     def _POST_rename(self, req):
+        # rename is identical to relink, but to_dir is not allowed
+        # and to_name is required.
+        if get_arg(req, "to_dir") is not None:
+            raise WebError("to_dir= is not valid for rename")
+        if get_arg(req, "to_name") is None:
+            raise WebError("to_name= is required for rename")
+        return self._POST_relink(req)
+
+    def _POST_relink(self, req):
         charset = get_arg(req, "_charset", "utf-8")
+        replace = parse_replace_arg(get_arg(req, "replace", "true"))
+
         from_name = get_arg(req, "from_name")
         if from_name is not None:
             from_name = from_name.strip()
             from_name = from_name.decode(charset)
             assert isinstance(from_name, unicode)
+        else:
+            raise WebError("from_name= is required")
+
         to_name = get_arg(req, "to_name")
         if to_name is not None:
             to_name = to_name.strip()
             to_name = to_name.decode(charset)
             assert isinstance(to_name, unicode)
-        if not from_name or not to_name:
-            raise WebError("rename requires from_name and to_name")
-        for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
-            if v and "/" in v:
-                raise WebError("%s= may not contain a slash" % k,
-                               http.BAD_REQUEST)
+        else:
+            to_name = from_name
+
+        # Disallow slashes in both from_name and to_name, that would only
+        # cause confusion.
+        if "/" in from_name:
+            raise WebError("from_name= may not contain a slash",
+                           http.BAD_REQUEST)
+        if "/" in to_name:
+            raise WebError("to_name= may not contain a slash",
+                           http.BAD_REQUEST)
+
+        to_dir = get_arg(req, "to_dir")
+        if to_dir is not None and to_dir != self.node.get_write_uri():
+            to_dir = to_dir.strip()
+            to_dir = to_dir.decode(charset)
+            assert isinstance(to_dir, unicode)
+            to_path = to_dir.split(u"/")
+            to_root = self.client.nodemaker.create_from_cap(to_str(to_path[0]))
+            if not IDirectoryNode.providedBy(to_root):
+                raise WebError("to_dir is not a directory", http.BAD_REQUEST)
+            d = to_root.get_child_at_path(to_path[1:])
+        else:
+            d = defer.succeed(self.node)
 
-        replace = boolean_of_arg(get_arg(req, "replace", "true"))
-        d = self.node.move_child_to(from_name, self.node, to_name, replace)
-        d.addCallback(lambda res: "thing renamed")
+        def _got_new_parent(new_parent):
+            if not IDirectoryNode.providedBy(new_parent):
+                raise WebError("to_dir is not a directory", http.BAD_REQUEST)
+
+            return self.node.move_child_to(from_name, new_parent,
+                                           to_name, replace)
+        d.addCallback(_got_new_parent)
+        d.addCallback(lambda res: "thing moved")
         return d
 
+    def _maybe_literal(self, res, Results_Class):
+        if res:
+            return Results_Class(self.client, res)
+        return LiteralCheckResultsRenderer(self.client)
+
     def _POST_check(self, req):
         # check this directory
         verify = boolean_of_arg(get_arg(req, "verify", "false"))
         repair = boolean_of_arg(get_arg(req, "repair", "false"))
+        add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
         if repair:
-            d = self.node.check_and_repair(verify)
-            d.addCallback(lambda res: CheckAndRepairResults(res))
+            d = self.node.check_and_repair(Monitor(), verify, add_lease)
+            d.addCallback(self._maybe_literal, CheckAndRepairResultsRenderer)
         else:
-            d = self.node.check(verify)
-            d.addCallback(lambda res: CheckerResults(res))
+            d = self.node.check(Monitor(), verify, add_lease)
+            d.addCallback(self._maybe_literal, CheckResultsRenderer)
         return d
 
-    def _POST_deep_check(self, req):
+    def _start_operation(self, monitor, renderer, ctx):
+        table = IOpHandleTable(ctx)
+        table.add_monitor(ctx, monitor, renderer)
+        return table.redirect_to(ctx)
+
+    def _POST_start_deep_check(self, ctx):
         # check this directory and everything reachable from it
-        verify = boolean_of_arg(get_arg(req, "verify", "false"))
-        repair = boolean_of_arg(get_arg(req, "repair", "false"))
+        if not get_arg(ctx, "ophandle"):
+            raise NeedOperationHandleError("slow operation requires ophandle=")
+        verify = boolean_of_arg(get_arg(ctx, "verify", "false"))
+        repair = boolean_of_arg(get_arg(ctx, "repair", "false"))
+        add_lease = boolean_of_arg(get_arg(ctx, "add-lease", "false"))
         if repair:
-            d = self.node.deep_check_and_repair(verify)
-            d.addCallback(lambda res: DeepCheckAndRepairResults(res))
+            monitor = self.node.start_deep_check_and_repair(verify, add_lease)
+            renderer = DeepCheckAndRepairResultsRenderer(self.client, monitor)
         else:
-            d = self.node.deep_check(verify)
-            d.addCallback(lambda res: DeepCheckResults(res))
+            monitor = self.node.start_deep_check(verify, add_lease)
+            renderer = DeepCheckResultsRenderer(self.client, monitor)
+        return self._start_operation(monitor, renderer, ctx)
+
+    def _POST_stream_deep_check(self, ctx):
+        verify = boolean_of_arg(get_arg(ctx, "verify", "false"))
+        repair = boolean_of_arg(get_arg(ctx, "repair", "false"))
+        add_lease = boolean_of_arg(get_arg(ctx, "add-lease", "false"))
+        walker = DeepCheckStreamer(ctx, self.node, verify, repair, add_lease)
+        monitor = self.node.deep_traverse(walker)
+        walker.setMonitor(monitor)
+        # register to hear stopProducing. The walker ignores pauseProducing.
+        IRequest(ctx).registerProducer(walker, True)
+        d = monitor.when_done()
+        def _done(res):
+            IRequest(ctx).unregisterProducer()
+            return res
+        d.addBoth(_done)
+        def _cancelled(f):
+            f.trap(OperationCancelledError)
+            return "Operation Cancelled"
+        d.addErrback(_cancelled)
+        def _error(f):
+            # signal the error as a non-JSON "ERROR:" line, plus exception
+            msg = "ERROR: %s(%s)\n" % (f.value.__class__.__name__,
+                                       ", ".join([str(a) for a in f.value.args]))
+            msg += str(f)
+            return msg
+        d.addErrback(_error)
+        return d
+
+    def _POST_start_manifest(self, ctx):
+        if not get_arg(ctx, "ophandle"):
+            raise NeedOperationHandleError("slow operation requires ophandle=")
+        monitor = self.node.build_manifest()
+        renderer = ManifestResults(self.client, monitor)
+        return self._start_operation(monitor, renderer, ctx)
+
+    def _POST_start_deep_size(self, ctx):
+        if not get_arg(ctx, "ophandle"):
+            raise NeedOperationHandleError("slow operation requires ophandle=")
+        monitor = self.node.start_deep_stats()
+        renderer = DeepSizeResults(self.client, monitor)
+        return self._start_operation(monitor, renderer, ctx)
+
+    def _POST_start_deep_stats(self, ctx):
+        if not get_arg(ctx, "ophandle"):
+            raise NeedOperationHandleError("slow operation requires ophandle=")
+        monitor = self.node.start_deep_stats()
+        renderer = DeepStatsResults(self.client, monitor)
+        return self._start_operation(monitor, renderer, ctx)
+
+    def _POST_stream_manifest(self, ctx):
+        walker = ManifestStreamer(ctx, self.node)
+        monitor = self.node.deep_traverse(walker)
+        walker.setMonitor(monitor)
+        # register to hear stopProducing. The walker ignores pauseProducing.
+        IRequest(ctx).registerProducer(walker, True)
+        d = monitor.when_done()
+        def _done(res):
+            IRequest(ctx).unregisterProducer()
+            return res
+        d.addBoth(_done)
+        def _cancelled(f):
+            f.trap(OperationCancelledError)
+            return "Operation Cancelled"
+        d.addErrback(_cancelled)
+        def _error(f):
+            # signal the error as a non-JSON "ERROR:" line, plus exception
+            msg = "ERROR: %s(%s)\n" % (f.value.__class__.__name__,
+                                       ", ".join([str(a) for a in f.value.args]))
+            msg += str(f)
+            return msg
+        d.addErrback(_error)
         return d
 
     def _POST_set_children(self, req):
-        replace = boolean_of_arg(get_arg(req, "replace", "true"))
+        replace = parse_replace_arg(get_arg(req, "replace", "true"))
         req.content.seek(0)
         body = req.content.read()
         try:
@@ -365,10 +554,16 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
             le.args = tuple(le.args + (body,))
             # TODO test handling of bad JSON
             raise
-        cs = []
+        cs = {}
         for name, (file_or_dir, mddict) in children.iteritems():
-            cap = str(mddict.get('rw_uri') or mddict.get('ro_uri'))
-            cs.append((name, cap, mddict.get('metadata')))
+            name = unicode(name) # simplejson-2.0.1 returns str *or* unicode
+            writecap = mddict.get('rw_uri')
+            if writecap is not None:
+                writecap = str(writecap)
+            readcap = mddict.get('ro_uri')
+            if readcap is not None:
+                readcap = str(readcap)
+            cs[name] = (writecap, readcap, mddict.get('metadata'))
         d = self.node.set_children(cs, replace)
         d.addCallback(lambda res: "Okay so I did it.")
         # TODO: results
@@ -376,9 +571,9 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
 
 def abbreviated_dirnode(dirnode):
     u = from_string_dirnode(dirnode.get_uri())
-    si = u.get_filenode_uri().storage_index
-    si_s = base32.b2a(si)
-    return si_s[:6]
+    return u.abbrev_si()
+
+SPACE = u"\u00A0"*2
 
 class DirectoryAsHTML(rend.Page):
     # The remainder of this class is to render the directory into
@@ -386,37 +581,18 @@ class DirectoryAsHTML(rend.Page):
     docFactory = getxmlfile("directory.xhtml")
     addSlash = True
 
-    def __init__(self, node):
+    def __init__(self, node, default_mutable_format):
         rend.Page.__init__(self)
         self.node = node
 
-    def render_title(self, ctx, data):
-        si_s = abbreviated_dirnode(self.node)
-        header = ["Directory SI=%s" % si_s]
-        return ctx.tag[header]
-
-    def render_header(self, ctx, data):
-        si_s = abbreviated_dirnode(self.node)
-        header = ["Directory SI=%s" % si_s]
-        if self.node.is_readonly():
-            header.append(" (readonly)")
-        return ctx.tag[header]
-
-    def get_root(self, ctx):
-        req = IRequest(ctx)
-        # the addSlash=True gives us one extra (empty) segment
-        depth = len(req.prepath) + len(req.postpath) - 1
-        link = "/".join([".."] * depth)
-        return link
-
-    def render_welcome(self, ctx, data):
-        link = self.get_root(ctx)
-        return T.div[T.a(href=link)["Return to Welcome page"]]
+        assert default_mutable_format in (MDMF_VERSION, SDMF_VERSION)
+        self.default_mutable_format = default_mutable_format
 
-    def data_children(self, ctx, data):
+    def beforeRender(self, ctx):
+        # attempt to get the dirnode's children, stashing them (or the
+        # failure that results) for later use
         d = self.node.list()
-        d.addCallback(lambda dict: sorted(dict.items()))
-        def _stall_some(items):
+        def _good(children):
             # Deferreds don't optimize out tail recursion, and the way
             # Nevow's flattener handles Deferreds doesn't take this into
             # account. As a result, large lists of Deferreds that fire in the
@@ -428,268 +604,316 @@ class DirectoryAsHTML(rend.Page):
             # inefficient. This addresses ticket #237, for which I was never
             # able to create a failing unit test.
             output = []
-            for i,item in enumerate(items):
+            for i,item in enumerate(sorted(children.items())):
                 if i % 100 == 0:
                     output.append(fireEventually(item))
                 else:
                     output.append(item)
-            return output
-        d.addCallback(_stall_some)
+            self.dirnode_children = output
+            return ctx
+        def _bad(f):
+            text, code = humanize_failure(f)
+            self.dirnode_children = None
+            self.dirnode_children_error = text
+            return ctx
+        d.addCallbacks(_good, _bad)
         return d
 
+    def render_title(self, ctx, data):
+        si_s = abbreviated_dirnode(self.node)
+        header = ["Tahoe-LAFS - Directory SI=%s" % si_s]
+        if self.node.is_unknown():
+            header.append(" (unknown)")
+        elif not self.node.is_mutable():
+            header.append(" (immutable)")
+        elif self.node.is_readonly():
+            header.append(" (read-only)")
+        else:
+            header.append(" (modifiable)")
+        return ctx.tag[header]
+
+    def render_header(self, ctx, data):
+        si_s = abbreviated_dirnode(self.node)
+        header = ["Tahoe-LAFS Directory SI=", T.span(class_="data-chars")[si_s]]
+        if self.node.is_unknown():
+            header.append(" (unknown)")
+        elif not self.node.is_mutable():
+            header.append(" (immutable)")
+        elif self.node.is_readonly():
+            header.append(" (read-only)")
+        return ctx.tag[header]
+
+    def render_welcome(self, ctx, data):
+        link = get_root(ctx)
+        return ctx.tag[T.a(href=link)["Return to Welcome page"]]
+
+    def render_show_readonly(self, ctx, data):
+        if self.node.is_unknown() or self.node.is_readonly():
+            return ""
+        rocap = self.node.get_readonly_uri()
+        root = get_root(ctx)
+        uri_link = "%s/uri/%s/" % (root, urllib.quote(rocap))
+        return ctx.tag[T.a(href=uri_link)["Read-Only Version"]]
+
+    def render_try_children(self, ctx, data):
+        # if the dirnode can be retrived, render a table of children.
+        # Otherwise, render an apologetic error message.
+        if self.dirnode_children is not None:
+            return ctx.tag
+        else:
+            return T.div[T.p["Error reading directory:"],
+                         T.p[self.dirnode_children_error]]
+
+    def data_children(self, ctx, data):
+        return self.dirnode_children
+
     def render_row(self, ctx, data):
         name, (target, metadata) = data
         name = name.encode("utf-8")
         assert not isinstance(name, unicode)
+        nameurl = urllib.quote(name, safe="") # encode any slashes too
 
-        root = self.get_root(ctx)
+        root = get_root(ctx)
         here = "%s/uri/%s/" % (root, urllib.quote(self.node.get_uri()))
-        if self.node.is_readonly():
-            delete = "-"
+        if self.node.is_unknown() or self.node.is_readonly():
+            unlink = "-"
             rename = "-"
         else:
-            # this creates a button which will cause our child__delete method
-            # to be invoked, which deletes the file and then redirects the
+            # this creates a button which will cause our _POST_unlink method
+            # to be invoked, which unlinks the file and then redirects the
             # browser back to this directory
-            delete = T.form(action=here, method="post")[
-                T.input(type='hidden', name='t', value='delete'),
+            unlink = T.form(action=here, method="post")[
+                T.input(type='hidden', name='t', value='unlink'),
                 T.input(type='hidden', name='name', value=name),
                 T.input(type='hidden', name='when_done', value="."),
-                T.input(type='submit', value='del', name="del"),
+                T.input(type='submit', _class='btn', value='unlink', name="unlink"),
                 ]
 
             rename = T.form(action=here, method="get")[
                 T.input(type='hidden', name='t', value='rename-form'),
                 T.input(type='hidden', name='name', value=name),
                 T.input(type='hidden', name='when_done', value="."),
-                T.input(type='submit', value='rename', name="rename"),
+                T.input(type='submit', _class='btn', value='rename/relink', name="rename"),
                 ]
 
-        ctx.fillSlots("delete", delete)
+        ctx.fillSlots("unlink", unlink)
         ctx.fillSlots("rename", rename)
-        if IDirectoryNode.providedBy(target):
-            check_url = "%s/uri/%s/" % (root, urllib.quote(target.get_uri()))
-            check_done_url = "../../uri/%s/" % urllib.quote(self.node.get_uri())
-        else:
-            check_url = "%s/uri/%s" % (root, urllib.quote(target.get_uri()))
-            check_done_url = "../uri/%s/" % urllib.quote(self.node.get_uri())
-        check = T.form(action=check_url, method="post")[
-            T.input(type='hidden', name='t', value='check'),
-            T.input(type='hidden', name='return_to', value=check_done_url),
-            T.input(type='submit', value='check', name="check"),
-            ]
-        ctx.fillSlots("overwrite",
-                      self.build_overwrite_form(ctx, name, target))
-        ctx.fillSlots("check", check)
 
         times = []
-        TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
-        if "ctime" in metadata:
-            ctime = time.strftime(TIME_FORMAT,
-                                  time.localtime(metadata["ctime"]))
-            times.append("c: " + ctime)
-        if "mtime" in metadata:
-            mtime = time.strftime(TIME_FORMAT,
-                                  time.localtime(metadata["mtime"]))
+        linkcrtime = metadata.get('tahoe', {}).get("linkcrtime")
+        if linkcrtime is not None:
+            times.append("lcr: " + render_time(linkcrtime))
+        else:
+            # For backwards-compatibility with links last modified by Tahoe < 1.4.0:
+            if "ctime" in metadata:
+                ctime = render_time(metadata["ctime"])
+                times.append("c: " + ctime)
+        linkmotime = metadata.get('tahoe', {}).get("linkmotime")
+        if linkmotime is not None:
             if times:
                 times.append(T.br())
+            times.append("lmo: " + render_time(linkmotime))
+        else:
+            # For backwards-compatibility with links last modified by Tahoe < 1.4.0:
+            if "mtime" in metadata:
+                mtime = render_time(metadata["mtime"])
+                if times:
+                    times.append(T.br())
                 times.append("m: " + mtime)
         ctx.fillSlots("times", times)
 
-        assert (IFileNode.providedBy(target)
-                or IDirectoryNode.providedBy(target)
-                or IMutableFileNode.providedBy(target)), target
-
-        quoted_uri = urllib.quote(target.get_uri())
+        assert IFilesystemNode.providedBy(target), target
+        target_uri = target.get_uri() or ""
+        quoted_uri = urllib.quote(target_uri, safe="") # escape slashes too
 
         if IMutableFileNode.providedBy(target):
             # to prevent javascript in displayed .html files from stealing a
             # secret directory URI from the URL, send the browser to a URI-based
             # page that doesn't know about the directory at all
-            dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, urllib.quote(name))
+            dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, nameurl)
 
-            ctx.fillSlots("filename",
-                          T.a(href=dlurl)[html.escape(name)])
+            ctx.fillSlots("filename", T.a(href=dlurl)[name])
             ctx.fillSlots("type", "SSK")
 
             ctx.fillSlots("size", "?")
 
-            text_plain_url = "%s/file/%s/@@named=/foo.txt" % (root, quoted_uri)
-            text_plain_tag = T.a(href=text_plain_url)["text/plain"]
+            info_link = "%s/uri/%s?t=info" % (root, quoted_uri)
 
-        elif IFileNode.providedBy(target):
-            dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, urllib.quote(name))
+        elif IImmutableFileNode.providedBy(target):
+            dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, nameurl)
 
-            ctx.fillSlots("filename",
-                          T.a(href=dlurl)[html.escape(name)])
+            ctx.fillSlots("filename", T.a(href=dlurl)[name])
             ctx.fillSlots("type", "FILE")
 
             ctx.fillSlots("size", target.get_size())
 
-            text_plain_url = "%s/file/%s/@@named=/foo.txt" % (root, quoted_uri)
-            text_plain_tag = T.a(href=text_plain_url)["text/plain"]
-
+            info_link = "%s/uri/%s?t=info" % (root, quoted_uri)
 
         elif IDirectoryNode.providedBy(target):
             # directory
-            uri_link = "%s/uri/%s/" % (root, urllib.quote(target.get_uri()))
-            ctx.fillSlots("filename",
-                          T.a(href=uri_link)[html.escape(name)])
-            if target.is_readonly():
+            uri_link = "%s/uri/%s/" % (root, urllib.quote(target_uri))
+            ctx.fillSlots("filename", T.a(href=uri_link)[name])
+            if not target.is_mutable():
+                dirtype = "DIR-IMM"
+            elif target.is_readonly():
                 dirtype = "DIR-RO"
             else:
                 dirtype = "DIR"
             ctx.fillSlots("type", dirtype)
             ctx.fillSlots("size", "-")
-            text_plain_tag = None
+            info_link = "%s/uri/%s/?t=info" % (root, quoted_uri)
 
-        childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ",
-                     T.a(href="%s?t=uri" % name)["URI"], ", ",
-                     T.a(href="%s?t=readonly-uri" % name)["readonly-URI"],
-                     ]
-        if text_plain_tag:
-            childdata.extend([", ", text_plain_tag])
+        elif isinstance(target, ProhibitedNode):
+            ctx.fillSlots("filename", T.strike[name])
+            if IDirectoryNode.providedBy(target.wrapped_node):
+                blacklisted_type = "DIR-BLACKLISTED"
+            else:
+                blacklisted_type = "BLACKLISTED"
+            ctx.fillSlots("type", blacklisted_type)
+            ctx.fillSlots("size", "-")
+            info_link = None
+            ctx.fillSlots("info", ["Access Prohibited:", T.br, target.reason])
 
-        ctx.fillSlots("data", childdata)
+        else:
+            # unknown
+            ctx.fillSlots("filename", name)
+            if target.get_write_uri() is not None:
+                unknowntype = "?"
+            elif not self.node.is_mutable() or target.is_alleged_immutable():
+                unknowntype = "?-IMM"
+            else:
+                unknowntype = "?-RO"
+            ctx.fillSlots("type", unknowntype)
+            ctx.fillSlots("size", "-")
+            # use a directory-relative info link, so we can extract both the
+            # writecap and the readcap
+            info_link = "%s?t=info" % urllib.quote(name)
 
-        results = "--"
-        # TODO: include a link to see more results, including timestamps
-        # TODO: use a sparkline
-        ctx.fillSlots("checker_results", results)
+        if info_link:
+            ctx.fillSlots("info", T.a(href=info_link)["More Info"])
 
         return ctx.tag
 
+    # XXX: similar to render_upload_form and render_mkdir_form in root.py.
     def render_forms(self, ctx, data):
         forms = []
-        deep_check = T.form(action=".", method="post",
-                            enctype="multipart/form-data")[
-            T.fieldset[
-            T.input(type="hidden", name="t", value="deep-check"),
-            T.input(type="hidden", name="return_to", value="."),
-            T.legend(class_="freeform-form-label")["Run a deep-check operation (EXPENSIVE)"],
-            T.input(type="submit", value="Deep-Check"),
-            " ",
-            "Verify every bit? (EVEN MORE EXPENSIVE):",
-            T.input(type="checkbox", name="verify"),
-            ]]
-        forms.append(T.div(class_="freeform-form")[deep_check])
 
         if self.node.is_readonly():
-            forms.append(T.div["No upload forms: directory is read-only"])
-            return forms
+            return T.div["No upload forms: directory is read-only"]
+        if self.dirnode_children is None:
+            return T.div["No upload forms: directory is unreadable"]
+
+        mkdir_sdmf = T.input(type='radio', name='format',
+                             value='sdmf', id='mkdir-sdmf',
+                             checked='checked')
+        mkdir_mdmf = T.input(type='radio', name='format',
+                             value='mdmf', id='mkdir-mdmf')
 
-        mkdir = T.form(action=".", method="post",
-                       enctype="multipart/form-data")[
+        mkdir_form = T.form(action=".", method="post",
+                            enctype="multipart/form-data")[
             T.fieldset[
             T.input(type="hidden", name="t", value="mkdir"),
             T.input(type="hidden", name="when_done", value="."),
-            T.legend(class_="freeform-form-label")["Create a new directory"],
-            "New directory name: ",
-            T.input(type="text", name="name"), " ",
-            T.input(type="submit", value="Create"),
+            T.legend(class_="freeform-form-label")["Create a new directory in this directory"],
+            "New directory name:"+SPACE, T.br,
+            T.input(type="text", name="name"), SPACE,
+            T.div(class_="form-inline")[
+                mkdir_sdmf, T.label(for_='mutable-directory-sdmf')[SPACE, "SDMF"], SPACE*2,
+                mkdir_mdmf, T.label(for_='mutable-directory-mdmf')[SPACE, "MDMF (experimental)"]
+            ],
+            T.input(type="submit", class_="btn", value="Create")
             ]]
-        forms.append(T.div(class_="freeform-form")[mkdir])
-
-        upload = T.form(action=".", method="post",
-                        enctype="multipart/form-data")[
+        forms.append(T.div(class_="freeform-form")[mkdir_form])
+
+        upload_chk  = T.input(type='radio', name='format',
+                              value='chk', id='upload-chk',
+                              checked='checked')
+        upload_sdmf = T.input(type='radio', name='format',
+                              value='sdmf', id='upload-sdmf')
+        upload_mdmf = T.input(type='radio', name='format',
+                              value='mdmf', id='upload-mdmf')
+
+        upload_form = T.form(action=".", method="post",
+                             enctype="multipart/form-data")[
             T.fieldset[
             T.input(type="hidden", name="t", value="upload"),
             T.input(type="hidden", name="when_done", value="."),
             T.legend(class_="freeform-form-label")["Upload a file to this directory"],
-            "Choose a file to upload: ",
-            T.input(type="file", name="file", class_="freeform-input-file"),
-            " ",
-            T.input(type="submit", value="Upload"),
-            " Mutable?:",
-            T.input(type="checkbox", name="mutable"),
+            "Choose a file to upload:"+SPACE,
+            T.input(type="file", name="file", class_="freeform-input-file"), SPACE,
+            T.div(class_="form-inline")[
+                upload_chk,  T.label(for_="upload-chk") [SPACE, "Immutable"], SPACE*2,
+                upload_sdmf, T.label(for_="upload-sdmf")[SPACE, "SDMF"], SPACE*2,
+                upload_mdmf, T.label(for_="upload-mdmf")[SPACE, "MDMF (experimental)"]
+            ],
+            T.input(type="submit", class_="btn", value="Upload"),             SPACE*2,
             ]]
-        forms.append(T.div(class_="freeform-form")[upload])
-
-        mount = T.form(action=".", method="post",
-                        enctype="multipart/form-data")[
-            T.fieldset[
-            T.input(type="hidden", name="t", value="uri"),
-            T.input(type="hidden", name="when_done", value="."),
-            T.legend(class_="freeform-form-label")["Attach a file or directory"
-                                                   " (by URI) to this"
-                                                   " directory"],
-            "New child name: ",
-            T.input(type="text", name="name"), " ",
-            "URI of new child: ",
-            T.input(type="text", name="uri"), " ",
-            T.input(type="submit", value="Attach"),
-            ]]
-        forms.append(T.div(class_="freeform-form")[mount])
+        forms.append(T.div(class_="freeform-form")[upload_form])
+
+        attach_form = T.form(action=".", method="post",
+                             enctype="multipart/form-data")[
+            T.fieldset[ T.div(class_="form-inline")[
+                T.input(type="hidden", name="t", value="uri"),
+                T.input(type="hidden", name="when_done", value="."),
+                T.legend(class_="freeform-form-label")["Add a link to a file or directory which is already in Tahoe-LAFS."],
+                "New child name:"+SPACE,
+                T.input(type="text", name="name"), SPACE*2, T.br,
+                "URI of new child:"+SPACE,
+                T.input(type="text", name="uri"), SPACE,
+                T.input(type="submit", class_="btn", value="Attach"),
+            ]]]
+        forms.append(T.div(class_="freeform-form")[attach_form])
         return forms
 
-    def build_overwrite_form(self, ctx, name, target):
-        if IMutableFileNode.providedBy(target) and not target.is_readonly():
-            root = self.get_root(ctx)
-            action = "%s/uri/%s" % (root, urllib.quote(target.get_uri()))
-            done_url = "../uri/%s/" % urllib.quote(self.node.get_uri())
-            overwrite = T.form(action=action, method="post",
-                               enctype="multipart/form-data")[
-                T.fieldset[
-                T.input(type="hidden", name="t", value="upload"),
-                T.input(type='hidden', name='when_done', value=done_url),
-                T.legend(class_="freeform-form-label")["Overwrite"],
-                "Choose new file: ",
-                T.input(type="file", name="file", class_="freeform-input-file"),
-                " ",
-                T.input(type="submit", value="Overwrite")
-                ]]
-            return [T.div(class_="freeform-form")[overwrite],]
-        else:
-            return []
-
     def render_results(self, ctx, data):
         req = IRequest(ctx)
         return get_arg(req, "results", "")
 
-
 def DirectoryJSONMetadata(ctx, dirnode):
     d = dirnode.list()
     def _got(children):
         kids = {}
         for name, (childnode, metadata) in children.iteritems():
-            if childnode.is_readonly():
-                rw_uri = None
-                ro_uri = childnode.get_uri()
-            else:
-                rw_uri = childnode.get_uri()
-                ro_uri = childnode.get_readonly_uri()
+            assert IFilesystemNode.providedBy(childnode), childnode
+            rw_uri = childnode.get_write_uri()
+            ro_uri = childnode.get_readonly_uri()
             if IFileNode.providedBy(childnode):
-                kiddata = ("filenode", {'size': childnode.get_size(),
-                                        'metadata': metadata,
-                                        })
+                kiddata = ("filenode", get_filenode_metadata(childnode))
+            elif IDirectoryNode.providedBy(childnode):
+                kiddata = ("dirnode", {'mutable': childnode.is_mutable()})
             else:
-                assert IDirectoryNode.providedBy(childnode), (childnode,
-                                                              children,)
-                kiddata = ("dirnode", {'metadata': metadata})
-            if ro_uri:
-                kiddata[1]["ro_uri"] = ro_uri
+                kiddata = ("unknown", {})
+
+            kiddata[1]["metadata"] = metadata
             if rw_uri:
                 kiddata[1]["rw_uri"] = rw_uri
-            kiddata[1]['mutable'] = childnode.is_mutable()
+            if ro_uri:
+                kiddata[1]["ro_uri"] = ro_uri
+            verifycap = childnode.get_verify_cap()
+            if verifycap:
+                kiddata[1]['verify_uri'] = verifycap.to_string()
+
             kids[name] = kiddata
-        if dirnode.is_readonly():
-            drw_uri = None
-            dro_uri = dirnode.get_uri()
-        else:
-            drw_uri = dirnode.get_uri()
-            dro_uri = dirnode.get_readonly_uri()
+
+        drw_uri = dirnode.get_write_uri()
+        dro_uri = dirnode.get_readonly_uri()
         contents = { 'children': kids }
         if dro_uri:
             contents['ro_uri'] = dro_uri
         if drw_uri:
             contents['rw_uri'] = drw_uri
+        verifycap = dirnode.get_verify_cap()
+        if verifycap:
+            contents['verify_uri'] = verifycap.to_string()
         contents['mutable'] = dirnode.is_mutable()
         data = ("dirnode", contents)
-        return simplejson.dumps(data, indent=1)
+        json = simplejson.dumps(data, indent=1) + "\n"
+        return json
     d.addCallback(_got)
     d.addCallback(text_plain, ctx)
     return d
 
+
 def DirectoryURI(ctx, dirnode):
     return text_plain(dirnode.get_uri(), ctx)
 
@@ -722,38 +946,312 @@ class RenameForm(rend.Page):
         ctx.tag.attributes['value'] = name
         return ctx.tag
 
-
-class Manifest(rend.Page):
+class ManifestResults(rend.Page, ReloadMixin):
     docFactory = getxmlfile("manifest.xhtml")
 
+    def __init__(self, client, monitor):
+        self.client = client
+        self.monitor = monitor
+
+    def renderHTTP(self, ctx):
+        req = inevow.IRequest(ctx)
+        output = get_arg(req, "output", "html").lower()
+        if output == "text":
+            return self.text(req)
+        if output == "json":
+            return self.json(req)
+        return rend.Page.renderHTTP(self, ctx)
+
+    def slashify_path(self, path):
+        if not path:
+            return ""
+        return "/".join([p.encode("utf-8") for p in path])
+
+    def text(self, req):
+        req.setHeader("content-type", "text/plain")
+        lines = []
+        is_finished = self.monitor.is_finished()
+        lines.append("finished: " + {True: "yes", False: "no"}[is_finished])
+        for (path, cap) in self.monitor.get_status()["manifest"]:
+            lines.append(self.slashify_path(path) + " " + cap)
+        return "\n".join(lines) + "\n"
+
+    def json(self, req):
+        req.setHeader("content-type", "text/plain")
+        m = self.monitor
+        s = m.get_status()
+
+        if m.origin_si:
+            origin_base32 = base32.b2a(m.origin_si)
+        else:
+            origin_base32 = ""
+        status = { "stats": s["stats"],
+                   "finished": m.is_finished(),
+                   "origin": origin_base32,
+                   }
+        if m.is_finished():
+            # don't return manifest/verifycaps/SIs unless the operation is
+            # done, to save on CPU/memory (both here and in the HTTP client
+            # who has to unpack the JSON). Tests show that the ManifestWalker
+            # needs about 1092 bytes per item, the JSON we generate here
+            # requires about 503 bytes per item, and some internal overhead
+            # (perhaps transport-layer buffers in twisted.web?) requires an
+            # additional 1047 bytes per item.
+            status.update({ "manifest": s["manifest"],
+                            "verifycaps": [i for i in s["verifycaps"]],
+                            "storage-index": [i for i in s["storage-index"]],
+                            })
+            # simplejson doesn't know how to serialize a set. We use a
+            # generator that walks the set rather than list(setofthing) to
+            # save a small amount of memory (4B*len) and a moderate amount of
+            # CPU.
+        return simplejson.dumps(status, indent=1)
+
+    def _si_abbrev(self):
+        si = self.monitor.origin_si
+        if not si:
+            return "<LIT>"
+        return base32.b2a(si)[:6]
+
     def render_title(self, ctx):
-        return T.title["Manifest of SI=%s" % abbreviated_dirnode(self.original)]
+        return T.title["Manifest of SI=%s" % self._si_abbrev()]
 
     def render_header(self, ctx):
-        return T.p["Manifest of SI=%s" % abbreviated_dirnode(self.original)]
+        return T.p["Manifest of SI=%s" % self._si_abbrev()]
 
     def data_items(self, ctx, data):
-        return self.original.build_manifest()
-
-    def render_row(self, ctx, refresh_cap):
-        ctx.fillSlots("refresh_capability", refresh_cap)
+        return self.monitor.get_status()["manifest"]
+
+    def render_row(self, ctx, (path, cap)):
+        ctx.fillSlots("path", self.slashify_path(path))
+        root = get_root(ctx)
+        # TODO: we need a clean consistent way to get the type of a cap string
+        if cap:
+            if cap.startswith("URI:CHK") or cap.startswith("URI:SSK"):
+                nameurl = urllib.quote(path[-1].encode("utf-8"))
+                uri_link = "%s/file/%s/@@named=/%s" % (root, urllib.quote(cap),
+                                                       nameurl)
+            else:
+                uri_link = "%s/uri/%s" % (root, urllib.quote(cap, safe=""))
+            ctx.fillSlots("cap", T.a(href=uri_link)[cap])
+        else:
+            ctx.fillSlots("cap", "")
         return ctx.tag
 
-def DeepSize(ctx, dirnode):
-    d = dirnode.build_manifest()
-    def _measure_size(manifest):
-        total = 0
-        for verifiercap in manifest:
-            u = from_string_verifier(verifiercap)
-            if isinstance(u, CHKFileVerifierURI):
-                total += u.size
-        return str(total)
-    d.addCallback(_measure_size)
-    d.addCallback(text_plain, ctx)
-    return d
+class DeepSizeResults(rend.Page):
+    def __init__(self, client, monitor):
+        self.client = client
+        self.monitor = monitor
+
+    def renderHTTP(self, ctx):
+        req = inevow.IRequest(ctx)
+        output = get_arg(req, "output", "html").lower()
+        req.setHeader("content-type", "text/plain")
+        if output == "json":
+            return self.json(req)
+        # plain text
+        is_finished = self.monitor.is_finished()
+        output = "finished: " + {True: "yes", False: "no"}[is_finished] + "\n"
+        if is_finished:
+            stats = self.monitor.get_status()
+            total = (stats.get("size-immutable-files", 0)
+                     + stats.get("size-mutable-files", 0)
+                     + stats.get("size-directories", 0))
+            output += "size: %d\n" % total
+        return output
+
+    def json(self, req):
+        status = {"finished": self.monitor.is_finished(),
+                  "size": self.monitor.get_status(),
+                  }
+        return simplejson.dumps(status)
+
+class DeepStatsResults(rend.Page):
+    def __init__(self, client, monitor):
+        self.client = client
+        self.monitor = monitor
+
+    def renderHTTP(self, ctx):
+        # JSON only
+        inevow.IRequest(ctx).setHeader("content-type", "text/plain")
+        s = self.monitor.get_status().copy()
+        s["finished"] = self.monitor.is_finished()
+        return simplejson.dumps(s, indent=1)
+
+class ManifestStreamer(dirnode.DeepStats):
+    implements(IPushProducer)
+
+    def __init__(self, ctx, origin):
+        dirnode.DeepStats.__init__(self, origin)
+        self.req = IRequest(ctx)
+
+    def setMonitor(self, monitor):
+        self.monitor = monitor
+    def pauseProducing(self):
+        pass
+    def resumeProducing(self):
+        pass
+    def stopProducing(self):
+        self.monitor.cancel()
+
+    def add_node(self, node, path):
+        dirnode.DeepStats.add_node(self, node, path)
+        d = {"path": path,
+             "cap": node.get_uri()}
+
+        if IDirectoryNode.providedBy(node):
+            d["type"] = "directory"
+        elif IFileNode.providedBy(node):
+            d["type"] = "file"
+        else:
+            d["type"] = "unknown"
+
+        v = node.get_verify_cap()
+        if v:
+            v = v.to_string()
+        d["verifycap"] = v or ""
+
+        r = node.get_repair_cap()
+        if r:
+            r = r.to_string()
+        d["repaircap"] = r or ""
+
+        si = node.get_storage_index()
+        if si:
+            si = base32.b2a(si)
+        d["storage-index"] = si or ""
+
+        j = simplejson.dumps(d, ensure_ascii=True)
+        assert "\n" not in j
+        self.req.write(j+"\n")
+
+    def finish(self):
+        stats = dirnode.DeepStats.get_results(self)
+        d = {"type": "stats",
+             "stats": stats,
+             }
+        j = simplejson.dumps(d, ensure_ascii=True)
+        assert "\n" not in j
+        self.req.write(j+"\n")
+        return ""
+
+class DeepCheckStreamer(dirnode.DeepStats):
+    implements(IPushProducer)
+
+    def __init__(self, ctx, origin, verify, repair, add_lease):
+        dirnode.DeepStats.__init__(self, origin)
+        self.req = IRequest(ctx)
+        self.verify = verify
+        self.repair = repair
+        self.add_lease = add_lease
+
+    def setMonitor(self, monitor):
+        self.monitor = monitor
+    def pauseProducing(self):
+        pass
+    def resumeProducing(self):
+        pass
+    def stopProducing(self):
+        self.monitor.cancel()
+
+    def add_node(self, node, path):
+        dirnode.DeepStats.add_node(self, node, path)
+        data = {"path": path,
+                "cap": node.get_uri()}
+
+        if IDirectoryNode.providedBy(node):
+            data["type"] = "directory"
+        elif IFileNode.providedBy(node):
+            data["type"] = "file"
+        else:
+            data["type"] = "unknown"
+
+        v = node.get_verify_cap()
+        if v:
+            v = v.to_string()
+        data["verifycap"] = v or ""
+
+        r = node.get_repair_cap()
+        if r:
+            r = r.to_string()
+        data["repaircap"] = r or ""
+
+        si = node.get_storage_index()
+        if si:
+            si = base32.b2a(si)
+        data["storage-index"] = si or ""
+
+        if self.repair:
+            d = node.check_and_repair(self.monitor, self.verify, self.add_lease)
+            d.addCallback(self.add_check_and_repair, data)
+        else:
+            d = node.check(self.monitor, self.verify, self.add_lease)
+            d.addCallback(self.add_check, data)
+        d.addCallback(self.write_line)
+        return d
 
-def DeepStats(ctx, dirnode):
-    d = dirnode.deep_stats()
-    d.addCallback(simplejson.dumps, indent=1)
-    d.addCallback(text_plain, ctx)
-    return d
+    def add_check_and_repair(self, crr, data):
+        data["check-and-repair-results"] = json_check_and_repair_results(crr)
+        return data
+
+    def add_check(self, cr, data):
+        data["check-results"] = json_check_results(cr)
+        return data
+
+    def write_line(self, data):
+        j = simplejson.dumps(data, ensure_ascii=True)
+        assert "\n" not in j
+        self.req.write(j+"\n")
+
+    def finish(self):
+        stats = dirnode.DeepStats.get_results(self)
+        d = {"type": "stats",
+             "stats": stats,
+             }
+        j = simplejson.dumps(d, ensure_ascii=True)
+        assert "\n" not in j
+        self.req.write(j+"\n")
+        return ""
+
+
+class UnknownNodeHandler(RenderMixin, rend.Page):
+    def __init__(self, client, node, parentnode=None, name=None):
+        rend.Page.__init__(self)
+        assert node
+        self.node = node
+        self.parentnode = parentnode
+        self.name = name
+
+    def render_GET(self, ctx):
+        req = IRequest(ctx)
+        t = get_arg(req, "t", "").strip()
+        if t == "info":
+            return MoreInfo(self.node)
+        if t == "json":
+            is_parent_known_immutable = self.parentnode and not self.parentnode.is_mutable()
+            if self.parentnode and self.name:
+                d = self.parentnode.get_metadata_for(self.name)
+            else:
+                d = defer.succeed(None)
+            d.addCallback(lambda md: UnknownJSONMetadata(ctx, self.node, md, is_parent_known_immutable))
+            return d
+        raise WebError("GET unknown URI type: can only do t=info and t=json, not t=%s.\n"
+                       "Using a webapi server that supports a later version of Tahoe "
+                       "may help." % t)
+
+def UnknownJSONMetadata(ctx, node, edge_metadata, is_parent_known_immutable):
+    rw_uri = node.get_write_uri()
+    ro_uri = node.get_readonly_uri()
+    data = ("unknown", {})
+    if ro_uri:
+        data[1]['ro_uri'] = ro_uri
+    if rw_uri:
+        data[1]['rw_uri'] = rw_uri
+        data[1]['mutable'] = True
+    elif is_parent_known_immutable or node.is_alleged_immutable():
+        data[1]['mutable'] = False
+    # else we don't know whether it is mutable.
+
+    if edge_metadata is not None:
+        data[1]['metadata'] = edge_metadata
+    return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)