【Python】【FTP双向备份】完整版

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
****************************************

你可能感兴趣的:(python,网络,服务器)