nuts_tool/cli/archive/
add.rs

1// MIT License
2//
3// Copyright (c) 2023,2024 Robin Doer
4//
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to
7// deal in the Software without restriction, including without limitation the
8// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9// sell copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11//
12// The above copyright notice and this permission notice shall be included in
13// all copies or substantial portions of the Software.
14//
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21// IN THE SOFTWARE.
22
23pub 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    /// Change the creation time to the specified date time instead of the
118    /// current time of day
119    #[clap(short = 'r', long, value_parser = value_parser!(Timestamp), value_name = "TIMESTAMP")]
120    created: Option<DateTime<Utc>>,
121
122    /// Change the changed time to the specified date time instead of the
123    /// current time of day
124    #[clap(short = 'n', long, value_parser = value_parser!(Timestamp), value_name = "TIMESTAMP")]
125    changed: Option<DateTime<Utc>>,
126
127    /// Change the modified time to the specified date time instead of
128    /// he current time of day
129    #[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(group(ArgGroup::new("input").required(true).multiple(false)))]
135#[clap(args_conflicts_with_subcommands = true)]
136pub struct ArchiveAddArgs {
137    #[clap(subcommand)]
138    command: Option<ArchiveAddCommand>,
139
140    /// Path to files/directories to be added to the archive. If PATHS contains
141    /// a directory all entries in the directory are also appended. If no PATHS
142    /// are specified an empty archive is created.
143    paths: Vec<PathBuf>,
144
145    /// Starts the migration when the container/archive is opened
146    #[clap(long, action = ArgAction::SetTrue)]
147    pub migrate: bool,
148
149    /// Specifies the name of the container
150    #[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    /// Appends a custom file to the archive.
175    File(ArchiveAddFileArgs),
176
177    /// Appends a custom directory to the archive.
178    Directory(ArchiveAddDirectoryArgs),
179
180    /// Appends a custom symlink to the archive.
181    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}