def _real_listdir(self, path): """ Return a list of directories, files etc. in the directory named `path`. If the directory listing from the server can't be parsed raise a `ParserError`. """ # We _can't_ put this check into `FTPHost._dir`; see its docstring. path = self._path.abspath(path) # `listdir` should only be allowed for directories and links to them. if not self._path.isdir(path): raise ftp_error.PermanentError( "550 %s: no such directory or wrong directory parser used" % path) # Set up for `for` loop lines = self._host_dir(path) # Exit the method now if there aren't any files if lines == ['']: return [] names = [] for line in lines: if self._parser.ignores_line(line): continue # For `listdir`, we are interested in just the names, # but we use the `time_shift` parameter to have the # correct timestamp values in the cache. stat_result = self._parser.parse_line(line, self._host.time_shift()) loop_path = self._path.join(path, stat_result._st_name) self._lstat_cache[loop_path] = stat_result st_name = stat_result._st_name if st_name not in (self._host.curdir, self._host.pardir): names.append(st_name) return names
def _real_lstat(self, path, _exception_for_missing_path=True): """ Return an object similar to that returned by `os.lstat`. If the directory listing from the server can't be parsed, raise a `ParserError`. If the directory can be parsed and the `path` is not found, raise a `PermanentError`. That means that if the directory containing `path` can't be parsed we get a `ParserError`, independent on the presence of `path` on the server. (`_exception_for_missing_path` is an implementation aid and _not_ intended for use by ftputil clients.) """ path = self._path.abspath(path) # If the path is in the cache, return the lstat result. if path in self._lstat_cache: return self._lstat_cache[path] # Note: (l)stat works by going one directory up and parsing # the output of an FTP `DIR` command. Unfortunately, it is # not possible to do this for the root directory `/`. if path == '/': raise ftp_error.RootDirError("can't stat remote root directory") dirname, basename = self._path.split(path) # If even the directory doesn't exist and we don't want the # exception, treat it the same as if the path wasn't found in # the directory's contents (compare below). The use of `isdir` # here causes a recursion but that should be ok because that # will at the latest stop when we've got to the root directory. # if not self._path.isdir(dirname) and not _exception_for_missing_path: # return None # Loop through all lines of the directory listing. We # probably won't need all lines for the particular path but # we want to collect as many stat results in the cache as # possible. lstat_result_for_path = None for stat_result in self._stat_results_from_dir(dirname): # Needed to work without cache or with disabled cache. if stat_result._st_name == basename: lstat_result_for_path = stat_result if lstat_result_for_path is not None: return lstat_result_for_path # Path was not found during the loop. if _exception_for_missing_path: #TODO Use FTP DIR command on the file to implicitly use # the usual status code of the server for missing files # (450 vs. 550). raise ftp_error.PermanentError( "550 %s: no such file or directory" % path) else: # Be explicit. Returning `None` is a signal for # `_Path.exists/isfile/isdir/islink` that the path was # not found. If we would raise an exception, there would # be no distinction between a missing path or a more # severe error in the code above. return None
def _real_lstat(self, path, _exception_for_missing_path=True): """ Return an object similar to that returned by `os.lstat`. If the directory listing from the server can't be parsed, raise a `ParserError`. If the directory can be parsed and the `path` is not found, raise a `PermanentError`. That means that if the directory containing `path` can't be parsed we get a `ParserError`, independent on the presence of `path` on the server. (`_exception_for_missing_path` is an implementation aid and _not_ intended for use by ftputil clients.) """ path = self._path.abspath(path) # if the path is in the cache, return the lstat result if path in self._lstat_cache: return self._lstat_cache[path] # get output from FTP's `DIR` command lines = [] # Note: (l)stat works by going one directory up and parsing # the output of an FTP `DIR` command. Unfortunately, it is # not possible to do this for the root directory `/`. if path == '/': raise ftp_error.RootDirError( "can't stat remote root directory") dirname, basename = self._path.split(path) lstat_result_for_path = None # loop through all lines of the directory listing; we # probably won't need all lines for the particular path but # we want to collect as many stat results in the cache as # possible lines = self._host_dir(dirname) for line in lines: if self._parser.ignores_line(line): continue stat_result = self._parser.parse_line(line, self._host.time_shift()) loop_path = self._path.join(dirname, stat_result._st_name) self._lstat_cache[loop_path] = stat_result # needed to work without cache or with disabled cache if stat_result._st_name == basename: lstat_result_for_path = stat_result if lstat_result_for_path: return lstat_result_for_path # path was not found if _exception_for_missing_path: raise ftp_error.PermanentError( "550 %s: no such file or directory" % path) else: # be explicit; returning `None` is a signal for # `_Path.exists/isfile/isdir/islink` that the path was # not found; if we would raise an exception, there would # be no distinction between a missing path or a more # severe error in the code above return None
def remove(self, path): """Remove the given file or link.""" path = self.path.abspath(path) # though `isfile` includes also links to files, `islink` # is needed to include links to directories if self.path.isfile(path) or self.path.islink(path): def command(self, path): ftp_error._try_with_oserror(self._session.delete, path) self._robust_ftp_command(command, path) else: raise ftp_error.PermanentError("remove/unlink can only delete " "files and links, not directories") self.stat_cache.invalidate(path)
def remove(self, path): """Remove the given file or link.""" path = self.path.abspath(path) # Though `isfile` includes also links to files, `islink` # is needed to include links to directories. if self.path.isfile(path) or self.path.islink(path) or \ not self.path.exists(path): # If the path doesn't exist, let the removal command trigger # an exception with a more appropriate error message. def command(self, path): """Callback function.""" ftp_error._try_with_oserror(self._session.delete, path) self._robust_ftp_command(command, path) else: raise ftp_error.PermanentError("remove/unlink can only delete " "files and links, not directories") self.stat_cache.invalidate(path)
def rmdir(self, path): """ Remove the _empty_ directory `path` on the remote host. Compatibility note: Previous versions of ftputil could possibly delete non- empty directories as well, - if the server allowed it. This is no longer supported. """ path = self.path.abspath(path) if self.listdir(path): raise ftp_error.PermanentError("directory '%s' not empty" % path) #XXX how will `rmd` work with links? def command(self, path): ftp_error._try_with_oserror(self._session.rmd, path) self._robust_ftp_command(command, path) self.stat_cache.invalidate(path)
def _real_stat(self, path, _exception_for_missing_path=True): """ Return info from a "stat" call on `path`. If the directory containing `path` can't be parsed, raise a `ParserError`. If the listing can be parsed but the `path` can't be found, raise a `PermanentError`. Also raise a `PermanentError` if there's an endless (cyclic) chain of symbolic links "behind" the `path`. (`_exception_for_missing_path` is an implementation aid and _not_ intended for use by ftputil clients.) """ # Save for error message. original_path = path # Most code in this method is used to detect recursive # link structures. visited_paths = set() while True: # Stat the link if it is one, else the file/directory. lstat_result = self._real_lstat(path, _exception_for_missing_path) if lstat_result is None: return None # If the file is not a link, the `stat` result is the # same as the `lstat` result. if not stat.S_ISLNK(lstat_result.st_mode): return lstat_result # If we stat'ed a link, calculate a normalized path for # the file the link points to. # We don't use `basename`. # pylint: disable=W0612 dirname, basename = self._path.split(path) path = self._path.join(dirname, lstat_result._st_target) path = self._path.abspath(self._path.normpath(path)) # Check for cyclic structure. if path in visited_paths: # We had seen this path already. raise ftp_error.PermanentError( "recursive link structure detected for remote path '%s'" % original_path) # Remember the path we have encountered. visited_paths.add(path)
def _real_listdir(self, path): """ Return a list of directories, files etc. in the directory named `path`. If the directory listing from the server can't be parsed raise a `ParserError`. """ # We _can't_ put this check into `FTPHost._dir`; see its docstring. path = self._path.abspath(path) # `listdir` should only be allowed for directories and links to them. if not self._path.isdir(path): raise ftp_error.PermanentError( "550 %s: no such directory or wrong directory parser used" % path) # Set up for `for` loop. names = [] for stat_result in self._stat_results_from_dir(path): st_name = stat_result._st_name names.append(st_name) return names
def rmdir(self, path): """ Remove the _empty_ directory `path` on the remote host. Compatibility note: Previous versions of ftputil could possibly delete non- empty directories as well, - if the server allowed it. This is no longer supported. """ # Fail early if we get a unicode path which can't be encoded. path = str(path) path = self.path.abspath(path) if self.listdir(path): raise ftp_error.PermanentError("directory '%s' not empty" % path) #XXX How does `rmd` work with links? def command(self, path): """Callback function.""" ftp_error._try_with_oserror(self._session.rmd, path) self._robust_ftp_command(command, path) self.stat_cache.invalidate(path)