openvk/CLI/CleanupPendingUploadsCommand.php
Ilia Breitburg 5ae8b62cd1
fix(api): implement pending uploads cleanup and enhanced error response (#1384)
## Summary
- Implements automatic cleanup mechanism for pending photo uploads older
than 24 hours
- Enhances error response to include actionable information about
pending uploads
- Adds CLI command for manual cleanup with dry-run support

## Changes Made
- **CLI/CleanupPendingUploadsCommand.php**: New command to auto-delete
stale `.oct` files
- **Web/Presenters/VKAPIPresenter.php**: Enhanced error response with
pending upload details
- **openvkctl**: Added cleanup command to CLI bootstrap
- **CLI/README.md**: Documentation with usage examples and cron setup

## Problem Solved
When users encounter "There are 3 pending already" error, they now
receive:
1. **Structured JSON response** with upload details (ID, filename, size,
age, timestamp)
2. **Automatic cleanup** removes uploads older than 24 hours
3. **Manual cleanup** available via CLI command with configurable age
threshold

## Usage
```bash
# Auto-cleanup (daily cron recommended)
php openvkctl cleanup-pending-uploads

# Custom age threshold
php openvkctl cleanup-pending-uploads --max-age=1

# Preview what would be deleted
php openvkctl cleanup-pending-uploads --dry-run
```

Fixes #1275
2025-07-15 23:19:30 +03:00

100 lines
3.2 KiB
PHP

<?php
declare(strict_types=1);
namespace openvk\CLI;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class CleanupPendingUploadsCommand extends Command
{
protected static $defaultName = "cleanup-pending-uploads";
public function __construct()
{
parent::__construct();
}
protected function configure(): void
{
$this->setDescription("Cleanup pending photo uploads older than specified time")
->addOption(
"max-age",
"a",
InputOption::VALUE_OPTIONAL,
"Maximum age in hours (default: 24)",
24
)
->addOption(
"dry-run",
"d",
InputOption::VALUE_NONE,
"Show what would be deleted without actually deleting"
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$maxAge = (int) $input->getOption("max-age");
$dryRun = $input->getOption("dry-run");
$photoFolder = __DIR__ . "/../tmp/api-storage/photos";
if (!is_dir($photoFolder)) {
$output->writeln("<error>Photo upload directory not found: {$photoFolder}</error>");
return Command::FAILURE;
}
$output->writeln("<info>Scanning for pending uploads older than {$maxAge} hours...</info>");
$cutoffTime = time() - ($maxAge * 3600);
$deletedCount = 0;
$totalSize = 0;
$files = glob($photoFolder . "/*_*.oct");
foreach ($files as $file) {
$fileTime = filemtime($file);
if ($fileTime < $cutoffTime) {
$fileSize = filesize($file);
$totalSize += $fileSize;
if ($dryRun) {
$age = round((time() - $fileTime) / 3600, 1);
$output->writeln("<comment>Would delete: " . basename($file) . " (age: {$age}h, size: " . $this->formatBytes($fileSize) . ")</comment>");
} else {
if (unlink($file)) {
$deletedCount++;
$output->writeln("<info>Deleted: " . basename($file) . "</info>");
} else {
$output->writeln("<error>Failed to delete: " . basename($file) . "</error>");
}
}
}
}
if ($dryRun) {
$output->writeln("<info>Dry run completed. Would delete {$deletedCount} files (" . $this->formatBytes($totalSize) . ")</info>");
} else {
$output->writeln("<info>Cleanup completed. Deleted {$deletedCount} files (" . $this->formatBytes($totalSize) . ")</info>");
}
return Command::SUCCESS;
}
private function formatBytes(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, 2) . ' ' . $units[$pow];
}
}