You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python ftplib二进制传输模式上传文件无法执行结束

问题根因
  • 挂起问题和你之前下载遇到的故障完全同源:隐式FTPS模式下,原生ftplib不会自动给数据传输通道的套接字包装SSL层。控制通道发送STOR指令收到150响应后,客户端会尝试建立明文数据连接,但隐式FTPS服务端要求所有连接(含数据传输连接)必须走SSL加密,双方握手不匹配直接卡死,不会抛出常规可捕获的异常,所以进程会一直挂住没有响应。
  • 你之前实现的下载逻辑只适配了retrbinary的传输场景,没有针对storbinary上传做兼容,也没有给上传流程加僵死连接检测、超时断开、重连逻辑,哪怕是小文件也会卡在数据连接建立阶段,和文件大小、传输速率无关。
  • 原生ftplib.FTP_TLS本身存在已知bug:调用prot_p()开启数据通道加密后,被动模式下建立数据连接时的SSL包装逻辑在隐式FTPS端口(默认990)下不会自动触发,这个bug是隐式FTPS传输挂起的核心诱因。
稳定上传实现方案

你不需要重构之前已经跑通的隐式FTPS下载逻辑,只需要在原有PyFTPclient类中补充上传方法,复用已有的SSL自动包装、速率监控、重连逻辑即可,核心调整点包括:

  • 所有数据连接强制走SSL包装,复用已有的ImplicitFTP_TLS子类
  • 上传前给套接字配置TCP保活参数,避免中间网络设备掐断空闲连接
  • 复用下载逻辑的定时监控机制,检测到传输速率为0时主动断开僵死连接触发重连
  • 支持断点续传,每次重连时先查询服务端已存文件大小,从断点位置续传
  • 配置最大重试次数,避免无限重连导致进程永久挂死
  • 传输结束后显式校验服务端返回的226 Transfer complete响应,确认传输真实完成

可直接复用的完整代码如下,你之前写好的类和方法无需修改,直接补充UploadFile方法即可:

import os
import time
import logging
import socket
import threading
import ssl
from ftplib import FTP_TLS

class ImplicitFTP_TLS(FTP_TLS):
    """FTP_TLS subclass that automatically wraps sockets in SSL to support implicit FTPS."""
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._sock = None
    @property
    def sock(self):
        """Return the socket."""
        return self._sock
    @sock.setter
    def sock(self, value):
        """When modifying the socket, ensure that it is ssl wrapped."""
        if value is not None and not isinstance(value, ssl.SSLSocket):
            value = self.context.wrap_socket(value)
        self._sock = value


def setInterval(interval, times = -1):
    def outer_wrap(function):
        def wrap(*args, **kwargs):
            stop = threading.Event()
            def inner_wrap():
                i = 0
                while i != times and not stop.isSet():
                    stop.wait(interval)
                    function(*args, **kwargs)
                    i += 1
            t = threading.Timer(0, inner_wrap)
            t.daemon = True
            t.start()
            return stop
        return wrap
    return outer_wrap

class PyFTPclient:
    def __init__(self, host, port = 990, login = 'anonymous', passwd = 'anonymous', monitor_interval = 30):
        self.host = host
        self.port = port
        self.login = login
        self.passwd = passwd
        self.monitor_interval = monitor_interval
        self.ptr = None
        self.max_attempts = 15
        self.waiting = True

    def get_files_names(self, dir_score='/'):
        ftp_client = ImplicitFTP_TLS()
        ftp_client.connect(host=self.host, port=self.port)
        ftp_client.login(user=self.login, passwd=self.passwd)
        ftp_client.prot_p()
        ftp_client.cwd(dir_score)
        files = []
        ftp_client.dir(files.append)
        ftp_client.quit()
        return files

    def DownloadFile(self, dst_filename, local_filename = None):
        res = ''
        if local_filename is None:
            local_filename = dst_filename.split('/')[-1]
        with open(local_filename, 'w+b') as f:
            self.ptr = f.tell()
            @setInterval(self.monitor_interval)
            def monitor():
                if not self.waiting:
                    i = f.tell()
                    if self.ptr < i:
                        logging.debug("%d  -  %0.1f Kb/s" % (i, (i-self.ptr)/(1024*self.monitor_interval)))
                        self.ptr = i
                    else:
                        ftp.close()
            def connect():
                ftp.connect(self.host, self.port)
                ftp.login(self.login, self.passwd)
                ftp.prot_p()
                ftp.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
                ftp.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 75)
                ftp.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60)
            ftp = ImplicitFTP_TLS()
            ftp.set_debuglevel(2)
            ftp.set_pasv(True)
            connect()
            ftp.voidcmd('TYPE I')
            dst_filesize = ftp.size(dst_filename)
            mon = monitor()
            while dst_filesize > f.tell():
                try:
                    connect()
                    self.waiting = False
                    res = ftp.retrbinary('RETR %s' % dst_filename, f.write) if f.tell() == 0 else \
                              ftp.retrbinary('RETR %s' % dst_filename, f.write, rest=f.tell())
                except:
                    self.max_attempts -= 1
                    if self.max_attempts == 0:
                        mon.set()
                        logging.exception('')
                        raise
                    self.waiting = True
                    time.sleep(30)
            mon.set()
            ftp.close()
            if not res.startswith('226 Transfer complete'):
                return None
            return 1

    # 新增的上传方法
    def UploadFile(self, local_filename, dst_filename = None):
        res = ''
        if dst_filename is None:
            dst_filename = os.path.basename(local_filename)
        local_filesize = os.path.getsize(local_filename)
        with open(local_filename, 'rb') as f:
            self.ptr = 0
            @setInterval(self.monitor_interval)
            def monitor():
                if not self.waiting:
                    i = f.tell()
                    if self.ptr < i:
                        logging.debug("%d  -  %0.1f Kb/s" % (i, (i-self.ptr)/(1024*self.monitor_interval)))
                        self.ptr = i
                    else:
                        ftp.close()
            
            def connect():
                ftp.connect(self.host, self.port)
                ftp.login(self.login, self.passwd)
                ftp.prot_p()
                ftp.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
                ftp.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 75)
                ftp.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60)

            ftp = ImplicitFTP_TLS()
            ftp.set_debuglevel(2)
            ftp.set_pasv(True)
            connect()
            ftp.voidcmd('TYPE I')
            mon = monitor()
            rest_pos = 0
            # 首次连接查询服务端已存文件大小,确定续传起点
            try:
                rest_pos = ftp.size(dst_filename)
                if rest_pos is None:
                    rest_pos = 0
            except:
                rest_pos = 0
            
            while local_filesize > rest_pos:
                try:
                    connect()
                    self.waiting = False
                    f.seek(rest_pos)
                    res = ftp.storbinary(f'STOR {dst_filename}', f, rest=rest_pos)
                    rest_pos = f.tell()
                except Exception as e:
                    self.max_attempts -= 1
                    if self.max_attempts == 0:
                        mon.set()
                        logging.exception('Upload failed after max retries')
                        raise
                    self.waiting = True
                    time.sleep(30)
                    # 重连后重新查询断点位置
                    try:
                        connect()
                        rest_pos = ftp.size(dst_filename)
                        if rest_pos is None:
                            rest_pos = 0
                    except:
                        rest_pos = 0
            mon.set()
            ftp.close()
            if not res.startswith('226 Transfer complete'):
                return None
            return 1

# 调用示例
if __name__ == '__main__':
    obj = PyFTPclient(FTP_HOST, port=FTP_PORT, login=FTP_USER, passwd=FTP_PWD)
    obj.UploadFile(local_filename=path_local_file, dst_filename=path_remote_save_path)
配置注意事项
  • 隐式FTPS默认端口是990,不要使用明文FTP的21端口,21端口是显式FTPS/明文FTP的默认端口
  • 如果网络环境较差,可以把monitor_interval参数调小到10-15秒,更快识别僵死连接
  • 上传文件必须用rb二进制只读模式打开,禁止用文本模式,否则会出现文件损坏、大小不匹配的问题
  • 如果服务端不支持断点续传,可以删掉rest相关逻辑,每次重连直接从文件头开始传输即可

内容的提问来源于stack exchange,提问作者vitormgou

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 01:51:20