Skip to content

Register Craft Cloud storage as Laravel disks - #212

Draft
timkelty wants to merge 6 commits into
5.xfrom
feature/craft-6-cloud-disks
Draft

timkelty wants to merge 6 commits into
5.xfrom
feature/craft-6-cloud-disks

Conversation

@timkelty

@timkelty timkelty commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Description

Craft 6 replaced Craft filesystems with Laravel disks. Volumes name a disk, and core applies each volume's subpath with a scoped disk. This PR adds Craft Cloud storage as an ordinary Laravel disk driver. It's designed for Craft 6 on its own terms, not ported from the 3.x filesystems.

The craft-cloud driver always means the project's Cloud bucket.

// config/filesystems.php
'images' => ['driver' => 'craft-cloud', 'prefix' => 'images'],
  • What it builds. A plain s3 disk, so it resolves to Laravel's AwsS3V3Adapter. Core checks for that class to use S3 multipart uploads and to copy uploads within the bucket.
    • The bucket is CRAFT_CLOUD_PROJECT_ID, and the root is {env}/assets.
    • URLs look like https://cdn.craft.cloud/{env}/assets/….
    • Objects are private by default.
  • Options. Anything you put on the disk is kept, except the settings the driver owns: bucket, region, endpoint, credentials, root and URL. So Laravel's own prefix (a subfolder), visibility, throw and asset_transformer work as on any disk. The driver doesn't add asset_transformer; a disk that should use Cloud transforms sets 'asset_transformer' => 'craft-cloud' itself.
  • root is optional and relative to the environment's folder. Left out, the disk is {env}/assets. 'root' => 'storage' makes it {env}/storage. The package uses that for its own craft-cloud-tmp disk.
  • Environment variables.
    • Required: CRAFT_CLOUD_PROJECT_ID, CRAFT_CLOUD_ENVIRONMENT_ID, and CRAFT_CLOUD_REGION or AWS_REGION.
    • Optional: CRAFT_CLOUD_ACCESS_KEY/_SECRET/_TOKEN (otherwise the SDK finds credentials itself, such as the function's role), CRAFT_CLOUD_STORAGE_ENDPOINT (path-style), and CRAFT_CLOUD_CDN_BASE_URL.
  • No fallback. Off Cloud, the driver still works when those variables are set, for example with a dev bucket or MinIO. When they aren't, resolving the disk throws an error that names the missing variables. Craft can still boot, and craft commands and installs still run.

Upload staging, on Cloud only. The provider adds a craft-cloud-tmp disk ({env}/storage/tmp). Unless the project sets them, it defaults uploadSessionDisk and tempAssetUploadDisk to that disk, because the function's own disk doesn't outlive an invocation.

  • Upload sessions and temporary uploads use different folders (upload-sessions/{id} and user_{id}), so one disk covers both.
  • That disk uses the same credentials and endpoint as asset disks. That's what lets core copy a finished upload into a volume with CopyObject instead of downloading it.
  • Core redefines its craft-tmp and craft-asset-temp disks on boot, so this PR changes the settings instead.

Local development uses ordinary Laravel config. The README documents choosing the disk per environment:

'images' => env('CRAFT_CLOUD')
    ? ['driver' => 'craft-cloud', 'prefix' => 'images']
    : ['driver' => 'local', 'root' => public_path('uploads/images'), 'url' => '/uploads/images'],

Upgrading from Craft 5. Core's remove_craft_filesystems migration needs a disk for each old filesystem handle. For each Craft Cloud filesystem, the README says to add '{handle}' => ['driver' => 'craft-cloud', 'prefix' => '{old subpath}'].

What changed from the first revision, and why

  • The local public/uploads fallback is gone (Tim's call). A craft-cloud disk no longer turns into a different disk when it runs off Cloud.
  • The subpath option is gone. Laravel's own prefix option does the same thing, including URLs and core's scoped volume subpaths.
  • The default craft-cloud disk is gone. Without a fallback, a disk that only exists on Cloud would break the same volume locally. Projects name their disks in config/filesystems.php, as with any Laravel disk.
  • cloud:disks is gone. It only printed one line per filesystem. The README's upgrade note does the same job.
  • craft-cloud-uploads and craft-cloud-temp are merged into craft-cloud-tmp.
  • Per-disk CDN url and expires are gone. Projects that need a different URL can define their own s3 disk.

Infra requirements

  • CRAFT_EPHEMERAL=true on Cloud functions. Core uses it to turn off project config YAML writes, put craft-tmp in /tmp, and skip publishing legacy asset bundles.
  • Bucket CORS. Allow PUT from control panel origins, and expose ETag for multipart uploads.
  • IAM. The function's role needs the multipart actions, plus GetObject, PutObject and DeleteObject on {env}/assets and {env}/storage.
  • A lifecycle rule on {env}/storage/tmp/. It should expire old objects and abort incomplete multipart uploads.

Contract changes to consider (not made here)

  • CRAFT_CLOUD_REGION: if buckets are always in the function's region, AWS_REGION (which Lambda always sets) is enough. For off-Cloud use, it's one fewer variable to document.
  • CRAFT_CLOUD_ACCESS_KEY/_SECRET/_TOKEN: if Lambda never receives them, they only matter off Cloud. Standard AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, which the SDK already reads, could replace them.
  • CRAFT_CLOUD_CDN_BASE_URL: this comes from 3.x's cdnBaseUrl setting. If infra doesn't use it to point at another CDN host, the URL can be hardcoded.

Notes for review

  • The local-development snippet checks env('CRAFT_CLOUD') in config. If Cloud ever caches config at build time without that variable set, the cache would pick the local disk.
  • Cloud asset transforms (Add the Craft Cloud asset transform driver #213) treat an AwsS3V3Adapter whose bucket is CRAFT_CLOUD_PROJECT_ID as a Cloud disk, and that still holds. Off Cloud with a dev bucket, the same check would also match, so transforms would expect the CDN to serve that bucket.
  • This adds league/flysystem-aws-s3-v3, requires craftcms/cms ^6.0.0-alpha.21, and locks the 6.x branch at craftcms/cms@8a13417. No tag includes #20542 yet. Packagist hadn't indexed that commit when the lock was made, so the lock's craftcms/cms and craftcms/cms-assets references were set to it by hand. Core's composer.json is unchanged from the indexed commit.
  • Nothing has run on Craft Cloud itself yet.

Follow-ups

  • CDN purges when files change.
  • The max-age and visibility object metadata the CDN relies on, likely through the disk's options.

Related issues

Adds a craft-cloud disk driver (S3 on the project bucket with CDN URLs on
Cloud, a local disk in public/uploads elsewhere), a default craft-cloud
assets disk, bucket disks for upload session staging and temporary asset
uploads on Cloud, and a cloud:disks command that prints disks for 3.x
AssetsFs filesystems before upgrading.

Amp-Thread-ID: https://ampcode.com/threads/T-79ed60c5-5654-4f30-8dbd-da4b0ecd4fb2
Co-authored-by: Amp <amp@ampcode.com>
timkelty and others added 2 commits November 10, 2026 13:15
Craft 6 is removing CRAFT_EPHEMERAL (craftcms/cms#20547), which is what
turned it off before.

Amp-Thread-ID: https://ampcode.com/threads/T-79ed60c5-5654-4f30-8dbd-da4b0ecd4fb2
Co-authored-by: Amp <amp@ampcode.com>
Requires a core newer than 6.0.0-alpha.20, and locks 6.x at 8a13417, which
has disk-owned asset transformers (craftcms/cms#20542). Packagist hadn't
indexed that commit yet, so the lock was pointed at it by hand; core's
composer.json is the same as at the indexed commit (cc77dd6).

Also checks that Cloud disks share the credentials and endpoint Craft needs
to copy staged uploads within S3.

Amp-Thread-ID: https://ampcode.com/threads/T-79ed60c5-5654-4f30-8dbd-da4b0ecd4fb2
Co-authored-by: Amp <amp@ampcode.com>
timkelty and others added 3 commits November 10, 2026 18:11
Replaces the craft-cloud driver, its local fallback and subpath option, and
the cloud:disks command. On Craft Cloud, the provider sets the craft-cloud
disk's connection to the bucket's {env}/assets prefix and the CDN, keeping
the project's other options, and adds a craft-cloud-tmp disk in
{env}/storage/tmp for upload sessions and temporary asset uploads.
Elsewhere, the project defines craft-cloud itself, and upgraded filesystem
handles scope it.

Amp-Thread-ID: https://ampcode.com/threads/T-79ed60c5-5654-4f30-8dbd-da4b0ecd4fb2
Co-authored-by: Amp <amp@ampcode.com>
Brings back the craft-cloud driver, without its local fallback: it always
builds an S3 disk on the project's bucket, and throws when resolved without
the bucket variables, so it works off Cloud against a dev bucket or MinIO
but never quietly becomes another disk. Subfolders use Laravel's own
prefix option. Drops the default craft-cloud disk, and documents choosing
a local disk per environment instead.

Amp-Thread-ID: https://ampcode.com/threads/T-79ed60c5-5654-4f30-8dbd-da4b0ecd4fb2
Co-authored-by: Amp <amp@ampcode.com>
It isn't part of Craft Cloud storage, and CRAFT_EPHEMERAL already turns it off.

Amp-Thread-ID: https://ampcode.com/threads/T-79ed60c5-5654-4f30-8dbd-da4b0ecd4fb2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant