Skip to main content

oxirs_samm/
cloud_backends_impl.rs

1//! Backend implementations for the SAMM cloud storage subsystem.
2//!
3//! Provides concrete [`CloudStorageBackend`] implementations for:
4//! - AWS S3 and S3-compatible stores ([`S3Backend`])
5//! - Google Cloud Storage ([`GcsBackend`])
6//! - Azure Blob Storage ([`AzureBlobBackend`])
7//! - Generic HTTP REST backend ([`HttpBackend`])
8//! - Local filesystem adapter ([`LocalFsBackend`])
9//!
10//! All backends use async/await with `reqwest` (rustls TLS — no OpenSSL).
11
12// Re-export the per-provider backend structs and their configs so that callers
13// can `use oxirs_samm::cloud_backends_impl::*`.
14pub 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
25// ──────────────────────────────────────────────────────────────────────────────
26// LocalFsBackend — filesystem adapter (useful for testing and air-gapped
27// deployments)
28// ──────────────────────────────────────────────────────────────────────────────
29
30/// A local-filesystem storage backend.
31///
32/// Stores objects as files under a configurable root directory.  The object
33/// `key` is mapped directly to a path beneath the root, so keys containing
34/// `/` become subdirectories.
35///
36/// This backend is **synchronous internally** but presents the async
37/// [`CloudStorageBackend`] interface by wrapping I/O with
38/// `tokio::task::spawn_blocking`.
39pub struct LocalFsBackend {
40    root: PathBuf,
41    /// In-memory "object index" so that list() does not have to walk the
42    /// directory tree on every call (keys are relative to `root`).
43    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    /// Create a new `LocalFsBackend` rooted at `root`.
56    ///
57    /// The directory is created if it does not already exist.
58    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        // Strip any leading slash so that key="/foo" maps to root/foo.
70        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}