@xcanwin/manyoyo 7.0.5 → 7.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ // 新容器主要复用镜像的只读层(overlayfs),没有历史容器数据可参考时,
4
+ // 按这个偏保守的写层体积假设估算——避免刚起步就把估算数拉得虚高
5
+ const DEFAULT_FALLBACK_CONTAINER_BYTES = 100 * 1024 * 1024;
6
+
7
+ function toPositiveNumber(value) {
8
+ const n = Number(value);
9
+ return Number.isFinite(n) && n > 0 ? n : 0;
10
+ }
11
+
12
+ function estimateContainerCapacity(options = {}) {
13
+ const runCommand = typeof options.runCommand === 'function'
14
+ ? options.runCommand
15
+ : () => { throw new Error('未配置命令执行器'); };
16
+ const statfs = typeof options.statfs === 'function'
17
+ ? options.statfs
18
+ : () => { throw new Error('未配置磁盘空间检测器'); };
19
+
20
+ const runtimeCommand = options.runtimeCommand || 'docker';
21
+ const imageRef = `${options.imageName || ''}:${options.imageVersion || ''}`;
22
+ const containerNames = Array.isArray(options.containerNames) ? options.containerNames : [];
23
+ const diskPath = options.diskPath || '';
24
+ const notes = [];
25
+
26
+ let imageSizeBytes = 0;
27
+ try {
28
+ const raw = runCommand(runtimeCommand, ['image', 'inspect', imageRef, '--format', '{{.Size}}']);
29
+ imageSizeBytes = toPositiveNumber(String(raw).trim().split('\n')[0]);
30
+ } catch (error) {
31
+ notes.push(`镜像 ${imageRef} 尚未在本地构建/拉取,暂无法读取体积。`);
32
+ }
33
+
34
+ let averageWritableBytes = 0;
35
+ let sampledCount = 0;
36
+ if (containerNames.length > 0) {
37
+ try {
38
+ const raw = runCommand(runtimeCommand, ['inspect', '--size', '--format', '{{.SizeRw}}', ...containerNames]);
39
+ const sizes = String(raw)
40
+ .split('\n')
41
+ .map(line => toPositiveNumber(line.trim()))
42
+ .filter(size => size > 0);
43
+ if (sizes.length > 0) {
44
+ averageWritableBytes = sizes.reduce((sum, size) => sum + size, 0) / sizes.length;
45
+ sampledCount = sizes.length;
46
+ }
47
+ } catch (error) {
48
+ notes.push('无法读取现有容器的可写层体积,改用保守默认值估算。');
49
+ }
50
+ }
51
+
52
+ const perContainerEstimateBytes = averageWritableBytes > 0 ? averageWritableBytes : DEFAULT_FALLBACK_CONTAINER_BYTES;
53
+ if (sampledCount === 0) {
54
+ notes.push(`当前没有可参考的历史容器数据,按 ${Math.round(DEFAULT_FALLBACK_CONTAINER_BYTES / (1024 * 1024))}MB / 容器的保守假设估算。`);
55
+ }
56
+
57
+ let availableBytes = 0;
58
+ try {
59
+ const stat = statfs(diskPath);
60
+ availableBytes = toPositiveNumber(stat.bavail) * toPositiveNumber(stat.bsize);
61
+ } catch (error) {
62
+ notes.push('无法读取宿主机磁盘剩余空间。');
63
+ }
64
+
65
+ const estimatedAdditionalContainers = perContainerEstimateBytes > 0 && availableBytes > 0
66
+ ? Math.max(0, Math.floor(availableBytes / perContainerEstimateBytes))
67
+ : null;
68
+
69
+ notes.push('新容器主要复用镜像的只读层,磁盘增量主要来自容器自身的可写层;这里按现有容器可写层的平均值估算,实际会随使用情况波动,仅供参考。');
70
+
71
+ return {
72
+ runtimeCommand,
73
+ image: {
74
+ reference: imageRef,
75
+ sizeBytes: imageSizeBytes || null
76
+ },
77
+ container: {
78
+ averageWritableBytes: Math.round(perContainerEstimateBytes),
79
+ sampledCount
80
+ },
81
+ disk: {
82
+ path: diskPath,
83
+ availableBytes: availableBytes || null
84
+ },
85
+ estimatedAdditionalContainers,
86
+ notes
87
+ };
88
+ }
89
+
90
+ module.exports = {
91
+ estimateContainerCapacity
92
+ };
@@ -231,8 +231,69 @@ function applyTextReplacements(text, replacements) {
231
231
  .reduce((result, item) => `${result.slice(0, item.start)}${item.text}${result.slice(item.end)}`, text);
232
232
  }
233
233
 
234
+ function formatPropertyKey(key) {
235
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
236
+ }
237
+
238
+ // 永远只在对象的 '{' 后面插入,不管这个对象原来是空的还是已经有属性——
239
+ // 不用去判断"是不是最后一个属性、要不要补逗号",语法上总是安全的
240
+ function insertObjectProperty(text, objectStartIndex, key, valueText) {
241
+ const prefix = `\n ${formatPropertyKey(key)}: `;
242
+ const insertion = `${prefix}${valueText},`;
243
+ const insertPos = objectStartIndex + 1;
244
+ const nextText = `${text.slice(0, insertPos)}${insertion}${text.slice(insertPos)}`;
245
+ return { text: nextText, valueStart: insertPos + prefix.length };
246
+ }
247
+
248
+ // findValueRangeByPath 的"读+建"版本:路径上缺失的中间对象会被逐级创建,
249
+ // 最终这个 key 已存在就原地替换值,不存在就插入新属性;已有内容/注释不受影响。
250
+ // 路径中间遇到"已存在但不是对象"的值会直接抛错,不做任何改动(避免静默吞掉用户数据)。
251
+ function upsertValueByPath(text, pathParts, valueText) {
252
+ if (!Array.isArray(pathParts) || pathParts.length === 0) {
253
+ throw new Error('path 不能为空');
254
+ }
255
+
256
+ let objectStart = findRootObjectStart(text);
257
+ if (objectStart === -1) {
258
+ throw new Error('未找到根对象');
259
+ }
260
+
261
+ let currentText = text;
262
+ let currentObjectStart = objectStart;
263
+
264
+ for (let i = 0; i < pathParts.length; i += 1) {
265
+ const key = pathParts[i];
266
+ const isLast = i === pathParts.length - 1;
267
+ const existingRange = findObjectPropertyValueRange(currentText, currentObjectStart, key);
268
+
269
+ if (existingRange) {
270
+ if (isLast) {
271
+ return applyTextReplacements(currentText, [
272
+ { start: existingRange.start, end: existingRange.end, text: valueText }
273
+ ]);
274
+ }
275
+ const nextObjectStart = skipTrivia(currentText, existingRange.start);
276
+ if (currentText[nextObjectStart] !== '{') {
277
+ throw new Error(`路径 ${pathParts.slice(0, i + 1).join('.')} 已存在且不是对象,无法继续`);
278
+ }
279
+ currentObjectStart = nextObjectStart;
280
+ continue;
281
+ }
282
+
283
+ const inserted = insertObjectProperty(currentText, currentObjectStart, key, isLast ? valueText : '{}');
284
+ currentText = inserted.text;
285
+ if (isLast) {
286
+ return currentText;
287
+ }
288
+ currentObjectStart = inserted.valueStart;
289
+ }
290
+
291
+ return currentText;
292
+ }
293
+
234
294
  module.exports = {
235
295
  findTopLevelPropertyValueRange,
236
296
  findValueRangeByPath,
237
- applyTextReplacements
297
+ applyTextReplacements,
298
+ upsertValueByPath
238
299
  };