rocfl/cmd/
mod.rs

1use std::borrow::Cow;
2use std::fmt::Display;
3use std::io::{self, Read, Write};
4use std::path::Path;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::Arc;
7use std::{fs, process};
8
9use ansi_term::{ANSIGenericString, Style};
10use enum_dispatch::enum_dispatch;
11use log::{error, info};
12#[cfg(feature = "s3")]
13use rusoto_core::Region;
14
15use crate::cmd::opts::*;
16use crate::config::{self, Config};
17use crate::ocfl::{
18    LayoutExtensionName, OcflRepo, Result, RocflError, SpecVersion as OcflSpecVersion,
19    StorageLayout,
20};
21
22mod cmds;
23mod diff;
24mod list;
25pub mod opts;
26mod style;
27mod table;
28mod validate;
29
30const DATE_FORMAT: &str = "%Y-%m-%d %H:%M";
31
32/// Executes a `rocfl` command
33pub fn exec_command(args: &RocflArgs, config: Config) -> Result<()> {
34    let config = resolve_config(args, config);
35    let config = default_values(config)?;
36
37    info!("Resolved configuration: {:?}", config);
38
39    config.validate()?;
40
41    match &args.command {
42        Command::Init(command) => {
43            // init cmd needs to be handled differently because the repo does not exist yet
44            init_repo(command, args, &config)
45        }
46        Command::Config(_command) => edit_config()
47            .map_err(|e| RocflError::General(format!("Failed to edit config file: {}", e))),
48        _ => {
49            let repo = Arc::new(create_repo(&config)?);
50            let terminate = Arc::new(AtomicBool::new(false));
51
52            let repo_ref = repo.clone();
53            let terminate_ref = terminate.clone();
54
55            ctrlc::set_handler(move || {
56                if terminate_ref.load(Ordering::Acquire) {
57                    error!("Force quitting. If a write operation was in progress, it is possible the resource was left in an inconsistent state.");
58                    process::exit(1);
59                } else {
60                    println("Stopping rocfl. If in the middle of a write operation, please wait for it to gracefully complete.");
61                    terminate_ref.store(true, Ordering::Release);
62                    repo_ref.close();
63                }
64            })?;
65
66            args.command.exec(
67                &repo,
68                GlobalArgs::new(args.quiet, args.verbose, args.no_styles),
69                &config,
70                &terminate,
71            )
72        }
73    }
74}
75
76/// Trait executing a CLI command
77#[enum_dispatch]
78trait Cmd {
79    /// Execute the command
80    fn exec(
81        &self,
82        repo: &OcflRepo,
83        args: GlobalArgs,
84        config: &Config,
85        terminate: &AtomicBool,
86    ) -> Result<()>;
87}
88
89struct GlobalArgs {
90    quiet: bool,
91    _verbose: bool,
92    no_styles: bool,
93}
94
95impl GlobalArgs {
96    fn new(quiet: bool, verbose: bool, no_styles: bool) -> Self {
97        Self {
98            quiet,
99            _verbose: verbose,
100            no_styles,
101        }
102    }
103}
104
105fn println(value: impl Display) {
106    let _ = writeln!(io::stdout(), "{}", value);
107}
108
109fn paint<'b, I, S: 'b + ToOwned + ?Sized>(
110    no_styles: bool,
111    style: Style,
112    text: I,
113) -> ANSIGenericString<'b, S>
114where
115    I: Into<Cow<'b, S>>,
116    <S as ToOwned>::Owned: std::fmt::Debug,
117{
118    if no_styles {
119        style::DEFAULT.paint(text)
120    } else {
121        style.paint(text)
122    }
123}
124
125pub fn init_repo(cmd: &InitCmd, args: &RocflArgs, config: &Config) -> Result<()> {
126    let spec_version = map_spec_version(cmd.spec_version);
127
128    if is_s3(config) {
129        #[cfg(not(feature = "s3"))]
130        return Err(RocflError::General(
131            "This binary was not compiled with S3 support.".to_string(),
132        ));
133
134        #[cfg(feature = "s3")]
135        let _ = init_s3_repo(
136            config,
137            spec_version,
138            create_layout(cmd.layout, cmd.config_file.as_deref())?,
139        )?;
140    } else {
141        let _ = OcflRepo::init_fs_repo(
142            config.root.as_ref().unwrap(),
143            config.staging_root.as_ref().map(Path::new),
144            spec_version,
145            create_layout(cmd.layout, cmd.config_file.as_deref())?,
146        )?;
147    }
148
149    if !args.quiet {
150        println(format!(
151            "Initialized OCFL {} repository with layout {}",
152            cmd.spec_version, cmd.layout
153        ));
154    }
155
156    Ok(())
157}
158
159fn create_repo(config: &Config) -> Result<OcflRepo> {
160    if is_s3(config) {
161        #[cfg(not(feature = "s3"))]
162        return Err(RocflError::General(
163            "This binary was not compiled with S3 support.".to_string(),
164        ));
165
166        #[cfg(feature = "s3")]
167        create_s3_repo(config)
168    } else {
169        OcflRepo::fs_repo(
170            config.root.as_ref().unwrap(),
171            config.staging_root.as_ref().map(Path::new),
172        )
173    }
174}
175
176fn create_layout(layout_name: Layout, config_file: Option<&Path>) -> Result<Option<StorageLayout>> {
177    let config_bytes = match read_layout_config(config_file) {
178        Ok(bytes) => bytes,
179        Err(e) => {
180            return Err(RocflError::InvalidValue(format!(
181                "Failed to read layout config file: {}",
182                e
183            )));
184        }
185    };
186
187    let layout = match layout_name {
188        Layout::None => None,
189        Layout::FlatDirect => Some(StorageLayout::new(
190            LayoutExtensionName::FlatDirectLayout,
191            config_bytes.as_deref(),
192        )?),
193        Layout::HashedNTuple => Some(StorageLayout::new(
194            LayoutExtensionName::HashedNTupleLayout,
195            config_bytes.as_deref(),
196        )?),
197        Layout::HashedNTupleObjectId => Some(StorageLayout::new(
198            LayoutExtensionName::HashedNTupleObjectIdLayout,
199            config_bytes.as_deref(),
200        )?),
201        Layout::FlatOmitPrefix => Some(StorageLayout::new(
202            LayoutExtensionName::FlatOmitPrefixLayout,
203            config_bytes.as_deref(),
204        )?),
205        Layout::NTupleOmitPrefix => Some(StorageLayout::new(
206            LayoutExtensionName::NTupleOmitPrefixLayout,
207            config_bytes.as_deref(),
208        )?),
209    };
210
211    Ok(layout)
212}
213
214fn read_layout_config(config_file: Option<&Path>) -> Result<Option<Vec<u8>>> {
215    let mut bytes = Vec::new();
216
217    if let Some(file) = config_file {
218        let _ = fs::File::open(file)?.read_to_end(&mut bytes)?;
219        return Ok(Some(bytes));
220    }
221
222    Ok(None)
223}
224
225#[cfg(feature = "s3")]
226fn create_s3_repo(config: &Config) -> Result<OcflRepo> {
227    let region = resolve_region(config)?;
228
229    OcflRepo::s3_repo(
230        region,
231        config.bucket.as_ref().unwrap(),
232        config.root.as_deref(),
233        config.staging_root.as_ref().unwrap(),
234        config.profile.as_deref(),
235    )
236}
237
238#[cfg(feature = "s3")]
239fn init_s3_repo(
240    config: &Config,
241    spec_version: OcflSpecVersion,
242    layout: Option<StorageLayout>,
243) -> Result<OcflRepo> {
244    let region = resolve_region(config)?;
245
246    OcflRepo::init_s3_repo(
247        region,
248        config.bucket.as_ref().unwrap(),
249        config.root.as_deref(),
250        config.profile.as_deref(),
251        config.staging_root.as_ref().unwrap(),
252        spec_version,
253        layout,
254    )
255}
256
257#[cfg(feature = "s3")]
258fn resolve_region(config: &Config) -> Result<Region> {
259    Ok(match config.endpoint.is_some() {
260        true => Region::Custom {
261            name: config.region.as_ref().unwrap().to_owned(),
262            endpoint: config.endpoint.as_ref().unwrap().to_owned(),
263        },
264        false => config.region.as_ref().unwrap().parse()?,
265    })
266}
267
268fn resolve_config(args: &RocflArgs, mut config: Config) -> Config {
269    if args.root.is_some() {
270        config.root = args.root.clone();
271    }
272    if args.staging_root.is_some() {
273        config.staging_root = args.staging_root.clone();
274    }
275    if args.bucket.is_some() {
276        config.bucket = args.bucket.clone();
277    }
278    if args.region.is_some() {
279        config.region = args.region.clone();
280    }
281    if args.endpoint.is_some() {
282        config.endpoint = args.endpoint.clone();
283    }
284    if args.profile.is_some() {
285        config.profile = args.profile.clone()
286    }
287
288    if let Command::Commit(commit) = &args.command {
289        if commit.user_name.is_some() {
290            config.author_name = commit.user_name.clone();
291        }
292        if commit.user_address.is_some() {
293            config.author_address = commit.user_address.clone();
294        }
295    }
296
297    if let Command::Upgrade(commit) = &args.command {
298        if commit.user_name.is_some() {
299            config.author_name = commit.user_name.clone();
300        }
301        if commit.user_address.is_some() {
302            config.author_address = commit.user_address.clone();
303        }
304    }
305
306    config
307}
308
309fn default_values(mut config: Config) -> Result<Config> {
310    if is_s3(&config) {
311        if config.staging_root.is_none() {
312            config.staging_root = Some(config::s3_staging_path(&config)?);
313        }
314    } else if config.root.is_none() {
315        config.root = Some(".".to_string());
316    }
317
318    Ok(config)
319}
320
321fn is_s3(config: &Config) -> bool {
322    config.bucket.is_some()
323}
324
325fn edit_config() -> Result<()> {
326    match config::config_path() {
327        Some(config_path) => {
328            if !config_path.exists() {
329                fs::create_dir_all(config_path.parent().unwrap())?;
330                let mut file = fs::File::create(&config_path)?;
331                write!(
332                    file,
333                    "{}",
334                    include_str!("../../resources/main/files/config.toml")
335                )?;
336            }
337
338            edit::edit_file(&config_path)?;
339            Ok(())
340        }
341        None => Err(RocflError::General(
342            "Failed to find rocfl config".to_string(),
343        )),
344    }
345}
346
347fn map_spec_version(spec_version: SpecVersion) -> OcflSpecVersion {
348    match spec_version {
349        SpecVersion::Ocfl1_0 => OcflSpecVersion::Ocfl1_0,
350        SpecVersion::Ocfl1_1 => OcflSpecVersion::Ocfl1_1,
351    }
352}