配置Uppy以与Laravel/Vue一起使用分段上传 [英] Configuring Uppy to Use Multipart Uploads with Laravel/Vue

查看:197
本文介绍了配置Uppy以与Laravel/Vue一起使用分段上传的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我知道了

这是丢失的一块.清理代码后,我将发布一个答案,以便希望下一个需要处理此问题的可怜灵魂将不必经历与我经历的相同的地狱;)

  $ command = $ client-> getCommand('UploadPart',array('桶'=>存储桶名称",'密钥'=>$键,'PartNumber'=>$ partNumber,'UploadId'=>$ uploadId,'Body'=>'',));$ signedUrl = $ client-> createPresignedRequest($ command,'+20分钟');$ presignedUrl =(字符串)$ signedUrl-> getUri();返回response()-> json(['url'=> $ presignedUrl]); 


我试图弄清楚如何配置服务器以与Uppy一起使用,以通过CompanionUrl选项将分段上传内容上传到AWS S3. https://uppy.io/docs/aws-s3-multipart/#createMultipartUpload-文件.

这就是我想到的路线的地方 https://github.com/transloadit/uppy/issues/1189#issuecomment-445521442 .

我无法弄清楚这一点,我觉得其他人也被卡住了,没有答案,所以我发布了到目前为止想出的内容,试图让Uppy使用Laravel进行分段上传/Vue.


对于Vue组件,我有这个:

 < template>< div>< id ="uppy-trigger";@ click =" isUppyOpen =!isUppyOpen">打开Uppy</a><仪表板模式:uppy ="uppy":open =" isUppyOpen":props =""{trigger:'#uppy-trigger'}"/></div></template>< script>从'@ uppy/core'导入Uppy从'@ uppy/aws-s3-multipart'导入AwsS3Multipart;导入'@ uppy/core/dist/style.css';导入'@ uppy/dashboard/dist/style.css';导出默认值{组件: {仪表板模式":DashboardModal,},数据() {返回 {isUppyOpen:否,}},计算值:{//Uppy实例uppy:()=>新的Uppy({记录器:Uppy.debugLogger}).use(AwsS3Multipart,{限制:4partnerUrl:"https://mysite.local/",}),},beforeDestroy(){this.uppy.close();},}</script> 


然后将我已将其添加到web.php文件中的路由.

 <代码>//AWS S3分段上传路由路线::名称('s3.multipart.')->前缀('s3/multipart')-> group(function(){路线:: post('/',['as'=>'createMultipartUpload','uses'=>'AwsS3MultipartController @ createMultipartUpload']);;路线:: get('{uploadId}',['as'=>'getUploadedParts','uses'=>'AwsS3MultipartController @ getUploadedParts]]);路线:: get('{uploadId}/{partNumber}',['as'=>'signPartUpload','uses'=>'AwsS3MultipartController @ signPartUpload']);路线:: post('{uploadId}/complete',['as'=>'completeMultipartUpload','uses'=>'AwsS3MultipartController @ completeMultipartUpload']);路线:: delete('{uploadId}',['as'=>'abortMultipartUpload','uses'=>'AwsS3MultipartController @ abortMultipartUpload']);}); 

基本上,发生的事情是我将"companionUrl"设置为到"https://mysite.local/",则在将分段上传文件上传到这些路由时,Uppy将发送五个请求,即"https://mysite.local/s3/multipart/createMultipartUpload".


然后我创建了一个控制器来处理请求:

 <代码><?php命名空间App \ Http \ Controllers;使用Aws \ S3 \ S3Client;使用Illuminate \ Http \ Request;AwsS3MultipartController类扩展了Controller{公共功能createMultipartUpload(Request $ request){$ client = new S3Client(['version'=>'最新的','region'=>'us-east-1',]);$ key = $ request-> has('filename')吗?$ request-> get('filename'):null;$ type = $ request-> has('type')吗?$ request-> get('type'):null;如果(!is_string($ key)){return response()-> json(['error'=>'s3:从"getKey"返回的文件名必须是字符串'],500);}如果(!is_string($ type)){return response()-> json(['error'=>'s3:内容类型必须为字符串'],400);}$ response = $ client-> createMultipartUpload(['桶'=>存储桶名称",'密钥'=>$键,'ContentType'=>$ type,'Expires'=>60]);$ mpuKey =!empty($ response ['Key'])吗?$ response ['Key']:null;$ mpuUploadId =!empty($ response ['UploadId'])?$ response ['UploadId']:null;如果(!$ mpuKey ||!$ mpuUploadId){返回response()-> json(['error'=>'无法处理上传请求.'],400);}返回response()-> json(['key'=>$ mpuKey,'uploadId'=>$ mpuUploadId]);}公共函数getUploadedParts($ uploadId){//到目前为止,我还没有配置此路由.返回$ uploadId;}公共功能signPartUpload(请求$ request,$ uploadId,$ partNumber){$ client = new S3Client(['version'=>'最新的','region'=>'us-east-1',]);$ key = $ request->是否有('key')?$ request-> get('key'):null;如果(!is_string($ key)){return response()-> json(['error'=>'s3:对象关键字必须作为查询参数传递.例如:?key = abc.jpg"'],400);}如果(!intval($ partNumber)){return response()-> json(['error'=>'s3:部件号必须是1到10000之间的数字.'],400);}//创建一个预签名的URL.我认为这是不正确的.$ cmd = $ client-> getCommand('PutObject',['桶'=>存储桶名称",'密钥'=>$键,'UploadId'=>$ uploadId,'PartNumber'=>$ partNumber,]);$ response = $ client-> createPresignedRequest($ cmd,'+20分钟');$ presignedUrl =(string)$ response-> getUri();返回response()-> json(['url'=> $ presignedUrl]);}公共功能completeMultipartUpload(Request $ request,$ uploadId){$ client = new S3Client(['version'=>'最新的','region'=>'us-east-1',]);$ key = $ request-> has('key')吗?$ request-> get('key'):null;$ parts = json_decode($ request-> getContent(),true)['parts'];如果(!is_string($ key)){return response()-> json(['error'=>'s3:对象关键字必须作为查询参数传递.例如:?key = abc.jpg"'],400);}if(!is_array($ parts)||!$ this-> arePartsValid($ parts)){return response()-> json(['error'=>'s3:"parts"必须是{ETag,PartNumber}对象的数组.'],400);}//completeMultipartUpload方法失败,并出现以下错误.//"执行" CompleteMultipartUpload"时出错;在``https://the-bucket-name.s3.amazonaws.com/NewProject.png?AWS HTTP错误:客户端错误:`POST https://the-bucket-name.s3.amazonaws.com/NewProject.png?uploadId=nlWLdbNgB9zgarpLBXnj17eOIGAmQM_xyBArymtwdM71fhbFvveggDmL6fz4blz.B95TLhMatDWZRpAQWaQNQFaQWaQRpAQTpAQTpAQWhPtPnPQP1PQP1P1P1B1C4B4B4B4B4B4B4B4B4B4B4B4C//<错误><代码> InvalidPart</代码><消息>找不到一个或多个指定的部分.该零件可能没有被(截断...)//InvalidPart(客户端):找不到一个或多个指定的零件.该部件可能尚未上载,或者指定的实体标签可能与该部件的实体标签不匹配.-<错误><代码> InvalidPart</代码><消息>找不到一个或多个指定部分.该部件可能尚未上载,或者指定的实体标签可能与该部件的"en"不匹配.$ result = $ client-> completeMultipartUpload(['桶'=>存储桶名称",'密钥'=>$键,'UploadId'=>$ uploadId,'MultipartUpload'=>['Parts'=>$份,],]);返回response()-> json(['location'=> $ result ['location']]);}公共函数abortMultipartUpload($ uploadId){//到目前为止,我还没有配置此路由.返回$ uploadId;}私有函数arePartsValid($ parts){//零件的验证将在此处进行,但现在返回true.返回true;}} 



我可以上载纯PHP/服务器端的多部分文件.但是对于大型文件,这将无法正常工作,因为我将不得不等待服务器上的上传完成,然后再将其上传到AWS中.

 <代码> $ s3_client = new S3Client(['version'=>'最新的','region'=>'us-east-1',]);$ bucket ='桶名称';$ tmp_name = $ request-> file('file')-> getPathname();$ folder = Carbon :: now()-> format('Y/m/d/');$ filename = pathinfo($ request-> file('file')-> getClientOriginalName(),PATHINFO_FILENAME);$ extension = $ extension = pathinfo($ request-> file('file')-> getClientOriginalName(),PATHINFO_EXTENSION);$ timestamp = Carbon :: now()-> format('H-i-s');$ name =" {$ folder} {$ filename} _ {$ timestamp}.{$ extension}" ;;$ response = $ s3_client-> createMultipartUpload(['桶'=>$ bucket,'密钥'=>$ name,]);$ uploadId = $ response ['UploadId'];$ file = fopen($ tmp_name,'r');$ parts = [];$ partNumber = 1;while(!feof($ file)){$ result = $ s3_client-> uploadPart(['桶'=>$ bucket,'密钥'=>$ name,'UploadId'=>$ uploadId,'PartNumber'=>$ partNumber,'Body'=>fread($ file,5 * 1024 * 1024),]);$ parts [] = ['PartNumber'=>$ partNumber ++,'ETag'=>$ result ['ETag'],];}$ result = $ s3_client-> completeMultipartUpload(['桶'=>$ bucket,'密钥'=>$ name,'UploadId'=>$ uploadId,'MultipartUpload'=>['Parts'=>$份,],]); 

我认为正在发生的事情是Uppy正在处理客户端的 while 循环部分.为此,我必须返回Uppy可以使用的预签名URL,但是我当前返回的预签名URL不正确.

我注意到的一件事是,当我在完全启动服务器端的分段上传时逐步执行while循环时,直到激发 completeMultipartUpload 方法之前,没有文件上传到我的存储桶.但是,如果我逐步浏览通过Uppy上传的部分,这些部分似乎将作为最终文件上传,并且每个部分都将覆盖先前的部分.然后剩下文件的一部分,即43.5MB文件的最后3.5MB.

解决方案

这就是我如何让Uppy,Vue和Laravel一起好玩的方式.

Vue组件:

 < template>< div>< id ="uppy-trigger";@ click =" isUppyOpen =!isUppyOpen">打开Uppy</a><仪表板模式:uppy ="uppy":open =" isUppyOpen":props =""{trigger:'#uppy-trigger'}"/></div></template>< script>从'@ uppy/core'导入Uppy从'@ uppy/aws-s3-multipart'导入AwsS3Multipart;导入'@ uppy/core/dist/style.css';导入'@ uppy/dashboard/dist/style.css';导出默认值{组件: {仪表板模式":DashboardModal,},数据() {返回 {isUppyOpen:否,}},计算值:{//Uppy实例uppy:()=>新的Uppy({记录器:Uppy.debugLogger}).use(AwsS3Multipart,{限制:4partnerUrl:"https://mysite.local/",}),},beforeDestroy(){this.uppy.close();},}</script> 

路由:

 <代码>//AWS S3分段上传路由路线::名称('s3.multipart.')->前缀('s3/multipart')-> group(function(){路线:: post('/',['as'=>'createMultipartUpload','uses'=>'AwsS3MultipartController @ createMultipartUpload']);;路线:: get('{uploadId}',['as'=>'getUploadedParts','uses'=>'AwsS3MultipartController @ getUploadedParts]]);路线:: get('{uploadId}/{partNumber}',['as'=>'signPartUpload','uses'=>'AwsS3MultipartController @ signPartUpload']);路线:: post('{uploadId}/complete',['as'=>'completeMultipartUpload','uses'=>'AwsS3MultipartController @ completeMultipartUpload']);路线:: delete('{uploadId}',['as'=>'abortMultipartUpload','uses'=>'AwsS3MultipartController @ abortMultipartUpload']);}); 

控制器:

 <代码><?php命名空间App \ Http \ Controllers;使用Aws \ S3 \ S3Client;使用碳\碳;使用Exception;使用Illuminate \ Http \ Request;AwsS3MultipartController类扩展了Controller{私人的$ bucket;私人$ client;公共功能__construct(){$ this-> bucket ='bucket的名称';$ this-> client = new S3Client(['version'=>'最新的','region'=>'us-east-1',]);}/***创建/启动分段上传* @param请求$ request* @return JsonResponse*/公共功能createMultipartUpload(Request $ request){//获取文件名并从请求中键入$ filename = $ request-> has('filename')吗?$ request-> get('filename'):null;$ type = $ request-> has('type')吗?$ request-> get('type'):null;//检查文件名如果(!is_string($ filename)){return response()-> json(['error'=>'s3:从"getKey"返回的文件名必须是字符串'],500);}//检查类型如果(!is_string($ type)){return response()-> json(['error'=>'s3:内容类型必须为字符串'],400);}//设置等于YYYY/MM/DD/filename_H-i-s.ext的密钥$ fileBaseName = pathinfo($ filename,PATHINFO_FILENAME);$ extension = pathinfo($ filename,PATHINFO_EXTENSION);$ folder = Carbon :: now()-> format('Y/m/d/');$ timestamp = Carbon :: now()-> format('H-i-s');$ key =" {$ folder} {$ fileBaseName} _ {$ timestamp}.{$ extension}" ;;//创建/启动分段上传尝试 {$ response = $ this->客户端-> createMultipartUpload(['桶'=>$ this->桶,'密钥'=>$键,'ContentType'=>$ type,'Expires'=>60]);} catch(Exception $ e){return response()-> json(['error'=> $ e-> getMessage()],400);}//分段上传密钥和ID$ mpuKey =!empty($ response ['Key'])吗?$ response ['Key']:null;$ mpuUploadId =!empty($ response ['UploadId'])?$ response ['UploadId']:null;//检查分段上传密钥和ID如果(!$ mpuKey ||!$ mpuUploadId){返回response()-> json(['error'=>'无法处理上传请求.'],400);}返回response()-> json(['key'=>$ mpuKey,'uploadId'=>$ mpuUploadId]);}/***获取已上传的零件* @param请求$ request* @param字符串$ uploadId* @return JsonResponse*/公共函数getUploadedParts(请求$ request,字符串$ uploadId){$ key = $ request->是否有('key')?$ request-> get('key'):null;//确认键如果(!is_string($ key)){return response()-> json(['error'=>'s3:对象关键字必须作为查询参数传递.例如:?key = abc.jpg"'],400);}$ parts = [];$ getParts = true;$ startAt = 0;//到目前为止上传零件而($ getParts){$ partsPage = $ this-> listPartsPage($ key,$ uploadId,$ startAt,$ parts);如果(isset($ partsPage ['error'])){返回response()-> json(['error'=> $ partsPage ['error']],400);}如果($ partsPage ['isTruncated']){$ startAt = $ partsPage ['nextPartNumberMarker'];} 别的 {$ getParts = false;}}返回response()-> json($份,);}/***为要上传到的零件创建一个预签名的URL* @param请求$ request* @param字符串$ uploadId* @param int $ partNumber* @return JsonResponse*/公共功能signPartUpload(Request $ request,字符串$ uploadId,int $ partNumber){$ key = $ request->是否有('key')?$ request-> get('key'):null;//确认键如果(!is_string($ key)){return response()-> json(['error'=>'s3:对象关键字必须作为查询参数传递.例如:?key = abc.jpg"'],400);}//检查零件号如果(!intval($ partNumber)){返回response()-> json(['error'=>'s3:部件号必须是1到10000之间的数字.'],400);}//创建上载部分命令并获取预签名的URL尝试 {$ command = $ this-> client-> getCommand('UploadPart',['桶'=>$ this->桶,'密钥'=>$键,'PartNumber'=>$ partNumber,'UploadId'=>$ uploadId,'Body'=>'',]);$ presignedUrl = $ this->客户端-> createPresignedRequest($ command,'+20分钟');} catch(Exception $ e){return response()-> json(['error'=> $ e-> getMessage()],400);}//将预签名的URL转换为字符串$ presignedUrlString =(字符串)$ presignedUrl-> getUri();返回response()-> json(['url'=> $ presignedUrlString]);}/***完成分段上传* @param请求$ request* @param字符串$ uploadId* @return JsonResponse*/公共函数completeMultipartUpload(请求$ request,字符串$ uploadId){$ key = $ request->是否有('key')?$ request-> get('key'):null;$ parts = json_decode($ request-> getContent(),true)['parts'];//检查钥匙如果(!is_string($ key)){return response()-> json(['error'=>'s3:对象关键字必须作为查询参数传递.例如:?key = abc.jpg"'],400);}//检查零件if(!is_array($ parts)||!$ this-> arePartsValid($ parts)){return response()-> json(['error'=>'s3:"parts"必须是{ETag,PartNumber}对象的数组.'],400);}//完成分段上传尝试 {$ result = $ this-> client-> completeMultipartUpload(['桶'=>$ this->桶,'密钥'=>$键,'UploadId'=>$ uploadId,'MultipartUpload'=>['Parts'=>$份,],]);} catch(Exception $ e){return response()-> json(['error'=> $ e-> getMessage()],400);}//将正斜杠实体更改为正斜杠$ location = urldecode($ result ['Location']);返回response()-> json(['location'=> $ location]);}公共函数abortMultipartUpload(Request $ request,$ uploadId){$ key = $ request->是否有('key')?$ request-> get('key'):null;//检查钥匙如果(!is_string($ key)){return response()-> json(['error'=>'s3:对象关键字必须作为查询参数传递.例如:?key = abc.jpg"'],400);}//取消分段上传尝试 {$ response = $ this-> client-> abortMultipartUpload(['桶'=>$ this->桶,'密钥'=>$键,'UploadId'=>$ uploadId,]);} catch(Exception $ e){//}返回response()-> json();}私有函数listPartsPage(字符串$ key,字符串$ uploadId,整数$ startAt,数组& $ parts){//配置响应$ response = ['isTruncated'=>错误的,];//获取上传的零件清单尝试 {$ result = $ this-> client-> listParts(['桶'=>$ this->桶,'密钥'=>$键,'PartNumberMarker'=>$ startAt,'UploadId'=>$ uploadId,]);} catch(Exception $ e){return ['error'=>'s3:无法继续上传.上载可能已中止.'];}//将找到的零件添加到零件数组if($ result-> hasKey('Parts')){array_push($ parts,... $ result-> get('Parts'));}//检查零件是否被截断if($ result-> hasKey('IsTruncated')&& $ result-> get('IsTruncated')){$ response ['isTruncated'] = true;$ response ['nextPartNumberMarker'] = $ result-> get('NextPartNumberMarker');}返回$ response;}/***验证分段上传的部分* @param array $ parts具有PartNumber和ETag的零件的关联数组* @返回布尔*/私有函数arePartsValid(array $ parts){如果(!is_array($ parts)){返回false;}foreach($ parts as $ part){如果(!is_int($ part ['PartNumber'])||!is_string($ part ['ETag'])){返回false;}}返回true;}} 

I figured it out

This was the missing piece. Once I clean up my code, I'll post an answer so that hopefully the next poor soul that has to deal with this will not have to go through the same hell I went through ;)

$command = $client->getCommand('UploadPart', array(
    'Bucket' => 'the-bucket-name',
    'Key' => $key,
    'PartNumber' => $partNumber,
    'UploadId' => $uploadId,
    'Body' => '',
));

$signedUrl = $client->createPresignedRequest($command, '+20 minutes');
$presignedUrl = (string)$signedUrl->getUri();
return response()->json(['url' => $presignedUrl]);


I'm trying to figure out how to configure my server to work with Uppy for uploading multipart uploads to AWS S3 by using the CompanionUrl option. https://uppy.io/docs/aws-s3-multipart/#createMultipartUpload-file.

This is where I got the idea to go this route https://github.com/transloadit/uppy/issues/1189#issuecomment-445521442.

I can't figure this out and I feel like others have been stuck as well with no answer, so I'm posting what I've come up with so far in trying to get Uppy to work with multipart uploads using Laravel/Vue.


For the Vue component I have this:

<template>
<div>
    <a id="uppy-trigger" @click="isUppyOpen = !isUppyOpen">Open Uppy</a>

    <dashboard-modal
        :uppy="uppy"
        :open="isUppyOpen"
        :props="{trigger: '#uppy-trigger'}"
    />
</div>
</template>

<script>
import Uppy from '@uppy/core'
import AwsS3Multipart from '@uppy/aws-s3-multipart';
import '@uppy/core/dist/style.css';
import '@uppy/dashboard/dist/style.css';

export default {
    components: {
        'dashboard-modal': DashboardModal,
    },

    data() {
        return {
            isUppyOpen: false,
        }
    },

    computed: {
        // Uppy Instance
        uppy: () => new Uppy({
            logger: Uppy.debugLogger
        }).use(AwsS3Multipart, {
            limit: 4,
            companionUrl: 'https://mysite.local/',
        }),
    },

    beforeDestroy () {
        this.uppy.close();
    },
}
</script>


Then for the routing I've added this to my web.php file.

// AWS S3 Multipart Upload Routes
Route::name('s3.multipart.')->prefix('s3/multipart')
    ->group(function () {
        Route::post('/', ['as' => 'createMultipartUpload', 'uses' => 'AwsS3MultipartController@createMultipartUpload']);
        Route::get('{uploadId}', ['as' => 'getUploadedParts', 'uses' => 'AwsS3MultipartController@getUploadedParts']);
        Route::get('{uploadId}/{partNumber}', ['as' => 'signPartUpload', 'uses' => 'AwsS3MultipartController@signPartUpload']);
        Route::post('{uploadId}/complete', ['as' => 'completeMultipartUpload', 'uses' => 'AwsS3MultipartController@completeMultipartUpload']);
        Route::delete('{uploadId}', ['as' => 'abortMultipartUpload', 'uses' => 'AwsS3MultipartController@abortMultipartUpload']);
    });

Basically what is happening is that I've set the "companionUrl" to "https://mysite.local/", then Uppy will send five requests when uploading a multipart upload file to these routes, ie "https://mysite.local/s3/multipart/createMultipartUpload".


I then created a controller to handle the requests:

<?php

namespace App\Http\Controllers;

use Aws\S3\S3Client;
use Illuminate\Http\Request;

class AwsS3MultipartController extends Controller
{
    public function createMultipartUpload(Request $request)
    {
        $client = new S3Client([
            'version' => 'latest',
            'region'  => 'us-east-1',
        ]);

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

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

        if (!is_string($key)) {
            return response()->json(['error' => 's3: filename returned from "getKey" must be a string'], 500);
        }

        if (!is_string($type)) {
            return response()->json(['error' => 's3: content type must be a string'], 400);
        }

        $response = $client->createMultipartUpload([
            'Bucket'        => 'the-bucket-name',
            'Key'           => $key,
            'ContentType'   => $type,
            'Expires'       => 60
        ]);

        $mpuKey = !empty($response['Key']) ? $response['Key'] : null;
        $mpuUploadId = !empty($response['UploadId']) ? $response['UploadId'] : null;

        if (!$mpuKey || !$mpuUploadId) {
            return response()->json(['error' => 'Unable to process upload request.'], 400);
        }

        return response()->json([
            'key'       => $mpuKey,
            'uploadId'  => $mpuUploadId
        ]);
    }

    public function getUploadedParts($uploadId)
    {
        // Haven't configured this route yet as I haven't made it this far.
        return $uploadId;
    }

    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. I don't think this is correct.
        $cmd = $client->getCommand('PutObject', [
            'Bucket'        => 'the-bucket-name',
            'Key'           => $key,
            'UploadId'      => $uploadId,
            'PartNumber'    => $partNumber,
        ]);

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

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

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

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

        $parts = json_decode($request->getContent(), true)['parts'];

        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 (!is_array($parts) || !$this->arePartsValid($parts)) {
            return response()->json(['error' => 's3: "parts" must be an array of {ETag, PartNumber} objects.'], 400);
        }


        // The completeMultipartUpload method fails with the following error.

        // "Error executing "CompleteMultipartUpload" on "https://the-bucket-name.s3.amazonaws.com/NewProject.png?uploadId=nlWLdbNgB9zgarpLBXnj17eOIGAmQM_xyBArymtwdM71fhbFvveggDmL6fz4blz.B95TLhMatDvodbMb5p2ZMKqdlLeLFoSW1qcu33aRQTlt6NbiP_dkDO90DFO.pWGH"; AWS HTTP error: Client error: `POST https://the-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"

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

        return response()->json(['location' => $result['location']]);
    }

    public function abortMultipartUpload($uploadId)
    {
        // Haven't configured this route yet as I haven't made it this far.
        return $uploadId;
    }

    private function arePartsValid($parts)
    {
        // Validation for the parts will go here, but returning true for now.
        return true;
    }
}



I can upload a multipart file fine purely PHP/server-side. For huge files though, this isn't going to work though since I would have to wait for the upload to finish on my server, then upload it to AWS in the parts.

$s3_client = new S3Client([
    'version' => 'latest',
    'region'  => 'us-east-1',
]);
$bucket = 'the-bucket-name';
$tmp_name = $request->file('file')->getPathname();
$folder = Carbon::now()->format('Y/m/d/');
$filename = pathinfo($request->file('file')->getClientOriginalName(), PATHINFO_FILENAME);
$extension = $extension = pathinfo($request->file('file')->getClientOriginalName(), PATHINFO_EXTENSION);
$timestamp = Carbon::now()->format('H-i-s');
$name = "{$folder}{$filename}_{$timestamp}.{$extension}";

$response = $s3_client->createMultipartUpload([
    'Bucket' => $bucket,
    'Key'    => $name,
]);

$uploadId = $response['UploadId'];

$file = fopen($tmp_name, 'r');
$parts = [];
$partNumber = 1;
while (! feof($file)) {
    $result = $s3_client->uploadPart([
        'Bucket'     => $bucket,
        'Key'        => $name,
        'UploadId'   => $uploadId,
        'PartNumber' => $partNumber,
        'Body'       => fread($file, 5 * 1024 * 1024),
    ]);

    $parts[] = [
        'PartNumber' => $partNumber++,
        'ETag'       => $result['ETag'],
    ];
}

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

What I believe is happening is that Uppy is handling the while loop part client-side. In order to do that, I have to return a pre-signed URL Uppy can use, but the pre-signed URL I'm currently returning isn't correct.

One thing I noted is that when I step through the while loop when initiating the multipart upload purely server-side, no file is uploaded to my bucket until the completeMultipartUpload method is fired. If however, I step through the parts being uploaded via Uppy, the parts seem to be being uploaded as the final file and each part is just overwriting the previous part. I'm then left with a fragment of the file, ie the last 3.5MB of a 43.5MB file.

解决方案

Here's how I was able to get Uppy, Vue, and Laravel to play nicely together.

The Vue Component:

<template>
<div>
    <a id="uppy-trigger" @click="isUppyOpen = !isUppyOpen">Open Uppy</a>

    <dashboard-modal
        :uppy="uppy"
        :open="isUppyOpen"
        :props="{trigger: '#uppy-trigger'}"
    />
</div>
</template>

<script>
import Uppy from '@uppy/core'
import AwsS3Multipart from '@uppy/aws-s3-multipart';
import '@uppy/core/dist/style.css';
import '@uppy/dashboard/dist/style.css';

export default {
    components: {
        'dashboard-modal': DashboardModal,
    },

    data() {
        return {
            isUppyOpen: false,
        }
    },

    computed: {
        // Uppy Instance
        uppy: () => new Uppy({
            logger: Uppy.debugLogger
        }).use(AwsS3Multipart, {
            limit: 4,
            companionUrl: 'https://mysite.local/',
        }),
    },

    beforeDestroy () {
        this.uppy.close();
    },
}
</script>

The Routing:

// AWS S3 Multipart Upload Routes
Route::name('s3.multipart.')->prefix('s3/multipart')
    ->group(function () {
        Route::post('/', ['as' => 'createMultipartUpload', 'uses' => 'AwsS3MultipartController@createMultipartUpload']);
        Route::get('{uploadId}', ['as' => 'getUploadedParts', 'uses' => 'AwsS3MultipartController@getUploadedParts']);
        Route::get('{uploadId}/{partNumber}', ['as' => 'signPartUpload', 'uses' => 'AwsS3MultipartController@signPartUpload']);
        Route::post('{uploadId}/complete', ['as' => 'completeMultipartUpload', 'uses' => 'AwsS3MultipartController@completeMultipartUpload']);
        Route::delete('{uploadId}', ['as' => 'abortMultipartUpload', 'uses' => 'AwsS3MultipartController@abortMultipartUpload']);
    });

The Controller:

<?php

namespace App\Http\Controllers;

use Aws\S3\S3Client;
use Carbon\Carbon;
use Exception;
use Illuminate\Http\Request;

class AwsS3MultipartController extends Controller
{
    private $bucket;
    private $client;

    public function __construct()
    {
        $this->bucket = 'the-name-of-the-bucket';

        $this->client = new S3Client([
            'version' => 'latest',
            'region'  => 'us-east-1',
        ]);
    }

    /**
     * Create/initiate the multipart upload
     * @param Request $request 
     * @return JsonResponse 
     */
    public function createMultipartUpload(Request $request)
    {
        // Get the filename and type from request
        $filename = $request->has('filename') ? $request->get('filename') : null;
        $type = $request->has('type') ? $request->get('type') : null;

        // Check filename
        if (!is_string($filename)) {
            return response()->json(['error' => 's3: filename returned from "getKey" must be a string'], 500);
        }

        // Check type
        if (!is_string($type)) {
            return response()->json(['error' => 's3: content type must be a string'], 400);
        }

        // Set up key equal to YYYY/MM/DD/filename_H-i-s.ext
        $fileBaseName = pathinfo($filename, PATHINFO_FILENAME);
        $extension = pathinfo($filename, PATHINFO_EXTENSION);
        $folder = Carbon::now()->format('Y/m/d/');
        $timestamp = Carbon::now()->format('H-i-s');
        $key = "{$folder}{$fileBaseName}_{$timestamp}.{$extension}";

        // Create/initiate the multipart upload
        try {
            $response = $this->client->createMultipartUpload([
                'Bucket'        => $this->bucket,
                'Key'           => $key,
                'ContentType'   => $type,
                'Expires'       => 60
            ]);
        } catch (Exception $e) {
            return response()->json(['error' => $e->getMessage()], 400);
        }

        // Multipart upload key and id
        $mpuKey = !empty($response['Key']) ? $response['Key'] : null;
        $mpuUploadId = !empty($response['UploadId']) ? $response['UploadId'] : null;

        // Check multipart upload key and id
        if (!$mpuKey || !$mpuUploadId) {
            return response()->json(['error' => 'Unable to process upload request.'], 400);
        }

        return response()->json([
            'key'       => $mpuKey,
            'uploadId'  => $mpuUploadId
        ]);
    }

    /**
     * Get parts that have been uploaded
     * @param Request $request 
     * @param string $uploadId 
     * @return JsonResponse 
     */
    public function getUploadedParts(Request $request, string $uploadId)
    {
        $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);
        }

        $parts = [];
        $getParts = true;
        $startAt = 0;

        // Get parts uploaded so far
        while ($getParts) {
            $partsPage = $this->listPartsPage($key, $uploadId, $startAt, $parts);

            if (isset($partsPage['error'])) {
                return response()->json(['error' => $partsPage['error']], 400);
            }

            if ($partsPage['isTruncated']) {
                $startAt = $partsPage['nextPartNumberMarker'];
            } else {
                $getParts = false;
            }
        }

        return response()->json(
            $parts,
        );
    }

    /**
     * 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]);
    }

    /**
     * Complete the multipart upload
     * @param Request $request 
     * @param string $uploadId 
     * @return JsonResponse 
     */
    public function completeMultipartUpload(Request $request, string $uploadId)
    {
        $key = $request->has('key') ? $request->get('key') : null;

        $parts = json_decode($request->getContent(), true)['parts'];

        // Check the 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 the parts
        if (!is_array($parts) || !$this->arePartsValid($parts)) {
            return response()->json(['error' => 's3: "parts" must be an array of {ETag, PartNumber} objects.'], 400);
        }

        // Complete the multipart upload
        try {
            $result = $this->client->completeMultipartUpload([
                'Bucket'          => $this->bucket,
                'Key'             => $key,
                'UploadId'        => $uploadId,
                'MultipartUpload' => [
                    'Parts' => $parts,
                ],
            ]);
        } catch (Exception $e) {
            return response()->json(['error' => $e->getMessage()], 400);
        }

        // Change forwardslash entities to forwardslashes
        $location = urldecode($result['Location']);

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

    public function abortMultipartUpload(Request $request, $uploadId)
    {
        $key = $request->has('key') ? $request->get('key') : null;

        // Check the 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);
        }

        // Cancel the multipart upload
        try {
            $response = $this->client->abortMultipartUpload([
                'Bucket' => $this->bucket,
                'Key' => $key,
                'UploadId' => $uploadId,
            ]);
        } catch (Exception $e) {
            //
        }

        return response()->json();
    }

    private function listPartsPage(string $key, string $uploadId, int $startAt, array &$parts)
    {
        // Configure response
        $response = [
            'isTruncated' => false,
        ];

        // Get list of parts uploaded
        try {
            $result = $this->client->listParts([
                'Bucket'            => $this->bucket,
                'Key'               => $key,
                'PartNumberMarker'  => $startAt,
                'UploadId'          => $uploadId,
            ]);
        } catch (Exception $e) {
            return ['error' => 's3: unable to continue upload. The upload may have been aborted.'];
        }

        // Add found parts to parts array
        if ($result->hasKey('Parts')) {
            array_push($parts, ...$result->get('Parts'));
        }

        // Check if parts are truncated
        if ($result->hasKey('IsTruncated') && $result->get('IsTruncated')) {
            $response['isTruncated'] = true;
            $response['nextPartNumberMarker'] = $result->get('NextPartNumberMarker');
        }

        return $response;
    }

    /**
     * Validate the parts for the multipart upload
     * @param array $parts An associative array of parts with PartNumber and ETag
     * @return bool 
     */
    private function arePartsValid(array $parts)
    {
        if (!is_array($parts)) {
            return false;
        }

        foreach ($parts as $part) {
            if (!is_int($part['PartNumber']) || !is_string($part['ETag'])) {
                return false;
            }
        }

        return true;
    }
}

这篇关于配置Uppy以与Laravel/Vue一起使用分段上传的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆