Skip to main content

mangofetch_core/core/
course_utils.rs

1use std::path::Path;
2
3use anyhow::anyhow;
4use tokio_util::sync::CancellationToken;
5
6pub async fn save_description(dir: &str, content: &str, format: &str) -> anyhow::Result<()> {
7    if content.trim().is_empty() {
8        return Ok(());
9    }
10
11    let ext = match format {
12        "markdown" | "md" => "md",
13        "text" | "txt" => "txt",
14        _ => "html",
15    };
16
17    let path = format!("{}/description.{}", dir, ext);
18
19    if Path::new(&path).exists() {
20        return Ok(());
21    }
22
23    let wrapped = if ext == "html"
24        && !content.trim_start().starts_with("<!")
25        && !content.trim_start().starts_with("<html")
26    {
27        format!(
28            "<!DOCTYPE html>\n<html>\n<head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><style>body{{max-width:800px;margin:40px auto;padding:0 20px;font-family:system-ui,sans-serif;line-height:1.6;color:#333}}img{{max-width:100%;height:auto}}a{{color:#0066cc}}</style></head>\n<body>\n{}\n</body>\n</html>",
29            content
30        )
31    } else {
32        content.to_string()
33    };
34
35    std::fs::write(&path, wrapped.as_bytes())?;
36    tracing::debug!("[course] saved description: {}", path);
37    Ok(())
38}
39
40pub async fn download_attachment(
41    client: &reqwest::Client,
42    url: &str,
43    dir: &str,
44    name: &str,
45    cancel_token: &CancellationToken,
46) -> anyhow::Result<u64> {
47    if url.is_empty() || name.is_empty() {
48        return Ok(0);
49    }
50
51    let sanitized = sanitize_filename::sanitize(name);
52    let filename = if sanitized.is_empty() {
53        let ext = url
54            .rsplit('.')
55            .next()
56            .and_then(|e| e.split('?').next())
57            .filter(|e| e.len() <= 5)
58            .unwrap_or("bin");
59        format!("attachment.{}", ext)
60    } else {
61        sanitized
62    };
63
64    let path = format!("{}/{}", dir, filename);
65
66    if Path::new(&path).exists() {
67        let meta = std::fs::metadata(&path);
68        if meta.map(|m| m.len() > 0).unwrap_or(false) {
69            return Ok(0);
70        }
71    }
72
73    if cancel_token.is_cancelled() {
74        return Err(anyhow!("Download cancelled"));
75    }
76
77    let resp = client
78        .get(url)
79        .send()
80        .await
81        .map_err(|e| anyhow!("Failed to download attachment: {}", e))?;
82
83    if !resp.status().is_success() {
84        return Err(anyhow!(
85            "Attachment download failed: HTTP {}",
86            resp.status()
87        ));
88    }
89
90    let bytes = resp.bytes().await?;
91    let size = bytes.len() as u64;
92
93    if size == 0 {
94        return Ok(0);
95    }
96
97    let part_path = format!("{}.part", path);
98    std::fs::write(&part_path, &bytes)?;
99    std::fs::rename(&part_path, &path)?;
100
101    tracing::debug!("[course] attachment saved: {} ({} bytes)", path, size);
102    Ok(size)
103}
104
105pub async fn mark_course_complete(course_dir: &str) -> anyhow::Result<()> {
106    let marker = format!("{}/.complete", course_dir);
107    std::fs::write(&marker, "done")?;
108    tracing::info!("[course] marked complete: {}", course_dir);
109    Ok(())
110}
111
112pub fn is_course_complete(course_dir: &str) -> bool {
113    Path::new(&format!("{}/.complete", course_dir)).exists()
114}
115
116pub async fn ensure_dir(path: &str) -> anyhow::Result<()> {
117    std::fs::create_dir_all(path)?;
118    Ok(())
119}