370 lines
10 KiB
Python
370 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Cleanup Script for NewsArchiver
|
|
|
|
This script removes files older than a specified date from the project directory.
|
|
It provides dry-run mode to preview what would be deleted before actually deleting.
|
|
|
|
Usage:
|
|
python cleanup_old_files.py --date "2024-03-19" --dry-run
|
|
python cleanup_old_files.py --date "2024-03-19"
|
|
|
|
Options:
|
|
--date, -d Date in YYYY-MM-DD format (required)
|
|
--dry-run, -n Show what would be deleted without actually deleting (default: True)
|
|
--force, -f Actually delete files (disables dry-run mode)
|
|
--verbose, -v Enable verbose output
|
|
--archival Include archival_data folder for cleanup
|
|
"""
|
|
|
|
import argparse
|
|
import logging
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import List, Tuple
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
handlers=[
|
|
logging.StreamHandler(sys.stdout),
|
|
],
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Project root directory
|
|
SCRIPT_DIR = Path(__file__).parent.resolve()
|
|
|
|
# Path to archival_data directory
|
|
ARCHIVAL_DATA_DIR = SCRIPT_DIR / "archival_data"
|
|
|
|
# Path to websites folder (only this folder will be scanned in archival_data)
|
|
WEBSITES_DIR = ARCHIVAL_DATA_DIR / "websites"
|
|
|
|
# Files and directories to always preserve (never delete)
|
|
PRESERVE_LIST = {
|
|
# Python files
|
|
"ap_processor.py",
|
|
"archive_engine.py",
|
|
"content_extractor.py",
|
|
"rebuild_database.py",
|
|
"restore_database.py",
|
|
"rss_feeds.json",
|
|
"rss_processor.py",
|
|
"run_archiver.py",
|
|
"scheduler.py",
|
|
"setup_cron.sh",
|
|
"singlefile_archive.py",
|
|
"stop_services.sh",
|
|
"web_interface.py",
|
|
"cleanup_old_files.py",
|
|
# Directories
|
|
"archival_data",
|
|
"static",
|
|
"templates",
|
|
"__pycache__",
|
|
}
|
|
|
|
# Database files to preserve
|
|
DATABASE_FILES = {"cache.db", "cache.db-shm", "cache.db-wal"}
|
|
|
|
# Files that should be excluded from cleanup regardless of date
|
|
EXCLUDE_PATTERNS = [
|
|
".git",
|
|
".gitignore",
|
|
]
|
|
|
|
|
|
def parse_date(date_str: str) -> datetime:
|
|
"""Parse date string in YYYY-MM-DD format.
|
|
|
|
Args:
|
|
date_str: Date string in YYYY-MM-DD format
|
|
|
|
Returns:
|
|
datetime object with the specified date at midnight
|
|
|
|
Raises:
|
|
ValueError: If date format is invalid
|
|
"""
|
|
try:
|
|
return datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)
|
|
except ValueError as e:
|
|
raise ValueError(
|
|
f"Invalid date format: '{date_str}'. Use YYYY-MM-DD format."
|
|
) from e
|
|
|
|
|
|
def should_preserve(path: Path) -> bool:
|
|
"""Check if a file/directory should be preserved.
|
|
|
|
Args:
|
|
path: Path to check
|
|
|
|
Returns:
|
|
True if the path should be preserved, False otherwise
|
|
"""
|
|
# Check if it's in the preserve list
|
|
if path.name in PRESERVE_LIST:
|
|
return True
|
|
|
|
# Check if it matches any exclude patterns
|
|
for pattern in EXCLUDE_PATTERNS:
|
|
if pattern in str(path):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def should_preserve_archival_file(path: Path) -> bool:
|
|
"""Check if a file in archival_data should be preserved.
|
|
|
|
Args:
|
|
path: Path to check
|
|
|
|
Returns:
|
|
True if the path should be preserved, False otherwise
|
|
"""
|
|
# Always preserve database files
|
|
if path.name in DATABASE_FILES:
|
|
return True
|
|
|
|
return should_preserve(path)
|
|
|
|
|
|
def get_files_older_than_date(
|
|
directory: Path, cutoff_date: datetime
|
|
) -> List[Tuple[Path, datetime]]:
|
|
"""Get all files older than the cutoff date.
|
|
|
|
Args:
|
|
directory: Directory to search
|
|
cutoff_date: Files older than this date will be selected
|
|
|
|
Returns:
|
|
List of tuples (path, modification_time) for files older than cutoff
|
|
"""
|
|
old_files = []
|
|
|
|
# Walk through all files in directory recursively
|
|
for item in directory.rglob("*"):
|
|
if item.is_file():
|
|
if should_preserve(item):
|
|
continue
|
|
|
|
try:
|
|
mtime = datetime.fromtimestamp(item.stat().st_mtime, tz=timezone.utc)
|
|
if mtime < cutoff_date:
|
|
old_files.append((item, mtime))
|
|
except (OSError, ValueError) as e:
|
|
logger.warning(f"Could not access file {item}: {e}")
|
|
|
|
return old_files
|
|
|
|
|
|
def get_files_older_than_date_non_recursive(
|
|
directory: Path, cutoff_date: datetime
|
|
) -> List[Tuple[Path, datetime]]:
|
|
"""Get all files older than the cutoff date (non-recursive).
|
|
|
|
Args:
|
|
directory: Directory to search
|
|
cutoff_date: Files older than this date will be selected
|
|
|
|
Returns:
|
|
List of tuples (path, modification_time) for files older than cutoff
|
|
"""
|
|
old_files = []
|
|
|
|
# Walk through all files in directory (non-recursive for safety)
|
|
for item in directory.iterdir():
|
|
if item.is_file():
|
|
if should_preserve(item):
|
|
continue
|
|
|
|
try:
|
|
mtime = datetime.fromtimestamp(item.stat().st_mtime, tz=timezone.utc)
|
|
if mtime < cutoff_date:
|
|
old_files.append((item, mtime))
|
|
except (OSError, ValueError) as e:
|
|
logger.warning(f"Could not access file {item}: {e}")
|
|
|
|
return old_files
|
|
|
|
|
|
def delete_files(files: List[Tuple[Path, datetime]]) -> Tuple[int, int]:
|
|
"""Delete files and return count of successful/failed deletions.
|
|
|
|
Args:
|
|
files: List of (path, modification_time) tuples to delete
|
|
|
|
Returns:
|
|
Tuple of (deleted_count, failed_count)
|
|
"""
|
|
deleted = 0
|
|
failed = 0
|
|
|
|
for path, mtime in files:
|
|
try:
|
|
path.unlink()
|
|
logger.info(
|
|
f"Deleted: {path.name} (modified: {mtime.strftime('%Y-%m-%d %H:%M:%S')})"
|
|
)
|
|
deleted += 1
|
|
except OSError as e:
|
|
logger.error(f"Failed to delete {path.name}: {e}")
|
|
failed += 1
|
|
|
|
return deleted, failed
|
|
|
|
|
|
def main():
|
|
"""Main entry point for cleanup script."""
|
|
parser = argparse.ArgumentParser(
|
|
description="Cleanup old files from NewsArchiver project",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
%(prog)s --date "2024-03-19" --dry-run
|
|
%(prog)s --date "2024-03-19" --force
|
|
%(prog)s --date "2024-03-19" --archival --force
|
|
""",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--date",
|
|
"-d",
|
|
type=str,
|
|
required=True,
|
|
help="Date in YYYY-MM-DD format - files older than this will be deleted",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
"-n",
|
|
action="store_true",
|
|
default=True,
|
|
help="Show what would be deleted without actually deleting (default)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--force",
|
|
"-f",
|
|
action="store_true",
|
|
help="Actually delete files (disables dry-run mode)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--verbose", "-v", action="store_true", help="Enable verbose output"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--archival",
|
|
action="store_true",
|
|
help="Include archival_data folder for cleanup",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Set logging level
|
|
if args.verbose:
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
# Parse the date
|
|
try:
|
|
cutoff_date = parse_date(args.date)
|
|
except ValueError as e:
|
|
logger.error(str(e))
|
|
sys.exit(1)
|
|
|
|
# Validate cutoff date is not in the future
|
|
now = datetime.now(timezone.utc)
|
|
if cutoff_date > now:
|
|
logger.error("Cutoff date cannot be in the future")
|
|
sys.exit(1)
|
|
|
|
logger.info("=" * 60)
|
|
logger.info("NewsArchiver Cleanup Script")
|
|
logger.info("=" * 60)
|
|
|
|
# Determine mode
|
|
dry_run = not args.force
|
|
mode = "DRY RUN" if dry_run else "ACTUAL DELETE"
|
|
logger.info("Mode: %s", mode)
|
|
logger.info(
|
|
"Cutoff Date: %s (files older than this will be %s)",
|
|
cutoff_date.strftime("%Y-%m-%d"),
|
|
"kept" if dry_run else "deleted",
|
|
)
|
|
logger.info("=" * 60)
|
|
|
|
# Find files older than cutoff date
|
|
if args.archival:
|
|
# Only scan websites folder when --archival is used
|
|
if not WEBSITES_DIR.exists():
|
|
logger.error("Websites folder not found at %s", WEBSITES_DIR)
|
|
sys.exit(1)
|
|
old_files = []
|
|
logger.info(
|
|
"Scanning websites folder for files older than %s...",
|
|
cutoff_date.strftime("%Y-%m-%d"),
|
|
)
|
|
# First check if there are any files directly in websites folder
|
|
website_root_files = get_files_older_than_date_non_recursive(
|
|
WEBSITES_DIR, cutoff_date
|
|
)
|
|
# Then check recursively in subdirectories
|
|
website_recursive_files = []
|
|
for subdir in WEBSITES_DIR.iterdir():
|
|
if subdir.is_dir():
|
|
website_recursive_files.extend(
|
|
get_files_older_than_date(subdir, cutoff_date)
|
|
)
|
|
old_files.extend(website_root_files)
|
|
old_files.extend(website_recursive_files)
|
|
logger.info(
|
|
"Found %d files in websites folder",
|
|
len(website_root_files) + len(website_recursive_files),
|
|
)
|
|
else:
|
|
# Scan root directory (non-archival mode)
|
|
old_files = get_files_older_than_date_non_recursive(SCRIPT_DIR, cutoff_date)
|
|
|
|
if not old_files:
|
|
logger.info("No files older than %s found.", cutoff_date.strftime("%Y-%m-%d"))
|
|
logger.info("Nothing to do.")
|
|
return
|
|
|
|
logger.info(
|
|
"Found %d files older than %s:",
|
|
len(old_files),
|
|
cutoff_date.strftime("%Y-%m-%d"),
|
|
)
|
|
|
|
# List all files that would be affected
|
|
for path, mtime in old_files:
|
|
# Get relative path for cleaner output
|
|
rel_path = path.relative_to(SCRIPT_DIR)
|
|
logger.info(
|
|
" - %s (modified: %s)", rel_path, mtime.strftime("%Y-%m-%d %H:%M:%S")
|
|
)
|
|
|
|
logger.info("=" * 60)
|
|
|
|
# Execute deletion if not dry run
|
|
if dry_run:
|
|
logger.info("DRY RUN: No files were deleted.")
|
|
logger.info("Run with --force to actually delete these files.")
|
|
else:
|
|
deleted, failed = delete_files(old_files)
|
|
logger.info("=" * 60)
|
|
logger.info("Cleanup complete!")
|
|
logger.info("Deleted: %d files", deleted)
|
|
logger.info("Failed: %d files", failed)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|