gravityfile_ops/
rename.rs1use std::fs;
4use std::path::PathBuf;
5
6use tokio::sync::mpsc;
7
8use crate::progress::{OperationComplete, OperationProgress, OperationType};
9use crate::{OPERATION_CHANNEL_SIZE, OperationError};
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.symlink_metadata().is_ok() && new_path != source {
62 progress.add_error(OperationError::new(
63 source.clone(),
64 format!("'{}' already exists", new_name),
65 ));
66 let _ = tx
67 .send(RenameResult::Complete(OperationComplete {
68 operation_type: OperationType::Rename,
69 succeeded: 0,
70 failed: 1,
71 bytes_processed: 0,
72 errors: progress.errors,
73 }))
74 .await;
75 return;
76 }
77
78 let source_clone = source.clone();
80 let new_path_clone = new_path.clone();
81
82 let result = tokio::task::spawn_blocking(move || fs::rename(&source_clone, &new_path_clone))
83 .await
84 .map_err(|e| format!("Task failed: {}", e));
85
86 match result {
87 Ok(Ok(())) => {
88 progress.complete_file(0);
89 let _ = tx
90 .send(RenameResult::Complete(OperationComplete {
91 operation_type: OperationType::Rename,
92 succeeded: 1,
93 failed: 0,
94 bytes_processed: 0,
95 errors: vec![],
96 }))
97 .await;
98 }
99 Ok(Err(e)) => {
100 progress.add_error(OperationError::new(source, format!("Rename failed: {}", e)));
101 let _ = tx
102 .send(RenameResult::Complete(OperationComplete {
103 operation_type: OperationType::Rename,
104 succeeded: 0,
105 failed: 1,
106 bytes_processed: 0,
107 errors: progress.errors,
108 }))
109 .await;
110 }
111 Err(e) => {
112 progress.add_error(OperationError::new(source, e));
113 let _ = tx
114 .send(RenameResult::Complete(OperationComplete {
115 operation_type: OperationType::Rename,
116 succeeded: 0,
117 failed: 1,
118 bytes_processed: 0,
119 errors: progress.errors,
120 }))
121 .await;
122 }
123 }
124}
125
126pub fn validate_filename(name: &str) -> Result<(), String> {
128 if name.is_empty() {
129 return Err("Name cannot be empty".into());
130 }
131
132 if name.len() > 255 {
136 return Err("Name is too long (max 255 bytes)".into());
137 }
138
139 let invalid_chars = ['/', '\0'];
141 for c in invalid_chars {
142 if name.contains(c) {
143 return Err(format!("Name cannot contain '{}'", c));
144 }
145 }
146
147 #[cfg(target_os = "windows")]
149 {
150 let windows_invalid = ['\\', ':', '*', '?', '"', '<', '>', '|'];
151 for c in windows_invalid {
152 if name.contains(c) {
153 return Err(format!("Name cannot contain '{}'", c));
154 }
155 }
156
157 let reserved = [
159 "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7",
160 "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
161 ];
162 let upper_name = name.to_uppercase();
163 let base_name = upper_name.split('.').next().unwrap_or("");
164 if reserved.contains(&base_name) {
165 return Err("Reserved filename".into());
166 }
167 }
168
169 if name.starts_with(' ') || name.ends_with(' ') {
171 return Err("Name cannot start or end with spaces".into());
172 }
173
174 if name.ends_with('.') {
175 return Err("Name cannot end with a dot".into());
176 }
177
178 if name == "." || name == ".." {
180 return Err("'.' and '..' are reserved names".into());
181 }
182
183 Ok(())
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn test_validate_filename_valid() {
192 assert!(validate_filename("test.txt").is_ok());
193 assert!(validate_filename("my-file").is_ok());
194 assert!(validate_filename(".hidden").is_ok());
195 assert!(validate_filename("file with spaces").is_ok());
196 }
197
198 #[test]
199 fn test_validate_filename_invalid() {
200 assert!(validate_filename("").is_err());
201 assert!(validate_filename("test/file").is_err());
202 assert!(validate_filename(".").is_err());
203 assert!(validate_filename("..").is_err());
204 assert!(validate_filename("file ").is_err());
205 assert!(validate_filename(" file").is_err());
206 assert!(validate_filename("file.").is_err());
207 }
208}