Skip to main content

ras_filesystem/infrastructure/
local_filesystem.rs

1use std::path::PathBuf;
2
3use async_trait::async_trait;
4use ras_errors::AppError;
5
6use crate::application::csv_normalize::normalize_csv;
7use crate::application::filename_validator::parse_filename;
8use crate::domain::file::{FileExtension, FileSystemFile};
9use crate::domain::repository::{FileSummary, FileSystemPort, FileSystemState};
10
11#[derive(Debug)]
12pub struct LocalFileSystem {
13    root: PathBuf,
14}
15
16impl LocalFileSystem {
17    pub fn new(root: PathBuf) -> Result<Self, AppError> {
18        std::fs::create_dir_all(&root)
19            .map_err(|e| AppError::InternalError(format!("mkdir: {e}")))?;
20        Ok(Self { root })
21    }
22
23    fn path_for(&self, name: &str) -> Result<PathBuf, AppError> {
24        let _ = parse_filename(name)?;
25        Ok(self.root.join(name))
26    }
27}
28
29#[async_trait]
30impl FileSystemPort for LocalFileSystem {
31    async fn read(&self, name: &str) -> Result<FileSystemFile, AppError> {
32        let (_, ext) = parse_filename(name)?;
33        let path = self.path_for(name)?;
34        let bytes = tokio::fs::read(&path)
35            .await
36            .map_err(|e| AppError::NotFound(format!("read {name}: {e}")))?;
37        Ok(FileSystemFile {
38            name: name.into(),
39            extension: ext,
40            bytes,
41        })
42    }
43
44    async fn write(&self, file: FileSystemFile) -> Result<(), AppError> {
45        let path = self.path_for(&file.name)?;
46        let mut bytes = file.bytes;
47        if file.extension == FileExtension::Csv {
48            let s = String::from_utf8_lossy(&bytes).to_string();
49            bytes = normalize_csv(&s).into_bytes();
50        }
51        tokio::fs::write(&path, &bytes)
52            .await
53            .map_err(|e| AppError::InternalError(format!("write {}: {e}", file.name)))?;
54        Ok(())
55    }
56
57    async fn append(&self, name: &str, content: &str) -> Result<(), AppError> {
58        let path = self.path_for(name)?;
59        let mut current = tokio::fs::read(&path).await.unwrap_or_default();
60        current.extend_from_slice(content.as_bytes());
61        tokio::fs::write(&path, &current)
62            .await
63            .map_err(|e| AppError::InternalError(format!("append {name}: {e}")))?;
64        Ok(())
65    }
66
67    async fn list(&self) -> Result<Vec<FileSummary>, AppError> {
68        let mut out = Vec::new();
69        let mut rd = tokio::fs::read_dir(&self.root)
70            .await
71            .map_err(|e| AppError::InternalError(format!("read_dir: {e}")))?;
72        while let Some(entry) = rd
73            .next_entry()
74            .await
75            .map_err(|e| AppError::InternalError(format!("read_dir entry: {e}")))?
76        {
77            let name = entry.file_name().to_string_lossy().to_string();
78            let Ok((_, ext)) = parse_filename(&name) else {
79                continue;
80            };
81            let meta = entry
82                .metadata()
83                .await
84                .map_err(|e| AppError::InternalError(format!("metadata: {e}")))?;
85            out.push(FileSummary {
86                name,
87                extension: ext,
88                size_bytes: meta.len(),
89            });
90        }
91        Ok(out)
92    }
93
94    async fn snapshot(&self) -> Result<FileSystemState, AppError> {
95        Ok(FileSystemState {
96            root: self.root.clone(),
97            files: self.list().await?,
98        })
99    }
100}