gravityfile_ops/
rename.rs1use std::fs;
4use std::path::PathBuf;
5
6use tokio::sync::mpsc;
7
8use crate::progress::{OperationComplete, OperationProgress, OperationType};
9use crate::{OperationError, OPERATION_CHANNEL_SIZE};
10
11#[derive(Debug)]
13pub enum RenameResult {
14 Progress(OperationProgress),
16 Complete(OperationComplete),
18}
19
20pub fn start_rename(source: PathBuf, new_name: String) -> mpsc::Receiver<RenameResult> {
24 let (tx, rx) = mpsc::channel(OPERATION_CHANNEL_SIZE);
25
26 tokio::spawn(async move {
27 rename_impl(source, new_name, tx).await;
28 });
29
30 rx
31}
32
33async fn rename_impl(source: PathBuf, new_name: String, tx: mpsc::Sender<RenameResult>) {
35 let mut progress = OperationProgress::new(OperationType::Rename, 1, 0);
36 progress.set_current_file(Some(source.clone()));
37
38 let _ = tx.send(RenameResult::Progress(progress.clone())).await;
39
40 if let Err(e) = validate_filename(&new_name) {
42 progress.add_error(OperationError::new(source.clone(), e));
43 let _ = tx
44 .send(RenameResult::Complete(OperationComplete {
45 operation_type: OperationType::Rename,
46 succeeded: 0,
47 failed: 1,
48 bytes_processed: 0,
49 errors: progress.errors,
50 }))
51 .await;
52 return;
53 }
54
55 let parent = source.parent().unwrap_or(std::path::Path::new(""));
57 let new_path = parent.join(&new_name);
58
59 if new_path.exists() && new_path != source {
61 progress.add_error(OperationError::new(
62 source.clone(),
63 format!("'{}' already exists", new_name),
64 ));
65 let _ = tx
66 .send(RenameResult::Complete(OperationComplete {
67 operation_type: OperationType::Rename,
68 succeeded: 0,
69 failed: 1,
70 bytes_processed: 0,
71 errors: progress.errors,
72 }))
73 .await;
74 return;
75 }
76
77 let source_clone = source.clone();
79 let new_path_clone = new_path.clone();
80
81 let result = tokio::task::spawn_blocking(move || fs::rename(&source_clone, &new_path_clone))
82 .await
83 .map_err(|e| format!("Task failed: {}", e));
84
85 match result {
86 Ok(Ok(())) => {
87 progress.complete_file(0);
88 let _ = tx
89 .send(RenameResult::Complete(OperationComplete {
90 operation_type: OperationType::Rename,
91 succeeded: 1,
92 failed: 0,
93 bytes_processed: 0,
94 errors: vec![],
95 }))
96 .await;
97 }
98 Ok(Err(e)) => {
99 progress.add_error(OperationError::new(source, format!("Rename failed: {}", e)));
100 let _ = tx
101 .send(RenameResult::Complete(OperationComplete {
102 operation_type: OperationType::Rename,
103 succeeded: 0,
104 failed: 1,
105 bytes_processed: 0,
106 errors: progress.errors,
107 }))
108 .await;
109 }
110 Err(e) => {
111 progress.add_error(OperationError::new(source, e));
112 let _ = tx
113 .send(RenameResult::Complete(OperationComplete {
114 operation_type: OperationType::Rename,
115 succeeded: 0,
116 failed: 1,
117 bytes_processed: 0,
118 errors: progress.errors,
119 }))
120 .await;
121 }
122 }
123}
124
125pub fn validate_filename(name: &str) -> Result<(), String> {
127 if name.is_empty() {
128 return Err("Name cannot be empty".into());
129 }
130
131 if name.len() > 255 {
132 return Err("Name is too long (max 255 characters)".into());
133 }
134
135 let invalid_chars = ['/', '\0'];
137 for c in invalid_chars {
138 if name.contains(c) {
139 return Err(format!("Name cannot contain '{}'", c));
140 }
141 }
142
143 #[cfg(target_os = "windows")]
145 {
146 let windows_invalid = ['\\', ':', '*', '?', '"', '<', '>', '|'];
147 for c in windows_invalid {
148 if name.contains(c) {
149 return Err(format!("Name cannot contain '{}'", c));
150 }
151 }
152
153 let reserved = [
155 "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7",
156 "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
157 ];
158 let upper_name = name.to_uppercase();
159 let base_name = upper_name.split('.').next().unwrap_or("");
160 if reserved.contains(&base_name) {
161 return Err("Reserved filename".into());
162 }
163 }
164
165 if name.starts_with(' ') || name.ends_with(' ') {
167 return Err("Name cannot start or end with spaces".into());
168 }
169
170 if name.ends_with('.') {
171 return Err("Name cannot end with a dot".into());
172 }
173
174 if name == "." || name == ".." {
176 return Err("'.' and '..' are reserved names".into());
177 }
178
179 Ok(())
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_validate_filename_valid() {
188 assert!(validate_filename("test.txt").is_ok());
189 assert!(validate_filename("my-file").is_ok());
190 assert!(validate_filename(".hidden").is_ok());
191 assert!(validate_filename("file with spaces").is_ok());
192 }
193
194 #[test]
195 fn test_validate_filename_invalid() {
196 assert!(validate_filename("").is_err());
197 assert!(validate_filename("test/file").is_err());
198 assert!(validate_filename(".").is_err());
199 assert!(validate_filename("..").is_err());
200 assert!(validate_filename("file ").is_err());
201 assert!(validate_filename(" file").is_err());
202 assert!(validate_filename("file.").is_err());
203 }
204}