Files
romm/backend/models/assets.py
nendo 287c487308 feat(saves): expose per-device sync attribution and origin device
saves responses now include one device_syncs entry per device that has
synced a save, not just the caller's, so clients can tell which devices
hold a save. is_current is computed per entry and the caller's own entry
is ordered first for backward compatibility.

add a saves.origin_device_id column (migration 0081) recording the
device that created a save, set on initial upload only, surfaced as
origin_device_id on the save schema.
2026-06-05 20:25:01 +09:00

113 lines
3.8 KiB
Python

from __future__ import annotations
from functools import cached_property
from typing import TYPE_CHECKING
from sqlalchemy import BigInteger, ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import (
FILE_EXTENSION_MAX_LENGTH,
FILE_NAME_MAX_LENGTH,
FILE_PATH_MAX_LENGTH,
BaseModel,
)
if TYPE_CHECKING:
from models.device_save_sync import DeviceSaveSync
from models.rom import Rom
from models.user import User
class BaseAsset(BaseModel):
__abstract__ = True
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
file_name: Mapped[str] = mapped_column(String(length=FILE_NAME_MAX_LENGTH))
file_name_no_tags: Mapped[str] = mapped_column(String(length=FILE_NAME_MAX_LENGTH))
file_name_no_ext: Mapped[str] = mapped_column(String(length=FILE_NAME_MAX_LENGTH))
file_extension: Mapped[str] = mapped_column(
String(length=FILE_EXTENSION_MAX_LENGTH)
)
file_path: Mapped[str] = mapped_column(String(length=FILE_PATH_MAX_LENGTH))
file_size_bytes: Mapped[int] = mapped_column(BigInteger(), default=0)
missing_from_fs: Mapped[bool] = mapped_column(default=False, nullable=False)
@cached_property
def full_path(self) -> str:
return f"{self.file_path}/{self.file_name}"
@cached_property
def download_path(self) -> str:
return f"/api/raw/assets/{self.full_path}?timestamp={self.updated_at}"
class RomAsset(BaseAsset):
__abstract__ = True
rom_id: Mapped[int] = mapped_column(ForeignKey("roms.id", ondelete="CASCADE"))
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
class Screenshot(RomAsset):
__tablename__ = "screenshots"
__table_args__ = {"extend_existing": True}
rom: Mapped[Rom] = relationship(lazy="joined", back_populates="screenshots")
user: Mapped[User] = relationship(lazy="joined", back_populates="screenshots")
class Save(RomAsset):
__tablename__ = "saves"
__table_args__ = {"extend_existing": True}
emulator: Mapped[str | None] = mapped_column(String(length=50))
slot: Mapped[str | None] = mapped_column(String(length=255))
content_hash: Mapped[str | None] = mapped_column(String(length=32))
origin_device_id: Mapped[str | None] = mapped_column(
String(length=255),
ForeignKey("devices.id", ondelete="SET NULL"),
default=None,
)
rom: Mapped[Rom] = relationship(lazy="joined", back_populates="saves")
user: Mapped[User] = relationship(lazy="joined", back_populates="saves")
device_syncs: Mapped[list[DeviceSaveSync]] = relationship(
back_populates="save",
cascade="all, delete-orphan",
lazy="raise",
)
@cached_property
def screenshot(self) -> Screenshot | None:
from handler.database import db_screenshot_handler
return db_screenshot_handler.get_screenshot(
rom_id=self.rom_id,
user_id=self.user_id,
file_name=self.file_name, # Match state filename against screenshot filename stem
file_name_no_ext=self.file_name_no_ext,
)
class State(RomAsset):
__tablename__ = "states"
__table_args__ = {"extend_existing": True}
emulator: Mapped[str | None] = mapped_column(String(length=50))
rom: Mapped[Rom] = relationship(lazy="joined", back_populates="states")
user: Mapped[User] = relationship(lazy="joined", back_populates="states")
@cached_property
def screenshot(self) -> Screenshot | None:
from handler.database import db_screenshot_handler
return db_screenshot_handler.get_screenshot(
rom_id=self.rom_id,
user_id=self.user_id,
file_name=self.file_name, # Match state filename against screenshot filename stem
file_name_no_ext=self.file_name_no_ext,
)