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.
This commit is contained in:
Brian Warner 2009-10-25 18:13:21 -07:00
parent 1273b5c233
commit 768c76aa5f
6 changed files with 101 additions and 97 deletions

View File

@ -345,15 +345,20 @@ PUT /uri
POST /uri?t=mkdir POST /uri?t=mkdir
PUT /uri?t=mkdir PUT /uri?t=mkdir
Create a new directory (either empty or with some initial children) and Create a new empty directory and return its write-cap as the HTTP response
return its write-cap as the HTTP response body. This does not make the newly body. This does not make the newly created directory visible from the
created directory visible from the virtual drive. The "PUT" operation is virtual drive. The "PUT" operation is provided for backwards compatibility:
provided for backwards compatibility: new code should use POST. new code should use POST.
Initial children are provided in the "children" field of the POST form, or POST /uri?t=mkdir-with-children
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 Create a new directory, populated with a set of child nodes, and return its
directory will be empty. 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 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 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 intermediate directories as necessary. If the named target directory already
exists, this will make no changes to it. exists, this will make no changes to it.
If a directory is created, it will be populated with initial children via If the final directory is created, it will be empty.
the PUT request body or POST 'children' form field, as described above.
This will return an error if a blocking file is present at any of the parent 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. 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 The write-cap of the new directory will be returned as the HTTP response
body. 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 POST /uri/$DIRCAP/[SUBDIRS../]?t=mkdir&name=NAME
Create a new empty directory and attach it to the given existing directory. Create a new empty directory and attach it to the given existing directory.
This will create additional intermediate directories as necessary. The new This will create additional intermediate directories as necessary.
directory will be populated with initial children via the PUT request body
or POST 'children' form field, as described above.
The URL of this form points to the parent of the bottom-most new directory, 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 whereas the previous form has a URL that points directly to the bottom-most
new directory. 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 Information About A File Or Directory (as JSON) ===
GET /uri/$FILECAP?t=json GET /uri/$FILECAP?t=json
@ -767,7 +781,7 @@ GET /uri/$DIRCAP/[SUBDIRS../]FILENAME?t=info
POST /uri?t=mkdir 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. filesystem.
If a "redirect_to_result=true" argument is provided, then the HTTP response 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 "false"), then the HTTP response body will simply be the write-cap of the
new directory. 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 POST /uri/$DIRCAP/[SUBDIRS../]?t=mkdir&name=CHILDNAME
This creates a new directory as a child of the designated SUBDIR. This will This creates a new empty directory as a child of the designated SUBDIR. This
create additional intermediate directories as necessary. will create additional intermediate directories as necessary.
If a "when_done=URL" argument is provided, the HTTP response will cause the 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 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 when_done= argument, the HTTP response will simply contain the write-cap of
the directory that was just created. 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 === === Uploading a File ===

View File

@ -1122,42 +1122,10 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
d.addCallback(self.failUnlessNodeKeysAre, []) d.addCallback(self.failUnlessNodeKeysAre, [])
return d 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): def test_POST_NEWDIRURL_initial_children(self):
(newkids, filecap1, filecap2, filecap3, (newkids, filecap1, filecap2, filecap3,
dircap) = self._create_initial_children() 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)) children=simplejson.dumps(newkids))
def _check(uri): def _check(uri):
n = self.s.create_node_from_uri(uri.strip()) 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): def test_POST_mkdir_initial_children(self):
newkids, filecap1, ign, ign, ign = self._create_initial_children() newkids, filecap1, ign, ign, ign = self._create_initial_children()
d = self.POST(self.public_url + "/foo", t="mkdir", name="newdir", d = self.POST(self.public_url + "/foo", t="mkdir-with-children",
children=simplejson.dumps(newkids)) name="newdir", children=simplejson.dumps(newkids))
d.addCallback(lambda res: d.addCallback(lambda res:
self.failUnlessNodeHasChild(self._foo_node, u"newdir")) self.failUnlessNodeHasChild(self._foo_node, u"newdir"))
d.addCallback(lambda res: self._foo_node.get(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): def test_POST_mkdir_no_parentdir_initial_children(self):
(newkids, filecap1, filecap2, filecap3, (newkids, filecap1, filecap2, filecap3,
dircap) = self._create_initial_children() 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): def _after_mkdir(res):
self.failUnless(res.startswith("URI:DIR"), res) self.failUnless(res.startswith("URI:DIR"), res)
n = self.s.create_node_from_uri(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) d.addCallback(_after_mkdir)
return d 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): def test_POST_noparent_bad(self):
d = self.shouldHTTPError("POST /uri?t=bogus", 400, "Bad Request", d = self.shouldHTTPError("POST /uri?t=bogus", 400, "Bad Request",
"/uri accepts only PUT, PUT?t=mkdir, " "/uri accepts only PUT, PUT?t=mkdir, "
@ -2503,27 +2485,6 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
d.addCallback(self.failUnlessIsEmptyJSON) d.addCallback(self.failUnlessIsEmptyJSON)
return d 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): def test_POST_check(self):
d = self.POST(self.public_url + "/foo", t="check", name="bar.txt") d = self.POST(self.public_url + "/foo", t="check", name="bar.txt")
def _done(res): def _done(res):

View File

@ -54,13 +54,13 @@ def get_arg(ctx_or_req, argname, default=None, multiple=False):
return results[0] return results[0]
return default 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 """I convert the JSON output of GET?t=json into the dict-of-nodes input
to both dirnode.create_subdirectory() and to both dirnode.create_subdirectory() and
client.create_directory(initial_children=).""" client.create_directory(initial_children=)."""
initial_children = {} initial_children = {}
if initial_children_json: if children_json:
data = simplejson.loads(initial_children_json) data = simplejson.loads(children_json)
for (name, (ctype, propdict)) in data.iteritems(): for (name, (ctype, propdict)) in data.iteritems():
name = unicode(name) name = unicode(name)
writecap = propdict.get("rw_uri") writecap = propdict.get("rw_uri")

View File

@ -22,7 +22,7 @@ from allmydata.web.common import text_plain, WebError, \
IOpHandleTable, NeedOperationHandleError, \ IOpHandleTable, NeedOperationHandleError, \
boolean_of_arg, get_arg, get_root, parse_replace_arg, \ boolean_of_arg, get_arg, get_root, parse_replace_arg, \
should_create_intermediate_directories, \ 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, \ from allmydata.web.filenode import ReplaceMeMixin, \
FileNodeHandler, PlaceHolderNodeHandler FileNodeHandler, PlaceHolderNodeHandler
from allmydata.web.check_results import CheckResults, \ from allmydata.web.check_results import CheckResults, \
@ -93,16 +93,15 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
else: else:
if DEBUG: print " terminal" if DEBUG: print " terminal"
# terminal node # 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" if DEBUG: print " making final directory"
# final directory # final directory
if method == "POST": kids = {}
if (method,t) == ("POST", "mkdir-with-children"):
kids_json = get_arg(req, "children", "") kids_json = get_arg(req, "children", "")
else: kids = convert_children_json(self.client.nodemaker,
req.content.seek(0) kids_json)
kids_json = req.content.read()
kids = convert_initial_children_json(self.client.nodemaker,
kids_json)
d = self.node.create_subdirectory(name, kids) d = self.node.create_subdirectory(name, kids)
d.addCallback(make_handler_for, d.addCallback(make_handler_for,
self.client, self.node, name) self.client, self.node, name)
@ -185,6 +184,8 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
if t == "mkdir": if t == "mkdir":
d = self._POST_mkdir(req) d = self._POST_mkdir(req)
elif t == "mkdir-with-children":
d = self._POST_mkdir_with_children(req)
elif t == "mkdir-p": elif t == "mkdir-p":
# TODO: docs, tests # TODO: docs, tests
d = self._POST_mkdir_p(req) d = self._POST_mkdir_p(req)
@ -221,6 +222,19 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
return d return d
def _POST_mkdir(self, req): 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", "") name = get_arg(req, "name", "")
if not name: if not name:
# our job is done, it was handled by the code in got_child # 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") name = name.decode("utf-8")
replace = boolean_of_arg(get_arg(req, "replace", "true")) replace = boolean_of_arg(get_arg(req, "replace", "true"))
kids_json = get_arg(req, "children", "") 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 = self.node.create_subdirectory(name, kids, overwrite=replace)
d.addCallback(lambda child: child.get_uri()) # TODO: urlencode d.addCallback(lambda child: child.get_uri()) # TODO: urlencode
return d return d

View File

@ -70,6 +70,9 @@ class URIHandler(RenderMixin, rend.Page):
return unlinked.POSTUnlinkedCHK(req, self.client) return unlinked.POSTUnlinkedCHK(req, self.client)
if t == "mkdir": if t == "mkdir":
return unlinked.POSTUnlinkedCreateDirectory(req, self.client) 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, " errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
"and POST?t=mkdir") "and POST?t=mkdir")
raise WebError(errmsg, http.BAD_REQUEST) raise WebError(errmsg, http.BAD_REQUEST)

View File

@ -5,7 +5,7 @@ from twisted.internet import defer
from nevow import rend, url, tags as T from nevow import rend, url, tags as T
from allmydata.immutable.upload import FileHandle from allmydata.immutable.upload import FileHandle
from allmydata.web.common import getxmlfile, get_arg, boolean_of_arg, \ from allmydata.web.common import getxmlfile, get_arg, boolean_of_arg, \
convert_initial_children_json convert_children_json, WebError
from allmydata.web import status from allmydata.web import status
def PUTUnlinkedCHK(req, client): def PUTUnlinkedCHK(req, client):
@ -26,10 +26,7 @@ def PUTUnlinkedSSK(req, client):
def PUTUnlinkedCreateDirectory(req, client): def PUTUnlinkedCreateDirectory(req, client):
# "PUT /uri?t=mkdir", to create an unlinked directory. # "PUT /uri?t=mkdir", to create an unlinked directory.
req.content.seek(0) d = client.create_dirnode()
kids_json = req.content.read()
kids = convert_initial_children_json(client.nodemaker, kids_json)
d = client.create_dirnode(initial_children=kids)
d.addCallback(lambda dirnode: dirnode.get_uri()) d.addCallback(lambda dirnode: dirnode.get_uri())
# XXX add redirect_to_result # XXX add redirect_to_result
return d return d
@ -93,9 +90,30 @@ def POSTUnlinkedSSK(req, client):
return d return d
def POSTUnlinkedCreateDirectory(req, client): 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. # "POST /uri?t=mkdir", to create an unlinked directory.
kids_json = get_arg(req, "children", "") 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) d = client.create_dirnode(initial_children=kids)
redirect = get_arg(req, "redirect_to_result", "false") redirect = get_arg(req, "redirect_to_result", "false")
if boolean_of_arg(redirect): if boolean_of_arg(redirect):