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
相关产品推荐
相关产品推荐

