1. 为什么自建个人云盘,以及我为什么锁定了Spring Boot
先交代一下背景。我一直有囤资料的习惯,论文、电子书、工作文档、各种安装包加起来快2个T,先后试过几家主流的免费网盘,体验都不太满意:免费容量越给越小,下载速度一到高峰期就“战术性”变慢,偶尔还会遇到文件被“审核”然后莫名消失的情况。折腾了一圈,我决定自己搭一套个人云盘系统,核心诉求就三个:数据在自己手里、速度不受限、想怎么扩展就怎么扩展。
市面上其实有现成的开源云盘,像NextCloud、Seafile、ownCloud这类,功能确实很全,部署也算方便。但我的情况比较特殊:手头有几台性能一般的云服务器,内存只有2G到4G,跑NextCloud全家桶有点吃力;更关键的是,我需要一个能完全按自己需求改、代码逻辑一目了然的系统,方便后续往里面加“按年月自动归档”、“重复文件清理”、“定时备份”这些个性化功能。所以最终决定自己写,技术栈锁定Spring Boot。
为什么是Spring Boot而不是PHP或Node?说句实在话,在这个场景下Spring Boot有几个不可替代的优势。第一,生态足够成熟,文件上传下载、权限管理、定时任务、消息队列这些都有现成方案,遇到问题搜一下基本都能解决;第二,Java内存管理和并发处理更稳,我实测过用Spring Boot处理5G以上的大文件下载,内存抖动明显比Node方案小;第三,后期如果想把单机版升级成带分布式存储的版本,Spring Cloud那一套可以直接接上,迁移成本低。
这里多说一句关于“源码+数据库+文档”的价值。很多人拿到一个开源项目习惯性先看代码,但我建议先读文档和数据库脚本。数据库表结构能直接反映设计者的思路——哪些信息被独立成表,哪些字段被冗余出来,表与表之间怎么关联,这些都搞清楚了,代码看起来会顺畅得多,改起来也有的放矢。这篇博文我不打算只贴代码,而是把设计思路、核心实现、部署过程和踩坑记录都串起来讲一遍,希望拿到源码的同学能真正理解它,而不是只会启动一下跑个demo。
2. 系统的技术架构与存储策略:先定方案再写代码
2.1 整体技术栈选择与分工
搭建这个项目之前,我先把技术选型定了下来,避免写代码过程中反复改。
- 后端:Spring Boot 2.7.x + MyBatis Plus + MySQL 8.0 + Redis
- 前端:Vue 3 + Element Plus(做管理界面,打包后放进Spring Boot静态目录)
- 存储:本地磁盘直接存储(小规模场景性价比最高,后面细说)
- 鉴权:JWT + Redis缓存Session
- 构建:Maven,前端单独用Vite构建
Spring Boot版本这里要提醒一下,不要一上来就用最新版,最好在2.5到2.7之间选一个稳定的。因为很多辅助库的兼容性还没跟上新版本,我身边就有同事因为Spring Boot 3.x和某些MyBatis Plus版本不兼容,折腾了大半天。我这个项目用的是2.7.6,实测很稳。
数据库层面我选了MySQL 8.0,主要看中它的窗口函数和JSON类型,后面做“目录树查询”和“文件标签扩展”会方便很多。Redis在这里不是必需的,但它干两件事很值:一是存JWT的黑名单,用户退出登录后token能立刻失效;二是做文件秒传的MD5索引缓存,这个后面细讲。
2.2 存储策略:本地磁盘 vs 对象存储 vs MinIO
这是很多自建云盘的人最纠结的地方。我当时列了个对比表:
| 方案 | 部署复杂度 | 成本 | 扩展性 | 适用场景 |
|---|---|---|---|---|
| 本地磁盘 | 零部署 | 仅硬盘成本 | 差,受单机容量限制 | 个人使用、家庭NAS场景 |
| 云厂商对象存储(阿里云OSS等) | 低,按API接入 | 按容量和流量计费 | 极好,无限扩展 | 有公网对外服务需求、需要高可用 |
| MinIO自建对象存储 | 中等,需单独部署服务 | 服务器成本 | 好,可集群化 | 数据量大且想保留私有化优势 |
我的建议是:初期就用本地磁盘。理由很直接,个人云盘的文件量级通常在几个T以内,一块3T的硬盘就够了,没必要引额外的中间件。用对象存储的话每次上传下载都要走公网API,延迟和流量费用都上来了,而且数据放在别人机房,和“数据自己掌握”的初衷就背道而驰了。
本地存储的目录结构我按“用户ID + 年月”两级分目录:
/storage/user/1/2025-01/ /storage/user/1/2025-02/ /storage/user/2/2025-01/为什么按年月再分一层?因为单目录下文件过多时,Linux文件系统的索引性能会下降。我做过测试,目录下超过1万个文件后,ls操作就能感觉到延迟,超过5万就很明显了。按年月拆开,一个用户一个月通常不会超过几千个文件,访问性能稳定很多。
2.3 数据流设计:一次完整的上传下载要经过哪些环节
这里把核心数据流画一下,不画时序图了,用文字描述。一次文件上传经过的环节是:前端将文件分片(或整体)POST到后端 → Spring Boot的MultipartFile接收 → 校验文件大小、后缀白名单 → 生成文件唯一标识(MD5) → 判断MD5是否已存在(秒传逻辑) → 不存在则写入磁盘 → 将文件元数据(文件名、大小、路径、MD5、所属用户、父目录ID等)写入MySQL → 同步更新Redis中的用户已用空间 → 返回上传成功响应。
下载流程则反过来:用户请求下载文件ID → 后端校验文件归属权限(防止越权访问他人文件) → 拼接磁盘完整路径 → 用StreamingResponseBody流式写出 → 完成后回写下载次数日志。
这套流程里最容易被忽略的是权限校验。我见过不少人做云盘demo,下载接口直接拿着文件ID查路径就返回了,压根不判断这个文件属于哪个用户。别人猜出你的文件ID就能拿走文件,这在自己用的时候还好,一旦对外开放就是安全事故。这个问题我在第四部分还会提。
2.4 前后端分离还是单体打包?我的取舍
这个项目原本是打算前后端分离部署的:后端跑在8080端口,前端跑在Vite的5173端口,联调时通过Vite代理转发请求。但考虑到最终要给用户一个“拿到源码就能一键跑起来”的体验,我选择了前端构建后放入Spring Boot的static目录,打成单个Jar包。
好处是部署极其简单,不用单独装Nginx、不用配置跨域、不用维护两个进程。坏处是前端代码更新后需要重新打包整个Jar,但个人项目无所谓,更新频率本来就不高。
如果你打算用前后端分离模式,一定要把跨域配置提前写好。我在本地联调时踩过坑:前端请求后端接口时,浏览器报CORS错误,一开始以为是跨域配置没写好,后来发现是Vite代理配置的target地址写错了端口。建议前后端分离调试时统一用代理方式,不要直接在后端代码里放开所有跨域,否则上线后接口相当于对全网开放,任何人都能调用。
3. 数据库设计:云盘项目的表结构到底怎么规划
3.1 五张核心表:从用户到文件再到分享
一个云盘系统的数据库设计,核心就是五张表:用户表、文件表、文件夹表、分享表、回收站记录表。下面把关键字段列出来讲讲设计原因。
用户表(t_user)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| account | varchar(32) | 登录账号,唯一索引 |
| password | varchar(64) | 密码,MD5加盐存储 |
| nickname | varchar(32) | 昵称,展示用 |
| total_space | bigint | 总空间,单位字节,默认10G |
| used_space | bigint | 已用空间,单位字节 |
| create_time | datetime | 注册时间 |
密码用MD5加盐,很多人问为什么不用BCrypt。BCrypt加密强度高,但每次登录验证都挺耗CPU,在小规模个人系统里两者差距不大。但如果你有对外开放的打算,我建议换成BCrypt,毕竟MD5撞库风险是实打实的。这个字段将来可以通过Spring Security的PasswordEncoder无缝切换,不影响表结构。
文件表(t_file)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 所属用户ID,建普通索引 |
| parent_id | bigint | 父目录ID,文件夹和根目录为0 |
| file_name | varchar(255) | 文件名 |
| file_path | varchar(500) | 存储相对路径 |
| file_size | bigint | 文件大小,字节 |
| file_md5 | varchar(64) | 文件MD5,用于秒传和去重 |
| file_type | varchar(10) | 类型标识,file或folder |
| is_delete | tinyint | 软删除标志,1为已删除 |
| delete_time | datetime | 删除时间,用于回收站自动清理 |
| create_time | datetime | 上传/创建时间 |
很多云盘的文件夹和文件是分两张表存的,但我把文件夹也当成一种特殊的“文件记录”,用file_type字段区分。好处是目录树的查询逻辑变得非常统一——查子项就是查parent_id等于某个值的所有记录,不管它是文件还是文件夹,一条SQL搞定;坏处是文件夹本身没有file_size和file_md5,需要允许这两个字段为空。这个取舍我认为对个人项目是划算的,代码精简很多。
分享表(t_share)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 分享发起人 |
| file_ids | varchar(500) | 分享的文件或文件夹ID,多个用逗号分隔 |
| share_code | varchar(10) | 提取码 |
| expire_time | datetime | 过期时间,空为永久 |
| visit_count | int | 访问次数 |
| create_time | datetime | 分享创建时间 |
注意file_ids是逗号分隔的字符串,这在正常范式设计里是不推荐的,但云盘分享有个特点:用户可能一次分享多个文件,而非固定数量的关系。单独建关联表会增加复杂度,对个人项目来说一个varchar字段完全够用。查询时再用FIND_IN_SET或IN语句拆一下就好。
回收站记录表(t_recycle)
这张表的字段和文件表几乎一样,多一个origin_路径字段用于还原。也可以不建独立表,直接在文件表上做is_delete软删除,我为了后续加“回收站列表独立展示”“到期自动清除”的定时任务,选择了独立表。从文件表删除记录时,把它插入回收站表,形成一次事务。
3.2 建表语句中的几个关键细节
建表时我建议给三个位置特别加索引,查询性能差距很大:
ALTER TABLE t_file ADD INDEX idx_user_parent (user_id, parent_id); ALTER TABLE t_file ADD INDEX idx_user_md5 (user_id, file_md5); ALTER TABLE t_share ADD INDEX idx_share_code (share_code);- idx_user_parent:支撑目录列表查询,这是最高频的查询,没有这个索引数据量一大就很慢
- idx_user_md5:支撑秒传功能的MD5查重,上传前先查一次
- idx_share_code:支撑分享链接打开时的提取码校验
另外,文件表的file_name建议用utf8mb4,别用utf8。utf8在MySQL里实际上只能存基本多语言平面字符,遇到特殊字符(比如emoji命名文件)会直接报错或者乱码。现在文件名叫“😊测试.pdf”的越来越多,用utf8mb4就没这个问题。
3.3 数据库脚本中的初始化数据
源码附带的数据库脚本里,除了表结构,我还塞了一条管理员账号(admin / 123456)和几个测试目录。有同学问为什么不全删掉让用户自己注册。我的想法是,第一次部署的人能立刻登录看到效果,确认系统没问题再改密码,体验会顺畅很多。但正式使用时,记得第一时间改掉默认密码,这个不用我说了吧。
4. 核心功能拆解:上传、下载、分享与用户模块的实现路径
4.1 文件上传:秒传、分片、进度反馈背后的逻辑
文件上传是一个云盘系统的核心,也是最容易出问题的地方。这个项目实现了三个能力:秒传、分片上传、断点续传。
先说秒传。原理很简单:用户上传文件时,前端先计算整个文件的MD5(用浏览器端的SparkMD5库),把MD5传给后端。后端去t_file表查这个MD5是否已存在于当前用户的文件列表中,如果存在且用户已用空间足够,就不再真正上传文件内容,直接返回“上传成功”,并把用户已用空间增加对应大小。这样一来,同一份文件不管传几次,只有第一次会真的占用磁盘,体检视频、安装包这种重复转存的场景体验极好。
MD5计算在浏览器端对大文件比较耗时,所以我对超过100MB的文件做了一个降级策略:不做全量秒传,而是直接走分片上传。分片上传就是前端把文件切成2MB一片,逐片POST到后端,后端暂存在临时目录,全部传完后调用一个合并接口,把分片拼接成完整文件再写入正式存储路径。
分片大小我调过几次,2MB比较均衡。太小的分片HTTP请求次数太多,网络开销大;太大的话单片传输时间变长,失败重传成本高。2MB片大小,5G的文件大约2560片,实测在网络稳定的内网场景下,传输效率接近整传,外网弱网环境下的失败重传成本也能接受。
断点续传的逻辑建立在分片基础上:前端把每个分片的序号和MD5都传给后端,后端记录哪些分片已经传到临时目录,前端重新上传时先调一个接口查哪些分片缺失,只重传缺失的就可以。这个功能对50MB以上文件的体验提升非常明显,值得优先实现。
涉及上传的Spring Boot配置,这里给出关键部分:
spring: servlet: multipart: max-file-size: 1024MB max-request-size: 2048MBmax-file-size控制单个文件大小,max-request-size控制单次请求总大小。很多人只配置了前者忘了后者,结果单文件不超过限制但分片连传时请求体超限,报错之后一脸懵。这两个配置大小要根据分片策略来定,比如分片大小2MB,一次请求传10片,那max-request-size至少得20MB以上。
4.2 文件下载:单文件、多文件打包下载与权限校验
单文件下载逻辑很简单,但有一个初学者容易犯的错误:直接用FileInputStream读取整个文件再返回给前端。小文件没事,大文件会把内存撑爆。正确做法是用StreamingResponseBody流式输出,让Response的OutputStream直接从磁盘搬数据,不经过JVM内存的完整拷贝。
@GetMapping("/download/{fileId}") public ResponseEntity<StreamingResponseBody> download(@PathVariable Long fileId, @RequestParam Long userId) { FileInfo file = fileService.getFileById(fileId); // 关键校验:文件必须属于当前用户 if (!file.getUserId().equals(userId)) { throw new RuntimeException("无权访问该文件"); } File diskFile = new File(file.getFilePath()); StreamingResponseBody body = outputStream -> { try (InputStream input = new FileInputStream(diskFile)) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = input.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } outputStream.flush(); } }; return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + URLEncoder.encode(file.getFileName(), "UTF-8") + "\"") .contentType(MediaType.APPLICATION_OCTET_STREAM) .contentLength(diskFile.length()) .body(body); }上面的代码里第一个if就是权限校验,看似多写了几行,但这是云盘系统最不能省的防线。另外注意响应头里的filename一定要做URL编码,否则包含中文或特殊字符的文件名会直接乱码,这个我踩过,客户端下下来文件名全是“%E6%B5%8B%E8%AF%95.pdf”这种。
多文件下载我实现的是打包成zip流式输出。用Java自带的ZipOutputStream,注意不能把整个zip文件先写到临时磁盘再返回,那样延迟高还浪费磁盘。正确做法是边遍历文件列表边写入ZipOutputStream,输出流直接指向HTTP响应。如果是视频、图片这种希望在线预览的文件,可以走独立的预览接口读取字节输出,配置正确的Content-Type就行。
4.3 分享机制:提取码、有效期设计
分享功能是我个人很喜欢的一个模块。用户勾选若干文件或文件夹,点击“创建分享”,后端生成一个随机字符串(我用的UUID前8位)作为链接标识,再生成一个4位数字提取码,设置有效期(可选1天、7天、30天、永久)。访问者拿到链接和提取码后,输入提取码验证通过才能查看和下载文件。
提取码的生成有个细节值得说一下。4位数字一共只有10000种组合,绝对安全谈不上,但作为分享场景的身份验证够用了。我见过有人在提取码校验逻辑里不做次数限制,这等于给了暴力破解的机会。我在实现时加了限制:同一个分享链接,提取码错误超过5次,锁定10分钟,这个功能用Redis的incr+expire实现,代码量很小,但安全性提升明显。
失效处理也很重要。在查询分享信息时同步检查过期时间,过期后接口返回“链接已失效”。同时,我写了一个定时任务,每天凌晨扫一次分享表,把过期的记录直接清除并把分享状态置为失效,避免无效数据越积越多。这个定时任务用Spring Boot自带的@Scheduled注解就能实现,没必要引入额外的调度框架。
4.4 用户体系:注册、登录、JWT鉴权
用户模块是其他所有功能的基础,我用JWT做无状态鉴权。用户登录成功后,后端生成一个带过期时间的token返回给前端,前端存到localStorage,每次请求在Authorization头带上。后端写一个拦截器统一校验token,解析用户ID放入ThreadLocal,后续处理请求的任何地方都能取到当前用户。
JWT的secret一定要放在配置中心或环境变量里,别硬编码在代码中。这个属于老生常谈了,但GitHub上泄露secret的事确实不少见,提一下没坏处。
用户“已用空间”的统计,我是在文件上传成功、删除文件、回收站清理三个时机单独维护的,没有用SQL统计再更新。原因很简单:单用户文件数量过万后,每次进首页都COUNT一次大表,接口延迟能从30ms飙升到300ms以上。用单独字段维护,换来的是首页加载的稳定快速。代价是偶尔会出现空间数值不同步,所以我另加了一个“空间校准”接口:扫描该用户所有未删除文件,累加file_size,然后回写used_space字段。每月手动执行一次就行,方便得很。
5. 从源码到上线:开发环境配置与部署要点
5.1 本地跑起来的完整步骤
拿到源码之后,我按下面这个顺序配置环境,目前来看是最省事的路径:
- 安装JDK 1.8或11,配置JAVA_HOME
- 安装Maven 3.6+,配置文件仓库地址,建议设置成阿里云镜像,不然依赖下载能等到怀疑人生
- 安装MySQL 8.0,执行项目根目录下的schema.sql,初始化所有表结构
- 安装Redis 6.x,默认端口即可
- 打开application.yml,修改数据库地址、账号、密码,以及Redis地址
- 在项目根目录执行
mvn spring-boot:run,或者导入Idea后直接运行主类
如果数据库连接报错关于公钥检索问题,记得在MySQL连接串后面加上allowPublicKeyRetrieval=true,这属于MySQL 8.0的认证机制变化导致的,不加这个参数直接连不上。
前端部分,如果我已经把构建后的dist目录打进了Jar包,那你跑后端起来打开http://localhost:8080就能看到界面,不需要单独启前端。如果你想改前端的界面样式,就需要在frontend目录下执行npm install装依赖,然后npm run build生成新的dist,把它拷贝到Spring Boot的src/main/resources/static目录下,再重新打包运行。
5.2 真机部署时要注意的坑
服务器部署和我本地跑通是两回事,这里有三个我说什么也要叮嘱一下的坑。
第一个是磁盘路径问题。Windows上文件路径分隔符是反斜杠\,Linux上是正斜杠/。我在本地Windows开发时上传的文件路径存的是D:\storage\user\1\2025-01\abc.pdf,把Jar包扔到Linux服务器上跑就全乱了。解决方案是存储路径不要写绝对路径,用相对路径或配置文件指定;存库时统一把路径分隔符替换成/;读取时用Java 7的Path类或File.separator去拼路径。我在代码里统一用"/"拼接字符串,再转成File对象,这样跨平台没问题。
第二个是静态资源映射。Spring Boot默认只映射static目录,我自定义的存储路径(比如/storage)不在这个范围内。需要在配置里加一段资源映射:
spring: resources: static-locations: classpath:/static/,file:/storage/如果不加这段,直接按URL访问上传的图片会404,很多人排查半天找不到原因。其实Spring Boot的资源映射很简单,就是把HTTP请求路径映射到磁盘路径上。
第三个是端口和防火墙。服务器上用java -jar cloud-disk.jar --server.port=8080启动后,记得在安全组和系统防火墙里同时放行8080端口。我上线时只在云控制台配了规则,忘了服务器本机的firewalld也要放行,结果折腾了半小时才确认是这个问题。
5.3 上线前的自检清单
我每次部署新版本都会过一遍这个清单,省了很多事:
- 默认密码是否已修改,admin账号是否绑定了自己的邮箱或手机号
- application.yml里是否开启了生产环境配置(关闭Swagger文档暴露、关闭DevTools热部署)
- 存储路径是否存在、是否有写权限,执行
chmod授权的时候别把用户主目录权限也改了 - MySQL连接是否使用独立的云盘数据库账号,而不是root直接跑
- 定时任务的执行日志是否输出到独立文件,方便排查回收站清理情况
- Jar包备份是否保留上一个稳定版本,方便快速回滚
6. 开发中遇到的真坑与避坑经验
写代码的时候踩了不少坑,这里挑几个最有代表性的记下来,都是百度搜不太到的经验。
坑一:大文件上传时的内存溢出
第一次跑通上传功能的时候,我直接用的MultipartFile接收整个文件然后转存,本地测的时候200MB的文件没问题,一部署到服务器传1.2GB的文件就报OutOfMemoryError。排查发现是Tomcat默认把上传文件先写进内存,文件太大直接撑爆。解决办法有两个:一是配置spring.servlet.multipart.file-size-threshold=2MB,让超过2MB的文件直接写入临时文件而不是内存;二是前端做分片,把单片控制在较小体积。我把两个方案都用了,效果很理想,传10GB大文件后端内存占用一直很平稳。
坑二:中文文件名下载时乱码
这个前面提过一嘴,再展开说下。最初我下载接口的Content-Disposition头是直接拼文件名的,浏览器打开后发现中文全部变成下划线或者乱码。后来查RFC文档才知道,HTTP头部默认不支持非ASCII字符,需要对文件名做RFC 5987编码。正确写法是:
String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20"); header("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName);注意URLEncoder会把空格编码成+,但URL里+有特殊含义,所以要再替换成%20。这个细节我在网上搜了好几个帖子才定位到,实际效果立竿见影。
坑三:秒传功能误判重复文件
最开始实现秒传时,我只按文件MD5判断,结果发现不同用户上传同一个文件时,A用户上传成功后B用户也能秒传,但B用户文件列表里确实有这个文件,没问题。真正的坑在于:同一个文件,两个用户都上传了,但其中一个用户把文件修改后又上传,MD5变了新文件正常创建;但原始版本一直存在,造成存储空间浪费。这个问题要彻底解决,需要做文件引用计数——同一MD5同时被多个用户引用时才真正保留一份物理文件。我一开始没做这个,只是做了“同一用户重复上传”的秒传,后来数据量大了才意识到这是个可优化项。如果你要扩展这个系统,可以从文件引用计数入手,这是比较高级的优化方向。
坑四:MyBatis Plus的Wrapper查询把parentId写成字符串
文件夹的parentId在数据库里是bigint,但我在前端路由里拿到的参数是字符串,传到后端没转型就拼接进了QueryWrapper,MyBatis Plus居然也能执行成功,直接查出空列表。这种类型不匹配导致的隐式转换问题非常隐蔽,需要靠日志才能发现。建议所有从前端拿到的ID类型,在后端接口里统一用Long.parseLong或@RequestParam(required = true) Long强制类型转换,尽早暴露问题。
坑五:回收站删除后用户空间没减
这个属于业务逻辑遗漏。删除文件时,我只把文件记录从t_file挪到了t_recycle,没有同步把用户used_space减掉。结果用户删了一堆大文件,空间却一点没释放,首页显示还是满的。后来我在删除方法里补了事务操作:插入回收站记录、删除原文件记录、更新用户空间统计,三步必须同时成功。空间统计这类数据,更新时机要尽量紧跟操作时机,不然很容易出现“看到的数据”和“实际数据”不一致。
结语:说说这个项目还能怎么玩
如果你拿到源码只是把它跑起来看了两眼,那体验到的价值大概只发挥了三分之一。我建议你按下面这几条路径继续折腾,它的扩展空间非常大:
第一,给文件表加一个“标签”字段,用MySQL 8.0的JSON类型存标签列表,配合全文索引实现全文搜索。云盘最重要的体验之一就是找文件找得快,光靠文件名模糊查询不够,标签能大幅提升检索速度。
第二,加一个在线预览模块。图片预览用浏览器原生能力就够了,PDF预览也容易,麻烦的是Office文档和视频。视频可以用FFmpeg做转码,转成HLS分片流输出,体验能追上主流网盘。
第三,把本地存储替换成MinIO。MinIO兼容S3协议,更换存储层之后,上传下载的逻辑改动其实不大,但能换来对象存储的很多高级特性,比如版本管理、生命周期规则、冷热数据分层。
个人云盘这种项目,技术栈不算前沿,但它把Web开发里最常见的那些问题——文件IO、权限校验、并发控制、缓存设计、部署运维——全都串在了一起。认认真真把它做到能用的程度,你收获的不仅是几份代码,而是从设计到落地的完整思维方式,这是啃多少篇教程都换不来的。希望这篇记录对你有点用处,如果你在部署或扩展的过程中遇到问题,欢迎留言交流。