Skip to content

Commit

Permalink
Fix touch to allow changing timestamps on directories, remake from #…
Browse files Browse the repository at this point in the history
…11760 (#12005)

<!--
if this PR closes one or more issues, you can automatically link the PR
with
them by using one of the [*linking
keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword),
e.g.
- this PR should close #xxxx
- fixes #xxxx

you can also mention related issues, PRs or discussions!
-->

Based off of #11760 to be mergable without conflicts.

# Description
<!--
Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.

Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.
-->
Fix for #11757.
The main issue in #11757 is I tried to copy the timestamp from one
directory to another only to realize that did not work whereas the
coreutils `^touch` had no problems. I thought `--reference` just did not
work, but apparently the whole `touch` command could not work on
directories because
`OpenOptions::new().write(true).create(true).open(&item)` tries to
create `touch`'s target in advance and then modify its timestamps. But
if the target is a directory that already exists then this would fail
even though the crate used for working with timestamps, `filetime`,
already works on directories.

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->
I don't believe this should change any existing valid behaviors. It just
changes a non-working behavior.

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use std testing; testing run-tests --path
crates/nu-std"` to run the tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->
~~I only could not run `cargo test` because I get compilation errors on
the latest main branch~~
All tests pass with `cargo test --features=sqlite`

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
  • Loading branch information
kik4444 committed Mar 1, 2024
1 parent 39cf43e commit 38a4290
Show file tree
Hide file tree
Showing 2 changed files with 420 additions and 132 deletions.
168 changes: 65 additions & 103 deletions crates/nu-command/src/filesystem/touch.rs
@@ -1,7 +1,6 @@
use std::fs::OpenOptions;
use std::path::Path;
use std::{fs::OpenOptions, time::SystemTime};

use chrono::{DateTime, Local};
use filetime::FileTime;

use nu_engine::CallExt;
Expand Down Expand Up @@ -64,7 +63,7 @@ impl Command for Touch {
) -> Result<PipelineData, ShellError> {
let mut change_mtime: bool = call.has_flag(engine_state, stack, "modified")?;
let mut change_atime: bool = call.has_flag(engine_state, stack, "access")?;
let use_reference: bool = call.has_flag(engine_state, stack, "reference")?;
let reference: Option<Spanned<String>> = call.get_flag(engine_state, stack, "reference")?;
let no_create: bool = call.has_flag(engine_state, stack, "no-create")?;
let files: Vec<String> = call.rest(engine_state, stack, 0)?;

Expand All @@ -75,88 +74,73 @@ impl Command for Touch {
});
}

let mut date: Option<DateTime<Local>> = None;
let mut ref_date_atime: Option<DateTime<Local>> = None;
let mut mtime = SystemTime::now();
let mut atime = mtime;

// Change both times if none is specified
// Change both times if neither is specified
if !change_mtime && !change_atime {
change_mtime = true;
change_atime = true;
}

if change_mtime || change_atime {
date = Some(Local::now());
}

if use_reference {
let reference: Option<Spanned<String>> =
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::TypeMismatch {
err_message: "path provided is invalid".to_string(),
span: 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 {
param_name: "reference".to_string(),
span: call.head,
});
}
if let Some(reference) = reference {
let reference_path = Path::new(&reference.item);
if !reference_path.exists() {
return Err(ShellError::FileNotFoundCustom {
msg: "Reference path not found".into(),
span: reference.span,
});
}

let metadata = reference_path
.metadata()
.map_err(|err| ShellError::IOErrorSpanned {
msg: format!("Failed to read metadata: {err}"),
span: reference.span,
})?;
mtime = metadata
.modified()
.map_err(|err| ShellError::IOErrorSpanned {
msg: format!("Failed to read modified time: {err}"),
span: reference.span,
})?;
atime = metadata
.accessed()
.map_err(|err| ShellError::IOErrorSpanned {
msg: format!("Failed to read access time: {err}"),
span: reference.span,
})?;
}

for (index, item) in files.into_iter().enumerate() {
if no_create {
let path = Path::new(&item);
if !path.exists() {
continue;
}
let path = Path::new(&item);

// If --no-create is passed and the file/dir does not exist there's nothing to do
if no_create && !path.exists() {
continue;
}

if let Err(err) = OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.open(&item)
{
return Err(ShellError::CreateNotPossible {
msg: format!("Failed to create file: {err}"),
span: call
.positional_nth(index)
.expect("already checked positional")
.span,
});
};
// Create a file at the given path unless the path is a directory
if !path.is_dir() {
if let Err(err) = OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.open(path)
{
return Err(ShellError::CreateNotPossible {
msg: format!("Failed to create file: {err}"),
span: call
.positional_nth(index)
.expect("already checked positional")
.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()),
) {
if let Err(err) = filetime::set_file_mtime(&item, FileTime::from_system_time(mtime))
{
return Err(ShellError::ChangeModifiedTimeNotPossible {
msg: format!("Failed to change the modified time: {err}"),
span: call
Expand All @@ -168,38 +152,16 @@ impl Command for Touch {
}

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 {
msg: format!("Failed to change the access time: {err}"),
span: call
.positional_nth(index)
.expect("already checked positional")
.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 {
msg: format!("Failed to change the access time: {err}"),
span: call
.positional_nth(index)
.expect("already checked positional")
.span,
});
};
}
if let Err(err) = filetime::set_file_atime(&item, FileTime::from_system_time(atime))
{
return Err(ShellError::ChangeAccessTimeNotPossible {
msg: format!("Failed to change the access time: {err}"),
span: call
.positional_nth(index)
.expect("already checked positional")
.span,
});
};
}
}

Expand Down

0 comments on commit 38a4290

Please sign in to comment.