Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 84 additions & 19 deletions src-tauri/src/commands/file_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ use super::utils::{get_app_data_dir, get_exe_directory, normalize_path};

/// 单个分卷 zip 的大小上限(字节)。
const MAX_VOLUME_BYTES: u64 = 24_500_000;
/// 单个 entry 的 local header + 中央目录条目大小上界(不含文件名)。
const ZIP_PER_ENTRY_HEADER_UPPER_BOUND: u64 = 128;
/// EOCD 记录(zip 末尾)固定大小。
const ZIP_EOCD_BYTES: u64 = 22;
/// 中央目录每条记录的固定字段大小(不含文件名)。
Expand Down Expand Up @@ -99,11 +97,42 @@ where
true
}

fn estimate_entry_upper_bound(entry: &ExportEntry) -> Option<u64> {
let file_size = entry.source_path.metadata().ok()?.len();
let name_len = entry.archive_name.len() as u64;
// 文件名在 local header 和中央目录条目里都出现一次,所以 ×2。
Some(file_size + ZIP_PER_ENTRY_HEADER_UPPER_BOUND + name_len.saturating_mul(2))
/// zip 每条目的 local header 固定开销(30)+ DEFLATE 帧头 + 余量。
/// 文件名部分在 local header 和中央目录各出现一次:
/// - local header 侧:包含在此常量余量内(本应用文件名短,够用)
/// - 中央目录侧:由 `entry_cd_bytes`(ZIP_CENTRAL_DIR_FIXED_BYTES + 文件名长度)计入
const ZIP_LOCAL_HEADER_OVERHEAD: u64 = 64;

/// 按扩展名估算压缩后**数据**大小的保守上界。
///
/// 注意:仅估计压缩数据,不含 zip local header / 中央目录等开销,调用侧自行加。
fn estimate_compressed_upper_bound(path: &Path, file_size: u64) -> u64 {
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
match ext.as_str() {
"png" | "jpg" | "jpeg" => file_size, // 已压缩,DEFLATE 无效
"log" | "json" | "txt" | "toml" | "yaml" | "yml" | "xml" | "csv" => {
file_size.saturating_div(4) // 实测 10-25x,4x 留有足够余量
}
_ => file_size, // 未知类型,不假设压缩
}
}

/// 用 flate2 预压缩文件到内存,返回 deflate 后字节数——与 zip crate 内部压缩同算法。
///
/// 只在保守估算触线时才调用(每卷最多一次),避免每个文件都压两遍。
fn pre_compress_measure(path: &Path) -> io::Result<u64> {
use flate2::write::DeflateEncoder;
use flate2::Compression;

let mut src = std::fs::File::open(path)?;
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
std::io::copy(&mut src, &mut encoder)?;
let compressed = encoder.finish()?;
Ok(compressed.len() as u64)
}

fn normalize_archive_path(path: &Path) -> String {
Expand Down Expand Up @@ -595,20 +624,56 @@ fn export_logs_blocking(
let mut central_dir_reserve: u64 = ZIP_EOCD_BYTES;

while let Some(entry) = iter.peek() {
let est_delta = estimate_entry_upper_bound(entry).unwrap_or(u64::MAX);
let current_bytes = counter.load(Ordering::Relaxed);
let entry_cd_bytes = ZIP_CENTRAL_DIR_FIXED_BYTES + entry.archive_name.len() as u64;
let projected = current_bytes
.saturating_add(est_delta)
.saturating_add(central_dir_reserve)
.saturating_add(entry_cd_bytes);
// 单文件超过卷上限时,当前卷为空就让它独占一卷,保证不丢文件。
if wrote_any && projected > MAX_VOLUME_BYTES {
break;
let entry_cd_bytes =
ZIP_CENTRAL_DIR_FIXED_BYTES + entry.archive_name.len() as u64;
let file_size = entry
.source_path
.metadata()
.ok()
.map(|m| m.len())
.unwrap_or(u64::MAX); // metadata 失败用极大值,保守触发预压缩

// 两阶段容量检查:先用保守估算快速通过大多数文件,
// 估算触线时才实际预压缩一次拿精确值,避免每个文件都压两遍。
let est_delta = estimate_compressed_upper_bound(&entry.source_path, file_size)
.saturating_add(ZIP_LOCAL_HEADER_OVERHEAD);
let current_total = counter
.load(Ordering::Relaxed)
.saturating_add(central_dir_reserve);
if wrote_any
&& current_total
.saturating_add(est_delta)
.saturating_add(entry_cd_bytes)
> MAX_VOLUME_BYTES
{
match pre_compress_measure(&entry.source_path) {
Ok(exact_delta) => {
let exact_delta =
exact_delta.saturating_add(ZIP_LOCAL_HEADER_OVERHEAD);
if current_total
.saturating_add(exact_delta)
.saturating_add(entry_cd_bytes)
> MAX_VOLUME_BYTES
{
break;
}
}
Err(_) => {
// IO 错误打不开文件,保守切卷
break;
}
}
}

let entry = iter.next().expect("peek 已确认存在");
if add_file_to_zip(&mut zip, &entry.source_path, &entry.archive_name, options) {
central_dir_reserve = central_dir_reserve.saturating_add(entry_cd_bytes);
if add_file_to_zip(
&mut zip,
&entry.source_path,
&entry.archive_name,
options,
) {
central_dir_reserve =
central_dir_reserve.saturating_add(entry_cd_bytes);
wrote_any = true;
volume_file_count += 1;
total_files_written += 1;
Expand Down
Loading