ASP.NET仿百度网盘源码拆解:无限级文件夹与异步上传实现
2026/9/15 15:18:41 网站建设 项目流程

简介:一套基于ASP.NET的仿百度网盘文件分享与管理系统源码,采用异步上传机制,支持在根目录下创建无限级文件夹并逐级进入管理,同时提供文件上传、删除、下载和分享链接生成提取码等完整功能,适合新手入门以及有一定经验的开发人员作为学习参考或二次开发底子。资源包整体约79.5MB,包含完整项目源代码、配置文档及必要说明,目录结构清晰,可直接在Visual Studio中还原运行,便于对照代码理解文件夹层级管理、文件流操作、异步上传、提取码校验等关键模块的实现思路。目前已有1327人学习或下载,代码注释与配置说明配合使用,可帮助读者快速搭建一套文件分享管理原型,并在此基础上扩展回收站、批量操作、用户权限等功能,尤其适合做课程设计、毕业设计或企业内部网盘基础版本。

1. 一个能跑的内网文件分享系统:ASP.NET 仿百度网盘源码拆解

年初遇到一个客户的内部知识库迁移项目,内外网隔离、U盘禁用,几十号人传文件只能靠邮件来回打转。当时我找到一套 ASP.NET 仿百度网盘的文件分享系统源码,改改跑在 IIS 上,配合域名访问,部门里传设计稿和安装包立刻顺畅了。这套代码的亮点不在于界面多现代,而在于它把无限级文件夹、异步上传、分享提取码、下载校验这些网盘核心动作完整串了起来,WebForms 技术栈在今天的私有化部署里依然有不少存量场景。新手能顺着它看清文件系统如何映射到数据库表,有经验的开发者也能从中提炼递归删除、并发上传、提取码防碰撞这几个可独立复用的模块。

2. 无限级文件夹的自关联表设计与递归遍历

2.1 为什么用 ParentId 自关联而不是存完整路径

文件夹做成无限级,第一反应往往是直接存"docs/project/2025"这样的路径字符串,导航时按/分割。但这个方案在网盘场景里很别扭:重命名中间层目录时,所有后代记录的路径都要批量更新,一旦某条记录没同步就出现死链;移动一个目录到新位置,同样需要把子树全部刷一遍。用ParentId自关联表就没有这些问题——每个节点只记录父节点主键,移动目录只需要改ParentId一个字段,重命名只看当前记录,子树自动跟着新父级走。

代价是查询某节点全部子孙时需要递归。项目初期数据量小,直接写递归方法最直观;记录超过几千条后,用 SQL Server 的递归 CTE 一次取完整子树,性能差别也不大。关键是表结构设计要留好索引,否则深层递归会带来明显的延迟。

2.2 文件目录表结构设计与初始化数据

源码中文件和文件夹共用一张资源表,用IsFile区分类型。这种设计的优势是分享、删除、移动都可以统一处理,不用维护两套节点模型。建表脚本大致如下:

CREATE TABLE ResourceNode ( Id INT IDENTITY PRIMARY KEY, ParentId INT NULL, Name NVARCHAR(255) NOT NULL, IsFile BIT NOT NULL DEFAULT 0, FilePath NVARCHAR(500) NULL, FileSize BIGINT NULL, ShareCode CHAR(4) NULL, CreatedAt DATETIME NOT NULL DEFAULT GETDATE() ); CREATE INDEX IX_ResourceNode_ParentId ON ResourceNode(ParentId);

注意ParentId允许为空,空值代表根目录文件,也可以约定使用0作为根节点,但空值配合外键更清晰。FilePath存放文件的实际物理路径相对值,如uploads/3/20250201_xxxx.pdf,目录类型的这个字段留空。ShareCode字段直接冗余在资源表上,方便分享判断时少一次关联。字段说明整理如下:

字段类型说明
Idint节点主键
ParentIdint nullable父节点Id,根目录为 NULL
Namenvarchar(255)文件或文件夹显示名
IsFilebitfalse 表示文件夹,true 表示文件
FilePathnvarchar(500)物理存储相对路径,仅文件使用
FileSizebigint文件字节数,文件夹为 NULL
ShareCodechar(4)分享提取码,未分享为 NULL
CreatedAtdatetime节点创建时间

ParentId上的索引是整个无限级文件夹查询的命脉。删除、移动、构建树都要按父级查找,没有索引时WHERE ParentId=@id会全表扫描,目录一旦上千条记录操作就开始变慢。

2.3 递归获取目录树与面包屑导航

源码里点击文件夹进入下一级,每次只加载当前层,这属于懒加载模式。懒加载实现简单,但需要同时提供面包屑,否则用户层级深了容易迷路。获取面包屑的经典做法是从当前节点一路回溯父级:

// 根据当前节点Id向上回溯,生成面包屑导航集合 private List<ResourceNode> GetBreadcrumb(int nodeId) { var path = new List<ResourceNode>(); var current = GetNodeById(nodeId); // 查单条记录 while (current != null) { path.Insert(0, current); // 插到头部保持根->当前顺序 current = current.ParentId.HasValue ? GetNodeById(current.ParentId.Value) : null; } return path; }

这个方法每向上回溯一层就执行一次数据库查询,层级为 N 时产生 N 次短查询。实际系统里目录层级通常不超过 10 层,单次查询走主键,开销可以接受。如果目录树特别深、节点总数上万,建议一次性把所有节点载入内存,用字典按 Id 索引后回溯,时间从 N 次数据库往返降为一次查询加 N 次内存操作。

递归删除子节点时可以使用 SQL Server 的递归 CTE,一次拿全子树的所有 Id,避免在 C# 里一层层调用:

WITH SubTree AS ( SELECT Id FROM ResourceNode WHERE Id = @parentId UNION ALL SELECT n.Id FROM ResourceNode n INNER JOIN SubTree s ON n.ParentId = s.Id ) SELECT Id FROM SubTree;

这段 CTE 的基查询是当前父节点,递归部分把ParentId指向SubTree中已有节点的记录不断收入集合,最终得到完整后代 Id 列表。注意 SQL Server 的递归 CTE 默认递归深度上限是 100,虽然无限级文件夹理论深度可以很大,但一般业务操作超过 100 层就要考虑改循环或者调优了。

3. 异步上传与文件落盘的实现细节

3.1 为什么用异步上传而不是传统 PostBack

WebForms 里最常见的上传做法是FileUpload控件配合 Button 回发,页面整个刷新,上传大文件时用户只能盯着浏览器转圈,没有进度提示,也无法继续操作其他区域。源码采用异步上传,前端用 XMLHttpRequest 把文件流独立提交到专门的 HttpHandler,页面主体不刷新,还能通过xhr.upload.onprogress拿到实时进度。另一个重要原因是,文件上传走专用 Handler 后,不受 WebForms 页面生命周期和 ViewState 序列化开销的影响,请求路径更短,出错时也更容易定位问题。

配套需要调整 ASP.NET 的上传大小限制。默认配置只允许 4MB,不调的话上传稍大文件直接报错。相关配置项如下:

配置项所在位置说明
maxRequestLengthsystem.web/httpRuntimeASP.NET 请求体上限,单位 KB
executionTimeoutsystem.web/httpRuntime脚本执行超时,单位秒
maxAllowedContentLengthsystem.webServer/security/requestFilteringIIS 7+ 层限制,单位字节
uploaderPathappSettings自定义上传根目录

maxAllowedContentLength是很多人忽略的坑。在集成模式下,IIS 先检查这个值,默认 30000000 字节(约 28.6MB),如果没调大,传个 30MB 的文件会返回 404.13 而不是常见的 ASP.NET 错误页。四者需要配合调整,单独改maxRequestLength解决不了 IIS 层的拦截。

3.2 前端异步上传:XMLHttpRequest + FormData

前端页面只需要一个文件选择框、一个上传按钮和一个进度条容器:

<input type="file" id="fileInput" name="fileInput" multiple /> <button type="button" id="btnUpload">上传</button> <div id="progress"></div>

对应的脚本绑定上传事件,把所有文件放进同一个 FormData 中提交:

document.getElementById('btnUpload').addEventListener('click', function () { var files = document.getElementById('fileInput').files; if (files.length === 0) return; var formData = new FormData(); formData.append('folderId', currentFolderId); // 当前所在目录Id for (var i = 0; i < files.length; i++) { formData.append('files', files[i]); } var xhr = new XMLHttpRequest(); xhr.open('POST', 'UploadHandler.ashx', true); xhr.upload.onprogress = function (e) { if (e.lengthComputable) { var percent = Math.round(e.loaded / e.total * 100); document.getElementById('progress').innerText = percent + '%'; } }; xhr.onload = function () { if (xhr.status === 200) { refreshFileList(); // 重新加载当前目录文件列表 } else { alert('上传失败,状态码:' + xhr.status); } }; xhr.send(formData); });

这段脚本里folderId是额外拼接的业务参数,服务端从表单字段中读取,用来决定文件挂到哪个父目录下。files字段名可以重复出现多次,后端能通过这个键名拿到文件集合。xhr.upload.onprogress事件只有在lengthComputable为 true 时才能计算百分比,某些代理或非标准响应头下该值可能为 false,界面要处理好降级逻辑,避免显示 NaN。上传完成后调用refreshFileList()重新拉取目录列表,这是异步上传相比 PostBack 减少刷屏的关键一步。

3.3 服务端保存:IHttpHandler 与文件重名处理

UploadHandler 是独立于页面之外的IHttpHandler,直接接手原始请求。核心处理逻辑如下:

public void ProcessRequest(HttpContext context) { string folderIdText = context.Request.Form["folderId"]; int folderId = string.IsNullOrEmpty(folderIdText) ? 0 : int.Parse(folderIdText); HttpFileCollection files = context.Request.Files; for (int i = 0; i < files.Count; i++) { HttpPostedFile file = files[i]; // 只取文件名部分,防止客户端绕过路径直接构造非法路径 string safeFileName = Path.GetFileName(file.FileName); // 按目录Id分目录存放物理文件,避免单目录文件过多 string physicalDir = context.Server.MapPath("~/Uploads/" + folderId); if (!Directory.Exists(physicalDir)) Directory.CreateDirectory(physicalDir); // 用时间戳加短GUID做物理文件名,保留原扩展名 string saveName = DateTime.Now.ToString("yyyyMMddHHmmss") + "_" + Guid.NewGuid().ToString("N").Substring(0, 8) + Path.GetExtension(safeFileName); string savePath = Path.Combine(physicalDir, saveName); file.SaveAs(savePath); // 插入资源表:展示名用原始名称,存储名用唯一物理名 InsertFileRecord(folderId, safeFileName, file.ContentLength, folderId + "/" + saveName); } context.Response.Write("{'code':0,'msg':'ok'}"); }

说明几点。Path.GetFileName必须加,因为某些浏览器或者构造请求可能携带"..\\evil.aspx"这类路径,直接拼接进MapPath会写出上传目录甚至覆盖系统文件。物理文件名用“时间戳 + 8位短GUID”组合,这样同一秒内上传多个同名文件也不会互相覆盖,同时保留了原扩展名。folderId为空时按 0 处理,代表文件进入根目录。

需要注意这里有个业务陷阱:InsertFileRecord必须先保存物理文件再写数据库,如果数据库写入失败,文件成了孤儿文件;反过来先写库再保存文件,文件保存失败则数据库里挂着不存在的记录。常见做法是先保存物理文件,写入数据库失败时尝试删除刚保存的文件回滚。更稳妥的方式是引入事务表,但网盘场景中这种概率不高,及时清理临时文件就能接受。

4. 分享提取码的生成、校验与下载响应

4.1 提取码格式与生成算法

源码中分享文件后生成提取码,其他用户访问链接时需要输入提取码。提取码首先要避免混淆字符,0O1I这类字符手输极易出错,所以字符集通常去掉它们。生成逻辑用加密随机数填充,保证并发下不低概率重复:

private static readonly char[] Chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789".ToCharArray(); // 生成指定位数的提取码,默认4位 public static string GenerateShareCode(int length = 4) { var bytes = new byte[length]; using (var rng = new RNGCryptoServiceProvider()) { rng.GetBytes(bytes); } var sb = new StringBuilder(length); foreach (var b in bytes) { sb.Append(Chars[b % Chars.Length]); } return sb.ToString(); }

这里用RNGCryptoServiceProvider而不是Random,是因为Random在同一毫秒内用相同种子可能生成同样的序列,多个用户同时点分享时会撞码。字符集长度 32,byte最大值 255,256 % 32 == 0,所以按b % Chars.Length取下标分布是均匀的。生成后还需要到库里查重,如果已存在就重新生成,最多循环 5 次。4 位码有 32^4 约 100 万种组合,内部系统同时存在的分享链接通常不超过几千个,碰撞概率很低,但查重逻辑是底线保障。

4.2 分享链接的数据库设计与短路径

分享不能直接在资源表上只留一个ShareCode字段,因为同一文件可能被不同用户创建多次分享,不同的提取码对应不同的有效期。独立分享表更好管理,结构如下:

字段类型说明
Idint分享记录主键
NodeIdint被分享的资源节点 Id
ShareCodechar(4)提取码,建立唯一索引
IsValidbit是否有效,0 表示被取消分享
ExpireTimedatetime nullable过期时间,空为永久有效
CreateTimedatetime分享创建时间

分享链接使用查询参数形式,如http://server/Share.aspx?code=AB3D,尽量短,方便用户手输和聊天工具内复制。ShareCode上添加唯一索引,查询分享记录时直接命中索引,不用回表过滤。页面端拿到code参数后,先查分享记录,再关联ResourceNode取得文件名和物理路径。

4.3 提取码比对与文件下载的 C# 实现

分享页面的逻辑分成两步:加载时读取链接里的 code 并保存在 ViewState,用户输入提取码后点击按钮比对。提取码在展示时统一转大写,输入也转大写,达到大小写不敏感的效果:

protected void btnSubmit_Click(object sender, EventArgs e) { string input = txtShareCode.Text.Trim().ToUpper(); string code = ViewState["ShareCode"].ToString(); if (input != code) { lblMsg.Text = "提取码错误,无法下载"; return; } var node = GetNodeByShareCode(code); if (node == null || !node.IsFile) { lblMsg.Text = "分享的资源不存在"; return; } DownloadFile(node); }

注意这里判断提取码错误时不能区分“提取码错误”和“分享不存在”两类提示,防止用户扫描码规律。DownloadFile方法负责写出文件流:

private void DownloadFile(ResourceNode node) { string physicalPath = Server.MapPath("~/Uploads/" + node.FilePath); FileInfo fi = new FileInfo(physicalPath); if (!fi.Exists) return; Response.Clear(); Response.ContentType = "application/octet-stream"; Response.AddHeader("Content-Disposition", "attachment; filename=" + HttpUtility.UrlEncode(node.Name, Encoding.UTF8)); Response.AddHeader("Content-Length", fi.Length.ToString()); Response.TransmitFile(physicalPath); Response.End(); }

Response.TransmitFile直接把服务器磁盘上的文件块写入响应流,不占用托管内存,适合几十 MB 以上的文件下载。Content-Disposition里的文件名用UrlEncode转码,否则浏览器收到中文文件名可能出现乱码或直接截断。下载前必须判断fi.Exists,防止数据库有记录但物理文件被异常删除的情况,这时应提示用户资源异常而不是抛出 500 错误。

至此,提取码校验和下载模块已经能正常工作。如果业务需要分享文件夹,推荐打包成 zip 再走同一套下载流程,但要注意 ZIP 压缩会占用临时空间和 CPU,内部系统建议限制单次分享文件大小。

5. 删除权限、下载防盗链与部署配置

5.1 文件夹递归删除的两种方式

删除分享系统里的文件夹时,最常见的错误是直接DELETE FROM ResourceNode WHERE Id=@id,导致子节点变成孤儿记录。推荐在服务端做递归删除:先收集所有子节点 Id,再逐条删除子节点,最后删除自身。物理文件也要一并清理,否则 Uploads 目录会堆积大量无主文件:

private void DeleteNode(int nodeId) { // 先递归找到所有子节点Id var childIds = QueryIds( "SELECT Id FROM ResourceNode WHERE ParentId=@pid", nodeId); foreach (var childId in childIds) { DeleteNode(childId); } var node = GetNodeById(nodeId); if (node.IsFile) { string fullPath = Server.MapPath("~/Uploads/" + node.FilePath); if (File.Exists(fullPath)) File.Delete(fullPath); } ExecuteNonQuery( "DELETE FROM ResourceNode WHERE Id=@id", nodeId); }

注意递归顺序必须是先处理子节点再删除父节点,否则外键约束会阻止删除或产生孤儿数据。这个操作在深目录下会产生多次数据库往返,数据量大时建议用第 2 章的 CTE 先取全部 Id,再一次性删除,同时批量删除物理文件。

5.2 下载防盗链与访问签名

分享链接里的code只负责第一步身份验证,但一旦用户拿到真实下载地址Download.aspx?id=xxx,绕过分享页直接访问该地址也能下载,分享形同虚设。我习惯在下载地址中追加签名参数,服务端校验通过后才输出文件流:

string secret = ConfigurationManager.AppSettings["ShareSecret"]; string sign = Md5(node.Id + code + secret); string downloadLink = "Download.aspx?id=" + node.Id + "&code=" + code + "&sign=" + sign;

分享页在生成下载链接时计算sign,Download.aspx 收到请求后也按同样算法计算一遍,比对一致才继续。签名参数中的secret只保存在服务端配置里,客户端无法伪造。如果别人拿到了分享链接但没经过分享页,就拿不到正确的 sign,下载会被拒绝。

5.3 IIS 部署时的关键配置

部署到 IIS 7+ 时注意两个坑:第一,system.webServer中的maxAllowedContentLength必须调大,否则大文件上传和下载会得到 404.13;第二,Uploads 物理目录必须禁止执行脚本,防止攻击者上传 aspx 文件后直接通过 URL 访问执行。常用做法是在 Uploads 目录对应的虚拟目录里移除 ASP.NET 的处理程序映射,并在 web.config 中加入:

<location path="Uploads"> <system.webServer> <handlers accessPolicy="Read" /> </system.webServer> </location>

最后检查一下应用池是否开启 32 位应用程序,如果项目用了 32 位的第三方组件,应用池默认关闭 32 位会直接加载失败。部署完成后,用一个包含中文文件名、大小超过 30MB 的测试文件走一遍上传、分享、提取码下载全流程,能通过基本就可以交给业务使用了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询