Rocky_Mountain_Vending/app/api/manuals/[...path]/route.ts

83 lines
2.8 KiB
TypeScript

import { NextResponse } from "next/server"
import { readFile } from "node:fs/promises"
import { existsSync } from "node:fs"
import { join } from "node:path"
import { getManualsFilesRoot } from "@/lib/manuals-paths"
import { getManualAssetFromStorage } from "@/lib/manuals-object-storage"
export const dynamic = "force-dynamic"
function decodeSegments(pathArray: string[]) {
return pathArray.map((segment) => {
try {
let decoded = segment
while (decoded !== decodeURIComponent(decoded)) {
decoded = decodeURIComponent(decoded)
}
return decoded
} catch {
return segment
}
})
}
function invalidPath(pathValue: string) {
return pathValue.includes("..") || pathValue.startsWith("/")
}
export async function GET(
_request: Request,
{ params }: { params: Promise<{ path: string[] }> }
) {
try {
const { path: pathArray } = await params
const decodedPath = decodeSegments(pathArray)
const filePath = decodedPath.join("/")
if (invalidPath(filePath) || !filePath.toLowerCase().endsWith(".pdf")) {
return new NextResponse("Invalid path", { status: 400 })
}
const storageObject = await getManualAssetFromStorage("manuals", filePath)
if (storageObject) {
return new NextResponse(Buffer.from(storageObject.body), {
headers: {
"Content-Type": storageObject.contentType || "application/pdf",
"Content-Disposition": `inline; filename="${encodeURIComponent(decodedPath.at(-1) || "manual.pdf")}"`,
"Cache-Control": "public, max-age=31536000, immutable",
"X-Content-Type-Options": "nosniff",
},
})
}
const manualsDir = getManualsFilesRoot()
const fullPath = join(manualsDir, filePath)
const normalizedFullPath = fullPath.replace(/\\/g, "/")
const normalizedManualsDir = manualsDir.replace(/\\/g, "/")
if (!existsSync(normalizedFullPath) && !existsSync(fullPath)) {
return new NextResponse("File not found", { status: 404 })
}
const fileToRead = existsSync(normalizedFullPath)
? normalizedFullPath
: fullPath
const resolvedPath = fileToRead.replace(/\\/g, "/")
if (!resolvedPath.startsWith(normalizedManualsDir)) {
return new NextResponse("Invalid path", { status: 400 })
}
const fileBuffer = await readFile(fileToRead)
return new NextResponse(fileBuffer, {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `inline; filename="${encodeURIComponent(decodedPath.at(-1) || "manual.pdf")}"`,
"Cache-Control": "public, max-age=31536000, immutable",
"X-Content-Type-Options": "nosniff",
},
})
} catch (error) {
console.error("[manuals-api] failed to serve manual", error)
return new NextResponse("Internal server error", { status: 500 })
}
}