chuk-artifacts 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chuk_artifacts/metadata.py +149 -124
- chuk_artifacts/session_operations.py +445 -0
- chuk_artifacts/store.py +115 -17
- {chuk_artifacts-0.1.0.dist-info → chuk_artifacts-0.1.1.dist-info}/METADATA +335 -144
- {chuk_artifacts-0.1.0.dist-info → chuk_artifacts-0.1.1.dist-info}/RECORD +8 -7
- {chuk_artifacts-0.1.0.dist-info → chuk_artifacts-0.1.1.dist-info}/WHEEL +0 -0
- {chuk_artifacts-0.1.0.dist-info → chuk_artifacts-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {chuk_artifacts-0.1.0.dist-info → chuk_artifacts-0.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,445 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# chuk_artifacts/session_operations.py (SECURE VERSION)
|
3
|
+
"""
|
4
|
+
Session-based file operations with strict session isolation.
|
5
|
+
NO cross-session operations allowed for security.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from __future__ import annotations
|
9
|
+
|
10
|
+
import uuid, hashlib, json, logging
|
11
|
+
from datetime import datetime
|
12
|
+
from typing import Any, Dict, Optional, Union, List
|
13
|
+
|
14
|
+
from .base import BaseOperations
|
15
|
+
from .exceptions import (
|
16
|
+
ArtifactStoreError, ArtifactNotFoundError, ArtifactExpiredError,
|
17
|
+
ProviderError, SessionError
|
18
|
+
)
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
_ANON_PREFIX = "anon"
|
23
|
+
_DEFAULT_TTL = 900
|
24
|
+
|
25
|
+
|
26
|
+
class SessionOperations(BaseOperations):
|
27
|
+
"""Session-based file operations with strict session isolation."""
|
28
|
+
|
29
|
+
async def move_file(
|
30
|
+
self,
|
31
|
+
artifact_id: str,
|
32
|
+
*,
|
33
|
+
new_filename: str = None,
|
34
|
+
new_session_id: str = None,
|
35
|
+
new_meta: Dict[str, Any] = None
|
36
|
+
) -> Dict[str, Any]:
|
37
|
+
"""
|
38
|
+
Move a file within the SAME session or rename it.
|
39
|
+
|
40
|
+
Parameters
|
41
|
+
----------
|
42
|
+
artifact_id : str
|
43
|
+
Source artifact ID
|
44
|
+
new_filename : str, optional
|
45
|
+
New filename (renames the file)
|
46
|
+
new_session_id : str, optional
|
47
|
+
Target session (BLOCKED - always raises error if different)
|
48
|
+
new_meta : dict, optional
|
49
|
+
Additional metadata to merge
|
50
|
+
|
51
|
+
Returns
|
52
|
+
-------
|
53
|
+
dict
|
54
|
+
Updated artifact metadata
|
55
|
+
|
56
|
+
Raises
|
57
|
+
------
|
58
|
+
ArtifactStoreError
|
59
|
+
If trying to move across sessions (always blocked)
|
60
|
+
"""
|
61
|
+
self._check_closed()
|
62
|
+
|
63
|
+
try:
|
64
|
+
# Get current metadata
|
65
|
+
record = await self._get_record(artifact_id)
|
66
|
+
current_session = record.get("session_id")
|
67
|
+
|
68
|
+
# STRICT SECURITY: Block ALL cross-session moves
|
69
|
+
if new_session_id and new_session_id != current_session:
|
70
|
+
raise ArtifactStoreError(
|
71
|
+
f"Cross-session moves are not permitted for security reasons. "
|
72
|
+
f"Artifact {artifact_id} belongs to session '{current_session}', "
|
73
|
+
f"cannot move to session '{new_session_id}'. Use copy operations within "
|
74
|
+
f"the same session only."
|
75
|
+
)
|
76
|
+
|
77
|
+
# Update metadata fields (only filename and meta allowed)
|
78
|
+
updates = {}
|
79
|
+
if new_filename:
|
80
|
+
updates["filename"] = new_filename
|
81
|
+
if new_meta:
|
82
|
+
existing_meta = record.get("meta", {})
|
83
|
+
existing_meta.update(new_meta)
|
84
|
+
updates["new_meta"] = existing_meta
|
85
|
+
updates["merge"] = True
|
86
|
+
|
87
|
+
if updates:
|
88
|
+
# Use the metadata operations to update
|
89
|
+
from .metadata import MetadataOperations
|
90
|
+
metadata_ops = MetadataOperations(self._artifact_store)
|
91
|
+
return await metadata_ops.update_metadata(artifact_id, **updates)
|
92
|
+
|
93
|
+
return record
|
94
|
+
|
95
|
+
except (ArtifactNotFoundError, ArtifactExpiredError):
|
96
|
+
raise
|
97
|
+
except Exception as e:
|
98
|
+
logger.error(
|
99
|
+
"File move failed",
|
100
|
+
extra={
|
101
|
+
"artifact_id": artifact_id,
|
102
|
+
"new_filename": new_filename,
|
103
|
+
"new_session_id": new_session_id,
|
104
|
+
"error": str(e)
|
105
|
+
}
|
106
|
+
)
|
107
|
+
raise ProviderError(f"Move operation failed: {e}") from e
|
108
|
+
|
109
|
+
async def copy_file(
|
110
|
+
self,
|
111
|
+
artifact_id: str,
|
112
|
+
*,
|
113
|
+
new_filename: str = None,
|
114
|
+
target_session_id: str = None,
|
115
|
+
new_meta: Dict[str, Any] = None,
|
116
|
+
summary: str = None
|
117
|
+
) -> str:
|
118
|
+
"""
|
119
|
+
Copy a file WITHIN THE SAME SESSION only.
|
120
|
+
|
121
|
+
Parameters
|
122
|
+
----------
|
123
|
+
artifact_id : str
|
124
|
+
Source artifact ID
|
125
|
+
new_filename : str, optional
|
126
|
+
Filename for the copy (defaults to original + "_copy")
|
127
|
+
target_session_id : str, optional
|
128
|
+
Target session (BLOCKED - must be same as source session)
|
129
|
+
new_meta : dict, optional
|
130
|
+
Additional metadata to merge
|
131
|
+
summary : str, optional
|
132
|
+
New summary for the copy
|
133
|
+
|
134
|
+
Returns
|
135
|
+
-------
|
136
|
+
str
|
137
|
+
New artifact ID of the copy
|
138
|
+
|
139
|
+
Raises
|
140
|
+
------
|
141
|
+
ArtifactStoreError
|
142
|
+
If trying to copy across sessions (always blocked)
|
143
|
+
"""
|
144
|
+
self._check_closed()
|
145
|
+
|
146
|
+
try:
|
147
|
+
# Get original metadata first to check session
|
148
|
+
original_meta = await self._get_record(artifact_id)
|
149
|
+
original_session = original_meta.get("session_id")
|
150
|
+
|
151
|
+
# STRICT SECURITY: Block ALL cross-session copies
|
152
|
+
if target_session_id and target_session_id != original_session:
|
153
|
+
raise ArtifactStoreError(
|
154
|
+
f"Cross-session copies are not permitted for security reasons. "
|
155
|
+
f"Artifact {artifact_id} belongs to session '{original_session}', "
|
156
|
+
f"cannot copy to session '{target_session_id}'. Files can only be "
|
157
|
+
f"copied within the same session."
|
158
|
+
)
|
159
|
+
|
160
|
+
# Ensure target session is the same as source
|
161
|
+
copy_session = original_session # Always use source session
|
162
|
+
|
163
|
+
# Get original data
|
164
|
+
original_data = await self._retrieve_data(artifact_id)
|
165
|
+
|
166
|
+
# Prepare copy metadata
|
167
|
+
copy_filename = new_filename or (
|
168
|
+
(original_meta.get("filename", "file") or "file") + "_copy"
|
169
|
+
)
|
170
|
+
copy_summary = summary or f"Copy of {original_meta.get('summary', 'artifact')}"
|
171
|
+
|
172
|
+
# Merge metadata
|
173
|
+
copy_meta = {**original_meta.get("meta", {})}
|
174
|
+
if new_meta:
|
175
|
+
copy_meta.update(new_meta)
|
176
|
+
|
177
|
+
# Add copy tracking
|
178
|
+
copy_meta["copied_from"] = artifact_id
|
179
|
+
copy_meta["copy_timestamp"] = datetime.utcnow().isoformat(timespec="seconds") + "Z"
|
180
|
+
copy_meta["copy_within_session"] = original_session
|
181
|
+
|
182
|
+
# Store the copy using core operations
|
183
|
+
from .core import CoreStorageOperations
|
184
|
+
core_ops = CoreStorageOperations(self._artifact_store)
|
185
|
+
|
186
|
+
new_artifact_id = await core_ops.store(
|
187
|
+
data=original_data,
|
188
|
+
mime=original_meta["mime"],
|
189
|
+
summary=copy_summary,
|
190
|
+
filename=copy_filename,
|
191
|
+
session_id=copy_session, # Always same session
|
192
|
+
meta=copy_meta
|
193
|
+
)
|
194
|
+
|
195
|
+
logger.info(
|
196
|
+
"File copied within session",
|
197
|
+
extra={
|
198
|
+
"source_artifact_id": artifact_id,
|
199
|
+
"new_artifact_id": new_artifact_id,
|
200
|
+
"session": copy_session,
|
201
|
+
"security_level": "same_session_only"
|
202
|
+
}
|
203
|
+
)
|
204
|
+
|
205
|
+
return new_artifact_id
|
206
|
+
|
207
|
+
except (ArtifactNotFoundError, ArtifactExpiredError):
|
208
|
+
raise
|
209
|
+
except Exception as e:
|
210
|
+
logger.error(
|
211
|
+
"File copy failed",
|
212
|
+
extra={
|
213
|
+
"artifact_id": artifact_id,
|
214
|
+
"new_filename": new_filename,
|
215
|
+
"target_session_id": target_session_id,
|
216
|
+
"error": str(e)
|
217
|
+
}
|
218
|
+
)
|
219
|
+
raise ProviderError(f"Copy operation failed: {e}") from e
|
220
|
+
|
221
|
+
async def read_file(
|
222
|
+
self,
|
223
|
+
artifact_id: str,
|
224
|
+
*,
|
225
|
+
encoding: str = "utf-8",
|
226
|
+
as_text: bool = True
|
227
|
+
) -> Union[str, bytes]:
|
228
|
+
"""
|
229
|
+
Read file content directly.
|
230
|
+
|
231
|
+
Note: This operation inherently respects session boundaries since
|
232
|
+
you can only read files you have artifact IDs for.
|
233
|
+
"""
|
234
|
+
self._check_closed()
|
235
|
+
|
236
|
+
try:
|
237
|
+
data = await self._retrieve_data(artifact_id)
|
238
|
+
|
239
|
+
if as_text:
|
240
|
+
try:
|
241
|
+
return data.decode(encoding)
|
242
|
+
except UnicodeDecodeError as e:
|
243
|
+
logger.warning(f"Failed to decode with {encoding}: {e}")
|
244
|
+
raise ProviderError(f"Cannot decode file as text with {encoding} encoding") from e
|
245
|
+
else:
|
246
|
+
return data
|
247
|
+
|
248
|
+
except (ArtifactNotFoundError, ArtifactExpiredError):
|
249
|
+
raise
|
250
|
+
except Exception as e:
|
251
|
+
logger.error(
|
252
|
+
"File read failed",
|
253
|
+
extra={"artifact_id": artifact_id, "error": str(e)}
|
254
|
+
)
|
255
|
+
raise ProviderError(f"Read operation failed: {e}") from e
|
256
|
+
|
257
|
+
async def write_file(
|
258
|
+
self,
|
259
|
+
content: Union[str, bytes],
|
260
|
+
*,
|
261
|
+
filename: str,
|
262
|
+
mime: str = "text/plain",
|
263
|
+
summary: str = "",
|
264
|
+
session_id: str = None,
|
265
|
+
meta: Dict[str, Any] = None,
|
266
|
+
encoding: str = "utf-8",
|
267
|
+
overwrite_artifact_id: str = None
|
268
|
+
) -> str:
|
269
|
+
"""
|
270
|
+
Write content to a new file or overwrite existing WITHIN THE SAME SESSION.
|
271
|
+
|
272
|
+
Parameters
|
273
|
+
----------
|
274
|
+
content : str or bytes
|
275
|
+
Content to write
|
276
|
+
filename : str
|
277
|
+
Filename for the new file
|
278
|
+
mime : str, optional
|
279
|
+
MIME type (default: text/plain)
|
280
|
+
summary : str, optional
|
281
|
+
File summary
|
282
|
+
session_id : str, optional
|
283
|
+
Session for the file
|
284
|
+
meta : dict, optional
|
285
|
+
Additional metadata
|
286
|
+
encoding : str, optional
|
287
|
+
Text encoding for string content (default: utf-8)
|
288
|
+
overwrite_artifact_id : str, optional
|
289
|
+
If provided, overwrite this existing artifact (must be in same session)
|
290
|
+
|
291
|
+
Returns
|
292
|
+
-------
|
293
|
+
str
|
294
|
+
Artifact ID of the written file
|
295
|
+
|
296
|
+
Raises
|
297
|
+
------
|
298
|
+
ArtifactStoreError
|
299
|
+
If trying to overwrite a file in a different session
|
300
|
+
"""
|
301
|
+
self._check_closed()
|
302
|
+
|
303
|
+
try:
|
304
|
+
# Convert content to bytes if needed
|
305
|
+
if isinstance(content, str):
|
306
|
+
data = content.encode(encoding)
|
307
|
+
else:
|
308
|
+
data = content
|
309
|
+
|
310
|
+
# Handle overwrite case with session security check
|
311
|
+
if overwrite_artifact_id:
|
312
|
+
try:
|
313
|
+
existing_meta = await self._get_record(overwrite_artifact_id)
|
314
|
+
existing_session = existing_meta.get("session_id")
|
315
|
+
|
316
|
+
# STRICT SECURITY: Can only overwrite files in the same session
|
317
|
+
if session_id and session_id != existing_session:
|
318
|
+
raise ArtifactStoreError(
|
319
|
+
f"Cross-session overwrite not permitted. Artifact {overwrite_artifact_id} "
|
320
|
+
f"belongs to session '{existing_session}', cannot overwrite from "
|
321
|
+
f"session '{session_id}'. Overwrite operations must be within the same session."
|
322
|
+
)
|
323
|
+
|
324
|
+
# Use the existing session if no session_id provided
|
325
|
+
session_id = session_id or existing_session
|
326
|
+
|
327
|
+
# Delete old version (within same session)
|
328
|
+
from .metadata import MetadataOperations
|
329
|
+
metadata_ops = MetadataOperations(self._artifact_store)
|
330
|
+
await metadata_ops.delete(overwrite_artifact_id)
|
331
|
+
|
332
|
+
except (ArtifactNotFoundError, ArtifactExpiredError):
|
333
|
+
pass # Original doesn't exist, proceed with new creation
|
334
|
+
|
335
|
+
# Store new content using core operations
|
336
|
+
from .core import CoreStorageOperations
|
337
|
+
core_ops = CoreStorageOperations(self._artifact_store)
|
338
|
+
|
339
|
+
write_meta = {**(meta or {})}
|
340
|
+
if overwrite_artifact_id:
|
341
|
+
write_meta["overwrote"] = overwrite_artifact_id
|
342
|
+
write_meta["overwrite_timestamp"] = datetime.utcnow().isoformat(timespec="seconds") + "Z"
|
343
|
+
write_meta["overwrite_within_session"] = session_id
|
344
|
+
|
345
|
+
artifact_id = await core_ops.store(
|
346
|
+
data=data,
|
347
|
+
mime=mime,
|
348
|
+
summary=summary or f"Written file: {filename}",
|
349
|
+
filename=filename,
|
350
|
+
session_id=session_id,
|
351
|
+
meta=write_meta
|
352
|
+
)
|
353
|
+
|
354
|
+
logger.info(
|
355
|
+
"File written successfully",
|
356
|
+
extra={
|
357
|
+
"artifact_id": artifact_id,
|
358
|
+
"filename": filename,
|
359
|
+
"bytes": len(data),
|
360
|
+
"overwrite": bool(overwrite_artifact_id),
|
361
|
+
"session_id": session_id,
|
362
|
+
"security_level": "session_isolated"
|
363
|
+
}
|
364
|
+
)
|
365
|
+
|
366
|
+
return artifact_id
|
367
|
+
|
368
|
+
except Exception as e:
|
369
|
+
logger.error(
|
370
|
+
"File write failed",
|
371
|
+
extra={
|
372
|
+
"filename": filename,
|
373
|
+
"overwrite_artifact_id": overwrite_artifact_id,
|
374
|
+
"session_id": session_id,
|
375
|
+
"error": str(e)
|
376
|
+
}
|
377
|
+
)
|
378
|
+
raise ProviderError(f"Write operation failed: {e}") from e
|
379
|
+
|
380
|
+
async def get_directory_contents(
|
381
|
+
self,
|
382
|
+
session_id: str,
|
383
|
+
directory_prefix: str = "",
|
384
|
+
limit: int = 100
|
385
|
+
) -> List[Dict[str, Any]]:
|
386
|
+
"""
|
387
|
+
List files in a directory-like structure within a session.
|
388
|
+
|
389
|
+
This operation is inherently session-safe since it requires
|
390
|
+
explicit session_id parameter.
|
391
|
+
"""
|
392
|
+
try:
|
393
|
+
from .metadata import MetadataOperations
|
394
|
+
metadata_ops = MetadataOperations(self._artifact_store)
|
395
|
+
return await metadata_ops.list_by_prefix(session_id, directory_prefix, limit)
|
396
|
+
except Exception as e:
|
397
|
+
logger.error(
|
398
|
+
"Directory listing failed",
|
399
|
+
extra={
|
400
|
+
"session_id": session_id,
|
401
|
+
"directory_prefix": directory_prefix,
|
402
|
+
"error": str(e)
|
403
|
+
}
|
404
|
+
)
|
405
|
+
raise ProviderError(f"Directory listing failed: {e}") from e
|
406
|
+
|
407
|
+
async def _retrieve_data(self, artifact_id: str) -> bytes:
|
408
|
+
"""Helper to retrieve artifact data using core operations."""
|
409
|
+
from .core import CoreStorageOperations
|
410
|
+
core_ops = CoreStorageOperations(self._artifact_store)
|
411
|
+
return await core_ops.retrieve(artifact_id)
|
412
|
+
|
413
|
+
# NEW: Session security validation helper
|
414
|
+
async def _validate_session_access(self, artifact_id: str, expected_session_id: str = None) -> Dict[str, Any]:
|
415
|
+
"""
|
416
|
+
Validate that an artifact belongs to the expected session.
|
417
|
+
|
418
|
+
Parameters
|
419
|
+
----------
|
420
|
+
artifact_id : str
|
421
|
+
Artifact to check
|
422
|
+
expected_session_id : str, optional
|
423
|
+
Expected session ID (if None, just returns the session)
|
424
|
+
|
425
|
+
Returns
|
426
|
+
-------
|
427
|
+
dict
|
428
|
+
Artifact metadata
|
429
|
+
|
430
|
+
Raises
|
431
|
+
------
|
432
|
+
ArtifactStoreError
|
433
|
+
If artifact belongs to different session
|
434
|
+
"""
|
435
|
+
record = await self._get_record(artifact_id)
|
436
|
+
actual_session = record.get("session_id")
|
437
|
+
|
438
|
+
if expected_session_id and actual_session != expected_session_id:
|
439
|
+
raise ArtifactStoreError(
|
440
|
+
f"Session access violation: Artifact {artifact_id} belongs to "
|
441
|
+
f"session '{actual_session}', but access was attempted from "
|
442
|
+
f"session '{expected_session_id}'. Cross-session access is not permitted."
|
443
|
+
)
|
444
|
+
|
445
|
+
return record
|
chuk_artifacts/store.py
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
|
-
# chuk_artifacts/store.py
|
2
|
+
# chuk_artifacts/store.py (ENHANCED)
|
3
3
|
"""
|
4
|
-
Asynchronous, object-store-backed artefact manager with
|
4
|
+
Asynchronous, object-store-backed artefact manager with MCP server support.
|
5
5
|
"""
|
6
6
|
|
7
7
|
from __future__ import annotations
|
8
8
|
|
9
9
|
import os, logging
|
10
|
-
from typing import Any, Dict, List, Callable, AsyncContextManager, Optional
|
10
|
+
from typing import Any, Dict, List, Callable, AsyncContextManager, Optional, Union
|
11
11
|
|
12
12
|
try:
|
13
13
|
import aioboto3
|
@@ -51,15 +51,9 @@ def _default_session_factory() -> Callable[[], AsyncContextManager]:
|
|
51
51
|
# ─────────────────────────────────────────────────────────────────────
|
52
52
|
class ArtifactStore:
|
53
53
|
"""
|
54
|
-
|
54
|
+
Asynchronous artifact storage with MCP server support.
|
55
55
|
|
56
|
-
|
57
|
-
Now properly delegates operations to specialized modules:
|
58
|
-
- CoreStorageOperations: store() and retrieve()
|
59
|
-
- PresignedURLOperations: presign*() methods
|
60
|
-
- MetadataOperations: metadata(), exists(), delete()
|
61
|
-
- BatchOperations: store_batch()
|
62
|
-
- AdminOperations: validate_configuration(), get_stats()
|
56
|
+
Enhanced with MCP-specific operations for file management within sessions.
|
63
57
|
"""
|
64
58
|
|
65
59
|
def __init__(
|
@@ -129,22 +123,23 @@ class ArtifactStore:
|
|
129
123
|
self._session_provider_name = session_provider or "memory"
|
130
124
|
self._closed = False
|
131
125
|
|
132
|
-
# Initialize operation modules
|
133
|
-
# FIXED: Now works correctly with the fixed BaseOperations
|
126
|
+
# Initialize operation modules
|
134
127
|
from .core import CoreStorageOperations
|
135
128
|
from .presigned import PresignedURLOperations
|
136
129
|
from .metadata import MetadataOperations
|
137
130
|
from .batch import BatchOperations
|
138
131
|
from .admin import AdminOperations
|
132
|
+
from .session_operations import SessionOperations
|
139
133
|
|
140
134
|
self._core = CoreStorageOperations(self)
|
141
135
|
self._presigned = PresignedURLOperations(self)
|
142
136
|
self._metadata = MetadataOperations(self)
|
143
137
|
self._batch = BatchOperations(self)
|
144
138
|
self._admin = AdminOperations(self)
|
139
|
+
self._session = SessionOperations(self)
|
145
140
|
|
146
141
|
logger.info(
|
147
|
-
"ArtifactStore initialized with
|
142
|
+
"ArtifactStore initialized with session operations support",
|
148
143
|
extra={
|
149
144
|
"bucket": bucket,
|
150
145
|
"storage_provider": self._storage_provider_name,
|
@@ -284,15 +279,20 @@ class ArtifactStore:
|
|
284
279
|
summary: str = None,
|
285
280
|
meta: Dict[str, Any] = None,
|
286
281
|
filename: str = None,
|
287
|
-
ttl: int = None
|
282
|
+
ttl: int = None,
|
283
|
+
# NEW: MCP-specific parameters
|
284
|
+
new_meta: Dict[str, Any] = None,
|
285
|
+
merge: bool = True
|
288
286
|
) -> Dict[str, Any]:
|
289
|
-
"""Update artifact metadata
|
287
|
+
"""Update artifact metadata with MCP server compatibility."""
|
290
288
|
return await self._metadata.update_metadata(
|
291
289
|
artifact_id,
|
292
290
|
summary=summary,
|
293
291
|
meta=meta,
|
294
292
|
filename=filename,
|
295
|
-
ttl=ttl
|
293
|
+
ttl=ttl,
|
294
|
+
new_meta=new_meta,
|
295
|
+
merge=merge
|
296
296
|
)
|
297
297
|
|
298
298
|
async def extend_ttl(self, artifact_id: str, additional_seconds: int) -> Dict[str, Any]:
|
@@ -303,6 +303,15 @@ class ArtifactStore:
|
|
303
303
|
"""List artifacts for a specific session."""
|
304
304
|
return await self._metadata.list_by_session(session_id, limit)
|
305
305
|
|
306
|
+
async def list_by_prefix(
|
307
|
+
self,
|
308
|
+
session_id: str,
|
309
|
+
prefix: str = "",
|
310
|
+
limit: int = 100
|
311
|
+
) -> List[Dict[str, Any]]:
|
312
|
+
"""List artifacts in a session with filename prefix filtering."""
|
313
|
+
return await self._metadata.list_by_prefix(session_id, prefix, limit)
|
314
|
+
|
306
315
|
# ─────────────────────────────────────────────────────────────────
|
307
316
|
# Batch operations (delegated to BatchOperations)
|
308
317
|
# ─────────────────────────────────────────────────────────────────
|
@@ -328,6 +337,95 @@ class ArtifactStore:
|
|
328
337
|
"""Get storage statistics."""
|
329
338
|
return await self._admin.get_stats()
|
330
339
|
|
340
|
+
# ─────────────────────────────────────────────────────────────────
|
341
|
+
# Session-based file operations (delegated to SessionOperations)
|
342
|
+
# ─────────────────────────────────────────────────────────────────
|
343
|
+
|
344
|
+
async def move_file(
|
345
|
+
self,
|
346
|
+
artifact_id: str,
|
347
|
+
*,
|
348
|
+
new_filename: str = None,
|
349
|
+
new_session_id: str = None,
|
350
|
+
new_meta: Dict[str, Any] = None
|
351
|
+
) -> Dict[str, Any]:
|
352
|
+
"""Move a file within sessions or rename it."""
|
353
|
+
return await self._session.move_file(
|
354
|
+
artifact_id,
|
355
|
+
new_filename=new_filename,
|
356
|
+
new_session_id=new_session_id,
|
357
|
+
new_meta=new_meta
|
358
|
+
)
|
359
|
+
|
360
|
+
async def copy_file(
|
361
|
+
self,
|
362
|
+
artifact_id: str,
|
363
|
+
*,
|
364
|
+
new_filename: str = None,
|
365
|
+
target_session_id: str = None,
|
366
|
+
new_meta: Dict[str, Any] = None,
|
367
|
+
summary: str = None
|
368
|
+
) -> str:
|
369
|
+
"""Copy a file within or across sessions."""
|
370
|
+
return await self._session.copy_file(
|
371
|
+
artifact_id,
|
372
|
+
new_filename=new_filename,
|
373
|
+
target_session_id=target_session_id,
|
374
|
+
new_meta=new_meta,
|
375
|
+
summary=summary
|
376
|
+
)
|
377
|
+
|
378
|
+
async def read_file(
|
379
|
+
self,
|
380
|
+
artifact_id: str,
|
381
|
+
*,
|
382
|
+
encoding: str = "utf-8",
|
383
|
+
as_text: bool = True
|
384
|
+
) -> Union[str, bytes]:
|
385
|
+
"""Read file content directly."""
|
386
|
+
return await self._session.read_file(
|
387
|
+
artifact_id,
|
388
|
+
encoding=encoding,
|
389
|
+
as_text=as_text
|
390
|
+
)
|
391
|
+
|
392
|
+
async def write_file(
|
393
|
+
self,
|
394
|
+
content: Union[str, bytes],
|
395
|
+
*,
|
396
|
+
filename: str,
|
397
|
+
mime: str = "text/plain",
|
398
|
+
summary: str = "",
|
399
|
+
session_id: str = None,
|
400
|
+
meta: Dict[str, Any] = None,
|
401
|
+
encoding: str = "utf-8",
|
402
|
+
overwrite_artifact_id: str = None
|
403
|
+
) -> str:
|
404
|
+
"""Write content to a new file or overwrite existing."""
|
405
|
+
return await self._session.write_file(
|
406
|
+
content,
|
407
|
+
filename=filename,
|
408
|
+
mime=mime,
|
409
|
+
summary=summary,
|
410
|
+
session_id=session_id,
|
411
|
+
meta=meta,
|
412
|
+
encoding=encoding,
|
413
|
+
overwrite_artifact_id=overwrite_artifact_id
|
414
|
+
)
|
415
|
+
|
416
|
+
async def get_directory_contents(
|
417
|
+
self,
|
418
|
+
session_id: str,
|
419
|
+
directory_prefix: str = "",
|
420
|
+
limit: int = 100
|
421
|
+
) -> List[Dict[str, Any]]:
|
422
|
+
"""List files in a directory-like structure within a session."""
|
423
|
+
return await self._session.get_directory_contents(
|
424
|
+
session_id,
|
425
|
+
directory_prefix,
|
426
|
+
limit
|
427
|
+
)
|
428
|
+
|
331
429
|
# ─────────────────────────────────────────────────────────────────
|
332
430
|
# Resource management
|
333
431
|
# ─────────────────────────────────────────────────────────────────
|