From 464f25e5f26faa97e6b7b7e0b862e1928df0af1f Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 7 Jul 2007 20:06:58 -0700 Subject: [PATCH] web: more test work, now all tests pass, POST too, only XMLRPC left to implement --- docs/webapi.txt | 42 ++++++-- src/allmydata/test/test_web.py | 185 +++++++++++++++++++++++++++------ src/allmydata/webish.py | 78 ++++++++++++-- 3 files changed, 263 insertions(+), 42 deletions(-) diff --git a/docs/webapi.txt b/docs/webapi.txt index f0076899..dc445a17 100644 --- a/docs/webapi.txt +++ b/docs/webapi.txt @@ -194,7 +194,10 @@ for files and directories which do not yet exist. == POST Forms == - POST DIRURL?t=upload-form + POST DIRURL + t=upload + name=childname (optional) + file=newfile This instructs the client to upload a file into the given dirnode. We need this because forms are the only way for a web browser to upload a file @@ -202,18 +205,25 @@ for files and directories which do not yet exist. new child name will be included in the form's arguments. This can only be used to upload a single file at a time. - POST DIRURL?t=mkdir-form + POST DIRURL + t=mkdir + name=childname This instructs the client to create a new empty directory. The name of the new child directory will be included in the form's arguments. - POST DIRURL?t=put-uri-form + POST DIRURL + t=uri + name=childname + uri=newuri This instructs the client to attach a child that is referenced by URI (just like the PUT NEWFILEURL?t=uri method). The name and URI of the new child will be included in the form's arguments. - POST DIRURL?t=delete-form + POST DIRURL + t=delete + name=childname This instructs the client to delete a file from the given dirnode. The name of the child to be deleted will be included in the form's arguments. @@ -232,8 +242,17 @@ for files and directories which do not yet exist. GET http://localhost:8011/uri/$URI - would retrieve the contents of the file. If the URI corresponds to a - directory, then: + would retrieve the contents of the file. Since files accessed this way do + not have a naturally-occurring filename (from which a MIME-type can be + derived), one can be specified using a 'filename=' query argument. This + filename is also the one used if the 'save=true' argument is set, which + adds a 'Content-Disposition: attachment' header to prompt most web browsers + to save the file to disk rather than attempting to display it: + + GET http://localhost:8011/uri/$URI?filename=foo.jpg + GET http://localhost:8011/uri/$URI?filename=foo.jpg&save=true + + If the URI corresponds to a directory, then: PUT http://localhost:8011/uri/$URI/subdir/newfile?localfile=$FILENAME @@ -247,6 +266,17 @@ for files and directories which do not yet exist. can be used to attach a shared directory to the vdrive. Intermediate directories are created on-demand just like with the regular PUT command. + GET http://localhost:8011/uri?uri=$URI + + This causes a redirect to /uri/$URI, and retains any additional query + arguments (like filename= or save=). This is for the convenience of web + forms which allow the user to paste in a URI (obtained through some + out-of-band channel, like IM or email). + + Note that this form only redirects to the specific node indicated by the + URI: unlike the GET /uri/$URI form, you cannot traverse to child nodes by + appending additional path segments to the URL. + == XMLRPC == http://localhost:8011/xmlrpc diff --git a/src/allmydata/test/test_web.py b/src/allmydata/test/test_web.py index 0f7a3bdc..78dc3366 100644 --- a/src/allmydata/test/test_web.py +++ b/src/allmydata/test/test_web.py @@ -1,11 +1,11 @@ -import re, os.path +import re, os.path, urllib from zope.interface import implements from twisted.application import service from twisted.trial import unittest from twisted.internet import defer from twisted.web import client, error -from twisted.python import failure +from twisted.python import failure, log from allmydata import webish, interfaces, dirnode, uri from allmydata.encode import NotEnoughPeersError from allmydata.util import fileutil @@ -125,6 +125,8 @@ class MyVirtualDrive(service.Service): name = "vdrive" public_root = None private_root = None + def __init__(self, nodes): + self._my_nodes = nodes def have_public_root(self): return bool(self.public_root) def have_private_root(self): @@ -134,6 +136,11 @@ class MyVirtualDrive(service.Service): def get_private_root(self): return defer.succeed(self.private_root) + def get_node(self, uri): + def _try(): + return self._my_nodes[uri] + return defer.maybeDeferred(_try) + class Web(unittest.TestCase): def setUp(self): self.s = MyClient() @@ -143,11 +150,12 @@ class Web(unittest.TestCase): port = s.listener._port.getHost().port self.webish_url = "http://localhost:%d" % port - v = MyVirtualDrive() - v.setServiceParent(self.s) - self.nodes = {} # maps URI to node self.files = {} # maps file URI to contents + + v = MyVirtualDrive(self.nodes) + v.setServiceParent(self.s) + dl = MyDownloader(self.files) dl.setServiceParent(self.s) ul = MyUploader(self.files) @@ -210,9 +218,15 @@ class Web(unittest.TestCase): def failUnlessIsBarDotTxt(self, res): self.failUnlessEqual(res, self.BAR_CONTENTS) - def GET(self, urlpath): + def failUnlessIsFooJSON(self, res): + self.failUnless("JSONny stuff here" in res) + self.failUnless("name=bar.txt, child_uri=%s" % self._bar_txt_uri + in res) + self.failUnless("name=blockingfile" in res) + + def GET(self, urlpath, followRedirect=False): url = self.webish_url + urlpath - return client.getPage(url, method="GET") + return client.getPage(url, method="GET", followRedirect=followRedirect) def PUT(self, urlpath, data): url = self.webish_url + urlpath @@ -222,10 +236,33 @@ class Web(unittest.TestCase): url = self.webish_url + urlpath return client.getPage(url, method="DELETE") - def POST(self, urlpath, data): - raise unittest.SkipTest("not yet") + def POST(self, urlpath, **fields): url = self.webish_url + urlpath - return client.getPage(url, method="POST", postdata=data) + sepbase = "boogabooga" + sep = "--" + sepbase + form = [] + form.append(sep) + form.append('Content-Disposition: form-data; name="_charset"') + form.append('') + form.append('UTF-8') + form.append(sep) + for name, value in fields.iteritems(): + if isinstance(value, tuple): + filename, value = value + form.append('Content-Disposition: form-data; name="%s"; ' + 'filename="%s"' % (name, filename)) + else: + form.append('Content-Disposition: form-data; name="%s"' % name) + form.append('') + form.append(value) + form.append(sep) + form[-1] += "--" + body = "\r\n".join(form) + "\r\n" + headers = {"content-type": "multipart/form-data; boundary=%s" % sepbase, + } + print "BODY", body + return client.getPage(url, method="POST", postdata=body, + headers=headers, followRedirect=False) def shouldFail(self, res, expected_failure, which, substring=None): print "SHOULDFAIL", res @@ -434,7 +471,8 @@ class Web(unittest.TestCase): return d def test_GET_DIRURL(self): # YES - d = self.GET("/vdrive/global/foo") + # the addSlash means we get a redirect here + d = self.GET("/vdrive/global/foo", followRedirect=True) def _check(res): self.failUnless(re.search(r'bar.txt' '\s+FILE' @@ -635,34 +673,123 @@ class Web(unittest.TestCase): d.addCallback(_check) return d - def test_POST_upload(self): - form = "TODO" - d = self.POST("/vdrive/global/foo", form) + def test_POST_upload(self): # YES + d = self.POST("/vdrive/global/foo", t="upload", + file=("new.txt", self.NEWFILE_CONTENTS)) + def _check(res): + self.failUnless("new.txt" in self._foo_node.children) + new_uri = self._foo_node.children["new.txt"] + new_contents = self.files[new_uri] + self.failUnlessEqual(new_contents, self.NEWFILE_CONTENTS) + self.failUnlessEqual(res.strip(), new_uri) + d.addCallback(_check) return d - def test_POST_mkdir(self): - form = "TODO" - d = self.POST("/vdrive/global/foo", form) + def test_POST_upload_named(self): # YES + d = self.POST("/vdrive/global/foo", t="upload", + name="new.txt", file=self.NEWFILE_CONTENTS) + def _check(res): + self.failUnless("new.txt" in self._foo_node.children) + new_uri = self._foo_node.children["new.txt"] + new_contents = self.files[new_uri] + self.failUnlessEqual(new_contents, self.NEWFILE_CONTENTS) + self.failUnlessEqual(res.strip(), new_uri) + d.addCallback(_check) return d - def test_POST_put_uri(self): - form = "TODO" - d = self.POST("/vdrive/global/foo", form) + def test_POST_mkdir(self): # YES, return value? + d = self.POST("/vdrive/global/foo", t="mkdir", name="newdir") + def _check(res): + self.failUnless("newdir" in self._foo_node.children) + newdir_uri = self._foo_node.children["newdir"] + newdir_node = self.nodes[newdir_uri] + self.failIf(newdir_node.children) + d.addCallback(_check) return d - def test_POST_delete(self): - form = "TODO, bar.txt" - d = self.POST("/vdrive/global/foo", form) + def test_POST_put_uri(self): # YES + newuri = self.makefile(8) + contents = self.files[newuri] + d = self.POST("/vdrive/global/foo", t="uri", name="new.txt", uri=newuri) + def _check(res): + self.failUnless("new.txt" in self._foo_node.children) + new_uri = self._foo_node.children["new.txt"] + new_contents = self.files[new_uri] + self.failUnlessEqual(new_contents, contents) + self.failUnlessEqual(res.strip(), new_uri) + d.addCallback(_check) return d - def test_URI_GET(self): - raise unittest.SkipTest("not yet") - d = self.GET("/uri/%s/bar.txt" % foo_uri) + def test_POST_delete(self): # yes + d = self.POST("/vdrive/global/foo", t="delete", name="bar.txt") + def _check(res): + self.failIf("bar.txt" in self._foo_node.children) + d.addCallback(_check) return d - def test_PUT_NEWFILEURL_uri(self): - raise unittest.SkipTest("not yet") - d = self.PUT("/vdrive/global/foo/new.txt?uri", new_uri) + def shouldRedirect(self, res, target): + if not isinstance(res, failure.Failure): + self.fail("we were expecting to get redirected to %s, not get an" + " actual page: %s" % (target, res)) + res.trap(error.PageRedirect) + # the PageRedirect does not seem to capture the uri= query arg + # properly, so we can't check for it. + print "location:", res.value.location + realtarget = self.webish_url + target + self.failUnlessEqual(res.value.location, realtarget) + + def test_GET_URI_form(self): # YES + base = "/uri?uri=%s" % self._bar_txt_uri + # this is supposed to give us a redirect to /uri/$URI, plus arguments + targetbase = "/uri/%s" % urllib.quote(self._bar_txt_uri) + d = self.GET(base) + d.addBoth(self.shouldRedirect, targetbase) + d.addCallback(lambda res: self.GET(base+"&filename=bar.txt")) + d.addBoth(self.shouldRedirect, targetbase+"?filename=bar.txt") + d.addCallback(lambda res: self.GET(base+"&t=json")) + d.addBoth(self.shouldRedirect, targetbase+"?t=json") + d.addCallback(self.log, "about to get file by uri") + d.addCallback(lambda res: self.GET(base, followRedirect=True)) + d.addCallback(self.failUnlessIsBarDotTxt) + d.addCallback(self.log, "got file by uri, about to get dir by uri") + d.addCallback(lambda res: self.GET("/uri?uri=%s&t=json" % self._foo_uri, + followRedirect=True)) + d.addCallback(self.failUnlessIsFooJSON) + d.addCallback(self.log, "got dir by uri") + + return d + + def log(self, res, msg): + print "MSG: %s RES: %s" % (msg, res) + log.msg(msg) + return res + + def test_GET_URI_URL(self): # YES + base = "/uri/%s" % self._bar_txt_uri + d = self.GET(base) + d.addCallback(self.failUnlessIsBarDotTxt) + d.addCallback(lambda res: self.GET(base+"?filename=bar.txt")) + d.addCallback(self.failUnlessIsBarDotTxt) + d.addCallback(lambda res: self.GET(base+"?filename=bar.txt&save=true")) + d.addCallback(self.failUnlessIsBarDotTxt) + return d + + def test_GET_URI_URL_dir(self): # YES + base = "/uri/%s?t=json" % self._foo_uri + d = self.GET(base) + d.addCallback(self.failUnlessIsFooJSON) + return d + + def test_PUT_NEWFILEURL_uri(self): # YES + new_uri = self.makefile(8) + d = self.PUT("/vdrive/global/foo/new.txt?t=uri", new_uri) + def _check(res): + self.failUnless("new.txt" in self._foo_node.children) + new_uri = self._foo_node.children["new.txt"] + new_contents = self.files[new_uri] + self.failUnlessEqual(new_contents, self.files[new_uri]) + self.failUnlessEqual(res.strip(), new_uri) + d.addCallback(_check) return d def test_XMLRPC(self): diff --git a/src/allmydata/webish.py b/src/allmydata/webish.py index f4c89cac..d388f533 100644 --- a/src/allmydata/webish.py +++ b/src/allmydata/webish.py @@ -303,10 +303,10 @@ class TypedFile(static.File): self.defaultType) class FileDownloader(resource.Resource): - def __init__(self, name, filenode): - self._name = name + def __init__(self, filenode, name): IFileNode(filenode) self._filenode = filenode + self._name = name def render(self, req): gte = static.getTypeAndEncoding @@ -515,8 +515,57 @@ class DirectoryReadonlyURI(DirectoryJSONMetadata): class POSTHandler(rend.Page): def __init__(self, node): self._node = node - # TODO: handler methods + def renderHTTP(self, ctx): + req = inevow.IRequest(ctx) + print "POST", req, req.args#, req.content.read() + #print " ", req.requestHeaders + #print req.__class__ + #print req.fields + #print dir(req.fields) + print req.fields.keys() + t = req.fields["t"].value + if t == "mkdir": + name = req.fields["name"].value + print "CREATING DIR", name + d = self._node.create_empty_directory(name) + def _done(res): + return "directory created" + d.addCallback(_done) + return d + elif t == "uri": + name = req.fields["name"].value + uri = req.fields["uri"].value + d = self._node.set_uri(name, uri) + def _done(res): + return uri + d.addCallback(_done) + return d + elif t == "delete": + name = req.fields["name"].value + d = self._node.delete(name) + def _done(res): + return "thing deleted" + d.addCallback(_done) + return d + elif t == "upload": + contents = req.fields["file"] + name = contents.filename + print "filename", name + if "name" in req.fields: + name = req.fields["name"].value + print "NAME WAS", name + uploadable = upload.FileHandle(contents.file) + d = self._node.add_file(name, uploadable) + def _done(newnode): + print "UPLOAD DONW", name + return newnode.get_uri() + d.addCallback(_done) + return d + else: + print "BAD t=%s" % t + return "BAD t=%s" % t + return "nope" class DELETEHandler(rend.Page): def __init__(self, node, name): @@ -742,13 +791,18 @@ class VDrive(rend.Page): d = self.get_child_at_path(path) def file_or_dir(node): if IFileNode.providedBy(node): + filename = "unknown" + if path: + filename = path[-1] + if "filename" in req.args: + filename = req.args["filename"][0] if localfile: # write contents to a local file return LocalFileDownloader(node, localfile), () elif t == "": # send contents as the result print "FileDownloader" - return FileDownloader(path[-1], node), () + return FileDownloader(node, filename), () elif t == "json": print "Localfilejsonmetadata" return FileJSONMetadata(node), () @@ -820,6 +874,7 @@ class Root(rend.Page): def locateChild(self, ctx, segments): client = IClient(ctx) + req = inevow.IRequest(ctx) vdrive = client.getServiceNamed("vdrive") print "Root.locateChild", segments @@ -838,17 +893,26 @@ class Root(rend.Page): d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:])) return d elif segments[0] == "uri": + print "looking at /uri", segments, req.args + if len(segments) == 1: + if "uri" in req.args: + uri = req.args["uri"][0] + print "REDIRECTING" + there = url.URL.fromContext(ctx) + there = there.clear("uri") + there = there.child("uri").child(uri) + print " TO", there + return there, () if len(segments) < 2: return rend.NotFound uri = segments[1] d = vdrive.get_node(uri) - d.addCallback(lambda node: VDrive(node), uri) + d.addCallback(lambda node: VDrive(node, "")) d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:])) return d elif segments[0] == "xmlrpc": pass # TODO elif segments[0] == "download_uri": - req = inevow.IRequest(ctx) dl = get_downloader_service(ctx) filename = "unknown_filename" if "filename" in req.args: @@ -861,7 +925,7 @@ class Root(rend.Page): uri = req.args["uri"][0] else: return rend.NotFound - child = FileDownloader(filename, FileNode(uri, IClient(ctx))) + child = FileDownloader(FileNode(uri, IClient(ctx)), filename) return child, () return rend.Page.locateChild(self, ctx, segments) -- 2.45.2