540 persistent data on assessments (#578)
* Add file upload functionality to data store with MinIO integration * Remove dependency restriction for file upload endpoint
taylor_socfortress committed
Dec 29, 2025 at 16:16 UTC
b21362716d6fc639b4cec5cb292445f9a7a77835
3 files changed
+134
backend/app/data_store/data_store_operations.py
+62
@@ -115,6 +115,68 @@ async def list_case_report_template_data_store_files(bucket_name: Optional[str]
115
return objects_list
116
117
118
+async def upload_file_to_datastore(
119
+ file: UploadFile,
120
+ bucket_name: str,
121
+ object_name: str,
122
+) -> dict:
123
+ """
124
+ Upload a file to MinIO data store.
125
+
126
+ Args:
127
+ file: The file to upload
128
+ bucket_name: The name of the bucket
129
+ object_name: The object path/name within the bucket
130
+
131
+ Returns:
132
+ dict: Upload details including object_key, file_size, file_hash, and content_type
133
+ """
134
+ client = await create_session()
135
+
136
+ # Create bucket if it doesn't exist
137
+ await create_bucket_if_not_exists(bucket_name)
138
+
139
+ logger.info(f"Uploading file {file.filename} to bucket {bucket_name} as {object_name}")
140
+
141
+ # Define the temporary file path
142
+ temp_file_path = os.path.join(os.getcwd(), file.filename)
143
+
144
+ # Save the file to the temporary location and calculate hash
145
+ sha256_hash = hashlib.sha256()
146
+ async with aiofiles.open(temp_file_path, "wb") as out_file:
147
+ content = await file.read()
148
+ await out_file.write(content)
149
+ sha256_hash.update(content)
150
+
151
+ file_hash = sha256_hash.hexdigest()
152
+ file_size = os.path.getsize(temp_file_path)
153
+
154
+ # Determine content type
155
+ content_type = file.content_type or "application/octet-stream"
156
+
157
+ # Upload the file to MinIO
158
+ await client.fput_object(
159
+ bucket_name=bucket_name,
160
+ object_name=object_name,
161
+ file_path=temp_file_path,
162
+ content_type=content_type,
163
+ )
164
+
165
+ # Remove the temporary file after upload
166
+ os.remove(temp_file_path)
167
+
168
+ logger.info(f"Successfully uploaded {file.filename} ({file_size} bytes) to {bucket_name}/{object_name}")
169
+
170
+ return {
171
+ "bucket_name": bucket_name,
172
+ "object_key": object_name,
173
+ "file_name": file.filename,
174
+ "file_size": file_size,
175
+ "file_hash": file_hash,
176
+ "content_type": content_type,
177
+ }
178
+
179
+
180
async def create_buckets() -> None:
181
await create_bucket_if_not_exists("copilot-cases")
182
backend/app/data_store/data_store_routes.py
+61
@@ -3,7 +3,10 @@ from typing import Optional
3
4
from fastapi import APIRouter
5
from fastapi import Depends
6
+from fastapi import File
7
+from fastapi import Form
8
from fastapi import HTTPException
9
+from fastapi import UploadFile
10
from fastapi import status
11
from fastapi.responses import StreamingResponse
12
from loguru import logger
@@ -15,15 +18,73 @@ from sqlalchemy.orm import selectinload
18
from app.auth.utils import AuthHandler
19
from app.data_store.data_store_operations import delete_agent_artifact_file
20
from app.data_store.data_store_operations import download_agent_artifact_file
21
+from app.data_store.data_store_operations import upload_file_to_datastore
22
from app.data_store.data_store_schema import AgentDataStoreData
23
from app.data_store.data_store_schema import AgentDataStoreListResponse
24
from app.data_store.data_store_schema import AgentDataStoreResponse
25
+from app.data_store.data_store_schema import FileUploadResponse
26
from app.db.db_session import get_db
27
from app.db.universal_models import AgentDataStore
28
29
agent_data_store_router = APIRouter()
30
31
32
+@agent_data_store_router.post(
33
+ "/upload",
34
+ response_model=FileUploadResponse,
35
+ description="Upload a file to the data store",
36
+)
37
+async def upload_file(
38
+ file: UploadFile = File(...),
39
+ bucket_name: str = Form(...),
40
+ object_name: str = Form(...),
41
+) -> FileUploadResponse:
42
+ """
43
+ Upload a file to the specified bucket and object path in the data store.
44
+
45
+ Args:
46
+ file: The file to upload
47
+ bucket_name: The name of the bucket to upload to
48
+ object_name: The object path/name within the bucket (e.g., "folder/subfolder/file.txt")
49
+
50
+ Returns:
51
+ FileUploadResponse with upload details
52
+ """
53
+ try:
54
+ logger.info(f"Uploading file {file.filename} to bucket {bucket_name} as {object_name}")
55
+
56
+ if not file.filename:
57
+ raise HTTPException(
58
+ status_code=status.HTTP_400_BAD_REQUEST,
59
+ detail="File name is required",
60
+ )
61
+
62
+ # Upload the file to MinIO
63
+ upload_result = await upload_file_to_datastore(
64
+ file=file,
65
+ bucket_name=bucket_name,
66
+ object_name=object_name,
67
+ )
68
+
69
+ return FileUploadResponse(
70
+ success=True,
71
+ message=f"File {file.filename} uploaded successfully",
72
+ bucket_name=upload_result["bucket_name"],
73
+ object_key=upload_result["object_key"],
74
+ file_name=upload_result["file_name"],
75
+ file_size=upload_result["file_size"],
76
+ file_hash=upload_result["file_hash"],
77
+ content_type=upload_result.get("content_type"),
78
+ )
79
+
80
+ except Exception as e:
81
+ logger.error(f"Failed to upload file: {e}")
82
+ raise HTTPException(
83
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
84
+ detail=f"Failed to upload file: {str(e)}",
85
+ )
86
+
87
+
88
@agent_data_store_router.get(
89
"/agent/{agent_id}/artifacts",
90
response_model=AgentDataStoreListResponse,
backend/app/data_store/data_store_schema.py
+11
@@ -5,6 +5,17 @@ from pydantic import BaseModel
5
from pydantic import Field
6
7
8
+class FileUploadResponse(BaseModel):
9
+ success: bool
10
+ message: str
11
+ bucket_name: str
12
+ object_key: str
13
+ file_name: str
14
+ file_size: int
15
+ file_hash: str
16
+ content_type: Optional[str] = None
17
+
18
+
19
class CaseDataStoreCreation(BaseModel):
20
case_id: int
21
bucket_name: str = Field(max_length=255)