mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2024-12-25 15:41:06 +00:00
b956d9a58c
Make the diff against master a tad bit smaller.
1493 lines
54 KiB
Python
1493 lines
54 KiB
Python
|
|
import json
|
|
import urllib
|
|
from datetime import timedelta
|
|
|
|
from zope.interface import implementer
|
|
from twisted.internet import defer
|
|
from twisted.internet.interfaces import IPushProducer
|
|
from twisted.python.failure import Failure
|
|
from twisted.web import http
|
|
from twisted.web.resource import ErrorPage
|
|
from twisted.web.resource import Resource
|
|
from twisted.web.template import (
|
|
Element,
|
|
XMLFile,
|
|
renderElement,
|
|
renderer,
|
|
tags,
|
|
)
|
|
from hyperlink import URL
|
|
from twisted.python.filepath import FilePath
|
|
|
|
from allmydata.util import base32
|
|
from allmydata.util.encodingutil import (
|
|
to_str,
|
|
quote_output,
|
|
)
|
|
from allmydata.uri import (
|
|
from_string_dirnode,
|
|
from_string,
|
|
CHKFileURI,
|
|
WriteableSSKFileURI,
|
|
ReadonlySSKFileURI,
|
|
)
|
|
from allmydata.interfaces import IDirectoryNode, IFileNode, IFilesystemNode, \
|
|
IImmutableFileNode, IMutableFileNode, ExistingChildError, \
|
|
NoSuchChildError, EmptyPathnameComponentError, SDMF_VERSION, MDMF_VERSION
|
|
from allmydata.blacklist import ProhibitedNode
|
|
from allmydata.monitor import Monitor, OperationCancelledError
|
|
from allmydata import dirnode
|
|
from allmydata.web.common import (
|
|
text_plain,
|
|
WebError,
|
|
NeedOperationHandleError,
|
|
boolean_of_arg,
|
|
get_arg,
|
|
get_root,
|
|
parse_replace_arg,
|
|
should_create_intermediate_directories,
|
|
humanize_failure,
|
|
convert_children_json,
|
|
get_format,
|
|
get_mutable_type,
|
|
get_filenode_metadata,
|
|
render_time,
|
|
MultiFormatPage,
|
|
MultiFormatResource,
|
|
SlotsSequenceElement,
|
|
)
|
|
from allmydata.web.filenode import ReplaceMeMixin, \
|
|
FileNodeHandler, PlaceHolderNodeHandler
|
|
from allmydata.web.check_results import CheckResultsRenderer, \
|
|
CheckAndRepairResultsRenderer, DeepCheckResultsRenderer, \
|
|
DeepCheckAndRepairResultsRenderer, LiteralCheckResultsRenderer
|
|
from allmydata.web.info import MoreInfo
|
|
from allmydata.web.operations import ReloadMixin
|
|
from allmydata.web.check_results import json_check_results, \
|
|
json_check_and_repair_results
|
|
|
|
|
|
class BlockingFileError(Exception):
|
|
# TODO: catch and transform
|
|
"""We cannot auto-create a parent directory, because there is a file in
|
|
the way"""
|
|
|
|
|
|
def make_handler_for(node, client, parentnode=None, name=None):
|
|
if parentnode:
|
|
assert IDirectoryNode.providedBy(parentnode)
|
|
if IFileNode.providedBy(node):
|
|
return FileNodeHandler(client, node, parentnode, name)
|
|
if IDirectoryNode.providedBy(node):
|
|
return DirectoryNodeHandler(client, node, parentnode, name)
|
|
return UnknownNodeHandler(client, node, parentnode, name)
|
|
|
|
|
|
class DirectoryNodeHandler(ReplaceMeMixin, Resource, object):
|
|
|
|
def __init__(self, client, node, parentnode=None, name=None):
|
|
super(DirectoryNodeHandler, self).__init__()
|
|
self.client = client
|
|
assert node
|
|
self.node = node
|
|
self.parentnode = parentnode
|
|
self.name = name
|
|
self._operations = client.get_web_service().get_operations()
|
|
|
|
def getChild(self, name, req):
|
|
"""
|
|
Dynamically create a child for the given request and name
|
|
"""
|
|
# trying to replicate what I have observed as Nevow behavior
|
|
# for these nodes, which is that a URI like
|
|
# "/uri/URI%3ADIR2%3Aj...vq/" (that is, with a trailing slash
|
|
# or no further children) renders "this" page. We also need
|
|
# to reject "/uri/URI:DIR2:..//", so we look at postpath.
|
|
name = name.decode('utf8')
|
|
if not name and req.postpath != ['']:
|
|
return self
|
|
|
|
# Rejecting URIs that contain empty path pieces (for example:
|
|
# "/uri/URI:DIR2:../foo//new.txt" or "/uri/URI:DIR2:..//") was
|
|
# the old nevow behavior and it is encoded in the test suite;
|
|
# we will follow suit.
|
|
for segment in req.prepath:
|
|
if not segment:
|
|
raise EmptyPathnameComponentError(
|
|
u"The webapi does not allow empty pathname components",
|
|
)
|
|
|
|
d = self.node.get(name)
|
|
d.addBoth(self._got_child, req, name)
|
|
return d
|
|
|
|
def _got_child(self, node_or_failure, req, name):
|
|
"""
|
|
Callback when self.node.get has returned, meaning we have received
|
|
whatever child was requested -- that is `node.get` has
|
|
returned something (maybe an error). This method then performs
|
|
the rest of the work of the Twisted API getChild(): returning
|
|
a suitable child resource to Twisted Web.
|
|
"""
|
|
terminal = (req.prepath + req.postpath)[-1].decode('utf8') == name
|
|
nonterminal = not terminal #len(req.postpath) > 0
|
|
|
|
t = get_arg(req, "t", "").strip()
|
|
if isinstance(node_or_failure, Failure):
|
|
f = node_or_failure
|
|
f.trap(NoSuchChildError)
|
|
# No child by this name. What should we do about it?
|
|
if nonterminal:
|
|
if should_create_intermediate_directories(req):
|
|
# create intermediate directories
|
|
d = self.node.create_subdirectory(name)
|
|
d.addCallback(make_handler_for,
|
|
self.client, self.node, name)
|
|
return d
|
|
else:
|
|
# terminal node
|
|
terminal_requests = (
|
|
("POST", "mkdir"),
|
|
("PUT", "mkdir"),
|
|
("POST", "mkdir-with-children"),
|
|
("POST", "mkdir-immutable")
|
|
)
|
|
if (req.method, t) in terminal_requests:
|
|
# final directory
|
|
kids = {}
|
|
if t in ("mkdir-with-children", "mkdir-immutable"):
|
|
req.content.seek(0)
|
|
kids_json = req.content.read()
|
|
kids = convert_children_json(
|
|
self.client.nodemaker,
|
|
kids_json,
|
|
)
|
|
file_format = get_format(req, None)
|
|
mutable = True
|
|
mt = get_mutable_type(file_format)
|
|
if t == "mkdir-immutable":
|
|
mutable = False
|
|
|
|
d = self.node.create_subdirectory(
|
|
name, kids,
|
|
mutable=mutable,
|
|
mutable_version=mt,
|
|
)
|
|
d.addCallback(
|
|
make_handler_for,
|
|
self.client, self.node, name,
|
|
)
|
|
return d
|
|
leaf_requests = (
|
|
("PUT",""),
|
|
("PUT","uri"),
|
|
)
|
|
if (req.method, t) in leaf_requests:
|
|
# we were trying to find the leaf filenode (to put a new
|
|
# file in its place), and it didn't exist. That's ok,
|
|
# since that's the leaf node that we're about to create.
|
|
# We make a dummy one, which will respond to the PUT
|
|
# request by replacing itself.
|
|
return PlaceHolderNodeHandler(self.client, self.node, name)
|
|
# otherwise, we just return a no-such-child error
|
|
return f
|
|
|
|
node = node_or_failure
|
|
if nonterminal and should_create_intermediate_directories(req):
|
|
if not IDirectoryNode.providedBy(node):
|
|
# we would have put a new directory here, but there was a
|
|
# file in the way.
|
|
return ErrorPage(
|
|
http.CONFLICT,
|
|
"Unable to create directory %s: a file was in the way" % quote_output(name),
|
|
"no details",
|
|
)
|
|
return make_handler_for(node, self.client, self.node, name)
|
|
|
|
def render_DELETE(self, req):
|
|
assert self.parentnode and self.name
|
|
d = self.parentnode.delete(self.name)
|
|
d.addCallback(lambda res: self.node.get_uri())
|
|
return d
|
|
|
|
def render_GET(self, req):
|
|
# This is where all of the directory-related ?t=* code goes.
|
|
t = get_arg(req, "t", "").strip()
|
|
|
|
# t=info contains variable ophandles, t=rename-form contains the name
|
|
# of the child being renamed. Neither is allowed an ETag.
|
|
FIXED_OUTPUT_TYPES = ["", "json", "uri", "readonly-uri"]
|
|
if not self.node.is_mutable() and t in FIXED_OUTPUT_TYPES:
|
|
si = self.node.get_storage_index()
|
|
if si and req.setETag('DIR:%s-%s' % (base32.b2a(si), t or "")):
|
|
return ""
|
|
|
|
if not t:
|
|
# render the directory as HTML
|
|
return renderElement(
|
|
req,
|
|
DirectoryAsHTML(
|
|
self.node,
|
|
self.client.mutable_file_default,
|
|
)
|
|
)
|
|
|
|
if t == "json":
|
|
return _directory_json_metadata(req, self.node)
|
|
if t == "info":
|
|
return MoreInfo(self.node)
|
|
if t == "uri":
|
|
return _directory_uri(req, self.node)
|
|
if t == "readonly-uri":
|
|
return _directory_readonly_uri(req, self.node)
|
|
if t == 'rename-form':
|
|
return renderElement(
|
|
req,
|
|
RenameForm(self.node),
|
|
)
|
|
|
|
raise WebError("GET directory: bad t=%s" % t)
|
|
|
|
def render_PUT(self, req):
|
|
t = get_arg(req, "t", "").strip()
|
|
replace = parse_replace_arg(get_arg(req, "replace", "true"))
|
|
|
|
if t == "mkdir":
|
|
# our job was done by the traversal/create-intermediate-directory
|
|
# process that got us here.
|
|
return text_plain(self.node.get_uri(), req) # TODO: urlencode
|
|
if t == "uri":
|
|
if not replace:
|
|
# they're trying to set_uri and that name is already occupied
|
|
# (by us).
|
|
raise ExistingChildError()
|
|
d = self.replace_me_with_a_childcap(req, self.client, replace)
|
|
# TODO: results
|
|
return d
|
|
|
|
raise WebError("PUT to a directory")
|
|
|
|
def render_POST(self, req):
|
|
t = get_arg(req, "t", "").strip()
|
|
|
|
if t == "mkdir":
|
|
d = self._POST_mkdir(req)
|
|
elif t == "mkdir-with-children":
|
|
d = self._POST_mkdir_with_children(req)
|
|
elif t == "mkdir-immutable":
|
|
d = self._POST_mkdir_immutable(req)
|
|
elif t == "upload":
|
|
d = self._POST_upload(req) # this one needs the context
|
|
elif t == "uri":
|
|
d = self._POST_uri(req)
|
|
elif t == "delete" or t == "unlink":
|
|
d = self._POST_unlink(req)
|
|
elif t == "rename":
|
|
d = self._POST_rename(req)
|
|
elif t == "relink":
|
|
d = self._POST_relink(req)
|
|
elif t == "check":
|
|
d = self._POST_check(req)
|
|
elif t == "start-deep-check":
|
|
d = self._POST_start_deep_check(req)
|
|
elif t == "stream-deep-check":
|
|
d = self._POST_stream_deep_check(req)
|
|
elif t == "start-manifest":
|
|
d = self._POST_start_manifest(req)
|
|
elif t == "start-deep-size":
|
|
d = self._POST_start_deep_size(req)
|
|
elif t == "start-deep-stats":
|
|
d = self._POST_start_deep_stats(req)
|
|
elif t == "stream-manifest":
|
|
d = self._POST_stream_manifest(req)
|
|
elif t == "set_children" or t == "set-children":
|
|
d = self._POST_set_children(req)
|
|
else:
|
|
raise WebError("POST to a directory with bad t=%s" % t)
|
|
|
|
when_done = get_arg(req, "when_done", None)
|
|
if when_done:
|
|
def done(res):
|
|
req.redirect(when_done)
|
|
return res
|
|
d.addCallback(done)
|
|
return d
|
|
|
|
def _POST_mkdir(self, req):
|
|
name = get_arg(req, "name", "")
|
|
if not name:
|
|
# our job is done, it was handled by the code in got_child
|
|
# which created the final directory (i.e. us)
|
|
return defer.succeed(self.node.get_uri()) # TODO: urlencode
|
|
name = name.decode("utf-8")
|
|
replace = boolean_of_arg(get_arg(req, "replace", "true"))
|
|
kids = {}
|
|
mt = get_mutable_type(get_format(req, None))
|
|
d = self.node.create_subdirectory(name, kids, overwrite=replace,
|
|
mutable_version=mt)
|
|
d.addCallback(lambda child: child.get_uri()) # TODO: urlencode
|
|
return d
|
|
|
|
def _POST_mkdir_with_children(self, req):
|
|
name = get_arg(req, "name", "")
|
|
if not name:
|
|
# our job is done, it was handled by the code in got_child
|
|
# which created the final directory (i.e. us)
|
|
return defer.succeed(self.node.get_uri()) # TODO: urlencode
|
|
name = name.decode("utf-8")
|
|
# TODO: decide on replace= behavior, see #903
|
|
#replace = boolean_of_arg(get_arg(req, "replace", "false"))
|
|
req.content.seek(0)
|
|
kids_json = req.content.read()
|
|
kids = convert_children_json(self.client.nodemaker, kids_json)
|
|
mt = get_mutable_type(get_format(req, None))
|
|
d = self.node.create_subdirectory(name, kids, overwrite=False,
|
|
mutable_version=mt)
|
|
d.addCallback(lambda child: child.get_uri()) # TODO: urlencode
|
|
return d
|
|
|
|
def _POST_mkdir_immutable(self, req):
|
|
name = get_arg(req, "name", "")
|
|
if not name:
|
|
# our job is done, it was handled by the code in got_child
|
|
# which created the final directory (i.e. us)
|
|
return defer.succeed(self.node.get_uri()) # TODO: urlencode
|
|
name = name.decode("utf-8")
|
|
# TODO: decide on replace= behavior, see #903
|
|
#replace = boolean_of_arg(get_arg(req, "replace", "false"))
|
|
req.content.seek(0)
|
|
kids_json = req.content.read()
|
|
kids = convert_children_json(self.client.nodemaker, kids_json)
|
|
d = self.node.create_subdirectory(name, kids, overwrite=False, mutable=False)
|
|
d.addCallback(lambda child: child.get_uri()) # TODO: urlencode
|
|
return d
|
|
|
|
def _POST_upload(self, req):
|
|
charset = get_arg(req, "_charset", "utf-8")
|
|
contents = req.fields["file"]
|
|
assert contents.filename is None or isinstance(contents.filename, str)
|
|
name = get_arg(req, "name")
|
|
name = name or contents.filename
|
|
if name is not None:
|
|
name = name.strip()
|
|
if not name:
|
|
# this prohibts empty, missing, and all-whitespace filenames
|
|
raise WebError("upload requires a name")
|
|
assert isinstance(name, str)
|
|
name = name.decode(charset)
|
|
if "/" in name:
|
|
raise WebError("name= may not contain a slash", http.BAD_REQUEST)
|
|
assert isinstance(name, unicode)
|
|
|
|
# since POST /uri/path/file?t=upload is equivalent to
|
|
# POST /uri/path/dir?t=upload&name=foo, just do the same thing that
|
|
# childFactory would do. Things are cleaner if we only do a subset of
|
|
# them, though, so we don't do: d = self.childFactory(req, name)
|
|
|
|
d = self.node.get(name)
|
|
def _maybe_got_node(node_or_failure):
|
|
if isinstance(node_or_failure, Failure):
|
|
f = node_or_failure
|
|
f.trap(NoSuchChildError)
|
|
# create a placeholder which will see POST t=upload
|
|
return PlaceHolderNodeHandler(self.client, self.node, name)
|
|
else:
|
|
node = node_or_failure
|
|
return make_handler_for(node, self.client, self.node, name)
|
|
d.addBoth(_maybe_got_node)
|
|
# now we have a placeholder or a filenodehandler, and we can just
|
|
# delegate to it. We could return the resource back out of
|
|
# DirectoryNodeHandler.renderHTTP, and nevow would recurse into it,
|
|
# but the addCallback() that handles when_done= would break.
|
|
d.addCallback(lambda child: child.render(req))
|
|
return d
|
|
|
|
def _POST_uri(self, req):
|
|
childcap = get_arg(req, "uri")
|
|
if not childcap:
|
|
raise WebError("set-uri requires a uri")
|
|
name = get_arg(req, "name")
|
|
if not name:
|
|
raise WebError("set-uri requires a name")
|
|
charset = get_arg(req, "_charset", "utf-8")
|
|
name = name.decode(charset)
|
|
replace = parse_replace_arg(get_arg(req, "replace", "true"))
|
|
|
|
# We mustn't pass childcap for the readcap argument because we don't
|
|
# know whether it is a read cap. Passing a read cap as the writecap
|
|
# argument will work (it ends up calling NodeMaker.create_from_cap,
|
|
# which derives a readcap if necessary and possible).
|
|
d = self.node.set_uri(name, childcap, None, overwrite=replace)
|
|
d.addCallback(lambda res: childcap)
|
|
return d
|
|
|
|
def _POST_unlink(self, req):
|
|
name = get_arg(req, "name")
|
|
if name is None:
|
|
# apparently an <input type="hidden" name="name" value="">
|
|
# won't show up in the resulting encoded form.. the 'name'
|
|
# field is completely missing. So to allow unlinking of a
|
|
# child with a name that is the empty string, we have to
|
|
# pretend that None means ''. The only downside of this is
|
|
# a slightly confusing error message if someone does a POST
|
|
# without a name= field. For our own HTML this isn't a big
|
|
# deal, because we create the 'unlink' POST buttons ourselves.
|
|
name = ''
|
|
charset = get_arg(req, "_charset", "utf-8")
|
|
name = name.decode(charset)
|
|
d = self.node.delete(name)
|
|
d.addCallback(lambda res: "thing unlinked")
|
|
return d
|
|
|
|
def _POST_rename(self, req):
|
|
# rename is identical to relink, but to_dir is not allowed
|
|
# and to_name is required.
|
|
if get_arg(req, "to_dir") is not None:
|
|
raise WebError("to_dir= is not valid for rename")
|
|
if get_arg(req, "to_name") is None:
|
|
raise WebError("to_name= is required for rename")
|
|
return self._POST_relink(req)
|
|
|
|
def _POST_relink(self, req):
|
|
charset = get_arg(req, "_charset", "utf-8")
|
|
replace = parse_replace_arg(get_arg(req, "replace", "true"))
|
|
|
|
from_name = get_arg(req, "from_name")
|
|
if from_name is not None:
|
|
from_name = from_name.strip()
|
|
from_name = from_name.decode(charset)
|
|
assert isinstance(from_name, unicode)
|
|
else:
|
|
raise WebError("from_name= is required")
|
|
|
|
to_name = get_arg(req, "to_name")
|
|
if to_name is not None:
|
|
to_name = to_name.strip()
|
|
to_name = to_name.decode(charset)
|
|
assert isinstance(to_name, unicode)
|
|
else:
|
|
to_name = from_name
|
|
|
|
# Disallow slashes in both from_name and to_name, that would only
|
|
# cause confusion.
|
|
if "/" in from_name:
|
|
raise WebError("from_name= may not contain a slash",
|
|
http.BAD_REQUEST)
|
|
if "/" in to_name:
|
|
raise WebError("to_name= may not contain a slash",
|
|
http.BAD_REQUEST)
|
|
|
|
to_dir = get_arg(req, "to_dir")
|
|
if to_dir is not None and to_dir != self.node.get_write_uri():
|
|
to_dir = to_dir.strip()
|
|
to_dir = to_dir.decode(charset)
|
|
assert isinstance(to_dir, unicode)
|
|
to_path = to_dir.split(u"/")
|
|
to_root = self.client.nodemaker.create_from_cap(to_str(to_path[0]))
|
|
if not IDirectoryNode.providedBy(to_root):
|
|
raise WebError("to_dir is not a directory", http.BAD_REQUEST)
|
|
d = to_root.get_child_at_path(to_path[1:])
|
|
else:
|
|
d = defer.succeed(self.node)
|
|
|
|
def _got_new_parent(new_parent):
|
|
if not IDirectoryNode.providedBy(new_parent):
|
|
raise WebError("to_dir is not a directory", http.BAD_REQUEST)
|
|
|
|
return self.node.move_child_to(from_name, new_parent,
|
|
to_name, replace)
|
|
d.addCallback(_got_new_parent)
|
|
d.addCallback(lambda res: "thing moved")
|
|
return d
|
|
|
|
def _maybe_literal(self, res, Results_Class):
|
|
if res:
|
|
return Results_Class(self.client, res)
|
|
return LiteralCheckResultsRenderer(self.client)
|
|
|
|
def _POST_check(self, req):
|
|
# check this directory
|
|
verify = boolean_of_arg(get_arg(req, "verify", "false"))
|
|
repair = boolean_of_arg(get_arg(req, "repair", "false"))
|
|
add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
|
|
if repair:
|
|
d = self.node.check_and_repair(Monitor(), verify, add_lease)
|
|
d.addCallback(self._maybe_literal, CheckAndRepairResultsRenderer)
|
|
else:
|
|
d = self.node.check(Monitor(), verify, add_lease)
|
|
d.addCallback(self._maybe_literal, CheckResultsRenderer)
|
|
return d
|
|
|
|
def _start_operation(self, monitor, renderer, ctx):
|
|
self._operations.add_monitor(ctx, monitor, renderer)
|
|
return self._operations.redirect_to(ctx)
|
|
|
|
def _POST_start_deep_check(self, req):
|
|
# check this directory and everything reachable from it
|
|
if not get_arg(req, "ophandle"):
|
|
raise NeedOperationHandleError("slow operation requires ophandle=")
|
|
verify = boolean_of_arg(get_arg(req, "verify", "false"))
|
|
repair = boolean_of_arg(get_arg(req, "repair", "false"))
|
|
add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
|
|
if repair:
|
|
monitor = self.node.start_deep_check_and_repair(verify, add_lease)
|
|
renderer = DeepCheckAndRepairResultsRenderer(self.client, monitor)
|
|
else:
|
|
monitor = self.node.start_deep_check(verify, add_lease)
|
|
renderer = DeepCheckResultsRenderer(self.client, monitor)
|
|
return self._start_operation(monitor, renderer, req)
|
|
|
|
def _POST_stream_deep_check(self, req):
|
|
verify = boolean_of_arg(get_arg(req, "verify", "false"))
|
|
repair = boolean_of_arg(get_arg(req, "repair", "false"))
|
|
add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
|
|
walker = DeepCheckStreamer(req, self.node, verify, repair, add_lease)
|
|
monitor = self.node.deep_traverse(walker)
|
|
walker.setMonitor(monitor)
|
|
# register to hear stopProducing. The walker ignores pauseProducing.
|
|
req.registerProducer(walker, True)
|
|
d = monitor.when_done()
|
|
def _done(res):
|
|
req.unregisterProducer()
|
|
return res
|
|
d.addBoth(_done)
|
|
def _cancelled(f):
|
|
f.trap(OperationCancelledError)
|
|
return "Operation Cancelled"
|
|
d.addErrback(_cancelled)
|
|
def _error(f):
|
|
# signal the error as a non-JSON "ERROR:" line, plus exception
|
|
msg = "ERROR: %s(%s)\n" % (f.value.__class__.__name__,
|
|
", ".join([str(a) for a in f.value.args]))
|
|
msg += str(f)
|
|
return msg
|
|
d.addErrback(_error)
|
|
return d
|
|
|
|
def _POST_start_manifest(self, req):
|
|
if not get_arg(req, "ophandle"):
|
|
raise NeedOperationHandleError("slow operation requires ophandle=")
|
|
monitor = self.node.build_manifest()
|
|
renderer = ManifestResults(self.client, monitor)
|
|
return self._start_operation(monitor, renderer, req)
|
|
|
|
def _POST_start_deep_size(self, req):
|
|
if not get_arg(req, "ophandle"):
|
|
raise NeedOperationHandleError("slow operation requires ophandle=")
|
|
monitor = self.node.start_deep_stats()
|
|
renderer = DeepSizeResults(self.client, monitor)
|
|
return self._start_operation(monitor, renderer, req)
|
|
|
|
def _POST_start_deep_stats(self, req):
|
|
if not get_arg(req, "ophandle"):
|
|
raise NeedOperationHandleError("slow operation requires ophandle=")
|
|
monitor = self.node.start_deep_stats()
|
|
renderer = DeepStatsResults(self.client, monitor)
|
|
return self._start_operation(monitor, renderer, req)
|
|
|
|
def _POST_stream_manifest(self, req):
|
|
walker = ManifestStreamer(req, self.node)
|
|
monitor = self.node.deep_traverse(walker)
|
|
walker.setMonitor(monitor)
|
|
# register to hear stopProducing. The walker ignores pauseProducing.
|
|
req.registerProducer(walker, True)
|
|
d = monitor.when_done()
|
|
def _done(res):
|
|
req.unregisterProducer()
|
|
return res
|
|
d.addBoth(_done)
|
|
def _cancelled(f):
|
|
f.trap(OperationCancelledError)
|
|
return "Operation Cancelled"
|
|
d.addErrback(_cancelled)
|
|
def _error(f):
|
|
# signal the error as a non-JSON "ERROR:" line, plus exception
|
|
msg = "ERROR: %s(%s)\n" % (f.value.__class__.__name__,
|
|
", ".join([str(a) for a in f.value.args]))
|
|
msg += str(f)
|
|
return msg
|
|
d.addErrback(_error)
|
|
return d
|
|
|
|
def _POST_set_children(self, req):
|
|
replace = parse_replace_arg(get_arg(req, "replace", "true"))
|
|
req.content.seek(0)
|
|
body = req.content.read()
|
|
try:
|
|
children = json.loads(body)
|
|
except ValueError as le:
|
|
le.args = tuple(le.args + (body,))
|
|
# TODO test handling of bad JSON
|
|
raise
|
|
cs = {}
|
|
for name, (file_or_dir, mddict) in children.iteritems():
|
|
name = unicode(name) # json returns str *or* unicode
|
|
writecap = mddict.get('rw_uri')
|
|
if writecap is not None:
|
|
writecap = str(writecap)
|
|
readcap = mddict.get('ro_uri')
|
|
if readcap is not None:
|
|
readcap = str(readcap)
|
|
cs[name] = (writecap, readcap, mddict.get('metadata'))
|
|
d = self.node.set_children(cs, replace)
|
|
d.addCallback(lambda res: "Okay so I did it.")
|
|
# TODO: results
|
|
return d
|
|
|
|
def abbreviated_dirnode(dirnode):
|
|
u = from_string_dirnode(dirnode.get_uri())
|
|
return u.abbrev_si()
|
|
|
|
SPACE = u"\u00A0"*2
|
|
|
|
class DirectoryAsHTML(Element):
|
|
# The remainder of this class is to render the directory into
|
|
# human+browser -oriented HTML.
|
|
loader = XMLFile(FilePath(__file__).sibling("directory.xhtml"))
|
|
|
|
def __init__(self, node, default_mutable_format):
|
|
super(DirectoryAsHTML, self).__init__()
|
|
self.node = node
|
|
if default_mutable_format not in (MDMF_VERSION, SDMF_VERSION):
|
|
raise ValueError(
|
|
"Uknown mutable format '{}'".format(default_mutable_format)
|
|
)
|
|
self.default_mutable_format = default_mutable_format
|
|
|
|
@defer.inlineCallbacks
|
|
def render(self, request):
|
|
"""
|
|
Override Element.render .. we have async work to do before we flatten our template
|
|
"""
|
|
# this could be improved; doing a more-straightforward port
|
|
# here, which used to be in "beforeRender" .. so lots of the
|
|
# renderers don't yield/wait properly, they expect the self.*
|
|
# state that is set up here to "just be there" :/
|
|
yield self._get_children(request)
|
|
template = Element.render(self, request)
|
|
defer.returnValue(template)
|
|
|
|
@defer.inlineCallbacks
|
|
def _get_children(self, req):
|
|
# attempt to get the dirnode's children, stashing them (or the
|
|
# failure that results) for later use
|
|
try:
|
|
children = yield self.node.list()
|
|
except Exception as e:
|
|
text, code = humanize_failure(Failure(e))
|
|
children = None
|
|
self.dirnode_children_error = text
|
|
|
|
self.dirnode_children = children
|
|
defer.returnValue(self.dirnode_children)
|
|
|
|
@renderer
|
|
def children(self, req, tag):
|
|
return SlotsSequenceElement(
|
|
tag,
|
|
[
|
|
self._child_slots(req, fname, data[0], data[1])
|
|
for fname, data in self.dirnode_children.items()
|
|
]
|
|
)
|
|
|
|
@renderer
|
|
def title(self, req, tag):
|
|
si_s = abbreviated_dirnode(self.node)
|
|
header = ["Tahoe-LAFS - Directory SI=%s" % si_s]
|
|
if self.node.is_unknown():
|
|
header.append(" (unknown)")
|
|
elif not self.node.is_mutable():
|
|
header.append(" (immutable)")
|
|
elif self.node.is_readonly():
|
|
header.append(" (read-only)")
|
|
else:
|
|
header.append(" (modifiable)")
|
|
return tag(header)
|
|
|
|
@renderer
|
|
def header(self, req, tag):
|
|
si_s = abbreviated_dirnode(self.node)
|
|
header = ["Tahoe-LAFS Directory SI=", tags.span(si_s, class_="data-chars")]
|
|
if self.node.is_unknown():
|
|
header.append(" (unknown)")
|
|
elif not self.node.is_mutable():
|
|
header.append(" (immutable)")
|
|
elif self.node.is_readonly():
|
|
header.append(" (read-only)")
|
|
return tag(header)
|
|
|
|
@renderer
|
|
def welcome(self, req, tag):
|
|
link = get_root(req)
|
|
return tag(tags.a("Return to Welcome page", href=link))
|
|
|
|
@renderer
|
|
def show_readonly(self, req, tag):
|
|
if self.node.is_unknown() or self.node.is_readonly():
|
|
return ""
|
|
rocap = self.node.get_readonly_uri()
|
|
root = get_root(req)
|
|
uri_link = "%s/uri/%s/" % (root, urllib.quote(rocap))
|
|
return tag(tags.a("Read-Only Version", href=uri_link))
|
|
|
|
@renderer
|
|
def try_children(self, req, tag):
|
|
# if the dirnode can be retrived, render a table of children.
|
|
# Otherwise, render an apologetic error message.
|
|
if self.dirnode_children is not None:
|
|
return tag
|
|
else:
|
|
return tags.div(
|
|
tags.p("Error reading directory:"),
|
|
tags.p(self.dirnode_children_error),
|
|
)
|
|
|
|
def _child_slots(self, req, name, target, metadata):
|
|
"""
|
|
:returns: a dict of key/values to give to each item in the table
|
|
of sub-items that directory.xhtml defines (this method is
|
|
called by the 'children' renderer)
|
|
"""
|
|
name = name.encode("utf-8")
|
|
nameurl = urllib.quote(name, safe="") # encode any slashes too
|
|
|
|
root = get_root(req)
|
|
here = "{}/uri/{}/".format(root, urllib.quote(self.node.get_uri()))
|
|
if self.node.is_unknown() or self.node.is_readonly():
|
|
unlink = "-"
|
|
rename = "-"
|
|
else:
|
|
# this creates a button which will cause our _POST_unlink method
|
|
# to be invoked, which unlinks the file and then redirects the
|
|
# browser back to this directory
|
|
unlink = tags.form(
|
|
[
|
|
tags.input(type='hidden', name='t', value='unlink'),
|
|
tags.input(type='hidden', name='name', value=name),
|
|
tags.input(type='hidden', name='when_done', value="."),
|
|
tags.input(type='submit', class_='btn', value='unlink', name="unlink"),
|
|
],
|
|
action=here, method="post"
|
|
)
|
|
rename = tags.form(
|
|
[
|
|
tags.input(type='hidden', name='t', value='rename-form'),
|
|
tags.input(type='hidden', name='name', value=name),
|
|
tags.input(type='hidden', name='when_done', value="."),
|
|
tags.input(type='submit', class_='btn', value='rename/relink', name="rename"),
|
|
],
|
|
action=here, method="get",
|
|
)
|
|
|
|
slots = {
|
|
"unlink": unlink,
|
|
"rename": rename,
|
|
}
|
|
|
|
times = []
|
|
linkcrtime = metadata.get('tahoe', {}).get("linkcrtime")
|
|
if linkcrtime is not None:
|
|
times.append("lcr: " + render_time(linkcrtime))
|
|
else:
|
|
# For backwards-compatibility with links last modified by Tahoe < 1.4.0:
|
|
if "ctime" in metadata:
|
|
ctime = render_time(metadata["ctime"])
|
|
times.append("c: " + ctime)
|
|
linkmotime = metadata.get('tahoe', {}).get("linkmotime")
|
|
if linkmotime is not None:
|
|
if times:
|
|
times.append(tags.br())
|
|
times.append("lmo: " + render_time(linkmotime))
|
|
else:
|
|
# For backwards-compatibility with links last modified by Tahoe < 1.4.0:
|
|
if "mtime" in metadata:
|
|
mtime = render_time(metadata["mtime"])
|
|
if times:
|
|
times.append(tags.br())
|
|
times.append("m: " + mtime)
|
|
slots["times"] = times
|
|
|
|
assert IFilesystemNode.providedBy(target), target
|
|
target_uri = target.get_uri() or ""
|
|
quoted_uri = urllib.quote(target_uri, safe="") # escape slashes too
|
|
|
|
if IMutableFileNode.providedBy(target):
|
|
# to prevent javascript in displayed .html files from stealing a
|
|
# secret directory URI from the URL, send the browser to a URI-based
|
|
# page that doesn't know about the directory at all
|
|
dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, nameurl)
|
|
slots["filename"] = tags.a(name, href=dlurl, rel="noreferrer")
|
|
slots["type"] = "SSK"
|
|
slots["size"] = "?"
|
|
info_link = "{}/uri/{}?t=info".format(root, quoted_uri)
|
|
|
|
elif IImmutableFileNode.providedBy(target):
|
|
dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, nameurl)
|
|
slots["filename"] = tags.a(name, href=dlurl, rel="noreferrer")
|
|
slots["type"] = "FILE"
|
|
slots["size"] = str(target.get_size())
|
|
info_link = "{}/uri/{}?t=info".format(root, quoted_uri)
|
|
|
|
elif IDirectoryNode.providedBy(target):
|
|
# directory
|
|
uri_link = "%s/uri/%s/" % (root, urllib.quote(target_uri))
|
|
slots["filename"] = tags.a(name, href=uri_link)
|
|
if not target.is_mutable():
|
|
dirtype = "DIR-IMM"
|
|
elif target.is_readonly():
|
|
dirtype = "DIR-RO"
|
|
else:
|
|
dirtype = "DIR"
|
|
slots["type"] = dirtype
|
|
slots["size"] = "-"
|
|
info_link = "%s/uri/%s/?t=info" % (root, quoted_uri)
|
|
|
|
elif isinstance(target, ProhibitedNode):
|
|
if IDirectoryNode.providedBy(target.wrapped_node):
|
|
blacklisted_type = "DIR-BLACKLISTED"
|
|
else:
|
|
blacklisted_type = "BLACKLISTED"
|
|
slots["type"] = blacklisted_type
|
|
slots["size"] = "-"
|
|
slots["info"] = ["Access Prohibited:", tags.br, target.reason]
|
|
slots["filename"] = tags.strike(name)
|
|
info_link = None
|
|
|
|
else:
|
|
# unknown
|
|
if target.get_write_uri() is not None:
|
|
unknowntype = "?"
|
|
elif not self.node.is_mutable() or target.is_alleged_immutable():
|
|
unknowntype = "?-IMM"
|
|
else:
|
|
unknowntype = "?-RO"
|
|
slots["filename"] = name
|
|
slots["type"] = unknowntype
|
|
slots["size"] = "-"
|
|
# use a directory-relative info link, so we can extract both the
|
|
# writecap and the readcap
|
|
info_link = "%s?t=info" % urllib.quote(name)
|
|
|
|
if info_link:
|
|
slots["info"] = tags.a("More Info", href=info_link)
|
|
|
|
return slots
|
|
|
|
# XXX: similar to render_upload_form and render_mkdir_form in root.py.
|
|
# XXX: also, is generating so much HTML in code a great idea? -> templates?
|
|
@renderer
|
|
def forms(self, req, data):
|
|
forms = []
|
|
|
|
# making this consistent with the other forms, and also
|
|
# because action="." doesn't get us back to the dir page (but
|
|
# instead /uri itself)
|
|
root = get_root(req)
|
|
here = "{}/uri/{}/".format(root, urllib.quote(self.node.get_uri()))
|
|
|
|
if self.node.is_readonly():
|
|
return tags.div("No upload forms: directory is read-only")
|
|
if self.dirnode_children is None:
|
|
return tags.div("No upload forms: directory is unreadable")
|
|
|
|
mkdir_sdmf = tags.input(
|
|
type='radio',
|
|
name='format',
|
|
value='sdmf',
|
|
id='mkdir-sdmf',
|
|
checked='checked',
|
|
)
|
|
mkdir_mdmf = tags.input(
|
|
type='radio',
|
|
name='format',
|
|
value='mdmf',
|
|
id='mkdir-mdmf',
|
|
)
|
|
|
|
mkdir_form = tags.form(
|
|
[
|
|
tags.fieldset([
|
|
tags.input(type="hidden", name="t", value="mkdir"),
|
|
tags.input(type="hidden", name="when_done", value="."),
|
|
tags.legend("Create a new directory in this directory", class_="freeform-form-label"),
|
|
"New directory name:"+SPACE, tags.br,
|
|
tags.input(type="text", name="name"), SPACE,
|
|
tags.div(
|
|
[
|
|
mkdir_sdmf, tags.label(SPACE, "SDMF", for_='mutable-directory-sdmf'), SPACE*2,
|
|
mkdir_mdmf, tags.label(SPACE, "MDMF (experimental)", for_='mutable-directory-mdmf'),
|
|
],
|
|
class_="form-inline",
|
|
),
|
|
tags.input(
|
|
type="submit",
|
|
class_="btn",
|
|
value="Create",
|
|
),
|
|
])
|
|
],
|
|
action=here,
|
|
method="post",
|
|
enctype="multipart/form-data",
|
|
)
|
|
forms.append(tags.div(mkdir_form, class_="freeform-form"))
|
|
|
|
upload_chk = tags.input(
|
|
type='radio',
|
|
name='format',
|
|
value='chk',
|
|
id='upload-chk',
|
|
checked='checked',
|
|
)
|
|
upload_sdmf = tags.input(
|
|
type='radio',
|
|
name='format',
|
|
value='sdmf',
|
|
id='upload-sdmf',
|
|
)
|
|
upload_mdmf = tags.input(
|
|
type='radio',
|
|
name='format',
|
|
value='mdmf',
|
|
id='upload-mdmf',
|
|
)
|
|
|
|
upload_form = tags.form(
|
|
tags.fieldset([
|
|
tags.input(type="hidden", name="t", value="upload"),
|
|
tags.input(type="hidden", name="when_done", value=req.uri),
|
|
tags.legend("Upload a file to this directory", class_="freeform-form-label"),
|
|
"Choose a file to upload:"+SPACE,
|
|
tags.input(type="file", name="file", class_="freeform-input-file"), SPACE,
|
|
tags.div([
|
|
upload_chk, tags.label(SPACE, "Immutable", for_="upload-chk"), SPACE*2,
|
|
upload_sdmf, tags.label(SPACE, "SDMF", for_="upload-sdmf"), SPACE*2,
|
|
upload_mdmf, tags.label(SPACE, "MDMF (experimental)", for_="upload-mdmf"),
|
|
], class_="form-inline"),
|
|
tags.input(type="submit", class_="btn", value="Upload"), SPACE*2,
|
|
]),
|
|
action=req.uri,
|
|
method="post",
|
|
enctype="multipart/form-data",
|
|
)
|
|
forms.append(tags.div(upload_form, class_="freeform-form"))
|
|
|
|
attach_form = tags.form(
|
|
tags.fieldset(
|
|
tags.div([
|
|
tags.input(type="hidden", name="t", value="uri"),
|
|
tags.input(type="hidden", name="when_done", value="."),
|
|
tags.legend("Add a link to a file or directory which is already in Tahoe-LAFS.", class_="freeform-form-label"),
|
|
"New child name:"+SPACE,
|
|
tags.input(type="text", name="name"), SPACE*2, tags.br,
|
|
"URI of new child:"+SPACE,
|
|
tags.input(type="text", name="uri"), SPACE,
|
|
tags.input(type="submit", class_="btn", value="Attach"),
|
|
], class_="form-inline"),
|
|
),
|
|
action=here,
|
|
method="post",
|
|
enctype="multipart/form-data",
|
|
)
|
|
forms.append(tags.div(attach_form, class_="freeform-form"))
|
|
return forms
|
|
|
|
@renderer
|
|
def results(self, req, tag):
|
|
return get_arg(req, "results", "")
|
|
|
|
def _directory_json_metadata(req, dirnode):
|
|
d = dirnode.list()
|
|
def _got(children):
|
|
kids = {}
|
|
for name, (childnode, metadata) in children.iteritems():
|
|
assert IFilesystemNode.providedBy(childnode), childnode
|
|
rw_uri = childnode.get_write_uri()
|
|
ro_uri = childnode.get_readonly_uri()
|
|
if IFileNode.providedBy(childnode):
|
|
kiddata = ("filenode", get_filenode_metadata(childnode))
|
|
elif IDirectoryNode.providedBy(childnode):
|
|
kiddata = ("dirnode", {'mutable': childnode.is_mutable()})
|
|
else:
|
|
kiddata = ("unknown", {})
|
|
|
|
kiddata[1]["metadata"] = metadata
|
|
if rw_uri:
|
|
kiddata[1]["rw_uri"] = rw_uri
|
|
if ro_uri:
|
|
kiddata[1]["ro_uri"] = ro_uri
|
|
verifycap = childnode.get_verify_cap()
|
|
if verifycap:
|
|
kiddata[1]['verify_uri'] = verifycap.to_string()
|
|
|
|
kids[name] = kiddata
|
|
|
|
drw_uri = dirnode.get_write_uri()
|
|
dro_uri = dirnode.get_readonly_uri()
|
|
contents = { 'children': kids }
|
|
if dro_uri:
|
|
contents['ro_uri'] = dro_uri
|
|
if drw_uri:
|
|
contents['rw_uri'] = drw_uri
|
|
verifycap = dirnode.get_verify_cap()
|
|
if verifycap:
|
|
contents['verify_uri'] = verifycap.to_string()
|
|
contents['mutable'] = dirnode.is_mutable()
|
|
data = ("dirnode", contents)
|
|
return json.dumps(data, indent=1) + "\n"
|
|
d.addCallback(_got)
|
|
d.addCallback(text_plain, req)
|
|
|
|
def error(f):
|
|
message, code = humanize_failure(f)
|
|
req.setResponseCode(code)
|
|
return json.dumps({
|
|
"error": message,
|
|
})
|
|
d.addErrback(error)
|
|
return d
|
|
|
|
|
|
def _directory_uri(req, dirnode):
|
|
return text_plain(dirnode.get_uri(), req)
|
|
|
|
def _directory_readonly_uri(req, dirnode):
|
|
return text_plain(dirnode.get_readonly_uri(), req)
|
|
|
|
class RenameForm(Element, object):
|
|
|
|
loader = XMLFile(FilePath(__file__).sibling("rename-form.xhtml"))
|
|
|
|
def __init__(self, original):
|
|
self.original = original
|
|
super(RenameForm, self).__init__()
|
|
|
|
@renderer
|
|
def title(self, req, tag):
|
|
return tag("Directory SI={}".format(abbreviated_dirnode(self.original)))
|
|
|
|
@renderer
|
|
def header(self, req, tag):
|
|
header = [
|
|
"Rename "
|
|
"in directory SI=%s" % abbreviated_dirnode(self.original),
|
|
]
|
|
|
|
if self.original.is_readonly():
|
|
header.append(" (readonly!)")
|
|
header.append(":")
|
|
return tag(header)
|
|
|
|
@renderer
|
|
def when_done(self, req, tag):
|
|
return tags.input(type="hidden", name="when_done", value=".")
|
|
|
|
@renderer
|
|
def get_name(self, req, tag):
|
|
name = get_arg(req, "name", "")
|
|
tag.attributes['value'] = name
|
|
return tag
|
|
|
|
|
|
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
|
|
formatArgument = "output"
|
|
formatDefault = "html"
|
|
|
|
def __init__(self, client, monitor):
|
|
self.client = client
|
|
self.monitor = monitor
|
|
|
|
def render_HTML(self, req):
|
|
return renderElement(
|
|
req,
|
|
ManifestElement(self.monitor)
|
|
)
|
|
|
|
def render_TEXT(self, req):
|
|
req.setHeader("content-type", "text/plain")
|
|
lines = []
|
|
is_finished = self.monitor.is_finished()
|
|
lines.append("finished: " + {True: "yes", False: "no"}[is_finished])
|
|
for path, cap in self.monitor.get_status()["manifest"]:
|
|
lines.append(_slashify_path(path) + " " + cap)
|
|
return "\n".join(lines) + "\n"
|
|
|
|
def render_JSON(self, req):
|
|
req.setHeader("content-type", "text/plain")
|
|
m = self.monitor
|
|
s = m.get_status()
|
|
|
|
if m.origin_si:
|
|
origin_base32 = base32.b2a(m.origin_si)
|
|
else:
|
|
origin_base32 = ""
|
|
status = { "stats": s["stats"],
|
|
"finished": m.is_finished(),
|
|
"origin": origin_base32,
|
|
}
|
|
if m.is_finished():
|
|
# don't return manifest/verifycaps/SIs unless the operation is
|
|
# done, to save on CPU/memory (both here and in the HTTP client
|
|
# who has to unpack the JSON). Tests show that the ManifestWalker
|
|
# needs about 1092 bytes per item, the JSON we generate here
|
|
# requires about 503 bytes per item, and some internal overhead
|
|
# (perhaps transport-layer buffers in twisted.web?) requires an
|
|
# additional 1047 bytes per item.
|
|
status.update({ "manifest": s["manifest"],
|
|
"verifycaps": [i for i in s["verifycaps"]],
|
|
"storage-index": [i for i in s["storage-index"]],
|
|
})
|
|
# simplejson doesn't know how to serialize a set. We use a
|
|
# generator that walks the set rather than list(setofthing) to
|
|
# save a small amount of memory (4B*len) and a moderate amount of
|
|
# CPU.
|
|
return json.dumps(status, indent=1)
|
|
|
|
|
|
class DeepSizeResults(MultiFormatPage):
|
|
# Control MultiFormatPage
|
|
formatArgument = "output"
|
|
formatDefault = "html"
|
|
|
|
def __init__(self, client, monitor):
|
|
self.client = client
|
|
self.monitor = monitor
|
|
|
|
def render_HTML(self, req):
|
|
is_finished = self.monitor.is_finished()
|
|
output = "finished: " + {True: "yes", False: "no"}[is_finished] + "\n"
|
|
if is_finished:
|
|
stats = self.monitor.get_status()
|
|
total = (stats.get("size-immutable-files", 0)
|
|
+ stats.get("size-mutable-files", 0)
|
|
+ stats.get("size-directories", 0))
|
|
output += "size: %d\n" % total
|
|
return output
|
|
render_TEXT = render_HTML
|
|
|
|
def render_JSON(self, req):
|
|
req.setHeader("content-type", "text/plain")
|
|
status = {"finished": self.monitor.is_finished(),
|
|
"size": self.monitor.get_status(),
|
|
}
|
|
return json.dumps(status)
|
|
|
|
|
|
class DeepStatsResults(Resource, object):
|
|
"""
|
|
Renders the results of a 'deep-stats' operation on a directory
|
|
capability.
|
|
"""
|
|
def __init__(self, client, monitor):
|
|
self.client = client
|
|
self.monitor = monitor
|
|
|
|
def render(self, req):
|
|
# JSON only
|
|
req.setHeader("content-type", "text/plain")
|
|
s = self.monitor.get_status().copy()
|
|
s["finished"] = self.monitor.is_finished()
|
|
return json.dumps(s, indent=1)
|
|
|
|
|
|
@implementer(IPushProducer)
|
|
class ManifestStreamer(dirnode.DeepStats):
|
|
|
|
def __init__(self, req, origin):
|
|
dirnode.DeepStats.__init__(self, origin)
|
|
self.req = req
|
|
|
|
def setMonitor(self, monitor):
|
|
self.monitor = monitor
|
|
def pauseProducing(self):
|
|
pass
|
|
def resumeProducing(self):
|
|
pass
|
|
def stopProducing(self):
|
|
self.monitor.cancel()
|
|
|
|
def add_node(self, node, path):
|
|
dirnode.DeepStats.add_node(self, node, path)
|
|
d = {"path": path,
|
|
"cap": node.get_uri()}
|
|
|
|
if IDirectoryNode.providedBy(node):
|
|
d["type"] = "directory"
|
|
elif IFileNode.providedBy(node):
|
|
d["type"] = "file"
|
|
else:
|
|
d["type"] = "unknown"
|
|
|
|
v = node.get_verify_cap()
|
|
if v:
|
|
v = v.to_string()
|
|
d["verifycap"] = v or ""
|
|
|
|
r = node.get_repair_cap()
|
|
if r:
|
|
r = r.to_string()
|
|
d["repaircap"] = r or ""
|
|
|
|
si = node.get_storage_index()
|
|
if si:
|
|
si = base32.b2a(si)
|
|
d["storage-index"] = si or ""
|
|
|
|
j = json.dumps(d, ensure_ascii=True)
|
|
assert "\n" not in j
|
|
self.req.write(j+"\n")
|
|
|
|
def finish(self):
|
|
stats = dirnode.DeepStats.get_results(self)
|
|
d = {"type": "stats",
|
|
"stats": stats,
|
|
}
|
|
j = json.dumps(d, ensure_ascii=True)
|
|
assert "\n" not in j
|
|
self.req.write(j+"\n")
|
|
return ""
|
|
|
|
@implementer(IPushProducer)
|
|
class DeepCheckStreamer(dirnode.DeepStats):
|
|
|
|
def __init__(self, req, origin, verify, repair, add_lease):
|
|
dirnode.DeepStats.__init__(self, origin)
|
|
self.req = req
|
|
self.verify = verify
|
|
self.repair = repair
|
|
self.add_lease = add_lease
|
|
|
|
def setMonitor(self, monitor):
|
|
self.monitor = monitor
|
|
def pauseProducing(self):
|
|
pass
|
|
def resumeProducing(self):
|
|
pass
|
|
def stopProducing(self):
|
|
self.monitor.cancel()
|
|
|
|
def add_node(self, node, path):
|
|
dirnode.DeepStats.add_node(self, node, path)
|
|
data = {"path": path,
|
|
"cap": node.get_uri()}
|
|
|
|
if IDirectoryNode.providedBy(node):
|
|
data["type"] = "directory"
|
|
elif IFileNode.providedBy(node):
|
|
data["type"] = "file"
|
|
else:
|
|
data["type"] = "unknown"
|
|
|
|
v = node.get_verify_cap()
|
|
if v:
|
|
v = v.to_string()
|
|
data["verifycap"] = v or ""
|
|
|
|
r = node.get_repair_cap()
|
|
if r:
|
|
r = r.to_string()
|
|
data["repaircap"] = r or ""
|
|
|
|
si = node.get_storage_index()
|
|
if si:
|
|
si = base32.b2a(si)
|
|
data["storage-index"] = si or ""
|
|
|
|
if self.repair:
|
|
d = node.check_and_repair(self.monitor, self.verify, self.add_lease)
|
|
d.addCallback(self.add_check_and_repair, data)
|
|
else:
|
|
d = node.check(self.monitor, self.verify, self.add_lease)
|
|
d.addCallback(self.add_check, data)
|
|
d.addCallback(self.write_line)
|
|
return d
|
|
|
|
def add_check_and_repair(self, crr, data):
|
|
data["check-and-repair-results"] = json_check_and_repair_results(crr)
|
|
return data
|
|
|
|
def add_check(self, cr, data):
|
|
data["check-results"] = json_check_results(cr)
|
|
return data
|
|
|
|
def write_line(self, data):
|
|
j = json.dumps(data, ensure_ascii=True)
|
|
assert "\n" not in j
|
|
self.req.write(j+"\n")
|
|
|
|
def finish(self):
|
|
stats = dirnode.DeepStats.get_results(self)
|
|
d = {"type": "stats",
|
|
"stats": stats,
|
|
}
|
|
j = json.dumps(d, ensure_ascii=True)
|
|
assert "\n" not in j
|
|
self.req.write(j+"\n")
|
|
return ""
|
|
|
|
|
|
class UnknownNodeHandler(Resource, object):
|
|
def __init__(self, client, node, parentnode=None, name=None):
|
|
super(UnknownNodeHandler, self).__init__()
|
|
assert node
|
|
self.node = node
|
|
self.parentnode = parentnode
|
|
self.name = name
|
|
|
|
def render_GET(self, req):
|
|
t = get_arg(req, "t", "").strip()
|
|
if t == "info":
|
|
return MoreInfo(self.node)
|
|
if t == "json":
|
|
is_parent_known_immutable = self.parentnode and not self.parentnode.is_mutable()
|
|
if self.parentnode and self.name:
|
|
d = self.parentnode.get_metadata_for(self.name)
|
|
else:
|
|
d = defer.succeed(None)
|
|
d.addCallback(lambda md: UnknownJSONMetadata(req, self.node, md, is_parent_known_immutable))
|
|
return d
|
|
raise WebError("GET unknown URI type: can only do t=info and t=json, not t=%s.\n"
|
|
"Using a webapi server that supports a later version of Tahoe "
|
|
"may help." % t)
|
|
|
|
def UnknownJSONMetadata(req, node, edge_metadata, is_parent_known_immutable):
|
|
rw_uri = node.get_write_uri()
|
|
ro_uri = node.get_readonly_uri()
|
|
data = ("unknown", {})
|
|
if ro_uri:
|
|
data[1]['ro_uri'] = ro_uri
|
|
if rw_uri:
|
|
data[1]['rw_uri'] = rw_uri
|
|
data[1]['mutable'] = True
|
|
elif is_parent_known_immutable or node.is_alleged_immutable():
|
|
data[1]['mutable'] = False
|
|
# else we don't know whether it is mutable.
|
|
|
|
if edge_metadata is not None:
|
|
data[1]['metadata'] = edge_metadata
|
|
return text_plain(json.dumps(data, indent=1) + "\n", req)
|