diff --git a/packages/actions/docs/07-prebuilt-actions/09-export.md b/packages/actions/docs/07-prebuilt-actions/09-export.md index 17085b95f4c..3f3cce77f84 100644 --- a/packages/actions/docs/07-prebuilt-actions/09-export.md +++ b/packages/actions/docs/07-prebuilt-actions/09-export.md @@ -691,3 +691,8 @@ public function view(User $user, Export $export): bool return $export->user()->is($user); } ``` + +## Streamed downloads + +Some hosting environments do not support streamed downloads. If you encounter issues with downloading files, you can disable streamed downloads by setting the `filament.supports_stream_downloads` config variable to `false`. +It is possible to define the location of the temporary file used for generating downloads by setting the `filament.temp_directory` config variable to the desired path. diff --git a/packages/actions/src/Exports/Downloaders/CsvDownloader.php b/packages/actions/src/Exports/Downloaders/CsvDownloader.php index 50ea98518ae..e7e2a9edf8e 100644 --- a/packages/actions/src/Exports/Downloaders/CsvDownloader.php +++ b/packages/actions/src/Exports/Downloaders/CsvDownloader.php @@ -17,26 +17,64 @@ public function __invoke(Export $export): StreamedResponse abort(404); } - return response()->streamDownload(function () use ($disk, $directory) { - echo $disk->get($directory . DIRECTORY_SEPARATOR . 'headers.csv'); + $fileName = "{$export->file_name}.csv"; + $filePath = $directory . DIRECTORY_SEPARATOR . $fileName; + + if ($disk->exists($filePath)) { + return $disk->download($filePath); + } + + if (! config('filament.supports_stream_downloads', true)) { + return $this->handleNonStreamedDownload($disk, $directory, $fileName, $filePath); + } + + return $this->handleStreamedDownload($disk, $directory, $fileName); + } - flush(); + private function handleNonStreamedDownload($disk, $directory, $fileName, $filePath): StreamedResponse + { + $tempPath = $this->getTempFilePath($fileName); - foreach ($disk->files($directory) as $file) { - if (str($file)->endsWith('headers.csv')) { - continue; - } + $this->writeCsvContent($disk, $directory, function ($content) use ($tempPath) { + file_put_contents($tempPath, $content, FILE_APPEND); + }); - if (! str($file)->endsWith('.csv')) { - continue; - } + $disk->put($filePath, file_get_contents($tempPath)); + unlink($tempPath); - echo $disk->get($file); + return $disk->download($filePath); + } + private function handleStreamedDownload($disk, $directory, $fileName): StreamedResponse + { + return response()->streamDownload(function () use ($disk, $directory) { + $this->writeCsvContent($disk, $directory, function ($content) { + echo $content; flush(); - } - }, "{$export->file_name}.csv", [ + }); + }, $fileName, [ 'Content-Type' => 'text/csv', ]); } + + private function writeCsvContent($disk, $directory, callable $outputCallback): void + { + $outputCallback($disk->get($directory . DIRECTORY_SEPARATOR . 'headers.csv')); + + foreach ($disk->files($directory) as $file) { + if ($this->shouldProcessFile($file)) { + $outputCallback($disk->get($file)); + } + } + } + + private function shouldProcessFile($file): bool + { + return ! str($file)->endsWith('headers.csv') && str($file)->endsWith('.csv'); + } + + private function getTempFilePath($fileName): string + { + return config('filament.temp_directory', storage_path('/tmp')) . DIRECTORY_SEPARATOR . $fileName; + } } diff --git a/packages/actions/src/Exports/Downloaders/XlsxDownloader.php b/packages/actions/src/Exports/Downloaders/XlsxDownloader.php index 3096d9b5eae..69c30e4fb67 100644 --- a/packages/actions/src/Exports/Downloaders/XlsxDownloader.php +++ b/packages/actions/src/Exports/Downloaders/XlsxDownloader.php @@ -22,45 +22,78 @@ public function __invoke(Export $export): StreamedResponse } $fileName = $export->file_name . '.xlsx'; + $filePath = $directory . DIRECTORY_SEPARATOR . $fileName; - if ($disk->exists($filePath = $directory . DIRECTORY_SEPARATOR . $fileName)) { + if ($disk->exists($filePath)) { return $disk->download($filePath); } $writer = app(Writer::class); - $csvDelimiter = $export->exporter::getCsvDelimiter(); - $writeRowsFromFile = function (string $file) use ($csvDelimiter, $disk, $writer) { - $csvReader = CsvReader::createFromStream($disk->readStream($file)); - $csvReader->setDelimiter($csvDelimiter); - $csvResults = Statement::create()->process($csvReader); + if (! config('filament.supports_stream_downloads', true)) { + return $this->handleNonStreamedDownload($disk, $directory, $fileName, $filePath, $writer, $csvDelimiter); + } - foreach ($csvResults->getRecords() as $row) { - $writer->addRow(Row::fromValues($row)); - } - }; + return $this->handleStreamedDownload($disk, $directory, $fileName, $writer, $csvDelimiter); + } - return response()->streamDownload(function () use ($disk, $directory, $fileName, $writer, $writeRowsFromFile) { - $writer->openToBrowser($fileName); + private function handleNonStreamedDownload($disk, $directory, $fileName, $filePath, $writer, $csvDelimiter): StreamedResponse + { + $tempPath = $this->getTempFilePath($fileName); + $writer->openToFile($tempPath); - $writeRowsFromFile($directory . DIRECTORY_SEPARATOR . 'headers.csv'); + $this->writeRowsToWriter($writer, $disk, $directory, $csvDelimiter); - foreach ($disk->files($directory) as $file) { - if (str($file)->endsWith('headers.csv')) { - continue; - } + $writer->close(); - if (! str($file)->endsWith('.csv')) { - continue; - } + $disk->put($filePath, file_get_contents($tempPath)); + unlink($tempPath); - $writeRowsFromFile($file); - } + return $disk->download($filePath); + } + private function handleStreamedDownload($disk, $directory, $fileName, $writer, $csvDelimiter): StreamedResponse + { + return response()->streamDownload(function () use ($disk, $directory, $fileName, $writer, $csvDelimiter) { + $writer->openToBrowser($fileName); + $this->writeRowsToWriter($writer, $disk, $directory, $csvDelimiter); $writer->close(); }, $fileName, [ 'Content-Type' => 'application/vnd.ms-excel', ]); } + + private function writeRowsToWriter($writer, $disk, $directory, $csvDelimiter): void + { + $this->writeRowsFromFile($writer, $disk, $directory . DIRECTORY_SEPARATOR . 'headers.csv', $csvDelimiter); + + foreach ($disk->files($directory) as $file) { + if (! $this->shouldProcessFile($file)) { + continue; + } + $this->writeRowsFromFile($writer, $disk, $file, $csvDelimiter); + } + } + + private function writeRowsFromFile($writer, $disk, $file, $csvDelimiter): void + { + $csvReader = CsvReader::createFromStream($disk->readStream($file)); + $csvReader->setDelimiter($csvDelimiter); + $csvResults = Statement::create()->process($csvReader); + + foreach ($csvResults->getRecords() as $row) { + $writer->addRow(Row::fromValues($row)); + } + } + + private function shouldProcessFile($file): bool + { + return ! str($file)->endsWith('headers.csv') && str($file)->endsWith('.csv'); + } + + private function getTempFilePath($fileName): string + { + return config('filament.temp_directory', storage_path('/tmp')) . DIRECTORY_SEPARATOR . $fileName; + } } diff --git a/packages/support/config/filament.php b/packages/support/config/filament.php index 65b4bf0e828..e3fdc264269 100644 --- a/packages/support/config/filament.php +++ b/packages/support/config/filament.php @@ -86,4 +86,16 @@ 'livewire_loading_delay' => 'default', + /* + |-------------------------------------------------------------------------- + | Supports Stream Downloads + |-------------------------------------------------------------------------- + | + | Some hosting environments (Like Vapor/Bref) don't support streaming. + | + | Setting this to 'false' makes sure a file is first written to disk before being downloaded + */ + + 'supports_stream_downloads' => true, + 'temp_directory' => storage_path('tmp'), ];