Skip to content
JaxBBLLPublic

About

a simple cloud disk

Resources

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

disk

一个简单的局域网云盘,用于局域网内传输文件。

基于 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      # 接口冒烟测试(需先启动服务)

配置文件 config.json

首次启动会自动创建,位于运行目录下:

{
  "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 startup

dist/ 内的 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 后再运行。

Docker

镜像采用多阶段构建:构建阶段装依赖并执行 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
}
EOF
docker 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:latest

目录结构(Nuxt 4)

app/                    前端源码(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

服务端与前端全量使用 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 运行)
  • archiver v8 的 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

About

a simple cloud disk

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages