mirror of
https://github.com/rommapp/romm.git
synced 2026-06-28 06:46:00 +00:00
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:
@@ -1,9 +1,12 @@
|
||||
import asyncio
|
||||
import enum
|
||||
import hashlib
|
||||
import zipfile
|
||||
from typing import Any
|
||||
|
||||
import socketio # type: ignore
|
||||
|
||||
from config import ASSETS_BASE_PATH
|
||||
from config.config_manager import config_manager as cm
|
||||
from endpoints.responses.rom import SimpleRomSchema
|
||||
from handler.database import db_platform_handler, db_rom_handler
|
||||
@@ -817,11 +820,41 @@ async def scan_rom(
|
||||
return Rom(**rom_attrs)
|
||||
|
||||
|
||||
async def _scan_asset(file_name: str, asset_path: str):
|
||||
def _compute_file_hash(file_path: str) -> str:
|
||||
hash_obj = hashlib.md5(usedforsecurity=False)
|
||||
with open(file_path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(8192), b""):
|
||||
hash_obj.update(chunk)
|
||||
return hash_obj.hexdigest()
|
||||
|
||||
|
||||
def _compute_zip_hash(zip_path: str) -> str:
|
||||
with zipfile.ZipFile(zip_path, "r") as zf:
|
||||
file_hashes = []
|
||||
for name in sorted(zf.namelist()):
|
||||
if not name.endswith("/"):
|
||||
content = zf.read(name)
|
||||
file_hash = hashlib.md5(content, usedforsecurity=False).hexdigest()
|
||||
file_hashes.append(f"{name}:{file_hash}")
|
||||
combined = "\n".join(file_hashes)
|
||||
return hashlib.md5(combined.encode(), usedforsecurity=False).hexdigest()
|
||||
|
||||
|
||||
def _compute_content_hash(file_path: str) -> str | None:
|
||||
try:
|
||||
if zipfile.is_zipfile(file_path):
|
||||
return _compute_zip_hash(file_path)
|
||||
return _compute_file_hash(file_path)
|
||||
except Exception as e:
|
||||
log.debug(f"Failed to compute content hash for {file_path}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def _scan_asset(file_name: str, asset_path: str, compute_hash: bool = False):
|
||||
file_path = f"{asset_path}/{file_name}"
|
||||
file_size = await fs_asset_handler.get_file_size(file_path)
|
||||
|
||||
return {
|
||||
result = {
|
||||
"file_path": asset_path,
|
||||
"file_name": file_name,
|
||||
"file_name_no_tags": fs_asset_handler.get_file_name_with_no_tags(file_name),
|
||||
@@ -830,6 +863,12 @@ async def _scan_asset(file_name: str, asset_path: str):
|
||||
"file_size_bytes": file_size,
|
||||
}
|
||||
|
||||
if compute_hash:
|
||||
absolute_path = f"{ASSETS_BASE_PATH}/{file_path}"
|
||||
result["content_hash"] = _compute_content_hash(absolute_path)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def scan_save(
|
||||
file_name: str,
|
||||
@@ -841,7 +880,7 @@ async def scan_save(
|
||||
saves_path = fs_asset_handler.build_saves_file_path(
|
||||
user=user, platform_fs_slug=platform_fs_slug, rom_id=rom_id, emulator=emulator
|
||||
)
|
||||
scanned_asset = await _scan_asset(file_name, saves_path)
|
||||
scanned_asset = await _scan_asset(file_name, saves_path, compute_hash=True)
|
||||
return Save(**scanned_asset)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user