use crate::error::{FsError, Result};
use crate::tree::{copy_tree, CopyOptions};
use std::fs;
use std::io;
use std::path::{Component, Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum MigrateMethod {
Rename,
CopyThenRemove,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Migrated {
pub method: MigrateMethod,
pub source_removed: bool,
}
pub fn migrate_dir(from: &Path, to: &Path) -> Result<Migrated> {
let meta = fs::symlink_metadata(from).map_err(|e| FsError::io(from, e))?;
if meta.file_type().is_symlink() {
return Err(FsError::Symlink(from.to_path_buf()));
}
if !meta.is_dir() {
return Err(FsError::NotADirectory(from.to_path_buf()));
}
let abs_from = std::path::absolute(from).map_err(|e| FsError::io(from, e))?;
let abs_to = std::path::absolute(to).map_err(|e| FsError::io(to, e))?;
refuse_inside(&abs_from, &abs_to, to)?;
let parent = match abs_to.parent() {
Some(p) => p.to_path_buf(),
None => return Err(FsError::BadName(to.display().to_string())),
};
let name = abs_to
.file_name()
.ok_or_else(|| FsError::BadName(to.display().to_string()))?
.to_string_lossy()
.chars()
.take(100)
.collect::<String>();
fs::create_dir_all(&parent).map_err(|e| FsError::io(&parent, e))?;
let existing = check_destination(&abs_to)?;
match rename_into_place(&abs_from, &abs_to, existing) {
Ok(()) => {
let _ = crate::atomic::try_sync_dir(&parent);
return Ok(Migrated {
method: MigrateMethod::Rename,
source_removed: true,
});
}
Err(e) if is_cross_device(&e) => {}
Err(e) if is_occupied(&e) => return Err(FsError::AlreadyExists(abs_to)),
Err(e) => return Err(FsError::io(&abs_to, e)),
}
sweep_stale_copies(&parent, &name);
let temp = parent.join(format!(
".{name}.rkmigrate-{}-{}",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed)
));
if let Err(e) = copy_tree(&abs_from, &temp, CopyOptions::new(true, true)) {
let _ = fs::remove_dir_all(&temp);
return Err(e);
}
if let Err(e) = sync_tree(&temp) {
let _ = fs::remove_dir_all(&temp);
return Err(e);
}
let published = check_destination(&abs_to).and_then(|existing| {
rename_into_place(&temp, &abs_to, existing).map_err(|e| classify(&abs_to, e))
});
if let Err(e) = published {
let _ = fs::remove_dir_all(&temp);
return Err(e);
}
let source_removed =
crate::atomic::try_sync_dir(&parent).is_ok() && fs::remove_dir_all(&abs_from).is_ok();
Ok(Migrated {
method: MigrateMethod::CopyThenRemove,
source_removed,
})
}
fn refuse_inside(abs_from: &Path, abs_to: &Path, to: &Path) -> Result<()> {
let canon_from =
crate::plain::canonicalize_plain(abs_from).map_err(|e| FsError::io(abs_from, e))?;
let canon_to = canonical_with_missing_tail(abs_to).map_err(|e| FsError::io(to, e))?;
if is_inside(&canon_to, &canon_from) {
return Err(FsError::io(
to,
io::Error::new(
io::ErrorKind::InvalidInput,
"the destination is the source or inside it",
),
));
}
Ok(())
}
fn canonical_with_missing_tail(path: &Path) -> io::Result<PathBuf> {
for ancestor in path.ancestors() {
if fs::symlink_metadata(ancestor).is_ok() {
let mut out = crate::plain::canonicalize_plain(ancestor)?;
let tail = path.strip_prefix(ancestor).unwrap_or(Path::new(""));
for component in tail.components() {
match component {
Component::Normal(part) => out.push(part),
Component::CurDir => {}
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"the missing part of the destination path holds `..`",
))
}
}
}
return Ok(out);
}
}
Err(io::Error::new(
io::ErrorKind::NotFound,
"no part of the destination path exists",
))
}
fn is_inside(child: &Path, root: &Path) -> bool {
fn fold(component: Component<'_>) -> String {
let text = component.as_os_str().to_string_lossy().into_owned();
if cfg!(windows) {
text.to_lowercase()
} else {
text
}
}
let mut child = child.components().map(fold);
root.components().map(fold).all(|r| child.next() == Some(r))
}
fn sync_tree(dir: &Path) -> Result<()> {
for entry in fs::read_dir(dir).map_err(|e| FsError::io(dir, e))? {
let entry = entry.map_err(|e| FsError::io(dir, e))?;
let path = entry.path();
let kind = entry.file_type().map_err(|e| FsError::io(&path, e))?;
if kind.is_dir() {
sync_tree(&path)?;
} else if kind.is_file() {
sync_file(&path).map_err(|e| FsError::io(&path, e))?;
}
}
sync_directory(dir).map_err(|e| FsError::io(dir, e))
}
fn sync_file(path: &Path) -> io::Result<()> {
match fs::OpenOptions::new().read(true).write(true).open(path) {
Ok(file) => file.sync_all(),
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => sync_read_only(path, e),
Err(e) => Err(e),
}
}
#[cfg(not(windows))]
fn sync_read_only(path: &Path, _refused: io::Error) -> io::Result<()> {
fs::File::open(path)?.sync_all()
}
#[cfg(windows)]
fn sync_read_only(path: &Path, refused: io::Error) -> io::Result<()> {
let mut permissions = fs::metadata(path)?.permissions();
if !permissions.readonly() {
return Err(refused);
}
#[allow(clippy::permissions_set_readonly_false)]
permissions.set_readonly(false);
fs::set_permissions(path, permissions)?;
let flushed = fs::OpenOptions::new()
.write(true)
.open(path)
.and_then(|file| file.sync_all());
let restored = fs::metadata(path).and_then(|meta| {
let mut back = meta.permissions();
back.set_readonly(true);
fs::set_permissions(path, back)
});
flushed.and(restored)
}
#[cfg(unix)]
fn sync_directory(dir: &Path) -> io::Result<()> {
fs::File::open(dir)?.sync_all()
}
#[cfg(not(unix))]
fn sync_directory(_dir: &Path) -> io::Result<()> {
Ok(())
}
fn check_destination(to: &Path) -> Result<bool> {
match fs::symlink_metadata(to) {
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(FsError::io(to, e)),
Ok(meta) if meta.file_type().is_symlink() => Err(FsError::Symlink(to.to_path_buf())),
Ok(meta) if !meta.is_dir() => Err(FsError::AlreadyExists(to.to_path_buf())),
Ok(_) => {
let mut entries = fs::read_dir(to).map_err(|e| FsError::io(to, e))?;
if entries.next().is_some() {
return Err(FsError::AlreadyExists(to.to_path_buf()));
}
Ok(true)
}
}
}
#[cfg(not(windows))]
fn rename_into_place(src: &Path, to: &Path, _existing_empty: bool) -> io::Result<()> {
fs::rename(src, to)
}
#[cfg(windows)]
fn rename_into_place(src: &Path, to: &Path, existing_empty: bool) -> io::Result<()> {
let first = fs::rename(src, to);
if !existing_empty {
return first;
}
match first {
Ok(()) => Ok(()),
Err(e) if is_cross_device(&e) => Err(e),
Err(e) if !(is_occupied(&e) || e.kind() == io::ErrorKind::PermissionDenied) => Err(e),
Err(_) => {
fs::remove_dir(to)?;
let result = fs::rename(src, to);
if result.is_err() {
let _ = fs::create_dir(to);
}
result
}
}
}
fn classify(path: &Path, e: io::Error) -> FsError {
if is_occupied(&e) {
FsError::AlreadyExists(path.to_path_buf())
} else {
FsError::io(path, e)
}
}
fn is_occupied(e: &io::Error) -> bool {
if matches!(
e.kind(),
io::ErrorKind::AlreadyExists | io::ErrorKind::DirectoryNotEmpty
) {
return true;
}
#[cfg(windows)]
{
matches!(e.raw_os_error(), Some(183 | 145 | 80))
}
#[cfg(not(windows))]
{
false
}
}
fn is_cross_device(e: &io::Error) -> bool {
if e.kind() == io::ErrorKind::CrossesDevices {
return true;
}
#[cfg(windows)]
{
e.raw_os_error() == Some(17)
}
#[cfg(unix)]
{
e.raw_os_error() == Some(libc::EXDEV)
}
#[cfg(not(any(windows, unix)))]
{
false
}
}
fn sweep_stale_copies(parent: &Path, name: &str) {
let prefix = format!(".{name}.rkmigrate-");
let Ok(entries) = fs::read_dir(parent) else {
return;
};
for entry in entries.flatten() {
let file_name = entry.file_name();
let file_name = file_name.to_string_lossy();
let Some(rest) = file_name.strip_prefix(prefix.as_str()) else {
continue;
};
let Some(pid) = rest.split('-').next().and_then(|p| p.parse::<u32>().ok()) else {
continue;
};
if pid != std::process::id()
&& crate::process_liveness(pid) == crate::ProcessLiveness::Dead
&& entry.file_type().map(|t| t.is_dir()).unwrap_or(false)
{
let _ = fs::remove_dir_all(entry.path());
}
}
}