From 690ec9abfa994e6cf8b85ec38173ee5f0c91011c Mon Sep 17 00:00:00 2001 From: Robert Broketa Date: Thu, 7 Apr 2022 11:44:05 +0000 Subject: [PATCH] Implement rest of `touch` flags (#5119) * Add timestamp flag to `touch` command * Add modify flag to `touch` command * Add date flag to `touch` command * Remove unnecessary `touch` test and fix tests setups * Change `touch` flags descriptions * Update `touch` example * Add reference flag to `touch` command * Add access flag to `touch` command * Add no-create flag to `touch` command * Replace `unwrap` with `expect` --- Cargo.lock | 13 + crates/nu-command/Cargo.toml | 1 + crates/nu-command/src/filesystem/touch.rs | 278 +++++++- crates/nu-command/tests/commands/touch.rs | 737 ++++++++++++++++++++++ crates/nu-protocol/src/shell_error.rs | 8 + 5 files changed, 1031 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fb45198ee..5dcab13e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1065,6 +1065,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "filetime" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi", +] + [[package]] name = "flate2" version = "1.0.22" @@ -2329,6 +2341,7 @@ dependencies = [ "eml-parser", "encoding_rs", "filesize", + "filetime", "fs_extra", "hamcrest2", "htmlescape", diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 2e88be05a..73a551a98 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -40,6 +40,7 @@ dtparse = "1.2.0" eml-parser = "0.1.0" encoding_rs = "0.8.30" filesize = "0.2.0" +filetime = "0.2.15" fs_extra = "1.2.0" htmlescape = "0.3.1" ical = "0.7.0" diff --git a/crates/nu-command/src/filesystem/touch.rs b/crates/nu-command/src/filesystem/touch.rs index 061b0f586..d9269e050 100644 --- a/crates/nu-command/src/filesystem/touch.rs +++ b/crates/nu-command/src/filesystem/touch.rs @@ -1,9 +1,20 @@ use std::fs::OpenOptions; +use std::path::Path; + +use chrono::{DateTime, Datelike, Local}; +use filetime::FileTime; use nu_engine::CallExt; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; -use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, SyntaxShape}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape}; + +use crate::parse_date_from_string; + +enum AddYear { + Full, + FirstDigits, +} #[derive(Clone)] pub struct Touch; @@ -20,6 +31,39 @@ impl Command for Touch { SyntaxShape::Filepath, "the path of the file you want to create", ) + .named( + "timestamp", + SyntaxShape::String, + "change the file or directory time to a timestamp. Format: [[CC]YY]MMDDhhmm[.ss]\n\n If neither YY or CC is given, the current year will be assumed. If YY is specified, but CC is not, CC will be derived as follows:\n \tIf YY is between [69, 99], CC is 19\n \tIf YY is between [00, 68], CC is 20\n Note: It is expected that in a future version of this standard the default century inferred from a 2-digit year will change", + Some('t'), + ) + .named( + "date", + SyntaxShape::String, + "change the file or directory time to a date", + Some('d'), + ) + .named( + "reference", + SyntaxShape::String, + "change the file or directory time to the time of the reference file/directory", + Some('r'), + ) + .switch( + "modified", + "change the modification time of the file or directory. If no timestamp, date or reference file/directory is given, the current time is used", + Some('m'), + ) + .switch( + "access", + "change the access time of the file or directory. If no timestamp, date or reference file/directory is given, the current time is used", + Some('a'), + ) + .switch( + "no-create", + "do not create the file if it does not exist", + Some('c'), + ) .rest("rest", SyntaxShape::Filepath, "additional files to create") .category(Category::FileSystem) } @@ -35,17 +79,209 @@ impl Command for Touch { call: &Call, _input: PipelineData, ) -> Result { + let mut change_mtime: bool = call.has_flag("modified"); + let mut change_atime: bool = call.has_flag("access"); + let use_stamp: bool = call.has_flag("timestamp"); + let use_date: bool = call.has_flag("date"); + let use_reference: bool = call.has_flag("reference"); + let no_create: bool = call.has_flag("no-create"); let target: String = call.req(engine_state, stack, 0)?; let rest: Vec = call.rest(engine_state, stack, 1)?; + let mut date: Option> = None; + let mut ref_date_atime: Option> = None; + + // Change both times if none is specified + if !change_mtime && !change_atime { + change_mtime = true; + change_atime = true; + } + + if change_mtime || change_atime { + date = Some(Local::now()); + } + + if use_stamp || use_date { + let (val, span) = if use_stamp { + let stamp: Option> = + call.get_flag(engine_state, stack, "timestamp")?; + let (stamp, span) = match stamp { + Some(stamp) => (stamp.item, stamp.span), + None => { + return Err(ShellError::MissingParameter( + "timestamp".to_string(), + call.head, + )); + } + }; + + // Checks for the seconds stamp and removes the '.' delimiter if any + let (val, has_sec): (String, bool) = match stamp.split_once('.') { + Some((dtime, sec)) => (format!("{}{}", dtime, sec), true), + None => (stamp.to_string(), false), + }; + + let size = val.len(); + + // Each stamp is a 2 digit number and the whole stamp must not be less than 4 or greater than 7 pairs + if (size % 2 != 0 || !(8..=14).contains(&size)) || val.parse::().is_err() { + return Err(ShellError::UnsupportedInput( + "input has an invalid timestamp".to_string(), + span, + )); + } + + let add_year: Option = if has_sec { + match size { + 10 => Some(AddYear::Full), + 12 => Some(AddYear::FirstDigits), + 14 => None, + _ => unreachable!(), // This should never happen as the check above should catch it + } + } else { + match size { + 8 => Some(AddYear::Full), + 10 => Some(AddYear::FirstDigits), + 12 => None, + _ => unreachable!(), // This should never happen as the check above should catch it + } + }; + + if let Some(add_year) = add_year { + let year = Local::now().year(); + match add_year { + AddYear::Full => (format!("{}{}", year, val), span), + AddYear::FirstDigits => { + // Compliance with the Unix version of touch + let yy = val[0..2] + .parse::() + .expect("should be a valid 2 digit number"); + let mut year = 20; + if (69..=99).contains(&yy) { + year = 19; + } + (format!("{}{}", year, val), span) + } + } + } else { + (val, span) + } + } else { + let date_string: Option> = + call.get_flag(engine_state, stack, "date")?; + match date_string { + Some(date_string) => (date_string.item, date_string.span), + None => { + return Err(ShellError::MissingParameter("date".to_string(), call.head)); + } + } + }; + + date = if let Ok(parsed_date) = parse_date_from_string(&val, span) { + Some(parsed_date.into()) + } else { + let flag = if use_stamp { "timestamp" } else { "date" }; + return Err(ShellError::UnsupportedInput( + format!("input has an invalid {}", flag), + span, + )); + }; + } + + if use_reference { + let reference: Option> = + call.get_flag(engine_state, stack, "reference")?; + match reference { + Some(reference) => { + let reference_path = Path::new(&reference.item); + if !reference_path.exists() { + return Err(ShellError::UnsupportedInput( + "path provided is invalid".to_string(), + reference.span, + )); + } + + date = Some( + reference_path + .metadata() + .expect("should be a valid path") // Should never fail as the path exists + .modified() + .expect("should have metadata") // This should always be valid as it is available on all nushell's supported platforms (Linux, Windows, MacOS) + .into(), + ); + + ref_date_atime = Some( + reference_path + .metadata() + .expect("should be a valid path") // Should never fail as the path exists + .accessed() + .expect("should have metadata") // This should always be valid as it is available on all nushell's supported platforms (Linux, Windows, MacOS) + .into(), + ); + } + None => { + return Err(ShellError::MissingParameter( + "reference".to_string(), + call.head, + )); + } + } + } + for (index, item) in vec![target].into_iter().chain(rest).enumerate() { - match OpenOptions::new().write(true).create(true).open(&item) { - Ok(_) => continue, - Err(err) => { - return Err(ShellError::CreateNotPossible( - format!("Failed to create file: {}", err), + if no_create { + let path = Path::new(&item); + if !path.exists() { + continue; + } + } + + if let Err(err) = OpenOptions::new().write(true).create(true).open(&item) { + return Err(ShellError::CreateNotPossible( + format!("Failed to create file: {}", err), + call.positional[index].span, + )); + }; + + if change_mtime { + // Should not panic as we return an error above if we can't parse the date + if let Err(err) = filetime::set_file_mtime( + &item, + FileTime::from_system_time(date.expect("should be a valid date").into()), + ) { + return Err(ShellError::ChangeModifiedTimeNotPossible( + format!("Failed to change the modified time: {}", err), call.positional[index].span, )); + }; + } + + if change_atime { + // Reference file/directory may have different access and modified times + if use_reference { + // Should not panic as we return an error above if we can't parse the date + if let Err(err) = filetime::set_file_atime( + &item, + FileTime::from_system_time( + ref_date_atime.expect("should be a valid date").into(), + ), + ) { + return Err(ShellError::ChangeAccessTimeNotPossible( + format!("Failed to change the access time: {}", err), + call.positional[index].span, + )); + }; + } else { + // Should not panic as we return an error above if we can't parse the date + if let Err(err) = filetime::set_file_atime( + &item, + FileTime::from_system_time(date.expect("should be a valid date").into()), + ) { + return Err(ShellError::ChangeAccessTimeNotPossible( + format!("Failed to change the access time: {}", err), + call.positional[index].span, + )); + }; } } } @@ -65,6 +301,36 @@ impl Command for Touch { example: "touch a b c", result: None, }, + Example { + description: r#"Changes the last modified time of "fixture.json" to today's date"#, + example: "touch -m fixture.json", + result: None, + }, + Example { + description: "Creates files d and e and set its last modified time to a timestamp", + example: "touch -m -t 201908241230.30 d e", + result: None, + }, + Example { + description: "Changes the last modified time of files a, b and c to a date", + example: r#"touch -m -d "yesterday" a b c"#, + result: None, + }, + Example { + description: r#"Changes the last modified time of file d and e to "fixture.json"'s last modified time"#, + example: r#"touch -m -r fixture.json d e"#, + result: None, + }, + Example { + description: r#"Changes the last accessed time of "fixture.json" to a date"#, + example: r#"touch -a -d "August 24, 2019; 12:30:30" fixture.json"#, + result: None, + }, + Example { + description: "Changes both last modified and accessed time of a, b and c to a timestamp only if they exist", + example: r#"touch -c -t 201908241230.30 a b c"#, + result: None, + }, ] } } diff --git a/crates/nu-command/tests/commands/touch.rs b/crates/nu-command/tests/commands/touch.rs index affc1d14c..66865c3f5 100644 --- a/crates/nu-command/tests/commands/touch.rs +++ b/crates/nu-command/tests/commands/touch.rs @@ -1,3 +1,5 @@ +use chrono::{Date, DateTime, Local, TimeZone}; +use nu_test_support::fs::Stub; use nu_test_support::nu; use nu_test_support::playground::Playground; @@ -29,3 +31,738 @@ fn creates_two_files() { assert!(path2.exists()); }) } + +#[test] +fn change_modified_time_of_file() { + Playground::setup("change_time_test_3", |dirs, sandbox| { + sandbox.with_files(vec![Stub::EmptyFile("file.txt")]); + + nu!( + cwd: dirs.test(), + "touch -m -t 201908241230.30 file.txt" + ); + + let path = dirs.test().join("file.txt"); + + let time = Local.ymd(2019, 8, 24).and_hms(12, 30, 30); + let actual_time: DateTime = + DateTime::from(path.metadata().unwrap().modified().unwrap()); + + assert_eq!(time, actual_time); + }) +} + +#[test] +fn create_and_change_modified_time_of_file() { + Playground::setup("change_time_test_4", |dirs, _sandbox| { + nu!( + cwd: dirs.test(), + "touch -m -t 201908241230 i_will_be_created.txt" + ); + + let path = dirs.test().join("i_will_be_created.txt"); + assert!(path.exists()); + let time = Local.ymd(2019, 8, 24).and_hms(12, 30, 0); + + let actual_time: DateTime = + DateTime::from(path.metadata().unwrap().modified().unwrap()); + + assert_eq!(time, actual_time); + }) +} + +#[test] +fn change_modified_time_of_file_no_year() { + Playground::setup("change_time_test_5", |dirs, sandbox| { + sandbox.with_files(vec![Stub::EmptyFile("file.txt")]); + + nu!( + cwd: dirs.test(), + "touch -m -t 08241230.12 file.txt" + ); + + let path = dirs.test().join("file.txt"); + + let time = Local.ymd(2022, 8, 24).and_hms(12, 30, 12); + let actual_time: DateTime = + DateTime::from(path.metadata().unwrap().modified().unwrap()); + + assert_eq!(time, actual_time); + }) +} + +#[test] +fn change_modified_time_of_file_no_year_no_second() { + Playground::setup("change_time_test_6", |dirs, sandbox| { + sandbox.with_files(vec![Stub::EmptyFile("file.txt")]); + + nu!( + cwd: dirs.test(), + "touch -m -t 08241230 file.txt" + ); + + let path = dirs.test().join("file.txt"); + + let time = Local.ymd(2022, 8, 24).and_hms(12, 30, 0); + let actual_time: DateTime = + DateTime::from(path.metadata().unwrap().modified().unwrap()); + + assert_eq!(time, actual_time); + }) +} + +#[test] +fn change_modified_time_of_files() { + Playground::setup("change_time_test_7", |dirs, sandbox| { + sandbox.with_files(vec![ + Stub::EmptyFile("file.txt"), + Stub::EmptyFile("file2.txt"), + ]); + + nu!( + cwd: dirs.test(), + "touch -m -t 1908241230.30 file.txt file2.txt" + ); + + let path = dirs.test().join("file.txt"); + + let time = Local.ymd(2019, 8, 24).and_hms(12, 30, 30); + let actual_time: DateTime = + DateTime::from(path.metadata().unwrap().modified().unwrap()); + + assert_eq!(time, actual_time); + + let path = dirs.test().join("file2.txt"); + + let actual_time: DateTime = + DateTime::from(path.metadata().unwrap().modified().unwrap()); + + assert_eq!(time, actual_time); + }) +} + +#[test] +fn errors_if_change_modified_time_of_file_with_invalid_timestamp() { + Playground::setup("change_time_test_8", |dirs, sandbox| { + sandbox.with_files(vec![Stub::EmptyFile("file.txt")]); + + let mut outcome = nu!( + cwd: dirs.test(), + "touch -m -t 1908241230.3030 file.txt" + ); + + assert!(outcome.err.contains("input has an invalid timestamp")); + + outcome = nu!( + cwd: dirs.test(), + "touch -m -t 1908241230.3O file.txt" + ); + + assert!(outcome.err.contains("input has an invalid timestamp")); + + outcome = nu!( + cwd: dirs.test(), + "touch -m -t 08241230.3 file.txt" + ); + + assert!(outcome.err.contains("input has an invalid timestamp")); + + outcome = nu!( + cwd: dirs.test(), + "touch -m -t 8241230 file.txt" + ); + + assert!(outcome.err.contains("input has an invalid timestamp")); + + outcome = nu!( + cwd: dirs.test(), + "touch -m -t 01908241230 file.txt" + ); + + assert!(outcome.err.contains("input has an invalid timestamp")); + }) +} + +#[test] +fn change_modified_time_of_file_to_today() { + Playground::setup("change_time_test_9", |dirs, sandbox| { + sandbox.with_files(vec![Stub::EmptyFile("file.txt")]); + + nu!( + cwd: dirs.test(), + "touch -m file.txt" + ); + + let path = dirs.test().join("file.txt"); + + // Check only the date since the time may not match exactly + let date: Date = Local::now().date(); + let actual_date: Date = + DateTime::from(path.metadata().unwrap().modified().unwrap()).date(); + + assert_eq!(date, actual_date); + }) +} + +#[test] +fn change_modified_time_to_date() { + Playground::setup("change_time_test_10", |dirs, sandbox| { + sandbox.with_files(vec![Stub::EmptyFile("file.txt")]); + + nu!( + cwd: dirs.test(), + r#"touch -m -d "August 24, 2019; 12:30:30" file.txt"# + ); + + let path = dirs.test().join("file.txt"); + + let time = Local.ymd(2019, 8, 24).and_hms(12, 30, 30); + let actual_time: DateTime = + DateTime::from(path.metadata().unwrap().modified().unwrap()); + + assert_eq!(time, actual_time); + }) +} + +#[test] +fn change_modified_time_to_time_of_reference() { + Playground::setup("change_time_test_11", |dirs, sandbox| { + sandbox.with_files(vec![ + Stub::EmptyFile("file.txt"), + Stub::EmptyFile("reference.txt"), + ]); + + nu!( + cwd: dirs.test(), + r#"touch -m -t 201908241230.30 reference.txt"# + ); + + nu!( + cwd: dirs.test(), + r#"touch -m -r reference.txt file.txt"# + ); + + let path = dirs.test().join("file.txt"); + let ref_path = dirs.test().join("reference.txt"); + + let time: DateTime = DateTime::from(path.metadata().unwrap().modified().unwrap()); + let ref_time: DateTime = + DateTime::from(ref_path.metadata().unwrap().modified().unwrap()); + + assert_eq!(time, ref_time); + }) +} + +#[test] +fn change_access_time_of_file() { + Playground::setup("change_time_test_12", |dirs, sandbox| { + sandbox.with_files(vec![Stub::EmptyFile("file.txt")]); + + nu!( + cwd: dirs.test(), + "touch -a -t 201908241230.30 file.txt" + ); + + let path = dirs.test().join("file.txt"); + + let time = Local.ymd(2019, 8, 24).and_hms(12, 30, 30); + let actual_time: DateTime = + DateTime::from(path.metadata().unwrap().accessed().unwrap()); + + assert_eq!(time, actual_time); + }) +} + +#[test] +fn create_and_change_access_time_of_file() { + Playground::setup("change_time_test_13", |dirs, _sandbox| { + nu!( + cwd: dirs.test(), + "touch -a -t 201908241230 i_will_be_created.txt" + ); + + let path = dirs.test().join("i_will_be_created.txt"); + assert!(path.exists()); + let time = Local.ymd(2019, 8, 24).and_hms(12, 30, 0); + + let actual_time: DateTime = + DateTime::from(path.metadata().unwrap().accessed().unwrap()); + + assert_eq!(time, actual_time); + }) +} + +#[test] +fn change_access_time_of_file_no_year() { + Playground::setup("change_time_test_14", |dirs, sandbox| { + sandbox.with_files(vec![Stub::EmptyFile("file.txt")]); + + nu!( + cwd: dirs.test(), + "touch -a -t 08241230.12 file.txt" + ); + + let path = dirs.test().join("file.txt"); + + let time = Local.ymd(2022, 8, 24).and_hms(12, 30, 12); + let actual_time: DateTime = + DateTime::from(path.metadata().unwrap().accessed().unwrap()); + + assert_eq!(time, actual_time); + }) +} + +#[test] +fn change_access_time_of_file_no_year_no_second() { + Playground::setup("change_time_test_15", |dirs, sandbox| { + sandbox.with_files(vec![Stub::EmptyFile("file.txt")]); + + nu!( + cwd: dirs.test(), + "touch -a -t 08241230 file.txt" + ); + + let path = dirs.test().join("file.txt"); + + let time = Local.ymd(2022, 8, 24).and_hms(12, 30, 0); + let actual_time: DateTime = + DateTime::from(path.metadata().unwrap().accessed().unwrap()); + + assert_eq!(time, actual_time); + }) +} + +#[test] +fn change_access_time_of_files() { + Playground::setup("change_time_test_16", |dirs, sandbox| { + sandbox.with_files(vec![ + Stub::EmptyFile("file.txt"), + Stub::EmptyFile("file2.txt"), + ]); + + nu!( + cwd: dirs.test(), + "touch -a -t 1908241230.30 file.txt file2.txt" + ); + + let path = dirs.test().join("file.txt"); + + let time = Local.ymd(2019, 8, 24).and_hms(12, 30, 30); + let actual_time: DateTime = + DateTime::from(path.metadata().unwrap().accessed().unwrap()); + + assert_eq!(time, actual_time); + + let path = dirs.test().join("file2.txt"); + + let actual_time: DateTime = + DateTime::from(path.metadata().unwrap().accessed().unwrap()); + + assert_eq!(time, actual_time); + }) +} + +#[test] +fn errors_if_change_access_time_of_file_with_invalid_timestamp() { + Playground::setup("change_time_test_17", |dirs, sandbox| { + sandbox.with_files(vec![Stub::EmptyFile("file.txt")]); + + let mut outcome = nu!( + cwd: dirs.test(), + "touch -a -t 1908241230.3030 file.txt" + ); + + assert!(outcome.err.contains("input has an invalid timestamp")); + + outcome = nu!( + cwd: dirs.test(), + "touch -a -t 1908241230.3O file.txt" + ); + + assert!(outcome.err.contains("input has an invalid timestamp")); + + outcome = nu!( + cwd: dirs.test(), + "touch -a -t 08241230.3 file.txt" + ); + + assert!(outcome.err.contains("input has an invalid timestamp")); + + outcome = nu!( + cwd: dirs.test(), + "touch -a -t 8241230 file.txt" + ); + + assert!(outcome.err.contains("input has an invalid timestamp")); + + outcome = nu!( + cwd: dirs.test(), + "touch -a -t 01908241230 file.txt" + ); + + assert!(outcome.err.contains("input has an invalid timestamp")); + }) +} + +#[test] +fn change_access_time_of_file_to_today() { + Playground::setup("change_time_test_18", |dirs, sandbox| { + sandbox.with_files(vec![Stub::EmptyFile("file.txt")]); + + nu!( + cwd: dirs.test(), + "touch -a file.txt" + ); + + let path = dirs.test().join("file.txt"); + + // Check only the date since the time may not match exactly + let date: Date = Local::now().date(); + let actual_date: Date = + DateTime::from(path.metadata().unwrap().accessed().unwrap()).date(); + + assert_eq!(date, actual_date); + }) +} + +#[test] +fn change_access_time_to_date() { + Playground::setup("change_time_test_19", |dirs, sandbox| { + sandbox.with_files(vec![Stub::EmptyFile("file.txt")]); + + nu!( + cwd: dirs.test(), + r#"touch -a -d "August 24, 2019; 12:30:30" file.txt"# + ); + + let path = dirs.test().join("file.txt"); + + let time = Local.ymd(2019, 8, 24).and_hms(12, 30, 30); + let actual_time: DateTime = + DateTime::from(path.metadata().unwrap().accessed().unwrap()); + + assert_eq!(time, actual_time); + }) +} + +#[test] +fn change_access_time_to_time_of_reference() { + Playground::setup("change_time_test_20", |dirs, sandbox| { + sandbox.with_files(vec![ + Stub::EmptyFile("file.txt"), + Stub::EmptyFile("reference.txt"), + ]); + + nu!( + cwd: dirs.test(), + r#"touch -a -t 201908241230.30 reference.txt"# + ); + + nu!( + cwd: dirs.test(), + r#"touch -a -r reference.txt file.txt"# + ); + + let path = dirs.test().join("file.txt"); + let ref_path = dirs.test().join("reference.txt"); + + let time: DateTime = DateTime::from(path.metadata().unwrap().accessed().unwrap()); + let ref_time: DateTime = + DateTime::from(ref_path.metadata().unwrap().accessed().unwrap()); + + assert_eq!(time, ref_time); + }) +} + +#[test] +fn change_modified_and_access_time_of_file() { + Playground::setup("change_time_test_21", |dirs, sandbox| { + sandbox.with_files(vec![Stub::EmptyFile("file.txt")]); + + nu!( + cwd: dirs.test(), + "touch -m -a -t 201908241230.30 file.txt" + ); + + let path = dirs.test().join("file.txt"); + let metadata = path.metadata().unwrap(); + + let time = Local.ymd(2019, 8, 24).and_hms(12, 30, 30); + let atime: DateTime = DateTime::from(metadata.accessed().unwrap()); + let mtime: DateTime = DateTime::from(metadata.modified().unwrap()); + + assert_eq!(time, atime); + assert_eq!(time, mtime); + }) +} + +#[test] +fn create_and_change_modified_and_access_time_of_file() { + Playground::setup("change_time_test_22", |dirs, _sandbox| { + nu!( + cwd: dirs.test(), + "touch -t 201908241230 i_will_be_created.txt" + ); + + let path = dirs.test().join("i_will_be_created.txt"); + assert!(path.exists()); + + let metadata = path.metadata().unwrap(); + + let time = Local.ymd(2019, 8, 24).and_hms(12, 30, 0); + + let atime: DateTime = DateTime::from(metadata.accessed().unwrap()); + let mtime: DateTime = DateTime::from(metadata.modified().unwrap()); + + assert_eq!(time, atime); + assert_eq!(time, mtime); + }) +} + +#[test] +fn change_modified_and_access_time_of_file_no_year() { + Playground::setup("change_time_test_23", |dirs, sandbox| { + sandbox.with_files(vec![Stub::EmptyFile("file.txt")]); + + nu!( + cwd: dirs.test(), + "touch -a -m -t 08241230.12 file.txt" + ); + + let metadata = dirs.test().join("file.txt").metadata().unwrap(); + + let time = Local.ymd(2022, 8, 24).and_hms(12, 30, 12); + + let atime: DateTime = DateTime::from(metadata.accessed().unwrap()); + let mtime: DateTime = DateTime::from(metadata.modified().unwrap()); + + assert_eq!(time, atime); + assert_eq!(time, mtime); + }) +} + +#[test] +fn change_modified_and_access_time_of_file_no_year_no_second() { + Playground::setup("change_time_test_24", |dirs, sandbox| { + sandbox.with_files(vec![Stub::EmptyFile("file.txt")]); + + nu!( + cwd: dirs.test(), + "touch -t 08241230 file.txt" + ); + + let metadata = dirs.test().join("file.txt").metadata().unwrap(); + + let time = Local.ymd(2022, 8, 24).and_hms(12, 30, 0); + + let atime: DateTime = DateTime::from(metadata.accessed().unwrap()); + let mtime: DateTime = DateTime::from(metadata.modified().unwrap()); + + assert_eq!(time, atime); + assert_eq!(time, mtime); + }) +} + +#[test] +fn change_modified_and_access_time_of_files() { + Playground::setup("change_time_test_25", |dirs, sandbox| { + sandbox.with_files(vec![ + Stub::EmptyFile("file.txt"), + Stub::EmptyFile("file2.txt"), + ]); + + nu!( + cwd: dirs.test(), + "touch -a -m -t 1908241230.30 file.txt file2.txt" + ); + + let metadata = dirs.test().join("file.txt").metadata().unwrap(); + + let time = Local.ymd(2019, 8, 24).and_hms(12, 30, 30); + let atime: DateTime = DateTime::from(metadata.accessed().unwrap()); + let mtime: DateTime = DateTime::from(metadata.modified().unwrap()); + + assert_eq!(time, atime); + assert_eq!(time, mtime); + + let metadata = dirs.test().join("file2.txt").metadata().unwrap(); + + let atime: DateTime = DateTime::from(metadata.accessed().unwrap()); + let mtime: DateTime = DateTime::from(metadata.modified().unwrap()); + + assert_eq!(time, atime); + assert_eq!(time, mtime); + }) +} + +#[test] +fn errors_if_change_modified_and_access_time_of_file_with_invalid_timestamp() { + Playground::setup("change_time_test_26", |dirs, sandbox| { + sandbox.with_files(vec![Stub::EmptyFile("file.txt")]); + + let mut outcome = nu!( + cwd: dirs.test(), + "touch -t 1908241230.3030 file.txt" + ); + + assert!(outcome.err.contains("input has an invalid timestamp")); + + outcome = nu!( + cwd: dirs.test(), + "touch -a -m -t 1908241230.3O file.txt" + ); + + assert!(outcome.err.contains("input has an invalid timestamp")); + + outcome = nu!( + cwd: dirs.test(), + "touch -t 08241230.3 file.txt" + ); + + assert!(outcome.err.contains("input has an invalid timestamp")); + + outcome = nu!( + cwd: dirs.test(), + "touch -m -a -t 8241230 file.txt" + ); + + assert!(outcome.err.contains("input has an invalid timestamp")); + + outcome = nu!( + cwd: dirs.test(), + "touch -t 01908241230 file.txt" + ); + + assert!(outcome.err.contains("input has an invalid timestamp")); + }) +} + +#[test] +fn change_modified_and_access_time_of_file_to_today() { + Playground::setup("change_time_test_27", |dirs, sandbox| { + sandbox.with_files(vec![Stub::EmptyFile("file.txt")]); + + nu!( + cwd: dirs.test(), + "touch -a -m file.txt" + ); + + let metadata = dirs.test().join("file.txt").metadata().unwrap(); + + // Check only the date since the time may not match exactly + let date: Date = Local::now().date(); + let adate: Date = DateTime::from(metadata.accessed().unwrap()).date(); + let mdate: Date = DateTime::from(metadata.modified().unwrap()).date(); + + assert_eq!(date, adate); + assert_eq!(date, mdate); + }) +} + +#[test] +fn change_modified_and_access_time_to_date() { + Playground::setup("change_time_test_28", |dirs, sandbox| { + sandbox.with_files(vec![Stub::EmptyFile("file.txt")]); + + nu!( + cwd: dirs.test(), + r#"touch -d "August 24, 2019; 12:30:30" file.txt"# + ); + + let metadata = dirs.test().join("file.txt").metadata().unwrap(); + + let time = Local.ymd(2019, 8, 24).and_hms(12, 30, 30); + let atime: DateTime = DateTime::from(metadata.accessed().unwrap()); + let mtime: DateTime = DateTime::from(metadata.modified().unwrap()); + + assert_eq!(time, atime); + assert_eq!(time, mtime); + }) +} + +#[test] +fn change_modified_and_access_time_to_time_of_reference() { + Playground::setup("change_time_test_29", |dirs, sandbox| { + sandbox.with_files(vec![ + Stub::EmptyFile("file.txt"), + Stub::EmptyFile("reference.txt"), + ]); + + let path = dirs.test().join("file.txt"); + let ref_path = dirs.test().join("reference.txt"); + + // Set the same time for the modified and access time of the reference file + nu!( + cwd: dirs.test(), + r#"touch -a -m -t 201908241230.30 reference.txt"# + ); + + nu!( + cwd: dirs.test(), + r#"touch -r reference.txt file.txt"# + ); + + let atime: DateTime = DateTime::from(path.metadata().unwrap().accessed().unwrap()); + let ref_atime: DateTime = + DateTime::from(ref_path.metadata().unwrap().accessed().unwrap()); + + assert_eq!(atime, ref_atime); + + let mtime: DateTime = DateTime::from(path.metadata().unwrap().modified().unwrap()); + let ref_mtime: DateTime = + DateTime::from(ref_path.metadata().unwrap().modified().unwrap()); + + assert_eq!(mtime, ref_mtime); + + // Set different time for the modified and access time of the reference file + nu!( + cwd: dirs.test(), + r#"touch -a -t 201908241230.30 reference.txt"# + ); + + nu!( + cwd: dirs.test(), + r#"touch -m -t 202009251340.40 reference.txt"# + ); + + nu!( + cwd: dirs.test(), + r#"touch -a -m -r reference.txt file.txt"# + ); + + let atime: DateTime = DateTime::from(path.metadata().unwrap().accessed().unwrap()); + let ref_atime: DateTime = + DateTime::from(ref_path.metadata().unwrap().accessed().unwrap()); + + assert_eq!(atime, ref_atime); + + let mtime: DateTime = DateTime::from(path.metadata().unwrap().modified().unwrap()); + let ref_mtime: DateTime = + DateTime::from(ref_path.metadata().unwrap().modified().unwrap()); + + assert_eq!(mtime, ref_mtime); + }) +} + +#[test] +fn not_create_file_if_it_not_exists() { + Playground::setup("change_time_test_28", |dirs, _sandbox| { + nu!( + cwd: dirs.test(), + r#"touch -c -d "August 24, 2019; 12:30:30" file.txt"# + ); + + let path = dirs.test().join("file.txt"); + + assert!(!path.exists()); + + nu!( + cwd: dirs.test(), + r#"touch -c file.txt"# + ); + + let path = dirs.test().join("file.txt"); + + assert!(!path.exists()); + }) +} diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index b0b8ae4ce..f72a8a93e 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -269,6 +269,14 @@ Either make sure {0} is a string, or add a 'to_string' entry for it in ENV_CONVE #[diagnostic(code(nu::shell::create_not_possible), url(docsrs))] CreateNotPossible(String, #[label("{0}")] Span), + #[error("Not possible to change the access time")] + #[diagnostic(code(nu::shell::change_access_time_not_possible), url(docsrs))] + ChangeAccessTimeNotPossible(String, #[label("{0}")] Span), + + #[error("Not possible to change the modified time")] + #[diagnostic(code(nu::shell::change_modified_time_not_possible), url(docsrs))] + ChangeModifiedTimeNotPossible(String, #[label("{0}")] Span), + #[error("Remove not possible")] #[diagnostic(code(nu::shell::remove_not_possible), url(docsrs))] RemoveNotPossible(String, #[label("{0}")] Span),