~sschwarzer/ftputil

ac808dc5d241b02249e86e632b933e8489cdfabe — Stefan Schwarzer 10 years ago a0361b7
If the server's `LIST` command accepts the `-a` option, use it.
The option will be used for all subsequent directory requests.

With some servers, this makes the server send directory lines
where the file or directory entry starts with a dot. However,
there's no _guarantee_ that such entries will be displayed even
if the `-a` option is used.

Note that the fact that the FTP server doesn't complain about the
`-a` option means the option has an effect. I did an experiment
and tried arbitrary "options" with `LIST`, but neither gave an
error message, but just a directory listing as without the `-a`
option.
M ftputil/__init__.py => ftputil/__init__.py +30 -1
@@ 124,6 124,11 @@ class FTPHost(object):
        # Set default time shift (used in `upload_if_newer` and
        #  `download_if_newer`).
        self.set_time_shift(0.0)
        # Check if the server accepts the `-a` option for the `LIST`
        #  command. If yes, always use it to tell the server to show
        #  directory and file names with a leading dot.
        self._accepts_list_a_option = False
        self._check_list_a_option()

    def keep_alive(self):
        """


@@ 797,6 802,26 @@ class FTPHost(object):
            # Use straightforward command.
            ftp_error._try_with_oserror(self._session.rename, source, target)

    def _check_list_a_option(self):
        """Check for support of the `-a` option for the `LIST` command.
        If the option is available, use it for all further directory
        listing requests.
        """
        def callback(line):
            """Directory listing callback."""
            pass
        # It seems that most servers just ignore unknown `LIST`
        #  options instead of reacting with an error status.
        #  In such a case, ftputil will subsequently use the `-a`
        #  option even if it doesn't have any apparent effect.
        try:
            ftp_error._try_with_oserror(self._session.dir, u"-a", self.curdir,
                                        callback)
        except ftp_error.PermanentError:
            pass
        else:
            self._accepts_list_a_option = True

    #XXX One could argue to put this method into the `_Stat` class, but
    #  I refrained from that because then `_Stat` would have to know
    #  about `FTPHost`'s `_session` attribute and in turn about


@@ 812,7 837,11 @@ class FTPHost(object):
            def callback(line):
                """Callback function."""
                lines.append(line)
            ftp_error._try_with_oserror(self._session.dir, path, callback)
            if self._accepts_list_a_option:
                args = (self._session.dir, u"-a", path, callback)
            else:
                args = (self._session.dir, path, callback)
            ftp_error._try_with_oserror(*args)
            return lines
        lines = self._robust_ftp_command(_FTPHost_dir_command, path,
                                         descend_deeply=True)

M test/mock_ftplib.py => test/mock_ftplib.py +8 -1
@@ 171,10 171,17 @@ total 1
    def cwd(self, path):
        self.current_dir = self._transform_path(path)

    def dir(self, path, callback=None):
    def dir(self, *args):
        """Provide a callback function for processing each line of
        a directory listing. Return nothing.
        """
        if callable(args[-1]):
            callback = args[-1]
            args = args[:-1]
        else:
            callback = None
        # Everything before the path argument are options.
        path = args[-1]
        if DEBUG:
            print 'dir: %s' % path
        path = self._transform_path(path)

M test/test_ftp_sync.py => test/test_ftp_sync.py +4 -0
@@ 88,6 88,10 @@ class DummyFTPSession(object):
    def pwd(self):
        return u"/"

    def dir(self, *args):
        # Called by `_check_list_a_option`, otherwise not used.
        pass


class DummyFTPPath(object):


M test/test_ftputil.py => test/test_ftputil.py +4 -0
@@ 52,6 52,10 @@ class FailOnLoginSession(mock_ftplib.MockSession):
        raise ftplib.error_perm

class FailOnKeepAliveSession(mock_ftplib.MockSession):
    def dir(self, *args):
        # Implicitly called by `_check_list_a_option`, otherwise unused.
        pass

    def pwd(self):
        # Raise exception on second call to let the constructor work.
        if not hasattr(self, "pwd_called"):

M test/test_real_ftp.py => test/test_real_ftp.py +13 -5
@@ 456,11 456,12 @@ class TestStat(RealFTPTest):
        # Make the cache very small initially and see if it gets resized.
        cache.size = 2
        entries = host.listdir("walk_test")
        # Actually, the cache is going to be 10 because `listdir`
        #  implicitly calls `path.isdir` on the directory argument
        #  which in turn reads the parent directory of `walk_test`
        #  which happens to have 9 entries.
        self.assertEqual(cache.size, 10)
        # The adjusted cache size should be larger or equal to than the
        # number of items in `walk_test` and its parent directory. The
        # latter is read implicitly upon `listdir`'s `isdir` call.
        expected_min_cache_size = max(len(host.listdir(host.curdir)),
                                      len(entries))
        self.assertTrue(cache.size >= expected_min_cache_size)


class TestUploadAndDownload(RealFTPTest):


@@ 792,6 793,13 @@ class TestOther(RealFTPTest):
        host.chdir("rootdir1")
        self.assertRaises(ftp_error.TimeShiftError, host.synchronize_times)

    def test_probing_of_list_a_option(self):
        # Test probing of `LIST -a` option (ticket #63, comment 12).
        host = self.host
        self.assertTrue(host._has_list_a_option)
        directory_entries = host.listdir(host.curdir)
        self.assertTrue(".hidden" in directory_entries)

    def _make_objects_to_be_garbage_collected(self):
        for i in xrange(10):
            with ftputil.FTPHost(server, user, password) as host: