diff --git a/src/app/api/github/download/route.ts b/src/app/api/github/download/route.ts new file mode 100644 index 00000000..1578c778 --- /dev/null +++ b/src/app/api/github/download/route.ts @@ -0,0 +1,54 @@ +// src/app/api/github/download/route.ts +'use server'; +import { NextRequest, NextResponse } from 'next/server'; +import { getToken } from 'next-auth/jwt'; +import { GITHUB_API_URL } from '@/types/const'; +import { getGitHubUsername } from '@/utils/github'; + +const UPSTREAM_REPO_NAME = process.env.NEXT_PUBLIC_TAXONOMY_REPO!; + +export async function POST(req: NextRequest) { + const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET! }); + + if (!token || !token.accessToken) { + console.error('Unauthorized: Missing or invalid access token'); + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const githubToken = token.accessToken as string; + const headers = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${githubToken}`, + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28' + }; + + const githubUsername = await getGitHubUsername(headers); + try { + const { branchName } = await req.json(); + + if (!branchName || typeof branchName !== 'string') { + return NextResponse.json({ error: 'contribution branch does not exist on remote taxonomy.' }, { status: 400 }); + } + + const tarballUrl = `${GITHUB_API_URL}/repos/${githubUsername}/${UPSTREAM_REPO_NAME}/tarball/${branchName}`; + const tarballRes = await fetch(tarballUrl, { + headers: headers + }); + + if (!tarballRes.ok) { + return NextResponse.json({ error: 'Failed to download taxonomy for the contribution.' }, { status: 500 }); + } + + return new NextResponse(tarballRes.body, { + headers: { + 'Content-Type': 'application/gzip', + 'Content-Disposition': `attachment`, + 'Cache-Control': 'no-store' + } + }); + } catch (error) { + console.error('failed to download taxonomy for the contribution:', error); + return NextResponse.json({ error: error }, { status: 500 }); + } +} diff --git a/src/app/api/download/route.ts b/src/app/api/native/download/route.ts similarity index 61% rename from src/app/api/download/route.ts rename to src/app/api/native/download/route.ts index 667abd58..e22587c2 100644 --- a/src/app/api/download/route.ts +++ b/src/app/api/native/download/route.ts @@ -1,31 +1,37 @@ -// src/app/api/download/route.ts +// src/app/api/native/download/route.ts 'use server'; -import { NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server'; import { spawn } from 'child_process'; import fs from 'fs'; import path from 'path'; +import * as git from 'isomorphic-git'; -// GET handler now takes the Request so we can watch for aborts -export async function GET(request: Request) { - const rootDir = process.env.NEXT_PUBLIC_TAXONOMY_ROOT_DIR; +const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || `${process.env.HOME}/.instructlab-ui`; + +export async function POST(req: NextRequest) { + const rootDir = LOCAL_TAXONOMY_ROOT_DIR; if (!rootDir) { - return NextResponse.json({ error: 'NEXT_PUBLIC_TAXONOMY_ROOT_DIR is not configured' }, { status: 500 }); + return NextResponse.json({ error: 'Failed to find the local taxonomy that contains the contribution.' }, { status: 500 }); } + const { branchName } = await req.json(); const taxonomyDir = path.join(rootDir, 'taxonomy'); try { await fs.promises.access(taxonomyDir, fs.constants.R_OK); } catch { - return NextResponse.json({ error: 'Taxonomy directory not found or not readable' }, { status: 404 }); + return NextResponse.json({ error: 'Taxonomy directory not found or not readable' }, { status: 500 }); } + // Checkout the new branch + await git.checkout({ fs, dir: taxonomyDir, ref: branchName }); + // Spawn tar to write gzipped archive to stdout const tar = spawn('tar', ['-czf', '-', '-C', rootDir, 'taxonomy'], { stdio: ['ignore', 'pipe', 'inherit'] }); // If the client aborts, make sure to kill the tar process - request.signal.addEventListener('abort', () => { + req.signal.addEventListener('abort', () => { tar.kill('SIGTERM'); }); @@ -51,7 +57,7 @@ export async function GET(request: Request) { status: 200, headers: { 'Content-Type': 'application/gzip', - 'Content-Disposition': 'attachment; filename="taxonomy.tar.gz"', + 'Content-Disposition': `attachment`, 'Cache-Control': 'no-store' } }); diff --git a/src/components/Dashboard/Github/dashboard.tsx b/src/components/Dashboard/Github/dashboard.tsx index 2050aaca..81155e52 100644 --- a/src/components/Dashboard/Github/dashboard.tsx +++ b/src/components/Dashboard/Github/dashboard.tsx @@ -3,7 +3,6 @@ import * as React from 'react'; import { useSession } from 'next-auth/react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; -import { fetchPullRequests, getGitHubUsername } from '../../../utils/github'; import { DraftEditFormInfo, PullRequest } from '@/types'; import { useState } from 'react'; import { @@ -40,6 +39,8 @@ import { } from '@patternfly/react-core'; import { ExternalLinkAltIcon, OutlinedQuestionCircleIcon, GithubIcon, EllipsisVIcon, PficonTemplateIcon } from '@patternfly/react-icons'; import { deleteDraftData, fetchDraftContributions } from '@/components/Contribute/Utils/autoSaveUtils'; +import { handleTaxonomyDownload } from '@/utils/taxonomy'; +import { fetchPullRequests, getGitHubUsername } from '@/utils/github'; const InstructLabLogo: React.FC = () => ; @@ -48,6 +49,7 @@ const DashboardGithub: React.FunctionComponent = () => { const [pullRequests, setPullRequests] = React.useState([]); const [draftContributions, setDraftContributions] = React.useState([]); const [isFirstPullDone, setIsFirstPullDone] = React.useState(false); + const [isDownloadDone, setIsDownloadDone] = React.useState(true); const [isLoading, setIsLoading] = useState(true); //const [error, setError] = React.useState(null); const [isActionMenuOpen, setIsActionMenuOpen] = React.useState<{ [key: number | string]: boolean }>({}); @@ -195,6 +197,16 @@ const DashboardGithub: React.FunctionComponent = () => { )} + {!isDownloadDone && ( + setIsDownloadDone(true)}> + + + + Retrieving the taxonomy compressed file with the contributed data. + + + + )} {isFirstPullDone && pullRequests.length === 0 && draftContributions.length === 0 ? ( @@ -347,6 +359,15 @@ const DashboardGithub: React.FunctionComponent = () => { )} + { + setIsDownloadDone(false); + handleTaxonomyDownload({ branchName: pr.head.ref, isGithubMode: true, setIsDownloadDone }); + }} + > + Download taxonomy + ) }} diff --git a/src/components/Dashboard/Native/dashboard.tsx b/src/components/Dashboard/Native/dashboard.tsx index db46a786..0bad08a1 100644 --- a/src/components/Dashboard/Native/dashboard.tsx +++ b/src/components/Dashboard/Native/dashboard.tsx @@ -47,6 +47,7 @@ import { ExpandableSection } from '@patternfly/react-core/dist/esm/components/Ex import { v4 as uuidv4 } from 'uuid'; import { DraftEditFormInfo } from '@/types'; import { deleteDraftData, fetchDraftContributions } from '@/components/Contribute/Utils/autoSaveUtils'; +import { handleTaxonomyDownload } from '@/utils/taxonomy'; const InstructLabLogo: React.FC = () => ; @@ -79,6 +80,7 @@ const DashboardNative: React.FunctionComponent = () => { const [selectedDraftContribution, setSelectedDraftContribution] = React.useState(null); const [isPublishing, setIsPublishing] = React.useState(false); const [expandedFiles, setExpandedFiles] = React.useState>({}); + const [isDownloadDone, setIsDownloadDone] = React.useState(true); const router = useRouter(); @@ -393,6 +395,17 @@ const DashboardNative: React.FunctionComponent = () => { ))} + {!isDownloadDone && ( + setIsDownloadDone(true)}> + + + + Retrieving the taxonomy compressed file with the contributed data. + + + + )} + {isLoading ? ( ) : branches.length === 0 && draftContributions.length === 0 ? ( @@ -546,8 +559,8 @@ const DashboardNative: React.FunctionComponent = () => { { - // this will trigger the browser download - window.location.href = '/api/download'; + setIsDownloadDone(false); + handleTaxonomyDownload({ branchName: branch.name, isGithubMode: false, setIsDownloadDone }); }} > Download taxonomy diff --git a/src/utils/taxonomy.ts b/src/utils/taxonomy.ts new file mode 100644 index 00000000..af6b9df5 --- /dev/null +++ b/src/utils/taxonomy.ts @@ -0,0 +1,33 @@ +// src/utils/taxonomy.ts +const GITHUB_TAXONOMY_DOWNLOAD_URL = '/api/github/download'; +const NATIVE_TAXONOMY_DOWNLOAD_URL = '/api/native/download'; + +interface TaxonomyDownloadProp { + branchName: string; + isGithubMode: boolean; + setIsDownloadDone: (isDownloadDone: boolean) => void; +} +export async function handleTaxonomyDownload({ branchName, isGithubMode, setIsDownloadDone }: TaxonomyDownloadProp) { + const res = await fetch(isGithubMode ? GITHUB_TAXONOMY_DOWNLOAD_URL : NATIVE_TAXONOMY_DOWNLOAD_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + branchName: branchName + }) + }); + + if (!res.ok) { + alert('Failed to download the taxonomy'); + return { message: res.statusText, status: res.status }; + } + + setIsDownloadDone(true); + + const blob = await res.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `taxonomy-${branchName}.tar.gz`; + a.click(); + window.URL.revokeObjectURL(url); +}