mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Compare commits
2 Commits
combined-p
...
lluisagust
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47f22e4923 | ||
|
|
21c3a98c5a |
@@ -131,6 +131,54 @@ class StorageUsageResponse(BaseModel):
|
||||
file_count: int
|
||||
|
||||
|
||||
class DownloadUrlResponse(BaseModel):
|
||||
url: str
|
||||
direct: bool # True = browser can fetch URL directly (signed GCS URL)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/files/{file_id}/download-url",
|
||||
summary="Get download URL for a file",
|
||||
)
|
||||
async def get_file_download_url(
|
||||
user_id: Annotated[str, fastapi.Security(get_user_id)],
|
||||
file_id: str,
|
||||
) -> DownloadUrlResponse:
|
||||
"""
|
||||
Return a download URL for a workspace file.
|
||||
|
||||
For GCS storage: returns a time-limited signed URL the browser can fetch directly.
|
||||
For local storage: returns the API download path (must still be proxied).
|
||||
"""
|
||||
workspace = await get_workspace(user_id)
|
||||
if workspace is None:
|
||||
raise fastapi.HTTPException(status_code=404, detail="Workspace not found")
|
||||
|
||||
file = await get_workspace_file(file_id, workspace.id)
|
||||
if file is None:
|
||||
raise fastapi.HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
storage = await get_workspace_storage()
|
||||
|
||||
if file.storage_path.startswith("local://"):
|
||||
return DownloadUrlResponse(
|
||||
url=f"/api/workspace/files/{file_id}/download",
|
||||
direct=False,
|
||||
)
|
||||
|
||||
# GCS — try to generate signed URL
|
||||
try:
|
||||
url = await storage.get_download_url(file.storage_path, expires_in=300)
|
||||
if url.startswith("/api/"):
|
||||
return DownloadUrlResponse(url=url, direct=False)
|
||||
return DownloadUrlResponse(url=url, direct=True)
|
||||
except Exception:
|
||||
return DownloadUrlResponse(
|
||||
url=f"/api/workspace/files/{file_id}/download",
|
||||
direct=False,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/files/{file_id}/download",
|
||||
summary="Download file by ID",
|
||||
|
||||
@@ -31,11 +31,11 @@ function isWorkspaceDownloadRequest(path: string[]): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle workspace file download requests with proper binary response streaming.
|
||||
* Handle workspace file download requests using signed URL redirect or full buffering.
|
||||
*/
|
||||
async function handleWorkspaceDownload(
|
||||
req: NextRequest,
|
||||
backendUrl: string,
|
||||
path: string[],
|
||||
): Promise<NextResponse> {
|
||||
const token = await getServerAuthToken();
|
||||
|
||||
@@ -44,40 +44,64 @@ async function handleWorkspaceDownload(
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(backendUrl, {
|
||||
// Build the download-url endpoint path (replace last segment)
|
||||
const urlPath = [...path];
|
||||
urlPath[urlPath.length - 1] = "download-url";
|
||||
const downloadUrlEndpoint = buildBackendUrl(urlPath, "");
|
||||
|
||||
// Ask backend for signed URL
|
||||
const urlResponse = await fetch(downloadUrlEndpoint, {
|
||||
method: "GET",
|
||||
headers,
|
||||
redirect: "follow", // Follow redirects to signed URLs
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (!urlResponse.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to download file: ${response.statusText}` },
|
||||
{ status: response.status },
|
||||
{ error: `Failed to get download URL: ${urlResponse.statusText}` },
|
||||
{ status: urlResponse.status },
|
||||
);
|
||||
}
|
||||
|
||||
// Get the content type from the backend response
|
||||
const contentType =
|
||||
response.headers.get("Content-Type") || "application/octet-stream";
|
||||
const contentDisposition = response.headers.get("Content-Disposition");
|
||||
const { url, direct } = (await urlResponse.json()) as {
|
||||
url: string;
|
||||
direct: boolean;
|
||||
};
|
||||
|
||||
// Direct URL (GCS signed) — redirect browser to fetch directly from GCS
|
||||
if (direct) {
|
||||
return NextResponse.redirect(url, 302);
|
||||
}
|
||||
|
||||
// Non-direct (local storage) — proxy with full buffering to avoid truncation
|
||||
const backendUrl = buildBackendUrl(path, new URL(req.url).search);
|
||||
const fileResponse = await fetch(backendUrl, {
|
||||
method: "GET",
|
||||
headers,
|
||||
redirect: "follow",
|
||||
});
|
||||
|
||||
if (!fileResponse.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to download file: ${fileResponse.statusText}` },
|
||||
{ status: fileResponse.status },
|
||||
);
|
||||
}
|
||||
|
||||
const buffer = await fileResponse.arrayBuffer();
|
||||
const contentType =
|
||||
fileResponse.headers.get("Content-Type") || "application/octet-stream";
|
||||
const contentDisposition = fileResponse.headers.get("Content-Disposition");
|
||||
|
||||
// Stream the response body
|
||||
const responseHeaders: Record<string, string> = {
|
||||
"Content-Type": contentType,
|
||||
"Content-Length": String(buffer.byteLength),
|
||||
};
|
||||
|
||||
if (contentDisposition) {
|
||||
responseHeaders["Content-Disposition"] = contentDisposition;
|
||||
}
|
||||
|
||||
const contentLength = response.headers.get("Content-Length");
|
||||
if (contentLength) {
|
||||
responseHeaders["Content-Length"] = contentLength;
|
||||
}
|
||||
|
||||
// Stream the response body directly instead of buffering in memory
|
||||
return new NextResponse(response.body, {
|
||||
return new NextResponse(buffer, {
|
||||
status: 200,
|
||||
headers: responseHeaders,
|
||||
});
|
||||
@@ -250,7 +274,7 @@ async function handler(
|
||||
try {
|
||||
// Handle workspace file downloads separately (binary response)
|
||||
if (method === "GET" && isWorkspaceDownloadRequest(path)) {
|
||||
return await handleWorkspaceDownload(req, backendUrl);
|
||||
return await handleWorkspaceDownload(req, path);
|
||||
}
|
||||
|
||||
if (method === "GET" || method === "DELETE") {
|
||||
|
||||
Reference in New Issue
Block a user