Jelajahi Sumber

Merge pull request #6312 from hexagonrecursion/bp-unlink

Backport:  safe_unlink (was: truncate_and_unlink)
TW 3 tahun lalu
induk
melakukan
794907d4d2

+ 2 - 2
src/borg/cache.py

@@ -25,7 +25,7 @@ from .helpers import yes, hostname_is_unique
 from .helpers import remove_surrogates
 from .helpers import remove_surrogates
 from .helpers import ProgressIndicatorPercent, ProgressIndicatorMessage
 from .helpers import ProgressIndicatorPercent, ProgressIndicatorMessage
 from .helpers import set_ec, EXIT_WARNING
 from .helpers import set_ec, EXIT_WARNING
-from .helpers import truncate_and_unlink
+from .helpers import safe_unlink
 from .helpers import msgpack
 from .helpers import msgpack
 from .item import ArchiveItem, ChunkListEntry
 from .item import ArchiveItem, ChunkListEntry
 from .crypto.key import PlaintextKey
 from .crypto.key import PlaintextKey
@@ -745,7 +745,7 @@ class LocalCache(CacheStatsMixin):
                                                   filename=bin_to_hex(archive_id) + '.compact') as fd:
                                                   filename=bin_to_hex(archive_id) + '.compact') as fd:
                     chunk_idx.write(fd)
                     chunk_idx.write(fd)
             except Exception:
             except Exception:
-                truncate_and_unlink(fn_tmp)
+                safe_unlink(fn_tmp)
             else:
             else:
                 os.rename(fn_tmp, fn)
                 os.rename(fn_tmp, fn)
 
 

+ 24 - 10
src/borg/helpers.py

@@ -2399,12 +2399,12 @@ def secure_erase(path):
     os.unlink(path)
     os.unlink(path)
 
 
 
 
-def truncate_and_unlink(path):
+def safe_unlink(path):
     """
     """
-    Truncate and then unlink *path*.
+    Safely unlink (delete) *path*.
 
 
-    Do not create *path* if it does not exist.
-    Open *path* for truncation in r+b mode (=O_RDWR|O_BINARY).
+    If we run out of space while deleting the file, we try truncating it first.
+    BUT we truncate only if path is the only hardlink referring to this content.
 
 
     Use this when deleting potentially large files when recovering
     Use this when deleting potentially large files when recovering
     from a VFS error such as ENOSPC. It can help a full file system
     from a VFS error such as ENOSPC. It can help a full file system
@@ -2412,13 +2412,27 @@ def truncate_and_unlink(path):
     in repository.py for further explanations.
     in repository.py for further explanations.
     """
     """
     try:
     try:
-        with open(path, 'r+b') as fd:
-            fd.truncate()
-    except OSError as err:
-        if err.errno != errno.ENOTSUP:
+        os.unlink(path)
+    except OSError as unlink_err:
+        if unlink_err.errno != errno.ENOSPC:
+            # not free space related, give up here.
             raise
             raise
-        # don't crash if the above ops are not supported.
-    os.unlink(path)
+        # we ran out of space while trying to delete the file.
+        st = os.stat(path)
+        if st.st_nlink > 1:
+            # rather give up here than cause collateral damage to the other hardlink.
+            raise
+        # no other hardlink! try to recover free space by truncating this file.
+        try:
+            # Do not create *path* if it does not exist, open for truncation in r+b mode (=O_RDWR|O_BINARY).
+            with open(path, 'r+b') as fd:
+                fd.truncate()
+        except OSError:
+            # truncate didn't work, so we still have the original unlink issue - give up:
+            raise unlink_err
+        else:
+            # successfully truncated the file, try again deleting it:
+            os.unlink(path)
 
 
 
 
 def popen_with_error_handling(cmd_line: str, log_prefix='', **kwargs):
 def popen_with_error_handling(cmd_line: str, log_prefix='', **kwargs):

+ 3 - 3
src/borg/platform/base.py

@@ -3,7 +3,7 @@ import os
 import socket
 import socket
 import uuid
 import uuid
 
 
-from borg.helpers import truncate_and_unlink
+from borg.helpers import safe_unlink
 
 
 """
 """
 platform base module
 platform base module
@@ -168,7 +168,7 @@ class SaveFile:
     def __enter__(self):
     def __enter__(self):
         from .. import platform
         from .. import platform
         try:
         try:
-            truncate_and_unlink(self.tmppath)
+            safe_unlink(self.tmppath)
         except FileNotFoundError:
         except FileNotFoundError:
             pass
             pass
         self.fd = platform.SyncFile(self.tmppath, self.binary)
         self.fd = platform.SyncFile(self.tmppath, self.binary)
@@ -178,7 +178,7 @@ class SaveFile:
         from .. import platform
         from .. import platform
         self.fd.close()
         self.fd.close()
         if exc_type is not None:
         if exc_type is not None:
-            truncate_and_unlink(self.tmppath)
+            safe_unlink(self.tmppath)
             return
             return
         os.replace(self.tmppath, self.path)
         os.replace(self.tmppath, self.path)
         platform.sync_dir(os.path.dirname(self.path))
         platform.sync_dir(os.path.dirname(self.path))

+ 2 - 2
src/borg/remote.py

@@ -27,7 +27,7 @@ from .helpers import hostname_is_unique
 from .helpers import replace_placeholders
 from .helpers import replace_placeholders
 from .helpers import sysinfo
 from .helpers import sysinfo
 from .helpers import format_file_size
 from .helpers import format_file_size
-from .helpers import truncate_and_unlink
+from .helpers import safe_unlink
 from .helpers import prepare_subprocess_env
 from .helpers import prepare_subprocess_env
 from .logger import create_logger, setup_logging
 from .logger import create_logger, setup_logging
 from .helpers import msgpack
 from .helpers import msgpack
@@ -1144,7 +1144,7 @@ class RepositoryCache(RepositoryNoCache):
                 fd.write(packed)
                 fd.write(packed)
         except OSError as os_error:
         except OSError as os_error:
             try:
             try:
-                truncate_and_unlink(file)
+                safe_unlink(file)
             except FileNotFoundError:
             except FileNotFoundError:
                 pass  # open() could have failed as well
                 pass  # open() could have failed as well
             if os_error.errno == errno.ENOSPC:
             if os_error.errno == errno.ENOSPC:

+ 3 - 3
src/borg/repository.py

@@ -19,7 +19,7 @@ from .helpers import Location
 from .helpers import ProgressIndicatorPercent
 from .helpers import ProgressIndicatorPercent
 from .helpers import bin_to_hex
 from .helpers import bin_to_hex
 from .helpers import hostname_is_unique
 from .helpers import hostname_is_unique
-from .helpers import secure_erase, truncate_and_unlink
+from .helpers import secure_erase, safe_unlink
 from .helpers import msgpack
 from .helpers import msgpack
 from .locking import Lock, LockError, LockErrorT
 from .locking import Lock, LockError, LockErrorT
 from .logger import create_logger
 from .logger import create_logger
@@ -1294,7 +1294,7 @@ class LoggedIO:
             if segment > transaction_id:
             if segment > transaction_id:
                 if segment in self.fds:
                 if segment in self.fds:
                     del self.fds[segment]
                     del self.fds[segment]
-                truncate_and_unlink(filename)
+                safe_unlink(filename)
                 count += 1
                 count += 1
             else:
             else:
                 break
                 break
@@ -1402,7 +1402,7 @@ class LoggedIO:
         if segment in self.fds:
         if segment in self.fds:
             del self.fds[segment]
             del self.fds[segment]
         try:
         try:
-            truncate_and_unlink(self.segment_filename(segment))
+            safe_unlink(self.segment_filename(segment))
         except FileNotFoundError:
         except FileNotFoundError:
             pass
             pass
 
 

+ 31 - 0
src/borg/testsuite/helpers.py

@@ -1,3 +1,4 @@
+import errno
 import hashlib
 import hashlib
 import io
 import io
 import os
 import os
@@ -27,6 +28,7 @@ from ..helpers import chunkit
 from ..helpers import safe_ns, safe_s, SUPPORT_32BIT_PLATFORMS
 from ..helpers import safe_ns, safe_s, SUPPORT_32BIT_PLATFORMS
 from ..helpers import popen_with_error_handling
 from ..helpers import popen_with_error_handling
 from ..helpers import dash_open
 from ..helpers import dash_open
+from ..helpers import safe_unlink
 
 
 from . import BaseTestCase, FakeInputs
 from . import BaseTestCase, FakeInputs
 
 
@@ -999,3 +1001,32 @@ def test_dash_open():
     assert dash_open('-', 'w') is sys.stdout
     assert dash_open('-', 'w') is sys.stdout
     assert dash_open('-', 'rb') is sys.stdin.buffer
     assert dash_open('-', 'rb') is sys.stdin.buffer
     assert dash_open('-', 'wb') is sys.stdout.buffer
     assert dash_open('-', 'wb') is sys.stdout.buffer
+
+
+def test_safe_unlink_is_safe(tmpdir):
+    contents = b"Hello, world\n"
+    victim = tmpdir / 'victim'
+    victim.write_binary(contents)
+    hard_link = tmpdir / 'hardlink'
+    hard_link.mklinkto(victim)
+
+    safe_unlink(str(hard_link))
+
+    assert victim.read_binary() == contents
+
+
+def test_safe_unlink_is_safe_ENOSPC(tmpdir, monkeypatch):
+    contents = b"Hello, world\n"
+    victim = tmpdir / 'victim'
+    victim.write_binary(contents)
+    hard_link = tmpdir / 'hardlink'
+    hard_link.mklinkto(victim)
+
+    def os_unlink(_):
+        raise OSError(errno.ENOSPC, "Pretend that we ran out of space")
+    monkeypatch.setattr(os, "unlink", os_unlink)
+
+    with pytest.raises(OSError):
+        safe_unlink(str(hard_link))
+
+    assert victim.read_binary() == contents