(二十一)初始化书目并启动个人书架
1. 准备一次,后续继续读取
初始化先生成一份随机签名密钥,保存后不覆盖;随后创建缺少的表,导入第九章实际采集的六本书。
# 项目文件:shelf_project/src/shelfweb/cli.py
import argparse
import os
from pathlib import Path
import secrets
from tempfile import NamedTemporaryFile
from . import create_app
from .db import init_db, seed_books
def provision_secret(location):
location.mkdir(parents=True, exist_ok=True)
destination = location / "secret.key"
# 先完整写入临时文件,再发布文件名,其他启动过程读到完整的密钥。
with NamedTemporaryFile(
mode="w", encoding="utf-8", dir=location, prefix=".secret-", delete=False
) as stream:
stream.write(secrets.token_hex(32))
temporary = Path(stream.name)
try:
try:
os.link(
temporary, destination
) # 已有密钥时不覆盖,保留原 Cookie 的签名依据。
except FileExistsError:
pass
finally:
temporary.unlink()NamedTemporaryFile 先在实例目录完整写好随机文本。os.link 给同一份文件内容建立最终名称,目标已存在时抛出 FileExistsError,代码保留已有密钥;最后删去临时名称。这让其他启动过程看到的是已经写好的内容,也让重复初始化继续使用旧签名依据。
# 续写同一文件:shelf_project/src/shelfweb/db.py
def seed_books(csv_path):
connection = get_db()
with connection:
for name in ["alice", "bob"]:
exists = connection.execute(
"SELECT id FROM users WHERE username=?", (name,)
).fetchone()
if exists is None:
connection.execute(
"INSERT INTO users(username,password_hash) VALUES (?,?)",
(name, generate_password_hash(f"learn-{name}")),
)
owner_id = connection.execute(
"SELECT id FROM users WHERE username=?", ("alice",)
).fetchone()["id"]
with csv_path.open(encoding="utf-8-sig", newline="") as stream:
for row in csv.DictReader(stream):
# 同一账号、同一书名已存在时跳过,不覆盖读者已经修改的状态。
exists = connection.execute(
"SELECT id FROM books WHERE owner_id=? AND title=?",
(owner_id, row["title"]),
).fetchone()
if exists is None:
connection.execute(
"INSERT INTO books(owner_id,title,finished,tags) VALUES (?,?,?,?)",
(owner_id, row["title"], int(row["finished"]), row["tags"]),
)账号查重后为 alice 导入书目,bob 保留空书架。书籍按用户和书名查重,已经存在就跳过,不覆盖用户之后修改的已读状态。with connection 结束时提交这一批变化。
# 续写同一文件:shelf_project/src/shelfweb/cli.py
def main():
parser = argparse.ArgumentParser(description="初始化或运行 Flask 个人书架")
parser.add_argument("command", choices=["init", "serve"])
parser.add_argument("--csv", type=Path, default=Path("data/books.csv"))
parser.add_argument("--port", type=int, default=8771)
args = parser.parse_args()
location = Path(os.getenv("SHELF_INSTANCE", "instance")).resolve()
if args.command == "init":
provision_secret(location)
app = create_app(instance_path=location)
with app.app_context():
# 命令也复用 g 中的连接,离开上下文后由回调关闭。
init_db()
seed_books(args.csv)
print("已准备账号、书目和本机签名密钥。")
else:
# 运行时沿用已经准备好的实例目录和签名密钥。
create_app(instance_path=location).run(
host="127.0.0.1", port=args.port, debug=False
)with app.app_context() 让命令复用 get_db,结束后也运行关闭连接的回调。启动和初始化使用相同的实例目录。
# 在 shelf_project 中准备账号、六本书和签名密钥。
uv run shelf-books init
# 启动本机服务,Ctrl+C 停止。
uv run shelf-books serve
打开 个人书架。演示账号为 alice / learn-alice、bob / learn-bob;也可以从页面注册自己的账号。第一次打开书架会跳到登录页,登录后显示自己的记录。
还可以在初始化之后使用 Flask 自带命令:
# --app 找到包中的 create_app 工厂;run 使用 Flask 的本机开发服务器。
uv run python -m flask --app shelfweb run --port 8771
# 只补建缺少的表;不清空现有数据。
uv run python -m flask --app shelfweb init-db
开发时的 --debug 会启用重载和交互调试器,可在自己的本机排查中使用。实际部署把这个工厂交给 WSGI 服务器,并配置 HTTPS、密钥与实例目录;请求处理逻辑继续复用这套应用。