"""分片上传会话 ORM:支撑断点续传。""" from __future__ import annotations import json import logging from datetime import datetime, timezone from typing import Any from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, TypeDecorator from sqlalchemy.orm import Mapped, mapped_column from ..database import Base logger = logging.getLogger("audio2text.models") def _now() -> datetime: return datetime.now(timezone.utc) class _IntList(TypeDecorator): """把 list[int] 存成 JSON 字符串。SQLite 没有 ARRAY 类型。""" impl = Text cache_ok = True def process_bind_param(self, value: Any, dialect) -> str | None: return json.dumps(value) if value is not None else None def process_result_value(self, value: Any, dialect) -> list[int]: if not value: return [] try: return json.loads(value) except (json.JSONDecodeError, TypeError): # DB 脏数据(手工修改/并发写截断)不应让整行读取失败 logger.warning("uploaded_chunks JSON 解析失败,回退空列表:%r", value[:80]) return [] class UploadSession(Base): __tablename__ = "upload_session" upload_id: Mapped[str] = mapped_column(String(64), primary_key=True) filename: Mapped[str] = mapped_column(String(512), nullable=False) size_bytes: Mapped[int] = mapped_column(Integer, nullable=False) chunk_size: Mapped[int] = mapped_column(Integer, nullable=False) total_chunks: Mapped[int] = mapped_column(Integer, nullable=False) uploaded_chunks: Mapped[list[int]] = mapped_column(_IntList, default=list) # pending → completed(complete 成功)| abandoned(reaper 清理) status: Mapped[str] = mapped_column(String(32), nullable=False, default="pending", index=True) # 拼接完成后的视频相对 upload_dir 路径 final_path: Mapped[str | None] = mapped_column(String(1024), nullable=True) # complete 后关联的 Task.id。ondelete=SET NULL:Task 被删时 session 保留(task_id 置空) task_id: Mapped[int | None] = mapped_column( ForeignKey("task.id", ondelete="SET NULL"), nullable=True ) created_at: Mapped[datetime] = mapped_column(DateTime, default=_now, index=True) updated_at: Mapped[datetime] = mapped_column(DateTime, default=_now, onupdate=_now)