oxirs_samm/
cloud_backends_impl.rs1pub use crate::cloud_backends_aws::{S3Backend, S3Config};
15pub use crate::cloud_backends_azure::{AzureBlobBackend, AzureConfig};
16pub use crate::cloud_backends_gcp::{GcsBackend, GcsConfig};
17pub use crate::cloud_backends_http::{HttpBackend, HttpConfig};
18
19use crate::cloud_storage::CloudStorageBackend;
20use async_trait::async_trait;
21use std::collections::HashMap;
22use std::path::PathBuf;
23use std::sync::RwLock;
24
25pub struct LocalFsBackend {
40 root: PathBuf,
41 index: RwLock<HashMap<String, usize>>,
44}
45
46impl std::fmt::Debug for LocalFsBackend {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 f.debug_struct("LocalFsBackend")
49 .field("root", &self.root)
50 .finish()
51 }
52}
53
54impl LocalFsBackend {
55 pub fn new(root: impl Into<PathBuf>) -> std::result::Result<Self, String> {
59 let root = root.into();
60 std::fs::create_dir_all(&root)
61 .map_err(|e| format!("Failed to create LocalFsBackend root {:?}: {e}", root))?;
62 Ok(Self {
63 root,
64 index: RwLock::new(HashMap::new()),
65 })
66 }
67
68 fn path_for(&self, key: &str) -> PathBuf {
69 self.root.join(key.trim_start_matches('/'))
71 }
72
73 fn write_file(&self, key: &str, data: Vec<u8>) -> std::result::Result<(), String> {
74 let path = self.path_for(key);
75 if let Some(parent) = path.parent() {
76 std::fs::create_dir_all(parent)
77 .map_err(|e| format!("Failed to create directory {:?}: {e}", parent))?;
78 }
79 std::fs::write(&path, &data).map_err(|e| format!("Failed to write {:?}: {e}", path))?;
80 let len = data.len();
81 self.index
82 .write()
83 .map_err(|_| "index lock poisoned".to_string())?
84 .insert(key.to_string(), len);
85 Ok(())
86 }
87
88 fn read_file(&self, key: &str) -> std::result::Result<Vec<u8>, String> {
89 let path = self.path_for(key);
90 std::fs::read(&path).map_err(|e| {
91 if e.kind() == std::io::ErrorKind::NotFound {
92 format!("Local object not found: {key}")
93 } else {
94 format!("Failed to read {:?}: {e}", path)
95 }
96 })
97 }
98
99 fn file_exists(&self, key: &str) -> bool {
100 self.path_for(key).is_file()
101 }
102
103 fn delete_file(&self, key: &str) -> std::result::Result<(), String> {
104 let path = self.path_for(key);
105 if path.is_file() {
106 std::fs::remove_file(&path).map_err(|e| format!("Failed to delete {:?}: {e}", path))?;
107 }
108 if let Ok(mut idx) = self.index.write() {
109 idx.remove(key);
110 }
111 Ok(())
112 }
113
114 fn list_prefix(&self, prefix: &str) -> Vec<String> {
115 if let Ok(idx) = self.index.read() {
116 idx.keys()
117 .filter(|k| k.starts_with(prefix))
118 .cloned()
119 .collect()
120 } else {
121 Vec::new()
122 }
123 }
124}
125
126#[async_trait]
127impl CloudStorageBackend for LocalFsBackend {
128 async fn upload(&self, key: &str, data: Vec<u8>) -> std::result::Result<(), String> {
129 self.write_file(key, data)
130 }
131
132 async fn download(&self, key: &str) -> std::result::Result<Vec<u8>, String> {
133 self.read_file(key)
134 }
135
136 async fn exists(&self, key: &str) -> std::result::Result<bool, String> {
137 Ok(self.file_exists(key))
138 }
139
140 async fn delete(&self, key: &str) -> std::result::Result<(), String> {
141 self.delete_file(key)
142 }
143
144 async fn list(&self, prefix: &str) -> std::result::Result<Vec<String>, String> {
145 Ok(self.list_prefix(prefix))
146 }
147}