sevenz_rust2/util/
compress.rs

1//! 7z Compressor helper functions
2
3use std::{
4    fs::File,
5    io::{Seek, Write},
6    path::{Path, PathBuf},
7};
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 `Write + Seek` to write the compressed archive to
18#[cfg_attr(docsrs, doc(cfg(all(feature = "compress", feature = "util"))))]
19pub fn compress<W: Write + Seek>(src: impl AsRef<Path>, dest: W) -> Result<W, Error> {
20    let mut archive_writer = ArchiveWriter::new(dest)?;
21    let parent = if src.as_ref().is_dir() {
22        src.as_ref()
23    } else {
24        src.as_ref().parent().unwrap_or(src.as_ref())
25    };
26    compress_path(src.as_ref(), parent, &mut archive_writer)?;
27    archive_writer.finish().map_err(Error::io)
28}
29
30/// Compresses a source file or directory to a destination writer with password encryption.
31///
32/// # Arguments
33/// * `src` - Path to the source file or directory to compress
34/// * `dest` - Writer that implements `Write + Seek` to write the compressed archive to
35/// * `password` - Password to encrypt the archive with
36#[cfg(feature = "aes256")]
37#[cfg_attr(
38    docsrs,
39    doc(cfg(all(feature = "aes256", feature = "compress", feature = "util")))
40)]
41pub fn compress_encrypted<W: Write + Seek>(
42    src: impl AsRef<Path>,
43    dest: W,
44    password: Password,
45) -> Result<W, Error> {
46    let mut archive_writer = ArchiveWriter::new(dest)?;
47    if !password.is_empty() {
48        archive_writer.set_content_methods(vec![
49            AesEncoderOptions::new(password).into(),
50            EncoderMethod::LZMA2.into(),
51        ]);
52    }
53    let parent = if src.as_ref().is_dir() {
54        src.as_ref()
55    } else {
56        src.as_ref().parent().unwrap_or(src.as_ref())
57    };
58    compress_path(src.as_ref(), parent, &mut archive_writer)?;
59    archive_writer.finish().map_err(Error::io)
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
69#[cfg_attr(docsrs, doc(cfg(all(feature = "compress", feature = "util"))))]
70pub fn compress_to_path(src: impl AsRef<Path>, dest: impl AsRef<Path>) -> Result<(), Error> {
71    if let Some(path) = dest.as_ref().parent() {
72        if !path.exists() {
73            std::fs::create_dir_all(path)
74                .map_err(|e| Error::io_msg(e, format!("Create dir failed:{:?}", dest.as_ref())))?;
75        }
76    }
77    compress(
78        src,
79        File::create(dest.as_ref())
80            .map_err(|e| Error::file_open(e, dest.as_ref().to_string_lossy().to_string()))?,
81    )?;
82    Ok(())
83}
84
85/// Compresses a source file or directory to a destination file path with password encryption.
86///
87/// This is a convenience function that handles file creation automatically.
88///
89/// # Arguments
90/// * `src` - Path to the source file or directory to compress
91/// * `dest` - Path where the encrypted compressed archive will be created
92/// * `password` - Password to encrypt the archive with
93#[cfg(feature = "aes256")]
94#[cfg_attr(
95    docsrs,
96    doc(cfg(all(feature = "aes256", feature = "compress", feature = "util")))
97)]
98pub fn compress_to_path_encrypted(
99    src: impl AsRef<Path>,
100    dest: impl AsRef<Path>,
101    password: Password,
102) -> Result<(), Error> {
103    if let Some(path) = dest.as_ref().parent() {
104        if !path.exists() {
105            std::fs::create_dir_all(path)
106                .map_err(|e| Error::io_msg(e, format!("Create dir failed:{:?}", dest.as_ref())))?;
107        }
108    }
109    compress_encrypted(
110        src,
111        File::create(dest.as_ref())
112            .map_err(|e| Error::file_open(e, dest.as_ref().to_string_lossy().to_string()))?,
113        password,
114    )?;
115    Ok(())
116}
117
118fn compress_path<W: Write + Seek, P: AsRef<Path>>(
119    src: P,
120    root: &Path,
121    archive_writer: &mut ArchiveWriter<W>,
122) -> Result<(), Error> {
123    let entry_name = src
124        .as_ref()
125        .strip_prefix(root)
126        .map_err(|e| Error::other(e.to_string()))?
127        .to_string_lossy()
128        .to_string();
129    let entry = ArchiveEntry::from_path(src.as_ref(), entry_name);
130    let path = src.as_ref();
131    if path.is_dir() {
132        archive_writer.push_archive_entry::<&[u8]>(entry, None)?;
133        for dir in path
134            .read_dir()
135            .map_err(|e| Error::io_msg(e, "error read dir"))?
136        {
137            let dir = dir.map_err(Error::io)?;
138            let ftype = dir.file_type().map_err(Error::io)?;
139            if ftype.is_dir() || ftype.is_file() {
140                compress_path(dir.path(), root, archive_writer)?;
141            }
142        }
143    } else {
144        archive_writer.push_archive_entry(
145            entry,
146            Some(
147                File::open(path)
148                    .map_err(|e| Error::file_open(e, path.to_string_lossy().to_string()))?,
149            ),
150        )?;
151    }
152    Ok(())
153}
154
155impl<W: Write + Seek> ArchiveWriter<W> {
156    /// Adds a source path to the compression builder with a filter function using solid compression.
157    ///
158    /// The filter function allows selective inclusion of files based on their paths.
159    /// Files are compressed using solid compression for better compression ratios.
160    ///
161    /// # Arguments
162    /// * `path` - Path to add to the compression
163    /// * `filter` - Function that returns `true` for paths that should be included
164    #[cfg_attr(docsrs, doc(cfg(all(feature = "compress", feature = "util"))))]
165    pub fn push_source_path(
166        &mut self,
167        path: impl AsRef<Path>,
168        filter: impl Fn(&Path) -> bool,
169    ) -> Result<&mut Self, Error> {
170        encode_path(true, &path, self, filter)?;
171        Ok(self)
172    }
173
174    /// Adds a source path to the compression builder with a filter function using non-solid compression.
175    ///
176    /// Non-solid compression allows individual file extraction without decompressing the entire archive,
177    /// but typically results in larger archive sizes compared to solid compression.
178    ///
179    /// # Arguments
180    /// * `path` - Path to add to the compression
181    /// * `filter` - Function that returns `true` for paths that should be included
182    #[cfg_attr(docsrs, doc(cfg(all(feature = "compress", feature = "util"))))]
183    pub fn push_source_path_non_solid(
184        &mut self,
185        path: impl AsRef<Path>,
186        filter: impl Fn(&Path) -> bool,
187    ) -> Result<&mut Self, Error> {
188        encode_path(false, &path, self, filter)?;
189        Ok(self)
190    }
191}
192
193fn collect_file_paths(
194    src: impl AsRef<Path>,
195    paths: &mut Vec<PathBuf>,
196    filter: &dyn Fn(&Path) -> bool,
197) -> std::io::Result<()> {
198    let path = src.as_ref();
199    if !filter(path) {
200        return Ok(());
201    }
202    if path.is_dir() {
203        for dir in path.read_dir()? {
204            let dir = dir?;
205            let ftype = dir.file_type()?;
206            if ftype.is_file() || ftype.is_dir() {
207                collect_file_paths(dir.path(), paths, filter)?;
208            }
209        }
210    } else {
211        paths.push(path.to_path_buf())
212    }
213    Ok(())
214}
215
216const MAX_BLOCK_SIZE: u64 = 4 * 1024 * 1024 * 1024; // 4 GiB
217
218fn encode_path<W: Write + Seek>(
219    solid: bool,
220    src: impl AsRef<Path>,
221    zip: &mut ArchiveWriter<W>,
222    filter: impl Fn(&Path) -> bool,
223) -> Result<(), Error> {
224    let mut entries = Vec::new();
225    let mut paths = Vec::new();
226    collect_file_paths(&src, &mut paths, &filter).map_err(|e| {
227        Error::io_msg(
228            e,
229            format!("Failed to collect entries from path:{:?}", src.as_ref()),
230        )
231    })?;
232    if !solid {
233        for ele in paths.into_iter() {
234            let name = ele
235                .strip_prefix(&src)
236                .unwrap()
237                .to_string_lossy()
238                .to_string();
239            zip.push_archive_entry(
240                ArchiveEntry::from_path(ele.as_path(), name),
241                Some(File::open(ele.as_path()).map_err(Error::io)?),
242            )?;
243        }
244        return Ok(());
245    }
246    let mut files = Vec::new();
247    let mut file_size = 0;
248    for ele in paths.into_iter() {
249        let size = ele.metadata()?.len();
250        let name = ele
251            .strip_prefix(&src)
252            .unwrap()
253            .to_string_lossy()
254            .to_string();
255        if size >= MAX_BLOCK_SIZE {
256            zip.push_archive_entry(
257                ArchiveEntry::from_path(ele.as_path(), name),
258                Some(File::open(ele.as_path()).map_err(Error::io)?),
259            )?;
260            continue;
261        }
262        if file_size + size >= MAX_BLOCK_SIZE {
263            zip.push_archive_entries(entries, files)?;
264            entries = Vec::new();
265            files = Vec::new();
266            file_size = 0;
267        }
268        file_size += size;
269        entries.push(ArchiveEntry::from_path(ele.as_path(), name));
270        files.push(LazyFileReader::new(ele).into());
271    }
272    if !entries.is_empty() {
273        zip.push_archive_entries(entries, files)?;
274    }
275
276    Ok(())
277}