Add Engine.send_file(name, mime, data) and MessageHandler.on_file. Files stream over each open RTC data channel as binary frames — M<json meta> · C<16-byte id><16KB chunk>… · E<id> — and reassemble on the receiver in arrival order (the channel is reliable+ordered). The data channel now distinguishes string (clipboard) from binary (file) messages. Files are direct-only — never relayed, matching prefer-direct. Transport trait gains start(...files) + a default-noop send_file (only RTC implements it). The agent saves received files to ~/Downloads; the apps stub on_file (receive UI is a follow-up). All MessageHandler impls updated. Proven by examples/file_demo.rs: A sends a 50KB blob (4 chunks), B reassembles it to identical bytes over RTC. Follow-ups: file transfer over LAN (same-network without RTC); attach/preview UI; progress + size cap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
57 lines
2.1 KiB
Rust
57 lines
2.1 KiB
Rust
//! Proves chunked file transfer over the RTC data channel. A sends a 50 KB blob
|
|
//! (multiple 16 KB chunks); B reassembles it and gets identical bytes — P2P,
|
|
//! never relayed.
|
|
//! cargo run --example file_demo -- <room>
|
|
|
|
use std::sync::mpsc;
|
|
use std::time::Duration;
|
|
|
|
use tethercore::{Engine, Message, MessageHandler};
|
|
|
|
struct Noop;
|
|
impl MessageHandler for Noop {
|
|
fn on_message(&self, _m: Message) {}
|
|
fn on_status(&self, _c: bool) {}
|
|
fn on_file(&self, _name: String, _mime: String, _data: Vec<u8>) {}
|
|
}
|
|
|
|
struct FileCollector(mpsc::Sender<(String, Vec<u8>)>);
|
|
impl MessageHandler for FileCollector {
|
|
fn on_message(&self, _m: Message) {}
|
|
fn on_status(&self, _c: bool) {}
|
|
fn on_file(&self, name: String, _mime: String, data: Vec<u8>) {
|
|
let _ = self.0.send((name, data));
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let server = std::env::var("TETHER_SERVER").unwrap_or_else(|_| "https://tether.pecord.io".into());
|
|
let room = std::env::args().nth(1).unwrap_or_else(|| "file-demo".into());
|
|
|
|
let payload: Vec<u8> = (0..50_000u32).map(|i| (i % 251) as u8).collect();
|
|
let (tx, rx) = mpsc::channel();
|
|
let a = Engine::new(server.clone(), room.clone(), "dev-a".into(), "macos".into(), "A".into(), "".into());
|
|
let b = Engine::new(server, room, "dev-b".into(), "ios".into(), "B".into(), "".into());
|
|
a.start(Box::new(Noop));
|
|
b.start(Box::new(FileCollector(tx)));
|
|
|
|
eprintln!("waiting ~12s for the RTC data channel…");
|
|
std::thread::sleep(Duration::from_secs(12));
|
|
|
|
eprintln!("A sends a {}-byte file…", payload.len());
|
|
a.send_file("test.bin".into(), "application/octet-stream".into(), payload.clone());
|
|
|
|
match rx.recv_timeout(Duration::from_secs(10)) {
|
|
Ok((name, data)) if name == "test.bin" && data == payload => {
|
|
println!("✅ file received intact over RTC: {name} ({} bytes)", data.len())
|
|
}
|
|
Ok((name, data)) => println!("⚠️ mismatch: name={name} len={} (expected {})", data.len(), payload.len()),
|
|
Err(_) => {
|
|
println!("❌ no file received");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
a.stop();
|
|
b.stop();
|
|
}
|