From 044b94b080f9e1ae3f136d65385445219a7f0ebb Mon Sep 17 00:00:00 2001 From: Thorsten Bus Date: Fri, 6 Mar 2026 10:25:21 +0100 Subject: [PATCH] refactor(export): use in-memory content for zip bundle and playlist entries Replace file-path-based zip entries with in-memory content via file_get_contents. Rename .pro entry to 'data' (raw protobuf), add addStoredEntry() helper with CM_STORE compression, and remove temp directory management. --- app/Services/PlaylistExportService.php | 12 ++--- app/Services/ProBundleExportService.php | 60 +++++++++---------------- tests/Feature/ProBundleExportTest.php | 32 ++++++++----- 3 files changed, 50 insertions(+), 54 deletions(-) diff --git a/app/Services/PlaylistExportService.php b/app/Services/PlaylistExportService.php index 6eff673..0160ff1 100644 --- a/app/Services/PlaylistExportService.php +++ b/app/Services/PlaylistExportService.php @@ -24,7 +24,7 @@ public function generatePlaylist(Service $service): array $skippedUnmatched = $service->serviceSongs()->whereNull('song_id')->count(); $skippedEmpty = 0; - $exportService = new ProExportService; + $exportService = new ProExportService(); $tempDir = sys_get_temp_dir().'/playlist-export-'.uniqid(); mkdir($tempDir, 0755, true); @@ -54,7 +54,7 @@ public function generatePlaylist(Service $service): array $destPath = $tempDir.'/'.$proFilename; rename($proPath, $destPath); - $embeddedFiles[$proFilename] = $destPath; + $embeddedFiles[$proFilename] = file_get_contents($destPath); $playlistItems[] = [ 'type' => 'presentation', @@ -148,7 +148,7 @@ private function addSlidePresentation( $destPath = $tempDir.'/'.$imageFilename; copy($storedPath, $destPath); - $imageFiles[$imageFilename] = $destPath; + $imageFiles[$imageFilename] = file_get_contents($destPath); $slideDataList[] = [ 'text' => '', @@ -182,11 +182,11 @@ private function addSlidePresentation( ProFileGenerator::generateAndWrite($proPath, $label, $groups, $arrangements); - foreach ($imageFiles as $filename => $path) { - $embeddedFiles[$filename] = $path; + foreach ($imageFiles as $filename => $contents) { + $embeddedFiles[$filename] = $contents; } - $embeddedFiles[$proFilename] = $proPath; + $embeddedFiles[$proFilename] = file_get_contents($proPath); $playlistItems[] = [ 'type' => 'presentation', diff --git a/app/Services/ProBundleExportService.php b/app/Services/ProBundleExportService.php index 336ad17..4e73422 100644 --- a/app/Services/ProBundleExportService.php +++ b/app/Services/ProBundleExportService.php @@ -3,9 +3,10 @@ namespace App\Services; use App\Models\Service; -use InvalidArgumentException; use Illuminate\Support\Facades\Storage; +use InvalidArgumentException; use ProPresenter\Parser\ProFileGenerator; +use RuntimeException; use ZipArchive; class ProBundleExportService @@ -23,12 +24,9 @@ public function generateBundle(Service $service, string $blockType): string ->orderBy('sort_order') ->get(); - $tempDir = sys_get_temp_dir().'/probundle-export-'.uniqid(); - mkdir($tempDir, 0755, true); - $groupName = ucfirst($blockType); $slideData = []; - $copiedImagePaths = []; + $mediaFiles = []; foreach ($slides as $slide) { $sourcePath = Storage::disk('public')->path('slides/'.$slide->stored_filename); @@ -37,9 +35,12 @@ public function generateBundle(Service $service, string $blockType): string } $imageFilename = basename($slide->stored_filename); - $tempImagePath = $tempDir.'/'.$imageFilename; - copy($sourcePath, $tempImagePath); - $copiedImagePaths[] = $tempImagePath; + $imageContent = file_get_contents($sourcePath); + if ($imageContent === false) { + continue; + } + + $mediaFiles[$imageFilename] = $imageContent; $slideData[] = [ 'text' => $slide->original_filename ?? '', @@ -63,55 +64,38 @@ public function generateBundle(Service $service, string $blockType): string ], ]; - $proFilePath = $tempDir.'/'.$blockType.'.pro'; - ProFileGenerator::generateAndWrite($proFilePath, $groupName, $groups, $arrangements); + $song = ProFileGenerator::generate($groupName, $groups, $arrangements); + $protoBytes = $song->getPresentation()->serializeToString(); $bundlePath = sys_get_temp_dir().'/'.uniqid($blockType.'-').'.probundle'; $zip = new ZipArchive(); $openResult = $zip->open($bundlePath, ZipArchive::CREATE | ZipArchive::OVERWRITE); if ($openResult !== true) { - $this->deleteDirectory($tempDir); throw new InvalidArgumentException('Konnte .probundle nicht erstellen.'); } - $zip->addFile($proFilePath, basename($proFilePath)); + $this->addStoredEntry($zip, 'data', $protoBytes); - foreach ($copiedImagePaths as $imagePath) { - $zip->addFile($imagePath, basename($imagePath)); + foreach ($mediaFiles as $filename => $contents) { + $this->addStoredEntry($zip, $filename, $contents); } - $zip->close(); - - $this->deleteDirectory($tempDir); + if (! $zip->close()) { + throw new RuntimeException('Fehler beim Finalisieren der .probundle Datei.'); + } return $bundlePath; } - private function deleteDirectory(string $dir): void + private function addStoredEntry(ZipArchive $zip, string $entryName, string $contents): void { - if (! is_dir($dir)) { - return; + if (! $zip->addFromString($entryName, $contents)) { + throw new RuntimeException(sprintf('Fehler beim Hinzufügen von %s zur .probundle Datei.', $entryName)); } - $entries = scandir($dir); - if ($entries === false) { - return; + if (! $zip->setCompressionName($entryName, ZipArchive::CM_STORE)) { + throw new RuntimeException(sprintf('Fehler beim Setzen der Kompression für %s.', $entryName)); } - - foreach ($entries as $entry) { - if ($entry === '.' || $entry === '..') { - continue; - } - - $path = $dir.'/'.$entry; - if (is_dir($path)) { - $this->deleteDirectory($path); - } else { - @unlink($path); - } - } - - @rmdir($dir); } } diff --git a/tests/Feature/ProBundleExportTest.php b/tests/Feature/ProBundleExportTest.php index eb09f8b..96c2b4e 100644 --- a/tests/Feature/ProBundleExportTest.php +++ b/tests/Feature/ProBundleExportTest.php @@ -5,8 +5,8 @@ use App\Models\Service; use App\Models\Slide; use App\Models\User; -use Illuminate\Foundation\Testing\Concerns\MakesHttpRequests; use Illuminate\Foundation\Testing\Concerns\InteractsWithAuthentication; +use Illuminate\Foundation\Testing\Concerns\MakesHttpRequests; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Storage; use Symfony\Component\HttpFoundation\BinaryFileResponse; @@ -15,11 +15,11 @@ final class ProBundleExportTest extends TestCase { - use RefreshDatabase; - use MakesHttpRequests; use InteractsWithAuthentication; + use MakesHttpRequests; + use RefreshDatabase; - public function test_probundle_enthaelt_pro_datei_und_bilder(): void + public function test_probundle_enthaelt_data_und_bilder(): void { Storage::fake('public'); @@ -73,14 +73,26 @@ public function test_probundle_enthaelt_pro_datei_und_bilder(): void $names[] = $name; } } - $zip->close(); - @unlink($copiedPath); - - $this->assertContains('information.pro', $names); + $this->assertContains('data', $names, '.probundle muss einen data-Eintrag (Protobuf) enthalten'); $this->assertContains('info-1.jpg', $names); $this->assertContains('info-2.jpg', $names); $this->assertContains('info-3.jpg', $names); + + // Verify media files contain actual content, not file paths + foreach ($filenames as $index => $filename) { + $content = $zip->getFromName($filename); + $this->assertSame('fake-image-content-'.$index, $content, "Bildinhalt von {$filename} muss korrekt sein"); + } + + // Verify data entry is non-empty protobuf (not a file path) + $dataContent = $zip->getFromName('data'); + $this->assertNotFalse($dataContent, 'data-Eintrag muss existieren'); + $this->assertGreaterThan(0, strlen($dataContent), 'data-Eintrag darf nicht leer sein'); + $this->assertFalse(str_starts_with($dataContent, '/'), 'data-Eintrag darf kein Dateipfad sein'); + + $zip->close(); + @unlink($copiedPath); } public function test_ungueltiger_block_type_liefert_422(): void @@ -96,7 +108,7 @@ public function test_ungueltiger_block_type_liefert_422(): void $response->assertStatus(422); } - public function test_probundle_ohne_slides_enthaelt_nur_pro_datei(): void + public function test_probundle_ohne_slides_enthaelt_nur_data(): void { $user = User::factory()->create(); $service = Service::factory()->create(); @@ -121,7 +133,7 @@ public function test_probundle_ohne_slides_enthaelt_nur_pro_datei(): void $this->assertTrue($openResult === true); $this->assertSame(1, $zip->numFiles); - $this->assertSame('sermon.pro', $zip->getNameIndex(0)); + $this->assertSame('data', $zip->getNameIndex(0)); $zip->close(); @unlink($copiedPath);