1use chrono::{DateTime, Utc};
7use ragfs_core::VectorStore;
8use serde::{Deserialize, Serialize};
9use std::fs;
10use std::path::PathBuf;
11use std::sync::Arc;
12use tokio::sync::{RwLock, mpsc};
13use tracing::{debug, info, warn};
14use uuid::Uuid;
15
16use crate::safety::{HistoryOperation, SafetyManager, UndoData};
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(tag = "op", rename_all = "snake_case")]
21pub enum Operation {
22 Create { path: PathBuf, content: String },
24 Delete { path: PathBuf },
26 Move { src: PathBuf, dst: PathBuf },
28 Copy { src: PathBuf, dst: PathBuf },
30 Write {
32 path: PathBuf,
33 content: String,
34 #[serde(default)]
35 append: bool,
36 },
37 Mkdir { path: PathBuf },
39 Symlink { target: PathBuf, link: PathBuf },
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct BatchRequest {
46 pub operations: Vec<Operation>,
48 #[serde(default)]
50 pub atomic: bool,
51 #[serde(default)]
53 pub dry_run: bool,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct OperationResult {
59 pub id: Uuid,
61 pub success: bool,
63 pub operation: String,
65 pub path: PathBuf,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub error: Option<String>,
70 pub timestamp: DateTime<Utc>,
72 pub indexed: bool,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub undo_id: Option<Uuid>,
77}
78
79impl OperationResult {
80 pub fn success(operation: &str, path: PathBuf, indexed: bool) -> Self {
82 Self {
83 id: Uuid::new_v4(),
84 success: true,
85 operation: operation.to_string(),
86 path,
87 error: None,
88 timestamp: Utc::now(),
89 indexed,
90 undo_id: Some(Uuid::new_v4()),
91 }
92 }
93
94 pub fn failure(operation: &str, path: PathBuf, error: String) -> Self {
96 Self {
97 id: Uuid::new_v4(),
98 success: false,
99 operation: operation.to_string(),
100 path,
101 error: Some(error),
102 timestamp: Utc::now(),
103 indexed: false,
104 undo_id: None,
105 }
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct BatchResult {
112 pub id: Uuid,
114 pub success: bool,
116 pub total: usize,
118 pub succeeded: usize,
120 pub failed: usize,
122 pub results: Vec<OperationResult>,
124 pub timestamp: DateTime<Utc>,
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub rollback_performed: Option<bool>,
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub rollback_details: Option<RollbackDetails>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct RollbackDetails {
137 pub rolled_back: usize,
139 pub rollback_failures: usize,
141 pub errors: Vec<RollbackError>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct RollbackError {
148 pub operation_index: usize,
150 pub error: String,
152}
153
154#[derive(Debug, Clone)]
157enum RollbackData {
158 Create { created_path: PathBuf },
160 Delete {
162 original_path: PathBuf,
163 trash_id: Option<Uuid>,
164 content_backup: Option<Vec<u8>>,
165 },
166 Move { src: PathBuf, dst: PathBuf },
168 Copy { copied_path: PathBuf },
170 Write {
172 path: PathBuf,
173 previous_content: Option<Vec<u8>>,
174 file_existed: bool,
175 },
176 Mkdir { created_path: PathBuf },
178 Symlink { link_path: PathBuf },
180}
181
182#[derive(Debug, Clone)]
184struct JournalEntry {
185 operation_index: usize,
187 rollback_data: RollbackData,
189}
190
191pub struct OpsManager {
193 source: PathBuf,
195 store: Option<Arc<dyn VectorStore>>,
197 reindex_sender: Option<mpsc::Sender<PathBuf>>,
199 safety_manager: Option<Arc<SafetyManager>>,
201 last_result: Arc<RwLock<Option<OperationResult>>>,
203 last_batch_result: Arc<RwLock<Option<BatchResult>>>,
205}
206
207impl OpsManager {
208 pub fn new(
210 source: PathBuf,
211 store: Option<Arc<dyn VectorStore>>,
212 reindex_sender: Option<mpsc::Sender<PathBuf>>,
213 ) -> Self {
214 Self {
215 source,
216 store,
217 reindex_sender,
218 safety_manager: None,
219 last_result: Arc::new(RwLock::new(None)),
220 last_batch_result: Arc::new(RwLock::new(None)),
221 }
222 }
223
224 pub fn with_safety(
226 source: PathBuf,
227 store: Option<Arc<dyn VectorStore>>,
228 reindex_sender: Option<mpsc::Sender<PathBuf>>,
229 safety_manager: Arc<SafetyManager>,
230 ) -> Self {
231 Self {
232 source,
233 store,
234 reindex_sender,
235 safety_manager: Some(safety_manager),
236 last_result: Arc::new(RwLock::new(None)),
237 last_batch_result: Arc::new(RwLock::new(None)),
238 }
239 }
240
241 pub fn set_safety_manager(&mut self, safety_manager: Arc<SafetyManager>) {
243 self.safety_manager = Some(safety_manager);
244 }
245
246 fn log_to_history(&self, operation: HistoryOperation, undo_data: Option<UndoData>) {
248 if let Some(ref safety) = self.safety_manager {
249 safety.log_success(operation, undo_data);
250 }
251 }
252
253 fn log_failure_to_history(&self, operation: HistoryOperation, error: &str) {
255 if let Some(ref safety) = self.safety_manager {
256 safety.log_failure(operation, error.to_string());
257 }
258 }
259
260 fn resolve_path(&self, path: &PathBuf) -> PathBuf {
262 if path.is_absolute() {
263 path.clone()
264 } else {
265 self.source.join(path)
266 }
267 }
268
269 async fn trigger_reindex(&self, path: &PathBuf) -> bool {
271 if let Some(ref sender) = self.reindex_sender {
272 match sender.send(path.clone()).await {
273 Ok(()) => {
274 debug!("Reindex triggered for {:?}", path);
275 true
276 }
277 Err(e) => {
278 warn!("Failed to trigger reindex for {:?}: {}", path, e);
279 false
280 }
281 }
282 } else {
283 false
284 }
285 }
286
287 async fn delete_from_store(&self, path: &PathBuf) {
289 if let Some(ref store) = self.store
290 && let Err(e) = store.delete_by_file_path(path).await
291 {
292 warn!("Failed to delete {:?} from store: {e}", path);
293 }
294 }
295
296 async fn update_store_path(&self, from: &PathBuf, to: &PathBuf) {
298 if let Some(ref store) = self.store
299 && let Err(e) = store.update_file_path(from, to).await
300 {
301 warn!("Failed to update path in store {:?} -> {:?}: {e}", from, to);
302 }
303 }
304
305 async fn execute_with_rollback(
308 &self,
309 op: &Operation,
310 ) -> (OperationResult, Option<RollbackData>) {
311 match op {
312 Operation::Create { path, content } => {
313 let resolved = self.resolve_path(path);
314 let result = self.create(path, content).await;
315 let rollback = if result.success {
316 Some(RollbackData::Create {
317 created_path: resolved,
318 })
319 } else {
320 None
321 };
322 (result, rollback)
323 }
324 Operation::Delete { path } => {
325 let resolved = self.resolve_path(path);
326 let content_backup = if resolved.exists() && resolved.is_file() {
328 fs::read(&resolved).ok()
329 } else {
330 None
331 };
332
333 let result = self.delete(path).await;
334 let rollback = if result.success {
335 Some(RollbackData::Delete {
336 original_path: resolved,
337 trash_id: result.undo_id, content_backup: if result.undo_id.is_none() {
339 content_backup
340 } else {
341 None
342 },
343 })
344 } else {
345 None
346 };
347 (result, rollback)
348 }
349 Operation::Move { src, dst } => {
350 let resolved_src = self.resolve_path(src);
351 let resolved_dst = self.resolve_path(dst);
352 let result = self.move_file(src, dst).await;
353 let rollback = if result.success {
354 Some(RollbackData::Move {
355 src: resolved_dst,
356 dst: resolved_src,
357 })
358 } else {
359 None
360 };
361 (result, rollback)
362 }
363 Operation::Copy { src, dst } => {
364 let resolved_dst = self.resolve_path(dst);
365 let result = self.copy(src, dst).await;
366 let rollback = if result.success {
367 Some(RollbackData::Copy {
368 copied_path: resolved_dst,
369 })
370 } else {
371 None
372 };
373 (result, rollback)
374 }
375 Operation::Write {
376 path,
377 content,
378 append,
379 } => {
380 let resolved = self.resolve_path(path);
381 let file_existed = resolved.exists();
382 let previous_content = if file_existed {
383 fs::read(&resolved).ok()
384 } else {
385 None
386 };
387
388 let result = self.write(path, content, *append).await;
389 let rollback = if result.success {
390 Some(RollbackData::Write {
391 path: resolved,
392 previous_content,
393 file_existed,
394 })
395 } else {
396 None
397 };
398 (result, rollback)
399 }
400 Operation::Mkdir { path } => {
401 let resolved = self.resolve_path(path);
402 let result = self.mkdir(path).await;
403 let rollback = if result.success {
404 Some(RollbackData::Mkdir {
405 created_path: resolved,
406 })
407 } else {
408 None
409 };
410 (result, rollback)
411 }
412 Operation::Symlink { target, link } => {
413 let resolved_link = self.resolve_path(link);
414 let result = self.symlink(target, link).await;
415 let rollback = if result.success {
416 Some(RollbackData::Symlink {
417 link_path: resolved_link,
418 })
419 } else {
420 None
421 };
422 (result, rollback)
423 }
424 }
425 }
426
427 async fn rollback_operation(&self, rollback_data: &RollbackData) -> Result<(), String> {
429 match rollback_data {
430 RollbackData::Create { created_path } => {
431 if created_path.exists() {
433 fs::remove_file(created_path)
434 .map_err(|e| format!("Failed to rollback create: {e}"))?;
435 self.delete_from_store(created_path).await;
437 }
438 Ok(())
439 }
440 RollbackData::Delete {
441 original_path,
442 trash_id,
443 content_backup,
444 } => {
445 if let Some(id) = trash_id
447 && let Some(ref safety) = self.safety_manager
448 {
449 return safety
450 .restore(*id)
451 .await
452 .map(|_| ())
453 .map_err(|e| format!("Failed to restore from trash: {e}"));
454 }
455 if let Some(content) = content_backup {
457 if let Some(parent) = original_path.parent() {
458 fs::create_dir_all(parent)
459 .map_err(|e| format!("Failed to create parent dir: {e}"))?;
460 }
461 fs::write(original_path, content)
462 .map_err(|e| format!("Failed to restore content: {e}"))?;
463 self.trigger_reindex(original_path).await;
464 Ok(())
465 } else {
466 Err("Cannot rollback delete: no trash entry or content backup".into())
467 }
468 }
469 RollbackData::Move { src, dst } => {
470 if src.exists() {
472 if let Some(parent) = dst.parent() {
473 fs::create_dir_all(parent)
474 .map_err(|e| format!("Failed to create parent dir: {e}"))?;
475 }
476 fs::rename(src, dst).map_err(|e| format!("Failed to rollback move: {e}"))?;
477 self.update_store_path(src, dst).await;
478 }
479 Ok(())
480 }
481 RollbackData::Copy { copied_path } => {
482 if copied_path.exists() {
484 fs::remove_file(copied_path)
485 .map_err(|e| format!("Failed to rollback copy: {e}"))?;
486 self.delete_from_store(copied_path).await;
487 }
488 Ok(())
489 }
490 RollbackData::Write {
491 path,
492 previous_content,
493 file_existed,
494 } => {
495 if *file_existed {
496 if let Some(content) = previous_content {
497 fs::write(path, content)
498 .map_err(|e| format!("Failed to rollback write: {e}"))?;
499 } else {
500 return Err("Cannot rollback write: no previous content saved".into());
501 }
502 } else {
503 if path.exists() {
505 fs::remove_file(path)
506 .map_err(|e| format!("Failed to rollback write (delete): {e}"))?;
507 self.delete_from_store(path).await;
508 }
509 }
510 self.trigger_reindex(path).await;
511 Ok(())
512 }
513 RollbackData::Mkdir { created_path } => {
514 if created_path.exists() && created_path.is_dir() {
516 fs::remove_dir(created_path)
517 .map_err(|e| format!("Failed to rollback mkdir: {e}"))?;
518 }
519 Ok(())
520 }
521 RollbackData::Symlink { link_path } => {
522 if link_path.exists() || link_path.symlink_metadata().is_ok() {
524 fs::remove_file(link_path)
525 .map_err(|e| format!("Failed to rollback symlink: {e}"))?;
526 }
527 Ok(())
528 }
529 }
530 }
531
532 async fn perform_rollback(&self, journal: &[JournalEntry]) -> RollbackDetails {
534 let mut rolled_back = 0;
535 let mut rollback_failures = 0;
536 let mut errors = Vec::new();
537
538 for entry in journal.iter().rev() {
540 match self.rollback_operation(&entry.rollback_data).await {
541 Ok(()) => {
542 rolled_back += 1;
543 info!("Rolled back operation index {}", entry.operation_index);
544 }
545 Err(e) => {
546 rollback_failures += 1;
547 warn!(
548 "Failed to rollback operation index {}: {}",
549 entry.operation_index, e
550 );
551 errors.push(RollbackError {
552 operation_index: entry.operation_index,
553 error: e,
554 });
555 }
556 }
557 }
558
559 RollbackDetails {
560 rolled_back,
561 rollback_failures,
562 errors,
563 }
564 }
565
566 pub async fn create(&self, path: &PathBuf, content: &str) -> OperationResult {
568 let resolved = self.resolve_path(path);
569 debug!("ops::create {:?}", resolved);
570
571 if resolved.exists() {
573 return OperationResult::failure("create", path.clone(), "File already exists".into());
574 }
575
576 if let Some(parent) = resolved.parent()
578 && !parent.exists()
579 && let Err(e) = fs::create_dir_all(parent)
580 {
581 return OperationResult::failure(
582 "create",
583 path.clone(),
584 format!("Failed to create parent directory: {e}"),
585 );
586 }
587
588 match fs::write(&resolved, content) {
590 Ok(()) => {
591 info!("Created file: {:?}", resolved);
592 let indexed = self.trigger_reindex(&resolved).await;
593 let result = OperationResult::success("create", path.clone(), indexed);
594
595 self.log_to_history(
597 HistoryOperation::Create {
598 path: resolved.clone(),
599 },
600 Some(UndoData::Create { path: resolved }),
601 );
602
603 *self.last_result.write().await = Some(result.clone());
604 result
605 }
606 Err(e) => {
607 let error_msg = format!("Failed to create file: {e}");
608 self.log_failure_to_history(
609 HistoryOperation::Create { path: resolved },
610 &error_msg,
611 );
612
613 let result = OperationResult::failure("create", path.clone(), error_msg);
614 *self.last_result.write().await = Some(result.clone());
615 result
616 }
617 }
618 }
619
620 pub async fn delete(&self, path: &PathBuf) -> OperationResult {
623 let resolved = self.resolve_path(path);
624 debug!("ops::delete {:?}", resolved);
625
626 if !resolved.exists() {
627 return OperationResult::failure("delete", path.clone(), "File not found".into());
628 }
629
630 if resolved.is_dir() {
631 return OperationResult::failure(
632 "delete",
633 path.clone(),
634 "Cannot delete directory with .delete, use rmdir".into(),
635 );
636 }
637
638 self.delete_from_store(&resolved).await;
640
641 if let Some(ref safety) = self.safety_manager {
643 match safety.soft_delete(&resolved).await {
644 Ok(entry) => {
645 info!("Soft deleted file: {:?} -> trash/{}", resolved, entry.id);
646
647 self.log_to_history(
649 HistoryOperation::Delete {
650 path: resolved,
651 trash_id: Some(entry.id),
652 },
653 Some(UndoData::Delete { trash_id: entry.id }),
654 );
655
656 let mut result = OperationResult::success("delete", path.clone(), false);
657 result.undo_id = Some(entry.id);
658 *self.last_result.write().await = Some(result.clone());
659 return result;
660 }
661 Err(e) => {
662 warn!("Soft delete failed, falling back to hard delete: {e}");
663 }
664 }
665 }
666
667 match fs::remove_file(&resolved) {
669 Ok(()) => {
670 info!("Hard deleted file: {:?}", resolved);
671
672 self.log_to_history(
673 HistoryOperation::Delete {
674 path: resolved,
675 trash_id: None,
676 },
677 None, );
679
680 let result = OperationResult::success("delete", path.clone(), false);
681 *self.last_result.write().await = Some(result.clone());
682 result
683 }
684 Err(e) => {
685 let error_msg = format!("Failed to delete file: {e}");
686 self.log_failure_to_history(
687 HistoryOperation::Delete {
688 path: resolved,
689 trash_id: None,
690 },
691 &error_msg,
692 );
693
694 let result = OperationResult::failure("delete", path.clone(), error_msg);
695 *self.last_result.write().await = Some(result.clone());
696 result
697 }
698 }
699 }
700
701 pub async fn move_file(&self, src: &PathBuf, dst: &PathBuf) -> OperationResult {
703 let resolved_src = self.resolve_path(src);
704 let resolved_dst = self.resolve_path(dst);
705 debug!("ops::move {:?} -> {:?}", resolved_src, resolved_dst);
706
707 if !resolved_src.exists() {
708 return OperationResult::failure("move", src.clone(), "Source file not found".into());
709 }
710
711 if resolved_dst.exists() {
712 return OperationResult::failure(
713 "move",
714 src.clone(),
715 "Destination already exists".into(),
716 );
717 }
718
719 if let Some(parent) = resolved_dst.parent()
721 && !parent.exists()
722 && let Err(e) = fs::create_dir_all(parent)
723 {
724 return OperationResult::failure(
725 "move",
726 src.clone(),
727 format!("Failed to create parent directory: {e}"),
728 );
729 }
730
731 match fs::rename(&resolved_src, &resolved_dst) {
733 Ok(()) => {
734 info!("Moved: {:?} -> {:?}", resolved_src, resolved_dst);
735 self.update_store_path(&resolved_src, &resolved_dst).await;
736
737 self.log_to_history(
739 HistoryOperation::Move {
740 src: resolved_src.clone(),
741 dst: resolved_dst.clone(),
742 },
743 Some(UndoData::Move {
744 src: resolved_dst,
745 dst: resolved_src,
746 }),
747 );
748
749 let result = OperationResult::success("move", dst.clone(), true);
750 *self.last_result.write().await = Some(result.clone());
751 result
752 }
753 Err(e) => {
754 let error_msg = format!("Failed to move file: {e}");
755 self.log_failure_to_history(
756 HistoryOperation::Move {
757 src: resolved_src,
758 dst: resolved_dst,
759 },
760 &error_msg,
761 );
762
763 let result = OperationResult::failure("move", src.clone(), error_msg);
764 *self.last_result.write().await = Some(result.clone());
765 result
766 }
767 }
768 }
769
770 pub async fn copy(&self, src: &PathBuf, dst: &PathBuf) -> OperationResult {
772 let resolved_src = self.resolve_path(src);
773 let resolved_dst = self.resolve_path(dst);
774 debug!("ops::copy {:?} -> {:?}", resolved_src, resolved_dst);
775
776 if !resolved_src.exists() {
777 return OperationResult::failure("copy", src.clone(), "Source file not found".into());
778 }
779
780 if resolved_dst.exists() {
781 return OperationResult::failure(
782 "copy",
783 src.clone(),
784 "Destination already exists".into(),
785 );
786 }
787
788 if let Some(parent) = resolved_dst.parent()
790 && !parent.exists()
791 && let Err(e) = fs::create_dir_all(parent)
792 {
793 return OperationResult::failure(
794 "copy",
795 src.clone(),
796 format!("Failed to create parent directory: {e}"),
797 );
798 }
799
800 match fs::copy(&resolved_src, &resolved_dst) {
802 Ok(_) => {
803 info!("Copied: {:?} -> {:?}", resolved_src, resolved_dst);
804 let indexed = self.trigger_reindex(&resolved_dst).await;
805
806 self.log_to_history(
808 HistoryOperation::Copy {
809 src: resolved_src,
810 dst: resolved_dst.clone(),
811 },
812 Some(UndoData::Copy { path: resolved_dst }),
813 );
814
815 let result = OperationResult::success("copy", dst.clone(), indexed);
816 *self.last_result.write().await = Some(result.clone());
817 result
818 }
819 Err(e) => {
820 let error_msg = format!("Failed to copy file: {e}");
821 self.log_failure_to_history(
822 HistoryOperation::Copy {
823 src: resolved_src,
824 dst: resolved_dst,
825 },
826 &error_msg,
827 );
828
829 let result = OperationResult::failure("copy", src.clone(), error_msg);
830 *self.last_result.write().await = Some(result.clone());
831 result
832 }
833 }
834 }
835
836 pub async fn write(&self, path: &PathBuf, content: &str, append: bool) -> OperationResult {
838 let resolved = self.resolve_path(path);
839 debug!("ops::write {:?} (append={})", resolved, append);
840
841 let write_result = if append {
842 use std::io::Write;
843 fs::OpenOptions::new()
844 .create(true)
845 .append(true)
846 .open(&resolved)
847 .and_then(|mut f| f.write_all(content.as_bytes()))
848 } else {
849 fs::write(&resolved, content)
850 };
851
852 match write_result {
853 Ok(()) => {
854 info!("Wrote to file: {:?}", resolved);
855 let indexed = self.trigger_reindex(&resolved).await;
856
857 self.log_to_history(
859 HistoryOperation::Write {
860 path: resolved,
861 append,
862 },
863 None, );
865
866 let result = OperationResult::success("write", path.clone(), indexed);
867 *self.last_result.write().await = Some(result.clone());
868 result
869 }
870 Err(e) => {
871 let error_msg = format!("Failed to write file: {e}");
872 self.log_failure_to_history(
873 HistoryOperation::Write {
874 path: resolved,
875 append,
876 },
877 &error_msg,
878 );
879
880 let result = OperationResult::failure("write", path.clone(), error_msg);
881 *self.last_result.write().await = Some(result.clone());
882 result
883 }
884 }
885 }
886
887 pub async fn mkdir(&self, path: &PathBuf) -> OperationResult {
889 let resolved = self.resolve_path(path);
890 debug!("ops::mkdir {:?}", resolved);
891
892 if resolved.exists() {
893 return OperationResult::failure("mkdir", path.clone(), "Path already exists".into());
894 }
895
896 match fs::create_dir_all(&resolved) {
897 Ok(()) => {
898 info!("Created directory: {:?}", resolved);
899
900 let mut result = OperationResult::success("mkdir", path.clone(), false);
902 result.undo_id = Some(Uuid::new_v4());
904
905 *self.last_result.write().await = Some(result.clone());
906 result
907 }
908 Err(e) => {
909 let error_msg = format!("Failed to create directory: {e}");
910 let result = OperationResult::failure("mkdir", path.clone(), error_msg);
911 *self.last_result.write().await = Some(result.clone());
912 result
913 }
914 }
915 }
916
917 #[cfg(unix)]
919 pub async fn symlink(&self, target: &PathBuf, link: &PathBuf) -> OperationResult {
920 let resolved_target = self.resolve_path(target);
921 let resolved_link = self.resolve_path(link);
922 debug!("ops::symlink {:?} -> {:?}", resolved_link, resolved_target);
923
924 if resolved_link.exists() {
925 return OperationResult::failure(
926 "symlink",
927 link.clone(),
928 "Link path already exists".into(),
929 );
930 }
931
932 if let Some(parent) = resolved_link.parent()
934 && !parent.exists()
935 && let Err(e) = fs::create_dir_all(parent)
936 {
937 return OperationResult::failure(
938 "symlink",
939 link.clone(),
940 format!("Failed to create parent directory: {e}"),
941 );
942 }
943
944 match std::os::unix::fs::symlink(&resolved_target, &resolved_link) {
945 Ok(()) => {
946 info!(
947 "Created symlink: {:?} -> {:?}",
948 resolved_link, resolved_target
949 );
950
951 let mut result = OperationResult::success("symlink", link.clone(), false);
952 result.undo_id = Some(Uuid::new_v4());
953
954 *self.last_result.write().await = Some(result.clone());
955 result
956 }
957 Err(e) => {
958 let error_msg = format!("Failed to create symlink: {e}");
959 let result = OperationResult::failure("symlink", link.clone(), error_msg);
960 *self.last_result.write().await = Some(result.clone());
961 result
962 }
963 }
964 }
965
966 #[cfg(not(unix))]
968 pub async fn symlink(&self, _target: &PathBuf, link: &PathBuf) -> OperationResult {
969 OperationResult::failure(
970 "symlink",
971 link.clone(),
972 "Symlinks are not supported on this platform".into(),
973 )
974 }
975
976 pub async fn execute_operation(&self, op: &Operation) -> OperationResult {
978 match op {
979 Operation::Create { path, content } => self.create(path, content).await,
980 Operation::Delete { path } => self.delete(path).await,
981 Operation::Move { src, dst } => self.move_file(src, dst).await,
982 Operation::Copy { src, dst } => self.copy(src, dst).await,
983 Operation::Write {
984 path,
985 content,
986 append,
987 } => self.write(path, content, *append).await,
988 Operation::Mkdir { path } => self.mkdir(path).await,
989 Operation::Symlink { target, link } => self.symlink(target, link).await,
990 }
991 }
992
993 pub async fn batch(&self, request: BatchRequest) -> BatchResult {
995 let batch_id = Uuid::new_v4();
996 let total = request.operations.len();
997 let mut results = Vec::with_capacity(total);
998 let mut succeeded = 0;
999 let mut failed = 0;
1000 let mut journal: Vec<JournalEntry> = Vec::new();
1001
1002 debug!(
1003 "ops::batch {} operations (atomic={}, dry_run={})",
1004 total, request.atomic, request.dry_run
1005 );
1006
1007 if request.dry_run {
1008 for op in &request.operations {
1010 let result = self.validate_operation(op);
1011 if result.success {
1012 succeeded += 1;
1013 } else {
1014 failed += 1;
1015 }
1016 results.push(result);
1017 }
1018
1019 let batch_result = BatchResult {
1020 id: batch_id,
1021 success: failed == 0,
1022 total,
1023 succeeded,
1024 failed,
1025 results,
1026 timestamp: Utc::now(),
1027 rollback_performed: None,
1028 rollback_details: None,
1029 };
1030
1031 *self.last_batch_result.write().await = Some(batch_result.clone());
1032 return batch_result;
1033 }
1034
1035 for (index, op) in request.operations.iter().enumerate() {
1037 let (result, rollback_data) = self.execute_with_rollback(op).await;
1038
1039 if result.success {
1040 succeeded += 1;
1041 if request.atomic
1043 && let Some(rd) = rollback_data
1044 {
1045 journal.push(JournalEntry {
1046 operation_index: index,
1047 rollback_data: rd,
1048 });
1049 }
1050 results.push(result);
1051 } else {
1052 failed += 1;
1053 results.push(result);
1054
1055 if request.atomic && !journal.is_empty() {
1056 info!(
1058 "Batch failed at operation {}, rolling back {} operations",
1059 index,
1060 journal.len()
1061 );
1062 let rollback_details = self.perform_rollback(&journal).await;
1063 let rollback_success = rollback_details.rollback_failures == 0;
1064
1065 let batch_result = BatchResult {
1066 id: batch_id,
1067 success: false,
1068 total,
1069 succeeded,
1070 failed,
1071 results,
1072 timestamp: Utc::now(),
1073 rollback_performed: Some(rollback_success),
1074 rollback_details: Some(rollback_details),
1075 };
1076
1077 *self.last_batch_result.write().await = Some(batch_result.clone());
1078 return batch_result;
1079 } else if request.atomic {
1080 let batch_result = BatchResult {
1082 id: batch_id,
1083 success: false,
1084 total,
1085 succeeded,
1086 failed,
1087 results,
1088 timestamp: Utc::now(),
1089 rollback_performed: None,
1090 rollback_details: None,
1091 };
1092
1093 *self.last_batch_result.write().await = Some(batch_result.clone());
1094 return batch_result;
1095 }
1096 }
1098 }
1099
1100 let batch_result = BatchResult {
1101 id: batch_id,
1102 success: failed == 0,
1103 total,
1104 succeeded,
1105 failed,
1106 results,
1107 timestamp: Utc::now(),
1108 rollback_performed: None,
1109 rollback_details: None,
1110 };
1111
1112 *self.last_batch_result.write().await = Some(batch_result.clone());
1113 batch_result
1114 }
1115
1116 fn validate_operation(&self, op: &Operation) -> OperationResult {
1118 match op {
1119 Operation::Create { path, .. } => {
1120 let resolved = self.resolve_path(path);
1121 if resolved.exists() {
1122 OperationResult::failure("create", path.clone(), "File already exists".into())
1123 } else {
1124 OperationResult::success("create", path.clone(), false)
1125 }
1126 }
1127 Operation::Delete { path } => {
1128 let resolved = self.resolve_path(path);
1129 if !resolved.exists() {
1130 OperationResult::failure("delete", path.clone(), "File not found".into())
1131 } else if resolved.is_dir() {
1132 OperationResult::failure(
1133 "delete",
1134 path.clone(),
1135 "Cannot delete directory".into(),
1136 )
1137 } else {
1138 OperationResult::success("delete", path.clone(), false)
1139 }
1140 }
1141 Operation::Move { src, dst } => {
1142 let resolved_src = self.resolve_path(src);
1143 let resolved_dst = self.resolve_path(dst);
1144 if !resolved_src.exists() {
1145 OperationResult::failure("move", src.clone(), "Source not found".into())
1146 } else if resolved_dst.exists() {
1147 OperationResult::failure(
1148 "move",
1149 src.clone(),
1150 "Destination already exists".into(),
1151 )
1152 } else {
1153 OperationResult::success("move", dst.clone(), false)
1154 }
1155 }
1156 Operation::Copy { src, dst } => {
1157 let resolved_src = self.resolve_path(src);
1158 let resolved_dst = self.resolve_path(dst);
1159 if !resolved_src.exists() {
1160 OperationResult::failure("copy", src.clone(), "Source not found".into())
1161 } else if resolved_dst.exists() {
1162 OperationResult::failure(
1163 "copy",
1164 src.clone(),
1165 "Destination already exists".into(),
1166 )
1167 } else {
1168 OperationResult::success("copy", dst.clone(), false)
1169 }
1170 }
1171 Operation::Write { path, .. } => {
1172 OperationResult::success("write", path.clone(), false)
1174 }
1175 Operation::Mkdir { path } => {
1176 let resolved = self.resolve_path(path);
1177 if resolved.exists() {
1178 OperationResult::failure("mkdir", path.clone(), "Path already exists".into())
1179 } else {
1180 OperationResult::success("mkdir", path.clone(), false)
1181 }
1182 }
1183 Operation::Symlink { target: _, link } => {
1184 let resolved_link = self.resolve_path(link);
1185 if resolved_link.exists() {
1186 OperationResult::failure(
1187 "symlink",
1188 link.clone(),
1189 "Link path already exists".into(),
1190 )
1191 } else {
1192 OperationResult::success("symlink", link.clone(), false)
1193 }
1194 }
1195 }
1196 }
1197
1198 pub async fn get_last_result(&self) -> Vec<u8> {
1200 let result = self.last_result.read().await;
1201 if let Some(r) = &*result {
1202 serde_json::to_string_pretty(r)
1203 .unwrap_or_else(|_| "{}".to_string())
1204 .into_bytes()
1205 } else {
1206 let empty = serde_json::json!({
1207 "message": "No operations performed yet"
1208 });
1209 serde_json::to_string_pretty(&empty)
1210 .unwrap_or_default()
1211 .into_bytes()
1212 }
1213 }
1214
1215 #[allow(dead_code)]
1217 pub async fn get_last_batch_result(&self) -> Vec<u8> {
1218 let result = self.last_batch_result.read().await;
1219 if let Some(r) = &*result {
1220 serde_json::to_string_pretty(r)
1221 .unwrap_or_else(|_| "{}".to_string())
1222 .into_bytes()
1223 } else {
1224 let empty = serde_json::json!({
1225 "message": "No batch operations performed yet"
1226 });
1227 serde_json::to_string_pretty(&empty)
1228 .unwrap_or_default()
1229 .into_bytes()
1230 }
1231 }
1232
1233 pub async fn parse_and_create(&self, input: &str) -> OperationResult {
1236 let parts: Vec<&str> = input.splitn(2, '\n').collect();
1237 if parts.len() < 2 {
1238 return OperationResult::failure(
1239 "create",
1240 PathBuf::new(),
1241 "Invalid format. Expected: path\\ncontent".into(),
1242 );
1243 }
1244 let path = PathBuf::from(parts[0].trim());
1245 let content = parts[1];
1246 self.create(&path, content).await
1247 }
1248
1249 pub async fn parse_and_delete(&self, input: &str) -> OperationResult {
1252 let path = PathBuf::from(input.trim());
1253 if path.as_os_str().is_empty() {
1254 return OperationResult::failure(
1255 "delete",
1256 PathBuf::new(),
1257 "Invalid format. Expected: path".into(),
1258 );
1259 }
1260 self.delete(&path).await
1261 }
1262
1263 pub async fn parse_and_move(&self, input: &str) -> OperationResult {
1266 let parts: Vec<&str> = input.splitn(2, '\n').collect();
1267 if parts.len() < 2 {
1268 return OperationResult::failure(
1269 "move",
1270 PathBuf::new(),
1271 "Invalid format. Expected: src\\ndst".into(),
1272 );
1273 }
1274 let src = PathBuf::from(parts[0].trim());
1275 let dst = PathBuf::from(parts[1].trim());
1276 self.move_file(&src, &dst).await
1277 }
1278
1279 pub async fn parse_and_batch(&self, input: &str) -> BatchResult {
1281 match serde_json::from_str::<BatchRequest>(input) {
1282 Ok(request) => self.batch(request).await,
1283 Err(e) => BatchResult {
1284 id: Uuid::new_v4(),
1285 success: false,
1286 total: 0,
1287 succeeded: 0,
1288 failed: 1,
1289 results: vec![OperationResult::failure(
1290 "batch",
1291 PathBuf::new(),
1292 format!("Invalid JSON: {e}"),
1293 )],
1294 timestamp: Utc::now(),
1295 rollback_performed: None,
1296 rollback_details: None,
1297 },
1298 }
1299 }
1300}
1301
1302#[cfg(test)]
1303mod tests {
1304 use super::*;
1305 use tempfile::TempDir;
1306
1307 fn create_test_manager() -> (OpsManager, TempDir) {
1308 let temp_dir = TempDir::new().unwrap();
1309 let manager = OpsManager::new(temp_dir.path().to_path_buf(), None, None);
1310 (manager, temp_dir)
1311 }
1312
1313 #[tokio::test]
1314 async fn test_create_file() {
1315 let (manager, _temp) = create_test_manager();
1316 let path = PathBuf::from("test.txt");
1317
1318 let result = manager.create(&path, "Hello, World!").await;
1319
1320 assert!(result.success);
1321 assert_eq!(result.operation, "create");
1322 assert!(manager.resolve_path(&path).exists());
1323 }
1324
1325 #[tokio::test]
1326 async fn test_create_file_already_exists() {
1327 let (manager, temp) = create_test_manager();
1328 let path = PathBuf::from("existing.txt");
1329 fs::write(temp.path().join("existing.txt"), "content").unwrap();
1330
1331 let result = manager.create(&path, "new content").await;
1332
1333 assert!(!result.success);
1334 assert!(result.error.unwrap().contains("already exists"));
1335 }
1336
1337 #[tokio::test]
1338 async fn test_delete_file() {
1339 let (manager, temp) = create_test_manager();
1340 let path = PathBuf::from("to_delete.txt");
1341 fs::write(temp.path().join("to_delete.txt"), "content").unwrap();
1342
1343 let result = manager.delete(&path).await;
1344
1345 assert!(result.success);
1346 assert!(!manager.resolve_path(&path).exists());
1347 }
1348
1349 #[tokio::test]
1350 async fn test_delete_nonexistent() {
1351 let (manager, _temp) = create_test_manager();
1352 let path = PathBuf::from("nonexistent.txt");
1353
1354 let result = manager.delete(&path).await;
1355
1356 assert!(!result.success);
1357 assert!(result.error.unwrap().contains("not found"));
1358 }
1359
1360 #[tokio::test]
1361 async fn test_move_file() {
1362 let (manager, temp) = create_test_manager();
1363 let src = PathBuf::from("source.txt");
1364 let dst = PathBuf::from("dest.txt");
1365 fs::write(temp.path().join("source.txt"), "content").unwrap();
1366
1367 let result = manager.move_file(&src, &dst).await;
1368
1369 assert!(result.success);
1370 assert!(!manager.resolve_path(&src).exists());
1371 assert!(manager.resolve_path(&dst).exists());
1372 }
1373
1374 #[tokio::test]
1375 async fn test_copy_file() {
1376 let (manager, temp) = create_test_manager();
1377 let src = PathBuf::from("original.txt");
1378 let dst = PathBuf::from("copy.txt");
1379 fs::write(temp.path().join("original.txt"), "content").unwrap();
1380
1381 let result = manager.copy(&src, &dst).await;
1382
1383 assert!(result.success);
1384 assert!(manager.resolve_path(&src).exists());
1385 assert!(manager.resolve_path(&dst).exists());
1386 }
1387
1388 #[tokio::test]
1389 async fn test_write_file() {
1390 let (manager, _temp) = create_test_manager();
1391 let path = PathBuf::from("write_test.txt");
1392
1393 let result = manager.write(&path, "content", false).await;
1394
1395 assert!(result.success);
1396 let content = fs::read_to_string(manager.resolve_path(&path)).unwrap();
1397 assert_eq!(content, "content");
1398 }
1399
1400 #[tokio::test]
1401 async fn test_write_append() {
1402 let (manager, temp) = create_test_manager();
1403 let path = PathBuf::from("append_test.txt");
1404 fs::write(temp.path().join("append_test.txt"), "first").unwrap();
1405
1406 let result = manager.write(&path, " second", true).await;
1407
1408 assert!(result.success);
1409 let content = fs::read_to_string(manager.resolve_path(&path)).unwrap();
1410 assert_eq!(content, "first second");
1411 }
1412
1413 #[tokio::test]
1414 async fn test_batch_operations() {
1415 let (manager, _temp) = create_test_manager();
1416
1417 let request = BatchRequest {
1418 operations: vec![
1419 Operation::Create {
1420 path: PathBuf::from("file1.txt"),
1421 content: "content1".to_string(),
1422 },
1423 Operation::Create {
1424 path: PathBuf::from("file2.txt"),
1425 content: "content2".to_string(),
1426 },
1427 ],
1428 atomic: false,
1429 dry_run: false,
1430 };
1431
1432 let result = manager.batch(request).await;
1433
1434 assert!(result.success);
1435 assert_eq!(result.total, 2);
1436 assert_eq!(result.succeeded, 2);
1437 assert_eq!(result.failed, 0);
1438 }
1439
1440 #[tokio::test]
1441 async fn test_batch_dry_run() {
1442 let (manager, _temp) = create_test_manager();
1443
1444 let request = BatchRequest {
1445 operations: vec![Operation::Create {
1446 path: PathBuf::from("dry_run.txt"),
1447 content: "content".to_string(),
1448 }],
1449 atomic: false,
1450 dry_run: true,
1451 };
1452
1453 let result = manager.batch(request).await;
1454
1455 assert!(result.success);
1456 assert!(!manager.resolve_path(&PathBuf::from("dry_run.txt")).exists());
1458 }
1459
1460 #[tokio::test]
1461 async fn test_parse_and_create() {
1462 let (manager, _temp) = create_test_manager();
1463
1464 let result = manager.parse_and_create("test.txt\nHello!").await;
1465
1466 assert!(result.success);
1467 let content = fs::read_to_string(manager.resolve_path(&PathBuf::from("test.txt"))).unwrap();
1468 assert_eq!(content, "Hello!");
1469 }
1470
1471 #[tokio::test]
1472 async fn test_parse_and_move() {
1473 let (manager, temp) = create_test_manager();
1474 fs::write(temp.path().join("src.txt"), "content").unwrap();
1475
1476 let result = manager.parse_and_move("src.txt\ndst.txt").await;
1477
1478 assert!(result.success);
1479 }
1480
1481 #[tokio::test]
1482 async fn test_parse_and_batch() {
1483 let (manager, _temp) = create_test_manager();
1484
1485 let json = r#"{"operations":[{"op":"create","path":"batch.txt","content":"test"}]}"#;
1486 let result = manager.parse_and_batch(json).await;
1487
1488 assert!(result.success);
1489 }
1490
1491 #[tokio::test]
1492 async fn test_last_result() {
1493 let (manager, _temp) = create_test_manager();
1494
1495 let initial = manager.get_last_result().await;
1497 let initial_str = String::from_utf8(initial).unwrap();
1498 assert!(initial_str.contains("No operations"));
1499
1500 manager.create(&PathBuf::from("test.txt"), "content").await;
1502 let after = manager.get_last_result().await;
1503 let after_str = String::from_utf8(after).unwrap();
1504 assert!(after_str.contains("create"));
1505 assert!(after_str.contains("success"));
1506 }
1507
1508 #[test]
1509 fn test_operation_result_success() {
1510 let result = OperationResult::success("test", PathBuf::from("/test"), true);
1511 assert!(result.success);
1512 assert!(result.error.is_none());
1513 assert!(result.undo_id.is_some());
1514 }
1515
1516 #[test]
1517 fn test_operation_result_failure() {
1518 let result = OperationResult::failure("test", PathBuf::from("/test"), "error".into());
1519 assert!(!result.success);
1520 assert!(result.error.is_some());
1521 assert!(result.undo_id.is_none());
1522 }
1523
1524 #[tokio::test]
1525 async fn test_atomic_batch_rollback_on_failure() {
1526 let (manager, temp) = create_test_manager();
1527
1528 fs::write(temp.path().join("existing.txt"), "exists").unwrap();
1530
1531 let request = BatchRequest {
1532 operations: vec![
1533 Operation::Create {
1534 path: PathBuf::from("new.txt"),
1535 content: "content".to_string(),
1536 },
1537 Operation::Create {
1538 path: PathBuf::from("existing.txt"), content: "content".to_string(),
1540 },
1541 ],
1542 atomic: true,
1543 dry_run: false,
1544 };
1545
1546 let result = manager.batch(request).await;
1547
1548 assert!(!result.success);
1549 assert_eq!(result.succeeded, 1);
1550 assert_eq!(result.failed, 1);
1551 assert_eq!(result.rollback_performed, Some(true));
1552 assert!(result.rollback_details.is_some());
1553 let details = result.rollback_details.unwrap();
1554 assert_eq!(details.rolled_back, 1);
1555 assert_eq!(details.rollback_failures, 0);
1556 assert!(!temp.path().join("new.txt").exists());
1558 }
1559
1560 #[tokio::test]
1561 async fn test_atomic_batch_rollback_move_operations() {
1562 let (manager, temp) = create_test_manager();
1563
1564 fs::write(temp.path().join("file1.txt"), "content1").unwrap();
1565
1566 let request = BatchRequest {
1567 operations: vec![
1568 Operation::Move {
1569 src: PathBuf::from("file1.txt"),
1570 dst: PathBuf::from("moved1.txt"),
1571 },
1572 Operation::Delete {
1573 path: PathBuf::from("nonexistent.txt"), },
1575 ],
1576 atomic: true,
1577 dry_run: false,
1578 };
1579
1580 let result = manager.batch(request).await;
1581
1582 assert!(!result.success);
1583 assert_eq!(result.rollback_performed, Some(true));
1584 assert!(temp.path().join("file1.txt").exists());
1586 assert!(!temp.path().join("moved1.txt").exists());
1587 }
1588
1589 #[tokio::test]
1590 async fn test_atomic_batch_rollback_write_restores_content() {
1591 let (manager, temp) = create_test_manager();
1592
1593 fs::write(temp.path().join("existing.txt"), "original content").unwrap();
1594
1595 let request = BatchRequest {
1596 operations: vec![
1597 Operation::Write {
1598 path: PathBuf::from("existing.txt"),
1599 content: "modified content".to_string(),
1600 append: false,
1601 },
1602 Operation::Delete {
1603 path: PathBuf::from("nonexistent.txt"), },
1605 ],
1606 atomic: true,
1607 dry_run: false,
1608 };
1609
1610 let result = manager.batch(request).await;
1611
1612 assert!(!result.success);
1613 assert_eq!(result.rollback_performed, Some(true));
1614 let content = fs::read_to_string(temp.path().join("existing.txt")).unwrap();
1616 assert_eq!(content, "original content");
1617 }
1618
1619 #[tokio::test]
1620 async fn test_non_atomic_batch_no_rollback() {
1621 let (manager, temp) = create_test_manager();
1622
1623 let request = BatchRequest {
1624 operations: vec![
1625 Operation::Create {
1626 path: PathBuf::from("file1.txt"),
1627 content: "content".to_string(),
1628 },
1629 Operation::Delete {
1630 path: PathBuf::from("nonexistent.txt"), },
1632 Operation::Create {
1633 path: PathBuf::from("file2.txt"),
1634 content: "content".to_string(),
1635 },
1636 ],
1637 atomic: false, dry_run: false,
1639 };
1640
1641 let result = manager.batch(request).await;
1642
1643 assert!(!result.success);
1644 assert!(result.rollback_performed.is_none());
1645 assert!(temp.path().join("file1.txt").exists());
1647 assert!(temp.path().join("file2.txt").exists());
1648 }
1649
1650 #[tokio::test]
1651 async fn test_atomic_batch_first_op_fails_no_rollback_needed() {
1652 let (manager, temp) = create_test_manager();
1653
1654 fs::write(temp.path().join("existing.txt"), "exists").unwrap();
1656
1657 let request = BatchRequest {
1658 operations: vec![
1659 Operation::Create {
1660 path: PathBuf::from("existing.txt"), content: "content".to_string(),
1662 },
1663 Operation::Create {
1664 path: PathBuf::from("new.txt"),
1665 content: "content".to_string(),
1666 },
1667 ],
1668 atomic: true,
1669 dry_run: false,
1670 };
1671
1672 let result = manager.batch(request).await;
1673
1674 assert!(!result.success);
1675 assert_eq!(result.succeeded, 0);
1676 assert_eq!(result.failed, 1);
1677 assert!(result.rollback_performed.is_none());
1679 assert!(result.rollback_details.is_none());
1680 assert!(!temp.path().join("new.txt").exists());
1682 }
1683
1684 #[tokio::test]
1685 async fn test_atomic_batch_copy_rollback() {
1686 let (manager, temp) = create_test_manager();
1687
1688 fs::write(temp.path().join("source.txt"), "source content").unwrap();
1689
1690 let request = BatchRequest {
1691 operations: vec![
1692 Operation::Copy {
1693 src: PathBuf::from("source.txt"),
1694 dst: PathBuf::from("copied.txt"),
1695 },
1696 Operation::Delete {
1697 path: PathBuf::from("nonexistent.txt"), },
1699 ],
1700 atomic: true,
1701 dry_run: false,
1702 };
1703
1704 let result = manager.batch(request).await;
1705
1706 assert!(!result.success);
1707 assert_eq!(result.rollback_performed, Some(true));
1708 assert!(temp.path().join("source.txt").exists());
1710 assert!(!temp.path().join("copied.txt").exists());
1711 }
1712
1713 #[tokio::test]
1714 async fn test_mkdir() {
1715 let (manager, temp) = create_test_manager();
1716 let path = PathBuf::from("new_directory");
1717
1718 let result = manager.mkdir(&path).await;
1719
1720 assert!(result.success);
1721 assert_eq!(result.operation, "mkdir");
1722 assert!(temp.path().join("new_directory").is_dir());
1723 }
1724
1725 #[tokio::test]
1726 async fn test_mkdir_nested() {
1727 let (manager, temp) = create_test_manager();
1728 let path = PathBuf::from("parent/child/grandchild");
1729
1730 let result = manager.mkdir(&path).await;
1731
1732 assert!(result.success);
1733 assert!(temp.path().join("parent/child/grandchild").is_dir());
1734 }
1735
1736 #[tokio::test]
1737 async fn test_mkdir_already_exists() {
1738 let (manager, temp) = create_test_manager();
1739 let path = PathBuf::from("existing_dir");
1740 fs::create_dir(temp.path().join("existing_dir")).unwrap();
1741
1742 let result = manager.mkdir(&path).await;
1743
1744 assert!(!result.success);
1745 assert!(result.error.unwrap().contains("already exists"));
1746 }
1747
1748 #[tokio::test]
1749 #[cfg(unix)]
1750 async fn test_symlink() {
1751 let (manager, temp) = create_test_manager();
1752 let target = PathBuf::from("target_file.txt");
1753 let link = PathBuf::from("link_to_target");
1754
1755 fs::write(temp.path().join("target_file.txt"), "content").unwrap();
1757
1758 let result = manager.symlink(&target, &link).await;
1759
1760 assert!(result.success);
1761 assert_eq!(result.operation, "symlink");
1762 let link_path = temp.path().join("link_to_target");
1763 assert!(link_path.symlink_metadata().is_ok());
1764 }
1765
1766 #[tokio::test]
1767 #[cfg(unix)]
1768 async fn test_symlink_already_exists() {
1769 let (manager, temp) = create_test_manager();
1770 let target = PathBuf::from("target.txt");
1771 let link = PathBuf::from("existing_link");
1772
1773 fs::write(temp.path().join("existing_link"), "content").unwrap();
1775
1776 let result = manager.symlink(&target, &link).await;
1777
1778 assert!(!result.success);
1779 assert!(result.error.unwrap().contains("already exists"));
1780 }
1781
1782 #[tokio::test]
1783 async fn test_atomic_batch_mkdir_rollback() {
1784 let (manager, temp) = create_test_manager();
1785
1786 let request = BatchRequest {
1787 operations: vec![
1788 Operation::Mkdir {
1789 path: PathBuf::from("new_dir"),
1790 },
1791 Operation::Delete {
1792 path: PathBuf::from("nonexistent.txt"), },
1794 ],
1795 atomic: true,
1796 dry_run: false,
1797 };
1798
1799 let result = manager.batch(request).await;
1800
1801 assert!(!result.success);
1802 assert_eq!(result.rollback_performed, Some(true));
1803 assert!(!temp.path().join("new_dir").exists());
1805 }
1806}