Editor image ingest

The MediaMarkdownEditor field lets writers paste or drop images straight into a markdown body. Images never touch the public disk. Every drop routes through EditorImageStore, which writes a Spatie media row on the s3 disk and returns the media URL for the editor to insert.

The invariant

Never pass a Livewire TemporaryUploadedFile into the Spatie FileAdder.

TemporaryUploadedFile extends Symfony UploadedFile, but its parent path is an empty tmpfile() handle — not the bytes on the Livewire temp disk. Feeding that object to addMedia(), addMediaFromDisk(), or addMediaFromString() writes an empty blob, or throws DiskCannotBeAccessed on MinIO and Bunny S3. EditorImageStore::copyLivewireToLocalTemp() reads the bytes via readStream() and writes them to a real file under sys_get_temp_dir() before Spatie ever sees them.

Flow

EditorImageStore::store($file, $record = null, $collection = null) runs:

  1. Resolve the target model — the $record if it implements Spatie\MediaLibrary\HasMedia and exists, otherwise the authenticated user (who must implement HasMedia).
  2. Pick the collection — body when the model registers it, else uploads. An explicit $collection argument wins.
  3. Copy a TemporaryUploadedFile to a real local temp file; pass a normal UploadedFile straight through.
  4. $model->addMedia($source)->usingFileName(...)->withCustomProperties(['user_id' => ...])->toMediaCollection($collection, MediaDriveCatalog::DISK).
  5. Return the Media instance. EditorImageStore::url($media) returns $media->getUrl() — the CDN host when the disk url (or AWS_URL) is set, otherwise the direct disk URL.

MediaMarkdownEditor wires saveUploadedFileAttachmentUsing to EditorImageStore::store and getFileAttachmentUrlUsing to EditorImageStore::url, so the markdown body receives the final CDN URL.

Why it matters on multi-replica

On Bunny Magic Containers the Livewire temp disk is s3 (LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK=s3). A temp upload lives at a remote key the Spatie FileAdder cannot read through the Livewire object's empty local path. The copy step is what makes ingest work across replicas — bytes are materialised on the pod, then re-uploaded as Spatie media on the same s3 disk.

sequenceDiagram
    participant Editor
    participant Livewire
    participant Store as EditorImageStore
    participant Spatie
    participant S3 as s3 disk
    Editor->>Livewire: drop image
    Livewire->>Livewire: temp upload (s3)
    Livewire->>Store: TemporaryUploadedFile
    Store->>Store: copyLivewireToLocalTemp()
    Store->>Spatie: addMedia(local path)
    Spatie->>S3: write media file
    Spatie-->>Store: Media model
    Store-->>Editor: $media->getUrl()
    Editor->>Editor: insert URL into markdown body

See also

Built by Qcentic