mame_parser/core/file_handling/
file_unpacker.rs

1use crate::helpers::file_system_helpers::{
2    ensure_folder_exists, find_file_with_pattern, WORKSPACE_PATHS,
3};
4use crate::{
5    core::models::{
6        callback_progress::{CallbackType, ProgressCallback, ProgressInfo, SharedProgressCallback},
7        mame_data_types::{get_data_type_details, MameDataType},
8    },
9    helpers::callback_progress_helper::get_progress_info,
10};
11use sevenz_rust::Password;
12use std::error::Error;
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15use std::thread;
16use std::{fs::File, io::Write};
17use zip::ZipArchive;
18
19/// Unpacks a data file for a specific `MameDataType` into a designated workspace folder.
20///
21/// This function checks if the required data file for the specified `MameDataType` is already unpacked.
22/// If not, it searches for the corresponding ZIP file in the download directory, and if found,
23/// unpacks it into the appropriate folder. Progress updates during the process can be provided via a callback function.
24///
25/// # Parameters
26/// - `data_type`: The `MameDataType` that specifies the type of data file to unpack (e.g., Series, Categories).
27/// - `workspace_path`: A reference to a `Path` representing the base directory where the data file will be unpacked.
28/// - `progress_callback`: A callback function of type `ProgressCallback` that provides progress updates during the unpacking process.
29///   The callback receives a `ProgressInfo` struct containing `progress`, `total`, `message`, and `callback_type`.
30///
31/// # Returns
32/// Returns a `Result<PathBuf, Box<dyn Error + Send + Sync>>`:
33/// - On success: Contains the path where the unpacked file is located.
34/// - On failure: Contains an error if the file cannot be unpacked, if the ZIP file is not found,
35///   or if there are issues creating the destination folder.
36///
37/// # Errors
38/// This function will return an error if:
39/// - The destination folder cannot be created.
40/// - The required ZIP file is not found in the download folder.
41/// - The unpacking process fails due to reading or writing errors.
42///
43/// # Callback
44/// The progress callback function can be used to monitor the unpacking process in real-time. It receives:
45/// - `progress`: The number of entries processed so far.
46/// - `total`: The total entries of the file being unpacked.
47/// - `message`: A status message indicating the current operation (e.g., "Unpacking file", "Checking if file already unpacked").
48/// - `callback_type`: The type of callback, typically `CallbackType::Progress` for ongoing updates, `CallbackType::Info` for informational messages, `CallbackType::Finish` for completion, or `CallbackType::Error` for errors.
49///
50/// # Example
51#[doc = docify::embed!("examples/unpack_file.rs", main)]
52///
53pub fn unpack_file(
54    data_type: MameDataType,
55    workspace_path: &Path,
56    progress_callback: ProgressCallback,
57) -> Result<PathBuf, Box<dyn Error + Send + Sync>> {
58    // Retrieves the details for a given `MameDataType`
59    let data_type_details = get_data_type_details(data_type);
60
61    // Creates a folder if it does not exist.
62    let extract_folder = workspace_path
63        .join(WORKSPACE_PATHS.extract_path)
64        .join(data_type_details.name.to_lowercase());
65
66    let folder_created = ensure_folder_exists(&extract_folder);
67    if let Err(err) = folder_created {
68        return Err(Box::new(err));
69    }
70
71    // Checks if file already unpacked
72    progress_callback(get_progress_info(
73        format!(
74            "Checking if {} file already unpacked",
75            data_type_details.name
76        )
77        .as_str(),
78    ));
79
80    if let Ok(existing_data_file) = find_file_with_pattern(
81        &extract_folder.to_str().unwrap(),
82        &data_type_details.data_file_pattern,
83    ) {
84        progress_callback(ProgressInfo {
85            progress: 0,
86            total: 0,
87            message: format!("{} file already unpacked", data_type_details.name),
88            callback_type: CallbackType::Finish,
89        });
90
91        return Ok(existing_data_file.into());
92    }
93
94    // Checks if zip file is present
95    progress_callback(get_progress_info(
96        format!("Checking if {} zip file exists", data_type_details.name).as_str(),
97    ));
98
99    let download_folder = workspace_path.join(WORKSPACE_PATHS.download_path);
100    let zip_file_path = find_file_with_pattern(
101        &download_folder.to_str().unwrap(),
102        &data_type_details.zip_file_pattern,
103    );
104
105    match zip_file_path {
106        // Unpack the file
107        Ok(zip_file_path) => {
108            let zip_file = zip_file_path.split('/').last().unwrap();
109
110            progress_callback(get_progress_info(
111                format!("Unpacking {}", zip_file).as_str(),
112            ));
113
114            let unpack_result = unpack(&zip_file_path, &extract_folder, &progress_callback);
115
116            // Check if unpacking was successful
117            match unpack_result {
118                Ok(_) => {
119                    if let Ok(existing_data_file) = find_file_with_pattern(
120                        &extract_folder.to_str().unwrap(),
121                        &data_type_details.data_file_pattern,
122                    ) {
123                        return Ok(existing_data_file.into());
124                    } else {
125                        let message = format!(
126                            "{} data file not present after unpacking",
127                            data_type_details.name
128                        );
129                        progress_callback(ProgressInfo {
130                            progress: 0,
131                            total: 0,
132                            message: message.clone(),
133                            callback_type: CallbackType::Error,
134                        });
135
136                        return Err(message.into());
137                    }
138                }
139                Err(err) => {
140                    return Err(err.into());
141                }
142            }
143        }
144        Err(err) => {
145            let message = format!("{} zip file not found", data_type_details.name);
146
147            progress_callback(ProgressInfo {
148                progress: 0,
149                total: 0,
150                message: message.clone(),
151                callback_type: CallbackType::Error,
152            });
153
154            return Err(err.into());
155        }
156    }
157}
158
159/// Unpacks multiple data files concurrently for all `MameDataType` variants into a designated workspace folder.
160///
161/// This function spawns a new thread for each data type to unpack its respective file, allowing for concurrent unpacking operations.
162/// Progress for each unpacking operation is reported via a provided shared callback function. The function returns a list of
163/// thread handles, each of which can be used to join and retrieve the result of the unpacking operation.
164///
165/// # Parameters
166/// - `workspace_path`: A reference to a `Path` representing the base directory where the data files will be unpacked.
167/// - `progress_callback`: A shared callback function of type `SharedProgressCallback` that tracks the progress of each file unpacking operation.
168///   The callback receives the `data_type` and a `ProgressInfo` struct containing `progress`, `total`, `message`, and `callback_type`.
169///
170/// # Returns
171/// Returns a `Vec<thread::JoinHandle<Result<PathBuf, Box<dyn Error + Send + Sync>>>>`:
172/// - Each handle represents a thread responsible for unpacking a specific file. The result of the unpacking can be accessed by joining the thread handle.
173/// - On success: Each thread handle contains the path where the unpacked file is located.
174/// - On failure: Each thread handle contains an error if the unpacking fails or if there are issues accessing or creating the destination folder.
175///
176/// # Errors
177/// This function does not directly return errors, but errors may be encountered and reported through the thread handles.
178/// The following errors might occur during the unpacking process:
179/// - The destination folder cannot be created.
180/// - The required ZIP file is not found in the download folder.
181/// - The unpacking process fails due to reading or writing errors.
182///
183/// # Callback
184/// The shared progress callback function can be used to monitor the unpacking process of each file in real-time. It receives:
185/// - `data_type`: An enum value of `MameDataType`, indicating the type of data being unpacked.
186/// - `progress`: The number of entries processed so far.
187/// - `total`: The total entries of the file being unpacked (if available).
188/// - `message`: A status message indicating the current operation (e.g., "Unpacking file", "Checking if file already unpacked").
189/// - `callback_type`: The type of callback, typically `CallbackType::Progress` for ongoing updates, `CallbackType::Info` for informational messages, `CallbackType::Finish` for completion, or `CallbackType::Error` for errors.
190///
191/// # Example
192#[doc = docify::embed!("examples/unpack_files.rs", main)]
193///
194pub fn unpack_files(
195    workspace_path: &Path,
196    progress_callback: SharedProgressCallback,
197) -> Vec<thread::JoinHandle<Result<PathBuf, Box<dyn Error + Send + Sync>>>> {
198    let progress_callback = Arc::new(progress_callback);
199
200    MameDataType::all_variants()
201        .iter()
202        .map(|&data_type| {
203            let workspace_path = workspace_path.to_path_buf();
204            let progress_callback = Arc::clone(&progress_callback);
205
206            thread::spawn(move || {
207                unpack_file(
208                    data_type,
209                    &workspace_path,
210                    Box::new(move |progress_info| {
211                        progress_callback(data_type, progress_info);
212                    }),
213                )
214            })
215        })
216        .collect()
217}
218
219/// Unpacks an archive file (ZIP or 7z) to the specified destination folder.
220///
221/// This function determines the type of archive file based on its extension (`.zip` or `.7z`)
222/// and calls the appropriate extraction function to unpack its contents into the provided folder.
223/// Progress updates during the unpacking process can be provided via a callback function.
224///
225/// # Parameters
226/// - `zip_file_path`: A string slice (`&str`) representing the path to the archive file to be unpacked.
227///   The file must have a `.zip` or `.7z` extension.
228/// - `extract_folder`: A reference to a `Path` representing the destination folder where the contents of the archive will be extracted.
229/// - `progress_callback`: A reference to a callback function of type `ProgressCallback` that provides progress updates during the unpacking process.
230///   The callback receives a `ProgressInfo` struct containing `progress`, `total`, `message`, and `callback_type`.
231///
232/// # Returns
233/// Returns a `Result<PathBuf, Box<dyn Error + Send + Sync>>`:
234/// - On success: Contains the path to the extracted contents in the destination folder.
235/// - On failure: Contains an error if the file cannot be unpacked due to an unsupported format, reading or writing errors, or issues accessing the destination folder.
236///
237/// # Errors
238/// This function will return an error if:
239/// - The archive format is unsupported (i.e., the file does not have a `.zip` or `.7z` extension).
240/// - The destination folder is invalid or inaccessible.
241/// - The extraction process fails due to reading or writing errors.
242fn unpack(
243    zip_file_path: &str,
244    extract_folder: &Path,
245    progress_callback: &ProgressCallback,
246) -> Result<PathBuf, Box<dyn Error + Send + Sync>> {
247    match zip_file_path {
248        path if path.ends_with(".zip") => {
249            return extract_zip(
250                zip_file_path,
251                extract_folder.to_str().unwrap(),
252                progress_callback,
253            );
254        }
255        path if path.ends_with(".7z") => {
256            return extract_7zip(
257                zip_file_path,
258                extract_folder.to_str().unwrap(),
259                progress_callback,
260            );
261        }
262        _ => return Err("Unsupported archive format".into()),
263    }
264}
265
266/// Extracts the contents of a ZIP archive to the specified destination folder.
267///
268/// This function opens a ZIP file, iterates over its contents, and extracts each file or directory
269/// to the specified destination folder. It provides real-time progress updates via a callback function
270/// during the extraction process.
271///
272/// # Parameters
273/// - `archive_path`: A string slice (`&str`) representing the path to the ZIP archive file to be extracted.
274/// - `destination_folder`: A string slice (`&str`) representing the destination folder where the contents of the archive will be extracted.
275/// - `progress_callback`: A reference to a callback function of type `ProgressCallback` that provides progress updates during the extraction process.
276///   The callback receives a `ProgressInfo` struct containing `progress`, `total`, `message`, and `callback_type`.
277///
278/// # Returns
279/// Returns a `Result<PathBuf, Box<dyn Error + Send + Sync>>`:
280/// - On success: Contains the path to the folder where the contents were extracted.
281/// - On failure: Contains an error if the extraction process fails due to reading errors, writing errors, or issues accessing the destination folder.
282///
283/// # Errors
284/// This function will return an error if:
285/// - The ZIP archive cannot be opened or read.
286/// - The destination folder cannot be created or is invalid.
287/// - There are errors during file extraction, such as reading from the archive or writing to the disk.
288fn extract_zip(
289    archive_path: &str,
290    destination_folder: &str,
291    progress_callback: &ProgressCallback,
292) -> Result<PathBuf, Box<dyn Error + Send + Sync>> {
293    let file = File::open(archive_path)?;
294    let mut archive = ZipArchive::new(file)?;
295
296    let total_files = archive.len() as u64;
297    let mut progress: u64 = 0;
298
299    for i in 0..archive.len() {
300        let mut file = archive.by_index(i)?;
301        let output_path = Path::new(destination_folder).join(file.name());
302
303        if (file.name()).ends_with('/') {
304            std::fs::create_dir_all(&output_path)?;
305        } else {
306            if let Some(p) = output_path.parent() {
307                if !p.exists() {
308                    std::fs::create_dir_all(&p)?;
309                }
310            }
311            let mut output_file = File::create(&output_path)?;
312            std::io::copy(&mut file, &mut output_file)?;
313        }
314
315        progress += 1;
316
317        progress_callback(ProgressInfo {
318            progress,
319            total: total_files,
320            message: String::from(""),
321            callback_type: CallbackType::Progress,
322        });
323    }
324
325    let zip_file = archive_path.split('/').last().unwrap();
326    progress_callback(ProgressInfo {
327        progress,
328        total: progress,
329        message: format!("{} unpacked successfully", zip_file),
330        callback_type: CallbackType::Finish,
331    });
332
333    Ok(destination_folder.into())
334}
335
336/// Extracts the contents of a 7z archive to the specified destination folder.
337///
338/// This function opens a 7z archive file, iterates over its contents, and extracts each file or directory
339/// to the specified destination folder. Progress updates during the extraction process can be provided
340/// via a callback function.
341///
342/// # Parameters
343/// - `archive_path`: A string slice (`&str`) representing the path to the 7z archive file to be extracted.
344/// - `destination_folder`: A string slice (`&str`) representing the destination folder where the contents of the archive will be extracted.
345/// - `progress_callback`: A reference to a callback function of type `ProgressCallback` that provides progress updates during the extraction process.
346///   The callback receives a `ProgressInfo` struct containing `progress`, `total`, `message`, and `callback_type`.
347///
348/// # Returns
349/// Returns a `Result<PathBuf, Box<dyn Error + Send + Sync>>`:
350/// - On success: Contains the path to the folder where the contents were extracted.
351/// - On failure: Contains an error if the extraction process fails due to reading errors, writing errors, or issues accessing the destination folder.
352///
353/// # Errors
354/// This function will return an error if:
355/// - The 7z archive cannot be opened or read.
356/// - The destination folder cannot be created or is invalid.
357/// - There are errors during file extraction, such as reading from the archive or writing to the disk.
358/// - The provided 7z archive format is unsupported or corrupted.
359fn extract_7zip(
360    archive_path: &str,
361    destination_folder: &str,
362    progress_callback: &ProgressCallback,
363) -> Result<PathBuf, Box<dyn Error + Send + Sync>> {
364    let mut sz = sevenz_rust::SevenZReader::open(archive_path, Password::empty()).unwrap();
365
366    let total_files = sz.archive().files.len();
367    let mut progress_entries: u64 = 0;
368
369    let dest = PathBuf::from(destination_folder);
370
371    sz.for_each_entries(|entry, reader| {
372        let mut buf = [0u8; 1024];
373        let path = dest.join(entry.name());
374        if entry.is_directory() {
375            std::fs::create_dir_all(path).unwrap();
376            return Ok(true);
377        }
378        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
379        let mut file = File::create(path).unwrap();
380        loop {
381            let read_size = reader.read(&mut buf)?;
382            if read_size == 0 {
383                progress_entries += 1;
384
385                progress_callback(ProgressInfo {
386                    progress: progress_entries,
387                    total: total_files as u64,
388                    message: String::from(""),
389                    callback_type: CallbackType::Progress,
390                });
391
392                break Ok(true);
393            }
394            file.write_all(&buf[..read_size])?;
395        }
396    })
397    .unwrap();
398
399    let zip_file = archive_path.split('/').last().unwrap();
400    progress_callback(ProgressInfo {
401        progress: progress_entries,
402        total: progress_entries,
403        message: format!("{} unpacked successfully", zip_file),
404        callback_type: CallbackType::Finish,
405    });
406
407    Ok(destination_folder.into())
408}