feat(saves): add slot-based save sync with content hash deduplication

- Add device registration and save synchronization
- Implement slot-based save organization with datetime tagging
- Add conflict detection for multi-device sync scenarios
- Add content hash computation for save deduplication
- Support ZIP inner-file hashing for consistent deduplication
- Add confirm_download endpoint for sync state management
- Add overwrite parameter to bypass conflict checks
This commit is contained in:
nendo
2026-01-31 21:57:22 +09:00
parent b420af7578
commit a236123e4f
8 changed files with 1868 additions and 134 deletions

View File

@@ -1,6 +1,6 @@
from collections.abc import Sequence
from sqlalchemy import and_, delete, select, update
from sqlalchemy import and_, delete, desc, select, update
from sqlalchemy.orm import QueryableAttribute, Session, load_only
from decorators.database import begin_session
@@ -42,12 +42,28 @@ class DBSavesHandler(DBBaseHandler):
.limit(1)
).first()
@begin_session
def get_save_by_content_hash(
self,
user_id: int,
rom_id: int,
content_hash: str,
session: Session = None, # type: ignore
) -> Save | None:
return session.scalar(
select(Save)
.filter_by(rom_id=rom_id, user_id=user_id, content_hash=content_hash)
.limit(1)
)
@begin_session
def get_saves(
self,
user_id: int,
rom_id: int | None = None,
platform_id: int | None = None,
slot: str | None = None,
order_by_updated_at_desc: bool = False,
only_fields: Sequence[QueryableAttribute] | None = None,
session: Session = None, # type: ignore
) -> Sequence[Save]:
@@ -61,6 +77,12 @@ class DBSavesHandler(DBBaseHandler):
Rom.platform_id == platform_id
)
if slot is not None:
query = query.filter(Save.slot == slot)
if order_by_updated_at_desc:
query = query.order_by(desc(Save.updated_at))
if only_fields:
query = query.options(load_only(*only_fields))
@@ -125,3 +147,28 @@ class DBSavesHandler(DBBaseHandler):
)
return missing_saves
@begin_session
def get_saves_summary(
self,
user_id: int,
rom_id: int,
session: Session = None, # type: ignore
) -> dict:
saves = session.scalars(
select(Save)
.filter_by(user_id=user_id, rom_id=rom_id)
.order_by(desc(Save.updated_at))
).all()
slots_data: dict[str | None, dict] = {}
for save in saves:
slot_key = save.slot
if slot_key not in slots_data:
slots_data[slot_key] = {"slot": slot_key, "count": 0, "latest": save}
slots_data[slot_key]["count"] += 1
return {
"total_count": len(saves),
"slots": list(slots_data.values()),
}