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 nevow import loaders
|
||||
from nevow import loaders, appserver
|
||||
from nevow.inevow import IRequest
|
||||
from nevow.util import resource_filename
|
||||
from allmydata.interfaces import ExistingChildError
|
||||
|
||||
class IClient(Interface):
|
||||
pass
|
||||
|
@ -11,6 +14,7 @@ def getxmlfile(name):
|
|||
return loaders.xmlfile(resource_filename('allmydata.web', '%s' % name))
|
||||
|
||||
def boolean_of_arg(arg):
|
||||
# TODO: ""
|
||||
assert arg.lower() in ("true", "t", "1", "false", "f", "0", "on", "off")
|
||||
return arg.lower() in ("true", "t", "1", "on")
|
||||
|
||||
|
@ -68,3 +72,56 @@ def abbreviate_size(data):
|
|||
if r > 1000:
|
||||
return "%.1fkB" % (r/1000)
|
||||
return "%dB" % r
|
||||
|
||||
def text_plain(text, ctx):
|
||||
req = IRequest(ctx)
|
||||
req.setHeader("content-type", "text/plain")
|
||||
req.setHeader("content-length", len(text))
|
||||
return text
|
||||
|
||||
class WebError(Exception):
|
||||
def __init__(self, text, code=http.BAD_REQUEST):
|
||||
self.text = text
|
||||
self.code = code
|
||||
|
||||
# XXX: to make UnsupportedMethod return 501 NOT_IMPLEMENTED instead of 500
|
||||
# Internal Server Error, we either need to do that ICanHandleException trick,
|
||||
# or make sure that childFactory returns a WebErrorResource (and never an
|
||||
# actual exception). The latter is growing increasingly annoying.
|
||||
|
||||
def should_create_intermediate_directories(req):
|
||||
t = get_arg(req, "t", "").strip()
|
||||
return bool(req.method in ("PUT", "POST") and
|
||||
t not in ("delete", "rename", "rename-form", "check"))
|
||||
|
||||
|
||||
class MyExceptionHandler(appserver.DefaultExceptionHandler):
|
||||
def simple(self, ctx, text, code=http.BAD_REQUEST):
|
||||
req = IRequest(ctx)
|
||||
req.setResponseCode(code)
|
||||
req.setHeader("content-type", "text/plain;charset=utf-8")
|
||||
if isinstance(text, unicode):
|
||||
text = text.encode("utf-8")
|
||||
req.write(text)
|
||||
req.finishRequest(False)
|
||||
|
||||
def renderHTTP_exception(self, ctx, f):
|
||||
if f.check(ExistingChildError):
|
||||
return self.simple(ctx,
|
||||
"There was already a child by that "
|
||||
"name, and you asked me to not "
|
||||
"replace it.",
|
||||
http.CONFLICT)
|
||||
elif f.check(WebError):
|
||||
return self.simple(ctx, f.value.text, f.value.code)
|
||||
elif f.check(server.UnsupportedMethod):
|
||||
# twisted.web.server.Request.render() has support for transforming
|
||||
# this into an appropriate 501 NOT_IMPLEMENTED or 405 NOT_ALLOWED
|
||||
# return code, but nevow does not.
|
||||
req = IRequest(ctx)
|
||||
method = req.method
|
||||
return self.simple(ctx,
|
||||
"I don't know how to treat a %s request." % method,
|
||||
http.NOT_IMPLEMENTED)
|
||||
super = appserver.DefaultExceptionHandler
|
||||
return super.renderHTTP_exception(self, ctx, f)
|
||||
|
|
|
@ -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,31 +1,25 @@
|
|||
|
||||
import urllib
|
||||
from twisted.web import http
|
||||
from nevow import rend, inevow, url, tags as T
|
||||
from twisted.internet import defer
|
||||
from nevow import rend, url, tags as T
|
||||
from nevow.inevow import IRequest
|
||||
from allmydata.upload import FileHandle
|
||||
from allmydata.web.common import IClient, getxmlfile, get_arg, boolean_of_arg
|
||||
from allmydata.web import status
|
||||
from allmydata.util import observer
|
||||
|
||||
class UnlinkedPUTCHKUploader(rend.Page):
|
||||
def renderHTTP(self, ctx):
|
||||
req = inevow.IRequest(ctx)
|
||||
assert req.method == "PUT"
|
||||
# "PUT /uri", to create an unlinked file. This is like PUT but
|
||||
# without the associated set_uri.
|
||||
|
||||
def PUTUnlinkedCHK(ctx):
|
||||
req = IRequest(ctx)
|
||||
# "PUT /uri", to create an unlinked file.
|
||||
client = IClient(ctx)
|
||||
|
||||
uploadable = FileHandle(req.content, client.convergence)
|
||||
d = client.upload(uploadable)
|
||||
d.addCallback(lambda results: results.uri)
|
||||
# that fires with the URI of the new file
|
||||
return d
|
||||
|
||||
class UnlinkedPUTSSKUploader(rend.Page):
|
||||
def renderHTTP(self, ctx):
|
||||
req = inevow.IRequest(ctx)
|
||||
assert req.method == "PUT"
|
||||
def PUTUnlinkedSSK(ctx):
|
||||
req = IRequest(ctx)
|
||||
# SDMF: files are small, and we can only upload data
|
||||
req.content.seek(0)
|
||||
data = req.content.read()
|
||||
|
@ -33,44 +27,47 @@ class UnlinkedPUTSSKUploader(rend.Page):
|
|||
d.addCallback(lambda n: n.get_uri())
|
||||
return d
|
||||
|
||||
class UnlinkedPUTCreateDirectory(rend.Page):
|
||||
def renderHTTP(self, ctx):
|
||||
req = inevow.IRequest(ctx)
|
||||
assert req.method == "PUT"
|
||||
def PUTUnlinkedCreateDirectory(ctx):
|
||||
req = IRequest(ctx)
|
||||
# "PUT /uri?t=mkdir", to create an unlinked directory.
|
||||
d = IClient(ctx).create_empty_dirnode()
|
||||
d.addCallback(lambda dirnode: dirnode.get_uri())
|
||||
# XXX add redirect_to_result
|
||||
return d
|
||||
|
||||
class UnlinkedPOSTCHKUploader(status.UploadResultsRendererMixin, rend.Page):
|
||||
"""'POST /uri', to create an unlinked file."""
|
||||
docFactory = getxmlfile("upload-results.xhtml")
|
||||
|
||||
def __init__(self, client, req):
|
||||
rend.Page.__init__(self)
|
||||
# we start the upload now, and distribute notification of its
|
||||
# completion to render_ methods with an ObserverList
|
||||
assert req.method == "POST"
|
||||
self._done = observer.OneShotObserverList()
|
||||
def POSTUnlinkedCHK(ctx):
|
||||
req = IRequest(ctx)
|
||||
client = IClient(ctx)
|
||||
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))
|
||||
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
|
||||
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):
|
||||
return self._done.when_fired()
|
||||
return defer.succeed(self.results)
|
||||
|
||||
def data_done(self, ctx, data):
|
||||
d = self.upload_results()
|
||||
|
@ -88,11 +85,8 @@ class UnlinkedPOSTCHKUploader(status.UploadResultsRendererMixin, rend.Page):
|
|||
["/uri/" + res.uri])
|
||||
return d
|
||||
|
||||
class UnlinkedPOSTSSKUploader(rend.Page):
|
||||
def renderHTTP(self, ctx):
|
||||
req = inevow.IRequest(ctx)
|
||||
assert req.method == "POST"
|
||||
|
||||
def POSTUnlinkedSSK(ctx):
|
||||
req = IRequest(ctx)
|
||||
# "POST /uri", to create an unlinked file.
|
||||
# SDMF: files are small, and we can only upload data
|
||||
contents = req.fields["file"]
|
||||
|
@ -102,11 +96,8 @@ class UnlinkedPOSTSSKUploader(rend.Page):
|
|||
d.addCallback(lambda n: n.get_uri())
|
||||
return d
|
||||
|
||||
class UnlinkedPOSTCreateDirectory(rend.Page):
|
||||
def renderHTTP(self, ctx):
|
||||
req = inevow.IRequest(ctx)
|
||||
assert req.method == "POST"
|
||||
|
||||
def POSTUnlinkedCreateDirectory(ctx):
|
||||
req = IRequest(ctx)
|
||||
# "POST /uri?t=mkdir", to create an unlinked directory.
|
||||
d = IClient(ctx).create_empty_dirnode()
|
||||
redirect = get_arg(req, "redirect_to_result", "false")
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue