API routes for publications

This commit is contained in:
2025-08-27 10:21:14 +02:00
parent fd73dde4cd
commit e47a984395
3 changed files with 79 additions and 122 deletions

View File

@@ -1,117 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const url = searchParams.get("url");
if (!url) {
return NextResponse.json(
{
error: "URL parameter is required",
usage: "Add ?url=<github-release-url> to test the proxy",
example:
"/api/pdf-proxy?url=https://github.com/user/repo/releases/download/latest/file.pdf",
},
{ status: 400 },
);
}
// Validate that the URL is from GitHub releases
if (!url.includes("github.com") || !url.includes("/releases/download/")) {
return NextResponse.json(
{ error: "Only GitHub release URLs are allowed" },
{ status: 403 },
);
}
try {
// Follow redirects and fetch the actual file
const response = await fetch(url, {
headers: {
"User-Agent": "Mozilla/5.0 (compatible; PDF-Proxy/1.0)",
Accept: "application/pdf,*/*",
},
redirect: "follow", // Follow redirects
});
if (!response.ok) {
return NextResponse.json(
{ error: `Failed to fetch PDF: ${response.status}` },
{ status: response.status },
);
}
// Get the response buffer first
const pdfBuffer = await response.arrayBuffer();
// Check if it's actually a PDF by looking at the file signature
const uint8Array = new Uint8Array(pdfBuffer);
// More robust PDF signature check
const firstBytes = Array.from(uint8Array.slice(0, 10));
const pdfSignature = String.fromCharCode(...uint8Array.slice(0, 4));
const extendedSignature = String.fromCharCode(...uint8Array.slice(0, 8));
// Only log in development
if (process.env.NODE_ENV === "development") {
console.log("PDF proxy - URL:", response.url);
console.log("PDF proxy - Size:", pdfBuffer.byteLength, "bytes");
console.log("PDF proxy - Signature:", JSON.stringify(pdfSignature));
}
// Check for PDF signature - should start with "%PDF"
const isPDF =
pdfSignature === "%PDF" ||
(uint8Array[0] === 0x25 && // %
uint8Array[1] === 0x50 && // P
uint8Array[2] === 0x44 && // D
uint8Array[3] === 0x46); // F
if (!isPDF) {
console.error(
"PDF proxy - Invalid signature:",
JSON.stringify(pdfSignature),
);
return NextResponse.json(
{
error: `Resource is not a valid PDF file. Got signature: ${JSON.stringify(pdfSignature)}`,
debug: {
signature: pdfSignature,
firstBytes: firstBytes.slice(0, 4),
size: pdfBuffer.byteLength,
},
},
{ status: 400 },
);
}
// Also check content-type as secondary validation (but be more lenient)
const contentType = response.headers.get("content-type");
return new NextResponse(pdfBuffer, {
headers: {
"Content-Type": "application/pdf",
"Content-Length": pdfBuffer.byteLength.toString(),
"Cache-Control": "public, max-age=7200", // Cache for 2 hours
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET",
"Access-Control-Allow-Headers": "Content-Type",
"Content-Disposition": "inline", // Display in browser instead of download
},
});
} catch (error) {
console.error("PDF proxy error:", error);
return NextResponse.json({ error: "Failed to proxy PDF" }, { status: 500 });
}
}
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}

View File

@@ -0,0 +1,74 @@
import { NextRequest, NextResponse } from "next/server";
import { track } from "@vercel/analytics/server";
import { parseBibtex } from "~/lib/bibtex";
import { readFile } from "fs/promises";
import { join } from "path";
export async function GET(
request: NextRequest,
{ params }: { params: { filename: string } },
) {
const filename = params.filename;
// Validate filename to prevent path traversal
if (!filename || filename.includes("..") || !filename.endsWith(".pdf")) {
return new NextResponse("Invalid filename", { status: 400 });
}
try {
// Read the BibTeX file to get publication metadata
const bibtexPath = join(process.cwd(), "public", "publications.bib");
const bibtexContent = await readFile(bibtexPath, "utf-8");
const publications = parseBibtex(bibtexContent);
// Find the publication that matches this PDF
const publication = publications.find(
(pub) =>
pub.paperUrl?.includes(filename) || pub.posterUrl?.includes(filename),
);
// Track the PDF access
if (publication) {
const isPoster = publication.posterUrl?.includes(filename);
await track("Publication PDF Access", {
title: publication.title,
type: publication.type,
year: publication.year.toString(),
pdf_type: isPoster ? "poster" : "paper",
citation_key: publication.citationKey || "",
venue: publication.venue || "",
access_method: "direct_url",
filename: filename,
});
} else {
// Track unknown PDF access
await track("Unknown PDF Access", {
filename: filename,
access_method: "direct_url",
});
}
// Serve the PDF file
const pdfPath = join(process.cwd(), "public", "publications", filename);
const pdfBuffer = await readFile(pdfPath);
return new NextResponse(new Uint8Array(pdfBuffer), {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `inline; filename="${filename}"`,
"Cache-Control": "public, max-age=31536000, immutable",
},
});
} catch (error) {
console.error("Error serving PDF:", error);
// Track the error
await track("PDF Access Error", {
filename: filename,
error: error instanceof Error ? error.message : "Unknown error",
});
return new NextResponse("PDF not found", { status: 404 });
}
}