xvc_file/mv/
mod.rs

1//! The home of `xvc file move` command.
2//!
3//! It contains [`MoveCLI`] to define command line for the command [`cmd_move`] as the entry point.
4use 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/// CLI for `xvc file copy`.
25#[derive(Debug, Clone, PartialEq, Eq, Parser)]
26#[command(rename_all = "kebab-case", author, version)]
27pub struct MoveCLI {
28    /// How the destination should be rechecked: One of copy, symlink, hardlink, reflink.
29    ///
30    /// Note: Reflink uses copy if the underlying file system doesn't support it.
31    ///
32    #[arg(long, alias = "as", add = ArgValueCompleter::new(strum_variants_completer::<RecheckMethod>) )]
33    pub recheck_method: Option<RecheckMethod>,
34
35    /// Do not recheck the destination files
36    /// This is useful when you want to copy only records, without updating the
37    /// workspace.
38    #[arg(long)]
39    pub no_recheck: bool,
40
41    /// Source file, glob or directory within the workspace.
42    ///
43    /// If the source ends with a slash, it's considered a directory and all
44    /// files in that directory are copied.
45    ///
46    /// If there are multiple source files, the destination must be a directory.
47    #[arg(add = ArgValueCompleter::new(xvc_path_completer))]
48    pub source: String,
49
50    /// Location we move file(s) to within the workspace.
51    ///
52    /// If this ends with a slash, it's considered a directory and
53    /// created if it doesn't exist.
54    ///
55    /// If the number of source files is more than one, the destination must be a directory.
56    #[arg()]
57    pub destination: String,
58}
59
60/// Return movable [`XvcPath`] entities.
61/// Unlike [`get_copy_source_dest_store`], this function doesn't create any new entities. The move sources should
62/// already be recorded.
63/// `stored_xvc_path_store` and `stored_xvc_metadata_store` are results of `load_targets_from_store`, and
64/// `source_xvc_paths` and `source_xvc_metadata` are loaded from `targets_from_disk`.
65pub 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    // Create targets in the store
75    // If destination is a directory, check if exists and create if not.
76    // If destination is a file, check if exists and return error if it does and
77    // force is not set.
78    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        // Destination doesn't end with '/'
128        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
163/// Entry point for `xvc file move`
164pub fn cmd_move(output_snd: &XvcOutputSender, xvc_root: &XvcRoot, opts: MoveCLI) -> Result<()> {
165    // Get all files to move
166    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                // Create destination parent directory records if they don't exist
192                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                // If both are copy, move the file
232                (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 no-recheck is given, this effectively works like a delete.
237                        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                // For others just delete the source and recheck.
255                // Moving symlinks relatively etc. is too much complexity for little gain.
256                _ => {
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}