1use async_trait::async_trait;
16use chrono::{DateTime, Utc};
17use notify::{Event as NotifyEvent, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
18use serde::{Deserialize, Serialize};
19use sha2::{Digest, Sha256};
20use std::collections::HashMap;
21use std::path::{Path, PathBuf};
22use std::sync::Arc;
23use std::time::{Duration, UNIX_EPOCH};
24use tokio::fs;
25use tokio::io::AsyncReadExt;
26use tokio::sync::{broadcast, RwLock};
27use uuid::Uuid;
28
29use crate::config::FileConfig;
30use crate::error::{Error, FileOperation, Result, ResultExt};
31use crate::event::{Event, EventBusManager};
32use crate::manager::{ManagedState, Manager, ManagerStatus};
33use crate::types::Metadata;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37pub enum FileType {
38 Text,
40 Binary,
42 Image,
44 Audio,
46 Video,
48 Archive,
50 Document,
52 Data,
54 Unknown,
56}
57
58impl FileType {
59 pub fn from_extension(path: &Path) -> Self {
61 let extension = path
62 .extension()
63 .and_then(|ext| ext.to_str())
64 .map(|s| s.to_lowercase());
65
66 match extension.as_deref() {
67 Some(ext) => match ext {
68 "txt" | "md" | "csv" | "json" | "xml" | "html" | "htm" | "css" | "js" | "py"
69 | "rs" | "toml" | "yaml" | "yml" | "ini" | "conf" | "cfg" | "log" => Self::Text,
70 "exe" | "dll" | "so" | "dylib" | "bin" => Self::Binary,
71 "jpg" | "jpeg" | "png" | "gif" | "bmp" | "tiff" | "svg" | "ico" | "webp" => {
72 Self::Image
73 }
74 "mp3" | "wav" | "flac" | "aac" | "ogg" | "m4a" | "wma" => Self::Audio,
75 "mp4" | "avi" | "mov" | "wmv" | "flv" | "webm" | "mkv" | "m4v" => Self::Video,
76 "zip" | "rar" | "7z" | "tar" | "gz" | "bz2" | "xz" | "lz4" | "zst" => Self::Archive,
77 "pdf" | "doc" | "docx" | "xls" | "xlsx" | "ppt" | "pptx" | "odt" | "ods"
78 | "odp" => Self::Document,
79 "db" | "sqlite" | "sqlite3" | "parquet" | "avro" | "tsv" => Self::Data,
80 _ => Self::Unknown,
81 },
82 None => Self::Unknown,
83 }
84 }
85
86 pub fn mime_type(&self) -> &'static str {
88 match self {
89 Self::Text => "text/plain",
90 Self::Binary => "application/octet-stream",
91 Self::Image => "image/*",
92 Self::Audio => "audio/*",
93 Self::Video => "video/*",
94 Self::Archive => "application/zip",
95 Self::Document => "application/pdf",
96 Self::Data => "application/json",
97 Self::Unknown => "application/octet-stream",
98 }
99 }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct FileMetadata {
105 pub path: PathBuf,
107 pub size: u64,
109 pub file_type: FileType,
111 pub mime_type: String,
113 pub permissions: u32,
115 pub read_only: bool,
117 pub hidden: bool,
119 pub created: Option<DateTime<Utc>>,
121 pub modified: Option<DateTime<Utc>>,
123 pub accessed: Option<DateTime<Utc>>,
125 pub hash: Option<String>,
127 pub metadata: Metadata,
129}
130
131impl FileMetadata {
132 pub async fn from_path(path: impl AsRef<Path>) -> Result<Self> {
134 let path = path.as_ref();
135 let metadata = fs::metadata(path)
136 .await
137 .with_context(|| format!("Failed to get metadata for: {}", path.display()))?;
138
139 let file_type = FileType::from_extension(path);
140 let mime_type = file_type.mime_type().to_string();
141
142 let created = metadata.created().ok().and_then(|t| {
143 DateTime::from_timestamp(t.duration_since(UNIX_EPOCH).ok()?.as_secs() as i64, 0)
144 });
145
146 let modified = metadata.modified().ok().and_then(|t| {
147 DateTime::from_timestamp(t.duration_since(UNIX_EPOCH).ok()?.as_secs() as i64, 0)
148 });
149
150 let accessed = metadata.accessed().ok().and_then(|t| {
151 DateTime::from_timestamp(t.duration_since(UNIX_EPOCH).ok()?.as_secs() as i64, 0)
152 });
153
154 #[cfg(unix)]
155 let permissions = {
156 use std::os::unix::fs::PermissionsExt;
157 metadata.permissions().mode()
158 };
159
160 #[cfg(not(unix))]
161 let permissions = if metadata.permissions().readonly() {
162 0o444
163 } else {
164 0o644
165 };
166
167 Ok(Self {
168 path: path.to_path_buf(),
169 size: metadata.len(),
170 file_type,
171 mime_type,
172 permissions,
173 read_only: metadata.permissions().readonly(),
174 hidden: path
175 .file_name()
176 .and_then(|name| name.to_str())
177 .map(|name| name.starts_with('.'))
178 .unwrap_or(false),
179 created,
180 modified,
181 accessed,
182 hash: None,
183 metadata: HashMap::new(),
184 })
185 }
186
187 pub async fn calculate_hash(&mut self) -> Result<()> {
189 let hash = calculate_file_hash(&self.path).await?;
190 self.hash = Some(hash);
191 Ok(())
192 }
193}
194
195#[derive(Debug, Clone)]
197pub struct FileOperationOptions {
198 pub create_parents: bool,
200 pub overwrite: bool,
202 pub permissions: Option<u32>,
204 pub preserve_timestamps: bool,
206 pub calculate_checksum: bool,
208 pub timeout: Option<Duration>,
210 pub atomic: bool,
212}
213
214impl Default for FileOperationOptions {
215 fn default() -> Self {
216 Self {
217 create_parents: true,
218 overwrite: false,
219 permissions: None,
220 preserve_timestamps: true,
221 calculate_checksum: false,
222 timeout: Some(Duration::from_secs(30)),
223 atomic: true,
224 }
225 }
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct FileOperationProgress {
231 pub operation_id: Uuid,
233 pub operation: FileOperation,
235 pub source: Option<PathBuf>,
237 pub destination: Option<PathBuf>,
239 pub total_bytes: u64,
241 pub processed_bytes: u64,
243 pub current_file: Option<PathBuf>,
245 pub started_at: DateTime<Utc>,
247 pub estimated_completion: Option<DateTime<Utc>>,
249 pub status: FileOperationStatus,
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
255pub enum FileOperationStatus {
256 Pending,
258 InProgress,
260 Completed,
262 Failed,
264 Cancelled,
266 Paused,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct FileChangeEvent {
273 pub event_type: FileChangeType,
275 pub path: PathBuf,
277 pub timestamp: DateTime<Utc>,
279 pub metadata: Metadata,
281 pub source: String,
283}
284
285impl Event for FileChangeEvent {
286 fn event_type(&self) -> &'static str {
287 "file.changed"
288 }
289
290 fn source(&self) -> &str {
291 &self.source
292 }
293
294 fn metadata(&self) -> &Metadata {
295 &self.metadata
296 }
297
298 fn as_any(&self) -> &dyn std::any::Any {
299 self
300 }
301
302 fn timestamp(&self) -> DateTime<Utc> {
303 self.timestamp
304 }
305}
306
307#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
309pub enum FileChangeType {
310 Created,
312 Modified,
314 Deleted,
316 Renamed,
318 MetadataChanged,
320}
321
322pub struct FileWatcher {
324 watcher: Option<RecommendedWatcher>,
325 event_sender: broadcast::Sender<FileChangeEvent>,
326 watched_paths: RwLock<HashMap<PathBuf, bool>>, }
328
329impl FileWatcher {
330 pub fn new() -> Result<Self> {
332 let (event_sender, _) = broadcast::channel(1000);
333
334 Ok(Self {
335 watcher: None,
336 event_sender,
337 watched_paths: RwLock::new(HashMap::new()),
338 })
339 }
340
341 pub async fn watch_path(&mut self, path: impl AsRef<Path>, recursive: bool) -> Result<()> {
343 let path = path.as_ref().to_path_buf();
344
345 if self.watcher.is_none() {
347 let sender = self.event_sender.clone();
348 let watcher = RecommendedWatcher::new(
349 move |result: notify::Result<NotifyEvent>| {
350 if let Ok(event) = result {
351 Self::handle_notify_event(event, &sender);
352 }
353 },
354 notify::Config::default(),
355 )
356 .map_err(|e| {
357 Error::new(
358 crate::error::ErrorKind::File {
359 path: Some(path.display().to_string()),
360 operation: FileOperation::Watch,
361 },
362 format!("Failed to create file watcher: {}", e),
363 )
364 })?;
365
366 self.watcher = Some(watcher);
367 }
368
369 let mode = if recursive {
371 RecursiveMode::Recursive
372 } else {
373 RecursiveMode::NonRecursive
374 };
375
376 if let Some(ref mut watcher) = self.watcher {
377 watcher.watch(&path, mode).map_err(|e| {
378 Error::new(
379 crate::error::ErrorKind::File {
380 path: Some(path.display().to_string()),
381 operation: FileOperation::Watch,
382 },
383 format!("Failed to watch path: {}", e),
384 )
385 })?;
386 }
387
388 self.watched_paths.write().await.insert(path, recursive);
390
391 Ok(())
392 }
393
394 pub async fn unwatch_path(&mut self, path: impl AsRef<Path>) -> Result<()> {
396 let path = path.as_ref().to_path_buf();
397
398 if let Some(ref mut watcher) = self.watcher {
399 watcher.unwatch(&path).map_err(|e| {
400 Error::new(
401 crate::error::ErrorKind::File {
402 path: Some(path.display().to_string()),
403 operation: FileOperation::Watch,
404 },
405 format!("Failed to unwatch path: {}", e),
406 )
407 })?;
408 }
409
410 self.watched_paths.write().await.remove(&path);
412
413 Ok(())
414 }
415
416 pub fn subscribe(&self) -> broadcast::Receiver<FileChangeEvent> {
418 self.event_sender.subscribe()
419 }
420
421 fn handle_notify_event(event: NotifyEvent, sender: &broadcast::Sender<FileChangeEvent>) {
423 let change_type = match event.kind {
424 EventKind::Create(_) => FileChangeType::Created,
425 EventKind::Modify(_) => FileChangeType::Modified,
426 EventKind::Remove(_) => FileChangeType::Deleted,
427 EventKind::Access(_) => FileChangeType::MetadataChanged,
428 _ => return, };
430
431 for path in event.paths {
432 let file_event = FileChangeEvent {
433 event_type: change_type,
434 path: path.clone(),
435 timestamp: Utc::now(),
436 metadata: HashMap::new(),
437 source: "file_watcher".to_string(),
438 };
439
440 let _ = sender.send(file_event);
441 }
442 }
443}
444
445pub struct FileManager {
447 state: ManagedState,
448 config: FileConfig,
449 watcher: Option<FileWatcher>,
450 event_bus: Option<Arc<EventBusManager>>,
451 operations: Arc<RwLock<HashMap<Uuid, FileOperationProgress>>>,
452}
453
454impl std::fmt::Debug for FileManager {
455 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
456 f.debug_struct("FileManager")
457 .field("config", &self.config)
458 .field("operations", &self.operations)
459 .finish()
460 }
461}
462
463impl FileManager {
464 pub fn new(config: FileConfig) -> Self {
466 Self {
467 state: ManagedState::new(Uuid::new_v4(), "file_manager"),
468 config,
469 watcher: None,
470 event_bus: None,
471 operations: Arc::new(RwLock::new(HashMap::new())),
472 }
473 }
474
475 pub fn set_event_bus(&mut self, event_bus: Arc<EventBusManager>) {
477 self.event_bus = Some(event_bus);
478 }
479
480 pub async fn read_file(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
482 let path = path.as_ref();
483
484 let metadata = fs::metadata(path)
486 .await
487 .with_context(|| format!("Failed to get metadata for: {}", path.display()))?;
488
489 if metadata.len() > self.config.max_file_size {
490 return Err(Error::new(
491 crate::error::ErrorKind::File {
492 path: Some(path.display().to_string()),
493 operation: FileOperation::Read,
494 },
495 format!(
496 "File size ({} bytes) exceeds maximum allowed size ({} bytes)",
497 metadata.len(),
498 self.config.max_file_size
499 ),
500 ));
501 }
502
503 fs::read(path)
504 .await
505 .with_context(|| format!("Failed to read file: {}", path.display()))
506 }
507
508 pub async fn read_file_to_string(&self, path: impl AsRef<Path>) -> Result<String> {
510 let contents = self.read_file(&path).await?;
511 String::from_utf8(contents).map_err(|e| {
512 Error::new(
513 crate::error::ErrorKind::File {
514 path: Some(path.as_ref().display().to_string()),
515 operation: FileOperation::Read,
516 },
517 format!("File contains invalid UTF-8: {}", e),
518 )
519 })
520 }
521
522 pub async fn write_file(
524 &self,
525 path: impl AsRef<Path>,
526 data: &[u8],
527 options: Option<FileOperationOptions>,
528 ) -> Result<()> {
529 let path = path.as_ref();
530 let options = options.unwrap_or_default();
531
532 if data.len() as u64 > self.config.max_file_size {
534 return Err(Error::new(
535 crate::error::ErrorKind::File {
536 path: Some(path.display().to_string()),
537 operation: FileOperation::Write,
538 },
539 format!(
540 "Data size ({} bytes) exceeds maximum allowed size ({} bytes)",
541 data.len(),
542 self.config.max_file_size
543 ),
544 ));
545 }
546
547 if options.create_parents {
549 if let Some(parent) = path.parent() {
550 fs::create_dir_all(parent).await.with_context(|| {
551 format!(
552 "Failed to create parent directories for: {}",
553 path.display()
554 )
555 })?;
556 }
557 }
558
559 if !options.overwrite && path.exists() {
561 return Err(Error::new(
562 crate::error::ErrorKind::File {
563 path: Some(path.display().to_string()),
564 operation: FileOperation::Write,
565 },
566 "File already exists and overwrite is disabled",
567 ));
568 }
569
570 if options.atomic {
571 self.atomic_write(path, data, &options).await
573 } else {
574 fs::write(path, data)
576 .await
577 .with_context(|| format!("Failed to write file: {}", path.display()))?;
578
579 self.apply_file_options(path, &options).await
580 }
581 }
582
583 async fn atomic_write(
585 &self,
586 path: &Path,
587 data: &[u8],
588 options: &FileOperationOptions,
589 ) -> Result<()> {
590 let temp_path = path.with_extension("tmp");
591
592 fs::write(&temp_path, data)
594 .await
595 .with_context(|| format!("Failed to write temporary file: {}", temp_path.display()))?;
596
597 self.apply_file_options(&temp_path, options).await?;
599
600 fs::rename(&temp_path, path).await.with_context(|| {
602 format!(
603 "Failed to rename {} to {}",
604 temp_path.display(),
605 path.display()
606 )
607 })?;
608
609 Ok(())
610 }
611
612 async fn apply_file_options(&self, _path: &Path, options: &FileOperationOptions) -> Result<()> {
614 if let Some(_permissions) = options.permissions {
616 #[cfg(unix)]
617 {
618 use std::os::unix::fs::PermissionsExt;
619 let perms = std::fs::Permissions::from_mode(_permissions);
620 fs::set_permissions(_path, perms).await.with_context(|| {
621 format!("Failed to set permissions for: {}", _path.display())
622 })?;
623 }
624 }
625
626 Ok(())
627 }
628
629 pub async fn copy_file(
631 &self,
632 source: impl AsRef<Path>,
633 destination: impl AsRef<Path>,
634 options: Option<FileOperationOptions>,
635 ) -> Result<u64> {
636 let source = source.as_ref();
637 let destination = destination.as_ref();
638 let options = options.unwrap_or_default();
639
640 if !source.exists() {
642 return Err(Error::new(
643 crate::error::ErrorKind::File {
644 path: Some(source.display().to_string()),
645 operation: FileOperation::Copy,
646 },
647 "Source file does not exist",
648 ));
649 }
650
651 if options.create_parents {
653 if let Some(parent) = destination.parent() {
654 fs::create_dir_all(parent).await.with_context(|| {
655 format!(
656 "Failed to create parent directories for: {}",
657 destination.display()
658 )
659 })?;
660 }
661 }
662
663 if !options.overwrite && destination.exists() {
665 return Err(Error::new(
666 crate::error::ErrorKind::File {
667 path: Some(destination.display().to_string()),
668 operation: FileOperation::Copy,
669 },
670 "Destination file already exists and overwrite is disabled",
671 ));
672 }
673
674 let src_metadata = fs::metadata(source)
676 .await
677 .with_context(|| format!("Failed to get source metadata: {}", source.display()))?;
678
679 if src_metadata.len() > self.config.max_file_size {
680 return Err(Error::new(
681 crate::error::ErrorKind::File {
682 path: Some(source.display().to_string()),
683 operation: FileOperation::Copy,
684 },
685 format!(
686 "Source file size ({} bytes) exceeds maximum allowed size ({} bytes)",
687 src_metadata.len(),
688 self.config.max_file_size
689 ),
690 ));
691 }
692
693 let bytes_copied = fs::copy(source, destination).await.with_context(|| {
695 format!(
696 "Failed to copy {} to {}",
697 source.display(),
698 destination.display()
699 )
700 })?;
701
702 if options.preserve_timestamps {
704 if let (Ok(_accessed), Ok(_modified)) =
705 (src_metadata.accessed(), src_metadata.modified())
706 {
707 }
710 }
711
712 self.apply_file_options(destination, &options).await?;
714
715 Ok(bytes_copied)
716 }
717
718 pub async fn move_file(
720 &self,
721 source: impl AsRef<Path>,
722 destination: impl AsRef<Path>,
723 options: Option<FileOperationOptions>,
724 ) -> Result<()> {
725 let source = source.as_ref();
726 let destination = destination.as_ref();
727 let options = options.unwrap_or_default();
728
729 if !source.exists() {
731 return Err(Error::new(
732 crate::error::ErrorKind::File {
733 path: Some(source.display().to_string()),
734 operation: FileOperation::Move,
735 },
736 "Source file does not exist",
737 ));
738 }
739
740 if options.create_parents {
742 if let Some(parent) = destination.parent() {
743 fs::create_dir_all(parent).await.with_context(|| {
744 format!(
745 "Failed to create parent directories for: {}",
746 destination.display()
747 )
748 })?;
749 }
750 }
751
752 if !options.overwrite && destination.exists() {
754 return Err(Error::new(
755 crate::error::ErrorKind::File {
756 path: Some(destination.display().to_string()),
757 operation: FileOperation::Move,
758 },
759 "Destination file already exists and overwrite is disabled",
760 ));
761 }
762
763 fs::rename(source, destination).await.with_context(|| {
765 format!(
766 "Failed to move {} to {}",
767 source.display(),
768 destination.display()
769 )
770 })?;
771
772 Ok(())
773 }
774
775 pub async fn delete_file(&self, path: impl AsRef<Path>) -> Result<()> {
777 let path = path.as_ref();
778
779 if !path.exists() {
780 return Err(Error::new(
781 crate::error::ErrorKind::File {
782 path: Some(path.display().to_string()),
783 operation: FileOperation::Delete,
784 },
785 "File does not exist",
786 ));
787 }
788
789 fs::remove_file(path)
790 .await
791 .with_context(|| format!("Failed to delete file: {}", path.display()))?;
792
793 Ok(())
794 }
795
796 pub async fn create_directory(&self, path: impl AsRef<Path>, recursive: bool) -> Result<()> {
798 let path = path.as_ref();
799
800 if recursive {
801 fs::create_dir_all(path).await
802 } else {
803 fs::create_dir(path).await
804 }
805 .with_context(|| format!("Failed to create directory: {}", path.display()))?;
806
807 Ok(())
808 }
809
810 pub async fn delete_directory(&self, path: impl AsRef<Path>, recursive: bool) -> Result<()> {
812 let path = path.as_ref();
813
814 if !path.exists() {
815 return Err(Error::new(
816 crate::error::ErrorKind::File {
817 path: Some(path.display().to_string()),
818 operation: FileOperation::Delete,
819 },
820 "Directory does not exist",
821 ));
822 }
823
824 if recursive {
825 fs::remove_dir_all(path).await
826 } else {
827 fs::remove_dir(path).await
828 }
829 .with_context(|| format!("Failed to delete directory: {}", path.display()))?;
830
831 Ok(())
832 }
833
834 pub async fn list_directory(&self, path: impl AsRef<Path>) -> Result<Vec<FileMetadata>> {
836 let path = path.as_ref();
837 let mut entries = fs::read_dir(path)
838 .await
839 .with_context(|| format!("Failed to read directory: {}", path.display()))?;
840
841 let mut file_list = Vec::new();
842
843 while let Some(entry) = entries
844 .next_entry()
845 .await
846 .with_context(|| format!("Failed to read directory entry in: {}", path.display()))?
847 {
848 let entry_path = entry.path();
849 match FileMetadata::from_path(&entry_path).await {
850 Ok(metadata) => file_list.push(metadata),
851 Err(e) => {
852 tracing::warn!("Failed to get metadata for {}: {}", entry_path.display(), e);
853 }
854 }
855 }
856
857 Ok(file_list)
858 }
859
860 pub async fn get_metadata(&self, path: impl AsRef<Path>) -> Result<FileMetadata> {
862 FileMetadata::from_path(path).await
863 }
864
865 pub async fn exists(&self, path: impl AsRef<Path>) -> bool {
867 path.as_ref().exists()
868 }
869
870 pub async fn file_size(&self, path: impl AsRef<Path>) -> Result<u64> {
872 let metadata = fs::metadata(path.as_ref())
873 .await
874 .with_context(|| format!("Failed to get metadata for: {}", path.as_ref().display()))?;
875 Ok(metadata.len())
876 }
877
878 pub async fn create_temp_file(
880 &self,
881 prefix: Option<&str>,
882 suffix: Option<&str>,
883 ) -> Result<PathBuf> {
884 let prefix = prefix.unwrap_or("temp");
885 let suffix = suffix.unwrap_or(".tmp");
886 let filename = format!("{}_{}_{}", prefix, Uuid::new_v4(), suffix);
887 let temp_path = self
889 .config
890 .temp_dir
891 .as_ref()
892 .map(|dir| dir.join(&filename))
893 .ok_or_else(|| {
894 Error::file(
895 "temp_dir",
896 FileOperation::Read,
897 "Temp directory not available",
898 )
899 })?;
900
901 if let Some(ref temp_dir) = self.config.temp_dir {
903 fs::create_dir_all(temp_dir).await.with_context(|| {
904 format!("Failed to create temp directory: {}", temp_dir.display())
905 })?;
906 }
907
908 fs::write(&temp_path, b"")
910 .await
911 .with_context(|| format!("Failed to create temp file: {}", temp_path.display()))?;
912
913 Ok(temp_path)
914 }
915
916 pub async fn cleanup_temp_files(&self, max_age: Duration) -> Result<u64> {
918 let Some(temp_dir) = &self.config.temp_dir else {
919 return Ok(0); };
921
922 let mut entries = fs::read_dir(temp_dir)
923 .await
924 .with_context(|| format!("Failed to read temp directory: {}", temp_dir.display()))?;
925
926 let mut cleaned_count = 0u64;
927 let cutoff_time = std::time::SystemTime::now() - max_age;
928
929 while let Some(entry) = entries.next_entry().await.with_context(|| {
930 format!(
931 "Failed to read temp directory entry in: {}",
932 temp_dir.display()
933 )
934 })? {
935 let entry_path = entry.path();
936
937 if let Ok(metadata) = entry.metadata().await {
938 if let Ok(modified) = metadata.modified() {
939 if modified < cutoff_time {
940 if let Err(e) = fs::remove_file(&entry_path).await {
941 tracing::warn!(
942 "Failed to remove temp file {}: {}",
943 entry_path.display(),
944 e
945 );
946 } else {
947 cleaned_count += 1;
948 }
949 }
950 }
951 }
952 }
953
954 Ok(cleaned_count)
955 }
956
957 pub async fn watch_path(&mut self, path: impl AsRef<Path>, recursive: bool) -> Result<()> {
959 if !self.config.enable_watching {
960 return Err(Error::new(
961 crate::error::ErrorKind::File {
962 path: Some(path.as_ref().display().to_string()),
963 operation: FileOperation::Watch,
964 },
965 "File watching is disabled in configuration",
966 ));
967 }
968
969 if self.watcher.is_none() {
970 self.watcher = Some(FileWatcher::new()?);
971 }
972
973 if let Some(ref mut watcher) = self.watcher {
974 watcher.watch_path(path, recursive).await?;
975 }
976
977 Ok(())
978 }
979
980 pub async fn unwatch_path(&mut self, path: impl AsRef<Path>) -> Result<()> {
982 if let Some(ref mut watcher) = self.watcher {
983 watcher.unwatch_path(path).await?;
984 }
985 Ok(())
986 }
987
988 pub fn subscribe_to_changes(&self) -> Option<broadcast::Receiver<FileChangeEvent>> {
990 self.watcher.as_ref().map(|w| w.subscribe())
991 }
992
993 pub async fn compress_file(
995 &self,
996 source: impl AsRef<Path>,
997 destination: impl AsRef<Path>,
998 ) -> Result<()> {
999 if !self.config.enable_compression {
1000 return Err(Error::new(
1001 crate::error::ErrorKind::File {
1002 path: Some(source.as_ref().display().to_string()),
1003 operation: FileOperation::Compress,
1004 },
1005 "File compression is disabled in configuration",
1006 ));
1007 }
1008
1009 let source_data = self.read_file(source).await?;
1011 let compressed_data = crate::utils_general::compression::compress_gzip(&source_data)?;
1012 self.write_file(destination, &compressed_data, None).await?;
1013
1014 Ok(())
1015 }
1016
1017 pub async fn decompress_file(
1019 &self,
1020 source: impl AsRef<Path>,
1021 destination: impl AsRef<Path>,
1022 ) -> Result<()> {
1023 let compressed_data = self.read_file(source).await?;
1024 let decompressed_data =
1025 crate::utils_general::compression::decompress_gzip(&compressed_data)?;
1026 self.write_file(destination, &decompressed_data, None)
1027 .await?;
1028
1029 Ok(())
1030 }
1031
1032 pub async fn get_active_operations(&self) -> Vec<FileOperationProgress> {
1034 self.operations
1035 .read()
1036 .await
1037 .values()
1038 .filter(|op| op.status == FileOperationStatus::InProgress)
1039 .cloned()
1040 .collect()
1041 }
1042
1043 pub async fn get_temp_usage(&self) -> Result<(u64, usize)> {
1045 let _temp_dir = &self.config.temp_dir;
1046
1047 Ok((0, 0))
1050 }
1051}
1052
1053#[async_trait]
1054impl Manager for FileManager {
1055 fn name(&self) -> &str {
1056 "file_manager"
1057 }
1058
1059 fn id(&self) -> Uuid {
1060 Uuid::new_v4() }
1062
1063 async fn initialize(&mut self) -> Result<()> {
1064 self.state
1065 .set_state(crate::manager::ManagerState::Initializing)
1066 .await;
1067
1068 if let Some(ref dir) = self.config.temp_dir {
1074 fs::create_dir_all(dir)
1075 .await
1076 .with_context(|| format!("Failed to create temp directory: {}", dir.display()))?;
1077 }
1078
1079 if self.config.enable_watching {
1081 self.watcher = Some(FileWatcher::new()?);
1082 }
1083
1084 self.state
1085 .set_state(crate::manager::ManagerState::Running)
1086 .await;
1087 Ok(())
1088 }
1089
1090 async fn shutdown(&mut self) -> Result<()> {
1091 self.state
1092 .set_state(crate::manager::ManagerState::ShuttingDown)
1093 .await;
1094
1095 self.watcher = None;
1097
1098 let _ = self.cleanup_temp_files(Duration::from_secs(0)).await;
1100
1101 self.state
1102 .set_state(crate::manager::ManagerState::Shutdown)
1103 .await;
1104 Ok(())
1105 }
1106
1107 async fn status(&self) -> ManagerStatus {
1108 let mut status = self.state.status().await;
1109
1110 let temp_dir_display = self
1111 .config
1112 .temp_dir
1113 .as_ref()
1114 .map(|p| p.display().to_string())
1115 .unwrap_or_else(|| "<none>".to_string());
1116
1117 status.add_metadata("temp_dir", serde_json::Value::String(temp_dir_display));
1118 status.add_metadata(
1119 "watching_enabled",
1120 serde_json::Value::Bool(self.config.enable_watching),
1121 );
1122 status.add_metadata(
1123 "compression_enabled",
1124 serde_json::Value::Bool(self.config.enable_compression),
1125 );
1126 status.add_metadata(
1127 "max_file_size",
1128 serde_json::Value::from(self.config.max_file_size),
1129 );
1130
1131 let active_ops = self.get_active_operations().await;
1132 status.add_metadata(
1133 "active_operations",
1134 serde_json::Value::from(active_ops.len()),
1135 );
1136
1137 if let Ok((usage_bytes, file_count)) = self.get_temp_usage().await {
1138 status.add_metadata("temp_usage_bytes", serde_json::Value::from(usage_bytes));
1139 status.add_metadata("temp_file_count", serde_json::Value::from(file_count));
1140 }
1141
1142 status
1143 }
1144}
1145
1146pub async fn calculate_file_hash(path: impl AsRef<Path>) -> Result<String> {
1148 let mut file = fs::File::open(path.as_ref()).await.with_context(|| {
1149 format!(
1150 "Failed to open file for hashing: {}",
1151 path.as_ref().display()
1152 )
1153 })?;
1154
1155 let mut hasher = Sha256::new();
1156 let mut buffer = vec![0u8; 8192]; loop {
1159 let bytes_read = file.read(&mut buffer).await.with_context(|| {
1160 format!(
1161 "Failed to read file for hashing: {}",
1162 path.as_ref().display()
1163 )
1164 })?;
1165
1166 if bytes_read == 0 {
1167 break;
1168 }
1169
1170 hasher.update(&buffer[..bytes_read]);
1171 }
1172
1173 let hash = hasher.finalize();
1174 Ok(format!("{:x}", hash))
1175}
1176
1177pub fn sanitize_filename(filename: &str) -> String {
1179 let invalid_chars = ['<', '>', ':', '"', '/', '\\', '|', '?', '*'];
1180 let mut sanitized = String::new();
1181
1182 for ch in filename.chars() {
1183 if invalid_chars.contains(&ch) || ch.is_control() {
1184 sanitized.push('_');
1185 } else {
1186 sanitized.push(ch);
1187 }
1188 }
1189
1190 sanitized.trim_end_matches(['.', ' ']).to_string()
1192}
1193
1194pub fn get_file_extension(path: &Path) -> Option<String> {
1196 path.extension()
1197 .and_then(|ext| ext.to_str())
1198 .map(|s| s.to_lowercase())
1199}
1200
1201fn canonicalization_error(path: &Path, op: FileOperation, source: &std::io::Error) -> Error {
1202 Error::new(
1203 crate::error::ErrorKind::File {
1204 path: Some(path.display().to_string()),
1205 operation: op,
1206 },
1207 format!("Failed to canonicalize {}: {}", path.display(), source),
1208 )
1209}
1210
1211pub fn safe_path_join(base: &Path, relative: &Path) -> Result<PathBuf> {
1213 let joined = base.join(relative);
1214
1215 let canonical_base = base
1216 .canonicalize()
1217 .map_err(|e| canonicalization_error(base, FileOperation::Read, &e))?;
1218
1219 let canonical_joined = joined
1220 .canonicalize()
1221 .map_err(|e| canonicalization_error(&joined, FileOperation::Read, &e))?;
1222
1223 if !canonical_joined.starts_with(&canonical_base) {
1224 return Err(Error::new(
1225 crate::error::ErrorKind::File {
1226 path: Some(joined.display().to_string()),
1227 operation: FileOperation::Read,
1228 },
1229 "Path traversal detected",
1230 ));
1231 }
1232
1233 Ok(joined)
1234}
1235
1236#[cfg(test)]
1237mod tests {
1238 use super::*;
1239 use tempfile::TempDir;
1240
1241 #[tokio::test]
1242 async fn test_file_manager_creation() {
1243 let config = FileConfig::default();
1244 let manager = FileManager::new(config);
1245 assert!(manager.operations.read().await.is_empty());
1246 }
1247
1248 #[tokio::test]
1249 async fn test_file_operations() {
1250 let temp_dir = TempDir::new().unwrap();
1251 let mut config = FileConfig::default();
1252 config.temp_dir = Some(temp_dir.path().to_path_buf());
1253
1254 let mut manager = FileManager::new(config);
1255 manager.initialize().await.unwrap();
1256
1257 let test_file = temp_dir.path().join("test.txt");
1258 let test_data = b"Hello, World!";
1259
1260 manager
1262 .write_file(&test_file, test_data, None)
1263 .await
1264 .unwrap();
1265 assert!(test_file.exists());
1266
1267 let read_data = manager.read_file(&test_file).await.unwrap();
1269 assert_eq!(read_data, test_data);
1270
1271 let content = manager.read_file_to_string(&test_file).await.unwrap();
1273 assert_eq!(content, "Hello, World!");
1274
1275 let metadata = manager.get_metadata(&test_file).await.unwrap();
1277 assert_eq!(metadata.size, test_data.len() as u64);
1278 assert_eq!(metadata.file_type, FileType::Text);
1279
1280 let copy_file = temp_dir.path().join("test_copy.txt");
1282 let bytes_copied = manager
1283 .copy_file(&test_file, ©_file, None)
1284 .await
1285 .unwrap();
1286 assert_eq!(bytes_copied, test_data.len() as u64);
1287 assert!(copy_file.exists());
1288
1289 let move_file = temp_dir.path().join("test_moved.txt");
1291 manager
1292 .move_file(©_file, &move_file, None)
1293 .await
1294 .unwrap();
1295 assert!(!copy_file.exists());
1296 assert!(move_file.exists());
1297
1298 manager.delete_file(&test_file).await.unwrap();
1300 assert!(!test_file.exists());
1301
1302 manager.shutdown().await.unwrap();
1303 }
1304
1305 #[tokio::test]
1306 async fn test_directory_operations() {
1307 let temp_dir = TempDir::new().unwrap();
1308 let mut config = FileConfig::default();
1309 config.temp_dir = Some(temp_dir.path().to_path_buf());
1310
1311 let mut manager = FileManager::new(config);
1312 manager.initialize().await.unwrap();
1313
1314 let test_dir = temp_dir.path().join("test_directory");
1315
1316 manager.create_directory(&test_dir, false).await.unwrap();
1318 assert!(test_dir.exists());
1319 assert!(test_dir.is_dir());
1320
1321 let test_file = test_dir.join("file.txt");
1323 manager.write_file(&test_file, b"test", None).await.unwrap();
1324
1325 let entries = manager.list_directory(&test_dir).await.unwrap();
1327 assert_eq!(entries.len(), 1);
1328 assert_eq!(entries[0].path.file_name(), test_file.file_name());
1329
1330 manager.delete_directory(&test_dir, true).await.unwrap();
1332 assert!(!test_dir.exists());
1333
1334 manager.shutdown().await.unwrap();
1335 }
1336
1337 #[test]
1338 fn test_file_type_detection() {
1339 assert_eq!(
1340 FileType::from_extension(Path::new("test.txt")),
1341 FileType::Text
1342 );
1343 assert_eq!(
1344 FileType::from_extension(Path::new("image.png")),
1345 FileType::Image
1346 );
1347 assert_eq!(
1348 FileType::from_extension(Path::new("video.mp4")),
1349 FileType::Video
1350 );
1351 assert_eq!(
1352 FileType::from_extension(Path::new("unknown.xyz")),
1353 FileType::Unknown
1354 );
1355 }
1356
1357 #[test]
1358 fn test_filename_sanitization() {
1359 assert_eq!(sanitize_filename("normal_file.txt"), "normal_file.txt");
1360 assert_eq!(
1361 sanitize_filename("file<with>bad:chars"),
1362 "file_with_bad_chars"
1363 );
1364 assert_eq!(sanitize_filename("file..."), "file");
1365 assert_eq!(sanitize_filename("file "), "file");
1366 }
1367
1368 #[tokio::test]
1369 async fn test_file_hash_calculation() {
1370 let temp_dir = TempDir::new().unwrap();
1371 let test_file = temp_dir.path().join("hash_test.txt");
1372 let test_data = b"Hello, World!";
1373
1374 fs::write(&test_file, test_data).await.unwrap();
1375 let hash = calculate_file_hash(&test_file).await.unwrap();
1376
1377 assert_eq!(
1379 hash,
1380 "dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f"
1381 );
1382 }
1383
1384 #[test]
1385 fn test_safe_path_join() {
1386 let base = Path::new("/safe/base");
1387
1388 let relative = Path::new("subdir/file.txt");
1390 let _malicious = Path::new("../../../etc/passwd");
1394 assert!(base.join(relative).to_string_lossy().contains("subdir"));
1398 }
1399}