Changeset b29d092 in trunk


Ignore:
Timestamp:
2012-05-09T20:07:13Z (13 years ago)
Author:
Brian Warner <warner@…>
Branches:
master
Children:
e58a012
Parents:
8aa690b
git-author:
Marcus Wanner <marcus@…> (2011-11-10 08:00:11)
git-committer:
Brian Warner <warner@…> (2012-05-09 20:07:13)
Message:

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.

Files:
1 added
5 edited

Legend:

Unmodified
Added
Removed
  • TabularUnified docs/frontends/webapi.rst

    r8aa690b rb29d092  
    3030    7.  `Unlinking A Child`_
    3131    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`_
    3435
    35367.  `Other Useful Pages`_
     
    12781279 behave like the UNIX "``mv -f``" command.
    12791280
     1281Moving A Child
     1282----------------
     1283
     1284``POST /uri/$DIRCAP/[SUBDIRS../]?t=rename&from_name=OLD&to_dir=TARGET[&to_name=NEW]``
     1285
     1286 This instructs the node to move a child of the given directory to a
     1287 different directory, both of which must be mutable. The child can also be
     1288 renamed in the process. The to_dir parameter can be either the name of a
     1289 subdirectory of the dircap from which the child is being moved (multiple
     1290 levels of descent are supported) or the writecap of an unrelated directory.
     1291
     1292 This operation will replace any existing child of the new name, making it
     1293 behave like the UNIX "``mv -f``" command. The original child is not
     1294 unlinked until it is linked into the target directory.
     1295
    12801296Other Utilities
    12811297---------------
     
    12991315  'from_name' field of that form. I.e. this presents a form offering to
    13001316  rename $CHILDNAME, requesting the new name, and submitting POST rename.
     1317  This same URL format can also be used with "move-form" with the expected
     1318  results.
    13011319
    13021320``GET /uri/$DIRCAP/[SUBDIRS../]CHILDNAME?t=uri``
  • TabularUnified src/allmydata/test/test_web.py

    r8aa690b rb29d092  
    246246            foo.set_uri(u"sub", sub_uri, sub_uri)
    247247            sub = self.s.create_node_from_uri(sub_uri)
     248            self._sub_node = sub
    248249
    249250            _ign, n, blocking_uri = self.makefile(1)
     
    255256            foo.set_uri(unicode_filename, self._bar_txt_uri, self._bar_txt_uri)
    256257
    257             _ign, n, baz_file = self.makefile(2)
     258            self.SUBBAZ_CONTENTS, n, baz_file = self.makefile(2)
    258259            self._baz_file_uri = baz_file
    259260            sub.set_uri(u"baz.txt", baz_file, baz_file)
     
    309310    def failUnlessIsBazDotTxt(self, res):
    310311        self.failUnlessReallyEqual(res, self.BAZ_CONTENTS, res)
     312
     313    def failUnlessIsSubBazDotTxt(self, res):
     314        self.failUnlessReallyEqual(res, self.SUBBAZ_CONTENTS, res)
    311315
    312316    def failUnlessIsBarJSON(self, res):
     
    12591263                               ])
    12601264            self.failUnless(re.search(get_bar, res), res)
    1261             for label in ['unlink', 'rename']:
     1265            for label in ['unlink', 'rename', 'move']:
    12621266                for line in res.split("\n"):
    12631267                    # find the line that contains the relevant button for bar.txt
     
    32433247        return d
    32443248
     3249    def test_POST_move_file(self):
     3250        """"""
     3251        d = self.POST(self.public_url + "/foo", t="move",
     3252                      from_name="bar.txt", to_dir="sub")
     3253        d.addCallback(lambda res:
     3254                      self.failIfNodeHasChild(self._foo_node, u"bar.txt"))
     3255        d.addCallback(lambda res:
     3256                      self.failUnlessNodeHasChild(self._sub_node, u"bar.txt"))
     3257        d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/bar.txt"))
     3258        d.addCallback(self.failUnlessIsBarDotTxt)
     3259        d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/bar.txt?t=json"))
     3260        d.addCallback(self.failUnlessIsBarJSON)
     3261        return d
     3262
     3263    def test_POST_move_file_new_name(self):
     3264        d = self.POST(self.public_url + "/foo", t="move",
     3265                      from_name="bar.txt", to_name="wibble.txt", to_dir="sub")
     3266        d.addCallback(lambda res:
     3267                      self.failIfNodeHasChild(self._foo_node, u"bar.txt"))
     3268        d.addCallback(lambda res:
     3269                      self.failIfNodeHasChild(self._sub_node, u"bar.txt"))
     3270        d.addCallback(lambda res:
     3271                      self.failUnlessNodeHasChild(self._sub_node, u"wibble.txt"))
     3272        d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/wibble.txt"))
     3273        d.addCallback(self.failUnlessIsBarDotTxt)
     3274        d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/wibble.txt?t=json"))
     3275        d.addCallback(self.failUnlessIsBarJSON)
     3276        return d
     3277
     3278    def test_POST_move_file_replace(self):
     3279        d = self.POST(self.public_url + "/foo", t="move",
     3280                      from_name="bar.txt", to_name="baz.txt", to_dir="sub")
     3281        d.addCallback(lambda res:
     3282                      self.failIfNodeHasChild(self._foo_node, u"bar.txt"))
     3283        d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/baz.txt"))
     3284        d.addCallback(self.failUnlessIsBarDotTxt)
     3285        d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/baz.txt?t=json"))
     3286        d.addCallback(self.failUnlessIsBarJSON)
     3287        return d
     3288
     3289    def test_POST_move_file_no_replace(self):
     3290        d = self.POST(self.public_url + "/foo", t="move", replace="false",
     3291                      from_name="bar.txt", to_name="baz.txt", to_dir="sub")
     3292        d.addBoth(self.shouldFail, error.Error,
     3293                  "POST_move_file_no_replace",
     3294                  "409 Conflict",
     3295                  "There was already a child by that name, and you asked me "
     3296                  "to not replace it")
     3297        d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt"))
     3298        d.addCallback(self.failUnlessIsBarDotTxt)
     3299        d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt?t=json"))
     3300        d.addCallback(self.failUnlessIsBarJSON)
     3301        d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/baz.txt"))
     3302        d.addCallback(self.failUnlessIsSubBazDotTxt)
     3303        return d
     3304
     3305    def test_POST_move_file_slash_fail(self):
     3306        d = self.POST(self.public_url + "/foo", t="move",
     3307                      from_name="bar.txt", to_name="slash/fail.txt", to_dir="sub")
     3308        d.addBoth(self.shouldFail, error.Error,
     3309                  "test_POST_rename_file_slash_fail",
     3310                  "400 Bad Request",
     3311                  "to_name= may not contain a slash",
     3312                  )
     3313        d.addCallback(lambda res:
     3314                      self.failUnlessNodeHasChild(self._foo_node, u"bar.txt"))
     3315        d.addCallback(lambda res:
     3316                      self.failIfNodeHasChild(self._sub_node, u"slash/fail.txt"))
     3317        return d
     3318
     3319    def test_POST_move_file_no_target(self):
     3320        d = self.POST(self.public_url + "/foo", t="move",
     3321                      from_name="bar.txt", to_name="baz.txt")
     3322        d.addBoth(self.shouldFail, error.Error,
     3323                  "POST_move_file_no_target",
     3324                  "400 Bad Request",
     3325                  "move requires from_name and to_dir")
     3326        d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt"))
     3327        d.addCallback(self.failUnlessIsBarDotTxt)
     3328        d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt?t=json"))
     3329        d.addCallback(self.failUnlessIsBarJSON)
     3330        d.addCallback(lambda res: self.GET(self.public_url + "/foo/baz.txt"))
     3331        d.addCallback(self.failUnlessIsBazDotTxt)
     3332        return d
     3333
     3334    def test_POST_move_file_multi_level(self):
     3335        d = self.POST(self.public_url + "/foo/sub/level2?t=mkdir", "")
     3336        d.addCallback(lambda res: self.POST(self.public_url + "/foo", t="move",
     3337                      from_name="bar.txt", to_dir="sub/level2"))
     3338        d.addCallback(lambda res: self.failIfNodeHasChild(self._foo_node, u"bar.txt"))
     3339        d.addCallback(lambda res: self.failIfNodeHasChild(self._sub_node, u"bar.txt"))
     3340        d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/level2/bar.txt"))
     3341        d.addCallback(self.failUnlessIsBarDotTxt)
     3342        d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/level2/bar.txt?t=json"))
     3343        d.addCallback(self.failUnlessIsBarJSON)
     3344        return d
     3345
     3346    def test_POST_move_file_to_uri(self):
     3347        d = self.POST(self.public_url + "/foo", t="move",
     3348                      from_name="bar.txt", to_dir=self._sub_uri)
     3349        d.addCallback(lambda res:
     3350                      self.failIfNodeHasChild(self._foo_node, u"bar.txt"))
     3351        d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/bar.txt"))
     3352        d.addCallback(self.failUnlessIsBarDotTxt)
     3353        d.addCallback(lambda res: self.GET(self.public_url + "/foo/sub/bar.txt?t=json"))
     3354        d.addCallback(self.failUnlessIsBarJSON)
     3355        return d
     3356
     3357    def test_POST_move_file_to_nonexist_dir(self):
     3358        d = self.POST(self.public_url + "/foo", t="move",
     3359                      from_name="bar.txt", to_dir="notchucktesta")
     3360        d.addBoth(self.shouldFail, error.Error,
     3361                  "POST_move_file_to_nonexist_dir",
     3362                  "404 Not Found",
     3363                  "No such child: notchucktesta")
     3364        return d
     3365
     3366    def test_POST_move_file_into_file(self):
     3367        d = self.POST(self.public_url + "/foo", t="move",
     3368                      from_name="bar.txt", to_dir="baz.txt")
     3369        d.addBoth(self.shouldFail, error.Error,
     3370                  "POST_move_file_into_file",
     3371                  "410 Gone",
     3372                  "to_dir is not a usable directory")
     3373        d.addCallback(lambda res: self.GET(self.public_url + "/foo/baz.txt"))
     3374        d.addCallback(self.failUnlessIsBazDotTxt)
     3375        d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt"))
     3376        d.addCallback(self.failUnlessIsBarDotTxt)
     3377        d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt?t=json"))
     3378        d.addCallback(self.failUnlessIsBarJSON)
     3379        return d
     3380
     3381    def test_POST_move_file_to_bad_uri(self):
     3382        d = self.POST(self.public_url + "/foo", t="move", from_name="bar.txt",
     3383                      to_dir="URI:DIR2:mn5jlyjnrjeuydyswlzyui72i:rmneifcj6k6sycjljjhj3f6majsq2zqffydnnul5hfa4j577arma")
     3384        d.addBoth(self.shouldFail, error.Error,
     3385                  "POST_move_file_to_bad_uri",
     3386                  "410 Gone",
     3387                  "to_dir is not a usable directory")
     3388        d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt"))
     3389        d.addCallback(self.failUnlessIsBarDotTxt)
     3390        d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt?t=json"))
     3391        d.addCallback(self.failUnlessIsBarJSON)
     3392        return d
     3393
    32453394    def shouldRedirect(self, res, target=None, statuscode=None, which=""):
    32463395        """ If target is not None then the redirection has to go to target.  If
     
    32973446            self.failUnless(re.search(r'name="from_name" value="bar\.txt"', res))
    32983447            self.failUnlessIn(FAVICON_MARKUP, res)
     3448        d.addCallback(_check)
     3449        return d
     3450
     3451    def test_GET_move_form(self):
     3452        d = self.GET(self.public_url + "/foo?t=move-form&name=bar.txt",
     3453                     followRedirect=True)
     3454        def _check(res):
     3455            self.failUnless('name="when_done" value="."' in res, res)
     3456            self.failUnless(re.search(r'name="from_name" value="bar\.txt"', res))
    32993457        d.addCallback(_check)
    33003458        return d
  • TabularUnified src/allmydata/uri.py

    r8aa690b rb29d092  
    935935            s.startswith(ALLEGED_IMMUTABLE_PREFIX + 'URI:LIT:'))
    936936
     937def 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
    937944def has_uri_prefix(s):
    938945    if not isinstance(s, str):
  • TabularUnified src/allmydata/web/directory.py

    r8aa690b rb29d092  
    1414
    1515from allmydata.util import base32, time_format
    16 from allmydata.uri import from_string_dirnode
     16from allmydata.uri import from_string_dirnode, is_writeable_directory_uri
    1717from allmydata.interfaces import IDirectoryNode, IFileNode, IFilesystemNode, \
    1818     IImmutableFileNode, IMutableFileNode, ExistingChildError, \
     
    170170        if t == 'rename-form':
    171171            return RenameForm(self.node)
     172        if t == 'move-form':
     173            return MoveForm(self.node)
    172174
    173175        raise WebError("GET directory: bad t=%s" % t)
     
    214216        elif t == "rename":
    215217            d = self._POST_rename(req)
     218        elif t == "move":
     219            d = self._POST_move(req)
    216220        elif t == "check":
    217221            d = self._POST_check(req)
     
    417421        d = self.node.move_child_to(from_name, self.node, to_name, replace)
    418422        d.addCallback(lambda res: "thing renamed")
     423        return d
     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")
    419469        return d
    420470
     
    663713            unlink = "-"
    664714            rename = "-"
     715            move = "-"
    665716        else:
    666717            # this creates a button which will cause our _POST_unlink method
     
    681732                ]
    682733
     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
    683741        ctx.fillSlots("unlink", unlink)
    684742        ctx.fillSlots("rename", rename)
     743        ctx.fillSlots("move", move)
    685744
    686745        times = []
     
    928987        header = ["Rename "
    929988                  "in directory SI=%s" % abbreviated_dirnode(self.original),
     989                  ]
     990
     991        if self.original.is_readonly():
     992            header.append(" (readonly!)")
     993        header.append(":")
     994        return ctx.tag[header]
     995
     996    def render_when_done(self, ctx, data):
     997        return T.input(type="hidden", name="when_done", value=".")
     998
     999    def render_get_name(self, ctx, data):
     1000        req = IRequest(ctx)
     1001        name = get_arg(req, "name", "")
     1002        ctx.tag.attributes['value'] = name
     1003        return ctx.tag
     1004
     1005class 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),
    9301015                  ]
    9311016
  • TabularUnified src/allmydata/web/directory.xhtml

    r8aa690b rb29d092  
    3434      <td><n:slot name="unlink"/></td>
    3535      <td><n:slot name="rename"/></td>
     36      <td><n:slot name="move"/></td>
    3637      <td><n:slot name="info"/></td>
    3738    </tr>
Note: See TracChangeset for help on using the changeset viewer.