Spring+Angular生产环境cPanel FTP上传图片损坏报404错误
问题根因
代码存在6个核心问题,直接导致文件损坏、访问404:
- FTPClient被定义为
static全局共享实例,Spring接口默认多线程并发处理请求,多个上传任务同时复用同一个FTP连接会出现流写入交叉、连接状态错乱,直接导致上传的文件缺字节损坏 SERVER_DOMAIN配置值前带多余空格,连接FTP时可能出现解析异常,且代码全程未校验ftp.storeFile()的返回结果,即使实际上传失败也会打印“上传成功”日志,误判上传状态- 上传时传入了FTP服务器绝对路径
/home/myproject/public_html/tmp/,cPanel创建的FTP账号默认被锁定在用户家目录(即登录后根目录就是/home/myproject/),传入全路径会导致实际存储路径重复拼接为/home/myproject/home/myproject/public_html/tmp/,你访问的公开路径下根本没有对应文件,自然返回404;cPanel手动上传时会自动处理路径映射,所以不会出现该问题 - 资源关闭顺序错误:代码先执行FTP登出、断开连接,再关闭本地输入流,会导致文件字节还没完全写入远端服务器就被强制断开,造成文件不完整损坏
- 传输模式参数使用错误:
FTP.BINARY_FILE_TYPE是文件类型配置参数,不是传输模式参数,虽然大部分场景下不影响结果,但属于错误写法 - 本地临时文件路径写死为服务器绝对路径,如果Spring服务和cPanel服务未部署在同一台机器,本地临时文件写入会失败,后续上传空流也会生成损坏文件
修复步骤
- 移除FTPClient的
static修饰,每次上传请求新建独立FTPClient实例,避免多线程并发冲突 - 清理
SERVER_DOMAIN配置的多余空格,所有FTP操作必须校验返回值,失败直接抛出异常终止流程,不要静默往下执行 - 上传时不要使用全路径,登录FTP后先切换到相对路径
public_html/tmp/,上传时直接传入文件名即可,避免路径重复拼接 - 调整资源关闭顺序,使用try-with-resources语法自动管理流关闭,确认文件传输完成、流关闭后再执行FTP登出、断开连接操作
- 修正传输模式配置,二进制文件类型设置必须在登录后、上传前执行,避免图片字节被ASCII转码损坏
- 本地临时文件改用系统临时目录存储,不要写死绝对路径,适配不同部署环境
修复后参考代码
import org.apache.commons.net.ftp.FTP; import org.apache.commons.net.ftp.FTPClient; import org.apache.commons.net.ftp.FTPReply; import org.springframework.web.multipart.MultipartFile; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; public class FTPService { private static final String SERVER_DOMAIN = "XX.XXX.XX.XX"; // 移除域名前的多余空格 private static final String SERVER_USERNAME = "username1"; private static final String SERVER_PASSWORD = "myPassword"; // cPanel FTP登录后的相对目标路径,不要写全路径 private static final String REMOTE_TMP_PATH = "public_html/tmp/"; private static final String FILE_ACCESS_PREFIX = "https://你的项目域名/tmp/"; public static String uFileUpload(MultipartFile file, String TypePhoto, Long clientId) throws IOException { if (file.isEmpty()) { System.out.println("Empty File"); return "Empty File"; } // 创建本地临时文件,使用系统临时目录避免路径兼容问题 Path localTempPath = Files.createTempFile("ftp_upload_", "_" + file.getOriginalFilename()); try { // 写入上传文件到本地临时目录 Files.write(localTempPath, file.getBytes()); System.out.println("Local temp file saved: " + localTempPath); // 每次上传新建独立FTPClient实例 FTPClient ftp = new FTPClient(); ftp.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out))); // 建立FTP连接 ftp.connect(SERVER_DOMAIN); int reply = ftp.getReplyCode(); if (!FTPReply.isPositiveCompletion(reply)) { ftp.disconnect(); throw new IOException("FTP server connect failed, reply code: " + reply); } // 登录校验 if (!ftp.login(SERVER_USERNAME, SERVER_PASSWORD)) { ftp.disconnect(); throw new IOException("FTP login failed, check username and password"); } // 配置传输参数 ftp.enterLocalPassiveMode(); ftp.setFileType(FTP.BINARY_FILE_TYPE); ftp.setFileTransferMode(FTP.STREAM_TRANSFER_MODE); // 切换到目标目录,不存在则自动创建 ftp.makeDirectory(REMOTE_TMP_PATH); if (!ftp.changeWorkingDirectory(REMOTE_TMP_PATH)) { throw new IOException("Change remote working directory failed, path: " + REMOTE_TMP_PATH); } // 自动管理输入流关闭 try (InputStream input = new FileInputStream(localTempPath.toFile())) { // 上传文件,直接传文件名即可,不需要拼接全路径 boolean uploadSuccess = ftp.storeFile(file.getOriginalFilename(), input); if (!uploadSuccess) { throw new IOException("File upload failed, FTP reply: " + ftp.getReplyString()); } ftp.logout(); System.out.println("File uploaded successfully"); } finally { // 确保连接断开 if (ftp.isConnected()) { ftp.disconnect(); } } // 返回可直接访问的文件URL return FILE_ACCESS_PREFIX + file.getOriginalFilename(); } finally { // 清理本地临时文件 Files.deleteIfExists(localTempPath); System.out.println("Local temp file deleted"); } } }
上线后验证点
- 上传完成后先通过cPanel文件管理器查看tmp目录下的文件大小,和本地原文件大小一致才说明传输完整
- 检查tmp目录权限,确保web服务进程有读权限,避免权限问题导致的404/403错误
- 上传文件名尽量不要包含中文、特殊空格、特殊符号,避免URL编码问题导致访问404
内容的提问来源于stack exchange,提问作者gassen
相关产品推荐
相关产品推荐

