74 lines
3.4 KiB
Python
74 lines
3.4 KiB
Python
"""Stable asset identity and path history.
|
|
|
|
A file path is mutable metadata, never identity: ``assets.id`` is a generated
|
|
UUID that never changes across moves, renames, or archival. ``asset_paths``
|
|
records every path an asset has occupied. ``state_version`` supports optimistic
|
|
concurrency; ``created_at``/``updated_at`` are the audit timestamps.
|
|
|
|
Later-stage columns (canonical link, safety score, archive location) are added by
|
|
the migrations of the stories that own them, not speculatively here.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import DateTime, ForeignKey, Integer, String, func
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from photo_pipeline.db import Base
|
|
|
|
|
|
class Asset(Base):
|
|
__tablename__ = "assets"
|
|
|
|
id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
original_path: Mapped[str] = mapped_column(String, nullable=False)
|
|
current_path: Mapped[str | None] = mapped_column(String, unique=True)
|
|
|
|
current_sha256: Mapped[str | None] = mapped_column(String)
|
|
pixel_sha256: Mapped[str | None] = mapped_column(String)
|
|
phash: Mapped[str | None] = mapped_column(String)
|
|
hash_version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
|
phash_version: Mapped[int | None] = mapped_column(Integer)
|
|
byte_size: Mapped[int | None] = mapped_column(Integer)
|
|
|
|
discovered_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
missing_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
# active | archiving | archived_online | archived_offline | restoring |
|
|
# missing_unexpected (concept §9). ``current_path`` is NULL once archived; the
|
|
# original is then explained by the location plus its relative archive path.
|
|
availability_state: Mapped[str] = mapped_column(String, nullable=False, default="active")
|
|
# Not a declared foreign key: SQLite cannot add one to an existing table, so the
|
|
# link is written and read by the archive service (US06-02).
|
|
archive_location_id: Mapped[str | None] = mapped_column(String)
|
|
archive_path: Mapped[str | None] = mapped_column(String)
|
|
# Set when the archived copy was proven to hold bytes other than the recorded
|
|
# ones (US06-04). Restore refuses such an asset instead of accepting a different
|
|
# file; cleared as soon as a verification matches again.
|
|
archive_divergent_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
# Duplicate canonical link: NULL when the asset is itself canonical or undecided.
|
|
canonical_asset_id: Mapped[str | None] = mapped_column(ForeignKey("assets.id"))
|
|
|
|
state_version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
|
)
|
|
|
|
|
|
class AssetPath(Base):
|
|
__tablename__ = "asset_paths"
|
|
|
|
asset_id: Mapped[str] = mapped_column(
|
|
ForeignKey("assets.id"), primary_key=True
|
|
)
|
|
path: Mapped[str] = mapped_column(String, primary_key=True)
|
|
valid_from: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), primary_key=True
|
|
)
|
|
valid_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
reason: Mapped[str | None] = mapped_column(String)
|