TenantAtlas/app/Jobs/RemovePoliciesFromBackupSetJob.php
2026-01-19 18:50:11 +01:00

312 lines
10 KiB
PHP

<?php
namespace App\Jobs;
use App\Jobs\Middleware\TrackOperationRun;
use App\Models\BackupItem;
use App\Models\BackupSet;
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Models\User;
use App\Services\Intune\AuditLogger;
use App\Services\OperationRunService;
use App\Support\OperationRunLinks;
use App\Support\OpsUx\RunFailureSanitizer;
use Filament\Notifications\Notification;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Throwable;
class RemovePoliciesFromBackupSetJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public ?OperationRun $operationRun = null;
/**
* @param array<int,int> $backupItemIds
*/
public function __construct(
public int $backupSetId,
public array $backupItemIds,
public int $initiatorUserId,
?OperationRun $operationRun = null,
) {
$this->operationRun = $operationRun;
}
/**
* @return array<int, object>
*/
public function middleware(): array
{
return [new TrackOperationRun];
}
public function handle(
AuditLogger $auditLogger,
): void {
$backupSet = BackupSet::query()->with(['tenant'])->find($this->backupSetId);
if (! $backupSet instanceof BackupSet) {
if ($this->operationRun) {
/** @var OperationRunService $opService */
$opService = app(OperationRunService::class);
$opService->updateRun(
$this->operationRun,
'completed',
'failed',
['failed' => 1],
[['code' => 'backup_set.not_found', 'message' => 'Backup set not found.']]
);
}
return;
}
$tenant = $backupSet->tenant;
$initiator = User::query()->find($this->initiatorUserId);
$requestedIds = collect($this->backupItemIds)
->map(fn (mixed $value): int => (int) $value)
->filter(fn (int $value): bool => $value > 0)
->unique()
->sort()
->values()
->all();
$requestedCount = count($requestedIds);
$failures = [];
try {
/** @var \Illuminate\Database\Eloquent\Collection<int, BackupItem> $items */
$items = BackupItem::query()
->where('backup_set_id', $backupSet->getKey())
->whereIn('id', $requestedIds)
->get();
$foundIds = $items->pluck('id')->map(fn (mixed $value): int => (int) $value)->all();
$missingIds = array_values(array_diff($requestedIds, $foundIds));
foreach ($missingIds as $missingId) {
$failures[] = [
'code' => 'backup_item.not_found',
'message' => RunFailureSanitizer::sanitizeMessage("Backup item {$missingId} not found (already removed?)."),
];
}
$removed = 0;
$policyIds = [];
$policyIdentifiers = [];
foreach ($items as $item) {
$item->delete();
$removed++;
if ($item->policy_id) {
$policyIds[] = (int) $item->policy_id;
}
if ($item->policy_identifier) {
$policyIdentifiers[] = (string) $item->policy_identifier;
}
}
$backupSet->update([
'item_count' => $backupSet->items()->count(),
]);
if ($tenant instanceof Tenant) {
$auditLogger->log(
tenant: $tenant,
action: 'backup.items_removed',
resourceType: 'backup_set',
resourceId: (string) $backupSet->getKey(),
status: 'success',
context: [
'metadata' => [
'removed_count' => $removed,
'requested_count' => $requestedCount,
'missing_count' => count($missingIds),
'policy_ids' => array_values(array_unique($policyIds)),
'policy_identifiers' => array_values(array_unique($policyIdentifiers)),
'backup_set_id' => (int) $backupSet->getKey(),
'initiator_user_id' => $initiator?->getKey(),
],
],
actorId: $initiator?->getKey(),
);
}
if ($this->operationRun) {
/** @var OperationRunService $opService */
$opService = app(OperationRunService::class);
$this->operationRun->update([
'context' => array_merge($this->operationRun->context ?? [], [
'backup_set_id' => (int) $backupSet->getKey(),
'requested_count' => $requestedCount,
'removed_count' => $removed,
'missing_count' => count($missingIds),
'remaining_count' => (int) $backupSet->item_count,
]),
]);
$outcome = 'succeeded';
if ($removed === 0) {
$outcome = 'failed';
} elseif ($failures !== []) {
$outcome = 'partially_succeeded';
}
$opService->updateRun(
$this->operationRun,
'completed',
$outcome,
[
'total' => $requestedCount,
'processed' => $requestedCount,
'succeeded' => $removed,
'failed' => count($missingIds),
'deleted' => $removed,
'items' => $requestedCount,
],
$failures,
);
}
$this->notifyCompleted(
initiator: $initiator,
tenant: $tenant instanceof Tenant ? $tenant : null,
removed: $removed,
requested: $requestedCount,
missing: count($missingIds),
outcome: $outcome,
);
} catch (Throwable $throwable) {
if ($tenant instanceof Tenant) {
$auditLogger->log(
tenant: $tenant,
action: 'backup.items_removed',
resourceType: 'backup_set',
resourceId: (string) $backupSet->getKey(),
status: 'failed',
context: [
'metadata' => [
'requested_count' => $requestedCount,
'backup_set_id' => (int) $backupSet->getKey(),
],
],
actorId: $initiator?->getKey(),
);
}
if ($this->operationRun) {
/** @var OperationRunService $opService */
$opService = app(OperationRunService::class);
$opService->failRun($this->operationRun, $throwable);
}
$this->notifyFailed(
initiator: $initiator,
tenant: $tenant instanceof Tenant ? $tenant : null,
reason: RunFailureSanitizer::sanitizeMessage($throwable->getMessage()),
);
throw $throwable;
}
}
private function notifyCompleted(
?User $initiator,
?Tenant $tenant,
int $removed,
int $requested,
int $missing,
?string $outcome,
): void {
if (! $initiator instanceof User) {
return;
}
if (! $this->operationRun) {
return;
}
$message = "Removed {$removed} policies";
if ($missing > 0) {
$message .= " ({$missing} missing)";
}
if ($requested !== $removed && $missing === 0) {
$skipped = max(0, $requested - $removed);
if ($skipped > 0) {
$message .= " ({$skipped} not removed)";
}
}
$message .= '.';
$partial = in_array((string) $outcome, ['partially_succeeded'], true) || $missing > 0;
$failed = in_array((string) $outcome, ['failed'], true);
$notification = Notification::make()
->title($failed ? 'Removal failed' : ($partial ? 'Removal completed (partial)' : 'Removal completed'))
->body($message);
if ($tenant instanceof Tenant) {
$notification->actions([
\Filament\Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($this->operationRun, $tenant)),
]);
}
if ($failed) {
$notification->danger();
} elseif ($partial) {
$notification->warning();
} else {
$notification->success();
}
$notification
->sendToDatabase($initiator)
->send();
}
private function notifyFailed(?User $initiator, ?Tenant $tenant, string $reason): void
{
if (! $initiator instanceof User) {
return;
}
if (! $this->operationRun) {
return;
}
$notification = Notification::make()
->title('Removal failed')
->body($reason);
if ($tenant instanceof Tenant) {
$notification->actions([
\Filament\Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($this->operationRun, $tenant)),
]);
}
$notification
->danger()
->sendToDatabase($initiator)
->send();
}
}