markerPath = storage_path('app/.business-storage-qiniu-synced'); } public function handle(): int { if (config('business_storage.driver') !== 'qiniu') { if ($this->option('require-qiniu')) { $this->components->error('生产业务文件存储尚未启用。'); return self::FAILURE; } $this->components->info('当前使用本机业务文件存储,无需同步。'); return self::SUCCESS; } if (! $this->hasCompleteCredentials()) { $this->components->error('生产业务文件存储凭据不完整。'); return self::FAILURE; } try { $this->probeQiniu(); if ($this->option('probe-only')) { $this->components->info('私有业务文件存储读写验证通过。'); return self::SUCCESS; } $copied = 0; $skipped = 0; if ($this->option('resync') || ! File::exists($this->markerPath)) { [$copied, $skipped] = $this->syncRetainedLocalFiles(); File::put( $this->markerPath, json_encode([ 'completed_at' => now()->toIso8601String(), 'copied' => $copied, 'skipped' => $skipped, ], JSON_UNESCAPED_SLASHES) . PHP_EOL ); } $this->components->info( "私有业务文件存储可用;复制 {$copied} 个,复用 {$skipped} 个,本机原件保留。" ); return self::SUCCESS; } catch (Throwable $e) { report($e); $this->components->error('私有业务文件存储验证或旧文件同步失败。'); return self::FAILURE; } } private function hasCompleteCredentials(): bool { return (string) config('business_storage.qiniu.access_key') !== '' && (string) config('business_storage.qiniu.secret_key') !== ''; } private function probeQiniu(): void { $disk = Storage::disk('business_uploads_qiniu'); $path = '.health/storage-' . bin2hex(random_bytes(12)) . '.txt'; $contents = bin2hex(random_bytes(24)); try { if (! $disk->put($path, $contents, ['visibility' => 'private'])) { throw new RuntimeException('Business storage probe write failed.'); } if ($disk->get($path) !== $contents) { throw new RuntimeException('Business storage probe read failed.'); } $signer = app(QiniuPrivateUrlSigner::class); $unsignedResponse = Http::timeout(20)->head( $signer->unsignedUrlForPath($path) ); if (! in_array($unsignedResponse->status(), [401, 403], true)) { throw new RuntimeException('Business storage is not private.'); } $signedUrl = $signer->forPath($path); $response = Http::timeout(20)->head($signedUrl); if (! $response->successful()) { throw new RuntimeException('Business storage signed read failed.'); } } finally { $disk->delete($path); } $this->confirmProbeCleanup($disk, $path); } private function confirmProbeCleanup(Filesystem $disk, string $path, ?callable $sleep = null): void { $sleep ??= static function (int $microseconds): void { usleep($microseconds); }; for ($attempt = 0; $attempt < self::PROBE_CLEANUP_MAX_ATTEMPTS; $attempt++) { if (! $disk->exists($path)) { return; } if ($attempt < self::PROBE_CLEANUP_MAX_ATTEMPTS - 1) { $sleep(self::PROBE_CLEANUP_RETRY_DELAY_MICROSECONDS); } } throw new RuntimeException('Business storage probe cleanup failed.'); } private function syncRetainedLocalFiles(): array { $source = Storage::disk('business_uploads_local'); $target = Storage::disk('business_uploads_qiniu'); $copied = 0; $skipped = 0; foreach ($source->allFiles() as $path) { $sourceSize = $source->size($path); if ($target->exists($path) && $target->size($path) === $sourceSize) { $skipped++; continue; } $stream = $source->readStream($path); if (! is_resource($stream)) { throw new RuntimeException('Unable to read retained local business file.'); } try { if (! $target->put($path, $stream, ['visibility' => 'private'])) { throw new RuntimeException('Unable to copy retained local business file.'); } } finally { fclose($stream); } if (! $target->exists($path) || $target->size($path) !== $sourceSize) { throw new RuntimeException('Copied business file verification failed.'); } $copied++; } return [$copied, $skipped]; } }