☰
深入解析 node-ftp:Publii 静态站点 CMS 的异步 FTP 客户端模块
2026/10/15 18:08:41 网站建设 项目流程
  • 开发工具
  • 前端

【免费下载链接】Publii

The most intuitive Static Site CMS designed for SEO-optimized and privacy-focused websites.

项目地址:https://gitcode.com/gh_mirrors/pu/Publii
点击查看免费下载

node-ftp(npm 包名ftp)是一个面向 node.js 的 FTP 客户端模块,它提供了与 FTP 服务器通信的完整异步接口:连接、认证、列目录、上传下载、文件操作等能力全部以事件与回调的方式暴露。在 Publii 这个开源静态站点 CMS 中,node-ftp 被作为 vendored 依赖内置于 app/back-end/modules/custom-changes/ftp 目录,是 Publii FTP(S) 部署链路的底层传输引擎。读完本文,你将完整掌握 node-ftp 的安装方式、事件模型、全部 API(含 RFC 959 / RFC 3659 命令分层)、连接配置项及其默认值,并理解 Publii 是如何基于它实现同步上传、增量删除与连接测试的。

一、模块定位:Publii 中的 FTP 传输引擎

在 Publii 仓库中,node-ftp 以"custom-changes"(定制改动)目录形式随源码分发。从 package.json 可以看到,其版本锁定为ftp@^0.3.10(即 0.3.10),声明依赖readable-stream 1.1.x与xregexp 2.0.0,入口文件为./lib/connection,引擎要求node >=0.8.0,采用 MIT 协议。

Publii 对它的消费位于 app/back-end/modules/deploy/ftp.js,FTP部署类直接require('./../custom-changes/ftp')创建客户端实例,实现站点静态文件到 FTP(S) 服务器的同步。可以推断,Publii 将第三方模块以源码形式 vendored 进仓库,是为了在不依赖运行时 npm 安装的情况下保证可移植性与版本可控。

二、环境要求与安装

原文档明确的环境要求为:

  • node.js —— v0.8.0 或更新版本

安装方式(常规 npm 用法):

npm install ftp

从源码层面看,模块由 lib/connection.js(客户端主体,约 1070 行)与 lib/parser.js(响应与列表解析器)组成。connection.js顶层引入了fs、tls、zlib、net.Socket与events.EventEmitter,这决定了它天然支持 TLS/SSL 加密连接、gzip 压缩传输(MODE Z)以及基于事件流的异步模型。

三、快速上手:三个典型示例

原文档提供了三个直接可运行的示例,完整保留如下,并补充源码层面的解读。

3.1 获取远程工作目录列表

var Client = require('ftp'); var c = new Client(); c.on('ready', function() { c.list(function(err, list) { if (err) throw err; console.dir(list); c.end(); }); }); // connect to localhost:21 as anonymous c.connect();

ready事件表示连接与认证全部完成,此时才能安全发起命令。list()返回的数组项由 parser.js 中的parseListEntry解析生成——它内置了 Unix(REX_LISTUNIX)与 MS-DOS(REX_LISTMSDOS)两套目录列表正则,自动识别type/rights/owner/group/size/date等字段。

3.2 下载远程文件到本地

var Client = require('ftp'); var fs = require('fs'); var c = new Client(); c.on('ready', function() { c.get('foo.txt', function(err, stream) { if (err) throw err; stream.once('close', function() { c.end(); }); stream.pipe(fs.createWriteStream('foo.local-copy.txt')); }); }); // connect to localhost:21 as anonymous c.connect();

get()的回调会收到一个可读流(fileStream),这正是 Publii 下载远端files.publii.json清单时使用的机制——见 ftp.js 中的downloadFilesList(),它将流式 chunk 拼接为完整 JSON 后与本地文件清单比对。

3.3 上传本地文件到服务器

var Client = require('ftp'); var fs = require('fs'); var c = new Client(); c.on('ready', function() { c.put('foo.txt', 'foo.remote-copy.txt', function(err) { if (err) throw err; c.end(); }); }); // connect to localhost:21 as anonymous c.connect();

put()的input参数支持可读流、Buffer 或本地文件路径三种形态。在 connection.js 的_store()内部实现中,字符串参数会先经fs.stat判断是否为文件路径:存在则fs.createReadStream(input).pipe(dest)流式上传,不存在则当作字符串数据直接写入。这也是 Publii 的uploadFile()与uploadNewFileList()方法的基础。

四、事件(Events)API

客户端继承自EventEmitter,对外暴露以下事件:

  • greeting(<string>msg) —— 连接建立后触发,msg是服务器连接时发送的欢迎文本。源码在 connection.js 处发出,初次响应会被用作认证握手(AUTH TLS / USER)的起点。
  • ready() —— 连接与认证成功时触发。握手序列为:USER→PASS→FEAT→TYPE I,最终在TYPE响应后触发ready(见 connection.js)。
  • close(<boolean>hadErr) —— 连接完全关闭时触发。Publii 在 ftp.js 中监听该事件:若同步尚未完成却收到关闭事件,会判定为服务器空闲超时等异常,并向主进程上报错误。
  • end() —— 连接结束时触发(对端发起 FIN 后)。
  • error(<Error>err) —— 出错时触发。对于协议级错误,err对象会带code属性,对应 FTP 三位数字响应码(如 421、425、450、550 等);该code由 connection.js 的makeError()注入。

Publii 的实践:在 ftp.js 中同时监听error事件,对包含ECONNRESET的字符串错误进行计数(最多容忍 5 次),超出后销毁连接并向 UI 发送app-connection-error;若 20 秒内未收到ready,则由超时定时器兜底销毁连接。

五、连接配置:connect() 全部参数

connect(<object>config)建立到 FTP 服务器的连接,其合法配置属性及默认值如下(与 connection.js 的options赋值一一对应):

参数类型说明默认值
hoststringFTP 服务器主机名或 IP'localhost'
portintegerFTP 服务器端口21
securemixed设为true表示控制连接与数据连接均加密;'control'仅加密控制连接;'implicit'表示隐式加密控制连接(现代已较少使用,通常配合端口 990)false
secureOptionsobject传递给tls.connect()的附加选项(无)
userstring认证用户名'anonymous'
passwordstring认证密码'anonymous@'
connTimeoutinteger等待控制连接建立的最长时间(毫秒)10000
pasvTimeoutinteger等待 PASV 数据连接建立的最长时间(毫秒)10000
keepaliveinteger发送空操作(NOOP)命令以保持连接的心跳间隔(毫秒)10000

三个超时/心跳机制的源码依据:

  • connTimeout:连接建立定时器在 connection.js 实现,超时后发出Timeout while connecting to server错误并销毁 socket;
  • pasvTimeout:数据连接定时器在_pasvConnect()(connection.js)中实现,超时则报Timed out while making data connection;
  • keepalive:NOOP 心跳逻辑在donoop()(connection.js)中实现——仅在无进行中请求且队列为空时发送NOOP\r\n,避免与真实命令冲突。

Publii 的实际连接参数可在 ftp.js 看到:它把connTimeout、pasvTimeout设为 15000ms,并额外传入debug回调(内部connectionDebugger会拦截PASS命令的调试输出,将密码替换为星号后打印);secure依据站点部署协议是否为ftp决定——协议为ftp+tls时开启加密,同时secureOptions中透传rejectUnauthorized供用户控制证书校验。

六、连接生命周期:end() 与 destroy()

  • end() —— 待所有已入队命令执行完毕后关闭服务器连接。源码中若队列仍有命令,会置_ending = true,待队列清空后在_send()内触发_reset()(connection.js)。
  • destroy() —— 立即关闭连接,丢弃未执行的排队命令(connection.js),等价于直接调用_reset()。

Publii 中二者的典型组合出现在uploadNewFileList():上传完files.publii.json后依次调用logout()→end()→destroy(),确保 QUIT 握手完成且资源被彻底回收(ftp.js)。

七、标准命令(RFC 959)

以下命令属 RFC 959 定义的标准命令,协议级错误同样会在回调的err.code中携带三位响应码。

  • list([<string>path, ][`<boolean>useCompression`, ]<function>callback) —— 获取path(默认当前工作目录)的目录列表。useCompression默认false。回调参数:(err, list),list为对象数组,元素属性包括:
    • type—— 单个字符表示条目类型:d目录、-文件,l符号链接(仅 *NIX);
    • name—— 条目名称;
    • size—— 字节数(string);
    • date—— 最后修改时间的Date对象;
    • rights—— 权限对象(仅 *NIX),含user、group、other三组字符串,值为r/w/x的任意组合或空串;
    • owner—— 归属用户或 UID(仅 *NIX);
    • group—— 归属组或 GID(仅 *NIX);
    • target—— 符号链接目标路径(仅 *NIX);
    • sticky—— 是否设置 sticky 位(仅 *NIX)。
  • get(<string>path, [<boolean>useCompression, ]<function>callback) —— 下载path处文件。回调参数:(err, fileStream),fileStream为可读流;开启压缩时内部会经zlib.createInflate()解压(connection.js)。
  • put(<mixed>input,<string>destPath, [<boolean>useCompression, ]<function>callback) —— 上传数据到destPath。input可为 ReadableStream、Buffer 或本地文件路径。回调参数:(err)。
  • append(<mixed>input,<string>destPath, [<boolean>useCompression, ]<function>callback) —— 与put()相同,但destPath已存在时执行追加而非覆盖(底层发送APPE命令)。
  • rename(<string>oldPath,<string>newPath,<function>callback) —— 重命名远程文件,底层为RNFR+RNTO两步(connection.js)。
  • logout(<function>callback) —— 注销当前用户(发送QUIT)。
  • delete(<string>path,<function>callback) —— 删除远程文件(发送DELE)。
  • cwd(<string>path,<function>callback) —— 切换当前工作目录。回调参数:(err, currentDir),currentDir仅在服务器响应文本中包含路径时返回(connection.js)。
  • abort(<function>callback) —— 中止进行中的数据传输(如 get/put/list),发送ABOR;若存在活跃数据连接,还会将其标记为aborting(connection.js)。
  • site(<string>command,<function>callback) —— 通过 SITE 发送服务器扩展命令,例如'CHMOD 755 foo'、'QUOTA'。回调参数:(err, responseText, responseCode)。
  • status(<function>callback) —— 获取服务器状态的可读信息(发送STAT)。回调参数:(err, status)。
  • ascii(<function>callback) —— 将传输类型设为 ASCII(发送TYPE A)。
  • binary(<function>callback) —— 将传输类型设为二进制,这也是连接建立时的默认类型(发送TYPE I)。

八、可选标准命令(RFC 959)

以下命令同样源自 RFC 959,但被标记为"可选"——服务器可能不支持,调用时需做好err.code === 502之类的兼容处理。

  • mkdir(<string>path, [<boolean>recursive, ]<function>callback) —— 创建目录。recursive启用类似mkdir -p的逐级创建算法,默认false。其递归实现(connection.js)会先PWD记录原工作目录,逐段CWD探测、MKD创建后回到原目录。Publii 的uploadDirectory()正是以mkdir(output, true, cb)的形式递归创建远程目录树。
  • rmdir(<string>path, [<boolean>recursive, ]<function>callback) —— 删除目录;recursive为真时先list()目录内容,再递归删除子目录/文件,最后删除目录本身(connection.js)。Publii 的removeDirectory()传true使用该递归能力。
  • cdup(<function>callback) —— 切换到父目录;服务器返回 502 时会回退为CWD ..。
  • pwd(<function>callback) —— 获取当前工作目录。回调参数:(err, cwd);服务器不支持PWD(502)时回退为CWD .探测(connection.js)。
  • system(<function>callback) —— 获取服务器操作系统标识(发送SYST)。回调参数:(err, OS)。
  • listSafe([<string>path, ][`<boolean>useCompression`, ]<function>callback) —— 与list()类似,但会临时cwd到目标目录再取列表,适用于对目录名含空格、引号处理不佳的服务器;它依赖pwd()可用,故归入"可选"(connection.js)。

九、扩展命令(RFC 3659)

RFC 3659 定义的扩展命令,同样需要服务器端支持:

  • size(<string>path,<function>callback) —— 获取文件大小。回调参数:(err, numBytes);服务器不支持SIZE(502)时回退为list()单条目的size字段(connection.js)。
  • lastMod(<string>path,<function>callback) —— 获取文件最后修改时间。回调参数:(err, lastModified);通过MDTM命令与REX_TIMEVAL正则解析,不支持时同样回退到目录列表(connection.js)。
  • restart(<integer>byteOffset,<function>callback) —— 为下一次 get/put 传输设置文件字节偏移(发送REST),可用于断点续传。

十、内部机制深挖:从源码理解连接与数据流

10.1 命令队列与串行模型

_send(cmd, cb, promote)(connection.js)是全部命令的底层通道:命令对象进入 FIFO 队列,每次仅有一个"当前请求"(_curReq)在途,收到终结响应后才继续发送下一条;promote参数为真时命令被插入队首(用于 PASV 数据连接建立前的关键命令,如TYPE、MODE)。这种串行化保证了 FTP 命令与响应的一一对应。

10.2 PASV 被动模式与数据连接

所有数据传输(list/get/put)都先通过_pasv()(connection.js)发起PASV,用RE_PASV正则解析服务器返回的(h1,h2,h3,h4,p1,p2)六元组,计算数据端口p1*256+p2;若连接失败且服务器给出的 IP 与控制连接 IP 不同,还会自动改用控制连接的对端地址重试,这是对"服务器返回内网 IP"这一常见配置问题的内建兜底。

10.3 协议响应解析器

parser.js 是一个WritableStream式的解析器:以\r?\n终止行为单位,用正则RE_RES_END匹配\d{3}终结响应码,并剥离多行响应中每行的前导NNN-,最终触发response(code, text)。响应码按首位数字划分为五类(RETVAL常量):1xx 预备、2xx 成功、3xx 等待、4xx 临时错误、5xx 永久错误,4xx/5xx 会转化为带code的Error。

10.4 加密连接的握手序列

当secure: true时,连接建立后先发送AUTH TLS(若服务器不支持则回退AUTH SSL),再依次完成PBSZ 0、PROT P、USER、PASS的完整 TLS 升级流程(connection.js);secure: 'implicit'则直接以tls.connect()建立加密 socket。Publii 的ftp+tls协议即走此路径,而普通ftp则使用明文连接。

十一、Publii 中的真实调用链

结合源码可将 node-ftp 在 Publii 部署流程中的位置梳理如下:

  1. workers/deploy/deployment.js 是部署 worker 进程入口,收到主进程消息后创建 modules/deploy/deployment.js 的Deployment实例;
  2. Deployment.initSession()根据siteConfig.deployment.protocol分支:ftp/ftp+tls落入FTP类(即本模块),useAltFtp为真时则改用基于basic-ftp的 ftp-alt.js 备选实现;
  3. FTP.initConnection()构造new ftpClient()并传入host/port/user/password/secure/connTimeout/pasvTimeout等连接参数;
  4. ready事件触发后调用downloadFilesList()用get()拉取远端files.publii.json,与本地清单比对出新增/变更/删除文件;
  5. 随后按差异调用put()/mkdir()/delete()/rmdir()批量同步(失败文件自动重试至多 5 次,超出计入硬错误列表);
  6. 完成后put()上传新清单,logout()+end()+destroy()收尾。

此外,FTP.testConnection()(ftp.js)会用该客户端向服务器上传并立即删除一个publii.test测试文件,以验证连接与写权限,对应界面中"测试连接"功能。

十二、已知限制与演进方向

仓库自带的 TODO 记录了该模块的待办事项:

  • 增加对部分 SITE 命令(如 CHMOD、CHGRP、QUOTA)的支持;
  • 支持主动(非被动)数据连接;
  • 支持 IPv6。

这也解释了为何 Publii 同时维护了基于basic-ftp的替代实现 ftp-alt.js 作为可切换的后备方案,以规避旧模块在部分服务器兼容性上的局限。

结语

node-ftp 以简洁的"事件 + 回调 + 可读流"异步模型完整覆盖了 FTP 协议的标准、可选与扩展命令,目录列表解析、TLS 升级、被动模式兜底、命令串行队列等内部设计在 lib/connection.js 与 lib/parser.js 中均有清晰的实现依据。在 Publii 中它承担着 FTP(S) 部署的核心传输职责,理解其 API 与源码细节,无论对于二次开发部署插件还是排查同步问题,都极具参考价值。

  • 开发工具
  • 前端

【免费下载链接】Publii

The most intuitive Static Site CMS designed for SEO-optimized and privacy-focused websites.

项目地址:https://gitcode.com/gh_mirrors/pu/Publii
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询