diff --git a/src/app/api/open-file/route.ts b/src/app/api/open-file/route.ts new file mode 100644 index 00000000..06d3d9a6 --- /dev/null +++ b/src/app/api/open-file/route.ts @@ -0,0 +1,83 @@ +import { NextRequest, NextResponse } from "next/server"; +import { execFile } from "child_process"; +import { promisify } from "util"; +import { stat } from "fs/promises"; +import path from "path"; +import os from "os"; + +const execFileAsync = promisify(execFile); + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { filePath: inputPath } = body; + + if (!inputPath || typeof inputPath !== "string") { + return NextResponse.json( + { success: false, error: "File path is required" }, + { status: 400 } + ); + } + + // Normalize and resolve the path to prevent traversal attacks + const normalizedPath = path.resolve(inputPath); + + // Validate that the path exists and is a file + try { + const stats = await stat(normalizedPath); + if (!stats.isFile()) { + return NextResponse.json( + { success: false, error: "Path is not a file" }, + { status: 400 } + ); + } + } catch { + return NextResponse.json( + { success: false, error: "File does not exist" }, + { status: 400 } + ); + } + + const platform = os.platform(); + let command = ""; + let args: string[] = []; + + switch (platform) { + case "darwin": + command = "open"; + args = ["-R", normalizedPath]; + break; + case "win32": + command = "explorer"; + args = [`/select,${normalizedPath}`]; + break; + case "linux": + // Linux has no universal "reveal in folder" — open parent directory + command = "xdg-open"; + args = [path.dirname(normalizedPath)]; + break; + default: + command = "xdg-open"; + args = [path.dirname(normalizedPath)]; + } + + try { + await execFileAsync(command, args); + } catch (err: unknown) { + // Windows explorer returns non-zero exit code even on success + if (platform === "win32" && err && typeof err === "object" && "code" in err) { + // Explorer launched successfully despite non-zero exit + } else { + throw err; + } + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Failed to reveal file:", error); + return NextResponse.json( + { success: false, error: "Failed to open file location" }, + { status: 500 } + ); + } +}