Skip to content

Commit cf9032a

Browse files
committed
feat: add LAN discovery, DAV, and transfer controls
1 parent 74d66ae commit cf9032a

46 files changed

Lines changed: 2843 additions & 184 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.en.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

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

5+
## 0.3.0 - 2026-08-05
6+
7+
### Added
8+
9+
- `ii send` now supports mixed file/folder sends, `--include`/`--exclude` filters, a global `--rate` cap, and JSON Lines events with `--json`.
10+
- Added `ii discover` LAN discovery and `--bind` IPv4/IPv6 listeners for `ii web`, `ii dav`, and `ii send --web`.
11+
- Added a read-write `ii dav` LAN WebDAV service with desktop-client methods, Range, chunked PUT, and process-local locks.
12+
513
## 0.2.9 - 2026-08-04
614

715
### Changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

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

5+
## 0.3.0 - 2026-08-05
6+
7+
### 新增
8+
9+
- `ii send` 支持多文件/目录混合发送、`--include`/`--exclude` 筛选、`--rate` 总带宽限制和 `--json` JSON Lines 事件。
10+
- 新增 `ii discover` 局域网发现,以及 `ii web``ii dav``ii send --web``--bind` IPv4/IPv6 监听。
11+
- 新增读写 `ii dav` 局域网 WebDAV 服务,支持桌面客户端常用请求、Range、chunked PUT 和进程内锁。
12+
513
## 0.2.9 - 2026-08-04
614

715
### 变更

Cargo.lock

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

Cargo.toml

Lines changed: 4 additions & 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.2.9"
7+
version = "0.3.0"
88
edition = "2024"
99
rust-version = "1.91"
1010
license = "MIT"
@@ -28,6 +28,7 @@ anyhow = "1"
2828
attohttpc = { version = "0.31", default-features = false, features = ["tls-rustls-webpki-roots-ring"] }
2929
base64 = "0.22"
3030
futures-util = "0.3"
31+
glob = "0.3.3"
3132
hmac = "0.12"
3233
iroh = { path = "vendor/iroh/iroh", default-features = false, features = ["tls-ring"] }
3334
iroh-relay = { path = "vendor/iroh/iroh-relay", default-features = false, features = ["server-minimal"] }
@@ -44,6 +45,7 @@ russh = { version = "0.61.0", default-features = false, features = ["ring", "rsa
4445
russh-sftp = "2.3.0"
4546
rcgen = { version = "0.14", default-features = false, features = ["crypto", "pem", "ring"] }
4647
serde = { version = "1", features = ["derive"] }
48+
socket2 = "0.6"
4749
sha2 = "0.10"
4850
suppaftp = { path = "vendor/suppaftp", default-features = false, features = ["tokio"] }
4951
tar = { version = "0.4", default-features = false }
@@ -57,6 +59,7 @@ url = { version = "2.5", features = ["serde"] }
5759

5860
[dev-dependencies]
5961
libunftp = { version = "0.23.0", default-features = false, features = ["ring"] }
62+
serde_json = "1"
6063
unftp-sbe-fs = "0.4.0"
6164

6265
[build-dependencies]

README.en.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
`ii` is built for temporary file transfer:
2323

2424
- The sender serves one successful receive by default, then exits
25-
- P2P direct / LAN discovery / public relay fallback / optional S3, WebDAV, FTP, and SFTP backends
26-
- `ii send <file-or-folder> --web` download sharing / `ii web [directory]` directory browsing / `ii webrtc` browser-to-browser transfer / `ii tunnel` TCP port forwarding
25+
- P2P direct / LAN discovery with `ii discover` / public relay fallback / optional S3, WebDAV, FTP, and SFTP backends
26+
- `ii send <file-or-folder> --web` download sharing / `ii web [directory]` directory browsing / `ii dav [directory]` WebDAV serving / `ii webrtc` browser-to-browser transfer / `ii tunnel` TCP port forwarding
2727
- Receives resume automatically by default
2828
- Existing files with the same MD5 are skipped
2929
- Folders can be sent directly
@@ -125,7 +125,7 @@ ii web .\shared
125125
ii webrtc
126126
```
127127

128-
All three print LAN URLs, other adapter URLs, and a terminal QR code. Without a directory, `ii web` serves the current directory. `--port 8080` fixes the port; bare `--token` generates a path token, while `--token <value>` supplies one. `ii send --web` and `ii web` are read-only by default; pass `--upload` to enable multi-file uploads and `--path <dir>` to choose the upload directory. These services have no account authentication and are for short-lived, trusted LAN use only. See [ii.md](ii.md) for the full rules and [webrtc.md](webrtc.md) for WebRTC limits.
128+
All three print LAN URLs, other adapter URLs, and a terminal QR code. Without a directory, `ii web` serves the current directory. `--port 8080` fixes the port; `--bind ::` selects IPv6 only. Bare `--token` generates a path token, while `--token <value>` supplies one. `ii send --web` and `ii web` are read-only by default; pass `--upload` to enable multi-file uploads and `--path <dir>` to choose the upload directory. `ii discover` listens on the local network for three seconds and lists `ii send -t`, `ii web`, and `ii dav` services; discovery exposes tickets or URLs to the LAN and is not access control. These services have no account authentication and are for short-lived, trusted LAN use only. See [ii.md](ii.md) for the full rules and [webrtc.md](webrtc.md) for WebRTC limits.
129129

130130
### TCP Tunnel
131131

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323

2424
直发目录 / 一次即走 / 持续发送 / 自动复制到粘贴板或落盘
2525

26-
P2P 直连 / 局域网发现 / 公网 relay 回退 / 支持 `--s3` / `--webdav` / `--ftp` / `--sftp` 后端中转
26+
P2P 直连 / 局域网发现 / `ii discover` / 公网 relay 回退 / 支持 `--s3` / `--webdav` / `--ftp` / `--sftp` 后端中转
2727

28-
`ii send <文件或文件夹> --web` 分享下载 / `ii web [目录]` 浏览目录 / `ii webrtc` 浏览器直传 / `ii tunnel` TCP 端口转发
28+
`ii send <文件或文件夹> --web` 分享下载 / `ii web [目录]` 浏览目录 / `ii dav [目录]` 挂载目录 / `ii webrtc` 浏览器直传 / `ii tunnel` TCP 端口转发
2929

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

@@ -128,7 +128,7 @@ ii web .\shared
128128
ii webrtc
129129
```
130130

131-
三者都会打印 LAN URL、其他网卡 URL 和终端二维码。`ii web` 省略目录时服务当前目录。`--port 8080` 固定端口;裸 `--token` 生成路径令牌,也可用 `--token <value>` 指定。`ii send --web``ii web` 默认只读,传入 `--upload` 才开放多文件上传;`--path <dir>` 指定上传目录。网页服务没有账号鉴权,只适合临时可信局域网。完整规则见 [ii.md](ii.md),WebRTC 限制见 [webrtc.md](webrtc.md)
131+
三者都会打印 LAN URL、其他网卡 URL 和终端二维码。`ii web` 省略目录时服务当前目录。`--port 8080` 固定端口`--bind ::` 只监听 IPv6;裸 `--token` 生成路径令牌,也可用 `--token <value>` 指定。`ii send --web``ii web` 默认只读,传入 `--upload` 才开放多文件上传;`--path <dir>` 指定上传目录`ii discover` 监听本地网络三秒,列出带 `-t` 的发送端、`ii web``ii dav`;发现公告会向同一 LAN 暴露 ticket 或 URL,不是访问控制。网页服务没有账号鉴权,只适合临时可信局域网。完整规则见 [ii.md](ii.md),WebRTC 限制见 [webrtc.md](webrtc.md)
132132

133133
### TCP 隧道
134134

SIZE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ All measurements use `cargo build -p ii --release --locked` followed by UPX
3232
| 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` |
3333
| `ii web` Range, media MIME, and HEAD responses | 6,078,464 | 2,194,432 | +4,608 | `cargo test -p ii --locked` (98 tests), `cargo build -p ii --release --locked`, UPX `5.1.0 --best --lzma`, `upx -t` |
3434
| `ii webrtc` text messages | 6,124,544 | 2,204,672 | +512 | Compared with the same `0.2.2` tunnel baseline (6,120,448 / 2,204,160); `cargo test -p ii --locked` (104 tests), `cargo build -p ii --release --locked`, UPX `5.1.0 --best --lzma`, `upx -t` |
35+
| Seven extension plan: multi-send, filters, rate, JSON, discover, bind, DAV | 6,315,008 | 2,265,600 | +60,928 | `cargo test -p ii --offline --no-fail-fast -- --test-threads=1` (121 tests), workspace check, `cargo build -p ii --release --locked`, `cargo bloat --crates`, UPX `5.1.0 --best --lzma`, `upx -t` |
3536

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

gui/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,7 @@ fn install_transfer_handlers(
609609
resume: false,
610610
local: false,
611611
trace: false,
612+
json: false,
612613
};
613614
let sender = background_sender.clone();
614615
thread::spawn(move || {

ii.md

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,21 @@
44

55
## 一句话
66

7-
`ii send` 发,`ii recv` 收,`ii web` 开局域网目录,`ii webrtc` 开浏览器直传,`ii tunnel` 转发 TCP,`ii relay` 管中继,`ii doctor` 查问题,`ii version` 看版本。
7+
`ii send` 发,`ii recv` 收,`ii web` 开局域网目录,`ii dav` 开 WebDAV,`ii discover` 找本地服务,`ii webrtc` 开浏览器直传,`ii tunnel` 转发 TCP,`ii relay` 管中继,`ii doctor` 查问题,`ii version` 看版本。
88

99
## 命令总览
1010

1111
```text
1212
ii help [<command>]
13-
ii send [<path>] [--name <name>] [-t] [-c] [-o <path>] [--web [--port <port>] [--token [<value>]] [--upload] [--path <dir>] | --s3 | --webdav | --ftp | --sftp] [--profile <name>] [-d] [-p] [--local] [--relay <url> [-k]] [--no-relay]
14-
ii web [<目录>] [--port <port>] [--token [<value>]] [--upload] [--path <目录>]
15-
ii webrtc [--port <port>] [--token [<value>]]
13+
ii send [<path>...] [--name <name>] [--include <glob>] [--exclude <glob>] [--rate <bytes/s>] [--json] [-t] [-c] [-o <path>] [--web [--port <port>] [--bind <ip>] [--token [<value>]] [--upload] [--path <path>] | --s3 | --webdav | --ftp | --sftp] [--profile <name>] [-d] [-p] [--local] [--relay <url> [-k]] [--no-relay]
14+
ii web [<目录>] [--port <port>] [--bind <ip>] [--token [<value>]] [--upload] [--path <目录>]
15+
ii dav [<目录>] [--port <port>] [--bind <ip>] [--token [<value>]] [--read-only]
16+
ii webrtc [--port <port>] [--bind <ip>] [--token [<value>]]
1617
ii tunnel -s <target-host:port> [--relay <url> [-k]]
1718
ii tunnel -c <ticket> [--listen <ip:port>]
1819
ii recv <ticket> [-o <dir>] [--stdout] [--overwrite] [--resume] [--local] [--trace]
1920
ii relay [--port <port>] [--tls [--domain <name>] [--cert <path> --key <path>]]
21+
ii discover [--json]
2022
ii doctor
2123
ii version
2224
```
@@ -99,6 +101,21 @@ tar czf - .\project | ii send --name project.tar.gz
99101
`--path <dir>`
100102
: 仅和 `--web --upload` 同用,指定网页上传文件直接写入的目录。相对路径以启动命令当前目录为基准;目录在首次上传时创建。不提供时仍写入当前目录的 `./ii/`。未提供 `--upload` 时本参数会被忽略。`-p` 仍是 FTP/SFTP/WebDAV 的便携 ticket 参数。
101103

104+
`<path>...`
105+
: 可一次发送多个文件或文件夹。多个输入使用现有目录 ticket 打包到集合根目录,默认根名为 `ii`,可用 `--name` 覆盖;stdin 不能和路径混用,顶层名称重复会报错。
106+
107+
`--include <glob>` / `--exclude <glob>`
108+
: 可重复使用,仅作用于目录和多路径归档。glob 按每个输入项内部的 `/` 相对路径匹配,exclude 永远优先;筛选后为空会报错。
109+
110+
`--rate <bytes/s>`
111+
: 限制发送端总带宽,支持正整数 bytes/s 以及 `KiB``MiB``GiB` 后缀;多接收端共享同一个上限。
112+
113+
`--json`
114+
: 输出稳定 JSON Lines;stdout 只输出事件,诊断和错误走 stderr。`recv --stdout --json` 不允许使用;JSON 后端缺 profile 时直接失败,不弹配置提示。
115+
116+
`--bind <ip>`
117+
: 仅和 `--web` 同用,固定监听 IPv4 或 IPv6 地址。未提供时保持 `0.0.0.0` 并打印主 LAN URL 与 `other:`;显式地址只打印该地址,IPv6 URL 使用方括号。
118+
102119
`--local`
103120
: 只走局域网优先路径,不走公网发现,不走公网 relay。
104121

@@ -160,10 +177,29 @@ ii web .\shared --port 8080 --token A1b2C3d4E5f6G7h8 --upload --path .\uploads
160177

161178
`ii web` 不带路径时服务启动命令当前目录;带路径时必须是已有目录,文件路径会报错。网页默认提供 nginx 风格的递归目录浏览:目录可继续进入、`../` 返回父目录、每项显示名称、修改时间和大小;普通文件直接响应,不强制下载。传入 `--upload` 后网页顶部才显示多文件上传控件,不支持上传目录。
162179

163-
命令行会在主 IPv4 LAN URL 上方打印根页二维码,并在 `other:` 下打印其他物理和虚拟网卡 URL;网页不显示二维码。按 `Ctrl+C` 关闭服务。
180+
命令行会在主 IPv4 LAN URL 上方打印根页二维码,并在 `other:` 下打印其他物理和虚拟网卡 URL;网页不显示二维码。`--bind <ip>` 可固定 IPv4 或 IPv6 listener;显式 bind 只打印该地址。`Ctrl+C` 关闭服务。
164181

165182
`--port <port>` 指定 `1``65535` 的 HTTP 监听端口,未提供时随机选择。裸 `--token` 自动生成 32 字符路径令牌;`--token <value>``--token=<value>` 使用指定令牌,把目录、文件和已启用的上传 URL 固定到 `/<value>/` 下;遗漏或写错返回 `404`。显式令牌必须为 16 到 128 个 ASCII 字母、数字、`-``_``--upload` 才显示上传控件并开放上传接口;`--path <目录>` 指定上传文件直接写入的目录,相对路径按启动目录解析,首次上传才创建目录,未指定时写入启动目录下的 `./ii/`,未提供 `--upload` 时会被忽略。`-p` 不适用于 `ii web`,仍仅用于 WebDAV、FTP、SFTP 的便携 ticket。
166183

184+
## `ii dav`
185+
186+
```powershell
187+
ii dav
188+
ii dav .\shared --port 8080 --bind 192.168.1.20 --token A1b2C3d4E5f6G7h8
189+
ii dav .\shared --read-only
190+
```
191+
192+
`ii dav` 把当前目录或指定目录作为局域网 WebDAV 根目录,默认可读写;支持 `OPTIONS``PROPFIND``GET``HEAD`、Range、`PUT``MKCOL``DELETE``MOVE``COPY``LOCK``UNLOCK`,上传支持 `Content-Length`、chunked body 和 `100-continue``--read-only` 禁止所有改写。`--token` 是 URL 路径令牌,不是账户认证;服务默认无 TLS 和 Basic Auth,只适合可信局域网。
193+
194+
## `ii discover`
195+
196+
```powershell
197+
ii discover
198+
ii discover --json
199+
```
200+
201+
在本地网络监听三秒,发现正在运行的 `ii send -t``ii web``ii dav`,并输出可直接使用的 `ii recv <ticket>` 或服务 URL。发现同时使用 IPv4 广播和 IPv6 链路本地 all-nodes 多播;公告会向同一 LAN 暴露 ticket 或 token URL,不是访问控制机制。`--json` 输出 JSON Lines,stdout 不混入其他文本。
202+
167203
## `ii webrtc`
168204

169205
```powershell

src/backend/ftp.rs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ use crate::{
55
ticket::{FtpPortableCredentials, PayloadKind, Ticket},
66
transport::{
77
p2p::{FilePlan, RecvTrace},
8-
progress::{TransferProgress, copy_with_progress},
8+
progress::{RateLimiter, TransferProgress, copy_with_progress, copy_with_progress_limited},
99
source::{Source, unique_object_id},
1010
},
1111
};
1212
use anyhow::{Context, Result, bail};
13-
use std::path::PathBuf;
13+
use std::{path::PathBuf, sync::Arc};
1414
use suppaftp::tokio::AsyncFtpStream;
1515
use tempfile::NamedTempFile;
1616
use tokio::{
@@ -26,6 +26,7 @@ pub(crate) async fn upload(
2626
source: &Source,
2727
profile: &storage::FtpProfile,
2828
show_progress: bool,
29+
rate_limiter: Option<Arc<RateLimiter>>,
2930
) -> Result<FtpUpload> {
3031
let object_key = remote_object_key(&profile.remote_dir, source);
3132
let mut client = connect(profile).await?;
@@ -41,9 +42,14 @@ pub(crate) async fn upload(
4142
.await
4243
.with_context(|| format!("upload FTP object {object_key}"))?;
4344
let mut progress = TransferProgress::new("ii send", show_progress, source.size(), 0);
44-
copy_with_progress(&mut source_file, &mut stream, &mut progress)
45-
.await
46-
.with_context(|| format!("upload FTP object {object_key}"))?;
45+
copy_with_progress_limited(
46+
&mut source_file,
47+
&mut stream,
48+
&mut progress,
49+
rate_limiter.as_deref(),
50+
)
51+
.await
52+
.with_context(|| format!("upload FTP object {object_key}"))?;
4753
stream.flush().await.context("flush FTP upload")?;
4854
progress.finish();
4955
client
@@ -125,7 +131,11 @@ pub(crate) async fn recv_ftp(
125131
(profile, Some(save))
126132
}
127133
None => {
128-
let selection = storage::load_or_prompt_ftp_profile_named(&ftp.profile)?;
134+
let selection = if args.json {
135+
storage::load_ftp_profile_noninteractive(Some(&ftp.profile))?
136+
} else {
137+
storage::load_or_prompt_ftp_profile_named(&ftp.profile)?
138+
};
129139
let save = selection
130140
.save_after_success
131141
.then_some((selection.path.clone(), selection.config.clone()));
@@ -221,6 +231,7 @@ fn portable_ftp_config(
221231
pub(crate) async fn try_delete_ftp_for_ticket(
222232
ftp: crate::ticket::FtpTicket,
223233
trace: &mut RecvTrace,
234+
noninteractive: bool,
224235
) {
225236
if !ftp.delete_after_recv {
226237
return;
@@ -233,7 +244,11 @@ pub(crate) async fn try_delete_ftp_for_ticket(
233244
(profile, Some(save))
234245
}
235246
None => {
236-
let selection = storage::load_or_prompt_ftp_profile_named(&ftp.profile)?;
247+
let selection = if noninteractive {
248+
storage::load_ftp_profile_noninteractive(Some(&ftp.profile))?
249+
} else {
250+
storage::load_or_prompt_ftp_profile_named(&ftp.profile)?
251+
};
237252
let save = selection
238253
.save_after_success
239254
.then_some((selection.path.clone(), selection.config.clone()));
@@ -436,7 +451,7 @@ mod tests {
436451
password: "pass".to_string(),
437452
remote_dir: "ii/".to_string(),
438453
};
439-
let upload = upload(&source, &profile, false).await.unwrap();
454+
let upload = upload(&source, &profile, false, None).await.unwrap();
440455

441456
let destination = root.path().join("received.txt");
442457
let mut client = connect(&profile).await.unwrap();

0 commit comments

Comments
 (0)