webish: complete rewrite, break into smaller pieces, auto-create directories, improve error handling

This commit is contained in:
Brian Warner 2008-05-19 12:57:04 -07:00
parent b8c0217ad5
commit f9cd30d9bc
6 changed files with 1596 additions and 1610 deletions

View File

@ -1,7 +1,10 @@
from twisted.web import http, server
from zope.interface import Interface 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 nevow.util import resource_filename
from allmydata.interfaces import ExistingChildError
class IClient(Interface): class IClient(Interface):
pass pass
@ -11,6 +14,7 @@ def getxmlfile(name):
return loaders.xmlfile(resource_filename('allmydata.web', '%s' % name)) return loaders.xmlfile(resource_filename('allmydata.web', '%s' % name))
def boolean_of_arg(arg): def boolean_of_arg(arg):
# TODO: ""
assert arg.lower() in ("true", "t", "1", "false", "f", "0", "on", "off") assert arg.lower() in ("true", "t", "1", "false", "f", "0", "on", "off")
return arg.lower() in ("true", "t", "1", "on") return arg.lower() in ("true", "t", "1", "on")
@ -68,3 +72,56 @@ def abbreviate_size(data):
if r > 1000: if r > 1000:
return "%.1fkB" % (r/1000) return "%.1fkB" % (r/1000)
return "%dB" % r 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)

View File

@ -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 <input type="hidden" name="name" value="">
# 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

View File

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

293
src/allmydata/web/root.py Normal file
View File

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

View File

@ -1,31 +1,25 @@
import urllib import urllib
from twisted.web import http 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.upload import FileHandle
from allmydata.web.common import IClient, getxmlfile, get_arg, boolean_of_arg from allmydata.web.common import IClient, getxmlfile, get_arg, boolean_of_arg
from allmydata.web import status 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.
def PUTUnlinkedCHK(ctx):
req = IRequest(ctx)
# "PUT /uri", to create an unlinked file.
client = IClient(ctx) client = IClient(ctx)
uploadable = FileHandle(req.content, client.convergence) uploadable = FileHandle(req.content, client.convergence)
d = client.upload(uploadable) d = client.upload(uploadable)
d.addCallback(lambda results: results.uri) d.addCallback(lambda results: results.uri)
# that fires with the URI of the new file # that fires with the URI of the new file
return d return d
class UnlinkedPUTSSKUploader(rend.Page): def PUTUnlinkedSSK(ctx):
def renderHTTP(self, ctx): req = IRequest(ctx)
req = inevow.IRequest(ctx)
assert req.method == "PUT"
# SDMF: files are small, and we can only upload data # SDMF: files are small, and we can only upload data
req.content.seek(0) req.content.seek(0)
data = req.content.read() data = req.content.read()
@ -33,44 +27,47 @@ class UnlinkedPUTSSKUploader(rend.Page):
d.addCallback(lambda n: n.get_uri()) d.addCallback(lambda n: n.get_uri())
return d return d
class UnlinkedPUTCreateDirectory(rend.Page): def PUTUnlinkedCreateDirectory(ctx):
def renderHTTP(self, ctx): req = IRequest(ctx)
req = inevow.IRequest(ctx)
assert req.method == "PUT"
# "PUT /uri?t=mkdir", to create an unlinked directory. # "PUT /uri?t=mkdir", to create an unlinked directory.
d = IClient(ctx).create_empty_dirnode() d = IClient(ctx).create_empty_dirnode()
d.addCallback(lambda dirnode: dirnode.get_uri()) d.addCallback(lambda dirnode: dirnode.get_uri())
# XXX add redirect_to_result # XXX add redirect_to_result
return d return d
class UnlinkedPOSTCHKUploader(status.UploadResultsRendererMixin, rend.Page):
"""'POST /uri', to create an unlinked file."""
docFactory = getxmlfile("upload-results.xhtml")
def __init__(self, client, req): def POSTUnlinkedCHK(ctx):
rend.Page.__init__(self) req = IRequest(ctx)
# we start the upload now, and distribute notification of its client = IClient(ctx)
# completion to render_ methods with an ObserverList
assert req.method == "POST"
self._done = observer.OneShotObserverList()
fileobj = req.fields["file"].file fileobj = req.fields["file"].file
uploadable = FileHandle(fileobj, client.convergence) uploadable = FileHandle(fileobj, client.convergence)
d = client.upload(uploadable) 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) when_done = get_arg(req, "when_done", None)
if when_done: if when_done:
# if when_done= is provided, return a redirect instead of our # if when_done= is provided, return a redirect instead of our
# usual upload-results page # usual upload-results page
d = self._done.when_fired() def _done(upload_results, redir_to):
d.addCallback(lambda res: url.URL.fromString(when_done)) 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 return d
return rend.Page.renderHTTP(self, ctx)
class UploadResultsPage(status.UploadResultsRendererMixin, rend.Page):
"""'POST /uri', to create an unlinked file."""
docFactory = getxmlfile("upload-results.xhtml")
def __init__(self, upload_results, ctx):
rend.Page.__init__(self)
self.results = upload_results
def upload_results(self): def upload_results(self):
return self._done.when_fired() return defer.succeed(self.results)
def data_done(self, ctx, data): def data_done(self, ctx, data):
d = self.upload_results() d = self.upload_results()
@ -88,11 +85,8 @@ class UnlinkedPOSTCHKUploader(status.UploadResultsRendererMixin, rend.Page):
["/uri/" + res.uri]) ["/uri/" + res.uri])
return d return d
class UnlinkedPOSTSSKUploader(rend.Page): def POSTUnlinkedSSK(ctx):
def renderHTTP(self, ctx): req = IRequest(ctx)
req = inevow.IRequest(ctx)
assert req.method == "POST"
# "POST /uri", to create an unlinked file. # "POST /uri", to create an unlinked file.
# SDMF: files are small, and we can only upload data # SDMF: files are small, and we can only upload data
contents = req.fields["file"] contents = req.fields["file"]
@ -102,11 +96,8 @@ class UnlinkedPOSTSSKUploader(rend.Page):
d.addCallback(lambda n: n.get_uri()) d.addCallback(lambda n: n.get_uri())
return d return d
class UnlinkedPOSTCreateDirectory(rend.Page): def POSTUnlinkedCreateDirectory(ctx):
def renderHTTP(self, ctx): req = IRequest(ctx)
req = inevow.IRequest(ctx)
assert req.method == "POST"
# "POST /uri?t=mkdir", to create an unlinked directory. # "POST /uri?t=mkdir", to create an unlinked directory.
d = IClient(ctx).create_empty_dirnode() d = IClient(ctx).create_empty_dirnode()
redirect = get_arg(req, "redirect_to_result", "false") redirect = get_arg(req, "redirect_to_result", "false")

File diff suppressed because it is too large Load Diff