文件的上传和下载
文件的上传和下载本质上都是两台计算机,建立连接,传输数据,然后把数据以文件的形式保存在硬盘中。
不同的协议会有不同的传输方式。例如 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
- 用 系统自带的邮件 接收邮件的附件
- certutilcertutil -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-BitsTransferStart-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 来传输二进制数据。