source: ftputil/file.py @ 1326:7751d4eedc79

Last change on this file since 1326:7751d4eedc79 was 1326:7751d4eedc79, checked in by Stefan Schwarzer <sschwarzer@…>, 7 years ago
Improved comment.
File size: 8.4 KB
Line 
1# Copyright (C) 2003-2013, Stefan Schwarzer <sschwarzer@sschwarzer.net>
2# Copyright (C) 2008, Roger Demetrescu <roger.demetrescu@gmail.com>
3# See the file LICENSE for licensing terms.
4
5"""
6ftputil.file - support for file-like objects on FTP servers
7"""
8
9from __future__ import unicode_literals
10
11import io
12
13import ftputil.compat
14import ftputil.error
15
16
17# This module shouldn't be used by clients of the ftputil library.
18__all__ = []
19
20
21class _FTPFile(object):
22    """
23    Represents a file-like object associated with an FTP host. File
24    and socket are closed appropriately if the `close` method is
25    called.
26    """
27
28    # Set timeout in seconds when closing file connections (see ticket #51).
29    _close_timeout = 5
30
31    def __init__(self, host):
32        """Construct the file(-like) object."""
33        self._host = host
34        self._session = host._session
35        # The file is still closed.
36        self.closed = True
37        self._conn = None
38        self._fobj = None
39
40    def _wrapped_file(self, fobj, is_readable=False, is_writable=False):
41        """
42        Return a new file-like object which wraps `fobj` and in
43        addition has the `readable`, `readinto` and `writable` methods
44        that `BufferedReader` or `BufferedWriter` require.
45        """
46        # I tried to assign the missing methods as bound methods
47        # directly to `fobj`, but this seemingly isn't possible with
48        # the file object returned from `socket.makefile`.
49        class Wrapper(io.RawIOBase):
50            def __init__(self, fobj):
51                super(Wrapper, self).__setattr__("_fobj", fobj)
52            def readable(self):
53                return is_readable
54            def writable(self):
55                return is_writable
56            def readinto(self, bytearray_):
57                data = self._fobj.read(len(bytearray_))
58                bytearray_[:len(data)] = data
59                return len(data)
60            def __getattr__(self, name):
61                return getattr(self._fobj, name)
62            def __setattr__(self, name, value):
63                if name == "__IOBase_closed":
64                    # Delegate to this (`RawIOBase`) instance.
65                    return super(Wrapper, self).__setattr__(name, value)
66                else:
67                    return setattr(self._fobj, name, value)
68        return Wrapper(fobj)
69
70    def _open(self, path, mode, buffering=None, encoding=None, errors=None,
71              newline=None):
72        """
73        Open the remote file with given path name and mode.
74
75        Contrary to the `open` builtin, this method returns `None`,
76        instead this file object is modified in-place.
77        """
78        # Python 3.x's `socket.makefile` supports the same interface
79        # as the new `open` builtin, but Python 2.x supports a mode,
80        # but neither buffering nor encoding/decoding. Therefore, to
81        # make the code work on Python 2.x _and_ 3.x, create an
82        # unbuffered binary file and possibly wrap it.
83        #
84        # Check mode.
85        if "a" in mode:
86            raise ftputil.error.FTPIOError("append mode not supported")
87        if mode not in ("r", "rb", "rt", "w", "wb", "wt"):
88            raise ftputil.error.FTPIOError("invalid mode '{0}'".format(mode))
89        if "b" in mode and "t" in mode:
90            # Raise a `ValueError` like Python would.
91            raise ValueError("can't have text and binary mode at once")
92        # Convenience variables
93        is_bin_mode = "b" in mode
94        is_read_mode = "r" in mode
95        # Always use binary mode (see above).
96        transfer_type = "I"
97        command = 'TYPE {0}'.format(transfer_type)
98        with ftputil.error.ftplib_error_to_ftp_io_error:
99            self._session.voidcmd(command)
100        # Make transfer command.
101        command_type = ('STOR', 'RETR')[is_read_mode]
102        command = '{0} {1}'.format(command_type, path)
103        # Force to binary regardless of transfer type (see above).
104        makefile_mode = mode
105        if "t" in mode:
106            makefile_mode = makefile_mode.replace("t", "")
107        if not "b" in makefile_mode:
108            makefile_mode += "b"
109        # Get connection and file object.
110        with ftputil.error.ftplib_error_to_ftp_io_error:
111            self._conn = self._session.transfercmd(command)
112        # The actual file object.
113        fobj = self._conn.makefile(makefile_mode)
114        if is_read_mode:
115            if ftputil.compat.python_version == 2:
116                # See implementation of `_wrapped_file`.
117                fobj = self._wrapped_file(fobj, is_readable=True)
118            fobj = io.BufferedReader(fobj)
119        else:
120            if ftputil.compat.python_version == 2:
121                # See implementation of `_wrapped_file`.
122                fobj = self._wrapped_file(fobj, is_writable=True)
123            fobj = io.BufferedWriter(fobj)
124        if not is_bin_mode:
125            fobj = io.TextIOWrapper(fobj, encoding=encoding,
126                                    errors=errors, newline=newline)
127        self._fobj = fobj
128        # This comes last so that `close` won't try to close `_FTPFile`
129        # objects without `_conn` and `_fo` attributes in case of an error.
130        self.closed = False
131
132    def __iter__(self):
133        """Return a file iterator."""
134        return self
135
136    def __next__(self):
137        """
138        Return the next line or raise `StopIteration`, if there are
139        no more.
140        """
141        # Apply implicit line ending conversion.
142        line = self.readline()
143        if line:
144            return line
145        else:
146            raise StopIteration
147
148    # Although Python 2.6+ has the `next` builtin function already, it
149    # still requires iterators to have a `next` method.
150    next = __next__
151
152    #
153    # Context manager methods
154    #
155    def __enter__(self):
156        # Return `self`, so it can be accessed as the variable
157        # component of the `with` statement.
158        return self
159
160    def __exit__(self, exc_type, exc_val, exc_tb):
161        # We don't need the `exc_*` arguments here
162        # pylint: disable=W0613
163        self.close()
164        # Be explicit
165        return False
166
167    #
168    # Other attributes
169    #
170    def __getattr__(self, attr_name):
171        """
172        Handle requests for attributes unknown to `_FTPFile` objects:
173        delegate the requests to the contained file object.
174        """
175        if attr_name in ("encoding flush isatty fileno read readline "
176                         "readlines seek tell truncate name softspace "
177                         "write writelines".split()):
178            return getattr(self._fobj, attr_name)
179        raise AttributeError(
180              "'FTPFile' object has no attribute '{0}'".format(attr_name))
181
182    # TODO: Implement `__dir__`? (See
183    # http://docs.python.org/py3k/whatsnew/2.6.html#other-language-changes )
184
185    def close(self):
186        """Close the `FTPFile`."""
187        if self.closed:
188            return
189        # Timeout value to restore, see below.
190        # Statement works only before the try/finally statement,
191        # otherwise Python raises an `UnboundLocalError`.
192        old_timeout = self._session.sock.gettimeout()
193        try:
194            self._fobj.close()
195            self._fobj = None
196            with ftputil.error.ftplib_error_to_ftp_io_error:
197                self._conn.close()
198            # Set a timeout to prevent waiting until server timeout
199            # if we have a server blocking here like in ticket #51.
200            self._session.sock.settimeout(self._close_timeout)
201            try:
202                with ftputil.error.ftplib_error_to_ftp_io_error:
203                    self._session.voidresp()
204            except ftputil.error.FTPIOError as exc:
205                # Ignore some errors, see tickets #51 and #17 at
206                # http://ftputil.sschwarzer.net/trac/ticket/51 and
207                # http://ftputil.sschwarzer.net/trac/ticket/17,
208                # respectively.
209                exc = str(exc)
210                error_code = exc[:3]
211                if exc.splitlines()[0] != "timed out" and \
212                  error_code not in ("150", "426", "450", "451"):
213                    raise
214        finally:
215            # Restore timeout for socket of `_FTPFile`'s `ftplib.FTP`
216            # object in case the connection is reused later.
217            self._session.sock.settimeout(old_timeout)
218            # If something went wrong before, the file is probably
219            # defunct and subsequent calls to `close` won't help
220            # either, so we consider the file closed for practical
221            # purposes.
222            self.closed = True
Note: See TracBrowser for help on using the repository browser.