Files
community-rule/lib/server/uploads/resolveUploadedFile.ts
T
2026-05-07 21:15:27 -06:00

60 lines
1.6 KiB
TypeScript

import { constants as fsConstants } from "node:fs";
import { access, readdir } from "node:fs/promises";
import path from "node:path";
import { getUploadRootFromEnv } from "./uploadRoot";
import { isValidUploadFileId } from "./uploadConstants";
export type ResolvedUploadFile = {
absolutePath: string;
/** MIME inferred from extension (no sidecar file in v1). */
contentType: string;
};
function contentTypeForFilename(fileName: string): string {
const ext = path.extname(fileName).toLowerCase();
switch (ext) {
case ".jpg":
case ".jpeg":
return "image/jpeg";
case ".png":
return "image/png";
case ".webp":
return "image/webp";
case ".gif":
return "image/gif";
case ".pdf":
return "application/pdf";
default:
return "application/octet-stream";
}
}
/**
* Resolves `id` (UUID stem) to the single matching file `{id}.*` under UPLOAD_ROOT.
* Returns null if missing, ambiguous, or invalid id.
*/
export async function resolveUploadedFileById(
id: string,
): Promise<ResolvedUploadFile | null> {
if (!isValidUploadFileId(id)) return null;
const root = getUploadRootFromEnv();
if (!root) return null;
const entries = await readdir(root);
const prefix = `${id}.`;
const matches = entries.filter((e) => e.startsWith(prefix));
if (matches.length !== 1) return null;
const absolutePath = path.join(root, matches[0]!);
try {
await access(absolutePath, fsConstants.R_OK);
} catch {
return null;
}
return {
absolutePath,
contentType: contentTypeForFilename(matches[0]!),
};
}