Direct uploads
A browser can upload a file straight to the bucket instead of through the application. The frontend posts to the bridge’s upload endpoint, which replies with a signed URL for a PUT to tmp/{uuid} on the upload disk; the browser then PUTs the file there. The route is registered on and off the platform, so uploads work the same way locally.
Configuration
Section titled “Configuration”The defaults ship with the bridge. To change them, publish config/ske.php:
php artisan vendor:publish --tag=ske-config'uploads' => [ 'enabled' => true, // false registers no route 'url' => env('SKE_UPLOADS_URL', '/ske/signed-storage-url'), // the path the frontend posts to 'middleware' => ['web'], // the route's middleware 'gate' => 'uploadFiles', // the ability checked before signing 'expires_after' => 5, // minutes the signed URL stays valid 'disk' => 's3', // the disk that signs; its bucket receives the files],The path can also be changed without publishing the config: set the SKE_UPLOADS_URL variable, e.g. as a secret, to the path the frontend already posts to. A published config/ske.php takes precedence, as usual.
The upload disk must be able to sign upload URLs, which in practice means an s3 disk. The application brings the AWS SDK through league/flysystem-aws-s3-v3. The bridge doesn’t require it.
The request and the response
Section titled “The request and the response”POST {url} accepts these optional fields:
| Field | Meaning |
|---|---|
content_type |
The file’s type, signed into the upload. Defaults to application/octet-stream. |
cache_control |
Stored as the object’s Cache-Control. |
expires |
Stored as the object’s Expires. |
bucket |
Only the upload disk’s bucket is accepted. |
visibility |
Only private is accepted. No ACL is ever signed. |
Any other value for bucket or visibility is refused with a 422. A successful request answers 201:
{"uuid": "…", "bucket": "…", "key": "tmp/…", "url": "https://…", "headers": {"Content-Type": "image/png"}}The browser PUTs the file to url with headers, leaving out Host. The application then moves the file from key to its permanent place, e.g. Storage::copy($key, 'avatars/'.$uuid). Files left under tmp/ should be expired by a bucket lifecycle rule.
The gate
Section titled “The gate”Every request is authorized with Gate::authorize($gate, [$user, $bucket]). If the application defines nothing for the ability, every request is refused with a 403. Define it as a gate:
Gate::define('uploadFiles', function (User $user, User $uploader, string $bucket) { return true;});Or define it as an uploadFiles method on App\Policies\UserPolicy:
public function uploadFiles(User $user): bool{ return true;}Guests reach the gate with a null user, so a gate that allows them must accept a nullable user.
Bucket CORS
Section titled “Bucket CORS”The browser PUTs the file to the bucket’s own domain, so the bucket needs a CORS rule that allows PUT from the application’s origin, with the Content-Type and Cache-Control headers. Without it, the browser blocks the upload.