1use 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
29pub 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;
35pub const MAX_STORAGE_SNAPSHOT_BYTES: u64 = 16 * 1024 * 1024 * 1024;
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct StorageBackupManifestFileV1 {
41 pub path: String,
43 pub size: u64,
45 pub sha256: String,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub struct StorageBackupManifestV1 {
52 pub format: String,
54 pub name: String,
56 pub created_at_ms: u64,
58 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 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 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 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 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}