*/ 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; }); } }