Skip to content

Commit 9f879a5

Browse files
committed
release: v0.2.0
1 parent 4e43620 commit 9f879a5

13 files changed

Lines changed: 1115 additions & 37 deletions

CHANGELOG.en.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,20 @@
22

33
All notable changes to `ii` are documented here. The default Chinese version is [CHANGELOG.md](CHANGELOG.md).
44

5-
## Unreleased
5+
## 0.2.0 - 2026-07-31
66

7-
Nothing yet.
7+
### Added
8+
9+
- Added `ii webrtc [--token <value>]` for temporary LAN browser-to-browser WebRTC file transfer rooms; it prints a QR code and all IPv4 LAN URLs, keeps file bytes out of the `ii` process, and uses no public STUN/TURN.
10+
11+
### Fixed
12+
13+
- Fixed `ii webrtc` DataChannel setup between mobile browsers when mDNS host candidates could not be resolved or ICE gathering did not finish; signalling now uses the client's LAN IPv4 and trickles candidates immediately.
14+
- `ii webrtc` now verifies an ICE host candidate before joining; browsers with disabled or blocked WebRTC receive an explicit message instead of only discovering peers without being able to transfer.
15+
16+
### Documentation
17+
18+
- Added [webrtc.md](webrtc.md) covering LAN scope, browser requirements, memory limits, the path token, and unsupported capabilities.
819

920
## 0.1.19 - 2026-07-31
1021

CHANGELOG.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,20 @@
22

33
本文件记录 `ii` 的重要变更。默认中文版本在这里,英文版本见 [CHANGELOG.en.md](CHANGELOG.en.md)
44

5-
## Unreleased
5+
## 0.2.0 - 2026-07-31
66

7-
暂无。
7+
### 新增
8+
9+
- 新增 `ii webrtc [--token <value>]`:临时提供局域网浏览器间 WebRTC 文件直传房间,终端输出二维码和全部 IPv4 LAN URL;文件不经过 `ii` 进程,不使用公网 STUN/TURN。
10+
11+
### 修复
12+
13+
- 修复 `ii webrtc` 在移动浏览器之间因 mDNS host candidate 无法解析或 ICE gathering 不结束而无法建立 DataChannel 的问题;信令使用访问服务的客户端局域网 IPv4 并即时转发候选。
14+
- `ii webrtc` 页面在加入房间前实际验证 ICE host candidate;浏览器禁用或阻止 WebRTC 时明确提示,避免只能发现设备但无法传输。
15+
16+
### 文档
17+
18+
- 新增 [webrtc.md](webrtc.md),说明局域网范围、浏览器要求、内存限制、路径令牌和不支持的能力。
819

920
## 0.1.19 - 2026-07-31
1021

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ exclude = ["vendor/iroh", "vendor/suppaftp"]
44
resolver = "3"
55

66
[workspace.package]
7-
version = "0.1.19"
7+
version = "0.2.0"
88
edition = "2024"
99
rust-version = "1.91"
1010
license = "MIT"

README.en.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
- The sender serves one successful receive by default, then exits
2525
- It briefly probes for a usable path through complex networks, with S3, WebDAV, FTP, and SFTP backend options
26-
- `ii send <file-or-folder> --web` opens a temporary LAN download page, while `ii web [directory]` browses a LAN directory
26+
- `ii send <file-or-folder> --web` opens a temporary LAN download page, `ii web [directory]` browses a LAN directory, and `ii webrtc` transfers files directly between LAN browsers
2727
- Receives resume automatically by default
2828
- Existing files with the same MD5 are skipped
2929
- Folders can be sent directly
@@ -134,12 +134,16 @@ ii send .\video.mp4 --web --token A1b2C3d4E5f6G7h8
134134
ii send .\video.mp4 --web --path .\uploads
135135
ii web
136136
ii web .\shared --token A1b2C3d4E5f6G7h8 --path .\uploads
137+
ii webrtc
138+
ii webrtc --token A1b2C3d4E5f6G7h8
137139
```
138140

139141
The command displays a QR code directly above the primary LAN URL for opening the download page, then lists the remaining physical and virtual adapter IPv4 URLs under `other:`. The QR code at the top of that page points directly to `/download` for phone downloads. The page can also upload multiple files into `./ii/` under the directory where the command started. Use `--path <dir>` to write directly into a different directory; relative paths are based on the startup directory, and the directory is created on the first upload. Directory uploads are not supported. Folders download as `.tar` archives. Press `Ctrl+C` to stop the server. Optional `--token <value>` adds a path access token to the page, download, and upload URLs; it must be 16 to 128 ASCII letters, digits, `-`, or `_`, and omitting it keeps the unprotected URLs. This mode has no account authentication and is intended only for short-lived, trusted LAN sharing.
140142

141143
Without a path, `ii web` serves the directory where the command starts; with a path, it accepts only an existing directory. Its page provides recursive nginx-style directory browsing, normal file responses, and multi-file uploads. The terminal still shows the QR code, primary IPv4 LAN URL, and `other:` adapter URLs, but the directory page has no QR code. `--token` and `--path` follow the same rules as `ii send ... --web`; `-p` does not apply to `ii web`.
142144

145+
`ii webrtc` opens a LAN browser-to-browser transfer room. The terminal prints a QR code, the primary IPv4 LAN URL, and `other:` adapter URLs. Browsers that open the same URL receive temporary device numbers, choose another online device, and send multiple independent files. Files travel directly between browser WebRTC DataChannels, never through the `ii` process or its disk. Before joining, the page verifies that the browser can create an ICE host candidate; if WebRTC is disabled or blocked it shows `WebRTC unavailable` and does not join the room. It exchanges host candidates only and has no public STUN/TURN fallback, so client isolation, firewalls, or blocked P2P can prevent connection. Receivers download automatically after buffering each file in browser memory, so it is unsuitable for files larger than the available browser memory. Optional `--token <value>` scopes the page and signalling URLs to a path token. See [webrtc.md](webrtc.md) for details.
146+
143147
Prefer local network paths and avoid public relays:
144148

145149
```powershell

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
自动找路 / 局域网优先 / 可公网中继或 `--s3` / `--webdav` / `--ftp` / `--sftp` 中继
2727

28-
也可用 `ii send <文件或文件夹> --web` 临时开下载页,或用 `ii web [目录]` 浏览局域网目录
28+
也可用 `ii send <文件或文件夹> --web` 临时开下载页`ii web [目录]` 浏览局域网目录,或用 `ii webrtc` 让浏览器局域网直传文件
2929

3030
断点续收 / 秒传跳过 / 冲突覆盖 / 支持传完清理中转
3131

@@ -137,12 +137,16 @@ ii send .\video.mp4 --web --token A1b2C3d4E5f6G7h8
137137
ii send .\video.mp4 --web --path .\uploads
138138
ii web
139139
ii web .\shared --token A1b2C3d4E5f6G7h8 --path .\uploads
140+
ii webrtc
141+
ii webrtc --token A1b2C3d4E5f6G7h8
140142
```
141143

142144
命令行会在主局域网 URL 上方显示进入下载页的二维码,并在 `other:` 下列出其余物理和虚拟网卡的 IPv4 URL;下载页顶部的二维码则直达 `/download`,方便手机扫码下载。网页也可以一次上传多个文件,默认写到启动命令当前目录的 `./ii/`;可用 `--path <目录>` 改为直接写入指定目录,相对路径仍以启动目录为基准,目录会在首次上传时创建。不支持上传目录。目录会下载为 `.tar`。按 `Ctrl+C` 关闭服务。可选 `--token <value>` 为网页、下载和上传加路径访问令牌,令牌只能是 16 到 128 个 ASCII 字母、数字、`-``_`;不带时保持无令牌 URL。该模式没有账号鉴权,只适合临时、可信的局域网。
143145

144146
`ii web` 不带路径时服务启动命令的当前目录;带路径时只接受已有目录。网页提供 nginx 风格的递归目录浏览、普通文件访问和多文件上传,终端输出同样有二维码、主 IPv4 LAN URL 与 `other:` 网卡 URL,但目录网页不显示二维码。`--token``--path` 的规则和 `ii send ... --web` 相同,`-p` 不适用于 `ii web`
145147

148+
`ii webrtc` 开一个局域网浏览器直传房间。终端输出二维码、主 IPv4 LAN URL 与 `other:` 网卡 URL;打开同一 URL 的浏览器会自动显示为临时设备编号,可选择另一个在线设备并发送多个独立文件。文件通过浏览器 WebRTC DataChannel 两端直传,不经过 `ii` 进程,也不会写入启动机器。页面会先实际测试浏览器能否创建 ICE host candidate;浏览器禁用或阻止 WebRTC 时会显示 `WebRTC unavailable`,不会加入房间。它只交换局域网 host candidates,不使用公网 STUN/TURN;网络隔离、防火墙或禁止 P2P 时会连接失败。接收端自动下载,但会先把单个文件聚合在浏览器内存中,因此不适合超出设备可用内存的大文件。可选 `--token <value>` 将页面和信令 URL 固定到路径令牌下。完整说明见 [webrtc.md](webrtc.md)
149+
146150
局域网优先,不走公网中继:
147151

148152
```powershell

SIZE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ All measurements use `cargo build -p ii --release --locked` followed by UPX
2929
| Web-share bidirectional file upload | 5,992,960 | 2,166,784 | +5,632 | `cargo test -p ii --locked` (78 tests), `cargo build -p ii --release --locked`, UPX `5.1.0 --best --lzma`, `upx -t` |
3030
| Web-share path access token | 6,001,664 | 2,169,344 | +2,560 | `cargo test -p ii --locked` (83 tests), `cargo build -p ii --release --locked`, UPX `5.1.0 --best --lzma`, `upx -t` |
3131
| Web upload path and `ii web` directory service | 6,038,016 | 2,180,608 | +11,264 | `cargo test -p ii --locked` (92 tests), `cargo build -p ii --release --locked`, UPX `5.1.0 --best --lzma`, `upx -t` |
32+
| Browser WebRTC LAN signalling and direct page | 6,068,736 | 2,189,824 | +9,216 | `cargo test -p ii --locked` (96 tests), `cargo build -p ii --release --locked`, UPX `5.1.0 --best --lzma`, `upx -t` |
3233

3334
Equivalent release rebuilds have varied by up to `1,536` UPX bytes; the latest
3435
measurement is recorded with the pinned `5.1.0` packer.

ii.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44

55
## 一句话
66

7-
`ii send` 发,`ii recv` 收,`ii web` 开局域网目录,`ii relay` 管中继,`ii doctor` 查问题,`ii version` 看版本。
7+
`ii send` 发,`ii recv` 收,`ii web` 开局域网目录,`ii webrtc` 开浏览器直传,`ii relay` 管中继,`ii doctor` 查问题,`ii version` 看版本。
88

99
## 命令总览
1010

1111
```text
1212
ii send [<path>] [--name <name>] [-t] [-c] [-o <path>] [--web [--token <value>] [--path <dir>] | --s3 | --webdav | --ftp | --sftp] [--profile <name>] [-d] [-p] [--local] [--relay <https-url> [-k]] [--no-relay]
1313
ii web [<目录>] [--token <value>] [--path <目录>]
14+
ii webrtc [--token <value>]
1415
ii recv <ticket> [-o <dir>] [--stdout] [--overwrite] [--resume] [--local] [--trace]
1516
ii relay (--public <https-url> | --tls <domain> --cert <path> --key <path>) [-H <bind-port>]
1617
ii doctor
@@ -152,6 +153,19 @@ ii web .\shared --token A1b2C3d4E5f6G7h8 --path .\uploads
152153

153154
`--token <value>` 把目录、文件和上传 URL 固定到 `/<value>/` 下;遗漏或写错返回 `404`。令牌必须为 16 到 128 个 ASCII 字母、数字、`-``_``--path <目录>` 指定网页上传文件直接写入的目录;相对路径按启动目录解析,首次上传才创建目录,未指定时写入启动目录下的 `./ii/``-p` 不适用于 `ii web`,仍仅用于 WebDAV、FTP、SFTP 的便携 ticket。
154155

156+
## `ii webrtc`
157+
158+
```powershell
159+
ii webrtc
160+
ii webrtc --token A1b2C3d4E5f6G7h8
161+
```
162+
163+
`ii webrtc``0.0.0.0` 的随机端口提供局域网页面。命令行在主 IPv4 LAN URL 上方打印二维码,并在 `other:` 下打印其他物理和虚拟网卡 URL;按 `Ctrl+C` 关闭服务。打开同一 URL 的浏览器自动成为临时编号设备,可以选择任一在线设备并发送多个独立文件;对方自动接收并下载。
164+
165+
`ii` 只在 HTTP 上转发 WebRTC 信令,不接收文件,不写入本机磁盘;文件通过浏览器的可靠、有序 DataChannel 点对点传输。页面加入房间前会实际测试浏览器能否创建 ICE host candidate;浏览器禁用或阻止 WebRTC 时会显示 `WebRTC unavailable`,不会加入房间。它只使用局域网 host candidates,不使用公网 STUN/TURN,也不支持目录、断点续传或 CLI 与浏览器直接传输。访客网络隔离、防火墙、跨网段限制或禁用 P2P 时会无法连接。接收浏览器会把单个文件聚合到内存后再下载,大文件会占用相近大小的内存。完整说明见 [webrtc.md](webrtc.md)
166+
167+
不带 `--token` 时使用根路径;`--token <value>` 将页面和信令接口固定到 `/<value>/` 下,遗漏或写错返回 `404`。令牌必须为 16 到 128 个 ASCII 字母、数字、`-``_``ii webrtc` 不接受路径、`--path``-p`
168+
155169
## `ii recv`
156170

157171
### 用法

src/cli.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub struct Cli {
1111
pub enum Command {
1212
Send(SendArgs),
1313
Web(WebArgs),
14+
Webrtc(WebrtcArgs),
1415
Recv(RecvArgs),
1516
Relay(RelayArgs),
1617
Doctor,
@@ -47,6 +48,11 @@ pub struct WebArgs {
4748
pub web_upload_dir: Option<PathBuf>,
4849
}
4950

51+
#[derive(Debug, Clone, Default)]
52+
pub struct WebrtcArgs {
53+
pub web_token: Option<String>,
54+
}
55+
5056
#[derive(Debug, Clone)]
5157
pub struct RecvArgs {
5258
pub ticket: String,
@@ -147,6 +153,7 @@ where
147153
let command = match command.as_str() {
148154
"send" => Command::Send(parse_send(rest)?),
149155
"web" => Command::Web(parse_web(rest)?),
156+
"webrtc" => Command::Webrtc(parse_webrtc(rest)?),
150157
"recv" => Command::Recv(parse_recv(rest)?),
151158
"relay" => Command::Relay(parse_relay(rest)?),
152159
"doctor" => reject_extra("doctor", rest).map(|_| Command::Doctor)?,
@@ -246,6 +253,42 @@ fn parse_web(args: Vec<String>) -> Result<WebArgs, ParseAction> {
246253
Ok(out)
247254
}
248255

256+
fn parse_webrtc(args: Vec<String>) -> Result<WebrtcArgs, ParseAction> {
257+
let mut out = WebrtcArgs::default();
258+
let mut iter = ArgsIter::new(args);
259+
260+
while let Some(arg) = iter.next() {
261+
match split_long_value(&arg) {
262+
Some(("token", value)) => out.web_token = Some(value.to_string()),
263+
Some((flag, _)) => {
264+
return Err(ParseAction::error(format!("unknown option `--{flag}`")));
265+
}
266+
None => match arg.as_str() {
267+
"-h" | "--help" => return Err(ParseAction::help(WEBRTC_HELP)),
268+
"--token" => out.web_token = Some(iter.value("--token")?),
269+
_ if arg.starts_with('-') => {
270+
return Err(ParseAction::error(format!("unknown option `{arg}`")));
271+
}
272+
_ => {
273+
return Err(ParseAction::error(
274+
"webrtc does not accept positional arguments",
275+
));
276+
}
277+
},
278+
}
279+
}
280+
281+
if let Some(token) = out.web_token.as_deref()
282+
&& !is_valid_web_token(token)
283+
{
284+
return Err(ParseAction::error(
285+
"--token must contain 16 to 128 ASCII letters, digits, '-' or '_'",
286+
));
287+
}
288+
289+
Ok(out)
290+
}
291+
249292
fn parse_recv(args: Vec<String>) -> Result<RecvArgs, ParseAction> {
250293
let mut ticket = None;
251294
let mut out_dir = None;
@@ -527,6 +570,7 @@ ii file transfer
527570
Usage:
528571
ii send [options] [path]
529572
ii web [directory] [--token <value>] [--path <dir>]
573+
ii webrtc [--token <value>]
530574
ii recv [options] <ticket>
531575
ii relay [options]
532576
ii doctor
@@ -567,6 +611,14 @@ Options:
567611
--path <dir>
568612
";
569613

614+
const WEBRTC_HELP: &str = "\
615+
Usage:
616+
ii webrtc [--token <value>]
617+
618+
Options:
619+
--token <value>
620+
";
621+
570622
const RECV_HELP: &str = "\
571623
Usage:
572624
ii recv [options] <ticket>
@@ -814,6 +866,42 @@ mod tests {
814866
));
815867
}
816868

869+
#[test]
870+
fn webrtc_accepts_optional_token() {
871+
let token = "A1b2C3d4E5f6G7h8";
872+
for args in [
873+
vec!["ii", "webrtc"],
874+
vec!["ii", "webrtc", "--token", token],
875+
vec!["ii", "webrtc", "--token=A1b2C3d4E5f6G7h8"],
876+
] {
877+
let cli = Cli::parse_from(args);
878+
match cli.command {
879+
Command::Webrtc(args) => {
880+
if args.web_token.is_some() {
881+
assert_eq!(args.web_token.as_deref(), Some(token));
882+
}
883+
}
884+
_ => panic!("expected webrtc command"),
885+
}
886+
}
887+
}
888+
889+
#[test]
890+
fn webrtc_rejects_invalid_or_unrelated_arguments() {
891+
for args in [
892+
vec!["ii", "webrtc", "shared"],
893+
vec!["ii", "webrtc", "-p"],
894+
vec!["ii", "webrtc", "--path", "uploads"],
895+
vec!["ii", "webrtc", "--token", "too-short"],
896+
vec!["ii", "webrtc", "--token"],
897+
] {
898+
assert!(matches!(
899+
parse_args(args),
900+
Err(ParseAction::Print { code: 2, .. })
901+
));
902+
}
903+
}
904+
817905
#[test]
818906
fn send_accepts_ftp_and_sftp_storage_options() {
819907
let ftp = Cli::parse_from(["ii", "send", "--ftp", "-p", "-d", "file.txt"]);

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub async fn run_cli() -> Result<()> {
2323
match cli.command {
2424
Command::Send(args) => transfer::send(args).await?,
2525
Command::Web(args) => transfer::web(args).await?,
26+
Command::Webrtc(args) => transfer::webrtc(args).await?,
2627
Command::Recv(args) => transfer::recv(args).await?,
2728
Command::Relay(args) => relay::run(args).await?,
2829
Command::Doctor => doctor::run().await?,

0 commit comments

Comments
 (0)