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

@@ -5,8 +5,8 @@
booktitle = {34th IEEE International Conference on Robot and Human Interactive Communication},
address = {Eindhoven, The Netherlands},
abstract = {Human-robot interaction (HRI) research plays a pivotal role in shaping how robots communicate and collaborate with humans. However, conducting HRI studies can be challenging, particularly those employing the Wizard-of-Oz (WoZ) technique. WoZ user studies can have technical and methodological complexities that may render the results irreproducible. We propose to address these challenges with HRIStudio, a modular web-based platform designed to streamline the design, the execution, and the analysis of WoZ experiments. HRIStudio offers an intuitive interface for experiment creation, real-time control and monitoring during experimental runs, and comprehensive data logging and playback tools for analysis and reproducibility. By lowering technical barriers, promoting collaboration, and offering methodological guidelines, HRIStudio aims to make human-centered robotics research easier and empower researchers to develop scientifically rigorous user studies.},
url = {https://soconnor.dev/publications/ROMAN25_0574_FI.pdf},
paperUrl = {/publications/ROMAN25_0574_FI.pdf}
url = {https://soconnor.dev/api/publications/ROMAN25_0574_FI.pdf},
paperUrl = {/api/publications/ROMAN25_0574_FI.pdf}
}
@inproceedings{OConnor2024,
@@ -16,8 +16,8 @@
organization = {33rd IEEE International Conference on Robot and Human Interactive Communication},
address = {Pasadena, CA, USA},
abstract = {Human-robot interaction (HRI) research plays a pivotal role in shaping how robots communicate and collaborate with humans. However, conducting HRI studies, particularly those employing the Wizard-of-Oz (WoZ) technique, can be challenging. WoZ user studies can have complexities at the technical and methodological levels that may render the results irreproducible. We propose to address these challenges with HRIStudio, a novel web-based platform designed to streamline the design, execution, and analysis of WoZ experiments. HRIStudio offers an intuitive interface for experiment creation, real-time control and monitoring during experimental runs, and comprehensive data logging and playback tools for analysis and reproducibility. By lowering technical barriers, promoting collaboration, and offering methodological guidelines, HRIStudio aims to make human-centered robotics research easier, and at the same time, empower researchers to develop scientifically rigorous user studies.},
url = {https://soconnor.dev/publications/hristudio-lbr.pdf},
paperUrl = {/publications/hristudio-lbr.pdf},
posterUrl = {/publications/hristudio-lbr-poster.pdf},
url = {https://soconnor.dev/api/publications/hristudio-lbr.pdf},
paperUrl = {/api/publications/hristudio-lbr.pdf},
posterUrl = {/api/publications/hristudio-lbr-poster.pdf},
note = {Late breaking report}
}

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 });
}
}