文件的上传和下载
文件的上传和下载本质上都是两台计算机,建立连接,传输数据,然后把数据以文件的形式保存在硬盘中。
不同的协议会有不同的传输方式。例如 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
- 而且现代浏览器本身就有这个功能
- 其实分卷压缩也可以算是一种断点下载
上传前的预览
- 文本 图片 音频 视频 PDF 其它类型的文件
- 通过文件名后缀和 mime 来判断文件的类型
- Blob 对象 和 ArrayBuffer 对象
- Blob 对象的 URL
- 在 mdn 中的例子 https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input/file#examples
参考
- 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 来传输二进制数据。