use crate::cstr_from_bytes_until_null;
use dashmap::{DashMap, DashSet};
use std::ffi::CString;
use std::mem::MaybeUninit;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use std::{fs, io};
use tempfile::{NamedTempFile, TempDir};
const TEMPDIR_PREFIX: &str = "applesauce_tmp";
const TEMPFILE_PREFIX: &str = "applesauce_tmp";
const ZFS_SUBTYPE: u32 = u32::from_be_bytes(*b"ZFS\0");
#[derive(Debug)]
enum VolumeInfo {
NoCompression,
SupportsCompression { temp_dir: Option<TempDir> },
}
#[derive(Debug)]
pub struct Volumes {
infos: DashMap<u64, VolumeInfo>,
tmp_dirs: DashSet<Box<Path>>,
}
impl Default for Volumes {
fn default() -> Self {
Self::new()
}
}
fn vol_with_file_supports_compression(path: &Path) -> io::Result<bool> {
let path_cstr = CString::new(path.as_os_str().as_bytes())?;
let mut statfs_buf = MaybeUninit::<libc::statfs>::uninit();
let rc = unsafe { libc::statfs(path_cstr.as_ptr(), statfs_buf.as_mut_ptr()) };
if rc != 0 {
return Err(io::Error::last_os_error());
}
let statfs_buf = unsafe { statfs_buf.assume_init_ref() };
if statfs_buf.f_fssubtype == ZFS_SUBTYPE {
return Ok(false);
}
let root_path = cstr_from_bytes_until_null(&statfs_buf.f_mntonname)
.ok_or_else(|| io::Error::other("failed to get root path from statfs"))?;
crate::vol_supports_compression_cap(root_path)
}
fn system_volinfo() -> Option<(u64, VolumeInfo)> {
let system = match TempDir::with_prefix(TEMPDIR_PREFIX) {
Ok(system) => system,
Err(e) => {
tracing::warn!("failed to create temp dir in system temp dir: {e}");
return None;
}
};
let system_metadata = match system.path().metadata() {
Ok(system_metadata) => system_metadata,
Err(e) => {
tracing::warn!("failed to get metadata for system temp dir: {e}");
return None;
}
};
let compression_support = vol_with_file_supports_compression(system.path()).unwrap_or(false);
let volume_info = if compression_support {
VolumeInfo::SupportsCompression {
temp_dir: Some(system),
}
} else {
VolumeInfo::NoCompression
};
Some((system_metadata.dev(), volume_info))
}
impl Volumes {
pub fn new() -> Self {
let infos = DashMap::new();
let tmp_dirs = DashSet::new();
if let Some((dev, vol_info)) = system_volinfo() {
if let VolumeInfo::SupportsCompression {
temp_dir: Some(dir),
} = &vol_info
{
tmp_dirs.insert(dir.path().into());
}
infos.insert(dev, vol_info);
}
Self { infos, tmp_dirs }
}
fn get_or_insert(
&self,
path: &Path,
metadata: &fs::Metadata,
) -> io::Result<dashmap::mapref::one::Ref<'_, u64, VolumeInfo>> {
let device = metadata.dev();
if let Some(vol_info) = self.infos.get(&device) {
return Ok(vol_info);
}
self.infos
.entry(device)
.or_try_insert_with(|| {
let tempdir_parent = if metadata.is_dir() {
path
} else {
let parent = path
.parent()
.ok_or_else(|| io::Error::other("no parent of file on volume"))?;
if parent.metadata()?.dev() != device {
return Err(io::Error::other(
"file on volume has different device than parent",
));
}
parent
};
let compression_support = vol_with_file_supports_compression(path)?;
let volume_info = if compression_support {
let temp_dir = TempDir::with_prefix_in(TEMPDIR_PREFIX, tempdir_parent)?;
self.tmp_dirs.insert(temp_dir.path().into());
VolumeInfo::SupportsCompression {
temp_dir: Some(temp_dir),
}
} else {
VolumeInfo::NoCompression
};
Ok(volume_info)
})
.map(|ref_mut| ref_mut.downgrade())
}
pub fn supports_compression(&self, path: &Path, metadata: &fs::Metadata) -> io::Result<bool> {
let current = self.get_or_insert(path, metadata)?;
Ok(matches!(
current.value(),
VolumeInfo::SupportsCompression { .. }
))
}
pub fn add_root_dir(&self, path: &Path, metadata: &fs::Metadata) -> io::Result<()> {
self.get_or_insert(path, metadata)?;
Ok(())
}
pub fn is_temp_dir(&self, path: &Path) -> bool {
self.tmp_dirs.contains(path)
}
pub fn tempfile_for(&self, path: &Path, metadata: &fs::Metadata) -> io::Result<NamedTempFile> {
let current = self.get_or_insert(path, metadata)?;
match current.value() {
VolumeInfo::NoCompression => {
Err(io::Error::other("volume does not support compression"))
}
VolumeInfo::SupportsCompression { temp_dir } => {
let dir = match temp_dir {
Some(dir) => dir.path(),
None => return Err(io::Error::other("no temp dir for volume")),
};
let mut builder = tempfile::Builder::new();
builder.prefix(TEMPFILE_PREFIX);
if let Some(file_name) = path.file_name() {
builder.suffix(file_name);
}
builder.tempfile_in(dir)
}
}
}
}