Maintain (or restore) file permissions when replacing file

Instead of using mv, just redirect cat. For example:

TMP=$(mktemp)
modifyfile "$original" "$TMP"
cat "$TMP" > "$original"

This overwrites $original with the contents of $TMP, without touching anything at the file level.


There are two strategies to replace a file by a new version:

  1. Create a temporary file with the new version, then move it into place.

    • Advantage: if a program opens that file, it will either read the old content or the new content, depending on whether it opened the file before or after the move. There is no mix-up.
    • Advantage: in case of a crash, the old content is preserved.
    • Downside: since a new file gets created, the file's attributes (ownership, permission, etc.) are not preserved.
  2. Overwrite the old file in place.

    • Advantage: the file's attributes are preserved.
    • Downside: in case of a crash, the file may be left half-written.
    • Downside: if a program has the file open when it is being updated, this program may read inconsistent data.

If you can, use method 1, but first replicate the attributes of the original file with cp -p --attributes-only. This requires GNU coreutils (i.e. non-embedded Linux, or sufficiently Linux-like environments). If your cp doesn't have --attributes-only, omit this option: it'll work but it'll replicate the data as well.

tmp=$(mktemp)
cp -p --attributes-only "$original" "$tmp"
modifyfile "$original" "$tmp"
mv -f "$tmp" "$original"

If you cannot replicate the attributes of the existing file, for example because you have write permissions on it but do not own it and you want to preserve the owner, then only method 2 is possible. To minimize the risk of data loss:

  • Make the window during which the file will be incomplete as small as possible. Prepare the data first in a temporary file, then copy it into place.
  • Make a backup of the old file first.

tmp=$(mktemp)
backup="${original}~"
modifyfile "$original" "$tmp"
cp -p "$original" "$backup"
cp -f "$tmp" "$original"

After our discussion on the first answer, I propose a different answer:

TMP="$(mktemp "$original".XXXXXXXXXX)"
modifyfile "$original" "$TMP"
chmod --reference="$original" "$TMP"
chown --reference="$original" "$TMP"
mv -f "$TMP" "$original"

Remarks:

  • I use $original in the mktemp template to ensure that the temporary file is not placed in /tmp but in the same folder as $original. I believe that if /tmp is mounted on a different filesystem the operation would no longer be atomic.
  • The result of mktemp is now quoted in case it contains whitespace.
  • I use $() instead of `` because I consider it cleaner.
  • ch{mod,own} --reference are used to transfer the permissions of $original to $TMP. If someone has additional ideas what metadata can and should be transferred, then please edit my post and add it.
  • Oh well, this requires root permissions as Gilles pointed out. Well, I'm not going to discard this now that I've written it :P