Set directory timestamps to most recently modified file within (recursively)

filestimestamps

How do I change the timestamp of a directory and all the sub-folders within that directory to reflect the modification times of the contained files?

For example with this directory structure:

[Jan 9]  root
├── [Jan 3]  file1
├── [Jan 7]  file2
├── [Jan 6]  sub1
│   ├── [Jan 2]  file3
│   └── [Jan 1]  file4
└── [Jan  4] sub2
    └── [Jan 8]  file5

Here is a one liner to generate that:

mkdir -p root/sub1 root/sub2 && touch -d '2018-01-08' root/sub2/file5 && touch -d '2018-01-04' root/sub2/ && touch -d '2018-01-01' root/sub1/file4 && touch -d '2018-01-02' root/sub1/file3 && touch -d '2018-01-06' root/sub1/ && touch -d '2018-01-07' root/file2 && touch -d '2018-01-03' root/file1 && touch -d '2018-01-09' root/

It can be listed with tree -D

I'd like to change the timestamps on the three directories to be:

[Jan 8]  root
├── [Jan 3]  file1
├── [Jan 7]  file2
├── [Jan 2]  sub1
│   ├── [Jan 2]  file3
│   └── [Jan 1]  file4
└── [Jan 8]  sub2
    └── [Jan 8]  file5

Note:

  • The current timestamps on the directories are completely ignored and the new time stamps are set only based on the contents.
  • Time stamps bubble up to multiple levels of parent directories.

The reason that I'm doing this is for a directory that gets copied with rsync. The directory is checked into git and could get rsynced from any place that has the repository checked out. To ensure that rsync is consistent and idempotent from the various places, I need to ensure that the time stamps and permissions of everything are in a known state. I already have a script that sets the timestamps of files based on when they were committed to git. I also have a script that sets the permissions on all files and directories to a known state. The only portion that I'm struggling with is bubbling time stamps from the files up to parent directories.

I would like one line or short script that I can run from the command line to set directory timestamps based on the timestamps of their contents.

Best Answer

If you can use Zsh, this seems to do what you wish:

$ mkdir -p x/y; touch x/y/a x/b; sleep .1; touch x/y/a; sleep .1; touch x/b
$ find -depth -type d -execdir zsh -c 'touch "$1" -r "$1"/*(om[1])' zsh {} \;

Test output with GNU find, ./x/b was the newest, its timestamp is copied to ./x and ..

$ find -printf "%TT %p\n"  | sort -n
15:16:24.0182222830 ./x/y
15:16:24.0182222830 ./x/y/a
15:16:24.1222150510 .
15:16:24.1222150510 ./x
15:16:24.1222150510 ./x/b

Finding the newest file by timestamp is somewhat tricky in general, if there are filenames with whitespace/newlines or such. (See BashFAQ 099.) Zsh does make it rather easy, though, the trick above is from Is it possible to reference the most recently modified file in a command line argument?

-depth tells find to handle subdirectories before their parents, so that the bubbling up actually works.

This doesn't actually fully ignore timestamps of directories since they must be counted when the timestamps bubble up. A directory containing files will get its timestamp from the files, but an empty directory will keep its original timestamp, and that timestamp will bubble up. I'm not sure if it's worth working around that.

Related Question