1010import argparse
1111import logging
1212
13- DEFAULT_SOURCE_ROOT = '/glade/campaign/cesm/cesmdata/cseg/inputdata/'
14- DEFAULT_TARGET_ROOT = '/glade/campaign/collections/gdex/data/d651077/cesmdata/inputdata/'
13+ DEFAULT_SOURCE_ROOT = "/glade/campaign/cesm/cesmdata/cseg/inputdata/"
14+ DEFAULT_TARGET_ROOT = (
15+ "/glade/campaign/collections/gdex/data/d651077/cesmdata/inputdata/"
16+ )
1517
1618# Set up logger
1719logger = logging .getLogger (__name__ )
1820
21+
1922def find_and_replace_owned_files (source_dir , target_dir , username ):
2023 """
2124 Finds files owned by a specific user in a source directory tree,
@@ -41,7 +44,7 @@ def find_and_replace_owned_files(source_dir, target_dir, username):
4144 "Searching for files owned by '%s' (UID: %s) in '%s'..." ,
4245 username ,
4346 user_uid ,
44- source_dir
47+ source_dir ,
4548 )
4649
4750 for dirpath , _ , filenames in os .walk (source_dir ):
@@ -56,7 +59,7 @@ def find_and_replace_owned_files(source_dir, target_dir, username):
5659
5760 file_uid = os .stat (file_path ).st_uid
5861 except FileNotFoundError :
59- continue # Skip if file was deleted during traversal
62+ continue # Skip if file was deleted during traversal
6063
6164 if file_uid == user_uid :
6265 logger .info ("Found owned file: %s" , file_path )
@@ -71,7 +74,7 @@ def find_and_replace_owned_files(source_dir, target_dir, username):
7174 "Warning: Corresponding file not found in '%s' "
7275 "for '%s'. Skipping." ,
7376 target_dir ,
74- file_path
77+ file_path ,
7578 )
7679 continue
7780
@@ -80,7 +83,7 @@ def find_and_replace_owned_files(source_dir, target_dir, username):
8083
8184 # Remove the original file
8285 try :
83- os .rename (link_name , link_name + ".tmp" )
86+ os .rename (link_name , link_name + ".tmp" )
8487 logger .info ("Deleted original file: %s" , link_name )
8588 except OSError as e :
8689 logger .error ("Error deleting file %s: %s. Skipping." , link_name , e )
@@ -91,11 +94,36 @@ def find_and_replace_owned_files(source_dir, target_dir, username):
9194 # Create parent directories for the link if they don't exist
9295 os .makedirs (os .path .dirname (link_name ), exist_ok = True )
9396 os .symlink (link_target , link_name )
94- os .remove (link_name + ".tmp" )
95- logger .info ("Created symbolic link: %s -> %s" , link_name , link_target )
97+ os .remove (link_name + ".tmp" )
98+ logger .info (
99+ "Created symbolic link: %s -> %s" , link_name , link_target
100+ )
96101 except OSError as e :
97- os .rename (link_name + ".tmp" , link_name )
98- logger .error ("Error creating symlink for %s: %s. Skipping." , link_name , e )
102+ os .rename (link_name + ".tmp" , link_name )
103+ logger .error (
104+ "Error creating symlink for %s: %s. Skipping." , link_name , e
105+ )
106+
107+
108+ def validate_directory (path ):
109+ """
110+ Validate that the path exists and is a directory.
111+
112+ Args:
113+ path (str): The path to validate.
114+
115+ Returns:
116+ str: The absolute path if valid.
117+
118+ Raises:
119+ argparse.ArgumentTypeError: If path doesn't exist or is not a directory.
120+ """
121+ if not os .path .exists (path ):
122+ raise argparse .ArgumentTypeError (f"Directory '{ path } ' does not exist" )
123+ if not os .path .isdir (path ):
124+ raise argparse .ArgumentTypeError (f"'{ path } ' is not a directory" )
125+ return os .path .abspath (path )
126+
99127
100128def parse_arguments ():
101129 """
@@ -107,41 +135,43 @@ def parse_arguments():
107135 """
108136 parser = argparse .ArgumentParser (
109137 description = (
110- ' Find files owned by a user and replace them with symbolic links to a target directory.'
138+ " Find files owned by a user and replace them with symbolic links to a target directory."
111139 )
112140 )
113141 parser .add_argument (
114- '--source-root' ,
142+ "--source-root" ,
143+ type = validate_directory ,
115144 default = DEFAULT_SOURCE_ROOT ,
116145 help = (
117- f' The root of the directory tree to search for files (default: { DEFAULT_SOURCE_ROOT } )'
118- )
146+ f" The root of the directory tree to search for files (default: { DEFAULT_SOURCE_ROOT } )"
147+ ),
119148 )
120149 parser .add_argument (
121- '--target-root' ,
150+ "--target-root" ,
151+ type = validate_directory ,
122152 default = DEFAULT_TARGET_ROOT ,
123153 help = (
124- f' The root of the directory tree where files should be moved to '
125- f' (default: { DEFAULT_TARGET_ROOT } )'
126- )
154+ f" The root of the directory tree where files should be moved to "
155+ f" (default: { DEFAULT_TARGET_ROOT } )"
156+ ),
127157 )
128158
129159 # Verbosity options (mutually exclusive)
130160 verbosity_group = parser .add_mutually_exclusive_group ()
131161 verbosity_group .add_argument (
132- '-v' , '--verbose' ,
133- action = 'store_true' ,
134- help = 'Enable verbose output'
162+ "-v" , "--verbose" , action = "store_true" , help = "Enable verbose output"
135163 )
136164 verbosity_group .add_argument (
137- '-q' , '--quiet' ,
138- action = 'store_true' ,
139- help = 'Quiet mode (show only warnings and errors)'
165+ "-q" ,
166+ "--quiet" ,
167+ action = "store_true" ,
168+ help = "Quiet mode (show only warnings and errors)" ,
140169 )
141170
142171 return parser .parse_args ()
143172
144- if __name__ == '__main__' :
173+
174+ if __name__ == "__main__" :
145175
146176 args = parse_arguments ()
147177
@@ -153,13 +183,9 @@ def parse_arguments():
153183 else :
154184 LOG_LEVEL = logging .INFO
155185
156- logging .basicConfig (
157- level = LOG_LEVEL ,
158- format = '%(message)s' ,
159- stream = sys .stdout
160- )
161-
162- my_username = os .environ ['USER' ]
186+ logging .basicConfig (level = LOG_LEVEL , format = "%(message)s" , stream = sys .stdout )
187+
188+ my_username = os .environ ["USER" ]
163189
164190 # --- Execution ---
165191 find_and_replace_owned_files (args .source_root , args .target_root , my_username )
0 commit comments