Skip to main content

magic_pack/
service.rs

1use std::fmt;
2use std::fs;
3use std::io::ErrorKind;
4use std::panic::{catch_unwind, AssertUnwindSafe};
5use std::path::{Path, PathBuf};
6
7use crate::contents::enums::{self, FileType};
8use crate::modules;
9
10#[derive(Debug, Clone)]
11pub struct CompressRequest {
12    pub file_type: FileType,
13    pub input: PathBuf,
14    pub output: PathBuf,
15}
16
17#[derive(Debug, Clone)]
18pub struct DecompressRequest {
19    pub input: PathBuf,
20    pub output: PathBuf,
21    pub level: i8,
22}
23
24#[derive(Debug, Clone)]
25pub struct OperationResult {
26    pub output_path: PathBuf,
27    pub message: String,
28}
29
30#[derive(Debug)]
31pub enum MagicPackError {
32    Io(std::io::Error),
33    UnsupportedFileType,
34    InvalidInput(String),
35    OperationFailed(String),
36}
37
38impl fmt::Display for MagicPackError {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            MagicPackError::Io(err) => write!(f, "{}", err),
42            MagicPackError::UnsupportedFileType => write!(f, "unsupported file type"),
43            MagicPackError::InvalidInput(message) => write!(f, "{}", message),
44            MagicPackError::OperationFailed(message) => write!(f, "{}", message),
45        }
46    }
47}
48
49impl std::error::Error for MagicPackError {}
50
51impl From<std::io::Error> for MagicPackError {
52    fn from(err: std::io::Error) -> Self {
53        match err.kind() {
54            ErrorKind::Unsupported => MagicPackError::UnsupportedFileType,
55            _ => MagicPackError::Io(err),
56        }
57    }
58}
59
60pub fn supported_formats() -> Vec<&'static str> {
61    vec![
62        "zip", "tar", "bz2", "gz", "tar.bz2", "tar.gz", "7z", "xz", "tar.xz", "zst", "tar.zst",
63        "lz4", "tar.lz4",
64    ]
65}
66
67pub fn detect_file_type(path: &Path) -> Result<FileType, MagicPackError> {
68    modules::get_file_type(&path.to_path_buf()).map_err(MagicPackError::from)
69}
70
71pub fn compress(req: CompressRequest) -> Result<OperationResult, MagicPackError> {
72    validate_compress_request(&req)?;
73
74    let output_path = if req.output == Path::new(".") {
75        default_compress_output_path(&req.input, &req.output, req.file_type)?
76    } else {
77        req.output.clone()
78    };
79
80    run_operation("compress", || {
81        modules::compress(req.file_type, &req.input, &output_path);
82    })?;
83
84    Ok(OperationResult {
85        output_path,
86        message: format!(
87            "compressed as {}",
88            enums::get_file_type_string(req.file_type)
89        ),
90    })
91}
92
93pub fn decompress(req: DecompressRequest) -> Result<OperationResult, MagicPackError> {
94    validate_decompress_request(&req)?;
95
96    if req.output != Path::new(".") {
97        fs::create_dir_all(&req.output)?;
98    }
99
100    let src_filename = req.input.file_stem().ok_or_else(|| {
101        MagicPackError::InvalidInput("input path must include a file name".into())
102    })?;
103
104    let mut decompress_output = req.output.join(src_filename);
105    let mut decompress_input = req.input.clone();
106    let filename = decompress_output.file_name().ok_or_else(|| {
107        MagicPackError::InvalidInput("output path must include a file name".into())
108    })?;
109    let mg_filename = format!("mg_{}", filename.to_string_lossy());
110    decompress_output.set_file_name(mg_filename);
111
112    for index in 0..req.level {
113        let file_type = match detect_file_type(&decompress_input) {
114            Ok(file_type) => file_type,
115            Err(MagicPackError::UnsupportedFileType) if index != 0 => break,
116            Err(err) => return Err(err),
117        };
118
119        let current_output = decompress_output.clone();
120        run_operation("decompress", || {
121            modules::decompress(file_type, &decompress_input, &current_output);
122        })?;
123        decompress_input = current_output;
124        let temp_filename = decompress_input.file_stem().ok_or_else(|| {
125            MagicPackError::InvalidInput("decompressed output must include a file name".into())
126        })?;
127        decompress_output.set_file_name(temp_filename);
128    }
129
130    let final_filename = decompress_input
131        .file_name()
132        .ok_or_else(|| {
133            MagicPackError::InvalidInput("decompressed output must include a file name".into())
134        })?
135        .to_string_lossy()
136        .replace("mg_", "");
137    let mut final_output = decompress_input.clone();
138    final_output.set_file_name(final_filename);
139    fs::rename(&decompress_input, &final_output)?;
140
141    Ok(OperationResult {
142        output_path: final_output,
143        message: String::from("decompressed"),
144    })
145}
146
147fn validate_compress_request(req: &CompressRequest) -> Result<(), MagicPackError> {
148    if !req.input.exists() {
149        return Err(MagicPackError::InvalidInput(format!(
150            "input path does not exist: {}",
151            req.input.display()
152        )));
153    }
154
155    if req.output == Path::new(".") {
156        return Ok(());
157    }
158
159    if let Some(parent) = req.output.parent() {
160        if !parent.as_os_str().is_empty() {
161            fs::create_dir_all(parent)?;
162        }
163    }
164
165    Ok(())
166}
167
168fn validate_decompress_request(req: &DecompressRequest) -> Result<(), MagicPackError> {
169    if !req.input.exists() {
170        return Err(MagicPackError::InvalidInput(format!(
171            "input path does not exist: {}",
172            req.input.display()
173        )));
174    }
175
176    if req.level <= 0 {
177        return Err(MagicPackError::InvalidInput(
178            "decompress level must be greater than 0".into(),
179        ));
180    }
181
182    Ok(())
183}
184
185fn default_compress_output_path(
186    src_path: &Path,
187    dst_path: &Path,
188    file_type: FileType,
189) -> Result<PathBuf, MagicPackError> {
190    let filename = src_path.file_stem().ok_or_else(|| {
191        MagicPackError::InvalidInput("input path must include a file name".into())
192    })?;
193    let mut temp_output = dst_path.join(filename);
194    temp_output.set_extension(enums::get_file_type_string(file_type));
195    Ok(temp_output)
196}
197
198fn run_operation<F>(label: &str, operation: F) -> Result<(), MagicPackError>
199where
200    F: FnOnce(),
201{
202    catch_unwind(AssertUnwindSafe(operation)).map_err(|panic_payload| {
203        let message = if let Some(message) = panic_payload.downcast_ref::<&str>() {
204            (*message).to_string()
205        } else if let Some(message) = panic_payload.downcast_ref::<String>() {
206            message.clone()
207        } else {
208            format!("{} failed", label)
209        };
210        MagicPackError::OperationFailed(message)
211    })
212}