markrs/
io.rs

1//! This module provides functionality related to reading/writing files.
2
3use std::fs;
4use std::path::PathBuf;
5use std::{
6    fs::{File, ReadDir, create_dir_all, read_dir},
7    io,
8    io::{Read, Write},
9    path::Path,
10};
11
12use dirs::config_dir;
13use log::{error, info};
14
15use crate::config::Config;
16use crate::html_generator::generate_default_css;
17
18/// Reads all markdown files from the specified input directory and returns their contents.
19///
20/// # Arguments
21/// * `input_dir` - The directory containing markdown files.
22///
23/// # Returns
24/// Returns a `Result` containing a vector of tuples, where each tuple contains the file name
25/// and its contents as a string.
26pub fn read_input_dir(
27    input_dir: &str,
28    run_recursively: &bool,
29    excluded_entries: &[String],
30) -> Result<Vec<(String, String)>, io::Error> {
31    if *run_recursively {
32        // If recursive, visit all subdirectories
33        let mut file_contents: Vec<(String, String)> = Vec::new();
34        let input_dir = Path::new(input_dir);
35        visit_dir(
36            Path::new(input_dir),
37            input_dir,
38            &mut file_contents,
39            excluded_entries,
40        )
41        .map_err(|e| {
42            error!(
43                "Failed to read input directory '{}': {}",
44                input_dir.display(),
45                e
46            );
47            e
48        })?;
49
50        Ok(file_contents)
51    } else {
52        let entries: ReadDir = read_dir(input_dir).map_err(|e| {
53            error!("Failed to read input directory '{input_dir}': {e}");
54            e
55        })?;
56
57        // Collect the contents of all markdown files in the directory
58        let mut file_contents: Vec<(String, String)> = Vec::new();
59        for entry in entries {
60            let entry = entry?;
61
62            let file_path = entry.path();
63            let file_name = file_path
64                .file_name()
65                .and_then(|s| s.to_str())
66                .ok_or_else(|| {
67                    io::Error::new(
68                        io::ErrorKind::InvalidData,
69                        format!(
70                            "Failed to extract file name from path '{}'",
71                            file_path.display()
72                        ),
73                    )
74                })?
75                .to_string();
76
77            if excluded_entries.contains(&file_name) {
78                continue;
79            }
80
81            if file_path.extension().and_then(|s| s.to_str()) == Some("md") {
82                let contents = read_file(file_path.to_str().unwrap()).map_err(|e| {
83                    io::Error::other(format!(
84                        "Failed to read file '{}': {}",
85                        file_path.display(),
86                        e
87                    ))
88                })?;
89                file_contents.push((file_name, contents));
90            }
91        }
92
93        Ok(file_contents)
94    }
95}
96
97/// Helper function to recursively visit subdirectories and collect markdown file contents.
98fn visit_dir(
99    dir: &Path,
100    base: &Path,
101    file_contents: &mut Vec<(String, String)>,
102    excluded_entries: &[String],
103) -> Result<(), std::io::Error> {
104    for entry in read_dir(dir)? {
105        let entry = entry?;
106        let path = entry.path();
107        let relative_path = path
108            .strip_prefix(base)
109            .map_err(|e| io::Error::other(format!("Failed to strip base path: {e}")))?
110            .to_string_lossy()
111            .to_string();
112
113        if excluded_entries.contains(&relative_path) {
114            continue;
115        }
116
117        if path.is_dir() {
118            visit_dir(&path, base, file_contents, excluded_entries)?;
119        } else if path.extension().and_then(|s| s.to_str()) == Some("md") {
120            let rel_path = path
121                .strip_prefix(base)
122                .map_err(|e| io::Error::other(format!("Failed to strip base path: {e}")))?
123                .to_string_lossy()
124                .to_string();
125            let contents = read_file(path.to_str().unwrap()).map_err(|e| {
126                io::Error::other(format!("Failed to read file '{}': {}", path.display(), e))
127            })?;
128
129            file_contents.push((rel_path, contents));
130        }
131    }
132
133    Ok(())
134}
135
136/// Reads the contents of a file into a String.
137///
138/// # Arguments
139/// * `file_path` - The path of the file to read.
140///
141/// # Returns
142/// Returns a `Result` containing the file contents as a string on success,
143/// or an error message on failure.
144pub fn read_file(file_path: &str) -> Result<String, std::io::Error> {
145    let mut md_file: File = File::open(file_path)?;
146
147    let mut contents = String::new();
148    md_file.read_to_string(&mut contents)?;
149
150    Ok(contents)
151}
152
153/// Writes the provided HTML string to a file in the specified output directory.
154///
155/// # Arguments
156/// * `html` - The HTML content to write to the file.
157/// * `output_dir` - The directory where the HTML file should be saved.
158/// * `input_filename` - The name of the input markdown file (used to derive the output filename).
159///
160/// # Returns
161/// Returns a `Result` indicating success or failure.
162pub fn write_html_to_file(
163    html: &str,
164    output_dir: &str,
165    input_filepath: &str,
166) -> Result<(), io::Error> {
167    info!("Writing output to directory: {}", output_dir);
168    let output_dir = Path::new(output_dir).join(input_filepath);
169
170    if let Some(parent) = output_dir.parent() {
171        create_dir_all(parent)?;
172    }
173
174    let mut output_file = File::create(&output_dir)?;
175
176    output_file.write_all(html.as_bytes())?;
177
178    info!("HTML written to: {}", output_dir.display());
179    Ok(())
180}
181
182/// Copies a file from the input path to the specified output directory, optionally creating a
183/// subdirectory.
184///
185/// # Arguments
186/// * `input_file_path` - The path of the file to copy.
187/// * `output_dir` - The directory where the file should be copied.
188/// * `subdir` - An optional subdirectory within the output directory.
189/// * `base_dir` - An optional base directory to resolve relative paths.
190///
191/// # Returns
192/// Returns a `Result` indicating success or failure. If successful, the file is copied to the
193/// output directory.
194pub fn copy_file_to_output_dir(
195    input_file_path: &str,
196    output_dir: &str,
197    subdir: Option<&str>,
198    base_dir: Option<&str>,
199) -> Result<(), io::Error> {
200    use std::path::PathBuf;
201
202    let abs_input_path = if let Some(base) = base_dir {
203        let input_path = Path::new(input_file_path);
204        if input_path.is_absolute() {
205            input_path.to_path_buf()
206        } else {
207            Path::new(base).join(input_file_path)
208        }
209    } else {
210        PathBuf::from(input_file_path)
211    };
212
213    let file_name = abs_input_path.file_name().ok_or_else(|| {
214        io::Error::new(
215            io::ErrorKind::InvalidInput,
216            format!(
217                "Failed to extract file name from path '{}'",
218                abs_input_path.display()
219            ),
220        )
221    })?;
222
223    let mut output_file_path = PathBuf::from(output_dir);
224    if let Some(sub) = subdir {
225        output_file_path.push(sub);
226        create_dir_all(&output_file_path)?;
227    } else {
228        create_dir_all(&output_file_path)?
229    }
230    output_file_path.push(file_name);
231
232    fs::copy(&abs_input_path, &output_file_path)?;
233
234    Ok(())
235}
236
237/// Copies a favicon file to the specified output directory.
238pub fn copy_favicon_to_output_dir(
239    input_file_path: &str,
240    output_dir: &str,
241) -> Result<(), io::Error> {
242    copy_file_to_output_dir(input_file_path, output_dir, Some("media"), None)
243}
244
245/// Copies an image file to the specified output directory.
246pub fn copy_image_to_output_dir(
247    input_file_path: &str,
248    output_dir: &str,
249    md_dir: &str,
250) -> Result<(), io::Error> {
251    copy_file_to_output_dir(input_file_path, output_dir, Some("media"), Some(md_dir))
252}
253
254/// Copies a CSS file to the specified output directory.
255pub fn copy_css_to_output_dir(input_file_path: &str, output_dir: &str) -> Result<(), io::Error> {
256    copy_file_to_output_dir(input_file_path, output_dir, None, None)
257}
258
259/// Writes a default CSS file to the specified output directory.
260pub fn write_default_css_file(output_dir: &str) -> Result<(), io::Error> {
261    let css_content = generate_default_css();
262    let css_file_path = format!("{}/styles.css", output_dir);
263
264    let mut file = File::create(&css_file_path)?;
265
266    file.write_all(css_content.as_bytes())?;
267
268    Ok(())
269}
270
271/// Returns the OS-specific configuration path.
272///
273/// This function creates a directory named "markrs" in the user's configuration directory.
274pub fn get_config_path() -> Result<PathBuf, io::Error> {
275    let mut config_path = config_dir().unwrap_or_else(|| PathBuf::from("."));
276
277    config_path.push("markrs");
278    create_dir_all(&config_path)?;
279    config_path.push("config.toml");
280
281    Ok(config_path)
282}
283
284/// Checks if the configuration file exists at the specified path.
285pub fn does_config_exist() -> Result<bool, io::Error> {
286    let config_path = get_config_path()?;
287
288    let config_exists = config_path.exists();
289    Ok(config_exists)
290}
291
292/// Writes the default configuration to the configuration file to the OS-specific default configuration
293/// path.
294pub fn write_default_config() -> Result<Config, crate::config::Error> {
295    let config_path = get_config_path()?;
296
297    info!(
298        "Config file does not exist, creating default config at: {}",
299        config_path.display()
300    );
301
302    let mut file = File::create(&config_path)?;
303
304    let default_config = Config::default();
305
306    let default_config_content = toml_edit::ser::to_string_pretty(&default_config)
307        .map_err(crate::config::Error::TomlSerialization)?;
308
309    file.write_all(default_config_content.as_bytes())?;
310
311    info!("Default config file created at: {}", config_path.display());
312
313    Ok(default_config)
314}