flask大文件分片上传

为什么要使用分片上传

分片上传,顾名思义,就是将一个大文件切分为多个小块(分片),然后独立上传这些分片,最后在服务器端再将这些分片合并成原始文件的技术。这一策略不仅极大地提高了文件上传的稳定性和效率,还为用户提供了更为友好的断点续传体验,即使在上传过程中遇到网络中断,也能够从断点处继续上传,无需重新开始。

falsk如何实现

项目结构

20240529095017512-image

 

前端

<!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
喜欢就支持一下吧
点赞0赞赏 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容