[ django下载文件浏览器显示进度条 ]


django使用流响应文件, 浏览器会有下载进度条, 可以点击进行暂停, 继续

后端

    @action(detail=False, methods=['get'], schema=AutoDocSchema())
    def get_big_files(self, request):
        def file_iterator(file_path, start_byte=0, chunk_size=256):
            with open(file_path, 'rb') as f:
                f.seek(start_byte)
                while chunk := f.read(chunk_size):
                    yield chunk

        file_path = '/home/chf/databases-bak/databaes-data-bak/backup/docker_image/mongo-express:latest'

        # 检查文件是否存在
        if not os.path.exists(file_path):
            return Response({"detail": "File not found."}, status=status.HTTP_404_NOT_FOUND)

        file_size = os.path.getsize(file_path)

        # 获取 Range 请求头(如 "bytes=0-999")
        range_header = request.headers.get('Range', None)
        if range_header:
            # 解析 Range 头
            try:
                byte_range = range_header.split('=')[1]
                start_byte, end_byte = byte_range.split('-')
                start_byte = int(start_byte)
                end_byte = int(end_byte) if end_byte else file_size - 1
            except (ValueError, IndexError):
                return Response({"detail": "Invalid Range header format."}, status=status.HTTP_400_BAD_REQUEST)
        else:
            # 如果没有 Range 请求头,则从头开始返回文件
            start_byte = 0
            end_byte = file_size - 1

        # 确保返回的字节范围不会超出文件大小
        if start_byte > file_size:
            start_byte = file_size - 1
        if end_byte > file_size - 1:
            end_byte = file_size - 1

        # 设置流式响应
        response = StreamingHttpResponse(file_iterator(file_path, start_byte, chunk_size=256), status=status.HTTP_206_PARTIAL_CONTENT)
        response['Content-Type'] = 'application/octet-stream'
        response['Content-Disposition'] = f'attachment; filename="file.sql"'
        response['Content-Length'] = str(end_byte - start_byte + 1)
        response['Content-Range'] = f"bytes {start_byte}-{end_byte}/{file_size}"
        response['Accept-Ranges'] = 'bytes'  # 支持分段下载

        # 返回部分文件数据
        return response

前端js实现下载文件获取进度条

XMLHttpRequest由于安全问题, 无法支持暂停,的时候获取数据, 只那下载整个文件, 获取一些请求头信息,进度条, 文件大小等...

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>文件下载进度</title>
    <style>
        #progress-container {
            width: 100%;
            background-color: #f3f3f3;
            height: 30px;
            border-radius: 5px;
            margin: 20px 0;
        }

        #progress-bar {
            height: 100%;
            width: 0;
            background-color: green;
            text-align: center;
            line-height: 30px;
            color: white;
            border-radius: 5px;
        }
    </style>
</head>
<body>
    <h1>下载文件</h1>
    <button id="download-button">开始下载</button>
    <div id="progress-container">
        <div id="progress-bar">0%</div>
    </div>

    <script>
        document.getElementById('download-button').addEventListener('click', function() {
            const xhr = new XMLHttpRequest();
            const url = 'http://192.168.1.80:8001/api/stock/get_big_files/'; // 你的API端点

            xhr.open('GET', url, true);
            xhr.setRequestHeader('authorization', 'Token bfd620b1d644dcf70c499423e441c083e0cefc98')
            xhr.responseType = 'blob';  // 处理二进制数据

            // 监听进度事件
            xhr.onprogress = function(event) {
                if (event.lengthComputable) {
                    const percentComplete = (event.loaded / event.total) * 100;
                    updateProgressBar(percentComplete);
                }
            };

            // 下载完成后
            xhr.onload = function() {
                if (xhr.status === 200) {
                    const blob = xhr.response;
                    const downloadUrl = window.URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = downloadUrl;
                    a.download = "文件.zip";  // 设置下载文件名
                    document.body.appendChild(a);
                    a.click();
                    a.remove();
                    window.URL.revokeObjectURL(downloadUrl);
                    updateProgressBar(100);  // 确保进度条达到100%
                }
            };

            xhr.onerror = function() {
                alert('下载失败');
            };

            xhr.send();

            // 重置进度条
            updateProgressBar(0);
        });

        // 更新进度条
        function updateProgressBar(percent) {
            const progressBar = document.getElementById('progress-bar');
            progressBar.style.width = percent + '%';
            progressBar.textContent = Math.round(percent) + '%';
        }
    </script>
</body>
</html>

js版本2

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>File Download with Progress</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
        }

        #progress-container {
            width: 100%;
            background-color: #f3f3f3;
            border-radius: 5px;
            height: 20px;
            margin-top: 20px;
            display: none;
        }

        #progress-bar {
            height: 100%;
            width: 0%;
            background-color: green;
            border-radius: 5px;
        }

        #status {
            margin-top: 10px;
        }

        button {
            padding: 10px 20px;
            margin-top: 20px;
            font-size: 16px;
        }

        #pause-btn {
            display: none;
        }
    </style>
</head>

<body>

    <h1>Download File with Progress</h1>
    <button id="startBtn">Start Download</button>
    <button id="pauseBtn">Pause Download</button>
    <button id="resumeBtn">Resume Download</button>
    <div id="progress-container">
        <div id="progress-bar"></div>
    </div>
    <p id="status"></p>

    <script>
        let xhr;
        let downloadUrl = 'http://192.168.1.80:8001/api/stock/get_big_files/';
        let isPaused = false;
        let downloadedBytes = 0;  // 已下载的字节数
        let currentResponse = 0;
        let currentBytes = 0;
        let totalBytes = 0;  // 文件总大小
        let partialData = [];     // 保存已下载的部分数据


        const status = document.getElementById('status');
        const startBtn = document.getElementById('startBtn');
        const pauseBtn = document.getElementById('pauseBtn');
        const resumeBtn = document.getElementById('resumeBtn');

        function startDownload() {
            xhr = new XMLHttpRequest();
            xhr.open('GET', downloadUrl, true);
            xhr.setRequestHeader('authorization', 'Token bfd620b1d644dcf70c499423e441c083e0cefc98')
            xhr.responseType = 'arraybuffer';

            // 设置进度回调
            xhr.onprogress = function (event) {

                if (event.lengthComputable) {
                    // console.log("downloadedBytes: ", event.loaded);
                    if (totalBytes == 0) {
                        totalBytes = event.total;
                        console.log("文件大小: ", totalBytes);
                        console.log(event);
                    }
                    currentBytes = event.loaded;
                    const percent = (downloadedBytes + event.loaded) / totalBytes * 100;
                    status.innerText = `Downloading... ${Math.round(percent)}%`;
                }

                let data = xhr.response;
                if (data) {
                    partialData.push(data);  // 保存每次接收到的数据
                }
            };

            // 请求完成后处理
            xhr.onload = function () {
                console.log("请求完成");

                if (xhr.status === 206) {
                    // 下载完成
                    let data = xhr.response;
                    downloadedBytes += data.byteLength;
                    // partialData.push(data); // 保存下载的数据
                    console.log("所有数据:", partialData);



                    // 合并所有已下载的数据
                    // 合并所有已下载的数据
                    let allData = new Uint8Array(downloadedBytes);
                    let offset = 0;

                    // 合并数据时,确保逐一拷贝每个部分
                    for (let i = 0; i < partialData.length; i++) {
                        if (partialData[i]) {  // 确保数据有效
                            allData.set(new Uint8Array(partialData[i]), offset);
                            offset += partialData[i].byteLength;  // 更新偏移量
                        }
                    }

                    document.getElementById('status').innerText = 'Download complete!';

                    // 将下载的二进制数据转为 Blob 对象
                    let blob = new Blob([allData], { type: 'application/octet-stream' });

                    // 创建一个临时的下载链接
                    let link = document.createElement('a');
                    link.href = URL.createObjectURL(blob);  // 使用 Blob 创建一个临时链接
                    link.download = 'downloadedFile.sql';    // 设置下载的文件名
                    link.click();  // 模拟点击下载
                }
            };

            // 开始请求时检查总文件大小
            xhr.onloadstart = function (event) {
                status.innerText = 'Starting download...';
            };
            xhr.onabort = function (event) {
                downloadedBytes += currentBytes;
                console.log('暂停下载进度:', downloadedBytes);
            };

            // 发送请求,使用 Range 请求头来限制下载的字节范围
            if (downloadedBytes > 0) {
                console.log("恢复下载: 从[", downloadedBytes, "]字节开始");
                // 恢复下载时,设置 Range 请求头从上次停止的字节继续下载
                xhr.setRequestHeader('Range', `bytes=${downloadedBytes}-`);
            }

            xhr.send();
        }



        function pauseDownload() {
            if (xhr && xhr.readyState === XMLHttpRequest.LOADING) {
                console.log(xhr.response);

                xhr.abort();  // 暂停下载
                isPaused = true;
            }
        }

        function resumeDownload() {
            startDownload();  // 重新发起请求,恢复下载
        }

        // 绑定暂停和恢复按钮
        pauseBtn.addEventListener('click', pauseDownload);
        resumeBtn.addEventListener('click', resumeDownload);
        startBtn.addEventListener("click", startDownload)
    </script>

</body>

</html>