惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Check Point Blog
GbyAI
GbyAI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
U
Unit 42
美团技术团队
NISL@THU
NISL@THU
C
Cisco Blogs
SecWiki News
SecWiki News
N
Netflix TechBlog - Medium
Forbes - Security
Forbes - Security
Cloudbric
Cloudbric
雷峰网
雷峰网
T
Tailwind CSS Blog
博客园 - 司徒正美
The Register - Security
The Register - Security
L
LangChain Blog
S
Security Affairs
Hacker News - Newest:
Hacker News - Newest: "LLM"
B
Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
Threat Research - Cisco Blogs
I
InfoQ
S
Schneier on Security
L
Lohrmann on Cybersecurity
量子位
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Martin Fowler
Martin Fowler
Schneier on Security
Schneier on Security
F
Fortinet All Blogs
TaoSecurity Blog
TaoSecurity Blog
K
Kaspersky official blog
Google DeepMind News
Google DeepMind News
Cisco Talos Blog
Cisco Talos Blog
PCI Perspectives
PCI Perspectives
Attack and Defense Labs
Attack and Defense Labs
WordPress大学
WordPress大学
Microsoft Azure Blog
Microsoft Azure Blog
H
Help Net Security
Project Zero
Project Zero
The GitHub Blog
The GitHub Blog
D
Docker
N
News | PayPal Newsroom
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
H
Hacker News: Front Page
云风的 BLOG
云风的 BLOG
Microsoft Security Blog
Microsoft Security Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 聂微东
Webroot Blog
Webroot Blog
MongoDB | Blog
MongoDB | Blog

f2h2h1's blog

使用yii3实现一个微框架 claw养殖技术 计算机网络基础知识 定时任务 ACME的使用经验 magento2加上varnish缓存 开发Magento2的模块 在magento2中使用persisted-query socket编程 一些开发笔记 一段CSDN文章主要内容的油猴脚本 电子邮件的不完整总结 git的笔记 在Windows下配置PHP服务器 终端,控制台和外壳 PHP各种运行方式的不完整总结 把网页导出成PDF 和颜色相关的笔记 HTTP认证方式的不完整总结 SEO的经验 密码学入门简明指南 用纯CSS3实现的滑动按钮 在VSCode里调试PHP Linux的GUI 关于字符编码的一些坑 nc的使用和原理 在Windows下安装Magento2 对JS原型链的理解 使用docker-compose部署magento2 浏览器和服务器通讯方式的不完整总结 观察网站性能 一些关于Linux的笔记 telnet的不完整总结 在Windows下安装pear MySQL的时间类型和时间相关的函数 Windows下通过PEB读取进程的环境变量 关于 在VSCode里使用Xdebug远程调试PHP 在Windows下搭建git服务 关于环境变量的不完整总结 使用shell实现的kv数据库 如何完成以xx管理系统为选题的毕业设计 数字号码资源 各种标记语言 使用PowerShell实现的http服务器 kind相关经验 DNSSEC简介 nginx+ffmpeg+websocket实现的直播例子 使用Tesseract识别字符验证码 使用docker部署nuxt FirstData后台的设置 paypal,firtdata,支付宝的不完整接入指南 微信支付的不完整接入指南 用docker-compose部署lnmp环境 mongodb分片 练习
文件的上传和下载
2025-07-03 · via f2h2h1's blog

文件的上传和下载

这篇文章最后更新的时间在六个月之前,文章所叙述的内容可能已经失效,请谨慎参考!

文件的上传和下载本质上都是两台计算机,建立连接,传输数据,然后把数据以文件的形式保存在硬盘中。

不同的协议会有不同的传输方式。例如 http 和 ftp 的传输方式肯定不一样,但本质上依然是传输数据,然后数据的接收方把数据保存成文件。

上传和下载是相对而言的,数据的发送方就是在上传,数据的接收方就是在下载。

HTTP 中的文件下载

前端上传文件的总结

http协议中关于文件上传的部分

RFC 7578

上传单个文件

关键是要把 input 的 type 设为 file

<form id="upload_form" action="" method="post" enctype="multipart/form-data">
    <label>label <input type="file" id="split_upload" name="split_upload" accept=".jpg, .jpeg, .png"/></label>
    <button type="submit">Submit</button>
</form>

上传多个文件

关键是要在上一个例子中 input 标签里加上 multiple 属性

<form id="upload_form" action="" method="post" enctype="multipart/form-data">
    <label>label <input type="file" id="split_upload" name="split_upload" accept=".jpg, .jpeg, .png" multiple/></label>
    <button type="submit">Submit</button>
</form>

用 ajax 上传

<form id="upload_form" action="" method="post" enctype="multipart/form-data">
    <label>label <input type="file" id="split_upload" name="split_upload" accept=".jpg, .jpeg, .png" multiple/></label>
    <button type="submit">Submit</button>
</form>
<script>
document.querySelector('#split_upload_form').addEventListener('submit', function(event) {
    var form_data = new FormData(this);
    var xhr = new XMLHttpRequest();
    xhr.setRequestHeader('Content-type', 'multipart/form-data');
    xhr.open("POST", url, true);
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
            if (xhr.status == 200) {
            }
        }
    }
    xhr.send(form_data);
    event.cancelBubble = true;
    event.stopPropagation();
    event.preventDefault();
    return false;
}
</script>

分片上传

<form id="split_upload_form" action="" method="post" enctype="multipart/form-data">
    <label>label <input type="file" id="split_upload" name="split_upload" accept=".jpg, .jpeg, .png"/></label>
    <button type="submit">Submit</button>
</form>
<script>
(function(){
    class SplitUpload {
        element = {};
        maxSize = 1;
        acceptType = [];
        splitSize = 262144; // 一个分片的大小 262144B 256KB 0.25MB
        splitTimeout = 0; // 上传一个分片的最大时间
        totalTimeout = 0; // 上传全部分片的最大时间
        customData = {};
        shaType = 'SHA-256';
        url = '';
        constructor(options) {
            this.element = options.element;
            this.maxSize = options.maxSize;
            this.acceptType = options.acceptType;
            this.customData = options.customData ?? {};
            console.log(options);
        }
        async handle(files) {
            let shaType = this.shaType;
            for (let i = 0, len = files.length; i < len; i++) {
                let file = files[i];
                console.log(file);
                let fileSize = file.size;
                let fileType = file.type;
                let fileName = file.name;
                if (this.checkSize(fileSize)) {

                }
                if (this.checkSize(fileName, fileType)) {

                }
                let splitNum = 1;
                let splitSize = this.splitSize;
                if (fileSize > splitSize) {
                    splitNum = Math.ceil(fileSize / splitSize); // 需要读取的次数
                    console.log(splitNum);
                }

                let originalShaValue = await this.digest(shaType, file);
                for (let i = 0; i < splitNum; i++) {
                    // 计算分片起始位置
                    let start = i * splitSize;
                    // 计算分片结束位置
                    let end = start + splitSize;
                    // 最后一片特殊处理
                    if (end > fileSize) {
                        end = fileSize;
                    }
                    let file_temp_name = (new Date()).getTime() + "_" + parseInt(Math.random()*(999-100+1)+100,10);
                    let newBlob = file.slice(start, end);
                    let splitShaValue = await this.digest(shaType, newBlob);
                    let data = {
                        file_temp_name: file_temp_name,
                        splitNum: splitNum,
                        currentIndex: i,
                        file_real_name: fileName,
                        file_full_size: fileSize,
                        file_type: fileType,
                        blob: newBlob,
                        shaType: shaType,
                        splitShaValue: splitShaValue,
                        originalShaValue: originalShaValue,
                    };
                    data = Object.assign({}, data, this.customData);
                    this.sendAsync(this.url, data);
                }
            }
        }
        checkSize(size) {
            return true;
        }
        checkType(name, mime) {
            return true;
        }
        sendAsync(url, data) {
            var form_data = new FormData();
            for (let i in data) {
                form_data.append(i, data[i]);
            }
            return new Promise((resolve, reject) => {
                var xhr = new XMLHttpRequest();
                xhr.open("POST", url, true);
                // xhr.setRequestHeader('Content-type', 'multipart/form-dataPOST; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW');
                xhr.onreadystatechange = function () {
                    if (xhr.readyState == 4) {
                        if (xhr.status == 200) {
                            if (xhr.responseText == "success") {
                                resolve("success");
                            } else {
                                reject("fail");
                            }
                        } else {
                            console.log(xhr)
                            reject("fail");
                        }
                    }
                }
                xhr.send(form_data);
            });
        }
        async digest(shaType, blob) {
            const blobArrayBuffer = await blob.arrayBuffer();                           // 编码为(utf-8)Uint8Array
            const hashBuffer = await crypto.subtle.digest(shaType, blobArrayBuffer);           // 计算消息的哈希值
            const hashArray = Array.from(new Uint8Array(hashBuffer));                     // 将缓冲区转换为字节数组
            const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); // 将字节数组转换为十六进制字符串
            return new Promise((resolve, reject) => {
                resolve(hashHex);
            });
            // return hashHex;
        }
    };
    window.addEventListener('load', () => {
        let splitUpload = new SplitUpload({
                maxSize: 1,
                acceptType: [],
                splitSize: 1,
                splitTimeout: 0,
                totalTimeout: 0,
                customData: {},
            });
        document.querySelector('#split_upload_form').addEventListener('submit', function(event) {
            let fileInput = this.querySelector("input[type='file']");

            splitUpload.handle(fileInput.files);

            event.cancelBubble = true;
            event.stopPropagation();
            event.preventDefault();
            return false;
        })
    });
})();
</script>

用 php 处理分片上传的例子

function processShardedUploads() {
    $file_temp_name = $_POST['file_temp_name'] ?? null;
    $splitNum = $_POST['splitNum'] ?? null;
    $currentIndex = $_POST['currentIndex'] ?? null;
    $file_real_name = $_POST['file_real_name'] ?? null;
    $file_full_size = $_POST['file_full_size'] ?? null;
    $file_type = $_POST['file_type'] ?? null;
    $shaType = $_POST['shaType'] ?? null;
    $splitShaValue = $_POST['splitShaValue'] ?? null;
    $originalShaValue = $_POST['originalShaValue'] ?? null;
    $blob = $_FILES['blob'] ?? null;

    if (
        $file_temp_name === null ||
        $splitNum === null ||
        $currentIndex === null ||
        $file_real_name === null ||
        $file_full_size === null ||
        $file_type === null ||
        $blob === null ||
        $shaType === null ||
        $splitShaValue === null ||
        $originalShaValue === null
    ) {
        return 'missing parameter';
    }

    $checkHash = function($shaType, $tagValue, $filePath) {
        $shaType = strtolower(str_replace('-', '', $shaType));
        try {
            if (hash_file($shaType, $filePath) == $tagValue) {
                return true;
            }
        } catch (\Exception $e) {
        }
        return false;
    };
    // 验证分片的哈希值
    if (!$checkHash($shaType, $splitShaValue, $blob['tmp_name'])) {
        return 'fail split hash';
    }

    $uploadDir = dirname(__FILE__) . DIRECTORY_SEPARATOR; // 用于保存上传文件的目录
    $tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'split_' . $file_real_name . '_' . $splitNum; // 用于保存分片的目录
    if (!is_dir($uploadDir)) {
        mkdir($uploadDir, 0777, true);
    }
    if (!is_dir($tempDir)) {
        mkdir($tempDir, 0777, true);
    }

    $clearTemp = function() use ($tempDir) {
        // 删除分片文件和文件夹
        array_map('unlink', glob($tempDir . DIRECTORY_SEPARATOR . '*'));
        rmdir($tempDir);
    };

    $baseFileName = $tempDir . DIRECTORY_SEPARATOR . $file_real_name;
    move_uploaded_file($blob['tmp_name'], $baseFileName . '_' . $currentIndex);
    sleep(mt_rand(1, 3)); // 随机暂停,防止上传速度过快导致无法合并文件

    if (count(glob($baseFileName . '_*')) != $splitNum) {
        return 'success split'; // 单个分片上传完,但还有其它分片没有上传完
    }

    // 合并文件
    $butffer = '';
    for ($i = 0; $i < $splitNum; $i++) {
        $sliceFileName = $baseFileName . '_' . $i;
        if (!is_file($sliceFileName)) {
            $clearTemp();
            return('fail merge ' . $i); // 合并的过程中缺失了某一个分片
        }
        $butffer .= file_get_contents($sliceFileName);
    }
    // 把合并后的文件复制到用于保存上传文件的目录
    $mergeFile = $uploadDir . DIRECTORY_SEPARATOR . $file_real_name;
    file_put_contents($mergeFile, $butffer);

    $clearTemp();

    // 验证合并后文件的哈希值
    if (!$checkHash($shaType, $originalShaValue, $mergeFile)) {
        unlink($mergeFile);
        return 'fail merge hash';
    }

    return 'success merge';
}

echo processShardedUploads();

上传时的进度条

  • 真实的 和 虚假的 进度条
  • 单个文件的 和 多个文件的 进度条

断点下载

  • http头
  • Range
  • Content-Range
  • 为什么没有js实现的断点下载
    • 因为小文件不需要
    • 大文件,js无法访问本地硬盘,不知道已经下载了多少
      • 把分片文件保存在 localStorage 里也可以,但 localStorage 的容量只有几十MB
    • 而且现代浏览器本身就有这个功能
    • 其实分卷压缩也可以算是一种断点下载

上传前的预览

参考

如何不使用浏览器在 windows 里下载文件

使用 Windows 自带的程序

  • ftp
  • tftp
  • sftp
  • telnet
  • webdav
  • smb (就是网上邻居共享那种)
  • nfs
  • 用远程桌面
  • 用 windows 商店
  • 用 winget
  • 用 系统自带的邮件 接收邮件的附件
  • certutil
    certutil -urlcache -split  -f http://127.0.0.1:80/download/file.txt  file.txt
    
  • bitsadmin
  • 直接在 cmd 里 start 下载地址
    • 这其实会调用 ie 的,但实际上即使系统里没有浏览器大概率也能用, ie 作为系统组件很难删干净的
    • 其实直接在文件管理的地址栏输入 下载地址 也是可以的,也是调用 ie
  • hh 命令 hh http://www.baidu.com ,micorsoft html help 也是调用 ie ,但不支持直接打开 https 链接
  • powershell
    • (new-object System.Net.WebClient).DownloadFile('https://www.php.net/distributions/php-7.4.9.tar.gz', 'E:/php-7.4.9.tar.gz')
    • Invoke-WebRequest -Uri 'https://www.php.net/distributions/php-7.4.10.tar.gz' -OutFile 'E:/php-7.4.10.tar.gz'
    • Start-BitsTransfer
      Start-BitsTransfer -Source "<File URL>" -Destination "<File Name>"
      Start-BitsTransfer -Source "https://www.unicode.org/Public/UNIDATA/Unihan.zip" -Destination "Unihan.zip"
      
    • 用 powershell 远程登录,然后再拷贝文件, new-PSSession 和 Copy-Item 两个命令配合
    • 这几个都可以勉强算是下载文件
      • Install-Module
      • Install-Script
      • Install-Package
  • .net
    • 一些版本的 Windows 会自带 .net ,这样也可以调用 .net 的库来下载文件
  • VBScript JScript mshta 等调用 Microsoft.XMLHTTP
  • 通过 光驱 u盘 移动硬盘 等。。。
  • 用心探索一下 windows 里很多自带的服务都能下载文件
    • wmic Rundll32 Regsvr32 Cmstp msiexec MSBulid odbcconf
    • 传说一些版本的 Windows Defender 也可以下载文件

下载文件方式的一些总结

各种协议

  • ftp
  • http
    • 最简单的 http
    • 支持断点下载的 http
    • 支持多线程或多进程和断点下载的 http
  • p2p
    • bt
    • pt
    • DHT
    • ed2k
  • 其它
    • svn
    • git
    • 电子邮件也可以算一种 stmp/pop3/imap
    • 各类网络共享的方式

下载链接的前缀

  • ftp/sftp/ftps
  • http/https
  • ed2k
  • magnet
  • thunder
    • 迅雷的私有前缀,其实是经过编码的 url
  • Flashget
    • 网际快车的私有前缀,其实是经过编码的 url
  • qqdl
    • qq旋风的私有前缀,其实是经过编码的 url

thunder, Flashget, qqdl 随便百度一下就能找到解码的方法

各种软件

  • BitTorrent(比特洪流)
  • qBittorrent
  • µTorrent
  • rtorrent
  • 网际快车(Flashget)
  • 迅雷
  • qq旋风
  • 电驴(eDonkey)
  • 电骡(eMule)
  • VeryCD电驴
  • idm
  • FDM
  • 比特彗星(BitComet)
  • 比特精灵(BitSpirit)
  • 各种网盘

p2p

BT种子: 用一个文本文件(通常以 .torrent 作为后缀)描述一个或多个 Tracker 服务器或 DHT 网络中的文件。 ed2k: 用一段字符描述一个 KAD 网络中的文件。 Magnet: 用一段字符描述一个 DHT 网络中的BT种子文件。

其他

BitTorrent 既是一个下载协议也是一个软件名称。 大概就是作为软件的 BitTorrent 是最早使用 BitTorrent 协议下载的软件。

eDonkey 是一个商业软件, eMule 是 eDonkey 的开源版, VeryCD电驴 是国内的公司的根据 eMule 修改而来的。 kad 网络还有很多客户端,但在国内都不流行。

前互联网时代,好像都很喜欢在产品或软件或服务的名称前面加一个字母 e (例如 eMule eDonkey)

理论上 telnet 和 ssh 也可以用来下载文件。 理论上只要有网络连接就能下载文件,即使只能传输文本,也可以用 base64 来传输二进制数据。

参考