188 lines
5.6 KiB
TypeScript
188 lines
5.6 KiB
TypeScript
export type UploadEntry = { file: File; path: string };
|
|
|
|
export type PublishManifestRule = {
|
|
line: number;
|
|
raw: string;
|
|
pattern: string;
|
|
exclude: boolean;
|
|
};
|
|
|
|
export const RELEASE_MANIFEST_NAMES = ["发布清单.txt", "release_manifest.txt"];
|
|
|
|
export function rawPathForFile(file: File) {
|
|
const raw = ((file as any).webkitRelativePath || file.name) as string;
|
|
return raw.replaceAll("\\", "/");
|
|
}
|
|
|
|
export function relativePathForFile(file: File) {
|
|
const raw = rawPathForFile(file);
|
|
const parts = raw.replaceAll("\\", "/").split("/").filter(Boolean);
|
|
if ((file as any).webkitRelativePath && parts.length > 1) parts.shift();
|
|
return parts.join("/");
|
|
}
|
|
|
|
export function detectSelectedRoot(files: File[]) {
|
|
const first = files[0];
|
|
if (!first || !(first as any).webkitRelativePath) return "";
|
|
const parts = rawPathForFile(first).split("/").filter(Boolean);
|
|
return parts.length > 1 ? parts[0] : "";
|
|
}
|
|
|
|
function normalizePublishPath(value: string) {
|
|
let normalized = value.replaceAll("\\", "/").trim();
|
|
while (normalized.startsWith("./")) normalized = normalized.slice(2);
|
|
normalized = normalized.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
return normalized;
|
|
}
|
|
|
|
function normalizeManifestPattern(value: string, rootName: string) {
|
|
let pattern = normalizePublishPath(value);
|
|
const root = normalizePublishPath(rootName);
|
|
if (root) {
|
|
if (pattern === root) return "*";
|
|
if (pattern.startsWith(`${root}/`)) {
|
|
pattern = pattern.slice(root.length + 1);
|
|
}
|
|
}
|
|
return pattern || "*";
|
|
}
|
|
|
|
export function parseReleaseManifest(text: string, rootName: string): PublishManifestRule[] {
|
|
return text
|
|
.split(/\r?\n/)
|
|
.map((raw, index) => ({ raw, line: index + 1, trimmed: raw.trim() }))
|
|
.filter(item => item.trimmed && !item.trimmed.startsWith("#"))
|
|
.map(item => {
|
|
const exclude = item.trimmed.startsWith("!");
|
|
const body = exclude ? item.trimmed.slice(1).trim() : item.trimmed;
|
|
return {
|
|
line: item.line,
|
|
raw: item.raw,
|
|
pattern: normalizeManifestPattern(body, rootName),
|
|
exclude
|
|
};
|
|
})
|
|
.filter(rule => rule.pattern);
|
|
}
|
|
|
|
function escapeRegExp(value: string) {
|
|
return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
}
|
|
|
|
export function manifestRuleMatches(pattern: string, filePath: string) {
|
|
const normalizedPattern = normalizePublishPath(pattern);
|
|
const normalizedPath = normalizePublishPath(filePath);
|
|
if (normalizedPattern === "*") return true;
|
|
if (normalizedPattern.endsWith("/*")) {
|
|
const prefix = normalizedPattern.slice(0, -2);
|
|
return normalizedPath.startsWith(`${prefix}/`);
|
|
}
|
|
if (!normalizedPattern.includes("*")) {
|
|
return normalizedPath === normalizedPattern;
|
|
}
|
|
const regex = new RegExp(
|
|
`^${normalizedPattern.split("*").map(escapeRegExp).join(".*")}$`
|
|
);
|
|
return regex.test(normalizedPath);
|
|
}
|
|
|
|
export function isReleaseManifestFile(path: string) {
|
|
const normalized = normalizePublishPath(path).toLowerCase();
|
|
return RELEASE_MANIFEST_NAMES.some(name => normalized === name.toLowerCase());
|
|
}
|
|
|
|
export function isSafePublishPath(itemPath: string) {
|
|
const protectedFiles = new Set([
|
|
"client.ini",
|
|
"bootstrap.exe",
|
|
"config/app_config.json",
|
|
"config/local_state.json",
|
|
"config/client_identity.dat",
|
|
"config/version_policy.dat"
|
|
]);
|
|
const path = normalizePublishPath(itemPath).toLowerCase();
|
|
const parts = path.split("/");
|
|
const blockedDirectory = parts
|
|
.slice(0, -1)
|
|
.some(
|
|
part =>
|
|
part === ".git" ||
|
|
part === ".vs" ||
|
|
part === "debug" ||
|
|
part === "update" ||
|
|
part === "update_temp" ||
|
|
part === "cmakefiles" ||
|
|
part.startsWith("build") ||
|
|
part.endsWith("_autogen")
|
|
);
|
|
const runtimeFile =
|
|
protectedFiles.has(path) ||
|
|
(path.startsWith("bin/") && protectedFiles.has(path.slice(4)));
|
|
return (
|
|
!blockedDirectory &&
|
|
!runtimeFile &&
|
|
!path.endsWith(".pdb") &&
|
|
!path.endsWith(".ilk") &&
|
|
!path.endsWith(".obj")
|
|
);
|
|
}
|
|
|
|
export function normalizeExecutableName(value: string) {
|
|
const normalized = normalizePublishPath(value || "SimCAE.exe");
|
|
if (!normalized) return "SimCAE.exe";
|
|
const parts = normalized.split("/");
|
|
const leaf = parts[parts.length - 1];
|
|
if (!leaf.includes(".")) {
|
|
parts[parts.length - 1] = `${leaf}.exe`;
|
|
}
|
|
return parts.join("/");
|
|
}
|
|
|
|
export function releaseMainExecutableForInstall(
|
|
installRoot: string,
|
|
mainExecutable: string
|
|
) {
|
|
const root = (installRoot || "..").trim();
|
|
const main = normalizeExecutableName(mainExecutable);
|
|
if (root === ".") return main;
|
|
if (main.toLowerCase().startsWith("bin/")) return main;
|
|
return `bin/${main}`;
|
|
}
|
|
|
|
export function executableNameCandidates(value: string, fallback: string) {
|
|
const leaf =
|
|
normalizePublishPath(value || fallback)
|
|
.split("/")
|
|
.filter(Boolean)
|
|
.pop() || fallback;
|
|
const lower = leaf.toLowerCase();
|
|
const candidates = new Set([lower]);
|
|
if (lower.endsWith(".exe")) {
|
|
candidates.add(lower.slice(0, -4));
|
|
} else {
|
|
candidates.add(`${lower}.exe`);
|
|
}
|
|
return candidates;
|
|
}
|
|
|
|
export function executableLeafName(value: string, fallback: string) {
|
|
return (
|
|
normalizePublishPath(value || fallback)
|
|
.split("/")
|
|
.filter(Boolean)
|
|
.pop() || fallback
|
|
);
|
|
}
|
|
|
|
export function dirname(path: string) {
|
|
const parts = normalizePublishPath(path).split("/").filter(Boolean);
|
|
parts.pop();
|
|
return parts.join("/");
|
|
}
|
|
|
|
export function findExecutablePath(entries: UploadEntry[], candidates: Set<string>) {
|
|
return entries
|
|
.map(item => normalizePublishPath(item.path))
|
|
.find(path => candidates.has(path.split("/").pop()?.toLowerCase() || ""));
|
|
}
|