root/tags/release1_1_2/ftputil.py

Revision 190, 32.2 kB (checked in by schwa, 7 years ago)
Release 1.1.1 (repackaged the zip to use its own directory).
  • Property svn:mime-type set to text/python
  • Property svn:eol-style set to native
Line 
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
Note: See TracBrowser for help on using the browser.