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

使用FTPSClient上传FTP文件时遭遇CopyStreamException异常

FTP文件上传异常排查:文件创建但内容为空

背景

使用lftp通过以下固定配置访问FTP服务器:

# FTPS_OPTIONS:
set ssl:verify-certificate/testhostname no;
set ftp:ssl-protect-data yes;
set ftp:passive-mode on;

Java应用使用apache-commons-net库实现文件上传,代码如下:

@Autowired
public FtpService() {
  ftpsClient = new FTPSClient();
  ftpsClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true));
}

public void uploadFile(String ftpHost, File tempFile, String destination, String filename)
throws UploadException {

  ftpsClient.connect(ftpHost, 990);
  ftpsClient.execPBSZ(0);
  ftpsClient.execPROT("P");
  ftpsClient.enterLocalPassiveMode();
  ftpsClient.setKeepAlive(true);
  ftpsClient.setControlKeepAliveTimeout(3000);

  if(ftpsClient.login("user", "password")) {
    try (InputStream fileStream = new FileInputStream(tempFile)) {

      if (!ftpsClient.changeWorkingDirectory(destination)) {
        throwUploadException("Destination directory not available in FTP server");
      }

      boolean saved = ftpsClient.storeFile(filename, fileStream);
      // 因上一行抛出异常,以下代码未执行
      if (!saved) {
        throwUploadException("Unable to save file in FTP server");
      }
      log.info("Saved FTP file: {}/{}", destination, filename);
    }
    catch (UploadException | IOException e)
    {
      throwUploadException(e.getMessage());
    }
    finally
    {
      ftpsClient.disconnect();
      if (!tempFile.delete()) {
        log.warn("Unable to delete '{}' file", tempFile.getAbsolutePath());
      }
    }
  }
}

问题现象

改用FTPSClient后:

  • ✅ 可正常切换工作目录、创建目录
  • ❌ 调用storeFile时抛出异常,服务器上创建空文件:
org.apache.commons.net.io.CopyStreamException: IOException caught while copying.
Cause: javax.net.ssl.SSLProtocolException: Software caused connection abort: socket write error
  • ❌ 调用listFiles()/listDirectories()返回空列表(用户已具备全权限)

FTP交互日志:

er: testhostname:990
USER *******
331 Usuario testuser OK. Clave requerida ( = User testuser OK. Password required)
PASS *******
230 OK. El directorio restringido actual es / ( = OK. The current restricted directory is /)
CWD /test/upload
250 OK. El directorio actual es /test/upload ( = Ok. The current directory is /test/upload)
PASV
227 Entering Passive Mode (<numbers...>)
[Replacing PASV mode reply address <ip_address> with testhostname]
STOR dummyfile.txt
150 Conexi├│n de datos aceptada ( = Data connection accepted)

排查与解决方案

1. 禁用SSL证书验证(匹配lftp配置)

lftp配置了跳过证书验证,但Java默认会强制验证服务器证书,这会导致数据通道SSL握手失败。需添加自定义信任管理器跳过验证:

// 在初始化FTPSClient时添加
private void disableCertificateValidation() throws NoSuchAlgorithmException, KeyManagementException {
    TrustManager[] trustAllCerts = new TrustManager[]{
        new X509TrustManager() {
            public X509Certificate[] getAcceptedIssuers() { return null; }
            public void checkClientTrusted(X509Certificate[] certs, String authType) {}
            public void checkServerTrusted(X509Certificate[] certs, String authType) {}
        }
    };
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(null, trustAllCerts, new SecureRandom());
    ftpsClient.setSSLContext(sslContext);
    ftpsClient.setTrustManager(trustAllCerts[0]);
}

修改构造方法:

@Autowired
public FtpService() throws NoSuchAlgorithmException, KeyManagementException {
  ftpsClient = new FTPSClient();
  disableCertificateValidation(); // 添加这行
  ftpsClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true));
}

2. 调整FTPS命令执行顺序

execPBSZ和execPROT需要在登录后执行,而非连接后。因为这两个命令需要在已认证的会话中发送:

public void uploadFile(String ftpHost, File tempFile, String destination, String filename)
throws UploadException {

  ftpsClient.connect(ftpHost, 990);
  ftpsClient.enterLocalPassiveMode();
  ftpsClient.setKeepAlive(true);
  ftpsClient.setControlKeepAliveTimeout(3000);

  if(ftpsClient.login("user", "password")) {
    // 移到登录后执行
    ftpsClient.execPBSZ(0);
    ftpsClient.execPROT("P");
    
    try (InputStream fileStream = new FileInputStream(tempFile)) {
      // 后续代码不变...
    }
  }
}

3. 修复被动模式地址解析问题

日志中显示替换PASV地址,可能导致数据连接路由异常。禁用远程地址验证:

// 在enterLocalPassiveMode()后添加
ftpsClient.setRemoteVerificationEnabled(false);

4. 确保数据通道加密生效

execPROT("P")表示数据通道使用私有加密模式,也可尝试使用库提供的封装方法替代手动执行命令:

// 替换execPROT("P")
ftpsClient.setDataChannelProtection(FTPSClient.PROTECTION_PRIVATE);

验证修改后的完整代码片段

@Autowired
public FtpService() throws NoSuchAlgorithmException, KeyManagementException {
  ftpsClient = new FTPSClient();
  disableCertificateValidation();
  ftpsClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true));
}

private void disableCertificateValidation() throws NoSuchAlgorithmException, KeyManagementException {
    TrustManager[] trustAllCerts = new TrustManager[]{
        new X509TrustManager() {
            public X509Certificate[] getAcceptedIssuers() { return null; }
            public void checkClientTrusted(X509Certificate[] certs, String authType) {}
            public void checkServerTrusted(X509Certificate[] certs, String authType) {}
        }
    };
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(null, trustAllCerts, new SecureRandom());
    ftpsClient.setSSLContext(sslContext);
    ftpsClient.setTrustManager(trustAllCerts[0]);
}

public void uploadFile(String ftpHost, File tempFile, String destination, String filename)
throws UploadException {
  try {
    ftpsClient.connect(ftpHost, 990);
    ftpsClient.enterLocalPassiveMode();
    ftpsClient.setRemoteVerificationEnabled(false);
    ftpsClient.setKeepAlive(true);
    ftpsClient.setControlKeepAliveTimeout(3000);

    if(ftpsClient.login("user", "password")) {
      ftpsClient.execPBSZ(0);
      ftpsClient.setDataChannelProtection(FTPSClient.PROTECTION_PRIVATE);
      
      try (InputStream fileStream = new FileInputStream(tempFile)) {
        if (!ftpsClient.changeWorkingDirectory(destination)) {
          throwUploadException("Destination directory not available in FTP server");
        }

        boolean saved = ftpsClient.storeFile(filename, fileStream);
        if (!saved) {
          throwUploadException("Unable to save file in FTP server");
        }
        log.info("Saved FTP file: {}/{}", destination, filename);
      }
    } else {
      throwUploadException("FTP login failed");
    }
  } catch (UploadException | IOException | NoSuchAlgorithmException | KeyManagementException e) {
    throwUploadException(e.getMessage());
  } finally {
    try {
      if (ftpsClient.isConnected()) {
        ftpsClient.logout();
        ftpsClient.disconnect();
      }
    } catch (IOException e) {
      log.error("Failed to disconnect FTP client", e);
    }
    if (!tempFile.delete()) {
      log.warn("Unable to delete '{}' file", tempFile.getAbsolutePath());
    }
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 13:03:25