Merge pull request #601 from pierresh/master

feat: method to download files easily
pull/602/head
n0nag0n 6 months ago committed by GitHub
commit c834f4cf0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -62,9 +62,10 @@ use flight\net\Route;
* @method void jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0) * @method void jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0)
* Sends a JSONP response. * Sends a JSONP response.
* *
* # HTTP caching * # HTTP methods
* @method void etag(string $id, ('strong'|'weak') $type = 'strong') Handles ETag HTTP caching. * @method void etag(string $id, ('strong'|'weak') $type = 'strong') Handles ETag HTTP caching.
* @method void lastModified(int $time) Handles last modified HTTP caching. * @method void lastModified(int $time) Handles last modified HTTP caching.
* @method void download(string $filePath) Downloads a file
* *
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore * phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
*/ */
@ -76,7 +77,7 @@ class Engine
private const MAPPABLE_METHODS = [ private const MAPPABLE_METHODS = [
'start', 'stop', 'route', 'halt', 'error', 'notFound', 'start', 'stop', 'route', 'halt', 'error', 'notFound',
'render', 'redirect', 'etag', 'lastModified', 'json', 'jsonHalt', 'jsonp', 'render', 'redirect', 'etag', 'lastModified', 'json', 'jsonHalt', 'jsonp',
'post', 'put', 'patch', 'delete', 'group', 'getUrl' 'post', 'put', 'patch', 'delete', 'group', 'getUrl', 'download'
]; ];
/** @var array<string, mixed> Stored variables. */ /** @var array<string, mixed> Stored variables. */
@ -895,6 +896,45 @@ class Engine
} }
} }
/**
* Downloads a file
*
* @param string $filePath The path to the file to download
* @throws Exception If the file cannot be found
*
* @return void
*/
public function _download(string $filePath): void {
if (file_exists($filePath) === false) {
throw new Exception("$filePath cannot be found.");
}
$fileSize = filesize($filePath);
$mimeType = mime_content_type($filePath);
$mimeType = $mimeType !== false ? $mimeType : 'application/octet-stream';
$response = $this->response();
$response->send();
$response->setRealHeader('Content-Description: File Transfer');
$response->setRealHeader('Content-Type: ' . $mimeType);
$response->setRealHeader('Content-Disposition: attachment; filename="' . basename($filePath) . '"');
$response->setRealHeader('Expires: 0');
$response->setRealHeader('Cache-Control: must-revalidate');
$response->setRealHeader('Pragma: public');
$response->setRealHeader('Content-Length: ' . $fileSize);
// // Clear the output buffer
ob_clean();
flush();
// // Read the file and send it to the output buffer
readfile($filePath);
if(empty(getenv('PHPUNIT_TEST'))) {
exit; // @codeCoverageIgnore
}
}
/** /**
* Handles ETag HTTP caching. * Handles ETag HTTP caching.
* *

@ -75,9 +75,10 @@ require_once __DIR__ . '/autoload.php';
* @method static void error(Throwable $exception) Sends an HTTP 500 response. * @method static void error(Throwable $exception) Sends an HTTP 500 response.
* @method static void notFound() Sends an HTTP 404 response. * @method static void notFound() Sends an HTTP 404 response.
* *
* # HTTP caching * # HTTP methods
* @method static void etag(string $id, ('strong'|'weak') $type = 'strong') Performs ETag HTTP caching. * @method static void etag(string $id, ('strong'|'weak') $type = 'strong') Performs ETag HTTP caching.
* @method static void lastModified(int $time) Performs last modified HTTP caching. * @method static void lastModified(int $time) Performs last modified HTTP caching.
* @method static void download(string $filePath) Downloads a file
*/ */
class Flight class Flight
{ {

@ -952,4 +952,38 @@ class EngineTest extends TestCase
$this->assertEquals('Method Not Allowed', $engine->response()->getBody()); $this->assertEquals('Method Not Allowed', $engine->response()->getBody());
} }
public function testDownload()
{
$engine = new class extends Engine {
public function getLoader()
{
return $this->loader;
}
};
// doing this so we can overwrite some parts of the response
$engine->getLoader()->register('response', function () {
return new class extends Response {
public function setRealHeader(
string $header_string,
bool $replace = true,
int $response_code = 0
): self {
return $this;
}
};
});
$tmpfile = tmpfile();
fwrite($tmpfile, 'I am a teapot');
$streamPath = stream_get_meta_data($tmpfile)['uri'];
$this->expectOutputString('I am a teapot');
$engine->download($streamPath);
}
public function testDownloadBadPath() {
$engine = new Engine();
$this->expectException(Exception::class);
$this->expectExceptionMessage("/path/to/nowhere cannot be found.");
$engine->download('/path/to/nowhere');
}
} }

@ -86,6 +86,7 @@ class LayoutMiddleware
<li><a href="/dice">Dice Container</a></li> <li><a href="/dice">Dice Container</a></li>
<li><a href="/no-container">No Container Registered</a></li> <li><a href="/no-container">No Container Registered</a></li>
<li><a href="/Pascal_Snake_Case">Pascal_Snake_Case</a></li> <li><a href="/Pascal_Snake_Case">Pascal_Snake_Case</a></li>
<li><a href="/download">Download File</a></li>
</ul> </ul>
HTML; HTML;
echo '<div id="container">'; echo '<div id="container">';

@ -175,6 +175,11 @@ Flight::route('/json-halt', function () {
Flight::jsonHalt(['message' => 'JSON rendered and halted successfully with no other body content!']); Flight::jsonHalt(['message' => 'JSON rendered and halted successfully with no other body content!']);
}); });
// Download a file
Flight::route('/download', function () {
Flight::download('test_file.txt');
});
Flight::map('error', function (Throwable $e) { Flight::map('error', function (Throwable $e) {
echo sprintf( echo sprintf(
<<<HTML <<<HTML

@ -0,0 +1 @@
This file downloaded successfully!
Loading…
Cancel
Save