Skip to main content

cranpose_services/
storage.rs

1//! Free space on the device, so an application can tell the person before a
2//! large download or save that it will not fit.
3
4use std::{path::Path, sync::Arc};
5
6use crate::registry::ServiceRegistry;
7
8/// Reports the free space a platform measures its own way.
9pub trait StorageSpace: Send + Sync {
10    /// The bytes the application can still write on the volume that holds
11    /// `path`, or `None` where the platform cannot tell.
12    fn available(&self, path: &Path) -> Option<u64>;
13}
14
15/// Shared handle to a [`StorageSpace`].
16pub type StorageSpaceRef = Arc<dyn StorageSpace>;
17
18static PLATFORM_STORAGE_SPACE: ServiceRegistry<dyn StorageSpace> = ServiceRegistry::new();
19
20/// Installs the platform's measure of free space. iOS installs one that counts
21/// the space the system frees on demand for important data.
22// forwards on purpose: the registry is private to this crate, and this is the
23// one way a platform backend installs into it.
24pub fn set_platform_storage_space(space: StorageSpaceRef) {
25    PLATFORM_STORAGE_SPACE.set(space);
26}
27
28/// The bytes the application can still write on the volume that holds `path`,
29/// or `None` where the platform cannot tell. `path` need not exist yet: the
30/// nearest folder above it that does is measured.
31pub fn available_storage(path: &Path) -> Option<u64> {
32    let existing = path.ancestors().find(|folder| folder.exists())?;
33    match PLATFORM_STORAGE_SPACE.get() {
34        Some(platform) => platform.available(existing),
35        None => file_system_available(existing),
36    }
37}
38
39#[cfg(unix)]
40fn file_system_available(path: &Path) -> Option<u64> {
41    let stat = rustix::fs::statvfs(path).ok()?;
42    Some(stat.f_bavail.saturating_mul(stat.f_frsize))
43}
44
45#[cfg(not(unix))]
46fn file_system_available(_path: &Path) -> Option<u64> {
47    None
48}
49
50#[cfg(test)]
51#[path = "tests/storage_tests.rs"]
52mod tests;