2019-09-04 05:48:01 +00:00
|
|
|
import os
|
|
|
|
import time
|
|
|
|
import json
|
|
|
|
import urllib
|
2008-05-19 19:57:04 +00:00
|
|
|
|
2019-09-04 05:06:16 +00:00
|
|
|
from twisted.web import (
|
|
|
|
http,
|
|
|
|
resource,
|
|
|
|
)
|
|
|
|
from twisted.web.util import redirectTo
|
2019-09-04 05:48:01 +00:00
|
|
|
|
2020-04-18 07:50:53 +00:00
|
|
|
from hyperlink import DecodedURL, URL
|
2019-09-04 18:21:53 +00:00
|
|
|
|
2019-09-04 05:19:49 +00:00
|
|
|
from nevow import rend, tags as T
|
2016-12-10 00:35:46 +00:00
|
|
|
from nevow.inevow import IRequest
|
2020-04-27 14:49:44 +00:00
|
|
|
from twisted.web import static
|
2008-05-19 19:57:04 +00:00
|
|
|
from nevow.util import resource_filename
|
|
|
|
|
2020-04-27 20:42:03 +00:00
|
|
|
from twisted.python.filepath import FilePath
|
|
|
|
from twisted.web.template import (
|
|
|
|
Element,
|
|
|
|
XMLFile,
|
|
|
|
renderer,
|
|
|
|
renderElement,
|
|
|
|
tags
|
|
|
|
)
|
|
|
|
|
2008-05-19 19:57:04 +00:00
|
|
|
import allmydata # to display import path
|
2019-08-13 19:11:01 +00:00
|
|
|
from allmydata.version_checks import get_package_versions_string
|
2013-04-14 21:32:13 +00:00
|
|
|
from allmydata.util import log
|
2011-10-02 03:45:03 +00:00
|
|
|
from allmydata.interfaces import IFileNode
|
2019-08-08 02:44:53 +00:00
|
|
|
from allmydata.web import filenode, directory, unlinked, status
|
2020-02-13 15:43:50 +00:00
|
|
|
from allmydata.web import storage
|
2017-07-25 15:36:06 +00:00
|
|
|
from allmydata.web.common import (
|
|
|
|
abbreviate_size,
|
|
|
|
getxmlfile,
|
|
|
|
WebError,
|
|
|
|
get_arg,
|
|
|
|
MultiFormatPage,
|
2020-04-09 19:09:58 +00:00
|
|
|
MultiFormatResource,
|
2017-07-25 15:36:06 +00:00
|
|
|
get_format,
|
|
|
|
get_mutable_type,
|
|
|
|
render_time_delta,
|
|
|
|
render_time,
|
|
|
|
render_time_attr,
|
|
|
|
)
|
2019-03-21 19:00:57 +00:00
|
|
|
from allmydata.web.private import (
|
|
|
|
create_private_tree,
|
|
|
|
)
|
2019-09-05 22:07:22 +00:00
|
|
|
from allmydata import uri
|
2008-05-19 19:57:04 +00:00
|
|
|
|
2019-09-04 01:46:17 +00:00
|
|
|
class URIHandler(resource.Resource, object):
|
|
|
|
"""
|
|
|
|
I live at /uri . There are several operations defined on /uri itself,
|
|
|
|
mostly involved with creation of unlinked files and directories.
|
|
|
|
"""
|
2009-01-18 17:56:08 +00:00
|
|
|
|
2009-02-20 19:15:54 +00:00
|
|
|
def __init__(self, client):
|
2019-09-04 01:46:17 +00:00
|
|
|
super(URIHandler, self).__init__()
|
2009-02-20 19:15:54 +00:00
|
|
|
self.client = client
|
|
|
|
|
2019-09-04 05:06:16 +00:00
|
|
|
def render_GET(self, req):
|
|
|
|
"""
|
|
|
|
Historically, accessing this via "GET /uri?uri=<capabilitiy>"
|
|
|
|
was/is a feature -- which simply redirects to the more-common
|
2019-09-04 18:21:53 +00:00
|
|
|
"GET /uri/<capability>" with any other query args
|
|
|
|
preserved. New code should use "/uri/<cap>"
|
2019-09-04 05:06:16 +00:00
|
|
|
"""
|
2019-09-05 22:48:08 +00:00
|
|
|
uri_arg = req.args.get(b"uri", [None])[0]
|
2019-09-04 18:21:53 +00:00
|
|
|
if uri_arg is None:
|
2008-05-19 19:57:04 +00:00
|
|
|
raise WebError("GET /uri requires uri=")
|
2019-09-04 19:59:00 +00:00
|
|
|
|
2019-09-05 22:07:22 +00:00
|
|
|
# shennanigans like putting "%2F" or just "/" itself, or ../
|
|
|
|
# etc in the <cap> might be a vector for weirdness so we
|
|
|
|
# validate that this is a valid capability before proceeding.
|
|
|
|
cap = uri.from_string(uri_arg)
|
|
|
|
if isinstance(cap, uri.UnknownURI):
|
|
|
|
raise WebError("Invalid capability")
|
2019-09-04 19:59:00 +00:00
|
|
|
|
2019-09-04 18:21:53 +00:00
|
|
|
# so, using URL.from_text(req.uri) isn't going to work because
|
|
|
|
# it seems Nevow was creating absolute URLs including
|
2019-09-05 22:07:22 +00:00
|
|
|
# host/port whereas req.uri is absolute (but lacks host/port)
|
|
|
|
redir_uri = URL.from_text(req.prePathURL().decode('utf8'))
|
|
|
|
redir_uri = redir_uri.child(urllib.quote(uri_arg).decode('utf8'))
|
|
|
|
# add back all the query args that AREN'T "?uri="
|
2019-09-04 18:21:53 +00:00
|
|
|
for k, values in req.args.items():
|
2019-09-27 17:52:27 +00:00
|
|
|
if k != b"uri":
|
2019-09-04 18:21:53 +00:00
|
|
|
for v in values:
|
2019-09-05 22:07:22 +00:00
|
|
|
redir_uri = redir_uri.add(k.decode('utf8'), v.decode('utf8'))
|
|
|
|
return redirectTo(redir_uri.to_text().encode('utf8'), req)
|
2008-05-19 19:57:04 +00:00
|
|
|
|
2019-09-04 05:09:00 +00:00
|
|
|
def render_PUT(self, req):
|
|
|
|
"""
|
|
|
|
either "PUT /uri" to create an unlinked file, or
|
|
|
|
"PUT /uri?t=mkdir" to create an unlinked directory
|
|
|
|
"""
|
2008-05-19 19:57:04 +00:00
|
|
|
t = get_arg(req, "t", "").strip()
|
|
|
|
if t == "":
|
2011-10-13 16:29:51 +00:00
|
|
|
file_format = get_format(req, "CHK")
|
2011-10-13 16:32:29 +00:00
|
|
|
mutable_type = get_mutable_type(file_format)
|
|
|
|
if mutable_type is not None:
|
|
|
|
return unlinked.PUTUnlinkedSSK(req, self.client, mutable_type)
|
2008-05-19 19:57:04 +00:00
|
|
|
else:
|
2009-02-20 19:15:54 +00:00
|
|
|
return unlinked.PUTUnlinkedCHK(req, self.client)
|
2008-05-19 19:57:04 +00:00
|
|
|
if t == "mkdir":
|
2009-02-20 19:15:54 +00:00
|
|
|
return unlinked.PUTUnlinkedCreateDirectory(req, self.client)
|
2019-09-04 05:09:00 +00:00
|
|
|
errmsg = (
|
|
|
|
"/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
|
|
|
|
"and POST?t=mkdir"
|
|
|
|
)
|
2008-05-19 19:57:04 +00:00
|
|
|
raise WebError(errmsg, http.BAD_REQUEST)
|
|
|
|
|
2019-09-04 05:09:00 +00:00
|
|
|
def render_POST(self, req):
|
|
|
|
"""
|
|
|
|
"POST /uri?t=upload&file=newfile" to upload an
|
|
|
|
unlinked file or "POST /uri?t=mkdir" to create a
|
|
|
|
new directory
|
|
|
|
"""
|
2008-05-19 19:57:04 +00:00
|
|
|
t = get_arg(req, "t", "").strip()
|
|
|
|
if t in ("", "upload"):
|
2011-10-13 16:29:51 +00:00
|
|
|
file_format = get_format(req)
|
2011-10-13 16:32:29 +00:00
|
|
|
mutable_type = get_mutable_type(file_format)
|
|
|
|
if mutable_type is not None:
|
|
|
|
return unlinked.POSTUnlinkedSSK(req, self.client, mutable_type)
|
2008-05-19 19:57:04 +00:00
|
|
|
else:
|
2009-02-20 19:15:54 +00:00
|
|
|
return unlinked.POSTUnlinkedCHK(req, self.client)
|
2008-05-19 19:57:04 +00:00
|
|
|
if t == "mkdir":
|
2009-02-20 19:15:54 +00:00
|
|
|
return unlinked.POSTUnlinkedCreateDirectory(req, self.client)
|
2009-10-26 01:13:21 +00:00
|
|
|
elif t == "mkdir-with-children":
|
|
|
|
return unlinked.POSTUnlinkedCreateDirectoryWithChildren(req,
|
|
|
|
self.client)
|
2009-11-18 07:09:00 +00:00
|
|
|
elif t == "mkdir-immutable":
|
|
|
|
return unlinked.POSTUnlinkedCreateImmutableDirectory(req,
|
|
|
|
self.client)
|
2008-05-19 19:57:04 +00:00
|
|
|
errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
|
|
|
|
"and POST?t=mkdir")
|
|
|
|
raise WebError(errmsg, http.BAD_REQUEST)
|
|
|
|
|
2019-09-04 01:46:17 +00:00
|
|
|
def getChild(self, name, req):
|
2019-09-04 05:09:00 +00:00
|
|
|
"""
|
|
|
|
Most requests look like /uri/<cap> so this fetches the capability
|
|
|
|
and creates and appropriate handler (depending on the kind of
|
|
|
|
capability it was passed).
|
|
|
|
"""
|
2020-02-13 13:21:42 +00:00
|
|
|
# this is in case a URI like "/uri/?cap=<valid capability>" is
|
2020-02-24 16:59:48 +00:00
|
|
|
# passed -- we re-direct to the non-trailing-slash version so
|
|
|
|
# that there is just one valid URI for "uri" resource.
|
2019-09-10 23:52:20 +00:00
|
|
|
if not name:
|
2020-04-18 07:50:53 +00:00
|
|
|
u = DecodedURL.from_text(req.uri.decode('utf8'))
|
2020-02-24 16:59:48 +00:00
|
|
|
u = u.replace(
|
|
|
|
path=(s for s in u.path if s), # remove empty segments
|
|
|
|
)
|
|
|
|
return redirectTo(u.to_uri().to_text().encode('utf8'), req)
|
2008-05-19 19:57:04 +00:00
|
|
|
try:
|
2009-02-20 19:15:54 +00:00
|
|
|
node = self.client.create_node_from_uri(name)
|
|
|
|
return directory.make_handler_for(node, self.client)
|
2010-01-27 06:44:30 +00:00
|
|
|
except (TypeError, AssertionError):
|
2019-09-04 01:46:17 +00:00
|
|
|
raise WebError(
|
|
|
|
"'{}' is not a valid file- or directory- cap".format(name)
|
|
|
|
)
|
|
|
|
|
2008-05-19 19:57:04 +00:00
|
|
|
|
2019-09-11 00:53:54 +00:00
|
|
|
class FileHandler(resource.Resource, object):
|
2008-05-19 19:57:04 +00:00
|
|
|
# I handle /file/$FILECAP[/IGNORED] , which provides a URL from which a
|
|
|
|
# file can be downloaded correctly by tools like "wget".
|
|
|
|
|
2009-02-20 19:15:54 +00:00
|
|
|
def __init__(self, client):
|
2019-09-11 00:53:54 +00:00
|
|
|
super(FileHandler, self).__init__()
|
2009-02-20 19:15:54 +00:00
|
|
|
self.client = client
|
|
|
|
|
2019-09-11 00:53:54 +00:00
|
|
|
def getChild(self, name, req):
|
2008-05-19 19:57:04 +00:00
|
|
|
if req.method not in ("GET", "HEAD"):
|
|
|
|
raise WebError("/file can only be used with GET or HEAD")
|
|
|
|
# 'name' must be a file URI
|
|
|
|
try:
|
2009-02-20 19:15:54 +00:00
|
|
|
node = self.client.create_node_from_uri(name)
|
2010-01-27 06:44:30 +00:00
|
|
|
except (TypeError, AssertionError):
|
Overhaul IFilesystemNode handling, to simplify tests and use POLA internally.
* stop using IURI as an adapter
* pass cap strings around instead of URI instances
* move filenode/dirnode creation duties from Client to new NodeMaker class
* move other Client duties to KeyGenerator, SecretHolder, History classes
* stop passing Client reference to dirnode/filenode constructors
- pass less-powerful references instead, like StorageBroker or Uploader
* always create DirectoryNodes by wrapping a filenode (mutable for now)
* remove some specialized mock classes from unit tests
Detailed list of changes (done one at a time, then merged together)
always pass a string to create_node_from_uri(), not an IURI instance
always pass a string to IFilesystemNode constructors, not an IURI instance
stop using IURI() as an adapter, switch on cap prefix in create_node_from_uri()
client.py: move SecretHolder code out to a separate class
test_web.py: hush pyflakes
client.py: move NodeMaker functionality out into a separate object
LiteralFileNode: stop storing a Client reference
immutable Checker: remove Client reference, it only needs a SecretHolder
immutable Upload: remove Client reference, leave SecretHolder and StorageBroker
immutable Repairer: replace Client reference with StorageBroker and SecretHolder
immutable FileNode: remove Client reference
mutable.Publish: stop passing Client
mutable.ServermapUpdater: get StorageBroker in constructor, not by peeking into Client reference
MutableChecker: reference StorageBroker and History directly, not through Client
mutable.FileNode: removed unused indirection to checker classes
mutable.FileNode: remove Client reference
client.py: move RSA key generation into a separate class, so it can be passed to the nodemaker
move create_mutable_file() into NodeMaker
test_dirnode.py: stop using FakeClient mockups, use NoNetworkGrid instead. This simplifies the code, but takes longer to run (17s instead of 6s). This should come down later when other cleanups make it possible to use simpler (non-RSA) fake mutable files for dirnode tests.
test_mutable.py: clean up basedir names
client.py: move create_empty_dirnode() into NodeMaker
dirnode.py: get rid of DirectoryNode.create
remove DirectoryNode.init_from_uri, refactor NodeMaker for customization, simplify test_web's mock Client to match
stop passing Client to DirectoryNode, make DirectoryNode.create_with_mutablefile the normal DirectoryNode constructor, start removing client from NodeMaker
remove Client from NodeMaker
move helper status into History, pass History to web.Status instead of Client
test_mutable.py: fix minor typo
2009-08-15 11:02:56 +00:00
|
|
|
# I think this can no longer be reached
|
2008-05-19 19:57:04 +00:00
|
|
|
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)
|
2009-02-20 19:15:54 +00:00
|
|
|
return filenode.FileNodeDownloadHandler(self.client, node)
|
2008-05-19 19:57:04 +00:00
|
|
|
|
2019-10-13 10:11:24 +00:00
|
|
|
def render_GET(self, ctx):
|
2008-05-19 19:57:04 +00:00
|
|
|
raise WebError("/file must be followed by a file-cap and a name",
|
|
|
|
http.NOT_FOUND)
|
|
|
|
|
2020-04-09 19:09:58 +00:00
|
|
|
class IncidentReporter(MultiFormatResource):
|
|
|
|
"""Handler for /report_incident POST request"""
|
|
|
|
|
|
|
|
def render(self, req):
|
2020-04-14 20:10:20 +00:00
|
|
|
if req.method != "POST":
|
|
|
|
raise WebError("/report_incident can only be used with POST")
|
|
|
|
|
2008-08-05 19:09:21 +00:00
|
|
|
log.msg(format="User reports incident through web page: %(details)s",
|
|
|
|
details=get_arg(req, "details", ""),
|
2008-08-26 01:57:59 +00:00
|
|
|
level=log.WEIRD, umid="LkD9Pw")
|
2020-04-09 19:09:58 +00:00
|
|
|
req.setHeader("content-type", "text/plain; charset=UTF-8")
|
|
|
|
return b"An incident report has been saved to logs/incidents/ in the node directory."
|
2008-08-05 19:09:21 +00:00
|
|
|
|
2011-10-02 03:45:03 +00:00
|
|
|
SPACE = u"\u00A0"*2
|
|
|
|
|
2019-03-21 00:14:47 +00:00
|
|
|
|
2020-04-27 20:42:03 +00:00
|
|
|
class Root(MultiFormatResource):
|
2008-05-19 19:57:04 +00:00
|
|
|
|
|
|
|
addSlash = True
|
|
|
|
|
2019-03-21 18:39:52 +00:00
|
|
|
def __init__(self, client, clock=None, now_fn=None):
|
2020-04-27 20:42:03 +00:00
|
|
|
super(Root, self).__init__()
|
2009-02-20 19:15:54 +00:00
|
|
|
self.client = client
|
2016-01-04 19:58:55 +00:00
|
|
|
self.now_fn = now_fn
|
2019-08-06 23:06:57 +00:00
|
|
|
|
|
|
|
self.putChild("uri", URIHandler(client))
|
|
|
|
self.putChild("cap", URIHandler(client))
|
2009-02-20 19:15:54 +00:00
|
|
|
|
2019-03-21 19:00:57 +00:00
|
|
|
# Handler for everything beneath "/private", an area of the resource
|
|
|
|
# hierarchy which is only accessible with the private per-node API
|
|
|
|
# auth token.
|
2019-08-06 23:06:57 +00:00
|
|
|
self.putChild("private", create_private_tree(client.get_auth_token))
|
2019-03-21 00:14:47 +00:00
|
|
|
|
2019-08-06 23:06:57 +00:00
|
|
|
self.putChild("file", FileHandler(client))
|
|
|
|
self.putChild("named", FileHandler(client))
|
|
|
|
self.putChild("status", status.Status(client.get_history()))
|
|
|
|
self.putChild("statistics", status.Statistics(client.stats_provider))
|
2011-11-17 21:49:23 +00:00
|
|
|
static_dir = resource_filename("allmydata.web", "static")
|
|
|
|
for filen in os.listdir(static_dir):
|
2020-04-27 14:49:44 +00:00
|
|
|
self.putChild(filen, static.File(os.path.join(static_dir, filen)))
|
2009-02-20 19:15:54 +00:00
|
|
|
|
2019-08-06 23:06:57 +00:00
|
|
|
self.putChild("report_incident", IncidentReporter())
|
|
|
|
|
2019-08-07 00:18:14 +00:00
|
|
|
# until we get rid of nevow.Page in favour of twisted.web.resource
|
|
|
|
# we can't use getChild() -- but we CAN use childFactory or
|
|
|
|
# override locatechild
|
|
|
|
def childFactory(self, ctx, name):
|
|
|
|
request = IRequest(ctx)
|
|
|
|
return self.getChild(name, request)
|
|
|
|
|
2008-05-19 19:57:04 +00:00
|
|
|
|
2019-08-06 23:06:57 +00:00
|
|
|
def getChild(self, path, request):
|
|
|
|
if path == "helper_status":
|
|
|
|
# the Helper isn't attached until after the Tub starts, so this child
|
|
|
|
# needs to created on each request
|
|
|
|
return status.HelperStatus(self.client.helper)
|
2019-11-08 19:12:38 +00:00
|
|
|
if path == "storage":
|
|
|
|
# Storage isn't initialized until after the web hierarchy is
|
|
|
|
# constructed so this child needs to be created later than
|
|
|
|
# `__init__`.
|
|
|
|
try:
|
|
|
|
storage_server = self.client.getServiceNamed("storage")
|
|
|
|
except KeyError:
|
|
|
|
storage_server = None
|
|
|
|
return storage.StorageStatus(storage_server, self.client.nickname)
|
2020-04-27 20:42:26 +00:00
|
|
|
if not path:
|
|
|
|
# Render "/" path.
|
|
|
|
return self
|
2008-08-05 19:09:21 +00:00
|
|
|
|
2009-05-03 20:34:42 +00:00
|
|
|
# FIXME: This code is duplicated in root.py and introweb.py.
|
2013-05-19 22:27:23 +00:00
|
|
|
def data_rendered_at(self, ctx, data):
|
2016-01-04 16:00:59 +00:00
|
|
|
return render_time(time.time())
|
2019-07-30 21:36:45 +00:00
|
|
|
|
2008-05-19 19:57:04 +00:00
|
|
|
def data_version(self, ctx, data):
|
|
|
|
return get_package_versions_string()
|
2019-07-30 21:36:45 +00:00
|
|
|
|
2008-05-19 19:57:04 +00:00
|
|
|
def data_import_path(self, ctx, data):
|
|
|
|
return str(allmydata)
|
2019-07-30 21:36:45 +00:00
|
|
|
|
2020-04-27 20:42:03 +00:00
|
|
|
def render_HTML(self, req):
|
|
|
|
return renderElement(req, RootElement(self.client))
|
2017-07-25 15:56:42 +00:00
|
|
|
|
2017-07-25 15:36:06 +00:00
|
|
|
def render_JSON(self, req):
|
|
|
|
req.setHeader("content-type", "application/json; charset=utf-8")
|
2017-01-12 23:14:04 +00:00
|
|
|
intro_summaries = [s.summary for s in self.client.introducer_connection_statuses()]
|
|
|
|
sb = self.client.get_storage_broker()
|
2017-07-25 15:56:42 +00:00
|
|
|
servers = self._describe_known_servers(sb)
|
|
|
|
result = {
|
|
|
|
"introducers": {
|
|
|
|
"statuses": intro_summaries,
|
|
|
|
},
|
|
|
|
"servers": servers
|
|
|
|
}
|
|
|
|
return json.dumps(result, indent=1) + "\n"
|
|
|
|
|
|
|
|
|
|
|
|
def _describe_known_servers(self, broker):
|
|
|
|
return sorted(list(
|
2017-07-27 19:32:47 +00:00
|
|
|
self._describe_server(server)
|
2017-07-25 15:56:42 +00:00
|
|
|
for server
|
|
|
|
in broker.get_known_servers()
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
def _describe_server(self, server):
|
2017-07-27 20:31:41 +00:00
|
|
|
status = server.get_connection_status()
|
2017-07-25 15:56:42 +00:00
|
|
|
description = {
|
2017-07-27 19:32:47 +00:00
|
|
|
u"nodeid": server.get_serverid(),
|
2017-07-27 20:31:41 +00:00
|
|
|
u"connection_status": status.summary,
|
2017-07-25 15:56:42 +00:00
|
|
|
u"available_space": server.get_available_space(),
|
|
|
|
u"nickname": server.get_nickname(),
|
|
|
|
u"version": None,
|
2017-07-27 20:31:41 +00:00
|
|
|
u"last_received_data": status.last_received_time,
|
2017-07-25 15:56:42 +00:00
|
|
|
}
|
|
|
|
version = server.get_version()
|
|
|
|
if version is not None:
|
|
|
|
description[u"version"] = version["application-version"]
|
|
|
|
|
|
|
|
return description
|
|
|
|
|
2020-04-27 20:42:03 +00:00
|
|
|
class RootElement(Element):
|
|
|
|
|
|
|
|
loader = XMLFile(FilePath(__file__).sibling("welcome.xhtml"))
|
|
|
|
|
|
|
|
def __init__(self, client):
|
|
|
|
super(RootElement, self).__init__()
|
|
|
|
self._client = client
|
|
|
|
|
2020-04-27 20:52:54 +00:00
|
|
|
_connectedalts = {
|
|
|
|
"not-configured": "Not Configured",
|
|
|
|
"yes": "Connected",
|
|
|
|
"no": "Disconnected",
|
|
|
|
}
|
|
|
|
|
2020-04-27 20:42:03 +00:00
|
|
|
@renderer
|
|
|
|
def my_nodeid(self, req, tag):
|
|
|
|
tubid_s = "TubID: "+self._client.get_long_tubid()
|
|
|
|
return tags.td(self._client.get_long_nodeid(), title=tubid_s)
|
|
|
|
|
|
|
|
@renderer
|
|
|
|
def my_nickname(self, req, tag):
|
|
|
|
return tag(self._client.nickname)
|
2017-01-11 02:25:42 +00:00
|
|
|
|
2020-04-27 20:52:54 +00:00
|
|
|
def _connected_introducers(self):
|
|
|
|
return len([1 for cs in self._client.introducer_connection_statuses()
|
|
|
|
if cs.connected])
|
|
|
|
|
2020-04-28 11:17:22 +00:00
|
|
|
@renderer
|
|
|
|
def connected_introducers(self, req, tag):
|
|
|
|
return tag(str(self._connected_introducers()))
|
|
|
|
|
2020-04-27 20:52:54 +00:00
|
|
|
@renderer
|
|
|
|
def connected_to_at_least_one_introducer(self, req, tag):
|
|
|
|
if self._connected_introducers():
|
|
|
|
return "yes"
|
|
|
|
return "no"
|
|
|
|
|
|
|
|
@renderer
|
|
|
|
def connected_to_at_least_one_introducer_alt(self, req, tag):
|
|
|
|
state = self.connected_to_at_least_one_introducer(req, tag)
|
|
|
|
return self._connectedalts.get(state)
|
|
|
|
|
2020-04-27 21:19:19 +00:00
|
|
|
@renderer
|
|
|
|
def services(self, req, tag):
|
|
|
|
ul = tags.ul()
|
2008-05-19 19:57:04 +00:00
|
|
|
try:
|
2020-04-27 21:19:19 +00:00
|
|
|
ss = self._client.getServiceNamed("storage")
|
2009-02-20 21:29:26 +00:00
|
|
|
stats = ss.get_stats()
|
|
|
|
if stats["storage_server.accepting_immutable_shares"]:
|
|
|
|
msg = "accepting new shares"
|
|
|
|
else:
|
|
|
|
msg = "not accepting new shares (read-only)"
|
|
|
|
available = stats.get("storage_server.disk_avail")
|
|
|
|
if available is not None:
|
|
|
|
msg += ", %s available" % abbreviate_size(available)
|
2020-04-27 21:19:19 +00:00
|
|
|
ul(tags.li(tags.a("Storage Server", ": ", msg, href="storage")))
|
2008-05-19 19:57:04 +00:00
|
|
|
except KeyError:
|
2020-04-27 21:19:19 +00:00
|
|
|
ul(tags.li("Not running storage server"))
|
2008-05-19 19:57:04 +00:00
|
|
|
|
2020-04-27 21:19:19 +00:00
|
|
|
if self._client.helper:
|
|
|
|
stats = self._client.helper.get_stats()
|
2008-05-19 19:57:04 +00:00
|
|
|
active_uploads = stats["chk_upload_helper.active_uploads"]
|
2020-04-27 21:19:19 +00:00
|
|
|
ul(tags.li("Helper: %d active uploads" % (active_uploads,)))
|
2009-08-15 20:17:37 +00:00
|
|
|
else:
|
2020-04-27 21:19:19 +00:00
|
|
|
ul(tags.li("Not running helper"))
|
2008-05-19 19:57:04 +00:00
|
|
|
|
2020-04-27 21:19:19 +00:00
|
|
|
return tag(ul)
|
2008-05-19 19:57:04 +00:00
|
|
|
|
2020-04-27 20:55:10 +00:00
|
|
|
@renderer
|
|
|
|
def introducer_description(self, req, tag):
|
|
|
|
connected_count = self._connected_introducers()
|
2016-09-12 23:01:23 +00:00
|
|
|
if connected_count == 0:
|
2020-04-27 20:55:10 +00:00
|
|
|
return tag("No introducers connected")
|
2016-09-12 23:01:23 +00:00
|
|
|
elif connected_count == 1:
|
2020-04-27 20:55:10 +00:00
|
|
|
return tag("1 introducer connected")
|
2013-03-21 00:25:49 +00:00
|
|
|
else:
|
2020-04-27 20:55:10 +00:00
|
|
|
return tag("%s introducers connected" % (connected_count,))
|
2012-12-29 04:17:00 +00:00
|
|
|
|
2020-04-28 11:19:47 +00:00
|
|
|
@renderer
|
|
|
|
def total_introducers(self, req, tag):
|
2020-04-28 11:42:57 +00:00
|
|
|
return tag(str(len(self._client.introducer_connection_statuses())))
|
2016-09-12 23:01:23 +00:00
|
|
|
|
|
|
|
# In case we configure multiple introducers
|
|
|
|
def data_introducers(self, ctx, data):
|
2016-12-08 23:15:49 +00:00
|
|
|
return self.client.introducer_connection_statuses()
|
|
|
|
|
2020-04-28 11:14:35 +00:00
|
|
|
def _render_connection_status(self, tag, cs):
|
2016-12-08 23:15:49 +00:00
|
|
|
connected = "yes" if cs.connected else "no"
|
2020-04-28 11:14:35 +00:00
|
|
|
tag.fillSlots("service_connection_status", connected)
|
|
|
|
tag.fillSlots("service_connection_status_alt",
|
2016-12-08 23:15:49 +00:00
|
|
|
self._connectedalts[connected])
|
|
|
|
|
|
|
|
since = cs.last_connection_time
|
2020-04-28 11:14:35 +00:00
|
|
|
tag.fillSlots("service_connection_status_rel_time",
|
2016-12-08 23:15:49 +00:00
|
|
|
render_time_delta(since, self.now_fn())
|
|
|
|
if since is not None
|
|
|
|
else "N/A")
|
2020-04-28 11:14:35 +00:00
|
|
|
tag.fillSlots("service_connection_status_abs_time",
|
2016-12-08 23:15:49 +00:00
|
|
|
render_time_attr(since)
|
|
|
|
if since is not None
|
|
|
|
else "N/A")
|
|
|
|
|
|
|
|
last_received_data_time = cs.last_received_time
|
2020-04-28 11:14:35 +00:00
|
|
|
tag.fillSlots("last_received_data_abs_time",
|
2016-12-08 23:15:49 +00:00
|
|
|
render_time_attr(last_received_data_time)
|
|
|
|
if last_received_data_time is not None
|
|
|
|
else "N/A")
|
2020-04-28 11:14:35 +00:00
|
|
|
tag.fillSlots("last_received_data_rel_time",
|
|
|
|
render_time_delta(last_received_data_time,
|
|
|
|
self.now_fn())
|
2016-12-08 23:15:49 +00:00
|
|
|
if last_received_data_time is not None
|
|
|
|
else "N/A")
|
2016-12-10 00:35:46 +00:00
|
|
|
|
|
|
|
others = cs.non_connected_statuses
|
|
|
|
if cs.connected:
|
2020-04-28 11:14:35 +00:00
|
|
|
tag.fillSlots("summary", cs.summary)
|
2016-12-10 00:35:46 +00:00
|
|
|
if others:
|
|
|
|
details = "\n".join(["* %s: %s\n" % (which, others[which])
|
|
|
|
for which in sorted(others)])
|
2020-04-28 11:14:35 +00:00
|
|
|
tag.fillSlots("details", "Other hints:\n" + details)
|
2016-12-10 00:35:46 +00:00
|
|
|
else:
|
2020-04-28 11:14:35 +00:00
|
|
|
tag.fillSlots("details", "(no other hints)")
|
2016-12-10 00:35:46 +00:00
|
|
|
else:
|
2020-04-28 11:14:35 +00:00
|
|
|
details = tags.ul()
|
2016-12-10 00:35:46 +00:00
|
|
|
for which in sorted(others):
|
2020-04-28 11:14:35 +00:00
|
|
|
details[tags.li("%s: %s" % (which, others[which]))]
|
|
|
|
tag.fillSlots("summary", [cs.summary, details])
|
|
|
|
tag.fillSlots("details", "")
|
2016-12-10 00:35:46 +00:00
|
|
|
|
|
|
|
def render_introducers_row(self, ctx, cs):
|
|
|
|
self._render_connection_status(ctx, cs)
|
2016-09-12 23:01:23 +00:00
|
|
|
return ctx.tag
|
2015-01-21 00:31:56 +00:00
|
|
|
|
2020-04-27 21:11:36 +00:00
|
|
|
@renderer
|
|
|
|
def helper_furl_prefix(self, req, tag):
|
2008-05-19 19:57:04 +00:00
|
|
|
try:
|
2020-04-27 21:11:36 +00:00
|
|
|
uploader = self._client.getServiceNamed("uploader")
|
2008-05-19 19:57:04 +00:00
|
|
|
except KeyError:
|
2020-04-27 21:11:36 +00:00
|
|
|
return tag("None")
|
2008-05-19 19:57:04 +00:00
|
|
|
furl, connected = uploader.get_helper_info()
|
2013-03-21 00:25:49 +00:00
|
|
|
if not furl:
|
2020-04-27 21:11:36 +00:00
|
|
|
return tag("None")
|
2013-03-21 00:25:49 +00:00
|
|
|
# trim off the secret swissnum
|
|
|
|
(prefix, _, swissnum) = furl.rpartition("/")
|
2020-04-27 21:11:36 +00:00
|
|
|
return tag("%s/[censored]" % (prefix,))
|
2012-12-29 04:17:00 +00:00
|
|
|
|
2020-04-27 21:07:09 +00:00
|
|
|
def _connected_to_helper(self):
|
2008-05-19 19:57:04 +00:00
|
|
|
try:
|
2020-04-27 21:01:37 +00:00
|
|
|
uploader = self._client.getServiceNamed("uploader")
|
2008-05-19 19:57:04 +00:00
|
|
|
except KeyError:
|
|
|
|
return "no" # we don't even have an Uploader
|
|
|
|
furl, connected = uploader.get_helper_info()
|
2012-12-29 04:17:00 +00:00
|
|
|
|
|
|
|
if furl is None:
|
|
|
|
return "not-configured"
|
2008-05-19 19:57:04 +00:00
|
|
|
if connected:
|
|
|
|
return "yes"
|
|
|
|
return "no"
|
|
|
|
|
2020-04-27 21:07:09 +00:00
|
|
|
@renderer
|
|
|
|
def helper_description(self, req, tag):
|
|
|
|
if self._connected_to_helper() == "no":
|
|
|
|
return tag("Helper not connected")
|
|
|
|
return tag("Helper")
|
|
|
|
|
|
|
|
@renderer
|
|
|
|
def connected_to_helper(self, req, tag):
|
|
|
|
return tag(self._connected_to_helper())
|
|
|
|
|
2020-04-27 21:01:37 +00:00
|
|
|
@renderer
|
|
|
|
def connected_to_helper_alt(self, req, tag):
|
2020-04-27 21:07:09 +00:00
|
|
|
return tag(self._connectedalts.get(self._connected_to_helper()))
|
2015-01-21 00:31:56 +00:00
|
|
|
|
2020-04-27 21:22:27 +00:00
|
|
|
@renderer
|
|
|
|
def known_storage_servers(self, req, tag):
|
|
|
|
sb = self._client.get_storage_broker()
|
|
|
|
return tag(str(len(sb.get_all_serverids())))
|
2008-05-19 19:57:04 +00:00
|
|
|
|
2020-04-27 21:22:27 +00:00
|
|
|
@renderer
|
|
|
|
def connected_storage_servers(self, req, tag):
|
|
|
|
sb = self._client.get_storage_broker()
|
|
|
|
return tag(str(len(sb.get_connected_servers())))
|
2008-05-19 19:57:04 +00:00
|
|
|
|
2020-04-28 11:14:35 +00:00
|
|
|
def _services(self):
|
|
|
|
sb = self._client.get_storage_broker()
|
2016-12-10 00:35:46 +00:00
|
|
|
return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
|
2009-06-23 02:10:47 +00:00
|
|
|
|
2020-04-28 11:14:35 +00:00
|
|
|
@renderer
|
|
|
|
def service_row(self, req, tag):
|
|
|
|
servers = self._services()
|
|
|
|
|
|
|
|
# FIXME: handle empty list of servers in a better manner.
|
|
|
|
if not servers:
|
|
|
|
tag.fillSlots(peerid="",
|
|
|
|
nickname="",
|
|
|
|
service_connection_status="",
|
|
|
|
service_connection_status_alt="",
|
|
|
|
details="",
|
|
|
|
summary="",
|
|
|
|
service_connection_status_abs_time="",
|
|
|
|
service_connection_status_rel_time="",
|
|
|
|
last_received_data_abs_time="",
|
|
|
|
last_received_data_rel_time="",
|
|
|
|
version="",
|
|
|
|
available_space="")
|
|
|
|
|
|
|
|
for server in servers:
|
|
|
|
cs = server.get_connection_status()
|
|
|
|
self._render_connection_status(tag, cs)
|
|
|
|
|
|
|
|
tag.fillSlots("peerid", server.get_longname())
|
|
|
|
tag.fillSlots("nickname", server.get_nickname())
|
|
|
|
|
|
|
|
announcement = server.get_announcement()
|
|
|
|
version = announcement.get("my-version", "")
|
|
|
|
available_space = server.get_available_space()
|
|
|
|
if available_space is None:
|
|
|
|
available_space = "N/A"
|
|
|
|
else:
|
|
|
|
available_space = abbreviate_size(available_space)
|
|
|
|
tag.fillSlots("version", version)
|
|
|
|
tag.fillSlots("available_space", available_space)
|
2008-05-19 19:57:04 +00:00
|
|
|
|
2020-04-28 11:14:35 +00:00
|
|
|
return tag
|
2008-05-19 19:57:04 +00:00
|
|
|
|
|
|
|
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"],
|
2011-10-02 03:45:03 +00:00
|
|
|
T.div["Tahoe-URI to download:"+SPACE,
|
2009-05-26 23:25:45 +00:00
|
|
|
T.input(type="text", name="uri")],
|
2011-10-02 03:45:03 +00:00
|
|
|
T.div["Filename to download as:"+SPACE,
|
2009-05-26 23:25:45 +00:00
|
|
|
T.input(type="text", name="filename")],
|
2008-05-19 19:57:04 +00:00
|
|
|
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"],
|
2011-10-02 03:45:03 +00:00
|
|
|
"Tahoe-URI to view:"+SPACE,
|
|
|
|
T.input(type="text", name="uri"), SPACE*2,
|
2008-05-19 19:57:04 +00:00
|
|
|
T.input(type="submit", value="View!"),
|
|
|
|
]]
|
|
|
|
return T.div[form]
|
|
|
|
|
|
|
|
def render_upload_form(self, ctx, data):
|
2011-10-02 03:45:03 +00:00
|
|
|
# This is a form where users can upload unlinked files.
|
|
|
|
# Users can choose immutable, SDMF, or MDMF from a radio button.
|
2011-08-07 00:43:48 +00:00
|
|
|
|
2011-10-02 03:45:03 +00:00
|
|
|
upload_chk = T.input(type='radio', name='format',
|
|
|
|
value='chk', id='upload-chk',
|
|
|
|
checked='checked')
|
|
|
|
upload_sdmf = T.input(type='radio', name='format',
|
|
|
|
value='sdmf', id='upload-sdmf')
|
|
|
|
upload_mdmf = T.input(type='radio', name='format',
|
|
|
|
value='mdmf', id='upload-mdmf')
|
2011-08-07 00:43:48 +00:00
|
|
|
|
2008-05-19 19:57:04 +00:00
|
|
|
form = T.form(action="uri", method="post",
|
|
|
|
enctype="multipart/form-data")[
|
|
|
|
T.fieldset[
|
|
|
|
T.legend(class_="freeform-form-label")["Upload a file"],
|
2011-10-02 03:45:03 +00:00
|
|
|
T.div["Choose a file:"+SPACE,
|
2009-05-26 23:25:45 +00:00
|
|
|
T.input(type="file", name="file", class_="freeform-input-file")],
|
2008-05-19 19:57:04 +00:00
|
|
|
T.input(type="hidden", name="t", value="upload"),
|
2011-10-02 03:45:03 +00:00
|
|
|
T.div[upload_chk, T.label(for_="upload-chk") [" Immutable"], SPACE,
|
|
|
|
upload_sdmf, T.label(for_="upload-sdmf")[" SDMF"], SPACE,
|
|
|
|
upload_mdmf, T.label(for_="upload-mdmf")[" MDMF (experimental)"], SPACE*2,
|
|
|
|
T.input(type="submit", value="Upload!")],
|
2008-05-19 19:57:04 +00:00
|
|
|
]]
|
|
|
|
return T.div[form]
|
|
|
|
|
|
|
|
def render_mkdir_form(self, ctx, data):
|
2011-10-02 03:45:03 +00:00
|
|
|
# This is a form where users can create new directories.
|
|
|
|
# Users can choose SDMF or MDMF from a radio button.
|
|
|
|
|
|
|
|
mkdir_sdmf = T.input(type='radio', name='format',
|
|
|
|
value='sdmf', id='mkdir-sdmf',
|
2011-08-07 00:43:48 +00:00
|
|
|
checked='checked')
|
2011-10-02 03:45:03 +00:00
|
|
|
mkdir_mdmf = T.input(type='radio', name='format',
|
|
|
|
value='mdmf', id='mkdir-mdmf')
|
|
|
|
|
2008-05-19 19:57:04 +00:00
|
|
|
form = T.form(action="uri", method="post",
|
|
|
|
enctype="multipart/form-data")[
|
|
|
|
T.fieldset[
|
2009-05-26 23:25:45 +00:00
|
|
|
T.legend(class_="freeform-form-label")["Create a directory"],
|
2011-10-02 03:45:03 +00:00
|
|
|
mkdir_sdmf, T.label(for_='mkdir-sdmf')[" SDMF"], SPACE,
|
|
|
|
mkdir_mdmf, T.label(for_='mkdir-mdmf')[" MDMF (experimental)"], SPACE*2,
|
2008-05-19 19:57:04 +00:00
|
|
|
T.input(type="hidden", name="t", value="mkdir"),
|
|
|
|
T.input(type="hidden", name="redirect_to_result", value="true"),
|
2009-05-26 23:25:45 +00:00
|
|
|
T.input(type="submit", value="Create a directory"),
|
2008-05-19 19:57:04 +00:00
|
|
|
]]
|
|
|
|
return T.div[form]
|
|
|
|
|
2020-04-27 20:43:47 +00:00
|
|
|
@renderer
|
|
|
|
def incident_button(self, req, tag):
|
2008-08-05 19:09:21 +00:00
|
|
|
# this button triggers a foolscap-logging "incident"
|
2020-04-27 20:43:47 +00:00
|
|
|
form = tags.form(
|
|
|
|
tags.fieldset(
|
|
|
|
tags.input(type="hidden", name="t", value="report-incident"),
|
|
|
|
"What went wrong?"+SPACE,
|
|
|
|
tags.input(type="text", name="details"), SPACE,
|
|
|
|
tags.input(type="submit", value=u"Save \u00BB"),
|
|
|
|
),
|
|
|
|
action="report_incident",
|
|
|
|
method="post",
|
|
|
|
enctype="multipart/form-data"
|
|
|
|
)
|
|
|
|
return tags.div(form)
|