A pure MoonBit FTP client library ported from jlaffaye/ftp, supporting passive mode, MLSD/LIST parsing, resume and FTPS.
Dependencies
curl -fsSL https://cli.moonbitlang.cn/install/unix.sh | bash
export PATH="$HOME/.moon/bin:$PATH"
# native 后端所需的 C 工具链(Debian / Ubuntu)
sudo apt-get install -y gcc libc6-devmoon fmt --check # 格式化检查
moon check --target native --deny-warn # 类型检查,0 warning 0 error
moon test --target native # 运行测试(真机用例默认跳过)
moon build --target native # 构建
moon run cmd/ftp # 运行 CLI(打印 usage)
moon info # 更新生成接口(.mbti)scripts/start-ftp.sh # 起 full / no-mlst / no-time / no-epsv 四个容器
export FTP_TEST_HOST=127.0.0.1 FTP_TEST_PORT=2121 FTP_TEST_USER=test FTP_TEST_PASS=test FTP_TEST_FIXTURE=/fixture FTP_TEST_DIR=/upload FTP_TEST_PORT_NO_MLST=2122 FTP_TEST_PORT_NO_TIME=2123 FTP_TEST_PORT_NO_EPSV=2124
moon test --target native
scripts/stop-ftp.sh # 打日志并清理容器| 脚本 | 作用 |
|---|---|
| scripts/start-ftp.sh | 起四个真实 FTP 服务器容器并轮询端口,起不来直接 exit 1 |
| scripts/stop-ftp.sh | 打印每个容器的日志并删除,从不失败 |
.
├── entry.mbt Entry / EntryType / TransferType 纯逻辑
├── status.mbt RFC 959 状态码与常量 + status_text() 纯逻辑
├── error.mbt FtpError / FtpErrors 纯逻辑
├── parse.mbt RFC3659 / UNIX ls / DOS DIR / hostedftp 解析器 纯逻辑
│ 回退链 + LIST 时间字段解析 + 字段扫描器
├── pathutil.mbt 远端路径 join(对齐 Go path.Join 语义) 纯逻辑
├── control.mbt 控制通道:命令编码、多行响应、状态码校验 IO
│ + 流量日志包装
├── transport.mbt EPSV / PASV / PRET / REST / 数据连接 / TLS 建立 IO
├── client.mbt FTPClient + Session/Options + DialOptions IO
│ + SIZE / MDTM / MFMT
├── dial.mbt dial / 登录 / FEAT 能力协商 IO
├── commands.mbt CWD / PWD / MKD / RMD / DELE / RNFR+RNTO IO
│ / NOOP / REIN / QUIT
├── list.mbt / transfer.mbt / walker.mbt
│ 列表、传输、目录树遍历 IO
├── cmd/ftp/ CLI 示例:ls / get / put / walk / mkdir / rm
├── scripts/ 跨 CI 复用的公共脚本
│ ├── start-ftp.sh 起四个真实 FTP 服务器容器(CNB / GitHub 共用)
│ └── stop-ftp.sh 打容器日志并清理
├── testdata/ftp/ 测试数据:fixture 与 vsftpd 配置模板
├── .cnb.yml / .github/workflows/ CNB 与 GitHub 两条流水线(调同一份 scripts/)
├── moon.pkg 根包清单(唯一的源码包)
└── moon.mod 模块根pub(all) suberror FtpError {
ServerError(code~ : Int, msg~ : String)
InvalidCommand(arg~ : String)
UnsupportedListLine(line~ : String)
UnsupportedListDate(field~ : String)
ParseError(msg~ : String)
} derive(Debug)async fn DataResponse::read(self : DataResponse, dst : FixedArray[Byte], offset? : Int, max_len? : Int) -> Intpub struct DialOptions {
timeout_ms : Int
shut_timeout_ms : Int
tls : Bool
explicit_tls : Bool
trust : TrustedRoot
disable_epsv : Bool
trust_pasv_ip : Bool
disable_utf8 : Bool
disable_mlsd : Bool
writing_mdtm : Bool
force_list_hidden : Bool
location : Zone
}pub struct Entry {
name : String
target : String
type_ : EntryType
size : UInt64
time : ZonedDateTime
} derive(Debug)type ListDateFieldpub struct Options {
disable_epsv : Bool
trust_pasv_ip : Bool
tls : Bool
timeout_ms : Int
shut_timeout_ms : Int
trust : TrustedRoot
}fn Options::new(disable_epsv : Bool, trust_pasv_ip : Bool, tls : Bool, timeout_ms : Int, shut_timeout_ms : Int, trust : TrustedRoot) -> Optionspub struct Scanner {
src : String
offset : Int
}new Scanner("foo bar x y")
.next() -> "foo", position stops *after* the run of spaces
.remaining() -> " bar x y" // the leading space is kept
.next() -> "bar"
.remaining() -> "x y"fn apply_list_time(entry : Entry, year : Int, month : Int, day : Int, has_time : Bool, hour : Int, minute : Int, now : ZonedDateTime) -> Unit raise FtpErrorlet default_dial_timeout_ms : Intasync fn dial(addr : String, timeout_ms? : Int, shut_timeout_ms? : Int, tls? : Bool, explicit_tls? : Bool, trust? : TrustedRoot, disable_epsv? : Bool, trust_pasv_ip? : Bool, disable_utf8? : Bool, disable_mlsd? : Bool, writing_mdtm? : Bool, force_list_hidden? : Bool, location? : Zone) -> FTPClientfn extract_quoted(message : String) -> String?fn find_date_start_pub(line : String, start : Int) -> Int?fn is_all_digits(src : String) -> Boolfn is_bogus_data_ip(cmd_ip : String, data_ip : String) -> Boolfn is_continuation(line : String) -> Boolfn is_loopback(ip : String) -> Boolfn is_multicast(ip : String) -> Boolfn is_positive_completion(code : Int) -> Boolfn is_positive_intermediate(code : Int) -> Boolfn is_private(ip : String) -> Boolfn join(base : String, name : String) -> Stringjoin("root/", "lo") == "root/lo"
join("root", "a") == "root/a"
join("root", "..") == "."
join("", "a") == "a"fn new_datetime(year : Int, month : Int, day : Int, hour : Int, minute : Int, zone : Zone) -> ZonedDateTime raise FtpErrorfn parse_decimal(text : String) -> Int?07-27-17 04:50PM 1264086 File.txt
11-06-16 09:31AM <DIR> Softlibdrwxr-xr-x folder 0 Aug 15 05:49 !!!-Tipp des Haus!drwxr-xr-x 3 110 1002 3 Dec 02 2009 pub
-rw-r--r-- 1 marketwired marketwired 12016 Mar 16 2016 newsml
drwxr-xr-x folder 0 Aug 15 05:49 !!!-Tipp des Haus!fn parse_ls_time(src : String, start : Int, now : ZonedDateTime) -> (ZonedDateTime, Int)? raise FtpErrorfn parse_month(name : String) -> Int?fn parse_octet(text : String) -> Int?fn parse_port(digits : String) -> Int?type=file;size=951;modify=20140101000000; welcome.msgfn split_addr(addr : String, default_port : Int) -> (String, Int)let status_closing_control_connection : Intlet status_restart_marker_answer : Intlet status_service_unavailable : Intfn status_text(code : Int) -> StringInstall
Download zipA pure MoonBit FTP client library ported from jlaffaye/ftp, supporting passive mode, MLSD/LIST parsing, resume and FTPS.
Dependencies