webish: complete rewrite, break into smaller pieces, auto-create directories, improve error handling
This commit is contained in:
parent
b8c0217ad5
commit
f9cd30d9bc
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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]
|
||||||
|
|
|
@ -1,76 +1,73 @@
|
||||||
|
|
||||||
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 PUTUnlinkedCHK(ctx):
|
||||||
def renderHTTP(self, ctx):
|
req = IRequest(ctx)
|
||||||
req = inevow.IRequest(ctx)
|
# "PUT /uri", to create an unlinked file.
|
||||||
assert req.method == "PUT"
|
client = IClient(ctx)
|
||||||
# "PUT /uri", to create an unlinked file. This is like PUT but
|
uploadable = FileHandle(req.content, client.convergence)
|
||||||
# without the associated set_uri.
|
d = client.upload(uploadable)
|
||||||
|
d.addCallback(lambda results: results.uri)
|
||||||
|
# that fires with the URI of the new file
|
||||||
|
return d
|
||||||
|
|
||||||
client = IClient(ctx)
|
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
|
||||||
|
|
||||||
uploadable = FileHandle(req.content, client.convergence)
|
def PUTUnlinkedCreateDirectory(ctx):
|
||||||
d = client.upload(uploadable)
|
req = IRequest(ctx)
|
||||||
d.addCallback(lambda results: results.uri)
|
# "PUT /uri?t=mkdir", to create an unlinked directory.
|
||||||
# that fires with the URI of the new file
|
d = IClient(ctx).create_empty_dirnode()
|
||||||
return d
|
d.addCallback(lambda dirnode: dirnode.get_uri())
|
||||||
|
# XXX add redirect_to_result
|
||||||
|
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 POSTUnlinkedCHK(ctx):
|
||||||
def renderHTTP(self, ctx):
|
req = IRequest(ctx)
|
||||||
req = inevow.IRequest(ctx)
|
client = IClient(ctx)
|
||||||
assert req.method == "PUT"
|
fileobj = req.fields["file"].file
|
||||||
# "PUT /uri?t=mkdir", to create an unlinked directory.
|
uploadable = FileHandle(fileobj, client.convergence)
|
||||||
d = IClient(ctx).create_empty_dirnode()
|
d = client.upload(uploadable)
|
||||||
d.addCallback(lambda dirnode: dirnode.get_uri())
|
when_done = get_arg(req, "when_done", None)
|
||||||
# XXX add redirect_to_result
|
if when_done:
|
||||||
return d
|
# 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 UnlinkedPOSTCHKUploader(status.UploadResultsRendererMixin, rend.Page):
|
|
||||||
|
class UploadResultsPage(status.UploadResultsRendererMixin, rend.Page):
|
||||||
"""'POST /uri', to create an unlinked file."""
|
"""'POST /uri', to create an unlinked file."""
|
||||||
docFactory = getxmlfile("upload-results.xhtml")
|
docFactory = getxmlfile("upload-results.xhtml")
|
||||||
|
|
||||||
def __init__(self, client, req):
|
def __init__(self, upload_results, ctx):
|
||||||
rend.Page.__init__(self)
|
rend.Page.__init__(self)
|
||||||
# we start the upload now, and distribute notification of its
|
self.results = upload_results
|
||||||
# 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)
|
|
||||||
|
|
||||||
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,37 +85,31 @@ 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)
|
# "POST /uri", to create an unlinked file.
|
||||||
assert req.method == "POST"
|
# 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
|
||||||
|
|
||||||
# "POST /uri", to create an unlinked file.
|
def POSTUnlinkedCreateDirectory(ctx):
|
||||||
# SDMF: files are small, and we can only upload data
|
req = IRequest(ctx)
|
||||||
contents = req.fields["file"]
|
# "POST /uri?t=mkdir", to create an unlinked directory.
|
||||||
contents.file.seek(0)
|
d = IClient(ctx).create_empty_dirnode()
|
||||||
data = contents.file.read()
|
redirect = get_arg(req, "redirect_to_result", "false")
|
||||||
d = IClient(ctx).create_mutable_file(data)
|
if boolean_of_arg(redirect):
|
||||||
d.addCallback(lambda n: n.get_uri())
|
def _then_redir(res):
|
||||||
return d
|
new_url = "uri/" + urllib.quote(res.get_uri())
|
||||||
|
req.setResponseCode(http.SEE_OTHER) # 303
|
||||||
class UnlinkedPOSTCreateDirectory(rend.Page):
|
req.setHeader('location', new_url)
|
||||||
def renderHTTP(self, ctx):
|
req.finish()
|
||||||
req = inevow.IRequest(ctx)
|
return ''
|
||||||
assert req.method == "POST"
|
d.addCallback(_then_redir)
|
||||||
|
else:
|
||||||
# "POST /uri?t=mkdir", to create an unlinked directory.
|
d.addCallback(lambda dirnode: dirnode.get_uri())
|
||||||
d = IClient(ctx).create_empty_dirnode()
|
return d
|
||||||
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
|
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue