feat: 文件浏览页 + 共享白板 + 白板管理页(含补登记分片上传/隧道历史改动)
本次提交包含两批改动(7月2日遗留未入库 + 本次新功能),分述如下:
【补登记:7月2日已上线但未提交的功能】
- 分片上传:chunk_upload_controller/service/dao + UploadSession model/schema,
支持 4MiB 分片、乱序、断点续传、去重、幂等 complete;后台 reaper 清理过期会话。
- 反向隧道:tunnel_controller/service/dao + TunnelSession model/schema,
SSH remote forward 经 /api/userPort/{userName} 反代到 user 本地服务。
- 上传页:views/upload_html.py(拖拽/多文件/分片/断点续传 UI)。
- config.py:StorageConfig.chunk_session_dir/ttl、TunnelConfig;
requirements.txt 加 httpx;start.sh 清理 .work/ 残留;
schema.sql 加 upload_session/tunnel_session 表;sftp_server 承载隧道转发。
【本次新功能】
- 文件浏览页:GET /files(Basic Auth 同 docs)+ /api/admin/files(list/get/download/DELETE)。
硬删除(DB 行 + 磁盘文件),删除后列表不再显示。前端 static/file_browser.*。
- 共享白板:GET /whiteboard/{id}(公开,不存在则新建)+ WS /ws/whiteboard/{id}。
MySQL 持久化(whiteboard 表),Canvas 实时同步,心跳 3s/5 次失活移除,
清空/复制按钮,移动端兼容。WhiteboardHub 管理 {board_id: set[Connection]},
disconnect 幂等 + 空 set 清理防泄漏,broadcast 失败连接自动移除。
- 白板管理页:GET /whiteboard-admin(Basic Auth)+ /api/admin/whiteboards(list/DELETE)。
删除时 hub.close_board 踢出在线连接。
- 清理:合并 UploadService.get_out_with_disk_path(下载/删除复用,消除重复 DB 读),
移除无用 resolve_disk_path。
- config.py:WhiteboardConfig(heartbeat/threshold/board_id 长度/list_limit);
schema.sql 加 whiteboard 表;README 补新接口与心跳/内存说明。
- 验证:tests/manual_whiteboard_hub.py / _ws.py / _kick.py 全部通过。
This commit is contained in:
244
README.md
244
README.md
@@ -1,94 +1,108 @@
|
||||
# zikai file service
|
||||
|
||||
A Python web service (FastAPI) for **f.zikai.wang** that provides host
|
||||
monitoring and large-file upload over **both HTTP and SFTP**. It follows a
|
||||
Spring-style layered architecture (`controllers` → `services` → `dao`, plus
|
||||
`models` and `schemas`), ships with auto-generated API docs, and runs entirely
|
||||
from a self-contained `.venv`.
|
||||
`f.zikai.wang` 的 Python Web 服务(FastAPI),提供主机监控、大文件上传、共享白板与
|
||||
文件浏览:**HTTP(整文件 + 分片/断点续传)**,并内置 **SFTP 服务器** 用于原始文件暂存。
|
||||
采用 Spring 风格分层架构(`controllers` -> `services` -> `dao`,外加 `models` 与
|
||||
`schemas`),自带自动生成的 API 文档,全部运行在自包含的 `.venv` 中。
|
||||
|
||||
## Features
|
||||
## 功能
|
||||
|
||||
- `GET /api/system/status` — CPU, memory, and per-disk usage (via `psutil`).
|
||||
- `POST /api/files/upload` — **streamed** multipart upload (flat memory, multi-GB friendly), with SHA-256.
|
||||
- `GET /api/files`, `GET /api/files/{id}`, `GET /api/files/{id}/download`.
|
||||
- **Embedded SFTP server** (asyncssh) supporting **password + public-key** auth, sharing the same storage as HTTP.
|
||||
- `/docs` (Swagger UI) and `/redoc` — interactive, auto-lists all APIs.
|
||||
- Metadata persisted in an **independent MySQL database** (`zikai_filesvc`).
|
||||
- `start.sh` / `stop.sh` lifecycle; `setup.sh` for one-time provisioning.
|
||||
- `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 /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` 一次性初始化。
|
||||
|
||||
## Architecture (Spring-style layers)
|
||||
## 架构(Spring 风格分层)
|
||||
|
||||
```
|
||||
app/
|
||||
├── controllers/ # FastAPI routers — HTTP boundary (like @RestController)
|
||||
├── services/ # business logic (SystemService, UploadService, SFTP server)
|
||||
├── dao/ # data access objects — the only layer that issues SQL/ORM
|
||||
├── models/ # SQLAlchemy ORM entities
|
||||
├── schemas/ # pydantic DTOs (request/response validation)
|
||||
├── database.py # engine, session, Base, get_db() dependency
|
||||
├── config.py # typed Settings loaded from config.yaml
|
||||
└── scripts/ # init_db.py — DB provisioning
|
||||
├── 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 -- 数据库初始化
|
||||
```
|
||||
|
||||
Request flow: **controller** → **service** → **dao** → **ORM model** → MySQL.
|
||||
The DB session is injected by FastAPI's `get_db` dependency and passed down.
|
||||
请求流程:**controller** -> **service** -> **dao** -> **ORM model** -> MySQL。
|
||||
DB Session 由 FastAPI 的 `get_db` 依赖注入并向下传递。前端三套页面走「独立静态文件 +
|
||||
StaticFiles 挂载」的前后端分离模式,HTML 壳由具名路由返回(便于各自挂 Basic Auth),
|
||||
JS 调用同源 `/api/...`。
|
||||
|
||||
## Quick start
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
cd /root/zikai
|
||||
./setup.sh # one-time: venv, deps, provision DB + user, SFTP host key
|
||||
./start.sh # start HTTP (127.0.0.1:6867) + SFTP (0.0.0.0:2022)
|
||||
./stop.sh # stop both
|
||||
./setup.sh # 一次性:venv、依赖、建库建账、SFTP 主机密钥
|
||||
./start.sh # 启动 HTTP(127.0.0.1:6867)+ SFTP(0.0.0.0:2022)
|
||||
./stop.sh # 停止两者
|
||||
```
|
||||
|
||||
`setup.sh` is re-runnable. It creates the `.venv`, installs `requirements.txt`,
|
||||
copies `config.example.yaml` → `config.yaml` (if absent), provisions a **new
|
||||
independent MySQL database and app user** via the local root socket, and
|
||||
generates the SFTP host key.
|
||||
`setup.sh` 可重复执行。它会创建 `.venv`、安装 `requirements.txt`、复制
|
||||
`config.example.yaml` → `config.yaml`(若不存在)、通过本机 root socket 建一个
|
||||
**全新的独立 MySQL 数据库与应用账户**,并生成 SFTP 主机密钥。
|
||||
|
||||
## Access
|
||||
## 访问方式
|
||||
|
||||
| Where | URL |
|
||||
| 入口 | URL |
|
||||
|------|-----|
|
||||
| Status page (HTML) | https://f.zikai.wang/api/system/status |
|
||||
| Status page (JSON) | https://f.zikai.wang/api/system/status?format=json (or `Accept: application/json`) |
|
||||
| API docs (Swagger) | https://f.zikai.wang/docs **(HTTP Basic Auth — see `docs:` in config.yaml)** |
|
||||
| API docs (ReDoc) | https://f.zikai.wang/redoc (same auth) |
|
||||
| Upload | `curl -F file=@big.iso https://f.zikai.wang/api/files/upload` |
|
||||
| SFTP | `sftp -P 2022 uploader@f.zikai.wang` |
|
||||
| 状态页(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` content-negotiates: browsers (`Accept: text/html`) get a
|
||||
human-readable page with progress bars; API clients get JSON. Force one with
|
||||
`?format=html` or `?format=json`.
|
||||
`/api/system/status` 做内容协商:浏览器(`Accept: text/html`)拿到带进度条的可读页面;
|
||||
API 客户端拿到 JSON。可用 `?format=html` 或 `?format=json` 强制指定。
|
||||
|
||||
`/docs`, `/redoc`, and `/openapi.json` require HTTP Basic Auth — the browser
|
||||
will prompt you. Username + plaintext password live in `config.yaml` under
|
||||
`docs:`. `/health` and `/` remain public.
|
||||
`/docs`、`/redoc`、`/openapi.json` 需要 HTTP Basic Auth —— 浏览器会弹出登录框。用户名与
|
||||
明文密码写在 `config.yaml` 的 `docs:` 段。`/health` 与 `/` 保持公开。
|
||||
|
||||
Apache (`/etc/apache2/sites-available/f.zikai.wang-le-ssl.conf`) proxies
|
||||
`f.zikai.wang` → `127.0.0.1:6867` with `ProxyPreserveHost On`, so the service
|
||||
only binds the loopback.
|
||||
Apache(`/etc/apache2/sites-available/f.zikai.wang-le-ssl.conf`)把 `f.zikai.wang` 反代到
|
||||
`127.0.0.1:6867`(`ProxyPreserveHost On`),因此服务只绑 loopback。
|
||||
|
||||
> **Large/slow HTTP uploads:** Apache's proxy leg inherits the global `Timeout 300`.
|
||||
> For multi-GB transfers over a slow link, prefer **SFTP** (it bypasses the HTTP
|
||||
> proxy entirely). To raise the HTTP ceiling you can add `ProxyTimeout`/`Timeout`
|
||||
> in the Apache vhost.
|
||||
> **大文件/慢速 HTTP 上传:** Apache 代理段继承全局 `Timeout 300`。多 GB 慢链路传输建议走
|
||||
> **分片上传**(`/upload` 页面或 `/api/files/chunk-uploads`,单片 4 MiB 在超时内可传完)或
|
||||
> **SFTP**(完全绕过 HTTP 代理)。要提高 HTTP 上限可在 Apache vhost 加 `ProxyTimeout`/`Timeout`。
|
||||
|
||||
## Configuration
|
||||
## 配置
|
||||
|
||||
All runtime config lives in **`config.yaml`** (git-ignored). See
|
||||
`config.example.yaml` for the full schema. Notable keys:
|
||||
所有运行时配置都在 **`config.yaml`**(git-ignored)。完整 schema 见 `config.example.yaml`。
|
||||
关键配置项:
|
||||
|
||||
- `server` — bind host/port (keep `127.0.0.1:6867` to match Apache).
|
||||
- `database` — host/port/user/password/database. The password is auto-generated
|
||||
and written here by `setup.sh`/`init_db.py`.
|
||||
- `storage.upload_dir`, `storage.chunk_bytes` (default 1 MiB streaming chunk).
|
||||
- `sftp` — enabled, host/port, host key + authorized_keys paths, and `users`.
|
||||
- `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)。
|
||||
|
||||
### Setting the /docs admin password
|
||||
### 设置 /docs 管理密码
|
||||
|
||||
Edit `config.yaml` directly — no hashing required:
|
||||
直接编辑 `config.yaml`,无需哈希:
|
||||
|
||||
```yaml
|
||||
docs:
|
||||
@@ -98,38 +112,102 @@ docs:
|
||||
realm: "zikai docs"
|
||||
```
|
||||
|
||||
Then `./stop.sh && ./start.sh`. The file is root-owned and stays on this
|
||||
host; comparison is constant-time (`secrets.compare_digest`).
|
||||
然后 `./stop.sh && ./start.sh`。该文件 root 持有且仅在本机;比较使用常量时间
|
||||
(`secrets.compare_digest`)。
|
||||
|
||||
### Setting SFTP credentials
|
||||
### 设置 SFTP 凭据
|
||||
|
||||
**Password auth** — generate a bcrypt hash and put it in `config.yaml`:
|
||||
**密码鉴权** —— 生成 bcrypt hash 写入 `config.yaml`:
|
||||
|
||||
```bash
|
||||
.venv/bin/python -c "import bcrypt;print(bcrypt.hashpw(b'yourpass',bcrypt.gensalt()).decode())"
|
||||
# paste the output into sftp.users[].password_hash, then ./stop.sh && ./start.sh
|
||||
# 输出粘贴到 sftp.users[].password_hash,然后 ./stop.sh && ./start.sh
|
||||
```
|
||||
|
||||
**Public-key auth** — append each client's public key (OpenSSH format) to
|
||||
`keys/authorized_keys` (one per line). Clients in `sftp.users[]` may then log
|
||||
in with either method.
|
||||
**公钥鉴权** —— 把每个客户端的公钥(OpenSSH 格式)追加到 `keys/authorized_keys`(每行一个)。
|
||||
`sftp.users[]` 中的用户随后可用任一方式登录。
|
||||
|
||||
### Regenerating the DB password
|
||||
### 重新生成数据库密码
|
||||
|
||||
```bash
|
||||
.venv/bin/python -m app.scripts.init_db # new random password
|
||||
KEEP_DB_PASSWORD=1 .venv/bin/python -m app.scripts.init_db # keep current
|
||||
.venv/bin/python -m app.scripts.init_db # 生成新随机密码
|
||||
KEEP_DB_PASSWORD=1 .venv/bin/python -m app.scripts.init_db # 保留现有密码
|
||||
```
|
||||
|
||||
## SFTP notes
|
||||
## SFTP 说明
|
||||
|
||||
- The SFTP server can't traverse Apache's HTTP proxy, so it binds `0.0.0.0:2022`
|
||||
directly. **Open port 2022** in your firewall for external clients
|
||||
(FileZilla/WinSCP/scp).
|
||||
- Sessions are chrooted to the upload root (`uploads/`), shared with HTTP.
|
||||
- Only users listed in `sftp.users` may connect; only SFTP (no shell/exec) is allowed.
|
||||
- SFTP 服务无法穿透 Apache 的 HTTP 代理,因此直接绑 `0.0.0.0:2022`。**请在防火墙放开
|
||||
2022 端口** 供外部客户端(FileZilla/WinSCP/scp)连接。
|
||||
- 会话 chroot 到上传根目录(`uploads/`),与 HTTP 共用存储。
|
||||
- 仅 `sftp.users` 中列出的用户可连接;只允许 SFTP(无 shell/exec)。
|
||||
- SFTP 服务器作为文件暂存通道;不再提供 HTTP 登记接口。
|
||||
|
||||
## Logs & pidfiles
|
||||
## 反向隧道
|
||||
|
||||
- HTTP logs → `logs/app.log`; SFTP logs → `logs/sftp.log`.
|
||||
- Pidfiles: `app.pid`, `sftp.pid` (used by `stop.sh`).
|
||||
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 依赖的查重/查询/下载)保留不变。
|
||||
|
||||
## 共享白板
|
||||
|
||||
白板无鉴权,任何人凭 `/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":"..."}`
|
||||
- **心跳**:客户端每 `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` 使用)。
|
||||
|
||||
Reference in New Issue
Block a user