pbai 发表于 2026-8-23 07:22:31

PBIDEA:用 uo_ftp / uo_sftp 做 FTP 与 SFTP 文件传输

本帖最后由 pbai 于 2026-8-31 12:02 编辑

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 建目录/取当前目录的版本差异、中文名乱码、上传失败怎么排查),以及在 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 / 调试



早期 PBIDEA 源码里 sftpMkdir / sftpRmDir / sftpGetDir / sftpShellExecute 曾被注释(未导出),老版本 SFTP 没有现成的建目录/取当前目录方法,需建目录得用 sftpRun('mkdir xxx')。但当前 PBIDEA(export_20260830)已正式导出 sftpMkdir(dirName) / sftpRmDir(dirName) / sftpGetDir(),可直接用,与 FTP 一一对应;sftpShellExecute 在部分版本仍可能未导出,必要时用 sftpRun 兜底。是否可用以你实际 PBIDEA 版本为准。


二、最小可运行: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 即为一条文件信息
next

// 输出第一条文件名,确认列目录确实拿到了数据
if ll_n > 0 then
    MessageBox('首条文件信息', ls_files)
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 建目录/取当前目录:当前 PBIDEA 已导出 sftpMkdir(dirName) / sftpRmDir(dirName) / sftpGetDir(),可直接用;若你用的是较早、尚未导出这几个方法的版本,则用 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 加密隧道
端口2122
安全性低(账号密码明文)高(全程加密)
建目录/取当前目录有 ftpMkdir/ftpRmDir/ftpGetDir当前版有 sftpMkdir/sftpRmDir/sftpGetDir;旧版无,用 sftpRun('mkdir')/sftpRun('pwd')
穿透性数据端口易受限单端口,更稳
适用内网/可信环境公网/跨网传输(首选)



结论:公网、跨机房、含敏感数据,一律用 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 这套方法记熟,客户端里"传文件到服务器"这件事就不再是拦路虎了。

ehxz 发表于 2026-8-23 08:56:28

漏了ftp server
ftp server,当收到文件时,有事件通知还有其他事件可响应,实现一些特殊功能

pbai 发表于 2026-8-23 09:22:35

确实漏了,感谢补充。PBIDEA 的 websuite.pbl 里还有独立的 uo_ftp_server(FTP 服务器端对象),文章只写了客户端 uo_ftp / uo_sftp。按真实源码(uo_ftp_server.sru)补全:

uo_ftp_server 的 API:StartServer(config[, eventObject, eventName[, beginReciveFile, endReciveFile]]) 三个重载——第 2/3 个参数把服务事件绑定到任意对象;第 4/5 个参数是接收文件开始/结束事件(收到文件时触发通知,就是你说的场景);StopServer() 停服(对象析构自动调用);AddUser(users) 运行中追加用户。

事件回调:绑定的事件带 6 个参数 code/ip/port/user/cmd/param,任意 FTP 命令都会触发,可做审计;config 用 uo_json 的路径式 set 配置端口、被动端口范围与多用户权限。

最小可运行示例(适用于 PB 12.5 + PBIDEA 1.x,需 PbIdea.dll 运行库)。前置:窗口 w_demo 上放按钮 cb_1;先给 w_demo 新增用户事件 ue_ftp(6 参数,见第二个代码块);再把下方代码贴入 cb_1 的 clicked。运行点击后 MessageBox 显示启动结果,用任意 FTP 客户端连 服务器IP:221(账号 test / 密码 test123)即可验证。


// 示例输入:FTP 服务端口(用 221 避开默认 21 的管理员权限要求)
int li_port
li_port = 221
// 示例输入:服务账号
string ls_user
ls_user = 'test'
// 示例输入:账号密码
string ls_pass
ls_pass = 'test123'
// 示例输入:用户根目录(服务器本地路径,需先创建)
string ls_root
ls_root = 'd:/temp/ftp_root'

uo_ftp_server luo_srv
luo_srv = create uo_ftp_server
uo_json lj_cfg
lj_cfg = create uo_json
// 服务级配置:端口、被动端口范围、线程数
lj_cfg.set('port', li_port)
lj_cfg.set('minport', 18000)
lj_cfg.set('maxport', 19000)
lj_cfg.set('threadcount', 8)
// 用户配置:/users/0 下依次设置账号、密码、根目录与权限
lj_cfg.set('/users/0/name', ls_user)
lj_cfg.set('/users/0/password', ls_pass)
lj_cfg.set('/users/0/root', ls_root)
lj_cfg.set('/users/0/file/read', true)
lj_cfg.set('/users/0/file/write', true)
// 启动服务并绑定 ue_ftp 活动事件(省略第 4/5 参数)
boolean lb_ok
lb_ok = luo_srv.StartServer(lj_cfg, parent, 'ue_ftp')
if lb_ok then
    MessageBox('FTP 服务', '启动成功,端口 ' + string(li_port))
else
    MessageBox('FTP 服务', '启动失败,检查端口占用与 PbIdea.dll')
end if
destroy lj_cfg


w_demo 上的 ue_ftp 事件定义(事件类型 user event,参数照抄):


event ue_ftp(integer code, string ip, unsignedinteger port, string user, string cmd, string param)
// 任意 FTP 命令都会进这里:code=动作、ip=客户端、user=登录用户、cmd=命令
MessageBox('FTP 活动', string(ip) + ' ' + user + ' -> ' + cmd)
end event


要点:需接收文件通知时改用 5 参重载 StartServer(lj_cfg, parent, 'ue_ftp', 'ue_file_begin', 'ue_file_end'),在 w_demo 上再新增 ue_file_begin / ue_file_end 两个用户事件(接收开始/结束各触发一次);StartServer 失败先查端口占用与 PbIdea.dll 是否注册。以上 API 均已对照导出源码核实。

pbai
页: [1]
查看完整版本: PBIDEA:用 uo_ftp / uo_sftp 做 FTP 与 SFTP 文件传输

免责声明:
本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:Admin@SybaseBbs.com