仓库源文站点原文

文件的上传和下载

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

不同的协议会有不同的传输方式。例如 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>
<!-- 上传时的并发要怎么处理? 多个文件并发上传 单个文件分片并发上传 多个文件分片并发上传 ajax 并发的限制? 微观并发,宏观顺序? --> ### 分片上传 ```html <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 处理分片上传的例子 ```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(); ``` <!-- 计算数字摘要 第三方库 https://github.com/satazor/js-spark-md5 https://github.com/pvorb/node-md5/ 使用 webapi https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest 分割文件 计算每个分割文件的数字摘要 用 ajax 上传 判断每个分割文件的数字摘要是否一致 后台按顺序合并 判断合并后的数字摘要是否和原本的一致 分片上传的中断和恢复 https://zh.javascript.info/resume-upload 把分片的信息放在http头应该会更好吧 --> ### 上传时的进度条 - 真实的 和 虚假的 进度条 - 单个文件的 和 多个文件的 进度条 ### 断点下载 - http头 - Range - Content-Range - 为什么没有js实现的断点下载 - 因为小文件不需要 - 大文件,js无法访问本地硬盘,不知道已经下载了多少 - 把分片文件保存在 localStorage 里也可以,但 localStorage 的容量只有几十MB - 而且现代浏览器本身就有这个功能 - 其实分卷压缩也可以算是一种断点下载 ### 上传前的预览 - 文本 图片 音频 视频 PDF 其它类型的文件 - 通过文件名后缀和 mime 来判断文件的类型 - Blob 对象 和 ArrayBuffer 对象 - Blob 对象的 URL - 在 mdn 中的例子 https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input/file#examples <!-- 拖拽上传 剪贴板上传 更多的奇技淫巧 实现例子 php 和 js 其实可以写成一个 js 的模块 在前端生成文件后再下载? --> ### 参考 - https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input/file - https://developer.mozilla.org/zh-CN/docs/Web/API/FormData/Using_FormData_Objects - https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest ## 如何不使用浏览器在 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 来传输二进制数据。 <!--本质上都是建立连接,传输数据,其实上传文件也是类似的套路-->

参考