]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/commitdiff
webish: complete rewrite, break into smaller pieces, auto-create directories, improve...
authorBrian Warner <warner@allmydata.com>
Mon, 19 May 2008 19:57:04 +0000 (12:57 -0700)
committerBrian Warner <warner@allmydata.com>
Mon, 19 May 2008 19:57:04 +0000 (12:57 -0700)
src/allmydata/web/common.py
src/allmydata/web/directory.py [new file with mode: 0644]
src/allmydata/web/filenode.py [new file with mode: 0644]
src/allmydata/web/root.py [new file with mode: 0644]
src/allmydata/web/unlinked.py
src/allmydata/webish.py

index 000fda3558845c045da93da5edd274d54c2008c9..10599269d62b42364de010c6e064f3c2a81c02c4 100644 (file)
@@ -1,7 +1,10 @@
 
+from twisted.web import http, server
 from zope.interface import Interface
-from nevow import loaders
+from nevow import loaders, appserver
+from nevow.inevow import IRequest
 from nevow.util import resource_filename
+from allmydata.interfaces import ExistingChildError
 
 class IClient(Interface):
     pass
@@ -11,6 +14,7 @@ def getxmlfile(name):
     return loaders.xmlfile(resource_filename('allmydata.web', '%s' % name))
 
 def boolean_of_arg(arg):
+    # TODO: ""
     assert arg.lower() in ("true", "t", "1", "false", "f", "0", "on", "off")
     return arg.lower() in ("true", "t", "1", "on")
 
@@ -68,3 +72,56 @@ def abbreviate_size(data):
     if r > 1000:
         return "%.1fkB" % (r/1000)
     return "%dB" % r
+
+def text_plain(text, ctx):
+    req = IRequest(ctx)
+    req.setHeader("content-type", "text/plain")
+    req.setHeader("content-length", len(text))
+    return text
+
+class WebError(Exception):
+    def __init__(self, text, code=http.BAD_REQUEST):
+        self.text = text
+        self.code = code
+
+# XXX: to make UnsupportedMethod return 501 NOT_IMPLEMENTED instead of 500
+# Internal Server Error, we either need to do that ICanHandleException trick,
+# or make sure that childFactory returns a WebErrorResource (and never an
+# actual exception). The latter is growing increasingly annoying.
+
+def should_create_intermediate_directories(req):
+    t = get_arg(req, "t", "").strip()
+    return bool(req.method in ("PUT", "POST") and
+                t not in ("delete", "rename", "rename-form", "check"))
+
+
+class MyExceptionHandler(appserver.DefaultExceptionHandler):
+    def simple(self, ctx, text, code=http.BAD_REQUEST):
+        req = IRequest(ctx)
+        req.setResponseCode(code)
+        req.setHeader("content-type", "text/plain;charset=utf-8")
+        if isinstance(text, unicode):
+            text = text.encode("utf-8")
+        req.write(text)
+        req.finishRequest(False)
+
+    def renderHTTP_exception(self, ctx, f):
+        if f.check(ExistingChildError):
+            return self.simple(ctx,
+                               "There was already a child by that "
+                               "name, and you asked me to not "
+                               "replace it.",
+                               http.CONFLICT)
+        elif f.check(WebError):
+            return self.simple(ctx, f.value.text, f.value.code)
+        elif f.check(server.UnsupportedMethod):
+            # twisted.web.server.Request.render() has support for transforming
+            # this into an appropriate 501 NOT_IMPLEMENTED or 405 NOT_ALLOWED
+            # return code, but nevow does not.
+            req = IRequest(ctx)
+            method = req.method
+            return self.simple(ctx,
+                               "I don't know how to treat a %s request." % method,
+                               http.NOT_IMPLEMENTED)
+        super = appserver.DefaultExceptionHandler
+        return super.renderHTTP_exception(self, ctx, f)
diff --git a/src/allmydata/web/directory.py b/src/allmydata/web/directory.py
new file mode 100644 (file)
index 0000000..a16af78
--- /dev/null
@@ -0,0 +1,740 @@
+
+import simplejson
+import urllib
+import time
+
+from twisted.internet import defer
+from twisted.python.failure import Failure
+from twisted.web import http, html
+from nevow import url, rend, tags as T
+from nevow.inevow import IRequest
+
+from foolscap.eventual import fireEventually
+
+from allmydata.util import log, 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
+from allmydata.web.filenode import ReplaceMeMixin, \
+     FileNodeHandler, PlaceHolderNodeHandler
+
+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):
+    if parentnode:
+        assert IDirectoryNode.providedBy(parentnode)
+    if IFileNode.providedBy(node):
+        return FileNodeHandler(node, parentnode, name)
+    if IMutableFileNode.providedBy(node):
+        return FileNodeHandler(node, parentnode, name)
+    if IDirectoryNode.providedBy(node):
+        return DirectoryNodeHandler(node, parentnode, name)
+    raise WebError("Cannot provide handler for '%s'" % node)
+
+class DirectoryNodeHandler(rend.Page, ReplaceMeMixin):
+    addSlash = True
+
+    def __init__(self, node, parentnode=None, name=None):
+        rend.Page.__init__(self)
+        assert node
+        self.node = node
+        self.parentnode = parentnode
+        self.name = name
+
+    def childFactory(self, ctx, name):
+        req = IRequest(ctx)
+        name = name.decode("utf-8")
+        d = self.node.get(name)
+        d.addBoth(self.got_child, ctx, name)
+        # got_child returns a handler resource: FileNodeHandler or
+        # DirectoryNodeHandler
+        return d
+
+    def got_child(self, node_or_failure, ctx, name):
+        DEBUG = False
+        if DEBUG: print "GOT_CHILD", name, node_or_failure
+        req = IRequest(ctx)
+        method = req.method
+        nonterminal = len(req.postpath) > 1
+        t = get_arg(req, "t", "").strip()
+        if isinstance(node_or_failure, Failure):
+            f = node_or_failure
+            f.trap(KeyError)
+            # No child by this name. What should we do about it?
+            if DEBUG: print "no child", name
+            if DEBUG: print "postpath", req.postpath
+            if nonterminal:
+                if DEBUG: print " intermediate"
+                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)
+                    return d
+            else:
+                if DEBUG: print " terminal"
+                # terminal node
+                if (method,t) in [ ("POST","mkdir"), ("PUT","mkdir") ]:
+                    if DEBUG: print " making final directory"
+                    # final directory
+                    d = self.node.create_empty_directory(name)
+                    d.addCallback(make_handler_for, self.node, name)
+                    return d
+                if (method,t) in ( ("PUT",""), ("PUT","uri"), ):
+                    if DEBUG: print " PUT, making leaf placeholder"
+                    # we were trying to find the leaf filenode (to put a new
+                    # file in its place), and it didn't exist. That's ok,
+                    # 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)
+            if DEBUG: print " 404"
+            # otherwise, we just return a no-such-child error
+            return rend.FourOhFour()
+
+        node = node_or_failure
+        if nonterminal and should_create_intermediate_directories(req):
+            if not IDirectoryNode.providedBy(node):
+                # we would have put a new directory here, but there was a
+                # file in the way.
+                if DEBUG: print "blocking"
+                raise WebError("Unable to create directory '%s': "
+                               "a file was in the way" % name,
+                               http.CONFLICT)
+        if DEBUG: print "good child"
+        return make_handler_for(node, self.node, name)
+
+    def renderHTTP(self, ctx):
+        # This is where all of the ?t=* actions are implemented.
+        request = IRequest(ctx)
+
+        # if we were using regular twisted.web Resources (and the regular
+        # twisted.web.server.Request object) then we could implement
+        # render_PUT and render_GET. But Nevow's request handler
+        # (NevowRequest.gotPageContext) goes directly to renderHTTP. Copy
+        # some code from the Resource.render method that Nevow bypasses, to
+        # do the same thing.
+        m = getattr(self, 'render_' + request.method, None)
+        if not m:
+            from twisted.web.server import UnsupportedMethod
+            raise UnsupportedMethod(getattr(self, 'allowedMethods', ()))
+        return m(ctx)
+
+    def render_DELETE(self, ctx):
+        assert self.parentnode and self.name
+        d = self.parentnode.delete(self.name)
+        d.addCallback(lambda res: self.node.get_uri())
+        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()
+        if not t:
+            # render the directory as HTML, using the docFactory and Nevow's
+            # whole templating thing.
+            return DirectoryAsHTML(self.node)
+
+        if t == "json":
+            return DirectoryJSONMetadata(ctx, 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)
+
+        raise WebError("GET directory: bad t=%s" % t)
+
+    def render_PUT(self, ctx):
+        req = IRequest(ctx)
+        t = get_arg(req, "t", "").strip()
+        replace = boolean_of_arg(get_arg(req, "replace", "true"))
+        if t == "mkdir":
+            # our job was done by the traversal/create-intermediate-directory
+            # process that got us here.
+            return text_plain(self.node.get_uri(), ctx) # TODO: urlencode
+        if t == "uri":
+            if not replace:
+                # 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)
+            # TODO: results
+            return d
+
+        raise WebError("PUT to a directory")
+
+    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 == "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 == "rename":
+            d = self._POST_rename(req)
+        elif t == "check":
+            d = self._POST_check(req)
+        elif t == "set_children":
+            # TODO: docs
+            d = self._POST_set_children(req)
+        else:
+            raise WebError("POST to a directory with bad t=%s" % t)
+
+        when_done = get_arg(req, "when_done", None)
+        if when_done:
+            d.addCallback(lambda res: url.URL.fromString(when_done))
+        return d
+
+    def _POST_mkdir(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")
+        replace = boolean_of_arg(get_arg(req, "replace", "true"))
+        d = self.node.create_empty_directory(name, overwrite=replace)
+        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())
+        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:])
+        return d
+
+    def _POST_upload(self, ctx):
+        req = IRequest(ctx)
+        charset = get_arg(req, "_charset", "utf-8")
+        contents = req.fields["file"]
+        name = get_arg(req, "name")
+        name = name or contents.filename
+        if name is not None:
+            name = name.strip()
+        if not name:
+            # this prohibts empty, missing, and all-whitespace filenames
+            raise WebError("upload requires a name")
+        name = name.decode(charset)
+        if "/" in name:
+            raise WebError("name= may not contain a slash", http.BAD_REQUEST)
+        assert isinstance(name, unicode)
+
+        # since POST /uri/path/file?t=upload is equivalent to
+        # POST /uri/path/dir?t=upload&name=foo, just do the same thing that
+        # childFactory would do. Things are cleaner if we only do a subset of
+        # them, though, so we don't do: d = self.childFactory(ctx, name)
+
+        d = self.node.get(name)
+        def _maybe_got_node(node_or_failure):
+            if isinstance(node_or_failure, Failure):
+                f = node_or_failure
+                f.trap(KeyError)
+                # create a placeholder
+                return PlaceHolderNodeHandler(self.node, name)
+            else:
+                node = node_or_failure
+                return make_handler_for(node, 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
+        # DirectoryNodeHandler.renderHTTP, and nevow would recurse into it,
+        # but the addCallback() that handles when_done= would break.
+        d.addCallback(lambda child: child.renderHTTP(ctx))
+        return d
+
+    def _POST_uri(self, req):
+        childcap = get_arg(req, "uri")
+        if not childcap:
+            raise WebError("set-uri requires a uri")
+        name = get_arg(req, "name")
+        if not name:
+            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)
+        d.addCallback(lambda res: childcap)
+        return d
+
+    def _POST_delete(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.
+            name = ''
+        charset = get_arg(req, "_charset", "utf-8")
+        name = name.decode(charset)
+        d = self.node.delete(name)
+        d.addCallback(lambda res: "thing deleted")
+        return d
+
+    def _POST_rename(self, req):
+        charset = get_arg(req, "_charset", "utf-8")
+        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)
+        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)
+
+        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")
+        return d
+
+    def _POST_check(self, req):
+        # check this directory
+        d = self.node.check()
+        def _done(res):
+            log.msg("checked %s, results %s" % (self.node, res),
+                    facility="tahoe.webish", level=log.NOISY)
+            return str(res)
+        d.addCallback(_done)
+        # TODO: results
+        return d
+
+    def _POST_set_children(self, req):
+        replace = boolean_of_arg(get_arg(req, "replace", "true"))
+        req.content.seek(0)
+        body = req.content.read()
+        try:
+            children = simplejson.loads(body)
+        except ValueError, le:
+            le.args = tuple(le.args + (body,))
+            # TODO test handling of bad JSON
+            raise
+        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')))
+        d = self.node.set_children(cs, replace)
+        d.addCallback(lambda res: "Okay so I did it.")
+        # TODO: results
+        return d
+
+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]
+
+class DirectoryAsHTML(rend.Page):
+    # The remainder of this class is to render the directory into
+    # human+browser -oriented HTML.
+    docFactory = getxmlfile("directory.xhtml")
+
+    def __init__(self, node):
+        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 render_welcome(self, ctx, data):
+        depth = len(IRequest(ctx).path) + 2
+        link = "/".join([".."] * depth)
+        return T.div[T.a(href=link)["Return to Welcome page"]]
+
+    def data_children(self, ctx, data):
+        d = self.node.list()
+        d.addCallback(lambda dict: sorted(dict.items()))
+        def _stall_some(items):
+            # 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
+            # same turn (i.e. the output of defer.succeed) will cause a stack
+            # overflow. To work around this, we insert a turn break after
+            # every 100 items, using foolscap's fireEventually(). This gives
+            # the stack a chance to be popped. It would also work to put
+            # every item in its own turn, but that'd be a lot more
+            # inefficient. This addresses ticket #237, for which I was never
+            # able to create a failing unit test.
+            output = []
+            for i,item in enumerate(items):
+                if i % 100 == 0:
+                    output.append(fireEventually(item))
+                else:
+                    output.append(item)
+            return output
+        d.addCallback(_stall_some)
+        return d
+
+    def render_row(self, ctx, data):
+        name, (target, metadata) = data
+        name = name.encode("utf-8")
+        assert not isinstance(name, unicode)
+
+        if self.node.is_readonly():
+            delete = "-"
+            rename = "-"
+        else:
+            # this creates a button which will cause our child__delete method
+            # to be invoked, which deletes the file and then redirects the
+            # browser back to this directory
+            delete = T.form(action=url.here, method="post")[
+                T.input(type='hidden', name='t', value='delete'),
+                T.input(type='hidden', name='name', value=name),
+                T.input(type='hidden', name='when_done', value=url.here),
+                T.input(type='submit', value='del', name="del"),
+                ]
+
+            rename = T.form(action=url.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=url.here),
+                T.input(type='submit', value='rename', name="rename"),
+                ]
+
+        ctx.fillSlots("delete", delete)
+        ctx.fillSlots("rename", rename)
+        check = T.form(action=url.here.child(name), method="post")[
+            T.input(type='hidden', name='t', value='check'),
+            T.input(type='hidden', name='when_done', value=url.here),
+            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"]))
+            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())
+
+        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 = "/file/%s/@@named=/%s" % (quoted_uri, urllib.quote(name))
+
+            ctx.fillSlots("filename",
+                          T.a(href=dlurl)[html.escape(name)])
+            ctx.fillSlots("type", "SSK")
+
+            ctx.fillSlots("size", "?")
+
+            text_plain_url = "/file/%s/@@named=/foo.txt" % quoted_uri
+            text_plain_tag = T.a(href=text_plain_url)["text/plain"]
+
+        elif IFileNode.providedBy(target):
+            dlurl = "/file/%s/@@named=/%s" % (quoted_uri, urllib.quote(name))
+
+            ctx.fillSlots("filename",
+                          T.a(href=dlurl)[html.escape(name)])
+            ctx.fillSlots("type", "FILE")
+
+            ctx.fillSlots("size", target.get_size())
+
+            text_plain_url = "/file/%s/@@named=/foo.txt" % quoted_uri
+            text_plain_tag = T.a(href=text_plain_url)["text/plain"]
+
+
+        elif IDirectoryNode.providedBy(target):
+            # directory
+            uri_link = "/uri/" + urllib.quote(target.get_uri())
+            ctx.fillSlots("filename",
+                          T.a(href=uri_link)[html.escape(name)])
+            if target.is_readonly():
+                dirtype = "DIR-RO"
+            else:
+                dirtype = "DIR"
+            ctx.fillSlots("type", dirtype)
+            ctx.fillSlots("size", "-")
+            text_plain_tag = None
+
+        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])
+
+        ctx.fillSlots("data", childdata)
+
+        try:
+            checker = IClient(ctx).getServiceNamed("checker")
+        except KeyError:
+            checker = None
+        if checker:
+            d = defer.maybeDeferred(checker.checker_results_for,
+                                    target.get_verifier())
+            def _got(checker_results):
+                recent_results = reversed(checker_results[-5:])
+                if IFileNode.providedBy(target):
+                    results = ("[" +
+                               ", ".join(["%d/%d" % (found, needed)
+                                          for (when,
+                                               (needed, total, found, sharemap))
+                                          in recent_results]) +
+                               "]")
+                elif IDirectoryNode.providedBy(target):
+                    results = ("[" +
+                               "".join([{True:"+",False:"-"}[res]
+                                        for (when, res) in recent_results]) +
+                               "]")
+                else:
+                    results = "%d results" % len(checker_results)
+                return results
+            d.addCallback(_got)
+            results = d
+        else:
+            results = "--"
+        # TODO: include a link to see more results, including timestamps
+        # TODO: use a sparkline
+        ctx.fillSlots("checker_results", results)
+
+        return ctx.tag
+
+    def render_forms(self, ctx, data):
+        if self.node.is_readonly():
+            return T.div["No upload forms: directory is read-only"]
+        mkdir = 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=url.here),
+            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"),
+            ]]
+
+        upload = 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=url.here),
+            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"),
+            ]]
+
+        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=url.here),
+            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"),
+            ]]
+        return [T.div(class_="freeform-form")[mkdir],
+                T.div(class_="freeform-form")[upload],
+                T.div(class_="freeform-form")[mount],
+                ]
+
+    def build_overwrite_form(self, ctx, name, target):
+        if IMutableFileNode.providedBy(target) and not target.is_readonly():
+            action = "/uri/" + urllib.quote(target.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=url.here),
+                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 IFileNode.providedBy(childnode):
+                kiduri = childnode.get_uri()
+                kiddata = ("filenode",
+                           {'ro_uri': kiduri,
+                            'size': childnode.get_size(),
+                            'metadata': metadata,
+                            })
+            else:
+                assert IDirectoryNode.providedBy(childnode), (childnode,
+                                                              children,)
+                kiddata = ("dirnode",
+                           {'ro_uri': childnode.get_readonly_uri(),
+                            'metadata': metadata,
+                            })
+                if not childnode.is_readonly():
+                    kiddata[1]['rw_uri'] = childnode.get_uri()
+            kids[name] = kiddata
+        contents = { 'children': kids,
+                     'ro_uri': dirnode.get_readonly_uri(),
+                     }
+        if not dirnode.is_readonly():
+            contents['rw_uri'] = dirnode.get_uri()
+        data = ("dirnode", contents)
+        return simplejson.dumps(data, indent=1)
+    d.addCallback(_got)
+    d.addCallback(text_plain, ctx)
+    return d
+
+def DirectoryURI(ctx, dirnode):
+    return text_plain(dirnode.get_uri(), ctx)
+
+def DirectoryReadonlyURI(ctx, dirnode):
+    return text_plain(dirnode.get_readonly_uri(), ctx)
+
+class RenameForm(rend.Page):
+    addSlash = True
+    docFactory = getxmlfile("rename-form.xhtml")
+
+    def render_title(self, ctx, data):
+        return ctx.tag["Directory SI=%s" % abbreviated_dirnode(self.original)]
+
+    def render_header(self, ctx, data):
+        header = ["Rename "
+                  "in directory SI=%s" % abbreviated_dirnode(self.original),
+                  ]
+
+        if self.original.is_readonly():
+            header.append(" (readonly!)")
+        header.append(":")
+        return ctx.tag[header]
+
+    def render_when_done(self, ctx, data):
+        return T.input(type="hidden", name="when_done", value=url.here)
+
+    def render_get_name(self, ctx, data):
+        req = IRequest(ctx)
+        name = get_arg(req, "name", "")
+        ctx.tag.attributes['value'] = name
+        return ctx.tag
+
+
+class Manifest(rend.Page):
+    docFactory = getxmlfile("manifest.xhtml")
+
+    def render_title(self, ctx):
+        return T.title["Manifest of SI=%s" % abbreviated_dirnode(self.original)]
+
+    def render_header(self, ctx):
+        return T.p["Manifest of SI=%s" % abbreviated_dirnode(self.original)]
+
+    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 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
+
+def DeepStats(ctx, dirnode):
+    d = dirnode.deep_stats()
+    d.addCallback(simplejson.dumps, indent=1)
+    d.addCallback(text_plain, ctx)
+    return d
diff --git a/src/allmydata/web/filenode.py b/src/allmydata/web/filenode.py
new file mode 100644 (file)
index 0000000..c17bc52
--- /dev/null
@@ -0,0 +1,419 @@
+
+import simplejson
+
+from zope.interface import implements
+from twisted.internet.interfaces import IConsumer
+from twisted.web import http, static, resource, server
+from twisted.internet import defer
+from nevow import url, rend
+from nevow.inevow import IRequest
+
+from allmydata.upload import FileHandle
+from allmydata.interfaces import IDownloadTarget, ExistingChildError
+from allmydata.mutable.common import MODE_READ
+from allmydata.util import log
+
+from allmydata.web.common import text_plain, WebError, IClient, \
+     boolean_of_arg, get_arg, should_create_intermediate_directories
+
+class ReplaceMeMixin:
+
+    def replace_me_with_a_child(self, ctx, replace):
+        # a new file is being uploaded in our place.
+        req = IRequest(ctx)
+        client = IClient(ctx)
+        uploadable = FileHandle(req.content, convergence=client.convergence)
+        d = self.parentnode.add_file(self.name, uploadable, overwrite=replace)
+        def _done(filenode):
+            log.msg("webish upload complete",
+                    facility="tahoe.webish", level=log.NOISY)
+            if self.node:
+                # we've replaced an existing file (or modified a mutable
+                # file), so the response code is 200
+                req.setResponseCode(http.OK)
+            else:
+                # we've created a new file, so the code is 201
+                req.setResponseCode(http.CREATED)
+            return filenode.get_uri()
+        d.addCallback(_done)
+        return d
+
+    def replace_me_with_a_childcap(self, ctx, replace):
+        req = IRequest(ctx)
+        req.content.seek(0)
+        childcap = req.content.read()
+        client = IClient(ctx)
+        childnode = client.create_node_from_uri(childcap)
+        d = self.parentnode.set_node(self.name, childnode, overwrite=replace)
+        d.addCallback(lambda res: childnode.get_uri())
+        return d
+
+    def _read_data_from_formpost(self, req):
+        # SDMF: files are small, and we can only upload data, so we read
+        # the whole file into memory before uploading.
+        contents = req.fields["file"]
+        contents.file.seek(0)
+        data = contents.file.read()
+        return data
+
+    def replace_me_with_a_formpost(self, ctx, replace):
+        # create a new file, maybe mutable, maybe immutable
+        req = IRequest(ctx)
+        client = IClient(ctx)
+        mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
+
+        if mutable:
+            data = self._read_data_from_formpost(req)
+            d = client.create_mutable_file(data)
+            def _uploaded(newnode):
+                d2 = self.parentnode.set_node(self.name, newnode,
+                                              overwrite=replace)
+                d2.addCallback(lambda res: newnode.get_uri())
+                return d2
+            d.addCallback(_uploaded)
+            return d
+        # create an immutable file
+        contents = req.fields["file"]
+        uploadable = FileHandle(contents.file, convergence=client.convergence)
+        d = self.parentnode.add_file(self.name, uploadable, overwrite=replace)
+        d.addCallback(lambda newnode: newnode.get_uri())
+        return d
+
+class PlaceHolderNodeHandler(rend.Page, ReplaceMeMixin):
+    def __init__(self, parentnode, name):
+        rend.Page.__init__(self)
+        assert parentnode
+        self.parentnode = parentnode
+        self.name = name
+        self.node = None
+
+    def childFactory(self, ctx, name):
+        req = IRequest(ctx)
+        if should_create_intermediate_directories(req):
+            raise WebError("Cannot create directory '%s', because its "
+                           "parent is a file, not a directory" % name,
+                           http.CONFLICT)
+        raise WebError("Files have no children, certainly not named '%s'"
+                       % name, http.CONFLICT)
+
+
+    def renderHTTP(self, ctx):
+        # This is where all of the ?t=* actions are implemented.
+        request = IRequest(ctx)
+
+        # if we were using regular twisted.web Resources (and the regular
+        # twisted.web.server.Request object) then we could implement
+        # render_PUT and render_GET. But Nevow's request handler
+        # (NevowRequest.gotPageContext) goes directly to renderHTTP. Copy
+        # some code from the Resource.render method that Nevow bypasses, to
+        # do the same thing.
+        m = getattr(self, 'render_' + request.method, None)
+        if not m:
+            from twisted.web.server import UnsupportedMethod
+            raise UnsupportedMethod(getattr(self, 'allowedMethods', ()))
+        return m(ctx)
+
+    def render_PUT(self, ctx):
+        req = IRequest(ctx)
+        t = get_arg(req, "t", "").strip()
+        replace = boolean_of_arg(get_arg(req, "replace", "true"))
+        assert self.parentnode and self.name
+        if not t:
+            return self.replace_me_with_a_child(ctx, replace)
+        if t == "uri":
+            return self.replace_me_with_a_childcap(ctx, replace)
+
+        raise WebError("PUT to a file: bad t=%s" % t)
+
+    def render_POST(self, ctx):
+        req = IRequest(ctx)
+        t = get_arg(req, "t", "").strip()
+        replace = boolean_of_arg(get_arg(req, "replace", "true"))
+        if t == "mkdir":
+            d = self.parentnode.create_empty_directory(self.name, replace)
+            d.addCallback(lambda node: node.get_uri())
+            d.addCallback(text_plain, ctx)
+        elif t == "upload":
+            # like PUT, but get the file data from an HTML form's input field.
+            # We could get here from POST /uri/mutablefilecap?t=upload,
+            # or POST /uri/path/file?t=upload, or
+            # POST /uri/path/dir?t=upload&name=foo . All have the same
+            # behavior, we just ignore any name= argument
+            d = self.replace_me_with_a_formpost(ctx, replace)
+        else:
+            raise WebError("POST to a file: bad t=%s" % t)
+
+        when_done = get_arg(req, "when_done", None)
+        if when_done:
+            d.addCallback(lambda res: url.URL.fromString(when_done))
+        return d
+
+
+class FileNodeHandler(rend.Page, ReplaceMeMixin):
+    def __init__(self, node, parentnode=None, name=None):
+        rend.Page.__init__(self)
+        assert node
+        self.node = node
+        self.parentnode = parentnode
+        self.name = name
+
+    def childFactory(self, ctx, name):
+        req = IRequest(ctx)
+        if should_create_intermediate_directories(req):
+            raise WebError("Cannot create directory '%s', because its "
+                           "parent is a file, not a directory" % name)
+        raise WebError("Files have no children, certainly not named '%s'"
+                       % name)
+
+
+    def renderHTTP(self, ctx):
+        # This is where all of the ?t=* actions are implemented.
+        request = IRequest(ctx)
+
+        # if we were using regular twisted.web Resources (and the regular
+        # twisted.web.server.Request object) then we could implement
+        # render_PUT and render_GET. But Nevow's request handler
+        # (NevowRequest.gotPageContext) goes directly to renderHTTP. Copy
+        # some code from the Resource.render method that Nevow bypasses, to
+        # do the same thing.
+        m = getattr(self, 'render_' + request.method, None)
+        if not m:
+            from twisted.web.server import UnsupportedMethod
+            raise UnsupportedMethod(getattr(self, 'allowedMethods', ()))
+        return m(ctx)
+
+    def render_GET(self, ctx):
+        req = IRequest(ctx)
+        t = get_arg(req, "t", "").strip()
+        if not t:
+            # just get the contents
+            filename = get_arg(req, "filename", self.name) or "unknown"
+            save_to_file = boolean_of_arg(get_arg(req, "save", "False"))
+            return FileDownloader(self.node, filename, save_to_file)
+        if t == "json":
+            return FileJSONMetadata(ctx, self.node)
+        if t == "uri":
+            return FileURI(ctx, self.node)
+        if t == "readonly-uri":
+            return FileReadOnlyURI(ctx, self.node)
+        raise WebError("GET file: bad t=%s" % t)
+
+    def render_HEAD(self, ctx):
+        req = IRequest(ctx)
+        t = get_arg(req, "t", "").strip()
+        if t:
+            raise WebError("GET file: bad t=%s" % t)
+        if self.node.is_mutable():
+            # update the servermap to get the size of this file without
+            # downloading the full contents.
+            d = self.node.get_servermap(MODE_READ)
+            def _got_servermap(smap):
+                ver = smap.best_recoverable_version()
+                if not ver:
+                    raise WebError("Unable to recover this file",
+                                   http.NOT_FOUND)
+                length = smap.size_of_version(ver)
+                return length
+            d.addCallback(_got_servermap)
+        # otherwise, we can get the size from the URI
+        else:
+            d = defer.succeed(self.node.get_size())
+        def _got_length(length):
+            req.setHeader("content-length", length)
+            return ""
+        d.addCallback(_got_length)
+        return d
+
+    def render_PUT(self, ctx):
+        req = IRequest(ctx)
+        t = get_arg(req, "t", "").strip()
+        replace = boolean_of_arg(get_arg(req, "replace", "true"))
+        if not t:
+            if self.node.is_mutable():
+                return self.replace_my_contents(ctx)
+            if not replace:
+                # this is the early trap: if someone else modifies the
+                # directory while we're uploading, the add_file(overwrite=)
+                # call in replace_me_with_a_child will do the late trap.
+                raise ExistingChildError()
+            assert self.parentnode and self.name
+            return self.replace_me_with_a_child(ctx, replace)
+        if t == "uri":
+            if not replace:
+                raise ExistingChildError()
+            assert self.parentnode and self.name
+            return self.replace_me_with_a_childcap(ctx, replace)
+
+        raise WebError("PUT to a file: bad t=%s" % t)
+
+    def render_POST(self, ctx):
+        req = IRequest(ctx)
+        t = get_arg(req, "t", "").strip()
+        replace = boolean_of_arg(get_arg(req, "replace", "true"))
+        if t == "check":
+            d = self._POST_check(req)
+        elif t == "upload":
+            # like PUT, but get the file data from an HTML form's input field
+            # We could get here from POST /uri/mutablefilecap?t=upload,
+            # or POST /uri/path/file?t=upload, or
+            # POST /uri/path/dir?t=upload&name=foo . All have the same
+            # behavior, we just ignore any name= argument
+            if self.node.is_mutable():
+                d = self.replace_my_contents_with_a_formpost(ctx)
+            else:
+                if not replace:
+                    raise ExistingChildError()
+                assert self.parentnode and self.name
+                d = self.replace_me_with_a_formpost(ctx, replace)
+        else:
+            raise WebError("POST to file: bad t=%s" % t)
+
+        when_done = get_arg(req, "when_done", None)
+        if when_done:
+            d.addCallback(lambda res: url.URL.fromString(when_done))
+        return d
+
+    def _POST_check(self, req):
+        d = self.node.check()
+        def _done(res):
+            log.msg("checked %s, results %s" % (self.node, res),
+                    facility="tahoe.webish", level=log.NOISY)
+            return str(res)
+        d.addCallback(_done)
+        # TODO: results
+        return d
+
+    def render_DELETE(self, ctx):
+        assert self.parentnode and self.name
+        d = self.parentnode.delete(self.name)
+        d.addCallback(lambda res: self.node.get_uri())
+        return d
+
+    def replace_my_contents(self, ctx):
+        req = IRequest(ctx)
+        req.content.seek(0)
+        new_contents = req.content.read()
+        d = self.node.overwrite(new_contents)
+        d.addCallback(lambda res: self.node.get_uri())
+        return d
+
+    def replace_my_contents_with_a_formpost(self, ctx):
+        # we have a mutable file. Get the data from the formpost, and replace
+        # the mutable file's contents with it.
+        req = IRequest(ctx)
+        new_contents = self._read_data_from_formpost(req)
+        d = self.node.overwrite(new_contents)
+        d.addCallback(lambda res: self.node.get_uri())
+        return d
+
+
+class WebDownloadTarget:
+    implements(IDownloadTarget, IConsumer)
+    def __init__(self, req, content_type, content_encoding, save_to_filename):
+        self._req = req
+        self._content_type = content_type
+        self._content_encoding = content_encoding
+        self._opened = False
+        self._producer = None
+        self._save_to_filename = save_to_filename
+
+    def registerProducer(self, producer, streaming):
+        self._req.registerProducer(producer, streaming)
+    def unregisterProducer(self):
+        self._req.unregisterProducer()
+
+    def open(self, size):
+        self._opened = True
+        self._req.setHeader("content-type", self._content_type)
+        if self._content_encoding:
+            self._req.setHeader("content-encoding", self._content_encoding)
+        self._req.setHeader("content-length", str(size))
+        if self._save_to_filename is not None:
+            # tell the browser to save the file rather display it
+            # TODO: indicate charset of filename= properly
+            filename = self._save_to_filename.encode("utf-8")
+            self._req.setHeader("content-disposition",
+                                'attachment; filename="%s"'
+                                % filename)
+
+    def write(self, data):
+        self._req.write(data)
+    def close(self):
+        self._req.finish()
+
+    def fail(self, why):
+        if self._opened:
+            # The content-type is already set, and the response code
+            # has already been sent, so we can't provide a clean error
+            # indication. We can emit text (which a browser might interpret
+            # as something else), and if we sent a Size header, they might
+            # notice that we've truncated the data. Keep the error message
+            # small to improve the chances of having our error response be
+            # shorter than the intended results.
+            #
+            # We don't have a lot of options, unfortunately.
+            self._req.write("problem during download\n")
+        else:
+            # We haven't written anything yet, so we can provide a sensible
+            # error message.
+            msg = str(why.type)
+            msg.replace("\n", "|")
+            self._req.setResponseCode(http.GONE, msg)
+            self._req.setHeader("content-type", "text/plain")
+            # TODO: HTML-formatted exception?
+            self._req.write(str(why))
+        self._req.finish()
+
+    def register_canceller(self, cb):
+        pass
+    def finish(self):
+        pass
+
+class FileDownloader(resource.Resource):
+    # since we override the rendering process (to let the tahoe Downloader
+    # drive things), we must inherit from regular old twisted.web.resource
+    # instead of nevow.rend.Page . Nevow will use adapters to wrap a
+    # nevow.appserver.OldResourceAdapter around any
+    # twisted.web.resource.IResource that it is given. TODO: it looks like
+    # that wrapper would allow us to return a Deferred from render(), which
+    # might could simplify the implementation of WebDownloadTarget.
+
+    def __init__(self, filenode, filename, save_to_file):
+        resource.Resource.__init__(self)
+        self.filenode = filenode
+        self.filename = filename
+        self.save_to_file = save_to_file
+    def render(self, req):
+        gte = static.getTypeAndEncoding
+        ctype, encoding = gte(self.filename,
+                              static.File.contentTypes,
+                              static.File.contentEncodings,
+                              defaultType="text/plain")
+        save_to_filename = None
+        if self.save_to_file:
+            save_to_filename = self.filename
+        wdt = WebDownloadTarget(req, ctype, encoding, save_to_filename)
+        d = self.filenode.download(wdt)
+        # exceptions during download are handled by the WebDownloadTarget
+        d.addErrback(lambda why: None)
+        return server.NOT_DONE_YET
+
+def FileJSONMetadata(ctx, filenode):
+    file_uri = filenode.get_uri()
+    data = ("filenode",
+            {'ro_uri': file_uri,
+             'size': filenode.get_size(),
+             })
+    return text_plain(simplejson.dumps(data, indent=1), ctx)
+
+def FileURI(ctx, filenode):
+    return text_plain(filenode.get_uri(), ctx)
+
+def FileReadOnlyURI(ctx, filenode):
+    if filenode.is_readonly():
+        return text_plain(filenode.get_uri(), ctx)
+    return text_plain(filenode.get_readonly().get_uri(), ctx)
+
+class FileNodeDownloadHandler(FileNodeHandler):
+    def childFactory(self, ctx, name):
+        return FileNodeDownloadHandler(self.node, name=name)
diff --git a/src/allmydata/web/root.py b/src/allmydata/web/root.py
new file mode 100644 (file)
index 0000000..51a9630
--- /dev/null
@@ -0,0 +1,293 @@
+
+import time
+
+from twisted.internet import address
+from twisted.web import http
+from nevow import rend, url, tags as T
+from nevow.inevow import IRequest
+from nevow.static import File as nevow_File # TODO: merge with static.File?
+from nevow.util import resource_filename
+from formless import webform
+
+import allmydata # to display import path
+from allmydata import get_package_versions_string
+from allmydata import provisioning
+from allmydata.util import idlib
+from allmydata.interfaces import IFileNode
+from allmydata.web import filenode, directory, unlinked, status
+from allmydata.web.common import abbreviate_size, IClient, getxmlfile, \
+     WebError, get_arg
+
+
+
+class URIHandler(rend.Page):
+    # I live at /uri . There are several operations defined on /uri itself,
+    # mostly involed with creation of unlinked files and directories.
+
+    def renderHTTP(self, ctx):
+        request = IRequest(ctx)
+
+        # if we were using regular twisted.web Resources (and the regular
+        # twisted.web.server.Request object) then we could implement
+        # render_PUT and render_GET. But Nevow's request handler
+        # (NevowRequest.gotPageContext) goes directly to renderHTTP. Copy
+        # some code from the Resource.render method that Nevow bypasses, to
+        # do the same thing.
+        m = getattr(self, 'render_' + request.method, None)
+        if not m:
+            from twisted.web.server import UnsupportedMethod
+            raise UnsupportedMethod(getattr(self, 'allowedMethods', ()))
+        return m(ctx)
+
+    def render_GET(self, ctx):
+        req = IRequest(ctx)
+        uri = get_arg(req, "uri", None)
+        if uri is None:
+            raise WebError("GET /uri requires uri=")
+        there = url.URL.fromContext(ctx)
+        there = there.clear("uri")
+        # I thought about escaping the childcap that we attach to the URL
+        # here, but it seems that nevow does that for us.
+        there = there.child(uri)
+        return there
+
+    def render_PUT(self, ctx):
+        req = IRequest(ctx)
+        # either "PUT /uri" to create an unlinked file, or
+        # "PUT /uri?t=mkdir" to create an unlinked directory
+        t = get_arg(req, "t", "").strip()
+        if t == "":
+            mutable = bool(get_arg(req, "mutable", "").strip())
+            if mutable:
+                return unlinked.PUTUnlinkedSSK(ctx)
+            else:
+                return unlinked.PUTUnlinkedCHK(ctx)
+        if t == "mkdir":
+            return unlinked.PUTUnlinkedCreateDirectory(ctx)
+        errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
+                  "and POST?t=mkdir")
+        raise WebError(errmsg, http.BAD_REQUEST)
+
+    def render_POST(self, ctx):
+        # "POST /uri?t=upload&file=newfile" to upload an
+        # unlinked file or "POST /uri?t=mkdir" to create a
+        # new directory
+        req = IRequest(ctx)
+        t = get_arg(req, "t", "").strip()
+        if t in ("", "upload"):
+            mutable = bool(get_arg(req, "mutable", "").strip())
+            if mutable:
+                return unlinked.POSTUnlinkedSSK(ctx)
+            else:
+                return unlinked.POSTUnlinkedCHK(ctx)
+        if t == "mkdir":
+            return unlinked.POSTUnlinkedCreateDirectory(ctx)
+        errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
+                  "and POST?t=mkdir")
+        raise WebError(errmsg, http.BAD_REQUEST)
+
+    def childFactory(self, ctx, name):
+        # 'name' is expected to be a URI
+        client = IClient(ctx)
+        try:
+            node = client.create_node_from_uri(name)
+            return directory.make_handler_for(node)
+        except (TypeError, AssertionError):
+            raise WebError("'%s' is not a valid file- or directory- cap"
+                           % name)
+
+class FileHandler(rend.Page):
+    # I handle /file/$FILECAP[/IGNORED] , which provides a URL from which a
+    # file can be downloaded correctly by tools like "wget".
+
+    def childFactory(self, ctx, name):
+        req = IRequest(ctx)
+        if req.method not in ("GET", "HEAD"):
+            raise WebError("/file can only be used with GET or HEAD")
+        # 'name' must be a file URI
+        client = IClient(ctx)
+        try:
+            node = client.create_node_from_uri(name)
+        except (TypeError, AssertionError):
+            raise WebError("'%s' is not a valid file- or directory- cap"
+                           % name)
+        if not IFileNode.providedBy(node):
+            raise WebError("'%s' is not a file-cap" % name)
+        return filenode.FileNodeDownloadHandler(node)
+
+    def renderHTTP(self, ctx):
+        raise WebError("/file must be followed by a file-cap and a name",
+                       http.NOT_FOUND)
+
+class Root(rend.Page):
+
+    addSlash = True
+    docFactory = getxmlfile("welcome.xhtml")
+
+    child_uri = URIHandler()
+    child_file = FileHandler()
+    child_named = FileHandler()
+
+    child_webform_css = webform.defaultCSS
+    child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
+
+    child_provisioning = provisioning.ProvisioningTool()
+    child_status = status.Status()
+    child_helper_status = status.HelperStatus()
+    child_statistics = status.Statistics()
+
+    def data_version(self, ctx, data):
+        return get_package_versions_string()
+    def data_import_path(self, ctx, data):
+        return str(allmydata)
+    def data_my_nodeid(self, ctx, data):
+        return idlib.nodeid_b2a(IClient(ctx).nodeid)
+
+    def render_services(self, ctx, data):
+        ul = T.ul()
+        client = IClient(ctx)
+        try:
+            ss = client.getServiceNamed("storage")
+            allocated_s = abbreviate_size(ss.allocated_size())
+            allocated = "about %s allocated" % allocated_s
+            sizelimit = "no size limit"
+            if ss.sizelimit is not None:
+                sizelimit = "size limit is %s" % abbreviate_size(ss.sizelimit)
+            ul[T.li["Storage Server: %s, %s" % (allocated, sizelimit)]]
+        except KeyError:
+            ul[T.li["Not running storage server"]]
+
+        try:
+            h = client.getServiceNamed("helper")
+            stats = h.get_stats()
+            active_uploads = stats["chk_upload_helper.active_uploads"]
+            ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
+        except KeyError:
+            ul[T.li["Not running helper"]]
+
+        return ctx.tag[ul]
+
+    def data_introducer_furl(self, ctx, data):
+        return IClient(ctx).introducer_furl
+    def data_connected_to_introducer(self, ctx, data):
+        if IClient(ctx).connected_to_introducer():
+            return "yes"
+        return "no"
+
+    def data_helper_furl(self, ctx, data):
+        try:
+            uploader = IClient(ctx).getServiceNamed("uploader")
+        except KeyError:
+            return None
+        furl, connected = uploader.get_helper_info()
+        return furl
+    def data_connected_to_helper(self, ctx, data):
+        try:
+            uploader = IClient(ctx).getServiceNamed("uploader")
+        except KeyError:
+            return "no" # we don't even have an Uploader
+        furl, connected = uploader.get_helper_info()
+        if connected:
+            return "yes"
+        return "no"
+
+    def data_known_storage_servers(self, ctx, data):
+        ic = IClient(ctx).introducer_client
+        servers = [c
+                   for c in ic.get_all_connectors().values()
+                   if c.service_name == "storage"]
+        return len(servers)
+
+    def data_connected_storage_servers(self, ctx, data):
+        ic = IClient(ctx).introducer_client
+        return len(ic.get_all_connections_for("storage"))
+
+    def data_services(self, ctx, data):
+        ic = IClient(ctx).introducer_client
+        c = [ (service_name, nodeid, rsc)
+              for (nodeid, service_name), rsc
+              in ic.get_all_connectors().items() ]
+        c.sort()
+        return c
+
+    def render_service_row(self, ctx, data):
+        (service_name, nodeid, rsc) = data
+        ctx.fillSlots("peerid", "%s %s" % (idlib.nodeid_b2a(nodeid),
+                                           rsc.nickname))
+        if rsc.rref:
+            rhost = rsc.remote_host
+            if nodeid == IClient(ctx).nodeid:
+                rhost_s = "(loopback)"
+            elif isinstance(rhost, address.IPv4Address):
+                rhost_s = "%s:%d" % (rhost.host, rhost.port)
+            else:
+                rhost_s = str(rhost)
+            connected = "Yes: to " + rhost_s
+            since = rsc.last_connect_time
+        else:
+            connected = "No"
+            since = rsc.last_loss_time
+
+        TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
+        ctx.fillSlots("connected", connected)
+        ctx.fillSlots("since", time.strftime(TIME_FORMAT, time.localtime(since)))
+        ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
+                                                 time.localtime(rsc.announcement_time)))
+        ctx.fillSlots("version", rsc.version)
+        ctx.fillSlots("service_name", rsc.service_name)
+
+        return ctx.tag
+
+    def render_download_form(self, ctx, data):
+        # this is a form where users can download files by URI
+        form = T.form(action="uri", method="get",
+                      enctype="multipart/form-data")[
+            T.fieldset[
+            T.legend(class_="freeform-form-label")["Download a file"],
+            "URI to download: ",
+            T.input(type="text", name="uri"), " ",
+            "Filename to download as: ",
+            T.input(type="text", name="filename"), " ",
+            T.input(type="submit", value="Download!"),
+            ]]
+        return T.div[form]
+
+    def render_view_form(self, ctx, data):
+        # this is a form where users can download files by URI, or jump to a
+        # named directory
+        form = T.form(action="uri", method="get",
+                      enctype="multipart/form-data")[
+            T.fieldset[
+            T.legend(class_="freeform-form-label")["View a file or directory"],
+            "URI to view: ",
+            T.input(type="text", name="uri"), " ",
+            T.input(type="submit", value="View!"),
+            ]]
+        return T.div[form]
+
+    def render_upload_form(self, ctx, data):
+        # this is a form where users can upload unlinked files
+        form = T.form(action="uri", method="post",
+                      enctype="multipart/form-data")[
+            T.fieldset[
+            T.legend(class_="freeform-form-label")["Upload a file"],
+            "Choose a file: ",
+            T.input(type="file", name="file", class_="freeform-input-file"),
+            T.input(type="hidden", name="t", value="upload"),
+            " Mutable?:", T.input(type="checkbox", name="mutable"),
+            T.input(type="submit", value="Upload!"),
+            ]]
+        return T.div[form]
+
+    def render_mkdir_form(self, ctx, data):
+        # this is a form where users can create new directories
+        form = T.form(action="uri", method="post",
+                      enctype="multipart/form-data")[
+            T.fieldset[
+            T.legend(class_="freeform-form-label")["Create a directory"],
+            T.input(type="hidden", name="t", value="mkdir"),
+            T.input(type="hidden", name="redirect_to_result", value="true"),
+            T.input(type="submit", value="Create Directory!"),
+            ]]
+        return T.div[form]
+
index 7e4ae6550c3d93bbad73b20c4c56fd6462668089..076908e96b076d922bb07389c3812dfc9f061144 100644 (file)
@@ -1,76 +1,73 @@
 
 import urllib
 from twisted.web import http
-from nevow import rend, inevow, url, tags as T
+from twisted.internet import defer
+from nevow import rend, url, tags as T
+from nevow.inevow import IRequest
 from allmydata.upload import FileHandle
 from allmydata.web.common import IClient, getxmlfile, get_arg, boolean_of_arg
 from allmydata.web import status
-from allmydata.util import observer
 
-class UnlinkedPUTCHKUploader(rend.Page):
-    def renderHTTP(self, ctx):
-        req = inevow.IRequest(ctx)
-        assert req.method == "PUT"
-        # "PUT /uri", to create an unlinked file. This is like PUT but
-        # without the associated set_uri.
-
-        client = IClient(ctx)
-
-        uploadable = FileHandle(req.content, client.convergence)
-        d = client.upload(uploadable)
-        d.addCallback(lambda results: results.uri)
-        # that fires with the URI of the new file
-        return d
-
-class UnlinkedPUTSSKUploader(rend.Page):
-    def renderHTTP(self, ctx):
-        req = inevow.IRequest(ctx)
-        assert req.method == "PUT"
-        # SDMF: files are small, and we can only upload data
-        req.content.seek(0)
-        data = req.content.read()
-        d = IClient(ctx).create_mutable_file(data)
-        d.addCallback(lambda n: n.get_uri())
-        return d
-
-class UnlinkedPUTCreateDirectory(rend.Page):
-    def renderHTTP(self, ctx):
-        req = inevow.IRequest(ctx)
-        assert req.method == "PUT"
-        # "PUT /uri?t=mkdir", to create an unlinked directory.
-        d = IClient(ctx).create_empty_dirnode()
-        d.addCallback(lambda dirnode: dirnode.get_uri())
-        # XXX add redirect_to_result
-        return d
-
-class UnlinkedPOSTCHKUploader(status.UploadResultsRendererMixin, rend.Page):
+def PUTUnlinkedCHK(ctx):
+    req = IRequest(ctx)
+    # "PUT /uri", to create an unlinked file.
+    client = IClient(ctx)
+    uploadable = FileHandle(req.content, client.convergence)
+    d = client.upload(uploadable)
+    d.addCallback(lambda results: results.uri)
+    # that fires with the URI of the new file
+    return d
+
+def PUTUnlinkedSSK(ctx):
+    req = IRequest(ctx)
+    # SDMF: files are small, and we can only upload data
+    req.content.seek(0)
+    data = req.content.read()
+    d = IClient(ctx).create_mutable_file(data)
+    d.addCallback(lambda n: n.get_uri())
+    return d
+
+def PUTUnlinkedCreateDirectory(ctx):
+    req = IRequest(ctx)
+    # "PUT /uri?t=mkdir", to create an unlinked directory.
+    d = IClient(ctx).create_empty_dirnode()
+    d.addCallback(lambda dirnode: dirnode.get_uri())
+    # XXX add redirect_to_result
+    return d
+
+
+def POSTUnlinkedCHK(ctx):
+    req = IRequest(ctx)
+    client = IClient(ctx)
+    fileobj = req.fields["file"].file
+    uploadable = FileHandle(fileobj, client.convergence)
+    d = client.upload(uploadable)
+    when_done = get_arg(req, "when_done", None)
+    if when_done:
+        # if when_done= is provided, return a redirect instead of our
+        # usual upload-results page
+        def _done(upload_results, redir_to):
+            if "%(uri)s" in redir_to:
+                redir_to = redir_to % {"uri": urllib.quote(upload_results.uri)
+                                         }
+            return url.URL.fromString(redir_to)
+        d.addCallback(_done, when_done)
+    else:
+        # return the Upload Results page, which includes the URI
+        d.addCallback(UploadResultsPage, ctx)
+    return d
+
+
+class UploadResultsPage(status.UploadResultsRendererMixin, rend.Page):
     """'POST /uri', to create an unlinked file."""
     docFactory = getxmlfile("upload-results.xhtml")
 
-    def __init__(self, client, req):
+    def __init__(self, upload_results, ctx):
         rend.Page.__init__(self)
-        # we start the upload now, and distribute notification of its
-        # completion to render_ methods with an ObserverList
-        assert req.method == "POST"
-        self._done = observer.OneShotObserverList()
-        fileobj = req.fields["file"].file
-        uploadable = FileHandle(fileobj, client.convergence)
-        d = client.upload(uploadable)
-        d.addBoth(self._done.fire)
-
-    def renderHTTP(self, ctx):
-        req = inevow.IRequest(ctx)
-        when_done = get_arg(req, "when_done", None)
-        if when_done:
-            # if when_done= is provided, return a redirect instead of our
-            # usual upload-results page
-            d = self._done.when_fired()
-            d.addCallback(lambda res: url.URL.fromString(when_done))
-            return d
-        return rend.Page.renderHTTP(self, ctx)
+        self.results = upload_results
 
     def upload_results(self):
-        return self._done.when_fired()
+        return defer.succeed(self.results)
 
     def data_done(self, ctx, data):
         d = self.upload_results()
@@ -88,37 +85,31 @@ class UnlinkedPOSTCHKUploader(status.UploadResultsRendererMixin, rend.Page):
                       ["/uri/" + res.uri])
         return d
 
-class UnlinkedPOSTSSKUploader(rend.Page):
-    def renderHTTP(self, ctx):
-        req = inevow.IRequest(ctx)
-        assert req.method == "POST"
-
-        # "POST /uri", to create an unlinked file.
-        # SDMF: files are small, and we can only upload data
-        contents = req.fields["file"]
-        contents.file.seek(0)
-        data = contents.file.read()
-        d = IClient(ctx).create_mutable_file(data)
-        d.addCallback(lambda n: n.get_uri())
-        return d
-
-class UnlinkedPOSTCreateDirectory(rend.Page):
-    def renderHTTP(self, ctx):
-        req = inevow.IRequest(ctx)
-        assert req.method == "POST"
-
-        # "POST /uri?t=mkdir", to create an unlinked directory.
-        d = IClient(ctx).create_empty_dirnode()
-        redirect = get_arg(req, "redirect_to_result", "false")
-        if boolean_of_arg(redirect):
-            def _then_redir(res):
-                new_url = "uri/" + urllib.quote(res.get_uri())
-                req.setResponseCode(http.SEE_OTHER) # 303
-                req.setHeader('location', new_url)
-                req.finish()
-                return ''
-            d.addCallback(_then_redir)
-        else:
-            d.addCallback(lambda dirnode: dirnode.get_uri())
-        return d
+def POSTUnlinkedSSK(ctx):
+    req = IRequest(ctx)
+    # "POST /uri", to create an unlinked file.
+    # SDMF: files are small, and we can only upload data
+    contents = req.fields["file"]
+    contents.file.seek(0)
+    data = contents.file.read()
+    d = IClient(ctx).create_mutable_file(data)
+    d.addCallback(lambda n: n.get_uri())
+    return d
+
+def POSTUnlinkedCreateDirectory(ctx):
+    req = IRequest(ctx)
+    # "POST /uri?t=mkdir", to create an unlinked directory.
+    d = IClient(ctx).create_empty_dirnode()
+    redirect = get_arg(req, "redirect_to_result", "false")
+    if boolean_of_arg(redirect):
+        def _then_redir(res):
+            new_url = "uri/" + urllib.quote(res.get_uri())
+            req.setResponseCode(http.SEE_OTHER) # 303
+            req.setHeader('location', new_url)
+            req.finish()
+            return ''
+        d.addCallback(_then_redir)
+    else:
+        d.addCallback(lambda dirnode: dirnode.get_uri())
+    return d
 
index b1c3c48ae94661c157fa95b621c4e2c5bf384f51..02edd6dd0e89a5e13496efe2c80fc73fccbe4585 100644 (file)
@@ -1,36 +1,12 @@
 
-import time, os.path
 from twisted.application import service, strports, internet
-from twisted.web import static, resource, server, html, http
-from twisted.internet import defer, address
-from twisted.internet.interfaces import IConsumer
-from nevow import inevow, rend, appserver, url, tags as T
-from nevow.static import File as nevow_File # TODO: merge with static.File?
-from allmydata.util import fileutil, idlib, log
-import simplejson
-from allmydata.interfaces import IDownloadTarget, IDirectoryNode, IFileNode, \
-     IMutableFileNode
-import allmydata # to display import path
-from allmydata import download
-from allmydata.uri import from_string_verifier, CHKFileVerifierURI
-from allmydata.upload import FileHandle, FileName
-from allmydata import provisioning
-from allmydata import get_package_versions_string
-from zope.interface import implements, Interface
-import urllib
-from formless import webform
-from foolscap.eventual import fireEventually
+from twisted.web import http
+from twisted.internet import defer
+from nevow import appserver, inevow
+from allmydata.util import log
 
-from nevow.util import resource_filename
-
-from allmydata.web import status, unlinked, introweb
-from allmydata.web.common import IClient, getxmlfile, get_arg, \
-     boolean_of_arg, abbreviate_size
-
-class ILocalAccess(Interface):
-    def local_access_is_allowed():
-        """Return True if t=upload&localdir= is allowed, giving anyone who
-        can talk to the webserver control over the local (disk) filesystem."""
+from allmydata.web import introweb, root
+from allmydata.web.common import IClient, MyExceptionHandler
 
 # we must override twisted.web.http.Request.requestReceived with a version
 # that doesn't use cgi.parse_multipart() . Since we actually use Nevow, we
@@ -99,7 +75,6 @@ class MyRequest(appserver.NevowRequest):
 ##                      self.channel.transport.loseConnection()
 ##                      return
 ##                  raise
-
         self.process()
 
     def _logger(self):
@@ -136,1496 +111,11 @@ class MyRequest(appserver.NevowRequest):
                 level=log.OPERATIONAL,
                 )
 
-class Directory(rend.Page):
-    addSlash = True
-    docFactory = getxmlfile("directory.xhtml")
-
-    def __init__(self, rootname, dirnode, dirpath):
-        self._rootname = rootname
-        self._dirnode = dirnode
-        self._dirpath = dirpath
-
-    def dirpath_as_string(self):
-        return "/" + "/".join(self._dirpath)
-
-    def render_title(self, ctx, data):
-        return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
-
-    def render_header(self, ctx, data):
-        parent_directories = ("<%s>" % self._rootname,) + self._dirpath
-        num_dirs = len(parent_directories)
-
-        header = ["Directory '"]
-        for i,d in enumerate(parent_directories):
-            upness = num_dirs - i - 1
-            if upness:
-                link = "/".join( ("..",) * upness )
-            else:
-                link = "."
-            header.append(T.a(href=link)[d])
-            if upness != 0:
-                header.append("/")
-        header.append("'")
-
-        if self._dirnode.is_readonly():
-            header.append(" (readonly)")
-        header.append(":")
-        return ctx.tag[header]
-
-    def render_welcome(self, ctx, data):
-        depth = len(self._dirpath) + 2
-        link = "/".join([".."] * depth)
-        return T.div[T.a(href=link)["Return to Welcome page"]]
-
-    def data_children(self, ctx, data):
-        d = self._dirnode.list()
-        d.addCallback(lambda dict: sorted(dict.items()))
-        def _stall_some(items):
-            # 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
-            # same turn (i.e. the output of defer.succeed) will cause a stack
-            # overflow. To work around this, we insert a turn break after
-            # every 100 items, using foolscap's fireEventually(). This gives
-            # the stack a chance to be popped. It would also work to put
-            # every item in its own turn, but that'd be a lot more
-            # inefficient. This addresses ticket #237, for which I was never
-            # able to create a failing unit test.
-            output = []
-            for i,item in enumerate(items):
-                if i % 100 == 0:
-                    output.append(fireEventually(item))
-                else:
-                    output.append(item)
-            return output
-        d.addCallback(_stall_some)
-        return d
-
-    def render_row(self, ctx, data):
-        name, (target, metadata) = data
-        name = name.encode("utf-8")
-        assert not isinstance(name, unicode)
-
-        if self._dirnode.is_readonly():
-            delete = "-"
-            rename = "-"
-        else:
-            # this creates a button which will cause our child__delete method
-            # to be invoked, which deletes the file and then redirects the
-            # browser back to this directory
-            delete = T.form(action=url.here, method="post")[
-                T.input(type='hidden', name='t', value='delete'),
-                T.input(type='hidden', name='name', value=name),
-                T.input(type='hidden', name='when_done', value=url.here),
-                T.input(type='submit', value='del', name="del"),
-                ]
-
-            rename = T.form(action=url.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=url.here),
-                T.input(type='submit', value='rename', name="rename"),
-                ]
-
-        ctx.fillSlots("delete", delete)
-        ctx.fillSlots("rename", rename)
-        check = T.form(action=url.here, method="post")[
-            T.input(type='hidden', name='t', value='check'),
-            T.input(type='hidden', name='name', value=name),
-            T.input(type='hidden', name='when_done', value=url.here),
-            T.input(type='submit', value='check', name="check"),
-            ]
-        ctx.fillSlots("overwrite", self.build_overwrite(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"]))
-            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())
-
-        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 = "/file/%s/@@named=/%s" % (quoted_uri, urllib.quote(name))
-
-            ctx.fillSlots("filename",
-                          T.a(href=dlurl)[html.escape(name)])
-            ctx.fillSlots("type", "SSK")
-
-            ctx.fillSlots("size", "?")
-
-            text_plain_url = "/file/%s/@@named=/foo.txt" % quoted_uri
-            text_plain_tag = T.a(href=text_plain_url)["text/plain"]
-
-        elif IFileNode.providedBy(target):
-            dlurl = "/file/%s/@@named=/%s" % (quoted_uri, urllib.quote(name))
-
-            ctx.fillSlots("filename",
-                          T.a(href=dlurl)[html.escape(name)])
-            ctx.fillSlots("type", "FILE")
-
-            ctx.fillSlots("size", target.get_size())
-
-            text_plain_url = "/file/%s/@@named=/foo.txt" % quoted_uri
-            text_plain_tag = T.a(href=text_plain_url)["text/plain"]
-
-
-        elif IDirectoryNode.providedBy(target):
-            # directory
-            uri_link = "/uri/" + urllib.quote(target.get_uri())
-            ctx.fillSlots("filename",
-                          T.a(href=uri_link)[html.escape(name)])
-            if target.is_readonly():
-                dirtype = "DIR-RO"
-            else:
-                dirtype = "DIR"
-            ctx.fillSlots("type", dirtype)
-            ctx.fillSlots("size", "-")
-            text_plain_tag = None
-
-        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])
-
-        ctx.fillSlots("data", childdata)
-
-        try:
-            checker = IClient(ctx).getServiceNamed("checker")
-        except KeyError:
-            checker = None
-        if checker:
-            d = defer.maybeDeferred(checker.checker_results_for,
-                                    target.get_verifier())
-            def _got(checker_results):
-                recent_results = reversed(checker_results[-5:])
-                if IFileNode.providedBy(target):
-                    results = ("[" +
-                               ", ".join(["%d/%d" % (found, needed)
-                                          for (when,
-                                               (needed, total, found, sharemap))
-                                          in recent_results]) +
-                               "]")
-                elif IDirectoryNode.providedBy(target):
-                    results = ("[" +
-                               "".join([{True:"+",False:"-"}[res]
-                                        for (when, res) in recent_results]) +
-                               "]")
-                else:
-                    results = "%d results" % len(checker_results)
-                return results
-            d.addCallback(_got)
-            results = d
-        else:
-            results = "--"
-        # TODO: include a link to see more results, including timestamps
-        # TODO: use a sparkline
-        ctx.fillSlots("checker_results", results)
-
-        return ctx.tag
-
-    def render_forms(self, ctx, data):
-        if self._dirnode.is_readonly():
-            return T.div["No upload forms: directory is read-only"]
-        mkdir = 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=url.here),
-            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"),
-            ]]
-
-        upload = 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=url.here),
-            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"),
-            ]]
-
-        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=url.here),
-            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"),
-            ]]
-        return [T.div(class_="freeform-form")[mkdir],
-                T.div(class_="freeform-form")[upload],
-                T.div(class_="freeform-form")[mount],
-                ]
-
-    def build_overwrite(self, ctx, data):
-        name, target = data
-        if IMutableFileNode.providedBy(target) and not target.is_readonly():
-            action="/uri/" + urllib.quote(target.get_uri())
-            overwrite = T.form(action=action, method="post",
-                               enctype="multipart/form-data")[
-                T.fieldset[
-                T.input(type="hidden", name="t", value="overwrite"),
-                T.input(type='hidden', name='name', value=name),
-                T.input(type='hidden', name='when_done', value=url.here),
-                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 = inevow.IRequest(ctx)
-        return get_arg(req, "results", "")
-
-class WebDownloadTarget:
-    implements(IDownloadTarget, IConsumer)
-    def __init__(self, req, content_type, content_encoding, save_to_file):
-        self._req = req
-        self._content_type = content_type
-        self._content_encoding = content_encoding
-        self._opened = False
-        self._producer = None
-        self._save_to_file = save_to_file
-
-    def registerProducer(self, producer, streaming):
-        self._req.registerProducer(producer, streaming)
-    def unregisterProducer(self):
-        self._req.unregisterProducer()
-
-    def open(self, size):
-        self._opened = True
-        self._req.setHeader("content-type", self._content_type)
-        if self._content_encoding:
-            self._req.setHeader("content-encoding", self._content_encoding)
-        self._req.setHeader("content-length", str(size))
-        if self._save_to_file is not None:
-            # tell the browser to save the file rather display it
-            # TODO: quote save_to_file properly
-            filename = self._save_to_file.encode("utf-8")
-            self._req.setHeader("content-disposition",
-                                'attachment; filename="%s"'
-                                % filename)
-
-    def write(self, data):
-        self._req.write(data)
-    def close(self):
-        self._req.finish()
-
-    def fail(self, why):
-        if self._opened:
-            # The content-type is already set, and the response code
-            # has already been sent, so we can't provide a clean error
-            # indication. We can emit text (which a browser might interpret
-            # as something else), and if we sent a Size header, they might
-            # notice that we've truncated the data. Keep the error message
-            # small to improve the chances of having our error response be
-            # shorter than the intended results.
-            #
-            # We don't have a lot of options, unfortunately.
-            self._req.write("problem during download\n")
-        else:
-            # We haven't written anything yet, so we can provide a sensible
-            # error message.
-            msg = str(why.type)
-            msg.replace("\n", "|")
-            self._req.setResponseCode(http.GONE, msg)
-            self._req.setHeader("content-type", "text/plain")
-            # TODO: HTML-formatted exception?
-            self._req.write(str(why))
-        self._req.finish()
-
-    def register_canceller(self, cb):
-        pass
-    def finish(self):
-        pass
-
-class FileDownloader(resource.Resource):
-    isLeaf = True
-    def __init__(self, filenode, name):
-        assert (IFileNode.providedBy(filenode)
-                or IMutableFileNode.providedBy(filenode))
-        self._filenode = filenode
-        self._name = name
-
-    def render(self, req):
-        gte = static.getTypeAndEncoding
-        ctype, encoding = gte(self._name,
-                              static.File.contentTypes,
-                              static.File.contentEncodings,
-                              defaultType="text/plain")
-        save_to_file = None
-        if get_arg(req, "save", False):
-            # TODO: make the API specification clear: should "save=" or
-            # "save=false" count?
-            save_to_file = self._name
-        wdt = WebDownloadTarget(req, ctype, encoding, save_to_file)
-        d = self._filenode.download(wdt)
-        # exceptions during download are handled by the WebDownloadTarget
-        d.addErrback(lambda why: None)
-        return server.NOT_DONE_YET
-
-class BlockingFileError(Exception):
-    """We cannot auto-create a parent directory, because there is a file in
-    the way"""
-class NoReplacementError(Exception):
-    """There was already a child by that name, and you asked me to not replace it"""
-class NoLocalDirectoryError(Exception):
-    """The localdir= directory didn't exist"""
-
-LOCALHOST = "127.0.0.1"
-
-class NeedLocalhostError:
-    implements(inevow.IResource)
-
-    def renderHTTP(self, ctx):
-        req = inevow.IRequest(ctx)
-        req.setResponseCode(http.FORBIDDEN)
-        req.setHeader("content-type", "text/plain")
-        return "localfile= or localdir= requires a local connection"
-
-class NeedAbsolutePathError:
-    implements(inevow.IResource)
-
-    def renderHTTP(self, ctx):
-        req = inevow.IRequest(ctx)
-        req.setResponseCode(http.FORBIDDEN)
-        req.setHeader("content-type", "text/plain")
-        return "localfile= or localdir= requires an absolute path"
-
-class LocalAccessDisabledError:
-    implements(inevow.IResource)
-
-    def renderHTTP(self, ctx):
-        req = inevow.IRequest(ctx)
-        req.setResponseCode(http.FORBIDDEN)
-        req.setHeader("content-type", "text/plain")
-        return "local file access is disabled"
-
-class WebError:
-    implements(inevow.IResource)
-    def __init__(self, response_code, errmsg):
-        self._response_code = response_code
-        self._errmsg = errmsg
-
-    def renderHTTP(self, ctx):
-        req = inevow.IRequest(ctx)
-        req.setResponseCode(self._response_code)
-        req.setHeader("content-type", "text/plain")
-        return self._errmsg
-
-
-class LocalFileDownloader(resource.Resource):
-    def __init__(self, filenode, local_filename):
-        self._local_filename = local_filename
-        IFileNode(filenode)
-        self._filenode = filenode
-
-    def render(self, req):
-        target = download.FileName(self._local_filename)
-        d = self._filenode.download(target)
-        def _done(res):
-            req.write(self._filenode.get_uri())
-            req.finish()
-        d.addCallback(_done)
-        return server.NOT_DONE_YET
-
-
-class FileJSONMetadata(rend.Page):
-    def __init__(self, filenode):
-        self._filenode = filenode
-
-    def renderHTTP(self, ctx):
-        req = inevow.IRequest(ctx)
-        req.setHeader("content-type", "text/plain")
-        return self.renderNode(self._filenode)
-
-    def renderNode(self, filenode):
-        file_uri = filenode.get_uri()
-        data = ("filenode",
-                {'ro_uri': file_uri,
-                 'size': filenode.get_size(),
-                 })
-        return simplejson.dumps(data, indent=1)
-
-class FileURI(FileJSONMetadata):
-    def renderNode(self, filenode):
-        file_uri = filenode.get_uri()
-        return file_uri
-
-class FileReadOnlyURI(FileJSONMetadata):
-    def renderNode(self, filenode):
-        if filenode.is_readonly():
-            return filenode.get_uri()
-        else:
-            return filenode.get_readonly().get_uri()
-
-class DirnodeWalkerMixin:
-    """Visit all nodes underneath (and including) the rootnode, one at a
-    time. For each one, call the visitor. The visitor will see the
-    IDirectoryNode before it sees any of the IFileNodes inside. If the
-    visitor returns a Deferred, I do not call the visitor again until it has
-    fired.
-    """
-
-##    def _walk_if_we_could_use_generators(self, rootnode, rootpath=()):
-##        # this is what we'd be doing if we didn't have the Deferreds and
-##        # thus could use generators
-##        yield rootpath, rootnode
-##        for childname, childnode in rootnode.list().items():
-##            childpath = rootpath + (childname,)
-##            if IFileNode.providedBy(childnode):
-##                yield childpath, childnode
-##            elif IDirectoryNode.providedBy(childnode):
-##                for res in self._walk_if_we_could_use_generators(childnode,
-##                                                                 childpath):
-##                    yield res
-
-    def walk(self, rootnode, visitor, rootpath=()):
-        d = rootnode.list()
-        def _listed(listing):
-            return listing.items()
-        d.addCallback(_listed)
-        d.addCallback(self._handle_items, visitor, rootpath)
-        return d
-
-    def _handle_items(self, items, visitor, rootpath):
-        if not items:
-            return
-        childname, (childnode, metadata) = items[0]
-        childpath = rootpath + (childname,)
-        d = defer.maybeDeferred(visitor, childpath, childnode, metadata)
-        if IDirectoryNode.providedBy(childnode):
-            d.addCallback(lambda res: self.walk(childnode, visitor, childpath))
-        d.addCallback(lambda res:
-                      self._handle_items(items[1:], visitor, rootpath))
-        return d
-
-class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin):
-    def __init__(self, dirnode, localdir):
-        self._dirnode = dirnode
-        self._localdir = localdir
-
-    def _handle(self, path, node, metadata):
-        path = tuple([p.encode("utf-8") for p in path])
-        localfile = os.path.join(self._localdir, os.sep.join(path))
-        if IDirectoryNode.providedBy(node):
-            fileutil.make_dirs(localfile)
-        elif IFileNode.providedBy(node):
-            target = download.FileName(localfile)
-            return node.download(target)
-
-    def render(self, req):
-        d = self.walk(self._dirnode, self._handle)
-        def _done(res):
-            req.setHeader("content-type", "text/plain")
-            return "operation complete"
-        d.addCallback(_done)
-        return d
-
-class DirectoryJSONMetadata(rend.Page):
-    def __init__(self, dirnode):
-        self._dirnode = dirnode
-
-    def renderHTTP(self, ctx):
-        req = inevow.IRequest(ctx)
-        req.setHeader("content-type", "text/plain")
-        return self.renderNode(self._dirnode)
-
-    def renderNode(self, node):
-        d = node.list()
-        def _got(children):
-            kids = {}
-            for name, (childnode, metadata) in children.iteritems():
-                if IFileNode.providedBy(childnode):
-                    kiduri = childnode.get_uri()
-                    kiddata = ("filenode",
-                               {'ro_uri': kiduri,
-                                'size': childnode.get_size(),
-                                'metadata': metadata,
-                                })
-                else:
-                    assert IDirectoryNode.providedBy(childnode), (childnode, children,)
-                    kiddata = ("dirnode",
-                               {'ro_uri': childnode.get_readonly_uri(),
-                                'metadata': metadata,
-                                })
-                    if not childnode.is_readonly():
-                        kiddata[1]['rw_uri'] = childnode.get_uri()
-                kids[name] = kiddata
-            contents = { 'children': kids,
-                         'ro_uri': node.get_readonly_uri(),
-                         }
-            if not node.is_readonly():
-                contents['rw_uri'] = node.get_uri()
-            data = ("dirnode", contents)
-            return simplejson.dumps(data, indent=1)
-        d.addCallback(_got)
-        return d
-
-class DirectoryURI(DirectoryJSONMetadata):
-    def renderNode(self, node):
-        return node.get_uri()
-
-class DirectoryReadonlyURI(DirectoryJSONMetadata):
-    def renderNode(self, node):
-        return node.get_readonly_uri()
-
-class RenameForm(rend.Page):
-    addSlash = True
-    docFactory = getxmlfile("rename-form.xhtml")
-
-    def __init__(self, rootname, dirnode, dirpath):
-        self._rootname = rootname
-        self._dirnode = dirnode
-        self._dirpath = dirpath
-
-    def dirpath_as_string(self):
-        return "/" + "/".join(self._dirpath)
-
-    def render_title(self, ctx, data):
-        return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
-
-    def render_header(self, ctx, data):
-        parent_directories = ("<%s>" % self._rootname,) + self._dirpath
-        num_dirs = len(parent_directories)
-
-        header = [ "Rename in directory '",
-                   "<%s>/" % self._rootname,
-                   "/".join(self._dirpath),
-                   "':", ]
-
-        if self._dirnode.is_readonly():
-            header.append(" (readonly)")
-        return ctx.tag[header]
-
-    def render_when_done(self, ctx, data):
-        return T.input(type="hidden", name="when_done", value=url.here)
-
-    def render_get_name(self, ctx, data):
-        req = inevow.IRequest(ctx)
-        name = get_arg(req, "name", "")
-        ctx.tag.attributes['value'] = name
-        return ctx.tag
-
-class POSTHandler(rend.Page):
-    def __init__(self, node, replace):
-        self._node = node
-        self._replace = replace
-
-    def _check_replacement(self, name):
-        if self._replace:
-            return defer.succeed(None)
-        d = self._node.has_child(name)
-        def _got(present):
-            if present:
-                raise NoReplacementError("There was already a child by that "
-                                         "name, and you asked me to not "
-                                         "replace it.")
-            return None
-        d.addCallback(_got)
-        return d
 
-    def _POST_mkdir(self, name):
-        d = self._check_replacement(name)
-        d.addCallback(lambda res: self._node.create_empty_directory(name))
-        d.addCallback(lambda res: "directory created")
-        return d
-
-    def _POST_mkdir_p(self, path):
-        path_ = tuple([seg.decode("utf-8") for seg in path.split('/') if seg ])
-        d = self._get_or_create_directories(self._node, path_)
-        d.addCallback(lambda node: node.get_uri())
-        return d
-
-    # this code stolen from PUTHandler: should be refactored to a more
-    # generally accesible place, perhaps...
-    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:])
-        return d
-
-    def _POST_uri(self, name, newuri):
-        d = self._check_replacement(name)
-        d.addCallback(lambda res: self._node.set_uri(name, newuri))
-        d.addCallback(lambda res: newuri)
-        return d
-
-    def _POST_delete(self, 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.
-            name = ''
-        d = self._node.delete(name)
-        d.addCallback(lambda res: "thing deleted")
-        return d
-
-    def _POST_rename(self, name, from_name, to_name):
-        d = self._check_replacement(to_name)
-        d.addCallback(lambda res: self._node.get(from_name))
-        def add_dest(child):
-            uri = child.get_uri()
-            # now actually do the rename
-            return self._node.set_uri(to_name, uri)
-        d.addCallback(add_dest)
-        def rm_src(junk):
-            return self._node.delete(from_name)
-        d.addCallback(rm_src)
-        d.addCallback(lambda res: "thing renamed")
-        return d
-
-    def _POST_upload(self, contents, name, mutable, client):
-        if mutable:
-            # SDMF: files are small, and we can only upload data.
-            contents.file.seek(0)
-            data = contents.file.read()
-            #uploadable = FileHandle(contents.file)
-            d = self._check_replacement(name)
-            d.addCallback(lambda res: self._node.has_child(name))
-            def _checked(present):
-                if present:
-                    # modify the existing one instead of creating a new
-                    # one
-                    d2 = self._node.get(name)
-                    def _got_newnode(newnode):
-                        d3 = newnode.overwrite(data)
-                        d3.addCallback(lambda res: newnode.get_uri())
-                        return d3
-                    d2.addCallback(_got_newnode)
-                else:
-                    d2 = client.create_mutable_file(data)
-                    def _uploaded(newnode):
-                        d1 = self._node.set_node(name, newnode)
-                        d1.addCallback(lambda res: newnode.get_uri())
-                        return d1
-                    d2.addCallback(_uploaded)
-                return d2
-            d.addCallback(_checked)
-        else:
-            uploadable = FileHandle(contents.file, convergence=client.convergence)
-            d = self._check_replacement(name)
-            d.addCallback(lambda res: self._node.add_file(name, uploadable))
-            def _done(newnode):
-                return newnode.get_uri()
-            d.addCallback(_done)
-        return d
-
-    def _POST_overwrite(self, contents):
-        # SDMF: files are small, and we can only upload data.
-        contents.file.seek(0)
-        data = contents.file.read()
-        # TODO: 'name' handling needs review
-        d = defer.succeed(self._node)
-        def _got_child_overwrite(child_node):
-            child_node.overwrite(data)
-            return child_node.get_uri()
-        d.addCallback(_got_child_overwrite)
-        return d
-
-    def _POST_check(self, name):
-        d = self._node.get(name)
-        def _got_child_check(child_node):
-            d2 = child_node.check()
-            def _done(res):
-                log.msg("checked %s, results %s" % (child_node, res),
-                        facility="tahoe.webish", level=log.NOISY)
-                return str(res)
-            d2.addCallback(_done)
-            return d2
-        d.addCallback(_got_child_check)
-        return d
-
-    def _POST_set_children(self, children):
-        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')))
-
-        d = self._node.set_children(cs)
-        d.addCallback(lambda res: "Okay so I did it.")
-        return d
-
-    def renderHTTP(self, ctx):
-        req = inevow.IRequest(ctx)
-
-        t = get_arg(req, "t")
-        assert t is not None
-
-        charset = get_arg(req, "_charset", "utf-8")
-
-        name = get_arg(req, "name", None)
-        if name and "/" in name:
-            req.setResponseCode(http.BAD_REQUEST)
-            req.setHeader("content-type", "text/plain")
-            return "name= may not contain a slash"
-        if name is not None:
-            name = name.strip()
-            name = name.decode(charset)
-            assert isinstance(name, unicode)
-        # we allow the user to delete an empty-named file, but not to create
-        # them, since that's an easy and confusing mistake to make
-
-        when_done = get_arg(req, "when_done", None)
-        if not boolean_of_arg(get_arg(req, "replace", "true")):
-            self._replace = False
-
-        if t == "mkdir":
-            if not name:
-                raise RuntimeError("mkdir requires a name")
-            d = self._POST_mkdir(name)
-        elif t == "mkdir-p":
-            path = get_arg(req, "path")
-            if not path:
-                raise RuntimeError("mkdir-p requires a path")
-            d = self._POST_mkdir_p(path)
-        elif t == "uri":
-            if not name:
-                raise RuntimeError("set-uri requires a name")
-            newuri = get_arg(req, "uri")
-            assert newuri is not None
-            d = self._POST_uri(name, newuri)
-        elif t == "delete":
-            d = self._POST_delete(name)
-        elif t == "rename":
-            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)
-            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 RuntimeError("rename requires from_name and to_name")
-            if not IDirectoryNode.providedBy(self._node):
-                raise RuntimeError("rename must only be called on directories")
-            for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
-                if v and "/" in v:
-                    req.setResponseCode(http.BAD_REQUEST)
-                    req.setHeader("content-type", "text/plain")
-                    return "%s= may not contain a slash" % (k,)
-            d = self._POST_rename(name, from_name, to_name)
-        elif t == "upload":
-            contents = req.fields["file"]
-            name = name or contents.filename
-            if name is not None:
-                name = name.strip()
-            if not name:
-                # this prohibts empty, missing, and all-whitespace filenames
-                raise RuntimeError("upload requires a name")
-            name = name.decode(charset)
-            assert isinstance(name, unicode)
-            mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
-            d = self._POST_upload(contents, name, mutable, IClient(ctx))
-        elif t == "overwrite":
-            contents = req.fields["file"]
-            d = self._POST_overwrite(contents)
-        elif t == "check":
-            d = self._POST_check(name)
-        elif t == "set_children":
-            req.content.seek(0)
-            body = req.content.read()
-            try:
-                children = simplejson.loads(body)
-            except ValueError, le:
-                le.args = tuple(le.args + (body,))
-                # TODO test handling of bad JSON
-                raise
-            d = self._POST_set_children(children)
-        else:
-            req.setResponseCode(http.BAD_REQUEST)
-            req.setHeader("content-type", "text/plain")
-            return "BAD t=%s" % t
-        if when_done:
-            d.addCallback(lambda res: url.URL.fromString(when_done))
-        def _check_replacement(f):
-            # TODO: make this more human-friendly: maybe send them to the
-            # when_done page but with an extra query-arg that will display
-            # the error message in a big box at the top of the page. The
-            # directory page that when_done= usually points to accepts a
-            # result= argument.. use that.
-            f.trap(NoReplacementError)
-            req.setResponseCode(http.CONFLICT)
-            req.setHeader("content-type", "text/plain")
-            return str(f.value)
-        d.addErrback(_check_replacement)
-        return d
-
-class DELETEHandler(rend.Page):
-    def __init__(self, node, name):
-        self._node = node
-        self._name = name
-
-    def renderHTTP(self, ctx):
-        req = inevow.IRequest(ctx)
-        d = self._node.delete(self._name)
-        def _done(res):
-            # what should this return??
-            return "%s deleted" % self._name.encode("utf-8")
-        d.addCallback(_done)
-        def _trap_missing(f):
-            f.trap(KeyError)
-            req.setResponseCode(http.NOT_FOUND)
-            req.setHeader("content-type", "text/plain")
-            return "no such child %s" % self._name.encode("utf-8")
-        d.addErrback(_trap_missing)
-        return d
-
-class PUTHandler(rend.Page):
-    def __init__(self, node, path, t, localfile, localdir, replace):
-        self._node = node
-        self._path = path
-        self._t = t
-        self._localfile = localfile
-        self._localdir = localdir
-        self._replace = replace
-
-    def renderHTTP(self, ctx):
-        client = IClient(ctx)
-        req = inevow.IRequest(ctx)
-        t = self._t
-        localfile = self._localfile
-        localdir = self._localdir
-
-        if t == "upload" and not (localfile or localdir):
-            req.setResponseCode(http.BAD_REQUEST)
-            req.setHeader("content-type", "text/plain")
-            return "t=upload requires localfile= or localdir="
-
-        # we must traverse the path, creating new directories as necessary
-        d = self._get_or_create_directories(self._node, self._path[:-1])
-        name = self._path[-1]
-        d.addCallback(self._check_replacement, name, self._replace)
-        if t == "upload":
-            if localfile:
-                d.addCallback(self._upload_localfile, localfile, name, convergence=client.convergence)
-            else:
-                # localdir
-                # take the last step
-                d.addCallback(self._get_or_create_directories, self._path[-1:])
-                d.addCallback(self._upload_localdir, localdir, convergence=client.convergence)
-        elif t == "uri":
-            d.addCallback(self._attach_uri, req.content, name)
-        elif t == "mkdir":
-            d.addCallback(self._mkdir, name)
-        else:
-            d.addCallback(self._upload_file, req.content, name, convergence=client.convergence)
-
-        def _transform_error(f):
-            errors = {BlockingFileError: http.BAD_REQUEST,
-                      NoReplacementError: http.CONFLICT,
-                      NoLocalDirectoryError: http.BAD_REQUEST,
-                      }
-            for k,v in errors.items():
-                if f.check(k):
-                    req.setResponseCode(v)
-                    req.setHeader("content-type", "text/plain")
-                    return str(f.value)
-            return f
-        d.addErrback(_transform_error)
-        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:])
-        return d
-
-    def _check_replacement(self, node, name, replace):
-        if replace:
-            return node
-        d = node.has_child(name)
-        def _got(present):
-            if present:
-                raise NoReplacementError("There was already a child by that "
-                                         "name, and you asked me to not "
-                                         "replace it.")
-            return node
-        d.addCallback(_got)
-        return d
-
-    def _mkdir(self, node, name):
-        d = node.create_empty_directory(name)
-        def _done(newnode):
-            return newnode.get_uri()
-        d.addCallback(_done)
-        return d
-
-    def _upload_file(self, node, contents, name, convergence):
-        uploadable = FileHandle(contents, convergence=convergence)
-        d = node.add_file(name, uploadable)
-        def _done(filenode):
-            log.msg("webish upload complete",
-                    facility="tahoe.webish", level=log.NOISY)
-            return filenode.get_uri()
-        d.addCallback(_done)
-        return d
-
-    def _upload_localfile(self, node, localfile, name, convergence):
-        uploadable = FileName(localfile, convergence=convergence)
-        d = node.add_file(name, uploadable)
-        d.addCallback(lambda filenode: filenode.get_uri())
-        return d
-
-    def _attach_uri(self, parentnode, contents, name):
-        newuri = contents.read().strip()
-        d = parentnode.set_uri(name, newuri)
-        def _done(res):
-            return newuri
-        d.addCallback(_done)
-        return d
-
-    def _upload_localdir(self, node, localdir, convergence):
-        # build up a list of files to upload. TODO: for now, these files and
-        # directories must have UTF-8 encoded filenames: anything else will
-        # cause the upload to break.
-        all_files = []
-        all_dirs = []
-        msg = "No files to upload! %s is empty" % localdir
-        if not os.path.exists(localdir):
-            msg = "%s doesn't exist!" % localdir
-            raise NoLocalDirectoryError(msg)
-        for root, dirs, files in os.walk(localdir):
-            if root == localdir:
-                path = ()
-            else:
-                relative_root = root[len(localdir)+1:]
-                path = tuple(relative_root.split(os.sep))
-            for d in dirs:
-                this_dir = path + (d,)
-                this_dir = tuple([p.decode("utf-8") for p in this_dir])
-                all_dirs.append(this_dir)
-            for f in files:
-                this_file = path + (f,)
-                this_file = tuple([p.decode("utf-8") for p in this_file])
-                all_files.append(this_file)
-        d = defer.succeed(msg)
-        for dir in all_dirs:
-            if dir:
-                d.addCallback(self._makedir, node, dir)
-        for f in all_files:
-            d.addCallback(self._upload_one_file, node, localdir, f, convergence=convergence)
-        return d
-
-    def _makedir(self, res, node, dir):
-        d = defer.succeed(None)
-        # get the parent. As long as os.walk gives us parents before
-        # children, this ought to work
-        d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
-        # then create the child directory
-        d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
-        return d
-
-    def _upload_one_file(self, res, node, localdir, f, convergence):
-        # get the parent. We can be sure this exists because we already
-        # went through and created all the directories we require.
-        localfile = os.path.join(localdir, *f)
-        d = node.get_child_at_path(f[:-1])
-        d.addCallback(self._upload_localfile, localfile, f[-1], convergence=convergence)
-        return d
-
-
-class Manifest(rend.Page):
-    docFactory = getxmlfile("manifest.xhtml")
-    def __init__(self, dirnode, dirpath):
-        self._dirnode = dirnode
-        self._dirpath = dirpath
-
-    def dirpath_as_string(self):
-        return "/" + "/".join(self._dirpath)
-
-    def render_title(self, ctx):
-        return T.title["Manifest of %s" % self.dirpath_as_string()]
-
-    def render_header(self, ctx):
-        return T.p["Manifest of %s" % self.dirpath_as_string()]
-
-    def data_items(self, ctx, data):
-        return self._dirnode.build_manifest()
-
-    def render_row(self, ctx, refresh_cap):
-        ctx.fillSlots("refresh_capability", refresh_cap)
-        return ctx.tag
-
-class DeepSize(rend.Page):
-
-    def __init__(self, dirnode, dirpath):
-        self._dirnode = dirnode
-        self._dirpath = dirpath
-
-    def renderHTTP(self, ctx):
-        inevow.IRequest(ctx).setHeader("content-type", "text/plain")
-        d = self._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)
-        return d
-
-class DeepStats(rend.Page):
-
-    def __init__(self, dirnode, dirpath):
-        self._dirnode = dirnode
-        self._dirpath = dirpath
-
-    def renderHTTP(self, ctx):
-        inevow.IRequest(ctx).setHeader("content-type", "text/plain")
-        d = self._dirnode.deep_stats()
-        d.addCallback(simplejson.dumps, indent=1)
-        return d
-
-class ChildError:
-    implements(inevow.IResource)
-    def renderHTTP(self, ctx):
-        req = inevow.IRequest(ctx)
-        req.setResponseCode(http.BAD_REQUEST)
-        req.setHeader("content-type", "text/plain")
-        return self.text
-
-def child_error(text):
-    ce = ChildError()
-    ce.text = text
-    return ce, ()
-
-class VDrive(rend.Page):
-
-    def __init__(self, node, name):
-        self.node = node
-        self.name = name
-
-    def get_child_at_path(self, path):
-        if path:
-            return self.node.get_child_at_path(path)
-        return defer.succeed(self.node)
-
-    def locateChild(self, ctx, segments):
-        req = inevow.IRequest(ctx)
-        method = req.method
-        path = tuple([seg.decode("utf-8") for seg in segments])
-
-        t = get_arg(req, "t", "")
-        localfile = get_arg(req, "localfile", None)
-        if localfile is not None:
-            if localfile != os.path.abspath(localfile):
-                return NeedAbsolutePathError(), ()
-        localdir = get_arg(req, "localdir", None)
-        if localdir is not None:
-            if localdir != os.path.abspath(localdir):
-                return NeedAbsolutePathError(), ()
-        if localfile or localdir:
-            if not ILocalAccess(ctx).local_access_is_allowed():
-                return LocalAccessDisabledError(), ()
-            if req.getHost().host != LOCALHOST:
-                return NeedLocalhostError(), ()
-        # TODO: think about clobbering/revealing config files and node secrets
-
-        replace = boolean_of_arg(get_arg(req, "replace", "true"))
-
-        if method == "GET":
-            # the node must exist, and our operation will be performed on the
-            # node itself.
-            d = self.get_child_at_path(path)
-            def file_or_dir(node):
-                if (IFileNode.providedBy(node)
-                    or IMutableFileNode.providedBy(node)):
-                    filename = "unknown"
-                    if path:
-                        filename = path[-1]
-                    filename = get_arg(req, "filename", filename)
-                    if t == "download":
-                        if localfile:
-                            # write contents to a local file
-                            return LocalFileDownloader(node, localfile), ()
-                        # send contents as the result
-                        return FileDownloader(node, filename), ()
-                    elif t == "":
-                        # send contents as the result
-                        return FileDownloader(node, filename), ()
-                    elif t == "json":
-                        return FileJSONMetadata(node), ()
-                    elif t == "uri":
-                        return FileURI(node), ()
-                    elif t == "readonly-uri":
-                        return FileReadOnlyURI(node), ()
-                    else:
-                        return child_error("bad t=%s" % t)
-                elif IDirectoryNode.providedBy(node):
-                    if t == "download":
-                        if localdir:
-                            # recursive download to a local directory
-                            return LocalDirectoryDownloader(node, localdir), ()
-                        return child_error("t=download requires localdir=")
-                    elif t == "":
-                        # send an HTML representation of the directory
-                        return Directory(self.name, node, path), ()
-                    elif t == "json":
-                        return DirectoryJSONMetadata(node), ()
-                    elif t == "uri":
-                        return DirectoryURI(node), ()
-                    elif t == "readonly-uri":
-                        return DirectoryReadonlyURI(node), ()
-                    elif t == "manifest":
-                        return Manifest(node, path), ()
-                    elif t == "deep-size":
-                        return DeepSize(node, path), ()
-                    elif t == "deep-stats":
-                        return DeepStats(node, path), ()
-                    elif t == 'rename-form':
-                        return RenameForm(self.name, node, path), ()
-                    else:
-                        return child_error("bad t=%s" % t)
-                else:
-                    return child_error("unknown node type")
-            d.addCallback(file_or_dir)
-        elif method == "POST":
-            # the node must exist, and our operation will be performed on the
-            # node itself.
-            d = self.get_child_at_path(path)
-            def _got_POST(node):
-                return POSTHandler(node, replace), ()
-            d.addCallback(_got_POST)
-        elif method == "DELETE":
-            # the node must exist, and our operation will be performed on its
-            # parent node.
-            assert path # you can't delete the root
-            d = self.get_child_at_path(path[:-1])
-            def _got_DELETE(node):
-                return DELETEHandler(node, path[-1]), ()
-            d.addCallback(_got_DELETE)
-        elif method in ("PUT",):
-            # the node may or may not exist, and our operation may involve
-            # all the ancestors of the node.
-            return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
-        else:
-            return rend.NotFound
-        return d
-
-class Root(rend.Page):
-
-    addSlash = True
-    docFactory = getxmlfile("welcome.xhtml")
-
-    def locateChild(self, ctx, segments):
-        client = IClient(ctx)
-        req = inevow.IRequest(ctx)
-
-        if not segments:
-            return rend.Page.locateChild(self, ctx, segments)
-
-        if segments[0] == "file":
-            if len(segments) < 2:
-                return rend.Page.locateChild(self, ctx, segments)
-            filecap = segments[1]
-            node = client.create_node_from_uri(filecap)
-            name = segments[-1]
-            return FileDownloader(node, name), ()
-
-        if segments[0] != "uri":
-            return rend.Page.locateChild(self, ctx, segments)
-
-        segments = list(segments)
-        while segments and not segments[-1]:
-            segments.pop()
-        if not segments:
-            segments.append('')
-        segments = tuple(segments)
-
-        if len(segments) == 1 or segments[1] == '':
-            uri = get_arg(req, "uri", None)
-            if uri is not None:
-                there = url.URL.fromContext(ctx)
-                there = there.clear("uri")
-                there = there.child("uri").child(uri)
-                return there, ()
-
-        if len(segments) == 1:
-            # /uri
-            if req.method == "PUT":
-                # either "PUT /uri" to create an unlinked file, or
-                # "PUT /uri?t=mkdir" to create an unlinked directory
-                t = get_arg(req, "t", "").strip()
-                if t == "":
-                    mutable = bool(get_arg(req, "mutable", "").strip())
-                    if mutable:
-                        return unlinked.UnlinkedPUTSSKUploader(), ()
-                    else:
-                        return unlinked.UnlinkedPUTCHKUploader(), ()
-                if t == "mkdir":
-                    return unlinked.UnlinkedPUTCreateDirectory(), ()
-                errmsg = "/uri only accepts PUT and PUT?t=mkdir"
-                return WebError(http.BAD_REQUEST, errmsg), ()
-
-            elif req.method == "POST":
-                # "POST /uri?t=upload&file=newfile" to upload an
-                # unlinked file or "POST /uri?t=mkdir" to create a
-                # new directory
-                t = get_arg(req, "t", "").strip()
-                if t in ("", "upload"):
-                    mutable = bool(get_arg(req, "mutable", "").strip())
-                    if mutable:
-                        return unlinked.UnlinkedPOSTSSKUploader(), ()
-                    else:
-                        return unlinked.UnlinkedPOSTCHKUploader(client, req), ()
-                if t == "mkdir":
-                    return unlinked.UnlinkedPOSTCreateDirectory(), ()
-                errmsg = "/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, and POST?t=mkdir"
-                return WebError(http.BAD_REQUEST, errmsg), ()
-
-        if len(segments) < 2:
-            return rend.NotFound
-
-        uri = segments[1]
-        d = defer.maybeDeferred(client.create_node_from_uri, uri)
-        d.addCallback(lambda node: VDrive(node, uri))
-        d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
-        def _trap_KeyError(f):
-            f.trap(KeyError)
-            return rend.FourOhFour(), ()
-        d.addErrback(_trap_KeyError)
-        return d
-
-    child_webform_css = webform.defaultCSS
-    child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
-
-    child_provisioning = provisioning.ProvisioningTool()
-    child_status = status.Status()
-    child_helper_status = status.HelperStatus()
-    child_statistics = status.Statistics()
-
-    def data_version(self, ctx, data):
-        return get_package_versions_string()
-    def data_import_path(self, ctx, data):
-        return str(allmydata)
-    def data_my_nodeid(self, ctx, data):
-        return idlib.nodeid_b2a(IClient(ctx).nodeid)
-
-    def render_services(self, ctx, data):
-        ul = T.ul()
-        client = IClient(ctx)
-        try:
-            ss = client.getServiceNamed("storage")
-            allocated_s = abbreviate_size(ss.allocated_size())
-            allocated = "about %s allocated" % allocated_s
-            sizelimit = "no size limit"
-            if ss.sizelimit is not None:
-                sizelimit = "size limit is %s" % abbreviate_size(ss.sizelimit)
-            ul[T.li["Storage Server: %s, %s" % (allocated, sizelimit)]]
-        except KeyError:
-            ul[T.li["Not running storage server"]]
-
-        try:
-            h = client.getServiceNamed("helper")
-            stats = h.get_stats()
-            active_uploads = stats["chk_upload_helper.active_uploads"]
-            ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
-        except KeyError:
-            ul[T.li["Not running helper"]]
-
-        return ctx.tag[ul]
-
-    def data_introducer_furl(self, ctx, data):
-        return IClient(ctx).introducer_furl
-    def data_connected_to_introducer(self, ctx, data):
-        if IClient(ctx).connected_to_introducer():
-            return "yes"
-        return "no"
-
-    def data_helper_furl(self, ctx, data):
-        try:
-            uploader = IClient(ctx).getServiceNamed("uploader")
-        except KeyError:
-            return None
-        furl, connected = uploader.get_helper_info()
-        return furl
-    def data_connected_to_helper(self, ctx, data):
-        try:
-            uploader = IClient(ctx).getServiceNamed("uploader")
-        except KeyError:
-            return "no" # we don't even have an Uploader
-        furl, connected = uploader.get_helper_info()
-        if connected:
-            return "yes"
-        return "no"
-
-    def data_known_storage_servers(self, ctx, data):
-        ic = IClient(ctx).introducer_client
-        servers = [c
-                   for c in ic.get_all_connectors().values()
-                   if c.service_name == "storage"]
-        return len(servers)
-
-    def data_connected_storage_servers(self, ctx, data):
-        ic = IClient(ctx).introducer_client
-        return len(ic.get_all_connections_for("storage"))
-
-    def data_services(self, ctx, data):
-        ic = IClient(ctx).introducer_client
-        c = [ (service_name, nodeid, rsc)
-              for (nodeid, service_name), rsc
-              in ic.get_all_connectors().items() ]
-        c.sort()
-        return c
-
-    def render_service_row(self, ctx, data):
-        (service_name, nodeid, rsc) = data
-        ctx.fillSlots("peerid", "%s %s" % (idlib.nodeid_b2a(nodeid),
-                                           rsc.nickname))
-        if rsc.rref:
-            rhost = rsc.remote_host
-            if nodeid == IClient(ctx).nodeid:
-                rhost_s = "(loopback)"
-            elif isinstance(rhost, address.IPv4Address):
-                rhost_s = "%s:%d" % (rhost.host, rhost.port)
-            else:
-                rhost_s = str(rhost)
-            connected = "Yes: to " + rhost_s
-            since = rsc.last_connect_time
-        else:
-            connected = "No"
-            since = rsc.last_loss_time
-
-        TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
-        ctx.fillSlots("connected", connected)
-        ctx.fillSlots("since", time.strftime(TIME_FORMAT, time.localtime(since)))
-        ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
-                                                 time.localtime(rsc.announcement_time)))
-        ctx.fillSlots("version", rsc.version)
-        ctx.fillSlots("service_name", rsc.service_name)
-
-        return ctx.tag
-
-    def render_download_form(self, ctx, data):
-        # this is a form where users can download files by URI
-        form = T.form(action="uri", method="get",
-                      enctype="multipart/form-data")[
-            T.fieldset[
-            T.legend(class_="freeform-form-label")["Download a file"],
-            "URI to download: ",
-            T.input(type="text", name="uri"), " ",
-            "Filename to download as: ",
-            T.input(type="text", name="filename"), " ",
-            T.input(type="submit", value="Download!"),
-            ]]
-        return T.div[form]
-
-    def render_view_form(self, ctx, data):
-        # this is a form where users can download files by URI, or jump to a
-        # named directory
-        form = T.form(action="uri", method="get",
-                      enctype="multipart/form-data")[
-            T.fieldset[
-            T.legend(class_="freeform-form-label")["View a file or directory"],
-            "URI to view: ",
-            T.input(type="text", name="uri"), " ",
-            T.input(type="submit", value="View!"),
-            ]]
-        return T.div[form]
-
-    def render_upload_form(self, ctx, data):
-        # this is a form where users can upload unlinked files
-        form = T.form(action="uri", method="post",
-                      enctype="multipart/form-data")[
-            T.fieldset[
-            T.legend(class_="freeform-form-label")["Upload a file"],
-            "Choose a file: ",
-            T.input(type="file", name="file", class_="freeform-input-file"),
-            T.input(type="hidden", name="t", value="upload"),
-            " Mutable?:", T.input(type="checkbox", name="mutable"),
-            T.input(type="submit", value="Upload!"),
-            ]]
-        return T.div[form]
-
-    def render_mkdir_form(self, ctx, data):
-        # this is a form where users can create new directories
-        form = T.form(action="uri", method="post",
-                      enctype="multipart/form-data")[
-            T.fieldset[
-            T.legend(class_="freeform-form-label")["Create a directory"],
-            T.input(type="hidden", name="t", value="mkdir"),
-            T.input(type="hidden", name="redirect_to_result", value="true"),
-            T.input(type="submit", value="Create Directory!"),
-            ]]
-        return T.div[form]
-
-
-class LocalAccess:
-    implements(ILocalAccess)
-    def __init__(self):
-        self.local_access = False
-    def local_access_is_allowed(self):
-        return self.local_access
 
 class WebishServer(service.MultiService):
     name = "webish"
-    root_class = Root
+    root_class = root.Root
 
     def __init__(self, webport, nodeurl_path=None):
         service.MultiService.__init__(self)
@@ -1633,8 +123,6 @@ class WebishServer(service.MultiService):
         self.root = self.root_class()
         self.site = site = appserver.NevowSite(self.root)
         self.site.requestFactory = MyRequest
-        self.allow_local = LocalAccess()
-        self.site.remember(self.allow_local, ILocalAccess)
         s = strports.service(webport, site)
         s.setServiceParent(self)
         self.listener = s # stash it so the tests can query for the portnum
@@ -1642,9 +130,6 @@ class WebishServer(service.MultiService):
         if nodeurl_path:
             self._started.addCallback(self._write_nodeurl_file, nodeurl_path)
 
-    def allow_local_access(self, enable=True):
-        self.allow_local.local_access = enable
-
     def startService(self):
         service.MultiService.startService(self)
         # to make various services available to render_* methods, we stash a
@@ -1655,6 +140,7 @@ class WebishServer(service.MultiService):
         # I thought you could do the same with an existing interface, but
         # apparently 'ISite' does not exist
         #self.site._client = self.parent
+        self.site.remember(MyExceptionHandler(), inevow.ICanHandleException)
         self._started.callback(None)
 
     def _write_nodeurl_file(self, junk, nodeurl_path):