web: add 'more info' pages for files and directories, move URI/checker-buttons/deep...
authorBrian Warner <warner@lothar.com>
Thu, 18 Sep 2008 05:00:41 +0000 (22:00 -0700)
committerBrian Warner <warner@lothar.com>
Thu, 18 Sep 2008 05:00:41 +0000 (22:00 -0700)
docs/webapi.txt
src/allmydata/test/test_system.py
src/allmydata/test/test_web.py
src/allmydata/web/directory.py
src/allmydata/web/directory.xhtml
src/allmydata/web/filenode.py
src/allmydata/web/info.py [new file with mode: 0644]
src/allmydata/web/info.xhtml [new file with mode: 0644]

index b558fda7f2624f167ef64029c455751bd85752b3..97f3baf5d61690775372e9bc8514473a1a07fcff 100644 (file)
@@ -459,6 +459,25 @@ GET /named/$FILECAP/FILENAME
  this form can *only* be used with file caps; it is an error to use a
  directory cap after the /named/ prefix.
 
+=== Get Information About A File Or Directory (as HTML) ===
+
+GET /uri/$FILECAP?t=info
+GET /uri/$DIRCAP/?t=info
+GET /uri/$DIRCAP/[SUBDIRS../]SUBDIR/?t=info
+GET /uri/$DIRCAP/[SUBDIRS../]FILENAME?t=info
+
+  This returns a human-oriented HTML page with more detail about the selected
+  file or directory object. This page contains the following items:
+
+   object size
+   storage index
+   JSON representation
+   raw contents (text/plain)
+   access caps (URIs): verify-cap, read-cap, write-cap (for mutable objects)
+   check/verify/repair form
+   deep-check/deep-size/deep-stats/manifest (for directories)
+   replace-conents form (for mutable files)
+
 === Creating a Directory ===
 
 POST /uri?t=mkdir
index 6f557a05c881e8899b8c47454b93d5de7ad2383e..bb29acb5b60df747d6dfd448b29fb73be5425955 100644 (file)
@@ -1986,10 +1986,10 @@ class MutableChecker(SystemTestMixin, unittest.TestCase):
 
         return d
 
-class DeepCheck(SystemTestMixin, unittest.TestCase):
+class DeepCheckWeb(SystemTestMixin, unittest.TestCase):
     # construct a small directory tree (with one dir, one immutable file, one
-    # mutable file, one LIT file, and a loop), and then check it in various
-    # ways.
+    # mutable file, one LIT file, and a loop), and then check/examine it in
+    # various ways.
 
     def set_up_tree(self, ignored):
         # 2.9s
@@ -2176,19 +2176,23 @@ class DeepCheck(SystemTestMixin, unittest.TestCase):
 
     def web_json(self, n, **kwargs):
         kwargs["output"] = "json"
-        return self.web(n, "POST", **kwargs)
+        d = self.web(n, "POST", **kwargs)
+        d.addCallback(self.decode_json)
+        return d
+
+    def decode_json(self, (s,url)):
+        try:
+            data = simplejson.loads(s)
+        except ValueError:
+            self.fail("%s: not JSON: '%s'" % (url, s))
+        return data
 
     def web(self, n, method="GET", **kwargs):
+        # returns (data, url)
         url = (self.webish_url + "uri/%s" % urllib.quote(n.get_uri())
                + "?" + "&".join(["%s=%s" % (k,v) for (k,v) in kwargs.items()]))
         d = getPage(url, method=method)
-        def _decode(s):
-            try:
-                data = simplejson.loads(s)
-            except ValueError:
-                self.fail("%s: not JSON: '%s'" % (url, s))
-            return data
-        d.addCallback(_decode)
+        d.addCallback(lambda data: (data,url))
         return d
 
     def json_check_is_healthy(self, data, n, where, incomplete=False):
@@ -2276,6 +2280,7 @@ class DeepCheck(SystemTestMixin, unittest.TestCase):
 
         # stats
         d.addCallback(lambda ign: self.web(self.root, t="deep-stats"))
+        d.addCallback(self.decode_json)
         d.addCallback(self.json_check_stats, "deep-stats")
 
         # check, no verify
@@ -2344,4 +2349,11 @@ class DeepCheck(SystemTestMixin, unittest.TestCase):
                       self.web_json(self.root, t="deep-check", verify="true", repair="true"))
         d.addCallback(self.json_full_deepcheck_and_repair_is_healthy, self.root, "root")
 
+        # now look at t=info
+        d.addCallback(lambda ign: self.web(self.root, t="info"))
+        # TODO: examine the output
+        d.addCallback(lambda ign: self.web(self.mutable, t="info"))
+        d.addCallback(lambda ign: self.web(self.large, t="info"))
+        d.addCallback(lambda ign: self.web(self.small, t="info"))
+
         return d
index 3c4d0cc6506e2a4043db7ddedee936f171417e6e..16da92e1a99d80067cddd24083845877bb4c8319 100644 (file)
@@ -1200,51 +1200,17 @@ class Web(WebMixin, unittest.TestCase):
                                                              NEW2_CONTENTS))
 
         # finally list the directory, since mutable files are displayed
-        # differently
+        # slightly differently
 
         d.addCallback(lambda res:
                       self.GET(self.public_url + "/foo/",
                                followRedirect=True))
         def _check_page(res):
             # TODO: assert more about the contents
-            self.failUnless("Overwrite" in res)
-            self.failUnless("Choose new file:" in res)
+            self.failUnless("SSK" in res)
             return res
         d.addCallback(_check_page)
 
-        # test that clicking on the "overwrite" button works
-        EVEN_NEWER_CONTENTS = NEWER_CONTENTS + "even newer\n"
-        def _parse_overwrite_form_and_submit(res):
-
-            OVERWRITE_FORM_RE=re.compile('<form action="([^"]*)" method="post" .*<input type="hidden" name="t" value="upload" /><input type="hidden" name="when_done" value="([^"]*)" />', re.I)
-            mo = OVERWRITE_FORM_RE.search(res)
-            self.failUnless(mo, "overwrite form not found in '" + res +
-                            "', in which the overwrite form was not found")
-            formaction=mo.group(1)
-            formwhendone=mo.group(2)
-
-            fileurl = "../../../uri/" + urllib.quote(self._mutable_uri)
-            self.failUnlessEqual(formaction, fileurl)
-            # to POST, we need to absoluteify the URL
-            new_formaction = "/uri/%s" % urllib.quote(self._mutable_uri)
-            self.failUnlessEqual(formwhendone,
-                                 "../uri/%s/" % urllib.quote(self._foo_uri))
-            return self.POST(new_formaction,
-                             t="upload",
-                             file=("new.txt", EVEN_NEWER_CONTENTS),
-                             when_done=formwhendone,
-                             followRedirect=False)
-        d.addCallback(_parse_overwrite_form_and_submit)
-        # This will redirect us to ../uri/$FOOURI, rather than
-        # ../uri/$PARENT/foo, but apparently twisted.web.client absolutifies
-        # the redirect for us, and remember that shouldRedirect prepends
-        # self.webish_url for us.
-        d.addBoth(self.shouldRedirect,
-                  "/uri/%s/" % urllib.quote(self._foo_uri),
-                  which="test_POST_upload_mutable.overwrite")
-        d.addCallback(lambda res:
-                      self.failUnlessMutableChildContentsAre(fn, u"new.txt",
-                                                             EVEN_NEWER_CONTENTS))
         d.addCallback(lambda res: self._foo_node.get(u"new.txt"))
         def _got3(newnode):
             self.failUnless(IMutableFileNode.providedBy(newnode))
@@ -1297,14 +1263,14 @@ class Web(WebMixin, unittest.TestCase):
         d.addCallback(lambda res:
                       self.GET("/uri/%s" % urllib.quote(self._mutable_uri)))
         d.addCallback(lambda res:
-                      self.failUnlessEqual(res, EVEN_NEWER_CONTENTS))
+                      self.failUnlessEqual(res, NEW2_CONTENTS))
 
         # and that HEAD computes the size correctly
         d.addCallback(lambda res:
                       self.HEAD(self.public_url + "/foo/new.txt"))
         def _got_headers(headers):
             self.failUnlessEqual(headers["content-length"][0],
-                                 str(len(EVEN_NEWER_CONTENTS)))
+                                 str(len(NEW2_CONTENTS)))
             self.failUnlessEqual(headers["content-type"], ["text/plain"])
         d.addCallback(_got_headers)
 
index e6cd3332fa588404744c0f9ba0596a8417c18e4a..a26007738543e1f877331f2a53aeb0d929224a02 100644 (file)
@@ -23,6 +23,7 @@ from allmydata.web.filenode import ReplaceMeMixin, \
      FileNodeHandler, PlaceHolderNodeHandler
 from allmydata.web.checker_results import CheckerResults, \
      CheckAndRepairResults, DeepCheckResults, DeepCheckAndRepairResults
+from allmydata.web.info import MoreInfo
 
 class BlockingFileError(Exception):
     # TODO: catch and transform
@@ -131,6 +132,8 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
 
         if t == "json":
             return DirectoryJSONMetadata(ctx, self.node)
+        if t == "info":
+            return MoreInfo(self.node)
         if t == "uri":
             return DirectoryURI(ctx, self.node)
         if t == "readonly-uri":
@@ -467,20 +470,6 @@ class DirectoryAsHTML(rend.Page):
 
         ctx.fillSlots("delete", delete)
         ctx.fillSlots("rename", rename)
-        if IDirectoryNode.providedBy(target):
-            check_url = "%s/uri/%s/" % (root, urllib.quote(target.get_uri()))
-            check_done_url = "../../uri/%s/" % urllib.quote(self.node.get_uri())
-        else:
-            check_url = "%s/uri/%s" % (root, urllib.quote(target.get_uri()))
-            check_done_url = "../uri/%s/" % urllib.quote(self.node.get_uri())
-        check = T.form(action=check_url, method="post")[
-            T.input(type='hidden', name='t', value='check'),
-            T.input(type='hidden', name='return_to', value=check_done_url),
-            T.input(type='submit', value='check', name="check"),
-            ]
-        ctx.fillSlots("overwrite",
-                      self.build_overwrite_form(ctx, name, target))
-        ctx.fillSlots("check", check)
 
         times = []
         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
@@ -515,7 +504,7 @@ class DirectoryAsHTML(rend.Page):
             ctx.fillSlots("size", "?")
 
             text_plain_url = "%s/file/%s/@@named=/foo.txt" % (root, quoted_uri)
-            text_plain_tag = T.a(href=text_plain_url)["text/plain"]
+            info_link = "%s?t=info" % name
 
         elif IFileNode.providedBy(target):
             dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, urllib.quote(name))
@@ -527,8 +516,7 @@ class DirectoryAsHTML(rend.Page):
             ctx.fillSlots("size", target.get_size())
 
             text_plain_url = "%s/file/%s/@@named=/foo.txt" % (root, quoted_uri)
-            text_plain_tag = T.a(href=text_plain_url)["text/plain"]
-
+            info_link = "%s?t=info" % name
 
         elif IDirectoryNode.providedBy(target):
             # directory
@@ -541,45 +529,14 @@ class DirectoryAsHTML(rend.Page):
                 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])
+            info_link = "%s/?t=info" % name
 
-        ctx.fillSlots("data", childdata)
-
-        results = "--"
-        # TODO: include a link to see more results, including timestamps
-        # TODO: use a sparkline
-        ctx.fillSlots("checker_results", results)
+        ctx.fillSlots("info", T.a(href=info_link)["More Info"])
 
         return ctx.tag
 
     def render_forms(self, ctx, data):
         forms = []
-        deep_check = T.form(action=".", method="post",
-                            enctype="multipart/form-data")[
-            T.fieldset[
-            T.input(type="hidden", name="t", value="deep-check"),
-            T.input(type="hidden", name="return_to", value="."),
-            T.legend(class_="freeform-form-label")["Run a deep-check operation (EXPENSIVE)"],
-            T.div[
-            "Verify every bit? (EVEN MORE EXPENSIVE):",
-            T.input(type="checkbox", name="verify"),
-            ],
-            T.div["Repair any problems?: ",
-                  T.input(type="checkbox", name="repair")],
-            T.div["Emit results in JSON format?: ",
-                  T.input(type="checkbox", name="output", value="JSON")],
-
-            T.input(type="submit", value="Deep-Check"),
-
-            ]]
-        forms.append(T.div(class_="freeform-form")[deep_check])
 
         if self.node.is_readonly():
             forms.append(T.div["No upload forms: directory is read-only"])
@@ -629,26 +586,6 @@ class DirectoryAsHTML(rend.Page):
         forms.append(T.div(class_="freeform-form")[mount])
         return forms
 
-    def build_overwrite_form(self, ctx, name, target):
-        if IMutableFileNode.providedBy(target) and not target.is_readonly():
-            root = self.get_root(ctx)
-            action = "%s/uri/%s" % (root, urllib.quote(target.get_uri()))
-            done_url = "../uri/%s/" % urllib.quote(self.node.get_uri())
-            overwrite = T.form(action=action, method="post",
-                               enctype="multipart/form-data")[
-                T.fieldset[
-                T.input(type="hidden", name="t", value="upload"),
-                T.input(type='hidden', name='when_done', value=done_url),
-                T.legend(class_="freeform-form-label")["Overwrite"],
-                "Choose new file: ",
-                T.input(type="file", name="file", class_="freeform-input-file"),
-                " ",
-                T.input(type="submit", value="Overwrite")
-                ]]
-            return [T.div(class_="freeform-form")[overwrite],]
-        else:
-            return []
-
     def render_results(self, ctx, data):
         req = IRequest(ctx)
         return get_arg(req, "results", "")
@@ -697,6 +634,8 @@ def DirectoryJSONMetadata(ctx, dirnode):
     d.addCallback(text_plain, ctx)
     return d
 
+
+
 def DirectoryURI(ctx, dirnode):
     return text_plain(dirnode.get_uri(), ctx)
 
index cdd2fd91bcdf8b2a23a29075d617b76475c662fe..f8635a5d6ef7c1ae5ef9c5dd7310b3fa8cd6607d 100644 (file)
 <div><a href=".">Refresh this view</a></div>
 <div n:render="welcome" />
 
-<div>Other representations of this directory:
-<a href="?t=manifest">manifest</a>,
-<a href="?t=deep-size">total size</a>,
-<a href="?t=uri">URI</a>,
-<a href="?t=readonly-uri">read-only URI</a>,
-<a href="?t=json">JSON</a>
-</div>
+<div><a href="?t=info">More info on this directory</a></div>
 
 <div>
 <table n:render="sequence" n:data="children" border="1">
     <td>Type</td>
     <td>Size</td>
     <td>Times</td>
-    <td>other representations</td>
-    <td></td>
     <td></td>
     <td></td>
     <td></td>
-    <td>Checker Results</td>
   </tr>
   <tr n:pattern="item" n:render="row">
     <td><n:slot name="filename"/></td>
     <td><n:slot name="type"/></td>
     <td><n:slot name="size"/></td>
     <td><n:slot name="times"/></td>
-    <td><n:slot name="data"/></td>
     <td><n:slot name="delete"/></td>
-    <td><n:slot name="overwrite"/></td>
     <td><n:slot name="rename"/></td>
-
-    <td><n:slot name="check"/></td>
-    <td><n:slot name="checker_results"/></td>
+    <td><n:slot name="info"/></td>
   </tr>
 
   <tr n:pattern="empty"><td>directory is empty!</td></tr>
index f2d9ce254b7ded86a914c4b8df24662f4a912a92..81f5cd7c497527c20cd67c85a2446889b86c801a 100644 (file)
@@ -17,6 +17,7 @@ from allmydata.web.common import text_plain, WebError, IClient, RenderMixin, \
      boolean_of_arg, get_arg, should_create_intermediate_directories
 from allmydata.web.checker_results import CheckerResults, \
      CheckAndRepairResults, LiteralCheckerResults
+from allmydata.web.info import MoreInfo
 
 class ReplaceMeMixin:
 
@@ -174,6 +175,8 @@ class FileNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
             return FileDownloader(self.node, filename, save_to_file)
         if t == "json":
             return FileJSONMetadata(ctx, self.node)
+        if t == "info":
+            return MoreInfo(self.node)
         if t == "uri":
             return FileURI(ctx, self.node)
         if t == "readonly-uri":
diff --git a/src/allmydata/web/info.py b/src/allmydata/web/info.py
new file mode 100644 (file)
index 0000000..dd0c3da
--- /dev/null
@@ -0,0 +1,238 @@
+
+import urllib
+
+from twisted.internet import defer
+from nevow import rend, tags as T
+from nevow.inevow import IRequest
+
+from allmydata.util import base32
+from allmydata.interfaces import IDirectoryNode
+from allmydata.web.common import getxmlfile
+
+class MoreInfo(rend.Page):
+    addSlash = False
+    docFactory = getxmlfile("info.xhtml")
+
+    def abbrev(self, storage_index_or_none):
+        if storage_index_or_none:
+            return base32.b2a(storage_index_or_none)[:6]
+        return "LIT file"
+
+    def get_type(self):
+        node = self.original
+        si = node.get_storage_index()
+        if IDirectoryNode.providedBy(node):
+            return "directory"
+        if si:
+            if node.is_mutable():
+                return "mutable file"
+            return "immutable file"
+        return "LIT file"
+
+    def render_title(self, ctx, data):
+        node = self.original
+        si = node.get_storage_index()
+        t = "More Info for %s" % self.get_type()
+        if si:
+            t += " (SI=%s)" % self.abbrev(si)
+        return ctx.tag[t]
+
+    def render_header(self, ctx, data):
+        return self.render_title(ctx, data)
+
+    def render_type(self, ctx, data):
+        return ctx.tag[self.get_type()]
+
+    def render_si(self, ctx, data):
+        si = self.original.get_storage_index()
+        if not si:
+            return "None"
+        return ctx.tag[base32.b2a(si)]
+
+    def render_size(self, ctx, data):
+        node = self.original
+        si = node.get_storage_index()
+        if IDirectoryNode.providedBy(node):
+            d = node._node.get_size_of_best_version()
+        elif node.is_mutable():
+            d = node.get_size_of_best_version()
+        else:
+            # for immutable files and LIT files, we get the size from the URI
+            d = defer.succeed(node.get_size())
+        d.addCallback(lambda size: ctx.tag[size])
+        return d
+
+    def render_directory_writecap(self, ctx, data):
+        node = self.original
+        if node.is_readonly():
+            return ""
+        if not IDirectoryNode.providedBy(node):
+            return ""
+        return ctx.tag[node.get_uri()]
+
+    def render_directory_readcap(self, ctx, data):
+        node = self.original
+        if not IDirectoryNode.providedBy(node):
+            return ""
+        return ctx.tag[node.get_readonly_uri()]
+
+    def render_directory_verifycap(self, ctx, data):
+        node = self.original
+        if not IDirectoryNode.providedBy(node):
+            return ""
+        return ctx.tag[node.get_verifier().to_string()]
+
+
+    def render_file_writecap(self, ctx, data):
+        node = self.original
+        if IDirectoryNode.providedBy(node):
+            node = node._node
+        if node.is_readonly():
+            return ""
+        return ctx.tag[node.get_uri()]
+
+    def render_file_readcap(self, ctx, data):
+        node = self.original
+        if IDirectoryNode.providedBy(node):
+            node = node._node
+        return ctx.tag[node.get_readonly_uri()]
+
+    def render_file_verifycap(self, ctx, data):
+        node = self.original
+        if IDirectoryNode.providedBy(node):
+            node = node._node
+        verifier = node.get_verifier()
+        if verifier:
+            return ctx.tag[node.get_verifier().to_string()]
+        return ""
+
+    def get_root(self, ctx):
+        req = IRequest(ctx)
+        # the addSlash=True gives us one extra (empty) segment
+        depth = len(req.prepath) + len(req.postpath) - 1
+        link = "/".join([".."] * depth)
+        return link
+
+    def render_raw_link(self, ctx, data):
+        node = self.original
+        if IDirectoryNode.providedBy(node):
+            node = node._node
+        root = self.get_root(ctx)
+        quoted_uri = urllib.quote(node.get_uri())
+        text_plain_url = "%s/file/%s/@@named=/raw.txt" % (root, quoted_uri)
+        return ctx.tag[text_plain_url]
+
+    def render_is_checkable(self, ctx, data):
+        node = self.original
+        si = node.get_storage_index()
+        if si:
+            return ctx.tag
+        # don't show checker button for LIT files
+        return ""
+
+    def render_check_form(self, ctx, data):
+        check = T.form(action=".", method="post",
+                       enctype="multipart/form-data")[
+            T.fieldset[
+            T.input(type="hidden", name="t", value="check"),
+            T.input(type="hidden", name="return_to", value="."),
+            T.legend(class_="freeform-form-label")["Check on this object"],
+            T.div[
+            "Verify every bit? (EXPENSIVE):",
+            T.input(type="checkbox", name="verify"),
+            ],
+            T.div["Repair any problems?: ",
+                  T.input(type="checkbox", name="repair")],
+            T.div["Emit results in JSON format?: ",
+                  T.input(type="checkbox", name="output", value="JSON")],
+
+            T.input(type="submit", value="Check"),
+
+            ]]
+        return ctx.tag[check]
+
+    def render_is_mutable_file(self, ctx, data):
+        node = self.original
+        if IDirectoryNode.providedBy(node):
+            return ""
+        if node.is_mutable() and not node.is_readonly():
+            return ctx.tag
+        return ""
+
+    def render_overwrite_form(self, ctx, data):
+        node = self.original
+        root = self.get_root(ctx)
+        action = "%s/uri/%s" % (root, urllib.quote(node.get_uri()))
+        done_url = "%s/uri/%s?t=info" % (root, urllib.quote(node.get_uri()))
+        overwrite = T.form(action=action, method="post",
+                           enctype="multipart/form-data")[
+            T.fieldset[
+            T.input(type="hidden", name="t", value="upload"),
+            T.input(type='hidden', name='when_done', value=done_url),
+            T.legend(class_="freeform-form-label")["Overwrite"],
+            "Upload new contents: ",
+            T.input(type="file", name="file"),
+            " ",
+            T.input(type="submit", value="Replace Contents")
+            ]]
+        return ctx.tag[overwrite]
+
+    def render_is_directory(self, ctx, data):
+        node = self.original
+        if IDirectoryNode.providedBy(node):
+            return ctx.tag
+        return ""
+
+    def render_deep_check_form(self, ctx, data):
+        deep_check = T.form(action=".", method="post",
+                            enctype="multipart/form-data")[
+            T.fieldset[
+            T.input(type="hidden", name="t", value="deep-check"),
+            T.input(type="hidden", name="return_to", value="."),
+            T.legend(class_="freeform-form-label")["Run a deep-check operation (EXPENSIVE)"],
+            T.div[
+            "Verify every bit? (EVEN MORE EXPENSIVE):",
+            T.input(type="checkbox", name="verify"),
+            ],
+            T.div["Repair any problems?: ",
+                  T.input(type="checkbox", name="repair")],
+            T.div["Emit results in JSON format?: ",
+                  T.input(type="checkbox", name="output", value="JSON")],
+
+            T.input(type="submit", value="Deep-Check"),
+
+            ]]
+        return ctx.tag[deep_check]
+
+    def render_deep_size_form(self, ctx, data):
+        deep_size = T.form(action=".", method="get",
+                            enctype="multipart/form-data")[
+            T.fieldset[
+            T.input(type="hidden", name="t", value="deep-size"),
+            T.legend(class_="freeform-form-label")["Run a deep-size operation (EXPENSIVE)"],
+            T.input(type="submit", value="Deep-Size"),
+            ]]
+        return ctx.tag[deep_size]
+
+    def render_deep_stats_form(self, ctx, data):
+        deep_stats = T.form(action=".", method="get",
+                            enctype="multipart/form-data")[
+            T.fieldset[
+            T.input(type="hidden", name="t", value="deep-stats"),
+            T.legend(class_="freeform-form-label")["Run a deep-stats operation (EXPENSIVE)"],
+            T.input(type="submit", value="Deep-Stats"),
+            ]]
+        return ctx.tag[deep_stats]
+
+    def render_manifest_form(self, ctx, data):
+        manifest = T.form(action=".", method="get",
+                            enctype="multipart/form-data")[
+            T.fieldset[
+            T.input(type="hidden", name="t", value="manifest"),
+            T.legend(class_="freeform-form-label")["Run a manifest operation (EXPENSIVE)"],
+            T.input(type="submit", value="Manifest"),
+            ]]
+        return ctx.tag[manifest]
+
+
+# TODO: edge metadata
diff --git a/src/allmydata/web/info.xhtml b/src/allmydata/web/info.xhtml
new file mode 100644 (file)
index 0000000..3392e2c
--- /dev/null
@@ -0,0 +1,72 @@
+<html xmlns:n="http://nevow.com/ns/nevow/0.1">
+  <head>
+    <title n:render="title"></title>
+    <!-- <link href="http://www.allmydata.com/common/css/styles.css"
+          rel="stylesheet" type="text/css"/> -->
+    <link href="/webform_css" rel="stylesheet" type="text/css"/>
+    <link href="/tahoe_css" rel="stylesheet" type="text/css"/>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+  </head>
+  <body>
+
+<h2 n:render="header"></h2>
+
+<ul>
+  <li>Object Type: <span n:render="type" /></li>
+  <li>Storage Index: <tt n:render="si" /></li>
+  <li>Object Size: <span n:render="size" /></li>
+  <li>Access Caps (URIs):
+  <table border="1">
+    <span n:render="is_directory">
+    <tr>
+      <td>Directory writecap</td>
+      <td><tt n:render="directory_writecap" /></td>
+    </tr>
+    <tr>
+      <td>Directory readcap</td>
+      <td><tt n:render="directory_readcap" /></td>
+    </tr>
+    <tr>
+      <td>Directory verifycap</td>
+      <td><tt n:render="directory_verifycap" /></td>
+    </tr>
+    </span>
+
+    <tr>
+      <td>File writecap</td>
+      <td><tt n:render="file_writecap" /></td>
+    </tr>
+    <tr>
+      <td>File readcap</td>
+      <td><tt n:render="file_readcap" /></td>
+    </tr>
+    <tr>
+      <td>File verifycap</td>
+      <td><tt n:render="file_verifycap" /></td>
+    </tr>
+  </table></li>
+  <li><a href="?t=json">JSON</a></li>
+  <li>Raw data as <a><n:attr name="href" n:render="raw_link" />text/plain</a></li>
+</ul>
+
+<div n:render="is_checkable">
+  <h2>Checker Operations</h2>
+  <div n:render="check_form" />
+</div>
+
+<div n:render="is_mutable_file">
+  <h2>Mutable File Operations</h2>
+  <div n:render="overwrite_form" />
+</div>
+
+<div n:render="is_directory">
+  <h2>Directory Operations</h2>
+  <div n:render="deep_check_form" />
+  <div n:render="deep_size_form" />
+  <div n:render="deep_stats_form" />
+  <div n:render="manifest_form" />
+</div>
+
+
+  </body>
+</html>