diff --git a/src/Controller/Api/Stations/Files/BulkDownloadAction.php b/src/Controller/Api/Stations/Files/BulkDownloadAction.php index 570f38673..3404932ee 100644 --- a/src/Controller/Api/Stations/Files/BulkDownloadAction.php +++ b/src/Controller/Api/Stations/Files/BulkDownloadAction.php @@ -8,9 +8,8 @@ use App\Entity\Repository\CustomFieldRepository; use App\Entity\Repository\StationPlaylistRepository; use App\Http\Response; use App\Http\ServerRequest; -use App\Utilities\File; +use App\Service\CsvWriterTempFile; use Doctrine\ORM\EntityManagerInterface; -use League\Csv\Writer; use Psr\Http\Message\ResponseInterface; class BulkDownloadAction @@ -46,9 +45,9 @@ class BulkDownloadAction )->setParameter('storageLocation', $station->getMediaStorageLocation()); $filename = $station->getShortName() . '_all_media.csv'; - $tempFile = File::generateTempPath($filename); - $csv = Writer::createFromPath($tempFile, 'w+'); - $csv->setOutputBOM($csv::BOM_UTF8); + + $tempFile = new CsvWriterTempFile(); + $csv = $tempFile->getWriter(); /* * NOTE: These field names should correspond with DB property names when converted into short_names. @@ -116,10 +115,6 @@ class BulkDownloadAction $csv->insertOne($bodyRow); } - try { - return $response->withFileDownload($tempFile, $filename, 'text/csv'); - } finally { - @unlink($filename); - } + return $response->withFileDownload($tempFile->getTempPath(), $filename, 'text/csv'); } } diff --git a/src/Controller/Api/Stations/HistoryController.php b/src/Controller/Api/Stations/HistoryController.php index dadd1c395..743132beb 100644 --- a/src/Controller/Api/Stations/HistoryController.php +++ b/src/Controller/Api/Stations/HistoryController.php @@ -10,12 +10,11 @@ use App\Environment; use App\Http\Response; use App\Http\ServerRequest; use App\OpenApi; -use App\Utilities\File; +use App\Service\CsvWriterTempFile; use Azura\DoctrineBatchUtils\ReadOnlyBatchIteratorAggregate; use Carbon\CarbonImmutable; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query; -use League\Csv\Writer; use OpenApi\Attributes as OA; use Psr\Http\Message\ResponseInterface; @@ -150,22 +149,19 @@ class HistoryController Query $query, string $filename ): ResponseInterface { - $tempFile = File::generateTempPath($filename); + $tempFile = new CsvWriterTempFile(); + $csv = $tempFile->getWriter(); - $csv = Writer::createFromPath($tempFile, 'w+'); - - $csv->insertOne( - [ - 'Date', - 'Time', - 'Listeners', - 'Delta', - 'Track', - 'Artist', - 'Playlist', - 'Streamer', - ] - ); + $csv->insertOne([ + 'Date', + 'Time', + 'Listeners', + 'Delta', + 'Track', + 'Artist', + 'Playlist', + 'Streamer', + ]); /** @var Entity\SongHistory $sh */ foreach (ReadOnlyBatchIteratorAggregate::fromQuery($query, 100) as $sh) { @@ -196,6 +192,6 @@ class HistoryController ]); } - return $response->withFileDownload($tempFile, $filename, 'text/csv'); + return $response->withFileDownload($tempFile->getTempPath(), $filename, 'text/csv'); } } diff --git a/src/Controller/Api/Stations/ListenersAction.php b/src/Controller/Api/Stations/ListenersAction.php index 15d70bd38..a47d96bf1 100644 --- a/src/Controller/Api/Stations/ListenersAction.php +++ b/src/Controller/Api/Stations/ListenersAction.php @@ -9,13 +9,12 @@ use App\Environment; use App\Http\Response; use App\Http\ServerRequest; use App\OpenApi; +use App\Service\CsvWriterTempFile; use App\Service\DeviceDetector; use App\Service\IpGeolocation; -use App\Utilities\File; use Carbon\CarbonImmutable; use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\EntityManagerInterface; -use League\Csv\Writer; use OpenApi\Attributes as OA; use Psr\Http\Message\ResponseInterface; @@ -207,9 +206,8 @@ class ListenersAction array $listeners, string $filename ): ResponseInterface { - $tempFile = File::generateTempPath($filename); - - $csv = Writer::createFromPath($tempFile, 'w+'); + $tempFile = new CsvWriterTempFile(); + $csv = $tempFile->getWriter(); $tz = $station->getTimezoneObject(); @@ -266,6 +264,6 @@ class ListenersAction $csv->insertOne($exportRow); } - return $response->withFileDownload($tempFile, $filename, 'text/csv'); + return $response->withFileDownload($tempFile->getTempPath(), $filename, 'text/csv'); } } diff --git a/src/Controller/Api/Stations/Reports/PerformanceAction.php b/src/Controller/Api/Stations/Reports/PerformanceAction.php index 4dddc33f2..e43969d0a 100644 --- a/src/Controller/Api/Stations/Reports/PerformanceAction.php +++ b/src/Controller/Api/Stations/Reports/PerformanceAction.php @@ -7,9 +7,8 @@ namespace App\Controller\Api\Stations\Reports; use App\Http\Response; use App\Http\ServerRequest; use App\Paginator; +use App\Service\CsvWriterTempFile; use App\Sync\Task\RunAutomatedAssignmentTask; -use App\Utilities\File; -use League\Csv\Writer; use Psr\Http\Message\ResponseInterface; class PerformanceAction @@ -59,9 +58,8 @@ class PerformanceAction array $reportData, string $filename ): ResponseInterface { - $tempFile = File::generateTempPath($filename); - - $csv = Writer::createFromPath($tempFile, 'w+'); + $tempFile = new CsvWriterTempFile(); + $csv = $tempFile->getWriter(); $csv->insertOne( [ @@ -95,6 +93,6 @@ class PerformanceAction ]); } - return $response->withFileDownload($tempFile, $filename, 'text/csv'); + return $response->withFileDownload($tempFile->getTempPath(), $filename, 'text/csv'); } } diff --git a/src/Http/Response.php b/src/Http/Response.php index 8ee36bc40..c379c7ddf 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -9,7 +9,6 @@ use Azura\Files\ExtendedFilesystemInterface; use InvalidArgumentException; use League\Flysystem\FileAttributes; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\StreamInterface; final class Response extends \Slim\Http\Response { @@ -80,36 +79,6 @@ final class Response extends \Slim\Http\Response return parent::withJson($data, $status, $options, $depth); } - /** - * Stream the contents of a file directly through to the response. - * - * @param string $file_path - * @param null $file_name - * - * @return static - */ - public function renderFile(string $file_path, $file_name = null): Response - { - set_time_limit(600); - - if (null === $file_name) { - $file_name = basename($file_path); - } - - $stream = $this->streamFactory->createStreamFromFile($file_path); - - $response = $this->response - ->withHeader('Pragma', 'public') - ->withHeader('Expires', '0') - ->withHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') - ->withHeader('Content-Type', mime_content_type($file_path) ?: '') - ->withHeader('Content-Length', (string)filesize($file_path)) - ->withHeader('Content-Disposition', 'attachment; filename=' . $file_name) - ->withBody($stream); - - return new Response($response, $this->streamFactory); - } - /** * Write a string of file data to the response as if it is a file for download. * @@ -136,37 +105,6 @@ final class Response extends \Slim\Http\Response return new Response($response, $this->streamFactory); } - /** - * Write a stream to the response as if it is a file for download. - * - * @param StreamInterface $fileStream - * @param string $contentType - * @param string|null $fileName - * - * @return static - */ - public function renderStreamAsFile( - StreamInterface $fileStream, - string $contentType, - ?string $fileName = null - ): Response { - set_time_limit(600); - - $response = $this->response - ->withHeader('Pragma', 'public') - ->withHeader('Expires', '0') - ->withHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') - ->withHeader('Content-Type', $contentType); - - if ($fileName !== null) { - $response = $response->withHeader('Content-Disposition', 'attachment; filename=' . $fileName); - } - - $response = $response->withBody($fileStream); - - return new Response($response, $this->streamFactory); - } - public function streamFilesystemFile( ExtendedFilesystemInterface $filesystem, string $path, diff --git a/src/Service/CsvWriterTempFile.php b/src/Service/CsvWriterTempFile.php new file mode 100644 index 000000000..b943228d2 --- /dev/null +++ b/src/Service/CsvWriterTempFile.php @@ -0,0 +1,41 @@ +tempPath = File::generateTempPath('temp_file.csv'); + + // Append UTF-8 BOM to temp file. + file_put_contents($this->tempPath, AbstractCsv::BOM_UTF8); + + $this->writer = Writer::createFromPath($this->tempPath, 'a+'); + } + + public function getWriter(): Writer + { + return $this->writer; + } + + public function getTempPath(): string + { + return $this->tempPath; + } + + public function __destruct() + { + @unlink($this->tempPath); + } +} diff --git a/tests/Functional/Api_Stations_ReportsCest.php b/tests/Functional/Api_Stations_ReportsCest.php index 1d8163791..9a2e0a998 100644 --- a/tests/Functional/Api_Stations_ReportsCest.php +++ b/tests/Functional/Api_Stations_ReportsCest.php @@ -3,6 +3,7 @@ namespace Functional; use Codeception\Util\Shared\Asserts; +use League\Csv\Reader; class Api_Stations_ReportsCest extends CestAbstract { @@ -153,18 +154,21 @@ class Api_Stations_ReportsCest extends CestAbstract $response = $I->grabResponse(); - $responseCsv = str_getcsv($response); + $csvReader = Reader::createFromString($response); + $csvReader->setHeaderOffset(0); - $this->assertIsArray($responseCsv); + $csvHeaders = $csvReader->getHeader(); + + $this->assertIsArray($csvHeaders); $this->assertTrue( - count($responseCsv) > 0, + count($csvHeaders) > 0, 'CSV is not empty' ); foreach ($headerFields as $csvHeaderField) { $this->assertContains( $csvHeaderField, - $responseCsv, + $csvHeaders, 'CSV has header field' ); }