| 1 |
# Copyright (C) 2002, Stefan Schwarzer <s.schwarzer@ndh.net> |
|---|
| 2 |
# All rights reserved. |
|---|
| 3 |
# |
|---|
| 4 |
# Redistribution and use in source and binary forms, with or without |
|---|
| 5 |
# modification, are permitted provided that the following conditions are |
|---|
| 6 |
# met: |
|---|
| 7 |
# |
|---|
| 8 |
# - Redistributions of source code must retain the above copyright |
|---|
| 9 |
# notice, this list of conditions and the following disclaimer. |
|---|
| 10 |
# |
|---|
| 11 |
# - Redistributions in binary form must reproduce the above copyright |
|---|
| 12 |
# notice, this list of conditions and the following disclaimer in the |
|---|
| 13 |
# documentation and/or other materials provided with the distribution. |
|---|
| 14 |
# |
|---|
| 15 |
# - Neither the name of the above author nor the names of the |
|---|
| 16 |
# contributors to the software may be used to endorse or promote |
|---|
| 17 |
# products derived from this software without specific prior written |
|---|
| 18 |
# permission. |
|---|
| 19 |
# |
|---|
| 20 |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
|---|
| 21 |
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
|---|
| 22 |
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
|---|
| 23 |
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR |
|---|
| 24 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
|---|
| 25 |
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
|---|
| 26 |
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
|---|
| 27 |
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
|---|
| 28 |
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
|---|
| 29 |
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|---|
| 30 |
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|---|
| 31 |
|
|---|
| 32 |
# $Id: ftputil.py,v 1.92 2002/04/20 23:05:59 schwa Exp $ |
|---|
| 33 |
|
|---|
| 34 |
""" |
|---|
| 35 |
ftputil - higher level support for FTP sessions |
|---|
| 36 |
|
|---|
| 37 |
FTPHost objects |
|---|
| 38 |
This class resembles the os module's interface to |
|---|
| 39 |
ordinary file systems. In addition, it provides a |
|---|
| 40 |
method file which will return file-objects correspond- |
|---|
| 41 |
ing to remote files. |
|---|
| 42 |
|
|---|
| 43 |
# example session |
|---|
| 44 |
host = ftputil.FTPHost('ftp.domain.com', 'me', 'secret') |
|---|
| 45 |
print host.getcwd() # e. g. '/home/me' |
|---|
| 46 |
source = host.file('sourcefile', 'r') |
|---|
| 47 |
host.mkdir('newdir') |
|---|
| 48 |
host.chdir('newdir') |
|---|
| 49 |
target = host.file('targetfile', 'w') |
|---|
| 50 |
host.copyfileobj(source, target) |
|---|
| 51 |
source.close() |
|---|
| 52 |
target.close() |
|---|
| 53 |
host.remove('targetfile') |
|---|
| 54 |
host.chdir(host.pardir) |
|---|
| 55 |
host.rmdir('newdir') |
|---|
| 56 |
host.close() |
|---|
| 57 |
|
|---|
| 58 |
There are also shortcuts for uploads and downloads: |
|---|
| 59 |
|
|---|
| 60 |
host.upload(local_file, remote_file) |
|---|
| 61 |
host.download(remote_file, local_file) |
|---|
| 62 |
|
|---|
| 63 |
Both accept an additional mode parameter. If it's 'b' |
|---|
| 64 |
the transfer mode will be for binary files. |
|---|
| 65 |
|
|---|
| 66 |
FTPFile objects |
|---|
| 67 |
FTPFile objects are constructed via the file method of |
|---|
| 68 |
FTPHost objects. FTPFile objects support the usual file |
|---|
| 69 |
operations for non-seekable files (read, readline, |
|---|
| 70 |
readlines, xreadlines, write, writelines, close). |
|---|
| 71 |
|
|---|
| 72 |
Note: ftputil currently is not threadsafe. More specifically, |
|---|
| 73 |
you can use different FTPHost objects in different |
|---|
| 74 |
threads but not using a single FTPHost object in |
|---|
| 75 |
different threads. |
|---|
| 76 |
""" |
|---|
| 77 |
|
|---|
| 78 |
# Ideas for future development: |
|---|
| 79 |
# - allow to set an offset for the time difference of local |
|---|
| 80 |
# and remote host |
|---|
| 81 |
# - handle connection timeouts |
|---|
| 82 |
# - caching of FTPHost.stat results?? |
|---|
| 83 |
# - map FTP error numbers to os error numbers (ENOENT etc.)? |
|---|
| 84 |
|
|---|
| 85 |
# for Python 2.1 |
|---|
| 86 |
from __future__ import nested_scopes |
|---|
| 87 |
|
|---|
| 88 |
import ftplib |
|---|
| 89 |
import stat |
|---|
| 90 |
import time |
|---|
| 91 |
import os |
|---|
| 92 |
import sys |
|---|
| 93 |
import posixpath |
|---|
| 94 |
|
|---|
| 95 |
if sys.version_info[:2] >= (2, 2): |
|---|
| 96 |
_StatBase = tuple |
|---|
| 97 |
else: |
|---|
| 98 |
import UserTuple |
|---|
| 99 |
_StatBase = UserTuple.UserTuple |
|---|
| 100 |
|
|---|
| 101 |
__all__ = ['FTPError', 'FTPOSError', 'TemporaryError', |
|---|
| 102 |
'PermanentError', 'ParserError', 'FTPIOError', |
|---|
| 103 |
'RootDirError', 'FTPHost'] |
|---|
| 104 |
__version__ = '1.1.1' |
|---|
| 105 |
|
|---|
| 106 |
|
|---|
| 107 |
##################################################################### |
|---|
| 108 |
# Exception classes and wrappers |
|---|
| 109 |
|
|---|
| 110 |
class FTPError: |
|---|
| 111 |
"""General error class""" |
|---|
| 112 |
|
|---|
| 113 |
def __init__(self, ftp_exception): |
|---|
| 114 |
self.args = (ftp_exception,) |
|---|
| 115 |
self.strerror = str(ftp_exception) |
|---|
| 116 |
try: |
|---|
| 117 |
self.errno = int(self.strerror[:3]) |
|---|
| 118 |
except (TypeError, IndexError, ValueError): |
|---|
| 119 |
self.errno = None |
|---|
| 120 |
self.filename = None |
|---|
| 121 |
|
|---|
| 122 |
def __str__(self): |
|---|
| 123 |
return self.strerror |
|---|
| 124 |
|
|---|
| 125 |
class RootDirError(FTPError): pass |
|---|
| 126 |
|
|---|
| 127 |
class FTPOSError(FTPError, OSError): pass |
|---|
| 128 |
class TemporaryError(FTPOSError): pass |
|---|
| 129 |
class PermanentError(FTPOSError): pass |
|---|
| 130 |
class ParserError(FTPOSError): pass |
|---|
| 131 |
|
|---|
| 132 |
#XXX Do you know better names for _try_with_oserror and |
|---|
| 133 |
# _try_with_ioerror? |
|---|
| 134 |
def _try_with_oserror(callee, *args, **kwargs): |
|---|
| 135 |
""" |
|---|
| 136 |
Try the callee with the given arguments and map resulting |
|---|
| 137 |
exceptions from ftplib.all_errors to FTPOSError and its |
|---|
| 138 |
derived classes. |
|---|
| 139 |
""" |
|---|
| 140 |
try: |
|---|
| 141 |
return callee(*args, **kwargs) |
|---|
| 142 |
except ftplib.error_temp, obj: |
|---|
| 143 |
raise TemporaryError(obj) |
|---|
| 144 |
except ftplib.error_perm, obj: |
|---|
| 145 |
raise PermanentError(obj) |
|---|
| 146 |
except ftplib.all_errors: |
|---|
| 147 |
ftp_error = sys.exc_info()[1] |
|---|
| 148 |
raise FTPOSError(ftp_error) |
|---|
| 149 |
|
|---|
| 150 |
class FTPIOError(FTPError, IOError): pass |
|---|
| 151 |
|
|---|
| 152 |
def _try_with_ioerror(callee, *args, **kwargs): |
|---|
| 153 |
""" |
|---|
| 154 |
Try the callee with the given arguments and map resulting |
|---|
| 155 |
exceptions from ftplib.all_errors to FTPIOError. |
|---|
| 156 |
""" |
|---|
| 157 |
try: |
|---|
| 158 |
return callee(*args, **kwargs) |
|---|
| 159 |
except ftplib.all_errors: |
|---|
| 160 |
ftp_error = sys.exc_info()[1] |
|---|
| 161 |
raise FTPIOError(ftp_error) |
|---|
| 162 |
|
|---|
| 163 |
|
|---|
| 164 |
##################################################################### |
|---|
| 165 |
# Support for file-like objects |
|---|
| 166 |
|
|---|
| 167 |
# converter for \r\n line ends to normalized ones in Python. |
|---|
| 168 |
# RFC 959 states that the server will send \r\n on text mode |
|---|
| 169 |
# transfers, so this conversion should be safe. I still use |
|---|
| 170 |
# text mode transfers (mode 'r', not 'rb') in socket.makefile |
|---|
| 171 |
# (below) because the server may do charset conversions on |
|---|
| 172 |
# text transfers. |
|---|
| 173 |
_crlf_to_python_linesep = lambda text: text.replace('\r', '') |
|---|
| 174 |
|
|---|
| 175 |
# converter for Python line ends into \r\n |
|---|
| 176 |
_python_to_crlf_linesep = lambda text: text.replace('\n', '\r\n') |
|---|
| 177 |
|
|---|
| 178 |
|
|---|
| 179 |
# helper class for xreadline protocol for ASCII transfers |
|---|
| 180 |
class _XReadlines: |
|---|
| 181 |
"""Represents xreadline objects for ASCII transfers.""" |
|---|
| 182 |
|
|---|
| 183 |
def __init__(self, ftp_file): |
|---|
| 184 |
self._ftp_file = ftp_file |
|---|
| 185 |
self._next_index = 0 |
|---|
| 186 |
|
|---|
| 187 |
def __getitem__(self, index): |
|---|
| 188 |
"""Return next line with specified index.""" |
|---|
| 189 |
if index != self._next_index: |
|---|
| 190 |
raise RuntimeError( "_XReadline access index " |
|---|
| 191 |
"out of order (expected %s but got %s)" % |
|---|
| 192 |
(self._next_index, index) ) |
|---|
| 193 |
line = self._ftp_file.readline() |
|---|
| 194 |
if not line: |
|---|
| 195 |
raise IndexError("_XReadline object out of data") |
|---|
| 196 |
self._next_index = self._next_index + 1 |
|---|
| 197 |
return line |
|---|
| 198 |
|
|---|
| 199 |
|
|---|
| 200 |
class _FTPFile: |
|---|
| 201 |
""" |
|---|
| 202 |
Represents a file-like object connected to an FTP host. |
|---|
| 203 |
File and socket are closed appropriately if the close |
|---|
| 204 |
operation is requested. |
|---|
| 205 |
""" |
|---|
| 206 |
|
|---|
| 207 |
def __init__(self, host): |
|---|
| 208 |
"""Construct the file(-like) object.""" |
|---|
| 209 |
self._host = host |
|---|
| 210 |
self._session = host._session |
|---|
| 211 |
self.closed = 1 # yet closed |
|---|
| 212 |
|
|---|
| 213 |
def _open(self, path, mode): |
|---|
| 214 |
"""Open the remote file with given pathname and mode.""" |
|---|
| 215 |
# check mode |
|---|
| 216 |
if 'a' in mode: |
|---|
| 217 |
raise FTPIOError("append mode not supported") |
|---|
| 218 |
if mode not in ('r', 'rb', 'w', 'wb'): |
|---|
| 219 |
raise FTPIOError("invalid mode '%s'" % mode) |
|---|
| 220 |
# remember convenience variables instead of mode |
|---|
| 221 |
self._binmode = 'b' in mode |
|---|
| 222 |
self._readmode = 'r' in mode |
|---|
| 223 |
# select ASCII or binary mode |
|---|
| 224 |
transfer_type = ('A', 'I')[self._binmode] |
|---|
| 225 |
command = 'TYPE %s' % transfer_type |
|---|
| 226 |
_try_with_ioerror(self._session.voidcmd, command) |
|---|
| 227 |
# make transfer command |
|---|
| 228 |
command_type = ('STOR', 'RETR')[self._readmode] |
|---|
| 229 |
command = '%s %s' % (command_type, path) |
|---|
| 230 |
# ensure we can process the raw line separators; |
|---|
| 231 |
# force to binary regardless of transfer type |
|---|
| 232 |
if not 'b' in mode: |
|---|
| 233 |
mode = mode + 'b' |
|---|
| 234 |
# get connection and file object |
|---|
| 235 |
self._conn = _try_with_ioerror(self._session.transfercmd, command) |
|---|
| 236 |
self._fo = self._conn.makefile(mode) |
|---|
| 237 |
# this comes last so that close does not try to |
|---|
| 238 |
# close _FTPFile objects without _conn and _fo |
|---|
| 239 |
# attributes |
|---|
| 240 |
self.closed = 0 |
|---|
| 241 |
|
|---|
| 242 |
# |
|---|
| 243 |
# Read and write operations with support for |
|---|
| 244 |
# line separator conversion for text modes. |
|---|
| 245 |
# |
|---|
| 246 |
# Note that we must convert line endings because |
|---|
| 247 |
# the FTP server expects \r\n to be sent on text |
|---|
| 248 |
# transfers. |
|---|
| 249 |
# |
|---|
| 250 |
def read(self, *args): |
|---|
| 251 |
"""Return read bytes, normalized if in text transfer mode.""" |
|---|
| 252 |
data = self._fo.read(*args) |
|---|
| 253 |
if self._binmode: |
|---|
| 254 |
return data |
|---|
| 255 |
data = _crlf_to_python_linesep(data) |
|---|
| 256 |
if args == (): |
|---|
| 257 |
return data |
|---|
| 258 |
# If the read data contains \r characters the number |
|---|
| 259 |
# of read characters will be too small! Thus we |
|---|
| 260 |
# (would) have to continue to read until we have |
|---|
| 261 |
# fetched the requested number of bytes (or run out |
|---|
| 262 |
# of source data). |
|---|
| 263 |
# The algorithm below avoids repetitive string |
|---|
| 264 |
# concatanations in the style of |
|---|
| 265 |
# data = data + more_data |
|---|
| 266 |
# and so should also work relatively well if there |
|---|
| 267 |
# are many short lines in the file. |
|---|
| 268 |
wanted_size = args[0] |
|---|
| 269 |
chunks = [data] |
|---|
| 270 |
current_size = len(data) |
|---|
| 271 |
while current_size < wanted_size: |
|---|
| 272 |
# print 'not enough bytes (now %s, wanting %s)' % \ |
|---|
| 273 |
# (current_size, wanted_size) |
|---|
| 274 |
more_data = self._fo.read(wanted_size - current_size) |
|---|
| 275 |
if not more_data: |
|---|
| 276 |
break |
|---|
| 277 |
more_data = _crlf_to_python_linesep(more_data) |
|---|
| 278 |
# print '-> new (normalized) data:', repr(more_data) |
|---|
| 279 |
chunks.append(more_data) |
|---|
| 280 |
current_size += len(more_data) |
|---|
| 281 |
return ''.join(chunks) |
|---|
| 282 |
|
|---|
| 283 |
def readline(self, *args): |
|---|
| 284 |
"""Return one read line, normalized if in text transfer mode.""" |
|---|
| 285 |
data = self._fo.readline(*args) |
|---|
| 286 |
if self._binmode: |
|---|
| 287 |
return data |
|---|
| 288 |
# eventually complete begun newline |
|---|
| 289 |
if data.endswith('\r'): |
|---|
| 290 |
data = data + self.read(1) |
|---|
| 291 |
return _crlf_to_python_linesep(data) |
|---|
| 292 |
|
|---|
| 293 |
def readlines(self, *args): |
|---|
| 294 |
"""Return read lines, normalized if in text transfer mode.""" |
|---|
| 295 |
lines = self._fo.readlines(*args) |
|---|
| 296 |
if self._binmode: |
|---|
| 297 |
return lines |
|---|
| 298 |
# more memory-friendly than |
|---|
| 299 |
# return [... for line in lines] |
|---|
| 300 |
for i in range( len(lines) ): |
|---|
| 301 |
lines[i] = _crlf_to_python_linesep(lines[i]) |
|---|
| 302 |
return lines |
|---|
| 303 |
|
|---|
| 304 |
def xreadlines(self): |
|---|
| 305 |
""" |
|---|
| 306 |
Return an appropriate xreadlines object with |
|---|
| 307 |
built-in line separator conversion support. |
|---|
| 308 |
""" |
|---|
| 309 |
if self._binmode: |
|---|
| 310 |
return self._fo.xreadlines() |
|---|
| 311 |
return _XReadlines(self) |
|---|
| 312 |
|
|---|
| 313 |
def write(self, data): |
|---|
| 314 |
"""Write data to file. Do linesep conversion for text mode.""" |
|---|
| 315 |
if not self._binmode: |
|---|
| 316 |
data = _python_to_crlf_linesep(data) |
|---|
| 317 |
self._fo.write(data) |
|---|
| 318 |
|
|---|
| 319 |
def writelines(self, lines): |
|---|
| 320 |
"""Write lines to file. Do linesep conversion for text mode.""" |
|---|
| 321 |
if self._binmode: |
|---|
| 322 |
self._fo.writelines(lines) |
|---|
| 323 |
return |
|---|
| 324 |
for line in lines: |
|---|
| 325 |
self._fo.write( _python_to_crlf_linesep(line) ) |
|---|
| 326 |
|
|---|
| 327 |
# |
|---|
| 328 |
# other attributes |
|---|
| 329 |
# |
|---|
| 330 |
def __getattr__(self, attr_name): |
|---|
| 331 |
"""Delegate unknown attribute requests to the file.""" |
|---|
| 332 |
if attr_name in ( 'flush isatty fileno seek tell ' |
|---|
| 333 |
'truncate name softspace'.split() ): |
|---|
| 334 |
return getattr(self._fo, attr_name) |
|---|
| 335 |
raise AttributeError("'FTPFile' object has no " |
|---|
| 336 |
"attribute '%s'" % attr_name) |
|---|
| 337 |
|
|---|
| 338 |
def close(self): |
|---|
| 339 |
"""Close the FTPFile.""" |
|---|
| 340 |
if not self.closed: |
|---|
| 341 |
self._fo.close() |
|---|
| 342 |
_try_with_ioerror(self._conn.close) |
|---|
| 343 |
_try_with_ioerror(self._session.voidresp) |
|---|
| 344 |
self.closed = 1 |
|---|
| 345 |
|
|---|
| 346 |
def __del__(self): |
|---|
| 347 |
self.close() |
|---|
| 348 |
|
|---|
| 349 |
|
|---|
| 350 |
############################################################ |
|---|
| 351 |
# FTPHost class with several methods similar to those of os |
|---|
| 352 |
|
|---|
| 353 |
class FTPHost: |
|---|
| 354 |
"""FTP host class""" |
|---|
| 355 |
|
|---|
| 356 |
# Implementation notes: |
|---|
| 357 |
# |
|---|
| 358 |
# Upon every request of a file (_FTPFile object) a |
|---|
| 359 |
# new FTP session is created ("cloned"), leading |
|---|
| 360 |
# to a child session of the FTPHost object from which the |
|---|
| 361 |
# file is requested. |
|---|
| 362 |
# |
|---|
| 363 |
# This is needed because opening an _FTPFile will make |
|---|
| 364 |
# the local session object wait for the completion of the |
|---|
| 365 |
# transfer. In fact, code like this would block |
|---|
| 366 |
# indefinitely, if the RETR request would be made on the |
|---|
| 367 |
# _session of the object host: |
|---|
| 368 |
# |
|---|
| 369 |
# host = FTPHost(ftp_server, user, password) |
|---|
| 370 |
# f = host.file('index.html') |
|---|
| 371 |
# host.getcwd() # would block! |
|---|
| 372 |
# |
|---|
| 373 |
# On the other hand, the initially constructed host object |
|---|
| 374 |
# will store references to already established _FTPFile |
|---|
| 375 |
# objects and reuse an associated connection if its |
|---|
| 376 |
# associated _FTPFile has been closed. |
|---|
| 377 |
|
|---|
| 378 |
def __init__(self, *args, **kwargs): |
|---|
| 379 |
"""Abstract initialization of FTPHost object.""" |
|---|
| 380 |
# store arguments for later operations |
|---|
| 381 |
self._args = args |
|---|
| 382 |
self._kwargs = kwargs |
|---|
| 383 |
# make a session according to these arguments |
|---|
| 384 |
self._session = self._make_session() |
|---|
| 385 |
# simulate os.path |
|---|
| 386 |
self.path = _Path(self) |
|---|
| 387 |
# associated FTPHost objects for data transfer |
|---|
| 388 |
self._children = [] |
|---|
| 389 |
self.closed = 0 |
|---|
| 390 |
# set curdir, pardir etc. for the remote host; |
|---|
| 391 |
# RFC 959 states that this is, strictly spoken, |
|---|
| 392 |
# dependent on the server OS but it seems to work |
|---|
| 393 |
# at least with Unix and Windows servers |
|---|
| 394 |
self.curdir, self.pardir, self.sep = '.', '..', '/' |
|---|
| 395 |
# check if we have a Microsoft ROBIN server |
|---|
| 396 |
try: |
|---|
| 397 |
response = _try_with_oserror(self._session.voidcmd, 'STAT') |
|---|
| 398 |
except PermanentError: |
|---|
| 399 |
response = '' |
|---|
| 400 |
if response.find('ROBIN Microsoft') != -1: |
|---|
| 401 |
self._parser = self._parse_robin_line |
|---|
| 402 |
else: |
|---|
| 403 |
self._parser = self._parse_unix_line |
|---|
| 404 |
|
|---|
| 405 |
# |
|---|
| 406 |
# dealing with child sessions and file-like objects |
|---|
| 407 |
# |
|---|
| 408 |
def _make_session(self): |
|---|
| 409 |
""" |
|---|
| 410 |
Return a new session object according to the current state |
|---|
| 411 |
of this FTPHost instance. |
|---|
| 412 |
""" |
|---|
| 413 |
args = self._args[:] |
|---|
| 414 |
kwargs = self._kwargs.copy() |
|---|
| 415 |
if kwargs.has_key('session_factory'): |
|---|
| 416 |
factory = kwargs['session_factory'] |
|---|
| 417 |
del kwargs['session_factory'] |
|---|
| 418 |
else: |
|---|
| 419 |
factory = ftplib.FTP |
|---|
| 420 |
return _try_with_oserror(factory, *args, **kwargs) |
|---|
| 421 |
|
|---|
| 422 |
def _copy(self): |
|---|
| 423 |
"""Return a copy of this FTPHost object.""" |
|---|
| 424 |
# The copy includes a new session factory return value |
|---|
| 425 |
# (aka session) but doesn't copy the state of self.getcwd(). |
|---|
| 426 |
return FTPHost(*self._args, **self._kwargs) |
|---|
| 427 |
|
|---|
| 428 |
def _available_child(self): |
|---|
| 429 |
""" |
|---|
| 430 |
Return an available (i. e. one whose _file object is closed) |
|---|
| 431 |
child (FTPHost object) from the pool of children or None if |
|---|
| 432 |
there aren't any. |
|---|
| 433 |
""" |
|---|
| 434 |
for host in self._children: |
|---|
| 435 |
if host._file.closed: |
|---|
| 436 |
return host |
|---|
| 437 |
return None |
|---|
| 438 |
|
|---|
| 439 |
def file(self, path, mode='r'): |
|---|
| 440 |
""" |
|---|
| 441 |
Return an open file(-like) object which is associated with |
|---|
| 442 |
this FTPHost object. |
|---|
| 443 |
|
|---|
| 444 |
This method tries to reuse a child but will generate a new one |
|---|
| 445 |
if none is available. |
|---|
| 446 |
""" |
|---|
| 447 |
host = self._available_child() |
|---|
| 448 |
if host is None: |
|---|
| 449 |
host = self._copy() |
|---|
| 450 |
self._children.append(host) |
|---|
| 451 |
host._file = _FTPFile(host) |
|---|
| 452 |
basedir = self.getcwd() |
|---|
| 453 |
host.chdir(basedir) |
|---|
| 454 |
host._file._open(path, mode) |
|---|
| 455 |
return host._file |
|---|
| 456 |
|
|---|
| 457 |
def open(self, path, mode='r'): |
|---|
| 458 |
return self.file(path, mode) |
|---|
| 459 |
|
|---|
| 460 |
def copyfileobj(self, source, target, length=64*1024): |
|---|
| 461 |
"Copy data from file-like object source to file-like object target." |
|---|
| 462 |
# inspired by shutil.copyfileobj (I don't use the |
|---|
| 463 |
# shutil code directly because it might change) |
|---|
| 464 |
while 1: |
|---|
| 465 |
buf = source.read(length) |
|---|
| 466 |
if not buf: |
|---|
| 467 |
break |
|---|
| 468 |
target.write(buf) |
|---|
| 469 |
|
|---|
| 470 |
def __get_modes(self, mode): |
|---|
| 471 |
"""Return modes for source and target file.""" |
|---|
| 472 |
if mode == 'b': |
|---|
| 473 |
return 'rb', 'wb' |
|---|
| 474 |
else: |
|---|
| 475 |
return 'r', 'w' |
|---|
| 476 |
|
|---|
| 477 |
def upload(self, source, target, mode=''): |
|---|
| 478 |
""" |
|---|
| 479 |
Upload a file from the local source (name) to the remote |
|---|
| 480 |
target (name). The argument mode is an empty string or 'a' for |
|---|
| 481 |
text copies, or 'b' for binary copies. |
|---|
| 482 |
""" |
|---|
| 483 |
source_mode, target_mode = self.__get_modes(mode) |
|---|
| 484 |
source = open(source, source_mode) |
|---|
| 485 |
target = self.file(target, target_mode) |
|---|
| 486 |
self.copyfileobj(source, target) |
|---|
| 487 |
source.close() |
|---|
| 488 |
target.close() |
|---|
| 489 |
|
|---|
| 490 |
def download(self, source, target, mode=''): |
|---|
| 491 |
""" |
|---|
| 492 |
Download a file from the remote source (name) to the local |
|---|
| 493 |
target (name). The argument mode is an empty string or 'a' for |
|---|
| 494 |
text copies, or 'b' for binary copies. |
|---|
| 495 |
""" |
|---|
| 496 |
source_mode, target_mode = self.__get_modes(mode) |
|---|
| 497 |
source = self.file(source, source_mode) |
|---|
| 498 |
target = open(target, target_mode) |
|---|
| 499 |
self.copyfileobj(source, target) |
|---|
| 500 |
source.close() |
|---|
| 501 |
target.close() |
|---|
| 502 |
|
|---|
| 503 |
def upload_if_newer(self, source, target, mode=''): |
|---|
| 504 |
""" |
|---|
| 505 |
Upload a file only if it's newer than the target on the |
|---|
| 506 |
remote host or if the target file does not exist. |
|---|
| 507 |
""" |
|---|
| 508 |
source_timestamp = os.path.getmtime(source) |
|---|
| 509 |
if self.path.exists(target): |
|---|
| 510 |
target_timestamp = self.path.getmtime(target) |
|---|
| 511 |
else: |
|---|
| 512 |
# every timestamp is newer than this one |
|---|
| 513 |
target_timestamp = 0.0 |
|---|
| 514 |
if source_timestamp > target_timestamp: |
|---|
| 515 |
self.upload(source, target, mode) |
|---|
| 516 |
|
|---|
| 517 |
def download_if_newer(self, source, target, mode=''): |
|---|
| 518 |
""" |
|---|
| 519 |
Download a file only if it's newer than the target on the |
|---|
| 520 |
local host or if the target file does not exist. |
|---|
| 521 |
""" |
|---|
| 522 |
source_timestamp = self.path.getmtime(source) |
|---|
| 523 |
if os.path.exists(target): |
|---|
| 524 |
target_timestamp = os.path.getmtime(target) |
|---|
| 525 |
else: |
|---|
| 526 |
# every timestamp is newer than this one |
|---|
| 527 |
target_timestamp = 0.0 |
|---|
| 528 |
if source_timestamp > target_timestamp: |
|---|
| 529 |
self.download(source, target, mode) |
|---|
| 530 |
|
|---|
| 531 |
def close(self): |
|---|
| 532 |
"""Close host connection.""" |
|---|
| 533 |
if not self.closed: |
|---|
| 534 |
# close associated children |
|---|
| 535 |
for host in self._children: |
|---|
| 536 |
# only children have _file attributes |
|---|
| 537 |
host._file.close() |
|---|
| 538 |
host.close() |
|---|
| 539 |
# now deal with our-self |
|---|
| 540 |
_try_with_oserror(self._session.close) |
|---|
| 541 |
self._children = [] |
|---|
| 542 |
self.closed = 1 |
|---|
| 543 |
|
|---|
| 544 |
def __del__(self): |
|---|
| 545 |
try: |
|---|
| 546 |
self.close() |
|---|
| 547 |
except: |
|---|
| 548 |
# don't want warnings if constructor had failed |
|---|
| 549 |
pass |
|---|
| 550 |
|
|---|
| 551 |
# |
|---|
| 552 |
# miscellaneous utility methods resembling those in os |
|---|
| 553 |
# |
|---|
| 554 |
def getcwd(self): |
|---|
| 555 |
"""Return the current path name.""" |
|---|
| 556 |
return _try_with_oserror(self._session.pwd) |
|---|
| 557 |
|
|---|
| 558 |
def chdir(self, path): |
|---|
| 559 |
"""Change the directory on the host.""" |
|---|
| 560 |
_try_with_oserror(self._session.cwd, path) |
|---|
| 561 |
|
|---|
| 562 |
def mkdir(self, path, mode=None): |
|---|
| 563 |
""" |
|---|
| 564 |
Make the directory path on the remote host. The argument mode |
|---|
| 565 |
is ignored and only "supported" for similarity with os.mkdir. |
|---|
| 566 |
""" |
|---|
| 567 |
_try_with_oserror(self._session.mkd, path) |
|---|
| 568 |
|
|---|
| 569 |
def rmdir(self, path): |
|---|
| 570 |
"""Remove the directory on the remote host.""" |
|---|
| 571 |
_try_with_oserror(self._session.rmd, path) |
|---|
| 572 |
|
|---|
| 573 |
def remove(self, path): |
|---|
| 574 |
"""Remove the given file.""" |
|---|
| 575 |
_try_with_oserror(self._session.delete, path) |
|---|
| 576 |
|
|---|
| 577 |
def unlink(self, path): |
|---|
| 578 |
"""Remove the given file.""" |
|---|
| 579 |
self.remove(path) |
|---|
| 580 |
|
|---|
| 581 |
def rename(self, source, target): |
|---|
| 582 |
"""Rename the source on the FTP host to target.""" |
|---|
| 583 |
_try_with_oserror(self._session.rename, source, target) |
|---|
| 584 |
|
|---|
| 585 |
def listdir(self, path): |
|---|
| 586 |
""" |
|---|
| 587 |
Return a list with directories, files etc. in the directory |
|---|
| 588 |
named path. |
|---|
| 589 |
""" |
|---|
| 590 |
path = self.path.abspath(path) |
|---|
| 591 |
if not self.path.isdir(path): |
|---|
| 592 |
raise PermanentError("550 %s: no such directory" % path) |
|---|
| 593 |
names = [] |
|---|
| 594 |
def callback(line): |
|---|
| 595 |
stat_result = self._parse_line(line, fail=0) |
|---|
| 596 |
if stat_result is not None: |
|---|
| 597 |
names.append(stat_result._st_name) |
|---|
| 598 |
_try_with_oserror(self._session.dir, path, callback) |
|---|
| 599 |
return names |
|---|
| 600 |
|
|---|
| 601 |
def _stat_candidates(self, lines, wanted_name): |
|---|
| 602 |
"""Return candidate lines for further analysis.""" |
|---|
| 603 |
return [line for line in lines |
|---|
| 604 |
if line.find(wanted_name) != -1] |
|---|
| 605 |
|
|---|
| 606 |
_month_numbers = { |
|---|
| 607 |
'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, |
|---|
| 608 |
'may': 5, 'jun': 6, 'jul': 7, 'aug': 8, |
|---|
| 609 |
'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12} |
|---|
| 610 |
|
|---|
| 611 |
def _parse_unix_line(self, line): |
|---|
| 612 |
""" |
|---|
| 613 |
Return _Stat instance corresponding to the given text line. |
|---|
| 614 |
Exceptions are caught in _parse_line. |
|---|
| 615 |
""" |
|---|
| 616 |
metadata, nlink, user, group, size, month, day, \ |
|---|
| 617 |
year_or_time, name = line.split(None, 8) |
|---|
| 618 |
# st_mode |
|---|
| 619 |
st_mode = 0 |
|---|
| 620 |
for bit in metadata[1:10]: |
|---|
| 621 |
bit = (bit != '-') |
|---|
| 622 |
st_mode = (st_mode << 1) + bit |
|---|
| 623 |
if metadata[3] == 's': |
|---|
| 624 |
st_mode = st_mode | stat.S_ISUID |
|---|
| 625 |
if metadata[6] == 's': |
|---|
| 626 |
st_mode = st_mode | stat.S_ISGID |
|---|
| 627 |
char_to_mode = {'d': stat.S_IFDIR, 'l': stat.S_IFLNK, |
|---|
| 628 |
'c': stat.S_IFCHR, '-': stat.S_IFREG} |
|---|
| 629 |
file_type = metadata[0] |
|---|
| 630 |
if char_to_mode.has_key(file_type): |
|---|
| 631 |
st_mode = st_mode | char_to_mode[file_type] |
|---|
| 632 |
else: |
|---|
| 633 |
raise ParserError("unknown file type character '%s'" % file_type) |
|---|
| 634 |
# st_ino, st_dev, st_nlink, st_uid, st_gid, |
|---|
| 635 |
# st_size, st_atime |
|---|
| 636 |
st_ino = None |
|---|
| 637 |
st_dev = None |
|---|
| 638 |
st_nlink = int(nlink) |
|---|
| 639 |
st_uid = user |
|---|
| 640 |
st_gid = group |
|---|
| 641 |
st_size = int(size) |
|---|
| 642 |
st_atime = None |
|---|
| 643 |
# st_mtime |
|---|
| 644 |
month = self._month_numbers[ month.lower() ] |
|---|
| 645 |
day = int(day) |
|---|
| 646 |
if year_or_time.find(':') == -1: |
|---|
| 647 |
# year_or_time is really a year |
|---|
| 648 |
year, hour, minute = int(year_or_time), 0, 0 |
|---|
| 649 |
st_mtime = time.mktime( (year, month, day, hour, |
|---|
| 650 |
minute, 0, 0, 0, -1) ) |
|---|
| 651 |
else: |
|---|
| 652 |
# year_or_time is a time hh:mm |
|---|
| 653 |
hour, minute = year_or_time.split(':') |
|---|
| 654 |
year, hour, minute = None, int(hour), int(minute) |
|---|
| 655 |
# try the current year |
|---|
| 656 |
year = time.localtime()[0] |
|---|
| 657 |
st_mtime = time.mktime( (year, month, day, hour, |
|---|
| 658 |
minute, 0, 0, 0, -1) ) |
|---|
| 659 |
if st_mtime > time.time(): |
|---|
| 660 |
# if it's in the future use previous year |
|---|
| 661 |
st_mtime = time.mktime( (year-1, month, day, |
|---|
| 662 |
hour, minute, 0, 0, 0, -1) ) |
|---|
| 663 |
# st_ctime |
|---|
| 664 |
st_ctime = None |
|---|
| 665 |
# st_name |
|---|
| 666 |
if name.find(' -> ') != -1: |
|---|
| 667 |
st_name, st_target = name.split(' -> ') |
|---|
| 668 |
else: |
|---|
| 669 |
st_name, st_target = name, None |
|---|
| 670 |
result = _Stat( (st_mode, st_ino, st_dev, st_nlink, |
|---|
| 671 |
st_uid, st_gid, st_size, st_atime, |
|---|
| 672 |
st_mtime, st_ctime) ) |
|---|
| 673 |
result._st_name = st_name |
|---|
| 674 |
result._st_target = st_target |
|---|
| 675 |
return result |
|---|
| 676 |
|
|---|
| 677 |
def _parse_robin_line(self, line): |
|---|
| 678 |
""" |
|---|
| 679 |
Return _Stat instance corresponding to the given text line |
|---|
| 680 |
from a MS ROBIN FTP server. Exceptions are caught in |
|---|
| 681 |
_parse_line. |
|---|
| 682 |
""" |
|---|
| 683 |
date, time_, dir_or_size, name = line.split(None, 3) |
|---|
| 684 |
# st_mode |
|---|
| 685 |
st_mode = 0400 # default to read access only; |
|---|
| 686 |
# in fact, we can't tell |
|---|
| 687 |
if dir_or_size == '<DIR>': |
|---|
| 688 |
st_mode = st_mode | stat.S_IFDIR |
|---|
| 689 |
else: |
|---|
| 690 |
st_mode = st_mode | stat.S_IFREG |
|---|
| 691 |
# st_ino, st_dev, st_nlink, st_uid, st_gid |
|---|
| 692 |
st_ino = None |
|---|
| 693 |
st_dev = None |
|---|
| 694 |
st_nlink = None |
|---|
| 695 |
st_uid = None |
|---|
| 696 |
st_gid = None |
|---|
| 697 |
# st_size |
|---|
| 698 |
if dir_or_size != '<DIR>': |
|---|
| 699 |
st_size = int(dir_or_size) |
|---|
| 700 |
else: |
|---|
| 701 |
st_size = None |
|---|
| 702 |
# st_atime |
|---|
| 703 |
st_atime = None |
|---|
| 704 |
# st_mtime |
|---|
| 705 |
month, day, year = map( int, date.split('-') ) |
|---|
| 706 |
if year >= 70: |
|---|
| 707 |
year = 1900 + year |
|---|
| 708 |
else: |
|---|
| 709 |
year = 2000 + year |
|---|
| 710 |
hour, minute, am_pm = time_[0:2], time_[3:5], time_[5] |
|---|
| 711 |
hour, minute = int(hour), int(minute) |
|---|
| 712 |
if am_pm == 'P': |
|---|
| 713 |
hour = 12 + hour |
|---|
| 714 |
st_mtime = time.mktime( (year, month, day, hour, |
|---|
| 715 |
minute, 0, 0, 0, -1) ) |
|---|
| 716 |
# st_ctime |
|---|
| 717 |
st_ctime = None |
|---|
| 718 |
result = _Stat( (st_mode, st_ino, st_dev, st_nlink, |
|---|
| 719 |
st_uid, st_gid, st_size, st_atime, |
|---|
| 720 |
st_mtime, st_ctime) ) |
|---|
| 721 |
# _st_name and _st_target |
|---|
| 722 |
result._st_name = name |
|---|
| 723 |
result._st_target = None |
|---|
| 724 |
return result |
|---|
| 725 |
|
|---|
| 726 |
def _parse_line(self, line, fail=1): |
|---|
| 727 |
"""Return _Stat instance corresponding to the given text line.""" |
|---|
| 728 |
try: |
|---|
| 729 |
return self._parser(line) |
|---|
| 730 |
except (ValueError, IndexError): |
|---|
| 731 |
if fail: |
|---|
| 732 |
raise ParserError("can't parse line '%s'" % line) |
|---|
| 733 |
else: |
|---|
| 734 |
return None |
|---|
| 735 |
|
|---|
| 736 |
def lstat(self, path): |
|---|
| 737 |
"""Return an object similar to that returned by os.lstat.""" |
|---|
| 738 |
# get output from DIR |
|---|
| 739 |
lines = [] |
|---|
| 740 |
path = self.path.abspath(path) |
|---|
| 741 |
# Note: (l)stat works by going one directory up and parsing |
|---|
| 742 |
# the output of an FTP DIR command. Unfortunately, it is not |
|---|
| 743 |
# possible to to this for the root directory / . |
|---|
| 744 |
if path == '/': |
|---|
| 745 |
raise RootDirError("can't invoke stat for remote root directory") |
|---|
| 746 |
dirname, basename = self.path.split(path) |
|---|
| 747 |
_try_with_oserror( self._session.dir, dirname, |
|---|
| 748 |
lambda line: lines.append(line) ) |
|---|
| 749 |
# search for name to be stat'ed without full parsing |
|---|
| 750 |
candidates = self._stat_candidates(lines, basename) |
|---|
| 751 |
# parse candidates |
|---|
| 752 |
for line in candidates: |
|---|
| 753 |
stat_result = self._parse_line(line, fail=0) |
|---|
| 754 |
if (stat_result is not None) and \ |
|---|
| 755 |
(stat_result._st_name == basename): |
|---|
| 756 |
return stat_result |
|---|
| 757 |
raise PermanentError("550 %s: no such file or directory" % path) |
|---|
| 758 |
|
|---|
| 759 |
def stat(self, path): |
|---|
| 760 |
"""Return info from a stat call.""" |
|---|
| 761 |
visited_paths = {} |
|---|
| 762 |
while 1: |
|---|
| 763 |
stat_result = self.lstat(path) |
|---|
| 764 |
if not stat.S_ISLNK(stat_result.st_mode): |
|---|
| 765 |
return stat_result |
|---|
| 766 |
dirname, basename = self.path.split(path) |
|---|
| 767 |
path = self.path.join(dirname, stat_result._st_target) |
|---|
| 768 |
path = self.path.normpath(path) |
|---|
| 769 |
if visited_paths.has_key(path): |
|---|
| 770 |
raise PermanentError("recursive link structure detected") |
|---|
| 771 |
visited_paths[path] = 1 |
|---|
| 772 |
|
|---|
| 773 |
|
|---|
| 774 |
##################################################################### |
|---|
| 775 |
# Helper classes _Stat and _Path to imitate behaviour of stat objects |
|---|
| 776 |
# and os.path module contents. |
|---|
| 777 |
|
|---|
| 778 |
class _Stat(_StatBase): |
|---|
| 779 |
""" |
|---|
| 780 |
Support class resembling a tuple like that which is returned |
|---|
| 781 |
from os.(l)stat. |
|---|
| 782 |
""" |
|---|
| 783 |
|
|---|
| 784 |
_index_mapping = { |
|---|
| 785 |
'st_mode': 0, 'st_ino': 1, 'st_dev': 2, 'st_nlink': 3, |
|---|
| 786 |
'st_uid': 4, 'st_gid': 5, 'st_size': 6, 'st_atime': 7, |
|---|
| 787 |
'st_mtime': 8, 'st_ctime': 9, '_st_name': 10, '_st_target': 11} |
|---|
| 788 |
|
|---|
| 789 |
def __getattr__(self, attr_name): |
|---|
| 790 |
if self._index_mapping.has_key(attr_name): |
|---|
| 791 |
return self[ self._index_mapping[attr_name] ] |
|---|
| 792 |
else: |
|---|
| 793 |
raise AttributeError("'_Stat' object has no attribute '%s'" % |
|---|
| 794 |
attr_name) |
|---|
| 795 |
|
|---|
| 796 |
|
|---|
| 797 |
class _Path: |
|---|
| 798 |
""" |
|---|
| 799 |
Support class resembling os.path, accessible from the |
|---|
| 800 |
FTPHost() object e. g. as FTPHost().path.abspath(path). |
|---|
| 801 |
Hint: substitute os with the FTPHost() object. |
|---|
| 802 |
""" |
|---|
| 803 |
|
|---|
| 804 |
def __init__(self, host): |
|---|
| 805 |
self._host = host |
|---|
| 806 |
# delegate these to posixpath |
|---|
| 807 |
pp = posixpath |
|---|
| 808 |
self.dirname = pp.dirname |
|---|
| 809 |
self.basename = pp.basename |
|---|
| 810 |
self.isabs = pp.isabs |
|---|
| 811 |
self.commonprefix = pp.commonprefix |
|---|
| 812 |
self.join = pp.join |
|---|
| 813 |
self.splitdrive = pp.splitdrive |
|---|
| 814 |
self.splitext = pp.splitext |
|---|
| 815 |
self.normcase = pp.normcase |
|---|
| 816 |
self.normpath = pp.normpath |
|---|
| 817 |
|
|---|
| 818 |
def abspath(self, path): |
|---|
| 819 |
"""Return an absolute path.""" |
|---|
| 820 |
if not self.isabs(path): |
|---|
| 821 |
path = self.join( self._host.getcwd(), path ) |
|---|
| 822 |
return self.normpath(path) |
|---|
| 823 |
|
|---|
| 824 |
def split(self, path): |
|---|
| 825 |
return posixpath.split(path) |
|---|
| 826 |
|
|---|
| 827 |
def exists(self, path): |
|---|
| 828 |
try: |
|---|
| 829 |
self._host.lstat(path) |
|---|
| 830 |
return 1 |
|---|
| 831 |
except RootDirError: |
|---|
| 832 |
return 1 |
|---|
| 833 |
except FTPOSError: |
|---|
| 834 |
return 0 |
|---|
| 835 |
|
|---|
| 836 |
def getmtime(self, path): |
|---|
| 837 |
return self._host.stat(path).st_mtime |
|---|
| 838 |
|
|---|
| 839 |
def getsize(self, path): |
|---|
| 840 |
return self._host.stat(path).st_size |
|---|
| 841 |
|
|---|
| 842 |
# check whether a path is a regular file/dir/link; |
|---|
| 843 |
# for the first two cases follow links (like in os.path) |
|---|
| 844 |
def isfile(self, path): |
|---|
| 845 |
try: |
|---|
| 846 |
stat_result = self._host.stat(path) |
|---|
| 847 |
except RootDirError: |
|---|
| 848 |
return 0 |
|---|
| 849 |
except FTPOSError: |
|---|
| 850 |
return 0 |
|---|
| 851 |
return stat.S_ISREG(stat_result.st_mode) |
|---|
| 852 |
|
|---|
| 853 |
def isdir(self, path): |
|---|
| 854 |
try: |
|---|
| 855 |
stat_result = self._host.stat(path) |
|---|
| 856 |
except RootDirError: |
|---|
| 857 |
return 1 |
|---|
| 858 |
except FTPOSError: |
|---|
| 859 |
return 0 |
|---|
| 860 |
return stat.S_ISDIR(stat_result.st_mode) |
|---|
| 861 |
|
|---|
| 862 |
def islink(self, path): |
|---|
| 863 |
try: |
|---|
| 864 |
stat_result = self._host.lstat(path) |
|---|
| 865 |
except RootDirError: |
|---|
| 866 |
return 0 |
|---|
| 867 |
except FTPOSError: |
|---|
| 868 |
return 0 |
|---|
| 869 |
return stat.S_ISLNK(stat_result.st_mode) |
|---|
| 870 |
|
|---|
| 871 |
def walk(self, top, func, arg): |
|---|
| 872 |
""" |
|---|
| 873 |
Directory tree walk with callback function. |
|---|
| 874 |
|
|---|
| 875 |
For each directory in the directory tree rooted at top |
|---|
| 876 |
(including top itself, but excluding '.' and '..'), call |
|---|
| 877 |
func(arg, dirname, fnames). dirname is the name of the |
|---|
| 878 |
directory, and fnames a list of the names of the files and |
|---|
| 879 |
subdirectories in dirname (excluding '.' and '..'). func may |
|---|
| 880 |
modify the fnames list in-place (e.g. via del or slice |
|---|
| 881 |
assignment), and walk will only recurse into the |
|---|
| 882 |
subdirectories whose names remain in fnames; this can be used |
|---|
| 883 |
to implement a filter, or to impose a specific order of |
|---|
| 884 |
visiting. No semantics are defined for, or required of, arg, |
|---|
| 885 |
beyond that arg is always passed to func. It can be used, |
|---|
| 886 |
e.g., to pass a filename pattern, or a mutable object designed |
|---|
| 887 |
to accumulate statistics. Passing None for arg is common. |
|---|
| 888 |
""" |
|---|
| 889 |
# This code (and the above documentation) is taken from |
|---|
| 890 |
# posixpath.py, with slight modifications |
|---|
| 891 |
try: |
|---|
| 892 |
names = self._host.listdir(top) |
|---|
| 893 |
except OSError: |
|---|
| 894 |
return |
|---|
| 895 |
func(arg, top, names) |
|---|
| 896 |
for name in names: |
|---|
| 897 |
name = self.join(top, name) |
|---|
| 898 |
try: |
|---|
| 899 |
st = self._host.lstat(name) |
|---|
| 900 |
except OSError: |
|---|
| 901 |
continue |
|---|
| 902 |
if stat.S_ISDIR(st[stat.ST_MODE]): |
|---|
| 903 |
self.walk(name, func, arg) |
|---|
| 904 |
|
|---|
| 905 |
# Unix format |
|---|
| 906 |
# total 14 |
|---|
| 907 |
# drwxr-sr-x 2 45854 200 512 May 4 2000 chemeng |
|---|
| 908 |
# drwxr-sr-x 2 45854 200 512 Jan 3 17:17 download |
|---|
| 909 |
# drwxr-sr-x 2 45854 200 512 Jul 30 17:14 image |
|---|
| 910 |
# -rw-r--r-- 1 45854 200 4604 Jan 19 23:11 index.html |
|---|
| 911 |
# drwxr-sr-x 2 45854 200 512 May 29 2000 os2 |
|---|
| 912 |
# lrwxrwxrwx 2 45854 200 512 May 29 2000 osup -> ../os2 |
|---|
| 913 |
# drwxr-sr-x 2 45854 200 512 May 25 2000 publications |
|---|
| 914 |
# drwxr-sr-x 2 45854 200 512 Jan 20 16:12 python |
|---|
| 915 |
# drwxr-sr-x 6 45854 200 512 Sep 20 1999 scios2 |
|---|
| 916 |
|
|---|
| 917 |
# Microsoft ROBIN FTP server |
|---|
| 918 |
# 07-04-01 12:57PM <DIR> SharePoint_Launch |
|---|
| 919 |
# 11-12-01 04:38PM <DIR> Solution Sales |
|---|
| 920 |
# 06-27-01 01:53PM <DIR> SPPS |
|---|
| 921 |
# 01-08-02 01:32PM <DIR> technet |
|---|
| 922 |
# 07-27-01 11:16AM <DIR> Test |
|---|
| 923 |
# 10-23-01 06:49PM <DIR> Wernerd |
|---|
| 924 |
# 10-23-01 03:25PM <DIR> WindowsXP |
|---|
| 925 |
# 12-07-01 02:05PM <DIR> XPLaunch |
|---|
| 926 |
# 07-17-00 02:08PM 12266720 digidash.exe |
|---|
| 927 |
# 07-17-00 02:08PM 89264 O2KKeys.exe |
|---|
| 928 |
|
|---|