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

Node.js上传前端选中文件至FTP服务器路径及ftp.mv不生效问题修复

核心问题根因

你首先搞混了两个完全独立的文件路径上下文,之前硬编码路径能跑只是本地调试的特殊情况:

  • 浏览器端的文件选择控件出于安全沙箱限制,永远不可能获取到用户设备上的文件真实绝对路径,你拿到的File对象是浏览器封装的沙箱文件引用,只能通过FormData的格式通过HTTP请求传到后端,不存在直接从前端读本地路径传给FTP的可能性。
  • 本地调试时你硬编码的文件路径,刚好是和你前端、后端、FTP服务同处一台机器的本地路径,后端能直接读到所以上传成功,这个逻辑在多用户场景下完全不成立——后端和FTP服务器根本访问不到用户个人设备上的硬盘路径。
  • 你用express-fileupload拿到的file.tempFilePath是后端服务所在机器上的临时文件路径,这个路径传给ftp.upload是完全合法的,之前mv无响应不是路径的问题,是异步流程和参数传错导致的。
现有实现的3个核心遗漏
  • 异步逻辑未对齐:easy-ftp所有IO方法都是异步非阻塞的,你如果在调用ftp.upload()后立刻同步执行ftp.mv(),此时文件还没上传到FTP服务器,mv找不到源文件自然不会有任何响应,且你没绑定error事件监听,所有报错都被静默吞掉了。
  • express-fileupload配置缺失:如果初始化中间件时没有开启useTempFiles: true,上传的文件会存在内存里,file.tempFilePath属性值为undefined,传给ftp.upload的路径无效,自然不会执行后续流程。
  • ftp.mv参数传参错误:该方法的两个路径参数都是FTP服务器侧的路径,不是你本地后端的临时文件路径,如果你把本地tempFilePath传给mv作为源路径,FTP服务器上找不到对应文件,方法会一直挂起无响应。
修正实现代码

后端服务配置(Express)

首先修正express-fileupload初始化配置,确保临时文件正常生成:

const express = require('express');
const fileUpload = require('express-fileupload');
const EasyFtp = require('easy-ftp');
const fs = require('fs');
const app = express();

// 必须开启临时文件存储,否则tempFilePath无效
app.use(fileUpload({
  useTempFiles: true,
  tempFileDir: './tmp/', // 自定义临时目录,提前在项目根目录创建tmp文件夹
  limits: { fileSize: 100 * 1024 * 1024 } // 限制单文件100M,按需调整
}));

// 替换成你自己的FTP服务配置
const FTP_CONFIG = {
  host: '127.0.0.1',
  port: 21,
  username: 'ftp-test',
  password: 'ftp-test-123'
};

然后写上传接口,严格按异步顺序执行操作:

app.post('/api/upload-to-ftp', async (req, res) => {
  const ftp = new EasyFtp();
  try {
    // 校验文件是否存在
    if (!req.files?.uploadFile) {
      return res.status(400).json({ code: 400, msg: '未接收到上传文件' });
    }
    const file = req.files.uploadFile;
    const localTempPath = file.tempFilePath;
    // 先上传到FTP临时路径,避免重名文件冲突
    const ftpTempPath = `/test/tmp_${Date.now()}_${file.name}`;
    const ftpFinalPath = `/test/${file.name}`;

    // 等待FTP连接建立
    await new Promise((resolve, reject) => {
      ftp.connect(FTP_CONFIG);
      ftp.on('ready', resolve);
      ftp.on('error', reject);
    });

    // 等待文件从后端临时目录上传到FTP
    await new Promise((resolve, reject) => {
      ftp.upload(localTempPath, ftpTempPath, (err) => {
        err ? reject(err) : resolve();
      });
    });

    // 上传完成后再执行FTP侧的重命名操作,两个参数都是FTP路径
    await new Promise((resolve, reject) => {
      ftp.mv(ftpTempPath, ftpFinalPath, (err) => {
        err ? reject(err) : resolve();
      });
    });

    // 收尾:关闭FTP连接、删除本地临时文件
    ftp.close();
    fs.existsSync(localTempPath) && fs.unlinkSync(localTempPath);
    return res.json({ code: 200, msg: '上传成功', ftpPath: ftpFinalPath });
  } catch (err) {
    // 异常场景也要做资源清理
    ftp.close();
    if (req.files?.uploadFile?.tempFilePath) {
      fs.existsSync(req.files.uploadFile.tempFilePath) && fs.unlinkSync(req.files.uploadFile.tempFilePath);
    }
    console.error('FTP上传失败:', err);
    return res.status(500).json({ code: 500, msg: '上传失败', error: err.message });
  }
});

app.listen(3001, () => {
  console.log('后端服务启动在 http://localhost:3001');
});

前端React组件

不需要尝试获取文件本地路径,直接把选中的File对象塞入FormData传给后端即可:

import { useState } from 'react';
import axios from 'axios';

export default function FtpUpload() {
  const [currentFile, setCurrentFile] = useState(null);
  const [uploading, setUploading] = useState(false);

  const handleSelect = (e) => {
    setCurrentFile(e.target.files[0] || null);
  };

  const handleUpload = async () => {
    if (!currentFile || uploading) return;
    setUploading(true);
    const formData = new FormData();
    formData.append('uploadFile', currentFile);
    try {
      const res = await axios.post('http://localhost:3001/api/upload-to-ftp', formData, {
        headers: { 'Content-Type': 'multipart/form-data' }
      });
      alert(`上传成功,文件在FTP路径:${res.data.ftpPath}`);
      setCurrentFile(null);
    } catch (err) {
      alert(`上传失败:${err.response?.data?.msg || err.message}`);
    } finally {
      setUploading(false);
    }
  };

  return (
    <div style={{ padding: 24 }}>
      <input type="file" onChange={handleSelect} disabled={uploading} />
      <button 
        onClick={handleUpload} 
        disabled={!currentFile || uploading}
        style={{ marginLeft: 12 }}
      >
        {uploading ? '上传中...' : '上传到FTP'}
      </button>
      {currentFile && <p style={{ marginTop: 12 }}>已选择文件:{currentFile.name}</p>}
    </div>
  );
}

注意:不要尝试在前端直接发起FTP请求上传文件,所有FTP操作必须放在后端执行,避免FTP账号密码泄露,同时规避浏览器跨域、协议不支持的问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 00:48:29