mangofetch_core/core/
course_utils.rs1use 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}