docs(deploy): document generated file retention

This commit is contained in:
Xiaoxia AI
2026-06-21 11:29:33 +08:00
parent ccb7e1e57a
commit bc92400eff
7 changed files with 100 additions and 14 deletions
+3 -2
View File
@@ -117,8 +117,9 @@
|--------|------|------|------|
| POST | `/` | 上传文件 | ✅ ACTIVE |
**Use Cases**:
- 直接调用 `MinIOService`
**Use Cases / Services**:
- `SubmitIngestJobUseCase`
- `OSSStorageService`(经 `get_storage_service` 注入)
---
+1 -1
View File
@@ -259,7 +259,7 @@ apps/web/src/
```
apps/api/app/api/routes/upload.py (API)
apps/api/app/core/storage.py (MinIO)
apps/api/app/core/storage.py (OSS / generated-files fallback)
packages/domain/entities.py::Asset (领域模型)
+6 -7
View File
@@ -99,10 +99,9 @@ uvicorn apps.api.main:app --reload
\q
```
3. **运行迁移**
3. **运行 Alembic 迁移**
```bash
psql postgresql://xiaoxia_user:password@localhost:5432/xiaoxia_saas \
-f migrations/001_initial_schema.sql
alembic upgrade head
```
4. **配置环境变量**
@@ -120,17 +119,17 @@ uvicorn apps.api.main:app --reload
## 🐳 Docker 环境
### docker-compose(自动配置 PostgreSQL
### Canonical Docker Compose(自动配置 PostgreSQL
```bash
# 自动启动 PostgreSQL 和应用
docker-compose up -d
WEB_PORT=3001 docker compose -f infra/docker/compose.yml up -d --build
# 查看日志
docker-compose logs -f api
docker compose -f infra/docker/compose.yml logs -f api
```
`docker-compose.yml` 默认使用 PostgreSQL
`infra/docker/compose.yml` 是当前唯一有效 compose 入口;根目录 `docker-compose.yml` 已退役
---
+10
View File
@@ -78,6 +78,16 @@ Staging 当前可以保持 no-opProduction 开启前必须先验证 SMTP/Redi
---
## 生成文件存储与保留
- Staging 未配置 OSS 凭证时,生成视频落盘到 `/var/lib/xiaoxia-saas-staging/generated`,并通过 Nginx `/generated-files/` 公开访问。
- Production 优先使用 OSS;若临时启用本地 fallback,必须配置独立持久化目录、Nginx 只读公开路径和磁盘告警。
- 保留策略建议:staging 生成文件保留 7 天或保留最近 20GB;production 按业务套餐/订单状态定义,禁止无上限增长。
- 清理脚本上线前必须先 dry-run 输出待删列表,再按 workspace/project 维度删除,避免误删仍被 GeneratedVideo 记录引用的文件。
- 当前脚本:`python scripts/cleanup_generated_files.py --dir /var/lib/xiaoxia-saas-staging/generated --days 7` 仅 dry-run;确认后再加 `--apply`
---
## 生产发布清单
生产发布前必须按 `docs/PRODUCTION-RELEASE-CHECKLIST.md` 执行:先备份,后 Alembic,最后 smoke;禁止靠临场记忆操作数据库。
+2
View File
@@ -1,5 +1,7 @@
# 小虾 SaaS Docker 部署指南
> ⚠️ 历史文档,仅供归档参考。当前 canonical 部署入口是 `infra/docker/compose.yml` 和 `infra/docker/deploy-staging.sh`;生产发布必须按 `docs/PRODUCTION-RELEASE-CHECKLIST.md` 执行。
## 🐳 快速启动
### 1. 准备环境变量
+3 -4
View File
@@ -211,14 +211,13 @@ redis-cli -u $REDIS_URL ping
python scripts/test_email.py
# 6. 检查 Docker 镜像
docker build -t xiaoxia-saas:latest .
docker run --rm xiaoxia-saas:latest python -c "import apps.api.main"
WEB_PORT=3001 docker compose -f infra/docker/compose.yml build api worker web
# 7. 检查数据库迁移
psql $DATABASE_URL -f migrations/001_initial_schema.sql
alembic upgrade head
# 8. 启动服务测试
docker-compose up -d
WEB_PORT=3001 docker compose -f infra/docker/compose.yml up -d
curl http://localhost:8000/health
```
+75
View File
@@ -0,0 +1,75 @@
"""Clean local generated-files storage with a dry-run-first workflow."""
from __future__ import annotations
import argparse
import os
import time
from pathlib import Path
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--dir",
default=os.getenv("GENERATED_FILES_DIR", "/app/generated"),
help="Generated files root directory. Defaults to GENERATED_FILES_DIR or /app/generated.",
)
parser.add_argument(
"--days",
type=int,
default=7,
help="Delete files older than this many days. Defaults to 7.",
)
parser.add_argument(
"--apply",
action="store_true",
help="Actually delete files. Without this flag the script only prints a dry run.",
)
return parser.parse_args()
def iter_expired_files(root: Path, cutoff_epoch: float):
for path in root.rglob("*"):
if path.is_file() and path.stat().st_mtime < cutoff_epoch:
yield path
def remove_empty_dirs(root: Path) -> None:
for path in sorted((p for p in root.rglob("*") if p.is_dir()), key=lambda p: len(p.parts), reverse=True):
try:
path.rmdir()
except OSError:
pass
def main() -> int:
args = parse_args()
root = Path(args.dir).resolve()
if not root.exists():
print(f"generated files directory does not exist: {root}")
return 0
if not root.is_dir():
raise SystemExit(f"not a directory: {root}")
if args.days < 1:
raise SystemExit("--days must be >= 1")
cutoff_epoch = time.time() - args.days * 24 * 60 * 60
expired_files = list(iter_expired_files(root, cutoff_epoch))
total_bytes = sum(path.stat().st_size for path in expired_files)
mode = "APPLY" if args.apply else "DRY-RUN"
print(f"mode={mode} root={root} days={args.days} files={len(expired_files)} bytes={total_bytes}")
for path in expired_files:
print(path)
if args.apply:
path.unlink()
if args.apply:
remove_empty_dirs(root)
return 0
if __name__ == "__main__":
raise SystemExit(main())