adb_kit/
transfer.rs

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/// 文件传输选项
10#[derive(Debug, Clone)]
11pub struct TransferOptions {
12    // 共用选项
13    pub compression: bool,                     // 启用压缩
14    pub compression_algorithm: Option<String>, // 压缩算法: "any", "none", "brotli", "lz4", "zstd"
15
16    // push 专用选项
17    pub sync: bool,    // 使用 sync 模式进行传输 (--sync)
18    pub dry_run: bool, // 干运行,不实际存储到文件系统 (-n)
19
20    // pull 专用选项
21    pub preserve_timestamp: bool, // 保留文件时间戳和模式 (-a)
22
23    // 内部选项,不直接映射到 ADB 命令参数
24    pub chunk_size: usize, // 分块大小(单位:字节)
25}
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, // 64KB
36        }
37    }
38}
39
40impl ADB {
41    /// 文件拉取
42    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            // 如果指定了设备 ID 则添加
55            if !device_id.is_empty() {
56                cmd.arg("-s").arg(device_id);
57            }
58
59            cmd.arg("pull");
60
61            // 添加传输选项
62            if options.preserve_timestamp {
63                cmd.arg("-a");
64            }
65
66            // 处理压缩选项
67            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            // 设置源路径和目标路径
78            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    /// 文件推送
99    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            // 如果指定了设备 ID 则添加
112            if !device_id.is_empty() {
113                cmd.arg("-s").arg(device_id);
114            }
115
116            cmd.arg("push");
117
118            // 添加传输选项
119            if options.sync {
120                cmd.arg("--sync");
121            }
122
123            if options.dry_run {
124                cmd.arg("-n");
125            }
126
127            // 处理压缩选项
128            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            // 设置源路径和目标路径
139            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    /// 分块推送大文件
160    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        // 确保文件存在
171        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        // 获取文件大小
178        let file_size = fs::metadata(file_path)?.len() as usize;
179
180        // 如果文件较小,直接使用标准推送
181        if file_size <= chunk_size {
182            debug!("文件大小较小,使用标准推送");
183            return self.push(device_id, local_path, device_path, Some(options));
184        }
185
186        // 分块处理大文件
187        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        // 创建设备上的临时目录
199        let device_temp_dir = format!("{}.parts", device_path);
200        self.shell(device_id, &format!("mkdir -p {}", device_temp_dir))?;
201
202        // 分块传输
203        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        // 创建单独的 TransferOptions 用于块传输,可能想要禁用某些选项
209        let chunk_options = options.clone();
210
211        // 对于部分传输可能不需要某些选项
212        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            // 创建临时部分文件
220            {
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            // 推送此部分到设备
233            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            // 删除临时部分文件
242            let _ = fs::remove_file(part_file);
243
244            // 检查推送结果
245            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        // 合并所有部分
254        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        // 清理临时目录
263        let _ = fs::remove_dir_all(temp_dir);
264
265        Ok(())
266    }
267
268    /// 文件存在性检查
269    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    /// 获取文件/目录大小
278    pub fn get_file_size(&self, device_id: &str, path: &str) -> ADBResult<u64> {
279        // 检查是文件还是目录
280        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            // 对于目录,使用 du 命令
290            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) // 转换为字节
297        } else {
298            // 对于文件,使用 wc 命令
299            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    /// 创建目录
310    pub fn create_directory(&self, device_id: &str, path: &str) -> ADBResult<()> {
311        // 递归创建目录
312        self.shell(device_id, &format!("mkdir -p {}", path))?;
313
314        // 验证目录是否创建成功
315        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    /// 删除文件或目录
324    pub fn remove_path(&self, device_id: &str, path: &str, recursive: bool) -> ADBResult<()> {
325        // 检查路径是否存在
326        let exists = self.file_exists(device_id, path)?;
327        if !exists {
328            return Err(ADBError::CommandError(format!("路径不存在: {}", path)));
329        }
330
331        // 检查是文件还是目录
332        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                // 递归删除目录
343                self.shell(device_id, &format!("rm -rf {}", path))?;
344            } else {
345                // 删除空目录
346                let output = self.shell(device_id, &format!("rmdir {}", path));
347
348                // 检查是否因为目录非空而失败
349                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            // 删除文件
363            self.shell(device_id, &format!("rm {}", path))?;
364        }
365
366        // 验证路径是否已删除
367        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    /// 复制设备上的文件
376    pub fn copy_on_device(&self, device_id: &str, src_path: &str, dst_path: &str) -> ADBResult<()> {
377        // 检查源文件是否存在
378        if !self.file_exists(device_id, src_path)? {
379            return Err(ADBError::FileError(format!("源文件不存在: {}", src_path)));
380        }
381
382        // 复制文件
383        let command = format!("cp -f {} {}", src_path, dst_path);
384        self.shell(device_id, &command)?;
385
386        // 验证目标文件是否存在
387        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    /// 移动设备上的文件
398    pub fn move_on_device(&self, device_id: &str, src_path: &str, dst_path: &str) -> ADBResult<()> {
399        // 检查源文件是否存在
400        if !self.file_exists(device_id, src_path)? {
401            return Err(ADBError::FileError(format!("源文件不存在: {}", src_path)));
402        }
403
404        // 移动文件
405        let command = format!("mv -f {} {}", src_path, dst_path);
406        self.shell(device_id, &command)?;
407
408        // 验证源文件不存在且目标文件存在
409        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    /// 列出目录内容
420    pub fn list_directory(&self, device_id: &str, path: &str) -> ADBResult<Vec<String>> {
421        // 检查路径是否存在且是目录
422        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        // 列出目录内容
439        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    /// 获取文件最后修改时间
450    pub fn get_file_mtime(&self, device_id: &str, path: &str) -> ADBResult<String> {
451        // 检查文件是否存在
452        if !self.file_exists(device_id, path)? {
453            return Err(ADBError::FileError(format!("文件不存在: {}", path)));
454        }
455
456        // 获取文件最后修改时间
457        let output = self.shell(device_id, &format!("stat -c %y {}", path))?;
458        Ok(output.trim().to_string())
459    }
460
461    /// 检查设备上的可用空间
462    pub fn get_available_space(&self, device_id: &str, path: &str) -> ADBResult<u64> {
463        // 获取目标路径所在分区的可用空间
464        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        // 获取可用空间(KB)
472        let available_kb = parts[3]
473            .parse::<u64>()
474            .map_err(|_| ADBError::CommandError("无法解析可用空间值".to_string()))?;
475
476        // 转换为字节
477        Ok(available_kb * 1024)
478    }
479
480    /// 计算文件或目录的 MD5 校验和
481    pub fn compute_md5(&self, device_id: &str, path: &str) -> ADBResult<String> {
482        // 检查文件是否存在
483        if !self.file_exists(device_id, path)? {
484            return Err(ADBError::FileError(format!("文件不存在: {}", path)));
485        }
486
487        // 检查是文件还是目录
488        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        // 计算 MD5
501        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    /// 写入文本到设备上的文件
512    pub fn write_text_to_file(&self, device_id: &str, path: &str, content: &str) -> ADBResult<()> {
513        // 创建父目录(如果需要)
514        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        // 写入内容
521        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        // 验证文件是否存在
526        if !self.file_exists(device_id, path)? {
527            return Err(ADBError::CommandError(format!("写入文件失败: {}", path)));
528        }
529
530        Ok(())
531    }
532
533    /// 读取设备上文件的文本内容
534    pub fn read_text_from_file(&self, device_id: &str, path: &str) -> ADBResult<String> {
535        // 检查文件是否存在
536        if !self.file_exists(device_id, path)? {
537            return Err(ADBError::FileError(format!("文件不存在: {}", path)));
538        }
539
540        // 读取文件内容
541        let content = self.shell(device_id, &format!("cat {}", path))?;
542        Ok(content)
543    }
544
545    /// 比较本地文件和设备文件是否相同
546    pub fn compare_files(
547        &self,
548        device_id: &str,
549        local_path: &str,
550        device_path: &str,
551    ) -> ADBResult<bool> {
552        // 检查本地文件是否存在
553        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        // 检查设备文件是否存在
562        if !self.file_exists(device_id, device_path)? {
563            return Err(ADBError::FileError(format!(
564                "设备文件不存在: {}",
565                device_path
566            )));
567        }
568
569        // 计算本地文件的 MD5
570        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        // 计算设备文件的 MD5
596        let device_md5 = self.compute_md5(device_id, device_path)?;
597
598        // 比较 MD5
599        Ok(local_md5 == device_md5)
600    }
601
602    /// 同步目录 (本地到设备)
603    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        // 确保本地目录存在
611        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        // 确保设备目录存在
620        self.create_directory(device_id, device_dir)?;
621
622        // 读取本地目录内容
623        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            // 检查排除模式
634            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                // 递归同步子目录
652                self.sync_directory_to_device(
653                    device_id,
654                    local_path.to_str().unwrap(),
655                    &device_path,
656                    exclude_patterns,
657                )?;
658            } else {
659                // 推送文件
660                self.push(device_id, local_path.to_str().unwrap(), &device_path, None)?;
661            }
662        }
663
664        Ok(())
665    }
666}