From f254563ca36ec806c1febd711cf48e0ae468dcec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Ovr=C3=A8?= Date: Tue, 15 Oct 2024 17:54:56 +0200 Subject: [PATCH] Add support for depth and case sensitivity in directory exclusions, by directory (#110) * Add depth by directory + ability to specify case sensitivity * Fix documentation indentation * Change depth behavior (until instead of at) * Update Find.py Modify description to better reflect behavior --------- Co-authored-by: jordan --- smbclientng/core/SMBSession.py | 46 ++++++++++++++++++++++------- smbclientng/modules/Find.py | 53 +++++++++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 14 deletions(-) diff --git a/smbclientng/core/SMBSession.py b/smbclientng/core/SMBSession.py index 784d3ca..fe158a9 100644 --- a/smbclientng/core/SMBSession.py +++ b/smbclientng/core/SMBSession.py @@ -238,7 +238,7 @@ def ping_smb_session(self): # Operations - def find(self, paths=[], callback=None, excluded_dirs=[], exclude_dir_depth=0): + def find(self, paths=[], callback=None, exclusion_rules=[]): """ Finds files and directories on the SMB share based on the provided paths and executes a callback function on each entry. @@ -250,12 +250,37 @@ def find(self, paths=[], callback=None, excluded_dirs=[], exclude_dir_depth=0): paths (list, optional): A list of paths to start the search from. Defaults to an empty list. callback (function, optional): A function to be called on each entry found. The function should accept three arguments: the entry object, the full path of the entry, and the current depth of recursion. Defaults to None. - excluded_dirs (list, optional): A list of directories to exclude from the search. Defaults to None. + exclusion_rules (list, optional): A list of exclusion rules, each being a dictionary with keys: + 'dirname', 'depth', 'case_sensitive'. Note: If the callback function is None, the method will print an error message and return without performing any action. """ + def should_exclude(dir_name, depth): + """ + Determines whether a directory should be excluded based on the exclusion rules. + + Args: + dir_name (str): The name of the directory. + depth (int): The current depth in the traversal. + + Returns: + bool: True if the directory should be excluded, False otherwise. + """ + for rule in exclusion_rules: + # Check if the depth is within the exclusion range + if rule['depth'] != -1 and depth > rule['depth']: + continue # Current depth is beyond the specified depth, do not exclude + # Perform matching based on case sensitivity + if rule['case_sensitive']: + if dir_name == rule['dirname']: + return True + else: + if dir_name.lower() == rule['dirname'].lower(): + return True + return False + def recurse_action(paths=[], depth=0, callback=None): if callback is None: return [] @@ -264,10 +289,11 @@ def recurse_action(paths=[], depth=0, callback=None): for path in paths: normalized_path = ntpath.normpath(path) - # Exclude paths that are in the exclude list based on depth - if exclude_dir_depth == -1 or depth == exclude_dir_depth: - if any(excluded.lower() == ntpath.basename(normalized_path).lower() for excluded in excluded_dirs): - continue # Skip this path + dir_name = ntpath.basename(normalized_path) + + # Check if current path should be excluded + if should_exclude(dir_name, depth): + continue # Skip this path remote_smb_path = ntpath.normpath(self.smb_cwd + ntpath.sep + path) entries = [] @@ -285,13 +311,13 @@ def recurse_action(paths=[], depth=0, callback=None): entries = sorted(entries, key=lambda x: x.get_longname().lower()) for entry in entries: + entry_name = entry.get_longname() fullpath = ntpath.join(path, entry.get_longname()) if entry.is_directory(): - # Exclude directories during traversal based on exclude_dir_depth - if exclude_dir_depth == -1 or depth + 1 == exclude_dir_depth: - if any(excluded.lower() == entry.get_longname().lower() for excluded in excluded_dirs): - continue # Skip this directory + # Check if this directory should be excluded + if should_exclude(entry_name, depth + 1): + continue # Skip this directory next_directories_to_explore.append(fullpath) diff --git a/smbclientng/modules/Find.py b/smbclientng/modules/Find.py index 4be2770..b07417f 100644 --- a/smbclientng/modules/Find.py +++ b/smbclientng/modules/Find.py @@ -47,8 +47,11 @@ def parseArgs(self, arguments): parser.add_argument("-iname", type=str, help="Like -name, but the match is case insensitive.") parser.add_argument("-type", type=str, default=None, help="File type (e.g., f for regular file, d for directory).") parser.add_argument("-size", type=str, help="File uses n units of space.") - parser.add_argument("-exclude-dir", type=str, action='append', default=[], help="Subdirectories to exclude from the search.") - parser.add_argument("-exclude-dir-depth", type=int, default=0, help="Specify the depth at which to exclude directories. Use -1 to exclude directories at all levels. Default is 0 (initial level only).") + parser.add_argument('--exclude-dir', action='append', default=[], metavar='DIRNAME[:DEPTH[:CASE]]', + help=("Exclude directories matching DIRNAME until specified depth and case sensitivity. " + "DEPTH specifies the recursion depth (-1 for all depths, default is 0). " + "CASE can be 'i' for case-insensitive or 's' for case-sensitive (default). " + "Format: DIRNAME[:DEPTH[:CASE]]")) # parser.add_argument("-mtime", type=str, help="File's data was last modified n*24 hours ago") # parser.add_argument("-ctime", type=str, help="File's status was last changed n*24 hours ago") # parser.add_argument("-atime", type=str, help="File was last accessed n*24 hours ago") @@ -74,6 +77,47 @@ def parseArgs(self, arguments): self.options = None return self.options + + def parse_exclude_dirs(self, exclude_dirs): + """ + Parses the exclude directory arguments and returns a list of exclusion rules. + + Each exclusion rule is a dictionary with keys: + - 'dirname': The directory name to exclude. + - 'depth': The depth until which to exclude the directory (-1 for all depths). + - 'case_sensitive': Boolean indicating if the match is case-sensitive. + """ + exclusion_rules = [] + for item in exclude_dirs: + parts = item.split(':') + dirname = parts[0] + depth = 0 # Default depth + case_sensitive = False # Default to case-insensitive + + # Parse depth if provided + if len(parts) > 1 and parts[1]: + try: + depth = int(parts[1]) + except ValueError: + depth = 0 # Default if depth is invalid + + # Parse case sensitivity if provided + if len(parts) > 2 and parts[2]: + case_flag = parts[2].lower() + if case_flag == 's': + case_sensitive = True + elif case_flag == 'i': + case_sensitive = False + else: + # Invalid case flag, default to case-sensitive + case_sensitive = True + + exclusion_rules.append({ + 'dirname': dirname, + 'depth': depth, + 'case_sensitive': case_sensitive + }) + return exclusion_rules def __find_callback(self, entry, fullpath, depth): # Documentation for __find_callback function @@ -243,11 +287,12 @@ def run(self, arguments): next_directories_to_explore.append(ntpath.normpath(path) + ntpath.sep) next_directories_to_explore = sorted(list(set(next_directories_to_explore))) + exclusion_rules = self.parse_exclude_dirs(self.options.exclude_dir) + self.smbSession.find( paths=next_directories_to_explore, callback=self.__find_callback, - excluded_dirs=self.options.exclude_dir, - exclude_dir_depth=self.options.exclude_dir_depth + exclusion_rules=exclusion_rules ) except (BrokenPipeError, KeyboardInterrupt) as e: