一个简单的局域网云盘,用于局域网内传输文件。
基于 Nuxt 4(Nitro)+ TypeScript 构建:单进程、单端口、无代理配置,nuxt build 后直接 node 运行。
文件操作
- 多文件上传、文件夹上传(保留目录层级)
- 拖拽上传:把文件 / 整个文件夹从资源管理器拖进页面任意位置即可
- 文件下载、文件夹打包下载
- 文件与文件夹删除、重命名、新建文件夹、移动
- 图片、PDF、部分文本文件预览
- 浏览器前进 / 后退、刷新后保持当前目录
界面
- 上传按钮悬停展开下拉菜单(上传文件 / 上传文件夹)
- 拖拽时弹出带目标路径的遮罩卡片
- 全局 toast 通知 + 自定义对话框(替换原生
alert/confirm/prompt) - 上传过程有进度提示;空目录显示可点击的上传引导
- 选中文件后底部浮出批量操作栏(下载 / 移动 / 删除 / 取消选择)
- 列表加载显示骨架屏;修改时间显示相对时间(悬停可看绝对时间)
- 表头点击排序(名称 / 大小 / 修改时间,目录始终在前)
- 深色 / 浅色主题切换,选择持久化到本地
需要 Node ≥ 20(推荐 20/22 LTS)。
pnpm install
pnpm dev # 开发,端口读 config.json
pnpm typecheck # 类型检查(vue-tsc)
pnpm build # 构建并打包到 dist/(自包含发布目录)
pnpm start # 生产运行(读 config.json 的 port,并监听 0.0.0.0)
pnpm test # 接口冒烟测试(需先启动服务)首次启动会自动创建,位于运行目录下:
{
"dest": "./disk", // 根目录,绝对路径或相对运行目录的路径
"hasDel": true, // 是否允许删除
"port": 9527 // 端口
}端口由
start.mjs读取后注入环境变量再启动 Nitro;dest会基于运行目录解析, 根目录不存在时自动创建,运行中若被删除也会自动重建。 Docker 镜像里已内置一份上述默认配置,直接启动即可,无需自己准备这个文件(见「使用已发布镜像」)。
pnpm build 会构建并打包出一个自包含的发布目录 dist/,包含运行所需的一切:
dist/
├── .output/ 服务端 + 静态资源(含运行时依赖,已解引用符号链接)
├── start.mjs 启动脚本(读 config.json 端口)
├── ecosystem.config.cjs PM2 配置
├── config.json 运行配置(可改 dest / port / hasDel)
└── package.json type: module + 启动命令
部署到服务器:把 dist/ 整体拷贝到服务器,然后在 dist 目录内运行:
# 方式一:前台运行
cd dist && node start.mjs
# 方式二:PM2 守护(崩溃自动重启)
cd dist && pm2 start ecosystem.config.cjs
# 日常管理
pm2 status # 查看状态
pm2 logs disk # 查看日志
pm2 restart disk # 重启
pm2 stop disk # 停止
pm2 delete disk # 移除
# 开机自启(可选)
pm2 save
pm2 startupdist/ 内的 package.json 也提供了 npm 脚本(npm start、npm run pm2:start 等),
pm2 相关命令只有在构建产物 dist/ 里才会出现,开发目录保持干净。
端口与根目录仍读
config.json,修改后执行pm2 restart disk生效。 若用 fnm 管理 Node,启动 PM2 前需保证 node 在 PATH 中(如eval "$(fnm env --shell bash)")。start.mjs以「运行目录」为基准读 config.json 并解析dest,部署到服务器时务必cd dist后再运行。
镜像采用多阶段构建:构建阶段装依赖并执行 pnpm build,运行阶段只保留自包含的 dist/,
基础镜像为 node:22-alpine,以非 root 的 node 用户(uid 1000)运行,容器内监听 9527。
相关文件:
Dockerfile 多阶段构建(builder → runner)
docker-compose.yml 本地构建编排(维护者使用)
.dockerignore 排除 node_modules / .output / dist / disk 等
已发布镜像:showzea/disk:latest。使用者不需要源码,也不需要准备配置文件——镜像内已内置一份默认 config.json:
{ "dest": "./disk", "hasDel": true, "port": 9527 }所以启动时只要给两个参数:端口映射(-p)与文件存储挂载(-v)。
1. 启动
方式一:docker run
# -v 左侧是宿主机上存文件的位置,换成任意路径即可
mkdir -p ~/disk-app/data
sudo chown -R 1000:1000 ~/disk-app/data # Linux 需要;macOS / Windows 的 Docker Desktop 可跳过
docker run -d --name disk \
--restart unless-stopped \
-p 9527:9527 \
-v ~/disk-app/data:/app/disk \
-e TZ=Asia/Shanghai \
showzea/disk:latest| 参数 | 说明 |
|---|---|
-p 9527:9527 |
<宿主机端口>:<容器内端口>。左侧随便改(如 -p 8000:9527),右侧固定 9527 |
-v ~/disk-app/data:/app/disk |
<宿主机目录>:<容器内目录>。左侧是文件实际落盘位置,右侧固定 /app/disk;也可写 -v disk-data:/app/disk 交给 named volume 管理 |
--restart unless-stopped |
崩溃 / 宿主机重启后自动拉起 |
-e TZ=Asia/Shanghai |
容器时区,影响日志时间 |
方式二:docker compose(存为 ~/disk-app/compose.yml)
services:
disk:
image: showzea/disk:latest
container_name: disk
restart: unless-stopped
ports:
- "9527:9527" # 左侧改宿主机端口,右侧固定 9527
environment:
TZ: Asia/Shanghai
volumes:
- ./data:/app/disk # 左侧改宿主机目录
# - disk-data:/app/disk # 或改用 named volume 托管
volumes:
disk-data:docker compose up -d # 启动
docker compose logs -f # 查看日志
docker compose down # 停止并移除容器(数据目录 / 数据卷保留)用宿主机目录挂载时,先
mkdir -p ./data && sudo chown -R 1000:1000 ./data(容器内以 uid 1000 的node用户运行;macOS / Windows 的 Docker Desktop 不需要这一步)。
2. 访问
浏览器打开 http://<宿主机IP>:<宿主机端口>,容器日志里也会打印 Local / Network 两个地址。
局域网内其它设备用宿主机内网 IP 访问;连不上先检查宿主机防火墙是否放行该端口。
3. 自定义配置(可选)
只有在需要关闭删除权限(hasDel: false)、限制上传大小 / 数量(maxFileSize 等)
或限定监听地址(bind)时,才需要挂一份自己的 config.json:
cat > config.json <<'EOF'
{
"dest": "./disk",
"hasDel": false,
"port": 9527
}
EOFdocker run -d --name disk --restart unless-stopped \
-p 9527:9527 \
-v ~/disk-app/data:/app/disk \
-v "$PWD/config.json:/app/config.json:ro" \
showzea/disk:latest| 字段 | 说明 |
|---|---|
dest |
文件根目录,保持 "./disk" 即可(解析为 /app/disk),需与 -v 右侧一致 |
hasDel |
是否允许删除 |
port |
容器内监听端口。改动时要同步 -p 右侧并加 -e PORT=<新端口>(见下) |
bind |
监听地址,默认 0.0.0.0(全部网卡) |
maxFileSize / maxRequestSize / maxFiles |
上传大小与数量限额,0 表示不限 |
公网 / 不可信网络场景建议设置
bind: "127.0.0.1"并显式配置这几个限额。 挂载的config.json不存在时,Docker 会把挂载点建成目录,服务将回退默认配置并监听3000(见常见问题)。
4. 换端口 / 升级
# 只换对外端口:改映射左侧即可,容器内仍是 9527(最省事)
docker rm -f disk
docker run -d --name disk --restart unless-stopped \
-p 8000:9527 \
-v ~/disk-app/data:/app/disk \
showzea/disk:latest
# 升级到新版本(数据目录 / 数据卷里的文件不受影响)
docker pull showzea/disk:latest
docker compose up -d # 或 docker rm -f disk 后重新 docker run容器内监听端口固定为
9527(由内置config.json决定)。只传-e PORT=8080不生效:start.mjs会用config.json的port填NITRO_PORT,而 Nitro 的优先级是NITRO_PORT>PORT。 确实要改容器内端口时,需同时改config.json的port、-p右侧和-e PORT=<新端口>(最后一项是让内置健康检查跟随探测;漏掉则docker ps会显示unhealthy,服务本身仍可用)。 绝大多数情况只改-p左侧。
-
挂载了自己的
config.json后映射的端口打不开,日志出现config.json 解析失败,回退默认配置宿主机上的config.json文件不存在时,Docker 会把挂载点创建成目录,读取时抛错并回退默认配置 (端口3000),于是-p右侧写的 9527 无人监听。确认-v左侧是文件而非目录:docker exec disk ls -l /app/config.json # 应是文件,不是目录
直接去掉
-v ...:/app/config.json:ro这一项即可回到镜像内置的默认配置。 -
上传 / 删除报 EACCES 权限错误 绑定挂载的宿主机目录属主不是 uid 1000,执行
sudo chown -R 1000:1000 <宿主机目录>; 或改用-v disk-data:/app/disk让 Docker 托管数据卷。 -
docker ps显示 unhealthy 多为容器内端口与健康检查探针不一致(见上「换端口」),或还在start_period(10 秒)内。 日志里出现[disk] Network http://...即说明服务已就绪。 -
查看日志
docker logs -f disk(或docker compose logs -f)。启动时会打印根目录、删除权限与访问地址。 -
ARM 设备(Apple Silicon / 树莓派)报
exec format error说明镜像未包含该架构,在 compose 中加platform: linux/amd64(走模拟,性能略降), 或改用对应架构的镜像。
docker compose up -d --build # 本地构建并后台启动
docker compose ps # 查看状态(含健康检查)
docker compose logs -f disk # 查看日志
docker compose restart disk # 改完 config.json 后重启生效
docker compose down # 停止并移除容器(数据卷保留)仓库内的 docker-compose.yml 会调用 Dockerfile 自行构建,语义与上面使用者用的片段一致,
只是把 image 换成了 build。构建产物 dist/ 与镜像内容保持一致(.output + start.mjs +
config.json),因此本地构建与拉取镜像的运行方式、配置项完全相同。
# 构建并推送到 Docker Hub
docker build -t showzea/disk:latest .
docker push showzea/disk:latestapp/ 前端源码(Nuxt 4 默认 srcDir)
app.vue 根布局:<NuxtPage /> + Toast + AppDialog
pages/index.vue 主页面
components/ FileBreadcrumb、FileTable、MoveDialog、ContextMenu、DropZone、Toast、AppDialog
composables/ useFileList、useFileAction、useDrop、useSelectAll、useToast、useDialog
plugins/ theme.client.ts(主题初始化)
utils/ 浏览器端工具(文件选择、拖拽收集、下载、错误提示、防抖 ref、相对时间)
assets/css/index.less 样式
server/ 服务端(留在根目录)
api/ 文件路由即接口,无需手动注册
list.post.ts 目录列表 / 搜索(stat 并发、目录优先排序)
create.post.ts 新建文件夹
delete.post.ts 删除(幂等,异步 rm)
rename.post.ts 重命名
move.post.ts 移动
upload.post.ts 上传(busboy 流式落盘、中断回滚)
download.get.ts 下载(单文件直推 / 多文件流式 ZIP)
download/preview.get.ts 预览
plugins/00.config.ts 启动时加载配置并打印访问地址
utils/ config / path / fs / format
shared/types/ 前后端共用类型契约(FileItem、ApiResponse…)与第三方包类型声明
scripts/smoke.mjs 接口冒烟测试
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/list |
列表,body: { name, filePath, isDirectory } |
| POST | /api/create |
新建文件夹,body: { filePath, name } |
| POST | /api/delete |
删除,body: { filePaths },幂等 |
| POST | /api/rename |
重命名,body: { filePath, newName } |
| POST | /api/move |
移动,body: { filePaths, newFolder } |
| POST | /api/upload?filePath=a/b |
上传,multipart/form-data,字段名 files |
| POST | /api/upload/init |
分片上传初始化,body: { fileName, totalChunks, chunkSize, fileSize } |
| POST | /api/upload/chunk |
上传单个分片,multipart:uploadId / index / chunk |
| POST | /api/upload/merge |
合并分片,body: { uploadId, filePath, fileName, totalChunks } |
| POST | /api/upload/cancel |
取消并清理临时分片,body: { uploadId },幂等 |
| GET | /api/download?filePaths=["a.txt"] |
下载,单文件直推,多个打包为 ZIP |
| GET | /api/download/preview?filePath=a.txt |
预览,Content-Disposition: inline |
所有接口的路径参数都必须经过 resolveSafe() 校验,越界一律返回 403。
服务端与前端全量使用 TypeScript(strict 已开启):
#shared/types是前后端唯一的数据契约(FileItem/ApiResponse/ListResult等), Nuxt 4 的shared/目录对 app 与 server 双向可见- 组件统一
<script setup lang="ts">,props / emits / model 均使用泛型声明 start.mjs与scripts/*.mjs是纯 Node 脚本,保持.mjs(无需引入 tsx 即可直接node运行)archiverv8 的ZipArchive具名导出缺类型,在shared/types/archiver.d.ts中做最小声明- 文件图标使用
@baybreezy/file-extension-icon(自带类型、零依赖),返回 base64 SVG data URI, 在app/utils/fileIcon.ts收口,无需引入图标字体样式
- 所有路径统一经
resolveSafe()校验,拦截../目录穿越 - 文件名统一校验非法字符;上传文件名剥离路径分隔符
- 下载 / 预览使用 RFC 5987 编码响应头,中文文件名不乱码
- 删除使用异步
rm,删除大目录不会阻塞事件循环 - 列表
stat并发执行,大目录下性能更优 - 上传统一按「目标目录」分组合并请求,多文件不会逐文件发请求
- 上传支持客户端中断回滚(all-or-nothing),断开连接后自动清理半成品
- 批次内同名互斥,同一请求上传多个同名文件不会互相覆盖
- 上传响应返回实际落盘名(重名时带序号)
- 删除接口幂等,目标已不存在时返回成功而非 500
ssr: false(SPA)。这是局域网文件管理器,首屏依赖浏览器 API,关闭 SSR 更简单; 如需 SSR,把nuxt.config.ts的ssr改回true(图标库不依赖 DOM,无需<ClientOnly>)。- 未做单文件打包(如
bun build --compile/pkg),如需可另行添加。
启动服务后运行:
node scripts/smoke.mjs
# 或指定地址
BASE=http://localhost:3000 node scripts/smoke.mjs覆盖目录操作、中文文件名、上传(含批次内重名与批量)、上传中断回滚、下载(单文件/ZIP)、 预览、以及目录穿越与非法文件名等安全校验,共 38 项。
所有测试数据都收敛在
smoke-root/下,运行结束后只删除该目录,可随时安全执行。
部分 IDE 会通过 NODE_OPTIONS 注入把 Node 的删除操作重定向到回收站的 shim。
从这个 IDE 内置终端启动服务时,删除文件可能失败或进入回收站,同时 nuxt build 清理构建缓存会报
[safe-delete] 操作失败。用系统终端正常执行不受影响;若必须在 IDE 终端内构建,可在命令前清除该变量:
env -u NODE_OPTIONS pnpm build