#!/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()