1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
//! Bring files from external storages to workspace.
//!
//! - [BringCLI] defines the command line options.
//!
//! - [cmd_bring]  is the entry point for the command.
//!   Uses [fetch] and [crate::recheck::cmd_recheck] to bring the file and copy/link it to the
//!   workspace.

use crate::common::{load_targets_from_store, move_to_cache};

use crate::{
    recheck::{cmd_recheck, RecheckCLI},
    Result,
};

use clap::Parser;

use xvc_core::{ContentDigest, RecheckMethod, XvcCachePath, XvcFileType, XvcMetadata, XvcRoot};
use xvc_ecs::{HStore, XvcStore};
use xvc_logging::{debug, uwr, warn, watch, XvcOutputSender};

use xvc_storage::XvcStorageEvent;
use xvc_storage::{storage::get_storage_record, StorageIdentifier, XvcStorageOperations};
use xvc_walker::PathSync;

/// Bring (download, pull, fetch) files from storage.
///
/// You can configure a new storage with [`xvc storage new`][xvc_storage::new] and use it to
/// download and upload tracked files.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
#[command(rename_all = "kebab-case")]
pub struct BringCLI {
    /// Storage name or guid to send the files
    #[arg(long, short, alias = "from")]
    storage: StorageIdentifier,

    /// Force even if the files are already present in the workspace
    #[arg(long)]
    force: bool,

    /// Don't recheck (checkout) after bringing the file to cache.
    ///
    /// This makes the command similar to `git fetch` in Git.
    /// It just updates the cache, and doesn't copy/link the file to workspace.
    #[arg(long)]
    no_recheck: bool,

    /// Recheck (checkout) the file in one of the four alternative ways.
    /// (See `xvc file recheck`) and [RecheckMethod]
    #[arg(long, alias = "as")]
    recheck_as: Option<RecheckMethod>,

    /// Targets to bring from the storage
    #[arg()]
    targets: Option<Vec<String>>,
}

/// Download files in `opts.targets` from `opts.storage` to cache.
///
/// - Retrieves the storage record from `xvc_root`.
/// - Expands globs in `opts.targets`.
/// - Gets the corresponding cache path for each file target.
/// - Calls `storage.receive` for each of these targets.
pub fn fetch(output_snd: &XvcOutputSender, xvc_root: &XvcRoot, opts: &BringCLI) -> Result<()> {
    let storage = get_storage_record(output_snd, xvc_root, &opts.storage)?;

    let current_dir = xvc_root.config().current_dir()?;
    let targets = load_targets_from_store(output_snd, xvc_root, current_dir, &opts.targets)?;
    let force = opts.force;
    watch!(targets);

    let target_xvc_metadata = xvc_root
        .load_store::<XvcMetadata>()?
        .subset(targets.keys().copied())?;

    let target_file_xvc_metadata =
        target_xvc_metadata.filter(|_, xmd| xmd.file_type == XvcFileType::File);

    let target_files = targets.subset(target_file_xvc_metadata.keys().copied())?;

    // Get all cache paths for these paths
    let content_digest_store: XvcStore<ContentDigest> = xvc_root.load_store()?;

    let target_content_digests = content_digest_store.subset(target_files.keys().copied())?;
    watch!(target_content_digests);

    assert! {
        target_content_digests.len() == target_files.len(),
        "All files should have a content digest"
    }

    let cache_paths: HStore<XvcCachePath> = target_content_digests
        .iter()
        .filter_map(|(xe, cd)| {
            let xvc_path = target_files.get(xe).unwrap();
            match XvcCachePath::new(xvc_path, cd) {
                Ok(cp) => Some((*xe, cp)),
                Err(e) => {
                    warn!(output_snd, "Error: {}", e);
                    None
                }
            }
        })
        .filter(|(_, cp)| {
            if force {
                return true;
            }
            let cache_path = cp.to_absolute_path(xvc_root);
            if cache_path.exists() {
                debug!(output_snd, "Cache path already exists: {}", cache_path);
                false
            } else {
                true
            }
        })
        .collect();

    watch!(cache_paths);

    let (temp_dir, event) = storage
        .receive(
            output_snd,
            xvc_root,
            cache_paths
                .values()
                .cloned()
                .collect::<Vec<XvcCachePath>>()
                .as_slice(),
            opts.force,
        )
        .map_err(|e| xvc_core::Error::from(anyhow::anyhow!("Remote error: {}", e)))?;

    watch!(temp_dir);
    watch!(event);

    let path_sync = PathSync::new();
    // Move the files from temp dir to cache
    for (_, cp) in cache_paths {
        let cache_path = cp.to_absolute_path(xvc_root);
        let temp_path = temp_dir.temp_cache_path(&cp)?;
        uwr!(
            move_to_cache(&temp_path, &cache_path, &path_sync),
            output_snd
        );
    }

    xvc_root.with_store_mut(|store: &mut XvcStore<XvcStorageEvent>| {
        store.insert(
            xvc_root.new_entity(),
            XvcStorageEvent::Receive(event.clone()),
        );
        Ok(())
    })?;

    Ok(())
}

/// Retrieve files from storage and checkout them into the workspace.
///
/// - [fetch] targets from the storage
/// - [checkout][cmd_checkout] them from storage if `opts.no_checkout` is false. (default)
pub fn cmd_bring(output_snd: &XvcOutputSender, xvc_root: &XvcRoot, opts: BringCLI) -> Result<()> {
    fetch(output_snd, xvc_root, &opts)?;
    watch!("Fetch completed");
    if !opts.no_recheck {
        let recheck_targets = opts.targets.clone();
        watch!(recheck_targets);

        let recheck_opts = RecheckCLI {
            recheck_method: opts.recheck_as,
            no_parallel: false,
            force: opts.force,
            targets: recheck_targets,
        };

        cmd_recheck(output_snd, xvc_root, recheck_opts)?;
    }

    Ok(())
}