def parse_unix_mode(self, mode_string): """ Return an integer from the `mode_string`, compatible with the `st_mode` value in stat results. Such a mode string may look like "drwxr-xr-x". If the mode string can't be parsed, raise an `ftp_error.ParserError`. """ st_mode = 0 if len(mode_string) != 10: raise ftp_error.ParserError("invalid mode string '%s'" % mode_string) for bit in mode_string[1:10]: bit = (bit != '-') st_mode = (st_mode << 1) + bit if mode_string[3] == 's': st_mode = st_mode | stat.S_ISUID if mode_string[6] == 's': st_mode = st_mode | stat.S_ISGID file_type_to_mode = {'d': stat.S_IFDIR, 'l': stat.S_IFLNK, 'c': stat.S_IFCHR, '-': stat.S_IFREG} file_type = mode_string[0] if file_type in file_type_to_mode: st_mode = st_mode | file_type_to_mode[file_type] else: raise ftp_error.ParserError( "unknown file type character '%s'" % file_type) return st_mode
def parse_line(self, line, time_shift=0.0): """ Return a `StatResult` instance corresponding to the given text line from a FTP server which emits "Microsoft format" (see end of file). If the line can't be parsed, raise a `ParserError`. The parameter `time_shift` isn't used in this method but is listed for compatibilty with the base class. """ try: date, time_, dir_or_size, name = line.split(None, 3) except ValueError: # "unpack list of wrong size" raise ftp_error.ParserError("line '%s' can't be parsed" % line) # st_mode # Default to read access only; in fact, we can't tell. st_mode = 0400 if dir_or_size == "<DIR>": st_mode = st_mode | stat.S_IFDIR else: st_mode = st_mode | stat.S_IFREG # st_ino, st_dev, st_nlink, st_uid, st_gid st_ino = None st_dev = None st_nlink = None st_uid = None st_gid = None # st_size if dir_or_size != "<DIR>": try: st_size = int(dir_or_size) except ValueError: raise ftp_error.ParserError("invalid size %s" % dir_or_size) else: st_size = None # st_atime st_atime = None # st_mtime st_mtime = self.parse_ms_time(date, time_, time_shift) # st_ctime st_ctime = None stat_result = StatResult( (st_mode, st_ino, st_dev, st_nlink, st_uid, st_gid, st_size, st_atime, st_mtime, st_ctime)) # _st_name and _st_target stat_result._st_name = name stat_result._st_target = None # mtime precision in seconds stat_result._st_mtime_precision = 60 return stat_result
def _split_line(self, line): """ Split a line in metadata, nlink, user, group, size, month, day, year_or_time and name and return the result as an nine-element list of these values. If the name is a link, it will be encoded as a string "link_name -> link_target". """ # This method encapsulates the recognition of an unusual # Unix format variant (see ticket # http://ftputil.sschwarzer.net/trac/ticket/12 ). line_parts = line.split() FIELD_COUNT_WITHOUT_USERID = 8 FIELD_COUNT_WITH_USERID = FIELD_COUNT_WITHOUT_USERID + 1 if len(line_parts) < FIELD_COUNT_WITHOUT_USERID: # No known Unix-style format raise ftp_error.ParserError("line '%s' can't be parsed" % line) # If we have a valid format (either with or without user id field), # the field with index 5 is either the month abbreviation or a day. try: int(line_parts[5]) except ValueError: # Month abbreviation, "invalid literal for int" line_parts = line.split(None, FIELD_COUNT_WITH_USERID - 1) else: # Day line_parts = line.split(None, FIELD_COUNT_WITHOUT_USERID - 1) USER_FIELD_INDEX = 2 line_parts.insert(USER_FIELD_INDEX, None) return line_parts
def _split_line(self, line): """ Split a line in metadata, nlink, user, group, size, month, day, year_or_time and name and return the result as an nine-element list of these values. """ # This method encapsulates the recognition of an unusual # Unix format variant (see ticket # http://ftputil.sschwarzer.net/trac/ticket/12 ) parts = line.split(None, 8) if len(parts) == 9: if parts[-1].startswith("-> "): # for the alternative format, the last part will not be # "link_name -> link_target" but "-> link_target" and the # link name will be in the previous field; # this heuristic will fail for names starting with "-> " # which should be _quite_ rare # insert `None` for the user field parts.insert(2, None) parts[-2] = "%s %s" % tuple(parts[-2:]) del parts[-1] return parts elif len(parts) == 8: # alternative unusual format, insert `None` for the user field parts.insert(2, None) return parts else: # no known Unix-style format raise ftp_error.ParserError("line '%s' can't be parsed" % line)
def parse_ms_time(self, date, time_, time_shift): """ Return a floating point number, like from `time.mktime`, by parsing the string arguments `date` and `time_`. The parameter `time_shift` is the difference "time on server" - "time on client" and is available as the `time_shift` parameter in the `parse_line` interface. Times in MS-style directory listings typically have the format "10-23-01 03:25PM" (month-day_of_month-two_digit_year, hour:minute, am/pm). If this method can not make sense of the given arguments, it raises an `ftp_error.ParserError`. """ try: month, day, year = map(int, date.split('-')) if year >= 70: year = 1900 + year else: year = 2000 + year hour, minute, am_pm = time_[0:2], time_[3:5], time_[5] hour, minute = int(hour), int(minute) except (ValueError, IndexError): raise ftp_error.ParserError("invalid time string '%s'" % time_) if am_pm == 'P': hour = hour + 12 st_mtime = time.mktime( (year, month, day, hour, minute, 0, 0, 0, -1) ) return st_mtime
def parse_unix_mode(self, mode_string): """ Return an integer from the `mode_string`, compatible with the `st_mode` value in stat results. Such a mode string may look like "drwxr-xr-x". If the mode string can't be parsed, raise an `ftp_error.ParserError`. """ if len(mode_string) != 10: raise ftp_error.ParserError("invalid mode string '%s'" % mode_string) st_mode = 0 #TODO Add support for "S" and sticky bit ("t", "T"). for bit in mode_string[1:10]: bit = (bit != '-') st_mode = (st_mode << 1) + bit if mode_string[3] == 's': st_mode = st_mode | stat.S_ISUID if mode_string[6] == 's': st_mode = st_mode | stat.S_ISGID file_type_to_mode = { 'b': stat.S_IFBLK, 'c': stat.S_IFCHR, 'd': stat.S_IFDIR, 'l': stat.S_IFLNK, 'p': stat.S_IFIFO, 's': stat.S_IFSOCK, '-': stat.S_IFREG, # Ignore types which `ls` can't make sense of # (assuming the FTP server returns listings # like `ls` does). '?': 0, } file_type = mode_string[0] if file_type in file_type_to_mode: st_mode = st_mode | file_type_to_mode[file_type] else: raise ftp_error.ParserError("unknown file type character '%s'" % file_type) return st_mode
def parse_unix_time(self, month_abbreviation, day, year_or_time, time_shift): """ Return a floating point number, like from `time.mktime`, by parsing the string arguments `month_abbreviation`, `day` and `year_or_time`. The parameter `time_shift` is the difference "time on server" - "time on client" and is available as the `time_shift` parameter in the `parse_line` interface. Times in Unix-style directory listings typically have one of these formats: - "Nov 23 02:33" (month name, day of month, time) - "May 26 2005" (month name, day of month, year) If this method can not make sense of the given arguments, it raises an `ftp_error.ParserError`. """ try: month = self._month_numbers[month_abbreviation.lower()] except KeyError: raise ftp_error.ParserError("invalid month name '%s'" % month) day = int(day) if ":" not in year_or_time: # `year_or_time` is really a year year, hour, minute = int(year_or_time), 0, 0 st_mtime = time.mktime( (year, month, day, hour, minute, 0, 0, 0, -1) ) else: # `year_or_time` is a time hh:mm hour, minute = year_or_time.split(':') year, hour, minute = None, int(hour), int(minute) # try the current year year = time.localtime()[0] st_mtime = time.mktime( (year, month, day, hour, minute, 0, 0, 0, -1) ) # rhs of comparison: transform client time to server time # (as on the lhs), so both can be compared with respect # to the set time shift (see the definition of the time # shift in `FTPHost.set_time_shift`'s docstring); the # last addend allows for small deviations between the # supposed (rounded) and the actual time shift # #XXX the downside of this "correction" is that there is # a one-minute time interval exactly one year ago that # may cause that datetime to be recognized as the current # datetime, but after all the datetime from the server # can only be exact up to a minute if st_mtime > time.time() + time_shift + 60.0: # if it's in the future, use previous year st_mtime = time.mktime( (year-1, month, day, hour, minute, 0, 0, 0, -1) ) return st_mtime
def parse_line(self, line, time_shift=0.0): """ Return a `StatResult` instance corresponding to the given text line. The `time_shift` value is needed to determine to which year a datetime without an explicit year belongs. If the line can't be parsed, raise a `ParserError`. """ mode_string, nlink, user, group, size, month, day, \ year_or_time, name = self._split_line(line) # st_mode st_mode = self.parse_unix_mode(mode_string) # st_ino, st_dev, st_nlink, st_uid, st_gid, st_size, st_atime st_ino = None st_dev = None st_nlink = int(nlink) st_uid = user st_gid = group st_size = int(size) st_atime = None # st_mtime st_mtime, st_mtime_precision = \ self.parse_unix_time(month, day, year_or_time, time_shift, with_precision=True) # st_ctime st_ctime = None # st_name if name.count(" -> ") > 1: # If we have more than one arrow we can't tell where the link # name ends and the target name starts. raise ftp_error.ParserError( 'name "%s" contains more than one "->"' % name) elif name.count(" -> ") == 1: st_name, st_target = name.split(' -> ') else: st_name, st_target = name, None stat_result = StatResult( (st_mode, st_ino, st_dev, st_nlink, st_uid, st_gid, st_size, st_atime, st_mtime, st_ctime)) stat_result._st_mtime_precision = st_mtime_precision stat_result._st_name = st_name stat_result._st_target = st_target return stat_result
def parse_ms_time(self, date, time_, time_shift): """ Return a floating point number, like from `time.mktime`, by parsing the string arguments `date` and `time_`. The parameter `time_shift` is the difference "time on server" - "time on client" and can be set as the `time_shift` parameter in the `parse_line` interface. Times in MS-style directory listings typically have the format "10-23-01 03:25PM" (month-day_of_month-two_digit_year, hour:minute, am/pm). If this method can not make sense of the given arguments, it raises an `ftp_error.ParserError`. """ # Don't complain about unused `time_shift` argument # pylint: disable=W0613 # For the time being, I don't add a `with_precision` # parameter as in the Unix parser because the precision for # the DOS format is always a minute and can be set in # `MSParser.parse_line`. Should you find yourself needing # support for `with_precision` for a derived class, please # send a mail (see ftputil.txt/html). try: month, day, year = [int(part) for part in date.split('-')] if year >= 70: year = 1900 + year else: year = 2000 + year hour, minute, am_pm = time_[0:2], time_[3:5], time_[5] hour, minute = int(hour), int(minute) except (ValueError, IndexError): raise ftp_error.ParserError("invalid time string '%s'" % time_) if hour == 12 and am_pm == 'A': hour = 0 if hour != 12 and am_pm == 'P': hour = hour + 12 st_mtime = time.mktime((year, month, day, hour, minute, 0, 0, 0, -1)) return st_mtime
def parse_unix_time(self, month_abbreviation, day, year_or_time, time_shift, with_precision=False): """ Return a floating point number, like from `time.mktime`, by parsing the string arguments `month_abbreviation`, `day` and `year_or_time`. The parameter `time_shift` is the difference "time on server" - "time on client" and is available as the `time_shift` parameter in the `parse_line` interface. If `with_precision` is true (default: false), return a two-element tuple consisting of the floating point number as described in the previous paragraph and the precision of the time in seconds. This takes into account that, for example, a time string like "May 26 2005" has only a precision of one day. This information is important for the `upload_if_newer` and `download_if_newer` methods in the `FTPHost` class. Times in Unix-style directory listings typically have one of these formats: - "Nov 23 02:33" (month name, day of month, time) - "May 26 2005" (month name, day of month, year) If this method can not make sense of the given arguments, it raises an `ftp_error.ParserError`. """ try: month = self._month_numbers[month_abbreviation.lower()] except KeyError: raise ftp_error.ParserError("invalid month name '%s'" % month) day = int(day) if ":" not in year_or_time: # `year_or_time` is really a year. year, hour, minute = int(year_or_time), 0, 0 st_mtime = time.mktime( (year, month, day, hour, minute, 0, 0, 0, -1)) # Precise up to a day. st_mtime_precision = 24 * 60 * 60 else: # `year_or_time` is a time hh:mm. hour, minute = year_or_time.split(':') year, hour, minute = None, int(hour), int(minute) # Try the current year year = time.localtime()[0] st_mtime = time.mktime( (year, month, day, hour, minute, 0, 0, 0, -1)) # Times are precise up to a minute. st_mtime_precision = 60 # Rhs of comparison: Transform client time to server time # (as on the lhs), so both can be compared with respect # to the set time shift (see the definition of the time # shift in `FTPHost.set_time_shift`'s docstring). The # last addend allows for small deviations between the # supposed (rounded) and the actual time shift. # #XXX The downside of this "correction" is that there is # a one-minute time interval exactly one year ago that # may cause that datetime to be recognized as the current # datetime, but after all the datetime from the server # can only be exact up to a minute. if st_mtime > time.time() + time_shift + st_mtime_precision: # If it's in the future, use previous year. st_mtime = time.mktime( (year - 1, month, day, hour, minute, 0, 0, 0, -1)) if with_precision: return (st_mtime, st_mtime_precision) else: return st_mtime