Skip to content

Commit a5097b6

Browse files
authored
Merge pull request #4112 from TowyTowy/fix/state-reupload-emulator-desync
fix(states): repoint the row when a re-upload changes emulator
2 parents b0b07c4 + f3d6be1 commit a5097b6

2 files changed

Lines changed: 78 additions & 1 deletion

File tree

backend/endpoints/states.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,23 @@ async def add_state(
105105
user_id=request.user.id, rom_id=rom.id, file_name=sanitized_state_filename
106106
)
107107
if db_state:
108+
# The new bytes land under the requested emulator's folder, so the row
109+
# follows them and the file at the old location is left orphaned.
110+
stale_full_path = db_state.full_path
108111
db_state = db_state_handler.update_state(
109-
db_state.id, {"file_size_bytes": scanned_state.file_size_bytes}
112+
db_state.id,
113+
{
114+
"file_size_bytes": scanned_state.file_size_bytes,
115+
"file_path": scanned_state.file_path,
116+
"emulator": emulator,
117+
},
110118
)
119+
120+
if stale_full_path != db_state.full_path:
121+
try:
122+
await fs_asset_handler.remove_file(stale_full_path)
123+
except FileNotFoundError:
124+
pass
111125
else:
112126
scanned_state.rom_id = rom.id
113127
scanned_state.user_id = request.user.id

backend/tests/endpoints/test_states.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,69 @@ def test_add_state_rejects_oversized_uploads(
155155
assert response.status_code == status.HTTP_413_CONTENT_TOO_LARGE
156156

157157

158+
@mock.patch(
159+
"endpoints.states.fs_asset_handler.remove_file", new_callable=mock.AsyncMock
160+
)
161+
@mock.patch("endpoints.states.fs_asset_handler.write_file", new_callable=mock.AsyncMock)
162+
@mock.patch("endpoints.states.scan_state", new_callable=mock.AsyncMock)
163+
def test_reupload_updates_file_path_and_emulator(
164+
mock_scan,
165+
_mock_write,
166+
mock_remove,
167+
client,
168+
access_token: str,
169+
rom: Rom,
170+
platform: Platform,
171+
admin_user: User,
172+
):
173+
"""Re-uploading the same filename under a different emulator must move the
174+
row's file_path/emulator to where the new bytes landed, so the row never
175+
serves the previous emulator's state."""
176+
existing = db_state_handler.add_state(
177+
State(
178+
file_name="game.state",
179+
file_name_no_tags="game",
180+
file_name_no_ext="game",
181+
file_extension="state",
182+
file_path=f"{platform.slug}/states/old_emu",
183+
file_size_bytes=100,
184+
emulator="old_emu",
185+
rom_id=rom.id,
186+
user_id=admin_user.id,
187+
)
188+
)
189+
190+
new_path = f"{platform.slug}/states/new_emu"
191+
mock_scan.return_value = State(
192+
file_name="game.state",
193+
file_name_no_tags="game",
194+
file_name_no_ext="game",
195+
file_extension="state",
196+
file_path=new_path,
197+
file_size_bytes=200,
198+
rom_id=rom.id,
199+
user_id=admin_user.id,
200+
)
201+
202+
response = client.post(
203+
f"/api/states?rom_id={rom.id}&emulator=new_emu",
204+
files={"stateFile": ("game.state", b"NEW STATE", "application/octet-stream")},
205+
headers=_auth(access_token),
206+
)
207+
208+
assert response.status_code == status.HTTP_200_OK
209+
210+
updated = db_state_handler.get_state(user_id=admin_user.id, id=existing.id)
211+
assert updated is not None
212+
assert updated.file_path == new_path
213+
assert updated.emulator == "new_emu"
214+
assert updated.file_size_bytes == 200
215+
# full_path now points at the freshly written bytes, not the stale ones.
216+
assert updated.full_path == f"{new_path}/game.state"
217+
# The orphaned bytes at the old location are cleaned up.
218+
mock_remove.assert_awaited_once_with(f"{platform.slug}/states/old_emu/game.state")
219+
220+
158221
@contextmanager
159222
def _kiosk_mode():
160223
"""Both call sites of the setting, as a real KIOSK_MODE=true deploy sees it."""

0 commit comments

Comments
 (0)