#!/usr/bin/env python """ Drop this script into a folder you want to clean, and run it. Eventually I'll configure this as runnable from a scripts directory without having to drop it at a root directory; that relies on a config. If no config found, it will run in current directory. """ __license__ = "MPL 2.0" __version__ = "0.1" __credits__ = ["Brian Ernst (leetNightshade)"] __maintainer__ = "leetNightshade" __status__ = "Prototype" import argparse import os import pathlib import shutil import stat import sys import time ignore_folders = { '.git' } # NOTE: These are recursively deleted. dirs_to_delete = { ".vs", "DerivedDataCache", "Intermediate", "Saved", } # NOTE: These are recursively deleted. # Be careful not to mark for deletion files that are included with your version # control. files_to_delete = { } def get_file_directory(): return pathlib.Path(__file__).parent def del_rw(action, name, exc): os.chmod(name, stat.S_IWRITE) os.remove(name) def clean_unreal_temps(dry_run = False, verbose = False, quiet = False): target_path = get_file_directory() count_dirs_deleted = 0 count_files_deleted = 0 if not quiet: print(f"Iterating {target_path} for directories and files to cleanup{', doing a dry-run ' if dry_run else ''}...") # TODO: If not doing a dry run could give people a 5+ second countdown or # something to cancel the script. start = time.time() counted_iterated = 0 if len(dirs_to_delete) or len(files_to_delete): for root, dirs, files, in os.walk(target_path): counted_iterated += 1 # Don't waste time processing an empty directory. if len(dirs) == 0 and len(files) == 0: continue root_path = pathlib.Path(root) if len(dirs_to_delete) or len(ignore_folders): for dir in reversed(dirs): if dir in ignore_folders: dirs.remove(dir) continue if dir not in dirs_to_delete: # We do want os.walk to continue to look into this directory. continue # We're processing a directory we're going to delete, no need to leave # os.walk to iterate over the files in this directory. Helpful during # a dry-run to run similarly during non-dry-run. But also if we were # multi-threaded, this wouldn't try to iterate files we're already # trying to delete. dirs.remove(dir) dir_path = root_path / dir if not dry_run: shutil.rmtree(dir_path, onerror=del_rw) count_dirs_deleted += 1 if verbose and not quiet: if dry_run: print(f" Pretend Deleted: {dir_path}") else: print(f" Deleted: {dir_path}") if len(files_to_delete): for file in files: if not any(pathlib.PurePath(file).match(pattern) for pattern in files_to_delete): continue file_path = root_path / file if not dry_run: file_path.unlink() count_files_deleted += 1 if verbose and not quiet: if dry_run: print(f" Pretend Deleted: {file_path}") else: print(f" Deleted: {file_path}") # Done, summarize everything. if not quiet: prefix = None if dry_run: prefix = "Dry-run, would have deleted" else: prefix = "Deleted" print(f" {prefix} {count_dirs_deleted} director{'ies' if count_dirs_deleted != 1 else 'y'}, {count_files_deleted} file{'s' if count_files_deleted != 1 else ''}. Iterated {counted_iterated} paths.") # TODO: Add up the file size deleted (including from deleted folders) # in a verbose run, or have that as an extra param. if count_dirs_deleted > 0: print(f" (NOTE: Number of {'potentially ' if dry_run else ''}deleted files does not include files from a deleted directory.)") print(f" Done. Finished in {str(time.time() - start)}s") else: print(" Skipped. Nothing configured to be deleted.") if __name__ == "__main__": parser = argparse.ArgumentParser(description="Script to clean temp Unreal files/folders") parser.add_argument('--dry-run', '-n', action='store_true', help='Dry run without any modifications.') parser.add_argument('--unattended', '-u', action='store_true', help='Run without waiting.') parser.add_argument('--quiet', '-q', action='store_true', help='Quiet output.') parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output.') args = parser.parse_args() clean_unreal_temps(dry_run = args.dry_run, verbose = args.verbose) if not args.unattended: input("Waiting for input...")