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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 17:25:04