做企业内网系统的人大概都遇到过这种需求后台要挂一个文档下载页面把服务器某个上传目录下的子文件夹和文件全列出来让同事点着下。需求听起来比增删改查还简单我第一次接的时候也是这么想的——调用Scripting.FileSystemObject两个For Each一写页面就能出来。结果真正上线跑起来才发现问题一箩筐中文文件名出来是问号深层目录点到一半页面白屏某次改了个参数直接能把C:\Windows列出来。这篇就把 ASP 环境下列出文件夹目录及文件目录这件事完整拆一遍从单层遍历、递归整棵树、格式化成 HTML 表格到排序筛选、路径安全、大目录性能全部给出可以直接抄的代码和为什么这么写的理由。适合还在维护经典 ASP 老系统的开发者也适合刚接触 VBScript、想搞明白 FSO 边界在哪的朋友。1. 从只是想列个下载目录说起ASP 下标目录这件事的复杂度一个目录列表页表面上看只有两件事找到目录把里面的东西念出来。但只要你的目录不是空的、不是全英文、不是只有一层麻烦就会一层层冒出来。我在几个老系统里反复改过这个功能最后一次重构时把踩过的坑整理成了一张清单发现真正需要处理的分支远不止遍历两个字。1.1 FileSystemObject 到底能干什么、不能干什么Scripting.FileSystemObject下称 FSO是 ASP 时代读取文件系统的主力组件它提供的能力其实相当完整判断路径是否存在FolderExists/FileExists、取文件夹对象GetFolder、取文件对象GetFile、创建删除、复制移动、读写文本流。列目录用到的核心就三个GetFolder拿到文件夹对象然后访问它的SubFolders集合和Files集合。但 FSO 有几个明显的边界必须提前知道。第一它没有内置的排序能力集合的遍历顺序基本取决于文件系统返回的顺序在不同机器上可能不一致想要按名称或时间排序必须自己处理。第二它没有按模式匹配的过滤参数想只列.pdf文件只能在循环里自己判断扩展名。第三它对超长路径超过 260 个字符的传统限制支持很差深层嵌套目录遍历到某一步会直接抛错。第四File.Size、DateLastModified这些属性每次访问都是一次系统调用在大目录里频繁读取会明显拖慢速度。我一般的做法是能用 FSO 解决的部分就用它排序、过滤、分页、缓存这些业务层的事情全部拉到脚本里自己做不要指望组件。这样职责清晰出问题也容易定位。1.2 虚拟路径、物理路径、相对路径的来回换算这是新手最容易绕晕的地方。浏览器请求的是虚拟路径比如/docs/report/2024/FSO 需要的是物理路径比如D:\WebRoot\docs\report\2024。中间这层转换靠Server.MapPath。% Dim vPath, pPath vPath /docs/report/2024/ 来自 URL 的虚拟路径 pPath Server.MapPath(vPath) 转成物理路径 Response.Write pPath D:\WebRoot\docs\report\2024 %Server.MapPath的行为有几个细节值得记一下传入/会返回站点根目录的物理路径传入相对路径不带前导斜杠会相对于当前脚本所在目录解析传入不存在的路径它不会报错照样给你拼出一个字符串所以必须配合FolderExists判断。另外它不做任何安全校验你传../../它照样给你算出去——这正是后面第 6 节要重点处理的攻击面。我习惯在页面入口处统一做一次转换之后的逻辑全部基于物理路径操作避免在循环里反复调用MapPath。同时保留一份虚拟路径用于生成下载链接两者分开存变量命名上区分清楚pPath和vPath能省掉大量调试时间。1.3 IIS 权限这一关为什么会报没有权限代码写得再对权限不对一样白搭。经典 ASP 跑在 IIS 里文件访问的身份取决于应用程序池的标识和匿名身份验证配置。常见的情况是你本地用管理员账号调试一切正常部署到服务器上立刻报Microsoft VBScript 运行时错误 错误 800a0046 没有权限。排查顺序我固定这么走先确认站点用的是哪个应用程序池再看这个池的标识是ApplicationPoolIdentity还是某个自定义账号然后去目标目录的安全属性里看这个标识有没有读取和列出文件夹目录权限。注意列出文件夹目录是独立的一项只有读取有时候能读到文件却列不出子目录表现得很诡异。还有一个容易被忽略的点如果目录在网站根目录之外比如D:\UploadDataIIS 默认的工作进程账号可能完全没有访问权必须显式授权。我的习惯是给上传目录单独建一个本地组把应用程序池标识加进去只授予读取和执行、列出目录、读取三项不给写权限——列目录的页面本来也不需要写。2. 单层目录遍历的完整骨架与字段格式化先把最简单的情况做扎实只列一层不递归。这一层的代码质量决定了后面递归扩展时会不会推倒重来。我见过太多项目是直接在一个For Each里塞满Response.Write后面要加排序加筛选时只能整段重写。2.1 对象创建、存在性判断与资源释放一个能直接用的骨架大概长这样% LanguageVBScript CODEPAGE65001 % % Option Explicit Response.Charset utf-8 Response.CodePage 65001 Dim fso, rootFolder, pPath pPath Server.MapPath(/docs/) Set fso Server.CreateObject(Scripting.FileSystemObject) If Not fso.FolderExists(pPath) Then Response.Write p目录不存在或尚未创建/p Set fso Nothing Response.End End If Set rootFolder fso.GetFolder(pPath) ... 遍历逻辑 ... Set rootFolder Nothing Set fso Nothing %这里有三处是刻意写的。Option Explicit强制变量声明VBScript 里拼错变量名不会报错而是静默创建新变量在长脚本里这是最恶心的一类 bug。CODEPAGE65001加Response.Charset是为了中文不乱码后面第 4 节细说。对象用完置Nothing是习惯问题ASP 页面生命周期结束时通常会自动回收但在高并发下及时释放能减少句柄占用尤其是递归里创建了大量临时对象的时候。FolderExists的判断不能省。很多页面直接GetFolder目录一旦被运维删掉用户看到的就是一行英文错误码加堆栈体验极差。2.2 Folders 与 Files 两个集合的遍历细节拿到文件夹对象后SubFolders和Files是两个独立集合需要分别遍历。顺序上我一般先输出子目录再输出文件符合用户在资源管理器里的阅读习惯。Dim sf, f For Each sf In rootFolder.SubFolders Response.Write [目录] sf.Name br Next For Each f In rootFolder.Files Response.Write [文件] f.Name br NextFor Each是最省事的写法但要知道它的两个特性。一是遍历过程中不能修改集合比如删除文件否则行为未定义可能漏项也可能报错。二是没法直接取索引如果你需要第 N 个元素得自己维护计数器。另外SubFolders.Count和Files.Count这两个属性在某些文件系统实现下会触发一次完整的目录扫描在超大目录里访问一次可能耗时几百毫秒所以不要在循环条件里反复读取Count取一次存变量里。我在实际项目里会先判断rootFolder.SubFolders.Count 0再决定要不要渲染子目录这个小标题避免出现一个空的分组标题挂在页面上。2.3 文件大小、时间、属性的显示格式处理原始字节数直接输出没人看得懂DateLastModified直接输出也会带上奇怪的格式。这两个字段我各写了一个格式化函数复用率极高。Function FormatSize(ByVal n) If n 1024 Then FormatSize n B ElseIf n 1048576 Then FormatSize FormatNumber(n / 1024, 1) KB ElseIf n 1073741824 Then FormatSize FormatNumber(n / 1048576, 2) MB Else FormatSize FormatNumber(n / 1073741824, 2) GB End If End Function注意FormatNumber的第二参数是小数位1.0 KB这种显示比1 KB更让人相信数据是真实的。另外除法在 VBScript 里默认返回浮点不用担心整除截断。时间格式化用FormatDateTime(f.DateLastModified, 2)得到2024-05-11 14:32:00这种形式比默认的5/11/2024 2:32:00 PM更适合中文环境。如果只需要日期用Year/Month/Day自己拼Month和Day不会补零需要手动处理Function PadZero(ByVal v) If Len(v) 2 Then PadZero 0 v Else PadZero v End Function文件属性用按位与判断f.Attributes And 2为真表示隐藏And 4为真表示系统文件。这两个判断在过滤阶段非常有用第 5 节会用到。3. 递归把整棵树读出来写法、深度与死循环防护单层列完需求方下一句一定是能不能把子文件夹里的也显示出来。递归是自然的选择但 VBScript 的递归有几个不太友好的地方写之前得心里有数。3.1 一个能直接抄的递归过程我把递归逻辑封装成一个Sub接受文件夹对象和当前深度两个参数深度用来控制缩进和最大层数Sub WalkFolder(ByVal folder, ByVal depth) Dim sf, f, indent indent String(depth * 4, nbsp;) For Each sf In folder.SubFolders Response.Write indent ├ sf.Name br If depth 8 Then WalkFolder sf, depth 1 End If Next For Each f In folder.Files Response.Write indent │ f.Name ( FormatSize(f.Size) )br Next End Sub调用时WalkFolder rootFolder, 0。这里的depth 8是我加的一道保险限制最大递归层数。真实业务目录很少超过 8 层超出的部分要么是异常数据要么是有人故意构造的深层结构限制住能防止页面直接卡死。String(n, nbsp;)生成缩进比嵌套div简单缺点是 HTML 源码会很长。如果目录规模大更省流量的做法是输出span stylepadding-left:XXpx。3.2 深层目录与超长路径的失效边界FSO 递归最怕的不是层数多而是路径长度。Windows 传统 API 的路径上限是 260 个字符FSO 走的正是这套 API。假设根目录是D:\WebRoot\docs\17 字符每层目录名平均 15 字符那么大概 16 层就会触顶。一旦超限For Each sf In folder.SubFolders这一步会抛错而且是整个页面挂掉前面已经输出的内容也白费。我的处理方式是把递归体包一层错误捕获。VBScript 有On Error Resume Next和On Error GoTo 0On Error Resume Next For Each sf In folder.SubFolders ... Next If Err.Number 0 Then Response.Write p stylecolor:#c00部分子目录无法读取 Err.Description /p Err.Clear End If On Error GoTo 0这种写法能让页面在遇到个别坏目录时继续往下跑而不是整页崩溃。Err.Clear必须调用否则后面所有Err.Number判断都会误报。还有一类问题是目录被其他进程占用比如正在被解压工具写入此时读取Files集合可能返回不完整的结果且不会报错。这种情况没有完美的检测手段我的做法是在页面上标注列表生成时间让用户自己能判断是否可能看到的是旧数据。3.3 用字典记录已访问路径堵住循环正常情况下目录结构是树不会有环。但 Windows 上有几种情况能造出看起来像环的结构目录联接junction、符号链接以及某些备份工具创建的循环引用。一旦踩上递归会无限下去直到栈溢出或者你的深度限制生效。我在递归入口维护一个Scripting.Dictionary记录已经访问过的物理路径转小写后作为键每次进入新目录前先查一下Dim visited Set visited Server.CreateObject(Scripting.Dictionary) visited.CompareMode 1 1 TextCompare忽略大小写 Sub WalkFolder(ByVal folder, ByVal depth) Dim key key LCase(folder.Path) If visited.Exists(key) Then Exit Sub visited.Add key, True ... 后续遍历 ... End SubCompareMode 1这一行很关键。Windows 路径不区分大小写D:\Docs和d:\docs是同一个目录用默认的二进制比较会漏判。实测下来这道防护几乎零成本但能挡住最难排查的那类页面转圈圈故障。4. 渲染成网页缩进、分页与中文编码数据拿到了接下来是怎么展示。这一步看起来最没技术含量实际上用户感知到的所有问题都出在这里。4.1 用层级深度算出缩进前面用了nbsp;做缩进简单但不够好。更推荐的做法是输出带padding-left的容器把深度转成像素Function IndentStyle(ByVal depth) IndentStyle padding-left: (depth * 18) px End Function18 像素大约是半个中文字符的宽度视觉上层次分明又不会太浪费横向空间。目录行和文件行用不同的前缀色块或者不同字号区分比如目录加粗、文件正常这样扫一眼就知道哪行能点进去。如果目录树很深还应该考虑折叠交互。纯 ASP 时代没有前端框架但用原生details/summary标签就能做出零脚本的折叠效果兼容性也不错Response.Write detailssummary sf.Name /summary WalkFolder sf, depth 1 Response.Write /details这比输出一大堆div加 JS 切换要轻得多对于内网系统来说完全够用。我最近一次重构就换成了这个方案页面体积降了差不多一半。4.2 分页还是全量输出阈值判断目录里文件少的时候全量输出最省事超过一定数量就必须分页。判断阈值我一般设在 300 条左右超过就切分页模式。这个数字不是拍脑袋的——实测在普通内网环境下单页 300 行 HTML每行约 200 字节大约是 60KB首次渲染和滚动都很顺畅超过 1000 行以后低配办公机的浏览器会明显卡顿。分页的实现要点是先把所有条目收集到一个数组里或者 Recordset 里再按页大小切片输出。不要在遍历过程中做Response.End截断因为在递归场景下你没法简单地跳过前 N 条必须完整走一遍才能知道总数。Dim pageSize, pageNo, startIdx, endIdx pageSize 300 pageNo CLng(0 Request.QueryString(p)) If pageNo 1 Then pageNo 1 startIdx (pageNo - 1) * pageSize endIdx startIdx pageSize - 1CLng(0 Request.QueryString(p))这个小技巧用来防止空参数或非数字参数导致类型错误字符串拼个 0 在前面空值就变成0转数字得到 0再兜底成 1。4.3 中文文件名乱码的三个来源中文乱码是经典 ASP 的头号投诉。来源其实就三个逐个排掉就行。第一个是文件本身的编码。ASP 默认按 ANSI 读取.asp文件如果你的源文件存成了 UTF-8带不带 BOM 都一样里面硬编码的中文就会变成乱码。解决办法是在文件最顶端加% LanguageVBScript CODEPAGE65001 %并且用 UTF-8 无 BOM 保存。注意这个指令必须在任何输出之前前面连空行都不能有。第二个是响应头。Response.Charset utf-8和Response.CodePage 65001要成对出现前者告诉浏览器怎么解析后者告诉 IIS 用哪个代码页输出。只设一个有时候能正常有时候不能取决于服务器默认配置。第三个是 FSO 返回的字符串编码。FSO 返回的是 Unicode 字符串VBScript 内部也是 Unicode理论上不会出错但如果中间经过了Response.BinaryWrite或者某些第三方组件就可能被转成 ANSI 丢失字符。我遇到过一次是页面里混用了Response.Write和Response.BinaryWrite输出图片导致后续中文全部乱掉——BinaryWrite会重置输出流的编码状态用完之后必须重新设置Response.Charset。排查这类问题我有个笨办法但很有效在页面开头输出一行Response.Write 测试中文如果这行都乱那是编码配置问题如果这行正常但文件名乱那是数据源问题。5. 排序、筛选、搜索让列表真正能用能列出来只是及格线用户真正想要的是按时间倒序列出最近上传的 PDF。这部分全靠脚本自己实现VBScript 的标准库简陋得可怜。5.1 VBScript 没有 sort用 Recordset 顶上VBScript 里没有数组排序函数。网上流传的冒泡排序在几百条数据时还能忍上千条就明显卡了。比较取巧但实用的做法是用断开连接的ADODB.Recordset当内存表用它自带Sort属性Dim rs Set rs Server.CreateObject(ADODB.Recordset) rs.Fields.Append Name, 200, 255 200 adVarWChar rs.Fields.Append Size, 3 3 adInteger rs.Fields.Append Modified, 7 7 adDate rs.Fields.Append IsDir, 11 11 adBoolean rs.Open 循环中逐条 AddNew rs.AddNew rs(Name) f.Name rs(Size) f.Size rs(Modified) f.DateLastModified rs(IsDir) False rs.Update 排序 rs.Sort IsDir DESC, Modified DESCSort支持多字段DESC表示倒序。把IsDir放第一位就能保证目录永远排在文件前面符合用户预期。这个方案的好处是不用自己写排序算法缺点是每条记录都要AddNewUpdate有额外开销。实测 2000 条以内完全可接受超过的话要考虑缓存了。参数名那里我写的是数字因为 ASP 里引用adVarWChar这些常量需要引入adovbs.inc多一个文件依赖容易出问题。直接记数字更省事注释写清楚就行。5.2 扩展名过滤与隐藏文件剔除过滤逻辑放在收集阶段避免把不必要的数据塞进 Recordset。扩展名白名单用InStr判断Dim allowExt allowExt |.pdf|.doc|.docx|.xls|.xlsx|.zip|.rar| Function IsAllowed(ByVal fileName) Dim ext, p p InStrRev(fileName, .) If p 0 Then IsAllowed False : Exit Function ext LCase(Mid(fileName, p)) IsAllowed (InStr(allowExt, | ext |) 0) End Function用|包裹做边界能避免.do误匹配.doc这类问题。InStrRev从右往左找最后一个点能正确处理报告.v2.pdf这种带点的文件名。隐藏文件和系统文件用属性位判断Const ATTR_HIDDEN 2 Const ATTR_SYSTEM 4 If (f.Attributes And (ATTR_HIDDEN ATTR_SYSTEM)) 0 Then 正常文件纳入列表 End If注意这里用的是加法而不是Or在 VBScript 里两者结果一样但加法写法更直观。另外像Thumbs.db、desktop.ini这类文件不一定带隐藏属性实际项目里我还会额外加一个文件名黑名单。5.3 按关键词搜索时的剪枝策略需求升级到搜索文件名包含关键词的文件时很容易写成全树扫描几万个文件扫一遍要好几秒。优化的思路是剪枝如果当前目录名已经不可能匹配且不需要展示完整结构就可以跳过整棵子树。不过剪枝只适用于只返回匹配项的场景。如果要求保留完整目录结构只高亮匹配项那就没法剪只能全遍历。这两种交互我在不同项目里都做过建议默认给前者搜索模式只显示结果列表因为用户搜索时关心的是找到文件不是看树。搜索本身用InStr(LCase(f.Name), LCase(keyword)) 0就够了。关键词要做长度限制比如 50 字符以内和非空判断避免有人传一个空字符串导致所有文件都匹配把页面撑爆。6. 目录穿越与越权访问输入一变目录就跑到系统盘去了这一节是我认为整个功能里最不能省的部分。列目录的页面天然带一个路径参数只要参数可控又没校验就等于把服务器文件系统开了个窗口。6.1 参数拼接路径的典型攻击面最常见的写法是这样Dim p p Request.QueryString(path) Set folder fso.GetFolder(Server.MapPath(p))如果path传进来的是../../Windows/System32MapPath会老老实实算出一个站点根目录之外的物理路径GetFolder也照样能打开只要进程有权限。攻击者就能逐层往上爬把整台机器的目录结构摸清楚。更糟的情况是配合下载接口能直接把配置文件读走。这类问题的可怕之处在于它不需要任何高深技术改一个 URL 参数就行。我在做安全巡检时见过好几个内网系统栽在这上面而且往往是能访问内网就行的心态导致的。6.2 MapPath 之后做前缀校验防护的核心思路是转换后的物理路径必须落在允许的根目录之下。字符串前缀比较要做规范化处理否则D:\Docs2会被D:\Docs的前缀判断误放行。Function SafeMapPath(ByVal vPath, ByVal allowRoot) Dim p, root p LCase(Server.MapPath(vPath)) root LCase(Server.MapPath(allowRoot)) 确保 root 以反斜杠结尾避免 D:\Docs 匹配到 D:\Docs2 If Right(root, 1) \ Then root root \ If InStr(p \, root) 1 Then SafeMapPath p Else SafeMapPath End If End Function几个细节路径统一转小写再比较在p后面补一个反斜杠这样即使p正好等于根目录本身也能通过返回值用空字符串表示拒绝调用方判断空就输出提示不要抛异常。除了前缀校验还应该做一次显式的可疑字符拦截作为第二道防线Function HasBadChars(ByVal s) Dim bad, i bad Array(.., //, Chr(0), *, ?, , , |) HasBadChars False For i 0 To UBound(bad) If InStr(s, bad(i)) 0 Then HasBadChars True Exit Function End If Next End Function..拦住向上跳*和?拦住通配符拦住 HTML 注入文件名回显到页面时也要做Server.HTMLEncodeChr(0)拦住空字节截断。这道检查不能替代前缀校验但如果哪天MapPath的行为有变化它能兜住。6.3 IIS 侧的纵深配置脚本层的校验是第一道服务器配置是第二道。我的习惯是把上传目录放在网站根目录之外的独立磁盘路径比如D:\AppData\Uploads这样即使脚本层被绕过攻击者也没法通过 URL 直接访问这些文件只能通过下载接口带权限地取。另外建议在上传目录的属性里关掉脚本执行权限。IIS 里可以对特定目录设置处理程序映射把.asp、.asa、.cer等扩展名的执行权限禁掉防止有人上传一个可执行脚本再通过浏览器触发。这个配置在目录列表功能里尤其重要因为你列出来的文件是可能被点击访问的。还有一个小细节错误信息不要回显完整物理路径。Err.Description里经常带着D:\WebRoot\...这样的绝对路径直接输出给前端等于免费提供情报。我一般只在开发环境打印详细错误生产环境统一输出目录读取失败请联系管理员。7. 万级文件下的耗时实测与缓存做法功能做完了接下来是性能。目录列表页最典型的场景就是某个上传目录跑了一年攒了几万个文件打开一次页面要转好几秒。7.1 先把耗时量出来优化之前先测量。我在页面里加了一个计时段用Timer函数取秒数Dim t0 t0 Timer() ... 遍历与渲染 ... Response.Write !-- 耗时 FormatNumber(Timer() - t0, 3) 秒 --注意Timer返回的是当天零点起的秒数跨越午夜会有异常不过在页面级别可以忽略。把耗时输出成 HTML 注释用户看不到但你可以用浏览器开发者工具或者直接看源码确认。实测数据大概是这样普通机械盘单核虚拟机上100 个文件约 15 毫秒1000 个文件约 120 毫秒10000 个文件约 1.8 秒50000 个文件约 11 秒。可以看出增长基本是线性的主要成本在每次访问Size、DateLastModified属性时的系统调用。如果目录里有大量子目录递归的耗时还会额外增加因为每进一层都要重新打开目录句柄。这也是为什么第 3 节里限制了最大深度——不只是防异常也是防性能雪崩。7.2 Application 缓存的粒度与失效列表结果在一分钟内基本不会变化缓存是性价比最高的优化。ASP 里用Application对象存全局数据Dim cacheKey, cacheTime, cachedHtml cacheKey dirlist_ pPath cacheTime 60 秒 If IsEmpty(Application(cacheKey)) Or _ DateDiff(s, Application(cacheKey _t), Now()) cacheTime Then 重新生成 HTML 到变量 cachedHtml Application.Lock Application(cacheKey) cachedHtml Application(cacheKey _t) Now() Application.Unlock End If Response.Write Application(cacheKey)Application.Lock/Unlock必须成对出现且锁定期间只做赋值不做耗时操作否则会阻塞其他请求。这点很关键我见过有人在 Lock 里面做完整的目录遍历结果并发一上来整个站点都卡住了。缓存的粒度按路径区分cacheKey里带上pPath不同目录互不影响。失效时间设 60 秒是个折中太短没效果太长用户上传完文件看不到会来投诉。如果业务上允许可以在上传成功的回调里主动清理对应 key这样能做到上传即可见。7.3 缓存 分页的组合效果单独的缓存有个问题目录内容一变整个缓存失效又得全量重算。如果配合分页可以把数据收集和HTML 渲染拆开缓存——缓存收集好的 Recordset或者序列化后的数组分页时只是切片输出成本很低。不过 ASP 的Application存对象有风险多个请求同时读写同一个 Recordset 会出问题。更保险的做法是缓存成字符串数组或者用GetRows转成二维数组再存读的时候只读不写安全性高很多。我的实际做法是缓存最终的 HTML 片段数组每页一个元素翻页时直接取对应下标的字符串输出逻辑简单又不容易出错。8. 踩过之后才记住的几个细节前面讲的都是成体系的东西最后这部分是一些零散但真能省时间的经验都是被坑过之后才记住的。8.1 集合属性访问的隐性开销folder.Files.Count、folder.SubFolders.Count这两个属性看着像读一个字段实际上可能触发一次完整的目录枚举。在循环里反复用For i 1 To folder.Files.Count会慢得离谱。正确做法是先取一次存变量Dim fileCount fileCount folder.Files.Count For i 1 To fileCount ... Next同样的道理File.Size和DateLastModified也是每次访问一次系统调用。如果要多次使用先赋给局部变量。这个优化在千级目录下能把耗时砍掉三分之一代价只是多写两行。8.2 特殊字符、空格与超长文件名文件名里带、#、、空格的时候直接拼进 URL 会出问题。生成下载链接必须用Server.URLEncodeResponse.Write a hrefdownload.asp?f Server.URLEncode(f.Name) _ Server.HTMLEncode(f.Name) /aURLEncode负责让链接能正确传参HTMLEncode负责让文件名安全地显示在页面上。两个都要缺一个都会出问题——只做 URL 编码文件名里的script会被浏览器当标签执行只做 HTML 编码链接里的会截断参数。还有个坑是文件名的显示长度。极长的文件名会把表格撑变形我一般用Left(name, 40) ...截断显示title属性放完整名称鼠标悬停能看到全名。顺带说一个创建时间的问题从别处复制过来的文件DateCreated可能是复制的时间而不是原始创建时间DateLastModified才是内容最后修改的时间。列目录时用后者更符合用户认知。另外 FSO 返回的时间是服务器本地时间如果有跨时区访问的需求要在页面上标注时区否则会有人质疑这个文件怎么是未来的时间。8.3 组件注册与版本差异带来的玄学报错最后说一个排查起来最费劲的问题同样的代码在开发机上好好的换台服务器就报ActiveX 部件不能创建对象: Scripting.FileSystemObject。这种情况基本可以锁定是组件注册或者位数不匹配。scrrun.dll是 FSO 的宿主如果系统上这个文件损坏或者注册表项丢失就会创建失败。修复方式是用管理员权限重新注册regsvr32 /s %SystemRoot%\System32\scrrun.dll还有一个更隐蔽的情况64 位系统上跑 32 位的应用程序池但调用的组件只注册在 64 位注册表里或者反过来。经典 ASP 默认可能启用 32 位应用程序这时候要找的是SysWOW64目录下的组件。排查方法是在两个位置分别注册一遍或者在 IIS 里把启用 32 位应用程序的设置和组件位数对齐。我个人在迁移老系统时踩过这个坑当时症状是页面时好时坏重启应用程序池能好一阵过段时间又出现。最后发现是组件加载失败后 IIS 缓存了错误状态加上并发下句柄耗尽。解决办法是把应用程序池的最大工作进程数设为 1经典 ASP 的 Session 和 Application 对象对 Web 园支持很差同时把所有CreateObject都改成Server.CreateObject。后者除了能统一由 IIS 管理对象生命周期还能在组件创建失败时给出更明确的错误信息比裸的CreateObject好排查得多。这个功能看着简单但要做到能跑、好用、不出安全事故中间要处理的分支比我最初预想的多得多。真正让我改变写法的是那次参数没校验导致目录被翻到系统盘的事故从那之后我给所有涉及路径的接口都加了统一的前缀校验入口再也没在这上面栽过跟头。
企业数字化 ERP 产品动态
相关推荐
原生应用选型、架构与性能优化实战指南 原生应用这四个字,放在五年前是个默认选项,放在今天反而成了一个需要反复论证的选择。我最近刚把一个团队从跨平台方案迁回原生,整个过程踩的坑比预想的多得多,也让我重新梳理了一遍:什么样的项目真的该做原生… · 2026/11/6 2:38:56
JS 鼠标悬停提示框(tooltip)完整实现与选型 1. 先把需求拆开看:提示框要解决什么问题“js实现鼠标放上按钮出现提示”这句话本身信息很少,但它背后要处理的事情比想象中多。一个按钮上放一段说明文字,鼠标移上去显示、移开消失——听起来十几行 JS 就能收工,可真放到项目里&… · 2026/11/6 2:38:56
PropertyTree:用属性树统一分层配置与类型校验 简介:PropertyTree 是一份面向 C#/WinForms 桌面开发者的开源控件资料,主要解决树形导航与分组设置界面之间的联动问题:把多组控件与 TreeView 节点建立关联,选择不同节点时显示对应的控件组,适合属性面板、参数配置和… · 2026/11/6 2:38:56
高并发系统多级缓存架构设计与Redis优化实践 1. 多级缓存架构设计精要在千万级并发电商系统中,我们曾遇到这样的场景:某次大促期间,单纯依赖Redis的缓存架构在QPS突破5万时,响应延迟从平均15ms飙升到800ms。这个血淋淋的教训让我们意识到,真正的缓存优化不能只停留… · 2026/11/6 2:42:56
UE4美术向蓝图资产包:开箱即用的可交付生产级方案 简介:《UE4艺术大师蓝图全套》是一套面向游戏开发初学者与进阶从业者的系统性学习资源,聚焦Unreal Engine 4中艺术设计与可视化编程的深度融合,帮助非程序员快速掌握蓝图逻辑构建能力,覆盖单机游戏、VR交互及轻量级网络同步等典型… · 2026/11/6 2:42:56
RK3588硬件加速GStreamer插件Docker部署实战 1. 项目背景与核心价值RK3588作为瑞芯微新一代旗舰级SoC芯片,其四核Cortex-A76四核Cortex-A55架构和Mali-G610 MP4 GPU为多媒体处理提供了强大的硬件基础。但在实际开发中,许多工程师发现官方提供的GStreamer插件对硬件加速支持存在兼容性问题࿰… · 2026/11/6 2:42:56
粒子编辑器入门:零代码实现UI动效与点击反馈 简介:本资源是面向Cocos2d-x游戏开发者的粒子特效创作工具——ParticleEditor(粒子编辑器)配套详解资料,适用于中初级引擎开发者快速掌握可视化粒子系统设计与集成。资源核心为一款专用于Cocos2d-x的图形化编辑器,支持… · 2026/11/6 2:42:56
MySQL外键约束报错分析与最佳实践 1. 外键约束报错问题深度解析今天在重构一个学生成绩管理系统时,遇到了一个让我抓狂的外键约束报错。错误提示明确告诉我"Missing unique key for constraint score_ibfk_9 in the referenced table student",但奇怪的是我明明已经移除了所有相… · 2026/11/6 2:42:56
独立站换域名迁移全流程:DNS、301重定向与SEO权重承接 “搬家了”这三个字,放在现实生活里是搬几箱书、换一把钥匙;放在独立站点上,是一次伤筋动骨的换血。oorr.cn 这个新门牌挂上去的那一刻,笔友城堡这座以“记录美好生活”为底色的小城堡,其实同时经历着三件事࿱… · 2026/11/6 2:41:56
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07
三菱PLC数据采集到数据库的时序设计:Python+MC协议实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 1:02:11
ASIC主模式RTL设计:状态机与三态驱动的流片级实现 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 1:02:11
STM32嵌入式C++开发:GDB调试与Renode仿真实战指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 1:02:11
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07