Ticket #1579: movebutton.diff
| File movebutton.diff, 20.1 KB (added by marcusw, at 2011-11-17T02:06:55Z) |
|---|
-
docs/frontends/webapi.rst
diff --git a/docs/frontends/webapi.rst b/docs/frontends/webapi.rst index 47ab754..a715a50 100644
a b The Tahoe REST-ful Web API 29 29 6. `Attaching An Existing File Or Directory (by URI)`_ 30 30 7. `Unlinking A Child`_ 31 31 8. `Renaming A Child`_ 32 9. `Other Utilities`_ 33 10. `Debugging and Testing Features`_ 32 9. `Moving A Child`_ 33 10. `Other Utilities`_ 34 11. `Debugging and Testing Features`_ 34 35 35 36 7. `Other Useful Pages`_ 36 37 8. `Static Files in /public_html`_ … … Renaming A Child 1273 1274 This operation will replace any existing child of the new name, making it 1274 1275 behave like the UNIX "``mv -f``" command. 1275 1276 1277 Moving A Child 1278 ---------------- 1279 1280 ``POST /uri/$DIRCAP/[SUBDIRS../]?t=rename&from_name=OLD&to_dir=TARGET[&to_name=NEW]`` 1281 1282 This instructs the node to move a child of the given directory to a 1283 different directory, both of which must be mutable. The child can also be 1284 renamed in the process. The to_dir parameter can be either the name of a 1285 subdirectory of the dircap from which the child is being moved (multiple 1286 levels of descent are supported) or the writecap of an unrelated directory. 1287 1288 This operation will replace any existing child of the new name, making it 1289 behave like the UNIX "``mv -f``" command. The original child is not 1290 unlinked until it is linked into the target directory. 1291 1276 1292 Other Utilities 1277 1293 --------------- 1278 1294 … … Other Utilities 1294 1310 functionality described above, with the provided $CHILDNAME present in the 1295 1311 'from_name' field of that form. I.e. this presents a form offering to 1296 1312 rename $CHILDNAME, requesting the new name, and submitting POST rename. 1313 This same URL format can also be used with "move-form" with the expected 1314 results. 1297 1315 1298 1316 ``GET /uri/$DIRCAP/[SUBDIRS../]CHILDNAME?t=uri`` 1299 1317 -
src/allmydata/test/test_web.py
diff --git a/src/allmydata/test/test_web.py b/src/allmydata/test/test_web.py index dc7ab9e..0913f0d 100644
a b class WebMixin(object): 236 236 self._sub_uri = sub_uri 237 237 foo.set_uri(u"sub", sub_uri, sub_uri) 238 238 sub = self.s.create_node_from_uri(sub_uri) 239 self._sub_node = sub 239 240 240 241 _ign, n, blocking_uri = self.makefile(1) 241 242 foo.set_uri(u"blockingfile", blocking_uri, blocking_uri) … … class WebMixin(object): 245 246 # still think of it as an umlaut 246 247 foo.set_uri(unicode_filename, self._bar_txt_uri, self._bar_txt_uri) 247 248 248 _ign, n, baz_file = self.makefile(2)249 self.SUBBAZ_CONTENTS, n, baz_file = self.makefile(2) 249 250 self._baz_file_uri = baz_file 250 251 sub.set_uri(u"baz.txt", baz_file, baz_file) 251 252 … … class WebMixin(object): 300 301 def failUnlessIsBazDotTxt(self, res): 301 302 self.failUnlessReallyEqual(res, self.BAZ_CONTENTS, res) 302 303 304 def failUnlessIsSubBazDotTxt(self, res): 305 self.failUnlessReallyEqual(res, self.SUBBAZ_CONTENTS, res) 306 303 307 def failUnlessIsBarJSON(self, res): 304 308 data = simplejson.loads(res) 305 309 self.failUnless(isinstance(data, list)) … … class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi 1335 1339 r'\s+<td align="right">%d</td>' % len(self.BAR_CONTENTS), 1336 1340 ]) 1337 1341 self.failUnless(re.search(get_bar, res), res) 1338 for label in ['unlink', 'rename' ]:1342 for label in ['unlink', 'rename', 'move']: 1339 1343 for line in res.split("\n"): 1340 1344 # find the line that contains the relevant button for bar.txt 1341 1345 if ("form action" in line and … … class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi 3316 3320 d.addCallback(self.failUnlessIsFooJSON) 3317 3321 return d 3318 3322 3323 def test_POST_move_file(self): 3324 """""" 3325 d = self.POST(self.public_url + "/foo", t="move", 3326 from_name="bar.txt", to_dir="sub") 3327 d.addCallback(lambda res: 3328 self.failIfNodeHasChild(self._foo_node, u"bar.txt")) 3329 d.addCallback(lambda res: 3330 self.failUnlessNodeHasChild(self._sub_node, u"bar.txt")) 3331 d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/bar.txt")) 3332 d.addCallback(self.failUnlessIsBarDotTxt) 3333 d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/bar.txt?t=json")) 3334 d.addCallback(self.failUnlessIsBarJSON) 3335 return d 3336 3337 def test_POST_move_file_new_name(self): 3338 d = self.POST(self.public_url + "/foo", t="move", 3339 from_name="bar.txt", to_name="wibble.txt", to_dir="sub") 3340 d.addCallback(lambda res: 3341 self.failIfNodeHasChild(self._foo_node, u"bar.txt")) 3342 d.addCallback(lambda res: 3343 self.failIfNodeHasChild(self._sub_node, u"bar.txt")) 3344 d.addCallback(lambda res: 3345 self.failUnlessNodeHasChild(self._sub_node, u"wibble.txt")) 3346 d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/wibble.txt")) 3347 d.addCallback(self.failUnlessIsBarDotTxt) 3348 d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/wibble.txt?t=json")) 3349 d.addCallback(self.failUnlessIsBarJSON) 3350 return d 3351 3352 def test_POST_move_file_replace(self): 3353 d = self.POST(self.public_url + "/foo", t="move", 3354 from_name="bar.txt", to_name="baz.txt", to_dir="sub") 3355 d.addCallback(lambda res: 3356 self.failIfNodeHasChild(self._foo_node, u"bar.txt")) 3357 d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/baz.txt")) 3358 d.addCallback(self.failUnlessIsBarDotTxt) 3359 d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/baz.txt?t=json")) 3360 d.addCallback(self.failUnlessIsBarJSON) 3361 return d 3362 3363 def test_POST_move_file_no_replace(self): 3364 d = self.POST(self.public_url + "/foo", t="move", replace="false", 3365 from_name="bar.txt", to_name="baz.txt", to_dir="sub") 3366 d.addBoth(self.shouldFail, error.Error, 3367 "POST_move_file_no_replace", 3368 "409 Conflict", 3369 "There was already a child by that name, and you asked me " 3370 "to not replace it") 3371 d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt")) 3372 d.addCallback(self.failUnlessIsBarDotTxt) 3373 d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt?t=json")) 3374 d.addCallback(self.failUnlessIsBarJSON) 3375 d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/baz.txt")) 3376 d.addCallback(self.failUnlessIsSubBazDotTxt) 3377 return d 3378 3379 def test_POST_move_file_slash_fail(self): 3380 d = self.POST(self.public_url + "/foo", t="move", 3381 from_name="bar.txt", to_name="slash/fail.txt", to_dir="sub") 3382 d.addBoth(self.shouldFail, error.Error, 3383 "test_POST_rename_file_slash_fail", 3384 "400 Bad Request", 3385 "to_name= may not contain a slash", 3386 ) 3387 d.addCallback(lambda res: 3388 self.failUnlessNodeHasChild(self._foo_node, u"bar.txt")) 3389 d.addCallback(lambda res: 3390 self.failIfNodeHasChild(self._sub_node, u"slash/fail.txt")) 3391 return d 3392 3393 def test_POST_move_file_no_target(self): 3394 d = self.POST(self.public_url + "/foo", t="move", 3395 from_name="bar.txt", to_name="baz.txt") 3396 d.addBoth(self.shouldFail, error.Error, 3397 "POST_move_file_no_target", 3398 "400 Bad Request", 3399 "move requires from_name and to_dir") 3400 d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt")) 3401 d.addCallback(self.failUnlessIsBarDotTxt) 3402 d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt?t=json")) 3403 d.addCallback(self.failUnlessIsBarJSON) 3404 d.addCallback(lambda res: self.GET(self.public_url + "/foo/baz.txt")) 3405 d.addCallback(self.failUnlessIsBazDotTxt) 3406 return d 3407 3408 def test_POST_move_file_multi_level(self): 3409 d = self.POST(self.public_url + "/foo/sub/level2?t=mkdir", "") 3410 d.addCallback(lambda res: self.POST(self.public_url + "/foo", t="move", 3411 from_name="bar.txt", to_dir="sub/level2")) 3412 d.addCallback(lambda res: self.failIfNodeHasChild(self._foo_node, u"bar.txt")) 3413 d.addCallback(lambda res: self.failIfNodeHasChild(self._sub_node, u"bar.txt")) 3414 d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/level2/bar.txt")) 3415 d.addCallback(self.failUnlessIsBarDotTxt) 3416 d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/level2/bar.txt?t=json")) 3417 d.addCallback(self.failUnlessIsBarJSON) 3418 return d 3419 3420 def test_POST_move_file_to_uri(self): 3421 d = self.POST(self.public_url + "/foo", t="move", 3422 from_name="bar.txt", to_dir=self._sub_uri) 3423 d.addCallback(lambda res: 3424 self.failIfNodeHasChild(self._foo_node, u"bar.txt")) 3425 d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/bar.txt")) 3426 d.addCallback(self.failUnlessIsBarDotTxt) 3427 d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/bar.txt?t=json")) 3428 d.addCallback(self.failUnlessIsBarJSON) 3429 return d 3430 3431 def test_POST_move_file_to_nonexist_dir(self): 3432 d = self.POST(self.public_url + "/foo", t="move", 3433 from_name="bar.txt", to_dir="notchucktesta") 3434 d.addBoth(self.shouldFail, error.Error, 3435 "POST_move_file_to_nonexist_dir", 3436 "404 Not Found", 3437 "No such child: notchucktesta") 3438 return d 3439 3440 def test_POST_move_file_into_file(self): 3441 d = self.POST(self.public_url + "/foo", t="move", 3442 from_name="bar.txt", to_dir="baz.txt") 3443 d.addBoth(self.shouldFail, error.Error, 3444 "POST_move_file_into_file", 3445 "410 Gone", 3446 "to_dir is not a usable directory") 3447 d.addCallback(lambda res: self.GET(self.public_url + "/foo/baz.txt")) 3448 d.addCallback(self.failUnlessIsBazDotTxt) 3449 d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt")) 3450 d.addCallback(self.failUnlessIsBarDotTxt) 3451 d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt?t=json")) 3452 d.addCallback(self.failUnlessIsBarJSON) 3453 return d 3454 3455 def test_POST_move_file_to_bad_uri(self): 3456 d = self.POST(self.public_url + "/foo", t="move", from_name="bar.txt", 3457 to_dir="URI:DIR2:mn5jlyjnrjeuydyswlzyui72i:rmneifcj6k6sycjljjhj3f6majsq2zqffydnnul5hfa4j577arma") 3458 d.addBoth(self.shouldFail, error.Error, 3459 "POST_move_file_to_bad_uri", 3460 "410 Gone", 3461 "to_dir is not a usable directory") 3462 d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt")) 3463 d.addCallback(self.failUnlessIsBarDotTxt) 3464 d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt?t=json")) 3465 d.addCallback(self.failUnlessIsBarJSON) 3466 return d 3467 3319 3468 def shouldRedirect(self, res, target=None, statuscode=None, which=""): 3320 3469 """ If target is not None then the redirection has to go to target. If 3321 3470 statuscode is not None then the redirection has to be accomplished with … … class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi 3372 3521 d.addCallback(_check) 3373 3522 return d 3374 3523 3524 def test_GET_move_form(self): 3525 d = self.GET(self.public_url + "/foo?t=move-form&name=bar.txt", 3526 followRedirect=True) 3527 def _check(res): 3528 self.failUnless('name="when_done" value="."' in res, res) 3529 self.failUnless(re.search(r'name="from_name" value="bar\.txt"', res)) 3530 d.addCallback(_check) 3531 return d 3532 3375 3533 def log(self, res, msg): 3376 3534 #print "MSG: %s RES: %s" % (msg, res) 3377 3535 log.msg(msg) -
src/allmydata/uri.py
diff --git a/src/allmydata/uri.py b/src/allmydata/uri.py index 8cb9d10..e2b78a2 100644
a b def is_literal_file_uri(s): 934 934 s.startswith(ALLEGED_READONLY_PREFIX + 'URI:LIT:') or 935 935 s.startswith(ALLEGED_IMMUTABLE_PREFIX + 'URI:LIT:')) 936 936 937 def is_writeable_directory_uri(s): 938 if not isinstance(s, str): 939 return False 940 return (s.startswith('URI:DIR2:') or 941 s.startswith(ALLEGED_READONLY_PREFIX + 'URI:DIR2:') or 942 s.startswith(ALLEGED_IMMUTABLE_PREFIX + 'URI:DIR2:')) 943 937 944 def has_uri_prefix(s): 938 945 if not isinstance(s, str): 939 946 return False -
src/allmydata/web/directory.py
diff --git a/src/allmydata/web/directory.py b/src/allmydata/web/directory.py index 58fe781..90ef36a 100644
a b from nevow.inevow import IRequest 13 13 from foolscap.api import fireEventually 14 14 15 15 from allmydata.util import base32, time_format 16 from allmydata.uri import from_string_dirnode 16 from allmydata.uri import from_string_dirnode, is_writeable_directory_uri 17 17 from allmydata.interfaces import IDirectoryNode, IFileNode, IFilesystemNode, \ 18 18 IImmutableFileNode, IMutableFileNode, ExistingChildError, \ 19 19 NoSuchChildError, EmptyPathnameComponentError, SDMF_VERSION, MDMF_VERSION … … class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin): 169 169 return DirectoryReadonlyURI(ctx, self.node) 170 170 if t == 'rename-form': 171 171 return RenameForm(self.node) 172 if t == 'move-form': 173 return MoveForm(self.node) 172 174 173 175 raise WebError("GET directory: bad t=%s" % t) 174 176 … … class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin): 213 215 d = self._POST_unlink(req) 214 216 elif t == "rename": 215 217 d = self._POST_rename(req) 218 elif t == "move": 219 d = self._POST_move(req) 216 220 elif t == "check": 217 221 d = self._POST_check(req) 218 222 elif t == "start-deep-check": … … class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin): 418 422 d.addCallback(lambda res: "thing renamed") 419 423 return d 420 424 425 def _POST_move(self, req): 426 charset = get_arg(req, "_charset", "utf-8") 427 from_name = get_arg(req, "from_name") 428 if from_name is not None: 429 from_name = from_name.strip() 430 from_name = from_name.decode(charset) 431 assert isinstance(from_name, unicode) 432 to_name = get_arg(req, "to_name") 433 if to_name is not None: 434 to_name = to_name.strip() 435 to_name = to_name.decode(charset) 436 assert isinstance(to_name, unicode) 437 if not to_name: 438 to_name = from_name 439 to_dir = get_arg(req, "to_dir") 440 if to_dir is not None: 441 to_dir = to_dir.strip() 442 to_dir = to_dir.decode(charset) 443 assert isinstance(to_dir, unicode) 444 if not from_name or not to_dir: 445 raise WebError("move requires from_name and to_dir") 446 replace = boolean_of_arg(get_arg(req, "replace", "true")) 447 448 # allow from_name to contain slashes, so they can fix names that 449 # were accidentally created with them. But disallow them in to_name 450 # (if it's specified), to discourage the practice. 451 if to_name and "/" in to_name: 452 raise WebError("to_name= may not contain a slash", http.BAD_REQUEST) 453 454 d = self.node.has_child(to_dir.split('/')[0]) 455 def get_target_node(isname): 456 if isname or not is_writeable_directory_uri(str(to_dir)): 457 return self.node.get_child_at_path(to_dir) 458 else: 459 return self.client.create_node_from_uri(str(to_dir)) 460 d.addCallback(get_target_node) 461 def is_target_node_usable(target_node): 462 if not IDirectoryNode.providedBy(target_node): 463 raise WebError("to_dir is not a usable directory", http.GONE) 464 return target_node 465 d.addCallback(is_target_node_usable) 466 d.addCallback(lambda new_parent: self.node.move_child_to( 467 from_name, new_parent, to_name, replace)) 468 d.addCallback(lambda res: "thing moved") 469 return d 470 421 471 def _maybe_literal(self, res, Results_Class): 422 472 if res: 423 473 return Results_Class(self.client, res) … … class DirectoryAsHTML(rend.Page): 662 712 if self.node.is_unknown() or self.node.is_readonly(): 663 713 unlink = "-" 664 714 rename = "-" 715 move = "-" 665 716 else: 666 717 # this creates a button which will cause our _POST_unlink method 667 718 # to be invoked, which unlinks the file and then redirects the … … class DirectoryAsHTML(rend.Page): 680 731 T.input(type='submit', value='rename', name="rename"), 681 732 ] 682 733 734 move = T.form(action=here, method="get")[ 735 T.input(type='hidden', name='t', value='move-form'), 736 T.input(type='hidden', name='name', value=name), 737 T.input(type='hidden', name='when_done', value="."), 738 T.input(type='submit', value='move', name="move"), 739 ] 740 683 741 ctx.fillSlots("unlink", unlink) 684 742 ctx.fillSlots("rename", rename) 743 ctx.fillSlots("move", move) 685 744 686 745 times = [] 687 746 linkcrtime = metadata.get('tahoe', {}).get("linkcrtime") … … class RenameForm(rend.Page): 943 1002 ctx.tag.attributes['value'] = name 944 1003 return ctx.tag 945 1004 1005 class MoveForm(rend.Page): 1006 addSlash = True 1007 docFactory = getxmlfile("move-form.xhtml") 1008 1009 def render_title(self, ctx, data): 1010 return ctx.tag["Directory SI=%s" % abbreviated_dirnode(self.original)] 1011 1012 def render_header(self, ctx, data): 1013 header = ["Move " 1014 "from directory SI=%s" % abbreviated_dirnode(self.original), 1015 ] 1016 1017 if self.original.is_readonly(): 1018 header.append(" (readonly!)") 1019 header.append(":") 1020 return ctx.tag[header] 1021 1022 def render_when_done(self, ctx, data): 1023 return T.input(type="hidden", name="when_done", value=".") 1024 1025 def render_get_name(self, ctx, data): 1026 req = IRequest(ctx) 1027 name = get_arg(req, "name", "") 1028 ctx.tag.attributes['value'] = name 1029 return ctx.tag 1030 946 1031 947 1032 class ManifestResults(rend.Page, ReloadMixin): 948 1033 docFactory = getxmlfile("manifest.xhtml") -
src/allmydata/web/directory.xhtml
diff --git a/src/allmydata/web/directory.xhtml b/src/allmydata/web/directory.xhtml index 4875738..1df38fe 100644
a b 33 33 <td><n:slot name="times"/></td> 34 34 <td><n:slot name="unlink"/></td> 35 35 <td><n:slot name="rename"/></td> 36 <td><n:slot name="move"/></td> 36 37 <td><n:slot name="info"/></td> 37 38 </tr> 38 39 -
new file src/allmydata/web/move-form.xhtml
diff --git a/src/allmydata/web/move-form.xhtml b/src/allmydata/web/move-form.xhtml new file mode 100644 index 0000000..0460add
- + 1 <html xmlns:n="http://nevow.com/ns/nevow/0.1"> 2 <head> 3 <title n:render="title"></title> 4 <link href="/tahoe_css" rel="stylesheet" type="text/css"/> 5 <link href="/webform_css" rel="stylesheet" type="text/css"/> 6 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 7 </head> 8 9 <body> 10 11 <h2 n:render="header" /> 12 13 <div class="freeform-form"> 14 <form action="." method="post" enctype="multipart/form-data"> 15 <fieldset> 16 <legend class="freeform-form-label">Rename child</legend> 17 <input type="hidden" name="t" value="move" /> 18 <input n:render="when_done" /> 19 20 Move child: 21 <input type="text" name="from_name" readonly="true" n:render="get_name" /> 22 to 23 <input type="text" name="to_dir" /><br /> 24 New name? 25 <input type="text" name="to_name" /> 26 <input type="submit" value="move" /> 27 </fieldset> 28 </form> 29 </div> 30 31 </body></html>
