127 lines
4.9 KiB
Rust
127 lines
4.9 KiB
Rust
|
use chrono::DateTime;
|
||
|
use clap::{value_parser, Arg, ArgAction, ArgGroup, Command};
|
||
|
use filetime::*;
|
||
|
use shitbox::Cmd;
|
||
|
use std::{error::Error, fs::File, io, path::PathBuf};
|
||
|
|
||
|
#[derive(Debug, Default)]
|
||
|
pub struct Touch;
|
||
|
|
||
|
impl Cmd for Touch {
|
||
|
fn cli(&self) -> clap::Command {
|
||
|
Command::new("touch")
|
||
|
.about("set file timestamps")
|
||
|
.author("Nathan Fisher")
|
||
|
.version(env!("CARGO_PKG_VERSION"))
|
||
|
.args([
|
||
|
Arg::new("access")
|
||
|
.help("set the access time of the file")
|
||
|
.short('a')
|
||
|
.long("access")
|
||
|
.action(ArgAction::SetTrue),
|
||
|
Arg::new("modify")
|
||
|
.help("set the modification time of the file")
|
||
|
.short('m')
|
||
|
.long("modification")
|
||
|
.conflicts_with("access")
|
||
|
.action(ArgAction::SetTrue),
|
||
|
Arg::new("create")
|
||
|
.help("Don't create file if it doesn't exist, not affecting exit status.")
|
||
|
.short('c')
|
||
|
.long("nocreate")
|
||
|
.action(ArgAction::SetFalse),
|
||
|
Arg::new("datetime")
|
||
|
.help("Set the time of the format YYYY-MM-DDThh:mm:SS[Z] used for [-am].")
|
||
|
.short('d')
|
||
|
.long("datetime")
|
||
|
.value_name("time")
|
||
|
.num_args(1),
|
||
|
Arg::new("ref")
|
||
|
.help("Set the time used for [-am] to the modification time of ref_file.")
|
||
|
.short('r')
|
||
|
.long("ref")
|
||
|
.value_name("file")
|
||
|
.num_args(1),
|
||
|
Arg::new("epoch")
|
||
|
.help(
|
||
|
"Set the time used for [-am] given as the number of seconds \
|
||
|
since the Unix epoch 1970-01-01T00:00:00Z.",
|
||
|
)
|
||
|
.short('T')
|
||
|
.long("epoch")
|
||
|
.value_name("time")
|
||
|
.value_parser(value_parser!(i64))
|
||
|
.num_args(1),
|
||
|
Arg::new("time")
|
||
|
.help("Set the time of the format [[CC]YY]MMDDhhmm[.SS] used for [-am].")
|
||
|
.short('t')
|
||
|
.long("time")
|
||
|
.value_name("time")
|
||
|
.num_args(1),
|
||
|
Arg::new("file").num_args(1..).required(true),
|
||
|
])
|
||
|
.group(
|
||
|
ArgGroup::new("timespec")
|
||
|
.args(["datetime", "ref", "epoch", "time"])
|
||
|
.multiple(false),
|
||
|
)
|
||
|
}
|
||
|
|
||
|
fn run(&self, matches: &clap::ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||
|
let ft = if let Some(time) = matches.get_one::<String>("datetime") {
|
||
|
FileTime::from_unix_time(parse_datetime(time)?, 0)
|
||
|
} else if let Some(time) = matches.get_one::<String>("time") {
|
||
|
FileTime::from_unix_time(parse_time(time)?, 0)
|
||
|
} else if let Some(ep) = matches.get_one::<i64>("epoch") {
|
||
|
FileTime::from_unix_time(*ep, 0)
|
||
|
} else if let Some(file_ref) = matches.get_one::<String>("ref") {
|
||
|
let meta = File::open(file_ref)?.metadata()?;
|
||
|
FileTime::from_last_modification_time(&meta)
|
||
|
} else {
|
||
|
FileTime::now()
|
||
|
};
|
||
|
|
||
|
if let Some(files) = matches.get_many::<String>("file") {
|
||
|
for f in files {
|
||
|
let path = PathBuf::from(f);
|
||
|
if !path.exists() && matches.get_flag("create") {
|
||
|
File::create(&path)?;
|
||
|
}
|
||
|
if matches.get_flag("access") {
|
||
|
set_file_atime(&path, ft)?;
|
||
|
} else if matches.get_flag("modify") {
|
||
|
set_file_mtime(&path, ft)?;
|
||
|
} else {
|
||
|
set_file_times(&path, ft, ft)?;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
Ok(())
|
||
|
}
|
||
|
|
||
|
fn path(&self) -> Option<shitbox::Path> {
|
||
|
Some(shitbox::Path::UsrBin)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fn parse_datetime(time: &str) -> Result<i64, Box<dyn Error>> {
|
||
|
match time.len() {
|
||
|
19 => Ok(DateTime::parse_from_str(time, "%Y-%m-%dT%H:%M:%S")?.timestamp()),
|
||
|
25 => Ok(DateTime::parse_from_rfc3339(time)?.timestamp()),
|
||
|
_ => Err(io::Error::new(io::ErrorKind::Other, "invalid datetime string").into()),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fn parse_time(time: &str) -> Result<i64, Box<dyn Error>> {
|
||
|
let dt = match time.len() {
|
||
|
8 => DateTime::parse_from_str(time, "%m%d%H%M")?,
|
||
|
10 => DateTime::parse_from_str(time, "%y%m%d%H%M")?,
|
||
|
11 => DateTime::parse_from_str(time, "%m%d%H%M.%S")?,
|
||
|
12 => DateTime::parse_from_str(time, "%Y%m%d%H%M")?,
|
||
|
13 => DateTime::parse_from_str(time, "%y%m%d%H%M.%S")?,
|
||
|
15 => DateTime::parse_from_str(time, "%Y%m%d%H%M.%S")?,
|
||
|
_ => return Err(io::Error::new(io::ErrorKind::Other, "invalid datetime string").into()),
|
||
|
};
|
||
|
Ok(dt.timestamp())
|
||
|
}
|