Skip to content

Commit 2a329bd

Browse files
committed
add downlod endpoint
1 parent cc3128c commit 2a329bd

File tree

4 files changed

+71
-17
lines changed

4 files changed

+71
-17
lines changed

src/server/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ class IngestSuccessResponse(BaseModel):
6969
Short form of repository URL (user/repo).
7070
summary : str
7171
Summary of the ingestion process including token estimates.
72+
ingest_id : str
73+
Ingestion id used to download full context.
7274
tree : str
7375
File tree structure of the repository.
7476
content : str
@@ -85,6 +87,7 @@ class IngestSuccessResponse(BaseModel):
8587
repo_url: str = Field(..., description="Original repository URL")
8688
short_repo_url: str = Field(..., description="Short repository URL (user/repo)")
8789
summary: str = Field(..., description="Ingestion summary with token estimates")
90+
ingest_id: str = Field(..., description="Ingestion id used to download full context")
8891
tree: str = Field(..., description="File tree structure")
8992
content: str = Field(..., description="Processed file content")
9093
default_max_file_size: int = Field(..., description="File size slider position used")

src/server/query_processor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ async def process_query(
122122
repo_url=input_text,
123123
short_repo_url=short_repo_url,
124124
summary=summary,
125+
ingest_id=query.id,
125126
tree=tree,
126127
content=content,
127128
default_max_file_size=slider_position,

src/server/routers/ingest.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"""Ingest endpoint for the API."""
22

3-
from fastapi import APIRouter, Request, status
4-
from fastapi.responses import JSONResponse
3+
from fastapi import APIRouter, HTTPException, Request, status
4+
from fastapi.responses import FileResponse, JSONResponse
55

6+
from gitingest.config import TMP_BASE_PATH
67
from server.models import IngestErrorResponse, IngestRequest, IngestSuccessResponse
78
from server.query_processor import process_query
89
from server.server_config import MAX_DISPLAY_SIZE
@@ -157,3 +158,46 @@ async def api_ingest_get(
157158
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
158159
content=error_response.model_dump(),
159160
)
161+
162+
163+
@router.get("/api/download/file/{ingest_id}", response_class=FileResponse)
164+
async def download_ingest(ingest_id: str) -> FileResponse:
165+
"""Return the first ``*.txt`` file produced for ``ingest_id`` as a download.
166+
167+
Parameters
168+
----------
169+
ingest_id : str
170+
Identifier that the ingest step emitted (also the directory name that stores the artefacts).
171+
172+
Returns
173+
-------
174+
FileResponse
175+
Streamed response with media type ``text/plain`` that prompts the browser to download the file.
176+
177+
Raises
178+
------
179+
HTTPException
180+
**404** - digest directory is missing or contains no ``*.txt`` file.
181+
**403** - the process lacks permission to read the directory or file.
182+
183+
"""
184+
directory = TMP_BASE_PATH / ingest_id
185+
186+
if not directory.is_dir():
187+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Digest {ingest_id!r} not found")
188+
189+
try:
190+
first_txt_file = next(directory.glob("*.txt"))
191+
except StopIteration as exc:
192+
raise HTTPException(
193+
status_code=status.HTTP_404_NOT_FOUND,
194+
detail=f"No .txt file found for digest {ingest_id!r}",
195+
) from exc
196+
197+
try:
198+
return FileResponse(path=first_txt_file, media_type="text/plain", filename=first_txt_file.name)
199+
except PermissionError as exc:
200+
raise HTTPException(
201+
status_code=status.HTTP_403_FORBIDDEN,
202+
detail=f"Permission denied for {first_txt_file}",
203+
) from exc

src/static/js/utils.js

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ function handleSuccessfulResponse(data) {
172172
// Show results section
173173
showResults();
174174

175+
// Store the ingest_id for download functionality
176+
window.currentIngestId = data.ingest_id;
177+
175178
// Set plain text content for summary, tree, and content
176179
document.getElementById('result-summary').value = data.summary || '';
177180
document.getElementById('directory-structure-content').value = data.tree || '';
@@ -268,33 +271,36 @@ function copyFullDigest() {
268271
}
269272

270273
function downloadFullDigest() {
271-
const summary = document.getElementById('result-summary').value;
272-
const directoryStructure = document.getElementById('directory-structure-content').value;
273-
const filesContent = document.querySelector('.result-text').value;
274+
// Check if we have an ingest_id
275+
if (!window.currentIngestId) {
276+
console.error('No ingest_id available for download');
274277

275-
// Create the full content with all three sections
276-
const fullContent = `${summary}\n${directoryStructure}\n${filesContent}`;
278+
return;
279+
}
277280

278-
// Create a blob with the content
279-
const blob = new Blob([fullContent], { type: 'text/plain' });
281+
// Show feedback on the button
282+
const button = document.querySelector('[onclick="downloadFullDigest()"]');
283+
const originalText = button.innerHTML;
280284

281-
// Create a download link
282-
const url = window.URL.createObjectURL(blob);
285+
button.innerHTML = `
286+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
287+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
288+
</svg>
289+
Downloading...
290+
`;
291+
292+
// Create a download link to the server endpoint
283293
const a = document.createElement('a');
284294

285-
a.href = url;
295+
a.href = `/api/download/file/${window.currentIngestId}`;
286296
a.download = 'digest.txt';
287297
document.body.appendChild(a);
288298
a.click();
289299

290300
// Clean up
291-
window.URL.revokeObjectURL(url);
292301
document.body.removeChild(a);
293302

294-
// Show feedback on the button
295-
const button = document.querySelector('[onclick="downloadFullDigest()"]');
296-
const originalText = button.innerHTML;
297-
303+
// Update button to show success
298304
button.innerHTML = `
299305
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
300306
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>

0 commit comments

Comments
 (0)