-
-
Notifications
You must be signed in to change notification settings - Fork 364
[LiveComponent] Add support for downloading files from LiveActions (Experimental) #2483
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
base: 2.x
Are you sure you want to change the base?
Changes from 7 commits
bd6b8bd
61cee45
2f34d71
f24dea6
d7d6bb5
dd75be0
fe92cc7
f81dcff
4f6f869
36143e6
32cd824
456c0e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -300,15 +300,60 @@ export default class Component { | |
|
||
this.backendRequest.promise.then(async (response) => { | ||
const backendResponse = new BackendResponse(response); | ||
const html = await backendResponse.getBody(); | ||
|
||
// clear sent files inputs | ||
for (const input of Object.values(this.pendingFiles)) { | ||
input.value = ''; | ||
} | ||
|
||
// if the response does not contain a component, render as an error | ||
const headers = backendResponse.response.headers; | ||
if (headers.get('X-Live-Download')) { | ||
if ( | ||
!( | ||
headers.get('Content-Disposition')?.includes('attachment') || | ||
headers.get('Content-Disposition')?.includes('inline') | ||
) || | ||
!headers.get('Content-Disposition')?.includes('filename=') | ||
) { | ||
throw new Error('Invalid LiveDownload response'); | ||
} | ||
smnandre marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const fileSize = Number.parseInt(headers.get('Content-Length') || '0'); | ||
smnandre marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (fileSize > 10000000) { | ||
throw new Error('File is too large to download (10MB limit)'); | ||
} | ||
smnandre marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const fileName = headers.get('Content-Disposition')?.split('filename=')[1]; | ||
smnandre marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (!fileName) { | ||
throw new Error('No filename found in Content-Disposition header'); | ||
} | ||
|
||
const blob = await backendResponse.getBlob(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It depends... if you trigger the "click" and the file start downloading I think browser play the "pipe" role here. But during my test I managed to crashed pretty violently Chrome and Safari multiple times. |
||
const link = Object.assign(window.document.createElement('a'), { | ||
target: '_blank', | ||
style: 'display: none', | ||
href: window.URL.createObjectURL(blob), | ||
download: fileName, | ||
}); | ||
this.element.appendChild(link); | ||
link.click(); | ||
this.element.removeChild(link); | ||
|
||
this.backendRequest = null; | ||
thisPromiseResolve(backendResponse); | ||
|
||
// do we already have another request pending? | ||
if (this.isRequestPending) { | ||
this.isRequestPending = false; | ||
this.performRequest(); | ||
} | ||
|
||
return response; | ||
} | ||
|
||
const html = await backendResponse.getBody(); | ||
|
||
// if the response does not contain a component, render as an error | ||
if ( | ||
!headers.get('Content-Type')?.includes('application/vnd.live-component+html') && | ||
!headers.get('X-Live-Redirect') | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<?php | ||
|
||
namespace Symfony\UX\LiveComponent; | ||
|
||
use SplFileInfo; | ||
use SplTempFileObject; | ||
use Symfony\Component\HttpFoundation\BinaryFileResponse; | ||
use Symfony\Component\HttpFoundation\HeaderUtils; | ||
|
||
/** | ||
* @author Simon André <[email protected]> | ||
*/ | ||
final class LiveDownloadResponse extends BinaryFileResponse | ||
{ | ||
public const HEADER_LIVE_DOWNLOAD = 'X-Live-Download'; | ||
|
||
public function __construct(string|SplFileInfo $file, ?string $filename = null) | ||
Kocal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
if (\is_string($file)) { | ||
$file = new SplFileInfo($file); | ||
} | ||
|
||
if ((!$file instanceof SplFileInfo)) { | ||
throw new \InvalidArgumentException(sprintf('The file "%s" does not exist.', $file)); | ||
} | ||
smnandre marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if ($file instanceof SplTempFileObject) { | ||
$file->rewind(); | ||
} | ||
|
||
parent::__construct($file, 200, [ | ||
self::HEADER_LIVE_DOWNLOAD => 1, | ||
'Content-Disposition' => HeaderUtils::makeDisposition(HeaderUtils::DISPOSITION_ATTACHMENT, $filename ?? basename($file)), | ||
'Content-Type' => 'application/octet-stream', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I hesitated (keep dep light) and chose not to... But in fact it's a good idea and Mime is a very small component. So let's make it a requirement for LiveComponent ? Or only when using downloads / uploads ? wdyt ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest to put it in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pretty sure there is no suggest in Symfony composer packages.. We have other situations in UX where we do not require a package (i.e. in Autocomplete for Form ..) My question was more: should we require it for everyone, or keep it as an "runtime" dependency ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. runtime dependency There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why aren't you deferring to BinaryFileResponse for this logic? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reason for the header? In the frontend, if not a standard/known content type, but has a content disposition (and an other requirements), trigger the download? This way we could say "to enable file downloads, return a response that has a content disposition" |
||
'Content-Length' => $file instanceof SplTempFileObject ? 0 : $file->getSize(), | ||
], false, HeaderUtils::DISPOSITION_ATTACHMENT); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\UX\LiveComponent\Tests\Fixtures\Component; | ||
|
||
use Symfony\Component\HttpFoundation\BinaryFileResponse; | ||
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; | ||
use Symfony\UX\LiveComponent\Attribute\LiveAction; | ||
use Symfony\UX\LiveComponent\Attribute\LiveArg; | ||
use Symfony\UX\LiveComponent\DefaultActionTrait; | ||
use Symfony\UX\LiveComponent\LiveDownloadResponse; | ||
|
||
/** | ||
* @author Simon André <[email protected]> | ||
*/ | ||
#[AsLiveComponent('download_file', template: 'components/download_file.html.twig')] | ||
class DownloadFileComponent | ||
{ | ||
use DefaultActionTrait; | ||
|
||
private const FILE_DIRECTORY = __DIR__.'/../files/'; | ||
|
||
#[LiveAction] | ||
public function download(): BinaryFileResponse | ||
{ | ||
$file = new \SplFileInfo(self::FILE_DIRECTORY.'/foo.json'); | ||
|
||
return new LiveDownloadResponse($file); | ||
} | ||
|
||
#[LiveAction] | ||
public function generate(): BinaryFileResponse | ||
{ | ||
$file = new \SplTempFileObject(); | ||
$file->fwrite(file_get_contents(self::FILE_DIRECTORY.'/foo.json')); | ||
|
||
return new LiveDownloadResponse($file, 'foo.json'); | ||
} | ||
|
||
#[LiveAction] | ||
public function heavyFile(#[LiveArg] int $size): BinaryFileResponse | ||
{ | ||
$file = new \SplFileInfo(self::FILE_DIRECTORY.'heavy.txt'); | ||
|
||
$response = new BinaryFileResponse($file); | ||
$response->headers->set('Content-Length', 10000000); // 10MB | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<title>Foo</title> | ||
</head> | ||
<body> | ||
<h1>Bar</h1> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"foo": "bar" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Foo | ||
|
||
## Bar |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
<div {{ attributes }}> | ||
|
||
</div> |
Uh oh!
There was an error while loading. Please reload this page.