大概思路:[序号1、2需要同步执行]
- 创建上传任务 [存储一些文件的重要信息, 如果之前有存储 就判断是否完成或者需要断点续传]
- 开始上传. [取出第一步存储的文件信息,进行单片上传->上传完成进行合并->返回上传成功的url路径.]
1.创建上传任务
/**
* 描述:创建上传任务
* 函数名: createUploadTask
* 创建人:wenhao
* 创建时间:2020/11/19 下午3:21
* @param Request $request
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response
* @throws CustomException
* @throws \App\Exceptions\IncorrectParamsException
*/
public function createUploadTask(Request $request)
{
// 文件md5
$md5 = $request->post('md5', "");
//文件名(不带后缀)
$fileName = $request->post('fileName', "");
//文件后缀
$ext = $request->post('ext', "");
//文件夹id(缺省或为空这默认去主文件夹)
$folderId = $request->post('folderId', ""); //文件mime类型
$mimeType = $request->post('mimeType', ""); //文件大小(字节)
$fileSize = $request->post('fileSize', 0); //上传类型
$uploadType = $request->post('uploadType', "");
//视频缩略图
$vodCover = (string)$request->post('vodCover', "");
//视频时长。
$vodDuration = (int)$request->post('vodDuration', 0);
if (empty($md5) || empty($fileName) || empty($ext) || empty($mimeType) || empty($fileSize) || empty($uploadType)) {
throw new IncorrectParamsException();
}
$result = UploadService::createUploadTask(self::$userId, $md5, $fileName, $ext, $uploadType, $mimeType, $fileSize, $folderId, $vodCover, $vodDuration);
return $this->success(UploadResources::make($result));
}
/**
* 描述:创建上传任务服务.
* 函数名: createUploadTask
* 创建人:wenhao
* 创建时间:2020/11/18 下午3:37
* @param int $userId 用户id
* @param string $fileMd5 文件md5
* @param string $fileName 文件名字
* @param string $ext 文件后缀
* @param string $mimeType 文件mime 类型
* // * @param int $chunkSize 分片大小(字节)
* // * @param int $chunkNum 分片个数
* @param int $fileSize 文件大小(字节)
* @param string $folderId 文件夹id(缺省或为空这默认去主文件夹)
* @param string $vodCover 视频缩略图
* @param string $vodDuration 视频时长
* @return array
* @throws CustomException
* @throws IncorrectParamsException
*/
public static function createUploadTask(int $userId, string $fileMd5, string $fileName, string $ext, string $uploadType, string $mimeType, int $fileSize, string $folderId, string $vodCover, int $vodDuration): array
{
/**************参数处理区域****************/
//检测上传类型是否存在
if (!in_array($uploadType, self::$uploadType) || empty($fileMd5)) {
throw new IncorrectParamsException();
}
//检测用户是否有权限上传[容量是否足够等]
switch ($uploadType) {
//如果是网盘文件
case "panFile":
$canUseSpace = UserService::getUserCanUseSpace($userId);
if ($canUseSpace < $fileSize) {
throw new CustomException(405024);
}
break;
}
//传入文件夹为空值,则获取主目录.
if (empty($folderId)) {
$folderId = FolderService::getHomeFolderIdByUser();
}
$chunkSize = 5242880; //分片大小为5M
//分片个数 分片大小 由服务端控制.
$chunkNum = ceil($fileSize / $chunkSize);
/**************参数处理区域结束****************/
//定义返回值:
//status=1的时候用到的字段有fileMd5
//status=2的时候用到的字段有fileMd5、notUploadChunks
//status=3的时候用到的字段有fileMd5、fileUrl
//status=4的时候用到的字段有fileMd5
$returnResult = [
'status' => 0, //状态:1=文件任务创建完成状态 2=文件任务可以断点续传状态 3=文件整体上传完成 此状态返回可供访问的文件url 4=文件切片上传完成
'fileMd5' => $fileMd5, //文件的唯一标识[md5]
'notUploadChunks' => [], //未上传的分片 片数
'fileUrl' => '', //文件的url
'chunkSize' => $chunkSize, //分片大小
'chunkNum' => $chunkNum, //分片个数
'folderId' => $folderId, //文件夹id
'uploadType' => $uploadType //文件夹id
];
//检查前端是否是小文件上传.
if ($chunkNum == 1) {
$uploadStatus = Pan::SMALL_FILE_TASK_CREATEOK_STATUS;
$saveCacheData = [];
//定义缓存存储信息
$saveCacheData['uploadTaskInfo'] = json_encode([
'file_name' => $fileName,
'folder_id' => $folderId,
'user_id' => $userId,
'ext' => $ext,
'md5' => $fileMd5,
'size' => $fileSize,
'type' => self::getFileTypeByExt($ext),
'mime' => $mimeType,
'upload_type' => $uploadType,
'vod_cover' => $vodCover,
'vod_duration' => $vodDuration,
'upload_status' => $uploadStatus
]);
Upload::hashSetUploadTask($saveCacheData, $userId, $fileMd5);
//如果文件大小小于切片大小 那么就引导前端走小文件上传接口.
$returnResult['status'] = $uploadStatus;
return $returnResult;
}
/**************检查是否可以断点续传和上传完毕待合并区域****************/
//获取任务信息.
$uploadTaskHashInfo = Upload::hashGetUoploadTask($userId, $fileMd5);
$uploadTaskInfo = $uploadTaskHashInfo['uploadTaskInfo'] ?? [];
$chunkUploadInfo = $uploadTaskHashInfo['chunkUploadInfo'] ?? [];
//获取已经分好的片段数量
$chunkOkCount = count($chunkUploadInfo);
if (!empty($uploadTaskInfo) && $chunkOkCount > 0 && $uploadTaskInfo['chunk_num'] != $chunkOkCount) {
//可以断点续传
//计算出1-N的数字到数组中
$completeChunkNum = [];
for ($i = 1; $i <= $uploadTaskInfo['chunk_num']; $i++) {
array_unshift($completeChunkNum, $i);
}
//已经完成的切片
$alreadyOk = [];
foreach ($chunkUploadInfo as $k => $v) {
array_unshift($alreadyOk, intval($v));
}
//未完成的切片[取差集]
$noOk = array_diff($completeChunkNum, $alreadyOk);
$returnResult['status'] = Pan::FILE_TASK_DDXC_STATUS;
$returnResult['notUploadChunks'] = $noOk;
return $returnResult;
} else if (!empty($uploadTaskInfo) && $uploadTaskInfo['chunk_num'] == $chunkOkCount) {
//全部上传完成 待合并
$url = self::chunkUploadFinishReport($userId, $uploadTaskInfo['md5'], $uploadType);
$returnResult['status'] = Pan::FILE_TASK_UPLOAD_OK;
$returnResult['fileUrl'] = $url;
return $returnResult;
}
/**************检查是否可以断点续传和上传完毕待合并区域结束****************/
if (empty($uploadTaskInfo)) {
$kkyResult = KkyOssService::createMultipartUpload($ext, $mimeType);
$uploadStatus = Pan::FILE_TASK_CREATEOK_STATUS;
$info = pathinfo($kkyResult['key']);
$saveCacheData = [];
//定义缓存存储信息
$saveCacheData['uploadTaskInfo'] = json_encode([
'file_name' => $fileName,
'folder_id' => $folderId,
'user_id' => $userId,
'ext' => $ext,
'md5' => $fileMd5,
'size' => $fileSize,
'type' => self::getFileTypeByExt($ext),
'mime' => $mimeType,
'upload_id' => $kkyResult['uploadId'],
'chunk_num' => $chunkNum,
'chunk_size' => $chunkSize,
'store_name' => $info['filename'],
'store_path' => $info['dirname'] . '/',
'upload_type' => $uploadType,
'vod_cover' => $vodCover,
'vod_duration' => $vodDuration,
'upload_status' => $uploadStatus
]);
Upload::hashSetUploadTask($saveCacheData, $userId, $fileMd5);
}
//定义返回值
$returnResult['status'] = Pan::FILE_TASK_CREATEOK_STATUS;
return $returnResult;
}
public static function hashSetUploadTask(array $saveData,int $userId,string $fileMd5)
{
$redisKey = sprintf(RedisKey::KEY_UPLOAD_TASK_USER_FILEMD5, $userId, $fileMd5);
$result = RedisService::hmset($redisKey,$saveData);
RedisService::expire($redisKey, 36000);
return $result;
}
2.文件上传[需要完成第一步的创建上传任务.]
/**
* 描述:文件上传[切片上传|小文件上传]
* 函数名: upload
* 创建人:wenhao
* 创建时间:2020/11/19 下午3:21
* @param Request $request
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response
* @throws CustomException
* @throws ResoursesNotExistException
* @throws \App\Exceptions\ServerErrorException
* @throws \App\Exceptions\InvalidParamsException
*/
public function upload(Request $request)
{
//文件的md5
$fileMd5 = $request->post('fileMd5');
//1.先检查该文件是否有创建其上传任务;
//获取上传任务hash缓存.
$uploadTaskHashInfo = Upload::hashGetUoploadTask(self::$userId, $fileMd5);
//上传任务信息
$uploadTaskInfo = $uploadTaskHashInfo['uploadTaskInfo'] ?? [];
if (empty($uploadTaskInfo)) {
throw new CustomException('405023');
}
//上传的状态 从创建上传任务接口拿
$uploadStatus = (int)$uploadTaskInfo['upload_status'];
//文件夹id
$folderId = $uploadTaskInfo['folder_id'];
//第几个分片
$index = (int)$request->post('index', 1);
$uploadFile = $request->file('file');
//上传类型
$uploadType = $uploadTaskInfo['upload_type'];
//视频缩略图
$vodCover = $uploadTaskInfo['vod_cover'] ?? '';
//视频时长。
$vodDuration = intval($uploadTaskInfo['vod_duration'] ?? 0);
if (empty($uploadStatus) || empty($index) || empty($uploadType) || empty($uploadFile)) {
throw new IncorrectParamsException();
}
switch ($uploadStatus) {
case 5:
//小文件上传.
$result = UploadService::smallFileUpload(self::$userId, $folderId, $uploadType, $uploadFile, $vodCover, $vodDuration,$fileMd5);
break;
default:
$result = UploadService::chunkUpload(self::$userId, $fileMd5, $index, $uploadType, $uploadFile);
}
return $this->success(UploadResources::make($result));
}
/**
* 描述:分片上传服务
* 函数名: chunkUpload
* 创建人:wenhao
* 创建时间:2020/11/18 下午3:46
* @param int $userId 用户id
* @param string $fileMd5 文件md5
* @param int $index 当前上传到第几片
* @param string $uploadType 上传类型
* @param $file
* @return mixed
* @throws CustomException
* @throws ResoursesNotExistException
* @throws \App\Exceptions\ServerErrorException
* @throws InvalidParamsException
*/
public static function chunkUpload(int $userId, string $fileMd5, int $index, string $uploadType, $file)
{
//获取上传任务hash缓存.
$uploadTaskHashInfo = Upload::hashGetUoploadTask($userId, $fileMd5);
//上传任务信息
$uploadTaskInfo = $uploadTaskHashInfo['uploadTaskInfo'] ?? [];
//切片信息
$chunkUploadInfo = $uploadTaskHashInfo['chunkUploadInfo'] ?? [];
if (empty($uploadTaskInfo)) {
throw new ResoursesNotExistException();
}
//检测$index是否大于总切片数量
if ($index > $uploadTaskInfo['chunk_num']) {
throw new InvalidParamsException();
}
//定义该服务的统一返回格式
$returnResult = [
'status' => 0, //状态:1=文件任务创建完成状态 2=文件任务可以断点续传状态 3=文件整体上传完成 此状态返回可供访问的文件url 4=文件切片上传完成
'fileUrl' => '',
];
//获取已经分好的片段数量
$chunkOkCount = count($chunkUploadInfo);
//上传前检测切片是否可以合并
if ($chunkOkCount == $uploadTaskInfo['chunk_num']) {
$url = self::chunkUploadFinishReport($userId, $fileMd5, $uploadType);
$returnResult['status'] = Pan::FILE_TASK_UPLOAD_OK;
$returnResult['fileUrl'] = $url;
return $returnResult;
}
$path = $file->store('files');
$chunk = storage_path('app') . '/' . $path;
$key = $uploadTaskInfo['store_path'] . $uploadTaskInfo['store_name'] . '.' . $uploadTaskInfo['ext'];
$uploadId = $uploadTaskInfo['upload_id'];
// 上传分片
$result = KkyOssService::uploadChunk($chunk, $key, $uploadId, $index);
$chunkUploadData = [];
if ($chunkOkCount > 0) {
//如果有切片信息.那么就修改
$chunkUploadInfo[$result['etag']] = $index;
$chunkUploadData['chunkUploadInfo'] = json_encode($chunkUploadInfo);
} else {
$chunkUploadData['chunkUploadInfo'] = json_encode([$result['etag'] => $index]);
}
Upload::hashSetUploadTask($chunkUploadData, $userId, $fileMd5);
// 删除临时分片
Storage::delete($path);
//检测如果分片全部分好 就去提交合并
if ($chunkOkCount + 1 == $uploadTaskInfo['chunk_num']) {
$url = self::chunkUploadFinishReport($userId, $fileMd5, $uploadType);
$returnResult['status'] = Pan::FILE_TASK_UPLOAD_OK;
$returnResult['fileUrl'] = $url;
return $returnResult;
}
$returnResult['status'] = Pan::FILE_TASK_CHUNK_UPLOAD_OK;
return $returnResult;
}
/**
* 描述:获取上传任务信息hash key值
* 此key有两个键: 分别是 uploadTaskInfo 和 chunkUploadInfo
* 函数名: hashGetUoploadTask
* 创建人:wenhao
* 创建时间:2020/11/23 上午10:58
* @param int $userId
* @param string $fileMd5
* @return mixed
*/
public static function hashGetUoploadTask(int $userId,string $fileMd5)
{
$redisKey = sprintf(RedisKey::KEY_UPLOAD_TASK_USER_FILEMD5, $userId, $fileMd5);
$result = RedisService::hgetAll($redisKey);
if(!empty($result['uploadTaskInfo'])){
$result['uploadTaskInfo'] = json_decode($result['uploadTaskInfo'],true);
}
if(!empty($result['chunkUploadInfo'])){
$result['chunkUploadInfo'] = json_decode($result['chunkUploadInfo'],true);
}
return $result;
}
/**
* 描述:分片完成上报合并服务
* 函数名: chunkUploadFinishReport
* 创建人:wenhao
* 创建时间:2020/11/18 下午3:46
* @param int $userId 用户id
* @param string $fileMd5 文件md5
* @param string $uploadType 上传类型
* @return mixed
* @throws CustomException
* @throws ResoursesNotExistException
* @throws \App\Exceptions\ServerErrorException
*/
private static function chunkUploadFinishReport(int $userId, string $fileMd5, string $uploadType)
{
//获取上传任务hash缓存.
$uploadTaskHashInfo = Upload::hashGetUoploadTask($userId, $fileMd5);
//上传任务信息
$uploadTaskInfo = $uploadTaskHashInfo['uploadTaskInfo'] ?? [];
//切片信息
$chunkUploadInfo = $uploadTaskHashInfo['chunkUploadInfo'] ?? [];
if (empty($uploadTaskInfo)) {
throw new ResoursesNotExistException();
}
//获取分段片段组成数组;
$etags = [];
foreach ($chunkUploadInfo as $k => $v) {
$etags[intval($v)] = $k;
}
// 片段合并
if ($etags && count($etags) > 0) {
$result = KkyOssService::completeMultipartUpload($uploadTaskInfo['store_path'] . $uploadTaskInfo['store_name'] . '.' . $uploadTaskInfo['ext'], $uploadTaskInfo['upload_id'], $etags);
} else {
throw new CustomException('600001', '没有可合并的片段');
}
switch ($uploadType) {
//如果是网盘文件就要进行存储
case "panFile":
//获取文件详情
$fileInfo = OssService::getFileInfo($result['key']);
//合并完成之后添加文件.
//重新组合file 数据库的添加数据
$saveFileData = [
'file_name' => $uploadTaskInfo['file_name'],
'folder_id' => $uploadTaskInfo['folder_id'],
'user_id' => $uploadTaskInfo['user_id'],
'ext' => $uploadTaskInfo['ext'],
'md5' => $uploadTaskInfo['md5'],
'size' => $fileInfo['file_size'],
'type' => $uploadTaskInfo['type'],
'mime' => $fileInfo['contentType'],
'store_name' => $uploadTaskInfo['store_name'],
'store_path' => $uploadTaskInfo['store_path'],
//....此处写你自己的存储数据
'status' => 1,
];
$saveFileId = (new File)->insertData($saveFileData);
if (!$saveFileId) {
throw new CustomException(600001, '文件上传失败');
}
break;
}
//删除上传任务缓存
Upload::delUploadTask($userId, $fileMd5);
return $result['key'];
}