1use 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
27pub 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct StorageBackupManifestFileV1 {
37 pub path: String,
39 pub size: u64,
41 pub sha256: String,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct StorageBackupManifestV1 {
48 pub format: String,
50 pub name: String,
52 pub created_at_ms: u64,
54 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 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 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 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 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}