From 1a5ddf89b5fe7191edb085bf38f9e0f3f434b1a8 Mon Sep 17 00:00:00 2001 From: "Kirill A. Korinsky" Date: Fri, 17 Sep 2021 15:50:22 +0200 Subject: [PATCH] Introduce atomic move and write of file The idea of this changes is simple: let move file to some temporary name inside distance folder, and after the file is already copy it renames to expected name. When someone tries to save anything it also moves file to trigger OS level notification for change FS. This commit also enforce that `beets.util.move` shouldn't be used to move directories as it described in comment. Thus, this is fixed #3849 --- beets/util/__init__.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index affdff12f0..05a2a800b6 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -21,6 +21,7 @@ import errno import locale import re +import tempfile import shutil import fnmatch import functools @@ -482,6 +483,10 @@ def move(path, dest, replace=False): instead, in which case metadata will *not* be preserved. Paths are translated to system paths. """ + if os.path.isdir(path): + raise FilesystemError(u'source is directory', 'move', (path, dest)) + if os.path.isdir(dest): + raise FilesystemError(u'distance is directory', 'move', (path, dest)) if samefile(path, dest): return path = syspath(path) @@ -491,15 +496,23 @@ def move(path, dest, replace=False): # First, try renaming the file. try: - os.rename(path, dest) + os.replace(path, dest) except OSError: - # Otherwise, copy and delete the original. + tmp = tempfile.mktemp(suffix='.beets', + prefix=py3_path(b'.' + os.path.basename(dest)), + dir=py3_path(os.path.dirname(dest))) + tmp = syspath(tmp) try: - shutil.copyfile(path, dest) + shutil.copyfile(path, tmp) + os.replace(tmp, dest) + tmp = None os.remove(path) except (OSError, IOError) as exc: raise FilesystemError(exc, 'move', (path, dest), traceback.format_exc()) + finally: + if tmp is not None: + os.remove(tmp) def link(path, dest, replace=False):