From: Brian Warner Date: Mon, 19 May 2008 19:57:04 +0000 (-0700) Subject: webish: complete rewrite, break into smaller pieces, auto-create directories, improve... X-Git-Tag: allmydata-tahoe-1.1.0~118 X-Git-Url: https://git.rkrishnan.org/tahoe_css?a=commitdiff_plain;h=f9cd30d9bca6db3da9885c5249124e2e87ae09c5;p=tahoe-lafs%2Ftahoe-lafs.git webish: complete rewrite, break into smaller pieces, auto-create directories, improve error handling --- diff --git a/src/allmydata/web/common.py b/src/allmydata/web/common.py index 000fda35..10599269 100644 --- a/src/allmydata/web/common.py +++ b/src/allmydata/web/common.py @@ -1,7 +1,10 @@ +from twisted.web import http, server from zope.interface import Interface -from nevow import loaders +from nevow import loaders, appserver +from nevow.inevow import IRequest from nevow.util import resource_filename +from allmydata.interfaces import ExistingChildError class IClient(Interface): pass @@ -11,6 +14,7 @@ def getxmlfile(name): return loaders.xmlfile(resource_filename('allmydata.web', '%s' % name)) def boolean_of_arg(arg): + # TODO: "" assert arg.lower() in ("true", "t", "1", "false", "f", "0", "on", "off") return arg.lower() in ("true", "t", "1", "on") @@ -68,3 +72,56 @@ def abbreviate_size(data): if r > 1000: return "%.1fkB" % (r/1000) return "%dB" % r + +def text_plain(text, ctx): + req = IRequest(ctx) + req.setHeader("content-type", "text/plain") + req.setHeader("content-length", len(text)) + return text + +class WebError(Exception): + def __init__(self, text, code=http.BAD_REQUEST): + self.text = text + self.code = code + +# XXX: to make UnsupportedMethod return 501 NOT_IMPLEMENTED instead of 500 +# Internal Server Error, we either need to do that ICanHandleException trick, +# or make sure that childFactory returns a WebErrorResource (and never an +# actual exception). The latter is growing increasingly annoying. + +def should_create_intermediate_directories(req): + t = get_arg(req, "t", "").strip() + return bool(req.method in ("PUT", "POST") and + t not in ("delete", "rename", "rename-form", "check")) + + +class MyExceptionHandler(appserver.DefaultExceptionHandler): + def simple(self, ctx, text, code=http.BAD_REQUEST): + req = IRequest(ctx) + req.setResponseCode(code) + req.setHeader("content-type", "text/plain;charset=utf-8") + if isinstance(text, unicode): + text = text.encode("utf-8") + req.write(text) + req.finishRequest(False) + + def renderHTTP_exception(self, ctx, f): + if f.check(ExistingChildError): + return self.simple(ctx, + "There was already a child by that " + "name, and you asked me to not " + "replace it.", + http.CONFLICT) + elif f.check(WebError): + return self.simple(ctx, f.value.text, f.value.code) + elif f.check(server.UnsupportedMethod): + # twisted.web.server.Request.render() has support for transforming + # this into an appropriate 501 NOT_IMPLEMENTED or 405 NOT_ALLOWED + # return code, but nevow does not. + req = IRequest(ctx) + method = req.method + return self.simple(ctx, + "I don't know how to treat a %s request." % method, + http.NOT_IMPLEMENTED) + super = appserver.DefaultExceptionHandler + return super.renderHTTP_exception(self, ctx, f) diff --git a/src/allmydata/web/directory.py b/src/allmydata/web/directory.py new file mode 100644 index 00000000..a16af78a --- /dev/null +++ b/src/allmydata/web/directory.py @@ -0,0 +1,740 @@ + +import simplejson +import urllib +import time + +from twisted.internet import defer +from twisted.python.failure import Failure +from twisted.web import http, html +from nevow import url, rend, tags as T +from nevow.inevow import IRequest + +from foolscap.eventual import fireEventually + +from allmydata.util import log, base32 +from allmydata.uri import from_string_verifier, from_string_dirnode, \ + CHKFileVerifierURI +from allmydata.interfaces import IDirectoryNode, IFileNode, IMutableFileNode, \ + ExistingChildError +from allmydata.web.common import text_plain, WebError, IClient, \ + boolean_of_arg, get_arg, should_create_intermediate_directories, \ + getxmlfile +from allmydata.web.filenode import ReplaceMeMixin, \ + FileNodeHandler, PlaceHolderNodeHandler + +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, parentnode=None, name=None): + if parentnode: + assert IDirectoryNode.providedBy(parentnode) + if IFileNode.providedBy(node): + return FileNodeHandler(node, parentnode, name) + if IMutableFileNode.providedBy(node): + return FileNodeHandler(node, parentnode, name) + if IDirectoryNode.providedBy(node): + return DirectoryNodeHandler(node, parentnode, name) + raise WebError("Cannot provide handler for '%s'" % node) + +class DirectoryNodeHandler(rend.Page, ReplaceMeMixin): + addSlash = True + + def __init__(self, node, parentnode=None, name=None): + rend.Page.__init__(self) + assert node + self.node = node + self.parentnode = parentnode + self.name = name + + def childFactory(self, ctx, name): + req = IRequest(ctx) + name = name.decode("utf-8") + d = self.node.get(name) + d.addBoth(self.got_child, ctx, name) + # got_child returns a handler resource: FileNodeHandler or + # DirectoryNodeHandler + return d + + def got_child(self, node_or_failure, ctx, name): + DEBUG = False + if DEBUG: print "GOT_CHILD", name, node_or_failure + req = IRequest(ctx) + method = req.method + nonterminal = len(req.postpath) > 1 + t = get_arg(req, "t", "").strip() + if isinstance(node_or_failure, Failure): + f = node_or_failure + f.trap(KeyError) + # No child by this name. What should we do about it? + if DEBUG: print "no child", name + if DEBUG: print "postpath", req.postpath + if nonterminal: + if DEBUG: print " intermediate" + if should_create_intermediate_directories(req): + # create intermediate directories + if DEBUG: print " making intermediate directory" + d = self.node.create_empty_directory(name) + d.addCallback(make_handler_for, self.node, name) + return d + else: + if DEBUG: print " terminal" + # terminal node + if (method,t) in [ ("POST","mkdir"), ("PUT","mkdir") ]: + if DEBUG: print " making final directory" + # final directory + d = self.node.create_empty_directory(name) + d.addCallback(make_handler_for, self.node, name) + return d + if (method,t) in ( ("PUT",""), ("PUT","uri"), ): + if DEBUG: print " PUT, making leaf placeholder" + # 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.node, name) + if DEBUG: print " 404" + # otherwise, we just return a no-such-child error + return rend.FourOhFour() + + 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. + if DEBUG: print "blocking" + raise WebError("Unable to create directory '%s': " + "a file was in the way" % name, + http.CONFLICT) + if DEBUG: print "good child" + return make_handler_for(node, self.node, name) + + def renderHTTP(self, ctx): + # This is where all of the ?t=* actions are implemented. + request = IRequest(ctx) + + # if we were using regular twisted.web Resources (and the regular + # twisted.web.server.Request object) then we could implement + # render_PUT and render_GET. But Nevow's request handler + # (NevowRequest.gotPageContext) goes directly to renderHTTP. Copy + # some code from the Resource.render method that Nevow bypasses, to + # do the same thing. + m = getattr(self, 'render_' + request.method, None) + if not m: + from twisted.web.server import UnsupportedMethod + raise UnsupportedMethod(getattr(self, 'allowedMethods', ())) + return m(ctx) + + def render_DELETE(self, ctx): + 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, ctx): + client = IClient(ctx) + req = IRequest(ctx) + # This is where all of the directory-related ?t=* code goes. + t = get_arg(req, "t", "").strip() + if not t: + # render the directory as HTML, using the docFactory and Nevow's + # whole templating thing. + return DirectoryAsHTML(self.node) + + if t == "json": + return DirectoryJSONMetadata(ctx, self.node) + if t == "uri": + return DirectoryURI(ctx, self.node) + if t == "readonly-uri": + return DirectoryReadonlyURI(ctx, self.node) + if t == "manifest": + return Manifest(self.node) + if t == "deep-size": + return DeepSize(ctx, self.node) + if t == "deep-stats": + return DeepStats(ctx, self.node) + if t == 'rename-form': + return RenameForm(self.node) + + raise WebError("GET directory: bad t=%s" % t) + + def render_PUT(self, ctx): + req = IRequest(ctx) + t = get_arg(req, "t", "").strip() + replace = boolean_of_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(), ctx) # 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.parentnode.replace_me_with_a_childcap(ctx, replace) + # TODO: results + return d + + raise WebError("PUT to a directory") + + def render_POST(self, ctx): + req = IRequest(ctx) + t = get_arg(req, "t", "").strip() + if t == "mkdir": + d = self._POST_mkdir(req) + elif t == "mkdir-p": + # TODO: docs, tests + d = self._POST_mkdir_p(req) + elif t == "upload": + d = self._POST_upload(ctx) # this one needs the context + elif t == "uri": + d = self._POST_uri(req) + elif t == "delete": + d = self._POST_delete(req) + elif t == "rename": + d = self._POST_rename(req) + elif t == "check": + d = self._POST_check(req) + elif t == "set_children": + # TODO: docs + 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: + d.addCallback(lambda res: url.URL.fromString(when_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")) + d = self.node.create_empty_directory(name, overwrite=replace) + d.addCallback(lambda child: child.get_uri()) # TODO: urlencode + return d + + def _POST_mkdir_p(self, req): + path = get_arg(req, "path") + if not path: + raise WebError("mkdir-p requires a path") + path_ = tuple([seg.decode("utf-8") for seg in path.split('/') if seg ]) + # TODO: replace + d = self._get_or_create_directories(self.node, path_) + d.addCallback(lambda node: node.get_uri()) + return d + + def _get_or_create_directories(self, node, path): + if not IDirectoryNode.providedBy(node): + # unfortunately it is too late to provide the name of the + # blocking directory in the error message. + raise BlockingFileError("cannot create directory because there " + "is a file in the way") + if not path: + return defer.succeed(node) + d = node.get(path[0]) + def _maybe_create(f): + f.trap(KeyError) + return node.create_empty_directory(path[0]) + d.addErrback(_maybe_create) + d.addCallback(self._get_or_create_directories, path[1:]) + return d + + def _POST_upload(self, ctx): + req = IRequest(ctx) + charset = get_arg(req, "_charset", "utf-8") + contents = req.fields["file"] + 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") + 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(ctx, 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(KeyError) + # create a placeholder + return PlaceHolderNodeHandler(self.node, name) + else: + node = node_or_failure + return make_handler_for(node, 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.renderHTTP(ctx)) + 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 = boolean_of_arg(get_arg(req, "replace", "true")) + d = self.node.set_uri(name, childcap, overwrite=replace) + d.addCallback(lambda res: childcap) + return d + + def _POST_delete(self, req): + name = get_arg(req, "name") + if name is None: + # apparently an + # won't show up in the resulting encoded form.. the 'name' + # field is completely missing. So to allow deletion of an + # empty file, we have to pretend that None means ''. The only + # downide of this is a slightly confusing error message if + # someone does a POST without a name= field. For our own HTML + # thisn't a big deal, because we create the 'delete' 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 deleted") + return d + + def _POST_rename(self, req): + charset = get_arg(req, "_charset", "utf-8") + 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) + 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) + if not from_name or not to_name: + raise WebError("rename requires from_name and to_name") + for k,v in [ ('from_name', from_name), ('to_name', to_name) ]: + if v and "/" in v: + raise WebError("%s= may not contain a slash" % k, + http.BAD_REQUEST) + + replace = boolean_of_arg(get_arg(req, "replace", "true")) + d = self.node.move_child_to(from_name, self.node, to_name, replace) + d.addCallback(lambda res: "thing renamed") + return d + + def _POST_check(self, req): + # check this directory + d = self.node.check() + def _done(res): + log.msg("checked %s, results %s" % (self.node, res), + facility="tahoe.webish", level=log.NOISY) + return str(res) + d.addCallback(_done) + # TODO: results + return d + + def _POST_set_children(self, req): + replace = boolean_of_arg(get_arg(req, "replace", "true")) + req.content.seek(0) + body = req.content.read() + try: + children = simplejson.loads(body) + except ValueError, le: + le.args = tuple(le.args + (body,)) + # TODO test handling of bad JSON + raise + cs = [] + for name, (file_or_dir, mddict) in children.iteritems(): + cap = str(mddict.get('rw_uri') or mddict.get('ro_uri')) + cs.append((name, cap, 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()) + si = u.get_filenode_uri().storage_index + si_s = base32.b2a(si) + return si_s[:6] + +class DirectoryAsHTML(rend.Page): + # The remainder of this class is to render the directory into + # human+browser -oriented HTML. + docFactory = getxmlfile("directory.xhtml") + + def __init__(self, node): + rend.Page.__init__(self) + self.node = node + + def render_title(self, ctx, data): + si_s = abbreviated_dirnode(self.node) + header = ["Directory SI=%s" % si_s] + return ctx.tag[header] + + def render_header(self, ctx, data): + si_s = abbreviated_dirnode(self.node) + header = ["Directory SI=%s" % si_s] + if self.node.is_readonly(): + header.append(" (readonly)") + return ctx.tag[header] + + def render_welcome(self, ctx, data): + depth = len(IRequest(ctx).path) + 2 + link = "/".join([".."] * depth) + return T.div[T.a(href=link)["Return to Welcome page"]] + + def data_children(self, ctx, data): + d = self.node.list() + d.addCallback(lambda dict: sorted(dict.items())) + def _stall_some(items): + # Deferreds don't optimize out tail recursion, and the way + # Nevow's flattener handles Deferreds doesn't take this into + # account. As a result, large lists of Deferreds that fire in the + # same turn (i.e. the output of defer.succeed) will cause a stack + # overflow. To work around this, we insert a turn break after + # every 100 items, using foolscap's fireEventually(). This gives + # the stack a chance to be popped. It would also work to put + # every item in its own turn, but that'd be a lot more + # inefficient. This addresses ticket #237, for which I was never + # able to create a failing unit test. + output = [] + for i,item in enumerate(items): + if i % 100 == 0: + output.append(fireEventually(item)) + else: + output.append(item) + return output + d.addCallback(_stall_some) + return d + + def render_row(self, ctx, data): + name, (target, metadata) = data + name = name.encode("utf-8") + assert not isinstance(name, unicode) + + if self.node.is_readonly(): + delete = "-" + rename = "-" + else: + # this creates a button which will cause our child__delete method + # to be invoked, which deletes the file and then redirects the + # browser back to this directory + delete = T.form(action=url.here, method="post")[ + T.input(type='hidden', name='t', value='delete'), + T.input(type='hidden', name='name', value=name), + T.input(type='hidden', name='when_done', value=url.here), + T.input(type='submit', value='del', name="del"), + ] + + rename = T.form(action=url.here, method="get")[ + T.input(type='hidden', name='t', value='rename-form'), + T.input(type='hidden', name='name', value=name), + T.input(type='hidden', name='when_done', value=url.here), + T.input(type='submit', value='rename', name="rename"), + ] + + ctx.fillSlots("delete", delete) + ctx.fillSlots("rename", rename) + check = T.form(action=url.here.child(name), method="post")[ + T.input(type='hidden', name='t', value='check'), + T.input(type='hidden', name='when_done', value=url.here), + T.input(type='submit', value='check', name="check"), + ] + ctx.fillSlots("overwrite", + self.build_overwrite_form(ctx, name, target)) + ctx.fillSlots("check", check) + + times = [] + TIME_FORMAT = "%H:%M:%S %d-%b-%Y" + if "ctime" in metadata: + ctime = time.strftime(TIME_FORMAT, + time.localtime(metadata["ctime"])) + times.append("c: " + ctime) + if "mtime" in metadata: + mtime = time.strftime(TIME_FORMAT, + time.localtime(metadata["mtime"])) + if times: + times.append(T.br()) + times.append("m: " + mtime) + ctx.fillSlots("times", times) + + assert (IFileNode.providedBy(target) + or IDirectoryNode.providedBy(target) + or IMutableFileNode.providedBy(target)), target + + quoted_uri = urllib.quote(target.get_uri()) + + 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 = "/file/%s/@@named=/%s" % (quoted_uri, urllib.quote(name)) + + ctx.fillSlots("filename", + T.a(href=dlurl)[html.escape(name)]) + ctx.fillSlots("type", "SSK") + + ctx.fillSlots("size", "?") + + text_plain_url = "/file/%s/@@named=/foo.txt" % quoted_uri + text_plain_tag = T.a(href=text_plain_url)["text/plain"] + + elif IFileNode.providedBy(target): + dlurl = "/file/%s/@@named=/%s" % (quoted_uri, urllib.quote(name)) + + ctx.fillSlots("filename", + T.a(href=dlurl)[html.escape(name)]) + ctx.fillSlots("type", "FILE") + + ctx.fillSlots("size", target.get_size()) + + text_plain_url = "/file/%s/@@named=/foo.txt" % quoted_uri + text_plain_tag = T.a(href=text_plain_url)["text/plain"] + + + elif IDirectoryNode.providedBy(target): + # directory + uri_link = "/uri/" + urllib.quote(target.get_uri()) + ctx.fillSlots("filename", + T.a(href=uri_link)[html.escape(name)]) + if target.is_readonly(): + dirtype = "DIR-RO" + else: + dirtype = "DIR" + ctx.fillSlots("type", dirtype) + ctx.fillSlots("size", "-") + text_plain_tag = None + + childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ", + T.a(href="%s?t=uri" % name)["URI"], ", ", + T.a(href="%s?t=readonly-uri" % name)["readonly-URI"], + ] + if text_plain_tag: + childdata.extend([", ", text_plain_tag]) + + ctx.fillSlots("data", childdata) + + try: + checker = IClient(ctx).getServiceNamed("checker") + except KeyError: + checker = None + if checker: + d = defer.maybeDeferred(checker.checker_results_for, + target.get_verifier()) + def _got(checker_results): + recent_results = reversed(checker_results[-5:]) + if IFileNode.providedBy(target): + results = ("[" + + ", ".join(["%d/%d" % (found, needed) + for (when, + (needed, total, found, sharemap)) + in recent_results]) + + "]") + elif IDirectoryNode.providedBy(target): + results = ("[" + + "".join([{True:"+",False:"-"}[res] + for (when, res) in recent_results]) + + "]") + else: + results = "%d results" % len(checker_results) + return results + d.addCallback(_got) + results = d + else: + results = "--" + # TODO: include a link to see more results, including timestamps + # TODO: use a sparkline + ctx.fillSlots("checker_results", results) + + return ctx.tag + + def render_forms(self, ctx, data): + if self.node.is_readonly(): + return T.div["No upload forms: directory is read-only"] + mkdir = T.form(action=".", method="post", + enctype="multipart/form-data")[ + T.fieldset[ + T.input(type="hidden", name="t", value="mkdir"), + T.input(type="hidden", name="when_done", value=url.here), + T.legend(class_="freeform-form-label")["Create a new directory"], + "New directory name: ", + T.input(type="text", name="name"), " ", + T.input(type="submit", value="Create"), + ]] + + upload = T.form(action=".", method="post", + enctype="multipart/form-data")[ + T.fieldset[ + T.input(type="hidden", name="t", value="upload"), + T.input(type="hidden", name="when_done", value=url.here), + T.legend(class_="freeform-form-label")["Upload a file to this directory"], + "Choose a file to upload: ", + T.input(type="file", name="file", class_="freeform-input-file"), + " ", + T.input(type="submit", value="Upload"), + " Mutable?:", + T.input(type="checkbox", name="mutable"), + ]] + + mount = T.form(action=".", method="post", + enctype="multipart/form-data")[ + T.fieldset[ + T.input(type="hidden", name="t", value="uri"), + T.input(type="hidden", name="when_done", value=url.here), + T.legend(class_="freeform-form-label")["Attach a file or directory" + " (by URI) to this" + " directory"], + "New child name: ", + T.input(type="text", name="name"), " ", + "URI of new child: ", + T.input(type="text", name="uri"), " ", + T.input(type="submit", value="Attach"), + ]] + return [T.div(class_="freeform-form")[mkdir], + T.div(class_="freeform-form")[upload], + T.div(class_="freeform-form")[mount], + ] + + def build_overwrite_form(self, ctx, name, target): + if IMutableFileNode.providedBy(target) and not target.is_readonly(): + action = "/uri/" + urllib.quote(target.get_uri()) + overwrite = T.form(action=action, method="post", + enctype="multipart/form-data")[ + T.fieldset[ + T.input(type="hidden", name="t", value="upload"), + T.input(type='hidden', name='when_done', value=url.here), + T.legend(class_="freeform-form-label")["Overwrite"], + "Choose new file: ", + T.input(type="file", name="file", class_="freeform-input-file"), + " ", + T.input(type="submit", value="Overwrite") + ]] + return [T.div(class_="freeform-form")[overwrite],] + else: + return [] + + def render_results(self, ctx, data): + req = IRequest(ctx) + return get_arg(req, "results", "") + + +def DirectoryJSONMetadata(ctx, dirnode): + d = dirnode.list() + def _got(children): + kids = {} + for name, (childnode, metadata) in children.iteritems(): + if IFileNode.providedBy(childnode): + kiduri = childnode.get_uri() + kiddata = ("filenode", + {'ro_uri': kiduri, + 'size': childnode.get_size(), + 'metadata': metadata, + }) + else: + assert IDirectoryNode.providedBy(childnode), (childnode, + children,) + kiddata = ("dirnode", + {'ro_uri': childnode.get_readonly_uri(), + 'metadata': metadata, + }) + if not childnode.is_readonly(): + kiddata[1]['rw_uri'] = childnode.get_uri() + kids[name] = kiddata + contents = { 'children': kids, + 'ro_uri': dirnode.get_readonly_uri(), + } + if not dirnode.is_readonly(): + contents['rw_uri'] = dirnode.get_uri() + data = ("dirnode", contents) + return simplejson.dumps(data, indent=1) + d.addCallback(_got) + d.addCallback(text_plain, ctx) + return d + +def DirectoryURI(ctx, dirnode): + return text_plain(dirnode.get_uri(), ctx) + +def DirectoryReadonlyURI(ctx, dirnode): + return text_plain(dirnode.get_readonly_uri(), ctx) + +class RenameForm(rend.Page): + addSlash = True + docFactory = getxmlfile("rename-form.xhtml") + + def render_title(self, ctx, data): + return ctx.tag["Directory SI=%s" % abbreviated_dirnode(self.original)] + + def render_header(self, ctx, data): + header = ["Rename " + "in directory SI=%s" % abbreviated_dirnode(self.original), + ] + + if self.original.is_readonly(): + header.append(" (readonly!)") + header.append(":") + return ctx.tag[header] + + def render_when_done(self, ctx, data): + return T.input(type="hidden", name="when_done", value=url.here) + + def render_get_name(self, ctx, data): + req = IRequest(ctx) + name = get_arg(req, "name", "") + ctx.tag.attributes['value'] = name + return ctx.tag + + +class Manifest(rend.Page): + docFactory = getxmlfile("manifest.xhtml") + + def render_title(self, ctx): + return T.title["Manifest of SI=%s" % abbreviated_dirnode(self.original)] + + def render_header(self, ctx): + return T.p["Manifest of SI=%s" % abbreviated_dirnode(self.original)] + + def data_items(self, ctx, data): + return self.original.build_manifest() + + def render_row(self, ctx, refresh_cap): + ctx.fillSlots("refresh_capability", refresh_cap) + return ctx.tag + +def DeepSize(ctx, dirnode): + d = dirnode.build_manifest() + def _measure_size(manifest): + total = 0 + for verifiercap in manifest: + u = from_string_verifier(verifiercap) + if isinstance(u, CHKFileVerifierURI): + total += u.size + return str(total) + d.addCallback(_measure_size) + d.addCallback(text_plain, ctx) + return d + +def DeepStats(ctx, dirnode): + d = dirnode.deep_stats() + d.addCallback(simplejson.dumps, indent=1) + d.addCallback(text_plain, ctx) + return d diff --git a/src/allmydata/web/filenode.py b/src/allmydata/web/filenode.py new file mode 100644 index 00000000..c17bc52f --- /dev/null +++ b/src/allmydata/web/filenode.py @@ -0,0 +1,419 @@ + +import simplejson + +from zope.interface import implements +from twisted.internet.interfaces import IConsumer +from twisted.web import http, static, resource, server +from twisted.internet import defer +from nevow import url, rend +from nevow.inevow import IRequest + +from allmydata.upload import FileHandle +from allmydata.interfaces import IDownloadTarget, ExistingChildError +from allmydata.mutable.common import MODE_READ +from allmydata.util import log + +from allmydata.web.common import text_plain, WebError, IClient, \ + boolean_of_arg, get_arg, should_create_intermediate_directories + +class ReplaceMeMixin: + + def replace_me_with_a_child(self, ctx, replace): + # a new file is being uploaded in our place. + req = IRequest(ctx) + client = IClient(ctx) + uploadable = FileHandle(req.content, convergence=client.convergence) + d = self.parentnode.add_file(self.name, uploadable, overwrite=replace) + def _done(filenode): + log.msg("webish upload complete", + facility="tahoe.webish", level=log.NOISY) + if self.node: + # we've replaced an existing file (or modified a mutable + # file), so the response code is 200 + req.setResponseCode(http.OK) + else: + # we've created a new file, so the code is 201 + req.setResponseCode(http.CREATED) + return filenode.get_uri() + d.addCallback(_done) + return d + + def replace_me_with_a_childcap(self, ctx, replace): + req = IRequest(ctx) + req.content.seek(0) + childcap = req.content.read() + client = IClient(ctx) + childnode = client.create_node_from_uri(childcap) + d = self.parentnode.set_node(self.name, childnode, overwrite=replace) + d.addCallback(lambda res: childnode.get_uri()) + return d + + def _read_data_from_formpost(self, req): + # SDMF: files are small, and we can only upload data, so we read + # the whole file into memory before uploading. + contents = req.fields["file"] + contents.file.seek(0) + data = contents.file.read() + return data + + def replace_me_with_a_formpost(self, ctx, replace): + # create a new file, maybe mutable, maybe immutable + req = IRequest(ctx) + client = IClient(ctx) + mutable = boolean_of_arg(get_arg(req, "mutable", "false")) + + if mutable: + data = self._read_data_from_formpost(req) + d = client.create_mutable_file(data) + def _uploaded(newnode): + d2 = self.parentnode.set_node(self.name, newnode, + overwrite=replace) + d2.addCallback(lambda res: newnode.get_uri()) + return d2 + d.addCallback(_uploaded) + return d + # create an immutable file + contents = req.fields["file"] + uploadable = FileHandle(contents.file, convergence=client.convergence) + d = self.parentnode.add_file(self.name, uploadable, overwrite=replace) + d.addCallback(lambda newnode: newnode.get_uri()) + return d + +class PlaceHolderNodeHandler(rend.Page, ReplaceMeMixin): + def __init__(self, parentnode, name): + rend.Page.__init__(self) + assert parentnode + self.parentnode = parentnode + self.name = name + self.node = None + + def childFactory(self, ctx, name): + req = IRequest(ctx) + if should_create_intermediate_directories(req): + raise WebError("Cannot create directory '%s', because its " + "parent is a file, not a directory" % name, + http.CONFLICT) + raise WebError("Files have no children, certainly not named '%s'" + % name, http.CONFLICT) + + + def renderHTTP(self, ctx): + # This is where all of the ?t=* actions are implemented. + request = IRequest(ctx) + + # if we were using regular twisted.web Resources (and the regular + # twisted.web.server.Request object) then we could implement + # render_PUT and render_GET. But Nevow's request handler + # (NevowRequest.gotPageContext) goes directly to renderHTTP. Copy + # some code from the Resource.render method that Nevow bypasses, to + # do the same thing. + m = getattr(self, 'render_' + request.method, None) + if not m: + from twisted.web.server import UnsupportedMethod + raise UnsupportedMethod(getattr(self, 'allowedMethods', ())) + return m(ctx) + + def render_PUT(self, ctx): + req = IRequest(ctx) + t = get_arg(req, "t", "").strip() + replace = boolean_of_arg(get_arg(req, "replace", "true")) + assert self.parentnode and self.name + if not t: + return self.replace_me_with_a_child(ctx, replace) + if t == "uri": + return self.replace_me_with_a_childcap(ctx, replace) + + raise WebError("PUT to a file: bad t=%s" % t) + + def render_POST(self, ctx): + req = IRequest(ctx) + t = get_arg(req, "t", "").strip() + replace = boolean_of_arg(get_arg(req, "replace", "true")) + if t == "mkdir": + d = self.parentnode.create_empty_directory(self.name, replace) + d.addCallback(lambda node: node.get_uri()) + d.addCallback(text_plain, ctx) + elif t == "upload": + # like PUT, but get the file data from an HTML form's input field. + # We could get here from POST /uri/mutablefilecap?t=upload, + # or POST /uri/path/file?t=upload, or + # POST /uri/path/dir?t=upload&name=foo . All have the same + # behavior, we just ignore any name= argument + d = self.replace_me_with_a_formpost(ctx, replace) + else: + raise WebError("POST to a file: bad t=%s" % t) + + when_done = get_arg(req, "when_done", None) + if when_done: + d.addCallback(lambda res: url.URL.fromString(when_done)) + return d + + +class FileNodeHandler(rend.Page, ReplaceMeMixin): + def __init__(self, node, parentnode=None, name=None): + rend.Page.__init__(self) + assert node + self.node = node + self.parentnode = parentnode + self.name = name + + def childFactory(self, ctx, name): + req = IRequest(ctx) + if should_create_intermediate_directories(req): + raise WebError("Cannot create directory '%s', because its " + "parent is a file, not a directory" % name) + raise WebError("Files have no children, certainly not named '%s'" + % name) + + + def renderHTTP(self, ctx): + # This is where all of the ?t=* actions are implemented. + request = IRequest(ctx) + + # if we were using regular twisted.web Resources (and the regular + # twisted.web.server.Request object) then we could implement + # render_PUT and render_GET. But Nevow's request handler + # (NevowRequest.gotPageContext) goes directly to renderHTTP. Copy + # some code from the Resource.render method that Nevow bypasses, to + # do the same thing. + m = getattr(self, 'render_' + request.method, None) + if not m: + from twisted.web.server import UnsupportedMethod + raise UnsupportedMethod(getattr(self, 'allowedMethods', ())) + return m(ctx) + + def render_GET(self, ctx): + req = IRequest(ctx) + t = get_arg(req, "t", "").strip() + if not t: + # just get the contents + filename = get_arg(req, "filename", self.name) or "unknown" + save_to_file = boolean_of_arg(get_arg(req, "save", "False")) + return FileDownloader(self.node, filename, save_to_file) + if t == "json": + return FileJSONMetadata(ctx, self.node) + if t == "uri": + return FileURI(ctx, self.node) + if t == "readonly-uri": + return FileReadOnlyURI(ctx, self.node) + raise WebError("GET file: bad t=%s" % t) + + def render_HEAD(self, ctx): + req = IRequest(ctx) + t = get_arg(req, "t", "").strip() + if t: + raise WebError("GET file: bad t=%s" % t) + if self.node.is_mutable(): + # update the servermap to get the size of this file without + # downloading the full contents. + d = self.node.get_servermap(MODE_READ) + def _got_servermap(smap): + ver = smap.best_recoverable_version() + if not ver: + raise WebError("Unable to recover this file", + http.NOT_FOUND) + length = smap.size_of_version(ver) + return length + d.addCallback(_got_servermap) + # otherwise, we can get the size from the URI + else: + d = defer.succeed(self.node.get_size()) + def _got_length(length): + req.setHeader("content-length", length) + return "" + d.addCallback(_got_length) + return d + + def render_PUT(self, ctx): + req = IRequest(ctx) + t = get_arg(req, "t", "").strip() + replace = boolean_of_arg(get_arg(req, "replace", "true")) + if not t: + if self.node.is_mutable(): + return self.replace_my_contents(ctx) + if not replace: + # this is the early trap: if someone else modifies the + # directory while we're uploading, the add_file(overwrite=) + # call in replace_me_with_a_child will do the late trap. + raise ExistingChildError() + assert self.parentnode and self.name + return self.replace_me_with_a_child(ctx, replace) + if t == "uri": + if not replace: + raise ExistingChildError() + assert self.parentnode and self.name + return self.replace_me_with_a_childcap(ctx, replace) + + raise WebError("PUT to a file: bad t=%s" % t) + + def render_POST(self, ctx): + req = IRequest(ctx) + t = get_arg(req, "t", "").strip() + replace = boolean_of_arg(get_arg(req, "replace", "true")) + if t == "check": + d = self._POST_check(req) + elif t == "upload": + # like PUT, but get the file data from an HTML form's input field + # We could get here from POST /uri/mutablefilecap?t=upload, + # or POST /uri/path/file?t=upload, or + # POST /uri/path/dir?t=upload&name=foo . All have the same + # behavior, we just ignore any name= argument + if self.node.is_mutable(): + d = self.replace_my_contents_with_a_formpost(ctx) + else: + if not replace: + raise ExistingChildError() + assert self.parentnode and self.name + d = self.replace_me_with_a_formpost(ctx, replace) + else: + raise WebError("POST to file: bad t=%s" % t) + + when_done = get_arg(req, "when_done", None) + if when_done: + d.addCallback(lambda res: url.URL.fromString(when_done)) + return d + + def _POST_check(self, req): + d = self.node.check() + def _done(res): + log.msg("checked %s, results %s" % (self.node, res), + facility="tahoe.webish", level=log.NOISY) + return str(res) + d.addCallback(_done) + # TODO: results + return d + + def render_DELETE(self, ctx): + assert self.parentnode and self.name + d = self.parentnode.delete(self.name) + d.addCallback(lambda res: self.node.get_uri()) + return d + + def replace_my_contents(self, ctx): + req = IRequest(ctx) + req.content.seek(0) + new_contents = req.content.read() + d = self.node.overwrite(new_contents) + d.addCallback(lambda res: self.node.get_uri()) + return d + + def replace_my_contents_with_a_formpost(self, ctx): + # we have a mutable file. Get the data from the formpost, and replace + # the mutable file's contents with it. + req = IRequest(ctx) + new_contents = self._read_data_from_formpost(req) + d = self.node.overwrite(new_contents) + d.addCallback(lambda res: self.node.get_uri()) + return d + + +class WebDownloadTarget: + implements(IDownloadTarget, IConsumer) + def __init__(self, req, content_type, content_encoding, save_to_filename): + self._req = req + self._content_type = content_type + self._content_encoding = content_encoding + self._opened = False + self._producer = None + self._save_to_filename = save_to_filename + + def registerProducer(self, producer, streaming): + self._req.registerProducer(producer, streaming) + def unregisterProducer(self): + self._req.unregisterProducer() + + def open(self, size): + self._opened = True + self._req.setHeader("content-type", self._content_type) + if self._content_encoding: + self._req.setHeader("content-encoding", self._content_encoding) + self._req.setHeader("content-length", str(size)) + if self._save_to_filename is not None: + # tell the browser to save the file rather display it + # TODO: indicate charset of filename= properly + filename = self._save_to_filename.encode("utf-8") + self._req.setHeader("content-disposition", + 'attachment; filename="%s"' + % filename) + + def write(self, data): + self._req.write(data) + def close(self): + self._req.finish() + + def fail(self, why): + if self._opened: + # The content-type is already set, and the response code + # has already been sent, so we can't provide a clean error + # indication. We can emit text (which a browser might interpret + # as something else), and if we sent a Size header, they might + # notice that we've truncated the data. Keep the error message + # small to improve the chances of having our error response be + # shorter than the intended results. + # + # We don't have a lot of options, unfortunately. + self._req.write("problem during download\n") + else: + # We haven't written anything yet, so we can provide a sensible + # error message. + msg = str(why.type) + msg.replace("\n", "|") + self._req.setResponseCode(http.GONE, msg) + self._req.setHeader("content-type", "text/plain") + # TODO: HTML-formatted exception? + self._req.write(str(why)) + self._req.finish() + + def register_canceller(self, cb): + pass + def finish(self): + pass + +class FileDownloader(resource.Resource): + # since we override the rendering process (to let the tahoe Downloader + # drive things), we must inherit from regular old twisted.web.resource + # instead of nevow.rend.Page . Nevow will use adapters to wrap a + # nevow.appserver.OldResourceAdapter around any + # twisted.web.resource.IResource that it is given. TODO: it looks like + # that wrapper would allow us to return a Deferred from render(), which + # might could simplify the implementation of WebDownloadTarget. + + def __init__(self, filenode, filename, save_to_file): + resource.Resource.__init__(self) + self.filenode = filenode + self.filename = filename + self.save_to_file = save_to_file + def render(self, req): + gte = static.getTypeAndEncoding + ctype, encoding = gte(self.filename, + static.File.contentTypes, + static.File.contentEncodings, + defaultType="text/plain") + save_to_filename = None + if self.save_to_file: + save_to_filename = self.filename + wdt = WebDownloadTarget(req, ctype, encoding, save_to_filename) + d = self.filenode.download(wdt) + # exceptions during download are handled by the WebDownloadTarget + d.addErrback(lambda why: None) + return server.NOT_DONE_YET + +def FileJSONMetadata(ctx, filenode): + file_uri = filenode.get_uri() + data = ("filenode", + {'ro_uri': file_uri, + 'size': filenode.get_size(), + }) + return text_plain(simplejson.dumps(data, indent=1), ctx) + +def FileURI(ctx, filenode): + return text_plain(filenode.get_uri(), ctx) + +def FileReadOnlyURI(ctx, filenode): + if filenode.is_readonly(): + return text_plain(filenode.get_uri(), ctx) + return text_plain(filenode.get_readonly().get_uri(), ctx) + +class FileNodeDownloadHandler(FileNodeHandler): + def childFactory(self, ctx, name): + return FileNodeDownloadHandler(self.node, name=name) diff --git a/src/allmydata/web/root.py b/src/allmydata/web/root.py new file mode 100644 index 00000000..51a96307 --- /dev/null +++ b/src/allmydata/web/root.py @@ -0,0 +1,293 @@ + +import time + +from twisted.internet import address +from twisted.web import http +from nevow import rend, url, tags as T +from nevow.inevow import IRequest +from nevow.static import File as nevow_File # TODO: merge with static.File? +from nevow.util import resource_filename +from formless import webform + +import allmydata # to display import path +from allmydata import get_package_versions_string +from allmydata import provisioning +from allmydata.util import idlib +from allmydata.interfaces import IFileNode +from allmydata.web import filenode, directory, unlinked, status +from allmydata.web.common import abbreviate_size, IClient, getxmlfile, \ + WebError, get_arg + + + +class URIHandler(rend.Page): + # I live at /uri . There are several operations defined on /uri itself, + # mostly involed with creation of unlinked files and directories. + + def renderHTTP(self, ctx): + request = IRequest(ctx) + + # if we were using regular twisted.web Resources (and the regular + # twisted.web.server.Request object) then we could implement + # render_PUT and render_GET. But Nevow's request handler + # (NevowRequest.gotPageContext) goes directly to renderHTTP. Copy + # some code from the Resource.render method that Nevow bypasses, to + # do the same thing. + m = getattr(self, 'render_' + request.method, None) + if not m: + from twisted.web.server import UnsupportedMethod + raise UnsupportedMethod(getattr(self, 'allowedMethods', ())) + return m(ctx) + + def render_GET(self, ctx): + req = IRequest(ctx) + uri = get_arg(req, "uri", None) + if uri is None: + raise WebError("GET /uri requires uri=") + there = url.URL.fromContext(ctx) + there = there.clear("uri") + # I thought about escaping the childcap that we attach to the URL + # here, but it seems that nevow does that for us. + there = there.child(uri) + return there + + def render_PUT(self, ctx): + req = IRequest(ctx) + # either "PUT /uri" to create an unlinked file, or + # "PUT /uri?t=mkdir" to create an unlinked directory + t = get_arg(req, "t", "").strip() + if t == "": + mutable = bool(get_arg(req, "mutable", "").strip()) + if mutable: + return unlinked.PUTUnlinkedSSK(ctx) + else: + return unlinked.PUTUnlinkedCHK(ctx) + if t == "mkdir": + return unlinked.PUTUnlinkedCreateDirectory(ctx) + errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, " + "and POST?t=mkdir") + raise WebError(errmsg, http.BAD_REQUEST) + + def render_POST(self, ctx): + # "POST /uri?t=upload&file=newfile" to upload an + # unlinked file or "POST /uri?t=mkdir" to create a + # new directory + req = IRequest(ctx) + t = get_arg(req, "t", "").strip() + if t in ("", "upload"): + mutable = bool(get_arg(req, "mutable", "").strip()) + if mutable: + return unlinked.POSTUnlinkedSSK(ctx) + else: + return unlinked.POSTUnlinkedCHK(ctx) + if t == "mkdir": + return unlinked.POSTUnlinkedCreateDirectory(ctx) + errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, " + "and POST?t=mkdir") + raise WebError(errmsg, http.BAD_REQUEST) + + def childFactory(self, ctx, name): + # 'name' is expected to be a URI + client = IClient(ctx) + try: + node = client.create_node_from_uri(name) + return directory.make_handler_for(node) + except (TypeError, AssertionError): + raise WebError("'%s' is not a valid file- or directory- cap" + % name) + +class FileHandler(rend.Page): + # I handle /file/$FILECAP[/IGNORED] , which provides a URL from which a + # file can be downloaded correctly by tools like "wget". + + def childFactory(self, ctx, name): + req = IRequest(ctx) + if req.method not in ("GET", "HEAD"): + raise WebError("/file can only be used with GET or HEAD") + # 'name' must be a file URI + client = IClient(ctx) + try: + node = client.create_node_from_uri(name) + except (TypeError, AssertionError): + 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) + return filenode.FileNodeDownloadHandler(node) + + def renderHTTP(self, ctx): + raise WebError("/file must be followed by a file-cap and a name", + http.NOT_FOUND) + +class Root(rend.Page): + + addSlash = True + docFactory = getxmlfile("welcome.xhtml") + + child_uri = URIHandler() + child_file = FileHandler() + child_named = FileHandler() + + child_webform_css = webform.defaultCSS + child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css')) + + child_provisioning = provisioning.ProvisioningTool() + child_status = status.Status() + child_helper_status = status.HelperStatus() + child_statistics = status.Statistics() + + def data_version(self, ctx, data): + return get_package_versions_string() + def data_import_path(self, ctx, data): + return str(allmydata) + def data_my_nodeid(self, ctx, data): + return idlib.nodeid_b2a(IClient(ctx).nodeid) + + def render_services(self, ctx, data): + ul = T.ul() + client = IClient(ctx) + try: + ss = client.getServiceNamed("storage") + allocated_s = abbreviate_size(ss.allocated_size()) + allocated = "about %s allocated" % allocated_s + sizelimit = "no size limit" + if ss.sizelimit is not None: + sizelimit = "size limit is %s" % abbreviate_size(ss.sizelimit) + ul[T.li["Storage Server: %s, %s" % (allocated, sizelimit)]] + except KeyError: + ul[T.li["Not running storage server"]] + + try: + h = client.getServiceNamed("helper") + stats = h.get_stats() + active_uploads = stats["chk_upload_helper.active_uploads"] + ul[T.li["Helper: %d active uploads" % (active_uploads,)]] + except KeyError: + ul[T.li["Not running helper"]] + + return ctx.tag[ul] + + def data_introducer_furl(self, ctx, data): + return IClient(ctx).introducer_furl + def data_connected_to_introducer(self, ctx, data): + if IClient(ctx).connected_to_introducer(): + return "yes" + return "no" + + def data_helper_furl(self, ctx, data): + try: + uploader = IClient(ctx).getServiceNamed("uploader") + except KeyError: + return None + furl, connected = uploader.get_helper_info() + return furl + def data_connected_to_helper(self, ctx, data): + try: + uploader = IClient(ctx).getServiceNamed("uploader") + except KeyError: + return "no" # we don't even have an Uploader + furl, connected = uploader.get_helper_info() + if connected: + return "yes" + return "no" + + def data_known_storage_servers(self, ctx, data): + ic = IClient(ctx).introducer_client + servers = [c + for c in ic.get_all_connectors().values() + if c.service_name == "storage"] + return len(servers) + + def data_connected_storage_servers(self, ctx, data): + ic = IClient(ctx).introducer_client + return len(ic.get_all_connections_for("storage")) + + def data_services(self, ctx, data): + ic = IClient(ctx).introducer_client + c = [ (service_name, nodeid, rsc) + for (nodeid, service_name), rsc + in ic.get_all_connectors().items() ] + c.sort() + return c + + def render_service_row(self, ctx, data): + (service_name, nodeid, rsc) = data + ctx.fillSlots("peerid", "%s %s" % (idlib.nodeid_b2a(nodeid), + rsc.nickname)) + if rsc.rref: + rhost = rsc.remote_host + if nodeid == IClient(ctx).nodeid: + rhost_s = "(loopback)" + elif isinstance(rhost, address.IPv4Address): + rhost_s = "%s:%d" % (rhost.host, rhost.port) + else: + rhost_s = str(rhost) + connected = "Yes: to " + rhost_s + since = rsc.last_connect_time + else: + connected = "No" + since = rsc.last_loss_time + + TIME_FORMAT = "%H:%M:%S %d-%b-%Y" + ctx.fillSlots("connected", connected) + ctx.fillSlots("since", time.strftime(TIME_FORMAT, time.localtime(since))) + ctx.fillSlots("announced", time.strftime(TIME_FORMAT, + time.localtime(rsc.announcement_time))) + ctx.fillSlots("version", rsc.version) + ctx.fillSlots("service_name", rsc.service_name) + + return ctx.tag + + 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"], + "URI to download: ", + T.input(type="text", name="uri"), " ", + "Filename to download as: ", + T.input(type="text", name="filename"), " ", + 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"], + "URI to view: ", + T.input(type="text", name="uri"), " ", + T.input(type="submit", value="View!"), + ]] + return T.div[form] + + def render_upload_form(self, ctx, data): + # this is a form where users can upload unlinked files + form = T.form(action="uri", method="post", + enctype="multipart/form-data")[ + T.fieldset[ + T.legend(class_="freeform-form-label")["Upload a file"], + "Choose a file: ", + T.input(type="file", name="file", class_="freeform-input-file"), + T.input(type="hidden", name="t", value="upload"), + " Mutable?:", T.input(type="checkbox", name="mutable"), + T.input(type="submit", value="Upload!"), + ]] + return T.div[form] + + def render_mkdir_form(self, ctx, data): + # this is a form where users can create new directories + form = T.form(action="uri", method="post", + enctype="multipart/form-data")[ + T.fieldset[ + T.legend(class_="freeform-form-label")["Create a directory"], + T.input(type="hidden", name="t", value="mkdir"), + T.input(type="hidden", name="redirect_to_result", value="true"), + T.input(type="submit", value="Create Directory!"), + ]] + return T.div[form] + diff --git a/src/allmydata/web/unlinked.py b/src/allmydata/web/unlinked.py index 7e4ae655..076908e9 100644 --- a/src/allmydata/web/unlinked.py +++ b/src/allmydata/web/unlinked.py @@ -1,76 +1,73 @@ import urllib from twisted.web import http -from nevow import rend, inevow, url, tags as T +from twisted.internet import defer +from nevow import rend, url, tags as T +from nevow.inevow import IRequest from allmydata.upload import FileHandle from allmydata.web.common import IClient, getxmlfile, get_arg, boolean_of_arg from allmydata.web import status -from allmydata.util import observer -class UnlinkedPUTCHKUploader(rend.Page): - def renderHTTP(self, ctx): - req = inevow.IRequest(ctx) - assert req.method == "PUT" - # "PUT /uri", to create an unlinked file. This is like PUT but - # without the associated set_uri. - - client = IClient(ctx) - - uploadable = FileHandle(req.content, client.convergence) - d = client.upload(uploadable) - d.addCallback(lambda results: results.uri) - # that fires with the URI of the new file - return d - -class UnlinkedPUTSSKUploader(rend.Page): - def renderHTTP(self, ctx): - req = inevow.IRequest(ctx) - assert req.method == "PUT" - # SDMF: files are small, and we can only upload data - req.content.seek(0) - data = req.content.read() - d = IClient(ctx).create_mutable_file(data) - d.addCallback(lambda n: n.get_uri()) - return d - -class UnlinkedPUTCreateDirectory(rend.Page): - def renderHTTP(self, ctx): - req = inevow.IRequest(ctx) - assert req.method == "PUT" - # "PUT /uri?t=mkdir", to create an unlinked directory. - d = IClient(ctx).create_empty_dirnode() - d.addCallback(lambda dirnode: dirnode.get_uri()) - # XXX add redirect_to_result - return d - -class UnlinkedPOSTCHKUploader(status.UploadResultsRendererMixin, rend.Page): +def PUTUnlinkedCHK(ctx): + req = IRequest(ctx) + # "PUT /uri", to create an unlinked file. + client = IClient(ctx) + uploadable = FileHandle(req.content, client.convergence) + d = client.upload(uploadable) + d.addCallback(lambda results: results.uri) + # that fires with the URI of the new file + return d + +def PUTUnlinkedSSK(ctx): + req = IRequest(ctx) + # SDMF: files are small, and we can only upload data + req.content.seek(0) + data = req.content.read() + d = IClient(ctx).create_mutable_file(data) + d.addCallback(lambda n: n.get_uri()) + return d + +def PUTUnlinkedCreateDirectory(ctx): + req = IRequest(ctx) + # "PUT /uri?t=mkdir", to create an unlinked directory. + d = IClient(ctx).create_empty_dirnode() + d.addCallback(lambda dirnode: dirnode.get_uri()) + # XXX add redirect_to_result + return d + + +def POSTUnlinkedCHK(ctx): + req = IRequest(ctx) + client = IClient(ctx) + fileobj = req.fields["file"].file + uploadable = FileHandle(fileobj, client.convergence) + d = client.upload(uploadable) + when_done = get_arg(req, "when_done", None) + if when_done: + # if when_done= is provided, return a redirect instead of our + # usual upload-results page + def _done(upload_results, redir_to): + if "%(uri)s" in redir_to: + redir_to = redir_to % {"uri": urllib.quote(upload_results.uri) + } + return url.URL.fromString(redir_to) + d.addCallback(_done, when_done) + else: + # return the Upload Results page, which includes the URI + d.addCallback(UploadResultsPage, ctx) + return d + + +class UploadResultsPage(status.UploadResultsRendererMixin, rend.Page): """'POST /uri', to create an unlinked file.""" docFactory = getxmlfile("upload-results.xhtml") - def __init__(self, client, req): + def __init__(self, upload_results, ctx): rend.Page.__init__(self) - # we start the upload now, and distribute notification of its - # completion to render_ methods with an ObserverList - assert req.method == "POST" - self._done = observer.OneShotObserverList() - fileobj = req.fields["file"].file - uploadable = FileHandle(fileobj, client.convergence) - d = client.upload(uploadable) - d.addBoth(self._done.fire) - - def renderHTTP(self, ctx): - req = inevow.IRequest(ctx) - when_done = get_arg(req, "when_done", None) - if when_done: - # if when_done= is provided, return a redirect instead of our - # usual upload-results page - d = self._done.when_fired() - d.addCallback(lambda res: url.URL.fromString(when_done)) - return d - return rend.Page.renderHTTP(self, ctx) + self.results = upload_results def upload_results(self): - return self._done.when_fired() + return defer.succeed(self.results) def data_done(self, ctx, data): d = self.upload_results() @@ -88,37 +85,31 @@ class UnlinkedPOSTCHKUploader(status.UploadResultsRendererMixin, rend.Page): ["/uri/" + res.uri]) return d -class UnlinkedPOSTSSKUploader(rend.Page): - def renderHTTP(self, ctx): - req = inevow.IRequest(ctx) - assert req.method == "POST" - - # "POST /uri", to create an unlinked file. - # SDMF: files are small, and we can only upload data - contents = req.fields["file"] - contents.file.seek(0) - data = contents.file.read() - d = IClient(ctx).create_mutable_file(data) - d.addCallback(lambda n: n.get_uri()) - return d - -class UnlinkedPOSTCreateDirectory(rend.Page): - def renderHTTP(self, ctx): - req = inevow.IRequest(ctx) - assert req.method == "POST" - - # "POST /uri?t=mkdir", to create an unlinked directory. - d = IClient(ctx).create_empty_dirnode() - redirect = get_arg(req, "redirect_to_result", "false") - if boolean_of_arg(redirect): - def _then_redir(res): - new_url = "uri/" + urllib.quote(res.get_uri()) - req.setResponseCode(http.SEE_OTHER) # 303 - req.setHeader('location', new_url) - req.finish() - return '' - d.addCallback(_then_redir) - else: - d.addCallback(lambda dirnode: dirnode.get_uri()) - return d +def POSTUnlinkedSSK(ctx): + req = IRequest(ctx) + # "POST /uri", to create an unlinked file. + # SDMF: files are small, and we can only upload data + contents = req.fields["file"] + contents.file.seek(0) + data = contents.file.read() + d = IClient(ctx).create_mutable_file(data) + d.addCallback(lambda n: n.get_uri()) + return d + +def POSTUnlinkedCreateDirectory(ctx): + req = IRequest(ctx) + # "POST /uri?t=mkdir", to create an unlinked directory. + d = IClient(ctx).create_empty_dirnode() + redirect = get_arg(req, "redirect_to_result", "false") + if boolean_of_arg(redirect): + def _then_redir(res): + new_url = "uri/" + urllib.quote(res.get_uri()) + req.setResponseCode(http.SEE_OTHER) # 303 + req.setHeader('location', new_url) + req.finish() + return '' + d.addCallback(_then_redir) + else: + d.addCallback(lambda dirnode: dirnode.get_uri()) + return d diff --git a/src/allmydata/webish.py b/src/allmydata/webish.py index b1c3c48a..02edd6dd 100644 --- a/src/allmydata/webish.py +++ b/src/allmydata/webish.py @@ -1,36 +1,12 @@ -import time, os.path from twisted.application import service, strports, internet -from twisted.web import static, resource, server, html, http -from twisted.internet import defer, address -from twisted.internet.interfaces import IConsumer -from nevow import inevow, rend, appserver, url, tags as T -from nevow.static import File as nevow_File # TODO: merge with static.File? -from allmydata.util import fileutil, idlib, log -import simplejson -from allmydata.interfaces import IDownloadTarget, IDirectoryNode, IFileNode, \ - IMutableFileNode -import allmydata # to display import path -from allmydata import download -from allmydata.uri import from_string_verifier, CHKFileVerifierURI -from allmydata.upload import FileHandle, FileName -from allmydata import provisioning -from allmydata import get_package_versions_string -from zope.interface import implements, Interface -import urllib -from formless import webform -from foolscap.eventual import fireEventually +from twisted.web import http +from twisted.internet import defer +from nevow import appserver, inevow +from allmydata.util import log -from nevow.util import resource_filename - -from allmydata.web import status, unlinked, introweb -from allmydata.web.common import IClient, getxmlfile, get_arg, \ - boolean_of_arg, abbreviate_size - -class ILocalAccess(Interface): - def local_access_is_allowed(): - """Return True if t=upload&localdir= is allowed, giving anyone who - can talk to the webserver control over the local (disk) filesystem.""" +from allmydata.web import introweb, root +from allmydata.web.common import IClient, MyExceptionHandler # we must override twisted.web.http.Request.requestReceived with a version # that doesn't use cgi.parse_multipart() . Since we actually use Nevow, we @@ -99,7 +75,6 @@ class MyRequest(appserver.NevowRequest): ## self.channel.transport.loseConnection() ## return ## raise - self.process() def _logger(self): @@ -136,1496 +111,11 @@ class MyRequest(appserver.NevowRequest): level=log.OPERATIONAL, ) -class Directory(rend.Page): - addSlash = True - docFactory = getxmlfile("directory.xhtml") - - def __init__(self, rootname, dirnode, dirpath): - self._rootname = rootname - self._dirnode = dirnode - self._dirpath = dirpath - - def dirpath_as_string(self): - return "/" + "/".join(self._dirpath) - - def render_title(self, ctx, data): - return ctx.tag["Directory '%s':" % self.dirpath_as_string()] - - def render_header(self, ctx, data): - parent_directories = ("<%s>" % self._rootname,) + self._dirpath - num_dirs = len(parent_directories) - - header = ["Directory '"] - for i,d in enumerate(parent_directories): - upness = num_dirs - i - 1 - if upness: - link = "/".join( ("..",) * upness ) - else: - link = "." - header.append(T.a(href=link)[d]) - if upness != 0: - header.append("/") - header.append("'") - - if self._dirnode.is_readonly(): - header.append(" (readonly)") - header.append(":") - return ctx.tag[header] - - def render_welcome(self, ctx, data): - depth = len(self._dirpath) + 2 - link = "/".join([".."] * depth) - return T.div[T.a(href=link)["Return to Welcome page"]] - - def data_children(self, ctx, data): - d = self._dirnode.list() - d.addCallback(lambda dict: sorted(dict.items())) - def _stall_some(items): - # Deferreds don't optimize out tail recursion, and the way - # Nevow's flattener handles Deferreds doesn't take this into - # account. As a result, large lists of Deferreds that fire in the - # same turn (i.e. the output of defer.succeed) will cause a stack - # overflow. To work around this, we insert a turn break after - # every 100 items, using foolscap's fireEventually(). This gives - # the stack a chance to be popped. It would also work to put - # every item in its own turn, but that'd be a lot more - # inefficient. This addresses ticket #237, for which I was never - # able to create a failing unit test. - output = [] - for i,item in enumerate(items): - if i % 100 == 0: - output.append(fireEventually(item)) - else: - output.append(item) - return output - d.addCallback(_stall_some) - return d - - def render_row(self, ctx, data): - name, (target, metadata) = data - name = name.encode("utf-8") - assert not isinstance(name, unicode) - - if self._dirnode.is_readonly(): - delete = "-" - rename = "-" - else: - # this creates a button which will cause our child__delete method - # to be invoked, which deletes the file and then redirects the - # browser back to this directory - delete = T.form(action=url.here, method="post")[ - T.input(type='hidden', name='t', value='delete'), - T.input(type='hidden', name='name', value=name), - T.input(type='hidden', name='when_done', value=url.here), - T.input(type='submit', value='del', name="del"), - ] - - rename = T.form(action=url.here, method="get")[ - T.input(type='hidden', name='t', value='rename-form'), - T.input(type='hidden', name='name', value=name), - T.input(type='hidden', name='when_done', value=url.here), - T.input(type='submit', value='rename', name="rename"), - ] - - ctx.fillSlots("delete", delete) - ctx.fillSlots("rename", rename) - check = T.form(action=url.here, method="post")[ - T.input(type='hidden', name='t', value='check'), - T.input(type='hidden', name='name', value=name), - T.input(type='hidden', name='when_done', value=url.here), - T.input(type='submit', value='check', name="check"), - ] - ctx.fillSlots("overwrite", self.build_overwrite(ctx, (name, target))) - ctx.fillSlots("check", check) - - times = [] - TIME_FORMAT = "%H:%M:%S %d-%b-%Y" - if "ctime" in metadata: - ctime = time.strftime(TIME_FORMAT, - time.localtime(metadata["ctime"])) - times.append("c: " + ctime) - if "mtime" in metadata: - mtime = time.strftime(TIME_FORMAT, - time.localtime(metadata["mtime"])) - if times: - times.append(T.br()) - times.append("m: " + mtime) - ctx.fillSlots("times", times) - - assert (IFileNode.providedBy(target) - or IDirectoryNode.providedBy(target) - or IMutableFileNode.providedBy(target)), target - - quoted_uri = urllib.quote(target.get_uri()) - - 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 = "/file/%s/@@named=/%s" % (quoted_uri, urllib.quote(name)) - - ctx.fillSlots("filename", - T.a(href=dlurl)[html.escape(name)]) - ctx.fillSlots("type", "SSK") - - ctx.fillSlots("size", "?") - - text_plain_url = "/file/%s/@@named=/foo.txt" % quoted_uri - text_plain_tag = T.a(href=text_plain_url)["text/plain"] - - elif IFileNode.providedBy(target): - dlurl = "/file/%s/@@named=/%s" % (quoted_uri, urllib.quote(name)) - - ctx.fillSlots("filename", - T.a(href=dlurl)[html.escape(name)]) - ctx.fillSlots("type", "FILE") - - ctx.fillSlots("size", target.get_size()) - - text_plain_url = "/file/%s/@@named=/foo.txt" % quoted_uri - text_plain_tag = T.a(href=text_plain_url)["text/plain"] - - - elif IDirectoryNode.providedBy(target): - # directory - uri_link = "/uri/" + urllib.quote(target.get_uri()) - ctx.fillSlots("filename", - T.a(href=uri_link)[html.escape(name)]) - if target.is_readonly(): - dirtype = "DIR-RO" - else: - dirtype = "DIR" - ctx.fillSlots("type", dirtype) - ctx.fillSlots("size", "-") - text_plain_tag = None - - childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ", - T.a(href="%s?t=uri" % name)["URI"], ", ", - T.a(href="%s?t=readonly-uri" % name)["readonly-URI"], - ] - if text_plain_tag: - childdata.extend([", ", text_plain_tag]) - - ctx.fillSlots("data", childdata) - - try: - checker = IClient(ctx).getServiceNamed("checker") - except KeyError: - checker = None - if checker: - d = defer.maybeDeferred(checker.checker_results_for, - target.get_verifier()) - def _got(checker_results): - recent_results = reversed(checker_results[-5:]) - if IFileNode.providedBy(target): - results = ("[" + - ", ".join(["%d/%d" % (found, needed) - for (when, - (needed, total, found, sharemap)) - in recent_results]) + - "]") - elif IDirectoryNode.providedBy(target): - results = ("[" + - "".join([{True:"+",False:"-"}[res] - for (when, res) in recent_results]) + - "]") - else: - results = "%d results" % len(checker_results) - return results - d.addCallback(_got) - results = d - else: - results = "--" - # TODO: include a link to see more results, including timestamps - # TODO: use a sparkline - ctx.fillSlots("checker_results", results) - - return ctx.tag - - def render_forms(self, ctx, data): - if self._dirnode.is_readonly(): - return T.div["No upload forms: directory is read-only"] - mkdir = T.form(action=".", method="post", - enctype="multipart/form-data")[ - T.fieldset[ - T.input(type="hidden", name="t", value="mkdir"), - T.input(type="hidden", name="when_done", value=url.here), - T.legend(class_="freeform-form-label")["Create a new directory"], - "New directory name: ", - T.input(type="text", name="name"), " ", - T.input(type="submit", value="Create"), - ]] - - upload = T.form(action=".", method="post", - enctype="multipart/form-data")[ - T.fieldset[ - T.input(type="hidden", name="t", value="upload"), - T.input(type="hidden", name="when_done", value=url.here), - T.legend(class_="freeform-form-label")["Upload a file to this directory"], - "Choose a file to upload: ", - T.input(type="file", name="file", class_="freeform-input-file"), - " ", - T.input(type="submit", value="Upload"), - " Mutable?:", - T.input(type="checkbox", name="mutable"), - ]] - - mount = T.form(action=".", method="post", - enctype="multipart/form-data")[ - T.fieldset[ - T.input(type="hidden", name="t", value="uri"), - T.input(type="hidden", name="when_done", value=url.here), - T.legend(class_="freeform-form-label")["Attach a file or directory" - " (by URI) to this" - " directory"], - "New child name: ", - T.input(type="text", name="name"), " ", - "URI of new child: ", - T.input(type="text", name="uri"), " ", - T.input(type="submit", value="Attach"), - ]] - return [T.div(class_="freeform-form")[mkdir], - T.div(class_="freeform-form")[upload], - T.div(class_="freeform-form")[mount], - ] - - def build_overwrite(self, ctx, data): - name, target = data - if IMutableFileNode.providedBy(target) and not target.is_readonly(): - action="/uri/" + urllib.quote(target.get_uri()) - overwrite = T.form(action=action, method="post", - enctype="multipart/form-data")[ - T.fieldset[ - T.input(type="hidden", name="t", value="overwrite"), - T.input(type='hidden', name='name', value=name), - T.input(type='hidden', name='when_done', value=url.here), - T.legend(class_="freeform-form-label")["Overwrite"], - "Choose new file: ", - T.input(type="file", name="file", class_="freeform-input-file"), - " ", - T.input(type="submit", value="Overwrite") - ]] - return [T.div(class_="freeform-form")[overwrite],] - else: - return [] - - def render_results(self, ctx, data): - req = inevow.IRequest(ctx) - return get_arg(req, "results", "") - -class WebDownloadTarget: - implements(IDownloadTarget, IConsumer) - def __init__(self, req, content_type, content_encoding, save_to_file): - self._req = req - self._content_type = content_type - self._content_encoding = content_encoding - self._opened = False - self._producer = None - self._save_to_file = save_to_file - - def registerProducer(self, producer, streaming): - self._req.registerProducer(producer, streaming) - def unregisterProducer(self): - self._req.unregisterProducer() - - def open(self, size): - self._opened = True - self._req.setHeader("content-type", self._content_type) - if self._content_encoding: - self._req.setHeader("content-encoding", self._content_encoding) - self._req.setHeader("content-length", str(size)) - if self._save_to_file is not None: - # tell the browser to save the file rather display it - # TODO: quote save_to_file properly - filename = self._save_to_file.encode("utf-8") - self._req.setHeader("content-disposition", - 'attachment; filename="%s"' - % filename) - - def write(self, data): - self._req.write(data) - def close(self): - self._req.finish() - - def fail(self, why): - if self._opened: - # The content-type is already set, and the response code - # has already been sent, so we can't provide a clean error - # indication. We can emit text (which a browser might interpret - # as something else), and if we sent a Size header, they might - # notice that we've truncated the data. Keep the error message - # small to improve the chances of having our error response be - # shorter than the intended results. - # - # We don't have a lot of options, unfortunately. - self._req.write("problem during download\n") - else: - # We haven't written anything yet, so we can provide a sensible - # error message. - msg = str(why.type) - msg.replace("\n", "|") - self._req.setResponseCode(http.GONE, msg) - self._req.setHeader("content-type", "text/plain") - # TODO: HTML-formatted exception? - self._req.write(str(why)) - self._req.finish() - - def register_canceller(self, cb): - pass - def finish(self): - pass - -class FileDownloader(resource.Resource): - isLeaf = True - def __init__(self, filenode, name): - assert (IFileNode.providedBy(filenode) - or IMutableFileNode.providedBy(filenode)) - self._filenode = filenode - self._name = name - - def render(self, req): - gte = static.getTypeAndEncoding - ctype, encoding = gte(self._name, - static.File.contentTypes, - static.File.contentEncodings, - defaultType="text/plain") - save_to_file = None - if get_arg(req, "save", False): - # TODO: make the API specification clear: should "save=" or - # "save=false" count? - save_to_file = self._name - wdt = WebDownloadTarget(req, ctype, encoding, save_to_file) - d = self._filenode.download(wdt) - # exceptions during download are handled by the WebDownloadTarget - d.addErrback(lambda why: None) - return server.NOT_DONE_YET - -class BlockingFileError(Exception): - """We cannot auto-create a parent directory, because there is a file in - the way""" -class NoReplacementError(Exception): - """There was already a child by that name, and you asked me to not replace it""" -class NoLocalDirectoryError(Exception): - """The localdir= directory didn't exist""" - -LOCALHOST = "127.0.0.1" - -class NeedLocalhostError: - implements(inevow.IResource) - - def renderHTTP(self, ctx): - req = inevow.IRequest(ctx) - req.setResponseCode(http.FORBIDDEN) - req.setHeader("content-type", "text/plain") - return "localfile= or localdir= requires a local connection" - -class NeedAbsolutePathError: - implements(inevow.IResource) - - def renderHTTP(self, ctx): - req = inevow.IRequest(ctx) - req.setResponseCode(http.FORBIDDEN) - req.setHeader("content-type", "text/plain") - return "localfile= or localdir= requires an absolute path" - -class LocalAccessDisabledError: - implements(inevow.IResource) - - def renderHTTP(self, ctx): - req = inevow.IRequest(ctx) - req.setResponseCode(http.FORBIDDEN) - req.setHeader("content-type", "text/plain") - return "local file access is disabled" - -class WebError: - implements(inevow.IResource) - def __init__(self, response_code, errmsg): - self._response_code = response_code - self._errmsg = errmsg - - def renderHTTP(self, ctx): - req = inevow.IRequest(ctx) - req.setResponseCode(self._response_code) - req.setHeader("content-type", "text/plain") - return self._errmsg - - -class LocalFileDownloader(resource.Resource): - def __init__(self, filenode, local_filename): - self._local_filename = local_filename - IFileNode(filenode) - self._filenode = filenode - - def render(self, req): - target = download.FileName(self._local_filename) - d = self._filenode.download(target) - def _done(res): - req.write(self._filenode.get_uri()) - req.finish() - d.addCallback(_done) - return server.NOT_DONE_YET - - -class FileJSONMetadata(rend.Page): - def __init__(self, filenode): - self._filenode = filenode - - def renderHTTP(self, ctx): - req = inevow.IRequest(ctx) - req.setHeader("content-type", "text/plain") - return self.renderNode(self._filenode) - - def renderNode(self, filenode): - file_uri = filenode.get_uri() - data = ("filenode", - {'ro_uri': file_uri, - 'size': filenode.get_size(), - }) - return simplejson.dumps(data, indent=1) - -class FileURI(FileJSONMetadata): - def renderNode(self, filenode): - file_uri = filenode.get_uri() - return file_uri - -class FileReadOnlyURI(FileJSONMetadata): - def renderNode(self, filenode): - if filenode.is_readonly(): - return filenode.get_uri() - else: - return filenode.get_readonly().get_uri() - -class DirnodeWalkerMixin: - """Visit all nodes underneath (and including) the rootnode, one at a - time. For each one, call the visitor. The visitor will see the - IDirectoryNode before it sees any of the IFileNodes inside. If the - visitor returns a Deferred, I do not call the visitor again until it has - fired. - """ - -## def _walk_if_we_could_use_generators(self, rootnode, rootpath=()): -## # this is what we'd be doing if we didn't have the Deferreds and -## # thus could use generators -## yield rootpath, rootnode -## for childname, childnode in rootnode.list().items(): -## childpath = rootpath + (childname,) -## if IFileNode.providedBy(childnode): -## yield childpath, childnode -## elif IDirectoryNode.providedBy(childnode): -## for res in self._walk_if_we_could_use_generators(childnode, -## childpath): -## yield res - - def walk(self, rootnode, visitor, rootpath=()): - d = rootnode.list() - def _listed(listing): - return listing.items() - d.addCallback(_listed) - d.addCallback(self._handle_items, visitor, rootpath) - return d - - def _handle_items(self, items, visitor, rootpath): - if not items: - return - childname, (childnode, metadata) = items[0] - childpath = rootpath + (childname,) - d = defer.maybeDeferred(visitor, childpath, childnode, metadata) - if IDirectoryNode.providedBy(childnode): - d.addCallback(lambda res: self.walk(childnode, visitor, childpath)) - d.addCallback(lambda res: - self._handle_items(items[1:], visitor, rootpath)) - return d - -class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin): - def __init__(self, dirnode, localdir): - self._dirnode = dirnode - self._localdir = localdir - - def _handle(self, path, node, metadata): - path = tuple([p.encode("utf-8") for p in path]) - localfile = os.path.join(self._localdir, os.sep.join(path)) - if IDirectoryNode.providedBy(node): - fileutil.make_dirs(localfile) - elif IFileNode.providedBy(node): - target = download.FileName(localfile) - return node.download(target) - - def render(self, req): - d = self.walk(self._dirnode, self._handle) - def _done(res): - req.setHeader("content-type", "text/plain") - return "operation complete" - d.addCallback(_done) - return d - -class DirectoryJSONMetadata(rend.Page): - def __init__(self, dirnode): - self._dirnode = dirnode - - def renderHTTP(self, ctx): - req = inevow.IRequest(ctx) - req.setHeader("content-type", "text/plain") - return self.renderNode(self._dirnode) - - def renderNode(self, node): - d = node.list() - def _got(children): - kids = {} - for name, (childnode, metadata) in children.iteritems(): - if IFileNode.providedBy(childnode): - kiduri = childnode.get_uri() - kiddata = ("filenode", - {'ro_uri': kiduri, - 'size': childnode.get_size(), - 'metadata': metadata, - }) - else: - assert IDirectoryNode.providedBy(childnode), (childnode, children,) - kiddata = ("dirnode", - {'ro_uri': childnode.get_readonly_uri(), - 'metadata': metadata, - }) - if not childnode.is_readonly(): - kiddata[1]['rw_uri'] = childnode.get_uri() - kids[name] = kiddata - contents = { 'children': kids, - 'ro_uri': node.get_readonly_uri(), - } - if not node.is_readonly(): - contents['rw_uri'] = node.get_uri() - data = ("dirnode", contents) - return simplejson.dumps(data, indent=1) - d.addCallback(_got) - return d - -class DirectoryURI(DirectoryJSONMetadata): - def renderNode(self, node): - return node.get_uri() - -class DirectoryReadonlyURI(DirectoryJSONMetadata): - def renderNode(self, node): - return node.get_readonly_uri() - -class RenameForm(rend.Page): - addSlash = True - docFactory = getxmlfile("rename-form.xhtml") - - def __init__(self, rootname, dirnode, dirpath): - self._rootname = rootname - self._dirnode = dirnode - self._dirpath = dirpath - - def dirpath_as_string(self): - return "/" + "/".join(self._dirpath) - - def render_title(self, ctx, data): - return ctx.tag["Directory '%s':" % self.dirpath_as_string()] - - def render_header(self, ctx, data): - parent_directories = ("<%s>" % self._rootname,) + self._dirpath - num_dirs = len(parent_directories) - - header = [ "Rename in directory '", - "<%s>/" % self._rootname, - "/".join(self._dirpath), - "':", ] - - if self._dirnode.is_readonly(): - header.append(" (readonly)") - return ctx.tag[header] - - def render_when_done(self, ctx, data): - return T.input(type="hidden", name="when_done", value=url.here) - - def render_get_name(self, ctx, data): - req = inevow.IRequest(ctx) - name = get_arg(req, "name", "") - ctx.tag.attributes['value'] = name - return ctx.tag - -class POSTHandler(rend.Page): - def __init__(self, node, replace): - self._node = node - self._replace = replace - - def _check_replacement(self, name): - if self._replace: - return defer.succeed(None) - d = self._node.has_child(name) - def _got(present): - if present: - raise NoReplacementError("There was already a child by that " - "name, and you asked me to not " - "replace it.") - return None - d.addCallback(_got) - return d - def _POST_mkdir(self, name): - d = self._check_replacement(name) - d.addCallback(lambda res: self._node.create_empty_directory(name)) - d.addCallback(lambda res: "directory created") - return d - - def _POST_mkdir_p(self, path): - path_ = tuple([seg.decode("utf-8") for seg in path.split('/') if seg ]) - d = self._get_or_create_directories(self._node, path_) - d.addCallback(lambda node: node.get_uri()) - return d - - # this code stolen from PUTHandler: should be refactored to a more - # generally accesible place, perhaps... - def _get_or_create_directories(self, node, path): - if not IDirectoryNode.providedBy(node): - # unfortunately it is too late to provide the name of the - # blocking directory in the error message. - raise BlockingFileError("cannot create directory because there " - "is a file in the way") - if not path: - return defer.succeed(node) - d = node.get(path[0]) - def _maybe_create(f): - f.trap(KeyError) - return node.create_empty_directory(path[0]) - d.addErrback(_maybe_create) - d.addCallback(self._get_or_create_directories, path[1:]) - return d - - def _POST_uri(self, name, newuri): - d = self._check_replacement(name) - d.addCallback(lambda res: self._node.set_uri(name, newuri)) - d.addCallback(lambda res: newuri) - return d - - def _POST_delete(self, name): - if name is None: - # apparently an - # won't show up in the resulting encoded form.. the 'name' - # field is completely missing. So to allow deletion of an - # empty file, we have to pretend that None means ''. The only - # downide of this is a slightly confusing error message if - # someone does a POST without a name= field. For our own HTML - # thisn't a big deal, because we create the 'delete' POST - # buttons ourselves. - name = '' - d = self._node.delete(name) - d.addCallback(lambda res: "thing deleted") - return d - - def _POST_rename(self, name, from_name, to_name): - d = self._check_replacement(to_name) - d.addCallback(lambda res: self._node.get(from_name)) - def add_dest(child): - uri = child.get_uri() - # now actually do the rename - return self._node.set_uri(to_name, uri) - d.addCallback(add_dest) - def rm_src(junk): - return self._node.delete(from_name) - d.addCallback(rm_src) - d.addCallback(lambda res: "thing renamed") - return d - - def _POST_upload(self, contents, name, mutable, client): - if mutable: - # SDMF: files are small, and we can only upload data. - contents.file.seek(0) - data = contents.file.read() - #uploadable = FileHandle(contents.file) - d = self._check_replacement(name) - d.addCallback(lambda res: self._node.has_child(name)) - def _checked(present): - if present: - # modify the existing one instead of creating a new - # one - d2 = self._node.get(name) - def _got_newnode(newnode): - d3 = newnode.overwrite(data) - d3.addCallback(lambda res: newnode.get_uri()) - return d3 - d2.addCallback(_got_newnode) - else: - d2 = client.create_mutable_file(data) - def _uploaded(newnode): - d1 = self._node.set_node(name, newnode) - d1.addCallback(lambda res: newnode.get_uri()) - return d1 - d2.addCallback(_uploaded) - return d2 - d.addCallback(_checked) - else: - uploadable = FileHandle(contents.file, convergence=client.convergence) - d = self._check_replacement(name) - d.addCallback(lambda res: self._node.add_file(name, uploadable)) - def _done(newnode): - return newnode.get_uri() - d.addCallback(_done) - return d - - def _POST_overwrite(self, contents): - # SDMF: files are small, and we can only upload data. - contents.file.seek(0) - data = contents.file.read() - # TODO: 'name' handling needs review - d = defer.succeed(self._node) - def _got_child_overwrite(child_node): - child_node.overwrite(data) - return child_node.get_uri() - d.addCallback(_got_child_overwrite) - return d - - def _POST_check(self, name): - d = self._node.get(name) - def _got_child_check(child_node): - d2 = child_node.check() - def _done(res): - log.msg("checked %s, results %s" % (child_node, res), - facility="tahoe.webish", level=log.NOISY) - return str(res) - d2.addCallback(_done) - return d2 - d.addCallback(_got_child_check) - return d - - def _POST_set_children(self, children): - cs = [] - for name, (file_or_dir, mddict) in children.iteritems(): - cap = str(mddict.get('rw_uri') or mddict.get('ro_uri')) - cs.append((name, cap, mddict.get('metadata'))) - - d = self._node.set_children(cs) - d.addCallback(lambda res: "Okay so I did it.") - return d - - def renderHTTP(self, ctx): - req = inevow.IRequest(ctx) - - t = get_arg(req, "t") - assert t is not None - - charset = get_arg(req, "_charset", "utf-8") - - name = get_arg(req, "name", None) - if name and "/" in name: - req.setResponseCode(http.BAD_REQUEST) - req.setHeader("content-type", "text/plain") - return "name= may not contain a slash" - if name is not None: - name = name.strip() - name = name.decode(charset) - assert isinstance(name, unicode) - # we allow the user to delete an empty-named file, but not to create - # them, since that's an easy and confusing mistake to make - - when_done = get_arg(req, "when_done", None) - if not boolean_of_arg(get_arg(req, "replace", "true")): - self._replace = False - - if t == "mkdir": - if not name: - raise RuntimeError("mkdir requires a name") - d = self._POST_mkdir(name) - elif t == "mkdir-p": - path = get_arg(req, "path") - if not path: - raise RuntimeError("mkdir-p requires a path") - d = self._POST_mkdir_p(path) - elif t == "uri": - if not name: - raise RuntimeError("set-uri requires a name") - newuri = get_arg(req, "uri") - assert newuri is not None - d = self._POST_uri(name, newuri) - elif t == "delete": - d = self._POST_delete(name) - elif t == "rename": - 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) - 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) - if not from_name or not to_name: - raise RuntimeError("rename requires from_name and to_name") - if not IDirectoryNode.providedBy(self._node): - raise RuntimeError("rename must only be called on directories") - for k,v in [ ('from_name', from_name), ('to_name', to_name) ]: - if v and "/" in v: - req.setResponseCode(http.BAD_REQUEST) - req.setHeader("content-type", "text/plain") - return "%s= may not contain a slash" % (k,) - d = self._POST_rename(name, from_name, to_name) - elif t == "upload": - contents = req.fields["file"] - 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 RuntimeError("upload requires a name") - name = name.decode(charset) - assert isinstance(name, unicode) - mutable = boolean_of_arg(get_arg(req, "mutable", "false")) - d = self._POST_upload(contents, name, mutable, IClient(ctx)) - elif t == "overwrite": - contents = req.fields["file"] - d = self._POST_overwrite(contents) - elif t == "check": - d = self._POST_check(name) - elif t == "set_children": - req.content.seek(0) - body = req.content.read() - try: - children = simplejson.loads(body) - except ValueError, le: - le.args = tuple(le.args + (body,)) - # TODO test handling of bad JSON - raise - d = self._POST_set_children(children) - else: - req.setResponseCode(http.BAD_REQUEST) - req.setHeader("content-type", "text/plain") - return "BAD t=%s" % t - if when_done: - d.addCallback(lambda res: url.URL.fromString(when_done)) - def _check_replacement(f): - # TODO: make this more human-friendly: maybe send them to the - # when_done page but with an extra query-arg that will display - # the error message in a big box at the top of the page. The - # directory page that when_done= usually points to accepts a - # result= argument.. use that. - f.trap(NoReplacementError) - req.setResponseCode(http.CONFLICT) - req.setHeader("content-type", "text/plain") - return str(f.value) - d.addErrback(_check_replacement) - return d - -class DELETEHandler(rend.Page): - def __init__(self, node, name): - self._node = node - self._name = name - - def renderHTTP(self, ctx): - req = inevow.IRequest(ctx) - d = self._node.delete(self._name) - def _done(res): - # what should this return?? - return "%s deleted" % self._name.encode("utf-8") - d.addCallback(_done) - def _trap_missing(f): - f.trap(KeyError) - req.setResponseCode(http.NOT_FOUND) - req.setHeader("content-type", "text/plain") - return "no such child %s" % self._name.encode("utf-8") - d.addErrback(_trap_missing) - return d - -class PUTHandler(rend.Page): - def __init__(self, node, path, t, localfile, localdir, replace): - self._node = node - self._path = path - self._t = t - self._localfile = localfile - self._localdir = localdir - self._replace = replace - - def renderHTTP(self, ctx): - client = IClient(ctx) - req = inevow.IRequest(ctx) - t = self._t - localfile = self._localfile - localdir = self._localdir - - if t == "upload" and not (localfile or localdir): - req.setResponseCode(http.BAD_REQUEST) - req.setHeader("content-type", "text/plain") - return "t=upload requires localfile= or localdir=" - - # we must traverse the path, creating new directories as necessary - d = self._get_or_create_directories(self._node, self._path[:-1]) - name = self._path[-1] - d.addCallback(self._check_replacement, name, self._replace) - if t == "upload": - if localfile: - d.addCallback(self._upload_localfile, localfile, name, convergence=client.convergence) - else: - # localdir - # take the last step - d.addCallback(self._get_or_create_directories, self._path[-1:]) - d.addCallback(self._upload_localdir, localdir, convergence=client.convergence) - elif t == "uri": - d.addCallback(self._attach_uri, req.content, name) - elif t == "mkdir": - d.addCallback(self._mkdir, name) - else: - d.addCallback(self._upload_file, req.content, name, convergence=client.convergence) - - def _transform_error(f): - errors = {BlockingFileError: http.BAD_REQUEST, - NoReplacementError: http.CONFLICT, - NoLocalDirectoryError: http.BAD_REQUEST, - } - for k,v in errors.items(): - if f.check(k): - req.setResponseCode(v) - req.setHeader("content-type", "text/plain") - return str(f.value) - return f - d.addErrback(_transform_error) - return d - - def _get_or_create_directories(self, node, path): - if not IDirectoryNode.providedBy(node): - # unfortunately it is too late to provide the name of the - # blocking directory in the error message. - raise BlockingFileError("cannot create directory because there " - "is a file in the way") - if not path: - return defer.succeed(node) - d = node.get(path[0]) - def _maybe_create(f): - f.trap(KeyError) - return node.create_empty_directory(path[0]) - d.addErrback(_maybe_create) - d.addCallback(self._get_or_create_directories, path[1:]) - return d - - def _check_replacement(self, node, name, replace): - if replace: - return node - d = node.has_child(name) - def _got(present): - if present: - raise NoReplacementError("There was already a child by that " - "name, and you asked me to not " - "replace it.") - return node - d.addCallback(_got) - return d - - def _mkdir(self, node, name): - d = node.create_empty_directory(name) - def _done(newnode): - return newnode.get_uri() - d.addCallback(_done) - return d - - def _upload_file(self, node, contents, name, convergence): - uploadable = FileHandle(contents, convergence=convergence) - d = node.add_file(name, uploadable) - def _done(filenode): - log.msg("webish upload complete", - facility="tahoe.webish", level=log.NOISY) - return filenode.get_uri() - d.addCallback(_done) - return d - - def _upload_localfile(self, node, localfile, name, convergence): - uploadable = FileName(localfile, convergence=convergence) - d = node.add_file(name, uploadable) - d.addCallback(lambda filenode: filenode.get_uri()) - return d - - def _attach_uri(self, parentnode, contents, name): - newuri = contents.read().strip() - d = parentnode.set_uri(name, newuri) - def _done(res): - return newuri - d.addCallback(_done) - return d - - def _upload_localdir(self, node, localdir, convergence): - # build up a list of files to upload. TODO: for now, these files and - # directories must have UTF-8 encoded filenames: anything else will - # cause the upload to break. - all_files = [] - all_dirs = [] - msg = "No files to upload! %s is empty" % localdir - if not os.path.exists(localdir): - msg = "%s doesn't exist!" % localdir - raise NoLocalDirectoryError(msg) - for root, dirs, files in os.walk(localdir): - if root == localdir: - path = () - else: - relative_root = root[len(localdir)+1:] - path = tuple(relative_root.split(os.sep)) - for d in dirs: - this_dir = path + (d,) - this_dir = tuple([p.decode("utf-8") for p in this_dir]) - all_dirs.append(this_dir) - for f in files: - this_file = path + (f,) - this_file = tuple([p.decode("utf-8") for p in this_file]) - all_files.append(this_file) - d = defer.succeed(msg) - for dir in all_dirs: - if dir: - d.addCallback(self._makedir, node, dir) - for f in all_files: - d.addCallback(self._upload_one_file, node, localdir, f, convergence=convergence) - return d - - def _makedir(self, res, node, dir): - d = defer.succeed(None) - # get the parent. As long as os.walk gives us parents before - # children, this ought to work - d.addCallback(lambda res: node.get_child_at_path(dir[:-1])) - # then create the child directory - d.addCallback(lambda parent: parent.create_empty_directory(dir[-1])) - return d - - def _upload_one_file(self, res, node, localdir, f, convergence): - # get the parent. We can be sure this exists because we already - # went through and created all the directories we require. - localfile = os.path.join(localdir, *f) - d = node.get_child_at_path(f[:-1]) - d.addCallback(self._upload_localfile, localfile, f[-1], convergence=convergence) - return d - - -class Manifest(rend.Page): - docFactory = getxmlfile("manifest.xhtml") - def __init__(self, dirnode, dirpath): - self._dirnode = dirnode - self._dirpath = dirpath - - def dirpath_as_string(self): - return "/" + "/".join(self._dirpath) - - def render_title(self, ctx): - return T.title["Manifest of %s" % self.dirpath_as_string()] - - def render_header(self, ctx): - return T.p["Manifest of %s" % self.dirpath_as_string()] - - def data_items(self, ctx, data): - return self._dirnode.build_manifest() - - def render_row(self, ctx, refresh_cap): - ctx.fillSlots("refresh_capability", refresh_cap) - return ctx.tag - -class DeepSize(rend.Page): - - def __init__(self, dirnode, dirpath): - self._dirnode = dirnode - self._dirpath = dirpath - - def renderHTTP(self, ctx): - inevow.IRequest(ctx).setHeader("content-type", "text/plain") - d = self._dirnode.build_manifest() - def _measure_size(manifest): - total = 0 - for verifiercap in manifest: - u = from_string_verifier(verifiercap) - if isinstance(u, CHKFileVerifierURI): - total += u.size - return str(total) - d.addCallback(_measure_size) - return d - -class DeepStats(rend.Page): - - def __init__(self, dirnode, dirpath): - self._dirnode = dirnode - self._dirpath = dirpath - - def renderHTTP(self, ctx): - inevow.IRequest(ctx).setHeader("content-type", "text/plain") - d = self._dirnode.deep_stats() - d.addCallback(simplejson.dumps, indent=1) - return d - -class ChildError: - implements(inevow.IResource) - def renderHTTP(self, ctx): - req = inevow.IRequest(ctx) - req.setResponseCode(http.BAD_REQUEST) - req.setHeader("content-type", "text/plain") - return self.text - -def child_error(text): - ce = ChildError() - ce.text = text - return ce, () - -class VDrive(rend.Page): - - def __init__(self, node, name): - self.node = node - self.name = name - - def get_child_at_path(self, path): - if path: - return self.node.get_child_at_path(path) - return defer.succeed(self.node) - - def locateChild(self, ctx, segments): - req = inevow.IRequest(ctx) - method = req.method - path = tuple([seg.decode("utf-8") for seg in segments]) - - t = get_arg(req, "t", "") - localfile = get_arg(req, "localfile", None) - if localfile is not None: - if localfile != os.path.abspath(localfile): - return NeedAbsolutePathError(), () - localdir = get_arg(req, "localdir", None) - if localdir is not None: - if localdir != os.path.abspath(localdir): - return NeedAbsolutePathError(), () - if localfile or localdir: - if not ILocalAccess(ctx).local_access_is_allowed(): - return LocalAccessDisabledError(), () - if req.getHost().host != LOCALHOST: - return NeedLocalhostError(), () - # TODO: think about clobbering/revealing config files and node secrets - - replace = boolean_of_arg(get_arg(req, "replace", "true")) - - if method == "GET": - # the node must exist, and our operation will be performed on the - # node itself. - d = self.get_child_at_path(path) - def file_or_dir(node): - if (IFileNode.providedBy(node) - or IMutableFileNode.providedBy(node)): - filename = "unknown" - if path: - filename = path[-1] - filename = get_arg(req, "filename", filename) - if t == "download": - if localfile: - # write contents to a local file - return LocalFileDownloader(node, localfile), () - # send contents as the result - return FileDownloader(node, filename), () - elif t == "": - # send contents as the result - return FileDownloader(node, filename), () - elif t == "json": - return FileJSONMetadata(node), () - elif t == "uri": - return FileURI(node), () - elif t == "readonly-uri": - return FileReadOnlyURI(node), () - else: - return child_error("bad t=%s" % t) - elif IDirectoryNode.providedBy(node): - if t == "download": - if localdir: - # recursive download to a local directory - return LocalDirectoryDownloader(node, localdir), () - return child_error("t=download requires localdir=") - elif t == "": - # send an HTML representation of the directory - return Directory(self.name, node, path), () - elif t == "json": - return DirectoryJSONMetadata(node), () - elif t == "uri": - return DirectoryURI(node), () - elif t == "readonly-uri": - return DirectoryReadonlyURI(node), () - elif t == "manifest": - return Manifest(node, path), () - elif t == "deep-size": - return DeepSize(node, path), () - elif t == "deep-stats": - return DeepStats(node, path), () - elif t == 'rename-form': - return RenameForm(self.name, node, path), () - else: - return child_error("bad t=%s" % t) - else: - return child_error("unknown node type") - d.addCallback(file_or_dir) - elif method == "POST": - # the node must exist, and our operation will be performed on the - # node itself. - d = self.get_child_at_path(path) - def _got_POST(node): - return POSTHandler(node, replace), () - d.addCallback(_got_POST) - elif method == "DELETE": - # the node must exist, and our operation will be performed on its - # parent node. - assert path # you can't delete the root - d = self.get_child_at_path(path[:-1]) - def _got_DELETE(node): - return DELETEHandler(node, path[-1]), () - d.addCallback(_got_DELETE) - elif method in ("PUT",): - # the node may or may not exist, and our operation may involve - # all the ancestors of the node. - return PUTHandler(self.node, path, t, localfile, localdir, replace), () - else: - return rend.NotFound - return d - -class Root(rend.Page): - - addSlash = True - docFactory = getxmlfile("welcome.xhtml") - - def locateChild(self, ctx, segments): - client = IClient(ctx) - req = inevow.IRequest(ctx) - - if not segments: - return rend.Page.locateChild(self, ctx, segments) - - if segments[0] == "file": - if len(segments) < 2: - return rend.Page.locateChild(self, ctx, segments) - filecap = segments[1] - node = client.create_node_from_uri(filecap) - name = segments[-1] - return FileDownloader(node, name), () - - if segments[0] != "uri": - return rend.Page.locateChild(self, ctx, segments) - - segments = list(segments) - while segments and not segments[-1]: - segments.pop() - if not segments: - segments.append('') - segments = tuple(segments) - - if len(segments) == 1 or segments[1] == '': - uri = get_arg(req, "uri", None) - if uri is not None: - there = url.URL.fromContext(ctx) - there = there.clear("uri") - there = there.child("uri").child(uri) - return there, () - - if len(segments) == 1: - # /uri - if req.method == "PUT": - # either "PUT /uri" to create an unlinked file, or - # "PUT /uri?t=mkdir" to create an unlinked directory - t = get_arg(req, "t", "").strip() - if t == "": - mutable = bool(get_arg(req, "mutable", "").strip()) - if mutable: - return unlinked.UnlinkedPUTSSKUploader(), () - else: - return unlinked.UnlinkedPUTCHKUploader(), () - if t == "mkdir": - return unlinked.UnlinkedPUTCreateDirectory(), () - errmsg = "/uri only accepts PUT and PUT?t=mkdir" - return WebError(http.BAD_REQUEST, errmsg), () - - elif req.method == "POST": - # "POST /uri?t=upload&file=newfile" to upload an - # unlinked file or "POST /uri?t=mkdir" to create a - # new directory - t = get_arg(req, "t", "").strip() - if t in ("", "upload"): - mutable = bool(get_arg(req, "mutable", "").strip()) - if mutable: - return unlinked.UnlinkedPOSTSSKUploader(), () - else: - return unlinked.UnlinkedPOSTCHKUploader(client, req), () - if t == "mkdir": - return unlinked.UnlinkedPOSTCreateDirectory(), () - errmsg = "/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, and POST?t=mkdir" - return WebError(http.BAD_REQUEST, errmsg), () - - if len(segments) < 2: - return rend.NotFound - - uri = segments[1] - d = defer.maybeDeferred(client.create_node_from_uri, uri) - d.addCallback(lambda node: VDrive(node, uri)) - d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:])) - def _trap_KeyError(f): - f.trap(KeyError) - return rend.FourOhFour(), () - d.addErrback(_trap_KeyError) - return d - - child_webform_css = webform.defaultCSS - child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css')) - - child_provisioning = provisioning.ProvisioningTool() - child_status = status.Status() - child_helper_status = status.HelperStatus() - child_statistics = status.Statistics() - - def data_version(self, ctx, data): - return get_package_versions_string() - def data_import_path(self, ctx, data): - return str(allmydata) - def data_my_nodeid(self, ctx, data): - return idlib.nodeid_b2a(IClient(ctx).nodeid) - - def render_services(self, ctx, data): - ul = T.ul() - client = IClient(ctx) - try: - ss = client.getServiceNamed("storage") - allocated_s = abbreviate_size(ss.allocated_size()) - allocated = "about %s allocated" % allocated_s - sizelimit = "no size limit" - if ss.sizelimit is not None: - sizelimit = "size limit is %s" % abbreviate_size(ss.sizelimit) - ul[T.li["Storage Server: %s, %s" % (allocated, sizelimit)]] - except KeyError: - ul[T.li["Not running storage server"]] - - try: - h = client.getServiceNamed("helper") - stats = h.get_stats() - active_uploads = stats["chk_upload_helper.active_uploads"] - ul[T.li["Helper: %d active uploads" % (active_uploads,)]] - except KeyError: - ul[T.li["Not running helper"]] - - return ctx.tag[ul] - - def data_introducer_furl(self, ctx, data): - return IClient(ctx).introducer_furl - def data_connected_to_introducer(self, ctx, data): - if IClient(ctx).connected_to_introducer(): - return "yes" - return "no" - - def data_helper_furl(self, ctx, data): - try: - uploader = IClient(ctx).getServiceNamed("uploader") - except KeyError: - return None - furl, connected = uploader.get_helper_info() - return furl - def data_connected_to_helper(self, ctx, data): - try: - uploader = IClient(ctx).getServiceNamed("uploader") - except KeyError: - return "no" # we don't even have an Uploader - furl, connected = uploader.get_helper_info() - if connected: - return "yes" - return "no" - - def data_known_storage_servers(self, ctx, data): - ic = IClient(ctx).introducer_client - servers = [c - for c in ic.get_all_connectors().values() - if c.service_name == "storage"] - return len(servers) - - def data_connected_storage_servers(self, ctx, data): - ic = IClient(ctx).introducer_client - return len(ic.get_all_connections_for("storage")) - - def data_services(self, ctx, data): - ic = IClient(ctx).introducer_client - c = [ (service_name, nodeid, rsc) - for (nodeid, service_name), rsc - in ic.get_all_connectors().items() ] - c.sort() - return c - - def render_service_row(self, ctx, data): - (service_name, nodeid, rsc) = data - ctx.fillSlots("peerid", "%s %s" % (idlib.nodeid_b2a(nodeid), - rsc.nickname)) - if rsc.rref: - rhost = rsc.remote_host - if nodeid == IClient(ctx).nodeid: - rhost_s = "(loopback)" - elif isinstance(rhost, address.IPv4Address): - rhost_s = "%s:%d" % (rhost.host, rhost.port) - else: - rhost_s = str(rhost) - connected = "Yes: to " + rhost_s - since = rsc.last_connect_time - else: - connected = "No" - since = rsc.last_loss_time - - TIME_FORMAT = "%H:%M:%S %d-%b-%Y" - ctx.fillSlots("connected", connected) - ctx.fillSlots("since", time.strftime(TIME_FORMAT, time.localtime(since))) - ctx.fillSlots("announced", time.strftime(TIME_FORMAT, - time.localtime(rsc.announcement_time))) - ctx.fillSlots("version", rsc.version) - ctx.fillSlots("service_name", rsc.service_name) - - return ctx.tag - - 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"], - "URI to download: ", - T.input(type="text", name="uri"), " ", - "Filename to download as: ", - T.input(type="text", name="filename"), " ", - 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"], - "URI to view: ", - T.input(type="text", name="uri"), " ", - T.input(type="submit", value="View!"), - ]] - return T.div[form] - - def render_upload_form(self, ctx, data): - # this is a form where users can upload unlinked files - form = T.form(action="uri", method="post", - enctype="multipart/form-data")[ - T.fieldset[ - T.legend(class_="freeform-form-label")["Upload a file"], - "Choose a file: ", - T.input(type="file", name="file", class_="freeform-input-file"), - T.input(type="hidden", name="t", value="upload"), - " Mutable?:", T.input(type="checkbox", name="mutable"), - T.input(type="submit", value="Upload!"), - ]] - return T.div[form] - - def render_mkdir_form(self, ctx, data): - # this is a form where users can create new directories - form = T.form(action="uri", method="post", - enctype="multipart/form-data")[ - T.fieldset[ - T.legend(class_="freeform-form-label")["Create a directory"], - T.input(type="hidden", name="t", value="mkdir"), - T.input(type="hidden", name="redirect_to_result", value="true"), - T.input(type="submit", value="Create Directory!"), - ]] - return T.div[form] - - -class LocalAccess: - implements(ILocalAccess) - def __init__(self): - self.local_access = False - def local_access_is_allowed(self): - return self.local_access class WebishServer(service.MultiService): name = "webish" - root_class = Root + root_class = root.Root def __init__(self, webport, nodeurl_path=None): service.MultiService.__init__(self) @@ -1633,8 +123,6 @@ class WebishServer(service.MultiService): self.root = self.root_class() self.site = site = appserver.NevowSite(self.root) self.site.requestFactory = MyRequest - self.allow_local = LocalAccess() - self.site.remember(self.allow_local, ILocalAccess) s = strports.service(webport, site) s.setServiceParent(self) self.listener = s # stash it so the tests can query for the portnum @@ -1642,9 +130,6 @@ class WebishServer(service.MultiService): if nodeurl_path: self._started.addCallback(self._write_nodeurl_file, nodeurl_path) - def allow_local_access(self, enable=True): - self.allow_local.local_access = enable - def startService(self): service.MultiService.startService(self) # to make various services available to render_* methods, we stash a @@ -1655,6 +140,7 @@ class WebishServer(service.MultiService): # I thought you could do the same with an existing interface, but # apparently 'ISite' does not exist #self.site._client = self.parent + self.site.remember(MyExceptionHandler(), inevow.ICanHandleException) self._started.callback(None) def _write_nodeurl_file(self, junk, nodeurl_path):