Node.js实现SFTP与本地文件夹同步问题:无修改仍重复上传
SFTP双向同步脚本重复上传问题:时间戳不一致导致无意义同步
我用Node.js实现SFTP远程文件夹与本地文件夹的双向同步,预期是两者始终保持一致,以较新的文件为准更新。但脚本存在问题:即使没有修改任何内容,也会重复上传文件夹和文件,怀疑是时间戳不一致导致的。以下是原代码:
const fs = require('fs'); const path = require('path'); const Client = require('ssh2-sftp-client'); const SFTP_CONFIG = { host: '123456', username: 'user', password: '1234', port: 22, remotePath: '/', localPath: './content', syncInterval: 10000, }; const sftp = new Client(); async function syncFiles() { try { await sftp.connect(SFTP_CONFIG); await syncDirectory(SFTP_CONFIG.remotePath, SFTP_CONFIG.localPath); setInterval(async () => { await syncDirectory(SFTP_CONFIG.remotePath, SFTP_CONFIG.localPath); }, SFTP_CONFIG.syncInterval); } catch (err) { console.error('Error:', err); } } async function syncDirectory(remotePath, localPath) { try { const remoteFiles = await sftp.list(remotePath); const localFiles = await getLocalFiles(localPath); // Download new or modified files from the remote server for (const remoteFile of remoteFiles) { const localFile = localFiles.find((f) => f.name === remoteFile.name); if (!localFile || remoteFile.modifyTime > localFile.modifyTime) { const remoteFilePath = path.posix.join(remotePath, remoteFile.name); const localFilePath = path.join(localPath, remoteFile.name); if (remoteFile.type === 'd') { await downloadDirectory(remoteFilePath, localFilePath); } else { await downloadFile(remoteFilePath, localFilePath); } } else if (remoteFile.modifyTime < localFile.modifyTime) { const remoteFilePath = path.posix.join(remotePath, remoteFile.name); const localFilePath = path.join(localPath, remoteFile.name); if (localFile.type === 'd') { await uploadDirectory(localFilePath, remoteFilePath); } else { await uploadFile(localFilePath, remoteFilePath); } } } // Upload new or modified files to the remote server for (const localFile of localFiles) { const remoteFile = remoteFiles.find((f) => f.name === localFile.name); if (!remoteFile || localFile.modifyTime > remoteFile.modifyTime) { const remoteFilePath = path.posix.join(remotePath, localFile.name); const localFilePath = path.join(localPath, localFile.name); if (localFile.type === 'd') { await uploadDirectory(localFilePath, remoteFilePath); } else { await uploadFile(localFilePath, remoteFilePath); } } } } catch (err) { console.error('Error syncing directory:', err); } } async function downloadFile(remotePath, localPath) { try { await sftp.fastGet(remotePath, localPath); console.log(`Downloaded ${remotePath} to ${localPath}`); } catch (err) { console.error('Error downloading file:', err); } } async function downloadDirectory(remotePath, localPath) { try { await fs.promises.mkdir(localPath, { recursive: true }); const remoteFiles = await sftp.list(remotePath); for (const remoteFile of remoteFiles) { const fileName = remoteFile.name; const remoteFilePath = path.posix.join(remotePath, fileName); const localFilePath = path.join(localPath, fileName); if (remoteFile.type === 'd') { await downloadDirectory(remoteFilePath, localFilePath); } else { await downloadFile(remoteFilePath, localFilePath); } } } catch (err) { console.error('Error downloading directory:', err); } } async function uploadFile(localPath, remotePath) { try { await sftp.fastPut(localPath, remotePath); console.log(`Uploaded ${localPath} to ${remotePath}`); } catch (err) { console.error('Error uploading file:', err); } } async function uploadDirectory(localPath, remotePath) { try { await sftp.mkdir(remotePath, true); console.log(`Uploaded ${localPath} to ${remotePath}`); const localFiles = await getLocalFiles(localPath); for (const localFile of localFiles) { const fileName = localFile.name; const localFilePath = path.join(localPath, fileName); const remoteFilePath = path.posix.join(remotePath, fileName); if (localFile.type === 'd') { await uploadDirectory(localFilePath, remoteFilePath); } else { await uploadFile(localFilePath, remoteFilePath); } } } catch (err) { console.error('Error uploading directory:', err); } } async function getLocalFiles(dirPath) { try { const files = await fs.promises.readdir(dirPath, { withFileTypes: true }); return Promise.all( files.map(async (file) => { const filePath = path.join(dirPath, file.name); const stats = await fs.promises.stat(filePath); return { name: file.name, modifyTime: stats.mtimeMs, type: file.isDirectory() ? 'd' : 'f' }; }) ); } catch (err) { if (err.code === 'ENOENT') { console.error(`Local path "${dirPath}" does not exist.`); return []; } else { console.error('Error reading local directory:', err); throw err; } } } syncFiles();
问题根源
- 时间戳精度不匹配:本地使用
mtimeMs(毫秒级时间戳),而SFTP服务器返回的modifyTime通常是秒级精度(或Date对象转换后存在精度差),导致即使文件未修改,两者数值也会存在微小差异,触发无意义的同步。 - 文件夹同步逻辑冗余:
uploadDirectory方法中,只要创建目录就会输出"Uploaded"提示,且syncDirectory里的两段循环(远程→本地、本地→远程)存在重复判断,会重复执行上传操作。 - 无容错的时间戳比较:直接用
>/<比较时间戳,没有考虑系统时间差或精度误差带来的微小差异,导致误判。
修复方案
1. 统一时间戳精度
将本地和远程的时间戳统一转换为秒级,消除精度差异:
- 修改
getLocalFiles中的modifyTime计算:return { name: file.name, modifyTime: Math.floor(stats.mtimeMs / 1000), type: file.isDirectory() ? 'd' : 'f' }; - 处理远程文件的时间戳(将
ssh2-sftp-client返回的Date对象转为秒级):
在syncDirectory获取远程文件后,先转换时间戳:const remoteFiles = (await sftp.list(remotePath)).map(file => ({ ...file, modifyTime: Math.floor(file.modifyTime.getTime() / 1000) }));
2. 优化文件夹同步逻辑
- 移除
uploadDirectory中创建目录时的"Uploaded"提示,避免误判;仅在文件上传时输出日志:async function uploadDirectory(localPath, remotePath) { try { // 仅当目录不存在时创建,避免重复输出 const exists = await sftp.exists(remotePath); if (!exists) { await sftp.mkdir(remotePath, true); console.log(`Created directory ${remotePath}`); } const localFiles = await getLocalFiles(localPath); for (const localFile of localFiles) { const fileName = localFile.name; const localFilePath = path.join(localPath, fileName); const remoteFilePath = path.posix.join(remotePath, fileName); if (localFile.type === 'd') { await uploadDirectory(localFilePath, remoteFilePath); } else { await uploadFile(localFilePath, remoteFilePath); } } } catch (err) { console.error('Error uploading directory:', err); } } - 合并
syncDirectory中的两段循环,避免重复处理同一个文件:async function syncDirectory(remotePath, localPath) { try { const remoteFiles = (await sftp.list(remotePath)).map(file => ({ ...file, modifyTime: Math.floor(file.modifyTime.getTime() / 1000) })); const localFiles = await getLocalFiles(localPath); // 合并处理:遍历所有存在的文件(远程+本地) const allFileNames = [...new Set([...remoteFiles.map(f => f.name), ...localFiles.map(f => f.name)])]; for (const fileName of allFileNames) { const remoteFile = remoteFiles.find(f => f.name === fileName); const localFile = localFiles.find(f => f.name === fileName); if (!remoteFile) { // 本地有,远程无:上传 const localFilePath = path.join(localPath, fileName); const remoteFilePath = path.posix.join(remotePath, fileName); if (localFile.type === 'd') { await uploadDirectory(localFilePath, remoteFilePath); } else { await uploadFile(localFilePath, remoteFilePath); } } else if (!localFile) { // 远程有,本地无:下载 const remoteFilePath = path.posix.join(remotePath, fileName); const localFilePath = path.join(localPath, fileName); if (remoteFile.type === 'd') { await downloadDirectory(remoteFilePath, localFilePath); } else { await downloadFile(remoteFilePath, localFilePath); } } else { // 两者都存在:比较时间戳 const timeDiff = Math.abs(remoteFile.modifyTime - localFile.modifyTime); // 允许1秒以内的误差,视为未修改 if (timeDiff > 1) { if (remoteFile.modifyTime > localFile.modifyTime) { // 远程更新:下载 const remoteFilePath = path.posix.join(remotePath, fileName); const localFilePath = path.join(localPath, fileName); if (remoteFile.type === 'd') { await downloadDirectory(remoteFilePath, localFilePath); } else { await downloadFile(remoteFilePath, localFilePath); } } else { // 本地更新:上传 const localFilePath = path.join(localPath, fileName); const remoteFilePath = path.posix.join(remotePath, fileName); if (localFile.type === 'd') { await uploadDirectory(localFilePath, remoteFilePath); } else { await uploadFile(localFilePath, remoteFilePath); } } } } } } catch (err) { console.error('Error syncing directory:', err); } }
3. 添加时间戳容错阈值
在比较时间戳时,允许1秒以内的差异(可根据需求调整),避免因系统时间微小偏差导致的误同步。
修改后的完整代码
const fs = require('fs'); const path = require('path'); const Client = require('ssh2-sftp-client'); const SFTP_CONFIG = { host: '123456', username: 'user', password: '1234', port: 22, remotePath: '/', localPath: './content', syncInterval: 10000, }; const sftp = new Client(); async function syncFiles() { try { await sftp.connect(SFTP_CONFIG); await syncDirectory(SFTP_CONFIG.remotePath, SFTP_CONFIG.localPath); setInterval(async () => { await syncDirectory(SFTP_CONFIG.remotePath, SFTP_CONFIG.localPath); }, SFTP_CONFIG.syncInterval); } catch (err) { console.error('Error:', err); } } async function syncDirectory(remotePath, localPath) { try { // 统一远程文件时间戳为秒级 const remoteFiles = (await sftp.list(remotePath)).map(file => ({ ...file, modifyTime: Math.floor(file.modifyTime.getTime() / 1000) })); const localFiles = await getLocalFiles(localPath); // 合并所有文件名,避免重复处理 const allFileNames = [...new Set([...remoteFiles.map(f => f.name), ...localFiles.map(f => f.name)])]; for (const fileName of allFileNames) { const remoteFile = remoteFiles.find(f => f.name === fileName); const localFile = localFiles.find(f => f.name === fileName); if (!remoteFile) { // 本地文件上传 const localFilePath = path.join(localPath, fileName); const remoteFilePath = path.posix.join(remotePath, fileName); localFile.type === 'd' ? await uploadDirectory(localFilePath, remoteFilePath) : await uploadFile(localFilePath, remoteFilePath); } else if (!localFile) { // 远程文件下载 const remoteFilePath = path.posix.join(remotePath, fileName); const localFilePath = path.join(localPath, fileName); remoteFile.type === 'd' ? await downloadDirectory(remoteFilePath, localFilePath) : await downloadFile(remoteFilePath, localFilePath); } else { // 时间戳比较,允许1秒误差 const timeDiff = Math.abs(remoteFile.modifyTime - localFile.modifyTime); if (timeDiff > 1) { if (remoteFile.modifyTime > localFile.modifyTime) { // 远程更新,下载 const remoteFilePath = path.posix.join(remotePath, fileName); const localFilePath = path.join(localPath, fileName); remoteFile.type === 'd' ? await downloadDirectory(remoteFilePath, localFilePath) : await downloadFile(remoteFilePath, localFilePath); } else { // 本地更新,上传 const localFilePath = path.join(localPath, fileName); const remoteFilePath = path.posix.join(remotePath, fileName); localFile.type === 'd' ? await uploadDirectory(localFilePath, remoteFilePath) : await uploadFile(localFilePath, remoteFilePath); } } } } } catch (err) { console.error('Error syncing directory:', err); } } async function downloadFile(remotePath, localPath) { try { await sftp.fastGet(remotePath, localPath); console.log(`Downloaded ${remotePath} to ${localPath}`); } catch (err) { console.error('Error downloading file:', err); } } async function downloadDirectory(remotePath, localPath) { try { await fs.promises.mkdir(localPath, { recursive: true }); const remoteFiles = (await sftp.list(remotePath)).map(file => ({ ...file, modifyTime: Math.floor(file.modifyTime.getTime() / 1000) })); for (const remoteFile of remoteFiles) { const remoteFilePath = path.posix.join(remotePath, remoteFile.name); const localFilePath = path.join(localPath, remoteFile.name); remoteFile.type === 'd' ? await downloadDirectory(remoteFilePath, localFilePath) : await downloadFile(remoteFilePath, localFilePath); } } catch (err) { console.error('Error downloading directory:', err); } } async function uploadFile(localPath, remotePath) { try { await sftp.fastPut(localPath, remotePath); console.log(`Uploaded ${localPath} to ${remotePath}`); } catch (err) { console.error('Error uploading file:', err); } } async function uploadDirectory(localPath, remotePath) { try { // 仅当目录不存在时创建 const exists = await sftp.exists(remotePath); if (!exists) { await sftp.mkdir(remotePath, true); console.log(`Created directory ${remotePath}`); } const localFiles = await getLocalFiles(localPath); for (const localFile of localFiles) { const localFilePath = path.join(localPath, localFile.name); const remoteFilePath = path.posix.join(remotePath, localFile.name); localFile.type === 'd' ? await uploadDirectory(localFilePath, remoteFilePath) : await uploadFile(localFilePath, remoteFilePath); } } catch (err) { console.error('Error uploading directory:', err); } } async function getLocalFiles(dirPath) {
相关产品推荐
相关产品推荐

