Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: method to download files easily #601

Merged
merged 2 commits into from
Jul 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions flight/Engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@
* @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.
*
* # HTTP caching
* # HTTP methods
* @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 download(string $filePath) Downloads a file
*
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
*/
Expand All @@ -76,7 +77,7 @@ class Engine
private const MAPPABLE_METHODS = [
'start', 'stop', 'route', 'halt', 'error', 'notFound',
'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. */
Expand Down Expand Up @@ -895,6 +896,45 @@ public function _jsonp(
}
}

/**
* 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.
*
Expand Down
3 changes: 2 additions & 1 deletion flight/Flight.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,10 @@
* @method static void error(Throwable $exception) Sends an HTTP 500 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 lastModified(int $time) Performs last modified HTTP caching.
* @method static void download(string $filePath) Downloads a file
*/
class Flight
{
Expand Down
34 changes: 34 additions & 0 deletions tests/EngineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -952,4 +952,38 @@ public function setRealHeader(
$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');
}

}
1 change: 1 addition & 0 deletions tests/server/LayoutMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public function before()
<li><a href="/dice">Dice Container</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="/download">Download File</a></li>
</ul>
HTML;
echo '<div id="container">';
Expand Down
5 changes: 5 additions & 0 deletions tests/server/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@
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) {
echo sprintf(
<<<HTML
Expand Down
1 change: 1 addition & 0 deletions tests/server/test_file.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file downloaded successfully!