Skip to main content

appcore_storage/
storage_file.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: storage_file.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/06/04 12:02:44 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/02 12:07:11 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11use super::storage_backup_list::list_backup_descriptors;
12use super::storage_file_fs::{
13    create_real_directory_all, open_regular_file, path_exists_no_follow, path_is_real_directory,
14    resolve_under_root, tmp_path_for, write_atomic_file,
15};
16use super::storage_tree::{bounded_tree_entries, StorageTreeEntryKind};
17use super::*;
18use crate::storage::RemoteAuthStorageClient;
19use appcore_security::{TokenClaims, TokenProvider};
20use std::fs;
21use std::io::Read;
22use std::path::{Path, PathBuf};
23
24/// Single-process local file provider with atomic replacement writes.
25#[derive(Debug, Clone)]
26pub struct FileStorageProvider {
27    pub(super) storage_path: PathBuf,
28    pub(super) backup_path: PathBuf,
29    opened: bool,
30}
31
32impl FileStorageProvider {
33    /// Creates a provider with separate data and backup roots.
34    pub fn new(storage_path: impl Into<PathBuf>, backup_path: impl Into<PathBuf>) -> Self {
35        Self {
36            storage_path: storage_path.into(),
37            backup_path: backup_path.into(),
38            opened: false,
39        }
40    }
41
42    /// Creates the configured data and backup directories.
43    pub fn create_dirs(&self) -> StorageResult<()> {
44        if let Some(parent) = self.storage_path.parent() {
45            fs::create_dir_all(parent).map_err(|_| StorageError::NotAvailable)?;
46        }
47        create_real_directory_all(&self.backup_path)?;
48        if path_exists_no_follow(&self.storage_path)? {
49            super::storage_file_fs::ensure_real_directory(&self.storage_path)?;
50        }
51        self.recover_snapshot_restore()?;
52        create_real_directory_all(&self.storage_path)?;
53        Ok(())
54    }
55
56    /// Atomically writes bytes below the data root.
57    pub fn write_bytes(&self, path: &str, bytes: &[u8]) -> StorageResult<()> {
58        self.write_bytes_atomic(path, bytes)
59    }
60
61    /// Writes through an exclusive temporary file, fsync and atomic rename.
62    pub fn write_bytes_atomic(&self, path: &str, bytes: &[u8]) -> StorageResult<()> {
63        self.resolve_storage(path)?;
64        self.with_storage_lock(|| {
65            let full = self.resolve_storage(path)?;
66            if let Some(parent) = full.parent() {
67                fs::create_dir_all(parent).map_err(|_| StorageError::NotAvailable)?;
68            }
69            let full = self.resolve_storage(path)?;
70            let tmp = tmp_path_for(&full);
71            write_atomic_file(&tmp, &full, bytes)
72                .map_err(|_| StorageError::TransactionFailed(path.to_string()))
73        })?;
74        Ok(())
75    }
76
77    /// Seals and atomically writes bytes using an explicit token provider.
78    pub fn write_secure_bytes<P: TokenProvider>(
79        &self,
80        path: &str,
81        bytes: &[u8],
82        provider: &P,
83        claims: &TokenClaims,
84    ) -> StorageResult<()> {
85        let sealed = provider
86            .seal(bytes, claims)
87            .map_err(|_| StorageError::SecurityFailed(path.to_string()))?;
88        self.write_bytes_atomic(path, &sealed)
89    }
90
91    /// Writes sealed bytes and fails when authentication is not configured.
92    pub fn write_auth_required_bytes<P: TokenProvider>(
93        &self,
94        path: &str,
95        bytes: &[u8],
96        provider: Option<&P>,
97        claims: &TokenClaims,
98    ) -> StorageResult<()> {
99        let provider = require_auth_provider(path, provider)?;
100        self.write_secure_bytes(path, bytes, provider, claims)
101    }
102
103    /// Seals bytes through a remote authentication service before writing.
104    pub fn write_remote_auth_required_bytes(
105        &self,
106        path: &str,
107        bytes: &[u8],
108        client: Option<&RemoteAuthStorageClient>,
109    ) -> StorageResult<()> {
110        let client = require_remote_auth_client(path, client)?;
111        let sealed = client.seal_resource(path, bytes)?;
112        self.write_bytes_atomic(path, &sealed)
113    }
114
115    /// Reads bytes below the data root.
116    pub fn read_bytes(&self, path: &str) -> StorageResult<Vec<u8>> {
117        self.resolve_storage(path)?;
118        self.with_storage_lock(|| {
119            let full = self.resolve_storage(path)?;
120            let mut file = open_regular_file(&full)
121                .map_err(|_| StorageError::RepositoryNotFound(path.to_string()))?;
122            let mut bytes = Vec::new();
123            file.read_to_end(&mut bytes)
124                .map_err(|_| StorageError::RepositoryNotFound(path.to_string()))?;
125            Ok(bytes)
126        })
127    }
128
129    pub(super) fn read_bytes_bounded(&self, path: &str, max_bytes: u64) -> StorageResult<Vec<u8>> {
130        self.resolve_storage(path)?;
131        self.with_storage_lock(|| {
132            let full = self.resolve_storage(path)?;
133            let file = open_regular_file(&full)
134                .map_err(|_| StorageError::RepositoryNotFound(path.to_string()))?;
135            let metadata = file
136                .metadata()
137                .map_err(|_| StorageError::RepositoryNotFound(path.to_string()))?;
138            if metadata.len() > max_bytes {
139                return Err(StorageError::TransactionFailed(path.to_string()));
140            }
141            let capacity = usize::try_from(metadata.len())
142                .map_err(|_| StorageError::TransactionFailed(path.to_string()))?;
143            let read_limit = max_bytes
144                .checked_add(1)
145                .ok_or_else(|| StorageError::TransactionFailed(path.to_string()))?;
146            let mut bytes = Vec::with_capacity(capacity);
147            file.take(read_limit)
148                .read_to_end(&mut bytes)
149                .map_err(|_| StorageError::RepositoryNotFound(path.to_string()))?;
150            if bytes.len() as u64 > max_bytes {
151                return Err(StorageError::TransactionFailed(path.to_string()));
152            }
153            Ok(bytes)
154        })
155    }
156
157    /// Reads and opens sealed bytes using an explicit token provider.
158    pub fn read_secure_bytes<P: TokenProvider>(
159        &self,
160        path: &str,
161        provider: &P,
162        claims: &TokenClaims,
163    ) -> StorageResult<Vec<u8>> {
164        let sealed = self.read_bytes(path)?;
165        provider
166            .open(&sealed, claims)
167            .map_err(|_| StorageError::SecurityFailed(path.to_string()))
168    }
169
170    /// Reads sealed bytes and fails when authentication is not configured.
171    pub fn read_auth_required_bytes<P: TokenProvider>(
172        &self,
173        path: &str,
174        provider: Option<&P>,
175        claims: &TokenClaims,
176    ) -> StorageResult<Vec<u8>> {
177        let provider = require_auth_provider(path, provider)?;
178        self.read_secure_bytes(path, provider, claims)
179    }
180
181    /// Reads and opens bytes through a remote authentication service.
182    pub fn read_remote_auth_required_bytes(
183        &self,
184        path: &str,
185        client: Option<&RemoteAuthStorageClient>,
186    ) -> StorageResult<Vec<u8>> {
187        let client = require_remote_auth_client(path, client)?;
188        let sealed = self.read_bytes(path)?;
189        client.open_resource(path, &sealed)
190    }
191
192    /// Reports whether a relative data path exists.
193    pub fn exists(&self, path: &str) -> StorageResult<bool> {
194        self.resolve_storage(path)?;
195        self.with_storage_lock(|| {
196            let full = self.resolve_storage(path)?;
197            path_exists_no_follow(&full)
198        })
199    }
200
201    /// Atomically copies a data file below the backup root.
202    pub fn backup_file(&self, source: &str, backup_name: &str) -> StorageResult<()> {
203        self.backup_file_atomic(source, backup_name)
204    }
205
206    /// Creates a backup through fsync and atomic replacement.
207    pub fn backup_file_atomic(&self, source: &str, backup_name: &str) -> StorageResult<()> {
208        self.resolve_storage(source)?;
209        self.resolve_backup(backup_name)?;
210        self.with_storage_lock(|| {
211            let source_full = self.resolve_storage(source)?;
212            if !path_exists_no_follow(&source_full)? {
213                return Err(StorageError::RepositoryNotFound(source.to_string()));
214            }
215            let backup_full = self.resolve_backup(backup_name)?;
216            if let Some(parent) = backup_full.parent() {
217                fs::create_dir_all(parent)
218                    .map_err(|_| StorageError::BackupFailed(backup_name.to_string()))?;
219            }
220            let backup_full = self.resolve_backup(backup_name)?;
221            let mut source_file = open_regular_file(&source_full)
222                .map_err(|_| StorageError::BackupFailed(backup_name.to_string()))?;
223            let mut bytes = Vec::new();
224            source_file
225                .read_to_end(&mut bytes)
226                .map_err(|_| StorageError::BackupFailed(backup_name.to_string()))?;
227            let tmp = tmp_path_for(&backup_full);
228            write_atomic_file(&tmp, &backup_full, &bytes)
229                .map_err(|_| StorageError::BackupFailed(backup_name.to_string()))
230        })?;
231        Ok(())
232    }
233
234    /// Removes orphaned temporary files below data and backup roots.
235    pub fn cleanup_temp_files(&self) -> StorageResult<usize> {
236        self.with_storage_lock(|| {
237            let mut removed = cleanup_tmp_in_dir(&self.storage_path)?;
238            removed += cleanup_tmp_in_dir(&self.backup_path)?;
239            Ok(removed)
240        })
241    }
242
243    fn resolve_storage(&self, relative: &str) -> StorageResult<PathBuf> {
244        resolve_under_root(&self.storage_path, relative)
245    }
246
247    fn resolve_backup(&self, relative: &str) -> StorageResult<PathBuf> {
248        resolve_under_root(&self.backup_path, relative)
249    }
250}
251
252fn require_auth_provider<'a, P: TokenProvider>(
253    path: &str,
254    provider: Option<&'a P>,
255) -> StorageResult<&'a P> {
256    provider.ok_or_else(|| StorageError::AuthUnavailable(path.to_string()))
257}
258
259fn require_remote_auth_client<'a>(
260    path: &str,
261    client: Option<&'a RemoteAuthStorageClient>,
262) -> StorageResult<&'a RemoteAuthStorageClient> {
263    client.ok_or_else(|| StorageError::AuthUnavailable(path.to_string()))
264}
265
266fn cleanup_tmp_in_dir(root: &Path) -> StorageResult<usize> {
267    if !path_exists_no_follow(root)? {
268        return Ok(0);
269    }
270    let mut removed = 0usize;
271    for entry in bounded_tree_entries(root)? {
272        if entry.kind == StorageTreeEntryKind::File
273            && entry
274                .path
275                .file_name()
276                .map(|name| name.to_string_lossy().ends_with(".tmp"))
277                .unwrap_or(false)
278        {
279            fs::remove_file(entry.path).map_err(|_| StorageError::NotAvailable)?;
280            removed += 1;
281        }
282    }
283    Ok(removed)
284}
285
286fn count_tmp_in_dir(root: &Path) -> StorageResult<usize> {
287    if !path_exists_no_follow(root)? {
288        return Ok(0);
289    }
290    let mut count = 0usize;
291    for entry in bounded_tree_entries(root)? {
292        if entry.kind == StorageTreeEntryKind::File
293            && entry
294                .path
295                .file_name()
296                .map(|name| name.to_string_lossy().ends_with(".tmp"))
297                .unwrap_or(false)
298        {
299            count += 1;
300        }
301    }
302    Ok(count)
303}
304
305impl StorageProvider for FileStorageProvider {
306    fn status(&self) -> StorageStatus {
307        if path_is_real_directory(&self.storage_path) && path_is_real_directory(&self.backup_path) {
308            StorageStatus::Online
309        } else {
310            StorageStatus::Offline
311        }
312    }
313
314    fn health(&self) -> StorageHealth {
315        if path_is_real_directory(&self.storage_path) && path_is_real_directory(&self.backup_path) {
316            let orphan_tmp = match (
317                count_tmp_in_dir(&self.storage_path),
318                count_tmp_in_dir(&self.backup_path),
319            ) {
320                (Ok(storage), Ok(backups)) => storage.saturating_add(backups),
321                _ => {
322                    return StorageHealth {
323                        status: StorageStatus::Degraded,
324                        message: Some("temporary file scan failed".to_string()),
325                    };
326                }
327            };
328            if orphan_tmp > 0 {
329                return StorageHealth {
330                    status: StorageStatus::Degraded,
331                    message: Some(format!("found {orphan_tmp} orphan temp files")),
332                };
333            }
334            return StorageHealth {
335                status: StorageStatus::Online,
336                message: None,
337            };
338        }
339        StorageHealth {
340            status: StorageStatus::Degraded,
341            message: Some("storage or backup path is missing".to_string()),
342        }
343    }
344
345    fn open(&mut self) -> StorageResult<()> {
346        self.create_dirs()?;
347        self.opened = true;
348        Ok(())
349    }
350
351    fn close(&mut self) -> StorageResult<()> {
352        self.opened = false;
353        Ok(())
354    }
355
356    fn begin_transaction(&mut self) -> StorageResult<Box<dyn Transaction>> {
357        if !self.opened {
358            return Err(StorageError::NotAvailable);
359        }
360        Err(StorageError::TransactionsUnsupported)
361    }
362
363    fn list_backups(&self) -> Vec<BackupDescriptor> {
364        self.with_storage_lock(|| Ok(list_backup_descriptors(&self.backup_path)))
365            .unwrap_or_default()
366    }
367}