use std::fs::{self, OpenOptions};
use std::path::PathBuf;
use tokio::sync::mpsc;
use crate::progress::{OperationComplete, OperationProgress, OperationType};
use crate::rename::validate_filename;
use crate::{OPERATION_CHANNEL_SIZE, OperationError};
#[derive(Debug)]
pub enum CreateResult {
Progress(OperationProgress),
Complete(OperationComplete),
}
pub fn start_create_file(path: PathBuf) -> mpsc::Receiver<CreateResult> {
start_create(path, false)
}
pub fn start_create_directory(path: PathBuf) -> mpsc::Receiver<CreateResult> {
start_create(path, true)
}
fn start_create(path: PathBuf, directory: bool) -> mpsc::Receiver<CreateResult> {
let (tx, rx) = mpsc::channel(OPERATION_CHANNEL_SIZE);
tokio::spawn(create_impl(path, directory, tx));
rx
}
async fn create_impl(path: PathBuf, directory: bool, tx: mpsc::Sender<CreateResult>) {
let operation_type = if directory {
OperationType::CreateDirectory
} else {
OperationType::CreateFile
};
match tx.try_reserve() {
Ok(permit) => {
let mut progress = OperationProgress::new(operation_type, 1, 0);
progress.set_current_file(Some(path.clone()));
permit.send(CreateResult::Progress(progress));
}
Err(mpsc::error::TrySendError::Closed(_)) => return,
Err(mpsc::error::TrySendError::Full(_)) => {}
}
let work_path = path.clone();
let receiver = tx.clone();
let result = tokio::task::spawn_blocking(move || {
create_on_worker(&work_path, directory, || !receiver.is_closed())
})
.await;
let (succeeded, failed, cancelled, errors) = match result {
Ok(Ok(created)) => (usize::from(created), 0, !created, Vec::new()),
Ok(Err(error)) => (0, 1, false, vec![OperationError::new(path, error)]),
Err(error) => (
0,
1,
false,
vec![OperationError::new(path, format!("Task failed: {error}"))],
),
};
let _ = tx
.send(CreateResult::Complete(OperationComplete {
operation_type,
succeeded,
failed,
cancelled,
skipped: 0,
bytes_processed: 0,
errors,
}))
.await;
}
fn create_on_worker(
path: &std::path::Path,
directory: bool,
wanted: impl Fn() -> bool,
) -> Result<bool, String> {
if !wanted() {
return Ok(false);
}
let name = path
.file_name()
.ok_or_else(|| "A filename is required".to_string())?;
if let Some(name) = name.to_str() {
validate_filename(name)?;
}
match path.symlink_metadata() {
Ok(_) => {
return Err(if directory {
"Directory already exists"
} else {
"File already exists"
}
.into());
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(format!("Cannot inspect destination: {error}")),
}
if !wanted() {
return Ok(false);
}
if let Some(parent) = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
{
fs::create_dir_all(parent)
.map_err(|error| format!("Failed to create parent directory: {error}"))?;
}
if !wanted() {
return Ok(false);
}
if directory {
fs::create_dir(path).map_err(|error| format!("Failed to create directory: {error}"))?;
} else {
OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.map_err(|error| format!("Failed to create file: {error}"))?;
}
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn full_progress_queue_does_not_stall_creation() {
for directory in [false, true] {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("item");
let (tx, mut rx) = mpsc::channel(1);
tx.try_send(CreateResult::Progress(OperationProgress::new(
OperationType::CreateFile,
1,
0,
)))
.unwrap();
let work = tokio::spawn(create_impl(path.clone(), directory, tx));
tokio::time::timeout(std::time::Duration::from_secs(5), async {
while !path.exists() {
tokio::task::yield_now().await;
}
})
.await
.expect("progress queue stalled creation");
rx.recv().await.unwrap();
let CreateResult::Complete(result) = rx.recv().await.unwrap() else {
panic!("completion expected")
};
assert!(result.is_success());
assert_eq!(result.succeeded, 1);
assert_eq!(path.is_dir(), directory);
work.await.unwrap();
}
}
#[tokio::test]
async fn closed_receiver_prevents_creating_parents_or_item() {
for directory in [false, true] {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("parent/item");
let (tx, rx) = mpsc::channel(1);
drop(rx);
create_impl(path.clone(), directory, tx).await;
assert!(!path.parent().unwrap().exists());
}
}
#[test]
fn abandonment_after_parent_creation_does_not_create_the_item() {
for directory in [false, true] {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("parent/item");
let checks = std::cell::Cell::new(0);
assert!(
!create_on_worker(&path, directory, || {
checks.set(checks.get() + 1);
checks.get() < 3
})
.unwrap()
);
assert!(path.parent().unwrap().is_dir());
assert!(!path.exists());
}
}
#[test]
fn concurrent_destination_creation_is_never_claimed_as_success() {
for directory in [false, true] {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("item");
let checks = std::cell::Cell::new(0);
let result = create_on_worker(&path, directory, || {
checks.set(checks.get() + 1);
if checks.get() == 3 {
if directory {
fs::create_dir(&path).unwrap();
} else {
fs::write(&path, b"other writer").unwrap();
}
}
true
});
assert!(result.is_err());
if directory {
assert!(path.is_dir());
} else {
assert_eq!(fs::read(path).unwrap(), b"other writer");
}
}
}
}