Skip to content

Commit 7acf2b3

Browse files
committed
Dereference symlinks before hardlinking
(see #5676)
1 parent 670a3bc commit 7acf2b3

File tree

3 files changed

+24
-2
lines changed

3 files changed

+24
-2
lines changed

beets/util/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,9 @@ def hardlink(path: bytes, dest: bytes, replace: bool = False):
605605
if os.path.exists(syspath(dest)) and not replace:
606606
raise FilesystemError("file exists", "rename", (path, dest))
607607
try:
608-
os.link(syspath(path), syspath(dest))
608+
# This step dereferences any symlinks and converts to an absolute path
609+
resolved_origin = Path(syspath(path)).resolve()
610+
os.link(resolved_origin, syspath(dest))
609611
except NotImplementedError:
610612
raise FilesystemError(
611613
"OS does not support hard links." "link",

docs/changelog.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ Bug fixes:
2929
* :doc:`plugins/fetchart`: Fix fetchart bug where a tempfile could not be deleted due to never being
3030
properly closed.
3131
:bug:`5521`
32+
* When hardlinking from a symlink (e.g. importing a symlink with hardlinking
33+
enabled), dereference the symlink then hardlink, rather than creating a new
34+
(potentially broken) symlink
35+
:bug:`5676`
3236
* :doc:`plugins/lyrics`: LRCLib will fallback to plain lyrics if synced lyrics
3337
are not found and `synced` flag is set to `yes`.
3438
* Synchronise files included in the source distribution with what we used to

test/test_files.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ def setUp(self):
3535
super().setUp()
3636

3737
# make a temporary file
38-
self.path = join(self.temp_dir, b"temp.mp3")
38+
self.temp_music_file_name = b"temp.mp3"
39+
self.path = join(self.temp_dir, self.temp_music_file_name)
3940
shutil.copy(
4041
syspath(join(_common.RSRC, b"full.mp3")),
4142
syspath(self.path),
@@ -199,6 +200,21 @@ def test_hardlink_changes_path(self):
199200
self.i.move(operation=MoveOperation.HARDLINK)
200201
assert self.i.path == util.normpath(self.dest)
201202

203+
@unittest.skipUnless(_common.HAVE_HARDLINK, "need hardlinks")
204+
def test_hardlink_from_symlink(self):
205+
link_path = join(self.temp_dir, b"temp_link.mp3")
206+
link_source = join(b"./", self.temp_music_file_name)
207+
os.symlink(syspath(link_source), syspath(link_path))
208+
self.i.path = link_path
209+
self.i.move(operation=MoveOperation.HARDLINK)
210+
211+
s1 = os.stat(syspath(self.path))
212+
s2 = os.stat(syspath(self.dest))
213+
assert (s1[stat.ST_INO], s1[stat.ST_DEV]) == (
214+
s2[stat.ST_INO],
215+
s2[stat.ST_DEV],
216+
)
217+
202218

203219
class HelperTest(BeetsTestCase):
204220
def test_ancestry_works_on_file(self):

0 commit comments

Comments
 (0)