dsh-plugin-pack 1.0.9 → 1.0.10
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.
- package/lib/fetch-bundle.js +2 -1
- package/lib/inbox.js +85 -5
- package/lib/server.js +39 -3
- package/package.json +1 -1
package/lib/fetch-bundle.js
CHANGED
|
@@ -417,8 +417,9 @@ export async function fetchBundleFrom({ source, home, onProgress = () => {} }) {
|
|
|
417
417
|
report(`下载完成:${(data.length / 1048576).toFixed(1)} MiB`);
|
|
418
418
|
|
|
419
419
|
// 落到 inbox(保留扩展名,后续按 .zip/.tgz 分流)
|
|
420
|
+
// origin='network':安装成功后可以自动删掉这份副本,不占用户磁盘
|
|
420
421
|
pruneSafe(home);
|
|
421
|
-
const saved = saveIncomingZip({ home, filename: name, data });
|
|
422
|
+
const saved = saveIncomingZip({ home, filename: name, data, origin: 'network' });
|
|
422
423
|
report(`已保存到 ${saved.file}`);
|
|
423
424
|
|
|
424
425
|
/* 校验阶段分两种:
|
package/lib/inbox.js
CHANGED
|
@@ -20,6 +20,32 @@ import { ensureDir, removeTree } from './util.js';
|
|
|
20
20
|
/** 单次落盘上限(默认 1 GiB,够大包也够防呆)。 */
|
|
21
21
|
const MAX_BYTES = 1024 * 1024 * 1024;
|
|
22
22
|
|
|
23
|
+
/** 记录这份 inbox 副本来源的标记文件名。 */
|
|
24
|
+
const ORIGIN_MARK = '.origin';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 在 root 下建一个唯一的子目录:`<时间戳>-<4位随机>`。
|
|
28
|
+
*
|
|
29
|
+
* stamp() 只到秒,同一秒内多次落盘会撞名;加随机后缀既保持可读
|
|
30
|
+
* (按时间排序仍然有效),又能避免互相覆盖。
|
|
31
|
+
*
|
|
32
|
+
* @param {string} root
|
|
33
|
+
* @returns {string} 已创建的目录绝对路径
|
|
34
|
+
*/
|
|
35
|
+
function uniqueDir(root) {
|
|
36
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
37
|
+
const suffix = Math.random().toString(16).slice(2, 6).padEnd(4, '0');
|
|
38
|
+
const dir = path.join(root, `${stamp()}-${suffix}`);
|
|
39
|
+
if (fs.existsSync(dir)) continue;
|
|
40
|
+
ensureDir(dir);
|
|
41
|
+
return dir;
|
|
42
|
+
}
|
|
43
|
+
// 极端情况下兜底:用纳秒精度的时间戳
|
|
44
|
+
const dir = path.join(root, `${stamp()}-${Date.now() % 100000}`);
|
|
45
|
+
ensureDir(dir);
|
|
46
|
+
return dir;
|
|
47
|
+
}
|
|
48
|
+
|
|
23
49
|
/**
|
|
24
50
|
* 从 multipart/form-data 里解析出文件条目。
|
|
25
51
|
*
|
|
@@ -27,8 +53,7 @@ const MAX_BYTES = 1024 * 1024 * 1024;
|
|
|
27
53
|
* 不引第三方库。
|
|
28
54
|
*
|
|
29
55
|
* @param {Buffer} body 整个请求体
|
|
30
|
-
* @param {string} boundary boundary 字符串(不含前置 --)
|
|
31
|
-
* @returns {Array<{ name: string, filename: string|null, data: Buffer }>}
|
|
56
|
+
* @param {string} boundary boundary 字符串(不含前置 --) * @returns {Array<{ name: string, filename: string|null, data: Buffer }>}
|
|
32
57
|
*/
|
|
33
58
|
export function parseMultipart(body, boundary) {
|
|
34
59
|
const delimiter = Buffer.from(`--${boundary}`);
|
|
@@ -87,9 +112,12 @@ export function boundaryOf(contentType) {
|
|
|
87
112
|
* @param {string} options.home $DSH_HOME
|
|
88
113
|
* @param {string} options.filename 原始文件名
|
|
89
114
|
* @param {Buffer} options.data 内容
|
|
115
|
+
* @param {'upload'|'network'} [options.origin]
|
|
116
|
+
* 来源。`network` = 从 GitHub/npm 下载来的,装完可以自动删;
|
|
117
|
+
* `upload` = 用户从本地拖/选的,装完要留着(用户自己的文件)。
|
|
90
118
|
* @returns {{ file: string, bytes: number }}
|
|
91
119
|
*/
|
|
92
|
-
export function saveIncomingZip({ home, filename, data }) {
|
|
120
|
+
export function saveIncomingZip({ home, filename, data, origin = 'upload' }) {
|
|
93
121
|
if (!data?.length) {
|
|
94
122
|
const error = new Error('上传内容为空');
|
|
95
123
|
error.code = 'BAD_UPLOAD';
|
|
@@ -101,8 +129,16 @@ export function saveIncomingZip({ home, filename, data }) {
|
|
|
101
129
|
throw error;
|
|
102
130
|
}
|
|
103
131
|
const base = safeName(String(filename ?? '').trim() || `bundle-${stamp()}.zip`);
|
|
104
|
-
|
|
105
|
-
|
|
132
|
+
/* 目录名要唯一:stamp() 只到秒,同一秒内连拖两个包会落到同一个目录,
|
|
133
|
+
* 后一个会覆盖前一个的 .origin 标记(甚至同名时覆盖文件本身)。
|
|
134
|
+
* 加个短随机后缀避开。 */
|
|
135
|
+
const dir = uniqueDir(path.join(home, 'plugin-pack', 'inbox'));
|
|
136
|
+
// 记下来源:安装成功后据此决定要不要删掉这份落盘副本
|
|
137
|
+
try {
|
|
138
|
+
fs.writeFileSync(path.join(dir, ORIGIN_MARK), origin === 'network' ? 'network' : 'upload');
|
|
139
|
+
} catch {
|
|
140
|
+
/* 标记写不进去不影响导入 */
|
|
141
|
+
}
|
|
106
142
|
// 保留原始扩展名:.zip = 整合包,.tgz/.tar.gz = 单个插件的 npm tarball
|
|
107
143
|
const lower = base.toLowerCase();
|
|
108
144
|
const hasKnownExt = lower.endsWith('.zip') || lower.endsWith('.tgz') || lower.endsWith('.tar.gz');
|
|
@@ -192,3 +228,47 @@ export function pruneInbox(home, days = 7) {
|
|
|
192
228
|
}
|
|
193
229
|
return removed;
|
|
194
230
|
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 读这份 inbox 副本的来源。
|
|
234
|
+
*
|
|
235
|
+
* @param {string} file inbox 里的文件绝对路径
|
|
236
|
+
* @returns {'upload'|'network'|null} 读不到标记时返回 null(老版本落盘的文件)
|
|
237
|
+
*/
|
|
238
|
+
export function inboxOrigin(file) {
|
|
239
|
+
try {
|
|
240
|
+
const mark = path.join(path.dirname(String(file)), ORIGIN_MARK);
|
|
241
|
+
const text = fs.readFileSync(mark, 'utf8').trim();
|
|
242
|
+
return text === 'network' ? 'network' : 'upload';
|
|
243
|
+
} catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 删掉 inbox 里的一份落盘副本(连它所在的时间戳目录一起删)。
|
|
250
|
+
*
|
|
251
|
+
* 只删 inbox 内部的目录:传进来的路径必须在 `<home>/plugin-pack/inbox` 之下,
|
|
252
|
+
* 否则直接拒绝 —— 免得哪天参数传错把用户自己的文件删了。
|
|
253
|
+
*
|
|
254
|
+
* @param {string} home $DSH_HOME
|
|
255
|
+
* @param {string} file inbox 里的文件路径
|
|
256
|
+
* @returns {boolean} 是否真的删了
|
|
257
|
+
*/
|
|
258
|
+
export function removeInboxEntry(home, file) {
|
|
259
|
+
try {
|
|
260
|
+
const root = path.resolve(home, 'plugin-pack', 'inbox');
|
|
261
|
+
const target = path.resolve(String(file));
|
|
262
|
+
// 必须严格在 inbox 之下,且不能就是 inbox 本身
|
|
263
|
+
if (target === root || !target.startsWith(root + path.sep)) return false;
|
|
264
|
+
const dir = path.dirname(target);
|
|
265
|
+
// 目录必须是 inbox 的直接子目录(时间戳目录),不能是更外层
|
|
266
|
+
if (path.dirname(dir) !== root) return false;
|
|
267
|
+
// 目录本身要存在,否则不算"删掉了"
|
|
268
|
+
if (!fs.existsSync(dir)) return false;
|
|
269
|
+
removeTree(dir);
|
|
270
|
+
return true;
|
|
271
|
+
} catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
package/lib/server.js
CHANGED
|
@@ -28,7 +28,15 @@ import { importBundle, inspectBundle, rollbackImport, listBackups } from './impo
|
|
|
28
28
|
import { reloadClientPackage, clientStatus, localVerdict } from '../hotload.js';
|
|
29
29
|
import { importLocalPlugin } from './import-plugin.js';
|
|
30
30
|
import { importTarball, isTarball } from './import-tgz.js';
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
parseMultipart,
|
|
33
|
+
boundaryOf,
|
|
34
|
+
saveIncomingZip,
|
|
35
|
+
saveIncomingDir,
|
|
36
|
+
pruneInbox,
|
|
37
|
+
inboxOrigin,
|
|
38
|
+
removeInboxEntry,
|
|
39
|
+
} from './inbox.js';
|
|
32
40
|
import { runJob, listJobs, getJob, runningJobs, clearFinished, openWaiting, getWaiting, listWaiting, cancelWaiting } from './jobs.js';
|
|
33
41
|
import { fetchBundleFrom, parseSource } from './fetch-bundle.js';
|
|
34
42
|
import { dshHome as resolveDshHome, profileDir as resolveProfileDir, profileName as resolveProfileName, humanBytes } from './home.js';
|
|
@@ -80,6 +88,28 @@ function sendJson(response, status, value) {
|
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
/** 把异常翻译成 HTTP 状态;其余仍是 500。 */
|
|
91
|
+
/**
|
|
92
|
+
* 安装成功后清理 inbox 里的落盘副本。
|
|
93
|
+
*
|
|
94
|
+
* 只删「从网络下载」的那份;用户自己拖/选的本地文件永远保留 ——
|
|
95
|
+
* 那是他自己的文件,工具不该动它。
|
|
96
|
+
*
|
|
97
|
+
* @param {object} config
|
|
98
|
+
* @param {string} file 本次导入用的文件路径
|
|
99
|
+
* @returns {boolean} 是否真的删掉了
|
|
100
|
+
*/
|
|
101
|
+
function autoCleanInbox(config, file) {
|
|
102
|
+
try {
|
|
103
|
+
if (!file) return false;
|
|
104
|
+
const home = contextOf(config).home;
|
|
105
|
+
// 只有网络下载的来源才删
|
|
106
|
+
if (inboxOrigin(file) !== 'network') return false;
|
|
107
|
+
return removeInboxEntry(home, file);
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
83
113
|
function statusOf(error) {
|
|
84
114
|
const explicit = Number(error?.status ?? error?.statusCode);
|
|
85
115
|
if (Number.isFinite(explicit) && explicit >= 400 && explicit < 600) return explicit;
|
|
@@ -590,7 +620,9 @@ export function mountRoutes(ctx, config = {}) {
|
|
|
590
620
|
meta: { file: body.file },
|
|
591
621
|
task: (report) => doImport(report),
|
|
592
622
|
});
|
|
593
|
-
|
|
623
|
+
// 网络下载来的 tgz,装完同样清掉副本
|
|
624
|
+
const cleanedTgz = autoCleanInbox(config, body.file);
|
|
625
|
+
sendJson(response, 200, { ...result, jobId: id, ...(cleanedTgz ? { inboxCleaned: true } : {}) });
|
|
594
626
|
return;
|
|
595
627
|
}
|
|
596
628
|
const dryRun = Boolean(body.dryRun);
|
|
@@ -641,7 +673,11 @@ export function mountRoutes(ctx, config = {}) {
|
|
|
641
673
|
jobId: body.jobId ? String(body.jobId) : undefined,
|
|
642
674
|
task: (report) => doImportBundle(report),
|
|
643
675
|
});
|
|
644
|
-
|
|
676
|
+
/* 装完清掉网络下载的副本:
|
|
677
|
+
* 从 GitHub/npm 下下来的包,装好后原件就没用了,留着白占磁盘。
|
|
678
|
+
* 但用户自己拖/选的本地文件(origin='upload')一律保留 —— 那是他的东西。 */
|
|
679
|
+
const cleaned = autoCleanInbox(config, body.file);
|
|
680
|
+
sendJson(response, 200, { ...result, jobId: id, ...(cleaned ? { inboxCleaned: true } : {}) });
|
|
645
681
|
},
|
|
646
682
|
},
|
|
647
683
|
'plugin-pack: import',
|
package/package.json
CHANGED