mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2024-12-26 08:01:06 +00:00
Merge pull request #665 from meejah/ticket3252-port-web-directory.manifest.2
3263 port web directory.manifest.2
This commit is contained in:
commit
5f68190363
0
newsfragments/3263.minor
Normal file
0
newsfragments/3263.minor
Normal file
@ -280,6 +280,13 @@ class NoNetworkGrid(service.MultiService):
|
|||||||
def __init__(self, basedir, num_clients, num_servers,
|
def __init__(self, basedir, num_clients, num_servers,
|
||||||
client_config_hooks, port_assigner):
|
client_config_hooks, port_assigner):
|
||||||
service.MultiService.__init__(self)
|
service.MultiService.__init__(self)
|
||||||
|
|
||||||
|
# We really need to get rid of this pattern here (and
|
||||||
|
# everywhere) in Tahoe where "async work" is started in
|
||||||
|
# __init__ For now, we at least keep the errors so they can
|
||||||
|
# cause tests to fail less-improperly (see _check_clients)
|
||||||
|
self._setup_errors = []
|
||||||
|
|
||||||
self.port_assigner = port_assigner
|
self.port_assigner = port_assigner
|
||||||
self.basedir = basedir
|
self.basedir = basedir
|
||||||
fileutil.make_dirs(basedir)
|
fileutil.make_dirs(basedir)
|
||||||
@ -300,6 +307,20 @@ class NoNetworkGrid(service.MultiService):
|
|||||||
d = self.make_client(i)
|
d = self.make_client(i)
|
||||||
d.addCallback(lambda c: self.clients.append(c))
|
d.addCallback(lambda c: self.clients.append(c))
|
||||||
|
|
||||||
|
def _bad(f):
|
||||||
|
self._setup_errors.append(f)
|
||||||
|
d.addErrback(_bad)
|
||||||
|
|
||||||
|
def _check_clients(self):
|
||||||
|
"""
|
||||||
|
The anti-pattern of doing async work in __init__ means we need to
|
||||||
|
check if that work completed successfully. This method either
|
||||||
|
returns nothing or raises an exception in case __init__ failed
|
||||||
|
to complete properly
|
||||||
|
"""
|
||||||
|
if self._setup_errors:
|
||||||
|
raise self._setup_errors[0].value
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def make_client(self, i, write_config=True):
|
def make_client(self, i, write_config=True):
|
||||||
clientid = hashutil.tagged_hash("clientid", str(i))[:20]
|
clientid = hashutil.tagged_hash("clientid", str(i))[:20]
|
||||||
@ -364,6 +385,7 @@ class NoNetworkGrid(service.MultiService):
|
|||||||
return self.proxies_by_id.keys()
|
return self.proxies_by_id.keys()
|
||||||
|
|
||||||
def rebuild_serverlist(self):
|
def rebuild_serverlist(self):
|
||||||
|
self._check_clients()
|
||||||
self.all_servers = frozenset(self.proxies_by_id.values())
|
self.all_servers = frozenset(self.proxies_by_id.values())
|
||||||
for c in self.clients:
|
for c in self.clients:
|
||||||
c._servers = self.all_servers
|
c._servers = self.all_servers
|
||||||
@ -440,12 +462,14 @@ class GridTestMixin(object):
|
|||||||
self._record_webports_and_baseurls()
|
self._record_webports_and_baseurls()
|
||||||
|
|
||||||
def _record_webports_and_baseurls(self):
|
def _record_webports_and_baseurls(self):
|
||||||
|
self.g._check_clients()
|
||||||
self.client_webports = [c.getServiceNamed("webish").getPortnum()
|
self.client_webports = [c.getServiceNamed("webish").getPortnum()
|
||||||
for c in self.g.clients]
|
for c in self.g.clients]
|
||||||
self.client_baseurls = [c.getServiceNamed("webish").getURL()
|
self.client_baseurls = [c.getServiceNamed("webish").getURL()
|
||||||
for c in self.g.clients]
|
for c in self.g.clients]
|
||||||
|
|
||||||
def get_client_config(self, i=0):
|
def get_client_config(self, i=0):
|
||||||
|
self.g._check_clients()
|
||||||
return self.g.clients[i].config
|
return self.g.clients[i].config
|
||||||
|
|
||||||
def get_clientdir(self, i=0):
|
def get_clientdir(self, i=0):
|
||||||
@ -454,9 +478,11 @@ class GridTestMixin(object):
|
|||||||
return self.get_client_config(i).get_config_path()
|
return self.get_client_config(i).get_config_path()
|
||||||
|
|
||||||
def get_client(self, i=0):
|
def get_client(self, i=0):
|
||||||
|
self.g._check_clients()
|
||||||
return self.g.clients[i]
|
return self.g.clients[i]
|
||||||
|
|
||||||
def restart_client(self, i=0):
|
def restart_client(self, i=0):
|
||||||
|
self.g._check_clients()
|
||||||
client = self.g.clients[i]
|
client = self.g.clients[i]
|
||||||
d = defer.succeed(None)
|
d = defer.succeed(None)
|
||||||
d.addCallback(lambda ign: self.g.removeService(client))
|
d.addCallback(lambda ign: self.g.removeService(client))
|
||||||
|
@ -5,6 +5,8 @@ import json
|
|||||||
import treq
|
import treq
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from twisted.application import service
|
from twisted.application import service
|
||||||
from twisted.trial import unittest
|
from twisted.trial import unittest
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
@ -49,6 +51,10 @@ from ..common import (
|
|||||||
make_mutable_file_uri,
|
make_mutable_file_uri,
|
||||||
create_mutable_filenode,
|
create_mutable_filenode,
|
||||||
)
|
)
|
||||||
|
from .common import (
|
||||||
|
assert_soup_has_favicon,
|
||||||
|
assert_soup_has_text,
|
||||||
|
)
|
||||||
from allmydata.interfaces import IMutableFileNode, SDMF_VERSION, MDMF_VERSION
|
from allmydata.interfaces import IMutableFileNode, SDMF_VERSION, MDMF_VERSION
|
||||||
from allmydata.mutable import servermap, publish, retrieve
|
from allmydata.mutable import servermap, publish, retrieve
|
||||||
from .. import common_util as testutil
|
from .. import common_util as testutil
|
||||||
@ -2038,11 +2044,12 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
|
|||||||
return d
|
return d
|
||||||
d.addCallback(getman, None)
|
d.addCallback(getman, None)
|
||||||
def _got_html(manifest):
|
def _got_html(manifest):
|
||||||
self.failUnlessIn("Manifest of SI=", manifest)
|
soup = BeautifulSoup(manifest, 'html5lib')
|
||||||
self.failUnlessIn("<td>sub</td>", manifest)
|
assert_soup_has_text(self, soup, "Manifest of SI=")
|
||||||
self.failUnlessIn(self._sub_uri, manifest)
|
assert_soup_has_text(self, soup, "sub")
|
||||||
self.failUnlessIn("<td>sub/baz.txt</td>", manifest)
|
assert_soup_has_text(self, soup, self._sub_uri)
|
||||||
self.failUnlessIn(FAVICON_MARKUP, manifest)
|
assert_soup_has_text(self, soup, "sub/baz.txt")
|
||||||
|
assert_soup_has_favicon(self, soup)
|
||||||
d.addCallback(_got_html)
|
d.addCallback(_got_html)
|
||||||
|
|
||||||
# both t=status and unadorned GET should be identical
|
# both t=status and unadorned GET should be identical
|
||||||
|
@ -1,12 +1,22 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import urllib
|
import urllib
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from zope.interface import implementer
|
from zope.interface import implementer
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
from twisted.internet.interfaces import IPushProducer
|
from twisted.internet.interfaces import IPushProducer
|
||||||
from twisted.python.failure import Failure
|
from twisted.python.failure import Failure
|
||||||
from twisted.web import http
|
from twisted.web import http
|
||||||
|
from twisted.web.template import (
|
||||||
|
Element,
|
||||||
|
XMLFile,
|
||||||
|
renderElement,
|
||||||
|
renderer,
|
||||||
|
tags,
|
||||||
|
)
|
||||||
|
from hyperlink import URL
|
||||||
|
from twisted.python.filepath import FilePath
|
||||||
from nevow import url, rend, inevow, tags as T
|
from nevow import url, rend, inevow, tags as T
|
||||||
from nevow.inevow import IRequest
|
from nevow.inevow import IRequest
|
||||||
|
|
||||||
@ -14,20 +24,40 @@ from foolscap.api import fireEventually
|
|||||||
|
|
||||||
from allmydata.util import base32
|
from allmydata.util import base32
|
||||||
from allmydata.util.encodingutil import to_str
|
from allmydata.util.encodingutil import to_str
|
||||||
from allmydata.uri import from_string_dirnode
|
from allmydata.uri import (
|
||||||
|
from_string_dirnode,
|
||||||
|
from_string,
|
||||||
|
CHKFileURI,
|
||||||
|
WriteableSSKFileURI,
|
||||||
|
ReadonlySSKFileURI,
|
||||||
|
)
|
||||||
from allmydata.interfaces import IDirectoryNode, IFileNode, IFilesystemNode, \
|
from allmydata.interfaces import IDirectoryNode, IFileNode, IFilesystemNode, \
|
||||||
IImmutableFileNode, IMutableFileNode, ExistingChildError, \
|
IImmutableFileNode, IMutableFileNode, ExistingChildError, \
|
||||||
NoSuchChildError, EmptyPathnameComponentError, SDMF_VERSION, MDMF_VERSION
|
NoSuchChildError, EmptyPathnameComponentError, SDMF_VERSION, MDMF_VERSION
|
||||||
from allmydata.blacklist import ProhibitedNode
|
from allmydata.blacklist import ProhibitedNode
|
||||||
from allmydata.monitor import Monitor, OperationCancelledError
|
from allmydata.monitor import Monitor, OperationCancelledError
|
||||||
from allmydata import dirnode
|
from allmydata import dirnode
|
||||||
from allmydata.web.common import text_plain, WebError, \
|
from allmydata.web.common import (
|
||||||
NeedOperationHandleError, \
|
text_plain,
|
||||||
boolean_of_arg, get_arg, get_root, parse_replace_arg, \
|
WebError,
|
||||||
should_create_intermediate_directories, \
|
NeedOperationHandleError,
|
||||||
getxmlfile, RenderMixin, humanize_failure, convert_children_json, \
|
boolean_of_arg,
|
||||||
get_format, get_mutable_type, get_filenode_metadata, render_time, \
|
get_arg,
|
||||||
MultiFormatPage
|
get_root,
|
||||||
|
parse_replace_arg,
|
||||||
|
should_create_intermediate_directories,
|
||||||
|
getxmlfile,
|
||||||
|
RenderMixin,
|
||||||
|
humanize_failure,
|
||||||
|
convert_children_json,
|
||||||
|
get_format,
|
||||||
|
get_mutable_type,
|
||||||
|
get_filenode_metadata,
|
||||||
|
render_time,
|
||||||
|
MultiFormatPage,
|
||||||
|
MultiFormatResource,
|
||||||
|
SlotsSequenceElement,
|
||||||
|
)
|
||||||
from allmydata.web.filenode import ReplaceMeMixin, \
|
from allmydata.web.filenode import ReplaceMeMixin, \
|
||||||
FileNodeHandler, PlaceHolderNodeHandler
|
FileNodeHandler, PlaceHolderNodeHandler
|
||||||
from allmydata.web.check_results import CheckResultsRenderer, \
|
from allmydata.web.check_results import CheckResultsRenderer, \
|
||||||
@ -943,8 +973,128 @@ class RenameForm(rend.Page):
|
|||||||
ctx.tag.attributes['value'] = name
|
ctx.tag.attributes['value'] = name
|
||||||
return ctx.tag
|
return ctx.tag
|
||||||
|
|
||||||
class ManifestResults(MultiFormatPage, ReloadMixin):
|
|
||||||
docFactory = getxmlfile("manifest.xhtml")
|
class ReloadableMonitorElement(Element):
|
||||||
|
"""
|
||||||
|
Like 'ReloadMixin', but for twisted.web.template style. This
|
||||||
|
provides renderers for "reload" and "refesh" and a self.monitor
|
||||||
|
attribute (which is an instance of IMonitor)
|
||||||
|
"""
|
||||||
|
refresh_time = timedelta(seconds=60)
|
||||||
|
|
||||||
|
def __init__(self, monitor):
|
||||||
|
self.monitor = monitor
|
||||||
|
super(ReloadableMonitorElement, self).__init__()
|
||||||
|
|
||||||
|
@renderer
|
||||||
|
def refresh(self, req, tag):
|
||||||
|
if self.monitor.is_finished():
|
||||||
|
return u""
|
||||||
|
tag.attributes[u"http-equiv"] = u"refresh"
|
||||||
|
tag.attributes[u"content"] = u"{}".format(self.refresh_time.seconds)
|
||||||
|
return tag
|
||||||
|
|
||||||
|
@renderer
|
||||||
|
def reload(self, req, tag):
|
||||||
|
if self.monitor.is_finished():
|
||||||
|
return u""
|
||||||
|
reload_url = URL.from_text(u"{}".format(req.path))
|
||||||
|
cancel_button = tags.form(
|
||||||
|
[
|
||||||
|
tags.input(type=u"submit", value=u"Cancel"),
|
||||||
|
],
|
||||||
|
action=reload_url.replace(query={u"t": u"cancel"}).to_uri().to_text(),
|
||||||
|
method=u"POST",
|
||||||
|
enctype=u"multipart/form-data",
|
||||||
|
)
|
||||||
|
|
||||||
|
return tag([
|
||||||
|
u"Operation still running: ",
|
||||||
|
tags.a(
|
||||||
|
u"Reload",
|
||||||
|
href=reload_url.replace(query={u"output": u"html"}).to_uri().to_text(),
|
||||||
|
),
|
||||||
|
cancel_button,
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def _slashify_path(path):
|
||||||
|
"""
|
||||||
|
Converts a tuple from a 'manifest' path into a string with slashes
|
||||||
|
in it
|
||||||
|
"""
|
||||||
|
if not path:
|
||||||
|
return ""
|
||||||
|
return "/".join([p.encode("utf-8") for p in path])
|
||||||
|
|
||||||
|
|
||||||
|
def _cap_to_link(root, path, cap):
|
||||||
|
"""
|
||||||
|
Turns a capability-string into a WebAPI link tag
|
||||||
|
|
||||||
|
:param text root: the root piece of the URI
|
||||||
|
|
||||||
|
:param text cap: the capability-string
|
||||||
|
|
||||||
|
:returns: something suitable for `IRenderable`, specifically
|
||||||
|
either a valid local link (tags.a instance) to the capability
|
||||||
|
or an empty string.
|
||||||
|
"""
|
||||||
|
if cap:
|
||||||
|
root_url = URL.from_text(u"{}".format(root))
|
||||||
|
cap_obj = from_string(cap)
|
||||||
|
if isinstance(cap_obj, (CHKFileURI, WriteableSSKFileURI, ReadonlySSKFileURI)):
|
||||||
|
uri_link = root_url.child(
|
||||||
|
u"file",
|
||||||
|
u"{}".format(urllib.quote(cap)),
|
||||||
|
u"{}".format(urllib.quote(path[-1])),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
uri_link = root_url.child(
|
||||||
|
u"uri",
|
||||||
|
u"{}".format(urllib.quote(cap, safe="")),
|
||||||
|
)
|
||||||
|
return tags.a(cap, href=uri_link.to_text())
|
||||||
|
else:
|
||||||
|
return u""
|
||||||
|
|
||||||
|
|
||||||
|
class ManifestElement(ReloadableMonitorElement):
|
||||||
|
loader = XMLFile(FilePath(__file__).sibling("manifest.xhtml"))
|
||||||
|
|
||||||
|
def _si_abbrev(self):
|
||||||
|
si = self.monitor.origin_si
|
||||||
|
if not si:
|
||||||
|
return "<LIT>"
|
||||||
|
return base32.b2a(si)[:6]
|
||||||
|
|
||||||
|
@renderer
|
||||||
|
def title(self, req, tag):
|
||||||
|
return tag(
|
||||||
|
"Manifest of SI={}".format(self._si_abbrev())
|
||||||
|
)
|
||||||
|
|
||||||
|
@renderer
|
||||||
|
def header(self, req, tag):
|
||||||
|
return tag(
|
||||||
|
"Manifest of SI={}".format(self._si_abbrev())
|
||||||
|
)
|
||||||
|
|
||||||
|
@renderer
|
||||||
|
def items(self, req, tag):
|
||||||
|
manifest = self.monitor.get_status()["manifest"]
|
||||||
|
root = get_root(req)
|
||||||
|
rows = [
|
||||||
|
{
|
||||||
|
"path": _slashify_path(path),
|
||||||
|
"cap": _cap_to_link(root, path, cap),
|
||||||
|
}
|
||||||
|
for path, cap in manifest
|
||||||
|
]
|
||||||
|
return SlotsSequenceElement(tag, rows)
|
||||||
|
|
||||||
|
|
||||||
|
class ManifestResults(MultiFormatResource, ReloadMixin):
|
||||||
|
|
||||||
# Control MultiFormatPage
|
# Control MultiFormatPage
|
||||||
formatArgument = "output"
|
formatArgument = "output"
|
||||||
@ -954,21 +1104,19 @@ class ManifestResults(MultiFormatPage, ReloadMixin):
|
|||||||
self.client = client
|
self.client = client
|
||||||
self.monitor = monitor
|
self.monitor = monitor
|
||||||
|
|
||||||
# The default format is HTML but the HTML renderer is just renderHTTP.
|
def render_HTML(self, req):
|
||||||
render_HTML = None
|
return renderElement(
|
||||||
|
req,
|
||||||
def slashify_path(self, path):
|
ManifestElement(self.monitor)
|
||||||
if not path:
|
)
|
||||||
return ""
|
|
||||||
return "/".join([p.encode("utf-8") for p in path])
|
|
||||||
|
|
||||||
def render_TEXT(self, req):
|
def render_TEXT(self, req):
|
||||||
req.setHeader("content-type", "text/plain")
|
req.setHeader("content-type", "text/plain")
|
||||||
lines = []
|
lines = []
|
||||||
is_finished = self.monitor.is_finished()
|
is_finished = self.monitor.is_finished()
|
||||||
lines.append("finished: " + {True: "yes", False: "no"}[is_finished])
|
lines.append("finished: " + {True: "yes", False: "no"}[is_finished])
|
||||||
for (path, cap) in self.monitor.get_status()["manifest"]:
|
for path, cap in self.monitor.get_status()["manifest"]:
|
||||||
lines.append(self.slashify_path(path) + " " + cap)
|
lines.append(_slashify_path(path) + " " + cap)
|
||||||
return "\n".join(lines) + "\n"
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
def render_JSON(self, req):
|
def render_JSON(self, req):
|
||||||
@ -1002,37 +1150,6 @@ class ManifestResults(MultiFormatPage, ReloadMixin):
|
|||||||
# CPU.
|
# CPU.
|
||||||
return json.dumps(status, indent=1)
|
return json.dumps(status, indent=1)
|
||||||
|
|
||||||
def _si_abbrev(self):
|
|
||||||
si = self.monitor.origin_si
|
|
||||||
if not si:
|
|
||||||
return "<LIT>"
|
|
||||||
return base32.b2a(si)[:6]
|
|
||||||
|
|
||||||
def render_title(self, ctx):
|
|
||||||
return T.title["Manifest of SI=%s" % self._si_abbrev()]
|
|
||||||
|
|
||||||
def render_header(self, ctx):
|
|
||||||
return T.p["Manifest of SI=%s" % self._si_abbrev()]
|
|
||||||
|
|
||||||
def data_items(self, ctx, data):
|
|
||||||
return self.monitor.get_status()["manifest"]
|
|
||||||
|
|
||||||
def render_row(self, ctx, path_cap):
|
|
||||||
path, cap = path_cap
|
|
||||||
ctx.fillSlots("path", self.slashify_path(path))
|
|
||||||
root = get_root(ctx)
|
|
||||||
# TODO: we need a clean consistent way to get the type of a cap string
|
|
||||||
if cap:
|
|
||||||
if cap.startswith("URI:CHK") or cap.startswith("URI:SSK"):
|
|
||||||
nameurl = urllib.quote(path[-1].encode("utf-8"))
|
|
||||||
uri_link = "%s/file/%s/@@named=/%s" % (root, urllib.quote(cap),
|
|
||||||
nameurl)
|
|
||||||
else:
|
|
||||||
uri_link = "%s/uri/%s" % (root, urllib.quote(cap, safe=""))
|
|
||||||
ctx.fillSlots("cap", T.a(href=uri_link)[cap])
|
|
||||||
else:
|
|
||||||
ctx.fillSlots("cap", "")
|
|
||||||
return ctx.tag
|
|
||||||
|
|
||||||
class DeepSizeResults(MultiFormatPage):
|
class DeepSizeResults(MultiFormatPage):
|
||||||
# Control MultiFormatPage
|
# Control MultiFormatPage
|
||||||
|
@ -1,28 +1,28 @@
|
|||||||
<html xmlns:n="http://nevow.com/ns/nevow/0.1">
|
<html xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
|
||||||
<head>
|
<head>
|
||||||
<title n:render="title"></title>
|
<title t:render="title"></title>
|
||||||
<link href="/tahoe.css" rel="stylesheet" type="text/css"/>
|
<link href="/tahoe.css" rel="stylesheet" type="text/css"/>
|
||||||
<link href="/icon.png" rel="shortcut icon" />
|
<link href="/icon.png" rel="shortcut icon" />
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
<meta n:render="refresh" />
|
<meta t:render="refresh" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<h1><p n:render="header"></p></h1>
|
<h1><p t:render="header"></p></h1>
|
||||||
|
|
||||||
<h2 n:render="reload" />
|
<h2 t:render="reload" />
|
||||||
|
|
||||||
<table n:render="sequence" n:data="items">
|
<table t:render="items">
|
||||||
<tr n:pattern="header">
|
<tr t:pattern="header">
|
||||||
<td>Path</td>
|
<td>Path</td>
|
||||||
<td>cap</td>
|
<td>cap</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr n:pattern="item" n:render="row">
|
<tr t:render="item">
|
||||||
<td><n:slot name="path"/></td>
|
<td><t:slot name="path"/></td>
|
||||||
<td><n:slot name="cap"/></td>
|
<td><t:slot name="cap"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr n:pattern="empty"><td>no items in the manifest!</td></tr>
|
<tr t:render="empty"><td>no items in the manifest!</td></tr>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user