Skip to content

Commit

Permalink
Add support for depth and case sensitivity in directory exclusions, b…
Browse files Browse the repository at this point in the history
…y 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 <[email protected]>
  • Loading branch information
ghecko and jordan authored Oct 15, 2024
1 parent 5034419 commit f254563
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 14 deletions.
46 changes: 36 additions & 10 deletions smbclientng/core/SMBSession.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 []
Expand All @@ -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 = []
Expand All @@ -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)

Expand Down
53 changes: 49 additions & 4 deletions smbclientng/modules/Find.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit f254563

Please sign in to comment.