@abtnode/core 1.8.65-beta-81d3340c → 1.8.65-beta-5405baf2

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,205 @@
1
+ const { EventEmitter } = require('events');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const get = require('lodash/get');
5
+ const { toBase58 } = require('@ocap/util');
6
+
7
+ const defaultLogger = require('@abtnode/logger')('@abtnode/core:blocklet-downloader');
8
+
9
+ const { forEachBlockletSync, getComponentBundleId } = require('@blocklet/meta/lib/util');
10
+ const validateBlockletEntry = require('@blocklet/meta/lib/entry');
11
+
12
+ const { BlockletSource, BLOCKLET_META_FILE, BLOCKLET_META_FILE_ALT, BLOCKLET_MODES } = require('@blocklet/constant');
13
+
14
+ const { getBundleDir } = require('../../util/blocklet');
15
+ const BundleDownloader = require('./bundle-downloader');
16
+ const { CACHE_KEY } = require('./constants');
17
+
18
+ const isSourceAccessible = (blocklet) =>
19
+ ![BlockletSource.upload, BlockletSource.local, BlockletSource.custom].includes(blocklet.source);
20
+
21
+ const isMetaFileExist = (dir) => {
22
+ const blockletMetaFile = path.join(dir, BLOCKLET_META_FILE);
23
+ const blockletMetaFileAlt = path.join(dir, BLOCKLET_META_FILE_ALT);
24
+
25
+ return fs.existsSync(blockletMetaFile) || fs.existsSync(blockletMetaFileAlt);
26
+ };
27
+
28
+ /**
29
+ * @param {blocklet} component
30
+ * @param {{
31
+ * cachedBundles: Array<{bundleId, integrity}>;
32
+ * }}
33
+ * @returns {boolean}
34
+ */
35
+ const needDownload = (component, { installDir, logger = defaultLogger, cachedBundles = [] } = {}) => {
36
+ if (component.mode === BLOCKLET_MODES.DEVELOPMENT) {
37
+ return false;
38
+ }
39
+
40
+ if (component.source === BlockletSource.local) {
41
+ return false;
42
+ }
43
+
44
+ const bundleId = getComponentBundleId(component);
45
+ const bundleDir = getBundleDir(installDir, component.meta);
46
+
47
+ // check bundle exist
48
+
49
+ if (!isMetaFileExist(bundleDir)) {
50
+ return true;
51
+ }
52
+
53
+ // check bundle broken
54
+ try {
55
+ validateBlockletEntry(bundleDir, component.meta);
56
+ } catch (err) {
57
+ logger.error('bundle is broken', { bundleId, msg: err.message });
58
+ return true;
59
+ }
60
+
61
+ if (!isSourceAccessible(component)) {
62
+ return false;
63
+ }
64
+
65
+ // check integrity
66
+
67
+ const cachedBundle = cachedBundles.find((x) => x.bundleId === bundleId);
68
+
69
+ if (!cachedBundle) {
70
+ return true;
71
+ }
72
+
73
+ const integrity = get(component, 'meta.dist.integrity');
74
+ if (toBase58(integrity) === cachedBundle.integrity) {
75
+ return false;
76
+ }
77
+
78
+ logger.error(`find duplicate bundle with different integrity when downloading ${component.meta.title}`, {
79
+ bundleId,
80
+ });
81
+ return true;
82
+ };
83
+
84
+ class BlockletDownloader extends EventEmitter {
85
+ /**
86
+ * @param {{
87
+ * cache: {
88
+ * set: (key: string, value: any) => Promise
89
+ * get: (key: string) => Promise<any>
90
+ * }
91
+ * }}
92
+ */
93
+ constructor({ installDir, downloadDir, cache, logger = defaultLogger }) {
94
+ super();
95
+
96
+ this.installDir = installDir;
97
+ this.cache = cache;
98
+ this.logger = logger;
99
+
100
+ this.bundleDownloader = new BundleDownloader({
101
+ installDir,
102
+ downloadDir,
103
+ cache,
104
+ });
105
+ }
106
+
107
+ /**
108
+ * @param {{}} blocklet
109
+ * @param {{}} [context={}]
110
+ * @return {*}
111
+ */
112
+ async download(blocklet, context = {}) {
113
+ const {
114
+ meta: { name, did },
115
+ } = blocklet;
116
+
117
+ this.logger.info('Download Blocklet', { name, did });
118
+
119
+ const { preDownload = () => {}, postDownload = () => {} } = context;
120
+
121
+ const { downloadComponentIds, downloadList } = await this.getDownloadList({
122
+ blocklet,
123
+ });
124
+
125
+ await preDownload({ downloadList, downloadComponentIds });
126
+
127
+ try {
128
+ this.logger.info('Start Download Blocklet', {
129
+ name,
130
+ did,
131
+ bundles: downloadList.map((x) => get(x, 'dist.tarball')),
132
+ });
133
+ const tasks = [];
134
+ for (const meta of downloadList) {
135
+ const url = meta.dist.tarball;
136
+ tasks.push(this.bundleDownloader.download(meta, did, url, context));
137
+ }
138
+ const results = await Promise.all(tasks);
139
+
140
+ await postDownload({ downloadList, downloadComponentIds });
141
+
142
+ if (results.find((x) => x.isCancelled)) {
143
+ return { isCancelled: true };
144
+ }
145
+ } catch (error) {
146
+ this.logger.error('Download blocklet failed', { did, name, error });
147
+ await this.bundleDownloader.cancelDownload(blocklet.meta.did);
148
+ throw error;
149
+ }
150
+
151
+ return { isCancelled: false };
152
+ }
153
+
154
+ async cancelDownload(did) {
155
+ return this.bundleDownloader.cancelDownload(did);
156
+ }
157
+
158
+ /**
159
+ * @param {{
160
+ * blocklet;
161
+ * }}
162
+ * @returns {{
163
+ * downloadList: Array<blockletMeta>;
164
+ * downloadComponentIds: Array<string>;
165
+ * }}
166
+ */
167
+ async getDownloadList({ blocklet }) {
168
+ const downloadComponentIds = [];
169
+ const metas = {};
170
+
171
+ const cachedBundles = (await this.cache.get(CACHE_KEY)) || [];
172
+
173
+ forEachBlockletSync(blocklet, (component, { id: componentId }) => {
174
+ const bundleId = getComponentBundleId(component);
175
+
176
+ if (metas[bundleId]) {
177
+ this.logger.info(`skip download duplicate bundle ${bundleId}`);
178
+ return;
179
+ }
180
+
181
+ const needComponentDownload = needDownload(component, { installDir: this.installDir, cachedBundles });
182
+ if (!needComponentDownload) {
183
+ this.logger.info(`skip download existed bundle ${bundleId}`, { source: component.source });
184
+ return;
185
+ }
186
+
187
+ if (!isSourceAccessible(component)) {
188
+ // should not throw error
189
+ // broken bundle should not block blocklet installing
190
+ // broken bundle should only block blocklet starting
191
+ this.logger.error(`Component bundle is broken and can not be recovered: ${bundleId}`);
192
+ return;
193
+ }
194
+
195
+ metas[bundleId] = component.meta;
196
+ downloadComponentIds.push(componentId);
197
+ });
198
+
199
+ const downloadList = Object.values(metas);
200
+
201
+ return { downloadList, downloadComponentIds };
202
+ }
203
+ }
204
+
205
+ module.exports = BlockletDownloader;
@@ -0,0 +1,267 @@
1
+ const { EventEmitter } = require('events');
2
+ const fs = require('fs-extra');
3
+ const { fileURLToPath } = require('url');
4
+ const path = require('path');
5
+ const cloneDeep = require('lodash/cloneDeep');
6
+ const { toBase58 } = require('@ocap/util');
7
+
8
+ const defaultLogger = require('@abtnode/logger')('@abtnode/core:bundle-downloader');
9
+ const downloadFile = require('@abtnode/util/lib/download-file');
10
+ const Lock = require('@abtnode/util/lib/lock');
11
+
12
+ const { getComponentBundleId } = require('@blocklet/meta/lib/util');
13
+
14
+ const { verifyIntegrity } = require('../../util/blocklet');
15
+ const { resolveDownload } = require('./resolve-download');
16
+ const { CACHE_KEY } = require('./constants');
17
+
18
+ class BundleDownloader extends EventEmitter {
19
+ /**
20
+ * @param {{
21
+ * cache: {
22
+ * set: (key: string, value: any) => Promise
23
+ * get: (key: string) => Promise<any>
24
+ * }
25
+ * }}
26
+ */
27
+ constructor({ installDir, downloadDir, cache, logger = defaultLogger }) {
28
+ super();
29
+
30
+ this.installDir = installDir;
31
+ this.downloadDir = downloadDir;
32
+ this.cache = cache;
33
+ this.logger = logger;
34
+
35
+ /**
36
+ * { did: Map({ <childDid>: <downloadFile.cancelCtrl> }) }
37
+ */
38
+ this.downloadCtrls = {};
39
+ /**
40
+ * { [download-did-version]: Lock }
41
+ */
42
+ this.downloadLocks = {};
43
+
44
+ this.cacheTarBallLock = new Lock();
45
+ }
46
+
47
+ /**
48
+ *
49
+ *
50
+ * @param {*} meta
51
+ * @param {*} rootDid
52
+ * @param {*} url
53
+ * @param {{}} [context={}]
54
+ * @return {*}
55
+ * @memberof BlockletManager
56
+ */
57
+ async download(meta, rootDid, url, context = {}) {
58
+ const { bundleName: name, bundleDid: did, version, dist = {} } = meta;
59
+ const { tarball, integrity } = dist;
60
+
61
+ const lockName = `download-${did}-${version}`;
62
+ let lock = this.downloadLocks[lockName];
63
+ if (!lock) {
64
+ lock = new Lock(lockName);
65
+ this.downloadLocks[lockName] = lock;
66
+ }
67
+
68
+ try {
69
+ await lock.acquire();
70
+ this.logger.info('downloaded blocklet for installing', { name, version, tarball, integrity });
71
+ const cwd = path.join(this.downloadDir, 'download', name);
72
+ await fs.ensureDir(cwd);
73
+ this.logger.info('start download blocklet', { name, version, cwd, tarball, integrity });
74
+ const tarballPath = await this._downloadTarball({
75
+ name,
76
+ did,
77
+ version,
78
+ cwd,
79
+ tarball,
80
+ integrity,
81
+ verify: true,
82
+ ctrlStore: this.downloadCtrls,
83
+ rootDid,
84
+ url,
85
+ context,
86
+ });
87
+ this.logger.info('downloaded blocklet tar file', { name, version, tarballPath });
88
+ if (tarballPath === downloadFile.CANCEL) {
89
+ lock.release();
90
+ return { isCancelled: true };
91
+ }
92
+
93
+ // resolve tarball and mv tarball to cache after resolved
94
+ await resolveDownload(tarballPath, this.installDir, { removeTarFile: false });
95
+ await this._addCacheTarFile(tarballPath, integrity, getComponentBundleId({ meta }));
96
+
97
+ this.logger.info('resolved blocklet tar file to install dir', { name, version });
98
+ lock.release();
99
+ return { isCancelled: false };
100
+ } catch (error) {
101
+ lock.release();
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ // eslint-disable-next-line no-unused-vars
107
+ async cancelDownload(rootDid) {
108
+ if (this.downloadCtrls[rootDid]) {
109
+ for (const cancelCtrl of this.downloadCtrls[rootDid].values()) {
110
+ cancelCtrl.cancel();
111
+ }
112
+ }
113
+ }
114
+
115
+ /**
116
+ *
117
+ *
118
+ * @param {{
119
+ * url: string,
120
+ * cwd: string,
121
+ * tarball: string,
122
+ * did: string,
123
+ * integrity: string,
124
+ * verify: boolean = true,
125
+ * ctrlStore: {},
126
+ * rootDid: string,
127
+ * context: {} = {},
128
+ * }} { url, cwd, tarball, did, integrity, verify = true, ctrlStore = {}, rootDid, context = {} }
129
+ * @return {*}
130
+ * @memberof BlockletManager
131
+ */
132
+ async _downloadTarball({ url, cwd, tarball, did, integrity, verify = true, ctrlStore = {}, rootDid, context = {} }) {
133
+ fs.mkdirSync(cwd, { recursive: true });
134
+
135
+ const tarballName = url.split('/').slice(-1)[0];
136
+
137
+ const tarballPath = path.join(cwd, tarballName);
138
+
139
+ const { protocol } = new URL(url);
140
+
141
+ const cachedTarFile = await this._getCachedTarFile(integrity);
142
+ if (cachedTarFile) {
143
+ this.logger.info('found cache tarFile', { did, tarballName, integrity });
144
+
145
+ await this.cacheTarBallLock.acquire();
146
+ try {
147
+ await fs.move(cachedTarFile, tarballPath, { overwrite: true });
148
+ } catch (error) {
149
+ await this.cacheTarBallLock.release();
150
+ this.logger.error('move cache tar file failed', { cachedTarFile, tarballPath, error });
151
+ throw error;
152
+ }
153
+ await this.cacheTarBallLock.release();
154
+ } else if (protocol.startsWith('file')) {
155
+ await fs.copy(decodeURIComponent(fileURLToPath(url)), tarballPath);
156
+ } else {
157
+ const cancelCtrl = new downloadFile.CancelCtrl();
158
+
159
+ if (!ctrlStore[rootDid]) {
160
+ ctrlStore[rootDid] = new Map();
161
+ }
162
+ ctrlStore[rootDid].set(did, cancelCtrl);
163
+
164
+ const headers = context.headers ? cloneDeep(context.headers) : {};
165
+ const exist = (context.downloadTokenList || []).find((x) => x.did === did);
166
+ if (exist) {
167
+ headers['x-download-token'] = exist.token;
168
+ }
169
+
170
+ await downloadFile(url, path.join(cwd, tarballName), { cancelCtrl }, { ...context, headers });
171
+
172
+ if (ctrlStore[rootDid]) {
173
+ ctrlStore[rootDid].delete(did);
174
+ if (!ctrlStore[rootDid].size) {
175
+ delete ctrlStore[rootDid];
176
+ }
177
+ }
178
+
179
+ if (cancelCtrl.isCancelled) {
180
+ return downloadFile.CANCEL;
181
+ }
182
+ }
183
+
184
+ if (verify) {
185
+ try {
186
+ await verifyIntegrity({ file: tarballPath, integrity });
187
+ } catch (error) {
188
+ this.logger.error('verify integrity error', { error, tarball, url });
189
+ throw new Error(`${tarball} integrity check failed.`);
190
+ }
191
+ }
192
+
193
+ return tarballPath;
194
+ }
195
+
196
+ /**
197
+ * use LRU algorithm
198
+ */
199
+ async _addCacheTarFile(tarballPath, integrity, bundleId) {
200
+ // eslint-disable-next-line no-param-reassign
201
+ integrity = toBase58(integrity);
202
+
203
+ // move tarball to cache dir
204
+ const cwd = path.join(this.downloadDir, 'download-cache');
205
+ const cachePath = path.join(cwd, `${integrity}.tar.gz`);
206
+ await fs.ensureDir(cwd);
207
+ await fs.move(tarballPath, cachePath, { overwrite: true });
208
+
209
+ const cacheList = (await this.cache.get(CACHE_KEY)) || [];
210
+ const exist = cacheList.find((x) => x.bundleId === bundleId && x.integrity === integrity);
211
+
212
+ // update
213
+ if (exist) {
214
+ this.logger.info('update cache tarFile', { base58: integrity });
215
+ exist.accessAt = Date.now();
216
+ await this.cache.set(CACHE_KEY, cacheList);
217
+ return;
218
+ }
219
+
220
+ // add
221
+ cacheList.push({ integrity, accessAt: Date.now(), bundleId });
222
+ if (cacheList.length > 50) {
223
+ // find and remove
224
+ let minIndex = 0;
225
+ let min = cacheList[0];
226
+ cacheList.forEach((x, i) => {
227
+ if (x.accessAt < min.accessAt) {
228
+ minIndex = i;
229
+ min = x;
230
+ }
231
+ });
232
+
233
+ cacheList.splice(minIndex, 1);
234
+
235
+ const removeFile = path.join(cwd, `${min.integrity}.tar.gz`);
236
+ await this.cacheTarBallLock.acquire();
237
+ try {
238
+ await fs.remove(removeFile);
239
+ } catch (error) {
240
+ this.logger.error('remove cache tar file failed', { file: removeFile, error });
241
+ }
242
+ await this.cacheTarBallLock.release();
243
+
244
+ this.logger.info('remove cache tarFile', { base58: min.integrity });
245
+ }
246
+ this.logger.info('add cache tarFile', { base58: integrity });
247
+
248
+ // update
249
+ await this.cache.set(CACHE_KEY, cacheList);
250
+ }
251
+
252
+ async _getCachedTarFile(integrity) {
253
+ // eslint-disable-next-line no-param-reassign
254
+ integrity = toBase58(integrity);
255
+
256
+ const cwd = path.join(this.downloadDir, 'download-cache');
257
+ const cachePath = path.join(cwd, `${integrity}.tar.gz`);
258
+
259
+ if (fs.existsSync(cachePath)) {
260
+ return cachePath;
261
+ }
262
+
263
+ return null;
264
+ }
265
+ }
266
+
267
+ module.exports = BundleDownloader;
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ CACHE_KEY: 'blocklet:manager:downloadCache',
3
+ };
@@ -0,0 +1,168 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+
4
+ const validateBlockletEntry = require('@blocklet/meta/lib/entry');
5
+ const { BLOCKLET_BUNDLE_FOLDER } = require('@blocklet/constant');
6
+
7
+ const defaultLogger = require('@abtnode/logger')('@abtnode/core:resolve-download');
8
+
9
+ const { getBlockletMeta } = require('../../util');
10
+
11
+ const { ensureBlockletExpanded, expandTarball, getBundleDir } = require('../../util/blocklet');
12
+
13
+ const asyncFs = fs.promises;
14
+
15
+ /**
16
+ * decompress file, format dir and move to installDir
17
+ * @param {string} src file
18
+ * @param {{
19
+ * cwd: string;
20
+ * removeTarFile: boolean;
21
+ * originalMeta; // for verification
22
+ * logger;
23
+ * }} option
24
+ */
25
+ const resolveDownload = async (
26
+ tarFile,
27
+ dist,
28
+ { cwd = '/', removeTarFile = true, logger = defaultLogger, originalMeta } = {}
29
+ ) => {
30
+ // eslint-disable-next-line no-param-reassign
31
+ tarFile = path.join(cwd, tarFile);
32
+ // eslint-disable-next-line no-param-reassign
33
+ dist = path.join(cwd, dist);
34
+
35
+ const downloadDir = path.join(path.dirname(tarFile), path.basename(tarFile, path.extname(tarFile)));
36
+ const tmp = `${downloadDir}-tmp`;
37
+ try {
38
+ await expandTarball({ source: tarFile, dest: tmp, strip: 0 });
39
+ } catch (error) {
40
+ logger.error('expand blocklet tar file error', { error });
41
+ throw error;
42
+ } finally {
43
+ if (removeTarFile) {
44
+ fs.removeSync(tarFile);
45
+ }
46
+ }
47
+ let installDir;
48
+ let meta;
49
+ try {
50
+ // resolve dir
51
+ let dir = tmp;
52
+ const files = await asyncFs.readdir(dir);
53
+ if (files.includes('package')) {
54
+ dir = path.join(tmp, 'package');
55
+ } else if (files.length === 1) {
56
+ const d = path.join(dir, files[0]);
57
+ if ((await asyncFs.stat(d)).isDirectory()) {
58
+ dir = d;
59
+ }
60
+ }
61
+
62
+ if (fs.existsSync(path.join(dir, BLOCKLET_BUNDLE_FOLDER))) {
63
+ dir = path.join(dir, BLOCKLET_BUNDLE_FOLDER);
64
+ }
65
+
66
+ logger.info('Move downloadDir to installDir', { downloadDir });
67
+ await fs.move(dir, downloadDir, { overwrite: true });
68
+ fs.removeSync(tmp);
69
+
70
+ meta = getBlockletMeta(downloadDir, { ensureMain: true });
71
+ const { did, name, version } = meta;
72
+
73
+ // validate
74
+ if (originalMeta && (originalMeta.did !== did || originalMeta.name !== name || originalMeta.version !== version)) {
75
+ logger.error('Meta has differences', { originalMeta, meta });
76
+ throw new Error('There are differences between the meta from tarball file and the original meta');
77
+ }
78
+ await validateBlockletEntry(downloadDir, meta);
79
+
80
+ await ensureBlockletExpanded(meta, downloadDir);
81
+
82
+ installDir = getBundleDir(dist, meta);
83
+ if (fs.existsSync(installDir)) {
84
+ fs.removeSync(installDir);
85
+ logger.info('cleanup blocklet upgrade dir', { name, version, installDir });
86
+ }
87
+
88
+ fs.mkdirSync(installDir, { recursive: true });
89
+
90
+ await fs.move(downloadDir, installDir, { overwrite: true });
91
+ } catch (error) {
92
+ fs.removeSync(downloadDir);
93
+ fs.removeSync(tmp);
94
+ throw error;
95
+ }
96
+
97
+ return { meta, installDir };
98
+ };
99
+
100
+ const resolveDiffDownload = async (
101
+ tarFile,
102
+ dist,
103
+ { meta: oldMeta, deleteSet, cwd = '/', logger = defaultLogger } = {}
104
+ ) => {
105
+ // eslint-disable-next-line no-param-reassign
106
+ tarFile = path.join(cwd, tarFile);
107
+ // eslint-disable-next-line no-param-reassign
108
+ dist = path.join(cwd, dist);
109
+
110
+ logger.info('Resolve diff download', { tarFile, cwd });
111
+ const downloadDir = path.join(path.dirname(tarFile), path.basename(tarFile, path.extname(tarFile)));
112
+ const diffDir = `${downloadDir}-diff`;
113
+ try {
114
+ await expandTarball({ source: tarFile, dest: diffDir, strip: 0 });
115
+ fs.removeSync(tarFile);
116
+ } catch (error) {
117
+ fs.removeSync(tarFile);
118
+ logger.error('expand blocklet tar file error', { error });
119
+ throw error;
120
+ }
121
+ logger.info('Copy installDir to downloadDir', { installDir: dist, downloadDir });
122
+ await fs.copy(getBundleDir(dist, oldMeta), downloadDir);
123
+ try {
124
+ // delete
125
+ logger.info('Delete files from downloadDir', { fileNum: deleteSet.length });
126
+ // eslint-disable-next-line no-restricted-syntax
127
+ for (const file of deleteSet) {
128
+ /* eslint-disable no-await-in-loop */
129
+ await fs.remove(path.join(downloadDir, file));
130
+ }
131
+ // walk & cover
132
+ logger.info('Move files from diffDir to downloadDir', { diffDir, downloadDir });
133
+ const walkDiff = async (dir) => {
134
+ const files = await asyncFs.readdir(dir);
135
+ // eslint-disable-next-line no-restricted-syntax
136
+ for (const file of files) {
137
+ const p = path.join(dir, file);
138
+ const stat = await asyncFs.stat(p);
139
+ if (stat.isDirectory()) {
140
+ await walkDiff(p);
141
+ } else if (stat.isFile()) {
142
+ await fs.move(p, path.join(downloadDir, path.relative(diffDir, p)), { overwrite: true });
143
+ }
144
+ }
145
+ };
146
+ await walkDiff(diffDir);
147
+ fs.removeSync(diffDir);
148
+ const meta = getBlockletMeta(downloadDir, { ensureMain: true });
149
+
150
+ await ensureBlockletExpanded(meta, downloadDir);
151
+
152
+ // move to installDir
153
+ const bundleDir = getBundleDir(dist, meta);
154
+ logger.info('Move downloadDir to installDir', { downloadDir, bundleDir });
155
+ await fs.move(downloadDir, bundleDir, { overwrite: true });
156
+
157
+ return { meta, installDir: bundleDir };
158
+ } catch (error) {
159
+ fs.removeSync(downloadDir);
160
+ fs.removeSync(diffDir);
161
+ throw error;
162
+ }
163
+ };
164
+
165
+ module.exports = {
166
+ resolveDownload,
167
+ resolveDiffDownload,
168
+ };