source: ftputil/file.py

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