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

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();

问题根源

  1. 时间戳精度不匹配:本地使用mtimeMs(毫秒级时间戳),而SFTP服务器返回的modifyTime通常是秒级精度(或Date对象转换后存在精度差),导致即使文件未修改,两者数值也会存在微小差异,触发无意义的同步。
  2. 文件夹同步逻辑冗余:uploadDirectory方法中,只要创建目录就会输出"Uploaded"提示,且syncDirectory里的两段循环(远程→本地、本地→远程)存在重复判断,会重复执行上传操作。
  3. 无容错的时间戳比较:直接用>/<比较时间戳,没有考虑系统时间差或精度误差带来的微小差异,导致误判。

修复方案

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) {
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 00:21:50