Skip to main content

kget/
utils.rs

1//! Utility functions for KGet.
2//!
3//! This module provides helper functions used throughout the library:
4//! - Console output management
5//! - URL filename extraction
6//! - Path resolution
7//!
8//! # Example
9//!
10//! ```rust
11//! use kget::get_filename_from_url_or_default;
12//!
13//! let name = get_filename_from_url_or_default(
14//!     "https://example.com/downloads/file.zip",
15//!     "download"
16//! );
17//! assert_eq!(name, "file.zip");
18//! ```
19
20/// Print a message to the console if not in quiet mode.
21///
22/// # Arguments
23///
24/// * `msg` - The message to print
25/// * `quiet_mode` - If true, suppress printing the message
26///
27/// # Example
28///
29/// ```rust
30/// use kget::print;
31///
32/// print("Starting download...", false); // Prints to stdout
33/// print("Starting download...", true);  // Suppressed
34/// ```
35pub fn print(msg: &str, quiet_mode: bool) {
36    if !quiet_mode {
37        println!("{}", msg);
38    }
39}
40
41/// Extract the filename from a URL or return a default.
42///
43/// Parses the URL and returns the last path segment as the filename.
44/// If parsing fails or the path is empty, returns the default filename.
45///
46/// # Arguments
47///
48/// * `url_str` - URL to extract filename from
49/// * `default_filename` - Fallback filename if extraction fails
50///
51/// # Returns
52///
53/// The extracted filename or the default.
54///
55/// # Example
56///
57/// ```rust
58/// use kget::get_filename_from_url_or_default;
59///
60/// // Successful extraction
61/// assert_eq!(
62///     get_filename_from_url_or_default("https://example.com/file.zip", "default"),
63///     "file.zip"
64/// );
65///
66/// // Fallback to default
67/// assert_eq!(
68///     get_filename_from_url_or_default("https://example.com/", "download.bin"),
69///     "download.bin"
70/// );
71/// ```
72pub fn get_filename_from_url_or_default(url_str: &str, default_filename: &str) -> String {
73    // Tries to parse the URL
74    if let Ok(parsed_url) = url::Url::parse(url_str) {
75        // Tries to get the last segment of the path
76        if let Some(segments) = parsed_url.path_segments() {
77            if let Some(last_segment) = segments.last() {
78                if !last_segment.is_empty() {
79                    return last_segment.to_string();
80                }
81            }
82        }
83    }
84    // Returns the default filename if parsing fails or the path is empty/invalid
85    default_filename.to_string()
86}
87
88/// Resolve the final output path for a download.
89///
90/// Handles three cases:
91/// 1. `output_arg` is `None`: Extract filename from URL
92/// 2. `output_arg` is a directory: Append filename from URL to directory
93/// 3. `output_arg` is a file path: Use it directly
94///
95/// # Arguments
96///
97/// * `output_arg` - User-provided output path (can be file or directory)
98/// * `url` - Source URL for filename extraction
99/// * `default_name` - Fallback filename if URL doesn't contain one
100///
101/// # Returns
102///
103/// The resolved output file path.
104///
105/// # Example
106///
107/// ```rust
108/// use kget::resolve_output_path;
109///
110/// // No output specified - use filename from URL
111/// let path = resolve_output_path(None, "https://example.com/file.zip", "download");
112/// assert_eq!(path, "file.zip");
113///
114/// // Custom filename
115/// let path = resolve_output_path(Some("myfile.zip".to_string()), "https://example.com/file.zip", "download");
116/// assert_eq!(path, "myfile.zip");
117/// ```
118pub fn resolve_output_path(output_arg: Option<String>, url: &str, default_name: &str) -> String {
119    if let Some(path_str) = output_arg {
120        let path = std::path::Path::new(&path_str);
121        if path.is_dir() {
122            let filename = get_filename_from_url_or_default(url, default_name);
123            path.join(filename).to_string_lossy().to_string()
124        } else {
125            path_str
126        }
127    } else {
128        get_filename_from_url_or_default(url, default_name)
129    }
130}
131
132/// Returns `true` when the path's extension is a supported archive format.
133pub fn is_extractable(path: &std::path::Path) -> bool {
134    let name = path
135        .file_name()
136        .and_then(|n| n.to_str())
137        .unwrap_or("")
138        .to_lowercase();
139    name.ends_with(".zip")
140        || name.ends_with(".tar.gz")
141        || name.ends_with(".tgz")
142        || name.ends_with(".tar.bz2")
143        || name.ends_with(".tbz2")
144        || name.ends_with(".tar.xz")
145        || name.ends_with(".txz")
146        || name.ends_with(".7z")
147}
148
149/// Extract an archive at `path` into its parent directory using the system tool
150/// (`unzip`, `tar`, or `7z`).  Returns `Ok(())` if the path is not a recognised
151/// archive type (no-op) or if extraction succeeds.
152///
153/// # Errors
154///
155/// Returns an error if the tool is not found or exits with a non-zero status.
156pub fn auto_extract(
157    path: &std::path::Path,
158    quiet: bool,
159) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
160    let path_str = path.to_str().ok_or("Invalid UTF-8 in path")?;
161    let dir_str = path
162        .parent()
163        .and_then(|d| d.to_str())
164        .filter(|s| !s.is_empty())
165        .unwrap_or(".");
166    let name = path
167        .file_name()
168        .and_then(|n| n.to_str())
169        .unwrap_or("")
170        .to_lowercase();
171
172    let (cmd, args): (&str, Vec<String>) = if name.ends_with(".zip") {
173        (
174            "unzip",
175            vec![
176                "-o".into(),
177                path_str.into(),
178                "-d".into(),
179                dir_str.into(),
180            ],
181        )
182    } else if name.ends_with(".tar.gz") || name.ends_with(".tgz") {
183        (
184            "tar",
185            vec![
186                "-xzf".into(),
187                path_str.into(),
188                "-C".into(),
189                dir_str.into(),
190            ],
191        )
192    } else if name.ends_with(".tar.bz2") || name.ends_with(".tbz2") {
193        (
194            "tar",
195            vec![
196                "-xjf".into(),
197                path_str.into(),
198                "-C".into(),
199                dir_str.into(),
200            ],
201        )
202    } else if name.ends_with(".tar.xz") || name.ends_with(".txz") {
203        (
204            "tar",
205            vec![
206                "-xJf".into(),
207                path_str.into(),
208                "-C".into(),
209                dir_str.into(),
210            ],
211        )
212    } else if name.ends_with(".7z") {
213        (
214            "7z",
215            vec!["x".into(), path_str.into(), format!("-o{dir_str}")],
216        )
217    } else {
218        return Ok(());
219    };
220
221    if !quiet {
222        println!("Extracting {} …", path.display());
223    }
224
225    let status = std::process::Command::new(cmd)
226        .args(&args)
227        .status()
228        .map_err(|e| format!("Failed to run `{cmd}`: {e} — is it installed?"))?;
229
230    if !status.success() {
231        return Err(format!(
232            "`{cmd}`: extraction failed (exit {:?})",
233            status.code()
234        )
235        .into());
236    }
237
238    if !quiet {
239        println!("Extracted to: {dir_str}");
240    }
241    Ok(())
242}