| 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 |
ftp_stat.py - stat result, parsers, and FTP stat'ing for `ftputil` |
|---|
| 34 |
""" |
|---|
| 35 |
|
|---|
| 36 |
|
|---|
| 37 |
|
|---|
| 38 |
import re |
|---|
| 39 |
import stat |
|---|
| 40 |
import time |
|---|
| 41 |
|
|---|
| 42 |
import ftp_error |
|---|
| 43 |
import ftp_stat_cache |
|---|
| 44 |
|
|---|
| 45 |
|
|---|
| 46 |
class StatResult(tuple): |
|---|
| 47 |
""" |
|---|
| 48 |
Support class resembling a tuple like that returned from |
|---|
| 49 |
`os.(l)stat`. |
|---|
| 50 |
""" |
|---|
| 51 |
|
|---|
| 52 |
_index_mapping = { |
|---|
| 53 |
'st_mode': 0, 'st_ino': 1, 'st_dev': 2, 'st_nlink': 3, |
|---|
| 54 |
'st_uid': 4, 'st_gid': 5, 'st_size': 6, 'st_atime': 7, |
|---|
| 55 |
'st_mtime': 8, 'st_ctime': 9, '_st_name': 10, '_st_target': 11} |
|---|
| 56 |
|
|---|
| 57 |
def __init__(self, sequence): |
|---|
| 58 |
super(StatResult, self).__init__(sequence) |
|---|
| 59 |
|
|---|
| 60 |
self._st_name = "" |
|---|
| 61 |
self._st_target = None |
|---|
| 62 |
|
|---|
| 63 |
def __getattr__(self, attr_name): |
|---|
| 64 |
if self._index_mapping.has_key(attr_name): |
|---|
| 65 |
return self[self._index_mapping[attr_name]] |
|---|
| 66 |
else: |
|---|
| 67 |
raise AttributeError("'StatResult' object has no attribute '%s'" % |
|---|
| 68 |
attr_name) |
|---|
| 69 |
|
|---|
| 70 |
|
|---|
| 71 |
|
|---|
| 72 |
|
|---|
| 73 |
class Parser(object): |
|---|
| 74 |
""" |
|---|
| 75 |
Represent a parser for directory lines. Parsers for specific |
|---|
| 76 |
directory formats inherit from this class. |
|---|
| 77 |
""" |
|---|
| 78 |
|
|---|
| 79 |
|
|---|
| 80 |
_month_numbers = { |
|---|
| 81 |
'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, |
|---|
| 82 |
'may': 5, 'jun': 6, 'jul': 7, 'aug': 8, |
|---|
| 83 |
'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12} |
|---|
| 84 |
|
|---|
| 85 |
_total_regex = re.compile(r"^total\s+\d+") |
|---|
| 86 |
|
|---|
| 87 |
def ignores_line(self, line): |
|---|
| 88 |
""" |
|---|
| 89 |
Return a true value if the line should be ignored, i. e. is |
|---|
| 90 |
assumed to _not_ contain actual directory/file/link data. |
|---|
| 91 |
A typical example are summary lines like "total 23" which |
|---|
| 92 |
are emitted by some FTP servers. |
|---|
| 93 |
|
|---|
| 94 |
If the line should be used to extract stat data from it, |
|---|
| 95 |
return a false value. |
|---|
| 96 |
""" |
|---|
| 97 |
|
|---|
| 98 |
match = self._total_regex.search(line) |
|---|
| 99 |
return bool(match) |
|---|
| 100 |
|
|---|
| 101 |
def parse_line(self, line, time_shift=0.0): |
|---|
| 102 |
""" |
|---|
| 103 |
Return a `StatResult` object as derived from the string |
|---|
| 104 |
`line`. The parser code to use depends on the directory format |
|---|
| 105 |
the FTP server delivers (also see examples at end of file). |
|---|
| 106 |
|
|---|
| 107 |
If the given text line can't be parsed, raise a `ParserError`. |
|---|
| 108 |
|
|---|
| 109 |
For the definition of `time_shift` see the docstring of |
|---|
| 110 |
`FTPHost.set_time_shift` in `ftputil.py`. Not all parsers |
|---|
| 111 |
use the `time_shift` parameter. |
|---|
| 112 |
""" |
|---|
| 113 |
raise NotImplementedError("must be defined by subclass") |
|---|
| 114 |
|
|---|
| 115 |
|
|---|
| 116 |
|
|---|
| 117 |
|
|---|
| 118 |
def parse_unix_mode(self, mode_string): |
|---|
| 119 |
""" |
|---|
| 120 |
Return an integer from the `mode_string`, compatible with |
|---|
| 121 |
the `st_mode` value in stat results. Such a mode string |
|---|
| 122 |
may look like "drwxr-xr-x". |
|---|
| 123 |
|
|---|
| 124 |
If the mode string can't be parsed, raise an |
|---|
| 125 |
`ftp_error.ParserError`. |
|---|
| 126 |
""" |
|---|
| 127 |
st_mode = 0 |
|---|
| 128 |
if len(mode_string) != 10: |
|---|
| 129 |
raise ftp_error.ParserError("invalid mode string '%s'" % |
|---|
| 130 |
mode_string) |
|---|
| 131 |
for bit in mode_string[1:10]: |
|---|
| 132 |
bit = (bit != '-') |
|---|
| 133 |
st_mode = (st_mode << 1) + bit |
|---|
| 134 |
if mode_string[3] == 's': |
|---|
| 135 |
st_mode = st_mode | stat.S_ISUID |
|---|
| 136 |
if mode_string[6] == 's': |
|---|
| 137 |
st_mode = st_mode | stat.S_ISGID |
|---|
| 138 |
file_type_to_mode = {'d': stat.S_IFDIR, 'l': stat.S_IFLNK, |
|---|
| 139 |
'c': stat.S_IFCHR, '-': stat.S_IFREG} |
|---|
| 140 |
file_type = mode_string[0] |
|---|
| 141 |
if file_type in file_type_to_mode: |
|---|
| 142 |
st_mode = st_mode | file_type_to_mode[file_type] |
|---|
| 143 |
else: |
|---|
| 144 |
raise ftp_error.ParserError( |
|---|
| 145 |
"unknown file type character '%s'" % file_type) |
|---|
| 146 |
return st_mode |
|---|
| 147 |
|
|---|
| 148 |
def parse_unix_time(self, month_abbreviation, day, year_or_time, |
|---|
| 149 |
time_shift): |
|---|
| 150 |
""" |
|---|
| 151 |
Return a floating point number, like from `time.mktime`, by |
|---|
| 152 |
parsing the string arguments `month_abbreviation`, `day` and |
|---|
| 153 |
`year_or_time`. The parameter `time_shift` is the difference |
|---|
| 154 |
"time on server" - "time on client" and is available as the |
|---|
| 155 |
`time_shift` parameter in the `parse_line` interface. |
|---|
| 156 |
|
|---|
| 157 |
Times in Unix-style directory listings typically have one of |
|---|
| 158 |
these formats: |
|---|
| 159 |
|
|---|
| 160 |
- "Nov 23 02:33" (month name, day of month, time) |
|---|
| 161 |
|
|---|
| 162 |
- "May 26 2005" (month name, day of month, year) |
|---|
| 163 |
|
|---|
| 164 |
If this method can not make sense of the given arguments, it |
|---|
| 165 |
raises an `ftp_error.ParserError`. |
|---|
| 166 |
""" |
|---|
| 167 |
try: |
|---|
| 168 |
month = self._month_numbers[month_abbreviation.lower()] |
|---|
| 169 |
except KeyError: |
|---|
| 170 |
raise ftp_error.ParserError("invalid month name '%s'" % month) |
|---|
| 171 |
day = int(day) |
|---|
| 172 |
if ":" not in year_or_time: |
|---|
| 173 |
|
|---|
| 174 |
year, hour, minute = int(year_or_time), 0, 0 |
|---|
| 175 |
st_mtime = time.mktime( (year, month, day, |
|---|
| 176 |
hour, minute, 0, 0, 0, -1) ) |
|---|
| 177 |
else: |
|---|
| 178 |
|
|---|
| 179 |
hour, minute = year_or_time.split(':') |
|---|
| 180 |
year, hour, minute = None, int(hour), int(minute) |
|---|
| 181 |
|
|---|
| 182 |
year = time.localtime()[0] |
|---|
| 183 |
st_mtime = time.mktime( (year, month, day, |
|---|
| 184 |
hour, minute, 0, 0, 0, -1) ) |
|---|
| 185 |
|
|---|
| 186 |
|
|---|
| 187 |
|
|---|
| 188 |
|
|---|
| 189 |
|
|---|
| 190 |
|
|---|
| 191 |
|
|---|
| 192 |
|
|---|
| 193 |
|
|---|
| 194 |
|
|---|
| 195 |
|
|---|
| 196 |
if st_mtime > time.time() + time_shift + 60.0: |
|---|
| 197 |
|
|---|
| 198 |
st_mtime = time.mktime( (year-1, month, day, |
|---|
| 199 |
hour, minute, 0, 0, 0, -1) ) |
|---|
| 200 |
return st_mtime |
|---|
| 201 |
|
|---|
| 202 |
def parse_ms_time(self, date, time_, time_shift): |
|---|
| 203 |
""" |
|---|
| 204 |
Return a floating point number, like from `time.mktime`, by |
|---|
| 205 |
parsing the string arguments `date` and `time_`. The parameter |
|---|
| 206 |
`time_shift` is the difference |
|---|
| 207 |
|
|---|
| 208 |
"time on server" - "time on client" |
|---|
| 209 |
|
|---|
| 210 |
and can be set as the `time_shift` parameter in the |
|---|
| 211 |
`parse_line` interface. |
|---|
| 212 |
|
|---|
| 213 |
Times in MS-style directory listings typically have the |
|---|
| 214 |
format "10-23-01 03:25PM" (month-day_of_month-two_digit_year, |
|---|
| 215 |
hour:minute, am/pm). |
|---|
| 216 |
|
|---|
| 217 |
If this method can not make sense of the given arguments, it |
|---|
| 218 |
raises an `ftp_error.ParserError`. |
|---|
| 219 |
""" |
|---|
| 220 |
|
|---|
| 221 |
|
|---|
| 222 |
try: |
|---|
| 223 |
month, day, year = [int(part) for part in date.split('-')] |
|---|
| 224 |
if year >= 70: |
|---|
| 225 |
year = 1900 + year |
|---|
| 226 |
else: |
|---|
| 227 |
year = 2000 + year |
|---|
| 228 |
hour, minute, am_pm = time_[0:2], time_[3:5], time_[5] |
|---|
| 229 |
hour, minute = int(hour), int(minute) |
|---|
| 230 |
except (ValueError, IndexError): |
|---|
| 231 |
raise ftp_error.ParserError("invalid time string '%s'" % time_) |
|---|
| 232 |
if am_pm == 'P': |
|---|
| 233 |
hour = hour + 12 |
|---|
| 234 |
st_mtime = time.mktime( (year, month, day, |
|---|
| 235 |
hour, minute, 0, 0, 0, -1) ) |
|---|
| 236 |
return st_mtime |
|---|
| 237 |
|
|---|
| 238 |
|
|---|
| 239 |
class UnixParser(Parser): |
|---|
| 240 |
"""`Parser` class for Unix-specific directory format.""" |
|---|
| 241 |
|
|---|
| 242 |
def _split_line(self, line): |
|---|
| 243 |
""" |
|---|
| 244 |
Split a line in metadata, nlink, user, group, size, month, |
|---|
| 245 |
day, year_or_time and name and return the result as an |
|---|
| 246 |
nine-element list of these values. |
|---|
| 247 |
""" |
|---|
| 248 |
|
|---|
| 249 |
|
|---|
| 250 |
|
|---|
| 251 |
parts = line.split(None, 8) |
|---|
| 252 |
if len(parts) == 9: |
|---|
| 253 |
if parts[-1].startswith("-> "): |
|---|
| 254 |
|
|---|
| 255 |
|
|---|
| 256 |
|
|---|
| 257 |
|
|---|
| 258 |
|
|---|
| 259 |
|
|---|
| 260 |
parts.insert(2, None) |
|---|
| 261 |
parts[-2] = "%s %s" % tuple(parts[-2:]) |
|---|
| 262 |
del parts[-1] |
|---|
| 263 |
return parts |
|---|
| 264 |
elif len(parts) == 8: |
|---|
| 265 |
|
|---|
| 266 |
parts.insert(2, None) |
|---|
| 267 |
return parts |
|---|
| 268 |
else: |
|---|
| 269 |
|
|---|
| 270 |
raise ftp_error.ParserError("line '%s' can't be parsed" % line) |
|---|
| 271 |
|
|---|
| 272 |
def parse_line(self, line, time_shift=0.0): |
|---|
| 273 |
""" |
|---|
| 274 |
Return a `StatResult` instance corresponding to the given |
|---|
| 275 |
text line. The `time_shift` value is needed to determine |
|---|
| 276 |
to which year a datetime without an explicit year belongs. |
|---|
| 277 |
|
|---|
| 278 |
If the line can't be parsed, raise a `ParserError`. |
|---|
| 279 |
""" |
|---|
| 280 |
mode_string, nlink, user, group, size, month, day, \ |
|---|
| 281 |
year_or_time, name = self._split_line(line) |
|---|
| 282 |
|
|---|
| 283 |
st_mode = self.parse_unix_mode(mode_string) |
|---|
| 284 |
|
|---|
| 285 |
st_ino = None |
|---|
| 286 |
st_dev = None |
|---|
| 287 |
st_nlink = int(nlink) |
|---|
| 288 |
st_uid = user |
|---|
| 289 |
st_gid = group |
|---|
| 290 |
st_size = int(size) |
|---|
| 291 |
st_atime = None |
|---|
| 292 |
|
|---|
| 293 |
st_mtime = self.parse_unix_time(month, day, year_or_time, time_shift) |
|---|
| 294 |
|
|---|
| 295 |
st_ctime = None |
|---|
| 296 |
|
|---|
| 297 |
if " -> " in name: |
|---|
| 298 |
st_name, st_target = name.split(' -> ') |
|---|
| 299 |
else: |
|---|
| 300 |
st_name, st_target = name, None |
|---|
| 301 |
stat_result = StatResult( |
|---|
| 302 |
(st_mode, st_ino, st_dev, st_nlink, st_uid, |
|---|
| 303 |
st_gid, st_size, st_atime, st_mtime, st_ctime) ) |
|---|
| 304 |
stat_result._st_name = st_name |
|---|
| 305 |
stat_result._st_target = st_target |
|---|
| 306 |
return stat_result |
|---|
| 307 |
|
|---|
| 308 |
|
|---|
| 309 |
class MSParser(Parser): |
|---|
| 310 |
"""`Parser` class for MS-specific directory format.""" |
|---|
| 311 |
|
|---|
| 312 |
def parse_line(self, line, time_shift=0.0): |
|---|
| 313 |
""" |
|---|
| 314 |
Return a `StatResult` instance corresponding to the given |
|---|
| 315 |
text line from a FTP server which emits "Microsoft format" |
|---|
| 316 |
(see end of file). |
|---|
| 317 |
|
|---|
| 318 |
If the line can't be parsed, raise a `ParserError`. |
|---|
| 319 |
|
|---|
| 320 |
The parameter `time_shift` isn't used in this method but is |
|---|
| 321 |
listed for compatibilty with the base class. |
|---|
| 322 |
""" |
|---|
| 323 |
try: |
|---|
| 324 |
date, time_, dir_or_size, name = line.split(None, 3) |
|---|
| 325 |
except ValueError: |
|---|
| 326 |
|
|---|
| 327 |
raise ftp_error.ParserError("line '%s' can't be parsed" % line ) |
|---|
| 328 |
|
|---|
| 329 |
|
|---|
| 330 |
st_mode = 0400 |
|---|
| 331 |
if dir_or_size == "<DIR>": |
|---|
| 332 |
st_mode = st_mode | stat.S_IFDIR |
|---|
| 333 |
else: |
|---|
| 334 |
st_mode = st_mode | stat.S_IFREG |
|---|
| 335 |
|
|---|
| 336 |
st_ino = None |
|---|
| 337 |
st_dev = None |
|---|
| 338 |
st_nlink = None |
|---|
| 339 |
st_uid = None |
|---|
| 340 |
st_gid = None |
|---|
| 341 |
|
|---|
| 342 |
if dir_or_size != "<DIR>": |
|---|
| 343 |
try: |
|---|
| 344 |
st_size = int(dir_or_size) |
|---|
| 345 |
except ValueError: |
|---|
| 346 |
raise ftp_error.ParserError("invalid size %s" % dir_or_size) |
|---|
| 347 |
else: |
|---|
| 348 |
st_size = None |
|---|
| 349 |
|
|---|
| 350 |
st_atime = None |
|---|
| 351 |
|
|---|
| 352 |
st_mtime = self.parse_ms_time(date, time_, time_shift) |
|---|
| 353 |
|
|---|
| 354 |
st_ctime = None |
|---|
| 355 |
stat_result = StatResult( |
|---|
| 356 |
(st_mode, st_ino, st_dev, st_nlink, st_uid, |
|---|
| 357 |
st_gid, st_size, st_atime, st_mtime, st_ctime) ) |
|---|
| 358 |
|
|---|
| 359 |
stat_result._st_name = name |
|---|
| 360 |
stat_result._st_target = None |
|---|
| 361 |
return stat_result |
|---|
| 362 |
|
|---|
| 363 |
|
|---|
| 364 |
|
|---|
| 365 |
|
|---|
| 366 |
class _Stat(object): |
|---|
| 367 |
"""Methods for stat'ing directories, links and regular files.""" |
|---|
| 368 |
|
|---|
| 369 |
def __init__(self, host): |
|---|
| 370 |
self._host = host |
|---|
| 371 |
self._path = host.path |
|---|
| 372 |
|
|---|
| 373 |
self._parser = UnixParser() |
|---|
| 374 |
|
|---|
| 375 |
|
|---|
| 376 |
self._allow_parser_switching = True |
|---|
| 377 |
|
|---|
| 378 |
self._lstat_cache = ftp_stat_cache.StatCache() |
|---|
| 379 |
|
|---|
| 380 |
def _host_dir(self, path): |
|---|
| 381 |
""" |
|---|
| 382 |
Return a list of lines, as fetched by FTP's `DIR` command, |
|---|
| 383 |
when applied to `path`. |
|---|
| 384 |
""" |
|---|
| 385 |
return self._host._dir(path) |
|---|
| 386 |
|
|---|
| 387 |
def _real_listdir(self, path): |
|---|
| 388 |
""" |
|---|
| 389 |
Return a list of directories, files etc. in the directory |
|---|
| 390 |
named `path`. |
|---|
| 391 |
|
|---|
| 392 |
If the directory listing from the server can't be parsed |
|---|
| 393 |
raise a `ParserError`. |
|---|
| 394 |
""" |
|---|
| 395 |
|
|---|
| 396 |
path = self._path.abspath(path) |
|---|
| 397 |
if not self._path.isdir(path): |
|---|
| 398 |
|
|---|
| 399 |
|
|---|
| 400 |
|
|---|
| 401 |
raise ftp_error.PermanentError( |
|---|
| 402 |
"550 %s: no such directory or wrong directory parser used" % |
|---|
| 403 |
path) |
|---|
| 404 |
|
|---|
| 405 |
lines = self._host_dir(path) |
|---|
| 406 |
|
|---|
| 407 |
if lines == ['']: |
|---|
| 408 |
return [] |
|---|
| 409 |
names = [] |
|---|
| 410 |
for line in lines: |
|---|
| 411 |
if self._parser.ignores_line(line): |
|---|
| 412 |
continue |
|---|
| 413 |
|
|---|
| 414 |
|
|---|
| 415 |
|
|---|
| 416 |
stat_result = self._parser.parse_line(line, |
|---|
| 417 |
self._host.time_shift()) |
|---|
| 418 |
loop_path = self._path.join(path, stat_result._st_name) |
|---|
| 419 |
self._lstat_cache[loop_path] = stat_result |
|---|
| 420 |
st_name = stat_result._st_name |
|---|
| 421 |
if st_name not in (self._host.curdir, self._host.pardir): |
|---|
| 422 |
names.append(st_name) |
|---|
| 423 |
return names |
|---|
| 424 |
|
|---|
| 425 |
def _real_lstat(self, path, _exception_for_missing_path=True): |
|---|
| 426 |
""" |
|---|
| 427 |
Return an object similar to that returned by `os.lstat`. |
|---|
| 428 |
|
|---|
| 429 |
If the directory listing from the server can't be parsed, |
|---|
| 430 |
raise a `ParserError`. If the directory can be parsed and the |
|---|
| 431 |
`path` is not found, raise a `PermanentError`. That means that |
|---|
| 432 |
if the directory containing `path` can't be parsed we get a |
|---|
| 433 |
`ParserError`, independent on the presence of `path` on the |
|---|
| 434 |
server. |
|---|
| 435 |
|
|---|
| 436 |
(`_exception_for_missing_path` is an implementation aid and |
|---|
| 437 |
_not_ intended for use by ftputil clients.) |
|---|
| 438 |
""" |
|---|
| 439 |
path = self._path.abspath(path) |
|---|
| 440 |
|
|---|
| 441 |
if path in self._lstat_cache: |
|---|
| 442 |
return self._lstat_cache[path] |
|---|
| 443 |
|
|---|
| 444 |
lines = [] |
|---|
| 445 |
|
|---|
| 446 |
|
|---|
| 447 |
|
|---|
| 448 |
if path == '/': |
|---|
| 449 |
raise ftp_error.RootDirError( |
|---|
| 450 |
"can't stat remote root directory") |
|---|
| 451 |
dirname, basename = self._path.split(path) |
|---|
| 452 |
lstat_result_for_path = None |
|---|
| 453 |
|
|---|
| 454 |
|
|---|
| 455 |
|
|---|
| 456 |
|
|---|
| 457 |
lines = self._host_dir(dirname) |
|---|
| 458 |
for line in lines: |
|---|
| 459 |
if self._parser.ignores_line(line): |
|---|
| 460 |
continue |
|---|
| 461 |
stat_result = self._parser.parse_line(line, |
|---|
| 462 |
self._host.time_shift()) |
|---|
| 463 |
loop_path = self._path.join(dirname, stat_result._st_name) |
|---|
| 464 |
self._lstat_cache[loop_path] = stat_result |
|---|
| 465 |
|
|---|
| 466 |
if stat_result._st_name == basename: |
|---|
| 467 |
lstat_result_for_path = stat_result |
|---|
| 468 |
if lstat_result_for_path: |
|---|
| 469 |
return lstat_result_for_path |
|---|
| 470 |
|
|---|
| 471 |
if _exception_for_missing_path: |
|---|
| 472 |
|
|---|
| 473 |
|
|---|
| 474 |
|
|---|
| 475 |
raise ftp_error.PermanentError( |
|---|
| 476 |
"550 %s: no such file or directory" % path) |
|---|
| 477 |
else: |
|---|
| 478 |
|
|---|
| 479 |
|
|---|
| 480 |
|
|---|
| 481 |
|
|---|
| 482 |
|
|---|
| 483 |
return None |
|---|
| 484 |
|
|---|
| 485 |
def _real_stat(self, path, _exception_for_missing_path=True): |
|---|
| 486 |
""" |
|---|
| 487 |
Return info from a "stat" call on `path`. |
|---|
| 488 |
|
|---|
| 489 |
If the directory containing `path` can't be parsed, raise |
|---|
| 490 |
a `ParserError`. If the listing can be parsed but the |
|---|
| 491 |
`path` can't be found, raise a `PermanentError`. Also raise |
|---|
| 492 |
a `PermanentError` if there's an endless (cyclic) chain of |
|---|
| 493 |
symbolic links "behind" the `path`. |
|---|
| 494 |
|
|---|
| 495 |
(`_exception_for_missing_path` is an implementation aid and |
|---|
| 496 |
_not_ intended for use by ftputil clients.) |
|---|
| 497 |
""" |
|---|
| 498 |
|
|---|
| 499 |
original_path = path |
|---|
| 500 |
|
|---|
| 501 |
|
|---|
| 502 |
visited_paths = {} |
|---|
| 503 |
while True: |
|---|
| 504 |
|
|---|
| 505 |
lstat_result = self._real_lstat(path, _exception_for_missing_path) |
|---|
| 506 |
if lstat_result is None: |
|---|
| 507 |
return None |
|---|
| 508 |
|
|---|
| 509 |
|
|---|
| 510 |
if not stat.S_ISLNK(lstat_result.st_mode): |
|---|
| 511 |
return lstat_result |
|---|
| 512 |
|
|---|
| 513 |
|
|---|
| 514 |
|
|---|
| 515 |
|
|---|
| 516 |
dirname, basename = self._path.split(path) |
|---|
| 517 |
path = self._path.join(dirname, lstat_result._st_target) |
|---|
| 518 |
path = self._path.normpath(path) |
|---|
| 519 |
|
|---|
| 520 |
if path in visited_paths: |
|---|
| 521 |
|
|---|
| 522 |
raise ftp_error.PermanentError( |
|---|
| 523 |
"recursive link structure detected for remote path '%s'" % |
|---|
| 524 |
original_path) |
|---|
| 525 |
|
|---|
| 526 |
visited_paths[path] = True |
|---|
| 527 |
|
|---|
| 528 |
def __call_with_parser_retry(self, method, *args, **kwargs): |
|---|
| 529 |
""" |
|---|
| 530 |
Call `method` with the `args` and `kwargs` once. If that |
|---|
| 531 |
results in a `ParserError` and only one parser has been |
|---|
| 532 |
used yet, try the other parser. If that still fails, |
|---|
| 533 |
propagate the `ParserError`. |
|---|
| 534 |
""" |
|---|
| 535 |
|
|---|
| 536 |
|
|---|
| 537 |
|
|---|
| 538 |
|
|---|
| 539 |
try: |
|---|
| 540 |
result = method(*args, **kwargs) |
|---|
| 541 |
|
|---|
| 542 |
|
|---|
| 543 |
if (method is not self._real_listdir) and result: |
|---|
| 544 |
self._allow_parser_switching = False |
|---|
| 545 |
return result |
|---|
| 546 |
except ftp_error.ParserError: |
|---|
| 547 |
if self._allow_parser_switching: |
|---|
| 548 |
self._allow_parser_switching = False |
|---|
| 549 |
self._parser = MSParser() |
|---|
| 550 |
return method(*args, **kwargs) |
|---|
| 551 |
else: |
|---|
| 552 |
raise |
|---|
| 553 |
|
|---|
| 554 |
def listdir(self, path): |
|---|
| 555 |
""" |
|---|
| 556 |
Return a list of items in `path`. |
|---|
| 557 |
|
|---|
| 558 |
Raise a `PermanentError` if the path doesn't exist, but |
|---|
| 559 |
maybe raise other exceptions depending on the state of |
|---|
| 560 |
the server (e. g. timeout). |
|---|
| 561 |
""" |
|---|
| 562 |
return self.__call_with_parser_retry(self._real_listdir, path) |
|---|
| 563 |
|
|---|
| 564 |
def lstat(self, path, _exception_for_missing_path=True): |
|---|
| 565 |
""" |
|---|
| 566 |
Return a `StatResult` without following links. |
|---|
| 567 |
|
|---|
| 568 |
Raise a `PermanentError` if the path doesn't exist, but |
|---|
| 569 |
maybe raise other exceptions depending on the state of |
|---|
| 570 |
the server (e. g. timeout). |
|---|
| 571 |
""" |
|---|
| 572 |
return self.__call_with_parser_retry(self._real_lstat, path, |
|---|
| 573 |
_exception_for_missing_path) |
|---|
| 574 |
|
|---|
| 575 |
def stat(self, path, _exception_for_missing_path=True): |
|---|
| 576 |
""" |
|---|
| 577 |
Return a `StatResult` with following links. |
|---|
| 578 |
|
|---|
| 579 |
Raise a `PermanentError` if the path doesn't exist, but |
|---|
| 580 |
maybe raise other exceptions depending on the state of |
|---|
| 581 |
the server (e. g. timeout). |
|---|
| 582 |
""" |
|---|
| 583 |
return self.__call_with_parser_retry(self._real_stat, path, |
|---|
| 584 |
_exception_for_missing_path) |
|---|
| 585 |
|
|---|