import ftplib
import mimetypes
import os
import sys
class FtpTools:
"""
远程ftp交互辅助类
"""
def __init__(self, ip, username, password, local_dir, remote_dir, is_clean: bool = True):
"""
初始化
:param ip: 远程ftp服务器 - 地址
:param username: 远程ftp服务器 - 用户名
:param password: 远程ftp服务器 - 密码
:param local_dir: 本地目录
:param remote_dir: 远程目录
:param is_clean: 是否删除备份目录下的文件,默认先删除再备份
"""
self.ip = ip
self.username = username
self.password = password
self.local_dir = local_dir
self.remote_dir = remote_dir
self.is_clean = is_clean
self.file_count = 0
self.folder_count = 0
self.connection = None
self.non_passive = False
self.print_info()
def print_info(self):
msg = f"| 地 址:ftp://{self.ip}:21\n" \
f"| 用 户:{self.username}\n" \
f"| 密 码:{self.password}\n" \
f"| 本地目录:{self.local_dir}\n" \
f"| 远程目录:{self.remote_dir}\n" \
f"| 是否清空:{self.is_clean}\n" \
f"| 连接方式:{'被动' if self.non_passive else '主动'}"
print('*' * 40 + '\n' + msg + '\n' + '*' * 40)
@staticmethod
def is_text_kind(rname, trace=True):
"""
判断文件是text文件还是二进制文件
对于未知类型文件,结果可能是(None,None)类型
:param rname: 远程ftp服务器文件名
:param trace: 是否输出提示信息
"""
mimetypes.add_type('text/x-python-win', '.pyw')
# strict=False 接受额外类型
mimetype, encoding = mimetypes.guess_type(rname, strict=False)
mimetype = mimetype or '?/?'
maintype = mimetype.split('/')[0]
if trace:
print(maintype, encoding or '')
return maintype == 'text' and encoding is None
def connect_ftp(self):
"""
根据配置信息连接远程ftp服务器
"""
print('连接中 ...')
connection = ftplib.FTP(self.ip)
connection.login(self.username, self.password)
connection.cwd(self.remote_dir)
connection.encoding = 'gbk'
if self.non_passive:
connection.set_pasv(False)
self.connection = connection
def clean_locals(self):
"""
根据配置文件尝试清空本地文件夹
"""
if self.is_clean:
for root, sdir, files in os.walk(self.local_dir, topdown=False, followlinks=False):
for directory in sdir:
path = os.path.join(root, directory)
print(f'正在删除本地目录 {path}')
try:
os.rmdir(path)
except:
print(f'无法删除本地目录 {path}')
for file in files:
path = os.path.join(root, file)
print(f'正在删除本地文件 {path}')
try:
os.remove(path)
except:
print(f'无法删除本地文件 {path}')
def clean_remotes(self):
"""
根据配置文件尝试清空远程ftp服务器文件夹
"""
if self.is_clean:
lines = []
self.connection.dir(lines.append)
for line in lines:
parsed = line.split()
permiss, fname = parsed[0], parsed[-1]
if fname in ('.', '..'):
continue
elif permiss[0] != 'd':
print(f'正在删除文件 {fname}')
self.connection.delete(fname)
self.file_count += 1
else:
print(f'正在删除目录 {fname}')
self.connection.cwd(fname)
self.clean_remotes()
self.connection.cwd('..')
self.connection.rmd(fname)
self.folder_count += 1
def download_one(self, rname, local_path):
"""
通过ftp下载单个文件
:param rname: 远程ftp文件名
:param local_path: 本地路径
"""
if self.is_text_kind(rname):
local_file = open(local_path, 'w', encoding=self.connection.encoding)
def callback(line):
local_file.write(line + '\n')
self.connection.retrlines('retr ' + rname, callback)
else:
local_file = open(local_path, 'wb')
self.connection.retrbinary('retr ' + rname, local_file.write)
local_file.close()
def upload_one(self, local_name, local_path, rname):
"""
通过ftp上传单个文件
:param local_name: 本地文件名
:param local_path: 本地路径
:param rname: 远程ftp文件名
"""
if self.is_text_kind(local_name):
local_file = open(local_path, 'rb')
self.connection.storlines('stor ' + rname, local_file)
else:
local_file = open(local_path, 'rb')
self.connection.storbinary('stor ' + rname, local_file)
def download_dir(self):
"""
从远程ftp服务器指定目录下载所有文件
"""
lines = []
self.connection.dir(lines.append)
for line in lines:
parsed = line.split()
permiss, fname = parsed[0], parsed[-1]
if fname in ('.', '..'):
continue
elif permiss[0] != 'd':
print(f'正在下载文件 {fname}', end=" ")
self.download_one(fname, os.path.join(self.local_dir, fname))
self.file_count += 1
else:
print(f'正在创建目录 {fname}')
self.local_dir = os.path.join(self.local_dir, fname)
try:
os.mkdir(self.local_dir)
except:
print(sys.exc_info()[0])
else:
self.folder_count += 1
self.connection.cwd(fname)
self.download_dir()
self.connection.cwd('..')
self.local_dir = os.path.split(self.local_dir)[0]
def callback(self):
print(f'终止 {self.local_dir} 出现一个例外')
def upload_dir(self):
"""
对于整个目录树里的每个目录上传简单的文件,递归进入子目录
"""
for root, sdirs, files in os.walk(self.local_dir, topdown=True, onerror=callable, followlinks=False):
try:
rdir = root.replace('.', self.remote_dir)
rdir, mdir = os.path.split(rdir)
print(rdir, mdir)
self.connection.cwd('~')
self.connection.cwd(rdir)
self.connection.mkd(mdir)
self.connection.cwd(mdir)
print(f'目录 {mdir} 已创建。')
except:
print(sys)
print(f'未创建目录 {mdir}')
else:
self.folder_count += 1
for name in files:
local_path = os.path.join(root, name)
print('上传 ', local_path, ' 至 ', name, end=' ')
self.upload_one(name, local_path, name)
self.file_count += 1
def run(self, clean_target=lambda: None, transfer_act=lambda: None):
self.connect_ftp()
clean_target()
transfer_act()
self.connection.quit()
msg = f'| 此次备份统计:\n' \
f'| \t文件夹数:{self.folder_count}\n' \
f'| \t文件数:{self.file_count}'
print('*' * 40 + '\n' + msg + '\n' + '*' * 40)
if __name__ == '__main__':
ip = '192.168.0.156'
username = 'ftpuser'
password = 'ftpuser_pwd'
local_dir = '/Users/Downloads/data'
remote_dir = '/data'
ftp = FtpTools(ip, username, password, local_dir, remote_dir)
# ftp.run(clean_target=ftp.clean_remotes,transfer_act=ftp.upload_dir) # 本地文件备份到FTP
ftp.run(clean_target=ftp.clean_locals, transfer_act=ftp.download_dir) # FTP文件备份到本地
运行结果
****************************************
| 地 址:ftp://192.168.0.156:21
| 用 户:ftpuser
| 密 码:ftpuser_pwd
| 本地目录:/Users/Downloads/data
| 远程目录:/data
| 是否清空:True
| 连接方式:主动
****************************************
连接中 ...
正在删除本地文件 /Users/Downloads/data/c/d.txt
正在删除本地目录 /Users/Downloads/data/c
正在删除本地文件 /Users/Downloads/data/b.txt
正在删除本地文件 /Users/Downloads/data/a.txt
正在下载文件 a.txt text
正在下载文件 b.txt text
正在创建目录 c
正在下载文件 d.txt text
正在下载文件 e ?
****************************************
| 此次备份统计:
| 文件夹数:1
| 文件数:4
****************************************