endpoint 2.0 dysnomia.ptr.pet
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 210 lines 7.0 kB view raw
1use std::io::{Read, Write}; 2 3use crate::{ 4 cmd::glob::{GlobOptions, expand_path}, 5 error::to_shell_err, 6 globals::{get_pwd, get_vfs}, 7}; 8use nu_engine::CallExt; 9use nu_protocol::{ 10 Category, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, 11 engine::{Command, EngineState, Stack}, 12}; 13use std::sync::Arc; 14use vfs::{VfsError, VfsFileType}; 15 16#[derive(Clone)] 17pub struct Mv; 18 19impl Command for Mv { 20 fn name(&self) -> &str { 21 "mv" 22 } 23 24 fn signature(&self) -> Signature { 25 Signature::build("mv") 26 .required( 27 "source", 28 SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::GlobPattern]), 29 "path to the file or directory to move", 30 ) 31 .required( 32 "destination", 33 SyntaxShape::Filepath, 34 "path to the destination", 35 ) 36 .input_output_type(Type::Nothing, Type::Nothing) 37 .category(Category::FileSystem) 38 } 39 40 fn description(&self) -> &str { 41 "move a file or directory in the virtual filesystem." 42 } 43 44 fn run( 45 &self, 46 engine_state: &EngineState, 47 stack: &mut Stack, 48 call: &nu_protocol::engine::Call, 49 _input: PipelineData, 50 ) -> Result<PipelineData, ShellError> { 51 let source_value: Value = call.req(engine_state, stack, 0)?; 52 let dest_path: String = call.req(engine_state, stack, 1)?; 53 54 let source_str = match source_value { 55 Value::String { val, .. } | Value::Glob { val, .. } => val, 56 _ => { 57 return Err(ShellError::GenericError { 58 error: "invalid source path".into(), 59 msg: "source must be a string or glob pattern".into(), 60 span: Some(call.arguments_span()), 61 help: None, 62 inner: vec![], 63 }); 64 } 65 }; 66 67 // Prevent moving root 68 if source_str == "/" { 69 return Err(ShellError::GenericError { 70 error: "cannot move root".to_string(), 71 msg: "refusing to move root directory".to_string(), 72 span: Some(call.arguments_span()), 73 help: None, 74 inner: vec![], 75 }); 76 } 77 78 // Expand source path (glob or single) into list of paths 79 let is_absolute = source_str.starts_with('/'); 80 let base_path: Arc<vfs::VfsPath> = if is_absolute { get_vfs() } else { get_pwd() }; 81 82 let options = GlobOptions { 83 max_depth: None, 84 no_dirs: false, 85 no_files: false, 86 }; 87 88 let matches = expand_path(&source_str, base_path.clone(), options)?; 89 let is_glob = matches.len() > 1 90 || source_str.contains('*') 91 || source_str.contains('?') 92 || source_str.contains('[') 93 || source_str.contains("**"); 94 95 // Resolve destination 96 let dest = get_pwd() 97 .join(dest_path.trim_end_matches('/')) 98 .map_err(to_shell_err(call.arguments_span()))?; 99 100 // For glob patterns, destination must be a directory 101 if is_glob { 102 let dest_meta = dest 103 .metadata() 104 .map_err(to_shell_err(call.arguments_span()))?; 105 if dest_meta.file_type != VfsFileType::Directory { 106 return Err(ShellError::GenericError { 107 error: "destination must be a directory".to_string(), 108 msg: "when using glob patterns, destination must be a directory".to_string(), 109 span: Some(call.arguments_span()), 110 help: None, 111 inner: vec![], 112 }); 113 } 114 } 115 116 // Move each matching file/directory 117 for rel_path in matches { 118 let source = base_path 119 .join(&rel_path) 120 .map_err(to_shell_err(call.arguments_span()))?; 121 let source_meta = source 122 .metadata() 123 .map_err(to_shell_err(call.arguments_span()))?; 124 125 // Determine destination path 126 let dest_entry = if is_glob { 127 // For glob patterns, use filename in destination directory 128 let filename = rel_path.split('/').last().unwrap_or(&rel_path); 129 dest.join(filename) 130 .map_err(to_shell_err(call.arguments_span()))? 131 } else { 132 // For single path, use destination as-is 133 dest.clone() 134 }; 135 136 match source_meta.file_type { 137 VfsFileType::File => move_file(&source, &dest_entry, call.arguments_span())?, 138 VfsFileType::Directory => { 139 move_directory(&source, &dest_entry, call.arguments_span())? 140 } 141 } 142 } 143 144 Ok(PipelineData::Empty) 145 } 146} 147 148fn move_file( 149 source: &vfs::VfsPath, 150 dest: &vfs::VfsPath, 151 span: nu_protocol::Span, 152) -> Result<(), ShellError> { 153 // Read source file content 154 let mut source_file = source.open_file().map_err(to_shell_err(span))?; 155 156 let mut contents = Vec::new(); 157 source_file 158 .read_to_end(&mut contents) 159 .map_err(|e| ShellError::GenericError { 160 error: "io error".to_string(), 161 msg: format!("failed to read source file: {}", e), 162 span: Some(span), 163 help: None, 164 inner: vec![], 165 })?; 166 167 // Create destination file and write content 168 dest.create_file() 169 .map_err(to_shell_err(span)) 170 .and_then(|mut f| { 171 f.write_all(&contents) 172 .map_err(VfsError::from) 173 .map_err(to_shell_err(span)) 174 })?; 175 176 // Remove source file 177 source.remove_file().map_err(to_shell_err(span))?; 178 179 Ok(()) 180} 181 182fn move_directory( 183 source: &vfs::VfsPath, 184 dest: &vfs::VfsPath, 185 span: nu_protocol::Span, 186) -> Result<(), ShellError> { 187 // Try to create destination directory (create_dir_all handles parent creation) 188 // If it already exists, that's fine - we'll move entries into it 189 let _ = dest.create_dir_all().map_err(to_shell_err(span)); 190 191 // Recursively move all entries 192 let entries = source.read_dir().map_err(to_shell_err(span))?; 193 for entry_name in entries { 194 let source_entry = source 195 .join(entry_name.as_str()) 196 .map_err(to_shell_err(span))?; 197 let dest_entry = dest.join(entry_name.as_str()).map_err(to_shell_err(span))?; 198 199 let entry_meta = source_entry.metadata().map_err(to_shell_err(span))?; 200 match entry_meta.file_type { 201 VfsFileType::File => move_file(&source_entry, &dest_entry, span)?, 202 VfsFileType::Directory => move_directory(&source_entry, &dest_entry, span)?, 203 } 204 } 205 206 // Remove source directory 207 source.remove_dir_all().map_err(to_shell_err(span))?; 208 209 Ok(()) 210}