tahoe-lafs/src/allmydata/webish.py
Brian Warner 622acc690a webish: improve reporting of web download errors that occur early enough
If the error occurs before any data has been sent, we can give a sensible
error message (code 500, stack trace, etc). This will cover most of the error
cases. The ones that aren't covered are when we run out of good peers after
successfully decoding the first segment, either because they go away or
because their shares are corrupt.
2007-07-03 13:47:37 -07:00

492 lines
18 KiB
Python

from twisted.application import service, strports
from twisted.web import static, resource, server, html, http
from twisted.python import util, log
from nevow import inevow, rend, loaders, appserver, url, tags as T
from nevow.static import File as nevow_File # TODO: merge with static.File?
from allmydata.util import idlib
from allmydata.uri import unpack_uri
from allmydata.interfaces import IDownloadTarget, IDirectoryNode, IFileNode
from allmydata.dirnode import FileNode
from allmydata import upload
from zope.interface import implements, Interface
import urllib
from formless import annotate, webform
def getxmlfile(name):
return loaders.xmlfile(util.sibpath(__file__, "web/%s" % name))
class IClient(Interface):
pass
def get_downloader_service(ctx):
return IClient(ctx).getServiceNamed("downloader")
def get_uploader_service(ctx):
return IClient(ctx).getServiceNamed("uploader")
class Welcome(rend.Page):
addSlash = True
docFactory = getxmlfile("welcome.xhtml")
def data_version(self, ctx, data):
v = IClient(ctx).get_versions()
return "tahoe: %s, zfec: %s, foolscap: %s, twisted: %s" % \
(v['allmydata'], v['zfec'], v['foolscap'], v['twisted'])
def data_my_nodeid(self, ctx, data):
return idlib.b2a(IClient(ctx).nodeid)
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_connected_to_vdrive(self, ctx, data):
if IClient(ctx).getServiceNamed("vdrive").have_public_root():
return "yes"
return "no"
def data_num_peers(self, ctx, data):
#client = inevow.ISite(ctx)._client
client = IClient(ctx)
return len(list(client.get_all_peerids()))
def data_peers(self, ctx, data):
d = []
client = IClient(ctx)
for nodeid in sorted(client.get_all_peerids()):
row = (idlib.b2a(nodeid),)
d.append(row)
return d
def render_row(self, ctx, data):
(nodeid_a,) = data
ctx.fillSlots("peerid", nodeid_a)
return ctx.tag
def render_global_vdrive(self, ctx, data):
if IClient(ctx).getServiceNamed("vdrive").have_public_root():
return T.p["To view the global shared filestore, ",
T.a(href="../global_vdrive")["Click Here!"],
]
return T.p["vdrive.furl not specified (or vdrive server not "
"responding), no vdrive available."]
def render_private_vdrive(self, ctx, data):
if IClient(ctx).getServiceNamed("vdrive").have_private_root():
return T.p["To view your personal private non-shared filestore, ",
T.a(href="../private_vdrive")["Click Here!"],
]
return T.p["personal vdrive not available."]
# this is a form where users can download files by URI
def bind_download(self, ctx):
uriarg = annotate.Argument("uri",
annotate.String("URI of file to download: "))
namearg = annotate.Argument("filename",
annotate.String("Filename to download as: "))
ctxarg = annotate.Argument("ctx", annotate.Context())
meth = annotate.Method(arguments=[uriarg, namearg, ctxarg],
label="Download File by URI")
# buttons always use value=data.label
# MethodBindingRenderer uses value=(data.action or data.label)
return annotate.MethodBinding("download", meth, action="Download")
def download(self, uri, filename, ctx):
log.msg("webish downloading URI")
target = url.here.sibling("download_uri").add("uri", uri)
if filename:
target = target.add("filename", filename)
return target
def render_forms(self, ctx, data):
return webform.renderForms()
class Directory(rend.Page):
addSlash = True
docFactory = getxmlfile("directory.xhtml")
def __init__(self, dirnode, dirname):
self._dirnode = dirnode
self._dirname = dirname
def childFactory(self, ctx, name):
if name.startswith("freeform"): # ick
return None
if name == "@manifest": # ick, this time it's my fault
return Manifest(self._dirnode, self._dirname)
if self._dirname == "/":
dirname = "/" + name
else:
dirname = self._dirname + "/" + name
d = self._dirnode.get(name)
def _got_child(res):
if IFileNode.providedBy(res):
dl = get_downloader_service(ctx)
return Downloader(dl, name, res)
elif IDirectoryNode.providedBy(res):
return Directory(res, dirname)
else:
raise RuntimeError("what is this %s" % res)
d.addCallback(_got_child)
return d
def render_title(self, ctx, data):
return ctx.tag["Directory '%s':" % self._dirname]
def render_header(self, ctx, data):
parent_directories = self._dirname.split("/")
num_dirs = len(parent_directories)
header = ["Directory '"]
for i,d in enumerate(parent_directories):
if d == "":
link = "/".join([".."] * (num_dirs - i))
header.append(T.a(href=link)["/"])
else:
if i == num_dirs-1:
link = "."
else:
link = "/".join([".."] * (num_dirs - i - 1))
header.append(T.a(href=link)[d])
if i < num_dirs - 1:
header.append("/")
header.append("'")
if not self._dirnode.is_mutable():
header.append(" (readonly)")
header.append(":")
return ctx.tag[header]
def data_share_uri(self, ctx, data):
return self._dirnode.get_uri()
def data_share_readonly_uri(self, ctx, data):
return self._dirnode.get_immutable_uri()
def data_children(self, ctx, data):
d = self._dirnode.list()
d.addCallback(lambda dict: sorted(dict.items()))
return d
def render_row(self, ctx, data):
name, target = data
if self._dirnode.is_mutable():
# 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
del_url = url.here.child("_delete")
#del_url = del_url.add("uri", target.uri)
del_url = del_url.add("name", name)
delete = T.form(action=del_url, method="post")[
T.input(type='submit', value='del', name="del"),
]
else:
delete = "-"
ctx.fillSlots("delete", delete)
if IFileNode.providedBy(target):
# file
dlurl = urllib.quote(name)
ctx.fillSlots("filename",
T.a(href=dlurl)[html.escape(name)])
ctx.fillSlots("type", "FILE")
uri = target.uri
dl_uri_url = url.root.child("download_uri").child(uri)
# add a filename= query argument to give it a Content-Type
dl_uri_url = dl_uri_url.add("filename", name)
ctx.fillSlots("uri", T.a(href=dl_uri_url)[html.escape(uri)])
#extract and display file size
ctx.fillSlots("size", unpack_uri(uri)['size'])
elif IDirectoryNode.providedBy(target):
# directory
subdir_url = urllib.quote(name)
ctx.fillSlots("filename",
T.a(href=subdir_url)[html.escape(name)])
if target.is_mutable():
dirtype = "DIR"
else:
dirtype = "DIR-RO"
ctx.fillSlots("type", dirtype)
ctx.fillSlots("size", "-")
ctx.fillSlots("uri", "-")
else:
raise RuntimeError("unknown thing %s" % (target,))
return ctx.tag
def render_forms(self, ctx, data):
if self._dirnode.is_mutable():
return webform.renderForms()
return T.div["No upload forms: directory is immutable"]
def render_results(self, ctx, data):
req = inevow.IRequest(ctx)
if "results" in req.args:
return req.args["results"]
else:
return ""
def bind_upload(self, ctx):
"""upload1"""
# Note: this comment is no longer accurate, as it reflects the older
# (apparently deprecated) formless.autocallable /
# annotate.TypedInterface approach.
# Each method gets a box. The string in the autocallable(action=)
# argument is put on the border of the box, as well as in the submit
# button. The top-most contents of the box are the method's
# docstring, if any. Each row contains a string for the argument
# followed by the argument's input box. If you do not provide an
# action= argument to autocallable, the method name is capitalized
# and used instead.
up = annotate.FileUpload(label="Choose a file to upload: ",
required=True,
requiredFailMessage="Do iT!")
contentsarg = annotate.Argument("contents", up)
privateUpload = annotate.Radio(label="Private?", choices=["Yes"])
privatearg = annotate.Argument("privateupload", privateUpload)
ctxarg = annotate.Argument("ctx", annotate.Context())
meth = annotate.Method(arguments=[contentsarg, privatearg, ctxarg],
label="Upload File to this directory")
return annotate.MethodBinding("upload", meth, action="Upload")
def uploadprivate(self, filename, uri):
message = "webish upload complete, filename %s %s" % (filename, uri)
log.msg(message)
return url.here.add("filename", filename).add("results", message)
def upload(self, contents, privateupload, ctx):
# contents is a cgi.FieldStorage instance
log.msg("starting webish upload")
uploader = get_uploader_service(ctx)
uploadable = upload.FileHandle(contents.file)
name = contents.filename
if privateupload:
d = uploader.upload(uploadable)
d.addCallback(lambda uri: self.uploadprivate(name, uri))
else:
d = self._dirnode.add_file(name, uploadable)
def _done(res):
log.msg("webish upload complete")
return res
d.addCallback(_done)
return d # TODO: huh?
return url.here.add("results",
"upload of '%s' complete!" % contents.filename)
def bind_mkdir(self, ctx):
"""Make new directory 1"""
namearg = annotate.Argument("name",
annotate.String("New directory name: "))
meth = annotate.Method(arguments=[namearg], label="Make New Subdirectory")
return annotate.MethodBinding("mkdir", meth, action="Create Directory")
def mkdir(self, name):
"""mkdir2"""
log.msg("making new webish directory: %s" % (name,))
d = self._dirnode.create_empty_directory(name)
def _done(res):
log.msg("webish mkdir complete")
return res
d.addCallback(_done)
return d
def bind_mount(self, ctx):
namearg = annotate.Argument("name",
annotate.String("Name to place incoming directory: "))
uriarg = annotate.Argument("uri",
annotate.String("URI of Shared Directory"))
meth = annotate.Method(arguments=[namearg, uriarg],
label="Add Shared Directory")
return annotate.MethodBinding("mount", meth,
action="Mount Shared Directory")
def mount(self, name, uri):
d = self._dirnode.set_uri(name, uri)
#d.addCallback(lambda done: url.here.child(name))
return d
def child__delete(self, ctx):
# perform the delete, then redirect back to the directory page
args = inevow.IRequest(ctx).args
name = args["name"][0]
d = self._dirnode.delete(name)
d.addCallback(lambda done: url.here.up())
return d
class WebDownloadTarget:
implements(IDownloadTarget)
def __init__(self, req, content_type, content_encoding):
self._req = req
self._content_type = content_type
self._content_encoding = content_encoding
self._opened = False
def open(self):
self._opened = True
self._req.setHeader("content-type", self._content_type)
if self._content_encoding:
self._req.setHeader('content-encoding', self._content_encoding)
# TODO: it would be nice to set the size header here
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.INTERNAL_SERVER_ERROR, 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 TypedFile(static.File):
# serve data from a named file, but using a Content-Type derived from a
# different filename
isLeaf = True
def __init__(self, path, requested_filename):
static.File.__init__(self, path)
gte = static.getTypeAndEncoding
self.type, self.encoding = gte(requested_filename,
self.contentTypes,
self.contentEncodings,
self.defaultType)
class Downloader(resource.Resource):
def __init__(self, downloader, name, filenode):
self._downloader = downloader
self._name = name
IFileNode(filenode)
self._filenode = filenode
def render(self, ctx):
req = inevow.IRequest(ctx)
gte = static.getTypeAndEncoding
type, encoding = gte(self._name,
static.File.contentTypes,
static.File.contentEncodings,
defaultType="text/plain")
d = self._filenode.download(WebDownloadTarget(req, type, encoding))
# exceptions during download are handled by the WebDownloadTarget
d.addErrback(lambda why: None)
return server.NOT_DONE_YET
class Manifest(rend.Page):
docFactory = getxmlfile("manifest.xhtml")
def __init__(self, dirnode, dirname):
self._dirnode = dirnode
self._dirname = dirname
def render_title(self, ctx):
return T.title["Manifest of %s" % self._dirname]
def render_header(self, ctx):
return T.p["Manifest of %s" % self._dirname]
def data_items(self, ctx, data):
return self._dirnode.build_manifest()
def render_row(self, ctx, refresh_cap):
ctx.fillSlots("refresh_capability", refresh_cap)
return ctx.tag
class Root(rend.Page):
def locateChild(self, ctx, segments):
if segments[0] == "download_uri":
req = inevow.IRequest(ctx)
dl = get_downloader_service(ctx)
filename = "unknown_filename"
if "filename" in req.args:
filename = req.args["filename"][0]
if len(segments) > 1:
# http://host/download_uri/URIGOESHERE
uri = segments[1]
elif "uri" in req.args:
# http://host/download_uri?uri=URIGOESHERE
uri = req.args["uri"][0]
else:
return rend.NotFound
child = Downloader(dl, filename, FileNode(uri, IClient(ctx)))
return child, ()
return rend.Page.locateChild(self, ctx, segments)
child_webform_css = webform.defaultCSS
child_tahoe_css = nevow_File(util.sibpath(__file__, "web/tahoe.css"))
child_welcome = Welcome()
def child_global_vdrive(self, ctx):
client = IClient(ctx)
vdrive = client.getServiceNamed("vdrive")
if vdrive.have_public_root():
d = vdrive.get_public_root()
d.addCallback(lambda dirnode: Directory(dirnode, "/"))
return d
else:
return static.Data("sorry, still initializing", "text/plain")
def child_private_vdrive(self, ctx):
client = IClient(ctx)
vdrive = client.getServiceNamed("vdrive")
if vdrive.have_private_root():
d = vdrive.get_private_root()
d.addCallback(lambda dirnode: Directory(dirnode, "~"))
return d
else:
return static.Data("sorry, still initializing", "text/plain")
class WebishServer(service.MultiService):
name = "webish"
def __init__(self, webport):
service.MultiService.__init__(self)
self.root = Root()
self.root.putChild("", url.here.child("welcome"))#Welcome())
self.site = site = appserver.NevowSite(self.root)
s = strports.service(webport, site)
s.setServiceParent(self)
self.listener = s # stash it so the tests can query for the portnum
def startService(self):
service.MultiService.startService(self)
# to make various services available to render_* methods, we stash a
# reference to the client on the NevowSite. This will be available by
# adapting the 'context' argument to a special marker interface named
# IClient.
self.site.remember(self.parent, IClient)
# I thought you could do the same with an existing interface, but
# apparently 'ISite' does not exist
#self.site._client = self.parent