Fix CSV export to include UTF-8 BOM.
This commit is contained in:
parent
910aa3b501
commit
41ac7bd810
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,12 +149,10 @@ class HistoryController
|
|||
Query $query,
|
||||
string $filename
|
||||
): ResponseInterface {
|
||||
$tempFile = File::generateTempPath($filename);
|
||||
$tempFile = new CsvWriterTempFile();
|
||||
$csv = $tempFile->getWriter();
|
||||
|
||||
$csv = Writer::createFromPath($tempFile, 'w+');
|
||||
|
||||
$csv->insertOne(
|
||||
[
|
||||
$csv->insertOne([
|
||||
'Date',
|
||||
'Time',
|
||||
'Listeners',
|
||||
|
@ -164,8 +161,7 @@ class HistoryController
|
|||
'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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Utilities\File;
|
||||
use League\Csv\AbstractCsv;
|
||||
use League\Csv\Writer;
|
||||
|
||||
class CsvWriterTempFile
|
||||
{
|
||||
protected string $tempPath;
|
||||
|
||||
protected Writer $writer;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue