Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FIX: Dereference symlinks before hardlinking #5684

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion beets/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,9 @@
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",
Expand Down Expand Up @@ -800,10 +802,10 @@

if fragment:
# Outputting Unicode.
extension = extension.decode("utf-8", "ignore")

Check failure on line 805 in beets/util/__init__.py

View workflow job for this annotation

GitHub Actions / Check types with mypy

Incompatible types in assignment (expression has type "str", variable has type "bytes")

first_stage_path, _ = _legalize_stage(
path, replacements, length, extension, fragment

Check failure on line 808 in beets/util/__init__.py

View workflow job for this annotation

GitHub Actions / Check types with mypy

Argument 4 to "_legalize_stage" has incompatible type "bytes"; expected "str"
)

# Convert back to Unicode with extension removed.
Expand All @@ -811,14 +813,14 @@

# Re-sanitize following truncation (including user replacements).
second_stage_path, retruncated = _legalize_stage(
first_stage_path, replacements, length, extension, fragment

Check failure on line 816 in beets/util/__init__.py

View workflow job for this annotation

GitHub Actions / Check types with mypy

Argument 4 to "_legalize_stage" has incompatible type "bytes"; expected "str"
)

# If the path was once again truncated, discard user replacements
# and run through one last legalization stage.
if retruncated:
second_stage_path, _ = _legalize_stage(
first_stage_path, None, length, extension, fragment

Check failure on line 823 in beets/util/__init__.py

View workflow job for this annotation

GitHub Actions / Check types with mypy

Argument 4 to "_legalize_stage" has incompatible type "bytes"; expected "str"
)

return second_stage_path, retruncated
Expand Down
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion test/test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ def setUp(self):
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),
Expand Down Expand Up @@ -199,6 +200,21 @@ def test_hardlink_changes_path(self):
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):
Expand Down
Loading