|
2 | 2 |
|
3 | 3 | import tempfile |
4 | 4 | from pathlib import Path |
| 5 | +from typing import Annotated |
5 | 6 |
|
6 | | -from fastapi import APIRouter, HTTPException, BackgroundTasks |
7 | | -from fastapi.responses import FileResponse |
| 7 | +from fastapi import APIRouter, HTTPException, BackgroundTasks, Body |
| 8 | +from fastapi.responses import FileResponse, JSONResponse |
8 | 9 | from loguru import logger |
9 | 10 |
|
10 | 11 | from basic_memory.deps import ( |
|
13 | 14 | SearchServiceDep, |
14 | 15 | EntityServiceDep, |
15 | 16 | FileServiceDep, |
| 17 | + EntityRepositoryDep, |
16 | 18 | ) |
17 | 19 | from basic_memory.repository.search_repository import SearchIndexRow |
18 | 20 | from basic_memory.schemas.memory import normalize_memory_url |
19 | 21 | from basic_memory.schemas.search import SearchQuery, SearchItemType |
| 22 | +from basic_memory.models.knowledge import Entity as EntityModel |
| 23 | +from datetime import datetime |
20 | 24 |
|
21 | 25 | router = APIRouter(prefix="/resource", tags=["resources"]) |
22 | 26 |
|
@@ -122,3 +126,102 @@ def cleanup_temp_file(file_path: str): |
122 | 126 | logger.debug(f"Temporary file deleted: {file_path}") |
123 | 127 | except Exception as e: # pragma: no cover |
124 | 128 | logger.error(f"Error deleting temporary file {file_path}: {e}") |
| 129 | + |
| 130 | + |
| 131 | +@router.put("/{file_path:path}") |
| 132 | +async def write_resource( |
| 133 | + config: ProjectConfigDep, |
| 134 | + file_service: FileServiceDep, |
| 135 | + entity_repository: EntityRepositoryDep, |
| 136 | + search_service: SearchServiceDep, |
| 137 | + file_path: str, |
| 138 | + content: Annotated[str, Body()], |
| 139 | +) -> JSONResponse: |
| 140 | + """Write content to a file in the project. |
| 141 | +
|
| 142 | + This endpoint allows writing content directly to a file in the project. |
| 143 | + Also creates an entity record and indexes the file for search. |
| 144 | +
|
| 145 | + Args: |
| 146 | + file_path: Path to write to, relative to project root |
| 147 | + request: Contains the content to write |
| 148 | +
|
| 149 | + Returns: |
| 150 | + JSON response with file information |
| 151 | + """ |
| 152 | + try: |
| 153 | + # Get content from request body |
| 154 | + |
| 155 | + # Ensure it's UTF-8 string content |
| 156 | + if isinstance(content, bytes): # pragma: no cover |
| 157 | + content_str = content.decode("utf-8") |
| 158 | + else: |
| 159 | + content_str = str(content) |
| 160 | + |
| 161 | + # Get full file path |
| 162 | + full_path = Path(f"{config.home}/{file_path}") |
| 163 | + |
| 164 | + # Ensure parent directory exists |
| 165 | + full_path.parent.mkdir(parents=True, exist_ok=True) |
| 166 | + |
| 167 | + # Write content to file |
| 168 | + checksum = await file_service.write_file(full_path, content_str) |
| 169 | + |
| 170 | + # Get file info |
| 171 | + file_stats = file_service.file_stats(full_path) |
| 172 | + |
| 173 | + # Determine file details |
| 174 | + file_name = Path(file_path).name |
| 175 | + content_type = file_service.content_type(full_path) |
| 176 | + |
| 177 | + entity_type = "canvas" if file_path.endswith(".canvas") else "file" |
| 178 | + |
| 179 | + # Check if entity already exists |
| 180 | + existing_entity = await entity_repository.get_by_file_path(file_path) |
| 181 | + |
| 182 | + if existing_entity: |
| 183 | + # Update existing entity |
| 184 | + entity = await entity_repository.update( |
| 185 | + existing_entity.id, |
| 186 | + { |
| 187 | + "title": file_name, |
| 188 | + "entity_type": entity_type, |
| 189 | + "content_type": content_type, |
| 190 | + "file_path": file_path, |
| 191 | + "checksum": checksum, |
| 192 | + "updated_at": datetime.fromtimestamp(file_stats.st_mtime), |
| 193 | + }, |
| 194 | + ) |
| 195 | + assert entity is not None, "Entity should be returned after update" |
| 196 | + status_code = 200 |
| 197 | + else: |
| 198 | + # Create a new entity model |
| 199 | + entity = EntityModel( |
| 200 | + title=file_name, |
| 201 | + entity_type=entity_type, |
| 202 | + content_type=content_type, |
| 203 | + file_path=file_path, |
| 204 | + checksum=checksum, |
| 205 | + created_at=datetime.fromtimestamp(file_stats.st_ctime), |
| 206 | + updated_at=datetime.fromtimestamp(file_stats.st_mtime), |
| 207 | + ) |
| 208 | + entity = await entity_repository.add(entity) |
| 209 | + status_code = 201 |
| 210 | + |
| 211 | + # Index the file for search |
| 212 | + await search_service.index_entity(entity) |
| 213 | + |
| 214 | + # Return success response |
| 215 | + return JSONResponse( |
| 216 | + status_code=status_code, |
| 217 | + content={ |
| 218 | + "file_path": file_path, |
| 219 | + "checksum": checksum, |
| 220 | + "size": file_stats.st_size, |
| 221 | + "created_at": file_stats.st_ctime, |
| 222 | + "modified_at": file_stats.st_mtime, |
| 223 | + }, |
| 224 | + ) |
| 225 | + except Exception as e: # pragma: no cover |
| 226 | + logger.error(f"Error writing resource {file_path}: {e}") |
| 227 | + raise HTTPException(status_code=500, detail=f"Failed to write resource: {str(e)}") |
0 commit comments