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