Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

troccoli's avatar

View PDF stored on S3 in new tab

My issue is very similar to this, and I have find a partial solution.

First though, my exact requirements are as follows:

  • PDFs are stored in an S3 bucket
  • PDFs are not publicly available, so just using the S3 URL is not possible
  • I want to open the PDF in the browser, in a new tab
  • Authorisation is already done on the page with the "Download" button, which means the it is available only for the right users

To go around the fact that the PDFs are not publicly available I thought I could copy the file from S3 to the local filesystem and then use a temporary URL, with a short lifespan. But I then have another issue: clearing up the local PDFs.

My app runs in a Docker container, so I do need to clear the files regularly. Obviously when I deploy the app those will be cleared, as I will have a new container. But I would want a way to clear them that does not rely on deployments.

I thought of a schedule job, but the scheduler and the queue workers run on separate containers (so I have 3 containers in total).

I'm not sure how to proceed now. I'm open to other suggestions of course.

0 likes
9 replies
LaryAI's avatar
Level 58

You don’t need to copy the PDF from S3 to your local filesystem to serve it securely. Instead, you can generate a temporary signed URL directly from S3, which allows authorized users to access the file for a limited time. This way, you avoid the hassle of managing local files and cleanup.

Here’s how you can do it in Laravel:

1. Generate a Temporary S3 URL

Assuming you’re using Laravel’s Filesystem (with S3 configured in config/filesystems.php):

use Illuminate\Support\Facades\Storage;

public function viewPdf($fileKey)
{
    // Authorize the user here as needed

    // Generate a temporary URL (valid for 5 minutes)
    $url = Storage::disk('s3')->temporaryUrl(
        $fileKey, now()->addMinutes(5)
    );

    // Redirect to the temporary URL, or return it as JSON for your frontend
    return redirect($url);
}

2. Open in New Tab

On your frontend, have your "View PDF" button open the route in a new tab:

<a href="{{ route('pdf.view', ['fileKey' => $fileKey]) }}" target="_blank">View PDF</a>

Or, if you want to fetch the URL via AJAX and open it:

fetch('/pdf/view/{{ $fileKey }}')
  .then(response => response.json())
  .then(data => window.open(data.url, '_blank'));

And in your controller:

return response()->json(['url' => $url]);

3. No Need for Local Cleanup

Since you’re not storing anything locally, you don’t need to worry about cleaning up files. The signed URL will expire automatically.


Summary:

  • Use Storage::disk('s3')->temporaryUrl() to generate a short-lived, secure link to the PDF.
  • Serve that link to the user (either via redirect or JSON).
  • Let the browser open it in a new tab.
  • No need to copy files locally or manage cleanup.

Let me know if you need a more detailed code example!

Glukinho's avatar

If you can't use S3 temporary URLs for some reason, try to utilize streamed response:

$stream = Storage::disk('s3')->readStream('folder/file.pdf');

return response()->stream(
	function () use ($stream): void {
		fpassthru($stream);
	},
	200,
	[
		'Content-Type'        => 'application/pdf',
		'Content-Disposition' => 'inline',
	]
);

https://laravel.com/docs/12.x/responses#streamed-responses

If your PDFs are rather small, you can simply read them from S3 in whole and output as a response, not concerning about memory usage:

$data = Storage::disk('s3')->get('folder/file.pdf');

return response($data, 200, [
	'Content-Type'        => 'application/pdf',
	'Content-Disposition' => 'inline',
]);

In both approaches there is nothing to clean up locally.

martinbean's avatar

@troccoli You‘re completely over-complicating the problem.

If the files aren’t publicly-viewable (which they shouldn’t be) then you should be using Laravel to generate a temporary signed URL for the file. If you want this to open in a new tab, then just put target="_blank" on the link used to view the file:

<a href="{{ Storage::disk('s3')->temporaryUrl('file.pdf', now()->addMinutes(5)) }}" target="_blank">View PDF</a>
Glukinho's avatar

@troccoli Start tinker and execute there:

Storage::disk('s3')->temporaryUrl('file.pdf', now()->addMinutes(5))

Most likely you will see an URL that is definitely accessible to any browser and anybody in Internet is able to download a file from your storage using this URL. All you need is show a link to a user, in a view.

There is also a possibility your S3 storage is fully internal and not exposed to your clients at all (and therefore it can't produce public-accessible URLs), then you should proxy your downloads through Laravel, see my comment above.

troccoli's avatar

@Glukinho

Apologies for deleting my previous comment.

I have now found a solution I'm satisfied with.

Yes, using a temporary URL as href and a target of _blank works, as in the PDF shows in a new tab. Great.

However, the PDF was available to anyone with the temporary URL. Yes, it's a small window, but still. Reducing the expiration is problematic as the user could stay on the page for longer and then click on the link, resulting in an error.

I solved this by adding wire:poll to the button (I'm using Livewire and Flux), which means I refresh the temporary URL constantly and then I can lower the expiration to 5 seconds.

This works great for me:

  • the PDF opens in a new tab
  • PDFS are only accessible using the temporary URL
  • the URL is short lived, minimising the risk of unauthorised access
  • an authorised user will always be able to access the PDF, not matter how long they stay on the page.

Thank you and @martinbean for your help

Glukinho's avatar

@troccoli Honestly, I don't really like your approach.

You are constantly fetching S3 for temp URLs, every 5 seconds, in every opened tab. If you have 10 users want to download PDF at the same time, you will have 10 requests to S3 every 5 seconds, even if files are downloaded but users forgot to close the tab. Besides this increases load to your app, you can be banned/restricted by S3 storage provider.

1 like
troccoli's avatar

@glukinho I agree, it wasn't a great solution. To be clear, I wasn't polling S3, I was polling my app to create a new temporary URL. But maybe that call S3 anyway, I don't know.

In any case I since refactored the code. I now have a dedicate route for the download, so that when the user clicks on the download button I can create a temporary URL and open the PDF in a new tab. No more polling, and the URL is still valid for only 5 seconds.

Glukinho's avatar

@troccoli ok! But may I ask, what exactly are you afraid of by setting 5 seconds urls? What's wrong with 1 minute or 5 minutes?

Please or to participate in this conversation.