Initial commit

This commit is contained in:
admin
2026-05-26 22:46:00 +02:00
commit 7e2c2ff89c
256 changed files with 51523 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
import { NextRequest, NextResponse } from "next/server";
import { spawn } from "node:child_process";
import { assertLocalRequest } from "@/lib/api/localOnly";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
/**
* Open a native OS folder-picker dialog and return the absolute path the
* user selected. Works because Pinkudex is local-only — the Next.js
* server has the same desktop session as the browser. Returns
* `{ path: null }` if the user cancels.
*
* Windows: PowerShell + WinForms FolderBrowserDialog.
* macOS: osascript "choose folder".
* Linux: zenity (must be installed).
*/
export async function POST(req: NextRequest) {
const blocked = assertLocalRequest(req);
if (blocked) return blocked;
const body = await req.json().catch(() => ({}));
const startPath = typeof body.start === "string" ? body.start : "";
try {
const path = await runPicker(startPath);
return NextResponse.json({ path });
} catch (e) {
return NextResponse.json({ error: (e as Error).message, path: null }, { status: 500 });
}
}
function runPicker(startPath: string): Promise<string | null> {
if (process.platform === "win32") return pickerWindows(startPath);
if (process.platform === "darwin") return pickerMacOS(startPath);
return pickerLinux(startPath);
}
function pickerWindows(startPath: string): Promise<string | null> {
// STA threading is required for WinForms dialogs in PowerShell.
// -Sta keeps it; -NoProfile avoids whatever the user's profile prints.
// startPath is passed via env var so PowerShell never parses it as code.
const script = `
Add-Type -AssemblyName System.Windows.Forms | Out-Null
$dlg = New-Object System.Windows.Forms.FolderBrowserDialog
$dlg.Description = 'Pinkudex — pick a folder'
$dlg.ShowNewFolderButton = $false
if ($env:PINKUDEX_PICK_START) { try { $dlg.SelectedPath = $env:PINKUDEX_PICK_START } catch {} }
$owner = New-Object System.Windows.Forms.Form
$owner.TopMost = $true
$owner.Opacity = 0
$owner.ShowInTaskbar = $false
$result = $dlg.ShowDialog($owner)
if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
Write-Output $dlg.SelectedPath
}
`.trim();
return runProcess("powershell.exe", ["-NoProfile", "-Sta", "-Command", script], {
PINKUDEX_PICK_START: startPath,
});
}
function pickerMacOS(startPath: string): Promise<string | null> {
const startClause = startPath
? ` default location (POSIX file "${startPath.replace(/"/g, '\\"')}")`
: "";
const script = `try
set f to choose folder with prompt "Pinkudex — pick a folder"${startClause}
return POSIX path of f
on error number -128
return ""
end try`;
return runProcess("osascript", ["-e", script]);
}
function pickerLinux(startPath: string): Promise<string | null> {
const args = ["--file-selection", "--directory", "--title=Pinkudex — pick a folder"];
if (startPath) args.push(`--filename=${startPath.endsWith("/") ? startPath : startPath + "/"}`);
return runProcess("zenity", args).catch((e) => {
throw new Error(`Linux folder pickers require zenity to be installed (${(e as Error).message})`);
});
}
function runProcess(
cmd: string,
args: string[],
extraEnv?: Record<string, string>,
): Promise<string | null> {
return new Promise((resolve, reject) => {
const env = extraEnv ? { ...process.env, ...extraEnv } : process.env;
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], env });
let out = "";
let err = "";
child.stdout.on("data", (b) => { out += b.toString(); });
child.stderr.on("data", (b) => { err += b.toString(); });
child.on("error", (e) => reject(e));
child.on("close", (code) => {
// Cancel paths return non-zero (zenity) or empty stdout — treat as null.
if (code !== 0 && code !== 1 && code !== null) {
reject(new Error(err.trim() || `picker exited with code ${code}`));
return;
}
const trimmed = out.trim().replace(/\r/g, "");
resolve(trimmed || null);
});
});
}