Skip to content

Instantly share code, notes, and snippets.

@mxmtsk
Last active February 16, 2024 03:13
Show Gist options
  • Save mxmtsk/78f92f5e87ff198ddcbc372c66c83611 to your computer and use it in GitHub Desktop.
Save mxmtsk/78f92f5e87ff198ddcbc372c66c83611 to your computer and use it in GitHub Desktop.
Laravel controller for handling AWS S3 Multipart uploads with Uppy.io uploader. Replicates the API of the node server so you only have to specify `companionUrl` in the Uppy Configuration without specific the custom methods.
<?php
namespace App\Http\Controllers;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Config;
class MultipartController extends Controller
{
public function createMultipartUpload(Request $request)
{
$input = $request->input();
if (!$input["filename"] || !is_string($input["filename"])) {
return response()->json(["error" => 's3: filename returned from `getKey` must be a string'], 500);
}
if (!$input["type"] || !is_string($input["type"])) {
return response()->json(["error" => 's3: content type must be a string'], 400);
}
$originalFilename = $input['filename'];
$filename = md5(uniqid()) . "-" . uniqid() . "." . File::extension($originalFilename);
$disk = Storage::disk('s3');
$client = $disk->getDriver()->getAdapter()->getClient();
try {
$uploader = $client->createMultipartUpload([
'Bucket' => Config::get('filesystems.disks.s3.bucket'),
'Key' => $filename,
'Metadata' => $input["metadata"],
'ACL' => 'public-read',
'Expires' => 3000,
'ContenType' => $input["type"],
]);
return response()->json([
"key" => $filename,
"uploadId" => $uploader["UploadId"],
]);
} catch (Exception $e) {
return response()->json(["error" => 's3: something went wrong'], 500);
}
}
public function getUploadedParts(Request $request)
{
$uploadId = $request->route('uploadId');
$key = $request->input('key');
if (!$key || !is_string($key)) {
return response()->json(["error" => 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"'], 400);
}
$disk = Storage::disk('s3');
$client = $disk->getDriver()->getAdapter()->getClient();
$parts = [];
$listPartsPage = function ($startAt) use ($key, $uploadId, $client, &$parts, &$listPartsPage) {
$listParts = $client->listParts([
'Bucket' => Config::get('filesystems.disks.s3.bucket'),
'Key' => $key,
'UploadId' => $uploadId,
'PartNumberMarker' => $startAt,
]);
if ($listParts["IsTruncated"]) {
$listPartsPage($listParts["NextPartNumberMarker"]);
}
$parts = array_merge($listParts["Parts"], $parts);
};
$listPartsPage(0);
return response()->json(["parts" => $parts]);
}
public function signPartUpload(Request $request)
{
$uploadId = $request->route('uploadId');
$partNumber = $request->route('partNumber');
$key = $request->input('key');
if (!$key || !is_string($key)) {
return response()->json(["error" => 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"'], 400);
}
$disk = Storage::disk('s3');
$client = $disk->getDriver()->getAdapter()->getClient();
try {
$cmd = $client->getCommand('UploadPart', [
'Bucket' => Config::get('filesystems.disks.s3.bucket'),
'Key' => $key,
'UploadId' => $uploadId,
'PartNumber' => $partNumber,
'ACL' => 'public-read',
//'Body' => '',
'Expires' => 3000,
//'ContentType' => 'application/octet-stream',
]);
$request = $client->createPresignedRequest($cmd, '+20 minutes');
// Get the actual presigned-url
$presignedUrl = (string) $request->getUri();
return response()->json([
"url" => $presignedUrl,
]);
} catch (Exception $e) {
return response()->json(["error" => 's3: something went wrong'], 500);
}
}
public function completeMultipartUpload(Request $request)
{
$uploadId = $request->route('uploadId');
$key = $request->input('key');
$parts = $request->input('parts');
if (!$key || !is_string($key)) {
return response()->json(["error" => 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"'], 400);
}
if (!$parts || !is_array($parts)) {
return response()->json(["error" => 's3: `parts` must be an array of {ETag, PartNumber} objects.'], 400);
}
$disk = Storage::disk('s3');
$client = $disk->getDriver()->getAdapter()->getClient();
try {
$completeUpload = $client->completeMultipartUpload([
'Bucket' => Config::get('filesystems.disks.s3.bucket'),
'Key' => $key,
'UploadId' => $uploadId,
'MultipartUpload' => [
'Parts' => $parts
],
]);
return response()->json([
"location" => $completeUpload["Location"],
]);
} catch (Exception $e) {
return response()->json(["error" => 's3: something went wrong'], 500);
}
}
public function abortMultipartUpload(Request $request)
{
$uploadId = $request->route('uploadId');
$key = $request->input('key');
if (!$key || !is_string($key)) {
return response()->json(["error" => 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"'], 400);
}
$disk = Storage::disk('s3');
$client = $disk->getDriver()->getAdapter()->getClient();
try {
$client->abortMultipartUpload([
'Bucket' => Config::get('filesystems.disks.s3.bucket'),
'Key' => $key,
'UploadId' => $uploadId,
]);
return response()->json([]);
} catch (Exception $e) {
return response()->json(["error" => 's3: something went wrong'], 500);
}
}
}
<?php
// Routes
Route::post('/s3/multipart', 'MultipartController@createMultipartUpload');
Route::get('/s3/multipart/{uploadId}', 'MultipartController@getUploadedParts');
Route::get('/s3/multipart/{uploadId}/{partNumber}', 'MultipartController@signPartUpload');
Route::post('/s3/multipart/{uploadId}/complete', 'MultipartController@completeMultipartUpload');
Route::delete('/s3/multipart/{uploadId}', 'MultipartController@abortMultipartUpload');
@ratxcat
Copy link

ratxcat commented Jan 23, 2024

Great! It would be helpful if you could make a blog or upload an example application on a repo.
I am looking for a similar solution. So need the AWS bucket config as well.

Getting 400 bad request : https://prnt.sc/Syz_qwimb70x
Though I have fixed the cors config and tested from another package. But I want to use this controller. Could you please help me on this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment