Skip to content

Commit 4eab4aa

Browse files
committed
✨ initial release
1 parent aab8a2a commit 4eab4aa

File tree

5 files changed

+386
-2
lines changed

5 files changed

+386
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target

Cargo.lock

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

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "ftoc"
3+
version = "0.1.0"
4+
authors = ["Ryan <smbserv@qq.com>"]
5+
edition = "2018"
6+
7+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8+
9+
[dependencies]
10+
clipboard-win="*"
11+
base64="*"

README.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,26 @@
1-
# ftoc
2-
File Transfer over Clipboard, 使用剪贴板传输文件的小工具
1+
# File Transfer over Clipboard (ftoc)
2+
是个通过剪贴板来传输文件的小工具,例如在VMWare Horizon的客户机和宿主机之间实现文件传输
3+
## 使用
4+
5+
接收方先开启ftoc:
6+
```
7+
ftoc
8+
```
9+
10+
发送方:
11+
```
12+
ftoc <file>
13+
```
14+
就可以了。
15+
16+
## 发送参数设置
17+
18+
* --size `n`: 设置单个文件块大小,越大传输越快,支持大小后缀,例如 --size 1k / --size 2m 等,但正常剪贴板有大小限制,注意不要超过。否则文件传输会不完整
19+
20+
* --skip `n`: 断点续传专用,从第`n`个块开始传输
21+
22+
* --send-timeout `n`: 设置块之间的发送间隔,越小传输越快
23+
24+
## 接收参数设置
25+
26+
* --recv-timeout `n`: 设置接收(剪贴板检测)超时,正常要比`--send-timeout`

src/main.rs

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
use base64::{decode_config, encode_config};
2+
use clipboard_win::{get_clipboard_string, set_clipboard_string};
3+
use std::{
4+
env::Args,
5+
fs::File,
6+
io,
7+
io::{prelude::*, BufReader, BufWriter},
8+
path::Path,
9+
};
10+
enum ArgState {
11+
Default,
12+
Size,
13+
Skip,
14+
SendTimeout,
15+
RecvTimeout,
16+
}
17+
18+
#[derive(Debug)]
19+
enum WorkingMode {
20+
Send(AppSendSetting),
21+
Recv(AppRecvSetting),
22+
}
23+
#[derive(Debug)]
24+
struct AppSetting {
25+
working_mode: WorkingMode,
26+
dry_run: bool,
27+
}
28+
29+
#[derive(Debug)]
30+
struct AppSendSetting {
31+
file_path: String,
32+
timeout: u64,
33+
size: usize,
34+
skip: i32,
35+
}
36+
37+
#[derive(Debug)]
38+
struct AppRecvSetting {
39+
timeout: u64,
40+
}
41+
fn parse_args(a: Args) -> Result<AppSetting, String> {
42+
let mut state = ArgState::Default;
43+
let mut send_timeout = 2000;
44+
let mut recv_timeout = 500;
45+
let mut dry_run = false;
46+
let mut file_path = "".to_owned();
47+
let mut unstated_arg_count = 0;
48+
let mut size: usize = 500 * 1024;
49+
let mut skip = 0;
50+
51+
for x in a {
52+
match x.as_str() {
53+
"--size" => {
54+
state = ArgState::Size;
55+
}
56+
"--skip" => {
57+
state = ArgState::Skip;
58+
}
59+
"--send-timeout" => {
60+
state = ArgState::SendTimeout;
61+
}
62+
"--recv-timeout" => {
63+
state = ArgState::RecvTimeout;
64+
}
65+
"--dry-run" => {
66+
dry_run = true;
67+
}
68+
_ => {
69+
match state {
70+
ArgState::Default => {
71+
unstated_arg_count += 1;
72+
if unstated_arg_count > 1 {
73+
if file_path.len() == 0 {
74+
file_path = x.clone();
75+
} else {
76+
// invalid input
77+
return Err(format!("invalid input `{}`", &x));
78+
}
79+
}
80+
}
81+
ArgState::Size => {
82+
let mut multiplier = 1;
83+
let mut len = x.len();
84+
match &x[x.len() - 1..] {
85+
"k" | "K" => multiplier = 1024,
86+
"m" | "M" => multiplier = 1024 * 1024,
87+
"g" | "G" => multiplier = 1024 * 1024 * 1024,
88+
_ => {}
89+
}
90+
91+
if multiplier > 1 {
92+
len = len - 1;
93+
}
94+
let base: usize = x[0..len].parse().expect("invalid size value");
95+
size = base * multiplier;
96+
state = ArgState::Default
97+
}
98+
ArgState::Skip => {
99+
skip = x.parse().expect("invalid skip value");
100+
state = ArgState::Default
101+
}
102+
ArgState::SendTimeout => {
103+
send_timeout = x.parse().expect("invalid timeout value");
104+
state = ArgState::Default
105+
}
106+
ArgState::RecvTimeout => {
107+
recv_timeout = x.parse().expect("invalid timeout value");
108+
state = ArgState::Default
109+
}
110+
}
111+
}
112+
}
113+
}
114+
if unstated_arg_count == 1 {
115+
Ok(AppSetting {
116+
dry_run,
117+
working_mode: WorkingMode::Recv(AppRecvSetting {
118+
timeout: recv_timeout,
119+
}),
120+
})
121+
} else {
122+
Ok(AppSetting {
123+
dry_run,
124+
working_mode: WorkingMode::Send(AppSendSetting {
125+
file_path,
126+
timeout: send_timeout,
127+
skip,
128+
size,
129+
}),
130+
})
131+
}
132+
}
133+
fn main() -> Result<(), String> {
134+
parse_args(std::env::args()).and_then(|x| {
135+
if x.dry_run {
136+
dbg!(x);
137+
Ok(())
138+
} else {
139+
let r = match x.working_mode {
140+
WorkingMode::Send(x) => send_file(&x),
141+
WorkingMode::Recv(x) => recv_file(&x),
142+
};
143+
r.map_err(|e| format!("{}", e))
144+
}
145+
})
146+
}
147+
enum RecvState {
148+
Wait,
149+
Start,
150+
End,
151+
}
152+
fn sleep_ms(ms: u64) {
153+
std::thread::sleep(std::time::Duration::from_millis(ms))
154+
}
155+
fn recv_file(s: &AppRecvSetting) -> Result<(), io::Error> {
156+
let _ = set_clipboard_string("---")?;
157+
let mut writer: Option<BufWriter<File>> = None;
158+
let mut state = RecvState::Wait;
159+
let mut last_index = 0;
160+
let mut has_started = false;
161+
let mut time_wait_ms = 0;
162+
println!("waiting for file");
163+
loop {
164+
match state {
165+
RecvState::Wait => {
166+
if let Ok(x) = get_clipboard_string() {
167+
if x.starts_with("ftoc-start") {
168+
if has_started {
169+
continue;
170+
}
171+
has_started = true;
172+
let x: Vec<&str> = x.split(":").collect();
173+
match File::create(Path::new(x[1])) {
174+
Ok(f) => {
175+
println!("start recv file: {}", x[1]);
176+
writer = Some(BufWriter::new(f));
177+
state = RecvState::Start;
178+
}
179+
Err(e) => {
180+
dbg!(e);
181+
break;
182+
}
183+
}
184+
} else {
185+
sleep_ms(1000);
186+
}
187+
} else {
188+
sleep_ms(100);
189+
continue;
190+
}
191+
}
192+
RecvState::Start => {
193+
if let Ok(x) = get_clipboard_string() {
194+
if x.starts_with("ftoc-end") {
195+
state = RecvState::End;
196+
} else if x.starts_with("ftoc") {
197+
let x: Vec<&str> = x.split(":").collect();
198+
if x.len() < 3 {
199+
sleep_ms(100);
200+
continue;
201+
}
202+
let idx: i32 = x[1].parse().expect("invalid index");
203+
204+
if last_index == idx - 1 {
205+
if let Ok(v) = decode_config(x[2], base64::URL_SAFE_NO_PAD) {
206+
if let Some(x) = &mut writer {
207+
time_wait_ms = 0;
208+
last_index = idx;
209+
println!("recv block {}", idx);
210+
let _ = x.write(v.as_ref());
211+
} else {
212+
println!("warn: block {} write failed", idx)
213+
}
214+
} else {
215+
println!("warn: block {} decode failed", idx)
216+
}
217+
} else {
218+
// wait for missed block or retransmission
219+
time_wait_ms += s.timeout;
220+
if time_wait_ms > 10000 {
221+
println!("warning: recv staled, last_index={}", last_index);
222+
time_wait_ms = 0;
223+
}
224+
}
225+
}
226+
sleep_ms(s.timeout);
227+
} else {
228+
sleep_ms(100);
229+
continue;
230+
}
231+
}
232+
RecvState::End => {
233+
if let Some(x) = &mut writer {
234+
let _ = x.flush();
235+
println!("file saved")
236+
}
237+
break;
238+
}
239+
}
240+
}
241+
Ok(())
242+
}
243+
fn send_file(s: &AppSendSetting) -> Result<(), io::Error> {
244+
let p = Path::new(&s.file_path);
245+
let file = File::open(p)?;
246+
let mut reader = BufReader::new(file);
247+
248+
let mut eof = false;
249+
250+
let mut index = 0;
251+
if s.skip != 0 {
252+
println!("(resume mode)");
253+
}
254+
p.file_name()
255+
.and_then(|x| x.to_str())
256+
.and_then(|x| {
257+
println!("sending file : {}", x);
258+
Some(format!("ftoc-start:{}", x))
259+
})
260+
.and_then(|x| {
261+
let _ = set_clipboard_string(x.as_str());
262+
Some(())
263+
});
264+
sleep_ms(2000);
265+
266+
let mut v = vec![0u8; s.size];
267+
let skip_count = s.skip as usize;
268+
if skip_count > 0 {
269+
reader.seek(io::SeekFrom::Start((skip_count * s.size) as u64))?;
270+
index = skip_count;
271+
}
272+
while !eof {
273+
let _ = reader.read(v.as_mut_slice()).map(|s| {
274+
if s == 0 {
275+
// eof
276+
eof = true;
277+
} else {
278+
index += 1;
279+
let s = encode_config(&v[0..s], base64::URL_SAFE_NO_PAD);
280+
let text = format!("ftoc:{}:{}", index, s);
281+
let _ = set_clipboard_string(text.as_str());
282+
println!("sending block {}", index);
283+
}
284+
});
285+
if eof {
286+
let _ = set_clipboard_string("ftoc-end")?;
287+
println!("file sent");
288+
break;
289+
}
290+
291+
sleep_ms(s.timeout);
292+
}
293+
294+
Ok(())
295+
}

0 commit comments

Comments
 (0)