Skip to content

Commit e2dd312

Browse files
committed
feat: add LAN web sharing
1 parent 9df2aca commit e2dd312

8 files changed

Lines changed: 640 additions & 14 deletions

File tree

Cargo.lock

Lines changed: 9 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: 2 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.1.16"
7+
version = "0.1.17"
88
edition = "2024"
99
rust-version = "1.91"
1010
license = "MIT"
@@ -35,6 +35,7 @@ md5 = { package = "md-5", version = "0.10" }
3535
percent-encoding = "2"
3636
postcard = { version = "1", features = ["use-std"] }
3737
quick-xml = "0.41"
38+
qrcodegen = "1.8.0"
3839
rustls = { version = "0.23.33", default-features = false, features = ["ring"] }
3940
reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider", "stream"] }
4041
digest_auth = "0.3"

README.en.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +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
2627
- Receives resume automatically by default
2728
- Existing files with the same MD5 are skipped
2829
- Folders can be sent directly
@@ -124,6 +125,15 @@ Receive to stdout:
124125
ii recv ii1k7v...x9a --stdout > project.tar.gz
125126
```
126127

128+
Open a temporary LAN download page:
129+
130+
```powershell
131+
ii send .\video.mp4 --web
132+
ii send .\my-folder --web
133+
```
134+
135+
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. Folders download as `.tar` archives. Press `Ctrl+C` to stop the server. This mode has no authentication and is intended only for short-lived, trusted LAN sharing.
136+
127137
Prefer local network paths and avoid public relays:
128138

129139
```powershell

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525

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

28+
也可用 `ii send <文件或文件夹> --web` 临时开一个局域网下载页
29+
2830
断点续收 / 秒传跳过 / 冲突覆盖 / 支持传完清理中转
2931

3032
进度速率 / 完成耗时 / 支持诊断本机 / 支持自建中继
@@ -126,6 +128,15 @@ tar czf - .\project | ii send --name project.tar.gz
126128
ii recv ii1k7v...x9a --stdout > project.tar.gz
127129
```
128130

131+
临时开局域网下载页:
132+
133+
```powershell
134+
ii send .\video.mp4 --web
135+
ii send .\my-folder --web
136+
```
137+
138+
命令行会在主局域网 URL 上方显示进入下载页的二维码,并在 `other:` 下列出其余物理和虚拟网卡的 IPv4 URL;下载页顶部的二维码则直达 `/download`,方便手机扫码下载。目录会下载为 `.tar`。按 `Ctrl+C` 关闭服务。该模式没有鉴权,只适合临时、可信的局域网。
139+
129140
局域网优先,不走公网中继:
130141

131142
```powershell

SIZE.md

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# Binary Size Ledger
22

33
The release artifact is the UPX-compressed CLI executable. The release workflow
4-
verifies the packed artifact and enforces a `1,048,576` byte limit on Windows,
5-
Linux, and macOS.
4+
verifies the packed artifact and reports its size on Windows, Linux, and macOS.
65

76
## Windows x86_64
87

@@ -24,9 +23,19 @@ All measurements use `cargo build -p ii --release --locked` followed by UPX
2423
| Rejected: relay rate-limit feature split | 5,952,000 | 2,154,496 | +1,536 | Full `ii` regression suite (66 tests), full relay-library feature compile, `upx -t`; reverted because packed size grew |
2524
| Final release reproduction | 5,956,096 | 2,153,472 | -512 | `cargo test -p ii --locked` (66 tests), `cargo build -p ii --release --locked`, UPX `5.1.0 --best --lzma`, `upx -t` |
2625
| Single-thread Tokio runtime | 5,946,368 | 2,148,864 | -4,608 | `cargo test -p ii --locked` (66 tests), `cargo build -p ii --release --locked`, UPX `5.1.0 --best --lzma`, `upx -t` |
26+
| LAN web-share QR SVG and terminal QR | 5,970,944 | 2,161,152 | +12,288 | `cargo test -p ii --locked` (76 tests), `cargo build -p ii --release --locked`, UPX `5.1.0 --best --lzma`, `upx -t` |
27+
| Web download QR and responsive share page | 5,975,040 | 2,159,616 | -1,536 | `cargo test -p ii --locked` (76 tests), `cargo build -p ii --release --locked`, UPX `5.1.0 --best --lzma`, `upx -t` |
28+
| Web-share other IPv4 URL list | 5,979,648 | 2,160,640 | +1,024 | `cargo test -p ii --locked` (77 tests), isolated `cargo build -p ii --release --locked`, UPX `5.1.0 --best --lzma`, `upx -t` |
2729

28-
The current artifact remains `1,100,288` bytes above the `1 MiB` budget. Equivalent
29-
release rebuilds have varied by up to `1,536` UPX bytes; the latest measurement is
30-
recorded with the pinned `5.1.0` packer. No
31-
release is considered size-compliant until every target satisfies the workflow
32-
budget check.
30+
Equivalent release rebuilds have varied by up to `1,536` UPX bytes; the latest
31+
measurement is recorded with the pinned `5.1.0` packer.
32+
33+
## QR Dependency Audit
34+
35+
`D:\cache\n0-scan` contains `--depth 1` clones of all 120 public
36+
`n0-computer` repositories. `squiggle` is an empty GitHub repository; all 119
37+
non-empty repositories were scanned with `git grep` for QR-code dependencies and
38+
source usage. The only direct generator found was `iroh-live`'s `qrcode 0.14.1`,
39+
which depends on `image`. `ii` uses `qrcodegen 1.8.0` instead: it only supplies
40+
the QR matrix, while `ii` emits its own inline SVG with no image or front-end
41+
dependency chain.

ii.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
## 命令总览
1010

1111
```text
12-
ii send [<path>] [--name <name>] [-t] [-c] [-o <path>] [--s3 | --webdav | --ftp | --sftp] [--profile <name>] [-d] [-p] [--local] [--relay <https-url> [-k]] [--no-relay]
12+
ii send [<path>] [--name <name>] [-t] [-c] [-o <path>] [--web | --s3 | --webdav | --ftp | --sftp] [--profile <name>] [-d] [-p] [--local] [--relay <https-url> [-k]] [--no-relay]
1313
ii recv <ticket> [-o <dir>] [--stdout] [--overwrite] [--resume] [--local] [--trace]
1414
ii relay (--public <https-url> | --tls <domain> --cert <path> --key <path>) [-H <bind-port>]
1515
ii doctor
@@ -74,9 +74,12 @@ tar czf - .\project | ii send --name project.tar.gz
7474
Windows 使用 `clip.exe`,macOS 使用 `pbcopy`,Linux 会依次尝试 `wl-copy``xclip``xsel`
7575

7676
`-o <path>`
77-
: 把完整的 `ii recv ...` 命令写到指定文件路径。
77+
: 把完整的 `ii recv ...` 命令写到指定文件路径。
7878
如果文件已存在,会覆盖。这个 `-o` 属于 `ii send`,不影响 `ii recv -o <dir>` 的保存目录语义。
7979

80+
`--web`
81+
: 在局域网内临时开放一个无鉴权 HTTP 下载页。执行后会在主 URL 上方展示进入下载页的二维码,随后在 `other:` 下列出其余物理和虚拟网卡的 IPv4 URL;下载页顶部二维码直达 `/download`。按 `Ctrl+C` 停止服务。文件直接下载;文件夹会按原目录名打包为 `.tar` 下载。它不生成 ticket,不能和 `-c``-o``--s3``--webdav``--ftp``--sftp``--local``--relay``--no-relay` 同时使用。
82+
8083
`--local`
8184
: 只走局域网优先路径,不走公网发现,不走公网 relay。
8285

@@ -122,7 +125,7 @@ tar czf - .\project | ii send --name project.tar.gz
122125

123126
### 路径规则
124127

125-
- `--s3``--webdav``--ftp``--sftp``--local``--relay``--no-relay` 互斥。
128+
- `--web``--s3``--webdav``--ftp``--sftp``--local``--relay``--no-relay` 互斥。
126129
- 默认不需要用户选 relay。
127130
- 如果没有局域网或直连可用,默认会自动退到公网 relay。
128131
- 指定 `--relay https://...` 后,当前发送会强制走 HTTPS relay-only,不使用默认公网 relay。

src/cli.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub struct SendArgs {
2929
pub profile: Option<String>,
3030
pub webdav: bool,
3131
pub sftp: bool,
32+
pub web: bool,
3233
pub portable_webdav: bool,
3334
pub local: bool,
3435
pub relay: Option<iroh::RelayUrl>,
@@ -170,6 +171,7 @@ fn parse_send(args: Vec<String>) -> Result<SendArgs, ParseAction> {
170171
"--profile" => out.profile = Some(iter.value("--profile")?),
171172
"--webdav" => out.webdav = true,
172173
"--sftp" => out.sftp = true,
174+
"--web" => out.web = true,
173175
"-p" => out.portable_webdav = true,
174176
"--local" => out.local = true,
175177
"--relay" => out.relay = Some(parse_relay_url(&iter.value("--relay")?)?),
@@ -315,6 +317,7 @@ fn validate_send(args: &SendArgs) -> Result<(), ParseAction> {
315317
args.ftp,
316318
args.webdav,
317319
args.sftp,
320+
args.web,
318321
args.local,
319322
args.relay.is_some(),
320323
args.no_relay,
@@ -325,7 +328,7 @@ fn validate_send(args: &SendArgs) -> Result<(), ParseAction> {
325328

326329
if backend_count > 1 {
327330
return Err(ParseAction::error(
328-
"--s3, --webdav, --ftp, --sftp, --local, --relay and --no-relay conflict with each other",
331+
"--s3, --webdav, --ftp, --sftp, --web, --local, --relay and --no-relay conflict with each other",
329332
));
330333
}
331334

@@ -335,6 +338,12 @@ fn validate_send(args: &SendArgs) -> Result<(), ParseAction> {
335338
if args.accept_self_signed_relay && args.relay.is_none() {
336339
return Err(ParseAction::error("-k requires --relay <https-url>"));
337340
}
341+
if args.web && args.path.is_none() {
342+
return Err(ParseAction::error("--web requires a file or folder path"));
343+
}
344+
if args.web && (args.copy || args.output.is_some()) {
345+
return Err(ParseAction::error("--web cannot be used with -c or -o"));
346+
}
338347

339348
Ok(())
340349
}
@@ -463,6 +472,7 @@ Options:
463472
--webdav
464473
--ftp
465474
--sftp
475+
--web
466476
-p
467477
-d
468478
--profile <name>
@@ -570,6 +580,30 @@ mod tests {
570580
}
571581
}
572582

583+
#[test]
584+
fn send_accepts_web_for_a_file_or_folder() {
585+
let cli = Cli::parse_from(["ii", "send", "photos", "--web"]);
586+
match cli.command {
587+
Command::Send(args) => {
588+
assert!(args.web);
589+
assert_eq!(args.path, Some(PathBuf::from("photos")));
590+
}
591+
_ => panic!("expected send command"),
592+
}
593+
}
594+
595+
#[test]
596+
fn send_rejects_web_without_a_path_or_with_another_mode() {
597+
assert!(matches!(
598+
parse_args(["ii", "send", "--web"]),
599+
Err(ParseAction::Print { code: 2, .. })
600+
));
601+
assert!(matches!(
602+
parse_args(["ii", "send", "file.txt", "--web", "--local"]),
603+
Err(ParseAction::Print { code: 2, .. })
604+
));
605+
}
606+
573607
#[test]
574608
fn send_accepts_ftp_and_sftp_storage_options() {
575609
let ftp = Cli::parse_from(["ii", "send", "--ftp", "-p", "-d", "file.txt"]);

0 commit comments

Comments
 (0)