1use anyhow::Result;
2use chrono::{DateTime, Utc};
3use std::path::Path;
4
5#[derive(Debug)]
6pub struct ProjectStats {
7 pub source_files: usize,
8 pub total_lines: usize,
9 pub avg_file_size_kb: f64,
10 pub largest_file_kb: f64,
11 pub max_depth: usize,
12 pub cross_references: usize,
13}
14
15pub async fn analyze_project(source_dir: &Path) -> Result<ProjectStats> {
16 let mut state = AnalysisState {
17 source_files: 0,
18 total_lines: 0,
19 total_size_bytes: 0,
20 largest_file_kb: 0.0,
21 max_depth: 0,
22 cross_references: 0,
23 };
24
25 analyze_directory_sync(source_dir, source_dir, 0, &mut state)?;
27
28 let avg_file_size_kb = if state.source_files > 0 {
29 (state.total_size_bytes as f64) / (state.source_files as f64) / 1024.0
30 } else {
31 0.0
32 };
33
34 Ok(ProjectStats {
35 source_files: state.source_files,
36 total_lines: state.total_lines,
37 avg_file_size_kb,
38 largest_file_kb: state.largest_file_kb,
39 max_depth: state.max_depth,
40 cross_references: state.cross_references,
41 })
42}
43
44struct AnalysisState {
46 source_files: usize,
47 total_lines: usize,
48 total_size_bytes: u64,
49 largest_file_kb: f64,
50 max_depth: usize,
51 cross_references: usize,
52}
53
54fn analyze_directory_sync(
55 dir: &Path,
56 _root_dir: &Path,
57 current_depth: usize,
58 state: &mut AnalysisState,
59) -> Result<()> {
60 state.max_depth = state.max_depth.max(current_depth);
61
62 for entry in std::fs::read_dir(dir)? {
63 let entry = entry?;
64 let path = entry.path();
65
66 if path.is_dir() {
67 if let Some(name) = path.file_name() {
69 if name.to_string_lossy().starts_with('.') {
70 continue;
71 }
72 }
73
74 analyze_directory_sync(&path, _root_dir, current_depth + 1, state)?;
75 } else if is_source_file(&path) {
76 state.source_files += 1;
77
78 let metadata = std::fs::metadata(&path)?;
79 let file_size_bytes = metadata.len();
80 let file_size_kb = file_size_bytes as f64 / 1024.0;
81
82 state.total_size_bytes += file_size_bytes;
83 state.largest_file_kb = state.largest_file_kb.max(file_size_kb);
84
85 if let Ok(content) = std::fs::read_to_string(&path) {
87 state.total_lines += content.lines().count();
88 state.cross_references += count_cross_references(&content);
89 }
90 }
91 }
92
93 Ok(())
94}
95
96pub fn is_source_file(path: &Path) -> bool {
97 if let Some(ext) = path.extension() {
98 matches!(ext.to_string_lossy().as_ref(), "rst" | "md" | "txt")
99 } else {
100 false
101 }
102}
103
104pub fn count_cross_references(content: &str) -> usize {
105 let patterns = [
106 r":doc:`",
107 r":ref:`",
108 r":func:`",
109 r":class:`",
110 r":meth:`",
111 r":attr:`",
112 r":mod:`",
113 r":py:",
114 r".. _",
115 r"`~",
116 ];
117
118 let mut count = 0;
119 for pattern in &patterns {
120 count += content.matches(pattern).count();
121 }
122 count
123}
124
125pub fn get_file_mtime(path: &Path) -> Result<DateTime<Utc>> {
126 let metadata = std::fs::metadata(path)?;
127 let mtime = metadata.modified()?;
128 Ok(DateTime::from(mtime))
129}
130
131pub async fn calculate_directory_size(dir: &Path) -> Result<u64> {
132 calculate_directory_size_sync(dir)
134}
135
136fn calculate_directory_size_sync(dir: &Path) -> Result<u64> {
137 let mut total_size = 0;
138
139 for entry in std::fs::read_dir(dir)? {
140 let entry = entry?;
141 let path = entry.path();
142
143 if path.is_dir() {
144 total_size += calculate_directory_size_sync(&path)?;
145 } else {
146 let metadata = std::fs::metadata(&path)?;
147 total_size += metadata.len();
148 }
149 }
150
151 Ok(total_size)
152}
153
154pub async fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
155 copy_dir_recursive_sync(src, dst)
157}
158
159fn copy_dir_recursive_sync(src: &Path, dst: &Path) -> Result<()> {
160 std::fs::create_dir_all(dst)?;
161
162 for entry in std::fs::read_dir(src)? {
163 let entry = entry?;
164 let src_path = entry.path();
165 let dst_path = dst.join(entry.file_name());
166
167 if src_path.is_dir() {
168 copy_dir_recursive_sync(&src_path, &dst_path)?;
169 } else {
170 std::fs::copy(&src_path, &dst_path)?;
171 }
172 }
173
174 Ok(())
175}
176
177#[allow(dead_code)]
178pub fn format_duration(duration: std::time::Duration) -> String {
179 let secs = duration.as_secs();
180 let millis = duration.subsec_millis();
181
182 if secs > 0 {
183 format!("{}.{:03}s", secs, millis)
184 } else {
185 format!("{}ms", millis)
186 }
187}
188
189#[allow(dead_code)]
190pub fn format_bytes(bytes: u64) -> String {
191 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
192
193 if bytes == 0 {
194 return "0 B".to_string();
195 }
196
197 let mut size = bytes as f64;
198 let mut unit_index = 0;
199
200 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
201 size /= 1024.0;
202 unit_index += 1;
203 }
204
205 format!("{:.1} {}", size, UNITS[unit_index])
206}
207
208#[allow(dead_code)]
210pub fn format_date(fmt: &str, _language: &Option<String>) -> String {
211 let now = chrono::Utc::now();
212
213 match fmt {
214 "%b %d, %Y" => now.format("%b %d, %Y").to_string(),
215 "%B %d, %Y" => now.format("%B %d, %Y").to_string(),
216 "%Y-%m-%d" => now.format("%Y-%m-%d").to_string(),
217 "%Y-%m-%d %H:%M:%S" => now.format("%Y-%m-%d %H:%M:%S").to_string(),
218 _ => {
219 match chrono::DateTime::parse_from_str(&now.to_rfc3339(), "%+") {
221 Ok(dt) => dt.format(fmt).to_string(),
222 Err(_) => now.format("%Y-%m-%d").to_string(),
223 }
224 }
225 }
226}
227
228#[allow(dead_code)]
230pub async fn ensure_dir(path: &Path) -> Result<()> {
231 use tokio::fs;
232
233 if !path.exists() {
234 fs::create_dir_all(path).await?;
235 }
236 Ok(())
237}
238
239#[allow(dead_code)]
241pub fn relative_uri(from: &str, to: &str, suffix: &str) -> String {
242 use std::path::Path;
243
244 let from_path = Path::new(from);
245 let to_path = Path::new(to);
246
247 if let Some(rel_path) =
249 pathdiff::diff_paths(to_path, from_path.parent().unwrap_or(Path::new("")))
250 {
251 let mut result = rel_path.to_string_lossy().to_string();
252 if !suffix.is_empty() && !result.ends_with(suffix) {
253 result.push_str(suffix);
254 }
255 result.replace('\\', "/") } else {
257 format!("{}{}", to, suffix)
258 }
259}
260
261#[allow(dead_code)]
263pub async fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
264 use tokio::fs;
265
266 ensure_dir(dst).await?;
267
268 let mut entries = fs::read_dir(src).await?;
269
270 while let Some(entry) = entries.next_entry().await? {
271 let entry_path = entry.path();
272 let file_name = entry.file_name();
273 let dest_path = dst.join(file_name);
274
275 if entry_path.is_dir() {
276 Box::pin(copy_dir_all(&entry_path, &dest_path)).await?;
277 } else {
278 if let Some(parent) = dest_path.parent() {
279 ensure_dir(parent).await?;
280 }
281 fs::copy(&entry_path, &dest_path).await?;
282 }
283 }
284
285 Ok(())
286}