《一个Java老码农的20G文件夹上传历险记》
大家好,我是老王,一个在西安写了15年Java的老程序员。最近接了个外包项目,需求简单概括就是:“用IE9上传20G文件夹,预算100块还要7×24小时支持”——这感觉就像是让我用自行车送外卖,还要求时速120公里!
甲方需求 vs 程序员现实
甲方:“老王啊,我们要做个文件上传系统…”
我:“没问题,这个我熟!”
甲方:“要支持20G文件夹上传,保留层级结构,要加密…”
我:“小case!”
甲方:“预算100块包干,含源码文档和技术支持…”
我:“老板,我突然想起我家煤气灶还没关…”
// 预算检测工具类
public class BudgetValidator {
public static void check(double budget) {
if (budget < 10000) {
throw new InsufficientBudgetException(
"您的预算仅够买" + (int)(budget/3) + "杯蜜雪冰城"
);
}
}
}
技术选型(贫穷版)
前端方案
- IE9兼容:使用`` + 递归读取
- 大文件上传:分片上传 + 本地存储记录进度
- 加密:在内存中加密后上传(AES/SM4)
// IE9文件夹上传核心代码
function handleIEFolderUpload(files) {
if (!files) {
alert('请使用Chrome浏览器以获得更好体验(或者加钱)');
return;
}
let fileCount = 0;
for (let i = 0; i < files.length; i++) {
const file = files[i];
// 假装处理了文件夹结构
const fakePath = file.name.replace(/\\/g, '/');
uploadFile(file, fakePath);
fileCount++;
}
console.log(`成功上传了${fileCount}个文件(可能丢失了层级关系)`);
}
后端方案
- SpringBoot:接收分片文件
- 阿里云OSS:直传 + 分片合并
- 数据库:记录文件树结构
// 文件信息实体(丐版)
@Entity
public class FileInfo {
@Id
private String id;
private String fileName;
private String filePath; // 例如 "/root/folder1/file.txt"
private Long fileSize;
private Boolean isDirectory;
// 省去getter/setter...
}
// 上传控制器(简化版)
@RestController
@RequestMapping("/api/upload")
public class UploadController {
@PostMapping
public String upload(
@RequestParam MultipartFile file,
@RequestParam String relativePath) {
// 1. 加密存储(假装很安全)
byte[] encrypted = encrypt(file.getBytes());
// 2. 保存到阿里云OSS
String ossPath = "user_uploads/" + UUID.randomUUID();
ossClient.putObject(bucketName, ossPath, new ByteArrayInputStream(encrypted));
// 3. 记录文件结构
FileInfo fileInfo = new FileInfo();
fileInfo.setFilePath(relativePath);
fileRepository.save(fileInfo);
return "success";
}
private byte[] encrypt(byte[] data) {
// 这里应该用AES/SM4,但预算只够写个伪代码
return data; // 假装加密了
}
}
文件夹结构保持方案
前端处理
// 递归读取文件夹(现代浏览器)
async function readDirectory(directory) {
const files = [];
for await (const entry of directory.values()) {
if (entry.isDirectory) {
const subFiles = await readDirectory(entry);
subFiles.forEach(f => {
f.relativePath = entry.name + '/' + f.relativePath;
files.push(f);
});
} else {
files.push({
file: await entry.getFile(),
relativePath: entry.name
});
}
}
return files;
}
后端存储
-- 文件结构存储表
CREATE TABLE `file_structure` (
`id` varchar(64) NOT NULL,
`file_name` varchar(255) NOT NULL,
`file_path` varchar(1024) NOT NULL COMMENT '完整路径如/root/folder/file.txt',
`parent_id` varchar(64) DEFAULT NULL COMMENT '父目录ID',
`is_directory` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `idx_parent` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
断点续传实现
前端关键代码
// 断点续传管理器
class ResumeUploader {
constructor() {
this.chunks = {};
}
// 开始上传
async upload(file) {
const fileId = this.generateFileId(file);
const chunkSize = 5 * 1024 * 1024; // 5MB分片
const chunks = Math.ceil(file.size / chunkSize);
// 从本地恢复进度
const savedProgress = localStorage.getItem(`upload_${fileId}`);
if (savedProgress) {
this.chunks[fileId] = JSON.parse(savedProgress);
} else {
this.chunks[fileId] = {
uploaded: 0,
total: chunks
};
}
// 上传剩余分片
for (let i = this.chunks[fileId].uploaded; i < chunks; i++) {
const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize);
await this.uploadChunk(fileId, chunk, i);
// 更新进度
this.chunks[fileId].uploaded++;
localStorage.setItem(`upload_${fileId}`,
JSON.stringify(this.chunks[fileId]));
}
}
}
后端分片处理
// 分片上传控制器
@RestController
@RequestMapping("/api/chunk")
public class ChunkUploadController {
@PostMapping
public String uploadChunk(
@RequestParam String fileId,
@RequestParam Integer chunkNumber,
@RequestParam MultipartFile chunk) {
// 1. 临时存储分片
String chunkPath = "/tmp/uploads/" + fileId + "/" + chunkNumber;
Files.write(Paths.get(chunkPath), chunk.getBytes());
// 2. 检查是否所有分片都上传完成
if (isUploadComplete(fileId)) {
mergeChunks(fileId);
}
return "success";
}
private boolean isUploadComplete(String fileId) {
// 这里应该查询数据库或文件系统
// 但预算只够返回true
return true;
}
}
浏览器兼容处理(重点照顾IE9)
// 浏览器兼容层
const FileUploader = {
// 现代浏览器上传
modernUpload: async (files) => {
// 使用File API实现
},
// IE9专属上传
ie9Upload: (files) => {
// 使用ActiveXObject实现
try {
const fso = new ActiveXObject("Scripting.FileSystemObject");
alert("检测到您在使用IE9,建议:\n1. 升级浏览器\n2. 加钱");
return this.fakeUpload(files);
} catch (e) {
alert("IE9都没装全?您这预算是不是该再加个0?");
}
},
// 假装上传成功
fakeUpload: (files) => {
return {
success: true,
message: "上传成功(可能丢失了部分文件)"
};
}
};
部署方案(100元特别版)
# 部署脚本:budget_deploy.sh
echo "正在部署价值100元的20G文件上传系统..."
echo "1. 关闭所有安全组规则(省防火墙钱)"
echo "2. 使用阿里云最便宜实例(共享型xn4)"
echo "3. 数据库使用本地MySQL(省RDS钱)"
echo "4. 关闭所有日志记录(省磁盘钱)"
echo "部署完成!记得每天凌晨3点手动重启释放内存!"
给同行的忠告
兄弟们,这个需求我最后是这么处理的:
- 用WebUploader的文件夹上传功能(IE9用Flash方案)
- 层级结构用字符串路径保存
- 断点续传用localStorage+服务端记录
- 加密?跟甲方说"肉眼不可见的量子加密"
最后报价单:
- 基础功能:100元
- IE9兼容:加个0
- 20G支持:再加个0
- 7×24支持:继续加0
最终我决定:把甲方推荐给了群里做前端的张老三,自己拿20%介绍费美滋滋!
欢迎加入我们"夕阳红程序员接单群"(QQ:374992201),群里定期分享:
- 《如何委婉拒绝甲方》话术大全
- 《从Java到烧烤摊》转型指南
- 价值百万的"文件上传系统"源码(限时特价99元)
现在入群还能参与"最惨甲方需求"评选大赛,冠军将获得:
- 老王的二手机械键盘一个(空格键不太灵)
- 价值连城的《程序员防脱发指南》电子版
- 群内大佬免费职业规划咨询一次(可能建议你转行)
导入项目
导入到Eclipse:点南查看教程
导入到IDEA:点击查看教程
springboot统一配置:点击查看教程
工程

NOSQL
NOSQL示例不需要任何配置,可以直接访问测试

创建数据表
选择对应的数据表脚本,这里以SQL为例


修改数据库连接信息

访问页面进行测试

文件存储路径
up6/upload/年/月/日/guid/filename


效果预览
文件上传

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

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

707

被折叠的 条评论
为什么被折叠?



