web: add 'more info' pages for files and directories, move URI/checker-buttons/deep-size/etc off to them

This commit is contained in:
Brian Warner 2008-09-17 22:00:41 -07:00
parent dde7d67498
commit 99d5a8d8b9
8 changed files with 370 additions and 134 deletions

View 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

View 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

View 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)

View 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
info_link = "%s/?t=info" % name
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)
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)

View File

@ -14,13 +14,7 @@
<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">
@ -29,25 +23,18 @@
<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>

View 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":

238
src/allmydata/web/info.py Normal file
View File

@ -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

View File

@ -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>