loadMissing('company'); $images = $pressRelease->images()->whereNotNull('rules_version')->publiclyVisible()->get(); foreach ($images as $image) { try { $this->capture($image, $pressRelease); } catch (Throwable $exception) { report($exception); } } } /** * @throws RuntimeException Eine Datei konnte nicht geschrieben werden; * der Snapshot-Pfad bleibt dann unverändert. */ public function capture(PressReleaseImage $image, ?PressRelease $pressRelease = null): void { $pressRelease ??= $image->pressRelease; if ($pressRelease === null || $image->isLegacyRules() || $image->original_sha256 === null || ! $image->isPubliclyDeliverable()) { return; } // Nur, was wirklich öffentlich ist oder war: Eine nie veröffentlichte // Datei an einer nicht veröffentlichten Meldung bekommt keinen Snapshot. if (! $image->wasEverPublished() && $pressRelease->status !== PressReleaseStatus::Published) { return; } $sourceDisk = Storage::disk($image->disk); $coverPath = $image->pathForVariant('cover') ?? $image->path; if (! $sourceDisk->exists($coverPath)) { return; } $bytes = (string) $sourceDisk->get($coverPath); $sha = hash('sha256', $bytes); $ogPath = $image->pathForVariant('og'); $ogBytes = $ogPath !== null && $sourceDisk->exists($ogPath) ? (string) $sourceDisk->get($ogPath) : null; $credit = ImageCredit::for($image, $pressRelease->company); $metadata = [ 'press_release_id' => $pressRelease->getKey(), 'image_id' => $image->getKey(), 'variant' => 'cover', 'variant_sha256' => $sha, 'og_sha256' => $ogBytes !== null ? hash('sha256', $ogBytes) : null, 'original_sha256' => $image->original_sha256, 'credit_line' => $credit->line(), 'credit' => $credit->credit, 'ai_declaration' => $image->ai_declaration?->value, 'ai_declaration_source' => $image->ai_declaration_source?->value, 'responsible_line' => $credit->responsibleLine, 'confirmation_key' => $credit->confirmation?->legal_text_key, 'confirmation_at' => $credit->confirmation?->confirmed_at?->toIso8601String(), ]; $metadataSha = hash('sha256', (string) json_encode($metadata)); $extension = pathinfo($coverPath, PATHINFO_EXTENSION) ?: 'jpg'; $base = sprintf('%s-%s-%s', $this->prefix($image, $pressRelease), substr($sha, 0, 12), substr($metadataSha, 0, 12)); $target = $base.'.'.$extension; $targetDisk = $this->disk($image); // Unverändert seit dem letzten Snapshot (Bytes und Angaben): nichts zu tun. if ($image->published_snapshot_path === $target && $targetDisk->exists($target)) { if (! $image->wasEverPublished()) { $image->forceFill(['published_ever_at' => now()])->save(); } return; } $ogTarget = $ogBytes !== null ? $base.'-og.'.(pathinfo((string) $ogPath, PATHINFO_EXTENSION) ?: 'jpg') : null; $files = [$target => $bytes]; if ($ogTarget !== null) { $files[$ogTarget] = $ogBytes; } $files[$base.'.json'] = (string) json_encode([ 'captured_at' => now()->toIso8601String(), 'metadata_sha256' => $metadataSha, 'og_path' => $ogTarget, ] + $metadata, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $written = []; foreach ($files as $path => $contents) { if (! $targetDisk->put($path, $contents)) { // Kein Phantom-Snapshot (N09): Teilstand entfernen, Pfad nicht setzen. foreach ($written as $partial) { $targetDisk->delete($partial); } throw new RuntimeException(sprintf('Veröffentlichungssnapshot für Bild #%d konnte nicht geschrieben werden (%s).', $image->getKey(), $path)); } $written[] = $path; } $image->forceFill([ 'published_snapshot_path' => $target, 'published_snapshot_sha256' => $sha, 'published_ever_at' => $image->published_ever_at ?? now(), ])->save(); } /** * Alle Snapshot-Dateien eines Bildes (jede Fassung: Cover, OG, JSON), * auch die aus der Zeit vor der Versionierung. * * @return list */ public static function versionPaths(PressReleaseImage $image): array { $directory = sprintf('%s/%d', config('images.published_snapshot_directory'), $image->press_release_id); $prefix = $image->getKey().'-'; $disk = Storage::disk($image->original_disk ?? (string) config('images.originals_disk')); $paths = array_values(array_filter( $disk->files($directory), fn (string $path): bool => str_starts_with(basename($path), $prefix), )); // Der aktuell verknüpfte Pfad gehört immer dazu (auch bei // abweichender Benennung, z. B. aus Tests oder Altbestand). foreach (array_filter([$image->published_snapshot_path, self::sidecarPath($image->published_snapshot_path)]) as $current) { if (! in_array($current, $paths, true) && $disk->exists($current)) { $paths[] = $current; } } sort($paths); return $paths; } public static function sidecarPath(?string $snapshotPath): ?string { if ($snapshotPath === null) { return null; } $sidecar = preg_replace('/\.[a-z0-9]+$/i', '.json', $snapshotPath); return is_string($sidecar) ? $sidecar : null; } private function prefix(PressReleaseImage $image, PressRelease $pressRelease): string { return sprintf('%s/%d/%d', config('images.published_snapshot_directory'), $pressRelease->getKey(), $image->getKey()); } private function disk(PressReleaseImage $image): Filesystem { return Storage::disk($image->original_disk ?? (string) config('images.originals_disk')); } }