phplaravelvue.jsamazon-s3uppy

Uploading Multipart Files with Uppy/Laravel/Vue


I'm using Uppy's Vue components to handle uploading large files and I'm having trouble getting it to work.

I've followed the suggestion by Janko here which involves setting the companionUrl in Uppy to point to my server, then write the necessary routes/functions to process the requests.

Everything is working until I try to fire the AWS "completeMultipartUpload" call.

$result = $client->completeMultipartUpload([
    'Bucket'          => 'bucket-name',
    'Key'             => $key,
    'UploadId'        => $uploadId,
    'MultipartUpload' => [
        'Parts' => $formattedParts,
    ],
]);

I get the following error:

"Error executing "CompleteMultipartUpload" on "https://bucket-name.s3.amazonaws.com/NewProject.png?uploadId=nlWLdbNgB9zgarpLBXnj17eOIGAmQM_xyBArymtwdM71fhbFvveggDmL6fz4blz.B95TLhMatDvodbMb5p2ZMKqdlLeLFoSW1qcu33aRQTlt6NbiP_dkDO90DFO.pWGH"; AWS HTTP error: Client error: `POST https://bucket-name.s3.amazonaws.com/NewProject.png?uploadId=nlWLdbNgB9zgarpLBXnj17eOIGAmQM_xyBArymtwdM71fhbFvveggDmL6fz4blz.B95TLhMatDvodbMb5p2ZMKqdlLeLFoSW1qcu33aRQTlt6NbiP_dkDO90DFO.pWGH` resulted in a `400 Bad Request` response:
    <Error><Code>InvalidPart</Code><Message>One or more of the specified parts could not be found.  The part may not have be (truncated...)
    InvalidPart (client): One or more of the specified parts could not be found.  The part may not have been uploaded, or the specified entity tag may not match the part's entity tag. - <Error><Code>InvalidPart</Code><Message>One or more of the specified parts could not be found.  The part may not have been uploaded, or the specified entity tag may not match the part's en"

I think that the problem is I've tried translating the JS found here to PHP, but although it's returning a URL, it's not correct. The notable difference is that I'm not passing an "UploadId" or a "PartNumber".

I've scoured the AWS docs, Google, Stackoverflow, Ask Jeeves, etc and couldn't find a PHP equivalent of the "getSignedUrl" method

public function signPartUpload(Request $request, $uploadId, $partNumber)
{
    $client = new S3Client([
        'version' => 'latest',
        'region'  => 'us-east-1',
    ]);

    $key = $request->has('key') ? $request->get('key') : null;

    if (!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 (!intval($partNumber)) {
        return response()->json(['error' => 's3: the part number must be a number between 1 and 10000.'], 400);
    }

    //Creating a presigned URL
    $cmd = $client->getCommand('PutObject', [
        'Bucket' => 'bucket-name',
        'Key' => $key
    ]);

    $response = $client->createPresignedRequest($cmd, '+20 minutes');
    $presignedUrl = (string)$response->getUri();

    return response()->json(['url' => $presignedUrl]);
}

Any help would be greatly appreciated!


Solution

  • I found the PHP equivalent of the getSignedUrl method.

    $command = $this->client->getCommand('UploadPart', [
        'Bucket'        => $this->bucket,
        'Key'           => $key,
        'PartNumber'    => $partNumber,
        'UploadId'      => $uploadId,
        'Body'          => '',
    ]);
    

    Here's my solution:

    /**
     * Create a pre-signed URL for parts to be uploaded to.
     * @param Request $request
     * @param string $uploadId
     * @param int $partNumber
     * @return JsonResponse
     */
    public function signPartUpload(Request $request, string $uploadId, int $partNumber)
    {
        $key = $request->has('key') ? $request->get('key') : null;
    
        // Check key
        if (! is_string($key)) {
            return response()->json(['error' => 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"'], 400);
        }
    
        // Check part number
        if (! intval($partNumber)) {
            return response()->json(['error' => 's3: the part number must be a number between 1 and 10000.'], 400);
        }
    
        // Create the upload part command and get the pre-signed URL
        try {
            $command = $this->client->getCommand('UploadPart', [
                'Bucket'        => $this->bucket,
                'Key'           => $key,
                'PartNumber'    => $partNumber,
                'UploadId'      => $uploadId,
                'Body'          => '',
            ]);
    
            $presignedUrl = $this->client->createPresignedRequest($command, '+20 minutes');
        } catch (Exception $e) {
            return response()->json(['error' => $e->getMessage()], 400);
        }
    
        // Convert the pre-signed URL to a string
        $presignedUrlString = (string) $presignedUrl->getUri();
    
        return response()->json(['url' => $presignedUrlString]);
    }