docs(deploy): document generated file retention
This commit is contained in:
@@ -117,8 +117,9 @@
|
||||
|--------|------|------|------|
|
||||
| POST | `/` | 上传文件 | ✅ ACTIVE |
|
||||
|
||||
**Use Cases**:
|
||||
- 直接调用 `MinIOService`
|
||||
**Use Cases / Services**:
|
||||
- `SubmitIngestJobUseCase`
|
||||
- `OSSStorageService`(经 `get_storage_service` 注入)
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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 (领域模型)
|
||||
↓
|
||||
|
||||
@@ -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` 已退役。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -78,6 +78,16 @@ Staging 当前可以保持 no-op;Production 开启前必须先验证 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;禁止靠临场记忆操作数据库。
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 小虾 SaaS Docker 部署指南
|
||||
|
||||
> ⚠️ 历史文档,仅供归档参考。当前 canonical 部署入口是 `infra/docker/compose.yml` 和 `infra/docker/deploy-staging.sh`;生产发布必须按 `docs/PRODUCTION-RELEASE-CHECKLIST.md` 执行。
|
||||
|
||||
## 🐳 快速启动
|
||||
|
||||
### 1. 准备环境变量
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user