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}