forked from EduCraft/curriculum-project-hub
fix(hub): send Feishu files explicitly
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import { access, stat } from "node:fs/promises";
|
||||
import { basename, extname, isAbsolute, join, relative, resolve } from "node:path";
|
||||
|
||||
export interface DeliverableFile {
|
||||
readonly path: string;
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
const DELIVERABLE_EXTENSIONS = new Set([
|
||||
".csv",
|
||||
".doc",
|
||||
".docx",
|
||||
".gif",
|
||||
".jpeg",
|
||||
".jpg",
|
||||
".md",
|
||||
".pdf",
|
||||
".png",
|
||||
".ppt",
|
||||
".pptx",
|
||||
".svg",
|
||||
".txt",
|
||||
".typ",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".zip",
|
||||
]);
|
||||
|
||||
export async function resolveDeliverableFile(
|
||||
requestedPath: string,
|
||||
workspaceDir: string,
|
||||
): Promise<DeliverableFile | null> {
|
||||
const token = requestedPath.trim();
|
||||
if (token === "" || !DELIVERABLE_EXTENSIONS.has(extname(token).toLowerCase())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const roots = await allowedRoots(workspaceDir);
|
||||
for (const candidate of candidatePaths(token, workspaceDir, roots)) {
|
||||
if (!isAllowedPath(candidate, roots)) continue;
|
||||
const existing = await existingFile(candidate);
|
||||
if (existing !== null) return { path: existing, name: basename(existing) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function allowedRoots(workspaceDir: string): Promise<string[]> {
|
||||
const roots = [resolve(workspaceDir)];
|
||||
const gitRoot = await findGitRoot(workspaceDir);
|
||||
if (gitRoot !== undefined) roots.push(gitRoot);
|
||||
return [...new Set(roots)];
|
||||
}
|
||||
|
||||
async function findGitRoot(startDir: string): Promise<string | undefined> {
|
||||
let current = resolve(startDir);
|
||||
while (true) {
|
||||
try {
|
||||
await access(join(current, ".git"));
|
||||
return current;
|
||||
} catch {
|
||||
const parent = resolve(current, "..");
|
||||
if (parent === current) return undefined;
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function candidatePaths(token: string, workspaceDir: string, roots: readonly string[]): string[] {
|
||||
if (isAbsolute(token)) return [resolve(token)];
|
||||
|
||||
const candidates: string[] = [];
|
||||
for (const root of roots) {
|
||||
candidates.push(resolve(root, token));
|
||||
}
|
||||
if (!token.includes("/") && !token.includes("\\")) {
|
||||
candidates.push(resolve(workspaceDir, "build", token));
|
||||
}
|
||||
return [...new Set(candidates)];
|
||||
}
|
||||
|
||||
function isAllowedPath(path: string, roots: readonly string[]): boolean {
|
||||
return roots.some((root) => {
|
||||
const rel = relative(root, path);
|
||||
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
||||
});
|
||||
}
|
||||
|
||||
async function existingFile(path: string): Promise<string | null> {
|
||||
try {
|
||||
const s = await stat(path);
|
||||
return s.isFile() ? resolve(path) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user