use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;
use zip::unstable::write::FileOptionsExt;
use crate::archive::types::*;
use crate::archive::types::ensure_path_within_dir;
pub struct ZipEngine;
impl ZipEngine {
pub fn list(path: &str) -> Result<ArchiveInfo, String> {
let file = File::open(path).map_err(|e| format!("打开文件失败: {}", e))?;
let mut archive = zip::ZipArchive::new(file).map_err(|e| format!("读取ZIP失败: {}", e))?;
let mut entries = Vec::new();
let mut total_size = 0u64;
let mut total_compressed_size = 0u64;
let mut has_password = false;
for i in 0..archive.len() {
let entry = archive.by_index(i).map_err(|e| format!("读取条目失败: {}", e))?;
let name = entry.name().to_string();
let is_dir = entry.is_dir();
let size = entry.size();
let compressed_size = entry.compressed_size();
let is_encrypted = entry.encrypted();
if is_encrypted {
has_password = true;
}
total_size += size;
total_compressed_size += compressed_size;
entries.push(ArchiveEntry {
name: name.clone(),
path: name,
size,
compressed_size,
is_dir,
modified: None,
is_encrypted,
});
}
Ok(ArchiveInfo {
format: ArchiveFormat::Zip,
path: path.to_string(),
file_count: entries.iter().filter(|e| !e.is_dir).count(),
entries,
total_size,
total_compressed_size,
has_password,
})
}
pub fn extract(path: &str, options: &ExtractOptions, progress_cb: &dyn Fn(ProgressInfo)) -> Result<TaskResult, String> {
let file = File::open(path).map_err(|e| format!("打开文件失败: {}", e))?;
let mut archive = zip::ZipArchive::new(file).map_err(|e| format!("读取ZIP失败: {}", e))?;
let output_dir = Path::new(&options.output_dir);
std::fs::create_dir_all(output_dir).map_err(|e| format!("创建目录失败: {}", e))?;
let total = if let Some(ref selected) = options.selected_entries {
let mut count = 0u64;
for i in 0..archive.len() {
let entry = archive.by_index(i).map_err(|e| format!("读取条目失败: {}", e))?;
if selected.contains(&entry.name().to_string()) {
count += 1;
}
}
count
} else {
archive.len() as u64
};
let task_id = uuid::Uuid::new_v4().to_string();
let mut processed: u64 = 0;
for i in 0..archive.len() {
let name = {
let entry = archive.by_index(i).map_err(|e| format!("读取条目失败: {}", e))?;
entry.name().to_string()
};
if let Some(ref selected) = options.selected_entries {
if !selected.contains(&name) {
continue;
}
}
let mut entry = if let Some(ref password) = options.password {
archive.by_index_decrypt(i, password.as_bytes())
.map_err(|e| format!("解密失败(密码可能不正确): {}", e))?
} else {
archive.by_index(i).map_err(|e| format!("读取条目失败: {}", e))?
};
let out_path = ensure_path_within_dir(output_dir, &name)?;
if entry.is_dir() {
std::fs::create_dir_all(&out_path).map_err(|e| format!("创建目录失败: {}", e))?;
} else {
if let Some(parent) = out_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?;
}
let mut outfile = File::create(&out_path).map_err(|e| format!("创建文件失败: {}", e))?;
std::io::copy(&mut entry, &mut outfile).map_err(|e| format!("写入文件失败: {}", e))?;
}
processed += 1;
progress_cb(ProgressInfo {
task_id: task_id.clone(),
current_file: name,
processed_bytes: processed,
total_bytes: total,
percent: processed as f64 / total.max(1) as f64 * 100.0,
speed: 0.0,
elapsed_secs: 0.0,
remaining_secs: 0.0,
});
}
Ok(TaskResult {
task_id,
success: true,
message: "解压完成".to_string(),
output_path: Some(options.output_dir.clone()),
})
}
pub fn compress(files: &[String], options: &CompressOptions, progress_cb: &dyn Fn(ProgressInfo)) -> Result<TaskResult, String> {
if let Some(parent) = Path::new(&options.output_path).parent() {
std::fs::create_dir_all(parent).map_err(|e| format!("创建输出目录失败: {}", e))?;
}
let file = File::create(&options.output_path).map_err(|e| format!("创建文件失败: {}", e))?;
let compression_method = match options.compression_level {
CompressionLevel::Store => zip::CompressionMethod::Stored,
_ => zip::CompressionMethod::Deflated,
};
let mut zip = zip::ZipWriter::new(file);
let task_id = uuid::Uuid::new_v4().to_string();
let mut all_files: Vec<(String, String)> = Vec::new(); for file_path in files {
let path = Path::new(file_path);
if !path.exists() {
continue;
}
if path.is_dir() {
let dir_name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
collect_dir_files(path, &dir_name, &mut all_files)?;
} else {
let name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
all_files.push((file_path.clone(), name));
}
}
let total = all_files.len() as u64;
for (i, (fs_path, archive_path)) in all_files.iter().enumerate() {
let path = Path::new(fs_path);
let mut file_options = zip::write::SimpleFileOptions::default()
.compression_method(compression_method);
if let Some(ref password) = options.password {
file_options = file_options
.with_deprecated_encryption(password.as_bytes());
}
if path.is_dir() {
zip.add_directory(&format!("{}/", archive_path), file_options)
.map_err(|e| format!("添加目录失败: {}", e))?;
} else {
let mut f = File::open(path).map_err(|e| format!("打开文件失败: {}", e))?;
let mut data = Vec::new();
f.read_to_end(&mut data).map_err(|e| format!("读取文件失败: {}", e))?;
zip.start_file(archive_path, file_options)
.map_err(|e| format!("写入条目失败: {}", e))?;
zip.write_all(&data).map_err(|e| format!("写入数据失败: {}", e))?;
}
progress_cb(ProgressInfo {
task_id: task_id.clone(),
current_file: archive_path.clone(),
processed_bytes: i as u64 + 1,
total_bytes: total,
percent: (i as f64 + 1.0) / total as f64 * 100.0,
speed: 0.0,
elapsed_secs: 0.0,
remaining_secs: 0.0,
});
}
zip.finish().map_err(|e| format!("完成压缩失败: {}", e))?;
Ok(TaskResult {
task_id,
success: true,
message: "压缩完成".to_string(),
output_path: Some(options.output_path.clone()),
})
}
}
fn collect_dir_files(dir: &Path, prefix: &str, result: &mut Vec<(String, String)>) -> Result<(), String> {
result.push((dir.to_string_lossy().to_string(), prefix.to_string()));
let entries = std::fs::read_dir(dir).map_err(|e| format!("读取目录失败: {}", e))?;
for entry in entries {
let entry = entry.map_err(|e| format!("读取条目失败: {}", e))?;
let path = entry.path();
let name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
let archive_path = format!("{}/{}", prefix, name);
if path.is_dir() {
collect_dir_files(&path, &archive_path, result)?;
} else {
result.push((path.to_string_lossy().to_string(), archive_path));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_folder_compress_recursive() {
let temp_dir = std::env::temp_dir().join("winrar_test_folder");
let sub_dir = temp_dir.join("subdir");
let _ = std::fs::remove_dir_all(&temp_dir);
std::fs::create_dir_all(&sub_dir).unwrap();
std::fs::write(temp_dir.join("file1.txt"), b"Hello World 1").unwrap();
std::fs::write(sub_dir.join("file2.txt"), b"Hello World 2").unwrap();
std::fs::write(sub_dir.join("file3.txt"), b"Hello World 3").unwrap();
let output_path = std::env::temp_dir().join("winrar_test_folder.zip");
let options = CompressOptions {
format: ArchiveFormat::Zip,
output_path: output_path.to_string_lossy().to_string(),
password: None,
compression_level: CompressionLevel::Normal,
split_volume: None,
};
let result = ZipEngine::compress(
&[temp_dir.to_string_lossy().to_string()],
&options,
&|_| {},
);
assert!(result.is_ok(), "压缩失败: {:?}", result);
let info = ZipEngine::list(&output_path.to_string_lossy().to_string()).unwrap();
println!("归档条目 (共 {} 个):", info.entries.len());
for entry in &info.entries {
println!(" {} (is_dir={}, size={})", entry.path, entry.is_dir, entry.size);
}
let paths: Vec<&str> = info.entries.iter().map(|e| e.path.as_str()).collect();
assert!(paths.iter().any(|p| p.contains("file1.txt")), "file1.txt 不在归档中! paths: {:?}", paths);
assert!(paths.iter().any(|p| p.contains("file2.txt")), "file2.txt 不在归档中! paths: {:?}", paths);
assert!(paths.iter().any(|p| p.contains("file3.txt")), "file3.txt 不在归档中! paths: {:?}", paths);
assert!(info.file_count >= 3, "应该至少有3个文件,实际有 {}", info.file_count);
let _ = std::fs::remove_dir_all(&temp_dir);
let _ = std::fs::remove_file(&output_path);
}
}