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