stetodd / s3-multipart-upload-bundle
Direct-to-S3 multipart upload endpoints for Symfony (Uppy-compatible): create, sign parts, complete and abort via presigned URLs
Package info
github.com/stetodd/s3-multipart-upload-bundle
Type:symfony-bundle
pkg:composer/stetodd/s3-multipart-upload-bundle
Requires
- php: >=8.4
- aws/aws-sdk-php: ^3.300
- psr/container: ^1.1|^2.0
- psr/event-dispatcher: ^1.0
- symfony/config: ^7.2|^8.0
- symfony/dependency-injection: ^7.2|^8.0
- symfony/event-dispatcher: ^7.2|^8.0
- symfony/http-foundation: ^7.2|^8.0
- symfony/http-kernel: ^7.2|^8.0
- symfony/routing: ^7.2|^8.0
- symfony/security-core: ^7.2|^8.0
- symfony/serializer: ^7.2|^8.0
- symfony/validator: ^7.2|^8.0
Suggests
- symfony/security-bundle: Required when using the required_grant configuration option
This package is auto-updated.
Last update: 2026-06-12 21:18:14 UTC
README
Direct-to-S3 multipart upload endpoints for Symfony, compatible with Uppy's AwsS3 multipart flow: create an upload, presign each part, then complete or abort — all against any S3-compatible store (AWS, R2, minio).
Install
composer require stetodd/s3-multipart-upload-bundle
Register in config/bundles.php:
Stetodd\S3MultipartUploadBundle\StetoddS3MultipartUploadBundle::class => ['all' => true],
Configure targets in config/packages/stetodd_s3_multipart_upload.yaml. Each target maps a client-supplied targetType to an S3 client service, bucket and key prefix:
stetodd_s3_multipart_upload: required_grant: IS_AUTHENTICATED # optional; null leaves access control to the app # presign_expiry: '+20 minutes' targets: artifact: client: app.storage.artifacts # service id of an Aws\S3\S3Client bucket: '%env(S3_BUCKET)%' base_path: 'artifact' tag: 'artifact-media' allowed_content_types: [image/jpeg, image/png, image/webp]
Import the routes with whatever prefix you like (config/routes/stetodd_s3_multipart_upload.yaml):
stetodd_s3_multipart_upload: resource: '@StetoddS3MultipartUploadBundle/config/routes.php' prefix: /s3
Endpoints
| Method | Path | Body / query | Response |
|---|---|---|---|
| OPTIONS | /create |
— | 204 |
| POST | /create |
{targetType, contentType, filename} |
{key, uploadId} |
| GET | /parts/sign |
?uploadId=&partNumber= |
{url} (presigned) |
| POST | /complete |
{uploadId, parts: [{PartNumber, ETag}]} |
{success: true}* |
| DELETE | /abort |
{uploadId} |
{} |
* listeners can replace any create/complete response — see below.
Application seams
The bundle deliberately persists nothing. Two integration points connect it to your domain:
1. Upload resolver (required for sign/complete/abort). Map an uploadId back to the object key and target you stored when the upload was created:
use Symfony\Component\DependencyInjection\Attribute\AsAlias; #[AsAlias(UploadResolverInterface::class)] final readonly class FileUploadResolver implements UploadResolverInterface { public function resolve(string $uploadId): ResolvedUpload { $upload = $this->repository->findByUploadId($uploadId) ?? throw UploadNotFoundException::forUploadId($uploadId); return new ResolvedUpload($upload->getKey(), $upload->getTargetType()); } }
Unknown ids surface as 404s.
2. Lifecycle events. Subscribe to persist your upload record and run post-completion behaviour:
MultipartUploadCreated—descriptor(filename, uploadId, key, bucket, tag) +target; persist your record here.setResponse()overrides the default{key, uploadId}body.MultipartUploadCompleted—uploadId,key,target; mark your record complete, kick off processing, and optionallysetResponse()(e.g. return the created domain resource).MultipartUploadAborted—uploadId,key,target.
Storage services
Each configured target also exposes stetodd_s3_multipart_upload.storage.<name> (a MultipartStorageInterface) with presigned GET URLs (getTemporaryUrl), part listing and ranged reads (readRange) for use elsewhere in your app, resolvable by target string via TargetStorageLocator.