← Laracrate 8 / 17

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:

  1. Init. POST /laracrate/multipart/init with disk, expected_size, and optionally mime, file_name, part_size, fileable_type, fileable_id, collection. The response gives you upload_id, id, key, part_size, total_parts, expires_at, and parts (an array of { part_number, url, method }).
  2. Upload parts. PUT each part's bytes to its url. Capture the ETag response header for every part.
  3. Reissue (optional). If a part URL expires before you use it, POST /laracrate/multipart/{id}/parts with part_numbers to get fresh URLs.
  4. Complete. POST /laracrate/multipart/{id}/complete with parts as [{ part_number, etag }, ...]. S3 assembles the object at key. 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).
  • cancel only touches temp/. The DELETE endpoint refuses any key that does not start with temp/, so it cannot be used to delete canonical objects. The deleteTemp() JS helper builds the segment for you.
  • Multipart ownership. parts, complete, and abort verify 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/local group is protected by Laravel's signed middleware. The URLs are minted by StorageManager::presignedUpload() and GenerateSignedUrlAction, 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 the pollFileStatus and pollFilesStatus JS helpers to consume them; see the displaying files and processing pipeline sections for how ready and variants are populated.