简单说说,前置是个快活儿,要对接华为对象存储服务OBS,另外类似云盘文件管理的功能,顺带提一嘴有关于服务端文件分片性能的问题,以及使用的组件;
前端
vue3版本,非框架限定用simple-uploader,v2的话不需要@next新版
npm install vue-simple-uploader@next --save
代码资料比较齐全、不多做赘述,强调一下,支持文件夹上传、进度、文件类型区分,小问题是列表显示没扩展自定义功能,可能需要自行扩展处理
<template>
<uploader
:options="options"
:file-status-text="statusText"
class="uploader-example"
ref="uploaderRef"
@file-complete="fileComplete"
@complete="complete"
>
<!-- <uploader-unsupport></uploader-unsupport>
<uploader-drop>
<p>Drop files here to upload or</p>
<uploader-btn>select files</uploader-btn>
<uploader-btn :attrs="attrs">select images</uploader-btn>
<uploader-btn :directory="true">select folder</uploader-btn>
</uploader-drop>
<uploader-list></uploader-list> -->
</uploader>
</template>
<script>
import { nextTick, ref, onMounted } from 'vue'
export default {
setup () {
const uploaderRef = ref(null)
const options = {
target: '//localhost:3000/upload', // '//jsonplaceholder.typicode.com/posts/',
testChunks: false
}
const attrs = {
accept: 'image/*'
}
const statusText = {
success: 'success',
error: 'error',
uploading: 'uploading',
paused: 'paused',
waiting: 'waiting'
}
const complete = () => {
console.log('complete', arguments)
}
const fileComplete = () => {
console.log('file complete', arguments)
}
onMounted(() => {
nextTick(() => {
window.uploader = uploaderRef.value.uploader
})
})
return {
uploaderRef,
options,
attrs,
statusText,
complete,
fileComplete
}
}
}
</script>
<style>
.uploader-example {
width: 880px;
padding: 15px;
margin: 40px auto 0;
font-size: 12px;
box-shadow: 0 0 10px rgba(0, 0, 0, .4);
}
.uploader-example .uploader-btn {
margin-right: 4px;
}
.uploader-example .uploader-list {
max-height: 440px;
overflow: auto;
overflow-x: hidden;
overflow-y: auto;
}
</style>
服务端
服务端是.netcore6.0,看思路即可,这里重点说说多线程无序传输的处理问题,因为之前处理过类似顺序传输的限定优化以及分成块上传及最终合并的部分,这里是揉合了这几种,做了归并,具体看给定的服务是否支持乱序传输。
- 确定乱序下的最后处理时机,既文件是否已经传输完成,在乱序场景下,判断需要外置变量,同时也嫌文件分块再移除有点儿太粘连,因此使用了文件key加块列表的临时结构
- 与第三方交互的问题,如要求顺序,既在最后正序,然后异步传输,若第三方支持乱序,同频即可,但出于性能考量,可以异步方式,弱化等待和确定性,在一定程度上用户侧是无感的,后续的文件完整性也不会造成性能上的问题。
- 最后再提醒一下文件下载的处理问题,同样在文件比较庞大的时候,若采用读取完输出的方式会出现
oom这种问题,因此这里读取部分输出刷新流的形式,能有效避免假死等问题。
private ConcurrentDictionary<string, ConcurrentBag<UploadPartResponse>> _uploadsChunks =
new ConcurrentDictionary<string, ConcurrentBag<UploadPartResponse>>();
/// <summary>
/// / 无序多线程模式传输
/// </summary>
/// <param name="fileName"></param>
/// <param name="chunkIndex"></param>
/// <param name="contentLen"></param>
/// <param name="chunk"></param>
/// <returns></returns>
[HttpPost("uploaddChunksOBS")]
public async Task<IActionResult> uploaddChunksOBS(string fileName, int chunkIndex,int chunkSize, int chunks, IFormFile chunk)
{
if (chunk == null || chunk.Length == 0)
return BadRequest("Chunk is empty or not provided.");
ObsClient client = BuildObsClient();
try
{
// 1. 初始化分段上传任务
InitiateMultipartUploadRequest initiateRequest = new InitiateMultipartUploadRequest
{
BucketName = bucketName,
ObjectKey = fileName
};
InitiateMultipartUploadResponse initResponse = client.InitiateMultipartUpload(initiateRequest);
long filePosition = chunkIndex* chunkSize;
using (var ms = new MemoryStream())
{
await chunk.CopyToAsync(ms);
var uploadRequest = new UploadPartRequest
{
BucketName = bucketName,
ObjectKey = fileName,
UploadId = initResponse.UploadId,
PartNumber = chunkIndex + 1,
PartSize = ms.Length,
Offset = filePosition,
InputStream = ms
//FilePath = fileName
};
var uoloadResp = client.UploadPart(uploadRequest);
_uploadsChunks.AddOrUpdate(fileName, new ConcurrentBag<UploadPartResponse> { uoloadResp },
(key, existingBag) => { existingBag.Add(uoloadResp); return existingBag; });
//filePosition += byts.Length;
}
// 3.合并段,最后一个块合并
_uploadsChunks.TryGetValue(fileName, out var uploadResponses);
if(uploadResponses.Count == chunks)
{
CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest()
{
BucketName = bucketName,
ObjectKey = fileName,
UploadId = initResponse.UploadId,
};
completeRequest.AddPartETags(uploadResponses);
CompleteMultipartUploadResponse completeUploadResponse = client.CompleteMultipartUpload(completeRequest);
// 清除已合并的分片数据
_uploadsChunks.TryRemove(fileName, out _);
}
}
catch (Exception ex)
{
}
string apiPath = "/api/XX/download?fileName=" + fileName;
return Ok(apiPath);
}
总结
额,速度既生命,就是催催催啊,索性功力还在,就对接费了点调试和看文档的时间,晚间接晚间交,算是吃了波快餐,最近小东西有点儿密集,分享的稍微有点儿粗犷、大伙见谅~。