diff --git a/duplicate_files_in_folders/file_manager.py b/duplicate_files_in_folders/file_manager.py index d54e6ec..536025d 100644 --- a/duplicate_files_in_folders/file_manager.py +++ b/duplicate_files_in_folders/file_manager.py @@ -3,7 +3,7 @@ import os import logging from collections import deque -from typing import Dict, List +from typing import Dict, List, Tuple import tqdm logger = logging.getLogger(__name__) @@ -310,6 +310,21 @@ def delete_empty_folders_in_tree(self, base_path: str, show_progress: bool = Fal return deleted_folders + @staticmethod + def any_is_subfolder_of(folders: List[str]) -> Tuple[bool, List[Tuple[str, str]]]: + """ + Check if any folder is a subfolder of another folder. + + :param folders: list of folder paths + :return: Tuple containing a boolean and a list of subfolder relationships + """ + subfolder_pairs = [] + for i in range(len(folders)): + for j in range(len(folders)): + if i != j and folders[i].startswith(folders[j]): + subfolder_pairs.append((folders[i], folders[j])) + return bool(subfolder_pairs), subfolder_pairs + def reset_all(self): """Reset the protected_dirs and allowed_dirs to empty sets.""" self.protected_dirs = set() diff --git a/duplicate_files_in_folders/utils.py b/duplicate_files_in_folders/utils.py index e01f5b6..72c3919 100644 --- a/duplicate_files_in_folders/utils.py +++ b/duplicate_files_in_folders/utils.py @@ -18,20 +18,6 @@ def detect_pytest(): return 'PYTEST_CURRENT_TEST' in os.environ -def any_is_subfolder_of(folders: List[str]) -> bool: - """ - Check if any folder is a subfolder of another folder. - :param folders: list of folder paths - :return: False if no folder is a subfolder of another folder, otherwise exit the script - """ - for i in range(len(folders)): - for j in range(len(folders)): - if i != j and folders[i].startswith(folders[j]): - logger.error(f"{folders[i]} is a subfolder of {folders[j]}") - sys.exit(1) - return False - - def parse_size(size_str: str | int) -> int: """ Parse a size string with units (B, KB, MB) to an integer size in bytes. @@ -116,7 +102,14 @@ def validate_arguments(args, parser, check_folders=True): parser.error(f"{name} folder does not exist.") if not os.listdir(folder): parser.error(f"{name} folder is empty.") - any_is_subfolder_of([args.scan_dir, args.reference_dir, args.move_to]) + + is_subfolder, relationships = FileManager.any_is_subfolder_of([args.scan_dir, args.reference_dir, args.move_to]) + if is_subfolder: + for subfolder, parent in relationships: + if subfolder != parent: + parser.error(f"{subfolder} is a subfolder of {parent}") + else: + parser.error(f"Several arguments are the same folder: {subfolder}") # for testing, barely used if args.extra_logging: diff --git a/tests/test_file_manager.py b/tests/test_file_manager.py index 85f3d55..10af61f 100644 --- a/tests/test_file_manager.py +++ b/tests/test_file_manager.py @@ -1,11 +1,12 @@ from tests.helpers_testing import * from pathlib import Path +from duplicate_files_in_folders.file_manager import FileManager def test_move_file(setup_teardown): scan_dir, reference_dir, move_to_dir, common_args = setup_teardown setup_test_files(range(1, 6), [2]) - fm = file_manager.FileManager(True).reset_all() + fm = FileManager(True).reset_all() fm.add_protected_dir(reference_dir) file_to_move = os.path.join(scan_dir, "1.jpg") dst_file = os.path.join(reference_dir, "1.jpg") @@ -41,7 +42,7 @@ def test_move_file(setup_teardown): def test_copy_file(setup_teardown): scan_dir, reference_dir, move_to_dir, common_args = setup_teardown setup_test_files(range(1, 6), [2, 3]) - fm = file_manager.FileManager(True).reset_all() + fm = FileManager(True).reset_all() fm.add_protected_dir(reference_dir) file_to_copy = os.path.join(scan_dir, "1.jpg") dst_file = os.path.join(reference_dir, "1.jpg") @@ -85,7 +86,7 @@ def test_copy_file(setup_teardown): def test_delete_file(setup_teardown): scan_dir, reference_dir, move_to_dir, common_args = setup_teardown setup_test_files(range(1, 6), [2, 3]) - fm = file_manager.FileManager(True).reset_all() + fm = FileManager(True).reset_all() fm.add_protected_dir(reference_dir) file_to_delete = os.path.join(scan_dir, "1.jpg") @@ -117,7 +118,7 @@ def test_delete_file(setup_teardown): def test_make_dirs(setup_teardown): scan_dir, reference_dir, move_to_dir, common_args = setup_teardown - fm = file_manager.FileManager(True).reset_all() + fm = FileManager(True).reset_all() fm.add_protected_dir(reference_dir) dir_to_make = os.path.join(scan_dir, "new_dir") @@ -150,7 +151,7 @@ def test_make_dirs(setup_teardown): def test_rmdir(setup_teardown): scan_dir, reference_dir, move_to_dir, common_args = setup_teardown - fm = file_manager.FileManager(True).reset_all() + fm = FileManager(True).reset_all() fm.add_protected_dir(reference_dir) dir_to_remove = os.path.join(scan_dir, "new_dir") os.makedirs(dir_to_remove) @@ -188,8 +189,8 @@ def test_rmdir(setup_teardown): # The FileManager class should be a singleton, so we should not be able to create multiple instances of it. def test_singleton(): - fm1 = file_manager.FileManager(True) - fm2 = file_manager.FileManager(True) + fm1 = FileManager(True) + fm2 = FileManager(True) assert fm1 is fm2 assert fm1 == fm2 assert fm1 is not None @@ -197,7 +198,7 @@ def test_singleton(): def test_add_protected_dir(): - fm = file_manager.FileManager(True).reset_all() + fm = FileManager(True).reset_all() fm.add_protected_dir("C:\\") fm.add_protected_dir("D:\\") assert len(fm.protected_dirs) == 2 @@ -216,7 +217,7 @@ def get_folder_files_as_set(folder): def test_list_tree_os_scandir_bfs_simple(setup_teardown): scan_dir, reference_dir, move_to_dir, common_args = setup_teardown setup_test_files(range(1, 6), [2, 3]) - fm = file_manager.FileManager.get_instance() + fm = FileManager.get_instance() scan_files = get_folder_files_as_set(scan_dir) scan_tree = fm.list_tree_os_scandir_bfs(scan_dir) # result is in the form of full path @@ -247,12 +248,56 @@ def test_list_tree_os_scandir_bfs_tree_with_many_subfolders(setup_teardown): copy_files(range(2, 5), os.path.join(scan_dir, "sub2", "sub1")) copy_files(range(1, 5), os.path.join(scan_dir, "sub2", "sub2")) - fm = file_manager.FileManager.get_instance() + fm = FileManager.get_instance() scan_files = get_folder_files_as_set(scan_dir) scan_tree = fm.list_tree_os_scandir_bfs(scan_dir) # result is in the form of full path assert set(scan_tree) == scan_files +def test_file_manager_any_is_subfolder_of(): + # Test case 1: one folder is subfolder of another + is_subfolder, relationships = FileManager.any_is_subfolder_of( + ["C:\\Users\\user\\Desktop\\folder", "C:\\Users\\user\\Desktop\\folder\\subfolder"]) + assert is_subfolder is True + + # Test case 2: no folder is subfolder of another + is_subfolder, relationships = FileManager.any_is_subfolder_of( + ["C:\\Users\\user\\Desktop\\folder1", "C:\\Users\\user\\Desktop\\folder2"]) + assert is_subfolder is False + + # Test case 3: one folder is subfolder of another + is_subfolder, relationships = FileManager.any_is_subfolder_of( + ["/path/to/folder", "/path/to/folder/subfolder"]) + assert is_subfolder is True + + # Test case 4: no folder is subfolder of another + is_subfolder, relationships = FileManager.any_is_subfolder_of( + ["/path/to/folder1", "/path/to/folder2"]) + assert is_subfolder is False + + # Test case 5: 3 folders, one is subfolder of another + is_subfolder, relationships = FileManager.any_is_subfolder_of( + ["/path/to/folder1", "/path/to/folder2", "/path/to/folder2/subfolder"]) + assert is_subfolder is True + + # Test case 6: 3 folders, no folder is subfolder of another + is_subfolder, relationships = FileManager.any_is_subfolder_of( + ["/path/to/folder1", "/path/to/folder2", "/path/to/folder3"]) + assert is_subfolder is False + + # Test case 7: 3 folders, one is subfolder of another + is_subfolder, relationships = FileManager.any_is_subfolder_of( + ["C:\\Users\\user\\Desktop\\folder1", "C:\\Users\\user\\Desktop\\folder2", + "C:\\Users\\user\\Desktop\\folder2\\subfolder"]) + assert is_subfolder is True + + # Test case 8: 3 folders, no folder is subfolder of another + is_subfolder, relationships = FileManager.any_is_subfolder_of( + ["C:\\Users\\user\\Desktop\\folder1", "C:\\Users\\user\\Desktop\\folder2", + "C:\\Users\\user\\Desktop\\folder3"]) + assert is_subfolder is False + + def test_python_source_files(): """ Test all python files in the project under duplicate_files_in_folders folder. Make sure that all python files diff --git a/tests/test_functions.py b/tests/test_functions.py index 54e02f1..3c49d62 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -1,8 +1,7 @@ from duplicate_files_in_folders.duplicates_finder import clean_scan_dir_duplications, find_duplicates_files_v3, \ process_duplicates from duplicate_files_in_folders.file_manager import FileManager -from duplicate_files_in_folders.utils import parse_arguments, any_is_subfolder_of, parse_size, \ - check_and_update_filename +from duplicate_files_in_folders.utils import parse_arguments, parse_size, check_and_update_filename from duplicate_files_in_folders.initializer import setup_file_manager from tests.helpers_testing import * @@ -118,7 +117,8 @@ def test_parse_arguments(): assert excinfo.type == SystemExit with pytest.raises(SystemExit) as excinfo: # invalid value for reference_dir - subfolder of scan_dir - parse_arguments(['--scan', scan_dir, '--reference_dir', os.path.join(scan_dir, 'subfolder'), '--move_to', move_to_folder], False) + parse_arguments(['--scan', scan_dir, '--reference_dir', os.path.join(scan_dir, 'subfolder'), + '--move_to', move_to_folder], False) assert excinfo.type == SystemExit with pytest.raises(SystemExit) as excinfo: # invalid value for reference_dir - subfolder of move_to