马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PBIDEA:用 uo_ftp / uo_sftp 做 FTP 与 SFTP 文件传输
阅读说明
1. 适用版本:PB 12.5 及以上(含 PBIDEA 1.x,本文 API 来自 PBIDEA 导出源码 websuite.pbl/uo_ftp.sru 与 uo_sftp.sru)
2. 支持数据库:不涉及(纯文件传输,与具体 DBMS 无关)
3. 操作系统与环境要求:Windows 7+,已安装 PBIDEA 运行库 PbIdea.dll;(FTP 走 21 端口、SFTP 走 22 端口,需服务端已开启且网络可达)
4. 难度系数:★★★☆☆
5. 其它阅读说明:只需在应用里 create uo_ftp / create uo_sftp 即可(构造时自动 ftpCreate()/sftpCreate(),无需手动初始化);示例中的服务器地址/账号请换成你自己的。
上传下载文件到远程服务器,是做客户端工具绕不开的需求。PBIDEA 自带两个对象:uo_ftp(经典 FTP)和 uo_sftp(基于 SSH 的 SFTP)。它们都封装在 PbIdea.dll 里,用法非常接近——连接、列目录、传文件、断连,一套思路走下来。
本文把这两个对象讲透:对象到底提供了哪些方法、怎么连、怎么列目录、怎么上传下载、怎么拿进度、有哪些坑(端口、被动模式差异、SFTP 没有 mkdir、中文名乱码、上传失败怎么排查),以及在 FTP 和 SFTP 之间怎么选。
一、两个对象的定位与全貌
1.1 uo_ftp(经典 FTP)
uo_ftp 继承 nonvisualobject,构造时自动调用 ftpCreate(),析构时自动调用 ftpDestroy()——你 create 出来就能直接用,不用手动初始化。
它的公开方法(均来自 uo_ftp.sru 源码,真实存在):
| 方法 | 说明 | | ftpConnect(host, port, user, passwd) | 连接 FTP,port 一般 21 | | ftpConnect(host, port, user, passwd, timeout) | 同上,带超时(毫秒,默认 5000) | | ftpDisconnect() | 断开连接 | | ftpIsConnected() | 是否仍连接(boolean) | | ftpSystemType() | 服务端系统类型:0 未知 / 1 UNIX / 2 WINDOWS | | ftpCD(dir) | 进入指定目录 | | ftpCDUP() | 返回上级目录 | | ftpGetDir() | 取当前目录 | | ftpDir(ref files[]) | 列当前目录明细(含大小/时间),返回条目数 | | ftpList(ref files[]) | 列当前目录摘要(仅文件名),返回条目数 | | ftpGet(remoteFile) | 下载到当前目录(同名) | | ftpGet(savefile, remoteFile) | 下载并另存为 savefile | | ftpGet(savefile, remoteFile, openfile) | 同上,openfile=TRUE 下载后自动打开 | | ftpPut(localfile, remotefile, bProgressEvent) | 上传,bProgressEvent=TRUE 触发进度事件 | | ftpDelete(file) | 删除文件 | | ftpRename(oldfile, newfile) | 重命名 | | ftpMkdir(dirName) | 建目录 | | ftpRmDir(dirName) | 删目录 | | ftpSize(file) | 取文件大小(ulong) | | ftpDateTime(file) | 取文件修改时间(string) | | ftpSendCmd(cmd) | 发送任意 FTP 命令(高级) | | ftpUTF8(bUtf8) | 是否用 UTF8 编文件名 | | ftpHeartOpen(seconds) | 心跳包,seconds=0 关闭 | | ftpSetDebug(bDebug) | 打开调试输出 |
注意计划里写的 PutFile/GetFile/Connect 是误记,真实方法名是 ftpConnect / ftpPut / ftpGet,本文以源码为准。
1.2 uo_sftp(基于 SSH 的 SFTP)
uo_sftp 同样构造时自动 sftpCreate()、析构时 sftpDestroy()。方法是 sftp 前缀,参数与 uo_ftp 高度一致:
| 方法 | 说明 | | sftpConnect(host, port, user, passwd) | 密码方式连接,port 一般 22 | | sftpConnect(host, port, user, passwd, privateKey) | 密钥方式连接(privateKey 为私钥路径/内容) | | sftpDisconnect() | 断开 | | sftpIsConnected() | 是否连接 | | sftpGet(savefile, remoteFile) / sftpGet(savefile, remoteFile, openfile) | 下载 | | sftpPut(localfile, remotefile) / sftpPut(localfile, remotefile, bProgressEvent) | 上传 | | sftpDir(strDir, ref files[]) | 列 strDir 目录明细,返回条目数 | | sftpList(strDir, ref files[]) | 列 strDir 目录摘要 | | sftpRename(oldfile, newfile) | 重命名 | | sftpDelete(file) | 删除文件 | | sftpSize(file) / sftpDateTime(file) | 大小 / 修改时间 | | sftpLastError() | 取最近错误信息(排查上传失败首选) | | sftpRun(cmd [, bCrLf]) | 在远端执行命令并返回结果 | | sftpUTF8(bUtf8) / sftpSetDebug(bDebug) | UTF8 / 调试 |
源码里 sftpMkdir / sftpRmDir / sftpGetDir / sftpShellExecute 是被注释掉的(未导出),所以 SFTP 没有现成的建目录/取当前目录方法,需要建目录得用 sftpRun('mkdir xxx')。这是与 FTP 的一个实质差异,别踩坑。
二、最小可运行:FTP 上传一个文件
前置:应用里放一个 uo_ftp 实例或 create uo_ftp;已有一个本地文件 C:\temp\report.xlsx 要传到服务器 /upload/ 目录;服务器地址、账号、密码换成你自己的。
步骤:1) 在按钮 clicked 里贴入下方代码;2) 运行并点击,MessageBox 会弹出"上传成功"或失败原因;3) 到服务器确认文件已到位。 - // 示例输入:FTP 服务器地址
- string ls_host
- ls_host = 'ftp.example.com'
- // 示例输入:FTP 端口(标准 21)
- int li_port
- li_port = 21
- // 示例输入:登录账号
- string ls_user
- ls_user = 'pbuser'
- // 示例输入:登录密码
- string ls_pass
- ls_pass = 'P@ssw0rd'
- // 示例输入:本地待上传文件
- string ls_local
- ls_local = 'C:\temp\report.xlsx'
- // 示例输入:远端保存路径(含文件名)
- string ls_remote
- ls_remote = '/upload/report.xlsx'
- uo_ftp luo_ftp
- luo_ftp = create uo_ftp
- boolean lb_ok
- lb_ok = luo_ftp.ftpConnect(ls_host, li_port, ls_user, ls_pass)
- if not lb_ok then
- MessageBox('FTP 连接失败', '请检查地址/端口/账号密码与网络')
- destroy luo_ftp
- return
- end if
- // 进入目标目录(远端路径用斜杠)
- luo_ftp.ftpCD('/upload')
- // 上传,bProgressEvent=TRUE 会触发 onprogress 事件显示进度
- lb_ok = luo_ftp.ftpPut(ls_local, ls_remote, true)
- if lb_ok then
- MessageBox('FTP 上传', '上传成功:' + ls_remote)
- else
- MessageBox('FTP 上传失败', 'ftpPut 返回 false,检查远端目录权限与文件名')
- end if
- luo_ftp.ftpDisconnect()
- destroy luo_ftp
复制代码
三、最小可运行:SFTP 下载一个文件
前置:create uo_sftp;本地要有可写目录 C:\temp\;服务器开启 SSH(22) 且账号可登录。
步骤:贴入 clicked 代码 → 运行点击 → MessageBox 显示下载结果;到 C:\temp\ 查看文件。 - // 示例输入:SFTP 服务器地址(走 SSH,端口通常 22)
- string ls_host
- ls_host = 'sftp.example.com'
- // 示例输入:SSH 端口
- int li_port
- li_port = 22
- // 示例输入:登录账号
- string ls_user
- ls_user = 'pbuser'
- // 示例输入:登录密码
- string ls_pass
- ls_pass = 'P@ssw0rd'
- // 示例输入:远端待下载文件
- string ls_remote
- ls_remote = '/data/report.xlsx'
- // 示例输入:本地保存路径
- string ls_local
- ls_local = 'C:\temp\report.xlsx'
- uo_sftp luo_sftp
- luo_sftp = create uo_sftp
- boolean lb_ok
- lb_ok = luo_sftp.sftpConnect(ls_host, li_port, ls_user, ls_pass)
- if not lb_ok then
- MessageBox('SFTP 连接失败', luo_sftp.sftpLastError())
- destroy luo_sftp
- return
- end if
- // 下载(savefile, remoteFile)
- lb_ok = luo_sftp.sftpGet(ls_local, ls_remote)
- if lb_ok then
- MessageBox('SFTP 下载', '下载成功:' + ls_local)
- else
- // 失败时一定先看 LastError,信息比 true/false 有用得多
- MessageBox('SFTP 下载失败', luo_sftp.sftpLastError())
- end if
- luo_sftp.sftpDisconnect()
- destroy luo_sftp
复制代码
四、列出远端目录
FTP 用 ftpDir/ftpList,SFTP 用 sftpDir/sftpList,都通过 ref string files[] 把结果带回来。
- // 示例输入:FTP 服务器连接信息
- string ls_host
- ls_host = 'ftp.example.com'
- int li_port
- li_port = 21
- string ls_user
- ls_user = 'pbuser'
- string ls_pass
- ls_pass = 'P@ssw0rd'
- uo_ftp luo_ftp
- luo_ftp = create uo_ftp
- boolean lb_ok
- lb_ok = luo_ftp.ftpConnect(ls_host, li_port, ls_user, ls_pass)
- if not lb_ok then
- MessageBox('连接失败', 'FTP 连接未建立')
- destroy luo_ftp
- return
- end if
- // 列当前目录明细,files[] 每行形如 "-rw-r--r-- 1234 Jan 01 10:00 file.txt"
- string ls_files[]
- long ll_n
- ll_n = luo_ftp.ftpDir(ls_files)
- MessageBox('目录条目数', string(ll_n))
- // 逐行打印(仅演示,真实场景可填 ListBox)
- long i
- for i = 1 to ll_n
- // ls_files[i] 即为一条文件信息
- next
- // 输出第一条文件名,确认列目录确实拿到了数据
- if ll_n > 0 then
- MessageBox('首条文件信息', ls_files[1])
- end if
- luo_ftp.ftpDisconnect()
- destroy luo_ftp
复制代码ftpDir 返回的是带权限/大小/时间的整行字符串;如果只想拿文件名清单,用 ftpList(更短)。SFTP 的 sftpDir(strDir, files[]) 第一个参数是目录路径,不传当前目录——要列根目录就传 '/'。
五、进度回调:大文件要知道传到哪了
ftpPut(..., true) 和 sftpPut(..., ..., true) 上传时会触发对象的 onprogress 事件:
- event type boolean onprogress(double ad_totalsize, double ad_downsize, integer ai_percent, string as_file, integer ai_action)
- // ad_totalsize:总字节数;ad_downsize:已传字节数;ai_percent:百分比;as_file:当前文件;ai_action:动作类型
- // 返回 TRUE 继续;返回 FALSE 中止传输
- return TRUE
- end event
复制代码演示:在 uo_ftp/uo_sftp 子类的 onprogress 事件里写代码,把 ai_percent 赋给你的进度条(如 p_1.position = ai_percent),界面就能实时显示进度。返回 FALSE 即可中途取消。下载(ftpGet/sftpGet)同样会触发该事件。
六、边界情况与常见坑
- 端口别混:FTP 默认 21,SFTP(SSH)默认 22。用 uo_sftp 连 21 必然失败,用 uo_ftp 连 22 也连不上。
- FTP 被动模式:uo_ftp 底层已处理好被动模式,一般内网/云服务器都能传;若卡在"列目录成功但传文件超时",多为防火墙只放了 21 没放数据端口范围——联系运维放行,或改用 SFTP(单端口 22,穿透更友好)。
- SFTP 没有 mkdir/rmdir/getdir:源码里这几个方法被注释未导出。要建目录用 sftpRun('mkdir /data/sub'),取当前目录用 sftpRun('pwd')。
- 中文文件名乱码:FTP 服务端可能是 GBK。先 luo_ftp.ftpUTF8(true) 试 UTF8;若仍乱,说明对方是 GBK,需要用 toUtf8/fromUtf8 全局函数做编解码(注意它们是裸全局函数、不是对象方法,正确写法是直接调用 fromUtf8(blob_var) / toUtf8(str_var),不要通过 uo_ftp 实例去点这两个名字)。
- 上传失败先查 LastError:SFTP 失败不要只信 sftpPut 返回的 false,立刻 sftpLastError() 拿原因——权限不足、路径不存在、磁盘满都会在这里说清楚。
- 连接状态:长连接建议定时 ftpHeartOpen(30) 发心跳防被踢;断开前先 ftpIsConnected()/sftpIsConnected() 判断,避免对已断连接重复操作报错。
- 对象必须 destroy:create 出来的 uo_ftp/uo_sftp 用完要 destroy,否则 PbIdea.dll 里的句柄泄漏,多次操作后可能连不上。
七、FTP 与 SFTP 怎么选
| 维度 | FTP (uo_ftp) | SFTP (uo_sftp) | | 协议 | 文件传输协议,明文 | 基于 SSH 加密隧道 | | 端口 | 21 | 22 | | 安全性 | 低(账号密码明文) | 高(全程加密) | | 建目录 | 有 ftpMkdir | 无,需 sftpRun('mkdir') | | 穿透性 | 数据端口易受限 | 单端口,更稳 | | 适用 | 内网/可信环境 | 公网/跨网传输(首选) |
结论:公网、跨机房、含敏感数据,一律用 SFTP;纯内网、对端只支持 FTP 时才用 uo_ftp。两个对象 API 几乎一一对应,从一个切到另一个成本很低。
八、进阶与扩展
- SFTP 密钥登录:sftpConnect(host, port, user, passwd, privateKey) 第五个参数传私钥(路径或内容),可免密码、更适合自动化脚本。
- 断点续传思路:先用 sftpSize(remoteFile) 拿远端大小,本地比对已下载字节,再 sftpGet 续传(PbIdea 当前未直接提供 offset 续传接口,可结合本地文件大小做"整体重下或跳过"判断)。
- 批量同步:sftpDir 拉列表 → 本地 Directory.FileList() 比对 → 循环 sftpPut/sftpGet,配合 onprogress 做总进度。
- 调试:ftpSetDebug(true) / sftpSetDebug(true) 会把底层交互打到调试输出,定位握手/命令错误时很好用。
- 与之前文章联动:需要把本地文件打成压缩包再传?参考 Day 16 的 uo_zip/uo_7z;需要把传完的结果写日志?参考 Day 20 的 uo_logger。
把 uo_ftp/uo_sftp 这套方法记熟,客户端里"传文件到服务器"这件事就不再是拦路虎了。 |