"""FTP support""" from __future__ import absolute_import import contextlib import fnmatch import ftplib import logging import netrc import glob import os from typing import List, Text # noqa F401 # pylint: disable=unused-import from six import PY2 from six.moves import urllib from schema_salad.ref_resolver import uri_file_path from typing import Tuple, Optional from cwltool.stdfsaccess import StdFsAccess from cwltool.loghandler import _logger def abspath(src, basedir): # type: (Text, Text) -> Text """http(s):, file:, ftp:, and plain path aware absolute path""" scheme = urllib.parse.urlparse(src).scheme if scheme == u"file": apath = Text(uri_file_path(str(src))) elif scheme: return src else: if basedir.startswith(u"file://"): apath = src if os.path.isabs(src) else basedir + '/' + src else: apath = src if os.path.isabs(src) else os.path.join(basedir, src) return apath class FtpFsAccess(StdFsAccess): """FTP access with upload.""" def __init__( self, basedir, cache=None, insecure=False): # type: (Text) -> None super(FtpFsAccess, self).__init__(basedir) self.cache = cache or {} self.netrc = None self.insecure = insecure try: if 'HOME' in os.environ: if os.path.exists(os.path.join(os.environ['HOME'], '.netrc')): self.netrc = netrc.netrc( os.path.join(os.environ['HOME'], '.netrc')) elif os.path.exists(os.path.join(os.curdir, '.netrc')): self.netrc = netrc.netrc(os.path.join(os.curdir, '.netrc')) except netrc.NetrcParseError as err: _logger.debug(err) def _parse_url(self, url): # type: (Text) -> Tuple[Optional[Text], Optional[Text]] parse = urllib.parse.urlparse(url) user = parse.username passwd = parse.password host = parse.hostname path = parse.path if parse.scheme == 'ftp': if not user and self.netrc: creds = self.netrc.authenticators(host) if creds: user, _, passwd = creds if not user: user, passwd = self._recall_credentials(host) if passwd is None: passwd = "anonymous@" if user is None: user = "anonymous" return host, user, passwd, path def _connect(self, url): # type: (Text) -> Optional[ftplib.FTP] parse = urllib.parse.urlparse(url) if parse.scheme == 'ftp': host, user, passwd, _ = self._parse_url(url) if (host, user, passwd) in self.cache: if self.cache[(host, user, passwd)].pwd(): return self.cache[(host, user, passwd)] ftp = ftplib.FTP_TLS() ftp.set_debuglevel(1 if _logger.isEnabledFor(logging.DEBUG) else 0) ftp.connect(host) ftp.login(user, passwd, secure=not self.insecure) self.cache[(host, user, passwd)] = ftp return ftp return None def _abs(self, p): # type: (Text) -> Text return abspath(p, self.basedir) def _recall_credentials(self, desired_host): for host, user, passwd in self.cache: if desired_host == host: return user, passwd return None, None def glob(self, pattern): # type: (Text) -> List[Text] if not self.basedir.startswith("ftp:"): return super(FtpFsAccess, self).glob(pattern) return self._glob(pattern) def _glob0(self, basename, basepath): if basename == '': if self.isdir(basepath): return [basename] else: if self.isfile(self.join(basepath, basename)): return [basename] return [] def _glob1(self, pattern, basepath=None): try: names = self.listdir(basepath) except ftplib.all_errors: return [] if pattern[0] != '.': names = filter(lambda x: x[0] != '.', names) return fnmatch.filter(names, pattern) def _glob(self, pattern): # type: (Text) -> List[Text] if pattern.endswith("/."): pattern = pattern[:-1] dirname, basename = pattern.rsplit('/', 1) if not glob.has_magic(pattern): if basename: if self.exists(pattern): return [pattern] else: # Patterns ending in slash should match only directories if self.isdir(dirname): return [pattern] return [] if not dirname: return self._glob1(basename) dirs = self._glob(dirname) if glob.has_magic(basename): glob_in_dir = self._glob1 else: glob_in_dir = self._glob0 results = [] for dirname in dirs: results.extend(glob_in_dir(basename, dirname)) return results def open(self, fn, mode): if not fn.startswith("ftp:"): return super(FtpFsAccess, self).open(fn, mode) if 'r' in mode: host, user, passwd, path = self._parse_url(fn) handle = urllib.request.urlopen( "ftp://{}:{}@{}/{}".format(user, passwd, host, path)) if PY2: return contextlib.closing(handle) return handle raise Exception('Write mode FTP not implemented') def exists(self, fn): # type: (Text) -> bool if not self.basedir.startswith("ftp:"): return super(FtpFsAccess, self).exists(fn) return self.isfile(fn) or self.isdir(fn) def isfile(self, fn): # type: (Text) -> bool ftp = self._connect(fn) if ftp: try: self.size(fn) return True except ftplib.all_errors: return False return super(FtpFsAccess, self).isfile(fn) def isdir(self, fn): # type: (Text) -> bool ftp = self._connect(fn) if ftp: try: cwd = ftp.pwd() ftp.cwd(urllib.parse.urlparse(fn).path) ftp.cwd(cwd) return True except ftplib.all_errors: return False return super(FtpFsAccess, self).isdir(fn) def mkdir(self, url, recursive=True): """Make the directory specified in the URL.""" ftp = self._connect(url) path = urllib.parse.urlparse(url).path if not recursive: return ftp.mkd(path) dirs = [d for d in path.split('/') if d != ''] for index, _ in enumerate(dirs): try: ftp.mkd("/".join(dirs[:index+1])+'/') except ftplib.all_errors: pass return None def listdir(self, fn): # type: (Text) -> List[Text] ftp = self._connect(fn) if ftp: host, username, passwd, path = self._parse_url(fn) if username != "anonymous": template = "ftp://{un}:{pw}@{0}/{1}" else: template = "ftp://{0}/{1}" return [template.format(host, item, un=username, pw=passwd) for item in ftp.nlst(path)] return super(FtpFsAccess, self).listdir(fn) def join(self, path, *paths): # type: (Text, *Text) -> Text if path.startswith('ftp:'): result = path for extra_path in paths: if extra_path.startswith('ftp:/'): result = extra_path else: result = result + "/" + extra_path return result return super(FtpFsAccess, self).join(path, *paths) def realpath(self, path): # type: (Text) -> Text if path.startswith('ftp:'): return path return os.path.realpath(path) def size(self, fn): ftp = self._connect(fn) if ftp: host, user, passwd, path = self._parse_url(fn) try: return ftp.size(path) except ftplib.all_errors: handle = urllib.request.urlopen( "ftp://{}:{}@{}/{}".format(user, passwd, host, path)) info = handle.info() handle.close() if 'Content-length' in info: return int(info['Content-length']) return None return super(FtpFsAccess, self).size(fn) def upload(self, file_handle, url): """FtpFsAccess specific method to upload a file to the given URL.""" ftp = self._connect(url) ftp.storbinary("STOR {}".format(self._parse_url(url)[3]), file_handle)