前言
本篇内容不涉及md5、断点续传等常规大文件上传要素,只是赶工出来的粗糙毛坯,留作备忘。前端Vue3,后端Python。 先引用 @哒布溜 的常规要素:
- 先对文件进行md5加密。使用md5加密的优点是:可以对文件进行唯一标识,同样可以为后台进行文件完整性校验进行比对。
- 拿到md5值以后,服务器端查询下该文件是否已经上传过,如果已经上传过的话,就不用重新再上传。
- 对大文件进行分片。比如一个100M的文件,我们一个分片是5M的话,那么这个文件可以分20次上传。
- 向后台请求接口,接口里的数据就是我们已经上传过的文件块。(注意:为什么要发这个请求?就是为了能续传,比如我们当一个文件传到一半的时候,突然想下班不想上传了,那么服务器就应该记住我之前上传过的文件块,当我打开电脑重新上传的时候,那么它应该跳过我之前已经上传的文件块。再上传后续的块)。
- 开始对未上传过的文件块进行上传。(这个是第二个请求,会把所有的分片合并,然后上传请求)。
- 上传成功后,服务器会进行文件合并。
正文
获取文件的File对象
它应该长这样:
File {
lastModified: 1710472518000,
lastModifiedDate: Fri Mar 15 2024 11:15:18 GMT+0800 (中国标准时间),
name: "test.pdf",
size: 32285991,
type: "application/pdf", // 我的文件是pdf,所以是“application/pdf”
webkitRelativePath: "",
}
这是二进制的,你看不到里面的东西。
文件切片
const size = file.size;
const shardSize = 1024 * 1024; // 假设以1MB为一个分片
const shardCount = Math.ceil(size / shardSize); // 总片数
const start = i * shardSize; // 切片的起始位置
const end = Math.min(size, start + shardSize); // 结束位置
const fileClip = file.slice(start, end); // File类型继承自Blob,原型链上有slice()方法
切出来的东西长这样:
fileClip: Blob {size: 1048576, type: ''}
这东西经过JSON.stringify()出来的是一个{},所以要么直接ws.send(fileClip),倘若send里是一个对象,就需要对其转码。
切片转码
需要用到FileReader:
const reader = new FileReader();
reader.onload = async (e) => {
const blob = reader.result;
console.log(blob);
// 'data:application/octet-stream;base64,ZmURO…U9GCg=='
// 这串东西就可以转string了
};
reader.readAsDataURL(fileClip); // 读为base64
// 你还可以
// reader.readAsArrayBuffer();
// reader.readAsText()
// 按需选择
传输到后端
这部分随你发挥,我用循环:
const sendMessage = () => {
const file = fileList.value[0].file;
const size = file.size;
const shardSize = 1024 * 1024; // 以1MB为一个分片
const shardCount = Math.ceil(size / shardSize); // 总片数
for (let i = 0; i < shardCount; i++) {
const start = i * shardSize;
const end = Math.min(size, start + shardSize);
const fileClip = file.slice(start, end);
const reader = new FileReader();
reader.onload = (e) => {
const blob = reader.result;
ws.value.send(
JSON.stringify({
fileName: file.name,
currentSlice: i + 1,
totalSlice: shardCount,
file: blob,
})
);
};
reader.readAsDataURL(fileClip);
}
};
后端接收与组装
我问GPT的。思路是做个临时存储,里面以分片下标做键,计数达到totalSlice时开始组装。
组装文件的部分写成一个类了:
class PdfAssembler:
def __init__(self, file_name, total_slices):
self.file_name = file_name
self.total_slices = total_slices
self.received_slices = {} # 分片存储的地方
self.received_data = b'' # 用于存储所有分片数据
def add_slice(self, current_slice, file_data):
self.received_slices[current_slice] = file_data
def is_complete(self):
return len(self.received_slices) == self.total_slices
# 组装文件
def assemble_pdf(self):
if not self.is_complete():
return None
# Sort received slices by current slice number
sorted_slices = sorted(self.received_slices.items(), key=lambda x: x[0])
for _, slice_data in sorted_slices:
b64 = slice_data.split(",", 1) # 拿掉base64的头,只要后面的编码本体
self.received_data += base64.b64decode(b64[1])
output_path = self.file_name
with open(output_path, "wb") as output_file:
output_file.write(self.received_data)
return output_path
python接收websocket的部分省略了,处理消息的部分如下:
async def on_message(self, message):
print("===== get_message =====")
data = tornado.escape.json_decode(message)
file_name = data.get('fileName')
current_slice = data.get('currentSlice')
total_slice = data.get('totalSlice')
file_data = data.get('file')
if file_name not in self.assemblers:
self.assemblers[file_name] = PdfAssembler(file_name, total_slice)
assembler = self.assemblers[file_name]
assembler.add_slice(current_slice, file_data) // 存储分片
if assembler.is_complete():
assembled_pdf_path = assembler.assemble_pdf()
if assembled_pdf_path:
print(f"PDF assembled: {assembled_pdf_path}")
# 可以在这里执行任何其他操作,例如将文件发送给客户端
# 然后清理已完成的 assembler
del self.assemblers[file_name]
custom_data = {"data": "your response"}
self.write_message(custom_data)
完结。