# zikai file service `f.zikai.wang` 的 Python Web 服务(FastAPI),提供主机监控、大文件上传、共享白板与 文件浏览:**HTTP(整文件 + 分片/断点续传)**,并内置 **SFTP 服务器** 用于原始文件暂存。 采用 Spring 风格分层架构(`controllers` -> `services` -> `dao`,外加 `models` 与 `schemas`),自带自动生成的 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/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 /wb/{id}`(公开,不存在则新建):纯文本实时协作 + **清空 / 复制文本**,兼容移动端。 实时同步走 `WS /ws/wb/{id}`(**心跳 3s,连续 5 次丢失判失活并移除**)。 - **白板管理页** `GET /wb-admin`(Basic Auth,同 docs):查看创建时间/编辑次数/上次修改时间/删除。 管理 API:`GET /api/admin/wb`、`DELETE /api/admin/wb/{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/...`。 ## 快速开始 ```bash 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/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.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。 > **WebSocket 反代**:记事本的实时同步走 `WS /ws/wb/{id}`,经 Apache 反代时必须用 > `proxy_wstunnel` 模块单独透传 `/ws/` 路径,否则升级请求被当普通 GET 返回 404、前端反复 > 「连接已关闭,重连中」。vhost 须在通用 `/` 规则**之前**加: > ```apache > ProxyPass /ws/ ws://127.0.0.1:6867/ws/ > ProxyPassReverse /ws/ ws://127.0.0.1:6867/ws/ > ``` > (外层 HTTPS 由 Apache 终结,Apache 到 uvicorn 之间是明文 `ws://`。) > 需启用 `proxy_wstunnel` 模块:`a2enmod proxy_wstunnel && systemctl reload apache2`。 > **大文件/慢速 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`,无需哈希: ```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`: ```bash .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[]` 中的用户随后可用任一方式登录。 ### 重新生成数据库密码 ```bash .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}` -> `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-admin`(Basic 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//` 立即删除。 - 被放弃的上传(`pending` 状态且超过 `chunk_session_ttl_seconds` 无活动,默认 5 分钟)由 **后台 reaper** 清理:每 60 秒扫一次,删 `.work//` 目录 + DB 会话行。 - `start.sh` 启动时仍会兜底清掉残留的 `.work/` 与 `*.part`(进程异常退出时的半成品)。 ## 日志与 pidfile - HTTP 日志 → `logs/app.log`;SFTP 日志 → `logs/sftp.log`。 - pidfile:`app.pid`、`sftp.pid`(`stop.sh` 使用)。