diff -r 2b47f0146639 Doc/library/shutil.rst --- a/Doc/library/shutil.rst Sun Sep 25 17:36:31 2011 +0200 +++ b/Doc/library/shutil.rst Mon Sep 26 17:18:43 2011 -0500 @@ -196,6 +196,33 @@ .. versionadded:: 3.3 +.. function:: chowntree(path, user=None, group=None, followlinks=False) + + Change owner *user* and/or *group* of the given *path* and all contents recursively. + + *user* can be a system user name or a uid; the same applies to *group*. At + least one argument is required. + + See also :func:`os.chown` and :func:`os.walk`, the underlying functions. + + By default, :func:`walk` will not walk down into symbolic links that resolve to + directories. Set *followlinks* to ``True`` to visit directories pointed to by + symlinks, on systems that support them. + + .. note:: + + Be aware that setting *followlinks* to ``True`` can lead to infinite recursion if a + link points to a parent directory of itself. This is due to the fact that the underlying + function :func:`os.walk` does not keep track of + the directories it visited already. + + .. note:: + + If there is an error with the underlying :func:`os.walk` method, then any files whose + ownership was successfully changed will be reverted back to the original ownership. + + Availability: Unix. + .. exception:: Error diff -r 2b47f0146639 Lib/shutil.py --- a/Lib/shutil.py Sun Sep 25 17:36:31 2011 +0200 +++ b/Lib/shutil.py Mon Sep 26 17:18:43 2011 -0500 @@ -822,3 +822,57 @@ raise LookupError("no such group: {!r}".format(group)) os.chown(path, _user, _group) + + +def chowntree(path, user=None, group=None, followlinks=False): + """Change owner user and group of the given path and all contents recursively. + + user and group can be the uid/gid or the user/group names, and in that case, + they are converted to their respective uid/gid. followlinks tells us whether we should + follow symlinks. This may lead to infinite recursion if links loop back. + + The dictionary _modified_items, will keep track of the old ownership details, + with keys being the full path, and values being tuples where the first element is + the uid and the second element is the gid. This way if there are any errors that + arise, we can undo any ownership changes to the old state. + + """ + + if user is None and group is None: + raise ValueError("user and/or group must be set") + + _user = user + _group = group + + # -1 means don't change it + if user is None: + _user = -1 + # user can either be an int (the uid) or a string (the system username) + elif isinstance(user, str): + _user = _get_uid(user) + if _user is None: + raise LookupError("no such user: {!r}".format(user)) + + if group is None: + _group = -1 + elif not isinstance(group, int): + _group = _get_gid(group) + if _group is None: + raise LookupError("no such group: {!r}".format(group)) + + + _modified_items = dict() + def _listdir_errorhandler(oserror): + for path, stat in _modified_items.items(): + os.chown(path, stat.st_uid, stat.st_gid) + raise oserror + + if(os.path.isdir(path)): + for root, dirs, files in os.walk(path, onerror = _listdir_errorhandler, followlinks = followlinks): + for item in (files + dirs): + _full_file_path = os.path.join(root, item) + + stat_info = os.stat(_full_file_path) + os.chown(_full_file_path, _user, _group) + _modified_items[_full_file_path] = (stat_info.st_uid, stat_info.st_gid) + + os.chown(path, _user, _group) diff -r 2b47f0146639 Lib/test/test_shutil.py --- a/Lib/test/test_shutil.py Sun Sep 25 17:36:31 2011 +0200 +++ b/Lib/test/test_shutil.py Mon Sep 26 17:18:43 2011 -0500 @@ -770,6 +770,50 @@ check_chown(filename, uid, gid) shutil.chown(dirname, user, group) check_chown(dirname, uid, gid) + + + @unittest.skipUnless(UID_GID_SUPPORT, "Requires grp and pwd support") + @unittest.skipUnless(hasattr(os, 'chown'), 'requires os.chown') + def test_chowntree(self): + + # cleaned-up automatically by TestShutil.tearDown method + dirname = self.mkdtemp() + filename = tempfile.mktemp(dir=dirname) + write_file(filename, 'testing chown function') + + with self.assertRaises(ValueError): + shutil.chowntree(filename) + + with self.assertRaises(LookupError): + shutil.chowntree(filename, user='non-exising username') + + with self.assertRaises(LookupError): + shutil.chowntree(filename, group='non-exising groupname') + + with self.assertRaises(TypeError): + shutil.chowntree(filename, b'spam') + + with self.assertRaises(TypeError): + shutil.chowntree(filename, 3.14) + + uid = os.getuid() + gid = os.getgid() + + def check_chown(path, uid=None, gid=None): + s = os.stat(filename) + if uid is not None: + self.assertEqual(uid, s.st_uid) + if gid is not None: + self.assertEqual(gid, s.st_gid) + + shutil.chowntree(filename, uid, gid) + check_chown(filename, uid, gid) + shutil.chowntree(filename, uid) + check_chown(filename, uid) + shutil.chowntree(filename, user=uid) + check_chown(filename, uid) + shutil.chowntree(filename, group=gid) + check_chown(filename, gid=gid) class TestMove(unittest.TestCase):