xvc_file/bring/
mod.rs

1//! Bring files from external storages to workspace.
2//!
3//! - [BringCLI] defines the command line options.
4//!
5//! - [cmd_bring]  is the entry point for the command.
6//!   Uses [fetch] and [crate::recheck::cmd_recheck] to bring the file and copy/link it to the
7//!   workspace.
8
9use crate::common::{load_targets_from_store, move_to_cache};
10
11use crate::{
12    recheck::{cmd_recheck, RecheckCLI},
13    Result,
14};
15
16use clap::Parser;
17
18use clap_complete::ArgValueCompleter;
19use xvc_core::util::completer::{strum_variants_completer, xvc_path_completer};
20use xvc_core::{debug, error, uwr, warn, XvcOutputSender};
21use xvc_core::{
22    ContentDigest, HStore, RecheckMethod, XvcCachePath, XvcFileType, XvcMetadata, XvcRoot, XvcStore,
23};
24
25use xvc_core::PathSync;
26use xvc_storage::storage::storage_identifier_completer;
27use xvc_storage::XvcStorageEvent;
28use xvc_storage::{storage::get_storage_record, StorageIdentifier, XvcStorageOperations};
29
30/// Bring (download, pull, fetch) files from storage.
31///
32/// You can configure a new storage with [`xvc storage new`][xvc_storage::new] and use it to
33/// download and upload tracked files.
34#[derive(Debug, Clone, PartialEq, Eq, Parser)]
35#[command(rename_all = "kebab-case")]
36pub struct BringCLI {
37    /// Storage name or guid to send the files
38    #[arg(long, short, alias = "from", add = ArgValueCompleter::new(storage_identifier_completer))]
39    storage: StorageIdentifier,
40
41    /// Force even if the files are already present in the workspace
42    #[arg(long)]
43    force: bool,
44
45    /// Don't recheck (checkout) after bringing the file to cache.
46    ///
47    /// This makes the command similar to `git fetch` in Git.
48    /// It just updates the cache, and doesn't copy/link the file to workspace.
49    #[arg(long)]
50    no_recheck: bool,
51
52    /// Recheck (checkout) the file in one of the four alternative ways.
53    /// (See `xvc file recheck`) and [RecheckMethod]
54    #[arg(long, alias = "as", add = ArgValueCompleter::new(strum_variants_completer::<RecheckMethod>))]
55    recheck_as: Option<RecheckMethod>,
56
57    /// Targets to bring from the storage
58    #[arg(add = ArgValueCompleter::new(xvc_path_completer))]
59    targets: Option<Vec<String>>,
60}
61
62/// Download files in `opts.targets` from `opts.storage` to cache.
63///
64/// - Retrieves the storage record from `xvc_root`.
65/// - Expands globs in `opts.targets`.
66/// - Gets the corresponding cache path for each file target.
67/// - Calls `storage.receive` for each of these targets.
68pub fn fetch(output_snd: &XvcOutputSender, xvc_root: &XvcRoot, opts: &BringCLI) -> Result<()> {
69    let storage = get_storage_record(output_snd, xvc_root, &opts.storage)?;
70
71    let current_dir = xvc_root.config().current_dir()?;
72    let targets = load_targets_from_store(output_snd, xvc_root, current_dir, &opts.targets)?;
73    let force = opts.force;
74
75    let target_xvc_metadata = xvc_root
76        .load_store::<XvcMetadata>()?
77        .subset(targets.keys().copied())?;
78
79    let target_file_xvc_metadata =
80        target_xvc_metadata.filter(|_, xmd| xmd.file_type == XvcFileType::File);
81
82    let target_files = targets.subset(target_file_xvc_metadata.keys().copied())?;
83
84    // Get all cache paths for these paths
85    let content_digest_store: XvcStore<ContentDigest> = xvc_root.load_store()?;
86
87    let target_content_digests = content_digest_store.subset(target_files.keys().copied())?;
88
89    assert! {
90        target_content_digests.len() == target_files.len(),
91        "All files should have a content digest"
92    }
93
94    let cache_paths: HStore<XvcCachePath> = target_content_digests
95        .iter()
96        .filter_map(|(xe, cd)| {
97            let xvc_path = target_files.get(xe).unwrap();
98            match XvcCachePath::new(xvc_path, cd) {
99                Ok(cp) => Some((*xe, cp)),
100                Err(e) => {
101                    warn!(output_snd, "Error: {}", e);
102                    None
103                }
104            }
105        })
106        .filter(|(_, cp)| {
107            if force {
108                return true;
109            }
110            let cache_path = cp.to_absolute_path(xvc_root);
111            if cache_path.exists() {
112                debug!(output_snd, "Cache path already exists: {}", cache_path);
113                false
114            } else {
115                true
116            }
117        })
118        .collect();
119
120    let (temp_dir, event) = storage
121        .receive(
122            output_snd,
123            xvc_root,
124            cache_paths
125                .values()
126                .cloned()
127                .collect::<Vec<XvcCachePath>>()
128                .as_slice(),
129            opts.force,
130        )
131        .map_err(|e| xvc_core::Error::from(anyhow::anyhow!("Remote error: {}", e)))?;
132
133    let path_sync = PathSync::new();
134    // Move the files from temp dir to cache
135    for (_, cp) in cache_paths {
136        let cache_path = cp.to_absolute_path(xvc_root);
137        let temp_path = temp_dir.temp_cache_path(&cp)?;
138        if temp_path.exists() {
139            uwr!(
140                move_to_cache(&temp_path, &cache_path, &path_sync),
141                output_snd
142            );
143        } else {
144            error!(output_snd, "Could not download {}", cp);
145        }
146    }
147
148    xvc_root.with_store_mut(|store: &mut XvcStore<XvcStorageEvent>| {
149        store.insert(
150            xvc_root.new_entity(),
151            XvcStorageEvent::Receive(event.clone()),
152        );
153        Ok(())
154    })?;
155
156    Ok(())
157}
158
159/// Retrieve files from storage and checkout them into the workspace.
160///
161/// - [fetch] targets from the storage
162/// - [checkout][cmd_checkout] them from storage if `opts.no_checkout` is false. (default)
163pub fn cmd_bring(output_snd: &XvcOutputSender, xvc_root: &XvcRoot, opts: BringCLI) -> Result<()> {
164    fetch(output_snd, xvc_root, &opts)?;
165    if !opts.no_recheck {
166        let recheck_targets = opts.targets.clone();
167
168        let recheck_opts = RecheckCLI {
169            recheck_method: opts.recheck_as,
170            no_parallel: false,
171            force: opts.force,
172            targets: recheck_targets,
173        };
174
175        cmd_recheck(output_snd, xvc_root, recheck_opts)?;
176    }
177
178    Ok(())
179}