河南手机网站建设公司排名,水利枢纽门户网站建设方案,建设官方网站企业官网,新浪 sae wordpress开发日记#xff1a;大文件上传系统的探索与实现
2024年5月15日 星期一 晴
今天接到一个颇具挑战性的项目需求#xff1a;开发一个支持20G大文件上传的系统#xff0c;要求包含文件和文件夹上传功能#xff0c;并保留文件夹层级结构。更复杂的是#xff0c;系统需要支持…开发日记大文件上传系统的探索与实现2024年5月15日 星期一 晴今天接到一个颇具挑战性的项目需求开发一个支持20G大文件上传的系统要求包含文件和文件夹上传功能并保留文件夹层级结构。更复杂的是系统需要支持从IE8到现代浏览器的全兼容还要适配国产信创环境。作为广西的一名个人开发者我既兴奋又感到压力山大。技术选型经过一番调研我决定采用以下技术栈前端Vue3 CLI WebUploader百度开源组件后端ASP.NET WebForm考虑到客户已有.NET环境数据库SQL Server客户指定存储阿里云OSS后续可扩展其他云存储文件夹上传的挑战WebUploader默认不支持文件夹上传需要额外处理。我找到了一个基于WebUploader的扩展方案但测试后发现对IE8的支持不够完善。看来需要自己动手改造了。2023年5月16日 星期二 多云前端实现首先搭建Vue3项目结构vue create file-uploader cd file-uploader npm install webuploader --save创建自定义的文件夹上传组件FolderUploader.vueimport WebUploader from webuploader import webuploader/dist/webuploader.css export default { name: FolderUploader, mounted() { this.initUploader() }, methods: { initUploader() { const uploader WebUploader.create({ auto: false, swf: /path/to/Uploader.swf, server: /api/upload, pick: { id: #picker, multiple: true, // 启用文件夹选择非标准API部分浏览器支持 directory: true }, compress: false, chunked: true, chunkSize: 5 * 1024 * 1024, // 5MB分片 threads: 3, formData: { // 可添加额外参数 } }) // 处理文件夹结构 uploader.on(beforeFileQueued, file { // 解析文件夹路径非标准属性部分浏览器支持 if (file._relativePath) { file.relativePath file._relativePath } return true }) // 文件加入队列 uploader.on(fileQueued, file { this.$emit(file-added, file) }) // 上传进度 uploader.on(uploadProgress, (file, percentage) { this.$emit(progress, { file, percentage }) }) // 上传成功 uploader.on(uploadSuccess, (file, response) { this.$emit(success, { file, response }) }) // 上传错误 uploader.on(uploadError, (file, reason) { this.$emit(error, { file, reason }) }) // 绑定开始按钮 document.getElementById(ctlBtn).addEventListener(click, () { uploader.upload() }) this.uploader uploader } }, beforeUnmount() { if (this.uploader) { this.uploader.destroy() } } } .wu-example { position: relative; padding: 45px 15px 15px; margin: 15px 0; background-color: #fafafa; box-shadow: inset 0 3px 6px rgba(0, 0, 0, 0.05); border-color: #e5e5e5 #eee #eee; border-style: solid; border-width: 1px 0; }IE8兼容性处理WebUploader在IE8下需要Flash支持我添加了降级方案// 在initUploader方法中添加if(WebUploader.Browser.ieWebUploader.Browser.version8){uploader.option(swf,/path/to/Uploader_ie8.swf)// IE8下禁用文件夹选择不支持uploader.option(pick).directoryfalse}2023年5月17日 星期三 阵雨后端实现ASP.NET WebForm创建文件上传处理页面UploadHandler.ashx% WebHandler LanguageC# ClassUploadHandler % using System; using System.Web; using System.IO; using System.Data.SqlClient; using System.Configuration; public class UploadHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.ContentType text/plain; try { HttpPostedFile file context.Request.Files[file]; string relativePath context.Request.Form[relativePath]; string chunk context.Request[chunk]; string chunks context.Request[chunks]; string fileName context.Request[name]; // 如果没有分片信息就是普通上传 if (string.IsNullOrEmpty(chunk)) { SaveFile(file, relativePath); context.Response.Write({\status\: \success\}); return; } // 分片上传处理 int chunkNumber int.Parse(chunk); int totalChunks int.Parse(chunks); string tempDir context.Server.MapPath(~/App_Data/UploadTemp/ fileName); if (!Directory.Exists(tempDir)) { Directory.CreateDirectory(tempDir); } string tempFilePath Path.Combine(tempDir, chunkNumber.ToString()); file.SaveAs(tempFilePath); // 如果是最后一个分片合并文件 if (chunkNumber totalChunks - 1) { string finalPath context.Server.MapPath(~/Uploads/ (string.IsNullOrEmpty(relativePath) ? : relativePath /) fileName); // 确保目录存在 string finalDir Path.GetDirectoryName(finalPath); if (!Directory.Exists(finalDir)) { Directory.CreateDirectory(finalDir); } // 合并分片 using (FileStream fs new FileStream(finalPath, FileMode.Create)) { for (int i 0; i totalChunks; i) { byte[] bytes File.ReadAllBytes(Path.Combine(tempDir, i.ToString())); fs.Write(bytes, 0, bytes.Length); } } // 删除临时目录 Directory.Delete(tempDir, true); // 记录到数据库 SaveToDatabase(fileName, relativePath, finalPath, file.ContentType, file.ContentLength); } context.Response.Write({\status\: \success\, \chunk\: chunk }); } catch (Exception ex) { context.Response.Write({\status\: \error\, \message\: \ ex.Message \}); } } private void SaveFile(HttpPostedFile file, string relativePath) { string uploadFolder HttpContext.Current.Server.MapPath(~/Uploads/); string filePath Path.Combine(uploadFolder, (string.IsNullOrEmpty(relativePath) ? : relativePath /) Path.GetFileName(file.FileName)); // 确保目录存在 string directory Path.GetDirectoryName(filePath); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } file.SaveAs(filePath); // 记录到数据库 SaveToDatabase(file.FileName, relativePath, filePath, file.ContentType, file.ContentLength); } private void SaveToDatabase(string fileName, string relativePath, string filePath, string contentType, long fileSize) { string connStr ConfigurationManager.ConnectionStrings[DefaultConnection].ConnectionString; using (SqlConnection conn new SqlConnection(connStr)) { string sql INSERT INTO UploadedFiles (FileName, RelativePath, FilePath, ContentType, FileSize, UploadTime) VALUES (FileName, RelativePath, FilePath, ContentType, FileSize, UploadTime); SqlCommand cmd new SqlCommand(sql, conn); cmd.Parameters.AddWithValue(FileName, fileName); cmd.Parameters.AddWithValue(RelativePath, relativePath ?? ); cmd.Parameters.AddWithValue(FilePath, filePath); cmd.Parameters.AddWithValue(ContentType, contentType); cmd.Parameters.AddWithValue(FileSize, fileSize); cmd.Parameters.AddWithValue(UploadTime, DateTime.Now); conn.Open(); cmd.ExecuteNonQuery(); } } public bool IsReusable { get { return false; } } }2023年5月18日 星期四 晴数据库设计创建SQL Server表结构CREATETABLEUploadedFiles(IdINTIDENTITY(1,1)PRIMARYKEY,FileName NVARCHAR(255)NOTNULL,RelativePath NVARCHAR(1000),FilePath NVARCHAR(1000)NOTNULL,ContentType NVARCHAR(100),FileSizeBIGINTNOTNULL,UploadTimeDATETIMENOTNULL,IsFolderBITDEFAULT0,ParentIdINTNULL,FOREIGNKEY(ParentId)REFERENCESUploadedFiles(Id));文件夹结构处理修改前端上传逻辑添加文件夹标记// 在fileQueued事件处理中添加uploader.on(fileQueued,file{// 如果是文件夹通过文件大小为0判断if(file.size0file.name.indexOf(.)-1){file.isFoldertrue// 在数据库中记录文件夹this.saveFolderToDatabase(file)}this.$emit(file-added,file)})// 在methods中添加asyncsaveFolderToDatabase(folder){try{constresponseawaitfetch(/api/saveFolder,{method:POST,headers:{Content-Type:application/json},body:JSON.stringify({name:folder.name,relativePath:folder.relativePath||})})constresultawaitresponse.json()if(result.statussuccess){folder.databaseIdresult.id}}catch(error){console.error(保存文件夹失败:,error)}}2023年5月19日 星期五 多云下载功能实现创建下载处理页面DownloadHandler.ashx% WebHandler LanguageC# ClassDownloadHandler % using System; using System.Web; using System.IO; using System.Data.SqlClient; using System.Configuration; public class DownloadHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { int fileId; if (!int.TryParse(context.Request.QueryString[id], out fileId)) { context.Response.StatusCode 400; context.Response.Write(无效的文件ID); return; } // 从数据库获取文件信息 string connStr ConfigurationManager.ConnectionStrings[DefaultConnection].ConnectionString; string filePath ; string fileName ; bool isFolder false; using (SqlConnection conn new SqlConnection(connStr)) { string sql SELECT FilePath, FileName, IsFolder FROM UploadedFiles WHERE Id Id; SqlCommand cmd new SqlCommand(sql, conn); cmd.Parameters.AddWithValue(Id, fileId); conn.Open(); SqlDataReader reader cmd.ExecuteReader(); if (reader.Read()) { filePath reader[FilePath].ToString(); fileName reader[FileName].ToString(); isFolder Convert.ToBoolean(reader[IsFolder]); } reader.Close(); } if (isFolder) { // 如果是文件夹打包下载 string zipPath context.Server.MapPath(~/App_Data/Temp/ Guid.NewGuid() .zip); System.IO.Compression.ZipFile.CreateFromDirectory(filePath, zipPath); context.Response.Clear(); context.Response.ContentType application/zip; context.Response.AddHeader(Content-Disposition, attachment; filename\ HttpUtility.UrlEncode(fileName .zip) \); context.Response.WriteFile(zipPath); context.Response.Flush(); // 删除临时zip文件 File.Delete(zipPath); } else if (File.Exists(filePath)) { // 普通文件下载 context.Response.Clear(); context.Response.ContentType application/octet-stream; context.Response.AddHeader(Content-Disposition, attachment; filename\ HttpUtility.UrlEncode(fileName) \); context.Response.WriteFile(filePath); context.Response.Flush(); } else { context.Response.StatusCode 404; context.Response.Write(文件不存在); } } public bool IsReusable { get { return false; } } }2023年5月20日 星期六 晴加密传输实现添加SM4和AES加密支持前端使用crypto-js库npm install crypto-js --save修改上传逻辑importCryptoJSfromcrypto-js// 在initUploader方法中添加加密选项constencryptOption{enabled:true,algorithm:SM4,// 或 AESkey:your-secret-key-1234567890,// 实际项目中应从安全配置获取iv:your-iv-123456// 初始化向量}// 修改上传前的处理uploader.on(beforeFileQueued,file{if(encryptOption.enabled){file.encryptencryptOption}// ...原有代码})// 添加加密过滤器uploader.on(uploadBeforeSend,(block,data){if(block.file.encrypt){const{algorithm,key,iv}block.file.encrypt// 读取文件内容需要使用FileReaderconstreadernewFileReader()reader.onloade{letencryptedif(algorithmSM4){// 实际项目中应使用支持SM4的库这里简化处理encryptedCryptoJS.AES.encrypt(CryptoJS.lib.WordArray.create(e.target.result),CryptoJS.enc.Utf8.parse(key),{iv:CryptoJS.enc.Utf8.parse(iv)}).toString()}else{// AESencryptedCryptoJS.AES.encrypt(CryptoJS.lib.WordArray.create(e.target.result),CryptoJS.enc.Utf8.parse(key),{iv:CryptoJS.enc.Utf8.parse(iv)}).toString()}// 替换原始数据block.blobnewBlob([encrypted],{type:block.file.type})block.sizeblock.blob.size}reader.readAsArrayBuffer(block.blob)}})后端解密处理在UploadHandler.ashx中添加private byte[] DecryptFile(byte[] encryptedData, string algorithm, string key, string iv) { try { if (algorithm SM4) { // 实际项目中应使用支持SM4的库这里简化处理 // 注意.NET默认不支持SM4需要引入第三方库 return encryptedData; // 临时返回加密数据实际应解密 } else // AES { using (Aes aesAlg Aes.Create()) { aesAlg.Key Encoding.UTF8.GetBytes(key); aesAlg.IV Encoding.UTF8.GetBytes(iv); using (MemoryStream msDecrypt new MemoryStream(encryptedData)) using (CryptoStream csDecrypt new CryptoStream( msDecrypt, aesAlg.CreateDecryptor(), CryptoStreamMode.Read)) using (MemoryStream msResult new MemoryStream()) { csDecrypt.CopyTo(msResult); return msResult.ToArray(); } } } } catch { return encryptedData; // 解密失败返回原始数据 } } // 修改SaveFile方法在保存前解密 private void SaveFile(HttpPostedFile file, string relativePath) { // ...原有代码 // 读取上传的数据 byte[] fileData; using (BinaryReader reader new BinaryReader(file.InputStream)) { fileData reader.ReadBytes(file.ContentLength); } // 如果有加密信息解密 string encryptAlgorithm HttpContext.Current.Request.Form[encryptAlgorithm]; string encryptKey HttpContext.Current.Request.Form[encryptKey]; string encryptIv HttpContext.Current.Request.Form[encryptIv]; if (!string.IsNullOrEmpty(encryptAlgorithm)) { fileData DecryptFile(fileData, encryptAlgorithm, encryptKey, encryptIv); } // 保存解密后的数据 File.WriteAllBytes(filePath, fileData); // ...原有代码 }2023年5月21日 星期日 晴测试与调试今天进行了全面的测试浏览器兼容性Chrome/Firefox/Edge完美支持文件夹上传IE8降级为单文件上传Flash方案Safari基本功能正常文件夹支持有限大文件测试成功上传5GB和10GB文件分片上传和合并功能正常文件夹结构保留了原始文件夹层级数据库记录了完整的路径信息加密测试AES加密上传和解密下载正常SM4需要引入专门库暂未完全实现待解决问题IE8下的文件夹上传限制SM4加密的完整实现性能优化大文件夹上传时的内存使用断点续传的完善2023年5月22日 星期一 多云最终优化IE8优化添加了明确的提示告知IE8用户无法使用文件夹上传提供了批量上传的替代方案前端优化添加了上传速度显示改进了错误处理和重试机制注意您当前使用的是IE8浏览器不支持文件夹上传功能。请使用Chrome、Firefox等现代浏览器以获得完整功能。 速度: {{ uploadStats.speed }} 剩余时间: {{ uploadStats.timeLeft }}后端优化添加了事务支持确保数据库记录和文件存储的一致性改进了错误日志记录// 在SaveToDatabase方法中添加事务 using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); SqlTransaction transaction conn.BeginTransaction(); try { string sql INSERT INTO UploadedFiles (FileName, RelativePath, FilePath, ContentType, FileSize, UploadTime) VALUES (FileName, RelativePath, FilePath, ContentType, FileSize, UploadTime); SqlCommand cmd new SqlCommand(sql, conn, transaction); // ...参数设置 cmd.ExecuteNonQuery(); transaction.Commit(); } catch { transaction.Rollback(); throw; } }总结经过一周的努力系统基本实现了需求支持20GB大文件上传支持文件和文件夹上传保留层级结构兼容IE8到现代浏览器支持AES加密SM4需要额外库完整的数据库记录待完成工作完善SM4加密支持添加更详细的日志系统实现更完善的断点续传添加用户权限控制代码获取完整的代码我已经上传到GitHub示例链接实际使用时请替换https://github.com/example/large-file-uploader欢迎加入QQ群交流374992201注由于时间和精力限制部分功能如SM4加密尚未完全实现但提供了框架和思路。实际项目中需要根据具体需求进一步完善。设置框架安装.NET Framework 4.7.2https://dotnet.microsoft.com/en-us/download/dotnet-framework/net472框架选择4.7.2添加3rd引用编译项目NOSQLNOSQL无需任何配置可直接访问页面进行测试SQL使用IIS大文件上传测试推荐使用IIS以获取更高性能。使用IIS Express小文件上传测试可以使用IIS Express创建数据库配置数据库连接信息检查数据库配置访问页面进行测试相关参考文件保存位置效果预览文件上传文件刷新续传支持离线保存文件进度在关闭浏览器刷新浏览器后进行不丢失仍然能够继续上传文件夹上传支持上传文件夹并保留层级结构同样支持进度信息离线保存刷新页面关闭页面重启系统不丢失上传进度。下载完整示例下载完整示例