说实话,我第一次接到"JSP页面怎么处理文件夹上传"这个需求时,心里是有点犯嘀咕的。因为任何一个做过Web开发的人都知道,浏览器的<input type="file">天生只支持单选或者多选文件,压根没有"文件夹"这个概念。当时后台管理系统的用户提了个需求:把整个项目的资料目录(几十个分类子文件夹,里面塞满了照片、Word、PDF)一次性传上来,还要在服务器上保留原来的目录层级。要是按传统方式一个个文件上传,别说用户,我自己都想骂人。所以那段时间我集中把JSP + Servlet场景下的文件夹上传整个链路摸了一遍,从HTML5的webkitdirectory属性到后端的Part接口,再到目录还原和路径安全,踩了不少坑,今天把完整方案和教训一起写出来。
1. HTTP协议里根本没有"文件夹",那文件夹上传到底传的是什么
要理解文件夹上传,先得搞清楚一个关键事实:HTTP协议层面,multipart/form-data这个格式本身是支持一次请求携带多个文件的。它把请求体按boundary(边界)拆成多个part,每个part可以理解为"一个独立的小包裹",包裹里有自己的Content-Disposition头、Content-Type头,以及文件内容。所以从协议角度讲,一次上传100个文件和上传1个文件,本质区别只是part的数量不同,协议完全不排斥批量文件。
那为什么以前做文件夹上传很费劲?卡点不在后端,而在浏览器前端。<input type="file">的文件选择框是浏览器固有的UI,它只允许用户从文件列表里挑文件,不允许挑目录。就算加了multiple属性,也只能多选文件,选完之后你拿到的还是一个扁平的FileList,文件之间的目录层级信息是丢掉的。
所以,业内说的"文件夹上传",实际要解决两件事:第一,让浏览器的文件选择框能选中一个目录以及目录下的所有文件;第二,把所有文件连同它们的相对路径一起提交给后端,由后端在磁盘上重建目录结构。第一件事靠HTML5的webkitdirectory属性解决,第二件事靠webkitRelativePath这个文件属性解决。明白了这两点,后面的方案才能理解到位。
我见过不少项目在这个问题上绕远路,有人用压缩包方式,让用户先把文件夹压成zip再上传,服务器解压——这条路也能用,但对非技术用户太不友好,而且多了一次压缩解压的等待。还有人用ActiveX控件或Flash插件去拿本地路径,且在现在的主流浏览器里基本全被禁了。HTML5 File API的方案干净、跨浏览器、不需要装任何插件,是当前最合理的默认选择。
html复制<form action="uploadFolder" method="post" enctype="multipart/form-data" id="uploadForm">
<input type="file" id="folderInput" webkitdirectory multiple />
<button type="submit">上传整个文件夹</button>
</form>
这里的关键就在webkitdirectory这个非标准属性。加上它之后,文件选择框摇身一变变成了目录选择框,用户可以直接选中一个文件夹。multiple属性要不要留?建议留着,在某些浏览器里这能让用户再追加选其他文件夹(虽说兼容性不完全一致,但不影响主流程)。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 前端JSP页面:拿到文件列表,再把目录结构装进请求
2.1 从File对象里挖出相对路径
选中文件夹之后,<input>元素上的files属性会返回一个FileList,这个列表包含了文件夹下的所有文件。这里有个特别重要的细节:每个File对象上除了常规的name、size、type属性外,还有一个webkitRelativePath属性,它直接返回一个相对路径字符串,比如images/2024/01/photo.jpg这样的格式——开头是文件夹根目录名,后面是各级子目录名,最后是文件名。
我用一个简单的change事件把文件列表打出来验证过:
javascript复制document.getElementById('folderInput').addEventListener('change', function(e) {
const files = e.target.files;
for (let i = 0; i < files.length; i++) {
console.log(files[i].webkitRelativePath);
}
});
在Chrome和Edge里,控制台会清晰打印出每个文件的相对路径。这个属性就是后面还原目录结构的钥匙。
如果用户用的是老掉牙的浏览器,webkitRelativePath可能是空的,这时可以退而求其次,用file.name当作文件名,丢到根目录下。但说实话,现在还在用IE/旧版Safari跑内部系统的场景已经很少了,我在后面兼容性部分再细说。
2.2 用FormData把文件和路径一起提交
确认能拿到路径之后,下一步就是把所有文件塞进FormData。FormData的好处是不用手动设置Content-Type,浏览器会自动生成带boundary的multipart/form-data请求体。
javascript复制const input = document.getElementById('folderInput');
input.addEventListener('change', function(e) {
const files = e.target.files;
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
const file = files[i];
const relativePath = file.webkitRelativePath || file.name;
// 第三个参数指定文件名,这里直接传相对路径
formData.append('folderFiles', file, relativePath);
}
// 用XMLHttpRequest发请求,不按传统form提交,为的是能拿进度
const xhr = new XMLHttpRequest();
xhr.open('POST', 'uploadFolder', true);
xhr.send(formData);
});
这里要专门解释一个不太多人注意的细节:formData.append(name, file, filename)的第三个参数filename,会直接覆盖Part头里的filename字段。所以当你把webkitRelativePath传进第三个参数,后端通过getSubmittedFileName()拿到的就不是单纯的文件名,而是完整相对路径,比如docs/guide/readme.md。这一步的路径信息传递,是整个方案能不能在后端重建目录的关键。我在一开始的实现里没传第三个参数,结果后端拿到全是一堆扁平文件名,目录结构完全还原不了,只能又改了一版,这个坑大家一定记住。
2.3 前端要不要做文件过滤
文件量大时,前端最好做一层基础过滤,不然垃圾文件(如Thumbs.db、.DS_Store)也会传上来,白白占带宽和服务器磁盘。我一般会过滤掉隐藏文件和系统生成文件:
javascript复制for (let i = 0; i < files.length; i++) {
const path = files[i].webkitRelativePath || files[i].name;
const fileName = path.split('/').pop();
// 跳过临时文件和系统文件
if (fileName.startsWith('.') || fileName === 'Thumbs.db' || fileName === '.DS_Store') {
continue;
}
formData.append('folderFiles', files[i], path);
}
再提醒一句:前端的过滤只是体验优化,后端必须做同样的校验甚至更严格的校验。别指望用户传上来的都是正经文件,后面讲安全时我会专门说路径穿越问题。
3. 后端Servlet:如何把multipart请求里的每个文件"拆包"落盘
3.1 看一眼请求体到底是什么结构
前端搞定后,后端要接得住。在Servlet 3.0之后,Java Web开发可以用原生API处理文件上传,不需要再依赖Apache Commons FileUpload。请求到达你的Servlet时,容器(比如Tomcat)已经帮你把multipart/form-data解析好了,你只需要通过request.getParts()拿所有Part就行。
如果你好奇请求体长什么样,我建议在本地抓一下包,或者在Servlet里打日志打印part.getSubmittedFileName()和part.getContentType()。一个典型的多文件请求体大概长下面这样:
code复制------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="folderFiles"; filename="docs/guide/readme.md"
Content-Type: text/markdown
(这里是文件内容)
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="folderFiles"; filename="images/logo.png"
Content-Type: image/png
(这里是文件内容)
------WebKitFormBoundary7MA4YWxkTrZu0gW--
看到没?filename字段里就是前端传的相对路径。后端的任务很简单:遍历每个Part,取文件名,把它当作相对路径去创建文件夹、写文件。
3.2 用Part API逐文件落盘
我用的Servlet版本是Java 8 + Tomcat 9,直接支持Servlet 3.1。完整的后端代码大概是这样的:
java复制@WebServlet("/uploadFolder")
@MultipartConfig(
fileSizeThreshold = 1024 * 1024 * 2, // 2MB阈值,超过直接写磁盘
maxFileSize = 1024 * 1024 * 100, // 单文件100MB
maxRequestSize = 1024 * 1024 * 1024 // 整个请求1GB
)
public class FolderUploadServlet extends HttpServlet {
private static final String UPLOAD_ROOT = "/data/uploads";
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
String rootDir = UPLOAD_ROOT;
File root = new File(rootDir);
if (!root.exists()) {
root.mkdirs();
}
int successCount = 0;
for (Part part : request.getParts()) {
// Part名称为空说明不是文件,跳过
if (!"folderFiles".equals(part.getName())) {
continue;
}
String submittedFileName = part.getSubmittedFileName();
if (submittedFileName == null || submittedFileName.isEmpty()) {
continue;
}
// 关键:把相对路径转成目标文件
File targetFile = safeResolve(root, submittedFileName);
if (targetFile == null) {
continue; // 路径非法,丢弃
}
// 确保父目录存在
File parentDir = targetFile.getParentFile();
if (!parentDir.exists()) {
parentDir.mkdirs();
}
// 用Files.copy写文件
try (InputStream in = part.getInputStream()) {
Files.copy(in, targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
successCount++;
}
}
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().write("上传完成,成功文件数:" + successCount);
}
}
这里面的safeResolve方法是我封装的一个安全工具,后面讲路径穿越时会展开。但先注意一个点:part.getName()对应前端formData.append时的第一个参数,即folderFiles,所以我在这里做了名称为空或不是folderFiles的跳过逻辑,避免把表单里其他普通字段当成文件处理。
@MultipartConfig的四个参数我后面会细讲,这里先记住:maxRequestSize一定不能太小,否则文件夹里文件一多,请求体积很容易超过限制。Tomcat默认的maxPostSize只有2MB,如果你不配置,传一个大点的文件夹直接就报413 Request Entity Too Large了。
3.3 老项目怎么办:Commons FileUpload的写法
如果是维护老项目,还是Servlet 2.5或更早版本,没法用request.getParts(),那只能用Apache Commons FileUpload。写法上其实也差不太多,核心差异是要自己解析FileItem,并手动用DiskFileItemFactory来控制临时文件写入策略:
java复制// 需要用 commons-fileupload 和 commons-io 两个依赖
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setRepository(new File("/tmp"));
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setFileSizeMax(1024 * 1024 * 100);
upload.setSizeMax(1024 * 1024 * 1024);
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
if (!item.isFormField()) {
String fileName = item.getName();
// 注意:Commons FileUpload 的 getName() 在某些浏览器下会带完整路径
// 比如 C:\fakepath\docs\readme.md,需要手动清理
fileName = fileName.replace("\\", "/");
// 如果包含 C:/ 前缀,截掉前面的协议部分
int idx = fileName.lastIndexOf(":/");
if (idx >= 0) {
fileName = fileName.substring(idx + 2);
}
// 后面的目录创建逻辑和Part版一样
}
}
用Commons FileUpload时有个特别的坑:不同浏览器的getName()返回值不统一。有的返回完整路径,有的返回文件名,还有的在Windows上返回带反斜杠的路径。所以要做一次路径清理,统一把\转成/,再把类似C:/之类的盘符前缀去掉。这也是当初我们用原生Part API不愿回退老方案的原因之一。
4. 目录还原路上的隐藏雷区:路径穿越、乱码与重名
4.1 路径穿越:好心传个文件夹,差点被当成提权入口
文件夹上传最大的安全隐患就是路径穿越。想象一下,如果用户在前端改了请求,把一个文件的filename字段改成../../../../etc/crontab,而后端直接new File(rootDir, fileName),那这个文件就会写到服务器上任意一个位置。轻则文件被覆盖,重则直接写入一个JSP webshell,服务器沦陷。这不是危言耸听,我当初做安全测试时,随手就拼接了一个../../test.jsp试着上传,果然写到了目标目录之外。
所以后端必须做白名单或规范化校验。我的safeResolve方法是这么写的:
java复制private File safeResolve(File root, String relativePath) {
// 统一分隔符
String normalizedPath = relativePath.replace("\\", "/");
// 去掉开头的斜杠,防止绝对路径
while (normalizedPath.startsWith("/")) {
normalizedPath = normalizedPath.substring(1);
}
// 用 Path.normalize 把 ../ 这类内容处理掉
Path rootPath = root.toPath().toAbsolutePath().normalize();
Path targetPath = rootPath.resolve(normalizedPath).normalize();
// 关键校验:最终路径必须还在 rootPath 之下
if (!targetPath.startsWith(rootPath)) {
return null;
}
return targetPath.toFile();
}
核心就一句话:拼接完路径后必须normalize(),然后用startsWith(rootPath)检查最终路径是否还在根目录内。normalize()会把..和.解析掉,../../etc/passwd经过解析后绝对路径变成了/etc/passwd,就不会以/data/uploads开头了,直接返回null丢弃。这步检查千万别省,也别以为用户不会构造恶意请求——任何暴露在公网的上传接口都会被人拿扫描器扫的。
4.2 中文文件名乱码:两次编码,一次都不能漏
文件夹里的文件名字经常是中文,比如项目文档/需求说明.md。上传时如果乱码,到服务器上就是满屏的???或锟斤拷,用户肯定不能接受。乱码问题的根源在于字符编码不一致,通常需要同时保证三处:
- JSP页面本身用UTF-8编码,可以在JSP顶部加
pageEncoding="UTF-8"; - 前端发送请求时,
FormData会自动按UTF-8编码文件名,这部分一般不用管; - 后端
request.setCharacterEncoding("UTF-8")必须放在getParts()之前调用。
如果你用的是Tomcat,还有一个隐蔽点:Tomcat 8.5+的URIEncoding默认是UTF-8,但老版本可能不是,如果用的是老Tomcat,建议在server.xml的Connector上显式配置:
xml复制<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
URIEncoding="UTF-8" />
但要注意,POST表单提交的文件名走的是请求体,不走URI,所以URIEncoding主要影响的是URL里的参数,不是文件名。真正决定文件名编码的,还是request.setCharacterEncoding("UTF-8")这个调用。
4.3 重名文件的覆盖策略:别让数据悄悄消失
文件夹里如果存在同名文件(比如子目录a和b下都有readme.md),那么它们的相对路径是不同的(a/readme.md和b/readme.md),正常不会冲突。真正要处理的是完全相同的路径被重复上传的情况。可能是用户选了两次同一文件夹,也可能是前端提交时文件列表里有重复项。
我在上面的代码里用了StandardCopyOption.REPLACE_EXISTING,也就是直接覆盖。对于一般的文件归档场景,覆盖问题不大。但如果你的业务要求保留历史版本,这里可以改成先判断目标文件是否存在,存在则自动加时间戳或序号:
java复制String fileName = submittedFileName;
File targetFile = safeResolve(root, fileName);
if (targetFile != null && targetFile.exists()) {
String baseName = fileName.substring(0, fileName.lastIndexOf('.'));
String ext = fileName.substring(fileName.lastIndexOf('.'));
targetFile = safeResolve(root, baseName + "_" + System.currentTimeMillis() + ext);
}
这个逻辑要放在safeResolve之后,确保路径安全的前提下再做重命名。
4.4 空文件夹和隐藏文件:浏览器给你过滤得一干二净
这是很多人的认知盲区。当用户选择一个文件夹上传时,浏览器只会把有实际文件内容的文件加入FileList,空文件夹压根不会出现在列表里。也就是说,那个空的emptyDir/目录,前端拿不到、请求里也不存在,后端也无从创建。
我当时的处理方案是:在前端选择文件夹之后,通过File对象的路径把目录名都收集一遍,至少能知道哪些目录是存在的(非空目录)。对于完全没有文件的空目录,目前HTML5方案确实无能为力,只能接受这个限制。另一个相关限制是隐藏文件:Windows的desktop.ini、macOS的.DS_Store,不同浏览器处理不一,有些会一起传上来,有些会过滤掉。我在前端做了过滤,但如果你真需要保留系统文件(比如内部工具想保留全部内容),过滤逻辑要自己去控制。
5. 大文件夹上传体验优化:进度条、并发与服务器边界
5.1 给上传加进度条:用XHR的upload事件
文件夹上传的体量通常不小,几百MB甚至几个GB都可能。没有进度反馈,用户就只能干等着,怀疑是不是卡死了。用XMLHttpRequest最容易实现进度条,核心是监听upload.onprogress事件:
javascript复制const xhr = new XMLHttpRequest();
xhr.open('POST', 'uploadFolder', true);
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
document.getElementById('progressText').textContent = percent + '%';
document.getElementById('progressBar').style.width = percent + '%';
}
};
xhr.onload = function() {
if (xhr.status === 200) {
alert('上传成功:' + xhr.responseText);
} else {
alert('上传失败,HTTP状态码:' + xhr.status);
}
};
xhr.onerror = function() {
alert('网络异常,上传中断');
};
xhr.send(formData);
这里有个细节:e.total是整个请求体的大小,也就是所有文件大小之和加上一些multipart头部的开销。对于超大文件夹,这个请求可能持续几分钟,浏览器和服务器都要做好超时配置。客户端的xhr.timeout如果设了,要设大一点,比如30分钟以上,否则中途就给你断掉了。
5.2 并发控制:一次请求全传 vs 拆成多个请求
前端把所有文件塞进一个FormData发出一个大请求,实现最简单,但服务器内存压力大,而且一旦网络抖动就得全部重来。另一种方案是把文件拆分,按顺序多次发出小请求,每次传几个文件,配合进度可以做到精细控制。这么做的好处是可以做断点续传(传过的文件不需要重传),坏处是实现复杂度大增。
我的建议是:对于内网或带宽可控的后台系统,一次大请求就够用,配合@MultipartConfig把maxRequestSize调大即可。如果要面向公网、网络不稳定,就拆成小批次。有一种折中方案:前端按目录层级分批发送,比如每次最多传20个文件,后端按批次落盘,传完一批再传下一批。这样至少不会因为网络抖动一次性丢几百个文件。
5.3 服务器参数三件套:Tomcat、Servlet、JVM
后端除了@MultipartConfig,Tomcat还有自己的隐藏参数,不配置的话大文件上传会莫名其妙报错。我在实践中总结成三件套:
| 配置项 | 位置 | 作用 | 建议值 |
|---|---|---|---|
maxPostSize |
Tomcat server.xml Connector |
限制POST表单请求体大小,默认2MB | 104857600(100MB)或0表示不限制 |
maxSwallowSize |
Tomcat server.xml Connector |
限制容器读取请求体的最大字节数 | -1表示不限制 |
@MultipartConfig(maxRequestSize) |
Servlet注解 | Servlet 3.0的请求体大小限制 | 视文件总量而定,建议1GB以上 |
特别注意maxSwallowSize。有次我调大了maxPostSize但没动它,Tomcat在客户端发出大请求时依然报错,查了半天才发现是它限制住了底层读取。把maxSwallowSize="-1"加上之后问题就消失了。
JVM内存方面,如果你用DiskFileItemFactory的默认配置,文件在小于阈值的时候是存内存的。@MultipartConfig的fileSizeThreshold建议设成1~2MB,让超过这个大小的文件直接落临时文件,避免大文件进来时堆内存被撑爆。
5.4 断点续传的简单思路
断点续传真正要解决的是"大目录传到一半网络断了"的问题。完整实现比较复杂,要做文件分片、片上续传、合并校验,但如果只是想要个粗糙版,可以靠拆分请求实现:前端每次发送一批文件,后端记录已经成功存盘的相对路径,下次请求时前端可以传一个已完成的清单,后端跳过这些文件。这样至少避免了"全部重来"。
真要做精细版的分片上传,就得对每个文件做切片(比如每片5MB),后端记录每个切片的上传状态,全传完再合并。这个方案工作量大,适合单个文件特别大的场景。对于"文件夹里都是中小文件"的典型场景,按文件粒度拆分就够用了。
6. 兼容性、完整可跑Demo与踩坑自查清单
6.1 浏览器兼容性:别在Safari上翻车
我刚做这个功能时天真地以为HTML5 File API浏览器全都支持,结果在Safari上一测,文件选择框根本弹不出来。做兼容性之前,先列一张表:
| 浏览器 | webkitdirectory支持 |
webkitRelativePath支持 |
备注 |
|---|---|---|---|
| Chrome 31+ | 支持 | 支持 | 最推荐的使用环境 |
| Edge 79+(Chromium) | 支持 | 支持 | 新版Edge没问题 |
| Firefox 50+ | 支持 | 支持 | 正常 |
| Safari 11+ | 不支持 | 不支持 | 目录选择框无法弹出 |
| IE 11 | 不支持 | 不支持 | 完全不可用 |
所以如果用户用的是macOS + Safari,这个方案会直接失效。我当时的应对是:检测到浏览器不支持webkitdirectory时,前端给出提示,引导用户改用手动多选文件上传(退化为普通multiple模式),后端兼容处理——没有webkitRelativePath就把文件放根目录。
检测代码很简单:
javascript复制const input = document.getElementById('folderInput');
if (!('webkitdirectory' in input)) {
alert('当前浏览器不支持文件夹上传,请使用Chrome、Edge或Firefox,或改为手动选择文件');
input.removeAttribute('webkitdirectory');
input.setAttribute('multiple', 'multiple');
}
6.2 一个最小可运行的JSP + Servlet完整示例
说了这么多,还是给一个完整示例最直观。假设项目是标准的Maven + Java 8 + Tomcat 9。前端的JSP页面代码如下:
jsp复制<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>文件夹上传Demo</title>
</head>
<body>
<h2>请选择一个文件夹上传</h2>
<input type="file" id="folderInput" webkitdirectory multiple />
<div id="progressWrap" style="display:none; margin-top:10px;">
<div style="width:100%; height:20px; background:#eee;">
<div id="progressBar" style="width:0; height:20px; background:#4caf50;"></div>
</div>
<span id="progressText">0%</span>
</div>
<p id="statusMsg"></p>
<script>
document.getElementById('folderInput').addEventListener('change', function (e) {
var files = e.target.files;
if (!files || files.length === 0) {
return;
}
var formData = new FormData();
var totalCount = files.length;
var validCount = 0;
for (var i = 0; i < totalCount; i++) {
var file = files[i];
var path = file.webkitRelativePath || file.name;
if (!path || path.charAt(0) === '.') {
continue;
}
formData.append('folderFiles', file, path);
validCount++;
}
if (validCount === 0) {
document.getElementById('statusMsg').textContent = '没有可上传的文件';
return;
}
document.getElementById('progressWrap').style.display = 'block';
var xhr = new XMLHttpRequest();
xhr.open('POST', 'uploadFolder', true);
xhr.upload.onprogress = function (ev) {
if (ev.lengthComputable) {
var percent = Math.round(ev.loaded * 100 / ev.total);
document.getElementById('progressBar').style.width = percent + '%';
document.getElementById('progressText').textContent = percent + '%';
}
};
xhr.onload = function () {
document.getElementById('statusMsg').textContent = xhr.responseText;
};
xhr.onerror = function () {
document.getElementById('statusMsg').textContent = '网络异常,上传中断';
};
xhr.send(formData);
});
</script>
</body>
</html>
后端Servlet用上面第3节的FolderUploadServlet就行,唯一记得要把@MultipartConfig的maxRequestSize调大。部署到Tomcat时,server.xml里也要配合修改。
6.3 自查清单:我每次上线前都会过一遍
这条清单是在实际项目里吃过亏总结出来的,建议收藏。
- 路径穿越检查:
safeResolve校验是否在根目录内;../../、..\\、绝对路径全部拒绝。 - 文件名编码:
request.setCharacterEncoding("UTF-8")必须在getParts()之前;JSP页面统一UTF-8。 - 服务器限制:Tomcat的
maxPostSize和maxSwallowSize是否调整;@MultipartConfig的maxRequestSize是否够大。 - 前端兼容:不支持
webkitdirectory的浏览器是否有降级提示。 - 重复文件策略:是覆盖还是保留版本,业务上要提前定。
- 大小写和特殊字符:Windows文件名不区分大小写,Linux区分;文件名里有
#、%、&等特殊字符时要确认不会出问题。 - 日志记录:每条成功/失败的文件路径要记录,方便排查问题。
我在实际使用中发现,这套文件夹上传方案最大的价值不只是省了用户几次点击,而是把一个"人肉整理目录结构"的重复劳动彻底自动化了。只要你把相对路径传递和目录重建这两环处理好,整个后台系统对用户来说就顺滑了很多。后面如果项目规模继续扩大,我可能会在这个基础上做带详情页的上传任务中心,把每个文件的传输状态、失败原因都列清楚,那体验就又上了一个台阶。就技术路径来说,JSP + Servlet这套组合虽然老,但配合HTML5 File API做文件夹上传,稳定可靠,不需要额外引入任何重框架,对大部分中小型系统来说完全够用。
