Files
zTools2/README.md
zikai 655e039aad refactor: 白板从画笔画板改为文本记事本
原实现是 Canvas 画笔画板,与「文字白板/记事本」需求不符。重做为纯文本实时协作:
- model:strokes JSON -> content TEXT + version INT(乐观锁)+ edit_count;
  schema/dao/service 同步重构,append_stroke/replace_strokes -> update_content。
- WS 协议:stroke -> edit(发完整文本,debounce 400ms);init 下发 content/version。
  update 帧广播给他人,cleared 广播给所有人。
- 前端:canvas -> textarea;收到远端 update 用最长公共前后缀算变更区间,
  仅替换该区间并保留光标(区间前不动/后平移/内移末尾);清空/复制文本按钮。
- schema.sql 更新 whiteboard 表 DDL;DB 旧表 DROP 重建(开发环境)。
- 测试脚本与 README 同步更新。
2026-07-21 15:00:25 +00:00

13 KiB
Raw Blame History

zikai file service

f.zikai.wang 的 Python Web 服务FastAPI提供主机监控、大文件上传、共享白板与 文件浏览:HTTP整文件 + 分片/断点续传),并内置 SFTP 服务器 用于原始文件暂存。 采用 Spring 风格分层架构(controllers -> services -> dao,外加 modelsschemas),自带自动生成的 API 文档,全部运行在自包含的 .venv 中。

功能

  • GET /api/system/status - CPU、内存、各磁盘使用率via psutil)。
  • POST /api/files/upload - 流式 multipart 上传(内存恒定,支持多 GB落盘时算 SHA-256。
  • GET /upload - 拖拽上传页面:多文件、分片4 MiB断点续传、sha256 去重。
  • POST /api/files/chunk-uploads/* - 支撑 /upload 的分片上传 API建会话 / 查状态 / 传分片 / 完成)。
  • GET /api/filesGET /api/files/{id}GET /api/files/{id}/download
  • 文件浏览页 GET /filesBasic Auth同 docs列出/下载/硬删除已上传文件;删除后不再显示。 管理 APIGET /api/admin/filesGET /api/admin/files/{id}GET /api/admin/files/{id}/downloadDELETE /api/admin/files/{id}(均 Basic Auth
  • 共享记事本(白板) GET /wb/{id}(公开,不存在则新建):纯文本实时协作 + 清空 / 复制文本,兼容移动端。 实时同步走 WS /ws/wb/{id}心跳 3s连续 5 次丢失判失活并移除)。
  • 白板管理页 GET /wb-adminBasic Auth同 docs查看创建时间/编辑次数/上次修改时间/删除。 管理 APIGET /api/admin/wbDELETE /api/admin/wb/{id}(均 Basic Auth
  • 反向隧道反代ALL /api/userPort/{userName} -- 把请求经 SSH 反向隧道转发到该 user 的本机服务。
  • 内置 SFTP/SSH 服务器asyncssh支持 密码 + 公钥 鉴权,同时承载 SFTP 文件暂存与反向隧道。
  • /docsSwagger 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     # 启动 HTTP127.0.0.1:6867+ SFTP0.0.0.0:2022
./stop.sh      # 停止两者

setup.sh 可重复执行。它会创建 .venv、安装 requirements.txt、复制 config.example.yamlconfig.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/wb/{id}(公开{id}[a-zA-Z0-9_-]{1,64},不存在则新建)
白板管理页 https://f.zikai.wang/wb-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.yamldocs: 段。/health/ 保持公开。

Apache/etc/apache2/sites-available/f.zikai.wang-le-ssl.conf)把 f.zikai.wang 反代到 127.0.0.1:6867ProxyPreserveHost On),因此服务只绑 loopback。

大文件/慢速 HTTP 上传: Apache 代理段继承全局 Timeout 300。多 GB 慢链路传输建议走 分片上传/upload 页面或 /api/files/chunk-uploads,单片 4 MiB 在超时内可传完)或 SFTP(完全绕过 HTTP 代理)。要提高 HTTP 上限可在 Apache vhost 加 ProxyTimeout/Timeout

配置

所有运行时配置都在 config.yamlgit-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_dirstorage.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(默认 3heartbeat_miss_threshold(默认 5max_board_id_length(默认 64list_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.yamltunnel.users[] 独立配置(与 sftp.users[] 分开):

  • user 端跑 user/tunnel.py,连 2022 请求 remote port forward 绑 tunnel_port
  • ZikaiSSHServer.server_requested 校验该 user 是否允许绑该端口,记一条 tunnel_session 到 DBuser 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.yamltunnel: 段。生成 bcrypt hash 的方式同 SFTP。

文件浏览页

  • GET /filesBasic Auth同 docs渲染 static/file_browser.htmlJS 调同源管理 API。
  • 管理 API均 Basic Auth
    • GET /api/admin/files?limit=&offset= -> {total, items:[UploadedFileOut]}
    • GET /api/admin/files/{id} -> UploadedFileOut
    • GET /api/admin/files/{id}/download -> 文件流(磁盘缺失返回 410
    • DELETE /api/admin/files/{id} -> 硬删除:删 DB 行 + 删磁盘文件(unlink missing_ok)。
  • 删除后不再显示:列表每次进入或删除后重新 fetch前端不缓存DB 行已删,列表自然不含。
  • 公开 /api/files 系列user.py 依赖的查重/查询/下载)保留不变。

共享记事本(白板)

白板无鉴权,任何人凭 /wb/{id} 即可访问并实时协作;{id} 须匹配 [a-zA-Z0-9_-]{1,64},非法返回 400。访问不存在的 id 自动新建空板。白板长期留存 (存 MySQL whiteboard 表,content TEXT 列),进程重启后内容仍在。

实时同步与心跳

  • 连接:WS /ws/wb/{id}公开。JSON 文本帧协议:
    • client -> server{"type":"hello","client_id":"..."}(首帧,可选)、 {"type":"ping"}(心跳)、{"type":"edit","content":"..."}debounce 后发完整文本)、 {"type":"clear"}
    • server -> client{"type":"init","content":"...","version":n,"edit_count":m}{"type":"pong"}{"type":"update","content":"...","version":n,"client_id":"..."}(广播给他人,不含发送者)、 {"type":"cleared","client_id":"..."}(广播给所有人)、{"type":"error","msg":"..."}
  • 同步策略:客户端本地编辑后 debounce 400ms 发完整文本服务端存为新版本version+1 并广播给同 board 的其他在线连接。其他端用最长公共前后缀算出变更区间,仅替换该区间并 保留本地光标位置(在变更区间前不动,在后平移,在区间内移到末尾)。
  • 心跳:客户端每 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 /wb-adminBasic Auth同 docs渲染 static/whiteboard_admin.html
  • 管理 API均 Basic Auth
    • GET /api/admin/wb?limit=&offset= -> {total, items:[{board_id, edit_count, created_at, updated_at}]}
    • DELETE /api/admin/wb/{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.logSFTP 日志 → logs/sftp.log
  • pidfileapp.pidsftp.pidstop.sh 使用)。