PresignedUrl is a PHP library that generates S3-style presigned URLs for any storage backend. It allows you to create secure, time-limited download links for your files.
symfony new my_project
cd my_projectDownload tattali/presigned-url and league/flysystem-bundle using composer
composer require tattali/presigned-url league/flysystem-bundle# config/packages/flysystem.yaml
flysystem:
storages:
default.storage:
adapter: 'local'
options:
directory: '%kernel.project_dir%/var/storage'# config/packages/presigned_url.yaml
presigned_url:
secret: '%env(PRESIGNED_URL_SECRET)%'
base_url: '%env(PRESIGNED_URL_BASE)%'
buckets:
documents:
adapter: flysystem
service: 'default.storage'# .env
PRESIGNED_URL_SECRET=your-secret-key-min-32-characters-long
PRESIGNED_URL_BASE=http://localhost:8000/storage# config/routes/presigned_url.yaml
presigned_url_serve:
path: /storage/{bucket}/{path}
controller: Tattali\PresignedUrl\Bridge\Symfony\Controller\ServeController
requirements:
path: .+
methods: [GET, HEAD]
# # Route with hidden bucket
# presigned_url_invoices:
# path: /invoices/{path}
# controller: Tattali\PresignedUrl\Bridge\Symfony\Controller\ServeController
# defaults:
# bucket: invoices
# requirements:
# path: .+
# methods: [GET, HEAD]// src/Controller/DocumentController.php
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Tattali\PresignedUrl\Storage\StorageInterface;
class DocumentController extends AbstractController
{
public function __construct(
private StorageInterface $storage,
) {}
#[Route('/document/{filename}', name: 'document_download')]
public function download(string $filename): Response
{
// Generate a presigned URL valid for 1 hour
$url = $this->storage->temporaryUrl(
'documents',
$filename,
3600
);
return new RedirectResponse($url);
}
}// src/Controller/UploadController.php
<?php
namespace App\Controller;
use League\Flysystem\FilesystemOperator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class UploadController extends AbstractController
{
public function __construct(
private FilesystemOperator $defaultStorage,
) {}
#[Route('/upload', name: 'upload', methods: ['POST'])]
public function upload(Request $request): Response
{
$file = $request->files->get('file');
if ($file) {
$filename = uniqid() . '.' . $file->guessExtension();
$this->defaultStorage->write($filename, $file->getContent());
return $this->json(['filename' => $filename]);
}
return $this->json(['error' => 'No file provided'], 400);
}
}The presigned URL looks like this:
http://localhost:8000/storage/documents/report.pdf?X-Expires=1704067200&X-Signature=a1b2c3d4
X-Expires: Unix timestamp when the URL expiresX-Signature: HMAC signature to verify the URL authenticity
- Secure signatures: HMAC with timing-safe comparison
- Expiration: Time-limited access to files
- Conditional caching: ETag, If-None-Match support (304 responses)
- Range requests: Partial content for video/audio streaming
- Gzip compression: Automatic for configured MIME types
- CORS support: Configurable allowed origins
# Start the server
symfony serve
# Create a test file
mkdir -p var/storage
echo "Hello World" > var/storage/test.txt
# Get a presigned URL (implement the controller first)
curl http://localhost:8000/document/test.txt