一个大三仔的编程血泪史:大文件上传系统开发实录

前言

各位老铁们好,我是广西某不知名大学网络工程专业的大三学生,最近被导师逼着做一个"支持10G文件上传、断点续传、文件夹层级保留、全浏览器兼容、还要加密传输存储"的变态文件管理系统。导师说做好了能推荐去大厂,做不好就等着延毕吧(哭唧唧)。

技术选型

经过一番折腾,我选定了以下"豪华套餐":

  • 前端:Vue3 + WebUploader(百度开源组件)
  • 后端:Python(虽然我还不会,但听说能速成)
  • 数据库:MySQL(学校机房老古董只认这个)
  • 存储:阿里云OSS(白嫖的免费额度)
  • 开发工具:VSCode(比Dev-C++香多了)

前端实现(原生JS版)

以下是经过我三天三夜不眠不休(实际是抄了20篇博客)写出的前端核心代码:

// 兼容IE8的XMLHttpRequest封装(头发已掉光)
function createXHR() {
    if (window.XMLHttpRequest) {
        return new XMLHttpRequest();
    } else if (window.ActiveXObject) {
        try {
            return new ActiveXObject("Msxml2.XMLHTTP");
        } catch (e) {
            try {
                return new ActiveXObject("Microsoft.XMLHTTP");
            } catch (e) {
                alert("您的浏览器太古老了,建议使用Chrome或Firefox");
                return null;
            }
        }
    }
}

// 文件分片上传类(命名暴露了我是菜鸟)
function FileUploader() {
    this.chunkSize = 5 * 1024 * 1024; // 5MB分片
    this.file = null;
    this.fileId = '';
    this.uploadedChunks = [];
    this.init();
}

FileUploader.prototype = {
    init: function() {
        // 本地存储进度(兼容IE8的localStorage替代方案)
        if (!window.localStorage) {
            window.localStorage = {
                getItem: function(key) {
                    return document.cookie.replace(new RegExp('(?:^|.*;\\s*)' + 
                        encodeURIComponent(key).replace(/[\-\.\+\*]/g, "\\$&") + 
                        '\\s*\\=\\s*((?:[^;](?!;))*[^;]?).*'), '$1') || null;
                },
                setItem: function(key, value) {
                    document.cookie = encodeURIComponent(key) + '=' + 
                        encodeURIComponent(value) + '; expires=Tue, 19 Jan 2038 03:14:07 GMT; path=/';
                }
            };
        }
    },
    
    // 计算文件MD5(IE8哭晕在厕所)
    calculateFileMD5: function(file, callback) {
        if (window.FileReader && window.crypto) {
            // 现代浏览器方案
            const reader = new FileReader();
            reader.onload = (e) => {
                const arrayBuffer = e.target.result;
                crypto.subtle.digest('MD5', arrayBuffer).then(buffer => {
                    const hashArray = Array.from(new Uint8Array(buffer));
                    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
                    callback(hashHex);
                });
            };
            reader.readAsArrayBuffer(file);
        } else {
            // IE8兼容方案(实际不可用,这里只是占位)
            alert("您的浏览器不支持MD5计算,上传可能不安全");
            callback('fake-md5-for-ie8');
        }
    },
    
    // 分片上传核心方法
    uploadFile: function(file) {
        this.file = file;
        this.fileId = file.name + '-' + file.size + '-' + file.lastModified;
        
        // 从本地存储加载已上传分片信息
        const savedChunks = localStorage.getItem('upload_' + this.fileId);
        this.uploadedChunks = savedChunks ? JSON.parse(savedChunks) : [];
        
        // 计算总分片数
        const totalChunks = Math.ceil(file.size / this.chunkSize);
        
        // 模拟上传过程(实际需要替换为真实AJAX请求)
        let uploaded = 0;
        for (let i = 0; i < totalChunks; i++) {
            // 跳过已上传的分片
            if (this.uploadedChunks.includes(i)) {
                uploaded++;
                continue;
            }
            
            // 这里应该是真实的AJAX上传代码
            // 为了演示,我们用setTimeout模拟
            setTimeout((chunkIndex) => {
                console.log(`上传分片 ${chunkIndex}/${totalChunks}`);
                
                // 模拟上传成功
                if (Math.random() > 0.1) { // 10%失败率测试断点续传
                    this.uploadedChunks.push(chunkIndex);
                    localStorage.setItem('upload_' + this.fileId, JSON.stringify(this.uploadedChunks));
                    
                    uploaded++;
                    if (uploaded === totalChunks) {
                        console.log('上传完成!');
                        // 这里应该调用合并文件的接口
                    }
                } else {
                    console.log(`分片 ${chunkIndex} 上传失败,将重试...`);
                }
            }, i * 1000, i);
        }
    },
    
    // 文件夹上传(递归实现)
    uploadFolder: function(folderEntry) {
        if (!window.FileReader || !window.DirectoryReader) {
            alert("您的浏览器不支持文件夹上传,请使用Chrome/Firefox最新版");
            return;
        }
        
        const directoryReader = folderEntry.createReader();
        directoryReader.readEntries((entries) => {
            for (let entry of entries) {
                if (entry.isFile) {
                    entry.file(file => {
                        // 为文件夹中的文件添加路径前缀
                        const relativePath = entry.fullPath.substring(1); // 去掉开头的'/'
                        const fakeFile = new File([file], file.name, {
                            type: file.type,
                            lastModified: file.lastModified
                        });
                        // 这里需要扩展File对象以保存路径信息(实际实现更复杂)
                        console.log('准备上传文件:', relativePath, fakeFile);
                        this.uploadFile(fakeFile);
                    });
                } else if (entry.isDirectory) {
                    this.uploadFolder(entry);
                }
            }
        });
    }
};

// Vue3组件示例(极简版)
const { createApp, ref } = Vue;

createApp({
    setup() {
        const uploader = ref(null);
        
        const handleFileChange = (e) => {
            const files = e.target.files;
            if (files.length > 0) {
                if (!uploader.value) {
                    uploader.value = new FileUploader();
                }
                uploader.value.uploadFile(files[0]);
            }
        };
        
        const handleFolderChange = (e) => {
            // 文件夹上传需要用户拖放或使用
            // 这里只是示例
            const folderInput = e.target;
            if (folderInput.files && folderInput.files.length > 0) {
                // 实际需要处理DirectoryEntry对象
                console.log('检测到文件夹输入,但前端实现复杂...');
            }
        };
        
        return {
            handleFileChange,
            handleFolderChange
        };
    }
}).mount('#app');

开发趣事

  1. IE8兼容噩梦:为了兼容IE8,我不得不实现了一个cookie-based的localStorage替代方案,结果发现IE8的cookie有4KB限制,大文件上传直接崩溃。

  2. MD5计算:IE8没有crypto API,我尝试用Flash实现MD5,结果发现学校机房禁用了Flash。

  3. 文件夹上传:发现WebUploader在IE和国产浏览器上对文件夹的支持各不相同,最后不得不写了一堆浏览器嗅探代码。

  4. 断点续传:本地存储在隐私模式下不可用,导致测试时经常丢失进度,差点摔键盘。

求助与招募

现在项目卡在后端开发上,求Python大神救救孩子:

  1. 需要实现文件分片接收接口
  2. 需要实现分片合并接口
  3. 需要实现加密存储功能
  4. 需要兼容所有浏览器(包括IE8)

重金求师

  • 加入QQ群:374992201,新人进群送1-99元红包
  • 推荐工作成功者,提成20%(项目金额)
  • 招收超级会员,提成直接50%(比如2万项目直接提1万)

最终目标

希望在毕业答辩时能演示一个:

  • 支持10G文件上传
  • 断点续传(重启电脑也不怕)
  • 文件夹层级完美保留
  • 全浏览器兼容
  • 加密传输存储

的完整文件管理系统,让导师刮目相看,顺利进入大厂!

(PS:如果实在做不出来,可能要考虑转行送外卖…)

将组件复制到项目中

示例中已经包含此目录
image

引入组件

image

配置接口地址

接口地址分别对应:文件初始化,文件数据上传,文件进度,文件上传完毕,文件删除,文件夹初始化,文件夹删除,文件列表
参考:http://www.ncmem.com/doc/view.aspx?id=e1f49f3e1d4742e19135e00bd41fa3de
image

处理事件

image

启动测试

image

启动成功

image

效果

image

数据库

image

效果预览

文件上传

文件上传

文件刷新续传

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

文件夹上传

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

批量下载

支持文件批量下载
批量下载

下载续传

文件下载支持离线保存进度信息,刷新页面,关闭页面,重启系统均不会丢失进度信息。
下载续传

文件夹下载

支持下载文件夹,并保留层级结构,不打包,不占用服务器资源。
文件夹下载

下载示例

点击下载完整示例

更多推荐