mirror of
https://github.com/openvk/openvk
synced 2025-07-28 02:02:02 +03:00
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
This commit is contained in:
parent
c8a97f8b8d
commit
5ae8b62cd1
4 changed files with 179 additions and 2 deletions
100
CLI/CleanupPendingUploadsCommand.php
Normal file
100
CLI/CleanupPendingUploadsCommand.php
Normal file
|
@ -0,0 +1,100 @@
|
|||
<?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];
|
||||
}
|
||||
}
|
40
CLI/README.md
Normal file
40
CLI/README.md
Normal file
|
@ -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.
|
|
@ -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");
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue