From 7fb3c24c102dea06d62893f381874222e1652030 Mon Sep 17 00:00:00 2001 From: Ruben De Smet Date: Fri, 28 Jul 2017 16:55:53 +0200 Subject: [PATCH 01/12] Add reflink to setup requirements and config. --- beets/config_default.yaml | 1 + beets/importer.py | 17 +++++++++++++++-- beets/util/__init__.py | 2 ++ setup.py | 2 ++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/beets/config_default.yaml b/beets/config_default.yaml index 0fd6eb592..892a5a336 100644 --- a/beets/config_default.yaml +++ b/beets/config_default.yaml @@ -7,6 +7,7 @@ import: move: no link: no hardlink: no + reflink: no delete: no resume: ask incremental: no diff --git a/beets/importer.py b/beets/importer.py index 68d5f3d5d..9c86b21c6 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -222,19 +222,30 @@ class ImportSession(object): iconfig['resume'] = False iconfig['incremental'] = False - # Copy, move, link, and hardlink are mutually exclusive. + if iconfig['reflink']: + iconfig['reflink'] = iconfig['reflink'].as_choice(['auto', True, False]) + + # Copy, move, reflink, link, and hardlink are mutually exclusive. if iconfig['move']: iconfig['copy'] = False iconfig['link'] = False iconfig['hardlink'] = False + iconfig['reflink'] = False elif iconfig['link']: iconfig['copy'] = False iconfig['move'] = False iconfig['hardlink'] = False + iconfig['reflink'] = False elif iconfig['hardlink']: iconfig['copy'] = False iconfig['move'] = False iconfig['link'] = False + iconfig['reflink'] = False + elif iconfig['reflink']: + iconfig['copy'] = False + iconfig['move'] = False + iconfig['link'] = False + iconfig['hardlink'] = False # Only delete when copying. if not iconfig['copy']: @@ -707,7 +718,7 @@ class ImportTask(BaseImportTask): item.update(changes) def manipulate_files(self, operation=None, write=False, session=None): - """ Copy, move, link or hardlink (depending on `operation`) the files + """ Copy, move, link, hardlink or reflink (depending on `operation`) the files as well as write metadata. `operation` should be an instance of `util.MoveOperation`. @@ -1536,6 +1547,8 @@ def manipulate_files(session, task): operation = MoveOperation.LINK elif session.config['hardlink']: operation = MoveOperation.HARDLINK + elif session.config['reflink']: + operation = MoveOperation.REFLINK else: operation = None diff --git a/beets/util/__init__.py b/beets/util/__init__.py index bb84aedc7..90f12e639 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -134,6 +134,8 @@ class MoveOperation(Enum): COPY = 1 LINK = 2 HARDLINK = 3 + REFLINK = 4 + REFLINK_AUTO = 5 def normpath(path): diff --git a/setup.py b/setup.py index 0e2cb332a..55714654e 100755 --- a/setup.py +++ b/setup.py @@ -93,6 +93,7 @@ setup( 'pyyaml', 'mediafile>=0.2.0', 'confuse>=1.0.0', + 'reflink', ] + [ # Avoid a version of munkres incompatible with Python 3. 'munkres~=1.0.0' if sys.version_info < (3, 5, 0) else @@ -123,6 +124,7 @@ setup( 'rarfile', 'responses>=0.3.0', 'requests_oauthlib', + 'reflink', ] + ( # Tests for the thumbnails plugin need pathlib on Python 2 too. ['pathlib'] if (sys.version_info < (3, 4, 0)) else [] From 2926b4962835e651cf2a0dde8115c59653e95350 Mon Sep 17 00:00:00 2001 From: Ruben De Smet Date: Fri, 28 Jul 2017 17:37:09 +0200 Subject: [PATCH 02/12] Add HAVE_REFLINK flag for tests --- test/_common.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/_common.py b/test/_common.py index 8e3b1dd18..e44fac48b 100644 --- a/test/_common.py +++ b/test/_common.py @@ -25,6 +25,8 @@ import six import unittest from contextlib import contextmanager +import reflink + # Mangle the search path to include the beets sources. sys.path.insert(0, '..') @@ -55,6 +57,7 @@ _item_ident = 0 # OS feature test. HAVE_SYMLINK = sys.platform != 'win32' HAVE_HARDLINK = sys.platform != 'win32' +HAVE_REFLINK = reflink.supported_at(tempfile.gettempdir()) def item(lib=None): From 5e2856ef87dd31fc73c7f852b230134beced1920 Mon Sep 17 00:00:00 2001 From: Ruben De Smet Date: Fri, 28 Jul 2017 16:51:19 +0200 Subject: [PATCH 03/12] Add reflink routine --- beets/importer.py | 3 ++- beets/library.py | 20 ++++++++++++++++++++ beets/util/__init__.py | 23 +++++++++++++++++++++++ test/test_files.py | 40 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 1 deletion(-) diff --git a/beets/importer.py b/beets/importer.py index 9c86b21c6..3220b260f 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -223,7 +223,8 @@ class ImportSession(object): iconfig['incremental'] = False if iconfig['reflink']: - iconfig['reflink'] = iconfig['reflink'].as_choice(['auto', True, False]) + iconfig['reflink'] = iconfig['reflink'] \ + .as_choice(['auto', True, False]) # Copy, move, reflink, link, and hardlink are mutually exclusive. if iconfig['move']: diff --git a/beets/library.py b/beets/library.py index e22d4edc0..dea2a937e 100644 --- a/beets/library.py +++ b/beets/library.py @@ -747,6 +747,20 @@ class Item(LibModel): util.hardlink(self.path, dest) plugins.send("item_hardlinked", item=self, source=self.path, destination=dest) + elif operation == MoveOperation.REFLINK: + util.reflink(self.path, dest, fallback=False) + plugins.send("item_reflinked", item=self, source=self.path, + destination=dest) + elif operation == MoveOperation.REFLINK_AUTO: + util.reflink(self.path, dest, fallback=True) + plugins.send("item_reflinked", item=self, source=self.path, + destination=dest) + else: + plugins.send("before_item_moved", item=self, source=self.path, + destination=dest) + util.move(self.path, dest) + plugins.send("item_moved", item=self, source=self.path, + destination=dest) # Either copying or moving succeeded, so update the stored path. self.path = dest @@ -1087,6 +1101,12 @@ class Album(LibModel): util.link(old_art, new_art) elif operation == MoveOperation.HARDLINK: util.hardlink(old_art, new_art) + elif operation == MoveOperation.REFLINK: + util.reflink(old_art, new_art, fallback=False) + elif operation == MoveOperation.REFLINK_AUTO: + util.reflink(old_art, new_art, fallback=True) + else: + util.move(old_art, new_art) self.artpath = new_art def move(self, operation=MoveOperation.MOVE, basedir=None, store=True): diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 90f12e639..3bd2a7649 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -34,6 +34,7 @@ from beets.util import hidden import six from unidecode import unidecode from enum import Enum +import reflink as pyreflink MAX_FILENAME_LENGTH = 200 @@ -547,6 +548,28 @@ def hardlink(path, dest, replace=False): traceback.format_exc()) +def reflink(path, dest, replace=False, fallback=False): + """Create a reflink from `dest` to `path`. Raises an `OSError` if + `dest` already exists, unless `replace` is True. Does nothing if + `path` == `dest`. When `fallback` is True, `reflink` falls back on + `copy` when the filesystem does not support reflinks. + """ + if samefile(path, dest): + return + + if os.path.exists(syspath(dest)) and not replace: + raise FilesystemError(u'file exists', 'rename', (path, dest)) + + try: + pyreflink.reflink(path, dest) + except (NotImplementedError, pyreflink.ReflinkImpossibleError) as exc: + if fallback: + copy(path, dest, replace) + else: + raise FilesystemError(u'OS/filesystem does not support reflinks.', + 'link', (path, dest), traceback.format_exc()) + + def unique_path(path): """Returns a version of ``path`` that does not exist on the filesystem. Specifically, if ``path` itself already exists, then diff --git a/test/test_files.py b/test/test_files.py index f31779672..e9aee3f5b 100644 --- a/test/test_files.py +++ b/test/test_files.py @@ -86,6 +86,24 @@ class MoveTest(_common.TestCase): self.i.move(operation=MoveOperation.COPY) self.assertExists(self.path) + def test_reflink_arrives(self): + self.i.move(operation=MoveOperation.REFLINK_AUTO) + self.assertExists(self.dest) + + def test_reflink_does_not_depart(self): + self.i.move(operation=MoveOperation.REFLINK_AUTO) + self.assertExists(self.path) + + @unittest.skipUnless(_common.HAVE_REFLINK, "need reflink") + def test_force_reflink_arrives(self): + self.i.move(operation=MoveOperation.REFLINK) + self.assertExists(self.dest) + + @unittest.skipUnless(_common.HAVE_REFLINK, "need reflink") + def test_force_reflink_does_not_depart(self): + self.i.move(operation=MoveOperation.REFLINK) + self.assertExists(self.path) + def test_move_changes_path(self): self.i.move() self.assertEqual(self.i.path, util.normpath(self.dest)) @@ -249,6 +267,17 @@ class AlbumFileTest(_common.TestCase): self.assertTrue(os.path.exists(oldpath)) self.assertTrue(os.path.exists(self.i.path)) + @unittest.skipUnless(_common.HAVE_REFLINK, "need reflink") + def test_albuminfo_move_reflinks_file(self): + oldpath = self.i.path + self.ai.album = u'newAlbumName' + self.ai.move(operation=MoveOperation.REFLINK) + self.ai.store() + self.i.load() + + self.assertTrue(os.path.exists(oldpath)) + self.assertTrue(os.path.exists(self.i.path)) + def test_albuminfo_move_to_custom_dir(self): self.ai.move(basedir=self.otherdir) self.i.load() @@ -530,6 +559,12 @@ class SafeMoveCopyTest(_common.TestCase): self.assertExists(self.dest) self.assertExists(self.path) + @unittest.skipUnless(_common.HAVE_REFLINK, "need reflink") + def test_successful_reflink(self): + util.reflink(self.path, self.dest) + self.assertExists(self.dest) + self.assertExists(self.path) + def test_unsuccessful_move(self): with self.assertRaises(util.FilesystemError): util.move(self.path, self.otherpath) @@ -538,6 +573,11 @@ class SafeMoveCopyTest(_common.TestCase): with self.assertRaises(util.FilesystemError): util.copy(self.path, self.otherpath) + @unittest.skipUnless(_common.HAVE_REFLINK, "need reflink") + def test_unsuccessful_reflink(self): + with self.assertRaises(util.FilesystemError): + util.reflink(self.path, self.otherpath) + def test_self_move(self): util.move(self.path, self.path) self.assertExists(self.path) From e1def7559ed598b280b8c8f0b19a0b62195142b0 Mon Sep 17 00:00:00 2001 From: Ruben De Smet Date: Sun, 30 Jul 2017 17:12:04 +0200 Subject: [PATCH 04/12] Add reflink docs --- docs/changelog.rst | 3 +++ docs/dev/plugins.rst | 4 ++++ docs/reference/config.rst | 21 +++++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0f41c38ec..81ca994a8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,6 +8,9 @@ New features: * :doc:`/plugins/lastgenre`: Added more heavy metal genres: https://en.wikipedia.org/wiki/Heavy_metal_genres to genres.txt and genres-tree.yaml * :doc:`/plugins/subsonicplaylist`: import playlist from a subsonic server. +* A new :ref:`reflink` config option instructs the importer to create reflinks + on filesystems that support them. Thanks to :user:`rubdos`. + :bug:`2642` * A new :ref:`extra_tags` configuration option allows more tagged metadata to be included in MusicBrainz queries. * A new :doc:`/plugins/fish` adds `Fish shell`_ tab autocompletion to beets diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index 3328654e0..3ead4f860 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -164,6 +164,10 @@ The events currently available are: created for a file. Parameters: ``item``, ``source`` path, ``destination`` path +* `item_reflinked`: called with an ``Item`` object whenever a reflink is + created for a file. + Parameters: ``item``, ``source`` path, ``destination`` path + * `item_removed`: called with an ``Item`` object every time an item (singleton or album's part) is removed from the library (even when its file is not deleted from disk). diff --git a/docs/reference/config.rst b/docs/reference/config.rst index 46f14f2c5..c6e319a43 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -483,6 +483,27 @@ As with symbolic links (see :ref:`link`, above), this will not work on Windows and you will want to set ``write`` to ``no``. Otherwise, metadata on the original file will be modified. +.. _reflink: + +reflink +~~~~~~~ + +Either ``yes``, ``no`` or ``auto``, indicating whether to `reflink clone`_ files +into the library directory when using ``beet import``. Defaults to ``no``. +When ``auto`` is specified, ``reflink`` will fall back on ``copy``, +in case that ``reflink``'s are not supported on the used filesystem. + + +The option is ignored if ``move`` is enabled (i.e., beets can move or +copy files but it doesn't make sense to do both). + + +The option is filesystem dependent. For filesystem support, refer to the +`pyreflink`_ documentation. + +.. _reflink clone: https://blogs.oracle.com/otn/save-disk-space-on-linux-by-cloning-files-on-btrfs-and-ocfs2 +.. _pyreflink: https://reflink.readthedocs.io/en/latest/ + resume ~~~~~~ From 43f27506bfb409564ec2c5d10d008163e2db5a89 Mon Sep 17 00:00:00 2001 From: Ruben De Smet Date: Mon, 31 Jul 2017 17:00:11 +0200 Subject: [PATCH 05/12] Make reflink optional --- beets/util/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 3bd2a7649..425e7ac87 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -34,7 +34,6 @@ from beets.util import hidden import six from unidecode import unidecode from enum import Enum -import reflink as pyreflink MAX_FILENAME_LENGTH = 200 @@ -554,6 +553,7 @@ def reflink(path, dest, replace=False, fallback=False): `path` == `dest`. When `fallback` is True, `reflink` falls back on `copy` when the filesystem does not support reflinks. """ + import reflink as pyreflink if samefile(path, dest): return diff --git a/setup.py b/setup.py index 55714654e..ac7ebc2a3 100755 --- a/setup.py +++ b/setup.py @@ -93,7 +93,6 @@ setup( 'pyyaml', 'mediafile>=0.2.0', 'confuse>=1.0.0', - 'reflink', ] + [ # Avoid a version of munkres incompatible with Python 3. 'munkres~=1.0.0' if sys.version_info < (3, 5, 0) else @@ -161,6 +160,7 @@ setup( 'scrub': ['mutagen>=1.33'], 'bpd': ['PyGObject'], 'replaygain': ['PyGObject'], + 'reflink': ['reflink'], }, # Non-Python/non-PyPI plugin dependencies: # chroma: chromaprint or fpcalc From b78c510ff29434383c2dca4125a1b11f8da489c5 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 29 Oct 2017 15:57:01 -0400 Subject: [PATCH 06/12] Expand docstring for reflink utility --- beets/util/__init__.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 425e7ac87..090151df2 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -548,12 +548,18 @@ def hardlink(path, dest, replace=False): def reflink(path, dest, replace=False, fallback=False): - """Create a reflink from `dest` to `path`. Raises an `OSError` if - `dest` already exists, unless `replace` is True. Does nothing if - `path` == `dest`. When `fallback` is True, `reflink` falls back on - `copy` when the filesystem does not support reflinks. + """Create a reflink from `dest` to `path`. + + Raise an `OSError` if `dest` already exists, unless `replace` is + True. If `path` == `dest`, then do nothing. + + If reflinking fails and `fallback` is enabled, try copying the file + instead. Otherwise, raise an error without trying a plain copy. + + May raise an `ImportError` if the `reflink` module is not available. """ import reflink as pyreflink + if samefile(path, dest): return From 39827394ae600735da328eda580b3947091edf3c Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 29 Oct 2017 15:57:27 -0400 Subject: [PATCH 07/12] Expand the reflink changelog entry --- docs/changelog.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 81ca994a8..5a631f582 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,9 +8,9 @@ New features: * :doc:`/plugins/lastgenre`: Added more heavy metal genres: https://en.wikipedia.org/wiki/Heavy_metal_genres to genres.txt and genres-tree.yaml * :doc:`/plugins/subsonicplaylist`: import playlist from a subsonic server. -* A new :ref:`reflink` config option instructs the importer to create reflinks - on filesystems that support them. Thanks to :user:`rubdos`. - :bug:`2642` +* A new :ref:`reflink` config option instructs the importer to create fast, + copy-on-write file clones on filesystems that support them. Thanks to + :user:`rubdos`. * A new :ref:`extra_tags` configuration option allows more tagged metadata to be included in MusicBrainz queries. * A new :doc:`/plugins/fish` adds `Fish shell`_ tab autocompletion to beets From e7597916a2e9c24da923ad1a921327ad4b896a04 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 29 Oct 2017 16:02:25 -0400 Subject: [PATCH 08/12] Revise reflink docs --- docs/reference/config.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/reference/config.rst b/docs/reference/config.rst index c6e319a43..ffc3a5c32 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -476,7 +476,7 @@ hardlink ~~~~~~~~ Either ``yes`` or ``no``, indicating whether to use hard links instead of -moving or copying or symlinking files. (It conflicts with the ``move``, +moving, copying, or symlinking files. (It conflicts with the ``move``, ``copy``, and ``link`` options.) Defaults to ``no``. As with symbolic links (see :ref:`link`, above), this will not work on Windows @@ -488,19 +488,19 @@ original file will be modified. reflink ~~~~~~~ -Either ``yes``, ``no`` or ``auto``, indicating whether to `reflink clone`_ files -into the library directory when using ``beet import``. Defaults to ``no``. -When ``auto`` is specified, ``reflink`` will fall back on ``copy``, -in case that ``reflink``'s are not supported on the used filesystem. +Either ``yes``, ``no``, or ``auto``, indicating whether to use copy-on-write +file clones (a.k.a. "reflinks") instead of copying or moving files. +The ``auto`` option uses reflinks when possible and falls back to plain +copying when necessary. +Defaults to ``no``. +This kind of clone is only available on certain filesystems: for example, +btrfs and APFS. For more details on filesystem support, see the `pyreflink`_ +documentation. The option is ignored if ``move`` is enabled (i.e., beets can move or copy files but it doesn't make sense to do both). - -The option is filesystem dependent. For filesystem support, refer to the -`pyreflink`_ documentation. - .. _reflink clone: https://blogs.oracle.com/otn/save-disk-space-on-linux-by-cloning-files-on-btrfs-and-ocfs2 .. _pyreflink: https://reflink.readthedocs.io/en/latest/ From 99cd7e2de457af1b05a2974fee975b48f15518dc Mon Sep 17 00:00:00 2001 From: Ruben De Smet Date: Wed, 22 Jul 2020 18:09:33 +0200 Subject: [PATCH 09/12] Fixup flake8 --- beets/util/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 090151df2..d2eca4963 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -568,7 +568,7 @@ def reflink(path, dest, replace=False, fallback=False): try: pyreflink.reflink(path, dest) - except (NotImplementedError, pyreflink.ReflinkImpossibleError) as exc: + except (NotImplementedError, pyreflink.ReflinkImpossibleError): if fallback: copy(path, dest, replace) else: From 14cfad7bd2ffacf1bdd532869e65123e1f3d3531 Mon Sep 17 00:00:00 2001 From: Ruben De Smet Date: Thu, 23 Jul 2020 09:55:32 +0200 Subject: [PATCH 10/12] Use Oracle documentation link --- docs/reference/config.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/config.rst b/docs/reference/config.rst index ffc3a5c32..d88f0e14e 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -489,7 +489,7 @@ reflink ~~~~~~~ Either ``yes``, ``no``, or ``auto``, indicating whether to use copy-on-write -file clones (a.k.a. "reflinks") instead of copying or moving files. +`file clones`_ (a.k.a. "reflinks") instead of copying or moving files. The ``auto`` option uses reflinks when possible and falls back to plain copying when necessary. Defaults to ``no``. @@ -501,7 +501,7 @@ documentation. The option is ignored if ``move`` is enabled (i.e., beets can move or copy files but it doesn't make sense to do both). -.. _reflink clone: https://blogs.oracle.com/otn/save-disk-space-on-linux-by-cloning-files-on-btrfs-and-ocfs2 +.. _file clones: https://blogs.oracle.com/otn/save-disk-space-on-linux-by-cloning-files-on-btrfs-and-ocfs2 .. _pyreflink: https://reflink.readthedocs.io/en/latest/ resume From 30a2dd99988e20b411e5079c2cee34c69f6b43e4 Mon Sep 17 00:00:00 2001 From: Ruben De Smet Date: Thu, 26 Nov 2020 13:05:10 +0100 Subject: [PATCH 11/12] assert False on unknown move operation --- beets/library.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/beets/library.py b/beets/library.py index dea2a937e..a060e93d6 100644 --- a/beets/library.py +++ b/beets/library.py @@ -756,11 +756,7 @@ class Item(LibModel): plugins.send("item_reflinked", item=self, source=self.path, destination=dest) else: - plugins.send("before_item_moved", item=self, source=self.path, - destination=dest) - util.move(self.path, dest) - plugins.send("item_moved", item=self, source=self.path, - destination=dest) + assert False, 'unknown MoveOperation' # Either copying or moving succeeded, so update the stored path. self.path = dest @@ -1106,7 +1102,7 @@ class Album(LibModel): elif operation == MoveOperation.REFLINK_AUTO: util.reflink(old_art, new_art, fallback=True) else: - util.move(old_art, new_art) + assert False, 'unknown MoveOperation' self.artpath = new_art def move(self, operation=MoveOperation.MOVE, basedir=None, store=True): From 5efaa09482d99f3ea60b23edd07e53564a3e4835 Mon Sep 17 00:00:00 2001 From: Ruben De Smet Date: Thu, 26 Nov 2020 13:45:33 +0100 Subject: [PATCH 12/12] Note installing pyreflink in docs --- docs/reference/config.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference/config.rst b/docs/reference/config.rst index d88f0e14e..d103b26d6 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -496,7 +496,8 @@ Defaults to ``no``. This kind of clone is only available on certain filesystems: for example, btrfs and APFS. For more details on filesystem support, see the `pyreflink`_ -documentation. +documentation. Note that you need to install ``pyreflink``, either through +``python -m pip install beets[reflink]`` or ``python -m pip install reflink``. The option is ignored if ``move`` is enabled (i.e., beets can move or copy files but it doesn't make sense to do both).