1use crate::device::ADB;
2use crate::error::{ADBError, ADBResult};
3use log::{debug, info};
4use std::fs::{self, File};
5use std::io::{Read, Write};
6use std::path::Path;
7use std::process::Command;
8
9#[derive(Debug, Clone)]
11pub struct TransferOptions {
12 pub compression: bool, pub compression_algorithm: Option<String>, pub sync: bool, pub dry_run: bool, pub preserve_timestamp: bool, pub chunk_size: usize, }
26
27impl Default for TransferOptions {
28 fn default() -> Self {
29 TransferOptions {
30 compression: false,
31 compression_algorithm: None,
32 sync: false,
33 dry_run: false,
34 preserve_timestamp: false,
35 chunk_size: 65536, }
37 }
38}
39
40impl ADB {
41 pub fn pull(
43 &self,
44 device_id: &str,
45 device_path: &str,
46 local_path: &str,
47 options: Option<TransferOptions>,
48 ) -> ADBResult<()> {
49 let options = options.unwrap_or_default();
50
51 self.with_retry(|| {
52 let mut cmd = Command::new(&self.config.path);
53
54 if !device_id.is_empty() {
56 cmd.arg("-s").arg(device_id);
57 }
58
59 cmd.arg("pull");
60
61 if options.preserve_timestamp {
63 cmd.arg("-a");
64 }
65
66 if options.compression {
68 if let Some(algorithm) = &options.compression_algorithm {
69 cmd.arg("-z").arg(algorithm);
70 } else {
71 cmd.arg("-z").arg("any");
72 }
73 } else {
74 cmd.arg("-Z");
75 }
76
77 cmd.arg(device_path).arg(local_path);
79
80 info!("开始从设备拉取文件: {} -> {}", device_path, local_path);
81 let output = cmd
82 .output()
83 .map_err(|e| ADBError::CommandError(format!("执行 ADB pull 命令失败: {}", e)))?;
84
85 if !output.status.success() {
86 let stderr = String::from_utf8_lossy(&output.stderr);
87 return Err(ADBError::CommandError(format!(
88 "ADB pull 命令失败: {}",
89 stderr
90 )));
91 }
92
93 debug!("成功拉取文件 {} 到 {}", device_path, local_path);
94 Ok(())
95 })
96 }
97
98 pub fn push(
100 &self,
101 device_id: &str,
102 local_path: &str,
103 device_path: &str,
104 options: Option<TransferOptions>,
105 ) -> ADBResult<()> {
106 let options = options.unwrap_or_default();
107
108 self.with_retry(|| {
109 let mut cmd = Command::new(&self.config.path);
110
111 if !device_id.is_empty() {
113 cmd.arg("-s").arg(device_id);
114 }
115
116 cmd.arg("push");
117
118 if options.sync {
120 cmd.arg("--sync");
121 }
122
123 if options.dry_run {
124 cmd.arg("-n");
125 }
126
127 if options.compression {
129 if let Some(algorithm) = &options.compression_algorithm {
130 cmd.arg("-z").arg(algorithm);
131 } else {
132 cmd.arg("-z").arg("any");
133 }
134 } else {
135 cmd.arg("-Z");
136 }
137
138 cmd.arg(local_path).arg(device_path);
140
141 info!("开始向设备推送文件: {} -> {}", local_path, device_path);
142 let output = cmd
143 .output()
144 .map_err(|e| ADBError::CommandError(format!("执行 ADB push 命令失败: {}", e)))?;
145
146 if !output.status.success() {
147 let stderr = String::from_utf8_lossy(&output.stderr);
148 return Err(ADBError::CommandError(format!(
149 "ADB push 命令失败: {}",
150 stderr
151 )));
152 }
153
154 debug!("成功推送文件 {} 到 {}", local_path, device_path);
155 Ok(())
156 })
157 }
158
159 pub fn push_large_file(
161 &self,
162 device_id: &str,
163 local_path: &str,
164 device_path: &str,
165 options: Option<TransferOptions>,
166 ) -> ADBResult<()> {
167 let options = options.unwrap_or_default();
168 let chunk_size = options.chunk_size;
169
170 let file_path = Path::new(local_path);
172 if !file_path.exists() || !file_path.is_file() {
173 let error_msg = format!("文件不存在: {}", local_path);
174 return Err(ADBError::FileError(error_msg));
175 }
176
177 let file_size = fs::metadata(file_path)?.len() as usize;
179
180 if file_size <= chunk_size {
182 debug!("文件大小较小,使用标准推送");
183 return self.push(device_id, local_path, device_path, Some(options));
184 }
185
186 let mut file = File::open(file_path).map_err(|e| {
188 let error_msg = format!("无法打开文件 {}: {}", local_path, e);
189 ADBError::FileError(error_msg)
190 })?;
191
192 info!(
193 "将文件 {} 分成 {} 块传输",
194 local_path,
195 (file_size + chunk_size - 1) / chunk_size
196 );
197
198 let device_temp_dir = format!("{}.parts", device_path);
200 self.shell(device_id, &format!("mkdir -p {}", device_temp_dir))?;
201
202 let temp_dir = crate::utils::create_temp_dir_path("adb_push")?;
204
205 let mut buffer = vec![0u8; chunk_size];
206 let chunks_count = (file_size + chunk_size - 1) / chunk_size;
207
208 let chunk_options = options.clone();
210
211 for i in 0..chunks_count {
213 let part_file = temp_dir.join(format!("part{}", i));
214 let bytes_read = file.read(&mut buffer[..]).map_err(|e| {
215 let error_msg = format!("读取文件块失败: {}", e);
216 ADBError::FileError(error_msg)
217 })?;
218
219 {
221 let mut part = File::create(&part_file).map_err(|e| {
222 let error_msg = format!("创建临时文件失败: {}", e);
223 ADBError::FileError(error_msg)
224 })?;
225
226 part.write_all(&buffer[..bytes_read]).map_err(|e| {
227 let error_msg = format!("写入临时文件失败: {}", e);
228 ADBError::FileError(error_msg)
229 })?;
230 }
231
232 let device_part_path = format!("{}/part{}", device_temp_dir, i);
234 let push_result = self.push(
235 device_id,
236 part_file.to_str().unwrap(),
237 &device_part_path,
238 Some(chunk_options.clone()),
239 );
240
241 let _ = fs::remove_file(part_file);
243
244 push_result.map_err(|e| {
246 let error_msg = format!("推送文件块失败: {}", e);
247 ADBError::CommandError(error_msg)
248 })?;
249
250 debug!("已推送块 {}/{}", i + 1, chunks_count);
251 }
252
253 let cat_cmd = format!(
255 "cat {}/* > {} && rm -rf {}",
256 device_temp_dir, device_path, device_temp_dir
257 );
258 self.shell(device_id, &cat_cmd)?;
259
260 info!("已成功推送和合并大文件 {} 到 {}", local_path, device_path);
261
262 let _ = fs::remove_dir_all(temp_dir);
264
265 Ok(())
266 }
267
268 pub fn file_exists(&self, device_id: &str, path: &str) -> ADBResult<bool> {
270 let result = self.shell(
271 device_id,
272 &format!("[ -e {} ] && echo 'exists' || echo 'not exists'", path),
273 )?;
274 Ok(result.trim() == "exists")
275 }
276
277 pub fn get_file_size(&self, device_id: &str, path: &str) -> ADBResult<u64> {
279 let is_dir = self
281 .shell(
282 device_id,
283 &format!("[ -d {} ] && echo 'true' || echo 'false'", path),
284 )?
285 .trim()
286 == "true";
287
288 if is_dir {
289 let output = self.shell(device_id, &format!("du -sk {} | cut -f1", path))?;
291 let size_kb = output
292 .trim()
293 .parse::<u64>()
294 .map_err(|_| ADBError::CommandError(format!("无法获取目录大小: {}", path)))?;
295
296 Ok(size_kb * 1024) } else {
298 let output = self.shell(device_id, &format!("wc -c < {}", path))?;
300 let size = output
301 .trim()
302 .parse::<u64>()
303 .map_err(|_| ADBError::CommandError(format!("无法获取文件大小: {}", path)))?;
304
305 Ok(size)
306 }
307 }
308
309 pub fn create_directory(&self, device_id: &str, path: &str) -> ADBResult<()> {
311 self.shell(device_id, &format!("mkdir -p {}", path))?;
313
314 let exists = self.file_exists(device_id, path)?;
316 if !exists {
317 return Err(ADBError::CommandError(format!("无法创建目录: {}", path)));
318 }
319
320 Ok(())
321 }
322
323 pub fn remove_path(&self, device_id: &str, path: &str, recursive: bool) -> ADBResult<()> {
325 let exists = self.file_exists(device_id, path)?;
327 if !exists {
328 return Err(ADBError::CommandError(format!("路径不存在: {}", path)));
329 }
330
331 let is_dir = self
333 .shell(
334 device_id,
335 &format!("[ -d {} ] && echo 'true' || echo 'false'", path),
336 )?
337 .trim()
338 == "true";
339
340 if is_dir {
341 if recursive {
342 self.shell(device_id, &format!("rm -rf {}", path))?;
344 } else {
345 let output = self.shell(device_id, &format!("rmdir {}", path));
347
348 if let Err(e) = &output {
350 if let ADBError::DeviceError(msg) = e {
351 if msg.contains("Directory not empty") {
352 return Err(ADBError::CommandError(
353 "目录不为空,使用 recursive=true 递归删除".to_string(),
354 ));
355 }
356 }
357 }
358
359 output?;
360 }
361 } else {
362 self.shell(device_id, &format!("rm {}", path))?;
364 }
365
366 let still_exists = self.file_exists(device_id, path)?;
368 if still_exists {
369 return Err(ADBError::CommandError(format!("无法删除路径: {}", path)));
370 }
371
372 Ok(())
373 }
374
375 pub fn copy_on_device(&self, device_id: &str, src_path: &str, dst_path: &str) -> ADBResult<()> {
377 if !self.file_exists(device_id, src_path)? {
379 return Err(ADBError::FileError(format!("源文件不存在: {}", src_path)));
380 }
381
382 let command = format!("cp -f {} {}", src_path, dst_path);
384 self.shell(device_id, &command)?;
385
386 if !self.file_exists(device_id, dst_path)? {
388 return Err(ADBError::CommandError(format!(
389 "复制文件失败: {} -> {}",
390 src_path, dst_path
391 )));
392 }
393
394 Ok(())
395 }
396
397 pub fn move_on_device(&self, device_id: &str, src_path: &str, dst_path: &str) -> ADBResult<()> {
399 if !self.file_exists(device_id, src_path)? {
401 return Err(ADBError::FileError(format!("源文件不存在: {}", src_path)));
402 }
403
404 let command = format!("mv -f {} {}", src_path, dst_path);
406 self.shell(device_id, &command)?;
407
408 if self.file_exists(device_id, src_path)? || !self.file_exists(device_id, dst_path)? {
410 return Err(ADBError::CommandError(format!(
411 "移动文件失败: {} -> {}",
412 src_path, dst_path
413 )));
414 }
415
416 Ok(())
417 }
418
419 pub fn list_directory(&self, device_id: &str, path: &str) -> ADBResult<Vec<String>> {
421 let exists = self.file_exists(device_id, path)?;
423 let is_dir = self
424 .shell(
425 device_id,
426 &format!("[ -d {} ] && echo 'true' || echo 'false'", path),
427 )?
428 .trim()
429 == "true";
430
431 if !exists || !is_dir {
432 return Err(ADBError::FileError(format!(
433 "路径不存在或不是目录: {}",
434 path
435 )));
436 }
437
438 let output = self.shell(device_id, &format!("ls -A {}", path))?;
440 let files = output
441 .lines()
442 .map(|s| s.trim().to_string())
443 .filter(|s| !s.is_empty())
444 .collect();
445
446 Ok(files)
447 }
448
449 pub fn get_file_mtime(&self, device_id: &str, path: &str) -> ADBResult<String> {
451 if !self.file_exists(device_id, path)? {
453 return Err(ADBError::FileError(format!("文件不存在: {}", path)));
454 }
455
456 let output = self.shell(device_id, &format!("stat -c %y {}", path))?;
458 Ok(output.trim().to_string())
459 }
460
461 pub fn get_available_space(&self, device_id: &str, path: &str) -> ADBResult<u64> {
463 let output = self.shell(device_id, &format!("df -k {} | tail -1", path))?;
465 let parts: Vec<&str> = output.split_whitespace().collect();
466
467 if parts.len() < 4 {
468 return Err(ADBError::CommandError("无法解析可用空间信息".to_string()));
469 }
470
471 let available_kb = parts[3]
473 .parse::<u64>()
474 .map_err(|_| ADBError::CommandError("无法解析可用空间值".to_string()))?;
475
476 Ok(available_kb * 1024)
478 }
479
480 pub fn compute_md5(&self, device_id: &str, path: &str) -> ADBResult<String> {
482 if !self.file_exists(device_id, path)? {
484 return Err(ADBError::FileError(format!("文件不存在: {}", path)));
485 }
486
487 let is_dir = self
489 .shell(
490 device_id,
491 &format!("[ -d {} ] && echo 'true' || echo 'false'", path),
492 )?
493 .trim()
494 == "true";
495
496 if is_dir {
497 return Err(ADBError::CommandError("MD5 计算不支持目录".to_string()));
498 }
499
500 let output = self.shell(device_id, &format!("md5sum {}", path))?;
502 let parts: Vec<&str> = output.split_whitespace().collect();
503
504 if parts.is_empty() {
505 return Err(ADBError::CommandError("无法计算 MD5".to_string()));
506 }
507
508 Ok(parts[0].to_string())
509 }
510
511 pub fn write_text_to_file(&self, device_id: &str, path: &str, content: &str) -> ADBResult<()> {
513 if let Some(parent_dir) = Path::new(path).parent().and_then(|p| p.to_str()) {
515 if !parent_dir.is_empty() {
516 let _ = self.create_directory(device_id, parent_dir);
517 }
518 }
519
520 let escaped_content = content.replace("\"", "\\\"").replace("\n", "\\n");
522 let command = format!("echo -e \"{}\" > {}", escaped_content, path);
523 self.shell(device_id, &command)?;
524
525 if !self.file_exists(device_id, path)? {
527 return Err(ADBError::CommandError(format!("写入文件失败: {}", path)));
528 }
529
530 Ok(())
531 }
532
533 pub fn read_text_from_file(&self, device_id: &str, path: &str) -> ADBResult<String> {
535 if !self.file_exists(device_id, path)? {
537 return Err(ADBError::FileError(format!("文件不存在: {}", path)));
538 }
539
540 let content = self.shell(device_id, &format!("cat {}", path))?;
542 Ok(content)
543 }
544
545 pub fn compare_files(
547 &self,
548 device_id: &str,
549 local_path: &str,
550 device_path: &str,
551 ) -> ADBResult<bool> {
552 let local_file_path = Path::new(local_path);
554 if !local_file_path.exists() || !local_file_path.is_file() {
555 return Err(ADBError::FileError(format!(
556 "本地文件不存在: {}",
557 local_path
558 )));
559 }
560
561 if !self.file_exists(device_id, device_path)? {
563 return Err(ADBError::FileError(format!(
564 "设备文件不存在: {}",
565 device_path
566 )));
567 }
568
569 let local_md5 = match std::process::Command::new("md5sum")
571 .arg(local_path)
572 .output()
573 {
574 Ok(output) => {
575 if output.status.success() {
576 let stdout = String::from_utf8_lossy(&output.stdout);
577 let parts: Vec<&str> = stdout.split_whitespace().collect();
578 if !parts.is_empty() {
579 parts[0].to_string()
580 } else {
581 return Err(ADBError::CommandError("无法计算本地文件 MD5".to_string()));
582 }
583 } else {
584 return Err(ADBError::CommandError("计算本地文件 MD5 失败".to_string()));
585 }
586 }
587 Err(e) => {
588 return Err(ADBError::CommandError(format!(
589 "执行 md5sum 命令失败: {}",
590 e
591 )))
592 }
593 };
594
595 let device_md5 = self.compute_md5(device_id, device_path)?;
597
598 Ok(local_md5 == device_md5)
600 }
601
602 pub fn sync_directory_to_device(
604 &self,
605 device_id: &str,
606 local_dir: &str,
607 device_dir: &str,
608 exclude_patterns: Option<&[&str]>,
609 ) -> ADBResult<()> {
610 let local_dir_path = Path::new(local_dir);
612 if !local_dir_path.exists() || !local_dir_path.is_dir() {
613 return Err(ADBError::FileError(format!(
614 "本地目录不存在: {}",
615 local_dir
616 )));
617 }
618
619 self.create_directory(device_id, device_dir)?;
621
622 let entries = fs::read_dir(local_dir_path)
624 .map_err(|e| ADBError::FileError(format!("无法读取本地目录: {}", e)))?;
625
626 for entry in entries {
627 let entry =
628 entry.map_err(|e| ADBError::FileError(format!("读取目录条目失败: {}", e)))?;
629
630 let file_name = entry.file_name();
631 let file_name_str = file_name.to_string_lossy();
632
633 if let Some(patterns) = exclude_patterns {
635 let mut skip = false;
636 for pattern in patterns {
637 if glob::Pattern::new(pattern).unwrap().matches(&file_name_str) {
638 skip = true;
639 break;
640 }
641 }
642 if skip {
643 continue;
644 }
645 }
646
647 let local_path = entry.path();
648 let device_path = format!("{}/{}", device_dir.trim_end_matches('/'), file_name_str);
649
650 if local_path.is_dir() {
651 self.sync_directory_to_device(
653 device_id,
654 local_path.to_str().unwrap(),
655 &device_path,
656 exclude_patterns,
657 )?;
658 } else {
659 self.push(device_id, local_path.to_str().unwrap(), &device_path, None)?;
661 }
662 }
663
664 Ok(())
665 }
666}