mirror of
https://github.com/getgrav/grav.git
synced 2026-05-07 21:46:00 +02:00
36
bin/cache-cleanup
Executable file
36
bin/cache-cleanup
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Cache Cleanup Script for Grav CMS
|
||||
*
|
||||
* This is a wrapper script that calls the proper Grav CLI command.
|
||||
* For full help and options, run: bin/grav cache-cleanup --help
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
// Get the directory where this script lives
|
||||
$binDir = __DIR__;
|
||||
$gravBin = $binDir . '/grav';
|
||||
|
||||
// Check if the grav binary exists
|
||||
if (!file_exists($gravBin)) {
|
||||
fwrite(STDERR, "Error: grav binary not found at {$gravBin}\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Build the command arguments, prepending 'cache-cleanup'
|
||||
$args = array_slice($_SERVER['argv'], 1);
|
||||
array_unshift($args, 'cache-cleanup');
|
||||
|
||||
// Build the full command
|
||||
$command = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($gravBin);
|
||||
foreach ($args as $arg) {
|
||||
$command .= ' ' . escapeshellarg($arg);
|
||||
}
|
||||
|
||||
// Execute and pass through the exit code
|
||||
passthru($command, $exitCode);
|
||||
exit($exitCode);
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace Grav\Console\Application;
|
||||
|
||||
use Grav\Console\Cli\BackupCommand;
|
||||
use Grav\Console\Cli\CacheCleanupCommand;
|
||||
use Grav\Console\Cli\CleanCommand;
|
||||
use Grav\Console\Cli\ClearCacheCommand;
|
||||
use Grav\Console\Cli\ComposerCommand;
|
||||
@@ -40,6 +41,7 @@ class GravApplication extends Application
|
||||
new SandboxCommand(),
|
||||
new CleanCommand(),
|
||||
new ClearCacheCommand(),
|
||||
new CacheCleanupCommand(),
|
||||
new BackupCommand(),
|
||||
new NewProjectCommand(),
|
||||
new SchedulerCommand(),
|
||||
|
||||
269
system/src/Grav/Console/Cli/CacheCleanupCommand.php
Normal file
269
system/src/Grav/Console/Cli/CacheCleanupCommand.php
Normal file
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Console\Cli
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Console\Cli;
|
||||
|
||||
use DirectoryIterator;
|
||||
use Exception;
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Console\GravCommand;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* Class CacheCleanupCommand
|
||||
* @package Grav\Console\Cli
|
||||
*/
|
||||
class CacheCleanupCommand extends GravCommand
|
||||
{
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('cache-cleanup')
|
||||
->setAliases(['cleanup'])
|
||||
->setDescription('Removes orphaned cache directories that are no longer in use')
|
||||
->addOption('force', 'f', InputOption::VALUE_NONE, 'Actually delete orphaned caches (dry run without this)')
|
||||
->addOption('max-age', 'd', InputOption::VALUE_REQUIRED, 'Delete orphaned caches older than N days', '1')
|
||||
->addOption('max-age-weeks', 'w', InputOption::VALUE_REQUIRED, 'Delete orphaned caches older than N weeks')
|
||||
->addOption('max-age-months', 'm', InputOption::VALUE_REQUIRED, 'Delete orphaned caches older than N months')
|
||||
->setHelp(<<<'EOF'
|
||||
The <info>cache-cleanup</info> command removes orphaned cache directories that are no longer in use.
|
||||
Only keeps the current cache key directory.
|
||||
|
||||
<comment>Dry run (shows what would be deleted):</comment>
|
||||
<info>bin/grav cache-cleanup</info>
|
||||
|
||||
<comment>Actually delete orphaned caches:</comment>
|
||||
<info>bin/grav cache-cleanup --force</info>
|
||||
|
||||
<comment>Delete orphaned caches older than 7 days:</comment>
|
||||
<info>bin/grav cache-cleanup --force --max-age=7</info>
|
||||
|
||||
<comment>Delete orphaned caches older than 2 weeks:</comment>
|
||||
<info>bin/grav cache-cleanup --force --max-age-weeks=2</info>
|
||||
|
||||
<comment>Delete orphaned caches older than 1 month:</comment>
|
||||
<info>bin/grav cache-cleanup --force --max-age-months=1</info>
|
||||
|
||||
<comment>Cron example (run daily at 3am):</comment>
|
||||
<info>0 3 * * * /path/to/grav/bin/grav cache-cleanup --force >> /var/log/grav-cache-cleanup.log 2>&1</info>
|
||||
EOF
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
protected function serve(): int
|
||||
{
|
||||
$this->initializeGrav();
|
||||
|
||||
$input = $this->getInput();
|
||||
$io = $this->getIO();
|
||||
|
||||
$force = $input->getOption('force');
|
||||
$maxAge = $this->calculateMaxAgeDays();
|
||||
$maxAgeSeconds = $maxAge * 86400;
|
||||
|
||||
$grav = Grav::instance();
|
||||
$cache = $grav['cache'];
|
||||
$currentKey = $cache->getKey();
|
||||
|
||||
// Extract just the uniqueness part (after the prefix and dash)
|
||||
$currentUniqueness = substr($currentKey, strpos($currentKey, '-') + 1);
|
||||
|
||||
$io->title('Grav Cache Cleanup');
|
||||
$io->writeln("Current cache key: <info>{$currentKey}</info>");
|
||||
$io->writeln("Current uniqueness: <info>{$currentUniqueness}</info>");
|
||||
$io->writeln("Max age for orphaned caches: <info>{$maxAge} day(s)</info>");
|
||||
$io->writeln('Mode: ' . ($force ? '<red>FORCE (will delete)</red>' : '<yellow>DRY RUN (use --force to delete)</yellow>'));
|
||||
$io->newLine();
|
||||
|
||||
$cacheDir = GRAV_ROOT . '/cache';
|
||||
|
||||
if (!is_dir($cacheDir)) {
|
||||
$io->error("Cache directory not found: {$cacheDir}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$totalDeleted = 0;
|
||||
$totalSize = 0;
|
||||
$keptCount = 0;
|
||||
$skippedCount = 0;
|
||||
|
||||
// Directories that contain cache key subdirectories (8-char hex)
|
||||
$cacheKeyDirs = [
|
||||
$cacheDir . '/doctrine',
|
||||
$cacheDir . '/grav',
|
||||
];
|
||||
|
||||
foreach ($cacheKeyDirs as $scanDir) {
|
||||
if (!is_dir($scanDir)) {
|
||||
if ($io->isVerbose()) {
|
||||
$io->writeln("Skipping (not found): {$scanDir}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$io->writeln("Scanning: <cyan>{$scanDir}</cyan>");
|
||||
$iterator = new DirectoryIterator($scanDir);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isDot() || !$file->isDir()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dirName = $file->getBasename();
|
||||
$dirPath = $file->getPathname();
|
||||
|
||||
// Only process directories that look like cache keys (8-char hex)
|
||||
if (!preg_match('/^[a-f0-9]{8}$/', $dirName)) {
|
||||
if ($io->isVerbose()) {
|
||||
$io->writeln("[SKIP] {$dirName} (not a cache key directory)");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$dirAge = $now - $file->getMTime();
|
||||
$dirAgeDays = round($dirAge / 86400, 1);
|
||||
|
||||
// Get directory size
|
||||
$size = $this->getDirectorySize($dirPath);
|
||||
$sizeFormatted = $this->formatBytes($size);
|
||||
|
||||
if ($dirName === $currentUniqueness) {
|
||||
$io->writeln("<green>[KEEP]</green> {$dirName} (CURRENT - {$sizeFormatted})");
|
||||
$keptCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if old enough to delete
|
||||
if ($dirAge < $maxAgeSeconds) {
|
||||
$io->writeln("<yellow>[SKIP]</yellow> {$dirName} (only {$dirAgeDays} days old, waiting for {$maxAge} days - {$sizeFormatted})");
|
||||
$skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$io->writeln("<red>[DELETE]</red> {$dirName} ({$dirAgeDays} days old - {$sizeFormatted})");
|
||||
|
||||
if ($force) {
|
||||
try {
|
||||
Folder::delete($dirPath);
|
||||
$totalDeleted++;
|
||||
$totalSize += $size;
|
||||
if ($io->isVerbose()) {
|
||||
$io->writeln(' -> Deleted successfully');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$io->writeln(' -> <red>ERROR:</red> ' . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
$totalDeleted++;
|
||||
$totalSize += $size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$io->newLine();
|
||||
$io->section('Summary');
|
||||
$io->writeln("Current cache kept: <green>{$keptCount}</green>");
|
||||
$io->writeln("Orphaned caches skipped (too new): <yellow>{$skippedCount}</yellow>");
|
||||
|
||||
if ($force) {
|
||||
$io->writeln("Orphaned caches deleted: <red>{$totalDeleted}</red>");
|
||||
$io->writeln('Space freed: <info>' . $this->formatBytes($totalSize) . '</info>');
|
||||
} else {
|
||||
$io->writeln("Orphaned caches to delete: <red>{$totalDeleted}</red>");
|
||||
$io->writeln('Space to free: <info>' . $this->formatBytes($totalSize) . '</info>');
|
||||
if ($totalDeleted > 0) {
|
||||
$io->newLine();
|
||||
$io->note('Run with --force to actually delete these directories.');
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate max age in days from the various options
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function calculateMaxAgeDays(): int
|
||||
{
|
||||
$input = $this->getInput();
|
||||
|
||||
// Check for months first (highest priority)
|
||||
$months = $input->getOption('max-age-months');
|
||||
if ($months !== null) {
|
||||
return (int)$months * 30;
|
||||
}
|
||||
|
||||
// Check for weeks
|
||||
$weeks = $input->getOption('max-age-weeks');
|
||||
if ($weeks !== null) {
|
||||
return (int)$weeks * 7;
|
||||
}
|
||||
|
||||
// Default to days
|
||||
return (int)$input->getOption('max-age');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get directory size recursively
|
||||
*
|
||||
* @param string $path
|
||||
* @return int
|
||||
*/
|
||||
private function getDirectorySize(string $path): int
|
||||
{
|
||||
$size = 0;
|
||||
|
||||
try {
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::LEAVES_ONLY
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile()) {
|
||||
$size += $file->getSize();
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Ignore errors, return what we have
|
||||
}
|
||||
|
||||
return $size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes to human readable
|
||||
*
|
||||
* @param int $bytes
|
||||
* @param int $precision
|
||||
* @return string
|
||||
*/
|
||||
private function formatBytes(int $bytes, int $precision = 2): string
|
||||
{
|
||||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
|
||||
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
|
||||
$bytes /= 1024;
|
||||
}
|
||||
|
||||
return round($bytes, $precision) . ' ' . $units[$i];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user