From 768c76aa5fbe2c7f751866830ab3f00cca27784e Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 25 Oct 2009 18:13:21 -0700 Subject: [PATCH] webapi: use t=mkdir-with-children instead of a children= arg to t=mkdir . This is safer: in the earlier API, an old webapi server would silently ignore the initial children, and clients trying to set them would have to fetch the newly-created directory to discover the incompatibility. In the new API, clients using t=mkdir-with-children against an old webapi server will get a clear error. --- docs/frontends/webapi.txt | 52 +++++++++++++---------- src/allmydata/test/test_web.py | 75 ++++++++-------------------------- src/allmydata/web/common.py | 6 +-- src/allmydata/web/directory.py | 32 +++++++++++---- src/allmydata/web/root.py | 3 ++ src/allmydata/web/unlinked.py | 30 +++++++++++--- 6 files changed, 101 insertions(+), 97 deletions(-) diff --git a/docs/frontends/webapi.txt b/docs/frontends/webapi.txt index 28b4fd44..7a6abbb9 100644 --- a/docs/frontends/webapi.txt +++ b/docs/frontends/webapi.txt @@ -345,15 +345,20 @@ PUT /uri POST /uri?t=mkdir PUT /uri?t=mkdir - Create a new directory (either empty or with some initial children) and - return its write-cap as the HTTP response body. This does not make the newly - created directory visible from the virtual drive. The "PUT" operation is - provided for backwards compatibility: new code should use POST. + Create a new empty directory and return its write-cap as the HTTP response + body. This does not make the newly created directory visible from the + virtual drive. The "PUT" operation is provided for backwards compatibility: + new code should use POST. - Initial children are provided in the "children" field of the POST form, or - as the request body of the PUT request. This is more efficient than doing - separate mkdir and add-children operations. If this value is empty, the new - directory will be empty. +POST /uri?t=mkdir-with-children + + Create a new directory, populated with a set of child nodes, and return its + write-cap as the HTTP response body. The new directory is not attached to + any other directory: the returned write-cap is the only reference to it. + + Initial children are provided in the "children" field of the POST form. This + is more efficient than doing separate mkdir and add-children operations. If + this value is empty, the new directory will be empty. If not empty, it will be interpreted as a JSON-encoded dictionary of children with which the new directory should be populated, using the same @@ -394,8 +399,7 @@ PUT /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir intermediate directories as necessary. If the named target directory already exists, this will make no changes to it. - If a directory is created, it will be populated with initial children via - the PUT request body or POST 'children' form field, as described above. + If the final directory is created, it will be empty. This will return an error if a blocking file is present at any of the parent names, preventing the server from creating the necessary parent directory. @@ -403,17 +407,27 @@ PUT /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir The write-cap of the new directory will be returned as the HTTP response body. +POST /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir-with-children + + Like above, but if the final directory is created, it will be populated with + initial children via the POST 'children' form field, as described above in + the /uri?t=mkdir-with-children operation. + POST /uri/$DIRCAP/[SUBDIRS../]?t=mkdir&name=NAME Create a new empty directory and attach it to the given existing directory. - This will create additional intermediate directories as necessary. The new - directory will be populated with initial children via the PUT request body - or POST 'children' form field, as described above. + This will create additional intermediate directories as necessary. The URL of this form points to the parent of the bottom-most new directory, whereas the previous form has a URL that points directly to the bottom-most new directory. +POST /uri/$DIRCAP/[SUBDIRS../]?t=mkdir-with-children&name=NAME + + As above, but the new directory will be populated with initial children via + the POST 'children' form field, as described in /uri?t=mkdir-with-children + above. + === Get Information About A File Or Directory (as JSON) === GET /uri/$FILECAP?t=json @@ -767,7 +781,7 @@ GET /uri/$DIRCAP/[SUBDIRS../]FILENAME?t=info POST /uri?t=mkdir - This creates a new directory, but does not attach it to the virtual + This creates a new empty directory, but does not attach it to the virtual filesystem. If a "redirect_to_result=true" argument is provided, then the HTTP response @@ -783,13 +797,10 @@ POST /uri?t=mkdir "false"), then the HTTP response body will simply be the write-cap of the new directory. - It accepts the same initial-children arguments as described in earlier - t=mkdir sections, but these are unlikely to be useful from a browser form. - POST /uri/$DIRCAP/[SUBDIRS../]?t=mkdir&name=CHILDNAME - This creates a new directory as a child of the designated SUBDIR. This will - create additional intermediate directories as necessary. + This creates a new empty directory as a child of the designated SUBDIR. This + will create additional intermediate directories as necessary. If a "when_done=URL" argument is provided, the HTTP response will cause the web browser to redirect to the given URL. This provides a convenient way to @@ -797,9 +808,6 @@ POST /uri/$DIRCAP/[SUBDIRS../]?t=mkdir&name=CHILDNAME when_done= argument, the HTTP response will simply contain the write-cap of the directory that was just created. - It accepts the same initial-children arguments as described in earlier - t=mkdir sections, but these are unlikely to be useful from a browser form. - === Uploading a File === diff --git a/src/allmydata/test/test_web.py b/src/allmydata/test/test_web.py index 967de384..c25aecd0 100644 --- a/src/allmydata/test/test_web.py +++ b/src/allmydata/test/test_web.py @@ -1122,42 +1122,10 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): d.addCallback(self.failUnlessNodeKeysAre, []) return d - def test_PUT_NEWDIRURL_initial_children(self): - (newkids, filecap1, filecap2, filecap3, - dircap) = self._create_initial_children() - d = self.PUT(self.public_url + "/foo/newdir?t=mkdir", - simplejson.dumps(newkids)) - def _check(uri): - n = self.s.create_node_from_uri(uri.strip()) - d2 = self.failUnlessNodeKeysAre(n, newkids.keys()) - d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"child-imm", filecap1)) - d2.addCallback(lambda ign: - n.get_child_and_metadata_at_path(u"child-imm")) - d2.addCallback(lambda (c1, md1): - self.failUnlessEqual(md1["metakey1"], "metavalue1")) - d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"child-mutable", - filecap2)) - d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"child-mutable-ro", - filecap3)) - d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"dirchild", dircap)) - return d2 - d.addCallback(_check) - d.addCallback(lambda res: - self.failUnlessNodeHasChild(self._foo_node, u"newdir")) - d.addCallback(lambda res: self._foo_node.get(u"newdir")) - d.addCallback(self.failUnlessNodeKeysAre, newkids.keys()) - d.addCallback(lambda res: self._foo_node.get(u"newdir")) - d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1) - return d - def test_POST_NEWDIRURL_initial_children(self): (newkids, filecap1, filecap2, filecap3, dircap) = self._create_initial_children() - d = self.POST(self.public_url + "/foo/newdir?t=mkdir", + d = self.POST(self.public_url + "/foo/newdir?t=mkdir-with-children", children=simplejson.dumps(newkids)) def _check(uri): n = self.s.create_node_from_uri(uri.strip()) @@ -1930,8 +1898,8 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): def test_POST_mkdir_initial_children(self): newkids, filecap1, ign, ign, ign = self._create_initial_children() - d = self.POST(self.public_url + "/foo", t="mkdir", name="newdir", - children=simplejson.dumps(newkids)) + d = self.POST(self.public_url + "/foo", t="mkdir-with-children", + name="newdir", children=simplejson.dumps(newkids)) d.addCallback(lambda res: self.failUnlessNodeHasChild(self._foo_node, u"newdir")) d.addCallback(lambda res: self._foo_node.get(u"newdir")) @@ -1992,7 +1960,8 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): def test_POST_mkdir_no_parentdir_initial_children(self): (newkids, filecap1, filecap2, filecap3, dircap) = self._create_initial_children() - d = self.POST("/uri?t=mkdir", children=simplejson.dumps(newkids)) + d = self.POST("/uri?t=mkdir-with-children", + children=simplejson.dumps(newkids)) def _after_mkdir(res): self.failUnless(res.startswith("URI:DIR"), res) n = self.s.create_node_from_uri(res) @@ -2011,6 +1980,19 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): d.addCallback(_after_mkdir) return d + def test_POST_mkdir_no_parentdir_unexpected_children(self): + # the regular /uri?t=mkdir operation is specified to ignore its body. + # Only t=mkdir-with-children pays attention to it. + (newkids, filecap1, filecap2, filecap3, + dircap) = self._create_initial_children() + d = self.shouldHTTPError("POST t=mkdir unexpected children", + 400, "Bad Request", + "t=mkdir does not accept children=, " + "try t=mkdir-with-children instead", + self.POST, "/uri?t=mkdir", # without children + children=simplejson.dumps(newkids)) + return d + def test_POST_noparent_bad(self): d = self.shouldHTTPError("POST /uri?t=bogus", 400, "Bad Request", "/uri accepts only PUT, PUT?t=mkdir, " @@ -2503,27 +2485,6 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): d.addCallback(self.failUnlessIsEmptyJSON) return d - def test_PUT_mkdir_initial_children(self): - (newkids, filecap1, filecap2, filecap3, - dircap) = self._create_initial_children() - d = self.PUT("/uri?t=mkdir", simplejson.dumps(newkids)) - def _check(uri): - n = self.s.create_node_from_uri(uri.strip()) - d2 = self.failUnlessNodeKeysAre(n, newkids.keys()) - d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"child-imm", filecap1)) - d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"child-mutable", - filecap2)) - d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"child-mutable-ro", - filecap3)) - d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"dirchild", dircap)) - return d2 - d.addCallback(_check) - return d - def test_POST_check(self): d = self.POST(self.public_url + "/foo", t="check", name="bar.txt") def _done(res): diff --git a/src/allmydata/web/common.py b/src/allmydata/web/common.py index 60a7e377..5089367f 100644 --- a/src/allmydata/web/common.py +++ b/src/allmydata/web/common.py @@ -54,13 +54,13 @@ def get_arg(ctx_or_req, argname, default=None, multiple=False): return results[0] return default -def convert_initial_children_json(nodemaker, initial_children_json): +def convert_children_json(nodemaker, children_json): """I convert the JSON output of GET?t=json into the dict-of-nodes input to both dirnode.create_subdirectory() and client.create_directory(initial_children=).""" initial_children = {} - if initial_children_json: - data = simplejson.loads(initial_children_json) + if children_json: + data = simplejson.loads(children_json) for (name, (ctype, propdict)) in data.iteritems(): name = unicode(name) writecap = propdict.get("rw_uri") diff --git a/src/allmydata/web/directory.py b/src/allmydata/web/directory.py index 81f26e59..8f81085d 100644 --- a/src/allmydata/web/directory.py +++ b/src/allmydata/web/directory.py @@ -22,7 +22,7 @@ from allmydata.web.common import text_plain, WebError, \ IOpHandleTable, NeedOperationHandleError, \ boolean_of_arg, get_arg, get_root, parse_replace_arg, \ should_create_intermediate_directories, \ - getxmlfile, RenderMixin, humanize_failure, convert_initial_children_json + getxmlfile, RenderMixin, humanize_failure, convert_children_json from allmydata.web.filenode import ReplaceMeMixin, \ FileNodeHandler, PlaceHolderNodeHandler from allmydata.web.check_results import CheckResults, \ @@ -93,16 +93,15 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin): else: if DEBUG: print " terminal" # terminal node - if (method,t) in [ ("POST","mkdir"), ("PUT","mkdir") ]: + if (method,t) in [ ("POST","mkdir"), ("PUT","mkdir"), + ("POST", "mkdir-with-children") ]: if DEBUG: print " making final directory" # final directory - if method == "POST": + kids = {} + if (method,t) == ("POST", "mkdir-with-children"): kids_json = get_arg(req, "children", "") - else: - req.content.seek(0) - kids_json = req.content.read() - kids = convert_initial_children_json(self.client.nodemaker, - kids_json) + kids = convert_children_json(self.client.nodemaker, + kids_json) d = self.node.create_subdirectory(name, kids) d.addCallback(make_handler_for, self.client, self.node, name) @@ -185,6 +184,8 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin): if t == "mkdir": d = self._POST_mkdir(req) + elif t == "mkdir-with-children": + d = self._POST_mkdir_with_children(req) elif t == "mkdir-p": # TODO: docs, tests d = self._POST_mkdir_p(req) @@ -221,6 +222,19 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin): 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")) + kids = {} + d = self.node.create_subdirectory(name, kids, overwrite=replace) + d.addCallback(lambda child: child.get_uri()) # TODO: urlencode + return d + + def _POST_mkdir_with_children(self, req): name = get_arg(req, "name", "") if not name: # our job is done, it was handled by the code in got_child @@ -229,7 +243,7 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin): name = name.decode("utf-8") replace = boolean_of_arg(get_arg(req, "replace", "true")) kids_json = get_arg(req, "children", "") - kids = convert_initial_children_json(self.client.nodemaker, kids_json) + kids = convert_children_json(self.client.nodemaker, kids_json) d = self.node.create_subdirectory(name, kids, overwrite=replace) d.addCallback(lambda child: child.get_uri()) # TODO: urlencode return d diff --git a/src/allmydata/web/root.py b/src/allmydata/web/root.py index 3598e813..97773bd6 100644 --- a/src/allmydata/web/root.py +++ b/src/allmydata/web/root.py @@ -70,6 +70,9 @@ class URIHandler(RenderMixin, rend.Page): return unlinked.POSTUnlinkedCHK(req, self.client) if t == "mkdir": return unlinked.POSTUnlinkedCreateDirectory(req, self.client) + elif t == "mkdir-with-children": + return unlinked.POSTUnlinkedCreateDirectoryWithChildren(req, + self.client) errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, " "and POST?t=mkdir") raise WebError(errmsg, http.BAD_REQUEST) diff --git a/src/allmydata/web/unlinked.py b/src/allmydata/web/unlinked.py index bfad1185..71472da3 100644 --- a/src/allmydata/web/unlinked.py +++ b/src/allmydata/web/unlinked.py @@ -5,7 +5,7 @@ from twisted.internet import defer from nevow import rend, url, tags as T from allmydata.immutable.upload import FileHandle from allmydata.web.common import getxmlfile, get_arg, boolean_of_arg, \ - convert_initial_children_json + convert_children_json, WebError from allmydata.web import status def PUTUnlinkedCHK(req, client): @@ -26,10 +26,7 @@ def PUTUnlinkedSSK(req, client): def PUTUnlinkedCreateDirectory(req, client): # "PUT /uri?t=mkdir", to create an unlinked directory. - req.content.seek(0) - kids_json = req.content.read() - kids = convert_initial_children_json(client.nodemaker, kids_json) - d = client.create_dirnode(initial_children=kids) + d = client.create_dirnode() d.addCallback(lambda dirnode: dirnode.get_uri()) # XXX add redirect_to_result return d @@ -93,9 +90,30 @@ def POSTUnlinkedSSK(req, client): return d def POSTUnlinkedCreateDirectory(req, client): + # "POST /uri?t=mkdir", to create an unlinked directory. + kids_json = get_arg(req, "children", None) + if kids_json is not None: + raise WebError("t=mkdir does not accept children=, " + "try t=mkdir-with-children instead", + http.BAD_REQUEST) + d = client.create_dirnode() + redirect = get_arg(req, "redirect_to_result", "false") + if boolean_of_arg(redirect): + def _then_redir(res): + new_url = "uri/" + urllib.quote(res.get_uri()) + req.setResponseCode(http.SEE_OTHER) # 303 + req.setHeader('location', new_url) + req.finish() + return '' + d.addCallback(_then_redir) + else: + d.addCallback(lambda dirnode: dirnode.get_uri()) + return d + +def POSTUnlinkedCreateDirectoryWithChildren(req, client): # "POST /uri?t=mkdir", to create an unlinked directory. kids_json = get_arg(req, "children", "") - kids = convert_initial_children_json(client.nodemaker, kids_json) + kids = convert_children_json(client.nodemaker, kids_json) d = client.create_dirnode(initial_children=kids) redirect = get_arg(req, "redirect_to_result", "false") if boolean_of_arg(redirect): -- 2.45.2