Skip to main content

gravityfile_ops/
create.rs

1//! File and directory creation operations.
2
3use 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/// Result sent through the channel during create operations.
13#[derive(Debug)]
14pub enum CreateResult {
15    /// Progress update.
16    Progress(OperationProgress),
17    /// The operation completed.
18    Complete(OperationComplete),
19}
20
21/// Start file creation. Dropping the receiver prevents queued work from starting;
22/// active filesystem calls are awaited and cannot be interrupted.
23pub fn start_create_file(path: PathBuf) -> mpsc::Receiver<CreateResult> {
24    start_create(path, false)
25}
26
27/// Start directory creation with the same delivery semantics as file creation.
28pub 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
82/// Returns false if abandoned before final creation. Parent directories already
83/// created remain in place; this is not a transaction across the parent chain.
84fn 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    // Exclusive creation is authoritative, including when another writer wins
124    // after the metadata check. Never claim an existing item for undo.
125    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}