4. FTP固件下载

任务目标

实现一个基于FTP协议的固件下载客户端,支持用户名/密码认证与被动模式(PASV),将远程固件文件完整下载到本地指定路径,并校验文件大小以确保下载完整性。

环境依赖

本案例使用Python 3.8+标准库 ftplib 实现,无需安装第三方包。运行环境需具备网络访问FTP服务器的能力。

核心实现

import ftplib
import os
from typing import Tuple

class FTPFirmwareDownloader:
    def __init__(self, host: str, port: int = 21, timeout: int = 30):
        self.host = host
        self.port = port
        self.timeout = timeout
        self.ftp = None

    def connect(self, username: str, password: str) -> None:
        """建立FTP连接并完成用户认证"""
        self.ftp = ftplib.FTP()
        self.ftp.connect(self.host, self.port, self.timeout)
        self.ftp.login(username, password)
        # 强制切换到被动模式(PASV)
        self.ftp.set_pasv(True)

    def download_firmware(self, remote_path: str, local_path: str) -> Tuple[bool, str]:
        """
        下载固件文件到本地
        返回: (是否成功, 结果描述)
        """
        if not self.ftp:
            return False, "未建立FTP连接,请先调用connect()"

        try:
            # 获取远程文件大小(用于校验)
            remote_size = self.ftp.size(remote_path)
            if remote_size is None:
                return False, "无法获取远程文件大小,文件可能不存在"

            # 执行二进制下载
            with open(local_path, 'wb') as fp:
                self.ftp.retrbinary(f'RETR {remote_path}', fp.write)

            # 校验本地文件大小
            local_size = os.path.getsize(local_path)
            if local_size != remote_size:
                os.remove(local_path)  # 删除不完整文件
                return False, f"文件大小不匹配: 远程{remote_size}字节, 本地{local_size}字节"

            return True, f"下载成功: {local_path} ({local_size}字节)"

        except ftplib.all_errors as e:
            # 清理可能残留的本地文件
            if os.path.exists(local_path):
                os.remove(local_path)
            return False, f"FTP错误: {str(e)}"
        except OSError as e:
            return False, f"本地文件错误: {str(e)}"

    def close(self) -> None:
        """关闭FTP连接"""
        if self.ftp:
            self.ftp.quit()
            self.ftp = None

if __name__ == "__main__":
    # 使用示例(请替换为实际FTP服务器参数)
    FTP_HOST = "192.168.1.100"
    FTP_PORT = 21
    FTP_USER = "firmware_user"
    FTP_PASS = "secure_pass"
    REMOTE_FILE = "/firmware/esp32_app_v2.1.bin"
    LOCAL_FILE = "./esp32_app_v2.1.bin"

    downloader = FTPFirmwareDownloader(FTP_HOST, FTP_PORT)
    try:
        downloader.connect(FTP_USER, FTP_PASS)
        success, message = downloader.download_firmware(REMOTE_FILE, LOCAL_FILE)
        print(message)
        if not success:
            exit(1)
    finally:
        downloader.close()

运行与验证

运行方式:将上述代码保存为 ftp_download.py,修改 __main__ 中的FTP服务器地址、端口、用户名、密码及远程/本地文件路径后执行:

python3 ftp_download.py

验证方法:

  1. 正常路径:程序输出 下载成功: 本地路径 (N字节),且本地文件大小与FTP服务器上文件大小一致。
  2. 认证失败:使用错误密码时,程序应抛出 ftplib.error_perm 异常并输出错误信息,退出码为1。
  3. 文件不存在:远程路径错误时,输出 无法获取远程文件大小 或FTP错误信息。
  4. 被动模式确认:可在FTP服务器端日志中观察到客户端使用PASV命令建立数据连接。

关键说明

  • 代码通过 set_pasv(True) 显式启用被动模式,这是NAT/防火墙环境下最常用的FTP传输方式。
  • 下载完成后自动比对远程与本地文件大小,防止因网络中断导致固件不完整。
  • 若需支持断点续传或进度显示,可在 retrbinary 回调函数中扩展,但当前最小实现已覆盖FTP固件下载的核心闭环。