Linux Permissions – Set Permissions with Rsync for All Files Except Root Directory

linuxpermissionsrsync

I want to sync two directories with rsync, preserving the permissions of all files. My user foo does have write access to the target directory, but does not own it.

$ ls -l                                                                                                                                                                                
total 8
drwxrwx--- 2 foo  foo 4096 Jun  3 16:01 a
drwxrwxr-x 3 root foo 4096 Jun  3 16:02 b

While syncing does work, it causes a permission error and a bad exit code:

$ rsync -av  -O  --delete a/ b/                                                                                                                                                        
sending incremental file list
rsync: failed to set permissions on "/tmp/r/b/.": Operation not permitted (1)
deleting 2
./
1

sent 115 bytes  received 138 bytes  506.00 bytes/sec
total size is 0  speedup is 0.00
rsync error: some files/attrs were not transferred (see previous errors) (code 23) at main.c(1196) [sender=3.1.2]

I am aware, that I could use the --no-perm option, but this would prevent setting the permissions of any files and not just of the target directory.

Another solution would be to use rsync -av --delete a/* b/, but this would prevent deleted files in a/ from being removed in b/.

This is probably a duplicate of this question, which remains unanswered since 2010 🙁

Best Answer

Another solution would be to use rsync -av --delete a/* b/, but this would prevent deleted files in a/ from being removed in b/.

And then you run

rsync -rv --delete --existing --ignore-existing a/ b/

to deal with this. You don't use -a here so the ownership of the target directory is not a problem.

From man 1 rsync:

--existing, --ignore-non-existing
This tells rsync to skip creating files (including directories) that do not exist yet on the destination. If this option is combined with the --ignore-existing option, no files will be updated (which can be useful if all you want to do is delete extraneous files).

(Credits to this answer.)


Improvements:

  • In general * is not enough to match all files and directories. You also need .[!.]* if you have dot files, and ..?* if you have file names beginning with two dots. In Bash dotglob helps, so additional patterns are not needed:

    # in Bash
    shopt -s dotglob
    rsync -av --delete a/* b/
    

    But you need to make sure that the pattern expands to something. If it doesn't, rsync will get its literal form and complain. Creating a dummy (temporary) file in a/ may be inelegant, but it will certainly make the pattern expand. If you remove the file before you run the second rsync then it will be removed from the destination as well.

  • If you ever decide to work from within a/ directory, the pattern will become *. In this case you should use double dash (--) or modify the pattern (./*) so file names like -n are not treated as options.

An example solution may be like this:

#!/bin/bash

shopt -s dotglob

tmpf="$(mktemp -p a/)" || exit 202
rsync -av --delete a/* b/
rm "$tmpf"
rsync -rv --delete --existing --ignore-existing a/ b/

Notes:

  • Each rsync generates its own exit status, you need additional logic to tell if everything was OK.
  • If there are many objects in a/ then the first invocation of rsync may yield argument list too long. Possible solutions:

    • find + xargs.
    • Processing objects one by one in a loop:

      for f in a/*; do
         rsync -av --delete -- "$f" b/
      done
      

      (in this case use shopt -s nullglob beforehand and you won't even need a dummy file).

      Note I used -- even though it's not necessary in this particular case (because expanded $f must start with a/). But let's say you modify the code and change a/* to *. Then you need to add -- in another line, it's very easy to miss this.

Related Question