83 lines
2.8 KiB
TypeScript
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 })
|
|
}
|
|
}
|