gravityfile_ops/
create.rs1use std::fs::{self, OpenOptions};
4use std::path::PathBuf;
5
6use tokio::sync::mpsc;
7
8use crate::progress::{OperationComplete, OperationProgress, OperationType};
9use crate::rename::validate_filename;
10use crate::{OPERATION_CHANNEL_SIZE, OperationError};
11
12#[derive(Debug)]
14pub enum CreateResult {
15 Progress(OperationProgress),
17 Complete(OperationComplete),
19}
20
21pub fn start_create_file(path: PathBuf) -> mpsc::Receiver<CreateResult> {
24 start_create(path, false)
25}
26
27pub fn start_create_directory(path: PathBuf) -> mpsc::Receiver<CreateResult> {
29 start_create(path, true)
30}
31
32fn start_create(path: PathBuf, directory: bool) -> mpsc::Receiver<CreateResult> {
33 let (tx, rx) = mpsc::channel(OPERATION_CHANNEL_SIZE);
34 tokio::spawn(create_impl(path, directory, tx));
35 rx
36}
37
38async fn create_impl(path: PathBuf, directory: bool, tx: mpsc::Sender<CreateResult>) {
39 let operation_type = if directory {
40 OperationType::CreateDirectory
41 } else {
42 OperationType::CreateFile
43 };
44 match tx.try_reserve() {
45 Ok(permit) => {
46 let mut progress = OperationProgress::new(operation_type, 1, 0);
47 progress.set_current_file(Some(path.clone()));
48 permit.send(CreateResult::Progress(progress));
49 }
50 Err(mpsc::error::TrySendError::Closed(_)) => return,
51 Err(mpsc::error::TrySendError::Full(_)) => {}
52 }
53 let work_path = path.clone();
54 let receiver = tx.clone();
55 let result = tokio::task::spawn_blocking(move || {
56 create_on_worker(&work_path, directory, || !receiver.is_closed())
57 })
58 .await;
59 let (succeeded, failed, cancelled, errors) = match result {
60 Ok(Ok(created)) => (usize::from(created), 0, !created, Vec::new()),
61 Ok(Err(error)) => (0, 1, false, vec![OperationError::new(path, error)]),
62 Err(error) => (
63 0,
64 1,
65 false,
66 vec![OperationError::new(path, format!("Task failed: {error}"))],
67 ),
68 };
69 let _ = tx
70 .send(CreateResult::Complete(OperationComplete {
71 operation_type,
72 succeeded,
73 failed,
74 cancelled,
75 skipped: 0,
76 bytes_processed: 0,
77 errors,
78 }))
79 .await;
80}
81
82fn create_on_worker(
85 path: &std::path::Path,
86 directory: bool,
87 wanted: impl Fn() -> bool,
88) -> Result<bool, String> {
89 if !wanted() {
90 return Ok(false);
91 }
92 let name = path
93 .file_name()
94 .ok_or_else(|| "A filename is required".to_string())?;
95 if let Some(name) = name.to_str() {
96 validate_filename(name)?;
97 }
98 match path.symlink_metadata() {
99 Ok(_) => {
100 return Err(if directory {
101 "Directory already exists"
102 } else {
103 "File already exists"
104 }
105 .into());
106 }
107 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
108 Err(error) => return Err(format!("Cannot inspect destination: {error}")),
109 }
110 if !wanted() {
111 return Ok(false);
112 }
113 if let Some(parent) = path
114 .parent()
115 .filter(|parent| !parent.as_os_str().is_empty())
116 {
117 fs::create_dir_all(parent)
118 .map_err(|error| format!("Failed to create parent directory: {error}"))?;
119 }
120 if !wanted() {
121 return Ok(false);
122 }
123 if directory {
126 fs::create_dir(path).map_err(|error| format!("Failed to create directory: {error}"))?;
127 } else {
128 OpenOptions::new()
129 .write(true)
130 .create_new(true)
131 .open(path)
132 .map_err(|error| format!("Failed to create file: {error}"))?;
133 }
134 Ok(true)
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[tokio::test]
142 async fn full_progress_queue_does_not_stall_creation() {
143 for directory in [false, true] {
144 let temp = tempfile::tempdir().unwrap();
145 let path = temp.path().join("item");
146 let (tx, mut rx) = mpsc::channel(1);
147 tx.try_send(CreateResult::Progress(OperationProgress::new(
148 OperationType::CreateFile,
149 1,
150 0,
151 )))
152 .unwrap();
153 let work = tokio::spawn(create_impl(path.clone(), directory, tx));
154 tokio::time::timeout(std::time::Duration::from_secs(5), async {
155 while !path.exists() {
156 tokio::task::yield_now().await;
157 }
158 })
159 .await
160 .expect("progress queue stalled creation");
161 rx.recv().await.unwrap();
162 let CreateResult::Complete(result) = rx.recv().await.unwrap() else {
163 panic!("completion expected")
164 };
165 assert!(result.is_success());
166 assert_eq!(result.succeeded, 1);
167 assert_eq!(path.is_dir(), directory);
168 work.await.unwrap();
169 }
170 }
171
172 #[tokio::test]
173 async fn closed_receiver_prevents_creating_parents_or_item() {
174 for directory in [false, true] {
175 let temp = tempfile::tempdir().unwrap();
176 let path = temp.path().join("parent/item");
177 let (tx, rx) = mpsc::channel(1);
178 drop(rx);
179 create_impl(path.clone(), directory, tx).await;
180 assert!(!path.parent().unwrap().exists());
181 }
182 }
183
184 #[test]
185 fn abandonment_after_parent_creation_does_not_create_the_item() {
186 for directory in [false, true] {
187 let temp = tempfile::tempdir().unwrap();
188 let path = temp.path().join("parent/item");
189 let checks = std::cell::Cell::new(0);
190 assert!(
191 !create_on_worker(&path, directory, || {
192 checks.set(checks.get() + 1);
193 checks.get() < 3
194 })
195 .unwrap()
196 );
197 assert!(path.parent().unwrap().is_dir());
198 assert!(!path.exists());
199 }
200 }
201
202 #[test]
203 fn concurrent_destination_creation_is_never_claimed_as_success() {
204 for directory in [false, true] {
205 let temp = tempfile::tempdir().unwrap();
206 let path = temp.path().join("item");
207 let checks = std::cell::Cell::new(0);
208 let result = create_on_worker(&path, directory, || {
209 checks.set(checks.get() + 1);
210 if checks.get() == 3 {
211 if directory {
212 fs::create_dir(&path).unwrap();
213 } else {
214 fs::write(&path, b"other writer").unwrap();
215 }
216 }
217 true
218 });
219 assert!(result.is_err());
220 if directory {
221 assert!(path.is_dir());
222 } else {
223 assert_eq!(fs::read(path).unwrap(), b"other writer");
224 }
225 }
226 }
227}