If par2 generate is interrupted, it may leave empty *.par2 files, and
not just the top-level PATH.par2 index.  To avoid that possiblity, run
par2 in a temporary directory and then move the recovery files to the
right place if par2 succeeds.
fsck currently checks whether the top-level index is empty, but
doesn't check any of the other vol files.
Thanks to Johannes Berg for reporting the problem.
Signed-off-by: Rob Browning <
r...@defaultvalue.org>
Tested-by: Rob Browning <
r...@defaultvalue.org>
---
 lib/bup/cmd/fsck.py | 39 ++++++++++++++++++++++++++++++---------
 note/
0.33.x.md      |  6 ++++++
 2 files changed, 36 insertions(+), 9 deletions(-)
diff --git a/lib/bup/cmd/fsck.py b/lib/bup/cmd/fsck.py
index 16a2f2178..f0aac3583 100644
--- a/lib/bup/cmd/fsck.py
+++ b/lib/bup/cmd/fsck.py
@@ -1,14 +1,14 @@
 
-from __future__ import absolute_import, print_function
 from shutil import rmtree
 from subprocess import PIPE
 from tempfile import mkdtemp
 from binascii import hexlify
+from os.path import join
 import glob, os, subprocess, sys
 
 from bup import options, git
 from bup.compat import argv_bytes
-from bup.helpers import Sha1, chunkyreader, istty2, log, progress
+from bup.helpers import Sha1, chunkyreader, istty2, log, progress, temp_dir
 from 
bup.io import byte_stream
 
 
@@ -20,14 +20,14 @@ def debug(s):
     if opt.verbose > 1:
         log(s)
 
-def run(argv):
+def run(argv, *, cwd=None):
     # at least in python 2.5, using "stdout=2" or "stdout=sys.stderr" below
     # doesn't actually work, because subprocess closes fd #2 right before
     # execing for some reason.  So we work around it by duplicating the fd
     # first.
     fd = os.dup(2)  # copy stderr
     try:
-        p = subprocess.Popen(argv, stdout=fd, close_fds=False)
+        p = subprocess.Popen(argv, stdout=fd, close_fds=False, cwd=cwd)
         return p.wait()
     finally:
         os.close(fd)
@@ -70,7 +70,7 @@ def is_par2_parallel():
 
 _par2_parallel = None
 
-def par2(action, args, verb_floor=0):
+def par2(action, args, verb_floor=0, cwd=None):
     global _par2_parallel
     if _par2_parallel is None:
         _par2_parallel = is_par2_parallel()
@@ -82,12 +82,33 @@ def par2(action, args, verb_floor=0):
     if _par2_parallel:
         cmd.append(b'-t1')
     cmd.extend(args)
-    return run(cmd)
+    return run(cmd, cwd=cwd)
 
 def par2_generate(base):
-    return par2(b'create',
-                [b'-n1', b'-c200', b'--', base, base + b'.pack', base + b'.idx'],
-                verb_floor=2)
+    parent, name = os.path.split(base)
+    # Work in a temp_dir because par2 was observed creating empty
+    # files when interrupted by C-c.
+    # cf. 
https://github.com/Parchive/par2cmdline/issues/84
+    with temp_dir(dir=parent, prefix=(name + b'-bup-tmp-')) as tmpdir:
+        idx_name = name + b'.idx'
+        pack_name = name + b'.pack'
+        os.symlink(join(b'../', idx_name), join(tmpdir, idx_name))
+        os.symlink(join(b'../', pack_name), join(tmpdir, pack_name))
+        rc = par2(b'create',
+                  [b'-n1', b'-c200', b'--', name, pack_name, idx_name],
+                  verb_floor=2, cwd=tmpdir)
+        if rc == 0:
+            p2_idx = name + b'.par2'
+            for tmp in os.listdir(tmpdir):
+                if tmp in (p2_idx, idx_name, pack_name):
+                    continue
+                os.rename(join(tmpdir, tmp), join(parent, tmp))
+            # Let this indicate success
+            os.rename(join(tmpdir, p2_idx), join(parent, p2_idx))
+            expected = frozenset((idx_name, pack_name))
+            remaining = frozenset(os.listdir(tmpdir))
+            assert expected == remaining
+        return rc
 
 def par2_verify(base):
     return par2(b'verify', [b'--', base], verb_floor=3)
diff --git a/note/
0.33.x.md b/note/
0.33.x.md
index 32d739697..1fbd153fa 100644
--- a/note/
0.33.x.md
+++ b/note/
0.33.x.md
@@ -4,6 +4,12 @@ Notable changes in main since 0.33.3 (incomplete)
 May require attention
 ---------------------
 
+* The `par2` command (invoked by `bup fsck -g`) may generate empty
+  recovery files if interrupted (say via C-c).  To mitigate this, bup
+  now runs `par2` in a temporary directory, and only moves the
+  recovery files into place if the generation succeeds.  See also
+  
https://github.com/Parchive/par2cmdline/issues/84
+
 * Previously, any `bup on REMOTE ...` commands that attempted to read
   from standard input (for example `bup on HOST split < something` or
   `bup on HOST split --git-ids ...`) would read nothing instead of the
-- 
2.43.0