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