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\Entity\Repository\StationPlaylistRepository;
|
||||||
use App\Http\Response;
|
use App\Http\Response;
|
||||||
use App\Http\ServerRequest;
|
use App\Http\ServerRequest;
|
||||||
use App\Utilities\File;
|
use App\Service\CsvWriterTempFile;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use League\Csv\Writer;
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
class BulkDownloadAction
|
class BulkDownloadAction
|
||||||
|
@ -46,9 +45,9 @@ class BulkDownloadAction
|
||||||
)->setParameter('storageLocation', $station->getMediaStorageLocation());
|
)->setParameter('storageLocation', $station->getMediaStorageLocation());
|
||||||
|
|
||||||
$filename = $station->getShortName() . '_all_media.csv';
|
$filename = $station->getShortName() . '_all_media.csv';
|
||||||
$tempFile = File::generateTempPath($filename);
|
|
||||||
$csv = Writer::createFromPath($tempFile, 'w+');
|
$tempFile = new CsvWriterTempFile();
|
||||||
$csv->setOutputBOM($csv::BOM_UTF8);
|
$csv = $tempFile->getWriter();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* NOTE: These field names should correspond with DB property names when converted into short_names.
|
* NOTE: These field names should correspond with DB property names when converted into short_names.
|
||||||
|
@ -116,10 +115,6 @@ class BulkDownloadAction
|
||||||
$csv->insertOne($bodyRow);
|
$csv->insertOne($bodyRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return $response->withFileDownload($tempFile->getTempPath(), $filename, 'text/csv');
|
||||||
return $response->withFileDownload($tempFile, $filename, 'text/csv');
|
|
||||||
} finally {
|
|
||||||
@unlink($filename);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,11 @@ use App\Environment;
|
||||||
use App\Http\Response;
|
use App\Http\Response;
|
||||||
use App\Http\ServerRequest;
|
use App\Http\ServerRequest;
|
||||||
use App\OpenApi;
|
use App\OpenApi;
|
||||||
use App\Utilities\File;
|
use App\Service\CsvWriterTempFile;
|
||||||
use Azura\DoctrineBatchUtils\ReadOnlyBatchIteratorAggregate;
|
use Azura\DoctrineBatchUtils\ReadOnlyBatchIteratorAggregate;
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\Query;
|
use Doctrine\ORM\Query;
|
||||||
use League\Csv\Writer;
|
|
||||||
use OpenApi\Attributes as OA;
|
use OpenApi\Attributes as OA;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
|
@ -150,22 +149,19 @@ class HistoryController
|
||||||
Query $query,
|
Query $query,
|
||||||
string $filename
|
string $filename
|
||||||
): ResponseInterface {
|
): ResponseInterface {
|
||||||
$tempFile = File::generateTempPath($filename);
|
$tempFile = new CsvWriterTempFile();
|
||||||
|
$csv = $tempFile->getWriter();
|
||||||
|
|
||||||
$csv = Writer::createFromPath($tempFile, 'w+');
|
$csv->insertOne([
|
||||||
|
'Date',
|
||||||
$csv->insertOne(
|
'Time',
|
||||||
[
|
'Listeners',
|
||||||
'Date',
|
'Delta',
|
||||||
'Time',
|
'Track',
|
||||||
'Listeners',
|
'Artist',
|
||||||
'Delta',
|
'Playlist',
|
||||||
'Track',
|
'Streamer',
|
||||||
'Artist',
|
]);
|
||||||
'Playlist',
|
|
||||||
'Streamer',
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
/** @var Entity\SongHistory $sh */
|
/** @var Entity\SongHistory $sh */
|
||||||
foreach (ReadOnlyBatchIteratorAggregate::fromQuery($query, 100) as $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\Response;
|
||||||
use App\Http\ServerRequest;
|
use App\Http\ServerRequest;
|
||||||
use App\OpenApi;
|
use App\OpenApi;
|
||||||
|
use App\Service\CsvWriterTempFile;
|
||||||
use App\Service\DeviceDetector;
|
use App\Service\DeviceDetector;
|
||||||
use App\Service\IpGeolocation;
|
use App\Service\IpGeolocation;
|
||||||
use App\Utilities\File;
|
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
use Doctrine\ORM\AbstractQuery;
|
use Doctrine\ORM\AbstractQuery;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use League\Csv\Writer;
|
|
||||||
use OpenApi\Attributes as OA;
|
use OpenApi\Attributes as OA;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
|
@ -207,9 +206,8 @@ class ListenersAction
|
||||||
array $listeners,
|
array $listeners,
|
||||||
string $filename
|
string $filename
|
||||||
): ResponseInterface {
|
): ResponseInterface {
|
||||||
$tempFile = File::generateTempPath($filename);
|
$tempFile = new CsvWriterTempFile();
|
||||||
|
$csv = $tempFile->getWriter();
|
||||||
$csv = Writer::createFromPath($tempFile, 'w+');
|
|
||||||
|
|
||||||
$tz = $station->getTimezoneObject();
|
$tz = $station->getTimezoneObject();
|
||||||
|
|
||||||
|
@ -266,6 +264,6 @@ class ListenersAction
|
||||||
$csv->insertOne($exportRow);
|
$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\Response;
|
||||||
use App\Http\ServerRequest;
|
use App\Http\ServerRequest;
|
||||||
use App\Paginator;
|
use App\Paginator;
|
||||||
|
use App\Service\CsvWriterTempFile;
|
||||||
use App\Sync\Task\RunAutomatedAssignmentTask;
|
use App\Sync\Task\RunAutomatedAssignmentTask;
|
||||||
use App\Utilities\File;
|
|
||||||
use League\Csv\Writer;
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
class PerformanceAction
|
class PerformanceAction
|
||||||
|
@ -59,9 +58,8 @@ class PerformanceAction
|
||||||
array $reportData,
|
array $reportData,
|
||||||
string $filename
|
string $filename
|
||||||
): ResponseInterface {
|
): ResponseInterface {
|
||||||
$tempFile = File::generateTempPath($filename);
|
$tempFile = new CsvWriterTempFile();
|
||||||
|
$csv = $tempFile->getWriter();
|
||||||
$csv = Writer::createFromPath($tempFile, 'w+');
|
|
||||||
|
|
||||||
$csv->insertOne(
|
$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 InvalidArgumentException;
|
||||||
use League\Flysystem\FileAttributes;
|
use League\Flysystem\FileAttributes;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\StreamInterface;
|
|
||||||
|
|
||||||
final class Response extends \Slim\Http\Response
|
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);
|
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.
|
* 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);
|
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(
|
public function streamFilesystemFile(
|
||||||
ExtendedFilesystemInterface $filesystem,
|
ExtendedFilesystemInterface $filesystem,
|
||||||
string $path,
|
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;
|
namespace Functional;
|
||||||
|
|
||||||
use Codeception\Util\Shared\Asserts;
|
use Codeception\Util\Shared\Asserts;
|
||||||
|
use League\Csv\Reader;
|
||||||
|
|
||||||
class Api_Stations_ReportsCest extends CestAbstract
|
class Api_Stations_ReportsCest extends CestAbstract
|
||||||
{
|
{
|
||||||
|
@ -153,18 +154,21 @@ class Api_Stations_ReportsCest extends CestAbstract
|
||||||
|
|
||||||
$response = $I->grabResponse();
|
$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(
|
$this->assertTrue(
|
||||||
count($responseCsv) > 0,
|
count($csvHeaders) > 0,
|
||||||
'CSV is not empty'
|
'CSV is not empty'
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach ($headerFields as $csvHeaderField) {
|
foreach ($headerFields as $csvHeaderField) {
|
||||||
$this->assertContains(
|
$this->assertContains(
|
||||||
$csvHeaderField,
|
$csvHeaderField,
|
||||||
$responseCsv,
|
$csvHeaders,
|
||||||
'CSV has header field'
|
'CSV has header field'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue