Add t=mkdir-immutable to the webapi. Closes #607.
* change t=mkdir-with-children to not use multipart/form encoding. Instead, the request body is all JSON. t=mkdir-immutable uses this format too. * make nodemaker.create_immutable_dirnode() get convergence from SecretHolder, but let callers override it * raise NotDeepImmutableError instead of using assert() * add mutable= argument to DirectoryNode.create_subdirectory(), default True
This commit is contained in:
parent
d2426ea3bd
commit
f85690697a
|
@ -356,17 +356,15 @@ POST /uri?t=mkdir-with-children
|
||||||
write-cap as the HTTP response body. The new directory is not attached to
|
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.
|
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
|
Initial children are provided as the body of the POST form (this is more
|
||||||
is more efficient than doing separate mkdir and add-children operations. If
|
efficient than doing separate mkdir and set_children operations). If the
|
||||||
this value is empty, the new directory will be empty.
|
body is empty, the new directory will be empty. If not empty, the body will
|
||||||
|
be interpreted as a JSON-encoded dictionary of children with which the new
|
||||||
If not empty, it will be interpreted as a JSON-encoded dictionary of
|
directory should be populated, using the same format as would be returned in
|
||||||
children with which the new directory should be populated, using the same
|
the 'children' value of the t=json GET request, described below. Each
|
||||||
format as would be returned in the 'children' value of the t=json GET
|
dictionary key should be a child name, and each value should be a list of
|
||||||
request, described below. Each dictionary key should be a child name, and
|
[TYPE, PROPDICT], where PROPDICT contains "rw_uri", "ro_uri", and "metadata"
|
||||||
each value should be a list of [TYPE, PROPDICT], where PROPDICT contains
|
keys (all others are ignored). For example, the PUT request body could be:
|
||||||
"rw_uri", "ro_uri", and "metadata" keys (all others are ignored). For
|
|
||||||
example, the PUT request body could be:
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"Fran\u00e7ais": [ "filenode", {
|
"Fran\u00e7ais": [ "filenode", {
|
||||||
|
@ -391,6 +389,20 @@ POST /uri?t=mkdir-with-children
|
||||||
} } } ]
|
} } } ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Note that the webapi-using client application must not provide the
|
||||||
|
"Content-Type: multipart/form-data" header that usually accompanies HTML
|
||||||
|
form submissions, since the body is not formatted this way. Doing so will
|
||||||
|
cause a server error as the lower-level code misparses the request body.
|
||||||
|
|
||||||
|
POST /uri?t=mkdir-immutable
|
||||||
|
|
||||||
|
Like t=mkdir-with-children above, but the new directory will be
|
||||||
|
deep-immutable. This means that the directory itself is immutable, and that
|
||||||
|
it can only contain deep-immutable objects, like immutable files, literal
|
||||||
|
files, and deep-immutable directories. A non-empty request body is
|
||||||
|
mandatory, since after the directory is created, it will not be possible to
|
||||||
|
add more children to it.
|
||||||
|
|
||||||
POST /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir
|
POST /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir
|
||||||
PUT /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir
|
PUT /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir
|
||||||
|
|
||||||
|
@ -410,8 +422,13 @@ PUT /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir
|
||||||
POST /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir-with-children
|
POST /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir-with-children
|
||||||
|
|
||||||
Like above, but if the final directory is created, it will be populated with
|
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
|
initial children from the POST request body, as described above in the
|
||||||
the /uri?t=mkdir-with-children operation.
|
/uri?t=mkdir-with-children operation.
|
||||||
|
|
||||||
|
POST /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir-immutable
|
||||||
|
|
||||||
|
Like above, but the final directory will be deep-immutable, with the
|
||||||
|
children specified as a JSON dictionary in the POST request body.
|
||||||
|
|
||||||
POST /uri/$DIRCAP/[SUBDIRS../]?t=mkdir&name=NAME
|
POST /uri/$DIRCAP/[SUBDIRS../]?t=mkdir&name=NAME
|
||||||
|
|
||||||
|
@ -425,8 +442,15 @@ POST /uri/$DIRCAP/[SUBDIRS../]?t=mkdir&name=NAME
|
||||||
POST /uri/$DIRCAP/[SUBDIRS../]?t=mkdir-with-children&name=NAME
|
POST /uri/$DIRCAP/[SUBDIRS../]?t=mkdir-with-children&name=NAME
|
||||||
|
|
||||||
As above, but the new directory will be populated with initial children via
|
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
|
the POST request body, as described in /uri?t=mkdir-with-children above.
|
||||||
above.
|
Note that the name= argument must be passed as a queryarg, because the POST
|
||||||
|
request body is used for the initial children JSON.
|
||||||
|
|
||||||
|
POST /uri/$DIRCAP/[SUBDIRS../]?t=mkdir-immutable&name=NAME
|
||||||
|
|
||||||
|
As above, but the new directory will be deep-immutable, with the children
|
||||||
|
specified as a JSON dictionary in the POST request body. Again, the name=
|
||||||
|
argument must be passed as a queryarg.
|
||||||
|
|
||||||
=== Get Information About A File Or Directory (as JSON) ===
|
=== Get Information About A File Or Directory (as JSON) ===
|
||||||
|
|
||||||
|
|
|
@ -465,9 +465,8 @@ class Client(node.Node, pollmixin.PollMixin):
|
||||||
def create_dirnode(self, initial_children={}):
|
def create_dirnode(self, initial_children={}):
|
||||||
d = self.nodemaker.create_new_mutable_directory(initial_children)
|
d = self.nodemaker.create_new_mutable_directory(initial_children)
|
||||||
return d
|
return d
|
||||||
def create_immutable_dirnode(self, children):
|
def create_immutable_dirnode(self, children, convergence=None):
|
||||||
return self.nodemaker.create_immutable_directory(children,
|
return self.nodemaker.create_immutable_directory(children, convergence)
|
||||||
self.convergence)
|
|
||||||
|
|
||||||
def create_mutable_file(self, contents=None, keysize=None):
|
def create_mutable_file(self, contents=None, keysize=None):
|
||||||
return self.nodemaker.create_mutable_file(contents, keysize)
|
return self.nodemaker.create_mutable_file(contents, keysize)
|
||||||
|
|
|
@ -188,6 +188,8 @@ class DirectoryNode:
|
||||||
filenode_class = MutableFileNode
|
filenode_class = MutableFileNode
|
||||||
|
|
||||||
def __init__(self, filenode, nodemaker, uploader):
|
def __init__(self, filenode, nodemaker, uploader):
|
||||||
|
assert IFileNode.providedBy(filenode), filenode
|
||||||
|
assert not IDirectoryNode.providedBy(filenode), filenode
|
||||||
self._node = filenode
|
self._node = filenode
|
||||||
filenode_cap = filenode.get_cap()
|
filenode_cap = filenode.get_cap()
|
||||||
self._uri = wrap_dirnode_cap(filenode_cap)
|
self._uri = wrap_dirnode_cap(filenode_cap)
|
||||||
|
@ -491,11 +493,15 @@ class DirectoryNode:
|
||||||
d.addCallback(lambda res: deleter.old_child)
|
d.addCallback(lambda res: deleter.old_child)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def create_subdirectory(self, name, initial_children={}, overwrite=True):
|
def create_subdirectory(self, name, initial_children={}, overwrite=True,
|
||||||
|
mutable=True):
|
||||||
assert isinstance(name, unicode)
|
assert isinstance(name, unicode)
|
||||||
if self.is_readonly():
|
if self.is_readonly():
|
||||||
return defer.fail(NotMutableError())
|
return defer.fail(NotMutableError())
|
||||||
d = self._nodemaker.create_new_mutable_directory(initial_children)
|
if mutable:
|
||||||
|
d = self._nodemaker.create_new_mutable_directory(initial_children)
|
||||||
|
else:
|
||||||
|
d = self._nodemaker.create_immutable_directory(initial_children)
|
||||||
def _created(child):
|
def _created(child):
|
||||||
entries = {name: (child, None)}
|
entries = {name: (child, None)}
|
||||||
a = Adder(self, entries, overwrite=overwrite)
|
a = Adder(self, entries, overwrite=overwrite)
|
||||||
|
|
|
@ -480,6 +480,9 @@ class UnhandledCapTypeError(Exception):
|
||||||
"""I recognize the cap/URI, but I cannot create an IFilesystemNode for
|
"""I recognize the cap/URI, but I cannot create an IFilesystemNode for
|
||||||
it."""
|
it."""
|
||||||
|
|
||||||
|
class NotDeepImmutableError(Exception):
|
||||||
|
"""Deep-immutable directories can only contain deep-immutable children"""
|
||||||
|
|
||||||
class IFilesystemNode(Interface):
|
class IFilesystemNode(Interface):
|
||||||
def get_cap():
|
def get_cap():
|
||||||
"""Return the strongest 'cap instance' associated with this node.
|
"""Return the strongest 'cap instance' associated with this node.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import weakref
|
import weakref
|
||||||
from zope.interface import implements
|
from zope.interface import implements
|
||||||
from allmydata.util.assertutil import precondition
|
from allmydata.util.assertutil import precondition
|
||||||
from allmydata.interfaces import INodeMaker
|
from allmydata.interfaces import INodeMaker, NotDeepImmutableError
|
||||||
from allmydata.immutable.filenode import FileNode, LiteralFileNode
|
from allmydata.immutable.filenode import FileNode, LiteralFileNode
|
||||||
from allmydata.immutable.upload import Data
|
from allmydata.immutable.upload import Data
|
||||||
from allmydata.mutable.filenode import MutableFileNode
|
from allmydata.mutable.filenode import MutableFileNode
|
||||||
|
@ -101,14 +101,16 @@ class NodeMaker:
|
||||||
d.addCallback(self._create_dirnode)
|
d.addCallback(self._create_dirnode)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def create_immutable_directory(self, children, convergence):
|
def create_immutable_directory(self, children, convergence=None):
|
||||||
|
if convergence is None:
|
||||||
|
convergence = self.secret_holder.get_convergence_secret()
|
||||||
for (name, (node, metadata)) in children.iteritems():
|
for (name, (node, metadata)) in children.iteritems():
|
||||||
precondition(not isinstance(node, UnknownNode),
|
precondition(not isinstance(node, UnknownNode),
|
||||||
"create_immutable_directory does not accept UnknownNode", node)
|
"create_immutable_directory does not accept UnknownNode", node)
|
||||||
precondition(isinstance(metadata, dict),
|
precondition(isinstance(metadata, dict),
|
||||||
"create_immutable_directory requires metadata to be a dict, not None", metadata)
|
"create_immutable_directory requires metadata to be a dict, not None", metadata)
|
||||||
precondition(not node.is_mutable(),
|
if node.is_mutable():
|
||||||
"create_immutable_directory requires immutable children", node)
|
raise NotDeepImmutableError("%s is not immutable" % (node,))
|
||||||
n = DummyImmutableFileNode() # writekey=None
|
n = DummyImmutableFileNode() # writekey=None
|
||||||
packed = pack_children(n, children)
|
packed = pack_children(n, children)
|
||||||
uploadable = Data(packed, convergence)
|
uploadable = Data(packed, convergence)
|
||||||
|
|
|
@ -47,6 +47,8 @@ class FakeCHKFileNode:
|
||||||
return self.my_uri.to_string()
|
return self.my_uri.to_string()
|
||||||
def get_readonly_uri(self):
|
def get_readonly_uri(self):
|
||||||
return self.my_uri.to_string()
|
return self.my_uri.to_string()
|
||||||
|
def get_cap(self):
|
||||||
|
return self.my_uri
|
||||||
def get_verify_cap(self):
|
def get_verify_cap(self):
|
||||||
return self.my_uri.get_verify_cap()
|
return self.my_uri.get_verify_cap()
|
||||||
def get_repair_cap(self):
|
def get_repair_cap(self):
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
from zope.interface import implements
|
||||||
from twisted.trial import unittest
|
from twisted.trial import unittest
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
from allmydata import uri, dirnode
|
from allmydata import uri, dirnode
|
||||||
from allmydata.client import Client
|
from allmydata.client import Client
|
||||||
from allmydata.immutable import upload
|
from allmydata.immutable import upload
|
||||||
from allmydata.interfaces import IFileNode, \
|
from allmydata.interfaces import IFileNode, IMutableFileNode, \
|
||||||
ExistingChildError, NoSuchChildError, \
|
ExistingChildError, NoSuchChildError, NotDeepImmutableError, \
|
||||||
IDeepCheckResults, IDeepCheckAndRepairResults, CannotPackUnknownNodeError
|
IDeepCheckResults, IDeepCheckAndRepairResults, CannotPackUnknownNodeError
|
||||||
from allmydata.mutable.filenode import MutableFileNode
|
from allmydata.mutable.filenode import MutableFileNode
|
||||||
from allmydata.mutable.common import UncoordinatedWriteError
|
from allmydata.mutable.common import UncoordinatedWriteError
|
||||||
|
@ -137,14 +138,14 @@ class Dirnode(GridTestMixin, unittest.TestCase,
|
||||||
bad_kids2))
|
bad_kids2))
|
||||||
bad_kids3 = {u"one": (nm.create_from_cap(mut_writecap), {})}
|
bad_kids3 = {u"one": (nm.create_from_cap(mut_writecap), {})}
|
||||||
d.addCallback(lambda ign:
|
d.addCallback(lambda ign:
|
||||||
self.shouldFail(AssertionError, "bad_kids3",
|
self.shouldFail(NotDeepImmutableError, "bad_kids3",
|
||||||
"create_immutable_directory requires immutable children",
|
"is not immutable",
|
||||||
c.create_immutable_dirnode,
|
c.create_immutable_dirnode,
|
||||||
bad_kids3))
|
bad_kids3))
|
||||||
bad_kids4 = {u"one": (nm.create_from_cap(mut_readcap), {})}
|
bad_kids4 = {u"one": (nm.create_from_cap(mut_readcap), {})}
|
||||||
d.addCallback(lambda ign:
|
d.addCallback(lambda ign:
|
||||||
self.shouldFail(AssertionError, "bad_kids4",
|
self.shouldFail(NotDeepImmutableError, "bad_kids4",
|
||||||
"create_immutable_directory requires immutable children",
|
"is not immutable",
|
||||||
c.create_immutable_dirnode,
|
c.create_immutable_dirnode,
|
||||||
bad_kids4))
|
bad_kids4))
|
||||||
d.addCallback(lambda ign: c.create_immutable_dirnode({}))
|
d.addCallback(lambda ign: c.create_immutable_dirnode({}))
|
||||||
|
@ -177,8 +178,32 @@ class Dirnode(GridTestMixin, unittest.TestCase,
|
||||||
return dn.list()
|
return dn.list()
|
||||||
d.addCallback(_created_small)
|
d.addCallback(_created_small)
|
||||||
d.addCallback(lambda kids: self.failUnlessEqual(kids.keys(), [u"o"]))
|
d.addCallback(lambda kids: self.failUnlessEqual(kids.keys(), [u"o"]))
|
||||||
|
|
||||||
|
# now test n.create_subdirectory(mutable=False)
|
||||||
|
d.addCallback(lambda ign: c.create_dirnode())
|
||||||
|
def _made_parent(n):
|
||||||
|
d = n.create_subdirectory(u"subdir", kids, mutable=False)
|
||||||
|
d.addCallback(lambda sd: sd.list())
|
||||||
|
d.addCallback(_check_kids)
|
||||||
|
d.addCallback(lambda ign: n.list())
|
||||||
|
d.addCallback(lambda children:
|
||||||
|
self.failUnlessEqual(children.keys(), [u"subdir"]))
|
||||||
|
d.addCallback(lambda ign: n.get(u"subdir"))
|
||||||
|
d.addCallback(lambda sd: sd.list())
|
||||||
|
d.addCallback(_check_kids)
|
||||||
|
d.addCallback(lambda ign: n.get(u"subdir"))
|
||||||
|
d.addCallback(lambda sd: self.failIf(sd.is_mutable()))
|
||||||
|
bad_kids = {u"one": (nm.create_from_cap(mut_writecap), {})}
|
||||||
|
d.addCallback(lambda ign:
|
||||||
|
self.shouldFail(NotDeepImmutableError, "YZ",
|
||||||
|
"is not immutable",
|
||||||
|
n.create_subdirectory,
|
||||||
|
u"sub2", bad_kids, mutable=False))
|
||||||
|
return d
|
||||||
|
d.addCallback(_made_parent)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
def test_check(self):
|
def test_check(self):
|
||||||
self.basedir = "dirnode/Dirnode/test_check"
|
self.basedir = "dirnode/Dirnode/test_check"
|
||||||
self.set_up_grid()
|
self.set_up_grid()
|
||||||
|
@ -972,6 +997,7 @@ class Packing(unittest.TestCase):
|
||||||
fn, kids, deep_immutable=True)
|
fn, kids, deep_immutable=True)
|
||||||
|
|
||||||
class FakeMutableFile:
|
class FakeMutableFile:
|
||||||
|
implements(IMutableFileNode)
|
||||||
counter = 0
|
counter = 0
|
||||||
def __init__(self, initial_contents=""):
|
def __init__(self, initial_contents=""):
|
||||||
self.data = self._get_initial_contents(initial_contents)
|
self.data = self._get_initial_contents(initial_contents)
|
||||||
|
|
|
@ -714,7 +714,8 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
|
||||||
d.addCallback(lambda junk: self.clients[3].create_dirnode())
|
d.addCallback(lambda junk: self.clients[3].create_dirnode())
|
||||||
d.addCallback(check_kg_poolsize, -2)
|
d.addCallback(check_kg_poolsize, -2)
|
||||||
# use_helper induces use of clients[3], which is the using-key_gen client
|
# use_helper induces use of clients[3], which is the using-key_gen client
|
||||||
d.addCallback(lambda junk: self.POST("uri", use_helper=True, t="mkdir", name='george'))
|
d.addCallback(lambda junk:
|
||||||
|
self.POST("uri?t=mkdir&name=george", use_helper=True))
|
||||||
d.addCallback(check_kg_poolsize, -3)
|
d.addCallback(check_kg_poolsize, -3)
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
@ -1053,10 +1054,6 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
|
||||||
return getPage(url, method="GET", followRedirect=followRedirect)
|
return getPage(url, method="GET", followRedirect=followRedirect)
|
||||||
|
|
||||||
def POST(self, urlpath, followRedirect=False, use_helper=False, **fields):
|
def POST(self, urlpath, followRedirect=False, use_helper=False, **fields):
|
||||||
if use_helper:
|
|
||||||
url = self.helper_webish_url + urlpath
|
|
||||||
else:
|
|
||||||
url = self.webish_url + urlpath
|
|
||||||
sepbase = "boogabooga"
|
sepbase = "boogabooga"
|
||||||
sep = "--" + sepbase
|
sep = "--" + sepbase
|
||||||
form = []
|
form = []
|
||||||
|
@ -1076,11 +1073,21 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
|
||||||
form.append(str(value))
|
form.append(str(value))
|
||||||
form.append(sep)
|
form.append(sep)
|
||||||
form[-1] += "--"
|
form[-1] += "--"
|
||||||
body = "\r\n".join(form) + "\r\n"
|
body = ""
|
||||||
headers = {"content-type": "multipart/form-data; boundary=%s" % sepbase,
|
headers = {}
|
||||||
}
|
if fields:
|
||||||
return getPage(url, method="POST", postdata=body,
|
body = "\r\n".join(form) + "\r\n"
|
||||||
headers=headers, followRedirect=followRedirect)
|
headers["content-type"] = "multipart/form-data; boundary=%s" % sepbase
|
||||||
|
return self.POST2(urlpath, body, headers, followRedirect, use_helper)
|
||||||
|
|
||||||
|
def POST2(self, urlpath, body="", headers={}, followRedirect=False,
|
||||||
|
use_helper=False):
|
||||||
|
if use_helper:
|
||||||
|
url = self.helper_webish_url + urlpath
|
||||||
|
else:
|
||||||
|
url = self.webish_url + urlpath
|
||||||
|
return getPage(url, method="POST", postdata=body, headers=headers,
|
||||||
|
followRedirect=followRedirect)
|
||||||
|
|
||||||
def _test_web(self, res):
|
def _test_web(self, res):
|
||||||
base = self.webish_url
|
base = self.webish_url
|
||||||
|
|
|
@ -281,7 +281,6 @@ class WebMixin(object):
|
||||||
return client.getPage(url, method="DELETE")
|
return client.getPage(url, method="DELETE")
|
||||||
|
|
||||||
def POST(self, urlpath, followRedirect=False, **fields):
|
def POST(self, urlpath, followRedirect=False, **fields):
|
||||||
url = self.webish_url + urlpath
|
|
||||||
sepbase = "boogabooga"
|
sepbase = "boogabooga"
|
||||||
sep = "--" + sepbase
|
sep = "--" + sepbase
|
||||||
form = []
|
form = []
|
||||||
|
@ -306,9 +305,15 @@ class WebMixin(object):
|
||||||
form.append(value)
|
form.append(value)
|
||||||
form.append(sep)
|
form.append(sep)
|
||||||
form[-1] += "--"
|
form[-1] += "--"
|
||||||
body = "\r\n".join(form) + "\r\n"
|
body = ""
|
||||||
headers = {"content-type": "multipart/form-data; boundary=%s" % sepbase,
|
headers = {}
|
||||||
}
|
if fields:
|
||||||
|
body = "\r\n".join(form) + "\r\n"
|
||||||
|
headers["content-type"] = "multipart/form-data; boundary=%s" % sepbase
|
||||||
|
return self.POST2(urlpath, body, headers, followRedirect)
|
||||||
|
|
||||||
|
def POST2(self, urlpath, body="", headers={}, followRedirect=False):
|
||||||
|
url = self.webish_url + urlpath
|
||||||
return client.getPage(url, method="POST", postdata=body,
|
return client.getPage(url, method="POST", postdata=body,
|
||||||
headers=headers, followRedirect=followRedirect)
|
headers=headers, followRedirect=followRedirect)
|
||||||
|
|
||||||
|
@ -1125,8 +1130,8 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
|
||||||
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-with-children",
|
d = self.POST2(self.public_url + "/foo/newdir?t=mkdir-with-children",
|
||||||
children=simplejson.dumps(newkids))
|
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())
|
||||||
d2 = self.failUnlessNodeKeysAre(n, newkids.keys())
|
d2 = self.failUnlessNodeKeysAre(n, newkids.keys())
|
||||||
|
@ -1150,6 +1155,42 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
|
||||||
d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1)
|
d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def test_POST_NEWDIRURL_immutable(self):
|
||||||
|
(newkids, filecap1, immdircap) = self._create_immutable_children()
|
||||||
|
d = self.POST2(self.public_url + "/foo/newdir?t=mkdir-immutable",
|
||||||
|
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"dirchild-imm",
|
||||||
|
immdircap))
|
||||||
|
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)
|
||||||
|
d.addCallback(lambda res: self._foo_node.get(u"newdir"))
|
||||||
|
d.addCallback(self.failUnlessChildURIIs, u"dirchild-imm", immdircap)
|
||||||
|
d.addErrback(self.explain_web_error)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_POST_NEWDIRURL_immutable_bad(self):
|
||||||
|
(newkids, filecap1, filecap2, filecap3,
|
||||||
|
dircap) = self._create_initial_children()
|
||||||
|
d = self.shouldFail2(error.Error, "test_POST_NEWDIRURL_immutable_bad",
|
||||||
|
"400 Bad Request",
|
||||||
|
"a mkdir-immutable operation was given a child that was not itself immutable",
|
||||||
|
self.POST2,
|
||||||
|
self.public_url + "/foo/newdir?t=mkdir-immutable",
|
||||||
|
simplejson.dumps(newkids))
|
||||||
|
return d
|
||||||
|
|
||||||
def test_PUT_NEWDIRURL_exists(self):
|
def test_PUT_NEWDIRURL_exists(self):
|
||||||
d = self.PUT(self.public_url + "/foo/sub?t=mkdir", "")
|
d = self.PUT(self.public_url + "/foo/sub?t=mkdir", "")
|
||||||
d.addCallback(lambda res:
|
d.addCallback(lambda res:
|
||||||
|
@ -1898,8 +1939,9 @@ 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-with-children",
|
d = self.POST2(self.public_url +
|
||||||
name="newdir", children=simplejson.dumps(newkids))
|
"/foo?t=mkdir-with-children&name=newdir",
|
||||||
|
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"))
|
||||||
|
@ -1908,6 +1950,33 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
|
||||||
d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1)
|
d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def test_POST_mkdir_immutable(self):
|
||||||
|
(newkids, filecap1, immdircap) = self._create_immutable_children()
|
||||||
|
d = self.POST2(self.public_url +
|
||||||
|
"/foo?t=mkdir-immutable&name=newdir",
|
||||||
|
simplejson.dumps(newkids))
|
||||||
|
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)
|
||||||
|
d.addCallback(lambda res: self._foo_node.get(u"newdir"))
|
||||||
|
d.addCallback(self.failUnlessChildURIIs, u"dirchild-imm", immdircap)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_POST_mkdir_immutable_bad(self):
|
||||||
|
(newkids, filecap1, filecap2, filecap3,
|
||||||
|
dircap) = self._create_initial_children()
|
||||||
|
d = self.shouldFail2(error.Error, "test_POST_mkdir_immutable_bad",
|
||||||
|
"400 Bad Request",
|
||||||
|
"a mkdir-immutable operation was given a child that was not itself immutable",
|
||||||
|
self.POST2,
|
||||||
|
self.public_url +
|
||||||
|
"/foo?t=mkdir-immutable&name=newdir",
|
||||||
|
simplejson.dumps(newkids))
|
||||||
|
return d
|
||||||
|
|
||||||
def test_POST_mkdir_2(self):
|
def test_POST_mkdir_2(self):
|
||||||
d = self.POST(self.public_url + "/foo/newdir?t=mkdir", "")
|
d = self.POST(self.public_url + "/foo/newdir?t=mkdir", "")
|
||||||
d.addCallback(lambda res:
|
d.addCallback(lambda res:
|
||||||
|
@ -1957,11 +2026,23 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
|
||||||
}
|
}
|
||||||
return newkids, filecap1, filecap2, filecap3, dircap
|
return newkids, filecap1, filecap2, filecap3, dircap
|
||||||
|
|
||||||
|
def _create_immutable_children(self):
|
||||||
|
contents, n, filecap1 = self.makefile(12)
|
||||||
|
md1 = {"metakey1": "metavalue1"}
|
||||||
|
tnode = create_chk_filenode("immutable directory contents\n"*10)
|
||||||
|
dnode = DirectoryNode(tnode, None, None)
|
||||||
|
assert not dnode.is_mutable()
|
||||||
|
immdircap = dnode.get_uri()
|
||||||
|
newkids = {u"child-imm": ["filenode", {"ro_uri": filecap1,
|
||||||
|
"metadata": md1, }],
|
||||||
|
u"dirchild-imm": ["dirnode", {"ro_uri": immdircap}],
|
||||||
|
}
|
||||||
|
return newkids, filecap1, immdircap
|
||||||
|
|
||||||
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-with-children",
|
d = self.POST2("/uri?t=mkdir-with-children", simplejson.dumps(newkids))
|
||||||
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)
|
||||||
|
@ -1989,8 +2070,8 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
|
||||||
400, "Bad Request",
|
400, "Bad Request",
|
||||||
"t=mkdir does not accept children=, "
|
"t=mkdir does not accept children=, "
|
||||||
"try t=mkdir-with-children instead",
|
"try t=mkdir-with-children instead",
|
||||||
self.POST, "/uri?t=mkdir", # without children
|
self.POST2, "/uri?t=mkdir", # without children
|
||||||
children=simplejson.dumps(newkids))
|
simplejson.dumps(newkids))
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def test_POST_noparent_bad(self):
|
def test_POST_noparent_bad(self):
|
||||||
|
@ -2000,6 +2081,34 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
|
||||||
self.POST, "/uri?t=bogus")
|
self.POST, "/uri?t=bogus")
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def test_POST_mkdir_no_parentdir_immutable(self):
|
||||||
|
(newkids, filecap1, immdircap) = self._create_immutable_children()
|
||||||
|
d = self.POST2("/uri?t=mkdir-immutable", simplejson.dumps(newkids))
|
||||||
|
def _after_mkdir(res):
|
||||||
|
self.failUnless(res.startswith("URI:DIR"), res)
|
||||||
|
n = self.s.create_node_from_uri(res)
|
||||||
|
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"dirchild-imm",
|
||||||
|
immdircap))
|
||||||
|
return d2
|
||||||
|
d.addCallback(_after_mkdir)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_POST_mkdir_no_parentdir_immutable_bad(self):
|
||||||
|
(newkids, filecap1, filecap2, filecap3,
|
||||||
|
dircap) = self._create_initial_children()
|
||||||
|
d = self.shouldFail2(error.Error,
|
||||||
|
"test_POST_mkdir_no_parentdir_immutable_bad",
|
||||||
|
"400 Bad Request",
|
||||||
|
"a mkdir-immutable operation was given a child that was not itself immutable",
|
||||||
|
self.POST2,
|
||||||
|
"/uri?t=mkdir-immutable",
|
||||||
|
simplejson.dumps(newkids))
|
||||||
|
return d
|
||||||
|
|
||||||
def test_welcome_page_mkdir_button(self):
|
def test_welcome_page_mkdir_button(self):
|
||||||
# Fetch the welcome page.
|
# Fetch the welcome page.
|
||||||
d = self.GET("/")
|
d = self.GET("/")
|
||||||
|
|
|
@ -7,7 +7,8 @@ from nevow import loaders, appserver
|
||||||
from nevow.inevow import IRequest
|
from nevow.inevow import IRequest
|
||||||
from nevow.util import resource_filename
|
from nevow.util import resource_filename
|
||||||
from allmydata.interfaces import ExistingChildError, NoSuchChildError, \
|
from allmydata.interfaces import ExistingChildError, NoSuchChildError, \
|
||||||
FileTooLargeError, NotEnoughSharesError, NoSharesError
|
FileTooLargeError, NotEnoughSharesError, NoSharesError, \
|
||||||
|
NotDeepImmutableError
|
||||||
from allmydata.mutable.common import UnrecoverableFileError
|
from allmydata.mutable.common import UnrecoverableFileError
|
||||||
from allmydata.util import abbreviate # TODO: consolidate
|
from allmydata.util import abbreviate # TODO: consolidate
|
||||||
|
|
||||||
|
@ -57,8 +58,9 @@ def get_arg(ctx_or_req, argname, default=None, multiple=False):
|
||||||
def convert_children_json(nodemaker, 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=). This is used by
|
||||||
initial_children = {}
|
t=mkdir-with-children and t=mkdir-immutable"""
|
||||||
|
children = {}
|
||||||
if children_json:
|
if children_json:
|
||||||
data = simplejson.loads(children_json)
|
data = simplejson.loads(children_json)
|
||||||
for (name, (ctype, propdict)) in data.iteritems():
|
for (name, (ctype, propdict)) in data.iteritems():
|
||||||
|
@ -71,8 +73,8 @@ def convert_children_json(nodemaker, children_json):
|
||||||
readcap = str(readcap)
|
readcap = str(readcap)
|
||||||
metadata = propdict.get("metadata", {})
|
metadata = propdict.get("metadata", {})
|
||||||
childnode = nodemaker.create_from_cap(writecap, readcap)
|
childnode = nodemaker.create_from_cap(writecap, readcap)
|
||||||
initial_children[name] = (childnode, metadata)
|
children[name] = (childnode, metadata)
|
||||||
return initial_children
|
return children
|
||||||
|
|
||||||
def abbreviate_time(data):
|
def abbreviate_time(data):
|
||||||
# 1.23s, 790ms, 132us
|
# 1.23s, 790ms, 132us
|
||||||
|
@ -176,6 +178,10 @@ def humanize_failure(f):
|
||||||
"failure, or disk corruption. You should perform a filecheck on "
|
"failure, or disk corruption. You should perform a filecheck on "
|
||||||
"this object to learn more.")
|
"this object to learn more.")
|
||||||
return (t, http.GONE)
|
return (t, http.GONE)
|
||||||
|
if f.check(NotDeepImmutableError):
|
||||||
|
t = ("NotDeepImmutableError: a mkdir-immutable operation was given "
|
||||||
|
"a child that was not itself immutable: %s" % (f.value,))
|
||||||
|
return (t, http.BAD_REQUEST)
|
||||||
if f.check(WebError):
|
if f.check(WebError):
|
||||||
return (f.value.text, f.value.code)
|
return (f.value.text, f.value.code)
|
||||||
if f.check(FileTooLargeError):
|
if f.check(FileTooLargeError):
|
||||||
|
|
|
@ -94,15 +94,21 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
|
||||||
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") ]:
|
("POST", "mkdir-with-children"),
|
||||||
|
("POST", "mkdir-immutable") ]:
|
||||||
if DEBUG: print " making final directory"
|
if DEBUG: print " making final directory"
|
||||||
# final directory
|
# final directory
|
||||||
kids = {}
|
kids = {}
|
||||||
if (method,t) == ("POST", "mkdir-with-children"):
|
if t in ("mkdir-with-children", "mkdir-immutable"):
|
||||||
kids_json = get_arg(req, "children", "")
|
req.content.seek(0)
|
||||||
|
kids_json = req.content.read()
|
||||||
kids = convert_children_json(self.client.nodemaker,
|
kids = convert_children_json(self.client.nodemaker,
|
||||||
kids_json)
|
kids_json)
|
||||||
d = self.node.create_subdirectory(name, kids)
|
mutable = True
|
||||||
|
if t == "mkdir-immutable":
|
||||||
|
mutable = False
|
||||||
|
d = self.node.create_subdirectory(name, kids,
|
||||||
|
mutable=mutable)
|
||||||
d.addCallback(make_handler_for,
|
d.addCallback(make_handler_for,
|
||||||
self.client, self.node, name)
|
self.client, self.node, name)
|
||||||
return d
|
return d
|
||||||
|
@ -186,6 +192,8 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
|
||||||
d = self._POST_mkdir(req)
|
d = self._POST_mkdir(req)
|
||||||
elif t == "mkdir-with-children":
|
elif t == "mkdir-with-children":
|
||||||
d = self._POST_mkdir_with_children(req)
|
d = self._POST_mkdir_with_children(req)
|
||||||
|
elif t == "mkdir-immutable":
|
||||||
|
d = self._POST_mkdir_immutable(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)
|
||||||
|
@ -242,12 +250,28 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
|
||||||
return defer.succeed(self.node.get_uri()) # TODO: urlencode
|
return defer.succeed(self.node.get_uri()) # TODO: urlencode
|
||||||
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", "")
|
req.content.seek(0)
|
||||||
|
kids_json = req.content.read()
|
||||||
kids = convert_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
|
||||||
|
|
||||||
|
def _POST_mkdir_immutable(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"))
|
||||||
|
req.content.seek(0)
|
||||||
|
kids_json = req.content.read()
|
||||||
|
kids = convert_children_json(self.client.nodemaker, kids_json)
|
||||||
|
d = self.node.create_subdirectory(name, kids, mutable=False)
|
||||||
|
d.addCallback(lambda child: child.get_uri()) # TODO: urlencode
|
||||||
|
return d
|
||||||
|
|
||||||
def _POST_mkdir_p(self, req):
|
def _POST_mkdir_p(self, req):
|
||||||
path = get_arg(req, "path")
|
path = get_arg(req, "path")
|
||||||
if not path:
|
if not path:
|
||||||
|
|
|
@ -73,6 +73,9 @@ class URIHandler(RenderMixin, rend.Page):
|
||||||
elif t == "mkdir-with-children":
|
elif t == "mkdir-with-children":
|
||||||
return unlinked.POSTUnlinkedCreateDirectoryWithChildren(req,
|
return unlinked.POSTUnlinkedCreateDirectoryWithChildren(req,
|
||||||
self.client)
|
self.client)
|
||||||
|
elif t == "mkdir-immutable":
|
||||||
|
return unlinked.POSTUnlinkedCreateImmutableDirectory(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)
|
||||||
|
|
|
@ -91,8 +91,9 @@ def POSTUnlinkedSSK(req, client):
|
||||||
|
|
||||||
def POSTUnlinkedCreateDirectory(req, client):
|
def POSTUnlinkedCreateDirectory(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", None)
|
req.content.seek(0)
|
||||||
if kids_json is not None:
|
kids_json = req.content.read()
|
||||||
|
if kids_json:
|
||||||
raise WebError("t=mkdir does not accept children=, "
|
raise WebError("t=mkdir does not accept children=, "
|
||||||
"try t=mkdir-with-children instead",
|
"try t=mkdir-with-children instead",
|
||||||
http.BAD_REQUEST)
|
http.BAD_REQUEST)
|
||||||
|
@ -112,7 +113,8 @@ def POSTUnlinkedCreateDirectory(req, client):
|
||||||
|
|
||||||
def POSTUnlinkedCreateDirectoryWithChildren(req, client):
|
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", "")
|
req.content.seek(0)
|
||||||
|
kids_json = req.content.read()
|
||||||
kids = convert_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")
|
||||||
|
@ -128,3 +130,21 @@ def POSTUnlinkedCreateDirectoryWithChildren(req, client):
|
||||||
d.addCallback(lambda dirnode: dirnode.get_uri())
|
d.addCallback(lambda dirnode: dirnode.get_uri())
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def POSTUnlinkedCreateImmutableDirectory(req, client):
|
||||||
|
# "POST /uri?t=mkdir", to create an unlinked directory.
|
||||||
|
req.content.seek(0)
|
||||||
|
kids_json = req.content.read()
|
||||||
|
kids = convert_children_json(client.nodemaker, kids_json)
|
||||||
|
d = client.create_immutable_dirnode(kids)
|
||||||
|
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
|
||||||
|
|
Loading…
Reference in New Issue