xvc_storage/
lib.rs

1//! Xvc storage management commands.
2//!
3//! Contains several modules to implement connection, upload and download of files to a storage.
4//! Most of the functionality is behind feature flags that are on by default. If you want to customize the functionality
5//! of this crate, you can disable the features you don't need.
6#![warn(missing_docs)]
7#![forbid(unsafe_code)]
8pub mod error;
9pub mod storage;
10
11use std::path::PathBuf;
12use std::str::FromStr;
13
14pub use crate::error::{Error, Result};
15use clap::{Parser, Subcommand};
16
17use clap_complete::ArgValueCompleter;
18use derive_more::Display;
19use storage::{get_storage_record, storage_identifier_completer};
20pub use storage::{
21    XvcLocalStorage, XvcStorage, XvcStorageEvent, XvcStorageGuid, XvcStorageOperations,
22};
23
24use xvc_core::XvcStore;
25
26use xvc_core::XvcRoot;
27use xvc_core::{output, XvcOutputSender};
28
29/// Storage (on the cloud) management commands
30#[derive(Debug, Parser, Clone)]
31#[command(name = "storage", about = "")]
32pub struct StorageCLI {
33    /// Subcommand for storage management
34    #[command(subcommand)]
35    pub subcommand: StorageSubCommand,
36}
37
38/// storage subcommands
39#[derive(Debug, Clone, Parser)]
40#[command(about = "Manage storages containing tracked file content")]
41pub enum StorageSubCommand {
42    /// List all configured storages
43    #[command(visible_aliases=&["l"])]
44    List,
45
46    /// Remove a storage configuration.
47    ///
48    /// This doesn't delete any files in the storage.
49    #[command(visible_aliases=&["R"])]
50    Remove {
51        /// Name of the storage to be deleted
52        #[arg(short, long, add = ArgValueCompleter::new(storage_identifier_completer))]
53        name: String,
54    },
55
56    /// Configure a new storage
57    #[command(subcommand, visible_aliases=&["n"])]
58    New(StorageNewSubCommand),
59}
60
61/// Add a new storage
62#[derive(Debug, Clone, Subcommand)]
63#[command()]
64pub enum StorageNewSubCommand {
65    /// Add a new local storage
66    ///
67    /// A local storage is a directory accessible from the local file system.
68    /// Xvc will use common file operations for this directory without accessing the network.
69    #[command()]
70    Local {
71        /// Directory (outside the repository) to be set as a storage
72        #[arg(long, value_hint=clap::ValueHint::DirPath)]
73        path: PathBuf,
74
75        /// Name of the storage.
76        ///
77        /// Recommended to keep this name unique to refer easily.
78        #[arg(long, short)]
79        name: String,
80    },
81
82    /// Add a new generic storage.
83    ///
84    /// ⚠️ Please note that this is an advanced method to configure storages.
85    /// You may damage your repository and local and storage files with incorrect configurations.
86    ///
87    /// Please see https://docs.xvc.dev/ref/xvc-storage-new-generic.html for examples and make
88    /// necessary backups.
89    #[command()]
90    Generic {
91        /// Name of the storage.
92        ///
93        /// Recommended to keep this name unique to refer easily.
94        #[arg(long = "name", short = 'n')]
95        name: String,
96        /// Command to initialize the storage.
97        /// This command is run once after defining the storage.
98        ///
99        /// You can use {URL} and {STORAGE_DIR}  as shortcuts.
100        #[arg(long = "init", short = 'i', value_hint=clap::ValueHint::CommandString)]
101        init_command: String,
102        /// Command to list the files in storage
103        ///
104        /// You can use {URL} and {STORAGE_DIR} placeholders and define values for these with --url and --storage_dir options.
105        #[arg(long = "list", short = 'l', value_hint=clap::ValueHint::CommandString)]
106        list_command: String,
107        /// Command to download a file from storage.
108        ///
109        /// You can use {URL} and {STORAGE_DIR} placeholders and define values for these with --url and --storage_dir options.
110        #[arg(long = "download", short = 'd', value_hint=clap::ValueHint::CommandString)]
111        download_command: String,
112        /// Command to upload a file to storage.
113        ///
114        /// You can use {URL} and {STORAGE_DIR} placeholders and define values for these with --url and --storage_dir options.
115        #[arg(long = "upload", short = 'u',value_hint=clap::ValueHint::CommandString )]
116        upload_command: String,
117        /// The delete command to remove a file from storage
118        /// You can use {URL} and {STORAGE_DIR} placeholders and define values for these with --url and --storage_dir options.
119        #[arg(long = "delete", short = 'D',value_hint=clap::ValueHint::CommandString )]
120        delete_command: String,
121        /// Number of maximum processes to run simultaneously
122        #[arg(long = "processes", short = 'M', default_value_t = 1)]
123        max_processes: usize,
124        /// You can set a string to replace {URL} placeholder in commands
125        #[arg(long, value_hint=clap::ValueHint::Url)]
126        url: Option<String>,
127        /// You can set a string to replace {STORAGE_DIR} placeholder in commands
128        #[arg(long)]
129        storage_dir: Option<String>,
130    },
131
132    /// Add a new rsync storages
133    ///
134    /// Uses rsync in separate processes to communicate.
135    /// This can be used when you already have an SSH/Rsync connection.
136    /// It doesn't prompt for any passwords. The connection must be set up with ssh keys beforehand.
137    #[command()]
138    Rsync {
139        /// Name of the storage.
140        ///
141        /// Recommended to keep this name unique to refer easily.
142        #[arg(long = "name", short = 'n')]
143        name: String,
144        /// Hostname for the connection in the form host.example.com  (without @, : or protocol)
145        #[arg(long, value_hint=clap::ValueHint::Hostname)]
146        host: String,
147        /// Port number for the connection in the form 22.
148        /// Doesn't add port number to connection string if not given.
149        #[arg(long)]
150        port: Option<usize>,
151        /// User name for the connection, the part before @ in user@example.com (without @,
152        /// hostname).
153        /// User name isn't included in connection strings if not given.
154        #[arg(long, value_hint=clap::ValueHint::Username)]
155        user: Option<String>,
156        /// storage directory in the host to store the files.
157        #[arg(long)]
158        storage_dir: String,
159    },
160
161    #[cfg(feature = "rclone")]
162    /// Add a new rclone storage
163    ///
164    /// Uses the rclone configuration to connect to the storage. The remotestorage must already be
165    /// configure with `rclone config`.
166    #[command()]
167    Rclone {
168        /// Name of the storage
169        ///
170        /// This must be unique among all storages of the project
171        #[arg(long = "name", short = 'n')]
172        name: String,
173
174        /// The name of the remote in rclone configuration
175        ///
176        /// This is the "remote" part in "remote://dir/" URL.
177        #[arg(long)]
178        remote_name: String,
179
180        /// The directory in the remote to store the files.
181        ///
182        /// This is the "dir" part in "remote://dir/" URL.
183        #[arg(long, default_value = "")]
184        storage_prefix: String,
185    },
186
187    #[cfg(feature = "s3")]
188    /// Add a new S3 storage
189    ///
190    /// Reads credentials from `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables.
191    /// Alternatively you can use `XVC_STORAGE_ACCESS_KEY_ID_<storage_name>` and
192    /// `XVC_STORAGE_SECRET_ACCESS_KEY_<storage_name>` environment variables if you have multiple storages of this type.
193    #[command()]
194    S3 {
195        /// Name of the storage
196        ///
197        /// This must be unique among all storages of the project
198        #[arg(long = "name", short = 'n')]
199        name: String,
200        /// You can set a directory in the bucket with this prefix
201        #[arg(long, default_value = "")]
202        storage_prefix: String,
203        /// S3 bucket name
204        #[arg(long)]
205        bucket_name: String,
206        /// AWS region
207        #[arg(long)]
208        region: String,
209    },
210
211    #[cfg(feature = "minio")]
212    /// Add a new Minio storage
213    ///
214    /// Reads credentials from `MINIO_ACCESS_KEY` and `MINIO_SECRET_ACCESS_KEY` environment variables.
215    /// Alternatively you can use `XVC_STORAGE_ACCESS_KEY_ID_<storage_name>` and
216    /// `XVC_STORAGE_SECRET_ACCESS_KEY_<storage_name>` environment variables if you have multiple storages of this type.
217    #[command()]
218    Minio {
219        /// Name of the storage
220        ///
221        /// This must be unique among all storages of the project
222        #[arg(long = "name", short = 'n')]
223        name: String,
224        /// Minio server url in the form https://myserver.example.com:9090
225        #[arg(long, value_hint=clap::ValueHint::Url)]
226        endpoint: String,
227        /// Bucket name
228        #[arg(long)]
229        bucket_name: String,
230        /// Region of the server
231        #[arg(long)]
232        region: String,
233        /// You can set a directory in the bucket with this prefix
234        #[arg(long, default_value = "")]
235        storage_prefix: String,
236    },
237
238    #[cfg(feature = "digital-ocean")]
239    /// Add a new Digital Ocean storage
240    ///
241    /// Reads credentials from `DIGITAL_OCEAN_ACCESS_KEY_ID` and `DIGITAL_OCEAN_SECRET_ACCESS_KEY` environment variables.
242    /// Alternatively you can use `XVC_STORAGE_ACCESS_KEY_ID_<storage_name>` and
243    /// `XVC_STORAGE_SECRET_ACCESS_KEY_<storage_name>` environment variables if you have multiple storages of this type.
244    #[command()]
245    DigitalOcean {
246        /// Name of the storage
247        ///
248        /// This must be unique among all storages of the project
249        #[arg(long = "name", short = 'n')]
250        name: String,
251        /// Bucket name
252        #[arg(long)]
253        bucket_name: String,
254        /// Region of the server
255        #[arg(long)]
256        region: String,
257        /// You can set a directory in the bucket with this prefix
258        #[arg(long, default_value = "")]
259        storage_prefix: String,
260    },
261
262    #[cfg(feature = "r2")]
263    /// Add a new R2 storage
264    ///
265    /// Reads credentials from `R2_ACCESS_KEY_ID` and `R2_SECRET_ACCESS_KEY` environment variables.
266    /// Alternatively you can use `XVC_STORAGE_ACCESS_KEY_ID_<storage_name>` and
267    /// `XVC_STORAGE_SECRET_ACCESS_KEY_<storage_name>` environment variables if you have multiple storages of this type.
268    #[command()]
269    R2 {
270        /// Name of the storage
271        ///
272        /// This must be unique among all storages of the project
273        #[arg(long = "name", short = 'n')]
274        name: String,
275        /// R2 account ID
276        #[arg(long)]
277        account_id: String,
278        /// Bucket name
279        #[arg(long)]
280        bucket_name: String,
281        /// You can set a directory in the bucket with this prefix
282        #[arg(long, default_value = "")]
283        storage_prefix: String,
284    },
285
286    #[cfg(feature = "gcs")]
287    /// Add a new Google Cloud Storage storage
288    ///
289    /// Reads credentials from `GCS_ACCESS_KEY_ID` and `GCS_SECRET_ACCESS_KEY` environment variables.
290    /// Alternatively you can use `XVC_STORAGE_ACCESS_KEY_ID_<storage_name>` and
291    /// `XVC_STORAGE_SECRET_ACCESS_KEY_<storage_name>` environment variables if you have multiple storages of this type.
292    #[command()]
293    Gcs {
294        /// Name of the storage
295        ///
296        /// This must be unique among all storages of the project
297        #[arg(long = "name", short = 'n')]
298        name: String,
299        /// Bucket name
300        #[arg(long)]
301        bucket_name: String,
302        /// Region of the server, e.g., europe-west3
303        #[arg(long)]
304        region: String,
305        /// You can set a directory in the bucket with this prefix
306        #[arg(long, default_value = "")]
307        storage_prefix: String,
308    },
309
310    #[cfg(feature = "wasabi")]
311    /// Add a new Wasabi storage
312    ///
313    /// Reads credentials from `WASABI_ACCESS_KEY_ID` and `WASABI_SECRET_ACCESS_KEY` environment variables.
314    /// Alternatively you can use `XVC_STORAGE_ACCESS_KEY_ID_<storage_name>` and
315    /// `XVC_STORAGE_SECRET_ACCESS_KEY_<storage_name>` environment variables if you have multiple storages of this type.
316    #[command()]
317    Wasabi {
318        /// Name of the storage
319        ///
320        /// This must be unique among all storages of the project
321        #[arg(long = "name", short = 'n')]
322        name: String,
323        /// Bucket name
324        #[arg(long)]
325        bucket_name: String,
326        /// Endpoint for the server, complete with the region if there is
327        ///
328        /// e.g. for eu-central-1 region, use s3.eu-central-1.wasabisys.com as the endpoint.
329        #[arg(long, default_value = "s3.wasabisys.com")]
330        endpoint: String,
331        /// You can set a directory in the bucket with this prefix
332        #[arg(long, default_value = "")]
333        storage_prefix: String,
334    },
335}
336
337/// Specifies a storage by either a name or a GUID.
338///
339/// Name is specified with `--name` option of most of the storage types.
340/// Guid is generated or loaded in [XvcStorageOperations::init] operations and
341/// kept in Storage structs.
342#[derive(Debug, Clone, PartialEq, Eq, Display)]
343pub enum StorageIdentifier {
344    /// Name of the storage
345    Name(String),
346    /// GUID of the storage
347    Uuid(uuid::Uuid),
348}
349
350impl FromStr for StorageIdentifier {
351    /// This tries to parse `s` as a [Uuid]. If it can't it
352    /// considers it a name.
353    ///
354    /// The only way this fails is when `s` cannot be converted to string.
355    /// That's very unlikely.
356    fn from_str(s: &str) -> Result<Self> {
357        match uuid::Uuid::parse_str(s) {
358            Ok(uuid) => Ok(Self::Uuid(uuid)),
359            Err(_) => Ok(Self::Name(s.to_string())),
360        }
361    }
362
363    type Err = crate::Error;
364}
365
366/// Entry point for `xvc storage` group of commands.
367///
368/// It matches the subcommand in [StorageCLI::subcommand] and runs the
369/// appropriate function.
370///
371/// Other arguments are passed to subcommands.
372pub fn cmd_storage(
373    input: std::io::StdinLock,
374    output_snd: &XvcOutputSender,
375    xvc_root: &XvcRoot,
376    opts: StorageCLI,
377) -> Result<()> {
378    match opts.subcommand {
379        StorageSubCommand::List => cmd_storage_list(input, output_snd, xvc_root),
380        StorageSubCommand::Remove { name } => cmd_storage_remove(input, output_snd, xvc_root, name),
381        StorageSubCommand::New(new) => cmd_storage_new(input, output_snd, xvc_root, new),
382    }
383}
384
385/// Configure a new storage.
386///
387/// The available storages and their configuration is dependent to compilation.
388/// In minimum, it includes [local][cmd_storage_new_local] and
389/// [generic][cmd_storage_new_generic].
390///
391/// This function matches [StorageNewSubCommand] and calls the appropriate
392/// function from child modules. Most of the available options are behind
393/// feature flags, that also guard the modules.
394fn cmd_storage_new(
395    input: std::io::StdinLock,
396    output_snd: &XvcOutputSender,
397    xvc_root: &XvcRoot,
398    sc: StorageNewSubCommand,
399) -> Result<()> {
400    match sc {
401        StorageNewSubCommand::Local { path, name } => {
402            storage::local::cmd_storage_new_local(input, output_snd, xvc_root, path, name)
403        }
404        StorageNewSubCommand::Generic {
405            name,
406            init_command,
407            list_command,
408            download_command,
409            upload_command,
410            delete_command,
411            max_processes,
412            url,
413            storage_dir,
414        } => storage::generic::cmd_storage_new_generic(
415            input,
416            output_snd,
417            xvc_root,
418            name,
419            url,
420            storage_dir,
421            max_processes,
422            init_command,
423            list_command,
424            download_command,
425            upload_command,
426            delete_command,
427        ),
428
429        #[cfg(feature = "rclone")]
430        StorageNewSubCommand::Rclone {
431            name,
432            remote_name,
433            storage_prefix,
434        } => {
435            storage::rclone::cmd_new_rclone(output_snd, xvc_root, name, remote_name, storage_prefix)
436        }
437
438        #[cfg(feature = "s3")]
439        StorageNewSubCommand::S3 {
440            name,
441            storage_prefix,
442            bucket_name,
443            region,
444        } => storage::s3::cmd_new_s3(
445            output_snd,
446            xvc_root,
447            name,
448            region,
449            bucket_name,
450            storage_prefix,
451        ),
452        #[cfg(feature = "minio")]
453        StorageNewSubCommand::Minio {
454            name,
455            endpoint,
456            bucket_name,
457            storage_prefix,
458            region,
459        } => storage::minio::cmd_new_minio(
460            input,
461            output_snd,
462            xvc_root,
463            name,
464            endpoint,
465            bucket_name,
466            region,
467            storage_prefix,
468        ),
469        #[cfg(feature = "digital-ocean")]
470        StorageNewSubCommand::DigitalOcean {
471            name,
472            bucket_name,
473            region,
474            storage_prefix,
475        } => storage::digital_ocean::cmd_new_digital_ocean(
476            input,
477            output_snd,
478            xvc_root,
479            name,
480            bucket_name,
481            region,
482            storage_prefix,
483        ),
484        #[cfg(feature = "r2")]
485        StorageNewSubCommand::R2 {
486            name,
487            account_id,
488            bucket_name,
489            storage_prefix,
490        } => storage::r2::cmd_new_r2(
491            input,
492            output_snd,
493            xvc_root,
494            name,
495            account_id,
496            bucket_name,
497            storage_prefix,
498        ),
499        #[cfg(feature = "gcs")]
500        StorageNewSubCommand::Gcs {
501            name,
502            bucket_name,
503            region,
504            storage_prefix,
505        } => storage::gcs::cmd_new_gcs(
506            input,
507            output_snd,
508            xvc_root,
509            name,
510            bucket_name,
511            region,
512            storage_prefix,
513        ),
514        #[cfg(feature = "wasabi")]
515        StorageNewSubCommand::Wasabi {
516            name,
517            bucket_name,
518            endpoint,
519            storage_prefix,
520        } => storage::wasabi::cmd_new_wasabi(
521            output_snd,
522            xvc_root,
523            name,
524            bucket_name,
525            endpoint,
526            storage_prefix,
527        ),
528        StorageNewSubCommand::Rsync {
529            name,
530            host,
531            port,
532            user,
533            storage_dir,
534        } => {
535            storage::rsync::cmd_new_rsync(output_snd, xvc_root, name, host, port, user, storage_dir)
536        }
537    }
538}
539
540/// Removes a storage from the configurations.
541///
542/// This doesn't remove the history associated with them.
543fn cmd_storage_remove(
544    _input: std::io::StdinLock,
545    output_snd: &XvcOutputSender,
546    xvc_root: &XvcRoot,
547    identifier: String,
548) -> Result<()> {
549    let identifier = StorageIdentifier::from_str(&identifier)?;
550    let storage = get_storage_record(output_snd, xvc_root, &identifier)?;
551    xvc_root.with_store_mut::<XvcStorage>(|store| {
552        let filtered = store.filter(|_xe, xs| xs.guid() == storage.guid());
553        if let Some((xe, xs)) = filtered.first() {
554            store.remove(*xe);
555            output!(output_snd, "Removed Storage {xs}");
556            Ok(())
557        } else {
558            Err(anyhow::anyhow!("Cannot find storage with identifier: {identifier}").into())
559        }
560    })?;
561    Ok(())
562}
563
564/// Lists all available storages.
565///
566/// It runs [XvcStorage::display] and lists all elements line by line to
567/// `output_snd`.
568fn cmd_storage_list(
569    _input: std::io::StdinLock,
570    output_snd: &XvcOutputSender,
571    xvc_root: &XvcRoot,
572) -> Result<()> {
573    let store: XvcStore<XvcStorage> = xvc_root.load_store()?;
574
575    for (_, s) in store.iter() {
576        output!(output_snd, "{}\n", s);
577    }
578
579    Ok(())
580}