Storage & Cloud Object Disks

Craft Engine provides a unified, expressive filesystem abstraction for local disks, public assets, and S3-compatible cloud storage (AWS S3, MinIO, Cloudflare R2, Google Cloud Storage).

🚀 Basic Usage

from craft.facades import Storage

# 1. Store contents
Storage.put("documents/report.pdf", pdf_bytes)

# 2. Check existence & read
if Storage.exists("documents/report.pdf"):
    content = Storage.get("documents/report.pdf")
    size = Storage.size("documents/report.pdf")
    mime = Storage.mime_type("documents/report.pdf")

# 3. Public or S3 URL
url = Storage.url("documents/report.pdf")

# 4. Signed Temporary URL (for private files)
temp_url = Storage.temporary_url("documents/report.pdf", minutes=15)

# 5. Delete file
Storage.delete("documents/report.pdf")

☁️ Multi-Disk Switching (local, public, s3)

Configured in config/storage.py:

# Upload directly to S3 disk
Storage.disk("s3").put("backups/database.sql", backup_content)

# Read from public disk
avatar_url = Storage.disk("public").url("avatars/user_1.jpg")

Tenant-Scoped Disks

A disk with "tenant_scoped": true keeps each tenant's files under tenants/<tenant_id>/. The path you pass is relative to the tenant's own directory, so one tenant can never name another's file, and a call with no tenant bound raises TenantNotBoundError instead of writing to a shared root:

# config/storage.py
"tenant": {"driver": "local", "root": "storage/app", "tenant_scoped": True},

Storage.disk("tenant").put("contracts/2026.pdf", pdf)   # storage/app/tenants/<id>/contracts/2026.pdf

Safe Paths

Every key is validated before it is used, by the local and S3 drivers alike: a .. segment, an absolute path, a \0 byte or a scheme:// is refused with a ValueError, whether the key is written to disk or rendered into a URL. A stored filename is therefore never a traversal, and neither is a link.