stetodd/s3-multipart-upload-bundle

Direct-to-S3 multipart upload endpoints for Symfony (Uppy-compatible): create, sign parts, complete and abort via presigned URLs

Maintainers

Package info

github.com/stetodd/s3-multipart-upload-bundle

Type:symfony-bundle

pkg:composer/stetodd/s3-multipart-upload-bundle

Statistics

Installs: 4

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-06-12 19:18 UTC

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:

  • MultipartUploadCreateddescriptor (filename, uploadId, key, bucket, tag) + target; persist your record here. setResponse() overrides the default {key, uploadId} body.
  • MultipartUploadCompleteduploadId, key, target; mark your record complete, kick off processing, and optionally setResponse() (e.g. return the created domain resource).
  • MultipartUploadAborteduploadId, 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.