Overview
This guide walks you through implementing your first multipart upload using S3M. You’ll learn how to upload files directly from the browser to S3 and handle the response in your Laravel backend.
Make sure you’ve completed the Installation steps before proceeding.
Step 1: Set Up Authorization
Before users can upload files, you need to define who has permission to upload.
Create User Policy
Generate a policy for your User model: php artisan make:policy UserPolicy --model=User
Add Upload Permission
Open app/Policies/UserPolicy.php and add the uploadFiles method: app/Policies/UserPolicy.php
<? php
namespace App\Policies ;
use App\Models\ User ;
class UserPolicy
{
/**
* Determine whether the user can upload files.
*
* @param \App\Models\ User $user
* @return mixed
*/
public function uploadFiles ( User $user )
{
return true ; // Customize based on your needs
}
}
Customize the authorization logic based on your requirements. For example, you might check user roles, subscription status, or storage quotas.
Step 2: Add Blade Directive
Ensure the @s3m directive is in your layout file before your application’s JavaScript:
resources/views/layouts/app.blade.php
<! DOCTYPE html >
< html >
< head >
< title > My App </ title >
</ head >
< body >
< div id = "app" >
@yield ( 'content' )
</ div >
@s3m
< script src = " {{ mix ('js/app.js') }} " ></ script >
</ body >
</ html >
Step 3: Create Upload Frontend
Implement the file upload interface in your frontend JavaScript:
resources/js/components/FileUpload.vue
< template >
< div >
< input
type = "file"
ref = "fileInput"
@ change = " uploadFile "
/>
< div v-if = " uploading " >
< p > Upload Progress: {{ uploadProgress }}% </ p >
< progress : value = " uploadProgress " max = "100" ></ progress >
</ div >
< div v-if = " uploadComplete " >
< p > Upload complete! </ p >
</ div >
</ div >
</ template >
< script setup >
import { ref } from 'vue'
import axios from 'axios'
const fileInput = ref ( null )
const uploadProgress = ref ( 0 )
const uploading = ref ( false )
const uploadComplete = ref ( false )
const uploadFile = ( e ) => {
const file = e . target . files [ 0 ]
if ( ! file ) return
uploading . value = true
uploadComplete . value = false
s3m ( file , {
progress : progress => {
uploadProgress . value = progress
}
}). then (( response ) => {
// Send file metadata to your backend
axios . post ( '/api/files' , {
uuid: response . uuid ,
key: response . key ,
bucket: response . bucket ,
name: file . name ,
content_type: file . type ,
}). then (() => {
uploading . value = false
uploadComplete . value = true
})
}). catch (( error ) => {
console . error ( 'Upload failed:' , error )
uploading . value = false
})
}
</ script >
resources/js/components/FileUpload.jsx
import React , { useState } from 'react'
import axios from 'axios'
export default function FileUpload () {
const [ uploadProgress , setUploadProgress ] = useState ( 0 )
const [ uploading , setUploading ] = useState ( false )
const [ uploadComplete , setUploadComplete ] = useState ( false )
const handleFileChange = ( e ) => {
const file = e . target . files [ 0 ]
if ( ! file ) return
setUploading ( true )
setUploadComplete ( false )
s3m ( file , {
progress : progress => {
setUploadProgress ( progress )
}
}). then (( response ) => {
// Send file metadata to your backend
axios . post ( '/api/files' , {
uuid: response . uuid ,
key: response . key ,
bucket: response . bucket ,
name: file . name ,
content_type: file . type ,
}). then (() => {
setUploading ( false )
setUploadComplete ( true )
})
}). catch (( error ) => {
console . error ( 'Upload failed:' , error )
setUploading ( false )
})
}
return (
< div >
< input
type = "file"
onChange = { handleFileChange }
/>
{ uploading && (
< div >
< p > Upload Progress: { uploadProgress } % </ p >
< progress value = { uploadProgress } max = "100" />
</ div >
) }
{ uploadComplete && (
< p > Upload complete! </ p >
) }
</ div >
)
}
resources/views/upload.blade.php
< div >
< input type = "file" id = "fileInput" />
< div id = "progressContainer" style = "display: none;" >
< p > Upload Progress: < span id = "progressText" > 0 </ span > % </ p >
< progress id = "progressBar" value = "0" max = "100" ></ progress >
</ div >
< div id = "completeMessage" style = "display: none;" >
< p > Upload complete! </ p >
</ div >
</ div >
< script >
document . getElementById ( 'fileInput' ). addEventListener ( 'change' , ( e ) => {
const file = e . target . files [ 0 ]
if ( ! file ) return
document . getElementById ( 'progressContainer' ). style . display = 'block'
document . getElementById ( 'completeMessage' ). style . display = 'none'
s3m ( file , {
progress : progress => {
document . getElementById ( 'progressText' ). textContent = progress
document . getElementById ( 'progressBar' ). value = progress
}
}). then (( response ) => {
// Send file metadata to your backend
fetch ( '/api/files' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
'X-CSRF-TOKEN' : document . querySelector ( 'meta[name="csrf-token"]' ). content
},
body: JSON . stringify ({
uuid: response . uuid ,
key: response . key ,
bucket: response . bucket ,
name: file . name ,
content_type: file . type ,
})
}). then (() => {
document . getElementById ( 'progressContainer' ). style . display = 'none'
document . getElementById ( 'completeMessage' ). style . display = 'block'
})
}). catch (( error ) => {
console . error ( 'Upload failed:' , error )
})
})
</ script >
Step 4: Handle Upload Response
Create a backend endpoint to receive the upload metadata and move the file from temporary to permanent storage:
app/Http/Controllers/FileController.php
<? php
namespace App\Http\Controllers ;
use Illuminate\Http\ Request ;
use Illuminate\Support\Facades\ Storage ;
class FileController extends Controller
{
public function store ( Request $request )
{
$validated = $request -> validate ([
'uuid' => 'required|string' ,
'key' => 'required|string' ,
'bucket' => 'required|string' ,
'name' => 'required|string' ,
'content_type' => 'required|string' ,
]);
// Move file from tmp/ to permanent location
$permanentKey = str_replace ( 'tmp/' , 'files/' , $validated [ 'key' ]);
Storage :: copy (
$validated [ 'key' ],
$permanentKey
);
// Store file metadata in database
$file = auth () -> user () -> files () -> create ([
'uuid' => $validated [ 'uuid' ],
'key' => $permanentKey ,
'bucket' => $validated [ 'bucket' ],
'name' => $validated [ 'name' ],
'content_type' => $validated [ 'content_type' ],
]);
return response () -> json ([
'message' => 'File uploaded successfully' ,
'file' => $file ,
]);
}
}
Files are initially stored in the tmp/ directory and should be moved to a permanent location after validation.
Step 5: Add Route
Add the route to your routes/api.php:
use App\Http\Controllers\ FileController ;
Route :: middleware ( 'auth:sanctum' ) -> group ( function () {
Route :: post ( '/files' , [ FileController :: class , 'store' ]);
});
Understanding the Response
The s3m() function returns a promise that resolves with the following data:
{
uuid : string , // Unique identifier for the file
key : string , // S3 object key (e.g., "tmp/uuid-here")
bucket : string , // S3 bucket name
extension : string , // File extension (e.g., "pdf")
name : string , // Original filename
url : string // S3 URL (if auto_complete is not false)
}
Advanced Options
Customize the upload behavior with additional options:
s3m ( file , {
visibility: 'public-read' , // Make file publicly accessible
progress : progress => {
console . log ( progress )
}
}). then ( response => {
// Handle response
})
This requires allow_change_visibility to be true in config/s3m.php.
s3m ( file , {
chunk_size: 20 * 1024 * 1024 , // 20MB chunks (default: 10MB)
progress : progress => {
console . log ( progress )
}
}). then ( response => {
// Handle response
})
Larger chunks mean fewer HTTP requests but require more memory. Adjust based on your use case.
s3m ( file , {
max_concurrent_uploads: 10 , // Upload 10 parts simultaneously (default: 5)
progress : progress => {
console . log ( progress )
}
}). then ( response => {
// Handle response
})
Higher concurrency can improve speed but may overwhelm slower connections.
s3m ( file , {
chunk_retries: 5 , // Retry failed chunks 5 times (default: 3)
progress : progress => {
console . log ( progress )
}
}). then ( response => {
// Handle response
})
s3m ( file , {
data: {
folder: 'uploads/images' , // Custom folder
bucket: 'my-custom-bucket' , // Custom bucket
},
progress : progress => {
console . log ( progress )
}
}). then ( response => {
// Handle response
})
Custom folders require allow_change_folder to be true in config/s3m.php.
Default Configuration
S3M uses these default settings:
Chunk Size
Max Concurrent Uploads
Chunk Retries
10 * 1024 * 1024 // 10MB per chunk
Testing Your Upload
Open in Browser
Navigate to your upload page and select a file.
Monitor Progress
Watch the progress bar as chunks upload to S3.
Verify in S3
Check your S3 bucket’s tmp/ directory to see the uploaded file.
Check Backend
Verify that your backend endpoint received the file metadata.
Common Issues
CORS errors in browser console
Make sure your S3 bucket has the correct CORS configuration. See the Installation guide for details. The ETag header must be exposed: "ExposeHeaders" : [ "ETag" ]
Check that:
Your UserPolicy has the uploadFiles method
The user is authenticated
The policy returns true for the current user
Ensure the @s3m directive is in your layout before your application’s JavaScript.
Files not appearing in S3
Verify your AWS credentials in .env are correct and the IAM user has S3 write permissions.
Next Steps
Authorization Learn how to implement fine-grained upload permissions
Configuration Explore all available configuration options
JavaScript API Deep dive into the JavaScript API reference
Backend Integration Learn how to process uploaded files in Laravel