feat: 完善服务端工程化、发布流程和自动化测试
This commit is contained in:
@@ -1,99 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { hasAuth, getAuths } from "@/router/utils";
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionButtonRouter"
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p class="mb-2!">当前拥有的code列表:{{ getAuths() }}</p>
|
||||
|
||||
<el-card shadow="never" class="mb-2">
|
||||
<template #header>
|
||||
<div class="card-header">组件方式判断权限</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<Auth value="permission:btn:add">
|
||||
<el-button plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
</Auth>
|
||||
<Auth :value="['permission:btn:edit']">
|
||||
<el-button plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
</Auth>
|
||||
<Auth
|
||||
:value="[
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
]"
|
||||
>
|
||||
<el-button plain type="danger">
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</Auth>
|
||||
</el-space>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="mb-2">
|
||||
<template #header>
|
||||
<div class="card-header">函数方式判断权限</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<el-button v-if="hasAuth('permission:btn:add')" plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
<el-button v-if="hasAuth(['permission:btn:edit'])" plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="
|
||||
hasAuth([
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
])
|
||||
"
|
||||
plain
|
||||
type="danger"
|
||||
>
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
指令方式判断权限(该方式不能动态修改权限)
|
||||
</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<el-button v-auth="'permission:btn:add'" plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
<el-button v-auth="['permission:btn:edit']" plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-auth="[
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
]"
|
||||
plain
|
||||
type="danger"
|
||||
>
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,109 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { hasPerms } from "@/utils/auth";
|
||||
import { useUserStoreHook } from "@/store/modules/user";
|
||||
|
||||
const { permissions } = useUserStoreHook();
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionButtonLogin"
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p class="mb-2!">当前拥有的code列表:{{ permissions }}</p>
|
||||
<p v-show="permissions?.[0] === '*:*:*'" class="mb-2!">
|
||||
*:*:* 代表拥有全部按钮级别权限
|
||||
</p>
|
||||
|
||||
<el-card shadow="never" class="mb-2">
|
||||
<template #header>
|
||||
<div class="card-header">组件方式判断权限</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<Perms value="permission:btn:add">
|
||||
<el-button plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
</Perms>
|
||||
<Perms :value="['permission:btn:edit']">
|
||||
<el-button plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
</Perms>
|
||||
<Perms
|
||||
:value="[
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
]"
|
||||
>
|
||||
<el-button plain type="danger">
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</Perms>
|
||||
</el-space>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="mb-2">
|
||||
<template #header>
|
||||
<div class="card-header">函数方式判断权限</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<el-button v-if="hasPerms('permission:btn:add')" plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPerms(['permission:btn:edit'])"
|
||||
plain
|
||||
type="primary"
|
||||
>
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="
|
||||
hasPerms([
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
])
|
||||
"
|
||||
plain
|
||||
type="danger"
|
||||
>
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
指令方式判断权限(该方式不能动态修改权限)
|
||||
</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<el-button v-perms="'permission:btn:add'" plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
<el-button v-perms="['permission:btn:edit']" plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="[
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
]"
|
||||
plain
|
||||
type="danger"
|
||||
>
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,66 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { initRouter } from "@/router/utils";
|
||||
import { storageLocal } from "@pureadmin/utils";
|
||||
import { type CSSProperties, ref, computed } from "vue";
|
||||
import { useUserStoreHook } from "@/store/modules/user";
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionPage"
|
||||
});
|
||||
|
||||
const elStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
width: "85vw",
|
||||
justifyContent: "start"
|
||||
};
|
||||
});
|
||||
|
||||
const username = ref(useUserStoreHook()?.username);
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: "admin",
|
||||
label: "管理员角色"
|
||||
},
|
||||
{
|
||||
value: "common",
|
||||
label: "普通角色"
|
||||
}
|
||||
];
|
||||
|
||||
function onChange() {
|
||||
useUserStoreHook()
|
||||
.loginByUsername({ username: username.value, password: "admin123" })
|
||||
.then(res => {
|
||||
if (res.success) {
|
||||
storageLocal().removeItem("async-routes");
|
||||
usePermissionStoreHook().clearAllCachePage();
|
||||
initRouter();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p class="mb-2!">
|
||||
模拟后台根据不同角色返回对应路由,观察左侧菜单变化(管理员角色可查看系统管理菜单、普通角色不可查看系统管理菜单)
|
||||
</p>
|
||||
<el-card shadow="never" :style="elStyle">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>当前角色:{{ username }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-select v-model="username" class="w-[160px]!" @change="onChange">
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -8,24 +8,30 @@ import {
|
||||
friendlyError
|
||||
} from "@/api/simcae";
|
||||
import { removeToken } from "@/utils/auth";
|
||||
import {
|
||||
RELEASE_MANIFEST_NAMES,
|
||||
type UploadEntry,
|
||||
dirname,
|
||||
detectSelectedRoot,
|
||||
executableLeafName,
|
||||
executableNameCandidates,
|
||||
findExecutablePath,
|
||||
isReleaseManifestFile,
|
||||
isSafePublishPath,
|
||||
manifestRuleMatches,
|
||||
parseReleaseManifest,
|
||||
relativePathForFile,
|
||||
releaseMainExecutableForInstall
|
||||
} from "./publishManifest";
|
||||
|
||||
defineOptions({
|
||||
name: "SimCAEConsole"
|
||||
});
|
||||
|
||||
type UploadEntry = { file: File; path: string };
|
||||
type PublishManifestRule = {
|
||||
line: number;
|
||||
raw: string;
|
||||
pattern: string;
|
||||
exclude: boolean;
|
||||
};
|
||||
|
||||
const RELEASE_MANIFEST_NAMES = ["发布清单.txt", "release_manifest.txt"];
|
||||
|
||||
const activeTab = ref("overview");
|
||||
const loading = ref(false);
|
||||
const publishing = ref(false);
|
||||
const publishJobMessage = ref("");
|
||||
const apps = ref<any[]>([]);
|
||||
const channels = ref<any[]>([]);
|
||||
const versions = ref<any[]>([]);
|
||||
@@ -204,168 +210,6 @@ const expectedReleaseMainExecutable = computed(() =>
|
||||
releaseMainExecutableForInstall(configForm.install_root, configForm.main_executable)
|
||||
);
|
||||
|
||||
function rawPathForFile(file: File) {
|
||||
const raw = ((file as any).webkitRelativePath || file.name) as string;
|
||||
return raw.replaceAll("\\", "/");
|
||||
}
|
||||
|
||||
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("/");
|
||||
}
|
||||
|
||||
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 || "*";
|
||||
}
|
||||
|
||||
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, "\\$&");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function isReleaseManifestFile(path: string) {
|
||||
const normalized = normalizePublishPath(path).toLowerCase();
|
||||
return RELEASE_MANIFEST_NAMES.some(name => normalized === name.toLowerCase());
|
||||
}
|
||||
|
||||
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")
|
||||
);
|
||||
}
|
||||
|
||||
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("/");
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function executableLeafName(value: string, fallback: string) {
|
||||
return (
|
||||
normalizePublishPath(value || fallback)
|
||||
.split("/")
|
||||
.filter(Boolean)
|
||||
.pop() || fallback
|
||||
);
|
||||
}
|
||||
|
||||
function launcherDisplayName() {
|
||||
return executableLeafName(
|
||||
runtime?.default_client_config?.launcher_executable,
|
||||
@@ -373,18 +217,6 @@ function launcherDisplayName() {
|
||||
);
|
||||
}
|
||||
|
||||
function dirname(path: string) {
|
||||
const parts = normalizePublishPath(path).split("/").filter(Boolean);
|
||||
parts.pop();
|
||||
return parts.join("/");
|
||||
}
|
||||
|
||||
function findExecutablePath(entries: UploadEntry[], candidates: Set<string>) {
|
||||
return entries
|
||||
.map(item => normalizePublishPath(item.path))
|
||||
.find(path => candidates.has(path.split("/").pop()?.toLowerCase() || ""));
|
||||
}
|
||||
|
||||
function installRootDescription() {
|
||||
const launcher = launcherDisplayName();
|
||||
return configForm.install_root === "."
|
||||
@@ -465,6 +297,10 @@ function handleError(prefix: string, error: unknown) {
|
||||
ElMessage.error(`${prefix}:${friendlyError(error)}`);
|
||||
}
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise(resolve => window.setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function roleLabel(role: string) {
|
||||
return adminRoleOptions.value.find(item => item.value === role)?.label || role;
|
||||
}
|
||||
@@ -637,17 +473,34 @@ async function publishVersion() {
|
||||
});
|
||||
}
|
||||
publishing.value = true;
|
||||
publishJobMessage.value = "正在上传发布文件...";
|
||||
try {
|
||||
const result = await adminRequest<any>("/admin/publish", {
|
||||
const job = await adminRequest<any>("/admin/publish/job", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
});
|
||||
publishJobMessage.value = job?.message || "发布文件已接收,正在后台处理...";
|
||||
const result = await waitPublishJob(job?.job_id);
|
||||
ElMessage.success(result?.msg || `发布成功:${publishForm.version}`);
|
||||
await loadVersions();
|
||||
} catch (error) {
|
||||
handleError("发布失败", error);
|
||||
} finally {
|
||||
publishing.value = false;
|
||||
publishJobMessage.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function waitPublishJob(jobId: string) {
|
||||
if (!jobId) throw new Error("服务端没有返回发布任务 ID");
|
||||
while (true) {
|
||||
await sleep(1500);
|
||||
const job = await adminRequest<any>(`/admin/publish/job/${encodeURIComponent(jobId)}`);
|
||||
publishJobMessage.value = job?.message || "正在后台发布版本...";
|
||||
if (job?.status === "success") return job.result || { msg: job.message };
|
||||
if (job?.status === "error") {
|
||||
throw new Error(job?.message || "发布任务失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1303,6 +1156,14 @@ test_file/*
|
||||
<input v-else type="file" accept=".zip,.tar.gz,.tgz,.tar.bz2,.tbz2,.rar" @change="onArchiveFile" />
|
||||
<span>{{ publishSummary }}</span>
|
||||
</div>
|
||||
<el-alert
|
||||
v-if="publishJobMessage"
|
||||
class="publish-job-alert"
|
||||
:title="publishJobMessage"
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<el-button type="primary" size="large" :loading="publishing" @click="publishVersion">发布版本</el-button>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
@@ -1799,6 +1660,10 @@ test_file/*
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.publish-job-alert {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.install-root-options {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
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() || ""));
|
||||
}
|
||||
Reference in New Issue
Block a user