Adding 'move' button to web UI, closes #1579
This adds "move file" capability to the web UI's directory display. The support and test framework is heavily based on the similar "rename file" feature. Unit tests and documentation are included. Multiple in-progress versions of this patch may be found in ticket 1579. This version includes arbitrary URI target support and is compatible with the change from tahoe_css to tahoe.css.
This commit is contained in:
parent
8aa690b64e
commit
b29d0920d3
|
@ -29,8 +29,9 @@ The Tahoe REST-ful Web API
|
||||||
6. `Attaching An Existing File Or Directory (by URI)`_
|
6. `Attaching An Existing File Or Directory (by URI)`_
|
||||||
7. `Unlinking A Child`_
|
7. `Unlinking A Child`_
|
||||||
8. `Renaming A Child`_
|
8. `Renaming A Child`_
|
||||||
9. `Other Utilities`_
|
9. `Moving A Child`_
|
||||||
10. `Debugging and Testing Features`_
|
10. `Other Utilities`_
|
||||||
|
11. `Debugging and Testing Features`_
|
||||||
|
|
||||||
7. `Other Useful Pages`_
|
7. `Other Useful Pages`_
|
||||||
8. `Static Files in /public_html`_
|
8. `Static Files in /public_html`_
|
||||||
|
@ -1277,6 +1278,21 @@ Renaming A Child
|
||||||
This operation will replace any existing child of the new name, making it
|
This operation will replace any existing child of the new name, making it
|
||||||
behave like the UNIX "``mv -f``" command.
|
behave like the UNIX "``mv -f``" command.
|
||||||
|
|
||||||
|
Moving A Child
|
||||||
|
----------------
|
||||||
|
|
||||||
|
``POST /uri/$DIRCAP/[SUBDIRS../]?t=rename&from_name=OLD&to_dir=TARGET[&to_name=NEW]``
|
||||||
|
|
||||||
|
This instructs the node to move a child of the given directory to a
|
||||||
|
different directory, both of which must be mutable. The child can also be
|
||||||
|
renamed in the process. The to_dir parameter can be either the name of a
|
||||||
|
subdirectory of the dircap from which the child is being moved (multiple
|
||||||
|
levels of descent are supported) or the writecap of an unrelated directory.
|
||||||
|
|
||||||
|
This operation will replace any existing child of the new name, making it
|
||||||
|
behave like the UNIX "``mv -f``" command. The original child is not
|
||||||
|
unlinked until it is linked into the target directory.
|
||||||
|
|
||||||
Other Utilities
|
Other Utilities
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
@ -1298,6 +1314,8 @@ Other Utilities
|
||||||
functionality described above, with the provided $CHILDNAME present in the
|
functionality described above, with the provided $CHILDNAME present in the
|
||||||
'from_name' field of that form. I.e. this presents a form offering to
|
'from_name' field of that form. I.e. this presents a form offering to
|
||||||
rename $CHILDNAME, requesting the new name, and submitting POST rename.
|
rename $CHILDNAME, requesting the new name, and submitting POST rename.
|
||||||
|
This same URL format can also be used with "move-form" with the expected
|
||||||
|
results.
|
||||||
|
|
||||||
``GET /uri/$DIRCAP/[SUBDIRS../]CHILDNAME?t=uri``
|
``GET /uri/$DIRCAP/[SUBDIRS../]CHILDNAME?t=uri``
|
||||||
|
|
||||||
|
|
|
@ -245,6 +245,7 @@ class WebMixin(object):
|
||||||
self._sub_uri = sub_uri
|
self._sub_uri = sub_uri
|
||||||
foo.set_uri(u"sub", sub_uri, sub_uri)
|
foo.set_uri(u"sub", sub_uri, sub_uri)
|
||||||
sub = self.s.create_node_from_uri(sub_uri)
|
sub = self.s.create_node_from_uri(sub_uri)
|
||||||
|
self._sub_node = sub
|
||||||
|
|
||||||
_ign, n, blocking_uri = self.makefile(1)
|
_ign, n, blocking_uri = self.makefile(1)
|
||||||
foo.set_uri(u"blockingfile", blocking_uri, blocking_uri)
|
foo.set_uri(u"blockingfile", blocking_uri, blocking_uri)
|
||||||
|
@ -254,7 +255,7 @@ class WebMixin(object):
|
||||||
# still think of it as an umlaut
|
# still think of it as an umlaut
|
||||||
foo.set_uri(unicode_filename, self._bar_txt_uri, self._bar_txt_uri)
|
foo.set_uri(unicode_filename, self._bar_txt_uri, self._bar_txt_uri)
|
||||||
|
|
||||||
_ign, n, baz_file = self.makefile(2)
|
self.SUBBAZ_CONTENTS, n, baz_file = self.makefile(2)
|
||||||
self._baz_file_uri = baz_file
|
self._baz_file_uri = baz_file
|
||||||
sub.set_uri(u"baz.txt", baz_file, baz_file)
|
sub.set_uri(u"baz.txt", baz_file, baz_file)
|
||||||
|
|
||||||
|
@ -309,6 +310,9 @@ class WebMixin(object):
|
||||||
def failUnlessIsBazDotTxt(self, res):
|
def failUnlessIsBazDotTxt(self, res):
|
||||||
self.failUnlessReallyEqual(res, self.BAZ_CONTENTS, res)
|
self.failUnlessReallyEqual(res, self.BAZ_CONTENTS, res)
|
||||||
|
|
||||||
|
def failUnlessIsSubBazDotTxt(self, res):
|
||||||
|
self.failUnlessReallyEqual(res, self.SUBBAZ_CONTENTS, res)
|
||||||
|
|
||||||
def failUnlessIsBarJSON(self, res):
|
def failUnlessIsBarJSON(self, res):
|
||||||
data = simplejson.loads(res)
|
data = simplejson.loads(res)
|
||||||
self.failUnless(isinstance(data, list))
|
self.failUnless(isinstance(data, list))
|
||||||
|
@ -1258,7 +1262,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
|
||||||
r'\s+<td align="right">%d</td>' % len(self.BAR_CONTENTS),
|
r'\s+<td align="right">%d</td>' % len(self.BAR_CONTENTS),
|
||||||
])
|
])
|
||||||
self.failUnless(re.search(get_bar, res), res)
|
self.failUnless(re.search(get_bar, res), res)
|
||||||
for label in ['unlink', 'rename']:
|
for label in ['unlink', 'rename', 'move']:
|
||||||
for line in res.split("\n"):
|
for line in res.split("\n"):
|
||||||
# find the line that contains the relevant button for bar.txt
|
# find the line that contains the relevant button for bar.txt
|
||||||
if ("form action" in line and
|
if ("form action" in line and
|
||||||
|
@ -3242,6 +3246,151 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
|
||||||
d.addCallback(self.failUnlessIsFooJSON)
|
d.addCallback(self.failUnlessIsFooJSON)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def test_POST_move_file(self):
|
||||||
|
""""""
|
||||||
|
d = self.POST(self.public_url + "/foo", t="move",
|
||||||
|
from_name="bar.txt", to_dir="sub")
|
||||||
|
d.addCallback(lambda res:
|
||||||
|
self.failIfNodeHasChild(self._foo_node, u"bar.txt"))
|
||||||
|
d.addCallback(lambda res:
|
||||||
|
self.failUnlessNodeHasChild(self._sub_node, u"bar.txt"))
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/bar.txt"))
|
||||||
|
d.addCallback(self.failUnlessIsBarDotTxt)
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/bar.txt?t=json"))
|
||||||
|
d.addCallback(self.failUnlessIsBarJSON)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_POST_move_file_new_name(self):
|
||||||
|
d = self.POST(self.public_url + "/foo", t="move",
|
||||||
|
from_name="bar.txt", to_name="wibble.txt", to_dir="sub")
|
||||||
|
d.addCallback(lambda res:
|
||||||
|
self.failIfNodeHasChild(self._foo_node, u"bar.txt"))
|
||||||
|
d.addCallback(lambda res:
|
||||||
|
self.failIfNodeHasChild(self._sub_node, u"bar.txt"))
|
||||||
|
d.addCallback(lambda res:
|
||||||
|
self.failUnlessNodeHasChild(self._sub_node, u"wibble.txt"))
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/wibble.txt"))
|
||||||
|
d.addCallback(self.failUnlessIsBarDotTxt)
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/wibble.txt?t=json"))
|
||||||
|
d.addCallback(self.failUnlessIsBarJSON)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_POST_move_file_replace(self):
|
||||||
|
d = self.POST(self.public_url + "/foo", t="move",
|
||||||
|
from_name="bar.txt", to_name="baz.txt", to_dir="sub")
|
||||||
|
d.addCallback(lambda res:
|
||||||
|
self.failIfNodeHasChild(self._foo_node, u"bar.txt"))
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/baz.txt"))
|
||||||
|
d.addCallback(self.failUnlessIsBarDotTxt)
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/baz.txt?t=json"))
|
||||||
|
d.addCallback(self.failUnlessIsBarJSON)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_POST_move_file_no_replace(self):
|
||||||
|
d = self.POST(self.public_url + "/foo", t="move", replace="false",
|
||||||
|
from_name="bar.txt", to_name="baz.txt", to_dir="sub")
|
||||||
|
d.addBoth(self.shouldFail, error.Error,
|
||||||
|
"POST_move_file_no_replace",
|
||||||
|
"409 Conflict",
|
||||||
|
"There was already a child by that name, and you asked me "
|
||||||
|
"to not replace it")
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt"))
|
||||||
|
d.addCallback(self.failUnlessIsBarDotTxt)
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt?t=json"))
|
||||||
|
d.addCallback(self.failUnlessIsBarJSON)
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/baz.txt"))
|
||||||
|
d.addCallback(self.failUnlessIsSubBazDotTxt)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_POST_move_file_slash_fail(self):
|
||||||
|
d = self.POST(self.public_url + "/foo", t="move",
|
||||||
|
from_name="bar.txt", to_name="slash/fail.txt", to_dir="sub")
|
||||||
|
d.addBoth(self.shouldFail, error.Error,
|
||||||
|
"test_POST_rename_file_slash_fail",
|
||||||
|
"400 Bad Request",
|
||||||
|
"to_name= may not contain a slash",
|
||||||
|
)
|
||||||
|
d.addCallback(lambda res:
|
||||||
|
self.failUnlessNodeHasChild(self._foo_node, u"bar.txt"))
|
||||||
|
d.addCallback(lambda res:
|
||||||
|
self.failIfNodeHasChild(self._sub_node, u"slash/fail.txt"))
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_POST_move_file_no_target(self):
|
||||||
|
d = self.POST(self.public_url + "/foo", t="move",
|
||||||
|
from_name="bar.txt", to_name="baz.txt")
|
||||||
|
d.addBoth(self.shouldFail, error.Error,
|
||||||
|
"POST_move_file_no_target",
|
||||||
|
"400 Bad Request",
|
||||||
|
"move requires from_name and to_dir")
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt"))
|
||||||
|
d.addCallback(self.failUnlessIsBarDotTxt)
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt?t=json"))
|
||||||
|
d.addCallback(self.failUnlessIsBarJSON)
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/baz.txt"))
|
||||||
|
d.addCallback(self.failUnlessIsBazDotTxt)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_POST_move_file_multi_level(self):
|
||||||
|
d = self.POST(self.public_url + "/foo/sub/level2?t=mkdir", "")
|
||||||
|
d.addCallback(lambda res: self.POST(self.public_url + "/foo", t="move",
|
||||||
|
from_name="bar.txt", to_dir="sub/level2"))
|
||||||
|
d.addCallback(lambda res: self.failIfNodeHasChild(self._foo_node, u"bar.txt"))
|
||||||
|
d.addCallback(lambda res: self.failIfNodeHasChild(self._sub_node, u"bar.txt"))
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/level2/bar.txt"))
|
||||||
|
d.addCallback(self.failUnlessIsBarDotTxt)
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/level2/bar.txt?t=json"))
|
||||||
|
d.addCallback(self.failUnlessIsBarJSON)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_POST_move_file_to_uri(self):
|
||||||
|
d = self.POST(self.public_url + "/foo", t="move",
|
||||||
|
from_name="bar.txt", to_dir=self._sub_uri)
|
||||||
|
d.addCallback(lambda res:
|
||||||
|
self.failIfNodeHasChild(self._foo_node, u"bar.txt"))
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/bar.txt"))
|
||||||
|
d.addCallback(self.failUnlessIsBarDotTxt)
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/bar.txt?t=json"))
|
||||||
|
d.addCallback(self.failUnlessIsBarJSON)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_POST_move_file_to_nonexist_dir(self):
|
||||||
|
d = self.POST(self.public_url + "/foo", t="move",
|
||||||
|
from_name="bar.txt", to_dir="notchucktesta")
|
||||||
|
d.addBoth(self.shouldFail, error.Error,
|
||||||
|
"POST_move_file_to_nonexist_dir",
|
||||||
|
"404 Not Found",
|
||||||
|
"No such child: notchucktesta")
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_POST_move_file_into_file(self):
|
||||||
|
d = self.POST(self.public_url + "/foo", t="move",
|
||||||
|
from_name="bar.txt", to_dir="baz.txt")
|
||||||
|
d.addBoth(self.shouldFail, error.Error,
|
||||||
|
"POST_move_file_into_file",
|
||||||
|
"410 Gone",
|
||||||
|
"to_dir is not a usable directory")
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/baz.txt"))
|
||||||
|
d.addCallback(self.failUnlessIsBazDotTxt)
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt"))
|
||||||
|
d.addCallback(self.failUnlessIsBarDotTxt)
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt?t=json"))
|
||||||
|
d.addCallback(self.failUnlessIsBarJSON)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_POST_move_file_to_bad_uri(self):
|
||||||
|
d = self.POST(self.public_url + "/foo", t="move", from_name="bar.txt",
|
||||||
|
to_dir="URI:DIR2:mn5jlyjnrjeuydyswlzyui72i:rmneifcj6k6sycjljjhj3f6majsq2zqffydnnul5hfa4j577arma")
|
||||||
|
d.addBoth(self.shouldFail, error.Error,
|
||||||
|
"POST_move_file_to_bad_uri",
|
||||||
|
"410 Gone",
|
||||||
|
"to_dir is not a usable directory")
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt"))
|
||||||
|
d.addCallback(self.failUnlessIsBarDotTxt)
|
||||||
|
d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt?t=json"))
|
||||||
|
d.addCallback(self.failUnlessIsBarJSON)
|
||||||
|
return d
|
||||||
|
|
||||||
def shouldRedirect(self, res, target=None, statuscode=None, which=""):
|
def shouldRedirect(self, res, target=None, statuscode=None, which=""):
|
||||||
""" If target is not None then the redirection has to go to target. If
|
""" If target is not None then the redirection has to go to target. If
|
||||||
statuscode is not None then the redirection has to be accomplished with
|
statuscode is not None then the redirection has to be accomplished with
|
||||||
|
@ -3299,6 +3448,15 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
|
||||||
d.addCallback(_check)
|
d.addCallback(_check)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def test_GET_move_form(self):
|
||||||
|
d = self.GET(self.public_url + "/foo?t=move-form&name=bar.txt",
|
||||||
|
followRedirect=True)
|
||||||
|
def _check(res):
|
||||||
|
self.failUnless('name="when_done" value="."' in res, res)
|
||||||
|
self.failUnless(re.search(r'name="from_name" value="bar\.txt"', res))
|
||||||
|
d.addCallback(_check)
|
||||||
|
return d
|
||||||
|
|
||||||
def log(self, res, msg):
|
def log(self, res, msg):
|
||||||
#print "MSG: %s RES: %s" % (msg, res)
|
#print "MSG: %s RES: %s" % (msg, res)
|
||||||
log.msg(msg)
|
log.msg(msg)
|
||||||
|
|
|
@ -934,6 +934,13 @@ def is_literal_file_uri(s):
|
||||||
s.startswith(ALLEGED_READONLY_PREFIX + 'URI:LIT:') or
|
s.startswith(ALLEGED_READONLY_PREFIX + 'URI:LIT:') or
|
||||||
s.startswith(ALLEGED_IMMUTABLE_PREFIX + 'URI:LIT:'))
|
s.startswith(ALLEGED_IMMUTABLE_PREFIX + 'URI:LIT:'))
|
||||||
|
|
||||||
|
def is_writeable_directory_uri(s):
|
||||||
|
if not isinstance(s, str):
|
||||||
|
return False
|
||||||
|
return (s.startswith('URI:DIR2:') or
|
||||||
|
s.startswith(ALLEGED_READONLY_PREFIX + 'URI:DIR2:') or
|
||||||
|
s.startswith(ALLEGED_IMMUTABLE_PREFIX + 'URI:DIR2:'))
|
||||||
|
|
||||||
def has_uri_prefix(s):
|
def has_uri_prefix(s):
|
||||||
if not isinstance(s, str):
|
if not isinstance(s, str):
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -13,7 +13,7 @@ from nevow.inevow import IRequest
|
||||||
from foolscap.api import fireEventually
|
from foolscap.api import fireEventually
|
||||||
|
|
||||||
from allmydata.util import base32, time_format
|
from allmydata.util import base32, time_format
|
||||||
from allmydata.uri import from_string_dirnode
|
from allmydata.uri import from_string_dirnode, is_writeable_directory_uri
|
||||||
from allmydata.interfaces import IDirectoryNode, IFileNode, IFilesystemNode, \
|
from allmydata.interfaces import IDirectoryNode, IFileNode, IFilesystemNode, \
|
||||||
IImmutableFileNode, IMutableFileNode, ExistingChildError, \
|
IImmutableFileNode, IMutableFileNode, ExistingChildError, \
|
||||||
NoSuchChildError, EmptyPathnameComponentError, SDMF_VERSION, MDMF_VERSION
|
NoSuchChildError, EmptyPathnameComponentError, SDMF_VERSION, MDMF_VERSION
|
||||||
|
@ -169,6 +169,8 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
|
||||||
return DirectoryReadonlyURI(ctx, self.node)
|
return DirectoryReadonlyURI(ctx, self.node)
|
||||||
if t == 'rename-form':
|
if t == 'rename-form':
|
||||||
return RenameForm(self.node)
|
return RenameForm(self.node)
|
||||||
|
if t == 'move-form':
|
||||||
|
return MoveForm(self.node)
|
||||||
|
|
||||||
raise WebError("GET directory: bad t=%s" % t)
|
raise WebError("GET directory: bad t=%s" % t)
|
||||||
|
|
||||||
|
@ -213,6 +215,8 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
|
||||||
d = self._POST_unlink(req)
|
d = self._POST_unlink(req)
|
||||||
elif t == "rename":
|
elif t == "rename":
|
||||||
d = self._POST_rename(req)
|
d = self._POST_rename(req)
|
||||||
|
elif t == "move":
|
||||||
|
d = self._POST_move(req)
|
||||||
elif t == "check":
|
elif t == "check":
|
||||||
d = self._POST_check(req)
|
d = self._POST_check(req)
|
||||||
elif t == "start-deep-check":
|
elif t == "start-deep-check":
|
||||||
|
@ -418,6 +422,52 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
|
||||||
d.addCallback(lambda res: "thing renamed")
|
d.addCallback(lambda res: "thing renamed")
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def _POST_move(self, req):
|
||||||
|
charset = get_arg(req, "_charset", "utf-8")
|
||||||
|
from_name = get_arg(req, "from_name")
|
||||||
|
if from_name is not None:
|
||||||
|
from_name = from_name.strip()
|
||||||
|
from_name = from_name.decode(charset)
|
||||||
|
assert isinstance(from_name, unicode)
|
||||||
|
to_name = get_arg(req, "to_name")
|
||||||
|
if to_name is not None:
|
||||||
|
to_name = to_name.strip()
|
||||||
|
to_name = to_name.decode(charset)
|
||||||
|
assert isinstance(to_name, unicode)
|
||||||
|
if not to_name:
|
||||||
|
to_name = from_name
|
||||||
|
to_dir = get_arg(req, "to_dir")
|
||||||
|
if to_dir is not None:
|
||||||
|
to_dir = to_dir.strip()
|
||||||
|
to_dir = to_dir.decode(charset)
|
||||||
|
assert isinstance(to_dir, unicode)
|
||||||
|
if not from_name or not to_dir:
|
||||||
|
raise WebError("move requires from_name and to_dir")
|
||||||
|
replace = boolean_of_arg(get_arg(req, "replace", "true"))
|
||||||
|
|
||||||
|
# allow from_name to contain slashes, so they can fix names that
|
||||||
|
# were accidentally created with them. But disallow them in to_name
|
||||||
|
# (if it's specified), to discourage the practice.
|
||||||
|
if to_name and "/" in to_name:
|
||||||
|
raise WebError("to_name= may not contain a slash", http.BAD_REQUEST)
|
||||||
|
|
||||||
|
d = self.node.has_child(to_dir.split('/')[0])
|
||||||
|
def get_target_node(isname):
|
||||||
|
if isname or not is_writeable_directory_uri(str(to_dir)):
|
||||||
|
return self.node.get_child_at_path(to_dir)
|
||||||
|
else:
|
||||||
|
return self.client.create_node_from_uri(str(to_dir))
|
||||||
|
d.addCallback(get_target_node)
|
||||||
|
def is_target_node_usable(target_node):
|
||||||
|
if not IDirectoryNode.providedBy(target_node):
|
||||||
|
raise WebError("to_dir is not a usable directory", http.GONE)
|
||||||
|
return target_node
|
||||||
|
d.addCallback(is_target_node_usable)
|
||||||
|
d.addCallback(lambda new_parent: self.node.move_child_to(
|
||||||
|
from_name, new_parent, to_name, replace))
|
||||||
|
d.addCallback(lambda res: "thing moved")
|
||||||
|
return d
|
||||||
|
|
||||||
def _maybe_literal(self, res, Results_Class):
|
def _maybe_literal(self, res, Results_Class):
|
||||||
if res:
|
if res:
|
||||||
return Results_Class(self.client, res)
|
return Results_Class(self.client, res)
|
||||||
|
@ -662,6 +712,7 @@ class DirectoryAsHTML(rend.Page):
|
||||||
if self.node.is_unknown() or self.node.is_readonly():
|
if self.node.is_unknown() or self.node.is_readonly():
|
||||||
unlink = "-"
|
unlink = "-"
|
||||||
rename = "-"
|
rename = "-"
|
||||||
|
move = "-"
|
||||||
else:
|
else:
|
||||||
# this creates a button which will cause our _POST_unlink method
|
# this creates a button which will cause our _POST_unlink method
|
||||||
# to be invoked, which unlinks the file and then redirects the
|
# to be invoked, which unlinks the file and then redirects the
|
||||||
|
@ -680,8 +731,16 @@ class DirectoryAsHTML(rend.Page):
|
||||||
T.input(type='submit', value='rename', name="rename"),
|
T.input(type='submit', value='rename', name="rename"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
move = T.form(action=here, method="get")[
|
||||||
|
T.input(type='hidden', name='t', value='move-form'),
|
||||||
|
T.input(type='hidden', name='name', value=name),
|
||||||
|
T.input(type='hidden', name='when_done', value="."),
|
||||||
|
T.input(type='submit', value='move', name="move"),
|
||||||
|
]
|
||||||
|
|
||||||
ctx.fillSlots("unlink", unlink)
|
ctx.fillSlots("unlink", unlink)
|
||||||
ctx.fillSlots("rename", rename)
|
ctx.fillSlots("rename", rename)
|
||||||
|
ctx.fillSlots("move", move)
|
||||||
|
|
||||||
times = []
|
times = []
|
||||||
linkcrtime = metadata.get('tahoe', {}).get("linkcrtime")
|
linkcrtime = metadata.get('tahoe', {}).get("linkcrtime")
|
||||||
|
@ -943,6 +1002,32 @@ class RenameForm(rend.Page):
|
||||||
ctx.tag.attributes['value'] = name
|
ctx.tag.attributes['value'] = name
|
||||||
return ctx.tag
|
return ctx.tag
|
||||||
|
|
||||||
|
class MoveForm(rend.Page):
|
||||||
|
addSlash = True
|
||||||
|
docFactory = getxmlfile("move-form.xhtml")
|
||||||
|
|
||||||
|
def render_title(self, ctx, data):
|
||||||
|
return ctx.tag["Directory SI=%s" % abbreviated_dirnode(self.original)]
|
||||||
|
|
||||||
|
def render_header(self, ctx, data):
|
||||||
|
header = ["Move "
|
||||||
|
"from directory SI=%s" % abbreviated_dirnode(self.original),
|
||||||
|
]
|
||||||
|
|
||||||
|
if self.original.is_readonly():
|
||||||
|
header.append(" (readonly!)")
|
||||||
|
header.append(":")
|
||||||
|
return ctx.tag[header]
|
||||||
|
|
||||||
|
def render_when_done(self, ctx, data):
|
||||||
|
return T.input(type="hidden", name="when_done", value=".")
|
||||||
|
|
||||||
|
def render_get_name(self, ctx, data):
|
||||||
|
req = IRequest(ctx)
|
||||||
|
name = get_arg(req, "name", "")
|
||||||
|
ctx.tag.attributes['value'] = name
|
||||||
|
return ctx.tag
|
||||||
|
|
||||||
|
|
||||||
class ManifestResults(rend.Page, ReloadMixin):
|
class ManifestResults(rend.Page, ReloadMixin):
|
||||||
docFactory = getxmlfile("manifest.xhtml")
|
docFactory = getxmlfile("manifest.xhtml")
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
<td><n:slot name="times"/></td>
|
<td><n:slot name="times"/></td>
|
||||||
<td><n:slot name="unlink"/></td>
|
<td><n:slot name="unlink"/></td>
|
||||||
<td><n:slot name="rename"/></td>
|
<td><n:slot name="rename"/></td>
|
||||||
|
<td><n:slot name="move"/></td>
|
||||||
<td><n:slot name="info"/></td>
|
<td><n:slot name="info"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
<html xmlns:n="http://nevow.com/ns/nevow/0.1">
|
||||||
|
<head>
|
||||||
|
<title n:render="title"></title>
|
||||||
|
<link href="/tahoe.css" rel="stylesheet" type="text/css"/>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h2 n:render="header" />
|
||||||
|
|
||||||
|
<div class="freeform-form">
|
||||||
|
<form action="." method="post" enctype="multipart/form-data">
|
||||||
|
<fieldset>
|
||||||
|
<legend class="freeform-form-label">Rename child</legend>
|
||||||
|
<input type="hidden" name="t" value="move" />
|
||||||
|
<input n:render="when_done" />
|
||||||
|
|
||||||
|
Move child:
|
||||||
|
<input type="text" name="from_name" readonly="true" n:render="get_name" />
|
||||||
|
to
|
||||||
|
<input type="text" name="to_dir" /><br />
|
||||||
|
New name?
|
||||||
|
<input type="text" name="to_name" />
|
||||||
|
<input type="submit" value="move" />
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body></html>
|
Loading…
Reference in New Issue