From 5ae8b62cd108b59d2cf060e294197fe3bb8c717e Mon Sep 17 00:00:00 2001 From: Ilia Breitburg Date: Tue, 15 Jul 2025 22:19:30 +0200 Subject: [PATCH] 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 --- CLI/CleanupPendingUploadsCommand.php | 100 +++++++++++++++++++++++++++ CLI/README.md | 40 +++++++++++ Web/Presenters/VKAPIPresenter.php | 40 ++++++++++- openvkctl | 1 + 4 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 CLI/CleanupPendingUploadsCommand.php create mode 100644 CLI/README.md diff --git a/CLI/CleanupPendingUploadsCommand.php b/CLI/CleanupPendingUploadsCommand.php new file mode 100644 index 00000000..8772c015 --- /dev/null +++ b/CLI/CleanupPendingUploadsCommand.php @@ -0,0 +1,100 @@ +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("Photo upload directory not found: {$photoFolder}"); + return Command::FAILURE; + } + + $output->writeln("Scanning for pending uploads older than {$maxAge} hours..."); + + $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("Would delete: " . basename($file) . " (age: {$age}h, size: " . $this->formatBytes($fileSize) . ")"); + } else { + if (unlink($file)) { + $deletedCount++; + $output->writeln("Deleted: " . basename($file) . ""); + } else { + $output->writeln("Failed to delete: " . basename($file) . ""); + } + } + } + } + + if ($dryRun) { + $output->writeln("Dry run completed. Would delete {$deletedCount} files (" . $this->formatBytes($totalSize) . ")"); + } else { + $output->writeln("Cleanup completed. Deleted {$deletedCount} files (" . $this->formatBytes($totalSize) . ")"); + } + + 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]; + } +} diff --git a/CLI/README.md b/CLI/README.md new file mode 100644 index 00000000..1b66610d --- /dev/null +++ b/CLI/README.md @@ -0,0 +1,40 @@ +# OpenVK CLI Commands + +This directory contains command-line utilities for OpenVK management. + +## Available Commands + +### cleanup-pending-uploads +Automatically removes pending photo uploads older than the specified time. + +**Usage:** +```bash +# Clean up uploads older than 24 hours (default) +php openvkctl cleanup-pending-uploads + +# Clean up uploads older than 1 hour +php openvkctl cleanup-pending-uploads --max-age=1 + +# Dry run to see what would be deleted +php openvkctl cleanup-pending-uploads --dry-run +``` + +**Options:** +- `--max-age`, `-a`: Maximum age in hours (default: 24) +- `--dry-run`, `-d`: Show what would be deleted without actually deleting + +**Cron Setup:** +To automatically clean up pending uploads daily, add to your crontab: +```bash +# Clean up pending uploads daily at 2 AM +0 2 * * * cd /path/to/openvk && php openvkctl cleanup-pending-uploads +``` + +### build-images +Rebuilds photo thumbnails and image sizes. + +### fetch-toncoin-transactions +Fetches Toncoin transactions for payment processing. + +### upgrade +Performs database upgrades and migrations. \ No newline at end of file diff --git a/Web/Presenters/VKAPIPresenter.php b/Web/Presenters/VKAPIPresenter.php index 2ab0818a..008e98c9 100644 --- a/Web/Presenters/VKAPIPresenter.php +++ b/Web/Presenters/VKAPIPresenter.php @@ -126,8 +126,14 @@ final class VKAPIPresenter extends OpenVKPresenter $maxFiles = OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["api"]["maxFilesPerDomain"]; $usrFiles = sizeof(glob("$folder/$data[USER]_*.oct")); if ($usrFiles >= $maxFiles) { + $pendingInfo = $this->getPendingUploadInfo($folder, $data["USER"]); header("HTTP/1.1 507 Insufficient Storage"); - exit("There are $maxFiles pending already. Please save them before uploading more :3"); + header("Content-Type: application/json"); + exit(json_encode([ + "error" => "insufficient_storage", + "error_description" => "There are $maxFiles pending already. Please save them before uploading more :3", + "pending_uploads" => $pendingInfo, + ])); } # Not multifile @@ -166,8 +172,14 @@ final class VKAPIPresenter extends OpenVKPresenter unlink($f); } + $pendingInfo = $this->getPendingUploadInfo($folder, $data["USER"]); header("HTTP/1.1 507 Insufficient Storage"); - exit("There are $maxFiles pending already. Please save them before uploading more :3"); + header("Content-Type: application/json"); + exit(json_encode([ + "error" => "insufficient_storage", + "error_description" => "There are $maxFiles pending already. Please save them before uploading more :3", + "pending_uploads" => $pendingInfo, + ])); } $files[++$usrFiles] = move_uploaded_file($file["tmp_name"], "$folder/$data[USER]_$usrFiles.oct"); @@ -194,6 +206,30 @@ final class VKAPIPresenter extends OpenVKPresenter ])); } + private function getPendingUploadInfo(string $folder, string $userId): array + { + $pendingFiles = glob("$folder/$userId" . "_*.oct"); + $pendingInfo = []; + + foreach ($pendingFiles as $file) { + $filename = basename($file); + $uploadId = str_replace([$userId . "_", ".oct"], "", $filename); + $fileTime = filemtime($file); + $fileSize = filesize($file); + $ageHours = round((time() - $fileTime) / 3600, 1); + + $pendingInfo[] = [ + "upload_id" => $uploadId, + "filename" => $filename, + "size" => $fileSize, + "age_hours" => $ageHours, + "uploaded_at" => date("Y-m-d H:i:s", $fileTime), + ]; + } + + return $pendingInfo; + } + public function renderRoute(string $object, string $method): void { $callback = $this->queryParam("callback"); diff --git a/openvkctl b/openvkctl index 13b02d45..dd1f08cc 100755 --- a/openvkctl +++ b/openvkctl @@ -13,5 +13,6 @@ $application = new Application(); $application->add(new CLI\UpgradeCommand()); $application->add(new CLI\RebuildImagesCommand()); $application->add(new CLI\FetchToncoinTransactions()); +$application->add(new CLI\CleanupPendingUploadsCommand()); $application->run();