网上很多说的,包括官网说的,用UploadFile可以解决大文件上传,
基本上都是吹牛逼,
现在的文件,动不动就是几百兆,几G的文件,
UploadFile能解决啥,
用一台2核4G的服务器测试过,UploadFile最多只能支持100多兆的文件上传。
文件超过150兆~200兆就悬了,看命,命好的话,服务器刚好缓存比较多,上传成功。
命差的话,直接撑爆缓存,上传失败
所以,大文件上传,有且只有一条路子,分片上传
废话不说了,直接上代码
一、文件上传接口,如下:
@router.post("/upload/uploadfile")
async def upload_big_file(file: UploadFile = File(...), chunknumber: str = Form(...), identifier: str = Form(...)):#分片上传文件【用唯一标志符+分片序号】作为文件名
if len(chunknumber) == 0 or len(identifier) == 0:
return {"eroor": "没有传递相关参数"}
task = identifier # 获取文件唯一标识符
chunk = chunknumber # 获取该分片在所有分片中的序号【客户端设定】
filename = '%s%s' % (task,chunk) # 构成该分片唯一标识符
contents = await file.read() #异步读取文件
with open(f"{settings.STATIC_DIR}/uploads/{filename}", "wb") as f:
f.write(contents)
return {"filename": file.filename}
传递参数有:文件、序号、唯一标志符
这段代码,可以用来接收分片文件,分片文件其实就是把大文件切割成一个一个的小文件,上传到服务器,服务器接收到这些文件以后,保存到服务器上,所以,分片文件上传,和普通文件上传是一样的,只是多了【唯一标志符】,把上传的文件命名为【唯一标志符】+【序号】就可以了,这个序号也就是用来区分这个上传的文件是第几段文件的。
建议用Unix时间戳用作唯一标志符,网上有很多分片的代码,很扯淡,什么调用这库那库进行加密啊,扯一堆有的没的,不就是怕文件重名嘛,至于那么弯弯绕绕吗,用Unix时间戳+需要命名文件,就行了
另外,不要后缀名,后缀名在合并分片文件的时候才需要
二、合并分片文件接口,如下:
@router.post("/upload/mergefile")
async def mergefile(identifier: str = Form(...), filename: str = Form(...), chunkstar: int = Form(...)):#根据唯一标识符合并文件
if len(filename) == 0 or len(identifier) == 0:
return {"eroor": "没有传递相关参数"}
target_filename = filename # 获取上传文件的文件名【保存的文件名】
task = identifier # 获取文件的唯一标识符
chunk = chunkstar # 分片序号开始的序号默认=0
if os.path.isfile(f"{settings.STATIC_DIR}/uploads/{target_filename}"):#如果客户端传递过来的文件名在服务器上已经存在,那么另外新建一个【时间戳.后缀名】文件
t = time.time() #时间戳
timeUnix = str(round(t * 1000)) #毫秒级时间戳
filesuffix = os.path.splitext(target_filename)[1] #后缀名
target_filename = timeUnix + filesuffix #新文件名【时间戳.后缀名】
error_i = 0
chunkfilename = ""
with open(f"{settings.STATIC_DIR}/uploads/{target_filename}", 'wb') as target_file: # 创建新文件
while True:#循环把分片文件写入新建的文件
if os.path.isfile(f"{settings.STATIC_DIR}/uploads/{task}{chunk}"):#存在这个文件
try:
#分片文件名
chunkfilename = f"{settings.STATIC_DIR}/uploads/{task}{chunk}"
# 按序打开每个分片
source_file = open(chunkfilename, 'rb')
# 读取分片内容写入新文件
target_file.write(source_file.read())
source_file.close()
except IOError:#当分片标志chunk累加到最后,文件夹里面不存在{task}{chunk}文件时,退出循环
break
os.remove(f"{settings.STATIC_DIR}/uploads/{task}{chunk}")#删除分片文件
else:#【如果分片文件上传中途出错,导致中间缺少某个分片文件,跳过它,不退出循环,直到累计缺少次数大于3次,再跳出循环】
error_i += 1
if error_i > 3:
break
chunk += 1
os.chmod(f"{settings.STATIC_DIR}/uploads/{target_filename}",0o664)#linux设置权限0o664、0o400
return {"code":200, "filename": f"{settings.STATIC_DIR}/uploads/{target_filename}"}
传递参数有:【唯一标志符】、文件名、【起始分片序号】
根据【唯一标志符+序号】,去查保存目录下面对应的文件名,循环查找,找到了,就把分片文件合并,直到找不到了为止,退出循环
最后返回文件名
三、客户端:
把大文件分片
例如:我用的是vue3+typescript+vite+antd的前端,
文件分片代码:
let blobSlice = File.prototype.slice; let chunkFile = blobSlice.call(file, start, end);
这两句话就是文件分片,start,end是文件分片起点和终点,例如0,3*1024*1024就是文件的0~3M
然后获取一下当前时间戳,用做唯一标志符,
然后把分片,一片片的传给接口【使用Axios】,第几片就传参数:序号=几
上传完全部的分片后,访问合并分片接口,传递【唯一标志符】+【分片序号】+【文件名】
最后获取接口返回的服务器文件地址,展示给用户,或者调用接口保存到后端数据库
类似的分片代码还有,例如:原生JS
blob = file.slice(start,end);
原生JS用XMLHttpRequest上传到服务器接口
最后总结,不管前端用哪种,都是:前端进行分片,然后一片一片的传给服务器文件上传接口,等服务器接收完毕后,再访问服务器文件合并接口,最后得到服务器接口返回的合并后的文件地址。
PS:前端分片的话,你最好看情况,服务器很垃圾的话,分片分小一点,例如:每片文件3M左右,然后上传,【上传中途要动态提示用户,第几片上传成功,总共多少片文件等待上传,当然,你不提示用户也可以,看谁用,自己公司人用的话,完全可以怼回去,你们等一等有啥关系啊,正好摸鱼,如果是客户用的话,你最好把动态提示的代码给加上,防止用户等待时间太长,心态爆炸】,如果你服务器是那种超级计算机的话,分啥片啊,直接整个文件上传就好。