forked from
ptr.pet/faunu
endpoint 2.0
dysnomia.ptr.pet
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}