1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
|
# Copyright (C) 2003-2022, Stefan Schwarzer <sschwarzer@sschwarzer.net>
# 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")
|