# Copyright (C) 2003-2022, Stefan Schwarzer # and ftputil contributors (see `doc/contributors.txt`) # See the file LICENSE for licensing terms. """ ftputil.file - support for file-like objects on FTP servers """ import ftputil.error # This module shouldn't be used by clients of the ftputil library. __all__ = [] try: import ssl except ImportError: SSLSocket = None else: SSLSocket = ssl.SSLSocket class FTPFile: """ Represents a file-like object associated with an FTP host. File and socket are closed appropriately if the `close` method is called. """ # Set timeout in seconds when closing file connections (see ticket #51). _close_timeout = 5 def __init__(self, host): """Construct the file(-like) object.""" self._host = host # pylint: disable=protected-access self._session = host._session # The file is still closed. self.closed = True self._conn = None self._fobj = None def _open( self, path, mode, buffering=None, encoding=None, errors=None, newline=None, *, rest=None, ): """ Open the remote file with given path name and mode. Contrary to the `open` builtin, this method returns `None`, instead this file object is modified in-place. """ # We use the same arguments as in `open`. # pylint: disable=unused-argument # pylint: disable=too-many-arguments # # Check mode. if mode is None: # This is Python's behavior for local files. raise TypeError("open() argument 2 must be str, not None") if "a" in mode: raise ftputil.error.FTPIOError("append mode not supported") if mode not in ("r", "rb", "rt", "w", "wb", "wt"): raise ftputil.error.FTPIOError("invalid mode '{}'".format(mode)) if "b" in mode and "t" in mode: # Raise a `ValueError` like Python would. raise ValueError("can't have text and binary mode at once") # Convenience variables is_binary_mode = "b" in mode is_read_mode = "r" in mode # `rest` is only allowed for binary mode. if (not is_binary_mode) and (rest is not None): raise ftputil.error.CommandNotImplementedError( "`rest` argument can't be used for text files" ) # Always use binary mode and leave any conversions to Python, # controlled by the arguments to `makefile` below. transfer_type = "I" command = "TYPE {}".format(transfer_type) with ftputil.error.ftplib_error_to_ftp_io_error: self._session.voidcmd(command) # Make transfer command. command_type = "RETR" if is_read_mode else "STOR" command = "{} {}".format(command_type, path) # Get connection and file object. with ftputil.error.ftplib_error_to_ftp_io_error: self._conn = self._session.transfercmd(command, rest) self._fobj = self._conn.makefile( mode, buffering=buffering, encoding=encoding, errors=errors, newline=newline ) # This comes last so that `close` won't try to close `FTPFile` objects # without `_conn` and `_fobj` attributes in case of an error. self.closed = False def __iter__(self): """ Return a file iterator. """ return self def __next__(self): """ Return the next line or raise `StopIteration`, if there are no more. """ # Apply implicit line ending conversion for text files. line = self.readline() if line: return line else: raise StopIteration # # Context manager methods # def __enter__(self): # Return `self`, so it can be accessed as the variable component of the # `with` statement. return self def __exit__(self, exc_type, exc_val, exc_tb): # We don't need the `exc_*` arguments here # pylint: disable=unused-argument self.close() # Be explicit return False # # Other attributes # def __getattr__(self, attr_name): """ Handle requests for attributes unknown to `FTPFile` objects: delegate the requests to the contained file object. """ if attr_name in ( "encoding flush isatty fileno read readline readlines seek tell " "truncate name softspace write writelines".split() ): return getattr(self._fobj, attr_name) raise AttributeError("'FTPFile' object has no attribute '{}'".format(attr_name)) # TODO: Implement `__dir__`? (See # http://docs.python.org/whatsnew/2.6.html#other-language-changes ) def close(self): """ Close the `FTPFile`. """ if self.closed: return # Timeout value to restore, see below. # Statement works only before the try/finally statement, otherwise # Python raises an `UnboundLocalError`. old_timeout = self._session.sock.gettimeout() try: self._fobj.close() self._fobj = None with ftputil.error.ftplib_error_to_ftp_io_error: if (SSLSocket is not None) and isinstance(self._conn, SSLSocket): self._conn.unwrap() self._conn.close() # Set a timeout to prevent waiting until server timeout if we have # a server blocking here like in ticket #51. self._session.sock.settimeout(self._close_timeout) try: with ftputil.error.ftplib_error_to_ftp_io_error: self._session.voidresp() except ftputil.error.FTPIOError as exc: # Ignore some errors, see tickets #51 and #17 at # http://ftputil.sschwarzer.net/trac/ticket/51 and # http://ftputil.sschwarzer.net/trac/ticket/17, respectively. exc = str(exc) error_code = exc[:3] if exc.splitlines()[0] != "timed out" and error_code not in ( "150", "426", "450", "451", ): raise finally: # Restore timeout for socket of `FTPFile`'s `ftplib.FTP` object in # case the connection is reused later. self._session.sock.settimeout(old_timeout) # If something went wrong before, the file is probably defunct and # subsequent calls to `close` won't help either, so we consider the # file closed for practical purposes. self.closed = True def __getstate__(self): raise TypeError("cannot serialize FTPFile object")