@nextclaw/app-runtime 0.10.0 → 0.11.0
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/dist/index.d.ts +6 -2
- package/dist/index.js +5 -1
- package/dist/package.js +1 -1
- package/dist/services/app-home.service.d.ts +4 -0
- package/dist/services/app-home.service.d.ts.map +1 -1
- package/dist/services/app-home.service.js +15 -1
- package/dist/services/app-home.service.js.map +1 -1
- package/dist/services/app-install-source.service.js +14 -1
- package/dist/services/app-install-source.service.js.map +1 -1
- package/dist/services/app-installation-filesystem.service.js +68 -0
- package/dist/services/app-installation-filesystem.service.js.map +1 -0
- package/dist/services/app-installation-integrity.service.d.ts +20 -0
- package/dist/services/app-installation-integrity.service.d.ts.map +1 -0
- package/dist/services/app-installation-integrity.service.js +84 -0
- package/dist/services/app-installation-integrity.service.js.map +1 -0
- package/dist/services/app-installation-lifecycle.service.js +110 -0
- package/dist/services/app-installation-lifecycle.service.js.map +1 -0
- package/dist/services/app-installation.service.d.ts +21 -11
- package/dist/services/app-installation.service.d.ts.map +1 -1
- package/dist/services/app-installation.service.js +262 -280
- package/dist/services/app-installation.service.js.map +1 -1
- package/dist/services/app-instance-storage.service.d.ts +49 -0
- package/dist/services/app-instance-storage.service.d.ts.map +1 -0
- package/dist/services/app-instance-storage.service.js +256 -0
- package/dist/services/app-instance-storage.service.js.map +1 -0
- package/dist/services/app-manifest.service.d.ts +5 -1
- package/dist/services/app-manifest.service.d.ts.map +1 -1
- package/dist/services/app-manifest.service.js +59 -3
- package/dist/services/app-manifest.service.js.map +1 -1
- package/dist/services/app-publish.service.js +1 -1
- package/dist/services/app-publish.service.js.map +1 -1
- package/dist/services/app-registry.service.d.ts +13 -3
- package/dist/services/app-registry.service.d.ts.map +1 -1
- package/dist/services/app-registry.service.js +143 -13
- package/dist/services/app-registry.service.js.map +1 -1
- package/dist/services/file-lock.service.d.ts +20 -0
- package/dist/services/file-lock.service.d.ts.map +1 -0
- package/dist/services/file-lock.service.js +156 -0
- package/dist/services/file-lock.service.js.map +1 -0
- package/dist/types/app-installation.types.d.ts +7 -0
- package/dist/types/app-installation.types.d.ts.map +1 -1
- package/dist/types/app-manifest.types.d.ts +22 -1
- package/dist/types/app-manifest.types.d.ts.map +1 -1
- package/dist/types/app-manifest.types.js.map +1 -1
- package/dist/types/app-registry.types.d.ts +7 -1
- package/dist/types/app-registry.types.d.ts.map +1 -1
- package/dist/types/app-storage.types.d.ts +37 -0
- package/dist/types/app-storage.types.d.ts.map +1 -0
- package/dist/types/app-storage.types.js +7 -0
- package/dist/types/app-storage.types.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-installation.service.d.ts","names":[],"sources":["../../src/services/app-installation.service.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"app-installation.service.d.ts","names":[],"sources":["../../src/services/app-installation.service.ts"],"mappings":";;;;;;;;;;;;;KAsCK,iBAAA;EACH,WAAA;EACA,UAAA,GAAa,yBAAA;EACb,QAAA;EACA,gBAAA,GAAmB,YAAA;AAAA;AAAA,cAWR,sBAAA;EAAA,iBAUQ,cAAA;EAAA,iBACA,aAAA;EAAA,iBACA,eAAA;EAAA,iBACA,YAAA;EAAA,iBACA,eAAA;EAAA,iBACA,qBAAA;EAAA,iBAGA,oBAAA;EAAA,iBAjBF,oBAAA;EAAA,iBACA,sBAAA;EAAA,iBACA,eAAA;EAAA,iBACA,gBAAA;EAAA,iBACA,iBAAA;EAAA,iBACA,gBAAA;EAAA,iBACA,mBAAA;cAGE,cAAA,GAAgB,cAAA,EAChB,aAAA,GAAe,gBAAA,EACf,eAAA,GAAiB,kBAAA,EACjB,YAAA,GAAc,eAAA,EACd,eAAA,GAAiB,kBAAA,EACjB,qBAAA,GAAuB,wBAAA,EAGvB,oBAAA,GAAsB,8BAAA,EAGvC,sBAAA,GAAyB,yBAAA;EAyB3B,OAAA,GACE,SAAA,UACA,OAAA,GAAS,iBAAA,KACR,OAAA,CAAQ,gBAAA;EAAA,QAkCH,qBAAA;EAAA,QAUA,qBAAA;EAmIR,MAAA,GACE,KAAA,UACA,OAAA;IACE,OAAA;IACA,WAAA;IACA,UAAA,GAAa,yBAAA;IACb,QAAA;EAAA,MAED,OAAA,CAAQ,eAAA;EAOX,SAAA,GACE,KAAA,UACA,SAAA,cACC,OAAA,CAAQ,kBAAA;EAOX,IAAA,QAAiB,OAAA,CAAQ,oBAAA;EAkBzB,mBAAA,QAAgC,OAAA;EA0GhC,IAAA,GAAc,KAAA,aAAgB,OAAA,CAAQ,aAAA;EAyCtC,UAAA,GAAoB,KAAA,UAAe,OAAA,cAAmB,OAAA,CAAQ,mBAAA;EAW9D,QAAA,GAAkB,KAAA,UAAe,OAAA,aAAkB,OAAA,CAAQ,iBAAA;EAoC3D,sBAAA,GAAgC,KAAA,UAAe,OAAA,cAAmB,OAAA;EAqBlE,gBAAA,MAA6B,KAAA,UAAe,SAAA,QAAiB,OAAA,CAAQ,CAAA,MAAK,OAAA,CAAQ,CAAA;EAgBlF,aAAA,GACE,YAAA,UACA,wBAAA,EAA0B,mBAAA,KACzB,OAAA,CAAQ,mBAAA;EA6BX,aAAA,GACE,KAAA,sBACA,gBAAA,EAAkB,mBAAA,KACjB,OAAA;AAAA"}
|
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
import { AppManifestService } from "./app-manifest.service.js";
|
|
2
|
-
import { isAppComponentManifestBundle
|
|
2
|
+
import { isAppComponentManifestBundle } from "../types/app-manifest.types.js";
|
|
3
3
|
import { AppBundleService } from "./app-bundle.service.js";
|
|
4
4
|
import { AppHomeService } from "./app-home.service.js";
|
|
5
|
+
import { FileLockService } from "./file-lock.service.js";
|
|
6
|
+
import { AppInstanceStorageService } from "./app-instance-storage.service.js";
|
|
7
|
+
import { AppInstallationIntegrityService } from "./app-installation-integrity.service.js";
|
|
8
|
+
import { AppInstallationFilesystemService } from "./app-installation-filesystem.service.js";
|
|
9
|
+
import { AppInstallationLifecycleService } from "./app-installation-lifecycle.service.js";
|
|
5
10
|
import { AppBuildService } from "./app-build.service.js";
|
|
6
11
|
import { AppRegistryConfigService } from "./app-registry-config.service.js";
|
|
7
12
|
import { AppRemoteRegistryClientService } from "./app-remote-registry-client.service.js";
|
|
8
13
|
import { AppRegistryService } from "./app-registry.service.js";
|
|
9
14
|
import { AppInstallSourceService } from "./app-install-source.service.js";
|
|
10
|
-
import {
|
|
11
|
-
import { access, cp, mkdir, readdir, rename, rm } from "node:fs/promises";
|
|
15
|
+
import { rm } from "node:fs/promises";
|
|
12
16
|
import path from "node:path";
|
|
17
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
18
|
//#region src/services/app-installation.service.ts
|
|
19
|
+
const SAFE_APP_ID_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
|
|
14
20
|
var AppInstallationService = class {
|
|
15
21
|
installSourceService;
|
|
16
|
-
|
|
22
|
+
instanceStorageService;
|
|
23
|
+
fileLockService = new FileLockService();
|
|
24
|
+
integrityService;
|
|
25
|
+
filesystemService;
|
|
26
|
+
lifecycleService;
|
|
27
|
+
appOperationContext = new AsyncLocalStorage();
|
|
28
|
+
constructor(appHomeService = new AppHomeService(), bundleService = new AppBundleService(), manifestService = new AppManifestService(), buildService = new AppBuildService(), registryService = new AppRegistryService(appHomeService), registryConfigService = new AppRegistryConfigService(appHomeService), remoteRegistryClient = new AppRemoteRegistryClientService(new AppRegistryConfigService(appHomeService)), instanceStorageService) {
|
|
17
29
|
this.appHomeService = appHomeService;
|
|
18
30
|
this.bundleService = bundleService;
|
|
19
31
|
this.manifestService = manifestService;
|
|
@@ -21,98 +33,49 @@ var AppInstallationService = class {
|
|
|
21
33
|
this.registryService = registryService;
|
|
22
34
|
this.registryConfigService = registryConfigService;
|
|
23
35
|
this.remoteRegistryClient = remoteRegistryClient;
|
|
24
|
-
this.installSourceService = new AppInstallSourceService(this.manifestService, this.remoteRegistryClient);
|
|
36
|
+
this.installSourceService = new AppInstallSourceService(this.manifestService, this.remoteRegistryClient, this.bundleService);
|
|
37
|
+
this.instanceStorageService = instanceStorageService ?? new AppInstanceStorageService(appHomeService);
|
|
38
|
+
this.integrityService = new AppInstallationIntegrityService(manifestService);
|
|
39
|
+
this.filesystemService = new AppInstallationFilesystemService({
|
|
40
|
+
buildService,
|
|
41
|
+
integrityService: this.integrityService,
|
|
42
|
+
manifestService
|
|
43
|
+
});
|
|
44
|
+
this.lifecycleService = new AppInstallationLifecycleService({
|
|
45
|
+
registryService,
|
|
46
|
+
registryConfigService,
|
|
47
|
+
remoteRegistryClient,
|
|
48
|
+
instanceStorageService: this.instanceStorageService,
|
|
49
|
+
integrityService: this.integrityService,
|
|
50
|
+
install: this.install,
|
|
51
|
+
rollback: this.rollback
|
|
52
|
+
});
|
|
25
53
|
}
|
|
26
|
-
install = async (appSource, options) => {
|
|
27
|
-
const { onProgress, registryUrl } = options
|
|
54
|
+
install = async (appSource, options = {}) => {
|
|
55
|
+
const { onProgress, registryUrl } = options;
|
|
28
56
|
await onProgress?.("resolving");
|
|
29
57
|
const source = await this.installSourceService.resolve(appSource, registryUrl);
|
|
30
58
|
const tempDirectory = await this.appHomeService.createTemporaryDirectory("napp-install-");
|
|
31
59
|
try {
|
|
32
|
-
|
|
33
|
-
const bundlePath = source.kind === "directory" ? (await this.bundleService.packAppDirectory({
|
|
34
|
-
appDirectory: source.appDirectory,
|
|
35
|
-
outputPath: path.join(tempDirectory, "app.napp")
|
|
36
|
-
})).bundlePath : source.kind === "bundle" ? source.bundlePath : (await this.remoteRegistryClient.downloadBundle({
|
|
37
|
-
resolution: source.registryResolution,
|
|
38
|
-
targetDirectory: tempDirectory
|
|
39
|
-
})).bundlePath;
|
|
60
|
+
const bundlePath = await this.installSourceService.materializeBundle(source, tempDirectory, onProgress);
|
|
40
61
|
await onProgress?.("verifying");
|
|
41
62
|
const extractedDirectory = path.join(tempDirectory, "bundle");
|
|
42
63
|
const extractedMetadata = await this.bundleService.extractBundle({
|
|
43
64
|
bundlePath,
|
|
44
65
|
targetDirectory: extractedDirectory
|
|
45
66
|
});
|
|
46
|
-
await this.materializeDistribution({
|
|
67
|
+
await this.filesystemService.materializeDistribution({
|
|
47
68
|
appDirectory: extractedDirectory,
|
|
48
69
|
distributionMode: extractedMetadata.metadata.distributionMode
|
|
49
70
|
});
|
|
50
71
|
const manifestBundle = await this.manifestService.load(extractedDirectory);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
await onProgress?.("installing");
|
|
54
|
-
const dataDirectory = this.appHomeService.getAppDataDirectory(manifestBundle.manifest.id);
|
|
55
|
-
await this.copyToImmutableInstallDirectory({
|
|
56
|
-
appId: manifestBundle.manifest.id,
|
|
57
|
-
appVersion: manifestBundle.manifest.version,
|
|
72
|
+
return await this.installPreparedBundle({
|
|
73
|
+
source,
|
|
58
74
|
extractedDirectory,
|
|
59
|
-
|
|
75
|
+
extractedMetadata,
|
|
76
|
+
manifestBundle,
|
|
77
|
+
options
|
|
60
78
|
});
|
|
61
|
-
await mkdir(dataDirectory, { recursive: true });
|
|
62
|
-
await onProgress?.("finalizing");
|
|
63
|
-
let registryRecord;
|
|
64
|
-
try {
|
|
65
|
-
registryRecord = await this.registryService.upsertInstallation({
|
|
66
|
-
appId: manifestBundle.manifest.id,
|
|
67
|
-
name: manifestBundle.manifest.name,
|
|
68
|
-
description: manifestBundle.manifest.description,
|
|
69
|
-
version: manifestBundle.manifest.version,
|
|
70
|
-
installDirectory,
|
|
71
|
-
dataDirectory,
|
|
72
|
-
sourceKind: source.kind,
|
|
73
|
-
distributionMode: source.kind === "registry" ? source.registryResolution.distributionMode : extractedMetadata.metadata.distributionMode,
|
|
74
|
-
sourceRef: source.sourceRef,
|
|
75
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
76
|
-
permissions: manifestBundle.manifest.schemaVersion === 1 ? manifestBundle.manifest.permissions ?? {} : {},
|
|
77
|
-
registryUrl: source.kind === "registry" ? source.registryResolution.registryUrl : void 0,
|
|
78
|
-
bundleUrl: source.kind === "registry" ? source.registryResolution.bundleUrl : void 0,
|
|
79
|
-
sha256: source.kind === "registry" ? source.registryResolution.sha256 : void 0,
|
|
80
|
-
publisher: source.kind === "registry" ? source.registryResolution.publisher : void 0,
|
|
81
|
-
manifestSchemaVersion: manifestBundle.manifest.schemaVersion,
|
|
82
|
-
components: isAppComponentManifestBundle(manifestBundle) ? manifestBundle.components.map((component) => ({
|
|
83
|
-
...component,
|
|
84
|
-
componentDirectory: path.join(installDirectory, component.path),
|
|
85
|
-
manifestPath: path.join(installDirectory, component.path, component.kind === "panel" ? "panel-app.json" : "service-app.json")
|
|
86
|
-
})) : void 0,
|
|
87
|
-
primaryPanelId: isAppComponentManifestBundle(manifestBundle) ? manifestBundle.primaryPanelId : void 0
|
|
88
|
-
});
|
|
89
|
-
} catch (error) {
|
|
90
|
-
await rm(installDirectory, {
|
|
91
|
-
recursive: true,
|
|
92
|
-
force: true
|
|
93
|
-
});
|
|
94
|
-
throw error;
|
|
95
|
-
}
|
|
96
|
-
const activeVersionRecord = registryRecord.installedVersions[registryRecord.activeVersion];
|
|
97
|
-
return {
|
|
98
|
-
appId: registryRecord.appId,
|
|
99
|
-
name: registryRecord.name,
|
|
100
|
-
version: registryRecord.activeVersion,
|
|
101
|
-
installDirectory,
|
|
102
|
-
dataDirectory,
|
|
103
|
-
sourceKind: source.kind,
|
|
104
|
-
sourceRef: source.sourceRef,
|
|
105
|
-
distributionMode: registryRecord.installedVersions[registryRecord.activeVersion]?.distributionMode,
|
|
106
|
-
permissions: registryRecord.installedVersions[registryRecord.activeVersion]?.permissions ?? {},
|
|
107
|
-
registryUrl: registryRecord.installedVersions[registryRecord.activeVersion]?.registryUrl,
|
|
108
|
-
bundleUrl: registryRecord.installedVersions[registryRecord.activeVersion]?.bundleUrl,
|
|
109
|
-
sha256: registryRecord.installedVersions[registryRecord.activeVersion]?.sha256,
|
|
110
|
-
publisher: registryRecord.installedVersions[registryRecord.activeVersion]?.publisher,
|
|
111
|
-
enabled: registryRecord.enabled,
|
|
112
|
-
manifestSchemaVersion: activeVersionRecord?.manifestSchemaVersion ?? 1,
|
|
113
|
-
components: activeVersionRecord?.components,
|
|
114
|
-
primaryPanelId: activeVersionRecord?.primaryPanelId
|
|
115
|
-
};
|
|
116
79
|
} finally {
|
|
117
80
|
await rm(tempDirectory, {
|
|
118
81
|
recursive: true,
|
|
@@ -120,117 +83,104 @@ var AppInstallationService = class {
|
|
|
120
83
|
});
|
|
121
84
|
}
|
|
122
85
|
};
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
await
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
const
|
|
86
|
+
installPreparedBundle = async (prepared) => {
|
|
87
|
+
const appId = prepared.manifestBundle.manifest.id;
|
|
88
|
+
return await this.withAppOperation(appId, async () => await this.commitPreparedInstall(prepared));
|
|
89
|
+
};
|
|
90
|
+
commitPreparedInstall = async (prepared) => {
|
|
91
|
+
const { extractedDirectory, extractedMetadata, manifestBundle, options, source } = prepared;
|
|
92
|
+
const { onProgress } = options;
|
|
93
|
+
const { id: appId, version } = manifestBundle.manifest;
|
|
94
|
+
const installDirectory = this.appHomeService.getInstallDirectory(appId, version);
|
|
95
|
+
if (source.kind === "registry" && appId !== source.registryResolution.appId) throw new Error(`bundle manifest.appId 与 registry 请求不一致:期望 ${source.registryResolution.appId},实际 ${appId}`);
|
|
96
|
+
await onProgress?.("installing");
|
|
97
|
+
const existingRecord = await this.registryService.getApp(appId);
|
|
98
|
+
const publisher = source.kind === "registry" ? source.registryResolution.publisher : options.trustedPublisher;
|
|
99
|
+
const currentPublisher = existingRecord?.publisher ?? existingRecord?.installedVersions[existingRecord.activeVersion]?.publisher;
|
|
100
|
+
if (currentPublisher && currentPublisher.id !== publisher?.id) throw new Error(`应用 ${appId} 已绑定发布者 ${currentPublisher.id},拒绝由 ${publisher?.id ?? "未验证本地来源"} 覆盖。`);
|
|
101
|
+
const dataSchemaVersion = manifestBundle.manifest.schemaVersion === 2 ? manifestBundle.manifest.storage?.schemaVersion ?? 1 : 1;
|
|
102
|
+
if (existingRecord?.defaultInstance.dataSchemaVersion !== void 0 && existingRecord.defaultInstance.dataSchemaVersion !== dataSchemaVersion) throw new Error(`应用 ${appId} 数据 schema 从 ${existingRecord.defaultInstance.dataSchemaVersion} 升级到 ${dataSchemaVersion},但包内没有受支持的迁移合同。更新已停止,现有数据和版本保持不变。`);
|
|
103
|
+
const instanceDirectory = this.appHomeService.getAppInstanceDirectory(appId, "default");
|
|
104
|
+
const instanceExisted = await this.integrityService.pathExists(instanceDirectory);
|
|
105
|
+
const contentSha256 = await this.filesystemService.copyToImmutableInstallDirectory({
|
|
131
106
|
appId,
|
|
132
|
-
version,
|
|
133
|
-
|
|
107
|
+
appVersion: version,
|
|
108
|
+
extractedDirectory,
|
|
109
|
+
installDirectory
|
|
134
110
|
});
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
dataDirectory: appRecord.dataDirectory,
|
|
144
|
-
sourceKind: activeVersionRecord.sourceKind,
|
|
145
|
-
sourceRef: activeVersionRecord.sourceRef,
|
|
146
|
-
permissions: activeVersionRecord.permissions,
|
|
147
|
-
registryUrl: activeVersionRecord.registryUrl,
|
|
148
|
-
bundleUrl: activeVersionRecord.bundleUrl,
|
|
149
|
-
sha256: activeVersionRecord.sha256,
|
|
150
|
-
publisher: activeVersionRecord.publisher,
|
|
151
|
-
enabled: appRecord.enabled,
|
|
152
|
-
manifestSchemaVersion: activeVersionRecord.manifestSchemaVersion,
|
|
153
|
-
components: activeVersionRecord.components,
|
|
154
|
-
primaryPanelId: activeVersionRecord.primaryPanelId,
|
|
155
|
-
updated: false
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
const installedTarget = appRecord.installedVersions[resolution.version];
|
|
159
|
-
if (installedTarget) {
|
|
160
|
-
await onProgress?.("verifying");
|
|
161
|
-
await onProgress?.("installing");
|
|
162
|
-
await this.rollback(appId, resolution.version);
|
|
163
|
-
await onProgress?.("finalizing");
|
|
164
|
-
return {
|
|
165
|
-
appId: appRecord.appId,
|
|
166
|
-
name: appRecord.name,
|
|
167
|
-
version: resolution.version,
|
|
168
|
-
previousVersion: appRecord.activeVersion,
|
|
169
|
-
installDirectory: installedTarget.installDirectory,
|
|
170
|
-
dataDirectory: appRecord.dataDirectory,
|
|
171
|
-
sourceKind: installedTarget.sourceKind,
|
|
172
|
-
distributionMode: installedTarget.distributionMode,
|
|
173
|
-
sourceRef: installedTarget.sourceRef,
|
|
174
|
-
permissions: installedTarget.permissions,
|
|
175
|
-
registryUrl: installedTarget.registryUrl,
|
|
176
|
-
bundleUrl: installedTarget.bundleUrl,
|
|
177
|
-
sha256: installedTarget.sha256,
|
|
178
|
-
publisher: installedTarget.publisher,
|
|
179
|
-
enabled: appRecord.enabled,
|
|
180
|
-
manifestSchemaVersion: installedTarget.manifestSchemaVersion,
|
|
181
|
-
components: installedTarget.components,
|
|
182
|
-
primaryPanelId: installedTarget.primaryPanelId,
|
|
183
|
-
updated: true
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
return {
|
|
187
|
-
...await this.install(`${appId}@${resolution.version}`, {
|
|
188
|
-
registryUrl,
|
|
189
|
-
onProgress
|
|
190
|
-
}),
|
|
191
|
-
previousVersion: appRecord.activeVersion,
|
|
192
|
-
updated: true
|
|
193
|
-
};
|
|
194
|
-
};
|
|
195
|
-
uninstall = async (appId, purgeData) => {
|
|
196
|
-
const appRecord = await this.registryService.getApp(appId);
|
|
197
|
-
if (!appRecord) throw new Error(`未找到已安装应用:${appId}`);
|
|
198
|
-
const removedVersions = Object.keys(appRecord.installedVersions).sort((left, right) => left.localeCompare(right));
|
|
199
|
-
const stagedPaths = [];
|
|
200
|
-
const stagePath = async (originalPath) => {
|
|
201
|
-
if (!await this.pathExists(originalPath)) return;
|
|
202
|
-
const stagedPath = `${originalPath}.uninstalling-${randomUUID()}`;
|
|
203
|
-
await rename(originalPath, stagedPath);
|
|
204
|
-
stagedPaths.push({
|
|
205
|
-
originalPath,
|
|
206
|
-
stagedPath
|
|
207
|
-
});
|
|
208
|
-
};
|
|
209
|
-
const restoreStagedPaths = async () => {
|
|
210
|
-
for (const entry of [...stagedPaths].reverse()) if (await this.pathExists(entry.stagedPath)) await rename(entry.stagedPath, entry.originalPath);
|
|
211
|
-
};
|
|
111
|
+
const defaultInstance = await this.instanceStorageService.materializeDefaultInstance({
|
|
112
|
+
appId,
|
|
113
|
+
publisherId: (currentPublisher ?? publisher)?.id,
|
|
114
|
+
legacyDataDirectory: existingRecord?.defaultInstance.storage.layout === "legacy" ? existingRecord.dataDirectory : void 0,
|
|
115
|
+
dataSchemaVersion
|
|
116
|
+
});
|
|
117
|
+
await onProgress?.("finalizing");
|
|
118
|
+
let registryRecord;
|
|
212
119
|
try {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
120
|
+
registryRecord = await this.registryService.upsertInstallation({
|
|
121
|
+
appId,
|
|
122
|
+
name: manifestBundle.manifest.name,
|
|
123
|
+
description: manifestBundle.manifest.description,
|
|
124
|
+
version,
|
|
125
|
+
installDirectory,
|
|
126
|
+
defaultInstance,
|
|
127
|
+
sourceKind: source.kind,
|
|
128
|
+
distributionMode: source.kind === "registry" ? source.registryResolution.distributionMode : extractedMetadata.metadata.distributionMode,
|
|
129
|
+
sourceRef: source.sourceRef,
|
|
130
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
131
|
+
permissions: manifestBundle.manifest.schemaVersion === 1 ? manifestBundle.manifest.permissions ?? {} : this.manifestService.resolvePlatformSecurity(manifestBundle.manifest).permissions,
|
|
132
|
+
registryUrl: source.kind === "registry" ? source.registryResolution.registryUrl : void 0,
|
|
133
|
+
bundleUrl: source.kind === "registry" ? source.registryResolution.bundleUrl : void 0,
|
|
134
|
+
sha256: source.kind === "registry" ? source.registryResolution.sha256 : void 0,
|
|
135
|
+
publisher,
|
|
136
|
+
manifestSchemaVersion: manifestBundle.manifest.schemaVersion,
|
|
137
|
+
components: isAppComponentManifestBundle(manifestBundle) ? manifestBundle.components.map((component) => ({
|
|
138
|
+
...component,
|
|
139
|
+
componentDirectory: path.join(installDirectory, component.path),
|
|
140
|
+
manifestPath: path.join(installDirectory, component.path, component.kind === "panel" ? "panel-app.json" : "service-app.json")
|
|
141
|
+
})) : void 0,
|
|
142
|
+
primaryPanelId: isAppComponentManifestBundle(manifestBundle) ? manifestBundle.primaryPanelId : void 0,
|
|
143
|
+
security: isAppComponentManifestBundle(manifestBundle) ? this.manifestService.resolvePlatformSecurity(manifestBundle.manifest) : void 0,
|
|
144
|
+
dataSchemaVersion,
|
|
145
|
+
contentSha256,
|
|
146
|
+
activate: options.activate
|
|
147
|
+
});
|
|
216
148
|
} catch (error) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
149
|
+
await this.integrityService.removeDirectory(installDirectory);
|
|
150
|
+
if (!instanceExisted) await this.instanceStorageService.rollbackNewInstance({
|
|
151
|
+
instance: defaultInstance,
|
|
152
|
+
legacyDataDirectory: existingRecord?.defaultInstance.storage.layout === "legacy" ? existingRecord.dataDirectory : void 0
|
|
153
|
+
});
|
|
222
154
|
throw error;
|
|
223
155
|
}
|
|
224
|
-
|
|
225
|
-
recursive: true,
|
|
226
|
-
force: true
|
|
227
|
-
})));
|
|
156
|
+
const installedVersion = registryRecord.installedVersions[version];
|
|
228
157
|
return {
|
|
229
|
-
appId,
|
|
230
|
-
|
|
231
|
-
|
|
158
|
+
appId: registryRecord.appId,
|
|
159
|
+
name: registryRecord.name,
|
|
160
|
+
version,
|
|
161
|
+
installDirectory,
|
|
162
|
+
dataDirectory: defaultInstance.storage.dataDirectory,
|
|
163
|
+
instance: registryRecord.defaultInstance,
|
|
164
|
+
sourceKind: source.kind,
|
|
165
|
+
sourceRef: source.sourceRef,
|
|
166
|
+
distributionMode: installedVersion?.distributionMode,
|
|
167
|
+
permissions: installedVersion?.permissions ?? {},
|
|
168
|
+
registryUrl: installedVersion?.registryUrl,
|
|
169
|
+
bundleUrl: installedVersion?.bundleUrl,
|
|
170
|
+
sha256: installedVersion?.sha256,
|
|
171
|
+
publisher: installedVersion?.publisher,
|
|
172
|
+
enabled: registryRecord.enabled,
|
|
173
|
+
manifestSchemaVersion: installedVersion?.manifestSchemaVersion ?? 1,
|
|
174
|
+
components: installedVersion?.components,
|
|
175
|
+
primaryPanelId: installedVersion?.primaryPanelId
|
|
232
176
|
};
|
|
233
177
|
};
|
|
178
|
+
update = async (appId, options) => {
|
|
179
|
+
return await this.withAppOperation(appId, async () => await this.lifecycleService.update(appId, options));
|
|
180
|
+
};
|
|
181
|
+
uninstall = async (appId, purgeData) => {
|
|
182
|
+
return await this.withAppOperation(appId, async () => await this.lifecycleService.uninstall(appId, purgeData));
|
|
183
|
+
};
|
|
234
184
|
list = async () => {
|
|
235
185
|
return (await this.registryService.listApps()).map((appRecord) => ({
|
|
236
186
|
appId: appRecord.appId,
|
|
@@ -245,12 +195,74 @@ var AppInstallationService = class {
|
|
|
245
195
|
};
|
|
246
196
|
reconcileFilesystem = async () => {
|
|
247
197
|
await this.appHomeService.ensureBaseDirectories();
|
|
248
|
-
|
|
198
|
+
let appRecords = await this.registryService.listApps();
|
|
199
|
+
for (const appRecord of appRecords) await this.withAppOperation(appRecord.appId, async () => {
|
|
200
|
+
for (const versionRecord of Object.values(appRecord.installedVersions)) {
|
|
201
|
+
if (!await this.integrityService.pathExists(versionRecord.installDirectory)) continue;
|
|
202
|
+
if (!versionRecord.contentSha256) {
|
|
203
|
+
const contentSha256 = await this.integrityService.calculateDigest(versionRecord.installDirectory);
|
|
204
|
+
await this.registryService.setVersionContentDigest(appRecord.appId, versionRecord.version, contentSha256);
|
|
205
|
+
}
|
|
206
|
+
await this.integrityService.protectDirectory(versionRecord.installDirectory);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
appRecords = await this.registryService.listApps();
|
|
249
210
|
const referencedInstallPaths = new Set(appRecords.flatMap((record) => Object.values(record.installedVersions).map((version) => path.resolve(version.installDirectory))));
|
|
250
|
-
const referencedDataPaths = new Set(appRecords.map((record) => path.resolve(record.dataDirectory)));
|
|
251
|
-
const packageDirectories = await this.readDirectories(this.appHomeService.getPackagesDirectory());
|
|
252
|
-
for (const appDirectory of packageDirectories) await this.reconcileGeneratedSiblings(appDirectory, referencedInstallPaths);
|
|
253
|
-
await this.reconcileGeneratedSiblings(this.appHomeService.getDataDirectory(), referencedDataPaths);
|
|
211
|
+
const referencedDataPaths = new Set(appRecords.map((record) => path.resolve(record.defaultInstance.storage.layout === "instance-v1" ? record.defaultInstance.storage.instanceDirectory : record.dataDirectory)));
|
|
212
|
+
const packageDirectories = await this.filesystemService.readDirectories(this.appHomeService.getPackagesDirectory());
|
|
213
|
+
for (const appDirectory of packageDirectories) await this.filesystemService.reconcileGeneratedSiblings(appDirectory, referencedInstallPaths);
|
|
214
|
+
await this.filesystemService.reconcileGeneratedSiblings(this.appHomeService.getDataDirectory(), referencedDataPaths);
|
|
215
|
+
for (const appRecord of appRecords) {
|
|
216
|
+
if (appRecord.defaultInstance.storage.layout !== "instance-v1") continue;
|
|
217
|
+
await this.filesystemService.reconcileGeneratedSiblings(appRecord.defaultInstance.storage.instanceDirectory, new Set([path.resolve(appRecord.defaultInstance.storage.dataDirectory)]));
|
|
218
|
+
}
|
|
219
|
+
for (const appRecord of appRecords) {
|
|
220
|
+
if (appRecord.defaultInstance.storage.layout !== "legacy") continue;
|
|
221
|
+
await this.withAppOperation(appRecord.appId, async () => {
|
|
222
|
+
const currentRecord = await this.registryService.getApp(appRecord.appId);
|
|
223
|
+
if (!currentRecord || currentRecord.defaultInstance.storage.layout !== "legacy") return;
|
|
224
|
+
const activeVersion = currentRecord.installedVersions[currentRecord.activeVersion];
|
|
225
|
+
if (!activeVersion) throw new Error(`已安装应用缺少激活版本:${currentRecord.appId}`);
|
|
226
|
+
const instance = await this.instanceStorageService.materializeDefaultInstance({
|
|
227
|
+
appId: currentRecord.appId,
|
|
228
|
+
publisherId: currentRecord.publisher?.id,
|
|
229
|
+
legacyDataDirectory: currentRecord.dataDirectory,
|
|
230
|
+
dataSchemaVersion: currentRecord.defaultInstance.dataSchemaVersion
|
|
231
|
+
});
|
|
232
|
+
try {
|
|
233
|
+
await this.registryService.upsertInstallation({
|
|
234
|
+
appId: currentRecord.appId,
|
|
235
|
+
name: currentRecord.name,
|
|
236
|
+
description: currentRecord.description,
|
|
237
|
+
version: currentRecord.activeVersion,
|
|
238
|
+
installDirectory: activeVersion.installDirectory,
|
|
239
|
+
defaultInstance: instance,
|
|
240
|
+
sourceKind: activeVersion.sourceKind,
|
|
241
|
+
sourceRef: activeVersion.sourceRef,
|
|
242
|
+
installedAt: activeVersion.installedAt,
|
|
243
|
+
distributionMode: activeVersion.distributionMode,
|
|
244
|
+
permissions: activeVersion.permissions,
|
|
245
|
+
registryUrl: activeVersion.registryUrl,
|
|
246
|
+
bundleUrl: activeVersion.bundleUrl,
|
|
247
|
+
sha256: activeVersion.sha256,
|
|
248
|
+
publisher: currentRecord.publisher ?? activeVersion.publisher,
|
|
249
|
+
manifestSchemaVersion: activeVersion.manifestSchemaVersion,
|
|
250
|
+
components: activeVersion.components,
|
|
251
|
+
primaryPanelId: activeVersion.primaryPanelId,
|
|
252
|
+
security: activeVersion.security,
|
|
253
|
+
dataSchemaVersion: activeVersion.dataSchemaVersion,
|
|
254
|
+
contentSha256: activeVersion.contentSha256,
|
|
255
|
+
enabled: currentRecord.enabled
|
|
256
|
+
});
|
|
257
|
+
} catch (error) {
|
|
258
|
+
await this.instanceStorageService.rollbackNewInstance({
|
|
259
|
+
instance,
|
|
260
|
+
legacyDataDirectory: currentRecord.dataDirectory
|
|
261
|
+
});
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
254
266
|
};
|
|
255
267
|
info = async (appId) => {
|
|
256
268
|
const appRecord = await this.registryService.getApp(appId);
|
|
@@ -263,6 +275,9 @@ var AppInstallationService = class {
|
|
|
263
275
|
activeVersion: appRecord.activeVersion,
|
|
264
276
|
enabled: appRecord.enabled,
|
|
265
277
|
dataDirectory: appRecord.dataDirectory,
|
|
278
|
+
instance: appRecord.defaultInstance,
|
|
279
|
+
storage: appRecord.defaultInstance.storage,
|
|
280
|
+
storageUsage: await this.instanceStorageService.measureUsage(appRecord.defaultInstance.storage),
|
|
266
281
|
installedVersions: installedVersions.map((versionRecord) => ({
|
|
267
282
|
version: versionRecord.version,
|
|
268
283
|
installDirectory: versionRecord.installDirectory,
|
|
@@ -277,41 +292,73 @@ var AppInstallationService = class {
|
|
|
277
292
|
publisher: versionRecord.publisher,
|
|
278
293
|
manifestSchemaVersion: versionRecord.manifestSchemaVersion,
|
|
279
294
|
components: versionRecord.components,
|
|
280
|
-
primaryPanelId: versionRecord.primaryPanelId
|
|
295
|
+
primaryPanelId: versionRecord.primaryPanelId,
|
|
296
|
+
contentSha256: versionRecord.contentSha256
|
|
281
297
|
})),
|
|
282
298
|
grants: appRecord.grants
|
|
283
299
|
};
|
|
284
300
|
};
|
|
285
301
|
setEnabled = async (appId, enabled) => {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
302
|
+
return await this.withAppOperation(appId, async () => {
|
|
303
|
+
const appRecord = await this.registryService.setEnabled(appId, enabled);
|
|
304
|
+
return {
|
|
305
|
+
appId: appRecord.appId,
|
|
306
|
+
activeVersion: appRecord.activeVersion,
|
|
307
|
+
enabled: appRecord.enabled
|
|
308
|
+
};
|
|
309
|
+
});
|
|
292
310
|
};
|
|
293
311
|
rollback = async (appId, version) => {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
312
|
+
return await this.withAppOperation(appId, async () => {
|
|
313
|
+
const currentRecord = await this.registryService.getApp(appId);
|
|
314
|
+
if (!currentRecord) throw new Error(`未找到已安装应用:${appId}`);
|
|
315
|
+
if (currentRecord.activeVersion === version) return {
|
|
316
|
+
appId,
|
|
317
|
+
activeVersion: version,
|
|
318
|
+
previousVersion: version,
|
|
319
|
+
enabled: currentRecord.enabled,
|
|
320
|
+
rolledBack: false
|
|
321
|
+
};
|
|
322
|
+
const targetVersion = currentRecord.installedVersions[version];
|
|
323
|
+
if (!targetVersion) throw new Error(`应用 ${appId} 未安装版本 ${version}。`);
|
|
324
|
+
if (targetVersion.dataSchemaVersion !== currentRecord.defaultInstance.dataSchemaVersion) throw new Error(`应用 ${appId}@${version} 需要数据 schema ${targetVersion.dataSchemaVersion},当前实例为 ${currentRecord.defaultInstance.dataSchemaVersion}。没有可用 checkpoint,已阻止回滚。`);
|
|
325
|
+
await this.assertVersionIntegrity(appId, version);
|
|
326
|
+
const appRecord = await this.registryService.activateVersion(appId, version);
|
|
327
|
+
return {
|
|
328
|
+
appId,
|
|
329
|
+
activeVersion: appRecord.activeVersion,
|
|
330
|
+
previousVersion: currentRecord.activeVersion,
|
|
331
|
+
enabled: appRecord.enabled,
|
|
332
|
+
rolledBack: true
|
|
333
|
+
};
|
|
334
|
+
});
|
|
335
|
+
};
|
|
336
|
+
assertVersionIntegrity = async (appId, version) => {
|
|
337
|
+
const appRecord = await this.registryService.getApp(appId);
|
|
338
|
+
if (!appRecord) throw new Error(`未找到已安装应用:${appId}`);
|
|
339
|
+
const targetVersion = version ?? appRecord.activeVersion;
|
|
340
|
+
const versionRecord = appRecord.installedVersions[targetVersion];
|
|
341
|
+
if (!versionRecord) throw new Error(`应用 ${appId} 未安装版本 ${targetVersion}。`);
|
|
342
|
+
const contentSha256 = await this.integrityService.assertVersion({
|
|
309
343
|
appId,
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
344
|
+
version: targetVersion,
|
|
345
|
+
versionRecord
|
|
346
|
+
});
|
|
347
|
+
if (!versionRecord.contentSha256) {
|
|
348
|
+
await this.registryService.setVersionContentDigest(appId, targetVersion, contentSha256);
|
|
349
|
+
await this.integrityService.protectDirectory(versionRecord.installDirectory);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
withAppOperation = async (appId, operation) => {
|
|
353
|
+
if (!SAFE_APP_ID_PATTERN.test(appId)) throw new Error(`appId 不是安全的 App 操作标识:${appId}`);
|
|
354
|
+
const lockPath = path.resolve(this.appHomeService.getAppOperationLockPath(appId));
|
|
355
|
+
const activeLocks = this.appOperationContext.getStore();
|
|
356
|
+
if (activeLocks?.has(lockPath)) return await operation();
|
|
357
|
+
return await this.fileLockService.withLock(lockPath, async () => {
|
|
358
|
+
const nextLocks = new Set(activeLocks ?? []);
|
|
359
|
+
nextLocks.add(lockPath);
|
|
360
|
+
return await this.appOperationContext.run(nextLocks, operation);
|
|
361
|
+
});
|
|
315
362
|
};
|
|
316
363
|
resolveLaunch = async (appReference, explicitDocumentGrantMap) => {
|
|
317
364
|
const sourceType = await this.installSourceService.detectLocal(appReference, false);
|
|
@@ -323,10 +370,12 @@ var AppInstallationService = class {
|
|
|
323
370
|
if (!appRecord) throw new Error(`未找到应用目录,也未找到已安装应用:${appReference}`);
|
|
324
371
|
const activeVersion = appRecord.installedVersions[appRecord.activeVersion];
|
|
325
372
|
if (!activeVersion) throw new Error(`已安装应用缺少激活版本:${appReference}`);
|
|
373
|
+
await this.assertVersionIntegrity(appRecord.appId, appRecord.activeVersion);
|
|
326
374
|
return {
|
|
327
375
|
appDirectory: activeVersion.installDirectory,
|
|
328
376
|
appId: appRecord.appId,
|
|
329
377
|
dataDirectory: appRecord.dataDirectory,
|
|
378
|
+
storage: appRecord.defaultInstance.storage,
|
|
330
379
|
documentGrantMap: {
|
|
331
380
|
...appRecord.grants,
|
|
332
381
|
...explicitDocumentGrantMap
|
|
@@ -337,73 +386,6 @@ var AppInstallationService = class {
|
|
|
337
386
|
if (!appId || Object.keys(documentGrantMap).length === 0) return;
|
|
338
387
|
await this.registryService.updateGrants(appId, documentGrantMap);
|
|
339
388
|
};
|
|
340
|
-
copyToImmutableInstallDirectory = async (params) => {
|
|
341
|
-
const { appId, appVersion, extractedDirectory, installDirectory } = params;
|
|
342
|
-
if (await this.pathExists(installDirectory)) throw new Error(`应用版本目录已存在,不能覆盖不可变版本:${appId}@${appVersion}`);
|
|
343
|
-
await mkdir(path.dirname(installDirectory), { recursive: true });
|
|
344
|
-
const stagedInstallDirectory = `${installDirectory}.staging-${randomUUID()}`;
|
|
345
|
-
try {
|
|
346
|
-
await cp(extractedDirectory, stagedInstallDirectory, { recursive: true });
|
|
347
|
-
const stagedManifest = await this.manifestService.load(stagedInstallDirectory);
|
|
348
|
-
if (stagedManifest.manifest.id !== appId || stagedManifest.manifest.version !== appVersion) throw new Error("staging manifest 与已验证 bundle 身份不一致。");
|
|
349
|
-
await rename(stagedInstallDirectory, installDirectory);
|
|
350
|
-
} catch (error) {
|
|
351
|
-
await rm(stagedInstallDirectory, {
|
|
352
|
-
recursive: true,
|
|
353
|
-
force: true
|
|
354
|
-
});
|
|
355
|
-
throw error;
|
|
356
|
-
}
|
|
357
|
-
};
|
|
358
|
-
materializeDistribution = async (params) => {
|
|
359
|
-
if (params.distributionMode !== "source") return;
|
|
360
|
-
const manifestBundle = await this.manifestService.load(params.appDirectory);
|
|
361
|
-
if (!isAppStandaloneManifestBundle(manifestBundle)) throw new Error("schema v2 组合包不允许运行安装期 build。");
|
|
362
|
-
if (manifestBundle.manifest.main.kind !== "wasi-http-component") return;
|
|
363
|
-
await this.buildService.build({
|
|
364
|
-
appDirectory: params.appDirectory,
|
|
365
|
-
install: true
|
|
366
|
-
});
|
|
367
|
-
};
|
|
368
|
-
reconcileGeneratedSiblings = async (parentDirectory, referencedPaths) => {
|
|
369
|
-
for (const entry of await this.readDirectories(parentDirectory)) {
|
|
370
|
-
const entryName = path.basename(entry);
|
|
371
|
-
const stagingMarker = ".staging-";
|
|
372
|
-
const uninstallingMarker = ".uninstalling-";
|
|
373
|
-
if (entryName.includes(stagingMarker)) {
|
|
374
|
-
await rm(entry, {
|
|
375
|
-
recursive: true,
|
|
376
|
-
force: true
|
|
377
|
-
});
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
const markerIndex = entryName.indexOf(uninstallingMarker);
|
|
381
|
-
if (markerIndex < 0) continue;
|
|
382
|
-
const originalPath = path.join(parentDirectory, entryName.slice(0, markerIndex));
|
|
383
|
-
if (referencedPaths.has(path.resolve(originalPath)) && !await this.pathExists(originalPath)) await rename(entry, originalPath);
|
|
384
|
-
else await rm(entry, {
|
|
385
|
-
recursive: true,
|
|
386
|
-
force: true
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
|
-
readDirectories = async (directory) => {
|
|
391
|
-
try {
|
|
392
|
-
return (await readdir(directory, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => path.join(directory, entry.name));
|
|
393
|
-
} catch (error) {
|
|
394
|
-
if (this.isMissingFileError(error)) return [];
|
|
395
|
-
throw error;
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
|
-
pathExists = async (targetPath) => {
|
|
399
|
-
try {
|
|
400
|
-
await access(targetPath);
|
|
401
|
-
return true;
|
|
402
|
-
} catch {
|
|
403
|
-
return false;
|
|
404
|
-
}
|
|
405
|
-
};
|
|
406
|
-
isMissingFileError = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
407
389
|
};
|
|
408
390
|
//#endregion
|
|
409
391
|
export { AppInstallationService };
|