use std::path::{Path, PathBuf};
#[cfg(feature = "tokio")]
use std::io::Read;
#[cfg(feature = "tokio")]
use encoding_rs_io::DecodeReaderBytes;
use tempfile::NamedTempFile;
use tracing::warn;
pub use crate::locked_file::*;
pub use crate::path::*;
pub mod cachedir;
pub mod link;
mod locked_file;
mod path;
pub mod which;
pub fn is_same_file_allow_missing(left: &Path, right: &Path) -> Option<bool> {
if left == right {
return Some(true);
}
if let Ok(value) = same_file::is_same_file(left, right) {
return Some(value);
}
if let (Some(left_parent), Some(right_parent), Some(left_name), Some(right_name)) = (
left.parent(),
right.parent(),
left.file_name(),
right.file_name(),
) {
match same_file::is_same_file(left_parent, right_parent) {
Ok(true) => return Some(left_name == right_name),
Ok(false) => return Some(false),
_ => (),
}
}
None
}
#[cfg(feature = "tokio")]
pub async fn read_to_string_transcode(path: impl AsRef<Path>) -> std::io::Result<String> {
let path = path.as_ref();
let raw = if path == Path::new("-") {
let mut buf = Vec::with_capacity(1024);
std::io::stdin().read_to_end(&mut buf)?;
buf
} else {
fs_err::tokio::read(path).await?
};
let mut buf = String::with_capacity(1024);
DecodeReaderBytes::new(&*raw)
.read_to_string(&mut buf)
.map_err(|err| {
let path = path.display();
std::io::Error::other(format!("failed to decode file {path}: {err}"))
})?;
Ok(buf)
}
#[cfg(windows)]
pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
if src.as_ref().is_file() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"Cannot create a junction for {}: is not a directory",
src.as_ref().display()
),
));
}
match junction::delete(dunce::simplified(dst.as_ref())) {
Ok(()) => match fs_err::remove_dir_all(dst.as_ref()) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err),
},
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err),
}
junction::create(
dunce::simplified(src.as_ref()),
dunce::simplified(dst.as_ref()),
)
}
#[cfg(unix)]
pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
match fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref()) {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
let temp_dir = tempfile::tempdir_in(dst.as_ref().parent().unwrap())?;
let temp_file = temp_dir.path().join("link");
fs_err::os::unix::fs::symlink(src, &temp_file)?;
fs_err::rename(&temp_file, dst.as_ref())?;
Ok(())
}
Err(err) => Err(err),
}
}
#[cfg(windows)]
pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
if src.as_ref().is_file() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"Cannot create a junction for {}: is not a directory",
src.as_ref().display()
),
));
}
junction::create(
dunce::simplified(src.as_ref()),
dunce::simplified(dst.as_ref()),
)
}
#[cfg(unix)]
pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())
}
#[cfg(unix)]
pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
fs_err::remove_file(path.as_ref())
}
pub fn symlink_or_copy_file(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
#[cfg(windows)]
{
fs_err::copy(src.as_ref(), dst.as_ref())?;
}
#[cfg(unix)]
{
fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())?;
}
Ok(())
}
#[cfg(windows)]
pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
match junction::delete(dunce::simplified(path.as_ref())) {
Ok(()) => match fs_err::remove_dir_all(path.as_ref()) {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(err),
},
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(err),
}
}
#[cfg(unix)]
pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
use std::os::unix::fs::PermissionsExt;
tempfile::Builder::new()
.permissions(std::fs::Permissions::from_mode(0o666))
.tempfile_in(path)
}
#[cfg(not(unix))]
pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
tempfile::Builder::new().tempfile_in(path)
}
#[cfg(feature = "tokio")]
pub async fn write_atomic(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
let temp_file = tempfile_in(
path.as_ref()
.parent()
.expect("Write path must have a parent"),
)?;
fs_err::tokio::write(&temp_file, &data).await?;
persist_with_retry(temp_file, path.as_ref()).await
}
pub fn write_atomic_sync(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
let temp_file = tempfile_in(
path.as_ref()
.parent()
.expect("Write path must have a parent"),
)?;
fs_err::write(&temp_file, &data)?;
persist_with_retry_sync(temp_file, path.as_ref())
}
pub fn copy_atomic_sync(from: impl AsRef<Path>, to: impl AsRef<Path>) -> std::io::Result<()> {
let temp_file = tempfile_in(to.as_ref().parent().expect("Write path must have a parent"))?;
fs_err::copy(from.as_ref(), &temp_file)?;
persist_with_retry_sync(temp_file, to.as_ref())
}
#[cfg(windows)]
fn backoff_file_move() -> backon::ExponentialBackoff {
use backon::BackoffBuilder;
backon::ExponentialBuilder::default()
.with_min_delay(std::time::Duration::from_millis(10))
.with_max_times(10)
.build()
}
#[cfg(feature = "tokio")]
pub async fn rename_with_retry(
from: impl AsRef<Path>,
to: impl AsRef<Path>,
) -> Result<(), std::io::Error> {
#[cfg(windows)]
{
use backon::Retryable;
let from = from.as_ref();
let to = to.as_ref();
let rename = async || fs_err::rename(from, to);
rename
.retry(backoff_file_move())
.sleep(tokio::time::sleep)
.when(|e| e.kind() == std::io::ErrorKind::PermissionDenied)
.notify(|err, _dur| {
warn!(
"Retrying rename from {} to {} due to transient error: {}",
from.display(),
to.display(),
err
);
})
.await
}
#[cfg(not(windows))]
{
fs_err::tokio::rename(from, to).await
}
}
#[cfg_attr(not(windows), allow(unused_variables))]
pub fn with_retry_sync(
from: impl AsRef<Path>,
to: impl AsRef<Path>,
operation_name: &str,
operation: impl Fn() -> Result<(), std::io::Error>,
) -> Result<(), std::io::Error> {
#[cfg(windows)]
{
use backon::BlockingRetryable;
let from = from.as_ref();
let to = to.as_ref();
operation
.retry(backoff_file_move())
.sleep(std::thread::sleep)
.when(|err| err.kind() == std::io::ErrorKind::PermissionDenied)
.notify(|err, _dur| {
warn!(
"Retrying {} from {} to {} due to transient error: {}",
operation_name,
from.display(),
to.display(),
err
);
})
.call()
.map_err(|err| {
std::io::Error::other(format!(
"Failed {} {} to {}: {}",
operation_name,
from.display(),
to.display(),
err
))
})
}
#[cfg(not(windows))]
{
operation()
}
}
#[cfg(windows)]
enum PersistRetryError {
Persist(String),
LostState,
}
#[cfg(feature = "tokio")]
pub async fn persist_with_retry(
from: NamedTempFile,
to: impl AsRef<Path>,
) -> Result<(), std::io::Error> {
#[cfg(windows)]
{
use backon::Retryable;
let to = to.as_ref();
let from = std::sync::Arc::new(std::sync::Mutex::new(Some(from)));
let persist = || {
let from2 = from.clone();
async move {
let maybe_file: Option<NamedTempFile> = from2
.lock()
.map_err(|_| PersistRetryError::LostState)?
.take();
if let Some(file) = maybe_file {
file.persist(to).map_err(|err| {
let error_message: String = err.to_string();
if let Ok(mut guard) = from2.lock() {
*guard = Some(err.file);
PersistRetryError::Persist(error_message)
} else {
PersistRetryError::LostState
}
})
} else {
Err(PersistRetryError::LostState)
}
}
};
let persisted = persist
.retry(backoff_file_move())
.sleep(tokio::time::sleep)
.when(|err| matches!(err, PersistRetryError::Persist(_)))
.notify(|err, _dur| {
if let PersistRetryError::Persist(error_message) = err {
warn!(
"Retrying to persist temporary file to {}: {}",
to.display(),
error_message,
);
}
})
.await;
match persisted {
Ok(_) => Ok(()),
Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
"Failed to persist temporary file to {}: {}",
to.display(),
error_message,
))),
Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
"Failed to retrieve temporary file while trying to persist to {}",
to.display()
))),
}
}
#[cfg(not(windows))]
{
async { fs_err::rename(from, to) }.await
}
}
pub fn persist_with_retry_sync(
from: NamedTempFile,
to: impl AsRef<Path>,
) -> Result<(), std::io::Error> {
#[cfg(windows)]
{
use backon::BlockingRetryable;
let to = to.as_ref();
let mut from = Some(from);
let persist = || {
if let Some(file) = from.take() {
file.persist(to).map_err(|err| {
let error_message = err.to_string();
from = Some(err.file);
PersistRetryError::Persist(error_message)
})
} else {
Err(PersistRetryError::LostState)
}
};
let persisted = persist
.retry(backoff_file_move())
.sleep(std::thread::sleep)
.when(|err| matches!(err, PersistRetryError::Persist(_)))
.notify(|err, _dur| {
if let PersistRetryError::Persist(error_message) = err {
warn!(
"Retrying to persist temporary file to {}: {}",
to.display(),
error_message,
);
}
})
.call();
match persisted {
Ok(_) => Ok(()),
Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
"Failed to persist temporary file to {}: {}",
to.display(),
error_message,
))),
Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
"Failed to retrieve temporary file while trying to persist to {}",
to.display()
))),
}
}
#[cfg(not(windows))]
{
fs_err::rename(from, to)
}
}
pub fn directories(
path: impl AsRef<Path>,
) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
let entries = match path.as_ref().read_dir() {
Ok(entries) => Some(entries),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
Err(err) => return Err(err),
};
Ok(entries
.into_iter()
.flatten()
.filter_map(|entry| match entry {
Ok(entry) => Some(entry),
Err(err) => {
warn!("Failed to read entry: {err}");
None
}
})
.filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_dir()))
.map(|entry| entry.path()))
}
pub fn entries(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
let entries = match path.as_ref().read_dir() {
Ok(entries) => Some(entries),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
Err(err) => return Err(err),
};
Ok(entries
.into_iter()
.flatten()
.filter_map(|entry| match entry {
Ok(entry) => Some(entry),
Err(err) => {
warn!("Failed to read entry: {err}");
None
}
})
.map(|entry| entry.path()))
}
pub fn files(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
let entries = match path.as_ref().read_dir() {
Ok(entries) => Some(entries),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
Err(err) => return Err(err),
};
Ok(entries
.into_iter()
.flatten()
.filter_map(|entry| match entry {
Ok(entry) => Some(entry),
Err(err) => {
warn!("Failed to read entry: {err}");
None
}
})
.filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_file()))
.map(|entry| entry.path()))
}
pub fn is_temporary(path: impl AsRef<Path>) -> bool {
path.as_ref()
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with(".tmp"))
}
pub fn is_virtualenv_executable(executable: impl AsRef<Path>) -> bool {
executable
.as_ref()
.parent()
.and_then(Path::parent)
.is_some_and(is_virtualenv_base)
}
pub fn is_virtualenv_base(path: impl AsRef<Path>) -> bool {
path.as_ref().join("pyvenv.cfg").is_file()
}
fn is_known_already_locked_error(err: &std::fs::TryLockError) -> bool {
match err {
std::fs::TryLockError::WouldBlock => true,
std::fs::TryLockError::Error(err) => {
if cfg!(windows) && err.raw_os_error() == Some(33) {
return true;
}
false
}
}
}
#[cfg(feature = "tokio")]
pub struct ProgressReader<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> {
reader: Reader,
callback: Callback,
}
#[cfg(feature = "tokio")]
impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin>
ProgressReader<Reader, Callback>
{
pub fn new(reader: Reader, callback: Callback) -> Self {
Self { reader, callback }
}
}
#[cfg(feature = "tokio")]
impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> tokio::io::AsyncRead
for ProgressReader<Reader, Callback>
{
fn poll_read(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::pin::Pin::new(&mut self.as_mut().reader)
.poll_read(cx, buf)
.map_ok(|()| {
(self.callback)(buf.filled().len());
})
}
}
pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
fs_err::create_dir_all(&dst)?;
for entry in fs_err::read_dir(src.as_ref())? {
let entry = entry?;
let ty = entry.file_type()?;
if ty.is_dir() {
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
} else {
fs_err::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
}
}
Ok(())
}