152 lines
7.3 KiB
Python
152 lines
7.3 KiB
Python
"""Upload batch persistence (US05-02).
|
|
|
|
One batch is one approved album folder handed to ``immich-go``. It is the durable
|
|
record of an irreversible external action, so it stores everything needed to answer
|
|
"which exact bytes did we send, with which command, and how did it end?" after a
|
|
crash: the preflight token that authorised it, the redacted command, the uploader
|
|
version, the per-asset pre-upload hashes, every attempt, and where the raw report
|
|
was written.
|
|
|
|
Item rows keep both digests: SHA-256 is the app's byte identity and SHA-1 is what
|
|
Immich/immich-go use to recognise a file it already has (concept §8).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import (
|
|
Boolean,
|
|
DateTime,
|
|
ForeignKey,
|
|
Integer,
|
|
String,
|
|
func,
|
|
)
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from photo_pipeline.db import Base
|
|
|
|
|
|
class UploadBatch(Base):
|
|
__tablename__ = "upload_batches"
|
|
|
|
id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
album: Mapped[str] = mapped_column(String, nullable=False, index=True)
|
|
folder: Mapped[str] = mapped_column(String, nullable=False)
|
|
album_name: Mapped[str] = mapped_column(String, nullable=False)
|
|
# planned | running | cancelling | cancelled | succeeded | failed
|
|
# | unknown_requires_verification
|
|
state: Mapped[str] = mapped_column(String, nullable=False, default="planned")
|
|
# The preflight token this batch was approved against; re-checked before every
|
|
# attempt so changed bytes or decisions cannot be uploaded silently.
|
|
preflight_token: Mapped[str] = mapped_column(String, nullable=False)
|
|
allow_partial: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
|
|
command: Mapped[str | None] = mapped_column(String) # JSON array, redacted
|
|
uploader_version: Mapped[str | None] = mapped_column(String)
|
|
asset_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
|
|
attempt_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
# Bumped on every claim and used as the fencing token, so a superseded attempt
|
|
# cannot commit its outcome.
|
|
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
|
worker_id: Mapped[str | None] = mapped_column(String)
|
|
|
|
report_path: Mapped[str | None] = mapped_column(String)
|
|
report_bytes: Mapped[int | None] = mapped_column(Integer)
|
|
report_truncated: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
exit_code: Mapped[int | None] = mapped_column(Integer)
|
|
|
|
# Parsed report evidence (US05-03). ``parser`` is NULL when the uploader's
|
|
# version has no pinned grammar; ``outcome_state`` is then
|
|
# ``requires_verification`` regardless of how the process exited.
|
|
parser: Mapped[str | None] = mapped_column(String)
|
|
parser_version: Mapped[int | None] = mapped_column(Integer)
|
|
parsed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
# verified | requires_verification; NULL until a report has been parsed.
|
|
outcome_state: Mapped[str | None] = mapped_column(String)
|
|
outcome_counts: Mapped[str | None] = mapped_column(String) # JSON, from the items
|
|
report_counts: Mapped[str | None] = mapped_column(String) # JSON, uploader's own
|
|
|
|
# Verification (US05-04). ``stale_bytes`` means at least one uploaded file has
|
|
# been edited since: the batch carries a visible warning and cannot be re-run.
|
|
verified_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
stale_bytes: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
|
|
error_code: Mapped[str | None] = mapped_column(String)
|
|
error_message: Mapped[str | None] = mapped_column(String)
|
|
|
|
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
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 UploadItem(Base):
|
|
__tablename__ = "upload_items"
|
|
|
|
batch_id: Mapped[str] = mapped_column(
|
|
ForeignKey("upload_batches.id", ondelete="CASCADE"), primary_key=True
|
|
)
|
|
asset_id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
path: Mapped[str] = mapped_column(String, nullable=False)
|
|
# Hashes of the bytes as they were when the batch was created.
|
|
sha256: Mapped[str | None] = mapped_column(String)
|
|
sha1: Mapped[str | None] = mapped_column(String)
|
|
# pending | sent | failed — what the batch *process* did with this item;
|
|
# ``sent`` only means the uploader exited successfully.
|
|
state: Mapped[str] = mapped_column(String, nullable=False, default="pending")
|
|
# What the uploader's report says happened (US05-03): uploaded | upgraded |
|
|
# duplicate | skipped | failed | unknown. NULL before the report is parsed;
|
|
# ``unknown`` whenever the report does not classify this file — never success.
|
|
outcome: Mapped[str | None] = mapped_column(String)
|
|
evidence: Mapped[str | None] = mapped_column(String) # the bounded report line
|
|
outcome_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
|
|
# Verification against the server (US05-04): present | absent | inconclusive |
|
|
# manual. NULL until the item has been verified; ``inconclusive`` whenever the
|
|
# server could not answer — which is never treated as success.
|
|
verification: Mapped[str | None] = mapped_column(String)
|
|
verified_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
# The bytes on disk at verification time, and whether they still are the bytes
|
|
# this batch uploaded.
|
|
observed_sha256: Mapped[str | None] = mapped_column(String)
|
|
changed_after_upload: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
|
)
|
|
|
|
|
|
class UploadVerification(Base):
|
|
"""Append-only evidence for every verification and manual resolution (US05-04).
|
|
|
|
The item row is a projection of the latest answer; this table is the history
|
|
that answers "who decided this, on what evidence, and when?". Rows are never
|
|
updated or deleted, so a manual resolution can always be told apart from
|
|
server evidence.
|
|
"""
|
|
|
|
__tablename__ = "upload_verifications"
|
|
|
|
id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
batch_id: Mapped[str] = mapped_column(
|
|
ForeignKey("upload_batches.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
asset_id: Mapped[str] = mapped_column(String, nullable=False)
|
|
action: Mapped[str] = mapped_column(String, nullable=False) # verify | resolve
|
|
source: Mapped[str] = mapped_column(String, nullable=False) # immich_api | operator
|
|
# present | absent | inconclusive for a verify; the recorded outcome for a resolve.
|
|
result: Mapped[str] = mapped_column(String, nullable=False)
|
|
outcome: Mapped[str | None] = mapped_column(String)
|
|
evidence: Mapped[str] = mapped_column(String, nullable=False)
|
|
actor: Mapped[str | None] = mapped_column(String)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|