xvc_file/send/
mod.rs

1//! Home of the `xvc file send` command
2//!
3//! - [`cmd_send`] implements the command
4//! - [`SendCLI`] is the command line interface
5use crate::common::load_targets_from_store;
6use crate::Result;
7
8use clap::Parser;
9
10use clap_complete::ArgValueCompleter;
11use xvc_core::{
12    util::completer::xvc_path_completer, ContentDigest, XvcCachePath, XvcFileType, XvcMetadata,
13    XvcRoot,
14};
15use xvc_core::{HStore, XvcStore};
16use xvc_core::{error, XvcOutputSender};
17use xvc_storage::{
18    storage::{get_storage_record, storage_identifier_completer},
19    StorageIdentifier, XvcStorageOperations,
20};
21
22/// Send (upload) tracked files to storage
23///
24/// When you define a new storage with [`xvc storage new`][xvc_storage::new] set of commands, you
25/// can send the tracked files with this.
26///
27/// Sent files are placed in a directory structure similar to the local cache.
28#[derive(Debug, Clone, PartialEq, Eq, Parser)]
29#[command(rename_all = "kebab-case")]
30pub struct SendCLI {
31    /// Storage name or guid to send the files
32    #[arg(long, short, alias = "to", add = ArgValueCompleter::new(storage_identifier_completer))]
33    storage: StorageIdentifier,
34
35    /// Force even if the files are already present in the storage
36    #[arg(long)]
37    force: bool,
38
39    /// Targets to send/push/upload to storage
40    #[arg(add = ArgValueCompleter::new(xvc_path_completer))]
41    targets: Option<Vec<String>>,
42}
43
44/// Send a targets in `opts.targets` in `xvc_root`  to `opt.remote`
45pub fn cmd_send(output_snd: &XvcOutputSender, xvc_root: &XvcRoot, opts: SendCLI) -> Result<()> {
46    let storage = get_storage_record(output_snd, xvc_root, &opts.storage)?;
47    let current_dir = xvc_root.config().current_dir()?;
48    let targets = load_targets_from_store(output_snd, xvc_root, current_dir, &opts.targets)?;
49
50    let target_file_xvc_metadata = xvc_root
51        .load_store::<XvcMetadata>()?
52        .subset(targets.keys().copied())?
53        .filter(|_, xmd| xmd.file_type == XvcFileType::File)
54        .cloned();
55
56    let target_files = targets.subset(target_file_xvc_metadata.keys().copied())?;
57
58    // Get all cache paths for these paths
59    let content_digest_store: XvcStore<ContentDigest> = xvc_root.load_store()?;
60
61    let target_content_digests = content_digest_store.subset(target_files.keys().copied())?;
62
63    assert! {
64        target_content_digests.len() == target_files.len(),
65        "All files should have a content digest"
66    }
67
68    let cache_paths: HStore<XvcCachePath> = target_content_digests
69        .iter()
70        .filter_map(|(xe, content_digest)| {
71            target_files.get(xe).and_then(|xvc_path| {
72                XvcCachePath::new(xvc_path, content_digest)
73                    .map_err(|e| {
74                        error!(output_snd, "{e}");
75                        e
76                    })
77                    .ok()
78                    .map(|cache_path| (*xe, cache_path))
79            })
80        })
81        .collect();
82
83    storage
84        .send(
85            output_snd,
86            xvc_root,
87            // TODO: Change interface of XvcStorage to get an HStore instead of Vec
88            cache_paths
89                .values()
90                .cloned()
91                .collect::<Vec<XvcCachePath>>()
92                .as_slice(),
93            opts.force,
94        )
95        .map_err(|e| xvc_core::Error::from(anyhow::anyhow!("Remote error: {}", e)))?;
96
97    Ok(())
98}