Skip to content

Commit

Permalink
issue56:
Browse files Browse the repository at this point in the history
purge readonly files as well
  • Loading branch information
dennisvang committed Nov 13, 2023
1 parent 3f8d877 commit bdff33b
Showing 4 changed files with 55 additions and 5 deletions.
2 changes: 1 addition & 1 deletion src/tufup/client.py
Original file line number Diff line number Diff line change
@@ -420,7 +420,7 @@ def purge(self) -> None:
for path in self.dir_to_purge.iterdir():
relative_path = path.relative_to(self.dir_to_purge)
if str(relative_path) in manifest:
remove_path(path=path)
remove_path(path=path, override_readonly=True)
else:
logger.warning(f'{path} remains: not in {self._filename}')
logger.info(f'directory purged: {self.dir_to_purge}')
25 changes: 22 additions & 3 deletions src/tufup/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import logging
import os
import pathlib
import shutil
import stat
import sys
from typing import List, Optional, Union

@@ -9,7 +11,7 @@
_INPUT_SEPARATOR = ' '


def remove_path(path: Union[pathlib.Path, str]) -> bool:
def remove_path(path: Union[pathlib.Path, str], override_readonly=False) -> bool:
"""
Recursively remove directory or file at specified path.
@@ -18,14 +20,31 @@ def remove_path(path: Union[pathlib.Path, str]) -> bool:
for path in my_dir_path.iterdir():
remove_path(path)
"""
def remove_readonly(func, path_, _):
"""
clear the readonly bit and reattempt the removal
copied from https://docs.python.org/3/library/shutil.html#rmtree-example
"""
os.chmod(path_, stat.S_IWRITE)
func(path_)

# enforce pathlib.Path
path = pathlib.Path(path)
try:
on_error = remove_readonly if override_readonly else None
if path.is_dir():
shutil.rmtree(path=path)
shutil.rmtree(path=path, onerror=on_error)
utils_logger.debug(f'Removed directory {path}')
elif path.is_file():
path.unlink()
try:
path.unlink()
except Exception as e:
if override_readonly:
path.chmod(stat.S_IWRITE)
path.unlink()
else:
raise e
utils_logger.debug(f'Removed file {path}')
except Exception as e:
utils_logger.error(f'Failed to remove {path}: {e}')
8 changes: 7 additions & 1 deletion tests/test_client.py
Original file line number Diff line number Diff line change
@@ -371,11 +371,17 @@ def test_purge(self):
dir_to_purge = self.temp_dir_path
subdir = dir_to_purge / 'subdir'
subdir.mkdir()
items_to_purge = [dir_to_purge / 'some.dummy', subdir / 'other.dummy', subdir]
readonly_file = dir_to_purge / 'readonly.dummy'
items_to_purge = [
readonly_file, dir_to_purge / 'some.dummy', subdir / 'other.dummy', subdir
]
items_to_keep = [dir_to_purge / 'file.to.keep']
for item in items_to_purge + items_to_keep:
if not item.exists():
item.touch()
# make sure readonly file is readonly
readonly_file.chmod(0o444) # could also use touch(mode=0o444)
self.assertFalse(os.access(readonly_file, os.W_OK))
# write manifest file manually (when writing the manifest from a .tar.gz
# archive, each dir and each item in that dir is listed, recursively,
# so we also need to include both subdir and the items inside subdir here)
25 changes: 25 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import os
import pathlib
import unittest
from unittest.mock import Mock, patch

import tufup.utils
from tufup.utils.platform_specific import ON_WINDOWS
from tests import TempDirTestCase

READ_ONLY = 0o444


class RemovePathTests(TempDirTestCase):
def test_remove_path(self):
@@ -23,6 +27,27 @@ def test_remove_path(self):
self.assertTrue(tufup.utils.remove_path(path=arg_type(dir_path)))
self.assertFalse(dir_path.exists())

def test_remove_path_readonly_file(self):
# prepare
dir_path = self.temp_dir_path / 'dir'
file_path = dir_path / 'dummy.file'
dir_path.mkdir()
file_path.touch(mode=READ_ONLY)
# test
self.assertFalse(tufup.utils.remove_path(dir_path))
self.assertTrue(tufup.utils.remove_path(dir_path, override_readonly=True))

def test_remove_path_readonly_dir(self):
# prepare
dir_path = self.temp_dir_path / 'dir'
file_path = dir_path / 'dummy.file'
dir_path.mkdir(mode=READ_ONLY)
file_path.touch()
# test (windows doesn't really do readonly dirs)
self.assertEqual(ON_WINDOWS, os.access(dir_path, os.W_OK))
self.assertEqual(ON_WINDOWS, tufup.utils.remove_path(dir_path))
self.assertTrue(tufup.utils.remove_path(dir_path, override_readonly=True))


class InputTests(unittest.TestCase):
def test_input_bool(self):

0 comments on commit bdff33b

Please sign in to comment.