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