PHP Classes

File: scripts/echomail_maintenance.php

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   scripts/echomail_maintenance.php   Download  
File: scripts/echomail_maintenance.php
Role: Example script
Content type: text/plain
Description: Example script
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 17,822 bytes
 

Contents

Class file image Download
#!/usr/bin/env php <?php /** * Echomail Maintenance Utility * * This script performs maintenance on echomail messages: * - Purge old messages over a certain age * - Delete oldest messages if maximum count exceeded * - Clean up related records (read status, saved messages, shared links, threading) * - Runs VACUUM ANALYZE on all affected tables to reclaim storage * - Works on per-echo basis * * Usage: * php echomail_maintenance.php --echo=TAGNAME --domain=DOMAIN --max-age=90 [--dry-run] [--quiet] * php echomail_maintenance.php --echo=all --domain=DOMAIN --max-count=1000 [--dry-run] [--quiet] * * Options: * --echo=TAG Echo area tag (use 'all' for all areas in domain) * --domain=DOMAIN Network domain (e.g., fidonet, fsxnet) - required * --max-age=DAYS Delete messages older than this many days * --max-count=NUM Keep only the newest NUM messages per echo (0 = delete all) * --dry-run Show what would be deleted without actually deleting * --quiet Suppress output except errors * --help Show this help message * * Examples: * # Delete messages older than 90 days in FIDO_SYSOP echo * php echomail_maintenance.php --echo=FIDO_SYSOP --domain=fidonet --max-age=90 * * # Keep only newest 500 messages in all fidonet echoes * php echomail_maintenance.php --echo=all --domain=fidonet --max-count=500 * * # Preview what would be deleted (dry run) * php echomail_maintenance.php --echo=all --domain=fsxnet --max-age=180 --dry-run */ require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../src/functions.php'; use BinktermPHP\Database; // Parse command line arguments $options = parseArguments($argv); // Show help if requested if (isset($options['help'])) { showHelp(); exit(0); } // Validate required arguments if (!isset($options['echo'])) { echo "Error: --echo parameter is required\n"; showHelp(); exit(1); } if (!isset($options['max-age']) && !isset($options['max-count'])) { echo "Error: At least one of --max-age or --max-count must be specified\n"; showHelp(); exit(1); } $echoTag = $options['echo']; // Domain is always required if (!isset($options['domain'])) { echo "Error: --domain is required\n"; showHelp(); exit(1); } $domain = $options['domain']; $maxAge = isset($options['max-age']) ? (int)$options['max-age'] : null; $maxCount = isset($options['max-count']) ? (int)$options['max-count'] : null; $dryRun = isset($options['dry-run']); $quiet = isset($options['quiet']); // Validate numeric parameters if ($maxAge !== null && $maxAge <= 0) { echo "Error: --max-age must be a positive number\n"; exit(1); } if ($maxCount !== null && $maxCount < 0) { echo "Error: --max-count must be zero or a positive number\n"; exit(1); } try { $db = Database::getInstance(); $pdo = $db->getPdo(); if (!$quiet) { echo "========================================\n"; echo "Echomail Maintenance Utility\n"; echo "========================================\n\n"; if ($dryRun) { echo "*** DRY RUN MODE - No changes will be made ***\n\n"; } } // Get list of echo areas to process $echoareas = getEchoareas($pdo, $echoTag, $domain); if (empty($echoareas)) { if ($echoTag === 'all') { echo "No echo areas found in database.\n"; } else { echo "Echo area '$echoTag' not found.\n"; } exit(1); } if (!$quiet) { echo "Processing " . count($echoareas) . " echo area(s)\n\n"; } $startTime = microtime(true); $totalDeleted = 0; // Process each echo area foreach ($echoareas as $echoarea) { $deletedCount = processEchoarea( $pdo, $echoarea, $maxAge, $maxCount, $dryRun, $quiet ); $totalDeleted += $deletedCount; } $endTime = microtime(true); $elapsedTime = $endTime - $startTime; // Run VACUUM on affected tables after deletions $vacuumTime = 0; if (!$dryRun && $totalDeleted > 0) { if (!$quiet) { echo "\n========================================\n"; echo "Database Maintenance\n"; echo "========================================\n"; echo "Running VACUUM on affected tables to reclaim storage...\n"; } $vacuumStart = microtime(true); $tables = ['echomail', 'message_read_status', 'saved_messages', 'shared_messages', 'message_links']; try { foreach ($tables as $table) { if (!$quiet) { echo " Vacuuming $table...\n"; } $pdo->exec("VACUUM ANALYZE $table"); } $vacuumTime = microtime(true) - $vacuumStart; if (!$quiet) { echo "? VACUUM completed in " . formatElapsedTime($vacuumTime) . "\n"; } } catch (Exception $e) { if (!$quiet) { echo "? Warning: VACUUM failed: " . $e->getMessage() . "\n"; } getServerLogger()->warning("Echomail maintenance VACUUM warning: " . $e->getMessage()); } } if (!$quiet) { echo "\n========================================\n"; echo "Summary\n"; echo "========================================\n"; echo "Total messages " . ($dryRun ? "would be deleted" : "deleted") . ": $totalDeleted\n"; echo "Deletion time: " . formatElapsedTime($elapsedTime) . "\n"; if ($vacuumTime > 0) { echo "VACUUM time: " . formatElapsedTime($vacuumTime) . "\n"; echo "Total time: " . formatElapsedTime($elapsedTime + $vacuumTime) . "\n"; } if ($dryRun) { echo "\nRun without --dry-run to actually delete messages.\n"; } else { echo "\n? Maintenance completed successfully\n"; } } } catch (Exception $e) { echo "? Error: " . $e->getMessage() . "\n"; getServerLogger()->error("Echomail maintenance error: " . $e->getMessage()); exit(1); } /** * Parse command line arguments */ function parseArguments($argv) { $options = []; for ($i = 1; $i < count($argv); $i++) { $arg = $argv[$i]; if (strpos($arg, '--') === 0) { $arg = substr($arg, 2); if (strpos($arg, '=') !== false) { list($key, $value) = explode('=', $arg, 2); $options[$key] = $value; } else { // Flag without value $options[$arg] = true; } } } return $options; } /** * Show help message */ function showHelp() { global $argv; $script = basename($argv[0]); echo "\nEchomail Maintenance Utility\n"; echo "============================\n\n"; echo "This script deletes echomail messages and cleans up related records from:\n"; echo " - message_read_status, saved_messages, shared_messages, message_links\n"; echo "Then runs VACUUM ANALYZE on all affected tables.\n\n"; echo "Usage:\n"; echo " php $script --echo=TAGNAME --domain=DOMAIN --max-age=DAYS [options]\n"; echo " php $script --echo=all --domain=DOMAIN --max-count=NUM [options]\n\n"; echo "Required:\n"; echo " --echo=TAG Echo area tag (use 'all' for all areas in domain)\n"; echo " --domain=DOMAIN Network domain (e.g., fidonet, fsxnet)\n\n"; echo "At least one of:\n"; echo " --max-age=DAYS Delete messages older than this many days\n"; echo " --max-count=NUM Keep only the newest NUM messages per echo (0 = delete all)\n\n"; echo "Optional:\n"; echo " --dry-run Show what would be deleted without deleting\n"; echo " --quiet Suppress output except errors\n"; echo " --help Show this help message\n\n"; echo "Examples:\n"; echo " # Delete messages older than 90 days in FIDO_SYSOP\n"; echo " php $script --echo=FIDO_SYSOP --domain=fidonet --max-age=90\n\n"; echo " # Keep only 500 newest messages in all fidonet echoes\n"; echo " php $script --echo=all --domain=fidonet --max-count=500\n\n"; echo " # Preview deletions in fsxnet (dry run)\n"; echo " php $script --echo=all --domain=fsxnet --max-age=180 --dry-run\n\n"; } /** * Get echo areas to process */ function getEchoareas($pdo, $echoTag, $domain) { if ($echoTag === 'all') { $stmt = $pdo->prepare(" SELECT id, tag, domain, description, message_count FROM echoareas WHERE is_active = TRUE AND domain = :domain ORDER BY tag "); $stmt->execute(['domain' => $domain]); return $stmt->fetchAll(PDO::FETCH_ASSOC); } else { $stmt = $pdo->prepare(" SELECT id, tag, domain, description, message_count FROM echoareas WHERE tag = :tag AND domain = :domain AND is_active = TRUE "); $stmt->execute(['tag' => $echoTag, 'domain' => $domain]); return $stmt->fetchAll(PDO::FETCH_ASSOC); } } /** * Process a single echo area */ function processEchoarea($pdo, $echoarea, $maxAge, $maxCount, $dryRun, $quiet) { $echoId = $echoarea['id']; $echoTag = $echoarea['tag']; $echoDomain = $echoarea['domain'] ?? 'fidonet'; $currentCount = $echoarea['message_count']; if (!$quiet) { echo "Processing: $echoTag [$echoDomain]\n"; echo " Current messages: $currentCount\n"; } $deletedCount = 0; $deletedByAge = 0; $deletedByCount = 0; // Delete by age if specified if ($maxAge !== null) { $deletedByAge = deleteByAge($pdo, $echoId, $maxAge, $dryRun, $quiet); $deletedCount += $deletedByAge; if (!$quiet && $deletedByAge > 0) { echo " Deleted by age (>$maxAge days): $deletedByAge\n"; } } // Delete by count if specified (after age deletion) // In dry-run mode with both parameters, we need to account for age deletions if ($maxCount !== null) { if ($dryRun && $maxAge !== null) { // In dry-run mode, simulate the remaining count after age deletion $remainingAfterAge = $currentCount - $deletedByAge; if ($remainingAfterAge > $maxCount) { $deletedByCount = $remainingAfterAge - $maxCount; } } else { $deletedByCount = deleteByCount($pdo, $echoId, $maxCount, $dryRun, $quiet); } $deletedCount += $deletedByCount; if (!$quiet && $deletedByCount > 0) { echo " Deleted by count (keep $maxCount): $deletedByCount\n"; } } // Update message count if not dry run if (!$dryRun && $deletedCount > 0) { updateMessageCount($pdo, $echoId); if (!$quiet) { $newCount = $currentCount - $deletedCount; echo " New message count: $newCount\n"; } } if (!$quiet) { if ($deletedCount > 0) { echo " ? " . ($dryRun ? "Would delete" : "Deleted") . " $deletedCount message(s)\n"; } else { echo " ? No messages to delete\n"; } echo "\n"; } return $deletedCount; } /** * Delete messages older than specified days */ function deleteByAge($pdo, $echoId, $maxAge, $dryRun, $quiet) { // Calculate cutoff date $cutoffDate = new DateTime(); $cutoffDate->modify("-$maxAge days"); $cutoffDateStr = $cutoffDate->format('Y-m-d H:i:s'); if ($dryRun) { // Count messages that would be deleted $stmt = $pdo->prepare(" SELECT COUNT(*) as count FROM echomail WHERE echoarea_id = :echoarea_id AND date_received < :cutoff_date "); $stmt->execute([ 'echoarea_id' => $echoId, 'cutoff_date' => $cutoffDateStr ]); $result = $stmt->fetch(PDO::FETCH_ASSOC); return (int)$result['count']; } else { // Delete related records first, then echomail messages // Get list of message IDs that will be deleted $stmt = $pdo->prepare(" SELECT id FROM echomail WHERE echoarea_id = :echoarea_id AND date_received < :cutoff_date "); $stmt->execute([ 'echoarea_id' => $echoId, 'cutoff_date' => $cutoffDateStr ]); $messageIds = $stmt->fetchAll(PDO::FETCH_COLUMN); if (empty($messageIds)) { return 0; } // Delete related records deleteRelatedRecords($pdo, $messageIds); // Now delete the echomail messages $stmt = $pdo->prepare(" DELETE FROM echomail WHERE echoarea_id = :echoarea_id AND date_received < :cutoff_date "); $stmt->execute([ 'echoarea_id' => $echoId, 'cutoff_date' => $cutoffDateStr ]); return $stmt->rowCount(); } } /** * Delete oldest messages if count exceeds maximum */ function deleteByCount($pdo, $echoId, $maxCount, $dryRun, $quiet) { // First, check current count $stmt = $pdo->prepare(" SELECT COUNT(*) as count FROM echomail WHERE echoarea_id = :echoarea_id "); $stmt->execute(['echoarea_id' => $echoId]); $result = $stmt->fetch(PDO::FETCH_ASSOC); $currentCount = (int)$result['count']; if ($currentCount <= $maxCount) { return 0; // Nothing to delete } $deleteCount = $currentCount - $maxCount; if ($dryRun) { return $deleteCount; } else { // Get list of message IDs to delete (oldest messages) $stmt = $pdo->prepare(" SELECT id FROM echomail WHERE echoarea_id = :echoarea_id ORDER BY date_received ASC, id ASC LIMIT :delete_count "); $stmt->execute([ 'echoarea_id' => $echoId, 'delete_count' => $deleteCount ]); $messageIds = $stmt->fetchAll(PDO::FETCH_COLUMN); if (empty($messageIds)) { return 0; } // Delete related records first deleteRelatedRecords($pdo, $messageIds); // Now delete the echomail messages using CTE for better PostgreSQL performance $stmt = $pdo->prepare(" WITH messages_to_delete AS ( SELECT id FROM echomail WHERE echoarea_id = :echoarea_id ORDER BY date_received ASC, id ASC LIMIT :delete_count ) DELETE FROM echomail WHERE id IN (SELECT id FROM messages_to_delete) "); $stmt->execute([ 'echoarea_id' => $echoId, 'delete_count' => $deleteCount ]); return $stmt->rowCount(); } } /** * Update the message count for an echo area */ function updateMessageCount($pdo, $echoId) { $stmt = $pdo->prepare(" UPDATE echoareas SET message_count = ( SELECT COUNT(*) FROM echomail WHERE echoarea_id = :echoarea_id ) WHERE id = :echoarea_id "); $stmt->execute(['echoarea_id' => $echoId]); } /** * Delete related records for echomail messages * This handles cleanup of message_read_status, saved_messages, shared_messages, and message_links */ function deleteRelatedRecords($pdo, $messageIds) { if (empty($messageIds)) { return; } // For large batches, process in chunks to avoid query size limits $chunkSize = 1000; $chunks = array_chunk($messageIds, $chunkSize); foreach ($chunks as $chunk) { $placeholders = str_repeat('?,', count($chunk) - 1) . '?'; // Delete from message_read_status $stmt = $pdo->prepare(" DELETE FROM message_read_status WHERE message_type = 'echomail' AND message_id IN ($placeholders) "); $stmt->execute($chunk); // Delete from saved_messages $stmt = $pdo->prepare(" DELETE FROM saved_messages WHERE message_type = 'echomail' AND message_id IN ($placeholders) "); $stmt->execute($chunk); // Delete from shared_messages $stmt = $pdo->prepare(" DELETE FROM shared_messages WHERE message_type = 'echomail' AND message_id IN ($placeholders) "); $stmt->execute($chunk); // Delete from message_links $stmt = $pdo->prepare(" DELETE FROM message_links WHERE message_type = 'echomail' AND message_id IN ($placeholders) "); $stmt->execute($chunk); // Clear reply_to_id references pointing to messages being deleted $stmt = $pdo->prepare(" UPDATE echomail SET reply_to_id = NULL WHERE reply_to_id IN ($placeholders) "); $stmt->execute($chunk); } } /** * Format elapsed time in a human-readable format */ function formatElapsedTime($seconds) { if ($seconds < 1) { return number_format($seconds * 1000, 0) . " ms"; } elseif ($seconds < 60) { return number_format($seconds, 2) . " seconds"; } elseif ($seconds < 3600) { $minutes = floor($seconds / 60); $remainingSeconds = $seconds % 60; return sprintf("%d min %d sec", $minutes, $remainingSeconds); } else { $hours = floor($seconds / 3600); $remainingMinutes = floor(($seconds % 3600) / 60); $remainingSeconds = $seconds % 60; return sprintf("%d hr %d min %d sec", $hours, $remainingMinutes, $remainingSeconds); } }