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), 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), and optional size, width, height, duration, digest.
The size is measured, not trusted. addFile() asks the bucket how big the object is (one HEAD, no download) and checks the collection's rules with that, whatever size says: a presigned PUT does not bind the length, so the size the browser declares means nothing. An object over the maximum is refused (UploadRejected with reason size) and deleted from temp/; one that is not in the bucket is refused with reason missing.
Where the file lands. The presign endpoint always signs a key under temp/; the request cannot name a model path, which used to let anyone write into another model's folder. When you then call addFile() with that temp/ key, Laracrate moves the object to its canonical key (/{fileable_id}/{collection}/{ulid}_{name}). 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. It works under an upload grant, like sign and confirm: the grant is the only source of the target, so the request never names a disk, a model or a key. There is no bundled multipart client yet (the upload queue sends every file with a single PUT), so you drive the endpoints yourself based on file.size:
- Init.
POST /laracrate/multipart/initwithgrant,file_name,mimeandsize, and optionallypart_size. The file is checked against the collection and slot rules first, assigndoes. The response gives youid,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 (your bucket's CORS has to expose it). - Reissue (optional). If a part URL expires before you use it,
POST /laracrate/multipart/{id}/partswithgrantandpart_numbersto get fresh URLs. - Complete.
POST /laracrate/multipart/{id}/completewithgrantandpartsas[{ part_number, etag }, ...]. S3 assembles the object and Laracrate creates the File, answering likeconfirm. The size checked is the one the bucket holds, since no part URL binds a length, and an object the collection refuses is deleted. To cancel instead,DELETE /laracrate/multipart/{id}withgrant.
Reissuing, completing and cancelling only accept the grant that started the session, which only the user it was issued to can send. The object goes straight to its canonical key rather than to temp/: moving it afterwards would be a single CopyObject, which S3 refuses past 5 GB.
Sessions are tracked in laracrate_multipart_uploads, with the File they produced in file_id. Schedule laracrate:abort-stale-multipart (see the artisan commands section): until a session is aborted, its parts take space in the bucket without showing in any listing.
An application that starts multipart sessions on its own, with InitiateMultipartUploadAction and its siblings behind its own routes, keeps doing so: those sessions carry no grant, and these endpoints do not reach them.
Earlier versions took the disk and the key of init from the request, and left these routes with no middleware at all when multipart.middleware was null, the shipped default. If you cannot update yet, set that key explicitly.
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/uploads/sign |
laracrate.uploads.sign |
Grant based: check a file against the rules, then a presigned PUT and a receipt, or the address to post it to |
| POST | laracrate/uploads/confirm |
laracrate.uploads.confirm |
Grant based: create the File of a direct upload (the receipt proves the key was issued to this grant) |
| POST | laracrate/uploads/receive |
laracrate.uploads.receive |
Grant based: take a file through PHP and create the File at once (encrypted collections) |
| POST | laracrate/uploads/register |
laracrate.uploads.register |
Presign mode of the components: create the File of an object the component signed, by its key alone |
| POST | laracrate/uploads/remove |
laracrate.uploads.remove |
Grant based: remove the current file of a target that replaces it (a replacing grant only) |
| GET | laracrate/uploads/targets/{target} |
laracrate.uploads.target |
An upload target: the collection's rules, the current file and the scope, with no grant (?params[venue]=12) |
| POST | laracrate/uploads/targets/{target} |
laracrate.uploads.target.grant |
An upload target: a grant, asked for when the user picks files |
| POST | laracrate/multipart/init |
laracrate.multipart.init |
Grant based: check a file against the rules and start a multipart session, returns the part URLs |
| POST | laracrate/multipart/{multipart}/parts |
laracrate.multipart.parts |
Reissue presigned URLs for specific parts (with the session's grant) |
| POST | laracrate/multipart/{multipart}/complete |
laracrate.multipart.complete |
Assemble the object from the part ETags and create the File (with the session's grant) |
| DELETE | laracrate/multipart/{multipart} |
laracrate.multipart.abort |
Abort and clean up a multipart session (with the session's grant) |
| GET | laracrate/files/{file:slug}/stream |
laracrate.files.stream |
The file through PHP, inline (streamUrl()). Checks canView on every request, honours Range, decrypts, logs; not counted as a download |
| GET | laracrate/files/{file:slug}/download |
laracrate.files.download |
The file through PHP as an attachment (downloadUrl()), counted in downloads_count |
| GET | laracrate/files/{file:slug}/viewer |
laracrate.files.viewer |
What the viewer shows (JSON): name, type and a link made at request time, after canView |
| 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 / download / viewer | 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 endpoint. The grant based endpoints, multipart included, take the disk from the grant's collection. cancelonly touchestemp/, and only what this browser signed. The DELETE endpoint refuses any key that is not a plaintemp/name, and any key that was not signed bypresignin the same browser session (or for the same user when there was no session). ThedeleteTemp()JS helper builds thesegment for you.- Files through PHP are not signed.
stream,downloadandviewersit behind the session and check the file's view policy on every request, so their links never expire. Keepauthinlaracrate.stream.middleware. - Multipart sessions answer to their grant.
parts,complete, andabortreturn 403 for any grant other than the one that started the session, and for sessions started outside these endpoints. - 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.