Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/airflow/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "carrot-airflow"
version = "4.2.2"
version = "4.2.3"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
2 changes: 1 addition & 1 deletion app/api/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "api"
version = "4.2.2"
version = "4.2.3"
description = "Web app for Carrot-Mapper"

authors = [
Expand Down
21 changes: 9 additions & 12 deletions app/next-client-app/api/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ const fetchKeys = {

export async function list(
scan_report_id: number,
filter: string | undefined
filter: string | undefined,
): Promise<PaginatedResponse<FileDownload> | null> {
try {
return await request<PaginatedResponse<FileDownload>>(
fetchKeys.list(scan_report_id, filter)
fetchKeys.list(scan_report_id, filter),
);
} catch (error) {
return null;
Expand All @@ -29,7 +29,7 @@ export async function list(

export async function requestFile(
scan_report_id: number,
file_type: FileTypeFormat
file_type: FileTypeFormat,
): Promise<{ success: boolean; errorMessage?: string }> {
try {
await request(fetchKeys.requestFile(scan_report_id), {
Expand All @@ -51,16 +51,17 @@ export async function requestFile(

export async function downloadFile(
scan_report_id: number,
file_id?: number
file_id?: number,
): Promise<{
success: boolean;
errorMessage?: string;
data?: any;
blob?: Blob;
downloadUrl?: string;
}> {
try {
if (!file_id) {
// The case of SR exporting
// The case of SR exporting - keep existing logic for now
const response = await request(fetchKeys.downloadFile(scan_report_id), {
download: true,
});
Expand All @@ -70,13 +71,9 @@ export async function downloadFile(

return { success: true, data: base64String };
} else {
// The case of mapping rules file downloading - use streaming for all file types
const response = await request(
fetchKeys.downloadFile(scan_report_id, file_id),
{ download: true }
);
// Return the blob directly
return { success: true, blob: response as Blob };
// The case of mapping rules file downloading - use streaming API route
const downloadUrl = `/api/download/${scan_report_id}/${file_id}`;
return { success: true, downloadUrl };
}
} catch (error: any) {
return { success: false, errorMessage: error.message };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const columns: ColumnDef<FileDownload>[] = [
return format(created_at, "d MMM HH:mm");
},
enableHiding: true,
enableSorting: true
enableSorting: true,
},
{
id: "User",
Expand All @@ -43,7 +43,7 @@ export const columns: ColumnDef<FileDownload>[] = [
return <>{user.username}</>;
},
enableHiding: true,
enableSorting: false
enableSorting: false,
},
{
id: "Type",
Expand All @@ -60,7 +60,7 @@ export const columns: ColumnDef<FileDownload>[] = [
return <Badge variant="outline">{file_type.display_name}</Badge>;
},
enableHiding: true,
enableSorting: false
enableSorting: false,
},
{
id: "Download",
Expand All @@ -69,22 +69,28 @@ export const columns: ColumnDef<FileDownload>[] = [
const { id, scan_report, name } = row.original;
const handleDownload = async () => {
const response = await downloadFile(scan_report, id);
if (response.success && response.blob) {
// Use the blob directly
if (response.success && response.downloadUrl) {
// Use the streaming download URL
window.open(response.downloadUrl, "_blank");
} else if (response.success && response.blob) {
// Fallback for SR exporting (blob approach)
saveAs(response.blob, name);
} else {
toast.error(
`Error downloading file: ${(response.errorMessage as any).message}`
`Error downloading file: ${response.errorMessage || "Unknown error"}`,
);
}
};
return (
<Button variant={"outline"} onClick={handleDownload}>
Download <Download />
Download{" "}
<a href={`/api/download/${scan_report}/${id}`} target="_blank">
<Download />
</a>
</Button>
);
},
enableHiding: true,
enableSorting: false
}
enableSorting: false,
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { options as authOptions } from "@/auth/options";

export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ scan_report_id: string; file_id: string }> },
) {
try {
const { scan_report_id, file_id } = await params;

// Get the session for authentication
const session = await getServerSession(authOptions);
const token = session?.access_token;

if (!token) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

// Construct the Django backend URL
const backendUrl = `${process.env.BACKEND_URL}/api/v2/scanreports/${scan_report_id}/rules/downloads/${file_id}/`;

// Forward the request to Django backend
const backendResponse = await fetch(backendUrl, {
method: "GET",
headers: {
Authorization: `JWT ${token}`,
// Forward any relevant headers from the original request
...(request.headers.get("user-agent") && {
"User-Agent": request.headers.get("user-agent")!,
}),
},
});

// If the backend request failed, return the same status
if (!backendResponse.ok) {
return new NextResponse(backendResponse.body, {
status: backendResponse.status,
statusText: backendResponse.statusText,
});
}

// Get the content type and disposition from the backend response
const contentType = backendResponse.headers.get("content-type");
const contentDisposition = backendResponse.headers.get(
"content-disposition",
);
const contentLength = backendResponse.headers.get("content-length");

// Create headers for the Next.js response
const responseHeaders = new Headers();

if (contentType) {
responseHeaders.set("content-type", contentType);
}

if (contentDisposition) {
responseHeaders.set("content-disposition", contentDisposition);
}

if (contentLength) {
responseHeaders.set("content-length", contentLength);
}

// Set cache headers to prevent caching of file downloads
responseHeaders.set("cache-control", "no-cache, no-store, must-revalidate");
responseHeaders.set("pragma", "no-cache");
responseHeaders.set("expires", "0");

// Stream the response body directly to the client
return new NextResponse(backendResponse.body, {
status: 200,
headers: responseHeaders,
});
} catch (error) {
console.error("Error in download proxy:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
2 changes: 1 addition & 1 deletion app/next-client-app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "next-client-app",
"version": "4.2.2",
"version": "4.2.3",
"private": true,
"scripts": {
"dev": "next dev --turbo",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "carrot-mapper"
version = "4.2.2"
version = "4.2.3"
description = ""
authors = [
{ name = "Sam Cox", email = "sam.cox@nottingham.ac.uk" },
Expand Down