use std::borrow::Cow;
use std::fs;
use std::path::{Path, PathBuf};
use tempfile::NamedTempFile;
use anyhow::{bail, ensure, Context, Result};
use tracing::warn;
use crate::{task::Mode, utils::to_human_readable_size};
pub(super) fn get_combined_file_size<P: AsRef<Path>, I: Iterator<Item = P>>(
files: I,
) -> Result<u64> {
let mut total = 0;
for f in files {
total += fs::metadata(f)?.len();
}
Ok(total)
}
fn get_available_space<P: AsRef<Path>>(path: P) -> Result<u64> {
let path = Path::new(path.as_ref());
ensure!(
path.is_absolute(),
"Available space can only be checked for absolute paths (provided path {:?})",
path
);
#[cfg(not(windows))]
{
let stat = rustix::fs::statvfs(path)?;
Ok(stat.f_frsize * stat.f_bavail)
}
#[cfg(windows)]
{
use sysinfo::Disks;
let disks = Disks::new_with_refreshed_list();
Ok(disks
.iter()
.find(|disk| {
disk.mount_point()
.canonicalize()
.map_or(false, |mount_point| path.starts_with(mount_point))
})
.context("Unable to find device for the given path")?
.available_space())
}
}
pub(super) fn check_writeable(path: impl AsRef<Path>, mode: Mode) -> Result<()> {
match NamedTempFile::new_in(path.as_ref()) {
Ok(marker_file) => {
marker_file.close()?;
}
Err(_) => {
let msg = format!(
"Destination directory '{}' is not writeable.",
path.as_ref().to_string_lossy()
);
if !matches!(mode, Mode::Force) {
bail!(msg);
}
warn!("{} Operation will most likely fail.", msg);
}
}
Ok(())
}
pub(super) fn check_space(required_space: u64, dest: impl AsRef<Path>, mode: Mode) -> Result<()> {
let Ok(available_space) = get_available_space(&dest) else {
warn!(
"Unable to check available space on the device for path {}.",
dest.as_ref().display()
);
return Ok(());
};
if available_space < required_space {
let msg = format!(
"No space left on device. Required: {}, available: {}.",
to_human_readable_size(required_space),
to_human_readable_size(available_space),
);
if !matches!(mode, Mode::Force) {
bail!(msg);
}
warn!("{} Operation will most likely fail.", msg);
}
Ok(())
}
pub(super) fn get_common_path<P: AsRef<Path>>(paths: &[P]) -> Result<PathBuf> {
let mut abs_paths = paths
.iter()
.map(|p| {
let p = p.as_ref();
p.is_absolute()
.then_some(p)
.with_context(|| format!("Path {p:?} is not absolute"))
})
.collect::<Result<Vec<_>, _>>()?;
abs_paths.sort();
let first = abs_paths.first().context("No files found")?.components();
let last = abs_paths.last().context("No files found")?.components();
Ok(first
.zip(last)
.take_while(|(x, y)| x == y)
.map(|(x, _)| x.as_os_str().to_string_lossy().into_owned())
.collect())
}
pub(super) fn to_posix_path(p: &Path) -> Option<Cow<'_, str>> {
#[cfg(not(windows))]
{
p.to_str().map(Cow::Borrowed)
}
#[cfg(windows)]
{
use std::path::Component;
let components = p
.components()
.map(|c| match c {
Component::RootDir => Some(""),
Component::CurDir => Some("."),
Component::ParentDir => Some(".."),
Component::Normal(s) => s.to_str(),
Component::Prefix(prefix) => prefix.as_os_str().to_str(),
})
.collect::<Option<Vec<_>>>()?;
Some(Cow::Owned(
if components.len() == 1 && components[0].is_empty() {
String::from("/")
} else {
components.join("/")
},
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_common_path() -> Result<()> {
let empty: [&Path; 0] = [];
assert!(get_common_path(&empty).is_err());
assert!(get_common_path(&["foo/bar"]).is_err());
#[cfg(windows)]
macro_rules! os_prefix {
($path:expr) => {{
["C:", $path].join("")
}};
}
#[cfg(not(windows))]
macro_rules! os_prefix {
($path:expr) => {{
$path
}};
}
assert_eq!(
get_common_path(&[
os_prefix!("/home/chuck/foo/data.json"),
os_prefix!("/home/chuck/bar/secret.csv"),
os_prefix!("/tmp/test.txt")
])?,
PathBuf::from(os_prefix!("/"))
);
assert_eq!(
get_common_path(&[
os_prefix!("/home/chuck/data/log/foo/2022-02-24T09-09-24.txt"),
os_prefix!("/home/chuck/data/log/foo/log.1"),
os_prefix!("/home/chuck/data/log/foo/log.7"),
os_prefix!("/home/chuck/data/log/foo/baz/2022-02-24T09-05-43.txt"),
os_prefix!("/home/chuck/data/log/foo/log.3"),
os_prefix!("/home/chuck/data/log/foo/log.6"),
os_prefix!("/home/chuck/data/log/foo/2022-02-24T09-07-37.txt"),
os_prefix!("/home/chuck/data/log/bar/baz/log.1"),
os_prefix!("/home/chuck/data/log/foo/2022-02-24T09-07-00.txt"),
os_prefix!("/home/chuck/data/log/bar/baz/log.2"),
])?,
PathBuf::from(os_prefix!("/home/chuck/data/log"))
);
Ok(())
}
#[test]
fn test_get_available_space() -> Result<()> {
let available_space = get_available_space(std::env::temp_dir().canonicalize()?)?;
assert!(available_space > 0);
Ok(())
}
#[test]
fn test_to_posix_path() -> Result<()> {
for p in [
"foo/bar/baz",
"/",
"/foo/bar",
"./foo",
"../foo",
"./foo/../bar",
] {
assert_eq!(to_posix_path(Path::new(p)), Some(Cow::Borrowed(p)));
}
Ok(())
}
#[test]
#[cfg(windows)]
fn test_to_posix_path_windows() -> Result<()> {
for row in [
[r"foo\bar\baz", "foo/bar/baz"],
[r"\", "/"],
[r"\foo\bar", "/foo/bar"],
[r".\foo", "./foo"],
[r"..\foo", "../foo"],
[r".\foo\..\bar", "./foo/../bar"],
] {
assert_eq!(
to_posix_path(Path::new(row[0])),
Some(Cow::Borrowed(row[1]))
);
}
Ok(())
}
}