]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/commitdiff
web: more test work, now all tests pass, POST too, only XMLRPC left to implement
authorBrian Warner <warner@lothar.com>
Sun, 8 Jul 2007 03:06:58 +0000 (20:06 -0700)
committerBrian Warner <warner@lothar.com>
Sun, 8 Jul 2007 03:06:58 +0000 (20:06 -0700)
docs/webapi.txt
src/allmydata/test/test_web.py
src/allmydata/webish.py

index f00768995fe520929a1fa4a81c3ae412d6ebb43d..dc445a17e9a319086e974422c042c3f0a7272c95 100644 (file)
@@ -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
index 0f7a3bdc08ce4e89d6765c9d744cdefad23057bc..78dc336649f0ae727ef7cd6be3e064a550812a9a 100644 (file)
@@ -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'<td><a href="bar.txt">bar.txt</a></td>'
                                       '\s+<td>FILE</td>'
@@ -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):
index f4c89cac0124ddc8ee518c2c20cd7883cbd843b0..d388f533aace2d90de0a37456ac8a6474e7803f9 100644 (file)
@@ -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, "<from-uri>"))
             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)