Edit files preserving the Unix timestamps

You may wonder why I would try to accomplish this in the first place. To be honest, this is probably an edge case that won't be useful to many people out there. Nevertheless, while trying to solve this problem at a large scale, I've realised it could be interesting enough to share how I did it. While trivial for a single file, I had to write a small script later on.

First off, the reason why I needed this today... For sometime now, I've been considering adding links to the root of my gemlog and capsule at the bottom of each post. A simple navigation aid that works everywhere, even on browsers that don't have shortcuts to move one level up. I could simply add this to newer posts, but my OCPD requires me to apply this logical change backwards to all my previous posts, which are quite a few by now.

You might think, well, that's easy, just append these new links to every post's .gmi file with something like:


cat footer.txt | tee -a *.gmi

Where "footer.txt" is a text file containing the gemtext of the links. Guess what, you're right. That's how I did it...

The catch is, this obviously changes the access and modification timestamps of all my posts. This is problematic because one of the ways I let users subscribe to my gemlog is through an atom feed that I generate whenever I create a new post. The script that generates the atom feed looks up the timestamps of the files that store my posts and publishes the modification time as the <updated> tag. By changing all my posts to include the changes at the bottom, the newly generated atom feed would show all my posts with an update date of today. All my subscribers that use atom would see dozens of posts from the past published again today. That's SPAM in my book.

It's not possible to change a file without modifying the timestamps, however, you can use the touch command to edit the timestamps. One possible way is to use the -d flag, which allows you to change the timestamps to a date you provide. An alternative is to use the -r flag, which allows you to specify a reference file, from which the timestamps are copied to a target file. It works like this:


touch -r ORIGINAL_FILE TARGET_FILE

What this does is create a new file called TARGET_FILE with exactly the same timestamps as ORIGINAL_FILE. The creation, access and modification dates are also exactly the same because they were transferred from one file to the other. So suppose I'd like to add my navigation links to my post called foo.gmi while preserving its timestamps. It would go like this:


touch -r foo.gmi foo.gmi.original_time
vim foo.gmi
#or
cat footer.txt >> foo.gmi
touch -r foo.gmi.original_time foo.gmi
rm foo.gmi.original_time

First I use touch to create foo.gmi.original_time that gets its timestamps copied from foo.gmi. Then I modify my foo.gmi post file either manually with vim or with a redirection of output. Now the timestamps of foo.gmi have obviously changed to the current time, so to fix it, I run the touch command again with the -r flag but in reverse. This time I copy the timestamps from foo.gmi.original_time (which was used basically to store the original times) to foo.gmi, my real post file. Finally I can remove foo.gmi.original_time since I no longer need it.

Since I needed to do this to all my posts and possibly in the future for other reasons, I ended up creating a shell script called tstool.sh that takes two mandatory arguments. The first one is either "save" or "restore", which creates a bunch of .original_time files with a copy of the original timestamps, or restores the original timestamps from those .original_time files back into the original files. The second one is the name of a file or a collection of files, including shell wildcards like for instance *.gmi. I'm posting the source below, as well as a link to the script:


#!/bin/bash

SAVE_EXTENSION='original_time'

show_usage() {
  echo "Usage: $0 OPTION FILE..."
  echo "Save or restore the original timestamps of a collection of files."
  echo
  echo "  save    - save the original timestamps of files"
  echo "  restore - restore the original timestamps of files"
  echo
}

save_timestamps() {
  for file in "$@"
  do
    touch -r $file $file.$SAVE_EXTENSION
    echo "$file timestamps saved to $file.$SAVE_EXTENSION"
  done
}

restore_timestamps() {
  ls *.$SAVE_EXTENSION > /dev/null 2>&1
  if [[ $? -gt 0 ]]; then
    echo "error: could not find stored timestamps"
    exit 1
  fi
  for file in $(ls *.$SAVE_EXTENSION)
  do
    if [[ ${file::-$((${#SAVE_EXTENSION} + 1))} -nt $file ]]; then
      touch -r $file ${file::-$((${#SAVE_EXTENSION} + 1))}
      echo "${file::-$((${#SAVE_EXTENSION} + 1))} timestamps restored from $file"
    fi
    rm $file
  done
}

if [[ -z $1 || -z $2 ]]; then
  show_usage
else
  if [[ $1 == "save" ]]; then
    shift
    save_timestamps "$@"
  elif [[ $1 == "restore" ]]; then
    restore_timestamps
  else
    show_usage
  fi
fi

tstool.sh