Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save tattali/a8334e83101bc0364f32d51342389e34 to your computer and use it in GitHub Desktop.

Select an option

Save tattali/a8334e83101bc0364f32d51342389e34 to your computer and use it in GitHub Desktop.
Use presigned url in your Symfony project with Flysystem

Use PresignedUrl in your Symfony project with Flysystem

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.

Create a new Symfony project

symfony new my_project
cd my_project

Installation

Download tattali/presigned-url and league/flysystem-bundle using composer

composer require tattali/presigned-url league/flysystem-bundle

Configuration

1. Configure Flysystem

# config/packages/flysystem.yaml
flysystem:
    storages:
        default.storage:
            adapter: 'local'
            options:
                directory: '%kernel.project_dir%/var/storage'

2. Configure PresignedUrl

# 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'

3. Add environment variables

# .env
PRESIGNED_URL_SECRET=your-secret-key-min-32-characters-long
PRESIGNED_URL_BASE=http://localhost:8000/storage

4. Configure the route

# 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]

Usage

Generate a presigned URL

// 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);
    }
}

Upload a file (example)

// 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);
    }
}

Generated URL format

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 expires
  • X-Signature: HMAC signature to verify the URL authenticity

Features included

  • 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

Test it

# 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment