99 lines
4.5 KiB
PHP
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;
|
|
});
|
|
}
|
|
}
|