[PATCH 12/17] fsck: fall back to copy2 from link for par2 sources when necessary

0 views
Skip to first unread message

Rob Browning

unread,
Nov 8, 2025, 4:39:01 PMNov 8
to bup-...@googlegroups.com
If the platform/filesystem doesn't appear to support hard links, fall
back to copy2(). See also:

commit daa6bf3eeabf1f50630b17b0cbbbdcbbef59c90e
fsck: switch from symlinks to hard links for par2 sandbox

Thanks to Daniel Distler for reporting the issue and Greg Troxel,
and Aaron M. Ucko for help with the errno values.

Signed-off-by: Rob Browning <r...@defaultvalue.org>
Tested-by: Rob Browning <r...@defaultvalue.org>
---
lib/bup/cmd/fsck.py | 19 ++++++++++++++++++-
note/main.md | 2 +-
2 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/lib/bup/cmd/fsck.py b/lib/bup/cmd/fsck.py
index 8dc1ccbc..14673119 100644
--- a/lib/bup/cmd/fsck.py
+++ b/lib/bup/cmd/fsck.py
@@ -1,9 +1,11 @@

+from errno import EMLINK, EOPNOTSUPP, EPERM, ERANGE, EREMOTEIO, EXDEV
from os import SEEK_END
from shutil import rmtree
from subprocess import DEVNULL, PIPE, run
from tempfile import mkdtemp
from os.path import join
+from shutil import copy2
import glob, os, sys

from bup import options, git
@@ -81,7 +83,22 @@ def par2_generate(stem):
# cf. https://github.com/Parchive/par2cmdline/issues/84
with temp_dir(dir=parent, prefix=(base + b'-bup-tmp-')) as tmpdir:
pack = base + b'.pack'
- os.link(join(tmpdir, b'..', pack), join(tmpdir, pack))
+ pack_src = join(tmpdir, b'..', pack)
+ pack_dst = join(tmpdir, pack)
+ copy_instead = False
+ try:
+ os.link(pack_src, pack_dst)
+ except OSError as ex:
+ if ex.errno not in (EMLINK,
+ EOPNOTSUPP, # freebsd
+ EPERM, # linux
+ ERANGE, # cryfs
+ EREMOTEIO, # kafs (cross-directory)
+ EXDEV): # openafs (cross-directory)
+ raise
+ copy_instead = True
+ if copy_instead:
+ copy2(pack_src, pack_dst)
rc = par2(b'create', [b'-n1', b'-c200', b'--', base, pack],
verb_floor=2, cwd=tmpdir)
if rc == 0:
diff --git a/note/main.md b/note/main.md
index 84b0265b..3b7732d0 100644
--- a/note/main.md
+++ b/note/main.md
@@ -177,7 +177,7 @@ Bugs
* `par2` changed its behavior in 1.0 to be incompatible with `bup`'s
use of symlinks to mitigate a `par2` bug (see the [0.33.4 release
notes](0.33.4-from-0.33.3.md) for additional information. `bup` now
- uses hardlinks instead.
+ uses hardlinks if possible, and copies the files if not.

* `bup rm some/SAVE` should now succeed even if the root metadata (the
`some/SAVE/.bupm` file) is missing. Previously it failed with
--
2.47.3

Rob Browning

unread,
Nov 19, 2025, 5:23:43 PMNov 19
to bup-...@googlegroups.com
If the platform/filesystem doesn't appear to support hard links, fall
back to copy2(). See also:

commit daa6bf3eeabf1f50630b17b0cbbbdcbbef59c90e
fsck: switch from symlinks to hard links for par2 sandbox

Thanks to Daniel Distler for reporting the issue and Greg Troxel,
and Aaron M. Ucko for help with the errno values.

Signed-off-by: Rob Browning <r...@defaultvalue.org>
Tested-by: Rob Browning <r...@defaultvalue.org>
---

Pushed to main.

lib/bup/cmd/fsck.py | 25 +++++++++++++++++++++++--
note/main.md | 2 +-
2 files changed, 24 insertions(+), 3 deletions(-)

diff --git a/lib/bup/cmd/fsck.py b/lib/bup/cmd/fsck.py
index 8dc1ccbc..348cde28 100644
--- a/lib/bup/cmd/fsck.py
+++ b/lib/bup/cmd/fsck.py
@@ -4,7 +4,8 @@ from shutil import rmtree
from subprocess import DEVNULL, PIPE, run
from tempfile import mkdtemp
from os.path import join
-import glob, os, sys
+from shutil import copy2
+import errno, glob, os, sys

from bup import options, git
from bup.compat import argv_bytes
@@ -74,6 +75,16 @@ def par2(action, args, verb_floor=0, cwd=None):
cmd.extend(args)
return run(cmd, stdout=2, cwd=cwd).returncode

+_unable_to_link = set()
+_unable_to_link.add(getattr(errno, 'EMLINK', None))
+_unable_to_link.add(getattr(errno, 'EOPNOTSUPP', None)) # freebsd
+_unable_to_link.add(getattr(errno, 'EPERM', None)) # linux
+_unable_to_link.add(getattr(errno, 'ERANGE', None)) # cryfs
+_unable_to_link.add(getattr(errno, 'EREMOTEIO', None)) # kafs (cross-directory)
+_unable_to_link.add(getattr(errno, 'EXDEV', None)) # openafs (cross-directory)
+_unable_to_link.discard(None)
+_unable_to_link = frozenset(_unable_to_link)
+
def par2_generate(stem):
parent, base = os.path.split(stem)
# Work in a temp_dir because par2 was observed creating empty
@@ -81,7 +92,17 @@ def par2_generate(stem):
# cf. https://github.com/Parchive/par2cmdline/issues/84
with temp_dir(dir=parent, prefix=(base + b'-bup-tmp-')) as tmpdir:
pack = base + b'.pack'
- os.link(join(tmpdir, b'..', pack), join(tmpdir, pack))
+ pack_src = join(tmpdir, b'..', pack)
+ pack_dst = join(tmpdir, pack)
+ copy_instead = False
+ try:
+ os.link(pack_src, pack_dst)
+ except OSError as ex:
+ if not ex.errno in _unable_to_link:
Reply all
Reply to author
Forward
0 new messages