我,一个被大文件上传折腾到秃头的PHP程序员,想和你唠唠这事儿

最近接了个外包项目,客户是做本地档案管理的,老板拍着桌子说:“小同志,咱们这系统得支持20G文件夹上传!用户每天传几千份资料,都是带1000个分类的文件夹,你得给我整明白——文件夹层级不能乱,断网重启能续传,加密存储还便宜!”

得,需求明确了:原生JS实现(别整框架)、20G文件夹上传(保留层级)、IE8兼容、加密传输+存储、断点续传、PHP后端、预算100元内。我熬了三个大夜,翻遍了GitHub和百度,终于整出一套“能跑能扛”的方案——今天全掏给你,省得你再踩坑!


一、需求拆解:这活儿到底难在哪儿?

先理清楚客户的“刚需”,咱们一条条啃:

需求维度关键点难点吐槽
文件夹上传保留层级(1000个分类)、非打包下载开源组件(如WebUploader)停更,不支持IE8;文件夹层级全靠手动模拟,头都大了
大文件传输20G文件、断点续传(关浏览器/重启电脑不丢进度)IE8不支持FormData,分片上传得用XMLHttpRequest.sendAsBinary,内存容易爆
加密合规传输(HTTPS)+存储(SM4/AES可配置)客户要国密SM4,PHP的SM4扩展得自己编译;AES密钥管理不能硬编码,得存配置文件
兼容性IE8+、主流浏览器、Windows/Linux/macOSIE8的File对象兼容性差,localStorage容量只有5MB,得省着用
成本预算100元内、免费代码+文档、7*24小时支持网上代码全是“残次品”,找个能跑的文件夹上传示例比登天还难;外包报价高,自己搞更划算

二、技术方案:用“土办法”解决“高难度”

1. 架构设计:前端“土分片”+ 后端“土存储”

没啥高大上的架构,就用最朴素的方式:前端把文件夹拆成“文件+相对路径”,分片上传;后端存分片+记录路径,合并时按路径拼。

核心逻辑:

  • 文件夹上传:用户选文件夹(IE8手动输入路径),前端递归遍历文件,记录每个文件的“相对路径”(如/文档/报告/2024.docx)。
  • 分片上传:每个文件切5MB分片(IE8内存扛不住太大的片),上传时带“文件哈希+分片索引”,服务端存分片到临时目录。
  • 断点续传:用localStorage存已上传分片索引(IE8支持),上传前查进度,跳过已传的分片。
  • 加密存储:传输层强制HTTPS,存储层用AES加密(SM4需要PHP扩展,客户预算有限,先上AES),密钥存config.php。
  • 非打包下载:下载时按路径遍历文件,逐个输出,避免打包导致内存爆炸。

三、前端代码:原生JS搞定文件夹上传(兼容IE8)

1. 文件夹上传核心逻辑(HTML+JS)




    大文件上传(兼容IE8)
    


    上传文件夹(保留层级)
     
    开始上传
    进度:0%
    
        // 兼容IE8的工具函数(ES5语法)
        var utils = {
            // 生成唯一文件ID(MD5,IE8需引入crypto-js)
            getFileId: function(file) {
                var reader = new FileReader();
                reader.onload = function(e) {
                    var wordArray = CryptoJS.lib.WordArray.create(e.target.result);
                    return CryptoJS.MD5(wordArray).toString();
                };
                reader.readAsArrayBuffer(file); // IE8用readAsBinaryString需特殊处理
            },
            // 遍历文件夹(递归记录相对路径)
            traverseFolder: function(files, basePath, callback) {
                for (var i = 0; i < files.length; i++) {
                    var file = files[i];
                    var relativePath = basePath ? basePath + '/' + file.name : file.name;
                    if (file.webkitRelativePath) { // 现代浏览器直接获取相对路径
                        callback(file, relativePath);
                    } else { // IE8手动输入路径(弹窗提示)
                        var path = prompt('请输入' + file.name + '的相对路径(如"文档/报告/")', basePath);
                        callback(file, path);
                    }
                    // 递归处理子文件夹(假设用户选了嵌套文件)
                    if (file.files) {
                        this.traverseFolder(file.files, relativePath, callback);
                    }
                }
            },
            // 分片上传(兼容IE8的XMLHttpRequest)
            uploadChunk: function(url, chunk, fileId, chunkIndex, totalChunks, callback) {
                var xhr = new XMLHttpRequest();
                xhr.open('POST', url, true);
                xhr.setRequestHeader('Content-Type', 'application/octet-stream');
                xhr.setRequestHeader('X-File-Id', fileId);
                xhr.setRequestHeader('X-Chunk-Index', chunkIndex);
                xhr.setRequestHeader('X-Total-Chunks', totalChunks);

                xhr.onreadystatechange = function() {
                    if (xhr.readyState === 4 && xhr.status === 200) {
                        callback(JSON.parse(xhr.responseText));
                    }
                };
                xhr.send(chunk); // IE8用sendAsBinary需处理Blob
            }
        };

        // 上传文件夹主逻辑
        function uploadFolder() {
            var input = document.getElementById('fileInput');
            var files = input.files;
            if (files.length === 0) {
                alert('请选择文件夹!');
                return;
            }

            // 生成全局唯一文件ID(防重复)
            var fileId = utils.getFileId(files[0]); // 简单示例,实际需遍历所有文件
            var uploadedChunks = JSON.parse(localStorage.getItem(fileId)) || []; // 从localStorage读进度

            // 遍历文件,记录相对路径(现代浏览器自动处理,IE8弹窗)
            utils.traverseFolder(files, '', function(file, relativePath) {
                // 计算分片
                var chunkSize = 5 * 1024 * 1024; // 5MB/片
                var totalChunks = Math.ceil(file.size / chunkSize);
                var currentChunk = 0;

                // 上传分片(跳过已传的)
                function uploadNextChunk() {
                    if (currentChunk >= totalChunks) {
                        alert('文件上传完成!');
                        return;
                    }
                    if (uploadedChunks.indexOf(currentChunk) !== -1) {
                        currentChunk++;
                        uploadNextChunk();
                        return;
                    }

                    var start = currentChunk * chunkSize;
                    var end = Math.min(start + chunkSize, file.size);
                    var chunk = file.slice(start, end); // IE8用webkitSlice

                    // 上传分片
                    utils.uploadChunk(
                        '/api/upload/chunk',
                        chunk,
                        fileId,
                        currentChunk,
                        totalChunks,
                        function(res) {
                            if (res.code === 200) {
                                uploadedChunks.push(currentChunk);
                                localStorage.setItem(fileId, JSON.stringify(uploadedChunks)); // 保存进度
                                currentChunk++;
                                uploadNextChunk();
                            } else {
                                alert('上传失败:' + res.msg);
                            }
                        }
                    );
                }

                uploadNextChunk();
            });
        }
    


2. 下载功能(非打包,按路径输出)
// 下载按钮点击事件(需后端配合)
function downloadFolder(folderId) {
    window.open('/api/download/folder?folderId=' + folderId); // 后端按路径遍历输出文件
}

四、后端PHP代码:分片上传+加密存储+文件夹管理

1. 分片上传接口(处理上传请求)
 200, 'msg' => '分片上传成功']);
?>
2. 合并分片接口(生成最终文件)
 200, 'msg' => '文件合并成功', 'path' => $mergedFile]);
?>

// AES加密函数(需安装openssl扩展)
function aes_encrypt($data, $key) {
    $iv = openssl_random_pseudo_bytes(16);
    $encrypted = openssl_encrypt($data, 'AES-256-CBC', $key, 0, $iv);
    return base64_encode($iv . $encrypted); // 存储IV+密文
}
?>  
3. 文件夹下载接口(非打包输出)


五、数据库设计(MySQL)

存文件夹层级和文件元数据,预算有限用单表:

CREATE TABLE files (
    id INT PRIMARY KEY AUTO_INCREMENT,
    folder_id VARCHAR(255) NOT NULL COMMENT '文件夹ID(对应前端生成的fileId)',
    file_name VARCHAR(255) NOT NULL COMMENT '文件名',
    relative_path VARCHAR(500) NOT NULL COMMENT '相对路径(如"文档/报告/")',
    file_size BIGINT NOT NULL COMMENT '文件大小(字节)',
    encrypt_key VARCHAR(32) NOT NULL COMMENT 'AES密钥(16/24/32字节)',
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间'
);

六、兼容性与稳定性保障

1. IE8兼容
  • 用XMLHttpRequest.sendAsBinary上传分片(IE8不支持FormData)。
  • localStorage存已上传分片索引(容量5MB,足够存1000个分片索引)。
  • 用prompt手动输入路径(IE8不支持webkitdirectory)。
2. 断点续传
  • 前端上传前查localStorage,跳过已传分片。
  • 服务端用文件记录已上传分片(uploaded.txt),重启后不丢失。
3. 加密存储
  • 传输层强制HTTPS(买个便宜的SSL证书,一年几十块)。
  • 存储层用AES-256-CBC(密钥存config.php,定期更换)。

七、预算与支持

  • 成本:代码免费,服务器用阿里云ECS(1核2G,一年500块),SSL证书(一年50块),总预算控制在600块内(远低于100元?不,用户说预算100元内,可能我超了,但实际可以优化,比如用免费SSL证书,服务器用轻量应用服务器)。
  • 支持:提供7*24小时QQ群支持(群号:374992201),群里有大神帮忙调试。
  • 文档:附《部署指南》《常见问题排查》,直接交给客户用。

写在最后:这活儿,咱们能搞定!

从需求分析到代码落地,从兼容性调试到加密合规,我踩过IE8的坑、分片的坑、文件夹层级的坑,现在把这套“能跑能扛”的方案掏出来——你直接拿去用,改改配置就能上线!

要是你也遇到类似需求,或者想组队接单,欢迎加群(QQ群:374992201)。群里有大神分享资源,有项目一起合作,没项目一起吹牛——毕竟,程序员的日子,互相搭把手,才能走得更远!

(最后小声说:要是群里有人能搞出SM4加密的PHP扩展,我分他一半项目钱!)

安装环境

PHP:7.2.14
Alt

调整块大小

Alt

NOSQL

NOSQL不需要任何配置,可以直接访问测试
Alt

SQL

创建数据库

您可以直接复制脚本进行创建
Alt
Alt

配置数据库连接

Alt

安装依赖

Alt

访问页面进行测试

Alt

数据表中的数据

Alt

效果预览

文件上传

文件上传

文件刷新续传

支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传
文件续传

文件夹上传

支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。
文件夹上传

免费下载示例

点击下载完整示例

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐