django关于文件分块上传的简单实现(template+view)
关于文件分块上传技术以及所谓的断点续存。逻辑其实很简单,就是将大文件切割成一个个小文件,一个个上传,给每个块打上id,然后单独校验完整性,上传如果失败,也只是重新上传上传失败的部分文件。该功能已经有功能相对完善的项目实践:
django-chunked-upload
https://github.com/juliomalegria/django-chunked-upload
但越是完善的东西学习起来越麻烦,因此笔者借助RACCON助手按自己的理解进行了关键功能的简单实现。
该功能需要前后端进行配合以实现:
前端负责文件的切分,分块文件的编号,分块文件的hash计算,以及分块文件的上传。
后端负责接收分块文件,hash值的校验,分块文件的存储,接收成功的响应,以及最终所有分块文件的合并。理论上应该还有传输失败文件id的响应,但本文没有去实现。
前端template
- 相关依赖(js库以及MD5hash计算api)
<script type="text/javascript" src="https://cdn.staticfile.org/twitter-bootstrap/5.1.1/js/bootstrap.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
- 文件分块以及上传
<script>
//file=document.getElementById("File").files[0];
//filename='filename'
//chunkSize=1024*1024
//uploadurl="{% url 'djangourlname' %}"
//callbackelement=document.getElementById("message");
//uploadFile(file,filename,chunkSize,uploadurl,callbackelement)
function uploadFile(file,filename,chunkSize,uploadurl,callbackelement) {
var chunks = Math.ceil(file.size / chunkSize);//计算分块数
var currentChunk = 0;
//分块上传
function sendChunk() {
var start = currentChunk * chunkSize;
var end = Math.min(file.size, start + chunkSize);
var chunk = file.slice(start, end);//分块截取
//为了计算分块文件的hash值,使用FileReader读取文件为ArrayBuffer类型
var reader = new FileReader();
reader.readAsArrayBuffer(chunk);
reader.onload = function(evt) {
md5 = SparkMD5.ArrayBuffer.hash(evt.target.result);
var formData = new FormData();//创建表单对象
formData.append('file', chunk);
formData.append('chunk', currentChunk);
formData.append('chunks', chunks);
formData.append('hash', md5);
formData.append('filename', filename);
//上传表单
fetch(uploadurl, {
method: 'POST',
body: formData,
headers: {'X-CSRFToken': '{{ csrf_token }}'},//django csrf验证
})
.then(response => response.json())
.then(data => {
if (data.success) {
//加一个进度条
var progress = parseInt(currentChunk / chunks * 100, 10);
callbackelement.innerHTML = '文件上传进度:' + progress + '%';
currentChunk++;//上传成功则继续上传下一个文件块
if (currentChunk < chunks) {
sendChunk();
} else {
callbackelement.innerHTML = '文件上传成功!';
}
} else {
//失败则继续上传,可以修改为继续上传下一个,但是记录失败分块,后续重传
var progress = parseInt(currentChunk / chunks * 100, 10);
callbackelement.innerHTML = '文件上传进度:' + progress + '%';
sendChunk();
}
})
.catch(error => {
var progress = parseInt(currentChunk / chunks * 100, 10);
callbackelement.innerHTML = '文件上传进度:' + progress + '%';
callbackelement.innerHTML += '文件上传出错!';
});
};
}
sendChunk();
}
</script>
后端视图
from django.http import JsonResponse
from django.views import View
import os
import hashlib
def calculate_md5(file_chunk):
md5 = hashlib.md5()
file_chunk.seek(0) # 重置文件块的读取位置,这很关键
while chunk := file_chunk.read(8192):
md5.update(chunk)
return md5.hexdigest()
class ChunkedUploadView(View):
def post(self, request):
# 获取文件名
file_name = request.POST.get('filename', None)
# 指定存储文件块的目录
chunk_dir = os.path.join('media/uploads/', file_name)
if not os.path.exists(chunk_dir):
os.makedirs(chunk_dir)
# 获取文件块数据
file_chunk = request.FILES['file']
print(file_chunk.size)
# 计算文件块的MD5哈希值
calculated_hash = calculate_md5(file_chunk)
# 获取文件的MD5哈希值
file_hash = request.POST['hash']
if calculated_hash != file_hash:
return JsonResponse({'success': False})
# 获取当前文件块的索引
current_chunk = int(request.POST['chunk'])
# 获取文件块的总数
total_chunks = int(request.POST['chunks'])
# 将文件块保存到指定目录
file_path = os.path.join(chunk_dir, f'chunk_{current_chunk}')
with open(file_path, 'wb') as f:
file_chunk.seek(0)#重置读取位置,这很关键
f.write(file_chunk.read())
# 如果所有文件块都已接收,则合并文件
if current_chunk == total_chunks - 1:
merged_file_path = os.path.join(chunk_dir, file_name+'.json')
with open(merged_file_path, 'wb') as merged_file:
for i in range(total_chunks):
chunk_path = os.path.join(chunk_dir, f'chunk_{i}')
with open(chunk_path, 'rb') as chunk_file:
merged_file.write(chunk_file.read())
chunk_file.close()#关闭文件,因为下面要删除,必须提取进行关闭
os.remove(chunk_path) # 删除临时文件块
return JsonResponse({'success': True})
else:
return JsonResponse({'success': True})