From: Brian Warner Date: Thu, 18 Sep 2008 05:00:41 +0000 (-0700) Subject: web: add 'more info' pages for files and directories, move URI/checker-buttons/deep... X-Git-Url: https://git.rkrishnan.org/?a=commitdiff_plain;h=99d5a8d8b9e29b34a80427ed44fcfd0ffbaee349;p=tahoe-lafs%2Ftahoe-lafs.git web: add 'more info' pages for files and directories, move URI/checker-buttons/deep-size/etc off to them --- diff --git a/docs/webapi.txt b/docs/webapi.txt index b558fda7..97f3baf5 100644 --- a/docs/webapi.txt +++ b/docs/webapi.txt @@ -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 diff --git a/src/allmydata/test/test_system.py b/src/allmydata/test/test_system.py index 6f557a05..bb29acb5 100644 --- a/src/allmydata/test/test_system.py +++ b/src/allmydata/test/test_system.py @@ -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 diff --git a/src/allmydata/test/test_web.py b/src/allmydata/test/test_web.py index 3c4d0cc6..16da92e1 100644 --- a/src/allmydata/test/test_web.py +++ b/src/allmydata/test/test_web.py @@ -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('
', 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) diff --git a/src/allmydata/web/directory.py b/src/allmydata/web/directory.py index e6cd3332..a2600773 100644 --- a/src/allmydata/web/directory.py +++ b/src/allmydata/web/directory.py @@ -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) diff --git a/src/allmydata/web/directory.xhtml b/src/allmydata/web/directory.xhtml index cdd2fd91..f8635a5d 100644 --- a/src/allmydata/web/directory.xhtml +++ b/src/allmydata/web/directory.xhtml @@ -14,13 +14,7 @@
Refresh this view
-
Other representations of this directory: -manifest, -total size, -URI, -read-only URI, -JSON -
+
More info on this directory
@@ -29,25 +23,18 @@ - - - - - - - - + diff --git a/src/allmydata/web/filenode.py b/src/allmydata/web/filenode.py index f2d9ce25..81f5cd7c 100644 --- a/src/allmydata/web/filenode.py +++ b/src/allmydata/web/filenode.py @@ -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 index 00000000..dd0c3daf --- /dev/null +++ b/src/allmydata/web/info.py @@ -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 index 00000000..3392e2c0 --- /dev/null +++ b/src/allmydata/web/info.xhtml @@ -0,0 +1,72 @@ + + + + + + + + + + +

+ +
    +
  • Object Type:
  • +
  • Storage Index:
  • +
  • Object Size:
  • +
  • Access Caps (URIs): +
Type Size Timesother representations Checker Results
directory is empty!
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Directory writecap
Directory readcap
Directory verifycap
File writecap
File readcap
File verifycap
+
  • JSON
  • +
  • Raw data as text/plain
  • + + +
    +

    Checker Operations

    +
    +
    + +
    +

    Mutable File Operations

    +
    +
    + +
    +

    Directory Operations

    +
    +
    +
    +
    +
    + + + +