管理端可配类型/体积;上传改流式避免整文件进内存;发帖先选再传并拦截未上传提交;提高 Next proxy 体积上限。 Co-authored-by: Cursor <cursoragent@cursor.com>
59 lines
1.7 KiB
TypeScript
59 lines
1.7 KiB
TypeScript
/** 帖子附件客户端校验(与后端 setting / fileallow 对齐;安全边界仍在 Go) */
|
||
|
||
export const FILE_MAX_MB_DEFAULT = 20;
|
||
export const FILE_MAX_COUNT_DEFAULT = 10;
|
||
|
||
export function normalizeExt(nameOrExt: string): string {
|
||
const base = nameOrExt.includes("/")
|
||
? nameOrExt.split(/[/\\]/).pop() || ""
|
||
: nameOrExt;
|
||
const i = base.lastIndexOf(".");
|
||
const ext = (i >= 0 ? base.slice(i + 1) : base).trim().toLowerCase();
|
||
return ext.replace(/[^a-z0-9]/g, "").slice(0, 16);
|
||
}
|
||
|
||
export function buildAcceptAttr(exts: string[], limit: boolean): string | undefined {
|
||
if (!limit) return undefined;
|
||
if (!exts.length) return undefined;
|
||
return exts.map((e) => `.${e}`).join(",");
|
||
}
|
||
|
||
export function isAllowedPostFileName(
|
||
name: string,
|
||
opts: { limit: boolean; exts: string[] }
|
||
): boolean {
|
||
if (!opts.limit) return true;
|
||
const ext = normalizeExt(name);
|
||
if (!ext) return false;
|
||
return opts.exts.includes(ext);
|
||
}
|
||
|
||
export function formatMB(n: number): string {
|
||
return `${n}MB`;
|
||
}
|
||
|
||
export type PostFileRejectReason = "size" | "type" | "count" | null;
|
||
|
||
export function rejectPostFile(
|
||
file: File,
|
||
opts: {
|
||
limit: boolean;
|
||
exts: string[];
|
||
maxMB: number;
|
||
maxCount: number;
|
||
currentCount: number;
|
||
}
|
||
): { reason: PostFileRejectReason; message?: string } {
|
||
if (opts.currentCount >= opts.maxCount) {
|
||
return { reason: "count", message: `最多 ${opts.maxCount} 个附件` };
|
||
}
|
||
const maxBytes = opts.maxMB * 1024 * 1024;
|
||
if (file.size > maxBytes) {
|
||
return { reason: "size", message: `附件不能超过 ${opts.maxMB}MB` };
|
||
}
|
||
if (!isAllowedPostFileName(file.name, opts)) {
|
||
return { reason: "type", message: "不支持该附件格式" };
|
||
}
|
||
return { reason: null };
|
||
}
|