async_sevenz/util/
compress.rs

1//! 7z Compressor helper functions
2
3use std::path::{Path, PathBuf};
4
5use async_fs as afs;
6use futures_lite::StreamExt;
7use futures_lite::io::{AsyncSeek, AsyncWrite, Cursor};
8
9#[cfg(feature = "aes256")]
10use crate::encoder_options::AesEncoderOptions;
11use crate::{ArchiveEntry, ArchiveWriter, EncoderMethod, Error, Password, writer::LazyFileReader};
12
13/// Compresses a source file or directory to a destination writer.
14///
15/// # Arguments
16/// * `src` - Path to the source file or directory to compress
17/// * `dest` - Writer that implements `AsyncWrite + AsyncSeek + Unpin`
18pub async fn compress<W: AsyncWrite + AsyncSeek + Unpin>(
19    src: impl AsRef<Path>,
20    dest: W,
21) -> Result<W, Error> {
22    let mut archive_writer = ArchiveWriter::new(dest).await?;
23    let parent = if src.as_ref().is_dir() {
24        src.as_ref()
25    } else {
26        src.as_ref().parent().unwrap_or(src.as_ref())
27    };
28    compress_path(src.as_ref(), parent, &mut archive_writer).await?;
29    let out = archive_writer.finish().await?;
30    Ok(out)
31}
32
33/// Compresses a source file or directory to a destination writer with password encryption.
34///
35/// # Arguments
36/// * `src` - Path to the source file or directory to compress
37/// * `dest` - Writer that implements `AsyncWrite + AsyncSeek + Unpin`
38/// * `password` - Password to encrypt the archive with
39#[cfg(feature = "aes256")]
40pub async fn compress_encrypted<W: AsyncWrite + AsyncSeek + Unpin>(
41    src: impl AsRef<Path>,
42    dest: W,
43    password: Password,
44) -> Result<W, Error> {
45    let mut archive_writer = ArchiveWriter::new(dest).await?;
46    if !password.is_empty() {
47        archive_writer.set_content_methods(vec![
48            AesEncoderOptions::new(password).into(),
49            EncoderMethod::LZMA2.into(),
50        ]);
51    }
52    let parent = if src.as_ref().is_dir() {
53        src.as_ref()
54    } else {
55        src.as_ref().parent().unwrap_or(src.as_ref())
56    };
57    compress_path(src.as_ref(), parent, &mut archive_writer).await?;
58    let out = archive_writer.finish().await?;
59    Ok(out)
60}
61
62/// Compresses a source file or directory to a destination file path.
63///
64/// This is a convenience function that handles file creation automatically.
65///
66/// # Arguments
67/// * `src` - Path to the source file or directory to compress
68/// * `dest` - Path where the compressed archive will be created
69pub async fn compress_to_path(src: impl AsRef<Path>, dest: impl AsRef<Path>) -> Result<(), Error> {
70    if let Some(path) = dest.as_ref().parent() {
71        if !path.exists() {
72            afs::create_dir_all(path)
73                .await
74                .map_err(|e| Error::io_msg(e, format!("Create dir failed:{:?}", dest.as_ref())))?;
75        }
76    }
77    let cursor = Cursor::new(Vec::<u8>::new());
78    let cursor = compress(src, cursor).await?;
79    let data = cursor.into_inner();
80    afs::write(dest.as_ref(), data).await?;
81    Ok(())
82}
83
84/// Compresses a source file or directory to a destination file path with password encryption.
85///
86/// This is a convenience function that handles file creation automatically.
87///
88/// # Arguments
89/// * `src` - Path to the source file or directory to compress
90/// * `dest` - Path where the encrypted compressed archive will be created
91/// * `password` - Password to encrypt the archive with
92#[cfg(feature = "aes256")]
93pub async fn compress_to_path_encrypted(
94    src: impl AsRef<Path>,
95    dest: impl AsRef<Path>,
96    password: Password,
97) -> Result<(), Error> {
98    if let Some(path) = dest.as_ref().parent() {
99        if !path.exists() {
100            afs::create_dir_all(path)
101                .await
102                .map_err(|e| Error::io_msg(e, format!("Create dir failed:{:?}", dest.as_ref())))?;
103        }
104    }
105    let cursor = Cursor::new(Vec::<u8>::new());
106    let cursor = compress_encrypted(src, cursor, password).await?;
107    let data = cursor.into_inner();
108    afs::write(dest.as_ref(), data).await?;
109    Ok(())
110}
111
112async fn compress_path<W: AsyncWrite + AsyncSeek + Unpin, P: AsRef<Path>>(
113    src: P,
114    root: &Path,
115    archive_writer: &mut ArchiveWriter<W>,
116) -> Result<(), Error> {
117    let mut stack: Vec<PathBuf> = vec![src.as_ref().to_path_buf()];
118    while let Some(path) = stack.pop() {
119        let entry_name = path
120            .strip_prefix(root)
121            .map_err(|e| Error::other(e.to_string()))?
122            .to_string_lossy()
123            .to_string();
124        let entry = ArchiveEntry::from_path(path.as_path(), entry_name).await;
125        let meta = afs::metadata(&path)
126            .await
127            .map_err(|e| Error::io_msg(e, "error metadata"))?;
128        if meta.is_dir() {
129            archive_writer
130                .push_archive_entry::<&[u8]>(entry, None)
131                .await?;
132            let mut rd = afs::read_dir(&path)
133                .await
134                .map_err(|e| Error::io_msg(e, "error read dir"))?;
135            while let Some(res) = rd.next().await {
136                let dir = res.map_err(|e| Error::io_msg(e, "error read dir entry"))?;
137                let ftype = dir
138                    .file_type()
139                    .await
140                    .map_err(|e| Error::io_msg(e, "error file type"))?;
141                if ftype.is_dir() || ftype.is_file() {
142                    stack.push(dir.path());
143                }
144            }
145        } else {
146            archive_writer
147                .push_archive_entry::<crate::writer::SourceReader<crate::writer::LazyFileReader>>(
148                    entry,
149                    Some(LazyFileReader::new(path.clone()).into()),
150                )
151                .await?;
152        }
153    }
154    Ok(())
155}
156
157impl<W: AsyncWrite + AsyncSeek + Unpin> ArchiveWriter<W> {
158    /// Adds a source path to the compression builder with a filter function using solid compression.
159    ///
160    /// The filter function allows selective inclusion of files based on their paths.
161    /// Files are compressed using solid compression for better compression ratios.
162    ///
163    /// # Arguments
164    /// * `path` - Path to add to the compression
165    /// * `filter` - Function that returns `true` for paths that should be included
166    pub async fn push_source_path<Fut>(
167        &mut self,
168        path: impl AsRef<Path>,
169        mut filter: impl FnMut(&Path) -> Fut,
170    ) -> Result<(), Error>
171    where
172        Fut: std::future::Future<Output = bool>,
173    {
174        encode_path(true, &path, self, &mut filter).await?;
175        Ok(())
176    }
177
178    /// Adds a source path to the compression builder with a filter function using non-solid compression.
179    ///
180    /// Non-solid compression allows individual file extraction without decompressing the entire archive,
181    /// but typically results in larger archive sizes compared to solid compression.
182    ///
183    /// # Arguments
184    /// * `path` - Path to add to the compression
185    /// * `filter` - Function that returns `true` for paths that should be included
186    pub async fn push_source_path_non_solid<Fut>(
187        &mut self,
188        path: impl AsRef<Path>,
189        mut filter: impl FnMut(&Path) -> Fut,
190    ) -> Result<(), Error>
191    where
192        Fut: std::future::Future<Output = bool>,
193    {
194        encode_path(false, &path, self, &mut filter).await?;
195        Ok(())
196    }
197}
198
199async fn collect_file_paths<Fut>(
200    src: impl AsRef<Path>,
201    paths: &mut Vec<PathBuf>,
202    filter: &mut impl FnMut(&Path) -> Fut,
203) -> std::io::Result<()>
204where
205    Fut: std::future::Future<Output = bool>,
206{
207    let mut stack: Vec<PathBuf> = vec![src.as_ref().to_path_buf()];
208    while let Some(path) = stack.pop() {
209        if !filter(&path).await {
210            continue;
211        }
212        let meta = afs::metadata(&path).await?;
213        if meta.is_dir() {
214            let mut rd = afs::read_dir(&path).await?;
215            while let Some(res) = rd.next().await {
216                let dir = res?;
217                let ftype = dir.file_type().await?;
218                if ftype.is_file() || ftype.is_dir() {
219                    stack.push(dir.path());
220                }
221            }
222        } else {
223            paths.push(path);
224        }
225    }
226    Ok(())
227}
228
229const MAX_BLOCK_SIZE: u64 = 4 * 1024 * 1024 * 1024; // 4 GiB
230
231async fn encode_path<W: AsyncWrite + AsyncSeek + Unpin, Fut>(
232    solid: bool,
233    src: impl AsRef<Path>,
234    zip: &mut ArchiveWriter<W>,
235    filter: &mut impl FnMut(&Path) -> Fut,
236) -> Result<(), Error>
237where
238    Fut: std::future::Future<Output = bool>,
239{
240    let mut entries = Vec::new();
241    let mut paths = Vec::new();
242    collect_file_paths(&src, &mut paths, filter)
243        .await
244        .map_err(|e| {
245            Error::io_msg(
246                e,
247                format!("Failed to collect entries from path:{:?}", src.as_ref()),
248            )
249        })?;
250
251    if !solid {
252        for ele in paths.into_iter() {
253            let name = extract_file_name(&src, &ele)?;
254
255            zip.push_archive_entry::<crate::writer::SourceReader<crate::writer::LazyFileReader>>(
256                ArchiveEntry::from_path(ele.as_path(), name).await,
257                Some(LazyFileReader::new(ele.clone()).into()),
258            )
259            .await?;
260        }
261        return Ok(());
262    }
263    let mut files = Vec::new();
264    let mut file_size = 0;
265    for ele in paths.into_iter() {
266        let size = afs::metadata(&ele).await?.len();
267        let name = extract_file_name(&src, &ele)?;
268
269        if size >= MAX_BLOCK_SIZE {
270            zip.push_archive_entry::<crate::writer::SourceReader<crate::writer::LazyFileReader>>(
271                ArchiveEntry::from_path(ele.as_path(), name).await,
272                Some(LazyFileReader::new(ele.clone()).into()),
273            )
274            .await?;
275            continue;
276        }
277        if file_size + size >= MAX_BLOCK_SIZE {
278            zip.push_archive_entries(entries, files).await?;
279            entries = Vec::new();
280            files = Vec::new();
281            file_size = 0;
282        }
283        file_size += size;
284        entries.push(ArchiveEntry::from_path(ele.as_path(), name).await);
285        files.push(LazyFileReader::new(ele).into());
286    }
287    if !entries.is_empty() {
288        zip.push_archive_entries(entries, files).await?;
289    }
290
291    Ok(())
292}
293
294fn extract_file_name(src: &impl AsRef<Path>, ele: &PathBuf) -> Result<String, Error> {
295    if ele == src.as_ref() {
296        // Single file case: use just the filename.
297        Ok(ele
298            .file_name()
299            .ok_or_else(|| {
300                Error::io_msg(
301                    std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid filename"),
302                    format!("Failed to get filename from {ele:?}"),
303                )
304            })?
305            .to_string_lossy()
306            .to_string())
307    } else {
308        // Directory case: remove path.
309        Ok(ele.strip_prefix(src).unwrap().to_string_lossy().to_string())
310    }
311}