Skip to content

[BUG] restoring a subdirectory doesn't play well with include/exclude filters #463

@lpancescu

Description

@lpancescu

Bug summary

rdiff-backup crashes due to a Python OSError exception while trying to restore a subdirectory if I specify include/exclude filters, and the files I'm trying to restore are completely gone from the destination directory.

Version, Python, Operating System

rdiff-backup 2.0.5, Python 3.8.5, Fedora 32

Call rdiff-backup -v9 and paste the output between the backquotes below, repeat for each environment impacted:

2020-09-11 22:59:21.708502 +0200  <CLIENT-7215>  Fatal Error: No arguments given
See the rdiff-backup manual page for more information.
2020-09-11 22:59:21.703167 +0200  <CLIENT-7215>  Using rdiff-backup version 2.0.5
2020-09-11 22:59:21.703273 +0200  <CLIENT-7215>  	with cpython /usr/bin/python3 version 3.8.5
2020-09-11 22:59:21.708363 +0200  <CLIENT-7215>  	on Linux-5.8.6-201.fc32.x86_64-x86_64-with-glibc2.2.5, fs encoding utf-8

rdiff-backup call

I have a fully reproducible test case. First, let's create a few files and directories to play with:

$ mkdir -p backup original/dir{1,2}
$ cd original
$ echo file1 | tee dir1/file1.txt > dir2/file1.txt
$ echo file2 | tee dir1/file2.txt > dir2/file2.txt
$ echo base > base.txt
$ cd -

Create our first backup in the empty backup directory, then overwrite a file with garbage and :

$ rdiff-backup original backup
$ echo garbage > original/dir1/file2.txt

We can now try to selectively restore dir1:

$ rdiff-backup -r now --force --include original/dir1/file2.txt --exclude '**' backup/dir1 original/dir1
Exception '[Errno 39] Directory not empty: b'original/dir1'' raised of class '<class 'OSError'>':
  File "/usr/lib64/python3.8/site-packages/rdiff_backup/Main.py", line 395, in error_check_Main
    Main(arglist)
  File "/usr/lib64/python3.8/site-packages/rdiff_backup/Main.py", line 417, in Main
    take_action(rps)
  File "/usr/lib64/python3.8/site-packages/rdiff_backup/Main.py", line 373, in take_action
    Restore(rps[0], rps[1], 1)
  File "/usr/lib64/python3.8/site-packages/rdiff_backup/Main.py", line 720, in Restore
    restore.Restore(
  File "/usr/lib64/python3.8/site-packages/rdiff_backup/restore.py", line 42, in Restore
    TargetS.patch(target, diff_iter)
  File "/usr/lib64/python3.8/site-packages/rdiff_backup/restore.py", line 337, in patch
    ITR.Finish()
  File "/usr/lib64/python3.8/site-packages/rdiff_backup/rorpiter.py", line 277, in Finish
    to_be_finished.end_process()
  File "/usr/lib64/python3.8/site-packages/rdiff_backup/restore.py", line 762, in end_process
    self.base_rp.rmdir()
  File "/usr/lib64/python3.8/site-packages/rdiff_backup/rpath.py", line 1222, in rmdir
    self.conn.os.rmdir(self.path)

Traceback (most recent call last):
  File "/usr/bin/rdiff-backup", line 32, in <module>
    rdiff_backup.Main.error_check_Main(sys.argv[1:])
  File "/usr/lib64/python3.8/site-packages/rdiff_backup/Main.py", line 395, in error_check_Main
    Main(arglist)
  File "/usr/lib64/python3.8/site-packages/rdiff_backup/Main.py", line 417, in Main
    take_action(rps)
  File "/usr/lib64/python3.8/site-packages/rdiff_backup/Main.py", line 373, in take_action
    Restore(rps[0], rps[1], 1)
  File "/usr/lib64/python3.8/site-packages/rdiff_backup/Main.py", line 720, in Restore
    restore.Restore(
  File "/usr/lib64/python3.8/site-packages/rdiff_backup/restore.py", line 42, in Restore
    TargetS.patch(target, diff_iter)
  File "/usr/lib64/python3.8/site-packages/rdiff_backup/restore.py", line 337, in patch
    ITR.Finish()
  File "/usr/lib64/python3.8/site-packages/rdiff_backup/rorpiter.py", line 277, in Finish
    to_be_finished.end_process()
  File "/usr/lib64/python3.8/site-packages/rdiff_backup/restore.py", line 762, in end_process
    self.base_rp.rmdir()
  File "/usr/lib64/python3.8/site-packages/rdiff_backup/rpath.py", line 1222, in rmdir
    self.conn.os.rmdir(self.path)
OSError: [Errno 39] Directory not empty: b'original/dir1'

What happened and what did you expect?

I expected rdiff-backup to restore the dir1/file1.txt instead of crashing. Even worse, dir1/file2.txt disappeared completely after the crash, but it can fortunately still be recovered from the backup directory:

$ find original/ -type f
original/dir2/file2.txt
original/dir2/file1.txt
original/dir1/file1.txt
$ ls backup/dir1
file1.txt  file2.txt

More information

Here's the -v9 output:

rdiff-backup.log

Possible workarounds:

  • restore the entire directory, losing changes to the other files: rdiff-backup -r now --force backup/dir1 original/dir1
  • use include/exclude filters without subdirectories: rdiff-backup -r now --force --include 'original/dir1/file2.txt' --exclude '**' backup original
  • use rsync, which offers its own filters (this only works for the latest backed up version, not older ones): rsync -a --include 'file2.txt' --exclude '**' backup/dir1/ original/dir1

The test case might seem contrived, since rdiff-backup backup/dir1/file1.txt original/dir1/file1.txt is easier, but I discovered this using rdiff-backup with a globbing filelist to restore several file in the /root directory - it was a bit scary to see it tried to remove the /root directory, running with root privileges.

I've been using rdiff-backup since at least 2009, many thanks for reviving the project!

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions