Files
compras/app/Services/ReceiptDocumentHealth.php
T
nala 63d20b10fc
tests / ci (push) Canceled after 0s
commit de armazens
2026-11-04 09:40:26 +02:00

99 lines
4.5 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use LogicException;
/** Global technical inspection, with aggregate output only. Never repairs or removes files. */
class ReceiptDocumentHealth
{
/** @return array<string,mixed> */
public function inspect(int $limit = 1000): array
{
abort_unless(app()->runningInConsole() && ! auth()->check(), 403, 'Verificação reservada à operação técnica.');
if ($limit < 1 || $limit > 10000) {
throw new LogicException('Limite fora do intervalo permitido.');
}
$result = ['checked_at' => now()->toIso8601String(), 'ok' => false, 'complete' => false, 'reasons' => [],
'documents_checked' => 0, 'files_checked' => 0, 'missing' => 0, 'invalid_reference' => 0, 'altered' => 0,
'unreferenced_recent' => 0, 'unreferenced_older' => 0];
if (! Schema::hasTable('receipt_documents')) {
return [...$result, 'reasons' => ['not_installed']];
}
return DB::transaction(function () use ($limit, $result) {
if (DB::getDriverName() === 'pgsql' && DB::transactionLevel() === 1) {
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
}
$disk = Storage::disk('receipt_documents');
$root = realpath($disk->path(''));
if ($root === false) {
return [...$result, 'reasons' => ['storage_unavailable']];
}
$records = DB::table('receipt_documents')->orderBy('id')->limit($limit + 1)->get();
$limited = $records->count() > $limit;
foreach ($records->take($limit) as $document) {
$result['documents_checked']++;
$prefix = $document->tenant_id.'/'.$document->goods_receipt_id.'/';
$name = substr($document->path, strlen($prefix));
if (! str_starts_with($document->path, $prefix) || ! preg_match('/\A[A-Za-z0-9_-]+\.[A-Za-z0-9]+\z/D', $name)
|| ! DB::table('goods_receipts')->where('id', $document->goods_receipt_id)->where('tenant_id', $document->tenant_id)->exists()) {
$result['invalid_reference']++;
continue;
}
$path = realpath($disk->path($document->path));
if ($path === false || ! is_file($path)) {
$result['missing']++;
continue;
}
$normalizedPath = str_replace('\\', '/', $path);
$normalizedRoot = str_replace('\\', '/', $root).'/';
$withinRoot = PHP_OS_FAMILY === 'Windows'
? str_starts_with(strtolower($normalizedPath), strtolower($normalizedRoot))
: str_starts_with($normalizedPath, $normalizedRoot);
if (! $withinRoot) {
$result['invalid_reference']++;
continue;
}
$size = filesize($path);
if ($size !== (int) $document->size || $size < 1 || $size > 2097152 || hash_file('sha256', $path) !== $document->sha256) {
$result['altered']++;
}
}
// Lazy listing and an independent ceiling also bound unreferenced-file inspection.
foreach ($disk->getDriver()->listContents('', true) as $entry) {
if (! $entry->isFile()) {
continue;
}
if ($result['files_checked'] >= $limit) {
$limited = true;
break;
}
$result['files_checked']++;
if (! DB::table('receipt_documents')->where('path', $entry->path())->exists()) {
$age = now()->getTimestamp() - $disk->lastModified($entry->path());
$result[$age >= 3600 ? 'unreferenced_older' : 'unreferenced_recent']++;
}
}
$result['complete'] = ! $limited;
foreach (['missing', 'invalid_reference', 'altered', 'unreferenced_recent', 'unreferenced_older'] as $reason) {
if ($result[$reason] > 0) {
$result['reasons'][] = $reason;
}
}
if ($limited) {
$result['reasons'][] = 'limit_reached';
}
$result['ok'] = $result['reasons'] === [];
return $result;
});
}
}