easy_archive/archive/zip/
encode.rs1use crate::{
3 File,
4 error::{ArchiveError, Result},
5 traits::Encode,
6 utils::check_duplicate_files,
7};
8use std::collections::HashSet;
9use std::io::{Cursor, Write};
10use time::OffsetDateTime;
11use zip::DateTime;
12
13use super::Zip;
14
15impl Encode for Zip {
16 fn encode(files: Vec<File>) -> Result<Vec<u8>> {
17 check_duplicate_files(&files)?;
19
20 let estimated_size: usize = files.iter().map(|f| f.buffer.len()).sum::<usize>() / 2;
23 let mut output = Vec::with_capacity(estimated_size);
24 let cursor = Cursor::new(&mut output);
25 let mut zip = zip::ZipWriter::new(cursor);
26 let mut dir_set = HashSet::with_capacity(files.len() / 4); let create_options = |last_modified: Option<u64>| -> zip::write::FullFileOptions {
31 let mut options = zip::write::FullFileOptions::default()
32 .compression_method(zip::CompressionMethod::Zstd);
34
35 if let Some(timestamp) = last_modified
36 && let Ok(offset_time) = OffsetDateTime::from_unix_timestamp(timestamp as i64)
37 && let Ok(datetime) = DateTime::try_from(
38 time::PrimitiveDateTime::new(offset_time.date(), offset_time.time())
39 )
40 {
41 options = options.last_modified_time(datetime);
42 }
43
44 options
45 };
46
47 for file in files.iter().filter(|f| f.is_dir) {
49 if dir_set.contains(&file.path) {
50 continue;
51 }
52
53 dir_set.insert(file.path.clone());
54 let options = create_options(file.last_modified);
55
56 zip.add_directory(&file.path, options)
57 .map_err(|e| ArchiveError::EncodeFailed {
58 format: "zip".to_string(),
59 reason: format!("Failed to add directory '{}': {}", file.path, e),
60 })?;
61 }
62
63 for file in files.iter().filter(|f| !f.is_dir) {
65 if !file.path.contains('/') {
66 continue;
67 }
68
69 if let Some(parent) = std::path::Path::new(&file.path).parent() {
70 let parent_path = parent.to_string_lossy().to_string();
71 if !parent_path.is_empty() && !dir_set.contains(&parent_path) {
72 dir_set.insert(parent_path.clone());
73 let options = create_options(file.last_modified);
74
75 zip.add_directory(&parent_path, options).map_err(|e| {
76 ArchiveError::EncodeFailed {
77 format: "zip".to_string(),
78 reason: format!(
79 "Failed to add parent directory '{}': {}",
80 parent_path, e
81 ),
82 }
83 })?;
84 }
85 }
86 }
87
88 for file in files.iter().filter(|f| !f.is_dir) {
90 let mode = file.mode.unwrap_or(0o755);
91 let options = create_options(file.last_modified).unix_permissions(mode);
92
93 zip.start_file(&file.path, options)
94 .map_err(|e| ArchiveError::EncodeFailed {
95 format: "zip".to_string(),
96 reason: format!("Failed to start file '{}': {}", file.path, e),
97 })?;
98
99 zip.write_all(&file.buffer)
100 .map_err(|e| ArchiveError::EncodeFailed {
101 format: "zip".to_string(),
102 reason: format!("Failed to write file '{}': {}", file.path, e),
103 })?;
104 }
105
106 zip.finish().map_err(|e| ArchiveError::EncodeFailed {
107 format: "zip".to_string(),
108 reason: format!("Failed to finalize zip archive: {}", e),
109 })?;
110
111 Ok(output)
112 }
113}