Limit extracted upgrade package size
This commit is contained in:
@@ -49,6 +49,9 @@ const COLD_ALLOWED_FILES = new Set([
|
||||
|
||||
const BLOCKED_TOP_LEVEL_NAMES = new Set(['.git', 'node_modules', '.next', 'dist', 'backups', 'local-storage', 'upgrade-state']);
|
||||
const BLOCKED_ANYWHERE_NAMES = new Set(['.git', 'node_modules', '.next']);
|
||||
const MAX_EXTRACTED_FILES = Number(process.env.UPGRADE_MAX_EXTRACTED_FILES || 5000);
|
||||
const MAX_EXTRACTED_BYTES = Number(process.env.UPGRADE_MAX_EXTRACTED_BYTES || 500 * 1024 * 1024);
|
||||
const MAX_EXTRACTED_FILE_BYTES = Number(process.env.UPGRADE_MAX_EXTRACTED_FILE_BYTES || 200 * 1024 * 1024);
|
||||
const PAYLOAD_TOP_LEVEL_DIRECTORIES = new Set([
|
||||
...HOT_ALLOWED_PREFIXES.map(prefix => prefix.replace(/\/$/, '')),
|
||||
...COLD_ALLOWED_PREFIXES.map(prefix => prefix.replace(/\/$/, '')),
|
||||
@@ -122,13 +125,15 @@ async function main() {
|
||||
run('tar', [...tarReadArgs('extract', packagePath), '-C', extractDir], { cwd: projectRoot, label: '解压升级包' });
|
||||
|
||||
const payloadRoot = resolvePayloadRoot(extractDir);
|
||||
const files = listFiles(payloadRoot);
|
||||
const packageStats = collectPackageStats(payloadRoot);
|
||||
const files = packageStats.files;
|
||||
if (files.length === 0) {
|
||||
throw new Error('升级包为空');
|
||||
}
|
||||
validatePackageSize(packageStats);
|
||||
|
||||
const validation = validateFiles(files, mode);
|
||||
logStep('升级包内容', `校验通过,共 ${files.length} 个文件:${files.slice(0, 20).join('、')}${files.length > 20 ? ` 等 ${files.length} 个文件` : ''}`);
|
||||
logStep('升级包内容', `校验通过,共 ${files.length} 个文件,解压后 ${formatBytes(packageStats.totalBytes)}:${files.slice(0, 20).join('、')}${files.length > 20 ? ` 等 ${files.length} 个文件` : ''}`);
|
||||
updateState({
|
||||
step: 'validated',
|
||||
progress: 14,
|
||||
@@ -136,6 +141,9 @@ async function main() {
|
||||
restartRequired: mode === 'cold' || validation.requiresRestart,
|
||||
packageHash: sha256(packagePath),
|
||||
changedFiles: files,
|
||||
extractedFileCount: packageStats.files.length,
|
||||
extractedBytes: packageStats.totalBytes,
|
||||
largestFileBytes: packageStats.largestFileBytes,
|
||||
dryRun,
|
||||
});
|
||||
|
||||
@@ -404,10 +412,12 @@ function resolvePayloadRoot(root) {
|
||||
return root;
|
||||
}
|
||||
|
||||
function listFiles(root) {
|
||||
function collectPackageStats(root) {
|
||||
const files = [];
|
||||
let totalBytes = 0;
|
||||
let largestFileBytes = 0;
|
||||
walk(root, '');
|
||||
return files.sort();
|
||||
return { files: files.sort(), totalBytes, largestFileBytes };
|
||||
|
||||
function walk(currentRoot, relativeRoot) {
|
||||
for (const entry of fs.readdirSync(currentRoot, { withFileTypes: true })) {
|
||||
@@ -417,6 +427,9 @@ function listFiles(root) {
|
||||
if (entry.isDirectory()) {
|
||||
walk(absolute, relative);
|
||||
} else if (entry.isFile()) {
|
||||
const stat = fs.statSync(absolute);
|
||||
totalBytes += stat.size;
|
||||
largestFileBytes = Math.max(largestFileBytes, stat.size);
|
||||
files.push(relative);
|
||||
} else {
|
||||
throw new Error(`升级包包含不支持的文件类型: ${relative}`);
|
||||
@@ -425,6 +438,18 @@ function listFiles(root) {
|
||||
}
|
||||
}
|
||||
|
||||
function validatePackageSize(stats) {
|
||||
if (Number.isFinite(MAX_EXTRACTED_FILES) && stats.files.length > MAX_EXTRACTED_FILES) {
|
||||
throw new Error(`升级包文件数量过多:${stats.files.length} 个,最多允许 ${MAX_EXTRACTED_FILES} 个`);
|
||||
}
|
||||
if (Number.isFinite(MAX_EXTRACTED_BYTES) && stats.totalBytes > MAX_EXTRACTED_BYTES) {
|
||||
throw new Error(`升级包解压后过大:${formatBytes(stats.totalBytes)},最多允许 ${formatBytes(MAX_EXTRACTED_BYTES)}`);
|
||||
}
|
||||
if (Number.isFinite(MAX_EXTRACTED_FILE_BYTES) && stats.largestFileBytes > MAX_EXTRACTED_FILE_BYTES) {
|
||||
throw new Error(`升级包包含过大的单个文件:${formatBytes(stats.largestFileBytes)},最多允许 ${formatBytes(MAX_EXTRACTED_FILE_BYTES)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateFiles(files, updateMode) {
|
||||
for (const file of files) {
|
||||
assertSafeRelativePath(file);
|
||||
@@ -586,6 +611,19 @@ function sha256(file) {
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (!Number.isFinite(bytes)) return 'unknown';
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
const units = ['KB', 'MB', 'GB'];
|
||||
let value = bytes / 1024;
|
||||
let unitIndex = 0;
|
||||
while (value >= 1024 && unitIndex < units.length - 1) {
|
||||
value /= 1024;
|
||||
unitIndex += 1;
|
||||
}
|
||||
return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
function toPosix(file) {
|
||||
return file.split(path.sep).join('/');
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ type UpgradeJobState = {
|
||||
sourceBackupHash?: string;
|
||||
restartRequired?: boolean;
|
||||
changedFiles?: string[];
|
||||
extractedFileCount?: number;
|
||||
extractedBytes?: number;
|
||||
largestFileBytes?: number;
|
||||
preExistingFiles?: string[];
|
||||
error?: string;
|
||||
startedAt: string;
|
||||
|
||||
@@ -54,6 +54,9 @@ type UpgradeJob = {
|
||||
sourceBackupHash?: string;
|
||||
restartRequired?: boolean;
|
||||
changedFiles?: string[];
|
||||
extractedFileCount?: number;
|
||||
extractedBytes?: number;
|
||||
largestFileBytes?: number;
|
||||
preExistingFiles?: string[];
|
||||
error?: string;
|
||||
startedAt: string;
|
||||
@@ -519,7 +522,9 @@ function UpgradeStatusPanel({
|
||||
<InfoRow label="任务 ID" value={job.id} />
|
||||
<InfoRow label="升级包" value={job.packageName} />
|
||||
<InfoRow label="当前步骤" value={job.step} />
|
||||
<InfoRow label="文件数量" value={`${changedFiles.length} 个文件`} />
|
||||
<InfoRow label="文件数量" value={`${job.extractedFileCount ?? changedFiles.length} 个文件`} />
|
||||
{typeof job.extractedBytes === 'number' && <InfoRow label="解压大小" value={formatBytes(job.extractedBytes)} />}
|
||||
{typeof job.largestFileBytes === 'number' && <InfoRow label="最大文件" value={formatBytes(job.largestFileBytes)} />}
|
||||
<InfoRow label="需要重启" value={job.restartRequired ? '是' : '否'} />
|
||||
{job.staleAt && <InfoRow label="超时标记" value={formatDate(job.staleAt)} />}
|
||||
{job.backupFile && <InfoRow label="数据备份" value={job.backupFile} />}
|
||||
|
||||
Reference in New Issue
Block a user