From e7a2719fa145fd27ac80b898d12f7362c1658c36 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Sun, 19 Apr 2026 13:33:43 +0200 Subject: [PATCH] add temporary deploy --- .dockerignore | 9 ++ .env.example | 14 +++ Dockerfile | 1 - README.md | 258 ++++++++++++++++++++++++++++++++----------- docker-compose.yml | 9 +- scripts/backup_db.sh | 27 +++++ scripts/deploy.sh | 37 +++++++ 7 files changed, 286 insertions(+), 69 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100755 scripts/backup_db.sh create mode 100755 scripts/deploy.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..456b108 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +.gitignore +.pytest_cache +.venv +__pycache__ +*.pyc +data +tests + diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..75a54ce --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# Runtime +HOST=0.0.0.0 +PORT=10000 + +# In Docker, keep the database inside the mounted /app/data directory. +DATABASE_URL=sqlite:////app/data/app.db + +# Host-side persistent data directory +DATA_DIR=./data + +# Container user mapping +UID=1000 +GID=1000 + diff --git a/Dockerfile b/Dockerfile index 9c82e15..fedbe86 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,6 @@ COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app ./app -COPY tests ./tests RUN mkdir -p /app/data diff --git a/README.md b/README.md index b1b0a13..046b8a2 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,17 @@ - pytest / FastAPI TestClient - Docker / Docker Compose -项目目标是小而稳、容易继续扩展。目前已经支持固定三层的数据结构、基础 CRUD、单图上传能力和全局搜索,但仍然没有加入 OCR、AI 识别或其他扩展功能。 +项目目标是小而稳、容易继续扩展。它不是企业平台,也不是复杂运维系统,重点是本地开发简单、容器部署稳定、数据持久化清楚、后续几个月后自己回来看也能快速接上。 + +## 当前已支持 + +- 固定 3 级结构:`Box -> Item -> SubItem` +- Box / Item / SubItem 基础 CRUD +- Box / Item / SubItem 单图上传、替换、删除、展示 +- Box / Item / SubItem 全局搜索 +- Docker / Compose 长期运行 +- SQLite 数据持久化 +- 基础自动化测试 ## 当前数据模型 @@ -36,30 +46,9 @@ Box └── SubItem ``` -## 当前已支持 - -目前已支持的基础能力: - -- Box 列表、详情、新建、编辑、删除 -- Item 新建、详情、编辑、删除 -- SubItem 新建、编辑、删除 -- Box / Item / SubItem 单张图片上传、替换、删除、展示 -- Box / Item / SubItem 全局搜索 -- `/` 重定向到 `/boxes` -- Jinja2 模板渲染 -- 静态文件挂载 -- SQLite 持久化 -- Docker 长期运行 -- 基础自动化测试 - -删除规则: - -- 删除 `Box` 时,会级联删除其下全部 `Item` 和对应 `SubItem` -- 删除容器型 `Item` 时,会级联删除其下 `SubItem` - ## 图片能力说明 -这一阶段的图片系统保持简单直接: +图片系统保持简单直接: - `Box` 最多支持 1 张图片 - `Item` 最多支持 1 张图片 @@ -67,10 +56,7 @@ Box - 支持上传、替换、删除 - 不支持多图 -图片的主要用途是帮助识别物品、提高浏览效率、方便手机拍照后直接附加到记录中。 -它不是一个原图归档系统。 - -### 图片处理方式 +图片主要用于帮助识别物品、提高浏览效率、方便手机拍照后直接附加到记录中。它不是原图归档系统。 上传图片后,系统会使用 Pillow 做统一处理: @@ -87,7 +73,7 @@ Box - `image_width` - `image_height` -图片访问通过普通 HTTP 路由返回 JPEG 数据,例如: +图片访问路由例如: - `/boxes/{id}/image` - `/items/{id}/image` @@ -95,10 +81,10 @@ Box ## 全局搜索 -当前已经支持一个轻量的全局搜索页: +当前支持一个轻量的全局搜索页: - 路由:`/search` -- 使用 query parameter,例如:`/search?q=电源线` +- 方式:`GET /search?q=关键词` 搜索范围包括: @@ -109,13 +95,13 @@ Box - `SubItem.name` - `SubItem.note` -当前使用 SQLite 上的简单模糊匹配完成搜索,不引入外部搜索引擎或复杂全文系统。 +当前使用 SQLite 上的简单模糊匹配,不引入外部搜索引擎或复杂全文系统。 -搜索结果会尽量帮助你快速定位: +搜索结果会显示: -- 显示对象类型:`Box / Item / SubItem` -- 显示名称和备注 -- 显示归属路径 +- 对象类型:`Box / Item / SubItem` +- 名称和备注 +- 归属路径 - 对 `Item` 展示所属 `Box` - 对 `SubItem` 展示所属 `Item` 和 `Box` - 如果对象已有图片,会显示一个小缩略图 @@ -124,7 +110,6 @@ Box 这一阶段仍然没有实现以下内容: -- 搜索 - 多图上传 - OCR - AI 识别物品 @@ -149,14 +134,13 @@ Box │ ├── static │ │ └── style.css │ └── templates -│ ├── base.html -│ ├── boxes -│ ├── items -│ └── subitems ├── data +├── scripts +│ ├── backup_db.sh +│ └── deploy.sh ├── tests -│ ├── conftest.py -│ └── test_app.py +├── .dockerignore +├── .env.example ├── docker-compose.yml ├── Dockerfile ├── pytest.ini @@ -166,17 +150,38 @@ Box ## 轻量配置 -项目通过环境变量支持以下配置项: +项目通过环境变量支持以下部署时真正需要关心的配置: -- `DATABASE_URL` - `HOST` - `PORT` +- `DATABASE_URL` +- `DATA_DIR` +- `UID` +- `GID` -默认值: +推荐从示例文件开始: -- `DATABASE_URL=sqlite:///./data/app.db` -- `HOST=0.0.0.0` -- `PORT=10000` +```bash +cp .env.example .env +``` + +默认值如下: + +```env +HOST=0.0.0.0 +PORT=10000 +DATABASE_URL=sqlite:////app/data/app.db +DATA_DIR=./data +UID=1000 +GID=1000 +``` + +说明: + +- 本地开发默认数据库仍然是 `./data/app.db` +- Docker 内建议继续使用 `sqlite:////app/data/app.db` +- `DATA_DIR` 控制宿主机上的持久化目录 +- `UID/GID` 用来让容器内文件权限更贴近宿主机用户 ## 本地开发模式 @@ -213,14 +218,33 @@ http://localhost:10000 ./data/app.db ``` -## Docker 部署模式 +## Docker 运行方式 Docker / Compose 是这个项目面向长期运行环境的方式。 -启动: +### 首次准备 ```bash -docker compose up --build +cp .env.example .env +mkdir -p data +``` + +### 启动 / 更新 + +```bash +docker compose up -d --build +``` + +### 查看状态 + +```bash +docker compose ps +``` + +### 查看日志 + +```bash +docker compose logs -f web ``` 访问: @@ -229,20 +253,129 @@ docker compose up --build http://localhost:10000 ``` -说明: +### Compose 配置说明 + +当前 `docker-compose.yml` 保持尽量简单: - 默认暴露 `10000` 端口 - `restart: unless-stopped` -- 容器使用 `1000:1000` 运行 -- SQLite 文件持久化到宿主机 `./data/app.db` -- 容器重建不会丢失数据 +- 容器用户来自 `UID:GID` +- 宿主机 `DATA_DIR` 挂载到容器内 `/app/data` +- SQLite 默认写入 `/app/data/app.db` -备份时直接复制 SQLite 文件即可: +## 自动化部署 + +这个项目没有复杂 CI/CD,只提供一个适合家用项目的轻量部署脚本: + +```bash +./scripts/deploy.sh +``` + +它会按顺序执行: + +1. 检查 `.env` +2. 准备数据目录 +3. 如果当前目录是 git 仓库,执行 `git pull --ff-only` +4. 执行 `docker compose up -d --build` +5. 输出容器状态 +6. 输出最近日志 + +这个脚本的目标不是做平台化发布,而是让“更新代码并刷新容器”变成一个稳定、可重复执行的动作。 + +如果你不想自动拉代码,也可以直接手动运行: + +```bash +docker compose up -d --build +``` + +## 数据持久化 + +当前 SQLite 文件默认会保存在: + +- 宿主机:`./data/app.db` +- 容器内:`/app/data/app.db` + +这是因为 `docker-compose.yml` 把: ```text -./data/app.db +${DATA_DIR:-./data} ``` +挂载到了容器内: + +```text +/app/data +``` + +因此: + +- 容器重建不会删除宿主机上的数据库文件 +- 更新镜像不会导致 SQLite 数据丢失 +- 只要保留 `DATA_DIR` 目录,数据就还在 + +## 备份与恢复 + +### 最简单的备份方式 + +直接复制 SQLite 文件即可: + +```bash +cp data/app.db backups/app.db +``` + +或者使用附带脚本: + +```bash +./scripts/backup_db.sh +``` + +它会在 `backups/` 目录下生成一个带时间戳的副本。 + +### 最简单的恢复方式 + +停止容器后,把备份文件覆盖回去: + +```bash +docker compose stop +cp backups/app-YYYYMMDD-HHMMSS.db data/app.db +docker compose up -d +``` + +## 常见排查 + +### 1. 查看容器日志 + +```bash +docker compose logs -f web +``` + +### 2. 确认服务是否已启动 + +```bash +docker compose ps +``` + +如果状态是 `Up`,通常说明容器已经跑起来了。 + +### 3. 确认端口映射 + +```bash +docker compose port web 10000 +``` + +### 4. 确认数据库文件还在 + +```bash +ls -lh data +``` + +如果看到 `app.db`,说明宿主机持久化文件还在。 + +### 5. 容器更新后数据为什么没丢 + +因为数据库不放在镜像里,而是放在宿主机挂载目录 `DATA_DIR` 中。 +镜像更新只会替换应用代码和运行环境,不会覆盖这个宿主机目录。 + ## 测试 运行测试: @@ -256,10 +389,7 @@ python -m pytest 当前测试覆盖包括: - Box / Item / SubItem 基础 CRUD -- 404 返回 -- 非容器 Item 不能创建 SubItem -- Box / Item 删除后的级联删除 -- 图片上传、转换为 JPEG、缩放、读取、替换、删除 -- 全局搜索 name / note,并展示对象类型与归属路径 -- 无图片访问和非法图片上传等错误路径 -- 关键 POST 请求后的重定向行为 +- 图片上传、替换、删除与错误路径 +- 全局搜索 name / note +- 创建后的重定向行为 +- 关键页面结构和 UX 文案 diff --git a/docker-compose.yml b/docker-compose.yml index 137114d..f584e63 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,15 @@ services: web: + container_name: moving-helper build: context: . - user: "1000:1000" + user: "${UID:-1000}:${GID:-1000}" ports: - "${PORT:-10000}:${PORT:-10000}" environment: - HOST: 0.0.0.0 + HOST: ${HOST:-0.0.0.0} PORT: ${PORT:-10000} - DATABASE_URL: sqlite:////app/data/app.db + DATABASE_URL: ${DATABASE_URL:-sqlite:////app/data/app.db} volumes: - - ./data:/app/data + - ${DATA_DIR:-./data}:/app/data restart: unless-stopped diff --git a/scripts/backup_db.sh b/scripts/backup_db.sh new file mode 100755 index 0000000..63087cd --- /dev/null +++ b/scripts/backup_db.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env sh +set -eu + +PROJECT_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +cd "$PROJECT_ROOT" + +if [ ! -f ".env" ] && [ -f ".env.example" ]; then + echo "未找到 .env,先从 .env.example 复制一份:" + echo " cp .env.example .env" + exit 1 +fi + +DATA_DIR_VALUE=$(grep '^DATA_DIR=' .env 2>/dev/null | tail -n 1 | cut -d '=' -f 2- || true) +DATA_DIR=${DATA_DIR_VALUE:-./data} +DB_PATH="$DATA_DIR/app.db" + +if [ ! -f "$DB_PATH" ]; then + echo "未找到数据库文件:$DB_PATH" + exit 1 +fi + +mkdir -p backups +TIMESTAMP=$(date +"%Y%m%d-%H%M%S") +DESTINATION="backups/app-$TIMESTAMP.db" + +cp "$DB_PATH" "$DESTINATION" +echo "备份已创建:$DESTINATION" diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..21a9add --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env sh +set -eu + +PROJECT_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +cd "$PROJECT_ROOT" + +if [ ! -f ".env" ] && [ -f ".env.example" ]; then + echo "未找到 .env,先从 .env.example 复制一份:" + echo " cp .env.example .env" + exit 1 +fi + +DATA_DIR_VALUE=$(grep '^DATA_DIR=' .env 2>/dev/null | tail -n 1 | cut -d '=' -f 2- || true) +DATA_DIR=${DATA_DIR_VALUE:-./data} + +mkdir -p "$DATA_DIR" + +echo "[1/4] 拉取最新代码(如果当前目录是 git 仓库)" +if [ -d ".git" ]; then + git pull --ff-only +else + echo "跳过:当前目录不是 git 仓库" +fi + +echo "[2/4] 构建并更新容器" +docker compose up -d --build + +echo "[3/4] 当前容器状态" +docker compose ps + +echo "[4/4] 最近日志" +docker compose logs --tail=50 web + +echo +echo "部署完成。应用默认地址:" +echo " http://localhost:$(grep '^PORT=' .env 2>/dev/null | tail -n 1 | cut -d '=' -f 2- || echo 10000)" +