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

推荐订阅源

Google DeepMind News
Google DeepMind News
TaoSecurity Blog
TaoSecurity Blog
T
Threat Research - Cisco Blogs
Cisco Talos Blog
Cisco Talos Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Tenable Blog
C
Cybersecurity and Infrastructure Security Agency CISA
GbyAI
GbyAI
Microsoft Security Blog
Microsoft Security Blog
N
Netflix TechBlog - Medium
AWS News Blog
AWS News Blog
Security Latest
Security Latest
L
LINUX DO - 热门话题
Blog — PlanetScale
Blog — PlanetScale
T
Threatpost
Stack Overflow Blog
Stack Overflow Blog
Vercel News
Vercel News
S
Securelist
I
Intezer
D
Darknet – Hacking Tools, Hacker News & Cyber Security
H
Heimdal Security Blog
T
The Exploit Database - CXSecurity.com
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
The Hacker News
The Hacker News
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
H
Hackread – Cybersecurity News, Data Breaches, AI and More
U
Unit 42
The Register - Security
The Register - Security
NISL@THU
NISL@THU
S
Schneier on Security
M
MIT News - Artificial intelligence
The Last Watchdog
The Last Watchdog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Hugging Face - Blog
Hugging Face - Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Hacker News - Newest:
Hacker News - Newest: "LLM"
Y
Y Combinator Blog
Know Your Adversary
Know Your Adversary
罗磊的独立博客
MongoDB | Blog
MongoDB | Blog
美团技术团队
W
WeLiveSecurity
P
Privacy International News Feed
Forbes - Security
Forbes - Security
H
Hacker News: Front Page
小众软件
小众软件
博客园 - 【当耐特】
P
Proofpoint News Feed
P
Privacy & Cybersecurity Law Blog
T
Tor Project 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 来传输二进制数据。

参考