为什么要使用分片上传
分片上传,顾名思义,就是将一个大文件切分为多个小块(分片),然后独立上传这些分片,最后在服务器端再将这些分片合并成原始文件的技术。这一策略不仅极大地提高了文件上传的稳定性和效率,还为用户提供了更为友好的断点续传体验,即使在上传过程中遇到网络中断,也能够从断点处继续上传,无需重新开始。
falsk如何实现
项目结构
前端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>视频文件分片上传</title>
<style>
*{
padding: 0;
margin: 0;
}
body {
background: #eee;
}
.title {
text-align: center;
font-size: 25px;
margin-top: 50px;
}
.video_upload {
width: 500px;
height: 210px;
line-height: 60px;
background-color: #fff;
margin: 30px auto 0;
border: 2px dashed #ccc;
border-radius: 10px;
position: relative;
cursor: pointer;
text-align: center;
line-height: 200px;
font-size: 17px;
}
.upload_icon {
width: 30px;
height: 30px;
margin: 90px auto 0;
display: block;
opacity: 0.5;
}
#fileInput {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
opacity: 0;
cursor: pointer;
}
#uploadButton {
width: 500px;
height: 40px;
border: none;
outline: none;
border-radius: 5px;
font-size: 17px;
margin: 15px auto;
background: #39f;
color: #fff;
cursor: pointer;
}
.progress {
width: 500px;
margin: 15px auto;
}
.progress progress {
width: 500px;
height: 30px;
}
#ret {
text-align: center;
font-size: 16px;
margin-top: 20px;
}
#ret video {
width: 300px;
}
#retUrl {
text-align: center;
font-size: 16px;
margin-top: 20px;
}
</style>
</head>
<body>
<p class="title">flask实现视频文件分片上传</p>
<div class="video_upload">
<img src="../static/img/upload.png" class="upload_icon" />
<input type="file" id="fileInput" >
</div>
<button id="uploadButton" style="display:none;">开始上传</button>
<div class="progress"></div>
<p id="ret"></p>
<p id="retUrl"></p>
<script>
// 定义全局变量
let videoFile = null;
let chunkSize = 1024 * 1024; // 1MB 分片大小
// 当文件选择框的值改变时触发该函数
function handleFileSelect(event) {
const fileList = event.target.files;
if (fileList.length > 0) {
videoFile = fileList[0];
console.log("选择了文件: ", videoFile.name);
document.querySelector('.video_upload').innerHTML = videoFile.name;
document.querySelector('#uploadButton').style.display = 'block';
}
}
// 分片并上传文件
async function uploadFile() {
// 检查文件是否准备就绪
if (!videoFile) {
alert('请先选择一个文件');
return;
}
const fileSize = videoFile.size;
let start = 0;
let end = Math.min(chunkSize, fileSize);
let chunkIndex = 0;
let totalChunks = Math.ceil(fileSize / chunkSize); // 总分片数
// 获取文件名
const fileName = videoFile.name;
while (start < fileSize) {
const chunk = videoFile.slice(start, end); // 从文件中截取一个分片
// 使用FormData来构建multipart/form-data格式的请求体
const formData = new FormData();
formData.append('file', chunk);
formData.append('chunkIndex', chunkIndex);
formData.append('fileName', fileName); // 将文件名作为 formData 的一部分
try {
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('上传失败');
}
console.log('上传分片 ', chunkIndex, ' 成功');
// 计算并显示上传进度
let progress = Math.round(((chunkIndex + 1) / totalChunks) * 100);
document.querySelector('.video_upload').textContent = '上传进度 ' + progress + '%';
document.querySelector('#uploadButton').textContent = '正在上传...';
document.querySelector('.progress').innerHTML = '<progress value="' + progress + '" max="100"><span>' + progress + '</span>%</progress>';
console.log('上传进度:', progress + '%');
} catch (error) {
console.error('上传分片 ', chunkIndex, ' 失败: ', error.message);
return;
}
start = end;
end = Math.min(start + chunkSize, fileSize);
chunkIndex++;
}
console.log('文件上传完成');
// 上传完成后发送通知给服务器进行合并
notifyServerForMerge(fileName);
}
// 发送通知给服务器进行合并
async function notifyServerForMerge(fileName) {
try {
const response = await fetch('/merge_chunks', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ fileName: fileName })
});
if (!response.ok) {
throw new Error('无法通知服务器进行合并');
}
const res_data = await response.json();
console.log('已通知服务器进行合并');
document.querySelector('.video_upload').textContent = '上传完成!';
} catch (error) {
console.error('通知服务器进行合并时发生错误: ', error.message);
}
}
// 注册文件选择框的change事件
document.getElementById('fileInput').addEventListener('change', handleFileSelect);
// 注册上传按钮的click事件
document.getElementById('uploadButton').addEventListener('click', uploadFile);
</script>
</body>
</html>
后端代码
from flask import Flask, request, redirect, jsonify, render_template
import os
from werkzeug.utils import secure_filename
import os
import shutil
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/upload', methods=['POST'])
def upload():
file = request.files['file']
chunk_index = int(request.form['chunkIndex'])
file_name = request.form['fileName']
upload_dir = './uploads/'
file_path = os.path.join(upload_dir, file_name + '.' + str(chunk_index))
file.save(file_path)
return jsonify(success='分片上传成功')
# 检查分片
@app.route('/merge_chunks', methods=['POST'])
def merge():
data = request.get_json()
file_name = data.get('fileName')
if not file_name:
return jsonify({'error': '缺少文件名'}), 400
# 确保文件名安全
safe_file_name = secure_filename(file_name)
final_file_path = os.path.join('./uploads', safe_file_name)
total_chunks = len([name for name in os.listdir('./uploads') if name.startswith(safe_file_name + '.')])
# 合并分片
with open(final_file_path, 'wb') as f:
for i in range(total_chunks):
chunk_path = os.path.join('./uploads', f'{safe_file_name}.{i}')
with open(chunk_path, 'rb') as chunk_file:
shutil.copyfileobj(chunk_file, f)
os.remove(chunk_path) # 删除已合并的分片
return jsonify({'success': '文件合并成功'})
if __name__ == '__main__':
app.run(debug=True)
© 版权声明
THE END
暂无评论内容