- 后端: - UploadService.delete_file 下沉删除逻辑(DB 行 + 磁盘文件),单删/批删复用。 - POST /api/admin/files/batch-delete:批量硬删除,返回 deleted 数与 not_found 列表。 - list 接口加 Query 约束(limit 1-10000、offset>=0),非法返回 422。 - 前端(file_browser): - 复选框列 + 全选/反选,选中行高亮,批量操作栏(已选计数/下载/删除/取消)。 - 分页控件:每页 20/50/100/全部 切换,页码导航(首页/末页/当前±1/省略号)。 - 批量下载逐个触发(间隔 150ms 防浏览器拦截);批量删除走 batch-delete 接口。 - 删除后重载当前页,当前页空则回退一页。
zikai file service
f.zikai.wang 的 Python Web 服务(FastAPI),提供主机监控、大文件上传、共享白板与
文件浏览:HTTP(整文件 + 分片/断点续传),并内置 SFTP 服务器 用于原始文件暂存。
采用 Spring 风格分层架构(controllers -> services -> dao,外加 models 与
schemas),自带自动生成的 API 文档,全部运行在自包含的 .venv 中。
功能
GET /api/system/status- CPU、内存、各磁盘使用率(viapsutil)。POST /api/files/upload- 流式 multipart 上传(内存恒定,支持多 GB),落盘时算 SHA-256。GET /upload- 拖拽上传页面:多文件、分片(4 MiB)、断点续传、sha256 去重。POST /api/files/chunk-uploads/*- 支撑/upload的分片上传 API(建会话 / 查状态 / 传分片 / 完成)。GET /api/files、GET /api/files/{id}、GET /api/files/{id}/download。- 文件浏览页
GET /files(Basic Auth,同 docs):列出/下载/硬删除已上传文件;删除后不再显示。 管理 API:GET /api/admin/files、GET /api/admin/files/{id}、GET /api/admin/files/{id}/download、DELETE /api/admin/files/{id}(均 Basic Auth)。 - 共享白板
GET /whiteboard/{id}(公开,不存在则新建):Canvas 实时协作 + 清空 / 复制链接,兼容移动端。 实时同步走WS /ws/whiteboard/{id}(心跳 3s,连续 5 次丢失判失活并移除)。 - 白板管理页
GET /whiteboard-admin(Basic Auth,同 docs):查看创建时间/修改次数/上次修改时间/删除。 管理 API:GET /api/admin/whiteboards、DELETE /api/admin/whiteboards/{id}(均 Basic Auth)。 - 反向隧道反代:
ALL /api/userPort/{userName}-- 把请求经 SSH 反向隧道转发到该 user 的本机服务。 - 内置 SFTP/SSH 服务器(asyncssh),支持 密码 + 公钥 鉴权,同时承载 SFTP 文件暂存与反向隧道。
/docs(Swagger UI)与/redoc- 交互式文档,自动列出所有 API。- 元数据持久化在 独立的 MySQL 数据库(
zikai_filesvc)。 start.sh/stop.sh生命周期管理;setup.sh一次性初始化。
架构(Spring 风格分层)
app/
├── controllers/ # FastAPI 路由 -- HTTP 边界(类似 @RestController)
├── services/ # 业务逻辑(SystemService, UploadService, ChunkUploadService,
│ # WhiteboardService, WhiteboardHub, SFTP 服务)
├── dao/ # 数据访问对象 -- 唯一发出 SQL/ORM 的层
├── models/ # SQLAlchemy ORM 实体(UploadedFile, UploadSession, Whiteboard, ...)
├── schemas/ # pydantic DTO(请求/响应校验)
├── views/ # 服务端渲染的 HTML 页面(系统状态、上传页)
├── static/ # 前端静态资源(文件浏览/白板/白板管理的 HTML+JS+CSS,经 StaticFiles 挂载)
├── database.py # 引擎、Session、Base、get_db() 依赖
├── config.py # 从 config.yaml 加载的类型化 Settings
└── scripts/ # init_db.py -- 数据库初始化
请求流程:controller -> service -> dao -> ORM model -> MySQL。
DB Session 由 FastAPI 的 get_db 依赖注入并向下传递。前端三套页面走「独立静态文件 +
StaticFiles 挂载」的前后端分离模式,HTML 壳由具名路由返回(便于各自挂 Basic Auth),
JS 调用同源 /api/...。
快速开始
cd /root/zikai
./setup.sh # 一次性:venv、依赖、建库建账、SFTP 主机密钥
./start.sh # 启动 HTTP(127.0.0.1:6867)+ SFTP(0.0.0.0:2022)
./stop.sh # 停止两者
setup.sh 可重复执行。它会创建 .venv、安装 requirements.txt、复制
config.example.yaml → config.yaml(若不存在)、通过本机 root socket 建一个
全新的独立 MySQL 数据库与应用账户,并生成 SFTP 主机密钥。
访问方式
| 入口 | URL |
|---|---|
| 状态页(HTML) | https://f.zikai.wang/api/system/status |
| 状态页(JSON) | https://f.zikai.wang/api/system/status?format=json(或 Accept: application/json) |
| API 文档(Swagger) | https://f.zikai.wang/docs (HTTP Basic Auth -- 见 config.yaml 的 docs: 段) |
| API 文档(ReDoc) | https://f.zikai.wang/redoc(同样鉴权) |
| 上传页(拖拽、分片、断点续传) | https://f.zikai.wang/upload |
| 文件浏览页(列出/下载/删除) | https://f.zikai.wang/files (Basic Auth,同 docs) |
| 共享白板(实时协作) | https://f.zikai.wang/whiteboard/{id}(公开,{id} 为 [a-zA-Z0-9_-]{1,64},不存在则新建) |
| 白板管理页 | https://f.zikai.wang/whiteboard-admin (Basic Auth,同 docs) |
| 上传(curl) | curl -F file=@big.iso https://f.zikai.wang/api/files/upload |
| SFTP | sftp -P 2022 uploader@f.zikai.wang |
/api/system/status 做内容协商:浏览器(Accept: text/html)拿到带进度条的可读页面;
API 客户端拿到 JSON。可用 ?format=html 或 ?format=json 强制指定。
/docs、/redoc、/openapi.json 需要 HTTP Basic Auth —— 浏览器会弹出登录框。用户名与
明文密码写在 config.yaml 的 docs: 段。/health 与 / 保持公开。
Apache(/etc/apache2/sites-available/f.zikai.wang-le-ssl.conf)把 f.zikai.wang 反代到
127.0.0.1:6867(ProxyPreserveHost On),因此服务只绑 loopback。
大文件/慢速 HTTP 上传: Apache 代理段继承全局
Timeout 300。多 GB 慢链路传输建议走 分片上传(/upload页面或/api/files/chunk-uploads,单片 4 MiB 在超时内可传完)或 SFTP(完全绕过 HTTP 代理)。要提高 HTTP 上限可在 Apache vhost 加ProxyTimeout/Timeout。
配置
所有运行时配置都在 config.yaml(git-ignored)。完整 schema 见 config.example.yaml。
关键配置项:
server— 绑定 host/port(保持127.0.0.1:6867以对齐 Apache)。database— host/port/user/password/database。密码由setup.sh/init_db.py自动生成并写回。storage.upload_dir、storage.chunk_bytes(默认 1 MiB 流式分片)、storage.chunk_session_dir(分片会话暂存目录)、storage.chunk_session_ttl_seconds(被放弃会话的存活秒数,默认 300)。sftp— enabled、host/port、host key + authorized_keys 路径、users。whiteboard-heartbeat_interval_seconds(默认 3)、heartbeat_miss_threshold(默认 5)、max_board_id_length(默认 64)、list_limit(默认 100)。
设置 /docs 管理密码
直接编辑 config.yaml,无需哈希:
docs:
enabled: true
username: admin
password: "your-plaintext-password"
realm: "zikai docs"
然后 ./stop.sh && ./start.sh。该文件 root 持有且仅在本机;比较使用常量时间
(secrets.compare_digest)。
设置 SFTP 凭据
密码鉴权 —— 生成 bcrypt hash 写入 config.yaml:
.venv/bin/python -c "import bcrypt;print(bcrypt.hashpw(b'yourpass',bcrypt.gensalt()).decode())"
# 输出粘贴到 sftp.users[].password_hash,然后 ./stop.sh && ./start.sh
公钥鉴权 —— 把每个客户端的公钥(OpenSSH 格式)追加到 keys/authorized_keys(每行一个)。
sftp.users[] 中的用户随后可用任一方式登录。
重新生成数据库密码
.venv/bin/python -m app.scripts.init_db # 生成新随机密码
KEEP_DB_PASSWORD=1 .venv/bin/python -m app.scripts.init_db # 保留现有密码
SFTP 说明
- SFTP 服务无法穿透 Apache 的 HTTP 代理,因此直接绑
0.0.0.0:2022。请在防火墙放开 2022 端口 供外部客户端(FileZilla/WinSCP/scp)连接。 - 会话 chroot 到上传根目录(
uploads/),与 HTTP 共用存储。 - 仅
sftp.users中列出的用户可连接;只允许 SFTP(无 shell/exec)。 - SFTP 服务器作为文件暂存通道;不再提供 HTTP 登记接口。
反向隧道
SSH 服务器(2022)同时承载 SFTP 文件暂存与反向隧道。隧道 user 在 config.yaml 的
tunnel.users[] 独立配置(与 sftp.users[] 分开):
- user 端跑
user/tunnel.py,连 2022 请求 remote port forward 绑tunnel_port。 ZikaiSSHServer.server_requested校验该 user 是否允许绑该端口,记一条tunnel_session到 DB(user IP、local_port、tunnel_port、起止时间)。GET /api/userPort/{userName}查 DB 该 user 活跃隧道的端口,反代到127.0.0.1:tunnel_port(经隧道回指 user 本地服务)。无活跃隧道返回 502。- user 断开时
connection_lost回调关闭 DB 会话(记ended_at);另有启动 reaper 兜底 清理进程异常重启后的孤儿记录。
配置示例见 config.example.yaml 的 tunnel: 段。生成 bcrypt hash 的方式同 SFTP。
文件浏览页
GET /files(Basic Auth,同 docs)渲染static/file_browser.html,JS 调同源管理 API。- 管理 API(均 Basic Auth):
GET /api/admin/files?limit=&offset=->{total, items:[UploadedFileOut]}GET /api/admin/files/{id}->UploadedFileOutGET /api/admin/files/{id}/download-> 文件流(磁盘缺失返回 410)DELETE /api/admin/files/{id}-> 硬删除:删 DB 行 + 删磁盘文件(unlink missing_ok)。
- 删除后不再显示:列表每次进入或删除后重新 fetch,前端不缓存;DB 行已删,列表自然不含。
- 公开
/api/files系列(user.py 依赖的查重/查询/下载)保留不变。
共享白板
白板无鉴权,任何人凭 /whiteboard/{id} 即可访问并实时协作;{id} 须匹配
[a-zA-Z0-9_-]{1,64},非法返回 400。访问不存在的 id 自动新建空板。白板长期留存
(存 MySQL whiteboard 表),进程重启后内容仍在。
实时同步与心跳
- 连接:
WS /ws/whiteboard/{id}(公开)。JSON 文本帧协议:- client -> server:
{"type":"hello","client_id":"..."}(首帧,可选)、{"type":"ping"}(心跳)、{"type":"stroke","stroke":{points,color,width}}、{"type":"clear"} - server -> client:
{"type":"init","strokes":[...],"stroke_count":n}、{"type":"pong"}、{"type":"stroke","stroke":{...},"client_id":"..."}(广播给他人,不含发送者)、{"type":"cleared","client_id":"..."}(广播给所有人)、{"type":"error","msg":"..."}
- client -> server:
- 心跳:客户端每
whiteboard.heartbeat_interval_seconds(默认 3s)发一次ping,服务端回pong并刷新计时。后台 reaper 每秒扫描,连续heartbeat_miss_threshold(默认 5)次未收到心跳 (即 15s)判失活,关闭该连接并从 hub 移除。 - 内存安全:
WhiteboardHub维护{board_id: set[Connection]}:disconnect幂等,空 set 从 dict 删除(防 board 键无限增长);- WS 主循环
try/finally必调disconnect,异常/断连均清理; broadcast对单连接发送失败立即disconnect,不影响其他连接;- 删除白板时
close_board关闭并清理该 board 的全部连接。
- 多 worker 限制:hub 是进程内存,多 uvicorn worker 下不同进程的连接不互通。生产部署需
保持
server.workers: 1,或后续接 Redis pub/sub 跨进程广播。
白板管理
GET /whiteboard-admin(Basic Auth,同 docs)渲染static/whiteboard_admin.html。- 管理 API(均 Basic Auth):
GET /api/admin/whiteboards?limit=&offset=->{total, items:[{board_id, stroke_count, created_at, updated_at}]}DELETE /api/admin/whiteboards/{id}-> 删 DB 行 + 关闭该 board 所有在线 WS 连接。
临时文件清理
complete成功(含去重命中)后,会话目录uploads/.work/<upload_id>/立即删除。- 被放弃的上传(
pending状态且超过chunk_session_ttl_seconds无活动,默认 5 分钟)由 后台 reaper 清理:每 60 秒扫一次,删.work/<upload_id>/目录 + DB 会话行。 start.sh启动时仍会兜底清掉残留的.work/与*.part(进程异常退出时的半成品)。
日志与 pidfile
- HTTP 日志 →
logs/app.log;SFTP 日志 →logs/sftp.log。 - pidfile:
app.pid、sftp.pid(stop.sh使用)。