ras_filesystem/infrastructure/
local_filesystem.rs1use 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, ¤t)
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}