Skip to main content

appcore_storage/
storage_backup.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: storage_backup.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/07/23 23:50:45 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/07/23 23:50:45 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11//! Versioned whole-provider backup, verification, restore, and crash recovery.
12
13use super::storage_file_fs::{
14    create_new_file, ensure_real_directory, fsync_parent, open_lock_file, open_regular_file,
15    path_exists_no_follow, resolve_under_root, sync_directory, tmp_path_for, write_atomic_file,
16};
17use super::storage_tree::{bounded_tree_entries, StorageTreeEntryKind};
18use super::{BackupDescriptor, FileStorageProvider, StorageError, StorageResult};
19use fs2::FileExt;
20use serde::{Deserialize, Serialize};
21use sha2::{Digest, Sha256};
22use std::fs::{self, File};
23use std::io::{self, Read};
24use std::path::{Component, Path, PathBuf};
25use std::time::{SystemTime, UNIX_EPOCH};
26
27/// Stable format identifier for complete local storage snapshots.
28pub const STORAGE_BACKUP_FORMAT_V1: &str = "appcore-storage-backup-v1";
29pub(super) const BACKUP_MANIFEST: &str = "manifest.json";
30const BACKUP_DATA: &str = "data";
31const MAX_BACKUP_FILES: usize = 100_000;
32const MAX_BACKUP_MANIFEST_BYTES: u64 = 16 * 1024 * 1024;
33
34/// One file recorded by a V1 storage backup manifest.
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct StorageBackupManifestFileV1 {
37    /// Slash-separated path relative to the provider data root.
38    pub path: String,
39    /// File size in bytes.
40    pub size: u64,
41    /// Lowercase SHA-256 digest of the complete file.
42    pub sha256: String,
43}
44
45/// Durable manifest for one complete V1 storage snapshot.
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct StorageBackupManifestV1 {
48    /// Stable persisted format identifier.
49    pub format: String,
50    /// Backup name selected by the operator.
51    pub name: String,
52    /// Creation timestamp in Unix milliseconds.
53    pub created_at_ms: u64,
54    /// Complete sorted file inventory.
55    pub files: Vec<StorageBackupManifestFileV1>,
56}
57
58struct StorageOperationLock {
59    file: File,
60}
61
62impl Drop for StorageOperationLock {
63    fn drop(&mut self) {
64        let _ = FileExt::unlock(&self.file);
65    }
66}
67
68impl FileStorageProvider {
69    /// Creates an atomic, checksummed snapshot of the complete data root.
70    pub fn create_snapshot_backup(&self, name: &str) -> StorageResult<BackupDescriptor> {
71        self.create_dirs()?;
72        self.with_storage_lock(|| self.create_snapshot_locked(name))
73    }
74
75    /// Verifies a complete snapshot without changing current data.
76    pub fn verify_snapshot_backup(&self, name: &str) -> StorageResult<BackupDescriptor> {
77        self.with_storage_lock(|| {
78            let (_, manifest) = self.load_verified_backup(name)?;
79            Ok(descriptor(&manifest))
80        })
81    }
82
83    /// Restores a verified snapshot through a recoverable directory swap.
84    pub fn restore_snapshot_backup(&self, name: &str) -> StorageResult<BackupDescriptor> {
85        self.create_dirs()?;
86        self.with_storage_lock(|| self.restore_snapshot_locked(name))
87    }
88
89    /// Resolves interrupted restore phases without discarding the last good root.
90    pub fn recover_snapshot_restore(&self) -> StorageResult<()> {
91        self.with_storage_lock(|| self.recover_restore_locked())
92    }
93
94    pub(super) fn with_storage_lock<T>(
95        &self,
96        operation: impl FnOnce() -> StorageResult<T>,
97    ) -> StorageResult<T> {
98        let _lock = self.acquire_operation_lock()?;
99        operation()
100    }
101
102    fn create_snapshot_locked(&self, name: &str) -> StorageResult<BackupDescriptor> {
103        validate_backup_name(name)?;
104        reject_overlapping_roots(&self.storage_path, &self.backup_path)?;
105        let final_path = resolve_under_root(&self.backup_path, name)?;
106        if path_exists_no_follow(&final_path)? {
107            return Err(StorageError::BackupFailed(name.to_string()));
108        }
109        let staging = snapshot_staging_path(&final_path);
110        fs::create_dir(&staging).map_err(|_| StorageError::BackupFailed(name.to_string()))?;
111        ensure_real_directory(&staging)?;
112        let result = self.populate_snapshot(name, &staging);
113        if result.is_err() {
114            let _ = fs::remove_dir_all(&staging);
115        }
116        let manifest = result?;
117        if path_exists_no_follow(&final_path)? {
118            return Err(StorageError::BackupFailed(name.to_string()));
119        }
120        ensure_real_directory(&staging)?;
121        fs::rename(&staging, &final_path)
122            .map_err(|_| StorageError::BackupFailed(name.to_string()))?;
123        fsync_parent(&final_path).map_err(|_| StorageError::BackupFailed(name.to_string()))?;
124        Ok(descriptor(&manifest))
125    }
126
127    fn populate_snapshot(
128        &self,
129        name: &str,
130        staging: &Path,
131    ) -> StorageResult<StorageBackupManifestV1> {
132        let data_root = staging.join(BACKUP_DATA);
133        fs::create_dir(&data_root).map_err(|_| StorageError::BackupFailed(name.to_string()))?;
134        let mut files = Vec::new();
135        collect_regular_files(&self.storage_path, &mut files)?;
136        files.sort();
137        if files.len() > MAX_BACKUP_FILES {
138            return Err(StorageError::BackupFailed(name.to_string()));
139        }
140        let mut entries = Vec::with_capacity(files.len());
141        for relative in files {
142            entries.push(copy_snapshot_file(
143                &self.storage_path,
144                &data_root,
145                &relative,
146            )?);
147        }
148        let manifest = StorageBackupManifestV1 {
149            format: STORAGE_BACKUP_FORMAT_V1.to_string(),
150            name: name.to_string(),
151            created_at_ms: now_ms(),
152            files: entries,
153        };
154        write_manifest(staging, &manifest)?;
155        sync_directory_tree(&data_root)?;
156        sync_directory(staging).map_err(|_| StorageError::BackupFailed(name.to_string()))?;
157        Ok(manifest)
158    }
159
160    fn restore_snapshot_locked(&self, name: &str) -> StorageResult<BackupDescriptor> {
161        self.recover_restore_locked()?;
162        let (backup_root, manifest) = self.load_verified_backup(name)?;
163        let pending = self.restore_pending_path()?;
164        let previous = self.restore_previous_path()?;
165        copy_verified_tree(&backup_root.join(BACKUP_DATA), &pending, &manifest)?;
166        fsync_parent(&pending).map_err(|_| StorageError::BackupFailed(name.to_string()))?;
167        fs::rename(&self.storage_path, &previous)
168            .map_err(|_| StorageError::BackupFailed(name.to_string()))?;
169        fsync_parent(&previous).map_err(|_| StorageError::BackupFailed(name.to_string()))?;
170        if fs::rename(&pending, &self.storage_path).is_err() {
171            let _ = fs::rename(&previous, &self.storage_path);
172            return Err(StorageError::BackupFailed(name.to_string()));
173        }
174        fsync_parent(&self.storage_path)
175            .map_err(|_| StorageError::BackupFailed(name.to_string()))?;
176        fs::remove_dir_all(&previous).map_err(|_| StorageError::BackupFailed(name.to_string()))?;
177        fsync_parent(&self.storage_path)
178            .map_err(|_| StorageError::BackupFailed(name.to_string()))?;
179        Ok(descriptor(&manifest))
180    }
181
182    fn load_verified_backup(
183        &self,
184        name: &str,
185    ) -> StorageResult<(PathBuf, StorageBackupManifestV1)> {
186        validate_backup_name(name)?;
187        let root = resolve_under_root(&self.backup_path, name)?;
188        reject_symlink_tree(&root)?;
189        let manifest = read_manifest(&root, name)?;
190        verify_manifest(name, &root.join(BACKUP_DATA), &manifest)?;
191        Ok((root, manifest))
192    }
193
194    fn recover_restore_locked(&self) -> StorageResult<()> {
195        let pending = self.restore_pending_path()?;
196        let previous = self.restore_previous_path()?;
197        if !real_directory_exists(&self.storage_path)? {
198            if real_directory_exists(&pending)? {
199                fs::rename(&pending, &self.storage_path).map_err(|_| StorageError::NotAvailable)?;
200            } else if real_directory_exists(&previous)? {
201                fs::rename(&previous, &self.storage_path)
202                    .map_err(|_| StorageError::NotAvailable)?;
203            }
204        }
205        if real_directory_exists(&self.storage_path)? && real_directory_exists(&pending)? {
206            fs::remove_dir_all(&pending).map_err(|_| StorageError::NotAvailable)?;
207        }
208        if real_directory_exists(&self.storage_path)? && real_directory_exists(&previous)? {
209            fs::remove_dir_all(&previous).map_err(|_| StorageError::NotAvailable)?;
210        }
211        if real_directory_exists(&self.storage_path)? {
212            fsync_parent(&self.storage_path).map_err(|_| StorageError::NotAvailable)?;
213        }
214        Ok(())
215    }
216
217    fn acquire_operation_lock(&self) -> StorageResult<StorageOperationLock> {
218        let path = sibling_path(&self.storage_path, "lock")?;
219        let file = open_lock_file(&path).map_err(|_| StorageError::NotAvailable)?;
220        file.lock_exclusive()
221            .map_err(|_| StorageError::NotAvailable)?;
222        Ok(StorageOperationLock { file })
223    }
224
225    fn restore_pending_path(&self) -> StorageResult<PathBuf> {
226        sibling_path(&self.storage_path, "restore.pending")
227    }
228
229    fn restore_previous_path(&self) -> StorageResult<PathBuf> {
230        sibling_path(&self.storage_path, "restore.previous")
231    }
232}
233
234fn collect_regular_files(root: &Path, output: &mut Vec<PathBuf>) -> StorageResult<()> {
235    for entry in bounded_tree_entries(root)? {
236        if entry.kind == StorageTreeEntryKind::Link {
237            return Err(StorageError::InvalidPath(entry.path.display().to_string()));
238        }
239        if entry.kind == StorageTreeEntryKind::File {
240            output.push(
241                entry
242                    .path
243                    .strip_prefix(root)
244                    .map_err(|_| StorageError::InvalidPath(entry.path.display().to_string()))?
245                    .to_path_buf(),
246            );
247        }
248    }
249    Ok(())
250}
251
252fn copy_snapshot_file(
253    source_root: &Path,
254    destination_root: &Path,
255    relative: &Path,
256) -> StorageResult<StorageBackupManifestFileV1> {
257    let portable = portable_path(relative)?;
258    let source = resolve_under_root(source_root, &portable)?;
259    let destination = resolve_under_root(destination_root, &portable)?;
260    if let Some(parent) = destination.parent() {
261        fs::create_dir_all(parent).map_err(|_| StorageError::BackupFailed(path_text(relative)))?;
262        ensure_real_directory(parent)?;
263    }
264    copy_and_sync(&source, &destination)?;
265    let (size, sha256) = hash_file(&destination)?;
266    Ok(StorageBackupManifestFileV1 {
267        path: portable,
268        size,
269        sha256,
270    })
271}
272
273fn copy_verified_tree(
274    source_root: &Path,
275    destination_root: &Path,
276    manifest: &StorageBackupManifestV1,
277) -> StorageResult<()> {
278    if path_exists_no_follow(destination_root)? {
279        ensure_real_directory(destination_root)?;
280        fs::remove_dir_all(destination_root).map_err(|_| StorageError::NotAvailable)?;
281    }
282    fs::create_dir(destination_root).map_err(|_| StorageError::NotAvailable)?;
283    for entry in &manifest.files {
284        validated_manifest_path(&entry.path)?;
285        let source = resolve_under_root(source_root, &entry.path)?;
286        let destination = resolve_under_root(destination_root, &entry.path)?;
287        if let Some(parent) = destination.parent() {
288            fs::create_dir_all(parent).map_err(|_| StorageError::NotAvailable)?;
289            ensure_real_directory(parent)?;
290        }
291        copy_and_sync(&source, &destination)?;
292    }
293    sync_directory_tree(destination_root)
294}
295
296fn copy_and_sync(source: &Path, destination: &Path) -> StorageResult<()> {
297    let mut source = open_regular_file(source).map_err(|_| StorageError::NotAvailable)?;
298    let mut destination = create_new_file(destination).map_err(|_| StorageError::NotAvailable)?;
299    io::copy(&mut source, &mut destination).map_err(|_| StorageError::NotAvailable)?;
300    destination
301        .sync_all()
302        .map_err(|_| StorageError::NotAvailable)?;
303    Ok(())
304}
305
306fn write_manifest(root: &Path, manifest: &StorageBackupManifestV1) -> StorageResult<()> {
307    let bytes = serde_json::to_vec_pretty(manifest)
308        .map_err(|_| StorageError::BackupFailed(manifest.name.clone()))?;
309    let path = root.join(BACKUP_MANIFEST);
310    write_atomic_file(&tmp_path_for(&path), &path, &bytes)
311        .map_err(|_| StorageError::BackupFailed(manifest.name.clone()))
312}
313
314pub(super) fn read_manifest(root: &Path, name: &str) -> StorageResult<StorageBackupManifestV1> {
315    let path = root.join(BACKUP_MANIFEST);
316    let mut file =
317        open_regular_file(&path).map_err(|_| StorageError::BackupFailed(name.to_string()))?;
318    let metadata = file
319        .metadata()
320        .map_err(|_| StorageError::BackupFailed(name.to_string()))?;
321    if metadata.len() > MAX_BACKUP_MANIFEST_BYTES {
322        return Err(StorageError::BackupFailed(name.to_string()));
323    }
324    let mut bytes = Vec::with_capacity(metadata.len() as usize);
325    file.read_to_end(&mut bytes)
326        .map_err(|_| StorageError::BackupFailed(name.to_string()))?;
327    serde_json::from_slice(&bytes).map_err(|_| StorageError::BackupFailed(name.to_string()))
328}
329
330fn verify_manifest(
331    name: &str,
332    data_root: &Path,
333    manifest: &StorageBackupManifestV1,
334) -> StorageResult<()> {
335    if manifest.format != STORAGE_BACKUP_FORMAT_V1
336        || manifest.name != name
337        || manifest.files.len() > MAX_BACKUP_FILES
338    {
339        return Err(StorageError::BackupFailed(name.to_string()));
340    }
341    let mut previous = None;
342    for entry in &manifest.files {
343        let relative = validated_manifest_path(&entry.path)?;
344        if previous.as_ref().is_some_and(|path| path >= &entry.path) {
345            return Err(StorageError::BackupFailed(name.to_string()));
346        }
347        let (size, sha256) = hash_file(&data_root.join(&relative))?;
348        if size != entry.size || sha256 != entry.sha256 {
349            return Err(StorageError::BackupFailed(name.to_string()));
350        }
351        previous = Some(entry.path.clone());
352    }
353    let mut actual = Vec::new();
354    collect_regular_files(data_root, &mut actual)?;
355    if actual.len() != manifest.files.len() {
356        return Err(StorageError::BackupFailed(name.to_string()));
357    }
358    Ok(())
359}
360
361fn hash_file(path: &Path) -> StorageResult<(u64, String)> {
362    let mut file = open_regular_file(path).map_err(|_| StorageError::NotAvailable)?;
363    let mut hasher = Sha256::new();
364    let mut buffer = [0u8; 64 * 1024];
365    let mut size = 0u64;
366    loop {
367        let read = file
368            .read(&mut buffer)
369            .map_err(|_| StorageError::NotAvailable)?;
370        if read == 0 {
371            break;
372        }
373        size = size.saturating_add(read as u64);
374        hasher.update(&buffer[..read]);
375    }
376    Ok((size, format!("{:x}", hasher.finalize())))
377}
378
379fn reject_symlink_tree(root: &Path) -> StorageResult<()> {
380    let mut files = Vec::new();
381    collect_regular_files(root, &mut files)
382}
383
384fn reject_overlapping_roots(storage: &Path, backup: &Path) -> StorageResult<()> {
385    let storage = fs::canonicalize(storage).map_err(|_| StorageError::NotAvailable)?;
386    let backup = fs::canonicalize(backup).map_err(|_| StorageError::NotAvailable)?;
387    if storage.starts_with(&backup) || backup.starts_with(&storage) {
388        return Err(StorageError::InvalidPath(backup.display().to_string()));
389    }
390    Ok(())
391}
392
393fn validate_backup_name(name: &str) -> StorageResult<()> {
394    let path = Path::new(name);
395    if name.is_empty() || path.components().count() != 1 || name.starts_with('.') {
396        return Err(StorageError::InvalidPath(name.to_string()));
397    }
398    match path.components().next() {
399        Some(std::path::Component::Normal(_)) => Ok(()),
400        _ => Err(StorageError::InvalidPath(name.to_string())),
401    }
402}
403
404fn validated_manifest_path(path: &str) -> StorageResult<PathBuf> {
405    let relative = Path::new(path);
406    if path.is_empty()
407        || relative.is_absolute()
408        || relative
409            .components()
410            .any(|component| !matches!(component, Component::Normal(_)))
411    {
412        return Err(StorageError::InvalidPath(path.to_string()));
413    }
414    Ok(relative.to_path_buf())
415}
416
417fn portable_path(path: &Path) -> StorageResult<String> {
418    let parts: Option<Vec<_>> = path
419        .components()
420        .map(|component| match component {
421            std::path::Component::Normal(part) => part.to_str(),
422            _ => None,
423        })
424        .collect();
425    parts
426        .map(|parts| parts.join("/"))
427        .filter(|path| !path.is_empty())
428        .ok_or_else(|| StorageError::InvalidPath(path.display().to_string()))
429}
430
431fn sync_directory_tree(root: &Path) -> StorageResult<()> {
432    let mut directories = vec![root.to_path_buf()];
433    for entry in bounded_tree_entries(root)? {
434        match entry.kind {
435            StorageTreeEntryKind::Directory => directories.push(entry.path),
436            StorageTreeEntryKind::Link => {
437                return Err(StorageError::InvalidPath(entry.path.display().to_string()));
438            }
439            StorageTreeEntryKind::File | StorageTreeEntryKind::Other => {}
440        }
441    }
442    for directory in directories.into_iter().rev() {
443        sync_directory(&directory).map_err(|_| StorageError::NotAvailable)?;
444    }
445    Ok(())
446}
447
448fn sibling_path(path: &Path, suffix: &str) -> StorageResult<PathBuf> {
449    let name = path
450        .file_name()
451        .and_then(|name| name.to_str())
452        .ok_or_else(|| StorageError::InvalidPath(path.display().to_string()))?;
453    Ok(path.with_file_name(format!("{name}.{suffix}")))
454}
455
456fn snapshot_staging_path(path: &Path) -> PathBuf {
457    let candidate = tmp_path_for(path);
458    candidate.with_extension("snapshot.tmp")
459}
460
461pub(super) fn descriptor(manifest: &StorageBackupManifestV1) -> BackupDescriptor {
462    BackupDescriptor {
463        name: manifest.name.clone(),
464        created_at_ms: manifest.created_at_ms,
465    }
466}
467
468fn real_directory_exists(path: &Path) -> StorageResult<bool> {
469    if !path_exists_no_follow(path)? {
470        return Ok(false);
471    }
472    ensure_real_directory(path)?;
473    Ok(true)
474}
475
476fn now_ms() -> u64 {
477    SystemTime::now()
478        .duration_since(UNIX_EPOCH)
479        .map(|duration| duration.as_millis() as u64)
480        .unwrap_or(0)
481}
482
483fn path_text(path: &Path) -> String {
484    path.to_string_lossy().into_owned()
485}