webish: add POST t=mutable, make it replace files in-place, add t=overwrite

This commit is contained in:
Brian Warner 2007-12-04 23:42:54 -07:00
parent 4c79b4cd07
commit 56e02b274b
4 changed files with 107 additions and 22 deletions

View File

@ -324,6 +324,7 @@ c. POST forms
t=upload t=upload
name=childname (optional) name=childname (optional)
file=newfile file=newfile
This instructs the node to upload a file into the given directory. We need This instructs the node to upload a file into the given directory. We need
this because forms are the only way for a web browser to upload a file this because forms are the only way for a web browser to upload a file
(browsers do not know how to do PUT or DELETE). The file's contents and the (browsers do not know how to do PUT or DELETE). The file's contents and the
@ -331,13 +332,29 @@ c. POST forms
used to upload a single file at a time. To avoid confusion, name= is not used to upload a single file at a time. To avoid confusion, name= is not
allowed to contain a slash (a 400 Bad Request error will result). allowed to contain a slash (a 400 Bad Request error will result).
POST $URL POST $URL
t=upload t=upload
name=childname (optional) name=childname (optional)
mutable="on" mutable="true"
file=newfile file=newfile
This instructs the node to upload a file into the given directory, using a This instructs the node to upload a file into the given directory, using a
mutable file (SSK) rather than the usual immutable file (CHK). mutable file (SSK) rather than the usual immutable file (CHK). As a result,
further operations to the same $URL will not cause the identity of the file
to change.
POST $URL
t=overwrite
file=newfile
This is used to replace the existing (mutable) file's contents with new
ones. It may only be used when $URL refers to a mutable file, as created by
POST $URL?t=upload&mutable=true, or PUT /uri?t=mutable . The name
associated with the uploaded file is ignored. TODO: rethink this, it's kind
of weird.
POST $URL POST $URL
t=mkdir t=mkdir
@ -346,6 +363,7 @@ c. POST forms
This instructs the node to create a new empty directory. The name of the This instructs the node to create a new empty directory. The name of the
new child directory will be included in the form's arguments. new child directory will be included in the form's arguments.
POST $URL POST $URL
t=uri t=uri
name=childname name=childname
@ -355,6 +373,7 @@ c. POST forms
like the PUT $URL?t=uri method). The name and URI of the new child like the PUT $URL?t=uri method). The name and URI of the new child
will be included in the form's arguments. will be included in the form's arguments.
POST $URL POST $URL
t=delete t=delete
name=childname name=childname
@ -362,6 +381,7 @@ c. POST forms
This instructs the node to delete a file from the given directory. The name This instructs the node to delete a file from the given directory. The name
of the child to be deleted will be included in the form's arguments. of the child to be deleted will be included in the form's arguments.
POST $URL POST $URL
t=rename t=rename
from_name=oldchildname from_name=oldchildname

View File

@ -81,7 +81,9 @@ class FakeMutableFileNode:
self.storage_index = self.my_uri.storage_index self.storage_index = self.my_uri.storage_index
return self return self
def get_uri(self): def get_uri(self):
return self.my_uri return self.my_uri.to_string()
def get_readonly_uri(self):
return self.my_uri.get_readonly().to_string()
def is_readonly(self): def is_readonly(self):
return self.my_uri.is_readonly() return self.my_uri.is_readonly()
def is_mutable(self): def is_mutable(self):

View File

@ -1,5 +1,6 @@
import re, os.path, urllib import re, os.path, urllib
import simplejson
from twisted.application import service from twisted.application import service
from twisted.trial import unittest from twisted.trial import unittest
from twisted.internet import defer from twisted.internet import defer
@ -8,7 +9,7 @@ from twisted.python import failure, log
from allmydata import webish, interfaces, provisioning from allmydata import webish, interfaces, provisioning
from allmydata.util import fileutil from allmydata.util import fileutil
from allmydata.test.common import NonGridDirectoryNode, FakeCHKFileNode, FakeMutableFileNode, create_chk_filenode from allmydata.test.common import NonGridDirectoryNode, FakeCHKFileNode, FakeMutableFileNode, create_chk_filenode
from allmydata.interfaces import IURI, INewDirectoryURI, IReadonlyNewDirectoryURI, IFileURI, IMutableFileURI from allmydata.interfaces import IURI, INewDirectoryURI, IReadonlyNewDirectoryURI, IFileURI, IMutableFileURI, IMutableFileNode
# create a fake uploader/downloader, and a couple of fake dirnodes, then # create a fake uploader/downloader, and a couple of fake dirnodes, then
# create a webserver that works against them # create a webserver that works against them
@ -130,15 +131,8 @@ class WebMixin(object):
def failUnlessIsBarDotTxt(self, res): def failUnlessIsBarDotTxt(self, res):
self.failUnlessEqual(res, self.BAR_CONTENTS) self.failUnlessEqual(res, self.BAR_CONTENTS)
def worlds_cheapest_json_decoder(self, json):
# don't write tests that use 'true' or 'false' as filenames
json = re.sub('false', 'False', json)
json = re.sub('true', 'True', json)
json = re.sub(r'\\/', '/', json)
return eval(json)
def failUnlessIsBarJSON(self, res): def failUnlessIsBarJSON(self, res):
data = self.worlds_cheapest_json_decoder(res) data = simplejson.loads(res)
self.failUnless(isinstance(data, list)) self.failUnless(isinstance(data, list))
self.failUnlessEqual(data[0], "filenode") self.failUnlessEqual(data[0], "filenode")
self.failUnless(isinstance(data[1], dict)) self.failUnless(isinstance(data[1], dict))
@ -147,7 +141,7 @@ class WebMixin(object):
self.failUnlessEqual(data[1]["size"], len(self.BAR_CONTENTS)) self.failUnlessEqual(data[1]["size"], len(self.BAR_CONTENTS))
def failUnlessIsFooJSON(self, res): def failUnlessIsFooJSON(self, res):
data = self.worlds_cheapest_json_decoder(res) data = simplejson.loads(res)
self.failUnless(isinstance(data, list)) self.failUnless(isinstance(data, list))
self.failUnlessEqual(data[0], "dirnode", res) self.failUnlessEqual(data[0], "dirnode", res)
self.failUnless(isinstance(data[1], dict)) self.failUnless(isinstance(data[1], dict))
@ -894,6 +888,61 @@ class Web(WebMixin, unittest.TestCase):
self.NEWFILE_CONTENTS)) self.NEWFILE_CONTENTS))
return d return d
def test_POST_upload_mutable(self):
# this creates a mutable file
d = self.POST(self.public_url + "/foo", t="upload", mutable="true",
file=("new.txt", self.NEWFILE_CONTENTS))
fn = self._foo_node
d.addCallback(self.failUnlessURIMatchesChild, fn, "new.txt")
d.addCallback(lambda res:
self.failUnlessChildContentsAre(fn, "new.txt",
self.NEWFILE_CONTENTS))
d.addCallback(lambda res: self._foo_node.get("new.txt"))
def _got(newnode):
self.failUnless(IMutableFileNode.providedBy(newnode))
self.failUnless(newnode.is_mutable())
self.failIf(newnode.is_readonly())
self._mutable_uri = newnode.get_uri()
d.addCallback(_got)
# now upload it again and make sure that the URI doesn't change
NEWER_CONTENTS = self.NEWFILE_CONTENTS + "newer\n"
d.addCallback(lambda res:
self.POST(self.public_url + "/foo", t="upload",
mutable="true",
file=("new.txt", NEWER_CONTENTS)))
d.addCallback(self.failUnlessURIMatchesChild, fn, "new.txt")
d.addCallback(lambda res:
self.failUnlessChildContentsAre(fn, "new.txt",
NEWER_CONTENTS))
d.addCallback(lambda res: self._foo_node.get("new.txt"))
def _got2(newnode):
self.failUnless(IMutableFileNode.providedBy(newnode))
self.failUnless(newnode.is_mutable())
self.failIf(newnode.is_readonly())
self.failUnlessEqual(self._mutable_uri, newnode.get_uri())
d.addCallback(_got2)
# also test t=overwrite while we're here
EVEN_NEWER_CONTENTS = NEWER_CONTENTS + "even newer\n"
d.addCallback(lambda res:
self.POST(self.public_url + "/foo/new.txt",
t="overwrite",
file=("new.txt", EVEN_NEWER_CONTENTS)))
d.addCallback(self.failUnlessURIMatchesChild, fn, "new.txt")
d.addCallback(lambda res:
self.failUnlessChildContentsAre(fn, "new.txt",
EVEN_NEWER_CONTENTS))
d.addCallback(lambda res: self._foo_node.get("new.txt"))
def _got3(newnode):
self.failUnless(IMutableFileNode.providedBy(newnode))
self.failUnless(newnode.is_mutable())
self.failIf(newnode.is_readonly())
self.failUnlessEqual(self._mutable_uri, newnode.get_uri())
d.addCallback(_got3)
return d
def test_POST_upload_replace(self): def test_POST_upload_replace(self):
d = self.POST(self.public_url + "/foo", t="upload", d = self.POST(self.public_url + "/foo", t="upload",
file=("bar.txt", self.NEWFILE_CONTENTS)) file=("bar.txt", self.NEWFILE_CONTENTS))
@ -1118,7 +1167,7 @@ class Web(WebMixin, unittest.TestCase):
return d return d
def failUnlessIsEmptyJSON(self, res): def failUnlessIsEmptyJSON(self, res):
data = self.worlds_cheapest_json_decoder(res) data = simplejson.loads(res)
self.failUnlessEqual(data[0], "dirnode", data) self.failUnlessEqual(data[0], "dirnode", data)
self.failUnlessEqual(len(data[1]["children"]), 0) self.failUnlessEqual(len(data[1]["children"]), 0)

View File

@ -788,13 +788,26 @@ class POSTHandler(rend.Page):
data = contents.file.read() data = contents.file.read()
uploadable = upload.FileHandle(contents.file) uploadable = upload.FileHandle(contents.file)
d = self._check_replacement(name) d = self._check_replacement(name)
d.addCallback(lambda res: d.addCallback(lambda res: self._node.has_child(name))
IClient(ctx).create_mutable_file(data)) def _checked(present):
def _uploaded(newnode): if present:
d1 = self._node.set_node(name, newnode) # modify the existing one instead of creating a new
d1.addCallback(lambda res: newnode.get_uri()) # one
return d1 d2 = self._node.get(name)
d.addCallback(_uploaded) def _got_newnode(newnode):
d3 = newnode.replace(data)
d3.addCallback(lambda res: newnode.get_uri())
return d3
d2.addCallback(_got_newnode)
else:
d2 = IClient(ctx).create_mutable_file(data)
def _uploaded(newnode):
d1 = self._node.set_node(name, newnode)
d1.addCallback(lambda res: newnode.get_uri())
return d1
d2.addCallback(_uploaded)
return d2
d.addCallback(_checked)
else: else:
contents = req.fields["file"] contents = req.fields["file"]
name = name or contents.filename name = name or contents.filename
@ -814,7 +827,8 @@ class POSTHandler(rend.Page):
# SDMF: files are small, and we can only upload data. # SDMF: files are small, and we can only upload data.
contents.file.seek(0) contents.file.seek(0)
data = contents.file.read() data = contents.file.read()
d = self._node.get(name) # TODO: 'name' handling needs review
d = defer.succeed(self._node)
def _got_child(child_node): def _got_child(child_node):
child_node.replace(data) child_node.replace(data)
return child_node.get_uri() return child_node.get_uri()