use std::borrow::Cow;
use std::fs;
use std::path::{Path, PathBuf};
use tempfile::NamedTempFile;
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, std::io::Error> {
files.map(|f| Ok(fs::metadata(f)?.len())).sum()
}
fn get_available_space<P: AsRef<Path>>(path: P) -> Result<u64, std::io::Error> {
let path = Path::new(path.as_ref());
if !path.is_absolute() {
return Err(std::io::Error::other(format!(
"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))
})
.ok_or_else(|| std::io::Error::other("Unable to find device for the given path"))?
.available_space())
}
}
pub(super) fn check_writeable(path: impl AsRef<Path>, mode: Mode) -> Result<(), std::io::Error> {
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) {
return Err(std::io::Error::new(
std::io::ErrorKind::ReadOnlyFilesystem,
msg,
));
}
tracing::warn!("{} Operation will most likely fail.", msg);
}
}
Ok(())
}
pub(super) fn check_space(
required_space: u64,
dest: impl AsRef<Path>,
mode: Mode,
) -> Result<(), std::io::Error> {
let Ok(available_space) = get_available_space(&dest) else {
tracing::warn!(
"Unable to check available space on the device for path {}.",
dest.as_ref().display()
);
return Ok(());
};
tracing::trace!(
available = available_space,
required = required_space,
"available storage check"
);
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) {
return Err(std::io::Error::new(std::io::ErrorKind::StorageFull, msg));
}
tracing::warn!("{} Operation will most likely fail.", msg);
}
Ok(())
}
pub(super) fn get_common_path<P: AsRef<Path>>(paths: &[P]) -> Result<PathBuf, std::io::Error> {
let mut abs_paths = paths
.iter()
.map(|p| {
let p = p.as_ref();
p.is_absolute()
.then_some(p)
.ok_or_else(|| std::io::Error::other(format!("Path {p:?} is not absolute")))
})
.collect::<Result<Vec<_>, _>>()?;
abs_paths.sort();
let (Some(first), Some(last)) = (abs_paths.first(), abs_paths.last()) else {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"No files found",
));
};
Ok(first
.components()
.zip(last.components())
.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() {
let empty: [&Path; 0] = [];
assert!(get_common_path(&empty).is_err());
assert!(get_common_path(&["foo/bar"]).is_err());
#[cfg(windows)]
fn os_prefix(path: &str) -> String {
["C:", path].join("")
}
#[cfg(not(windows))]
fn os_prefix(path: &str) -> &str {
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")
])
.unwrap(),
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"),
])
.unwrap(),
PathBuf::from(os_prefix("/home/chuck/data/log"))
);
}
#[test]
fn test_get_available_space() {
let available_space =
get_available_space(std::env::temp_dir().canonicalize().unwrap()).unwrap();
assert!(available_space > 0);
}
#[test]
fn test_to_posix_path() {
for p in [
"foo/bar/baz",
"/",
"/foo/bar",
"./foo",
"../foo",
"./foo/../bar",
] {
assert_eq!(to_posix_path(Path::new(p)), Some(Cow::Borrowed(p)));
}
}
#[test]
#[cfg(windows)]
fn test_to_posix_path_windows() {
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]))
);
}
}
}