Created
August 7, 2025 09:12
-
-
Save magenbrot/08ca4b49865374ce9a8deabc0057bf0d to your computer and use it in GitHub Desktop.
cleanup_folders.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """ | |
| This script cleans up a specified directory by deleting: | |
| 1. Files older than one year. | |
| 2. Empty subdirectories. | |
| Home directories of system users are explicitly excluded from deletion | |
| if they are empty, but their empty subdirectories will be removed. | |
| A dry-run mode is available via the --check flag to display potential | |
| deletions without actually executing them. | |
| """ | |
| import os | |
| import sys | |
| import time | |
| import argparse | |
| import pwd | |
| from pathlib import Path | |
| from datetime import datetime | |
| # One year in seconds for time comparison | |
| ONE_YEAR_IN_SECONDS = 365 * 24 * 60 * 60 | |
| def get_user_home_dirs(): | |
| """ | |
| Retrieves the home directories of all users on the system. | |
| This is done by reading the password database. This list is used | |
| to prevent the deletion of empty home directories. | |
| Returns: | |
| set: A set containing the absolute paths of the home directories. | |
| """ | |
| home_dirs = set() | |
| try: | |
| for user in pwd.getpwall(): | |
| # Only add valid and existing home directories | |
| if user.pw_dir and os.path.isdir(user.pw_dir): | |
| home_dirs.add(os.path.realpath(user.pw_dir)) | |
| except (ImportError, KeyError): | |
| print("Warning: The 'pwd' module is not available. " | |
| "Cannot protect user home directories.", file=sys.stderr) | |
| return home_dirs | |
| def delete_old_files(directory: Path, check_mode: bool): | |
| """ | |
| Deletes all files in the specified directory that are older than one year. | |
| Args: | |
| directory (Path): The target directory to be searched. | |
| check_mode (bool): If True, actions are only simulated (dry run). | |
| """ | |
| print("\n--- Searching for files older than one year ---") | |
| now = time.time() | |
| prefix = "[DRY RUN] Would delete: " if check_mode else "[DELETING] " | |
| for dirpath, _, filenames in os.walk(directory): | |
| for filename in filenames: | |
| file_path = Path(dirpath) / filename | |
| try: | |
| # Skip symbolic links to avoid issues | |
| if file_path.is_symlink(): | |
| continue | |
| file_stat = file_path.stat() | |
| file_mod_time = file_stat.st_mtime | |
| if now - file_mod_time > ONE_YEAR_IN_SECONDS: | |
| # Format timestamp for human-readable output | |
| timestamp_str = datetime.fromtimestamp(file_mod_time).strftime('%Y-%m-%d %H:%M:%S') | |
| print(f"{prefix}{file_path} (Last modified: {timestamp_str})") | |
| if not check_mode: | |
| os.remove(file_path) | |
| except FileNotFoundError: | |
| # File might have been deleted already by another process | |
| continue | |
| except OSError as e: | |
| print(f"Error deleting {file_path}: {e}", file=sys.stderr) | |
| def delete_empty_folders(directory: Path, check_mode: bool): | |
| """ | |
| Deletes all empty folders, excluding user home directories. | |
| Args: | |
| directory (Path): The target directory to be cleaned. | |
| check_mode (bool): If True, actions are only simulated (dry run). | |
| """ | |
| print("\n--- Searching for empty folders ---") | |
| home_dirs = get_user_home_dirs() | |
| prefix = "[DRY RUN] Would delete empty folder: " if check_mode else "[DELETING] Empty folder: " | |
| # Use os.walk with topdown=False to process subdirectories before their parents | |
| for dirpath, _, _ in os.walk(directory, topdown=False): | |
| dir_path_obj = Path(dirpath) | |
| real_dir_path = os.path.realpath(dir_path_obj) | |
| try: | |
| # Check if the directory is empty | |
| if not os.listdir(dir_path_obj): | |
| # Check if it is a protected home directory | |
| if real_dir_path in home_dirs: | |
| print(f"[INFO] Skipping empty home directory: {dir_path_obj}") | |
| continue | |
| # Format timestamp for human-readable output | |
| dir_mod_time = dir_path_obj.stat().st_mtime | |
| timestamp_str = datetime.fromtimestamp(dir_mod_time).strftime('%Y-%m-%d %H:%M:%S') | |
| print(f"{prefix}{dir_path_obj} (Last modified: {timestamp_str})") | |
| if not check_mode: | |
| os.rmdir(dir_path_obj) | |
| except FileNotFoundError: | |
| # Directory might have been deleted already by another process | |
| continue | |
| except OSError as e: | |
| print(f"Error deleting {dir_path_obj}: {e}", file=sys.stderr) | |
| def main(): | |
| """ | |
| Main function to parse arguments and start the cleanup process. | |
| """ | |
| parser = argparse.ArgumentParser( | |
| description="Cleans a directory of old files and empty folders.", | |
| epilog="Example: python3 cleanup_folders.py /srv --check" | |
| ) | |
| parser.add_argument( | |
| "directory", | |
| type=Path, | |
| help="The target directory to clean (e.g., /srv)." | |
| ) | |
| parser.add_argument( | |
| "--check", | |
| action="store_true", | |
| help="Perform a dry run without deleting any files or folders." | |
| ) | |
| args = parser.parse_args() | |
| target_dir = args.directory | |
| is_check_mode = args.check | |
| if not target_dir.is_dir(): | |
| print(f"Error: The specified directory '{target_dir}' does not exist.", | |
| file=sys.stderr) | |
| sys.exit(1) | |
| if is_check_mode: | |
| print("*** DRY RUN MODE ACTIVE: No changes will be made. ***") | |
| else: | |
| print("--- WARNING: The script will now perform actual deletions. ---") | |
| time.sleep(3) # A short pause for safety | |
| delete_old_files(target_dir, is_check_mode) | |
| delete_empty_folders(target_dir, is_check_mode) | |
| print("\nCleanup complete.") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment