1use std::fs;
5use std::path::Path;
6
7use crate::copy::{
8 check_if_destination_is_a_directory, check_if_sources_have_changed, get_source_path_metadata,
9 recheck_destination,
10};
11
12use crate::Result;
13use anyhow::anyhow;
14use clap::Parser;
15
16use clap_complete::ArgValueCompleter;
17use itertools::Itertools;
18use xvc_core::util::completer::{strum_variants_completer, xvc_path_completer};
19use xvc_core::FromConfigKey;
20use xvc_core::{info, uwr, XvcOutputSender};
21use xvc_core::{HStore, XvcEntity, XvcStore};
22use xvc_core::{RecheckMethod, XvcFileType, XvcMetadata, XvcPath, XvcRoot};
23
24#[derive(Debug, Clone, PartialEq, Eq, Parser)]
26#[command(rename_all = "kebab-case", author, version)]
27pub struct MoveCLI {
28 #[arg(long, alias = "as", add = ArgValueCompleter::new(strum_variants_completer::<RecheckMethod>) )]
33 pub recheck_method: Option<RecheckMethod>,
34
35 #[arg(long)]
39 pub no_recheck: bool,
40
41 #[arg(add = ArgValueCompleter::new(xvc_path_completer))]
48 pub source: String,
49
50 #[arg()]
57 pub destination: String,
58}
59
60pub fn get_move_source_dest_store(
66 output_snd: &XvcOutputSender,
67 xvc_root: &XvcRoot,
68 stored_xvc_path_store: &XvcStore<XvcPath>,
69 stored_xvc_metadata_store: &XvcStore<XvcMetadata>,
70 source_xvc_paths: &HStore<XvcPath>,
71 source_xvc_metadata: &HStore<XvcMetadata>,
72 destination: &str,
73) -> Result<HStore<XvcPath>> {
74 if destination.ends_with('/') {
79 let dir_path = XvcPath::new(
80 xvc_root,
81 xvc_root,
82 Path::new(destination.strip_suffix('/').unwrap()),
83 )?;
84
85 check_if_destination_is_a_directory(
86 &dir_path,
87 stored_xvc_path_store,
88 stored_xvc_metadata_store,
89 )?;
90
91 check_if_sources_have_changed(
92 output_snd,
93 xvc_root,
94 stored_xvc_path_store,
95 stored_xvc_metadata_store,
96 source_xvc_paths,
97 source_xvc_metadata,
98 )?;
99
100 let mut source_dest_store = HStore::new();
101
102 let mut error_paths = vec![];
103
104 for (source_xe, source_path) in source_xvc_paths.iter() {
105 let dest_path = dir_path.join(source_path).unwrap();
106
107 match stored_xvc_path_store.entities_for(&dest_path) {
108 Some(_v) => {
109 error_paths.push(dest_path);
110 }
111 None => {
112 source_dest_store.insert(*source_xe, dest_path);
113 }
114 }
115 }
116
117 if !error_paths.is_empty() {
118 Err(anyhow!(
119 "Destination files already exist. Operation cancelled. Delete them first: {}",
120 error_paths.iter().map(|xp| xp.to_string()).join("\n")
121 )
122 .into())
123 } else {
124 Ok(source_dest_store)
125 }
126 } else {
127 if source_xvc_paths.len() > 1 {
129 return Err(
130 anyhow!("Destination must be a directory if multiple sources are given").into(),
131 );
132 }
133
134 check_if_sources_have_changed(
135 output_snd,
136 xvc_root,
137 stored_xvc_path_store,
138 stored_xvc_metadata_store,
139 source_xvc_paths,
140 source_xvc_metadata,
141 )?;
142
143 let current_dir = xvc_root.config().current_dir()?;
144 let source_xe = source_xvc_paths.keys().next().unwrap();
145
146 let mut source_dest_store = HStore::<XvcPath>::with_capacity(1);
147 let dest_path = XvcPath::new(xvc_root, current_dir, Path::new(destination))?;
148
149 match stored_xvc_path_store.entity_by_value(&dest_path) {
150 Some(_) => Err(anyhow!(
151 "Destination file {} already exists. Delete it first.",
152 dest_path
153 )
154 .into()),
155 None => {
156 source_dest_store.insert(*source_xe, dest_path);
157 Ok(source_dest_store)
158 }
159 }
160 }
161}
162
163pub fn cmd_move(output_snd: &XvcOutputSender, xvc_root: &XvcRoot, opts: MoveCLI) -> Result<()> {
165 let stored_metadata_store = xvc_root.load_store::<XvcMetadata>()?;
167 let stored_xvc_path_store = xvc_root.load_store::<XvcPath>()?;
168 let (source_xvc_paths, source_metadata) = get_source_path_metadata(
169 output_snd,
170 xvc_root,
171 &stored_xvc_path_store,
172 &stored_metadata_store,
173 &opts.source,
174 &opts.destination,
175 )?;
176
177 let source_dest_store = get_move_source_dest_store(
178 output_snd,
179 xvc_root,
180 &stored_xvc_path_store,
181 &stored_metadata_store,
182 &source_xvc_paths,
183 &source_metadata,
184 &opts.destination,
185 )?;
186
187 xvc_root.with_store_mut(|xvc_path_store: &mut XvcStore<XvcPath>| {
188 xvc_root.with_store_mut(|xvc_metadata_store: &mut XvcStore<XvcMetadata>| {
189 for (source_xe, dest_path) in source_dest_store.iter() {
190 xvc_path_store.update(*source_xe, dest_path.clone());
191 for parent in dest_path.parents() {
193 let parent_entities = xvc_path_store.entities_for(&parent);
194 if parent_entities.is_none() || parent_entities.unwrap().is_empty() {
195 let parent_entity = xvc_root.new_entity();
196 xvc_path_store.insert(parent_entity, parent.clone());
197 xvc_metadata_store.insert(
198 parent_entity,
199 XvcMetadata {
200 file_type: XvcFileType::Directory,
201 ..Default::default()
202 },
203 );
204 }
205 }
206 }
207 Ok(())
208 })?;
209 Ok(())
210 })?;
211
212 let mut recheck_entities = Vec::<XvcEntity>::new();
213 xvc_root.with_store_mut(|recheck_method_store: &mut XvcStore<RecheckMethod>| {
214 for (source_xe, dest_path) in source_dest_store.iter() {
215 let source_path = stored_xvc_path_store.get(source_xe).unwrap();
216 let source_recheck_method = recheck_method_store
217 .get(source_xe)
218 .copied()
219 .unwrap_or_else(|| RecheckMethod::from_conf(xvc_root.config()));
220
221 let dest_recheck_method = if let Some(given_recheck_method) = opts.recheck_method {
222 given_recheck_method
223 } else {
224 source_recheck_method
225 };
226
227 if dest_recheck_method != source_recheck_method {
228 recheck_method_store.update(*source_xe, dest_recheck_method);
229 }
230 match (source_recheck_method, dest_recheck_method) {
231 (RecheckMethod::Copy, RecheckMethod::Copy) => {
233 let source_path = source_path.to_absolute_path(xvc_root);
234 let dest_path = dest_path.to_absolute_path(xvc_root);
235 if source_path != dest_path {
236 if opts.no_recheck {
238 fs::remove_file(&source_path)?;
239 } else {
240 let parent = dest_path.parent().unwrap();
241 if !parent.exists() {
242 fs::create_dir_all(parent)?;
243 }
244 fs::rename(&source_path, &dest_path)?;
245 }
246 } else {
247 info!(
248 output_snd,
249 "Source and destination are the same. Skipping move for {}",
250 source_path
251 );
252 }
253 }
254 _ => {
257 let source_path = source_path.to_absolute_path(xvc_root);
258 if source_path.exists() {
259 uwr!(fs::remove_file(&source_path), output_snd);
260 }
261 recheck_entities.push(*source_xe);
262 }
263 }
264 }
265 Ok(())
266 })?;
267
268 if !opts.no_recheck {
269 recheck_destination(output_snd, xvc_root, &recheck_entities)?;
270 }
271
272 Ok(())
273}