Allow heading tags (h1-h6) in ContentSanitizer so news editor headings render
This commit is contained in:
290
app/Console/Commands/Enhance/CleanupEnhanceJobsCommand.php
Normal file
290
app/Console/Commands/Enhance/CleanupEnhanceJobsCommand.php
Normal file
@@ -0,0 +1,290 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands\Enhance;
|
||||
|
||||
use App\Models\EnhanceJob;
|
||||
use App\Services\Enhance\EnhanceStorageService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Throwable;
|
||||
|
||||
final class CleanupEnhanceJobsCommand extends Command
|
||||
{
|
||||
protected $signature = 'enhance:cleanup
|
||||
{--dry-run : Preview cleanup actions only}
|
||||
{--force : Delete files and update records}
|
||||
{--only= : Restrict cleanup to expired, deleted, failed, or orphaned}
|
||||
{--days= : Override retention days for failed or deleted cleanup}';
|
||||
|
||||
protected $description = 'Safely clean expired, deleted, failed, and orphaned Enhance files.';
|
||||
|
||||
public function __construct(
|
||||
private readonly EnhanceStorageService $storage,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$target = strtolower(trim((string) $this->option('only')));
|
||||
$validTargets = ['', 'expired', 'deleted', 'failed', 'orphaned'];
|
||||
|
||||
if (! in_array($target, $validTargets, true)) {
|
||||
$this->error('The --only option must be one of: expired, deleted, failed, orphaned.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ((bool) $this->option('dry-run') && (bool) $this->option('force')) {
|
||||
$this->error('Use either --dry-run or --force, not both.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$dryRun = (bool) $this->option('dry-run') || ! (bool) $this->option('force');
|
||||
$daysOverride = $this->option('days');
|
||||
$selectedTarget = $target !== '' ? $target : 'all';
|
||||
|
||||
Log::info('enhance.cleanup.started', [
|
||||
'dry_run' => $dryRun,
|
||||
'target' => $selectedTarget,
|
||||
'days_override' => $daysOverride,
|
||||
]);
|
||||
|
||||
if ($dryRun) {
|
||||
$this->warn('Running in dry-run mode. No files will be deleted.');
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
|
||||
if ($target === '' || $target === 'expired') {
|
||||
$expired = $this->cleanupExpiredCompletedJobs($dryRun);
|
||||
$rows[] = ['expired', $expired['jobs'], $expired['files'], $dryRun ? 'dry-run' : 'cleaned'];
|
||||
}
|
||||
|
||||
if ($target === '' || $target === 'failed') {
|
||||
$failed = $this->cleanupFailedJobs($dryRun, $daysOverride);
|
||||
$rows[] = ['failed', $failed['jobs'], $failed['files'], $dryRun ? 'dry-run' : 'cleaned'];
|
||||
}
|
||||
|
||||
if ($target === '' || $target === 'deleted') {
|
||||
$deleted = $this->cleanupSoftDeletedJobs($dryRun, $daysOverride);
|
||||
$rows[] = ['deleted', $deleted['jobs'], $deleted['files'], $dryRun ? 'dry-run' : 'cleaned'];
|
||||
}
|
||||
|
||||
if ($target === 'orphaned') {
|
||||
$orphaned = $this->scanOrphanedFiles($dryRun);
|
||||
$rows[] = ['orphaned', $orphaned['files'], $orphaned['deleted'], $dryRun ? 'dry-run' : 'deleted'];
|
||||
|
||||
if ($orphaned['unsupported']) {
|
||||
$this->warn('Orphaned file scan was skipped because the configured disk does not support safe listing.');
|
||||
}
|
||||
|
||||
foreach ($orphaned['sample'] as $path) {
|
||||
$this->line(' - ' . $path);
|
||||
}
|
||||
}
|
||||
|
||||
if ($rows !== []) {
|
||||
$this->table(['Target', 'Jobs/Files', 'Files deleted', 'Mode'], $rows);
|
||||
}
|
||||
|
||||
Log::info('enhance.cleanup.completed', [
|
||||
'dry_run' => $dryRun,
|
||||
'target' => $selectedTarget,
|
||||
'rows' => $rows,
|
||||
]);
|
||||
|
||||
$this->info('Enhance cleanup finished.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function cleanupExpiredCompletedJobs(bool $dryRun): array
|
||||
{
|
||||
$query = EnhanceJob::query()
|
||||
->where('status', EnhanceJob::STATUS_COMPLETED)
|
||||
->whereNotNull('expires_at')
|
||||
->where('expires_at', '<=', now());
|
||||
|
||||
return $this->cleanupJobs($query, $dryRun, 'expired', fn (): array => [
|
||||
'status' => EnhanceJob::STATUS_EXPIRED,
|
||||
]);
|
||||
}
|
||||
|
||||
private function cleanupFailedJobs(bool $dryRun, mixed $daysOverride): array
|
||||
{
|
||||
$days = $this->resolveDays($daysOverride, (int) config('enhance.lifecycle.failed_expires_after_days', 7));
|
||||
$cutoff = now()->subDays($days);
|
||||
|
||||
$query = EnhanceJob::query()
|
||||
->where('status', EnhanceJob::STATUS_FAILED)
|
||||
->where(function (Builder $builder) use ($cutoff): void {
|
||||
$builder
|
||||
->where('finished_at', '<=', $cutoff)
|
||||
->orWhere(function (Builder $fallback) use ($cutoff): void {
|
||||
$fallback->whereNull('finished_at')->where('created_at', '<=', $cutoff);
|
||||
});
|
||||
});
|
||||
|
||||
return $this->cleanupJobs($query, $dryRun, 'failed-expired');
|
||||
}
|
||||
|
||||
private function cleanupSoftDeletedJobs(bool $dryRun, mixed $daysOverride): array
|
||||
{
|
||||
$days = $this->resolveDays($daysOverride, (int) config('enhance.lifecycle.deleted_file_grace_days', 1));
|
||||
$cutoff = now()->subDays($days);
|
||||
|
||||
$query = EnhanceJob::withTrashed()
|
||||
->whereNotNull('deleted_at')
|
||||
->where('deleted_at', '<=', $cutoff);
|
||||
|
||||
return $this->cleanupJobs($query, $dryRun, 'deleted-grace');
|
||||
}
|
||||
|
||||
private function cleanupJobs(Builder $query, bool $dryRun, string $reason, ?callable $attributes = null): array
|
||||
{
|
||||
$result = ['jobs' => 0, 'files' => 0];
|
||||
$chunkSize = max(1, (int) config('enhance.lifecycle.cleanup_chunk_size', 100));
|
||||
|
||||
$query->chunkById($chunkSize, function ($jobs) use (&$result, $dryRun, $reason, $attributes): void {
|
||||
foreach ($jobs as $job) {
|
||||
$result['jobs']++;
|
||||
$result['files'] += count($this->enhanceJobPaths($job));
|
||||
|
||||
if ($dryRun) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$deleteResult = $this->storage->deleteFilesForJob($job);
|
||||
$metadata = is_array($job->metadata) ? $job->metadata : [];
|
||||
|
||||
$job->forceFill(array_merge(
|
||||
$this->cleanupAttributesForJob($job),
|
||||
$attributes ? $attributes($job) : [],
|
||||
[
|
||||
'metadata' => array_merge($metadata, [
|
||||
'cleanup' => [
|
||||
'files_removed_at' => now()->toIso8601String(),
|
||||
'reason' => $reason,
|
||||
'deleted' => $deleteResult['deleted'],
|
||||
],
|
||||
]),
|
||||
],
|
||||
))->save();
|
||||
}
|
||||
});
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function scanOrphanedFiles(bool $dryRun): array
|
||||
{
|
||||
$disk = $this->storage->diskName();
|
||||
$knownPaths = array_fill_keys(array_map(
|
||||
static fn (string $path): string => ltrim($path, '/'),
|
||||
$this->storage->listKnownJobPaths(),
|
||||
), true);
|
||||
$sample = [];
|
||||
$result = [
|
||||
'files' => 0,
|
||||
'deleted' => 0,
|
||||
'unsupported' => false,
|
||||
'sample' => [],
|
||||
];
|
||||
|
||||
foreach ($this->enhancePrefixes() as $prefix) {
|
||||
try {
|
||||
$files = Storage::disk($disk)->allFiles($prefix);
|
||||
} catch (Throwable) {
|
||||
$result['unsupported'] = true;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
foreach ($files as $file) {
|
||||
$normalized = ltrim($file, '/');
|
||||
|
||||
if (isset($knownPaths[$normalized])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result['files']++;
|
||||
|
||||
if (count($sample) < 20) {
|
||||
$sample[] = $normalized;
|
||||
}
|
||||
|
||||
if (! $dryRun && $this->storage->safeDelete($disk, $normalized)) {
|
||||
$result['deleted']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result['sample'] = $sample;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function cleanupAttributesForJob(EnhanceJob $job): array
|
||||
{
|
||||
$attributes = [];
|
||||
|
||||
if ($this->storage->isEnhancePath($job->source_path)) {
|
||||
$attributes['source_disk'] = null;
|
||||
$attributes['source_path'] = null;
|
||||
$attributes['source_hash'] = null;
|
||||
}
|
||||
|
||||
if ($this->storage->isEnhancePath($job->output_path)) {
|
||||
$attributes['output_disk'] = null;
|
||||
$attributes['output_path'] = null;
|
||||
$attributes['output_hash'] = null;
|
||||
$attributes['output_width'] = null;
|
||||
$attributes['output_height'] = null;
|
||||
$attributes['output_filesize'] = null;
|
||||
$attributes['output_mime'] = null;
|
||||
}
|
||||
|
||||
if ($this->storage->isEnhancePath($job->preview_path)) {
|
||||
$attributes['preview_disk'] = null;
|
||||
$attributes['preview_path'] = null;
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
private function enhanceJobPaths(EnhanceJob $job): array
|
||||
{
|
||||
return array_values(array_filter([
|
||||
$this->storage->isEnhancePath($job->source_path) ? trim((string) $job->source_path) : null,
|
||||
$this->storage->isEnhancePath($job->output_path) ? trim((string) $job->output_path) : null,
|
||||
$this->storage->isEnhancePath($job->preview_path) ? trim((string) $job->preview_path) : null,
|
||||
]));
|
||||
}
|
||||
|
||||
private function enhancePrefixes(): array
|
||||
{
|
||||
return array_values(array_filter(array_unique(array_map(
|
||||
static fn (string $prefix): string => trim($prefix, '/'),
|
||||
[
|
||||
(string) config('enhance.source_prefix', 'enhance/sources'),
|
||||
(string) config('enhance.output_prefix', 'enhance/outputs'),
|
||||
(string) config('enhance.preview_prefix', 'enhance/previews'),
|
||||
],
|
||||
))));
|
||||
}
|
||||
|
||||
private function resolveDays(mixed $daysOverride, int $default): int
|
||||
{
|
||||
if ($daysOverride === null || $daysOverride === '') {
|
||||
return max(0, $default);
|
||||
}
|
||||
|
||||
return max(0, (int) $daysOverride);
|
||||
}
|
||||
}
|
||||
108
app/Console/Commands/Enhance/EnhanceHealthCommand.php
Normal file
108
app/Console/Commands/Enhance/EnhanceHealthCommand.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands\Enhance;
|
||||
|
||||
use App\Models\EnhanceJob;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
final class EnhanceHealthCommand extends Command
|
||||
{
|
||||
protected $signature = 'enhance:health {--json : Output machine-readable JSON}';
|
||||
|
||||
protected $description = 'Report operational health and lifecycle metrics for Enhance jobs.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$payload = $this->payload();
|
||||
|
||||
if ((bool) $this->option('json')) {
|
||||
$this->line(json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info('Enhance health');
|
||||
$this->newLine();
|
||||
|
||||
$this->table(['Metric', 'Value'], [
|
||||
['Configured engine', $payload['engine']],
|
||||
['Configured queue', $payload['queue']],
|
||||
['Worker URL configured', $payload['worker_configured'] ? 'yes' : 'no'],
|
||||
['Storage disk', $payload['storage_disk']],
|
||||
['Total jobs', $payload['counts']['total']],
|
||||
['Pending jobs', $payload['counts']['pending']],
|
||||
['Queued jobs', $payload['counts']['queued']],
|
||||
['Processing jobs', $payload['counts']['processing']],
|
||||
['Completed jobs', $payload['counts']['completed']],
|
||||
['Failed jobs', $payload['counts']['failed']],
|
||||
['Cancelled jobs', $payload['counts']['cancelled']],
|
||||
['Expired jobs', $payload['counts']['expired']],
|
||||
['Stuck queued jobs', $payload['health']['stuck_queued']],
|
||||
['Stuck processing jobs', $payload['health']['stuck_processing']],
|
||||
['Jobs created today', $payload['today']['created']],
|
||||
['Jobs completed today', $payload['today']['completed']],
|
||||
['Jobs failed today', $payload['today']['failed']],
|
||||
['Average processing time today', $payload['today']['average_processing_seconds'] ?? '—'],
|
||||
]);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function payload(): array
|
||||
{
|
||||
$todayStart = now()->startOfDay();
|
||||
$todayEnd = now()->endOfDay();
|
||||
$stuckQueuedCutoff = now()->subMinutes((int) config('enhance.health.stuck_queued_after_minutes', 60));
|
||||
$stuckProcessingCutoff = now()->subMinutes((int) config('enhance.health.stuck_processing_after_minutes', 30));
|
||||
|
||||
$counts = [
|
||||
'total' => EnhanceJob::query()->count(),
|
||||
'pending' => EnhanceJob::query()->where('status', EnhanceJob::STATUS_PENDING)->count(),
|
||||
'queued' => EnhanceJob::query()->where('status', EnhanceJob::STATUS_QUEUED)->count(),
|
||||
'processing' => EnhanceJob::query()->where('status', EnhanceJob::STATUS_PROCESSING)->count(),
|
||||
'completed' => EnhanceJob::query()->where('status', EnhanceJob::STATUS_COMPLETED)->count(),
|
||||
'failed' => EnhanceJob::query()->where('status', EnhanceJob::STATUS_FAILED)->count(),
|
||||
'cancelled' => EnhanceJob::query()->where('status', EnhanceJob::STATUS_CANCELLED)->count(),
|
||||
'expired' => EnhanceJob::query()->where('status', EnhanceJob::STATUS_EXPIRED)->count(),
|
||||
];
|
||||
|
||||
return [
|
||||
'engine' => (string) config('enhance.default_engine', EnhanceJob::ENGINE_STUB),
|
||||
'queue' => (string) config('enhance.queue', 'default'),
|
||||
'worker_configured' => trim((string) config('enhance.external_worker.url', '')) !== '',
|
||||
'storage_disk' => (string) config('enhance.disk', 'public'),
|
||||
'counts' => $counts,
|
||||
'health' => [
|
||||
'stuck_queued' => EnhanceJob::query()
|
||||
->where('status', EnhanceJob::STATUS_QUEUED)
|
||||
->whereNotNull('queued_at')
|
||||
->where('queued_at', '<=', $stuckQueuedCutoff)
|
||||
->count(),
|
||||
'stuck_processing' => EnhanceJob::query()
|
||||
->where('status', EnhanceJob::STATUS_PROCESSING)
|
||||
->whereNotNull('started_at')
|
||||
->where('started_at', '<=', $stuckProcessingCutoff)
|
||||
->count(),
|
||||
],
|
||||
'today' => [
|
||||
'created' => EnhanceJob::query()->whereBetween('created_at', [$todayStart, $todayEnd])->count(),
|
||||
'completed' => EnhanceJob::query()
|
||||
->where('status', EnhanceJob::STATUS_COMPLETED)
|
||||
->whereBetween('finished_at', [$todayStart, $todayEnd])
|
||||
->count(),
|
||||
'failed' => EnhanceJob::query()
|
||||
->where('status', EnhanceJob::STATUS_FAILED)
|
||||
->whereBetween('finished_at', [$todayStart, $todayEnd])
|
||||
->count(),
|
||||
'average_processing_seconds' => ($average = EnhanceJob::query()
|
||||
->whereNotNull('processing_seconds')
|
||||
->whereBetween('finished_at', [$todayStart, $todayEnd])
|
||||
->avg('processing_seconds')) !== null
|
||||
? round((float) $average, 2)
|
||||
: null,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
290
app/Console/Commands/Enhance/EnhanceRunCommand.php
Normal file
290
app/Console/Commands/Enhance/EnhanceRunCommand.php
Normal file
@@ -0,0 +1,290 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands\Enhance;
|
||||
|
||||
use App\Models\EnhanceJob;
|
||||
use App\Services\Enhance\EnhanceProcessorFactory;
|
||||
use App\Services\Enhance\EnhanceStorageService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Throwable;
|
||||
|
||||
final class EnhanceRunCommand extends Command
|
||||
{
|
||||
protected $signature = 'enhance:run
|
||||
{--id= : Process specific job ID(s), comma-separated}
|
||||
{--limit=1 : Max pending/queued jobs to pick up from the queue (0 = all)}
|
||||
{--engine= : Override the processing engine for this run (stub, external_worker)}
|
||||
{--failed : Also include failed jobs when scanning the queue}
|
||||
{--dry-run : Show what would be processed without executing}';
|
||||
|
||||
protected $description = 'Synchronously process pending enhance jobs inline — useful for debugging with -v / -vv / -vvv.';
|
||||
|
||||
private const PROCESSABLE_STATUSES = [
|
||||
EnhanceJob::STATUS_PENDING,
|
||||
EnhanceJob::STATUS_QUEUED,
|
||||
EnhanceJob::STATUS_PROCESSING,
|
||||
EnhanceJob::STATUS_FAILED,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly EnhanceProcessorFactory $processorFactory,
|
||||
private readonly EnhanceStorageService $storage,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
$engineOverride = trim((string) $this->option('engine'));
|
||||
$idOption = trim((string) $this->option('id'));
|
||||
$limit = max(0, (int) $this->option('limit'));
|
||||
$includeFailed = (bool) $this->option('failed');
|
||||
|
||||
if ($engineOverride !== '' && ! in_array($engineOverride, [EnhanceJob::ENGINE_STUB, EnhanceJob::ENGINE_EXTERNAL_WORKER], true)) {
|
||||
$this->error("Unknown engine override: {$engineOverride}. Use 'stub' or 'external_worker'.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$jobs = $this->resolveJobs($idOption, $limit, $includeFailed);
|
||||
|
||||
if ($jobs->isEmpty()) {
|
||||
$this->info('No eligible enhance jobs found.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info(sprintf('Found %d job(s) to process.', $jobs->count()));
|
||||
$this->newLine();
|
||||
|
||||
if ($dryRun) {
|
||||
$this->warn('Dry-run mode — no jobs will be processed.');
|
||||
|
||||
foreach ($jobs as $job) {
|
||||
$engine = $engineOverride !== '' ? "{$engineOverride} (overridden)" : $job->engine;
|
||||
$this->line(sprintf(
|
||||
' [dry-run] Job #%d status=%-12s engine=%-18s scale=%dx mode=%-14s user_id=%d',
|
||||
$job->id,
|
||||
$job->status,
|
||||
$engine,
|
||||
$job->scale,
|
||||
$job->mode,
|
||||
$job->user_id,
|
||||
));
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$processed = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($jobs as $job) {
|
||||
if ($this->processJob($job, $engineOverride)) {
|
||||
$processed++;
|
||||
} else {
|
||||
$failed++;
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
$this->info(sprintf('Done: %d completed, %d failed.', $processed, $failed));
|
||||
|
||||
return $failed > 0 ? self::FAILURE : self::SUCCESS;
|
||||
}
|
||||
|
||||
private function resolveJobs(string $idOption, int $limit, bool $includeFailed): Collection
|
||||
{
|
||||
if ($idOption !== '') {
|
||||
$ids = array_filter(array_map('intval', explode(',', $idOption)));
|
||||
|
||||
return EnhanceJob::query()
|
||||
->whereIn('id', $ids)
|
||||
->whereIn('status', self::PROCESSABLE_STATUSES)
|
||||
->get();
|
||||
}
|
||||
|
||||
$statuses = [EnhanceJob::STATUS_PENDING, EnhanceJob::STATUS_QUEUED, EnhanceJob::STATUS_PROCESSING];
|
||||
|
||||
if ($includeFailed) {
|
||||
$statuses[] = EnhanceJob::STATUS_FAILED;
|
||||
}
|
||||
|
||||
$query = EnhanceJob::query()->whereIn('status', $statuses)->oldest();
|
||||
|
||||
if ($limit > 0) {
|
||||
$query->limit($limit);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
private function processJob(EnhanceJob $job, string $engineOverride): bool
|
||||
{
|
||||
$engine = $engineOverride !== '' ? $engineOverride : (string) $job->engine;
|
||||
|
||||
$this->line(sprintf('<comment>--- Job #%d ---</comment>', $job->id));
|
||||
$this->line(sprintf(' Status : %s', $job->status));
|
||||
$this->line(sprintf(' Engine : %s%s', $engine, $engineOverride !== '' ? ' (overridden)' : ''));
|
||||
$this->line(sprintf(' Scale : %dx', $job->scale));
|
||||
$this->line(sprintf(' Mode : %s', $job->mode));
|
||||
$this->line(sprintf(' User : #%d', $job->user_id));
|
||||
|
||||
if ($this->output->isVerbose()) {
|
||||
$this->line(sprintf(
|
||||
' Source : disk=%-10s path=%s',
|
||||
$job->source_disk ?: '(default)',
|
||||
$job->source_path ?: '—',
|
||||
));
|
||||
$this->line(sprintf(
|
||||
' Input : %dx%d size=%s mime=%s',
|
||||
(int) $job->input_width,
|
||||
(int) $job->input_height,
|
||||
$this->formatBytes((int) $job->input_filesize),
|
||||
$job->input_mime ?: '—',
|
||||
));
|
||||
|
||||
if ($job->error_message !== null) {
|
||||
$this->warn(sprintf(' Previous error: %s', $job->error_message));
|
||||
}
|
||||
}
|
||||
|
||||
if (! in_array($job->status, self::PROCESSABLE_STATUSES, true)) {
|
||||
$this->warn(sprintf(' Skipping: status "%s" is not processable.', $job->status));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$job->forceFill([
|
||||
'status' => EnhanceJob::STATUS_PROCESSING,
|
||||
'started_at' => now(),
|
||||
'finished_at' => null,
|
||||
'error_message' => null,
|
||||
])->save();
|
||||
|
||||
$started = microtime(true);
|
||||
$completedExpiryDays = (int) config('enhance.lifecycle.completed_expires_after_days', 30);
|
||||
|
||||
try {
|
||||
$this->line(' Processing...');
|
||||
|
||||
$processor = $this->processorFactory->make($engine);
|
||||
$result = $processor->process($job);
|
||||
|
||||
if ($this->output->isVerbose()) {
|
||||
$this->line(sprintf(
|
||||
' Output : %dx%d size=%s mime=%s',
|
||||
$result->width,
|
||||
$result->height,
|
||||
$this->formatBytes($result->filesize),
|
||||
$result->mime,
|
||||
));
|
||||
$this->line(sprintf(
|
||||
' Stored : disk=%-10s path=%s',
|
||||
$result->disk,
|
||||
$result->path,
|
||||
));
|
||||
}
|
||||
|
||||
$this->line(' Generating preview...');
|
||||
$preview = $this->storage->createPreviewFromStoredOutput($job, $result->disk, $result->path) ?? [];
|
||||
|
||||
$outputHash = null;
|
||||
$outputContents = Storage::disk($result->disk)->get($result->path);
|
||||
|
||||
if (is_string($outputContents) && $outputContents !== '') {
|
||||
$outputHash = hash('sha256', $outputContents);
|
||||
}
|
||||
|
||||
$job->forceFill([
|
||||
'status' => EnhanceJob::STATUS_COMPLETED,
|
||||
'output_disk' => $result->disk,
|
||||
'output_path' => $result->path,
|
||||
'output_hash' => $outputHash,
|
||||
'output_width' => $result->width,
|
||||
'output_height' => $result->height,
|
||||
'output_filesize' => $result->filesize,
|
||||
'output_mime' => $result->mime,
|
||||
'metadata' => array_merge($job->metadata ?? [], $result->metadata ?? []),
|
||||
'processing_seconds' => (int) round(microtime(true) - $started),
|
||||
'finished_at' => now(),
|
||||
'expires_at' => $completedExpiryDays > 0 ? now()->addDays($completedExpiryDays) : null,
|
||||
] + $preview)->save();
|
||||
|
||||
$elapsed = round(microtime(true) - $started, 2);
|
||||
$this->info(sprintf(' Completed in %.2fs', $elapsed));
|
||||
|
||||
if ($this->output->isVerbose() && ! empty($result->metadata)) {
|
||||
$this->line(' Metadata:');
|
||||
|
||||
foreach ($result->metadata as $key => $value) {
|
||||
$display = is_scalar($value) ? (string) $value : json_encode($value);
|
||||
$this->line(sprintf(' %-30s %s', $key . ':', $display));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Throwable $exception) {
|
||||
$elapsed = round(microtime(true) - $started, 2);
|
||||
|
||||
$job->forceFill([
|
||||
'status' => EnhanceJob::STATUS_FAILED,
|
||||
'error_message' => Str::limit($exception->getMessage(), 1000),
|
||||
'processing_seconds' => (int) round(microtime(true) - $started),
|
||||
'finished_at' => now(),
|
||||
])->save();
|
||||
|
||||
$this->error(sprintf(' Failed in %.2fs: %s', $elapsed, $exception->getMessage()));
|
||||
|
||||
if ($this->output->isVerbose()) {
|
||||
$this->line(sprintf(' Exception : %s', get_class($exception)));
|
||||
$this->line(sprintf(' At : %s:%d', $exception->getFile(), $exception->getLine()));
|
||||
|
||||
$previous = $exception->getPrevious();
|
||||
|
||||
if ($previous !== null) {
|
||||
$this->line(sprintf(' Caused by : %s: %s', get_class($previous), $previous->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->output->isVeryVerbose()) {
|
||||
$this->line(' Stack trace:');
|
||||
$frames = array_slice(explode("\n", $exception->getTraceAsString()), 0, 25);
|
||||
|
||||
foreach ($frames as $frame) {
|
||||
$this->line(' ' . $frame);
|
||||
}
|
||||
}
|
||||
|
||||
Log::warning('enhance.run.command.failed', [
|
||||
'enhance_job_id' => $job->id,
|
||||
'engine' => $engine,
|
||||
'message' => $exception->getMessage(),
|
||||
'exception' => get_class($exception),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function formatBytes(int $bytes): string
|
||||
{
|
||||
if ($bytes < 1024) {
|
||||
return $bytes . 'B';
|
||||
}
|
||||
|
||||
if ($bytes < 1_048_576) {
|
||||
return round($bytes / 1024, 1) . 'KB';
|
||||
}
|
||||
|
||||
return round($bytes / 1_048_576, 1) . 'MB';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user