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}