python FTPS使用ftplib下载文件(详细)

前提摘要:
本篇文章主要是介绍如何实现FTP上传下载文件,也相当于项目经验的一个介绍吧。在我们的实际项目中文件传输其实用的是FTPS,关于FTP大家应该不陌生,那么FTPS是什么东西呢? 简单一句话概括:类似于http 和https的关系。也就是说FTPS就是加密过的FTP。
关于详细的FTP 和 FTPS的关系下面这篇文章说的更清晰一些:
https://blog.csdn.net/yumeng8525/article/details/17758553
项目介绍:
首先介绍一下,python有专门ftp相关的第三方库:ftplib,关于ftplib的信息网上有很多,但是我觉得最有用的还是看源码,或者官方文档:
https://docs.python.org/zh-cn/3/library/ftplib.html?highlight=ftplib
ftplib中有两个大类:FTP 和 FTP_TLS, 其中FTP_TLS的类,是专门用于FTPS的;该类其实也是继承了FTP类的;也就是FTP的子类,这里大家可以看下源码,我这里不做赘述了。
接下来会贴一些代码,边看边解释:
step 1:
在使用之前需要先导入FTP_TLS, 因为FTP_TLS的类是class FTP的子类,所以也拥有FTP类的所有方法。
from ftplib import FTP_TLS
step 2:
接下来就是建立ftps连接了
def open_ftps_connection(self, host=None, username=None, password=None, port=990):host = host if host is not None else self._device.active_device().ipAddressusername = username if username is not None else self._device.active_device().rbacUsernamepassword = password if password is not None else self._device.active_device().rbacPasswordself.ftps = _FTPS()self.ftps.connect(host=host, port=port)try:self.ftps.login(username, password)self.ftps.prot_p()self._access = Trueexcept Exception as err:if 'Not logged in' in str(err):self._access = Falselogger.error('Login ftp server error :{}'.format(err))self.close_ftps_connection()return self.ftps
由于连接时报错,这时候就需要修改源码了,但是直接修改源码肯定不是最明智的选择;但是我们可以重写源码中的方法,来实现我们想要的效果。我这里是重写了一个名为_FTPS的类,继承FTP_TLS类,然后在类中重写了connect 方法和 nlst 方法:
#region FTPS Operation Helper Classclass _FTPS(FTP_TLS):def __init__(self, *args):super().__init__(*args)def connect(self, host='', port=0, timeout=-999, source_address=None):'''The original connect function could not successfully connect our device, so we reconstructed this function'''if host != '':self.host = hostif port > 0:self.port = portif timeout != -999:self.timeout = timeoutif source_address is not None:self.source_address = source_addressself.sock = socket.create_connection((self.host, self.port), self.timeout, source_address=self.source_address)self.af = self.sock.familyself.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile, ssl_version=ssl.PROTOCOL_TLSv1_2)self.file = self.sock.makefile('r', encoding=self.encoding)self.welcome = self.getresp()return self.welcomedef nlst(self, *args):'''The original nlst method in ftplib does not list complete files, so the nlst method is reconstructed here'''cmd = 'LIST'templist = []func = Noneif args[-1:] and type(args[-1]) != type(''):args, func = args[:-1], args[-1]for arg in args:if arg:cmd = cmd + (' ' + arg)self.retrlines(cmd, templist.append)return templist#endregion
其中,重写connect方法就是增加了一行:ssl_version参数赋值你们项目所用的tls 版本即可,我们这里用的是tls 1.2
self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile, ssl_version=ssl.PROTOCOL_TLSv1_2)
其中,重写nlst方法是因为原有的nlst方法并不能列出所有的文件,所以稍作修改。
step 3:
基础工作完成之后就可以封装FTPS相关的函数给项目使用了。
1.是建立ftps连接的函数;很简单,没啥可介绍的,其中添加的一个self._acess类属性是为了其他函数使用的,目的是为了验证该账号是否有ftps的登录权限(因为装置根据不同角色做了权限限制)
def open_ftps_connection(self, host=None, username=None, password=None, port=990):host = host if host is not None else self._device.active_device().ipAddressusername = username if username is not None else self._device.active_device().rbacUsernamepassword = password if password is not None else self._device.active_device().rbacPasswordself.ftps = _FTPS()self.ftps.connect(host=host, port=port)try:self.ftps.login(username, password)self.ftps.prot_p()self._access = Trueexcept Exception as err:if 'Not logged in' in str(err):self._access = Falselogger.error('Login ftp server error :{}'.format(err))self.close_ftps_connection()return self.ftps
2.关闭ftps连接的函数;更简单,就是调用了ftp已有的方法(其实就是发送了一条‘QUIT’命令)。
def close_ftps_connection(self):self.ftps.quit()
3.获取文件列表的函数;由于我们项目文件层级较多,所以这个函数主要是使用递归的方法来列出某一文件夹下所有的文件,并返回每个文件的绝对路径(为什么要返回绝对路径,因为FTP下载的时候需传入文件绝对路径)。
def get_file_list_from_ftps(self, remoteFolder):self.filePath = [] # 全局变量self._get_ftps_file_path(remoteFolder) # 调用下面的私有函数return self.filePath # 返回全局变量,存放的是所有文件绝对路径def _get_ftps_file_path(self, remoteFolder):self.ftps.cwd(remoteFolder) # 进入文件夹remotePath = self.ftps.nlst() # 列出文件for path in remotePath:fullPath = os.path.join(remoteFolder, self._get_ftp_dir_name(path))if self._is_ftp_dir(path):self._get_ftps_file_path(fullPath) # 再次调用该函数,实现递归else:self.filePath.append(fullPath)def _is_ftp_dir(self, path):path = str(path)isDir = Falseif '' in path: isDir = True return isDir def _get_ftp_dir_name(self, path): path = str(path) dirName = path.split(' ')[-1] return dirName
4.开始下载文件:很简单不多做介绍,就是传入文件名称,下载到什么路径,然后调用retrbinary方法即可。
def download_file_from_ftps(self, filePath, destFilePath=None, bufsize=1024):if destFilePath is None:targetFolder = self._common.get_output_folder()filename = pathlib.PurePath(filePath).namedestFilePath = targetFolder / filenameelse:destFilePath = pathlib.Path(destFilePath)if destFilePath.exists():raise IOError('File {:s} already exists in test case file folder'.format(destFilePath.name))try:with open(destFilePath, 'wb') as f:self.ftps.retrbinary('RETR {}'.format(filePath), f.write, bufsize)except Exception as err:logger.error('Download file error :{}'.format(err))return destFilePath
5.验证用户是否有权限登录ftps服务器;因为我司项目中有多个角色的成员,admin,viewer,manager 等等,不同角色权限不同,有的角色是没有ftp权限的,所以这里写了两个函数,便于再robotframework中写case。
def validate_user_can_access_ftps(self, username, password):logger.info('Validating user can access ftps with name:{} password:{}'.format(username, password))self.open_ftps_connection(username=username, password=password)if self._access:logger.info('User is able to access ftps server.')self.close_ftps_connection()else:raise AssertionError('User is unable to access ftps server.')def validate_user_can_not_access_ftps(self, username, password):logger.info('Validating user can not access ftps with name:{} password:{}'.format(username, password))self.open_ftps_connection(username=username, password=password)if self._access:self.close_ftps_connection()raise AssertionError('User is able to access ftps server.')else:logger.info('User is unable to access ftps server.')
step 4:
至此,ftps的基础操作的函数就完成了,接下来就是再次封装这些基础函数来下载不同类型的文件,便于在robotframework中构建case。往下就牵扯到具体业务相关内容了,也就没有太大参考意义,这里就不做赘述了。
写在最后:
总结:其实关于FTPS的操作没有太复杂的东西,因为ftp的方法可以直接看源码,即可直接使用了,我这里无非是根据项目需要做了一层封装,然后便于写case。如果对大家有帮助,那就再好不过了。
哎,拖延症患者终于再2021年最后一天把几个月前做的一个小项目总结了一下。。
夜雨聆风