Skip to content

Commit e9a3d3c

Browse files
committed
fix: redirect input key in all file-system key-value store operations
1 parent 99ea41f commit e9a3d3c

2 files changed

Lines changed: 69 additions & 5 deletions

File tree

src/apify/storage_clients/_file_system/_key_value_store_client.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44
from itertools import chain
55
from pathlib import Path
6-
from typing import Self
6+
from typing import Any, Self
77

88
from typing_extensions import override
99

@@ -89,10 +89,32 @@ async def purge(self) -> None:
8989

9090
@override
9191
async def get_value(self, *, key: str) -> KeyValueStoreRecord | None:
92-
if key == self._input_key:
93-
# Potentially point to custom input file name instead
94-
key = self._input_key_filename
95-
return await super().get_value(key=key)
92+
return await super().get_value(key=self._resolve_input_key(key))
93+
94+
@override
95+
async def set_value(self, *, key: str, value: Any, content_type: str | None = None) -> None:
96+
await super().set_value(key=self._resolve_input_key(key), value=value, content_type=content_type)
97+
98+
@override
99+
async def record_exists(self, *, key: str) -> bool:
100+
return await super().record_exists(key=self._resolve_input_key(key))
101+
102+
@override
103+
async def delete_value(self, *, key: str) -> None:
104+
await super().delete_value(key=self._resolve_input_key(key))
105+
106+
@override
107+
async def get_public_url(self, *, key: str) -> str:
108+
return await super().get_public_url(key=self._resolve_input_key(key))
109+
110+
def _resolve_input_key(self, key: str) -> str:
111+
"""Redirect the logical input key to the actual input file name on disk.
112+
113+
The platform may store the Actor input under a name with an extension (e.g. `INPUT.json`) while the
114+
logical key stays `INPUT`. Redirecting keeps every record operation pointed at that single file, so
115+
e.g. `set_value` overwrites it instead of creating a duplicate that would later be rejected on open.
116+
"""
117+
return self._input_key_filename if key == self._input_key else key
96118

97119
@staticmethod
98120
async def _create_missing_metadata_for_input_file(key: str, record_path: Path) -> None:

tests/unit/storage_clients/test_file_system.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,45 @@ async def test_pre_existing_input_used_by_actor(input_file_name: str) -> None:
9090
path_to_input / input_file_name,
9191
path_to_input / f'{input_file_name}.__metadata__.json',
9292
}
93+
94+
95+
async def test_set_value_with_input_key_targets_existing_input_file() -> None:
96+
"""`set_value` with the input key overwrites the existing `INPUT.json` instead of creating a duplicate."""
97+
configuration = Configuration.get_global_configuration()
98+
99+
# Pre-create a custom-named input file (with extension) before opening the client.
100+
kvs_path = Path(configuration.storage_dir) / 'key_value_stores' / 'default'
101+
kvs_path.mkdir(parents=True)
102+
(kvs_path / 'INPUT.json').write_text(json.dumps({'foo': 'bar'}))
103+
104+
client = await ApifyFileSystemKeyValueStoreClient.open(id=None, name=None, alias=None, configuration=configuration)
105+
await client.set_value(key=configuration.input_key, value={'foo': 'baz'})
106+
107+
# The existing input file is overwritten in place. No second input file (e.g. `INPUT`) is created.
108+
assert set(kvs_path.glob('*')) == {
109+
kvs_path / '__metadata__.json',
110+
kvs_path / 'INPUT.json',
111+
kvs_path / f'INPUT.json.{METADATA_FILENAME}',
112+
}
113+
114+
# Reopening must not raise "Only one input file is allowed", i.e. no duplicate input file was created.
115+
client = await ApifyFileSystemKeyValueStoreClient.open(id=None, name=None, alias=None, configuration=configuration)
116+
record = await client.get_value(key=configuration.input_key)
117+
assert record is not None
118+
assert record.value == {'foo': 'baz'}
119+
120+
121+
async def test_record_exists_and_delete_value_target_existing_input_file() -> None:
122+
"""`record_exists` and `delete_value` with the input key operate on the existing `INPUT.json`."""
123+
configuration = Configuration.get_global_configuration()
124+
125+
kvs_path = Path(configuration.storage_dir) / 'key_value_stores' / 'default'
126+
kvs_path.mkdir(parents=True)
127+
(kvs_path / 'INPUT.json').write_text(json.dumps({'foo': 'bar'}))
128+
129+
client = await ApifyFileSystemKeyValueStoreClient.open(id=None, name=None, alias=None, configuration=configuration)
130+
assert await client.record_exists(key=configuration.input_key) is True
131+
132+
await client.delete_value(key=configuration.input_key)
133+
assert await client.record_exists(key=configuration.input_key) is False
134+
assert not (kvs_path / 'INPUT.json').exists()

0 commit comments

Comments
 (0)