use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
pub const NETWORK_FILESYSTEMS: &[&str] = &[
"nfs",
"nfs4",
"cifs",
"smb3",
"smbfs",
"afs",
"9p",
"ceph",
"glusterfs",
"fuse.sshfs",
"fuse.rclone",
"fuse.s3fs",
"fuse.davfs",
"fuse.gcsfuse",
"fuse.juicefs",
"davfs",
"ftpfs",
"autofs",
];
const MEMORY_FILESYSTEMS: &[&str] = &["tmpfs", "ramfs", "devtmpfs"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Locality {
Local,
Memory,
Network,
Object,
Unknown,
}
impl Locality {
pub fn of_fstype(fstype: &str) -> Locality {
match fstype {
"s3" | "s3a" | "gs" | "gcs" | "az" | "cloud" | "http" | "https" => Locality::Object,
"" | "unknown" => Locality::Unknown,
other => classify(other),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Source {
pub fstype: String,
pub locality: Locality,
}
impl Source {
pub fn label(&self) -> &str {
&self.fstype
}
pub fn network(&self) -> bool {
self.locality == Locality::Network
}
pub fn from_fstype(fstype: &str) -> Self {
if let Some(scheme) = ["s3", "gs", "http", "https", "az", "hdfs"]
.into_iter()
.find(|s| *s == fstype)
{
return Self {
fstype: scheme.to_string(),
locality: Locality::Object,
};
}
Self {
locality: classify(fstype),
fstype: fstype.to_string(),
}
}
}
const MOUNTS_TTL: Duration = Duration::from_secs(5);
static CACHED_MOUNTS: Mutex<Option<(Instant, Arc<Mounts>)>> = Mutex::new(None);
#[derive(Debug, Clone, Default)]
pub struct Mounts {
entries: Vec<(String, String)>,
}
impl Mounts {
pub fn current() -> Self {
std::fs::read_to_string("/proc/self/mountinfo")
.map(|s| Self::parse(&s))
.unwrap_or_default()
}
pub fn cached() -> Arc<Self> {
let now = Instant::now();
let mut slot = CACHED_MOUNTS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some((read_at, mounts)) = slot.as_ref()
&& now.duration_since(*read_at) < MOUNTS_TTL
{
return Arc::clone(mounts);
}
let mounts = Arc::new(Self::current());
*slot = Some((now, Arc::clone(&mounts)));
mounts
}
pub fn parse(mountinfo: &str) -> Self {
let mut entries = Vec::new();
for line in mountinfo.lines() {
let Some((before, after)) = line.split_once(" - ") else {
continue;
};
let Some(point) = before.split_whitespace().nth(4) else {
continue;
};
let Some(fstype) = after.split_whitespace().next() else {
continue;
};
entries.push((point.to_string(), fstype.to_string()));
}
Self { entries }
}
pub fn fstype_for(&self, path: &Path) -> Option<&str> {
self.covering(path).map(|(_, fstype)| fstype)
}
pub fn mount_point_for(&self, path: &Path) -> std::path::PathBuf {
self.covering(path)
.map(|(point, _)| std::path::PathBuf::from(point))
.unwrap_or_default()
}
fn covering(&self, path: &Path) -> Option<(&str, &str)> {
let joined;
let path = if path.has_root() {
path
} else {
match std::env::current_dir() {
Ok(cwd) => {
joined = cwd.join(path);
&joined
}
Err(_) => path,
}
};
let mut best: Option<(&str, &str)> = None;
for (point, fstype) in &self.entries {
if !path.starts_with(point) {
continue;
}
if best.is_none_or(|(at, _)| point.len() >= at.len()) {
best = Some((point, fstype));
}
}
best
}
pub fn describe(&self, path: &Path) -> Source {
if let Some(scheme) = object_scheme(path) {
return Source {
fstype: scheme,
locality: Locality::Object,
};
}
match self.fstype_for(path) {
Some(fstype) => Source {
locality: classify(fstype),
fstype: fstype.to_string(),
},
None => Source {
fstype: "unknown".to_string(),
locality: Locality::Unknown,
},
}
}
pub fn is_network(&self, path: &Path) -> bool {
self.describe(path).network()
}
pub fn could_block(&self, path: &Path) -> bool {
let source = self.describe(path);
source.network() || source.fstype.starts_with("fuse.")
}
}
fn classify(fstype: &str) -> Locality {
if NETWORK_FILESYSTEMS.contains(&fstype) {
Locality::Network
} else if MEMORY_FILESYSTEMS.contains(&fstype) {
Locality::Memory
} else {
Locality::Local
}
}
pub fn object_scheme(path: &Path) -> Option<String> {
match crate::cloud::source::input_source(path) {
crate::cloud::source::InputSource::Local(_) => None,
crate::cloud::source::InputSource::S3(_) => Some("s3".to_string()),
crate::cloud::source::InputSource::Gcs(_) => Some("gs".to_string()),
crate::cloud::source::InputSource::Azure(_) => Some("az".to_string()),
crate::cloud::source::InputSource::Http(_) => Some("http".to_string()),
}
}
#[cfg(test)]
mod locality_of_fstype_tests {
use super::*;
#[test]
fn object_store_schemes_are_not_local_disks() {
for scheme in ["s3", "s3a", "gs", "gcs", "http", "https"] {
assert_eq!(
Locality::of_fstype(scheme),
Locality::Object,
"{scheme} should be an object store"
);
}
}
#[test]
fn network_filesystems_are_network() {
for fstype in ["nfs", "nfs4", "cifs", "smb3"] {
assert_eq!(
Locality::of_fstype(fstype),
Locality::Network,
"{fstype} should be network"
);
}
}
#[test]
fn memory_filesystems_are_memory() {
assert_eq!(Locality::of_fstype("tmpfs"), Locality::Memory);
}
#[test]
fn ordinary_filesystems_are_local() {
for fstype in ["ext4", "btrfs", "xfs", "apfs", "ntfs"] {
assert_eq!(
Locality::of_fstype(fstype),
Locality::Local,
"{fstype} should be local"
);
}
}
#[test]
fn nothing_known_is_not_guessed_at() {
assert_eq!(Locality::of_fstype(""), Locality::Unknown);
assert_eq!(Locality::of_fstype("unknown"), Locality::Unknown);
}
#[test]
fn it_agrees_with_describe() {
let mounts = Mounts::parse("");
for path in ["s3://bucket/key.parquet", "gs://bucket/key.parquet"] {
let source = mounts.describe(std::path::Path::new(path));
assert_eq!(source.locality, Locality::of_fstype(&source.fstype));
}
}
}