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
32pub 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_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#[enum_dispatch]
78trait Cmd {
79 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}