Skip to content

Instantly share code, notes, and snippets.

@magenbrot
Created August 7, 2025 09:12
Show Gist options
  • Select an option

  • Save magenbrot/08ca4b49865374ce9a8deabc0057bf0d to your computer and use it in GitHub Desktop.

Select an option

Save magenbrot/08ca4b49865374ce9a8deabc0057bf0d to your computer and use it in GitHub Desktop.
cleanup_folders.py
#!/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