Uploads
Uploads
Upload modes
Laracrate gives you three ways to get a file's bytes into your storage backend. They differ in who carries the bytes (your PHP server or the browser talking straight to S3/R2) and how large the file is. In every mode the end result is the same: a row in laracrate_files created through $model->addFile(...), with the binary already at its canonical key.
| Mode | How the bytes travel | Best for | Pros | Cons |
|---|---|---|---|---|
| Server-side | Browser to your PHP, then PHP to the disk | Small files, simple forms, trusted server flows | One request, no JS, works on any disk | Bytes pass through PHP (memory, request size limits) |
| Direct presigned PUT | Browser straight to S3/R2 via a presigned URL | Most uploads up to roughly 100 MB | Offloads bandwidth from PHP, progress events | Needs S3-compatible disk (or the local fallback), two steps |
| Multipart | Browser uploads in parts straight to S3/R2 | Large files (video, archives) | Parallel parts, resumable, no PHP transfer | S3/R2 only, more orchestration |
The threshold between presigned and multipart is just a frontend hint, config('laracrate.multipart.threshold') (100 MB by default). The server does not enforce it. Your client code decides which path to take based on file.size.
Server-side (addFile with an UploadedFile)
The simplest mode. Hand addFile() the UploadedFile straight from the request and Laracrate writes it to the collection's disk for you.
public function store(Request $request) { $request->validate(['avatar' => 'required|image|max:5120']); $file = $request->user()->addFile($request->file('avatar'), 'avatar'); return back(); }
addFile() also accepts a Binary value object, a FileUpload (see below), or a string key. See the Working with files from your models section for the full signature and the $data, $slots, $folder parameters.
Direct presigned PUT
Here the browser uploads straight to S3/R2 and your server never touches the bytes. The flow is: ask the server for a presigned URL, PUT the file to it, then confirm by sending the resulting key back so addFile() can persist the File row.
The JS helper ships at resources/js/laracrate.js inside the package (it is not published to npm). Copy it into your app's JS sources, or point a bundler alias at vendor/edulazaro/laracrate/resources/js/laracrate.js, then import presignAndUpload, which does the presign request and the PUT (with progress) in one call:
import { presignAndUpload } from './laracrate'; const result = await presignAndUpload(file, { disk: 'media', maxSizeKb: 10240, onProgress: (ratio) => console.log(Math.round(ratio * 100) + '%'), }); // result = { key, disk, original_name, mime_type, size } // Send it to your own controller to confirm. await fetch('/profile/avatar', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, }, body: JSON.stringify(result), });
presignAndUpload(file, opts) options: disk (required), collection, fileable ({ type, id }), maxSizeKb, presignUrl (override the presign route), and onProgress (a 0..1 callback). It returns { key, disk, original_name, mime_type, size }.
On the backend, rebuild a FileUpload from that payload and pass it to addFile():
use EduLazaro\Laracrate\Support\FileUpload; public function confirm(Request $request) { $data = $request->validate([ 'disk' => 'required|string', 'key' => 'required|string', 'original_name' => 'required|string', 'mime_type' => 'required|string', 'size' => 'required|integer', ]); $file = $request->user()->addFile( FileUpload::fromArray($data), 'avatar' ); return response()->json(['slug' => $file->slug]); }
FileUpload::fromArray() accepts disk, key, original_name (or originalName), mime_type (or mimeType), size, and optional width, height, duration, digest.
Where the file lands. The presign endpoint chooses the key two ways. If you pass fileable_type, fileable_id, and collection, the file is uploaded straight to its canonical key (/{fileable_id}/{collection}/{ulid}_{name}), so no move is needed afterward. If you do not (the common case for a creation form with no model yet), it lands under temp/. When you then call addFile() with that temp/ key, Laracrate moves the object to its canonical key. On S3-compatible disks the move is a server-side copyObject plus deleteObject (StorageManager::moveServerSide()): the bytes never come back through PHP.
If the user cancels before confirming, delete the orphaned temp/ object with deleteTemp(disk, key) from the JS helper. It only deletes keys that start with temp/.
Multipart (large files)
For files past the threshold, S3/R2 multipart splits the upload into parts that the browser PUTs in parallel, each returning an ETag. Laracrate exposes the orchestration endpoints; there is no bundled multipart JS helper, so you drive the four endpoints yourself based on file.size:
- Init.
POST /laracrate/multipart/initwithdisk,expected_size, and optionallymime,file_name,part_size,fileable_type,fileable_id,collection. The response gives youupload_id,id,key,part_size,total_parts,expires_at, andparts(an array of{ part_number, url, method }). - Upload parts.
PUTeach part's bytes to itsurl. Capture theETagresponse header for every part. - Reissue (optional). If a part URL expires before you use it,
POST /laracrate/multipart/{id}/partswithpart_numbersto get fresh URLs. - Complete.
POST /laracrate/multipart/{id}/completewithpartsas[{ part_number, etag }, ...]. S3 assembles the object atkey. To cancel instead,DELETE /laracrate/multipart/{id}.
After complete, the binary exists at key but no File row has been created yet. Persist it the same way as a presigned upload, by calling addFile(FileUpload::fromArray([...]), $collection) with the final disk and key. Multipart sessions are tracked in laracrate_multipart_uploads; only the creator can complete or abort a session, and stale ones are reaped by laracrate:abort-stale-multipart (see the artisan commands section).
Multipart requires an S3-compatible disk. Tuning lives under config('laracrate.multipart'): part_size (10 MB default, 5 MB minimum), expire_minutes, and url_ttl_minutes.
Local driver for development
You usually do not have S3/R2 in local dev. When the upload disk uses Laravel's local driver, StorageManager::presignedUpload() cannot mint a true presigned URL, so it returns a Laravel signed route to the local upload endpoint instead (POST instead of PUT). presignAndUpload follows whatever URL and method the presign response specifies, so the same client code works against local storage with no changes. The signed-route endpoints serving this are described in the next section.
HTTP endpoints
Laracrate registers its routes in routes/web.php. They cover four concerns: direct uploads (presign and multipart), streaming and downloading protected files, polling processing status, and the local-driver upload/serve fallback. Each group has its own prefix, middleware, and route-name prefix driven by config, so you can move or re-protect them without touching the package.
You rarely call these routes by name from PHP. The JS helper and your upload flow hit them by URL. The full set:
| Method | URI | Route name | Purpose |
|---|---|---|---|
| POST | laracrate/uploads/presign |
laracrate.uploads.presign |
Mint a presigned PUT URL (or local signed-route fallback) for a direct upload |
| DELETE | laracrate/uploads/{disk}/{encodedKey} |
laracrate.uploads.cancel |
Delete an abandoned temp/ object (encodedKey is base64 then URL-encoded) |
| POST | laracrate/multipart/init |
laracrate.multipart.init |
Start a multipart session, returns upload_id, total_parts, and part URLs |
| POST | laracrate/multipart/{multipart}/parts |
laracrate.multipart.parts |
Reissue presigned URLs for specific parts |
| POST | laracrate/multipart/{multipart}/complete |
laracrate.multipart.complete |
Assemble the final object from uploaded part ETags |
| DELETE | laracrate/multipart/{multipart} |
laracrate.multipart.abort |
Abort and clean up a multipart session |
| GET | laracrate/files/{file:slug}/stream |
laracrate.files.stream |
Stream a protected file inline (audit + viewer bind) |
| GET | laracrate/files/{file:slug}/preview |
laracrate.files.preview |
Stream the file's preview variant |
| GET | laracrate/files/{file:slug}/download |
laracrate.files.download |
Download a protected file as an attachment |
| GET | laracrate/files/{file:slug}/status |
laracrate.files.status |
Processing status for one file (JSON) |
| POST | laracrate/files/status |
laracrate.files.status.batch |
Processing status for many files in one request |
| POST | _laracrate/local/upload |
laracrate.local.upload |
Receive bytes for the local-driver presigned fallback (signed) |
| GET | _laracrate/local/serve/{file:slug} |
laracrate.local.serve |
Serve a local-disk file after verifying the URL signature (signed) |
Prefixes and middleware. Each group reads its own config, so prefixes and route names shown above are the defaults:
| Group | Prefix config | Middleware config | Name prefix |
|---|---|---|---|
| Presigned uploads | laracrate.uploads.route_prefix (laracrate/uploads) |
laracrate.uploads.middleware (['web', 'auth']) |
laracrate.uploads. |
| Multipart | laracrate.multipart.route_prefix (laracrate/multipart) |
laracrate.multipart.middleware (falls back to the uploads middleware when null) |
laracrate.multipart. |
| Stream / preview / download | laracrate.stream.route_prefix (laracrate/files) |
laracrate.stream.middleware (['web', 'auth']) |
laracrate.stream.route_name_prefix (laracrate.files) |
| Status | laracrate.status.route_prefix (laracrate/files) |
laracrate.status.middleware (['web', 'auth']) |
laracrate.files. |
| Local driver | fixed _laracrate/local |
signed |
laracrate.local. |
A few details worth knowing:
- Authorization is yours. The upload and multipart groups only apply the configured middleware. Restricting which disks a user may write to is enforced separately by
config('laracrate.uploads.allowed_disks'), checked in the presign and init endpoints (empty means no restriction). cancelonly touchestemp/. The DELETE endpoint refuses any key that does not start withtemp/, so it cannot be used to delete canonical objects. ThedeleteTemp()JS helper builds thesegment for you.- Multipart ownership.
parts,complete, andabortverify the caller is the session's creator (when a creator was recorded at init), returning 403 otherwise. - Local routes are signed, not authed. The
_laracrate/localgroup is protected by Laravel'ssignedmiddleware. The URLs are minted byStorageManager::presignedUpload()andGenerateSignedUrlAction, so they carry their own expiry. They exist only as the dev-time stand-in for real presigned S3/R2 URLs. - Status responses. Both status endpoints check
canView()per file and return{ slug, status, ready, url, preview, variants, error }per file (the batch endpoint keys the map by slug and silently omits files you cannot view). Use thepollFileStatusandpollFilesStatusJS helpers to consume them; see the displaying files and processing pipeline sections for howreadyandvariantsare populated.