基于Spring Boot的私人云网盘实现:分片上传、断点续传与安全部署实战
先交代下背景我手头有一台不关机的小主机平时要存的东西越来越多——手机照片、工作文档、影视资源散落在电脑、移动硬盘和各个网盘里。公有网盘要么限速要么担心隐私折腾一圈下来还是决定自己搭一个私人云网盘。技术栈很自然地选了Spring Boot生态成熟、上手快、资料多前后端一个人搞也扛得住。这篇文章把我从零开始实现这套系统的过程、踩过的坑、以及生产部署后的实测体验完整记录下来给同样想自己搭网盘的人一个参考。1. 私有网盘的定位差异先想清楚它和公有网盘的区别动手写代码之前我花了不少时间想一个问题私人云网盘和公有网盘到底差在哪如果只是把文件存到服务器上、提供一个下载链接那这项目没什么意思。真正的差异在于使用对象和使用场景。公有网盘面向海量用户功能堆得很多同步盘、在线编辑、多端实时同步、共享空间。私人网盘面向的是自己或者三五个人核心需求其实就几条文件能安全存进去、能方便取出来、分享给别人的时候不泄露隐私。功能一定要克制做得越多维护成本越高出问题的面也越大。所以我给这个项目划了一条清晰的功能边界用户登录与注册其实注册也只给自己人用没必要做复杂的审批流目录管理创建、重命名、移动文件上传支持分片、秒传、断点续传算加分项但一开始就必须设计好文件下载与在线预览图片、视频、音频、常见文档分享链接可选有效期和提取码暂缓的功能包括多人在线协同编辑、增量同步客户端、移动端App。这些后续有精力了再加不影响主体框架。这个定位影响了我后续所有的技术决策。比如存储层我最终选的是本地磁盘Mysql元数据而不是引入MinIO或者公有云对象存储。下面详细说说。2. 存储方案选型为什么我坚持用本地磁盘 MySQL 而不上对象存储项目开始前我在存储层面纠结了很久。网上搜Spring Boot网盘项目大半推荐接MinIO理由是对象存储适合海量文件、支持桶策略、有现成的S3 API。这些都对但对于私人云网盘这个场景我认真算了一笔账。2.1 三种存储方案的真实对比方案优点缺点适合场景本地磁盘 DB存元数据实现简单、零额外依赖、读写快、便于直接备份横向扩展难、磁盘坏了数据可能丢单机自用、小团队内网MinIO 对象存储有S3接口、支持桶策略、可集群多一个中间件要维护、配置项多、小规模反而绕多节点、需要统一对象接口公有云OSS无限容量、CDN加持要花钱、数据出海、依赖外网有预算、追求稳定托管我这个场景数据量撑死在几TB以内用户数两位数封顶。MinIO引入后确实能学到东西但也会带来新问题部署要占内存要管理accessKey还要处理桶和对象的生命周期。用本地磁盘目录结构一目了然出问题可以直接用du、ls这些基础命令排查对于一个以实用为主的私人项目这是最大的优势。2.2 元数据与文件实体分离为什么数据库只存索引选完存储介质紧接着是架构上的关键决策用户看到的目录树和文件并不直接对应服务器上的真实路径。我在MySQL里维护了一张file_info表记录了文件的逻辑路径、文件名、大小、MD5等信息而文件实体则以对象ID.扩展名的形式躺在磁盘存储目录里。这样做有三个直接好处解决重名问题用户上传两个都叫报告.pdf的文件物理文件名会是a1b2c3d4.pdf和e5f6a7b8.pdf互不干扰。方便移动和重命名用户在前端把文件从A目录拖到B目录后台只需要改数据库里的一行path字段磁盘上的文件动都不用动秒完成。天然防止路径穿越物理路径完全由服务端生成用户输入的原始文件名只作为展示名保存在数据库里不会直接拼进文件系统路径。2.3 Spring Boot 版本选型与依赖清单我用的是Spring Boot 2.7.x。为什么不直接上3.x因为2.7还在维护期内javax包体系对老资料兼容性最好网上踩坑记录也多。考虑到核心是文件上传下载依赖尽量精简dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependency /dependenciesSpring Security这里不是用来做复杂权限模型的主要是配合JWT做登录认证和接口拦截后面章节会展开说。MyBatis-Plus纯粹图它写CRUD快反正表结构就那几张不需要上JPA全家桶。3. 数据库表设计3张核心表和1张辅助表私人网盘的表结构不需要很复杂但字段一定要设计对。我最终沉淀下来4张表用户表、文件信息表、分享链接表、分片上传临时表。下面逐个拆解。3.1 用户表认证与配额CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, nickname varchar(50) DEFAULT NULL, total_quota bigint DEFAULT 107374182400 COMMENT 总配额默认100GB, used_quota bigint DEFAULT 0 COMMENT 已用空间, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;很多网盘项目把配额忽略掉了实际用起来迟早要补。我一开始没做结果有次导照片把磁盘塞满整个服务挂掉排查半天才发现是没配额控制。后来老老实实加上文件上传前先检查used_quota 文件大小是否超过total_quota不够就直接拒绝。3.2 文件信息表逻辑目录的落点CREATE TABLE file_info ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL, parent_id bigint DEFAULT 0 COMMENT 父目录ID0表示根目录, file_name varchar(255) NOT NULL, file_path varchar(500) DEFAULT NULL COMMENT 当前目录路径用于列表查询, file_type tinyint DEFAULT 0 COMMENT 0-目录 1-文件, file_size bigint DEFAULT 0, file_md5 varchar(32) DEFAULT NULL, storage_path varchar(255) DEFAULT NULL COMMENT 物理存储路径, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_user_parent (user_id, parent_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个细节parent_id表示目录结构file_path我当时图省事存了一份冗余路径后来发现这个字段其实可以不要。列表查询时用user_id parent_id足够定位了而且更灵活重命名目录时不用连带更新所有子文件。如果非要做面包屑导航通过 parent_id 递归向上查或者在前端维护路径映射即可。这是我的一个教训冗余字段看着省事实际引入了一致性问题改目录名时要同步更新所有子记录的字段很容易漏。3.3 分享链接表提权访问的桥梁CREATE TABLE share_link ( id bigint NOT NULL AUTO_INCREMENT, share_token varchar(32) NOT NULL COMMENT 分享token, extract_code varchar(6) DEFAULT NULL COMMENT 提取码, user_id bigint NOT NULL, file_id bigint NOT NULL COMMENT 被分享的文件/目录ID, expire_time datetime DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_token (share_token) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;分享链接的思路是不直接开放文件ID访问而是生成一个随机的share_token作为入口。这样即使别人拿到链接也只能访问被分享的那一个文件不能通过修改ID来遍历别人的文件。提取码是可选的用于私密分享。3.4 分片上传临时表承担断点续传的元数据CREATE TABLE upload_task ( id bigint NOT NULL AUTO_INCREMENT, task_id varchar(64) NOT NULL COMMENT 前端生成的上传任务ID, user_id bigint NOT NULL, file_name varchar(255) NOT NULL, file_md5 varchar(32) NOT NULL, file_size bigint NOT NULL, chunk_size int NOT NULL, chunk_total int NOT NULL, chunk_uploaded int DEFAULT 0 COMMENT 已上传分片数, storage_path varchar(255) DEFAULT NULL, status tinyint DEFAULT 0 COMMENT 0-上传中 1-合并完成 2-已失效, PRIMARY KEY (id), UNIQUE KEY uk_task (task_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这张表是整个断点续传功能的数据基础。上传分片会在临时目录落盘所有分片传完后触发合并。如果中途断了下次带着同一个task_id回来服务端能查出已上传了哪些分片前端自动跳过。表的数量不多但每张的字段我都磨了几轮。尤其是file_info表后期加配额字段、加MD5字段的时候数据迁移是真的麻烦。建议动手前就把用户配额、秒传校验这些考虑进去别等上线了再改表。4. 上传链路分片、秒传、断点续传的完整实现网盘系统最核心的部分不是列表、不是下载而是上传。小文件还好几GB的大文件如果用普通POST上传要么超时要么断线全部重来。所以上传模块我重点做了三件事分片上传、MD5秒传、断点续传。4.1 前端为什么要切片以及怎么切浏览器XMLHttpRequest不支持直接暂停恢复大文件上传但通过File.slice()可以把文件切成一个个Blob然后按顺序上传每个分片。前端逻辑不复杂const CHUNK_SIZE 5 * 1024 * 1024; // 5MB一片 function createChunks(file) { const chunks []; let start 0; while (start file.size) { const end Math.min(start CHUNK_SIZE, file.size); chunks.push({ file: file.slice(start, end), index: chunks.length, total: Math.ceil(file.size / CHUNK_SIZE) }); start end; } return chunks; }切片大小我试过1MB、2MB、5MB、10MB。在小主机环境网络上行一般10-20Mbps5MB一片最平衡一片的传输时间大概2-5秒失败重传的成本可控分片数量又不会太多导致请求太频繁。4.2 后端接口三连初始化、上传分片、合并后端设计成三个接口配合使用。第一步前端拿到文件后先算MD5Web Worker里算避免卡UI然后请求初始化接口PostMapping(/upload/init) public ResultUploadInitVO init(RequestBody UploadInitDTO dto) { // 1. 先查秒传如果文件MD5已经存在且校验大小一致直接返回秒传成功 // 2. 校验配额是否足够 // 3. 生成taskId和临时存储路径在upload_task表插入记录 // 4. 返回 { taskId, chunkSize, chunkTotal, uploadedChunks: [] } }uploadedChunks这个返回值很关键。如果之前传过一部分片这里返回已上传的分片序号列表前端直接跳过这就是断点续传的核心。第二步循环上传每个分片PostMapping(/upload/chunk) public ResultBoolean uploadChunk( RequestParam(taskId) String taskId, RequestParam(chunkIndex) Integer chunkIndex, RequestPart(file) MultipartFile chunk) { // 1. 从upload_task表查出任务校验状态 // 2. 写临时文件{存储根目录}/tmp/{taskId}/{chunkIndex}.part // 3. 更新upload_task表的chunk_uploaded计数用乐观锁防止重复提交 }第三步当前端收到所有分片上传成功的响应后调用合并接口PostMapping(/upload/merge) public ResultFileVO merge(RequestBody MergeDTO dto) { // 1. 校验分片数量是否齐全 // 2. 按分片序号从小到大逐个写入最终文件 // 3. 计算最终文件的MD5与前端传来的MD5比对不一致说明分片传错了 // 4. 清理临时目录和upload_task记录 // 5. 在file_info表插入正式记录更新用户已用空间 }4.3 合并时我踩过的最深的坑并发覆盖这个坑几乎让我怀疑人生。现象是两个完全不同的文件各自在前端正常上传合并的时候后合并的那个文件把先合并的那个覆盖了。查了很久发现问题出在最终文件的命名上。我当时偷懒直接用原始文件名_时间戳.ext作为存储文件名。理论上不会重但合并代码里我在写入最终文件之前先创建了File对象然后两个线程同时走到创建这一步都拿到了同一个路径的File对象引用。由于时间戳精度只到秒两个几乎同时到达的合并请求生成了完全相同的文件名于是后一个覆盖了前一个。解决办法很粗暴也很有效存储文件名直接用UUID或雪花ID生成拼上扩展名完全摆脱时间戳依赖。后来我干脆用MD5前12位 随机4位 .ext作为存储名既能保留一定的可读性又保证唯一。4.4 秒传的实现细节秒传的逻辑其实很简单用户上传文件时先传MD5给后端后端去file_info表里查如果存在相同MD5且文件大小一致的文件就直接把用户的file_info记录指向这个已存在的文件实体不再做分片上传。因为这个系统用户量很小甚至可以跨用户秒传同一个局域网里大家一起用的网盘某个人上传过的资源其他人再传就是秒完非常爽。但这里有个容易被忽略的问题MD5碰撞虽然概率极低但不是零。所以光比较MD5不够我增加了文件大小比较两个条件都命中才触发秒传。另外秒传只对完整的文件用户有意义如果服务器上出现过相同MD5但文件损坏的情况秒传会把坏文件关联给下一个用户。为了稳妥我在合并完成后会重新计算整个文件的MD5并和任务记录的MD5比对不一致就标记合并失败宁可让用户重传也不能保留一个不可信的秒传结果。5. 下载与在线预览Range请求是绕不开的坎上传搞定后最爽的下载功能反而暴露了一个问题直接用ResponseEntitybyte[]或者输入流拷贝下载几GB的文件浏览器会一直转圈而且拖动进度条完全失效。这是因为没有实现HTTP Range请求。5.1 理解Range请求HTTP协议里客户端可以通过请求头Range: bytes0-1023告诉服务器只返回文件的某一段字节。服务器返回206 Partial Content并带上Content-Range响应头。这个能力是所有在线视频播放、音频拖动的基石。Spring Boot里实现Range并不复杂但框架默认不带这个逻辑需要自己处理请求头。我的实现思路GetMapping(/file/download) public ResponseEntityResource download( RequestParam Long fileId, RequestHeader(value Range, required false) String rangeHeader) throws IOException { File file new File(storagePath); long fileLength file.length(); long start 0; long end fileLength - 1; if (rangeHeader ! null) { String[] range rangeHeader.replace(bytes, ).split(-); start Long.parseLong(range[0]); if (range.length 1 !range[1].isEmpty()) { end Long.parseLong(range[1]); } long length end - start 1; InputStream inputStream new FileInputStream(file); inputStream.skip(start); return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT) .header(Content-Range, bytes start - end / fileLength) .header(Accept-Ranges, bytes) .header(Content-Length, String.valueOf(length)) .header(Content-Disposition, attachment; filename\ URLEncoder.encode(fileName, UTF-8) \) .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(new InputStreamResource(inputStream)); } // 无Range头时返回完整文件 }这里用InputStreamResource而不是把文件读进内存是为了避免大文件引起OOM。InputStreamResource基于流读取边读边写内存占用几乎是恒定的。5.2 预览功能的取舍在线预览我分了三个等级来实现图片和音频直接通过流式接口返回配合Content-Type浏览器自己就能展示。视频只要实现了上面的Range请求HTML5 Video标签就能拖动播放不需要任何前端插件。文档类Word/Excel/PDF这块最麻烦。PDF可以用浏览器内置预览Word和Excel原生不能直接预览。我的做法是PDF直接返回application/pdf让浏览器加载Word和Excel先用LibreOffice无头转换一次转成PDF后再预览。受小主机性能限制这个转换对超大文档会很慢所以我做了一层文件缓存转换过的PDF存在临时目录下次直接读缓存。如果文件超过5MB直接让它下载不强行预览。5.3 下载限速防止一个人占满带宽局域网里有人用网盘下大文件带宽很容易被吃光其他人的视频预览就卡住了。这个在私人网盘场景下非常真实。我的解决方案是在下载响应流外面包一层限速流每读一个chunk就sleep一下控制读取速率。public class ThrottledInputStream extends InputStream { private final InputStream original; private final long maxBytesPerSecond; private long lastRefreshTime; private long bytesSinceLastRefresh; Override public int read(byte[] b, int off, int len) throws IOException { long now System.currentTimeMillis(); long elapsed now - lastRefreshTime; if (elapsed 1000) { bytesSinceLastRefresh 0; lastRefreshTime now; } long budget maxBytesPerSecond * elapsed / 1000 - bytesSinceLastRefresh; if (budget 0) { try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return 0; } int toRead (int) Math.min(len, budget); int bytesRead original.read(b, off, toRead); bytesSinceLastRefresh bytesRead; return bytesRead; } }限速值我默认设为单线程下载5MB/s基本满足个人远程取文件的需求又不至于拖垮其他服务。这种妥协在私人网盘里很合理但如果是团队共享使用最好做成可配置项放在管理页面里而不是写死在代码中。6. 权限与安全登录校验、路径穿越和分享链接那点事网盘系统最怕的是什么不是硬盘坏是隐私泄露。自己用的系统反而容易忽略安全因为觉得没人会来攻击。但实际上只要服务暴露在公网上扫描器半小时就能发现你。6.1 用JWT作为无状态认证我选择JWT而不是Session主要因为Spring Security JWT可以做到完全无状态服务重启后不用重新登录也有利于以后拆成更多服务时共用同一套认证逻辑。实现时在Security配置里注册一个JWT过滤器每次请求从Header取token验签通过后把用户信息放进SecurityContext。文件上传下载接口需要登录态分享链接接口则单独放行。Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(/api/login, /api/register, /s/**, /file/preview/**).permitAll() .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }这里s/**路径下的分享链接是公开访问的后面会讲它内部如何校验。6.2 路径穿越攻击与防护最容易被忽略的高危漏洞路径穿越漏洞在文件网盘里属于经典必测漏洞。攻击手法一般是请求下载某个文件时构造../../etc/passwd之类的参数试图读服务器上的敏感文件。我的防护分了三层。第一层所有文件操作的接口只接收fileId数据库主键不接收任何路径参数。物理路径在服务端存储路径基础上拼出来的用户根本碰不到路径。第二层即使传了路径参数也要做规范化检查String basePath new File(StorageProperties.getRootPath()).getCanonicalPath(); String targetPath new File(basePath, userInputPath).getCanonicalPath(); if (!targetPath.startsWith(basePath)) { throw new BusinessException(非法路径); }用getCanonicalPath()会把..和符号链接解析掉再比对前缀这一招能挡住绝大多数路径穿越变体。第三层上传的文件名做了白名单校验强制过滤掉/、\、..等字符并且最终落盘文件名永远走UUID逻辑和用户文件名无关。6.3 分享链接的访问控制细节分享链接的公开接口必须格外小心。我的处理逻辑是前端分享时生成 share_token 和 extract_codeexpire_time 可为空表示永久。访问公开预览接口时先按 token 查出 share_link如果设置了提取码且不是空字符串要求请求参数里带上 code比对失败拒绝访问。查到期时间过期的直接返回链接已失效。由 file_id 定位到 file_info校验文件所有者的id是否与share_link.user_id一致防止用户A把用户B的文件ID拼进自己的分享链接绕过权限。之后公开访问时在response header里加一句Content-Disposition同时给不带提取码的链接附上短有效期默认7天这样即使拿到了分享链接但忘了提取码过期链接也自动作废。6.4 密码存储与登录防爆破密码不能用MD5直接存这是常识。我用的BCrypt加密Spring Security自带该实现加盐成本是自动处理的。登录接口做了简单的失败计数限制同一个IP 5分钟内失败超过10次就锁定15分钟。暴力破解这个漏洞在私网盘上同样不能掉以轻心因为一旦登录被突破整个文件库就暴露了。7. 大文件合并、磁盘IO与内存占用实测开发时大家都用小文件测试看着都跑得好好的一到真实使用就翻车。我这边翻车翻在往网盘里导入一个40GB的文件夹备份时里面是大量小文件加几个几GB的虚拟磁盘镜像IO直接卡死服务监控告警SSH都几乎敲不动命令。后来查下来合并逻辑里有三个问题一是用FileOutputStream按单个分片循环写入每次写完没flush导致文件描述符和缓冲一直积压。改成每片写完后flush()再关闭流。二是合并大文件时代码里有把文件读进byte数组做校验的操作虽然用的是Files.readAllBytes()但一个5GB的文件直接OOMJVM垃圾回收频繁整个服务响应变慢。改成边合并边计算MD5用DigestInputStream包住输入流避免同时加载全文件到内存。三是临时分片文件全放在同一个目录文件数量达到几千上万后文件系统访问性能明显下降。Linux ext4单目录文件数量超过几万后寻址耗时显著增加。我把临时分片按照taskId前两位做了二级目录划分每个目录下的文件数量最多几百个性能问题解除。以下是合并模块最终形态的核心代码结构public File mergeChunks(UploadTask task) throws IOException { File target new File(storagePath); try (FileOutputStream fos new FileOutputStream(target)) { for (int i 0; i task.getChunkTotal(); i) { File chunkFile new File(chunkDir, i .part); try (FileInputStream fis new FileInputStream(chunkFile)) { byte[] buffer new byte[1024 * 1024]; int len; while ((len fis.read(buffer)) ! -1) { fos.write(buffer, 0, len); } } chunkFile.delete(); } } // 计算合并后的MD5做一致性校验 return target; }这个过程中没有涉及任何花哨操作全都是基础API但正因为是基础API一不留神就会性能翻车。我的建议是大文件合并这种IO密集型的代码写完一定要用真实的大文件压测一遍别信看起来没问题。8. 生产环境部署Nginx反向代理、HTTPS与断点续传的兼容本机跑通只是第一步要真正替代公有网盘必须放到一台公网服务器上。部署阶段我踩的坑也不少挑两个最影响体验的说。8.1 Nginx配置中的Range请求转发问题本地直连Spring Boot测试视频拖动没问题一旦上了Nginx反向代理视频进度条拖动就失效。排查下来Nginx默认配置会把Range请求头透传给后端但后端响应中的Accept-Ranges如果没设置代理层就会把响应缓存或转成整个文件返回。解决的要点有两个server { listen 443 ssl http2; server_name your-domain.com; client_max_body_size 0; # 不限制上传大小 location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_request_buffering off; # 关闭请求缓冲让分片上传即时转发 proxy_buffering off; # 关闭响应缓冲避免Range响应被代理层缓存 } }关键的坑就是proxy_request_buffering off。Nginx默认会先把整个请求体缓冲完再转给后端一个大分片上传会等全部接收完成才转发前端看着一直卡在发送中体验极差对大文件断点续传影响更大已经传完的分片数据会被Nginx缓冲导致重复转发。client_max_body_size 0也很关键默认1MB限制一旦碰到大分片直接413纯粹的分片上传不受单个分片大小影响但如果以后要支持直传整个小文件这里不关掉就会炸。8.2 HTTPS 证书配置用免费的证书就好私人网盘没有预算买商业证书用免费的Lets Encrypt完全够用。我的操作思路是先用certbot申请证书生成证书文件后在Nginx中配置ssl_certificate和ssl_certificate_key每年到期前自动续签。要注意的是证书申请过程会短暂占用80端口做验证如果Nginx已经在运行certbot默认方式会失败。建议先把Nginx停掉申请完成后再启动或者用standalone插件手工验证不要图省事跳过这一环。8.3 文件备份策略最后还想提一句备份。网盘的命根子是文件实体数据库丢了可以重新建文件丢了就全完了。我的备份方案很简单服务器上的存储目录每天凌晨通过rsync增量同步到另一块独立硬盘数据库用mysqldump每天导出一份SQL备份。整个备份放到cron里自动执行不依赖人力。# 每天凌晨2点执行 0 2 * * * rsync -av --delete /data/netdisk-storage/ /backup/disk/ 0 3 * * * mysqldump -u netdisk -pYOUR_PASSWORD netdisk /backup/db/netdisk_$(date \%Y\%m\%d).sql我个人的体会是私人网盘这个项目最迷人的地方不在于功能有多花哨而在于所有逻辑都在自己的掌控范围内。上传链路自己写的下载限速自己配的安全策略自己定的用起来踏实。如果你也打算动手做一个建议先把上传和下载这两条主链路跑顺再逐步加分享、预览、配额这些外围能力。先把核心链路搞稳后面每一步扩展都会轻松很多。

相关新闻

VLA模型显存压缩实战:QVLA量化方案解析与部署指南

VLA模型显存压缩实战:QVLA量化方案解析与部署指南

先交代一下背景:我在做具身智能和机器人操作方向,VLA(视觉-语言-动作)模型这两年是真的火,但也是真的难落地。动不动就是十几B甚至几十B的参数,一张H100都差点塞不下,推理时显存占用直接把人劝退…

2026/10/18 5:39:42 阅读更多 →
从源码收集到工程复用:打造个人可检索可运行的代码资源库

从源码收集到工程复用:打造个人可检索可运行的代码资源库

经常有朋友问我:你整理的那个源码系列,规模都按万套算了,真的看得完吗?说实话,这种量级的资源,谁都看不完,我前两年也是这样。但这个系列做到第三期,我的答案慢慢变了——上万套源码…

2026/10/18 5:39:42 阅读更多 →
汇编期末填空题复习指南:高频考点与提分技巧

汇编期末填空题复习指南:高频考点与提分技巧

1. 从"更新完毕"四个字里读出的复习信号看到"汇编期末考试复习【填空题】更新完毕"这个标题,我第一反应不是"哦,又一份资料",而是"终于有人把填空题单独拎出来整理了"。考过汇编的人都知道&#xff…

2026/10/18 5:38:42 阅读更多 →

最新新闻

SpringBoot+Vue健身房管理系统设计与实现全解析

SpringBoot+Vue健身房管理系统设计与实现全解析

1. 项目概述与业务价值拆解1.1 这个管理系统到底解决什么问题先说结论:健身房管理系统是目前JavaWeb毕设里性价比极高的一类选题。它不像电商系统那样业务链路冗长,也不像纯后台管理系统那样缺乏亮点,而是处在一个恰到好处的位置——业务逻辑…

2026/10/18 6:31:52 阅读更多 →
PCL点云识别实战:从FPFH特征到6自由度位姿估计

PCL点云识别实战:从FPFH特征到6自由度位姿估计

做点云处理的人迟早会遇到一个尴尬局面:模型库里的三维点云有鼻子有眼,可面对桌面上一堆散乱摆放的真实物体,程序死活认不出哪个是目标。PCL的Recognition模块,就是在这类场景里做点云识别的“工具箱”。它的目标很直白&#xff1…

2026/10/18 6:31:52 阅读更多 →
让 AI 接管电脑重复工作:OpenClaw 从解压到指令测试全流程接入 TaoToken

让 AI 接管电脑重复工作:OpenClaw 从解压到指令测试全流程接入 TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 6:31:52 阅读更多 →
超图三维GIS平台实战:从PPT课件到可运行三维场景的完整链路

超图三维GIS平台实战:从PPT课件到可运行三维场景的完整链路

简介:这份PPT课件面向GIS专业学生、三维开发人员及技术选型决策者,系统讲解SuperMap Realspace二三维一体化三维GIS平台。内容从2005年Google Earth掀起的三维热潮切入,剖析三维GIS“好看却难用”的应用困境,梳理CAD到GIS的演替脉…

2026/10/18 6:31:52 阅读更多 →
一篇万字热帖说通行密钥是“陷阱“,我拿两个本地域名实测了域名绑定

一篇万字热帖说通行密钥是“陷阱“,我拿两个本地域名实测了域名绑定

文章目录 📖 介绍 📖 🏡 演示环境 🏡 🔑 从争论到实测:凭据的域名归属 🔑 🧭 先把这场争论还原出来 🎯 域名绑定:把凭据从一个源搬到另一个源会发生什么 🗂️ 凭据在认证器里到底长什么样 🧪 一个坑:认证器的两个小门槛 🧾 支持"更安全"那一…

2026/10/18 6:31:52 阅读更多 →
Servlet+JSP+MySQL:手把手拆解经典Java Web新闻发布系统

Servlet+JSP+MySQL:手把手拆解经典Java Web新闻发布系统

简介:基于Java Servlet、JSP和MySQL的Web新闻发布系统源码,适合正在学习Java Web开发的初学者,以及需要课程设计或毕业设计参考的学生。项目采用MVC分层模式,实现了新闻的发布、编辑、删除和展示,还包含用户登录认证、…

2026/10/18 6:30:52 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →