use crate::device::ADB;
use crate::error::{ADBError, ADBResult};
use log::{debug, info};
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path;
use std::process::Command;
#[derive(Debug, Clone)]
pub struct TransferOptions {
pub compression: bool, pub compression_algorithm: Option<String>,
pub sync: bool, pub dry_run: bool,
pub preserve_timestamp: bool,
pub chunk_size: usize, }
impl Default for TransferOptions {
fn default() -> Self {
TransferOptions {
compression: false,
compression_algorithm: None,
sync: false,
dry_run: false,
preserve_timestamp: false,
chunk_size: 65536, }
}
}
impl ADB {
pub fn pull(
&self,
device_id: &str,
device_path: &str,
local_path: &str,
options: Option<TransferOptions>,
) -> ADBResult<()> {
let options = options.unwrap_or_default();
self.with_retry(|| {
let mut cmd = Command::new(&self.config.path);
if !device_id.is_empty() {
cmd.arg("-s").arg(device_id);
}
cmd.arg("pull");
if options.preserve_timestamp {
cmd.arg("-a");
}
if options.compression {
if let Some(algorithm) = &options.compression_algorithm {
cmd.arg("-z").arg(algorithm);
} else {
cmd.arg("-z").arg("any");
}
} else {
cmd.arg("-Z");
}
cmd.arg(device_path).arg(local_path);
info!("开始从设备拉取文件: {} -> {}", device_path, local_path);
let output = cmd
.output()
.map_err(|e| ADBError::CommandError(format!("执行 ADB pull 命令失败: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ADBError::CommandError(format!(
"ADB pull 命令失败: {}",
stderr
)));
}
debug!("成功拉取文件 {} 到 {}", device_path, local_path);
Ok(())
})
}
pub fn push(
&self,
device_id: &str,
local_path: &str,
device_path: &str,
options: Option<TransferOptions>,
) -> ADBResult<()> {
let options = options.unwrap_or_default();
self.with_retry(|| {
let mut cmd = Command::new(&self.config.path);
if !device_id.is_empty() {
cmd.arg("-s").arg(device_id);
}
cmd.arg("push");
if options.sync {
cmd.arg("--sync");
}
if options.dry_run {
cmd.arg("-n");
}
if options.compression {
if let Some(algorithm) = &options.compression_algorithm {
cmd.arg("-z").arg(algorithm);
} else {
cmd.arg("-z").arg("any");
}
} else {
cmd.arg("-Z");
}
cmd.arg(local_path).arg(device_path);
info!("开始向设备推送文件: {} -> {}", local_path, device_path);
let output = cmd
.output()
.map_err(|e| ADBError::CommandError(format!("执行 ADB push 命令失败: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ADBError::CommandError(format!(
"ADB push 命令失败: {}",
stderr
)));
}
debug!("成功推送文件 {} 到 {}", local_path, device_path);
Ok(())
})
}
pub fn push_large_file(
&self,
device_id: &str,
local_path: &str,
device_path: &str,
options: Option<TransferOptions>,
) -> ADBResult<()> {
let options = options.unwrap_or_default();
let chunk_size = options.chunk_size;
let file_path = Path::new(local_path);
if !file_path.exists() || !file_path.is_file() {
let error_msg = format!("文件不存在: {}", local_path);
return Err(ADBError::FileError(error_msg));
}
let file_size = fs::metadata(file_path)?.len() as usize;
if file_size <= chunk_size {
debug!("文件大小较小,使用标准推送");
return self.push(device_id, local_path, device_path, Some(options));
}
let mut file = File::open(file_path).map_err(|e| {
let error_msg = format!("无法打开文件 {}: {}", local_path, e);
ADBError::FileError(error_msg)
})?;
info!(
"将文件 {} 分成 {} 块传输",
local_path,
(file_size + chunk_size - 1) / chunk_size
);
let device_temp_dir = format!("{}.parts", device_path);
self.shell(device_id, &format!("mkdir -p {}", device_temp_dir))?;
let temp_dir = crate::utils::create_temp_dir_path("adb_push")?;
let mut buffer = vec![0u8; chunk_size];
let chunks_count = (file_size + chunk_size - 1) / chunk_size;
let chunk_options = options.clone();
for i in 0..chunks_count {
let part_file = temp_dir.join(format!("part{}", i));
let bytes_read = file.read(&mut buffer[..]).map_err(|e| {
let error_msg = format!("读取文件块失败: {}", e);
ADBError::FileError(error_msg)
})?;
{
let mut part = File::create(&part_file).map_err(|e| {
let error_msg = format!("创建临时文件失败: {}", e);
ADBError::FileError(error_msg)
})?;
part.write_all(&buffer[..bytes_read]).map_err(|e| {
let error_msg = format!("写入临时文件失败: {}", e);
ADBError::FileError(error_msg)
})?;
}
let device_part_path = format!("{}/part{}", device_temp_dir, i);
let push_result = self.push(
device_id,
part_file.to_str().unwrap(),
&device_part_path,
Some(chunk_options.clone()),
);
let _ = fs::remove_file(part_file);
push_result.map_err(|e| {
let error_msg = format!("推送文件块失败: {}", e);
ADBError::CommandError(error_msg)
})?;
debug!("已推送块 {}/{}", i + 1, chunks_count);
}
let cat_cmd = format!(
"cat {}/* > {} && rm -rf {}",
device_temp_dir, device_path, device_temp_dir
);
self.shell(device_id, &cat_cmd)?;
info!("已成功推送和合并大文件 {} 到 {}", local_path, device_path);
let _ = fs::remove_dir_all(temp_dir);
Ok(())
}
pub fn file_exists(&self, device_id: &str, path: &str) -> ADBResult<bool> {
let result = self.shell(
device_id,
&format!("[ -e {} ] && echo 'exists' || echo 'not exists'", path),
)?;
Ok(result.trim() == "exists")
}
pub fn get_file_size(&self, device_id: &str, path: &str) -> ADBResult<u64> {
let is_dir = self
.shell(
device_id,
&format!("[ -d {} ] && echo 'true' || echo 'false'", path),
)?
.trim()
== "true";
if is_dir {
let output = self.shell(device_id, &format!("du -sk {} | cut -f1", path))?;
let size_kb = output
.trim()
.parse::<u64>()
.map_err(|_| ADBError::CommandError(format!("无法获取目录大小: {}", path)))?;
Ok(size_kb * 1024) } else {
let output = self.shell(device_id, &format!("wc -c < {}", path))?;
let size = output
.trim()
.parse::<u64>()
.map_err(|_| ADBError::CommandError(format!("无法获取文件大小: {}", path)))?;
Ok(size)
}
}
pub fn create_directory(&self, device_id: &str, path: &str) -> ADBResult<()> {
self.shell(device_id, &format!("mkdir -p {}", path))?;
let exists = self.file_exists(device_id, path)?;
if !exists {
return Err(ADBError::CommandError(format!("无法创建目录: {}", path)));
}
Ok(())
}
pub fn remove_path(&self, device_id: &str, path: &str, recursive: bool) -> ADBResult<()> {
let exists = self.file_exists(device_id, path)?;
if !exists {
return Err(ADBError::CommandError(format!("路径不存在: {}", path)));
}
let is_dir = self
.shell(
device_id,
&format!("[ -d {} ] && echo 'true' || echo 'false'", path),
)?
.trim()
== "true";
if is_dir {
if recursive {
self.shell(device_id, &format!("rm -rf {}", path))?;
} else {
let output = self.shell(device_id, &format!("rmdir {}", path));
if let Err(e) = &output {
if let ADBError::DeviceError(msg) = e {
if msg.contains("Directory not empty") {
return Err(ADBError::CommandError(
"目录不为空,使用 recursive=true 递归删除".to_string(),
));
}
}
}
output?;
}
} else {
self.shell(device_id, &format!("rm {}", path))?;
}
let still_exists = self.file_exists(device_id, path)?;
if still_exists {
return Err(ADBError::CommandError(format!("无法删除路径: {}", path)));
}
Ok(())
}
pub fn copy_on_device(&self, device_id: &str, src_path: &str, dst_path: &str) -> ADBResult<()> {
if !self.file_exists(device_id, src_path)? {
return Err(ADBError::FileError(format!("源文件不存在: {}", src_path)));
}
let command = format!("cp -f {} {}", src_path, dst_path);
self.shell(device_id, &command)?;
if !self.file_exists(device_id, dst_path)? {
return Err(ADBError::CommandError(format!(
"复制文件失败: {} -> {}",
src_path, dst_path
)));
}
Ok(())
}
pub fn move_on_device(&self, device_id: &str, src_path: &str, dst_path: &str) -> ADBResult<()> {
if !self.file_exists(device_id, src_path)? {
return Err(ADBError::FileError(format!("源文件不存在: {}", src_path)));
}
let command = format!("mv -f {} {}", src_path, dst_path);
self.shell(device_id, &command)?;
if self.file_exists(device_id, src_path)? || !self.file_exists(device_id, dst_path)? {
return Err(ADBError::CommandError(format!(
"移动文件失败: {} -> {}",
src_path, dst_path
)));
}
Ok(())
}
pub fn list_directory(&self, device_id: &str, path: &str) -> ADBResult<Vec<String>> {
let exists = self.file_exists(device_id, path)?;
let is_dir = self
.shell(
device_id,
&format!("[ -d {} ] && echo 'true' || echo 'false'", path),
)?
.trim()
== "true";
if !exists || !is_dir {
return Err(ADBError::FileError(format!(
"路径不存在或不是目录: {}",
path
)));
}
let output = self.shell(device_id, &format!("ls -A {}", path))?;
let files = output
.lines()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
Ok(files)
}
pub fn get_file_mtime(&self, device_id: &str, path: &str) -> ADBResult<String> {
if !self.file_exists(device_id, path)? {
return Err(ADBError::FileError(format!("文件不存在: {}", path)));
}
let output = self.shell(device_id, &format!("stat -c %y {}", path))?;
Ok(output.trim().to_string())
}
pub fn get_available_space(&self, device_id: &str, path: &str) -> ADBResult<u64> {
let output = self.shell(device_id, &format!("df -k {} | tail -1", path))?;
let parts: Vec<&str> = output.split_whitespace().collect();
if parts.len() < 4 {
return Err(ADBError::CommandError("无法解析可用空间信息".to_string()));
}
let available_kb = parts[3]
.parse::<u64>()
.map_err(|_| ADBError::CommandError("无法解析可用空间值".to_string()))?;
Ok(available_kb * 1024)
}
pub fn compute_md5(&self, device_id: &str, path: &str) -> ADBResult<String> {
if !self.file_exists(device_id, path)? {
return Err(ADBError::FileError(format!("文件不存在: {}", path)));
}
let is_dir = self
.shell(
device_id,
&format!("[ -d {} ] && echo 'true' || echo 'false'", path),
)?
.trim()
== "true";
if is_dir {
return Err(ADBError::CommandError("MD5 计算不支持目录".to_string()));
}
let output = self.shell(device_id, &format!("md5sum {}", path))?;
let parts: Vec<&str> = output.split_whitespace().collect();
if parts.is_empty() {
return Err(ADBError::CommandError("无法计算 MD5".to_string()));
}
Ok(parts[0].to_string())
}
pub fn write_text_to_file(&self, device_id: &str, path: &str, content: &str) -> ADBResult<()> {
if let Some(parent_dir) = Path::new(path).parent().and_then(|p| p.to_str()) {
if !parent_dir.is_empty() {
let _ = self.create_directory(device_id, parent_dir);
}
}
let escaped_content = content.replace("\"", "\\\"").replace("\n", "\\n");
let command = format!("echo -e \"{}\" > {}", escaped_content, path);
self.shell(device_id, &command)?;
if !self.file_exists(device_id, path)? {
return Err(ADBError::CommandError(format!("写入文件失败: {}", path)));
}
Ok(())
}
pub fn read_text_from_file(&self, device_id: &str, path: &str) -> ADBResult<String> {
if !self.file_exists(device_id, path)? {
return Err(ADBError::FileError(format!("文件不存在: {}", path)));
}
let content = self.shell(device_id, &format!("cat {}", path))?;
Ok(content)
}
pub fn compare_files(
&self,
device_id: &str,
local_path: &str,
device_path: &str,
) -> ADBResult<bool> {
let local_file_path = Path::new(local_path);
if !local_file_path.exists() || !local_file_path.is_file() {
return Err(ADBError::FileError(format!(
"本地文件不存在: {}",
local_path
)));
}
if !self.file_exists(device_id, device_path)? {
return Err(ADBError::FileError(format!(
"设备文件不存在: {}",
device_path
)));
}
let local_md5 = match std::process::Command::new("md5sum")
.arg(local_path)
.output()
{
Ok(output) => {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let parts: Vec<&str> = stdout.split_whitespace().collect();
if !parts.is_empty() {
parts[0].to_string()
} else {
return Err(ADBError::CommandError("无法计算本地文件 MD5".to_string()));
}
} else {
return Err(ADBError::CommandError("计算本地文件 MD5 失败".to_string()));
}
}
Err(e) => {
return Err(ADBError::CommandError(format!(
"执行 md5sum 命令失败: {}",
e
)))
}
};
let device_md5 = self.compute_md5(device_id, device_path)?;
Ok(local_md5 == device_md5)
}
pub fn sync_directory_to_device(
&self,
device_id: &str,
local_dir: &str,
device_dir: &str,
exclude_patterns: Option<&[&str]>,
) -> ADBResult<()> {
let local_dir_path = Path::new(local_dir);
if !local_dir_path.exists() || !local_dir_path.is_dir() {
return Err(ADBError::FileError(format!(
"本地目录不存在: {}",
local_dir
)));
}
self.create_directory(device_id, device_dir)?;
let entries = fs::read_dir(local_dir_path)
.map_err(|e| ADBError::FileError(format!("无法读取本地目录: {}", e)))?;
for entry in entries {
let entry =
entry.map_err(|e| ADBError::FileError(format!("读取目录条目失败: {}", e)))?;
let file_name = entry.file_name();
let file_name_str = file_name.to_string_lossy();
if let Some(patterns) = exclude_patterns {
let mut skip = false;
for pattern in patterns {
if glob::Pattern::new(pattern).unwrap().matches(&file_name_str) {
skip = true;
break;
}
}
if skip {
continue;
}
}
let local_path = entry.path();
let device_path = format!("{}/{}", device_dir.trim_end_matches('/'), file_name_str);
if local_path.is_dir() {
self.sync_directory_to_device(
device_id,
local_path.to_str().unwrap(),
&device_path,
exclude_patterns,
)?;
} else {
self.push(device_id, local_path.to_str().unwrap(), &device_path, None)?;
}
}
Ok(())
}
}