Skip to content

Commit

Permalink
Fix deletion of locked files on Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
az0 committed Feb 11, 2017
1 parent 66271f4 commit 193e07a
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 7 deletions.
24 changes: 19 additions & 5 deletions bleachbit/FileUtilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,13 @@ def sync():
ctypes.cdll.LoadLibrary('msvcrt.dll')._flushall()


def truncate_f(f):
"""Truncate the file object"""
f.truncate(0)
f.flush()
os.fsync(f.fileno())


def whitelisted_posix(path, check_realpath = True):
"""Check whether this POSIX path is whitelisted"""
from bleachbit.Options import options
Expand Down Expand Up @@ -642,24 +649,31 @@ def wipe_write():
file_wipe(path)
except pywinerror as e:
# 32=The process cannot access the file because it is being used by another process.
# 33=The process cannot access the file because another process has locked a portion of the file.
# 33=The process cannot access the file because another process has
# locked a portion of the file.
if not e.winerror in (32, 33):
# handle only locking errors
raise
# Try to truncate the file. This makes the behavior consistent
# with Linux and with Windows when IsUserAdmin=False.
try:
with open(path, 'wb') as f:
truncate_f(f)
except Exception as e2:
logger.exception('exception in truncate')
# translate exception to mark file to deletion in Command.py
raise WindowsError(e.winerror, e.strerror)
except UnsupportedFileSystemError as e:
warnings.warn(
_('At least one file was on a file system that does not support advanced overwriting.'), UserWarning)
f = open(path, 'wb')
else:
# The wipe succeed, so prepare to truncate.
f = open(path, 'wb')
else:
f = wipe_write()
if truncate:
f.truncate(0)
f.flush()
os.fsync(f.fileno())
truncate_f(f)
f.close()


Expand Down Expand Up @@ -816,7 +830,7 @@ def estimate_completion():
stats.f_bsize * stats.f_bfree, stats.f_ffree)
# truncate and close files
for f in files:
f.truncate(0)
truncate_f(f)

while True:
try:
Expand Down
42 changes: 42 additions & 0 deletions tests/TestFileUtilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,48 @@ def win_symlink(src, linkname):
delete(path, shred)
self.assert_(not os.path.exists(path))

@unittest.skipIf('nt' != os.name, 'skipping on non-Windows')
def test_delete_locked(self):
"""Unit test for delete() with locked file"""
# set up
def test_delete_locked_setup():
(fd, filename) = tempfile.mkstemp(prefix='bleachbit-test-worker')
os.write(fd, '123')
os.close(fd)
self.assertExists(filename)
self.assertEqual(3, getsize(filename))
return filename

# File is open but not opened exclusive, so expect that the
# file is truncated but not deleted.
# O_EXCL = fail if file exists (i.e., not an exclusive lock)
filename = test_delete_locked_setup()
f = os.open(filename, os.O_WRONLY | os.O_EXCL)
self.assertExists(filename)
self.assertEqual(3, getsize(filename))
with self.assertRaises(WindowsError):
delete(filename)
os.close(f)
self.assertExists(filename)
self.assertEqual(0, getsize(filename))
delete(filename)
self.assertNotExists(filename)

# File is open with exclusive lock, so expect the file is neither
# deleted nor truncated.
filename = test_delete_locked_setup()
from subprocess import call
call('start notepad.exe>>{}'.format(filename), shell=True)
self.assertExists(filename)
self.assertEqual(3, getsize(filename))
with self.assertRaises(WindowsError):
delete(filename)
call('taskkill /f /im notepad.exe')
self.assertExists(filename)
self.assertEqual(3, getsize(filename))
delete(filename)
self.assertNotExists(filename)

@unittest.skipIf('nt' == os.name, 'skipping on Windows')
def test_ego_owner(self):
"""Unit test for ego_owner()"""
Expand Down
8 changes: 6 additions & 2 deletions tests/TestWorker.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,14 @@ def __init__(self, action_element):
self.pathname = action_element.getAttribute('path')

def get_commands(self):
# Lock the file on Windows. It should be marked for deletion.
f = os.open(self.pathname, os.O_RDWR | os.O_EXCL)
# Open the file with a non-exclusive lock, so the file should
# be truncated and marked for deletion. This is checked just on
# on Windows.
f = os.open(self.pathname, os.O_RDWR)
yield Command.Delete(self.pathname)
assert(os.path.exists(self.pathname))
from bleachbit.FileUtilities import getsize
assert(0==getsize(self.pathname))
os.close(f)

# real file, should succeed
Expand Down

0 comments on commit 193e07a

Please sign in to comment.