utc()->format('YmdHis') . '-' . Str::lower((string) Str::ulid()); } public function releaseExists(string $releaseId): bool { return Storage::disk($this->disk())->exists($this->manifestPath($releaseId)); } public function putDocument(string $releaseId, string $documentName, string $content): void { Storage::disk($this->disk())->put($this->releaseDocumentPath($releaseId, $documentName), $content); } public function getDocument(string $releaseId, string $documentName): ?string { $disk = Storage::disk($this->disk()); $path = $this->releaseDocumentPath($releaseId, $documentName); if (! $disk->exists($path)) { return null; } $content = $disk->get($path); return is_string($content) && $content !== '' ? $content : null; } public function writeManifest(string $releaseId, array $manifest): void { Storage::disk($this->disk())->put($this->manifestPath($releaseId), json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } public function readManifest(string $releaseId): ?array { $disk = Storage::disk($this->disk()); $path = $this->manifestPath($releaseId); if (! $disk->exists($path)) { return null; } $decoded = json_decode((string) $disk->get($path), true); return is_array($decoded) ? $decoded : null; } public function writeBuildReport(string $releaseId, array $report): void { Storage::disk($this->disk())->put($this->buildReportPath($releaseId), json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } public function activate(string $releaseId): void { $payload = [ 'release_id' => $releaseId, 'activated_at' => now()->toAtomString(), ]; $this->atomicJsonWrite($this->activePointerPath(), $payload); } public function activeReleaseId(): ?string { $disk = Storage::disk($this->disk()); $path = $this->activePointerPath(); if (! $disk->exists($path)) { return null; } $decoded = json_decode((string) $disk->get($path), true); return is_array($decoded) && is_string($decoded['release_id'] ?? null) ? $decoded['release_id'] : null; } public function activeManifest(): ?array { $releaseId = $this->activeReleaseId(); return $releaseId !== null ? $this->readManifest($releaseId) : null; } /** * @return list> */ public function listReleases(): array { $releases = []; foreach (Storage::disk($this->disk())->allDirectories($this->releasesRootPath()) as $directory) { $releaseId = basename($directory); $manifest = $this->readManifest($releaseId); if ($manifest !== null) { $releases[] = $manifest; } } usort($releases, static fn (array $left, array $right): int => strcmp((string) ($right['release_id'] ?? ''), (string) ($left['release_id'] ?? ''))); return $releases; } public function deleteRelease(string $releaseId): void { Storage::disk($this->disk())->deleteDirectory($this->releaseRootPath($releaseId)); } public function documentRelativePath(string $documentName): string { return $documentName === SitemapCacheService::INDEX_DOCUMENT ? 'sitemap.xml' : 'sitemaps/' . $documentName . '.xml'; } public function releaseDocumentPath(string $releaseId, string $documentName): string { return $this->releaseRootPath($releaseId) . '/' . $this->documentRelativePath($documentName); } public function manifestPath(string $releaseId): string { return $this->releaseRootPath($releaseId) . '/manifest.json'; } public function buildReportPath(string $releaseId): string { return $this->releaseRootPath($releaseId) . '/build-report.json'; } private function releaseRootPath(string $releaseId): string { return $this->releasesRootPath() . '/' . $releaseId; } private function releasesRootPath(): string { return trim((string) config('sitemaps.releases.path', 'sitemaps'), '/') . '/releases'; } private function activePointerPath(): string { return trim((string) config('sitemaps.releases.path', 'sitemaps'), '/') . '/active.json'; } private function disk(): string { return (string) config('sitemaps.releases.disk', 'local'); } private function atomicJsonWrite(string $relativePath, array $payload): void { $disk = Storage::disk($this->disk()); $json = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); try { $absolutePath = $disk->path($relativePath); $directory = dirname($absolutePath); if (! is_dir($directory)) { mkdir($directory, 0755, true); } $temporaryPath = $absolutePath . '.tmp'; file_put_contents($temporaryPath, $json, LOCK_EX); rename($temporaryPath, $absolutePath); } catch (\Throwable) { $disk->put($relativePath, $json); } } }