nuts_tool/cli/archive/
add.rs1pub mod dir;
24pub mod file;
25pub mod symlink;
26
27use anyhow::Result;
28use chrono::offset::LocalResult;
29use chrono::{DateTime, Local, NaiveDateTime, TimeZone, Utc};
30use clap::builder::{TypedValueParser, ValueParserFactory};
31use clap::{value_parser, Arg, ArgAction, Args, Command, Subcommand};
32use log::debug;
33use std::ffi::OsStr;
34use std::path::PathBuf;
35
36use crate::archive::append_recursive;
37use crate::cli::archive::add::dir::ArchiveAddDirectoryArgs;
38use crate::cli::archive::add::file::ArchiveAddFileArgs;
39use crate::cli::archive::add::symlink::ArchiveAddSymlinkArgs;
40use crate::cli::archive::open_archive;
41
42const TSTAMP_HELP: &str = "\x1B[1m\x1B[4mTimestamps:\x1B[0m
43
44A <TIMESTAMP> argument is of the form \"YYYY-MM-DDThh:mm:ss[tz]\" where the letters represent the following:
45
46 \x1B[4mYYYY\x1B[0m Four decimal digits representing the year.
47 \x1B[4mMM\x1B[0m The month of the year, from 01 to 12.
48 \x1B[4mDD\x1B[0m the day of the month, from 01 to 31.
49 \x1B[4mhh\x1B[0m The hour of the day, from 00 to 23.
50 \x1B[4mmm\x1B[0m The minute of the hour, from 00 to 59.
51 \x1B[4mss\x1B[0m The second of the minute, from 00 to 60.
52 \x1B[4mtz\x1B[0m An optional letter Z indicating the time is in UTC. Otherwise, the time is assumed to be in local time.";
53
54fn tstamp_error(cmd: &Command, arg: Option<&Arg>, value: &OsStr) -> clap::Error {
55 use clap::error::{ContextKind, ContextValue, ErrorKind};
56
57 let mut err = clap::Error::new(ErrorKind::ValueValidation).with_cmd(cmd);
58
59 if let Some(a) = arg {
60 err.insert(ContextKind::InvalidArg, ContextValue::String(a.to_string()));
61 }
62
63 err.insert(
64 ContextKind::InvalidValue,
65 ContextValue::String(value.to_string_lossy().to_string()),
66 );
67
68 err
69}
70
71#[derive(Clone, Debug)]
72struct Timestamp;
73
74impl TypedValueParser for Timestamp {
75 type Value = DateTime<Utc>;
76
77 fn parse_ref(
78 &self,
79 cmd: &Command,
80 arg: Option<&Arg>,
81 value: &OsStr,
82 ) -> Result<DateTime<Utc>, clap::Error> {
83 let str_value = value.to_string_lossy();
84
85 let (str_value, utc) = match str_value.strip_suffix('Z') {
86 Some(s) => (s, true),
87 None => (str_value.as_ref(), false),
88 };
89
90 let dt = NaiveDateTime::parse_from_str(str_value, "%Y-%m-%dT%H:%M:%S")
91 .map_err(|_| tstamp_error(cmd, arg, value))?;
92
93 let dt_utc = if utc {
94 Utc.from_local_datetime(&dt)
95 } else {
96 Local.from_local_datetime(&dt).map(Into::into)
97 };
98
99 match dt_utc {
100 LocalResult::Single(dt) => Ok(dt),
101 LocalResult::Ambiguous(_earliest, latest) => Ok(latest),
102 LocalResult::None => Err(tstamp_error(cmd, arg, value)),
103 }
104 }
105}
106
107impl ValueParserFactory for Timestamp {
108 type Parser = Self;
109
110 fn value_parser() -> Self {
111 Timestamp
112 }
113}
114
115#[derive(Args, Debug)]
116struct TimestampArgs {
117 #[clap(short = 'r', long, value_parser = value_parser!(Timestamp), value_name = "TIMESTAMP")]
120 created: Option<DateTime<Utc>>,
121
122 #[clap(short = 'n', long, value_parser = value_parser!(Timestamp), value_name = "TIMESTAMP")]
125 changed: Option<DateTime<Utc>>,
126
127 #[clap(short = 'm', long, value_parser = value_parser!(Timestamp), value_name = "TIMESTAMP")]
130 modified: Option<DateTime<Utc>>,
131}
132
133#[derive(Args, Debug)]
134#[clap(args_conflicts_with_subcommands = true)]
136pub struct ArchiveAddArgs {
137 #[clap(subcommand)]
138 command: Option<ArchiveAddCommand>,
139
140 paths: Vec<PathBuf>,
144
145 #[clap(long, action = ArgAction::SetTrue)]
147 pub migrate: bool,
148
149 #[clap(short, long, env = "NUTS_CONTAINER")]
151 container: String,
152}
153
154impl ArchiveAddArgs {
155 pub fn run(&self) -> Result<()> {
156 if let Some(command) = self.command.as_ref() {
157 return command.run();
158 }
159
160 debug!("args: {:?}", self);
161
162 let mut archive = open_archive(&self.container, self.migrate)?;
163
164 for path in self.paths.iter() {
165 append_recursive(&mut archive, path)?;
166 }
167
168 Ok(())
169 }
170}
171
172#[derive(Debug, Subcommand)]
173pub enum ArchiveAddCommand {
174 File(ArchiveAddFileArgs),
176
177 Directory(ArchiveAddDirectoryArgs),
179
180 Symlink(ArchiveAddSymlinkArgs),
182}
183
184impl ArchiveAddCommand {
185 pub fn run(&self) -> Result<()> {
186 match self {
187 Self::File(args) => args.run(),
188 Self::Directory(args) => args.run(),
189 Self::Symlink(args) => args.run(),
190 }
191 }
192}