winter/wn-driveraws-plugin
Adds Amazon S3 as a storage driver for Winter CMS, letting you offload media, assets, and uploads to AWS instead of local disk. Configure credentials and a bucket in the backend, then serve files from S3 or CloudFront for scalable, durable cloud storage.
This is a thin glue package: it wires the AWS SDK into Winter's storage/queue/mail managers and (per the description) adds streamed upload support. The package is small and has almost no downstream usage, so treat the plugin's Plugin.php as the ground truth documentation — it will be a few Storage::extend(), Queue::extend(), Mail::extend() / Cache::extend() calls plus a composer dependency on aws/aws-sdk-php.
Install
composer require winter/wn-driveraws-plugin
php artisan winter:up # run migrations / refresh caches after install
If your project pins plugins via the marketplace, the equivalent is installing the plugin in Settings → System → Updates (or php artisan plugin:install <Author>.<Plugin> using the code name from the README) and then php artisan plugin:enable <Author>.<Plugin>.
First place to look
plugins/winter/driveraws/Plugin.php — confirms which Laravel managers are extended and under which driver names (s3, sqs, ses, dynamodb vs custom names like aws-s3). This matters: if the plugin registers s3, it may be redundant with Laravel's built-in S3 driver; if it registers something else, you must use that name in your config.composer.json of the plugin — whether aws/aws-sdk-php and league/flysystem-aws-s3-v3 come along automatically.config/filesystems.php, config/queue.php, config/mail.php, config/cache.php.First use case: move uploads to S3
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_DEFAULT_REGION=eu-west-1
AWS_BUCKET=my-winter-site
AWS_USE_PATH_STYLE_ENDPOINT=false
// config/filesystems.php
'disks' => [
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => true, // fail loudly instead of returning false
],
],
Storage::disk('s3')->put('exports/report.csv', $csv);
Storage::disk('s3')->url('exports/report.csv');
Storage::disk('s3')->temporaryUrl('exports/report.csv', now()->addMinutes(15));
Verify with php artisan tinker: Storage::disk('s3')->put('healthcheck.txt', 'ok'). If that throws a League\Flysystem or InvalidArgumentException: Driver [s3] is not supported, the plugin isn't enabled or is registering a different driver name — go back to step 1.
Keep credentials out of code, and prefer IAM roles in production. The SDK's default credential chain will pick up an instance profile / ECS task role if you simply omit key and secret from the disk config. Static keys are fine locally but should be injected by your secret manager on EC2/ECS/Lambda-hosted Winter installs. Avoid AWS_PROFILE on shared hosts; it silently wins over env vars in confusing ways.
Centralize the client config. Rather than repeating key/region/bucket across four config files, define one service entry and reference it:
// plugins/acme/site/Plugin.php (register())
$this->app->singleton('acme.s3', fn () => new \Aws\S3\S3Client([
'version' => 'latest',
'region' => config('filesystems.disks.s3.region'),
'endpoint' => config('filesystems.disks.s3.endpoint') ?: null,
'use_path_style_endpoint' => (bool) config('filesystems.disks.s3.use_path_style_endpoint'),
]));
Then use Storage::disk('s3') for normal file ops and the raw client only for things Flysystem doesn't expose (multipart aborts, bucket policies, presigned POSTs).
Large / streamed uploads. This is the headline feature and the one that saves you from PHP's upload_max_filesize. Two patterns:
$stream = fopen($request->file('video')->getRealPath(), 'r');
Storage::disk('s3')->writeStream("media/{$name}", $stream);
if (is_resource($stream)) fclose($stream);
Pair with a queued job for anything over a few MB so the request doesn't time out.$cmd = $s3->getCommand('PutObject', [
'Bucket' => config('filesystems.disks.s3.bucket'),
'Key' => "media/{$uuid}",
]);
$url = (string) $s3->createPresignedRequest($cmd, '+10 minutes')->getUri();
Constrain content-length-range and content type in the POST policy, otherwise you've built an open upload proxy on your AWS bill.Queues. SQS is the most common reason to install this plugin:
// config/queue.php
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX'), // https://sqs.<region>.amazonaws.com/<account-id>
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION'),
'after_commit' => false,
],
php artisan queue:work sqs --queue=high,default --tries=3 --timeout=120. Use queue:listen only in development; SQS + work is the intended production pairing. Set visibility timeout on the queue to at least --timeout plus a minute, and attach a dead-letter queue — otherwise a poison job silently redelivers forever.
Mail via SES.
// config/mail.php
'ses' => [
'transport' => 'ses',
'region' => env('AWS_DEFAULT_REGION', 'eu-west-1'),
],
Note Laravel ships its own ses transport (via symfony/amazon-mailer); if the plugin also registers ses, confirm which one wins. For most projects the framework transport is enough and the plugin's value-add is elsewhere. Either way: verify the sending domain, set up DKIM, publish a MAIL FROM subdomain, and subscribe an SNS topic to bounce/complaint notifications — Winter will happily keep mailing hard-bounced addresses otherwise.
DynamoDB is best used as a cache or session store for multi-node installs (Laravel has a built-in dynamodb cache driver; the plugin may register it for you). Create the table ahead of time with partition key id (String) and attribute expires_at (Number) for TTL, and enable on-demand billing to avoid capacity planning for a CMS.
Media/file attachments. Winter's System\Models\File and attachOne/attachMany go through the filesystem abstraction, so a disk swap should carry your attachments. Whether the backend Media Manager follows depends on your Winter version — check System\Classes\MediaLibrary before promising editors that S3 is transparent.
Environment split. Local: local disk + sync queue + log mailer. Staging/production: S3 + SQS + SES. Drive it entirely from .env and never call env() outside config files (see gotchas). Add php artisan config:clear && php artisan cache:clear && php artisan queue:restart to your deploy script.
env() outside config returns null once config is cached. This is the #1 failure mode with driver plugins — they often read env() directly in Plugin.php. If S3 works in dev but not after php artisan config:cache, that's it. Copy the values into a config file and read via config().
Public ACLs are dead on new buckets. Since April 2023 S3 buckets default to Bucket owner enforced object ownership, where ACLs are disabled entirely. 'visibility' => 'public' will throw AccessControlListNotSupported. Serve public assets through CloudFront with an OAC, or a bucket policy scoped to a prefix — don't fight the ACL setting.
temporaryUrl and clock skew. SignatureDoesNotMatch on presigned URLs is almost always a server clock drifting more than 15 minutes, or a proxy rewriting the host. sudo ntpdate / chrony fixes the first; check APP_URL and trusted proxies for the second.
Endpoint overrides for S3-compatible stores. MinIO, Cloudflare R2, DigitalOcean Spaces and Backblaze all work, but you need endpoint and use_path_style_endpoint => true for MinIO, and R2/Spaces reject some ACL and CopyObject-across-bucket operations. R2 also has no temporaryUrl host rewriting unless AWS_URL is set correctly.
IAM least privilege. At minimum: s3:PutObject, s3:GetObject, s3:DeleteObject, s3:ListBucket (on the bucket ARN, not /*), sqs:SendMessage|ReceiveMessage|DeleteMessage|GetQueueAttributes|ChangeMessageVisibility, ses:SendRawEmail|SendEmail, dynamodb:GetItem|PutItem|UpdateItem|DeleteItem|Query|Scan. Grant s3:ListBucket on the bucket resource and object actions on bucket/* — mixing them up produces "Access Denied" on exists() calls only, which is a fun one to debug.
throw => true. Flysystem swallows failures and returns false by default. For a CMS where a lost image is a lost article, always set 'throw' => true on the disk so failures surface in storage/logs/laravel.log rather than becoming a silent 200.
Queue timeouts kill in-flight jobs. If a worker's --timeout exceeds the queue visibility timeout, SQS redelivers the message while it's still running and you get duplicate side effects. Keep visibility timeout > timeout > max job runtime, and make jobs idempotent regardless.
Sandbox SES. New SES accounts can only send to verified addresses and are capped at 200/day, 1 msg/sec. The error is MessageRejected — it's not a credentials problem even though it looks like one. Request production access early.
DynamoDB Local for tests. Point DYNAMODB_ENDPOINT=http://localhost:8000 and run the official Docker image; otherwise your test suite needs real AWS credentials and will be slow and flaky.
Debugging the SDK. Set 'debug' => true in the client config (or AWS_DEBUG) to get full request/response dumps in storage/logs — invaluable for signing and endpoint issues, but never leave it on in production since it logs authorization headers. php artisan winter:version and composer show aws/aws-sdk-php first, then reproduce in tinker.
Testing. Storage::fake('s3') and Queue::fake() work normally and should be your default in tests — don't hit real AWS from CI.
The maintenance risk is the real gotcha. Two stars and zero dependents means one maintainer, no community pressure to fix breakage, and no track record across Winter/Laravel major upgrades (the SDK and Flysystem both churn). Mitigations: pin the version in composer.json, read Plugin.php once so you could vendor the ~50 lines into your own plugin if it goes stale, and prefer Laravel's first-party s3/sqs/ses drivers where they cover your need — the plugin's genuine value is the streamed-upload and Winter-specific glue, so evaluate whether you actually need those before adopting it for the whole stack.
How can I help you explore Laravel packages today?