metis_core/application/services/document/
deletion.rs1use crate::Result;
2use std::path::Path;
3
4pub struct DeletionService {}
12
13impl Default for DeletionService {
14 fn default() -> Self {
15 Self::new()
16 }
17}
18
19impl DeletionService {
20 pub fn new() -> Self {
21 Self {}
22 }
23
24 pub async fn delete_document_recursive(&self, filepath: &str) -> Result<DeletionResult> {
26 let file_path = Path::new(filepath);
27
28 if !file_path.exists() {
29 return Ok(DeletionResult {
30 deleted_files: vec![],
31 cleaned_directories: vec![],
32 });
33 }
34
35 let mut deleted_files = Vec::new();
36 let mut cleaned_directories = Vec::new();
37
38 if let Some(parent_dir) = file_path.parent() {
41 if parent_dir != Path::new(".") && parent_dir != Path::new("") && parent_dir.is_dir() {
43 if file_path.file_name() == Some(std::ffi::OsStr::new("strategy.md"))
46 || file_path.file_name() == Some(std::ffi::OsStr::new("initiative.md"))
47 {
48 Self::remove_directory_recursive(
49 parent_dir,
50 &mut deleted_files,
51 &mut cleaned_directories,
52 )?;
53 return Ok(DeletionResult {
54 deleted_files,
55 cleaned_directories,
56 });
57 }
58 }
59 }
60
61 if file_path.is_file() {
63 if let Err(e) = std::fs::remove_file(file_path) {
64 eprintln!(
65 "Warning: Could not delete file {}: {}",
66 file_path.display(),
67 e
68 );
69 } else {
70 deleted_files.push(file_path.display().to_string());
71 }
72 }
73
74 Ok(DeletionResult {
75 deleted_files,
76 cleaned_directories,
77 })
78 }
79
80 fn remove_directory_recursive(
82 dir_path: &Path,
83 deleted_files: &mut Vec<String>,
84 cleaned_directories: &mut Vec<String>,
85 ) -> Result<()> {
86 if !dir_path.exists() || !dir_path.is_dir() {
87 return Ok(());
88 }
89
90 let entries = std::fs::read_dir(dir_path).map_err(|e| {
92 crate::MetisError::FileSystem(format!(
93 "Failed to read directory {}: {}",
94 dir_path.display(),
95 e
96 ))
97 })?;
98
99 for entry in entries {
100 let entry = entry.map_err(|e| crate::MetisError::FileSystem(e.to_string()))?;
101 let path = entry.path();
102
103 if path.is_file() {
104 if let Err(e) = std::fs::remove_file(&path) {
106 eprintln!("Warning: Could not delete file {}: {}", path.display(), e);
107 } else {
108 deleted_files.push(path.display().to_string());
109 }
110 } else if path.is_dir() {
111 Self::remove_directory_recursive(&path, deleted_files, cleaned_directories)?;
113 }
114 }
115
116 if let Err(e) = std::fs::remove_dir(dir_path) {
118 eprintln!(
119 "Warning: Could not remove directory {}: {}",
120 dir_path.display(),
121 e
122 );
123 } else {
124 cleaned_directories.push(dir_path.display().to_string());
125 }
126
127 Ok(())
128 }
129}
130
131#[derive(Debug)]
133pub struct DeletionResult {
134 pub deleted_files: Vec<String>,
135 pub cleaned_directories: Vec<String>,
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::application::services::document::{
142 creation::DocumentCreationConfig, DocumentCreationService,
143 };
144 use std::fs;
145 use std::path::PathBuf;
146 use tempfile::tempdir;
147
148 use crate::application::Application;
149 use crate::dal::Database;
150
151 async fn setup_test_workspace() -> (tempfile::TempDir, PathBuf) {
152 let temp_dir = tempdir().unwrap();
153 let workspace_dir = temp_dir.path().to_path_buf();
154
155 let metis_dir = workspace_dir.join(".metis");
157 fs::create_dir_all(&metis_dir).unwrap();
158
159 let db_path = workspace_dir.join("metis.db");
161 let db = Database::new(&db_path.to_string_lossy()).unwrap();
162 let app = Application::new(db);
163
164 let creation_service = DocumentCreationService::new(&metis_dir);
166 let vision_config = DocumentCreationConfig {
167 title: "Test Vision".to_string(),
168 description: Some("Root vision for testing".to_string()),
169 parent_id: None,
170 tags: vec![],
171 phase: None,
172 complexity: None,
173 risk_level: None,
174 };
175 creation_service.create_vision(vision_config).await.unwrap();
176
177 app.sync_directory(&metis_dir).await.unwrap();
179
180 (temp_dir, metis_dir)
181 }
182
183 #[tokio::test]
184 async fn test_delete_single_document_no_children() {
185 let (_temp_dir, workspace_dir) = setup_test_workspace().await;
186 let service = DeletionService::new();
187
188 let doc_path = workspace_dir.join("test.md");
190 fs::write(&doc_path, "# Test Document\nContent here").unwrap();
191
192 let result = service
194 .delete_document_recursive(&doc_path.display().to_string())
195 .await
196 .unwrap();
197
198 assert_eq!(result.deleted_files.len(), 1);
200 assert!(!doc_path.exists());
201 }
202
203 #[tokio::test]
204 async fn test_delete_strategy_with_folder() {
205 let (_temp_dir, workspace_dir) = setup_test_workspace().await;
206
207 let creation_service = DocumentCreationService::new(&workspace_dir);
209 let strategy_config = DocumentCreationConfig {
210 title: "Test Strategy".to_string(),
211 description: Some("Test strategy description".to_string()),
212 parent_id: None,
213 tags: vec![],
214 phase: None,
215 complexity: None,
216 risk_level: None,
217 };
218 let strategy_result = creation_service
219 .create_strategy(strategy_config)
220 .await
221 .unwrap();
222
223 let initiative_config = DocumentCreationConfig {
225 title: "Test Initiative".to_string(),
226 description: Some("Test initiative description".to_string()),
227 parent_id: Some(strategy_result.document_id.clone()),
228 tags: vec![],
229 phase: None,
230 complexity: None,
231 risk_level: None,
232 };
233 let initiative_result = creation_service
234 .create_initiative(initiative_config, &strategy_result.document_id.to_string())
235 .await
236 .unwrap();
237
238 let task_config = DocumentCreationConfig {
240 title: "Test Task".to_string(),
241 description: Some("Test task description".to_string()),
242 parent_id: Some(initiative_result.document_id.clone()),
243 tags: vec![],
244 phase: None,
245 complexity: None,
246 risk_level: None,
247 };
248 let task_result = creation_service
249 .create_task(
250 task_config,
251 &strategy_result.document_id.to_string(),
252 &initiative_result.document_id.to_string(),
253 )
254 .await
255 .unwrap();
256
257 assert!(strategy_result.file_path.exists());
259 assert!(initiative_result.file_path.exists());
260 assert!(task_result.file_path.exists());
261
262 let deletion_service = DeletionService::new();
264 let result = deletion_service
265 .delete_document_recursive(&strategy_result.file_path.to_string_lossy())
266 .await
267 .unwrap();
268
269 let strategy_path = &strategy_result.file_path;
271 let strategy_folder = strategy_path.parent().unwrap();
272 assert!(!strategy_folder.exists());
273 assert!(!initiative_result.file_path.exists());
274 assert!(!task_result.file_path.exists());
275
276 assert!(result.deleted_files.len() >= 3); assert!(!result.cleaned_directories.is_empty()); }
280
281 #[tokio::test]
282 async fn test_delete_initiative_with_folder() {
283 let (_temp_dir, workspace_dir) = setup_test_workspace().await;
284
285 let creation_service = DocumentCreationService::new(&workspace_dir);
287 let strategy_config = DocumentCreationConfig {
288 title: "Parent Strategy".to_string(),
289 description: Some("Parent strategy".to_string()),
290 parent_id: None,
291 tags: vec![],
292 phase: None,
293 complexity: None,
294 risk_level: None,
295 };
296 let strategy_result = creation_service
297 .create_strategy(strategy_config)
298 .await
299 .unwrap();
300
301 let initiative_config = DocumentCreationConfig {
303 title: "Test Initiative".to_string(),
304 description: Some("Test initiative".to_string()),
305 parent_id: Some(strategy_result.document_id.clone()),
306 tags: vec![],
307 phase: None,
308 complexity: None,
309 risk_level: None,
310 };
311 let initiative_result = creation_service
312 .create_initiative(initiative_config, &strategy_result.document_id.to_string())
313 .await
314 .unwrap();
315
316 let task1_config = DocumentCreationConfig {
318 title: "Task One".to_string(),
319 description: Some("First task".to_string()),
320 parent_id: Some(initiative_result.document_id.clone()),
321 tags: vec![],
322 phase: None,
323 complexity: None,
324 risk_level: None,
325 };
326 let task1_result = creation_service
327 .create_task(
328 task1_config,
329 &strategy_result.document_id.to_string(),
330 &initiative_result.document_id.to_string(),
331 )
332 .await
333 .unwrap();
334
335 let task2_config = DocumentCreationConfig {
336 title: "Task Two".to_string(),
337 description: Some("Second task".to_string()),
338 parent_id: Some(initiative_result.document_id.clone()),
339 tags: vec![],
340 phase: None,
341 complexity: None,
342 risk_level: None,
343 };
344 let task2_result = creation_service
345 .create_task(
346 task2_config,
347 &strategy_result.document_id.to_string(),
348 &initiative_result.document_id.to_string(),
349 )
350 .await
351 .unwrap();
352
353 let deletion_service = DeletionService::new();
355 let result = deletion_service
356 .delete_document_recursive(&initiative_result.file_path.to_string_lossy())
357 .await
358 .unwrap();
359
360 let initiative_path = &initiative_result.file_path;
362 let initiative_folder = initiative_path.parent().unwrap();
363 assert!(!initiative_folder.exists());
364 assert!(!task1_result.file_path.exists());
365 assert!(!task2_result.file_path.exists());
366
367 assert!(strategy_result.file_path.exists());
369
370 assert!(result.deleted_files.len() >= 3); assert!(!result.cleaned_directories.is_empty()); }
374
375 #[tokio::test]
376 async fn test_delete_nonexistent_document() {
377 let (_temp_dir, workspace_dir) = setup_test_workspace().await;
378 let service = DeletionService::new();
379
380 let nonexistent_path = workspace_dir.join("nonexistent.md");
381
382 let result = service
384 .delete_document_recursive(&nonexistent_path.display().to_string())
385 .await
386 .unwrap();
387
388 assert_eq!(result.deleted_files.len(), 0);
389 assert_eq!(result.cleaned_directories.len(), 0);
390 }
391
392 #[tokio::test]
393 async fn test_delete_task_file_only() {
394 let (_temp_dir, workspace_dir) = setup_test_workspace().await;
395
396 let creation_service = DocumentCreationService::new(&workspace_dir);
398 let strategy_config = DocumentCreationConfig {
399 title: "Test Strategy".to_string(),
400 description: Some("Test strategy".to_string()),
401 parent_id: None,
402 tags: vec![],
403 phase: None,
404 complexity: None,
405 risk_level: None,
406 };
407 let strategy_result = creation_service
408 .create_strategy(strategy_config)
409 .await
410 .unwrap();
411
412 let initiative_config = DocumentCreationConfig {
413 title: "Test Initiative".to_string(),
414 description: Some("Test initiative".to_string()),
415 parent_id: Some(strategy_result.document_id.clone()),
416 tags: vec![],
417 phase: None,
418 complexity: None,
419 risk_level: None,
420 };
421 let initiative_result = creation_service
422 .create_initiative(initiative_config, &strategy_result.document_id.to_string())
423 .await
424 .unwrap();
425
426 let task_config = DocumentCreationConfig {
427 title: "Test Task".to_string(),
428 description: Some("Test task".to_string()),
429 parent_id: Some(initiative_result.document_id.clone()),
430 tags: vec![],
431 phase: None,
432 complexity: None,
433 risk_level: None,
434 };
435 let task_result = creation_service
436 .create_task(
437 task_config,
438 &strategy_result.document_id.to_string(),
439 &initiative_result.document_id.to_string(),
440 )
441 .await
442 .unwrap();
443
444 let deletion_service = DeletionService::new();
446 let result = deletion_service
447 .delete_document_recursive(&task_result.file_path.to_string_lossy())
448 .await
449 .unwrap();
450
451 assert!(!task_result.file_path.exists());
453
454 assert!(initiative_result.file_path.exists());
456 assert!(strategy_result.file_path.exists());
457
458 assert_eq!(result.deleted_files.len(), 1);
460 assert_eq!(result.cleaned_directories.len(), 0);
461 }
462
463 #[tokio::test]
464 async fn test_delete_document_no_folder() {
465 let (_temp_dir, workspace_dir) = setup_test_workspace().await;
466 let service = DeletionService::new();
467
468 let doc_path = workspace_dir.join("document.md");
470 fs::write(&doc_path, "# Document").unwrap();
471
472 let result = service
474 .delete_document_recursive(&doc_path.display().to_string())
475 .await
476 .unwrap();
477
478 assert!(!doc_path.exists());
480 assert_eq!(result.deleted_files.len(), 1);
481 assert_eq!(result.cleaned_directories.len(), 0);
482 }
483}