
给小公司搭一套轻量知识库从 Nextcloud 搬到 Cloudreve 的完整记录一、为什么要换掉 Nextcloud二、选型轻量在哪里省三、数据迁移先看清再动手探查发现了什么迁移方式为什么不能直接拷文件四、三个坑坑一把 IO 争用误判成性能瓶颈坑二文件已加密请输入密码——但文件根本没有密码坑三一个无认证的 SSRF五、上公网前的安全清单六、几个不完美的地方一台服务器、两个容器、1.6 GB 内存扛住 100 GB 公司资料的在线预览和播放。以及三个花了大半天才定位的坑。一、为什么要换掉 Nextcloud我们公司二十几号人几年下来在 Nextcloud 上攒了 100 多 GB 资料产品文档、项目过程资产、培训视频、售前方案。东西是有价值的但这套系统用得越来越不舒服重。NextcloudPHP OnlyOffice Document Server 一起跑内存常驻 4 GB 以上其中 OnlyOffice 一个人就吃掉 2–3 GB。心智模型不对。Nextcloud 的模型是每个人有自己的网盘而我们要的是公司有一个统一的知识库。最后的实际用法是——所有资料塞在一个公共账号下其他数百个账号形同虚设。功能冗余。日历、聊天、任务、邮件、看板……我们一个都没用。需求其实很朴素文档和视频能在线预览、能下载、能播放目录结构清楚。为这点需求扛 4 GB 内存和一堆用不上的模块不值。二、选型轻量在哪里省最终方案是Cloudreve v4 fileview两个 Docker 容器。旧方案新方案主体Nextcloud (PHP)Cloudreve v4 (Go 单二进制)Office 预览OnlyOffice Document Serverfileview (Java)内存占用~4 GB~1.6 GB主体进程内存~800 MB (php-fpm 多进程)~170 MBCloudreve 是 Go 写的单二进制空载 145 MB跑满 100 GB 数据也就 170 MB 左右。国产项目界面天然合适自带目录管理、分享链接、视频播放器Artplayer、WebDAV。有个意外收获Cloudreve v4 的官方镜像自带 ffmpeg、LibreOffice、libvips这也是镜像 1.3 GB 的原因所以视频缩略图、Office 文件缩略图开箱可用不需要额外装东西。关于轻量的一个诚实说明Office 的高保真在线预览无论用哪套网盘都得额外跑一个转换服务这部分内存省不掉。fileview 占 1.25 GB比 OnlyOffice 的 2–3 GB 好一些但不是零成本。如果你能接受 Office 文件“点击下载、本地打开”那整套系统可以压到 200 MB 以内。架构很简单浏览器 ├─→ :5212 Cloudreve 主入口浏览、下载、视频播放、分享 └─→ :8012 fileview 只在打开 Office 文件时被加载没上 Nginx 反代。内网 HTTP 场景下它只是多一层配置和故障点Cloudreve 自带的 HTTP 服务够用。后续要上公网加 HTTPS 时再补架构不用改。三、数据迁移先看清再动手100 多 GB 的迁移最容易犯的错是直接开始搬。我们先花了二十分钟做只读探查结果直接改变了方案。探查发现了什么du -sm */排个序Nextcloud 数据目录下 87 个用户目录的分布是这样的117 GB 某个公共账号 ← 占总量 93% 3.2 GB 用户A 1.5 GB 用户B 1.2 GB 用户C ... 37 MB 用户D 37 MB 用户E ← 大量 37 MB 的账号 37 MB ...那一堆 37 MB 是什么是 Nextcloud 新建账号时自动放进去的示例文件欢迎 PDF、示例图片。多个账号从来没被真正用过。再往那个 117 GB 的账号里看一层89 GB files/ ← 真实文件 26 GB files_trashbin/ ← 回收站 1.8 GB files_versions/ ← 历史版本回收站和版本历史占了 28 GB不需要迁。还有个发现那几个看起来有几 GB 的个人账号files/里其实只有几十 MB剩下全是回收站。其中三个账号当前一个文件都没有。结论126 GB 的表面数据量真正要迁的只有87 GB87 个账号里真正有内容的只有 4 个。顺手统计了文件类型分布这决定了预览方案758 mp4 ← 全是浏览器可直接播放的格式不用转码 671 docx 326 pdf 237 pptx 199 xlsx 143 md 131 xmind ← 无预览器只能下载迁移方式为什么不能直接拷文件第一反应可能是把文件拷进 Cloudreve 的存储目录就行。不行。Cloudreve 的文件名和目录结构记录在数据库里磁盘上存的是加了随机前缀的文件uploads/1/某目录/1_MYEphK73_文件名.docx ↑ 用户ID ↑ 随机串直接拷进去Cloudreve 完全不认识网页上看不到任何东西。必须走上传接口。Cloudreve v4 有个POST /api/v4/workflow/import接口号称能扫描已有目录建索引。我在上面花了不少时间src要用相对路径试出来的、dst格式怎么试都不对、user_id要哈希 ID 而不是数字。最接近成功的一次日志显示Importing 303 physical files但文件全挂到了一个畸形位置而且每次失败都在数据库留下已导入标记重试会被跳过——也就是留下了一批孤儿记录。最后放弃这条路改用最朴素的方案rsync 拉到本地 rclone 走 WebDAV 上传。慢一点但每一步都可验证、可重跑、可回滚。# 1. 从旧机器拉到本地中转目录内网千兆实测 90-100 MB/srsync-a--infoprogress2 --rsync-pathsudo rsync\userold-server:/path/to/nextcloud/data/公共账号/files/ /srv/staging/# 2. rclone 走 WebDAV 上传实测 40-75 MB/srclone copy /srv/staging cr:\--transfers16--ignore-existing\--exclude.attachments.*/**--exclude**/.DS_StoreWebDAV 密码要在 Cloudreve 里单独创建不是登录密码。总耗时rsync 15 分钟 上传 49 分钟。3199 个文件零失败。迁完做了两轮校验逐目录比对文件数11 个目录全部一致再随机抽 10 个文件下载后比对 MD5含一个 1.6 GB 的视频全部一致。确认无误后才删掉 88 GB 的中转目录。四、三个坑真正花时间的不是搭建是这三个。坑一把 IO 争用误判成性能瓶颈上传测试时观察到12 秒才传一个文件。按这个速度 3000 个文件要 10 小时。我的第一反应是Cloudreve 每收一个文件就调 LibreOffice/ffmpeg 生成缩略图CPU 被吃满了准备去关缩略图功能。但先看了一眼实际数据cloudreve: MEM 192MiB CPU 0.06%CPU 只有 0.06%服务几乎在闲着。缩略图完全不是瓶颈。真正的原因rsync 正在满速100 MB/s占用磁盘 IO上传和它抢同一块盘。rsync 结束后立刻重测116 个文件 / 161 MB耗时 4.3 秒38 MB/s从 12 秒/文件 到 27 文件/秒。什么都没改只是不再抢 IO 了。教训很朴素动手优化之前先看一眼资源占用。如果我当时直接去关缩略图不但白费功夫还会以为关了也没用进而怀疑到别的地方去。坑二“文件已加密请输入密码”——但文件根本没有密码这个最有意思。Office 预览接通后所有 Word/Excel/PPT 打开都提示“文件已加密需要输入密码”。而那些文件在本地打开好得很从来没设过密码。顺着 fileview 的日志往下看 文件大小: 91 bytes ✅ 文件下载成功 - LocalPath: /opt/fileview/data/downloads/xxx.docx ⚠️ DOCX文件既非ZIP也OLE2读取失败保守认为加密 错误: Invalid header signature; read 0x3A2265646F63227B, expected 0xE11AB1A1E011CFD0 Office文档需要密码91 字节的 docx而那个文件头0x3A2265646F63227B转成字符是{code:。它下载到的不是文件是一段 JSON。把那 91 字节 cat 出来{code:40071,msg:sign expired}签名过期。可是我手动用同样的链接测试5 分钟后都还能正常下载。对比浏览器实际发出的请求和我手动构造的我的 signU14xJjCuRZ2PBwObURMH0Hk3qyn2k14LTO2Ba1gTcLY%3D%3A1786345482 浏览器signZJYidJHcBN4qVby-0Y4K6-yx60DlQbo2KFXDawt1aCw ↑ 到这里就没了签名本体一样尾部的%3D%3A时间戳丢了%3D%3A解码是:。去 fileview 的前端 bundle 里找 URL 处理函数找到了这段constsn.split().map(l{const[c,d]l.split();// ← 这里...return.concat(v,).concat(p);});signhash:timestamp用split()会切成三段[sign, hash, :timestamp]而解构[c, d]只取前两个第三段永久丢失。重新拼回去时只拼了cd。于是完整的因果链是签名尾部被前端截断 → Cloudreve 收到残缺签名返回 91 字节的 sign expired → fileview 把这段 JSON 当 docx 解析 → 发现既不是 ZIPdocx 本质是 zip也不是 OLE2老式 doc → 代码里写的是保守认为加密 → 前端提示文件已加密请输入密码一个 URL 解析 bug最后表现成文件加密。这种报错的误导性极强——如果不看日志很容易一路怀疑到文件本身、编码、权限上去。修复很简单改用indexOf只在第一个等号处分割const_il.indexOf();constcl.slice(0,_i),dl.slice(_i1);改的是容器里打包好的 JS所以要考虑持久化——把补丁后的文件放在宿主机上用 docker-compose 的只读 volume 覆盖进去容器重建也不会丢volumes:-./patched-js/index-XXXX.js:/opt/fileview/frontend/dist/js/index-XXXX.js:ro前端文件名带 hash镜像升级后会变这一点要在文档里写清楚否则下次升级挂载会失效。还有个细节fileview 会缓存失败状态。补丁打完不清缓存之前打开过的文件仍然报需要密码因为它直接读缓存不重新转换。坑三一个无认证的 SSRF准备把服务映射到公网前做了一轮安全检查发现 fileview 的/preview/api/netFile接口不需要任何认证且接受任意 URL。它的正常用途是从这个 URL 下载文件然后转换但没有任何来源限制。实测让它去请求内网另一台服务器curl-XPOST http://server:8012/preview/api/netFile\-d{networkFileUrl:http://192.168.x.x/,fileName:t.docx}# {code:0, ...} 接受了日志里能看到它真的去连了 开始下载网络文件 - URL: http://192.168.x.x ❗ 下载失败 - Error: HTTP connect timed out这就是典型的 SSRF。返回的错误信息超时 vs 连接拒绝 vs 404足以判断目标端口是否开放。好在 fileview 自带白名单配置不用改代码environment:-FILEVIEW_NETWORK_SECURITY_TRUSTEDSITESCloudreve的IP,127.0.0.1对应配置项fileview.network.security.trusted-sites逗号分隔为空则不限制。踩了个小坑白名单写成IP:5212带端口会导致合法请求也被拒它比对的是主机名不含端口。修完验证内网其他主机、外网地址、网关、云元数据地址169.254.169.254全部返回访问拒绝六种常见绕过手法也都无效——userinfo 混淆http://白名单IP目标IP/、十进制 IPhttp://3232261131/、十六进制 IP、.nip.io这类 DNS 后缀伪装、file://协议。五、上公网前的安全清单内网用没什么问题但要映射到公网我实测出这几条必须先处理问题实测情况预览服务端口不能暴露fileview 全程无认证。除了 SSRF它的其他接口也对任何人开放登录无暴力破解防护连续 8 次错误密码全部只返回密码错误无限流、无锁定、无验证码。必须在反代层补纯 HTTP 明文密码和文件内容明文传输管理接口虽有鉴权但建议反代层直接拒绝外网访问减小攻击面确认没问题的部分Cloudreve 自身鉴权是可靠的未登录访问文件接口返回 401管理接口 404用户注册默认关闭。六、几个不完美的地方写文章容易只讲成功的部分这里列一下这套方案的真实短板视频格式受限。浏览器只能播 mp4(H.264)/webm。avi/mkv/flv/wmv 能上传能下载但不能在线播放镜像里没有转码流程。我们运气好758 个视频全是 mp4/mov。没有团队空间。Cloudreve v4 社区版的存储路径含用户 ID物理隔离没有共享容器的概念。要让别人看到资料得靠分享链接分享是目录级的子目录自动包含不用逐个文件分享对方打开链接后可以保存快捷方式到自己的文件列表里体验上接近共享空间。但没有直接指定分享给某个账号这种操作。账号即归属。文件挂在创建者名下。想换个账号持有资料要么改现有账号的邮箱要么重传一遍——因为存储路径含用户 ID。单机无冗余。这是最需要补的一块。100 GB 数据在一块盘上盘坏就全丢。本地备份意义不大得有外部目标。xmind 之类的格式无预览只能下载。