Dereference symlinks before hardlinking

(see #5676)
This commit is contained in:
Emi Katagiri-Simpson 2025-03-22 23:15:45 -04:00
parent 670a3bcd17
commit 7acf2b3acf
No known key found for this signature in database
3 changed files with 24 additions and 2 deletions

View file

@ -605,7 +605,9 @@ def hardlink(path: bytes, dest: bytes, replace: bool = False):
if os.path.exists(syspath(dest)) and not replace:
raise FilesystemError("file exists", "rename", (path, dest))
try:
os.link(syspath(path), syspath(dest))
# This step dereferences any symlinks and converts to an absolute path
resolved_origin = Path(syspath(path)).resolve()
os.link(resolved_origin, syspath(dest))
except NotImplementedError:
raise FilesystemError(
"OS does not support hard links." "link",

View file

@ -29,6 +29,10 @@ Bug fixes:
* :doc:`plugins/fetchart`: Fix fetchart bug where a tempfile could not be deleted due to never being
properly closed.
:bug:`5521`
* When hardlinking from a symlink (e.g. importing a symlink with hardlinking
enabled), dereference the symlink then hardlink, rather than creating a new
(potentially broken) symlink
:bug:`5676`
* :doc:`plugins/lyrics`: LRCLib will fallback to plain lyrics if synced lyrics
are not found and `synced` flag is set to `yes`.
* Synchronise files included in the source distribution with what we used to

View file

@ -35,7 +35,8 @@ class MoveTest(BeetsTestCase):
super().setUp()
# make a temporary file
self.path = join(self.temp_dir, b"temp.mp3")
self.temp_music_file_name = b"temp.mp3"
self.path = join(self.temp_dir, self.temp_music_file_name)
shutil.copy(
syspath(join(_common.RSRC, b"full.mp3")),
syspath(self.path),
@ -199,6 +200,21 @@ class MoveTest(BeetsTestCase):
self.i.move(operation=MoveOperation.HARDLINK)
assert self.i.path == util.normpath(self.dest)
@unittest.skipUnless(_common.HAVE_HARDLINK, "need hardlinks")
def test_hardlink_from_symlink(self):
link_path = join(self.temp_dir, b"temp_link.mp3")
link_source = join(b"./", self.temp_music_file_name)
os.symlink(syspath(link_source), syspath(link_path))
self.i.path = link_path
self.i.move(operation=MoveOperation.HARDLINK)
s1 = os.stat(syspath(self.path))
s2 = os.stat(syspath(self.dest))
assert (s1[stat.ST_INO], s1[stat.ST_DEV]) == (
s2[stat.ST_INO],
s2[stat.ST_DEV],
)
class HelperTest(BeetsTestCase):
def test_ancestry_works_on_file(self):